From 8ac3426c0df60b82bf6a2b06c678de32f21fd4fd Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 6 Sep 2026 12:47:27 +0000 Subject: [PATCH] fix(crawlproof): `crawlproof update` updates it `/cli-tools crawlproof update` printed the dashboard's usage and exited 2. The wrapper spells its own flags `--self-*` so that every plain word belongs to upstream, which is the right default and was the wrong answer here: `update` is the word `cli-tools update` already uses in this repo, so getting usage back is the command being wrong rather than the person. So the wrapper claims exactly three words -- update, upgrade, self-update -- and only in first position, which keeps `crawlproof ads budget update` upstream's. Installing is the wrapper's job and can never come to mean something in the dashboard, so this is the one place the rule is safe to break. The predicate lives in src/ and is exported, so the tests exercise the real thing rather than a copy of the word list. Note: test/root-ubuntu.test.ts fails on master as of #56, unrelated to this. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HvWJ4336pxTFRdRbvsTQeD --- bin/crawlproof.ts | 10 ++++++++-- src/crawlproof.ts | 19 +++++++++++++++++++ test/crawlproof.test.ts | 22 ++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/bin/crawlproof.ts b/bin/crawlproof.ts index 382376c..9b07826 100755 --- a/bin/crawlproof.ts +++ b/bin/crawlproof.ts @@ -28,6 +28,7 @@ import { meetsNodeFloor, resolveRunner, vendorBin, + wantsSelfUpdate, } from '../src/crawlproof.ts'; import { spawnInherit } from '../src/codeburn.ts'; import { isMain } from '../src/is-main.ts'; @@ -41,9 +42,14 @@ import { isMain } from '../src/is-main.ts'; */ const OURS = new Set(['--self-update', '--self-where']); + async function main(argv: string[]): Promise { - const flags = new Set(argv.filter((argument) => OURS.has(argument))); - const rest = argv.filter((argument) => !OURS.has(argument)); + const wantsUpdate = wantsSelfUpdate(argv); + const given = wantsUpdate ? argv.slice(1) : argv; + + const flags = new Set(given.filter((argument) => OURS.has(argument))); + if (wantsUpdate) flags.add('--self-update'); + const rest = given.filter((argument) => !OURS.has(argument)); if (!meetsNodeFloor(process.versions.node)) { process.stderr.write( diff --git a/src/crawlproof.ts b/src/crawlproof.ts index e113e23..3f3f67f 100644 --- a/src/crawlproof.ts +++ b/src/crawlproof.ts @@ -193,6 +193,25 @@ export function hasCoinpaySession(env: NodeJS.ProcessEnv = process.env): boolean return existsSync(join(env.HOME ?? homedir(), '.coinpay.json')); } +/** + * The plain words this wrapper claims for itself. + * + * Everything else goes to the dashboard untouched. These are the exception, + * deliberately and narrowly: installing is the wrapper's job and can never come + * to mean something upstream, and `update` is the word `cli-tools update` + * already uses, so typing it here and getting the dashboard's usage back is the + * command being wrong rather than the person. + */ +export const UPDATE_WORDS = new Set(['update', 'upgrade', 'self-update']); + +/** + * Only the FIRST argument counts, so the word can never swallow some later + * subcommand's own argument (`crawlproof ads budget update` stays upstream's). + */ +export function wantsSelfUpdate(argv: readonly string[]): boolean { + return UPDATE_WORDS.has(argv[0] ?? ''); +} + /** Whether a CrawlProof API token is reachable without the caller exporting one. */ export function hasToken(env: NodeJS.ProcessEnv = process.env): boolean { if (env.CRAWLPROOF_TOKEN?.trim()) return true; diff --git a/test/crawlproof.test.ts b/test/crawlproof.test.ts index e969104..b91892d 100644 --- a/test/crawlproof.test.ts +++ b/test/crawlproof.test.ts @@ -12,6 +12,7 @@ import { resolveRunner, vendorBin, vendorRoot, + wantsSelfUpdate, } from '../src/crawlproof.ts'; import { mkdtempSync, readFileSync, rmSync, existsSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; @@ -135,6 +136,27 @@ describe('meetsNodeFloor', () => { }); }); +describe('the update verb', () => { + it('claims the words that mean "install the latest"', () => { + expect(wantsSelfUpdate(['update'])).toBe(true); + expect(wantsSelfUpdate(['upgrade'])).toBe(true); + expect(wantsSelfUpdate(['self-update'])).toBe(true); + }); + + it('leaves every other first word to the dashboard', () => { + for (const word of ['dashboard', 'stats', 'ad', 'ads', 'help', 'version', '--json']) { + expect(wantsSelfUpdate([word])).toBe(false); + } + expect(wantsSelfUpdate([])).toBe(false); + }); + + it('only claims it in first position, so a subcommand argument is safe', () => { + // `crawlproof ads budget update` must reach upstream untouched. + expect(wantsSelfUpdate(['ads', 'budget', 'update'])).toBe(false); + expect(wantsSelfUpdate(['stats', 'update'])).toBe(false); + }); +}); + describe('credential probes', () => { it('sees a token in the environment', () => { expect(hasToken({ CRAWLPROOF_TOKEN: 'crp_x' })).toBe(true);