@voltx/cli 0.3.3 → 0.3.5

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