volaro 0.0.2 → 0.1.0-alpha.3

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.
Files changed (37) hide show
  1. package/README.md +114 -22
  2. package/bin/vl.js +466 -28
  3. package/compiler/SOURCE_INFO.json +6 -0
  4. package/compiler/SOURCE_REV +1 -0
  5. package/compiler/validator/vlcheck/__init__.py +10 -0
  6. package/compiler/validator/vlcheck/__main__.py +128 -0
  7. package/compiler/validator/vlcheck/ast_nodes.py +397 -0
  8. package/compiler/validator/vlcheck/checks.py +1227 -0
  9. package/compiler/validator/vlcheck/diagnostics.py +88 -0
  10. package/compiler/validator/vlcheck/lexer.py +343 -0
  11. package/compiler/validator/vlcheck/parser.py +1638 -0
  12. package/compiler/validator/vlcheck/project_config.py +97 -0
  13. package/compiler/validator/vlcheck/resolve.py +849 -0
  14. package/compiler/validator/vlcheck/test_ids.py +98 -0
  15. package/compiler/vlbuild/styling/README.md +39 -0
  16. package/compiler/vlbuild/styling/build-css.mjs +103 -0
  17. package/compiler/vlbuild/styling/package-lock.json +1254 -0
  18. package/compiler/vlbuild/styling/package.json +15 -0
  19. package/compiler/vlbuild/styling/test-build-css.mjs +69 -0
  20. package/compiler/vlbuild/vlbuild/__init__.py +16 -0
  21. package/compiler/vlbuild/vlbuild/__main__.py +246 -0
  22. package/compiler/vlbuild/vlbuild/assets/vlrt.css +165 -0
  23. package/compiler/vlbuild/vlbuild/assets/vlrt.js +1291 -0
  24. package/compiler/vlbuild/vlbuild/emit.py +2023 -0
  25. package/compiler/vlbuild/vlbuild/server_emit.py +1551 -0
  26. package/compiler/vlbuild/vlbuild/static_assets.py +83 -0
  27. package/compiler/vlbuild/vlbuild/style_config.py +347 -0
  28. package/compiler/vlbuild/vlbuild/styling.py +39 -0
  29. package/examples/station.vl +2 -2
  30. package/language/crib.md +131 -10
  31. package/language/spec.md +209 -7
  32. package/language/supported.md +185 -0
  33. package/lib/env.js +107 -0
  34. package/package.json +19 -2
  35. package/scripts/record-provenance.mjs +51 -0
  36. package/scripts/selftest.mjs +54 -0
  37. package/scripts/sync-compiler.sh +52 -0
package/README.md CHANGED
@@ -1,46 +1,138 @@
1
1
  # Volaro
2
2
 
3
- **Pre-release language reference CLI compiler not included.**
3
+ **Limited alpha, published as `0.1.0-alpha.1` under the `alpha` dist-tag** —
4
+ `npm install volaro@alpha`. Not `latest` (that still resolves to an earlier
5
+ `0.0.x` placeholder) — always specify `@alpha` or the exact version.
4
6
 
5
7
  Volaro is an experimental application language intended for AI authoring and
6
- human review. This package contains its specification, authoring crib and
7
- examples. It does not include the repository's prototype compiler.
8
+ human review. This package ships the language reference **and a working
9
+ compiler for a supported subset**: `volaro check`, `volaro build`,
10
+ `volaro dev`.
8
11
 
