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.
Files changed (141) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +108 -283
  3. package/dist/args-Cnq0nwSM.mjs +272 -0
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.d.mts +28 -3
  6. package/dist/codegen/index.d.mts.map +1 -1
  7. package/dist/codegen/index.mjs +169 -19
  8. package/dist/codegen/index.mjs.map +1 -1
  9. package/dist/commands-B_gufyR9.mjs +514 -0
  10. package/dist/commands-B_gufyR9.mjs.map +1 -0
  11. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
  12. package/dist/completion-BEuflbDO.mjs.map +1 -0
  13. package/dist/docs/index.d.mts +22 -2
  14. package/dist/docs/index.d.mts.map +1 -1
  15. package/dist/docs/index.mjs +92 -7
  16. package/dist/docs/index.mjs.map +1 -1
  17. package/dist/errors-CL63UOzt.mjs +137 -0
  18. package/dist/errors-CL63UOzt.mjs.map +1 -0
  19. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
  20. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  21. package/dist/help-B5Kk83of.mjs +849 -0
  22. package/dist/help-B5Kk83of.mjs.map +1 -0
  23. package/dist/index-BaU3X6dY.d.mts +1178 -0
  24. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  25. package/dist/index.d.mts +763 -36
  26. package/dist/index.d.mts.map +1 -1
  27. package/dist/index.mjs +3608 -1534
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp-BM-d0nZi.mjs +377 -0
  30. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  31. package/dist/serve-Bk0JUlCj.mjs +402 -0
  32. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  33. package/dist/stream-DC4H8YTx.mjs +77 -0
  34. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  35. package/dist/test.d.mts +5 -8
  36. package/dist/test.d.mts.map +1 -1
  37. package/dist/test.mjs +5 -27
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  40. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  41. package/dist/zod.d.mts +32 -0
  42. package/dist/zod.d.mts.map +1 -0
  43. package/dist/zod.mjs +50 -0
  44. package/dist/zod.mjs.map +1 -0
  45. package/package.json +20 -9
  46. package/src/cli/completions.ts +14 -11
  47. package/src/cli/docs.ts +13 -16
  48. package/src/cli/doctor.ts +213 -24
  49. package/src/cli/index.ts +28 -82
  50. package/src/cli/init.ts +12 -10
  51. package/src/cli/link.ts +22 -18
  52. package/src/cli/wrap.ts +14 -11
  53. package/src/codegen/discovery.ts +80 -28
  54. package/src/codegen/index.ts +2 -1
  55. package/src/codegen/parsers/bash.ts +179 -0
  56. package/src/codegen/schema-to-code.ts +2 -1
  57. package/src/core/args.ts +296 -0
  58. package/src/core/commands.ts +373 -0
  59. package/src/core/create.ts +268 -0
  60. package/src/{runtime.ts → core/default-runtime.ts} +70 -135
  61. package/src/{errors.ts → core/errors.ts} +22 -0
  62. package/src/core/exec.ts +259 -0
  63. package/src/core/interceptors.ts +302 -0
  64. package/src/{parse.ts → core/parse.ts} +36 -89
  65. package/src/core/program-methods.ts +301 -0
  66. package/src/core/results.ts +229 -0
  67. package/src/core/runtime.ts +246 -0
  68. package/src/core/validate.ts +247 -0
  69. package/src/docs/index.ts +124 -11
  70. package/src/extension/auto-output.ts +95 -0
  71. package/src/extension/color.ts +38 -0
  72. package/src/extension/completion.ts +49 -0
  73. package/src/extension/config.ts +262 -0
  74. package/src/extension/env.ts +101 -0
  75. package/src/extension/help.ts +192 -0
  76. package/src/extension/index.ts +43 -0
  77. package/src/extension/ink.ts +93 -0
  78. package/src/extension/interactive.ts +106 -0
  79. package/src/extension/logger.ts +214 -0
  80. package/src/extension/man.ts +51 -0
  81. package/src/extension/mcp.ts +52 -0
  82. package/src/extension/progress-renderer.ts +338 -0
  83. package/src/extension/progress.ts +299 -0
  84. package/src/extension/repl.ts +94 -0
  85. package/src/extension/serve.ts +48 -0
  86. package/src/extension/signal.ts +87 -0
  87. package/src/extension/stdin.ts +62 -0
  88. package/src/extension/suggestions.ts +114 -0
  89. package/src/extension/timing.ts +81 -0
  90. package/src/extension/tracing.ts +175 -0
  91. package/src/extension/update-check.ts +77 -0
  92. package/src/extension/utils.ts +51 -0
  93. package/src/extension/version.ts +63 -0
  94. package/src/{completion.ts → feature/completion.ts} +130 -57
  95. package/src/{interactive.ts → feature/interactive.ts} +47 -6
  96. package/src/feature/mcp.ts +387 -0
  97. package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
  98. package/src/feature/serve.ts +438 -0
  99. package/src/feature/test.ts +262 -0
  100. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  101. package/src/{wrap.ts → feature/wrap.ts} +27 -27
  102. package/src/index.ts +120 -11
  103. package/src/output/colorizer.ts +154 -0
  104. package/src/{formatter.ts → output/formatter.ts} +281 -135
  105. package/src/{help.ts → output/help.ts} +62 -15
  106. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  107. package/src/schema/zod.ts +50 -0
  108. package/src/test.ts +2 -285
  109. package/src/types/args-meta.ts +151 -0
  110. package/src/types/builder.ts +697 -0
  111. package/src/types/command.ts +157 -0
  112. package/src/types/index.ts +59 -0
  113. package/src/types/interceptor.ts +296 -0
  114. package/src/types/preferences.ts +83 -0
  115. package/src/types/result.ts +71 -0
  116. package/src/types/schema.ts +19 -0
  117. package/src/util/dotenv.ts +244 -0
  118. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  119. package/src/util/stream.ts +101 -0
  120. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  121. package/src/{type-utils.ts → util/type-utils.ts} +99 -37
  122. package/src/util/utils.ts +51 -0
  123. package/src/zod.ts +1 -0
  124. package/dist/args-CVDbyyzG.mjs +0 -199
  125. package/dist/args-CVDbyyzG.mjs.map +0 -1
  126. package/dist/chunk-y_GBKt04.mjs +0 -5
  127. package/dist/completion.d.mts +0 -64
  128. package/dist/completion.d.mts.map +0 -1
  129. package/dist/completion.mjs.map +0 -1
  130. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  131. package/dist/help-CcBe91bV.mjs +0 -1254
  132. package/dist/help-CcBe91bV.mjs.map +0 -1
  133. package/dist/types-DjIdJN5G.d.mts +0 -1059
  134. package/dist/types-DjIdJN5G.d.mts.map +0 -1
  135. package/dist/update-check-EbNDkzyV.mjs.map +0 -1
  136. package/src/args.ts +0 -461
  137. package/src/colorizer.ts +0 -41
  138. package/src/command-utils.ts +0 -532
  139. package/src/create.ts +0 -1477
  140. package/src/types.ts +0 -1109
  141. 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
+ }