pi-repl-py 0.7.1 → 0.8.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.
- package/bridge.py +495 -0
- package/docs/ARCHITECTURE.md +71 -51
- package/index.ts +56 -44
- package/package.json +2 -1
- package/scripts/setup-venv.mjs +22 -9
- package/src/engine/index.ts +55 -136
- package/src/engine/kernel.ts +272 -496
- package/src/extension/prompt.ts +12 -17
- package/src/extension/session-engine.ts +1 -3
- package/src/engine/session.ts +0 -146
- package/src/engine/zmtp.ts +0 -239
- package/src/extension/tool-meta.ts +0 -8
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,39 +1,49 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
pi-repl runs in **
|
|
4
|
-
Python `
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
pi-repl runs in **three processes**: pi hosts the TypeScript extension, which manages a small
|
|
4
|
+
Python bridge (`bridge.py`) that owns the real `ipykernel` evaluator through `jupyter_client`.
|
|
5
|
+
The host and the bridge speak one tiny JSON-lines vocabulary over a stdio pipe; the bridge speaks
|
|
6
|
+
the standard Jupyter protocol to the kernel with ready-made libraries. A cell can raise or wedge
|
|
7
|
+
the kernel without taking pi down; the host stays answerable.
|
|
7
8
|
|
|
8
9
|
```
|
|
9
10
|
pi
|
|
10
11
|
└─ extension (index.ts) registers `execute`; dormant until --repl
|
|
11
12
|
└─ EngineManager (src/engine/index.ts) venv resolution, lazy spawn,
|
|
12
|
-
│ the call queue,
|
|
13
|
+
│ the call queue, output caps,
|
|
13
14
|
│ abort grace, teardown
|
|
14
|
-
└─ KernelClient (src/engine/kernel.ts) one
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
└─ KernelClient (src/engine/kernel.ts) spawn the bridge, one JSON
|
|
16
|
+
│ line per op, route by id
|
|
17
|
+
└─ stdio pipe ─── bridge.py owns jupyter_client + ipykernel:
|
|
18
|
+
│ cells, streaming, snapshots,
|
|
19
|
+
│ restore, interrupts
|
|
20
|
+
└─ ipykernel the evaluator
|
|
18
21
|
```
|
|
19
22
|
|
|
20
|
-
## Why the
|
|
23
|
+
## Why the boundary is one pipe
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
An earlier iteration had the TypeScript host speak ZMTP 3.0, HMAC-sign every frame, and
|
|
26
|
+
re-implement the Jupyter client protocol by hand (`zmtp.ts`, `session.ts`, ~1,100 lines) because
|
|
27
|
+
libzmq's native bindings crash `bun`. Each layer compensated the one below it: the wire had no
|
|
28
|
+
auth so frames were signed; two channels raced so cells settled on a two-message protocol; the
|
|
29
|
+
machine could wedge so it grew eight watchdog timers. All of it is deleted.
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
- **
|
|
31
|
-
|
|
31
|
+
The replacement is one stdio pipe to `bridge.py` (~250 lines), which owns everything Python-side
|
|
32
|
+
with ready-made libraries:
|
|
33
|
+
|
|
34
|
+
- **the file descriptor is the authentication** — the OS gives the pipe to exactly two processes;
|
|
35
|
+
- **one FIFO owner** — the bridge's single-threaded loop serializes every op, so ordering needs
|
|
36
|
+
no protocol;
|
|
37
|
+
- **death is EOF plus an exit code** — no socket-liveness guessing; ipykernel also shuts itself
|
|
38
|
+
down when its parent (the bridge) dies;
|
|
39
|
+
- **the host caps output** exactly as before, but snapshot payloads never cross the pipe — the
|
|
40
|
+
bridge writes the snapshot file itself and replies with names and counts only.
|
|
32
41
|
|
|
33
42
|
## The Python environment (the venv)
|
|
34
43
|
|
|
35
44
|
The evaluator is a real ipykernel, so it needs Python with `ipykernel` installed — a hard runtime
|
|
36
|
-
dependency
|
|
45
|
+
dependency. `jupyter_client` ships with ipykernel (the bridge drives the kernel through it), and
|
|
46
|
+
`cloudpickle` serializes snapshots (functions and classes by value). A package install runs
|
|
37
47
|
`postinstall` (`scripts/setup-venv.mjs`), which builds a stable per-user venv at
|
|
38
48
|
`~/.pi/agent/pi-repl/venv/bin/python3` — stable because it sits outside the package dir that npm
|
|
39
49
|
replaces on each update. If `python3` or the network is missing at install time, it prints a
|
|
@@ -44,31 +54,32 @@ At spawn, `resolvePythonPath` uses exactly one interpreter: the install venv, el
|
|
|
44
54
|
killed the kernel whenever cwd happened to contain one. The kernel starts in the session's cwd
|
|
45
55
|
and falls back to the host cwd if that directory is gone, so a stale cwd never prevents boot.
|
|
46
56
|
|
|
47
|
-
## The
|
|
57
|
+
## The bridge
|
|
48
58
|
|
|
49
|
-
`KernelClient.start` spawns
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
`KernelClient.start` spawns `<venv python> bridge.py` and waits for its `ready` event; the bridge
|
|
60
|
+
starts ipykernel through `jupyter_client`, waits for `kernel_info_reply`, and preloads each
|
|
61
|
+
helper in its own cell (one broken helper fails alone). Host and bridge exchange one JSON object
|
|
62
|
+
per line:
|
|
52
63
|
|
|
53
|
-
- **
|
|
54
|
-
|
|
55
|
-
- **
|
|
56
|
-
|
|
64
|
+
- **host → bridge**: `boot` (helper sources + snapshot policy), `exec`, `snapshot`, `restore`,
|
|
65
|
+
`listNames`, `interrupt`, `shutdown`;
|
|
66
|
+
- **bridge → host**: `ready`, `stream` (cell output, streamed), `result` (a cell settles only
|
|
67
|
+
once the shell reply **and** iopub idle have arrived — a tiny reply can beat a large output),
|
|
68
|
+
`reply` (op results), `error`.
|
|
57
69
|
|
|
58
|
-
|
|
70
|
+
Failure semantics have contract tests.
|
|
59
71
|
|
|
60
|
-
**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
settling on the reply alone would drop output still in flight.
|
|
72
|
+
**Death is immediate and truthful.** When the kernel dies, the bridge exits; the host observes
|
|
73
|
+
the pipe's EOF and the exit code, settles the running cell with an error, and the next call
|
|
74
|
+
rebuilds from the last snapshot.
|
|
64
75
|
|
|
65
76
|
**Output is capped per channel and per line**, both announced with markers: channels accumulate
|
|
66
|
-
against `maxOutputChars` (checked within each
|
|
77
|
+
against `maxOutputChars` (checked within each event), and each line is capped at 4096 chars, so
|
|
67
78
|
one oversized line cannot own the budget while long JSON/reprs/errors pass whole.
|
|
68
79
|
|
|
69
|
-
**Cancellation is real.** An abort
|
|
80
|
+
**Cancellation is real.** An abort makes the bridge send `interrupt_request`, raising a genuine
|
|
70
81
|
`KeyboardInterrupt`; the namespace survives. Cells wedged in C code (which ignore interrupts)
|
|
71
|
-
get a 20-second grace, then the
|
|
82
|
+
get a 20-second grace, then the process group is killed and the next call rebuilds from the last
|
|
72
83
|
snapshot.
|
|
73
84
|
|
|
74
85
|
**History is off.** IPython's `In`/`Out` retention pins every last-expression result and cannot be
|
|
@@ -97,21 +108,30 @@ Full contract: [helpers.md](helpers.md).
|
|
|
97
108
|
|
|
98
109
|
## Snapshots & honest resets
|
|
99
110
|
|
|
100
|
-
After each successful cell,
|
|
101
|
-
(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
After each successful cell, the bridge pickles the kernel's `globals` entry by entry with
|
|
112
|
+
cloudpickle (functions and classes serialize by value; one un-picklable binding costs only
|
|
113
|
+
itself), zlib-compresses each payload, and writes `namespace.snapshot` (format v4) under
|
|
114
|
+
`~/.pi/agent/pi-repl/state/<session>/` atomically. The gate — only a persisted write marks the
|
|
115
|
+
namespace as saved — lives in the bridge too, and a pickling snapshot never runs ahead of a user
|
|
116
|
+
cell because the bridge's own loop is the queue.
|
|
117
|
+
|
|
118
|
+
A fresh engine restores that snapshot **in the background**: the revive is a quiet-gap job the
|
|
119
|
+
bridge runs only when its queue has been idle, so a large revive does not delay the first cell;
|
|
120
|
+
only a mid-session rebuild (kernel death) forces the restore before the cell that found the
|
|
121
|
+
kernel dead. Functions and classes revive through cloudpickle's by-value serialization;
|
|
122
|
+
bindings that still fail are reported by name, never dropped silently. Entries are capped
|
|
123
|
+
per-binding and in total (128 MiB default); the file is written via temp-file-and-rename so a
|
|
124
|
+
crash cannot corrupt the last good copy; a binding skipped at save time is named in the resume
|
|
125
|
+
notice, never dropped silently, and a failed snapshot leaves the retry gate in place (only a
|
|
126
|
+
persisted write advances it); a periodic refresh (default 2 min, `snapshot.periodMs`, 0
|
|
127
|
+
disables) bounds the loss window for same-name mutations and stands down when the last snapshot
|
|
128
|
+
exceeded 8 MiB; value payloads are zlib-compressed cloudpickle streams (file format version 4 —
|
|
129
|
+
v1/v2/v3 files remain restorable); session dirs are pruned to the newest 25, and dirs whose
|
|
130
|
+
conversation file no longer exists are swept entirely — deleting a conversation deletes its
|
|
131
|
+
snapshots. "ephemeral" and the live session are exempt. A /fork'd conversation inherits the
|
|
132
|
+
parent's last snapshot — copied once into the fork's own key at first start, so it resumes with
|
|
133
|
+
state, carries the standard reset marker on its first cell, and the human gets a dedicated fork
|
|
134
|
+
toast; the parent is untouched.
|
|
115
135
|
|
|
116
136
|
A revive that never completes (a poisoned pickle) is bounded by an engine restore-cell watchdog
|
|
117
137
|
(`PI_REPL_BOOT_TIMEOUT_MS`, default 90s): the kernel is killed and the restore marked skipped —
|
package/index.ts
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
// --- pi-repl: one execute tool over Python; everything else runs as functions inside it ---
|
|
2
2
|
|
|
3
|
-
import { basename, join } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
|
-
import { withSkillsBlock } from "./src/extension/skill-hook.js";
|
|
8
|
-
import { buildHelpersPromptSection } from "./src/extension/helpers.js";
|
|
9
7
|
import { EngineManager, pruneOrphanedSnapshotDirs, pruneSnapshotDirs } from "./src/engine/index.js";
|
|
8
|
+
import { buildHelpersPromptSection } from "./src/extension/helpers.js";
|
|
9
|
+
import { buildPromptGuidelines, executePromptSnippet, executeToolDescription } from "./src/extension/prompt.js";
|
|
10
10
|
import { ExecuteCellComponent, type ExecuteDetails, type ExecuteRenderState } from "./src/extension/render.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
EngineLifecycle,
|
|
13
|
+
formatForkToast,
|
|
14
|
+
formatHelperFailuresLine,
|
|
15
|
+
formatHelperToast,
|
|
16
|
+
formatResetToast,
|
|
17
|
+
} from "./src/extension/session-engine.js";
|
|
18
|
+
import { withSkillsBlock } from "./src/extension/skill-hook.js";
|
|
12
19
|
import { conversationName, inheritForkSnapshot, resolveStateDir } from "./src/extension/state-layout.js";
|
|
13
|
-
import { EXECUTE_DESCRIPTION, buildExecutePromptGuidelines, EXECUTE_PROMPT_SNIPPET } from "./src/extension/tool-meta.js";
|
|
14
20
|
|
|
15
21
|
const executeSchema = Type.Object({
|
|
16
22
|
code: Type.String({
|
|
17
|
-
description: "Python
|
|
23
|
+
description: "Python source for this cell; stdout, stderr, and the last-expression result are returned.",
|
|
18
24
|
}),
|
|
19
25
|
});
|
|
20
26
|
|
|
@@ -76,15 +82,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
76
82
|
// --- a /fork'd conversation inherits the parent's last namespace (copied once into the fork's own key) ---
|
|
77
83
|
try {
|
|
78
84
|
forkInherited = inheritForkSnapshot(stateRoot, sessionFile, snapshotPath);
|
|
79
|
-
} catch {
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// a racing rename or unreadable session file: the fork starts empty, but loudly
|
|
87
|
+
console.error("[pi-repl] fork snapshot inheritance failed:", error);
|
|
88
|
+
}
|
|
80
89
|
// --- keep the state root from growing one dir per session forever; the live dir is exempt ---
|
|
81
90
|
try {
|
|
82
91
|
pruneSnapshotDirs(stateRoot, 25, currentDir);
|
|
83
|
-
} catch {
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error("[pi-repl] snapshot dir pruning failed:", error);
|
|
94
|
+
}
|
|
84
95
|
// --- sweep state dirs whose conversation file exists in no project root: deleting a conversation deletes its snapshots (both dir formats) ---
|
|
85
96
|
try {
|
|
86
97
|
pruneOrphanedSnapshotDirs(stateRoot, sessionFile ? dirname(dirname(sessionFile)) : undefined, currentDir);
|
|
87
|
-
} catch {
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// a readdir race with a concurrent sweep: the sweep retries next session, but loudly
|
|
100
|
+
console.error("[pi-repl] orphan snapshot sweep failed:", error);
|
|
101
|
+
}
|
|
88
102
|
}
|
|
89
103
|
return new EngineManager({
|
|
90
104
|
cwd,
|
|
@@ -146,9 +160,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
146
160
|
pi.registerTool<typeof executeSchema, ExecuteDetails, Partial<ExecuteRenderState>>({
|
|
147
161
|
name: "execute",
|
|
148
162
|
label: "execute",
|
|
149
|
-
description:
|
|
150
|
-
promptSnippet:
|
|
151
|
-
promptGuidelines:
|
|
163
|
+
description: executeToolDescription,
|
|
164
|
+
promptSnippet: executePromptSnippet,
|
|
165
|
+
promptGuidelines: buildPromptGuidelines(),
|
|
152
166
|
parameters: executeSchema,
|
|
153
167
|
renderShell: "self",
|
|
154
168
|
renderCall(args, theme, context) {
|
|
@@ -173,7 +187,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
173
187
|
},
|
|
174
188
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
175
189
|
if (!active()) {
|
|
176
|
-
throw new Error(
|
|
190
|
+
throw new Error(
|
|
191
|
+
"pi-repl is dormant in this session. Start pi with --repl (or PI_REPL_FORCE=1) to use execute.",
|
|
192
|
+
);
|
|
177
193
|
}
|
|
178
194
|
if (ctx?.cwd) location = { cwd: ctx.cwd, sessionFile: ctx.sessionManager?.getSessionFile?.() ?? undefined };
|
|
179
195
|
// --- establish the body slot at call time so Ctrl+O can expand a live, still-streaming cell ---
|
|
@@ -194,47 +210,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
194
210
|
const reset = lifecycle.takeResetNotice();
|
|
195
211
|
if (reset?.notice)
|
|
196
212
|
ctx?.ui?.notify?.(
|
|
197
|
-
m.inheritedFromFork
|
|
213
|
+
m.inheritedFromFork
|
|
214
|
+
? formatForkToast(reset.restore)
|
|
215
|
+
: formatResetToast(reset.origin, reset.restore, reset.wedged),
|
|
198
216
|
"info",
|
|
199
217
|
);
|
|
200
218
|
// --- helper verdicts once per boot: toast for the human, marker for the model only when a helper failed (all-good boots stay silent) ---
|
|
201
219
|
const helperReport = m.takeHelperReport();
|
|
202
220
|
if (helperReport && helperReport.length > 0) ctx?.ui?.notify?.(formatHelperToast(helperReport), "info");
|
|
203
|
-
const sections = [
|
|
204
|
-
reset?.notice,
|
|
205
|
-
formatHelperFailuresLine(helperReport),
|
|
206
|
-
r.stdout,
|
|
207
|
-
r.stderr,
|
|
208
|
-
r.result,
|
|
209
|
-
];
|
|
221
|
+
const sections = [reset?.notice, formatHelperFailuresLine(helperReport), r.stdout, r.stderr, r.result];
|
|
210
222
|
const errorLines = r.error ? composeErrorLines(r.error) : undefined;
|
|
211
223
|
if (r.status === "error" && errorLines) sections.push(errorLines.join("\n"));
|
|
212
224
|
if (r.status === "aborted") sections.push("[cell aborted]");
|
|
213
|
-
|
|
225
|
+
const text = sections.filter((section) => section !== undefined && section !== "").join("\n");
|
|
214
226
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
227
|
+
const details: ExecuteDetails = {
|
|
228
|
+
status: r.status,
|
|
229
|
+
durationMs: r.durationMs,
|
|
230
|
+
errorName: r.error?.name,
|
|
231
|
+
stdout: r.stdout || undefined,
|
|
232
|
+
stderr: r.stderr || undefined,
|
|
233
|
+
result: r.result,
|
|
234
|
+
errorStack: errorLines,
|
|
235
|
+
};
|
|
236
|
+
const result = { content: [{ type: "text" as const, text: text || "(no output)" }], details };
|
|
237
|
+
if (r.status === "error") {
|
|
238
|
+
pendingErrorResults.set(toolCallId, { details });
|
|
239
|
+
throw new Error(text || "(no output)");
|
|
240
|
+
}
|
|
241
|
+
// --- an aborted cell was interrupted, not wedged: the kernel keeps running, nothing to discard ---
|
|
242
|
+
return result;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
// --- a kernel that died (or was killed as the abort backstop) is down; drop it so the next cell rebuilds ---
|
|
245
|
+
if (m.isRunning === false) {
|
|
246
|
+
await lifecycle.discard();
|
|
247
|
+
}
|
|
248
|
+
throw error;
|
|
235
249
|
}
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
238
250
|
},
|
|
239
251
|
});
|
|
240
252
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-repl-py",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A pi extension with a single tool: execute, running a TypeScript host with a persistent Python (ipykernel) evaluator and a user-configurable toolbox of functions.",
|
|
6
6
|
"keywords": [
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"scripts",
|
|
30
30
|
"docs",
|
|
31
31
|
"index.ts",
|
|
32
|
+
"bridge.py",
|
|
32
33
|
"README.md",
|
|
33
34
|
"LICENSE"
|
|
34
35
|
],
|
package/scripts/setup-venv.mjs
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
/** postinstall: build the stable per-user venv at ~/.pi/agent/pi-repl/venv; repair in place if ipykernel is missing; a bad build fails the install loudly. */
|
|
3
3
|
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
|
-
import { mkdirSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
9
|
const VENV_DIR = join(homedir(), ".pi", "agent", "pi-repl", "venv");
|
|
10
10
|
const PY = join(VENV_DIR, "bin", "python3");
|
|
11
|
-
const DEPS = ["ipykernel"];
|
|
11
|
+
const DEPS = ["ipykernel", "cloudpickle"];
|
|
12
12
|
const HELPERS_DIR = join(homedir(), ".pi", "agent", "pi-repl", "helpers");
|
|
13
13
|
|
|
14
|
-
// A venv that can't import
|
|
15
|
-
function
|
|
14
|
+
// A venv that can't import every runtime dep is broken — never trust the binary alone.
|
|
15
|
+
function depsOk() {
|
|
16
16
|
try {
|
|
17
|
-
execSync(`${PY} -c "import ipykernel"`, { stdio: "ignore" });
|
|
17
|
+
execSync(`${PY} -c "import ipykernel, cloudpickle"`, { stdio: "ignore" });
|
|
18
18
|
return true;
|
|
19
19
|
} catch {
|
|
20
20
|
return false;
|
|
@@ -49,8 +49,8 @@ function seedHelpersDir() {
|
|
|
49
49
|
|
|
50
50
|
function main() {
|
|
51
51
|
seedHelpersDir();
|
|
52
|
-
if (
|
|
53
|
-
log(`venv ready (
|
|
52
|
+
if (depsOk()) {
|
|
53
|
+
log(`venv ready (${DEPS.join(", ")}) at ${VENV_DIR}`);
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
const systemPython = findSystemPython();
|
|
@@ -61,6 +61,19 @@ function main() {
|
|
|
61
61
|
);
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
+
// an existing venv missing a dep (e.g. cloudpickle added in an upgrade) is repaired in place;
|
|
65
|
+
// a missing/broken venv is rebuilt from a clean slate
|
|
66
|
+
if (existsSync(PY)) {
|
|
67
|
+
log(`repairing evaluator venv at ${VENV_DIR} (installing ${DEPS.join(" ")})...`);
|
|
68
|
+
try {
|
|
69
|
+
execSync(`${PY} -m pip install ${DEPS.join(" ")}`, { stdio: "inherit" });
|
|
70
|
+
if (!depsOk()) fail(`deps still not importable after install; the evaluator won't start.`);
|
|
71
|
+
log("done. The pi-repl evaluator will use this venv.");
|
|
72
|
+
return;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
fail(`could not repair the evaluator venv (${error && error.message ? error.message : error}). `);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
64
77
|
log(`building evaluator venv at ${VENV_DIR} (uses ${systemPython})...`);
|
|
65
78
|
try {
|
|
66
79
|
mkdirSync(join(VENV_DIR, ".."), { recursive: true });
|
|
@@ -68,8 +81,8 @@ function main() {
|
|
|
68
81
|
execSync(`${systemPython} -m venv --clear ${VENV_DIR}`, { stdio: "inherit" });
|
|
69
82
|
execSync(`${PY} -m pip install --upgrade pip`, { stdio: "inherit" });
|
|
70
83
|
execSync(`${PY} -m pip install ${DEPS.join(" ")}`, { stdio: "inherit" });
|
|
71
|
-
if (!
|
|
72
|
-
fail(`
|
|
84
|
+
if (!depsOk()) {
|
|
85
|
+
fail(`runtime deps still not importable after install; the evaluator won't start.`);
|
|
73
86
|
return;
|
|
74
87
|
}
|
|
75
88
|
log("done. The pi-repl evaluator will use this venv.");
|