create-volt 0.44.0 → 0.45.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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/templates/blog/server.js +36 -0
- package/templates/blog/setup/setup.js +16 -0
- package/templates/default/server.js +36 -0
- package/templates/default/setup/setup.js +16 -0
- package/templates/docs/server.js +36 -0
- package/templates/docs/setup/setup.js +16 -0
- package/templates/starter/server.js +36 -0
- package/templates/starter/setup/setup.js +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ 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.0] - 2026-06-29
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Buy AI credits from the config wizard.** When an app uses the hosted gateway
|
|
11
|
+
(`VOLT_AI_TOKEN` set), the `--edit` wizard shows an **AI credits** card — live
|
|
12
|
+
balance + tier, and top-up buttons that open Stripe Checkout. The purchase flow
|
|
13
|
+
lives in the (shell-gated) config only; the running app never exposes it.
|
|
14
|
+
Proxied via `/setup/ai-credits` + `/setup/ai-credits/checkout` to the gateway.
|
|
15
|
+
|
|
7
16
|
## [0.44.0] - 2026-06-29
|
|
8
17
|
|
|
9
18
|
### Added
|
|
@@ -584,6 +593,7 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
584
593
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
585
594
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
586
595
|
|
|
596
|
+
[0.45.0]: https://github.com/MIR-2025/volt/releases/tag/v0.45.0
|
|
587
597
|
[0.44.0]: https://github.com/MIR-2025/volt/releases/tag/v0.44.0
|
|
588
598
|
[0.43.0]: https://github.com/MIR-2025/volt/releases/tag/v0.43.0
|
|
589
599
|
[0.42.0]: https://github.com/MIR-2025/volt/releases/tag/v0.42.0
|
package/package.json
CHANGED
package/templates/blog/server.js
CHANGED
|
@@ -341,6 +341,42 @@ function startSetup() {
|
|
|
341
341
|
}
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
344
|
+
// --- AI credits: in-config purchase flow. Proxies the hosted gateway with
|
|
345
|
+
// the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
|
|
346
|
+
// config only, never in the running app. ---
|
|
347
|
+
if (req.method === "GET" && p === "/setup/ai-credits") {
|
|
348
|
+
const env = readEnvFile();
|
|
349
|
+
res.setHeader("Content-Type", "application/json");
|
|
350
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
|
|
351
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
352
|
+
fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
|
|
353
|
+
.then((r) => r.json())
|
|
354
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
355
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
|
|
359
|
+
let cbody = "";
|
|
360
|
+
req.on("data", (c) => (cbody += c));
|
|
361
|
+
req.on("end", () => {
|
|
362
|
+
const env = readEnvFile();
|
|
363
|
+
res.setHeader("Content-Type", "application/json");
|
|
364
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
|
|
365
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
366
|
+
let amountUsd = 0;
|
|
367
|
+
try {
|
|
368
|
+
amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
|
|
369
|
+
} catch {
|
|
370
|
+
/* bad json */
|
|
371
|
+
}
|
|
372
|
+
const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
|
|
373
|
+
fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
|
|
374
|
+
.then((r) => r.json())
|
|
375
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
376
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
344
380
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
345
381
|
if (req.method === "GET" && p === "/setup/content") {
|
|
346
382
|
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>
|
|
@@ -341,6 +341,42 @@ function startSetup() {
|
|
|
341
341
|
}
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
344
|
+
// --- AI credits: in-config purchase flow. Proxies the hosted gateway with
|
|
345
|
+
// the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
|
|
346
|
+
// config only, never in the running app. ---
|
|
347
|
+
if (req.method === "GET" && p === "/setup/ai-credits") {
|
|
348
|
+
const env = readEnvFile();
|
|
349
|
+
res.setHeader("Content-Type", "application/json");
|
|
350
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
|
|
351
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
352
|
+
fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
|
|
353
|
+
.then((r) => r.json())
|
|
354
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
355
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
|
|
359
|
+
let cbody = "";
|
|
360
|
+
req.on("data", (c) => (cbody += c));
|
|
361
|
+
req.on("end", () => {
|
|
362
|
+
const env = readEnvFile();
|
|
363
|
+
res.setHeader("Content-Type", "application/json");
|
|
364
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
|
|
365
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
366
|
+
let amountUsd = 0;
|
|
367
|
+
try {
|
|
368
|
+
amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
|
|
369
|
+
} catch {
|
|
370
|
+
/* bad json */
|
|
371
|
+
}
|
|
372
|
+
const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
|
|
373
|
+
fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
|
|
374
|
+
.then((r) => r.json())
|
|
375
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
376
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
344
380
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
345
381
|
if (req.method === "GET" && p === "/setup/content") {
|
|
346
382
|
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
|
@@ -341,6 +341,42 @@ function startSetup() {
|
|
|
341
341
|
}
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
344
|
+
// --- AI credits: in-config purchase flow. Proxies the hosted gateway with
|
|
345
|
+
// the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
|
|
346
|
+
// config only, never in the running app. ---
|
|
347
|
+
if (req.method === "GET" && p === "/setup/ai-credits") {
|
|
348
|
+
const env = readEnvFile();
|
|
349
|
+
res.setHeader("Content-Type", "application/json");
|
|
350
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
|
|
351
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
352
|
+
fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
|
|
353
|
+
.then((r) => r.json())
|
|
354
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
355
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
|
|
359
|
+
let cbody = "";
|
|
360
|
+
req.on("data", (c) => (cbody += c));
|
|
361
|
+
req.on("end", () => {
|
|
362
|
+
const env = readEnvFile();
|
|
363
|
+
res.setHeader("Content-Type", "application/json");
|
|
364
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
|
|
365
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
366
|
+
let amountUsd = 0;
|
|
367
|
+
try {
|
|
368
|
+
amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
|
|
369
|
+
} catch {
|
|
370
|
+
/* bad json */
|
|
371
|
+
}
|
|
372
|
+
const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
|
|
373
|
+
fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
|
|
374
|
+
.then((r) => r.json())
|
|
375
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
376
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
344
380
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
345
381
|
if (req.method === "GET" && p === "/setup/content") {
|
|
346
382
|
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>
|
|
@@ -367,6 +367,42 @@ function startSetup() {
|
|
|
367
367
|
}
|
|
368
368
|
return;
|
|
369
369
|
}
|
|
370
|
+
// --- AI credits: in-config purchase flow. Proxies the hosted gateway with
|
|
371
|
+
// the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
|
|
372
|
+
// config only, never in the running app. ---
|
|
373
|
+
if (req.method === "GET" && p === "/setup/ai-credits") {
|
|
374
|
+
const env = readEnvFile();
|
|
375
|
+
res.setHeader("Content-Type", "application/json");
|
|
376
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN (BYO or unset)" }));
|
|
377
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
378
|
+
fetch(base + "/api/credits", { headers: { authorization: "Bearer " + env.VOLT_AI_TOKEN } })
|
|
379
|
+
.then((r) => r.json())
|
|
380
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
381
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (req.method === "POST" && p === "/setup/ai-credits/checkout") {
|
|
385
|
+
let cbody = "";
|
|
386
|
+
req.on("data", (c) => (cbody += c));
|
|
387
|
+
req.on("end", () => {
|
|
388
|
+
const env = readEnvFile();
|
|
389
|
+
res.setHeader("Content-Type", "application/json");
|
|
390
|
+
if (!env.VOLT_AI_TOKEN) return res.end(JSON.stringify({ ok: false, error: "no VOLT_AI_TOKEN" }));
|
|
391
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
392
|
+
let amountUsd = 0;
|
|
393
|
+
try {
|
|
394
|
+
amountUsd = Number(JSON.parse(cbody || "{}").amountUsd) || 0;
|
|
395
|
+
} catch {
|
|
396
|
+
/* bad json */
|
|
397
|
+
}
|
|
398
|
+
const baseUrl = env.SITE_URL || `http://localhost:${configPort()}`;
|
|
399
|
+
fetch(base + "/api/credits/checkout", { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer " + env.VOLT_AI_TOKEN }, body: JSON.stringify({ amountUsd, baseUrl }) })
|
|
400
|
+
.then((r) => r.json())
|
|
401
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
402
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
370
406
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
371
407
|
if (req.method === "GET" && p === "/setup/content") {
|
|
372
408
|
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>
|