padrone 1.0.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 +51 -0
- package/LICENSE +1 -1
- package/README.md +92 -49
- 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 +122 -438
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1240 -1161
- 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 -20
- 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 +1044 -284
- 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 +13 -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 +12 -12
- package/src/type-utils.ts +124 -14
- package/src/types.ts +803 -144
- package/src/update-check.ts +244 -0
- package/src/wrap.ts +185 -0
- package/src/zod.d.ts +2 -2
- package/src/options.ts +0 -180
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { CodeBuilder, CodeBuildResult } from './types.ts';
|
|
2
|
+
|
|
3
|
+
interface ImportEntry {
|
|
4
|
+
specifiers: Set<string>;
|
|
5
|
+
defaultSpecifier?: string;
|
|
6
|
+
typeOnly: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface CodeLine {
|
|
10
|
+
type: 'line' | 'raw' | 'block-open' | 'block-close';
|
|
11
|
+
content: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class CodeBuilderImpl implements CodeBuilder {
|
|
15
|
+
private imports = new Map<string, ImportEntry>();
|
|
16
|
+
private lines: CodeLine[] = [];
|
|
17
|
+
private indent: number;
|
|
18
|
+
|
|
19
|
+
constructor(indent = 0) {
|
|
20
|
+
this.indent = indent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import(specifier: string | string[], source: string): CodeBuilder {
|
|
24
|
+
const specs = Array.isArray(specifier) ? specifier : [specifier];
|
|
25
|
+
const existing = this.imports.get(source);
|
|
26
|
+
if (existing) {
|
|
27
|
+
for (const s of specs) existing.specifiers.add(s);
|
|
28
|
+
// If we're adding a value import and existing was type-only, downgrade to value
|
|
29
|
+
existing.typeOnly = false;
|
|
30
|
+
} else {
|
|
31
|
+
this.imports.set(source, { specifiers: new Set(specs), typeOnly: false });
|
|
32
|
+
}
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
importDefault(name: string, source: string): CodeBuilder {
|
|
37
|
+
const existing = this.imports.get(source);
|
|
38
|
+
if (existing) {
|
|
39
|
+
existing.defaultSpecifier = name;
|
|
40
|
+
existing.typeOnly = false;
|
|
41
|
+
} else {
|
|
42
|
+
this.imports.set(source, { specifiers: new Set(), defaultSpecifier: name, typeOnly: false });
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
importType(specifier: string | string[], source: string): CodeBuilder {
|
|
48
|
+
const specs = Array.isArray(specifier) ? specifier : [specifier];
|
|
49
|
+
const existing = this.imports.get(source);
|
|
50
|
+
if (existing) {
|
|
51
|
+
for (const s of specs) existing.specifiers.add(s);
|
|
52
|
+
// Don't downgrade: if existing is value import, keep it as value
|
|
53
|
+
} else {
|
|
54
|
+
this.imports.set(source, { specifiers: new Set(specs), typeOnly: true });
|
|
55
|
+
}
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
line(code?: string): CodeBuilder {
|
|
60
|
+
this.lines.push({ type: 'line', content: code ?? '' });
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
block(
|
|
65
|
+
openOrBuilder: string | ((b: CodeBuilder) => CodeBuilder),
|
|
66
|
+
builderOrClose?: string | ((b: CodeBuilder) => CodeBuilder),
|
|
67
|
+
closeOrBuilder?: string | ((b: CodeBuilder) => CodeBuilder),
|
|
68
|
+
): CodeBuilder {
|
|
69
|
+
let open: string | undefined;
|
|
70
|
+
let close: string | undefined;
|
|
71
|
+
let builder: (b: CodeBuilder) => CodeBuilder;
|
|
72
|
+
|
|
73
|
+
if (typeof openOrBuilder === 'function') {
|
|
74
|
+
// block(builder)
|
|
75
|
+
builder = openOrBuilder;
|
|
76
|
+
} else if (typeof builderOrClose === 'function') {
|
|
77
|
+
// block(open, builder, close?)
|
|
78
|
+
open = openOrBuilder;
|
|
79
|
+
builder = builderOrClose;
|
|
80
|
+
close = typeof closeOrBuilder === 'string' ? closeOrBuilder : undefined;
|
|
81
|
+
} else if (typeof closeOrBuilder === 'function') {
|
|
82
|
+
// block(open, close, builder)
|
|
83
|
+
open = openOrBuilder;
|
|
84
|
+
close = typeof builderOrClose === 'string' ? builderOrClose : undefined;
|
|
85
|
+
builder = closeOrBuilder;
|
|
86
|
+
} else {
|
|
87
|
+
throw new Error('Invalid block() arguments');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Always push block-open to increment indent
|
|
91
|
+
this.lines.push({ type: 'block-open', content: open ?? '' });
|
|
92
|
+
|
|
93
|
+
const inner = new CodeBuilderImpl(this.indent + 1);
|
|
94
|
+
builder(inner);
|
|
95
|
+
|
|
96
|
+
// Merge inner imports into ours
|
|
97
|
+
for (const [source, entry] of inner.imports) {
|
|
98
|
+
const existing = this.imports.get(source);
|
|
99
|
+
if (existing) {
|
|
100
|
+
for (const s of entry.specifiers) existing.specifiers.add(s);
|
|
101
|
+
if (entry.defaultSpecifier) existing.defaultSpecifier = entry.defaultSpecifier;
|
|
102
|
+
if (!entry.typeOnly) existing.typeOnly = false;
|
|
103
|
+
} else {
|
|
104
|
+
this.imports.set(source, {
|
|
105
|
+
specifiers: new Set(entry.specifiers),
|
|
106
|
+
defaultSpecifier: entry.defaultSpecifier,
|
|
107
|
+
typeOnly: entry.typeOnly,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Merge inner lines with extra indentation
|
|
113
|
+
for (const line of inner.lines) {
|
|
114
|
+
this.lines.push(line);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Always push block-close to decrement indent
|
|
118
|
+
this.lines.push({ type: 'block-close', content: close ?? '' });
|
|
119
|
+
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
comment(text: string): CodeBuilder {
|
|
124
|
+
this.lines.push({ type: 'line', content: `// ${text}` });
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
docComment(text: string): CodeBuilder {
|
|
129
|
+
const lines = text.split('\n');
|
|
130
|
+
if (lines.length === 1) {
|
|
131
|
+
this.lines.push({ type: 'line', content: `/** ${text} */` });
|
|
132
|
+
} else {
|
|
133
|
+
this.lines.push({ type: 'line', content: '/**' });
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
this.lines.push({ type: 'line', content: ` * ${line}` });
|
|
136
|
+
}
|
|
137
|
+
this.lines.push({ type: 'line', content: ' */' });
|
|
138
|
+
}
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
todoComment(text: string): CodeBuilder {
|
|
143
|
+
this.lines.push({ type: 'line', content: `// TODO: ${text}` });
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
raw(code: string): CodeBuilder {
|
|
148
|
+
this.lines.push({ type: 'raw', content: code });
|
|
149
|
+
return this;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
build(): CodeBuildResult {
|
|
153
|
+
const parts: string[] = [];
|
|
154
|
+
|
|
155
|
+
// Emit imports first
|
|
156
|
+
if (this.imports.size > 0) {
|
|
157
|
+
const typeImports: string[] = [];
|
|
158
|
+
const valueImports: string[] = [];
|
|
159
|
+
|
|
160
|
+
for (const [source, entry] of this.imports) {
|
|
161
|
+
const specs = [...entry.specifiers].sort();
|
|
162
|
+
const namedPart =
|
|
163
|
+
specs.length > 0 ? (specs.length === 1 && !specs[0]!.includes(' ') ? `{ ${specs[0]} }` : `{ ${specs.join(', ')} }`) : null;
|
|
164
|
+
const specStr = entry.defaultSpecifier
|
|
165
|
+
? namedPart
|
|
166
|
+
? `${entry.defaultSpecifier}, ${namedPart}`
|
|
167
|
+
: entry.defaultSpecifier
|
|
168
|
+
: namedPart!;
|
|
169
|
+
|
|
170
|
+
const line = entry.typeOnly ? `import type ${specStr} from '${source}'` : `import ${specStr} from '${source}'`;
|
|
171
|
+
|
|
172
|
+
if (entry.typeOnly) {
|
|
173
|
+
typeImports.push(line);
|
|
174
|
+
} else {
|
|
175
|
+
valueImports.push(line);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Value imports first, then type imports
|
|
180
|
+
parts.push([...valueImports, ...typeImports].join('\n'));
|
|
181
|
+
parts.push('');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Emit code lines
|
|
185
|
+
let currentIndent = this.indent;
|
|
186
|
+
for (const line of this.lines) {
|
|
187
|
+
if (line.type === 'raw') {
|
|
188
|
+
parts.push(line.content);
|
|
189
|
+
} else if (line.type === 'block-open') {
|
|
190
|
+
if (line.content) {
|
|
191
|
+
const indent = ' '.repeat(currentIndent);
|
|
192
|
+
parts.push(`${indent}${line.content}`);
|
|
193
|
+
}
|
|
194
|
+
currentIndent++;
|
|
195
|
+
} else if (line.type === 'block-close') {
|
|
196
|
+
currentIndent--;
|
|
197
|
+
if (line.content) {
|
|
198
|
+
const indent = ' '.repeat(currentIndent);
|
|
199
|
+
parts.push(`${indent}${line.content}`);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// Regular line
|
|
203
|
+
if (line.content === '') {
|
|
204
|
+
parts.push('');
|
|
205
|
+
} else {
|
|
206
|
+
const indent = ' '.repeat(currentIndent);
|
|
207
|
+
parts.push(`${indent}${line.content}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
text: parts.join('\n'),
|
|
214
|
+
imports: new Map(
|
|
215
|
+
[...this.imports].map(([source, entry]) => [source, { specifiers: new Set(entry.specifiers), typeOnly: entry.typeOnly }]),
|
|
216
|
+
),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create a new CodeBuilder for constructing TypeScript source files.
|
|
223
|
+
*/
|
|
224
|
+
export function createCodeBuilder(): CodeBuilder {
|
|
225
|
+
return new CodeBuilderImpl();
|
|
226
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { parseFishCompletions } from './parsers/fish.ts';
|
|
2
|
+
import { parseHelpOutput } from './parsers/help.ts';
|
|
3
|
+
import { mergeCommandMeta } from './parsers/merge.ts';
|
|
4
|
+
import { parseZshCompletions } from './parsers/zsh.ts';
|
|
5
|
+
import type { CommandMeta, GeneratorLogger } from './types.ts';
|
|
6
|
+
|
|
7
|
+
export type DiscoverySource = 'help' | 'fish' | 'zsh';
|
|
8
|
+
|
|
9
|
+
export interface DiscoveryOptions {
|
|
10
|
+
/** The command to discover (e.g. 'gh', 'docker', 'kubectl'). */
|
|
11
|
+
command: string;
|
|
12
|
+
/** Which parsing sources to use. Default: ['help'] */
|
|
13
|
+
sources?: DiscoverySource[];
|
|
14
|
+
/** Max subcommand depth. 0 = root only, undefined = unlimited. */
|
|
15
|
+
depth?: number;
|
|
16
|
+
/** Delay in ms between help invocations. Default: 50 */
|
|
17
|
+
delay?: number;
|
|
18
|
+
/** Logger for progress reporting. */
|
|
19
|
+
log?: GeneratorLogger;
|
|
20
|
+
/** Timeout per help invocation in ms. Default: 10000 */
|
|
21
|
+
timeout?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DiscoveryResult {
|
|
25
|
+
/** The discovered command tree. */
|
|
26
|
+
command: CommandMeta;
|
|
27
|
+
/** Number of help invocations made. */
|
|
28
|
+
invocations: number;
|
|
29
|
+
/** Errors encountered (non-fatal). */
|
|
30
|
+
warnings: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Discover CLI structure by running --help recursively and optionally
|
|
35
|
+
* parsing shell completion scripts.
|
|
36
|
+
*/
|
|
37
|
+
export async function discoverCli(options: DiscoveryOptions): Promise<DiscoveryResult> {
|
|
38
|
+
const { command, sources = ['help'], depth, delay = 50, log, timeout = 10000 } = options;
|
|
39
|
+
|
|
40
|
+
const warnings: string[] = [];
|
|
41
|
+
let invocations = 0;
|
|
42
|
+
|
|
43
|
+
const results: CommandMeta[] = [];
|
|
44
|
+
|
|
45
|
+
// Source 1: --help recursive crawl
|
|
46
|
+
if (sources.includes('help')) {
|
|
47
|
+
log?.info(`Discovering ${command} via --help...`);
|
|
48
|
+
const helpResult = await crawlHelp(command, [], {
|
|
49
|
+
depth,
|
|
50
|
+
delay,
|
|
51
|
+
timeout,
|
|
52
|
+
log,
|
|
53
|
+
onInvocation: () => {
|
|
54
|
+
invocations++;
|
|
55
|
+
},
|
|
56
|
+
onWarning: (msg) => {
|
|
57
|
+
warnings.push(msg);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
results.push(helpResult);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Source 2: Fish completions
|
|
64
|
+
if (sources.includes('fish')) {
|
|
65
|
+
log?.info(`Parsing fish completions for ${command}...`);
|
|
66
|
+
const fishText = await getCompletionScript(command, 'fish', timeout);
|
|
67
|
+
if (fishText) {
|
|
68
|
+
results.push(parseFishCompletions(fishText));
|
|
69
|
+
} else {
|
|
70
|
+
warnings.push('Could not obtain fish completion script');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Source 3: Zsh completions
|
|
75
|
+
if (sources.includes('zsh')) {
|
|
76
|
+
log?.info(`Parsing zsh completions for ${command}...`);
|
|
77
|
+
const zshText = await getCompletionScript(command, 'zsh', timeout);
|
|
78
|
+
if (zshText) {
|
|
79
|
+
results.push(parseZshCompletions(zshText));
|
|
80
|
+
} else {
|
|
81
|
+
warnings.push('Could not obtain zsh completion script');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const merged = results.length > 0 ? mergeCommandMeta(...results) : { name: command };
|
|
86
|
+
|
|
87
|
+
// Ensure the root has the correct name
|
|
88
|
+
if (!merged.name) merged.name = command;
|
|
89
|
+
|
|
90
|
+
return { command: merged, invocations, warnings };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface CrawlOptions {
|
|
94
|
+
depth?: number;
|
|
95
|
+
delay: number;
|
|
96
|
+
timeout: number;
|
|
97
|
+
log?: GeneratorLogger;
|
|
98
|
+
onInvocation: () => void;
|
|
99
|
+
onWarning: (msg: string) => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Breadth-first crawl of --help output.
|
|
104
|
+
*/
|
|
105
|
+
async function crawlHelp(command: string, prefixArgs: string[], options: CrawlOptions): Promise<CommandMeta> {
|
|
106
|
+
const fullCmd = [command, ...prefixArgs].join(' ');
|
|
107
|
+
|
|
108
|
+
options.onInvocation();
|
|
109
|
+
const helpText = await runHelp(command, prefixArgs, options.timeout);
|
|
110
|
+
|
|
111
|
+
if (!helpText) {
|
|
112
|
+
options.onWarning(`No help output from: ${fullCmd} --help`);
|
|
113
|
+
return { name: prefixArgs[prefixArgs.length - 1] || command };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const name = prefixArgs[prefixArgs.length - 1] || command;
|
|
117
|
+
const parsed = parseHelpOutput(helpText, { name });
|
|
118
|
+
|
|
119
|
+
options.log?.info(` ${fullCmd}: ${parsed.subcommands?.length || 0} subcommands, ${parsed.arguments?.length || 0} options`);
|
|
120
|
+
|
|
121
|
+
// Recurse into subcommands breadth-first
|
|
122
|
+
const currentDepth = prefixArgs.length;
|
|
123
|
+
if (parsed.subcommands && parsed.subcommands.length > 0 && (options.depth === undefined || currentDepth < options.depth)) {
|
|
124
|
+
const resolvedSubs: CommandMeta[] = [];
|
|
125
|
+
|
|
126
|
+
for (const sub of parsed.subcommands) {
|
|
127
|
+
if (options.delay > 0) {
|
|
128
|
+
await sleep(options.delay);
|
|
129
|
+
}
|
|
130
|
+
const resolved = await crawlHelp(command, [...prefixArgs, sub.name], options);
|
|
131
|
+
// Preserve description from parent's subcommand list if child didn't have one
|
|
132
|
+
if (!resolved.description && sub.description) {
|
|
133
|
+
resolved.description = sub.description;
|
|
134
|
+
}
|
|
135
|
+
resolvedSubs.push(resolved);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
parsed.subcommands = resolvedSubs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return parsed;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Run `<cmd> --help` or `<cmd> help` and return combined stdout+stderr.
|
|
146
|
+
*/
|
|
147
|
+
async function runHelp(command: string, args: string[], timeout: number): Promise<string | null> {
|
|
148
|
+
// Try --help first
|
|
149
|
+
let result = await runCommand(command, [...args, '--help'], timeout);
|
|
150
|
+
if (result) return result;
|
|
151
|
+
|
|
152
|
+
// Some CLIs use `help <cmd>` instead
|
|
153
|
+
if (args.length > 0) {
|
|
154
|
+
result = await runCommand(command, ['help', ...args], timeout);
|
|
155
|
+
if (result) return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Run a command and return its combined output, or null on failure.
|
|
163
|
+
*/
|
|
164
|
+
async function runCommand(command: string, args: string[], timeout: number): Promise<string | null> {
|
|
165
|
+
try {
|
|
166
|
+
const proc = Bun.spawn([command, ...args], {
|
|
167
|
+
stdout: 'pipe',
|
|
168
|
+
stderr: 'pipe',
|
|
169
|
+
stdin: 'ignore',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const timer = setTimeout(() => proc.kill(), timeout);
|
|
173
|
+
|
|
174
|
+
const [_exitCode, stdoutBuf, stderrBuf] = await Promise.all([
|
|
175
|
+
proc.exited,
|
|
176
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
177
|
+
new Response(proc.stderr).arrayBuffer(),
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
|
|
182
|
+
const stdout = new TextDecoder().decode(stdoutBuf).trim();
|
|
183
|
+
const stderr = new TextDecoder().decode(stderrBuf).trim();
|
|
184
|
+
|
|
185
|
+
// Some CLIs output help to stderr, some exit non-zero on --help
|
|
186
|
+
const combined = stdout || stderr;
|
|
187
|
+
if (!combined) return null;
|
|
188
|
+
|
|
189
|
+
// Basic sanity check: help text usually has some structure
|
|
190
|
+
if (combined.length < 10) return null;
|
|
191
|
+
|
|
192
|
+
return combined;
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Try to get a shell completion script for a command.
|
|
200
|
+
* Checks both `<cmd> completion <shell>` and well-known file paths.
|
|
201
|
+
*/
|
|
202
|
+
async function getCompletionScript(command: string, shell: 'fish' | 'zsh', timeout: number): Promise<string | null> {
|
|
203
|
+
// Try `<cmd> completion <shell>`
|
|
204
|
+
const completionArgs = ['completion', shell];
|
|
205
|
+
let result = await runCommand(command, completionArgs, timeout);
|
|
206
|
+
if (result) return result;
|
|
207
|
+
|
|
208
|
+
// Try `<cmd> completions <shell>`
|
|
209
|
+
result = await runCommand(command, ['completions', shell], timeout);
|
|
210
|
+
if (result) return result;
|
|
211
|
+
|
|
212
|
+
// Try reading from well-known paths
|
|
213
|
+
const paths =
|
|
214
|
+
shell === 'fish'
|
|
215
|
+
? [`/usr/share/fish/vendor_completions.d/${command}.fish`, `/usr/local/share/fish/vendor_completions.d/${command}.fish`]
|
|
216
|
+
: [`/usr/share/zsh/site-functions/_${command}`, `/usr/local/share/zsh/site-functions/_${command}`];
|
|
217
|
+
|
|
218
|
+
for (const path of paths) {
|
|
219
|
+
try {
|
|
220
|
+
const file = Bun.file(path);
|
|
221
|
+
if (await file.exists()) {
|
|
222
|
+
return await file.text();
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sleep(ms: number): Promise<void> {
|
|
231
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
232
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import type { CodeBuildResult, EmitResult, FileEmitter, FileEmitterOptions } from './types.ts';
|
|
4
|
+
|
|
5
|
+
interface QueuedFile {
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class FileEmitterImpl implements FileEmitter {
|
|
11
|
+
private files: QueuedFile[] = [];
|
|
12
|
+
private options: FileEmitterOptions;
|
|
13
|
+
|
|
14
|
+
constructor(options: FileEmitterOptions) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
addFile(path: string, content: string | CodeBuildResult): void {
|
|
19
|
+
const text = typeof content === 'string' ? content : content.text;
|
|
20
|
+
const fullContent = this.options.header ? `${this.options.header}\n\n${text}` : text;
|
|
21
|
+
|
|
22
|
+
this.files.push({ path, content: fullContent });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async emit(): Promise<EmitResult> {
|
|
26
|
+
const result: EmitResult = {
|
|
27
|
+
written: [],
|
|
28
|
+
skipped: [],
|
|
29
|
+
errors: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const outDir = resolve(this.options.outDir);
|
|
33
|
+
|
|
34
|
+
for (const file of this.files) {
|
|
35
|
+
const fullPath = join(outDir, file.path);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Check if file already exists
|
|
39
|
+
if (existsSync(fullPath) && !this.options.overwrite) {
|
|
40
|
+
result.skipped.push(file.path);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (this.options.dryRun) {
|
|
45
|
+
result.written.push(file.path);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Ensure directory exists
|
|
50
|
+
const dir = dirname(fullPath);
|
|
51
|
+
mkdirSync(dir, { recursive: true });
|
|
52
|
+
|
|
53
|
+
// Write the file
|
|
54
|
+
writeFileSync(fullPath, file.content, 'utf-8');
|
|
55
|
+
result.written.push(file.path);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
result.errors.push({
|
|
58
|
+
file: file.path,
|
|
59
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a FileEmitter for writing multiple generated files to disk.
|
|
70
|
+
*/
|
|
71
|
+
export function createFileEmitter(options: FileEmitterOptions): FileEmitter {
|
|
72
|
+
return new FileEmitterImpl(options);
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CodeBuilder, GeneratorContext } from '../types.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate an index.ts barrel file that re-exports all given files.
|
|
5
|
+
*/
|
|
6
|
+
export function generateBarrelFile(files: string[], ctx: GeneratorContext): CodeBuilder {
|
|
7
|
+
const code = ctx.createCodeBuilder();
|
|
8
|
+
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
// Strip .ts extension for the import path and ensure relative path
|
|
11
|
+
const importPath = file.startsWith('./') ? file : `./${file}`;
|
|
12
|
+
code.line(`export * from '${importPath}'`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return code;
|
|
16
|
+
}
|