9
- ## Use the reference
12
+ The command is **`volaro`** (canonical). **`vl` is a compatibility alias** for
13
+ the same binary. Run it via your project's `npm run …` scripts or
14
+ `npx --no-install volaro …` inside the project — a bare `npx volaro` / `npx vl`
15
+ from elsewhere may resolve an unrelated package.
16
+
17
+ ## What you need
18
+
19
+ - **Python 3.10+** — runs the compiler, which is bundled inside this package
20
+ (`compiler/`). `volaro` finds `python3` on your PATH; set `VOLARO_PYTHON` to
21
+ choose a different interpreter. A missing or too-old Python is reported with
22
+ the fix, not a stack trace.
23
+ - **Node.js** — runs the `volaro` command and the dev server. The minimum
24
+ depends on what you build:
25
+
26
+ | App shape | Node | Verified in CI |
27
+ |---|---|---|
28
+ | Minimal single-page (view only): `check` / `build` / static `dev` | **≥ 18** | 18, 20, 22, 24 (packaging matrix) |
29
+ | Full-stack: a `service` backed by a `model` → generated `server.js` (`node:sqlite`) | **≥ 22.5** | 22, 24 (packaging matrix, full-stack `dev`) |
30
+ | Password-auth: the generated server's built-in Argon2 | **≥ 24.7** | 24 only (`./verify.sh` browser journey) |
31
+
32
+ A green packaging matrix establishes the minimal-app row across 18–24; it
33
+ does **not** by itself establish the full-stack or auth rows on 18/20.
34
+
35
+ ### Operating systems
36
+
37
+ | OS | Status |
38
+ |---|---|
39
+ | **Linux** | **Verified** — CI (`verify` + `packaging` matrix) and an isolated-install test |
40
+ | **macOS** | **Unverified** — never run. No known blocker, but no evidence. |
41
+ | **Windows** | **Unverified** — the `npm.cmd` / `PYTHONPATH` handling is present but untested; the packaging test scripts are `bash`. |
42
+
43
+ Do not treat this as cross-platform support. Only Linux has been tested.
44
+
45
+ No global install, no repository checkout, and no network access are needed
46
+ once the package is installed.
47
+
48
+ ## Commands
49
+
50
+ ```bash
51
+ volaro check <path>... # validate .vl source (full resolver), exit non-zero on error
52
+ volaro build app.vl # transpile to ./build (index.html + app.js + runtime)
53
+ volaro build app.vl -o dist --release
54
+ volaro dev # build ./app.vl, serve http://127.0.0.1:5173, rebuild on change
55
+ volaro crib # the authoring reference (write from this)
56
+ volaro supported # what THIS version accepts (the shipped-feature guide)
57
+ volaro spec # the full language design (a superset of this build)
58
+ volaro example sensors # a worked example (also: station)
59
+ volaro version | volaro help
60
+ ```
61
+
62
+ `volaro dev` serves a static bundle for a single-page app. If the app compiles
63
+ to a full-stack server (`server.js`), it runs that instead and needs
64
+ Node 22.5+ for `node:sqlite`.
65
+
66
+ ## Scaffold a project
10
67
 
11
68
  ```bash
12
- npx volaro crib
13
- npx volaro spec
14
- npx volaro example sensors
15
- npx volaro example station
69
+ npm create volaro@alpha my-app # the separate create-volaro package
16
70
  ```
17
71
 
18
- The CLI command is `vl`. The crib is approximately 2.8k o200k_base tokens.
72
+ (`@alpha` matters `npm create volaro` with no tag resolves `latest`, which
73
+ is still an earlier `0.0.x` placeholder, not this build.)
74
+
75
+ It writes one single-page starter, a `volaro.json`, and `package.json` scripts
76
+ (`dev` / `build` / `check`) that call `volaro`.
19
77
 
20
78
  ## Scope and evidence
21
79
 
22
- The project-authored twelve-feature corpus records roughly 3.00× source density
23
- against selected baselines. This is not a general productivity result.
80
+ The project-authored twelve-feature corpus records roughly 3.00× source
81
+ density against selected baselines not a general productivity result.
24
82
  Security and accessibility checks cover a tested prototype subset, not a
25
- universal guarantee. Independent human-readability validation remains outstanding.
83
+ universal guarantee. Independent human-readability validation is outstanding.
26
84
 
27
- ## Not included
85
+ **MVP scope is single-page.** Routing, nested layouts and multi-page starters
86
+ are later work; `volaro build` / `volaro dev` target one entry file.
28
87
 
29
- `vl new`, `vl check`, `vl build` and `vl dev` are not provided by this package.
30
- The repository has a working compiler for a supported subset, but distributing
31
- that toolchain is separate work. Styling and project creation are not supplied
32
- by this reference CLI.
88
+ ## Not included
33
89
 
34
- `npm create volaro` currently runs a separate placeholder package: it prints
35
- a status message and does not create an application.
90
+ Migrations, deployable production output, a specified stdlib surface, and
91
+ escape hatches to npm packages (email, payments, storage) are not here. Styled
92
+ builds (`theme` / `recipe`) additionally need a one-time
93
+ `npm ci` inside `compiler/vlbuild/styling`; the single-page starter does not
94
+ use them.
36
95
 
