From f449a415e71ed13900d24e81ba5e62d421c8ca2a Mon Sep 17 00:00:00 2001 From: Ryoya Tamura Date: Sat, 18 Jul 2026 21:16:59 +0900 Subject: [PATCH 1/2] feat: use commander for CLI parsing The CLI parsed argv by hand and had no version flag, so `ghostq -v` printed nothing. Adopt commander to get `-v`/`--version`, auto-generated `--help` for the root command and every subcommand, and variadic positional handling for `adopt ` without manually filtering rawArgs. commander is the sole runtime dependency and is bundled into the compiled binary, so the distributed artifact still needs no external runtime. Docs that claimed zero runtime dependencies are updated to reflect this. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 6 ++- DEVELOPMENT.md | 2 +- README.md | 3 ++ bun.lock | 5 +++ package.json | 21 +++++---- src/index.ts | 113 +++++++++++++++++++++++++++---------------------- 6 files changed, 88 insertions(+), 62 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4d11045..3f10622 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,8 +49,10 @@ The e2e suite encodes these invariants; a change that makes ## Conventions -- Runtime dependencies: none, and keep it that way. git is invoked via - `spawnSync`. +- Runtime dependencies: only `commander`, for CLI argument parsing in + `src/index.ts`. It is bundled into the compiled binary, so the distributed + artifact still needs no external runtime. Keep the dependency list minimal — + don't add more without good reason. git is invoked via `spawnSync`. - Only `src/index.ts` prints; other modules return data structures. - Where information belongs: code says How, tests say What (spec-style test names), commit bodies say Why, code comments say only Why-not (why the diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index afde773..6836ea5 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -11,7 +11,7 @@ and the test suite drives the real git on your machine. ## Commands ```sh -bun install # dependencies (dev-only; the tool itself has none) +bun install # dependencies (commander for CLI parsing, bundled into the binary) bun test # unit + end-to-end suite bun run typecheck # tsc --noEmit bun run build # bun build --compile → dist/ghostq (single binary) diff --git a/README.md b/README.md index e247560..656731a 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,9 @@ ghostq status [path] show link states and warnings without changing anything ghostq prune [path] remove dangling ghostq-managed links (idempotent) ghostq root print the overlay root ghostq uninstall remove the hook wiring + +ghostq -v, --version print the version +ghostq -h, --help show usage (works on any subcommand too) ``` ### Adopting an existing file diff --git a/bun.lock b/bun.lock index bda7d77..b8c3af7 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "@simochee/ghostq", + "dependencies": { + "commander": "^15.0.0", + }, "devDependencies": { "@tsconfig/bun": "^1.0.10", "@types/bun": "^1.2.0", @@ -60,6 +63,8 @@ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], diff --git a/package.json b/package.json index 48f8665..0252f3b 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,30 @@ { "name": "@simochee/ghostq", "version": "1.1.0", - "description": "Restore your gitignored per-repo files automatically on every clone and worktree via a git hook", - "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/simochee/ghostq.git" }, - "type": "module", - "engines": { - "bun": "1.3" + "devDependencies": { + "@tsconfig/bun": "^1.0.10", + "@types/bun": "^1.2.0", + "typescript": "^7" }, "bin": { "ghostq": "./src/index.ts" }, + "description": "Restore your gitignored per-repo files automatically on every clone and worktree via a git hook", + "engines": { + "bun": "1.3" + }, + "license": "MIT", "scripts": { "build": "bun build --compile ./src/index.ts --outfile dist/ghostq", "test": "bun test", "typecheck": "tsc --noEmit" }, - "devDependencies": { - "@tsconfig/bun": "^1.0.10", - "@types/bun": "^1.2.0", - "typescript": "^7" + "type": "module", + "dependencies": { + "commander": "^15.0.0" } } diff --git a/src/index.ts b/src/index.ts index a088063..018faea 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,24 +1,12 @@ #!/usr/bin/env bun +import { Command } from "commander"; +import pkg from "../package.json" with { type: "json" }; import { type AdoptOutcome, adoptFiles } from "./adopt.ts"; -import { applyFiles, inspectFiles, pruneFiles, type FileState } from "./apply.ts"; +import { applyFiles, type FileState, inspectFiles, pruneFiles } from "./apply.ts"; import { resolveContext } from "./context.ts"; import { install, uninstall } from "./install.ts"; import { overlayRoot } from "./paths.ts"; -const USAGE = `ghostq — re-link personal, gitignored, per-repo files on clone / worktree add - -Usage: - ghostq install install the post-checkout hook globally (init.templateDir) - ghostq apply [path] link overlay files into the checkout (idempotent) - ghostq adopt ... move existing gitignored files into the overlay and link them - ghostq status [path] show link states and warnings without changing anything - ghostq prune [path] remove dangling ghostq-managed links (idempotent) - ghostq root print the overlay root - ghostq uninstall remove the hook wiring - -Overlay root: $GHOSTQ_ROOT or \${XDG_CONFIG_HOME:-~/.config}/ghostq/overlay, -laid out as //// mirroring each repo's remote URL.`; - async function apply(path: string): Promise { const res = await resolveContext(path); if (!res.ok) { @@ -127,39 +115,64 @@ async function status(path: string): Promise { return 0; } -async function main(): Promise { - const [cmd, ...rest] = process.argv.slice(2); - switch (cmd) { - case "install": - return install(); - case "uninstall": - return uninstall(); - case "apply": - return apply(rest[0] ?? process.cwd()); - case "adopt": - if (rest.length === 0) { - console.error("ghostq: adopt requires at least one file"); - return 1; - } - return adopt(rest); - case "status": - return status(rest[0] ?? process.cwd()); - case "prune": - return prune(rest[0] ?? process.cwd()); - case "root": - console.log(overlayRoot()); - return 0; - case undefined: - case "help": - case "-h": - case "--help": - console.log(USAGE); - return cmd === undefined ? 1 : 0; - default: - console.error(`ghostq: unknown command: ${cmd}`); - console.error(USAGE); - return 1; - } -} +const program = new Command(); + +program + .name("ghostq") + .description("re-link personal, gitignored, per-repo files on clone / worktree add") + .version(pkg.version, "-v, --version", "print the version"); + +program + .command("install") + .description("install the post-checkout hook globally (init.templateDir)") + .action(async () => { + process.exit(await install()); + }); + +program + .command("uninstall") + .description("remove the hook wiring") + .action(async () => { + process.exit(await uninstall()); + }); + +program + .command("apply") + .description("link overlay files into the checkout (idempotent)") + .argument("[path]", "repo path (defaults to cwd)") + .action(async (path?: string) => { + process.exit(await apply(path ?? process.cwd())); + }); + +program + .command("adopt") + .description("move existing gitignored files into the overlay and link them") + .argument("", "one or more files to adopt") + .action(async (files: string[]) => { + process.exit(await adopt(files)); + }); + +program + .command("status") + .description("show link states and warnings without changing anything") + .argument("[path]", "repo path (defaults to cwd)") + .action(async (path?: string) => { + process.exit(await status(path ?? process.cwd())); + }); + +program + .command("prune") + .description("remove dangling ghostq-managed links (idempotent)") + .argument("[path]", "repo path (defaults to cwd)") + .action(async (path?: string) => { + process.exit(await prune(path ?? process.cwd())); + }); + +program + .command("root") + .description("print the overlay root") + .action(() => { + console.log(overlayRoot()); + }); -process.exit(await main()); +await program.parseAsync(); From 5078f8727221dc00a8b54b0acb968e4943b0f921 Mon Sep 17 00:00:00 2001 From: Ryoya Tamura Date: Sat, 18 Jul 2026 21:20:44 +0900 Subject: [PATCH 2/2] docs: paste `ghostq --help` output verbatim into Commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hand-maintained command list with the actual `--help` output so it can't drift, and add a comment reminding to re-paste it when the CLI changes. Drop the version/help option lines — they don't need documenting. Co-Authored-By: Claude Opus 4.8 --- README.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 656731a..0dd7738 100644 --- a/README.md +++ b/README.md @@ -40,17 +40,23 @@ run `ghostq apply` in it once. ## 📖 Commands + + ``` -ghostq install install the post-checkout hook globally (init.templateDir) -ghostq apply [path] link overlay files into the checkout (idempotent) -ghostq adopt ... move existing gitignored files into the overlay and link them -ghostq status [path] show link states and warnings without changing anything -ghostq prune [path] remove dangling ghostq-managed links (idempotent) -ghostq root print the overlay root -ghostq uninstall remove the hook wiring - -ghostq -v, --version print the version -ghostq -h, --help show usage (works on any subcommand too) +Usage: ghostq [options] [command] + +re-link personal, gitignored, per-repo files on clone / worktree add + +Commands: + install install the post-checkout hook globally (init.templateDir) + uninstall remove the hook wiring + apply [path] link overlay files into the checkout (idempotent) + adopt move existing gitignored files into the overlay and link + them + status [path] show link states and warnings without changing anything + prune [path] remove dangling ghostq-managed links (idempotent) + root print the overlay root + help [command] display help for command ``` ### Adopting an existing file