velaris-lang 7.1.1 → 8.0.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.
Files changed (5) hide show
  1. package/README.md +71 -71
  2. package/bin/velaris.js +111 -111
  3. package/index.js +127 -127
  4. package/package.json +1 -1
  5. package/python.js +143 -143
package/README.md CHANGED
@@ -1,71 +1,71 @@
1
- # velaris-lang
2
-
3
- Run code you did not write.
4
-
5
- ```
6
- npx velaris-lang script.vel
7
- ```
8
-
9
- That program cannot read a file, reach the network or call Python -
10
- whatever its source says about itself - and a refusal cannot be caught
11
- and carried past. A run with no `--allow` gets `io` (5.0), so that is
12
- the default rather than something you have to remember; widen it by
13
- naming what the program needs, and `--allow all` grants every effect
14
- and says so on stderr.
15
-
16
- ```javascript
17
- import { audit, run } from "velaris-lang";
18
-
19
- const report = await audit(source);
20
- console.log(report.effects); // ['fs', 'net']
21
- console.log(report.proven_share); // 66.7
22
-
23
- const result = await run(source, { allow: ["io"] });
24
- console.log(result.ok, result.output, result.refusedEffect);
25
- ```
26
-
27
- Velaris is a language where a function's signature declares its types,
28
- the effects it may perform, whether it can fail, and promises a theorem
29
- prover checks before the program runs. This package is a thin wrapper
30
- around that compiler.
31
-
32
- Not a security boundary - allowing `ffi` grants everything Python can
33
- do. It is a real guard for running a script a model wrote.
34
-
35
- ## The compiler
36
-
37
- The compiler itself is a Python package. This wrapper calls it, so
38
- install it once:
39
-
40
- ```
41
- pip install velaris-lang
42
- ```
43
-
44
- Two optional pieces are worth having:
45
-
46
- ```
47
- pip install "velaris-lang[full]"
48
- ```
49
-
50
- That adds `z3-solver`, which proves a function's promises before the
51
- program runs instead of leaving them to be checked as it goes, and
52
- `llvmlite`, which compiles pure integer and float code to native code
53
- instead of interpreting it. Neither is required. Without them Velaris
54
- runs fully interpreted and checks promises while running - it prints
55
- `note: llvmlite is not installed - running fully interpreted` and
56
- carries on, refusing effects outside the budget exactly the same way.
57
-
58
- ### Which Python it uses
59
-
60
- The wrapper tries `py`, `python` and `python3` on Windows, `python3`
61
- then `python` elsewhere, asks each one which Velaris it has, and uses
62
- the newest. If the newest it finds is older than this npm package it
63
- still uses it, but says so first, on stderr, naming both versions and
64
- the interpreter - so a stale install left on PATH is visible rather
65
- than mysterious. If you ask for a subcommand that version does not
66
- have, it says which command and which version introduced it instead of
67
- handing it over. `pip install -U velaris-lang` upgrades the compiler.
68
-
69
- [Documentation](https://gowrishankar-infra.github.io/velaris-lang/) ·
70
- [Playground](https://gowrishankar-infra.github.io/velaris-lang/playground.html) ·
71
- [Source](https://github.com/gowrishankar-infra/velaris-lang)
1
+ # velaris-lang
2
+
3
+ Run code you did not write.
4
+
5
+ ```
6
+ npx velaris-lang script.vel
7
+ ```
8
+
9
+ That program cannot read a file, reach the network or call Python -
10
+ whatever its source says about itself - and a refusal cannot be caught
11
+ and carried past. A run with no `--allow` gets `io` (5.0), so that is
12
+ the default rather than something you have to remember; widen it by
13
+ naming what the program needs, and `--allow all` grants every effect
14
+ and says so on stderr.
15
+
16
+ ```javascript
17
+ import { audit, run } from "velaris-lang";
18
+
19
+ const report = await audit(source);
20
+ console.log(report.effects); // ['fs', 'net']
21
+ console.log(report.proven_share); // 66.7
22
+
23
+ const result = await run(source, { allow: ["io"] });
24
+ console.log(result.ok, result.output, result.refusedEffect);
25
+ ```
26
+
27
+ Velaris is a language where a function's signature declares its types,
28
+ the effects it may perform, whether it can fail, and promises a theorem
29
+ prover checks before the program runs. This package is a thin wrapper
30
+ around that compiler.
31
+
32
+ Not a security boundary - allowing `ffi` grants everything Python can
33
+ do. It is a real guard for running a script a model wrote.
34
+
35
+ ## The compiler
36
+
37
+ The compiler itself is a Python package. This wrapper calls it, so
38
+ install it once:
39
+
40
+ ```
41
+ pip install velaris-lang
42
+ ```
43
+
44
+ Two optional pieces are worth having:
45
+
46
+ ```
47
+ pip install "velaris-lang[full]"
48
+ ```
49
+
50
+ That adds `z3-solver`, which proves a function's promises before the
51
+ program runs instead of leaving them to be checked as it goes, and
52
+ `llvmlite`, which compiles pure integer and float code to native code
53
+ instead of interpreting it. Neither is required. Without them Velaris
54
+ runs fully interpreted and checks promises while running - it prints
55
+ `note: llvmlite is not installed - running fully interpreted` and
56
+ carries on, refusing effects outside the budget exactly the same way.
57
+
58
+ ### Which Python it uses
59
+
60
+ The wrapper tries `py`, `python` and `python3` on Windows, `python3`
61
+ then `python` elsewhere, asks each one which Velaris it has, and uses
62
+ the newest. If the newest it finds is older than this npm package it
63
+ still uses it, but says so first, on stderr, naming both versions and
64
+ the interpreter - so a stale install left on PATH is visible rather
65
+ than mysterious. If you ask for a subcommand that version does not
66
+ have, it says which command and which version introduced it instead of
67
+ handing it over. `pip install -U velaris-lang` upgrades the compiler.
68
+
69
+ [Documentation](https://gowrishankar-infra.github.io/velaris-lang/) ·
70
+ [Playground](https://gowrishankar-infra.github.io/velaris-lang/playground.html) ·
71
+ [Source](https://github.com/gowrishankar-infra/velaris-lang)
package/bin/velaris.js CHANGED
@@ -1,111 +1,111 @@
1
- #!/usr/bin/env node
2
- // npx velaris hello.vel (it gets io; --allow to widen)
3
- //
4
- // Velaris's compiler is one Python file. This hands your arguments to
5
- // it, and if it is not installed, says exactly how to fix that rather
6
- // than failing with a confusing spawn error.
7
- //
8
- // It also says when the compiler it found is older than this package.
9
- // Two things can go wrong quietly otherwise: a stale Velaris earlier in
10
- // PATH shadows a newer one, and a subcommand added after that stale
11
- // version came out is read by the old compiler as a file name, so
12
- // `npx velaris-lang mcp` fails with "cannot find file 'mcp'".
13
-
14
- import { spawn } from "node:child_process";
15
- import {
16
- behindWarning,
17
- compareVersions,
18
- findVelaris,
19
- NOT_INSTALLED,
20
- packageVersion,
21
- } from "../python.js";
22
-
23
- // The version each subcommand first shipped in, from the changelog. It
24
- // is what lets this wrapper name a missing command instead of leaving
25
- // the old compiler to mistake it for a file. check_library.py holds it
26
- // to the compiler's own dispatch, so a new subcommand cannot be added
27
- // without an entry here.
28
- const INTRODUCED = {
29
- repl: "1.9",
30
- run: "1.9",
31
- version: "1.9",
32
- fmt: "1.10",
33
- lsp: "1.11",
34
- doctor: "2.2",
35
- new: "2.2",
36
- explain: "2.6",
37
- proofs: "2.6",
38
- check: "2.7",
39
- test: "2.17",
40
- trace: "2.21",
41
- add: "2.23",
42
- deps: "2.23",
43
- verify: "2.23",
44
- build: "2.28",
45
- clean: "2.29",
46
- audit: "2.41.1",
47
- card: "2.41.1",
48
- "mcp-install": "2.53",
49
- serve: "2.54",
50
- "mcp-manifest": "3.4.0",
51
- "mcp-verify": "3.4.0",
52
- capabilities: "4.0.0",
53
- review: "4.0.0",
54
- conformance: "4.1.0",
55
- attest: "4.2.0",
56
- mcp: "4.3.3",
57
- migrate: "5.0.0",
58
- "deps-diff": "7.1.0",
59
- };
60
-
61
- // The two flags the compiler takes before any command, each with a
62
- // value of its own; everything else beginning with "-" is a flag whose
63
- // value, if it has one, is attached. What is left is the subcommand,
64
- // or a file name - which is not in the table, so it is left alone.
65
- const GLOBAL_WITH_VALUE = ["--proof-timeout", "--max-memory-mb"];
66
-
67
- function requestedCommand(args) {
68
- for (let i = 0; i < args.length; i++) {
69
- if (GLOBAL_WITH_VALUE.includes(args[i])) {
70
- i++;
71
- continue;
72
- }
73
- if (args[i].startsWith("-")) continue;
74
- return args[i];
75
- }
76
- return null;
77
- }
78
-
79
- const args = process.argv.slice(2);
80
- const ours = packageVersion();
81
- const found = findVelaris();
82
-
83
- if (!found) {
84
- console.error(NOT_INSTALLED);
85
- process.exit(127);
86
- }
87
-
88
- const wanted = requestedCommand(args);
89
- const needs = wanted ? INTRODUCED[wanted] : null;
90
- if (needs && found.version && compareVersions(found.version, needs) < 0) {
91
- console.error(
92
- `velaris ${wanted} arrived in Velaris ${needs}; the compiler at ` +
93
- `${found.interpreter} is ${found.version}` +
94
- (ours ? `, while this npm package is ${ours}` : "") +
95
- ". Upgrade the compiler with: pip install -U velaris-lang"
96
- );
97
- process.exit(127);
98
- }
99
-
100
- const behind = behindWarning(found, ours);
101
- if (behind) console.error(behind);
102
-
103
- // The interpreter, not the name that found it: the version answer came
104
- // from that executable, so that executable is the one to run.
105
- const child = spawn(found.interpreter, ["-m", "velaris", ...args], {
106
- stdio: "inherit",
107
- });
108
- child.on("exit", (code, signal) => {
109
- if (signal) process.kill(process.pid, signal);
110
- else process.exit(code ?? 0);
111
- });
1
+ #!/usr/bin/env node
2
+ // npx velaris hello.vel (it gets io; --allow to widen)
3
+ //
4
+ // Velaris's compiler is one Python file. This hands your arguments to
5
+ // it, and if it is not installed, says exactly how to fix that rather
6
+ // than failing with a confusing spawn error.
7
+ //
8
+ // It also says when the compiler it found is older than this package.
9
+ // Two things can go wrong quietly otherwise: a stale Velaris earlier in
10
+ // PATH shadows a newer one, and a subcommand added after that stale
11
+ // version came out is read by the old compiler as a file name, so
12
+ // `npx velaris-lang mcp` fails with "cannot find file 'mcp'".
13
+
14
+ import { spawn } from "node:child_process";
15
+ import {
16
+ behindWarning,
17
+ compareVersions,
18
+ findVelaris,
19
+ NOT_INSTALLED,
20
+ packageVersion,
21
+ } from "../python.js";
22
+
23
+ // The version each subcommand first shipped in, from the changelog. It
24
+ // is what lets this wrapper name a missing command instead of leaving
25
+ // the old compiler to mistake it for a file. check_library.py holds it
26
+ // to the compiler's own dispatch, so a new subcommand cannot be added
27
+ // without an entry here.
28
+ const INTRODUCED = {
29
+ repl: "1.9",
30
+ run: "1.9",
31
+ version: "1.9",
32
+ fmt: "1.10",
33
+ lsp: "1.11",
34
+ doctor: "2.2",
35
+ new: "2.2",
36
+ explain: "2.6",
37
+ proofs: "2.6",
38
+ check: "2.7",
39
+ test: "2.17",
40
+ trace: "2.21",
41
+ add: "2.23",
42
+ deps: "2.23",
43
+ verify: "2.23",
44
+ build: "2.28",
45
+ clean: "2.29",
46
+ audit: "2.41.1",
47
+ card: "2.41.1",
48
+ "mcp-install": "2.53",
49
+ serve: "2.54",
50
+ "mcp-manifest": "3.4.0",
51
+ "mcp-verify": "3.4.0",
52
+ capabilities: "4.0.0",
53
+ review: "4.0.0",
54
+ conformance: "4.1.0",
55
+ attest: "4.2.0",
56
+ mcp: "4.3.3",
57
+ migrate: "5.0.0",
58
+ "deps-diff": "7.1.0",
59
+ };
60
+
61
+ // The two flags the compiler takes before any command, each with a
62
+ // value of its own; everything else beginning with "-" is a flag whose
63
+ // value, if it has one, is attached. What is left is the subcommand,
64
+ // or a file name - which is not in the table, so it is left alone.
65
+ const GLOBAL_WITH_VALUE = ["--proof-timeout", "--max-memory-mb"];
66
+
67
+ function requestedCommand(args) {
68
+ for (let i = 0; i < args.length; i++) {
69
+ if (GLOBAL_WITH_VALUE.includes(args[i])) {
70
+ i++;
71
+ continue;
72
+ }
73
+ if (args[i].startsWith("-")) continue;
74
+ return args[i];
75
+ }
76
+ return null;
77
+ }
78
+
79
+ const args = process.argv.slice(2);
80
+ const ours = packageVersion();
81
+ const found = findVelaris();
82
+
83
+ if (!found) {
84
+ console.error(NOT_INSTALLED);
85
+ process.exit(127);
86
+ }
87
+
88
+ const wanted = requestedCommand(args);
89
+ const needs = wanted ? INTRODUCED[wanted] : null;
90
+ if (needs && found.version && compareVersions(found.version, needs) < 0) {
91
+ console.error(
92
+ `velaris ${wanted} arrived in Velaris ${needs}; the compiler at ` +
93
+ `${found.interpreter} is ${found.version}` +
94
+ (ours ? `, while this npm package is ${ours}` : "") +
95
+ ". Upgrade the compiler with: pip install -U velaris-lang"
96
+ );
97
+ process.exit(127);
98
+ }
99
+
100
+ const behind = behindWarning(found, ours);
101
+ if (behind) console.error(behind);
102
+
103
+ // The interpreter, not the name that found it: the version answer came
104
+ // from that executable, so that executable is the one to run.
105
+ const child = spawn(found.interpreter, ["-m", "velaris", ...args], {
106
+ stdio: "inherit",
107
+ });
108
+ child.on("exit", (code, signal) => {
109
+ if (signal) process.kill(process.pid, signal);
110
+ else process.exit(code ?? 0);
111
+ });
package/index.js CHANGED
@@ -1,127 +1,127 @@
1
- // Velaris from Node: check what a program does, audit what it may
2
- // touch, and run it under an effect budget.
3
- //
4
- // import { check, audit, run } from "velaris-lang";
5
- //
6
- // const report = await audit(source);
7
- // console.log(report.effects); // ['fs', 'net']
8
- //
9
- // const result = await run(source, { allow: ["io"] });
10
- // console.log(result.ok, result.output, result.refusedEffect);
11
- //
12
- // Every call goes to the same compiler the command line uses, so the
13
- // guarantees are the same: an effect outside the budget is refused
14
- // while the program runs, whatever its source claims, and a refusal
15
- // cannot be caught by the program.
16
-
17
- import { spawn } from "node:child_process";
18
- import { behindWarning, findVelaris, packageVersion } from "./python.js";
19
-
20
- let cachedPython = null;
21
-
22
- // Said once per process, not once per call: several calls through one
23
- // stale compiler are one mistake, and repeating it would bury whatever
24
- // the caller was printing.
25
- let saidBehind = false;
26
-
27
- // Not the first Python that can import velaris - the one with the
28
- // newest velaris. Taking the first let an old install earlier in PATH
29
- // shadow a newer one, which then answered every call in this module
30
- // with the behaviour of a version the caller did not ask for.
31
- function findPython() {
32
- if (cachedPython) return cachedPython;
33
- const found = findVelaris();
34
- if (!found) {
35
- throw new Error("velaris is not installed: pip install velaris-lang");
36
- }
37
- if (!saidBehind) {
38
- const behind = behindWarning(found, packageVersion());
39
- if (behind) console.error(behind);
40
- saidBehind = true;
41
- }
42
- return (cachedPython = found.interpreter);
43
- }
44
-
45
- function callPython(script, payload) {
46
- const exe = findPython();
47
- return new Promise((resolve, reject) => {
48
- const child = spawn(exe, ["-c", script], {
49
- stdio: ["pipe", "pipe", "pipe"],
50
- });
51
- let out = "";
52
- let err = "";
53
- child.stdout.on("data", (d) => (out += d));
54
- child.stderr.on("data", (d) => (err += d));
55
- child.on("error", reject);
56
- child.on("close", () => {
57
- try {
58
- resolve(JSON.parse(out));
59
- } catch {
60
- reject(new Error(err.trim() || "velaris gave no answer"));
61
- }
62
- });
63
- child.stdin.end(JSON.stringify(payload));
64
- });
65
- }
66
-
67
- const BRIDGE = `
68
- import json, sys
69
- import velaris
70
- ask = json.load(sys.stdin)
71
- what = ask["what"]
72
- source = ask["source"]
73
- if what == "check":
74
- print(json.dumps(velaris.check(source).as_dict()))
75
- elif what == "audit":
76
- print(json.dumps(velaris.audit(source).as_dict()))
77
- elif what == "card":
78
- print(json.dumps({"card": velaris.card()}))
79
- else:
80
- out = velaris.run(source, allow=set(ask.get("allow") or ["io"]),
81
- stdin=ask.get("stdin", ""),
82
- args=ask.get("args") or [])
83
- print(json.dumps(out.as_dict()))
84
- `;
85
-
86
- /** Compile without running. Problems, and what was proven. */
87
- export async function check(source) {
88
- return callPython(BRIDGE, { what: "check", source });
89
- }
90
-
91
- /** What a program can touch, promise and fail at - before running. */
92
- export async function audit(source) {
93
- return callPython(BRIDGE, { what: "audit", source });
94
- }
95
-
96
- /** The language, small enough to paste into a model. */
97
- export async function card() {
98
- const answer = await callPython(BRIDGE, { what: "card", source: "x" });
99
- return answer.card;
100
- }
101
-
102
- /**
103
- * Run under an effect budget.
104
- *
105
- * allow: ["io"] means it cannot read files, reach the network, call
106
- * Python, ask the clock or use randomness - whatever the source says.
107
- * Not a security boundary: allowing "ffi" grants everything Python can.
108
- */
109
- export async function run(source, options = {}) {
110
- const answer = await callPython(BRIDGE, {
111
- what: "run",
112
- source,
113
- allow: options.allow ?? ["io"],
114
- stdin: options.stdin ?? "",
115
- args: options.args ?? [],
116
- });
117
- return {
118
- ok: answer.ok,
119
- output: answer.output,
120
- logs: answer.logs,
121
- problems: answer.problems,
122
- refusedEffect: answer.refused_effect,
123
- exitCode: answer.exit_code,
124
- };
125
- }
126
-
127
- export default { check, audit, run, card };
1
+ // Velaris from Node: check what a program does, audit what it may
2
+ // touch, and run it under an effect budget.
3
+ //
4
+ // import { check, audit, run } from "velaris-lang";
5
+ //
6
+ // const report = await audit(source);
7
+ // console.log(report.effects); // ['fs', 'net']
8
+ //
9
+ // const result = await run(source, { allow: ["io"] });
10
+ // console.log(result.ok, result.output, result.refusedEffect);
11
+ //
12
+ // Every call goes to the same compiler the command line uses, so the
13
+ // guarantees are the same: an effect outside the budget is refused
14
+ // while the program runs, whatever its source claims, and a refusal
15
+ // cannot be caught by the program.
16
+
17
+ import { spawn } from "node:child_process";
18
+ import { behindWarning, findVelaris, packageVersion } from "./python.js";
19
+
20
+ let cachedPython = null;
21
+
22
+ // Said once per process, not once per call: several calls through one
23
+ // stale compiler are one mistake, and repeating it would bury whatever
24
+ // the caller was printing.
25
+ let saidBehind = false;
26
+
27
+ // Not the first Python that can import velaris - the one with the
28
+ // newest velaris. Taking the first let an old install earlier in PATH
29
+ // shadow a newer one, which then answered every call in this module
30
+ // with the behaviour of a version the caller did not ask for.
31
+ function findPython() {
32
+ if (cachedPython) return cachedPython;
33
+ const found = findVelaris();
34
+ if (!found) {
35
+ throw new Error("velaris is not installed: pip install velaris-lang");
36
+ }
37
+ if (!saidBehind) {
38
+ const behind = behindWarning(found, packageVersion());
39
+ if (behind) console.error(behind);
40
+ saidBehind = true;
41
+ }
42
+ return (cachedPython = found.interpreter);
43
+ }
44
+
45
+ function callPython(script, payload) {
46
+ const exe = findPython();
47
+ return new Promise((resolve, reject) => {
48
+ const child = spawn(exe, ["-c", script], {
49
+ stdio: ["pipe", "pipe", "pipe"],
50
+ });
51
+ let out = "";
52
+ let err = "";
53
+ child.stdout.on("data", (d) => (out += d));
54
+ child.stderr.on("data", (d) => (err += d));
55
+ child.on("error", reject);
56
+ child.on("close", () => {
57
+ try {
58
+ resolve(JSON.parse(out));
59
+ } catch {
60
+ reject(new Error(err.trim() || "velaris gave no answer"));
61
+ }
62
+ });
63
+ child.stdin.end(JSON.stringify(payload));
64
+ });
65
+ }
66
+
67
+ const BRIDGE = `
68
+ import json, sys
69
+ import velaris
70
+ ask = json.load(sys.stdin)
71
+ what = ask["what"]
72
+ source = ask["source"]
73
+ if what == "check":
74
+ print(json.dumps(velaris.check(source).as_dict()))
75
+ elif what == "audit":
76
+ print(json.dumps(velaris.audit(source).as_dict()))
77
+ elif what == "card":
78
+ print(json.dumps({"card": velaris.card()}))
79
+ else:
80
+ out = velaris.run(source, allow=set(ask.get("allow") or ["io"]),
81
+ stdin=ask.get("stdin", ""),
82
+ args=ask.get("args") or [])
83
+ print(json.dumps(out.as_dict()))
84
+ `;
85
+
86
+ /** Compile without running. Problems, and what was proven. */
87
+ export async function check(source) {
88
+ return callPython(BRIDGE, { what: "check", source });
89
+ }
90
+
91
+ /** What a program can touch, promise and fail at - before running. */
92
+ export async function audit(source) {
93
+ return callPython(BRIDGE, { what: "audit", source });
94
+ }
95
+
96
+ /** The language, small enough to paste into a model. */
97
+ export async function card() {
98
+ const answer = await callPython(BRIDGE, { what: "card", source: "x" });
99
+ return answer.card;
100
+ }
101
+
102
+ /**
103
+ * Run under an effect budget.
104
+ *
105
+ * allow: ["io"] means it cannot read files, reach the network, call
106
+ * Python, ask the clock or use randomness - whatever the source says.
107
+ * Not a security boundary: allowing "ffi" grants everything Python can.
108
+ */
109
+ export async function run(source, options = {}) {
110
+ const answer = await callPython(BRIDGE, {
111
+ what: "run",
112
+ source,
113
+ allow: options.allow ?? ["io"],
114
+ stdin: options.stdin ?? "",
115
+ args: options.args ?? [],
116
+ });
117
+ return {
118
+ ok: answer.ok,
119
+ output: answer.output,
120
+ logs: answer.logs,
121
+ problems: answer.problems,
122
+ refusedEffect: answer.refused_effect,
123
+ exitCode: answer.exit_code,
124
+ };
125
+ }
126
+
127
+ export default { check, audit, run, card };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "velaris-lang",
3
- "version": "7.1.1",
3
+ "version": "8.0.0",
4
4
  "mcpName": "io.github.gowrishankar-infra/velaris",
5
5
  "description": "Run code you did not write. A language where a signature declares its effects and promises, and the runtime refuses anything you did not allow.",
6
6
  "keywords": [
package/python.js CHANGED
@@ -1,143 +1,143 @@
1
- // Finding the Python that holds the Velaris compiler.
2
- //
3
- // The npm package is a wrapper, not a copy: it finds a Python that can
4
- // import `velaris` and calls it. Until 4.3.4 it took the first Python
5
- // that could import the module at all, which meant an old Velaris in an
6
- // early candidate silently shadowed a newer one further down - and a
7
- // subcommand added after that old version came out looked to the user
8
- // like a missing file rather than a missing compiler.
9
- //
10
- // So: ask every candidate which version it has, and take the newest.
11
- // The caller decides what to say when the newest is still older than
12
- // the package that invoked it; what this module promises is that the
13
- // choice is made on version and that the version is known.
14
-
15
- import { spawnSync } from "node:child_process";
16
- import { readFileSync } from "node:fs";
17
-
18
- // One line of JSON on stdout: the version the module declares, and the
19
- // interpreter it came from. `velaris.VERSION` has been there since 1.0,
20
- // so every real Velaris answers; something importable as `velaris` that
21
- // is not one is reported as a null version rather than as no Velaris at
22
- // all, and ranks below everything that can say what it is.
23
- const PROBE =
24
- "import json,sys,velaris;" +
25
- "sys.stdout.write(json.dumps({" +
26
- '"version": getattr(velaris, "VERSION", None), ' +
27
- '"interpreter": sys.executable}))';
28
-
29
- export function candidates() {
30
- return process.platform === "win32"
31
- ? ["py", "python", "python3"]
32
- : ["python3", "python"];
33
- }
34
-
35
- /** -1, 0 or 1, comparing dotted numeric versions of any length. */
36
- export function compareVersions(a, b) {
37
- const parts = (v) =>
38
- String(v)
39
- .split(".")
40
- .map((n) => {
41
- const x = parseInt(n, 10);
42
- return Number.isFinite(x) ? x : 0;
43
- });
44
- const pa = parts(a);
45
- const pb = parts(b);
46
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
47
- const x = pa[i] ?? 0;
48
- const y = pb[i] ?? 0;
49
- if (x !== y) return x < y ? -1 : 1;
50
- }
51
- return 0;
52
- }
53
-
54
- // A version we could not read loses to every version we could, so an
55
- // unreadable one is never preferred over a known one.
56
- function newer(a, b) {
57
- if (a === null) return false;
58
- if (b === null) return true;
59
- return compareVersions(a, b) > 0;
60
- }
61
-
62
- /**
63
- * The candidate holding the newest Velaris.
64
- *
65
- * Returns null if no candidate has it at all, else { command,
66
- * interpreter, version, all }: `version` is null when the module
67
- * declares none, and `all` is every candidate that had Velaris, in the
68
- * order they were tried.
69
- */
70
- export function findVelaris() {
71
- const found = [];
72
- const seen = new Set();
73
- for (const command of candidates()) {
74
- const probe = spawnSync(command, ["-c", PROBE], { encoding: "utf8" });
75
- if (probe.error || probe.status !== 0) continue;
76
- let answer;
77
- try {
78
- const lines = (probe.stdout || "").trim().split(/\r?\n/).filter(Boolean);
79
- answer = JSON.parse(lines[lines.length - 1]);
80
- } catch {
81
- continue; // it imported but would not say what it is
82
- }
83
- const version =
84
- typeof answer.version === "string" && /^\d+(\.\d+)*$/.test(answer.version)
85
- ? answer.version
86
- : null;
87
- const interpreter = answer.interpreter || command;
88
- // `py` and `python` are often the same interpreter; count it once
89
- // so that "the newest of several" is about several Pythons.
90
- if (seen.has(interpreter)) continue;
91
- seen.add(interpreter);
92
- found.push({ command, interpreter, version });
93
- }
94
- if (found.length === 0) return null;
95
- let best = found[0];
96
- for (const one of found.slice(1)) {
97
- if (newer(one.version, best.version)) best = one;
98
- }
99
- return { ...best, all: found };
100
- }
101
-
102
- /**
103
- * The version of the npm package this file belongs to, or null.
104
- *
105
- * Resolved against this file, which sits beside package.json, so it is
106
- * the same answer wherever it is called from.
107
- */
108
- export function packageVersion() {
109
- try {
110
- const beside = new URL("./package.json", import.meta.url);
111
- return JSON.parse(readFileSync(beside, "utf8")).version || null;
112
- } catch {
113
- return null;
114
- }
115
- }
116
-
117
- /**
118
- * One line for stderr when the compiler is older than the package that
119
- * invoked it, or null when it is not. Never silence: a wrapper running
120
- * a compiler older than itself is the one thing the user cannot see.
121
- */
122
- export function behindWarning(found, ours) {
123
- if (!ours) return null;
124
- if (found.version === null) {
125
- return (
126
- `velaris-lang ${ours} (npm) is using a Velaris at ${found.interpreter} ` +
127
- "that does not say which version it is; upgrade it with: " +
128
- "pip install -U velaris-lang"
129
- );
130
- }
131
- if (compareVersions(found.version, ours) >= 0) return null;
132
- return (
133
- `velaris-lang ${ours} (npm) is using Velaris ${found.version}, from ` +
134
- `${found.interpreter}; upgrade the compiler with: ` +
135
- "pip install -U velaris-lang"
136
- );
137
- }
138
-
139
- export const NOT_INSTALLED =
140
- "Velaris needs its compiler, which is a Python package:\n" +
141
- "\n pip install velaris-lang\n" +
142
- "\nOr try it with nothing installed:\n" +
143
- " https://gowrishankar-infra.github.io/velaris-lang/playground.html";
1
+ // Finding the Python that holds the Velaris compiler.
2
+ //
3
+ // The npm package is a wrapper, not a copy: it finds a Python that can
4
+ // import `velaris` and calls it. Until 4.3.4 it took the first Python
5
+ // that could import the module at all, which meant an old Velaris in an
6
+ // early candidate silently shadowed a newer one further down - and a
7
+ // subcommand added after that old version came out looked to the user
8
+ // like a missing file rather than a missing compiler.
9
+ //
10
+ // So: ask every candidate which version it has, and take the newest.
11
+ // The caller decides what to say when the newest is still older than
12
+ // the package that invoked it; what this module promises is that the
13
+ // choice is made on version and that the version is known.
14
+
15
+ import { spawnSync } from "node:child_process";
16
+ import { readFileSync } from "node:fs";
17
+
18
+ // One line of JSON on stdout: the version the module declares, and the
19
+ // interpreter it came from. `velaris.VERSION` has been there since 1.0,
20
+ // so every real Velaris answers; something importable as `velaris` that
21
+ // is not one is reported as a null version rather than as no Velaris at
22
+ // all, and ranks below everything that can say what it is.
23
+ const PROBE =
24
+ "import json,sys,velaris;" +
25
+ "sys.stdout.write(json.dumps({" +
26
+ '"version": getattr(velaris, "VERSION", None), ' +
27
+ '"interpreter": sys.executable}))';
28
+
29
+ export function candidates() {
30
+ return process.platform === "win32"
31
+ ? ["py", "python", "python3"]
32
+ : ["python3", "python"];
33
+ }
34
+
35
+ /** -1, 0 or 1, comparing dotted numeric versions of any length. */
36
+ export function compareVersions(a, b) {
37
+ const parts = (v) =>
38
+ String(v)
39
+ .split(".")
40
+ .map((n) => {
41
+ const x = parseInt(n, 10);
42
+ return Number.isFinite(x) ? x : 0;
43
+ });
44
+ const pa = parts(a);
45
+ const pb = parts(b);
46
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
47
+ const x = pa[i] ?? 0;
48
+ const y = pb[i] ?? 0;
49
+ if (x !== y) return x < y ? -1 : 1;
50
+ }
51
+ return 0;
52
+ }
53
+
54
+ // A version we could not read loses to every version we could, so an
55
+ // unreadable one is never preferred over a known one.
56
+ function newer(a, b) {
57
+ if (a === null) return false;
58
+ if (b === null) return true;
59
+ return compareVersions(a, b) > 0;
60
+ }
61
+
62
+ /**
63
+ * The candidate holding the newest Velaris.
64
+ *
65
+ * Returns null if no candidate has it at all, else { command,
66
+ * interpreter, version, all }: `version` is null when the module
67
+ * declares none, and `all` is every candidate that had Velaris, in the
68
+ * order they were tried.
69
+ */
70
+ export function findVelaris() {
71
+ const found = [];
72
+ const seen = new Set();
73
+ for (const command of candidates()) {
74
+ const probe = spawnSync(command, ["-c", PROBE], { encoding: "utf8" });
75
+ if (probe.error || probe.status !== 0) continue;
76
+ let answer;
77
+ try {
78
+ const lines = (probe.stdout || "").trim().split(/\r?\n/).filter(Boolean);
79
+ answer = JSON.parse(lines[lines.length - 1]);
80
+ } catch {
81
+ continue; // it imported but would not say what it is
82
+ }
83
+ const version =
84
+ typeof answer.version === "string" && /^\d+(\.\d+)*$/.test(answer.version)
85
+ ? answer.version
86
+ : null;
87
+ const interpreter = answer.interpreter || command;
88
+ // `py` and `python` are often the same interpreter; count it once
89
+ // so that "the newest of several" is about several Pythons.
90
+ if (seen.has(interpreter)) continue;
91
+ seen.add(interpreter);
92
+ found.push({ command, interpreter, version });
93
+ }
94
+ if (found.length === 0) return null;
95
+ let best = found[0];
96
+ for (const one of found.slice(1)) {
97
+ if (newer(one.version, best.version)) best = one;
98
+ }
99
+ return { ...best, all: found };
100
+ }
101
+
102
+ /**
103
+ * The version of the npm package this file belongs to, or null.
104
+ *
105
+ * Resolved against this file, which sits beside package.json, so it is
106
+ * the same answer wherever it is called from.
107
+ */
108
+ export function packageVersion() {
109
+ try {
110
+ const beside = new URL("./package.json", import.meta.url);
111
+ return JSON.parse(readFileSync(beside, "utf8")).version || null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * One line for stderr when the compiler is older than the package that
119
+ * invoked it, or null when it is not. Never silence: a wrapper running
120
+ * a compiler older than itself is the one thing the user cannot see.
121
+ */
122
+ export function behindWarning(found, ours) {
123
+ if (!ours) return null;
124
+ if (found.version === null) {
125
+ return (
126
+ `velaris-lang ${ours} (npm) is using a Velaris at ${found.interpreter} ` +
127
+ "that does not say which version it is; upgrade it with: " +
128
+ "pip install -U velaris-lang"
129
+ );
130
+ }
131
+ if (compareVersions(found.version, ours) >= 0) return null;
132
+ return (
133
+ `velaris-lang ${ours} (npm) is using Velaris ${found.version}, from ` +
134
+ `${found.interpreter}; upgrade the compiler with: ` +
135
+ "pip install -U velaris-lang"
136
+ );
137
+ }
138
+
139
+ export const NOT_INSTALLED =
140
+ "Velaris needs its compiler, which is a Python package:\n" +
141
+ "\n pip install velaris-lang\n" +
142
+ "\nOr try it with nothing installed:\n" +
143
+ " https://gowrishankar-infra.github.io/velaris-lang/playground.html";