37
- The legacy `volara` npm package is unchanged by this release.
96
+ **Form controls:** the element set is `text` inputs plus `button` / `link`
97
+ there is no `checkbox`, `radio`, `select`, `textarea`, or `disabled` attribute.
98
+ Model a toggle as a `button` reading a `state` bool. `variant:` is a literal
99
+ style name, not a computed expression. `if` used as an expression is binary
100
+ (`if c a else b`); `match` and block `if` are statements only. A module-level
101
+ `fn` is not available to a view. **`volaro supported` and `volaro crib` are
102
+ authoritative** for what this build accepts — `volaro spec` describes the
103
+ wider language design, not this subset.
38
104
 
39
105
  ## Local testing
40
106
 
41
- Run `node bin/vl.js crib` from this package directory, or use `npm pack` and
42
- install the tarball into a temporary project. No global installation is required.
107
+ ```bash
108
+ npm pack # runs prepack -> vendors compiler/ -> volaro-*.tgz
109
+ npm test # scripts/selftest.mjs: check + build in a temp dir
110
+ ```
111
+
112
+ The legacy `volara` npm package is unrelated to this release.
43
113
 
44
114
  ## License
45
115
 
46
116
  MIT
117
+
118
+
119
+ ## Local assets
120
+
121
+ Put public files in `assets/` beside your entry `.vl` file and use
122
+ `img src:"assets/logo.svg" alt:"Logo"`. Build and dev copy them to the browser
123
+ bundle. Dev watches changes; refresh the browser after a rebuild. Missing
124
+ literal asset references fail the build. Output `assets/` is compiler-owned.
125
+
126
+ ## Package provenance
127
+
128
+ `compiler/SOURCE_REV` records the source commit. `compiler/SOURCE_INFO.json`
129
+ adds its origin, Git dirty status where available, and a SHA-256 digest of
130
+ compiler and CLI/documentation content. A package version alone does not
131
+ identify a local candidate; preserve the tarball hash as well.
132
+
133
+ Packaging a Git export uses `.volaro-source-rev`, expanded by `git archive`.
134
+ For other exported source, set `VOLARO_SOURCE_REV` to the full source commit
135
+ hash before `npm pack`. Missing/invalid revisions fail packaging; an explicit
136
+ revision cannot override a different checkout HEAD. Export metadata identifies
137
+ the base revision, not an assurance that someone has not modified the export;
138
+ the content hash distinguishes modified candidates. Nothing is published by packing.
package/bin/vl.js CHANGED
@@ -1,34 +1,472 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "node:fs";
2
+ // The `volaro` command (alias: `vl`): a thin Node wrapper around the Volaro compiler, which
3
+ // ships as Python sources under ../compiler and is invoked as a subprocess.
4
+ //
5
+ // volaro check <path>... validate .vl source (lexer / parser / resolver)
6
+ // volaro build <file.vl> transpile to a runnable bundle
7
+ // volaro dev [file.vl] build, serve on localhost, rebuild on change
8
+ // volaro crib | volaro spec print the language reference
9
+ // volaro example <name> print a worked example
10
+ // volaro version | volaro help
11
+ //
12
+ // Python is located and version-checked up front with an actionable message
13
+ // (see ../lib/env.js). Nothing here depends on the Volaro repository layout.
14
+
15
+ import { readFileSync, existsSync, statSync, lstatSync, readdirSync } from "node:fs";
3
16
  import { fileURLToPath } from "node:url";
4
- import { dirname, join } from "node:path";
17
+ import { dirname, join, resolve as resolvePath, extname, relative, sep, delimiter } from "node:path";
18
+ import { spawn, spawnSync } from "node:child_process";
19
+ import { createServer } from "node:http";
20
+ import { resolvePython, nodeCanRunServer, VolaroEnvError, MIN_NODE_FOR_SERVER } from "../lib/env.js";
5
21
 
6
22
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
7
23
  const read = (p) => readFileSync(join(root, p), "utf8");
