@voltx/cli 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +54 -22
  2. package/dist/build.d.mts +4 -3
  3. package/dist/build.d.ts +4 -3
  4. package/dist/build.js +56 -27
  5. package/dist/build.mjs +1 -2
  6. package/dist/chunk-2LHDOMF2.mjs +680 -0
  7. package/dist/chunk-5DVBIJXU.mjs +84 -0
  8. package/dist/chunk-65PVXS4D.mjs +894 -0
  9. package/dist/chunk-7JVIEGSA.mjs +141 -0
  10. package/dist/chunk-A4NA4AJN.mjs +786 -0
  11. package/dist/chunk-AAAHANST.mjs +839 -0
  12. package/dist/chunk-AD3WMFZF.mjs +205 -0
  13. package/dist/chunk-CSSHLVYS.mjs +83 -0
  14. package/dist/chunk-CWOSNV5O.mjs +150 -0
  15. package/dist/chunk-EI6XBYKB.mjs +84 -0
  16. package/dist/chunk-FI2W4L4S.mjs +205 -0
  17. package/dist/chunk-G2INQCCJ.mjs +907 -0
  18. package/dist/chunk-H2DTIOEO.mjs +150 -0
  19. package/dist/chunk-IS2WTE3C.mjs +138 -0
  20. package/dist/chunk-JECCDBYI.mjs +730 -0
  21. package/dist/chunk-KX2MRJUO.mjs +795 -0
  22. package/dist/chunk-LTGMHAZS.mjs +147 -0
  23. package/dist/chunk-OPO6RUFP.mjs +698 -0
  24. package/dist/chunk-PWQSKYAM.mjs +682 -0
  25. package/dist/chunk-Q5XCFN7L.mjs +1026 -0
  26. package/dist/chunk-QSU6FZC7.mjs +497 -0
  27. package/dist/chunk-RYWRFHEC.mjs +83 -0
  28. package/dist/chunk-SU4Q3PTH.mjs +201 -0
  29. package/dist/chunk-UXI3QSDN.mjs +121 -0
  30. package/dist/chunk-VD3CNPNP.mjs +123 -0
  31. package/dist/chunk-X6VOAPRJ.mjs +756 -0
  32. package/dist/cli.js +932 -307
  33. package/dist/cli.mjs +7 -6
  34. package/dist/create.d.mts +1 -0
  35. package/dist/create.d.ts +1 -0
  36. package/dist/create.js +723 -191
  37. package/dist/create.mjs +1 -2
  38. package/dist/dev.d.mts +6 -4
  39. package/dist/dev.d.ts +6 -4
  40. package/dist/dev.js +119 -46
  41. package/dist/dev.mjs +1 -2
  42. package/dist/generate.js +13 -13
  43. package/dist/generate.mjs +1 -2
  44. package/dist/index.js +919 -295
  45. package/dist/index.mjs +5 -6
  46. package/dist/start.js +7 -17
  47. package/dist/start.mjs +1 -2
  48. package/dist/welcome.mjs +0 -1
  49. package/package.json +11 -3
package/dist/index.js CHANGED
@@ -148,15 +148,28 @@ function printWelcomeBanner(projectName) {
148
148
  }
149
149
 
150
150
  // src/create.ts
