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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/prompts.ts","../src/templates.ts"],"sourcesContent":["// ─────────────────────────────────────────────────────\n// create-termui-app — Interactive CLI scaffolding tool\n// ─────────────────────────────────────────────────────\n\nimport { resolve, join } from 'node:path';\nimport { mkdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { getBuiltinThemeNames } from '@termuijs/tss';\nimport { textPrompt, selectPrompt, multiSelectPrompt } from './prompts.js';\nimport { generateProject, type ProjectConfig } from './templates.js';\n\nconst TEMPLATES = ['Empty (start from scratch)', 'Dashboard (real-time data)', 'Interactive Tool (forms, prompts)', 'CLI Wrapper (wrap existing CLI)'];\nconst TEMPLATE_KEYS = ['empty', 'dashboard', 'interactive-tool', 'cli-wrapper'] as const;\nconst FEATURES = ['Screen Router', 'Data Providers', 'Hot Reload'];\n\nasync function main() {\n console.log();\n console.log(' ┌──────────────────────────────────┐');\n console.log(' │ create-termui-app │');\n console.log(' │ The React/Next.js for CLI apps │');\n console.log(' └──────────────────────────────────┘');\n console.log();\n\n // ── Get project name from args or prompt ──\n let projectName = process.argv[2];\n if (!projectName) {\n projectName = await textPrompt('Project name', 'my-termui-app');\n }\n\n // ── Template selection ──\n const templateIdx = await selectPrompt('What kind of app?', TEMPLATES);\n const template = TEMPLATE_KEYS[templateIdx];\n\n // ── Theme selection ──\n const themes = getBuiltinThemeNames();\n const themeIdx = await selectPrompt('Choose a theme', themes.map(t => t.charAt(0).toUpperCase() + t.slice(1)));\n const theme = themes[themeIdx];\n\n // ── Feature selection ──\n const featureDefaults = [false, template === 'dashboard', true]; // Router off, Data on for dashboard, HotReload on\n const featureFlags = await multiSelectPrompt('Features to include', FEATURES, featureDefaults);\n\n const config: ProjectConfig = {\n name: projectName,\n template,\n theme,\n features: {\n router: featureFlags[0],\n dataProviders: featureFlags[1],\n hotReload: featureFlags[2],\n },\n };\n\n // ── Generate project ──\n const projectDir = resolve(process.cwd(), projectName);\n if (existsSync(projectDir)) {\n console.log(`\\n ⚠ Directory \"${projectName}\" already exists. Files may be overwritten.\\n`);\n }\n\n console.log(`\\n Creating ${projectName}...`);\n\n const files = generateProject(config);\n\n for (const file of files) {\n const fullPath = join(projectDir, file.path);\n const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));\n mkdirSync(dir, { recursive: true });\n writeFileSync(fullPath, file.content, 'utf-8');\n console.log(` ✓ ${file.path}`);\n }\n\n console.log();\n console.log(' ┌──────────────────────────────────┐');\n console.log(' │ ✅ Project created successfully! │');\n console.log(' └──────────────────────────────────┘');\n console.log();\n console.log(` Next steps:`);\n console.log(` cd ${projectName}`);\n console.log(` npm install`);\n console.log(` npm run dev`);\n console.log();\n}\n\nmain().catch(err => {\n console.error('Error:', err.message);\n process.exit(1);\n});\n","// ─────────────────────────────────────────────────────\n// Minimal interactive prompts (no external deps)\n// ─────────────────────────────────────────────────────\n\nimport { createInterface } from 'node:readline';\n\nconst rl = () => createInterface({ input: process.stdin, output: process.stdout });\n\nexport async function textPrompt(question: string, defaultValue?: string): Promise<string> {\n return new Promise(resolve => {\n const r = rl();\n const suffix = defaultValue ? ` (${defaultValue})` : '';\n r.question(` ${question}${suffix}: `, answer => {\n r.close();\n resolve(answer.trim() || defaultValue || '');\n });\n });\n}\n\nexport async function selectPrompt(question: string, options: string[]): Promise<number> {\n console.log(`\\n ${question}`);\n for (let i = 0; i < options.length; i++) {\n console.log(` ${i + 1}) ${options[i]}`);\n }\n const answer = await textPrompt('Enter number', '1');\n const idx = parseInt(answer) - 1;\n return Math.max(0, Math.min(idx, options.length - 1));\n}\n\nexport async function confirmPrompt(question: string, defaultValue = true): Promise<boolean> {\n const suffix = defaultValue ? '(Y/n)' : '(y/N)';\n const answer = await textPrompt(`${question} ${suffix}`);\n if (!answer) return defaultValue;\n return answer.toLowerCase().startsWith('y');\n}\n\nexport async function multiSelectPrompt(question: string, options: string[], defaults: boolean[] = []): Promise<boolean[]> {\n console.log(`\\n ${question} (comma-separated numbers, or 'all')`);\n for (let i = 0; i < options.length; i++) {\n const def = defaults[i] ? '✓' : ' ';\n console.log(` ${i + 1}) [${def}] ${options[i]}`);\n }\n const answer = await textPrompt('Enter numbers', defaults.some(d => d) ? 'keep defaults' : 'all');\n if (answer === 'all') return options.map(() => true);\n if (answer === 'keep defaults' || answer === '') return defaults.length ? defaults : options.map(() => true);\n const selected = answer.split(',').map(s => parseInt(s.trim()) - 1);\n return options.map((_, i) => selected.includes(i));\n}\n","// ─────────────────────────────────────────────────────\n// Project Templates — generates files for new apps\n// ─────────────────────────────────────────────────────\n\nimport { getBuiltinTheme } from '@termuijs/tss';\n\nexport interface ProjectConfig {\n name: string;\n template: 'empty' | 'dashboard' | 'interactive-tool' | 'cli-wrapper';\n theme: string;\n features: {\n router: boolean;\n dataProviders: boolean;\n hotReload: boolean;\n };\n}\n\nexport interface GeneratedFile {\n path: string;\n content: string;\n}\n\nexport function generateProject(config: ProjectConfig): GeneratedFile[] {\n const files: GeneratedFile[] = [];\n\n // ── package.json ──\n files.push({\n path: 'package.json',\n content: JSON.stringify({\n name: config.name,\n version: '0.1.0',\n private: true,\n type: 'module',\n scripts: {\n dev: 'tsx --watch src/index.tsx',\n build: 'tsup src/index.tsx --format esm',\n start: 'node dist/index.js',\n },\n dependencies: {\n '@termuijs/core': 'latest',\n '@termuijs/widgets': 'latest',\n '@termuijs/ui': 'latest',\n '@termuijs/jsx': 'latest',\n '@termuijs/tss': 'latest',\n '@termuijs/quick': 'latest',\n '@termuijs/motion': 'latest',\n ...(config.features.dataProviders ? { '@termuijs/data': 'latest' } : {}),\n ...(config.features.router ? { '@termuijs/router': 'latest' } : {}),\n },\n devDependencies: {\n tsx: '^4.0.0',\n tsup: '^8.0.0',\n typescript: '^5.3.0',\n },\n }, null, 2) + '\\n',\n });\n\n // ── tsconfig.json ──\n files.push({\n path: 'tsconfig.json',\n content: JSON.stringify({\n compilerOptions: {\n target: 'ES2022',\n module: 'ESNext',\n moduleResolution: 'bundler',\n jsx: 'react-jsx',\n jsxImportSource: '@termuijs/jsx',\n strict: true,\n esModuleInterop: true,\n outDir: 'dist',\n rootDir: 'src',\n },\n include: ['src'],\n }, null, 2) + '\\n',\n });\n\n // ── termui.config.ts ──\n files.push({\n path: 'termui.config.ts',\n content: `import { defineConfig } from '@termuijs/core';\n\nexport default defineConfig({\n theme: '${config.theme}',\n ${config.features.hotReload ? \"hotReload: true,\" : ''}\n ${config.features.router ? \"router: { dir: './screens' },\" : ''}\n});\n`,\n });\n\n // ── Theme file ──\n const themeSrc = getBuiltinTheme(config.theme);\n if (themeSrc) {\n files.push({ path: `themes/${config.theme}.tss`, content: themeSrc.trim() + '\\n' });\n }\n\n // ── Template-specific files ──\n switch (config.template) {\n case 'dashboard':\n files.push(...generateDashboardTemplate(config));\n break;\n case 'interactive-tool':\n files.push(...generateInteractiveTemplate(config));\n break;\n case 'cli-wrapper':\n files.push(...generateCliWrapperTemplate(config));\n break;\n default:\n files.push(...generateEmptyTemplate(config));\n }\n\n return files;\n}\n\nfunction generateEmptyTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState, useKeymap, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider } from '@termuijs/tss';\n\nfunction App() {\n const [count, setCount] = useState(0);\n\n useKeymap([\n { key: 'q', action: () => process.exit(0), description: 'Quit' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n { key: '+', action: () => setCount(c => c + 1), description: 'Increment' },\n { key: '-', action: () => setCount(c => Math.max(0, c - 1)), description: 'Decrement' },\n ]);\n\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => <text color=\"red\">{err.message}</text>}>\n <box border=\"single\" padding={1}>\n <text bold>Welcome to ${config.name}!</text>\n <text>Edit src/index.tsx to get started.</text>\n <text>Count: {count} (+/- to change, q to quit)</text>\n </box>\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\nfunction generateDashboardTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState, useEffect, useKeymap, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider, useTheme } from '@termuijs/tss';\n${config.features.dataProviders ? \"import { useCpu, useMemory, useDisk } from '@termuijs/data';\" : ''}\n\n// ── Sample static data (replace with live hooks when dataProviders = true) ──\n${config.features.dataProviders ? '' : `const SAMPLE_PROCS = [\n { Name: 'node', PID: 1234, 'CPU%': '5.0', 'MEM%': '2.1' },\n { Name: 'chrome', PID: 5678, 'CPU%': '12.3', 'MEM%': '8.4' },\n { Name: 'bash', PID: 9012, 'CPU%': '0.1', 'MEM%': '0.3' },\n];`}\n\nfunction GaugeRow({ label, value }: { label: string; value: number }) {\n const theme = useTheme();\n const filled = Math.round(value * 20);\n const empty = 20 - filled;\n const bar = '[' + '#'.repeat(filled) + '-'.repeat(empty) + ']';\n return (\n <row gap={2}>\n <text color={theme.colors.primary}>{label.padEnd(4)}</text>\n <text>{bar}</text>\n <text>{(value * 100).toFixed(1).padStart(5)}%</text>\n </row>\n );\n}\n\nfunction Dashboard() {\n const [tick, setTick] = useState(0);\n${config.features.dataProviders\n ? ` const cpu = useCpu();\n const mem = useMemory();\n const disk = useDisk();\n const cpuVal = (cpu.percent ?? 0) / 100;\n const memVal = (mem.percent ?? 0) / 100;\n const diskVal = (disk.percent ?? 0) / 100;`\n : ` const [cpuVal, setCpuVal] = useState(0.45);\n const [memVal, setMemVal] = useState(0.62);\n const [diskVal, setDiskVal] = useState(0.38);\n\n // Simulate live updates\n useEffect(() => {\n const id = setInterval(() => {\n setCpuVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.05)));\n setMemVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.02)));\n setDiskVal(v => Math.min(1, Math.max(0, v + (Math.random() - 0.5) * 0.01)));\n setTick(t => t + 1);\n }, 1000);\n return () => clearInterval(id);\n }, []);`}\n\n useKeymap([\n { key: 'q', action: () => process.exit(0), description: 'Quit' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n { key: 'r', action: () => setTick(t => t + 1), description: 'Refresh' },\n ]);\n\n const theme = useTheme();\n\n return (\n <box flexDirection=\"column\" padding={1}>\n <text bold color={theme.colors.primary}>${config.name} Dashboard</text>\n <divider />\n\n <grid columns={12} gap={1}>\n {/* Gauges — top row */}\n <box width=\"100%\" flexDirection=\"column\" border=\"single\" padding={1} flexGrow={4}>\n <text bold>System Resources</text>\n <GaugeRow label=\"CPU\" value={cpuVal} />\n <GaugeRow label=\"MEM\" value={memVal} />\n <GaugeRow label=\"DISK\" value={diskVal} />\n </box>\n\n {/* Info panel */}\n <box width=\"100%\" flexDirection=\"column\" border=\"single\" padding={1} flexGrow={8}>\n <text bold>Process Summary</text>\n <text color={theme.colors.muted}>Press r to refresh, q to quit</text>\n <text>Tick: {tick}</text>\n${config.features.dataProviders\n ? ` <skeleton variant=\"shimmer\" />`\n : ` <text>node PID:1234 CPU: {(cpuVal * 100).toFixed(1)}%</text>\n <text>chrome PID:5678 MEM: {(memVal * 100).toFixed(1)}%</text>`}\n </box>\n </grid>\n </box>\n );\n}\n\nfunction App() {\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => (\n <box border=\"single\" borderColor=\"red\" padding={1}>\n <text color=\"red\" bold>Dashboard Error</text>\n <text>{err.message}</text>\n </box>\n )}>\n <Dashboard />\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\nfunction generateInteractiveTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState, useKeymap, useRef, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider, useTheme } from '@termuijs/tss';\nimport { caps } from '@termuijs/core';\n\n// ASCII-safe symbols\nconst CHECK = caps.unicode ? '✓' : 'v';\nconst BULLET = caps.unicode ? '›' : '>';\nconst SEP = caps.unicode ? '─'.repeat(40) : '-'.repeat(40);\n\nconst INITIAL_ITEMS = ['Option A', 'Option B', 'Option C'];\n\nfunction InteractiveTool() {\n const [items, setItems] = useState<string[]>(INITIAL_ITEMS);\n const [selected, setSelected] = useState(0);\n const [input, setInput] = useState('');\n const [done, setDone] = useState<string[]>([]);\n const theme = useTheme();\n\n useKeymap([\n { key: 'q', action: () => process.exit(0), description: 'Quit' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n { key: 'ArrowUp', action: () => setSelected(s => Math.max(0, s - 1)), description: 'Move up' },\n { key: 'ArrowDown', action: () => setSelected(s => Math.min(items.length - 1, s + 1)), description: 'Move down' },\n { key: 'k', action: () => setSelected(s => Math.max(0, s - 1)), description: 'Move up (vim)' },\n { key: 'j', action: () => setSelected(s => Math.min(items.length - 1, s + 1)), description: 'Move down (vim)' },\n { key: 'Enter', action: () => {\n const item = items[selected];\n if (item) setDone(d => d.includes(item) ? d.filter(x => x !== item) : [...d, item]);\n }, description: 'Toggle selected' },\n { key: 'Backspace', action: () => setInput(v => v.slice(0, -1)), description: 'Delete char' },\n { key: 'n', action: () => {\n if (input.trim()) {\n setItems(prev => [...prev, input.trim()]);\n setInput('');\n }\n }, description: 'Add new item' },\n ]);\n\n return (\n <box flexDirection=\"column\" padding={1}>\n <text bold color={theme.colors.primary}>${config.name}</text>\n <text color={theme.colors.muted}>j/k or arrows: navigate | Enter: toggle | n: add | q: quit</text>\n <text>{SEP}</text>\n\n <box flexDirection=\"column\">\n {items.map((item, i) => (\n <row key={item} gap={1}>\n <text color={i === selected ? theme.colors.primary : undefined}>\n {i === selected ? BULLET : ' '}\n </text>\n <text color={done.includes(item) ? theme.colors.success : undefined}>\n {done.includes(item) ? CHECK + ' ' : ' '}{item}\n </text>\n </row>\n ))}\n </box>\n\n <text>{SEP}</text>\n <row gap={1}>\n <text color={theme.colors.muted}>New item:</text>\n <text>{input}_</text>\n </row>\n <text color={theme.colors.muted} dim>Type letters then press n to add</text>\n </box>\n );\n}\n\nfunction App() {\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => (\n <box border=\"single\" borderColor=\"red\" padding={1}>\n <text color=\"red\" bold>Error</text>\n <text>{err.message}</text>\n </box>\n )}>\n <InteractiveTool />\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\nfunction generateCliWrapperTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState, useEffect, useKeymap, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider, useTheme } from '@termuijs/tss';\nimport { caps } from '@termuijs/core';\nimport { spawn } from 'node:child_process';\n\n// ASCII-safe symbols for terminals without full unicode support\nconst ICON_RUN = caps.unicode ? '>' : '>';\nconst ICON_DONE = caps.unicode ? '*' : '*';\nconst ICON_ERR = caps.unicode ? '!' : '!';\nconst SEP = '-'.repeat(60);\n\ntype LogLevel = 'info' | 'debug' | 'error' | 'warn';\ninterface LogLine {\n level: LogLevel;\n text: string;\n ts: number;\n}\n\nfunction levelColor(level: LogLevel): string {\n switch (level) {\n case 'info': return 'green';\n case 'debug': return 'cyan';\n case 'warn': return 'yellow';\n case 'error': return 'red';\n }\n}\n\nfunction CliWrapper() {\n const [logs, setLogs] = useState<LogLine[]>([\n { level: 'info', text: 'Application started', ts: Date.now() },\n { level: 'debug', text: 'Press r to re-run, q to quit', ts: Date.now() },\n ]);\n const [running, setRunning] = useState(false);\n const [exitCode, setExitCode] = useState<number | null>(null);\n const procRef = useRef<any>(null);\n const theme = useTheme();\n\n const addLog = (level: LogLevel, text: string) =>\n setLogs(prev => [...prev.slice(-200), { level, text, ts: Date.now() }]);\n\n // Example: run 'echo hello' — replace with your real command\n const runCommand = () => {\n if (running) return;\n setRunning(true);\n setExitCode(null);\n addLog('info', \\`\\${ICON_RUN} Running command...\\`);\n\n const proc = spawn('echo', ['Hello from CLI wrapper!']);\n procRef.current = proc;\n proc.stdout.on('data', (d: Buffer) => {\n for (const line of d.toString().split('\\\\n').filter(Boolean)) {\n addLog('info', line);\n }\n });\n proc.stderr.on('data', (d: Buffer) => {\n for (const line of d.toString().split('\\\\n').filter(Boolean)) {\n addLog('error', line);\n }\n });\n proc.on('close', (code: number | null) => {\n setRunning(false);\n setExitCode(code);\n addLog(code === 0 ? 'info' : 'error',\n \\`\\${code === 0 ? ICON_DONE : ICON_ERR} Process exited with code \\${code ?? 'null'}\\`);\n procRef.current = null;\n });\n };\n\n // Auto-run on mount, kill process on unmount\n useEffect(() => {\n runCommand();\n return () => {\n if (procRef.current) {\n procRef.current.kill();\n procRef.current = null;\n }\n };\n }, []);\n\n useKeymap([\n { key: 'q', action: () => process.exit(0), description: 'Quit' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n { key: 'r', action: runCommand, description: 'Re-run command' },\n { key: 'l', action: () => setLogs([]), description: 'Clear logs' },\n ]);\n\n return (\n <box flexDirection=\"column\" padding={1}>\n <row gap={2}>\n <text bold color={theme.colors.primary}>${config.name}</text>\n <text color={running ? theme.colors.warning : theme.colors.muted}>\n {running ? 'Running...' : exitCode === null ? 'Ready' : \\`Exit: \\${exitCode}\\`}\n </text>\n <spacer />\n <text color={theme.colors.muted}>r: re-run | l: clear | q: quit</text>\n </row>\n <text>{SEP}</text>\n\n <box flexDirection=\"column\" flexGrow={1}>\n {logs.map((line, i) => (\n <row key={i} gap={1}>\n <text color={theme.colors.muted} dim>\n {new Date(line.ts).toLocaleTimeString()}\n </text>\n <text color={levelColor(line.level)} bold>\n {line.level.toUpperCase().padEnd(5)}\n </text>\n <text>{line.text}</text>\n </row>\n ))}\n </box>\n\n {!caps.color && (\n <text color=\"yellow\" dim>\n Note: running in a terminal without color support (TERM={process.env.TERM ?? 'unset'})\n </text>\n )}\n </box>\n );\n}\n\nfunction App() {\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => (\n <box border=\"single\" borderColor=\"red\" padding={1}>\n <text color=\"red\" bold>CLI Wrapper Error</text>\n <text>{err.message}</text>\n <text color=\"yellow\">Check that the command exists and is executable.</text>\n </box>\n )}>\n <CliWrapper />\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\n"],"mappings":"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","names":["resolve"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/prompts.ts","../src/templates.ts","../src/args.ts","../src/commands/add.ts","../src/validate.ts"],"sourcesContent":["// ─────────────────────────────────────────────────────\n// create-termui-app — Interactive CLI scaffolding tool\n// ─────────────────────────────────────────────────────\n\nimport { dirname, resolve, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { mkdirSync, writeFileSync, existsSync } from 'node:fs';\nimport { getBuiltinThemeNames } from '@termuijs/tss';\nimport { textPrompt, selectPrompt, multiSelectPrompt } from './prompts.js';\nimport { generateProject, type ProjectConfig } from './templates.js';\nimport { parseArgs, isNonInteractive, type CliArgs } from './args.js';\nimport { runAddCommand } from './commands/add.js';\nimport { validateProjectName, validateResolvedPath } from \"./validate.js\";\n\nconst TEMPLATES = [\n 'Empty (start from scratch)',\n 'Dashboard (real-time data)',\n 'Interactive Tool (forms, prompts)',\n 'CLI Wrapper (wrap existing CLI)',\n 'CLI Tool (minimal: box + text + useKeymap)',\n 'File Manager',\n 'AI Assistant (Claude + mock mode)',\n 'Form Wizard',\n];\n\nconst TEMPLATE_KEYS = [\n 'empty',\n 'dashboard',\n 'interactive-tool',\n 'cli-wrapper',\n 'cli-tool',\n 'file-manager',\n 'ai-assistant',\n 'form-wizard',\n] as const;\nconst FEATURES = ['Screen Router', 'Data Providers', 'Hot Reload'];\n\nexport async function runCli(argv: string[]): Promise<void> {\n const args = parseArgs(argv);\n\n if (args.command === 'add') {\n await runAddCommand({\n component: args.component ?? '',\n dir: args.dir,\n dryRun: args.dryRun,\n yes: args.yes,\n });\n return;\n }\n\n await runProjectScaffold(args);\n}\n\nasync function runProjectScaffold(args: CliArgs): Promise<void> {\n console.log();\n console.log(' ┌──────────────────────────────────┐');\n console.log(' │ create-termui-app │');\n console.log(' │ The React/Next.js for CLI apps │');\n console.log(' └──────────────────────────────────┘');\n console.log();\n\n const nonInteractive = isNonInteractive(args);\n\n // ── Get project name from args or prompt ──\n let projectName = args.name;\n if (!projectName) {\n if (nonInteractive) {\n projectName = 'my-termui-app';\n } else {\n projectName = await textPrompt('Project name', 'my-termui-app');\n }\n }\n projectName = validateProjectName(projectName);\n validateResolvedPath(process.cwd(), projectName);\n\n // ── Template selection ──\n let template: typeof TEMPLATE_KEYS[number];\n if (args.template) {\n const templateIdx = TEMPLATE_KEYS.indexOf(args.template as typeof TEMPLATE_KEYS[number]);\n template = TEMPLATE_KEYS[templateIdx >= 0 ? templateIdx : 0];\n } else if (nonInteractive) {\n template = 'empty';\n } else {\n const templateIdx = await selectPrompt('What kind of app?', TEMPLATES);\n template = TEMPLATE_KEYS[templateIdx >= 0 ? templateIdx : 0];\n }\n\n // ── Theme selection ──\n const themes = getBuiltinThemeNames();\n let theme: string;\n if (args.theme) {\n const themeIdx = themes.indexOf(args.theme);\n theme = themes[themeIdx >= 0 ? themeIdx : 0];\n } else if (nonInteractive) {\n theme = themes[0] || 'default';\n } else {\n const themeIdx = await selectPrompt('Choose a theme', themes.map(t => t.charAt(0).toUpperCase() + t.slice(1)));\n theme = themes[themeIdx >= 0 ? themeIdx : 0];\n }\n\n // ── Feature selection ──\n const featureDefaults = [false, template === 'dashboard', true]; // Router off, Data on for dashboard, HotReload on\n const featureFlags = nonInteractive\n ? featureDefaults\n : await multiSelectPrompt('Features to include', FEATURES, featureDefaults);\n\n const config: ProjectConfig = {\n name: projectName,\n template,\n theme,\n features: {\n router: featureFlags[0],\n dataProviders: featureFlags[1],\n hotReload: featureFlags[2],\n },\n };\n\n // ── Generate project ──\n const projectDir = resolve(process.cwd(), projectName);\n if (existsSync(projectDir)) {\n console.log(`\\n ⚠ Directory \"${projectName}\" already exists. Files may be overwritten.\\n`);\n }\n\n console.log(`\\n Creating ${projectName}...`);\n\n const files = generateProject(config);\n\n for (const file of files) {\n const fullPath = join(projectDir, file.path);\n const dir = dirname(fullPath);\n\n mkdirSync(dir, { recursive: true });\n writeFileSync(fullPath, file.content, 'utf-8');\n\n console.log(` ✓ ${file.path}`);\n }\n\n console.log();\n console.log(' ┌──────────────────────────────────┐');\n console.log(' │ ✅ Project created successfully! │');\n console.log(' └──────────────────────────────────┘');\n console.log();\n console.log(` Next steps:`);\n console.log(` cd ${projectName}`);\n console.log(` bun install`);\n console.log(` bun run dev`);\n console.log();\n}\n\nif (process.argv[1] === fileURLToPath(import.meta.url)) {\n runCli(process.argv.slice(2)).catch(err => {\n console.error('Error:', err.message);\n process.exit(1);\n });\n}\n\n\n","// ─────────────────────────────────────────────────────\n// Minimal interactive prompts (no external deps)\n// ─────────────────────────────────────────────────────\n\nimport { createInterface } from 'node:readline';\n\nconst rl = () => createInterface({ input: process.stdin, output: process.stdout });\n\nexport async function textPrompt(question: string, defaultValue?: string): Promise<string> {\n return new Promise(resolve => {\n const r = rl();\n const suffix = defaultValue ? ` (${defaultValue})` : '';\n r.question(` ${question}${suffix}: `, answer => {\n r.close();\n resolve(answer.trim() || defaultValue || '');\n });\n });\n}\n\nexport async function selectPrompt(question: string, options: string[]): Promise<number> {\n console.log(`\\n ${question}`);\n for (let i = 0; i < options.length; i++) {\n console.log(` ${i + 1}) ${options[i]}`);\n }\n const answer = await textPrompt('Enter number', '1');\n const idx = parseInt(answer) - 1;\n return Math.max(0, Math.min(idx, options.length - 1));\n}\n\nexport async function confirmPrompt(question: string, defaultValue = true): Promise<boolean> {\n const suffix = defaultValue ? '(Y/n)' : '(y/N)';\n const answer = await textPrompt(`${question} ${suffix}`);\n if (!answer) return defaultValue;\n return answer.toLowerCase().startsWith('y');\n}\n\nexport async function multiSelectPrompt(question: string, options: string[], defaults: boolean[] = []): Promise<boolean[]> {\n console.log(`\\n ${question} (comma-separated numbers, or 'all')`);\n for (let i = 0; i < options.length; i++) {\n const def = defaults[i] ? '✓' : ' ';\n console.log(` ${i + 1}) [${def}] ${options[i]}`);\n }\n const answer = await textPrompt('Enter numbers', defaults.some(d => d) ? 'keep defaults' : 'all');\n if (answer === 'all') return options.map(() => true);\n if (answer === 'keep defaults' || answer === '') return defaults.length ? defaults : options.map(() => true);\n const selected = answer.split(',').map(s => parseInt(s.trim()) - 1);\n return options.map((_, i) => selected.includes(i));\n}\n","// ─────────────────────────────────────────────────────\n// Project Templates — generates files for new apps\n// ─────────────────────────────────────────────────────\n\nimport { getBuiltinTheme } from '@termuijs/tss';\nimport { readdirSync, readFileSync } from 'node:fs';\nimport { dirname, join, relative, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst TEMPLATES_ROOT = resolve(__dirname, '../templates');\n\nexport interface ProjectConfig {\n name: string;\n template:\n | 'empty'\n | 'dashboard'\n | 'interactive-tool'\n | 'cli-wrapper'\n | 'cli-tool'\n | 'file-manager'\n | 'ai-assistant'\n | 'form-wizard';\n theme: string;\n features: {\n router: boolean;\n dataProviders: boolean;\n hotReload: boolean;\n };\n}\n\nexport interface GeneratedFile {\n path: string;\n content: string;\n}\n\nexport function generateProject(config: ProjectConfig): GeneratedFile[] {\n const files: GeneratedFile[] = [];\n\n // ── package.json ──\n files.push({\n path: 'package.json',\n content: createPackageJson(config),\n });\n\n // ── tsconfig.json ──\n files.push({\n path: 'tsconfig.json',\n content: JSON.stringify({\n compilerOptions: {\n target: 'ES2022',\n module: 'ESNext',\n moduleResolution: 'bundler',\n jsx: 'react-jsx',\n jsxImportSource: '@termuijs/jsx',\n strict: true,\n esModuleInterop: true,\n outDir: 'dist',\n rootDir: 'src',\n },\n include: ['src'],\n }, null, 2) + '\\n',\n });\n\n // ── termui.config.ts ──\n files.push({\n path: 'termui.config.ts',\n content: `import { defineConfig } from '@termuijs/core';\n\nexport default defineConfig({\n theme: '${config.theme}',\n ${config.features.hotReload ? \"hotReload: true,\" : ''}\n ${config.features.router ? \"router: { dir: './screens' },\" : ''}\n});\n`,\n });\n\n // ── Theme file ──\n const themeSrc = getBuiltinTheme(config.theme);\n if (themeSrc) {\n files.push({ path: `themes/${config.theme}.tss`, content: themeSrc.trim() + '\\n' });\n }\n\n // ── Template-specific files ──\n switch (config.template) {\n case 'dashboard':\n files.push(...generateDashboardTemplate(config));\n break;\n case 'interactive-tool':\n files.push(...generateInteractiveTemplate(config));\n break;\n case 'cli-wrapper':\n files.push(...generateCliWrapperTemplate(config));\n break;\n case 'cli-tool':\n files.push(...generateCliToolTemplate(config));\n break;\n\n case 'ai-assistant':\n files.push(...generateAiAssistantTemplate(config));\n break;\n\n case 'file-manager':\n files.push(...generateFileManagerTemplate(config));\n break;\n case 'form-wizard':\n files.push(...generateFormWizardTemplate(config));\n break;\n\n default:\n files.push(...generateEmptyTemplate(config));\n }\n\n return files;\n}\n\nfunction createPackageJson(config: ProjectConfig): string {\n const isFileManager = config.template === 'file-manager';\n const isAiAssistant = config.template === 'ai-assistant';\n return JSON.stringify({\n name: config.name,\n version: '0.1.0',\n private: true,\n type: 'module',\n scripts: {\n dev: 'bun --watch src/index.tsx',\n build: 'tsup src/index.tsx --format esm',\n start: 'bun dist/index.js',\n },\n dependencies: isAiAssistant\n ? {\n '@termuijs/core': 'latest',\n '@termuijs/widgets': 'latest',\n '@termuijs/ui': 'latest',\n '@termuijs/jsx': 'latest',\n '@termuijs/tss': 'latest',\n }\n : isFileManager\n ? {\n '@termuijs/core': 'latest',\n '@termuijs/widgets': 'latest',\n '@termuijs/ui': 'latest',\n '@termuijs/jsx': 'latest',\n '@termuijs/tss': 'latest',\n }\n : {\n '@termuijs/core': 'latest',\n '@termuijs/widgets': 'latest',\n '@termuijs/ui': 'latest',\n '@termuijs/jsx': 'latest',\n '@termuijs/tss': 'latest',\n '@termuijs/quick': 'latest',\n '@termuijs/motion': 'latest',\n ...(config.features.dataProviders ? { '@termuijs/data': 'latest' } : {}),\n ...(config.features.router ? { '@termuijs/router': 'latest' } : {}),\n },\n devDependencies: {\n '@types/bun': 'latest',\n tsup: '^8.0.0',\n typescript: '^5.3.0',\n },\n engines: {\n bun: '>=1.3.0',\n },\n }, null, 2) + '\\n';\n}\n\nfunction generateFormWizardTemplate(\n config: ProjectConfig\n): GeneratedFile[] {\n return [\n {\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState } from '@termuijs/jsx';\nimport { Wizard } from '@termuijs/ui';\nimport { TextInput, Spinner } from '@termuijs/widgets';\n\nfunction App() {\n const [name, setName] = useState('');\n const [theme, setTheme] = useState('');\n const [submitting, setSubmitting] = useState(false);\n\n const handleComplete = async () => {\n setSubmitting(true);\n\n const data = {\n name,\n theme,\n };\n\n console.log(JSON.stringify(data, null, 2));\n\n setTimeout(() => {\n setSubmitting(false);\n }, 1000);\n };\n\n return (\n <box flexDirection=\"column\" padding={1}>\n <text bold>Form Wizard</text>\n\n <Wizard\n steps={['Info', 'Preferences', 'Confirm']}\n onComplete={handleComplete}\n >\n <box flexDirection=\"column\">\n <text>Name</text>\n <TextInput\n value={name}\n onChange={setName}\n />\n </box>\n\n <box flexDirection=\"column\">\n <text>Theme</text>\n <TextInput\n value={theme}\n onChange={setTheme}\n />\n </box>\n\n <box flexDirection=\"column\">\n <text>Confirm Details</text>\n <text>Name: {name}</text>\n <text>Theme: {theme}</text>\n </box>\n </Wizard>\n\n {submitting && <Spinner />}\n </box>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n },\n ];\n}\n\nfunction generateEmptyTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState, useKeymap, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider } from '@termuijs/tss';\n\nfunction App() {\n const [count, setCount] = useState(0);\n\n useKeymap([\n { key: 'q', action: () => process.exit(0), description: 'Quit' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n { key: '+', action: () => setCount(c => c + 1), description: 'Increment' },\n { key: '-', action: () => setCount(c => Math.max(0, c - 1)), description: 'Decrement' },\n ]);\n\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => <text color=\"red\">{err.message}</text>}>\n <box border=\"single\" padding={1}>\n <text bold>Welcome to ${config.name}!</text>\n <text>Edit src/index.tsx to get started.</text>\n <text>Count: {count} (+/- to change, q to quit)</text>\n </box>\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\nfunction generateDashboardTemplate(config: ProjectConfig): GeneratedFile[] {\n return loadTemplateFiles('dashboard', config);\n}\n\nfunction loadTemplateFiles(templateName: string, config: ProjectConfig): GeneratedFile[] {\n const templatePath = resolve(TEMPLATES_ROOT, templateName);\n return walkTemplateDirectory(templatePath, templatePath, config);\n}\n\nfunction walkTemplateDirectory(rootPath: string, currentPath: string, config: ProjectConfig): GeneratedFile[] {\n const entries = readdirSync(currentPath, { withFileTypes: true });\n const files: GeneratedFile[] = [];\n\n for (const entry of entries) {\n const entryPath = join(currentPath, entry.name);\n if (entry.isDirectory()) {\n files.push(...walkTemplateDirectory(rootPath, entryPath, config));\n continue;\n }\n\n if (entry.name === 'package.json') {\n continue;\n }\n\n const relativePath = relative(rootPath, entryPath).replace(/\\\\/g, '/');\n const content = replaceTemplatePlaceholders(readFileSync(entryPath, 'utf8'), config);\n files.push({ path: relativePath, content });\n }\n\n return files;\n}\n\nfunction replaceTemplatePlaceholders(content: string, config: ProjectConfig) {\n return content.replace(/{{name}}/g, config.name);\n}\n\nfunction generateInteractiveTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState, useKeymap, useRef, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider, useTheme } from '@termuijs/tss';\nimport { caps } from '@termuijs/core';\n\n// ASCII-safe symbols\nconst CHECK = caps.unicode ? '✓' : 'v';\nconst BULLET = caps.unicode ? '›' : '>';\nconst SEP = caps.unicode ? '─'.repeat(40) : '-'.repeat(40);\n\nconst INITIAL_ITEMS = ['Option A', 'Option B', 'Option C'];\n\nfunction InteractiveTool() {\n const [items, setItems] = useState<string[]>(INITIAL_ITEMS);\n const [selected, setSelected] = useState(0);\n const [input, setInput] = useState('');\n const [done, setDone] = useState<string[]>([]);\n const theme = useTheme();\n\n useKeymap([\n { key: 'q', action: () => process.exit(0), description: 'Quit' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n { key: 'up', action: () => setSelected(s => Math.max(0, s - 1)), description: 'Move up' },\n { key: 'down', action: () => setSelected(s => Math.min(items.length - 1, s + 1)), description: 'Move down' },\n { key: 'k', action: () => setSelected(s => Math.max(0, s - 1)), description: 'Move up (vim)' },\n { key: 'j', action: () => setSelected(s => Math.min(items.length - 1, s + 1)), description: 'Move down (vim)' },\n { key: 'enter', action: () => {\n const item = items[selected];\n if (item) setDone(d => d.includes(item) ? d.filter(x => x !== item) : [...d, item]);\n }, description: 'Toggle selected' },\n { key: 'Backspace', action: () => setInput(v => v.slice(0, -1)), description: 'Delete char' },\n { key: 'n', action: () => {\n if (input.trim()) {\n setItems(prev => [...prev, input.trim()]);\n setInput('');\n }\n }, description: 'Add new item' },\n ]);\n\n return (\n <box flexDirection=\"column\" padding={1}>\n <text bold color={theme.colors.primary}>${config.name}</text>\n <text color={theme.colors.muted}>j/k or arrows: navigate | Enter: toggle | n: add | q: quit</text>\n <text>{SEP}</text>\n\n <box flexDirection=\"column\">\n {items.map((item, i) => (\n <row key={item} gap={1}>\n <text color={i === selected ? theme.colors.primary : undefined}>\n {i === selected ? BULLET : ' '}\n </text>\n <text color={done.includes(item) ? theme.colors.success : undefined}>\n {done.includes(item) ? CHECK + ' ' : ' '}{item}\n </text>\n </row>\n ))}\n </box>\n\n <text>{SEP}</text>\n <row gap={1}>\n <text color={theme.colors.muted}>New item:</text>\n <text>{input}_</text>\n </row>\n <text color={theme.colors.muted} dim>Type letters then press n to add</text>\n </box>\n );\n}\n\nfunction App() {\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => (\n <box border=\"single\" borderColor=\"red\" padding={1}>\n <text color=\"red\" bold>Error</text>\n <text>{err.message}</text>\n </box>\n )}>\n <InteractiveTool />\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\nfunction generateCliToolTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useKeymap } from '@termuijs/jsx';\nfunction App() {\n useKeymap([{ key: 'q', action: () => process.exit(0), description: 'Quit' }]);\n return (\n <box flexDirection=\"column\">\n <text bold>${config.name}</text>\n <text dim>Press q to quit</text>\n </box>\n );\n}\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\n\nfunction generateCliWrapperTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState, useEffect, useKeymap, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider, useTheme } from '@termuijs/tss';\nimport { caps } from '@termuijs/core';\nimport { spawn } from 'node:child_process';\n\n// ASCII-safe symbols for terminals without full unicode support\nconst ICON_RUN = caps.unicode ? '>' : '>';\nconst ICON_DONE = caps.unicode ? '*' : '*';\nconst ICON_ERR = caps.unicode ? '!' : '!';\nconst SEP = '-'.repeat(60);\n\ntype LogLevel = 'info' | 'debug' | 'error' | 'warn';\ninterface LogLine {\n level: LogLevel;\n text: string;\n ts: number;\n}\n\nfunction levelColor(level: LogLevel): string {\n switch (level) {\n case 'info': return 'green';\n case 'debug': return 'cyan';\n case 'warn': return 'yellow';\n case 'error': return 'red';\n }\n}\n\nfunction CliWrapper() {\n const [logs, setLogs] = useState<LogLine[]>([\n { level: 'info', text: 'Application started', ts: Date.now() },\n { level: 'debug', text: 'Press r to re-run, q to quit', ts: Date.now() },\n ]);\n const [running, setRunning] = useState(false);\n const [exitCode, setExitCode] = useState<number | null>(null);\n const procRef = useRef<ReturnType<typeof spawn> | null>(null);\n const theme = useTheme();\n\n const addLog = (level: LogLevel, text: string) =>\n setLogs(prev => [...prev.slice(-200), { level, text, ts: Date.now() }]);\n\n // Example: run 'echo hello' — replace with your real command\n const runCommand = () => {\n if (running) return;\n setRunning(true);\n setExitCode(null);\n addLog('info', \\`\\${ICON_RUN} Running command...\\`);\n\n const proc = spawn('echo', ['Hello from CLI wrapper!']);\n procRef.current = proc;\n proc.stdout.on('data', (d: Buffer) => {\n for (const line of d.toString().split('\\\\n').filter(Boolean)) {\n addLog('info', line);\n }\n });\n proc.stderr.on('data', (d: Buffer) => {\n for (const line of d.toString().split('\\\\n').filter(Boolean)) {\n addLog('error', line);\n }\n });\n proc.on('close', (code: number | null) => {\n setRunning(false);\n setExitCode(code);\n addLog(code === 0 ? 'info' : 'error',\n \\`\\${code === 0 ? ICON_DONE : ICON_ERR} Process exited with code \\${code ?? 'null'}\\`);\n procRef.current = null;\n });\n };\n\n // Auto-run on mount, kill process on unmount\n useEffect(() => {\n runCommand();\n return () => {\n if (procRef.current) {\n procRef.current.kill();\n procRef.current = null;\n }\n };\n }, []);\n\n useKeymap([\n { key: 'q', action: () => process.exit(0), description: 'Quit' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n { key: 'r', action: runCommand, description: 'Re-run command' },\n { key: 'l', action: () => setLogs([]), description: 'Clear logs' },\n ]);\n\n return (\n <box flexDirection=\"column\" padding={1}>\n <row gap={2}>\n <text bold color={theme.colors.primary}>${config.name}</text>\n <text color={running ? theme.colors.warning : theme.colors.muted}>\n {running ? 'Running...' : exitCode === null ? 'Ready' : \\`Exit: \\${exitCode}\\`}\n </text>\n <spacer />\n <text color={theme.colors.muted}>r: re-run | l: clear | q: quit</text>\n </row>\n <text>{SEP}</text>\n\n <box flexDirection=\"column\" flexGrow={1}>\n {logs.map((line, i) => (\n <row key={i} gap={1}>\n <text color={theme.colors.muted} dim>\n {new Date(line.ts).toLocaleTimeString()}\n </text>\n <text color={levelColor(line.level)} bold>\n {line.level.toUpperCase().padEnd(5)}\n </text>\n <text>{line.text}</text>\n </row>\n ))}\n </box>\n\n {!caps.color && (\n <text color=\"yellow\" dim>\n Note: running in a terminal without color support (TERM={process.env.TERM ?? 'unset'})\n </text>\n )}\n </box>\n );\n}\n\nfunction App() {\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => (\n <box border=\"single\" borderColor=\"red\" padding={1}>\n <text color=\"red\" bold>CLI Wrapper Error</text>\n <text>{err.message}</text>\n <text color=\"yellow\">Check that the command exists and is executable.</text>\n </box>\n )}>\n <CliWrapper />\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\nfunction generateFileManagerTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport { render, useEffect, useKeymap, useMemo, useRef, useState, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider } from '@termuijs/tss';\nimport { AppShell, FilePicker } from '@termuijs/ui';\nimport { Box, DiffView, Tree, Text, type DiffLine, type TreeNode } from '@termuijs/widgets';\n\ntype Pane = 'tree' | 'picker' | 'preview';\n\nfunction escapeText(value: string): string {\n return value.replace(/\\r/g, '');\n}\n\nfunction readPreview(filePath: string): DiffLine[] {\n try {\n const content = fs.readFileSync(filePath, 'utf8');\n const lines = escapeText(content).split('\\n');\n return lines.map((line, index) => ({ type: 'context' as const, content: line || ' ', lineNo: index + 1 }));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return [{ type: 'context', content: message }];\n }\n}\n\nfunction findFirstPreviewPath(rootPath: string): string {\n try {\n const entries = fs.readdirSync(rootPath, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isDirectory() || entry.name.startsWith('.')) continue;\n return path.join(rootPath, entry.name);\n }\n } catch {\n // Fall back to the current directory path when reading fails.\n }\n return rootPath;\n}\n\nfunction buildTree(rootPath: string, depth = 0, maxDepth = 3): TreeNode[] {\n const entries: TreeNode[] = [];\n\n if (depth === 0) {\n entries.push({ label: path.basename(rootPath) || rootPath, expanded: true, children: buildTree(rootPath, depth + 1, maxDepth) });\n return entries;\n }\n\n if (depth > maxDepth) {\n return entries;\n }\n\n try {\n const dirents = fs.readdirSync(rootPath, { withFileTypes: true });\n const sorted = [...dirents].sort((left, right) => left.name.localeCompare(right.name));\n\n for (const entry of sorted) {\n if (entry.name.startsWith('.')) continue;\n const fullPath = path.join(rootPath, entry.name);\n if (entry.isDirectory()) {\n entries.push({\n label: entry.name,\n expanded: depth < 2,\n data: { path: fullPath, type: 'directory' },\n children: buildTree(fullPath, depth + 1, maxDepth),\n });\n } else {\n entries.push({\n label: entry.name,\n data: { path: fullPath, type: 'file' },\n });\n }\n }\n } catch (error) {\n entries.push({\n label: error instanceof Error ? error.message : String(error),\n data: { path: rootPath, type: 'error' },\n });\n }\n\n return entries;\n}\n\nfunction setPaneFocus(widget: Box, focused: boolean): void {\n widget.setStyle({\n borderColor: focused ? { type: 'named', name: 'cyan' } : { type: 'named', name: 'brightBlack' },\n });\n}\n\nfunction FileManager() {\n const initialPath = process.cwd();\n const [cwd, setCwd] = useState(initialPath);\n const [previewPath, setPreviewPath] = useState(findFirstPreviewPath(initialPath));\n const [focusedPane, setFocusedPane] = useState<Pane>('picker');\n\n const tree = useRef<Tree>(new Tree({\n nodes: buildTree(initialPath),\n onSelect: (node) => {\n const payload = node.data as { path?: string; type?: string } | undefined;\n if (!payload?.path) return;\n if (payload.type === 'file') {\n setPreviewPath(payload.path);\n }\n },\n }, { flexGrow: 1 }));\n\n const filePicker = useRef(new FilePicker({\n startPath: initialPath,\n onSelect: (selectedPath: string) => {\n setPreviewPath(selectedPath);\n },\n onCancel: () => process.exit(0),\n }));\n const preview = useRef(new DiffView({ lines: readPreview(findFirstPreviewPath(initialPath)) }, { flexGrow: 1 }));\n\n const header = useMemo(() => new Box({\n flexDirection: 'row',\n border: 'single',\n padding: 1,\n }), []);\n\n const footer = useMemo(() => new Box({\n flexDirection: 'row',\n border: 'single',\n padding: 1,\n }), []);\n\n const leftPane = useMemo(() => new Box({\n flexDirection: 'column',\n border: 'single',\n padding: 1,\n flexGrow: 1,\n }), []);\n\n const centerPane = useMemo(() => new Box({\n flexDirection: 'column',\n border: 'single',\n padding: 1,\n flexGrow: 1,\n }), []);\n\n const rightPane = useMemo(() => new Box({\n flexDirection: 'column',\n border: 'single',\n padding: 1,\n flexGrow: 1,\n }), []);\n\n const mainArea = useMemo(() => new Box({\n flexDirection: 'row',\n gap: 1,\n flexGrow: 1,\n }), []);\n\n const shell = useMemo(() => new AppShell({\n header,\n footer,\n sidebar: leftPane,\n main: mainArea,\n sidebarWidth: 32,\n }), [footer, header, leftPane, mainArea]);\n\n useEffect(() => {\n header.clearChildren();\n header.addChild(new Text('Path: ' + cwd));\n header.addChild(new Text('Focus: ' + focusedPane));\n }, [cwd, focusedPane, header]);\n\n useEffect(() => {\n footer.clearChildren();\n footer.addChild(new Text('Tab / Shift+Tab: switch panes'));\n footer.addChild(new Text('Enter: open item | q: quit'));\n }, [footer]);\n\n useEffect(() => {\n tree.current.setNodes(buildTree(cwd));\n }, [cwd]);\n\n useEffect(() => {\n preview.current.setLines(readPreview(previewPath));\n }, [previewPath]);\n\n useEffect(() => {\n setPaneFocus(leftPane, focusedPane === 'tree');\n setPaneFocus(centerPane, focusedPane === 'picker');\n setPaneFocus(rightPane, focusedPane === 'preview');\n }, [centerPane, focusedPane, leftPane, rightPane]);\n\n useEffect(() => {\n leftPane.clearChildren();\n leftPane.addChild(tree.current);\n\n centerPane.clearChildren();\n centerPane.addChild(filePicker.current);\n\n rightPane.clearChildren();\n rightPane.addChild(preview.current);\n\n mainArea.clearChildren();\n mainArea.addChild(centerPane);\n mainArea.addChild(rightPane);\n\n setPaneFocus(leftPane, focusedPane === 'tree');\n setPaneFocus(centerPane, focusedPane === 'picker');\n setPaneFocus(rightPane, focusedPane === 'preview');\n }, [centerPane, focusedPane, leftPane, mainArea, rightPane]);\n\n const cyclePane = (direction: 1 | -1): void => {\n const order: Pane[] = ['tree', 'picker', 'preview'];\n const index = order.indexOf(focusedPane);\n const next = order[(index + direction + order.length) % order.length];\n setFocusedPane(next);\n };\n\n const syncPickerPath = (): void => {\n setCwd(filePicker.current.currentPath);\n const selected = filePicker.current.selectedEntry;\n setPreviewPath(\n selected && !selected.isDir\n ? selected.fullPath\n : findFirstPreviewPath(filePicker.current.currentPath),\n );\n };\n\n useKeymap([\n { key: 'tab', action: () => cyclePane(1), description: 'Next pane' },\n { key: 'tab', shift: true, action: () => cyclePane(-1), description: 'Previous pane' },\n { key: 'enter', action: () => {\n if (focusedPane === 'tree') {\n tree.current.handleKey('enter');\n const selected = tree.current.selectedNode;\n const payload = selected?.data as { path?: string; type?: string } | undefined;\n if (payload?.type === 'file' && payload.path) {\n setPreviewPath(payload.path);\n }\n return;\n }\n\n if (focusedPane === 'picker') {\n filePicker.current.confirm();\n syncPickerPath();\n return;\n }\n\n preview.current.handleKey('enter');\n }, description: 'Open item' },\n { key: 'up', action: () => {\n if (focusedPane === 'tree') tree.current.handleKey('up');\n else if (focusedPane === 'picker') filePicker.current.selectPrev();\n else preview.current.handleKey('up');\n }, description: 'Move up' },\n { key: 'down', action: () => {\n if (focusedPane === 'tree') tree.current.handleKey('down');\n else if (focusedPane === 'picker') filePicker.current.selectNext();\n else preview.current.handleKey('down');\n }, description: 'Move down' },\n { key: 'left', action: () => {\n if (focusedPane === 'tree') tree.current.handleKey('left');\n else if (focusedPane === 'picker') {\n filePicker.current.goUp();\n syncPickerPath();\n }\n }, description: 'Collapse or go up' },\n { key: 'right', action: () => {\n if (focusedPane === 'tree') tree.current.handleKey('right');\n else if (focusedPane === 'picker') {\n filePicker.current.confirm();\n syncPickerPath();\n }\n }, description: 'Expand or open' },\n { key: 'backspace', action: () => {\n if (focusedPane === 'picker') {\n filePicker.current.goUp();\n syncPickerPath();\n }\n }, description: 'Parent directory' },\n { key: 'q', action: () => process.exit(0), description: 'Quit' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n ]);\n\n return shell;\n}\n\nfunction App() {\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => (\n <box border=\"single\" borderColor=\"red\" padding={1}>\n <text color=\"red\" bold>File Manager Error</text>\n <text>{err.message}</text>\n </box>\n )}>\n <FileManager />\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n\n\n\nfunction generateAiAssistantTemplate(config: ProjectConfig): GeneratedFile[] {\n return [{\n path: 'src/index.tsx',\n content: `/** @jsxImportSource @termuijs/jsx */\nimport { render, useState, useKeymap, useEffect, ErrorBoundary } from '@termuijs/jsx';\nimport { AutoThemeProvider, useTheme } from '@termuijs/tss';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\ninterface Message { role: 'user' | 'assistant'; content: string; }\ninterface TokenUsageData { inputTokens: number; outputTokens: number; }\n\n// ── Mock adapter (works without ANTHROPIC_API_KEY) ────────────────────────────\n\nconst MOCK_REPLIES = [\n 'Hello! Running in mock mode. Set ANTHROPIC_API_KEY for real Claude.',\n 'Mock mode active — your message was received!',\n 'No API key needed in mock mode. Real Claude would answer here.',\n];\n\nasync function* mockStream(_prompt: string): AsyncGenerator<string> {\n const reply = MOCK_REPLIES[Math.floor(Math.random() * MOCK_REPLIES.length)];\n for (const ch of reply) {\n yield ch;\n await new Promise(r => setTimeout(r, 20));\n }\n}\n\nasync function* claudeStream(\n messages: Message[],\n onUsage: (u: TokenUsageData) => void,\n): AsyncGenerator<string> {\n const res = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'x-api-key': process.env.ANTHROPIC_API_KEY ?? '',\n 'anthropic-version': '2023-06-01',\n },\n body: JSON.stringify({\n model: 'claude-3-5-haiku-20241022',\n max_tokens: 1024,\n stream: true,\n messages: messages.map(m => ({ role: m.role, content: m.content })),\n }),\n });\n if (!res.ok) throw new Error('Claude API ' + res.status + ': ' + res.statusText);\n const reader = res.body!.getReader();\n const dec = new TextDecoder();\n let buf = '';\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buf += dec.decode(value, { stream: true });\n const lines = buf.split('\\\\n');\n buf = lines.pop() ?? '';\n for (const line of lines) {\n if (!line.startsWith('data: ')) continue;\n const raw = line.slice(6).trim();\n if (raw === '[DONE]') return;\n try {\n const ev = JSON.parse(raw);\n if (ev.type === 'content_block_delta' && ev.delta?.type === 'text_delta') yield ev.delta.text as string;\n if (ev.type === 'message_delta' && ev.usage) onUsage({ inputTokens: ev.usage.input_tokens ?? 0, outputTokens: ev.usage.output_tokens ?? 0 });\n } catch { /* skip */ }\n }\n }\n}\n\n// ── Components ────────────────────────────────────────────────────────────────\n\nconst IS_MOCK = !process.env.ANTHROPIC_API_KEY;\n\nfunction AiAssistant() {\n const theme = useTheme();\n const [messages, setMessages] = useState<Message[]>([{\n role: 'assistant',\n content: IS_MOCK\n ? 'Hi! Running in mock mode (no ANTHROPIC_API_KEY). Type and press Enter!'\n : 'Hi! I am Claude. How can I help you?',\n }]);\n const [input, setInput] = useState('');\n const [streaming, setStreaming] = useState('');\n const [busy, setBusy] = useState(false);\n const [usage, setUsage] = useState<TokenUsageData>({ inputTokens: 0, outputTokens: 0 });\n\n const send = async () => {\n const text = input.trim();\n if (!text || busy) return;\n const next: Message[] = [...messages, { role: 'user', content: text }];\n setMessages(next);\n setInput('');\n setBusy(true);\n setStreaming('');\n try {\n let full = '';\n const src = IS_MOCK ? mockStream(text) : claudeStream(next, setUsage);\n for await (const chunk of src) { full += chunk; setStreaming(full); }\n setMessages(m => [...m, { role: 'assistant', content: full }]);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n setMessages(m => [...m, { role: 'assistant', content: 'Error: ' + msg }]);\n } finally { setStreaming(''); setBusy(false); }\n };\n\n useKeymap([\n { key: 'enter', action: () => { void send(); }, description: 'Send' },\n { key: 'backspace', action: () => setInput(v => v.slice(0, -1)), description: 'Delete' },\n { key: 'c', ctrl: true, action: () => process.exit(0), description: 'Quit' },\n ...(' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-_()').split('').map(ch => ({\n key: ch, action: () => { if (!busy) setInput(v => v + ch); }, description: '',\n })),\n ]);\n\n return (\n <box flexDirection=\"column\" flexGrow={1} padding={1}>\n <box border=\"single\" padding={1} flexDirection=\"row\">\n <text bold>AI Assistant</text>\n <text> {IS_MOCK ? '[mock mode]' : '[claude-3-5-haiku]'}</text>\n <text color={theme.colors.muted}> in:{usage.inputTokens} out:{usage.outputTokens}</text>\n </box>\n\n <box flexDirection=\"column\" flexGrow={1} padding={1}>\n {messages.map((m, i) => (\n <box key={i} flexDirection=\"column\" marginBottom={1}>\n <text bold color={m.role === 'user' ? theme.colors.primary : theme.colors.success}>\n {m.role === 'user' ? 'You' : 'Claude'}\n </text>\n <text>{m.content}</text>\n </box>\n ))}\n {streaming.length > 0 && (\n <box flexDirection=\"column\">\n <text bold color={theme.colors.success}>Claude</text>\n <text>{streaming}█</text>\n </box>\n )}\n </box>\n\n <box border=\"single\" padding={1}>\n <text color={theme.colors.muted}>&gt; </text>\n <text>{input}{busy ? '' : '█'}</text>\n {busy && <text color={theme.colors.muted}> thinking...</text>}\n </box>\n\n <box padding={1}>\n <text dim>Ctrl+C to quit{IS_MOCK ? ' | Set ANTHROPIC_API_KEY for real Claude' : ''}</text>\n </box>\n </box>\n );\n}\n\nfunction App() {\n return (\n <AutoThemeProvider>\n <ErrorBoundary fallback={(err) => (\n <box border=\"single\" borderColor=\"red\" padding={1}>\n <text color=\"red\" bold>Error</text>\n <text>{err.message}</text>\n </box>\n )}>\n <AiAssistant />\n </ErrorBoundary>\n </AutoThemeProvider>\n );\n}\n\nrender(<App />, { title: '${config.name}' });\n`,\n }];\n}\n","export interface CliArgs {\n name?: string;\n template?: string;\n theme?: string;\n yes: boolean;\n dir?: string;\n\n command?: string;\n component?: string;\n dryRun?: boolean;\n}\n\nconst TEMPLATE_KEYS = [\n \"empty\",\n \"dashboard\",\n \"interactive-tool\",\n \"cli-wrapper\",\n \"cli-tool\",\n \"file-manager\",\n \"ai-assistant\",\n \"form-wizard\",\n] as const;\n\nfunction getValue(\n argv: string[],\n key: string\n): string | undefined {\n const index = argv.findIndex(a => a === key || a.startsWith(`${key}=`));\n\n if (index === -1) return undefined;\n\n const value = argv[index];\n\n if (value.includes(\"=\")) {\n return value.split(\"=\")[1];\n }\n\n return argv[index + 1];\n}\n\nexport function parseArgs(argv: string[]): CliArgs {\n const args: CliArgs = {\n yes: false,\n dryRun: false,\n };\n\n if (argv[0] === \"add\") {\n const positional: string[] = [];\n\n for (let index = 1; index < argv.length; index++) {\n const value = argv[index];\n\n if (value === \"--dir\") {\n index++;\n continue;\n }\n\n if (!value.startsWith(\"-\")) {\n positional.push(value);\n }\n }\n\n args.command = \"add\";\n args.component = positional[0];\n args.dryRun = argv.includes(\"--dry-run\");\n args.yes = argv.includes(\"--yes\");\n\n const dirValue = getValue(argv, \"--dir\");\n if (dirValue) {\n args.dir = dirValue;\n }\n\n return args;\n }\n\n // positional (first non-flag)\n const positional = argv.find(a => !a.startsWith(\"-\"));\n if (positional) {\n args.name = positional;\n }\n\n if (argv.includes(\"--yes\")) {\n args.yes = true;\n }\n\n const template = getValue(argv, \"--template\");\n if (template) {\n if (!TEMPLATE_KEYS.includes(template as any)) {\n throw new Error(\n `Invalid template \"${template}\". Valid: ${TEMPLATE_KEYS.join(\", \")}`\n );\n }\n args.template = template;\n }\n\n const theme = getValue(argv, \"--theme\");\n if (theme) {\n args.theme = theme;\n }\n\n if (args.yes) {\n args.name = args.name ?? \"my-termui-app\";\n args.template = args.template ?? \"empty\";\n args.theme = args.theme ?? \"default\";\n }\n\n return args;\n}\n\nexport function isNonInteractive(args: CliArgs): boolean {\n return args.yes === true || (!!args.name && !!args.template && !!args.theme);\n}\n","import { mkdirSync, writeFileSync, existsSync } from \"node:fs\";\nimport { dirname, join, resolve, relative, isAbsolute } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\nimport { confirmPrompt } from \"../prompts.js\";\n\nconst REGISTRY_BASE_URL =\n process.env.TERMUI_REGISTRY_URL ?? \"https://termui.io\";\n\nexport interface AddCommandOptions {\n component: string;\n dir?: string;\n dryRun?: boolean;\n yes?: boolean;\n}\n\ninterface RegistryComponent {\n name: string;\n category?: string;\n description?: string;\n files: string[];\n deps?: string[];\n peerDeps?: string[];\n version?: string;\n}\n\ninterface RegistrySchema {\n components: RegistryComponent[];\n}\n\nexport async function runAddCommand(options: AddCommandOptions): Promise<void> {\n const componentName = options.component.trim();\n if (!componentName) {\n throw new Error(\"Component name is required.\");\n }\n\n const schema = await fetchRegistrySchema();\n const componentEntry = findComponentEntry(schema, componentName);\n\n if (!componentEntry) {\n printAvailableComponents(schema);\n throw new Error(`Component \"${componentName}\" not found in registry.`);\n }\n\n const outputRoot = resolve(process.cwd(), options.dir ?? \"src/components\");\n const destinationRoot = join(outputRoot, componentEntry.name);\n const fileEntries = await downloadComponentFiles(componentEntry);\n\n if (options.dryRun) {\n printDryRunPreview(destinationRoot, fileEntries);\n return;\n }\n\n if (existsSync(destinationRoot) && !options.yes) {\n const overwrite = await confirmPrompt(\n `Component directory already exists at ${destinationRoot}. Overwrite?`,\n false,\n );\n\n if (!overwrite) {\n throw new Error(\"Aborted by user.\");\n }\n }\n\n writeComponentFiles(destinationRoot, fileEntries, componentEntry.name);\n await installPackages(componentEntry);\n\n console.log();\n console.log(\" ┌─────────────────────────────────┐\");\n console.log(` │ ✅ ${componentEntry.name} added successfully! │`);\n console.log(\" └─────────────────────────────────┘\");\n console.log();\n console.log(\" Import it with:\");\n console.log(\n ` import { ${pascalCase(componentEntry.name)} } from './components/${componentEntry.name}';`,\n );\n}\n\nasync function fetchRegistrySchema(): Promise<RegistrySchema> {\n const url = `${REGISTRY_BASE_URL}/registry/schema.json`;\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch registry schema from ${url}: ${response.status} ${response.statusText}`,\n );\n }\n\n return await response.json();\n}\n\nfunction findComponentEntry(\n schema: RegistrySchema,\n componentName: string,\n): RegistryComponent | undefined {\n const normalized = componentName.toLowerCase();\n return schema.components.find(\n (entry) => entry.name.toLowerCase() === normalized,\n );\n}\n\nfunction printAvailableComponents(schema: RegistrySchema): void {\n const names = schema.components\n .map((entry) => entry.name)\n .sort((a, b) => a.localeCompare(b));\n\n console.log(\"Available registry components:\");\n for (const name of names) {\n console.log(` - ${name}`);\n }\n}\n\nasync function downloadComponentFiles(\n entry: RegistryComponent,\n): Promise<Array<{ path: string; content: string }>> {\n const downloads = entry.files.map(async (filePath) => {\n const rawUrl = `${REGISTRY_BASE_URL}/${filePath}`;\n const response = await fetch(rawUrl);\n\n if (!response.ok) {\n throw new Error(\n `Failed to download ${filePath} from registry: ${response.status} ${response.statusText}`,\n );\n }\n\n return {\n path: filePath,\n content: await response.text(),\n };\n });\n\n return await Promise.all(downloads);\n}\n\nfunction printDryRunPreview(\n destinationRoot: string,\n fileEntries: Array<{ path: string; content: string }>,\n): void {\n console.log(\"Dry run preview — no files will be written.\");\n console.log();\n\n for (const file of fileEntries) {\n const relative = getDestinationRelativePath(file.path, destinationRoot);\n console.log(` Would create: ${relative}`);\n const preview = file.content\n .split(\"\\n\")\n .slice(0, 6)\n .map((line) => ` ${line}`)\n .join(\"\\n\");\n console.log(preview);\n if (file.content.split(\"\\n\").length > 6) {\n console.log(\" ...\");\n }\n console.log();\n }\n}\n\nfunction writeComponentFiles(\n destinationRoot: string,\n fileEntries: Array<{ path: string; content: string }>,\n componentName: string,\n): void {\n for (const file of fileEntries) {\n const destPath = resolveDestinationPath(\n destinationRoot,\n file.path,\n componentName,\n );\n const directory = dirname(destPath);\n mkdirSync(directory, { recursive: true });\n writeFileSync(destPath, file.content, \"utf-8\");\n console.log(` ✓ ${destPath}`);\n }\n}\n\nfunction resolveDestinationPath(\n destinationRoot: string,\n registryFilePath: string,\n componentName: string,\n): string {\n const prefix = `registry/components/${componentName}/`;\n const relativePath = registryFilePath.startsWith(prefix)\n ? registryFilePath.slice(prefix.length)\n : registryFilePath;\n const destination = resolve(destinationRoot, relativePath);\n\n const rel = relative(resolve(destinationRoot), destination);\n if (rel.startsWith(\"..\") || isAbsolute(rel)) {\n throw new Error(`Destination ${destination} is outside project root`);\n }\n\n return destination;\n}\n\nfunction getDestinationRelativePath(\n registryFilePath: string,\n destinationRoot: string,\n): string {\n const pathSegments = registryFilePath.split(\"/\");\n const componentIndex = pathSegments.indexOf(\"components\");\n if (componentIndex !== -1 && componentIndex + 2 < pathSegments.length) {\n const relativePath = pathSegments.slice(componentIndex + 2).join(\"/\");\n return join(destinationRoot, relativePath);\n }\n\n return join(destinationRoot, registryFilePath);\n}\n\nasync function installPackages(entry: RegistryComponent): Promise<void> {\n const deps = [\n ...new Set([...(entry.deps ?? []), ...(entry.peerDeps ?? [])]),\n ];\n if (deps.length === 0) {\n return;\n }\n\n execFileSync(\"bun\", [\"add\", ...deps], {\n stdio: \"inherit\",\n });\n}\n\nfunction pascalCase(value: string): string {\n return value\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)\n .join(\"\");\n}\n","// ─────────────────────────────────────────────────────\n// Project name validation to prevent path traversal attacks\n// ─────────────────────────────────────────────────────\n\nimport { resolve, relative, isAbsolute, sep } from \"node:path\";\n\n// Regex allows: lowercase letters, numbers, hyphens, underscores\n// Must start with a lowercase letter or number\nconst VALID_NAME_RE = /^[a-z0-9][a-z0-9_-]*$/;\n\n/**\n * Validates a project name for security and npm compliance.\n * Prevents path traversal, absolute paths, and invalid characters.\n *\n * @param name - The project name to validate\n * @returns The validated name, or throws an error\n * @throws Error if name is invalid\n */\nexport function validateProjectName(name: unknown): string {\n // Check if name has the right type first\n if (typeof name !== \"string\") {\n throw new Error(\"Project name is required\");\n }\n\n const trimmed = name.trim();\n\n // Check if empty after trim\n if (trimmed.length === 0) {\n throw new Error(\"Project name cannot be empty\");\n }\n\n // Reject absolute paths (Unix and Windows)\n if (trimmed.startsWith(\"/\") || trimmed.startsWith(\"\\\\\")) {\n throw new Error(\n \"Project name cannot be an absolute path\"\n );\n }\n\n // Reject path traversal sequences and separators\n if (trimmed.includes(\"..\") || trimmed.includes(\"/\") || trimmed.includes(\"\\\\\")) {\n throw new Error(\n \"Project name cannot contain path separators or traversal sequences (/, \\\\, ..)\"\n );\n }\n\n // Reject names that don't match the npm-safe pattern\n if (!VALID_NAME_RE.test(trimmed)) {\n throw new Error(\n \"Project name must contain only lowercase letters, numbers, hyphens, and underscores, and start with a letter or number\"\n );\n }\n\n return trimmed;\n}\n\n/**\n * Ensures the resolved project path remains within the current working directory.\n * This is a defensive check to prevent any path traversal bypasses.\n *\n * @param cwd - Current working directory\n * @param projectName - The validated project name\n * @throws Error if path traversal is detected\n */\nexport function validateResolvedPath(cwd: string, projectName: string): void {\n const resolved = resolve(cwd, projectName);\n const cwdNorm = resolve(cwd);\n\n const rel = relative(cwdNorm, resolved);\n\n // If relative path starts with '..' or is absolute, it escapes cwd\n if (rel === \"..\" || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {\n throw new Error(\n \"Security check failed: resolved path escapes current working directory\"\n );\n }\n}\n"],"mappings":"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","names":["dirname","resolve","join","fileURLToPath","mkdirSync","writeFileSync","existsSync","resolve","positional","dirname","join","resolve","relative","resolve","join","relative","dirname","resolve","relative","isAbsolute","TEMPLATE_KEYS","resolve","existsSync","join","dirname","mkdirSync","writeFileSync","fileURLToPath"]}
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "https://github.com/Karanjot786/TermUI"
7
7
  },
8
- "version": "0.1.4",
8
+ "version": "0.1.6",
9
9
  "description": "Scaffold a new TermUI project with templates, themes, and dev server",
10
10
  "type": "module",
11
11
  "bin": {
@@ -13,10 +13,15 @@
13
13
  },
14
14
  "main": "./dist/index.js",
15
15
  "files": [
16
- "dist"
16
+ "dist",
17
+ "templates"
17
18
  ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch"
22
+ },
18
23
  "dependencies": {
19
- "@termuijs/tss": "0.1.4"
24
+ "@termuijs/tss": "0.1.5"
20
25
  },
21
26
  "devDependencies": {
22
27
  "tsup": "^8.0.0",
@@ -30,9 +35,8 @@
30
35
  "create-app",
31
36
  "generator"
32
37
  ],
33
- "license": "MIT",
34
- "scripts": {
35
- "build": "tsup",
36
- "dev": "tsup --watch"
37
- }
38
- }
38
+ "engines": {
39
+ "bun": ">=1.3.0"
40
+ },
41
+ "license": "MIT"
42
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "ai-assistant",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --watch src/index.tsx",
8
+ "build": "tsup src/index.tsx --format esm",
9
+ "start": "bun dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "@termuijs/core": "latest",
13
+ "@termuijs/widgets": "latest",
14
+ "@termuijs/adapters": "latest",
15
+ "@termuijs/ui": "latest",
16
+ "@termuijs/jsx": "latest",
17
+ "@termuijs/tss": "latest"
18
+ },
19
+ "devDependencies": {
20
+ "@types/bun": "latest",
21
+ "tsup": "^8.0.0",
22
+ "typescript": "^5.3.0"
23
+ },
24
+ "engines": {
25
+ "bun": ">=1.3.0"
26
+ }
27
+ }
@@ -0,0 +1,254 @@
1
+ import { App } from '@termuijs/core';
2
+ import { Widget, Box, Text, ChatMessage, StreamingText, ScrollView, TextInput } from '@termuijs/widgets';
3
+ import type { Screen, KeyEvent } from '@termuijs/core';
4
+ import { useAI, type AIMessage } from '@termuijs/adapters';
5
+
6
+ // ──────────────────────────────────────────────────────────────────────────────
7
+ // AI Assistant Template - Minimal Version
8
+ // ──────────────────────────────────────────────────────────────────────────────
9
+ //
10
+ // A simple starter template demonstrating:
11
+ // - ChatMessage widget for conversation display
12
+ // - StreamingText widget for streamed responses
13
+ // - useAI for Claude API integration
14
+ // - Dual-mode: mock (no API key) and real (with API key)
15
+
16
+ const IS_MOCK = !process.env.ANTHROPIC_API_KEY;
17
+
18
+ const MOCK_REPLY = 'Hello! This is a mock response. Set ANTHROPIC_API_KEY to use real Claude.';
19
+
20
+ async function* mockStream(): AsyncGenerator<string> {
21
+ for (const ch of MOCK_REPLY) {
22
+ yield ch;
23
+ await new Promise(r => setTimeout(r, 20));
24
+ }
25
+ }
26
+
27
+ class AIAssistantApp extends Widget {
28
+ private chatContainer: Box;
29
+ private streamingTextWidget: StreamingText | null = null;
30
+ private textInput: TextInput;
31
+ private isStreaming = false;
32
+ private isMockMode = IS_MOCK;
33
+ private aiAdapter: ReturnType<typeof useAI> | null = null;
34
+
35
+ constructor() {
36
+ super({
37
+ flexDirection: 'column',
38
+ flexGrow: 1,
39
+ padding: 1,
40
+ gap: 1,
41
+ });
42
+
43
+ if (!IS_MOCK) {
44
+ try {
45
+ this.aiAdapter = useAI('anthropic', {
46
+ apiKey: process.env.ANTHROPIC_API_KEY!,
47
+ });
48
+ } catch (e) {
49
+ console.error('Failed to initialize AI adapter:', e);
50
+ this.isMockMode = true;
51
+ this.aiAdapter = null;
52
+ }
53
+ }
54
+
55
+ // Header
56
+ const headerBox = new Box({
57
+ flexDirection: 'row',
58
+ height: 1,
59
+ gap: 1,
60
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
61
+ border: 'single',
62
+ borderColor: { type: 'named' as const, name: 'brightBlack' as const },
63
+ });
64
+
65
+ const titleText = new Text('AI Assistant', {
66
+ bold: true,
67
+ fg: { type: 'named' as const, name: 'cyan' as const },
68
+ });
69
+
70
+ const modeLabel = new Text(this.isMockMode ? '[mock mode]' : '[claude]', {
71
+ dim: true,
72
+ });
73
+
74
+ headerBox.addChild(titleText);
75
+ headerBox.addChild(modeLabel);
76
+
77
+ // Messages scroll view
78
+ const messagesScroll = new ScrollView(
79
+ {
80
+ flexGrow: 1,
81
+ border: 'single',
82
+ borderColor: { type: 'named' as const, name: 'brightBlack' as const },
83
+ },
84
+ { showScrollbar: true }
85
+ );
86
+
87
+ this.chatContainer = new Box({
88
+ flexDirection: 'column',
89
+ gap: 1,
90
+ });
91
+
92
+ messagesScroll.addChild(this.chatContainer);
93
+
94
+ const initialMessage = new ChatMessage(
95
+ {
96
+ role: 'assistant',
97
+ content: this.isMockMode
98
+ ? 'Hi! Running in mock mode. Set ANTHROPIC_API_KEY to use real Claude.'
99
+ : 'Hi! I am Claude. How can I help you?',
100
+ timestamp: new Date(),
101
+ },
102
+ { height: 3 }
103
+ );
104
+ this.chatContainer.addChild(initialMessage);
105
+
106
+ // Input area
107
+ const inputBox = new Box({
108
+ flexDirection: 'row',
109
+ height: 3,
110
+ gap: 1,
111
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
112
+ border: 'single',
113
+ borderColor: { type: 'named' as const, name: 'brightBlack' as const },
114
+ });
115
+
116
+ const inputLabel = new Text('> ', {
117
+ fg: { type: 'named' as const, name: 'green' as const },
118
+ bold: true,
119
+ });
120
+
121
+ this.textInput = new TextInput(
122
+ { flexGrow: 1 },
123
+ {
124
+ placeholder: 'Type a message...',
125
+ onSubmit: (val) => this.handleSendMessage(val),
126
+ }
127
+ );
128
+
129
+ inputBox.addChild(inputLabel);
130
+ inputBox.addChild(this.textInput);
131
+
132
+ const helpText = new Text(' [Enter] Send | [Ctrl+C] Quit ', {
133
+ dim: true,
134
+ height: 1,
135
+ });
136
+
137
+ this.addChild(headerBox);
138
+ this.addChild(messagesScroll);
139
+ this.addChild(inputBox);
140
+ this.addChild(helpText);
141
+
142
+ this.textInput.isFocused = true;
143
+ }
144
+
145
+ private removeStreamingTextWidget(): void {
146
+ if (!this.streamingTextWidget) return;
147
+ this.chatContainer.removeChild(this.streamingTextWidget);
148
+ this.streamingTextWidget = null;
149
+ }
150
+
151
+ private async handleSendMessage(userText: string): Promise<void> {
152
+ if (!userText.trim() || this.isStreaming) return;
153
+
154
+ this.isStreaming = true;
155
+ this.textInput.isFocused = false;
156
+
157
+ const userMessage = new ChatMessage(
158
+ { role: 'user', content: userText, timestamp: new Date() },
159
+ { height: 3 }
160
+ );
161
+ this.chatContainer.addChild(userMessage);
162
+
163
+ try {
164
+ let fullResponse = '';
165
+
166
+ const streamingTextWidget = new StreamingText(
167
+ { text: '', speed: 1 },
168
+ { border: 'single', height: 5 }
169
+ );
170
+ this.streamingTextWidget = streamingTextWidget;
171
+ this.chatContainer.addChild(streamingTextWidget);
172
+
173
+ if (this.isMockMode || !this.aiAdapter) {
174
+ const stream = mockStream();
175
+ for await (const token of stream) {
176
+ fullResponse += token;
177
+ streamingTextWidget.setText(fullResponse);
178
+ streamingTextWidget.tick();
179
+ this.markDirty();
180
+ }
181
+ } else {
182
+ const aiMessages: AIMessage[] = [{ role: 'user', content: userText }];
183
+ for await (const token of this.aiAdapter.chat(aiMessages)) {
184
+ fullResponse += token;
185
+ streamingTextWidget.setText(fullResponse);
186
+ streamingTextWidget.tick();
187
+ this.markDirty();
188
+ }
189
+ }
190
+
191
+ this.removeStreamingTextWidget();
192
+ const assistantMessage = new ChatMessage(
193
+ { role: 'assistant', content: fullResponse, timestamp: new Date() },
194
+ { height: 5 }
195
+ );
196
+ this.chatContainer.addChild(assistantMessage);
197
+ } catch (e) {
198
+ this.removeStreamingTextWidget();
199
+ const errorMsg = e instanceof Error ? e.message : String(e);
200
+ const errorMessage = new ChatMessage(
201
+ { role: 'assistant', content: `Error: ${errorMsg}`, timestamp: new Date() },
202
+ { height: 3 }
203
+ );
204
+ this.chatContainer.addChild(errorMessage);
205
+ } finally {
206
+ this.removeStreamingTextWidget();
207
+ this.isStreaming = false;
208
+ this.textInput.isFocused = true;
209
+ this.markDirty();
210
+ }
211
+ }
212
+
213
+ handleKey(event: KeyEvent): boolean {
214
+ if (event.key === 'q' || (event.ctrl && event.key === 'c')) {
215
+ return false;
216
+ }
217
+
218
+ if (event.key === 'enter' || event.key === 'return') {
219
+ this.textInput.submit();
220
+ return true;
221
+ }
222
+
223
+ if (event.key === 'backspace') {
224
+ this.textInput.deleteBack();
225
+ return true;
226
+ }
227
+
228
+ if (event.key && event.key.length === 1 && !event.ctrl && !event.alt) {
229
+ this.textInput.insertChar(event.key);
230
+ return true;
231
+ }
232
+
233
+ return true;
234
+ }
235
+
236
+ protected _renderSelf(_screen: Screen): void {}
237
+ }
238
+
239
+ async function main() {
240
+ const root = new AIAssistantApp();
241
+ const app = new App(root, {
242
+ fullscreen: true,
243
+ title: 'AI Assistant',
244
+ fps: 30,
245
+ });
246
+ const exitCode = await app.mount();
247
+ process.exit(exitCode);
248
+ }
249
+
250
+ main().catch((err) => {
251
+ console.error(err);
252
+ process.exit(1);
253
+ });
254
+
@@ -0,0 +1,16 @@
1
+ # {{name}} Dashboard Template
2
+
3
+ A dashboard starter app for TermUI with:
4
+
5
+ - `AppShell` root layout
6
+ - `Tabs` for Overview and Processes
7
+ - 4 metric cards: CPU, Memory, Disk, Processes
8
+ - `LineChart` request history
9
+ - `BarChart` error/process metrics
10
+ - Automatic refresh interval sample data
11
+ - Optional `@termuijs/data` hooks when available
12
+
13
+ ## Run
14
+
15
+ bun install
16
+ bun run dev
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "dashboard",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --watch src/index.tsx",
8
+ "build": "tsup src/index.tsx --format esm",
9
+ "start": "bun dist/index.js"
10
+ },
11
+ "dependencies": {
12
+ "@termuijs/core": "latest",
13
+ "@termuijs/widgets": "latest",
14
+ "@termuijs/ui": "latest",
15
+ "@termuijs/jsx": "latest",
16
+ "@termuijs/tss": "latest"
17
+ },
18
+ "devDependencies": {
19
+ "@types/bun": "latest",
20
+ "tsup": "^8.0.0",
21
+ "typescript": "^5.3.0"
22
+ },
23
+ "engines": {
24
+ "bun": ">=1.3.0"
25
+ }
26
+ }