@voltx/cli 0.3.3 → 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 (58) hide show
  1. package/README.md +68 -14
  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-4T26KROZ.mjs +456 -0
  8. package/dist/chunk-5DVBIJXU.mjs +84 -0
  9. package/dist/chunk-5RN2FYST.mjs +494 -0
  10. package/dist/chunk-65PVXS4D.mjs +894 -0
  11. package/dist/chunk-7JVIEGSA.mjs +141 -0
  12. package/dist/chunk-A4NA4AJN.mjs +786 -0
  13. package/dist/chunk-AAAHANST.mjs +839 -0
  14. package/dist/chunk-AD3WMFZF.mjs +205 -0
  15. package/dist/chunk-CFWXMM7Q.mjs +450 -0
  16. package/dist/chunk-CSSHLVYS.mjs +83 -0
  17. package/dist/chunk-CWOSNV5O.mjs +150 -0
  18. package/dist/chunk-EI6XBYKB.mjs +84 -0
  19. package/dist/chunk-EXMRIKIX.mjs +404 -0
  20. package/dist/chunk-FI2W4L4S.mjs +205 -0
  21. package/dist/chunk-FN7KZJ6H.mjs +363 -0
  22. package/dist/chunk-G2INQCCJ.mjs +907 -0
  23. package/dist/chunk-H2DTIOEO.mjs +150 -0
  24. package/dist/chunk-IS2WTE3C.mjs +138 -0
  25. package/dist/chunk-JECCDBYI.mjs +730 -0
  26. package/dist/chunk-KX2MRJUO.mjs +795 -0
  27. package/dist/chunk-LTGMHAZS.mjs +147 -0
  28. package/dist/chunk-OPO6RUFP.mjs +698 -0
  29. package/dist/chunk-P5FSO2UH.mjs +497 -0
  30. package/dist/chunk-PWQSKYAM.mjs +682 -0
  31. package/dist/chunk-Q5XCFN7L.mjs +1026 -0
  32. package/dist/chunk-QSU6FZC7.mjs +497 -0
  33. package/dist/chunk-RYWRFHEC.mjs +83 -0
  34. package/dist/chunk-SU4Q3PTH.mjs +201 -0
  35. package/dist/chunk-UO43CY7Y.mjs +497 -0
  36. package/dist/chunk-UXI3QSDN.mjs +121 -0
  37. package/dist/chunk-VD3CNPNP.mjs +123 -0
  38. package/dist/chunk-X6VOAPRJ.mjs +756 -0
  39. package/dist/cli.js +1023 -306
  40. package/dist/cli.mjs +7 -6
  41. package/dist/create.d.mts +1 -0
  42. package/dist/create.d.ts +1 -0
  43. package/dist/create.js +813 -189
  44. package/dist/create.mjs +1 -2
  45. package/dist/dev.d.mts +6 -4
  46. package/dist/dev.d.ts +6 -4
  47. package/dist/dev.js +119 -46
  48. package/dist/dev.mjs +1 -2
  49. package/dist/generate.js +13 -13
  50. package/dist/generate.mjs +1 -2
  51. package/dist/index.d.mts +1 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.js +1010 -294
  54. package/dist/index.mjs +6 -7
  55. package/dist/start.js +7 -17
  56. package/dist/start.mjs +1 -2
  57. package/dist/welcome.mjs +0 -1
  58. package/package.json +11 -3
package/dist/create.js CHANGED
@@ -142,70 +142,72 @@ function printWelcomeBanner(projectName) {
142
142
  }
143
143
 
