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
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { fieldMetaToCode } from '../schema-to-code.ts';
|
|
2
|
+
import type { CodeBuilder, CommandMeta, FieldMeta, GeneratorContext } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
const JS_RESERVED = new Set([
|
|
5
|
+
'break',
|
|
6
|
+
'case',
|
|
7
|
+
'catch',
|
|
8
|
+
'continue',
|
|
9
|
+
'debugger',
|
|
10
|
+
'default',
|
|
11
|
+
'delete',
|
|
12
|
+
'do',
|
|
13
|
+
'else',
|
|
14
|
+
'export',
|
|
15
|
+
'extends',
|
|
16
|
+
'finally',
|
|
17
|
+
'for',
|
|
18
|
+
'function',
|
|
19
|
+
'if',
|
|
20
|
+
'import',
|
|
21
|
+
'in',
|
|
22
|
+
'instanceof',
|
|
23
|
+
'new',
|
|
24
|
+
'return',
|
|
25
|
+
'super',
|
|
26
|
+
'switch',
|
|
27
|
+
'this',
|
|
28
|
+
'throw',
|
|
29
|
+
'try',
|
|
30
|
+
'typeof',
|
|
31
|
+
'var',
|
|
32
|
+
'void',
|
|
33
|
+
'while',
|
|
34
|
+
'with',
|
|
35
|
+
'yield',
|
|
36
|
+
'class',
|
|
37
|
+
'const',
|
|
38
|
+
'enum',
|
|
39
|
+
'let',
|
|
40
|
+
'static',
|
|
41
|
+
'implements',
|
|
42
|
+
'interface',
|
|
43
|
+
'package',
|
|
44
|
+
'private',
|
|
45
|
+
'protected',
|
|
46
|
+
'public',
|
|
47
|
+
'await',
|
|
48
|
+
'async',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/** Convert a command name to a safe JS identifier (camelCase, reserved-word-safe). */
|
|
52
|
+
export function toSafeIdentifier(name: string): string {
|
|
53
|
+
const camel = name.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
54
|
+
if (/^\d/.test(camel)) return `_${camel}`;
|
|
55
|
+
if (JS_RESERVED.has(camel)) return `_${camel}`;
|
|
56
|
+
return camel;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Build the exported function name for a command (e.g. 'repo' → 'repoCommand'). */
|
|
60
|
+
export function toCommandFunctionName(name: string): string {
|
|
61
|
+
return `${toSafeIdentifier(name)}Command`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CommandFileOptions {
|
|
65
|
+
/** Wrap config: generates .wrap() instead of .action(). */
|
|
66
|
+
wrap?: {
|
|
67
|
+
/** The external command to wrap (e.g. 'gh'). */
|
|
68
|
+
command: string;
|
|
69
|
+
/** Fixed args preceding the options (e.g. ['pr', 'list']). */
|
|
70
|
+
args?: string[];
|
|
71
|
+
};
|
|
72
|
+
/** Subcommand references to wire into this command via .command() calls. */
|
|
73
|
+
subcommands?: { name: string; varName: string; importPath: string; aliases?: string[] }[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate a single Padrone command file from a CommandMeta.
|
|
78
|
+
* Produces a named function that chains .configure(), .arguments(), and .wrap() or .action().
|
|
79
|
+
*/
|
|
80
|
+
export function generateCommandFile(command: CommandMeta, ctx: GeneratorContext, options?: CommandFileOptions): CodeBuilder {
|
|
81
|
+
const code = ctx.createCodeBuilder();
|
|
82
|
+
|
|
83
|
+
const hasArgs = (command.arguments && command.arguments.length > 0) || (command.positionals && command.positionals.length > 0);
|
|
84
|
+
|
|
85
|
+
if (hasArgs) {
|
|
86
|
+
code.import('z', 'zod/v4');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
code.importType('AnyPadroneBuilder', 'padrone');
|
|
90
|
+
|
|
91
|
+
// Import subcommand modules
|
|
92
|
+
if (options?.subcommands) {
|
|
93
|
+
for (const sub of options.subcommands) {
|
|
94
|
+
code.import([sub.varName], sub.importPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
code.line();
|
|
99
|
+
|
|
100
|
+
if (command.deprecated) {
|
|
101
|
+
const msg = typeof command.deprecated === 'string' ? command.deprecated : 'This command is deprecated';
|
|
102
|
+
code.comment(`@deprecated ${msg}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fnName = toCommandFunctionName(command.name);
|
|
106
|
+
code.line(`export function ${fnName}<T extends AnyPadroneBuilder>(cmd: T) {`);
|
|
107
|
+
code.line(` return cmd`);
|
|
108
|
+
|
|
109
|
+
// .configure()
|
|
110
|
+
const configParts: string[] = [];
|
|
111
|
+
if (command.description) {
|
|
112
|
+
configParts.push(`description: ${JSON.stringify(command.description)}`);
|
|
113
|
+
}
|
|
114
|
+
if (command.deprecated) {
|
|
115
|
+
configParts.push(`deprecated: ${typeof command.deprecated === 'string' ? JSON.stringify(command.deprecated) : 'true'}`);
|
|
116
|
+
}
|
|
117
|
+
if (configParts.length > 0) {
|
|
118
|
+
code.line(` .configure({ ${configParts.join(', ')} })`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// .arguments()
|
|
122
|
+
if (hasArgs) {
|
|
123
|
+
const allFields = [...(command.arguments || []), ...(command.positionals || [])];
|
|
124
|
+
const schemaCode = fieldMetaToCode(allFields);
|
|
125
|
+
|
|
126
|
+
const positionalNames = (command.positionals || []).map((p) => (p.type === 'array' ? `'...${p.name}'` : `'${p.name}'`));
|
|
127
|
+
const fieldsMap = buildFieldsMap(allFields);
|
|
128
|
+
const hasMetaOptions = positionalNames.length > 0 || fieldsMap;
|
|
129
|
+
|
|
130
|
+
if (hasMetaOptions) {
|
|
131
|
+
code.line(` .arguments(${schemaCode.code}, {`);
|
|
132
|
+
if (positionalNames.length > 0) {
|
|
133
|
+
code.line(` positional: [${positionalNames.join(', ')}],`);
|
|
134
|
+
}
|
|
135
|
+
if (fieldsMap) {
|
|
136
|
+
code.line(` fields: ${fieldsMap},`);
|
|
137
|
+
}
|
|
138
|
+
code.line(` })`);
|
|
139
|
+
} else {
|
|
140
|
+
code.line(` .arguments(${schemaCode.code})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// .command() calls for subcommands
|
|
145
|
+
if (options?.subcommands) {
|
|
146
|
+
for (const sub of options.subcommands) {
|
|
147
|
+
const nameArg =
|
|
148
|
+
sub.aliases && sub.aliases.length > 0
|
|
149
|
+
? `[${JSON.stringify(sub.name)}, ${sub.aliases.map((a) => JSON.stringify(a)).join(', ')}]`
|
|
150
|
+
: JSON.stringify(sub.name);
|
|
151
|
+
code.line(` .command(${nameArg}, ${sub.varName})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// .wrap() or .action()
|
|
156
|
+
if (options?.wrap) {
|
|
157
|
+
const wrapParts: string[] = [];
|
|
158
|
+
wrapParts.push(`command: ${JSON.stringify(options.wrap.command)}`);
|
|
159
|
+
if (options.wrap.args && options.wrap.args.length > 0) {
|
|
160
|
+
wrapParts.push(`args: [${options.wrap.args.map((a) => JSON.stringify(a)).join(', ')}]`);
|
|
161
|
+
}
|
|
162
|
+
code.line(` .wrap({ ${wrapParts.join(', ')} })`);
|
|
163
|
+
} else {
|
|
164
|
+
code.line(` .action((args) => { /* TODO */ })`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
code.line(`}`);
|
|
168
|
+
|
|
169
|
+
return code;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildFieldsMap(fields: FieldMeta[]): string | null {
|
|
173
|
+
const entries: string[] = [];
|
|
174
|
+
for (const field of fields) {
|
|
175
|
+
if (field.aliases && field.aliases.length > 0) {
|
|
176
|
+
const alias =
|
|
177
|
+
field.aliases.length === 1 ? JSON.stringify(field.aliases[0]) : `[${field.aliases.map((a) => JSON.stringify(a)).join(', ')}]`;
|
|
178
|
+
const key = /^[a-zA-Z_$][\w$]*$/.test(field.name) ? field.name : JSON.stringify(field.name);
|
|
179
|
+
entries.push(`${key}: { alias: ${alias} }`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (entries.length === 0) return null;
|
|
183
|
+
return `{ ${entries.join(', ')} }`;
|
|
184
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { fieldMetaToCode } from '../schema-to-code.ts';
|
|
2
|
+
import type { CommandMeta, GeneratorContext } from '../types.ts';
|
|
3
|
+
import type { CommandFileOptions } from './command-file.ts';
|
|
4
|
+
import { generateCommandFile, toCommandFunctionName } from './command-file.ts';
|
|
5
|
+
|
|
6
|
+
export interface CommandTreeOptions {
|
|
7
|
+
/** When set, generates .wrap() calls instead of .action(). */
|
|
8
|
+
wrap?: {
|
|
9
|
+
/** The external command being wrapped (e.g. 'gh'). */
|
|
10
|
+
command: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Walk a CommandMeta tree and emit one file per command plus a root program file.
|
|
16
|
+
* Maps nested subcommands to a directory structure.
|
|
17
|
+
*/
|
|
18
|
+
export function generateCommandTree(root: CommandMeta, ctx: GeneratorContext, options?: CommandTreeOptions): void {
|
|
19
|
+
const rootImports: { name: string; varName: string; path: string; aliases?: string[] }[] = [];
|
|
20
|
+
|
|
21
|
+
// Recursively generate command files (depth-first so children exist before parents)
|
|
22
|
+
function walkCommands(cmd: CommandMeta, dirPath: string, parentArgs: string[]): void {
|
|
23
|
+
if (cmd === root) {
|
|
24
|
+
for (const sub of cmd.subcommands || []) {
|
|
25
|
+
walkCommands(sub, 'commands', []);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const filePath = `${dirPath}/${cmd.name}.ts`;
|
|
31
|
+
|
|
32
|
+
// Recurse into subcommands first so we can reference them
|
|
33
|
+
const childRefs: { name: string; varName: string; importPath: string; aliases?: string[] }[] = [];
|
|
34
|
+
if (cmd.subcommands && cmd.subcommands.length > 0) {
|
|
35
|
+
for (const sub of cmd.subcommands) {
|
|
36
|
+
walkCommands(sub, `${dirPath}/${cmd.name}`, [...parentArgs, cmd.name]);
|
|
37
|
+
childRefs.push({
|
|
38
|
+
name: sub.name,
|
|
39
|
+
varName: toCommandFunctionName(sub.name),
|
|
40
|
+
importPath: `./${cmd.name}/${sub.name}.ts`,
|
|
41
|
+
aliases: sub.aliases,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const fileOptions: CommandFileOptions = {};
|
|
47
|
+
if (options?.wrap) {
|
|
48
|
+
fileOptions.wrap = { command: options.wrap.command, args: [...parentArgs, cmd.name] };
|
|
49
|
+
}
|
|
50
|
+
if (childRefs.length > 0) {
|
|
51
|
+
fileOptions.subcommands = childRefs;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const code = generateCommandFile(cmd, ctx, Object.keys(fileOptions).length > 0 ? fileOptions : undefined);
|
|
55
|
+
ctx.emitter.addFile(filePath, code.build());
|
|
56
|
+
|
|
57
|
+
rootImports.push({
|
|
58
|
+
name: cmd.name,
|
|
59
|
+
varName: toCommandFunctionName(cmd.name),
|
|
60
|
+
path: `./${filePath.replace(/\.ts$/, '.ts')}`,
|
|
61
|
+
aliases: cmd.aliases,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
walkCommands(root, '', []);
|
|
66
|
+
|
|
67
|
+
// Generate root program.ts
|
|
68
|
+
const program = ctx.createCodeBuilder();
|
|
69
|
+
|
|
70
|
+
const rootHasArgs = (root.arguments && root.arguments.length > 0) || (root.positionals && root.positionals.length > 0);
|
|
71
|
+
if (rootHasArgs) {
|
|
72
|
+
program.import('z', 'zod/v4');
|
|
73
|
+
}
|
|
74
|
+
program.import(['createPadrone'], 'padrone');
|
|
75
|
+
|
|
76
|
+
// Only import direct children of root
|
|
77
|
+
const directChildren = rootImports.filter((imp) => imp.path.split('/').length <= 3);
|
|
78
|
+
for (const imp of directChildren) {
|
|
79
|
+
program.import([imp.varName], imp.path);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
program.line();
|
|
83
|
+
program.line(`const program = createPadrone(${JSON.stringify(root.name)})`);
|
|
84
|
+
|
|
85
|
+
// .configure()
|
|
86
|
+
const configParts: string[] = [];
|
|
87
|
+
if (root.description) {
|
|
88
|
+
configParts.push(`description: ${JSON.stringify(root.description)}`);
|
|
89
|
+
}
|
|
90
|
+
if (configParts.length > 0) {
|
|
91
|
+
program.line(` .configure({ ${configParts.join(', ')} })`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Root arguments (for programs that have options at the root level)
|
|
95
|
+
if (rootHasArgs) {
|
|
96
|
+
const allFields = [...(root.arguments || []), ...(root.positionals || [])];
|
|
97
|
+
const schemaCode = fieldMetaToCode(allFields);
|
|
98
|
+
program.line(` .arguments(${schemaCode.code})`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Chain .command() calls for direct children
|
|
102
|
+
for (const imp of directChildren) {
|
|
103
|
+
const nameArg =
|
|
104
|
+
imp.aliases && imp.aliases.length > 0
|
|
105
|
+
? `[${JSON.stringify(imp.name)}, ${imp.aliases.map((a) => JSON.stringify(a)).join(', ')}]`
|
|
106
|
+
: JSON.stringify(imp.name);
|
|
107
|
+
program.line(` .command(${nameArg}, ${imp.varName})`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If root has no subcommands, add .wrap() or .action()
|
|
111
|
+
if (directChildren.length === 0 && options?.wrap) {
|
|
112
|
+
program.line(` .wrap({ command: ${JSON.stringify(options.wrap.command)} })`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
program.line();
|
|
116
|
+
program.line(`export default program`);
|
|
117
|
+
|
|
118
|
+
ctx.emitter.addFile('program.ts', program.build());
|
|
119
|
+
|
|
120
|
+
// Generate index.ts
|
|
121
|
+
const index = ctx.createCodeBuilder();
|
|
122
|
+
index.line(`export { default } from './program.ts'`);
|
|
123
|
+
ctx.emitter.addFile('index.ts', index.build());
|
|
124
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Types
|
|
2
|
+
|
|
3
|
+
// Core utilities
|
|
4
|
+
export { createCodeBuilder } from './code-builder.ts';
|
|
5
|
+
export type { DiscoveryOptions, DiscoveryResult, DiscoverySource } from './discovery.ts';
|
|
6
|
+
// Discovery
|
|
7
|
+
export { discoverCli } from './discovery.ts';
|
|
8
|
+
export { createFileEmitter } from './file-emitter.ts';
|
|
9
|
+
// Generators
|
|
10
|
+
export { generateBarrelFile } from './generators/barrel-file.ts';
|
|
11
|
+
export type { CommandFileOptions } from './generators/command-file.ts';
|
|
12
|
+
export { generateCommandFile } from './generators/command-file.ts';
|
|
13
|
+
export type { CommandTreeOptions } from './generators/command-tree.ts';
|
|
14
|
+
export { generateCommandTree } from './generators/command-tree.ts';
|
|
15
|
+
// Parsers
|
|
16
|
+
export { parseFishCompletions } from './parsers/fish.ts';
|
|
17
|
+
export { parseHelpOutput } from './parsers/help.ts';
|
|
18
|
+
export { mergeCommandMeta } from './parsers/merge.ts';
|
|
19
|
+
export { parseZshCompletions } from './parsers/zsh.ts';
|
|
20
|
+
export { fieldMetaToCode, schemaToCode } from './schema-to-code.ts';
|
|
21
|
+
export { template } from './template.ts';
|
|
22
|
+
export type {
|
|
23
|
+
CodeBuilder,
|
|
24
|
+
CodeBuildResult,
|
|
25
|
+
CommandMeta,
|
|
26
|
+
EmitResult,
|
|
27
|
+
FieldMeta,
|
|
28
|
+
FileEmitter,
|
|
29
|
+
FileEmitterOptions,
|
|
30
|
+
GeneratorContext,
|
|
31
|
+
GeneratorLogger,
|
|
32
|
+
TemplateFunction,
|
|
33
|
+
} from './types.ts';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { CommandMeta, FieldMeta } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse fish shell completion scripts into CommandMeta.
|
|
5
|
+
*
|
|
6
|
+
* Fish completions use the `complete` builtin:
|
|
7
|
+
* complete -c <command> -s <short> -l <long> -d <description> -a <arguments> -r -f
|
|
8
|
+
*/
|
|
9
|
+
export function parseFishCompletions(text: string): CommandMeta {
|
|
10
|
+
const lines = text.split('\n');
|
|
11
|
+
const result: CommandMeta = {
|
|
12
|
+
name: '',
|
|
13
|
+
arguments: [],
|
|
14
|
+
subcommands: [],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const subcommandMap = new Map<string, CommandMeta>();
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
22
|
+
|
|
23
|
+
// Match: complete -c <command> [options]
|
|
24
|
+
const completeMatch = trimmed.match(/^complete\s+/);
|
|
25
|
+
if (!completeMatch) continue;
|
|
26
|
+
|
|
27
|
+
const parts = parseCompleteLine(trimmed);
|
|
28
|
+
if (!parts) continue;
|
|
29
|
+
|
|
30
|
+
// Set root command name
|
|
31
|
+
if (!result.name && parts.command) {
|
|
32
|
+
result.name = parts.command;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If this completion has a condition like "__fish_seen_subcommand_from <sub>"
|
|
36
|
+
// it belongs to a subcommand
|
|
37
|
+
const subcommandCondition = parts.condition?.match(/__fish_seen_subcommand_from\s+(\S+)/);
|
|
38
|
+
if (subcommandCondition) {
|
|
39
|
+
const subName = subcommandCondition[1]!;
|
|
40
|
+
let sub = subcommandMap.get(subName);
|
|
41
|
+
if (!sub) {
|
|
42
|
+
sub = { name: subName, arguments: [] };
|
|
43
|
+
subcommandMap.set(subName, sub);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (parts.longFlag || parts.shortFlag) {
|
|
47
|
+
const field = completionToField(parts);
|
|
48
|
+
if (field) sub.arguments!.push(field);
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If this defines a subcommand (has -a with no flags)
|
|
54
|
+
if (parts.arguments && !parts.longFlag && !parts.shortFlag) {
|
|
55
|
+
// Arguments list could be subcommand names
|
|
56
|
+
const names = parts.arguments.split(/\s+/);
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
if (!name || name.startsWith('(')) continue;
|
|
59
|
+
if (!subcommandMap.has(name)) {
|
|
60
|
+
subcommandMap.set(name, {
|
|
61
|
+
name,
|
|
62
|
+
description: parts.description,
|
|
63
|
+
arguments: [],
|
|
64
|
+
});
|
|
65
|
+
} else if (parts.description) {
|
|
66
|
+
subcommandMap.get(name)!.description = parts.description;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Global option
|
|
73
|
+
if (parts.longFlag || parts.shortFlag) {
|
|
74
|
+
const field = completionToField(parts);
|
|
75
|
+
if (field) result.arguments!.push(field);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add subcommands
|
|
80
|
+
for (const sub of subcommandMap.values()) {
|
|
81
|
+
if (sub.arguments!.length === 0) delete sub.arguments;
|
|
82
|
+
result.subcommands!.push(sub);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (result.arguments!.length === 0) delete result.arguments;
|
|
86
|
+
if (result.subcommands!.length === 0) delete result.subcommands;
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface CompleteParts {
|
|
92
|
+
command?: string;
|
|
93
|
+
shortFlag?: string;
|
|
94
|
+
longFlag?: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
arguments?: string;
|
|
97
|
+
condition?: string;
|
|
98
|
+
requiresArg?: boolean;
|
|
99
|
+
noFiles?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseCompleteLine(line: string): CompleteParts | null {
|
|
103
|
+
const parts: CompleteParts = {};
|
|
104
|
+
|
|
105
|
+
// Extract -c <command>
|
|
106
|
+
const cmdMatch = line.match(/-c\s+(\S+)/);
|
|
107
|
+
if (cmdMatch) parts.command = cmdMatch[1];
|
|
108
|
+
|
|
109
|
+
// Extract -s <short>
|
|
110
|
+
const shortMatch = line.match(/-s\s+(\S+)/);
|
|
111
|
+
if (shortMatch) parts.shortFlag = shortMatch[1];
|
|
112
|
+
|
|
113
|
+
// Extract -l <long>
|
|
114
|
+
const longMatch = line.match(/-l\s+(\S+)/);
|
|
115
|
+
if (longMatch) parts.longFlag = longMatch[1];
|
|
116
|
+
|
|
117
|
+
// Extract -d '<description>' or -d "<description>"
|
|
118
|
+
const descMatch = line.match(/-d\s+['"]([^'"]+)['"]/) || line.match(/-d\s+(\S+)/);
|
|
119
|
+
if (descMatch) parts.description = descMatch[1];
|
|
120
|
+
|
|
121
|
+
// Extract -a '<arguments>'
|
|
122
|
+
const argsMatch = line.match(/-a\s+['"]([^'"]+)['"]/) || line.match(/-a\s+(\S+)/);
|
|
123
|
+
if (argsMatch) parts.arguments = argsMatch[1];
|
|
124
|
+
|
|
125
|
+
// Extract -n '<condition>'
|
|
126
|
+
const condMatch = line.match(/-n\s+['"]([^'"]+)['"]/) || line.match(/-n\s+(\S+)/);
|
|
127
|
+
if (condMatch) parts.condition = condMatch[1];
|
|
128
|
+
|
|
129
|
+
// -r means requires argument
|
|
130
|
+
parts.requiresArg = /-r\b/.test(line);
|
|
131
|
+
// -f means no file completion
|
|
132
|
+
parts.noFiles = /-f\b/.test(line);
|
|
133
|
+
|
|
134
|
+
return parts;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function completionToField(parts: CompleteParts): FieldMeta | null {
|
|
138
|
+
const name = parts.longFlag ? parts.longFlag.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()) : parts.shortFlag || '';
|
|
139
|
+
|
|
140
|
+
if (!name) return null;
|
|
141
|
+
|
|
142
|
+
let type: FieldMeta['type'] = parts.requiresArg ? 'string' : 'boolean';
|
|
143
|
+
let enumValues: string[] | undefined;
|
|
144
|
+
|
|
145
|
+
// If -a provides specific values, treat as enum
|
|
146
|
+
if (parts.arguments) {
|
|
147
|
+
const values = parts.arguments.split(/\s+/).filter((v) => !v.startsWith('('));
|
|
148
|
+
if (values.length > 0 && values.length <= 20) {
|
|
149
|
+
enumValues = values;
|
|
150
|
+
type = 'enum';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const aliases = parts.shortFlag && parts.longFlag ? [`-${parts.shortFlag}`] : undefined;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name,
|
|
158
|
+
type,
|
|
159
|
+
description: parts.description,
|
|
160
|
+
aliases,
|
|
161
|
+
enumValues,
|
|
162
|
+
};
|
|
163
|
+
}
|