From b1ca79ac9c63c3943c01e9b90dfd89b763e52efa Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Mon, 11 May 2026 10:22:09 +0200 Subject: [PATCH] feat: route sl query managed runtime policy --- packages/cli/src/command-schemas.ts | 2 + packages/cli/src/commands/sl-commands.ts | 15 ++++++ packages/cli/src/index.test.ts | 65 ++++++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/packages/cli/src/command-schemas.ts b/packages/cli/src/command-schemas.ts index 0e251b96..1a442af7 100644 --- a/packages/cli/src/command-schemas.ts +++ b/packages/cli/src/command-schemas.ts @@ -64,6 +64,8 @@ export const slQueryCommandSchema = z.object({ }), format: z.enum(['json', 'sql']), execute: z.boolean(), + cliVersion: z.string().min(1), + runtimeInstallPolicy: z.enum(['prompt', 'auto', 'never']), maxRows: z.number().int().positive().optional(), }); diff --git a/packages/cli/src/commands/sl-commands.ts b/packages/cli/src/commands/sl-commands.ts index 24ab95aa..8627dd83 100644 --- a/packages/cli/src/commands/sl-commands.ts +++ b/packages/cli/src/commands/sl-commands.ts @@ -6,6 +6,7 @@ import { resolveCommandProjectDir, } from '../cli-program.js'; import { slQueryCommandSchema } from '../command-schemas.js'; +import type { KtxManagedPythonInstallPolicy } from '../managed-python-command.js'; import type { KtxSlArgs } from '../sl.js'; import { profileMark } from '../startup-profile.js'; @@ -32,6 +33,16 @@ function collectOrderBy( return [...previous, parseOrderBy(value)]; } +function runtimeInstallPolicy(options: { yes?: boolean; input?: boolean }): KtxManagedPythonInstallPolicy { + if (options.yes === true && options.input === false) { + throw new Error('Choose only one runtime install mode: --yes or --no-input'); + } + if (options.yes === true) { + return 'auto'; + } + return options.input === false ? 'never' : 'prompt'; +} + async function runSlArgs(context: KtxCliCommandContext, args: KtxSlArgs): Promise { const runner = context.deps.sl ?? (await import('../sl.js')).runKtxSl; context.setExitCode(await runner(args, context.io)); @@ -121,6 +132,8 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte .option('--include-empty', 'Include empty rows', false) .addOption(new Option('--format ', 'json or sql').choices(['json', 'sql']).default('json')) .option('--execute', 'Execute the compiled query', false) + .option('--yes', 'Install the managed Python runtime without prompting when required', false) + .option('--no-input', 'Disable interactive managed runtime installation') .option('--max-rows ', 'Maximum rows to return when executing', parsePositiveIntegerOption) .action(async (options, command) => { if (options.measure.length === 0) { @@ -141,6 +154,8 @@ export function registerSlCommands(program: Command, context: KtxCliCommandConte }, format: options.format, execute: options.execute === true, + cliVersion: context.packageInfo.version, + runtimeInstallPolicy: runtimeInstallPolicy(options), ...(options.maxRows !== undefined ? { maxRows: options.maxRows } : {}), }); await runSlArgs(context, args); diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 2e23216a..7019f8d6 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -178,6 +178,71 @@ describe('runKtxCli', () => { ); }); + it('routes sl query managed runtime install policies', async () => { + const sl = vi.fn(async () => 0); + + const promptIo = makeIo(); + await expect( + runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count'], promptIo.io, { sl }), + ).resolves.toBe(0); + expect(sl).toHaveBeenLastCalledWith( + expect.objectContaining({ + command: 'query', + projectDir: tempDir, + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'prompt', + query: expect.objectContaining({ measures: ['orders.order_count'], dimensions: [] }), + }), + promptIo.io, + ); + + const autoIo = makeIo(); + await expect( + runKtxCli(['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--yes'], autoIo.io, { + sl, + }), + ).resolves.toBe(0); + expect(sl).toHaveBeenLastCalledWith( + expect.objectContaining({ + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'auto', + }), + autoIo.io, + ); + + const noInputIo = makeIo(); + await expect( + runKtxCli( + ['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--no-input'], + noInputIo.io, + { sl }, + ), + ).resolves.toBe(0); + expect(sl).toHaveBeenLastCalledWith( + expect.objectContaining({ + cliVersion: '0.0.0-private', + runtimeInstallPolicy: 'never', + }), + noInputIo.io, + ); + }); + + it('rejects conflicting sl query runtime install flags', async () => { + const io = makeIo(); + const sl = vi.fn(async () => 0); + + await expect( + runKtxCli( + ['--project-dir', tempDir, 'sl', 'query', '--measure', 'orders.order_count', '--yes', '--no-input'], + io.io, + { sl }, + ), + ).resolves.toBe(1); + + expect(sl).not.toHaveBeenCalled(); + expect(io.stderr()).toContain('Choose only one runtime install mode: --yes or --no-input'); + }); + it('exposes demo under setup help instead of root help', async () => { const testIo = makeIo();