velaris-lang 4.3.3 → 7.1.1
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/README.md +71 -57
- package/bin/velaris.js +111 -43
- package/index.js +127 -120
- package/package.json +2 -1
- package/python.js +143 -0
package/README.md
CHANGED
|
@@ -1,57 +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.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
console.log(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
runs
|
|
52
|
-
`
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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,43 +1,111 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// npx velaris hello.vel
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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,120 +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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
child
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
child.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
print(json.dumps(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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": "
|
|
3
|
+
"version": "7.1.1",
|
|
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": [
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"index.js",
|
|
32
32
|
"index.d.ts",
|
|
33
|
+
"python.js",
|
|
33
34
|
"bin/",
|
|
34
35
|
"README.md"
|
|
35
36
|
],
|
package/python.js
ADDED
|
@@ -0,0 +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";
|