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.
Files changed (141) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +15 -11
  3. package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.mjs +4 -4
  6. package/dist/codegen/index.mjs.map +1 -1
  7. package/dist/commands-B_gufyR9.mjs +514 -0
  8. package/dist/commands-B_gufyR9.mjs.map +1 -0
  9. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
  10. package/dist/completion-BEuflbDO.mjs.map +1 -0
  11. package/dist/docs/index.d.mts +4 -4
  12. package/dist/docs/index.d.mts.map +1 -1
  13. package/dist/docs/index.mjs +10 -12
  14. package/dist/docs/index.mjs.map +1 -1
  15. package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
  16. package/dist/errors-DA4KzK1M.mjs.map +1 -0
  17. package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
  18. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  19. package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
  20. package/dist/help-BtxLgrF_.mjs.map +1 -0
  21. package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
  22. package/dist/index-D6-7dz0l.d.mts.map +1 -0
  23. package/dist/index.d.mts +869 -36
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +3884 -1699
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
  28. package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
  29. package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
  30. package/dist/serve-YVTPzBCl.mjs.map +1 -0
  31. package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
  32. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  33. package/dist/test.d.mts +5 -8
  34. package/dist/test.d.mts.map +1 -1
  35. package/dist/test.mjs +2 -13
  36. package/dist/test.mjs.map +1 -1
  37. package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  38. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  39. package/dist/zod.d.mts +2 -2
  40. package/dist/zod.d.mts.map +1 -1
  41. package/dist/zod.mjs +2 -2
  42. package/dist/zod.mjs.map +1 -1
  43. package/package.json +15 -12
  44. package/src/cli/completions.ts +14 -11
  45. package/src/cli/docs.ts +13 -10
  46. package/src/cli/doctor.ts +22 -18
  47. package/src/cli/index.ts +28 -82
  48. package/src/cli/init.ts +10 -7
  49. package/src/cli/link.ts +20 -16
  50. package/src/cli/wrap.ts +14 -11
  51. package/src/codegen/schema-to-code.ts +2 -2
  52. package/src/{args.ts → core/args.ts} +32 -225
  53. package/src/core/commands.ts +373 -0
  54. package/src/core/create.ts +301 -0
  55. package/src/core/default-runtime.ts +239 -0
  56. package/src/{errors.ts → core/errors.ts} +22 -0
  57. package/src/core/exec.ts +259 -0
  58. package/src/core/interceptors.ts +302 -0
  59. package/src/{parse.ts → core/parse.ts} +36 -89
  60. package/src/core/program-methods.ts +301 -0
  61. package/src/core/results.ts +229 -0
  62. package/src/core/runtime.ts +246 -0
  63. package/src/core/validate.ts +247 -0
  64. package/src/docs/index.ts +12 -13
  65. package/src/extension/auto-output.ts +146 -0
  66. package/src/extension/color.ts +38 -0
  67. package/src/extension/completion.ts +49 -0
  68. package/src/extension/config.ts +262 -0
  69. package/src/extension/env.ts +101 -0
  70. package/src/extension/help.ts +192 -0
  71. package/src/extension/index.ts +44 -0
  72. package/src/extension/ink.ts +93 -0
  73. package/src/extension/interactive.ts +106 -0
  74. package/src/extension/logger.ts +262 -0
  75. package/src/extension/man.ts +51 -0
  76. package/src/extension/mcp.ts +52 -0
  77. package/src/extension/progress-renderer.ts +338 -0
  78. package/src/extension/progress.ts +299 -0
  79. package/src/extension/repl.ts +94 -0
  80. package/src/extension/serve.ts +48 -0
  81. package/src/extension/signal.ts +87 -0
  82. package/src/extension/stdin.ts +62 -0
  83. package/src/extension/suggestions.ts +114 -0
  84. package/src/extension/timing.ts +81 -0
  85. package/src/extension/tracing.ts +175 -0
  86. package/src/extension/update-check.ts +77 -0
  87. package/src/extension/utils.ts +51 -0
  88. package/src/extension/version.ts +63 -0
  89. package/src/{completion.ts → feature/completion.ts} +12 -12
  90. package/src/{interactive.ts → feature/interactive.ts} +4 -4
  91. package/src/{mcp.ts → feature/mcp.ts} +12 -15
  92. package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
  93. package/src/{serve.ts → feature/serve.ts} +11 -15
  94. package/src/feature/test.ts +262 -0
  95. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  96. package/src/{wrap.ts → feature/wrap.ts} +10 -8
  97. package/src/index.ts +115 -30
  98. package/src/{formatter.ts → output/formatter.ts} +124 -176
  99. package/src/{help.ts → output/help.ts} +22 -8
  100. package/src/output/output-indicator.ts +87 -0
  101. package/src/output/primitives.ts +335 -0
  102. package/src/output/styling.ts +221 -0
  103. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  104. package/src/schema/zod.ts +50 -0
  105. package/src/test.ts +2 -276
  106. package/src/types/args-meta.ts +151 -0
  107. package/src/types/builder.ts +718 -0
  108. package/src/types/command.ts +157 -0
  109. package/src/types/index.ts +60 -0
  110. package/src/types/interceptor.ts +296 -0
  111. package/src/types/preferences.ts +83 -0
  112. package/src/types/result.ts +71 -0
  113. package/src/types/schema.ts +19 -0
  114. package/src/util/dotenv.ts +244 -0
  115. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  116. package/src/{stream.ts → util/stream.ts} +27 -1
  117. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  118. package/src/{type-utils.ts → util/type-utils.ts} +71 -33
  119. package/src/util/utils.ts +51 -0
  120. package/src/zod.ts +1 -50
  121. package/dist/args-D5PNDyNu.mjs.map +0 -1
  122. package/dist/chunk-CjcI7cDX.mjs +0 -15
  123. package/dist/command-utils-B1D-HqCd.mjs +0 -1117
  124. package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
  125. package/dist/completion.d.mts +0 -64
  126. package/dist/completion.d.mts.map +0 -1
  127. package/dist/completion.mjs.map +0 -1
  128. package/dist/errors-BiVrBgi6.mjs.map +0 -1
  129. package/dist/formatter-DtHzbP22.d.mts.map +0 -1
  130. package/dist/help-bbmu9-qd.mjs.map +0 -1
  131. package/dist/mcp-mLWIdUIu.mjs.map +0 -1
  132. package/dist/serve-B0u43DK7.mjs.map +0 -1
  133. package/dist/stream-BcC146Ud.mjs.map +0 -1
  134. package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
  135. package/dist/update-check-CFX1FV3v.mjs.map +0 -1
  136. package/src/command-utils.ts +0 -882
  137. package/src/create.ts +0 -1829
  138. package/src/runtime.ts +0 -497
  139. package/src/types.ts +0 -1291
  140. package/src/utils.ts +0 -140
  141. /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
