@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
|
@@ -0,0 +1,907 @@
|
|
|
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
|
+
fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
|
|
66
|
+
name,
|
|
67
|
+
version: "0.1.0",
|
|
68
|
+
private: true,
|
|
69
|
+
scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
|
|
70
|
+
dependencies: deps,
|
|
71
|
+
devDependencies: devDeps
|
|
72
|
+
}, null, 2));
|
|
73
|
+
let config = `import { defineConfig } from "@voltx/core";
|
|
74
|
+
|
|
75
|
+
export default defineConfig({
|
|
76
|
+
name: "${name}",
|
|
77
|
+
port: 3000,
|
|
78
|
+
ai: {
|
|
79
|
+
provider: "${provider}",
|
|
80
|
+
model: "${model}",
|
|
81
|
+
},`;
|
|
82
|
+
if (hasDb) config += `
|
|
83
|
+
db: {
|
|
84
|
+
url: process.env.DATABASE_URL,
|
|
85
|
+
},`;
|
|
86
|
+
if (auth !== "none") config += `
|
|
87
|
+
auth: {
|
|
88
|
+
provider: "${auth}",
|
|
89
|
+
},`;
|
|
90
|
+
config += `
|
|
91
|
+
server: {
|
|
92
|
+
routesDir: "api",
|
|
93
|
+
staticDir: "public",
|
|
94
|
+
cors: true,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
`;
|
|
98
|
+
fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
|
|
99
|
+
fs.mkdirSync(path.join(targetDir, "api"), { recursive: true });
|
|
100
|
+
fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
|
|
101
|
+
fs.mkdirSync(path.join(targetDir, "src"), { recursive: true });
|
|
102
|
+
fs.mkdirSync(path.join(targetDir, "src", "components"), { recursive: true });
|
|
103
|
+
fs.mkdirSync(path.join(targetDir, "src", "hooks"), { recursive: true });
|
|
104
|
+
fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
|
|
105
|
+
fs.writeFileSync(path.join(targetDir, "public", "favicon.svg"), generateFaviconSVG());
|
|
106
|
+
fs.writeFileSync(path.join(targetDir, "public", "robots.txt"), generateRobotsTxt());
|
|
107
|
+
fs.writeFileSync(path.join(targetDir, "public", "site.webmanifest"), generateWebManifest(name));
|
|
108
|
+
const tsconfig = {
|
|
109
|
+
compilerOptions: {
|
|
110
|
+
target: "ES2022",
|
|
111
|
+
module: "ESNext",
|
|
112
|
+
moduleResolution: "bundler",
|
|
113
|
+
strict: true,
|
|
114
|
+
esModuleInterop: true,
|
|
115
|
+
skipLibCheck: true,
|
|
116
|
+
outDir: "dist",
|
|
117
|
+
baseUrl: ".",
|
|
118
|
+
paths: { "@/*": ["./src/*"] },
|
|
119
|
+
jsx: "react-jsx"
|
|
120
|
+
},
|
|
121
|
+
include: ["src", "api", "server.ts", "voltx.config.ts"]
|
|
122
|
+
};
|
|
123
|
+
if (template === "agent-app") tsconfig.include.push("agents", "tools");
|
|
124
|
+
fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
|
|
125
|
+
fs.writeFileSync(path.join(targetDir, "server.ts"), generateServerEntry(name, template));
|
|
126
|
+
fs.writeFileSync(path.join(targetDir, "vite.config.ts"), generateViteConfigFile("server.ts"));
|
|
127
|
+
fs.writeFileSync(path.join(targetDir, "src", "entry-client.tsx"), generateEntryClient());
|
|
128
|
+
fs.writeFileSync(path.join(targetDir, "src", "entry-server.tsx"), generateEntryServer());
|
|
129
|
+
fs.writeFileSync(path.join(targetDir, "src", "layout.tsx"), generateLayoutComponent(name));
|
|
130
|
+
fs.writeFileSync(path.join(targetDir, "src", "globals.css"), generateGlobalCSS(shadcn));
|
|
131
|
+
if (shadcn) {
|
|
132
|
+
fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.ts"), generateCnUtil());
|
|
133
|
+
fs.writeFileSync(path.join(targetDir, "components.json"), generateComponentsJson());
|
|
134
|
+
}
|
|
135
|
+
fs.writeFileSync(path.join(targetDir, "src", "app.tsx"), generateAppComponent(name, template));
|
|
136
|
+
fs.writeFileSync(
|
|
137
|
+
path.join(targetDir, "api", "index.ts"),
|
|
138
|
+
`// GET /api \u2014 Health check
|
|
139
|
+
import type { Context } from "@voltx/server";
|
|
140
|
+
|
|
141
|
+
export function GET(c: Context) {
|
|
142
|
+
return c.json({ name: "${name}", status: "ok" });
|
|
143
|
+
}
|
|
144
|
+
`
|
|
145
|
+
);
|
|
146
|
+
if (template === "chatbot" || template === "agent-app") {
|
|
147
|
+
fs.writeFileSync(
|
|
148
|
+
path.join(targetDir, "api", "chat.ts"),
|
|
149
|
+
`// POST /api/chat \u2014 Streaming chat with conversation memory
|
|
150
|
+
import type { Context } from "@voltx/server";
|
|
151
|
+
import { streamText } from "@voltx/ai";
|
|
152
|
+
import { createMemory } from "@voltx/memory";
|
|
153
|
+
|
|
154
|
+
const memory = createMemory({ maxMessages: 50 });
|
|
155
|
+
|
|
156
|
+
export async function POST(c: Context) {
|
|
157
|
+
const { messages, conversationId = "default" } = await c.req.json();
|
|
158
|
+
|
|
159
|
+
const lastMessage = messages[messages.length - 1];
|
|
160
|
+
if (lastMessage?.role === "user") {
|
|
161
|
+
await memory.add(conversationId, { role: "user", content: lastMessage.content });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const history = await memory.get(conversationId);
|
|
165
|
+
|
|
166
|
+
const result = await streamText({
|
|
167
|
+
model: "${provider}:${model}",
|
|
168
|
+
system: "You are a helpful AI assistant.",
|
|
169
|
+
messages: history.map((m) => ({ role: m.role, content: m.content })),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
result.text.then(async (text) => {
|
|
173
|
+
await memory.add(conversationId, { role: "assistant", content: text });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return result.toSSEResponse();
|
|
177
|
+
}
|
|
178
|
+
`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (template === "agent-app") {
|
|
182
|
+
fs.mkdirSync(path.join(targetDir, "agents"), { recursive: true });
|
|
183
|
+
fs.mkdirSync(path.join(targetDir, "tools"), { recursive: true });
|
|
184
|
+
fs.writeFileSync(path.join(targetDir, "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
|
|
185
|
+
import type { Tool } from "@voltx/agents";
|
|
186
|
+
|
|
187
|
+
export const calculatorTool: Tool = {
|
|
188
|
+
name: "calculator",
|
|
189
|
+
description: "Evaluate a math expression. Supports +, -, *, /, %, parentheses, and Math functions.",
|
|
190
|
+
parameters: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: { expression: { type: "string", description: "The math expression to evaluate" } },
|
|
193
|
+
required: ["expression"],
|
|
194
|
+
},
|
|
195
|
+
async execute(args: { expression: string }) {
|
|
196
|
+
try {
|
|
197
|
+
const safe = args.expression.replace(/[^0-9+\\-*/.()%\\s,]|(?<!Math)\\.[a-z]/gi, (match) => {
|
|
198
|
+
if (args.expression.includes("Math.")) return match;
|
|
199
|
+
throw new Error("Invalid character: " + match);
|
|
200
|
+
});
|
|
201
|
+
const result = new Function("return " + safe)();
|
|
202
|
+
return \`\${args.expression} = \${result}\`;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
return \`Error: \${err instanceof Error ? err.message : String(err)}\`;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
`);
|
|
209
|
+
fs.writeFileSync(path.join(targetDir, "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
|
|
210
|
+
import type { Tool } from "@voltx/agents";
|
|
211
|
+
|
|
212
|
+
export const datetimeTool: Tool = {
|
|
213
|
+
name: "datetime",
|
|
214
|
+
description: "Get the current date, time, day of week, and timezone.",
|
|
215
|
+
parameters: {
|
|
216
|
+
type: "object",
|
|
217
|
+
properties: { timezone: { type: "string", description: "Optional IANA timezone. Defaults to server timezone." } },
|
|
218
|
+
},
|
|
219
|
+
async execute(args: { timezone?: string }) {
|
|
220
|
+
const now = new Date();
|
|
221
|
+
const tz = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
222
|
+
const formatted = now.toLocaleString("en-US", {
|
|
223
|
+
timeZone: tz, weekday: "long", year: "numeric", month: "long", day: "numeric",
|
|
224
|
+
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true,
|
|
225
|
+
});
|
|
226
|
+
return \`Current date/time (\${tz}): \${formatted}\`;
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
`);
|
|
230
|
+
fs.writeFileSync(path.join(targetDir, "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
|
|
231
|
+
import { createAgent } from "@voltx/agents";
|
|
232
|
+
import { calculatorTool } from "../tools/calculator";
|
|
233
|
+
import { datetimeTool } from "../tools/datetime";
|
|
234
|
+
|
|
235
|
+
export const assistant = createAgent({
|
|
236
|
+
name: "assistant",
|
|
237
|
+
model: "${provider}:${model}",
|
|
238
|
+
instructions: "You are a helpful AI assistant with access to tools: Calculator, Date & Time. Use them when needed to answer questions accurately.",
|
|
239
|
+
tools: [calculatorTool, datetimeTool],
|
|
240
|
+
maxIterations: 5,
|
|
241
|
+
});
|
|
242
|
+
`);
|
|
243
|
+
fs.writeFileSync(
|
|
244
|
+
path.join(targetDir, "api", "agent.ts"),
|
|
245
|
+
`// POST /api/agent \u2014 Run the AI agent
|
|
246
|
+
import type { Context } from "@voltx/server";
|
|
247
|
+
import { assistant } from "../agents/assistant";
|
|
248
|
+
|
|
249
|
+
export async function POST(c: Context) {
|
|
250
|
+
const { input } = await c.req.json();
|
|
251
|
+
if (!input) return c.json({ error: "Missing 'input' field" }, 400);
|
|
252
|
+
const result = await assistant.run(input);
|
|
253
|
+
return c.json({ content: result.content, steps: result.steps });
|
|
254
|
+
}
|
|
255
|
+
`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (template === "rag-app") {
|
|
259
|
+
const embedModel = "openai:text-embedding-3-small";
|
|
260
|
+
fs.mkdirSync(path.join(targetDir, "api", "rag"), { recursive: true });
|
|
261
|
+
fs.writeFileSync(
|
|
262
|
+
path.join(targetDir, "api", "rag", "query.ts"),
|
|
263
|
+
`// POST /api/rag/query \u2014 Query documents with RAG
|
|
264
|
+
import type { Context } from "@voltx/server";
|
|
265
|
+
import { streamText } from "@voltx/ai";
|
|
266
|
+
import { createRAGPipeline, createEmbedder } from "@voltx/rag";
|
|
267
|
+
import { createVectorStore } from "@voltx/db";
|
|
268
|
+
|
|
269
|
+
const vectorStore = createVectorStore();
|
|
270
|
+
const embedder = createEmbedder({ model: "${embedModel}" });
|
|
271
|
+
const rag = createRAGPipeline({ embedder, vectorStore });
|
|
272
|
+
|
|
273
|
+
export async function POST(c: Context) {
|
|
274
|
+
const { question } = await c.req.json();
|
|
275
|
+
const context = await rag.getContext(question, { topK: 5 });
|
|
276
|
+
const result = await streamText({
|
|
277
|
+
model: "${provider}:${model}",
|
|
278
|
+
system: \`Answer based on context. If not relevant, say so.\\n\\nContext:\\n\${context}\`,
|
|
279
|
+
messages: [{ role: "user", content: question }],
|
|
280
|
+
});
|
|
281
|
+
return result.toSSEResponse();
|
|
282
|
+
}
|
|
283
|
+
`
|
|
284
|
+
);
|
|
285
|
+
fs.writeFileSync(
|
|
286
|
+
path.join(targetDir, "api", "rag", "ingest.ts"),
|
|
287
|
+
`// POST /api/rag/ingest \u2014 Ingest documents into the vector store
|
|
288
|
+
import type { Context } from "@voltx/server";
|
|
289
|
+
import { createRAGPipeline, createEmbedder } from "@voltx/rag";
|
|
290
|
+
import { createVectorStore } from "@voltx/db";
|
|
291
|
+
|
|
292
|
+
const vectorStore = createVectorStore();
|
|
293
|
+
const embedder = createEmbedder({ model: "${embedModel}" });
|
|
294
|
+
const rag = createRAGPipeline({ embedder, vectorStore });
|
|
295
|
+
|
|
296
|
+
export async function POST(c: Context) {
|
|
297
|
+
const { text, idPrefix } = await c.req.json();
|
|
298
|
+
if (!text || typeof text !== "string") return c.json({ error: "Missing 'text' field" }, 400);
|
|
299
|
+
const result = await rag.ingest(text, idPrefix ?? "doc");
|
|
300
|
+
return c.json({ status: "ok", chunks: result.chunks, ids: result.ids });
|
|
301
|
+
}
|
|
302
|
+
`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (auth === "better-auth") {
|
|
306
|
+
fs.mkdirSync(path.join(targetDir, "api", "auth"), { recursive: true });
|
|
307
|
+
fs.writeFileSync(
|
|
308
|
+
path.join(targetDir, "api", "auth", "[...path].ts"),
|
|
309
|
+
`// ALL /api/auth/* \u2014 Better Auth handler
|
|
310
|
+
import type { Context } from "@voltx/server";
|
|
311
|
+
import { auth } from "../../src/lib/auth";
|
|
312
|
+
import { createAuthHandler } from "@voltx/auth";
|
|
313
|
+
|
|
314
|
+
const handler = createAuthHandler(auth);
|
|
315
|
+
|
|
316
|
+
export const GET = (c: Context) => handler(c);
|
|
317
|
+
export const POST = (c: Context) => handler(c);
|
|
318
|
+
`
|
|
319
|
+
);
|
|
320
|
+
fs.writeFileSync(
|
|
321
|
+
path.join(targetDir, "src", "lib", "auth.ts"),
|
|
322
|
+
`import { createAuth, createAuthMiddleware } from "@voltx/auth";
|
|
323
|
+
|
|
324
|
+
export const auth = createAuth("better-auth", {
|
|
325
|
+
database: process.env.DATABASE_URL!,
|
|
326
|
+
emailAndPassword: true,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
export const authMiddleware = createAuthMiddleware({
|
|
330
|
+
provider: auth,
|
|
331
|
+
publicPaths: ["/api/auth", "/api/health", "/"],
|
|
332
|
+
});
|
|
333
|
+
`
|
|
334
|
+
);
|
|
335
|
+
} else if (auth === "jwt") {
|
|
336
|
+
fs.writeFileSync(
|
|
337
|
+
path.join(targetDir, "src", "lib", "auth.ts"),
|
|
338
|
+
`import { createAuth, createAuthMiddleware } from "@voltx/auth";
|
|
339
|
+
|
|
340
|
+
export const jwt = createAuth("jwt", {
|
|
341
|
+
secret: process.env.JWT_SECRET!,
|
|
342
|
+
expiresIn: "7d",
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
export const authMiddleware = createAuthMiddleware({
|
|
346
|
+
provider: jwt,
|
|
347
|
+
publicPaths: ["/api/auth", "/api/health", "/"],
|
|
348
|
+
});
|
|
349
|
+
`
|
|
350
|
+
);
|
|
351
|
+
fs.writeFileSync(
|
|
352
|
+
path.join(targetDir, "api", "auth.ts"),
|
|
353
|
+
`import type { Context } from "@voltx/server";
|
|
354
|
+
import { jwt } from "../src/lib/auth";
|
|
355
|
+
|
|
356
|
+
export async function POST(c: Context) {
|
|
357
|
+
const { email, password } = await c.req.json();
|
|
358
|
+
if (!email || !password) return c.json({ error: "Email and password are required" }, 400);
|
|
359
|
+
const token = await jwt.sign({ sub: email, email });
|
|
360
|
+
return c.json({ token });
|
|
361
|
+
}
|
|
362
|
+
`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
let envContent = "";
|
|
366
|
+
if (template === "rag-app") {
|
|
367
|
+
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";
|
|
368
|
+
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";
|
|
369
|
+
} else if (template === "chatbot" || template === "agent-app") {
|
|
370
|
+
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";
|
|
371
|
+
} else {
|
|
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\n# OPENAI_API_KEY=sk-...\n# CEREBRAS_API_KEY=csk-...\n\n";
|
|
373
|
+
}
|
|
374
|
+
if (auth === "better-auth") {
|
|
375
|
+
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";
|
|
376
|
+
} else if (auth === "jwt") {
|
|
377
|
+
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";
|
|
378
|
+
}
|
|
379
|
+
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";
|
|
380
|
+
fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
|
|
381
|
+
fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.env.local\n.env.*.local\nvite.config.voltx.ts\n");
|
|
382
|
+
printWelcomeBanner(name);
|
|
383
|
+
}
|
|
384
|
+
function generateServerEntry(projectName, template) {
|
|
385
|
+
const imports = [];
|
|
386
|
+
const mounts = [];
|
|
387
|
+
imports.push('import { GET as healthGET } from "./api/index";');
|
|
388
|
+
mounts.push('app.get("/api", healthGET);');
|
|
389
|
+
if (template === "chatbot" || template === "agent-app") {
|
|
390
|
+
imports.push('import { POST as chatPOST } from "./api/chat";');
|
|
391
|
+
mounts.push('app.post("/api/chat", chatPOST);');
|
|
392
|
+
}
|
|
393
|
+
if (template === "agent-app") {
|
|
394
|
+
imports.push('import { POST as agentPOST } from "./api/agent";');
|
|
395
|
+
mounts.push('app.post("/api/agent", agentPOST);');
|
|
396
|
+
}
|
|
397
|
+
if (template === "rag-app") {
|
|
398
|
+
imports.push('import { POST as ragQueryPOST } from "./api/rag/query";');
|
|
399
|
+
imports.push('import { POST as ragIngestPOST } from "./api/rag/ingest";');
|
|
400
|
+
mounts.push('app.post("/api/rag/query", ragQueryPOST);');
|
|
401
|
+
mounts.push('app.post("/api/rag/ingest", ragIngestPOST);');
|
|
402
|
+
}
|
|
403
|
+
return `import { Hono } from "hono";
|
|
404
|
+
import { serve } from "@hono/node-server";
|
|
405
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
406
|
+
import { registerSSR } from "@voltx/server";
|
|
407
|
+
import { loadEnv } from "@voltx/core";
|
|
408
|
+
${imports.join("\n")}
|
|
409
|
+
|
|
410
|
+
loadEnv(process.env.NODE_ENV ?? "development");
|
|
411
|
+
|
|
412
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
413
|
+
const app = new Hono();
|
|
414
|
+
|
|
415
|
+
// \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
|
|
416
|
+
${mounts.join("\n")}
|
|
417
|
+
|
|
418
|
+
// \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
|
|
419
|
+
if (isProd) {
|
|
420
|
+
app.use("/assets/*", serveStatic({ root: "./dist/client/" }));
|
|
421
|
+
app.use("/favicon.svg", serveStatic({ root: "./public/" }));
|
|
422
|
+
app.use("/robots.txt", serveStatic({ root: "./public/" }));
|
|
423
|
+
app.use("/site.webmanifest", serveStatic({ root: "./public/" }));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// \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
|
|
427
|
+
registerSSR(app, null, {
|
|
428
|
+
title: "${projectName}",
|
|
429
|
+
entryServer: "src/entry-server.tsx",
|
|
430
|
+
entryClient: "src/entry-client.tsx",
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
export default app;
|
|
434
|
+
|
|
435
|
+
if (isProd) {
|
|
436
|
+
const port = Number(process.env.PORT) || 3000;
|
|
437
|
+
serve({ fetch: app.fetch, port }, (info) => {
|
|
438
|
+
console.log(\`\\n \u26A1 ${projectName} running at http://localhost:\${info.port}\\n\`);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
`;
|
|
442
|
+
}
|
|
443
|
+
function generateViteConfigFile(entry) {
|
|
444
|
+
return `import { defineConfig } from "vite";
|
|
445
|
+
import devServer from "@hono/vite-dev-server";
|
|
446
|
+
import react from "@vitejs/plugin-react";
|
|
447
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
448
|
+
|
|
449
|
+
export default defineConfig({
|
|
450
|
+
resolve: {
|
|
451
|
+
alias: {
|
|
452
|
+
"@": "/src",
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
plugins: [
|
|
456
|
+
react(),
|
|
457
|
+
tailwindcss(),
|
|
458
|
+
devServer({
|
|
459
|
+
entry: "${entry}",
|
|
460
|
+
exclude: [
|
|
461
|
+
/.*\\.tsx?($|\\?)/,
|
|
462
|
+
/.*\\.(s?css|less)($|\\?)/,
|
|
463
|
+
/.*\\.(svg|png|jpg|jpeg|gif|webp|ico)($|\\?)/,
|
|
464
|
+
/^\\/@.+$/,
|
|
465
|
+
/^\\/favicon\\.svg$/,
|
|
466
|
+
/^\\/node_modules\\/.*/,
|
|
467
|
+
/^\\/src\\/.*/,
|
|
468
|
+
],
|
|
469
|
+
injectClientScript: false,
|
|
470
|
+
}),
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
function generateEntryClient() {
|
|
476
|
+
return `import React from "react";
|
|
477
|
+
import { hydrateRoot } from "react-dom/client";
|
|
478
|
+
import Layout from "./layout";
|
|
479
|
+
import App from "./app";
|
|
480
|
+
import "./globals.css";
|
|
481
|
+
|
|
482
|
+
hydrateRoot(
|
|
483
|
+
document.getElementById("root")!,
|
|
484
|
+
<React.StrictMode>
|
|
485
|
+
<Layout>
|
|
486
|
+
<App />
|
|
487
|
+
</Layout>
|
|
488
|
+
</React.StrictMode>
|
|
489
|
+
);
|
|
490
|
+
`;
|
|
491
|
+
}
|
|
492
|
+
function generateEntryServer() {
|
|
493
|
+
return `import React from "react";
|
|
494
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
495
|
+
import Layout from "./layout";
|
|
496
|
+
import App from "./app";
|
|
497
|
+
|
|
498
|
+
export async function render(_url: string): Promise<ReadableStream> {
|
|
499
|
+
const stream = await renderToReadableStream(
|
|
500
|
+
<React.StrictMode>
|
|
501
|
+
<Layout>
|
|
502
|
+
<App />
|
|
503
|
+
</Layout>
|
|
504
|
+
</React.StrictMode>,
|
|
505
|
+
{
|
|
506
|
+
onError(error: unknown) {
|
|
507
|
+
console.error("[voltx] SSR render error:", error);
|
|
508
|
+
},
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
return stream;
|
|
512
|
+
}
|
|
513
|
+
`;
|
|
514
|
+
}
|
|
515
|
+
function generateLayoutComponent(projectName) {
|
|
516
|
+
return `import React from "react";
|
|
517
|
+
|
|
518
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
519
|
+
return (
|
|
520
|
+
<div style={{ minHeight: "100vh", fontFamily: "system-ui, -apple-system, sans-serif", background: "#0a0a0a", color: "#ededed" }}>
|
|
521
|
+
<header style={{ borderBottom: "1px solid #1a1a1a", padding: "0.75rem 1.5rem", display: "flex", alignItems: "center", justifyContent: "space-between" }}>
|
|
522
|
+
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
|
523
|
+
<span style={{ fontSize: "1.25rem" }}>\u26A1</span>
|
|
524
|
+
<span style={{ fontWeight: 600 }}>${projectName}</span>
|
|
525
|
+
</div>
|
|
526
|
+
<a href="https://github.com/codewithshail/voltx" target="_blank" rel="noopener noreferrer" style={{ color: "#666", fontSize: "0.85rem", textDecoration: "none" }}>
|
|
527
|
+
Built with VoltX
|
|
528
|
+
</a>
|
|
529
|
+
</header>
|
|
530
|
+
<main>{children}</main>
|
|
531
|
+
</div>
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
`;
|
|
535
|
+
}
|
|
536
|
+
function generateGlobalCSS(useShadcn = false) {
|
|
537
|
+
if (useShadcn) {
|
|
538
|
+
return `@import "tailwindcss";
|
|
539
|
+
|
|
540
|
+
@theme inline {
|
|
541
|
+
--radius-sm: 0.25rem;
|
|
542
|
+
--radius-md: 0.375rem;
|
|
543
|
+
--radius-lg: 0.5rem;
|
|
544
|
+
--radius-xl: 0.75rem;
|
|
545
|
+
--color-background: hsl(var(--background));
|
|
546
|
+
--color-foreground: hsl(var(--foreground));
|
|
547
|
+
--color-card: hsl(var(--card));
|
|
548
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
549
|
+
--color-popover: hsl(var(--popover));
|
|
550
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
551
|
+
--color-primary: hsl(var(--primary));
|
|
552
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
553
|
+
--color-secondary: hsl(var(--secondary));
|
|
554
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
555
|
+
--color-muted: hsl(var(--muted));
|
|
556
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
557
|
+
--color-accent: hsl(var(--accent));
|
|
558
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
559
|
+
--color-destructive: hsl(var(--destructive));
|
|
560
|
+
--color-border: hsl(var(--border));
|
|
561
|
+
--color-input: hsl(var(--input));
|
|
562
|
+
--color-ring: hsl(var(--ring));
|
|
563
|
+
--color-chart-1: hsl(var(--chart-1));
|
|
564
|
+
--color-chart-2: hsl(var(--chart-2));
|
|
565
|
+
--color-chart-3: hsl(var(--chart-3));
|
|
566
|
+
--color-chart-4: hsl(var(--chart-4));
|
|
567
|
+
--color-chart-5: hsl(var(--chart-5));
|
|
568
|
+
--color-sidebar: hsl(var(--sidebar));
|
|
569
|
+
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
|
|
570
|
+
--color-sidebar-primary: hsl(var(--sidebar-primary));
|
|
571
|
+
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
|
|
572
|
+
--color-sidebar-accent: hsl(var(--sidebar-accent));
|
|
573
|
+
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
|
|
574
|
+
--color-sidebar-border: hsl(var(--sidebar-border));
|
|
575
|
+
--color-sidebar-ring: hsl(var(--sidebar-ring));
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
:root {
|
|
579
|
+
--background: 0 0% 4%;
|
|
580
|
+
--foreground: 0 0% 93%;
|
|
581
|
+
--card: 0 0% 6%;
|
|
582
|
+
--card-foreground: 0 0% 93%;
|
|
583
|
+
--popover: 0 0% 6%;
|
|
584
|
+
--popover-foreground: 0 0% 93%;
|
|
585
|
+
--primary: 0 0% 93%;
|
|
586
|
+
--primary-foreground: 0 0% 6%;
|
|
587
|
+
--secondary: 0 0% 12%;
|
|
588
|
+
--secondary-foreground: 0 0% 93%;
|
|
589
|
+
--muted: 0 0% 12%;
|
|
590
|
+
--muted-foreground: 0 0% 55%;
|
|
591
|
+
--accent: 0 0% 12%;
|
|
592
|
+
--accent-foreground: 0 0% 93%;
|
|
593
|
+
--destructive: 0 62% 50%;
|
|
594
|
+
--border: 0 0% 14%;
|
|
595
|
+
--input: 0 0% 14%;
|
|
596
|
+
--ring: 0 0% 83%;
|
|
597
|
+
--chart-1: 220 70% 50%;
|
|
598
|
+
--chart-2: 160 60% 45%;
|
|
599
|
+
--chart-3: 30 80% 55%;
|
|
600
|
+
--chart-4: 280 65% 60%;
|
|
601
|
+
--chart-5: 340 75% 55%;
|
|
602
|
+
--sidebar: 0 0% 5%;
|
|
603
|
+
--sidebar-foreground: 0 0% 93%;
|
|
604
|
+
--sidebar-primary: 0 0% 93%;
|
|
605
|
+
--sidebar-primary-foreground: 0 0% 6%;
|
|
606
|
+
--sidebar-accent: 0 0% 12%;
|
|
607
|
+
--sidebar-accent-foreground: 0 0% 93%;
|
|
608
|
+
--sidebar-border: 0 0% 14%;
|
|
609
|
+
--sidebar-ring: 0 0% 83%;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
*,
|
|
613
|
+
*::before,
|
|
614
|
+
*::after {
|
|
615
|
+
box-sizing: border-box;
|
|
616
|
+
margin: 0;
|
|
617
|
+
padding: 0;
|
|
618
|
+
border-color: var(--color-border);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
html,
|
|
622
|
+
body {
|
|
623
|
+
height: 100%;
|
|
624
|
+
background: var(--color-background);
|
|
625
|
+
color: var(--color-foreground);
|
|
626
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
627
|
+
-webkit-font-smoothing: antialiased;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#root {
|
|
631
|
+
height: 100%;
|
|
632
|
+
}
|
|
633
|
+
`;
|
|
634
|
+
}
|
|
635
|
+
return `@import "tailwindcss";
|
|
636
|
+
|
|
637
|
+
@theme {
|
|
638
|
+
--color-background: #0a0a0a;
|
|
639
|
+
--color-foreground: #ededed;
|
|
640
|
+
--color-muted: #888888;
|
|
641
|
+
--color-border: #222222;
|
|
642
|
+
--color-primary: #2563eb;
|
|
643
|
+
--color-accent: #a78bfa;
|
|
644
|
+
--font-sans: system-ui, -apple-system, sans-serif;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
*,
|
|
648
|
+
*::before,
|
|
649
|
+
*::after {
|
|
650
|
+
box-sizing: border-box;
|
|
651
|
+
margin: 0;
|
|
652
|
+
padding: 0;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
html,
|
|
656
|
+
body {
|
|
657
|
+
height: 100%;
|
|
658
|
+
background: var(--color-background);
|
|
659
|
+
color: var(--color-foreground);
|
|
660
|
+
font-family: var(--font-sans);
|
|
661
|
+
-webkit-font-smoothing: antialiased;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
#root {
|
|
665
|
+
height: 100%;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
a {
|
|
669
|
+
color: inherit;
|
|
670
|
+
text-decoration: none;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
a:hover {
|
|
674
|
+
text-decoration: underline;
|
|
675
|
+
}
|
|
676
|
+
`;
|
|
677
|
+
}
|
|
678
|
+
function generateCnUtil() {
|
|
679
|
+
return `import { type ClassValue, clsx } from "clsx";
|
|
680
|
+
import { twMerge } from "tailwind-merge";
|
|
681
|
+
|
|
682
|
+
export function cn(...inputs: ClassValue[]) {
|
|
683
|
+
return twMerge(clsx(inputs));
|
|
684
|
+
}
|
|
685
|
+
`;
|
|
686
|
+
}
|
|
687
|
+
function generateComponentsJson() {
|
|
688
|
+
return JSON.stringify({
|
|
689
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
690
|
+
style: "new-york",
|
|
691
|
+
rsc: false,
|
|
692
|
+
tsx: true,
|
|
693
|
+
tailwind: {
|
|
694
|
+
config: "",
|
|
695
|
+
css: "src/globals.css",
|
|
696
|
+
baseColor: "neutral",
|
|
697
|
+
cssVariables: true
|
|
698
|
+
},
|
|
699
|
+
aliases: {
|
|
700
|
+
components: "@/components",
|
|
701
|
+
utils: "@/lib/utils",
|
|
702
|
+
ui: "@/components/ui",
|
|
703
|
+
lib: "@/lib",
|
|
704
|
+
hooks: "@/hooks"
|
|
705
|
+
}
|
|
706
|
+
}, null, 2) + "\n";
|
|
707
|
+
}
|
|
708
|
+
function generateAppComponent(projectName, template) {
|
|
709
|
+
if (template === "blank") {
|
|
710
|
+
return `import React, { useState, useEffect } from "react";
|
|
711
|
+
|
|
712
|
+
export default function App() {
|
|
713
|
+
const [status, setStatus] = useState<string>("checking...");
|
|
714
|
+
|
|
715
|
+
useEffect(() => {
|
|
716
|
+
fetch("/api")
|
|
717
|
+
.then((res) => res.json())
|
|
718
|
+
.then((data) => setStatus(data.status || "ok"))
|
|
719
|
+
.catch(() => setStatus("error"));
|
|
720
|
+
}, []);
|
|
721
|
+
|
|
722
|
+
return (
|
|
723
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "calc(100vh - 60px)", padding: "2rem" }}>
|
|
724
|
+
<div style={{ textAlign: "center", maxWidth: "600px" }}>
|
|
725
|
+
<div style={{ fontSize: "4rem", marginBottom: "1rem" }}>\u26A1</div>
|
|
726
|
+
<h1 style={{ fontSize: "2.5rem", fontWeight: 700, margin: "0 0 0.5rem" }}>${projectName}</h1>
|
|
727
|
+
<p style={{ color: "#888", fontSize: "1.1rem", margin: "0 0 2rem" }}>Built with VoltX</p>
|
|
728
|
+
<div style={{ display: "flex", gap: "1rem", justifyContent: "center", marginBottom: "2rem" }}>
|
|
729
|
+
<div style={{ padding: "1rem 1.5rem", borderRadius: "0.75rem", background: "#111", border: "1px solid #222" }}>
|
|
730
|
+
<div style={{ fontSize: "0.75rem", color: "#666", marginBottom: "0.25rem" }}>Server</div>
|
|
731
|
+
<div style={{ color: status === "ok" ? "#4ade80" : "#f87171" }}>{status}</div>
|
|
732
|
+
</div>
|
|
733
|
+
<div style={{ padding: "1rem 1.5rem", borderRadius: "0.75rem", background: "#111", border: "1px solid #222" }}>
|
|
734
|
+
<div style={{ fontSize: "0.75rem", color: "#666", marginBottom: "0.25rem" }}>Frontend</div>
|
|
735
|
+
<div style={{ color: "#4ade80" }}>React + Vite</div>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
<div style={{ background: "#111", border: "1px solid #222", borderRadius: "0.75rem", padding: "1.5rem", textAlign: "left", fontSize: "0.9rem" }}>
|
|
739
|
+
<p style={{ color: "#888", margin: "0 0 1rem" }}>Get started:</p>
|
|
740
|
+
<code style={{ display: "block", color: "#a78bfa", marginBottom: "0.5rem" }}>src/app.tsx</code>
|
|
741
|
+
<p style={{ color: "#666", margin: "0 0 0.75rem", fontSize: "0.85rem" }}>Edit this file to build your UI</p>
|
|
742
|
+
<code style={{ display: "block", color: "#a78bfa", marginBottom: "0.5rem" }}>api/</code>
|
|
743
|
+
<p style={{ color: "#666", margin: 0, fontSize: "0.85rem" }}>Add API routes here (file-based routing)</p>
|
|
744
|
+
</div>
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
`;
|
|
750
|
+
}
|
|
751
|
+
const apiEndpoint = template === "agent-app" ? "/api/agent" : template === "rag-app" ? "/api/rag/query" : "/api/chat";
|
|
752
|
+
const isAgent = template === "agent-app";
|
|
753
|
+
const isRag = template === "rag-app";
|
|
754
|
+
return `import React, { useState, useRef, useEffect, useCallback } from "react";
|
|
755
|
+
|
|
756
|
+
interface Message {
|
|
757
|
+
id: string;
|
|
758
|
+
role: "user" | "assistant";
|
|
759
|
+
content: string;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export default function App() {
|
|
763
|
+
const [messages, setMessages] = useState<Message[]>([]);
|
|
764
|
+
const [input, setInput] = useState("");
|
|
765
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
766
|
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
767
|
+
|
|
768
|
+
useEffect(() => {
|
|
769
|
+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
770
|
+
}, [messages]);
|
|
771
|
+
|
|
772
|
+
const sendMessage = useCallback(async () => {
|
|
773
|
+
const text = input.trim();
|
|
774
|
+
if (!text || isLoading) return;
|
|
775
|
+
|
|
776
|
+
const userMsg: Message = { id: crypto.randomUUID(), role: "user", content: text };
|
|
777
|
+
setMessages((prev) => [...prev, userMsg]);
|
|
778
|
+
setInput("");
|
|
779
|
+
setIsLoading(true);
|
|
780
|
+
|
|
781
|
+
const assistantMsg: Message = { id: crypto.randomUUID(), role: "assistant", content: "" };
|
|
782
|
+
setMessages((prev) => [...prev, assistantMsg]);
|
|
783
|
+
|
|
784
|
+
try {${isAgent ? `
|
|
785
|
+
const res = await fetch("${apiEndpoint}", {
|
|
786
|
+
method: "POST",
|
|
787
|
+
headers: { "Content-Type": "application/json" },
|
|
788
|
+
body: JSON.stringify({ input: text }),
|
|
789
|
+
});
|
|
790
|
+
const data = await res.json();
|
|
791
|
+
setMessages((prev) =>
|
|
792
|
+
prev.map((m) => m.id === assistantMsg.id ? { ...m, content: data.content || "No response" } : m)
|
|
793
|
+
);` : `
|
|
794
|
+
const res = await fetch("${apiEndpoint}", {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: { "Content-Type": "application/json" },
|
|
797
|
+
body: JSON.stringify({${isRag ? ` question: text ` : ` messages: [...messages, { role: "user", content: text }] `}}),
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
if (!res.body) throw new Error("No response body");
|
|
801
|
+
|
|
802
|
+
const reader = res.body.getReader();
|
|
803
|
+
const decoder = new TextDecoder();
|
|
804
|
+
let buffer = "";
|
|
805
|
+
let fullContent = "";
|
|
806
|
+
|
|
807
|
+
while (true) {
|
|
808
|
+
const { done, value } = await reader.read();
|
|
809
|
+
if (done) break;
|
|
810
|
+
buffer += decoder.decode(value, { stream: true });
|
|
811
|
+
const lines = buffer.split("\\n");
|
|
812
|
+
buffer = lines.pop() ?? "";
|
|
813
|
+
|
|
814
|
+
for (const line of lines) {
|
|
815
|
+
if (!line.startsWith("data: ")) continue;
|
|
816
|
+
const data = line.slice(6);
|
|
817
|
+
if (data === "[DONE]") break;
|
|
818
|
+
try {
|
|
819
|
+
const parsed = JSON.parse(data);
|
|
820
|
+
const chunk = parsed.textDelta ?? parsed.content ?? parsed.choices?.[0]?.delta?.content ?? "";
|
|
821
|
+
if (chunk) {
|
|
822
|
+
fullContent += chunk;
|
|
823
|
+
setMessages((prev) =>
|
|
824
|
+
prev.map((m) => m.id === assistantMsg.id ? { ...m, content: fullContent } : m)
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
} catch {}
|
|
828
|
+
}
|
|
829
|
+
}`}
|
|
830
|
+
} catch (err) {
|
|
831
|
+
setMessages((prev) =>
|
|
832
|
+
prev.map((m) => m.id === assistantMsg.id ? { ...m, content: "Error: " + (err instanceof Error ? err.message : String(err)) } : m)
|
|
833
|
+
);
|
|
834
|
+
} finally {
|
|
835
|
+
setIsLoading(false);
|
|
836
|
+
}
|
|
837
|
+
}, [input, isLoading, messages]);
|
|
838
|
+
|
|
839
|
+
return (
|
|
840
|
+
<div style={{ height: "calc(100vh - 60px)", display: "flex", flexDirection: "column" }}>
|
|
841
|
+
<main style={{ flex: 1, overflow: "auto", padding: "1rem 1.5rem" }}>
|
|
842
|
+
{messages.length === 0 && (
|
|
843
|
+
<div style={{ textAlign: "center", marginTop: "4rem", color: "#555" }}>
|
|
844
|
+
<p style={{ fontSize: "1.5rem" }}>\u26A1</p>
|
|
845
|
+
<p>Start a conversation</p>
|
|
846
|
+
</div>
|
|
847
|
+
)}
|
|
848
|
+
{messages.map((msg) => (
|
|
849
|
+
<div key={msg.id} style={{ marginBottom: "1rem", display: "flex", justifyContent: msg.role === "user" ? "flex-end" : "flex-start" }}>
|
|
850
|
+
<div style={{
|
|
851
|
+
maxWidth: "70%", padding: "0.75rem 1rem", borderRadius: "0.75rem",
|
|
852
|
+
background: msg.role === "user" ? "#2563eb" : "#1a1a1a",
|
|
853
|
+
border: msg.role === "assistant" ? "1px solid #333" : "none",
|
|
854
|
+
whiteSpace: "pre-wrap", lineHeight: 1.5,
|
|
855
|
+
}}>
|
|
856
|
+
{msg.content || (isLoading && msg.role === "assistant" ? "..." : "")}
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
))}
|
|
860
|
+
<div ref={messagesEndRef} />
|
|
861
|
+
</main>
|
|
862
|
+
<footer style={{ padding: "1rem 1.5rem", borderTop: "1px solid #222" }}>
|
|
863
|
+
<form onSubmit={(e) => { e.preventDefault(); sendMessage(); }} style={{ display: "flex", gap: "0.5rem" }}>
|
|
864
|
+
<input value={input} onChange={(e) => setInput(e.target.value)} placeholder="Type a message..." disabled={isLoading}
|
|
865
|
+
style={{ flex: 1, padding: "0.75rem 1rem", borderRadius: "0.5rem", border: "1px solid #333", background: "#111", color: "#e5e5e5", outline: "none", fontSize: "0.95rem" }} />
|
|
866
|
+
<button type="submit" disabled={isLoading || !input.trim()}
|
|
867
|
+
style={{ padding: "0.75rem 1.5rem", borderRadius: "0.5rem", border: "none", background: isLoading || !input.trim() ? "#333" : "#2563eb", color: "#fff", cursor: isLoading ? "wait" : "pointer", fontSize: "0.95rem" }}>
|
|
868
|
+
{isLoading ? "..." : "Send"}
|
|
869
|
+
</button>
|
|
870
|
+
</form>
|
|
871
|
+
</footer>
|
|
872
|
+
</div>
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
`;
|
|
876
|
+
}
|
|
877
|
+
function generateFaviconSVG() {
|
|
878
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
|
879
|
+
<rect width="32" height="32" rx="6" fill="#0a0a0a"/>
|
|
880
|
+
<path d="M18.5 4L8 18h7l-1.5 10L24 14h-7l1.5-10z" fill="#facc15" stroke="#facc15" stroke-width="0.5" stroke-linejoin="round"/>
|
|
881
|
+
</svg>
|
|
882
|
+
`;
|
|
883
|
+
}
|
|
884
|
+
function generateRobotsTxt() {
|
|
885
|
+
return `# https://www.robotstxt.org/robotstxt.html
|
|
886
|
+
User-agent: *
|
|
887
|
+
Allow: /
|
|
888
|
+
|
|
889
|
+
Sitemap: /sitemap.xml
|
|
890
|
+
`;
|
|
891
|
+
}
|
|
892
|
+
function generateWebManifest(projectName) {
|
|
893
|
+
return JSON.stringify({
|
|
894
|
+
name: projectName,
|
|
895
|
+
short_name: projectName,
|
|
896
|
+
icons: [
|
|
897
|
+
{ src: "/favicon.svg", sizes: "any", type: "image/svg+xml" }
|
|
898
|
+
],
|
|
899
|
+
theme_color: "#0a0a0a",
|
|
900
|
+
background_color: "#0a0a0a",
|
|
901
|
+
display: "standalone"
|
|
902
|
+
}, null, 2) + "\n";
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export {
|
|
906
|
+
createProject
|
|
907
|
+
};
|