create-volt 0.39.1 → 0.40.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,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.40.0] - 2026-06-30
8
+
9
+ ### Added
10
+ - **Themed front page.** `pages/index.md` now takes over `/` (rendered in your
11
+ theme), so a content site's home matches the rest of the site instead of
12
+ showing the demo `views/index.html`. With no `pages/index.md`, `/` stays the
13
+ app's index.html. (Answers: "I chose a theme but the home page was unchanged" —
14
+ the theme styles pages/posts; the home needs to *be* a page.)
15
+
7
16
  ## [0.39.1] - 2026-06-30
8
17
 
9
18
  ### Fixed
@@ -528,6 +537,7 @@ All notable changes to `create-volt` are documented here. The format follows
528
537
  watching and full-page hot reload. Supports `--skip-install` and `--force`,
529
538
  and auto-detects npm / pnpm / yarn / bun for the install step.
530
539
 
540
+ [0.40.0]: https://github.com/MIR-2025/volt/releases/tag/v0.40.0
531
541
  [0.39.1]: https://github.com/MIR-2025/volt/releases/tag/v0.39.1
532
542
  [0.39.0]: https://github.com/MIR-2025/volt/releases/tag/v0.39.0
533
543
  [0.38.0]: https://github.com/MIR-2025/volt/releases/tag/v0.38.0
