create-volt 0.44.0 → 0.45.1
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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/templates/blog/server.js +40 -1
- package/templates/blog/setup/setup.js +16 -0
- package/templates/default/server.js +40 -1
- package/templates/default/setup/setup.js +16 -0
- package/templates/docs/server.js +40 -1
- package/templates/docs/setup/setup.js +16 -0
- package/templates/starter/server.js +40 -1
- package/templates/starter/setup/setup.js +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.45.1] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **.env inline comments.** `KEY=value # note` now parses to `value` (trailing
|
|
11
|
+
comment stripped); quoted values stay literal. Previously the comment became
|
|
12
|
+
part of the value.
|
|
13
|
+
|
|
14
|
+
## [0.45.0] - 2026-06-29
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Buy AI credits from the config wizard.** When an app uses the hosted gateway
|
|
18
|
+
(`VOLT_AI_TOKEN` set), the `--edit` wizard shows an **AI credits** card — live
|
|
19
|
+
balance + tier, and top-up buttons that open Stripe Checkout. The purchase flow
|
|
20
|
+
lives in the (shell-gated) config only; the running app never exposes it.
|
|
21
|
+
Proxied via `/setup/ai-credits` + `/setup/ai-credits/checkout` to the gateway.
|
|
22
|
+
|
|
7
23
|
## [0.44.0] - 2026-06-29
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -584,6 +600,8 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
584
600
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
585
601
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
586
602
|
|
|
603
|
+
[0.45.1]: https://github.com/MIR-2025/volt/releases/tag/v0.45.1
|
|
604
|
+
[0.45.0]: https://github.com/MIR-2025/volt/releases/tag/v0.45.0
|
|
587
605
|
[0.44.0]: https://github.com/MIR-2025/volt/releases/tag/v0.44.0
|
|
588
606
|
[0.43.0]: https://github.com/MIR-2025/volt/releases/tag/v0.43.0
|
|
589
607
|
[0.42.0]: https://github.com/MIR-2025/volt/releases/tag/v0.42.0
|
package/package.json
CHANGED
package/templates/blog/server.js
CHANGED
|
@@ -51,7 +51,10 @@ function readEnvFile() {
|
|
|
51
51
|
if (!fs.existsSync(ENV_PATH)) return out;
|
|
52
52
|
for (const line of fs.readFileSync(ENV_PATH, "utf8").split("\n")) {
|
|
53
53
|
const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$/);
|
|
54
|
-
if (m)
|
|
54
|
+
if (m) {
|
|
55
|
+
const v = m[2];
|
|
56
|
+
out[m[1]] = (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")) ? v.slice(1, -1) : v.replace(/(?:^|\s+)#.*$/, "");
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
59
|
return out;
|
|
57
60
|
}
|
|
@@ -341,6 +344,42 @@ function startSetup() {
|
|
|
341
344
|
}
|
|
342
345
|
return;
|
|
343
346
|
}
|
|
347
|
+
// --- AI credits: in-config purchase flow. Proxies the hosted gateway with
|
|
348
|
+
// the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
|
|
349
|
+
// config only, never in the running app. ---
|
|
350
|
+
if (req.method === "GET" && p === "/setup/ai-credits") {
|
|
351
|
+
const env = readEnvFile();
|
|
352
|
+
res.setHeader("Content-Type", "application/json");
|
|
353
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
|
|
354
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
355
|
+
fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
|
|
356
|
+
.then((r) => r.json())
|
|
357
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
358
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
|
|
362
|
+
let cbody = "";
|
|
363
|
+
req.on("data", (c) => (cbody += c));
|
|
364
|
+
req.on("end", () => {
|
|
365
|
+
const env = readEnvFile();
|
|
366
|
+
res.setHeader("Content-Type", "application/json");
|
|
367
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
|
|
368
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
369
|
+
let amountUsd = 0;
|
|
370
|
+
try {
|
|
371
|
+
amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
|
|
372
|
+
} catch {
|
|
373
|
+
/* bad json */
|
|
374
|
+
}
|
|
375
|
+
const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
|
|
376
|
+
fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
|
|
377
|
+
.then((r) => r.json())
|
|
378
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
379
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
344
383
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
345
384
|
if (req.method === "GET" && p === "/setup/content") {
|
|
346
385
|
const list = (type) => {
|
|
@@ -284,6 +284,21 @@ async function doUpgrade() {
|
|
|
284
284
|
status("Upgrade request failed.");
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
|
|
288
|
+
// AI credits — config-only purchase flow (gateway mode). Hidden unless a
|
|
289
|
+
// VOLT_AI_TOKEN is set and the gateway answers.
|
|
290
|
+
const aiCredits = signal(null); // { ok, tier, creditBalanceUsd, payments } | { ok:false }
|
|
291
|
+
fetch("/setup/ai-credits").then((r) => r.json()).then((c) => aiCredits(c)).catch(() => {});
|
|
292
|
+
async function buyCredits(amountUsd) {
|
|
293
|
+
status("Starting checkout…");
|
|
294
|
+
try {
|
|
295
|
+
const r = await (await fetch("/setup/ai-credits/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountUsd }) })).json();
|
|
296
|
+
if (r.ok && r.url) window.open(r.url, "_blank");
|
|
297
|
+
else status("Checkout failed: " + (r.error || "?"));
|
|
298
|
+
} catch {
|
|
299
|
+
status("Checkout request failed.");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
287
302
|
const items = signal({ pages: [], posts: [] });
|
|
288
303
|
const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
|
|
289
304
|
const loadItems = async () => items(await (await fetch("/setup/content")).json());
|
|
@@ -339,6 +354,7 @@ const manageView = () =>
|
|
|
339
354
|
|
|
340
355
|
const configView = () =>
|
|
341
356
|
html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
|
|
357
|
+
${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
|
|
342
358
|
${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
|
|
343
359
|
<div class="card-x p-4 mb-3">
|
|
344
360
|
<h2 class="h6 mb-3">Settings</h2>
|
|
@@ -51,7 +51,10 @@ function readEnvFile() {
|
|
|
51
51
|
if (!fs.existsSync(ENV_PATH)) return out;
|
|
52
52
|
for (const line of fs.readFileSync(ENV_PATH, "utf8").split("\n")) {
|
|
53
53
|
const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$/);
|
|
54
|
-
if (m)
|
|
54
|
+
if (m) {
|
|
55
|
+
const v = m[2];
|
|
56
|
+
out[m[1]] = (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")) ? v.slice(1, -1) : v.replace(/(?:^|\s+)#.*$/, "");
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
59
|
return out;
|
|
57
60
|
}
|
|
@@ -341,6 +344,42 @@ function startSetup() {
|
|
|
341
344
|
}
|
|
342
345
|
return;
|
|
343
346
|
}
|
|
347
|
+
// --- AI credits: in-config purchase flow. Proxies the hosted gateway with
|
|
348
|
+
// the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
|
|
349
|
+
// config only, never in the running app. ---
|
|
350
|
+
if (req.method === "GET" && p === "/setup/ai-credits") {
|
|
351
|
+
const env = readEnvFile();
|
|
352
|
+
res.setHeader("Content-Type", "application/json");
|
|
353
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
|
|
354
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
355
|
+
fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
|
|
356
|
+
.then((r) => r.json())
|
|
357
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
358
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
|
|
362
|
+
let cbody = "";
|
|
363
|
+
req.on("data", (c) => (cbody += c));
|
|
364
|
+
req.on("end", () => {
|
|
365
|
+
const env = readEnvFile();
|
|
366
|
+
res.setHeader("Content-Type", "application/json");
|
|
367
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
|
|
368
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
369
|
+
let amountUsd = 0;
|
|
370
|
+
try {
|
|
371
|
+
amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
|
|
372
|
+
} catch {
|
|
373
|
+
/* bad json */
|
|
374
|
+
}
|
|
375
|
+
const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
|
|
376
|
+
fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
|
|
377
|
+
.then((r) => r.json())
|
|
378
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
379
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
344
383
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
345
384
|
if (req.method === "GET" && p === "/setup/content") {
|
|
346
385
|
const list = (type) => {
|
|
@@ -284,6 +284,21 @@ async function doUpgrade() {
|
|
|
284
284
|
status("Upgrade request failed.");
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
|
|
288
|
+
// AI credits — config-only purchase flow (gateway mode). Hidden unless a
|
|
289
|
+
// VOLT_AI_TOKEN is set and the gateway answers.
|
|
290
|
+
const aiCredits = signal(null); // { ok, tier, creditBalanceUsd, payments } | { ok:false }
|
|
291
|
+
fetch("/setup/ai-credits").then((r) => r.json()).then((c) => aiCredits(c)).catch(() => {});
|
|
292
|
+
async function buyCredits(amountUsd) {
|
|
293
|
+
status("Starting checkout…");
|
|
294
|
+
try {
|
|
295
|
+
const r = await (await fetch("/setup/ai-credits/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountUsd }) })).json();
|
|
296
|
+
if (r.ok && r.url) window.open(r.url, "_blank");
|
|
297
|
+
else status("Checkout failed: " + (r.error || "?"));
|
|
298
|
+
} catch {
|
|
299
|
+
status("Checkout request failed.");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
287
302
|
const items = signal({ pages: [], posts: [] });
|
|
288
303
|
const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
|
|
289
304
|
const loadItems = async () => items(await (await fetch("/setup/content")).json());
|
|
@@ -339,6 +354,7 @@ const manageView = () =>
|
|
|
339
354
|
|
|
340
355
|
const configView = () =>
|
|
341
356
|
html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
|
|
357
|
+
${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
|
|
342
358
|
${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
|
|
343
359
|
<div class="card-x p-4 mb-3">
|
|
344
360
|
<h2 class="h6 mb-3">Settings</h2>
|
package/templates/docs/server.js
CHANGED
|
@@ -51,7 +51,10 @@ function readEnvFile() {
|
|
|
51
51
|
if (!fs.existsSync(ENV_PATH)) return out;
|
|
52
52
|
for (const line of fs.readFileSync(ENV_PATH, "utf8").split("\n")) {
|
|
53
53
|
const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$/);
|
|
54
|
-
if (m)
|
|
54
|
+
if (m) {
|
|
55
|
+
const v = m[2];
|
|
56
|
+
out[m[1]] = (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")) ? v.slice(1, -1) : v.replace(/(?:^|\s+)#.*$/, "");
|
|
57
|
+
}
|
|
55
58
|
}
|
|
56
59
|
return out;
|
|
57
60
|
}
|
|
@@ -341,6 +344,42 @@ function startSetup() {
|
|
|
341
344
|
}
|
|
342
345
|
return;
|
|
343
346
|
}
|
|
347
|
+
// --- AI credits: in-config purchase flow. Proxies the hosted gateway with
|
|
348
|
+
// the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
|
|
349
|
+
// config only, never in the running app. ---
|
|
350
|
+
if (req.method === "GET" && p === "/setup/ai-credits") {
|
|
351
|
+
const env = readEnvFile();
|
|
352
|
+
res.setHeader("Content-Type", "application/json");
|
|
353
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
|
|
354
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
355
|
+
fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
|
|
356
|
+
.then((r) => r.json())
|
|
357
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
358
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
|
|
362
|
+
let cbody = "";
|
|
363
|
+
req.on("data", (c) => (cbody += c));
|
|
364
|
+
req.on("end", () => {
|
|
365
|
+
const env = readEnvFile();
|
|
366
|
+
res.setHeader("Content-Type", "application/json");
|
|
367
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
|
|
368
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
369
|
+
let amountUsd = 0;
|
|
370
|
+
try {
|
|
371
|
+
amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
|
|
372
|
+
} catch {
|
|
373
|
+
/* bad json */
|
|
374
|
+
}
|
|
375
|
+
const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
|
|
376
|
+
fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
|
|
377
|
+
.then((r) => r.json())
|
|
378
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
379
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
344
383
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
345
384
|
if (req.method === "GET" && p === "/setup/content") {
|
|
346
385
|
const list = (type) => {
|
|
@@ -284,6 +284,21 @@ async function doUpgrade() {
|
|
|
284
284
|
status("Upgrade request failed.");
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
|
|
288
|
+
// AI credits — config-only purchase flow (gateway mode). Hidden unless a
|
|
289
|
+
// VOLT_AI_TOKEN is set and the gateway answers.
|
|
290
|
+
const aiCredits = signal(null); // { ok, tier, creditBalanceUsd, payments } | { ok:false }
|
|
291
|
+
fetch("/setup/ai-credits").then((r) => r.json()).then((c) => aiCredits(c)).catch(() => {});
|
|
292
|
+
async function buyCredits(amountUsd) {
|
|
293
|
+
status("Starting checkout…");
|
|
294
|
+
try {
|
|
295
|
+
const r = await (await fetch("/setup/ai-credits/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountUsd }) })).json();
|
|
296
|
+
if (r.ok && r.url) window.open(r.url, "_blank");
|
|
297
|
+
else status("Checkout failed: " + (r.error || "?"));
|
|
298
|
+
} catch {
|
|
299
|
+
status("Checkout request failed.");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
287
302
|
const items = signal({ pages: [], posts: [] });
|
|
288
303
|
const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
|
|
289
304
|
const loadItems = async () => items(await (await fetch("/setup/content")).json());
|
|
@@ -339,6 +354,7 @@ const manageView = () =>
|
|
|
339
354
|
|
|
340
355
|
const configView = () =>
|
|
341
356
|
html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
|
|
357
|
+
${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
|
|
342
358
|
${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
|
|
343
359
|
<div class="card-x p-4 mb-3">
|
|
344
360
|
<h2 class="h6 mb-3">Settings</h2>
|
|
@@ -52,7 +52,10 @@ function readEnvFile() {
|
|
|
52
52
|
if (!fs.existsSync(ENV_PATH)) return out;
|
|
53
53
|
for (const line of fs.readFileSync(ENV_PATH, "utf8").split("\n")) {
|
|
54
54
|
const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$/);
|
|
55
|
-
if (m)
|
|
55
|
+
if (m) {
|
|
56
|
+
const v = m[2];
|
|
57
|
+
out[m[1]] = (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")) ? v.slice(1, -1) : v.replace(/(?:^|\s+)#.*$/, "");
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
return out;
|
|
58
61
|
}
|
|
@@ -367,6 +370,42 @@ function startSetup() {
|
|
|
367
370
|
}
|
|
368
371
|
return;
|
|
369
372
|
}
|
|
373
|
+
// --- AI credits: in-config purchase flow. Proxies the hosted gateway with
|
|
374
|
+
// the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
|
|
375
|
+
// config only, never in the running app. ---
|
|
376
|
+
if (req.method === "GET" && p === "/setup/ai-credits") {
|
|
377
|
+
const env = readEnvFile();
|
|
378
|
+
res.setHeader("Content-Type", "application/json");
|
|
379
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
|
|
380
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
381
|
+
fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
|
|
382
|
+
.then((r) => r.json())
|
|
383
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
384
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
|
|
388
|
+
let cbody = "";
|
|
389
|
+
req.on("data", (c) => (cbody += c));
|
|
390
|
+
req.on("end", () => {
|
|
391
|
+
const env = readEnvFile();
|
|
392
|
+
res.setHeader("Content-Type", "application/json");
|
|
393
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
|
|
394
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
395
|
+
let amountUsd = 0;
|
|
396
|
+
try {
|
|
397
|
+
amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
|
|
398
|
+
} catch {
|
|
399
|
+
/* bad json */
|
|
400
|
+
}
|
|
401
|
+
const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
|
|
402
|
+
fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
|
|
403
|
+
.then((r) => r.json())
|
|
404
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
405
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
370
409
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
371
410
|
if (req.method === "GET" && p === "/setup/content") {
|
|
372
411
|
const list = (type) => {
|
|
@@ -284,6 +284,21 @@ async function doUpgrade() {
|
|
|
284
284
|
status("Upgrade request failed.");
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
|
|
288
|
+
// AI credits — config-only purchase flow (gateway mode). Hidden unless a
|
|
289
|
+
// VOLT_AI_TOKEN is set and the gateway answers.
|
|
290
|
+
const aiCredits = signal(null); // { ok, tier, creditBalanceUsd, payments } | { ok:false }
|
|
291
|
+
fetch("/setup/ai-credits").then((r) => r.json()).then((c) => aiCredits(c)).catch(() => {});
|
|
292
|
+
async function buyCredits(amountUsd) {
|
|
293
|
+
status("Starting checkout…");
|
|
294
|
+
try {
|
|
295
|
+
const r = await (await fetch("/setup/ai-credits/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amountUsd }) })).json();
|
|
296
|
+
if (r.ok && r.url) window.open(r.url, "_blank");
|
|
297
|
+
else status("Checkout failed: " + (r.error || "?"));
|
|
298
|
+
} catch {
|
|
299
|
+
status("Checkout request failed.");
|
|
300
|
+
}
|
|
301
|
+
}
|
|
287
302
|
const items = signal({ pages: [], posts: [] });
|
|
288
303
|
const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
|
|
289
304
|
const loadItems = async () => items(await (await fetch("/setup/content")).json());
|
|
@@ -339,6 +354,7 @@ const manageView = () =>
|
|
|
339
354
|
|
|
340
355
|
const configView = () =>
|
|
341
356
|
html`${() => (upgrade()?.available ? html`<div class="card-x p-3 mb-3 d-flex justify-content-between align-items-center"><span class="small">⬆ <strong>create-volt ${upgrade().latest}</strong> is available — you have ${upgrade().current}.</span><button class="btn btn-sm btn-primary" onclick=${doUpgrade}>Upgrade</button></div>` : "")}
|
|
357
|
+
${() => (aiCredits()?.ok ? html`<div class="card-x p-3 mb-3"><div class="d-flex justify-content-between align-items-center mb-2"><strong>AI credits</strong><span class="small text-muted">${aiCredits().tier}${typeof aiCredits().creditBalanceUsd === "number" ? ` · $${aiCredits().creditBalanceUsd.toFixed(2)} left` : ""}</span></div>${aiCredits().payments ? html`<div class="d-flex gap-2 align-items-center"><span class="small text-muted me-1">Top up:</span>${[10, 25, 50].map((a) => html`<button class="btn btn-sm btn-outline-primary" onclick=${() => buyCredits(a)}>$${a}</button>`)}</div>` : html`<div class="small text-muted">Pay-as-you-go isn't enabled on the gateway yet — using the free tier.</div>`}</div>` : "")}
|
|
342
358
|
${available.length ? html`<div class="card-x p-4 mb-3"><h2 class="h6 mb-3">Features</h2>${available.map(addonRow)}<p class="small text-muted mb-0">Enabling a feature wires its backend automatically. Frontend UI (login form, chat) is yours to build — or start from <code>--template guestbook</code>.</p></div>` : ""}
|
|
343
359
|
<div class="card-x p-4 mb-3">
|
|
344
360
|
<h2 class="h6 mb-3">Settings</h2>
|