@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/index.js CHANGED
@@ -148,70 +148,72 @@ function printWelcomeBanner(projectName) {
148
148
  }
149
149
 
150
150
  // src/create.ts
151
+ var VV = {
152
+ "@voltx/core": "^0.3.1",
153
+ "@voltx/server": "^0.3.1",
154
+ "@voltx/cli": "^0.3.5",
155
+ "@voltx/ai": "^0.3.0",
156
+ "@voltx/agents": "^0.3.1",
157
+ "@voltx/memory": "^0.3.0",
158
+ "@voltx/db": "^0.3.0",
159
+ "@voltx/rag": "^0.3.1",
160
+ "@voltx/auth": "^0.3.0"
161
+ };
162
+ function v(pkg) {
163
+ return VV[pkg] ?? "^0.3.0";
164
+ }
165
+ var TEMPLATE_DEPS = {
166
+ blank: { "@voltx/core": v("@voltx/core"), "@voltx/server": v("@voltx/server") },
167
+ chatbot: { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/memory": v("@voltx/memory") },
168
+ "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") },
169
+ "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") }
170
+ };
151
171
  async function createProject(options) {
152
- const { name, template = "blank", auth = "none" } = options;
172
+ const { name, template = "blank", auth = "none", shadcn = false } = options;
153
173
  const targetDir = path.resolve(process.cwd(), name);
154
174
  if (fs.existsSync(targetDir)) {
155
175
  console.error(`[voltx] Directory "${name}" already exists.`);
156
176
  process.exit(1);
157
177
  }
158
178
  fs.mkdirSync(targetDir, { recursive: true });
159
- const templateDeps = {
160
- blank: {
161
- "@voltx/core": "^0.3.0",
162
- "@voltx/server": "^0.3.0"
163
- },
164
- chatbot: {
165
- "@voltx/core": "^0.3.0",
166
- "@voltx/ai": "^0.3.0",
167
- "@voltx/server": "^0.3.0",
168
- "@voltx/memory": "^0.3.0"
169
- },
170
- "rag-app": {
171
- "@voltx/core": "^0.3.0",
172
- "@voltx/ai": "^0.3.0",
173
- "@voltx/server": "^0.3.0",
174
- "@voltx/rag": "^0.3.0",
175
- "@voltx/db": "^0.3.0"
176
- },
177
- "agent-app": {
178
- "@voltx/core": "^0.3.0",
179
- "@voltx/ai": "^0.3.0",
180
- "@voltx/server": "^0.3.0",
181
- "@voltx/agents": "^0.3.0",
182
- "@voltx/memory": "^0.3.0"
183
- }
184
- };
185
- const packageJson = {
179
+ const provider = template === "rag-app" ? "openai" : "cerebras";
180
+ const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
181
+ const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
182
+ const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": v("@voltx/cli") };
183
+ if (auth === "better-auth") {
184
+ deps["@voltx/auth"] = v("@voltx/auth");
185
+ deps["better-auth"] = "^1.5.0";
186
+ } else if (auth === "jwt") {
187
+ deps["@voltx/auth"] = v("@voltx/auth");
188
+ deps["jose"] = "^6.0.0";
189
+ }
190
+ const devDeps = { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" };
191
+ deps["hono"] = "^4.7.0";
192
+ deps["@hono/node-server"] = "^1.14.0";
193
+ deps["react"] = "^19.0.0";
194
+ deps["react-dom"] = "^19.0.0";
195
+ deps["tailwindcss"] = "^4.0.0";
196
+ devDeps["vite"] = "^6.0.0";
197
+ devDeps["@hono/vite-dev-server"] = "^0.7.0";
198
+ devDeps["@vitejs/plugin-react"] = "^4.3.0";
199
+ devDeps["@tailwindcss/vite"] = "^4.0.0";
200
+ devDeps["@types/react"] = "^19.0.0";
201
+ devDeps["@types/react-dom"] = "^19.0.0";
202
+ if (shadcn) {
203
+ deps["class-variance-authority"] = "^0.7.0";
204
+ deps["clsx"] = "^2.1.0";
205
+ deps["tailwind-merge"] = "^3.0.0";
206
+ deps["lucide-react"] = "^0.468.0";
207
+ }
208
+ fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
186
209
  name,
187
210
  version: "0.1.0",
188
211
  private: true,
189
- scripts: {
190
- dev: "voltx dev",
191
- build: "voltx build",
192
- start: "voltx start"
193
- },
194
- dependencies: {
195
- ...templateDeps[template] ?? templateDeps["blank"],
196
- "@voltx/cli": "^0.3.0",
197
- ...auth === "better-auth" ? { "@voltx/auth": "^0.3.0", "better-auth": "^1.5.0" } : {},
198
- ...auth === "jwt" ? { "@voltx/auth": "^0.3.0", "jose": "^6.0.0" } : {}
199
- },
200
- devDependencies: {
201
- typescript: "^5.7.0",
202
- tsx: "^4.21.0",
203
- tsup: "^8.0.0",
204
- "@types/node": "^22.0.0"
205
- }
206
- };
207
- fs.writeFileSync(
208
- path.join(targetDir, "package.json"),
209
- JSON.stringify(packageJson, null, 2)
210
- );
211
- const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
212
- const provider = template === "rag-app" ? "openai" : "cerebras";
213
- const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
214
- let configContent = `import { defineConfig } from "@voltx/core";
212
+ scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
213
+ dependencies: deps,
214
+ devDependencies: devDeps
215
+ }, null, 2));
216
+ let config = `import { defineConfig } from "@voltx/core";
215
217
 
216
218
  export default defineConfig({
217
219
  name: "${name}",
@@ -220,41 +222,63 @@ export default defineConfig({
220
222
  provider: "${provider}",
221
223
  model: "${model}",
222
224
  },`;
223
- if (hasDb) {
224
- configContent += `
225
+ if (hasDb) config += `
225
226
  db: {
226
227
  url: process.env.DATABASE_URL,
227
228
  },`;
228
- }
229
- if (auth !== "none") {
230
- configContent += `
229
+ if (auth !== "none") config += `
231
230
  auth: {
232
231
  provider: "${auth}",
233
232
  },`;
234
- }
235
- configContent += `
233
+ config += `
236
234
  server: {
237
- routesDir: "src/routes",
235
+ routesDir: "api",
238
236
  staticDir: "public",
239
237
  cors: true,
240
238
  },
241
239
  });
242
240
  `;
243
- fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), configContent);
244
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api"), { recursive: true });
241
+ fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
242
+ fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
245
243
  fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
244
+ fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
245
+ fs.mkdirSync(path.join(targetDir, "src", "components"), { recursive: true });
246
+ fs.mkdirSync(path.join(targetDir, "src", "hooks"), { recursive: true });
247
+ fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
248
+ fs.writeFileSync(path.join(targetDir, "public", "favicon.svg"), generateFaviconSVG());
249
+ fs.writeFileSync(path.join(targetDir, "public", "robots.txt"), generateRobotsTxt());
250
+ fs.writeFileSync(path.join(targetDir, "public", "site.webmanifest"), generateWebManifest(name));
251
+ const tsconfig = {
252
+ compilerOptions: {
253
+ target: "ES2022",
254
+ module: "ESNext",
255
+ moduleResolution: "bundler",
256
+ strict: true,
257
+ esModuleInterop: true,
258
+ skipLibCheck: true,
259
+ outDir: "dist",
260
+ baseUrl: ".",
261
+ paths: { "@/*": ["./src/*"] },
262
+ jsx: "react-jsx"
263
+ },
264
+ include: ["src", "api", "server.ts", "voltx.config.ts"]
265
+ };
266
+ if (template === "agent-app") tsconfig.include.push("agents", "tools");
267
+ fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
268
+ fs.writeFileSync(path.join(targetDir, "server.ts"), generateServerEntry(name, template));
269
+ fs.writeFileSync(path.join(targetDir, "vite.config.ts"), generateViteConfigFile("server.ts"));
270
+ fs.writeFileSync(path.join(targetDir, "src", "entry-client.tsx"), generateEntryClient());
271
+ fs.writeFileSync(path.join(targetDir, "src", "entry-server.tsx"), generateEntryServer());
272
+ fs.writeFileSync(path.join(targetDir, "src", "layout.tsx"), generateLayoutComponent(name));
273
+ fs.writeFileSync(path.join(targetDir, "src", "globals.css"), generateGlobalCSS(shadcn));
274
+ if (shadcn) {
275
+ fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.ts"), generateCnUtil());
276
+ fs.writeFileSync(path.join(targetDir, "components.json"), generateComponentsJson());
277
+ }
278
+ fs.writeFileSync(path.join(targetDir, "src", "app.tsx"), generateAppComponent(name, template));
246
279
  fs.writeFileSync(
247
- path.join(targetDir, "src", "index.ts"),
248
- `import { createApp } from "@voltx/core";
249
- import config from "../voltx.config";
250
-
251
- const app = createApp(config);
252
- app.start();
253
- `
254
- );
255
- fs.writeFileSync(
256
- path.join(targetDir, "src", "routes", "index.ts"),
257
- `// GET / \u2014 Health check
280
+ path.join(targetDir, "api", "index.ts"),
281
+ `// GET /api \u2014 Health check
258
282
  import type { Context } from "@voltx/server";
259
283
 
260
284
  export function GET(c: Context) {
@@ -264,25 +288,22 @@ export function GET(c: Context) {
264
288
  );
265
289
  if (template === "chatbot" || template === "agent-app") {
266
290
  fs.writeFileSync(
267
- path.join(targetDir, "src", "routes", "api", "chat.ts"),
291
+ path.join(targetDir, "api", "chat.ts"),
268
292
  `// POST /api/chat \u2014 Streaming chat with conversation memory
269
293
  import type { Context } from "@voltx/server";
270
294
  import { streamText } from "@voltx/ai";
271
295
  import { createMemory } from "@voltx/memory";
272
296
 
273
- // In-memory for dev; swap to createMemory("postgres", { url }) for production
274
297
  const memory = createMemory({ maxMessages: 50 });
275
298
 
276
299
  export async function POST(c: Context) {
277
300
  const { messages, conversationId = "default" } = await c.req.json();
278
301
 
279
- // Store the latest user message
280
302
  const lastMessage = messages[messages.length - 1];
281
303
  if (lastMessage?.role === "user") {
282
304
  await memory.add(conversationId, { role: "user", content: lastMessage.content });
283
305
  }
284
306
 
285
- // Get conversation history from memory
286
307
  const history = await memory.get(conversationId);
287
308
 
288
309
  const result = await streamText({
@@ -291,7 +312,6 @@ export async function POST(c: Context) {
291
312
  messages: history.map((m) => ({ role: m.role, content: m.content })),
292
313
  });
293
314
 
294
- // Store assistant response after stream completes
295
315
  result.text.then(async (text) => {
296
316
  await memory.add(conversationId, { role: "assistant", content: text });
297
317
  });
@@ -302,44 +322,72 @@ export async function POST(c: Context) {
302
322
  );
303
323
  }
304
324
  if (template === "agent-app") {
305
- fs.mkdirSync(path.join(targetDir, "src", "agents"), { recursive: true });
306
- fs.mkdirSync(path.join(targetDir, "src", "tools"), { recursive: true });
307
- fs.writeFileSync(
308
- path.join(targetDir, "src", "agents", "assistant.ts"),
309
- `import { createAgent } from "@voltx/agents";
310
- import { searchTool } from "../tools/search";
325
+ fs.mkdirSync(path.join(targetDir, "agents"), { recursive: true });
326
+ fs.mkdirSync(path.join(targetDir, "tools"), { recursive: true });
327
+ fs.writeFileSync(path.join(targetDir, "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
328
+ import type { Tool } from "@voltx/agents";
311
329
 
312
- export const assistant = createAgent({
313
- name: "assistant",
314
- model: "${provider}:${model}",
315
- instructions: "You are a helpful AI assistant. Use your tools when needed.",
316
- tools: [searchTool],
317
- maxIterations: 5,
318
- });
319
- `
320
- );
321
- fs.writeFileSync(
322
- path.join(targetDir, "src", "tools", "search.ts"),
323
- `import type { Tool } from "@voltx/agents";
330
+ export const calculatorTool: Tool = {
331
+ name: "calculator",
332
+ description: "Evaluate a math expression. Supports +, -, *, /, %, parentheses, and Math functions.",
333
+ parameters: {
334
+ type: "object",
335
+ properties: { expression: { type: "string", description: "The math expression to evaluate" } },
336
+ required: ["expression"],
337
+ },
338
+ async execute(args: { expression: string }) {
339
+ try {
340
+ const safe = args.expression.replace(/[^0-9+\\-*/.()%\\s,]|(?<!Math)\\.[a-z]/gi, (match) => {
341
+ if (args.expression.includes("Math.")) return match;
342
+ throw new Error("Invalid character: " + match);
343
+ });
344
+ const result = new Function("return " + safe)();
345
+ return \`\${args.expression} = \${result}\`;
346
+ } catch (err) {
347
+ return \`Error: \${err instanceof Error ? err.message : String(err)}\`;
348
+ }
349
+ },
350
+ };
351
+ `);
352
+ fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
353
+ import type { Tool } from "@voltx/agents";
324
354
 
325
- export const searchTool: Tool = {
326
- name: "search",
327
- description: "Search for information on a topic.",
355
+ export const datetimeTool: Tool = {
356
+ name: "datetime",
357
+ description: "Get the current date, time, day of week, and timezone.",
328
358
  parameters: {
329
359
  type: "object",
330
- properties: { query: { type: "string", description: "The search query" } },
331
- required: ["query"],
360
+ properties: { timezone: { type: "string", description: "Optional IANA timezone. Defaults to server timezone." } },
332
361
  },
333
- async execute(args: { query: string }) {
334
- return \`Search results for "\${args.query}": Placeholder \u2014 connect a real search API.\`;
362
+ async execute(args: { timezone?: string }) {
363
+ const now = new Date();
364
+ const tz = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
365
+ const formatted = now.toLocaleString("en-US", {
366
+ timeZone: tz, weekday: "long", year: "numeric", month: "long", day: "numeric",
367
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true,
368
+ });
369
+ return \`Current date/time (\${tz}): \${formatted}\`;
335
370
  },
336
371
  };
337
- `
338
- );
372
+ `);
373
+ fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
374
+ import { createAgent } from "@voltx/agents";
375
+ import { calculatorTool } from "../tools/calculator";
376
+ import { datetimeTool } from "../tools/datetime";
377
+
378
+ export const assistant = createAgent({
379
+ name: "assistant",
380
+ model: "${provider}:${model}",
381
+ instructions: "You are a helpful AI assistant with access to tools: Calculator, Date & Time. Use them when needed to answer questions accurately.",
382
+ tools: [calculatorTool, datetimeTool],
383
+ maxIterations: 5,
384
+ });
385
+ `);
339
386
  fs.writeFileSync(
340
- path.join(targetDir, "src", "routes", "api", "agent.ts"),
341
- `import type { Context } from "@voltx/server";
342
- import { assistant } from "../../agents/assistant";
387
+ path.join(targetDir, "api", "agent.ts"),
388
+ `// POST /api/agent \u2014 Run the AI agent
389
+ import type { Context } from "@voltx/server";
390
+ import { assistant } from "../agents/assistant";
343
391
 
344
392
  export async function POST(c: Context) {
345
393
  const { input } = await c.req.json();
@@ -352,36 +400,33 @@ export async function POST(c: Context) {
352
400
  }
353
401
  if (template === "rag-app") {
354
402
  const embedModel = "openai:text-embedding-3-small";
355
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "rag"), { recursive: true });
403
+ fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
356
404
  fs.writeFileSync(
357
- path.join(targetDir, "src", "routes", "api", "rag", "query.ts"),
405
+ path.join(targetDir, "api", "rag", "query.ts"),
358
406
  `// POST /api/rag/query \u2014 Query documents with RAG
359
407
  import type { Context } from "@voltx/server";
360
408
  import { streamText } from "@voltx/ai";
361
409
  import { createRAGPipeline, createEmbedder } from "@voltx/rag";
362
410
  import { createVectorStore } from "@voltx/db";
363
411
 
364
- const vectorStore = createVectorStore(); // swap to "pinecone" or "pgvector" for production
412
+ const vectorStore = createVectorStore();
365
413
  const embedder = createEmbedder({ model: "${embedModel}" });
366
414
  const rag = createRAGPipeline({ embedder, vectorStore });
367
415
 
368
416
  export async function POST(c: Context) {
369
417
  const { question } = await c.req.json();
370
-
371
418
  const context = await rag.getContext(question, { topK: 5 });
372
-
373
419
  const result = await streamText({
374
420
  model: "${provider}:${model}",
375
- 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}\`,
421
+ system: \`Answer based on context. If not relevant, say so.\\n\\nContext:\\n\${context}\`,
376
422
  messages: [{ role: "user", content: question }],
377
423
  });
378
-
379
424
  return result.toSSEResponse();
380
425
  }
381
426
  `
382
427
  );
383
428
  fs.writeFileSync(
384
- path.join(targetDir, "src", "routes", "api", "rag", "ingest.ts"),
429
+ path.join(targetDir, "api", "rag", "ingest.ts"),
385
430
  `// POST /api/rag/ingest \u2014 Ingest documents into the vector store
386
431
  import type { Context } from "@voltx/server";
387
432
  import { createRAGPipeline, createEmbedder } from "@voltx/rag";
@@ -393,11 +438,7 @@ const rag = createRAGPipeline({ embedder, vectorStore });
393
438
 
394
439
  export async function POST(c: Context) {
395
440
  const { text, idPrefix } = await c.req.json();
396
-
397
- if (!text || typeof text !== "string") {
398
- return c.json({ error: "Missing 'text' field" }, 400);
399
- }
400
-
441
+ if (!text || typeof text !== "string") return c.json({ error: "Missing 'text' field" }, 400);
401
442
  const result = await rag.ingest(text, idPrefix ?? "doc");
402
443
  return c.json({ status: "ok", chunks: result.chunks, ids: result.ids });
403
444
  }
@@ -405,12 +446,12 @@ export async function POST(c: Context) {
405
446
  );
406
447
  }
407
448
  if (auth === "better-auth") {
408
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "auth"), { recursive: true });
449
+ fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
409
450
  fs.writeFileSync(
410
- path.join(targetDir, "src", "routes", "api", "auth", "[...path].ts"),
451
+ path.join(targetDir, "api", "auth", "[...path].ts"),
411
452
  `// ALL /api/auth/* \u2014 Better Auth handler
412
453
  import type { Context } from "@voltx/server";
413
- import { auth } from "../../../lib/auth";
454
+ import { auth } from "../../src/lib/auth";
414
455
  import { createAuthHandler } from "@voltx/auth";
415
456
 
416
457
  const handler = createAuthHandler(auth);
@@ -419,11 +460,9 @@ export const GET = (c: Context) => handler(c);
419
460
  export const POST = (c: Context) => handler(c);
420
461
  `
421
462
  );
422
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
423
463
  fs.writeFileSync(
424
464
  path.join(targetDir, "src", "lib", "auth.ts"),
425
- `// Auth configuration \u2014 Better Auth with DB-backed sessions
426
- import { createAuth, createAuthMiddleware } from "@voltx/auth";
465
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
427
466
 
428
467
  export const auth = createAuth("better-auth", {
429
468
  database: process.env.DATABASE_URL!,
@@ -437,11 +476,9 @@ export const authMiddleware = createAuthMiddleware({
437
476
  `
438
477
  );
439
478
  } else if (auth === "jwt") {
440
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
441
479
  fs.writeFileSync(
442
480
  path.join(targetDir, "src", "lib", "auth.ts"),
443
- `// Auth configuration \u2014 JWT (stateless)
444
- import { createAuth, createAuthMiddleware } from "@voltx/auth";
481
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
445
482
 
446
483
  export const jwt = createAuth("jwt", {
447
484
  secret: process.env.JWT_SECRET!,
@@ -455,18 +492,13 @@ export const authMiddleware = createAuthMiddleware({
455
492
  `
456
493
  );
457
494
  fs.writeFileSync(
458
- path.join(targetDir, "src", "routes", "api", "auth.ts"),
459
- `// POST /api/auth/login \u2014 Example JWT login route
460
- import type { Context } from "@voltx/server";
461
- import { jwt } from "../../lib/auth";
495
+ path.join(targetDir, "api", "auth.ts"),
496
+ `import type { Context } from "@voltx/server";
497
+ import { jwt } from "../src/lib/auth";
462
498
 
463
499
  export async function POST(c: Context) {
464
500
  const { email, password } = await c.req.json();
465
-
466
- if (!email || !password) {
467
- return c.json({ error: "Email and password are required" }, 400);
468
- }
469
-
501
+ if (!email || !password) return c.json({ error: "Email and password are required" }, 400);
470
502
  const token = await jwt.sign({ sub: email, email });
471
503
  return c.json({ token });
472
504
  }
@@ -476,74 +508,667 @@ export async function POST(c: Context) {
476
508
  let envContent = "";
477
509
  if (template === "rag-app") {
478
510
  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";
479
- 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";
480
- 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";
511
+ 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";
481
512
  } else if (template === "chatbot" || template === "agent-app") {
482
513
  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";
483
- if (template === "agent-app") {
484
- envContent += "# \u2500\u2500\u2500 Database (Neon Postgres \u2014 optional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
485
- }
486
514
  } else {
487
- 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";
515
+ 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";
488
516
  }
489
517
  if (auth === "better-auth") {
490
- 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";
491
- if (template !== "rag-app" && template !== "agent-app") {
492
- envContent += "DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require\n";
493
- }
494
- envContent += "# GITHUB_CLIENT_ID=\n# GITHUB_CLIENT_SECRET=\n\n";
518
+ 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";
495
519
  } else if (auth === "jwt") {
496
520
  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";
497
521
  }
498
522
  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";
499
523
  fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
500
- fs.writeFileSync(
501
- path.join(targetDir, ".gitignore"),
502
- "node_modules\ndist\n.env\n"
503
- );
504
- fs.writeFileSync(
505
- path.join(targetDir, "tsconfig.json"),
506
- JSON.stringify(
507
- {
508
- compilerOptions: {
509
- target: "ES2022",
510
- module: "ESNext",
511
- moduleResolution: "bundler",
512
- strict: true,
513
- esModuleInterop: true,
514
- skipLibCheck: true,
515
- outDir: "dist",
516
- rootDir: "src"
517
- },
518
- include: ["src"]
519
- },
520
- null,
521
- 2
522
- )
523
- );
524
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
524
525
  printWelcomeBanner(name);
525
526
  }
526
- var isDirectRun = typeof require !== "undefined" && require.main === module && process.argv[1]?.includes("create");
527
- if (isDirectRun) {
528
- const projectName = process.argv[2];
529
- if (!projectName) {
530
- console.log("Usage: create-voltx-app <project-name> [--template chatbot] [--auth jwt]");
531
- process.exit(1);
527
+ function generateServerEntry(projectName, template) {
528
+ const imports = [];
529
+ const mounts = [];
530
+ imports.push('import { GET as healthGET } from "./api/index";');
531
+ mounts.push('app.get("/api", healthGET);');
532
+ if (template === "chatbot" || template === "agent-app") {
533
+ imports.push('import { POST as chatPOST } from "./api/chat";');
534
+ mounts.push('app.post("/api/chat", chatPOST);');
532
535
  }
533
- const templateFlag = process.argv.indexOf("--template");
534
- const template = templateFlag !== -1 ? process.argv[templateFlag + 1] : "blank";
535
- const authFlag = process.argv.indexOf("--auth");
536
- const auth = authFlag !== -1 ? process.argv[authFlag + 1] : "none";
537
- createProject({ name: projectName, template, auth }).catch((err) => {
538
- console.error("[voltx] Error:", err);
539
- process.exit(1);
536
+ if (template === "agent-app") {
537
+ imports.push('import { POST as agentPOST } from "./api/agent";');
538
+ mounts.push('app.post("/api/agent", agentPOST);');
539
+ }
540
+ if (template === "rag-app") {
541
+ imports.push('import { POST as ragQueryPOST } from "./api/rag/query";');
542
+ imports.push('import { POST as ragIngestPOST } from "./api/rag/ingest";');
543
+ mounts.push('app.post("/api/rag/query", ragQueryPOST);');
544
+ mounts.push('app.post("/api/rag/ingest", ragIngestPOST);');
545
+ }
546
+ return `import { Hono } from "hono";
547
+ import { serve } from "@hono/node-server";
548
+ import { serveStatic } from "@hono/node-server/serve-static";
549
+ import { registerSSR } from "@voltx/server";
550
+ import { loadEnv } from "@voltx/core";
551
+ ${imports.join("\n")}
552
+
553
+ loadEnv(process.env.NODE_ENV ?? "development");
554
+
555
+ const isProd = process.env.NODE_ENV === "production";
556
+ const app = new Hono();
557
+
558
+ // \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
559
+ ${mounts.join("\n")}
560
+
561
+ // \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
562
+ if (isProd) {
563
+ app.use("/assets/*", serveStatic({ root: "./dist/client/" }));
564
+ app.use("/favicon.svg", serveStatic({ root: "./public/" }));
565
+ app.use("/robots.txt", serveStatic({ root: "./public/" }));
566
+ app.use("/site.webmanifest", serveStatic({ root: "./public/" }));
567
+ }
568
+
569
+ // \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
570
+ registerSSR(app, null, {
571
+ title: "${projectName}",
572
+ entryServer: "src/entry-server.tsx",
573
+ entryClient: "src/entry-client.tsx",
574
+ });
575
+
576
+ export default app;
577
+
578
+ if (isProd) {
579
+ const port = Number(process.env.PORT) || 3000;
580
+ serve({ fetch: app.fetch, port }, (info) => {
581
+ console.log(\`\\n \u26A1 ${projectName} running at http://localhost:\${info.port}\\n\`);
540
582
  });
541
583
  }
584
+ `;
585
+ }
586
+ function generateViteConfigFile(entry) {
587
+ return `import { defineConfig } from "vite";
588
+ import devServer from "@hono/vite-dev-server";
589
+ import react from "@vitejs/plugin-react";
590
+ import tailwindcss from "@tailwindcss/vite";
591
+
592
+ export default defineConfig({
593
+ resolve: {
594
+ alias: {
595
+ "@": "/src",
596
+ },
597
+ },
598
+ plugins: [
599
+ react(),
600
+ tailwindcss(),
601
+ devServer({
602
+ entry: "${entry}",
603
+ exclude: [
604
+ /.*\\.tsx?($|\\?)/,
605
+ /.*\\.(s?css|less)($|\\?)/,
606
+ /.*\\.(svg|png|jpg|jpeg|gif|webp|ico)($|\\?)/,
607
+ /^\\/@.+$/,
608
+ /^\\/favicon\\.svg$/,
609
+ /^\\/node_modules\\/.*/,
610
+ /^\\/src\\/.*/,
611
+ ],
612
+ injectClientScript: false,
613
+ }),
614
+ ],
615
+ });
616
+ `;
617
+ }
618
+ function generateEntryClient() {
619
+ return `import React from "react";
620
+ import { hydrateRoot } from "react-dom/client";
621
+ import Layout from "./layout";
622
+ import App from "./app";
623
+ import "./globals.css";
624
+
625
+ hydrateRoot(
626
+ document.getElementById("root")!,
627
+ <React.StrictMode>
628
+ <Layout>
629
+ <App />
630
+ </Layout>
631
+ </React.StrictMode>
632
+ );
633
+ `;
634
+ }
635
+ function generateEntryServer() {
636
+ return `import React from "react";
637
+ import { renderToReadableStream } from "react-dom/server";
638
+ import Layout from "./layout";
639
+ import App from "./app";
640
+
641
+ export async function render(_url: string): Promise<ReadableStream> {
642
+ const stream = await renderToReadableStream(
643
+ <React.StrictMode>
644
+ <Layout>
645
+ <App />
646
+ </Layout>
647
+ </React.StrictMode>,
648
+ {
649
+ onError(error: unknown) {
650
+ console.error("[voltx] SSR render error:", error);
651
+ },
652
+ }
653
+ );
654
+ return stream;
655
+ }
656
+ `;
657
+ }
658
+ function generateLayoutComponent(projectName) {
659
+ return `import React from "react";
660
+
661
+ export default function Layout({ children }: { children: React.ReactNode }) {
662
+ return (
663
+ <div className="min-h-screen bg-background text-foreground font-sans">
664
+ <header className="border-b border-border px-6 py-3 flex items-center justify-between">
665
+ <div className="flex items-center gap-2">
666
+ <span className="text-xl">\u26A1</span>
667
+ <span className="font-semibold">${projectName}</span>
668
+ </div>
669
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-muted text-sm hover:text-foreground transition-colors">
670
+ Built with VoltX
671
+ </a>
672
+ </header>
673
+ <main>{children}</main>
674
+ </div>
675
+ );
676
+ }
677
+ `;
678
+ }
679
+ function generateGlobalCSS(useShadcn = false) {
680
+ if (useShadcn) {
681
+ return `@import "tailwindcss";
682
+
683
+ @theme inline {
684
+ --radius-sm: 0.25rem;
685
+ --radius-md: 0.375rem;
686
+ --radius-lg: 0.5rem;
687
+ --radius-xl: 0.75rem;
688
+ --color-background: hsl(var(--background));
689
+ --color-foreground: hsl(var(--foreground));
690
+ --color-card: hsl(var(--card));
691
+ --color-card-foreground: hsl(var(--card-foreground));
692
+ --color-popover: hsl(var(--popover));
693
+ --color-popover-foreground: hsl(var(--popover-foreground));
694
+ --color-primary: hsl(var(--primary));
695
+ --color-primary-foreground: hsl(var(--primary-foreground));
696
+ --color-secondary: hsl(var(--secondary));
697
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
698
+ --color-muted: hsl(var(--muted));
699
+ --color-muted-foreground: hsl(var(--muted-foreground));
700
+ --color-accent: hsl(var(--accent));
701
+ --color-accent-foreground: hsl(var(--accent-foreground));
702
+ --color-destructive: hsl(var(--destructive));
703
+ --color-border: hsl(var(--border));
704
+ --color-input: hsl(var(--input));
705
+ --color-ring: hsl(var(--ring));
706
+ --color-chart-1: hsl(var(--chart-1));
707
+ --color-chart-2: hsl(var(--chart-2));
708
+ --color-chart-3: hsl(var(--chart-3));
709
+ --color-chart-4: hsl(var(--chart-4));
710
+ --color-chart-5: hsl(var(--chart-5));
711
+ --color-sidebar: hsl(var(--sidebar));
712
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
713
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
714
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
715
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
716
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
717
+ --color-sidebar-border: hsl(var(--sidebar-border));
718
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
719
+ }
720
+
721
+ :root {
722
+ --background: 0 0% 4%;
723
+ --foreground: 0 0% 93%;
724
+ --card: 0 0% 6%;
725
+ --card-foreground: 0 0% 93%;
726
+ --popover: 0 0% 6%;
727
+ --popover-foreground: 0 0% 93%;
728
+ --primary: 0 0% 93%;
729
+ --primary-foreground: 0 0% 6%;
730
+ --secondary: 0 0% 12%;
731
+ --secondary-foreground: 0 0% 93%;
732
+ --muted: 0 0% 12%;
733
+ --muted-foreground: 0 0% 55%;
734
+ --accent: 0 0% 12%;
735
+ --accent-foreground: 0 0% 93%;
736
+ --destructive: 0 62% 50%;
737
+ --border: 0 0% 14%;
738
+ --input: 0 0% 14%;
739
+ --ring: 0 0% 83%;
740
+ --chart-1: 220 70% 50%;
741
+ --chart-2: 160 60% 45%;
742
+ --chart-3: 30 80% 55%;
743
+ --chart-4: 280 65% 60%;
744
+ --chart-5: 340 75% 55%;
745
+ --sidebar: 0 0% 5%;
746
+ --sidebar-foreground: 0 0% 93%;
747
+ --sidebar-primary: 0 0% 93%;
748
+ --sidebar-primary-foreground: 0 0% 6%;
749
+ --sidebar-accent: 0 0% 12%;
750
+ --sidebar-accent-foreground: 0 0% 93%;
751
+ --sidebar-border: 0 0% 14%;
752
+ --sidebar-ring: 0 0% 83%;
753
+ }
754
+
755
+ *,
756
+ *::before,
757
+ *::after {
758
+ box-sizing: border-box;
759
+ margin: 0;
760
+ padding: 0;
761
+ border-color: var(--color-border);
762
+ }
763
+
764
+ html,
765
+ body {
766
+ height: 100%;
767
+ background: var(--color-background);
768
+ color: var(--color-foreground);
769
+ font-family: system-ui, -apple-system, sans-serif;
770
+ -webkit-font-smoothing: antialiased;
771
+ }
772
+
773
+ #root {
774
+ height: 100%;
775
+ }
776
+ `;
777
+ }
778
+ return `@import "tailwindcss";
779
+
780
+ @theme {
781
+ --color-background: #0a0a0a;
782
+ --color-foreground: #ededed;
783
+ --color-muted: #888888;
784
+ --color-border: #222222;
785
+ --color-primary: #2563eb;
786
+ --color-accent: #a78bfa;
787
+ --font-sans: system-ui, -apple-system, sans-serif;
788
+ }
789
+
790
+ *,
791
+ *::before,
792
+ *::after {
793
+ box-sizing: border-box;
794
+ margin: 0;
795
+ padding: 0;
796
+ }
797
+
798
+ html,
799
+ body {
800
+ height: 100%;
801
+ background: var(--color-background);
802
+ color: var(--color-foreground);
803
+ font-family: var(--font-sans);
804
+ -webkit-font-smoothing: antialiased;
805
+ }
806
+
807
+ #root {
808
+ height: 100%;
809
+ }
810
+
811
+ a {
812
+ color: inherit;
813
+ text-decoration: none;
814
+ }
815
+
816
+ a:hover {
817
+ text-decoration: underline;
818
+ }
819
+ `;
820
+ }
821
+ function generateCnUtil() {
822
+ return `import { type ClassValue, clsx } from "clsx";
823
+ import { twMerge } from "tailwind-merge";
824
+
825
+ export function cn(...inputs: ClassValue[]) {
826
+ return twMerge(clsx(inputs));
827
+ }
828
+ `;
829
+ }
830
+ function generateComponentsJson() {
831
+ return JSON.stringify({
832
+ "$schema": "https://ui.shadcn.com/schema.json",
833
+ style: "new-york",
834
+ rsc: false,
835
+ tsx: true,
836
+ tailwind: {
837
+ config: "",
838
+ css: "src/globals.css",
839
+ baseColor: "neutral",
840
+ cssVariables: true
841
+ },
842
+ aliases: {
843
+ components: "@/components",
844
+ utils: "@/lib/utils",
845
+ ui: "@/components/ui",
846
+ lib: "@/lib",
847
+ hooks: "@/hooks"
848
+ }
849
+ }, null, 2) + "\n";
850
+ }
851
+ function generateAppComponent(projectName, template) {
852
+ if (template === "blank") {
853
+ return `import React, { useState, useEffect } from "react";
854
+
855
+ export default function App() {
856
+ const [status, setStatus] = useState<string>("checking...");
857
+
858
+ useEffect(() => {
859
+ fetch("/api")
860
+ .then((res) => res.json())
861
+ .then((data) => setStatus(data.status || "ok"))
862
+ .catch(() => setStatus("error"));
863
+ }, []);
864
+
865
+ return (
866
+ <div className="flex flex-col items-center justify-center min-h-[calc(100vh-60px)] px-6 py-12">
867
+ <div className="text-center max-w-2xl w-full">
868
+ {/* Hero */}
869
+ <div className="relative mb-8">
870
+ <div className="absolute inset-0 blur-3xl opacity-20 bg-gradient-to-r from-blue-500 via-purple-500 to-blue-500 rounded-full" />
871
+ <div className="relative text-7xl mb-4">\u26A1</div>
872
+ </div>
873
+ <h1 className="text-5xl font-bold tracking-tight mb-3 bg-gradient-to-r from-white to-white/60 bg-clip-text text-transparent">
874
+ \${"\${projectName}"}
875
+ </h1>
876
+ <p className="text-muted text-lg mb-10">The AI-first full-stack framework</p>
877
+
878
+ {/* Status cards */}
879
+ <div className="flex gap-4 justify-center mb-10">
880
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
881
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Server</div>
882
+ <div className={\`text-sm font-medium \${status === "ok" ? "text-emerald-400" : "text-red-400"}\`}>
883
+ <span className={\`inline-block w-2 h-2 rounded-full mr-2 \${status === "ok" ? "bg-emerald-400 animate-pulse" : "bg-red-400"}\`} />
884
+ {status}
885
+ </div>
886
+ </div>
887
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
888
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Frontend</div>
889
+ <div className="text-sm font-medium text-emerald-400">
890
+ <span className="inline-block w-2 h-2 rounded-full mr-2 bg-emerald-400 animate-pulse" />
891
+ React + Vite
892
+ </div>
893
+ </div>
894
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
895
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">CSS</div>
896
+ <div className="text-sm font-medium text-sky-400">Tailwind v4</div>
897
+ </div>
898
+ </div>
899
+
900
+ {/* Get started */}
901
+ <div className="bg-white/[0.03] border border-border rounded-2xl p-8 text-left backdrop-blur-sm">
902
+ <h2 className="text-sm font-medium text-muted mb-6 uppercase tracking-wider">Get started</h2>
903
+ <div className="space-y-4">
904
+ <div className="flex items-start gap-4">
905
+ <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>
906
+ <div>
907
+ <code className="text-purple-400 text-sm">src/app.tsx</code>
908
+ <p className="text-muted text-sm mt-1">Edit this file to build your UI</p>
909
+ </div>
910
+ </div>
911
+ <div className="flex items-start gap-4">
912
+ <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>
913
+ <div>
914
+ <code className="text-blue-400 text-sm">api/</code>
915
+ <p className="text-muted text-sm mt-1">Add API routes here (file-based routing)</p>
916
+ </div>
917
+ </div>
918
+ <div className="flex items-start gap-4">
919
+ <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>
920
+ <div>
921
+ <code className="text-emerald-400 text-sm">src/components/</code>
922
+ <p className="text-muted text-sm mt-1">Create React components with Tailwind CSS</p>
923
+ </div>
924
+ </div>
925
+ </div>
926
+ </div>
927
+
928
+ {/* Links */}
929
+ <div className="flex gap-6 justify-center mt-8">
930
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
931
+ GitHub \u2192
932
+ </a>
933
+ <a href="https://voltx.co.in" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
934
+ Docs \u2192
935
+ </a>
936
+ </div>
937
+ </div>
938
+ </div>
939
+ );
940
+ }
941
+ `;
942
+ }
943
+ const apiEndpoint = template === "agent-app" ? "/api/agent" : template === "rag-app" ? "/api/rag/query" : "/api/chat";
944
+ const isAgent = template === "agent-app";
945
+ const isRag = template === "rag-app";
946
+ let emptyStateTitle = "Start a conversation";
947
+ let emptyStateHint = "Type a message below to chat with AI";
948
+ let accentColor = "blue";
949
+ let inputPlaceholder = "Type a message...";
950
+ if (isAgent) {
951
+ emptyStateTitle = "Talk to your AI agent";
952
+ emptyStateHint = "The agent can use tools like Calculator and Date/Time";
953
+ accentColor = "purple";
954
+ inputPlaceholder = "Ask the agent anything...";
955
+ } else if (isRag) {
956
+ emptyStateTitle = "Ask your documents";
957
+ emptyStateHint = "Query your knowledge base \u2014 ingest docs via POST /api/rag/ingest";
958
+ accentColor = "emerald";
959
+ inputPlaceholder = "Ask a question about your documents...";
960
+ }
961
+ return `import React, { useState, useRef, useEffect, useCallback } from "react";
962
+
963
+ interface Message {
964
+ id: string;
965
+ role: "user" | "assistant";
966
+ content: string;
967
+ }
968
+
969
+ export default function App() {
970
+ const [messages, setMessages] = useState<Message[]>([]);
971
+ const [input, setInput] = useState("");
972
+ const [isLoading, setIsLoading] = useState(false);
973
+ const messagesEndRef = useRef<HTMLDivElement>(null);
974
+ const inputRef = useRef<HTMLInputElement>(null);
975
+
976
+ useEffect(() => {
977
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
978
+ }, [messages]);
979
+
980
+ useEffect(() => {
981
+ inputRef.current?.focus();
982
+ }, []);
983
+
984
+ const sendMessage = useCallback(async () => {
985
+ const text = input.trim();
986
+ if (!text || isLoading) return;
987
+
988
+ const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: text };
989
+ setMessages((prev) => [...prev, userMsg]);
990
+ setInput("");
991
+ setIsLoading(true);
992
+
993
+ const assistantMsg: Message = { id: crypto.randomUUID(), role: "assistant", content: "" };
994
+ setMessages((prev) => [...prev, assistantMsg]);
995
+
996
+ try {${isAgent ? `
997
+ const res = await fetch("${apiEndpoint}", {
998
+ method: "POST",
999
+ headers: { "Content-Type": "application/json" },
1000
+ body: JSON.stringify({ input: text }),
1001
+ });
1002
+ const data = await res.json();
1003
+ setMessages((prev) =>
1004
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: data.content || "No response" } : m)
1005
+ );` : `
1006
+ const res = await fetch("${apiEndpoint}", {
1007
+ method: "POST",
1008
+ headers: { "Content-Type": "application/json" },
1009
+ body: JSON.stringify({${isRag ? ` question: text ` : ` messages: [...messages, { role: "user", content: text }] `}}),
1010
+ });
1011
+
1012
+ if (!res.body) throw new Error("No response body");
1013
+
1014
+ const reader = res.body.getReader();
1015
+ const decoder = new TextDecoder();
1016
+ let buffer = "";
1017
+ let fullContent = "";
1018
+
1019
+ while (true) {
1020
+ const { done, value } = await reader.read();
1021
+ if (done) break;
1022
+ buffer += decoder.decode(value, { stream: true });
1023
+ const lines = buffer.split("\\n");
1024
+ buffer = lines.pop() ?? "";
1025
+
1026
+ for (const line of lines) {
1027
+ if (!line.startsWith("data: ")) continue;
1028
+ const data = line.slice(6);
1029
+ if (data === "[DONE]") break;
1030
+ try {
1031
+ const parsed = JSON.parse(data);
1032
+ const chunk = parsed.textDelta ?? parsed.content ?? parsed.choices?.[0]?.delta?.content ?? "";
1033
+ if (chunk) {
1034
+ fullContent += chunk;
1035
+ setMessages((prev) =>
1036
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: fullContent } : m)
1037
+ );
1038
+ }
1039
+ } catch {}
1040
+ }
1041
+ }`}
1042
+ } catch (err) {
1043
+ setMessages((prev) =>
1044
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: "Error: " + (err instanceof Error ? err.message : String(err)) } : m)
1045
+ );
1046
+ } finally {
1047
+ setIsLoading(false);
1048
+ }
1049
+ }, [input, isLoading, messages]);
1050
+
1051
+ return (
1052
+ <div className="h-[calc(100vh-60px)] flex flex-col">
1053
+ <main className="flex-1 overflow-y-auto px-4 py-6">
1054
+ <div className="max-w-3xl mx-auto">
1055
+ {messages.length === 0 && (
1056
+ <div className="flex flex-col items-center justify-center h-[60vh] text-center">
1057
+ <div className="relative mb-6">
1058
+ <div className="absolute inset-0 blur-2xl opacity-20 bg-${accentColor}-500 rounded-full" />
1059
+ <div className="relative text-5xl">\u26A1</div>
1060
+ </div>
1061
+ <h2 className="text-2xl font-semibold mb-2">${emptyStateTitle}</h2>
1062
+ <p className="text-muted text-sm max-w-md">${emptyStateHint}</p>
1063
+ <div className="flex gap-2 mt-6">
1064
+ ${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">
1065
+ What is 42 \xD7 17?
1066
+ </button>
1067
+ <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">
1068
+ What day is it?
1069
+ </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">
1070
+ Summarize main topics
1071
+ </button>
1072
+ <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">
1073
+ Key findings
1074
+ </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">
1075
+ Hello! What can you do?
1076
+ </button>
1077
+ <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">
1078
+ Tell me a fun fact
1079
+ </button>`}
1080
+ </div>
1081
+ </div>
1082
+ )}
1083
+ {messages.map((msg) => (
1084
+ <div key={msg.id} className={\`mb-6 flex \${msg.role === "user" ? "justify-end" : "justify-start"}\`}>
1085
+ <div className={\`flex items-start gap-3 max-w-[80%] \${msg.role === "user" ? "flex-row-reverse" : ""}\`}>
1086
+ <div className={\`w-8 h-8 rounded-full flex items-center justify-center text-sm shrink-0 \${
1087
+ msg.role === "user" ? "bg-${accentColor}-500 text-white" : "bg-white/10 text-muted"
1088
+ }\`}>
1089
+ {msg.role === "user" ? "Y" : "\u26A1"}
1090
+ </div>
1091
+ <div className={\`px-4 py-3 rounded-2xl whitespace-pre-wrap leading-relaxed text-sm \${
1092
+ msg.role === "user"
1093
+ ? "bg-${accentColor}-500 text-white rounded-br-md"
1094
+ : "bg-white/[0.05] border border-border rounded-bl-md"
1095
+ }\`}>
1096
+ {msg.content || (isLoading && msg.role === "assistant" ? (
1097
+ <span className="flex gap-1">
1098
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:0ms]" />
1099
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:150ms]" />
1100
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:300ms]" />
1101
+ </span>
1102
+ ) : "")}
1103
+ </div>
1104
+ </div>
1105
+ </div>
1106
+ ))}
1107
+ <div ref={messagesEndRef} />
1108
+ </div>
1109
+ </main>
1110
+ <footer className="border-t border-border px-4 py-4">
1111
+ <form onSubmit={(e) => { e.preventDefault(); sendMessage(); }} className="max-w-3xl mx-auto flex gap-3">
1112
+ <input
1113
+ ref={inputRef}
1114
+ value={input}
1115
+ onChange={(e) => setInput(e.target.value)}
1116
+ placeholder="${inputPlaceholder}"
1117
+ disabled={isLoading}
1118
+ 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"
1119
+ />
1120
+ <button
1121
+ type="submit"
1122
+ disabled={isLoading || !input.trim()}
1123
+ 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"
1124
+ >
1125
+ {isLoading ? (
1126
+ <svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
1127
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
1128
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
1129
+ </svg>
1130
+ ) : "Send"}
1131
+ </button>
1132
+ </form>
1133
+ </footer>
1134
+ </div>
1135
+ );
1136
+ }
1137
+ `;
1138
+ }
1139
+ function generateFaviconSVG() {
1140
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1141
+ <rect width="32" height="32" rx="6" fill="#0a0a0a"/>
1142
+ <path d="M18.5 4L8 18h7l-1.5 10L24 14h-7l1.5-10z" fill="#facc15" stroke="#facc15" stroke-width="0.5" stroke-linejoin="round"/>
1143
+ </svg>
1144
+ `;
1145
+ }
1146
+ function generateRobotsTxt() {
1147
+ return `# https://www.robotstxt.org/robotstxt.html
1148
+ User-agent: *
1149
+ Allow: /
1150
+
1151
+ Sitemap: /sitemap.xml
1152
+ `;
1153
+ }
1154
+ function generateWebManifest(projectName) {
1155
+ return JSON.stringify({
1156
+ name: projectName,
1157
+ short_name: projectName,
1158
+ icons: [
1159
+ { src: "/favicon.svg", sizes: "any", type: "image/svg+xml" }
1160
+ ],
1161
+ theme_color: "#0a0a0a",
1162
+ background_color: "#0a0a0a",
1163
+ display: "standalone"
1164
+ }, null, 2) + "\n";
1165
+ }
542
1166
 
543
1167
  // src/dev.ts
544
1168
  var import_node_child_process = require("child_process");
545
1169
  var import_node_path = require("path");
546
1170
  var import_node_fs = require("fs");
1171
+ var import_core = require("@voltx/core");
547
1172
  async function runDev(options = {}) {
548
1173
  const cwd = process.cwd();
549
1174
  const {
@@ -552,7 +1177,7 @@ async function runDev(options = {}) {
552
1177
  clearScreen = true
553
1178
  } = options;
554
1179
  if (!entry) {
555
- console.error("[voltx] Could not find entry point. Expected src/index.ts or src/index.js");
1180
+ console.error("[voltx] Could not find entry point. Expected server.ts or src/index.ts");
556
1181
  process.exit(1);
557
1182
  }
558
1183
  const entryPath = (0, import_node_path.resolve)(cwd, entry);
@@ -560,56 +1185,124 @@ async function runDev(options = {}) {
560
1185
  console.error(`[voltx] Entry file not found: ${entry}`);
561
1186
  process.exit(1);
562
1187
  }
563
- const envFile = (0, import_node_path.resolve)(cwd, ".env");
1188
+ (0, import_core.loadEnv)("development", cwd);
1189
+ const hasViteConfig = (0, import_node_fs.existsSync)((0, import_node_path.resolve)(cwd, "vite.config.ts"));
1190
+ const hasServerEntry = (0, import_node_fs.existsSync)((0, import_node_path.resolve)(cwd, "server.ts"));
1191
+ const isFullStack = hasViteConfig || hasServerEntry;
1192
+ if (isFullStack) {
1193
+ await startViteDevServer(cwd, port, entry, options);
1194
+ } else {
1195
+ await startTsxWatch(cwd, port, entry, options);
1196
+ }
1197
+ }
1198
+ async function startViteDevServer(cwd, port, entry, options) {
1199
+ const resolvedPort = port ?? (Number(process.env.PORT) || 3e3);
1200
+ printDevBanner(entry, resolvedPort, true);
1201
+ const userViteConfig = (0, import_node_path.resolve)(cwd, "vite.config.ts");
1202
+ const hasUserViteConfig = (0, import_node_fs.existsSync)(userViteConfig);
1203
+ let tempConfigPath = null;
1204
+ if (!hasUserViteConfig) {
1205
+ tempConfigPath = (0, import_node_path.resolve)(cwd, "vite.config.voltx.ts");
1206
+ const viteConfigContent = generateViteConfig(entry, resolvedPort);
1207
+ (0, import_node_fs.writeFileSync)(tempConfigPath, viteConfigContent, "utf-8");
1208
+ }
564
1209
  const env = {
565
1210
  ...process.env,
566
1211
  NODE_ENV: "development"
567
1212
  };
568
- if ((0, import_node_fs.existsSync)(envFile)) {
569
- const envContent = (0, import_node_fs.readFileSync)(envFile, "utf-8");
570
- for (const line of envContent.split("\n")) {
571
- const trimmed = line.trim();
572
- if (!trimmed || trimmed.startsWith("#")) continue;
573
- const eqIdx = trimmed.indexOf("=");
574
- if (eqIdx === -1) continue;
575
- const key = trimmed.slice(0, eqIdx).trim();
576
- const value = trimmed.slice(eqIdx + 1).trim();
577
- env[key] = value;
1213
+ if (port) {
1214
+ env.PORT = String(port);
1215
+ }
1216
+ const viteBin = findBin(cwd, "vite");
1217
+ const viteArgs = ["dev"];
1218
+ if (tempConfigPath) {
1219
+ viteArgs.push("--config", tempConfigPath);
1220
+ }
1221
+ viteArgs.push("--port", String(resolvedPort));
1222
+ let child;
1223
+ if (viteBin) {
1224
+ child = (0, import_node_child_process.spawn)(viteBin, viteArgs, { cwd, env, stdio: "inherit" });
1225
+ } else {
1226
+ child = (0, import_node_child_process.spawn)("npx", ["vite", ...viteArgs], { cwd, env, stdio: "inherit" });
1227
+ }
1228
+ const cleanup = () => {
1229
+ if (tempConfigPath && (0, import_node_fs.existsSync)(tempConfigPath)) {
1230
+ try {
1231
+ (0, import_node_fs.unlinkSync)(tempConfigPath);
1232
+ } catch {
1233
+ }
578
1234
  }
1235
+ };
1236
+ const signals = ["SIGINT", "SIGTERM"];
1237
+ for (const signal of signals) {
1238
+ process.on(signal, () => {
1239
+ cleanup();
1240
+ child.kill(signal);
1241
+ });
579
1242
  }
1243
+ child.on("error", (err) => {
1244
+ cleanup();
1245
+ if (err.code === "ENOENT") {
1246
+ console.error("[voltx] vite not found. Install it with: npm install -D vite @hono/vite-dev-server");
1247
+ } else {
1248
+ console.error("[voltx] Dev server error:", err.message);
1249
+ }
1250
+ process.exit(1);
1251
+ });
1252
+ child.on("exit", (code) => {
1253
+ cleanup();
1254
+ process.exit(code ?? 0);
1255
+ });
1256
+ }
1257
+ function generateViteConfig(entry, port) {
1258
+ return `// Auto-generated by VoltX \u2014 do not commit this file
1259
+ import { defineConfig } from "vite";
1260
+ import devServer from "@hono/vite-dev-server";
1261
+
1262
+ export default defineConfig({
1263
+ server: {
1264
+ port: ${port},
1265
+ },
1266
+ plugins: [
1267
+ devServer({
1268
+ entry: "${entry}",
1269
+ exclude: [
1270
+ /.*\\.tsx?($|\\?)/,
1271
+ /.*\\.(s?css|less)($|\\?)/,
1272
+ /.*\\.(svg|png|jpg|jpeg|gif|webp|ico)($|\\?)/,
1273
+ /^\\/@.+$/,
1274
+ /^\\/favicon\\.svg$/,
1275
+ /^\\/node_modules\\/.*/,
1276
+ /^\\/src\\/.*/,
1277
+ ],
1278
+ injectClientScript: false,
1279
+ }),
1280
+ ],
1281
+ });
1282
+ `;
1283
+ }
1284
+ async function startTsxWatch(cwd, port, entry, options) {
1285
+ const resolvedPort = port ?? (Number(process.env.PORT) || 3e3);
1286
+ printDevBanner(entry, resolvedPort, false);
1287
+ const env = {
1288
+ ...process.env,
1289
+ NODE_ENV: "development"
1290
+ };
580
1291
  if (port) {
581
1292
  env.PORT = String(port);
582
1293
  }
583
- printDevBanner(entry, port);
584
1294
  const tsxArgs = ["watch"];
585
- if (clearScreen) {
1295
+ if (options.clearScreen ?? true) {
586
1296
  tsxArgs.push("--clear-screen=false");
587
1297
  }
588
- const watchDirs = [
589
- "src/routes",
590
- "src/agents",
591
- "src/tools",
592
- "src/jobs",
593
- "src/lib",
594
- "voltx.config.ts",
595
- ...options.watch ?? []
596
- ];
597
1298
  tsxArgs.push("--ignore=node_modules", "--ignore=dist", "--ignore=.turbo");
598
1299
  tsxArgs.push(entry);
599
- const tsxBin = findTsxBin(cwd);
1300
+ const tsxBin = findBin(cwd, "tsx");
600
1301
  let child;
601
1302
  if (tsxBin) {
602
- child = (0, import_node_child_process.spawn)(tsxBin, tsxArgs, {
603
- cwd,
604
- env,
605
- stdio: "inherit"
606
- });
1303
+ child = (0, import_node_child_process.spawn)(tsxBin, tsxArgs, { cwd, env, stdio: "inherit" });
607
1304
  } else {
608
- child = (0, import_node_child_process.spawn)("npx", ["tsx", ...tsxArgs], {
609
- cwd,
610
- env,
611
- stdio: "inherit"
612
- });
1305
+ child = (0, import_node_child_process.spawn)("npx", ["tsx", ...tsxArgs], { cwd, env, stdio: "inherit" });
613
1306
  }
614
1307
  const signals = ["SIGINT", "SIGTERM"];
615
1308
  for (const signal of signals) {
@@ -620,7 +1313,6 @@ async function runDev(options = {}) {
620
1313
  child.on("error", (err) => {
621
1314
  if (err.code === "ENOENT") {
622
1315
  console.error("[voltx] tsx not found. Install it with: npm install -D tsx");
623
- console.error("[voltx] Or run your app directly: npx tsx watch src/index.ts");
624
1316
  } else {
625
1317
  console.error("[voltx] Dev server error:", err.message);
626
1318
  }
@@ -632,6 +1324,8 @@ async function runDev(options = {}) {
632
1324
  }
633
1325
  function findEntryPoint(cwd) {
634
1326
  const candidates = [
1327
+ "server.ts",
1328
+ "server.js",
635
1329
  "src/index.ts",
636
1330
  "src/index.js",
637
1331
  "src/index.mts",
@@ -647,24 +1341,27 @@ function findEntryPoint(cwd) {
647
1341
  }
648
1342
  return null;
649
1343
  }
650
- function findTsxBin(cwd) {
651
- const localBin = (0, import_node_path.join)(cwd, "node_modules", ".bin", "tsx");
652
- if ((0, import_node_fs.existsSync)(localBin)) return localBin;
653
- const parentBin = (0, import_node_path.join)(cwd, "..", "node_modules", ".bin", "tsx");
654
- if ((0, import_node_fs.existsSync)(parentBin)) return parentBin;
655
- const rootBin = (0, import_node_path.join)(cwd, "..", "..", "node_modules", ".bin", "tsx");
656
- if ((0, import_node_fs.existsSync)(rootBin)) return rootBin;
1344
+ function findBin(cwd, name) {
1345
+ const paths = [
1346
+ (0, import_node_path.join)(cwd, "node_modules", ".bin", name),
1347
+ (0, import_node_path.join)(cwd, "..", "node_modules", ".bin", name),
1348
+ (0, import_node_path.join)(cwd, "..", "..", "node_modules", ".bin", name)
1349
+ ];
1350
+ for (const p of paths) {
1351
+ if ((0, import_node_fs.existsSync)(p)) return p;
1352
+ }
657
1353
  return null;
658
1354
  }
659
- function printDevBanner(entry, port) {
1355
+ function printDevBanner(entry, port, fullStack) {
660
1356
  console.log("");
661
1357
  console.log(" \u26A1 VoltX Dev Server");
662
1358
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
663
1359
  console.log(` Entry: ${entry}`);
664
- if (port) {
665
- console.log(` Port: ${port}`);
1360
+ console.log(` Port: ${port}`);
1361
+ console.log(` Mode: ${fullStack ? "full-stack (Vite + Hono)" : "API-only (tsx watch)"}`);
1362
+ if (fullStack) {
1363
+ console.log(` HMR: enabled (frontend + backend)`);
666
1364
  }
667
- console.log(` Mode: development (hot reload)`);
668
1365
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
669
1366
  console.log("");
670
1367
  }
@@ -673,6 +1370,7 @@ function printDevBanner(entry, port) {
673
1370
  var import_node_child_process2 = require("child_process");
674
1371
  var import_node_path2 = require("path");
675
1372
  var import_node_fs2 = require("fs");
1373
+ var import_core2 = require("@voltx/core");
676
1374
  async function runBuild(options = {}) {
677
1375
  const cwd = process.cwd();
678
1376
  const {
@@ -682,7 +1380,7 @@ async function runBuild(options = {}) {
682
1380
  sourcemap = false
683
1381
  } = options;
684
1382
  if (!entry) {
685
- console.error("[voltx] Could not find entry point. Expected src/index.ts");
1383
+ console.error("[voltx] Could not find entry point. Expected server.ts or src/index.ts");
686
1384
  process.exit(1);
687
1385
  }
688
1386
  const entryPath = (0, import_node_path2.resolve)(cwd, entry);
@@ -690,59 +1388,87 @@ async function runBuild(options = {}) {
690
1388
  console.error(`[voltx] Entry file not found: ${entry}`);
691
1389
  process.exit(1);
692
1390
  }
1391
+ (0, import_core2.loadEnv)("production", cwd);
1392
+ const hasViteConfig = (0, import_node_fs2.existsSync)((0, import_node_path2.resolve)(cwd, "vite.config.ts"));
1393
+ const hasServerEntry = (0, import_node_fs2.existsSync)((0, import_node_path2.resolve)(cwd, "server.ts"));
1394
+ const isFullStack = hasViteConfig || hasServerEntry;
693
1395
  console.log("");
694
1396
  console.log(" \u26A1 VoltX Build");
695
1397
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
696
1398
  console.log(` Entry: ${entry}`);
697
1399
  console.log(` Output: ${outDir}/`);
1400
+ console.log(` Mode: ${isFullStack ? "full-stack" : "API-only"}`);
698
1401
  console.log(` Minify: ${minify}`);
699
1402
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
700
1403
  console.log("");
701
1404
  (0, import_node_fs2.mkdirSync)((0, import_node_path2.resolve)(cwd, outDir), { recursive: true });
702
- console.log(" [1/2] Building server...");
1405
+ if (isFullStack) {
1406
+ await buildFullStack(cwd, entry, outDir, minify, sourcemap);
1407
+ } else {
1408
+ await buildApiOnly(cwd, entry, outDir, minify, sourcemap);
1409
+ }
1410
+ console.log("");
1411
+ console.log(" \u26A1 Build complete!");
1412
+ console.log(` Run \`voltx start\` to start the production server.`);
1413
+ console.log("");
1414
+ }
1415
+ async function buildFullStack(cwd, entry, outDir, minify, sourcemap) {
1416
+ const ssrEntry = (0, import_node_fs2.existsSync)((0, import_node_path2.join)(cwd, "src", "entry-server.tsx")) ? "src/entry-server.tsx" : null;
1417
+ const totalSteps = ssrEntry ? 3 : 2;
1418
+ console.log(` [1/${totalSteps}] Building client...`);
1419
+ const viteBin = findBin2(cwd, "vite");
1420
+ const clientOutDir = (0, import_node_path2.join)(outDir, "client");
1421
+ await runCommand(
1422
+ viteBin ?? "npx",
1423
+ viteBin ? ["build", "--outDir", clientOutDir] : ["vite", "build", "--outDir", clientOutDir],
1424
+ cwd
1425
+ );
1426
+ console.log(" \u2713 Client built");
1427
+ if (ssrEntry) {
1428
+ console.log(` [2/${totalSteps}] Building SSR bundle...`);
1429
+ const serverOutDir = (0, import_node_path2.join)(outDir, "server");
1430
+ await runCommand(
1431
+ viteBin ?? "npx",
1432
+ viteBin ? ["build", "--ssr", ssrEntry, "--outDir", serverOutDir] : ["vite", "build", "--ssr", ssrEntry, "--outDir", serverOutDir],
1433
+ cwd
1434
+ );
1435
+ console.log(" \u2713 SSR bundle built");
1436
+ }
1437
+ const serverStep = ssrEntry ? 3 : 2;
1438
+ console.log(` [${serverStep}/${totalSteps}] Building server...`);
1439
+ await buildServer(cwd, entry, outDir, minify, sourcemap, false);
1440
+ console.log(" \u2713 Server built");
1441
+ }
1442
+ async function buildApiOnly(cwd, entry, outDir, minify, sourcemap) {
1443
+ console.log(" [1/1] Building server...");
1444
+ await buildServer(cwd, entry, outDir, minify, sourcemap);
1445
+ console.log(" \u2713 Server built");
1446
+ }
1447
+ async function buildServer(cwd, entry, outDir, minify, sourcemap, clean = true) {
1448
+ const tsupBin = findBin2(cwd, "tsup");
703
1449
  const tsupArgs = [
704
1450
  entry,
705
1451
  "--format",
706
1452
  "esm",
707
1453
  "--out-dir",
708
1454
  outDir,
709
- "--clean",
710
1455
  "--target",
711
- "node20"
1456
+ "node20",
1457
+ "--no-splitting"
712
1458
  ];
1459
+ if (clean) tsupArgs.push("--clean");
713
1460
  if (minify) tsupArgs.push("--minify");
714
1461
  if (sourcemap) tsupArgs.push("--sourcemap");
715
- tsupArgs.push("--no-splitting");
716
- const tsupBin = findBin(cwd, "tsup");
717
1462
  await runCommand(
718
1463
  tsupBin ?? "npx",
719
1464
  tsupBin ? tsupArgs : ["tsup", ...tsupArgs],
720
1465
  cwd
721
1466
  );
722
- console.log(" \u2713 Server built successfully");
723
- const frontendDir = (0, import_node_path2.resolve)(cwd, "src", "frontend");
724
- const viteConfig = (0, import_node_path2.resolve)(cwd, "vite.config.ts");
725
- const hasFrontend = (0, import_node_fs2.existsSync)(frontendDir) || (0, import_node_fs2.existsSync)(viteConfig);
726
- if (hasFrontend) {
727
- console.log(" [2/2] Building frontend...");
728
- const viteBin = findBin(cwd, "vite");
729
- const viteArgs = ["build", "--outDir", (0, import_node_path2.join)(outDir, "public")];
730
- await runCommand(
731
- viteBin ?? "npx",
732
- viteBin ? viteArgs : ["vite", ...viteArgs],
733
- cwd
734
- );
735
- console.log(" \u2713 Frontend built successfully");
736
- } else {
737
- console.log(" [2/2] No frontend found, skipping...");
738
- }
739
- console.log("");
740
- console.log(" \u26A1 Build complete!");
741
- console.log(` Run \`voltx start\` to start the production server.`);
742
- console.log("");
743
1467
  }
744
1468
  function findEntryPoint2(cwd) {
745
1469
  const candidates = [
1470
+ "server.ts",
1471
+ "server.js",
746
1472
  "src/index.ts",
747
1473
  "src/index.js",
748
1474
  "src/index.mts",
@@ -756,7 +1482,7 @@ function findEntryPoint2(cwd) {
756
1482
  }
757
1483
  return null;
758
1484
  }
759
- function findBin(cwd, name) {
1485
+ function findBin2(cwd, name) {
760
1486
  const paths = [
761
1487
  (0, import_node_path2.join)(cwd, "node_modules", ".bin", name),
762
1488
  (0, import_node_path2.join)(cwd, "..", "node_modules", ".bin", name),
@@ -791,6 +1517,7 @@ function runCommand(cmd, args, cwd) {
791
1517
  var import_node_child_process3 = require("child_process");
792
1518
  var import_node_path3 = require("path");
793
1519
  var import_node_fs3 = require("fs");
1520
+ var import_core3 = require("@voltx/core");
794
1521
  async function runStart(options = {}) {
795
1522
  const cwd = process.cwd();
796
1523
  const { port, outDir = "dist" } = options;
@@ -811,26 +1538,15 @@ async function runStart(options = {}) {
811
1538
  console.error(`[voltx] Entry file not found: ${outDir}/${entry}`);
812
1539
  process.exit(1);
813
1540
  }
1541
+ (0, import_core3.loadEnv)("production", cwd);
814
1542
  const env = {
815
1543
  ...process.env,
816
1544
  NODE_ENV: "production"
817
1545
  };
818
- const envFile = (0, import_node_path3.resolve)(cwd, ".env");
819
- if ((0, import_node_fs3.existsSync)(envFile)) {
820
- const envContent = (0, import_node_fs3.readFileSync)(envFile, "utf-8");
821
- for (const line of envContent.split("\n")) {
822
- const trimmed = line.trim();
823
- if (!trimmed || trimmed.startsWith("#")) continue;
824
- const eqIdx = trimmed.indexOf("=");
825
- if (eqIdx === -1) continue;
826
- const key = trimmed.slice(0, eqIdx).trim();
827
- const value = trimmed.slice(eqIdx + 1).trim();
828
- env[key] = value;
829
- }
830
- }
831
1546
  if (port) {
832
1547
  env.PORT = String(port);
833
1548
  }
1549
+ const hasClientBuild = (0, import_node_fs3.existsSync)((0, import_node_path3.resolve)(distDir, "client"));
834
1550
  console.log("");
835
1551
  console.log(" \u26A1 VoltX Production Server");
836
1552
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
@@ -838,7 +1554,7 @@ async function runStart(options = {}) {
838
1554
  if (port) {
839
1555
  console.log(` Port: ${port}`);
840
1556
  }
841
- console.log(` Mode: production`);
1557
+ console.log(` Mode: ${hasClientBuild ? "full-stack" : "API-only"}`);
842
1558
  console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
843
1559
  console.log("");
844
1560
  const child = (0, import_node_child_process3.spawn)("node", [entryPath], {
@@ -862,13 +1578,13 @@ async function runStart(options = {}) {
862
1578
  }
863
1579
  function findDistEntry(distDir) {
864
1580
  const candidates = [
1581
+ "server.mjs",
1582
+ "server.js",
865
1583
  "index.mjs",
866
1584
  "index.js",
867
1585
  "index.cjs",
868
1586
  "main.mjs",
869
- "main.js",
870
- "src/index.mjs",
871
- "src/index.js"
1587
+ "main.js"
872
1588
  ];
873
1589
  for (const candidate of candidates) {
874
1590
  if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(distDir, candidate))) {
@@ -905,13 +1621,13 @@ async function runGenerate(options) {
905
1621
  }
906
1622
  function generateRoute(cwd, name, method = "POST") {
907
1623
  const routePath = name.startsWith("/") ? name.slice(1) : name;
908
- const filePath = (0, import_node_path4.join)(cwd, "src", "routes", `${routePath}.ts`);
1624
+ const filePath = (0, import_node_path4.join)(cwd, "api", `${routePath}.ts`);
909
1625
  if ((0, import_node_fs4.existsSync)(filePath)) {
910
- console.error(`[voltx] Route already exists: src/routes/${routePath}.ts`);
1626
+ console.error(`[voltx] Route already exists: api/${routePath}.ts`);
911
1627
  process.exit(1);
912
1628
  }
913
1629
  const upperMethod = method.toUpperCase();
914
- const urlPath = "/" + routePath;
1630
+ const urlPath = "/api/" + routePath;
915
1631
  const content = `// ${upperMethod} ${urlPath}
916
1632
  import type { Context } from "@voltx/server";
917
1633
 
@@ -920,13 +1636,13 @@ export async function ${upperMethod}(c: Context) {
920
1636
  }
921
1637
  `;
922
1638
  writeFileSafe(filePath, content);
923
- console.log(` \u2713 Created route: src/routes/${routePath}.ts`);
1639
+ console.log(` \u2713 Created route: api/${routePath}.ts`);
924
1640
  console.log(` ${upperMethod} ${urlPath}`);
925
1641
  }
926
1642
  function generateAgent(cwd, name) {
927
- const filePath = (0, import_node_path4.join)(cwd, "src", "agents", `${name}.ts`);
1643
+ const filePath = (0, import_node_path4.join)(cwd, "agents", `${name}.ts`);
928
1644
  if ((0, import_node_fs4.existsSync)(filePath)) {
929
- console.error(`[voltx] Agent already exists: src/agents/${name}.ts`);
1645
+ console.error(`[voltx] Agent already exists: agents/${name}.ts`);
930
1646
  process.exit(1);
931
1647
  }
932
1648
  const content = `// Agent: ${name}
@@ -948,12 +1664,12 @@ export const ${toCamelCase(name)} = createAgent({
948
1664
  });
949
1665
  `;
950
1666
  writeFileSafe(filePath, content);
951
- console.log(` \u2713 Created agent: src/agents/${name}.ts`);
1667
+ console.log(` \u2713 Created agent: agents/${name}.ts`);
952
1668
  }
953
1669
  function generateTool(cwd, name) {
954
- const filePath = (0, import_node_path4.join)(cwd, "src", "tools", `${name}.ts`);
1670
+ const filePath = (0, import_node_path4.join)(cwd, "tools", `${name}.ts`);
955
1671
  if ((0, import_node_fs4.existsSync)(filePath)) {
956
- console.error(`[voltx] Tool already exists: src/tools/${name}.ts`);
1672
+ console.error(`[voltx] Tool already exists: tools/${name}.ts`);
957
1673
  process.exit(1);
958
1674
  }
959
1675
  const content = `// Tool: ${name}
@@ -975,12 +1691,12 @@ export const ${toCamelCase(name)}Tool = {
975
1691
  };
976
1692
  `;
977
1693
  writeFileSafe(filePath, content);
978
- console.log(` \u2713 Created tool: src/tools/${name}.ts`);
1694
+ console.log(` \u2713 Created tool: tools/${name}.ts`);
979
1695
  }
980
1696
  function generateJob(cwd, name) {
981
- const filePath = (0, import_node_path4.join)(cwd, "src", "jobs", `${name}.ts`);
1697
+ const filePath = (0, import_node_path4.join)(cwd, "jobs", `${name}.ts`);
982
1698
  if ((0, import_node_fs4.existsSync)(filePath)) {
983
- console.error(`[voltx] Job already exists: src/jobs/${name}.ts`);
1699
+ console.error(`[voltx] Job already exists: jobs/${name}.ts`);
984
1700
  process.exit(1);
985
1701
  }
986
1702
  const content = `// Job: ${name}
@@ -1005,7 +1721,7 @@ export async function run(ctx: any, data?: Record<string, unknown>) {
1005
1721
  }
1006
1722
  `;
1007
1723
  writeFileSafe(filePath, content);
1008
- console.log(` \u2713 Created job: src/jobs/${name}.ts`);
1724
+ console.log(` \u2713 Created job: jobs/${name}.ts`);
1009
1725
  }
1010
1726
  function writeFileSafe(filePath, content) {
1011
1727
  const dir = (0, import_node_path4.dirname)(filePath);
@@ -1017,7 +1733,7 @@ function toCamelCase(str) {
1017
1733
  }
1018
1734
 
1019
1735
  // src/index.ts
1020
- var CLI_VERSION = "0.3.3";
1736
+ var CLI_VERSION = "0.3.5";
1021
1737
  // Annotate the CommonJS export names for ESM import in node:
1022
1738
  0 && (module.exports = {
1023
1739
  CLI_VERSION,