@voltx/cli 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) 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-TFVNHM7S.mjs +1028 -0
  30. package/dist/chunk-UXI3QSDN.mjs +121 -0
  31. package/dist/chunk-VD3CNPNP.mjs +123 -0
  32. package/dist/chunk-X6VOAPRJ.mjs +756 -0
  33. package/dist/cli.js +935 -308
  34. package/dist/cli.mjs +7 -6
  35. package/dist/create.d.mts +1 -0
  36. package/dist/create.d.ts +1 -0
  37. package/dist/create.js +726 -192
  38. package/dist/create.mjs +1 -2
  39. package/dist/dev.d.mts +6 -4
  40. package/dist/dev.d.ts +6 -4
  41. package/dist/dev.js +119 -46
  42. package/dist/dev.mjs +1 -2
  43. package/dist/generate.js +13 -13
  44. package/dist/generate.mjs +1 -2
  45. package/dist/index.js +922 -296
  46. package/dist/index.mjs +5 -6
  47. package/dist/start.js +7 -17
  48. package/dist/start.mjs +1 -2
  49. package/dist/welcome.mjs +0 -1
  50. 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,41 @@ 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
+ }
202
+ devDeps["@voltx/cli"] = deps["@voltx/cli"] ?? v("@voltx/cli");
203
+ delete deps["@voltx/cli"];
171
204
  fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
172
205
  name,
173
206
  version: "0.1.0",
174
207
  private: true,
175
- scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
208
+ scripts: { dev: "npx voltx dev", build: "npx voltx build", start: "npx voltx start" },
176
209
  dependencies: deps,
177
- devDependencies: { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" }
210
+ devDependencies: devDeps
178
211
  }, null, 2));
179
212
  let config = `import { defineConfig } from "@voltx/core";
180
213
 
@@ -195,31 +228,53 @@ export default defineConfig({
195
228
  },`;
196
229
  config += `
197
230
  server: {
198
- routesDir: "src/routes",
231
+ routesDir: "api",
199
232
  staticDir: "public",
200
233
  cors: true,
201
234
  },
202
235
  });
203
236
  `;
204
237
  fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
205
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api"), { recursive: true });
238
+ fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
206
239
  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