8
- const { version } = JSON.parse(read("package.json"));
9
- const cmd = process.argv[2];
10
-
11
- if (cmd === "crib") process.stdout.write(read("language/crib.md"));
12
- else if (cmd === "spec") process.stdout.write(read("language/spec.md"));
13
- else if (cmd === "example") {
14
- const n = process.argv[3];
15
- if (n === "sensors" || n === "station") process.stdout.write(read(`examples/${n}.vl`));
16
- else console.log("usage: vl example sensors|station");
17
- } else {
18
- console.log(`Volaro ${version} pre-release.
19
-
20
- An application language written to be authored by an AI agent and read by a
21
- person: views, HTTP endpoints, outbound API clients and user accounts.
22
-
23
- vl crib the one-page language reference (~1,700 tokens)
24
- vl spec the full language specification
25
- vl example sensors a worked example — models, unions, a fallible fn
26
- vl example station a worked example — a guarded service and a view
27
-
28
- TO AN AI AGENT reading this: run \`vl crib\` and load its output. That is the
29
- complete authoring reference — it is designed to be read once and written from.
30
-
31
- The toolchain (vl new / check / build / dev) is not published yet, so Volaro
32
- written today cannot yet be compiled from this package.
33
- `);
24
+ const pkg = JSON.parse(read("package.json"));
25
+
26
+ // The canonical command is `volaro`; `vl` is a compatibility alias. Messages
27
+ // use whichever name this process was invoked as, defaulting to `volaro`.
28
+ const CLI = (() => {
29
+ const b = (process.argv[1] || "").split(/[\\/]/).pop() || "";
30
+ return b === "vl" || b === "vl.js" ? "vl" : "volaro";
31
+ })();
32
+
33
+ const COMPILER = join(root, "compiler");
34
+ const VALIDATOR_PP = join(COMPILER, "validator");
35
+ const VLBUILD_PP = join(COMPILER, "vlbuild");
36
+
37
+ function fail(msg, code = 1) {
38
+ process.stderr.write((msg.endsWith("\n") ? msg : msg + "\n"));
39
+ process.exit(code);
40
+ }
41
+
42
+ function haveCompiler() {
43
+ return existsSync(join(VALIDATOR_PP, "vlcheck", "__main__.py")) &&
44
+ existsSync(join(VLBUILD_PP, "vlbuild", "__main__.py"));
45
+ }
46
+
47
+ function python() {
48
+ if (!haveCompiler()) {
49
+ fail(
50
+ `${CLI}: the bundled compiler is missing from this package.\n` +
51
+ "If you are working inside the Volaro repo, run:\n" +
52
+ " bash packaging/volaro/scripts/sync-compiler.sh\n" +
53
+ "An installed copy from `npm pack` / a published release always includes it.",
54
+ );
55
+ }
56
+ try {
57
+ return resolvePython();
58
+ } catch (err) {
59
+ if (err instanceof VolaroEnvError) fail(`${CLI}: ` + err.message);
60
+ throw err;
61
+ }
62
+ }
63
+
64
+ // Shared Python-invocation setup for both the streaming and capturing paths.
65
+ function pyInvocation(pp, moduleName, moduleArgs) {
66
+ const py = python();
67
+ const env = {
68
+ ...process.env,
69
+ PYTHONPATH: [pp, VALIDATOR_PP, process.env.PYTHONPATH].filter(Boolean).join(delimiter),
70
+ PYTHONDONTWRITEBYTECODE: "1",
71
+ };
72
+ return { cmd: py.cmd, argv: ["-B", "-m", moduleName, ...moduleArgs], env };
73
+ }
74
+
75
+ // Run a Python module, streaming its output, and exit with its status.
76
+ function runPython(pp, moduleName, moduleArgs) {
77
+ const { cmd, argv, env } = pyInvocation(pp, moduleName, moduleArgs);
78
+ const child = spawn(cmd, argv, { stdio: "inherit", env });
79
+ child.on("error", (e) => fail(`${CLI}: failed to start Python (${cmd}): ${e.message}`));
80
+ child.on("exit", (code, signal) => process.exit(signal ? 1 : code ?? 0));
81
+ }
82
+
83
+ function pyCapture(pp, moduleName, moduleArgs) {
84
+ const { cmd, argv, env } = pyInvocation(pp, moduleName, moduleArgs);
85
+ return spawnSync(cmd, argv, { encoding: "utf8", env });
86
+ }
87
+
88
+ // ---- subcommands -----------------------------------------------------------
89
+
90
+ function cmdCheck(args) {
91
+ if (args.length === 0) fail("usage: volaro check <file-or-dir>... [--release] [--config PATH]");
92
+ // Always resolve: a bare structural pass is weaker than the real check.
93
+ runPython(VALIDATOR_PP, "vlcheck", ["--resolve", ...args]);
94
+ }
95
+
96
+ function parseBuildArgs(args) {
97
+ const out = { source: null, o: null, data: null, release: false, config: null };
98
+ for (let i = 0; i < args.length; i++) {
99
+ const a = args[i];
100
+ if (a === "-o" || a === "--out") out.o = args[++i];
101
+ else if (a === "--data") out.data = args[++i];
102
+ else if (a === "--config") out.config = args[++i];
103
+ else if (a === "--release") out.release = true;
104
+ else if (a.startsWith("-")) fail(`volaro build: unknown option ${a}`);
105
+ else if (!out.source) out.source = a;
106
+ else fail(`volaro build: unexpected argument ${a}`);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function resolveEntry(explicit) {
112
+ if (explicit) {
113
+ if (!existsSync(explicit)) fail(`${CLI}: no such file: ${explicit}`);
114
+ return resolvePath(explicit);
115
+ }
116
+ for (const c of ["app.vl", join("src", "app.vl")]) {
117
+ if (existsSync(c)) return resolvePath(c);
118
+ }
119
+ fail(
120
+ "volaro: no entry file given and no app.vl found in this directory.\n" +
121
+ "Pass one explicitly: volaro build path/to/app.vl",
122
+ );
123
+ }
124
+
125
+ function cmdBuild(args) {
126
+ const b = parseBuildArgs(args);
127
+ const source = resolveEntry(b.source);
128
+ const outDir = b.o ? resolvePath(b.o) : resolvePath("build");
129
+ const pyArgs = [source, "-o", outDir];
130
+ if (b.data) pyArgs.push("--data", resolvePath(b.data));
131
+ if (b.config) pyArgs.push("--config", resolvePath(b.config));
132
+ if (b.release) pyArgs.push("--release");
133
+ runPython(VLBUILD_PP, "vlbuild", pyArgs);
134
+ }
135
+
136
+ // Synchronous single build for `volaro dev`; returns { ok, server } or exits on env error.
137
+ function buildOnce(source, outDir, extra = []) {
138
+ const r = pyCapture(VLBUILD_PP, "vlbuild", [source, "-o", outDir, ...extra]);
139
+ if (r.error) fail(`${CLI}: failed to start Python: ${r.error.message}`);
140
+ if (r.stdout) process.stdout.write(r.stdout);
141
+ if (r.stderr) process.stderr.write(r.stderr);
142
+ return { ok: r.status === 0, server: existsSync(join(outDir, "server.js")) };
143
+ }
144
+
145
+ const MIME = {
146
+ ".html": "text/html; charset=utf-8",
147
+ ".js": "text/javascript; charset=utf-8",
148
+ ".css": "text/css; charset=utf-8",
149
+ ".json": "application/json; charset=utf-8",
150
+ ".svg": "image/svg+xml",
151
+ ".png": "image/png",
152
+ ".jpg": "image/jpeg",
153
+ ".jpeg": "image/jpeg",
154
+ ".webp": "image/webp",
155
+ ".gif": "image/gif",
156
+ ".ico": "image/x-icon",
157
+ ".woff2": "font/woff2",
158
+ ".map": "application/json",
159
+ };
160
+
161
+ function staticServer(dir, port) {
162
+ const base = resolvePath(dir);
163
+ const srv = createServer((req, res) => {
164
+ let rel = decodeURIComponent((req.url || "/").split("?")[0]);
165
+ if (rel.endsWith("/")) rel += "index.html";
166
+ const target = resolvePath(join(base, rel));
167
+ // Contain to `base`: an exact match, or a path that starts with `base` +
168
+ // separator. A bare `startsWith(base)` would also accept a sibling like
169
+ // `<base>-notes/…`.
170
+ if (target !== base && !target.startsWith(base + sep)) {
171
+ res.writeHead(403).end("forbidden");
172
+ return;
173
+ }
174
+ if (!existsSync(target) || statSync(target).isDirectory()) {
175
+ res.writeHead(404, { "content-type": "text/html; charset=utf-8" });
176
+ res.end("<!doctype html><meta charset=utf-8><title>404</title><p>Not found. The dev server serves the built bundle in <code>" + dir + "</code>.");
177
+ return;
178
+ }
179
+ res.writeHead(200, { "content-type": MIME[extname(target)] || "application/octet-stream", "cache-control": "no-store" });
180
+ res.end(readFileSync(target));
181
+ });
182
+ srv.on("error", (e) => {
183
+ if (e.code === "EADDRINUSE") fail(`volaro dev: port ${port} is already in use. Pass --port <n>.`);
184
+ fail(`volaro dev: ${e.message}`);
185
+ });
186
+ srv.listen(port, "127.0.0.1");
187
+ return srv;
188
+ }
189
+
190
+ // Snapshot every .vl file under dir (bounded depth) as path -> "mtime:size".
191
+ // Polling beats fs.watch here: it is identical across platforms and editors
192
+ // (rename-replace, truncate-write and append all show up), which fs.watch is
193
+ // not, and the cost is trivial for a project-sized tree.
194
+ function scanVl(dir, depth = 6, acc = new Map(), excluded = null) {
195
+ let entries;
196
+ try {
197
+ entries = readdirSync(dir, { withFileTypes: true });
198
+ } catch {
199
+ return acc;
200
+ }
201
+ for (const e of entries) {
202
+ if (e.name === "node_modules" || e.name === ".git" || e.name.startsWith(".")) continue;
203
+ const p = join(dir, e.name);
204
+ if (resolvePath(p) === excluded) continue;
205
+ if (e.isDirectory()) {
206
+ if (depth > 0) scanVl(p, depth - 1, acc, excluded);
207
+ } else if (extname(e.name) === ".vl") {
208
+ try {
209
+ const s = statSync(p);
210
+ acc.set(p, `${s.mtimeMs}:${s.size}`);
211
+ } catch {}
212
+ }
213
+ }
214
+ return acc;
215
+ }
216
+
217
+ function snapEqual(a, b) {
218
+ if (a.size !== b.size) return false;
219
+ for (const [k, v] of a) if (b.get(k) !== v) return false;
220
+ return true;
221
+ }
222
+
223
+ // Poll watchDir for .vl changes and call onChange (debounced by the interval).
224
+ // Returns a close() that stops polling.
225
+ function watchVl(watchDir, onChange, outDir) {
226
+ const snapshot = () => {
227
+ const acc = scanVl(watchDir, 6, new Map(), resolvePath(outDir));
228
+ const scanAssets = (dir) => {
229
+ try {
230
+ const s = lstatSync(dir);
231
+ acc.set(dir, `${s.mtimeMs}:${s.ctimeMs}:${s.size}`);
232
+ if (s.isDirectory() && !s.isSymbolicLink()) {
233
+ for (const name of readdirSync(dir)) scanAssets(join(dir, name));
234
+ }
235
+ } catch {}
236
+ };
237
+ scanAssets(join(watchDir, "assets"));
238
+ return acc;
239
+ };
240
+ let prev = snapshot();
241
+ const iv = setInterval(() => {
242
+ const now = snapshot();
243
+ if (!snapEqual(prev, now)) {
244
+ prev = now;
245
+ onChange();
246
+ }
247
+ }, 250);
248
+ iv.unref();
249
+ return () => clearInterval(iv);
250
+ }
251
+
252
+ function cmdDev(args) {
253
+ let source = null,
254
+ port = 5173,
255
+ outDir = null;
256
+ for (let i = 0; i < args.length; i++) {
257
+ const a = args[i];
258
+ if (a === "--port" || a === "-p") port = Number(args[++i]);
259
+ else if (a === "-o" || a === "--out") outDir = args[++i];
260
+ else if (a.startsWith("-")) fail(`volaro dev: unknown option ${a}`);
261
+ else if (!source) source = a;
262
+ }
263
+ if (!Number.isInteger(port) || port < 1 || port > 65535) fail("volaro dev: --port must be 1..65535");
264
+ const entry = resolveEntry(source);
265
+ const out = outDir ? resolvePath(outDir) : resolvePath("build");
266
+ const watchDir = dirname(entry);
267
+
268
+ process.stdout.write(`volaro dev: building ${relative(process.cwd(), entry) || entry}\n`);
269
+ const first = buildOnce(entry, out);
270
+ if (!first.ok) fail("volaro dev: initial build failed. Fix the errors above and re-run.");
271
+
272
+ let shuttingDown = false;
273
+ let stopWatch = () => {};
274
+
275
+ // ---- full-stack: run server.js, restart it on a successful rebuild ----
276
+ if (first.server) {
277
+ if (!nodeCanRunServer()) {
278
+ fail(
279
+ `volaro dev: this app builds a full-stack server (server.js), which needs Node ` +
280
+ `${MIN_NODE_FOR_SERVER.join(".")}+ for node:sqlite. You have ${process.versions.node}.`,
281
+ );
282
+ }
283
+ const serverPath = join(out, "server.js");
284
+ let child = null;
285
+ let restarting = false;
286
+
287
+ const startServer = () => {
288
+ child = spawn(process.execPath, [serverPath], {
289
+ stdio: "inherit",
290
+ env: { ...process.env, PORT: String(port) },
291
+ });
292
+ child.on("exit", (code, signal) => {
293
+ if (shuttingDown || restarting) return;
294
+ // the server died on its own (e.g. a port clash it reported to stderr)
295
+ stopWatch();
296
+ process.exit(signal ? 1 : code ?? 0);
297
+ });
298
+ };
299
+
300
+ const restartServer = () =>
301
+ new Promise((done) => {
302
+ if (!child) return done();
303
+ restarting = true;
304
+ child.once("exit", () => {
305
+ restarting = false;
306
+ done();
307
+ });
308
+ child.kill("SIGTERM");
309
+ });
310
+
311
+ process.stdout.write(`volaro dev: full-stack app — starting ${relative(process.cwd(), serverPath) || serverPath}\n`);
312
+ startServer();
313
+
314
+ stopWatch = watchVl(watchDir, async () => {
315
+ process.stdout.write("volaro dev: change detected — rebuilding\n");
316
+ const r = buildOnce(entry, out);
317
+ if (!r.ok) {
318
+ process.stdout.write("volaro dev: build failed — server left running on the last good build\n");
319
+ return;
320
+ }
321
+ await restartServer();
322
+ startServer();
323
+ process.stdout.write("volaro dev: server restarted\n");
324
+ }, out);
325
+
326
+ const bye = () => {
327
+ if (shuttingDown) return;
328
+ shuttingDown = true;
329
+ stopWatch();
330
+ if (child) child.kill("SIGTERM");
331
+ setTimeout(() => {
332
+ if (child) child.kill("SIGKILL");
333
+ process.exit(0);
334
+ }, 2000).unref();
335
+ if (child) child.once("exit", () => process.exit(0));
336
+ else process.exit(0);
337
+ };
338
+ process.on("SIGINT", bye);
339
+ process.on("SIGTERM", bye);
340
+ return;
341
+ }
342
+
343
+ // ---- static single-page: serve the bundle, rebuild on change ----
344
+ const srv = staticServer(out, port);
345
+ process.stdout.write(`volaro dev: serving http://127.0.0.1:${port} (Ctrl+C to stop)\n`);
346
+
347
+ stopWatch = watchVl(watchDir, () => {
348
+ process.stdout.write("volaro dev: change detected — rebuilding\n");
349
+ const r = buildOnce(entry, out);
350
+ process.stdout.write(r.ok ? "volaro dev: rebuilt\n" : "volaro dev: build failed — keeping last good bundle\n");
351
+ }, out);
352
+
353
+ const bye = () => {
354
+ if (shuttingDown) return;
355
+ shuttingDown = true;
356
+ stopWatch();
357
+ srv.close(() => process.exit(0));
358
+ setTimeout(() => process.exit(0), 1000).unref();
359
+ };
360
+ process.on("SIGINT", bye);
361
+ process.on("SIGTERM", bye);
362
+ }
363
+
364
+ function cmdReference(which) {
365
+ if (which === "spec") {
366
+ // `volaro-language-spec.md` is the whole-language DESIGN — a superset of
367
+ // what this build accepts. `supported.md` is the version-specific
368
+ // shipped-feature guide; `crib.md` is the authoring reference.
369
+ process.stderr.write(
370
+ `note: \`${CLI} spec\` is the full language design — a SUPERSET of this build.\n` +
371
+ ` \`${CLI} supported\` is what version ${pkg.version} actually accepts;\n` +
372
+ ` \`${CLI} crib\` is the authoring reference.\n\n`,
373
+ );
374
+ }
375
+ const file = which === "supported" ? "supported" : which;
376
+ // `supported.md`'s own title carries this build's exact version as a
377
+ // `{{VERSION}}` placeholder rather than a hardcoded string -- found stale
378
+ // (still reading a prior release's version after a version bump, since
379
+ // nothing kept it in sync) in the Test_003 benchmark's packaged-docs
380
+ // finding. Substituted from `package.json` at print time so it can never
381
+ // drift again, the same way the "supported"-vs-"spec" note above already
382
+ // reads `pkg.version` live rather than a copy-pasted string.
383
+ process.stdout.write(read(`language/${file}.md`).replace("{{VERSION}}", pkg.version));
384
+ }
385
+
386
+ function cmdExample(name) {
387
+ if (name === "sensors" || name === "station") process.stdout.write(read(`examples/${name}.vl`));
388
+ else fail("usage: volaro example sensors|station");
389
+ }
390
+
391
+ const HELP = `Volaro ${pkg.version} — an application language for AI authoring and human review.
392
+
393
+ USAGE
394
+ volaro <command> [args] (installed as both \`volaro\` and \`vl\`;
395
+ \`volaro\` is the canonical name)
396
+
397
+ COMMANDS
398
+ check <path>... Validate .vl source. Runs the full resolver, not just
399
+ a structural pass. Exit non-zero on any error.
400
+ build <file.vl> Transpile to a runnable bundle (default: ./build).
401
+ -o <dir> output directory
402
+ --data <file> data.json to bundle
403
+ --release fail on unresolved placeholders (e.g. img alt_todo:)
404
+ dev [file.vl] Build, serve on http://127.0.0.1:5173, and rebuild
405
+ when a .vl file changes. Defaults to ./app.vl. A
406
+ full-stack build runs server.js instead (Node 22.5+)
407
+ and restarts it after each successful rebuild.
408
+ --port <n> dev server port (passed to server.js as $PORT)
409
+ crib The authoring reference (write from this).
410
+ supported What version ${pkg.version} actually accepts.
411
+ spec The full language design (a SUPERSET of this build).
412
+ example <name> Print a worked example: sensors | station.
413
+ version Print the version.
414
+ help This message.
415
+
416
+ The compiler is Python and ships inside this package. Set VOLARO_PYTHON to
417
+ choose the interpreter if \`python3\` is not the one you want.
418
+
419
+ SHIPPED SUBSET: single page (no router); text \`input\` / \`button\` / \`link\`
420
+ only (no checkbox/select/textarea/disabled — a toggle is a \`button\` + \`state\`,
421
+ not a semantic checkbox); \`if\` as an expression is binary; \`match\` is a
422
+ statement; a module-level \`fn\` cannot be called from a view. \`volaro crib\` +
423
+ \`volaro supported\` are authoritative for this build; \`volaro spec\` is the wider
424
+ design, not what it accepts.
425
+
426
+ Run commands via your project's \`npm run …\` scripts or \`npx --no-install
427
+ volaro …\` inside the project. A bare \`npx volaro …\` from elsewhere may fetch
428
+ the wrong thing.
429
+
430
+ TO AN AI AGENT: run \`volaro crib\` and load its output — that is the complete
431
+ authoring reference. Then write source, run \`volaro check\`, and repair from the
432
+ diagnostics until it is clean.`;
433
+
434
+ // ---- dispatch ------------------------------------------------------------
435
+
436
+ const [cmd, ...rest] = process.argv.slice(2);
437
+ switch (cmd) {
438
+ case "check":
439
+ cmdCheck(rest);
440
+ break;
441
+ case "build":
442
+ cmdBuild(rest);
443
+ break;
444
+ case "dev":
445
+ cmdDev(rest);
446
+ break;
447
+ case "crib":
448
+ cmdReference("crib");
449
+ break;
450
+ case "spec":
451
+ cmdReference("spec");
452
+ break;
453
+ case "supported":
454
+ cmdReference("supported");
455
+ break;
456
+ case "example":
457
+ cmdExample(rest[0]);
458
+ break;
459
+ case "version":
460
+ case "--version":
461
+ case "-v":
462
+ process.stdout.write(pkg.version + "\n");
463
+ break;
464
+ case undefined:
465
+ case "help":
466
+ case "--help":
467
+ case "-h":
468
+ process.stdout.write(HELP + "\n");
469
+ break;
470
+ default:
471
+ fail(`${CLI}: unknown command "${cmd}". Run \`${CLI} help\`.`, 2);
34
472
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "revision": "a450e6fe6549b8c7df43ba343793ab2e52758a78",
3
+ "source": "git",
4
+ "dirty": false,
5
+ "content_sha256": "095f3d5b55826d6aa0c560ce5570c91752d8f03cbd0e869ce61df1fcf53e2593"
6
+ }
@@ -0,0 +1 @@
1
+ a450e6fe6549b8c7df43ba343793ab2e52758a78