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.
- package/CHANGELOG.md +97 -1
- package/LICENSE +1 -1
- package/README.md +60 -30
- package/dist/args-DFEI7_G_.mjs +197 -0
- package/dist/args-DFEI7_G_.mjs.map +1 -0
- package/dist/chunk-y_GBKt04.mjs +5 -0
- package/dist/codegen/index.d.mts +305 -0
- package/dist/codegen/index.d.mts.map +1 -0
- package/dist/codegen/index.mjs +1358 -0
- package/dist/codegen/index.mjs.map +1 -0
- package/dist/completion.d.mts +64 -0
- package/dist/completion.d.mts.map +1 -0
- package/dist/completion.mjs +417 -0
- package/dist/completion.mjs.map +1 -0
- package/dist/docs/index.d.mts +34 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +405 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/formatter-XroimS3Q.d.mts +83 -0
- package/dist/formatter-XroimS3Q.d.mts.map +1 -0
- package/dist/help-CgGP7hQU.mjs +1229 -0
- package/dist/help-CgGP7hQU.mjs.map +1 -0
- package/dist/index.d.mts +120 -546
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1220 -1204
- package/dist/index.mjs.map +1 -1
- package/dist/test.d.mts +112 -0
- package/dist/test.d.mts.map +1 -0
- package/dist/test.mjs +138 -0
- package/dist/test.mjs.map +1 -0
- package/dist/types-BS7RP5Ls.d.mts +1059 -0
- package/dist/types-BS7RP5Ls.d.mts.map +1 -0
- package/dist/update-check-EbNDkzyV.mjs +146 -0
- package/dist/update-check-EbNDkzyV.mjs.map +1 -0
- package/package.json +61 -21
- package/src/args.ts +457 -0
- package/src/cli/completions.ts +29 -0
- package/src/cli/docs.ts +86 -0
- package/src/cli/doctor.ts +330 -0
- package/src/cli/index.ts +159 -0
- package/src/cli/init.ts +135 -0
- package/src/cli/link.ts +320 -0
- package/src/cli/wrap.ts +152 -0
- package/src/codegen/README.md +118 -0
- package/src/codegen/code-builder.ts +226 -0
- package/src/codegen/discovery.ts +232 -0
- package/src/codegen/file-emitter.ts +73 -0
- package/src/codegen/generators/barrel-file.ts +16 -0
- package/src/codegen/generators/command-file.ts +197 -0
- package/src/codegen/generators/command-tree.ts +124 -0
- package/src/codegen/index.ts +33 -0
- package/src/codegen/parsers/fish.ts +163 -0
- package/src/codegen/parsers/help.ts +378 -0
- package/src/codegen/parsers/merge.ts +158 -0
- package/src/codegen/parsers/zsh.ts +221 -0
- package/src/codegen/schema-to-code.ts +199 -0
- package/src/codegen/template.ts +69 -0
- package/src/codegen/types.ts +143 -0
- package/src/colorizer.ts +2 -2
- package/src/command-utils.ts +504 -0
- package/src/completion.ts +110 -97
- package/src/create.ts +1048 -308
- package/src/docs/index.ts +607 -0
- package/src/errors.ts +131 -0
- package/src/formatter.ts +195 -73
- package/src/help.ts +159 -58
- package/src/index.ts +12 -15
- package/src/interactive.ts +169 -0
- package/src/parse.ts +52 -21
- package/src/repl-loop.ts +317 -0
- package/src/runtime.ts +304 -0
- package/src/shell-utils.ts +83 -0
- package/src/test.ts +285 -0
- package/src/type-helpers.ts +10 -10
- package/src/type-utils.ts +124 -14
- package/src/types.ts +752 -154
- package/src/update-check.ts +244 -0
- package/src/wrap.ts +44 -40
- package/src/zod.d.ts +2 -2
- 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
|
|
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
|
|
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?:
|
|
25
|
-
): { args:
|
|
26
|
-
const args:
|
|
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.
|
|
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
|
|
66
|
-
const result:
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
119
|
-
// or this
|
|
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',
|
|
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:
|
|
156
|
+
export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['detail'] = 'standard'): HelpInfo {
|
|
155
157
|
const rootCmd = getRootCommand(cmd);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
const
|
|
160
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
|
201
|
-
if (
|
|
202
|
-
helpInfo.
|
|
256
|
+
// Build arguments info from positionals
|
|
257
|
+
if (hasPositionals) {
|
|
258
|
+
helpInfo.positionals = positionalArgs;
|
|
203
259
|
}
|
|
204
260
|
|
|
205
|
-
// Build
|
|
206
|
-
if (cmd.
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
// Merge aliases into
|
|
211
|
-
const { aliases } = extractSchemaMetadata(cmd.
|
|
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
|
|
214
|
-
if (!
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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,
|
|
233
|
-
const helpInfo = getHelpInfo(commandObj,
|
|
234
|
-
const formatter = createFormatter(
|
|
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 {
|
|
3
|
-
export
|
|
4
|
-
export type {
|
|
5
|
-
export type {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
23
|
-
* If the
|
|
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
|
|
25
|
+
* The key is an array representing the path for nested args (e.g., `--user.id=123` becomes `['user', 'id']`).
|
|
26
26
|
*/
|
|
27
|
-
|
|
28
|
-
type: '
|
|
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
|
|
35
|
-
* Which
|
|
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['
|
|
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
|
|
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: '
|
|
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] =
|
|
145
|
+
const [keyStr = '', value] = splitNamedArgValue(part.slice(2));
|
|
131
146
|
const key = keyStr.split('.');
|
|
132
147
|
|
|
133
|
-
const p = { type: '
|
|
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
|
|
138
|
-
//
|
|
139
|
-
const [keyStr = '', value] =
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
189
|
+
* Split named arg key and value, handling quoted values after =.
|
|
159
190
|
*/
|
|
160
|
-
function
|
|
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
|
|