mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
refactor(cli): centralize Clack prompt handling
This commit is contained in:
parent
9409d50d1d
commit
42365481ac
3 changed files with 106 additions and 12 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { spinner } from '@clack/prompts';
|
||||
import { cancel, confirm, isCancel, log, spinner } from '@clack/prompts';
|
||||
|
||||
export interface KtxCliSpinner {
|
||||
start(message: string): void;
|
||||
|
|
@ -6,6 +6,62 @@ export interface KtxCliSpinner {
|
|||
error(message: string): void;
|
||||
}
|
||||
|
||||
export interface KtxCliPromptAdapter {
|
||||
confirm(options: { message: string; initialValue?: boolean }): Promise<boolean>;
|
||||
cancel(message: string): void;
|
||||
log: {
|
||||
info(message: string): void;
|
||||
warn(message: string): void;
|
||||
error(message: string): void;
|
||||
success(message: string): void;
|
||||
step(message: string): void;
|
||||
};
|
||||
spinner(): KtxCliSpinner;
|
||||
}
|
||||
|
||||
export class KtxCliPromptCancelledError extends Error {
|
||||
constructor(message = 'Operation cancelled.') {
|
||||
super(message);
|
||||
this.name = 'KtxCliPromptCancelledError';
|
||||
}
|
||||
}
|
||||
|
||||
export function createClackSpinner(): KtxCliSpinner {
|
||||
return spinner();
|
||||
}
|
||||
|
||||
export function createClackPromptAdapter(): KtxCliPromptAdapter {
|
||||
return {
|
||||
async confirm(options) {
|
||||
const value = await confirm(options);
|
||||
if (isCancel(value)) {
|
||||
cancel('Operation cancelled.');
|
||||
throw new KtxCliPromptCancelledError();
|
||||
}
|
||||
return value;
|
||||
},
|
||||
cancel(message) {
|
||||
cancel(message);
|
||||
},
|
||||
log: {
|
||||
info(message) {
|
||||
log.info(message);
|
||||
},
|
||||
warn(message) {
|
||||
log.warn(message);
|
||||
},
|
||||
error(message) {
|
||||
log.error(message);
|
||||
},
|
||||
success(message) {
|
||||
log.success(message);
|
||||
},
|
||||
step(message) {
|
||||
log.step(message);
|
||||
},
|
||||
},
|
||||
spinner() {
|
||||
return createClackSpinner();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
|
||||
expect(confirmInstall).toHaveBeenCalledWith(
|
||||
'KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
||||
io.io,
|
||||
);
|
||||
expect(installRuntime).toHaveBeenCalledWith({
|
||||
cliVersion: '0.2.0',
|
||||
|
|
@ -221,4 +222,45 @@ describe('createManagedPythonSemanticLayerComputePort', () => {
|
|||
force: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses injected runtime confirmation instead of reading process TTY directly', async () => {
|
||||
const io = makeIo();
|
||||
const compute = { query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() };
|
||||
const installRuntime = vi.fn(async (): Promise<ManagedPythonRuntimeInstallResult> => installResult());
|
||||
const confirmInstall = vi.fn(async () => true);
|
||||
|
||||
await expect(
|
||||
createManagedPythonSemanticLayerComputePort({
|
||||
cliVersion: '0.2.0',
|
||||
installPolicy: 'prompt',
|
||||
io: io.io,
|
||||
readStatus: async () => missingStatus(),
|
||||
installRuntime,
|
||||
confirmInstall,
|
||||
createPythonCompute: () => compute,
|
||||
}),
|
||||
).resolves.toBe(compute);
|
||||
|
||||
expect(confirmInstall).toHaveBeenCalledWith(
|
||||
'KTX needs to install the core Python runtime. This downloads Python dependencies with uv. Continue?',
|
||||
io.io,
|
||||
);
|
||||
expect(io.stderr()).toContain('Installing KTX Python runtime (core) with uv...');
|
||||
});
|
||||
|
||||
it('can decide default runtime prompting from injected io capabilities', async () => {
|
||||
const io = makeIo();
|
||||
Object.assign(io.io.stdout, { isTTY: false });
|
||||
|
||||
await expect(
|
||||
createManagedPythonSemanticLayerComputePort({
|
||||
cliVersion: '0.2.0',
|
||||
installPolicy: 'prompt',
|
||||
io: io.io,
|
||||
readStatus: async () => missingStatus(),
|
||||
installRuntime: vi.fn(),
|
||||
createPythonCompute: () => ({ query: vi.fn(), validateSources: vi.fn(), generateSources: vi.fn() }),
|
||||
}),
|
||||
).rejects.toThrow('KTX Python runtime installation was cancelled');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { cancel, confirm, isCancel } from '@clack/prompts';
|
||||
import { createPythonSemanticLayerComputePort, type KtxSemanticLayerComputePort } from '@ktx/context/daemon';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { createClackPromptAdapter } from './clack.js';
|
||||
import {
|
||||
installManagedPythonRuntime,
|
||||
readManagedPythonRuntimeStatus,
|
||||
|
|
@ -36,7 +36,7 @@ export interface ManagedPythonCommandRuntime {
|
|||
export interface ManagedPythonCommandDeps {
|
||||
readStatus?: (options: ManagedPythonRuntimeLayoutOptions) => Promise<ManagedPythonRuntimeStatus>;
|
||||
installRuntime?: (options: ManagedPythonRuntimeInstallOptions) => Promise<ManagedPythonRuntimeInstallResult>;
|
||||
confirmInstall?: (message: string) => Promise<boolean>;
|
||||
confirmInstall?: (message: string, io: KtxCliIo) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface ManagedPythonCommandOptions extends ManagedPythonCommandDeps {
|
||||
|
|
@ -69,16 +69,12 @@ function hasFeature(manifest: InstalledKtxRuntimeManifest, feature: KtxRuntimeFe
|
|||
return manifest.features.includes(feature);
|
||||
}
|
||||
|
||||
async function defaultConfirmInstall(message: string): Promise<boolean> {
|
||||
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
||||
async function defaultConfirmInstall(message: string, io: KtxCliIo): Promise<boolean> {
|
||||
if (io.stdout.isTTY !== true) {
|
||||
return false;
|
||||
}
|
||||
const response = await confirm({ message, initialValue: true });
|
||||
if (isCancel(response)) {
|
||||
cancel('Runtime installation cancelled.');
|
||||
return false;
|
||||
}
|
||||
return response === true;
|
||||
const prompts = createClackPromptAdapter();
|
||||
return await prompts.confirm({ message, initialValue: true });
|
||||
}
|
||||
|
||||
export async function ensureManagedPythonCommandRuntime(
|
||||
|
|
@ -99,7 +95,7 @@ export async function ensureManagedPythonCommandRuntime(
|
|||
|
||||
if (options.installPolicy === 'prompt') {
|
||||
const confirmInstall = options.confirmInstall ?? defaultConfirmInstall;
|
||||
const confirmed = await confirmInstall(installPrompt(feature));
|
||||
const confirmed = await confirmInstall(installPrompt(feature), options.io);
|
||||
if (!confirmed) {
|
||||
throw new Error(`KTX Python runtime installation was cancelled. Run: ${managedRuntimeInstallCommand(feature)}`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue