ktx/packages/cli/src/setup-interrupt.ts
Andrey Avtomonov 00cdf2de90
refactor: enforce ktx naming and AGENTS.md compliance sweep (#289)
Align the tree with AGENTS.md/CLAUDE.md conventions:

- Rewrite user-facing strings, docs, and tests to lowercase `ktx`
  (no bare uppercase `KTX` tokens remain outside literal identifiers).
- Drop the legacy `historicSql` migration path and its now-unused
  helpers, per the no-backward-compat rule.
- Remove `as unknown as` / `any` casts: narrow `BaseTool` generics to
  `z.ZodObject`, add a typed `createLookerClient`, and delete the dead
  `getParametersSchema`/`toAnthropicFormat` pre-AI-SDK helpers.
- Use `InvalidArgumentError` for Commander parse failures.
- Finish the adapter→connector prose conversion in the `ktx.yaml` docs
  while keeping the literal `adapters` config key.
2026-06-11 13:49:45 +02:00

98 lines
2.6 KiB
TypeScript

import { stdin } from 'node:process';
import type { Key } from 'node:readline';
import { cancel, confirm, isCancel as isClackCancel } from '@clack/prompts';
export class KtxSetupExitError extends Error {
constructor() {
super('ktx setup exit requested');
this.name = 'KtxSetupExitError';
}
}
/** @internal */
export interface SetupInterruptTracker {
track<T>(run: () => Promise<T>): Promise<T>;
wasCtrlC(): boolean;
}
interface SetupInterruptOptions {
confirmExit?: () => Promise<boolean | symbol>;
isCancel?: (value: unknown) => value is symbol;
tracker?: SetupInterruptTracker;
}
const NON_INTERACTIVE_SETUP_MESSAGE =
'Interactive setup requires a terminal. Re-run this command in a TTY, or pass --no-input with the required options.';
function refSetupInput(input: NodeJS.ReadStream = stdin): void {
input.ref?.();
}
function createSetupInterruptTracker(input: NodeJS.ReadStream = stdin): SetupInterruptTracker {
let ctrlCPressed = false;
const onKeypress = (char: string | undefined, key: Key) => {
if (char === '\x03' || key.sequence === '\x03') {
ctrlCPressed = true;
}
};
return {
async track(run) {
ctrlCPressed = false;
input.on('keypress', onKeypress);
try {
return await run();
} finally {
input.off('keypress', onKeypress);
}
},
wasCtrlC() {
return ctrlCPressed;
},
};
}
async function defaultConfirmExit(): Promise<boolean | symbol> {
return await confirm({
message: 'Exit setup wizard?',
active: 'Yes, exit',
inactive: 'No, continue setup',
initialValue: false,
});
}
export function isKtxSetupExitError(error: unknown): error is KtxSetupExitError {
return error instanceof KtxSetupExitError;
}
export async function withSetupInterruptConfirmation<T>(
prompt: () => Promise<T | symbol>,
options: SetupInterruptOptions = {},
): Promise<T | symbol> {
if (!options.tracker && stdin.isTTY !== true) {
throw new Error(NON_INTERACTIVE_SETUP_MESSAGE);
}
const isCancel = options.isCancel ?? isClackCancel;
const tracker = options.tracker ?? createSetupInterruptTracker();
const confirmExit = options.confirmExit ?? defaultConfirmExit;
while (true) {
if (!options.tracker) {
refSetupInput();
}
const value = await tracker.track(prompt);
if (!isCancel(value)) {
return value;
}
if (!tracker.wasCtrlC()) {
return value;
}
const shouldExit = await confirmExit();
if (isCancel(shouldExit) || shouldExit === true) {
cancel('Setup cancelled.');
throw new KtxSetupExitError();
}
}
}