@@ -176,20 +176,29 @@ export async function pagesRouter({ dir }) {
176
176
  const { marked } = await import("marked");
177
177
  ensure(dir);
178
178
  const getTheme = themeResolver(dir);
179
+ // render one markdown file into the theme. `format: html` pages (e.g. from the
180
+ // WYSIWYG editor) are served verbatim; everything else is rendered with marked.
181
+ const renderFile = async (file, fallbackTitle, res) => {
182
+ const { meta, body } = parseFrontMatter(fs.readFileSync(file, "utf8"));
183
+ const content = meta.format === "html" ? body : marked.parse(body);
184
+ const m = { ...meta, title: meta.title || fallbackTitle };
185
+ const { layout } = await getTheme();
186
+ res.type("html").send(injectHot(layout({ title: m.title, head: metaHead(m), content, meta: m })));
187
+ };
179
188
  const r = express.Router();
180
189
  r.get("/_theme.css", async (_req, res) => res.type("css").send((await getTheme()).css));
190
+ // themed home: pages/index.md takes over "/" (the site's front page) when present
191
+ r.get("/", async (_req, res, next) => {
192
+ const file = path.join(dir, "index.md");
193
+ if (!fs.existsSync(file)) return next();
194
+ await renderFile(file, "Home", res);
195
+ });
181
196
  r.get("/:slug", async (req, res, next) => {
182
197
  const slug = req.params.slug;
183
198
  if (!isSafeSlug(slug)) return next(); // safe slug only — no traversal
184
199
  const file = path.join(dir, slug + ".md");
185
200
  if (!fs.existsSync(file)) return next();
186
- const { meta, body } = parseFrontMatter(fs.readFileSync(file, "utf8"));
187
- // `format: html` pages (e.g. from the WYSIWYG editor) are served verbatim to
188
- // preserve complex layouts; everything else is markdown rendered with marked.
189
- const content = meta.format === "html" ? body : marked.parse(body);
190
- const m = { ...meta, title: meta.title || slug };
191
- const { layout } = await getTheme();
192
- res.type("html").send(injectHot(layout({ title: m.title, head: metaHead(m), content, meta: m })));
201
+ await renderFile(file, slug, res);
193
202
  });
194
203
  return r;
195
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.39.1",
3
+ "version": "0.40.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": {
@@ -1,9 +1,9 @@
1
- // server.js — dev server with a built-in first-run setup wizard.
1
+ // server.js — dev server with a built-in first-run setup wizard.
2
2
  //
3
3
  // First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
4
4
  // config page: tick add-ons, fill settings, Apply. Apply writes .env (a
5
5
  // VOLT_ADDONS list + settings) and adds any needed packages to package.json,
6
- // runs npm install, then starts the app — which wires whatever .env enables.
6
+ // runs npm install, then starts the app — which wires whatever .env enables.
7
7
  // Add-on code is bundled under .volt/addons; nothing is copied into your code.
8
8
  //
9
9
  // No build step, no env-file flag: .env is auto-loaded below.
@@ -24,7 +24,7 @@ const THEMES_DIR = path.join(__dirname, ".volt", "themes"); // bundled themes th
24
24
  const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
25
25
  const CONFIG_DEFAULT_PORT = 5050; // the --edit/--studio config UI's default port (its own, so it never clashes with a running app)
26
26
 
27
- // `--port <n>` (or --port=<n>) overrides the listen port for this run lets
27
+ // `--port <n>` (or --port=<n>) overrides the listen port for this run — lets
28
28
  // --edit/--studio dodge a port the running app already holds, and runs the app
29
29
  // itself on a one-off port. Explicit flag wins over PORT in .env.
30
30
  function cliPort() {
@@ -104,7 +104,7 @@ const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
104
104
  const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
105
105
 
106
106
  // Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
107
- // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
107
+ // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
108
108
  // npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
109
109
  const BUILTINS = new Set(Object.keys(LIB_FILE));
110
110
  async function loadAddon(name) {
@@ -128,7 +128,7 @@ function openBrowser(url) {
128
128
  const args = plat === "win32" ? ["/c", "start", "", url] : [url];
129
129
  try {
130
130
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
131
- child.on("error", () => {}); // launcher missing — emits async, don't crash
131
+ child.on("error", () => {}); // launcher missing — emits async, don't crash
132
132
  child.unref();
133
133
  return true;
134
134
  } catch {
@@ -163,7 +163,11 @@ async function startApp() {
163
163
  if (fs.existsSync(pub)) app.use(express.static(pub));
164
164
  }
165
165
 
166
- app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
166
+ app.get("/", (_req, res, next) => {
167
+ // a themed home page (pages/index.md) takes over "/" — else the app's index.html
168
+ if (enabled.has("pages") && fs.existsSync(path.join(__dirname, "pages", "index.md"))) return next();
169
+ res.sendFile(path.join(__dirname, "views", "index.html"));
170
+ });
167
171
 
168
172
  // media uploads (POST /api/media, auth-gated; local files served at /media)
169
173
  if (enabled.has("media") && store) {
@@ -171,8 +175,8 @@ async function startApp() {
171
175
  app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
172
176
  }
173
177
 
174
- // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
175
- // blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) before pages so /blog wins; renders in the same theme.
178
+ // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
179
+ // blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) — before pages so /blog wins; renders in the same theme.
176
180
  if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
177
181
  if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
178
182
 
@@ -180,7 +184,7 @@ async function startApp() {
180
184
  const io = new SocketServer(server);
181
185
  if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
182
186
 
183
- // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
187
+ // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
184
188
  // are provided so add-ons can gate routes by login.
185
189
  let requireAuth = null;
186
190
  let sessionFromReq = null;
@@ -196,7 +200,7 @@ async function startApp() {
196
200
  if (typeof register === "function") {
197
201
  await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
198
202
  } else {
199
- console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
203
+ console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
200
204
  }
201
205
  }
202
206
 
@@ -204,7 +208,7 @@ async function startApp() {
204
208
  const onChange = (file) => {
205
209
  clearTimeout(timer);
206
210
  timer = setTimeout(() => {
207
- console.log(`[volt] change: ${file ?? "?"} → reload`);
211
+ console.log(`[volt] change: ${file ?? "?"} → reload`);
208
212
  io.emit("volt:reload", { file });
209
213
  }, 80);
210
214
  };
@@ -233,7 +237,7 @@ async function startApp() {
233
237
  }
234
238
 
235
239
  const on = [...enabled];
236
- server.listen(PORT, () => console.log(`⚡ Volt → http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
240
+ server.listen(PORT, () => console.log(`⚡ Volt → http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
237
241
  }
238
242
 
239
243
  // Packages an .env's selections need, beyond what package.json already has.
@@ -257,7 +261,7 @@ function neededPackages(env) {
257
261
  function ensureDriverInstalled(driver) {
258
262
  const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
259
263
  if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
260
- console.log(`[volt] installing ${pkg} for the connection test…`);
264
+ console.log(`[volt] installing ${pkg} for the connection test…`);
261
265
  spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
262
266
  }
263
267
 
@@ -380,15 +384,15 @@ function startSetup() {
380
384
  server.closeIdleConnections?.();
381
385
  };
382
386
  if (added.length) {
383
- console.log(`[volt] installing ${added.join(", ")}…`);
387
+ console.log(`[volt] installing ${added.join(", ")}…`);
384
388
  const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
385
389
  npm.on("error", () => handoff());
386
390
  npm.on("close", () => {
387
- console.log("[volt] saved .env — starting the app…");
391
+ console.log("[volt] saved .env — starting the app…");
388
392
  handoff();
389
393
  });
390
394
  } else {
391
- console.log("[volt] saved .env — starting the app…");
395
+ console.log("[volt] saved .env — starting the app…");
392
396
  handoff();
393
397
  }
394
398
  });
@@ -406,18 +410,18 @@ function startSetup() {
406
410
  server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
407
411
  server.listen(PORT, "127.0.0.1", () => {
408
412
  const url = `http://localhost:${PORT}`;
409
- console.log(`\n⚡ Volt setup → ${url}`);
413
+ console.log(`\n⚡ Volt setup → ${url}`);
410
414
  console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
411
415
  const ssh = process.env.SSH_CONNECTION;
412
416
  if (ssh) {
413
417
  const host = ssh.split(" ")[2];
414
418
  const user = process.env.USER || process.env.USERNAME || "you";
415
- console.log(" Remote box — the server is up here; bridge it from your LOCAL machine:");
419
+ console.log(" Remote box — the server is up here; bridge it from your LOCAL machine:");
416
420
  console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
417
- console.log(` …then open ${url} on your machine (the tunnel points it here).`);
421
+ console.log(` …then open ${url} on your machine (the tunnel points it here).`);
418
422
  }
419
423
  console.log("");
420
- if (openBrowser(url)) console.log(" (opening your browser…)\n");
424
+ if (openBrowser(url)) console.log(" (opening your browser…)\n");
421
425
  });
422
426
  }
423
427
 
@@ -425,8 +429,8 @@ function readEnvFileLines() {
425
429
  return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
426
430
  }
427
431
 
428
- // --- Studio: an ephemeral, localhost-only data browser (à la Prisma Studio).
429
- // Not a route in the running app — it only exists while you run `--studio`, on
432
+ // --- Studio: an ephemeral, localhost-only data browser (à la Prisma Studio).
433
+ // Not a route in the running app — it only exists while you run `--studio`, on
430
434
  // loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
431
435
  const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
432
436
  async function startStudio() {
@@ -439,7 +443,7 @@ async function startStudio() {
439
443
  try {
440
444
  store = await (await addonMod("db")).createStore();
441
445
  } catch (e) {
442
- console.error("Studio: couldn't connect the store — " + e.message);
446
+ console.error("Studio: couldn't connect the store — " + e.message);
443
447
  process.exit(1);
444
448
  }
445
449
  const PORT = configPort();
@@ -497,15 +501,15 @@ async function startStudio() {
497
501
  server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
498
502
  server.listen(PORT, "127.0.0.1", () => {
499
503
  const url = `http://localhost:${PORT}`;
500
- console.log(`\n⚡ Volt Studio → ${url} (${store.name})`);
501
- console.log(" Browse your data. localhost-only, disposable — Ctrl-C when done.");
504
+ console.log(`\n⚡ Volt Studio → ${url} (${store.name})`);
505
+ console.log(" Browse your data. localhost-only, disposable — Ctrl-C when done.");
502
506
  const ssh = process.env.SSH_CONNECTION;
503
507
  if (ssh) {
504
508
  const host = ssh.split(" ")[2];
505
509
  const user = process.env.USER || process.env.USERNAME || "you";
506
- console.log(" Remote box — from your LOCAL machine:");
510
+ console.log(" Remote box — from your LOCAL machine:");
507
511
  console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
508
- console.log(` …then open ${url}.`);
512
+ console.log(` …then open ${url}.`);
509
513
  }
510
514
  console.log("");
511
515
  openBrowser(url);
@@ -1,9 +1,9 @@
1
- // server.js — dev server with a built-in first-run setup wizard.
1
+ // server.js — dev server with a built-in first-run setup wizard.
2
2
  //
3
3
  // First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
4
4
  // config page: tick add-ons, fill settings, Apply. Apply writes .env (a
5
5
  // VOLT_ADDONS list + settings) and adds any needed packages to package.json,
6
- // runs npm install, then starts the app — which wires whatever .env enables.
6
+ // runs npm install, then starts the app — which wires whatever .env enables.
7
7
  // Add-on code is bundled under .volt/addons; nothing is copied into your code.
8
8
  //
9
9
  // No build step, no env-file flag: .env is auto-loaded below.
@@ -24,7 +24,7 @@ const THEMES_DIR = path.join(__dirname, ".volt", "themes"); // bundled themes th
24
24
  const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
25
25
  const CONFIG_DEFAULT_PORT = 5050; // the --edit/--studio config UI's default port (its own, so it never clashes with a running app)
26
26
 
27
- // `--port <n>` (or --port=<n>) overrides the listen port for this run lets
27
+ // `--port <n>` (or --port=<n>) overrides the listen port for this run — lets
28
28
  // --edit/--studio dodge a port the running app already holds, and runs the app
29
29
  // itself on a one-off port. Explicit flag wins over PORT in .env.
30
30
  function cliPort() {
@@ -104,7 +104,7 @@ const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
104
104
  const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
105
105
 
106
106
  // Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
107
- // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
107
+ // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
108
108
  // npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
109
109
  const BUILTINS = new Set(Object.keys(LIB_FILE));
110
110
  async function loadAddon(name) {
@@ -128,7 +128,7 @@ function openBrowser(url) {
128
128
  const args = plat === "win32" ? ["/c", "start", "", url] : [url];
129
129
  try {
130
130
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
131
- child.on("error", () => {}); // launcher missing — emits async, don't crash
131
+ child.on("error", () => {}); // launcher missing — emits async, don't crash
132
132
  child.unref();
133
133
  return true;
134
134
  } catch {
@@ -163,7 +163,11 @@ async function startApp() {
163
163
  if (fs.existsSync(pub)) app.use(express.static(pub));
164
164
  }
165
165
 
166
- app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
166
+ app.get("/", (_req, res, next) => {
167
+ // a themed home page (pages/index.md) takes over "/" — else the app's index.html
168
+ if (enabled.has("pages") && fs.existsSync(path.join(__dirname, "pages", "index.md"))) return next();
169
+ res.sendFile(path.join(__dirname, "views", "index.html"));
170
+ });
167
171
 
168
172
  // media uploads (POST /api/media, auth-gated; local files served at /media)
169
173
  if (enabled.has("media") && store) {
@@ -171,8 +175,8 @@ async function startApp() {
171
175
  app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
172
176
  }
173
177
 
174
- // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
175
- // blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) before pages so /blog wins; renders in the same theme.
178
+ // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
179
+ // blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) — before pages so /blog wins; renders in the same theme.
176
180
  if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
177
181
  if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
178
182
 
@@ -180,7 +184,7 @@ async function startApp() {
180
184
  const io = new SocketServer(server);
181
185
  if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
182
186
 
183
- // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
187
+ // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
184
188
  // are provided so add-ons can gate routes by login.
185
189
  let requireAuth = null;
186
190
  let sessionFromReq = null;
@@ -196,7 +200,7 @@ async function startApp() {
196
200
  if (typeof register === "function") {
197
201
  await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
198
202
  } else {
199
- console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
203
+ console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
200
204
  }
201
205
  }
202
206
 
@@ -204,7 +208,7 @@ async function startApp() {
204
208
  const onChange = (file) => {
205
209
  clearTimeout(timer);
206
210
  timer = setTimeout(() => {
207
- console.log(`[volt] change: ${file ?? "?"} → reload`);
211
+ console.log(`[volt] change: ${file ?? "?"} → reload`);
208
212
  io.emit("volt:reload", { file });
209
213
  }, 80);
210
214
  };
@@ -233,7 +237,7 @@ async function startApp() {
233
237
  }
234
238
 
235
239
  const on = [...enabled];
236
- server.listen(PORT, () => console.log(`⚡ Volt → http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
240
+ server.listen(PORT, () => console.log(`⚡ Volt → http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
237
241
  }
238
242
 
239
243
  // Packages an .env's selections need, beyond what package.json already has.
@@ -257,7 +261,7 @@ function neededPackages(env) {
257
261
  function ensureDriverInstalled(driver) {
258
262
  const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
259
263
  if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
260
- console.log(`[volt] installing ${pkg} for the connection test…`);
264
+ console.log(`[volt] installing ${pkg} for the connection test…`);
261
265
  spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
262
266
  }
263
267
 
@@ -380,15 +384,15 @@ function startSetup() {
380
384
  server.closeIdleConnections?.();
381
385
  };
382
386
  if (added.length) {
383
- console.log(`[volt] installing ${added.join(", ")}…`);
387
+ console.log(`[volt] installing ${added.join(", ")}…`);
384
388
  const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
385
389
  npm.on("error", () => handoff());
386
390
  npm.on("close", () => {
387
- console.log("[volt] saved .env — starting the app…");
391
+ console.log("[volt] saved .env — starting the app…");
388
392
  handoff();
389
393
  });
390
394
  } else {
391
- console.log("[volt] saved .env — starting the app…");
395
+ console.log("[volt] saved .env — starting the app…");
392
396
  handoff();
393
397
  }
394
398
  });
@@ -406,18 +410,18 @@ function startSetup() {
406
410
  server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
407
411
  server.listen(PORT, "127.0.0.1", () => {
408
412
  const url = `http://localhost:${PORT}`;
409
- console.log(`\n⚡ Volt setup → ${url}`);
413
+ console.log(`\n⚡ Volt setup → ${url}`);
410
414
  console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
411
415
  const ssh = process.env.SSH_CONNECTION;
412
416
  if (ssh) {
413
417
  const host = ssh.split(" ")[2];
414
418
  const user = process.env.USER || process.env.USERNAME || "you";
415
- console.log(" Remote box — the server is up here; bridge it from your LOCAL machine:");
419
+ console.log(" Remote box — the server is up here; bridge it from your LOCAL machine:");
416
420
  console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
417
- console.log(` …then open ${url} on your machine (the tunnel points it here).`);
421
+ console.log(` …then open ${url} on your machine (the tunnel points it here).`);
418
422
  }
419
423
  console.log("");
420
- if (openBrowser(url)) console.log(" (opening your browser…)\n");
424
+ if (openBrowser(url)) console.log(" (opening your browser…)\n");
421
425
  });
422
426
  }
423
427
 
@@ -425,8 +429,8 @@ function readEnvFileLines() {
425
429
  return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
426
430
  }
427
431
 
428
- // --- Studio: an ephemeral, localhost-only data browser (à la Prisma Studio).
429
- // Not a route in the running app — it only exists while you run `--studio`, on
432
+ // --- Studio: an ephemeral, localhost-only data browser (à la Prisma Studio).
433
+ // Not a route in the running app — it only exists while you run `--studio`, on
430
434
  // loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
431
435
  const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
432
436
  async function startStudio() {
@@ -439,7 +443,7 @@ async function startStudio() {
439
443
  try {
440
444
  store = await (await addonMod("db")).createStore();
441
445
  } catch (e) {
442
- console.error("Studio: couldn't connect the store — " + e.message);
446
+ console.error("Studio: couldn't connect the store — " + e.message);
443
447
  process.exit(1);
444
448
  }
445
449
  const PORT = configPort();
@@ -497,15 +501,15 @@ async function startStudio() {
497
501
  server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
498
502
  server.listen(PORT, "127.0.0.1", () => {
499
503
  const url = `http://localhost:${PORT}`;
500
- console.log(`\n⚡ Volt Studio → ${url} (${store.name})`);
501
- console.log(" Browse your data. localhost-only, disposable — Ctrl-C when done.");
504
+ console.log(`\n⚡ Volt Studio → ${url} (${store.name})`);
505
+ console.log(" Browse your data. localhost-only, disposable — Ctrl-C when done.");
502
506
  const ssh = process.env.SSH_CONNECTION;
503
507
  if (ssh) {
504
508
  const host = ssh.split(" ")[2];
505
509
  const user = process.env.USER || process.env.USERNAME || "you";
506
- console.log(" Remote box — from your LOCAL machine:");
510
+ console.log(" Remote box — from your LOCAL machine:");
507
511
  console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
508
- console.log(` …then open ${url}.`);
512
+ console.log(` …then open ${url}.`);
509
513
  }
510
514
  console.log("");
511
515
  openBrowser(url);
@@ -1,9 +1,9 @@
1
- // server.js — dev server with a built-in first-run setup wizard.
1
+ // server.js — dev server with a built-in first-run setup wizard.
2
2
  //
3
3
  // First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
4
4
  // config page: tick add-ons, fill settings, Apply. Apply writes .env (a
5
5
  // VOLT_ADDONS list + settings) and adds any needed packages to package.json,
6
- // runs npm install, then starts the app — which wires whatever .env enables.
6
+ // runs npm install, then starts the app — which wires whatever .env enables.
7
7
  // Add-on code is bundled under .volt/addons; nothing is copied into your code.
8
8
  //
9
9
  // No build step, no env-file flag: .env is auto-loaded below.
@@ -24,7 +24,7 @@ const THEMES_DIR = path.join(__dirname, ".volt", "themes"); // bundled themes th
24
24
  const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
25
25
  const CONFIG_DEFAULT_PORT = 5050; // the --edit/--studio config UI's default port (its own, so it never clashes with a running app)
26
26
 
27
- // `--port <n>` (or --port=<n>) overrides the listen port for this run lets
27
+ // `--port <n>` (or --port=<n>) overrides the listen port for this run — lets
28
28
  // --edit/--studio dodge a port the running app already holds, and runs the app
29
29
  // itself on a one-off port. Explicit flag wins over PORT in .env.
30
30
  function cliPort() {
@@ -104,7 +104,7 @@ const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
104
104
  const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
105
105
 
106
106
  // Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
107
- // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
107
+ // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
108
108
  // npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
109
109
  const BUILTINS = new Set(Object.keys(LIB_FILE));
110
110
  async function loadAddon(name) {
@@ -128,7 +128,7 @@ function openBrowser(url) {
128
128
  const args = plat === "win32" ? ["/c", "start", "", url] : [url];
129
129
  try {
130
130
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
131
- child.on("error", () => {}); // launcher missing — emits async, don't crash
131
+ child.on("error", () => {}); // launcher missing — emits async, don't crash
132
132
  child.unref();
133
133
  return true;
134
134
  } catch {
@@ -163,7 +163,11 @@ async function startApp() {
163
163
  if (fs.existsSync(pub)) app.use(express.static(pub));
164
164
  }
165
165
 
166
- app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
166
+ app.get("/", (_req, res, next) => {
167
+ // a themed home page (pages/index.md) takes over "/" — else the app's index.html
168
+ if (enabled.has("pages") && fs.existsSync(path.join(__dirname, "pages", "index.md"))) return next();
169
+ res.sendFile(path.join(__dirname, "views", "index.html"));
170
+ });
167
171
 
168
172
  // media uploads (POST /api/media, auth-gated; local files served at /media)
169
173
  if (enabled.has("media") && store) {
@@ -171,8 +175,8 @@ async function startApp() {
171
175
  app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
172
176
  }
173
177
 
174
- // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
175
- // blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) before pages so /blog wins; renders in the same theme.
178
+ // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
179
+ // blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) — before pages so /blog wins; renders in the same theme.
176
180
  if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
177
181
  if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
178
182
 
@@ -180,7 +184,7 @@ async function startApp() {
180
184
  const io = new SocketServer(server);
181
185
  if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
182
186
 
183
- // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
187
+ // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
184
188
  // are provided so add-ons can gate routes by login.
185
189
  let requireAuth = null;
186
190
  let sessionFromReq = null;
@@ -196,7 +200,7 @@ async function startApp() {
196
200
  if (typeof register === "function") {
197
201
  await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
198
202
  } else {
199
- console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
203
+ console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
200
204
  }
201
205
  }
202
206
 
@@ -204,7 +208,7 @@ async function startApp() {
204
208
  const onChange = (file) => {
205
209
  clearTimeout(timer);
206
210
  timer = setTimeout(() => {
207
- console.log(`[volt] change: ${file ?? "?"} → reload`);
211
+ console.log(`[volt] change: ${file ?? "?"} → reload`);
208
212
  io.emit("volt:reload", { file });
209
213
  }, 80);
210
214
  };
@@ -233,7 +237,7 @@ async function startApp() {
233
237
  }
234
238
 
235
239
  const on = [...enabled];
236
- server.listen(PORT, () => console.log(`⚡ Volt → http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
240
+ server.listen(PORT, () => console.log(`⚡ Volt → http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
237
241
  }
238
242
 
239
243
  // Packages an .env's selections need, beyond what package.json already has.
@@ -257,7 +261,7 @@ function neededPackages(env) {
257
261
  function ensureDriverInstalled(driver) {
258
262
  const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
259
263
  if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
260
- console.log(`[volt] installing ${pkg} for the connection test…`);
264
+ console.log(`[volt] installing ${pkg} for the connection test…`);
261
265
  spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
262
266
  }
263
267
 
@@ -380,15 +384,15 @@ function startSetup() {
380
384
  server.closeIdleConnections?.();
381
385
  };
382
386
  if (added.length) {
383
- console.log(`[volt] installing ${added.join(", ")}…`);
387
+ console.log(`[volt] installing ${added.join(", ")}…`);
384
388
  const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
385
389
  npm.on("error", () => handoff());
386
390
  npm.on("close", () => {
387
- console.log("[volt] saved .env — starting the app…");
391
+ console.log("[volt] saved .env — starting the app…");
388
392
  handoff();
389
393
  });
390
394
  } else {
391
- console.log("[volt] saved .env — starting the app…");
395
+ console.log("[volt] saved .env — starting the app…");
392
396
  handoff();
393
397
  }
394
398
  });
@@ -406,18 +410,18 @@ function startSetup() {
406
410
  server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
407
411
  server.listen(PORT, "127.0.0.1", () => {
408
412
  const url = `http://localhost:${PORT}`;
409
- console.log(`\n⚡ Volt setup → ${url}`);
413
+ console.log(`\n⚡ Volt setup → ${url}`);
410
414
  console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
411
415
  const ssh = process.env.SSH_CONNECTION;
412
416
  if (ssh) {
413
417
  const host = ssh.split(" ")[2];
414
418
  const user = process.env.USER || process.env.USERNAME || "you";
415
- console.log(" Remote box — the server is up here; bridge it from your LOCAL machine:");
419
+ console.log(" Remote box — the server is up here; bridge it from your LOCAL machine:");
416
420
  console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
417
- console.log(` …then open ${url} on your machine (the tunnel points it here).`);
421
+ console.log(` …then open ${url} on your machine (the tunnel points it here).`);
418
422
  }
419
423
  console.log("");
420
- if (openBrowser(url)) console.log(" (opening your browser…)\n");
424
+ if (openBrowser(url)) console.log(" (opening your browser…)\n");
421
425
  });
422
426
  }
423
427
 
@@ -425,8 +429,8 @@ function readEnvFileLines() {
425
429
  return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
426
430
  }
427
431
 
428
- // --- Studio: an ephemeral, localhost-only data browser (à la Prisma Studio).
429
- // Not a route in the running app — it only exists while you run `--studio`, on
432
+ // --- Studio: an ephemeral, localhost-only data browser (à la Prisma Studio).
433
+ // Not a route in the running app — it only exists while you run `--studio`, on
430
434
  // loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
431
435
  const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
432
436
  async function startStudio() {
@@ -439,7 +443,7 @@ async function startStudio() {
439
443
  try {
440
444
  store = await (await addonMod("db")).createStore();
441
445
  } catch (e) {
442
- console.error("Studio: couldn't connect the store — " + e.message);
446
+ console.error("Studio: couldn't connect the store — " + e.message);
443
447
  process.exit(1);
444
448
  }
445
449
  const PORT = configPort();
@@ -497,15 +501,15 @@ async function startStudio() {
497
501
  server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
498
502
  server.listen(PORT, "127.0.0.1", () => {
499
503
  const url = `http://localhost:${PORT}`;
500
- console.log(`\n⚡ Volt Studio → ${url} (${store.name})`);
501
- console.log(" Browse your data. localhost-only, disposable — Ctrl-C when done.");
504
+ console.log(`\n⚡ Volt Studio → ${url} (${store.name})`);
505
+ console.log(" Browse your data. localhost-only, disposable — Ctrl-C when done.");
502
506
  const ssh = process.env.SSH_CONNECTION;
503
507
  if (ssh) {
504
508
  const host = ssh.split(" ")[2];
505
509
  const user = process.env.USER || process.env.USERNAME || "you";
506
- console.log(" Remote box — from your LOCAL machine:");
510
+ console.log(" Remote box — from your LOCAL machine:");
507
511
  console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
508
- console.log(` …then open ${url}.`);
512
+ console.log(` …then open ${url}.`);
509
513
  }
510
514
  console.log("");
511
515
  openBrowser(url);
@@ -1,9 +1,9 @@
1
- // server.js — dev server with a built-in first-run setup wizard.
1
+ // server.js — dev server with a built-in first-run setup wizard.
2
2
  //
3
3
  // First run (no .env) or `node server.js --edit` (-e) opens a disposable, local
4
4
  // config page: tick add-ons, fill settings, Apply. Apply writes .env (a
5
5
  // VOLT_ADDONS list + settings) and adds any needed packages to package.json,
6
- // runs npm install, then starts the app — which wires whatever .env enables.
6
+ // runs npm install, then starts the app — which wires whatever .env enables.
7
7
  // Add-on code is bundled under .volt/addons; nothing is copied into your code.
8
8
  //
9
9
  // No build step, no env-file flag: .env is auto-loaded below.
@@ -25,7 +25,7 @@ const THEMES_DIR = path.join(__dirname, ".volt", "themes"); // bundled themes th
25
25
  const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
26
26
  const CONFIG_DEFAULT_PORT = 5050; // the --edit/--studio config UI's default port (its own, so it never clashes with a running app)
27
27
 
28
- // `--port <n>` (or --port=<n>) overrides the listen port for this run lets
28
+ // `--port <n>` (or --port=<n>) overrides the listen port for this run — lets
29
29
  // --edit/--studio dodge a port the running app already holds, and runs the app
30
30
  // itself on a one-off port. Explicit flag wins over PORT in .env.
31
31
  function cliPort() {
@@ -105,7 +105,7 @@ const imp = (rel) => import(pathToFileURL(path.join(__dirname, rel)).href);
105
105
  const addonMod = (n) => imp(path.join(".volt", "addons", n, "files", "lib", LIB_FILE[n]));
106
106
 
107
107
  // Built-in add-ons are wired explicitly below; everything else in VOLT_ADDONS is
108
- // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
108
+ // a third-party add-on — a local .volt/addons/<name>/index.js or an installed
109
109
  // npm package "volt-addon-<name>" exporting register(ctx). See /docs/plugins.
110
110
  const BUILTINS = new Set(Object.keys(LIB_FILE));
111
111
  async function loadAddon(name) {
@@ -129,7 +129,7 @@ function openBrowser(url) {
129
129
  const args = plat === "win32" ? ["/c", "start", "", url] : [url];
130
130
  try {
131
131
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
132
- child.on("error", () => {}); // launcher missing — emits async, don't crash
132
+ child.on("error", () => {}); // launcher missing — emits async, don't crash
133
133
  child.unref();
134
134
  return true;
135
135
  } catch {
@@ -157,7 +157,7 @@ async function startApp() {
157
157
  if (enabled.has("mailer")) mailer = await (await addonMod("mailer")).createMailer();
158
158
  if (enabled.has("auth") && store && mailer) app.use((await addonMod("auth")).authRouter({ store, mailer }));
159
159
 
160
- // notes — a per-user CRUD example (auth-gated, owner-scoped, db-backed)
160
+ // notes — a per-user CRUD example (auth-gated, owner-scoped, db-backed)
161
161
  if (enabled.has("db") && enabled.has("auth") && store) {
162
162
  const guard = (await addonMod("auth")).requireAuth(store);
163
163
  const notes = store.collection("notes");
@@ -189,7 +189,11 @@ async function startApp() {
189
189
  if (fs.existsSync(pub)) app.use(express.static(pub));
190
190
  }
191
191
 
192
- app.get("/", (_req, res) => res.sendFile(path.join(__dirname, "views", "index.html")));
192
+ app.get("/", (_req, res, next) => {
193
+ // a themed home page (pages/index.md) takes over "/" — else the app's index.html
194
+ if (enabled.has("pages") && fs.existsSync(path.join(__dirname, "pages", "index.md"))) return next();
195
+ res.sendFile(path.join(__dirname, "views", "index.html"));
196
+ });
193
197
 
194
198
  // media uploads (POST /api/media, auth-gated; local files served at /media)
195
199
  if (enabled.has("media") && store) {
@@ -197,8 +201,8 @@ async function startApp() {
197
201
  app.use(await (await addonMod("media")).mediaRouter({ requireAuth, dir: path.join(__dirname, "media") }));
198
202
  }
199
203
 
200
- // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
201
- // blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) before pages so /blog wins; renders in the same theme.
204
+ // markdown pages (/<slug> ← pages/<slug>.md) — mounted last, so app routes win
205
+ // blog posts (/blog, /blog/<slug>, /category, /tag, /feed.xml) — before pages so /blog wins; renders in the same theme.
202
206
  if (enabled.has("posts")) app.use(await (await addonMod("posts")).postsRouter({ dir: path.join(__dirname, "posts"), themeDir: path.join(__dirname, "pages") }));
203
207
  if (enabled.has("pages")) app.use(await (await addonMod("pages")).pagesRouter({ dir: path.join(__dirname, "pages") }));
204
208
 
@@ -206,7 +210,7 @@ async function startApp() {
206
210
  const io = new SocketServer(server);
207
211
  if (enabled.has("realtime") && store) (await addonMod("realtime")).attachRealtime(io, { store });
208
212
 
209
- // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
213
+ // third-party add-ons — register(ctx). When auth is on, requireAuth/sessionFromReq
210
214
  // are provided so add-ons can gate routes by login.
211
215
  let requireAuth = null;
212
216
  let sessionFromReq = null;
@@ -222,7 +226,7 @@ async function startApp() {
222
226
  if (typeof register === "function") {
223
227
  await register({ app, express, io, store, mailer, env: process.env, requireAuth, sessionFromReq, log: (...a) => console.log(`[${name}]`, ...a) });
224
228
  } else {
225
- console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
229
+ console.warn(`[volt] add-on "${name}" not found or missing a register() export — skipped`);
226
230
  }
227
231
  }
228
232
 
@@ -230,7 +234,7 @@ async function startApp() {
230
234
  const onChange = (file) => {
231
235
  clearTimeout(timer);
232
236
  timer = setTimeout(() => {
233
- console.log(`[volt] change: ${file ?? "?"} → reload`);
237
+ console.log(`[volt] change: ${file ?? "?"} → reload`);
234
238
  io.emit("volt:reload", { file });
235
239
  }, 80);
236
240
  };
@@ -259,7 +263,7 @@ async function startApp() {
259
263
  }
260
264
 
261
265
  const on = [...enabled];
262
- server.listen(PORT, () => console.log(`⚡ Volt → http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
266
+ server.listen(PORT, () => console.log(`⚡ Volt → http://localhost:${PORT}${on.length ? " (add-ons: " + on.join(", ") + ")" : ""}`));
263
267
  }
264
268
 
265
269
  // Packages an .env's selections need, beyond what package.json already has.
@@ -283,7 +287,7 @@ function neededPackages(env) {
283
287
  function ensureDriverInstalled(driver) {
284
288
  const pkg = { mongodb: "mongodb", mongo: "mongodb", mysql: "mysql2", postgres: "pg", postgresql: "pg", pg: "pg" }[String(driver || "").toLowerCase()];
285
289
  if (!pkg || fs.existsSync(path.join(__dirname, "node_modules", pkg))) return;
286
- console.log(`[volt] installing ${pkg} for the connection test…`);
290
+ console.log(`[volt] installing ${pkg} for the connection test…`);
287
291
  spawnSync("npm", ["install", `${pkg}@${PKG_VERSIONS[pkg] || "latest"}`], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
288
292
  }
289
293
 
@@ -406,15 +410,15 @@ function startSetup() {
406
410
  server.closeIdleConnections?.();
407
411
  };
408
412
  if (added.length) {
409
- console.log(`[volt] installing ${added.join(", ")}…`);
413
+ console.log(`[volt] installing ${added.join(", ")}…`);
410
414
  const npm = spawn("npm", ["install"], { cwd: __dirname, stdio: "inherit", shell: process.platform === "win32" });
411
415
  npm.on("error", () => handoff());
412
416
  npm.on("close", () => {
413
- console.log("[volt] saved .env — starting the app…");
417
+ console.log("[volt] saved .env — starting the app…");
414
418
  handoff();
415
419
  });
416
420
  } else {
417
- console.log("[volt] saved .env — starting the app…");
421
+ console.log("[volt] saved .env — starting the app…");
418
422
  handoff();
419
423
  }
420
424
  });
@@ -432,18 +436,18 @@ function startSetup() {
432
436
  server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
433
437
  server.listen(PORT, "127.0.0.1", () => {
434
438
  const url = `http://localhost:${PORT}`;
435
- console.log(`\n⚡ Volt setup → ${url}`);
439
+ console.log(`\n⚡ Volt setup → ${url}`);
436
440
  console.log(" Configure your app; it starts automatically on Apply. (reopen later: npm run dev -- --edit)");
437
441
  const ssh = process.env.SSH_CONNECTION;
438
442
  if (ssh) {
439
443
  const host = ssh.split(" ")[2];
440
444
  const user = process.env.USER || process.env.USERNAME || "you";
441
- console.log(" Remote box — the server is up here; bridge it from your LOCAL machine:");
445
+ console.log(" Remote box — the server is up here; bridge it from your LOCAL machine:");
442
446
  console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
443
- console.log(` …then open ${url} on your machine (the tunnel points it here).`);
447
+ console.log(` …then open ${url} on your machine (the tunnel points it here).`);
444
448
  }
445
449
  console.log("");
446
- if (openBrowser(url)) console.log(" (opening your browser…)\n");
450
+ if (openBrowser(url)) console.log(" (opening your browser…)\n");
447
451
  });
448
452
  }
449
453
 
@@ -451,8 +455,8 @@ function readEnvFileLines() {
451
455
  return fs.existsSync(ENV_PATH) ? fs.readFileSync(ENV_PATH, "utf8").split("\n") : [];
452
456
  }
453
457
 
454
- // --- Studio: an ephemeral, localhost-only data browser (à la Prisma Studio).
455
- // Not a route in the running app — it only exists while you run `--studio`, on
458
+ // --- Studio: an ephemeral, localhost-only data browser (à la Prisma Studio).
459
+ // Not a route in the running app — it only exists while you run `--studio`, on
456
460
  // loopback, and disappears on Ctrl-C. Shell/SSH access is the auth. ---
457
461
  const HIDDEN_COLLECTIONS = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
458
462
  async function startStudio() {
@@ -465,7 +469,7 @@ async function startStudio() {
465
469
  try {
466
470
  store = await (await addonMod("db")).createStore();
467
471
  } catch (e) {
468
- console.error("Studio: couldn't connect the store — " + e.message);
472
+ console.error("Studio: couldn't connect the store — " + e.message);
469
473
  process.exit(1);
470
474
  }
471
475
  const PORT = configPort();
@@ -523,15 +527,15 @@ async function startStudio() {
523
527
  server.on("error", (e) => { if (e.code === "EADDRINUSE") { console.error(`\n[volt] Config UI port ${PORT} is in use (is the app already running?). Set CONFIG_PORT in .env or pass --port <n>.\n`); process.exit(1); } throw e; });
524
528
  server.listen(PORT, "127.0.0.1", () => {
525
529
  const url = `http://localhost:${PORT}`;
526
- console.log(`\n⚡ Volt Studio → ${url} (${store.name})`);
527
- console.log(" Browse your data. localhost-only, disposable — Ctrl-C when done.");
530
+ console.log(`\n⚡ Volt Studio → ${url} (${store.name})`);
531
+ console.log(" Browse your data. localhost-only, disposable — Ctrl-C when done.");
528
532
  const ssh = process.env.SSH_CONNECTION;
529
533
  if (ssh) {
530
534
  const host = ssh.split(" ")[2];
531
535
  const user = process.env.USER || process.env.USERNAME || "you";
532
- console.log(" Remote box — from your LOCAL machine:");
536
+ console.log(" Remote box — from your LOCAL machine:");
533
537
  console.log(` ssh -N -L 127.0.0.1:${PORT}:localhost:${PORT} ${user}@${host}`);
534
- console.log(` …then open ${url}.`);
538
+ console.log(` …then open ${url}.`);
535
539
  }
536
540
  console.log("");
537
541
  openBrowser(url);