create-volt 0.5.0 → 0.8.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.
@@ -0,0 +1,168 @@
1
+ // config-app.js — the disposable add-on configurator, built with Volt itself.
2
+ import { signal, computed, html, mount } from "/volt.js";
3
+
4
+ const datePort = () => {
5
+ const d = new Date();
6
+ return `${String(d.getFullYear()).slice(-2)}${d.getMonth() + 1}${String(d.getDate()).padStart(2, "0")}`;
7
+ };
8
+
9
+ const addons = await (await fetch("/addons.json")).json();
10
+ const current = await (await fetch("/current.json")).json(); // existing .env, so Apply doesn't clobber it
11
+ const byName = (n) => addons.find((a) => a.name === n);
12
+
13
+ const state = signal({
14
+ addons: Object.fromEntries(addons.map((a) => [a.name, a.installed])),
15
+ dbDriver: current.DB_DRIVER || "memory",
16
+ mongoUri: current.MONGODB_URI || "",
17
+ mongoDb: current.MONGODB_DATABASE || "",
18
+ dbUrl: current.DATABASE_URL || "",
19
+ smtpUrl: current.SMTP_URL || "",
20
+ mailFrom: current.MAIL_FROM || "",
21
+ port: current.PORT || datePort(),
22
+ });
23
+ const set = (patch) => state({ ...state(), ...patch });
24
+ const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
25
+ const status = signal("");
26
+
27
+ // --- generators ---
28
+ function genEnv(s) {
29
+ const out = [];
30
+ if (s.port) out.push(`PORT=${s.port}`);
31
+ if (s.addons.db) {
32
+ out.push(`DB_DRIVER=${s.dbDriver}`);
33
+ if (s.dbDriver === "mongodb") {
34
+ out.push(`MONGODB_URI=${s.mongoUri}`);
35
+ if (s.mongoDb) out.push(`MONGODB_DATABASE=${s.mongoDb}`);
36
+ } else if (s.dbDriver === "mysql" || s.dbDriver === "postgres") {
37
+ out.push(`DATABASE_URL=${s.dbUrl}`);
38
+ }
39
+ }
40
+ if (s.addons.mailer) {
41
+ if (s.smtpUrl) out.push(`SMTP_URL=${s.smtpUrl}`);
42
+ else out.push(`# SMTP_URL= # unset → emails print to the console`);
43
+ if (s.mailFrom) out.push(`MAIL_FROM=${s.mailFrom}`);
44
+ }
45
+ return out.join("\n") + "\n";
46
+ }
47
+ function genInstall(s) {
48
+ const deps = new Set();
49
+ for (const a of addons) if (s.addons[a.name]) (a.install || []).forEach((d) => deps.add(d));
50
+ if (s.addons.db) {
51
+ if (s.dbDriver === "mongodb") deps.add("mongodb");
52
+ if (s.dbDriver === "mysql") deps.add("mysql2");
53
+ if (s.dbDriver === "postgres") deps.add("pg");
54
+ }
55
+ if (s.addons.mailer && s.smtpUrl) deps.add("nodemailer");
56
+ return deps.size ? `npm install ${[...deps].join(" ")}` : "# no extra packages needed";
57
+ }
58
+ function genWiring(s) {
59
+ const parts = [];
60
+ for (const name of ["db", "mailer", "auth", "realtime"]) {
61
+ if (s.addons[name]) parts.push(`// ── ${name} ──\n${byName(name).wiring}`);
62
+ }
63
+ return parts.join("\n\n") || "// select add-ons above";
64
+ }
65
+ function warnings(s) {
66
+ const w = [];
67
+ if (s.addons.auth && (!s.addons.db || !s.addons.mailer)) w.push("auth needs db + mailer");
68
+ if (s.addons.realtime && !s.addons.db) w.push("realtime needs db (for message persistence)");
69
+ return w;
70
+ }
71
+
72
+ const env = computed(() => genEnv(state()));
73
+
74
+ const KEY = new URLSearchParams(location.search).get("key") || "";
75
+ const copy = (text) => navigator.clipboard.writeText(text).then(() => (status("Copied."), setTimeout(() => status(""), 1500)));
76
+ const setAll = (v) => state({ ...state(), addons: Object.fromEntries(addons.map((a) => [a.name, v])) });
77
+ async function apply() {
78
+ const selected = Object.keys(state().addons).filter((n) => state().addons[n]);
79
+ status("Applying…");
80
+ const r = await fetch("/apply", { method: "POST", headers: { "Content-Type": "application/json", "x-config-key": KEY }, body: JSON.stringify({ addons: selected, env: env() }) });
81
+ const d = await r.json();
82
+ status(d.ok ? `Applied ${selected.join(", ") || "(none)"} — copied ${d.copied.length} file(s) + wrote .env. Next: ${genInstall(state())} · npm run dev` : `Error: ${d.error}`);
83
+ setTimeout(() => status(""), 9000);
84
+ }
85
+
86
+ // --- views ---
87
+ const addonToggles = () =>
88
+ html`<div class="card-x p-4 mb-3">
89
+ <div class="d-flex justify-content-between align-items-center mb-3">
90
+ <h2 class="h6 mb-0">Add-ons</h2>
91
+ <div>
92
+ <button class="btn btn-sm btn-outline-secondary" onclick=${() => setAll(true)}>All</button>
93
+ <button class="btn btn-sm btn-outline-secondary ms-1" onclick=${() => setAll(false)}>None</button>
94
+ </div>
95
+ </div>
96
+ ${addons.map(
97
+ (a) => html`<div class="form-check mb-2">
98
+ <input class="form-check-input" type="checkbox" id=${"x-" + a.name}
99
+ checked=${() => state().addons[a.name]} onchange=${() => toggle(a.name)} />
100
+ <label class="form-check-label" for=${"x-" + a.name}>
101
+ <span class="accent">${a.name}</span>${a.installed ? " · installed" : ""}
102
+ <div class="small text-muted">${a.description}</div>
103
+ </label>
104
+ </div>`,
105
+ )}
106
+ </div>`;
107
+
108
+ const field = (label, key, placeholder = "") =>
109
+ html`<div class="mb-2">
110
+ <label class="form-label small mb-1">${label}</label>
111
+ <input class="form-control" placeholder=${placeholder}
112
+ value=${() => state()[key]} oninput=${(e) => set({ [key]: e.target.value })} />
113
+ </div>`;
114
+
115
+ const settings = () =>
116
+ html`<div class="card-x p-4 mb-3">
117
+ <h2 class="h6 mb-3">Settings</h2>
118
+ ${field("PORT", "port", "26628")}
119
+ ${() =>
120
+ state().addons.db
121
+ ? html`<div class="mb-2">
122
+ <label class="form-label small mb-1">DB_DRIVER</label>
123
+ <select class="form-select" value=${() => state().dbDriver} onchange=${(e) => set({ dbDriver: e.target.value })}>
124
+ <option value="memory">memory (no setup)</option>
125
+ <option value="mongodb">mongodb</option>
126
+ <option value="mysql">mysql</option>
127
+ <option value="postgres">postgres</option>
128
+ </select>
129
+ </div>
130
+ ${() =>
131
+ state().dbDriver === "mongodb"
132
+ ? html`${field("MONGODB_URI", "mongoUri", "mongodb://user:pass@host:27017/db")}${field("MONGODB_DATABASE", "mongoDb", "db")}`
133
+ : state().dbDriver === "mysql" || state().dbDriver === "postgres"
134
+ ? field("DATABASE_URL", "dbUrl", state().dbDriver + "://user:pass@host/db")
135
+ : null}`
136
+ : null}
137
+ ${() => (state().addons.mailer ? html`${field("SMTP_URL (optional)", "smtpUrl", "smtp://user:pass@smtp.host:587")}${field("MAIL_FROM", "mailFrom", "App <no-reply@you.com>")}` : null)}
138
+ </div>`;
139
+
140
+ const warnRow = () =>
141
+ html`${() => {
142
+ const w = warnings(state());
143
+ return w.length ? html`<div class="alert alert-warning py-2 small">⚠ ${w.join(" · ")}</div>` : null;
144
+ }}`;
145
+
146
+ const output = (title, getText, withWrite = false) =>
147
+ html`<div class="card-x p-4 mb-3">
148
+ <div class="d-flex justify-content-between align-items-center mb-2">
149
+ <h2 class="h6 mb-0">${title}</h2>
150
+ <div>
151
+ <button class="btn btn-sm btn-outline-secondary" onclick=${() => copy(getText())}>Copy</button>
152
+ ${withWrite ? html`<button class="btn btn-sm btn-primary ms-2" onclick=${apply}>Apply</button>` : null}
153
+ </div>
154
+ </div>
155
+ <pre class="mb-0">${getText}</pre>
156
+ </div>`;
157
+
158
+ mount(
159
+ "#app",
160
+ addonToggles(),
161
+ settings(),
162
+ warnRow(),
163
+ output(".env", env, true),
164
+ html`<p class="small text-muted mb-3">The app auto-loads <code>.env</code> on start — just run <code>npm run dev</code> (no flags; same on Windows).</p>`,
165
+ output("Install", computed(() => genInstall(state()))),
166
+ output("server.js wiring", computed(() => genWiring(state()))),
167
+ () => (status() ? html`<p class="small accent">${status}</p>` : null),
168
+ );
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>create-volt · configure add-ons</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
8
+ <style>
9
+ body { background: #0f1115; color: #e7e9ee; }
10
+ .accent { color: #ffd24a; }
11
+ .card-x { background: #161a22; border: 1px solid #232a36; border-radius: 14px; }
12
+ .form-control, .form-select { background: #0f1115; color: #e7e9ee; border-color: #232a36; }
13
+ .form-control:focus, .form-select:focus { background: #0f1115; color: #e7e9ee; border-color: #ffd24a; box-shadow: none; }
14
+ pre { background: #0b0d11; border: 1px solid #232a36; border-radius: 10px; padding: 12px; color: #cfe3ff; white-space: pre-wrap; }
15
+ a { color: #ffd24a; }
16
+ code { color: #ffd24a; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <main class="container py-5" style="max-width: 760px;">
21
+ <header class="mb-4">
22
+ <h1 class="h3"><span class="accent">⚡ create-volt</span> · configure add-ons</h1>
23
+ <p class="text-muted mb-0">Pick add-ons, fill in settings, write your <code>.env</code>. This page is disposable — close the terminal when you're done.</p>
24
+ </header>
25
+ <div id="app"></div>
26
+ </main>
27
+ <script type="module" src="/config-app.js"></script>
28
+ </body>
29
+ </html>
package/index.js CHANGED
@@ -10,6 +10,9 @@
10
10
 
11
11
  import fs from "node:fs";
12
12
  import path from "node:path";
13
+ import http from "node:http";
14
+ import os from "node:os";
15
+ import crypto from "node:crypto";
13
16
  import { spawnSync } from "node:child_process";
14
17
  import { fileURLToPath } from "node:url";
15
18
  import { createRequire } from "node:module";
@@ -35,12 +38,15 @@ ${bold("Usage")}
35
38
  npm create volt@latest <project-directory> [options]
36
39
  npx create-volt <project-directory> [options]
37
40
  npx create-volt@latest update # refresh public/volt.js in an existing app
41
+ npx create-volt@latest config # disposable page: add db/auth/realtime/mailer + write .env
38
42
 
39
43
  ${bold("Options")}
40
44
  --template <name> Starter template: default | guestbook (default: default)
41
45
  --port <number> Dev port for the app (default: derived from today's date)
42
46
  --skip-install Don't run the package manager install step
43
47
  --no-git Don't initialize a git repository
48
+ --start After scaffolding, run the dev server (opens the setup page)
49
+ --no-open Don't auto-open the browser on start
44
50
  --dry-run Show what would be created without writing anything
45
51
  --force Scaffold into an existing non-empty directory
46
52
  -h, --help Show this help
@@ -57,12 +63,15 @@ const flags = new Set();
57
63
  const positionals = [];
58
64
  let portArg = null;
59
65
  let templateArg = null;
66
+ let hostArg = null;
60
67
  for (let i = 0; i < argv.length; i++) {
61
68
  const a = argv[i];
62
69
  if (a === "--port") portArg = argv[++i];
63
70
  else if (a.startsWith("--port=")) portArg = a.slice("--port=".length);
64
71
  else if (a === "--template") templateArg = argv[++i];
65
72
  else if (a.startsWith("--template=")) templateArg = a.slice("--template=".length);
73
+ else if (a === "--host") hostArg = argv[++i];
74
+ else if (a.startsWith("--host=")) hostArg = a.slice("--host=".length);
66
75
  else if (a.startsWith("-")) flags.add(a);
67
76
  else positionals.push(a);
68
77
  }
@@ -81,6 +90,11 @@ const force = flags.has("--force");
81
90
  const dryRun = flags.has("--dry-run");
82
91
  const noGit = flags.has("--no-git");
83
92
 
93
+ // The `add` command was replaced by `config` — catch old muscle memory.
94
+ if (positionals[0] === "add") {
95
+ die(`${cyan("add")} was replaced by ${cyan("create-volt config")} — run that to add integrations.`);
96
+ }
97
+
84
98
  // --- `update` subcommand: refresh public/volt.js in the current app to the
85
99
  // version bundled with this create-volt (so `npx create-volt@latest update`
86
100
  // pulls the latest library). Only touches the library file — never the user's
@@ -107,6 +121,165 @@ if (positionals[0] === "update") {
107
121
  process.exit(0);
108
122
  }
109
123
 
124
+ // --- `config` subcommand: a disposable local page to configure add-ons and
125
+ // write a .env. Dependency-free (node:http); the page is built with Volt. ---
126
+ if (positionals[0] === "config") {
127
+ const cwd = process.cwd();
128
+ const addonsDir = path.join(__dirname, "addons");
129
+ const addonList = fs
130
+ .readdirSync(addonsDir, { withFileTypes: true })
131
+ .filter((e) => e.isDirectory())
132
+ .map((e) => {
133
+ const m = JSON.parse(fs.readFileSync(path.join(addonsDir, e.name, "meta.json"), "utf8"));
134
+ return {
135
+ name: e.name,
136
+ description: m.description,
137
+ install: m.install || [],
138
+ dependsOn: m.dependsOn || [],
139
+ wiring: m.wiring,
140
+ installed: fs.existsSync(path.join(cwd, m.sentinel)),
141
+ };
142
+ });
143
+ const assets = {
144
+ "/config-app.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "config", "config-app.js"))],
145
+ "/volt.js": ["text/javascript; charset=utf-8", fs.readFileSync(path.join(__dirname, "templates", "default", "public", "volt.js"))],
146
+ };
147
+ const indexHtml = fs.readFileSync(path.join(__dirname, "config", "index.html"));
148
+ // localhost by default — shell/SSH access is the auth. --host exposes it on the
149
+ // network, and only then do we mint a random key to gate it.
150
+ const host = hostArg || "127.0.0.1";
151
+ const exposed = host !== "127.0.0.1" && host !== "localhost";
152
+ const key = exposed ? crypto.randomBytes(12).toString("hex") : null;
153
+
154
+ const server = http.createServer((req, res) => {
155
+ const u = new URL(req.url, "http://localhost");
156
+ const p = u.pathname;
157
+ if (req.method === "GET" && p === "/") {
158
+ if (key && u.searchParams.get("key") !== key) {
159
+ res.statusCode = 403;
160
+ return res.end("Forbidden — open the ?key=… link printed in the terminal.");
161
+ }
162
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
163
+ return res.end(indexHtml);
164
+ }
165
+ if (req.method === "GET" && assets[p]) {
166
+ res.setHeader("Content-Type", assets[p][0]);
167
+ return res.end(assets[p][1]);
168
+ }
169
+ if (req.method === "GET" && p === "/addons.json") {
170
+ res.setHeader("Content-Type", "application/json");
171
+ return res.end(JSON.stringify(addonList));
172
+ }
173
+ if (req.method === "GET" && p === "/current.json") {
174
+ const current = {};
175
+ const envPath = path.join(cwd, ".env");
176
+ if (fs.existsSync(envPath)) {
177
+ for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
178
+ const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*?)\s*$/);
179
+ if (m) current[m[1]] = m[2];
180
+ }
181
+ }
182
+ res.setHeader("Content-Type", "application/json");
183
+ return res.end(JSON.stringify(current));
184
+ }
185
+ if (req.method === "POST" && p === "/apply") {
186
+ if (key && req.headers["x-config-key"] !== key) {
187
+ res.statusCode = 403;
188
+ return res.end(JSON.stringify({ ok: false, error: "bad or missing key" }));
189
+ }
190
+ let body = "";
191
+ req.on("data", (c) => (body += c));
192
+ req.on("end", () => {
193
+ try {
194
+ const { addons = [], env } = JSON.parse(body);
195
+ const valid = new Set(addonList.map((a) => a.name));
196
+ const depsOf = Object.fromEntries(addonList.map((a) => [a.name, a.dependsOn]));
197
+ // expand selection to include required dependencies (deps before dependents)
198
+ const want = [];
199
+ const seen = new Set();
200
+ const visit = (n) => {
201
+ if (!valid.has(n) || seen.has(n)) return;
202
+ seen.add(n);
203
+ for (const d of depsOf[n] || []) visit(d);
204
+ want.push(n);
205
+ };
206
+ for (const n of addons) visit(n);
207
+
208
+ const copied = [];
209
+ const skipped = [];
210
+ for (const n of want) {
211
+ const filesDir = path.join(addonsDir, n, "files");
212
+ for (const f of listTemplateFiles(filesDir)) {
213
+ const dest = path.join(cwd, f);
214
+ if (fs.existsSync(dest)) {
215
+ skipped.push(f);
216
+ continue;
217
+ }
218
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
219
+ fs.copyFileSync(path.join(filesDir, f), dest);
220
+ copied.push(f);
221
+ }
222
+ }
223
+ if (typeof env === "string") {
224
+ // preserve any custom keys already in .env that this form doesn't manage
225
+ const envPath = path.join(cwd, ".env");
226
+ let finalEnv = env;
227
+ if (fs.existsSync(envPath)) {
228
+ const managed = new Set([...env.matchAll(/^\s*([A-Za-z0-9_]+)\s*=/gm)].map((m) => m[1]));
229
+ const extra = fs
230
+ .readFileSync(envPath, "utf8")
231
+ .split("\n")
232
+ .filter((line) => {
233
+ const m = line.match(/^\s*([A-Za-z0-9_]+)\s*=/);
234
+ return m && !managed.has(m[1]);
235
+ });
236
+ if (extra.length) finalEnv = env.replace(/\n*$/, "\n") + extra.join("\n") + "\n";
237
+ }
238
+ fs.writeFileSync(envPath, finalEnv);
239
+ }
240
+ console.log(`${green("✔")} applied [${want.join(", ")}] — ${copied.length} file(s) copied, .env written`);
241
+ res.setHeader("Content-Type", "application/json");
242
+ res.end(JSON.stringify({ ok: true, copied, skipped, applied: want }));
243
+ } catch (e) {
244
+ res.statusCode = 400;
245
+ res.end(JSON.stringify({ ok: false, error: e.message }));
246
+ }
247
+ });
248
+ return;
249
+ }
250
+ res.statusCode = 404;
251
+ res.end("not found");
252
+ });
253
+
254
+ const wanted = portArg ? Number(portArg) : 0;
255
+ server.listen(Number.isInteger(wanted) && wanted > 0 ? wanted : 0, host, () => {
256
+ const port = server.address().port;
257
+ const q = key ? `/?key=${key}` : "/";
258
+ console.log(`\n${bold("⚡ create-volt config")} — add-ons for ${dim(cwd)}\n`);
259
+ if (exposed) {
260
+ const lan = [];
261
+ for (const iface of Object.values(os.networkInterfaces())) {
262
+ for (const a of iface || []) if (a.family === "IPv4" && !a.internal) lan.push(a.address);
263
+ }
264
+ console.log(" Open (key-gated, reachable on your network):");
265
+ for (const ip of lan) console.log(" " + cyan(`http://${ip}:${port}${q}`));
266
+ console.log(" " + cyan(`http://localhost:${port}${q}`) + dim(" (this box)"));
267
+ } else {
268
+ const url = `http://localhost:${port}${q}`;
269
+ console.log(" Open: " + cyan(url));
270
+ const ssh = process.env.SSH_CONNECTION; // "clientIP clientPort serverIP serverPort"
271
+ const user = process.env.USER || process.env.USERNAME || "you";
272
+ const sshHost = ssh ? ssh.split(" ")[2] : os.hostname();
273
+ console.log(` ${dim(ssh ? "Remote box — the server is up here; from your LOCAL machine run:" : "Remote box? from your local machine run:")}`);
274
+ console.log(" " + dim(`ssh -N -L 127.0.0.1:${port}:localhost:${port} ${user}@${sshHost}`));
275
+ console.log(` ${dim(`…then open ${url} on your machine — shell access is the auth.`)}`);
276
+ console.log(` ${dim("(LAN access instead: --host 0.0.0.0 — adds a key)")}`);
277
+ }
278
+ console.log(`\n ${dim("Applies add-ons + writes .env here · Ctrl-C when done (disposable).")}\n`);
279
+ });
280
+ await new Promise(() => {}); // keep the server up; never fall through to scaffolding
281
+ }
282
+
110
283
  // Resolve the dev port: --port wins, else derive it from today's date as
111
284
  // two-digit-year + month (no leading zero) + two-digit-day (house convention),
112
285
  // so apps scaffolded on different days don't collide on the same port.
@@ -214,10 +387,10 @@ fs.writeFileSync(appPkgPath, JSON.stringify(appPkg, null, 2) + "\n");
214
387
 
215
388
  // --- stamp the chosen dev port into server.js + README ---
216
389
  const serverPath = path.join(targetDir, "server.js");
217
- fs.writeFileSync(
218
- serverPath,
219
- fs.readFileSync(serverPath, "utf8").replace(/(Number\(process\.env\.PORT\)\s*\|\|\s*)\d+/, `$1${port}`),
220
- );
390
+ let serverSrc = fs.readFileSync(serverPath, "utf8");
391
+ serverSrc = serverSrc.replace(/(const DEFAULT_PORT\s*=\s*)\d+/, `$1${port}`); // default template
392
+ serverSrc = serverSrc.replace(/(Number\(process\.env\.PORT\)\s*\|\|\s*)\d+/, `$1${port}`); // other templates
393
+ fs.writeFileSync(serverPath, serverSrc);
221
394
  const appReadme = path.join(targetDir, "README.md");
222
395
  if (fs.existsSync(appReadme)) {
223
396
  fs.writeFileSync(appReadme, fs.readFileSync(appReadme, "utf8").replace(/localhost:\d+/g, `localhost:${port}`));
@@ -287,4 +460,18 @@ console.log(`\n${green("✔")} ${bold("Done!")} Next steps:\n`);
287
460
  console.log(` ${cyan("cd")} ${projectName}`);
288
461
  if (!installed) console.log(` ${cyan(installCmd)}`);
289
462
  console.log(` ${cyan(runCmd)}`);
290
- console.log(`\nThen open ${cyan("http://localhost:" + port)} and edit ${bold("public/app.js")}.\n`);
463
+ console.log(`\nFirst run opens a quick ${bold("setup")} page at ${cyan("http://localhost:" + port)}, then your app starts.\n`);
464
+
465
+ if (flags.has("--start")) {
466
+ if (!installed) {
467
+ console.log(dim(`(--start needs dependencies — run ${installCmd}, then ${runCmd}.)\n`));
468
+ } else {
469
+ console.log(`${bold("Starting…")}\n`);
470
+ spawnSync(pm, ["run", "dev"], {
471
+ cwd: targetDir,
472
+ stdio: "inherit",
473
+ shell: process.platform === "win32",
474
+ env: flags.has("--no-open") ? { ...process.env, VOLT_NO_OPEN: "1" } : process.env,
475
+ });
476
+ }
477
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-volt",
3
- "version": "0.5.0",
3
+ "version": "0.8.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": {
@@ -9,6 +9,8 @@
9
9
  "files": [
10
10
  "index.js",
11
11
  "templates",
12
+ "addons",
13
+ "config",
12
14
  "CHANGELOG.md"
13
15
  ],
14
16
  "engines": {
@@ -10,9 +10,15 @@ Not React: no JSX, no virtual DOM, no re-render-the-world. State lives in
10
10
 
11
11
  ```bash
12
12
  npm install # if you scaffolded with --skip-install
13
- npm run dev # → http://localhost:26628 (PORT env to override)
13
+ npm run dev # → http://localhost:26628
14
14
  ```
15
15
 
16
+ The **first run opens a quick setup page** in your browser (configure settings,
17
+ click Apply, and the app starts — the setup page then disappears). On a headless
18
+ or remote box it prints the link instead. Reopen settings anytime with
19
+ `npm run dev -- --edit` (`-e`). `.env` is auto-loaded, so no `--env-file` flag
20
+ is needed — it works the same on Windows.
21
+
16
22
  Edit anything in `public/` or `views/` and save — the dev server pushes a
17
23
  reload over Socket.io and the page refreshes itself.
18
24