Add alias command for creating extra CLI names - #32
Conversation
- Add `commit alias` with an interactive hub for listing, creating, and deleting aliases, plus scriptable `list`, `add <name> <target>`, and `remove <name>` subcommands. - Bind each alias to a subcommand and install it as a POSIX shim under `~/.commit-tools/bin`, so reinstalling or removing the npm package never touches user aliases. - Introduce a branded `AliasName` in `src/domain/alias/alias.ts` whose private constructor makes an unsafe filesystem path unrepresentable in `shimPath` and `removeShim`. - Keep the registry in `aliases.json` separate from `config.json` so aliases work before `commit setup` has run, validating names and rejecting duplicates at the load boundary. - Offer a one-time managed `PATH` block in `.zshrc`, `.bashrc`, or `config.fish`, written only after an explicit confirmation and idempotent on repeat runs. - Warn before an alias shadows an existing binary on `PATH`, since the alias bin dir is prepended. - Quote interpreter and entry-script paths with POSIX single-quote escaping so a `$` or `'` in the path cannot break the generated shim. - Reject `commit alias` on Windows with a clear message rather than writing an unusable `sh` shim. - Cover the domain, storage, shim, `PATH` setup, CLI, and parser behavior with unit tests.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a37f85e385
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| addAlias(aliases, alias).either( | ||
| (msg) => Future.reject<Error, readonly Alias[]>(new Error(msg)), | ||
| (next) => | ||
| writeShim(alias) |
There was a problem hiding this comment.
Reconcile existing shims during direct mutations
When a user changes an nvm prefix or reinstalls the CLI and then runs scripted commit alias add ..., this call rewrites only the new alias; every existing shim continues executing the old absolute Node and entry-script paths embedded by shimSource, so those aliases run stale code or fail once the old prefix is removed. This is a P1 availability regression and contradicts the documented self-healing behavior. Replace the per-alias write with reconcileShims(next), and likewise reconcile the remaining aliases after direct removal.
AGENTS.md reference: AGENTS.md:L5-L12
Useful? React with 👍 / 👎.
| const current = await readProfile(profile.file); | ||
| if (current.includes(MARKER_START)) return "already-present" as const; | ||
|
|
||
| await writeFile(profile.file, current + managedBlock(profile.shell), "utf-8"); |
There was a problem hiding this comment.
Create the Fish profile directory before writing
For a Fish user whose ~/.config/fish directory does not yet exist, accepting the PATH setup reaches this writeFile after readProfile treats the missing file as empty, but it fails with ENOENT because no parent directory is created. The alias and registry have already been written, so the command exits unsuccessfully, the alias remains unavailable on PATH, and retrying alias add reports a duplicate. This is a P1 first-use correctness failure; create dirname(profile.file) recursively before writing the profile.
AGENTS.md reference: AGENTS.md:L9-L12
Useful? React with 👍 / 👎.
Why
commitwas the only name the CLI answered to, and there was no way to get a shorter one or a one-word shortcut for a subcommand —commit branchis 14 keystrokes for something run dozens of times a day.This adds
commit alias: list, create, and delete extra CLI names, each bound to a subcommand.What changed
commit aliaswith no arguments opens an interactive hub (table + Create/Delete/Done, looping) styled like the existingsetup/loginflows. Scripted use is also supported:src/domain/alias/alias.tsAliasNamebranded type, registry operations, target vocabularysrc/infra/storage/aliases.tsaliases.jsonload/save with boundary validationsrc/infra/alias/shims.tsPATHconflict scansrc/infra/alias/path-setup.tssrc/cli/alias.tsWired through
src/cli/parser.tsandindex.ts; documented inREADME.md.Design decisions worth reviewing
Shims, not shell aliases or npm symlinks. Each alias is a small
shscript in~/.commit-tools/bin, with a one-time managedPATHblock added to the user's rc file after explicit confirmation. This keeps aliases out of npm's global bin (nosudo, andnpm uninstall -gcannot orphan or delete them), makes deletion a single file removal, and keeps aliases working in non-interactive shells and git hooks — which a.zshrcaliasline would not.AliasNameis a branded class with a private constructor. The name becomes a filesystem path, soAliasName.parseis the only way to construct one.shimPath/removeShimtakeAliasNameand do not re-validate. Names are also validated when loadingaliases.json, so a hand-edited{"name": "../../evil"}is rejected at the boundary rather than reachingresolve().Registry lives in
aliases.json, notconfig.json. Aliases must work beforecommit setuphas ever run, andsetup/model/efforteach rewrite the wholeConfigobject.Shim quoting is POSIX, not JSON.
shellQuoteuses single-quote escaping, so a$or'in the Node or entry-script path cannot break or expand inside the generated shim.Reviewer notes
PATH, so an alias could shadow an existing binary (git). Interactive creation warns with the conflicting path and requires confirmation; scripted creation warns and proceeds.commit aliasexits with a clear message instead of writing an unusableshshim. Windows support needs.cmdshims andsetx; not attempted here.reconcileShimsrewrites every shim on hub entry and on add/remove, so they self-heal after an nvm or npm move.alias listdeliberately performs no writes.Verification
pnpm typecheck,pnpm lint,pnpm exec prettier . --check, andpnpm testall pass — 199 tests, 33 of them new.dist/: shim contents and0755mode, shim executes the bound subcommand, and duplicate / unsafe-name / reserved-name / invalid-target / unknown-remove each exit non-zero with a specific message.alias remove <name>on an empty registry exited0silently, and a missing target produced the same error as an invalid one.