@voltx/cli 0.3.9 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1032 @@
1
+ import {
2
+ printWelcomeBanner
3
+ } from "./chunk-IV352HZA.mjs";
4
+
5
+ // src/create.ts
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ var VV = {
9
+ "@voltx/core": "^0.3.3",
10
+ "@voltx/server": "^0.3.7",
11
+ "@voltx/cli": "^0.3.9",
12
+ "@voltx/ai": "^0.3.1",
13
+ "@voltx/agents": "^0.3.2",
14
+ "@voltx/memory": "^0.3.1",
15
+ "@voltx/db": "^0.3.1",
16
+ "@voltx/rag": "^0.3.2",
17
+ "@voltx/auth": "^0.3.1"
18
+ };
19
+ function v(pkg) {
20
+ return VV[pkg] ?? "^0.3.0";
21
+ }
22
+ var TEMPLATE_DEPS = {
23
+ blank: { "@voltx/core": v("@voltx/core"), "@voltx/server": v("@voltx/server") },
24
+ chatbot: { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/memory": v("@voltx/memory") },
25
+ "rag-app": { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/rag": v("@voltx/rag"), "@voltx/db": v("@voltx/db") },
26
+ "agent-app": { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/agents": v("@voltx/agents"), "@voltx/memory": v("@voltx/memory") }
27
+ };
28
+ async function createProject(options) {
29
+ const { name, template = "blank", auth = "none", shadcn = false } = options;
30
+ const targetDir = path.resolve(process.cwd(), name);
31
+ if (fs.existsSync(targetDir)) {
32
+ console.error(`[voltx] Directory "${name}" already exists.`);
33
+ process.exit(1);
34
+ }
35
+ fs.mkdirSync(targetDir, { recursive: true });
36
+ const provider = template === "rag-app" ? "openai" : "cerebras";
37
+ const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
38
+ const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
39
+ const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": v("@voltx/cli") };
40
+ if (auth === "better-auth") {
41
+ deps["@voltx/auth"] = v("@voltx/auth");
42
+ deps["better-auth"] = "^1.5.0";
43
+ } else if (auth === "jwt") {
44
+ deps["@voltx/auth"] = v("@voltx/auth");
45
+ deps["jose"] = "^6.0.0";
46
+ }
47
+ const devDeps = { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" };
48
+ deps["hono"] = "^4.7.0";
49
+ deps["@hono/node-server"] = "^1.14.0";
50
+ deps["react"] = "^19.0.0";
51
+ deps["react-dom"] = "^19.0.0";
52
+ deps["tailwindcss"] = "^4.0.0";
53
+ devDeps["vite"] = "^6.0.0";
54
+ devDeps["@hono/vite-dev-server"] = "^0.7.0";
55
+ devDeps["@vitejs/plugin-react"] = "^4.3.0";
56
+ devDeps["@tailwindcss/vite"] = "^4.0.0";
57
+ devDeps["@types/react"] = "^19.0.0";
58
+ devDeps["@types/react-dom"] = "^19.0.0";
59
+ if (shadcn) {
60
+ deps["class-variance-authority"] = "^0.7.0";
61
+ deps["clsx"] = "^2.1.0";
62
+ deps["tailwind-merge"] = "^3.0.0";
63
+ deps["lucide-react"] = "^0.468.0";
64
+ }
65
+ devDeps["@voltx/cli"] = deps["@voltx/cli"] ?? v("@voltx/cli");
66
+ delete deps["@voltx/cli"];
67
+ fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
68
+ name,
69
+ version: "0.1.0",
70
+ private: true,
71
+ scripts: { dev: "npx voltx dev", build: "npx voltx build", start: "npx voltx start" },
72
+ dependencies: deps,
73
+ devDependencies: devDeps
74
+ }, null, 2));
75
+ let config = `import { defineConfig } from "@voltx/core";
76
+
77
+ export default defineConfig({
78
+ name: "${name}",
79
+ port: 3000,
80
+ ai: {
81
+ provider: "${provider}",
82
+ model: "${model}",
83
+ },`;
84
+ if (hasDb) config += `
85
+ db: {
86
+ url: process.env.DATABASE_URL,
87
+ },`;
88
+ if (auth !== "none") config += `
89
+ auth: {
90
+ provider: "${auth}",
91
+ },`;
92
+ config += `
93
+ server: {
94
+ routesDir: "api",
95
+ staticDir: "public",
96
+ cors: true,
97
+ },
98
+ });
99
+ `;
100
+ fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
101
+ fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
102
+ fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
103
+ fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
104
+ fs.mkdirSync(path.join(targetDir, "src", "components"), { recursive: true });
105
+ fs.mkdirSync(path.join(targetDir, "src", "hooks"), { recursive: true });
106
+ fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
107
+ fs.writeFileSync(path.join(targetDir, "public", "favicon.svg"), generateFaviconSVG());
108
+ fs.writeFileSync(path.join(targetDir, "public", "robots.txt"), generateRobotsTxt());
109
+ fs.writeFileSync(path.join(targetDir, "public", "site.webmanifest"), generateWebManifest(name));
110
+ const tsconfig = {
111
+ compilerOptions: {
112
+ target: "ES2022",
113
+ module: "ESNext",
114
+ moduleResolution: "bundler",
115
+ strict: true,
116
+ esModuleInterop: true,
117
+ skipLibCheck: true,
118
+ outDir: "dist",
119
+ baseUrl: ".",
120
+ paths: { "@/*": ["./src/*"] },
121
+ jsx: "react-jsx"
122
+ },
123
+ include: ["src", "api", "server.ts", "voltx.config.ts"]
124
+ };
125
+ if (template === "agent-app") tsconfig.include.push("agents", "tools");
126
+ fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
127
+ fs.writeFileSync(path.join(targetDir, "server.ts"), generateServerEntry(name, template));
128
+ fs.writeFileSync(path.join(targetDir, "vite.config.ts"), generateViteConfigFile("server.ts"));
129
+ fs.writeFileSync(path.join(targetDir, "src", "entry-client.tsx"), generateEntryClient());
130
+ fs.writeFileSync(path.join(targetDir, "src", "entry-server.tsx"), generateEntryServer());
131
+ fs.writeFileSync(path.join(targetDir, "src", "layout.tsx"), generateLayoutComponent(name));
132
+ fs.writeFileSync(path.join(targetDir, "src", "globals.css"), generateGlobalCSS(shadcn));
133
+ if (shadcn) {
134
+ fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.ts"), generateCnUtil());
135
+ fs.writeFileSync(path.join(targetDir, "components.json"), generateComponentsJson());
136
+ }
137
+ fs.writeFileSync(path.join(targetDir, "src", "app.tsx"), generateAppComponent(name, template));
138
+ fs.writeFileSync(
139
+ path.join(targetDir, "api", "index.ts"),
140
+ `// GET /api \u2014 Health check
141
+ import type { Context } from "@voltx/server";
142
+
143
+ export function GET(c: Context) {
144
+ return c.json({ name: "${name}", status: "ok" });
145
+ }
146
+ `
147
+ );
148
+ if (template === "chatbot" || template === "agent-app") {
149
+ fs.writeFileSync(
150
+ path.join(targetDir, "api", "chat.ts"),
151
+ `// POST /api/chat \u2014 Streaming chat with conversation memory
152
+ import type { Context } from "@voltx/server";
153
+ import { streamText } from "@voltx/ai";
154
+ import { createMemory } from "@voltx/memory";
155
+
156
+ const memory = createMemory({ maxMessages: 50 });
157
+
158
+ export async function POST(c: Context) {
159
+ const { messages, conversationId = "default" } = await c.req.json();
160
+
161
+ const lastMessage = messages[messages.length - 1];
162
+ if (lastMessage?.role === "user") {
163
+ await memory.add(conversationId, { role: "user", content: lastMessage.content });
164
+ }
165
+
166
+ const history = await memory.get(conversationId);
167
+
168
+ const result = await streamText({
169
+ model: "${provider}:${model}",
170
+ system: "You are a helpful AI assistant.",
171
+ messages: history.map((m) => ({ role: m.role, content: m.content })),
172
+ });
173
+
174
+ result.text.then(async (text) => {
175
+ await memory.add(conversationId, { role: "assistant", content: text });
176
+ });
177
+
178
+ return result.toSSEResponse();
179
+ }
180
+ `
181
+ );
182
+ }
183
+ if (template === "agent-app") {
184
+ fs.mkdirSync(path.join(targetDir, "agents"), { recursive: true });
185
+ fs.mkdirSync(path.join(targetDir, "tools"), { recursive: true });
186
+ fs.writeFileSync(path.join(targetDir, "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
187
+ import type { Tool } from "@voltx/agents";
188
+
189
+ export const calculatorTool: Tool = {
190
+ name: "calculator",
191
+ description: "Evaluate a math expression. Supports +, -, *, /, %, parentheses, and Math functions.",
192
+ parameters: {
193
+ type: "object",
194
+ properties: { expression: { type: "string", description: "The math expression to evaluate" } },
195
+ required: ["expression"],
196
+ },
197
+ async execute(args: { expression: string }) {
198
+ try {
199
+ const safe = args.expression.replace(/[^0-9+\\-*/.()%\\s,]|(?<!Math)\\.[a-z]/gi, (match) => {
200
+ if (args.expression.includes("Math.")) return match;
201
+ throw new Error("Invalid character: " + match);
202
+ });
203
+ const result = new Function("return " + safe)();
204
+ return \`\${args.expression} = \${result}\`;
205
+ } catch (err) {
206
+ return \`Error: \${err instanceof Error ? err.message : String(err)}\`;
207
+ }
208
+ },
209
+ };
210
+ `);
211
+ fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
212
+ import type { Tool } from "@voltx/agents";
213
+
214
+ export const datetimeTool: Tool = {
215
+ name: "datetime",
216
+ description: "Get the current date, time, day of week, and timezone.",
217
+ parameters: {
218
+ type: "object",
219
+ properties: { timezone: { type: "string", description: "Optional IANA timezone. Defaults to server timezone." } },
220
+ },
221
+ async execute(args: { timezone?: string }) {
222
+ const now = new Date();
223
+ const tz = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
224
+ const formatted = now.toLocaleString("en-US", {
225
+ timeZone: tz, weekday: "long", year: "numeric", month: "long", day: "numeric",
226
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true,
227
+ });
228
+ return \`Current date/time (\${tz}): \${formatted}\`;
229
+ },
230
+ };
231
+ `);
232
+ fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
233
+ import { createAgent } from "@voltx/agents";
234
+ import { calculatorTool } from "../tools/calculator";
235
+ import { datetimeTool } from "../tools/datetime";
236
+
237
+ export const assistant = createAgent({
238
+ name: "assistant",
239
+ model: "${provider}:${model}",
240
+ instructions: "You are a helpful AI assistant with access to tools: Calculator, Date & Time. Use them when needed to answer questions accurately.",
241
+ tools: [calculatorTool, datetimeTool],
242
+ maxIterations: 5,
243
+ });
244
+ `);
245
+ fs.writeFileSync(
246
+ path.join(targetDir, "api", "agent.ts"),
247
+ `// POST /api/agent \u2014 Run the AI agent
248
+ import type { Context } from "@voltx/server";
249
+ import { assistant } from "../agents/assistant";
250
+
251
+ export async function POST(c: Context) {
252
+ const { input } = await c.req.json();
253
+ if (!input) return c.json({ error: "Missing 'input' field" }, 400);
254
+ const result = await assistant.run(input);
255
+ return c.json({ content: result.content, steps: result.steps });
256
+ }
257
+ `
258
+ );
259
+ }
260
+ if (template === "rag-app") {
261
+ const embedModel = "openai:text-embedding-3-small";
262
+ fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
263
+ fs.writeFileSync(
264
+ path.join(targetDir, "api", "rag", "query.ts"),
265
+ `// POST /api/rag/query \u2014 Query documents with RAG
266
+ import type { Context } from "@voltx/server";
267
+ import { streamText } from "@voltx/ai";
268
+ import { createRAGPipeline, createEmbedder } from "@voltx/rag";
269
+ import { createVectorStore } from "@voltx/db";
270
+
271
+ const vectorStore = createVectorStore();
272
+ const embedder = createEmbedder({ model: "${embedModel}" });
273
+ const rag = createRAGPipeline({ embedder, vectorStore });
274
+
275
+ export async function POST(c: Context) {
276
+ const { question } = await c.req.json();
277
+ const context = await rag.getContext(question, { topK: 5 });
278
+ const result = await streamText({
279
+ model: "${provider}:${model}",
280
+ system: \`Answer based on context. If not relevant, say so.\\n\\nContext:\\n\${context}\`,
281
+ messages: [{ role: "user", content: question }],
282
+ });
283
+ return result.toSSEResponse();
284
+ }
285
+ `
286
+ );
287
+ fs.writeFileSync(
288
+ path.join(targetDir, "api", "rag", "ingest.ts"),
289
+ `// POST /api/rag/ingest \u2014 Ingest documents into the vector store
290
+ import type { Context } from "@voltx/server";
291
+ import { createRAGPipeline, createEmbedder } from "@voltx/rag";
292
+ import { createVectorStore } from "@voltx/db";
293
+
294
+ const vectorStore = createVectorStore();
295
+ const embedder = createEmbedder({ model: "${embedModel}" });
296
+ const rag = createRAGPipeline({ embedder, vectorStore });
297
+
298
+ export async function POST(c: Context) {
299
+ const { text, idPrefix } = await c.req.json();
300
+ if (!text || typeof text !== "string") return c.json({ error: "Missing 'text' field" }, 400);
301
+ const result = await rag.ingest(text, idPrefix ?? "doc");
302
+ return c.json({ status: "ok", chunks: result.chunks, ids: result.ids });
303
+ }
304
+ `
305
+ );
306
+ }
307
+ if (auth === "better-auth") {
308
+ fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
309
+ fs.writeFileSync(
310
+ path.join(targetDir, "api", "auth", "[...path].ts"),
311
+ `// ALL /api/auth/* \u2014 Better Auth handler
312
+ import type { Context } from "@voltx/server";
313
+ import { auth } from "../../src/lib/auth";
314
+ import { createAuthHandler } from "@voltx/auth";
315
+
316
+ const handler = createAuthHandler(auth);
317
+
318
+ export const GET = (c: Context) => handler(c);
319
+ export const POST = (c: Context) => handler(c);
320
+ `
321
+ );
322
+ fs.writeFileSync(
323
+ path.join(targetDir, "src", "lib", "auth.ts"),
324
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
325
+
326
+ export const auth = createAuth("better-auth", {
327
+ database: process.env.DATABASE_URL!,
328
+ emailAndPassword: true,
329
+ });
330
+
331
+ export const authMiddleware = createAuthMiddleware({
332
+ provider: auth,
333
+ publicPaths: ["/api/auth", "/api/health", "/"],
334
+ });
335
+ `
336
+ );
337
+ } else if (auth === "jwt") {
338
+ fs.writeFileSync(
339
+ path.join(targetDir, "src", "lib", "auth.ts"),
340
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
341
+
342
+ export const jwt = createAuth("jwt", {
343
+ secret: process.env.JWT_SECRET!,
344
+ expiresIn: "7d",
345
+ });
346
+
347
+ export const authMiddleware = createAuthMiddleware({
348
+ provider: jwt,
349
+ publicPaths: ["/api/auth", "/api/health", "/"],
350
+ });
351
+ `
352
+ );
353
+ fs.writeFileSync(
354
+ path.join(targetDir, "api", "auth.ts"),
355
+ `import type { Context } from "@voltx/server";
356
+ import { jwt } from "../src/lib/auth";
357
+
358
+ export async function POST(c: Context) {
359
+ const { email, password } = await c.req.json();
360
+ if (!email || !password) return c.json({ error: "Email and password are required" }, 400);
361
+ const token = await jwt.sign({ sub: email, email });
362
+ return c.json({ token });
363
+ }
364
+ `
365
+ );
366
+ }
367
+ let envContent = "";
368
+ if (template === "rag-app") {
369
+ envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nOPENAI_API_KEY=sk-...\n\n";
370
+ envContent += "# \u2500\u2500\u2500 Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
371
+ } else if (template === "chatbot" || template === "agent-app") {
372
+ envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCEREBRAS_API_KEY=csk-...\n\n";
373
+ } else {
374
+ envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# OPENAI_API_KEY=sk-...\n# CEREBRAS_API_KEY=csk-...\n\n";
375
+ }
376
+ if (auth === "better-auth") {
377
+ envContent += "# \u2500\u2500\u2500 Auth (Better Auth) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nBETTER_AUTH_SECRET=your-secret-key-min-32-chars-here\nBETTER_AUTH_URL=http://localhost:3000\n\n";
378
+ } else if (auth === "jwt") {
379
+ envContent += "# \u2500\u2500\u2500 Auth (JWT) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nJWT_SECRET=your-jwt-secret-key\n\n";
380
+ }
381
+ envContent += "# \u2500\u2500\u2500 App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nPORT=3000\nNODE_ENV=development\n";
382
+ fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
383
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
384
+ printWelcomeBanner(name);
385
+ }
386
+ function generateServerEntry(projectName, template) {
387
+ const imports = [];
388
+ const mounts = [];
389
+ imports.push('import { GET as healthGET } from "./api/index";');
390
+ mounts.push('app.get("/api", healthGET);');
391
+ if (template === "chatbot" || template === "agent-app") {
392
+ imports.push('import { POST as chatPOST } from "./api/chat";');
393
+ mounts.push('app.post("/api/chat", chatPOST);');
394
+ }
395
+ if (template === "agent-app") {
396
+ imports.push('import { POST as agentPOST } from "./api/agent";');
397
+ mounts.push('app.post("/api/agent", agentPOST);');
398
+ }
399
+ if (template === "rag-app") {
400
+ imports.push('import { POST as ragQueryPOST } from "./api/rag/query";');
401
+ imports.push('import { POST as ragIngestPOST } from "./api/rag/ingest";');
402
+ mounts.push('app.post("/api/rag/query", ragQueryPOST);');
403
+ mounts.push('app.post("/api/rag/ingest", ragIngestPOST);');
404
+ }
405
+ return `import { Hono } from "hono";
406
+ import { serve } from "@hono/node-server";
407
+ import { serveStatic } from "@hono/node-server/serve-static";
408
+ import { registerSSR } from "@voltx/server";
409
+ import { loadEnv } from "@voltx/core";
410
+ ${imports.join("\n")}
411
+
412
+ loadEnv(process.env.NODE_ENV ?? "development");
413
+
414
+ const isProd = process.env.NODE_ENV === "production";
415
+ const app = new Hono();
416
+
417
+ // \u2500\u2500 API Routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
418
+ ${mounts.join("\n")}
419
+
420
+ // \u2500\u2500 Static assets (production) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
421
+ if (isProd) {
422
+ app.use("/assets/*", serveStatic({ root: "./dist/client/" }));
423
+ app.use("/favicon.svg", serveStatic({ root: "./public/" }));
424
+ app.use("/robots.txt", serveStatic({ root: "./public/" }));
425
+ app.use("/site.webmanifest", serveStatic({ root: "./public/" }));
426
+ }
427
+
428
+ // \u2500\u2500 SSR catch-all \u2014 renders React on the server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
429
+ // Note: Do NOT statically import entry-server.tsx here \u2014 Node.js cannot
430
+ // handle .tsx files natively. In dev, the Vite instance injected by
431
+ // @hono/vite-dev-server will load it via ssrLoadModule. In production,
432
+ // the pre-built SSR bundle at dist/server/entry-server.js is loaded.
433
+ registerSSR(app, null, {
434
+ title: "${projectName}",
435
+ entryClient: "src/entry-client.tsx",
436
+ entryServer: "src/entry-server.tsx",
437
+ });
438
+
439
+ export default app;
440
+
441
+ if (isProd) {
442
+ const port = Number(process.env.PORT) || 3000;
443
+ serve({ fetch: app.fetch, port }, (info) => {
444
+ console.log(\`\\n \u26A1 ${projectName} running at http://localhost:\${info.port}\\n\`);
445
+ });
446
+ }
447
+ `;
448
+ }
449
+ function generateViteConfigFile(entry) {
450
+ return `import { defineConfig } from "vite";
451
+ import devServer from "@hono/vite-dev-server";
452
+ import react from "@vitejs/plugin-react";
453
+ import tailwindcss from "@tailwindcss/vite";
454
+
455
+ export default defineConfig({
456
+ resolve: {
457
+ alias: {
458
+ "@": "/src",
459
+ },
460
+ },
461
+ plugins: [
462
+ react(),
463
+ tailwindcss(),
464
+ devServer({
465
+ entry: "${entry}",
466
+ exclude: [
467
+ /.*\\.tsx?($|\\?)/,
468
+ /.*\\.(s?css|less)($|\\?)/,
469
+ /.*\\.(svg|png|jpg|jpeg|gif|webp|ico)($|\\?)/,
470
+ /^\\/@.+$/,
471
+ /^\\/favicon\\.svg$/,
472
+ /^\\/node_modules\\/.*/,
473
+ /^\\/src\\/.*/,
474
+ ],
475
+ injectClientScript: false,
476
+ }),
477
+ ],
478
+ });
479
+ `;
480
+ }
481
+ function generateEntryClient() {
482
+ return `import React from "react";
483
+ import { hydrateRoot } from "react-dom/client";
484
+ import Layout from "./layout";
485
+ import App from "./app";
486
+ import "./globals.css";
487
+
488
+ hydrateRoot(
489
+ document.getElementById("root")!,
490
+ <React.StrictMode>
491
+ <Layout>
492
+ <App />
493
+ </Layout>
494
+ </React.StrictMode>
495
+ );
496
+ `;
497
+ }
498
+ function generateEntryServer() {
499
+ return `import React from "react";
500
+ import { renderToReadableStream } from "react-dom/server";
501
+ import Layout from "./layout";
502
+ import App from "./app";
503
+
504
+ export async function render(_url: string): Promise<ReadableStream> {
505
+ const stream = await renderToReadableStream(
506
+ <React.StrictMode>
507
+ <Layout>
508
+ <App />
509
+ </Layout>
510
+ </React.StrictMode>,
511
+ {
512
+ onError(error: unknown) {
513
+ console.error("[voltx] SSR render error:", error);
514
+ },
515
+ }
516
+ );
517
+ return stream;
518
+ }
519
+ `;
520
+ }
521
+ function generateLayoutComponent(projectName) {
522
+ return `import React from "react";
523
+
524
+ export default function Layout({ children }: { children: React.ReactNode }) {
525
+ return (
526
+ <div className="min-h-screen bg-background text-foreground font-sans">
527
+ <header className="border-b border-border px-6 py-3 flex items-center justify-between">
528
+ <div className="flex items-center gap-2">
529
+ <span className="text-xl">\u26A1</span>
530
+ <span className="font-semibold">${projectName}</span>
531
+ </div>
532
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-muted text-sm hover:text-foreground transition-colors">
533
+ Built with VoltX
534
+ </a>
535
+ </header>
536
+ <main>{children}</main>
537
+ </div>
538
+ );
539
+ }
540
+ `;
541
+ }
542
+ function generateGlobalCSS(useShadcn = false) {
543
+ if (useShadcn) {
544
+ return `@import "tailwindcss";
545
+
546
+ @theme inline {
547
+ --radius-sm: 0.25rem;
548
+ --radius-md: 0.375rem;
549
+ --radius-lg: 0.5rem;
550
+ --radius-xl: 0.75rem;
551
+ --color-background: hsl(var(--background));
552
+ --color-foreground: hsl(var(--foreground));
553
+ --color-card: hsl(var(--card));
554
+ --color-card-foreground: hsl(var(--card-foreground));
555
+ --color-popover: hsl(var(--popover));
556
+ --color-popover-foreground: hsl(var(--popover-foreground));
557
+ --color-primary: hsl(var(--primary));
558
+ --color-primary-foreground: hsl(var(--primary-foreground));
559
+ --color-secondary: hsl(var(--secondary));
560
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
561
+ --color-muted: hsl(var(--muted));
562
+ --color-muted-foreground: hsl(var(--muted-foreground));
563
+ --color-accent: hsl(var(--accent));
564
+ --color-accent-foreground: hsl(var(--accent-foreground));
565
+ --color-destructive: hsl(var(--destructive));
566
+ --color-border: hsl(var(--border));
567
+ --color-input: hsl(var(--input));
568
+ --color-ring: hsl(var(--ring));
569
+ --color-chart-1: hsl(var(--chart-1));
570
+ --color-chart-2: hsl(var(--chart-2));
571
+ --color-chart-3: hsl(var(--chart-3));
572
+ --color-chart-4: hsl(var(--chart-4));
573
+ --color-chart-5: hsl(var(--chart-5));
574
+ --color-sidebar: hsl(var(--sidebar));
575
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
576
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
577
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
578
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
579
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
580
+ --color-sidebar-border: hsl(var(--sidebar-border));
581
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
582
+ }
583
+
584
+ :root {
585
+ --background: 0 0% 4%;
586
+ --foreground: 0 0% 93%;
587
+ --card: 0 0% 6%;
588
+ --card-foreground: 0 0% 93%;
589
+ --popover: 0 0% 6%;
590
+ --popover-foreground: 0 0% 93%;
591
+ --primary: 0 0% 93%;
592
+ --primary-foreground: 0 0% 6%;
593
+ --secondary: 0 0% 12%;
594
+ --secondary-foreground: 0 0% 93%;
595
+ --muted: 0 0% 12%;
596
+ --muted-foreground: 0 0% 55%;
597
+ --accent: 0 0% 12%;
598
+ --accent-foreground: 0 0% 93%;
599
+ --destructive: 0 62% 50%;
600
+ --border: 0 0% 14%;
601
+ --input: 0 0% 14%;
602
+ --ring: 0 0% 83%;
603
+ --chart-1: 220 70% 50%;
604
+ --chart-2: 160 60% 45%;
605
+ --chart-3: 30 80% 55%;
606
+ --chart-4: 280 65% 60%;
607
+ --chart-5: 340 75% 55%;
608
+ --sidebar: 0 0% 5%;
609
+ --sidebar-foreground: 0 0% 93%;
610
+ --sidebar-primary: 0 0% 93%;
611
+ --sidebar-primary-foreground: 0 0% 6%;
612
+ --sidebar-accent: 0 0% 12%;
613
+ --sidebar-accent-foreground: 0 0% 93%;
614
+ --sidebar-border: 0 0% 14%;
615
+ --sidebar-ring: 0 0% 83%;
616
+ }
617
+
618
+ *,
619
+ *::before,
620
+ *::after {
621
+ box-sizing: border-box;
622
+ margin: 0;
623
+ padding: 0;
624
+ border-color: var(--color-border);
625
+ }
626
+
627
+ html,
628
+ body {
629
+ height: 100%;
630
+ background: var(--color-background);
631
+ color: var(--color-foreground);
632
+ font-family: system-ui, -apple-system, sans-serif;
633
+ -webkit-font-smoothing: antialiased;
634
+ }
635
+
636
+ #root {
637
+ height: 100%;
638
+ }
639
+ `;
640
+ }
641
+ return `@import "tailwindcss";
642
+
643
+ @theme {
644
+ --color-background: #0a0a0a;
645
+ --color-foreground: #ededed;
646
+ --color-muted: #888888;
647
+ --color-border: #222222;
648
+ --color-primary: #2563eb;
649
+ --color-accent: #a78bfa;
650
+ --font-sans: system-ui, -apple-system, sans-serif;
651
+ }
652
+
653
+ *,
654
+ *::before,
655
+ *::after {
656
+ box-sizing: border-box;
657
+ margin: 0;
658
+ padding: 0;
659
+ }
660
+
661
+ html,
662
+ body {
663
+ height: 100%;
664
+ background: var(--color-background);
665
+ color: var(--color-foreground);
666
+ font-family: var(--font-sans);
667
+ -webkit-font-smoothing: antialiased;
668
+ }
669
+
670
+ #root {
671
+ height: 100%;
672
+ }
673
+
674
+ a {
675
+ color: inherit;
676
+ text-decoration: none;
677
+ }
678
+
679
+ a:hover {
680
+ text-decoration: underline;
681
+ }
682
+ `;
683
+ }
684
+ function generateCnUtil() {
685
+ return `import { type ClassValue, clsx } from "clsx";
686
+ import { twMerge } from "tailwind-merge";
687
+
688
+ export function cn(...inputs: ClassValue[]) {
689
+ return twMerge(clsx(inputs));
690
+ }
691
+ `;
692
+ }
693
+ function generateComponentsJson() {
694
+ return JSON.stringify({
695
+ "$schema": "https://ui.shadcn.com/schema.json",
696
+ style: "new-york",
697
+ rsc: false,
698
+ tsx: true,
699
+ tailwind: {
700
+ config: "",
701
+ css: "src/globals.css",
702
+ baseColor: "neutral",
703
+ cssVariables: true
704
+ },
705
+ aliases: {
706
+ components: "@/components",
707
+ utils: "@/lib/utils",
708
+ ui: "@/components/ui",
709
+ lib: "@/lib",
710
+ hooks: "@/hooks"
711
+ }
712
+ }, null, 2) + "\n";
713
+ }
714
+ function generateAppComponent(projectName, template) {
715
+ if (template === "blank") {
716
+ return `import React, { useState, useEffect } from "react";
717
+
718
+ export default function App() {
719
+ const [status, setStatus] = useState<string>("checking...");
720
+
721
+ useEffect(() => {
722
+ fetch("/api")
723
+ .then((res) => res.json())
724
+ .then((data) => setStatus(data.status || "ok"))
725
+ .catch(() => setStatus("error"));
726
+ }, []);
727
+
728
+ return (
729
+ <div className="flex flex-col items-center justify-center min-h-[calc(100vh-60px)] px-6 py-12">
730
+ <div className="text-center max-w-2xl w-full">
731
+ {/* Hero */}
732
+ <div className="relative mb-8">
733
+ <div className="absolute inset-0 blur-3xl opacity-20 bg-gradient-to-r from-blue-500 via-purple-500 to-blue-500 rounded-full" />
734
+ <div className="relative text-7xl mb-4">\u26A1</div>
735
+ </div>
736
+ <h1 className="text-5xl font-bold tracking-tight mb-3 bg-gradient-to-r from-white to-white/60 bg-clip-text text-transparent">
737
+ \${"\${projectName}"}
738
+ </h1>
739
+ <p className="text-muted text-lg mb-10">The AI-first full-stack framework</p>
740
+
741
+ {/* Status cards */}
742
+ <div className="flex gap-4 justify-center mb-10">
743
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
744
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Server</div>
745
+ <div className={\`text-sm font-medium \${status === "ok" ? "text-emerald-400" : "text-red-400"}\`}>
746
+ <span className={\`inline-block w-2 h-2 rounded-full mr-2 \${status === "ok" ? "bg-emerald-400 animate-pulse" : "bg-red-400"}\`} />
747
+ {status}
748
+ </div>
749
+ </div>
750
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
751
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Frontend</div>
752
+ <div className="text-sm font-medium text-emerald-400">
753
+ <span className="inline-block w-2 h-2 rounded-full mr-2 bg-emerald-400 animate-pulse" />
754
+ React + Vite
755
+ </div>
756
+ </div>
757
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
758
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">CSS</div>
759
+ <div className="text-sm font-medium text-sky-400">Tailwind v4</div>
760
+ </div>
761
+ </div>
762
+
763
+ {/* Get started */}
764
+ <div className="bg-white/[0.03] border border-border rounded-2xl p-8 text-left backdrop-blur-sm">
765
+ <h2 className="text-sm font-medium text-muted mb-6 uppercase tracking-wider">Get started</h2>
766
+ <div className="space-y-4">
767
+ <div className="flex items-start gap-4">
768
+ <div className="w-8 h-8 rounded-lg bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400 text-sm shrink-0">1</div>
769
+ <div>
770
+ <code className="text-purple-400 text-sm">src/app.tsx</code>
771
+ <p className="text-muted text-sm mt-1">Edit this file to build your UI</p>
772
+ </div>
773
+ </div>
774
+ <div className="flex items-start gap-4">
775
+ <div className="w-8 h-8 rounded-lg bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 text-sm shrink-0">2</div>
776
+ <div>
777
+ <code className="text-blue-400 text-sm">api/</code>
778
+ <p className="text-muted text-sm mt-1">Add API routes here (file-based routing)</p>
779
+ </div>
780
+ </div>
781
+ <div className="flex items-start gap-4">
782
+ <div className="w-8 h-8 rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400 text-sm shrink-0">3</div>
783
+ <div>
784
+ <code className="text-emerald-400 text-sm">src/components/</code>
785
+ <p className="text-muted text-sm mt-1">Create React components with Tailwind CSS</p>
786
+ </div>
787
+ </div>
788
+ </div>
789
+ </div>
790
+
791
+ {/* Links */}
792
+ <div className="flex gap-6 justify-center mt-8">
793
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
794
+ GitHub \u2192
795
+ </a>
796
+ <a href="https://voltx.co.in" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
797
+ Docs \u2192
798
+ </a>
799
+ </div>
800
+ </div>
801
+ </div>
802
+ );
803
+ }
804
+ `;
805
+ }
806
+ const apiEndpoint = template === "agent-app" ? "/api/agent" : template === "rag-app" ? "/api/rag/query" : "/api/chat";
807
+ const isAgent = template === "agent-app";
808
+ const isRag = template === "rag-app";
809
+ let emptyStateTitle = "Start a conversation";
810
+ let emptyStateHint = "Type a message below to chat with AI";
811
+ let accentColor = "blue";
812
+ let inputPlaceholder = "Type a message...";
813
+ if (isAgent) {
814
+ emptyStateTitle = "Talk to your AI agent";
815
+ emptyStateHint = "The agent can use tools like Calculator and Date/Time";
816
+ accentColor = "purple";
817
+ inputPlaceholder = "Ask the agent anything...";
818
+ } else if (isRag) {
819
+ emptyStateTitle = "Ask your documents";
820
+ emptyStateHint = "Query your knowledge base \u2014 ingest docs via POST /api/rag/ingest";
821
+ accentColor = "emerald";
822
+ inputPlaceholder = "Ask a question about your documents...";
823
+ }
824
+ return `import React, { useState, useRef, useEffect, useCallback } from "react";
825
+
826
+ interface Message {
827
+ id: string;
828
+ role: "user" | "assistant";
829
+ content: string;
830
+ }
831
+
832
+ export default function App() {
833
+ const [messages, setMessages] = useState<Message[]>([]);
834
+ const [input, setInput] = useState("");
835
+ const [isLoading, setIsLoading] = useState(false);
836
+ const messagesEndRef = useRef<HTMLDivElement>(null);
837
+ const inputRef = useRef<HTMLInputElement>(null);
838
+
839
+ useEffect(() => {
840
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
841
+ }, [messages]);
842
+
843
+ useEffect(() => {
844
+ inputRef.current?.focus();
845
+ }, []);
846
+
847
+ const sendMessage = useCallback(async () => {
848
+ const text = input.trim();
849
+ if (!text || isLoading) return;
850
+
851
+ const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: text };
852
+ setMessages((prev) => [...prev, userMsg]);
853
+ setInput("");
854
+ setIsLoading(true);
855
+
856
+ const assistantMsg: Message = { id: crypto.randomUUID(), role: "assistant", content: "" };
857
+ setMessages((prev) => [...prev, assistantMsg]);
858
+
859
+ try {${isAgent ? `
860
+ const res = await fetch("${apiEndpoint}", {
861
+ method: "POST",
862
+ headers: { "Content-Type": "application/json" },
863
+ body: JSON.stringify({ input: text }),
864
+ });
865
+ const data = await res.json();
866
+ setMessages((prev) =>
867
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: data.content || "No response" } : m)
868
+ );` : `
869
+ const res = await fetch("${apiEndpoint}", {
870
+ method: "POST",
871
+ headers: { "Content-Type": "application/json" },
872
+ body: JSON.stringify({${isRag ? ` question: text ` : ` messages: [...messages, { role: "user", content: text }] `}}),
873
+ });
874
+
875
+ if (!res.body) throw new Error("No response body");
876
+
877
+ const reader = res.body.getReader();
878
+ const decoder = new TextDecoder();
879
+ let buffer = "";
880
+ let fullContent = "";
881
+
882
+ while (true) {
883
+ const { done, value } = await reader.read();
884
+ if (done) break;
885
+ buffer += decoder.decode(value, { stream: true });
886
+ const lines = buffer.split("\\n");
887
+ buffer = lines.pop() ?? "";
888
+
889
+ for (const line of lines) {
890
+ if (!line.startsWith("data: ")) continue;
891
+ const data = line.slice(6);
892
+ if (data === "[DONE]") break;
893
+ try {
894
+ const parsed = JSON.parse(data);
895
+ const chunk = parsed.textDelta ?? parsed.content ?? parsed.choices?.[0]?.delta?.content ?? "";
896
+ if (chunk) {
897
+ fullContent += chunk;
898
+ setMessages((prev) =>
899
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: fullContent } : m)
900
+ );
901
+ }
902
+ } catch {}
903
+ }
904
+ }`}
905
+ } catch (err) {
906
+ setMessages((prev) =>
907
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: "Error: " + (err instanceof Error ? err.message : String(err)) } : m)
908
+ );
909
+ } finally {
910
+ setIsLoading(false);
911
+ }
912
+ }, [input, isLoading, messages]);
913
+
914
+ return (
915
+ <div className="h-[calc(100vh-60px)] flex flex-col">
916
+ <main className="flex-1 overflow-y-auto px-4 py-6">
917
+ <div className="max-w-3xl mx-auto">
918
+ {messages.length === 0 && (
919
+ <div className="flex flex-col items-center justify-center h-[60vh] text-center">
920
+ <div className="relative mb-6">
921
+ <div className="absolute inset-0 blur-2xl opacity-20 bg-${accentColor}-500 rounded-full" />
922
+ <div className="relative text-5xl">\u26A1</div>
923
+ </div>
924
+ <h2 className="text-2xl font-semibold mb-2">${emptyStateTitle}</h2>
925
+ <p className="text-muted text-sm max-w-md">${emptyStateHint}</p>
926
+ <div className="flex gap-2 mt-6">
927
+ ${isAgent ? `<button onClick={() => { setInput("What is 42 * 17?"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
928
+ What is 42 \xD7 17?
929
+ </button>
930
+ <button onClick={() => { setInput("What day is it today?"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
931
+ What day is it?
932
+ </button>` : isRag ? `<button onClick={() => { setInput("Summarize the main topics"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
933
+ Summarize main topics
934
+ </button>
935
+ <button onClick={() => { setInput("What are the key findings?"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
936
+ Key findings
937
+ </button>` : `<button onClick={() => { setInput("Hello! What can you do?"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
938
+ Hello! What can you do?
939
+ </button>
940
+ <button onClick={() => { setInput("Tell me a fun fact"); }} className="px-3 py-1.5 text-xs rounded-full bg-white/5 border border-border text-muted hover:text-foreground hover:border-${accentColor}-500/50 transition-all cursor-pointer">
941
+ Tell me a fun fact
942
+ </button>`}
943
+ </div>
944
+ </div>
945
+ )}
946
+ {messages.map((msg) => (
947
+ <div key={msg.id} className={\`mb-6 flex \${msg.role === "user" ? "justify-end" : "justify-start"}\`}>
948
+ <div className={\`flex items-start gap-3 max-w-[80%] \${msg.role === "user" ? "flex-row-reverse" : ""}\`}>
949
+ <div className={\`w-8 h-8 rounded-full flex items-center justify-center text-sm shrink-0 \${
950
+ msg.role === "user" ? "bg-${accentColor}-500 text-white" : "bg-white/10 text-muted"
951
+ }\`}>
952
+ {msg.role === "user" ? "Y" : "\u26A1"}
953
+ </div>
954
+ <div className={\`px-4 py-3 rounded-2xl whitespace-pre-wrap leading-relaxed text-sm \${
955
+ msg.role === "user"
956
+ ? "bg-${accentColor}-500 text-white rounded-br-md"
957
+ : "bg-white/[0.05] border border-border rounded-bl-md"
958
+ }\`}>
959
+ {msg.content || (isLoading && msg.role === "assistant" ? (
960
+ <span className="flex gap-1">
961
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:0ms]" />
962
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:150ms]" />
963
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:300ms]" />
964
+ </span>
965
+ ) : "")}
966
+ </div>
967
+ </div>
968
+ </div>
969
+ ))}
970
+ <div ref={messagesEndRef} />
971
+ </div>
972
+ </main>
973
+ <footer className="border-t border-border px-4 py-4">
974
+ <form onSubmit={(e) => { e.preventDefault(); sendMessage(); }} className="max-w-3xl mx-auto flex gap-3">
975
+ <input
976
+ ref={inputRef}
977
+ value={input}
978
+ onChange={(e) => setInput(e.target.value)}
979
+ placeholder="${inputPlaceholder}"
980
+ disabled={isLoading}
981
+ className="flex-1 px-4 py-3 rounded-xl bg-white/[0.05] border border-border text-foreground placeholder:text-muted/50 outline-none focus:border-${accentColor}-500/50 focus:ring-1 focus:ring-${accentColor}-500/25 transition-all disabled:opacity-50"
982
+ />
983
+ <button
984
+ type="submit"
985
+ disabled={isLoading || !input.trim()}
986
+ className="px-6 py-3 rounded-xl font-medium text-sm transition-all disabled:opacity-30 disabled:cursor-not-allowed bg-${accentColor}-500 hover:bg-${accentColor}-400 text-white cursor-pointer"
987
+ >
988
+ {isLoading ? (
989
+ <svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
990
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
991
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
992
+ </svg>
993
+ ) : "Send"}
994
+ </button>
995
+ </form>
996
+ </footer>
997
+ </div>
998
+ );
999
+ }
1000
+ `;
1001
+ }
1002
+ function generateFaviconSVG() {
1003
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1004
+ <rect width="32" height="32" rx="6" fill="#0a0a0a"/>
1005
+ <path d="M18.5 4L8 18h7l-1.5 10L24 14h-7l1.5-10z" fill="#facc15" stroke="#facc15" stroke-width="0.5" stroke-linejoin="round"/>
1006
+ </svg>
1007
+ `;
1008
+ }
1009
+ function generateRobotsTxt() {
1010
+ return `# https://www.robotstxt.org/robotstxt.html
1011
+ User-agent: *
1012
+ Allow: /
1013
+
1014
+ Sitemap: /sitemap.xml
1015
+ `;
1016
+ }
1017
+ function generateWebManifest(projectName) {
1018
+ return JSON.stringify({
1019
+ name: projectName,
1020
+ short_name: projectName,
1021
+ icons: [
1022
+ { src: "/favicon.svg", sizes: "any", type: "image/svg+xml" }
1023
+ ],
1024
+ theme_color: "#0a0a0a",
1025
+ background_color: "#0a0a0a",
1026
+ display: "standalone"
1027
+ }, null, 2) + "\n";
1028
+ }
1029
+
1030
+ export {
1031
+ createProject
1032
+ };
package/dist/cli.js CHANGED
@@ -1157,15 +1157,15 @@ var init_create = __esm({
1157
1157
  path = __toESM(require("path"));
1158
1158
  init_welcome();
1159
1159
  VV = {
1160
- "@voltx/core": "^0.3.2",
1161
- "@voltx/server": "^0.3.2",
1162
- "@voltx/cli": "^0.3.7",
1163
- "@voltx/ai": "^0.3.0",
1164
- "@voltx/agents": "^0.3.1",
1165
- "@voltx/memory": "^0.3.0",
1166
- "@voltx/db": "^0.3.0",
1167
- "@voltx/rag": "^0.3.1",
1168
- "@voltx/auth": "^0.3.0"
1160
+ "@voltx/core": "^0.3.3",
1161
+ "@voltx/server": "^0.3.7",
1162
+ "@voltx/cli": "^0.3.9",
1163
+ "@voltx/ai": "^0.3.1",
1164
+ "@voltx/agents": "^0.3.2",
1165
+ "@voltx/memory": "^0.3.1",
1166
+ "@voltx/db": "^0.3.1",
1167
+ "@voltx/rag": "^0.3.2",
1168
+ "@voltx/auth": "^0.3.1"
1169
1169
  };
1170
1170
  TEMPLATE_DEPS = {
1171
1171
  blank: { "@voltx/core": v("@voltx/core"), "@voltx/server": v("@voltx/server") },
package/dist/create.js CHANGED
@@ -143,15 +143,15 @@ function printWelcomeBanner(projectName) {
143
143
 
144
144
  // src/create.ts
145
145
  var VV = {
146
- "@voltx/core": "^0.3.2",
147
- "@voltx/server": "^0.3.2",
148
- "@voltx/cli": "^0.3.7",
149
- "@voltx/ai": "^0.3.0",
150
- "@voltx/agents": "^0.3.1",
151
- "@voltx/memory": "^0.3.0",
152
- "@voltx/db": "^0.3.0",
153
- "@voltx/rag": "^0.3.1",
154
- "@voltx/auth": "^0.3.0"
146
+ "@voltx/core": "^0.3.3",
147
+ "@voltx/server": "^0.3.7",
148
+ "@voltx/cli": "^0.3.9",
149
+ "@voltx/ai": "^0.3.1",
150
+ "@voltx/agents": "^0.3.2",
151
+ "@voltx/memory": "^0.3.1",
152
+ "@voltx/db": "^0.3.1",
153
+ "@voltx/rag": "^0.3.2",
154
+ "@voltx/auth": "^0.3.1"
155
155
  };
156
156
  function v(pkg) {
157
157
  return VV[pkg] ?? "^0.3.0";
package/dist/create.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createProject
4
- } from "./chunk-T4ZF62MG.mjs";
4
+ } from "./chunk-X5JEHOO4.mjs";
5
5
  import "./chunk-IV352HZA.mjs";
6
6
  export {
7
7
  createProject
package/dist/index.js CHANGED
@@ -149,15 +149,15 @@ function printWelcomeBanner(projectName) {
149
149
 
150
150
  // src/create.ts
151
151
  var VV = {
152
- "@voltx/core": "^0.3.2",
153
- "@voltx/server": "^0.3.2",
154
- "@voltx/cli": "^0.3.7",
155
- "@voltx/ai": "^0.3.0",
156
- "@voltx/agents": "^0.3.1",
157
- "@voltx/memory": "^0.3.0",
158
- "@voltx/db": "^0.3.0",
159
- "@voltx/rag": "^0.3.1",
160
- "@voltx/auth": "^0.3.0"
152
+ "@voltx/core": "^0.3.3",
153
+ "@voltx/server": "^0.3.7",
154
+ "@voltx/cli": "^0.3.9",
155
+ "@voltx/ai": "^0.3.1",
156
+ "@voltx/agents": "^0.3.2",
157
+ "@voltx/memory": "^0.3.1",
158
+ "@voltx/db": "^0.3.1",
159
+ "@voltx/rag": "^0.3.2",
160
+ "@voltx/auth": "^0.3.1"
161
161
  };
162
162
  function v(pkg) {
163
163
  return VV[pkg] ?? "^0.3.0";
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-H2DTIOEO.mjs";
4
4
  import {
5
5
  createProject
6
- } from "./chunk-T4ZF62MG.mjs";
6
+ } from "./chunk-X5JEHOO4.mjs";
7
7
  import {
8
8
  runDev
9
9
  } from "./chunk-JCDKZPUB.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltx/cli",
3
- "version": "0.3.9",
3
+ "version": "0.4.0",
4
4
  "description": "VoltX CLI — dev server, build, start, generate, and scaffolding",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",