beatrina 0.8.7 → 0.9.43
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/NOTICES +1 -1
- package/README.md +6 -0
- package/beatrina_V0.9.43.html +1885 -0
- package/beatrina_V0.9.43.html.inputs.json +1 -0
- package/bin/beatrina.mjs +152 -24
- package/bin/browser.mjs +31 -0
- package/bin/cli.mjs +64 -5
- package/bin/doctor-rows.mjs +31 -0
- package/bin/failsafe.mjs +8 -3
- package/bin/finder.mjs +74 -0
- package/bin/lsquery.swift +55 -0
- package/bin/pages.mjs +81 -0
- package/bin/python-setup.mjs +207 -0
- package/bin/runtime-dirs.mjs +27 -0
- package/bin/sessions.mjs +30 -10
- package/bin/shortcut.mjs +250 -42
- package/bin/update-check.mjs +2 -2
- package/build-info.json +1 -1
- package/engines/js/worker.mjs +6 -3
- package/engines/python/adapter.py +14 -13
- package/engines/python/analyze.py +4 -2
- package/engines/python/bootstrap.py +32 -31
- package/engines/python/debugger.py +10 -6
- package/engines/python/engine.json +1 -1
- package/engines/python/handoff.py +4 -2
- package/engines/python/worker.py +39 -10
- package/engines/r/engine.json +1 -1
- package/engines/r/handoff.R +4 -3
- package/failsafe/ai-policy.R +16 -16
- package/failsafe/ai-store.R +6 -6
- package/failsafe/cite.R +11 -11
- package/failsafe/journal.R +11 -11
- package/failsafe/plugins.R +27 -27
- package/failsafe/serve.R +241 -241
- package/host/ai-authority.mjs +55 -0
- package/host/ai-policy.mjs +48 -26
- package/host/bundle.mjs +194 -0
- package/host/deployment.mjs +25 -18
- package/host/engine-js.mjs +12 -17
- package/host/engine-pool.mjs +48 -6
- package/host/engine-python.mjs +80 -31
- package/host/engine-r.mjs +20 -21
- package/host/engine-stdio.mjs +35 -10
- package/host/env-names.mjs +148 -0
- package/host/gateway-token.mjs +51 -0
- package/host/journal-store.mjs +10 -9
- package/host/main.mjs +193 -77
- package/host/payload.mjs +131 -0
- package/host/planes/README.md +1 -1
- package/host/planes/ai-store.mjs +6 -5
- package/host/planes/ai.mjs +49 -29
- package/host/planes/analyze.mjs +6 -5
- package/host/planes/bundle.mjs +344 -0
- package/host/planes/choose.mjs +404 -0
- package/host/planes/cite.mjs +18 -17
- package/host/planes/files.mjs +0 -0
- package/host/planes/jobs.mjs +43 -20
- package/host/planes/latex.mjs +15 -5
- package/host/planes/mcp.mjs +80 -91
- package/host/planes/pair.mjs +24 -24
- package/host/planes/pipe-term.mjs +2 -2
- package/host/planes/plugins.mjs +1 -1
- package/host/planes/recent-documents.mjs +170 -0
- package/host/planes/sessions.mjs +226 -82
- package/host/planes/settings.mjs +4 -4
- package/host/planes/terminal.mjs +13 -11
- package/host/planes/test-file.mjs +2 -2
- package/host/planes/update.mjs +6 -13
- package/host/plugin-store.mjs +35 -52
- package/host/recent-documents.mjs +124 -0
- package/host/runtime-dir.mjs +47 -0
- package/host/server.mjs +75 -18
- package/host/session-keep.mjs +1 -1
- package/host/settings.mjs +85 -37
- package/host/update-record.mjs +3 -2
- package/host/user-dirs.mjs +60 -18
- package/host/which.mjs +39 -0
- package/host/windows-runtime.mjs +6 -3
- package/host/worker-plane.mjs +34 -6
- package/host/ws.mjs +9 -2
- package/host/zip.mjs +237 -0
- package/kernel/analyze.R +1 -1
- package/{check → kernel/check}/acceptance.mjs +1 -1
- package/kernel/check/knit-file.mjs +19639 -0
- package/{check → kernel/check}/session.mjs +49 -8
- package/kernel/deployment.R +20 -20
- package/kernel/examples/NOTICE.md +1 -1
- package/kernel/fileio.R +6 -6
- package/kernel/index.html +2 -2
- package/kernel/job-run.R +83 -22
- package/kernel/jobs.R +28 -12
- package/kernel/kernel-version +1 -1
- package/kernel/kernel.R +18 -18
- package/kernel/knitr-run.R +50 -9
- package/kernel/latex.R +128 -30
- package/kernel/mcp/{carmar-mcp.mjs → beatrina-mcp.mjs} +249 -59
- package/kernel/notebook-page.R +7 -7
- package/kernel/project.R +10 -10
- package/kernel/settings.R +105 -56
- package/kernel/sniff.R +4 -4
- package/kernel/typst.R +121 -0
- package/kernel/worker.R +522 -119
- package/kernel/workspace-keep.R +3 -3
- package/lib/agent-authoring-contract.js +28 -19
- package/lib/cell-kinds.js +3 -3
- package/lib/engine-labels.js +4 -4
- package/menu/Beatrina Menu.app/Contents/Info.plist +14 -0
- package/menu/Beatrina Menu.app/Contents/MacOS/Beatrina Menu +0 -0
- package/menu/Beatrina Menu.app/Contents/_CodeSignature/CodeResources +115 -0
- package/package.json +4 -3
- package/carmar_V0.8.7.html +0 -1522
package/bin/shortcut.mjs
CHANGED
|
@@ -20,8 +20,11 @@ import { spawnSync } from "node:child_process";
|
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import os from "node:os";
|
|
22
22
|
import path from "node:path";
|
|
23
|
+
import { listSessions } from "./sessions.mjs";
|
|
24
|
+
import { defaultFor, lsQuery, registeredPaths } from "./finder.mjs";
|
|
23
25
|
|
|
24
26
|
export const BUNDLE_ID = "me.saqr.beatrina";
|
|
27
|
+
// beatrina-names: keep-start — the other product's identities, reclaimed from, are named here on purpose.
|
|
25
28
|
// Each type is EXPORTED by Beatrina.app as its own UTI (UTExportedTypeDeclarations) and named in its
|
|
26
29
|
// document types (LSItemContentTypes): Launch Services sets a default handler per UTI, and an app that
|
|
27
30
|
// only lists an extension has no UTI of its own to be the default FOR — the first --default answered
|
|
@@ -36,12 +39,23 @@ export const DOC_TYPES = Object.freeze([
|
|
|
36
39
|
{ ext: "qmd", name: "Quarto document", mime: "text/x-quarto-markdown", uti: "me.saqr.beatrina.qmd", legacy: "me.saqr.carmar.qmd" },
|
|
37
40
|
{ ext: "Rmd", name: "R Markdown document", mime: "text/x-r-markdown", uti: "me.saqr.beatrina.rmd", legacy: "me.saqr.carmar.rmd" },
|
|
38
41
|
{ ext: "carmd", name: "Beatrina notebook document", mime: "text/markdown", uti: "me.saqr.beatrina.carmd", legacy: "me.saqr.carmar.carmd" },
|
|
42
|
+
// The .btrn is a ZIP (host/bundle.mjs): it conforms to zip-archive, not text, and its MIME type is
|
|
43
|
+
// its own — declaring application/zip would make Beatrina a Linux handler for every zip on the disk.
|
|
44
|
+
{ ext: "btrn", name: "Beatrina document", mime: "application/x-beatrina-bundle", uti: "me.saqr.beatrina.btrn", conforms: "public.zip-archive" },
|
|
39
45
|
]);
|
|
46
|
+
// beatrina-names: keep-end
|
|
40
47
|
|
|
41
48
|
const q = (s) => `'${String(s).replace(/'/g, "'\\''")}'`; // POSIX single-quote
|
|
42
49
|
const asString = (s) => `"${String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
43
50
|
|
|
44
|
-
/** The
|
|
51
|
+
/** The URL scheme the applet registers: `beatrina://start` from the page's Start R (lib/attach-menu.js startHint). */
|
|
52
|
+
export const URL_SCHEME = "beatrina";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The AppleScript applet's source. `launch` is the shell command prefix. `open location` is the
|
|
56
|
+
* page's Start R (beatrina://start, docs/launch-menu-plan.md L6): another session, whatever runs —
|
|
57
|
+
* the page asked because it found none — through the same background start the icon makes.
|
|
58
|
+
*/
|
|
45
59
|
export function appleScriptSource(launch) {
|
|
46
60
|
return `on run
|
|
47
61
|
\tdo shell script ${asString(`${launch} --from-app > /dev/null 2>&1 &`)}
|
|
@@ -52,23 +66,36 @@ on open theItems
|
|
|
52
66
|
\t\tdo shell script ${asString(launch)} & " --from-app " & quoted form of (POSIX path of anItem) & " > /dev/null 2>&1 &"
|
|
53
67
|
\tend repeat
|
|
54
68
|
end open
|
|
69
|
+
|
|
70
|
+
on open location theURL
|
|
71
|
+
\tdo shell script ${asString(`${launch} --from-app --new > /dev/null 2>&1 &`)}
|
|
72
|
+
end open location
|
|
55
73
|
`;
|
|
56
74
|
}
|
|
57
75
|
|
|
76
|
+
/** The URL type the applet's Info.plist declares (CFBundleURLTypes), as plutil-insertable XML. */
|
|
77
|
+
export function urlTypesXml() {
|
|
78
|
+
return `<array><dict><key>CFBundleURLName</key><string>${BUNDLE_ID}.start</string>`
|
|
79
|
+
+ `<key>CFBundleURLSchemes</key><array><string>${URL_SCHEME}</string></array></dict></array>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A type's extension spellings, once each (`Rmd` and `rmd`; `qmd` alone — not `qmd, qmd`). */
|
|
83
|
+
const extensionsXml = (t) => `<array>${[...new Set([t.ext, t.ext.toLowerCase()])].map((e) => `<string>${e}</string>`).join("")}</array>`;
|
|
84
|
+
|
|
58
85
|
/** The document types an Info.plist declares, as plutil-insertable XML. */
|
|
59
86
|
export function documentTypesXml() {
|
|
60
87
|
return `<array>${DOC_TYPES.map((t) => `<dict><key>CFBundleTypeName</key><string>${t.name}</string>`
|
|
61
88
|
+ `<key>CFBundleTypeRole</key><string>Editor</string><key>LSHandlerRank</key><string>Alternate</string>`
|
|
62
|
-
+ `<key>LSItemContentTypes</key><array
|
|
63
|
-
+ `<key>CFBundleTypeExtensions</key
|
|
89
|
+
+ `<key>LSItemContentTypes</key><array>${[t.uti, t.legacy].filter(Boolean).map((u) => `<string>${u}</string>`).join("")}</array>`
|
|
90
|
+
+ `<key>CFBundleTypeExtensions</key>${extensionsXml(t)}</dict>`).join("")}</array>`;
|
|
64
91
|
}
|
|
65
92
|
|
|
66
93
|
/** The UTIs Beatrina.app exports for those types (UTExportedTypeDeclarations), as plutil-insertable XML. */
|
|
67
94
|
export function exportedTypesXml() {
|
|
68
95
|
return `<array>${DOC_TYPES.map((t) => `<dict><key>UTTypeIdentifier</key><string>${t.uti}</string>`
|
|
69
96
|
+ `<key>UTTypeDescription</key><string>${t.name}</string>`
|
|
70
|
-
+ `<key>UTTypeConformsTo</key><array><string
|
|
71
|
-
+ `<key>UTTypeTagSpecification</key><dict><key>public.filename-extension</key
|
|
97
|
+
+ `<key>UTTypeConformsTo</key><array><string>${t.conforms || "public.plain-text"}</string></array>`
|
|
98
|
+
+ `<key>UTTypeTagSpecification</key><dict><key>public.filename-extension</key>${extensionsXml(t)}`
|
|
72
99
|
+ `<key>public.mime-type</key><array><string>${t.mime}</string></array></dict></dict>`).join("")}</array>`;
|
|
73
100
|
}
|
|
74
101
|
|
|
@@ -85,34 +112,103 @@ function run(cmd, args, opts = {}) {
|
|
|
85
112
|
|
|
86
113
|
const LSREGISTER = "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
87
114
|
|
|
88
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Take one registered copy of the app out of Launch Services. A path that no longer exists (a
|
|
117
|
+
* folder renamed or deleted under a registration) is made to resolve for the moment it takes — a
|
|
118
|
+
* symlink to `resolveTo`, the parents it needs — unregistered, and the symlink and those parents
|
|
119
|
+
* removed again; that is the one technique that made LS drop a dead path (LEARNINGS 2026-09-16).
|
|
120
|
+
* @returns {{ok: boolean, gone: boolean, detail?: string}}
|
|
121
|
+
*/
|
|
122
|
+
export function unregisterCopy(stale, resolveTo, { lsregister = LSREGISTER } = {}) {
|
|
123
|
+
const gone = !fs.existsSync(stale);
|
|
124
|
+
const made = [];
|
|
125
|
+
try {
|
|
126
|
+
if (gone) {
|
|
127
|
+
let parent = path.dirname(stale);
|
|
128
|
+
const missing = [];
|
|
129
|
+
while (!fs.existsSync(parent)) { missing.unshift(parent); parent = path.dirname(parent); }
|
|
130
|
+
for (const d of missing) { fs.mkdirSync(d); made.push(d); }
|
|
131
|
+
fs.symlinkSync(resolveTo, stale);
|
|
132
|
+
}
|
|
133
|
+
run(lsregister, ["-u", stale]);
|
|
134
|
+
return { ok: true, gone };
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return { ok: false, gone, detail: e.message };
|
|
137
|
+
} finally {
|
|
138
|
+
if (gone) {
|
|
139
|
+
try { fs.unlinkSync(stale); } catch { /* never made */ }
|
|
140
|
+
for (const d of made.reverse()) { try { fs.rmdirSync(d); } catch { /* not empty: leave it */ } }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Point every UTI a type can carry at `bundleId` (the JXA bridge is the only writer; 0 is success). */
|
|
146
|
+
function setDefaultHandler(types, bundleId) {
|
|
147
|
+
// Beatrina's own UTI for each type, CarmaR's legacy one, and whatever UTI Launch Services currently (beatrina-names: keep)
|
|
148
|
+
// resolves the extension to (another app's declaration, or a dynamic one) — all pointed at Beatrina,
|
|
149
|
+
// so a .qmd opens here whichever identity the system had attached to it.
|
|
150
|
+
const js = `ObjC.import("CoreServices");
|
|
151
|
+
const types = ${JSON.stringify(types.map((t) => ({ ext: t.ext, uti: t.uti, legacy: t.legacy })))};
|
|
152
|
+
types.map((t) => { const tagged = $.UTTypeCreatePreferredIdentifierForTag($.kUTTagClassFilenameExtension, $(t.ext), $()).js;
|
|
153
|
+
const utis = [...new Set([t.uti, t.legacy, tagged])].filter(Boolean);
|
|
154
|
+
return t.ext + "=" + utis.map((u) => $.LSSetDefaultRoleHandlerForContentType($(u), $.kLSRolesAll, $(${JSON.stringify(bundleId)}))).join("/"); }).join(" ");`;
|
|
155
|
+
const out = run("osascript", ["-l", "JavaScript", "-e", js]).trim();
|
|
156
|
+
return { out, failed: /=(?:[^0 /]|-\d)/.test(out) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function macShortcut({ cli, dir, port, makeDefault, noOpen, bundleId }) {
|
|
89
160
|
const app = path.join(dir || path.join(os.homedir(), "Applications"), "Beatrina.app");
|
|
161
|
+
const exts = DOC_TYPES.map((t) => t.ext);
|
|
162
|
+
// Before anything changes: what Finder does today (finder.mjs, read-only) — which extensions
|
|
163
|
+
// Beatrina is already the default for, so a rebuild puts that choice back rather than losing it,
|
|
164
|
+
// and which copies are registered under this id, so every other one can be taken out below.
|
|
165
|
+
const before = lsQuery({ exts, bundleIds: [bundleId] });
|
|
166
|
+
const wasDefault = DOC_TYPES.filter((t) => defaultFor(before, bundleId, [t.ext]).length);
|
|
90
167
|
fs.mkdirSync(path.dirname(app), { recursive: true });
|
|
91
168
|
fs.rmSync(app, { recursive: true, force: true });
|
|
92
169
|
const src = path.join(fs.mkdtempSync(path.join(os.tmpdir(), "beatrina-app-")), "Beatrina.applescript");
|
|
93
170
|
fs.writeFileSync(src, appleScriptSource(launchCommand({ cli, port, noOpen })));
|
|
94
171
|
run("osacompile", ["-o", app, src]);
|
|
95
172
|
const plist = path.join(app, "Contents", "Info.plist");
|
|
96
|
-
run("plutil", ["-replace", "CFBundleIdentifier", "-string",
|
|
173
|
+
run("plutil", ["-replace", "CFBundleIdentifier", "-string", bundleId, plist]);
|
|
97
174
|
run("plutil", ["-replace", "CFBundleName", "-string", "Beatrina", plist]);
|
|
98
175
|
run("plutil", ["-replace", "CFBundleDocumentTypes", "-xml", documentTypesXml(), plist]);
|
|
99
176
|
run("plutil", ["-replace", "UTExportedTypeDeclarations", "-xml", exportedTypesXml(), plist]);
|
|
177
|
+
run("plutil", ["-replace", "CFBundleURLTypes", "-xml", urlTypesXml(), plist]);
|
|
100
178
|
run("codesign", ["--force", "--sign", "-", app]); // the plist changed after osacompile signed it
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
179
|
+
const said = [];
|
|
180
|
+
// Idempotent: every OTHER copy registered under this id is unregistered before this one is
|
|
181
|
+
// registered. Launch Services picks between copies at its own discretion — an older Beatrina.app
|
|
182
|
+
// dragged somewhere, a scratch build a killed test left behind, a folder that no longer exists —
|
|
183
|
+
// so which copy a double-click reaches is its choice until there is one.
|
|
184
|
+
if (fs.existsSync(LSREGISTER)) {
|
|
185
|
+
for (const stale of registeredPaths(before, bundleId).filter((p) => path.resolve(p) !== path.resolve(app))) {
|
|
186
|
+
const done = unregisterCopy(stale, app);
|
|
187
|
+
said.push(done.ok ? `Unregistered another copy of Beatrina.app at ${stale}${done.gone ? " (a folder that no longer exists)" : ""}.`
|
|
188
|
+
: `Could not unregister the copy of Beatrina.app at ${stale} (${done.detail}); Finder may still pick it.`);
|
|
189
|
+
}
|
|
190
|
+
run(LSREGISTER, ["-f", app]);
|
|
191
|
+
}
|
|
192
|
+
said.push(`Beatrina.app is in ${path.dirname(app)}. Double-click it, or right-click a .qmd and choose Open With ▸ Beatrina.`);
|
|
193
|
+
// The default: every type with --default; otherwise only the types Beatrina was already the
|
|
194
|
+
// default for (a rebuild is not a choice — "never by surprise"), re-asserted because a fresh
|
|
195
|
+
// registration can lose it.
|
|
196
|
+
const wanted = makeDefault ? DOC_TYPES : wasDefault;
|
|
197
|
+
if (wanted.length) {
|
|
198
|
+
const set = setDefaultHandler(wanted, bundleId);
|
|
199
|
+
if (set.failed) said.push(`Beatrina could not be made the default for every type (${set.out}; 0 means done).`);
|
|
200
|
+
else if (!makeDefault) said.push(`Beatrina stays the default app for ${wasDefault.map((t) => `.${t.ext}`).join(", ")}, as before.`);
|
|
201
|
+
if (makeDefault && !set.failed) {
|
|
202
|
+
// Read back from Launch Services, never inferred from return codes.
|
|
203
|
+
const after = lsQuery({ exts, bundleIds: [bundleId] });
|
|
204
|
+
if (!after) said.push(`Beatrina is now the default app for ${exts.map((e) => `.${e}`).join(", ")} (not read back: no Swift toolchain).`);
|
|
205
|
+
else {
|
|
206
|
+
const now = defaultFor(after, bundleId, exts);
|
|
207
|
+
const still = exts.filter((e) => !now.includes(e)).map((e) => { const r = after.ext.find((x) => x.ext === e); return `.${e} (${(r && (r.name || r.handler)) || "no app"})`; });
|
|
208
|
+
if (now.length) said.push(`Finder now opens ${now.map((e) => `.${e}`).join(", ")} with Beatrina.`);
|
|
209
|
+
if (still.length) said.push(`Finder still opens ${still.join(", ")} elsewhere — run beatrina doctor, then beatrina shortcut --default again.`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
116
212
|
}
|
|
117
213
|
return { app, said };
|
|
118
214
|
}
|
|
@@ -155,25 +251,137 @@ function linuxShortcut({ cli, port, makeDefault }) {
|
|
|
155
251
|
return { app: desktop, said };
|
|
156
252
|
}
|
|
157
253
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
254
|
+
// ── the menu (docs/launch-menu-plan.md M2) ──────────────────────────────────
|
|
255
|
+
// `Beatrina Menu.app` ships in the npm package at menu/ (tools/app/build-helper.sh --product
|
|
256
|
+
// beatrina; a binary and an Info.plist, nothing of its own inside). `beatrina shortcut --menu`
|
|
257
|
+
// copies it beside Beatrina.app, writes the exact command line the applet bakes to
|
|
258
|
+
// Contents/Resources/beatrina-command (an absolute Node and CLI path: a menu-bar process has no
|
|
259
|
+
// PATH), registers it and opens it; --login adds a LaunchAgent with RunAtLoad and NO KeepAlive (a
|
|
260
|
+
// menu, not a daemon — helper-agent.sh's "additive and explicit" rule); --no-menu removes both.
|
|
261
|
+
// Explicit both ways: `beatrina shortcut` alone installs no menu, and only refreshes the command
|
|
262
|
+
// line of a menu already installed beside the applet, so the two stay in step after a Node upgrade.
|
|
263
|
+
export const MENU_BUNDLE_ID = "me.saqr.beatrina.menu";
|
|
264
|
+
export const MENU_NAME = "Beatrina Menu.app";
|
|
265
|
+
const menuLabel = () => process.env.BEATRINA_MENU_LABEL || MENU_BUNDLE_ID;
|
|
266
|
+
const menuAgentPlist = () => path.join(os.homedir(), "Library", "LaunchAgents", `${menuLabel()}.plist`);
|
|
267
|
+
const launchDomain = () => `gui/${process.getuid()}`;
|
|
268
|
+
|
|
269
|
+
/** The LaunchAgent that starts the menu at login: RunAtLoad, and deliberately no KeepAlive. */
|
|
270
|
+
export function menuAgentXml({ label, binary }) {
|
|
271
|
+
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<");
|
|
272
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
273
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
274
|
+
<plist version="1.0"><dict>
|
|
275
|
+
<key>Label</key><string>${esc(label)}</string>
|
|
276
|
+
<key>ProgramArguments</key><array><string>${esc(binary)}</string></array>
|
|
277
|
+
<key>RunAtLoad</key><true/>
|
|
278
|
+
<key>ProcessType</key><string>Interactive</string>
|
|
279
|
+
</dict></plist>
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Write (or refresh) the command line the menu runs, and re-seal the bundle around it. */
|
|
284
|
+
function writeMenuCommand(menuApp, cli) {
|
|
285
|
+
const resources = path.join(menuApp, "Contents", "Resources");
|
|
286
|
+
fs.mkdirSync(resources, { recursive: true });
|
|
287
|
+
fs.writeFileSync(path.join(resources, "beatrina-command"), `${launchCommand({ cli })}\n`, { mode: 0o644 });
|
|
288
|
+
run("codesign", ["--force", "--deep", "--sign", "-", menuApp]); // the resources changed after the build signed it
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Install the menu beside the applet. @returns {string[]} what was done */
|
|
292
|
+
function installMenu({ cli, dir, login, noOpen, menuSource }) {
|
|
293
|
+
const said = [];
|
|
294
|
+
if (!menuSource || !fs.existsSync(path.join(menuSource, "Contents", "MacOS", "Beatrina Menu"))) {
|
|
295
|
+
return [`This Beatrina carries no menu (${menuSource || "no menu/ folder"}); a package built on a Mac ships one. Nothing was installed.`];
|
|
177
296
|
}
|
|
178
|
-
|
|
297
|
+
const menuApp = path.join(dir || path.join(os.homedir(), "Applications"), MENU_NAME);
|
|
298
|
+
// Quit a running menu before its files change under it; a menu that is not running is not an error.
|
|
299
|
+
spawnSync("osascript", ["-e", `tell application id "${MENU_BUNDLE_ID}" to quit`], { timeout: 10000 });
|
|
300
|
+
fs.rmSync(menuApp, { recursive: true, force: true });
|
|
301
|
+
fs.cpSync(menuSource, menuApp, { recursive: true });
|
|
302
|
+
writeMenuCommand(menuApp, cli);
|
|
303
|
+
if (fs.existsSync(LSREGISTER)) run(LSREGISTER, ["-f", menuApp]);
|
|
304
|
+
said.push(`${MENU_NAME} is in ${path.dirname(menuApp)}; it runs \`beatrina\` for you and starts nothing itself.`);
|
|
305
|
+
if (login) {
|
|
306
|
+
const plist = menuAgentPlist();
|
|
307
|
+
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
308
|
+
fs.writeFileSync(plist, menuAgentXml({ label: menuLabel(), binary: path.join(menuApp, "Contents", "MacOS", "Beatrina Menu") }), { mode: 0o644 });
|
|
309
|
+
spawnSync("launchctl", ["bootout", `${launchDomain()}/${menuLabel()}`], { timeout: 10000 });
|
|
310
|
+
if (noOpen) said.push(`It will start at your next login (${plist}).`);
|
|
311
|
+
else {
|
|
312
|
+
const up = spawnSync("launchctl", ["bootstrap", launchDomain(), plist], { encoding: "utf8", timeout: 20000 });
|
|
313
|
+
said.push(up.status === 0 ? `It is running now and starts at every login (${plist}); beatrina shortcut --no-menu removes it.`
|
|
314
|
+
: `It starts at every login (${plist}), but launchctl could not start it now (${String(up.stderr).trim() || up.status}) — open it from ${path.dirname(menuApp)}.`);
|
|
315
|
+
}
|
|
316
|
+
} else if (!noOpen) {
|
|
317
|
+
const opened = spawnSync("open", ["-a", menuApp], { encoding: "utf8", timeout: 20000 });
|
|
318
|
+
said.push(opened.status === 0 ? "It is in your menu bar now (the B); beatrina shortcut --menu --login makes it start at every login, --no-menu removes it."
|
|
319
|
+
: `It could not be opened (${String(opened.stderr).trim() || opened.status}); open it from ${path.dirname(menuApp)}.`);
|
|
320
|
+
}
|
|
321
|
+
return said;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* `beatrina shortcut --no-menu`: the login agent, the running menu and the bundle, gone. Nothing
|
|
326
|
+
* else changes — Beatrina.app, the file types and the sessions are untouched.
|
|
327
|
+
*/
|
|
328
|
+
export function removeMenu({ dir = "", platform = process.platform } = {}) {
|
|
329
|
+
if (platform !== "darwin") return { said: ["The menu-bar item is macOS only; nothing to remove."] };
|
|
330
|
+
const said = [];
|
|
331
|
+
const menuApp = path.join(dir || path.join(os.homedir(), "Applications"), MENU_NAME);
|
|
332
|
+
const plist = menuAgentPlist();
|
|
333
|
+
spawnSync("launchctl", ["bootout", `${launchDomain()}/${menuLabel()}`], { timeout: 10000 });
|
|
334
|
+
if (fs.existsSync(plist)) { fs.rmSync(plist, { force: true }); said.push(`Removed the login item ${plist}.`); }
|
|
335
|
+
spawnSync("osascript", ["-e", `tell application id "${MENU_BUNDLE_ID}" to quit`], { timeout: 10000 });
|
|
336
|
+
if (fs.existsSync(menuApp)) {
|
|
337
|
+
if (fs.existsSync(LSREGISTER)) spawnSync(LSREGISTER, ["-u", menuApp]);
|
|
338
|
+
fs.rmSync(menuApp, { recursive: true, force: true });
|
|
339
|
+
said.push(`Removed ${menuApp}.`);
|
|
340
|
+
}
|
|
341
|
+
if (!said.length) said.push("No Beatrina menu was installed; nothing to remove.");
|
|
342
|
+
said.push("Beatrina.app, the file types and any running session are as they were.");
|
|
343
|
+
return { said };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Make the icon (and, with makeDefault, the default .qmd app; with menu, the menu-bar item).
|
|
348
|
+
* BEATRINA_BUNDLE_ID substitutes the bundle id (the suites build a scratch Beatrina.app under a
|
|
349
|
+
* scratch id, so the one in ~/Applications is never a second copy of theirs).
|
|
350
|
+
*/
|
|
351
|
+
export function makeShortcut({ cli, dir = "", port = "", makeDefault = false, noOpen = false, platform = process.platform,
|
|
352
|
+
bundleId = process.env.BEATRINA_BUNDLE_ID || BUNDLE_ID, menu = false, login = false, menuSource = "" }) {
|
|
353
|
+
if (platform === "darwin") {
|
|
354
|
+
const made = macShortcut({ cli, dir, port, makeDefault, noOpen, bundleId });
|
|
355
|
+
const menuApp = path.join(dir || path.join(os.homedir(), "Applications"), MENU_NAME);
|
|
356
|
+
if (menu) made.said.push(...installMenu({ cli, dir, login, noOpen, menuSource }));
|
|
357
|
+
else if (fs.existsSync(path.join(menuApp, "Contents", "MacOS", "Beatrina Menu"))) {
|
|
358
|
+
writeMenuCommand(menuApp, cli);
|
|
359
|
+
made.said.push(`${MENU_NAME} in ${path.dirname(menuApp)} now runs this beatrina too.`);
|
|
360
|
+
}
|
|
361
|
+
return made;
|
|
362
|
+
}
|
|
363
|
+
const said = menu ? ["The menu-bar item is macOS only; the rest was done."] : [];
|
|
364
|
+
const made = platform === "win32" ? windowsShortcut({ cli, port, makeDefault }) : linuxShortcut({ cli, port, makeDefault });
|
|
365
|
+
made.said.push(...said);
|
|
366
|
+
return made;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* A running session of this app, or null — the door every double-click, icon and menu row goes through.
|
|
371
|
+
*
|
|
372
|
+
* A thin wrapper over `listSessions` (sessions.mjs), so `beatrina status`, `stop`, `doctor`, the menu
|
|
373
|
+
* and this one door read ONE discovery: the runtime records of the asked app whose /health proves the
|
|
374
|
+
* same session (host and boot instant). With no `port`, the NEWEST `started` first — the rule
|
|
375
|
+
* launch.sh applies to a delivered document — so a double-click with two sessions running lands in the
|
|
376
|
+
* one started most recently, not on the lowest port readdir happens to list first. With `port`, that
|
|
377
|
+
* session or null. The row carries what the record does (port, url, file, pid, started, document…).
|
|
378
|
+
*/
|
|
379
|
+
export async function liveSession({ app = "beatrina", runtimeDir = null, port = "" } = {}) {
|
|
380
|
+
// Records live in ~/.beatrina/run, and in the older directory for a session an older build started;
|
|
381
|
+
// an explicit BEATRINA_RUNTIME_DIR, under either spelling (or the `runtimeDir` option), is read alone.
|
|
382
|
+
const rows = await listSessions({ dir: runtimeDir, apps: new Set([app]) });
|
|
383
|
+
const startedAt = (r) => Date.parse(r.started) || 0;
|
|
384
|
+
const live = rows.filter((r) => r.live && (!port || String(r.port) === String(port)))
|
|
385
|
+
.sort((a, b) => startedAt(b) - startedAt(a) || a.port - b.port);
|
|
386
|
+
return live[0] || null;
|
|
179
387
|
}
|
package/bin/update-check.mjs
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// At most once a day per user (the last check is recorded in a small JSON file, which the host reads
|
|
5
5
|
// back to put `update_available` on /health — host/update-record.mjs), with a short timeout, and never
|
|
6
6
|
// in the way: a registry that cannot be reached, an unpublished package (404) or a garbled answer all
|
|
7
|
-
// mean "nothing to say". BEATRINA_NO_UPDATE_CHECK=1 or
|
|
8
|
-
// battery runner sets
|
|
7
|
+
// mean "nothing to say". BEATRINA_NO_UPDATE_CHECK=1 (or its older spelling) turns it off (the
|
|
8
|
+
// battery runner sets it). BEATRINA_REGISTRY points at another registry (tests).
|
|
9
9
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
package/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.
|
|
1
|
+
{"version":"0.9.43","commit":"ce1dbc09721024a43c3666122e3e0759a24b294f","dirty":false}
|
package/engines/js/worker.mjs
CHANGED
|
@@ -39,10 +39,13 @@ import vm from "node:vm";
|
|
|
39
39
|
import { createRequire } from "node:module";
|
|
40
40
|
import { persistDeclarations } from "./persist.mjs";
|
|
41
41
|
import { DocumentExecError, DocumentSetups, FEATURE } from "./document-exec.mjs";
|
|
42
|
+
// host/ sits beside engines/ in every distribution (tools/npm/build-npm.mjs).
|
|
43
|
+
import { readEnv } from "../../host/env-names.mjs";
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
45
|
+
// BEATRINA_* first, the old CARMAR_* spelling second (host/env-names.mjs).
|
|
46
|
+
const SENTINEL = readEnv(process.env, "SENTINEL");
|
|
47
|
+
const CMD_TAG = readEnv(process.env, "CMD_TAG");
|
|
48
|
+
const HANDOFF_DIR = readEnv(process.env, "HANDOFF_DIR");
|
|
46
49
|
const COMMANDS = ["exec", "wd", "doctor", "env", "rm", "parse", "complete"];
|
|
47
50
|
const PREVIEW_ROWS = 200;
|
|
48
51
|
const PREVIEW_COLS = 20;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""adapter.py — Jupyter messages in, Beatrix frames out.
|
|
2
2
|
|
|
3
|
-
Ported from
|
|
3
|
+
Ported from carmapy's `py/carmapy/kernel_adapter.py` (read 2026-09-14; that
|
|
4
|
+
repository is closed — github.com/mohsaqr/carmar-python, see docs/python-kernel-records/).
|
|
4
5
|
It does not evaluate anything itself; `ipykernel` does that, in its own OS
|
|
5
6
|
process, and this translates.
|
|
6
7
|
|
|
7
8
|
THE RULE THAT COST THREE WRONG MEASUREMENTS OVER THERE
|
|
8
|
-
(
|
|
9
|
+
(`docs/python-kernel-records/probes/RESULTS.md` P1):
|
|
9
10
|
|
|
10
11
|
A cell is finished when iopub reports `status: idle` for its parent
|
|
11
12
|
msg_id — NEVER when execute_reply arrives.
|
|
@@ -37,14 +38,14 @@ from typing import Awaitable, Callable
|
|
|
37
38
|
|
|
38
39
|
from jupyter_client.manager import AsyncKernelManager
|
|
39
40
|
|
|
40
|
-
from bootstrap import BOOTSTRAP,
|
|
41
|
+
from bootstrap import BOOTSTRAP, BEATRINA_MIME, CONTROL_MIME, REPLY_MIME
|
|
41
42
|
|
|
42
43
|
#: debugger.py's PROMPT. Restated, not imported: debugger.py is kernel-side
|
|
43
44
|
#: source and is never imported into this process. test/host-python-debug
|
|
44
45
|
#: pins the two spellings against each other.
|
|
45
|
-
DEBUG_PROMPT = "\
|
|
46
|
-
#: debugger.py's
|
|
47
|
-
DEBUG_ABORT = "
|
|
46
|
+
DEBUG_PROMPT = "\x1bbeatrina-debug\x1b"
|
|
47
|
+
#: debugger.py's BeatrinaDebugAbort, as ipykernel names it on the error message.
|
|
48
|
+
DEBUG_ABORT = "BeatrinaDebugAbort"
|
|
48
49
|
|
|
49
50
|
Emit = Callable[[dict], Awaitable[None]]
|
|
50
51
|
|
|
@@ -56,7 +57,7 @@ class PythonSession:
|
|
|
56
57
|
"""One Python session: the notebook's `worker.R` equivalent.
|
|
57
58
|
|
|
58
59
|
Owns exactly one ipykernel process. Not a pool, not a registry — one
|
|
59
|
-
document, one kernel, which is both Jupyter's rule and
|
|
60
|
+
document, one kernel, which is both Jupyter's rule and Beatrina's.
|
|
60
61
|
"""
|
|
61
62
|
|
|
62
63
|
def __init__(self, kernel_name: str = "python3", on_input=None, on_debug=None):
|
|
@@ -219,7 +220,7 @@ class PythonSession:
|
|
|
219
220
|
try:
|
|
220
221
|
if srcname:
|
|
221
222
|
spec = json.dumps(json.dumps({"srcname": srcname, "breaks": breaks or {}}))
|
|
222
|
-
prep = self.kc.execute(f"
|
|
223
|
+
prep = self.kc.execute(f"__beatrina_debug_prepare({spec})",
|
|
223
224
|
store_history=False, allow_stdin=False)
|
|
224
225
|
await self._drain_until_idle(prep, None, None, timeout=30)
|
|
225
226
|
msg_id = self.kc.execute(code, store_history=store_history,
|
|
@@ -250,13 +251,13 @@ class PythonSession:
|
|
|
250
251
|
|
|
251
252
|
The wire never carries code. `spec` is JSON, passed as ONE argument to
|
|
252
253
|
a function the bootstrap installed; the kernel-side half parses it. A
|
|
253
|
-
composed `
|
|
254
|
+
composed `__beatrina_view(df, sort='...')` string would be the browser
|
|
254
255
|
writing Python, which is the door this design does not have.
|
|
255
256
|
"""
|
|
256
257
|
async with self._chan:
|
|
257
258
|
self._reply = None
|
|
258
259
|
payload = json.dumps(json.dumps(spec, allow_nan=False))
|
|
259
|
-
code = f"
|
|
260
|
+
code = f"__beatrina_reply({op!r}, {payload})"
|
|
260
261
|
msg_id = self.kc.execute(code, store_history=False, allow_stdin=False)
|
|
261
262
|
await self._drain_until_idle(msg_id, None, None, timeout=300)
|
|
262
263
|
reply, self._reply = self._reply, None
|
|
@@ -403,7 +404,7 @@ class PythonSession:
|
|
|
403
404
|
self.interrupted = True
|
|
404
405
|
return []
|
|
405
406
|
# The debugger's Stop: R reports it as `interrupted` with the
|
|
406
|
-
# message "Stopped from the debugger" (worker.R
|
|
407
|
+
# message "Stopped from the debugger" (worker.R beatrina_abort_cell),
|
|
407
408
|
# and so does this — no traceback for a button the user pressed.
|
|
408
409
|
if ename == DEBUG_ABORT:
|
|
409
410
|
self.interrupted = True
|
|
@@ -449,8 +450,8 @@ class PythonSession:
|
|
|
449
450
|
self._reply = json.loads(payload) if isinstance(payload, str) else payload
|
|
450
451
|
return []
|
|
451
452
|
|
|
452
|
-
if
|
|
453
|
-
payload = data[
|
|
453
|
+
if BEATRINA_MIME in data:
|
|
454
|
+
payload = data[BEATRINA_MIME]
|
|
454
455
|
if isinstance(payload, str):
|
|
455
456
|
payload = json.loads(payload)
|
|
456
457
|
return [{"type": "dataframe", "id": wire_id, "source": None, **payload}]
|
|
@@ -8,7 +8,8 @@ and that must not take syntax checking, navigation and rename down with it.
|
|
|
8
8
|
browser ⇅ host/main.mjs ⇅ ipykernel — evaluates everything
|
|
9
9
|
⇅ analyze.py — evaluates NOTHING
|
|
10
10
|
|
|
11
|
-
Ported from carmapy's analyze.py (
|
|
11
|
+
Ported from carmapy's analyze.py (`py/carmapy/analyze.py` in the closed
|
|
12
|
+
repository github.com/mohsaqr/carmar-python, see docs/python-kernel-records/), with
|
|
12
13
|
one deliberate change recorded in docs/wp/wp8a-python-analyzer.md: the
|
|
13
14
|
reference index is built from `ast`, not from `jedi`, and jedi is not imported.
|
|
14
15
|
|
|
@@ -69,7 +70,8 @@ import warnings
|
|
|
69
70
|
|
|
70
71
|
def _budget() -> float:
|
|
71
72
|
try:
|
|
72
|
-
v = float(os.environ.get("
|
|
73
|
+
v = float(os.environ.get("BEATRINA_ANALYZE_BUDGET")
|
|
74
|
+
or os.environ.get("CARMAR_ANALYZE_BUDGET", "3")) # beatrina-names: keep
|
|
73
75
|
except ValueError:
|
|
74
76
|
v = 3.0
|
|
75
77
|
if v != v or v in (float("inf"), float("-inf")):
|