padrone 1.1.0 → 1.3.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 +97 -1
  2. package/LICENSE +1 -1
  3. package/README.md +60 -30
  4. package/dist/args-DFEI7_G_.mjs +197 -0
  5. package/dist/args-DFEI7_G_.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 +1358 -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 +405 -0
  18. package/dist/docs/index.mjs.map +1 -0
  19. package/dist/formatter-XroimS3Q.d.mts +83 -0
  20. package/dist/formatter-XroimS3Q.d.mts.map +1 -0
  21. package/dist/help-CgGP7hQU.mjs +1229 -0
  22. package/dist/help-CgGP7hQU.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 +1220 -1204
  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-BS7RP5Ls.d.mts +1059 -0
  32. package/dist/types-BS7RP5Ls.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 +457 -0
  37. package/src/cli/completions.ts +29 -0
  38. package/src/cli/docs.ts +86 -0
  39. package/src/cli/doctor.ts +330 -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 +197 -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 +504 -0
  61. package/src/completion.ts +110 -97
  62. package/src/create.ts +1048 -308
  63. package/src/docs/index.ts +607 -0
  64. package/src/errors.ts +131 -0
  65. package/src/formatter.ts +195 -73
  66. package/src/help.ts +159 -58
  67. package/src/index.ts +12 -15
  68. package/src/interactive.ts +169 -0
  69. package/src/parse.ts +52 -21
  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({
@@ -124,12 +126,12 @@ function extractOptionsInfo(schema: StandardJSONSchemaV1, meta?: PadroneMeta, po
124
126
  description: optMeta?.description ?? prop.description,
125
127
  optional: isOptional,
126
128
  default: prop.default,
127
- type: propType,
129
+ type: propType === 'array' ? `${prop.items?.type || 'string'}[]` : propType,
128
130
  enum: enumValues,
129
131
  deprecated: optMeta?.deprecated ?? prop?.deprecated,
130
132
  hidden: optMeta?.hidden ?? prop?.hidden,
131
133
  examples: optMeta?.examples ?? prop?.examples,
132
- variadic: propType === 'array', // Arrays are always variadic
134
+ variadic: propType === 'array',
133
135
  negatable: isNegatable,
134
136
  });
135
137
  }
@@ -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,73 @@ 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]));
209
-
210
- // Merge aliases into options
211
- const { aliases } = extractSchemaMetadata(cmd.options, cmd.meta?.options);
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]));
265
+
266
+ // Merge flags and aliases into arguments
267
+ const { flags, aliases } = extractSchemaMetadata(cmd.argsSchema, cmd.meta?.fields, cmd.meta?.autoAlias);
268
+ for (const [flag, name] of Object.entries(flags)) {
269
+ const arg = argMap[name];
270
+ if (!arg) continue;
271
+ arg.flags = [...(arg.flags || []), flag];
272
+ }
212
273
  for (const [alias, name] of Object.entries(aliases)) {
213
- const opt = optMap[name];
214
- if (!opt) continue;
215
- opt.aliases = [...(opt.aliases || []), alias];
274
+ const arg = argMap[name];
275
+ if (!arg) continue;
276
+ arg.aliases = [...(arg.aliases || []), alias];
277
+ }
278
+
279
+ // Filter out hidden arguments
280
+ const visibleArgs = argsInfo.filter((arg) => !arg.hidden);
281
+ if (visibleArgs.length > 0) {
282
+ helpInfo.arguments = visibleArgs;
283
+ helpInfo.usage.hasArguments = true;
216
284
  }
