Troubleshooting

From docs/TROUBLESHOOTING.md at 4e43843 View on GitHub

Failures ketch already explains, collected in one place so the fix does not have to be searched out of the command that reported it.

ketch list shows ? or latest: offline

latest is looked up from each package's source. ? means that one source did not answer — GitHub's unauthenticated rate limit (60 requests an hour) is the usual cause, a repository with no releases or one that was renamed or removed is the next — and the line under the table names every such package. The rest of the list is still right. ketch list -v shows each failure; set GITHUB_TOKEN (export GITHUB_TOKEN=$(gh auth token) works) to raise the rate limit.

latest: offline under the installed packages means no source answered at all, so ketch showed what it knows without the network. ketch list remote fails instead, since it has nothing to show without one.

Answers are cached for 10 minutes in ~/.ketch/cache/latest.json, and only answers: a ? is asked again on the next run, while a version found a minute ago is not. Delete the file to look everything up again now. The ketch list section of the command reference has the details.

"another ketch process holds the lock"

Commands that change the install tree (install, upgrade, uninstall, rollback, prune, pin, link, lock's sync, self ...) take one lock per ketch root, and a second one does not wait: it fails at once with exit code 8.

$ ketch install ripgrep
error: another ketch process holds the lock (pid 4242)

The pid is the holder: another terminal, a script, or an app that calls ketch in-process (then it is that app's pid, and the operation is still running). Wait for it to finish and run the command again. Two ketch processes writing state.json together would each save a view that omits the other's package, so the lock is never skipped.

A crash leaves ~/.ketch/.lock behind. ketch reads the pid in it and takes the file over when that process is gone, so nothing needs deleting by hand. If the pid is still running, the lock is real; ketch doctor reports a lock file and says whether its holder is alive or stale.

A Windows executable is locked mid-upgrade

Another process is running from a file ketch is about to replace. ketch lists the processes, asks whether to stop them, and on yes sends TERM then KILL (taskkill /F on Windows); the current ketch pid is never offered. Declining leaves them running and replacement continues as before — on Windows the busy binary is renamed aside first, so the new one can still land.

$ ketch upgrade
testtool 1.0.0 -> 2.0.0
stop 1 process using testtool? [y/N]

An install stops at "ships several binaries sharing its name"

The release holds more than one executable that answers to the package's name (rtok-cli and rtok-hook for a package called rtok), none of them is named exactly like the package, the manifest has no bin entry, and there was no terminal to ask in — piped, in CI, or run with --yes. Nothing is installed.

$ ketch install owner/rtok --yes
error: `rtok` ships several binaries sharing its name (rtok-cli, rtok-hook) and none is named `rtok`; ...

Pass --bin with the one you want — ketch install owner/rtok --bin rtok-cli, which needs no terminal — or run the same command in a terminal without --yes and pick one from the list. Either way the choice is remembered in state.json, reused on every upgrade, and copied into ketch.lock by ketch lock, so ketch sync repeats it on another machine. Or name the binary in your own manifest, which wins over the registry:

# ~/.ketch/manifests/rtok.toml
name = "rtok"
source = "github:owner/rtok"
bin = [{ name = "rtok-cli" }]

The same choice is made on macOS, Linux and Windows; see the bin section of MANIFESTS.md for the order.

ketch.exe.old is still in the bin dir

Windows will not delete the file that backs a running image. self upgrade renames the running ketch.exe to ketch.exe.old, and the delete of that aside fails until this process exits. The next ketch self upgrade or ketch self install removes it. ketch doctor names the file when it is still there; delete it by hand once nothing is running from it.

A swap that fails with Access is denied (os error 5) on ketch.exe itself is a short lock, usually antivirus scanning the new file. The rename and the copy are retried for under a second. A lock that outlasts those retries restores the previous binary.

If the new binary or the process listing never finishes, ketch stops that process and says so, including its pid when the process had been created. A spawn that never returns has no pid to stop; the message says the wait was ended. When that happens to the version check after a swap, the previous binary is kept.

brew upgrade does not upgrade ketch itself

Homebrew keeps only the bootstrap binary; the ketch it installed is one ketch downloaded and verified itself. Hand over explicitly after the cask updates:

brew upgrade --cask ketch
ketch self upgrade

ketch self update is kept as an alias, so existing scripts keep working.

ketch self upgrade says ketch is managed by mise

A ketch installed with mise use -g github:pyrlyn/ketch lives in mise's install tree, under a directory named for its version. Rewriting that binary would leave mise reporting a version that is no longer on disk, so ketch declines. Either let mise upgrade it:

mise upgrade

or hand ketch over to itself once, after which ketch self upgrade works and the mise copy is only a bootstrap:

ketch self install

A registry entry collides or will not validate

Name and alias collisions are fatal in ketch registry validate (so they cannot land) and warnings in ketch update (so one bad folder never takes a working copy down with it). From the registry checkout root:

ketch registry validate .
ketch registry validate . --fixture ./ci/fixtures --changed ripgrep --changed fd

The ketch-registry repository carries no CI by the owner's choice (plan.md F2), so validate locally and keep the pre-push hook in REGISTRY.md installed.

Notarisation fails on a release run

The Notarise step in release.yml (from .github/build-check.yml) only runs when the repository variable KETCH_NOTARIZE is true, and then fails rather than ships unsigned: the three secrets must exist — APPSTORE_CONNECT_KEY (the .p8, base64), APPSTORE_CONNECT_KEY_ID, APPSTORE_CONNECT_ISSUER_ID. The smoke test needs spctl to report source=Notarized Developer ID; a bare Mach-O binary cannot be stapled, so Gatekeeper looks its ticket up online. See AGENTS.md Releasing and tests/release-workflows.sh.