@voltx/cli 0.3.5 → 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.
@@ -0,0 +1,1028 @@
1
+ import {
2
+ printWelcomeBanner
3
+ } from "./chunk-IV352HZA.mjs";
4
+
5
+ // src/create.ts
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ var VV = {
9
+ "@voltx/core": "^0.3.1",
10
+ "@voltx/server": "^0.3.1",
11
+ "@voltx/cli": "^0.3.5",
12
+ "@voltx/ai": "^0.3.0",
13
+ "@voltx/agents": "^0.3.1",
14
+ "@voltx/memory": "^0.3.0",
15
+ "@voltx/db": "^0.3.0",
16
+ "@voltx/rag": "^0.3.1",
17
+ "@voltx/auth": "^0.3.0"
18
+ };
19
+ function v(pkg) {
20
+ return VV[pkg] ?? "^0.3.0";
21
+ }
22
+ var TEMPLATE_DEPS = {
23
+ blank: { "@voltx/core": v("@voltx/core"), "@voltx/server": v("@voltx/server") },
24
+ chatbot: { "@voltx/core": v("@voltx/core"), "@voltx/ai": v("@voltx/ai"), "@voltx/server": v("@voltx/server"), "@voltx/memory": v("@voltx/memory") },
25
+ "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") },
26
+ "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") }
27
+ };
28
+ async function createProject(options) {
29
+ const { name, template = "blank", auth = "none", shadcn = false } = options;
30
+ const targetDir = path.resolve(process.cwd(), name);
31
+ if (fs.existsSync(targetDir)) {
32
+ console.error(`[voltx] Directory "${name}" already exists.`);
33
+ process.exit(1);
34
+ }
35
+ fs.mkdirSync(targetDir, { recursive: true });
36
+ const provider = template === "rag-app" ? "openai" : "cerebras";
37
+ const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
38
+ const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
39
+ const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": v("@voltx/cli") };
40
+ if (auth === "better-auth") {
41
+ deps["@voltx/auth"] = v("@voltx/auth");
42
+ deps["better-auth"] = "^1.5.0";
43
+ } else if (auth === "jwt") {
44
+ deps["@voltx/auth"] = v("@voltx/auth");
45
+ deps["jose"] = "^6.0.0";
46
+ }
47
+ const devDeps = { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" };
48
+ deps["hono"] = "^4.7.0";
49
+ deps["@hono/node-server"] = "^1.14.0";
50
+ deps["react"] = "^19.0.0";
51
+ deps["react-dom"] = "^19.0.0";
52
+ deps["tailwindcss"] = "^4.0.0";
53
+ devDeps["vite"] = "^6.0.0";
54
+ devDeps["@hono/vite-dev-server"] = "^0.7.0";
55
+ devDeps["@vitejs/plugin-react"] = "^4.3.0";
56
+ devDeps["@tailwindcss/vite"] = "^4.0.0";
57
+ devDeps["@types/react"] = "^19.0.0";
58
+ devDeps["@types/react-dom"] = "^19.0.0";
59
+ if (shadcn) {
60
+ deps["class-variance-authority"] = "^0.7.0";
61
+ deps["clsx"] = "^2.1.0";
62
+ deps["tailwind-merge"] = "^3.0.0";
63
+ deps["lucide-react"] = "^0.468.0";
64
+ }
65
+ devDeps["@voltx/cli"] = deps["@voltx/cli"] ?? v("@voltx/cli");
66
+ delete deps["@voltx/cli"];
67
+ fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
68
+ name,
69
+ version: "0.1.0",
70
+ private: true,
71
+ scripts: { dev: "npx voltx dev", build: "npx voltx build", start: "npx voltx start" },
72
+ dependencies: deps,
73
+ devDependencies: devDeps
74
+ }, null, 2));
75
+ let config = `import { defineConfig } from "@voltx/core";
76
+
77
+ export default defineConfig({
78
+ name: "${name}",
79
+ port: 3000,
80
+ ai: {
81
+ provider: "${provider}",
82
+ model: "${model}",
83
+ },`;
84
+ if (hasDb) config += `
85
+ db: {
86
+ url: process.env.DATABASE_URL,
87
+ },`;
88
+ if (auth !== "none") config += `
89
+ auth: {
90
+ provider: "${auth}",
91
+ },`;
92
+ config += `
93
+ server: {
94
+ routesDir: "api",
95
+ staticDir: "public",
96
+ cors: true,
97
+ },
98
+ });
99
+ `;
100
+ fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
101
+ fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
102
+ fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
103
+ fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
104
+ fs.mkdirSync(path.join(targetDir, "src", "components"), { recursive: true });
105
+ fs.mkdirSync(path.join(targetDir, "src", "hooks"), { recursive: true });
106
+ fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
107
+ fs.writeFileSync(path.join(targetDir, "public", "favicon.svg"), generateFaviconSVG());
108
+ fs.writeFileSync(path.join(targetDir, "public", "robots.txt"), generateRobotsTxt());
109
+ fs.writeFileSync(path.join(targetDir, "public", "site.webmanifest"), generateWebManifest(name));
110
+ const tsconfig = {
111
+ compilerOptions: {
112
+ target: "ES2022",
113
+ module: "ESNext",
114
+ moduleResolution: "bundler",
115
+ strict: true,
116
+ esModuleInterop: true,
117
+ skipLibCheck: true,
118
+ outDir: "dist",
119
+ baseUrl: ".",
120
+ paths: { "@/*": ["./src/*"] },
121
+ jsx: "react-jsx"
122
+ },
123
+ include: ["src", "api", "server.ts", "voltx.config.ts"]
124
+ };
125
+ if (template === "agent-app") tsconfig.include.push("agents", "tools");
126
+ fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
127
+ fs.writeFileSync(path.join(targetDir, "server.ts"), generateServerEntry(name, template));
128
+ fs.writeFileSync(path.join(targetDir, "vite.config.ts"), generateViteConfigFile("server.ts"));
129
+ fs.writeFileSync(path.join(targetDir, "src", "entry-client.tsx"), generateEntryClient());
130
+ fs.writeFileSync(path.join(targetDir, "src", "entry-server.tsx"), generateEntryServer());
131
+ fs.writeFileSync(path.join(targetDir, "src", "layout.tsx"), generateLayoutComponent(name));
132
+ fs.writeFileSync(path.join(targetDir, "src", "globals.css"), generateGlobalCSS(shadcn));
133
+ if (shadcn) {
134
+ fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.ts"), generateCnUtil());
135
+ fs.writeFileSync(path.join(targetDir, "components.json"), generateComponentsJson());
136
+ }
137
+ fs.writeFileSync(path.join(targetDir, "src", "app.tsx"), generateAppComponent(name, template));
138
+ fs.writeFileSync(
139
+ path.join(targetDir, "api", "index.ts"),
140
+ `// GET /api \u2014 Health check
141
+ import type { Context } from "@voltx/server";
142
+
143
+ export function GET(c: Context) {
144
+ return c.json({ name: "${name}", status: "ok" });
145
+ }
146
+ `
147
+ );
148
+ if (template === "chatbot" || template === "agent-app") {
149
+ fs.writeFileSync(
150
+ path.join(targetDir, "api", "chat.ts"),
151
+ `// POST /api/chat \u2014 Streaming chat with conversation memory
152
+ import type { Context } from "@voltx/server";
153
+ import { streamText } from "@voltx/ai";
154
+ import { createMemory } from "@voltx/memory";
155
+
156
+ const memory = createMemory({ maxMessages: 50 });
157
+
158
+ export async function POST(c: Context) {
159
+ const { messages, conversationId = "default" } = await c.req.json();
160
+
161
+ const lastMessage = messages[messages.length - 1];
162
+ if (lastMessage?.role === "user") {
163
+ await memory.add(conversationId, { role: "user", content: lastMessage.content });
164
+ }
165
+
166
+ const history = await memory.get(conversationId);
167
+
168
+ const result = await streamText({
169
+ model: "${provider}:${model}",
170
+ system: "You are a helpful AI assistant.",
171
+ messages: history.map((m) => ({ role: m.role, content: m.content })),
172
+ });
173
+
174
+ result.text.then(async (text) => {
175
+ await memory.add(conversationId, { role: "assistant", content: text });
176
+ });
177
+
178
+ return result.toSSEResponse();
179
+ }
180
+ `
181
+ );
182
+ }
183
+ if (template === "agent-app") {
184
+ fs.mkdirSync(path.join(targetDir, "agents"), { recursive: true });
185
+ fs.mkdirSync(path.join(targetDir, "tools"), { recursive: true });
186
+ fs.writeFileSync(path.join(targetDir, "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
187
+ import type { Tool } from "@voltx/agents";
188
+
189
+ export const calculatorTool: Tool = {
190
+ name: "calculator",
191
+ description: "Evaluate a math expression. Supports +, -, *, /, %, parentheses, and Math functions.",
192
+ parameters: {
193
+ type: "object",
194
+ properties: { expression: { type: "string", description: "The math expression to evaluate" } },
195
+ required: ["expression"],
196
+ },
197
+ async execute(args: { expression: string }) {
198
+ try {
199
+ const safe = args.expression.replace(/[^0-9+\\-*/.()%\\s,]|(?<!Math)\\.[a-z]/gi, (match) => {
200
+ if (args.expression.includes("Math.")) return match;
201
+ throw new Error("Invalid character: " + match);
202
+ });
203
+ const result = new Function("return " + safe)();
204
+ return \`\${args.expression} = \${result}\`;
205
+ } catch (err) {
206
+ return \`Error: \${err instanceof Error ? err.message : String(err)}\`;
207
+ }
208
+ },
209
+ };
210
+ `);
211
+ fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
212
+ import type { Tool } from "@voltx/agents";
213
+
214
+ export const datetimeTool: Tool = {
215
+ name: "datetime",
216
+ description: "Get the current date, time, day of week, and timezone.",
217
+ parameters: {
218
+ type: "object",
219
+ properties: { timezone: { type: "string", description: "Optional IANA timezone. Defaults to server timezone." } },
220
+ },
221
+ async execute(args: { timezone?: string }) {
222
+ const now = new Date();
223
+ const tz = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
224
+ const formatted = now.toLocaleString("en-US", {
225
+ timeZone: tz, weekday: "long", year: "numeric", month: "long", day: "numeric",
226
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true,
227
+ });
228
+ return \`Current date/time (\${tz}): \${formatted}\`;
229
+ },
230
+ };
231
+ `);
232
+ fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
233
+ import { createAgent } from "@voltx/agents";
234
+ import { calculatorTool } from "../tools/calculator";
235
+ import { datetimeTool } from "../tools/datetime";
236
+
237
+ export const assistant = createAgent({
238
+ name: "assistant",
239
+ model: "${provider}:${model}",
240
+ instructions: "You are a helpful AI assistant with access to tools: Calculator, Date & Time. Use them when needed to answer questions accurately.",
241
+ tools: [calculatorTool, datetimeTool],
242
+ maxIterations: 5,
243
+ });
244
+ `);
245
+ fs.writeFileSync(
246
+ path.join(targetDir, "api", "agent.ts"),
247
+ `// POST /api/agent \u2014 Run the AI agent
248
+ import type { Context } from "@voltx/server";
249
+ import { assistant } from "../agents/assistant";
250
+
251
+ export async function POST(c: Context) {
252
+ const { input } = await c.req.json();
253
+ if (!input) return c.json({ error: "Missing 'input' field" }, 400);
254
+ const result = await assistant.run(input);
255
+ return c.json({ content: result.content, steps: result.steps });
256
+ }
257
+ `
258
+ );
259
+ }
260
+ if (template === "rag-app") {
261
+ const embedModel = "openai:text-embedding-3-small";
262
+ fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
263
+ fs.writeFileSync(
264
+ path.join(targetDir, "api", "rag", "query.ts"),
265
+ `// POST /api/rag/query \u2014 Query documents with RAG
266
+ import type { Context } from "@voltx/server";
267
+ import { streamText } from "@voltx/ai";
268
+ import { createRAGPipeline, createEmbedder } from "@voltx/rag";
269
+ import { createVectorStore } from "@voltx/db";
270
+
271
+ const vectorStore = createVectorStore();
272
+ const embedder = createEmbedder({ model: "${embedModel}" });
273
+ const rag = createRAGPipeline({ embedder, vectorStore });
274
+
275
+ export async function POST(c: Context) {
276
+ const { question } = await c.req.json();
277
+ const context = await rag.getContext(question, { topK: 5 });
278
+ const result = await streamText({
279
+ model: "${provider}:${model}",
280
+ system: \`Answer based on context. If not relevant, say so.\\n\\nContext:\\n\${context}\`,
281
+ messages: [{ role: "user", content: question }],
282
+ });
283
+ return result.toSSEResponse();
284
+ }
285
+ `
286
+ );
287
+ fs.writeFileSync(
288
+ path.join(targetDir, "api", "rag", "ingest.ts"),
289
+ `// POST /api/rag/ingest \u2014 Ingest documents into the vector store
290
+ import type { Context } from "@voltx/server";
291
+ import { createRAGPipeline, createEmbedder } from "@voltx/rag";
292
+ import { createVectorStore } from "@voltx/db";
293
+
294
+ const vectorStore = createVectorStore();
295
+ const embedder = createEmbedder({ model: "${embedModel}" });
296
+ const rag = createRAGPipeline({ embedder, vectorStore });
297
+
298
+ export async function POST(c: Context) {
299
+ const { text, idPrefix } = await c.req.json();
300
+ if (!text || typeof text !== "string") return c.json({ error: "Missing 'text' field" }, 400);
301
+ const result = await rag.ingest(text, idPrefix ?? "doc");
302
+ return c.json({ status: "ok", chunks: result.chunks, ids: result.ids });
303
+ }
304
+ `
305
+ );
306
+ }
307
+ if (auth === "better-auth") {
308
+ fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
309
+ fs.writeFileSync(
310
+ path.join(targetDir, "api", "auth", "[...path].ts"),
311
+ `// ALL /api/auth/* \u2014 Better Auth handler
312
+ import type { Context } from "@voltx/server";
313
+ import { auth } from "../../src/lib/auth";
314
+ import { createAuthHandler } from "@voltx/auth";
315
+
316
+ const handler = createAuthHandler(auth);
317
+
318
+ export const GET = (c: Context) => handler(c);
319
+ export const POST = (c: Context) => handler(c);
320
+ `
321
+ );
322
+ fs.writeFileSync(
323
+ path.join(targetDir, "src", "lib", "auth.ts"),
324
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
325
+
326
+ export const auth = createAuth("better-auth", {
327
+ database: process.env.DATABASE_URL!,
328
+ emailAndPassword: true,
329
+ });
330
+
331
+ export const authMiddleware = createAuthMiddleware({
332
+ provider: auth,
333
+ publicPaths: ["/api/auth", "/api/health", "/"],
334
+ });
335
+ `
336
+ );
337
+ } else if (auth === "jwt") {
338
+ fs.writeFileSync(
339
+ path.join(targetDir, "src", "lib", "auth.ts"),
340
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
341
+
342
+ export const jwt = createAuth("jwt", {
343
+ secret: process.env.JWT_SECRET!,
344
+ expiresIn: "7d",
345
+ });
346
+
347
+ export const authMiddleware = createAuthMiddleware({
348
+ provider: jwt,
349
+ publicPaths: ["/api/auth", "/api/health", "/"],
350
+ });
351
+ `
352
+ );
353
+ fs.writeFileSync(
354
+ path.join(targetDir, "api", "auth.ts"),
355
+ `import type { Context } from "@voltx/server";
356
+ import { jwt } from "../src/lib/auth";
357
+
358
+ export async function POST(c: Context) {
359
+ const { email, password } = await c.req.json();
360
+ if (!email || !password) return c.json({ error: "Email and password are required" }, 400);
361
+ const token = await jwt.sign({ sub: email, email });
362
+ return c.json({ token });
363
+ }
364
+ `
365
+ );
366
+ }
367
+ let envContent = "";
368
+ if (template === "rag-app") {
369
+ 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";
370
+ 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";
371
+ } else if (template === "chatbot" || template === "agent-app") {
372
+ 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";
373
+ } else {
374
+ 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";
375
+ }
376
+ if (auth === "better-auth") {
377
+ 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";
378
+ } else if (auth === "jwt") {
379
+ 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";
380
+ }
381
+ 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";
382
+ fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
383
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
384
+ printWelcomeBanner(name);
385
+ }
386
+ function generateServerEntry(projectName, template) {
387
+ const imports = [];
388
+ const mounts = [];
389
+ imports.push('import { GET as healthGET } from "./api/index";');
390
+ mounts.push('app.get("/api", healthGET);');
391
+ if (template === "chatbot" || template === "agent-app") {
392
+ imports.push('import { POST as chatPOST } from "./api/chat";');
393
+ mounts.push('app.post("/api/chat", chatPOST);');
394
+ }
395
+ if (template === "agent-app") {
396
+ imports.push('import { POST as agentPOST } from "./api/agent";');
397
+ mounts.push('app.post("/api/agent", agentPOST);');
398
+ }
399
+ if (template === "rag-app") {
400
+ imports.push('import { POST as ragQueryPOST } from "./api/rag/query";');
401
+ imports.push('import { POST as ragIngestPOST } from "./api/rag/ingest";');
402
+ mounts.push('app.post("/api/rag/query", ragQueryPOST);');
403
+ mounts.push('app.post("/api/rag/ingest", ragIngestPOST);');
404
+ }
405
+ return `import { Hono } from "hono";
406
+ import { serve } from "@hono/node-server";
407
+ import { serveStatic } from "@hono/node-server/serve-static";
408
+ import { registerSSR } from "@voltx/server";
409
+ import { loadEnv } from "@voltx/core";
410
+ ${imports.join("\n")}
411
+
412
+ loadEnv(process.env.NODE_ENV ?? "development");
413
+
414
+ const isProd = process.env.NODE_ENV === "production";
415
+ const app = new Hono();
416
+
417
+ // \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
418
+ ${mounts.join("\n")}
419
+
420
+ // \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
421
+ if (isProd) {
422
+ app.use("/assets/*", serveStatic({ root: "./dist/client/" }));
423
+ app.use("/favicon.svg", serveStatic({ root: "./public/" }));
424
+ app.use("/robots.txt", serveStatic({ root: "./public/" }));
425
+ app.use("/site.webmanifest", serveStatic({ root: "./public/" }));
426
+ }
427
+
428
+ // \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
429
+ registerSSR(app, null, {
430
+ title: "${projectName}",
431
+ entryServer: "src/entry-server.tsx",
432
+ entryClient: "src/entry-client.tsx",
433
+ });
434
+
435
+ export default app;
436
+
437
+ if (isProd) {
438
+ const port = Number(process.env.PORT) || 3000;
439
+ serve({ fetch: app.fetch, port }, (info) => {
440
+ console.log(\`\\n \u26A1 ${projectName} running at http://localhost:\${info.port}\\n\`);
441
+ });
442
+ }
443
+ `;
444
+ }
445
+ function generateViteConfigFile(entry) {
446
+ return `import { defineConfig } from "vite";
447
+ import devServer from "@hono/vite-dev-server";
448
+ import react from "@vitejs/plugin-react";
449
+ import tailwindcss from "@tailwindcss/vite";
450
+
451
+ export default defineConfig({
452
+ resolve: {
453
+ alias: {
454
+ "@": "/src",
455
+ },
456
+ },
457
+ plugins: [
458
+ react(),
459
+ tailwindcss(),
460
+ devServer({
461
+ entry: "${entry}",
462
+ exclude: [
463
+ /.*\\.tsx?($|\\?)/,
464
+ /.*\\.(s?css|less)($|\\?)/,
465
+ /.*\\.(svg|png|jpg|jpeg|gif|webp|ico)($|\\?)/,
466
+ /^\\/@.+$/,
467
+ /^\\/favicon\\.svg$/,
468
+ /^\\/node_modules\\/.*/,
469
+ /^\\/src\\/.*/,
470
+ ],
471
+ injectClientScript: false,
472
+ }),
473
+ ],
474
+ });
475
+ `;
476
+ }
477
+ function generateEntryClient() {
478
+ return `import React from "react";
479
+ import { hydrateRoot } from "react-dom/client";
480
+ import Layout from "./layout";
481
+ import App from "./app";
482
+ import "./globals.css";
483
+
484
+ hydrateRoot(
485
+ document.getElementById("root")!,
486
+ <React.StrictMode>
487
+ <Layout>
488
+ <App />
489
+ </Layout>
490
+ </React.StrictMode>
491
+ );
492
+ `;
493
+ }
494
+ function generateEntryServer() {
495
+ return `import React from "react";
496
+ import { renderToReadableStream } from "react-dom/server";
497
+ import Layout from "./layout";
498
+ import App from "./app";
499
+
500
+ export async function render(_url: string): Promise<ReadableStream> {
501
+ const stream = await renderToReadableStream(
502
+ <React.StrictMode>
503
+ <Layout>
504
+ <App />
505
+ </Layout>
506
+ </React.StrictMode>,
507
+ {
508
+ onError(error: unknown) {
509
+ console.error("[voltx] SSR render error:", error);
510
+ },
511
+ }
512
+ );
513
+ return stream;
514
+ }
515
+ `;
516
+ }
517
+ function generateLayoutComponent(projectName) {
518
+ return `import React from "react";
519
+
520
+ export default function Layout({ children }: { children: React.ReactNode }) {
521
+ return (
522
+ <div className="min-h-screen bg-background text-foreground font-sans">
523
+ <header className="border-b border-border px-6 py-3 flex items-center justify-between">
524
+ <div className="flex items-center gap-2">
525
+ <span className="text-xl">\u26A1</span>
526
+ <span className="font-semibold">${projectName}</span>
527
+ </div>
528
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-muted text-sm hover:text-foreground transition-colors">
529
+ Built with VoltX
530
+ </a>
531
+ </header>
532
+ <main>{children}</main>
533
+ </div>
534
+ );
535
+ }
536
+ `;
537
+ }
538
+ function generateGlobalCSS(useShadcn = false) {
539
+ if (useShadcn) {
540
+ return `@import "tailwindcss";
541
+
542
+ @theme inline {
543
+ --radius-sm: 0.25rem;
544
+ --radius-md: 0.375rem;
545
+ --radius-lg: 0.5rem;
546
+ --radius-xl: 0.75rem;
547
+ --color-background: hsl(var(--background));
548
+ --color-foreground: hsl(var(--foreground));
549
+ --color-card: hsl(var(--card));
550
+ --color-card-foreground: hsl(var(--card-foreground));
551
+ --color-popover: hsl(var(--popover));
552
+ --color-popover-foreground: hsl(var(--popover-foreground));
553
+ --color-primary: hsl(var(--primary));
554
+ --color-primary-foreground: hsl(var(--primary-foreground));
555
+ --color-secondary: hsl(var(--secondary));
556
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
557
+ --color-muted: hsl(var(--muted));
558
+ --color-muted-foreground: hsl(var(--muted-foreground));
559
+ --color-accent: hsl(var(--accent));
560
+ --color-accent-foreground: hsl(var(--accent-foreground));
561
+ --color-destructive: hsl(var(--destructive));
562
+ --color-border: hsl(var(--border));
563
+ --color-input: hsl(var(--input));
564
+ --color-ring: hsl(var(--ring));
565
+ --color-chart-1: hsl(var(--chart-1));
566
+ --color-chart-2: hsl(var(--chart-2));
567
+ --color-chart-3: hsl(var(--chart-3));
568
+ --color-chart-4: hsl(var(--chart-4));
569
+ --color-chart-5: hsl(var(--chart-5));
570
+ --color-sidebar: hsl(var(--sidebar));
571
+ --color-sidebar-foreground: hsl(var(--sidebar-foreground));
572
+ --color-sidebar-primary: hsl(var(--sidebar-primary));
573
+ --color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
574
+ --color-sidebar-accent: hsl(var(--sidebar-accent));
575
+ --color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
576
+ --color-sidebar-border: hsl(var(--sidebar-border));
577
+ --color-sidebar-ring: hsl(var(--sidebar-ring));
578
+ }
579
+
580
+ :root {
581
+ --background: 0 0% 4%;
582
+ --foreground: 0 0% 93%;
583
+ --card: 0 0% 6%;
584
+ --card-foreground: 0 0% 93%;
585
+ --popover: 0 0% 6%;
586
+ --popover-foreground: 0 0% 93%;
587
+ --primary: 0 0% 93%;
588
+ --primary-foreground: 0 0% 6%;
589
+ --secondary: 0 0% 12%;
590
+ --secondary-foreground: 0 0% 93%;
591
+ --muted: 0 0% 12%;
592
+ --muted-foreground: 0 0% 55%;
593
+ --accent: 0 0% 12%;
594
+ --accent-foreground: 0 0% 93%;
595
+ --destructive: 0 62% 50%;
596
+ --border: 0 0% 14%;
597
+ --input: 0 0% 14%;
598
+ --ring: 0 0% 83%;
599
+ --chart-1: 220 70% 50%;
600
+ --chart-2: 160 60% 45%;
601
+ --chart-3: 30 80% 55%;
602
+ --chart-4: 280 65% 60%;
603
+ --chart-5: 340 75% 55%;
604
+ --sidebar: 0 0% 5%;
605
+ --sidebar-foreground: 0 0% 93%;
606
+ --sidebar-primary: 0 0% 93%;
607
+ --sidebar-primary-foreground: 0 0% 6%;
608
+ --sidebar-accent: 0 0% 12%;
609
+ --sidebar-accent-foreground: 0 0% 93%;
610
+ --sidebar-border: 0 0% 14%;
611
+ --sidebar-ring: 0 0% 83%;
612
+ }
613
+
614
+ *,
615
+ *::before,
616
+ *::after {
617
+ box-sizing: border-box;
618
+ margin: 0;
619
+ padding: 0;
620
+ border-color: var(--color-border);
621
+ }
622
+
623
+ html,
624
+ body {
625
+ height: 100%;
626
+ background: var(--color-background);
627
+ color: var(--color-foreground);
628
+ font-family: system-ui, -apple-system, sans-serif;
629
+ -webkit-font-smoothing: antialiased;
630
+ }
631
+
632
+ #root {
633
+ height: 100%;
634
+ }
635
+ `;
636
+ }
637
+ return `@import "tailwindcss";
638
+
639
+ @theme {
640
+ --color-background: #0a0a0a;
641
+ --color-foreground: #ededed;
642
+ --color-muted: #888888;
643
+ --color-border: #222222;
644
+ --color-primary: #2563eb;
645
+ --color-accent: #a78bfa;
646
+ --font-sans: system-ui, -apple-system, sans-serif;
647
+ }
648
+
649
+ *,
650
+ *::before,
651
+ *::after {
652
+ box-sizing: border-box;
653
+ margin: 0;
654
+ padding: 0;
655
+ }
656
+
657
+ html,
658
+ body {
659
+ height: 100%;
660
+ background: var(--color-background);
661
+ color: var(--color-foreground);
662
+ font-family: var(--font-sans);
663
+ -webkit-font-smoothing: antialiased;
664
+ }
665
+
666
+ #root {
667
+ height: 100%;
668
+ }
669
+
670
+ a {
671
+ color: inherit;
672
+ text-decoration: none;
673
+ }
674
+
675
+ a:hover {
676
+ text-decoration: underline;
677
+ }
678
+ `;
679
+ }
680
+ function generateCnUtil() {
681
+ return `import { type ClassValue, clsx } from "clsx";
682
+ import { twMerge } from "tailwind-merge";
683
+
684
+ export function cn(...inputs: ClassValue[]) {
685
+ return twMerge(clsx(inputs));
686
+ }
687
+ `;
688
+ }
689
+ function generateComponentsJson() {
690
+ return JSON.stringify({
691
+ "$schema": "https://ui.shadcn.com/schema.json",
692
+ style: "new-york",
693
+ rsc: false,
694
+ tsx: true,
695
+ tailwind: {
696
+ config: "",
697
+ css: "src/globals.css",
698
+ baseColor: "neutral",
699
+ cssVariables: true
700
+ },
701
+ aliases: {
702
+ components: "@/components",
703
+ utils: "@/lib/utils",
704
+ ui: "@/components/ui",
705
+ lib: "@/lib",
706
+ hooks: "@/hooks"
707
+ }
708
+ }, null, 2) + "\n";
709
+ }
710
+ function generateAppComponent(projectName, template) {
711
+ if (template === "blank") {
712
+ return `import React, { useState, useEffect } from "react";
713
+
714
+ export default function App() {
715
+ const [status, setStatus] = useState<string>("checking...");
716
+
717
+ useEffect(() => {
718
+ fetch("/api")
719
+ .then((res) => res.json())
720
+ .then((data) => setStatus(data.status || "ok"))
721
+ .catch(() => setStatus("error"));
722
+ }, []);
723
+
724
+ return (
725
+ <div className="flex flex-col items-center justify-center min-h-[calc(100vh-60px)] px-6 py-12">
726
+ <div className="text-center max-w-2xl w-full">
727
+ {/* Hero */}
728
+ <div className="relative mb-8">
729
+ <div className="absolute inset-0 blur-3xl opacity-20 bg-gradient-to-r from-blue-500 via-purple-500 to-blue-500 rounded-full" />
730
+ <div className="relative text-7xl mb-4">\u26A1</div>
731
+ </div>
732
+ <h1 className="text-5xl font-bold tracking-tight mb-3 bg-gradient-to-r from-white to-white/60 bg-clip-text text-transparent">
733
+ \${"\${projectName}"}
734
+ </h1>
735
+ <p className="text-muted text-lg mb-10">The AI-first full-stack framework</p>
736
+
737
+ {/* Status cards */}
738
+ <div className="flex gap-4 justify-center mb-10">
739
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
740
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Server</div>
741
+ <div className={\`text-sm font-medium \${status === "ok" ? "text-emerald-400" : "text-red-400"}\`}>
742
+ <span className={\`inline-block w-2 h-2 rounded-full mr-2 \${status === "ok" ? "bg-emerald-400 animate-pulse" : "bg-red-400"}\`} />
743
+ {status}
744
+ </div>
745
+ </div>
746
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
747
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">Frontend</div>
748
+ <div className="text-sm font-medium text-emerald-400">
749
+ <span className="inline-block w-2 h-2 rounded-full mr-2 bg-emerald-400 animate-pulse" />
750
+ React + Vite
751
+ </div>
752
+ </div>
753
+ <div className="px-6 py-4 rounded-xl bg-white/5 border border-border backdrop-blur-sm">
754
+ <div className="text-xs text-muted mb-1 uppercase tracking-wider">CSS</div>
755
+ <div className="text-sm font-medium text-sky-400">Tailwind v4</div>
756
+ </div>
757
+ </div>
758
+
759
+ {/* Get started */}
760
+ <div className="bg-white/[0.03] border border-border rounded-2xl p-8 text-left backdrop-blur-sm">
761
+ <h2 className="text-sm font-medium text-muted mb-6 uppercase tracking-wider">Get started</h2>
762
+ <div className="space-y-4">
763
+ <div className="flex items-start gap-4">
764
+ <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>
765
+ <div>
766
+ <code className="text-purple-400 text-sm">src/app.tsx</code>
767
+ <p className="text-muted text-sm mt-1">Edit this file to build your UI</p>
768
+ </div>
769
+ </div>
770
+ <div className="flex items-start gap-4">
771
+ <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>
772
+ <div>
773
+ <code className="text-blue-400 text-sm">api/</code>
774
+ <p className="text-muted text-sm mt-1">Add API routes here (file-based routing)</p>
775
+ </div>
776
+ </div>
777
+ <div className="flex items-start gap-4">
778
+ <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>
779
+ <div>
780
+ <code className="text-emerald-400 text-sm">src/components/</code>
781
+ <p className="text-muted text-sm mt-1">Create React components with Tailwind CSS</p>
782
+ </div>
783
+ </div>
784
+ </div>
785
+ </div>
786
+
787
+ {/* Links */}
788
+ <div className="flex gap-6 justify-center mt-8">
789
+ <a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
790
+ GitHub \u2192
791
+ </a>
792
+ <a href="https://voltx.co.in" target="_blank" rel="noopener noreferrer" className="text-sm text-muted hover:text-foreground transition-colors">
793
+ Docs \u2192
794
+ </a>
795
+ </div>
796
+ </div>
797
+ </div>
798
+ );
799
+ }
800
+ `;
801
+ }
802
+ const apiEndpoint = template === "agent-app" ? "/api/agent" : template === "rag-app" ? "/api/rag/query" : "/api/chat";
803
+ const isAgent = template === "agent-app";
804
+ const isRag = template === "rag-app";
805
+ let emptyStateTitle = "Start a conversation";
806
+ let emptyStateHint = "Type a message below to chat with AI";
807
+ let accentColor = "blue";
808
+ let inputPlaceholder = "Type a message...";
809
+ if (isAgent) {
810
+ emptyStateTitle = "Talk to your AI agent";
811
+ emptyStateHint = "The agent can use tools like Calculator and Date/Time";
812
+ accentColor = "purple";
813
+ inputPlaceholder = "Ask the agent anything...";
814
+ } else if (isRag) {
815
+ emptyStateTitle = "Ask your documents";
816
+ emptyStateHint = "Query your knowledge base \u2014 ingest docs via POST /api/rag/ingest";
817
+ accentColor = "emerald";
818
+ inputPlaceholder = "Ask a question about your documents...";
819
+ }
820
+ return `import React, { useState, useRef, useEffect, useCallback } from "react";
821
+
822
+ interface Message {
823
+ id: string;
824
+ role: "user" | "assistant";
825
+ content: string;
826
+ }
827
+
828
+ export default function App() {
829
+ const [messages, setMessages] = useState<Message[]>([]);
830
+ const [input, setInput] = useState("");
831
+ const [isLoading, setIsLoading] = useState(false);
832
+ const messagesEndRef = useRef<HTMLDivElement>(null);
833
+ const inputRef = useRef<HTMLInputElement>(null);
834
+
835
+ useEffect(() => {
836
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
837
+ }, [messages]);
838
+
839
+ useEffect(() => {
840
+ inputRef.current?.focus();
841
+ }, []);
842
+
843
+ const sendMessage = useCallback(async () => {
844
+ const text = input.trim();
845
+ if (!text || isLoading) return;
846
+
847
+ const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: text };
848
+ setMessages((prev) => [...prev, userMsg]);
849
+ setInput("");
850
+ setIsLoading(true);
851
+
852
+ const assistantMsg: Message = { id: crypto.randomUUID(), role: "assistant", content: "" };
853
+ setMessages((prev) => [...prev, assistantMsg]);
854
+
855
+ try {${isAgent ? `
856
+ const res = await fetch("${apiEndpoint}", {
857
+ method: "POST",
858
+ headers: { "Content-Type": "application/json" },
859
+ body: JSON.stringify({ input: text }),
860
+ });
861
+ const data = await res.json();
862
+ setMessages((prev) =>
863
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: data.content || "No response" } : m)
864
+ );` : `
865
+ const res = await fetch("${apiEndpoint}", {
866
+ method: "POST",
867
+ headers: { "Content-Type": "application/json" },
868
+ body: JSON.stringify({${isRag ? ` question: text ` : ` messages: [...messages, { role: "user", content: text }] `}}),
869
+ });
870
+
871
+ if (!res.body) throw new Error("No response body");
872
+
873
+ const reader = res.body.getReader();
874
+ const decoder = new TextDecoder();
875
+ let buffer = "";
876
+ let fullContent = "";
877
+
878
+ while (true) {
879
+ const { done, value } = await reader.read();
880
+ if (done) break;
881
+ buffer += decoder.decode(value, { stream: true });
882
+ const lines = buffer.split("\\n");
883
+ buffer = lines.pop() ?? "";
884
+
885
+ for (const line of lines) {
886
+ if (!line.startsWith("data: ")) continue;
887
+ const data = line.slice(6);
888
+ if (data === "[DONE]") break;
889
+ try {
890
+ const parsed = JSON.parse(data);
891
+ const chunk = parsed.textDelta ?? parsed.content ?? parsed.choices?.[0]?.delta?.content ?? "";
892
+ if (chunk) {
893
+ fullContent += chunk;
894
+ setMessages((prev) =>
895
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: fullContent } : m)
896
+ );
897
+ }
898
+ } catch {}
899
+ }
900
+ }`}
901
+ } catch (err) {
902
+ setMessages((prev) =>
903
+ prev.map((m) => m.id === assistantMsg.id ? { ...m, content: "Error: " + (err instanceof Error ? err.message : String(err)) } : m)
904
+ );
905
+ } finally {
906
+ setIsLoading(false);
907
+ }
908
+ }, [input, isLoading, messages]);
909
+
910
+ return (
911
+ <div className="h-[calc(100vh-60px)] flex flex-col">
912
+ <main className="flex-1 overflow-y-auto px-4 py-6">
913
+ <div className="max-w-3xl mx-auto">
914
+ {messages.length === 0 && (
915
+ <div className="flex flex-col items-center justify-center h-[60vh] text-center">
916
+ <div className="relative mb-6">
917
+ <div className="absolute inset-0 blur-2xl opacity-20 bg-${accentColor}-500 rounded-full" />
918
+ <div className="relative text-5xl">\u26A1</div>
919
+ </div>
920
+ <h2 className="text-2xl font-semibold mb-2">${emptyStateTitle}</h2>
921
+ <p className="text-muted text-sm max-w-md">${emptyStateHint}</p>
922
+ <div className="flex gap-2 mt-6">
923
+ ${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">
924
+ What is 42 \xD7 17?
925
+ </button>
926
+ <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">
927
+ What day is it?
928
+ </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">
929
+ Summarize main topics
930
+ </button>
931
+ <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">
932
+ Key findings
933
+ </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">
934
+ Hello! What can you do?
935
+ </button>
936
+ <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">
937
+ Tell me a fun fact
938
+ </button>`}
939
+ </div>
940
+ </div>
941
+ )}
942
+ {messages.map((msg) => (
943
+ <div key={msg.id} className={\`mb-6 flex \${msg.role === "user" ? "justify-end" : "justify-start"}\`}>
944
+ <div className={\`flex items-start gap-3 max-w-[80%] \${msg.role === "user" ? "flex-row-reverse" : ""}\`}>
945
+ <div className={\`w-8 h-8 rounded-full flex items-center justify-center text-sm shrink-0 \${
946
+ msg.role === "user" ? "bg-${accentColor}-500 text-white" : "bg-white/10 text-muted"
947
+ }\`}>
948
+ {msg.role === "user" ? "Y" : "\u26A1"}
949
+ </div>
950
+ <div className={\`px-4 py-3 rounded-2xl whitespace-pre-wrap leading-relaxed text-sm \${
951
+ msg.role === "user"
952
+ ? "bg-${accentColor}-500 text-white rounded-br-md"
953
+ : "bg-white/[0.05] border border-border rounded-bl-md"
954
+ }\`}>
955
+ {msg.content || (isLoading && msg.role === "assistant" ? (
956
+ <span className="flex gap-1">
957
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:0ms]" />
958
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:150ms]" />
959
+ <span className="w-2 h-2 bg-muted rounded-full animate-bounce [animation-delay:300ms]" />
960
+ </span>
961
+ ) : "")}
962
+ </div>
963
+ </div>
964
+ </div>
965
+ ))}
966
+ <div ref={messagesEndRef} />
967
+ </div>
968
+ </main>
969
+ <footer className="border-t border-border px-4 py-4">
970
+ <form onSubmit={(e) => { e.preventDefault(); sendMessage(); }} className="max-w-3xl mx-auto flex gap-3">
971
+ <input
972
+ ref={inputRef}
973
+ value={input}
974
+ onChange={(e) => setInput(e.target.value)}
975
+ placeholder="${inputPlaceholder}"
976
+ disabled={isLoading}
977
+ 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"
978
+ />
979
+ <button
980
+ type="submit"
981
+ disabled={isLoading || !input.trim()}
982
+ 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"
983
+ >
984
+ {isLoading ? (
985
+ <svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
986
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
987
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
988
+ </svg>
989
+ ) : "Send"}
990
+ </button>
991
+ </form>
992
+ </footer>
993
+ </div>
994
+ );
995
+ }
996
+ `;
997
+ }
998
+ function generateFaviconSVG() {
999
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
1000
+ <rect width="32" height="32" rx="6" fill="#0a0a0a"/>
1001
+ <path d="M18.5 4L8 18h7l-1.5 10L24 14h-7l1.5-10z" fill="#facc15" stroke="#facc15" stroke-width="0.5" stroke-linejoin="round"/>
1002
+ </svg>
1003
+ `;
1004
+ }
1005
+ function generateRobotsTxt() {
1006
+ return `# https://www.robotstxt.org/robotstxt.html
1007
+ User-agent: *
1008
+ Allow: /
1009
+
1010
+ Sitemap: /sitemap.xml
1011
+ `;
1012
+ }
1013
+ function generateWebManifest(projectName) {
1014
+ return JSON.stringify({
1015
+ name: projectName,
1016
+ short_name: projectName,
1017
+ icons: [
1018
+ { src: "/favicon.svg", sizes: "any", type: "image/svg+xml" }
1019
+ ],
1020
+ theme_color: "#0a0a0a",
1021
+ background_color: "#0a0a0a",
1022
+ display: "standalone"
1023
+ }, null, 2) + "\n";
1024
+ }
1025
+
1026
+ export {
1027
+ createProject
1028
+ };
package/dist/cli.js CHANGED
@@ -185,11 +185,13 @@ async function createProject(options) {
185
185
  deps["tailwind-merge"] = "^3.0.0";
186
186
  deps["lucide-react"] = "^0.468.0";
187
187
  }
188
+ devDeps["@voltx/cli"] = deps["@voltx/cli"] ?? v("@voltx/cli");
189
+ delete deps["@voltx/cli"];
188
190
  fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
189
191
  name,
190
192
  version: "0.1.0",
191
193
  private: true,
192
- scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
194
+ scripts: { dev: "npx voltx dev", build: "npx voltx build", start: "npx voltx start" },
193
195
  dependencies: deps,
194
196
  devDependencies: devDeps
195
197
  }, null, 2));
package/dist/create.js CHANGED
@@ -199,11 +199,13 @@ async function createProject(options) {
199
199
  deps["tailwind-merge"] = "^3.0.0";
200
200
  deps["lucide-react"] = "^0.468.0";
201
201
  }
202
+ devDeps["@voltx/cli"] = deps["@voltx/cli"] ?? v("@voltx/cli");
203
+ delete deps["@voltx/cli"];
202
204
  fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
203
205
  name,
204
206
  version: "0.1.0",
205
207
  private: true,
206
- scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
208
+ scripts: { dev: "npx voltx dev", build: "npx voltx build", start: "npx voltx start" },
207
209
  dependencies: deps,
208
210
  devDependencies: devDeps
209
211
  }, null, 2));
package/dist/create.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createProject
4
- } from "./chunk-Q5XCFN7L.mjs";
4
+ } from "./chunk-TFVNHM7S.mjs";
5
5
  import "./chunk-IV352HZA.mjs";
6
6
  export {
7
7
  createProject
package/dist/index.js CHANGED
@@ -205,11 +205,13 @@ async function createProject(options) {
205
205
  deps["tailwind-merge"] = "^3.0.0";
206
206
  deps["lucide-react"] = "^0.468.0";
207
207
  }
208
+ devDeps["@voltx/cli"] = deps["@voltx/cli"] ?? v("@voltx/cli");
209
+ delete deps["@voltx/cli"];
208
210
  fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
209
211
  name,
210
212
  version: "0.1.0",
211
213
  private: true,
212
- scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
214
+ scripts: { dev: "npx voltx dev", build: "npx voltx build", start: "npx voltx start" },
213
215
  dependencies: deps,
214
216
  devDependencies: devDeps
215
217
  }, null, 2));
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-H2DTIOEO.mjs";
4
4
  import {
5
5
  createProject
6
- } from "./chunk-Q5XCFN7L.mjs";
6
+ } from "./chunk-TFVNHM7S.mjs";
7
7
  import {
8
8
  runDev
9
9
  } from "./chunk-AD3WMFZF.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltx/cli",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "VoltX CLI — dev server, build, start, generate, and scaffolding",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -21,7 +21,7 @@
21
21
  "clean": "rm -rf dist"
22
22
  },
23
23
  "dependencies": {
24
- "@voltx/core": "workspace:*"
24
+ "@voltx/core": "^0.3.1"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "tsx": ">=4.0.0",