From f0703adff61dc4259540d500417779b3e1a58af9 Mon Sep 17 00:00:00 2001 From: Max Howell Date: Tue, 7 Jan 2025 23:04:21 -0500 Subject: [PATCH] `dev integrate` and `dev deintegrate` --- README.md | 21 +++++++-- app.ts | 8 ++++ deno.lock | 1 + src/integrate.ts | 117 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 src/integrate.ts diff --git a/README.md b/README.md index 105092f..0fb7ca6 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,9 @@ packages you need for different projects as you navigate in your shell. ## Installation ```sh -echo 'eval "$(pkgx dev --shellcode)"' >> ~/.zshrc +pkgx dev integrate ``` -We support bashlike shells (adapt the rc file above). Fish support is welcome, -but I don’t understand Fish so please PR! - > [!NOTE] > > `pkgx` is a required dependency. @@ -20,6 +17,22 @@ but I don’t understand Fish so please PR! > brew install pkgxdev/made/pkgx || sh <(curl https://pkgx.sh) > ``` +> `pkgx dev integrate` looks for and edits known `shell.rc` files adding one +> line: +> +> ```sh +> eval "$(pkgx dev --shellcode)" +> ``` +> +> If you don’t trust us (good on you), then do a dry run first: +> +> ```sh +> pkgx dev integrate --dry-run +> ``` + +> We support **Bash** and **Zsh**. We would love to support more shells. PRs +> very welcome. + > [!TIP] > If you like, preview the shellcode: `pkgx dev --shellcode`. diff --git a/app.ts b/app.ts index e3f5dc7..a5f2350 100755 --- a/app.ts +++ b/app.ts @@ -8,6 +8,7 @@ import shellcode from "./src/shellcode().ts"; import sniff from "./src/sniff.ts"; import shell_escape from "./src/shell-escape.ts"; import app_version from "./src/app-version.ts"; +import integrate from "./src/integrate.ts"; switch (Deno.args[0]) { case "--help": { @@ -25,6 +26,13 @@ switch (Deno.args[0]) { console.log(`dev ${app_version}`); Deno.exit(0); break; // deno lint insists + case "integrate": + await integrate("install", { dryrun: Deno.args[1] == "--dry-run" }); + Deno.exit(0); + break; + case "deintegrate": + await integrate("uninstall", { dryrun: Deno.args[1] == "--dry-run" }); + Deno.exit(0); } const snuff = await sniff(Path.cwd()); diff --git a/deno.lock b/deno.lock index 088394b..4868c86 100644 --- a/deno.lock +++ b/deno.lock @@ -5,6 +5,7 @@ "jsr:@std/crypto@1": "1.0.3", "jsr:@std/encoding@1": "1.0.5", "jsr:@std/fs@1": "1.0.8", + "jsr:@std/io@*": "0.225.0", "jsr:@std/io@0.225": "0.225.0", "jsr:@std/json@1": "1.0.0", "jsr:@std/jsonc@*": "1.0.1", diff --git a/src/integrate.ts b/src/integrate.ts new file mode 100644 index 0000000..7ca6818 --- /dev/null +++ b/src/integrate.ts @@ -0,0 +1,117 @@ +import readLines from "libpkgx/utils/read-lines.ts"; +import { readAll, writeAll } from "jsr:@std/io"; +import { Path, utils } from "libpkgx"; +const { flatmap } = utils; + +export default async function ( + op: "install" | "uninstall", + { dryrun }: { dryrun: boolean }, +) { + let opd_at_least_once = false; + const encode = ((e) => e.encode.bind(e))(new TextEncoder()); + + const fopts = { read: true, ...dryrun ? {} : { write: true, create: true } }; + + here: for (const [file, line] of shells()) { + const fd = await Deno.open(file.string, fopts); + try { + let pos = 0; + for await (const readline of readLines(fd)) { + if (readline.trim().endsWith("# https://github.com/pkgxdev/dev")) { + if (op == "install") { + console.error("hook already integrated:", file); + continue here; + } else if (op == "uninstall") { + // we have to seek because readLines is buffered and thus the seek pos is probs already at the file end + await fd.seek(pos + readline.length + 1, Deno.SeekMode.Start); + const rest = await readAll(fd); + + if (!dryrun) await fd.truncate(pos); // deno has no way I can find to truncate from the current seek position + await fd.seek(pos, Deno.SeekMode.Start); + if (!dryrun) await writeAll(fd, rest); + + opd_at_least_once = true; + console.error("removed hook:", file); + + continue here; + } + } + + pos += readline.length + 1; // the +1 is because readLines() truncates it + } + + if (op == "install") { + const byte = new Uint8Array(1); + if (pos) { + await fd.seek(0, Deno.SeekMode.End); // potentially the above didn't reach the end + while (true && pos > 0) { + await fd.seek(-1, Deno.SeekMode.Current); + await fd.read(byte); + if (byte[0] != 10) break; + await fd.seek(-1, Deno.SeekMode.Current); + pos -= 1; + } + + if (!dryrun) { + await writeAll( + fd, + encode(`\n\n${line} # https://github.com/pkgxdev/dev\n`), + ); + } + } + opd_at_least_once = true; + console.error(`${file} << \`${line}\``); + } + } finally { + fd.close(); + } + } + if (dryrun && opd_at_least_once) { + console.error( + "%cthis was a dry-run. %cnothing was changed.", + "color: #5f5fff", + "color: initial", + ); + } else {switch (op) { + case "uninstall": + if (!opd_at_least_once) { + console.error("nothing to deintegrate found"); + } + break; + case "install": + if (opd_at_least_once) { + console.log( + "now %crestart your terminal%c for `dev` hooks to take effect", + "color: #5f5fff", + "color: initial", + ); + } + }} +} + +function shells(): [Path, string][] { + const eval_ln = 'eval "$(pkgx dev --shellcode)"'; + + const zdotdir = flatmap(Deno.env.get("ZDOTDIR"), Path.abs) ?? Path.home(); + const zshpair: [Path, string] = [zdotdir.join(".zshrc"), eval_ln]; + + const candidates: [Path, string][] = [ + zshpair, + [Path.home().join(".bashrc"), eval_ln], + [Path.home().join(".bash_profile"), eval_ln], + ]; + + const viable_candidates = candidates.filter(([file]) => file.exists()); + + if (viable_candidates.length == 0) { + if (Deno.build.os == "darwin") { + /// macOS has no .zshrc by default and we want mac users to get a just works experience + return [zshpair]; + } else { + console.error("no `.shellrc` files found"); + Deno.exit(1); + } + } + + return viable_candidates; +}