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 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 target = path.join(process.cwd(), "public", "volt.js");
241
- if (!fs.existsSync(target)) {
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("!")} An update is available for ${bold("public/volt.js")} (create-volt ${pkg.version}).`);
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
- fs.writeFileSync(target, latest);
256
- console.log(`\n${green("✔")} Updated ${bold("public/volt.js")} to the version in create-volt ${pkg.version}.`);
257
- console.log(` Review the change with ${cyan("git diff public/volt.js")}.\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "Scaffold a new Volt app — no-build, signals-based UI with Socket.io hot reload.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 keys (optional) — for the editor's assistant</summary>
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
- <label class="form-label small mb-1">Provider (AI_PROVIDER)</label>
256
- <select class="form-select mb-2" value=${() => state().aiProvider} onchange=${(e) => set({ aiProvider: e.target.value })}>
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
- ${field("API key stays server-side, written to .env", "aiKey", "sk-…")}
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.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>` : ""}
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 keys (optional) — for the editor's assistant</summary>
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
- <label class="form-label small mb-1">Provider (AI_PROVIDER)</label>
256
- <select class="form-select mb-2" value=${() => state().aiProvider} onchange=${(e) => set({ aiProvider: e.target.value })}>
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
- ${field("API key stays server-side, written to .env", "aiKey", "sk-…")}
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.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>` : ""}
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 keys (optional) — for the editor's assistant</summary>
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
- <label class="form-label small mb-1">Provider (AI_PROVIDER)</label>
256
- <select class="form-select mb-2" value=${() => state().aiProvider} onchange=${(e) => set({ aiProvider: e.target.value })}>
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
- ${field("API key stays server-side, written to .env", "aiKey", "sk-…")}
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.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>` : ""}
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 keys (optional) — for the editor's assistant</summary>
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
- <label class="form-label small mb-1">Provider (AI_PROVIDER)</label>
256
- <select class="form-select mb-2" value=${() => state().aiProvider} onchange=${(e) => set({ aiProvider: e.target.value })}>
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
- ${field("API key stays server-side, written to .env", "aiKey", "sk-…")}
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.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>` : ""}
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))}