padrone 1.1.0 → 1.2.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 (80) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/LICENSE +1 -1
  3. package/README.md +60 -30
  4. package/dist/args-CKNh7Dm9.mjs +175 -0
  5. package/dist/args-CKNh7Dm9.mjs.map +1 -0
  6. package/dist/chunk-y_GBKt04.mjs +5 -0
  7. package/dist/codegen/index.d.mts +305 -0
  8. package/dist/codegen/index.d.mts.map +1 -0
  9. package/dist/codegen/index.mjs +1348 -0
  10. package/dist/codegen/index.mjs.map +1 -0
  11. package/dist/completion.d.mts +64 -0
  12. package/dist/completion.d.mts.map +1 -0
  13. package/dist/completion.mjs +417 -0
  14. package/dist/completion.mjs.map +1 -0
  15. package/dist/docs/index.d.mts +34 -0
  16. package/dist/docs/index.d.mts.map +1 -0
  17. package/dist/docs/index.mjs +404 -0
  18. package/dist/docs/index.mjs.map +1 -0
  19. package/dist/formatter-Dvx7jFXr.d.mts +82 -0
  20. package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
  21. package/dist/help-mUIX0T0V.mjs +1195 -0
  22. package/dist/help-mUIX0T0V.mjs.map +1 -0
  23. package/dist/index.d.mts +120 -546
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +1180 -1197
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/test.d.mts +112 -0
  28. package/dist/test.d.mts.map +1 -0
  29. package/dist/test.mjs +138 -0
  30. package/dist/test.mjs.map +1 -0
  31. package/dist/types-qrtt0135.d.mts +1037 -0
  32. package/dist/types-qrtt0135.d.mts.map +1 -0
  33. package/dist/update-check-EbNDkzyV.mjs +146 -0
  34. package/dist/update-check-EbNDkzyV.mjs.map +1 -0
  35. package/package.json +61 -21
  36. package/src/args.ts +365 -0
  37. package/src/cli/completions.ts +29 -0
  38. package/src/cli/docs.ts +86 -0
  39. package/src/cli/doctor.ts +312 -0
  40. package/src/cli/index.ts +159 -0
  41. package/src/cli/init.ts +135 -0
  42. package/src/cli/link.ts +320 -0
  43. package/src/cli/wrap.ts +152 -0
  44. package/src/codegen/README.md +118 -0
  45. package/src/codegen/code-builder.ts +226 -0
  46. package/src/codegen/discovery.ts +232 -0
  47. package/src/codegen/file-emitter.ts +73 -0
  48. package/src/codegen/generators/barrel-file.ts +16 -0
  49. package/src/codegen/generators/command-file.ts +184 -0
  50. package/src/codegen/generators/command-tree.ts +124 -0
  51. package/src/codegen/index.ts +33 -0
  52. package/src/codegen/parsers/fish.ts +163 -0
  53. package/src/codegen/parsers/help.ts +378 -0
  54. package/src/codegen/parsers/merge.ts +158 -0
  55. package/src/codegen/parsers/zsh.ts +221 -0
  56. package/src/codegen/schema-to-code.ts +199 -0
  57. package/src/codegen/template.ts +69 -0
  58. package/src/codegen/types.ts +143 -0
  59. package/src/colorizer.ts +2 -2
  60. package/src/command-utils.ts +501 -0
  61. package/src/completion.ts +110 -97
  62. package/src/create.ts +1036 -305
  63. package/src/docs/index.ts +607 -0
  64. package/src/errors.ts +131 -0
  65. package/src/formatter.ts +149 -63
  66. package/src/help.ts +151 -55
  67. package/src/index.ts +12 -15
  68. package/src/interactive.ts +169 -0
  69. package/src/parse.ts +31 -16
  70. package/src/repl-loop.ts +317 -0
  71. package/src/runtime.ts +304 -0
  72. package/src/shell-utils.ts +83 -0
  73. package/src/test.ts +285 -0
  74. package/src/type-helpers.ts +10 -10
  75. package/src/type-utils.ts +124 -14
  76. package/src/types.ts +752 -154
  77. package/src/update-check.ts +244 -0
  78. package/src/wrap.ts +44 -40
  79. package/src/zod.d.ts +2 -2
  80. package/src/options.ts +0 -180
