import { defineInterceptor } from '../core/interceptors.ts'; import { parseCliInputToParts } from '../core/parse.ts'; import { withDrain } from '../core/results.ts'; import type { AnyPadroneBuilder, AnyPadroneCommand, CommandTypesBase, InterceptorStartContext, PadroneCommand, PadroneReplPreferences, } from '../types/index.ts'; import type { PadroneSchema } from '../types/schema.ts'; import type { WithCommand } from '../util/type-utils.ts'; import { passthroughSchema } from './utils.ts'; // ── Types ──────────────────────────────────────────────────────────────── type ReplArgs = { scope?: string }; type ReplCommand = PadroneCommand<'repl', '', PadroneSchema, void, [], [], true>; export type WithRepl = WithCommand; // ── Extension ──────────────────────────────────────────────────────────── /** * Extension that adds REPL support: * - `repl` command that starts an interactive REPL * - `--repl` flag that starts the REPL from any invocation * * Usage: * ```ts * createPadrone('my-cli').extend(padroneRepl()) * ``` */ export function padroneRepl( defaults?: PadroneReplPreferences & { disabled?: boolean }, ): (builder: T) => WithRepl { const disabled = defaults?.disabled; return ((builder: AnyPadroneBuilder) => builder .command('repl', (c) => c .configure({ description: 'Start an interactive REPL', hidden: true }) .arguments(passthroughSchema({ scope: 'string' }), { positional: ['scope'] }) .async() .action(async (args, ctx) => { const prefs: PadroneReplPreferences = { ...defaults, scope: args.scope ?? defaults?.scope }; const repl = ctx.program.repl(prefs); const { value } = await repl.drain(); return value; }), ) .intercept(createReplInterceptor(defaults, disabled))) as any; } function createReplInterceptor(defaults?: PadroneReplPreferences, disabled?: boolean) { return defineInterceptor({ id: 'padrone:repl', name: 'padrone:repl', order: -1000, disabled }, () => ({ start(ctx: InterceptorStartContext, next: () => unknown) { const replInfo = checkReplFlag(ctx.input, ctx.command); if (!replInfo) return next(); const program = ctx.program; if (!program?.repl) return next(); const prefs: PadroneReplPreferences = { ...defaults, scope: replInfo.scope ?? defaults?.scope }; // Return a Promise so the pipeline awaits the REPL result return program .repl(prefs) .drain() .then((r: any) => withDrain({ command: ctx.command, args: undefined, result: r.value })); }, })); } /** Check for --repl flag in input. */ function checkReplFlag(input: string | undefined, rootCommand: AnyPadroneCommand): { scope?: string } | null { if (!input) return null; const parts = parseCliInputToParts(input); const terms = parts.filter((p) => p.type === 'term').map((p) => p.value); const args = parts.filter((p) => p.type === 'named'); const keyIs = (key: string[], name: string) => key.length === 1 && key[0] === name; const hasReplFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'repl')); if (!hasReplFlag) return null; const normalizedTerms = [...terms]; if (normalizedTerms[0] === rootCommand.name) normalizedTerms.shift(); const scope = normalizedTerms.length > 0 ? normalizedTerms.join(' ') : undefined; return { scope }; }