From 478c06d467813fdc60a64ddd9b7cd00d4ee3a361 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Mon, 11 May 2026 12:37:24 +0200 Subject: [PATCH] feat(cli): pass managed daemon options to scan --- packages/cli/src/commands/scan-commands.ts | 5 +++ packages/cli/src/dev.test.ts | 4 ++ packages/cli/src/index.test.ts | 37 +++++++++++++++++++ packages/cli/src/scan.test.ts | 43 ++++++++++++++++++++++ packages/cli/src/scan.ts | 18 ++++++++- 5 files changed, 106 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/commands/scan-commands.ts b/packages/cli/src/commands/scan-commands.ts index 9f3d35f7..fc30fafa 100644 --- a/packages/cli/src/commands/scan-commands.ts +++ b/packages/cli/src/commands/scan-commands.ts @@ -1,5 +1,6 @@ import { type Command, InvalidArgumentError, Option } from '@commander-js/extra-typings'; import { type KtxCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir } from '../cli-program.js'; +import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js'; import type { KtxScanArgs } from '../scan.js'; import { profileMark } from '../startup-profile.js'; @@ -102,6 +103,8 @@ export function registerScanCommands(program: Command, context: KtxCliCommandCon ) .option('--dry-run', 'Run without writing scan results', false) .option('--database-introspection-url ', 'Daemon URL for live-database introspection') + .option('--yes', 'Install the managed Python runtime without prompting when required', false) + .option('--no-input', 'Disable interactive managed runtime installation') .showHelpAfterError() .addHelpText( 'after', @@ -126,6 +129,8 @@ export function registerScanCommands(program: Command, context: KtxCliCommandCon detectRelationships: mode === 'relationships', dryRun: options.dryRun === true, databaseIntrospectionUrl: options.databaseIntrospectionUrl, + cliVersion: context.packageInfo.version, + runtimeInstallPolicy: runtimeInstallPolicyFromFlags(options), }); }); diff --git a/packages/cli/src/dev.test.ts b/packages/cli/src/dev.test.ts index 834fb3d5..167513d5 100644 --- a/packages/cli/src/dev.test.ts +++ b/packages/cli/src/dev.test.ts @@ -234,6 +234,8 @@ describe('dev Commander tree', () => { detectRelationships: false, dryRun: true, databaseIntrospectionUrl: undefined, + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'prompt', }, scanIo.io, ); @@ -259,6 +261,8 @@ describe('dev Commander tree', () => { detectRelationships: true, dryRun: false, databaseIntrospectionUrl: undefined, + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'prompt', }, io.io, ); diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 84884743..f26215d1 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -2117,11 +2117,48 @@ describe('runKtxCli', () => { detectRelationships: false, dryRun: false, databaseIntrospectionUrl: undefined, + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'prompt', }, testIo.io, ); }); + it('routes scan managed runtime install policies', async () => { + const autoIo = makeIo(); + const neverIo = makeIo(); + const conflictIo = makeIo(); + const scan = vi.fn().mockResolvedValue(0); + + await expect(runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse', '--yes'], autoIo.io, { scan })) + .resolves.toBe(0); + await expect(runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse', '--no-input'], neverIo.io, { scan })) + .resolves.toBe(0); + await expect( + runKtxCli(['--project-dir', tempDir, 'dev', 'scan', 'warehouse', '--yes', '--no-input'], conflictIo.io, { + scan, + }), + ).resolves.toBe(1); + + expect(scan).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + command: 'run', + runtimeInstallPolicy: 'auto', + }), + autoIo.io, + ); + expect(scan).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + command: 'run', + runtimeInstallPolicy: 'never', + }), + neverIo.io, + ); + expect(conflictIo.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input'); + }); + it('dispatches serve public command options through Commander', async () => { const serveIo = makeIo(); const serveStdio = vi.fn(async () => 0); diff --git a/packages/cli/src/scan.test.ts b/packages/cli/src/scan.test.ts index 603a0091..525ae53d 100644 --- a/packages/cli/src/scan.test.ts +++ b/packages/cli/src/scan.test.ts @@ -356,6 +356,49 @@ describe('runKtxScan', () => { expect(io.stdout()).not.toContain('/~'); }); + it('passes managed daemon options to local ingest adapters when no explicit daemon URL is set', async () => { + await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' }); + const createLocalIngestAdapters = vi.fn(() => []); + const runLocalScan = vi.fn( + async (_input: RunLocalScanOptions): Promise => ({ + runId: 'scan-run-1', + status: 'done', + done: true, + connectionId: 'warehouse', + mode: 'structural', + dryRun: false, + syncId: 'sync-1', + report, + }), + ); + const io = makeIo(); + + await expect( + runKtxScan( + { + command: 'run', + projectDir: tempDir, + connectionId: 'warehouse', + mode: 'structural', + detectRelationships: false, + dryRun: false, + cliVersion: '0.2.0', + runtimeInstallPolicy: 'auto', + }, + io.io, + { runLocalScan, createLocalIngestAdapters }, + ), + ).resolves.toBe(0); + + expect(createLocalIngestAdapters).toHaveBeenCalledWith(expect.objectContaining({ projectDir: tempDir }), { + managedDaemon: { + cliVersion: '0.2.0', + installPolicy: 'auto', + io: io.io, + }, + }); + }); + it('explains warnings, capability gaps, and relationships in human scan summaries', async () => { await initKtxProject({ projectDir: tempDir, projectName: 'warehouse' }); const runLocalScan = vi.fn( diff --git a/packages/cli/src/scan.ts b/packages/cli/src/scan.ts index accccf57..f89a9d18 100644 --- a/packages/cli/src/scan.ts +++ b/packages/cli/src/scan.ts @@ -33,6 +33,7 @@ import { import type { KtxCliIo } from './index.js'; import { createKtxCliLocalIngestAdapters } from './local-adapters.js'; import { createKtxCliScanConnector } from './local-scan-connectors.js'; +import type { KtxManagedPythonInstallPolicy } from './managed-python-command.js'; import { profileMark } from './startup-profile.js'; profileMark('module:scan'); @@ -46,6 +47,8 @@ export type KtxScanArgs = detectRelationships: boolean; dryRun: boolean; databaseIntrospectionUrl?: string; + cliVersion?: string; + runtimeInstallPolicy?: KtxManagedPythonInstallPolicy; } | { command: 'status'; projectDir: string; runId: string } | { command: 'report'; projectDir: string; runId: string; json: boolean } @@ -220,6 +223,17 @@ function warningLine(warning: KtxScanWarning): string { return `${warning.code}: ${location}${warning.message}`; } +function managedDaemonOptionsForScanRun(args: Extract, io: KtxCliIo) { + if (args.databaseIntrospectionUrl || !args.cliVersion || !args.runtimeInstallPolicy) { + return undefined; + } + return { + cliVersion: args.cliVersion, + installPolicy: args.runtimeInstallPolicy, + io, + }; +} + function writeNeedsAttention(report: KtxScanReport, io: KtxCliIo): void { io.stdout.write('\nNeeds attention\n'); if (report.warnings.length === 0 && report.capabilityGaps.length === 0) { @@ -704,6 +718,7 @@ export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps return 0; } + const managedDaemon = managedDaemonOptionsForScanRun(args, io); const connector = args.mode !== 'structural' || args.detectRelationships ? await createKtxCliScanConnector(project, args.connectionId) @@ -720,7 +735,8 @@ export async function runKtxScan(args: KtxScanArgs, io: KtxCliIo = process, deps databaseIntrospectionUrl: args.databaseIntrospectionUrl, connector, adapters: (deps.createLocalIngestAdapters ?? createKtxCliLocalIngestAdapters)(project, { - databaseIntrospectionUrl: args.databaseIntrospectionUrl, + ...(args.databaseIntrospectionUrl ? { databaseIntrospectionUrl: args.databaseIntrospectionUrl } : {}), + ...(managedDaemon ? { managedDaemon } : {}), }), progress, });