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.
- package/CHANGELOG.md +38 -1
- package/LICENSE +1 -1
- package/README.md +60 -30
- package/dist/args-CKNh7Dm9.mjs +175 -0
- package/dist/args-CKNh7Dm9.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 +1348 -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 +404 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/formatter-Dvx7jFXr.d.mts +82 -0
- package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
- package/dist/help-mUIX0T0V.mjs +1195 -0
- package/dist/help-mUIX0T0V.mjs.map +1 -0
- package/dist/index.d.mts +120 -546
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1180 -1197
- 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-qrtt0135.d.mts +1037 -0
- package/dist/types-qrtt0135.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 +365 -0
- package/src/cli/completions.ts +29 -0
- package/src/cli/docs.ts +86 -0
- package/src/cli/doctor.ts +312 -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 +184 -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 +501 -0
- package/src/completion.ts +110 -97
- package/src/create.ts +1036 -305
- package/src/docs/index.ts +607 -0
- package/src/errors.ts +131 -0
- package/src/formatter.ts +149 -63
- package/src/help.ts +151 -55
- package/src/index.ts +12 -15
- package/src/interactive.ts +169 -0
- package/src/parse.ts +31 -16
- 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({
|
|
@@ -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,68 @@ 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
|
|
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
|
|
211
|
-
const { aliases } = extractSchemaMetadata(cmd.
|
|
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
|
|
214
|
-
if (!
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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,
|
|
233
|
-
const helpInfo = getHelpInfo(commandObj,
|
|
234
|
-
const formatter = createFormatter(
|
|
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 {
|
|
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,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['
|
|
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
|
|
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] =
|
|
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
|
|
173
|
+
* Split named arg key and value, handling quoted values after =.
|
|
159
174
|
*/
|
|
160
|
-
function
|
|
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
|
|