create-termui-app 0.1.4 → 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 +1007 -157
- package/dist/index.js.map +1 -1
- package/package.json +13 -9
- 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/LICENSE +0 -21
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,37 +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: "tsx --watch src/index.tsx",
|
|
58
|
-
build: "tsup src/index.tsx --format esm",
|
|
59
|
-
start: "node 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
|
-
tsx: "^4.0.0",
|
|
74
|
-
tsup: "^8.0.0",
|
|
75
|
-
typescript: "^5.3.0"
|
|
76
|
-
}
|
|
77
|
-
}, null, 2) + "\n"
|
|
63
|
+
content: createPackageJson(config)
|
|
78
64
|
});
|
|
79
65
|
files.push({
|
|
80
66
|
path: "tsconfig.json",
|
|
@@ -118,11 +104,139 @@ export default defineConfig({
|
|
|
118
104
|
case "cli-wrapper":
|
|
119
105
|
files.push(...generateCliWrapperTemplate(config));
|
|
120
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;
|
|
121
119
|
default:
|
|
122
120
|
files.push(...generateEmptyTemplate(config));
|
|
123
121
|
}
|
|
124
122
|
return files;
|
|
125
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
|
+
}
|
|
126
240
|
function generateEmptyTemplate(config) {
|
|
127
241
|
return [{
|
|
128
242
|
path: "src/index.tsx",
|
|
@@ -158,109 +272,32 @@ render(<App />, { title: '${config.name}' });
|
|
|
158
272
|
}];
|
|
159
273
|
}
|
|
160
274
|
function generateDashboardTemplate(config) {
|
|
161
|
-
return
|
|
162
|
-
path: "src/index.tsx",
|
|
163
|
-
content: `/** @jsxImportSource @termuijs/jsx */
|
|
164
|
-
import { render, useState, useEffect, useKeymap, ErrorBoundary } from '@termuijs/jsx';
|
|
165
|
-
import { AutoThemeProvider, useTheme } from '@termuijs/tss';
|
|
166
|
-
${config.features.dataProviders ? "import { useCpu, useMemory, useDisk } from '@termuijs/data';" : ""}
|
|
167
|
-
|
|
168
|
-
// \u2500\u2500 Sample static data (replace with live hooks when dataProviders = true) \u2500\u2500
|
|
169
|
-
${config.features.dataProviders ? "" : `const SAMPLE_PROCS = [
|
|
170
|
-
{ Name: 'node', PID: 1234, 'CPU%': '5.0', 'MEM%': '2.1' },
|
|
171
|
-
{ Name: 'chrome', PID: 5678, 'CPU%': '12.3', 'MEM%': '8.4' },
|
|
172
|
-
{ Name: 'bash', PID: 9012, 'CPU%': '0.1', 'MEM%': '0.3' },
|
|
173
|
-
];`}
|
|
174
|
-
|
|
175
|
-
function GaugeRow({ label, value }: { label: string; value: number }) {
|
|
176
|
-
const theme = useTheme();
|
|
177
|
-
const filled = Math.round(value * 20);
|
|
178
|
-
const empty = 20 - filled;
|
|
179
|
-
const bar = '[' + '#'.repeat(filled) + '-'.repeat(empty) + ']';
|
|
180
|
-
return (
|
|
181
|
-
<row gap={2}>
|
|
182
|
-
<text color={theme.colors.primary}>{label.padEnd(4)}</text>
|
|
183
|
-
<text>{bar}</text>
|
|
184
|
-
<text>{(value * 100).toFixed(1).padStart(5)}%</text>
|
|
185
|
-
</row>
|
|
186
|
-
);
|
|
275
|
+
return loadTemplateFiles("dashboard", config);
|
|
187
276
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
${config.features.dataProviders ? ` const cpu = useCpu();
|
|
192
|
-
const mem = useMemory();
|
|
193
|
-
const disk = useDisk();
|
|
194
|
-
const cpuVal = (cpu.percent ?? 0) / 100;
|
|
195
|
-
const memVal = (mem.percent ?? 0) / 100;
|
|
196
|
-
const diskVal = (disk.percent ?? 0) / 100;` : ` const [cpuVal, setCpuVal] = useState(0.45);
|
|
197
|
-
const [memVal, setMemVal] = useState(0.62);
|
|
198
|
-
const [diskVal, setDiskVal] = useState(0.38);
|
|
199
|
-
|
|
200
|
-
// Simulate live updates
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
const id = setInterval(() => {
|
|
203
|
-
setCpuVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.05)));
|
|
204
|
-
setMemVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.02)));
|
|
205
|
-
setDiskVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.01)));
|
|
206
|
-
setTick(t => t + 1);
|
|
207
|
-
}, 1000);
|
|
208
|
-
return () => clearInterval(id);
|
|
209
|
-
}, []);`}
|
|
210
|
-
|
|
211
|
-
useKeymap([
|
|
212
|
-
{ key: 'q', action: () => process.exit(0), description: 'Quit' },
|
|
213
|
-
{ key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },
|
|
214
|
-
{ key: 'r', action: () => setTick(t => t + 1), description: 'Refresh' },
|
|
215
|
-
]);
|
|
216
|
-
|
|
217
|
-
const theme = useTheme();
|
|
218
|
-
|
|
219
|
-
return (
|
|
220
|
-
<box flexDirection="column" padding={1}>
|
|
221
|
-
<text bold color={theme.colors.primary}>${config.name} Dashboard</text>
|
|
222
|
-
<divider />
|
|
223
|
-
|
|
224
|
-
<grid columns={12} gap={1}>
|
|
225
|
-
{/* Gauges \u2014 top row */}
|
|
226
|
-
<box width="100%" flexDirection="column" border="single" padding={1} flexGrow={4}>
|
|
227
|
-
<text bold>System Resources</text>
|
|
228
|
-
<GaugeRow label="CPU" value={cpuVal} />
|
|
229
|
-
<GaugeRow label="MEM" value={memVal} />
|
|
230
|
-
<GaugeRow label="DISK" value={diskVal} />
|
|
231
|
-
</box>
|
|
232
|
-
|
|
233
|
-
{/* Info panel */}
|
|
234
|
-
<box width="100%" flexDirection="column" border="single" padding={1} flexGrow={8}>
|
|
235
|
-
<text bold>Process Summary</text>
|
|
236
|
-
<text color={theme.colors.muted}>Press r to refresh, q to quit</text>
|
|
237
|
-
<text>Tick: {tick}</text>
|
|
238
|
-
${config.features.dataProviders ? ` <skeleton variant="shimmer" />` : ` <text>node PID:1234 CPU: {(cpuVal * 100).toFixed(1)}%</text>
|
|
239
|
-
<text>chrome PID:5678 MEM: {(memVal * 100).toFixed(1)}%</text>`}
|
|
240
|
-
</box>
|
|
241
|
-
</grid>
|
|
242
|
-
</box>
|
|
243
|
-
);
|
|
277
|
+
function loadTemplateFiles(templateName, config) {
|
|
278
|
+
const templatePath = resolve(TEMPLATES_ROOT, templateName);
|
|
279
|
+
return walkTemplateDirectory(templatePath, templatePath, config);
|
|
244
280
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
);
|
|
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;
|
|
259
298
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
`
|
|
263
|
-
}];
|
|
299
|
+
function replaceTemplatePlaceholders(content, config) {
|
|
300
|
+
return content.replace(/{{name}}/g, config.name);
|
|
264
301
|
}
|
|
265
302
|
function generateInteractiveTemplate(config) {
|
|
266
303
|
return [{
|
|
@@ -287,11 +324,11 @@ function InteractiveTool() {
|
|
|
287
324
|
useKeymap([
|
|
288
325
|
{ key: 'q', action: () => process.exit(0), description: 'Quit' },
|
|
289
326
|
{ key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },
|
|
290
|
-
{ key: '
|
|
291
|
-
{ 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' },
|
|
292
329
|
{ key: 'k', action: () => setSelected(s => Math.max(0, s - 1)), description: 'Move up (vim)' },
|
|
293
330
|
{ key: 'j', action: () => setSelected(s => Math.min(items.length - 1, s + 1)), description: 'Move down (vim)' },
|
|
294
|
-
{ key: '
|
|
331
|
+
{ key: 'enter', action: () => {
|
|
295
332
|
const item = items[selected];
|
|
296
333
|
if (item) setDone(d => d.includes(item) ? d.filter(x => x !== item) : [...d, item]);
|
|
297
334
|
}, description: 'Toggle selected' },
|
|
@@ -348,6 +385,24 @@ function App() {
|
|
|
348
385
|
);
|
|
349
386
|
}
|
|
350
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
|
+
}
|
|
351
406
|
render(<App />, { title: '${config.name}' });
|
|
352
407
|
`
|
|
353
408
|
}];
|
|
@@ -390,7 +445,7 @@ function CliWrapper() {
|
|
|
390
445
|
]);
|
|
391
446
|
const [running, setRunning] = useState(false);
|
|
392
447
|
const [exitCode, setExitCode] = useState<number | null>(null);
|
|
393
|
-
const procRef = useRef<
|
|
448
|
+
const procRef = useRef<ReturnType<typeof spawn> | null>(null);
|
|
394
449
|
const theme = useTheme();
|
|
395
450
|
|
|
396
451
|
const addLog = (level: LogLevel, text: string) =>
|
|
@@ -493,33 +548,823 @@ function App() {
|
|
|
493
548
|
);
|
|
494
549
|
}
|
|
495
550
|
|
|
551
|
+
render(<App />, { title: '${config.name}' });
|
|
552
|
+
`
|
|
553
|
+
}];
|
|
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
|
+
|
|
496
1026
|
render(<App />, { title: '${config.name}' });
|
|
497
1027
|
`
|
|
498
1028
|
}];
|
|
499
1029
|
}
|
|
500
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
|
+
}
|
|
1291
|
+
|
|
501
1292
|
// src/index.ts
|
|
502
|
-
var TEMPLATES = [
|
|
503
|
-
|
|
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
|
+
];
|
|
504
1313
|
var FEATURES = ["Screen Router", "Data Providers", "Hot Reload"];
|
|
505
|
-
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) {
|
|
506
1328
|
console.log();
|
|
507
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");
|
|
508
1330
|
console.log(" \u2502 create-termui-app \u2502");
|
|
509
1331
|
console.log(" \u2502 The React/Next.js for CLI apps \u2502");
|
|
510
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");
|
|
511
1333
|
console.log();
|
|
512
|
-
|
|
1334
|
+
const nonInteractive = isNonInteractive(args);
|
|
1335
|
+
let projectName = args.name;
|
|
513
1336
|
if (!projectName) {
|
|
514
|
-
|
|
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];
|
|
515
1354
|
}
|
|
516
|
-
const templateIdx = await selectPrompt("What kind of app?", TEMPLATES);
|
|
517
|
-
const template = TEMPLATE_KEYS[templateIdx];
|
|
518
1355
|
const themes = getBuiltinThemeNames();
|
|
519
|
-
|
|
520
|
-
|
|
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
|
+
}
|
|
521
1366
|
const featureDefaults = [false, template === "dashboard", true];
|
|
522
|
-
const featureFlags = await multiSelectPrompt("Features to include", FEATURES, featureDefaults);
|
|
1367
|
+
const featureFlags = nonInteractive ? featureDefaults : await multiSelectPrompt("Features to include", FEATURES, featureDefaults);
|
|
523
1368
|
const config = {
|
|
524
1369
|
name: projectName,
|
|
525
1370
|
template,
|
|
@@ -530,8 +1375,8 @@ async function main() {
|
|
|
530
1375
|
hotReload: featureFlags[2]
|
|
531
1376
|
}
|
|
532
1377
|
};
|
|
533
|
-
const projectDir =
|
|
534
|
-
if (
|
|
1378
|
+
const projectDir = resolve4(process.cwd(), projectName);
|
|
1379
|
+
if (existsSync2(projectDir)) {
|
|
535
1380
|
console.log(`
|
|
536
1381
|
\u26A0 Directory "${projectName}" already exists. Files may be overwritten.
|
|
537
1382
|
`);
|
|
@@ -540,10 +1385,10 @@ async function main() {
|
|
|
540
1385
|
Creating ${projectName}...`);
|
|
541
1386
|
const files = generateProject(config);
|
|
542
1387
|
for (const file of files) {
|
|
543
|
-
const fullPath =
|
|
544
|
-
const dir =
|
|
545
|
-
|
|
546
|
-
|
|
1388
|
+
const fullPath = join3(projectDir, file.path);
|
|
1389
|
+
const dir = dirname3(fullPath);
|
|
1390
|
+
mkdirSync2(dir, { recursive: true });
|
|
1391
|
+
writeFileSync2(fullPath, file.content, "utf-8");
|
|
547
1392
|
console.log(` \u2713 ${file.path}`);
|
|
548
1393
|
}
|
|
549
1394
|
console.log();
|
|
@@ -553,12 +1398,17 @@ async function main() {
|
|
|
553
1398
|
console.log();
|
|
554
1399
|
console.log(` Next steps:`);
|
|
555
1400
|
console.log(` cd ${projectName}`);
|
|
556
|
-
console.log(`
|
|
557
|
-
console.log(`
|
|
1401
|
+
console.log(` bun install`);
|
|
1402
|
+
console.log(` bun run dev`);
|
|
558
1403
|
console.log();
|
|
559
1404
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
+
};
|
|
564
1414
|
//# sourceMappingURL=index.js.map
|