beatrina 0.8.6
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/LICENSE +92 -0
- package/NOTICES +72 -0
- package/README.md +124 -0
- package/bin/beatrina.mjs +223 -0
- package/bin/cli.mjs +80 -0
- package/bin/failsafe.mjs +74 -0
- package/bin/identity.mjs +45 -0
- package/bin/prova-post.mjs +51 -0
- package/bin/sessions.mjs +95 -0
- package/bin/shortcut.mjs +151 -0
- package/bin/update-check.mjs +55 -0
- package/bin/upgrade.mjs +76 -0
- package/build-info.json +1 -0
- package/carmar_V0.8.6.html +1310 -0
- package/check/acceptance.mjs +278 -0
- package/check/session.mjs +215 -0
- package/engines/js/document-exec.mjs +82 -0
- package/engines/js/persist.mjs +214 -0
- package/engines/js/worker.mjs +424 -0
- package/engines/python/adapter.py +577 -0
- package/engines/python/analyze.py +814 -0
- package/engines/python/bootstrap.py +309 -0
- package/engines/python/dataview.py +735 -0
- package/engines/python/debugger.py +346 -0
- package/engines/python/document_exec.py +158 -0
- package/engines/python/engine.json +28 -0
- package/engines/python/handoff.py +118 -0
- package/engines/python/worker.py +564 -0
- package/engines/r/engine.json +25 -0
- package/engines/r/handoff.R +92 -0
- package/failsafe/ai-policy.R +255 -0
- package/failsafe/ai-store.R +373 -0
- package/failsafe/cite.R +418 -0
- package/failsafe/journal.R +684 -0
- package/failsafe/plugins.R +809 -0
- package/failsafe/serve.R +5500 -0
- package/host/ai-policy.mjs +218 -0
- package/host/deployment.mjs +160 -0
- package/host/engine-js.mjs +98 -0
- package/host/engine-pool.mjs +383 -0
- package/host/engine-python.mjs +228 -0
- package/host/engine-r.mjs +206 -0
- package/host/engine-stdio.mjs +401 -0
- package/host/journal-store.mjs +749 -0
- package/host/main.mjs +503 -0
- package/host/planes/README.md +41 -0
- package/host/planes/ai-store.mjs +327 -0
- package/host/planes/ai.mjs +467 -0
- package/host/planes/analyze.mjs +397 -0
- package/host/planes/cite.mjs +517 -0
- package/host/planes/files.mjs +0 -0
- package/host/planes/jobs.mjs +704 -0
- package/host/planes/journal.mjs +53 -0
- package/host/planes/latex.mjs +201 -0
- package/host/planes/mcp.mjs +493 -0
- package/host/planes/pair.mjs +325 -0
- package/host/planes/pipe-term.mjs +122 -0
- package/host/planes/plugins.mjs +112 -0
- package/host/planes/proc-tree.mjs +76 -0
- package/host/planes/sessions.mjs +434 -0
- package/host/planes/settings.mjs +164 -0
- package/host/planes/terminal.mjs +286 -0
- package/host/planes/test-file.mjs +80 -0
- package/host/planes/update.mjs +214 -0
- package/host/plugin-store.mjs +838 -0
- package/host/server.mjs +441 -0
- package/host/settings.mjs +379 -0
- package/host/update-record.mjs +59 -0
- package/host/user-dirs.mjs +117 -0
- package/host/windows-runtime.mjs +145 -0
- package/host/worker-plane.mjs +713 -0
- package/host/ws.mjs +190 -0
- package/kernel/analyze.R +668 -0
- package/kernel/deployment.R +165 -0
- package/kernel/examples/NOTICE.md +38 -0
- package/kernel/examples/tna-complete-tutorial.Rmd +210 -0
- package/kernel/fileio.R +656 -0
- package/kernel/index.html +96 -0
- package/kernel/job-run.R +391 -0
- package/kernel/jobs.R +276 -0
- package/kernel/kernel-protocol +1 -0
- package/kernel/kernel-version +1 -0
- package/kernel/kernel.R +671 -0
- package/kernel/knitr-run.R +245 -0
- package/kernel/latex.R +609 -0
- package/kernel/mcp/carmar-mcp.mjs +516 -0
- package/kernel/notebook-page.R +67 -0
- package/kernel/plugins/csl/apa/apa.csl +2273 -0
- package/kernel/plugins/csl/apa/plugin.json +19 -0
- package/kernel/plugins/csl/chicago-author-date/chicago-author-date.csl +4216 -0
- package/kernel/plugins/csl/chicago-author-date/plugin.json +19 -0
- package/kernel/plugins/csl/harvard-cite-them-right/harvard-cite-them-right.csl +316 -0
- package/kernel/plugins/csl/harvard-cite-them-right/plugin.json +19 -0
- package/kernel/plugins/csl/ieee/ieee.csl +519 -0
- package/kernel/plugins/csl/ieee/plugin.json +19 -0
- package/kernel/plugins/csl/modern-language-association/modern-language-association.csl +1184 -0
- package/kernel/plugins/csl/modern-language-association/plugin.json +19 -0
- package/kernel/plugins/csl/nature/nature.csl +189 -0
- package/kernel/plugins/csl/nature/plugin.json +19 -0
- package/kernel/plugins/latex/apa7/apa7.json +14 -0
- package/kernel/plugins/latex/apa7/plugin.json +19 -0
- package/kernel/plugins/latex/elsarticle/elsarticle.json +14 -0
- package/kernel/plugins/latex/elsarticle/plugin.json +19 -0
- package/kernel/plugins/latex/ieeetran/ieeetran.json +10 -0
- package/kernel/plugins/latex/ieeetran/plugin.json +19 -0
- package/kernel/project.R +131 -0
- package/kernel/settings.R +410 -0
- package/kernel/sniff.R +769 -0
- package/kernel/worker-boot.R +22 -0
- package/kernel/worker.R +3496 -0
- package/lib/agent-authoring-contract.js +547 -0
- package/lib/cell-kinds.js +108 -0
- package/lib/engine-labels.js +324 -0
- package/package.json +32 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// persist.mjs — make a chunk's top-level declarations outlive the chunk.
|
|
2
|
+
//
|
|
3
|
+
// A notebook runs chunk after chunk in ONE scope, and a chunk is re-run many
|
|
4
|
+
// times. JavaScript's scripts fight both halves:
|
|
5
|
+
//
|
|
6
|
+
// · top-level `let`/`const`/`class` in a classic script DO persist in the
|
|
7
|
+
// context — and re-running the chunk then throws "Identifier 'x' has already
|
|
8
|
+
// been declared". So the second Run of any chunk would fail.
|
|
9
|
+
// · a chunk that uses top-level `await` cannot be a classic script at all; it
|
|
10
|
+
// has to run inside an async function, where every declaration is LOCAL and
|
|
11
|
+
// vanishes when the chunk ends.
|
|
12
|
+
//
|
|
13
|
+
// The Node REPL solves both with an internal acorn pass that is not exported.
|
|
14
|
+
// This is the smallest honest equivalent: a tokenizer that knows strings,
|
|
15
|
+
// template literals (with nesting), comments and regular-expression literals,
|
|
16
|
+
// and rewrites ONLY declarations at brace depth 0 —
|
|
17
|
+
//
|
|
18
|
+
// let / const → var (re-declarable; `var` persists in the context)
|
|
19
|
+
// class X {…} → var X = class X {…};
|
|
20
|
+
//
|
|
21
|
+
// and, in async mode, reports the names declared at depth 0 so the worker can
|
|
22
|
+
// copy them to the context's global object when the chunk ends (finally). The
|
|
23
|
+
// names of a destructuring declaration are collected too. What this does NOT
|
|
24
|
+
// do, stated rather than discovered: `const` loses its immutability (as it
|
|
25
|
+
// does in the REPL's own editor mode), and a name declared by destructuring
|
|
26
|
+
// with a computed key is not collected.
|
|
27
|
+
|
|
28
|
+
const ID_START = /[A-Za-z_$ -]/;
|
|
29
|
+
const ID_PART = /[A-Za-z0-9_$ -]/;
|
|
30
|
+
// After these tokens a `/` starts a regular expression, not a division.
|
|
31
|
+
const REGEX_AFTER_WORDS = new Set(["return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw", "case", "do", "else", "yield", "await"]);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Tokenize enough of JavaScript to find declarations safely.
|
|
35
|
+
* @returns {Array<{type: "word"|"punct"|"string"|"template"|"regex"|"comment"|"space"|"number", text: string, start: number, depth: number}>}
|
|
36
|
+
*/
|
|
37
|
+
export function tokenize(src) {
|
|
38
|
+
const toks = [];
|
|
39
|
+
let i = 0;
|
|
40
|
+
let depth = 0; // { } depth; parens and brackets do not change it
|
|
41
|
+
const templateStack = []; // brace depth at which each `${` opened
|
|
42
|
+
let lastSignificant = null;
|
|
43
|
+
const push = (type, start, end) => {
|
|
44
|
+
const t = { type, text: src.slice(start, end), start, depth };
|
|
45
|
+
toks.push(t);
|
|
46
|
+
if (type !== "space" && type !== "comment") lastSignificant = t;
|
|
47
|
+
};
|
|
48
|
+
const regexAllowed = () => {
|
|
49
|
+
const t = lastSignificant;
|
|
50
|
+
if (!t) return true;
|
|
51
|
+
if (t.type === "word") return REGEX_AFTER_WORDS.has(t.text);
|
|
52
|
+
if (t.type === "number" || t.type === "string" || t.type === "template" || t.type === "regex") return false;
|
|
53
|
+
return !/^[)\]}]$/.test(t.text);
|
|
54
|
+
};
|
|
55
|
+
const readTemplate = (start) => {
|
|
56
|
+
// from just after ` or after the } that closes a ${ … }
|
|
57
|
+
let j = start;
|
|
58
|
+
while (j < src.length) {
|
|
59
|
+
if (src[j] === "\\") { j += 2; continue; }
|
|
60
|
+
if (src[j] === "`") return { end: j + 1, open: false };
|
|
61
|
+
if (src[j] === "$" && src[j + 1] === "{") return { end: j + 2, open: true };
|
|
62
|
+
j += 1;
|
|
63
|
+
}
|
|
64
|
+
return { end: src.length, open: false };
|
|
65
|
+
};
|
|
66
|
+
while (i < src.length) {
|
|
67
|
+
const c = src[i];
|
|
68
|
+
const start = i;
|
|
69
|
+
if (/\s/.test(c)) { while (i < src.length && /\s/.test(src[i])) i += 1; push("space", start, i); continue; }
|
|
70
|
+
if (c === "/" && src[i + 1] === "/") { while (i < src.length && src[i] !== "\n") i += 1; push("comment", start, i); continue; }
|
|
71
|
+
if (c === "/" && src[i + 1] === "*") { const e = src.indexOf("*/", i + 2); i = e < 0 ? src.length : e + 2; push("comment", start, i); continue; }
|
|
72
|
+
if (c === "'" || c === "\"") {
|
|
73
|
+
i += 1;
|
|
74
|
+
while (i < src.length && src[i] !== c && src[i] !== "\n") i += src[i] === "\\" ? 2 : 1;
|
|
75
|
+
i += 1; push("string", start, Math.min(i, src.length)); continue;
|
|
76
|
+
}
|
|
77
|
+
if (c === "`") {
|
|
78
|
+
const r = readTemplate(i + 1);
|
|
79
|
+
i = r.end;
|
|
80
|
+
push("template", start, i);
|
|
81
|
+
if (r.open) { templateStack.push(depth); depth += 1; }
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (c === "}" && templateStack.length && templateStack[templateStack.length - 1] === depth - 1) {
|
|
85
|
+
depth -= 1;
|
|
86
|
+
templateStack.pop();
|
|
87
|
+
const r = readTemplate(i + 1);
|
|
88
|
+
i = r.end;
|
|
89
|
+
push("template", start, i);
|
|
90
|
+
if (r.open) { templateStack.push(depth); depth += 1; }
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (c === "/" && regexAllowed()) {
|
|
94
|
+
i += 1;
|
|
95
|
+
let inClass = false;
|
|
96
|
+
while (i < src.length && src[i] !== "\n") {
|
|
97
|
+
if (src[i] === "\\") { i += 2; continue; }
|
|
98
|
+
if (src[i] === "[") inClass = true;
|
|
99
|
+
else if (src[i] === "]") inClass = false;
|
|
100
|
+
else if (src[i] === "/" && !inClass) break;
|
|
101
|
+
i += 1;
|
|
102
|
+
}
|
|
103
|
+
i += 1;
|
|
104
|
+
while (i < src.length && ID_PART.test(src[i])) i += 1;
|
|
105
|
+
push("regex", start, i); continue;
|
|
106
|
+
}
|
|
107
|
+
if (/[0-9]/.test(c) || (c === "." && /[0-9]/.test(src[i + 1] || ""))) {
|
|
108
|
+
while (i < src.length && /[0-9A-Za-z_.]/.test(src[i])) i += 1;
|
|
109
|
+
push("number", start, i); continue;
|
|
110
|
+
}
|
|
111
|
+
if (ID_START.test(c)) { while (i < src.length && ID_PART.test(src[i])) i += 1; push("word", start, i); continue; }
|
|
112
|
+
if (c === "{") { push("punct", start, i + 1); depth += 1; i += 1; continue; }
|
|
113
|
+
if (c === "}") { depth = Math.max(0, depth - 1); i += 1; push("punct", start, i); continue; }
|
|
114
|
+
const three = src.slice(i, i + 3);
|
|
115
|
+
const two = src.slice(i, i + 2);
|
|
116
|
+
const len = ["...", "===", "!==", "**=", "<<=", ">>=", "&&=", "||=", "??="].includes(three) ? 3
|
|
117
|
+
: ["=>", "==", "!=", "<=", ">=", "&&", "||", "??", "?.", "++", "--", "+=", "-=", "*=", "/=", "%=", "**"].includes(two) ? 2 : 1;
|
|
118
|
+
i += len;
|
|
119
|
+
push("punct", start, i);
|
|
120
|
+
}
|
|
121
|
+
return toks;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Identifiers bound by a destructuring pattern between `open` and its closer. */
|
|
125
|
+
function patternNames(toks, open) {
|
|
126
|
+
const names = [];
|
|
127
|
+
let level = 0;
|
|
128
|
+
let k = open;
|
|
129
|
+
for (; k < toks.length; k += 1) {
|
|
130
|
+
const t = toks[k];
|
|
131
|
+
if (t.type === "space" || t.type === "comment") continue;
|
|
132
|
+
if (t.text === "{" || t.text === "[") level += 1;
|
|
133
|
+
else if (t.text === "}" || t.text === "]") { level -= 1; if (level === 0) break; }
|
|
134
|
+
else if (t.type === "word") {
|
|
135
|
+
const next = toks.slice(k + 1).find((x) => x.type !== "space" && x.type !== "comment");
|
|
136
|
+
const prev = [...toks.slice(0, k)].reverse().find((x) => x.type !== "space" && x.type !== "comment");
|
|
137
|
+
if (next && next.text === ":") continue; // a property key
|
|
138
|
+
if (prev && prev.text === "=") continue; // a default value's first word (approximation)
|
|
139
|
+
names.push(t.text);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { names, end: k };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Rewrite a chunk so its top-level declarations persist and re-run cleanly.
|
|
147
|
+
* @returns {{code: string, names: string[]}} names declared at depth 0
|
|
148
|
+
*/
|
|
149
|
+
export function persistDeclarations(src) {
|
|
150
|
+
const toks = tokenize(src);
|
|
151
|
+
const edits = []; // {at, remove, insert}
|
|
152
|
+
const names = new Set();
|
|
153
|
+
const significant = (from) => { for (let k = from; k < toks.length; k += 1) if (toks[k].type !== "space" && toks[k].type !== "comment") return k; return -1; };
|
|
154
|
+
const prevSignificant = (from) => { for (let k = from; k >= 0; k -= 1) if (toks[k].type !== "space" && toks[k].type !== "comment") return toks[k]; return null; };
|
|
155
|
+
for (let k = 0; k < toks.length; k += 1) {
|
|
156
|
+
const t = toks[k];
|
|
157
|
+
if (t.type !== "word" || t.depth !== 0) continue;
|
|
158
|
+
const prev = prevSignificant(k - 1);
|
|
159
|
+
// A declaration starts a statement: after `;`, `{`, `}`, at the start, or
|
|
160
|
+
// after a line break that ends a statement without a semicolon. Never
|
|
161
|
+
// `x.let`, never `for (let …` (its `(` is the previous token).
|
|
162
|
+
const newlineBefore = toks[k - 1] && toks[k - 1].type === "space" && toks[k - 1].text.includes("\n");
|
|
163
|
+
const atStatement = !prev || (prev.type === "punct" ? [";", "{", "}"].includes(prev.text) : newlineBefore);
|
|
164
|
+
if (!atStatement) continue;
|
|
165
|
+
if (t.text === "let" || t.text === "const" || t.text === "var") {
|
|
166
|
+
const n = significant(k + 1);
|
|
167
|
+
if (n < 0) continue;
|
|
168
|
+
const nt = toks[n];
|
|
169
|
+
// `let` followed by something that cannot start a binding is an identifier named let.
|
|
170
|
+
if (!(nt.type === "word" || nt.text === "{" || nt.text === "[")) continue;
|
|
171
|
+
if (t.text !== "var") edits.push({ at: t.start, remove: t.text.length, insert: "var" });
|
|
172
|
+
// Collect every binding of the declaration list at this level.
|
|
173
|
+
let m = n;
|
|
174
|
+
let paren = 0;
|
|
175
|
+
let expectBinding = true;
|
|
176
|
+
for (; m < toks.length; m += 1) {
|
|
177
|
+
const x = toks[m];
|
|
178
|
+
if (x.type === "space" || x.type === "comment") { if (paren === 0 && x.depth === 0 && /\n/.test(x.text) && !expectBinding) { const nx = significant(m + 1); if (nx < 0 || ![",", "=", "."].includes(toks[nx].text) && toks[nx].type === "word") break; } continue; }
|
|
179
|
+
if (x.depth > 0 && !(x.text === "{" && expectBinding)) continue;
|
|
180
|
+
if (expectBinding && paren === 0) {
|
|
181
|
+
if (x.type === "word") { names.add(x.text); expectBinding = false; continue; }
|
|
182
|
+
if (x.text === "{" || x.text === "[") { const p = patternNames(toks, m); p.names.forEach((nm) => names.add(nm)); m = p.end; expectBinding = false; continue; }
|
|
183
|
+
}
|
|
184
|
+
if (x.text === "(" || x.text === "[") paren += 1;
|
|
185
|
+
else if (x.text === ")" || x.text === "]") paren -= 1;
|
|
186
|
+
else if (paren === 0 && x.text === ",") expectBinding = true;
|
|
187
|
+
else if (paren === 0 && x.text === ";") break;
|
|
188
|
+
}
|
|
189
|
+
k = Math.max(k, n);
|
|
190
|
+
} else if (t.text === "class") {
|
|
191
|
+
const n = significant(k + 1);
|
|
192
|
+
if (n < 0 || toks[n].type !== "word" || toks[n].text === "extends") continue;
|
|
193
|
+
const name = toks[n].text;
|
|
194
|
+
// Find the class body's closing brace: the first `{` at depth 0 opens it.
|
|
195
|
+
let open = -1;
|
|
196
|
+
for (let m = n + 1; m < toks.length; m += 1) if (toks[m].text === "{" && toks[m].depth === 0) { open = m; break; }
|
|
197
|
+
if (open < 0) continue;
|
|
198
|
+
let close = -1;
|
|
199
|
+
for (let m = open + 1; m < toks.length; m += 1) if (toks[m].text === "}" && toks[m].depth === 0) { close = m; break; }
|
|
200
|
+
if (close < 0) continue;
|
|
201
|
+
edits.push({ at: t.start, remove: 0, insert: `var ${name} = ` });
|
|
202
|
+
edits.push({ at: toks[close].start + 1, remove: 0, insert: ";" });
|
|
203
|
+
names.add(name);
|
|
204
|
+
} else if (t.text === "function" || (t.text === "async" && toks[significant(k + 1)]?.text === "function")) {
|
|
205
|
+
let n = significant(k + 1);
|
|
206
|
+
if (t.text === "async") n = significant(n + 1);
|
|
207
|
+
if (n >= 0 && toks[n].text === "*") n = significant(n + 1);
|
|
208
|
+
if (n >= 0 && toks[n].type === "word") names.add(toks[n].text);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
let code = src;
|
|
212
|
+
for (const e of edits.sort((a, b) => b.at - a.at)) code = code.slice(0, e.at) + e.insert + code.slice(e.at + e.remove);
|
|
213
|
+
return { code, names: [...names] };
|
|
214
|
+
}
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// worker.mjs — the JavaScript engine: one Node process, one persistent scope.
|
|
2
|
+
//
|
|
3
|
+
// It speaks the wire spike/worker.R and engines/python/worker.py speak
|
|
4
|
+
// (docs/worker-protocol.md): control frames on stdout behind the session's
|
|
5
|
+
// sentinel, commands on stdin behind the command tag, Stop as SIGINT. The
|
|
6
|
+
// host adapter is host/engine-js.mjs.
|
|
7
|
+
//
|
|
8
|
+
// Decisions, each with its reason:
|
|
9
|
+
//
|
|
10
|
+
// · ONE vm CONTEXT for the session, not the worker's own globals, so a chunk
|
|
11
|
+
// that assigns `console` or `process.stdout.write` cannot take the wire down.
|
|
12
|
+
// The context still gets the real `process`, `require` (resolved from the
|
|
13
|
+
// working directory) and dynamic `import()` through Node's own loader —
|
|
14
|
+
// a notebook that cannot load a package is not a notebook.
|
|
15
|
+
// · DECLARATIONS PERSIST AND RE-RUN (engines/js/persist.mjs): top-level
|
|
16
|
+
// let/const/class become var-bound, and a chunk using top-level await runs
|
|
17
|
+
// in an async function whose declared names are copied out when it ends.
|
|
18
|
+
// · STOP KEEPS THE SESSION. `breakOnSigint` interrupts synchronous code
|
|
19
|
+
// (a tight loop) with the context intact — the host's group SIGINT reaches
|
|
20
|
+
// it exactly as it reaches R. An await in progress is abandoned: the chunk
|
|
21
|
+
// ends `interrupted` and a timer the user started keeps running in the
|
|
22
|
+
// background, as a promise cannot be cancelled. Code a timer runs LATER is
|
|
23
|
+
// outside any Stop; Force Stop ends the process.
|
|
24
|
+
// · WHAT A CHUNK SHOWS is its last expression, as R and IPython do: an array
|
|
25
|
+
// of plain objects is a table (the `dataframe` frame R's worker sends),
|
|
26
|
+
// `display.html/svg/table/text` are explicit, anything else is
|
|
27
|
+
// util.inspect. A Promise as the last value is awaited first.
|
|
28
|
+
// · SHARE/USE is the ONLY bridge between languages, and it is explicit:
|
|
29
|
+
// `share("name", rows)` writes a column table to the host's hand-off
|
|
30
|
+
// directory and R's `use("name")` reads it as a data.frame (and the other
|
|
31
|
+
// way). Numbers travel as JSON doubles with full precision.
|
|
32
|
+
|
|
33
|
+
import fs from "node:fs";
|
|
34
|
+
import os from "node:os";
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
import readline from "node:readline";
|
|
37
|
+
import util from "node:util";
|
|
38
|
+
import vm from "node:vm";
|
|
39
|
+
import { createRequire } from "node:module";
|
|
40
|
+
import { persistDeclarations } from "./persist.mjs";
|
|
41
|
+
import { DocumentExecError, DocumentSetups, FEATURE } from "./document-exec.mjs";
|
|
42
|
+
|
|
43
|
+
const SENTINEL = process.env.CARMAR_SENTINEL || "";
|
|
44
|
+
const CMD_TAG = process.env.CARMAR_CMD_TAG || "";
|
|
45
|
+
const HANDOFF_DIR = process.env.CARMAR_HANDOFF_DIR || "";
|
|
46
|
+
const COMMANDS = ["exec", "wd", "doctor", "env", "rm", "parse", "complete"];
|
|
47
|
+
const PREVIEW_ROWS = 200;
|
|
48
|
+
const PREVIEW_COLS = 20;
|
|
49
|
+
const CELL_CHARS = 160;
|
|
50
|
+
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_.]*$/;
|
|
51
|
+
|
|
52
|
+
// ── the wire ─────────────────────────────────────────────────────────────────
|
|
53
|
+
const realStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
54
|
+
// A setup chunk's output is hidden (knitr include = FALSE); the gate is how.
|
|
55
|
+
const emitGate = { filter: null };
|
|
56
|
+
function emit(frame) {
|
|
57
|
+
if (emitGate.filter && !emitGate.filter(frame)) return;
|
|
58
|
+
let body;
|
|
59
|
+
try {
|
|
60
|
+
body = JSON.stringify(frame, (_k, v) => (typeof v === "bigint" ? Number(v) : v));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
body = JSON.stringify({ type: frame.type || "error", id: frame.id ?? null, error: `this engine produced a frame that is not JSON: ${e.message}` });
|
|
63
|
+
}
|
|
64
|
+
realStdoutWrite(`${SENTINEL}${body}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── the session ──────────────────────────────────────────────────────────────
|
|
68
|
+
let current = null; // { wire, interrupted, onInterrupt }
|
|
69
|
+
const docs = new DocumentSetups();
|
|
70
|
+
|
|
71
|
+
function text(kind, args) {
|
|
72
|
+
const line = util.formatWithOptions({ colors: false, depth: 4 }, ...args);
|
|
73
|
+
if (current) emit({ type: kind, id: current.wire, text: line });
|
|
74
|
+
else (kind === "stderr" ? process.stderr : process.stdout).write(`${line}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const sessionConsole = {
|
|
78
|
+
log: (...a) => text("stdout", a), info: (...a) => text("stdout", a), debug: (...a) => text("stdout", a),
|
|
79
|
+
warn: (...a) => text("stderr", a), error: (...a) => text("stderr", a),
|
|
80
|
+
dir: (v) => text("stdout", [util.inspect(v, { depth: 4 })]),
|
|
81
|
+
table: (v) => display.table(v),
|
|
82
|
+
time() {}, timeEnd() {}, group() {}, groupEnd() {},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** A cell value → a short printable description, as R's safe_column does. */
|
|
86
|
+
function cell(v) {
|
|
87
|
+
if (v === null || v === undefined) return null;
|
|
88
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
89
|
+
if (typeof v === "boolean") return v;
|
|
90
|
+
if (typeof v === "bigint") return Number(v);
|
|
91
|
+
if (typeof v === "string") return v.length > CELL_CHARS ? `${v.slice(0, CELL_CHARS - 1)}…` : v;
|
|
92
|
+
if (v instanceof Date) return v.toISOString();
|
|
93
|
+
return `<${Array.isArray(v) ? `Array(${v.length})` : (v.constructor && v.constructor.name) || "Object"}>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const isPlainRow = (r) => r !== null && typeof r === "object" && !Array.isArray(r) && !(r instanceof Date);
|
|
97
|
+
function isTable(v) {
|
|
98
|
+
if (!Array.isArray(v) || v.length === 0) return false;
|
|
99
|
+
return v.slice(0, PREVIEW_ROWS).every(isPlainRow);
|
|
100
|
+
}
|
|
101
|
+
/** A columns object {a: [...], b: [...]} with equal lengths → rows. */
|
|
102
|
+
function columnsToRows(obj) {
|
|
103
|
+
const keys = Object.keys(obj);
|
|
104
|
+
if (!keys.length || !keys.every((k) => Array.isArray(obj[k]))) return null;
|
|
105
|
+
const n = obj[keys[0]].length;
|
|
106
|
+
if (!keys.every((k) => obj[k].length === n)) return null;
|
|
107
|
+
return Array.from({ length: n }, (_x, i) => Object.fromEntries(keys.map((k) => [k, obj[k][i]])));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function tableFrame(rows, source) {
|
|
111
|
+
const names = [];
|
|
112
|
+
for (const r of rows.slice(0, PREVIEW_ROWS)) for (const k of Object.keys(r)) if (!names.includes(k)) names.push(k);
|
|
113
|
+
const shownNames = names.slice(0, PREVIEW_COLS);
|
|
114
|
+
const shown = rows.slice(0, PREVIEW_ROWS);
|
|
115
|
+
const types = shownNames.map((k) => {
|
|
116
|
+
const vals = shown.map((r) => r[k]).filter((x) => x !== null && x !== undefined);
|
|
117
|
+
if (vals.length && vals.every((x) => typeof x === "number" || typeof x === "bigint")) return "numeric";
|
|
118
|
+
if (vals.length && vals.every((x) => typeof x === "boolean")) return "logical";
|
|
119
|
+
return "character";
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
type: "dataframe", id: current ? current.wire : null,
|
|
123
|
+
columns: shownNames, types, nrow: rows.length, ncol: names.length,
|
|
124
|
+
shown: shown.length, shownCols: shownNames.length,
|
|
125
|
+
truncated: rows.length > shown.length || names.length > shownNames.length,
|
|
126
|
+
...(source ? { source } : {}),
|
|
127
|
+
rows: shown.map((r) => Object.fromEntries(shownNames.map((k) => [k, cell(r[k])]))),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function show(value, source = null) {
|
|
132
|
+
if (value === undefined) return;
|
|
133
|
+
if (isTable(value)) return emit(tableFrame(value, source));
|
|
134
|
+
text("stdout", [util.inspect(value, { depth: 4, colors: false, maxArrayLength: 100, breakLength: 100 })]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const display = Object.assign((v) => show(v), {
|
|
138
|
+
table(v) {
|
|
139
|
+
const rows = Array.isArray(v) ? v : (v && typeof v === "object" ? columnsToRows(v) : null);
|
|
140
|
+
if (!rows || !isTable(rows)) throw new TypeError("display.table() takes an array of row objects or an object of equal-length columns.");
|
|
141
|
+
emit(tableFrame(rows, null));
|
|
142
|
+
},
|
|
143
|
+
html(html) {
|
|
144
|
+
emit({ type: "widget", id: current ? current.wire : null, kind: "html", class: "html", html: String(html) });
|
|
145
|
+
},
|
|
146
|
+
svg(svg, { width = 672, height = 480 } = {}) {
|
|
147
|
+
emit({ type: "plot", id: current ? current.wire : null, mime: "image/svg+xml", width, height, res: 96, data: String(svg) });
|
|
148
|
+
},
|
|
149
|
+
text(v) { text("stdout", [String(v)]); },
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── share / use ──────────────────────────────────────────────────────────────
|
|
153
|
+
function handoffPath(name) {
|
|
154
|
+
if (!HANDOFF_DIR) throw new Error("share()/use() need the Beatrina host's hand-off directory; this worker was started without one.");
|
|
155
|
+
if (typeof name !== "string" || !NAME_RE.test(name)) throw new TypeError("A shared name is letters, digits, dots and underscores, starting with a letter or underscore.");
|
|
156
|
+
return path.join(HANDOFF_DIR, `${name}.json`);
|
|
157
|
+
}
|
|
158
|
+
function share(a, b) {
|
|
159
|
+
// share(name, value) or share(value, name): whichever order the author reaches for.
|
|
160
|
+
const [name, value] = typeof a === "string" ? [a, b] : [b, a];
|
|
161
|
+
const rows = Array.isArray(value) ? value : (value && typeof value === "object" ? columnsToRows(value) : null);
|
|
162
|
+
if (!rows || !(rows.length === 0 || isTable(rows))) throw new TypeError("share(name, value) takes an array of row objects or an object of equal-length columns.");
|
|
163
|
+
const columns = [];
|
|
164
|
+
for (const r of rows) for (const k of Object.keys(r)) if (!columns.includes(k)) columns.push(k);
|
|
165
|
+
const data = Object.fromEntries(columns.map((k) => [k, rows.map((r) => {
|
|
166
|
+
const v = r[k];
|
|
167
|
+
if (v === undefined || v === null) return null;
|
|
168
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
169
|
+
if (v instanceof Date) return v.toISOString();
|
|
170
|
+
return typeof v === "object" ? JSON.stringify(v) : v;
|
|
171
|
+
})]));
|
|
172
|
+
const file = handoffPath(name);
|
|
173
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
174
|
+
fs.writeFileSync(tmp, JSON.stringify({ format: "beatrix-columns-v1", name, from: "js", columns, data }), { mode: 0o600 });
|
|
175
|
+
fs.renameSync(tmp, file);
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
function use(name) {
|
|
179
|
+
const file = handoffPath(name);
|
|
180
|
+
if (!fs.existsSync(file)) throw new Error(`Nothing has been shared as "${name}" in this session. Share it first with share("${name}", …) in JavaScript or share(${name}) in R.`);
|
|
181
|
+
const rec = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
182
|
+
if (rec.format !== "beatrix-columns-v1") throw new Error(`"${name}" is not a Beatrina hand-off table.`);
|
|
183
|
+
const n = rec.columns.length ? rec.data[rec.columns[0]].length : 0;
|
|
184
|
+
return Array.from({ length: n }, (_x, i) => Object.fromEntries(rec.columns.map((k) => [k, rec.data[k][i]])));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── the context ──────────────────────────────────────────────────────────────
|
|
188
|
+
const sandbox = {
|
|
189
|
+
console: sessionConsole, display, share, use, process,
|
|
190
|
+
setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate, queueMicrotask, structuredClone,
|
|
191
|
+
fetch, URL, URLSearchParams, TextEncoder, TextDecoder, Buffer, AbortController, AbortSignal, performance,
|
|
192
|
+
crypto: globalThis.crypto, Blob, atob, btoa,
|
|
193
|
+
};
|
|
194
|
+
Object.defineProperty(sandbox, "require", {
|
|
195
|
+
get: () => createRequire(path.join(process.cwd(), "__beatrina_chunk__.js")), enumerable: false, configurable: true,
|
|
196
|
+
});
|
|
197
|
+
const context = vm.createContext(sandbox, { name: "Beatrina JavaScript session" });
|
|
198
|
+
const BUILTINS = new Set([...Object.getOwnPropertyNames(vm.runInContext("globalThis", context)), "require"]);
|
|
199
|
+
const userNames = () => Object.getOwnPropertyNames(vm.runInContext("globalThis", context)).filter((k) => !BUILTINS.has(k));
|
|
200
|
+
|
|
201
|
+
const INTERRUPTED = Symbol("interrupted");
|
|
202
|
+
process.on("SIGINT", () => {
|
|
203
|
+
// Idle: nothing to stop (the host's ladder may send one after a run ended).
|
|
204
|
+
if (current && !current.interrupted) { current.interrupted = true; if (current.onInterrupt) current.onInterrupt(); }
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/** Evaluate one chunk in the session. @returns {"ok"|"error"|"interrupted"} */
|
|
208
|
+
async function evaluate(source, wire, srcname) {
|
|
209
|
+
current = { wire, interrupted: false, onInterrupt: null, failure: null };
|
|
210
|
+
const run = current;
|
|
211
|
+
const filename = path.join(process.cwd(), `${srcname && NAME_RE.test(srcname) ? srcname : "chunk"}.js`);
|
|
212
|
+
const opts = { filename, importModuleDynamically: vm.constants.USE_MAIN_CONTEXT_DEFAULT_LOADER };
|
|
213
|
+
const { code, names } = persistDeclarations(source);
|
|
214
|
+
let script;
|
|
215
|
+
let asyncMode = false;
|
|
216
|
+
try {
|
|
217
|
+
script = new vm.Script(code, opts);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
if (e instanceof SyntaxError && /await is only valid/.test(e.message)) {
|
|
220
|
+
asyncMode = true;
|
|
221
|
+
const copy = names.map((n) => `try { globalThis[${JSON.stringify(n)}] = ${n}; } catch {}`).join(" ");
|
|
222
|
+
try { script = new vm.Script(`(async () => { try {\n${code}\n} finally { ${copy} } })()`, { ...opts, lineOffset: -1 }); } catch (e2) { return fail(run, e2); }
|
|
223
|
+
} else return fail(run, e);
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
let value = script.runInContext(context, { breakOnSigint: true, displayErrors: false });
|
|
227
|
+
if (value && typeof value.then === "function") {
|
|
228
|
+
value = await new Promise((resolve, reject) => {
|
|
229
|
+
run.onInterrupt = () => resolve(INTERRUPTED);
|
|
230
|
+
if (run.interrupted) resolve(INTERRUPTED);
|
|
231
|
+
value.then(resolve, reject);
|
|
232
|
+
});
|
|
233
|
+
if (value === INTERRUPTED) return "interrupted";
|
|
234
|
+
}
|
|
235
|
+
if (run.interrupted) return "interrupted";
|
|
236
|
+
if (!asyncMode) {
|
|
237
|
+
const last = source.trim().split("\n").pop().trim().replace(/;$/, "");
|
|
238
|
+
show(value, /^[A-Za-z_$][\w$]*$/.test(last) ? last : null);
|
|
239
|
+
}
|
|
240
|
+
return "ok";
|
|
241
|
+
} catch (e) {
|
|
242
|
+
if (e && e.code === "ERR_SCRIPT_EXECUTION_INTERRUPTED") return "interrupted";
|
|
243
|
+
return fail(run, e);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function fail(run, e) {
|
|
248
|
+
const headline = e && e.name ? `${e.name}: ${e.message}` : String(e);
|
|
249
|
+
run.failure = headline;
|
|
250
|
+
const stack = e && typeof e.stack === "string" ? e.stack.split("\n").filter((l) => !/node:internal|engines[\\/]js[\\/]worker\.mjs/.test(l)).join("\n") : headline;
|
|
251
|
+
const m = e && typeof e.stack === "string" ? e.stack.match(/\.js:(\d+)(?::\d+)?\)?\s*$/m) : null;
|
|
252
|
+
emit({ type: "stderr", id: run.wire, text: stack });
|
|
253
|
+
emit({ type: "traceback", id: run.wire, message: headline, line: m ? Number(m[1]) : null, call: e && e.name ? e.name : "Error", frames: [] });
|
|
254
|
+
return "error";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── commands ─────────────────────────────────────────────────────────────────
|
|
258
|
+
async function exec(cmd) {
|
|
259
|
+
const wire = cmd.id;
|
|
260
|
+
const source = typeof cmd.source === "string" ? cmd.source : (typeof cmd.code === "string" ? cmd.code : null);
|
|
261
|
+
const doneFrame = (fields) => { current = null; emit({ type: "done", id: wire, cwd: process.cwd(), ...fields }); };
|
|
262
|
+
if (source === null) return doneFrame({ status: "error", message: "no source" });
|
|
263
|
+
const engine = cmd.engine == null ? "js" : String(cmd.engine).toLowerCase();
|
|
264
|
+
if (!["js", "javascript", "node"].includes(engine)) {
|
|
265
|
+
return doneFrame({ status: "error", message: `This engine runs JavaScript; it was asked to run a ${cmd.engine} chunk.` });
|
|
266
|
+
}
|
|
267
|
+
const lines = source.split("\n").length;
|
|
268
|
+
if (cmd.document == null) {
|
|
269
|
+
emit({ type: "progress", id: wire, index: 1, total: 1, line: 1, end: lines });
|
|
270
|
+
const status = await evaluate(source, wire, cmd.srcname);
|
|
271
|
+
return doneFrame({ status, message: status === "interrupted" ? "Execution interrupted" : (current && current.failure) || null });
|
|
272
|
+
}
|
|
273
|
+
const done = { status: "ok", message: null, setup: null, cacheHit: false, effectiveOptions: null };
|
|
274
|
+
let plan;
|
|
275
|
+
try { plan = docs.plan(source, cmd.document); } catch (e) {
|
|
276
|
+
if (e instanceof DocumentExecError) return doneFrame({ ...done, status: "error", message: e.message });
|
|
277
|
+
throw e;
|
|
278
|
+
}
|
|
279
|
+
done.execution = plan.execution;
|
|
280
|
+
if (plan.runSetup) {
|
|
281
|
+
docs.setupStarted(plan.docId);
|
|
282
|
+
const report = { source: plan.setupSource, srcname: plan.setupSrcname, status: "running" };
|
|
283
|
+
emit({ type: "setup", id: wire, ...report });
|
|
284
|
+
emit({ type: "progress", id: wire, index: 1, total: 1, line: 1, end: plan.setupSource.split("\n").length, phase: "setup" });
|
|
285
|
+
// knitr include = FALSE: the setup's output is hidden, its failure is not.
|
|
286
|
+
const status = await withHiddenOutput(() => evaluate(plan.setupSource, wire, plan.setupSrcname));
|
|
287
|
+
const why = current && current.failure;
|
|
288
|
+
if (status !== "ok") {
|
|
289
|
+
docs.setupFailed(plan.docId, plan.setupSource);
|
|
290
|
+
report.status = status;
|
|
291
|
+
return doneFrame({ ...done, status, setup: report, message: `Setup chunk failed; dependent chunk was not run. ${why || `Chunk ${status}`}` });
|
|
292
|
+
}
|
|
293
|
+
docs.setupOk(plan.docId, plan.setupSource);
|
|
294
|
+
report.status = "ok";
|
|
295
|
+
done.setup = report;
|
|
296
|
+
} else if (plan.setupSource === null) {
|
|
297
|
+
docs.noSetup(plan.docId);
|
|
298
|
+
}
|
|
299
|
+
if (plan.runChunk) {
|
|
300
|
+
emit({ type: "progress", id: wire, index: 1, total: 1, line: 1, end: lines });
|
|
301
|
+
done.status = await evaluate(source, wire, cmd.srcname);
|
|
302
|
+
done.message = done.status === "interrupted" ? "Execution interrupted" : (current && current.failure) || null;
|
|
303
|
+
}
|
|
304
|
+
return doneFrame(done);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Run with stdout/table/html frames suppressed; stderr and tracebacks pass. */
|
|
308
|
+
async function withHiddenOutput(fn) {
|
|
309
|
+
const hidden = new Set(["stdout", "dataframe", "widget", "plot"]);
|
|
310
|
+
const original = emitGate.filter;
|
|
311
|
+
emitGate.filter = (f) => !hidden.has(f.type);
|
|
312
|
+
try { return await fn(); } finally { emitGate.filter = original; }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function describe(name) {
|
|
316
|
+
const v = context[name];
|
|
317
|
+
const kind = typeof v === "function" ? "function" : isTable(v) ? "data" : "value";
|
|
318
|
+
const cls = v === null ? "null" : Array.isArray(v) ? "Array" : typeof v === "object" ? (v.constructor && v.constructor.name) || "Object" : typeof v;
|
|
319
|
+
const summary = kind === "data" ? `${v.length} rows of ${Object.keys(v[0]).length} fields`
|
|
320
|
+
: kind === "function" ? `function(${(String(v).match(/^[^(]*\(([^)]*)\)/) || [0, ""])[1]})`
|
|
321
|
+
: util.inspect(v, { depth: 0, breakLength: Infinity, maxArrayLength: 5, maxStringLength: 60 }).slice(0, 120);
|
|
322
|
+
const base = { name, class: cls, dims: Array.isArray(v) ? String(v.length) : "1", bytes: null, summary, kind };
|
|
323
|
+
return kind === "data" ? { ...base, nrow: v.length, ncol: Object.keys(v[0]).length } : base;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function complete(line, cursor) {
|
|
327
|
+
const upto = line.slice(0, cursor);
|
|
328
|
+
const m = upto.match(/([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\.?([A-Za-z_$][\w$]*)?$/);
|
|
329
|
+
if (!m) return { start: cursor, end: cursor, token: "", items: [], truncated: false };
|
|
330
|
+
const dotted = upto.endsWith(".") ? `${m[1]}.` : m[0];
|
|
331
|
+
const parts = dotted.split(".");
|
|
332
|
+
const prefix = parts.pop();
|
|
333
|
+
let target = vm.runInContext("globalThis", context);
|
|
334
|
+
for (const p of parts) {
|
|
335
|
+
// Property reads only — never a call. A getter can still run, as in the REPL.
|
|
336
|
+
try { target = target == null ? undefined : target[p]; } catch { target = undefined; }
|
|
337
|
+
}
|
|
338
|
+
const keys = new Set();
|
|
339
|
+
for (let o = target; o != null && keys.size < 2000; o = Object.getPrototypeOf(o)) {
|
|
340
|
+
for (const k of Object.getOwnPropertyNames(o)) if (k.startsWith(prefix) && !/^__/.test(k)) keys.add(k);
|
|
341
|
+
if (o === Object.prototype) break;
|
|
342
|
+
}
|
|
343
|
+
const all = [...keys].sort();
|
|
344
|
+
const items = all.slice(0, 200).map((k) => {
|
|
345
|
+
let v;
|
|
346
|
+
try { v = target[k]; } catch { v = undefined; }
|
|
347
|
+
return { value: k, kind: typeof v === "function" ? "function" : "variable", detail: "" };
|
|
348
|
+
});
|
|
349
|
+
return { start: cursor - prefix.length, end: cursor, token: prefix, items, truncated: all.length > 200 };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function dispatch(cmd) {
|
|
353
|
+
const wire = cmd.id;
|
|
354
|
+
switch (cmd.type) {
|
|
355
|
+
case "exec": return exec(cmd);
|
|
356
|
+
case "wd":
|
|
357
|
+
if (typeof cmd.path === "string" && cmd.path) { try { process.chdir(cmd.path); } catch (e) { return emit({ type: "wd", id: wire, path: process.cwd(), error: e.message }); } }
|
|
358
|
+
return emit({ type: "wd", id: wire, path: process.cwd() });
|
|
359
|
+
case "doctor":
|
|
360
|
+
return emit({ type: "doctor", id: wire, language: "javascript", version: process.version, executable: process.execPath, platform: `${os.type()} ${os.release()} ${process.arch}`, cwd: process.cwd(), handoff: HANDOFF_DIR || null });
|
|
361
|
+
case "env":
|
|
362
|
+
return emit({ type: "env", id: wire, objects: userNames().map(describe) });
|
|
363
|
+
case "rm": {
|
|
364
|
+
const names = Array.isArray(cmd.names) ? cmd.names : (typeof cmd.name === "string" ? [cmd.name] : []);
|
|
365
|
+
for (const n of names) if (typeof n === "string" && !BUILTINS.has(n)) delete context[n];
|
|
366
|
+
return emit({ type: "rm", id: wire, removed: names });
|
|
367
|
+
}
|
|
368
|
+
case "parse": {
|
|
369
|
+
let complete_ = true;
|
|
370
|
+
let error = null;
|
|
371
|
+
try { new vm.Script(persistDeclarations(String(cmd.source || "")).code); } catch (e) {
|
|
372
|
+
if (/Unexpected end of input|Unterminated/.test(e.message)) complete_ = false; else if (!/await is only valid/.test(e.message)) error = e.message;
|
|
373
|
+
}
|
|
374
|
+
return emit({ type: "parse", id: wire, complete: complete_, error });
|
|
375
|
+
}
|
|
376
|
+
case "complete":
|
|
377
|
+
return emit({ type: "complete", id: wire, ...complete(String(cmd.line || ""), Number.isInteger(cmd.cursor) ? cmd.cursor : String(cmd.line || "").length) });
|
|
378
|
+
default:
|
|
379
|
+
return undefined; // unknown: silent, as worker.R's dispatch chain is
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── the reader and the queue ─────────────────────────────────────────────────
|
|
384
|
+
const queue = [];
|
|
385
|
+
let pumping = false;
|
|
386
|
+
async function pump() {
|
|
387
|
+
if (pumping) return;
|
|
388
|
+
pumping = true;
|
|
389
|
+
while (queue.length) {
|
|
390
|
+
const cmd = queue.shift();
|
|
391
|
+
try { await dispatch(cmd); } catch (e) {
|
|
392
|
+
if (cmd.type === "exec") { current = null; emit({ type: "done", id: cmd.id, status: "error", message: `${e.name}: ${e.message}`, cwd: process.cwd() }); }
|
|
393
|
+
else if (cmd.id != null) emit({ type: cmd.type, id: cmd.id, error: `${e.name}: ${e.message}` });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
pumping = false;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function accept(line) {
|
|
400
|
+
let body = line.trim();
|
|
401
|
+
if (!body) return;
|
|
402
|
+
const tag = CMD_TAG ? `#${CMD_TAG} ` : "";
|
|
403
|
+
if (tag && body.startsWith(tag)) body = body.slice(tag.length);
|
|
404
|
+
else if (tag && body.startsWith("#")) return;
|
|
405
|
+
let cmd;
|
|
406
|
+
try { cmd = JSON.parse(body); } catch { return; }
|
|
407
|
+
if (!cmd || typeof cmd !== "object" || Array.isArray(cmd) || typeof cmd.type !== "string") return;
|
|
408
|
+
if (cmd.type === "cmdfile") {
|
|
409
|
+
try { cmd = JSON.parse(fs.readFileSync(cmd.path, "utf8")); } catch { return; } finally { try { fs.unlinkSync(cmd.path); } catch { /* taken */ } }
|
|
410
|
+
if (!cmd || typeof cmd.type !== "string") return;
|
|
411
|
+
}
|
|
412
|
+
if (cmd.type === "shutdown") { process.exit(0); }
|
|
413
|
+
if (cmd.type === "interrupt") { process.emit("SIGINT"); return; }
|
|
414
|
+
queue.push(cmd);
|
|
415
|
+
pump();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
readline.createInterface({ input: process.stdin, crlfDelay: Infinity }).on("line", accept).on("close", () => process.exit(0));
|
|
419
|
+
|
|
420
|
+
emit({
|
|
421
|
+
type: "ready", pid: process.pid, language: "js", language_version: process.versions.node,
|
|
422
|
+
engines: ["js"], executable: process.execPath, cwd: process.cwd(), home: os.homedir(), libs: [],
|
|
423
|
+
mode: "batch", features: [FEATURE], commands: COMMANDS, handoff: Boolean(HANDOFF_DIR),
|
|
424
|
+
});
|