create-termui-app 0.1.5 → 0.1.6
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/README.md +4 -4
- package/dist/index.js +1005 -158
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/templates/ai-assistant/package.json +27 -0
- package/templates/ai-assistant/src/index.tsx +254 -0
- package/templates/dashboard/README.md +16 -0
- package/templates/dashboard/package.json +26 -0
- package/templates/dashboard/src/index.tsx +325 -0
- package/templates/file-manager/package.json +26 -0
- package/templates/file-manager/src/index.tsx +296 -0
package/dist/index.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { resolve, join } from "path";
|
|
5
|
-
import {
|
|
4
|
+
import { dirname as dirname3, resolve as resolve4, join as join3 } from "path";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
6
7
|
import { getBuiltinThemeNames } from "@termuijs/tss";
|
|
7
8
|
|
|
8
9
|
// src/prompts.ts
|
|
9
10
|
import { createInterface } from "readline";
|
|
10
11
|
var rl = () => createInterface({ input: process.stdin, output: process.stdout });
|
|
11
12
|
async function textPrompt(question, defaultValue) {
|
|
12
|
-
return new Promise((
|
|
13
|
+
return new Promise((resolve5) => {
|
|
13
14
|
const r = rl();
|
|
14
15
|
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
15
16
|
r.question(` ${question}${suffix}: `, (answer) => {
|
|
16
17
|
r.close();
|
|
17
|
-
|
|
18
|
+
resolve5(answer.trim() || defaultValue || "");
|
|
18
19
|
});
|
|
19
20
|
});
|
|
20
21
|
}
|
|
@@ -28,6 +29,12 @@ async function selectPrompt(question, options) {
|
|
|
28
29
|
const idx = parseInt(answer) - 1;
|
|
29
30
|
return Math.max(0, Math.min(idx, options.length - 1));
|
|
30
31
|
}
|
|
32
|
+
async function confirmPrompt(question, defaultValue = true) {
|
|
33
|
+
const suffix = defaultValue ? "(Y/n)" : "(y/N)";
|
|
34
|
+
const answer = await textPrompt(`${question} ${suffix}`);
|
|
35
|
+
if (!answer) return defaultValue;
|
|
36
|
+
return answer.toLowerCase().startsWith("y");
|
|
37
|
+
}
|
|
31
38
|
async function multiSelectPrompt(question, options, defaults = []) {
|
|
32
39
|
console.log(`
|
|
33
40
|
${question} (comma-separated numbers, or 'all')`);
|
|
@@ -44,40 +51,16 @@ async function multiSelectPrompt(question, options, defaults = []) {
|
|
|
44
51
|
|
|
45
52
|
// src/templates.ts
|
|
46
53
|
import { getBuiltinTheme } from "@termuijs/tss";
|
|
54
|
+
import { readdirSync, readFileSync } from "fs";
|
|
55
|
+
import { dirname, join, relative, resolve } from "path";
|
|
56
|
+
import { fileURLToPath } from "url";
|
|
57
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
58
|
+
var TEMPLATES_ROOT = resolve(__dirname, "../templates");
|
|
47
59
|
function generateProject(config) {
|
|
48
60
|
const files = [];
|
|
49
61
|
files.push({
|
|
50
62
|
path: "package.json",
|
|
51
|
-
content:
|
|
52
|
-
name: config.name,
|
|
53
|
-
version: "0.1.0",
|
|
54
|
-
private: true,
|
|
55
|
-
type: "module",
|
|
56
|
-
scripts: {
|
|
57
|
-
dev: "bun --watch src/index.tsx",
|
|
58
|
-
build: "tsup src/index.tsx --format esm",
|
|
59
|
-
start: "bun dist/index.js"
|
|
60
|
-
},
|
|
61
|
-
dependencies: {
|
|
62
|
-
"@termuijs/core": "latest",
|
|
63
|
-
"@termuijs/widgets": "latest",
|
|
64
|
-
"@termuijs/ui": "latest",
|
|
65
|
-
"@termuijs/jsx": "latest",
|
|
66
|
-
"@termuijs/tss": "latest",
|
|
67
|
-
"@termuijs/quick": "latest",
|
|
68
|
-
"@termuijs/motion": "latest",
|
|
69
|
-
...config.features.dataProviders ? { "@termuijs/data": "latest" } : {},
|
|
70
|
-
...config.features.router ? { "@termuijs/router": "latest" } : {}
|
|
71
|
-
},
|
|
72
|
-
devDependencies: {
|
|
73
|
-
"@types/bun": "latest",
|
|
74
|
-
tsup: "^8.0.0",
|
|
75
|
-
typescript: "^5.3.0"
|
|
76
|
-
},
|
|
77
|
-
engines: {
|
|
78
|
-
bun: ">=1.3.0"
|
|
79
|
-
}
|
|
80
|
-
}, null, 2) + "\n"
|
|
63
|
+
content: createPackageJson(config)
|
|
81
64
|
});
|
|
82
65
|
files.push({
|
|
83
66
|
path: "tsconfig.json",
|
|
@@ -121,11 +104,139 @@ export default defineConfig({
|
|
|
121
104
|
case "cli-wrapper":
|
|
122
105
|
files.push(...generateCliWrapperTemplate(config));
|
|
123
106
|
break;
|
|
107
|
+
case "cli-tool":
|
|
108
|
+
files.push(...generateCliToolTemplate(config));
|
|
109
|
+
break;
|
|
110
|
+
case "ai-assistant":
|
|
111
|
+
files.push(...generateAiAssistantTemplate(config));
|
|
112
|
+
break;
|
|
113
|
+
case "file-manager":
|
|
114
|
+
files.push(...generateFileManagerTemplate(config));
|
|
115
|
+
break;
|
|
116
|
+
case "form-wizard":
|
|
117
|
+
files.push(...generateFormWizardTemplate(config));
|
|
118
|
+
break;
|
|
124
119
|
default:
|
|
125
120
|
files.push(...generateEmptyTemplate(config));
|
|
126
121
|
}
|
|
127
122
|
return files;
|
|
128
123
|
}
|
|
124
|
+
function createPackageJson(config) {
|
|
125
|
+
const isFileManager = config.template === "file-manager";
|
|
126
|
+
const isAiAssistant = config.template === "ai-assistant";
|
|
127
|
+
return JSON.stringify({
|
|
128
|
+
name: config.name,
|
|
129
|
+
version: "0.1.0",
|
|
130
|
+
private: true,
|
|
131
|
+
type: "module",
|
|
132
|
+
scripts: {
|
|
133
|
+
dev: "bun --watch src/index.tsx",
|
|
134
|
+
build: "tsup src/index.tsx --format esm",
|
|
135
|
+
start: "bun dist/index.js"
|
|
136
|
+
},
|
|
137
|
+
dependencies: isAiAssistant ? {
|
|
138
|
+
"@termuijs/core": "latest",
|
|
139
|
+
"@termuijs/widgets": "latest",
|
|
140
|
+
"@termuijs/ui": "latest",
|
|
141
|
+
"@termuijs/jsx": "latest",
|
|
142
|
+
"@termuijs/tss": "latest"
|
|
143
|
+
} : isFileManager ? {
|
|
144
|
+
"@termuijs/core": "latest",
|
|
145
|
+
"@termuijs/widgets": "latest",
|
|
146
|
+
"@termuijs/ui": "latest",
|
|
147
|
+
"@termuijs/jsx": "latest",
|
|
148
|
+
"@termuijs/tss": "latest"
|
|
149
|
+
} : {
|
|
150
|
+
"@termuijs/core": "latest",
|
|
151
|
+
"@termuijs/widgets": "latest",
|
|
152
|
+
"@termuijs/ui": "latest",
|
|
153
|
+
"@termuijs/jsx": "latest",
|
|
154
|
+
"@termuijs/tss": "latest",
|
|
155
|
+
"@termuijs/quick": "latest",
|
|
156
|
+
"@termuijs/motion": "latest",
|
|
157
|
+
...config.features.dataProviders ? { "@termuijs/data": "latest" } : {},
|
|
158
|
+
...config.features.router ? { "@termuijs/router": "latest" } : {}
|
|
159
|
+
},
|
|
160
|
+
devDependencies: {
|
|
161
|
+
"@types/bun": "latest",
|
|
162
|
+
tsup: "^8.0.0",
|
|
163
|
+
typescript: "^5.3.0"
|
|
164
|
+
},
|
|
165
|
+
engines: {
|
|
166
|
+
bun: ">=1.3.0"
|
|
167
|
+
}
|
|
168
|
+
}, null, 2) + "\n";
|
|
169
|
+
}
|
|
170
|
+
function generateFormWizardTemplate(config) {
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
path: "src/index.tsx",
|
|
174
|
+
content: `/** @jsxImportSource @termuijs/jsx */
|
|
175
|
+
import { render, useState } from '@termuijs/jsx';
|
|
176
|
+
import { Wizard } from '@termuijs/ui';
|
|
177
|
+
import { TextInput, Spinner } from '@termuijs/widgets';
|
|
178
|
+
|
|
179
|
+
function App() {
|
|
180
|
+
const [name, setName] = useState('');
|
|
181
|
+
const [theme, setTheme] = useState('');
|
|
182
|
+
const [submitting, setSubmitting] = useState(false);
|
|
183
|
+
|
|
184
|
+
const handleComplete = async () => {
|
|
185
|
+
setSubmitting(true);
|
|
186
|
+
|
|
187
|
+
const data = {
|
|
188
|
+
name,
|
|
189
|
+
theme,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
console.log(JSON.stringify(data, null, 2));
|
|
193
|
+
|
|
194
|
+
setTimeout(() => {
|
|
195
|
+
setSubmitting(false);
|
|
196
|
+
}, 1000);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<box flexDirection="column" padding={1}>
|
|
201
|
+
<text bold>Form Wizard</text>
|
|
202
|
+
|
|
203
|
+
<Wizard
|
|
204
|
+
steps={['Info', 'Preferences', 'Confirm']}
|
|
205
|
+
onComplete={handleComplete}
|
|
206
|
+
>
|
|
207
|
+
<box flexDirection="column">
|
|
208
|
+
<text>Name</text>
|
|
209
|
+
<TextInput
|
|
210
|
+
value={name}
|
|
211
|
+
onChange={setName}
|
|
212
|
+
/>
|
|
213
|
+
</box>
|
|
214
|
+
|
|
215
|
+
<box flexDirection="column">
|
|
216
|
+
<text>Theme</text>
|
|
217
|
+
<TextInput
|
|
218
|
+
value={theme}
|
|
219
|
+
onChange={setTheme}
|
|
220
|
+
/>
|
|
221
|
+
</box>
|
|
222
|
+
|
|
223
|
+
<box flexDirection="column">
|
|
224
|
+
<text>Confirm Details</text>
|
|
225
|
+
<text>Name: {name}</text>
|
|
226
|
+
<text>Theme: {theme}</text>
|
|
227
|
+
</box>
|
|
228
|
+
</Wizard>
|
|
229
|
+
|
|
230
|
+
{submitting && <Spinner />}
|
|
231
|
+
</box>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
render(<App />, { title: '${config.name}' });
|
|
236
|
+
`
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
}
|
|
129
240
|
function generateEmptyTemplate(config) {
|
|
130
241
|
return [{
|
|
131
242
|
path: "src/index.tsx",
|
|
@@ -161,109 +272,32 @@ render(<App />, { title: '${config.name}' });
|
|
|
161
272
|
}];
|
|
162
273
|
}
|
|
163
274
|
function generateDashboardTemplate(config) {
|
|
164
|
-
return
|
|
165
|
-
path: "src/index.tsx",
|
|
166
|
-
content: `/** @jsxImportSource @termuijs/jsx */
|
|
167
|
-
import { render, useState, useEffect, useKeymap, ErrorBoundary } from '@termuijs/jsx';
|
|
168
|
-
import { AutoThemeProvider, useTheme } from '@termuijs/tss';
|
|
169
|
-
${config.features.dataProviders ? "import { useCpu, useMemory, useDisk } from '@termuijs/data';" : ""}
|
|
170
|
-
|
|
171
|
-
// \u2500\u2500 Sample static data (replace with live hooks when dataProviders = true) \u2500\u2500
|
|
172
|
-
${config.features.dataProviders ? "" : `const SAMPLE_PROCS = [
|
|
173
|
-
{ Name: 'node', PID: 1234, 'CPU%': '5.0', 'MEM%': '2.1' },
|
|
174
|
-
{ Name: 'chrome', PID: 5678, 'CPU%': '12.3', 'MEM%': '8.4' },
|
|
175
|
-
{ Name: 'bash', PID: 9012, 'CPU%': '0.1', 'MEM%': '0.3' },
|
|
176
|
-
];`}
|
|
177
|
-
|
|
178
|
-
function GaugeRow({ label, value }: { label: string; value: number }) {
|
|
179
|
-
const theme = useTheme();
|
|
180
|
-
const filled = Math.round(value * 20);
|
|
181
|
-
const empty = 20 - filled;
|
|
182
|
-
const bar = '[' + '#'.repeat(filled) + '-'.repeat(empty) + ']';
|
|
183
|
-
return (
|
|
184
|
-
<row gap={2}>
|
|
185
|
-
<text color={theme.colors.primary}>{label.padEnd(4)}</text>
|
|
186
|
-
<text>{bar}</text>
|
|
187
|
-
<text>{(value * 100).toFixed(1).padStart(5)}%</text>
|
|
188
|
-
</row>
|
|
189
|
-
);
|
|
275
|
+
return loadTemplateFiles("dashboard", config);
|
|
190
276
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
${config.features.dataProviders ? ` const cpu = useCpu();
|
|
195
|
-
const mem = useMemory();
|
|
196
|
-
const disk = useDisk();
|
|
197
|
-
const cpuVal = (cpu.percent ?? 0) / 100;
|
|
198
|
-
const memVal = (mem.percent ?? 0) / 100;
|
|
199
|
-
const diskVal = (disk.percent ?? 0) / 100;` : ` const [cpuVal, setCpuVal] = useState(0.45);
|
|
200
|
-
const [memVal, setMemVal] = useState(0.62);
|
|
201
|
-
const [diskVal, setDiskVal] = useState(0.38);
|
|
202
|
-
|
|
203
|
-
// Simulate live updates
|
|
204
|
-
useEffect(() => {
|
|
205
|
-
const id = setInterval(() => {
|
|
206
|
-
setCpuVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.05)));
|
|
207
|
-
setMemVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.02)));
|
|
208
|
-
setDiskVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.01)));
|
|
209
|
-
setTick(t => t + 1);
|
|
210
|
-
}, 1000);
|
|
211
|
-
return () => clearInterval(id);
|
|
212
|
-
}, []);`}
|
|
213
|
-
|
|
214
|
-
useKeymap([
|
|
215
|
-
{ key: 'q', action: () => process.exit(0), description: 'Quit' },
|
|
216
|
-
{ key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },
|
|
217
|
-
{ key: 'r', action: () => setTick(t => t + 1), description: 'Refresh' },
|
|
218
|
-
]);
|
|
219
|
-
|
|
220
|
-
const theme = useTheme();
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<box flexDirection="column" padding={1}>
|
|
224
|
-
<text bold color={theme.colors.primary}>${config.name} Dashboard</text>
|
|
225
|
-
<divider />
|
|
226
|
-
|
|
227
|
-
<grid columns={12} gap={1}>
|
|
228
|
-
{/* Gauges \u2014 top row */}
|
|
229
|
-
<box width="100%" flexDirection="column" border="single" padding={1} flexGrow={4}>
|
|
230
|
-
<text bold>System Resources</text>
|
|
231
|
-
<GaugeRow label="CPU" value={cpuVal} />
|
|
232
|
-
<GaugeRow label="MEM" value={memVal} />
|
|
233
|
-
<GaugeRow label="DISK" value={diskVal} />
|
|
234
|
-
</box>
|
|
235
|
-
|
|
236
|
-
{/* Info panel */}
|
|
237
|
-
<box width="100%" flexDirection="column" border="single" padding={1} flexGrow={8}>
|
|
238
|
-
<text bold>Process Summary</text>
|
|
239
|
-
<text color={theme.colors.muted}>Press r to refresh, q to quit</text>
|
|
240
|
-
<text>Tick: {tick}</text>
|
|
241
|
-
${config.features.dataProviders ? ` <skeleton variant="shimmer" />` : ` <text>node PID:1234 CPU: {(cpuVal * 100).toFixed(1)}%</text>
|
|
242
|
-
<text>chrome PID:5678 MEM: {(memVal * 100).toFixed(1)}%</text>`}
|
|
243
|
-
</box>
|
|
244
|
-
</grid>
|
|
245
|
-
</box>
|
|
246
|
-
);
|
|
277
|
+
function loadTemplateFiles(templateName, config) {
|
|
278
|
+
const templatePath = resolve(TEMPLATES_ROOT, templateName);
|
|
279
|
+
return walkTemplateDirectory(templatePath, templatePath, config);
|
|
247
280
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
);
|
|
281
|
+
function walkTemplateDirectory(rootPath, currentPath, config) {
|
|
282
|
+
const entries = readdirSync(currentPath, { withFileTypes: true });
|
|
283
|
+
const files = [];
|
|
284
|
+
for (const entry of entries) {
|
|
285
|
+
const entryPath = join(currentPath, entry.name);
|
|
286
|
+
if (entry.isDirectory()) {
|
|
287
|
+
files.push(...walkTemplateDirectory(rootPath, entryPath, config));
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (entry.name === "package.json") {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const relativePath = relative(rootPath, entryPath).replace(/\\/g, "/");
|
|
294
|
+
const content = replaceTemplatePlaceholders(readFileSync(entryPath, "utf8"), config);
|
|
295
|
+
files.push({ path: relativePath, content });
|
|
296
|
+
}
|
|
297
|
+
return files;
|
|
262
298
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
`
|
|
266
|
-
}];
|
|
299
|
+
function replaceTemplatePlaceholders(content, config) {
|
|
300
|
+
return content.replace(/{{name}}/g, config.name);
|
|
267
301
|
}
|
|
268
302
|
function generateInteractiveTemplate(config) {
|
|
269
303
|
return [{
|
|
@@ -290,11 +324,11 @@ function InteractiveTool() {
|
|
|
290
324
|
useKeymap([
|
|
291
325
|
{ key: 'q', action: () => process.exit(0), description: 'Quit' },
|
|
292
326
|
{ key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },
|
|
293
|
-
{ key: '
|
|
294
|
-
{ key: '
|
|
327
|
+
{ key: 'up', action: () => setSelected(s => Math.max(0, s - 1)), description: 'Move up' },
|
|
328
|
+
{ key: 'down', action: () => setSelected(s => Math.min(items.length - 1, s + 1)), description: 'Move down' },
|
|
295
329
|
{ key: 'k', action: () => setSelected(s => Math.max(0, s - 1)), description: 'Move up (vim)' },
|
|
296
330
|
{ key: 'j', action: () => setSelected(s => Math.min(items.length - 1, s + 1)), description: 'Move down (vim)' },
|
|
297
|
-
{ key: '
|
|
331
|
+
{ key: 'enter', action: () => {
|
|
298
332
|
const item = items[selected];
|
|
299
333
|
if (item) setDone(d => d.includes(item) ? d.filter(x => x !== item) : [...d, item]);
|
|
300
334
|
}, description: 'Toggle selected' },
|
|
@@ -351,6 +385,24 @@ function App() {
|
|
|
351
385
|
);
|
|
352
386
|
}
|
|
353
387
|
|
|
388
|
+
render(<App />, { title: '${config.name}' });
|
|
389
|
+
`
|
|
390
|
+
}];
|
|
391
|
+
}
|
|
392
|
+
function generateCliToolTemplate(config) {
|
|
393
|
+
return [{
|
|
394
|
+
path: "src/index.tsx",
|
|
395
|
+
content: `/** @jsxImportSource @termuijs/jsx */
|
|
396
|
+
import { render, useKeymap } from '@termuijs/jsx';
|
|
397
|
+
function App() {
|
|
398
|
+
useKeymap([{ key: 'q', action: () => process.exit(0), description: 'Quit' }]);
|
|
399
|
+
return (
|
|
400
|
+
<box flexDirection="column">
|
|
401
|
+
<text bold>${config.name}</text>
|
|
402
|
+
<text dim>Press q to quit</text>
|
|
403
|
+
</box>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
354
406
|
render(<App />, { title: '${config.name}' });
|
|
355
407
|
`
|
|
356
408
|
}];
|
|
@@ -393,7 +445,7 @@ function CliWrapper() {
|
|
|
393
445
|
]);
|
|
394
446
|
const [running, setRunning] = useState(false);
|
|
395
447
|
const [exitCode, setExitCode] = useState<number | null>(null);
|
|
396
|
-
const procRef = useRef<
|
|
448
|
+
const procRef = useRef<ReturnType<typeof spawn> | null>(null);
|
|
397
449
|
const theme = useTheme();
|
|
398
450
|
|
|
399
451
|
const addLog = (level: LogLevel, text: string) =>
|
|
@@ -500,29 +552,819 @@ render(<App />, { title: '${config.name}' });
|
|
|
500
552
|
`
|
|
501
553
|
}];
|
|
502
554
|
}
|
|
555
|
+
function generateFileManagerTemplate(config) {
|
|
556
|
+
return [{
|
|
557
|
+
path: "src/index.tsx",
|
|
558
|
+
content: `/** @jsxImportSource @termuijs/jsx */
|
|
559
|
+
import * as fs from 'node:fs';
|
|
560
|
+
import * as path from 'node:path';
|
|
561
|
+
import { render, useEffect, useKeymap, useMemo, useRef, useState, ErrorBoundary } from '@termuijs/jsx';
|
|
562
|
+
import { AutoThemeProvider } from '@termuijs/tss';
|
|
563
|
+
import { AppShell, FilePicker } from '@termuijs/ui';
|
|
564
|
+
import { Box, DiffView, Tree, Text, type DiffLine, type TreeNode } from '@termuijs/widgets';
|
|
565
|
+
|
|
566
|
+
type Pane = 'tree' | 'picker' | 'preview';
|
|
567
|
+
|
|
568
|
+
function escapeText(value: string): string {
|
|
569
|
+
return value.replace(/\r/g, '');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function readPreview(filePath: string): DiffLine[] {
|
|
573
|
+
try {
|
|
574
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
575
|
+
const lines = escapeText(content).split('
|
|
576
|
+
');
|
|
577
|
+
return lines.map((line, index) => ({ type: 'context' as const, content: line || ' ', lineNo: index + 1 }));
|
|
578
|
+
} catch (error) {
|
|
579
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
580
|
+
return [{ type: 'context', content: message }];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function findFirstPreviewPath(rootPath: string): string {
|
|
585
|
+
try {
|
|
586
|
+
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
587
|
+
for (const entry of entries) {
|
|
588
|
+
if (entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
589
|
+
return path.join(rootPath, entry.name);
|
|
590
|
+
}
|
|
591
|
+
} catch {
|
|
592
|
+
// Fall back to the current directory path when reading fails.
|
|
593
|
+
}
|
|
594
|
+
return rootPath;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function buildTree(rootPath: string, depth = 0, maxDepth = 3): TreeNode[] {
|
|
598
|
+
const entries: TreeNode[] = [];
|
|
599
|
+
|
|
600
|
+
if (depth === 0) {
|
|
601
|
+
entries.push({ label: path.basename(rootPath) || rootPath, expanded: true, children: buildTree(rootPath, depth + 1, maxDepth) });
|
|
602
|
+
return entries;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (depth > maxDepth) {
|
|
606
|
+
return entries;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
const dirents = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
611
|
+
const sorted = [...dirents].sort((left, right) => left.name.localeCompare(right.name));
|
|
612
|
+
|
|
613
|
+
for (const entry of sorted) {
|
|
614
|
+
if (entry.name.startsWith('.')) continue;
|
|
615
|
+
const fullPath = path.join(rootPath, entry.name);
|
|
616
|
+
if (entry.isDirectory()) {
|
|
617
|
+
entries.push({
|
|
618
|
+
label: entry.name,
|
|
619
|
+
expanded: depth < 2,
|
|
620
|
+
data: { path: fullPath, type: 'directory' },
|
|
621
|
+
children: buildTree(fullPath, depth + 1, maxDepth),
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
entries.push({
|
|
625
|
+
label: entry.name,
|
|
626
|
+
data: { path: fullPath, type: 'file' },
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
} catch (error) {
|
|
631
|
+
entries.push({
|
|
632
|
+
label: error instanceof Error ? error.message : String(error),
|
|
633
|
+
data: { path: rootPath, type: 'error' },
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return entries;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function setPaneFocus(widget: Box, focused: boolean): void {
|
|
641
|
+
widget.setStyle({
|
|
642
|
+
borderColor: focused ? { type: 'named', name: 'cyan' } : { type: 'named', name: 'brightBlack' },
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function FileManager() {
|
|
647
|
+
const initialPath = process.cwd();
|
|
648
|
+
const [cwd, setCwd] = useState(initialPath);
|
|
649
|
+
const [previewPath, setPreviewPath] = useState(findFirstPreviewPath(initialPath));
|
|
650
|
+
const [focusedPane, setFocusedPane] = useState<Pane>('picker');
|
|
651
|
+
|
|
652
|
+
const tree = useRef<Tree>(new Tree({
|
|
653
|
+
nodes: buildTree(initialPath),
|
|
654
|
+
onSelect: (node) => {
|
|
655
|
+
const payload = node.data as { path?: string; type?: string } | undefined;
|
|
656
|
+
if (!payload?.path) return;
|
|
657
|
+
if (payload.type === 'file') {
|
|
658
|
+
setPreviewPath(payload.path);
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
}, { flexGrow: 1 }));
|
|
662
|
+
|
|
663
|
+
const filePicker = useRef(new FilePicker({
|
|
664
|
+
startPath: initialPath,
|
|
665
|
+
onSelect: (selectedPath: string) => {
|
|
666
|
+
setPreviewPath(selectedPath);
|
|
667
|
+
},
|
|
668
|
+
onCancel: () => process.exit(0),
|
|
669
|
+
}));
|
|
670
|
+
const preview = useRef(new DiffView({ lines: readPreview(findFirstPreviewPath(initialPath)) }, { flexGrow: 1 }));
|
|
671
|
+
|
|
672
|
+
const header = useMemo(() => new Box({
|
|
673
|
+
flexDirection: 'row',
|
|
674
|
+
border: 'single',
|
|
675
|
+
padding: 1,
|
|
676
|
+
}), []);
|
|
677
|
+
|
|
678
|
+
const footer = useMemo(() => new Box({
|
|
679
|
+
flexDirection: 'row',
|
|
680
|
+
border: 'single',
|
|
681
|
+
padding: 1,
|
|
682
|
+
}), []);
|
|
683
|
+
|
|
684
|
+
const leftPane = useMemo(() => new Box({
|
|
685
|
+
flexDirection: 'column',
|
|
686
|
+
border: 'single',
|
|
687
|
+
padding: 1,
|
|
688
|
+
flexGrow: 1,
|
|
689
|
+
}), []);
|
|
690
|
+
|
|
691
|
+
const centerPane = useMemo(() => new Box({
|
|
692
|
+
flexDirection: 'column',
|
|
693
|
+
border: 'single',
|
|
694
|
+
padding: 1,
|
|
695
|
+
flexGrow: 1,
|
|
696
|
+
}), []);
|
|
697
|
+
|
|
698
|
+
const rightPane = useMemo(() => new Box({
|
|
699
|
+
flexDirection: 'column',
|
|
700
|
+
border: 'single',
|
|
701
|
+
padding: 1,
|
|
702
|
+
flexGrow: 1,
|
|
703
|
+
}), []);
|
|
704
|
+
|
|
705
|
+
const mainArea = useMemo(() => new Box({
|
|
706
|
+
flexDirection: 'row',
|
|
707
|
+
gap: 1,
|
|
708
|
+
flexGrow: 1,
|
|
709
|
+
}), []);
|
|
710
|
+
|
|
711
|
+
const shell = useMemo(() => new AppShell({
|
|
712
|
+
header,
|
|
713
|
+
footer,
|
|
714
|
+
sidebar: leftPane,
|
|
715
|
+
main: mainArea,
|
|
716
|
+
sidebarWidth: 32,
|
|
717
|
+
}), [footer, header, leftPane, mainArea]);
|
|
718
|
+
|
|
719
|
+
useEffect(() => {
|
|
720
|
+
header.clearChildren();
|
|
721
|
+
header.addChild(new Text('Path: ' + cwd));
|
|
722
|
+
header.addChild(new Text('Focus: ' + focusedPane));
|
|
723
|
+
}, [cwd, focusedPane, header]);
|
|
724
|
+
|
|
725
|
+
useEffect(() => {
|
|
726
|
+
footer.clearChildren();
|
|
727
|
+
footer.addChild(new Text('Tab / Shift+Tab: switch panes'));
|
|
728
|
+
footer.addChild(new Text('Enter: open item | q: quit'));
|
|
729
|
+
}, [footer]);
|
|
730
|
+
|
|
731
|
+
useEffect(() => {
|
|
732
|
+
tree.current.setNodes(buildTree(cwd));
|
|
733
|
+
}, [cwd]);
|
|
734
|
+
|
|
735
|
+
useEffect(() => {
|
|
736
|
+
preview.current.setLines(readPreview(previewPath));
|
|
737
|
+
}, [previewPath]);
|
|
738
|
+
|
|
739
|
+
useEffect(() => {
|
|
740
|
+
setPaneFocus(leftPane, focusedPane === 'tree');
|
|
741
|
+
setPaneFocus(centerPane, focusedPane === 'picker');
|
|
742
|
+
setPaneFocus(rightPane, focusedPane === 'preview');
|
|
743
|
+
}, [centerPane, focusedPane, leftPane, rightPane]);
|
|
744
|
+
|
|
745
|
+
useEffect(() => {
|
|
746
|
+
leftPane.clearChildren();
|
|
747
|
+
leftPane.addChild(tree.current);
|
|
748
|
+
|
|
749
|
+
centerPane.clearChildren();
|
|
750
|
+
centerPane.addChild(filePicker.current);
|
|
751
|
+
|
|
752
|
+
rightPane.clearChildren();
|
|
753
|
+
rightPane.addChild(preview.current);
|
|
754
|
+
|
|
755
|
+
mainArea.clearChildren();
|
|
756
|
+
mainArea.addChild(centerPane);
|
|
757
|
+
mainArea.addChild(rightPane);
|
|
758
|
+
|
|
759
|
+
setPaneFocus(leftPane, focusedPane === 'tree');
|
|
760
|
+
setPaneFocus(centerPane, focusedPane === 'picker');
|
|
761
|
+
setPaneFocus(rightPane, focusedPane === 'preview');
|
|
762
|
+
}, [centerPane, focusedPane, leftPane, mainArea, rightPane]);
|
|
763
|
+
|
|
764
|
+
const cyclePane = (direction: 1 | -1): void => {
|
|
765
|
+
const order: Pane[] = ['tree', 'picker', 'preview'];
|
|
766
|
+
const index = order.indexOf(focusedPane);
|
|
767
|
+
const next = order[(index + direction + order.length) % order.length];
|
|
768
|
+
setFocusedPane(next);
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const syncPickerPath = (): void => {
|
|
772
|
+
setCwd(filePicker.current.currentPath);
|
|
773
|
+
const selected = filePicker.current.selectedEntry;
|
|
774
|
+
setPreviewPath(
|
|
775
|
+
selected && !selected.isDir
|
|
776
|
+
? selected.fullPath
|
|
777
|
+
: findFirstPreviewPath(filePicker.current.currentPath),
|
|
778
|
+
);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
useKeymap([
|
|
782
|
+
{ key: 'tab', action: () => cyclePane(1), description: 'Next pane' },
|
|
783
|
+
{ key: 'tab', shift: true, action: () => cyclePane(-1), description: 'Previous pane' },
|
|
784
|
+
{ key: 'enter', action: () => {
|
|
785
|
+
if (focusedPane === 'tree') {
|
|
786
|
+
tree.current.handleKey('enter');
|
|
787
|
+
const selected = tree.current.selectedNode;
|
|
788
|
+
const payload = selected?.data as { path?: string; type?: string } | undefined;
|
|
789
|
+
if (payload?.type === 'file' && payload.path) {
|
|
790
|
+
setPreviewPath(payload.path);
|
|
791
|
+
}
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (focusedPane === 'picker') {
|
|
796
|
+
filePicker.current.confirm();
|
|
797
|
+
syncPickerPath();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
preview.current.handleKey('enter');
|
|
802
|
+
}, description: 'Open item' },
|
|
803
|
+
{ key: 'up', action: () => {
|
|
804
|
+
if (focusedPane === 'tree') tree.current.handleKey('up');
|
|
805
|
+
else if (focusedPane === 'picker') filePicker.current.selectPrev();
|
|
806
|
+
else preview.current.handleKey('up');
|
|
807
|
+
}, description: 'Move up' },
|
|
808
|
+
{ key: 'down', action: () => {
|
|
809
|
+
if (focusedPane === 'tree') tree.current.handleKey('down');
|
|
810
|
+
else if (focusedPane === 'picker') filePicker.current.selectNext();
|
|
811
|
+
else preview.current.handleKey('down');
|
|
812
|
+
}, description: 'Move down' },
|
|
813
|
+
{ key: 'left', action: () => {
|
|
814
|
+
if (focusedPane === 'tree') tree.current.handleKey('left');
|
|
815
|
+
else if (focusedPane === 'picker') {
|
|
816
|
+
filePicker.current.goUp();
|
|
817
|
+
syncPickerPath();
|
|
818
|
+
}
|
|
819
|
+
}, description: 'Collapse or go up' },
|
|
820
|
+
{ key: 'right', action: () => {
|
|
821
|
+
if (focusedPane === 'tree') tree.current.handleKey('right');
|
|
822
|
+
else if (focusedPane === 'picker') {
|
|
823
|
+
filePicker.current.confirm();
|
|
824
|
+
syncPickerPath();
|
|
825
|
+
}
|
|
826
|
+
}, description: 'Expand or open' },
|
|
827
|
+
{ key: 'backspace', action: () => {
|
|
828
|
+
if (focusedPane === 'picker') {
|
|
829
|
+
filePicker.current.goUp();
|
|
830
|
+
syncPickerPath();
|
|
831
|
+
}
|
|
832
|
+
}, description: 'Parent directory' },
|
|
833
|
+
{ key: 'q', action: () => process.exit(0), description: 'Quit' },
|
|
834
|
+
{ key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },
|
|
835
|
+
]);
|
|
836
|
+
|
|
837
|
+
return shell;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function App() {
|
|
841
|
+
return (
|
|
842
|
+
<AutoThemeProvider>
|
|
843
|
+
<ErrorBoundary fallback={(err) => (
|
|
844
|
+
<box border="single" borderColor="red" padding={1}>
|
|
845
|
+
<text color="red" bold>File Manager Error</text>
|
|
846
|
+
<text>{err.message}</text>
|
|
847
|
+
</box>
|
|
848
|
+
)}>
|
|
849
|
+
<FileManager />
|
|
850
|
+
</ErrorBoundary>
|
|
851
|
+
</AutoThemeProvider>
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
render(<App />, { title: '${config.name}' });
|
|
856
|
+
`
|
|
857
|
+
}];
|
|
858
|
+
}
|
|
859
|
+
function generateAiAssistantTemplate(config) {
|
|
860
|
+
return [{
|
|
861
|
+
path: "src/index.tsx",
|
|
862
|
+
content: `/** @jsxImportSource @termuijs/jsx */
|
|
863
|
+
import { render, useState, useKeymap, useEffect, ErrorBoundary } from '@termuijs/jsx';
|
|
864
|
+
import { AutoThemeProvider, useTheme } from '@termuijs/tss';
|
|
865
|
+
|
|
866
|
+
// \u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
867
|
+
|
|
868
|
+
interface Message { role: 'user' | 'assistant'; content: string; }
|
|
869
|
+
interface TokenUsageData { inputTokens: number; outputTokens: number; }
|
|
870
|
+
|
|
871
|
+
// \u2500\u2500 Mock adapter (works without ANTHROPIC_API_KEY) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
872
|
+
|
|
873
|
+
const MOCK_REPLIES = [
|
|
874
|
+
'Hello! Running in mock mode. Set ANTHROPIC_API_KEY for real Claude.',
|
|
875
|
+
'Mock mode active \u2014 your message was received!',
|
|
876
|
+
'No API key needed in mock mode. Real Claude would answer here.',
|
|
877
|
+
];
|
|
878
|
+
|
|
879
|
+
async function* mockStream(_prompt: string): AsyncGenerator<string> {
|
|
880
|
+
const reply = MOCK_REPLIES[Math.floor(Math.random() * MOCK_REPLIES.length)];
|
|
881
|
+
for (const ch of reply) {
|
|
882
|
+
yield ch;
|
|
883
|
+
await new Promise(r => setTimeout(r, 20));
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function* claudeStream(
|
|
888
|
+
messages: Message[],
|
|
889
|
+
onUsage: (u: TokenUsageData) => void,
|
|
890
|
+
): AsyncGenerator<string> {
|
|
891
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
892
|
+
method: 'POST',
|
|
893
|
+
headers: {
|
|
894
|
+
'content-type': 'application/json',
|
|
895
|
+
'x-api-key': process.env.ANTHROPIC_API_KEY ?? '',
|
|
896
|
+
'anthropic-version': '2023-06-01',
|
|
897
|
+
},
|
|
898
|
+
body: JSON.stringify({
|
|
899
|
+
model: 'claude-3-5-haiku-20241022',
|
|
900
|
+
max_tokens: 1024,
|
|
901
|
+
stream: true,
|
|
902
|
+
messages: messages.map(m => ({ role: m.role, content: m.content })),
|
|
903
|
+
}),
|
|
904
|
+
});
|
|
905
|
+
if (!res.ok) throw new Error('Claude API ' + res.status + ': ' + res.statusText);
|
|
906
|
+
const reader = res.body!.getReader();
|
|
907
|
+
const dec = new TextDecoder();
|
|
908
|
+
let buf = '';
|
|
909
|
+
while (true) {
|
|
910
|
+
const { done, value } = await reader.read();
|
|
911
|
+
if (done) break;
|
|
912
|
+
buf += dec.decode(value, { stream: true });
|
|
913
|
+
const lines = buf.split('\\n');
|
|
914
|
+
buf = lines.pop() ?? '';
|
|
915
|
+
for (const line of lines) {
|
|
916
|
+
if (!line.startsWith('data: ')) continue;
|
|
917
|
+
const raw = line.slice(6).trim();
|
|
918
|
+
if (raw === '[DONE]') return;
|
|
919
|
+
try {
|
|
920
|
+
const ev = JSON.parse(raw);
|
|
921
|
+
if (ev.type === 'content_block_delta' && ev.delta?.type === 'text_delta') yield ev.delta.text as string;
|
|
922
|
+
if (ev.type === 'message_delta' && ev.usage) onUsage({ inputTokens: ev.usage.input_tokens ?? 0, outputTokens: ev.usage.output_tokens ?? 0 });
|
|
923
|
+
} catch { /* skip */ }
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// \u2500\u2500 Components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
929
|
+
|
|
930
|
+
const IS_MOCK = !process.env.ANTHROPIC_API_KEY;
|
|
931
|
+
|
|
932
|
+
function AiAssistant() {
|
|
933
|
+
const theme = useTheme();
|
|
934
|
+
const [messages, setMessages] = useState<Message[]>([{
|
|
935
|
+
role: 'assistant',
|
|
936
|
+
content: IS_MOCK
|
|
937
|
+
? 'Hi! Running in mock mode (no ANTHROPIC_API_KEY). Type and press Enter!'
|
|
938
|
+
: 'Hi! I am Claude. How can I help you?',
|
|
939
|
+
}]);
|
|
940
|
+
const [input, setInput] = useState('');
|
|
941
|
+
const [streaming, setStreaming] = useState('');
|
|
942
|
+
const [busy, setBusy] = useState(false);
|
|
943
|
+
const [usage, setUsage] = useState<TokenUsageData>({ inputTokens: 0, outputTokens: 0 });
|
|
944
|
+
|
|
945
|
+
const send = async () => {
|
|
946
|
+
const text = input.trim();
|
|
947
|
+
if (!text || busy) return;
|
|
948
|
+
const next: Message[] = [...messages, { role: 'user', content: text }];
|
|
949
|
+
setMessages(next);
|
|
950
|
+
setInput('');
|
|
951
|
+
setBusy(true);
|
|
952
|
+
setStreaming('');
|
|
953
|
+
try {
|
|
954
|
+
let full = '';
|
|
955
|
+
const src = IS_MOCK ? mockStream(text) : claudeStream(next, setUsage);
|
|
956
|
+
for await (const chunk of src) { full += chunk; setStreaming(full); }
|
|
957
|
+
setMessages(m => [...m, { role: 'assistant', content: full }]);
|
|
958
|
+
} catch (e) {
|
|
959
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
960
|
+
setMessages(m => [...m, { role: 'assistant', content: 'Error: ' + msg }]);
|
|
961
|
+
} finally { setStreaming(''); setBusy(false); }
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
useKeymap([
|
|
965
|
+
{ key: 'enter', action: () => { void send(); }, description: 'Send' },
|
|
966
|
+
{ key: 'backspace', action: () => setInput(v => v.slice(0, -1)), description: 'Delete' },
|
|
967
|
+
{ key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },
|
|
968
|
+
...(' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-_()').split('').map(ch => ({
|
|
969
|
+
key: ch, action: () => { if (!busy) setInput(v => v + ch); }, description: '',
|
|
970
|
+
})),
|
|
971
|
+
]);
|
|
972
|
+
|
|
973
|
+
return (
|
|
974
|
+
<box flexDirection="column" flexGrow={1} padding={1}>
|
|
975
|
+
<box border="single" padding={1} flexDirection="row">
|
|
976
|
+
<text bold>AI Assistant</text>
|
|
977
|
+
<text> {IS_MOCK ? '[mock mode]' : '[claude-3-5-haiku]'}</text>
|
|
978
|
+
<text color={theme.colors.muted}> in:{usage.inputTokens} out:{usage.outputTokens}</text>
|
|
979
|
+
</box>
|
|
980
|
+
|
|
981
|
+
<box flexDirection="column" flexGrow={1} padding={1}>
|
|
982
|
+
{messages.map((m, i) => (
|
|
983
|
+
<box key={i} flexDirection="column" marginBottom={1}>
|
|
984
|
+
<text bold color={m.role === 'user' ? theme.colors.primary : theme.colors.success}>
|
|
985
|
+
{m.role === 'user' ? 'You' : 'Claude'}
|
|
986
|
+
</text>
|
|
987
|
+
<text>{m.content}</text>
|
|
988
|
+
</box>
|
|
989
|
+
))}
|
|
990
|
+
{streaming.length > 0 && (
|
|
991
|
+
<box flexDirection="column">
|
|
992
|
+
<text bold color={theme.colors.success}>Claude</text>
|
|
993
|
+
<text>{streaming}\u2588</text>
|
|
994
|
+
</box>
|
|
995
|
+
)}
|
|
996
|
+
</box>
|
|
997
|
+
|
|
998
|
+
<box border="single" padding={1}>
|
|
999
|
+
<text color={theme.colors.muted}>> </text>
|
|
1000
|
+
<text>{input}{busy ? '' : '\u2588'}</text>
|
|
1001
|
+
{busy && <text color={theme.colors.muted}> thinking...</text>}
|
|
1002
|
+
</box>
|
|
1003
|
+
|
|
1004
|
+
<box padding={1}>
|
|
1005
|
+
<text dim>Ctrl+C to quit{IS_MOCK ? ' | Set ANTHROPIC_API_KEY for real Claude' : ''}</text>
|
|
1006
|
+
</box>
|
|
1007
|
+
</box>
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function App() {
|
|
1012
|
+
return (
|
|
1013
|
+
<AutoThemeProvider>
|
|
1014
|
+
<ErrorBoundary fallback={(err) => (
|
|
1015
|
+
<box border="single" borderColor="red" padding={1}>
|
|
1016
|
+
<text color="red" bold>Error</text>
|
|
1017
|
+
<text>{err.message}</text>
|
|
1018
|
+
</box>
|
|
1019
|
+
)}>
|
|
1020
|
+
<AiAssistant />
|
|
1021
|
+
</ErrorBoundary>
|
|
1022
|
+
</AutoThemeProvider>
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
render(<App />, { title: '${config.name}' });
|
|
1027
|
+
`
|
|
1028
|
+
}];
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/args.ts
|
|
1032
|
+
var TEMPLATE_KEYS = [
|
|
1033
|
+
"empty",
|
|
1034
|
+
"dashboard",
|
|
1035
|
+
"interactive-tool",
|
|
1036
|
+
"cli-wrapper",
|
|
1037
|
+
"cli-tool",
|
|
1038
|
+
"file-manager",
|
|
1039
|
+
"ai-assistant",
|
|
1040
|
+
"form-wizard"
|
|
1041
|
+
];
|
|
1042
|
+
function getValue(argv, key) {
|
|
1043
|
+
const index = argv.findIndex((a) => a === key || a.startsWith(`${key}=`));
|
|
1044
|
+
if (index === -1) return void 0;
|
|
1045
|
+
const value = argv[index];
|
|
1046
|
+
if (value.includes("=")) {
|
|
1047
|
+
return value.split("=")[1];
|
|
1048
|
+
}
|
|
1049
|
+
return argv[index + 1];
|
|
1050
|
+
}
|
|
1051
|
+
function parseArgs(argv) {
|
|
1052
|
+
const args = {
|
|
1053
|
+
yes: false,
|
|
1054
|
+
dryRun: false
|
|
1055
|
+
};
|
|
1056
|
+
if (argv[0] === "add") {
|
|
1057
|
+
const positional2 = [];
|
|
1058
|
+
for (let index = 1; index < argv.length; index++) {
|
|
1059
|
+
const value = argv[index];
|
|
1060
|
+
if (value === "--dir") {
|
|
1061
|
+
index++;
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (!value.startsWith("-")) {
|
|
1065
|
+
positional2.push(value);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
args.command = "add";
|
|
1069
|
+
args.component = positional2[0];
|
|
1070
|
+
args.dryRun = argv.includes("--dry-run");
|
|
1071
|
+
args.yes = argv.includes("--yes");
|
|
1072
|
+
const dirValue = getValue(argv, "--dir");
|
|
1073
|
+
if (dirValue) {
|
|
1074
|
+
args.dir = dirValue;
|
|
1075
|
+
}
|
|
1076
|
+
return args;
|
|
1077
|
+
}
|
|
1078
|
+
const positional = argv.find((a) => !a.startsWith("-"));
|
|
1079
|
+
if (positional) {
|
|
1080
|
+
args.name = positional;
|
|
1081
|
+
}
|
|
1082
|
+
if (argv.includes("--yes")) {
|
|
1083
|
+
args.yes = true;
|
|
1084
|
+
}
|
|
1085
|
+
const template = getValue(argv, "--template");
|
|
1086
|
+
if (template) {
|
|
1087
|
+
if (!TEMPLATE_KEYS.includes(template)) {
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
`Invalid template "${template}". Valid: ${TEMPLATE_KEYS.join(", ")}`
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
args.template = template;
|
|
1093
|
+
}
|
|
1094
|
+
const theme = getValue(argv, "--theme");
|
|
1095
|
+
if (theme) {
|
|
1096
|
+
args.theme = theme;
|
|
1097
|
+
}
|
|
1098
|
+
if (args.yes) {
|
|
1099
|
+
args.name = args.name ?? "my-termui-app";
|
|
1100
|
+
args.template = args.template ?? "empty";
|
|
1101
|
+
args.theme = args.theme ?? "default";
|
|
1102
|
+
}
|
|
1103
|
+
return args;
|
|
1104
|
+
}
|
|
1105
|
+
function isNonInteractive(args) {
|
|
1106
|
+
return args.yes === true || !!args.name && !!args.template && !!args.theme;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/commands/add.ts
|
|
1110
|
+
import { mkdirSync, writeFileSync, existsSync } from "fs";
|
|
1111
|
+
import { dirname as dirname2, join as join2, resolve as resolve2, relative as relative2, isAbsolute } from "path";
|
|
1112
|
+
import { execFileSync } from "child_process";
|
|
1113
|
+
var REGISTRY_BASE_URL = process.env.TERMUI_REGISTRY_URL ?? "https://termui.io";
|
|
1114
|
+
async function runAddCommand(options) {
|
|
1115
|
+
const componentName = options.component.trim();
|
|
1116
|
+
if (!componentName) {
|
|
1117
|
+
throw new Error("Component name is required.");
|
|
1118
|
+
}
|
|
1119
|
+
const schema = await fetchRegistrySchema();
|
|
1120
|
+
const componentEntry = findComponentEntry(schema, componentName);
|
|
1121
|
+
if (!componentEntry) {
|
|
1122
|
+
printAvailableComponents(schema);
|
|
1123
|
+
throw new Error(`Component "${componentName}" not found in registry.`);
|
|
1124
|
+
}
|
|
1125
|
+
const outputRoot = resolve2(process.cwd(), options.dir ?? "src/components");
|
|
1126
|
+
const destinationRoot = join2(outputRoot, componentEntry.name);
|
|
1127
|
+
const fileEntries = await downloadComponentFiles(componentEntry);
|
|
1128
|
+
if (options.dryRun) {
|
|
1129
|
+
printDryRunPreview(destinationRoot, fileEntries);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (existsSync(destinationRoot) && !options.yes) {
|
|
1133
|
+
const overwrite = await confirmPrompt(
|
|
1134
|
+
`Component directory already exists at ${destinationRoot}. Overwrite?`,
|
|
1135
|
+
false
|
|
1136
|
+
);
|
|
1137
|
+
if (!overwrite) {
|
|
1138
|
+
throw new Error("Aborted by user.");
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
writeComponentFiles(destinationRoot, fileEntries, componentEntry.name);
|
|
1142
|
+
await installPackages(componentEntry);
|
|
1143
|
+
console.log();
|
|
1144
|
+
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
1145
|
+
console.log(` \u2502 \u2705 ${componentEntry.name} added successfully! \u2502`);
|
|
1146
|
+
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
1147
|
+
console.log();
|
|
1148
|
+
console.log(" Import it with:");
|
|
1149
|
+
console.log(
|
|
1150
|
+
` import { ${pascalCase(componentEntry.name)} } from './components/${componentEntry.name}';`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
async function fetchRegistrySchema() {
|
|
1154
|
+
const url = `${REGISTRY_BASE_URL}/registry/schema.json`;
|
|
1155
|
+
const response = await fetch(url);
|
|
1156
|
+
if (!response.ok) {
|
|
1157
|
+
throw new Error(
|
|
1158
|
+
`Failed to fetch registry schema from ${url}: ${response.status} ${response.statusText}`
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
return await response.json();
|
|
1162
|
+
}
|
|
1163
|
+
function findComponentEntry(schema, componentName) {
|
|
1164
|
+
const normalized = componentName.toLowerCase();
|
|
1165
|
+
return schema.components.find(
|
|
1166
|
+
(entry) => entry.name.toLowerCase() === normalized
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
function printAvailableComponents(schema) {
|
|
1170
|
+
const names = schema.components.map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
1171
|
+
console.log("Available registry components:");
|
|
1172
|
+
for (const name of names) {
|
|
1173
|
+
console.log(` - ${name}`);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async function downloadComponentFiles(entry) {
|
|
1177
|
+
const downloads = entry.files.map(async (filePath) => {
|
|
1178
|
+
const rawUrl = `${REGISTRY_BASE_URL}/${filePath}`;
|
|
1179
|
+
const response = await fetch(rawUrl);
|
|
1180
|
+
if (!response.ok) {
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`Failed to download ${filePath} from registry: ${response.status} ${response.statusText}`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
path: filePath,
|
|
1187
|
+
content: await response.text()
|
|
1188
|
+
};
|
|
1189
|
+
});
|
|
1190
|
+
return await Promise.all(downloads);
|
|
1191
|
+
}
|
|
1192
|
+
function printDryRunPreview(destinationRoot, fileEntries) {
|
|
1193
|
+
console.log("Dry run preview \u2014 no files will be written.");
|
|
1194
|
+
console.log();
|
|
1195
|
+
for (const file of fileEntries) {
|
|
1196
|
+
const relative4 = getDestinationRelativePath(file.path, destinationRoot);
|
|
1197
|
+
console.log(` Would create: ${relative4}`);
|
|
1198
|
+
const preview = file.content.split("\n").slice(0, 6).map((line) => ` ${line}`).join("\n");
|
|
1199
|
+
console.log(preview);
|
|
1200
|
+
if (file.content.split("\n").length > 6) {
|
|
1201
|
+
console.log(" ...");
|
|
1202
|
+
}
|
|
1203
|
+
console.log();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function writeComponentFiles(destinationRoot, fileEntries, componentName) {
|
|
1207
|
+
for (const file of fileEntries) {
|
|
1208
|
+
const destPath = resolveDestinationPath(
|
|
1209
|
+
destinationRoot,
|
|
1210
|
+
file.path,
|
|
1211
|
+
componentName
|
|
1212
|
+
);
|
|
1213
|
+
const directory = dirname2(destPath);
|
|
1214
|
+
mkdirSync(directory, { recursive: true });
|
|
1215
|
+
writeFileSync(destPath, file.content, "utf-8");
|
|
1216
|
+
console.log(` \u2713 ${destPath}`);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function resolveDestinationPath(destinationRoot, registryFilePath, componentName) {
|
|
1220
|
+
const prefix = `registry/components/${componentName}/`;
|
|
1221
|
+
const relativePath = registryFilePath.startsWith(prefix) ? registryFilePath.slice(prefix.length) : registryFilePath;
|
|
1222
|
+
const destination = resolve2(destinationRoot, relativePath);
|
|
1223
|
+
const rel = relative2(resolve2(destinationRoot), destination);
|
|
1224
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
1225
|
+
throw new Error(`Destination ${destination} is outside project root`);
|
|
1226
|
+
}
|
|
1227
|
+
return destination;
|
|
1228
|
+
}
|
|
1229
|
+
function getDestinationRelativePath(registryFilePath, destinationRoot) {
|
|
1230
|
+
const pathSegments = registryFilePath.split("/");
|
|
1231
|
+
const componentIndex = pathSegments.indexOf("components");
|
|
1232
|
+
if (componentIndex !== -1 && componentIndex + 2 < pathSegments.length) {
|
|
1233
|
+
const relativePath = pathSegments.slice(componentIndex + 2).join("/");
|
|
1234
|
+
return join2(destinationRoot, relativePath);
|
|
1235
|
+
}
|
|
1236
|
+
return join2(destinationRoot, registryFilePath);
|
|
1237
|
+
}
|
|
1238
|
+
async function installPackages(entry) {
|
|
1239
|
+
const deps = [
|
|
1240
|
+
.../* @__PURE__ */ new Set([...entry.deps ?? [], ...entry.peerDeps ?? []])
|
|
1241
|
+
];
|
|
1242
|
+
if (deps.length === 0) {
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
execFileSync("bun", ["add", ...deps], {
|
|
1246
|
+
stdio: "inherit"
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
function pascalCase(value) {
|
|
1250
|
+
return value.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("");
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/validate.ts
|
|
1254
|
+
import { resolve as resolve3, relative as relative3, isAbsolute as isAbsolute2, sep } from "path";
|
|
1255
|
+
var VALID_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
1256
|
+
function validateProjectName(name) {
|
|
1257
|
+
if (typeof name !== "string") {
|
|
1258
|
+
throw new Error("Project name is required");
|
|
1259
|
+
}
|
|
1260
|
+
const trimmed = name.trim();
|
|
1261
|
+
if (trimmed.length === 0) {
|
|
1262
|
+
throw new Error("Project name cannot be empty");
|
|
1263
|
+
}
|
|
1264
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("\\")) {
|
|
1265
|
+
throw new Error(
|
|
1266
|
+
"Project name cannot be an absolute path"
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
"Project name cannot contain path separators or traversal sequences (/, \\, ..)"
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
if (!VALID_NAME_RE.test(trimmed)) {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
"Project name must contain only lowercase letters, numbers, hyphens, and underscores, and start with a letter or number"
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return trimmed;
|
|
1280
|
+
}
|
|
1281
|
+
function validateResolvedPath(cwd, projectName) {
|
|
1282
|
+
const resolved = resolve3(cwd, projectName);
|
|
1283
|
+
const cwdNorm = resolve3(cwd);
|
|
1284
|
+
const rel = relative3(cwdNorm, resolved);
|
|
1285
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute2(rel)) {
|
|
1286
|
+
throw new Error(
|
|
1287
|
+
"Security check failed: resolved path escapes current working directory"
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
503
1291
|
|
|
504
1292
|
// src/index.ts
|
|
505
|
-
var TEMPLATES = [
|
|
506
|
-
|
|
1293
|
+
var TEMPLATES = [
|
|
1294
|
+
"Empty (start from scratch)",
|
|
1295
|
+
"Dashboard (real-time data)",
|
|
1296
|
+
"Interactive Tool (forms, prompts)",
|
|
1297
|
+
"CLI Wrapper (wrap existing CLI)",
|
|
1298
|
+
"CLI Tool (minimal: box + text + useKeymap)",
|
|
1299
|
+
"File Manager",
|
|
1300
|
+
"AI Assistant (Claude + mock mode)",
|
|
1301
|
+
"Form Wizard"
|
|
1302
|
+
];
|
|
1303
|
+
var TEMPLATE_KEYS2 = [
|
|
1304
|
+
"empty",
|
|
1305
|
+
"dashboard",
|
|
1306
|
+
"interactive-tool",
|
|
1307
|
+
"cli-wrapper",
|
|
1308
|
+
"cli-tool",
|
|
1309
|
+
"file-manager",
|
|
1310
|
+
"ai-assistant",
|
|
1311
|
+
"form-wizard"
|
|
1312
|
+
];
|
|
507
1313
|
var FEATURES = ["Screen Router", "Data Providers", "Hot Reload"];
|
|
508
|
-
async function
|
|
1314
|
+
async function runCli(argv) {
|
|
1315
|
+
const args = parseArgs(argv);
|
|
1316
|
+
if (args.command === "add") {
|
|
1317
|
+
await runAddCommand({
|
|
1318
|
+
component: args.component ?? "",
|
|
1319
|
+
dir: args.dir,
|
|
1320
|
+
dryRun: args.dryRun,
|
|
1321
|
+
yes: args.yes
|
|
1322
|
+
});
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
await runProjectScaffold(args);
|
|
1326
|
+
}
|
|
1327
|
+
async function runProjectScaffold(args) {
|
|
509
1328
|
console.log();
|
|
510
1329
|
console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
511
1330
|
console.log(" \u2502 create-termui-app \u2502");
|
|
512
1331
|
console.log(" \u2502 The React/Next.js for CLI apps \u2502");
|
|
513
1332
|
console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
514
1333
|
console.log();
|
|
515
|
-
|
|
1334
|
+
const nonInteractive = isNonInteractive(args);
|
|
1335
|
+
let projectName = args.name;
|
|
516
1336
|
if (!projectName) {
|
|
517
|
-
|
|
1337
|
+
if (nonInteractive) {
|
|
1338
|
+
projectName = "my-termui-app";
|
|
1339
|
+
} else {
|
|
1340
|
+
projectName = await textPrompt("Project name", "my-termui-app");
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
projectName = validateProjectName(projectName);
|
|
1344
|
+
validateResolvedPath(process.cwd(), projectName);
|
|
1345
|
+
let template;
|
|
1346
|
+
if (args.template) {
|
|
1347
|
+
const templateIdx = TEMPLATE_KEYS2.indexOf(args.template);
|
|
1348
|
+
template = TEMPLATE_KEYS2[templateIdx >= 0 ? templateIdx : 0];
|
|
1349
|
+
} else if (nonInteractive) {
|
|
1350
|
+
template = "empty";
|
|
1351
|
+
} else {
|
|
1352
|
+
const templateIdx = await selectPrompt("What kind of app?", TEMPLATES);
|
|
1353
|
+
template = TEMPLATE_KEYS2[templateIdx >= 0 ? templateIdx : 0];
|
|
518
1354
|
}
|
|
519
|
-
const templateIdx = await selectPrompt("What kind of app?", TEMPLATES);
|
|
520
|
-
const template = TEMPLATE_KEYS[templateIdx];
|
|
521
1355
|
const themes = getBuiltinThemeNames();
|
|
522
|
-
|
|
523
|
-
|
|
1356
|
+
let theme;
|
|
1357
|
+
if (args.theme) {
|
|
1358
|
+
const themeIdx = themes.indexOf(args.theme);
|
|
1359
|
+
theme = themes[themeIdx >= 0 ? themeIdx : 0];
|
|
1360
|
+
} else if (nonInteractive) {
|
|
1361
|
+
theme = themes[0] || "default";
|
|
1362
|
+
} else {
|
|
1363
|
+
const themeIdx = await selectPrompt("Choose a theme", themes.map((t) => t.charAt(0).toUpperCase() + t.slice(1)));
|
|
1364
|
+
theme = themes[themeIdx >= 0 ? themeIdx : 0];
|
|
1365
|
+
}
|
|
524
1366
|
const featureDefaults = [false, template === "dashboard", true];
|
|
525
|
-
const featureFlags = await multiSelectPrompt("Features to include", FEATURES, featureDefaults);
|
|
1367
|
+
const featureFlags = nonInteractive ? featureDefaults : await multiSelectPrompt("Features to include", FEATURES, featureDefaults);
|
|
526
1368
|
const config = {
|
|
527
1369
|
name: projectName,
|
|
528
1370
|
template,
|
|
@@ -533,8 +1375,8 @@ async function main() {
|
|
|
533
1375
|
hotReload: featureFlags[2]
|
|
534
1376
|
}
|
|
535
1377
|
};
|
|
536
|
-
const projectDir =
|
|
537
|
-
if (
|
|
1378
|
+
const projectDir = resolve4(process.cwd(), projectName);
|
|
1379
|
+
if (existsSync2(projectDir)) {
|
|
538
1380
|
console.log(`
|
|
539
1381
|
\u26A0 Directory "${projectName}" already exists. Files may be overwritten.
|
|
540
1382
|
`);
|
|
@@ -543,10 +1385,10 @@ async function main() {
|
|
|
543
1385
|
Creating ${projectName}...`);
|
|
544
1386
|
const files = generateProject(config);
|
|
545
1387
|
for (const file of files) {
|
|
546
|
-
const fullPath =
|
|
547
|
-
const dir =
|
|
548
|
-
|
|
549
|
-
|
|
1388
|
+
const fullPath = join3(projectDir, file.path);
|
|
1389
|
+
const dir = dirname3(fullPath);
|
|
1390
|
+
mkdirSync2(dir, { recursive: true });
|
|
1391
|
+
writeFileSync2(fullPath, file.content, "utf-8");
|
|
550
1392
|
console.log(` \u2713 ${file.path}`);
|
|
551
1393
|
}
|
|
552
1394
|
console.log();
|
|
@@ -560,8 +1402,13 @@ async function main() {
|
|
|
560
1402
|
console.log(` bun run dev`);
|
|
561
1403
|
console.log();
|
|
562
1404
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1405
|
+
if (process.argv[1] === fileURLToPath2(import.meta.url)) {
|
|
1406
|
+
runCli(process.argv.slice(2)).catch((err) => {
|
|
1407
|
+
console.error("Error:", err.message);
|
|
1408
|
+
process.exit(1);
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
export {
|
|
1412
|
+
runCli
|
|
1413
|
+
};
|
|
567
1414
|
//# sourceMappingURL=index.js.map
|