diff --git a/src/cli.js b/src/cli.js index b5efda7..78f1e6a 100644 --- a/src/cli.js +++ b/src/cli.js @@ -4,6 +4,7 @@ import { Command } from 'commander'; import { readFileSync } from 'node:fs'; import { brand } from './lib/brand.js'; +import { USING_VIT_REMINDER } from './lib/constants.js'; import registerAdopt from './cmd/adopt.js'; import registerBeacon from './cmd/beacon.js'; import registerConfig from './cmd/config.js'; @@ -52,4 +53,12 @@ registerHack(program); registerLink(program); registerInbox(program); +program.addHelpText('after', USING_VIT_REMINDER); +program.configureOutput({ + outputError(str, write) { + write(str); + write(`${USING_VIT_REMINDER}\n`); + }, +}); + export { program }; diff --git a/src/lib/constants.js b/src/lib/constants.js index 7645a33..5af16b4 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -6,3 +6,4 @@ export const VOUCH_COLLECTION = 'org.v-it.vouch'; export const SKILL_COLLECTION = 'org.v-it.skill'; export const DEFAULT_JETSTREAM_URL = 'wss://jetstream2.us-east.bsky.network/subscribe'; export const DEFAULT_EXPLORE_URL = 'https://explore.v-it.org'; +export const USING_VIT_REMINDER = 'Load the using-vit skill before using this CLI.'; diff --git a/test/help-reminder.test.js b/test/help-reminder.test.js new file mode 100644 index 0000000..aa29939 --- /dev/null +++ b/test/help-reminder.test.js @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 sol pbc + +import { describe, expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { run } from './helpers.js'; + +const R = 'Load the using-vit skill before using this CLI.'; +const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8')); + +function count(haystack, needle) { + return haystack.split(needle).length - 1; +} + +describe('using-vit skill reminder', () => { + test('--help prints the reminder once on stdout', () => { + const result = run('--help', '/tmp'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('Usage: vit'); + expect(result.stdout).toContain('help [command]'); + expect(count(result.stdout, R)).toBe(1); + expect(typeof result.stderr).toBe('string'); + expect(result.stderr.length).toBe(0); + }); + + test('bare invocation prints help and the reminder once on stderr', () => { + const result = run('', '/tmp'); + expect(result.exitCode).toBe(1); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('Usage: vit'); + expect(result.stderr).toContain('help [command]'); + expect(count(result.stderr, R)).toBe(1); + }); + + test('unknown command prints the reminder once and no root help', () => { + const result = run('notacommand', '/tmp'); + expect(result.exitCode).toBe(1); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain("error: unknown command 'notacommand'"); + expect(count(result.stderr, R)).toBe(1); + expect(result.stderr).not.toContain('Usage: vit [options] [command]'); + }); + + test('unknown option prints the reminder once and no root help', () => { + const result = run('--bogus', '/tmp'); + expect(result.exitCode).toBe(1); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain("error: unknown option '--bogus'"); + expect(count(result.stderr, R)).toBe(1); + expect(result.stderr).not.toContain('Usage: vit [options] [command]'); + }); + + test('vouch missing ref prints the reminder once and no root help', () => { + const result = run('vouch', '/tmp'); + expect(result.exitCode).toBe(1); + expect(result.stdout).toBe(''); + expect(result.stderr).toContain('error: missing required argument'); + expect(count(result.stderr, R)).toBe(1); + expect(result.stderr).not.toContain('Usage: vit [options] [command]'); + }); + + test('--version is reminder-free', () => { + const result = run('--version', '/tmp'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe(pkg.version); + expect(typeof result.stderr).toBe('string'); + expect(result.stderr.length).toBe(0); + expect(result.stdout).not.toContain(R); + expect(result.stderr).not.toContain(R); + }); + + test('application --kind error is not decorated', () => { + const result = run('vouch fast-cache-invalidation --kind badkind', '/tmp'); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain('--kind must be one of'); + expect(result.stdout).not.toContain(R); + expect(result.stderr).not.toContain(R); + }); +}); diff --git a/test/helpers.js b/test/helpers.js index 1d8b33b..0410fe4 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -2,29 +2,23 @@ // Copyright (c) 2026 sol pbc import { join } from 'node:path'; -import { execSync } from 'node:child_process'; +import { spawnSync } from 'node:child_process'; const vitBin = join(import.meta.dir, '..', 'bin', 'vit.js'); const nonAgentEnv = { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '', OPENCODE: '' }; export function run(args, cwd, env, input) { - try { - return { - stdout: execSync(`bun ${vitBin} ${args}`, { - cwd, - encoding: 'utf-8', - timeout: 30000, - stdio: ['pipe', 'pipe', 'pipe'], - env: { ...process.env, ...nonAgentEnv, ...(env || {}) }, - input, - }).trim(), - exitCode: 0, - }; - } catch (err) { - return { - stdout: (err.stdout || '').trim(), - stderr: (err.stderr || '').trim(), - exitCode: err.status, - }; - } + const result = spawnSync(`bun ${vitBin} ${args}`, { + shell: true, + cwd, + encoding: 'utf-8', + timeout: 30000, + env: { ...process.env, ...nonAgentEnv, ...(env || {}) }, + input, + }); + return { + stdout: (result.stdout || '').trim(), + stderr: (result.stderr || '').trim(), + exitCode: result.status ?? 1, + }; }