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,221 @@
|
|
|
1
|
+
import type { CommandMeta, FieldMeta } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse zsh completion function definitions into CommandMeta.
|
|
5
|
+
*
|
|
6
|
+
* Zsh completions typically use _arguments or compadd:
|
|
7
|
+
* _arguments \
|
|
8
|
+
* '-v[verbose mode]' \
|
|
9
|
+
* '--output=[output file]:filename:_files' \
|
|
10
|
+
* '1:command:(start stop restart)'
|
|
11
|
+
*/
|
|
12
|
+
export function parseZshCompletions(text: string): CommandMeta {
|
|
13
|
+
const result: CommandMeta = {
|
|
14
|
+
name: '',
|
|
15
|
+
arguments: [],
|
|
16
|
+
positionals: [],
|
|
17
|
+
subcommands: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Try to detect command name from function name: _command() or #compdef command
|
|
21
|
+
const compdefMatch = text.match(/#compdef\s+(\S+)/);
|
|
22
|
+
if (compdefMatch) {
|
|
23
|
+
result.name = compdefMatch[1]!;
|
|
24
|
+
} else {
|
|
25
|
+
const funcMatch = text.match(/^_(\w+)\s*\(\)/m);
|
|
26
|
+
if (funcMatch) {
|
|
27
|
+
result.name = funcMatch[1]!;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find _arguments blocks
|
|
32
|
+
const argumentsBlocks = findArgumentsBlocks(text);
|
|
33
|
+
|
|
34
|
+
for (const block of argumentsBlocks) {
|
|
35
|
+
const specs = parseArgumentSpecs(block);
|
|
36
|
+
for (const spec of specs) {
|
|
37
|
+
if (spec.positional) {
|
|
38
|
+
result.positionals!.push(spec);
|
|
39
|
+
} else {
|
|
40
|
+
result.arguments!.push(spec);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Find subcommand definitions from case statements or _describe
|
|
46
|
+
const subcommands = findSubcommands(text);
|
|
47
|
+
result.subcommands!.push(...subcommands);
|
|
48
|
+
|
|
49
|
+
if (result.arguments!.length === 0) delete result.arguments;
|
|
50
|
+
if (result.positionals!.length === 0) delete result.positionals;
|
|
51
|
+
if (result.subcommands!.length === 0) delete result.subcommands;
|
|
52
|
+
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract _arguments blocks from zsh completion text.
|
|
58
|
+
* Handles backslash continuation lines.
|
|
59
|
+
*/
|
|
60
|
+
function findArgumentsBlocks(text: string): string[] {
|
|
61
|
+
const blocks: string[] = [];
|
|
62
|
+
|
|
63
|
+
// Join continuation lines
|
|
64
|
+
const joined = text.replace(/\\\n\s*/g, ' ');
|
|
65
|
+
const lines = joined.split('\n');
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const match = line.match(/_arguments\s+(.+)/);
|
|
69
|
+
if (match) {
|
|
70
|
+
blocks.push(match[1]!);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return blocks;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse individual argument specs from an _arguments line.
|
|
79
|
+
*/
|
|
80
|
+
function parseArgumentSpecs(block: string): FieldMeta[] {
|
|
81
|
+
const results: FieldMeta[] = [];
|
|
82
|
+
|
|
83
|
+
// Split into individual specs (quoted strings)
|
|
84
|
+
const specs = extractQuotedStrings(block);
|
|
85
|
+
|
|
86
|
+
for (const spec of specs) {
|
|
87
|
+
const field = parseZshSpec(spec);
|
|
88
|
+
if (field) results.push(field);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract single-quoted strings from an _arguments block.
|
|
96
|
+
*/
|
|
97
|
+
function extractQuotedStrings(text: string): string[] {
|
|
98
|
+
const strings: string[] = [];
|
|
99
|
+
const regex = /'([^']+)'/g;
|
|
100
|
+
let match: RegExpExecArray | null;
|
|
101
|
+
|
|
102
|
+
while ((match = regex.exec(text)) !== null) {
|
|
103
|
+
strings.push(match[1]!);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return strings;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parse a single zsh argument spec.
|
|
111
|
+
*
|
|
112
|
+
* Formats:
|
|
113
|
+
* '-v[verbose mode]' → boolean flag
|
|
114
|
+
* '--output=[output file]:filename:_files' → string with value
|
|
115
|
+
* '(-v --verbose)'{-v,--verbose}'[verbose mode]' → flag with aliases
|
|
116
|
+
* '*--flag[repeatable]' → array
|
|
117
|
+
* '1:command:(start stop restart)' → positional enum
|
|
118
|
+
* ':filename:_files' → positional
|
|
119
|
+
*/
|
|
120
|
+
function parseZshSpec(spec: string): FieldMeta | null {
|
|
121
|
+
// Positional argument: 'N:description:action' or ':description:action'
|
|
122
|
+
const positionalMatch = spec.match(/^(\d+)?:([^:]*):(.*)$/);
|
|
123
|
+
if (positionalMatch) {
|
|
124
|
+
const description = positionalMatch[2] || undefined;
|
|
125
|
+
|
|
126
|
+
// Check for enum values in (val1 val2 val3)
|
|
127
|
+
const enumMatch = positionalMatch[3]?.match(/^\(([^)]+)\)$/);
|
|
128
|
+
if (enumMatch) {
|
|
129
|
+
const values = enumMatch[1]!.split(/\s+/);
|
|
130
|
+
return {
|
|
131
|
+
name: description?.toLowerCase().replace(/\s+/g, '_') || `arg${positionalMatch[1] || '0'}`,
|
|
132
|
+
type: 'enum',
|
|
133
|
+
enumValues: values,
|
|
134
|
+
description,
|
|
135
|
+
positional: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
name: description?.toLowerCase().replace(/\s+/g, '_') || `arg${positionalMatch[1] || '0'}`,
|
|
141
|
+
type: 'string',
|
|
142
|
+
description,
|
|
143
|
+
positional: true,
|
|
144
|
+
ambiguous: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Flag argument
|
|
149
|
+
const repeatable = spec.startsWith('*');
|
|
150
|
+
const flagSpec = repeatable ? spec.slice(1) : spec;
|
|
151
|
+
|
|
152
|
+
// Extract flag name and description
|
|
153
|
+
// Pattern: --flag=[description]:value_description:action
|
|
154
|
+
// Pattern: -f[description]
|
|
155
|
+
const flagMatch = flagSpec.match(/^(-{1,2}[\w-]+)(?:=?)(?:\[([^\]]*)\])?(?::([^:]*):?(.*))?$/);
|
|
156
|
+
if (!flagMatch) return null;
|
|
157
|
+
|
|
158
|
+
const rawName = flagMatch[1]!;
|
|
159
|
+
const description = flagMatch[2] || undefined;
|
|
160
|
+
const valueName = flagMatch[3] || undefined;
|
|
161
|
+
|
|
162
|
+
const name = rawName.replace(/^-+/, '').replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
|
|
163
|
+
const hasValue = rawName.includes('=') || !!valueName;
|
|
164
|
+
let type: FieldMeta['type'] = hasValue ? 'string' : 'boolean';
|
|
165
|
+
|
|
166
|
+
if (repeatable) type = 'array';
|
|
167
|
+
|
|
168
|
+
// Check for enum values
|
|
169
|
+
let enumValues: string[] | undefined;
|
|
170
|
+
if (flagMatch[4]) {
|
|
171
|
+
const enumMatch = flagMatch[4].match(/^\(([^)]+)\)$/);
|
|
172
|
+
if (enumMatch) {
|
|
173
|
+
enumValues = enumMatch[1]!.split(/\s+/);
|
|
174
|
+
type = 'enum';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
name,
|
|
180
|
+
type,
|
|
181
|
+
description,
|
|
182
|
+
enumValues,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find subcommand definitions from _describe calls or case statements.
|
|
188
|
+
*/
|
|
189
|
+
function findSubcommands(text: string): CommandMeta[] {
|
|
190
|
+
const subcommands: CommandMeta[] = [];
|
|
191
|
+
const seen = new Set<string>();
|
|
192
|
+
|
|
193
|
+
// Look for _describe patterns: _describe 'command' commands
|
|
194
|
+
// where commands is an array like: ('start:start the service' 'stop:stop the service')
|
|
195
|
+
const describeRegex = /\(([^)]+)\)/g;
|
|
196
|
+
const joined = text.replace(/\\\n\s*/g, ' ');
|
|
197
|
+
|
|
198
|
+
// Look for arrays of 'name:description' patterns near _describe
|
|
199
|
+
let match: RegExpExecArray | null;
|
|
200
|
+
while ((match = describeRegex.exec(joined)) !== null) {
|
|
201
|
+
const content = match[1]!;
|
|
202
|
+
const entries = content.match(/'([^']+)'/g) || content.match(/"([^"]+)"/g);
|
|
203
|
+
if (!entries) continue;
|
|
204
|
+
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const clean = entry.replace(/^['"]|['"]$/g, '');
|
|
207
|
+
const parts = clean.split(':');
|
|
208
|
+
if (parts.length >= 2 && /^[\w-]+$/.test(parts[0]!)) {
|
|
209
|
+
const name = parts[0]!;
|
|
210
|
+
if (seen.has(name)) continue;
|
|
211
|
+
seen.add(name);
|
|
212
|
+
subcommands.push({
|
|
213
|
+
name,
|
|
214
|
+
description: parts.slice(1).join(':'),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return subcommands;
|
|
221
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import type { FieldMeta } from './types.ts';
|
|
3
|
+
|
|
4
|
+
interface SchemaToCodeResult {
|
|
5
|
+
/** The generated Zod source code */
|
|
6
|
+
code: string;
|
|
7
|
+
/** Imports needed (e.g. ['z']) */
|
|
8
|
+
imports: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert a JSON Schema property to Zod code.
|
|
13
|
+
*/
|
|
14
|
+
function jsonSchemaPropertyToZod(prop: Record<string, any>, required: boolean, ambiguous?: boolean): string {
|
|
15
|
+
let code: string;
|
|
16
|
+
|
|
17
|
+
const type = prop.type as string | undefined;
|
|
18
|
+
const enumValues = prop.enum as unknown[] | undefined;
|
|
19
|
+
|
|
20
|
+
if (enumValues && enumValues.length > 0) {
|
|
21
|
+
const values = enumValues.map((v) => JSON.stringify(v)).join(', ');
|
|
22
|
+
code = `z.enum([${values}])`;
|
|
23
|
+
} else if (type === 'string') {
|
|
24
|
+
code = 'z.string()';
|
|
25
|
+
} else if (type === 'number' || type === 'integer') {
|
|
26
|
+
code = 'z.number()';
|
|
27
|
+
} else if (type === 'boolean') {
|
|
28
|
+
code = 'z.boolean()';
|
|
29
|
+
} else if (type === 'array') {
|
|
30
|
+
const items = prop.items as Record<string, any> | undefined;
|
|
31
|
+
const itemCode = items ? jsonSchemaPropertyToZod(items, true) : 'z.unknown()';
|
|
32
|
+
code = `${itemCode}.array()`;
|
|
33
|
+
} else {
|
|
34
|
+
code = 'z.unknown()';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (prop.default !== undefined) {
|
|
38
|
+
code += `.default(${JSON.stringify(prop.default)})`;
|
|
39
|
+
} else if (!required) {
|
|
40
|
+
code += '.optional()';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (prop.description) {
|
|
44
|
+
code += `.describe(${JSON.stringify(prop.description)})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (ambiguous) {
|
|
48
|
+
code += ' /* TODO: verify type */';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return code;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generate Zod source code from a Standard Schema instance by introspecting
|
|
56
|
+
* its `~standard.jsonSchema` interface.
|
|
57
|
+
*/
|
|
58
|
+
export function schemaToCode(schema: StandardSchemaV1): SchemaToCodeResult {
|
|
59
|
+
try {
|
|
60
|
+
const jsonSchema = (schema as any)['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
61
|
+
return jsonSchemaToCode(jsonSchema);
|
|
62
|
+
} catch {
|
|
63
|
+
return { code: 'z.unknown()', imports: ['z'] };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function jsonSchemaToCode(jsonSchema: Record<string, any>): SchemaToCodeResult {
|
|
68
|
+
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
69
|
+
const properties = jsonSchema.properties as Record<string, any>;
|
|
70
|
+
const required = new Set((jsonSchema.required as string[]) || []);
|
|
71
|
+
|
|
72
|
+
const entries = Object.entries(properties).map(([key, prop]) => {
|
|
73
|
+
const isRequired = required.has(key);
|
|
74
|
+
const zodCode = jsonSchemaPropertyToZod(prop as Record<string, any>, isRequired);
|
|
75
|
+
return ` ${key}: ${zodCode},`;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const code = `z.object({\n${entries.join('\n')}\n})`;
|
|
79
|
+
return { code, imports: ['z'] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fallback for non-object schemas
|
|
83
|
+
const code = jsonSchemaPropertyToZod(jsonSchema, true);
|
|
84
|
+
return { code, imports: ['z'] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build a real Zod schema from FieldMeta objects.
|
|
89
|
+
* Returns the schema as Zod source code, since we can't dynamically import Zod here
|
|
90
|
+
* (codegen has no runtime dependency on padrone's main entry point).
|
|
91
|
+
*/
|
|
92
|
+
export function fieldMetaToCode(fields: FieldMeta[]): SchemaToCodeResult {
|
|
93
|
+
const entries = fields.map((field) => {
|
|
94
|
+
let code: string;
|
|
95
|
+
|
|
96
|
+
switch (field.type) {
|
|
97
|
+
case 'string':
|
|
98
|
+
code = 'z.string()';
|
|
99
|
+
break;
|
|
100
|
+
case 'number':
|
|
101
|
+
code = 'z.number()';
|
|
102
|
+
break;
|
|
103
|
+
case 'boolean':
|
|
104
|
+
code = 'z.boolean()';
|
|
105
|
+
break;
|
|
106
|
+
case 'array': {
|
|
107
|
+
const itemType = field.items || 'string';
|
|
108
|
+
const itemCode = itemType === 'number' ? 'z.number()' : 'z.string()';
|
|
109
|
+
code = `${itemCode}.array()`;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case 'enum':
|
|
113
|
+
if (field.enumValues && field.enumValues.length > 0) {
|
|
114
|
+
const values = field.enumValues.map((v) => JSON.stringify(v)).join(', ');
|
|
115
|
+
code = `z.enum([${values}])`;
|
|
116
|
+
} else {
|
|
117
|
+
code = 'z.string()';
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
default:
|
|
121
|
+
code = 'z.unknown()';
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (field.default !== undefined) {
|
|
126
|
+
code += `.default(${JSON.stringify(field.default)})`;
|
|
127
|
+
} else if (!field.required) {
|
|
128
|
+
code += '.optional()';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (field.description) {
|
|
132
|
+
code += `.describe(${JSON.stringify(field.description)})`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (field.ambiguous) {
|
|
136
|
+
code += ' /* TODO: verify type */';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const key = needsQuoting(field.name) ? JSON.stringify(field.name) : field.name;
|
|
140
|
+
return ` ${key}: ${code},`;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const code = `z.object({\n${entries.join('\n')}\n})`;
|
|
144
|
+
return { code, imports: ['z'] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const JS_RESERVED = new Set([
|
|
148
|
+
'break',
|
|
149
|
+
'case',
|
|
150
|
+
'catch',
|
|
151
|
+
'continue',
|
|
152
|
+
'debugger',
|
|
153
|
+
'default',
|
|
154
|
+
'delete',
|
|
155
|
+
'do',
|
|
156
|
+
'else',
|
|
157
|
+
'export',
|
|
158
|
+
'extends',
|
|
159
|
+
'finally',
|
|
160
|
+
'for',
|
|
161
|
+
'function',
|
|
162
|
+
'if',
|
|
163
|
+
'import',
|
|
164
|
+
'in',
|
|
165
|
+
'instanceof',
|
|
166
|
+
'new',
|
|
167
|
+
'return',
|
|
168
|
+
'super',
|
|
169
|
+
'switch',
|
|
170
|
+
'this',
|
|
171
|
+
'throw',
|
|
172
|
+
'try',
|
|
173
|
+
'typeof',
|
|
174
|
+
'var',
|
|
175
|
+
'void',
|
|
176
|
+
'while',
|
|
177
|
+
'with',
|
|
178
|
+
'yield',
|
|
179
|
+
'class',
|
|
180
|
+
'const',
|
|
181
|
+
'enum',
|
|
182
|
+
'let',
|
|
183
|
+
'static',
|
|
184
|
+
'implements',
|
|
185
|
+
'interface',
|
|
186
|
+
'package',
|
|
187
|
+
'private',
|
|
188
|
+
'protected',
|
|
189
|
+
'public',
|
|
190
|
+
'await',
|
|
191
|
+
'async',
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
/** Returns true if the name needs quoting to be a valid JS object key. */
|
|
195
|
+
function needsQuoting(name: string): boolean {
|
|
196
|
+
if (JS_RESERVED.has(name)) return true;
|
|
197
|
+
// Must be a valid identifier: starts with letter/$/_, contains only word chars
|
|
198
|
+
return !/^[a-zA-Z_$][\w$]*$/.test(name);
|
|
199
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight string template engine.
|
|
3
|
+
*
|
|
4
|
+
* Syntax:
|
|
5
|
+
* - `{{var}}` — interpolation
|
|
6
|
+
* - `{{#arr}}...{{/arr}}` — iteration (`.` refers to current item)
|
|
7
|
+
* - `{{#bool}}...{{/bool}}` — conditional blocks
|
|
8
|
+
* - `{{>partial}}` — include a named sub-template
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type TemplateRenderer = (data: Record<string, unknown>, partials?: Record<string, string>) => string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Compile a template string into a reusable render function.
|
|
15
|
+
*/
|
|
16
|
+
export function template(text: string): TemplateRenderer {
|
|
17
|
+
return (data: Record<string, unknown>, partials?: Record<string, string>) => {
|
|
18
|
+
return render(text, data, partials);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function render(text: string, data: Record<string, unknown>, partials?: Record<string, string>): string {
|
|
23
|
+
let result = text;
|
|
24
|
+
|
|
25
|
+
// Process block sections: {{#key}}...{{/key}}
|
|
26
|
+
result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_match, key: string, body: string) => {
|
|
27
|
+
const value = data[key];
|
|
28
|
+
|
|
29
|
+
if (value === undefined || value === null || value === false) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
return value
|
|
35
|
+
.map((item) => {
|
|
36
|
+
if (typeof item === 'object' && item !== null) {
|
|
37
|
+
return render(body, item as Record<string, unknown>, partials);
|
|
38
|
+
}
|
|
39
|
+
// For primitive items, replace {{.}} with the item value
|
|
40
|
+
return body.replace(/\{\{\.\}\}/g, String(item));
|
|
41
|
+
})
|
|
42
|
+
.join('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Truthy conditional
|
|
46
|
+
if (typeof value === 'object') {
|
|
47
|
+
return render(body, value as Record<string, unknown>, partials);
|
|
48
|
+
}
|
|
49
|
+
return render(body, data, partials);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Process partials: {{>partialName}}
|
|
53
|
+
if (partials) {
|
|
54
|
+
result = result.replace(/\{\{>(\w+)\}\}/g, (_match, name: string) => {
|
|
55
|
+
const partialText = partials[name];
|
|
56
|
+
if (!partialText) return '';
|
|
57
|
+
return render(partialText, data, partials);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Process interpolation: {{var}}
|
|
62
|
+
result = result.replace(/\{\{(\w+)\}\}/g, (_match, key: string) => {
|
|
63
|
+
const value = data[key];
|
|
64
|
+
if (value === undefined || value === null) return '';
|
|
65
|
+
return String(value);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata for a single field/option/flag parsed from CLI help or completion data.
|
|
3
|
+
*/
|
|
4
|
+
export interface FieldMeta {
|
|
5
|
+
name: string;
|
|
6
|
+
type: 'string' | 'number' | 'boolean' | 'array' | 'enum' | 'unknown';
|
|
7
|
+
/** For arrays: the item type */
|
|
8
|
+
items?: string;
|
|
9
|
+
/** For enums: the allowed values */
|
|
10
|
+
enumValues?: string[];
|
|
11
|
+
description?: string;
|
|
12
|
+
default?: unknown;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
aliases?: string[];
|
|
15
|
+
positional?: boolean;
|
|
16
|
+
/** Mark fields the parser wasn't confident about */
|
|
17
|
+
ambiguous?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Intermediate representation for a CLI command.
|
|
22
|
+
* All parsers produce these, all generators consume them.
|
|
23
|
+
*/
|
|
24
|
+
export interface CommandMeta {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
aliases?: string[];
|
|
28
|
+
/** Named options/flags */
|
|
29
|
+
arguments?: FieldMeta[];
|
|
30
|
+
/** Positional arguments */
|
|
31
|
+
positionals?: FieldMeta[];
|
|
32
|
+
/** Recursive subcommands */
|
|
33
|
+
subcommands?: CommandMeta[];
|
|
34
|
+
examples?: string[];
|
|
35
|
+
deprecated?: boolean | string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Logger interface for generators to report progress.
|
|
40
|
+
*/
|
|
41
|
+
export interface GeneratorLogger {
|
|
42
|
+
info(message: string): void;
|
|
43
|
+
warn(message: string): void;
|
|
44
|
+
error(message: string): void;
|
|
45
|
+
success(message: string): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Shared context passed to all generators.
|
|
50
|
+
*/
|
|
51
|
+
export interface GeneratorContext {
|
|
52
|
+
/** Target output directory */
|
|
53
|
+
outDir: string;
|
|
54
|
+
/** Code builder factory */
|
|
55
|
+
createCodeBuilder: () => CodeBuilder;
|
|
56
|
+
/** File emitter for writing output */
|
|
57
|
+
emitter: FileEmitter;
|
|
58
|
+
/** Template engine */
|
|
59
|
+
template: TemplateFunction;
|
|
60
|
+
/** User-facing logger */
|
|
61
|
+
log: GeneratorLogger;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Result from CodeBuilder.build()
|
|
66
|
+
*/
|
|
67
|
+
export interface CodeBuildResult {
|
|
68
|
+
/** The formatted source string */
|
|
69
|
+
text: string;
|
|
70
|
+
/** Resolved import map for deduplication across files */
|
|
71
|
+
imports: Map<string, { specifiers: Set<string>; typeOnly: boolean }>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Fluent builder for constructing TypeScript source files.
|
|
76
|
+
*/
|
|
77
|
+
export interface CodeBuilder {
|
|
78
|
+
/** Add a named import: import { specifier } from source */
|
|
79
|
+
import(specifier: string | string[], source: string): CodeBuilder;
|
|
80
|
+
/** Add a default import: import name from source */
|
|
81
|
+
importDefault(name: string, source: string): CodeBuilder;
|
|
82
|
+
/** Add a type-only import: import type { specifier } from source */
|
|
83
|
+
importType(specifier: string | string[], source: string): CodeBuilder;
|
|
84
|
+
/** Add a line of code (empty string or no argument for blank line) */
|
|
85
|
+
line(code?: string): CodeBuilder;
|
|
86
|
+
/** Add a nested block with automatic indentation */
|
|
87
|
+
block(builder: (b: CodeBuilder) => CodeBuilder): CodeBuilder;
|
|
88
|
+
/** Add a nested block with open/close strings */
|
|
89
|
+
block(open: string, builder: (b: CodeBuilder) => CodeBuilder, close?: string): CodeBuilder;
|
|
90
|
+
/** Add a nested block with open string, close string override, and builder */
|
|
91
|
+
block(open: string, close: string, builder: (b: CodeBuilder) => CodeBuilder): CodeBuilder;
|
|
92
|
+
/** Add a single-line comment */
|
|
93
|
+
comment(text: string): CodeBuilder;
|
|
94
|
+
/** Add a JSDoc comment */
|
|
95
|
+
docComment(text: string): CodeBuilder;
|
|
96
|
+
/** Add a TODO comment */
|
|
97
|
+
todoComment(text: string): CodeBuilder;
|
|
98
|
+
/** Add raw pre-formatted code (no indentation adjustment) */
|
|
99
|
+
raw(code: string): CodeBuilder;
|
|
100
|
+
/** Build the final source string */
|
|
101
|
+
build(): CodeBuildResult;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Result from FileEmitter.emit()
|
|
106
|
+
*/
|
|
107
|
+
export interface EmitResult {
|
|
108
|
+
/** Files that were written */
|
|
109
|
+
written: string[];
|
|
110
|
+
/** Files that were skipped (already exist, no overwrite) */
|
|
111
|
+
skipped: string[];
|
|
112
|
+
/** Files that failed to write */
|
|
113
|
+
errors: { file: string; error: Error }[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Options for creating a FileEmitter.
|
|
118
|
+
*/
|
|
119
|
+
export interface FileEmitterOptions {
|
|
120
|
+
/** Target output directory */
|
|
121
|
+
outDir: string;
|
|
122
|
+
/** Header comment prepended to every file */
|
|
123
|
+
header?: string;
|
|
124
|
+
/** How to handle existing files: true = overwrite, false = skip */
|
|
125
|
+
overwrite?: boolean;
|
|
126
|
+
/** Print what would be written without writing */
|
|
127
|
+
dryRun?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Manages writing multiple generated files to disk.
|
|
132
|
+
*/
|
|
133
|
+
export interface FileEmitter {
|
|
134
|
+
/** Queue a file for writing */
|
|
135
|
+
addFile(path: string, content: string | CodeBuildResult): void;
|
|
136
|
+
/** Write all queued files to disk */
|
|
137
|
+
emit(): Promise<EmitResult>;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Template function returned by template().
|
|
142
|
+
*/
|
|
143
|
+
export type TemplateFunction = (text: string) => (data: Record<string, unknown>) => string;
|
package/src/colorizer.ts
CHANGED
|
@@ -16,7 +16,7 @@ const colors = {
|
|
|
16
16
|
|
|
17
17
|
export type Colorizer = {
|
|
18
18
|
command: (text: string) => string;
|
|
19
|
-
|
|
19
|
+
arg: (text: string) => string;
|
|
20
20
|
type: (text: string) => string;
|
|
21
21
|
description: (text: string) => string;
|
|
22
22
|
label: (text: string) => string;
|
|
@@ -29,7 +29,7 @@ export type Colorizer = {
|
|
|
29
29
|
export function createColorizer(): Colorizer {
|
|
30
30
|
return {
|
|
31
31
|
command: (text: string) => `${colors.cyan}${colors.bold}${text}${colors.reset}`,
|
|
32
|
-
|
|
32
|
+
arg: (text: string) => `${colors.green}${text}${colors.reset}`,
|
|
33
33
|
type: (text: string) => `${colors.yellow}${text}${colors.reset}`,
|
|
34
34
|
description: (text: string) => `${colors.dim}${text}${colors.reset}`,
|
|
35
35
|
label: (text: string) => `${colors.bold}${text}${colors.reset}`,
|