@voltx/cli 0.3.4 → 0.3.5

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.
Files changed (49) hide show
  1. package/README.md +54 -22
  2. package/dist/build.d.mts +4 -3
  3. package/dist/build.d.ts +4 -3
  4. package/dist/build.js +56 -27
  5. package/dist/build.mjs +1 -2
  6. package/dist/chunk-2LHDOMF2.mjs +680 -0
  7. package/dist/chunk-5DVBIJXU.mjs +84 -0
  8. package/dist/chunk-65PVXS4D.mjs +894 -0
  9. package/dist/chunk-7JVIEGSA.mjs +141 -0
  10. package/dist/chunk-A4NA4AJN.mjs +786 -0
  11. package/dist/chunk-AAAHANST.mjs +839 -0
  12. package/dist/chunk-AD3WMFZF.mjs +205 -0
  13. package/dist/chunk-CSSHLVYS.mjs +83 -0
  14. package/dist/chunk-CWOSNV5O.mjs +150 -0
  15. package/dist/chunk-EI6XBYKB.mjs +84 -0
  16. package/dist/chunk-FI2W4L4S.mjs +205 -0
  17. package/dist/chunk-G2INQCCJ.mjs +907 -0
  18. package/dist/chunk-H2DTIOEO.mjs +150 -0
  19. package/dist/chunk-IS2WTE3C.mjs +138 -0
  20. package/dist/chunk-JECCDBYI.mjs +730 -0
  21. package/dist/chunk-KX2MRJUO.mjs +795 -0
  22. package/dist/chunk-LTGMHAZS.mjs +147 -0
  23. package/dist/chunk-OPO6RUFP.mjs +698 -0
  24. package/dist/chunk-PWQSKYAM.mjs +682 -0
  25. package/dist/chunk-Q5XCFN7L.mjs +1026 -0
  26. package/dist/chunk-QSU6FZC7.mjs +497 -0
  27. package/dist/chunk-RYWRFHEC.mjs +83 -0
  28. package/dist/chunk-SU4Q3PTH.mjs +201 -0
  29. package/dist/chunk-UXI3QSDN.mjs +121 -0
  30. package/dist/chunk-VD3CNPNP.mjs +123 -0
  31. package/dist/chunk-X6VOAPRJ.mjs +756 -0
  32. package/dist/cli.js +932 -307
  33. package/dist/cli.mjs +7 -6
  34. package/dist/create.d.mts +1 -0
  35. package/dist/create.d.ts +1 -0
  36. package/dist/create.js +723 -191
  37. package/dist/create.mjs +1 -2
  38. package/dist/dev.d.mts +6 -4
  39. package/dist/dev.d.ts +6 -4
  40. package/dist/dev.js +119 -46
  41. package/dist/dev.mjs +1 -2
  42. package/dist/generate.js +13 -13
  43. package/dist/generate.mjs +1 -2
  44. package/dist/index.js +919 -295
  45. package/dist/index.mjs +5 -6
  46. package/dist/start.js +7 -17
  47. package/dist/start.mjs +1 -2
  48. package/dist/welcome.mjs +0 -1
  49. package/package.json +11 -3
package/dist/create.js CHANGED
@@ -142,15 +142,28 @@ function printWelcomeBanner(projectName) {
142
142
  }
143
143
 
144
144
  // src/create.ts
145
- var V = "^0.3.0";
145
+ var VV = {
146
+ "@voltx/core": "^0.3.1",
147
+ "@voltx/server": "^0.3.1",
148
+ "@voltx/cli": "^0.3.5",
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"
155
+ };
156
+ function v(pkg) {
157
+ return VV[pkg] ?? "^0.3.0";
158
+ }
146
159
  var TEMPLATE_DEPS = {
147
- blank: { "@voltx/core": V, "@voltx/server": V },
148
- chatbot: { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/memory": V },
149
- "rag-app": { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/rag": V, "@voltx/db": V },
150
- "agent-app": { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/agents": V, "@voltx/memory": V }
160
+ blank: { "@voltx/core": v("@voltx/core"), "@voltx/server": v("@voltx/server") },
161
+ chatbot: { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/memory": v("@voltx/memory") },
162
+ "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") },
163
+ "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") }
151
164
  };