151
- var V = "^0.3.0";
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
+ }
152
165
  var TEMPLATE_DEPS = {
153
- blank: { "@voltx/core": V, "@voltx/server": V },
154
- chatbot: { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/memory": V },
155
- "rag-app": { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/rag": V, "@voltx/db": V },
156
- "agent-app": { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/agents": V, "@voltx/memory": V }
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") }
157
170
  };
158
171
  async function createProject(options) {
159
- const { name, template = "blank", auth = "none" } = options;
172
+ const { name, template = "blank", auth = "none", shadcn = false } = options;
160
173
  const targetDir = path.resolve(process.cwd(), name);
161
174
  if (fs.existsSync(targetDir)) {
162
175
  console.error(`[voltx] Directory "${name}" already exists.`);
@@ -166,21 +179,39 @@ async function createProject(options) {
166
179
  const provider = template === "rag-app" ? "openai" : "cerebras";
167
180
  const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
168
181
  const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
169
- const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": V };
182
+ const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": v("@voltx/cli") };
170
183
  if (auth === "better-auth") {
171
- deps["@voltx/auth"] = V;
184
+ deps["@voltx/auth"] = v("@voltx/auth");
172
185
  deps["better-auth"] = "^1.5.0";
173
186
  } else if (auth === "jwt") {
174
- deps["@voltx/auth"] = V;
187
+ deps["@voltx/auth"] = v("@voltx/auth");
175
188
  deps["jose"] = "^6.0.0";
176
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
+ }
177
208
  fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
178
209
  name,
179
210
  version: "0.1.0",
180
211
  private: true,
181
212
  scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
182
213
  dependencies: deps,
183
- devDependencies: { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" }
214
+ devDependencies: devDeps
184
215
  }, null, 2));
185
216
  let config = `import { defineConfig } from "@voltx/core";
186
217
 
@@ -201,31 +232,53 @@ export default defineConfig({
201
232
  },`;
202
233
  config += `
203
234
  server: {
204
- routesDir: "src/routes",
235
+ routesDir: "api",
205
236
  staticDir: "public",
206
237
  cors: true,
207
238
  },
208
239
  });
209
240
  `;
210
241
  fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
211
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api"), { recursive: true });
242
+ fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
212
243
  fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
213
- fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify({
214
- compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", strict: true, esModuleInterop: true, skipLibCheck: true, outDir: "dist" },
215
- include: ["src", "voltx.config.ts"]
216
- }, null, 2));
217
- fs.writeFileSync(
218
- path.join(targetDir, "src", "index.ts"),
219
- `import { createApp } from "@voltx/core";
220
- import config from "../voltx.config";
221
-
222
- const app = createApp(config);
223
- app.start();
224
- `
225
- );
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));
226
279
  fs.writeFileSync(
227
- path.join(targetDir, "src", "routes", "index.ts"),
228
- `// GET / \u2014 Health check
280
+ path.join(targetDir, "api", "index.ts"),
281
+ `// GET /api \u2014 Health check
229
282
  import type { Context } from "@voltx/server";
230
283
 
231
284
  export function GET(c: Context) {
@@ -235,7 +288,7 @@ export function GET(c: Context) {
235
288
  );
236
289
  if (template === "chatbot" || template === "agent-app") {
237
290
  fs.writeFileSync(
238
- path.join(targetDir, "src", "routes", "api", "chat.ts"),
291
+ path.join(targetDir, "api", "chat.ts"),
239
292
  `// POST /api/chat \u2014 Streaming chat with conversation memory
240
293
  import type { Context } from "@voltx/server";
241
294
  import { streamText } from "@voltx/ai";
@@ -269,9 +322,9 @@ export async function POST(c: Context) {
269
322
  );
270
323
  }
271
324
  if (template === "agent-app") {
272
- fs.mkdirSync(path.join(targetDir, "src", "agents"), { recursive: true });
273
- fs.mkdirSync(path.join(targetDir, "src", "tools"), { recursive: true });
274
- fs.writeFileSync(path.join(targetDir, "src", "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
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)
275
328
  import type { Tool } from "@voltx/agents";
276
329
 
277
330
  export const calculatorTool: Tool = {
@@ -296,7 +349,7 @@ export const calculatorTool: Tool = {
296
349
  },
297
350
  };
298
351
  `);
299
- fs.writeFileSync(path.join(targetDir, "src", "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
352
+ fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
300
353
  import type { Tool } from "@voltx/agents";
301
354
 
302
355
  export const datetimeTool: Tool = {
@@ -317,7 +370,7 @@ export const datetimeTool: Tool = {
317
370
  },
318
371
  };
319
372
  `);
320
- fs.writeFileSync(path.join(targetDir, "src", "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
373
+ fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
321
374
  import { createAgent } from "@voltx/agents";
322
375
  import { calculatorTool } from "../tools/calculator";
323
376
  import { datetimeTool } from "../tools/datetime";
@@ -331,10 +384,10 @@ export const assistant = createAgent({
331
384
  });
332
385
  `);
333
386
  fs.writeFileSync(
334
- path.join(targetDir, "src", "routes", "api", "agent.ts"),
387
+ path.join(targetDir, "api", "agent.ts"),
335
388
  `// POST /api/agent \u2014 Run the AI agent
336
389
  import type { Context } from "@voltx/server";
337
- import { assistant } from "../../agents/assistant";
390
+ import { assistant } from "../agents/assistant";
338
391
 
339
392
  export async function POST(c: Context) {
340
393
  const { input } = await c.req.json();
@@ -347,9 +400,9 @@ export async function POST(c: Context) {
347
400
  }
348
401
  if (template === "rag-app") {
349
402
  const embedModel = "openai:text-embedding-3-small";
350
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "rag"), { recursive: true });
403
+ fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
351
404
  fs.writeFileSync(
352
- path.join(targetDir, "src", "routes", "api", "rag", "query.ts"),
405
+ path.join(targetDir, "api", "rag", "query.ts"),
353
406
  `// POST /api/rag/query \u2014 Query documents with RAG
354
407
  import type { Context } from "@voltx/server";
355
408
  import { streamText } from "@voltx/ai";
@@ -373,7 +426,7 @@ export async function POST(c: Context) {
373
426
  `
374
427
  );
375
428
  fs.writeFileSync(
376
- path.join(targetDir, "src", "routes", "api", "rag", "ingest.ts"),
429
+ path.join(targetDir, "api", "rag", "ingest.ts"),
377
430
  `// POST /api/rag/ingest \u2014 Ingest documents into the vector store
378
431
  import type { Context } from "@voltx/server";
379
432
  import { createRAGPipeline, createEmbedder } from "@voltx/rag";
@@ -393,12 +446,12 @@ export async function POST(c: Context) {
393
446
  );
394
447
  }
395
448
  if (auth === "better-auth") {
396
- fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "auth"), { recursive: true });
449
+ fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
397
450
  fs.writeFileSync(
398
- path.join(targetDir, "src", "routes", "api", "auth", "[...path].ts"),
451
+ path.join(targetDir, "api", "auth", "[...path].ts"),
399
452
  `// ALL /api/auth/* \u2014 Better Auth handler
400
453
  import type { Context } from "@voltx/server";
401
- import { auth } from "../../../lib/auth";
454
+ import { auth } from "../../src/lib/auth";
402
455
  import { createAuthHandler } from "@voltx/auth";
403
456
 
404
457
  const handler = createAuthHandler(auth);
@@ -407,7 +460,6 @@ export const GET = (c: Context) => handler(c);
407
460
  export const POST = (c: Context) => handler(c);
408
461
  `
409
462
  );
410
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
411
463
  fs.writeFileSync(
412
464
  path.join(targetDir, "src", "lib", "auth.ts"),
413
465
  `import { createAuth, createAuthMiddleware } from "@voltx/auth";
@@ -424,7 +476,6 @@ export const authMiddleware = createAuthMiddleware({
424
476
  `
425
477
  );
426
478
  } else if (auth === "jwt") {
427
- fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
428
479
  fs.writeFileSync(
429
480
  path.join(targetDir, "src", "lib", "auth.ts"),
430
481
  `import { createAuth, createAuthMiddleware } from "@voltx/auth";
@@ -441,9 +492,9 @@ export const authMiddleware = createAuthMiddleware({
441
492
  `
442
493
  );
443
494
  fs.writeFileSync(
444
- path.join(targetDir, "src", "routes", "api", "auth.ts"),
495
+ path.join(targetDir, "api", "auth.ts"),
445
496
  `import type { Context } from "@voltx/server";
446
- import { jwt } from "../../lib/auth";
497
+ import { jwt } from "../src/lib/auth";
447
498
 
448
499
  export async function POST(c: Context) {
449
500
  const { email, password } = await c.req.json();
@@ -460,14 +511,6 @@ export async function POST(c: Context) {
460
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";
461
512
  } else if (template === "chatbot" || template === "agent-app") {
462
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";
463
- if (template === "agent-app") {
464
- envContent += "# \u2500\u2500\u2500 Database (optional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
465
- envContent += "# \u2500\u2500\u2500 Tool API Keys (add keys for tools you use) \u2500\u2500\n";
466
- envContent += "# TAVILY_API_KEY=tvly-... (Web Search \u2014 https://tavily.com)\n";
467
- envContent += "# SERPER_API_KEY= (Google Search \u2014 https://serper.dev)\n";
468
- envContent += "# OPENWEATHER_API_KEY= (Weather \u2014 https://openweathermap.org/api)\n";
469
- envContent += "# NEWS_API_KEY= (News \u2014 https://newsapi.org)\n\n";
470
- }
471
514
  } else {
472
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";
473
516
  }
@@ -478,164 +521,654 @@ export async function POST(c: Context) {
478
521
  }
479
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";
480
523
  fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
481
- if (template !== "blank") {
482
- fs.writeFileSync(path.join(targetDir, "public", "index.html"), generateFrontendHTML(name, template));
483
- }
484
- fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n");
524
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
485
525
  printWelcomeBanner(name);
486
526
  }
487
- function generateFrontendHTML(projectName, template) {
488
- const badge = template === "chatbot" ? "Chatbot" : template === "rag-app" ? "RAG App" : "Agent App";
489
- const accentClass = template === "rag-app" ? "emerald" : template === "agent-app" ? "purple" : "blue";
490
- const shell = (body) => `<!DOCTYPE html>
491
- <html lang="en" class="h-full">
492
- <head>
493
- <meta charset="UTF-8" />
494
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
495
- <title>${projectName}</title>
496
- <script src="https://cdn.tailwindcss.com/3.4.17"></script>
497
- <style>
498
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
499
- body { font-family: 'Inter', system-ui, sans-serif; }
500
- .msg-enter { animation: msgIn 0.25s ease-out; }
501
- @keyframes msgIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
502
- .typing-dot { animation: blink 1.4s infinite both; }
503
- .typing-dot:nth-child(2) { animation-delay: 0.2s; }
504
- .typing-dot:nth-child(3) { animation-delay: 0.4s; }
505
- @keyframes blink { 0%, 80%, 100% { opacity: 0.2; } 40% { opacity: 1; } }
506
- ::-webkit-scrollbar { width: 6px; }
507
- ::-webkit-scrollbar-track { background: transparent; }
508
- ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
509
- </style>
510
- </head>
511
- <body class="h-full bg-gray-950 text-gray-100">
512
- <div id="app" class="h-full flex flex-col">
513
- <header class="flex-shrink-0 border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm px-4 py-3">
514
- <div class="max-w-4xl mx-auto flex items-center justify-between">
515
- <div class="flex items-center gap-3">
516
- <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-sm font-bold">V</div>
517
- <h1 class="text-lg font-semibold text-white">${projectName}</h1>
518
- <span class="text-xs px-2 py-0.5 rounded-full bg-gray-800 text-gray-400">${badge}</span>
519
- </div>
520
- <a href="https://github.com/codewithshail/voltx" target="_blank" class="text-gray-500 hover:text-gray-300 transition-colors text-sm">Built with VoltX</a>
521
- </div>
522
- </header>
523
- <main class="flex-1 overflow-hidden"><div class="h-full max-w-4xl mx-auto">
524
- ${body}
525
- </div></main>
526
- </div>
527
- </body>
528
- </html>`;
529
- if (template === "chatbot") return shell(chatbotBody());
530
- if (template === "rag-app") return shell(ragAppBody());
531
- if (template === "agent-app") return shell(agentAppBody());
532
- return "";
533
- }
534
- function chatbotBody() {
535
- return ` <div class="h-full flex flex-col">
536
- <div id="messages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
537
- <div class="text-center py-12">
538
- <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-blue-500/20 to-purple-600/20 border border-blue-500/20 flex items-center justify-center">
539
- <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
540
- </div>
541
- <h2 class="text-xl font-semibold text-white mb-2">Start a conversation</h2>
542
- <p class="text-gray-500 text-sm">Type a message below to chat with your AI assistant.</p>
543
- </div>
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);');
535
+ }
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\`);
582
+ });
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>
544
668
  </div>
545
- <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
546
- <form id="chatForm" class="flex gap-3">
547
- <input id="chatInput" type="text" placeholder="Type your message..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors" />
548
- <button type="submit" id="sendBtn" class="px-5 py-3 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Send</button>
549
- </form>
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>
550
872
  </div>
551
- </div>
552
- <script>
553
- const messages=[], messagesEl=document.getElementById("messages"), form=document.getElementById("chatForm"), input=document.getElementById("chatInput"), sendBtn=document.getElementById("sendBtn");
554
- function addMsg(role,content){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-blue-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed";b.textContent=content;d.appendChild(b);const w=messagesEl.querySelector(".text-center.py-12");if(w)w.remove();messagesEl.appendChild(d);messagesEl.scrollTop=messagesEl.scrollHeight;return b}
555
- form.addEventListener("submit",async e=>{e.preventDefault();const text=input.value.trim();if(!text)return;messages.push({role:"user",content:text});addMsg("user",text);input.value="";sendBtn.disabled=true;try{const res=await fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({messages,conversationId:"default"})});if(!res.ok){addMsg("assistant","Error: "+res.status);sendBtn.disabled=false;return}const bubble=addMsg("assistant","");const reader=res.body.getReader();const dec=new TextDecoder();let full="";while(true){const{done,value}=await reader.read();if(done)break;const chunk=dec.decode(value,{stream:true});for(const line of chunk.split("\\n")){if(line.startsWith("data: ")){const d=line.slice(6);if(d==="[DONE]")continue;try{const p=JSON.parse(d);const t=p.textDelta||p.choices?.[0]?.delta?.content||p.content||p.text||"";full+=t;bubble.textContent=full;messagesEl.scrollTop=messagesEl.scrollHeight}catch{}}}}messages.push({role:"assistant",content:full})}catch(err){addMsg("assistant","Error: "+err.message)}sendBtn.disabled=false;input.focus()});
556
- input.focus();
557
- </script>`;
558
- }
559
- function ragAppBody() {
560
- return ` <div class="h-full flex flex-col md:flex-row">
561
- <div class="md:w-80 flex-shrink-0 border-b md:border-b-0 md:border-r border-gray-800 flex flex-col">
562
- <div class="px-4 py-3 border-b border-gray-800"><h2 class="text-sm font-semibold text-white flex items-center gap-2"><svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>Ingest Documents</h2></div>
563
- <div class="flex-1 p-4 flex flex-col gap-3">
564
- <textarea id="ingestText" rows="6" placeholder="Paste text to ingest..." class="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-emerald-500 resize-none"></textarea>
565
- <button id="ingestBtn" onclick="ingestDoc()" class="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-lg transition-colors">Ingest</button>
566
- <div id="ingestStatus" class="text-xs text-gray-500 hidden"></div>
567
- <div class="mt-auto pt-3 border-t border-gray-800"><p class="text-xs text-gray-600">Documents are chunked, embedded, and stored for retrieval.</p></div>
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>
568
886
  </div>
569
- </div>
570
- <div class="flex-1 flex flex-col min-w-0">
571
- <div id="ragMessages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
572
- <div class="text-center py-12">
573
- <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-emerald-500/20 to-blue-600/20 border border-emerald-500/20 flex items-center justify-center"><svg class="w-8 h-8 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg></div>
574
- <h2 class="text-xl font-semibold text-white mb-2">Ask your documents</h2>
575
- <p class="text-gray-500 text-sm">Ingest documents on the left, then ask questions here.</p>
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
576
892
  </div>
577
893
  </div>
578
- <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
579
- <form id="ragForm" class="flex gap-3">
580
- <input id="ragInput" type="text" placeholder="Ask about your documents..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors" />
581
- <button type="submit" id="ragSendBtn" class="px-5 py-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Ask</button>
582
- </form>
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>
583
897
  </div>
584
898
  </div>
585
- </div>
586
- <script>
587
- const ragEl=document.getElementById("ragMessages");
588
- function addRagMsg(role,c){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-emerald-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed";b.textContent=c;d.appendChild(b);const w=ragEl.querySelector(".text-center.py-12");if(w)w.remove();ragEl.appendChild(d);ragEl.scrollTop=ragEl.scrollHeight;return b}
589
- async function ingestDoc(){const text=document.getElementById("ingestText").value.trim();if(!text)return;const btn=document.getElementById("ingestBtn"),st=document.getElementById("ingestStatus");btn.disabled=true;btn.textContent="Ingesting...";st.className="text-xs text-yellow-400";st.textContent="Processing...";st.classList.remove("hidden");try{const res=await fetch("/api/rag/ingest",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text})});const data=await res.json();if(res.ok){st.className="text-xs text-emerald-400";st.textContent="Ingested "+(data.chunks||0)+" chunks.";document.getElementById("ingestText").value=""}else{st.className="text-xs text-red-400";st.textContent="Error: "+(data.error||res.statusText)}}catch(e){st.className="text-xs text-red-400";st.textContent="Error: "+e.message}btn.disabled=false;btn.textContent="Ingest"}
590
- document.getElementById("ragForm").addEventListener("submit",async e=>{e.preventDefault();const input=document.getElementById("ragInput"),text=input.value.trim();if(!text)return;addRagMsg("user",text);input.value="";document.getElementById("ragSendBtn").disabled=true;try{const res=await fetch("/api/rag/query",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question:text})});if(!res.ok){addRagMsg("assistant","Error: "+res.status);document.getElementById("ragSendBtn").disabled=false;return}const bubble=addRagMsg("assistant","");const reader=res.body.getReader();const dec=new TextDecoder();let full="";while(true){const{done,value}=await reader.read();if(done)break;const chunk=dec.decode(value,{stream:true});for(const line of chunk.split("\\n")){if(line.startsWith("data: ")){const d=line.slice(6);if(d==="[DONE]")continue;try{const p=JSON.parse(d);const t=p.textDelta||p.choices?.[0]?.delta?.content||p.content||p.text||"";full+=t;bubble.textContent=full;ragEl.scrollTop=ragEl.scrollHeight}catch{}}}};}catch(err){addRagMsg("assistant","Error: "+err.message)}document.getElementById("ragSendBtn").disabled=false;document.getElementById("ragInput").focus()});
591
- document.getElementById("ragInput").focus();
592
- </script>`;
593
- }
594
- function agentAppBody() {
595
- return ` <div class="h-full flex flex-col">
596
- <div id="agentMessages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
597
- <div class="text-center py-12">
598
- <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-purple-500/20 to-orange-500/20 border border-purple-500/20 flex items-center justify-center"><svg class="w-8 h-8 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg></div>
599
- <h2 class="text-xl font-semibold text-white mb-2">AI Agent</h2>
600
- <p class="text-gray-500 text-sm">Your agent can use tools to answer questions accurately.</p>
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>
601
925
  </div>
602
926
  </div>
603
- <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
604
- <form id="agentForm" class="flex gap-3">
605
- <input id="agentInput" type="text" placeholder="Ask the agent anything..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors" />
606
- <button type="submit" id="agentSendBtn" class="px-5 py-3 bg-purple-600 hover:bg-purple-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Run</button>
607
- </form>
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>
608
936
  </div>
609
937
  </div>
610
- <script>
611
- const agentEl=document.getElementById("agentMessages");
612
- function addAgentMsg(role,c,isStep){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");if(isStep){b.className="max-w-[85%] px-3 py-2 rounded-lg bg-gray-900 border border-gray-700 text-xs text-gray-400 font-mono"}else{b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-purple-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed whitespace-pre-wrap"}b.textContent=c;d.appendChild(b);const w=agentEl.querySelector(".text-center.py-12");if(w)w.remove();agentEl.appendChild(d);agentEl.scrollTop=agentEl.scrollHeight;return b}
613
- function addThinking(){const d=document.createElement("div");d.id="thinking";d.className="msg-enter flex justify-start";d.innerHTML='<div class="px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 flex items-center gap-2 text-sm text-gray-400"><svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>Agent is thinking...</div>';agentEl.appendChild(d);agentEl.scrollTop=agentEl.scrollHeight}
614
- function removeThinking(){const t=document.getElementById("thinking");if(t)t.remove()}
615
- document.getElementById("agentForm").addEventListener("submit",async e=>{e.preventDefault();const input=document.getElementById("agentInput"),text=input.value.trim();if(!text)return;addAgentMsg("user",text);input.value="";document.getElementById("agentSendBtn").disabled=true;addThinking();try{const res=await fetch("/api/agent",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:text})});removeThinking();if(!res.ok){const err=await res.json().catch(()=>({}));addAgentMsg("assistant","Error: "+(err.error||res.statusText));document.getElementById("agentSendBtn").disabled=false;return}const data=await res.json();if(data.steps&&data.steps.length>0){for(const s of data.steps){const name=s.tool||s.name||"tool";const inp=typeof s.input==="string"?s.input:JSON.stringify(s.input||{});const out=typeof s.output==="string"?s.output:JSON.stringify(s.output||{});addAgentMsg("assistant","\u{1F527} "+name+"("+inp+")\\n\u2192 "+out.slice(0,300),true)}}addAgentMsg("assistant",data.content||"No response.")}catch(err){removeThinking();addAgentMsg("assistant","Error: "+err.message)}document.getElementById("agentSendBtn").disabled=false;input.focus()});
616
- document.getElementById("agentInput").focus();
617
- </script>`;
618
- }
619
- if (typeof require !== "undefined" && require.main === module && process.argv[1]?.includes("create")) {
620
- const projectName = process.argv[2];
621
- if (!projectName) {
622
- console.log("Usage: create-voltx-app <project-name> [--template chatbot] [--auth jwt]");
623
- process.exit(1);
938
+ </div>
939
+ );
940
+ }
941
+ `;
624
942
  }
625
- const templateFlag = process.argv.indexOf("--template");
626
- const template = templateFlag !== -1 ? process.argv[templateFlag + 1] : "blank";
627
- const authFlag = process.argv.indexOf("--auth");
628
- const auth = authFlag !== -1 ? process.argv[authFlag + 1] : "none";
629
- createProject({ name: projectName, template, auth }).catch((err) => {
630
- console.error("[voltx] Error:", err);
631
- process.exit(1);
632
- });
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";
633
1165
  }
634
1166
 
635
1167
  // src/dev.ts
636
1168
  var import_node_child_process = require("child_process");
637
1169
  var import_node_path = require("path");
638
1170
  var import_node_fs = require("fs");
1171
+ var import_core = require("@voltx/core");
639
1172
  async function runDev(options = {}) {
640
1173
  const cwd = process.cwd();
641
1174
  const {
@@ -644,7 +1177,7 @@ async function runDev(options = {}) {
644
1177
  clearScreen = true
645
1178
  } = options;
646
1179
  if (!entry) {
647
- 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");
648
1181
  process.exit(1);
649
1182
  }
650
1183
  const entryPath = (0, import_node_path.resolve)(cwd, entry);
@@ -652,56 +1185,124 @@ async function runDev(options = {}) {
652
1185
  console.error(`[voltx] Entry file not found: ${entry}`);
653
1186
  process.exit(1);
654
1187
  }
655
- 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
+ }
656
1209
  const env = {
657
1210
  ...process.env,
658
1211
  NODE_ENV: "development"
659
1212
  };
660
- if ((0, import_node_fs.existsSync)(envFile)) {
661
- const envContent = (0, import_node_fs.readFileSync)(envFile, "utf-8");
662
- for (const line of envContent.split("\n")) {
663
- const trimmed = line.trim();
664
- if (!trimmed || trimmed.startsWith("#")) continue;
665
- const eqIdx = trimmed.indexOf("=");
666
- if (eqIdx === -1) continue;
667
- const key = trimmed.slice(0, eqIdx).trim();
668
- const value = trimmed.slice(eqIdx + 1).trim();
669
- 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
+ }
670
1234
  }
1235
+ };
1236
+ const signals = ["SIGINT", "SIGTERM"];
1237
+ for (const signal of signals) {
1238
+ process.on(signal, () => {
1239
+ cleanup();
1240
+ child.kill(signal);
1241
+ });
671
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
+ };
672
1291
  if (port) {
673
1292
  env.PORT = String(port);
674
1293
  }
675
- printDevBanner(entry, port);
676
1294
  const tsxArgs = ["watch"];
677
- if (clearScreen) {
1295
+ if (options.clearScreen ?? true) {
678
1296
  tsxArgs.push("--clear-screen=false");
679
1297
  }
680
- const watchDirs = [
681
- "src/routes",
682
- "src/agents",
683
- "src/tools",
684
- "src/jobs",
685
- "src/lib",
686
- "voltx.config.ts",
687
- ...options.watch ?? []
688
- ];
689
1298
  tsxArgs.push("--ignore=node_modules", "--ignore=dist", "--ignore=.turbo");
690
1299
  tsxArgs.push(entry);
691
- const tsxBin = findTsxBin(cwd);
1300
+ const tsxBin = findBin(cwd, "tsx");
692
1301
  let child;
693
1302
  if (tsxBin) {
694
- child = (0, import_node_child_process.spawn)(tsxBin, tsxArgs, {
695
- cwd,
696
- env,
697
- stdio: "inherit"
698
- });
1303
+ child = (0, import_node_child_process.spawn)(tsxBin, tsxArgs, { cwd, env, stdio: "inherit" });
699
1304
  } else {
700
- child = (0, import_node_child_process.spawn)("npx", ["tsx", ...tsxArgs], {
701
- cwd,
702
- env,
703
- stdio: "inherit"
704
- });
1305
+ child = (0, import_node_child_process.spawn)("npx", ["tsx", ...tsxArgs], { cwd, env, stdio: "inherit" });
705
1306
  }
706
1307
  const signals = ["SIGINT", "SIGTERM"];
707
1308
  for (const signal of signals) {
@@ -712,7 +1313,6 @@ async function runDev(options = {}) {
712
1313
  child.on("error", (err) => {
713
1314
  if (err.code === "ENOENT") {
714
1315
  console.error("[voltx] tsx not found. Install it with: npm install -D tsx");
715
- console.error("[voltx] Or run your app directly: npx tsx watch src/index.ts");
716
1316
  } else {
717
1317
  console.error("[voltx] Dev server error:", err.message);
718
1318
  }
@@ -724,6 +1324,8 @@ async function runDev(options = {}) {
724
1324
  }
725
1325
  function findEntryPoint(cwd) {
726
1326
  const candidates = [
1327
+ "server.ts",
1328
+ "server.js",
727
1329
  "src/index.ts",
728
1330
  "src/index.js",
729
1331
  "src/index.mts",
@@ -739,24 +1341,27 @@ function findEntryPoint(cwd) {
739
1341
  }
740
1342
  return null;
741
1343
  }
742
- function findTsxBin(cwd) {
743
- const localBin = (0, import_node_path.join)(cwd, "node_modules", ".bin", "tsx");
744
- if ((0, import_node_fs.existsSync)(localBin)) return localBin;
745
- const parentBin = (0, import_node_path.join)(cwd, "..", "node_modules", ".bin", "tsx");
746
- if ((0, import_node_fs.existsSync)(parentBin)) return parentBin;
747
- const rootBin = (0, import_node_path.join)(cwd, "..", "..", "node_modules", ".bin", "tsx");
748
- 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
+ }
749
1353
  return null;
750
1354
  }
751
- function printDevBanner(entry, port) {
1355
+ function printDevBanner(entry, port, fullStack) {
752
1356
  console.log("");
753
1357
  console.log(" \u26A1 VoltX Dev Server");
754
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");
755
1359
  console.log(` Entry: ${entry}`);
756
- if (port) {
757
- 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)`);
758
1364
  }
759
- console.log(` Mode: development (hot reload)`);
760
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");
761
1366
  console.log("");
762
1367
  }
