create-volt 0.42.0 → 0.43.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 +14 -0
- package/index.js +30 -12
- package/package.json +1 -1
- package/templates/blog/server.js +28 -0
- package/templates/blog/setup/setup.js +26 -5
- package/templates/default/server.js +28 -0
- package/templates/default/setup/setup.js +26 -5
- package/templates/docs/server.js +28 -0
- package/templates/docs/setup/setup.js +26 -5
- package/templates/starter/server.js +28 -0
- package/templates/starter/setup/setup.js +26 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.43.0] - 2026-06-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Upgrade from the wizard.** Scaffolds record their version in `.volt/version`;
|
|
11
|
+
the `--edit` wizard checks npm and shows an "create-volt X available" notice with
|
|
12
|
+
a one-click **Upgrade** button (runs `npx create-volt@latest update`).
|
|
13
|
+
- **`update` refreshes everything framework-owned** — vendored runtime
|
|
14
|
+
(`volt.js`, `volt-ssr.js`), the setup wizard, and bundled add-ons + themes (was
|
|
15
|
+
just `volt.js`). Your `server.js` + content are left untouched.
|
|
16
|
+
- **Simpler AI setup.** The wizard AI section is clearly optional with a
|
|
17
|
+
per-provider **Get a key →** link (Anthropic / OpenAI / Gemini); leave it blank
|
|
18
|
+
and the editor works without AI.
|
|
19
|
+
|
|
7
20
|
## [0.42.0] - 2026-06-30
|
|
8
21
|
|
|
9
22
|
### Added
|
|
@@ -561,6 +574,7 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
561
574
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
562
575
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
563
576
|
|
|
577
|
+
[0.43.0]: https://github.com/MIR-2025/volt/releases/tag/v0.43.0
|
|
564
578
|
[0.42.0]: https://github.com/MIR-2025/volt/releases/tag/v0.42.0
|
|
565
579
|
[0.41.0]: https://github.com/MIR-2025/volt/releases/tag/v0.41.0
|
|
566
580
|
[0.40.0]: https://github.com/MIR-2025/volt/releases/tag/v0.40.0
|
package/index.js
CHANGED
|
@@ -237,24 +237,41 @@ if (positionals[0] === "create-theme") {
|
|
|
237
237
|
// pulls the latest library). Only touches the library file — never the user's
|
|
238
238
|
// app.js, server.js, or port. ---
|
|
239
239
|
if (positionals[0] === "update") {
|
|
240
|
-
const
|
|
241
|
-
if (!fs.existsSync(
|
|
240
|
+
const cwd = process.cwd();
|
|
241
|
+
if (!fs.existsSync(path.join(cwd, "public", "volt.js"))) {
|
|
242
242
|
die(`No ${cyan("public/volt.js")} here — run ${cyan("create-volt update")} from inside a Volt app.`);
|
|
243
243
|
}
|
|
244
|
-
const latest = fs.readFileSync(path.join(__dirname, "templates", "default", "public", "volt.js"), "utf8");
|
|
245
|
-
const current = fs.readFileSync(target, "utf8");
|
|
246
|
-
if (current === latest) {
|
|
247
|
-
console.log(`\n${green("✔")} ${bold("public/volt.js")} is already current (create-volt ${pkg.version}).\n`);
|
|
248
|
-
process.exit(0);
|
|
249
|
-
}
|
|
250
244
|
if (dryRun) {
|
|
251
|
-
console.log(`\n${yellow("!")}
|
|
245
|
+
console.log(`\n${yellow("!")} Would refresh the vendored runtime + bundled add-ons/themes to create-volt ${pkg.version}.`);
|
|
252
246
|
console.log(` Re-run without ${cyan("--dry-run")} to apply.\n`);
|
|
253
247
|
process.exit(0);
|
|
254
248
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
249
|
+
// Refresh the framework-owned files (not your server.js / content): the vendored
|
|
250
|
+
// runtime, the setup wizard, and the bundled add-ons + themes.
|
|
251
|
+
const T = path.join(__dirname, "templates", "default");
|
|
252
|
+
const done = [];
|
|
253
|
+
const copyFile = (rel, src) => {
|
|
254
|
+
if (fs.existsSync(src) && fs.existsSync(path.dirname(path.join(cwd, rel)))) {
|
|
255
|
+
fs.copyFileSync(src, path.join(cwd, rel));
|
|
256
|
+
done.push(rel);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
copyFile("public/volt.js", path.join(T, "public", "volt.js"));
|
|
260
|
+
copyFile("public/volt-ssr.js", path.join(T, "public", "volt-ssr.js"));
|
|
261
|
+
if (fs.existsSync(path.join(cwd, "setup"))) {
|
|
262
|
+
fs.cpSync(path.join(T, "setup"), path.join(cwd, "setup"), { recursive: true });
|
|
263
|
+
done.push("setup/ (wizard)");
|
|
264
|
+
}
|
|
265
|
+
if (fs.existsSync(path.join(cwd, ".volt"))) {
|
|
266
|
+
for (const d of ["addons", "themes"]) {
|
|
267
|
+
fs.cpSync(path.join(__dirname, d), path.join(cwd, ".volt", d), { recursive: true });
|
|
268
|
+
done.push(".volt/" + d);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
fs.mkdirSync(path.join(cwd, ".volt"), { recursive: true });
|
|
272
|
+
fs.writeFileSync(path.join(cwd, ".volt", "version"), pkg.version + "\n");
|
|
273
|
+
console.log(`\n${green("✔")} Updated to create-volt ${pkg.version}: ${done.join(", ")}.`);
|
|
274
|
+
console.log(dim(` Your server.js + content are untouched (re-scaffold to adopt entry-point changes). Restart the app.`));
|
|
258
275
|
process.exit(0);
|
|
259
276
|
}
|
|
260
277
|
|
|
@@ -471,6 +488,7 @@ if (fs.existsSync(path.join(targetDir, "setup"))) {
|
|
|
471
488
|
const src = path.join(__dirname, name);
|
|
472
489
|
if (fs.existsSync(src)) fs.cpSync(src, path.join(targetDir, ".volt", name), { recursive: true });
|
|
473
490
|
}
|
|
491
|
+
fs.writeFileSync(path.join(targetDir, ".volt", "version"), pkg.version + "\n"); // for the wizard's upgrade check
|
|
474
492
|
}
|
|
475
493
|
|
|
476
494
|
// --- stamp the project name into package.json ---
|
package/package.json
CHANGED
package/templates/blog/server.js
CHANGED
|
@@ -312,6 +312,34 @@ function startSetup() {
|
|
|
312
312
|
res.setHeader("Content-Type", "application/json");
|
|
313
313
|
return res.end(JSON.stringify({ available: availableAddons(), themes: availableThemes(), current: readEnvFile(), defaultPort: DEFAULT_PORT, configDefaultPort: CONFIG_DEFAULT_PORT }));
|
|
314
314
|
}
|
|
315
|
+
// --- upgrade: compare .volt/version to npm latest, and run the update ---
|
|
316
|
+
if (req.method === "GET" && p === "/setup/upgrade-check") {
|
|
317
|
+
const vf = path.join(__dirname, ".volt", "version");
|
|
318
|
+
const current = (fs.existsSync(vf) ? fs.readFileSync(vf, "utf8").trim() : "") || "?";
|
|
319
|
+
fetch("https://registry.npmjs.org/create-volt/latest")
|
|
320
|
+
.then((r) => r.json())
|
|
321
|
+
.then((j) => {
|
|
322
|
+
const latest = j.version || "?";
|
|
323
|
+
res.setHeader("Content-Type", "application/json");
|
|
324
|
+
res.end(JSON.stringify({ current, latest, available: latest !== "?" && current !== "?" && latest !== current }));
|
|
325
|
+
})
|
|
326
|
+
.catch(() => {
|
|
327
|
+
res.setHeader("Content-Type", "application/json");
|
|
328
|
+
res.end(JSON.stringify({ current, latest: "?", available: false }));
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (req.method === "POST" && p === "/setup/upgrade") {
|
|
333
|
+
res.setHeader("Content-Type", "application/json");
|
|
334
|
+
try {
|
|
335
|
+
const r = spawnSync("npx", ["--yes", "create-volt@latest", "update"], { cwd: __dirname, encoding: "utf8", shell: process.platform === "win32" });
|
|
336
|
+
res.end(JSON.stringify({ ok: r.status === 0, output: ((r.stdout || "") + (r.stderr || "")).slice(-2000) }));
|
|
337
|
+
} catch (e) {
|
|
338
|
+
res.statusCode = 500;
|
|
339
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
315
343
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
316
344
|
if (req.method === "GET" && p === "/setup/content") {
|
|
317
345
|
const list = (type) => {
|
|
@@ -249,21 +249,41 @@ const themePicker = () =>
|
|
|
249
249
|
</div>`;
|
|
250
250
|
|
|
251
251
|
// AI keys (optional) — used by the WYSIWYG editor's assistant. Kept server-side.
|
|
252
|
+
const AI_KEY_URL = {
|
|
253
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
254
|
+
openai: "https://platform.openai.com/api-keys",
|
|
255
|
+
gemini: "https://aistudio.google.com/app/apikey",
|
|
256
|
+
};
|
|
252
257
|
const aiSettings = () =>
|
|
253
|
-
html`<details class="mb-2"><summary class="form-label small mb-0" style="cursor:pointer">AI
|
|
258
|
+
html`<details class="mb-2"><summary class="form-label small mb-0" style="cursor:pointer">AI assistant for the editor (optional)</summary>
|
|
254
259
|
<div class="mt-2">
|
|
255
|
-
<
|
|
256
|
-
<
|
|
260
|
+
<p class="small text-muted mb-2">Powers the WYSIWYG editor's "write with AI" button. <strong>Totally optional</strong> — leave the key blank and the editor still works, just without AI.</p>
|
|
261
|
+
<label class="form-label small mb-1">Provider</label>
|
|
262
|
+
<select class="form-select mb-1" value=${() => state().aiProvider} onchange=${(e) => set({ aiProvider: e.target.value })}>
|
|
257
263
|
<option value="anthropic">Anthropic (Claude)</option>
|
|
258
264
|
<option value="openai">OpenAI</option>
|
|
259
265
|
<option value="gemini">Google Gemini</option>
|
|
260
266
|
</select>
|
|
261
|
-
${
|
|
267
|
+
${() => html`<a class="small d-inline-block mb-1" href=${AI_KEY_URL[state().aiProvider] || AI_KEY_URL.anthropic} target="_blank" rel="noopener">Get a ${state().aiProvider} key → paste it below (stays server-side in .env)</a>`}
|
|
268
|
+
${field("API key", "aiKey", "sk-…")}
|
|
262
269
|
</div>
|
|
263
270
|
</details>`;
|
|
264
271
|
|
|
265
272
|
// --- Manage content (a second screen reached via "Manage content →") ---
|
|
266
273
|
const view = signal("config"); // "config" | "manage"
|
|
274
|
+
// upgrade check: compare bundled version to npm latest; offer a one-click upgrade
|
|
275
|
+
const upgrade = signal(null); // { current, latest, available }
|
|
276
|
+
fetch("/setup/upgrade-check").then((r) => r.json()).then((u) => upgrade(u)).catch(() => {});
|
|
277
|
+
async function doUpgrade() {
|
|
278
|
+
status("Upgrading via npx create-volt@latest update…");
|
|
279
|
+
try {
|
|
280
|
+
const r = await (await fetch("/setup/upgrade", { method: "POST" })).json();
|
|
281
|
+
status(r.ok ? "Upgraded — restart the wizard/app to load the new version." : "Upgrade failed (see terminal).");
|
|
282
|
+
if (r.ok) upgrade({ ...upgrade(), available: false });
|
|
283
|
+
} catch {
|
|
284
|
+
status("Upgrade request failed.");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
267
287
|
const items = signal({ pages: [], posts: [] });
|
|
268
288
|
const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
|
|
269
289
|
const loadItems = async () => items(await (await fetch("/setup/content")).json());
|
|
@@ -318,7 +338,8 @@ const manageView = () =>
|
|
|
318
338
|
</div>`;
|
|
319
339
|
|
|
320
340
|
const configView = () =>
|
|
321
|
-
html`${available
|
|
341
|
+
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>` : "")}
|
|
342
|
+
${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>` : ""}
|
|
322
343
|
<div class="card-x p-4 mb-3">
|
|
323
344
|
<h2 class="h6 mb-3">Settings</h2>
|
|
324
345
|
${field("PORT", "port", String(defaultPort))}
|
|
@@ -312,6 +312,34 @@ function startSetup() {
|
|
|
312
312
|
res.setHeader("Content-Type", "application/json");
|
|
313
313
|
return res.end(JSON.stringify({ available: availableAddons(), themes: availableThemes(), current: readEnvFile(), defaultPort: DEFAULT_PORT, configDefaultPort: CONFIG_DEFAULT_PORT }));
|
|
314
314
|
}
|
|
315
|
+
// --- upgrade: compare .volt/version to npm latest, and run the update ---
|
|
316
|
+
if (req.method === "GET" && p === "/setup/upgrade-check") {
|
|
317
|
+
const vf = path.join(__dirname, ".volt", "version");
|
|
318
|
+
const current = (fs.existsSync(vf) ? fs.readFileSync(vf, "utf8").trim() : "") || "?";
|
|
319
|
+
fetch("https://registry.npmjs.org/create-volt/latest")
|
|
320
|
+
.then((r) => r.json())
|
|
321
|
+
.then((j) => {
|
|
322
|
+
const latest = j.version || "?";
|
|
323
|
+
res.setHeader("Content-Type", "application/json");
|
|
324
|
+
res.end(JSON.stringify({ current, latest, available: latest !== "?" && current !== "?" && latest !== current }));
|
|
325
|
+
})
|
|
326
|
+
.catch(() => {
|
|
327
|
+
res.setHeader("Content-Type", "application/json");
|
|
328
|
+
res.end(JSON.stringify({ current, latest: "?", available: false }));
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (req.method === "POST" && p === "/setup/upgrade") {
|
|
333
|
+
res.setHeader("Content-Type", "application/json");
|
|
334
|
+
try {
|
|
335
|
+
const r = spawnSync("npx", ["--yes", "create-volt@latest", "update"], { cwd: __dirname, encoding: "utf8", shell: process.platform === "win32" });
|
|
336
|
+
res.end(JSON.stringify({ ok: r.status === 0, output: ((r.stdout || "") + (r.stderr || "")).slice(-2000) }));
|
|
337
|
+
} catch (e) {
|
|
338
|
+
res.statusCode = 500;
|
|
339
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
315
343
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
316
344
|
if (req.method === "GET" && p === "/setup/content") {
|
|
317
345
|
const list = (type) => {
|
|
@@ -249,21 +249,41 @@ const themePicker = () =>
|
|
|
249
249
|
</div>`;
|
|
250
250
|
|
|
251
251
|
// AI keys (optional) — used by the WYSIWYG editor's assistant. Kept server-side.
|
|
252
|
+
const AI_KEY_URL = {
|
|
253
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
254
|
+
openai: "https://platform.openai.com/api-keys",
|
|
255
|
+
gemini: "https://aistudio.google.com/app/apikey",
|
|
256
|
+
};
|
|
252
257
|
const aiSettings = () =>
|
|
253
|
-
html`<details class="mb-2"><summary class="form-label small mb-0" style="cursor:pointer">AI
|
|
258
|
+
html`<details class="mb-2"><summary class="form-label small mb-0" style="cursor:pointer">AI assistant for the editor (optional)</summary>
|
|
254
259
|
<div class="mt-2">
|
|
255
|
-
<
|
|
256
|
-
<
|
|
260
|
+
<p class="small text-muted mb-2">Powers the WYSIWYG editor's "write with AI" button. <strong>Totally optional</strong> — leave the key blank and the editor still works, just without AI.</p>
|
|
261
|
+
<label class="form-label small mb-1">Provider</label>
|
|
262
|
+
<select class="form-select mb-1" value=${() => state().aiProvider} onchange=${(e) => set({ aiProvider: e.target.value })}>
|
|
257
263
|
<option value="anthropic">Anthropic (Claude)</option>
|
|
258
264
|
<option value="openai">OpenAI</option>
|
|
259
265
|
<option value="gemini">Google Gemini</option>
|
|
260
266
|
</select>
|
|
261
|
-
${
|
|
267
|
+
${() => html`<a class="small d-inline-block mb-1" href=${AI_KEY_URL[state().aiProvider] || AI_KEY_URL.anthropic} target="_blank" rel="noopener">Get a ${state().aiProvider} key → paste it below (stays server-side in .env)</a>`}
|
|
268
|
+
${field("API key", "aiKey", "sk-…")}
|
|
262
269
|
</div>
|
|
263
270
|
</details>`;
|
|
264
271
|
|
|
265
272
|
// --- Manage content (a second screen reached via "Manage content →") ---
|
|
266
273
|
const view = signal("config"); // "config" | "manage"
|
|
274
|
+
// upgrade check: compare bundled version to npm latest; offer a one-click upgrade
|
|
275
|
+
const upgrade = signal(null); // { current, latest, available }
|
|
276
|
+
fetch("/setup/upgrade-check").then((r) => r.json()).then((u) => upgrade(u)).catch(() => {});
|
|
277
|
+
async function doUpgrade() {
|
|
278
|
+
status("Upgrading via npx create-volt@latest update…");
|
|
279
|
+
try {
|
|
280
|
+
const r = await (await fetch("/setup/upgrade", { method: "POST" })).json();
|
|
281
|
+
status(r.ok ? "Upgraded — restart the wizard/app to load the new version." : "Upgrade failed (see terminal).");
|
|
282
|
+
if (r.ok) upgrade({ ...upgrade(), available: false });
|
|
283
|
+
} catch {
|
|
284
|
+
status("Upgrade request failed.");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
267
287
|
const items = signal({ pages: [], posts: [] });
|
|
268
288
|
const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
|
|
269
289
|
const loadItems = async () => items(await (await fetch("/setup/content")).json());
|
|
@@ -318,7 +338,8 @@ const manageView = () =>
|
|
|
318
338
|
</div>`;
|
|
319
339
|
|
|
320
340
|
const configView = () =>
|
|
321
|
-
html`${available
|
|
341
|
+
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>` : "")}
|
|
342
|
+
${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>` : ""}
|
|
322
343
|
<div class="card-x p-4 mb-3">
|
|
323
344
|
<h2 class="h6 mb-3">Settings</h2>
|
|
324
345
|
${field("PORT", "port", String(defaultPort))}
|
package/templates/docs/server.js
CHANGED
|
@@ -312,6 +312,34 @@ function startSetup() {
|
|
|
312
312
|
res.setHeader("Content-Type", "application/json");
|
|
313
313
|
return res.end(JSON.stringify({ available: availableAddons(), themes: availableThemes(), current: readEnvFile(), defaultPort: DEFAULT_PORT, configDefaultPort: CONFIG_DEFAULT_PORT }));
|
|
314
314
|
}
|
|
315
|
+
// --- upgrade: compare .volt/version to npm latest, and run the update ---
|
|
316
|
+
if (req.method === "GET" && p === "/setup/upgrade-check") {
|
|
317
|
+
const vf = path.join(__dirname, ".volt", "version");
|
|
318
|
+
const current = (fs.existsSync(vf) ? fs.readFileSync(vf, "utf8").trim() : "") || "?";
|
|
319
|
+
fetch("https://registry.npmjs.org/create-volt/latest")
|
|
320
|
+
.then((r) => r.json())
|
|
321
|
+
.then((j) => {
|
|
322
|
+
const latest = j.version || "?";
|
|
323
|
+
res.setHeader("Content-Type", "application/json");
|
|
324
|
+
res.end(JSON.stringify({ current, latest, available: latest !== "?" && current !== "?" && latest !== current }));
|
|
325
|
+
})
|
|
326
|
+
.catch(() => {
|
|
327
|
+
res.setHeader("Content-Type", "application/json");
|
|
328
|
+
res.end(JSON.stringify({ current, latest: "?", available: false }));
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (req.method === "POST" && p === "/setup/upgrade") {
|
|
333
|
+
res.setHeader("Content-Type", "application/json");
|
|
334
|
+
try {
|
|
335
|
+
const r = spawnSync("npx", ["--yes", "create-volt@latest", "update"], { cwd: __dirname, encoding: "utf8", shell: process.platform === "win32" });
|
|
336
|
+
res.end(JSON.stringify({ ok: r.status === 0, output: ((r.stdout || "") + (r.stderr || "")).slice(-2000) }));
|
|
337
|
+
} catch (e) {
|
|
338
|
+
res.statusCode = 500;
|
|
339
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
315
343
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
316
344
|
if (req.method === "GET" && p === "/setup/content") {
|
|
317
345
|
const list = (type) => {
|
|
@@ -249,21 +249,41 @@ const themePicker = () =>
|
|
|
249
249
|
</div>`;
|
|
250
250
|
|
|
251
251
|
// AI keys (optional) — used by the WYSIWYG editor's assistant. Kept server-side.
|
|
252
|
+
const AI_KEY_URL = {
|
|
253
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
254
|
+
openai: "https://platform.openai.com/api-keys",
|
|
255
|
+
gemini: "https://aistudio.google.com/app/apikey",
|
|
256
|
+
};
|
|
252
257
|
const aiSettings = () =>
|
|
253
|
-
html`<details class="mb-2"><summary class="form-label small mb-0" style="cursor:pointer">AI
|
|
258
|
+
html`<details class="mb-2"><summary class="form-label small mb-0" style="cursor:pointer">AI assistant for the editor (optional)</summary>
|
|
254
259
|
<div class="mt-2">
|
|
255
|
-
<
|
|
256
|
-
<
|
|
260
|
+
<p class="small text-muted mb-2">Powers the WYSIWYG editor's "write with AI" button. <strong>Totally optional</strong> — leave the key blank and the editor still works, just without AI.</p>
|
|
261
|
+
<label class="form-label small mb-1">Provider</label>
|
|
262
|
+
<select class="form-select mb-1" value=${() => state().aiProvider} onchange=${(e) => set({ aiProvider: e.target.value })}>
|
|
257
263
|
<option value="anthropic">Anthropic (Claude)</option>
|
|
258
264
|
<option value="openai">OpenAI</option>
|
|
259
265
|
<option value="gemini">Google Gemini</option>
|
|
260
266
|
</select>
|
|
261
|
-
${
|
|
267
|
+
${() => html`<a class="small d-inline-block mb-1" href=${AI_KEY_URL[state().aiProvider] || AI_KEY_URL.anthropic} target="_blank" rel="noopener">Get a ${state().aiProvider} key → paste it below (stays server-side in .env)</a>`}
|
|
268
|
+
${field("API key", "aiKey", "sk-…")}
|
|
262
269
|
</div>
|
|
263
270
|
</details>`;
|
|
264
271
|
|
|
265
272
|
// --- Manage content (a second screen reached via "Manage content →") ---
|
|
266
273
|
const view = signal("config"); // "config" | "manage"
|
|
274
|
+
// upgrade check: compare bundled version to npm latest; offer a one-click upgrade
|
|
275
|
+
const upgrade = signal(null); // { current, latest, available }
|
|
276
|
+
fetch("/setup/upgrade-check").then((r) => r.json()).then((u) => upgrade(u)).catch(() => {});
|
|
277
|
+
async function doUpgrade() {
|
|
278
|
+
status("Upgrading via npx create-volt@latest update…");
|
|
279
|
+
try {
|
|
280
|
+
const r = await (await fetch("/setup/upgrade", { method: "POST" })).json();
|
|
281
|
+
status(r.ok ? "Upgraded — restart the wizard/app to load the new version." : "Upgrade failed (see terminal).");
|
|
282
|
+
if (r.ok) upgrade({ ...upgrade(), available: false });
|
|
283
|
+
} catch {
|
|
284
|
+
status("Upgrade request failed.");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
267
287
|
const items = signal({ pages: [], posts: [] });
|
|
268
288
|
const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
|
|
269
289
|
const loadItems = async () => items(await (await fetch("/setup/content")).json());
|
|
@@ -318,7 +338,8 @@ const manageView = () =>
|
|
|
318
338
|
</div>`;
|
|
319
339
|
|
|
320
340
|
const configView = () =>
|
|
321
|
-
html`${available
|
|
341
|
+
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>` : "")}
|
|
342
|
+
${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>` : ""}
|
|
322
343
|
<div class="card-x p-4 mb-3">
|
|
323
344
|
<h2 class="h6 mb-3">Settings</h2>
|
|
324
345
|
${field("PORT", "port", String(defaultPort))}
|
|
@@ -338,6 +338,34 @@ function startSetup() {
|
|
|
338
338
|
res.setHeader("Content-Type", "application/json");
|
|
339
339
|
return res.end(JSON.stringify({ available: availableAddons(), themes: availableThemes(), current: readEnvFile(), defaultPort: DEFAULT_PORT, configDefaultPort: CONFIG_DEFAULT_PORT }));
|
|
340
340
|
}
|
|
341
|
+
// --- upgrade: compare .volt/version to npm latest, and run the update ---
|
|
342
|
+
if (req.method === "GET" && p === "/setup/upgrade-check") {
|
|
343
|
+
const vf = path.join(__dirname, ".volt", "version");
|
|
344
|
+
const current = (fs.existsSync(vf) ? fs.readFileSync(vf, "utf8").trim() : "") || "?";
|
|
345
|
+
fetch("https://registry.npmjs.org/create-volt/latest")
|
|
346
|
+
.then((r) => r.json())
|
|
347
|
+
.then((j) => {
|
|
348
|
+
const latest = j.version || "?";
|
|
349
|
+
res.setHeader("Content-Type", "application/json");
|
|
350
|
+
res.end(JSON.stringify({ current, latest, available: latest !== "?" && current !== "?" && latest !== current }));
|
|
351
|
+
})
|
|
352
|
+
.catch(() => {
|
|
353
|
+
res.setHeader("Content-Type", "application/json");
|
|
354
|
+
res.end(JSON.stringify({ current, latest: "?", available: false }));
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (req.method === "POST" && p === "/setup/upgrade") {
|
|
359
|
+
res.setHeader("Content-Type", "application/json");
|
|
360
|
+
try {
|
|
361
|
+
const r = spawnSync("npx", ["--yes", "create-volt@latest", "update"], { cwd: __dirname, encoding: "utf8", shell: process.platform === "win32" });
|
|
362
|
+
res.end(JSON.stringify({ ok: r.status === 0, output: ((r.stdout || "") + (r.stderr || "")).slice(-2000) }));
|
|
363
|
+
} catch (e) {
|
|
364
|
+
res.statusCode = 500;
|
|
365
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
341
369
|
// --- content manager: list / read / write / delete pages + posts ---
|
|
342
370
|
if (req.method === "GET" && p === "/setup/content") {
|
|
343
371
|
const list = (type) => {
|
|
@@ -249,21 +249,41 @@ const themePicker = () =>
|
|
|
249
249
|
</div>`;
|
|
250
250
|
|
|
251
251
|
// AI keys (optional) — used by the WYSIWYG editor's assistant. Kept server-side.
|
|
252
|
+
const AI_KEY_URL = {
|
|
253
|
+
anthropic: "https://console.anthropic.com/settings/keys",
|
|
254
|
+
openai: "https://platform.openai.com/api-keys",
|
|
255
|
+
gemini: "https://aistudio.google.com/app/apikey",
|
|
256
|
+
};
|
|
252
257
|
const aiSettings = () =>
|
|
253
|
-
html`<details class="mb-2"><summary class="form-label small mb-0" style="cursor:pointer">AI
|
|
258
|
+
html`<details class="mb-2"><summary class="form-label small mb-0" style="cursor:pointer">AI assistant for the editor (optional)</summary>
|
|
254
259
|
<div class="mt-2">
|
|
255
|
-
<
|
|
256
|
-
<
|
|
260
|
+
<p class="small text-muted mb-2">Powers the WYSIWYG editor's "write with AI" button. <strong>Totally optional</strong> — leave the key blank and the editor still works, just without AI.</p>
|
|
261
|
+
<label class="form-label small mb-1">Provider</label>
|
|
262
|
+
<select class="form-select mb-1" value=${() => state().aiProvider} onchange=${(e) => set({ aiProvider: e.target.value })}>
|
|
257
263
|
<option value="anthropic">Anthropic (Claude)</option>
|
|
258
264
|
<option value="openai">OpenAI</option>
|
|
259
265
|
<option value="gemini">Google Gemini</option>
|
|
260
266
|
</select>
|
|
261
|
-
${
|
|
267
|
+
${() => html`<a class="small d-inline-block mb-1" href=${AI_KEY_URL[state().aiProvider] || AI_KEY_URL.anthropic} target="_blank" rel="noopener">Get a ${state().aiProvider} key → paste it below (stays server-side in .env)</a>`}
|
|
268
|
+
${field("API key", "aiKey", "sk-…")}
|
|
262
269
|
</div>
|
|
263
270
|
</details>`;
|
|
264
271
|
|
|
265
272
|
// --- Manage content (a second screen reached via "Manage content →") ---
|
|
266
273
|
const view = signal("config"); // "config" | "manage"
|
|
274
|
+
// upgrade check: compare bundled version to npm latest; offer a one-click upgrade
|
|
275
|
+
const upgrade = signal(null); // { current, latest, available }
|
|
276
|
+
fetch("/setup/upgrade-check").then((r) => r.json()).then((u) => upgrade(u)).catch(() => {});
|
|
277
|
+
async function doUpgrade() {
|
|
278
|
+
status("Upgrading via npx create-volt@latest update…");
|
|
279
|
+
try {
|
|
280
|
+
const r = await (await fetch("/setup/upgrade", { method: "POST" })).json();
|
|
281
|
+
status(r.ok ? "Upgraded — restart the wizard/app to load the new version." : "Upgrade failed (see terminal).");
|
|
282
|
+
if (r.ok) upgrade({ ...upgrade(), available: false });
|
|
283
|
+
} catch {
|
|
284
|
+
status("Upgrade request failed.");
|
|
285
|
+
}
|
|
286
|
+
}
|
|
267
287
|
const items = signal({ pages: [], posts: [] });
|
|
268
288
|
const editing = signal(null); // { type, slug, body, isNew } — set only on open/save/close, so typing doesn't re-render
|
|
269
289
|
const loadItems = async () => items(await (await fetch("/setup/content")).json());
|
|
@@ -318,7 +338,8 @@ const manageView = () =>
|
|
|
318
338
|
</div>`;
|
|
319
339
|
|
|
320
340
|
const configView = () =>
|
|
321
|
-
html`${available
|
|
341
|
+
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>` : "")}
|
|
342
|
+
${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>` : ""}
|
|
322
343
|
<div class="card-x p-4 mb-3">
|
|
323
344
|
<h2 class="h6 mb-3">Settings</h2>
|
|
324
345
|
${field("PORT", "port", String(defaultPort))}
|