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/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 { mkdirSync, writeFileSync, existsSync } from "fs";
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((resolve2) => {
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
- resolve2(answer.trim() || defaultValue || "");
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: JSON.stringify({
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
- function Dashboard() {
193
- const [tick, setTick] = useState(0);
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
- function App() {
250
- return (
251
- <AutoThemeProvider>
252
- <ErrorBoundary fallback={(err) => (
253
- <box border="single" borderColor="red" padding={1}>
254
- <text color="red" bold>Dashboard Error</text>
255
- <text>{err.message}</text>
256
- </box>
257
- )}>
258
- <Dashboard />
259
- </ErrorBoundary>
260
- </AutoThemeProvider>
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
- render(<App />, { title: '${config.name}' });
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: 'ArrowUp', action: () => setSelected(s => Math.max(0, s - 1)), description: 'Move up' },
294
- { key: 'ArrowDown', action: () => setSelected(s => Math.min(items.length - 1, s + 1)), description: 'Move down' },
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: 'Enter', action: () => {
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<any>(null);
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}>&gt; </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 = ["Empty (start from scratch)", "Dashboard (real-time data)", "Interactive Tool (forms, prompts)", "CLI Wrapper (wrap existing CLI)"];
506
- var TEMPLATE_KEYS = ["empty", "dashboard", "interactive-tool", "cli-wrapper"];
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 main() {
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
- let projectName = process.argv[2];
1334
+ const nonInteractive = isNonInteractive(args);
1335
+ let projectName = args.name;
516
1336
  if (!projectName) {
517
- projectName = await textPrompt("Project name", "my-termui-app");
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
- const themeIdx = await selectPrompt("Choose a theme", themes.map((t) => t.charAt(0).toUpperCase() + t.slice(1)));
523
- const theme = themes[themeIdx];
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 = resolve(process.cwd(), projectName);
537
- if (existsSync(projectDir)) {
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 = join(projectDir, file.path);
547
- const dir = fullPath.substring(0, fullPath.lastIndexOf("/"));
548
- mkdirSync(dir, { recursive: true });
549
- writeFileSync(fullPath, file.content, "utf-8");
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
- main().catch((err) => {
564
- console.error("Error:", err.message);
565
- process.exit(1);
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