create-volt 0.48.2 → 0.49.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,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.49.0] - 2026-06-29
8
+
9
+ ### Added
10
+ - **AI in the config editor.** The embedded RTEPro editor's AI button now works:
11
+ a new `/setup/ai` proxy injects the `.env` provider key server-side
12
+ (Anthropic / OpenAI / Gemini, BYO) and RTEPro is wired to it via aiProxy. The
13
+ key never reaches the browser. (Set ANTHROPIC_API_KEY etc. in the app's .env.)
14
+
15
+ ## [0.48.3] - 2026-06-29
16
+
17
+ ### Fixed
18
+ - **Unreadable text in the dark config.** The wizard never set Bootstrap's
19
+ `data-bs-theme`, so muted/secondary text (feature descriptions, hints) was
20
+ colored for a light background and vanished on the dark cards. It now tracks the
21
+ light/dark toggle, so all Bootstrap text is readable in both modes.
22
+
7
23
  ## [0.48.2] - 2026-06-29
8
24
 
9
25
  ### Changed
@@ -641,6 +657,8 @@ All notable changes to `create-volt` are documented here. The format follows
641
657
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
642
658
  and auto-detects npm / pnpm / yarn / bun for the install step.
643
659
 
660
+ [0.49.0]: https://github.com/MIR-2025/volt/releases/tag/v0.49.0
661
+ [0.48.3]: https://github.com/MIR-2025/volt/releases/tag/v0.48.3
644
662
  [0.48.2]: https://github.com/MIR-2025/volt/releases/tag/v0.48.2
645
663
  [0.48.1]: https://github.com/MIR-2025/volt/releases/tag/v0.48.1
646
664
  [0.48.0]: https://github.com/MIR-2025/volt/releases/tag/v0.48.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.48.2",
3
+ "version": "0.49.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": {
@@ -344,6 +344,47 @@ function startSetup() {
344
344
  }
345
345
  return;
346
346
  }
347
+ // --- AI proxy for the in-config editor (RTEPro). Injects the .env provider
348
+ // key server-side (never reaches the browser). BYO keys; RTEPro POSTs a
349
+ // provider-native body with a _provider field. ---
350
+ if (req.method === "POST" && p === "/setup/ai") {
351
+ let cbody = "";
352
+ req.on("data", (c) => (cbody += c));
353
+ req.on("end", async () => {
354
+ res.setHeader("Content-Type", "application/json");
355
+ try {
356
+ const env = readEnvFile();
357
+ const body = JSON.parse(cbody || "{}");
358
+ const provider = body._provider || env.AI_PROVIDER || "anthropic";
359
+ delete body._provider;
360
+ let url, headers;
361
+ if (provider === "anthropic") {
362
+ if (!env.ANTHROPIC_API_KEY) throw new Error("ANTHROPIC_API_KEY not set in .env");
363
+ url = "https://api.anthropic.com/v1/messages";
364
+ headers = { "x-api-key": env.ANTHROPIC_API_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json" };
365
+ } else if (provider === "openai") {
366
+ if (!env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY not set in .env");
367
+ url = "https://api.openai.com/v1/chat/completions";
368
+ headers = { authorization: "Bearer " + env.OPENAI_API_KEY, "content-type": "application/json" };
369
+ } else if (provider === "gemini") {
370
+ if (!env.GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set in .env");
371
+ const model = body.model || "gemini-2.0-flash";
372
+ delete body.model;
373
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${env.GEMINI_API_KEY}`;
374
+ headers = { "content-type": "application/json" };
375
+ } else throw new Error("unknown AI provider: " + provider);
376
+ const r = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
377
+ const text = await r.text();
378
+ res.statusCode = r.status;
379
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
380
+ res.end(text);
381
+ } catch (e) {
382
+ res.statusCode = 400;
383
+ res.end(JSON.stringify({ error: e.message }));
384
+ }
385
+ });
386
+ return;
387
+ }
347
388
  // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
348
389
  // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
349
390
  // config only, never in the running app. ---
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="en" data-bs-theme="dark">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -31,7 +31,7 @@
31
31
  (function () {
32
32
  const root = document.documentElement;
33
33
  const btn = document.getElementById("theme-toggle");
34
- const apply = (t) => { root.setAttribute("data-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
34
+ const apply = (t) => { root.setAttribute("data-theme", t); root.setAttribute("data-bs-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
35
35
  apply(localStorage.getItem("volt-setup-theme") || "dark");
36
36
  btn.addEventListener("click", () => {
37
37
  const next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
@@ -317,7 +317,7 @@ function parseDoc(raw) {
317
317
  return { title, body: fm ? raw.slice(fm[0].length) : raw, isHtml: /^format:\s*html\s*$/m.test(front) };
318
318
  }
319
319
  function mountEditor(doc) {
320
- ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…" });
320
+ ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…", aiProxy: "/setup/ai", aiProvider: state().aiProvider || "anthropic" });
321
321
  if (doc && doc.isHtml) ed.setHTML(doc.body || "");
322
322
  else ed.setMarkdown((doc && doc.body) || "");
323
323
  }
@@ -344,6 +344,47 @@ function startSetup() {
344
344
  }
345
345
  return;
346
346
  }
347
+ // --- AI proxy for the in-config editor (RTEPro). Injects the .env provider
348
+ // key server-side (never reaches the browser). BYO keys; RTEPro POSTs a
349
+ // provider-native body with a _provider field. ---
350
+ if (req.method === "POST" && p === "/setup/ai") {
351
+ let cbody = "";
352
+ req.on("data", (c) => (cbody += c));
353
+ req.on("end", async () => {
354
+ res.setHeader("Content-Type", "application/json");
355
+ try {
356
+ const env = readEnvFile();
357
+ const body = JSON.parse(cbody || "{}");
358
+ const provider = body._provider || env.AI_PROVIDER || "anthropic";
359
+ delete body._provider;
360
+ let url, headers;
361
+ if (provider === "anthropic") {
362
+ if (!env.ANTHROPIC_API_KEY) throw new Error("ANTHROPIC_API_KEY not set in .env");
363
+ url = "https://api.anthropic.com/v1/messages";
364
+ headers = { "x-api-key": env.ANTHROPIC_API_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json" };
365
+ } else if (provider === "openai") {
366
+ if (!env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY not set in .env");
367
+ url = "https://api.openai.com/v1/chat/completions";
368
+ headers = { authorization: "Bearer " + env.OPENAI_API_KEY, "content-type": "application/json" };
369
+ } else if (provider === "gemini") {
370
+ if (!env.GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set in .env");
371
+ const model = body.model || "gemini-2.0-flash";
372
+ delete body.model;
373
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${env.GEMINI_API_KEY}`;
374
+ headers = { "content-type": "application/json" };
375
+ } else throw new Error("unknown AI provider: " + provider);
376
+ const r = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
377
+ const text = await r.text();
378
+ res.statusCode = r.status;
379
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
380
+ res.end(text);
381
+ } catch (e) {
382
+ res.statusCode = 400;
383
+ res.end(JSON.stringify({ error: e.message }));
384
+ }
385
+ });
386
+ return;
387
+ }
347
388
  // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
348
389
  // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
349
390
  // config only, never in the running app. ---
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="en" data-bs-theme="dark">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -31,7 +31,7 @@
31
31
  (function () {
32
32
  const root = document.documentElement;
33
33
  const btn = document.getElementById("theme-toggle");
34
- const apply = (t) => { root.setAttribute("data-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
34
+ const apply = (t) => { root.setAttribute("data-theme", t); root.setAttribute("data-bs-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
35
35
  apply(localStorage.getItem("volt-setup-theme") || "dark");
36
36
  btn.addEventListener("click", () => {
37
37
  const next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
@@ -317,7 +317,7 @@ function parseDoc(raw) {
317
317
  return { title, body: fm ? raw.slice(fm[0].length) : raw, isHtml: /^format:\s*html\s*$/m.test(front) };
318
318
  }
319
319
  function mountEditor(doc) {
320
- ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…" });
320
+ ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…", aiProxy: "/setup/ai", aiProvider: state().aiProvider || "anthropic" });
321
321
  if (doc && doc.isHtml) ed.setHTML(doc.body || "");
322
322
  else ed.setMarkdown((doc && doc.body) || "");
323
323
  }
@@ -344,6 +344,47 @@ function startSetup() {
344
344
  }
345
345
  return;
346
346
  }
347
+ // --- AI proxy for the in-config editor (RTEPro). Injects the .env provider
348
+ // key server-side (never reaches the browser). BYO keys; RTEPro POSTs a
349
+ // provider-native body with a _provider field. ---
350
+ if (req.method === "POST" && p === "/setup/ai") {
351
+ let cbody = "";
352
+ req.on("data", (c) => (cbody += c));
353
+ req.on("end", async () => {
354
+ res.setHeader("Content-Type", "application/json");
355
+ try {
356
+ const env = readEnvFile();
357
+ const body = JSON.parse(cbody || "{}");
358
+ const provider = body._provider || env.AI_PROVIDER || "anthropic";
359
+ delete body._provider;
360
+ let url, headers;
361
+ if (provider === "anthropic") {
362
+ if (!env.ANTHROPIC_API_KEY) throw new Error("ANTHROPIC_API_KEY not set in .env");
363
+ url = "https://api.anthropic.com/v1/messages";
364
+ headers = { "x-api-key": env.ANTHROPIC_API_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json" };
365
+ } else if (provider === "openai") {
366
+ if (!env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY not set in .env");
367
+ url = "https://api.openai.com/v1/chat/completions";
368
+ headers = { authorization: "Bearer " + env.OPENAI_API_KEY, "content-type": "application/json" };
369
+ } else if (provider === "gemini") {
370
+ if (!env.GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set in .env");
371
+ const model = body.model || "gemini-2.0-flash";
372
+ delete body.model;
373
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${env.GEMINI_API_KEY}`;
374
+ headers = { "content-type": "application/json" };
375
+ } else throw new Error("unknown AI provider: " + provider);
376
+ const r = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
377
+ const text = await r.text();
378
+ res.statusCode = r.status;
379
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
380
+ res.end(text);
381
+ } catch (e) {
382
+ res.statusCode = 400;
383
+ res.end(JSON.stringify({ error: e.message }));
384
+ }
385
+ });
386
+ return;
387
+ }
347
388
  // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
348
389
  // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
349
390
  // config only, never in the running app. ---
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="en" data-bs-theme="dark">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -31,7 +31,7 @@
31
31
  (function () {
32
32
  const root = document.documentElement;
33
33
  const btn = document.getElementById("theme-toggle");
34
- const apply = (t) => { root.setAttribute("data-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
34
+ const apply = (t) => { root.setAttribute("data-theme", t); root.setAttribute("data-bs-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
35
35
  apply(localStorage.getItem("volt-setup-theme") || "dark");
36
36
  btn.addEventListener("click", () => {
37
37
  const next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
@@ -317,7 +317,7 @@ function parseDoc(raw) {
317
317
  return { title, body: fm ? raw.slice(fm[0].length) : raw, isHtml: /^format:\s*html\s*$/m.test(front) };
318
318
  }
319
319
  function mountEditor(doc) {
320
- ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…" });
320
+ ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…", aiProxy: "/setup/ai", aiProvider: state().aiProvider || "anthropic" });
321
321
  if (doc && doc.isHtml) ed.setHTML(doc.body || "");
322
322
  else ed.setMarkdown((doc && doc.body) || "");
323
323
  }
@@ -370,6 +370,47 @@ function startSetup() {
370
370
  }
371
371
  return;
372
372
  }
373
+ // --- AI proxy for the in-config editor (RTEPro). Injects the .env provider
374
+ // key server-side (never reaches the browser). BYO keys; RTEPro POSTs a
375
+ // provider-native body with a _provider field. ---
376
+ if (req.method === "POST" && p === "/setup/ai") {
377
+ let cbody = "";
378
+ req.on("data", (c) => (cbody += c));
379
+ req.on("end", async () => {
380
+ res.setHeader("Content-Type", "application/json");
381
+ try {
382
+ const env = readEnvFile();
383
+ const body = JSON.parse(cbody || "{}");
384
+ const provider = body._provider || env.AI_PROVIDER || "anthropic";
385
+ delete body._provider;
386
+ let url, headers;
387
+ if (provider === "anthropic") {
388
+ if (!env.ANTHROPIC_API_KEY) throw new Error("ANTHROPIC_API_KEY not set in .env");
389
+ url = "https://api.anthropic.com/v1/messages";
390
+ headers = { "x-api-key": env.ANTHROPIC_API_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json" };
391
+ } else if (provider === "openai") {
392
+ if (!env.OPENAI_API_KEY) throw new Error("OPENAI_API_KEY not set in .env");
393
+ url = "https://api.openai.com/v1/chat/completions";
394
+ headers = { authorization: "Bearer " + env.OPENAI_API_KEY, "content-type": "application/json" };
395
+ } else if (provider === "gemini") {
396
+ if (!env.GEMINI_API_KEY) throw new Error("GEMINI_API_KEY not set in .env");
397
+ const model = body.model || "gemini-2.0-flash";
398
+ delete body.model;
399
+ url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${env.GEMINI_API_KEY}`;
400
+ headers = { "content-type": "application/json" };
401
+ } else throw new Error("unknown AI provider: " + provider);
402
+ const r = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
403
+ const text = await r.text();
404
+ res.statusCode = r.status;
405
+ res.setHeader("Content-Type", r.headers.get("content-type") || "application/json");
406
+ res.end(text);
407
+ } catch (e) {
408
+ res.statusCode = 400;
409
+ res.end(JSON.stringify({ error: e.message }));
410
+ }
411
+ });
412
+ return;
413
+ }
373
414
  // --- AI credits: in-config purchase flow. Proxies the hosted gateway with
374
415
  // the app's VOLT_AI_TOKEN — the buy flow lives here in the (shell-gated)
375
416
  // config only, never in the running app. ---
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="en">
2
+ <html lang="en" data-bs-theme="dark">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -31,7 +31,7 @@
31
31
  (function () {
32
32
  const root = document.documentElement;
33
33
  const btn = document.getElementById("theme-toggle");
34
- const apply = (t) => { root.setAttribute("data-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
34
+ const apply = (t) => { root.setAttribute("data-theme", t); root.setAttribute("data-bs-theme", t); btn.textContent = t === "light" ? "Dark mode" : "Light mode"; };
35
35
  apply(localStorage.getItem("volt-setup-theme") || "dark");
36
36
  btn.addEventListener("click", () => {
37
37
  const next = root.getAttribute("data-theme") === "light" ? "dark" : "light";
@@ -317,7 +317,7 @@ function parseDoc(raw) {
317
317
  return { title, body: fm ? raw.slice(fm[0].length) : raw, isHtml: /^format:\s*html\s*$/m.test(front) };
318
318
  }
319
319
  function mountEditor(doc) {
320
- ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…" });
320
+ ed = window.RTEPro.init("#mg-editor", { height: "60vh", placeholder: "Write…", aiProxy: "/setup/ai", aiProvider: state().aiProvider || "anthropic" });
321
321
  if (doc && doc.isHtml) ed.setHTML(doc.body || "");
322
322
  else ed.setMarkdown((doc && doc.body) || "");
323
323
  }