padrone 1.4.0 → 1.6.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 +115 -0
- package/README.md +108 -283
- package/dist/args-Cnq0nwSM.mjs +272 -0
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.d.mts +28 -3
- package/dist/codegen/index.d.mts.map +1 -1
- package/dist/codegen/index.mjs +169 -19
- 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} +86 -108
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +22 -2
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +92 -7
- package/dist/docs/index.mjs.map +1 -1
- package/dist/errors-CL63UOzt.mjs +137 -0
- package/dist/errors-CL63UOzt.mjs.map +1 -0
- package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/help-B5Kk83of.mjs +849 -0
- package/dist/help-B5Kk83of.mjs.map +1 -0
- package/dist/index-BaU3X6dY.d.mts +1178 -0
- package/dist/index-BaU3X6dY.d.mts.map +1 -0
- package/dist/index.d.mts +763 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3608 -1534
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-BM-d0nZi.mjs +377 -0
- package/dist/mcp-BM-d0nZi.mjs.map +1 -0
- package/dist/serve-Bk0JUlCj.mjs +402 -0
- package/dist/serve-Bk0JUlCj.mjs.map +1 -0
- package/dist/stream-DC4H8YTx.mjs +77 -0
- 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 +5 -27
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +32 -0
- package/dist/zod.d.mts.map +1 -0
- package/dist/zod.mjs +50 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +20 -9
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -16
- package/src/cli/doctor.ts +213 -24
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +12 -10
- package/src/cli/link.ts +22 -18
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/discovery.ts +80 -28
- package/src/codegen/index.ts +2 -1
- package/src/codegen/parsers/bash.ts +179 -0
- package/src/codegen/schema-to-code.ts +2 -1
- package/src/core/args.ts +296 -0
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +268 -0
- package/src/{runtime.ts → core/default-runtime.ts} +70 -135
- 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 +124 -11
- package/src/extension/auto-output.ts +95 -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 +43 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +214 -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} +130 -57
- package/src/{interactive.ts → feature/interactive.ts} +47 -6
- package/src/feature/mcp.ts +387 -0
- package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
- package/src/feature/serve.ts +438 -0
- 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} +27 -27
- package/src/index.ts +120 -11
- package/src/output/colorizer.ts +154 -0
- package/src/{formatter.ts → output/formatter.ts} +281 -135
- package/src/{help.ts → output/help.ts} +62 -15
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -285
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +697 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +59 -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/util/stream.ts +101 -0
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +99 -37
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -0
- package/dist/args-CVDbyyzG.mjs +0 -199
- package/dist/args-CVDbyyzG.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
- package/dist/help-CcBe91bV.mjs +0 -1254
- package/dist/help-CcBe91bV.mjs.map +0 -1
- package/dist/types-DjIdJN5G.d.mts +0 -1059
- package/dist/types-DjIdJN5G.d.mts.map +0 -1
- package/dist/update-check-EbNDkzyV.mjs.map +0 -1
- package/src/args.ts +0 -461
- package/src/colorizer.ts +0 -41
- package/src/command-utils.ts +0 -532
- package/src/create.ts +0 -1477
- package/src/types.ts +0 -1109
- package/src/utils.ts +0 -140
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { resolveAllCommands, resolveCommand } from '../core/commands.ts';
|
|
2
|
+
import { RoutingError, ValidationError } from '../core/errors.ts';
|
|
3
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
4
|
+
import { thenMaybe } from '../core/results.ts';
|
|
5
|
+
import { formatIssueMessages } from '../core/validate.ts';
|
|
6
|
+
import type { HelpDetail, HelpFormat } from '../output/formatter.ts';
|
|
7
|
+
import { generateHelp } from '../output/help.ts';
|
|
8
|
+
import type { AnyPadroneBuilder, AnyPadroneCommand, CommandTypesBase, PadroneCommand } from '../types/index.ts';
|
|
9
|
+
import type { PadroneSchema } from '../types/schema.ts';
|
|
10
|
+
import type { WithCommand } from '../util/type-utils.ts';
|
|
11
|
+
import { getRootCommand } from '../util/utils.ts';
|
|
12
|
+
import { findCommandInTree, passthroughSchema } from './utils.ts';
|
|
13
|
+
|
|
14
|
+
// ── Types ────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
type HelpArgs = { command?: string[]; detail?: HelpDetail; format?: HelpFormat; all?: boolean };
|
|
17
|
+
|
|
18
|
+
export type HelpCommand = PadroneCommand<'help', '', PadroneSchema<HelpArgs>, string, [], ['h', ''], false>;
|
|
19
|
+
|
|
20
|
+
export type WithHelp<T> = WithCommand<T, 'help', HelpCommand>;
|
|
21
|
+
|
|
22
|
+
// ── Interceptor ─────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const helpInterceptor = defineInterceptor({ id: 'padrone:help', name: 'padrone:help', order: -1000 }, () => {
|
|
25
|
+
let helpText: string | undefined;
|
|
26
|
+
let showDefaultHelp = false;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
parse(ctx, next) {
|
|
30
|
+
return thenMaybe(next(), (res) => {
|
|
31
|
+
const hasHelpFlag = res.rawArgs.help || res.rawArgs.h;
|
|
32
|
+
const reverseHelp = !hasHelpFlag && res.positionalArgs?.length > 0 && res.positionalArgs[res.positionalArgs.length - 1] === 'help';
|
|
33
|
+
|
|
34
|
+
if (hasHelpFlag || reverseHelp) {
|
|
35
|
+
delete res.rawArgs.help;
|
|
36
|
+
delete res.rawArgs.h;
|
|
37
|
+
|
|
38
|
+
const detail = res.rawArgs.detail as HelpDetail | undefined;
|
|
39
|
+
const format = res.rawArgs.format as HelpFormat | undefined;
|
|
40
|
+
const all = res.rawArgs.all as boolean | undefined;
|
|
41
|
+
delete res.rawArgs.detail;
|
|
42
|
+
delete res.rawArgs.format;
|
|
43
|
+
delete res.rawArgs.all;
|
|
44
|
+
delete res.rawArgs.d;
|
|
45
|
+
delete res.rawArgs.f;
|
|
46
|
+
|
|
47
|
+
const rootCommand = getRootCommand(res.command);
|
|
48
|
+
resolveAllCommands(rootCommand);
|
|
49
|
+
|
|
50
|
+
helpText = generateHelp(rootCommand, res.command, {
|
|
51
|
+
detail,
|
|
52
|
+
format: format ?? ctx.runtime.format,
|
|
53
|
+
theme: ctx.runtime.theme,
|
|
54
|
+
all,
|
|
55
|
+
terminal: ctx.runtime.terminal,
|
|
56
|
+
env: ctx.runtime.env(),
|
|
57
|
+
});
|
|
58
|
+
return res;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Track whether the parsed command has no action (for default help in execute phase)
|
|
62
|
+
if (helpText === undefined) {
|
|
63
|
+
const { command } = res;
|
|
64
|
+
const hasSubcommands = command.commands && command.commands.length > 0;
|
|
65
|
+
const hasSchema = command.argsSchema != null;
|
|
66
|
+
const hasUnmatchedTerms = res.positionalArgs?.length > 0 && !command.meta?.positional?.length;
|
|
67
|
+
if (!command.action && (hasSubcommands || !hasSchema) && !hasUnmatchedTerms) {
|
|
68
|
+
showDefaultHelp = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return res;
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
validate(_ctx, next) {
|
|
76
|
+
if (helpText !== undefined) return { args: undefined as any, argsResult: { value: undefined } as any };
|
|
77
|
+
return next();
|
|
78
|
+
},
|
|
79
|
+
execute(ctx, next) {
|
|
80
|
+
if (helpText !== undefined) return { result: helpText };
|
|
81
|
+
if (showDefaultHelp) {
|
|
82
|
+
const rootCommand = getRootCommand(ctx.command);
|
|
83
|
+
resolveAllCommands(rootCommand);
|
|
84
|
+
return {
|
|
85
|
+
result: generateHelp(rootCommand, ctx.command, {
|
|
86
|
+
format: ctx.runtime.format,
|
|
87
|
+
theme: ctx.runtime.theme,
|
|
88
|
+
terminal: ctx.runtime.terminal,
|
|
89
|
+
env: ctx.runtime.env(),
|
|
90
|
+
}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return next();
|
|
94
|
+
},
|
|
95
|
+
error(ctx, next) {
|
|
96
|
+
return thenMaybe(next(), (er) => {
|
|
97
|
+
if (ctx.caller !== 'cli' || !er.error) return er;
|
|
98
|
+
|
|
99
|
+
const rootCommand = getRootCommand(ctx.command);
|
|
100
|
+
|
|
101
|
+
if (er.error instanceof RoutingError) {
|
|
102
|
+
const targetPath = er.error.command;
|
|
103
|
+
const targetCommand = targetPath ? findCommandInTree(targetPath, rootCommand) : undefined;
|
|
104
|
+
const sourceCmd = targetCommand ?? rootCommand;
|
|
105
|
+
|
|
106
|
+
ctx.runtime.error(er.error.message);
|
|
107
|
+
|
|
108
|
+
if (er.error.suggestions.length > 0) {
|
|
109
|
+
const visibleCommands = (sourceCmd.commands ?? []).filter((c: AnyPadroneCommand) => !c.hidden && c.name);
|
|
110
|
+
if (visibleCommands.length > 0) {
|
|
111
|
+
for (const cmd of visibleCommands) resolveCommand(cmd);
|
|
112
|
+
const cmdList = visibleCommands.map((c: AnyPadroneCommand) => c.name).join(', ');
|
|
113
|
+
ctx.runtime.output(`\nAvailable commands: ${cmdList}`);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
resolveAllCommands(rootCommand);
|
|
117
|
+
const helpText = generateHelp(rootCommand, sourceCmd, {
|
|
118
|
+
format: ctx.runtime.format,
|
|
119
|
+
theme: ctx.runtime.theme,
|
|
120
|
+
terminal: ctx.runtime.terminal,
|
|
121
|
+
env: ctx.runtime.env(),
|
|
122
|
+
});
|
|
123
|
+
ctx.runtime.error(helpText);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return er;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (er.error instanceof ValidationError) {
|
|
130
|
+
const targetPath = er.error.command;
|
|
131
|
+
const targetCommand = targetPath ? findCommandInTree(targetPath, rootCommand) : undefined;
|
|
132
|
+
const issueMessages = formatIssueMessages(er.error.issues);
|
|
133
|
+
|
|
134
|
+
resolveAllCommands(rootCommand);
|
|
135
|
+
const helpText = generateHelp(rootCommand, targetCommand ?? rootCommand, {
|
|
136
|
+
format: ctx.runtime.format,
|
|
137
|
+
theme: ctx.runtime.theme,
|
|
138
|
+
terminal: ctx.runtime.terminal,
|
|
139
|
+
env: ctx.runtime.env(),
|
|
140
|
+
});
|
|
141
|
+
ctx.runtime.error(`Validation error:\n${issueMessages}`);
|
|
142
|
+
ctx.runtime.error(helpText);
|
|
143
|
+
|
|
144
|
+
return er;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return er;
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── Extension ────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extension that adds help support:
|
|
157
|
+
* - `help` command with aliases `h` and `` (empty = executes on root when no subcommand matches)
|
|
158
|
+
* - `--help` / `-h` flags
|
|
159
|
+
* - `<cmd> help` reverse syntax
|
|
160
|
+
* - Default help display when a command has no action
|
|
161
|
+
*
|
|
162
|
+
* Usage:
|
|
163
|
+
* ```ts
|
|
164
|
+
* createPadrone('my-cli').extend(padroneHelp())
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export function padroneHelp(): <T extends CommandTypesBase>(builder: T) => WithHelp<T> {
|
|
168
|
+
return ((builder: AnyPadroneBuilder) =>
|
|
169
|
+
builder
|
|
170
|
+
.command(['help', 'h'], (c) =>
|
|
171
|
+
c
|
|
172
|
+
.configure({ description: 'Display help for a command', hidden: true })
|
|
173
|
+
.arguments(passthroughSchema({ command: 'string[]', detail: 'string', format: 'string', all: 'boolean' }), {
|
|
174
|
+
positional: ['...command'],
|
|
175
|
+
})
|
|
176
|
+
.action((args, ctx) => {
|
|
177
|
+
const rootCommand = getRootCommand(ctx.command);
|
|
178
|
+
resolveAllCommands(rootCommand);
|
|
179
|
+
const commandName = args.command?.join(' ');
|
|
180
|
+
const targetCommand = commandName ? findCommandInTree(commandName, rootCommand) : rootCommand;
|
|
181
|
+
return generateHelp(rootCommand, targetCommand ?? rootCommand, {
|
|
182
|
+
detail: args.detail as HelpDetail,
|
|
183
|
+
format: (args.format as HelpFormat) ?? ctx.runtime.format,
|
|
184
|
+
theme: ctx.runtime.theme,
|
|
185
|
+
all: args.all,
|
|
186
|
+
terminal: ctx.runtime.terminal,
|
|
187
|
+
env: ctx.runtime.env(),
|
|
188
|
+
});
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
.intercept(helpInterceptor)) as any;
|
|
192
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export { padroneAutoOutput } from './auto-output.ts';
|
|
2
|
+
export { padroneColor } from './color.ts';
|
|
3
|
+
export type { WithCompletion } from './completion.ts';
|
|
4
|
+
export { padroneCompletion } from './completion.ts';
|
|
5
|
+
export type { PadroneConfigOptions } from './config.ts';
|
|
6
|
+
export { padroneConfig } from './config.ts';
|
|
7
|
+
export type { PadroneEnvOptions } from './env.ts';
|
|
8
|
+
export { padroneEnv } from './env.ts';
|
|
9
|
+
export type { HelpCommand, WithHelp } from './help.ts';
|
|
10
|
+
export { padroneHelp } from './help.ts';
|
|
11
|
+
export type { InkOptions } from './ink.ts';
|
|
12
|
+
export { isReactElement, padroneInk } from './ink.ts';
|
|
13
|
+
export { padroneInteractive } from './interactive.ts';
|
|
14
|
+
export type { PadroneLogger, PadroneLoggerConfig, PadroneLogLevel, WithLogger } from './logger.ts';
|
|
15
|
+
export { padroneLogger } from './logger.ts';
|
|
16
|
+
export type { WithMan } from './man.ts';
|
|
17
|
+
export { padroneMan } from './man.ts';
|
|
18
|
+
export type { WithMcp } from './mcp.ts';
|
|
19
|
+
export { padroneMcp } from './mcp.ts';
|
|
20
|
+
export type {
|
|
21
|
+
PadroneProgressConfig,
|
|
22
|
+
PadroneProgressDefaults,
|
|
23
|
+
PadroneProgressMessage,
|
|
24
|
+
PadroneProgressMessages,
|
|
25
|
+
WithProgress,
|
|
26
|
+
} from './progress.ts';
|
|
27
|
+
export { padroneProgress } from './progress.ts';
|
|
28
|
+
export type { PadroneProgressRenderer } from './progress-renderer.ts';
|
|
29
|
+
export { createTerminalProgress } from './progress-renderer.ts';
|
|
30
|
+
export type { WithRepl } from './repl.ts';
|
|
31
|
+
export { padroneRepl } from './repl.ts';
|
|
32
|
+
export type { WithServe } from './serve.ts';
|
|
33
|
+
export { padroneServe } from './serve.ts';
|
|
34
|
+
export { padroneSignalHandling } from './signal.ts';
|
|
35
|
+
export { padroneStdin } from './stdin.ts';
|
|
36
|
+
export { padroneSuggestions } from './suggestions.ts';
|
|
37
|
+
export type { PadroneTimingOptions } from './timing.ts';
|
|
38
|
+
export { padroneTiming } from './timing.ts';
|
|
39
|
+
export type { OtelSpan, OtelTracer, OtelTracerProvider, PadroneTracer, PadroneTracingConfig, WithTracing } from './tracing.ts';
|
|
40
|
+
export { padroneTracing } from './tracing.ts';
|
|
41
|
+
export { padroneUpdateCheck } from './update-check.ts';
|
|
42
|
+
export type { VersionCommand, WithVersion } from './version.ts';
|
|
43
|
+
export { padroneVersion } from './version.ts';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
2
|
+
import type { AnyPadroneBuilder, CommandTypesBase } from '../types/index.ts';
|
|
3
|
+
import type { InterceptorExecuteResult } from '../types/interceptor.ts';
|
|
4
|
+
|
|
5
|
+
// ── React element detection ─────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const reactElement = Symbol.for('react.element');
|
|
8
|
+
const reactTransitional = Symbol.for('react.transitional.element');
|
|
9
|
+
|
|
10
|
+
/** Checks whether a value is a React element (JSX) by inspecting its `$$typeof` symbol. */
|
|
11
|
+
export function isReactElement(value: unknown): boolean {
|
|
12
|
+
if (value === null || typeof value !== 'object') return false;
|
|
13
|
+
const tag = (value as Record<string | symbol, unknown>).$$typeof;
|
|
14
|
+
return tag === reactElement || tag === reactTransitional;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── Types ───────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export type InkOptions = {
|
|
20
|
+
/** Whether to wait for the Ink app to unmount before resolving. Defaults to `true`. */
|
|
21
|
+
waitUntilExit?: boolean;
|
|
22
|
+
/** Options forwarded to Ink's `render()`. */
|
|
23
|
+
render?: import('ink').RenderOptions;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ── Interceptor ─────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const inkMeta = { id: 'padrone:ink', name: 'padrone:ink', order: -1050 } as const;
|
|
29
|
+
|
|
30
|
+
function createInkInterceptor(rawOptions?: InkOptions) {
|
|
31
|
+
return defineInterceptor(inkMeta)
|
|
32
|
+
.requires<{ inkConfig?: InkOptions }>()
|
|
33
|
+
.factory(() => ({
|
|
34
|
+
execute(ctx, next) {
|
|
35
|
+
const ctxCfg = (ctx.context as Record<string, unknown> | undefined)?.inkConfig as InkOptions | undefined;
|
|
36
|
+
const options: InkOptions = { ...ctxCfg, ...rawOptions };
|
|
37
|
+
const { waitUntilExit = true } = options;
|
|
38
|
+
|
|
39
|
+
const handleResult = async (e: InterceptorExecuteResult): Promise<InterceptorExecuteResult> => {
|
|
40
|
+
let value = e.result;
|
|
41
|
+
if (value instanceof Promise) value = await value;
|
|
42
|
+
if (!isReactElement(value)) return e;
|
|
43
|
+
|
|
44
|
+
const { render } = await import('ink');
|
|
45
|
+
const instance = render(value as import('react').ReactElement, options.render);
|
|
46
|
+
|
|
47
|
+
// Unmount on abort so Ink cleans up stdin/stdout
|
|
48
|
+
const onAbort = () => instance.unmount();
|
|
49
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
50
|
+
|
|
51
|
+
if (waitUntilExit) {
|
|
52
|
+
try {
|
|
53
|
+
await instance.waitUntilExit();
|
|
54
|
+
} finally {
|
|
55
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Return undefined so auto-output skips this result
|
|
60
|
+
return { result: undefined };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const executedOrPromise = next();
|
|
64
|
+
if (executedOrPromise instanceof Promise) return executedOrPromise.then(handleResult);
|
|
65
|
+
return handleResult(executedOrPromise);
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Extension ───────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extension that renders React (Ink) components returned from command actions.
|
|
74
|
+
*
|
|
75
|
+
* When a command's action returns a React element (JSX), this extension
|
|
76
|
+
* renders it using Ink instead of passing it to the normal output path.
|
|
77
|
+
*
|
|
78
|
+
* Requires `ink` and `react` as peer dependencies.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* import { createPadrone, padroneInk } from 'padrone';
|
|
82
|
+
*
|
|
83
|
+
* const program = createPadrone('my-tui')
|
|
84
|
+
* .extend(padroneInk())
|
|
85
|
+
* .command('dashboard', (c) =>
|
|
86
|
+
* c.action(() => <Dashboard />)
|
|
87
|
+
* );
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function padroneInk(options?: InkOptions): <T extends CommandTypesBase>(builder: T) => T {
|
|
91
|
+
const interceptor = createInkInterceptor(options);
|
|
92
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any;
|
|
93
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { defineInterceptor } from '../core/interceptors.ts';
|
|
3
|
+
import { hasInteractiveConfig, thenMaybe } from '../core/results.ts';
|
|
4
|
+
import { buildCommandArgs, checkUnknownArgs } from '../core/validate.ts';
|
|
5
|
+
import { promptInteractiveFields } from '../feature/interactive.ts';
|
|
6
|
+
import type { AnyPadroneBuilder, CommandTypesBase, InterceptorValidateContext, InterceptorValidateResult } from '../types/index.ts';
|
|
7
|
+
|
|
8
|
+
// ── Interceptor ─────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const interactiveInterceptor = defineInterceptor({ id: 'padrone:interactive', name: 'padrone:interactive', order: -999 }, () => ({
|
|
11
|
+
validate(ctx: InterceptorValidateContext, next) {
|
|
12
|
+
// Extract --interactive / -i flags from rawArgs
|
|
13
|
+
let flagInteractive: boolean | undefined;
|
|
14
|
+
if (hasInteractiveConfig(ctx.command.meta)) {
|
|
15
|
+
if (ctx.rawArgs.interactive !== undefined) {
|
|
16
|
+
flagInteractive = ctx.rawArgs.interactive !== false && ctx.rawArgs.interactive !== 'false';
|
|
17
|
+
delete ctx.rawArgs.interactive;
|
|
18
|
+
}
|
|
19
|
+
if (ctx.rawArgs.i !== undefined) {
|
|
20
|
+
flagInteractive = ctx.rawArgs.i !== false && ctx.rawArgs.i !== 'false';
|
|
21
|
+
delete ctx.rawArgs.i;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Resolve effective interactivity
|
|
26
|
+
const { runtime, command } = ctx;
|
|
27
|
+
const runtimeDefault: boolean | undefined =
|
|
28
|
+
runtime.interactive === 'forced' ? true : runtime.interactive === 'disabled' ? false : undefined;
|
|
29
|
+
const effectiveInteractive: boolean | undefined = flagInteractive ?? ctx.evalInteractive ?? runtimeDefault;
|
|
30
|
+
const commandUsesStdin = !!command.meta?.stdin;
|
|
31
|
+
const stdinIsPiped = commandUsesStdin && (runtime.stdin ? !runtime.stdin.isTTY : runtime.terminal?.isTTY !== true);
|
|
32
|
+
const interactivitySuppressed =
|
|
33
|
+
runtime.interactive === 'unsupported' || effectiveInteractive === false || (stdinIsPiped && effectiveInteractive !== true);
|
|
34
|
+
const forceInteractive = !interactivitySuppressed && effectiveInteractive === true;
|
|
35
|
+
|
|
36
|
+
const willPrompt = !interactivitySuppressed && runtime.prompt && hasInteractiveConfig(command.meta);
|
|
37
|
+
if (!willPrompt) return next();
|
|
38
|
+
|
|
39
|
+
// Preprocess args to determine what's missing
|
|
40
|
+
const preprocessedArgs = buildCommandArgs(command, ctx.rawArgs, ctx.positionalArgs);
|
|
41
|
+
|
|
42
|
+
// Check for unknown args before prompting
|
|
43
|
+
const unknowns = checkUnknownArgs(command, preprocessedArgs);
|
|
44
|
+
if (unknowns.length > 0) {
|
|
45
|
+
const issues: StandardSchemaV1.Issue[] = unknowns.map(({ key }) => ({
|
|
46
|
+
path: [key],
|
|
47
|
+
message: `Unknown option: "${key}"`,
|
|
48
|
+
}));
|
|
49
|
+
return { args: undefined, argsResult: { issues } } as any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Early-validate provided fields — fail fast on user-supplied errors before prompting
|
|
53
|
+
const earlyValidateAndPrompt = (): InterceptorValidateResult | Promise<InterceptorValidateResult> => {
|
|
54
|
+
if (command.argsSchema) {
|
|
55
|
+
const providedKeys = new Set(Object.keys(preprocessedArgs).filter((k) => preprocessedArgs[k] !== undefined));
|
|
56
|
+
const earlyCheck = command.argsSchema['~standard'].validate(preprocessedArgs);
|
|
57
|
+
|
|
58
|
+
const checkForProvidedFieldErrors = (result: StandardSchemaV1.Result<unknown>): InterceptorValidateResult | undefined => {
|
|
59
|
+
if (!result.issues) return undefined;
|
|
60
|
+
const providedFieldIssues = result.issues.filter((issue: StandardSchemaV1.Issue) => {
|
|
61
|
+
const rootKey = issue.path?.[0];
|
|
62
|
+
return rootKey !== undefined && providedKeys.has(String(rootKey));
|
|
63
|
+
});
|
|
64
|
+
if (providedFieldIssues.length > 0) return { args: undefined, argsResult: { issues: providedFieldIssues } as any };
|
|
65
|
+
return undefined;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const earlyResult = thenMaybe(earlyCheck, (result) => checkForProvidedFieldErrors(result) ?? undefined);
|
|
69
|
+
if (earlyResult instanceof Promise) {
|
|
70
|
+
return earlyResult.then((err) => (err ? err : doPrompt()));
|
|
71
|
+
}
|
|
72
|
+
if (earlyResult) return earlyResult;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return doPrompt();
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Prompt for missing fields, then pass filled args to downstream validation via next()
|
|
79
|
+
const doPrompt = (): InterceptorValidateResult | Promise<InterceptorValidateResult> => {
|
|
80
|
+
const afterInteractive = promptInteractiveFields(preprocessedArgs, command, runtime, forceInteractive || undefined);
|
|
81
|
+
|
|
82
|
+
return thenMaybe(afterInteractive, (filledArgs) => {
|
|
83
|
+
// Pass preprocessed+prompted args downstream with empty positionalArgs (already mapped)
|
|
84
|
+
return next({ rawArgs: filledArgs, positionalArgs: [] });
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return earlyValidateAndPrompt();
|
|
89
|
+
},
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
// ── Extension ────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extension that handles interactive prompting for missing arguments.
|
|
96
|
+
* Extracts `--interactive` / `-i` flags, resolves effective interactivity,
|
|
97
|
+
* and prompts for missing fields before passing filled args to validation.
|
|
98
|
+
*
|
|
99
|
+
* Usage:
|
|
100
|
+
* ```ts
|
|
101
|
+
* createPadrone('my-cli').extend(padroneInteractive())
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function padroneInteractive(): <T extends CommandTypesBase>(builder: T) => T {
|
|
105
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(interactiveInterceptor)) as any;
|
|
106
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { defineInterceptor } from '#src/core/interceptors.ts';
|
|
2
|
+
import { thenMaybe } from '#src/core/results.ts';
|
|
3
|
+
import type { ResolvedPadroneRuntime } from '#src/core/runtime.ts';
|
|
4
|
+
import type { AnyPadroneBuilder, CommandTypesBase } from '#src/types/index.ts';
|
|
5
|
+
import type { WithInterceptor } from '#src/util/type-utils.ts';
|
|
6
|
+
import type { PadroneTracer } from './tracing.ts';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** Log level values ordered by severity. */
|
|
13
|
+
export type PadroneLogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
14
|
+
|
|
15
|
+
/** Logger instance injected into the command context. */
|
|
16
|
+
export type PadroneLogger = {
|
|
17
|
+
trace: (...args: unknown[]) => void;
|
|
18
|
+
debug: (...args: unknown[]) => void;
|
|
19
|
+
info: (...args: unknown[]) => void;
|
|
20
|
+
warn: (...args: unknown[]) => void;
|
|
21
|
+
error: (...args: unknown[]) => void;
|
|
22
|
+
/** The current effective log level. */
|
|
23
|
+
level: PadroneLogLevel;
|
|
24
|
+
/** Create a child logger with a prefix label. */
|
|
25
|
+
child: (label: string) => PadroneLogger;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Configuration for the logger extension. */
|
|
29
|
+
export type PadroneLoggerConfig = {
|
|
30
|
+
/** Minimum log level to output. Defaults to `'info'`. */
|
|
31
|
+
level?: PadroneLogLevel;
|
|
32
|
+
/** Prefix prepended to every log message. */
|
|
33
|
+
prefix?: string;
|
|
34
|
+
/** Include timestamps in log output. Defaults to `false`. */
|
|
35
|
+
timestamps?: boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Builder/program type after applying `padroneLogger()`. Adds `{ logger: PadroneLogger }` to the command context. */
|
|
39
|
+
export type WithLogger<T> = WithInterceptor<T, { logger: PadroneLogger }>;
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Internal helpers
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
const LEVEL_ORDER: Record<PadroneLogLevel, number> = { trace: 0, debug: 1, info: 2, warn: 3, error: 4, silent: 5 };
|
|
46
|
+
const LEVEL_LABELS: Record<Exclude<PadroneLogLevel, 'silent'>, string> = {
|
|
47
|
+
trace: 'TRACE',
|
|
48
|
+
debug: 'DEBUG',
|
|
49
|
+
info: 'INFO',
|
|
50
|
+
warn: 'WARN',
|
|
51
|
+
error: 'ERROR',
|
|
52
|
+
};
|
|
53
|
+
const VALID_LEVELS = new Set<string>(Object.keys(LEVEL_ORDER));
|
|
54
|
+
|
|
55
|
+
function resolveCliLevel(rawArgs: Record<string, unknown>): PadroneLogLevel | undefined {
|
|
56
|
+
// --trace → trace level
|
|
57
|
+
if ('trace' in rawArgs) {
|
|
58
|
+
const t = rawArgs.trace;
|
|
59
|
+
delete rawArgs.trace;
|
|
60
|
+
if (t !== false) return 'trace';
|
|
61
|
+
}
|
|
62
|
+
// --verbose / --debug → debug level
|
|
63
|
+
if ('verbose' in rawArgs) {
|
|
64
|
+
const v = rawArgs.verbose;
|
|
65
|
+
delete rawArgs.verbose;
|
|
66
|
+
if (v !== false) return 'debug';
|
|
67
|
+
}
|
|
68
|
+
if ('debug' in rawArgs) {
|
|
69
|
+
const d = rawArgs.debug;
|
|
70
|
+
delete rawArgs.debug;
|
|
71
|
+
if (d !== false) return 'debug';
|
|
72
|
+
}
|
|
73
|
+
// --silent / --quiet → suppress all output
|
|
74
|
+
if ('silent' in rawArgs) {
|
|
75
|
+
const s = rawArgs.silent;
|
|
76
|
+
delete rawArgs.silent;
|
|
77
|
+
if (s !== false) return 'silent';
|
|
78
|
+
}
|
|
79
|
+
if ('quiet' in rawArgs) {
|
|
80
|
+
const q = rawArgs.quiet;
|
|
81
|
+
delete rawArgs.quiet;
|
|
82
|
+
if (q !== false) return 'silent';
|
|
83
|
+
}
|
|
84
|
+
// --log-level=<level> → explicit level (parser keeps kebab-case)
|
|
85
|
+
if ('log-level' in rawArgs) {
|
|
86
|
+
const val = rawArgs['log-level'];
|
|
87
|
+
delete rawArgs['log-level'];
|
|
88
|
+
if (typeof val === 'string' && VALID_LEVELS.has(val)) return val as PadroneLogLevel;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createLogger(
|
|
94
|
+
runtime: ResolvedPadroneRuntime,
|
|
95
|
+
level: PadroneLogLevel,
|
|
96
|
+
config: ResolvedLoggerConfig,
|
|
97
|
+
tracing?: PadroneTracer,
|
|
98
|
+
): PadroneLogger {
|
|
99
|
+
const threshold = LEVEL_ORDER[level];
|
|
100
|
+
|
|
101
|
+
function format(lvl: Exclude<PadroneLogLevel, 'silent'>, prefix: string, args: unknown[]): string {
|
|
102
|
+
const parts: string[] = [];
|
|
103
|
+
if (config.timestamps) parts.push(new Date().toISOString());
|
|
104
|
+
parts.push(`[${LEVEL_LABELS[lvl]}]`);
|
|
105
|
+
if (prefix) parts.push(prefix);
|
|
106
|
+
parts.push(args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' '));
|
|
107
|
+
return parts.join(' ');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function makeLogger(prefix: string): PadroneLogger {
|
|
111
|
+
const emit = (lvl: Exclude<PadroneLogLevel, 'silent'>, args: unknown[]) => {
|
|
112
|
+
if (LEVEL_ORDER[lvl] < threshold) return;
|
|
113
|
+
const message = format(lvl, prefix, args);
|
|
114
|
+
tracing?.rootSpan.addEvent('log', {
|
|
115
|
+
'log.level': lvl,
|
|
116
|
+
'log.message': args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' '),
|
|
117
|
+
});
|
|
118
|
+
if (lvl === 'error' || lvl === 'warn') runtime.error(message);
|
|
119
|
+
else runtime.output(message);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
trace: (...args) => emit('trace', args),
|
|
124
|
+
debug: (...args) => emit('debug', args),
|
|
125
|
+
info: (...args) => emit('info', args),
|
|
126
|
+
warn: (...args) => emit('warn', args),
|
|
127
|
+
error: (...args) => emit('error', args),
|
|
128
|
+
level,
|
|
129
|
+
child: (label) => makeLogger(prefix ? `${prefix} [${label}]` : `[${label}]`),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return makeLogger(config.prefix);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Interceptor
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
type ResolvedLoggerConfig = { level: PadroneLogLevel; prefix: string; timestamps: boolean };
|
|
141
|
+
|
|
142
|
+
function loggerInterceptor(rawConfig?: PadroneLoggerConfig) {
|
|
143
|
+
return defineInterceptor({ id: 'padrone:logger', name: 'padrone:logger' })
|
|
144
|
+
.requires<{ tracing?: PadroneTracer; loggerConfig?: PadroneLoggerConfig }>()
|
|
145
|
+
.factory(() => {
|
|
146
|
+
let cliLevel: PadroneLogLevel | undefined;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
parse(_ctx, next) {
|
|
150
|
+
return thenMaybe(next(), (res) => {
|
|
151
|
+
cliLevel = resolveCliLevel(res.rawArgs);
|
|
152
|
+
return res;
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
execute(ctx, next) {
|
|
157
|
+
const ctxCfg = (ctx.context as Record<string, unknown> | undefined)?.loggerConfig as PadroneLoggerConfig | undefined;
|
|
158
|
+
const resolved: ResolvedLoggerConfig = {
|
|
159
|
+
level: cliLevel ?? rawConfig?.level ?? ctxCfg?.level ?? 'info',
|
|
160
|
+
prefix: rawConfig?.prefix ?? '',
|
|
161
|
+
timestamps: rawConfig?.timestamps ?? ctxCfg?.timestamps ?? false,
|
|
162
|
+
};
|
|
163
|
+
const logger = createLogger(ctx.runtime, resolved.level, resolved, ctx.context?.tracing);
|
|
164
|
+
return next({ context: { ...ctx.context, logger } });
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Extension
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Extension that injects a structured logger into the command context.
|
|
176
|
+
*
|
|
177
|
+
* The logger respects a configurable log level threshold, supports prefixed
|
|
178
|
+
* child loggers, and routes output through the runtime's `output`/`error`
|
|
179
|
+
* functions so it works in any environment (terminal, test, web).
|
|
180
|
+
*
|
|
181
|
+
* Supports CLI flags for runtime level overrides:
|
|
182
|
+
* - `--trace` → sets level to `trace`
|
|
183
|
+
* - `--verbose` or `--debug` → sets level to `debug`
|
|
184
|
+
* - `--silent` or `--quiet` → sets level to `silent`
|
|
185
|
+
* - `--log-level=<level>` → sets an explicit level (`trace`, `debug`, `info`, `warn`, `error`, `silent`)
|
|
186
|
+
*
|
|
187
|
+
* CLI flags take precedence over the programmatic config.
|
|
188
|
+
*
|
|
189
|
+
* Provides `{ logger: PadroneLogger }` on the command context.
|
|
190
|
+
* Access it in action handlers as `ctx.context.logger`.
|
|
191
|
+
*
|
|
192
|
+
* Usage:
|
|
193
|
+
* ```ts
|
|
194
|
+
* createPadrone('my-cli')
|
|
195
|
+
* .extend(padroneLogger({ level: 'info' }))
|
|
196
|
+
* .command('sync', (c) =>
|
|
197
|
+
* c.action((_args, ctx) => {
|
|
198
|
+
* ctx.context.logger.info('starting sync');
|
|
199
|
+
* const db = ctx.context.logger.child('db');
|
|
200
|
+
* db.debug('connecting...');
|
|
201
|
+
* })
|
|
202
|
+
* )
|
|
203
|
+
* ```
|
|
204
|
+
*
|
|
205
|
+
* Then run:
|
|
206
|
+
* ```sh
|
|
207
|
+
* my-cli sync --verbose # debug level
|
|
208
|
+
* my-cli sync --quiet # silent
|
|
209
|
+
* my-cli sync --log-level=warn
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
export function padroneLogger<T extends CommandTypesBase>(config?: PadroneLoggerConfig): (builder: T) => WithLogger<T> {
|
|
213
|
+
return ((builder: AnyPadroneBuilder) => builder.intercept(loggerInterceptor(config))) as any;
|
|
214
|
+
}
|