@@ -765,6 +1370,7 @@ function printDevBanner(entry, port) {
765
1370
  var import_node_child_process2 = require("child_process");
766
1371
  var import_node_path2 = require("path");
767
1372
  var import_node_fs2 = require("fs");
1373
+ var import_core2 = require("@voltx/core");
768
1374
  async function runBuild(options = {}) {
769
1375
  const cwd = process.cwd();
770
1376
  const {
@@ -774,7 +1380,7 @@ async function runBuild(options = {}) {
774
1380
  sourcemap = false
775
1381
  } = options;
776
1382
  if (!entry) {
777
- 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");
778
1384
  process.exit(1);
779
1385
  }
780
1386
  const entryPath = (0, import_node_path2.resolve)(cwd, entry);
@@ -782,59 +1388,87 @@ async function runBuild(options = {}) {
782
1388
  console.error(`[voltx] Entry file not found: ${entry}`);
783
1389
  process.exit(1);
784
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;
785
1395
  console.log("");
786
1396
  console.log(" \u26A1 VoltX Build");
787
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");
788
1398
  console.log(` Entry: ${entry}`);
789
1399
  console.log(` Output: ${outDir}/`);
1400
+ console.log(` Mode: ${isFullStack ? "full-stack" : "API-only"}`);
790
1401
  console.log(` Minify: ${minify}`);
791
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");
792
1403
  console.log("");
793
1404
  (0, import_node_fs2.mkdirSync)((0, import_node_path2.resolve)(cwd, outDir), { recursive: true });
794
- 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");
795
1449
  const tsupArgs = [
796
1450
  entry,
797
1451
  "--format",
798
1452
  "esm",
799
1453
  "--out-dir",
800
1454
  outDir,
801
- "--clean",
802
1455
  "--target",
803
- "node20"
1456
+ "node20",
1457
+ "--no-splitting"
804
1458
  ];
1459
+ if (clean) tsupArgs.push("--clean");
805
1460
  if (minify) tsupArgs.push("--minify");
806
1461
  if (sourcemap) tsupArgs.push("--sourcemap");
807
- tsupArgs.push("--no-splitting");
808
- const tsupBin = findBin(cwd, "tsup");
809
1462
  await runCommand(
810
1463
  tsupBin ?? "npx",
811
1464
  tsupBin ? tsupArgs : ["tsup", ...tsupArgs],
812
1465
  cwd
813
1466
  );
814
- console.log(" \u2713 Server built successfully");
815
- const frontendDir = (0, import_node_path2.resolve)(cwd, "src", "frontend");
816
- const viteConfig = (0, import_node_path2.resolve)(cwd, "vite.config.ts");
817
- const hasFrontend = (0, import_node_fs2.existsSync)(frontendDir) || (0, import_node_fs2.existsSync)(viteConfig);
818
- if (hasFrontend) {
819
- console.log(" [2/2] Building frontend...");
820
- const viteBin = findBin(cwd, "vite");
821
- const viteArgs = ["build", "--outDir", (0, import_node_path2.join)(outDir, "public")];
822
- await runCommand(
823
- viteBin ?? "npx",
824
- viteBin ? viteArgs : ["vite", ...viteArgs],
825
- cwd
826
- );
827
- console.log(" \u2713 Frontend built successfully");
828
- } else {
829
- console.log(" [2/2] No frontend found, skipping...");
830
- }
831
- console.log("");
832
- console.log(" \u26A1 Build complete!");
833
- console.log(` Run \`voltx start\` to start the production server.`);
834
- console.log("");
835
1467
  }
836
1468
  function findEntryPoint2(cwd) {
837
1469
  const candidates = [
1470
+ "server.ts",
1471
+ "server.js",
838
1472
  "src/index.ts",
839
1473
  "src/index.js",
840
1474
  "src/index.mts",
@@ -848,7 +1482,7 @@ function findEntryPoint2(cwd) {
848
1482
  }
849
1483
  return null;
850
1484
  }
851
- function findBin(cwd, name) {
1485
+ function findBin2(cwd, name) {
852
1486
  const paths = [
853
1487
  (0, import_node_path2.join)(cwd, "node_modules", ".bin", name),
854
1488
  (0, import_node_path2.join)(cwd, "..", "node_modules", ".bin", name),
@@ -883,6 +1517,7 @@ function runCommand(cmd, args, cwd) {
883
1517
  var import_node_child_process3 = require("child_process");
884
1518
  var import_node_path3 = require("path");
885
1519
  var import_node_fs3 = require("fs");
1520
+ var import_core3 = require("@voltx/core");
886
1521
  async function runStart(options = {}) {
887
1522
  const cwd = process.cwd();
888
1523
  const { port, outDir = "dist" } = options;
@@ -903,26 +1538,15 @@ async function runStart(options = {}) {
903
1538
  console.error(`[voltx] Entry file not found: ${outDir}/${entry}`);
904
1539
  process.exit(1);
905
1540
  }
1541
+ (0, import_core3.loadEnv)("production", cwd);
906
1542
  const env = {
907
1543
  ...process.env,
908
1544
  NODE_ENV: "production"
909
1545
  };
910
- const envFile = (0, import_node_path3.resolve)(cwd, ".env");
911
- if ((0, import_node_fs3.existsSync)(envFile)) {
912
- const envContent = (0, import_node_fs3.readFileSync)(envFile, "utf-8");
913
- for (const line of envContent.split("\n")) {
914
- const trimmed = line.trim();
915
- if (!trimmed || trimmed.startsWith("#")) continue;
916
- const eqIdx = trimmed.indexOf("=");
917
- if (eqIdx === -1) continue;
918
- const key = trimmed.slice(0, eqIdx).trim();
919
- const value = trimmed.slice(eqIdx + 1).trim();
920
- env[key] = value;
921
- }
922
- }
923
1546
  if (port) {
924
1547
  env.PORT = String(port);
925
1548
  }
1549
+ const hasClientBuild = (0, import_node_fs3.existsSync)((0, import_node_path3.resolve)(distDir, "client"));
926
1550
  console.log("");
927
1551
  console.log(" \u26A1 VoltX Production Server");
928
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");
@@ -930,7 +1554,7 @@ async function runStart(options = {}) {
930
1554
  if (port) {
931
1555
  console.log(` Port: ${port}`);
932
1556
  }
933
- console.log(` Mode: production`);
1557
+ console.log(` Mode: ${hasClientBuild ? "full-stack" : "API-only"}`);
934
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");
935
1559
  console.log("");
936
1560
  const child = (0, import_node_child_process3.spawn)("node", [entryPath], {
@@ -954,13 +1578,13 @@ async function runStart(options = {}) {
954
1578
  }
955
1579
  function findDistEntry(distDir) {
956
1580
  const candidates = [
1581
+ "server.mjs",
1582
+ "server.js",
957
1583
  "index.mjs",
958
1584
  "index.js",
959
1585
  "index.cjs",
960
1586
  "main.mjs",
961
- "main.js",
962
- "src/index.mjs",
963
- "src/index.js"
1587
+ "main.js"
964
1588
  ];
965
1589
  for (const candidate of candidates) {
966
1590
  if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(distDir, candidate))) {
@@ -997,13 +1621,13 @@ async function runGenerate(options) {
997
1621
  }
998
1622
  function generateRoute(cwd, name, method = "POST") {
999
1623
  const routePath = name.startsWith("/") ? name.slice(1) : name;
1000
- const filePath = (0, import_node_path4.join)(cwd, "src", "routes", `${routePath}.ts`);
1624
+ const filePath = (0, import_node_path4.join)(cwd, "api", `${routePath}.ts`);
1001
1625
  if ((0, import_node_fs4.existsSync)(filePath)) {
1002
- console.error(`[voltx] Route already exists: src/routes/${routePath}.ts`);
1626
+ console.error(`[voltx] Route already exists: api/${routePath}.ts`);
1003
1627
  process.exit(1);
1004
1628
  }
1005
1629
  const upperMethod = method.toUpperCase();
1006
- const urlPath = "/" + routePath;
1630
+ const urlPath = "/api/" + routePath;
1007
1631
  const content = `// ${upperMethod} ${urlPath}
1008
1632
  import type { Context } from "@voltx/server";
1009
1633
 
@@ -1012,13 +1636,13 @@ export async function ${upperMethod}(c: Context) {
1012
1636
  }
1013
1637
  `;
1014
1638
  writeFileSafe(filePath, content);
1015
- console.log(` \u2713 Created route: src/routes/${routePath}.ts`);
1639
+ console.log(` \u2713 Created route: api/${routePath}.ts`);
1016
1640
  console.log(` ${upperMethod} ${urlPath}`);
1017
1641
  }
1018
1642
  function generateAgent(cwd, name) {
1019
- const filePath = (0, import_node_path4.join)(cwd, "src", "agents", `${name}.ts`);
1643
+ const filePath = (0, import_node_path4.join)(cwd, "agents", `${name}.ts`);
1020
1644
  if ((0, import_node_fs4.existsSync)(filePath)) {
1021
- console.error(`[voltx] Agent already exists: src/agents/${name}.ts`);
1645
+ console.error(`[voltx] Agent already exists: agents/${name}.ts`);
1022
1646
  process.exit(1);
1023
1647
  }
1024
1648
  const content = `// Agent: ${name}
@@ -1040,12 +1664,12 @@ export const ${toCamelCase(name)} = createAgent({
1040
1664
  });
1041
1665
  `;
1042
1666
  writeFileSafe(filePath, content);
1043
- console.log(` \u2713 Created agent: src/agents/${name}.ts`);
1667
+ console.log(` \u2713 Created agent: agents/${name}.ts`);
1044
1668
  }
1045
1669
  function generateTool(cwd, name) {
1046
- const filePath = (0, import_node_path4.join)(cwd, "src", "tools", `${name}.ts`);
1670
+ const filePath = (0, import_node_path4.join)(cwd, "tools", `${name}.ts`);
1047
1671
  if ((0, import_node_fs4.existsSync)(filePath)) {
1048
- console.error(`[voltx] Tool already exists: src/tools/${name}.ts`);
1672
+ console.error(`[voltx] Tool already exists: tools/${name}.ts`);
1049
1673
  process.exit(1);
1050
1674
  }
1051
1675
  const content = `// Tool: ${name}
@@ -1067,12 +1691,12 @@ export const ${toCamelCase(name)}Tool = {
1067
1691
  };
1068
1692
  `;
1069
1693
  writeFileSafe(filePath, content);
1070
- console.log(` \u2713 Created tool: src/tools/${name}.ts`);
1694
+ console.log(` \u2713 Created tool: tools/${name}.ts`);
1071
1695
  }
1072
1696
  function generateJob(cwd, name) {
1073
- const filePath = (0, import_node_path4.join)(cwd, "src", "jobs", `${name}.ts`);
1697
+ const filePath = (0, import_node_path4.join)(cwd, "jobs", `${name}.ts`);
1074
1698
  if ((0, import_node_fs4.existsSync)(filePath)) {
1075
- console.error(`[voltx] Job already exists: src/jobs/${name}.ts`);
1699
+ console.error(`[voltx] Job already exists: jobs/${name}.ts`);
1076
1700
  process.exit(1);
1077
1701
  }
1078
1702
  const content = `// Job: ${name}
@@ -1097,7 +1721,7 @@ export async function run(ctx: any, data?: Record<string, unknown>) {
1097
1721
  }
1098
1722
  `;
1099
1723
  writeFileSafe(filePath, content);
1100
- console.log(` \u2713 Created job: src/jobs/${name}.ts`);
1724
+ console.log(` \u2713 Created job: jobs/${name}.ts`);
1101
1725
  }
1102
1726
  function writeFileSafe(filePath, content) {
1103
1727
  const dir = (0, import_node_path4.dirname)(filePath);