152
165
  async function createProject(options) {
153
- const { name, template = "blank", auth = "none" } = options;
166
+ const { name, template = "blank", auth = "none", shadcn = false } = options;
154
167
  const targetDir = path.resolve(process.cwd(), name);
155
168
  if (fs.existsSync(targetDir)) {
156
169
  console.error(`[voltx] Directory "${name}" already exists.`);
@@ -160,21 +173,39 @@ async function createProject(options) {
160
173
  const provider = template === "rag-app" ? "openai" : "cerebras";
161
174
  const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
162
175
  const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
163
- const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": V };
176
+ const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": v("@voltx/cli") };
164
177
  if (auth === "better-auth") {
165
- deps["@voltx/auth"] = V;
178
+ deps["@voltx/auth"] = v("@voltx/auth");
166
179
  deps["better-auth"] = "^1.5.0";
167
180
  } else if (auth === "jwt") {
168
- deps["@voltx/auth"] = V;
181
+ deps["@voltx/auth"] = v("@voltx/auth");
169
182
  deps["jose"] = "^6.0.0";
170
183
  }
184
+ const devDeps = { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" };
185
+ deps["hono"] = "^4.7.0";
186
+ deps["@hono/node-server"] = "^1.14.0";
187
+ deps["react"] = "^19.0.0";
188
+ deps["react-dom"] = "^19.0.0";
189
+ deps["tailwindcss"] = "^4.0.0";
190
+ devDeps["vite"] = "^6.0.0";
191
+ devDeps["@hono/vite-dev-server"] = "^0.7.0";
192
+ devDeps["@vitejs/plugin-react"] = "^4.3.0";
193
+ devDeps["@tailwindcss/vite"] = "^4.0.0";
194
+ devDeps["@types/react"] = "^19.0.0";
195
+ devDeps["@types/react-dom"] = "^19.0.0";
196
+ if (shadcn) {
197
+ deps["class-variance-authority"] = "^0.7.0";
198
+ deps["clsx"] = "^2.1.0";
199
+ deps["tailwind-merge"] = "^3.0.0";
200
+ deps["lucide-react"] = "^0.468.0";
201
+ }
171
202
  fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
172
203
  name,
173
204
  version: "0.1.0",
174
205
  private: true,
175
206
  scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
176
207
  dependencies: deps,
177
- devDependencies: { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" }
208
+ devDependencies: devDeps
178
209
  }, null, 2));
179
210
  let config = `import { defineConfig } from "@voltx/core";
180
211
 
@@ -195,31 +226,53 @@ export default defineConfig({
195
226
  },`;
196
227
  config += `
197
228
  server: {
198
- routesDir: "src/routes",
229
+ routesDir: "api",
199
230
  staticDir: "public",
200
231
  cors: true,
201
232
  },
202
233
  });
203
234
  `;
204
235
  fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
205
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api"), { recursive: true });
236
+ fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
206
237
  fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
207
- fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify({
208
- compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", strict: true, esModuleInterop: true, skipLibCheck: true, outDir: "dist" },
209
- include: ["src", "voltx.config.ts"]
210
- }, null, 2));
211
- fs.writeFileSync(
212
- path.join(targetDir, "src", "index.ts"),
213
- `import { createApp } from "@voltx/core";
214
- import config from "../voltx.config";
215
-
216
- const app = createApp(config);
217
- app.start();
218
- `
219
- );
238
+ fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
239
+ fs.mkdirSync(path.join(targetDir, "src", "components"), { recursive: true });
240
+ fs.mkdirSync(path.join(targetDir, "src", "hooks"), { recursive: true });
241
+ fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
242
+ fs.writeFileSync(path.join(targetDir, "public", "favicon.svg"), generateFaviconSVG());
243
+ fs.writeFileSync(path.join(targetDir, "public", "robots.txt"), generateRobotsTxt());
244
+ fs.writeFileSync(path.join(targetDir, "public", "site.webmanifest"), generateWebManifest(name));
245
+ const tsconfig = {
246
+ compilerOptions: {
247
+ target: "ES2022",
248
+ module: "ESNext",
249
+ moduleResolution: "bundler",
250
+ strict: true,
251
+ esModuleInterop: true,
252
+ skipLibCheck: true,
253
+ outDir: "dist",
254
+ baseUrl: ".",
255
+ paths: { "@/*": ["./src/*"] },
256
+ jsx: "react-jsx"
257
+ },
258
+ include: ["src", "api", "server.ts", "voltx.config.ts"]
259
+ };
260
+ if (template === "agent-app") tsconfig.include.push("agents", "tools");
261
+ fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
262
+ fs.writeFileSync(path.join(targetDir, "server.ts"), generateServerEntry(name, template));
263
+ fs.writeFileSync(path.join(targetDir, "vite.config.ts"), generateViteConfigFile("server.ts"));
264
+ fs.writeFileSync(path.join(targetDir, "src", "entry-client.tsx"), generateEntryClient());
265
+ fs.writeFileSync(path.join(targetDir, "src", "entry-server.tsx"), generateEntryServer());
266
+ fs.writeFileSync(path.join(targetDir, "src", "layout.tsx"), generateLayoutComponent(name));
267
+ fs.writeFileSync(path.join(targetDir, "src", "globals.css"), generateGlobalCSS(shadcn));
268
+ if (shadcn) {
269
+ fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.ts"), generateCnUtil());
270
+ fs.writeFileSync(path.join(targetDir, "components.json"), generateComponentsJson());
271
+ }
272
+ fs.writeFileSync(path.join(targetDir, "src", "app.tsx"), generateAppComponent(name, template));
220
273
  fs.writeFileSync(
221
- path.join(targetDir, "src", "routes", "index.ts"),
222
- `// GET / \u2014 Health check
274
+ path.join(targetDir, "api", "index.ts"),
275
+ `// GET /api \u2014 Health check
223
276
  import type { Context } from "@voltx/server";
224
277
 
225
278
  export function GET(c: Context) {
@@ -229,7 +282,7 @@ export function GET(c: Context) {
229
282
  );
230
283
  if (template === "chatbot" || template === "agent-app") {
231
284
  fs.writeFileSync(
232
- path.join(targetDir, "src", "routes", "api", "chat.ts"),
285
+ path.join(targetDir, "api", "chat.ts"),
233
286
  `// POST /api/chat \u2014 Streaming chat with conversation memory
234
287
  import type { Context } from "@voltx/server";
235
288
  import { streamText } from "@voltx/ai";
@@ -263,9 +316,9 @@ export async function POST(c: Context) {
263
316
  );
264
317
  }
265
318
  if (template === "agent-app") {
266
- fs.mkdirSync(path.join(targetDir, "src", "agents"), { recursive: true });
267
- fs.mkdirSync(path.join(targetDir, "src", "tools"), { recursive: true });
268
- fs.writeFileSync(path.join(targetDir, "src", "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
319
+ fs.mkdirSync(path.join(targetDir, "agents"), { recursive: true });
320
+ fs.mkdirSync(path.join(targetDir, "tools"), { recursive: true });
321
+ fs.writeFileSync(path.join(targetDir, "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
269
322
  import type { Tool } from "@voltx/agents";
270
323
 
271
324
  export const calculatorTool: Tool = {
@@ -290,7 +343,7 @@ export const calculatorTool: Tool = {
290
343
  },
291
344
  };
292
345
  `);
293
- fs.writeFileSync(path.join(targetDir, "src", "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
346
+ fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
294
347
  import type { Tool } from "@voltx/agents";
295
348
 
296
349
  export const datetimeTool: Tool = {
@@ -311,7 +364,7 @@ export const datetimeTool: Tool = {
311
364
  },
312
365
  };
313
366
  `);
314
- fs.writeFileSync(path.join(targetDir, "src", "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
367
+ fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
315
368
  import { createAgent } from "@voltx/agents";
316
369
  import { calculatorTool } from "../tools/calculator";
317
370
  import { datetimeTool } from "../tools/datetime";
@@ -325,10 +378,10 @@ export const assistant = createAgent({
325
378
  });
326
379
  `);
327
380
  fs.writeFileSync(
328
- path.join(targetDir, "src", "routes", "api", "agent.ts"),
381
+ path.join(targetDir, "api", "agent.ts"),
329
382
  `// POST /api/agent \u2014 Run the AI agent
330
383
  import type { Context } from "@voltx/server";
331
- import { assistant } from "../../agents/assistant";
384
+ import { assistant } from "../agents/assistant";
332
385
 
333
386
  export async function POST(c: Context) {
334
387
  const { input } = await c.req.json();
@@ -341,9 +394,9 @@ export async function POST(c: Context) {
341
394
  }
342
395
  if (template === "rag-app") {
343
396
  const embedModel = "openai:text-embedding-3-small";
344
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "rag"), { recursive: true });
397
+ fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
345
398
  fs.writeFileSync(
346
- path.join(targetDir, "src", "routes", "api", "rag", "query.ts"),
399
+ path.join(targetDir, "api", "rag", "query.ts"),
347
400
  `// POST /api/rag/query \u2014 Query documents with RAG
348
401
  import type { Context } from "@voltx/server";
349
402
  import { streamText } from "@voltx/ai";
@@ -367,7 +420,7 @@ export async function POST(c: Context) {
367
420
  `
368
421
  );
369
422
  fs.writeFileSync(
370
- path.join(targetDir, "src", "routes", "api", "rag", "ingest.ts"),
423
+ path.join(targetDir, "api", "rag", "ingest.ts"),
371
424
  `// POST /api/rag/ingest \u2014 Ingest documents into the vector store
372
425
  import type { Context } from "@voltx/server";
373
426
  import { createRAGPipeline, createEmbedder } from "@voltx/rag";
@@ -387,12 +440,12 @@ export async function POST(c: Context) {
387
440
  );
388
441
  }
389
442
  if (auth === "better-auth") {
390
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "auth"), { recursive: true });
443
+ fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
391
444
  fs.writeFileSync(
392
- path.join(targetDir, "src", "routes", "api", "auth", "[...path].ts"),
445
+ path.join(targetDir, "api", "auth", "[...path].ts"),
393
446
  `// ALL /api/auth/* \u2014 Better Auth handler
394
447
  import type { Context } from "@voltx/server";
395
- import { auth } from "../../../lib/auth";
448
+ import { auth } from "../../src/lib/auth";
396
449
  import { createAuthHandler } from "@voltx/auth";
397
450
 
398
451
  const handler = createAuthHandler(auth);
@@ -401,7 +454,6 @@ export const GET = (c: Context) => handler(c);
401
454
  export const POST = (c: Context) => handler(c);
402
455
  `
403
456
  );
404
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
405
457
  fs.writeFileSync(
406
458
  path.join(targetDir, "src", "lib", "auth.ts"),
407
459
  `import { createAuth, createAuthMiddleware } from "@voltx/auth";
@@ -418,7 +470,6 @@ export const authMiddleware = createAuthMiddleware({
418
470
  `
419
471
  );
420
472
  } else if (auth === "jwt") {
421
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
422
473
  fs.writeFileSync(
423
474
  path.join(targetDir, "src", "lib", "auth.ts"),
424
475
  `import { createAuth, createAuthMiddleware } from "@voltx/auth";
@@ -435,9 +486,9 @@ export const authMiddleware = createAuthMiddleware({
435
486
  `
436
487
  );
437
488
  fs.writeFileSync(
438
- path.join(targetDir, "src", "routes", "api", "auth.ts"),
489
+ path.join(targetDir, "api", "auth.ts"),
439
490
  `import type { Context } from "@voltx/server";
440
- import { jwt } from "../../lib/auth";
491
+ import { jwt } from "../src/lib/auth";
441
492
 
442
493
  export async function POST(c: Context) {
443
494
  const { email, password } = await c.req.json();
@@ -454,14 +505,6 @@ export async function POST(c: Context) {
454
505
  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";
455
506
  } else if (template === "chatbot" || template === "agent-app") {
456
507
  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";
457
- if (template === "agent-app") {
458
- envContent += "# \u2500\u2500\u2500 Database (optional) \u2500\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";
459
- envContent += "# \u2500\u2500\u2500 Tool API Keys (add keys for tools you use) \u2500\u2500\n";
460
- envContent += "# TAVILY_API_KEY=tvly-... (Web Search \u2014 https://tavily.com)\n";
461
- envContent += "# SERPER_API_KEY= (Google Search \u2014 https://serper.dev)\n";
462
- envContent += "# OPENWEATHER_API_KEY= (Weather \u2014 https://openweathermap.org/api)\n";
463
- envContent += "# NEWS_API_KEY= (News \u2014 https://newsapi.org)\n\n";
464
- }
465
508
  } else {
466
509
  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";
467
510
  }
@@ -472,158 +515,647 @@ export async function POST(c: Context) {
472
515
  }
473
516
  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";
474
517
  fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
475
- if (template !== "blank") {
476
- fs.writeFileSync(path.join(targetDir, "public", "index.html"), generateFrontendHTML(name, template));
477
- }
478
- fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n");
518
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
479
519
  printWelcomeBanner(name);
480
520
  }
481
- function generateFrontendHTML(projectName, template) {
482
- const badge = template === "chatbot" ? "Chatbot" : template === "rag-app" ? "RAG App" : "Agent App";
483
- const accentClass = template === "rag-app" ? "emerald" : template === "agent-app" ? "purple" : "blue";
484
- const shell = (body) => `<!DOCTYPE html>
485
- <html lang="en" class="h-full">
486
- <head>
487
- <meta charset="UTF-8" />
488
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
489
- <title>${projectName}</title>
490
- <script src="https://cdn.tailwindcss.com/3.4.17"></script>
491
- <style>
492
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
493
- body { font-family: 'Inter', system-ui, sans-serif; }
494
- .msg-enter { animation: msgIn 0.25s ease-out; }
495
- @keyframes msgIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
496
- .typing-dot { animation: blink 1.4s infinite both; }
497
- .typing-dot:nth-child(2) { animation-delay: 0.2s; }
498
- .typing-dot:nth-child(3) { animation-delay: 0.4s; }
499
- @keyframes blink { 0%, 80%, 100% { opacity: 0.2; } 40% { opacity: 1; } }
500
- ::-webkit-scrollbar { width: 6px; }
501
- ::-webkit-scrollbar-track { background: transparent; }
502
- ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
503
- </style>
504
- </head>
505
- <body class="h-full bg-gray-950 text-gray-100">
506
- <div id="app" class="h-full flex flex-col">
507
- <header class="flex-shrink-0 border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm px-4 py-3">
508
- <div class="max-w-4xl mx-auto flex items-center justify-between">
509
- <div class="flex items-center gap-3">
510
- <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-sm font-bold">V</div>
511
- <h1 class="text-lg font-semibold text-white">${projectName}</h1>
512
- <span class="text-xs px-2 py-0.5 rounded-full bg-gray-800 text-gray-400">${badge}</span>
513
- </div>
514
- <a href="https://github.com/codewithshail/voltx" target="_blank" class="text-gray-500 hover:text-gray-300 transition-colors text-sm">Built with VoltX</a>
515
- </div>
516
- </header>
517
- <main class="flex-1 overflow-hidden"><div class="h-full max-w-4xl mx-auto">
518
- ${body}
519
- </div></main>
520
- </div>
521
- </body>
522
- </html>`;
523
- if (template === "chatbot") return shell(chatbotBody());
524
- if (template === "rag-app") return shell(ragAppBody());
525
- if (template === "agent-app") return shell(agentAppBody());
526
- return "";
527
- }
528
- function chatbotBody() {
529
- return ` <div class="h-full flex flex-col">
530
- <div id="messages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
531
- <div class="text-center py-12">
532
- <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-blue-500/20 to-purple-600/20 border border-blue-500/20 flex items-center justify-center">
533
- <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
534
- </div>
535
- <h2 class="text-xl font-semibold text-white mb-2">Start a conversation</h2>
536
- <p class="text-gray-500 text-sm">Type a message below to chat with your AI assistant.</p>
537
- </div>
521
+ function generateServerEntry(projectName, template) {
522
+ const imports = [];
523
+ const mounts = [];
524
+ imports.push('import { GET as healthGET } from "./api/index";');
525
+ mounts.push('app.get("/api", healthGET);');
526
+ if (template === "chatbot" || template === "agent-app") {
527
+ imports.push('import { POST as chatPOST } from "./api/chat";');
528
+ mounts.push('app.post("/api/chat", chatPOST);');
529
+ }
530
+ if (template === "agent-app") {
531
+ imports.push('import { POST as agentPOST } from "./api/agent";');
532
+ mounts.push('app.post("/api/agent", agentPOST);');
533
+ }
534
+ if (template === "rag-app") {
535
+ imports.push('import { POST as ragQueryPOST } from "./api/rag/query";');
536
+ imports.push('import { POST as ragIngestPOST } from "./api/rag/ingest";');
537
+ mounts.push('app.post("/api/rag/query", ragQueryPOST);');
538
+ mounts.push('app.post("/api/rag/ingest", ragIngestPOST);');
539
+ }
540
+ return `import { Hono } from "hono";
541
+ import { serve } from "@hono/node-server";
542
+ import { serveStatic } from "@hono/node-server/serve-static";
543
+ import { registerSSR } from "@voltx/server";
544
+ import { loadEnv } from "@voltx/core";
545
+ ${imports.join("\n")}
546
+
547
+ loadEnv(process.env.NODE_ENV ?? "development");
548
+
549
+ const isProd = process.env.NODE_ENV === "production";
550
+ const app = new Hono();
551
+
552
+ // \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
553
+ ${mounts.join("\n")}
554
+
555
+ // \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
556
+ if (isProd) {
557
+ app.use("/assets/*", serveStatic({ root: "./dist/client/" }));
558
+ app.use("/favicon.svg", serveStatic({ root: "./public/" }));
559
+ app.use("/robots.txt", serveStatic({ root: "./public/" }));
560
+ app.use("/site.webmanifest", serveStatic({ root: "./public/" }));
561
+ }
562
+
563
+ // \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
564
+ registerSSR(app, null, {
565
+ title: "${projectName}",
566
+ entryServer: "src/entry-server.tsx",
567
+ entryClient: "src/entry-client.tsx",
568
+ });
569
+
570
+ export default app;
571
+
572
+ if (isProd) {
573
+ const port = Number(process.env.PORT) || 3000;
574
+ serve({ fetch: app.fetch, port }, (info) => {
575
+ console.log(\`\\n \u26A1 ${projectName} running at http://localhost:\${info.port}\\n\`);
576
+ });
577
+ }
578
+ `;
579
+ }
580
+ function generateViteConfigFile(entry) {
581
+ return `import { defineConfig } from "vite";
582
+ import devServer from "@hono/vite-dev-server";
583
+ import react from "@vitejs/plugin-react";
584
+ import tailwindcss from "@tailwindcss/vite";
585
+
586
+ export default defineConfig({
587
+ resolve: {
588
+ alias: {
589
+ "@": "/src",
590
+ },
591
+ },
592
+ plugins: [
593
+ react(),
594
+ tailwindcss(),
595
+ devServer({
596
+ entry: "${entry}",
597
+ exclude: [
598
+ /.*\\.tsx?($|\\?)/,
599
+ /.*\\.(s?css|less)($|\\?)/,
600
+ /.*\\.(svg|png|jpg|jpeg|gif|webp|ico)($|\\?)/,
601
+ /^\\/@.+$/,
602
+ /^\\/favicon\\.svg$/,
603
+ /^\\/node_modules\\/.*/,
604
+ /^\\/src\\/.*/,
605
+ ],
606
+ injectClientScript: false,
607
+ }),
608
+ ],
609
+ });
610
+ `;
611
+ }
612
+ function generateEntryClient() {
613
+ return `import React from "react";
614
+ import { hydrateRoot } from "react-dom/client";
615
+ import Layout from "./layout";
616
+ import App from "./app";
617
+ import "./globals.css";
618
+
619
+ hydrateRoot(
620
+ document.getElementById("root")!,
621
+ <React.StrictMode>
622
+ <Layout>
623
+ <App />
624
+ </Layout>
625
+ </React.StrictMode>
626
+ );
627
+ `;
628
+ }
629
+ function generateEntryServer() {
630
+ return `import React from "react";
631
+ import { renderToReadableStream } from "react-dom/server";
632
+ import Layout from "./layout";
633
+ import App from "./app";
634
+
635
+ export async function render(_url: string): Promise<ReadableStream> {
636
+ const stream = await renderToReadableStream(
637
+ <React.StrictMode>
638
+ <Layout>
639
+ <App />
640
+ </Layout>
641
+ </React.StrictMode>,
642
+ {
643
+ onError(error: unknown) {
644
+ console.error("[voltx] SSR render error:", error);
645
+ },
646
+ }
647
+ );
648
+ return stream;
649
+ }
650
+ `;
651
+ }
652
+ function generateLayoutComponent(projectName) {
653
+ return `import React from "react";
654
+
655
+ export default function Layout({ children }: { children: React.ReactNode }) {
656
+ return (
657
+ <div className="min-h-screen bg-background text-foreground font-sans">
658
+ <header className="border-b border-border px-6 py-3 flex items-center justify-between">
659
+ <div className="flex items-center gap-2">
660
+ <span className="text-xl">\u26A1</span>
661
+ <span className="font-semibold">${projectName}</span>
538
662
  </div>
539
- <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
540
- <form id="chatForm" class="flex gap-3">
541
- <input id="chatInput" type="text" placeholder="Type your message..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors" />
542
- <button type="submit" id="sendBtn" class="px-5 py-3 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Send</button>
543
- </form>
663
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-muted text-sm hover:text-foreground transition-colors">
664
+ Built with VoltX
665
+ </a>
666
+ </header>
667
+ <main>{children}</main>
668
+ </div>
669
+ );
670
+ }
671
+ `;
672
+ }
673
+ function generateGlobalCSS(useShadcn = false) {
674
+ if (useShadcn) {
675
+ return `@import "tailwindcss";
676
+
677
+ @theme inline {
678
+ --radius-sm: 0.25rem;
679
+ --radius-md: 0.375rem;
680
+ --radius-lg: 0.5rem;
681
+ --radius-xl: 0.75rem;
682
+ --color-background: hsl(var(--background));
683
+ --color-foreground: hsl(var(--foreground));
684
+ --color-card: hsl(var(--card));
685
+ --color-card-foreground: hsl(var(--card-foreground));
686
+ --color-popover: hsl(var(--popover));
687
+ --color-popover-foreground: hsl(var(--popover-foreground));
688
+ --color-primary: hsl(var(--primary));
689
+ --color-primary-foreground: hsl(var(--primary-foreground));
690
+ --color-secondary: hsl(var(--secondary));
691
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
692
+ --color-muted: hsl(var(--muted));
693
+ --color-muted-foreground: hsl(var(--muted-foreground));
694
+ --color-accent: hsl(var(--accent));
695
+ --color-accent-foreground: hsl(var(--accent-foreground));
696
+ --color-destructive: hsl(var(--destructive));
697
+ --color-border: hsl(var(--border));
698
+ --color-input: hsl(var(--input));
699
+ --color-ring: hsl(var(--ring));
700
+ --color-chart-1: hsl(var(--chart-1));
701
+ --color-chart-2: hsl(var(--chart-2));
702
+ --color-chart-3: hsl(var(--chart-3));
703
+ --color-chart-4: hsl(var(--chart-4));
704
+ --color-chart-5: hsl(var(--chart-5));
705
+ --color-sidebar: hsl(var(--sidebar));
706
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
707
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
708
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
709
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
710
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
711
+ --color-sidebar-border: hsl(var(--sidebar-border));
712
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
713
+ }
714
+
715
+ :root {
716
+ --background: 0 0% 4%;
717
+ --foreground: 0 0% 93%;
718
+ --card: 0 0% 6%;
719
+ --card-foreground: 0 0% 93%;
720
+ --popover: 0 0% 6%;
721
+ --popover-foreground: 0 0% 93%;
722
+ --primary: 0 0% 93%;
723
+ --primary-foreground: 0 0% 6%;
724
+ --secondary: 0 0% 12%;
725
+ --secondary-foreground: 0 0% 93%;
726
+ --muted: 0 0% 12%;
727
+ --muted-foreground: 0 0% 55%;
728
+ --accent: 0 0% 12%;
729
+ --accent-foreground: 0 0% 93%;
730
+ --destructive: 0 62% 50%;
731
+ --border: 0 0% 14%;
732
+ --input: 0 0% 14%;
733
+ --ring: 0 0% 83%;
734
+ --chart-1: 220 70% 50%;
735
+ --chart-2: 160 60% 45%;
736
+ --chart-3: 30 80% 55%;
737
+ --chart-4: 280 65% 60%;
738
+ --chart-5: 340 75% 55%;
739
+ --sidebar: 0 0% 5%;
740
+ --sidebar-foreground: 0 0% 93%;
741
+ --sidebar-primary: 0 0% 93%;
742
+ --sidebar-primary-foreground: 0 0% 6%;
743
+ --sidebar-accent: 0 0% 12%;
744
+ --sidebar-accent-foreground: 0 0% 93%;
745
+ --sidebar-border: 0 0% 14%;
746
+ --sidebar-ring: 0 0% 83%;
747
+ }
748
+
749
+ *,
750
+ *::before,
751
+ *::after {
752
+ box-sizing: border-box;
753
+ margin: 0;
754
+ padding: 0;
755
+ border-color: var(--color-border);
756
+ }
757
+
758
+ html,
759
+ body {
760
+ height: 100%;
761
+ background: var(--color-background);
762
+ color: var(--color-foreground);
763
+ font-family: system-ui, -apple-system, sans-serif;
764
+ -webkit-font-smoothing: antialiased;
765
+ }
766
+
767
+ #root {
768
+ height: 100%;
769
+ }
770
+ `;
771
+ }
772
+ return `@import "tailwindcss";
773
+
774
+ @theme {
775
+ --color-background: #0a0a0a;
776
+ --color-foreground: #ededed;
777
+ --color-muted: #888888;
778
+ --color-border: #222222;
779
+ --color-primary: #2563eb;
780
+ --color-accent: #a78bfa;
781
+ --font-sans: system-ui, -apple-system, sans-serif;
782
+ }
783
+
784
+ *,
785
+ *::before,
786
+ *::after {
787
+ box-sizing: border-box;
788
+ margin: 0;
789
+ padding: 0;
790
+ }
791
+
792
+ html,
793
+ body {
794
+ height: 100%;
795
+ background: var(--color-background);
796
+ color: var(--color-foreground);
797
+ font-family: var(--font-sans);
798
+ -webkit-font-smoothing: antialiased;
799
+ }
800
+
801
+ #root {
802
+ height: 100%;
803
+ }
804
+
805
+ a {
806
+ color: inherit;
807
+ text-decoration: none;
808
+ }
809
+
810
+ a:hover {
811
+ text-decoration: underline;
812
+ }
813
+ `;
814
+ }
815
+ function generateCnUtil() {
816
+ return `import { type ClassValue, clsx } from "clsx";
817
+ import { twMerge } from "tailwind-merge";
818
+
819
+ export function cn(...inputs: ClassValue[]) {
820
+ return twMerge(clsx(inputs));
821
+ }
822
+ `;
823
+ }
824
+ function generateComponentsJson() {
825
+ return JSON.stringify({
826
+ "$schema": "https://ui.shadcn.com/schema.json",
827
+ style: "new-york",
828
+ rsc: false,
829
+ tsx: true,
830
+ tailwind: {
831
+ config: "",
832
+ css: "src/globals.css",
833
+ baseColor: "neutral",
834
+ cssVariables: true
835
+ },
836
+ aliases: {
837
+ components: "@/components",
838
+ utils: "@/lib/utils",
839
+ ui: "@/components/ui",
840
+ lib: "@/lib",
841
+ hooks: "@/hooks"
842
+ }
843
+ }, null, 2) + "\n";
844
+ }
845
+ function generateAppComponent(projectName, template) {
846
+ if (template === "blank") {
847
+ return `import React, { useState, useEffect } from "react";
848
+
849
+ export default function App() {
850
+ const [status, setStatus] = useState<string>("checking...");
851
+
852
+ useEffect(() => {
853
+ fetch("/api")
854
+ .then((res) => res.json())
855
+ .then((data) => setStatus(data.status || "ok"))
856
+ .catch(() => setStatus("error"));
857
+ }, []);
858
+
859
+ return (
860
+ <div className="flex flex-col items-center justify-center min-h-[calc(100vh-60px)] px-6 py-12">
861
+ <div className="text-center max-w-2xl w-full">
862
+ {/* Hero */}
863
+ <div className="relative mb-8">
864
+ <div className="absolute inset-0 blur-3xl opacity-20 bg-gradient-to-r from-blue-500 via-purple-500 to-blue-500 rounded-full" />
865
+ <div className="relative text-7xl mb-4">\u26A1</div>
544
866
  </div>
545
- </div>
546
- <script>
547
- const messages=[], messagesEl=document.getElementById("messages"), form=document.getElementById("chatForm"), input=document.getElementById("chatInput"), sendBtn=document.getElementById("sendBtn");
548
- function addMsg(role,content){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-blue-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed";b.textContent=content;d.appendChild(b);const w=messagesEl.querySelector(".text-center.py-12");if(w)w.remove();messagesEl.appendChild(d);messagesEl.scrollTop=messagesEl.scrollHeight;return b}
549
- form.addEventListener("submit",async e=>{e.preventDefault();const text=input.value.trim();if(!text)return;messages.push({role:"user",content:text});addMsg("user",text);input.value="";sendBtn.disabled=true;try{const res=await fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({messages,conversationId:"default"})});if(!res.ok){addMsg("assistant","Error: "+res.status);sendBtn.disabled=false;return}const bubble=addMsg("assistant","");const reader=res.body.getReader();const dec=new TextDecoder();let full="";while(true){const{done,value}=await reader.read();if(done)break;const chunk=dec.decode(value,{stream:true});for(const line of chunk.split("\\n")){if(line.startsWith("data: ")){const d=line.slice(6);if(d==="[DONE]")continue;try{const p=JSON.parse(d);const t=p.textDelta||p.choices?.[0]?.delta?.content||p.content||p.text||"";full+=t;bubble.textContent=full;messagesEl.scrollTop=messagesEl.scrollHeight}catch{}}}}messages.push({role:"assistant",content:full})}catch(err){addMsg("assistant","Error: "+err.message)}sendBtn.disabled=false;input.focus()});
550
- input.focus();
551
- </script>`;
552
- }
553
- function ragAppBody() {
554
- return ` <div class="h-full flex flex-col md:flex-row">
555
- <div class="md:w-80 flex-shrink-0 border-b md:border-b-0 md:border-r border-gray-800 flex flex-col">
556
- <div class="px-4 py-3 border-b border-gray-800"><h2 class="text-sm font-semibold text-white flex items-center gap-2"><svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>Ingest Documents</h2></div>
557
- <div class="flex-1 p-4 flex flex-col gap-3">
558
- <textarea id="ingestText" rows="6" placeholder="Paste text to ingest..." class="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-emerald-500 resize-none"></textarea>
559
- <button id="ingestBtn" onclick="ingestDoc()" class="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-lg transition-colors">Ingest</button>
560
- <div id="ingestStatus" class="text-xs text-gray-500 hidden"></div>
561
- <div class="mt-auto pt-3 border-t border-gray-800"><p class="text-xs text-gray-600">Documents are chunked, embedded, and stored for retrieval.</p></div>
867
+ <h1 className="text-5xl font-bold tracking-tight mb-3 bg-gradient-to-r from-white to-white/60 bg-clip-text text-transparent">
868
+ \${"\${projectName}"}
869
+ </h1>
870
+ <p className="text-muted text-lg mb-10">The AI-first full-stack framework</p>
871
+
872
+ {/* Status cards */}
873
+ <div className="flex gap-4 justify-center mb-10">
874
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
875
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Server</div>
876
+ <div className={\`text-sm font-medium \${status === "ok" ? "text-emerald-400" : "text-red-400"}\`}>
877
+ <span className={\`inline-block w-2 h-2 rounded-full mr-2 \${status === "ok" ? "bg-emerald-400 animate-pulse" : "bg-red-400"}\`} />
878
+ {status}
879
+ </div>
562
880
  </div>
563
- </div>
564
- <div class="flex-1 flex flex-col min-w-0">
565
- <div id="ragMessages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
566
- <div class="text-center py-12">
567
- <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-emerald-500/20 to-blue-600/20 border border-emerald-500/20 flex items-center justify-center"><svg class="w-8 h-8 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg></div>
568
- <h2 class="text-xl font-semibold text-white mb-2">Ask your documents</h2>
569
- <p class="text-gray-500 text-sm">Ingest documents on the left, then ask questions here.</p>
881
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
882
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Frontend</div>
883
+ <div className="text-sm font-medium text-emerald-400">
884
+ <span className="inline-block w-2 h-2 rounded-full mr-2 bg-emerald-400 animate-pulse" />
885
+ React + Vite
570
886
  </div>
571
887
  </div>
572
- <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
573
- <form id="ragForm" class="flex gap-3">
574
- <input id="ragInput" type="text" placeholder="Ask about your documents..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors" />
575
- <button type="submit" id="ragSendBtn" class="px-5 py-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Ask</button>
576
- </form>
888
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
889
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">CSS</div>
890
+ <div className="text-sm font-medium text-sky-400">Tailwind v4</div>
577
891
  </div>
578
892
  </div>
579
- </div>
580
- <script>
581
- const ragEl=document.getElementById("ragMessages");
582
- function addRagMsg(role,c){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-emerald-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed";b.textContent=c;d.appendChild(b);const w=ragEl.querySelector(".text-center.py-12");if(w)w.remove();ragEl.appendChild(d);ragEl.scrollTop=ragEl.scrollHeight;return b}
583
- async function ingestDoc(){const text=document.getElementById("ingestText").value.trim();if(!text)return;const btn=document.getElementById("ingestBtn"),st=document.getElementById("ingestStatus");btn.disabled=true;btn.textContent="Ingesting...";st.className="text-xs text-yellow-400";st.textContent="Processing...";st.classList.remove("hidden");try{const res=await fetch("/api/rag/ingest",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text})});const data=await res.json();if(res.ok){st.className="text-xs text-emerald-400";st.textContent="Ingested "+(data.chunks||0)+" chunks.";document.getElementById("ingestText").value=""}else{st.className="text-xs text-red-400";st.textContent="Error: "+(data.error||res.statusText)}}catch(e){st.className="text-xs text-red-400";st.textContent="Error: "+e.message}btn.disabled=false;btn.textContent="Ingest"}
584
- document.getElementById("ragForm").addEventListener("submit",async e=>{e.preventDefault();const input=document.getElementById("ragInput"),text=input.value.trim();if(!text)return;addRagMsg("user",text);input.value="";document.getElementById("ragSendBtn").disabled=true;try{const res=await fetch("/api/rag/query",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question:text})});if(!res.ok){addRagMsg("assistant","Error: "+res.status);document.getElementById("ragSendBtn").disabled=false;return}const bubble=addRagMsg("assistant","");const reader=res.body.getReader();const dec=new TextDecoder();let full="";while(true){const{done,value}=await reader.read();if(done)break;const chunk=dec.decode(value,{stream:true});for(const line of chunk.split("\\n")){if(line.startsWith("data: ")){const d=line.slice(6);if(d==="[DONE]")continue;try{const p=JSON.parse(d);const t=p.textDelta||p.choices?.[0]?.delta?.content||p.content||p.text||"";full+=t;bubble.textContent=full;ragEl.scrollTop=ragEl.scrollHeight}catch{}}}};}catch(err){addRagMsg("assistant","Error: "+err.message)}document.getElementById("ragSendBtn").disabled=false;document.getElementById("ragInput").focus()});
585
- document.getElementById("ragInput").focus();
586
- </script>`;
587
- }
588
- function agentAppBody() {
589
- return ` <div class="h-full flex flex-col">
590
- <div id="agentMessages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
591
- <div class="text-center py-12">
592
- <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-purple-500/20 to-orange-500/20 border border-purple-500/20 flex items-center justify-center"><svg class="w-8 h-8 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg></div>
593
- <h2 class="text-xl font-semibold text-white mb-2">AI Agent</h2>
594
- <p class="text-gray-500 text-sm">Your agent can use tools to answer questions accurately.</p>
893
+
894
+ {/* Get started */}
895
+ <div className="bg-white/[0.03] border border-border rounded-2xl p-8 text-left backdrop-blur-sm">
896
+ <h2 className="text-sm font-medium text-muted mb-6 uppercase tracking-wider">Get started</h2>
897
+ <div className="space-y-4">
898
+ <div className="flex items-start gap-4">
899
+ <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>
900
+ <div>
901
+ <code className="text-purple-400 text-sm">src/app.tsx</code>
902
+ <p className="text-muted text-sm mt-1">Edit this file to build your UI</p>
903
+ </div>
904
+ </div>
905
+ <div className="flex items-start gap-4">
906
+ <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>
907
+ <div>
908
+ <code className="text-blue-400 text-sm">api/</code>
909
+ <p className="text-muted text-sm mt-1">Add API routes here (file-based routing)</p>
910
+ </div>
911
+ </div>
912
+ <div className="flex items-start gap-4">
913
+ <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>
914
+ <div>
915
+ <code className="text-emerald-400 text-sm">src/components/</code>
916
+ <p className="text-muted text-sm mt-1">Create React components with Tailwind CSS</p>
917
+ </div>
918
+ </div>
595
919
  </div>
596
920
  </div>
597
- <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
598
- <form id="agentForm" class="flex gap-3">
599
- <input id="agentInput" type="text" placeholder="Ask the agent anything..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors" />
600
- <button type="submit" id="agentSendBtn" class="px-5 py-3 bg-purple-600 hover:bg-purple-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Run</button>
601
- </form>
921
+
922
+ {/* Links */}
923
+ <div className="flex gap-6 justify-center mt-8">
924
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
925
+ GitHub \u2192
926
+ </a>
927
+ <a href="https://voltx.co.in" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
928
+ Docs \u2192
929
+ </a>
602
930
  </div>
603
931
  </div>
604
- <script>
605
- const agentEl=document.getElementById("agentMessages");
606
- function addAgentMsg(role,c,isStep){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");if(isStep){b.className="max-w-[85%] px-3 py-2 rounded-lg bg-gray-900 border border-gray-700 text-xs text-gray-400 font-mono"}else{b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-purple-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed whitespace-pre-wrap"}b.textContent=c;d.appendChild(b);const w=agentEl.querySelector(".text-center.py-12");if(w)w.remove();agentEl.appendChild(d);agentEl.scrollTop=agentEl.scrollHeight;return b}
607
- function addThinking(){const d=document.createElement("div");d.id="thinking";d.className="msg-enter flex justify-start";d.innerHTML='<div class="px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 flex items-center gap-2 text-sm text-gray-400"><svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>Agent is thinking...</div>';agentEl.appendChild(d);agentEl.scrollTop=agentEl.scrollHeight}
608
- function removeThinking(){const t=document.getElementById("thinking");if(t)t.remove()}
609
- document.getElementById("agentForm").addEventListener("submit",async e=>{e.preventDefault();const input=document.getElementById("agentInput"),text=input.value.trim();if(!text)return;addAgentMsg("user",text);input.value="";document.getElementById("agentSendBtn").disabled=true;addThinking();try{const res=await fetch("/api/agent",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:text})});removeThinking();if(!res.ok){const err=await res.json().catch(()=>({}));addAgentMsg("assistant","Error: "+(err.error||res.statusText));document.getElementById("agentSendBtn").disabled=false;return}const data=await res.json();if(data.steps&&data.steps.length>0){for(const s of data.steps){const name=s.tool||s.name||"tool";const inp=typeof s.input==="string"?s.input:JSON.stringify(s.input||{});const out=typeof s.output==="string"?s.output:JSON.stringify(s.output||{});addAgentMsg("assistant","\u{1F527} "+name+"("+inp+")\\n\u2192 "+out.slice(0,300),true)}}addAgentMsg("assistant",data.content||"No response.")}catch(err){removeThinking();addAgentMsg("assistant","Error: "+err.message)}document.getElementById("agentSendBtn").disabled=false;input.focus()});
610
- document.getElementById("agentInput").focus();
611
- </script>`;
612
- }
613
- if (typeof require !== "undefined" && require.main === module && process.argv[1]?.includes("create")) {
614
- const projectName = process.argv[2];
615
- if (!projectName) {
616
- console.log("Usage: create-voltx-app <project-name> [--template chatbot] [--auth jwt]");
617
- process.exit(1);
932
+ </div>
933
+ );
934
+ }
935
+ `;
618
936
  }
619
- const templateFlag = process.argv.indexOf("--template");
620
- const template = templateFlag !== -1 ? process.argv[templateFlag + 1] : "blank";
621
- const authFlag = process.argv.indexOf("--auth");
622
- const auth = authFlag !== -1 ? process.argv[authFlag + 1] : "none";
623
- createProject({ name: projectName, template, auth }).catch((err) => {
624
- console.error("[voltx] Error:", err);
625
- process.exit(1);
626
- });
937
+ const apiEndpoint = template === "agent-app" ? "/api/agent" : template === "rag-app" ? "/api/rag/query" : "/api/chat";
938
+ const isAgent = template === "agent-app";
939
+ const isRag = template === "rag-app";
940
+ let emptyStateTitle = "Start a conversation";
941
+ let emptyStateHint = "Type a message below to chat with AI";
942
+ let accentColor = "blue";
943
+ let inputPlaceholder = "Type a message...";
944
+ if (isAgent) {
945
+ emptyStateTitle = "Talk to your AI agent";
946
+ emptyStateHint = "The agent can use tools like Calculator and Date/Time";
947
+ accentColor = "purple";
948
+ inputPlaceholder = "Ask the agent anything...";
949
+ } else if (isRag) {
950
+ emptyStateTitle = "Ask your documents";
951
+ emptyStateHint = "Query your knowledge base \u2014 ingest docs via POST /api/rag/ingest";
952
+ accentColor = "emerald";
953
+ inputPlaceholder = "Ask a question about your documents...";
954
+ }
955
+ return `import React, { useState, useRef, useEffect, useCallback } from "react";
956
+
957
+ interface Message {
958
+ id: string;
959
+ role: "user" | "assistant";
960
+ content: string;
961
+ }
962
+
963
+ export default function App() {
964
+ const [messages, setMessages] = useState<Message[]>([]);
965
+ const [input, setInput] = useState("");
966
+ const [isLoading, setIsLoading] = useState(false);
967
+ const messagesEndRef = useRef<HTMLDivElement>(null);
968
+ const inputRef = useRef<HTMLInputElement>(null);
969
+
970
+ useEffect(() => {
971
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
972
+ }, [messages]);
973
+
974
+ useEffect(() => {
975
+ inputRef.current?.focus();
976
+ }, []);
977
+
978
+ const sendMessage = useCallback(async () => {
979
+ const text = input.trim();
980
+ if (!text || isLoading) return;
981
+
982
+ const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: text };
983
+ setMessages((prev) => [...prev, userMsg]);
984
+ setInput("");
985
+ setIsLoading(true);
986
+
987
+ const assistantMsg: Message = { id: crypto.randomUUID(), role: "assistant", content: "" };
988
+ setMessages((prev) => [...prev, assistantMsg]);
989
+
990
+ try {${isAgent ? `
991
+ const res = await fetch("${apiEndpoint}", {
992
+ method: "POST",
993
+ headers: { "Content-Type": "application/json" },
994
+ body: JSON.stringify({ input: text }),
995
+ });
996
+ const data = await res.json();
997
+ setMessages((prev) =>
998
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: data.content || "No response" } : m)
999
+ );` : `
1000
+ const res = await fetch("${apiEndpoint}", {
1001
+ method: "POST",
1002
+ headers: { "Content-Type": "application/json" },
1003
+ body: JSON.stringify({${isRag ? ` question: text ` : ` messages: [...messages, { role: "user", content: text }] `}}),
1004
+ });
1005
+
1006
+ if (!res.body) throw new Error("No response body");
1007
+
1008
+ const reader = res.body.getReader();
1009
+ const decoder = new TextDecoder();
1010
+ let buffer = "";
1011
+ let fullContent = "";
1012
+
1013
+ while (true) {
1014
+ const { done, value } = await reader.read();
1015
+ if (done) break;
1016
+ buffer += decoder.decode(value, { stream: true });
1017
+ const lines = buffer.split("\\n");
1018
+ buffer = lines.pop() ?? "";
1019
+
1020
+ for (const line of lines) {
1021
+ if (!line.startsWith("data: ")) continue;
1022
+ const data = line.slice(6);
1023
+ if (data === "[DONE]") break;
1024
+ try {
1025
+ const parsed = JSON.parse(data);
1026
+ const chunk = parsed.textDelta ?? parsed.content ?? parsed.choices?.[0]?.delta?.content ?? "";
1027
+ if (chunk) {
1028
+ fullContent += chunk;
1029
+ setMessages((prev) =>
1030
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: fullContent } : m)
1031
+ );
1032
+ }
1033
+ } catch {}
1034
+ }
1035
+ }`}
1036
+ } catch (err) {
1037
+ setMessages((prev) =>
1038
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: "Error: " + (err instanceof Error ? err.message : String(err)) } : m)
1039
+ );
1040
+ } finally {
1041
+ setIsLoading(false);
1042
+ }
1043
+ }, [input, isLoading, messages]);
1044
+
1045
+ return (
1046
+ <div className="h-[calc(100vh-60px)] flex flex-col">
1047
+ <main className="flex-1 overflow-y-auto px-4 py-6">
1048
+ <div className="max-w-3xl mx-auto">
1049
+ {messages.length === 0 && (
1050
+ <div className="flex flex-col items-center justify-center h-[60vh] text-center">
1051
+ <div className="relative mb-6">
1052
+ <div className="absolute inset-0 blur-2xl opacity-20 bg-${accentColor}-500 rounded-full" />
1053
+ <div className="relative text-5xl">\u26A1</div>
1054
+ </div>
1055
+ <h2 className="text-2xl font-semibold mb-2">${emptyStateTitle}</h2>
1056
+ <p className="text-muted text-sm max-w-md">${emptyStateHint}</p>
1057
+ <div className="flex gap-2 mt-6">
1058
+ ${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">
1059
+ What is 42 \xD7 17?
1060
+ </button>
1061
+ <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">
1062
+ What day is it?
1063
+ </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">
1064
+ Summarize main topics
1065
+ </button>
1066
+ <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">
1067
+ Key findings
1068
+ </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">
1069
+ Hello! What can you do?
1070
+ </button>
1071
+ <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">
1072
+ Tell me a fun fact
1073
+ </button>`}
1074
+ </div>
1075
+ </div>
1076
+ )}
1077
+ {messages.map((msg) => (
1078
+ <div key={msg.id} className={\`mb-6 flex \${msg.role === "user" ? "justify-end" : "justify-start"}\`}>
1079
+ <div className={\`flex items-start gap-3 max-w-[80%] \${msg.role === "user" ? "flex-row-reverse" : ""}\`}>
1080
+ <div className={\`w-8 h-8 rounded-full flex items-center justify-center text-sm shrink-0 \${
1081
+ msg.role === "user" ? "bg-${accentColor}-500 text-white" : "bg-white/10 text-muted"
1082
+ }\`}>
1083
+ {msg.role === "user" ? "Y" : "\u26A1"}
1084
+ </div>
1085
+ <div className={\`px-4 py-3 rounded-2xl whitespace-pre-wrap leading-relaxed text-sm \${
1086
+ msg.role === "user"
1087
+ ? "bg-${accentColor}-500 text-white rounded-br-md"
1088
+ : "bg-white/[0.05] border border-border rounded-bl-md"
1089
+ }\`}>
1090
+ {msg.content || (isLoading && msg.role === "assistant" ? (
1091
+ <span className="flex gap-1">
1092
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:0ms]" />
1093
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:150ms]" />
1094
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:300ms]" />
1095
+ </span>
1096
+ ) : "")}
1097
+ </div>
1098
+ </div>
1099
+ </div>
1100
+ ))}
1101
+ <div ref={messagesEndRef} />
1102
+ </div>
1103
+ </main>
1104
+ <footer className="border-t border-border px-4 py-4">
1105
+ <form onSubmit={(e) => { e.preventDefault(); sendMessage(); }} className="max-w-3xl mx-auto flex gap-3">
1106
+ <input
1107
+ ref={inputRef}
1108
+ value={input}
1109
+ onChange={(e) => setInput(e.target.value)}
1110
+ placeholder="${inputPlaceholder}"
1111
+ disabled={isLoading}
1112
+ 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"
1113
+ />
1114
+ <button
1115
+ type="submit"
1116
+ disabled={isLoading || !input.trim()}
1117
+ 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"
1118
+ >
1119
+ {isLoading ? (
1120
+ <svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
1121
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
1122
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
1123
+ </svg>
1124
+ ) : "Send"}
1125
+ </button>
1126
+ </form>
1127
+ </footer>
1128
+ </div>
1129
+ );
1130
+ }
1131
+ `;
1132
+ }
1133
+ function generateFaviconSVG() {
1134
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1135
+ <rect width="32" height="32" rx="6" fill="#0a0a0a"/>
1136
+ <path d="M18.5 4L8 18h7l-1.5 10L24 14h-7l1.5-10z" fill="#facc15" stroke="#facc15" stroke-width="0.5" stroke-linejoin="round"/>
1137
+ </svg>
1138
+ `;
1139
+ }
1140
+ function generateRobotsTxt() {
1141
+ return `# https://www.robotstxt.org/robotstxt.html
1142
+ User-agent: *
1143
+ Allow: /
1144
+
1145
+ Sitemap: /sitemap.xml
1146
+ `;
1147
+ }
1148
+ function generateWebManifest(projectName) {
1149
+ return JSON.stringify({
1150
+ name: projectName,
1151
+ short_name: projectName,
1152
+ icons: [
1153
+ { src: "/favicon.svg", sizes: "any", type: "image/svg+xml" }
1154
+ ],
1155
+ theme_color: "#0a0a0a",
1156
+ background_color: "#0a0a0a",
1157
+ display: "standalone"
1158
+ }, null, 2) + "\n";
627
1159
  }
628
1160
  // Annotate the CommonJS export names for ESM import in node:
629
1161
  0 && (module.exports = {