mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
Merge pull request #39 from Kaelio/cli-print-project-dir
feat(cli): print resolved project dir
This commit is contained in:
commit
866d33e71a
7 changed files with 196 additions and 40 deletions
|
|
@ -42,6 +42,13 @@ interface KtxGlobalOptionValues {
|
|||
debug?: boolean;
|
||||
}
|
||||
|
||||
type CommandPathNode = CommandWithGlobalOptions & {
|
||||
name: () => string;
|
||||
parent?: CommandPathNode | null;
|
||||
};
|
||||
|
||||
const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']);
|
||||
|
||||
export interface CommandWithGlobalOptions {
|
||||
opts: () => object;
|
||||
optsWithGlobals?: () => object;
|
||||
|
|
@ -115,6 +122,80 @@ function optionsWithGlobals(command: CommandWithGlobalOptions): KtxGlobalOptionV
|
|||
};
|
||||
}
|
||||
|
||||
function commandOptions(command: CommandWithGlobalOptions): Record<string, unknown> {
|
||||
return (command.optsWithGlobals ? command.optsWithGlobals() : command.opts()) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function commandPath(command: CommandPathNode): string[] {
|
||||
const path: string[] = [];
|
||||
let current: CommandPathNode | null | undefined = command;
|
||||
|
||||
while (current) {
|
||||
path.unshift(current.name());
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
function isProjectAwareCommand(path: string[]): boolean {
|
||||
if (path.includes('__complete')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const rootCommand = path[1];
|
||||
if (rootCommand === 'dev') {
|
||||
return path[2] !== undefined && path[2] !== 'completion';
|
||||
}
|
||||
return rootCommand !== undefined && PROJECT_AWARE_ROOT_COMMANDS.has(rootCommand);
|
||||
}
|
||||
|
||||
function shouldSuppressProjectDirLine(path: string[], options: Record<string, unknown>): boolean {
|
||||
if (path.join(' ') === 'ktx dev init') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.viz === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const commandPathKey = path.join(' ');
|
||||
if (commandPathKey === 'ktx ingest watch') {
|
||||
return options.json !== true;
|
||||
}
|
||||
if (commandPathKey === 'ktx dev ingest watch') {
|
||||
return options.json !== true && options.plain !== true;
|
||||
}
|
||||
if (commandPathKey === 'ktx connection notion pick') {
|
||||
return options.input !== false;
|
||||
}
|
||||
const demoIndex = path.indexOf('demo');
|
||||
if (demoIndex >= 0) {
|
||||
const demoCommand = path[demoIndex + 1];
|
||||
return (
|
||||
options.json !== true &&
|
||||
options.plain !== true &&
|
||||
(demoCommand === undefined || demoCommand === 'replay' || demoCommand === 'ingest')
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldPrintProjectDir(command: CommandPathNode): boolean {
|
||||
const path = commandPath(command);
|
||||
if (!isProjectAwareCommand(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const options = commandOptions(command);
|
||||
if (options.json === true || options.output === 'json' || options.format === 'json') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !shouldSuppressProjectDirLine(path, options);
|
||||
}
|
||||
|
||||
export function resolveCommandProjectDir(command: CommandWithGlobalOptions): string {
|
||||
return resolveKtxProjectDir({ explicitProjectDir: optionsWithGlobals(command).projectDir });
|
||||
}
|
||||
|
|
@ -154,6 +235,13 @@ function writeDebug(io: KtxCliIo, commandContext: CommandWithGlobalOptions, comm
|
|||
io.stderr.write(`[debug] dispatch=${command}\n`);
|
||||
}
|
||||
|
||||
function writeProjectDir(io: KtxCliIo, commandContext: CommandPathNode): void {
|
||||
if (!shouldPrintProjectDir(commandContext)) {
|
||||
return;
|
||||
}
|
||||
io.stderr.write(`Project: ${resolveCommandProjectDir(commandContext)}\n`);
|
||||
}
|
||||
|
||||
function formatCliError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
@ -204,6 +292,9 @@ export async function runCommanderKtxCli(
|
|||
profileMark('commander:entry');
|
||||
let exitCode = 0;
|
||||
const program = createBaseProgram(info, io);
|
||||
program.hook('preAction', (_thisCommand, actionCommand) => {
|
||||
writeProjectDir(io, actionCommand as CommandPathNode);
|
||||
});
|
||||
profileMark('commander:base-program');
|
||||
const context: KtxCliCommandContext = {
|
||||
io,
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ describe('dev Commander tree', () => {
|
|||
},
|
||||
scanIo.io,
|
||||
);
|
||||
expect(scanIo.stderr()).toBe('');
|
||||
expect(scanIo.stderr()).toBe('Project: /tmp/project\n');
|
||||
});
|
||||
|
||||
it('dispatches dev scan --mode relationships through Commander', async () => {
|
||||
|
|
@ -266,7 +266,7 @@ describe('dev Commander tree', () => {
|
|||
},
|
||||
io.io,
|
||||
);
|
||||
expect(io.stderr()).toBe('');
|
||||
expect(io.stderr()).toBe('Project: /tmp/project\n');
|
||||
});
|
||||
|
||||
it.each(['--enrich', '--detect-relationships'])('rejects removed scan shorthand option %s', async (option) => {
|
||||
|
|
|
|||
|
|
@ -231,6 +231,38 @@ describe('runKtxCli', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('prints the resolved project directory for ordinary project commands', async () => {
|
||||
const connection = vi.fn(async () => 0);
|
||||
const testIo = makeIo();
|
||||
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'connection', 'list'], testIo.io, { connection })).resolves.toBe(
|
||||
0,
|
||||
);
|
||||
|
||||
expect(connection).toHaveBeenCalledWith({ command: 'list', projectDir: tempDir }, testIo.io);
|
||||
expect(testIo.stderr()).toBe(`Project: ${tempDir}\n`);
|
||||
});
|
||||
|
||||
it('skips the project directory line for JSON and TUI output modes', async () => {
|
||||
const publicIngest = vi.fn(async () => 0);
|
||||
const ingest = vi.fn(async () => 0);
|
||||
const jsonIo = makeIo();
|
||||
const vizIo = makeIo({ stdoutIsTty: true });
|
||||
|
||||
await expect(runKtxCli(['--project-dir', tempDir, 'ingest', '--all', '--json'], jsonIo.io, { publicIngest }))
|
||||
.resolves.toBe(0);
|
||||
await expect(
|
||||
runKtxCli(
|
||||
['--project-dir', tempDir, 'dev', 'ingest', 'status', 'run-1', '--viz'],
|
||||
vizIo.io,
|
||||
{ ingest },
|
||||
),
|
||||
).resolves.toBe(0);
|
||||
|
||||
expect(jsonIo.stderr()).toBe('');
|
||||
expect(vizIo.stderr()).toBe('');
|
||||
});
|
||||
|
||||
it('documents runtime stop all in command help', async () => {
|
||||
const testIo = makeIo();
|
||||
|
||||
|
|
@ -1035,7 +1067,7 @@ describe('runKtxCli', () => {
|
|||
}),
|
||||
nonInteractiveIo.io,
|
||||
);
|
||||
expect(nonInteractiveIo.stderr()).toBe('');
|
||||
expect(nonInteractiveIo.stderr()).toBe(`Project: ${tempDir}\n`);
|
||||
});
|
||||
|
||||
it('dispatches public connection through the existing connection implementation', async () => {
|
||||
|
|
@ -1047,7 +1079,7 @@ describe('runKtxCli', () => {
|
|||
);
|
||||
|
||||
expect(connection).toHaveBeenCalledWith({ command: 'list', projectDir: tempDir }, testIo.io);
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(testIo.stderr()).toBe(`Project: ${tempDir}\n`);
|
||||
});
|
||||
|
||||
it('dispatches setup status and top-level status through the setup runner', async () => {
|
||||
|
|
@ -1893,7 +1925,7 @@ describe('runKtxCli', () => {
|
|||
},
|
||||
expect.anything(),
|
||||
);
|
||||
expect(setupIo.stderr()).toBe('');
|
||||
expect(setupIo.stderr()).toBe(`Project: ${tempDir}\n`);
|
||||
});
|
||||
|
||||
it('validates connection metabase setup option values before runner dispatch', async () => {
|
||||
|
|
@ -2044,7 +2076,7 @@ describe('runKtxCli', () => {
|
|||
},
|
||||
testIo.io,
|
||||
);
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(testIo.stderr()).toBe(`Project: ${tempDir}\n`);
|
||||
});
|
||||
|
||||
it('prints generated connection notion pick help without invoking execution', async () => {
|
||||
|
|
@ -2101,7 +2133,7 @@ describe('runKtxCli', () => {
|
|||
},
|
||||
testIo.io,
|
||||
);
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(testIo.stderr()).toBe(`Project: ${tempDir}\n`);
|
||||
});
|
||||
|
||||
it('ignores connection notion pick root page flags in interactive mode', async () => {
|
||||
|
|
|
|||
|
|
@ -46,54 +46,63 @@ describe('project directory defaults', () => {
|
|||
spy: ReturnType<typeof vi.fn>;
|
||||
expected: Record<string, unknown>;
|
||||
runnerType: 'cli' | 'serve';
|
||||
expectedStderr: string;
|
||||
}> = [
|
||||
{
|
||||
argv: ['connection', 'list'],
|
||||
spy: connection,
|
||||
expected: { command: 'list', projectDir: '/tmp/ktx-env-project' },
|
||||
runnerType: 'cli',
|
||||
expectedStderr: 'Project: /tmp/ktx-env-project\n',
|
||||
},
|
||||
{
|
||||
argv: ['setup', 'demo', 'scan', '--no-input'],
|
||||
spy: demo,
|
||||
expected: { command: 'scan', projectDir: '/tmp/ktx-env-project' },
|
||||
runnerType: 'cli',
|
||||
expectedStderr: 'Project: /tmp/ktx-env-project\n',
|
||||
},
|
||||
{
|
||||
argv: ['dev', 'doctor', '--no-input'],
|
||||
spy: doctor,
|
||||
expected: { command: 'project', projectDir: '/tmp/ktx-env-project' },
|
||||
runnerType: 'cli',
|
||||
expectedStderr: 'Project: /tmp/ktx-env-project\n',
|
||||
},
|
||||
{
|
||||
argv: ['ingest', 'status', 'run-1'],
|
||||
spy: publicIngest,
|
||||
expected: { command: 'status', projectDir: '/tmp/ktx-env-project', runId: 'run-1' },
|
||||
runnerType: 'cli',
|
||||
expectedStderr: 'Project: /tmp/ktx-env-project\n',
|
||||
},
|
||||
{
|
||||
argv: ['setup', 'status'],
|
||||
spy: setup,
|
||||
expected: { command: 'status', projectDir: '/tmp/ktx-env-project' },
|
||||
runnerType: 'cli',
|
||||
expectedStderr: 'Project: /tmp/ktx-env-project\n',
|
||||
},
|
||||
{
|
||||
argv: ['dev', 'scan', 'warehouse'],
|
||||
spy: scan,
|
||||
expected: { command: 'run', projectDir: '/tmp/ktx-env-project', connectionId: 'warehouse' },
|
||||
runnerType: 'cli',
|
||||
expectedStderr: 'Project: /tmp/ktx-env-project\n',
|
||||
},
|
||||
{
|
||||
argv: ['serve', '--mcp', 'stdio'],
|
||||
spy: serveStdio,
|
||||
expected: { mcp: 'stdio', projectDir: '/tmp/ktx-env-project' },
|
||||
runnerType: 'serve',
|
||||
expectedStderr: '',
|
||||
},
|
||||
{
|
||||
argv: ['agent', 'tools', '--json'],
|
||||
spy: agent,
|
||||
expected: { command: 'tools', projectDir: '/tmp/ktx-env-project' },
|
||||
runnerType: 'cli',
|
||||
expectedStderr: '',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -105,7 +114,7 @@ describe('project directory defaults', () => {
|
|||
} else {
|
||||
expect(item.spy).toHaveBeenLastCalledWith(expect.objectContaining(item.expected), testIo.io);
|
||||
}
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(testIo.stderr()).toBe(item.expectedStderr);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -134,8 +143,8 @@ describe('project directory defaults', () => {
|
|||
expect.objectContaining({ command: 'status', projectDir: '/tmp/ktx-explicit-project' }),
|
||||
ingestIo.io,
|
||||
);
|
||||
expect(scanIo.stderr()).toBe('');
|
||||
expect(ingestIo.stderr()).toBe('');
|
||||
expect(scanIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n');
|
||||
expect(ingestIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n');
|
||||
});
|
||||
|
||||
it('uses nearest ancestor containing ktx.yaml when no explicit or environment project-dir exists', async () => {
|
||||
|
|
@ -167,6 +176,6 @@ describe('project directory defaults', () => {
|
|||
expect.objectContaining({ command: 'run', projectDir: expectedProjectDir }),
|
||||
testIo.io,
|
||||
);
|
||||
expect(testIo.stderr()).toBe('');
|
||||
expect(testIo.stderr()).toBe(`Project: ${expectedProjectDir}\n`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@ function parseJsonOutput<T>(stdout: string): T {
|
|||
return JSON.parse(stdout) as T;
|
||||
}
|
||||
|
||||
function expectProjectStderr(result: CliResult, projectDir: string): void {
|
||||
expect(result).toMatchObject({ code: 0, stderr: `Project: ${projectDir}\n` });
|
||||
}
|
||||
|
||||
async function runSetupNewProject(projectDir: string): Promise<CliResult> {
|
||||
return await runBuiltCli([
|
||||
'setup',
|
||||
|
|
@ -178,7 +182,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
const sourceDir = join(tempDir, 'source');
|
||||
|
||||
const init = await runSetupNewProject(projectDir);
|
||||
expect(init).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(init, projectDir);
|
||||
expect(init.stdout).toContain(`Project: ${projectDir}`);
|
||||
|
||||
await writeWarehouseConfig(projectDir);
|
||||
|
|
@ -204,14 +208,15 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
});
|
||||
|
||||
it('runs the default pre-seeded demo without credentials', async () => {
|
||||
const projectDir = join(tempDir, 'demo-project');
|
||||
const result = await runBuiltCli(
|
||||
['setup', 'demo', '--project-dir', join(tempDir, 'demo-project'), '--plain', '--no-input'],
|
||||
['setup', 'demo', '--project-dir', projectDir, '--plain', '--no-input'],
|
||||
{
|
||||
env: { ...process.env, ANTHROPIC_API_KEY: '' },
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(result, projectDir);
|
||||
expect(result.stdout).toContain('Mode: seeded');
|
||||
expect(result.stdout).toContain('Source: packaged demo project');
|
||||
expect(result.stdout).toContain('LLM calls: none');
|
||||
|
|
@ -231,7 +236,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
const seeded = await runBuiltCli(['setup', 'demo', '--project-dir', projectDir, '--plain', '--no-input'], {
|
||||
env: { ...process.env, ANTHROPIC_API_KEY: '' },
|
||||
});
|
||||
expect(seeded).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(seeded, projectDir);
|
||||
expect(seeded.stdout).toContain('Mode: seeded');
|
||||
|
||||
const wikiSearch = await runBuiltCli([
|
||||
|
|
@ -319,7 +324,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
expect(seeded.stdout).toContain('Knowledge pages:');
|
||||
|
||||
const inspect = await runBuiltCli(['setup', 'demo', 'inspect', '--project-dir', projectDir, '--no-input']);
|
||||
expect(inspect).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(inspect, projectDir);
|
||||
expect(inspect.stdout).toContain('Mode: seeded');
|
||||
expect(inspect.stdout).toContain('Status: ready');
|
||||
expect(inspect.stdout).toContain('Warehouse: 8 tables, 11,234 rows');
|
||||
|
|
@ -348,7 +353,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
env: { ...process.env, ANTHROPIC_API_KEY: '' },
|
||||
},
|
||||
);
|
||||
expect(seeded).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(seeded, projectDir);
|
||||
expect(seeded.stdout).toContain('Mode: seeded');
|
||||
|
||||
const client = new Client({ name: 'ktx-seeded-demo-smoke-client', version: '0.0.0' });
|
||||
|
|
@ -413,7 +418,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
expect(result.stdout).toContain('KTX setup doctor');
|
||||
expect(result.stdout).toContain('Node 22+');
|
||||
expect(result.stdout).toContain('Workspace-local CLI');
|
||||
expect(result.stderr).toBe('');
|
||||
expect(result.stderr).toBe(`Project: ${process.cwd()}\n`);
|
||||
expect([0, 1]).toContain(result.code);
|
||||
});
|
||||
|
||||
|
|
@ -433,7 +438,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
const projectDir = join(tempDir, 'reset-demo-project');
|
||||
|
||||
const init = await runBuiltCli(['setup', 'demo', 'init', '--project-dir', projectDir, '--no-input']);
|
||||
expect(init).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(init, projectDir);
|
||||
|
||||
const withoutForce = await runBuiltCli(['setup', 'demo', 'reset', '--project-dir', projectDir, '--no-input']);
|
||||
expect(withoutForce.code).toBe(1);
|
||||
|
|
@ -450,7 +455,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
'--force',
|
||||
'--no-input',
|
||||
]);
|
||||
expect(withForce).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(withForce, projectDir);
|
||||
expect(withForce.stdout).toContain(`Demo project reset: ${projectDir}`);
|
||||
});
|
||||
|
||||
|
|
@ -458,7 +463,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
const projectDir = join(tempDir, 'corrupt-demo-project');
|
||||
|
||||
const init = await runBuiltCli(['setup', 'demo', 'init', '--project-dir', projectDir, '--no-input']);
|
||||
expect(init).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(init, projectDir);
|
||||
await rm(join(projectDir, 'demo.db'), { force: true });
|
||||
|
||||
const replay = await runBuiltCli(['setup', 'demo', '--mode', 'replay', '--project-dir', projectDir, '--no-input']);
|
||||
|
|
@ -471,14 +476,14 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
const projectDir = join(tempDir, 'doctor-demo-project');
|
||||
|
||||
const init = await runBuiltCli(['setup', 'demo', 'init', '--project-dir', projectDir, '--no-input']);
|
||||
expect(init).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(init, projectDir);
|
||||
|
||||
const result = await runBuiltCli(['setup', 'demo', 'doctor', '--project-dir', projectDir, '--no-input']);
|
||||
expect(result.stdout).toContain('KTX demo doctor');
|
||||
expect(result.stdout).toContain('Demo dataset');
|
||||
expect(result.stdout).toContain('Demo replay');
|
||||
expect(result.stdout).toContain('Demo LLM provider');
|
||||
expect(result.stderr).toBe('');
|
||||
expect(result.stderr).toBe(`Project: ${projectDir}\n`);
|
||||
expect([0, 1]).toContain(result.code);
|
||||
});
|
||||
|
||||
|
|
@ -504,20 +509,20 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
it('runs structural and enriched scans through the built binary with manifest artifacts', async () => {
|
||||
const projectDir = join(tempDir, 'scan-project');
|
||||
const init = await runSetupNewProject(projectDir);
|
||||
expect(init).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(init, projectDir);
|
||||
|
||||
const dbPath = join(projectDir, 'warehouse.db');
|
||||
createSqliteWarehouse(dbPath);
|
||||
await writeSqliteScanConfig(projectDir, dbPath);
|
||||
|
||||
const connectionTest = await runBuiltCli(['connection', 'test', 'warehouse', '--project-dir', projectDir]);
|
||||
expect(connectionTest).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(connectionTest, projectDir);
|
||||
expect(connectionTest.stdout).toContain('Connection test passed: warehouse');
|
||||
expect(connectionTest.stdout).toContain('Driver: sqlite');
|
||||
expect(connectionTest.stdout).toContain('Tables: 2');
|
||||
|
||||
const structural = await runBuiltCli(['dev', 'scan', 'warehouse', '--project-dir', projectDir]);
|
||||
expect(structural).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(structural, projectDir);
|
||||
expect(structural.stdout).toContain('Status: done');
|
||||
expect(structural.stdout).toContain('Mode: structural');
|
||||
const structuralRunId = getRunId(structural.stdout);
|
||||
|
|
@ -560,7 +565,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
'--mode',
|
||||
'enriched',
|
||||
]);
|
||||
expect(providerlessEnriched).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(providerlessEnriched, projectDir);
|
||||
expect(providerlessEnriched.stdout).toContain('Mode: enriched');
|
||||
expect(providerlessEnriched.stdout).toContain('Relationships');
|
||||
expect(providerlessEnriched.stdout).toContain('Accepted: 1');
|
||||
|
|
@ -614,7 +619,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
|
||||
await writeSqliteScanConfig(projectDir, dbPath, true);
|
||||
const enriched = await runBuiltCli(['dev', 'scan', 'warehouse', '--project-dir', projectDir, '--mode', 'enriched']);
|
||||
expect(enriched).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(enriched, projectDir);
|
||||
expect(enriched.stdout).toContain('Mode: enriched');
|
||||
const enrichedRunId = getRunId(enriched.stdout);
|
||||
|
||||
|
|
@ -706,7 +711,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
it('adds a redacted Notion connection through the built binary', async () => {
|
||||
const projectDir = join(tempDir, 'notion-project');
|
||||
const init = await runSetupNewProject(projectDir);
|
||||
expect(init).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(init, projectDir);
|
||||
|
||||
const add = await runBuiltCli([
|
||||
'connection',
|
||||
|
|
@ -723,7 +728,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
'5',
|
||||
]);
|
||||
|
||||
expect(add).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(add, projectDir);
|
||||
expect(add.stdout).toContain('Connection: notion-main');
|
||||
expect(add.stdout).toContain('Driver: notion');
|
||||
|
||||
|
|
@ -746,7 +751,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
const projectDir = join(tempDir, 'project');
|
||||
|
||||
const init = await runSetupNewProject(projectDir);
|
||||
expect(init).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(init, projectDir);
|
||||
await writeWarehouseConfig(projectDir);
|
||||
|
||||
const client = new Client({ name: 'ktx-smoke-client', version: '0.0.0' });
|
||||
|
|
@ -812,7 +817,7 @@ describe('standalone built ktx CLI smoke', () => {
|
|||
it('serves scan execution and artifact inspection tools over stdio from the built binary', async () => {
|
||||
const projectDir = join(tempDir, 'scan-mcp-project');
|
||||
const init = await runSetupNewProject(projectDir);
|
||||
expect(init).toMatchObject({ code: 0, stderr: '' });
|
||||
expectProjectStderr(init, projectDir);
|
||||
|
||||
const dbPath = join(projectDir, 'warehouse.db');
|
||||
createSqliteWarehouse(dbPath);
|
||||
|
|
|
|||
|
|
@ -520,6 +520,15 @@ function requireSuccess(label, result) {
|
|||
assert.equal(result.stderr, '', label + ' wrote unexpected stderr');
|
||||
}
|
||||
|
||||
function requireProjectStderr(label, result, projectDir) {
|
||||
assert.equal(
|
||||
result.code,
|
||||
0,
|
||||
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
||||
);
|
||||
assert.equal(result.stderr, 'Project: ' + projectDir + '\\n', label + ' wrote unexpected stderr');
|
||||
}
|
||||
|
||||
function requireSuccessWithStderr(label, result, stderrPattern) {
|
||||
assert.equal(
|
||||
result.code,
|
||||
|
|
@ -631,7 +640,7 @@ try {
|
|||
'--skip-sources',
|
||||
'--skip-agents',
|
||||
]);
|
||||
requireSuccess('ktx setup', init);
|
||||
requireProjectStderr('ktx setup', init, projectDir);
|
||||
requireOutput('ktx setup', init, /Project: /);
|
||||
|
||||
const emptyProjectDir = join(root, 'empty-project');
|
||||
|
|
@ -650,7 +659,7 @@ try {
|
|||
'--skip-sources',
|
||||
'--skip-agents',
|
||||
]);
|
||||
requireSuccess('ktx setup empty project', emptyInit);
|
||||
requireProjectStderr('ktx setup empty project', emptyInit, emptyProjectDir);
|
||||
const emptySearch = await run('pnpm', [
|
||||
'exec',
|
||||
'ktx',
|
||||
|
|
@ -905,7 +914,7 @@ try {
|
|||
'--project-dir',
|
||||
projectDir,
|
||||
]);
|
||||
requireSuccess('ktx scan structural', structuralScan);
|
||||
requireProjectStderr('ktx scan structural', structuralScan, projectDir);
|
||||
requireOutput('ktx scan structural', structuralScan, /Status: done/);
|
||||
requireOutput('ktx scan structural', structuralScan, /Mode: structural/);
|
||||
requireOutput('ktx scan structural', structuralScan, /Needs attention\\s+None/);
|
||||
|
|
@ -916,7 +925,7 @@ try {
|
|||
projectDir,
|
||||
structuralScanRunId,
|
||||
]);
|
||||
requireSuccess('ktx scan status', scanStatus);
|
||||
requireProjectStderr('ktx scan status', scanStatus, projectDir);
|
||||
requireOutput('ktx scan status', scanStatus, new RegExp('Run: ' + structuralScanRunId));
|
||||
requireOutput('ktx scan status', scanStatus, /Status: done/);
|
||||
requireOutput('ktx scan status', scanStatus, /Mode: structural/);
|
||||
|
|
@ -943,7 +952,7 @@ try {
|
|||
'--mode',
|
||||
'enriched',
|
||||
]);
|
||||
requireSuccess('ktx scan enriched', enrichedScan);
|
||||
requireProjectStderr('ktx scan enriched', enrichedScan, projectDir);
|
||||
requireOutput('ktx scan enriched', enrichedScan, /Status: done/);
|
||||
requireOutput('ktx scan enriched', enrichedScan, /Mode: enriched/);
|
||||
const enrichedScanRunId = getRunId(enrichedScan.stdout);
|
||||
|
|
@ -1037,6 +1046,15 @@ function requireStdout(label, result, pattern) {
|
|||
assert.match(result.stdout, pattern, label + ' stdout did not match ' + pattern);
|
||||
}
|
||||
|
||||
function requireProjectStderr(label, result, projectDir) {
|
||||
assert.equal(
|
||||
result.code,
|
||||
0,
|
||||
label + ' failed with code ' + result.code + '\\nstdout:\\n' + result.stdout + '\\nstderr:\\n' + result.stderr,
|
||||
);
|
||||
assert.equal(result.stderr, 'Project: ' + projectDir + '\\n', label + ' wrote unexpected stderr');
|
||||
}
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), 'ktx-packed-demo-smoke-'));
|
||||
try {
|
||||
const projectDir = join(root, 'demo-project');
|
||||
|
|
@ -1059,7 +1077,7 @@ try {
|
|||
requireStdout('ktx setup demo seeded', seeded, /ktx serve --mcp stdio/);
|
||||
assert.doesNotMatch(seeded.stdout, new RegExp(['--mode', 'deterministic'].join(' ')));
|
||||
assert.doesNotMatch(seeded.stdout, /KTX memory flow/);
|
||||
assert.equal(seeded.stderr, '', 'ktx setup demo seeded wrote unexpected stderr');
|
||||
requireProjectStderr('ktx setup demo seeded', seeded, projectDir);
|
||||
|
||||
const demoWikiSearch = await run('pnpm', [
|
||||
'exec',
|
||||
|
|
@ -1108,7 +1126,7 @@ try {
|
|||
assert.ok([0, 1].includes(doctor.code), 'ktx dev doctor setup exit code must be 0 or 1');
|
||||
requireStdout('ktx dev doctor setup', doctor, /KTX setup doctor/);
|
||||
requireStdout('ktx dev doctor setup', doctor, /Node 22\\+/);
|
||||
assert.equal(doctor.stderr, '', 'ktx dev doctor setup wrote unexpected stderr');
|
||||
assert.equal(doctor.stderr, 'Project: ' + process.cwd() + '\\n', 'ktx dev doctor setup wrote unexpected stderr');
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -521,7 +521,8 @@ describe('verification snippets', () => {
|
|||
assert.doesNotMatch(source, new RegExp(["'demo'", "'--mode'", "'deterministic'"].join(', ')));
|
||||
assert.match(source, /'dev', 'doctor', 'setup', '--no-input'/);
|
||||
assert.match(source, /'--plain'/);
|
||||
assert.match(source, /ktx setup demo seeded wrote unexpected stderr/);
|
||||
assert.match(source, /function requireProjectStderr/);
|
||||
assert.match(source, /requireProjectStderr\('ktx setup demo seeded', seeded, projectDir\)/);
|
||||
assert.match(source, /Object\.keys\(packageJson\.dependencies\)/);
|
||||
assert.match(source, /'@kaelio\/ktx'/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue