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.
Files changed (114) hide show
  1. package/LICENSE +92 -0
  2. package/NOTICES +72 -0
  3. package/README.md +124 -0
  4. package/bin/beatrina.mjs +223 -0
  5. package/bin/cli.mjs +80 -0
  6. package/bin/failsafe.mjs +74 -0
  7. package/bin/identity.mjs +45 -0
  8. package/bin/prova-post.mjs +51 -0
  9. package/bin/sessions.mjs +95 -0
  10. package/bin/shortcut.mjs +151 -0
  11. package/bin/update-check.mjs +55 -0
  12. package/bin/upgrade.mjs +76 -0
  13. package/build-info.json +1 -0
  14. package/carmar_V0.8.6.html +1310 -0
  15. package/check/acceptance.mjs +278 -0
  16. package/check/session.mjs +215 -0
  17. package/engines/js/document-exec.mjs +82 -0
  18. package/engines/js/persist.mjs +214 -0
  19. package/engines/js/worker.mjs +424 -0
  20. package/engines/python/adapter.py +577 -0
  21. package/engines/python/analyze.py +814 -0
  22. package/engines/python/bootstrap.py +309 -0
  23. package/engines/python/dataview.py +735 -0
  24. package/engines/python/debugger.py +346 -0
  25. package/engines/python/document_exec.py +158 -0
  26. package/engines/python/engine.json +28 -0
  27. package/engines/python/handoff.py +118 -0
  28. package/engines/python/worker.py +564 -0
  29. package/engines/r/engine.json +25 -0
  30. package/engines/r/handoff.R +92 -0
  31. package/failsafe/ai-policy.R +255 -0
  32. package/failsafe/ai-store.R +373 -0
  33. package/failsafe/cite.R +418 -0
  34. package/failsafe/journal.R +684 -0
  35. package/failsafe/plugins.R +809 -0
  36. package/failsafe/serve.R +5500 -0
  37. package/host/ai-policy.mjs +218 -0
  38. package/host/deployment.mjs +160 -0
  39. package/host/engine-js.mjs +98 -0
  40. package/host/engine-pool.mjs +383 -0
  41. package/host/engine-python.mjs +228 -0
  42. package/host/engine-r.mjs +206 -0
  43. package/host/engine-stdio.mjs +401 -0
  44. package/host/journal-store.mjs +749 -0
  45. package/host/main.mjs +503 -0
  46. package/host/planes/README.md +41 -0
  47. package/host/planes/ai-store.mjs +327 -0
  48. package/host/planes/ai.mjs +467 -0
  49. package/host/planes/analyze.mjs +397 -0
  50. package/host/planes/cite.mjs +517 -0
  51. package/host/planes/files.mjs +0 -0
  52. package/host/planes/jobs.mjs +704 -0
  53. package/host/planes/journal.mjs +53 -0
  54. package/host/planes/latex.mjs +201 -0
  55. package/host/planes/mcp.mjs +493 -0
  56. package/host/planes/pair.mjs +325 -0
  57. package/host/planes/pipe-term.mjs +122 -0
  58. package/host/planes/plugins.mjs +112 -0
  59. package/host/planes/proc-tree.mjs +76 -0
  60. package/host/planes/sessions.mjs +434 -0
  61. package/host/planes/settings.mjs +164 -0
  62. package/host/planes/terminal.mjs +286 -0
  63. package/host/planes/test-file.mjs +80 -0
  64. package/host/planes/update.mjs +214 -0
  65. package/host/plugin-store.mjs +838 -0
  66. package/host/server.mjs +441 -0
  67. package/host/settings.mjs +379 -0
  68. package/host/update-record.mjs +59 -0
  69. package/host/user-dirs.mjs +117 -0
  70. package/host/windows-runtime.mjs +145 -0
  71. package/host/worker-plane.mjs +713 -0
  72. package/host/ws.mjs +190 -0
  73. package/kernel/analyze.R +668 -0
  74. package/kernel/deployment.R +165 -0
  75. package/kernel/examples/NOTICE.md +38 -0
  76. package/kernel/examples/tna-complete-tutorial.Rmd +210 -0
  77. package/kernel/fileio.R +656 -0
  78. package/kernel/index.html +96 -0
  79. package/kernel/job-run.R +391 -0
  80. package/kernel/jobs.R +276 -0
  81. package/kernel/kernel-protocol +1 -0
  82. package/kernel/kernel-version +1 -0
  83. package/kernel/kernel.R +671 -0
  84. package/kernel/knitr-run.R +245 -0
  85. package/kernel/latex.R +609 -0
  86. package/kernel/mcp/carmar-mcp.mjs +516 -0
  87. package/kernel/notebook-page.R +67 -0
  88. package/kernel/plugins/csl/apa/apa.csl +2273 -0
  89. package/kernel/plugins/csl/apa/plugin.json +19 -0
  90. package/kernel/plugins/csl/chicago-author-date/chicago-author-date.csl +4216 -0
  91. package/kernel/plugins/csl/chicago-author-date/plugin.json +19 -0
  92. package/kernel/plugins/csl/harvard-cite-them-right/harvard-cite-them-right.csl +316 -0
  93. package/kernel/plugins/csl/harvard-cite-them-right/plugin.json +19 -0
  94. package/kernel/plugins/csl/ieee/ieee.csl +519 -0
  95. package/kernel/plugins/csl/ieee/plugin.json +19 -0
  96. package/kernel/plugins/csl/modern-language-association/modern-language-association.csl +1184 -0
  97. package/kernel/plugins/csl/modern-language-association/plugin.json +19 -0
  98. package/kernel/plugins/csl/nature/nature.csl +189 -0
  99. package/kernel/plugins/csl/nature/plugin.json +19 -0
  100. package/kernel/plugins/latex/apa7/apa7.json +14 -0
  101. package/kernel/plugins/latex/apa7/plugin.json +19 -0
  102. package/kernel/plugins/latex/elsarticle/elsarticle.json +14 -0
  103. package/kernel/plugins/latex/elsarticle/plugin.json +19 -0
  104. package/kernel/plugins/latex/ieeetran/ieeetran.json +10 -0
  105. package/kernel/plugins/latex/ieeetran/plugin.json +19 -0
  106. package/kernel/project.R +131 -0
  107. package/kernel/settings.R +410 -0
  108. package/kernel/sniff.R +769 -0
  109. package/kernel/worker-boot.R +22 -0
  110. package/kernel/worker.R +3496 -0
  111. package/lib/agent-authoring-contract.js +547 -0
  112. package/lib/cell-kinds.js +108 -0
  113. package/lib/engine-labels.js +324 -0
  114. package/package.json +32 -0
package/LICENSE ADDED
@@ -0,0 +1,92 @@
1
+ # Carm Research License v1.4
2
+
3
+ Copyright (c) 2025–2026 Professor Mohammed Saqr, PhD. All rights reserved.
4
+ https://saqr.me | saqr@saqr.me
5
+
6
+ This License applies to Carm and all associated products, libraries, tools, and components in the Carm ecosystem (collectively, the "Software").
7
+
8
+ ## DEFINITIONS
9
+
10
+ "Non-commercial use" means use that is not intended for or directed toward commercial advantage or monetary compensation. It includes academic research, teaching and classroom use, personal learning, student coursework, and use by non-profit organizations and NGOs.
11
+
12
+ Research and teaching carried out at an academic or non-profit institution is non-commercial regardless of how it is funded. Public grants, charitable funding, institutional funding, industry-sponsored or collaboratively funded projects, and industry-sponsored doctoral work all remain within this grant, and nothing here depends on whether or when the results are published.
13
+
14
+ Use is commercial, and requires a paid license, where the Software is used by or on behalf of a for-profit entity for its own business purposes, is incorporated into a product or service offered for a fee, or is used to deliver paid consulting or contract work. Where the character of a particular use is unclear, contact the copyright holder.
15
+
16
+ ## GRANT FOR RESEARCH AND EDUCATION
17
+
18
+ Permission is hereby granted, free of charge, to use, copy, and distribute the Software for non-commercial purposes.
19
+
20
+ This grant is subject to the following conditions: (1) this License, the copyright notice, and the third-party notices required under THIRD-PARTY COMPONENTS shall be included in all copies of the Software; (2) attribution must be provided as set out in ATTRIBUTION AND CITATION below; and (3) the Software may not be sublicensed, resold, or offered as a service without written permission from the copyright holder.
21
+
22
+ All outputs produced by the Software, including notebooks, reports, tables, charts, exports, and statistical results, are the user's own and may be freely shared, published, distributed, or otherwise used without restriction. The user assumes sole responsibility for the accuracy, interpretation, use, and distribution of such outputs.
23
+
24
+ The copyright holder makes no representation regarding the correctness, completeness, reproducibility, or suitability of any analyses performed using the Software. Users are solely responsible for validating all results before publication, regulatory submission, clinical use, or other consequential decisions.
25
+
26
+ ## PAID LICENSE REQUIRED
27
+
28
+ A separate paid license is required for any commercial use, including use in commercial products or services, consulting or contract work that generates revenue, and use by for-profit organizations for their own business purposes.
29
+
30
+ For the avoidance of doubt, research and teaching at an academic or non-profit institution does not require a paid license by reason of its funding source. Industry-sponsored, collaboratively funded, and jointly funded academic projects fall within the free grant.
31
+
32
+ Embedding, repurposing, or integrating the Software or its components into other software, platforms, or products requires explicit written approval from the copyright holder, whether or not the resulting work is distributed commercially.
33
+
34
+ ## INTELLECTUAL PROPERTY
35
+
36
+ The Software, including its source code, algorithms, architecture, design, and documentation, is and shall remain the sole property of Professor Mohammed Saqr, subject only to the rights of third parties in the components identified under THIRD-PARTY COMPONENTS. No rights, title, or interest are transferred except the limited license granted herein.
37
+
38
+ Reverse engineering, decompilation, disassembly, translation, modification, circumvention of technical protection measures, or any other attempt to derive, reconstruct, or extract the source code, algorithms, architecture, or underlying implementation of the Software is prohibited, except to the extent such restriction is prohibited by applicable law.
39
+
40
+ Creating derivative works based on the Software, and extracting or reusing any portion of the Software — including individual functions, algorithms, numerical constants, coefficients, or parameter values — in any other software, library, or service, is prohibited without written permission, except to the extent such restriction is prohibited by applicable law.
41
+
42
+ ## MACHINE LEARNING AND AUTOMATED PROCESSING
43
+
44
+ The Software and its source code may not be used to train, fine-tune, or evaluate machine-learning models, nor incorporated into any dataset assembled for that purpose, without written permission from the copyright holder.
45
+
46
+ This restriction applies to the Software itself. It does not apply to outputs the user generates by running the Software on the user's own data, which remain governed by the GRANT FOR RESEARCH AND EDUCATION above.
47
+
48
+ ## THIRD-PARTY COMPONENTS
49
+
50
+ The Software incorporates third-party open-source components that remain the property of their respective authors and are licensed under their own terms. Those terms are reproduced in the distributed artifacts, and nothing in this License limits the rights granted, or the obligations imposed, by them.
51
+
52
+ As of this version, the distributed CarmNote artifacts incorporate D3.js (https://d3js.org), licensed under the ISC License, Copyright 2010–2026 Mike Bostock and Copyright 2018–2021 Observable, Inc.
53
+
54
+ ## ATTRIBUTION AND CITATION
55
+
56
+ Any publication or report presenting results generated by the Software must acknowledge it as: "Analysis performed using Carm (https://saqr.me)".
57
+
58
+ Where the work makes substantive use of CarmNote, the following citation should also be included:
59
+
60
+ Saqr, M., & Lopez-Pernas, S. (2026). CarmNote: A Portable, Reproducible, Single-File Computational Software Purely in JavaScript. The 26th International Symposium on Computers in Education (SIIE 2026).
61
+
62
+ ## TERMINATION
63
+
64
+ This License terminates automatically upon violation of its terms. Upon termination, the user must cease all use and destroy all copies.
65
+
66
+ The DEFINITIONS, INTELLECTUAL PROPERTY, MACHINE LEARNING AND AUTOMATED PROCESSING, THIRD-PARTY COMPONENTS, DISCLAIMER, SEVERABILITY, and GOVERNING LAW sections survive termination of this License.
67
+
68
+ ## AMENDMENTS
69
+
70
+ The copyright holder reserves the right to amend the terms of this License. Any such amendments shall apply prospectively to copies of the Software obtained or updated after the effective date of the amendment. Prior versions shall remain governed by the License terms in effect at the time of acquisition.
71
+
72
+ ## DISCLAIMER
73
+
74
+ THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE COPYRIGHT HOLDER SHALL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF DATA, PROFITS, OR BUSINESS INTERRUPTION, ARISING FROM OR RELATED TO THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE COPYRIGHT HOLDER HAS NO OBLIGATION TO PROVIDE SUPPORT, MAINTENANCE, OR UPDATES.
75
+
76
+ The Software is under active development and continuous improvement. Features, functionality, interfaces, algorithms, and documentation may be added, modified, improved, or discontinued without notice, and some functionality may be experimental or subject to change. Users are responsible for validating the suitability and accuracy of the Software and any results produced for their intended use.
77
+
78
+ ## SEVERABILITY
79
+
80
+ If any provision of this License is held invalid or unenforceable in any jurisdiction, that provision shall be modified to the minimum extent necessary to make it enforceable, or severed if modification is not possible. The remaining provisions shall continue in full force and effect, and the invalidity of a provision in one jurisdiction shall not affect its validity in any other.
81
+
82
+ ## GOVERNING LAW
83
+
84
+ This License is governed by the laws of Finland, excluding its conflict-of-laws rules and the United Nations Convention on Contracts for the International Sale of Goods. The courts of Finland shall have exclusive jurisdiction over any dispute arising under it, without prejudice to any mandatory consumer or statutory protections available to the user under the law of their place of residence.
85
+
86
+ ## CONTACT
87
+
88
+ For paid licensing, institutional agreements, or inquiries:
89
+
90
+ Professor Mohammed Saqr, PhD
91
+ https://saqr.me
92
+ saqr@saqr.me
package/NOTICES ADDED
@@ -0,0 +1,72 @@
1
+ Beatrina — third-party components
2
+ =================================
3
+
4
+ Beatrina is distributed under the Carm Research License v1.4 (LICENSE). That licence's
5
+ THIRD-PARTY COMPONENTS section says the components below remain their authors' property under
6
+ their own terms, reproduced here; nothing in the licence limits the rights they grant or the
7
+ obligations they impose. This file is the npm package's counterpart of the R package's AUTHORS
8
+ and LICENSE.note, and records what the package ACTUALLY carries.
9
+
10
+ What is Beatrina's own
11
+ ----------------------
12
+
13
+ The host (host/), the engines (engines/), the kernel scripts (kernel/), the failsafe supervisor
14
+ (failsafe/), the command (bin/), the acceptance check (check/) and the notebook page
15
+ (carmar_V<version>.html) are written by Mohammed Saqr. The page is one self-contained file built
16
+ from one JavaScript source tree by one author; the notebook shell inside it was written for the
17
+ carmnote-core project (same author) and vendored into this tree on 2026-09-10. No third-party
18
+ JavaScript library is bundled into the page: this was checked at packaging time by inspecting the
19
+ built file for library banners and API surfaces, and the D3.js notice in the licence's
20
+ THIRD-PARTY COMPONENTS section applies to CarmNote artifacts, not to this page, which carries no D3.
21
+
22
+ Data files from other authors, shipped in this package
23
+ ------------------------------------------------------
24
+
25
+ kernel/plugins/csl/<id>/<id>.csl — six Citation Style Language styles that pandoc reads to format
26
+ citations: APA Style 7th edition (apa), Chicago Manual of Style 18th edition author-date
27
+ (chicago-author-date), Cite Them Right 12th edition Harvard (harvard-cite-them-right), IEEE
28
+ Reference Guide (ieee), MLA Handbook 9th edition (modern-language-association), Nature (nature).
29
+ From the CSL project's style repository, github.com/citation-style-language/styles, licensed
30
+ Creative Commons Attribution-ShareAlike 3.0 (creativecommons.org/licenses/by-sa/3.0/); each file
31
+ names its own authors in its <info> block and carries its <rights> element unchanged. They are
32
+ data, not code, shipped unmodified beside a plugin.json manifest recording each file's SHA-256.
33
+
34
+ kernel/examples/tna-complete-tutorial.Rmd — the complete tutorial vignette of the tna R package
35
+ (github.com/sonsoleslp/tna, revision 5c08ec9), copied unmodified: MIT License, Copyright (c)
36
+ 2025-2026 tna authors. The full licence text is beside it in kernel/examples/NOTICE.md. It is a
37
+ document a person may open; nothing loads or runs it by itself.
38
+
39
+ Colour values taken from another project
40
+ ----------------------------------------
41
+
42
+ The page carries 34 editor colour palettes whose colour VALUES are taken from the Ace editor's
43
+ theme stylesheets as the RStudio IDE ships them: Ace editor themes, BSD-3-Clause, Copyright (c)
44
+ 2010, Ajax.org B.V. (github.com/ajaxorg/ace, src/theme/). No Ace code is included — the palettes
45
+ are data, one object of CSS custom properties per theme, with text colours adjusted where needed
46
+ to reach 4.5:1 contrast. The theme names are the ones RStudio uses so a person can find the one
47
+ they know.
48
+
49
+ Fetched by npm at install time, not shipped
50
+ -------------------------------------------
51
+
52
+ node-pty (github.com/microsoft/node-pty), MIT License, Copyright (c) 2012-2015 Christopher
53
+ Jeffrey and Copyright (c) 2016 The xterm.js authors — an OPTIONAL dependency: npm installs its
54
+ prebuilt binary where one exists and the terminal pane runs on pipes where it does not. Its
55
+ licence text arrives with it under node_modules/node-pty/LICENSE.
56
+
57
+ Fetched by the page at runtime, on the user's request, not shipped
58
+ ------------------------------------------------------------------
59
+
60
+ en_US spelling data — SCOWL (Kevin Atkinson), via the dictionary-en 4.0.0 npm package, fetched
61
+ from cdn.jsdelivr.net when the user turns spell checking on.
62
+
63
+ @mlc-ai/web-llm 0.2.84 — Apache-2.0, fetched from cdn.jsdelivr.net when the user chooses the
64
+ in-browser (WebGPU) AI provider.
65
+
66
+ What the engines run on
67
+ -----------------------
68
+
69
+ The R engine runs the user's own R and needs the CRAN packages jsonlite, knitr and evaluate in the
70
+ user's library (offered for installation on first run; never shipped). The Python engine
71
+ (engines/python) imports only the Python standard library. The JavaScript engine runs on the Node
72
+ that runs Beatrina. None of those runtimes or packages is part of this package.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # Beatrina
2
+
3
+ Beatrina is a modern, powerful, feature-rich integrated development environment (IDE) for R, Python, and JavaScript.
4
+
5
+ It provides a unified environment for interactive computing, scientific programming, data analysis, and reproducible research. Beatrina runs locally on your computer, providing direct access to your code, computational environment, data, and installed packages.
6
+
7
+ Beatrina supports R, Python, and JavaScript notebooks, with computation performed locally on your machine.
8
+
9
+ ## Installation
10
+
11
+ Beatrina requires Node.js 22 or later and R.
12
+
13
+ ```sh
14
+ npm install -g beatrina
15
+ beatrina
16
+ ```
17
+
18
+ On first launch, Beatrina checks for the required R packages:
19
+
20
+ - jsonlite
21
+ - knitr
22
+ - evaluate
23
+
24
+ If any are missing, Beatrina offers to install them.
25
+
26
+ ## Command-Line Interface
27
+
28
+ ```sh
29
+ beatrina doctor
30
+ ```
31
+
32
+ Reports the detected development environment, including Node.js, R, required R packages, Python, and the latest Beatrina version available through npm.
33
+
34
+ ```sh
35
+ beatrina status
36
+ ```
37
+
38
+ Displays active Beatrina sessions.
39
+
40
+ ```sh
41
+ beatrina stop
42
+ ```
43
+
44
+ Stops the active Beatrina session.
45
+
46
+ ```sh
47
+ beatrina shortcut
48
+ ```
49
+
50
+ Creates an application icon and configures Open With integration for `.qmd` and `.Rmd` files.
51
+
52
+ ```sh
53
+ beatrina check
54
+ ```
55
+
56
+ Runs the Beatrina acceptance checks on the current machine.
57
+
58
+ ```sh
59
+ beatrina upgrade
60
+ ```
61
+
62
+ Installs the latest version of Beatrina from npm.
63
+
64
+ ```sh
65
+ beatrina --version
66
+ ```
67
+
68
+ Displays the installed Beatrina version.
69
+
70
+ ## Upgrading
71
+
72
+ When Beatrina starts, it periodically checks npm for a newer version and reports its availability in the terminal and in the notebook.
73
+
74
+ The available update information can also be found in the notebook under:
75
+
76
+ - Info → Build
77
+ - Tools ▾ → Doctor
78
+
79
+ To upgrade:
80
+
81
+ ```sh
82
+ beatrina upgrade
83
+ ```
84
+
85
+ or:
86
+
87
+ ```sh
88
+ npm install -g beatrina@latest
89
+ ```
90
+
91
+ Running sessions continue with the version they were started with. Restart the session from:
92
+
93
+ - Session ▾ → Restart
94
+
95
+ or restart Beatrina from the terminal:
96
+
97
+ ```sh
98
+ beatrina stop
99
+ beatrina
100
+ ```
101
+
102
+ Your documents, R packages, and settings are preserved during an upgrade.
103
+
104
+ To disable the periodic update check:
105
+
106
+ ```sh
107
+ BEATRINA_NO_UPDATE_CHECK=1
108
+ ```
109
+
110
+ ## Uninstallation
111
+
112
+ ```sh
113
+ npm uninstall -g beatrina
114
+ ```
115
+
116
+ ## License
117
+
118
+ Beatrina is distributed under the Carm Research License v1.4, provided in the LICENSE file.
119
+
120
+ The license permits free use for research, teaching, and personal learning. Commercial use requires a paid license.
121
+
122
+ For commercial licensing, visit https://saqr.me or contact saqr@saqr.me.
123
+
124
+ The NOTICES file lists the third-party components included with Beatrina and their respective license terms.
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+ // beatrina — start a session from the command line (npm install -g beatrina).
3
+ //
4
+ // The host runs in THIS process, in the foreground, as a CLI tool does: Ctrl+C
5
+ // stops the session. Before it starts, three things are checked and said in
6
+ // words: Node is new enough, R is there, and R has the packages the worker needs
7
+ // (asked once, installed from CRAN into the user's own library on yes).
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { USAGE, ask, installRPackages, missingRPackages, parseArgs, REQUIRED_R_PACKAGES } from "./cli.mjs";
13
+
14
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
16
+ const fail = (msg) => { console.error(`beatrina: ${msg}`); process.exit(1); };
17
+
18
+ let args;
19
+ try { args = parseArgs(process.argv.slice(2)); } catch (e) { fail(e.message); }
20
+ if (args.help) { console.log(USAGE); process.exit(0); }
21
+ if (args.version) { console.log(pkg.version); process.exit(0); }
22
+
23
+ const major = Number(process.versions.node.split(".")[0]);
24
+ if (major < 22) fail(`Beatrina needs Node.js 22 or newer; this is ${process.version}. Update Node from https://nodejs.org.`);
25
+
26
+ const { APP_ID, describeMigration, migrateAppData } = await import("./identity.mjs");
27
+
28
+ if (args.command === "status" || args.command === "stop") {
29
+ const { chooseToStop, formatSessions, listSessions, stopSession } = await import("./sessions.mjs");
30
+ const rows = await listSessions();
31
+ if (args.command === "status") {
32
+ for (const line of formatSessions(rows)) console.log(line);
33
+ process.exit(0);
34
+ }
35
+ const chosen = chooseToStop(rows, { port: args.port, all: args.all });
36
+ if (chosen.error) fail(chosen.error);
37
+ if (!chosen.rows.length) { console.log("No Beatrina session is running."); process.exit(0); }
38
+ let failed = 0;
39
+ for (const row of chosen.rows) {
40
+ const done = await stopSession(row);
41
+ const said = { stopped: `Stopped the session on port ${row.port}.`, killed: `The session on port ${row.port} did not answer the request to stop; its process ${row.pid} was ended.`,
42
+ "removed-stale": `Removed the record of a session on port ${row.port} that was no longer running.`, failed: `Could not stop the session on port ${row.port}: ${done.detail}` }[done.outcome];
43
+ if (done.outcome === "failed") { failed += 1; console.error(said); } else console.log(said);
44
+ }
45
+ process.exit(failed ? 1 : 0);
46
+ }
47
+
48
+ if (args.command === "upgrade") {
49
+ // The explicit door (upgrade.mjs): npm's work, so R is not needed for it.
50
+ const { runUpgrade } = await import("./upgrade.mjs");
51
+ const { listSessions } = await import("./sessions.mjs");
52
+ process.exit(await runUpgrade({ current: pkg.version, listSessions }));
53
+ }
54
+
55
+ const { detectRscript } = await import("../host/engine-r.mjs");
56
+ const { detectPython } = await import("../host/engine-python.mjs");
57
+ const rscript = detectRscript(process.env.CARMAR_RSCRIPT || "");
58
+
59
+ if (args.command === "doctor") {
60
+ const rows = [["Beatrina", `${pkg.version} (${root})`], ["Node.js", `${process.version} at ${process.execPath}`]];
61
+ {
62
+ // Whether npm has a newer Beatrina: asked here because doctor IS the question (the once-a-day
63
+ // check at start is update-check.mjs); switched off by the same variables as that check.
64
+ const { compareVersions, fetchLatest, updateCheckOff } = await import("./update-check.mjs");
65
+ const off = updateCheckOff(process.env);
66
+ const latest = off ? null : await fetchLatest({ timeoutMs: 3000 });
67
+ rows.push(["npm registry", off ? "the update check is switched off"
68
+ : !latest ? "not reached — is this machine online?"
69
+ : compareVersions(latest, pkg.version) > 0 ? `Beatrina ${latest} is available (this is ${pkg.version}) — run: beatrina upgrade`
70
+ : `${pkg.version} is the newest`]);
71
+ }
72
+ if (!rscript) rows.push(["R", "NOT FOUND — install R from https://cran.r-project.org"]);
73
+ else {
74
+ rows.push(["R", rscript]);
75
+ let missing = [];
76
+ try { missing = missingRPackages(rscript); } catch (e) { missing = [`(${e.message})`]; }
77
+ rows.push(["R packages", missing.length ? `missing: ${missing.join(", ")} — run beatrina, which offers to install them` : `${REQUIRED_R_PACKAGES.join(", ")}: installed`]);
78
+ }
79
+ const py = await detectPython(process.env);
80
+ rows.push(["Python (optional)", py.available ? `${py.version} at ${py.path}` : py.detail]);
81
+ let pty = "not installed — the terminal pane runs line programs only";
82
+ try { await import("node-pty"); pty = "installed — full terminal"; } catch { /* optional */ }
83
+ rows.push(["node-pty (optional)", pty]);
84
+ const width = Math.max(...rows.map((r) => r[0].length));
85
+ for (const [k, v] of rows) console.log(`${k.padEnd(width)} ${v}`);
86
+ process.exit(rscript ? 0 : 1);
87
+ }
88
+
89
+ if (args.command === "check") {
90
+ // The acceptance check (tools/beatrina/acceptance.mjs, staged as check/) against THIS package's host.
91
+ if (!rscript) fail("the check needs R and none was found. Install R from https://cran.r-project.org.");
92
+ const os = await import("node:os");
93
+ const { spawn } = await import("node:child_process");
94
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), "beatrina-check-"));
95
+ // BEATRINA_CHECK_JUNIT keeps the report where the caller wants it (fordist/beatrina-windows-check.ps1).
96
+ const junit = process.env.BEATRINA_CHECK_JUNIT || path.join(scratch, "acceptance.xml");
97
+ const code = await new Promise((resolve) => {
98
+ const child = spawn(process.execPath, [path.join(root, "check", "acceptance.mjs"), `--host=${path.join(root, "host", "main.mjs")}`, `--junit=${junit}`],
99
+ { stdio: "inherit", env: { ...process.env, CARMAR_RSCRIPT: rscript } });
100
+ child.on("exit", (c) => resolve(c ?? 1));
101
+ child.on("error", () => resolve(1));
102
+ });
103
+ const url = args.prova || process.env.PROVA_URL || "";
104
+ const token = args.token || process.env.PROVA_TOKEN || "";
105
+ if (url && token && fs.existsSync(junit)) {
106
+ const { postJunitRun, rVersionOf, runBody } = await import("./prova-post.mjs");
107
+ let build = { version: pkg.version, commit: "", dirty: false };
108
+ try { build = { ...build, ...JSON.parse(fs.readFileSync(path.join(root, "build-info.json"), "utf8")) }; } catch { /* a package without one */ }
109
+ try {
110
+ const sent = await postJunitRun({ url, token, xml: fs.readFileSync(junit), body: runBody({ build, rVersion: rVersionOf(rscript), label: `beatrina check on ${process.platform}` }) });
111
+ console.log(`Sent to Prova: run ${sent.runId} — pass ${sent.counts?.pass ?? "?"} fail ${sent.counts?.fail ?? "?"} skip ${sent.counts?.skip ?? "?"} → ${sent.url}`);
112
+ } catch (e) { console.error(`beatrina: the results were not sent to Prova: ${e.message}`); }
113
+ } else if (url || token) console.error("beatrina: to send results to Prova give both --prova <url> and --token <token>.");
114
+ fs.rmSync(scratch, { recursive: true, force: true });
115
+ process.exit(code);
116
+ }
117
+
118
+ if (args.command === "shortcut") {
119
+ const { makeShortcut } = await import("./shortcut.mjs");
120
+ try {
121
+ const made = makeShortcut({ cli: fileURLToPath(import.meta.url), dir: args.dir, port: args.port, makeDefault: args.makeDefault, noOpen: !args.open });
122
+ for (const line of made.said) console.log(line);
123
+ process.exit(0);
124
+ } catch (e) { fail(`the shortcut could not be made: ${e.message}`); }
125
+ }
126
+
127
+ // Launched from the icon or a double-clicked document: no terminal to talk to.
128
+ // A running Beatrina session takes the file; otherwise one starts in the
129
+ // background, and anything that goes wrong is said in a dialog.
130
+ if (args.fromApp) {
131
+ const { liveSession } = await import("./shortcut.mjs");
132
+ const { spawn, spawnSync } = await import("node:child_process");
133
+ const alert = (msg) => {
134
+ if (process.platform === "darwin") spawnSync("osascript", ["-e", `display alert "Beatrina" message ${JSON.stringify(msg)}`]);
135
+ else if (process.platform === "win32") spawnSync("powershell.exe", ["-NoProfile", "-Command", `Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show(${JSON.stringify(msg).replace(/^"|"$/g, "'")}, 'Beatrina')`]);
136
+ else console.error(msg);
137
+ };
138
+ const file = args.file ? path.resolve(args.file) : "";
139
+ const live = await liveSession({ app: process.env.BEATRIX_APP_ID || APP_ID, port: args.port });
140
+ if (live) {
141
+ let delivered = "none";
142
+ if (file) {
143
+ try {
144
+ const res = await fetch(`http://127.0.0.1:${live.port}/open?file=${encodeURIComponent(file)}`, { signal: AbortSignal.timeout(5000) });
145
+ delivered = res.ok ? (await res.json()).delivered : "refused";
146
+ } catch { delivered = "refused"; }
147
+ }
148
+ // A page already shows the session: the document went there. No page (the
149
+ // browser was closed): open the session's notebook, which collects it.
150
+ if (delivered !== "page" && args.open && live.file) {
151
+ // BEATRINA_OPENER substitutes the program that opens it (test/beatrina-sessions.test.mjs).
152
+ const opener = process.env.BEATRINA_OPENER ? [process.env.BEATRINA_OPENER, [live.file]]
153
+ : process.platform === "darwin" ? ["open", [live.file]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", live.file]] : ["xdg-open", [live.file]];
154
+ spawnSync(opener[0], opener[1]);
155
+ }
156
+ if (delivered === "refused") alert(`The running Beatrina session could not open ${file}.`);
157
+ process.exit(0);
158
+ }
159
+ if (!rscript) { alert("Beatrina needs R. Install R from https://cran.r-project.org, then open Beatrina again."); process.exit(1); }
160
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), ...(file ? [file] : []), "--yes",
161
+ ...(args.port ? ["--port", args.port] : []), ...(args.open ? [] : ["--no-open"])], { detached: true, stdio: "ignore", windowsHide: true });
162
+ child.unref();
163
+ process.exit(0);
164
+ }
165
+
166
+ if (!rscript) fail("Beatrina needs R and none was found. Install R from https://cran.r-project.org, or set CARMAR_RSCRIPT to your Rscript.");
167
+ let missing;
168
+ try { missing = missingRPackages(rscript); } catch (e) { fail(e.message); }
169
+ if (missing.length) {
170
+ const yes = args.yes || process.env.BEATRIX_YES === "1" ? true
171
+ : await ask(`R is missing ${missing.join(", ")}, which Beatrina needs. Install from CRAN now?`);
172
+ if (yes === null) fail(`R is missing ${missing.join(", ")}. Run beatrina in a terminal to be asked, or run: beatrina --yes`);
173
+ if (!yes) fail(`Beatrina cannot start without ${missing.join(", ")}.`);
174
+ if (!installRPackages(rscript, missing)) fail(`installing ${missing.join(", ")} failed; the messages above say why.`);
175
+ }
176
+
177
+ // Beatrina's own data folders and port, unless the caller chose otherwise. Data a Beatrix build
178
+ // left under R/beatrix moves across once (identity.mjs).
179
+ if (!process.env.BEATRIX_APP_ID) {
180
+ process.env.BEATRIX_APP_ID = APP_ID;
181
+ const { rUserDir } = await import("../host/user-dirs.mjs");
182
+ for (const line of describeMigration(migrateAppData({ dirOf: rUserDir }))) console.error(`beatrina: ${line}`);
183
+ }
184
+ process.env.CARMAR_RSCRIPT = rscript;
185
+ if (args.port) process.env.CARMAR_PORT = args.port;
186
+ else process.env.CARMAR_PORT ||= "4848";
187
+ if (args.file) {
188
+ const file = path.resolve(args.file);
189
+ if (!fs.existsSync(file)) fail(`no such file: ${file}`);
190
+ process.env.CARMAR_OPEN_FILE = file;
191
+ }
192
+ const open = args.open && process.env.BEATRIX_NO_OPEN !== "1";
193
+ // Once a day: is a newer Beatrina on the registry? Said, never acted on, and never waited for.
194
+ {
195
+ const { checkForUpdate, updateRecordFile } = await import("./update-check.mjs");
196
+ checkForUpdate({ current: pkg.version, stateFile: updateRecordFile(APP_ID) })
197
+ .then((said) => { if (said) console.error(`beatrina: ${said}`); }).catch(() => {});
198
+ }
199
+ const { FAILSAFE_R_PACKAGES, followChild, runUntilAnnounced, stageFailsafe } = await import("./failsafe.mjs");
200
+ const forced = process.env.BEATRINA_FORCE_FAILSAFE === "1";
201
+ const deadlineS = Number(process.env.BEATRINA_HOST_DEADLINE || 60);
202
+ let started = forced
203
+ ? { announced: false, refused: false, reason: "BEATRINA_FORCE_FAILSAFE=1 asked for it", code: null }
204
+ : await runUntilAnnounced({ cmd: process.execPath, args: [path.join(root, "host", "main.mjs"), ...(open ? ["--open"] : [])], env: process.env,
205
+ deadlineMs: (Number.isFinite(deadlineS) && deadlineS > 0 ? deadlineS : 60) * 1000 });
206
+ if (started.announced) followChild(started.child);
207
+ else {
208
+ if (started.refused) process.exit(started.code ?? 2);
209
+ // The failsafe: the R supervisor, with the reason said here and on the page.
210
+ console.error(`beatrina: ${started.reason}. Starting the R supervisor instead (failsafe mode).`);
211
+ let needed;
212
+ try { needed = missingRPackages(rscript, FAILSAFE_R_PACKAGES); } catch (e) { fail(e.message); }
213
+ if (needed.length) {
214
+ const yes = args.yes || process.env.BEATRIX_YES === "1" ? true : await ask(`The failsafe needs the R packages ${needed.join(", ")}. Install from CRAN now?`);
215
+ if (!yes || !installRPackages(rscript, needed)) fail(`Beatrina could not start: ${started.reason}, and the failsafe needs ${needed.join(", ")}.`);
216
+ }
217
+ let staged;
218
+ try { staged = stageFailsafe(root); } catch (e) { fail(`Beatrina could not start: ${started.reason}, and ${e.message}.`); }
219
+ started = await runUntilAnnounced({ cmd: rscript, args: [staged.serve, ...(open ? ["--open"] : [])],
220
+ env: { ...process.env, BEATRINA_FAILSAFE_REASON: started.reason }, deadlineMs: 120000 });
221
+ if (!started.announced) fail(`the failsafe did not start either: ${started.reason.replace("the Node host", "the R supervisor")}.`);
222
+ followChild(started.child, { onExit: (code) => { fs.rmSync(staged.stage, { recursive: true, force: true }); process.exit(code); } });
223
+ }
package/bin/cli.mjs ADDED
@@ -0,0 +1,80 @@
1
+ // cli.mjs — the `beatrina` command's decisions, kept apart from starting the host so they can be tested.
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import readline from "node:readline";
5
+
6
+ /** The R packages the worker cannot run without (spike/worker.R, knitr-run.R). */
7
+ export const REQUIRED_R_PACKAGES = Object.freeze(["jsonlite", "knitr", "evaluate"]);
8
+
9
+ export const USAGE = `beatrina — R, Python and JavaScript notebooks on your own computer
10
+
11
+ beatrina [file] start a session and open the notebook (Ctrl+C stops it)
12
+ beatrina status list running sessions: port, process, uptime, document
13
+ beatrina stop stop the running session (--port <n> for one of several, --all for every one)
14
+ beatrina doctor show what Beatrina found: Node, R, R packages, Python, and whether npm has a newer Beatrina
15
+ beatrina check run the acceptance check on this machine (a private session on a spare port);
16
+ --prova <url> --token <t> (or PROVA_URL / PROVA_TOKEN) sends the results to Prova
17
+ beatrina shortcut add a Beatrina icon, and "Open With ▸ Beatrina" for .qmd and .Rmd files
18
+ (--default also makes it the default app for them)
19
+ beatrina upgrade install the newest Beatrina from npm (npm install -g beatrina@latest);
20
+ a session already running keeps this version until it restarts
21
+
22
+ --port <n> port for the session (default 4848)
23
+ --no-open do not open a browser
24
+ --yes install missing R packages without asking
25
+ --version, --help`;
26
+
27
+ export const COMMANDS = Object.freeze(["doctor", "shortcut", "status", "stop", "check", "upgrade"]);
28
+
29
+ /** Parse the command line. */
30
+ export function parseArgs(argv) {
31
+ const out = { command: "run", file: "", port: "", open: true, yes: false, help: false, version: false, fromApp: false, makeDefault: false, dir: "", all: false, prova: "", token: "" };
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const a = argv[i];
34
+ if (a === "--help" || a === "-h") out.help = true;
35
+ else if (a === "--version" || a === "-v") out.version = true;
36
+ else if (a === "--no-open") out.open = false;
37
+ else if (a === "--yes" || a === "-y") out.yes = true;
38
+ else if (a === "--port") out.port = argv[++i] || "";
39
+ else if (a.startsWith("--port=")) out.port = a.slice(7);
40
+ else if (COMMANDS.includes(a) && out.command === "run" && !out.file) out.command = a;
41
+ else if (a === "--all") out.all = true;
42
+ else if (a === "--prova") out.prova = argv[++i] || "";
43
+ else if (a === "--token") out.token = argv[++i] || "";
44
+ else if (a === "--from-app") out.fromApp = true;
45
+ else if (a === "--default") out.makeDefault = true;
46
+ else if (a === "--dir") out.dir = argv[++i] || "";
47
+ else if (a.startsWith("-")) throw new Error(`unknown option ${a}\n\n${USAGE}`);
48
+ else if (!out.file) out.file = a;
49
+ else throw new Error(`one file at a time\n\n${USAGE}`);
50
+ }
51
+ if (out.port && !/^\d{2,5}$/.test(out.port)) throw new Error(`--port must be a number, not ${JSON.stringify(out.port)}`);
52
+ if (out.all && out.command !== "stop") throw new Error(`--all belongs to beatrina stop\n\n${USAGE}`);
53
+ if ((out.prova || out.token) && out.command !== "check") throw new Error(`--prova and --token belong to beatrina check\n\n${USAGE}`);
54
+ if (out.file && (out.command === "status" || out.command === "stop" || out.command === "check" || out.command === "upgrade")) throw new Error(`beatrina ${out.command} takes no file\n\n${USAGE}`);
55
+ return out;
56
+ }
57
+
58
+ /** Which of `packages` this R cannot load. Asked of R itself, never guessed. */
59
+ export function missingRPackages(rscript, packages = REQUIRED_R_PACKAGES) {
60
+ const code = `p <- c(${packages.map((x) => JSON.stringify(x)).join(", ")}); cat(p[!vapply(p, requireNamespace, logical(1), quietly = TRUE)], sep = "\\n")`;
61
+ const r = spawnSync(rscript, ["-e", code], { encoding: "utf8", timeout: 60000 });
62
+ if (r.status !== 0) throw new Error(`R could not be asked which packages it has: ${String(r.stderr).trim().split("\n").pop()}`);
63
+ return String(r.stdout).split("\n").map((s) => s.trim()).filter(Boolean);
64
+ }
65
+
66
+ /** Install packages into the user's own R library (created if needed), from CRAN. */
67
+ export function installRPackages(rscript, packages) {
68
+ const code = `lib <- Sys.getenv("R_LIBS_USER"); dir.create(lib, recursive = TRUE, showWarnings = FALSE); `
69
+ + `.libPaths(c(lib, .libPaths())); install.packages(c(${packages.map((x) => JSON.stringify(x)).join(", ")}), lib = lib, repos = "https://cloud.r-project.org")`;
70
+ return spawnSync(rscript, ["-e", code], { stdio: "inherit", timeout: 1800000 }).status === 0;
71
+ }
72
+
73
+ /** Ask a yes/no question on a terminal. Anything but n/no is yes. No terminal → null (the caller explains). */
74
+ export async function ask(question, { input = process.stdin, output = process.stdout } = {}) {
75
+ if (!input.isTTY) return null;
76
+ const rl = readline.createInterface({ input, output });
77
+ const answer = await new Promise((resolve) => rl.question(`${question} [Y/n] `, resolve));
78
+ rl.close();
79
+ return !/^\s*n/i.test(answer);
80
+ }