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