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
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.
|
package/bin/beatrina.mjs
ADDED
|
@@ -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
|
+
}
|