From 6bc8d200ea5cf01d47e464a3dce375c30040ac52 Mon Sep 17 00:00:00 2001 From: Luca Martial <48870843+luca-martial@users.noreply.github.com> Date: Thu, 14 May 2026 19:04:22 -0400 Subject: [PATCH] Remove deleted CLI command remnants (#105) * fix(cli): reject unknown commands generically * fix(cli): refresh ready command hints * refactor(cli): drop removed wiki command internals --- .../content/docs/cli-reference/index.mdx | 4 +- packages/cli/src/cli-program.ts | 57 ++----- packages/cli/src/doctor.test.ts | 13 ++ packages/cli/src/doctor.ts | 3 +- packages/cli/src/io/print-list.ts | 4 +- packages/cli/src/knowledge.test.ts | 143 +++++------------- packages/cli/src/knowledge.ts | 49 +----- packages/cli/src/status-project.ts | 9 +- 8 files changed, 75 insertions(+), 207 deletions(-) diff --git a/docs-site/content/docs/cli-reference/index.mdx b/docs-site/content/docs/cli-reference/index.mdx index 4eb11648..c4ef07db 100644 --- a/docs-site/content/docs/cli-reference/index.mdx +++ b/docs-site/content/docs/cli-reference/index.mdx @@ -37,9 +37,7 @@ ktx ``` The public context-build entrypoint is `ktx ingest [connectionId]` or -`ktx ingest --all`. Legacy command shapes such as `ktx scan`, `ktx ingest run`, -`ktx ingest status`, `ktx ingest replay`, `ktx ingest watch`, and -`ktx setup status` are not part of the current public CLI. +`ktx ingest --all`. ## Global Options diff --git a/packages/cli/src/cli-program.ts b/packages/cli/src/cli-program.ts index 01f66332..e8bdf445 100644 --- a/packages/cli/src/cli-program.ts +++ b/packages/cli/src/cli-program.ts @@ -58,6 +58,8 @@ type CommandPathNode = CommandWithGlobalOptions & { const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']); const COMMANDS_THAT_CREATE_PROJECT = new Set(['setup', 'ktx dev init']); const COMMANDS_WITH_OWN_MISSING_PROJECT_HANDLING = new Set(['status']); +const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']); +const GLOBAL_OPTIONS_WITHOUT_VALUE = new Set(['--debug', '--help', '-h', '--version', '-v']); class KtxProjectMissingAbortError extends Error { readonly isKtxProjectMissingAbort = true; @@ -72,24 +74,6 @@ function isKtxProjectMissingAbortError(error: unknown): error is KtxProjectMissi (typeof error === 'object' && error !== null && (error as { isKtxProjectMissingAbort?: unknown }).isKtxProjectMissingAbort === true) ); } -const REMOVED_COMMAND_PATHS = new Set([ - 'scan', - 'wiki read', - 'wiki write', -]); -const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']); -const OPTIONS_WITH_VALUE = new Set([ - '--project-dir', - '--query-history-window-days', - '--user-id', - '--limit', - '--format', - '--connection-id', - '--source-name', - '--query-file', - '--max-rows', -]); - export interface CommandWithGlobalOptions { opts: () => object; optsWithGlobals?: () => object; @@ -336,43 +320,32 @@ function formatCliError(error: unknown): string { return error instanceof Error ? error.message : String(error); } -function commandPathFromArgv(argv: string[]): string[] { - const path: string[] = []; - for (let index = 0; index < argv.length && path.length < 2; index += 1) { +function firstTopLevelCommandToken(argv: string[]): string | null { + for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === undefined) { continue; } if (arg === '--') { - break; + return null; } - if ((path.length === 0 ? GLOBAL_OPTIONS_WITH_VALUE : OPTIONS_WITH_VALUE).has(arg)) { + if (GLOBAL_OPTIONS_WITH_VALUE.has(arg)) { index += 1; continue; } - const optionsWithValue = path.length === 0 ? GLOBAL_OPTIONS_WITH_VALUE : OPTIONS_WITH_VALUE; - if ([...optionsWithValue].some((option) => arg.startsWith(`${option}=`))) { + if ([...GLOBAL_OPTIONS_WITH_VALUE].some((option) => arg.startsWith(`${option}=`))) { continue; } - if (path.length === 0 && arg === '--debug') { + if (GLOBAL_OPTIONS_WITHOUT_VALUE.has(arg) || arg.startsWith('-')) { continue; } - if (arg.startsWith('-')) { - continue; - } - path.push(arg); + return arg; } - return path; + return null; } -function removedCommandName(argv: string[]): string | null { - const path = commandPathFromArgv(argv); - if (path.length === 0) { - return null; - } - - const pathKey = path.join(' '); - return REMOVED_COMMAND_PATHS.has(pathKey) ? path.at(-1) ?? null : null; +function isKnownTopLevelCommand(program: Command, commandName: string): boolean { + return program.commands.some((command) => command.name() === commandName || command.aliases().includes(commandName)); } async function runBareInteractiveCommand( @@ -489,9 +462,9 @@ export async function runCommanderKtxCli( return 0; } - const removedCommand = removedCommandName(argv); - if (removedCommand) { - io.stderr.write(`error: unknown command '${removedCommand}'\n`); + const topLevelCommand = firstTopLevelCommandToken(argv); + if (topLevelCommand && !isKnownTopLevelCommand(program, topLevelCommand)) { + io.stderr.write(`error: unknown command '${topLevelCommand}'\n`); return 1; } diff --git a/packages/cli/src/doctor.test.ts b/packages/cli/src/doctor.test.ts index fddb4c68..daeb5b96 100644 --- a/packages/cli/src/doctor.test.ts +++ b/packages/cli/src/doctor.test.ts @@ -64,6 +64,11 @@ describe('formatDoctorReport', () => { expect(output).toContain('Node 22+ · pnpm 10.20+'); expect(output).not.toContain('v22.16.0'); expect(output).toContain('Everything ready.'); + expect(output).toContain('ktx status --json'); + expect(output).toContain('ktx sl list'); + expect(output).toContain('ktx wiki list'); + expect(output).not.toContain('ktx scan'); + expect(output).not.toContain('ktx sl ask'); }); it('shows the underlying detail for a single-check group on the group line', () => { @@ -462,6 +467,7 @@ describe('runKtxDoctor', () => { it('includes Postgres query-history readiness in project doctor output', async () => { process.env.ANTHROPIC_API_KEY = 'test-key'; // pragma: allowlist secret process.env.OPENAI_API_KEY = 'test-key'; // pragma: allowlist secret + process.env.WAREHOUSE_DATABASE_URL = 'postgresql://reader@example.test/warehouse'; await writeFile( join(tempDir, 'ktx.yaml'), [ @@ -516,8 +522,14 @@ describe('runKtxDoctor', () => { expect(out).toContain('pg_stat_statements ready (PostgreSQL 16.4)'); expect(out).toContain('info: pg_stat_statements.max is 1000'); expect(out).not.toContain('Update the Postgres parameter group or config'); + expect(out).toContain('ktx status --json'); + expect(out).toContain('ktx sl list'); + expect(out).toContain('ktx wiki list'); + expect(out).not.toContain('ktx scan'); + expect(out).not.toContain('ktx sl ask'); delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENAI_API_KEY; + delete process.env.WAREHOUSE_DATABASE_URL; }); it('returns blocked verdict when LLM is not configured', async () => { @@ -543,6 +555,7 @@ describe('runKtxDoctor', () => { ).resolves.toBe(1); expect(testIo.stdout()).toContain('no LLM configured'); + expect(testIo.stdout()).not.toContain('ktx ask'); expect(testIo.stdout()).toContain('ktx setup'); }); diff --git a/packages/cli/src/doctor.ts b/packages/cli/src/doctor.ts index b1845ae3..efb87e2b 100644 --- a/packages/cli/src/doctor.ts +++ b/packages/cli/src/doctor.ts @@ -5,6 +5,7 @@ import { join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; import type { KtxConfigIssue } from '@ktx/context/project'; +import { KTX_NEXT_STEP_DIRECT_COMMANDS } from './next-steps.js'; import type { BuildProjectStatusOptions } from './status-project.js'; const execFileAsync = promisify(execFile); @@ -287,7 +288,7 @@ interface RenderOptions { command?: 'setup' | 'project'; } -const NEXT_STEPS_PROJECT = ['ktx scan', 'ktx wiki', 'ktx sl ask "…"']; +const NEXT_STEPS_PROJECT = KTX_NEXT_STEP_DIRECT_COMMANDS.map((step) => step.command); export function formatDoctorReport(report: DoctorReport, options: Partial = {}): string { const opts: RenderOptions = { diff --git a/packages/cli/src/io/print-list.ts b/packages/cli/src/io/print-list.ts index bd7ab20a..b05e12f2 100644 --- a/packages/cli/src/io/print-list.ts +++ b/packages/cli/src/io/print-list.ts @@ -43,13 +43,13 @@ export interface PrintListArgs { io: KtxCliIo; } -export interface KtxJsonResultEnvelope { +interface KtxJsonResultEnvelope { kind: string; data: T; meta?: Record; } -export function writeJsonResult(io: KtxCliIo, envelope: KtxJsonResultEnvelope): void { +function writeJsonResult(io: KtxCliIo, envelope: KtxJsonResultEnvelope): void { io.stdout.write(`${JSON.stringify(envelope, null, 2)}\n`); } diff --git a/packages/cli/src/knowledge.test.ts b/packages/cli/src/knowledge.test.ts index 04f367c0..523d8a1b 100644 --- a/packages/cli/src/knowledge.test.ts +++ b/packages/cli/src/knowledge.test.ts @@ -1,8 +1,9 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { initKtxProject } from '@ktx/context/project'; +import { initKtxProject, loadKtxProject } from '@ktx/context/project'; import type { KtxEmbeddingPort } from '@ktx/context'; +import { writeLocalKnowledgePage } from '@ktx/context/wiki'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { runKtxKnowledge } from './knowledge.js'; @@ -40,6 +41,28 @@ class FakeEmbeddingPort implements KtxEmbeddingPort { } } +interface WikiPageFixture { + key?: string; + summary?: string; + content?: string; + tags?: string[]; + slRefs?: string[]; +} + +async function seedWikiPage(projectDir: string, fixture: WikiPageFixture = {}): Promise { + const project = await loadKtxProject({ projectDir }); + await writeLocalKnowledgePage(project, { + key: fixture.key ?? 'metrics-revenue', + scope: 'GLOBAL', + userId: 'local', + summary: fixture.summary ?? 'Revenue', + content: fixture.content ?? 'Revenue is paid order value.', + tags: fixture.tags ?? ['finance'], + refs: [], + slRefs: fixture.slRefs ?? ['orders'], + }); +} + describe('runKtxKnowledge', () => { let tempDir: string; @@ -51,36 +74,10 @@ describe('runKtxKnowledge', () => { await rm(tempDir, { recursive: true, force: true }); }); - it('writes, reads, lists, and searches wiki pages', async () => { + it('lists and searches wiki pages', async () => { const projectDir = join(tempDir, 'project'); await initKtxProject({ projectDir }); - - const writeIo = makeIo(); - await expect( - runKtxKnowledge( - { - command: 'write', - projectDir, - key: 'metrics-revenue', - scope: 'GLOBAL', - userId: 'local', - summary: 'Revenue', - content: 'Revenue is paid order value.', - tags: ['finance'], - refs: [], - slRefs: ['orders'], - }, - writeIo.io, - ), - ).resolves.toBe(0); - expect(writeIo.stdout()).toContain('Wrote wiki/global/metrics-revenue.md'); - - const readIo = makeIo(); - await expect( - runKtxKnowledge({ command: 'read', projectDir, key: 'metrics-revenue', userId: 'local' }, readIo.io), - ).resolves.toBe(0); - expect(readIo.stdout()).toContain('# metrics-revenue'); - expect(readIo.stdout()).toContain('Revenue is paid order value.'); + await seedWikiPage(projectDir); const listIo = makeIo(); await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local' }, listIo.io)).resolves.toBe(0); @@ -93,27 +90,10 @@ describe('runKtxKnowledge', () => { expect(searchIo.stdout()).toContain('metrics-revenue'); }); - it('prints wiki list, search, and read as public JSON envelopes', async () => { + it('prints wiki list and search as public JSON envelopes', async () => { const projectDir = join(tempDir, 'project'); await initKtxProject({ projectDir }); - - await expect( - runKtxKnowledge( - { - command: 'write', - projectDir, - key: 'metrics-revenue', - scope: 'GLOBAL', - userId: 'local', - summary: 'Revenue', - content: 'Revenue is paid order value.', - tags: ['finance'], - refs: [], - slRefs: ['orders'], - }, - makeIo().io, - ), - ).resolves.toBe(0); + await seedWikiPage(projectDir); const listIo = makeIo(); await expect(runKtxKnowledge({ command: 'list', projectDir, userId: 'local', json: true }, listIo.io)).resolves.toBe( @@ -137,48 +117,6 @@ describe('runKtxKnowledge', () => { data: { items: [expect.objectContaining({ key: 'metrics-revenue', summary: 'Revenue' })] }, meta: { command: 'wiki search' }, }); - - const readIo = makeIo(); - await expect( - runKtxKnowledge({ command: 'read', projectDir, key: 'metrics-revenue', userId: 'local', json: true }, readIo.io), - ).resolves.toBe(0); - expect(JSON.parse(readIo.stdout())).toMatchObject({ - kind: 'wiki.page', - data: { - key: 'metrics-revenue', - summary: 'Revenue', - content: 'Revenue is paid order value.', - }, - }); - }); - - it('rejects slash-delimited write keys with a flat-key suggestion', async () => { - const projectDir = join(tempDir, 'project'); - await initKtxProject({ projectDir }); - - const writeIo = makeIo(); - await expect( - runKtxKnowledge( - { - command: 'write', - projectDir, - key: 'orbit/company-overview', - scope: 'GLOBAL', - userId: 'local', - summary: 'Orbit', - content: 'Orbit overview.', - tags: [], - refs: [], - slRefs: [], - }, - writeIo.io, - ), - ).resolves.toBe(1); - - expect(writeIo.stderr()).toContain( - 'Invalid wiki key "orbit/company-overview". Wiki keys must be flat; use "orbit-company-overview".', - ); - expect(writeIo.stdout()).toBe(''); }); it('explains empty search results for a project without wiki pages', async () => { @@ -198,24 +136,13 @@ describe('runKtxKnowledge', () => { it('uses configured embeddings for semantic wiki search', async () => { const projectDir = join(tempDir, 'semantic-project'); await initKtxProject({ projectDir }); - - await expect( - runKtxKnowledge( - { - command: 'write', - projectDir, - key: 'active-contract-arr-open-tickets', - scope: 'GLOBAL', - userId: 'local', - summary: 'Active Contract ARR Ranked by Open Support Ticket Count', - content: 'Accounts ranked by annual recurring contract value and support ticket load.', - tags: ['historic-sql'], - refs: [], - slRefs: [], - }, - makeIo().io, - ), - ).resolves.toBe(0); + await seedWikiPage(projectDir, { + key: 'active-contract-arr-open-tickets', + summary: 'Active Contract ARR Ranked by Open Support Ticket Count', + content: 'Accounts ranked by annual recurring contract value and support ticket load.', + tags: ['historic-sql'], + slRefs: [], + }); const searchIo = makeIo(); await expect( diff --git a/packages/cli/src/knowledge.ts b/packages/cli/src/knowledge.ts index 66109301..d98df9e8 100644 --- a/packages/cli/src/knowledge.ts +++ b/packages/cli/src/knowledge.ts @@ -5,20 +5,16 @@ import { } from '@ktx/context'; import { loadKtxProject } from '@ktx/context/project'; import { - type LocalKnowledgeScope, type LocalKnowledgeSearchResult, type LocalKnowledgeSummary, listLocalKnowledgePages, - readLocalKnowledgePage, searchLocalKnowledgePages, - writeLocalKnowledgePage, } from '@ktx/context/wiki'; import { resolveOutputMode } from './io/mode.js'; -import { printList, type PrintListColumn, writeJsonResult } from './io/print-list.js'; +import { printList, type PrintListColumn } from './io/print-list.js'; export type KtxKnowledgeArgs = | { command: 'list'; projectDir: string; userId: string; output?: string; json?: boolean } - | { command: 'read'; projectDir: string; key: string; userId: string; json?: boolean } | { command: 'search'; projectDir: string; @@ -27,18 +23,6 @@ export type KtxKnowledgeArgs = output?: string; json?: boolean; limit?: number; - } - | { - command: 'write'; - projectDir: string; - key: string; - scope: LocalKnowledgeScope; - userId: string; - summary: string; - content: string; - tags: string[]; - refs: string[]; - slRefs: string[]; }; type KtxKnowledgeIo = import('./cli-runtime.js').KtxCliIo; @@ -104,25 +88,6 @@ export async function runKtxKnowledge( }); return 0; } - if (args.command === 'read') { - const page = await readLocalKnowledgePage(project, { key: args.key, userId: args.userId }); - if (!page) { - throw new Error(`Wiki page "${args.key}" was not found`); - } - if (args.json) { - writeJsonResult(io, { - kind: 'wiki.page', - data: page, - meta: { command: 'wiki read' }, - }); - return 0; - } - io.stdout.write(`# ${page.key}\n\n`); - io.stdout.write(`Scope: ${page.scope}\n`); - io.stdout.write(`Summary: ${page.summary}\n\n`); - io.stdout.write(`${page.content}\n`); - return 0; - } if (args.command === 'search') { const results = await searchLocalKnowledgePages(project, { query: args.query, @@ -153,18 +118,6 @@ export async function runKtxKnowledge( }); return 0; } - - const write = await writeLocalKnowledgePage(project, { - key: args.key, - scope: args.scope, - userId: args.userId, - summary: args.summary, - content: args.content, - tags: args.tags, - refs: args.refs, - slRefs: args.slRefs, - }); - io.stdout.write(`Wrote ${write.path}\n`); return 0; } catch (error) { io.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); diff --git a/packages/cli/src/status-project.ts b/packages/cli/src/status-project.ts index 6e953dc8..2aab1e5c 100644 --- a/packages/cli/src/status-project.ts +++ b/packages/cli/src/status-project.ts @@ -9,6 +9,7 @@ import type { } from '@ktx/context/project'; import type { PostgresPgssProbeResult } from '@ktx/context/ingest'; import type { DoctorCheck } from './doctor.js'; +import { KTX_NEXT_STEP_DIRECT_COMMANDS } from './next-steps.js'; type ProjectStatusLevel = 'ok' | 'warn' | 'fail'; type ProjectVerdict = 'ready' | 'partial' | 'blocked'; @@ -69,6 +70,8 @@ interface WarningItem { fix?: string; } +const PROJECT_READY_COMMANDS = KTX_NEXT_STEP_DIRECT_COMMANDS.map((step) => step.command); + function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } @@ -132,7 +135,7 @@ function buildLlmStatus(config: KtxProjectLlmConfig, env: NodeJS.ProcessEnv): Ll backend, model, status: 'fail', - detail: 'no LLM configured — ktx ask will not work', + detail: 'no LLM configured; research agent will not run', fix: 'Run: ktx setup (choose an LLM provider)', }; } @@ -571,7 +574,7 @@ function buildVerdict( if (llm.status === 'fail') { return { verdict: 'blocked', - reason: 'LLM not configured — `ktx ask` will not work.', + reason: 'LLM not configured; research agent will not run.', nextActions: ['ktx setup'], }; } @@ -605,7 +608,7 @@ function buildVerdict( return { verdict: 'ready', reason: 'Ready.', - nextActions: ['ktx scan', 'ktx wiki', 'ktx sl ask "…"'], + nextActions: [...PROJECT_READY_COMMANDS], }; }