- );
240
+ fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
241
+ fs.mkdirSync(path.join(targetDir, "src", "components"), { recursive: true });
242
+ fs.mkdirSync(path.join(targetDir, "src", "hooks"), { recursive: true });
243
+ fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
244
+ fs.writeFileSync(path.join(targetDir, "public", "favicon.svg"), generateFaviconSVG());
245
+ fs.writeFileSync(path.join(targetDir, "public", "robots.txt"), generateRobotsTxt());
246
+ fs.writeFileSync(path.join(targetDir, "public", "site.webmanifest"), generateWebManifest(name));
247
+ const tsconfig = {
248
+ compilerOptions: {
249
+ target: "ES2022",
250
+ module: "ESNext",
251
+ moduleResolution: "bundler",
252
+ strict: true,
253
+ esModuleInterop: true,
254
+ skipLibCheck: true,
255
+ outDir: "dist",
256
+ baseUrl: ".",
257
+ paths: { "@/*": ["./src/*"] },
258
+ jsx: "react-jsx"
259
+ },
260
+ include: ["src", "api", "server.ts", "voltx.config.ts"]
261
+ };
262
+ if (template === "agent-app") tsconfig.include.push("agents", "tools");
263
+ fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
264
+ fs.writeFileSync(path.join(targetDir, "server.ts"), generateServerEntry(name, template));
265
+ fs.writeFileSync(path.join(targetDir, "vite.config.ts"), generateViteConfigFile("server.ts"));
266
+ fs.writeFileSync(path.join(targetDir, "src", "entry-client.tsx"), generateEntryClient());
267
+ fs.writeFileSync(path.join(targetDir, "src", "entry-server.tsx"), generateEntryServer());
268
+ fs.writeFileSync(path.join(targetDir, "src", "layout.tsx"), generateLayoutComponent(name));
269
+ fs.writeFileSync(path.join(targetDir, "src", "globals.css"), generateGlobalCSS(shadcn));
270
+ if (shadcn) {
271
+ fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.ts"), generateCnUtil());
272
+ fs.writeFileSync(path.join(targetDir, "components.json"), generateComponentsJson());
273
+ }
274
+ fs.writeFileSync(path.join(targetDir, "src", "app.tsx"), generateAppComponent(name, template));
220
275
  fs.writeFileSync(
221
- path.join(targetDir, "src", "routes", "index.ts"),
222
- `// GET / \u2014 Health check
276
+ path.join(targetDir, "api", "index.ts"),
277
+ `// GET /api \u2014 Health check
223
278
  import type { Context } from "@voltx/server";
224
279
 
225
280
  export function GET(c: Context) {
@@ -229,7 +284,7 @@ export function GET(c: Context) {
229
284
  );
230
285
  if (template === "chatbot" || template === "agent-app") {
231
286
  fs.writeFileSync(
232
- path.join(targetDir, "src", "routes", "api", "chat.ts"),
287
+ path.join(targetDir, "api", "chat.ts"),
233
288
  `// POST /api/chat \u2014 Streaming chat with conversation memory
234
289
  import type { Context } from "@voltx/server";
235
290
  import { streamText } from "@voltx/ai";
@@ -263,9 +318,9 @@ export async function POST(c: Context) {
263
318
  );
264
319
  }
265
320
  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)
321
+ fs.mkdirSync(path.join(targetDir, "agents"), { recursive: true });
322
+ fs.mkdirSync(path.join(targetDir, "tools"), { recursive: true });
323
+ fs.writeFileSync(path.join(targetDir, "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
269
324
  import type { Tool } from "@voltx/agents";
270
325
 
271
326
  export const calculatorTool: Tool = {
@@ -290,7 +345,7 @@ export const calculatorTool: Tool = {
290
345
  },
291
346
  };
292
347
  `);
293
- fs.writeFileSync(path.join(targetDir, "src", "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
348
+ fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
294
349
  import type { Tool } from "@voltx/agents";
295
350
 
296
351
  export const datetimeTool: Tool = {
@@ -311,7 +366,7 @@ export const datetimeTool: Tool = {
311
366
  },
312
367
  };
313
368
  `);
314
- fs.writeFileSync(path.join(targetDir, "src", "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
369
+ fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
315
370
  import { createAgent } from "@voltx/agents";
316
371
  import { calculatorTool } from "../tools/calculator";
317
372
  import { datetimeTool } from "../tools/datetime";
@@ -325,10 +380,10 @@ export const assistant = createAgent({
325
380
  });
326
381
  `);
327
382
  fs.writeFileSync(
328
- path.join(targetDir, "src", "routes", "api", "agent.ts"),
383
+ path.join(targetDir, "api", "agent.ts"),
329
384
  `// POST /api/agent \u2014 Run the AI agent
330
385
  import type { Context } from "@voltx/server";
331
- import { assistant } from "../../agents/assistant";
386
+ import { assistant } from "../agents/assistant";
332
387
 
333
388
  export async function POST(c: Context) {
334
389
  const { input } = await c.req.json();
@@ -341,9 +396,9 @@ export async function POST(c: Context) {
341
396
  }
342
397
  if (template === "rag-app") {
343
398
  const embedModel = "openai:text-embedding-3-small";
344
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "rag"), { recursive: true });
399
+ fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
345
400
  fs.writeFileSync(
346
- path.join(targetDir, "src", "routes", "api", "rag", "query.ts"),
401
+ path.join(targetDir, "api", "rag", "query.ts"),
347
402
  `// POST /api/rag/query \u2014 Query documents with RAG
348
403
  import type { Context } from "@voltx/server";
349
404
  import { streamText } from "@voltx/ai";
@@ -367,7 +422,7 @@ export async function POST(c: Context) {
367
422
  `
368
423
  );
369
424
  fs.writeFileSync(
370
- path.join(targetDir, "src", "routes", "api", "rag", "ingest.ts"),
425
+ path.join(targetDir, "api", "rag", "ingest.ts"),
371
426
  `// POST /api/rag/ingest \u2014 Ingest documents into the vector store
372
427
  import type { Context } from "@voltx/server";
373
428
  import { createRAGPipeline, createEmbedder } from "@voltx/rag";
@@ -387,12 +442,12 @@ export async function POST(c: Context) {
387
442
  );
388
443
  }
389
444
  if (auth === "better-auth") {
390
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "auth"), { recursive: true });
445
+ fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
391
446
  fs.writeFileSync(
392
- path.join(targetDir, "src", "routes", "api", "auth", "[...path].ts"),
447
+ path.join(targetDir, "api", "auth", "[...path].ts"),
393
448
  `// ALL /api/auth/* \u2014 Better Auth handler
394
449
  import type { Context } from "@voltx/server";
395
- import { auth } from "../../../lib/auth";
450
+ import { auth } from "../../src/lib/auth";
396
451
  import { createAuthHandler } from "@voltx/auth";
397
452
 
398
453
  const handler = createAuthHandler(auth);
@@ -401,7 +456,6 @@ export const GET = (c: Context) => handler(c);
401
456
  export const POST = (c: Context) => handler(c);
402
457
  `
403
458
  );
404
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
405
459
  fs.writeFileSync(
406
460
  path.join(targetDir, "src", "lib", "auth.ts"),
407
461
  `import { createAuth, createAuthMiddleware } from "@voltx/auth";
@@ -418,7 +472,6 @@ export const authMiddleware = createAuthMiddleware({
418
472
  `
419
473
  );
420
474
  } else if (auth === "jwt") {
421
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
422
475
  fs.writeFileSync(
423
476
  path.join(targetDir, "src", "lib", "auth.ts"),
424
477
  `import { createAuth, createAuthMiddleware } from "@voltx/auth";
@@ -435,9 +488,9 @@ export const authMiddleware = createAuthMiddleware({
435
488
  `
436
489
  );
437
490
  fs.writeFileSync(
438
- path.join(targetDir, "src", "routes", "api", "auth.ts"),
491
+ path.join(targetDir, "api", "auth.ts"),
439
492
  `import type { Context } from "@voltx/server";
440
- import { jwt } from "../../lib/auth";
493
+ import { jwt } from "../src/lib/auth";
441
494
 
442
495
  export async function POST(c: Context) {
443
496
  const { email, password } = await c.req.json();
@@ -454,14 +507,6 @@ export async function POST(c: Context) {
454
507
  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
508
  } else if (template === "chatbot" || template === "agent-app") {
456
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\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
510
  } else {
466
511
  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
512
  }
@@ -472,158 +517,647 @@ export async function POST(c: Context) {
472
517
  }
473
518
  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
519
  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");
520
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
479
521
  printWelcomeBanner(name);
480
522
  }
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>
523
+ function generateServerEntry(projectName, template) {
524
+ const imports = [];
525
+ const mounts = [];
526
+ imports.push('import { GET as healthGET } from "./api/index";');
527
+ mounts.push('app.get("/api", healthGET);');
528
+ if (template === "chatbot" || template === "agent-app") {
529
+ imports.push('import { POST as chatPOST } from "./api/chat";');
530
+ mounts.push('app.post("/api/chat", chatPOST);');
531
+ }
532
+ if (template === "agent-app") {
533
+ imports.push('import { POST as agentPOST } from "./api/agent";');
534
+ mounts.push('app.post("/api/agent", agentPOST);');
535
+ }
536
+ if (template === "rag-app") {
537
+ imports.push('import { POST as ragQueryPOST } from "./api/rag/query";');
538
+ imports.push('import { POST as ragIngestPOST } from "./api/rag/ingest";');
539
+ mounts.push('app.post("/api/rag/query", ragQueryPOST);');
540
+ mounts.push('app.post("/api/rag/ingest", ragIngestPOST);');
541
+ }
542
+ return `import { Hono } from "hono";
543
+ import { serve } from "@hono/node-server";
544
+ import { serveStatic } from "@hono/node-server/serve-static";
545
+ import { registerSSR } from "@voltx/server";
546
+ import { loadEnv } from "@voltx/core";
547
+ ${imports.join("\n")}
548
+
549
+ loadEnv(process.env.NODE_ENV ?? "development");
550
+
551
+ const isProd = process.env.NODE_ENV === "production";
552
+ const app = new Hono();
553
+
554
+ // \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
555
+ ${mounts.join("\n")}
556
+
557
+ // \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
558
+ if (isProd) {
559
+ app.use("/assets/*", serveStatic({ root: "./dist/client/" }));
560
+ app.use("/favicon.svg", serveStatic({ root: "./public/" }));
561
+ app.use("/robots.txt", serveStatic({ root: "./public/" }));
562
+ app.use("/site.webmanifest", serveStatic({ root: "./public/" }));
563
+ }
564
+
565
+ // \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
566
+ registerSSR(app, null, {
567
+ title: "${projectName}",
568
+ entryServer: "src/entry-server.tsx",
569
+ entryClient: "src/entry-client.tsx",
570
+ });
571
+
572
+ export default app;
573
+
574
+ if (isProd) {
575
+ const port = Number(process.env.PORT) || 3000;
576
+ serve({ fetch: app.fetch, port }, (info) => {
577
+ console.log(\`\\n \u26A1 ${projectName} running at http://localhost:\${info.port}\\n\`);
578
+ });
579
+ }
580
+ `;
581
+ }
582
+ function generateViteConfigFile(entry) {
583
+ return `import { defineConfig } from "vite";
584
+ import devServer from "@hono/vite-dev-server";
585
+ import react from "@vitejs/plugin-react";
586
+ import tailwindcss from "@tailwindcss/vite";
587
+
588
+ export default defineConfig({
589
+ resolve: {
590
+ alias: {
591
+ "@": "/src",
592
+ },
593
+ },
594
+ plugins: [
595
+ react(),
596
+ tailwindcss(),
597
+ devServer({
598
+ entry: "${entry}",
599
+ exclude: [
600
+ /.*\\.tsx?($|\\?)/,
601
+ /.*\\.(s?css|less)($|\\?)/,
602
+ /.*\\.(svg|png|jpg|jpeg|gif|webp|ico)($|\\?)/,
603
+ /^\\/@.+$/,
604
+ /^\\/favicon\\.svg$/,
605
+ /^\\/node_modules\\/.*/,
606
+ /^\\/src\\/.*/,
607
+ ],
608
+ injectClientScript: false,
609
+ }),
610
+ ],
611
+ });
612
+ `;
613
+ }
614
+ function generateEntryClient() {
615
+ return `import React from "react";
616
+ import { hydrateRoot } from "react-dom/client";
617
+ import Layout from "./layout";
618
+ import App from "./app";
619
+ import "./globals.css";
620
+
621
+ hydrateRoot(
622
+ document.getElementById("root")!,
623
+ <React.StrictMode>
624
+ <Layout>
625
+ <App />
626
+ </Layout>
627
+ </React.StrictMode>
628
+ );
629
+ `;
630
+ }
631
+ function generateEntryServer() {
632
+ return `import React from "react";
633
+ import { renderToReadableStream } from "react-dom/server";
634
+ import Layout from "./layout";
635
+ import App from "./app";
636
+
637
+ export async function render(_url: string): Promise<ReadableStream> {
638
+ const stream = await renderToReadableStream(
639
+ <React.StrictMode>
640
+ <Layout>
641
+ <App />
642
+ </Layout>
643
+ </React.StrictMode>,
644
+ {
645
+ onError(error: unknown) {
646
+ console.error("[voltx] SSR render error:", error);
647
+ },
648
+ }
649
+ );
650
+ return stream;
651
+ }
652
+ `;
653
+ }
654
+ function generateLayoutComponent(projectName) {
655
+ return `import React from "react";
656
+
657
+ export default function Layout({ children }: { children: React.ReactNode }) {
658
+ return (
659
+ <div className="min-h-screen bg-background text-foreground font-sans">
660
+ <header className="border-b border-border px-6 py-3 flex items-center justify-between">
661
+ <div className="flex items-center gap-2">
662
+ <span className="text-xl">\u26A1</span>
663
+ <span className="font-semibold">${projectName}</span>
538
664
  </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>
665
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-muted text-sm hover:text-foreground transition-colors">
666
+ Built with VoltX
667
+ </a>
668
+ </header>
669
+ <main>{children}</main>
670
+ </div>
671
+ );
672
+ }
673
+ `;
674
+ }
675
+ function generateGlobalCSS(useShadcn = false) {
676
+ if (useShadcn) {
677
+ return `@import "tailwindcss";
678
+
679
+ @theme inline {
680
+ --radius-sm: 0.25rem;
681
+ --radius-md: 0.375rem;
682
+ --radius-lg: 0.5rem;
683
+ --radius-xl: 0.75rem;
684
+ --color-background: hsl(var(--background));
685
+ --color-foreground: hsl(var(--foreground));
686
+ --color-card: hsl(var(--card));
687
+ --color-card-foreground: hsl(var(--card-foreground));
688
+ --color-popover: hsl(var(--popover));
689
+ --color-popover-foreground: hsl(var(--popover-foreground));
690
+ --color-primary: hsl(var(--primary));
691
+ --color-primary-foreground: hsl(var(--primary-foreground));
692
+ --color-secondary: hsl(var(--secondary));
693
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
694
+ --color-muted: hsl(var(--muted));
695
+ --color-muted-foreground: hsl(var(--muted-foreground));
696
+ --color-accent: hsl(var(--accent));
697
+ --color-accent-foreground: hsl(var(--accent-foreground));
698
+ --color-destructive: hsl(var(--destructive));
699
+ --color-border: hsl(var(--border));
700
+ --color-input: hsl(var(--input));
701
+ --color-ring: hsl(var(--ring));
702
+ --color-chart-1: hsl(var(--chart-1));
703
+ --color-chart-2: hsl(var(--chart-2));
704
+ --color-chart-3: hsl(var(--chart-3));
705
+ --color-chart-4: hsl(var(--chart-4));
706
+ --color-chart-5: hsl(var(--chart-5));
707
+ --color-sidebar: hsl(var(--sidebar));
708
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
709
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
710
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
711
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
712
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
713
+ --color-sidebar-border: hsl(var(--sidebar-border));
714
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
715
+ }
716
+
717
+ :root {
718
+ --background: 0 0% 4%;
719
+ --foreground: 0 0% 93%;
720
+ --card: 0 0% 6%;
721
+ --card-foreground: 0 0% 93%;
722
+ --popover: 0 0% 6%;
723
+ --popover-foreground: 0 0% 93%;
724
+ --primary: 0 0% 93%;
725
+ --primary-foreground: 0 0% 6%;
726
+ --secondary: 0 0% 12%;
727
+ --secondary-foreground: 0 0% 93%;
728
+ --muted: 0 0% 12%;
729
+ --muted-foreground: 0 0% 55%;
730
+ --accent: 0 0% 12%;
731
+ --accent-foreground: 0 0% 93%;
732
+ --destructive: 0 62% 50%;
733
+ --border: 0 0% 14%;
734
+ --input: 0 0% 14%;
735
+ --ring: 0 0% 83%;
736
+ --chart-1: 220 70% 50%;
737
+ --chart-2: 160 60% 45%;
738
+ --chart-3: 30 80% 55%;
739
+ --chart-4: 280 65% 60%;
740
+ --chart-5: 340 75% 55%;
741
+ --sidebar: 0 0% 5%;
742
+ --sidebar-foreground: 0 0% 93%;
743
+ --sidebar-primary: 0 0% 93%;
744
+ --sidebar-primary-foreground: 0 0% 6%;
745
+ --sidebar-accent: 0 0% 12%;
746
+ --sidebar-accent-foreground: 0 0% 93%;
747
+ --sidebar-border: 0 0% 14%;
748
+ --sidebar-ring: 0 0% 83%;
749
+ }
750
+
751
+ *,
752
+ *::before,
753
+ *::after {
754
+ box-sizing: border-box;
755
+ margin: 0;
756
+ padding: 0;
757
+ border-color: var(--color-border);
758
+ }
759
+
760
+ html,
761
+ body {
762
+ height: 100%;
763
+ background: var(--color-background);
764
+ color: var(--color-foreground);
765
+ font-family: system-ui, -apple-system, sans-serif;
766
+ -webkit-font-smoothing: antialiased;
767
+ }
768
+
769
+ #root {
770
+ height: 100%;
771
+ }
772
+ `;
773
+ }
774
+ return `@import "tailwindcss";
775
+
776
+ @theme {
777
+ --color-background: #0a0a0a;
778
+ --color-foreground: #ededed;
779
+ --color-muted: #888888;
780
+ --color-border: #222222;
781
+ --color-primary: #2563eb;
782
+ --color-accent: #a78bfa;
783
+ --font-sans: system-ui, -apple-system, sans-serif;
784
+ }
785
+
786
+ *,
787
+ *::before,
788
+ *::after {
789
+ box-sizing: border-box;
790
+ margin: 0;
791
+ padding: 0;
792
+ }
793
+
794
+ html,
795
+ body {
796
+ height: 100%;
797
+ background: var(--color-background);
798
+ color: var(--color-foreground);
799
+ font-family: var(--font-sans);
800
+ -webkit-font-smoothing: antialiased;
801
+ }
802
+
803
+ #root {
804
+ height: 100%;
805
+ }
806
+
807
+ a {
808
+ color: inherit;
809
+ text-decoration: none;
810
+ }
811
+
812
+ a:hover {
813
+ text-decoration: underline;
814
+ }
815
+ `;
816
+ }
817
+ function generateCnUtil() {
818
+ return `import { type ClassValue, clsx } from "clsx";
819
+ import { twMerge } from "tailwind-merge";
820
+
821
+ export function cn(...inputs: ClassValue[]) {
822
+ return twMerge(clsx(inputs));
823
+ }
824
+ `;
825
+ }
826
+ function generateComponentsJson() {
827
+ return JSON.stringify({
828
+ "$schema": "https://ui.shadcn.com/schema.json",
829
+ style: "new-york",
830
+ rsc: false,
831
+ tsx: true,
832
+ tailwind: {
833
+ config: "",
834
+ css: "src/globals.css",
835
+ baseColor: "neutral",
836
+ cssVariables: true
837
+ },
838
+ aliases: {
839
+ components: "@/components",
840
+ utils: "@/lib/utils",
841
+ ui: "@/components/ui",
842
+ lib: "@/lib",
843
+ hooks: "@/hooks"
844
+ }
845
+ }, null, 2) + "\n";
846
+ }
847
+ function generateAppComponent(projectName, template) {
848
+ if (template === "blank") {
849
+ return `import React, { useState, useEffect } from "react";
850
+
851
+ export default function App() {
852
+ const [status, setStatus] = useState<string>("checking...");
853
+
854
+ useEffect(() => {
855
+ fetch("/api")
856
+ .then((res) => res.json())
857
+ .then((data) => setStatus(data.status || "ok"))
858
+ .catch(() => setStatus("error"));
859
+ }, []);
860
+
861
+ return (
862
+ <div className="flex flex-col items-center justify-center min-h-[calc(100vh-60px)] px-6 py-12">
863
+ <div className="text-center max-w-2xl w-full">
864
+ {/* Hero */}
865
+ <div className="relative mb-8">
866
+ <div className="absolute inset-0 blur-3xl opacity-20 bg-gradient-to-r from-blue-500 via-purple-500 to-blue-500 rounded-full" />
867
+ <div className="relative text-7xl mb-4">\u26A1</div>
544
868
  </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>
869
+ <h1 className="text-5xl font-bold tracking-tight mb-3 bg-gradient-to-r from-white to-white/60 bg-clip-text text-transparent">
870
+ \${"\${projectName}"}
871
+ </h1>
872
+ <p className="text-muted text-lg mb-10">The AI-first full-stack framework</p>
873
+
874
+ {/* Status cards */}
875
+ <div className="flex gap-4 justify-center mb-10">
876
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
877
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Server</div>
878
+ <div className={\`text-sm font-medium \${status === "ok" ? "text-emerald-400" : "text-red-400"}\`}>
879
+ <span className={\`inline-block w-2 h-2 rounded-full mr-2 \${status === "ok" ? "bg-emerald-400 animate-pulse" : "bg-red-400"}\`} />
880
+ {status}
881
+ </div>
562
882
  </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>
883
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
884
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Frontend</div>
885
+ <div className="text-sm font-medium text-emerald-400">
886
+ <span className="inline-block w-2 h-2 rounded-full mr-2 bg-emerald-400 animate-pulse" />
887
+ React + Vite
570
888
  </div>
571
889
  </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>
890
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
891
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">CSS</div>
892
+ <div className="text-sm font-medium text-sky-400">Tailwind v4</div>
577
893
  </div>
578
894
  </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>
895
+
896
+ {/* Get started */}
897
+ <div className="bg-white/[0.03] border border-border rounded-2xl p-8 text-left backdrop-blur-sm">
898
+ <h2 className="text-sm font-medium text-muted mb-6 uppercase tracking-wider">Get started</h2>
899
+ <div className="space-y-4">
900
+ <div className="flex items-start gap-4">
901
+ <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>
902
+ <div>
903
+ <code className="text-purple-400 text-sm">src/app.tsx</code>
904
+ <p className="text-muted text-sm mt-1">Edit this file to build your UI</p>
905
+ </div>
906
+ </div>
907
+ <div className="flex items-start gap-4">
908
+ <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>
909
+ <div>
910
+ <code className="text-blue-400 text-sm">api/</code>
911
+ <p className="text-muted text-sm mt-1">Add API routes here (file-based routing)</p>
912
+ </div>
913
+ </div>
914
+ <div className="flex items-start gap-4">
915
+ <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>
916
+ <div>
917
+ <code className="text-emerald-400 text-sm">src/components/</code>
918
+ <p className="text-muted text-sm mt-1">Create React components with Tailwind CSS</p>
919
+ </div>
920
+ </div>
595
921
  </div>
596
922
  </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>
923
+
924
+ {/* Links */}
925
+ <div className="flex gap-6 justify-center mt-8">
926
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
927
+ GitHub \u2192
928
+ </a>
929
+ <a href="https://voltx.co.in" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
930
+ Docs \u2192
931
+ </a>
602
932
  </div>
603
933
  </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);
934
+ </div>
935
+ );
936
+ }
937
+ `;
618
938
  }
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
- });
939
+ const apiEndpoint = template === "agent-app" ? "/api/agent" : template === "rag-app" ? "/api/rag/query" : "/api/chat";
940
+ const isAgent = template === "agent-app";
941
+ const isRag = template === "rag-app";
942
+ let emptyStateTitle = "Start a conversation";
943
+ let emptyStateHint = "Type a message below to chat with AI";
944
+ let accentColor = "blue";
945
+ let inputPlaceholder = "Type a message...";
946
+ if (isAgent) {
947
+ emptyStateTitle = "Talk to your AI agent";
948
+ emptyStateHint = "The agent can use tools like Calculator and Date/Time";
949
+ accentColor = "purple";
950
+ inputPlaceholder = "Ask the agent anything...";
951
+ } else if (isRag) {
952
+ emptyStateTitle = "Ask your documents";
953
+ emptyStateHint = "Query your knowledge base \u2014 ingest docs via POST /api/rag/ingest";
954
+ accentColor = "emerald";
955
+ inputPlaceholder = "Ask a question about your documents...";
956
+ }
957
+ return `import React, { useState, useRef, useEffect, useCallback } from "react";
958
+
959
+ interface Message {
960
+ id: string;
961
+ role: "user" | "assistant";
962
+ content: string;
963
+ }
964
+
965
+ export default function App() {
966
+ const [messages, setMessages] = useState<Message[]>([]);
967
+ const [input, setInput] = useState("");
968
+ const [isLoading, setIsLoading] = useState(false);
969
+ const messagesEndRef = useRef<HTMLDivElement>(null);
970
+ const inputRef = useRef<HTMLInputElement>(null);
971
+
972
+ useEffect(() => {
973
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
974
+ }, [messages]);
975
+
976
+ useEffect(() => {
977
+ inputRef.current?.focus();
978
+ }, []);
979
+
980
+ const sendMessage = useCallback(async () => {
981
+ const text = input.trim();
982
+ if (!text || isLoading) return;
983
+
984
+ const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: text };
985
+ setMessages((prev) => [...prev, userMsg]);
986
+ setInput("");
987
+ setIsLoading(true);
988
+
989
+ const assistantMsg: Message = { id: crypto.randomUUID(), role: "assistant", content: "" };
990
+ setMessages((prev) => [...prev, assistantMsg]);
991
+
992
+ try {${isAgent ? `
993
+ const res = await fetch("${apiEndpoint}", {
994
+ method: "POST",
995
+ headers: { "Content-Type": "application/json" },
996
+ body: JSON.stringify({ input: text }),
997
+ });
998
+ const data = await res.json();
999
+ setMessages((prev) =>
1000
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: data.content || "No response" } : m)
1001
+ );` : `
1002
+ const res = await fetch("${apiEndpoint}", {
1003
+ method: "POST",
1004
+ headers: { "Content-Type": "application/json" },
1005
+ body: JSON.stringify({${isRag ? ` question: text ` : ` messages: [...messages, { role: "user", content: text }] `}}),
1006
+ });
1007
+
1008
+ if (!res.body) throw new Error("No response body");
1009
+
1010
+ const reader = res.body.getReader();
1011
+ const decoder = new TextDecoder();
1012
+ let buffer = "";
1013
+ let fullContent = "";
1014
+
1015
+ while (true) {
1016
+ const { done, value } = await reader.read();
1017
+ if (done) break;
1018
+ buffer += decoder.decode(value, { stream: true });
1019
+ const lines = buffer.split("\\n");
1020
+ buffer = lines.pop() ?? "";
1021
+
1022
+ for (const line of lines) {
1023
+ if (!line.startsWith("data: ")) continue;
1024
+ const data = line.slice(6);
1025
+ if (data === "[DONE]") break;
1026
+ try {
1027
+ const parsed = JSON.parse(data);
1028
+ const chunk = parsed.textDelta ?? parsed.content ?? parsed.choices?.[0]?.delta?.content ?? "";
1029
+ if (chunk) {
1030
+ fullContent += chunk;
1031
+ setMessages((prev) =>
1032
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: fullContent } : m)
1033
+ );
1034
+ }
1035
+ } catch {}
1036
+ }
1037
+ }`}
1038
+ } catch (err) {
1039
+ setMessages((prev) =>
1040
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: "Error: " + (err instanceof Error ? err.message : String(err)) } : m)
1041
+ );
1042
+ } finally {
1043
+ setIsLoading(false);
1044
+ }
1045
+ }, [input, isLoading, messages]);
1046
+
1047
+ return (
1048
+ <div className="h-[calc(100vh-60px)] flex flex-col">
1049
+ <main className="flex-1 overflow-y-auto px-4 py-6">
1050
+ <div className="max-w-3xl mx-auto">
1051
+ {messages.length === 0 && (
1052
+ <div className="flex flex-col items-center justify-center h-[60vh] text-center">
1053
+ <div className="relative mb-6">
1054
+ <div className="absolute inset-0 blur-2xl opacity-20 bg-${accentColor}-500 rounded-full" />
1055
+ <div className="relative text-5xl">\u26A1</div>
1056
+ </div>
1057
+ <h2 className="text-2xl font-semibold mb-2">${emptyStateTitle}</h2>
1058
+ <p className="text-muted text-sm max-w-md">${emptyStateHint}</p>
1059
+ <div className="flex gap-2 mt-6">
1060
+ ${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">
1061
+ What is 42 \xD7 17?
1062
+ </button>
1063
+ <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">
1064
+ What day is it?
1065
+ </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">
1066
+ Summarize main topics
1067
+ </button>
1068
+ <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">
1069
+ Key findings
1070
+ </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">
1071
+ Hello! What can you do?
1072
+ </button>
1073
+ <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">
1074
+ Tell me a fun fact
1075
+ </button>`}
1076
+ </div>
1077
+ </div>
1078
+ )}
1079
+ {messages.map((msg) => (
1080
+ <div key={msg.id} className={\`mb-6 flex \${msg.role === "user" ? "justify-end" : "justify-start"}\`}>
1081
+ <div className={\`flex items-start gap-3 max-w-[80%] \${msg.role === "user" ? "flex-row-reverse" : ""}\`}>
1082
+ <div className={\`w-8 h-8 rounded-full flex items-center justify-center text-sm shrink-0 \${
1083
+ msg.role === "user" ? "bg-${accentColor}-500 text-white" : "bg-white/10 text-muted"
1084
+ }\`}>
1085
+ {msg.role === "user" ? "Y" : "\u26A1"}
1086
+ </div>
1087
+ <div className={\`px-4 py-3 rounded-2xl whitespace-pre-wrap leading-relaxed text-sm \${
1088
+ msg.role === "user"
1089
+ ? "bg-${accentColor}-500 text-white rounded-br-md"
1090
+ : "bg-white/[0.05] border border-border rounded-bl-md"
1091
+ }\`}>
1092
+ {msg.content || (isLoading && msg.role === "assistant" ? (
1093
+ <span className="flex gap-1">
1094
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:0ms]" />
1095
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:150ms]" />
1096
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:300ms]" />
1097
+ </span>
1098
+ ) : "")}
1099
+ </div>
1100
+ </div>
1101
+ </div>
1102
+ ))}
1103
+ <div ref={messagesEndRef} />
1104
+ </div>
1105
+ </main>
1106
+ <footer className="border-t border-border px-4 py-4">
1107
+ <form onSubmit={(e) => { e.preventDefault(); sendMessage(); }} className="max-w-3xl mx-auto flex gap-3">
1108
+ <input
1109
+ ref={inputRef}
1110
+ value={input}
1111
+ onChange={(e) => setInput(e.target.value)}
1112
+ placeholder="${inputPlaceholder}"
1113
+ disabled={isLoading}
1114
+ 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"
1115
+ />
1116
+ <button
1117
+ type="submit"
1118
+ disabled={isLoading || !input.trim()}
1119
+ 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"
1120
+ >
1121
+ {isLoading ? (
1122
+ <svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
1123
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
1124
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
1125
+ </svg>
1126
+ ) : "Send"}
1127
+ </button>
1128
+ </form>
1129
+ </footer>
1130
+ </div>
1131
+ );
1132
+ }
1133
+ `;
1134
+ }
1135
+ function generateFaviconSVG() {
1136
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1137
+ <rect width="32" height="32" rx="6" fill="#0a0a0a"/>
1138
+ <path d="M18.5 4L8 18h7l-1.5 10L24 14h-7l1.5-10z" fill="#facc15" stroke="#facc15" stroke-width="0.5" stroke-linejoin="round"/>
1139
+ </svg>
1140
+ `;
1141
+ }
1142
+ function generateRobotsTxt() {
1143
+ return `# https://www.robotstxt.org/robotstxt.html
1144
+ User-agent: *
1145
+ Allow: /
1146
+
1147
+ Sitemap: /sitemap.xml
1148
+ `;
1149
+ }
1150
+ function generateWebManifest(projectName) {
1151
+ return JSON.stringify({
1152
+ name: projectName,
1153
+ short_name: projectName,
1154
+ icons: [
1155
+ { src: "/favicon.svg", sizes: "any", type: "image/svg+xml" }
1156
+ ],
1157
+ theme_color: "#0a0a0a",
1158
+ background_color: "#0a0a0a",
1159
+ display: "standalone"
1160
+ }, null, 2) + "\n";
627
1161
  }
628
1162
  // Annotate the CommonJS export names for ESM import in node:
629
1163
  0 && (module.exports = {