@@ -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,44 @@
1
+ export type { PadroneAutoOutputOptions } from './auto-output.ts';
2
+ export { padroneAutoOutput } from './auto-output.ts';
3
+ export { padroneColor } from './color.ts';
4
+ export type { WithCompletion } from './completion.ts';
5
+ export { padroneCompletion } from './completion.ts';
6
+ export type { PadroneConfigOptions } from './config.ts';
7
+ export { padroneConfig } from './config.ts';
8
+ export type { PadroneEnvOptions } from './env.ts';
9
+ export { padroneEnv } from './env.ts';
10
+ export type { HelpCommand, WithHelp } from './help.ts';
11
+ export { padroneHelp } from './help.ts';
12
+ export type { InkOptions } from './ink.ts';
13
+ export { isReactElement, padroneInk } from './ink.ts';
14
+ export { padroneInteractive } from './interactive.ts';
15
+ export type { PadroneLogger, PadroneLoggerConfig, PadroneLogLevel, WithLogger } from './logger.ts';
16
+ export { padroneLogger } from './logger.ts';
17
+ export type { WithMan } from './man.ts';
18
+ export { padroneMan } from './man.ts';
19
+ export type { WithMcp } from './mcp.ts';
20
+ export { padroneMcp } from './mcp.ts';
21
+ export type {
22
+ PadroneProgressConfig,
23
+ PadroneProgressDefaults,
24
+ PadroneProgressMessage,
25
+ PadroneProgressMessages,
26
+ WithProgress,
27
+ } from './progress.ts';
28
+ export { padroneProgress } from './progress.ts';
29
+ export type { PadroneProgressRenderer } from './progress-renderer.ts';
30
+ export { createTerminalProgress } from './progress-renderer.ts';
31
+ export type { WithRepl } from './repl.ts';
32
+ export { padroneRepl } from './repl.ts';
33
+ export type { WithServe } from './serve.ts';
34
+ export { padroneServe } from './serve.ts';
35
+ export { padroneSignalHandling } from './signal.ts';
36
+ export { padroneStdin } from './stdin.ts';
37
+ export { padroneSuggestions } from './suggestions.ts';
38
+ export type { PadroneTimingOptions } from './timing.ts';
39
+ export { padroneTiming } from './timing.ts';
40
+ export type { OtelSpan, OtelTracer, OtelTracerProvider, PadroneTracer, PadroneTracingConfig, WithTracing } from './tracing.ts';
41
+ export { padroneTracing } from './tracing.ts';
42
+ export { padroneUpdateCheck } from './update-check.ts';
43
+ export type { VersionCommand, WithVersion } from './version.ts';
44
+ 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
+ }