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.
Files changed (80) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +1 -1
  3. package/README.md +92 -49
  4. package/dist/args-CKNh7Dm9.mjs +175 -0
  5. package/dist/args-CKNh7Dm9.mjs.map +1 -0
  6. package/dist/chunk-y_GBKt04.mjs +5 -0
  7. package/dist/codegen/index.d.mts +305 -0
  8. package/dist/codegen/index.d.mts.map +1 -0
  9. package/dist/codegen/index.mjs +1348 -0
  10. package/dist/codegen/index.mjs.map +1 -0
  11. package/dist/completion.d.mts +64 -0
  12. package/dist/completion.d.mts.map +1 -0
  13. package/dist/completion.mjs +417 -0
  14. package/dist/completion.mjs.map +1 -0
  15. package/dist/docs/index.d.mts +34 -0
  16. package/dist/docs/index.d.mts.map +1 -0
  17. package/dist/docs/index.mjs +404 -0
  18. package/dist/docs/index.mjs.map +1 -0
  19. package/dist/formatter-Dvx7jFXr.d.mts +82 -0
  20. package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
  21. package/dist/help-mUIX0T0V.mjs +1195 -0
  22. package/dist/help-mUIX0T0V.mjs.map +1 -0
  23. package/dist/index.d.mts +122 -438
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +1240 -1161
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/test.d.mts +112 -0
  28. package/dist/test.d.mts.map +1 -0
  29. package/dist/test.mjs +138 -0
  30. package/dist/test.mjs.map +1 -0
  31. package/dist/types-qrtt0135.d.mts +1037 -0
  32. package/dist/types-qrtt0135.d.mts.map +1 -0
  33. package/dist/update-check-EbNDkzyV.mjs +146 -0
  34. package/dist/update-check-EbNDkzyV.mjs.map +1 -0
  35. package/package.json +61 -20
  36. package/src/args.ts +365 -0
  37. package/src/cli/completions.ts +29 -0
  38. package/src/cli/docs.ts +86 -0
  39. package/src/cli/doctor.ts +312 -0
  40. package/src/cli/index.ts +159 -0
  41. package/src/cli/init.ts +135 -0
  42. package/src/cli/link.ts +320 -0
  43. package/src/cli/wrap.ts +152 -0
  44. package/src/codegen/README.md +118 -0
  45. package/src/codegen/code-builder.ts +226 -0
  46. package/src/codegen/discovery.ts +232 -0
  47. package/src/codegen/file-emitter.ts +73 -0
  48. package/src/codegen/generators/barrel-file.ts +16 -0
  49. package/src/codegen/generators/command-file.ts +184 -0
  50. package/src/codegen/generators/command-tree.ts +124 -0
  51. package/src/codegen/index.ts +33 -0
  52. package/src/codegen/parsers/fish.ts +163 -0
  53. package/src/codegen/parsers/help.ts +378 -0
  54. package/src/codegen/parsers/merge.ts +158 -0
  55. package/src/codegen/parsers/zsh.ts +221 -0
  56. package/src/codegen/schema-to-code.ts +199 -0
  57. package/src/codegen/template.ts +69 -0
  58. package/src/codegen/types.ts +143 -0
  59. package/src/colorizer.ts +2 -2
  60. package/src/command-utils.ts +501 -0
  61. package/src/completion.ts +110 -97
  62. package/src/create.ts +1044 -284
  63. package/src/docs/index.ts +607 -0
  64. package/src/errors.ts +131 -0
  65. package/src/formatter.ts +149 -63
  66. package/src/help.ts +151 -55
  67. package/src/index.ts +13 -15
  68. package/src/interactive.ts +169 -0
  69. package/src/parse.ts +31 -16
  70. package/src/repl-loop.ts +317 -0
  71. package/src/runtime.ts +304 -0
  72. package/src/shell-utils.ts +83 -0
  73. package/src/test.ts +285 -0
  74. package/src/type-helpers.ts +12 -12
  75. package/src/type-utils.ts +124 -14
  76. package/src/types.ts +803 -144
  77. package/src/update-check.ts +244 -0
  78. package/src/wrap.ts +185 -0
  79. package/src/zod.d.ts +2 -2
  80. 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
+ }