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