package/src/help.ts CHANGED
@@ -1,17 +1,19 @@
1
1
  import type { StandardJSONSchemaV1 } from '@standard-schema/spec';
2
+ import { extractSchemaMetadata, type PadroneArgsSchemaMeta, parsePositionalConfig, parseStdinConfig } from './args.ts';
3
+ import { findCommandByName } from './command-utils.ts';
2
4
  import {
3
5
  createFormatter,
4
6
  type HelpArgumentInfo,
5
7
  type HelpDetail,
6
8
  type HelpFormat,
7
9
  type HelpInfo,
8
- type HelpOptionInfo,
10
+ type HelpPositionalInfo,
11
+ type HelpSubcommandInfo,
9
12
  } from './formatter.ts';
10
- import { extractSchemaMetadata, type PadroneMeta, parsePositionalConfig } from './options.ts';
11
13
  import type { AnyPadroneCommand } from './types.ts';
12
14
  import { getRootCommand } from './utils.ts';
13
15
 
14
- export type HelpOptions = {
16
+ export type HelpPreferences = {
15
17
  format?: HelpFormat | 'auto';
16
18
  detail?: HelpDetail;
17
19
  };
@@ -21,9 +23,9 @@ export type HelpOptions = {
21
23
  */
22
24
  function extractPositionalArgsInfo(
23
25
  schema: StandardJSONSchemaV1,
24
- meta?: PadroneMeta,
25
- ): { args: HelpArgumentInfo[]; positionalNames: Set<string> } {
26
- const args: HelpArgumentInfo[] = [];
26
+ meta?: PadroneArgsSchemaMeta,
27
+ ): { args: HelpPositionalInfo[]; positionalNames: Set<string> } {
28
+ const args: HelpPositionalInfo[] = [];
27
29
  const positionalNames = new Set<string>();
28
30
 
29
31
  if (!schema || !meta?.positional || meta.positional.length === 0) {
@@ -44,7 +46,7 @@ function extractPositionalArgsInfo(
44
46
  if (!prop) continue;
45
47
 
46
48
  positionalNames.add(name);
47
- const optMeta = meta.options?.[name];
49
+ const optMeta = meta.fields?.[name];
48
50
 
49
51
  args.push({
50
52
  name: variadic ? `...${name}` : name,
@@ -62,14 +64,14 @@ function extractPositionalArgsInfo(
62
64
  return { args, positionalNames };
63
65
  }
64
66
 
65
- function extractOptionsInfo(schema: StandardJSONSchemaV1, meta?: PadroneMeta, positionalNames?: Set<string>) {
66
- const result: HelpOptionInfo[] = [];
67
+ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaMeta, positionalNames?: Set<string>) {
68
+ const result: HelpArgumentInfo[] = [];
67
69
  if (!schema) return result;
68
70
 
69
71
  const vendor = schema['~standard'].vendor;
70
72
  if (!vendor.includes('zod')) return result;
71
73
 
72
- const optionsMeta = meta?.options;
74
+ const argsMeta = meta?.fields;
73
75
 
74
76
  try {
75
77
  const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
@@ -80,7 +82,7 @@ function extractOptionsInfo(schema: StandardJSONSchemaV1, meta?: PadroneMeta, po
80
82
  const required = (jsonSchema.required as string[]) || [];
81
83
  const propertyNames = new Set(Object.keys(properties));
82
84
 
83
- // Helper to check if a negated version of an option exists
85
+ // Helper to check if a negated version of an arg exists
84
86
  const hasExplicitNegation = (key: string): boolean => {
85
87
  // Check for noVerbose style (camelCase)
86
88
  const camelNegated = `no${key.charAt(0).toUpperCase()}${key.slice(1)}`;
@@ -91,7 +93,7 @@ function extractOptionsInfo(schema: StandardJSONSchemaV1, meta?: PadroneMeta, po
91
93
  return false;
92
94
  };
93
95
 
94
- // Helper to check if this option is itself a negation of another option
96
+ // Helper to check if this arg is itself a negation of another arg
95
97
  const isNegationOf = (key: string): boolean => {
96
98
  // Check for noVerbose -> verbose (camelCase)
97
99
  if (key.startsWith('no') && key.length > 2 && key[2] === key[2]?.toUpperCase()) {
@@ -111,12 +113,12 @@ function extractOptionsInfo(schema: StandardJSONSchemaV1, meta?: PadroneMeta, po
111
113
  if (positionalNames?.has(key)) continue;
112
114
 
113
115
  const isOptional = !required.includes(key);
114
- const enumValues = prop.enum as string[] | undefined;
115
- const optMeta = optionsMeta?.[key];
116
+ const enumValues = (prop.enum ?? prop.items?.enum) as string[] | undefined;
117
+ const optMeta = argsMeta?.[key];
116
118
  const propType = prop.type as string;
117
119
 
118
- // Booleans are negatable unless there's an explicit noOption property
119
- // or this option is itself a negation of another option
120
+ // Booleans are negatable unless there's an explicit noArg property
121
+ // or this arg is itself a negation of another arg
120
122
  const isNegatable = propType === 'boolean' && !hasExplicitNegation(key) && !isNegationOf(key);
121
123
 
122
124
  result.push({
@@ -151,45 +153,99 @@ function extractOptionsInfo(schema: StandardJSONSchemaV1, meta?: PadroneMeta, po
151
153
  * @param cmd - The command to build help info for
152
154
  * @param detail - The level of detail ('minimal', 'standard', or 'full')
153
155
  */
154
- function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpOptions['detail'] = 'standard'): HelpInfo {
156
+ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['detail'] = 'standard'): HelpInfo {
155
157
  const rootCmd = getRootCommand(cmd);
156
- const commandName = cmd.path || cmd.name || 'program';
157
-
158
- // Extract positional args from options schema based on meta.positional
159
- const { args: positionalArgs, positionalNames } = cmd.options
160
- ? extractPositionalArgsInfo(cmd.options, cmd.meta)
158
+ // A command is a "default" command if its name is '' or it has '' as an alias
159
+ const isDefaultCommand = cmd.parent && (!cmd.name || cmd.aliases?.includes(''));
160
+ // For commands with empty name, use the first non-empty alias as display name
161
+ const nonEmptyAliases = cmd.aliases?.filter(Boolean);
162
+ const commandName = cmd.path || cmd.name || nonEmptyAliases?.[0] || (cmd.parent ? '[default]' : 'program');
163
+ // Build display aliases: real aliases (excluding the one promoted to display name) + [default] marker
164
+ const remainingAliases = !cmd.name && nonEmptyAliases?.length ? nonEmptyAliases.slice(1) : (nonEmptyAliases ?? []);
165
+ const displayAliases = isDefaultCommand ? [...remainingAliases, '[default]'] : nonEmptyAliases;
166
+
167
+ // Extract positional args from schema based on meta.positional
168
+ const { args: positionalArgs, positionalNames } = cmd.argsSchema
169
+ ? extractPositionalArgsInfo(cmd.argsSchema, cmd.meta)
161
170
  : { args: [], positionalNames: new Set<string>() };
162
171
 
163
- const hasArguments = positionalArgs.length > 0;
172
+ const hasPositionals = positionalArgs.length > 0;
164
173
 
165
174
  const helpInfo: HelpInfo = {
166
175
  name: commandName,
167
176
  title: cmd.title,
168
177
  description: cmd.description,
169
- aliases: cmd.aliases,
178
+ aliases: displayAliases,
170
179
  deprecated: cmd.deprecated,
171
180
  hidden: cmd.hidden,
172
181
  usage: {
173
182
  command: rootCmd === cmd ? commandName : `${rootCmd.name} ${commandName}`,
174
183
  hasSubcommands: !!(cmd.commands && cmd.commands.length > 0),
175
- hasArguments,
176
- hasOptions: !!cmd.options,
184
+ hasPositionals,
185
+ hasArguments: false, // updated below after extracting arguments
186
+ stdinField: cmd.meta?.stdin ? parseStdinConfig(cmd.meta.stdin).field : undefined,
177
187
  },
178
188
  };
179
189
 
180
190
  // Build subcommands info (filter out hidden commands unless showing full detail)
181
191
  if (cmd.commands && cmd.commands.length > 0) {
182
192
  const visibleCommands = detail === 'full' ? cmd.commands : cmd.commands.filter((c) => !c.hidden);
183
- helpInfo.subcommands = visibleCommands.map((c) => {
184
- return {
185
- name: c.name,
186
- title: c.title,
187
- description: c.description,
188
- aliases: c.aliases,
189
- deprecated: c.deprecated,
190
- hidden: c.hidden,
191
- };
192
- });
193
+ // If the command has both a handler and subcommands, show the handler as a "[default]" entry
194
+ const selfEntry: typeof helpInfo.subcommands = cmd.action
195
+ ? [{ name: '[default]', title: cmd.title, description: cmd.description }]
196
+ : [];
197
+
198
+ helpInfo.subcommands = [
199
+ ...selfEntry,
200
+ ...visibleCommands.flatMap((c): HelpSubcommandInfo[] => {
201
+ const isDefault = !c.name || c.aliases?.includes('');
202
+ const nonEmptyAliases = c.aliases?.filter(Boolean);
203
+ const displayName = c.name || nonEmptyAliases?.[0] || '[default]';
204
+ const remainingAliases = !c.name && nonEmptyAliases?.length ? nonEmptyAliases.slice(1) : (nonEmptyAliases ?? []);
205
+ // Only add [default] alias marker if it's not already the display name
206
+ const displayAliases =
207
+ isDefault && displayName !== '[default]' ? [...remainingAliases, '[default]'] : isDefault ? remainingAliases : nonEmptyAliases;
208
+ const hasSubcommands = !!(c.commands && c.commands.length > 0);
209
+
210
+ // If a command has subcommands AND a default handler (direct or '' subcommand),
211
+ // show two entries: one for the default action, one for the subcommand router
212
+ const hasDefaultHandler = c.action || c.commands?.some((sub) => !sub.name || sub.aliases?.includes(''));
213
+ if (hasSubcommands && hasDefaultHandler) {
214
+ const defaultSub = !c.action ? c.commands?.find((sub) => !sub.name || sub.aliases?.includes('')) : undefined;
215
+ const hasDefaultSubInfo = defaultSub && (defaultSub.title || defaultSub.description);
216
+ return [
217
+ {
218
+ name: displayName,
219
+ title: hasDefaultSubInfo ? defaultSub.title : c.title,
220
+ description: hasDefaultSubInfo ? defaultSub.description : c.description,
221
+ aliases: displayAliases?.length ? displayAliases : undefined,
222
+ deprecated: c.deprecated,
223
+ hidden: c.hidden,
224
+ },
225
+ {
226
+ name: displayName,
227
+ title: c.title,
228
+ description: c.description,
229
+ deprecated: c.deprecated,
230
+ hidden: c.hidden,
231
+ hasSubcommands: true,
232
+ },
233
+ ];
234
+ }
235
+
236
+ return [
237
+ {
238
+ name: displayName,
239
+ title: c.title,
240
+ description: c.description,
241
+ aliases: displayAliases?.length ? displayAliases : undefined,
242
+ deprecated: c.deprecated,
243
+ hidden: c.hidden,
244
+ hasSubcommands,
245
+ },
246
+ ];
247
+ }),
248
+ ];
193
249
 
194
250
  // In 'full' detail mode, recursively build help for all nested commands
195
251
  if (detail === 'full') {
@@ -197,28 +253,68 @@ function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpOptions['detail'] = 'st
197
253
  }
198
254
  }
199
255
 
200
- // Build arguments info from positional options
201
- if (hasArguments) {
202
- helpInfo.arguments = positionalArgs;
256
+ // Build arguments info from positionals
257
+ if (hasPositionals) {
258
+ helpInfo.positionals = positionalArgs;
203
259
  }
204
260
 
205
- // Build options info with aliases (excluding positional args)
206
- if (cmd.options) {
207
- const optionsInfo = extractOptionsInfo(cmd.options, cmd.meta, positionalNames);
208
- const optMap: Record<string, HelpOptionInfo> = Object.fromEntries(optionsInfo.map((opt) => [opt.name, opt]));
261
+ // Build arguments info with aliases (excluding positional args)
262
+ if (cmd.argsSchema) {
263
+ const argsInfo = extractArgsInfo(cmd.argsSchema, cmd.meta, positionalNames);
264
+ const argMap: Record<string, HelpArgumentInfo> = Object.fromEntries(argsInfo.map((arg) => [arg.name, arg]));
209
265
 
210
- // Merge aliases into options
211
- const { aliases } = extractSchemaMetadata(cmd.options, cmd.meta?.options);
266
+ // Merge aliases into arguments
267
+ const { aliases } = extractSchemaMetadata(cmd.argsSchema, cmd.meta?.fields);
212
268
  for (const [alias, name] of Object.entries(aliases)) {
213
- const opt = optMap[name];
214
- if (!opt) continue;
215
- opt.aliases = [...(opt.aliases || []), alias];
269
+ const arg = argMap[name];
270
+ if (!arg) continue;
271
+ arg.aliases = [...(arg.aliases || []), alias];
272
+ }
273
+
274
+ // Filter out hidden arguments
275
+ const visibleArgs = argsInfo.filter((arg) => !arg.hidden);
276
+ if (visibleArgs.length > 0) {
277
+ helpInfo.arguments = visibleArgs;
278
+ helpInfo.usage.hasArguments = true;
279
+ }
280
+ }
281
+
282
+ // Add built-in commands/flags for root command only
283
+ if (!cmd.parent) {
284
+ const builtins: HelpInfo['builtins'] = [];
285
+
286
+ if (!findCommandByName('help', cmd.commands)) {
287
+ builtins.push({
288
+ name: 'help [command], -h, --help',
289
+ description: 'Show help for a command',
290
+ sub: [
291
+ { name: '--detail <level>', description: 'Detail level (minimal, standard, full)' },
292
+ { name: '--format <format>', description: 'Output format (text, ansi, json, markdown, html)' },
293
+ ],
294
+ });
295
+ }
296
+
297
+ if (!findCommandByName('version', cmd.commands)) {
298
+ builtins.push({
299
+ name: 'version, -v, --version',
300
+ description: 'Show version information',
301
+ });
216
302
  }
217
303
 
218
- // Filter out hidden options
219
- const visibleOptions = optionsInfo.filter((opt) => !opt.hidden);
220
- if (visibleOptions.length > 0) {
221
- helpInfo.options = visibleOptions;
304
+ if (!findCommandByName('completion', cmd.commands)) {
305
+ builtins.push({
306
+ name: 'completion [shell]',
307
+ description: 'Generate shell completions (bash, zsh, fish, powershell)',
308
+ });
309
+ }
310
+
311
+ builtins.push({
312
+ name: '[command] --repl',
313
+ description: 'Start interactive REPL scoped to a command',
314
+ });
315
+
316
+ if (builtins.length > 0) {
317
+ helpInfo.builtins = builtins;
222
318
  }
223
319
  }
224
320
 
@@ -229,8 +325,8 @@ function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpOptions['detail'] = 'st
229
325
  // Main Entry Point
230
326
  // ============================================================================
231
327
 
232
- export function generateHelp(rootCommand: AnyPadroneCommand, commandObj: AnyPadroneCommand = rootCommand, options?: HelpOptions): string {
233
- const helpInfo = getHelpInfo(commandObj, options?.detail);
234
- const formatter = createFormatter(options?.format ?? 'auto', options?.detail);
328
+ export function generateHelp(rootCommand: AnyPadroneCommand, commandObj: AnyPadroneCommand = rootCommand, prefs?: HelpPreferences): string {
329
+ const helpInfo = getHelpInfo(commandObj, prefs?.detail);
330
+ const formatter = createFormatter(prefs?.format ?? 'auto', prefs?.detail);
235
331
  return formatter.format(helpInfo);
236
332
  }
package/src/index.ts CHANGED
@@ -1,26 +1,23 @@
1
- export { createPadrone } from './create.ts';
2
- export type { HelpArgumentInfo, HelpFormat, HelpInfo, HelpOptionInfo, HelpSubcommandInfo } from './formatter.ts';
3
- export type { HelpOptions } from './help.ts';
4
- export type { PadroneOptionsMeta } from './options.ts';
5
- export type {
6
- InferCommand,
7
- InferConfigInput,
8
- InferConfigOutput,
9
- InferEnvInput,
10
- InferEnvOutput,
11
- InferOptionsInput,
12
- InferOptionsOutput,
13
- } from './type-helpers.ts';
1
+ export { asyncSchema, buildReplCompleter, createPadrone } from './create.ts';
2
+ export type { PadroneErrorOptions } from './errors.ts';
3
+ export { ActionError, ConfigError, PadroneError, RoutingError, ValidationError } from './errors.ts';
4
+ export type { HelpInfo } from './formatter.ts';
5
+ export type { InteractiveMode, InteractivePromptConfig, PadroneRuntime } from './runtime.ts';
6
+ export { REPL_SIGINT } from './runtime.ts';
7
+ export type { InferArgsInput, InferArgsOutput, InferCommand } from './type-helpers.ts';
14
8
  export type {
9
+ AnyPadroneBuilder,
15
10
  AnyPadroneCommand,
16
11
  AnyPadroneProgram,
12
+ AsyncPadroneSchema,
13
+ PadroneActionContext,
17
14
  PadroneBuilder,
18
15
  PadroneCommand,
19
- PadroneCommandConfig,
20
16
  PadroneCommandResult,
21
- PadroneParseOptions,
22
17
  PadroneParseResult,
18
+ PadronePlugin,
23
19
  PadroneProgram,
24
20
  PadroneSchema,
25
21
  } from './types.ts';
22
+ export type { UpdateCheckConfig } from './update-check.ts';
26
23
  export type { WrapConfig, WrapResult } from './wrap.ts';
@@ -0,0 +1,169 @@
1
+ import type { InteractivePromptConfig, ResolvedPadroneRuntime } from './runtime.ts';
2
+ import type { AnyPadroneCommand } from './types.ts';
3
+
4
+ /**
5
+ * Auto-detect the prompt type for a field based on its JSON schema property definition.
6
+ */
7
+ export function detectPromptConfig(
8
+ name: string,
9
+ propSchema: Record<string, any> | undefined,
10
+ description?: string,
11
+ ): InteractivePromptConfig {
12
+ const message = description || propSchema?.description || name;
13
+
14
+ if (!propSchema) return { name, message, type: 'input' };
15
+
16
+ if (propSchema.type === 'boolean') {
17
+ return { name, message, type: 'confirm', default: propSchema.default };
18
+ }
19
+
20
+ if (propSchema.enum) {
21
+ return {
22
+ name,
23
+ message,
24
+ type: 'select',
25
+ choices: propSchema.enum.map((v: unknown) => ({ label: String(v), value: v })),
26
+ default: propSchema.default,
27
+ };
28
+ }
29
+
30
+ if (propSchema.type === 'array' && propSchema.items?.enum) {
31
+ return {
32
+ name,
33
+ message,
34
+ type: 'multiselect',
35
+ choices: propSchema.items.enum.map((v: unknown) => ({ label: String(v), value: v })),
36
+ default: propSchema.default,
37
+ };
38
+ }
39
+
40
+ if (propSchema.format === 'password') {
41
+ return { name, message, type: 'password', default: propSchema.default };
42
+ }
43
+
44
+ return { name, message, type: 'input', default: propSchema.default };
45
+ }
46
+
47
+ /**
48
+ * Prompt for missing interactive fields.
49
+ * Runs after env/config preprocessing and before schema validation.
50
+ *
51
+ * When `force` is true, all configured interactive fields are prompted even if they already
52
+ * have values. The current values are used as defaults in the prompts.
53
+ */
54
+ export async function promptInteractiveFields(
55
+ data: Record<string, unknown>,
56
+ command: AnyPadroneCommand,
57
+ runtime: ResolvedPadroneRuntime,
58
+ force?: boolean,
59
+ ): Promise<Record<string, unknown>> {
60
+ if (!runtime.prompt) return data;
61
+
62
+ const meta = command.meta;
63
+ const interactiveConfig = meta?.interactive;
64
+ const optionalInteractiveConfig = meta?.optionalInteractive;
65
+ if (!interactiveConfig && !optionalInteractiveConfig) return data;
66
+
67
+ // Extract JSON schema properties for prompt type detection
68
+ let jsonProperties: Record<string, any> = {};
69
+ let requiredFields: Set<string> = new Set();
70
+ if (command.argsSchema) {
71
+ try {
72
+ const jsonSchema = command.argsSchema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
73
+ if (jsonSchema.type === 'object' && jsonSchema.properties) {
74
+ jsonProperties = jsonSchema.properties;
75
+ }
76
+ if (Array.isArray(jsonSchema.required)) {
77
+ requiredFields = new Set(jsonSchema.required);
78
+ }
79
+ } catch {
80
+ // Ignore schema parsing errors
81
+ }
82
+ }
83
+
84
+ const fieldDescriptions: Record<string, string | undefined> = {};
85
+ if (meta?.fields) {
86
+ for (const [key, value] of Object.entries(meta.fields)) {
87
+ if (value?.description) fieldDescriptions[key] = value.description;
88
+ }
89
+ }
90
+
91
+ const result = { ...data };
92
+
93
+ // Determine which required interactive fields to prompt
94
+ let fieldsToPrompt: string[] = [];
95
+ if (interactiveConfig === true) {
96
+ if (force) {
97
+ // When forced, prompt all required fields regardless of current value
98
+ fieldsToPrompt = [...requiredFields];
99
+ } else {
100
+ // All required fields that are missing
101
+ fieldsToPrompt = [...requiredFields].filter((name) => result[name] === undefined);
102
+ }
103
+ } else if (Array.isArray(interactiveConfig)) {
104
+ if (force) {
105
+ fieldsToPrompt = [...interactiveConfig];
106
+ } else {
107
+ fieldsToPrompt = interactiveConfig.filter((name) => result[name] === undefined);
108
+ }
109
+ }
110
+
111
+ // Prompt each required interactive field
112
+ for (const field of fieldsToPrompt) {
113
+ const config = detectPromptConfig(field, jsonProperties[field], fieldDescriptions[field]);
114
+ // When forced, use the current value as the default
115
+ if (force && result[field] !== undefined) {
116
+ config.default = result[field];
117
+ }
118
+ result[field] = await runtime.prompt(config);
119
+ }
120
+
121
+ // Determine optional interactive fields
122
+ let optionalFields: string[] = [];
123
+ if (optionalInteractiveConfig === true) {
124
+ if (force) {
125
+ // When forced, include all non-required fields (even those with values)
126
+ const allKeys = Object.keys(jsonProperties);
127
+ optionalFields = allKeys.filter((name) => !requiredFields.has(name));
128
+ } else {
129
+ // All non-required fields that are still missing
130
+ const allKeys = Object.keys(jsonProperties);
131
+ optionalFields = allKeys.filter((name) => !requiredFields.has(name) && result[name] === undefined);
132
+ }
133
+ } else if (Array.isArray(optionalInteractiveConfig)) {
134
+ if (force) {
135
+ optionalFields = [...optionalInteractiveConfig];
136
+ } else {
137
+ optionalFields = optionalInteractiveConfig.filter((name) => result[name] === undefined);
138
+ }
139
+ }
140
+
141
+ // Show multiselect for optional fields, then prompt selected ones
142
+ if (optionalFields.length > 0) {
143
+ const selected = (await runtime.prompt({
144
+ name: '_optionalFields',
145
+ message: 'Would you also like to configure:',
146
+ type: 'multiselect',
147
+ choices: optionalFields.map((f) => {
148
+ const label = fieldDescriptions[f] || jsonProperties[f]?.description || f;
149
+ const currentValue = result[f];
150
+ // When forced, show current value next to the label for fields that already have values
151
+ const displayLabel = force && currentValue !== undefined ? `${label} (current: ${currentValue})` : label;
152
+ return { label: displayLabel, value: f };
153
+ }),
154
+ })) as string[];
155
+
156
+ if (Array.isArray(selected)) {
157
+ for (const field of selected) {
158
+ const config = detectPromptConfig(field, jsonProperties[field], fieldDescriptions[field]);
159
+ // When forced, use the current value as the default
160
+ if (force && result[field] !== undefined) {
161
+ config.default = result[field];
162
+ }
163
+ result[field] = await runtime.prompt(config);
164
+ }
165
+ }
166
+ }
167
+
168
+ return result;
169
+ }
package/src/parse.ts CHANGED
@@ -19,20 +19,20 @@ type ParseParts = {
19
19
  value: string;
20
20
  };
21
21
  /**
22
- * An option provided to the command, prefixed with `--`.
23
- * If the option has an `=` sign, the value after it is used as the option's value.
22
+ * An arg provided to the command, prefixed with `--`.
23
+ * If the arg has an `=` sign, the value after it is used as the arg's value.
24
24
  * Otherwise, the value is obtained from the next part or set to `true` if no value is provided.
25
- * The key is an array representing the path for nested options (e.g., `--user.id=123` becomes `['user', 'id']`).
25
+ * The key is an array representing the path for nested args (e.g., `--user.id=123` becomes `['user', 'id']`).
26
26
  */
27
- option: {
28
- type: 'option';
27
+ named: {
28
+ type: 'named';
29
29
  key: string[];
30
30
  value?: string | string[];
31
31
  negated?: boolean;
32
32
  };
33
33
  /**
34
- * An alias option provided to the command, prefixed with `-`.
35
- * Which option it maps to cannot be determined until the command structure is known.
34
+ * An alias arg provided to the command, prefixed with `-`.
35
+ * Which arg it maps to cannot be determined until the command structure is known.
36
36
  * Aliases cannot be nested, so the key is always a single-element array.
37
37
  */
38
38
  alias: {
@@ -112,31 +112,46 @@ export function parseCliInputToParts(input: string): ParsePart[] {
112
112
  const parts = tokenizeInput(input.trim());
113
113
  const result: ParsePart[] = [];
114
114
 
115
- let pendingValue: ParseParts['option'] | ParseParts['alias'] | undefined;
115
+ let pendingValue: ParseParts['named'] | ParseParts['alias'] | undefined;
116
116
  let allowTerm = true;
117
+ let afterDoubleDash = false;
117
118
 
118
119
  for (const part of parts) {
119
120
  if (!part) continue;
121
+
122
+ // Bare `--` separator: everything after is a literal positional arg
123
+ if (part === '--' && !afterDoubleDash) {
124
+ if (pendingValue) pendingValue = undefined;
125
+ afterDoubleDash = true;
126
+ allowTerm = false;
127
+ continue;
128
+ }
129
+
130
+ if (afterDoubleDash) {
131
+ result.push({ type: 'arg', value: part });
132
+ continue;
133
+ }
134
+
120
135
  const wasPending = pendingValue;
121
136
  pendingValue = undefined;
122
137
 
123
138
  if (part.startsWith('--no-') && part.length > 5) {
124
- // Negated boolean option (--no-verbose or --no-config.debug)
139
+ // Negated boolean arg (--no-verbose or --no-config.debug)
125
140
  const keyStr = part.slice(5);
126
141
  const key = keyStr.split('.');
127
- const p = { type: 'option' as const, key, value: undefined, negated: true };
142
+ const p = { type: 'named' as const, key, value: undefined, negated: true };
128
143
  result.push(p);
129
144
  } else if (part.startsWith('--')) {
130
- const [keyStr = '', value] = splitOptionValue(part.slice(2));
145
+ const [keyStr = '', value] = splitNamedArgValue(part.slice(2));
131
146
  const key = keyStr.split('.');
132
147
 
133
- const p = { type: 'option' as const, key, value };
148
+ const p = { type: 'named' as const, key, value };
134
149
  if (typeof value === 'undefined') pendingValue = p;
135
150
  result.push(p);
136
151
  } else if (part.startsWith('-') && part.length > 1 && !/^-\d/.test(part)) {
137
- // Short option (but not negative numbers like -5)
152
+ // Short arg (but not negative numbers like -5)
138
153
  // Aliases cannot be nested, so key is always a single-element array
139
- const [keyStr = '', value] = splitOptionValue(part.slice(1));
154
+ const [keyStr = '', value] = splitNamedArgValue(part.slice(1));
140
155
  const key = [keyStr];
141
156
 
142
157
  const p = { type: 'alias' as const, key, value };
@@ -155,9 +170,9 @@ export function parseCliInputToParts(input: string): ParsePart[] {
155
170
  }
156
171
 
157
172
  /**
158
- * Split option key and value, handling quoted values after =.
173
+ * Split named arg key and value, handling quoted values after =.
159
174
  */
160
- function splitOptionValue(str: string): [string, string | string[] | undefined] {
175
+ function splitNamedArgValue(str: string): [string, string | string[] | undefined] {
161
176
  const eqIndex = str.indexOf('=');
162
177
  if (eqIndex === -1) return [str, undefined];
163
178