@alisio/core 0.1.0-alpha.8 → 0.1.0

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 (189) hide show
  1. package/README.md +52 -7
  2. package/dist/agents/active.d.ts +99 -0
  3. package/dist/agents/active.js +218 -0
  4. package/dist/agents/capabilities.d.ts +26 -0
  5. package/dist/agents/capabilities.js +120 -0
  6. package/dist/agents/create-agent-skill.d.ts +11 -0
  7. package/dist/agents/create-agent-skill.js +60 -0
  8. package/dist/agents/definitions.d.ts +88 -0
  9. package/dist/agents/definitions.js +404 -0
  10. package/dist/agents/draft.d.ts +41 -0
  11. package/dist/agents/draft.js +133 -0
  12. package/dist/agents/service.d.ts +57 -0
  13. package/dist/agents/service.js +86 -0
  14. package/dist/agents/templates.d.ts +8 -0
  15. package/dist/agents/templates.js +102 -0
  16. package/dist/analysis/capabilities.d.ts +48 -0
  17. package/dist/analysis/capabilities.js +90 -0
  18. package/dist/analysis/data/csv.d.ts +46 -0
  19. package/dist/analysis/data/csv.js +240 -0
  20. package/dist/analysis/data/datasets.d.ts +132 -0
  21. package/dist/analysis/data/datasets.js +441 -0
  22. package/dist/analysis/data/describe.d.ts +27 -0
  23. package/dist/analysis/data/describe.js +103 -0
  24. package/dist/analysis/data/engine-client.d.ts +58 -0
  25. package/dist/analysis/data/engine-client.js +304 -0
  26. package/dist/analysis/data/engine-entry.d.ts +1 -0
  27. package/dist/analysis/data/engine-entry.js +59 -0
  28. package/dist/analysis/data/engine-source.d.ts +4 -0
  29. package/dist/analysis/data/engine-source.js +6 -0
  30. package/dist/analysis/data/engine-types.d.ts +86 -0
  31. package/dist/analysis/data/engine-types.js +7 -0
  32. package/dist/analysis/data/infer.d.ts +42 -0
  33. package/dist/analysis/data/infer.js +118 -0
  34. package/dist/analysis/data/ingest-worker.d.ts +17 -0
  35. package/dist/analysis/data/ingest-worker.js +365 -0
  36. package/dist/analysis/data/json.d.ts +19 -0
  37. package/dist/analysis/data/json.js +44 -0
  38. package/dist/analysis/data/query-worker.d.ts +12 -0
  39. package/dist/analysis/data/query-worker.js +75 -0
  40. package/dist/analysis/data/rows.d.ts +39 -0
  41. package/dist/analysis/data/rows.js +120 -0
  42. package/dist/analysis/data/sql-guard.d.ts +20 -0
  43. package/dist/analysis/data/sql-guard.js +129 -0
  44. package/dist/analysis/data/stats.d.ts +18 -0
  45. package/dist/analysis/data/stats.js +77 -0
  46. package/dist/analysis/install-hints.d.ts +33 -0
  47. package/dist/analysis/install-hints.js +144 -0
  48. package/dist/analysis/janitor.d.ts +60 -0
  49. package/dist/analysis/janitor.js +264 -0
  50. package/dist/analysis/jobs.d.ts +94 -0
  51. package/dist/analysis/jobs.js +165 -0
  52. package/dist/analysis/oci.d.ts +129 -0
  53. package/dist/analysis/oci.js +330 -0
  54. package/dist/analysis/python/sources.d.ts +2 -0
  55. package/dist/analysis/python/sources.js +10 -0
  56. package/dist/analysis/requirements.d.ts +32 -0
  57. package/dist/analysis/requirements.js +73 -0
  58. package/dist/analysis/rerun.d.ts +40 -0
  59. package/dist/analysis/rerun.js +136 -0
  60. package/dist/analysis/runtime-manager.d.ts +146 -0
  61. package/dist/analysis/runtime-manager.js +472 -0
  62. package/dist/analysis/status.d.ts +21 -0
  63. package/dist/analysis/status.js +52 -0
  64. package/dist/application.d.ts +118 -3
  65. package/dist/application.js +510 -19
  66. package/dist/artifacts/kinds.d.ts +35 -0
  67. package/dist/artifacts/kinds.js +177 -0
  68. package/dist/artifacts/publisher.d.ts +7 -0
  69. package/dist/artifacts/publisher.js +29 -0
  70. package/dist/artifacts/store.d.ts +119 -0
  71. package/dist/artifacts/store.js +581 -0
  72. package/dist/background/janitor.d.ts +22 -0
  73. package/dist/background/janitor.js +62 -0
  74. package/dist/background/mirror.d.ts +14 -0
  75. package/dist/background/mirror.js +41 -0
  76. package/dist/background/notify.d.ts +58 -0
  77. package/dist/background/notify.js +172 -0
  78. package/dist/background/output.d.ts +46 -0
  79. package/dist/background/output.js +179 -0
  80. package/dist/background/service.d.ts +137 -0
  81. package/dist/background/service.js +410 -0
  82. package/dist/background/store.d.ts +103 -0
  83. package/dist/background/store.js +195 -0
  84. package/dist/changelog/data.d.ts +6 -0
  85. package/dist/changelog/data.js +293 -0
  86. package/dist/changelog/index.d.ts +5 -0
  87. package/dist/changelog/index.js +7 -0
  88. package/dist/changelog/parse.d.ts +53 -0
  89. package/dist/changelog/parse.js +160 -0
  90. package/dist/commands/builtins.d.ts +10 -0
  91. package/dist/commands/builtins.js +161 -0
  92. package/dist/commands/catalog.d.ts +86 -0
  93. package/dist/commands/catalog.js +304 -0
  94. package/dist/config.d.ts +252 -5
  95. package/dist/config.js +514 -13
  96. package/dist/core/compaction.js +13 -2
  97. package/dist/core/contracts.d.ts +180 -3
  98. package/dist/core/opt-in.d.ts +12 -0
  99. package/dist/core/opt-in.js +12 -0
  100. package/dist/core/output-limit.d.ts +34 -0
  101. package/dist/core/output-limit.js +39 -0
  102. package/dist/core/run-stats.d.ts +16 -0
  103. package/dist/core/run-stats.js +8 -0
  104. package/dist/core/runner.d.ts +152 -4
  105. package/dist/core/runner.js +792 -138
  106. package/dist/core/timeout.d.ts +15 -0
  107. package/dist/core/timeout.js +61 -0
  108. package/dist/core/truncation.d.ts +30 -0
  109. package/dist/core/truncation.js +83 -0
  110. package/dist/goal/blockers.d.ts +45 -0
  111. package/dist/goal/blockers.js +51 -0
  112. package/dist/goal/budget.d.ts +24 -0
  113. package/dist/goal/budget.js +53 -0
  114. package/dist/goal/command.d.ts +36 -0
  115. package/dist/goal/command.js +98 -0
  116. package/dist/goal/format.d.ts +19 -0
  117. package/dist/goal/format.js +102 -0
  118. package/dist/goal/host.d.ts +29 -0
  119. package/dist/goal/host.js +42 -0
  120. package/dist/goal/index.d.ts +9 -0
  121. package/dist/goal/index.js +9 -0
  122. package/dist/goal/machine.d.ts +186 -0
  123. package/dist/goal/machine.js +344 -0
  124. package/dist/goal/prompts.d.ts +10 -0
  125. package/dist/goal/prompts.js +75 -0
  126. package/dist/goal/service.d.ts +113 -0
  127. package/dist/goal/service.js +273 -0
  128. package/dist/goal/store.d.ts +59 -0
  129. package/dist/goal/store.js +184 -0
  130. package/dist/index.d.ts +51 -6
  131. package/dist/index.js +49 -4
  132. package/dist/mcp/connector.js +38 -6
  133. package/dist/mcp/rich.d.ts +41 -0
  134. package/dist/mcp/rich.js +362 -0
  135. package/dist/permissions/modes.d.ts +72 -0
  136. package/dist/permissions/modes.js +98 -0
  137. package/dist/plan/exit-plan.d.ts +25 -0
  138. package/dist/plan/exit-plan.js +130 -0
  139. package/dist/plan/state.d.ts +102 -0
  140. package/dist/plan/state.js +165 -0
  141. package/dist/plugins/host.d.ts +39 -1
  142. package/dist/plugins/host.js +150 -9
  143. package/dist/providers/settings.d.ts +15 -0
  144. package/dist/providers/settings.js +38 -0
  145. package/dist/reload.d.ts +75 -0
  146. package/dist/reload.js +204 -0
  147. package/dist/resources/context.d.ts +5 -0
  148. package/dist/resources/context.js +13 -1
  149. package/dist/runtime/access.d.ts +62 -0
  150. package/dist/runtime/access.js +115 -0
  151. package/dist/runtime/blobs.d.ts +51 -0
  152. package/dist/runtime/blobs.js +177 -0
  153. package/dist/runtime/diff.d.ts +23 -0
  154. package/dist/runtime/diff.js +154 -0
  155. package/dist/runtime/ids.d.ts +8 -0
  156. package/dist/runtime/ids.js +32 -0
  157. package/dist/runtime/paths.d.ts +11 -0
  158. package/dist/runtime/paths.js +17 -1
  159. package/dist/runtime/process.d.ts +22 -0
  160. package/dist/runtime/process.js +100 -15
  161. package/dist/runtime/sqlite.d.ts +2 -0
  162. package/dist/runtime/sqlite.js +18 -0
  163. package/dist/runtime/store.d.ts +91 -3
  164. package/dist/runtime/store.js +465 -11
  165. package/dist/runtime/zip.d.ts +24 -0
  166. package/dist/runtime/zip.js +126 -0
  167. package/dist/sessions/children.js +17 -1
  168. package/dist/sessions/side-questions.d.ts +57 -0
  169. package/dist/sessions/side-questions.js +161 -0
  170. package/dist/startup/screen.js +6 -5
  171. package/dist/tools/analysis.d.ts +37 -0
  172. package/dist/tools/analysis.js +573 -0
  173. package/dist/tools/artifacts.d.ts +14 -0
  174. package/dist/tools/artifacts.js +143 -0
  175. package/dist/tools/background.d.ts +7 -0
  176. package/dist/tools/background.js +165 -0
  177. package/dist/tools/data.d.ts +7 -0
  178. package/dist/tools/data.js +160 -0
  179. package/dist/tools/execute.d.ts +5 -0
  180. package/dist/tools/execute.js +15 -3
  181. package/dist/tools/goal.d.ts +19 -0
  182. package/dist/tools/goal.js +126 -0
  183. package/dist/tools/search.d.ts +17 -3
  184. package/dist/tools/search.js +153 -0
  185. package/dist/tools/standard.d.ts +4 -1
  186. package/dist/tools/standard.js +99 -18
  187. package/dist/version.d.ts +5 -3
  188. package/dist/version.js +35 -6
  189. package/package.json +2 -2
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Zero-configuration Python discovery for `python_run`. Startup only looks for candidate
3
+ * executables on PATH with `stat` (no process); the first call probes them in order
4
+ * (`--python` → `uv python find` → `py -3` on Windows → `python3` → `python`), accepts the first
5
+ * Python ≥ 3.10 and caches `{ executable, version, mtime }` in `runtimes/python/discovery.json`.
6
+ * Only positive results are cached, so installing Python is picked up without a restart.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
10
+ import { isAbsolute, join, posix, resolve, win32 } from "node:path";
11
+ import { runProcess } from "../runtime/process.js";
12
+ import { EXTRAS_DISTRIBUTIONS, EXTRAS_ESTIMATED_BYTES, EXTRAS_LOCK, EXTRAS_PACKAGES, lockedDistributionCount, } from "./requirements.js";
13
+ export const MIN_PYTHON = [3, 10];
14
+ /** Prints `[[major, minor, micro], executable]` as JSON. */
15
+ export const PROBE_CODE = "import sys, json; print(json.dumps([list(sys.version_info[:3]), sys.executable]))";
16
+ /** The interpreter of a virtual environment, built with the target platform's path rules. */
17
+ export function venvInterpreter(venv, platform = process.platform) {
18
+ return platform === "win32"
19
+ ? win32.join(venv, "Scripts", "python.exe")
20
+ : posix.join(venv, "bin", "python");
21
+ }
22
+ /** `3.10.12` → true when at least `MIN_PYTHON`. */
23
+ export function supportedVersion(version) {
24
+ const [major = 0, minor = 0] = version.split(".").map(Number);
25
+ return major > MIN_PYTHON[0] || (major === MIN_PYTHON[0] && minor >= MIN_PYTHON[1]);
26
+ }
27
+ /** Parses the probe output; undefined when it is not ours. */
28
+ export function parseProbe(stdout) {
29
+ try {
30
+ const value = JSON.parse(stdout.trim().split(/\r?\n/).pop() ?? "");
31
+ if (!Array.isArray(value) ||
32
+ !Array.isArray(value[0]) ||
33
+ typeof value[1] !== "string" ||
34
+ !value[0].every((n) => typeof n === "number"))
35
+ return undefined;
36
+ return { version: value[0].join("."), executable: value[1] };
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ export function defaultDiscoveryDeps() {
43
+ return {
44
+ platform: process.platform,
45
+ path: process.env.PATH ?? process.env.Path ?? "",
46
+ ...(process.env.PATHEXT ? { pathext: process.env.PATHEXT } : {}),
47
+ ...(process.env.LOCALAPPDATA ? { localAppData: process.env.LOCALAPPDATA } : {}),
48
+ async stat(path) {
49
+ try {
50
+ const s = await stat(path);
51
+ return { mtimeMs: s.mtimeMs, isFile: s.isFile() };
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ },
57
+ async exec(command, args, signal) {
58
+ const result = await runProcess(command, args, {
59
+ cwd: process.cwd(),
60
+ signal,
61
+ timeoutMs: 15_000,
62
+ });
63
+ return { stdout: result.stdout, exitCode: result.exitCode };
64
+ },
65
+ };
66
+ }
67
+ /**
68
+ * The installation failed. `offline` means the download could not start or finish because there
69
+ * is no network (or the package index is unreachable): nothing was installed and the base runtime
70
+ * is untouched, so the message says so and how to retry.
71
+ */
72
+ export class ExtrasInstallError extends Error {
73
+ kind;
74
+ constructor(message, kind) {
75
+ super(message);
76
+ this.kind = kind;
77
+ this.name = "ExtrasInstallError";
78
+ }
79
+ }
80
+ const OFFLINE_SIGNS = /Temporary failure in name resolution|Name or service not known|nodename nor servname|Failed to establish a new connection|Connection (refused|reset|aborted|timed out)|Network is unreachable|No route to host|getaddrinfo|dns error|error sending request|failed to lookup address|Max retries exceeded|ReadTimeout|ConnectTimeout|ConnectionError|SSLError|Could not find a version that satisfies|No matching distribution found|Failed to download|Failed to fetch|Temporary failure/i;
81
+ /** The command that installs the extras by hand (the optional, explicit route). */
82
+ export const setupCommand = (extras) => `alisio analysis setup --extras ${extras}`;
83
+ /** What an `analysis.install` approval shows for a set of extras. */
84
+ export function installPreview(extras) {
85
+ return {
86
+ extras,
87
+ packages: [...EXTRAS_DISTRIBUTIONS[extras]],
88
+ packageCount: lockedDistributionCount(extras),
89
+ estimatedBytes: EXTRAS_ESTIMATED_BYTES[extras],
90
+ network: true,
91
+ };
92
+ }
93
+ const installEnv = () => ({
94
+ ...Object.fromEntries([
95
+ "PATH",
96
+ "HOME",
97
+ "USERPROFILE",
98
+ "TMPDIR",
99
+ "TEMP",
100
+ "SystemRoot",
101
+ "LANG",
102
+ "LOCALAPPDATA",
103
+ "APPDATA",
104
+ "HTTPS_PROXY",
105
+ "HTTP_PROXY",
106
+ "NO_PROXY",
107
+ "SSL_CERT_FILE",
108
+ "REQUESTS_CA_BUNDLE",
109
+ "PIP_INDEX_URL",
110
+ "UV_INDEX_URL",
111
+ ].flatMap((k) => (process.env[k] ? [[k, process.env[k]]] : []))),
112
+ // Fail fast when offline instead of retrying for minutes.
113
+ UV_HTTP_TIMEOUT: "20",
114
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
115
+ });
116
+ const defaultInstallStep = (command, args, options) => runProcess(command, args, {
117
+ cwd: options.cwd,
118
+ signal: options.signal,
119
+ timeoutMs: 30 * 60_000,
120
+ maxBytes: 256 * 1024,
121
+ onOverflow: "truncate",
122
+ env: installEnv(),
123
+ ...(options.onOutput ? { onData: options.onOutput } : {}),
124
+ });
125
+ export class AnalysisRuntimeManager {
126
+ options;
127
+ deps;
128
+ memory;
129
+ constructor(options) {
130
+ this.options = options;
131
+ this.deps = { ...defaultDiscoveryDeps(), ...options.deps };
132
+ }
133
+ get pythonFlag() {
134
+ return this.options.python ? resolve(this.options.python) : undefined;
135
+ }
136
+ get dir() {
137
+ return join(this.options.stateDir, "runtimes", "python");
138
+ }
139
+ sep() {
140
+ return this.deps.platform === "win32" ? ";" : ":";
141
+ }
142
+ /** First PATH match of `name` (with `PATHEXT` on Windows), by `stat` only. */
143
+ async onPath(name) {
144
+ const win = this.deps.platform === "win32";
145
+ const join_ = win ? win32.join : posix.join;
146
+ const extensions = win
147
+ ? (this.deps.pathext ?? ".COM;.EXE;.BAT;.CMD")
148
+ .split(";")
149
+ .filter(Boolean)
150
+ .map((e) => e.toLowerCase())
151
+ : [""];
152
+ for (const dir of this.deps.path.split(this.sep()).filter(Boolean))
153
+ for (const ext of extensions) {
154
+ const candidate = join_(dir, `${name}${ext}`);
155
+ if (win && this.storeAlias(candidate))
156
+ continue;
157
+ if ((await this.deps.stat(candidate))?.isFile)
158
+ return candidate;
159
+ }
160
+ return undefined;
161
+ }
162
+ /** The Microsoft Store alias opens the Store instead of running Python. */
163
+ storeAlias(path) {
164
+ if (this.deps.platform !== "win32")
165
+ return false;
166
+ const base = this.deps.localAppData
167
+ ? win32.join(this.deps.localAppData, "Microsoft", "WindowsApps")
168
+ : undefined;
169
+ const normalized = path.toLowerCase();
170
+ return base
171
+ ? normalized.startsWith(`${base.toLowerCase()}\\`)
172
+ : normalized.includes("\\microsoft\\windowsapps\\");
173
+ }
174
+ /** Candidate executables, in discovery order (stat only; launches nothing). */
175
+ async candidates() {
176
+ const flag = this.pythonFlag;
177
+ if (flag)
178
+ return [{ source: "flag", command: flag }];
179
+ const out = [];
180
+ const uv = await this.onPath("uv");
181
+ if (uv)
182
+ out.push({ source: "uv", command: uv });
183
+ if (this.deps.platform === "win32") {
184
+ const py = await this.onPath("py");
185
+ if (py)
186
+ out.push({ source: "py", command: py });
187
+ }
188
+ for (const name of ["python3", "python"]) {
189
+ const found = await this.onPath(name);
190
+ if (found)
191
+ out.push({ source: name, command: found });
192
+ }
193
+ return out;
194
+ }
195
+ async readCache() {
196
+ if (this.memory)
197
+ return this.memory;
198
+ try {
199
+ const value = JSON.parse(await readFile(join(this.dir, "discovery.json"), "utf8"));
200
+ return typeof value.executable === "string" && typeof value.mtime === "number"
201
+ ? value
202
+ : undefined;
203
+ }
204
+ catch {
205
+ return undefined;
206
+ }
207
+ }
208
+ async writeCache(cache) {
209
+ this.memory = cache;
210
+ try {
211
+ await mkdir(this.dir, { recursive: true });
212
+ const file = join(this.dir, "discovery.json");
213
+ const temporary = `${file}.${process.pid}.tmp`;
214
+ await writeFile(temporary, `${JSON.stringify(cache, null, 2)}\n`);
215
+ await rename(temporary, file);
216
+ }
217
+ catch {
218
+ /* The cache is an optimization; discovery still works without it. */
219
+ }
220
+ }
221
+ /** Probes one interpreter path: its version and real executable, or undefined. */
222
+ async probe(command, prefix, signal) {
223
+ try {
224
+ const result = await this.deps.exec(command, [...prefix, "-c", PROBE_CODE], signal);
225
+ return result.exitCode === 0 ? parseProbe(result.stdout) : undefined;
226
+ }
227
+ catch {
228
+ signal.throwIfAborted();
229
+ return undefined;
230
+ }
231
+ }
232
+ /**
233
+ * The interpreter to run: the cached one while its file is unchanged, else a fresh discovery.
234
+ * With `--python`, only that path is considered.
235
+ */
236
+ async resolve(signal = AbortSignal.timeout(60_000)) {
237
+ const flag = this.pythonFlag;
238
+ const cached = await this.readCache();
239
+ if (cached &&
240
+ (flag ? cached.source === "flag" && cached.executable === flag : cached.source !== "flag")) {
241
+ const info = await this.deps.stat(cached.executable);
242
+ if (info?.isFile && info.mtimeMs === cached.mtime)
243
+ return {
244
+ ok: true,
245
+ executable: cached.executable,
246
+ version: cached.version,
247
+ source: cached.source,
248
+ };
249
+ }
250
+ this.memory = undefined;
251
+ let tooOld;
252
+ const accept = async (source, probed) => {
253
+ if (!probed)
254
+ return undefined;
255
+ if (!supportedVersion(probed.version)) {
256
+ tooOld ??= { version: probed.version, path: probed.executable };
257
+ return undefined;
258
+ }
259
+ if (this.storeAlias(probed.executable))
260
+ return undefined;
261
+ const info = await this.deps.stat(probed.executable);
262
+ const executable = info?.isFile ? probed.executable : undefined;
263
+ if (!executable)
264
+ return undefined;
265
+ await this.writeCache({
266
+ executable,
267
+ version: probed.version,
268
+ mtime: info?.mtimeMs ?? 0,
269
+ source,
270
+ });
271
+ return { ok: true, executable, version: probed.version, source };
272
+ };
273
+ if (flag) {
274
+ const info = await this.deps.stat(flag);
275
+ if (!info?.isFile)
276
+ return {
277
+ ok: false,
278
+ flag: true,
279
+ reason: `--python ${flag} does not exist`,
280
+ found: { path: flag, reason: "not found" },
281
+ };
282
+ const probed = await this.probe(flag, [], signal);
283
+ const accepted = await accept("flag", probed);
284
+ if (accepted)
285
+ return accepted;
286
+ return {
287
+ ok: false,
288
+ flag: true,
289
+ reason: probed
290
+ ? `--python ${flag} is Python ${probed.version}; Alisio needs ${MIN_PYTHON.join(".")} or newer`
291
+ : `--python ${flag} is not a working Python interpreter`,
292
+ found: probed
293
+ ? { version: probed.version, path: flag }
294
+ : { path: flag, reason: "not a working Python interpreter" },
295
+ };
296
+ }
297
+ for (const candidate of await this.candidates()) {
298
+ signal.throwIfAborted();
299
+ let accepted;
300
+ if (candidate.source === "uv") {
301
+ try {
302
+ const found = await this.deps.exec(candidate.command, ["python", "find", ">=3.10"], signal);
303
+ const path = found.stdout.trim().split(/\r?\n/).pop()?.trim();
304
+ if (found.exitCode === 0 && path && isAbsolute(path))
305
+ accepted = await accept("uv", await this.probe(path, [], signal));
306
+ }
307
+ catch {
308
+ signal.throwIfAborted();
309
+ }
310
+ }
311
+ else if (candidate.source === "py")
312
+ accepted = await accept("py", await this.probe(candidate.command, ["-3"], signal));
313
+ else
314
+ accepted = await accept(candidate.source, await this.probe(candidate.command, [], signal));
315
+ if (accepted)
316
+ return accepted;
317
+ }
318
+ const old = tooOld;
319
+ return old
320
+ ? { ok: false, reason: `Python ${old.version} is too old`, found: old }
321
+ : { ok: false, reason: "No Python interpreter was found on PATH" };
322
+ }
323
+ /** The interpreter `python_run` uses: an installed extras environment, else the base one. */
324
+ async interpreter(signal) {
325
+ const base = await this.resolve(signal);
326
+ if (!base.ok)
327
+ return base;
328
+ const active = await this.activeRuntime();
329
+ if (active && active.pythonVersion === base.version) {
330
+ const python = venvInterpreter(join(this.dir, active.runtimeVersion, "venv"), this.deps.platform);
331
+ if ((await this.deps.stat(python))?.isFile)
332
+ return {
333
+ ...base,
334
+ executable: python,
335
+ venv: true,
336
+ extras: active.extras,
337
+ runtimeVersion: active.runtimeVersion,
338
+ };
339
+ }
340
+ return base;
341
+ }
342
+ /** The extras environment recorded by `alisio analysis setup --extras`, when healthy. */
343
+ async activeRuntime() {
344
+ try {
345
+ const pointer = JSON.parse(await readFile(join(this.dir, "active.json"), "utf8"));
346
+ if (!pointer.runtimeVersion || !/^[a-f0-9]{8,64}$/.test(pointer.runtimeVersion))
347
+ return undefined;
348
+ const state = JSON.parse(await readFile(join(this.dir, pointer.runtimeVersion, "runtime.json"), "utf8"));
349
+ return state.ok ? state : undefined;
350
+ }
351
+ catch {
352
+ return undefined;
353
+ }
354
+ }
355
+ installing = new Map();
356
+ /** Whether the healthy extras environment already provides `extras` (`science` includes `analysis`). */
357
+ async hasExtras(extras) {
358
+ const active = await this.activeRuntime();
359
+ if (!active)
360
+ return false;
361
+ const base = await this.resolve().catch(() => undefined);
362
+ if (!base?.ok || base.version !== active.pythonVersion)
363
+ return false;
364
+ return (active.extras.includes(extras) || (extras === "analysis" && active.extras.includes("science")));
365
+ }
366
+ /**
367
+ * Creates the extras environment and installs the hash-locked wheels (`--require-hashes`,
368
+ * `--only-binary=:all:`; nothing is compiled). Needs network. Never called by `python_run`
369
+ * without an `analysis.install` approval. Failing (offline, no wheel for this platform) leaves
370
+ * no half-built environment and does not touch the active one. Concurrent calls share one run.
371
+ */
372
+ installExtras(extras, options) {
373
+ const running = this.installing.get(extras);
374
+ if (running)
375
+ return running;
376
+ const work = this.doInstall(extras, options).finally(() => this.installing.delete(extras));
377
+ this.installing.set(extras, work);
378
+ return work;
379
+ }
380
+ async doInstall(extras, options) {
381
+ const base = await this.resolve(options.signal);
382
+ if (!base.ok)
383
+ throw new Error(base.reason);
384
+ const lock = EXTRAS_LOCK[extras];
385
+ const runtimeVersion = createHash("sha256")
386
+ .update(`${extras}\n${base.version}\n${lock}`)
387
+ .digest("hex")
388
+ .slice(0, 8);
389
+ const root = join(this.dir, runtimeVersion);
390
+ const venv = join(root, "venv");
391
+ const python = venvInterpreter(venv, this.deps.platform);
392
+ const existed = !!(await this.deps.stat(root));
393
+ await mkdir(root, { recursive: true });
394
+ const lockFile = join(root, `${extras}.txt`);
395
+ await writeFile(lockFile, lock);
396
+ const uv = await this.onPath("uv");
397
+ const step = this.options.install ?? defaultInstallStep;
398
+ const run = async (command, args) => {
399
+ const result = await step(command, args, {
400
+ cwd: root,
401
+ signal: options.signal,
402
+ ...(options.onOutput ? { onOutput: options.onOutput } : {}),
403
+ });
404
+ if (result.exitCode !== 0) {
405
+ const detail = `${result.stderr || result.stdout}`.slice(-4000);
406
+ if (OFFLINE_SIGNS.test(detail))
407
+ throw new ExtrasInstallError(`Could not download the Python packages: there is no network connection, or the package index is unreachable. ` +
408
+ `Nothing was installed and python_run keeps working with the standard library. ` +
409
+ `Connect to the internet and try again (${setupCommand(extras)}).\n${detail.trim().split(/\r?\n/).slice(-3).join("\n")}`, "offline");
410
+ throw new ExtrasInstallError(`${command} ${args.join(" ")} failed (exit ${result.exitCode}):\n${detail}`, "failed");
411
+ }
412
+ };
413
+ try {
414
+ if (!(await this.deps.stat(python))?.isFile) {
415
+ if (uv)
416
+ await run(uv, ["venv", "--python", base.executable, venv]);
417
+ else
418
+ await run(base.executable, ["-m", "venv", venv]);
419
+ }
420
+ if (uv)
421
+ await run(uv, [
422
+ "pip",
423
+ "install",
424
+ "--python",
425
+ python,
426
+ "--require-hashes",
427
+ "--only-binary",
428
+ ":all:",
429
+ "-r",
430
+ lockFile,
431
+ ]);
432
+ else
433
+ await run(python, [
434
+ "-m",
435
+ "pip",
436
+ "install",
437
+ "--require-hashes",
438
+ "--only-binary=:all:",
439
+ "--no-input",
440
+ "--retries",
441
+ "1",
442
+ "--timeout",
443
+ "20",
444
+ "-r",
445
+ lockFile,
446
+ ]);
447
+ await run(python, ["-c", `import ${EXTRAS_PACKAGES[extras].join(", ")}`]);
448
+ }
449
+ catch (error) {
450
+ // A new, half-built environment is useless (and misleading): remove what this call made.
451
+ if (!existed)
452
+ await rm(root, { recursive: true, force: true }).catch(() => undefined);
453
+ if (options.signal.aborted)
454
+ throw options.signal.reason ?? error;
455
+ throw error;
456
+ }
457
+ const state = {
458
+ runtimeVersion,
459
+ python: base.executable,
460
+ pythonVersion: base.version,
461
+ extras: [extras],
462
+ createdAt: Date.now(),
463
+ ok: true,
464
+ };
465
+ await writeFile(join(root, "runtime.json"), `${JSON.stringify(state, null, 2)}\n`);
466
+ const pointer = join(this.dir, "active.json");
467
+ const temporary = `${pointer}.${process.pid}.tmp`;
468
+ await writeFile(temporary, `${JSON.stringify({ runtimeVersion }, null, 2)}\n`);
469
+ await rename(temporary, pointer);
470
+ return state;
471
+ }
472
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The state of the analysis runtime for `alisio analysis status`, `alisio doctor` and the web
3
+ * Settings page: mode, interpreter and extras, container engine (probed only when it matters),
4
+ * limits and retention. One shape (`AnalysisStatus` of the SDK) for every surface.
5
+ */
6
+ import type { AnalysisStatus } from "@alisio/sdk";
7
+ import type { Config } from "../config.ts";
8
+ import type { AnalysisJanitor } from "./janitor.ts";
9
+ import type { OciRuntime } from "./oci.ts";
10
+ import type { AnalysisRuntimeManager } from "./runtime-manager.ts";
11
+ export declare function analysisStatus(input: {
12
+ config: Config["analysis"];
13
+ runtime: Pick<AnalysisRuntimeManager, "interpreter">;
14
+ /** Builds the container runtime of the current settings (probed on demand). */
15
+ oci?: () => OciRuntime;
16
+ janitor?: Pick<AnalysisJanitor, "lastSweep">;
17
+ /** python_run is registered (analysis enabled and not `--read-only`). */
18
+ enabled: boolean;
19
+ readOnly: boolean;
20
+ signal?: AbortSignal;
21
+ }): Promise<AnalysisStatus>;
@@ -0,0 +1,52 @@
1
+ import { detectHintHost, pythonInstallHints } from "./install-hints.js";
2
+ export async function analysisStatus(input) {
3
+ const { config } = input;
4
+ const resolved = await input.runtime.interpreter(input.signal);
5
+ const python = resolved.ok
6
+ ? {
7
+ found: true,
8
+ path: resolved.executable,
9
+ version: resolved.version,
10
+ source: resolved.source,
11
+ extras: resolved.extras ?? [],
12
+ ...(resolved.runtimeVersion ? { runtimeVersion: resolved.runtimeVersion } : {}),
13
+ }
14
+ : {
15
+ found: false,
16
+ reason: resolved.reason,
17
+ hints: await (async () => {
18
+ const hints = pythonInstallHints(await detectHintHost(resolved.found));
19
+ return {
20
+ system: hints.system,
21
+ ...(hints.heading ? { heading: hints.heading } : {}),
22
+ primary: hints.primary,
23
+ alternatives: hints.alternatives,
24
+ notes: hints.notes,
25
+ };
26
+ })(),
27
+ };
28
+ const oci = {
29
+ engine: config.oci.engine,
30
+ ...(config.oci.image ? { image: config.oci.image } : {}),
31
+ memoryMb: config.oci.memoryMb,
32
+ cpus: config.oci.cpus,
33
+ };
34
+ if (input.oci && (config.runtime === "oci" || config.oci.image)) {
35
+ const status = await input.oci().status(input.signal);
36
+ oci.available = status.ok;
37
+ if (status.ok)
38
+ oci.version = status.version;
39
+ else
40
+ oci.reason = status.reason;
41
+ }
42
+ const lastSweep = await input.janitor?.lastSweep();
43
+ return {
44
+ enabled: input.enabled,
45
+ readOnly: input.readOnly,
46
+ mode: config.runtime,
47
+ python,
48
+ oci,
49
+ limits: { timeoutMs: config.limits.timeoutMs },
50
+ retention: { ...config.retention, ...(lastSweep !== undefined ? { lastSweep } : {}) },
51
+ };
52
+ }