@voltx/cli 0.3.4 → 0.3.6

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