From 0ff14e1953d936cef6ac1ae9330e2fbb2259d268 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Tue, 12 May 2026 15:04:01 +0200 Subject: [PATCH 1/3] feat(cli): print resolved project dir --- packages/cli/src/cli-program.ts | 91 ++++++++++++++++++++++++++++ packages/cli/src/dev.test.ts | 4 +- packages/cli/src/index.test.ts | 42 +++++++++++-- packages/cli/src/project-dir.test.ts | 17 ++++-- 4 files changed, 143 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/cli-program.ts b/packages/cli/src/cli-program.ts index fd7a50ef..330477bb 100644 --- a/packages/cli/src/cli-program.ts +++ b/packages/cli/src/cli-program.ts @@ -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 { + return (command.optsWithGlobals ? command.optsWithGlobals() : command.opts()) as Record; +} + +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): 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, diff --git a/packages/cli/src/dev.test.ts b/packages/cli/src/dev.test.ts index 167513d5..dfa35395 100644 --- a/packages/cli/src/dev.test.ts +++ b/packages/cli/src/dev.test.ts @@ -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) => { diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 87a0089f..855ff5fd 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -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 () => { diff --git a/packages/cli/src/project-dir.test.ts b/packages/cli/src/project-dir.test.ts index 02f6dfde..3fd8cc06 100644 --- a/packages/cli/src/project-dir.test.ts +++ b/packages/cli/src/project-dir.test.ts @@ -46,54 +46,63 @@ describe('project directory defaults', () => { spy: ReturnType; expected: Record; 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`); }); }); From 6a1fded5ce38c24b1af7f6f4ce64715027fbc70b Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Tue, 12 May 2026 15:31:41 +0200 Subject: [PATCH 2/3] fix(ci): align smoke stderr expectations --- packages/cli/src/standalone-smoke.test.ts | 47 +++++++++++++---------- scripts/package-artifacts.mjs | 32 +++++++++++---- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/packages/cli/src/standalone-smoke.test.ts b/packages/cli/src/standalone-smoke.test.ts index 1bab2b73..ac9d3e47 100644 --- a/packages/cli/src/standalone-smoke.test.ts +++ b/packages/cli/src/standalone-smoke.test.ts @@ -146,6 +146,10 @@ function parseJsonOutput(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 { 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); diff --git a/scripts/package-artifacts.mjs b/scripts/package-artifacts.mjs index 032df825..c419f490 100644 --- a/scripts/package-artifacts.mjs +++ b/scripts/package-artifacts.mjs @@ -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 }); } From 5277c81b335c24b5b0203c78569f10ae198a6198 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Tue, 12 May 2026 16:17:45 +0200 Subject: [PATCH 3/3] fix(ci): update artifact source test --- scripts/package-artifacts.test.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index 624248d0..7e98020b 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -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'/); });