144
144
  // src/create.ts
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
+ }
159
+ var TEMPLATE_DEPS = {
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") }
164
+ };
145
165
  async function createProject(options) {
146
- const { name, template = "blank", auth = "none" } = options;
166
+ const { name, template = "blank", auth = "none", shadcn = false } = options;
147
167
  const targetDir = path.resolve(process.cwd(), name);
148
168
  if (fs.existsSync(targetDir)) {
149
169
  console.error(`[voltx] Directory "${name}" already exists.`);
150
170
  process.exit(1);
151
171
  }
152
172
  fs.mkdirSync(targetDir, { recursive: true });
153
- const templateDeps = {
154
- blank: {
155
- "@voltx/core": "^0.3.0",
156
- "@voltx/server": "^0.3.0"
157
- },
158
- chatbot: {
159
- "@voltx/core": "^0.3.0",
160
- "@voltx/ai": "^0.3.0",
161
- "@voltx/server": "^0.3.0",
162
- "@voltx/memory": "^0.3.0"
163
- },
164
- "rag-app": {
165
- "@voltx/core": "^0.3.0",
166
- "@voltx/ai": "^0.3.0",
167
- "@voltx/server": "^0.3.0",
168
- "@voltx/rag": "^0.3.0",
169
- "@voltx/db": "^0.3.0"
170
- },
171
- "agent-app": {
172
- "@voltx/core": "^0.3.0",
173
- "@voltx/ai": "^0.3.0",
174
- "@voltx/server": "^0.3.0",
175
- "@voltx/agents": "^0.3.0",
176
- "@voltx/memory": "^0.3.0"
177
- }
178
- };
179
- const packageJson = {
173
+ const provider = template === "rag-app" ? "openai" : "cerebras";
174
+ const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
175
+ const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
176
+ const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": v("@voltx/cli") };
177
+ if (auth === "better-auth") {
178
+ deps["@voltx/auth"] = v("@voltx/auth");
179
+ deps["better-auth"] = "^1.5.0";
180
+ } else if (auth === "jwt") {
181
+ deps["@voltx/auth"] = v("@voltx/auth");
182
+ deps["jose"] = "^6.0.0";
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
+ fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
180
203
  name,
181
204
  version: "0.1.0",
182
205
  private: true,
183
- scripts: {
184
- dev: "voltx dev",
185
- build: "voltx build",
186
- start: "voltx start"
187
- },
188
- dependencies: {
189
- ...templateDeps[template] ?? templateDeps["blank"],
190
- "@voltx/cli": "^0.3.0",
191
- ...auth === "better-auth" ? { "@voltx/auth": "^0.3.0", "better-auth": "^1.5.0" } : {},
192
- ...auth === "jwt" ? { "@voltx/auth": "^0.3.0", "jose": "^6.0.0" } : {}
193
- },
194
- devDependencies: {
195
- typescript: "^5.7.0",
196
- tsx: "^4.21.0",
197
- tsup: "^8.0.0",
198
- "@types/node": "^22.0.0"
199
- }
200
- };
201
- fs.writeFileSync(
202
- path.join(targetDir, "package.json"),
203
- JSON.stringify(packageJson, null, 2)
204
- );
205
- const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
206
- const provider = template === "rag-app" ? "openai" : "cerebras";
207
- const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
208
- let configContent = `import { defineConfig } from "@voltx/core";
206
+ scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
207
+ dependencies: deps,
208
+ devDependencies: devDeps
209
+ }, null, 2));
210
+ let config = `import { defineConfig } from "@voltx/core";
209
211
 
210
212
  export default defineConfig({
211
213
  name: "${name}",
@@ -214,41 +216,63 @@ export default defineConfig({
214
216
  provider: "${provider}",
215
217
  model: "${model}",
216
218
  },`;
217
- if (hasDb) {
218
- configContent += `
219
+ if (hasDb) config += `
219
220
  db: {
220
221
  url: process.env.DATABASE_URL,
221
222
  },`;
222
- }
223
- if (auth !== "none") {
224
- configContent += `
223
+ if (auth !== "none") config += `
225
224
  auth: {
226
225
  provider: "${auth}",
227
226
  },`;
228
- }
229
- configContent += `
227
+ config += `
230
228
  server: {
231
- routesDir: "src/routes",
229
+ routesDir: "api",
232
230
  staticDir: "public",
233
231
  cors: true,
234
232
  },
235
233
  });
236
234
  `;
237
- fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), configContent);
238
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api"), { recursive: true });
235
+ fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
236
+ fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
239
237
  fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
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));
240
273
  fs.writeFileSync(
241
- path.join(targetDir, "src", "index.ts"),
242
- `import { createApp } from "@voltx/core";
243
- import config from "../voltx.config";
244
-
245
- const app = createApp(config);
246
- app.start();
247
- `
248
- );
249
- fs.writeFileSync(
250
- path.join(targetDir, "src", "routes", "index.ts"),
251
- `// GET / \u2014 Health check
274
+ path.join(targetDir, "api", "index.ts"),
275
+ `// GET /api \u2014 Health check
252
276
  import type { Context } from "@voltx/server";
253
277
 
254
278
  export function GET(c: Context) {
@@ -258,25 +282,22 @@ export function GET(c: Context) {
258
282
  );
259
283
  if (template === "chatbot" || template === "agent-app") {
260
284
  fs.writeFileSync(
261
- path.join(targetDir, "src", "routes", "api", "chat.ts"),
285
+ path.join(targetDir, "api", "chat.ts"),
262
286
  `// POST /api/chat \u2014 Streaming chat with conversation memory
263
287
  import type { Context } from "@voltx/server";
264
288
  import { streamText } from "@voltx/ai";
265
289
  import { createMemory } from "@voltx/memory";
266
290
 
267
- // In-memory for dev; swap to createMemory("postgres", { url }) for production
268
291
  const memory = createMemory({ maxMessages: 50 });
269
292
 
270
293
  export async function POST(c: Context) {
271
294
  const { messages, conversationId = "default" } = await c.req.json();
272
295
 
273
- // Store the latest user message
274
296
  const lastMessage = messages[messages.length - 1];
275
297
  if (lastMessage?.role === "user") {
276
298
  await memory.add(conversationId, { role: "user", content: lastMessage.content });
277
299
  }
278
300
 
279
- // Get conversation history from memory
280
301
  const history = await memory.get(conversationId);
281
302
 
282
303
  const result = await streamText({
@@ -285,7 +306,6 @@ export async function POST(c: Context) {
285
306
  messages: history.map((m) => ({ role: m.role, content: m.content })),
286
307
  });
287
308
 
288
- // Store assistant response after stream completes
289
309
  result.text.then(async (text) => {
290
310
  await memory.add(conversationId, { role: "assistant", content: text });
291
311
  });
@@ -296,44 +316,72 @@ export async function POST(c: Context) {
296
316
  );
297
317
  }
298
318
  if (template === "agent-app") {
299
- fs.mkdirSync(path.join(targetDir, "src", "agents"), { recursive: true });
300
- fs.mkdirSync(path.join(targetDir, "src", "tools"), { recursive: true });
301
- fs.writeFileSync(
302
- path.join(targetDir, "src", "agents", "assistant.ts"),
303
- `import { createAgent } from "@voltx/agents";
304
- import { searchTool } from "../tools/search";
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)
322
+ import type { Tool } from "@voltx/agents";
305
323
 
306
- export const assistant = createAgent({
307
- name: "assistant",
308
- model: "${provider}:${model}",
309
- instructions: "You are a helpful AI assistant. Use your tools when needed.",
310
- tools: [searchTool],
311
- maxIterations: 5,
312
- });
313
- `
314
- );
315
- fs.writeFileSync(
316
- path.join(targetDir, "src", "tools", "search.ts"),
317
- `import type { Tool } from "@voltx/agents";
324
+ export const calculatorTool: Tool = {
325
+ name: "calculator",
326
+ description: "Evaluate a math expression. Supports +, -, *, /, %, parentheses, and Math functions.",
327
+ parameters: {
328
+ type: "object",
329
+ properties: { expression: { type: "string", description: "The math expression to evaluate" } },
330
+ required: ["expression"],
331
+ },
332
+ async execute(args: { expression: string }) {
333
+ try {
334
+ const safe = args.expression.replace(/[^0-9+\\-*/.()%\\s,]|(?<!Math)\\.[a-z]/gi, (match) => {
335
+ if (args.expression.includes("Math.")) return match;
336
+ throw new Error("Invalid character: " + match);
337
+ });
338
+ const result = new Function("return " + safe)();
339
+ return \`\${args.expression} = \${result}\`;
340
+ } catch (err) {
341
+ return \`Error: \${err instanceof Error ? err.message : String(err)}\`;
342
+ }
343
+ },
344
+ };
345
+ `);
346
+ fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
347
+ import type { Tool } from "@voltx/agents";
318
348
 
319
- export const searchTool: Tool = {
320
- name: "search",
321
- description: "Search for information on a topic.",
349
+ export const datetimeTool: Tool = {
350
+ name: "datetime",
351
+ description: "Get the current date, time, day of week, and timezone.",
322
352
  parameters: {
323
353
  type: "object",
324
- properties: { query: { type: "string", description: "The search query" } },
325
- required: ["query"],
354
+ properties: { timezone: { type: "string", description: "Optional IANA timezone. Defaults to server timezone." } },
326
355
  },
327
- async execute(args: { query: string }) {
328
- return \`Search results for "\${args.query}": Placeholder \u2014 connect a real search API.\`;
356
+ async execute(args: { timezone?: string }) {
357
+ const now = new Date();
358
+ const tz = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
359
+ const formatted = now.toLocaleString("en-US", {
360
+ timeZone: tz, weekday: "long", year: "numeric", month: "long", day: "numeric",
361
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true,
362
+ });
363
+ return \`Current date/time (\${tz}): \${formatted}\`;
329
364
  },
330
365
  };
331
- `
332
- );
366
+ `);
367
+ fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
368
+ import { createAgent } from "@voltx/agents";
369
+ import { calculatorTool } from "../tools/calculator";
370
+ import { datetimeTool } from "../tools/datetime";
371
+
372
+ export const assistant = createAgent({
373
+ name: "assistant",
374
+ model: "${provider}:${model}",
375
+ instructions: "You are a helpful AI assistant with access to tools: Calculator, Date & Time. Use them when needed to answer questions accurately.",
376
+ tools: [calculatorTool, datetimeTool],
377
+ maxIterations: 5,
378
+ });
379
+ `);
333
380
  fs.writeFileSync(
334
- path.join(targetDir, "src", "routes", "api", "agent.ts"),
335
- `import type { Context } from "@voltx/server";
336
- import { assistant } from "../../agents/assistant";
381
+ path.join(targetDir, "api", "agent.ts"),
382
+ `// POST /api/agent \u2014 Run the AI agent
383
+ import type { Context } from "@voltx/server";
384
+ import { assistant } from "../agents/assistant";
337
385
 
338
386
  export async function POST(c: Context) {
339
387
  const { input } = await c.req.json();
@@ -346,36 +394,33 @@ export async function POST(c: Context) {
346
394
  }
347
395
  if (template === "rag-app") {
348
396
  const embedModel = "openai:text-embedding-3-small";
349
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "rag"), { recursive: true });
397
+ fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
350
398
  fs.writeFileSync(
351
- path.join(targetDir, "src", "routes", "api", "rag", "query.ts"),
399
+ path.join(targetDir, "api", "rag", "query.ts"),
352
400
  `// POST /api/rag/query \u2014 Query documents with RAG
353
401
  import type { Context } from "@voltx/server";
354
402
  import { streamText } from "@voltx/ai";
355
403
  import { createRAGPipeline, createEmbedder } from "@voltx/rag";
356
404
  import { createVectorStore } from "@voltx/db";
357
405
 
358
- const vectorStore = createVectorStore(); // swap to "pinecone" or "pgvector" for production
406
+ const vectorStore = createVectorStore();
359
407
  const embedder = createEmbedder({ model: "${embedModel}" });
360
408
  const rag = createRAGPipeline({ embedder, vectorStore });
361
409
 
362
410
  export async function POST(c: Context) {
363
411
  const { question } = await c.req.json();
364
-
365
412
  const context = await rag.getContext(question, { topK: 5 });
366
-
367
413
  const result = await streamText({
368
414
  model: "${provider}:${model}",
369
- system: \`Answer the user's question based on the following context. If the context doesn't contain relevant information, say so.\\n\\nContext:\\n\${context}\`,
415
+ system: \`Answer based on context. If not relevant, say so.\\n\\nContext:\\n\${context}\`,
370
416
  messages: [{ role: "user", content: question }],
371
417
  });
372
-
373
418
  return result.toSSEResponse();
374
419
  }
375
420
  `
376
421
  );
377
422
  fs.writeFileSync(
378
- path.join(targetDir, "src", "routes", "api", "rag", "ingest.ts"),
423
+ path.join(targetDir, "api", "rag", "ingest.ts"),
379
424
  `// POST /api/rag/ingest \u2014 Ingest documents into the vector store
380
425
  import type { Context } from "@voltx/server";
381
426
  import { createRAGPipeline, createEmbedder } from "@voltx/rag";
@@ -387,11 +432,7 @@ const rag = createRAGPipeline({ embedder, vectorStore });
387
432
 
388
433
  export async function POST(c: Context) {
389
434
  const { text, idPrefix } = await c.req.json();
390
-
391
- if (!text || typeof text !== "string") {
392
- return c.json({ error: "Missing 'text' field" }, 400);
393
- }
394
-
435
+ if (!text || typeof text !== "string") return c.json({ error: "Missing 'text' field" }, 400);
395
436
  const result = await rag.ingest(text, idPrefix ?? "doc");
396
437
  return c.json({ status: "ok", chunks: result.chunks, ids: result.ids });
397
438
  }
@@ -399,12 +440,12 @@ export async function POST(c: Context) {
399
440
  );
400
441
  }
401
442
  if (auth === "better-auth") {
402
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "auth"), { recursive: true });
443
+ fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
403
444
  fs.writeFileSync(
404
- path.join(targetDir, "src", "routes", "api", "auth", "[...path].ts"),
445
+ path.join(targetDir, "api", "auth", "[...path].ts"),
405
446
  `// ALL /api/auth/* \u2014 Better Auth handler
406
447
  import type { Context } from "@voltx/server";
407
- import { auth } from "../../../lib/auth";
448
+ import { auth } from "../../src/lib/auth";
408
449
  import { createAuthHandler } from "@voltx/auth";
409
450
 
410
451
  const handler = createAuthHandler(auth);
@@ -413,11 +454,9 @@ export const GET = (c: Context) => handler(c);
413
454
  export const POST = (c: Context) => handler(c);
414
455
  `
415
456
  );
416
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
417
457
  fs.writeFileSync(
418
458
  path.join(targetDir, "src", "lib", "auth.ts"),
419
- `// Auth configuration \u2014 Better Auth with DB-backed sessions
420
- import { createAuth, createAuthMiddleware } from "@voltx/auth";
459
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
421
460
 
422
461
  export const auth = createAuth("better-auth", {
423
462
  database: process.env.DATABASE_URL!,
@@ -431,11 +470,9 @@ export const authMiddleware = createAuthMiddleware({
431
470
  `
432
471
  );
433
472
  } else if (auth === "jwt") {
434
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
435
473
  fs.writeFileSync(
436
474
  path.join(targetDir, "src", "lib", "auth.ts"),
437
- `// Auth configuration \u2014 JWT (stateless)
438
- import { createAuth, createAuthMiddleware } from "@voltx/auth";
475
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
439
476
 
440
477
  export const jwt = createAuth("jwt", {
441
478
  secret: process.env.JWT_SECRET!,
@@ -449,18 +486,13 @@ export const authMiddleware = createAuthMiddleware({
449
486
  `
450
487
  );
451
488
  fs.writeFileSync(
452
- path.join(targetDir, "src", "routes", "api", "auth.ts"),
453
- `// POST /api/auth/login \u2014 Example JWT login route
454
- import type { Context } from "@voltx/server";
455
- import { jwt } from "../../lib/auth";
489
+ path.join(targetDir, "api", "auth.ts"),
490
+ `import type { Context } from "@voltx/server";
491
+ import { jwt } from "../src/lib/auth";
456
492
 
457
493
  export async function POST(c: Context) {
458
494
  const { email, password } = await c.req.json();
459
-
460
- if (!email || !password) {
461
- return c.json({ error: "Email and password are required" }, 400);
462
- }
463
-
495
+ if (!email || !password) return c.json({ error: "Email and password are required" }, 400);
464
496
  const token = await jwt.sign({ sub: email, email });
465
497
  return c.json({ token });
466
498
  }
@@ -470,69 +502,661 @@ export async function POST(c: Context) {
470
502
  let envContent = "";
471
503
  if (template === "rag-app") {
472
504
  envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nOPENAI_API_KEY=sk-...\n\n";
473
- envContent += "# \u2500\u2500\u2500 Database (Neon Postgres) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require\n\n";
474
- envContent += "# \u2500\u2500\u2500 Vector Database (Pinecone) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nPINECONE_API_KEY=pc-...\nPINECONE_INDEX=voltx-embeddings\n\n";
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";
475
506
  } else if (template === "chatbot" || template === "agent-app") {
476
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";
477
- if (template === "agent-app") {
478
- envContent += "# \u2500\u2500\u2500 Database (Neon Postgres \u2014 optional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
479
- }
480
508
  } else {
481
- envContent += "# \u2500\u2500\u2500 LLM Provider (add your key) \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";
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";
482
510
  }
483
511
  if (auth === "better-auth") {
484
- envContent += "# \u2500\u2500\u2500 Auth (Better Auth) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nBETTER_AUTH_SECRET=your-secret-key-min-32-chars-here\nBETTER_AUTH_URL=http://localhost:3000\n";
485
- if (template !== "rag-app" && template !== "agent-app") {
486
- envContent += "DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require\n";
487
- }
488
- envContent += "# GITHUB_CLIENT_ID=\n# GITHUB_CLIENT_SECRET=\n\n";
512
+ envContent += "# \u2500\u2500\u2500 Auth (Better Auth) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nBETTER_AUTH_SECRET=your-secret-key-min-32-chars-here\nBETTER_AUTH_URL=http://localhost:3000\n\n";
489
513
  } else if (auth === "jwt") {
490
514
  envContent += "# \u2500\u2500\u2500 Auth (JWT) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nJWT_SECRET=your-jwt-secret-key\n\n";
491
515
  }
492
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";
493
517
  fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
494
- fs.writeFileSync(
495
- path.join(targetDir, ".gitignore"),
496
- "node_modules\ndist\n.env\n"
497
- );
498
- fs.writeFileSync(
499
- path.join(targetDir, "tsconfig.json"),
500
- JSON.stringify(
501
- {
502
- compilerOptions: {
503
- target: "ES2022",
504
- module: "ESNext",
505
- moduleResolution: "bundler",
506
- strict: true,
507
- esModuleInterop: true,
508
- skipLibCheck: true,
509
- outDir: "dist",
510
- rootDir: "src"
511
- },
512
- include: ["src"]
513
- },
514
- null,
515
- 2
516
- )
517
- );
518
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
518
519
  printWelcomeBanner(name);
519
520
  }
520
- var isDirectRun = typeof require !== "undefined" && require.main === module && process.argv[1]?.includes("create");
521
- if (isDirectRun) {
522
- const projectName = process.argv[2];
523
- if (!projectName) {
524
- console.log("Usage: create-voltx-app <project-name> [--template chatbot] [--auth jwt]");
525
- process.exit(1);
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);');
526
529
  }
527
- const templateFlag = process.argv.indexOf("--template");
528
- const template = templateFlag !== -1 ? process.argv[templateFlag + 1] : "blank";
529
- const authFlag = process.argv.indexOf("--auth");
530
- const auth = authFlag !== -1 ? process.argv[authFlag + 1] : "none";
531
- createProject({ name: projectName, template, auth }).catch((err) => {
532
- console.error("[voltx] Error:", err);
533
- process.exit(1);
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\`);
534
576
  });
535
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>
662
+ </div>
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>
866
+ </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>
880
+ </div>
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
886
+ </div>
887
+ </div>
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>
891
+ </div>
892
+ </div>
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>
919
+ </div>
920
+ </div>
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>
930
+ </div>
931
+ </div>
932
+ </div>
933
+ );
934
+ }
935
+ `;
936
+ }
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";
1159
+ }
536
1160
  // Annotate the CommonJS export names for ESM import in node:
537
1161
  0 && (module.exports = {
538
1162
  createProject