Skip to content

cli-tools list: say which implementation is on PATH - #12

Merged
ralyodio merged 1 commit into
masterfrom
fix-list-reporting
Aug 19, 2026
Merged

cli-tools list: say which implementation is on PATH#12
ralyodio merged 1 commit into
masterfrom
fix-list-reporting

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Fixes a defect in the list command shipped in #10.

The bug

list asked whether a file of each name existed on PATH and printed * when one did. On the machine this was written for, that produced:

* gh-prs           Every open PR across the owners you name
* gh-prs-merge     Squash-merge the PRs that are genuinely ready
...
All on PATH.

Eight stars, "All on PATH" — while only three of the eight were this checkout's code. The other five resolved to the older hand-written scripts they were ported from, in ~/scripts/bin. A presence check cannot tell "installed" from "something answers to that name", and here those are different programs.

That distinction is not cosmetic. gh-prs-merge repairs by default under --apply in the older script and only when asked with --fix here. Reporting both as installed is the same misleading-green-row failure as an exit code that cannot tell "I refused" from "there was nothing to do".

The fix

resolveCommand() follows the symlink on both sides and classifies each name as ours, other or missing:

* blog-post        Publish to a plain-HTML blog without breaking the feed
* cli-tools        This dispatcher: list, update and wire up the others
* domainfree       Which of these domains can you actually register
! domainjson       whois-style, JSON-first name lookup
                   ↳ on PATH: /home/anthony/scripts/bin/domainjson
! gh-prs-merge     Squash-merge the PRs that are genuinely ready
                   ↳ on PATH: /home/anthony/scripts/bin/gh-prs-merge

3 of 8 running from this checkout.
5 shadowed by another implementation (!). `cli-tools link --force`
takes over a symlink; a real file of that name is refused either way.
Check the flags first — a port does not always keep the original defaults.

The ! row names the file, because otherwise you know a name is taken but not by what, and the next step is a readlink you should not have had to think of.

Falling out of it: run from a worktree, every installed command correctly reads as ! — the stale-checkout trap the docs warn about, now visible without asking for it.

Checks

120 tests pass (was 114), typecheck clean. New cases cover ours / other / missing, PATH order precedence, a broken symlink, and a sibling directory with a shared prefix (bin-old beside bin) that a naive startsWith would have called ours.

🤖 Generated with Claude Code

…e is

`list` asked whether a file of each name existed on PATH and printed `*` when
one did. On the machine this was written for it printed eight stars and "All on
PATH" while only three of the eight were this checkout's code — the other five
resolved to the older hand-written scripts they were ported from, in
~/scripts/bin. A presence check cannot tell "installed" from "something answers
to that name", and here those are different programs.

That distinction is not cosmetic. gh-prs-merge repairs by default under --apply
in the older script and only when asked with --fix here, so which file is on
PATH changes what a merge run does. Reporting both as installed is the same
misleading-green-row failure as an exit code that cannot tell "I refused" from
"there was nothing to do".

So resolveCommand() follows the symlink on both sides and classifies each name
as ours, other, or missing. A row that is somebody else's is marked `!` and
names the file it found, because otherwise you know a name is taken but not by
what, and the next step is a readlink you should not have had to think of. The
summary counts the three states separately, and points at `link --force` while
saying to check the flags first.

Falling out of it: run from a worktree, every installed command now correctly
reads as `!`. That is the stale-checkout trap the docs warn about, visible
without asking for it.

120 tests pass (was 114), typecheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

3 finding(s)

MEDIUM: 1 | LOW: 2

Severity Rule Location
MEDIUM redos-nested-quantifier src/domain-free.ts:56
LOW insecure-temp-file test/blog.test.ts:73
LOW insecure-temp-file test/blog.test.ts:74

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 1c9a514 into master Aug 19, 2026
5 checks passed
@ralyodio
ralyodio deleted the fix-list-reporting branch August 19, 2026 14:03
ralyodio added a commit that referenced this pull request Aug 19, 2026
…#14)

generate-names needed OPENAI_API_KEY or ANTHROPIC_API_KEY in the environment,
which in practice means writing the key into a shell profile — a plaintext copy
in a file that is backed up, synced, and read by every process you start, to
configure one command that runs occasionally.

So there is now a store: ~/.config/cli-tools/credentials.json, written 0600
inside a 0700 directory, with a command to manage it.

  cli-tools config                what is set, and where each key came from
  cli-tools config set openai     prompts with echo off
  cli-tools config unset openai

Nothing prints a whole key. `config` shows a masked preview and a length —
enough to tell two keys apart, not enough to use one — and --json carries the
same previews rather than the values.

The environment still wins over the file, because a one-off `KEY=… command` and
a CI-injected key both have to keep working. That precedence is invisible at the
moment it bites: you store a key, the old one keeps being used, and nothing says
why. So `config` reports the *source* of each key rather than only whether one
exists, and states plainly when a stored value is being shadowed. It is the same
lesson as the `list` fix in #12 — presence is not the useful question, provenance
is.

A value may be piped for scripts, or passed inline; inline warns, because it
lands in shell history and in `ps`.

Deliberately not a .env: nothing loads it into an environment wholesale, nothing
syncs it, and it is not how a key travels between machines. A secret a deployed
service needs still belongs on that service.

resolveCredentials returns an environment-shaped record, so resolveProvider
consumes it unchanged — the wiring is one line, and a test covers it.

167 tests pass (was 164 plus 3 new wiring cases), typecheck clean. Verified end
to end in a sandbox: set/unset/re-set, 0600 on a file that already existed with
a looser mode, masked output, and no key in --json.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant