padrone 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +15 -11
- package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.mjs +4 -4
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +4 -4
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +10 -12
- package/dist/docs/index.mjs.map +1 -1
- package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
- package/dist/errors-DA4KzK1M.mjs.map +1 -0
- package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
- package/dist/help-BtxLgrF_.mjs.map +1 -0
- package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
- package/dist/index-D6-7dz0l.d.mts.map +1 -0
- package/dist/index.d.mts +869 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3884 -1699
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
- package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
- package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
- package/dist/serve-YVTPzBCl.mjs.map +1 -0
- package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
- package/dist/stream-DC4H8YTx.mjs.map +1 -0
- package/dist/test.d.mts +5 -8
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +2 -13
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +2 -2
- package/dist/zod.d.mts.map +1 -1
- package/dist/zod.mjs +2 -2
- package/dist/zod.mjs.map +1 -1
- package/package.json +15 -12
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -10
- package/src/cli/doctor.ts +22 -18
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +10 -7
- package/src/cli/link.ts +20 -16
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/schema-to-code.ts +2 -2
- package/src/{args.ts → core/args.ts} +32 -225
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +301 -0
- package/src/core/default-runtime.ts +239 -0
- package/src/{errors.ts → core/errors.ts} +22 -0
- package/src/core/exec.ts +259 -0
- package/src/core/interceptors.ts +302 -0
- package/src/{parse.ts → core/parse.ts} +36 -89
- package/src/core/program-methods.ts +301 -0
- package/src/core/results.ts +229 -0
- package/src/core/runtime.ts +246 -0
- package/src/core/validate.ts +247 -0
- package/src/docs/index.ts +12 -13
- package/src/extension/auto-output.ts +146 -0
- package/src/extension/color.ts +38 -0
- package/src/extension/completion.ts +49 -0
- package/src/extension/config.ts +262 -0
- package/src/extension/env.ts +101 -0
- package/src/extension/help.ts +192 -0
- package/src/extension/index.ts +44 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +262 -0
- package/src/extension/man.ts +51 -0
- package/src/extension/mcp.ts +52 -0
- package/src/extension/progress-renderer.ts +338 -0
- package/src/extension/progress.ts +299 -0
- package/src/extension/repl.ts +94 -0
- package/src/extension/serve.ts +48 -0
- package/src/extension/signal.ts +87 -0
- package/src/extension/stdin.ts +62 -0
- package/src/extension/suggestions.ts +114 -0
- package/src/extension/timing.ts +81 -0
- package/src/extension/tracing.ts +175 -0
- package/src/extension/update-check.ts +77 -0
- package/src/extension/utils.ts +51 -0
- package/src/extension/version.ts +63 -0
- package/src/{completion.ts → feature/completion.ts} +12 -12
- package/src/{interactive.ts → feature/interactive.ts} +4 -4
- package/src/{mcp.ts → feature/mcp.ts} +12 -15
- package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
- package/src/{serve.ts → feature/serve.ts} +11 -15
- package/src/feature/test.ts +262 -0
- package/src/{update-check.ts → feature/update-check.ts} +16 -16
- package/src/{wrap.ts → feature/wrap.ts} +10 -8
- package/src/index.ts +115 -30
- package/src/{formatter.ts → output/formatter.ts} +124 -176
- package/src/{help.ts → output/help.ts} +22 -8
- package/src/output/output-indicator.ts +87 -0
- package/src/output/primitives.ts +335 -0
- package/src/output/styling.ts +221 -0
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -276
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +718 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +60 -0
- package/src/types/interceptor.ts +296 -0
- package/src/types/preferences.ts +83 -0
- package/src/types/result.ts +71 -0
- package/src/types/schema.ts +19 -0
- package/src/util/dotenv.ts +244 -0
- package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
- package/src/{stream.ts → util/stream.ts} +27 -1
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +71 -33
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -50
- package/dist/args-D5PNDyNu.mjs.map +0 -1
- package/dist/chunk-CjcI7cDX.mjs +0 -15
- package/dist/command-utils-B1D-HqCd.mjs +0 -1117
- package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/errors-BiVrBgi6.mjs.map +0 -1
- package/dist/formatter-DtHzbP22.d.mts.map +0 -1
- package/dist/help-bbmu9-qd.mjs.map +0 -1
- package/dist/mcp-mLWIdUIu.mjs.map +0 -1
- package/dist/serve-B0u43DK7.mjs.map +0 -1
- package/dist/stream-BcC146Ud.mjs.map +0 -1
- package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
- package/dist/update-check-CFX1FV3v.mjs.map +0 -1
- package/src/command-utils.ts +0 -882
- package/src/create.ts +0 -1829
- package/src/runtime.ts +0 -497
- package/src/types.ts +0 -1291
- package/src/utils.ts +0 -140
- /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
2
|
+
import { parseCliInputToParts } from '../core/parse.ts';
|
|
3
|
+
import { withDrain } from '../core/results.ts';
|
|
4
|
+
import type {
|
|
5
|
+
AnyPadroneBuilder,
|
|
6
|
+
AnyPadroneCommand,
|
|
7
|
+
CommandTypesBase,
|
|
8
|
+
InterceptorStartContext,
|
|
9
|
+
PadroneCommand,
|
|
10
|
+
PadroneReplPreferences,
|
|
11
|
+
} from '../types/index.ts';
|
|
12
|
+
import type { PadroneSchema } from '../types/schema.ts';
|
|
13
|
+
import type { WithCommand } from '../util/type-utils.ts';
|
|
14
|
+
import { passthroughSchema } from './utils.ts';
|
|
15
|
+
|
|
16
|
+
// ── Types ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
type ReplArgs = { scope?: string };
|
|
19
|
+
|
|
20
|
+
type ReplCommand = PadroneCommand<'repl', '', PadroneSchema<ReplArgs>, void, [], [], true>;
|
|
21
|
+
|
|
22
|
+
export type WithRepl<T> = WithCommand<T, 'repl', ReplCommand>;
|
|
23
|
+
|
|
24
|
+
// ── Extension ────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extension that adds REPL support:
|
|
28
|
+
* - `repl` command that starts an interactive REPL
|
|
29
|
+
* - `--repl` flag that starts the REPL from any invocation
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* ```ts
|
|
33
|
+
* createPadrone('my-cli').extend(padroneRepl())
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function padroneRepl(
|
|
37
|
+
defaults?: PadroneReplPreferences & { disabled?: boolean },
|
|
38
|
+
): <T extends CommandTypesBase>(builder: T) => WithRepl<T> {
|
|
39
|
+
const disabled = defaults?.disabled;
|
|
40
|
+
return ((builder: AnyPadroneBuilder) =>
|
|
41
|
+
builder
|
|
42
|
+
.command('repl', (c) =>
|
|
43
|
+
c
|
|
44
|
+
.configure({ description: 'Start an interactive REPL', hidden: true })
|
|
45
|
+
.arguments(passthroughSchema({ scope: 'string' }), { positional: ['scope'] })
|
|
46
|
+
.async()
|
|
47
|
+
.action(async (args, ctx) => {
|
|
48
|
+
const prefs: PadroneReplPreferences = { ...defaults, scope: args.scope ?? defaults?.scope };
|
|
49
|
+
const repl = ctx.program.repl(prefs);
|
|
50
|
+
const { value } = await repl.drain();
|
|
51
|
+
return value;
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
.intercept(createReplInterceptor(defaults, disabled))) as any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createReplInterceptor(defaults?: PadroneReplPreferences, disabled?: boolean) {
|
|
58
|
+
return defineInterceptor({ id: 'padrone:repl', name: 'padrone:repl', order: -1000, disabled }, () => ({
|
|
59
|
+
start(ctx: InterceptorStartContext, next: () => unknown) {
|
|
60
|
+
const replInfo = checkReplFlag(ctx.input, ctx.command);
|
|
61
|
+
if (!replInfo) return next();
|
|
62
|
+
|
|
63
|
+
const program = ctx.program;
|
|
64
|
+
if (!program?.repl) return next();
|
|
65
|
+
|
|
66
|
+
const prefs: PadroneReplPreferences = { ...defaults, scope: replInfo.scope ?? defaults?.scope };
|
|
67
|
+
|
|
68
|
+
// Return a Promise so the pipeline awaits the REPL result
|
|
69
|
+
return program
|
|
70
|
+
.repl(prefs)
|
|
71
|
+
.drain()
|
|
72
|
+
.then((r: any) => withDrain({ command: ctx.command, args: undefined, result: r.value }));
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Check for --repl flag in input. */
|
|
78
|
+
function checkReplFlag(input: string | undefined, rootCommand: AnyPadroneCommand): { scope?: string } | null {
|
|
79
|
+
if (!input) return null;
|
|
80
|
+
|
|
81
|
+
const parts = parseCliInputToParts(input);
|
|
82
|
+
const terms = parts.filter((p) => p.type === 'term').map((p) => p.value);
|
|
83
|
+
const args = parts.filter((p) => p.type === 'named');
|
|
84
|
+
const keyIs = (key: string[], name: string) => key.length === 1 && key[0] === name;
|
|
85
|
+
|
|
86
|
+
const hasReplFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'repl'));
|
|
87
|
+
if (!hasReplFlag) return null;
|
|
88
|
+
|
|
89
|
+
const normalizedTerms = [...terms];
|
|
90
|
+
if (normalizedTerms[0] === rootCommand.name) normalizedTerms.shift();
|
|
91
|
+
|
|
92
|
+
const scope = normalizedTerms.length > 0 ? normalizedTerms.join(' ') : undefined;
|
|
93
|
+
return { scope };
|
|
94
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { resolveAllCommands } from '../core/commands.ts';
|
|
2
|
+
import type { PadroneServePreferences } from '../feature/serve.ts';
|
|
3
|
+
import type { AnyPadroneBuilder, CommandTypesBase, PadroneCommand } from '../types/index.ts';
|
|
4
|
+
import type { PadroneSchema } from '../types/schema.ts';
|
|
5
|
+
import type { WithCommand } from '../util/type-utils.ts';
|
|
6
|
+
import { getRootCommand } from '../util/utils.ts';
|
|
7
|
+
import { passthroughSchema } from './utils.ts';
|
|
8
|
+
|
|
9
|
+
// ── Types ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
type ServeArgs = { port?: string; host?: string; basePath?: string };
|
|
12
|
+
|
|
13
|
+
type ServeCommand = PadroneCommand<'serve', '', PadroneSchema<ServeArgs>, void, [], [], true>;
|
|
14
|
+
|
|
15
|
+
export type WithServe<T> = WithCommand<T, 'serve', ServeCommand>;
|
|
16
|
+
|
|
17
|
+
// ── Extension ────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extension that adds the `serve` command for starting a REST HTTP server.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```ts
|
|
24
|
+
* createPadrone('my-cli').extend(padroneServe())
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function padroneServe(defaults?: PadroneServePreferences): <T extends CommandTypesBase>(builder: T) => WithServe<T> {
|
|
28
|
+
return ((builder: AnyPadroneBuilder) =>
|
|
29
|
+
builder.command('serve', (c) =>
|
|
30
|
+
c
|
|
31
|
+
.configure({ description: 'Start a REST HTTP server', hidden: true })
|
|
32
|
+
.arguments(passthroughSchema({ port: 'string', host: 'string', 'base-path': 'string' }))
|
|
33
|
+
.async()
|
|
34
|
+
.action(async (args, ctx) => {
|
|
35
|
+
const rootCommand = getRootCommand(ctx.command);
|
|
36
|
+
resolveAllCommands(rootCommand);
|
|
37
|
+
const { startServeServer } = await import('../feature/serve.ts');
|
|
38
|
+
const port = args.port ? parseInt(args.port, 10) : undefined;
|
|
39
|
+
const prefs: PadroneServePreferences = {
|
|
40
|
+
...defaults,
|
|
41
|
+
port: port && !Number.isNaN(port) ? port : defaults?.port,
|
|
42
|
+
host: args.host ?? defaults?.host,
|
|
43
|
+
basePath: args['base-path'] ?? defaults?.basePath,
|
|
44
|
+
};
|
|
45
|
+
await startServeServer(ctx.program, rootCommand, ctx.program.eval, prefs);
|
|
46
|
+
}),
|
|
47
|
+
)) as any;
|
|
48
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { SignalError, signalExitCode } from '../core/errors.ts';
|
|
2
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
3
|
+
import { thenMaybe } from '../core/results.ts';
|
|
4
|
+
import type { PadroneSignal } from '../core/runtime.ts';
|
|
5
|
+
import type { AnyPadroneBuilder, CommandTypesBase } from '../types/index.ts';
|
|
6
|
+
|
|
7
|
+
// ── Interceptor ─────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const signalMeta = { id: 'padrone:signal', name: 'padrone:signal', order: -2000 } as const;
|
|
10
|
+
|
|
11
|
+
const signalInterceptor = defineInterceptor(signalMeta, () => {
|
|
12
|
+
const abortController = new AbortController();
|
|
13
|
+
let receivedSignal: PadroneSignal | undefined;
|
|
14
|
+
let lastSigintTime = 0;
|
|
15
|
+
let unsubscribe: (() => void) | undefined;
|
|
16
|
+
const DOUBLE_SIGINT_MS = 2000;
|
|
17
|
+
|
|
18
|
+
const cleanup = () => {
|
|
19
|
+
unsubscribe?.();
|
|
20
|
+
unsubscribe = undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const attachSignalInfo = <T>(result: T): T => {
|
|
24
|
+
if (receivedSignal && result && typeof result === 'object') {
|
|
25
|
+
(result as any).signal = receivedSignal;
|
|
26
|
+
(result as any).exitCode = signalExitCode(receivedSignal);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
start(ctx, next) {
|
|
33
|
+
const runtimeExit = ctx.runtime.exit;
|
|
34
|
+
unsubscribe = ctx.runtime.onSignal?.((sig) => {
|
|
35
|
+
if (abortController.signal.aborted) {
|
|
36
|
+
if (sig === 'SIGINT') {
|
|
37
|
+
const elapsed = Date.now() - lastSigintTime;
|
|
38
|
+
if (elapsed > 0 && elapsed < DOUBLE_SIGINT_MS) {
|
|
39
|
+
runtimeExit?.(signalExitCode(sig));
|
|
40
|
+
}
|
|
41
|
+
lastSigintTime = Date.now();
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (sig === 'SIGINT') lastSigintTime = Date.now();
|
|
46
|
+
receivedSignal = sig;
|
|
47
|
+
abortController.abort(sig);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = next({ signal: abortController.signal });
|
|
51
|
+
return thenMaybe(result, (r) => {
|
|
52
|
+
cleanup();
|
|
53
|
+
return attachSignalInfo(r);
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
error(_ctx, next) {
|
|
57
|
+
return thenMaybe(next(), (er) => {
|
|
58
|
+
if (receivedSignal && er.error instanceof Error) {
|
|
59
|
+
er.error = new SignalError(receivedSignal, { cause: er.error });
|
|
60
|
+
}
|
|
61
|
+
return er;
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
shutdown(_ctx, next) {
|
|
65
|
+
cleanup();
|
|
66
|
+
return next();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── Extension ───────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extension that wires process signal handling (SIGINT, SIGTERM, SIGHUP) into the interceptor lifecycle.
|
|
75
|
+
*
|
|
76
|
+
* - Creates an `AbortController` whose signal is propagated to all downstream phases.
|
|
77
|
+
* - Subscribes to `runtime.onSignal` to forward OS signals to the abort controller.
|
|
78
|
+
* - Implements SIGINT double-tap: two SIGINTs within 2 seconds force-exits the process.
|
|
79
|
+
* - Attaches `signal` and `exitCode` to results and errors when interrupted.
|
|
80
|
+
* - Cleans up the signal subscription on completion or failure.
|
|
81
|
+
*
|
|
82
|
+
* Included in the default extensions. Runs at order `-2000` (outermost).
|
|
83
|
+
*/
|
|
84
|
+
export function padroneSignalHandling(options?: { disabled?: boolean }): <T extends CommandTypesBase>(builder: T) => T {
|
|
85
|
+
const interceptor = options?.disabled ? defineInterceptor({ ...signalMeta, disabled: true }, () => ({})) : signalInterceptor;
|
|
86
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any;
|
|
87
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { applyValues, isArrayField, isAsyncStreamField } from '../core/args.ts';
|
|
2
|
+
import { resolveStdin, resolveStdinAlways } from '../core/default-runtime.ts';
|
|
3
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
4
|
+
import type { AnyPadroneBuilder, CommandTypesBase, InterceptorValidateContext } from '../types/index.ts';
|
|
5
|
+
import { createStdinStream } from '../util/stream.ts';
|
|
6
|
+
|
|
7
|
+
// ── Interceptor ─────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const stdinInterceptor = defineInterceptor({ id: 'padrone:stdin', name: 'padrone:stdin', order: -1001 }, () => ({
|
|
10
|
+
validate(ctx: InterceptorValidateContext, next) {
|
|
11
|
+
const stdinField = ctx.command.meta?.stdin;
|
|
12
|
+
if (!stdinField) return next();
|
|
13
|
+
|
|
14
|
+
// Skip if the field was already provided via CLI flags
|
|
15
|
+
if (stdinField in ctx.rawArgs && ctx.rawArgs[stdinField] !== undefined) return next();
|
|
16
|
+
|
|
17
|
+
const streamInfo = isAsyncStreamField(ctx.command.argsSchema, stdinField);
|
|
18
|
+
if (streamInfo) {
|
|
19
|
+
const stdinForStream = resolveStdinAlways(ctx.runtime as any);
|
|
20
|
+
const mergedRawArgs = applyValues(ctx.rawArgs, { [stdinField]: createStdinStream(stdinForStream, streamInfo.itemSchema) });
|
|
21
|
+
return next({ rawArgs: mergedRawArgs });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const stdin = resolveStdin(ctx.runtime as any);
|
|
25
|
+
if (!stdin) return next();
|
|
26
|
+
|
|
27
|
+
if (isArrayField(ctx.command.argsSchema, stdinField)) {
|
|
28
|
+
return (async () => {
|
|
29
|
+
const lines: string[] = [];
|
|
30
|
+
for await (const line of stdin.lines()) {
|
|
31
|
+
lines.push(line);
|
|
32
|
+
}
|
|
33
|
+
const mergedRawArgs = applyValues(ctx.rawArgs, { [stdinField]: lines });
|
|
34
|
+
return next({ rawArgs: mergedRawArgs });
|
|
35
|
+
})();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return stdin.text().then((text) => {
|
|
39
|
+
if (!text) return next();
|
|
40
|
+
const mergedRawArgs = applyValues(ctx.rawArgs, { [stdinField]: text });
|
|
41
|
+
return next({ rawArgs: mergedRawArgs });
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
// ── Extension ────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extension that reads stdin data into the argument field specified by `meta.stdin`.
|
|
50
|
+
* Included by default via `createPadrone()`.
|
|
51
|
+
*
|
|
52
|
+
* Read mode is inferred from the schema type:
|
|
53
|
+
* - `string` field → reads all stdin as a single string
|
|
54
|
+
* - `string[]` field → reads stdin line-by-line into an array
|
|
55
|
+
* - `AsyncIterable` field → returns a stream for line-by-line async consumption
|
|
56
|
+
*
|
|
57
|
+
* Stdin is only read when piped (not a TTY) and the field wasn't already provided via CLI flags.
|
|
58
|
+
*/
|
|
59
|
+
export function padroneStdin(options?: { disabled?: boolean }): <T extends CommandTypesBase>(builder: T) => T {
|
|
60
|
+
const interceptor = options?.disabled ? defineInterceptor({ ...stdinInterceptor, disabled: true }, () => ({})) : stdinInterceptor;
|
|
61
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any;
|
|
62
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { resolveCommand, suggestSimilar } from '../core/commands.ts';
|
|
2
|
+
import { RoutingError } from '../core/errors.ts';
|
|
3
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
4
|
+
import { thenMaybe } from '../core/results.ts';
|
|
5
|
+
import { getKnownOptionNames } from '../core/validate.ts';
|
|
6
|
+
import type { AnyPadroneBuilder, AnyPadroneCommand, CommandTypesBase } from '../types/index.ts';
|
|
7
|
+
|
|
8
|
+
function formatSuggestions(names: string[], prefix = ''): string {
|
|
9
|
+
if (names.length === 0) return '';
|
|
10
|
+
const quoted = names.map((n) => `"${prefix}${n}"`);
|
|
11
|
+
if (quoted.length === 1) return `Did you mean ${quoted[0]}?`;
|
|
12
|
+
return `Did you mean ${quoted.slice(0, -1).join(', ')} or ${quoted.at(-1)}?`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function findSourceCommand(commandPath: string | undefined, root: AnyPadroneCommand): AnyPadroneCommand {
|
|
16
|
+
if (!commandPath || commandPath === root.name || commandPath === root.path) return root;
|
|
17
|
+
const parts = commandPath.split(' ');
|
|
18
|
+
let current = root;
|
|
19
|
+
for (const part of parts) {
|
|
20
|
+
const found = current.commands?.find((c) => {
|
|
21
|
+
resolveCommand(c);
|
|
22
|
+
return c.name === part || c.aliases?.includes(part);
|
|
23
|
+
});
|
|
24
|
+
if (found) current = found;
|
|
25
|
+
else break;
|
|
26
|
+
}
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function enrichRoutingError(err: unknown, rootCommand: AnyPadroneCommand): unknown {
|
|
31
|
+
if (!(err instanceof RoutingError)) return err;
|
|
32
|
+
|
|
33
|
+
const unknownMatch = err.message.match(/^Unknown command: (\S+)/);
|
|
34
|
+
const unexpectedMatch = err.message.match(/^Unexpected arguments for '[^']+': (\S+)/);
|
|
35
|
+
const term = unknownMatch?.[1] ?? unexpectedMatch?.[1];
|
|
36
|
+
if (!term) return err;
|
|
37
|
+
|
|
38
|
+
const sourceCmd = findSourceCommand(err.command, rootCommand);
|
|
39
|
+
|
|
40
|
+
const candidateNames: string[] = [];
|
|
41
|
+
if (sourceCmd.commands) {
|
|
42
|
+
for (const cmd of sourceCmd.commands) {
|
|
43
|
+
resolveCommand(cmd);
|
|
44
|
+
if (!cmd.hidden) {
|
|
45
|
+
candidateNames.push(cmd.name);
|
|
46
|
+
if (cmd.aliases) candidateNames.push(...cmd.aliases);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const similar = suggestSimilar(term, candidateNames);
|
|
52
|
+
const suggestionText = formatSuggestions(similar);
|
|
53
|
+
if (!suggestionText) return err;
|
|
54
|
+
|
|
55
|
+
const suggestions = [suggestionText];
|
|
56
|
+
const enrichedMsg = `${err.message}\n\n ${suggestionText}`;
|
|
57
|
+
return new RoutingError(enrichedMsg, { suggestions, command: err.command });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function enrichIssuesWithSuggestions(
|
|
61
|
+
issues: readonly { path?: readonly unknown[]; message: string }[],
|
|
62
|
+
knownOptions: () => string[],
|
|
63
|
+
): typeof issues {
|
|
64
|
+
return issues.map((i: any) => {
|
|
65
|
+
// Handle direct unknown option detection (from checkUnknownArgs)
|
|
66
|
+
const unknownMatch = i.message?.match(/^Unknown option: "([^"]+)"$/);
|
|
67
|
+
if (unknownMatch) {
|
|
68
|
+
const similar = suggestSimilar(unknownMatch[1], knownOptions());
|
|
69
|
+
if (similar.length) {
|
|
70
|
+
const hint = formatSuggestions(similar, '--');
|
|
71
|
+
return { ...i, message: `${i.message} ${hint}` };
|
|
72
|
+
}
|
|
73
|
+
return i;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle Zod strict schema errors (Unrecognized key(s) in object: "foo")
|
|
77
|
+
const keys: string[] | undefined = i.keys ?? i.message?.match(/[Uu]nrecognized key(?:s)?[^"]*"([^"]+)"/)?.slice(1);
|
|
78
|
+
if (!keys?.length) return i;
|
|
79
|
+
const hints = keys.flatMap((k: string) => {
|
|
80
|
+
const similar = suggestSimilar(k, knownOptions());
|
|
81
|
+
return similar.length ? [formatSuggestions(similar, '--')] : [];
|
|
82
|
+
});
|
|
83
|
+
if (!hints.length) return i;
|
|
84
|
+
return { ...i, message: `${i.message} ${hints.join(' ')}` };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const suggestionsInterceptor = defineInterceptor({ id: 'padrone:suggestions', name: 'padrone:suggestions', order: -500 }, () => ({
|
|
89
|
+
parse(ctx, next) {
|
|
90
|
+
try {
|
|
91
|
+
const result = next();
|
|
92
|
+
if (result instanceof Promise) {
|
|
93
|
+
return result.catch((err: unknown) => {
|
|
94
|
+
throw enrichRoutingError(err, ctx.command);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
throw enrichRoutingError(err, ctx.command);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
validate(ctx, next) {
|
|
103
|
+
const result = next();
|
|
104
|
+
return thenMaybe(result, (v) => {
|
|
105
|
+
if (!v.argsResult?.issues?.length) return v;
|
|
106
|
+
const enriched = enrichIssuesWithSuggestions(v.argsResult.issues, () => getKnownOptionNames(ctx.command));
|
|
107
|
+
return { ...v, argsResult: { ...v.argsResult, issues: enriched } } as typeof v;
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
export function padroneSuggestions(): <T extends CommandTypesBase>(builder: T) => T {
|
|
113
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(suggestionsInterceptor)) as any;
|
|
114
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
2
|
+
import { thenMaybe } from '../core/results.ts';
|
|
3
|
+
import type { AnyPadroneBuilder, CommandTypesBase } from '../types/index.ts';
|
|
4
|
+
|
|
5
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
function formatDuration(ms: number): string {
|
|
8
|
+
if (ms < 1000) return `${ms.toFixed(0)}ms`;
|
|
9
|
+
if (ms < 60_000) return `${(ms / 1000).toFixed(2)}s`;
|
|
10
|
+
const mins = Math.floor(ms / 60_000);
|
|
11
|
+
const secs = ((ms % 60_000) / 1000).toFixed(2);
|
|
12
|
+
return `${mins}m ${secs}s`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Interceptor ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const timingMeta = { id: 'padrone:timing', name: 'padrone:timing', order: -1002 } as const;
|
|
18
|
+
|
|
19
|
+
function createTimingInterceptor(enabledByDefault: boolean) {
|
|
20
|
+
return defineInterceptor(timingMeta, () => {
|
|
21
|
+
let enabled = enabledByDefault;
|
|
22
|
+
let startTime = 0;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
parse(_ctx, next) {
|
|
26
|
+
return thenMaybe(next(), (res) => {
|
|
27
|
+
if ('timing' in res.rawArgs) {
|
|
28
|
+
enabled = res.rawArgs.timing !== false;
|
|
29
|
+
delete res.rawArgs.timing;
|
|
30
|
+
}
|
|
31
|
+
if ('time' in res.rawArgs) {
|
|
32
|
+
enabled = res.rawArgs.time !== false;
|
|
33
|
+
delete res.rawArgs.time;
|
|
34
|
+
}
|
|
35
|
+
return res;
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
start(_ctx, next) {
|
|
39
|
+
startTime = performance.now();
|
|
40
|
+
return next();
|
|
41
|
+
},
|
|
42
|
+
shutdown(ctx, next) {
|
|
43
|
+
return thenMaybe(next(), (res) => {
|
|
44
|
+
if (enabled) {
|
|
45
|
+
const elapsed = performance.now() - startTime;
|
|
46
|
+
ctx.runtime.error(`\nDone in ${formatDuration(elapsed)}`);
|
|
47
|
+
}
|
|
48
|
+
return res;
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Extension ───────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface PadroneTimingOptions {
|
|
58
|
+
/** Enable timing by default without requiring `--time` flag. Default: `false`. */
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extension that tracks command execution time.
|
|
64
|
+
*
|
|
65
|
+
* - `--time` / `--timing` → enables timing output
|
|
66
|
+
* - `--no-time` / `--no-timing` → disables timing output
|
|
67
|
+
*
|
|
68
|
+
* Pass `{ enabled: true }` to enable timing by default (can be disabled via `--no-time`).
|
|
69
|
+
*
|
|
70
|
+
* Usage:
|
|
71
|
+
* ```ts
|
|
72
|
+
* // Opt-in via flag
|
|
73
|
+
* createPadrone('my-cli').extend(padroneTiming())
|
|
74
|
+
*
|
|
75
|
+
* // Always on, opt-out via --no-time
|
|
76
|
+
* createPadrone('my-cli').extend(padroneTiming({ enabled: true }))
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export function padroneTiming(options?: PadroneTimingOptions): <T extends CommandTypesBase>(builder: T) => T {
|
|
80
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(createTimingInterceptor(options?.enabled ?? false))) as any;
|
|
81
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { defineInterceptor } from '#src/core/interceptors.ts';
|
|
2
|
+
import { thenMaybe } from '#src/core/results.ts';
|
|
3
|
+
import type { AnyPadroneBuilder, CommandTypesBase } from '#src/types/index.ts';
|
|
4
|
+
import type { WithInterceptor } from '#src/util/type-utils.ts';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Types — minimal OTEL-compatible interfaces so we don't hard-depend on
|
|
8
|
+
// `@opentelemetry/api`. Users pass their real Tracer / Span instances.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/** Minimal subset of OTEL `SpanStatusCode`. */
|
|
12
|
+
type SpanStatusCode = 0 | 1 | 2;
|
|
13
|
+
|
|
14
|
+
/** Minimal subset of OTEL `SpanStatus`. */
|
|
15
|
+
type SpanStatus = { code: SpanStatusCode; message?: string };
|
|
16
|
+
|
|
17
|
+
/** Minimal subset of OTEL `Span`. */
|
|
18
|
+
export interface OtelSpan {
|
|
19
|
+
setAttribute(key: string, value: string | number | boolean): this;
|
|
20
|
+
addEvent(name: string, attributes?: Record<string, string | number | boolean>): this;
|
|
21
|
+
setStatus(status: SpanStatus): this;
|
|
22
|
+
recordException(error: unknown): this;
|
|
23
|
+
end(): void;
|
|
24
|
+
spanContext(): { traceId: string; spanId: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Minimal subset of OTEL `Tracer`. */
|
|
28
|
+
export interface OtelTracer {
|
|
29
|
+
startSpan(name: string): OtelSpan;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Minimal subset of OTEL `TracerProvider`. */
|
|
33
|
+
export interface OtelTracerProvider {
|
|
34
|
+
getTracer(name: string, version?: string): OtelTracer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Tracing handle injected into the command context. */
|
|
38
|
+
export type PadroneTracer = {
|
|
39
|
+
/** The underlying OTEL tracer. */
|
|
40
|
+
tracer: OtelTracer;
|
|
41
|
+
/** Root span covering the full command execution. */
|
|
42
|
+
rootSpan: OtelSpan;
|
|
43
|
+
/** Run `fn` inside a child span that is automatically ended when `fn` returns (or rejects). */
|
|
44
|
+
span: <T>(name: string, fn: (span: OtelSpan) => T) => T;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Configuration for the tracing extension. */
|
|
48
|
+
export type PadroneTracingConfig = {
|
|
49
|
+
/** OTEL `TracerProvider`. Required — there is no global fallback. */
|
|
50
|
+
provider: OtelTracerProvider;
|
|
51
|
+
/** Service / tracer name. Defaults to the CLI program name. */
|
|
52
|
+
serviceName?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Builder/program type after applying `padroneTracing()`. Adds `{ tracing: PadroneTracer }` to the command context. */
|
|
56
|
+
export type WithTracing<T> = WithInterceptor<T, { tracing: PadroneTracer }>;
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Interceptor
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const OTEL_ERROR: SpanStatusCode = 2;
|
|
63
|
+
|
|
64
|
+
type ResolvedTracingConfig = { provider: OtelTracerProvider; serviceName: string | undefined };
|
|
65
|
+
|
|
66
|
+
function tracingInterceptor(config: ResolvedTracingConfig) {
|
|
67
|
+
return defineInterceptor({ id: 'padrone:tracing', name: 'padrone:tracing', order: -1 }, () => {
|
|
68
|
+
let rootSpan: OtelSpan;
|
|
69
|
+
let tracer: OtelTracer;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
start(ctx, next) {
|
|
73
|
+
tracer = config.provider.getTracer(config.serviceName ?? ctx.command.name);
|
|
74
|
+
return next();
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
execute(ctx, next) {
|
|
78
|
+
rootSpan = tracer.startSpan(`cli ${ctx.command.name}`);
|
|
79
|
+
|
|
80
|
+
const padroneTracer: PadroneTracer = {
|
|
81
|
+
tracer,
|
|
82
|
+
rootSpan,
|
|
83
|
+
span(name, fn) {
|
|
84
|
+
const child = tracer.startSpan(name);
|
|
85
|
+
try {
|
|
86
|
+
const result = fn(child);
|
|
87
|
+
if (result != null && typeof (result as any).then === 'function') {
|
|
88
|
+
return (result as any).then(
|
|
89
|
+
(v: any) => {
|
|
90
|
+
child.end();
|
|
91
|
+
return v;
|
|
92
|
+
},
|
|
93
|
+
(err: unknown) => {
|
|
94
|
+
child.recordException(err);
|
|
95
|
+
child.setStatus({ code: OTEL_ERROR });
|
|
96
|
+
child.end();
|
|
97
|
+
throw err;
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
child.end();
|
|
102
|
+
return result;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
child.recordException(err);
|
|
105
|
+
child.setStatus({ code: OTEL_ERROR });
|
|
106
|
+
child.end();
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return next({ context: { ...(ctx.context as any), tracing: padroneTracer } });
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
error(ctx, next) {
|
|
116
|
+
rootSpan?.recordException(ctx.error);
|
|
117
|
+
rootSpan?.setStatus({ code: OTEL_ERROR });
|
|
118
|
+
return next();
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
shutdown(_ctx, next) {
|
|
122
|
+
return thenMaybe(next(), (res) => {
|
|
123
|
+
rootSpan?.end();
|
|
124
|
+
return res;
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Extension
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extension that adds OpenTelemetry tracing to command execution.
|
|
137
|
+
*
|
|
138
|
+
* Creates a root span for each command invocation and provides a `PadroneTracer`
|
|
139
|
+
* on the command context for creating child spans in action handlers.
|
|
140
|
+
*
|
|
141
|
+
* When used with `padroneLogger()`, the logger automatically emits span events
|
|
142
|
+
* for each log call — no extra configuration needed. The logger detects the
|
|
143
|
+
* tracing context and bridges log output to span events.
|
|
144
|
+
*
|
|
145
|
+
* Uses minimal OTEL-compatible interfaces — pass any `TracerProvider` that
|
|
146
|
+
* implements `getTracer()`. Works with `@opentelemetry/api` or compatible
|
|
147
|
+
* libraries.
|
|
148
|
+
*
|
|
149
|
+
* Provides `{ tracing: PadroneTracer }` on the command context.
|
|
150
|
+
* Access it in action handlers as `ctx.context.tracing`.
|
|
151
|
+
*
|
|
152
|
+
* Usage:
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { trace } from '@opentelemetry/api';
|
|
155
|
+
*
|
|
156
|
+
* createPadrone('my-cli')
|
|
157
|
+
* .extend(padroneTracing({ provider: trace.getTracerProvider() }))
|
|
158
|
+
* .extend(padroneLogger())
|
|
159
|
+
* .command('deploy', (c) =>
|
|
160
|
+
* c.action((_args, ctx) => {
|
|
161
|
+
* ctx.context.logger.info('deploying'); // also emits a span event
|
|
162
|
+
* ctx.context.tracing.span('build', (span) => {
|
|
163
|
+
* span.setAttribute('target', 'production');
|
|
164
|
+
* });
|
|
165
|
+
* })
|
|
166
|
+
* )
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export function padroneTracing<T extends CommandTypesBase>(config: PadroneTracingConfig): (builder: T) => WithTracing<T> {
|
|
170
|
+
const resolved: ResolvedTracingConfig = {
|
|
171
|
+
provider: config.provider,
|
|
172
|
+
serviceName: config.serviceName,
|
|
173
|
+
};
|
|
174
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(tracingInterceptor(resolved))) as any;
|
|
175
|
+
}
|