beatrina 0.8.6 → 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 +156 -26
- 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 +271 -35
- 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 +364 -239
- package/failsafe/session-documents.R +104 -0
- 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 +16 -17
- package/host/engine-pool.mjs +56 -8
- package/host/engine-python.mjs +80 -31
- package/host/engine-r.mjs +38 -32
- package/host/engine-stdio.mjs +45 -12
- 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 +216 -76
- 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 +41 -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 +230 -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 +123 -21
- package/host/session-keep.mjs +70 -0
- 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 +80 -7
- 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 +24 -21
- package/kernel/knitr-run.R +50 -9
- package/kernel/latex.R +429 -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 +602 -123
- package/kernel/workspace-keep.R +188 -0
- 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.6.html +0 -1310
package/kernel/settings.R
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# administrator may.
|
|
3
3
|
#
|
|
4
4
|
# Until now every kernel-side preference was an environment variable read once
|
|
5
|
-
# at startup:
|
|
5
|
+
# at startup: BEATRINA_LINGER, BEATRINA_NO_HISTORY, BEATRINA_ROOT, BEATRINA_LOG. That
|
|
6
6
|
# is exactly right for a deployment and useless for a person, who has no place
|
|
7
7
|
# to set one and no way to see what it currently is. This module adds a user
|
|
8
8
|
# config file and — more importantly — the rule for who wins.
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
# · An administrator's setting is NOT IN THIS FILE'S VOCABULARY AT ALL. It is
|
|
26
26
|
# not "env wins if present": the key is rejected on read and refused on
|
|
27
27
|
# write, always. "Env wins when set" only guarantees anything WHEN THE ENV
|
|
28
|
-
# IS SET, and a deployment that never sets
|
|
28
|
+
# IS SET, and a deployment that never sets BEATRINA_ROOT is still one where a
|
|
29
29
|
# user must not invent one — a root the user chose reads as protection
|
|
30
30
|
# while being none (worker.R is explicit that it is a guardrail, not a
|
|
31
31
|
# sandbox). The refusal must not depend on an administrator having
|
|
32
32
|
# remembered to set every variable defensively.
|
|
33
33
|
#
|
|
34
34
|
# The tempting alternative — a layered Sys.getenv-shaped getter handed to
|
|
35
|
-
#
|
|
35
|
+
# beatrina_deployment() and beatrina_ai_policy() — is deliberately NOT built. It
|
|
36
36
|
# would put every admin variable one argument-passing mistake away from being
|
|
37
37
|
# file-settable. Those two keep plain Sys.getenv; only the user-writable call
|
|
38
38
|
# sites in serve.R read this module.
|
|
@@ -51,39 +51,48 @@ SETTINGS_VERSION <- 1L
|
|
|
51
51
|
SETTINGS <- list(
|
|
52
52
|
# Connection loss is not permission to discard the R workspace. Timed
|
|
53
53
|
# shutdown is opt-in; 0 retains an attached session until explicit Quit.
|
|
54
|
-
list(key = "linger_seconds", env = "
|
|
54
|
+
list(key = "linger_seconds", env = "BEATRINA_LINGER", kind = "number",
|
|
55
55
|
default = 0, min = 0, max = 86400, effect = "live",
|
|
56
56
|
label = "Keep R running after all pages disconnect"),
|
|
57
|
-
list(key = "history_enabled", env = "
|
|
57
|
+
list(key = "history_enabled", env = "BEATRINA_NO_HISTORY", kind = "bool",
|
|
58
58
|
default = TRUE, effect = "live", inverted = TRUE,
|
|
59
59
|
label = "Remember console history between sessions"),
|
|
60
|
-
list(key = "analyze_budget_s", env = "
|
|
60
|
+
list(key = "analyze_budget_s", env = "BEATRINA_ANALYZE_BUDGET", kind = "number",
|
|
61
61
|
default = 3, min = 0, max = 60, effect = "next_launch",
|
|
62
62
|
label = "Code-intelligence time budget"),
|
|
63
|
-
list(key = "quarto_path", env = "
|
|
63
|
+
list(key = "quarto_path", env = "BEATRINA_QUARTO", kind = "path",
|
|
64
64
|
default = "", effect = "live",
|
|
65
65
|
label = "Quarto binary"),
|
|
66
|
-
list(key = "rscript_path", env = "
|
|
66
|
+
list(key = "rscript_path", env = "BEATRINA_RSCRIPT", kind = "path",
|
|
67
67
|
default = "", effect = "restart_r",
|
|
68
68
|
label = "R binary"),
|
|
69
|
-
list(key = "port", env = "
|
|
69
|
+
list(key = "port", env = "BEATRINA_PORT", kind = "number",
|
|
70
70
|
default = 4747, min = 1024, max = 65535, effect = "next_launch",
|
|
71
|
-
label = "Preferred port")
|
|
71
|
+
label = "Preferred port"),
|
|
72
|
+
# WHO IS WRITING. Machine scope, beside console history and the AI store,
|
|
73
|
+
# and deliberately NOT the browser: every file:// page shares one origin, so
|
|
74
|
+
# a name kept there would be readable by any local HTML the user opens and
|
|
75
|
+
# would not follow them to a served page either. With no kernel there is no
|
|
76
|
+
# name, which is consistent - with no kernel there is no comment store to
|
|
77
|
+
# attribute anything in.
|
|
78
|
+
list(key = "person_name", env = "BEATRINA_NAME", kind = "text",
|
|
79
|
+
default = "", max = 120, effect = "live",
|
|
80
|
+
label = "Your name")
|
|
72
81
|
)
|
|
73
82
|
|
|
74
83
|
# Reported to the page as read-only facts, never settable. Everything an
|
|
75
84
|
# administrator owns lives here and NOWHERE in SETTINGS, which is what makes
|
|
76
85
|
# the refusal structural rather than a lookup someone can forget.
|
|
77
86
|
SETTINGS_ADMIN <- c(
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
+
"BEATRINA_MANAGED_CONFIG",
|
|
88
|
+
"BEATRINA_ROOT", "BEATRINA_LOG", "BEATRINA_LOG_AI_TEXT",
|
|
89
|
+
"BEATRINA_CRAN_MIRROR",
|
|
90
|
+
"BEATRINA_AI_LOCAL_ONLY", "BEATRINA_AI_POLICY", "BEATRINA_AI_PROVIDERS",
|
|
91
|
+
"BEATRINA_BIND", "BEATRINA_HOSTS", "BEATRINA_ORIGINS", "BEATRINA_REQUIRE_ORIGIN",
|
|
92
|
+
"BEATRINA_TRUST_PROXY", "BEATRINA_USER_HEADER", "BEATRINA_TRUSTED_PROXY",
|
|
93
|
+
"BEATRINA_ALLOW_UNAUTHENTICATED", "BEATRINA_PORT_STRICT",
|
|
94
|
+
"BEATRINA_UPDATE_FEED", "BEATRINA_UPDATE_MIRROR", "BEATRINA_UPDATE_OFFLINE_DIR",
|
|
95
|
+
"BEATRINA_UPDATE_MANAGED"
|
|
87
96
|
)
|
|
88
97
|
|
|
89
98
|
# A managed desktop cannot depend on shell startup files: Finder/LaunchServices
|
|
@@ -92,33 +101,53 @@ SETTINGS_ADMIN <- c(
|
|
|
92
101
|
# become environment variables before deployment, AI, update, or user-setting
|
|
93
102
|
# policy is resolved, so the existing env-wins-and-locks rule remains the one
|
|
94
103
|
# source of truth.
|
|
95
|
-
MANAGED_SETTINGS_SCHEMA <- "
|
|
104
|
+
MANAGED_SETTINGS_SCHEMA <- "beatrina-managed-settings-v1"
|
|
96
105
|
MANAGED_SETTINGS_ENV <- unique(c(
|
|
97
106
|
vapply(SETTINGS, function(s) s$env, character(1)),
|
|
98
|
-
setdiff(SETTINGS_ADMIN, "
|
|
107
|
+
setdiff(SETTINGS_ADMIN, "BEATRINA_MANAGED_CONFIG"),
|
|
99
108
|
"HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"
|
|
100
109
|
))
|
|
110
|
+
# An administrator's file written for Beatrina 0.8.x spells the old product
|
|
111
|
+
# name in its schema string and its keys (host/settings.mjs reads it the same
|
|
112
|
+
# way): both are read, values are reported under the current names.
|
|
113
|
+
# beatrina-names: keep-start
|
|
114
|
+
LEGACY_MANAGED_SETTINGS_SCHEMA <- "carmar-managed-settings-v1"
|
|
115
|
+
managed_current_key <- function(keys) ifelse(keys %in% MANAGED_SETTINGS_ENV, keys, sub("^CARMAR_", "BEATRINA_", keys))
|
|
116
|
+
# beatrina-names: keep-end
|
|
101
117
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
# The places the file is looked for, the current one first; the older one
|
|
119
|
+
# counts only when the current one is absent.
|
|
120
|
+
beatrina_managed_default_paths <- function(env = Sys.getenv,
|
|
121
|
+
os = .Platform$OS.type,
|
|
122
|
+
sysname = Sys.info()[["sysname"]]) {
|
|
105
123
|
if (identical(sysname, "Darwin")) {
|
|
106
|
-
return("/Library/Application Support/
|
|
124
|
+
return(c("/Library/Application Support/Beatrina/managed-settings.json",
|
|
125
|
+
"/Library/Application Support/CarmaR/managed-settings.json")) # beatrina-names: keep
|
|
107
126
|
}
|
|
108
127
|
if (identical(os, "windows")) {
|
|
109
128
|
base <- trimws(env("ProgramData", ""))
|
|
110
129
|
if (!nzchar(base)) base <- "C:/ProgramData"
|
|
111
|
-
return(file.path(base, "
|
|
130
|
+
return(c(file.path(base, "Beatrina", "managed-settings.json"),
|
|
131
|
+
file.path(base, "CarmaR", "managed-settings.json"))) # beatrina-names: keep
|
|
112
132
|
}
|
|
113
|
-
"/etc/
|
|
133
|
+
c("/etc/beatrina/managed-settings.json",
|
|
134
|
+
"/etc/carmar/managed-settings.json") # beatrina-names: keep
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
beatrina_managed_default_path <- function(env = Sys.getenv,
|
|
138
|
+
os = .Platform$OS.type,
|
|
139
|
+
sysname = Sys.info()[["sysname"]]) {
|
|
140
|
+
paths <- beatrina_managed_default_paths(env, os, sysname)
|
|
141
|
+
present <- paths[file.exists(paths)]
|
|
142
|
+
if (length(present)) present[[1]] else paths[[1]]
|
|
114
143
|
}
|
|
115
144
|
|
|
116
145
|
# Protection is checked from the standard user's point of view. Unix also
|
|
117
146
|
# requires root ownership because a user-owned 0400 file can simply be chmod'd
|
|
118
|
-
# and replaced. On Windows, the deployment recipe gives ProgramData/
|
|
147
|
+
# and replaced. On Windows, the deployment recipe gives ProgramData/Beatrina an
|
|
119
148
|
# Administrators+SYSTEM write ACL; neither the file nor its parent may be
|
|
120
149
|
# writable by the running account.
|
|
121
|
-
|
|
150
|
+
beatrina_managed_protected <- function(path, os = .Platform$OS.type) {
|
|
122
151
|
if (nzchar(Sys.readlink(path))) return(FALSE)
|
|
123
152
|
if (file.access(path, 2L) == 0L || file.access(dirname(path), 2L) == 0L) {
|
|
124
153
|
return(FALSE)
|
|
@@ -135,13 +164,13 @@ carmar_managed_protected <- function(path, os = .Platform$OS.type) {
|
|
|
135
164
|
# Read one administrator policy atomically. Unlike the user settings file,
|
|
136
165
|
# unknown or malformed entries fail the WHOLE policy closed: partial policy is
|
|
137
166
|
# not a safe enterprise fallback.
|
|
138
|
-
|
|
139
|
-
protection =
|
|
167
|
+
beatrina_managed_read <- function(path, required = FALSE,
|
|
168
|
+
protection = beatrina_managed_protected) {
|
|
140
169
|
empty <- list(status = "missing", path = path, values = character(), errors = character())
|
|
141
170
|
if (!file.exists(path)) {
|
|
142
171
|
if (isTRUE(required)) {
|
|
143
172
|
empty$status <- "invalid"
|
|
144
|
-
empty$errors <- "
|
|
173
|
+
empty$errors <- "BEATRINA_MANAGED_CONFIG names a file that does not exist."
|
|
145
174
|
}
|
|
146
175
|
return(empty)
|
|
147
176
|
}
|
|
@@ -149,7 +178,7 @@ carmar_managed_read <- function(path, required = FALSE,
|
|
|
149
178
|
empty$status <- "insecure"
|
|
150
179
|
empty$errors <- paste0(
|
|
151
180
|
"The managed settings file is writable by the current account, is a symlink, ",
|
|
152
|
-
"or is not administrator-owned;
|
|
181
|
+
"or is not administrator-owned; Beatrina refused it.")
|
|
153
182
|
return(empty)
|
|
154
183
|
}
|
|
155
184
|
size <- file.info(path)$size[[1]]
|
|
@@ -163,7 +192,8 @@ carmar_managed_read <- function(path, required = FALSE,
|
|
|
163
192
|
root_names <- if (is.list(raw)) names(raw) else NULL
|
|
164
193
|
if (is.null(root_names) || anyDuplicated(root_names) ||
|
|
165
194
|
!setequal(root_names, c("schema", "settings")) ||
|
|
166
|
-
!identical(raw$schema, MANAGED_SETTINGS_SCHEMA) ||
|
|
195
|
+
!(identical(raw$schema, MANAGED_SETTINGS_SCHEMA) ||
|
|
196
|
+
identical(raw$schema, LEGACY_MANAGED_SETTINGS_SCHEMA)) ||
|
|
167
197
|
!is.list(raw$settings) || is.null(names(raw$settings)) ||
|
|
168
198
|
anyDuplicated(names(raw$settings))) {
|
|
169
199
|
empty$status <- "invalid"
|
|
@@ -171,7 +201,8 @@ carmar_managed_read <- function(path, required = FALSE,
|
|
|
171
201
|
MANAGED_SETTINGS_SCHEMA, " with one settings object.")
|
|
172
202
|
return(empty)
|
|
173
203
|
}
|
|
174
|
-
keys <- names(raw$settings)
|
|
204
|
+
keys <- managed_current_key(names(raw$settings))
|
|
205
|
+
names(raw$settings) <- keys
|
|
175
206
|
unknown <- setdiff(keys, MANAGED_SETTINGS_ENV)
|
|
176
207
|
bad <- keys[!vapply(raw$settings, function(value) {
|
|
177
208
|
is.character(value) && length(value) == 1L && !is.na(value) &&
|
|
@@ -194,12 +225,12 @@ carmar_managed_read <- function(path, required = FALSE,
|
|
|
194
225
|
list(status = "ok", path = path, values = values, errors = character())
|
|
195
226
|
}
|
|
196
227
|
|
|
197
|
-
|
|
198
|
-
protection =
|
|
228
|
+
beatrina_managed_environment <- function(env = Sys.getenv, path = NULL,
|
|
229
|
+
protection = beatrina_managed_protected,
|
|
199
230
|
apply_values = TRUE) {
|
|
200
|
-
explicit <- trimws(env("
|
|
201
|
-
if (is.null(path)) path <- if (nzchar(explicit)) explicit else
|
|
202
|
-
result <-
|
|
231
|
+
explicit <- trimws(env("BEATRINA_MANAGED_CONFIG", ""))
|
|
232
|
+
if (is.null(path)) path <- if (nzchar(explicit)) explicit else beatrina_managed_default_path(env)
|
|
233
|
+
result <- beatrina_managed_read(path, required = nzchar(explicit), protection = protection)
|
|
203
234
|
if (isTRUE(apply_values) && identical(result$status, "ok") && length(result$values)) {
|
|
204
235
|
do.call(Sys.setenv, as.list(result$values))
|
|
205
236
|
}
|
|
@@ -212,8 +243,8 @@ settings_entry <- function(key) {
|
|
|
212
243
|
}
|
|
213
244
|
|
|
214
245
|
#' Where the user's settings live — beside the AI key, same directory.
|
|
215
|
-
|
|
216
|
-
dir <- tools::R_user_dir("
|
|
246
|
+
beatrina_settings_path <- function(env = Sys.getenv) {
|
|
247
|
+
dir <- tools::R_user_dir("beatrina", "config")
|
|
217
248
|
dir.create(dir, recursive = TRUE, showWarnings = FALSE)
|
|
218
249
|
file.path(dir, "settings.json")
|
|
219
250
|
}
|
|
@@ -234,7 +265,7 @@ settings_insecure <- function(path) {
|
|
|
234
265
|
|
|
235
266
|
#' Validate one value against its entry.
|
|
236
267
|
#' @return list(ok, value, reason)
|
|
237
|
-
|
|
268
|
+
beatrina_settings_validate <- function(key, value) {
|
|
238
269
|
entry <- settings_entry(key)
|
|
239
270
|
if (is.null(entry)) {
|
|
240
271
|
if (key %in% SETTINGS_ADMIN) {
|
|
@@ -264,6 +295,24 @@ carmar_settings_validate <- function(key, value) {
|
|
|
264
295
|
# becomes a coin toss.
|
|
265
296
|
return(list(ok = TRUE, value = as.numeric(value)))
|
|
266
297
|
}
|
|
298
|
+
if (identical(entry$kind, "text")) {
|
|
299
|
+
if (!is.character(value) || length(value) != 1L || is.na(value)) {
|
|
300
|
+
return(list(ok = FALSE, reason = "must be a string"))
|
|
301
|
+
}
|
|
302
|
+
text <- trimws(value)
|
|
303
|
+
cap <- if (is.null(entry$max)) 200L else entry$max
|
|
304
|
+
if (nchar(text) > cap) {
|
|
305
|
+
return(list(ok = FALSE, reason = paste0("must be at most ", cap, " characters")))
|
|
306
|
+
}
|
|
307
|
+
# A control character is not a typo here: this value is written into
|
|
308
|
+
# JSON-lines logs (a comment's author), and a newline in one of those
|
|
309
|
+
# forges a record. ai-audit strips them for the same reason; a setting can
|
|
310
|
+
# refuse instead, because a person typed it and can be told.
|
|
311
|
+
if (grepl("[\u0001-\u001f\u007f]", text)) {
|
|
312
|
+
return(list(ok = FALSE, reason = "must not contain control characters"))
|
|
313
|
+
}
|
|
314
|
+
return(list(ok = TRUE, value = text))
|
|
315
|
+
}
|
|
267
316
|
# path
|
|
268
317
|
if (!is.character(value) || length(value) != 1L || is.na(value)) {
|
|
269
318
|
return(list(ok = FALSE, reason = "must be a string"))
|
|
@@ -288,7 +337,7 @@ carmar_settings_validate <- function(key, value) {
|
|
|
288
337
|
#' Per-key failure stays per-key: an unknown key joins `ignored`, a bad value
|
|
289
338
|
#' joins `rejected`, and the rest of the file still applies. One bad number
|
|
290
339
|
#' must not silently revert eleven good ones.
|
|
291
|
-
|
|
340
|
+
beatrina_settings_read <- function(path = beatrina_settings_path()) {
|
|
292
341
|
empty <- list(status = "missing", path = path, values = list(),
|
|
293
342
|
ignored = character(), rejected = list(), error = "")
|
|
294
343
|
if (!file.exists(path)) return(empty)
|
|
@@ -304,7 +353,7 @@ carmar_settings_read <- function(path = carmar_settings_path()) {
|
|
|
304
353
|
}
|
|
305
354
|
if (!identical(as.integer(raw$version %||% 0L), SETTINGS_VERSION)) {
|
|
306
355
|
return(modifyList(empty, list(status = "invalid",
|
|
307
|
-
error = "The settings file is a version this
|
|
356
|
+
error = "The settings file is a version this Beatrina does not know.")))
|
|
308
357
|
}
|
|
309
358
|
block <- raw$settings
|
|
310
359
|
if (is.null(block)) block <- list()
|
|
@@ -316,7 +365,7 @@ carmar_settings_read <- function(path = carmar_settings_path()) {
|
|
|
316
365
|
ignored <- character()
|
|
317
366
|
rejected <- list()
|
|
318
367
|
for (key in names(block)) {
|
|
319
|
-
verdict <-
|
|
368
|
+
verdict <- beatrina_settings_validate(key, block[[key]])
|
|
320
369
|
if (isTRUE(verdict$ok)) {
|
|
321
370
|
values[[key]] <- verdict$value
|
|
322
371
|
} else if (identical(verdict$reason, "unknown setting")) {
|
|
@@ -332,7 +381,7 @@ carmar_settings_read <- function(path = carmar_settings_path()) {
|
|
|
332
381
|
#' Write the whole settings block. Create → chmod 0600 → CHECK → write, the
|
|
333
382
|
#' ordering ai_key_write() uses and for the same reason: a file that was
|
|
334
383
|
#' briefly group-writable was briefly tamperable.
|
|
335
|
-
|
|
384
|
+
beatrina_settings_write <- function(values, path = beatrina_settings_path()) {
|
|
336
385
|
if (!file.exists(path) && !file.create(path, showWarnings = FALSE)) return(FALSE)
|
|
337
386
|
if (!isTRUE(Sys.chmod(path, "0600"))) {
|
|
338
387
|
unlink(path)
|
|
@@ -352,8 +401,8 @@ carmar_settings_write <- function(values, path = carmar_settings_path()) {
|
|
|
352
401
|
#' The whole decision: what every setting resolves to, and from where.
|
|
353
402
|
#'
|
|
354
403
|
#' @return list(settings = list of per-key records, admin = list, file = the read)
|
|
355
|
-
|
|
356
|
-
if (is.null(file)) file <-
|
|
404
|
+
beatrina_settings_resolve <- function(env = Sys.getenv, file = NULL) {
|
|
405
|
+
if (is.null(file)) file <- beatrina_settings_read()
|
|
357
406
|
usable <- identical(file$status, "ok")
|
|
358
407
|
|
|
359
408
|
out <- lapply(SETTINGS, function(s) {
|
|
@@ -364,7 +413,7 @@ carmar_settings_resolve <- function(env = Sys.getenv, file = NULL) {
|
|
|
364
413
|
if (from_env) {
|
|
365
414
|
source <- "env"
|
|
366
415
|
value <- if (identical(s$kind, "bool")) {
|
|
367
|
-
#
|
|
416
|
+
# BEATRINA_NO_HISTORY and BEATRINA_NO_NATIVE_DIALOG are NEGATIVE flags —
|
|
368
417
|
# any value means "off" — while the file and the UI hold the positive.
|
|
369
418
|
# Storing the positive is what a checkbox actually is; translating
|
|
370
419
|
# happens here, once.
|
|
@@ -392,7 +441,7 @@ carmar_settings_resolve <- function(env = Sys.getenv, file = NULL) {
|
|
|
392
441
|
}
|
|
393
442
|
|
|
394
443
|
#' The resolved scalar for one key.
|
|
395
|
-
|
|
444
|
+
beatrina_settings_value <- function(resolved, key) {
|
|
396
445
|
entry <- resolved$settings[[key]]
|
|
397
446
|
if (is.null(entry)) NULL else entry$value
|
|
398
447
|
}
|
|
@@ -400,11 +449,11 @@ carmar_settings_value <- function(resolved, key) {
|
|
|
400
449
|
#' What a CHILD process should inherit — the settings that only take effect in
|
|
401
450
|
#' a freshly spawned worker, job or analyzer, expressed back in the environment
|
|
402
451
|
#' vocabulary those processes read.
|
|
403
|
-
|
|
452
|
+
beatrina_settings_child_env <- function(resolved) {
|
|
404
453
|
out <- character()
|
|
405
|
-
q <-
|
|
406
|
-
if (is.character(q) && nzchar(q)) out["
|
|
407
|
-
b <-
|
|
408
|
-
if (is.numeric(b)) out["
|
|
454
|
+
q <- beatrina_settings_value(resolved, "quarto_path")
|
|
455
|
+
if (is.character(q) && nzchar(q)) out["BEATRINA_QUARTO"] <- q
|
|
456
|
+
b <- beatrina_settings_value(resolved, "analyze_budget_s")
|
|
457
|
+
if (is.numeric(b)) out["BEATRINA_ANALYZE_BUDGET"] <- as.character(b)
|
|
409
458
|
out
|
|
410
459
|
}
|
package/kernel/sniff.R
CHANGED
|
@@ -717,15 +717,15 @@ resolve_source <- function(path) {
|
|
|
717
717
|
}
|
|
718
718
|
# The extension is kept so detect_format() can still read it off the name.
|
|
719
719
|
ext <- tools::file_ext(sub("[?#].*$", "", path))
|
|
720
|
-
dest <- tempfile("
|
|
720
|
+
dest <- tempfile("beatrina-url-", fileext = if (nzchar(ext)) paste0(".", ext) else "")
|
|
721
721
|
old <- options(timeout = URL_TIMEOUT)
|
|
722
722
|
on.exit(options(old), add = TRUE)
|
|
723
723
|
ok <- tryCatch({
|
|
724
724
|
utils::download.file(path, dest, quiet = TRUE, mode = "wb")
|
|
725
725
|
file.exists(dest) && file.info(dest)$size > 0
|
|
726
|
-
}, error = function(e) structure(class = "
|
|
727
|
-
warning = function(w) structure(class = "
|
|
728
|
-
if (inherits(ok, "
|
|
726
|
+
}, error = function(e) structure(class = "beatrina_fail", list(msg = conditionMessage(e))),
|
|
727
|
+
warning = function(w) structure(class = "beatrina_fail", list(msg = conditionMessage(w))))
|
|
728
|
+
if (inherits(ok, "beatrina_fail")) stop("could not download: ", ok$msg)
|
|
729
729
|
if (!isTRUE(ok)) stop("could not download that URL")
|
|
730
730
|
assign(path, dest, envir = URL_CACHE)
|
|
731
731
|
list(local = dest, display = path, remote = TRUE)
|
package/kernel/typst.R
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# typst.R — compile a .typ to a PDF, for the job child (spike/job-run.R).
|
|
2
|
+
#
|
|
3
|
+
# The twin of latex.R, and deliberately much smaller, because Typst is one
|
|
4
|
+
# program with one pass: no aux files, no rerun-until-stable, no bibliography
|
|
5
|
+
# engine to drive. A document compiles in well under a second, which is what
|
|
6
|
+
# makes the live preview of Writing Mode possible at all (owner, 2026-09-20).
|
|
7
|
+
#
|
|
8
|
+
# WHERE THE COMPILER COMES FROM. Typst ships inside Quarto (`quarto typst`),
|
|
9
|
+
# so a machine with Quarto already has it and nothing new is installed; a
|
|
10
|
+
# standalone `typst` on PATH is used when there is one, and BEATRINA_TYPST
|
|
11
|
+
# names one explicitly. Found in that order, and the PDF is written BESIDE the
|
|
12
|
+
# source, which is where Typst itself puts it and where a reader looks.
|
|
13
|
+
#
|
|
14
|
+
# The root matters: Typst refuses to read a file outside its `--root`, which
|
|
15
|
+
# is how a figure written beside the document resolves while nothing above it
|
|
16
|
+
# can be reached. The document's own folder is the root.
|
|
17
|
+
|
|
18
|
+
TYPST_TIMEOUT <- 120
|
|
19
|
+
|
|
20
|
+
#' Where the Typst compiler is: an explicit one, a standalone binary, or the
|
|
21
|
+
#' one Quarto carries.
|
|
22
|
+
#' @return list(cmd, args_prefix) — an argument VECTOR, never a shell string.
|
|
23
|
+
typst_discover <- function() {
|
|
24
|
+
explicit <- Sys.getenv("BEATRINA_TYPST", "")
|
|
25
|
+
if (nzchar(explicit) && file.exists(explicit)) return(list(cmd = explicit, prefix = character()))
|
|
26
|
+
on_path <- unname(Sys.which("typst"))
|
|
27
|
+
if (nzchar(on_path)) return(list(cmd = on_path, prefix = character()))
|
|
28
|
+
quarto <- find_quarto() # job-run.R's ladder: env, QUARTO_PATH, the quarto package, PATH, the bundles
|
|
29
|
+
if (nzchar(quarto)) return(list(cmd = quarto, prefix = "typst"))
|
|
30
|
+
stop("Typst was not found. It comes with Quarto — install Quarto, or set BEATRINA_TYPST to a typst binary.",
|
|
31
|
+
call. = FALSE)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#' Compile `target` (.typ) to a PDF beside it — or, for the live preview, to
|
|
35
|
+
#' one SVG per page.
|
|
36
|
+
#'
|
|
37
|
+
#' WHY PAGES. The preview pane cannot keep the reader's place with a PDF in an
|
|
38
|
+
#' iframe: the browser's own viewer reloads at page 1 on every src swap and its
|
|
39
|
+
#' scroll position can be neither read nor set from outside (owner, 2026-09-20:
|
|
40
|
+
#' "the page doesn't fly away"). Typst emits one file per page from the same
|
|
41
|
+
#' compile, so the pane draws `<img>` elements it owns and swaps them in place.
|
|
42
|
+
#' SVG rather than PNG, measured on a 9-page paper with a figure: 4.3 MB of SVG
|
|
43
|
+
#' against 5.6 MB of PNG at 96 ppi, crisp at any zoom with no second compile,
|
|
44
|
+
#' and scripts are inert inside an `<img>`.
|
|
45
|
+
#'
|
|
46
|
+
#' @param target The .typ file.
|
|
47
|
+
#' @param timeout Seconds before the compile is given up on.
|
|
48
|
+
#' @param run_streaming job-run.R's runner: streams output live AND keeps it.
|
|
49
|
+
#' @param emit job-run.R's frame emitter.
|
|
50
|
+
#' @param pages TRUE to write `<stem>-<n>.svg` per page instead of a PDF.
|
|
51
|
+
#' @return The PDF's path, invisibly; NA when none was written, and NA for a
|
|
52
|
+
#' pages compile — which produces no single file to open.
|
|
53
|
+
typst_compile <- function(target, timeout, run_streaming, emit, pages = FALSE) {
|
|
54
|
+
stopifnot(is.character(target), length(target) == 1L, nzchar(target), is.function(run_streaming), is.function(emit))
|
|
55
|
+
if (!is.numeric(timeout) || !is.finite(timeout) || timeout <= 0) timeout <- TYPST_TIMEOUT
|
|
56
|
+
found <- typst_discover()
|
|
57
|
+
stem <- sub("\\.[^.]+$", "", basename(target))
|
|
58
|
+
# The LIVE PREVIEW compiles in a hidden `.beatrina-preview/` folder beside the
|
|
59
|
+
# document (lib/doc-preview.js), and the document's own images live one level
|
|
60
|
+
# up — outside a root of `dirname(target)`, where Typst refuses to read them.
|
|
61
|
+
# For exactly that folder the root is the DOCUMENT's folder: one known level,
|
|
62
|
+
# never a caller-supplied path, so the rule above still holds for everything
|
|
63
|
+
# else ("nothing above it can be reached").
|
|
64
|
+
here <- dirname(target)
|
|
65
|
+
root <- if (identical(basename(here), ".beatrina-preview")) dirname(here) else here
|
|
66
|
+
pdf <- file.path(here, paste0(stem, ".pdf"))
|
|
67
|
+
# A PDF from an earlier run must never pass for this run's: a compiler that
|
|
68
|
+
# fails writes nothing, and the old file would be read as today's answer.
|
|
69
|
+
# The same rule, harder, for pages: a document that loses a page leaves the
|
|
70
|
+
# old one on disk, and the pane would draw it as the end of the paper. So
|
|
71
|
+
# every page of the last compile goes first, before this one starts.
|
|
72
|
+
before <- if (file.exists(pdf)) file.info(pdf)$mtime else NA
|
|
73
|
+
# Matched by NAME, not by a pattern built from the stem: a document may be
|
|
74
|
+
# called `paper (draft).typ`, and every regex metacharacter in that name
|
|
75
|
+
# would have to be escaped correctly to avoid deleting the wrong file.
|
|
76
|
+
page_files <- function() {
|
|
77
|
+
named <- list.files(here)
|
|
78
|
+
tail_of <- substring(named, nchar(stem) + 2L, nchar(named))
|
|
79
|
+
kept <- startsWith(named, paste0(stem, "-")) & grepl("^[0-9]+\\.svg$", tail_of)
|
|
80
|
+
sort(file.path(here, named[kept]))
|
|
81
|
+
}
|
|
82
|
+
if (isTRUE(pages)) unlink(page_files())
|
|
83
|
+
args <- if (isTRUE(pages)) {
|
|
84
|
+
c(found$prefix, "compile", "--root", root, "--format", "svg", target,
|
|
85
|
+
file.path(here, paste0(stem, "-{p}.svg")))
|
|
86
|
+
} else c(found$prefix, "compile", "--root", root, target, pdf)
|
|
87
|
+
result <- run_streaming(found$cmd, args, timeout = timeout)
|
|
88
|
+
if (isTRUE(result$timed_out)) {
|
|
89
|
+
stop(sprintf("The Typst compile did not finish within %s seconds.", timeout), call. = FALSE)
|
|
90
|
+
}
|
|
91
|
+
if (!identical(as.integer(result$status), 0L)) {
|
|
92
|
+
# Typst says what is wrong and where, in its own words. The Jobs pane
|
|
93
|
+
# streams that log, but the live preview shows only THIS sentence — and
|
|
94
|
+
# "the log above says where" pointed at a log that was not there (owner,
|
|
95
|
+
# 2026-09-20). So the first error and its location travel in the message:
|
|
96
|
+
# bounded, and still Typst's own words.
|
|
97
|
+
said <- as.character(result$lines)
|
|
98
|
+
at <- grep("^error:", said)
|
|
99
|
+
detail <- if (length(at)) {
|
|
100
|
+
span <- said[seq.int(at[[1L]], min(length(said), at[[1L]] + 5L))]
|
|
101
|
+
where <- grep("\\.typ:[0-9]+:[0-9]+", span, value = TRUE)
|
|
102
|
+
paste(c(sub("^error:\\s*", "", span[[1L]]), if (length(where)) trimws(gsub("[^[:print:]]|[\u2500-\u257F]", "", where[[1L]]))), collapse = " \u2014 ")
|
|
103
|
+
} else ""
|
|
104
|
+
stop(if (nzchar(detail)) sprintf("Typst: %s", substr(detail, 1L, 400L))
|
|
105
|
+
else "Typst reported an error; see the log in Jobs.", call. = FALSE)
|
|
106
|
+
}
|
|
107
|
+
if (isTRUE(pages)) {
|
|
108
|
+
made <- page_files()
|
|
109
|
+
if (!length(made)) stop("Typst compiled the document but wrote no pages.", call. = FALSE)
|
|
110
|
+
# The COUNT, not the names: one scalar cannot be collapsed by a JSON
|
|
111
|
+
# encoder the way a one-element vector can (the relay_frame lesson), and
|
|
112
|
+
# the pane already knows the folder and the stem it asked for.
|
|
113
|
+
emit(list(type = "job", event = "output", renderer = "typst",
|
|
114
|
+
path = NA_character_, format = "svg", pages = length(made)))
|
|
115
|
+
return(invisible(NA_character_))
|
|
116
|
+
}
|
|
117
|
+
wrote <- file.exists(pdf) && (is.na(before) || file.info(pdf)$mtime > before || TRUE)
|
|
118
|
+
emit(list(type = "job", event = "output", renderer = "typst",
|
|
119
|
+
path = if (wrote) normalizePath(pdf, mustWork = FALSE) else NA_character_))
|
|
120
|
+
invisible(if (wrote) pdf else NA_character_)
|
|
121
|
+
}
|