285
+ }
286
+
287
+ // Add built-in commands/flags for root command only
288
+ if (!cmd.parent) {
289
+ const builtins: HelpInfo['builtins'] = [];
290
+
291
+ if (!findCommandByName('help', cmd.commands)) {
292
+ builtins.push({
293
+ name: 'help [command], -h, --help',
294
+ description: 'Show help for a command',
295
+ sub: [
296
+ { name: '--detail <level>', description: 'Detail level (minimal, standard, full)' },
297
+ { name: '--format <format>', description: 'Output format (text, ansi, json, markdown, html)' },
298
+ ],
299
+ });
300
+ }
301
+
302
+ if (!findCommandByName('version', cmd.commands)) {
303
+ builtins.push({
304
+ name: 'version, -v, --version',
305
+ description: 'Show version information',
306
+ });
307
+ }
308
+
309
+ if (!findCommandByName('completion', cmd.commands)) {
310
+ builtins.push({
311
+ name: 'completion [shell]',
312
+ description: 'Generate shell completions (bash, zsh, fish, powershell)',
313
+ });
314
+ }
315
+
316
+ builtins.push({
317
+ name: '[command] --repl',
318
+ description: 'Start interactive REPL scoped to a command',
319
+ });
217
320
 
218
- // Filter out hidden options
219
- const visibleOptions = optionsInfo.filter((opt) => !opt.hidden);
220
- if (visibleOptions.length > 0) {
221
- helpInfo.options = visibleOptions;
321
+ if (builtins.length > 0) {
322
+ helpInfo.builtins = builtins;
222
323
  }
223
324
  }
224
325
 
@@ -229,8 +330,8 @@ function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpOptions['detail'] = 'st
229
330
  // Main Entry Point
230
331
  // ============================================================================
231
332
 
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);
333
+ export function generateHelp(rootCommand: AnyPadroneCommand, commandObj: AnyPadroneCommand = rootCommand, prefs?: HelpPreferences): string {
334
+ const helpInfo = getHelpInfo(commandObj, prefs?.detail);
335
+ const formatter = createFormatter(prefs?.format ?? 'auto', prefs?.detail);
235
336
  return formatter.format(helpInfo);
236
337
  }
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,36 +112,67 @@ 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)
138
- // Aliases cannot be nested, so key is always a single-element array
139
- const [keyStr = '', value] = splitOptionValue(part.slice(1));
140
- const key = [keyStr];
152
+ // Short flag(s) (but not negative numbers like -5)
153
+ // Supports flag stacking: -abc -a -b -c (last flag can take a value)
154
+ const [keyStr = '', value] = splitNamedArgValue(part.slice(1));
141
155
 
142
- const p = { type: 'alias' as const, key, value };
143
- if (typeof value === 'undefined') pendingValue = p;
144
- result.push(p);
156
+ if (keyStr.length > 1 && typeof value === 'undefined') {
157
+ // Flag stacking: -abc → -a, -b, -c (all set to true except last which can take next arg's value)
158
+ for (let ci = 0; ci < keyStr.length - 1; ci++) {
159
+ result.push({ type: 'alias' as const, key: [keyStr[ci]!], value: undefined });
160
+ }
161
+ const lastFlag = { type: 'alias' as const, key: [keyStr[keyStr.length - 1]!], value: undefined as string | string[] | undefined };
162
+ pendingValue = lastFlag;
163
+ result.push(lastFlag);
164
+ } else if (keyStr.length > 1 && typeof value !== 'undefined') {
165
+ // -abc=val → -a, -b, -c=val (stacked with value on last)
166
+ for (let ci = 0; ci < keyStr.length - 1; ci++) {
167
+ result.push({ type: 'alias' as const, key: [keyStr[ci]!], value: undefined });
168
+ }
169
+ result.push({ type: 'alias' as const, key: [keyStr[keyStr.length - 1]!], value });
170
+ } else {
171
+ // Single char: -v or -v=value
172
+ const p = { type: 'alias' as const, key: [keyStr], value };
173
+ if (typeof value === 'undefined') pendingValue = p;
174
+ result.push(p);
175
+ }
145
176
  } else if (wasPending) {
146
177
  wasPending.value = part;
147
178
  } else if (/^[a-zA-Z0-9_-]+$/.test(part) && allowTerm) {
@@ -155,9 +186,9 @@ export function parseCliInputToParts(input: string): ParsePart[] {
155
186
  }
156
187
 
157
188
  /**
158
- * Split option key and value, handling quoted values after =.
189
+ * Split named arg key and value, handling quoted values after =.
159
190
  */
160
- function splitOptionValue(str: string): [string, string | string[] | undefined] {
191
+ function splitNamedArgValue(str: string): [string, string | string[] | undefined] {
161
192
  const eqIndex = str.indexOf('=');
162
193
  if (eqIndex === -1) return [str, undefined];
163
194