better-dsh 0.3.0 → 0.3.1
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/README.md +49 -178
- package/cordis.patch.yml +4 -35
- package/docs/adr/0015-independent-bundles-agent-remote.md +40 -0
- package/docs/remote-tools.md +56 -0
- package/docs/specs/omp-python-repl-port/runtime-guide.md +33 -0
- package/docs/specs/omp-python-repl-port/spec.md +61 -0
- package/docs/specs/remote-tool-enhancement/spec.md +203 -0
- package/dsh-docs/baseline.md +13 -0
- package/dsh-docs/subsystems/invariants.md +3 -0
- package/dsh-docs/subsystems/invariants.zh.md +3 -0
- package/eval-description.md +7 -3
- package/lib/{LaunchOptions-kGXcZOe0.js → LaunchOptions-5eg7E0ye.js} +1 -1
- package/lib/LaunchOptions-DxQQ0s-A.js +4 -0
- package/lib/{bidi-BalpKaJc.js → bidi-CwabFH07.js} +1 -1
- package/lib/client/index.js +30 -6
- package/lib/failover/index.js +1 -1
- package/lib/fs-aware/sandbox-plugin.js +3 -3
- package/lib/{index-browser-CysrjMyM.js → index-browser-B-9jm12n.js} +4 -4
- package/lib/index.d.ts +17 -41
- package/lib/index.js +473 -1678
- package/lib/kernel-env-BOF0fNHn.js +91 -0
- package/lib/kernel-env.d.ts +4 -58
- package/lib/kernel-env.js +2 -2
- package/lib/{lsp-server-registry-BXvUTBY7.js → lsp-server-registry-BcdkHROD.js} +1 -1
- package/lib/{lsp-server-registry-CH9WNFFt.js → lsp-server-registry-D4VLs1Sm.js} +1 -1
- package/lib/{main-CupOhMp5.js → main-BeCgUd20.js} +8 -6
- package/lib/{main-BQeFrBM1.js → main-Dv_UQqG2.js} +1 -1
- package/lib/mobile/plugin.d.ts +1 -1
- package/lib/omp-python/DILL-LICENSE +35 -0
- package/lib/omp-python/OMP-LICENSE +23 -0
- package/lib/omp-python/better_snapshot.py +391 -0
- package/lib/omp-python/better_support.py +425 -0
- package/lib/omp-python/dill-provenance.json +55 -0
- package/lib/omp-python/provenance.json +13 -0
- package/lib/omp-python/runner.py +2499 -0
- package/lib/omp-python/vendor/dill/__diff.py +234 -0
- package/lib/omp-python/vendor/dill/__info__.py +291 -0
- package/lib/omp-python/vendor/dill/__init__.py +119 -0
- package/lib/omp-python/vendor/dill/_dill.py +2265 -0
- package/lib/omp-python/vendor/dill/_objects.py +545 -0
- package/lib/omp-python/vendor/dill/_shims.py +193 -0
- package/lib/omp-python/vendor/dill/detect.py +287 -0
- package/lib/omp-python/vendor/dill/logger.py +285 -0
- package/lib/omp-python/vendor/dill/objtypes.py +24 -0
- package/lib/omp-python/vendor/dill/pointers.py +122 -0
- package/lib/omp-python/vendor/dill/session.py +612 -0
- package/lib/omp-python/vendor/dill/settings.py +25 -0
- package/lib/omp-python/vendor/dill/source.py +1025 -0
- package/lib/omp-python/vendor/dill/temp.py +252 -0
- package/lib/{puppeteer-core-CSk2PtSm.js → puppeteer-core-Dnd7eqel.js} +9 -9
- package/lib/remote/plugin.d.ts +459 -5
- package/lib/remote/plugin.js +2389 -210
- package/lib/url-schemes/index.d.ts +11 -11
- package/lib/url-schemes/index.js +12 -12
- package/lib/web-password.d.ts +8 -8
- package/lib/web-password.js +5 -1
- package/lib/{web-trust-Dej_RI2B.d.ts → web-trust-CwZrTBcl.d.ts} +11 -11
- package/lib/web-trust.d.ts +1 -2
- package/lib/welcome-notice.d.ts +5 -7
- package/lib/welcome-notice.js +9 -48
- package/lib/{wrap-T4I0nfPM.js → wrap-0VA0uFx8.js} +1 -1
- package/package.json +6 -23
- package/scripts/kernel-provision.mjs +4 -34
- package/lib/LaunchOptions-B874eBu2.js +0 -4
- package/lib/bundle-delivery.d.ts +0 -20
- package/lib/bundle-delivery.js +0 -209
- package/lib/hmr.d.ts +0 -32
- package/lib/hmr.js +0 -49
- package/lib/kernel-bridge.py +0 -172
- package/lib/kernel-env-D8UOWeji.js +0 -195
- package/lib/webserver.d.ts +0 -10
- package/lib/webserver.js +0 -19
- package/scripts/register-bundle.mjs +0 -80
package/lib/bundle-delivery.js
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
7
|
-
|
|
8
|
-
//#region src/bundle-delivery.ts
|
|
9
|
-
const name = "dashr-bundle-delivery";
|
|
10
|
-
const inject = ["profileContext", "appReady"];
|
|
11
|
-
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
12
|
-
const json = async (path$1) => JSON.parse(await readFile(path$1, "utf8"));
|
|
13
|
-
async function optionalJson(path$1) {
|
|
14
|
-
try {
|
|
15
|
-
return await json(path$1);
|
|
16
|
-
} catch (error) {
|
|
17
|
-
if (error.code === "ENOENT") return void 0;
|
|
18
|
-
throw error;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
async function contained(root, path$1) {
|
|
22
|
-
if (isAbsolute(path$1)) throw new Error("Pack delivery path must be relative");
|
|
23
|
-
const base = await realpath(root), file = await realpath(resolve(base, path$1));
|
|
24
|
-
const rel = relative(base, file);
|
|
25
|
-
if (rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel)) throw new Error("Pack delivery path escapes its owner");
|
|
26
|
-
return file;
|
|
27
|
-
}
|
|
28
|
-
/** A child process avoids inheriting a running HMR transaction's AsyncLocalStorage. */
|
|
29
|
-
async function installNative(profile, archive, expected, signal) {
|
|
30
|
-
signal?.throwIfAborted();
|
|
31
|
-
const childScript = fileURLToPath(new URL("../scripts/register-bundle.mjs", import.meta.url));
|
|
32
|
-
return new Promise((done, fail) => {
|
|
33
|
-
const child = spawn(process.execPath, [childScript], {
|
|
34
|
-
stdio: [
|
|
35
|
-
"pipe",
|
|
36
|
-
"pipe",
|
|
37
|
-
"pipe"
|
|
38
|
-
],
|
|
39
|
-
cwd: profile.dir
|
|
40
|
-
});
|
|
41
|
-
let output = "", diagnostic = "";
|
|
42
|
-
const abort = () => {
|
|
43
|
-
child.kill("SIGTERM");
|
|
44
|
-
};
|
|
45
|
-
const timeout = setTimeout(abort, 36e4);
|
|
46
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
47
|
-
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
48
|
-
output = (output + chunk).slice(-32768);
|
|
49
|
-
});
|
|
50
|
-
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
51
|
-
diagnostic = (diagnostic + chunk).slice(-4096);
|
|
52
|
-
});
|
|
53
|
-
child.on("error", fail);
|
|
54
|
-
child.on("close", (code) => {
|
|
55
|
-
clearTimeout(timeout);
|
|
56
|
-
signal?.removeEventListener("abort", abort);
|
|
57
|
-
if (signal?.aborted) {
|
|
58
|
-
fail(signal.reason);
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (code !== 0) {
|
|
62
|
-
fail(/* @__PURE__ */ new Error(`Native companion registration failed (${code}): ${diagnostic.trim() || output.trim()}`));
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const result = JSON.parse(output);
|
|
67
|
-
if (result.exitCode !== 0) fail(/* @__PURE__ */ new Error(`Native companion registration failed (${result.exitCode}); diagnostics: ${result.logPath ?? "unavailable"}`));
|
|
68
|
-
else if (![
|
|
69
|
-
"installed",
|
|
70
|
-
"owner-changed",
|
|
71
|
-
"companion-changed",
|
|
72
|
-
"companion-removed"
|
|
73
|
-
].includes(result.disposition)) fail(/* @__PURE__ */ new Error("Native companion registration returned an invalid disposition"));
|
|
74
|
-
else done(result.disposition);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
fail(error);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
child.stdin.on("error", () => {});
|
|
80
|
-
child.stdin.end(JSON.stringify({
|
|
81
|
-
profile,
|
|
82
|
-
archive,
|
|
83
|
-
expected
|
|
84
|
-
}));
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
/** First registration enables a companion; native reconciliation preserves retained disablement. */
|
|
88
|
-
async function reconcileBundledDelivery(profile, signal) {
|
|
89
|
-
const directory = join(profile.home, "bundle-artifacts");
|
|
90
|
-
await mkdir(directory, {
|
|
91
|
-
recursive: true,
|
|
92
|
-
mode: 448
|
|
93
|
-
});
|
|
94
|
-
const key = hash(profile.dir);
|
|
95
|
-
return withFileLock(join(directory, key + ".delivery"), async () => {
|
|
96
|
-
signal?.throwIfAborted();
|
|
97
|
-
const receiptPath = join(directory, key + ".json");
|
|
98
|
-
const receipts = await optionalJson(receiptPath) ?? {};
|
|
99
|
-
const manifest = await json(join(profile.dir, "package.json"));
|
|
100
|
-
const result = {
|
|
101
|
-
installed: [],
|
|
102
|
-
restartRequired: false
|
|
103
|
-
};
|
|
104
|
-
for (const owner of Object.keys(manifest.dependencies ?? {})) {
|
|
105
|
-
const ownerDir = join(profile.dir, "node_modules", owner);
|
|
106
|
-
const ownerManifest = await optionalJson(join(ownerDir, "package.json"));
|
|
107
|
-
if (ownerManifest?.superd?.delivery === void 0) continue;
|
|
108
|
-
const descriptorPath = await contained(ownerDir, ownerManifest.superd.delivery);
|
|
109
|
-
const descriptor = await json(descriptorPath);
|
|
110
|
-
if (!Array.isArray(descriptor.companions)) throw new Error("Invalid pack delivery descriptor");
|
|
111
|
-
for (const item of descriptor.companions) {
|
|
112
|
-
if (item.name !== "better-dsh" || item.version !== ownerManifest.dependencies?.[item.name] || !/^[a-f0-9]{64}$/.test(item.sha256)) throw new Error("Invalid companion identity/version/digest");
|
|
113
|
-
const bytes = await readFile(await contained(dirname(descriptorPath), item.tarball));
|
|
114
|
-
if (hash(bytes) !== item.sha256) throw new Error(`Pack companion SHA256 mismatch: ${item.name}`);
|
|
115
|
-
const archive = join(directory, `${item.name}-${item.sha256}.tgz`);
|
|
116
|
-
const cached = await optionalJson(join(profile.dir, "node_modules", item.name, "package.json"));
|
|
117
|
-
const current = await json(join(profile.dir, "package.json"));
|
|
118
|
-
const ownerSpec = current.dependencies?.[owner];
|
|
119
|
-
if (ownerSpec === void 0) continue;
|
|
120
|
-
const priorSpec = current.dependencies?.[item.name];
|
|
121
|
-
const receiptKey = `${owner}:${item.name}`;
|
|
122
|
-
if (priorSpec === void 0 && receipts[receiptKey] === item.sha256) continue;
|
|
123
|
-
if (priorSpec === "file:" + archive && cached?.name === item.name && cached.version === item.version) {
|
|
124
|
-
receipts[receiptKey] = item.sha256;
|
|
125
|
-
await writeFileAtomic(receiptPath, JSON.stringify(receipts) + "\n", { mode: 384 });
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
if (hash(await readFile(archive)) !== item.sha256) throw new Error(`Cached companion SHA256 mismatch: ${item.name}`);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
if (error.code !== "ENOENT") throw error;
|
|
132
|
-
const temporary = archive + "." + randomUUID() + ".tmp";
|
|
133
|
-
try {
|
|
134
|
-
await writeFile(temporary, bytes, {
|
|
135
|
-
flag: "wx",
|
|
136
|
-
mode: 384
|
|
137
|
-
});
|
|
138
|
-
await rename(temporary, archive);
|
|
139
|
-
} finally {
|
|
140
|
-
await rm(temporary, { force: true });
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
signal?.throwIfAborted();
|
|
144
|
-
const disposition = await installNative(profile, archive, {
|
|
145
|
-
owner,
|
|
146
|
-
ownerSpec,
|
|
147
|
-
companion: item,
|
|
148
|
-
...priorSpec === void 0 ? {} : { priorSpec }
|
|
149
|
-
}, signal);
|
|
150
|
-
if (disposition === "owner-changed" || disposition === "companion-changed") continue;
|
|
151
|
-
receipts[receiptKey] = item.sha256;
|
|
152
|
-
await writeFileAtomic(receiptPath, JSON.stringify(receipts) + "\n", { mode: 384 });
|
|
153
|
-
if (disposition === "companion-removed") continue;
|
|
154
|
-
result.installed.push(`${item.name}@${item.version}`);
|
|
155
|
-
result.restartRequired ||= priorSpec !== void 0;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return result;
|
|
159
|
-
}, { waitMs: 12e4 });
|
|
160
|
-
}
|
|
161
|
-
function apply(ctx) {
|
|
162
|
-
const signal = new AbortController();
|
|
163
|
-
let started = false, running = false, again = false;
|
|
164
|
-
const reconcile = async () => {
|
|
165
|
-
if (!started || signal.signal.aborted) return;
|
|
166
|
-
if (running) {
|
|
167
|
-
again = true;
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
running = true;
|
|
171
|
-
try {
|
|
172
|
-
const result = await reconcileBundledDelivery(ctx.profileContext, signal.signal);
|
|
173
|
-
if (result.installed.length) {
|
|
174
|
-
ctx.logger.info(`Registered ${result.installed.join(", ")} from its containing local pack${result.restartRequired ? "; restart DSH to use the updated code" : ""}`);
|
|
175
|
-
ctx.emit("plugin-manager/changed", { reason: "install" });
|
|
176
|
-
}
|
|
177
|
-
} catch (error) {
|
|
178
|
-
if (!signal.signal.aborted) ctx.logger.error("Local pack companion registration failed; retry by re-enabling the bundle", error);
|
|
179
|
-
} finally {
|
|
180
|
-
running = false;
|
|
181
|
-
if (again && !signal.signal.aborted) {
|
|
182
|
-
again = false;
|
|
183
|
-
reconcile();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
ctx.effect(() => {
|
|
188
|
-
const offReady = ctx.appReady.onReady(() => {
|
|
189
|
-
started = true;
|
|
190
|
-
reconcile();
|
|
191
|
-
});
|
|
192
|
-
const offChange = ctx.on("plugin-manager/changed", () => {
|
|
193
|
-
reconcile();
|
|
194
|
-
});
|
|
195
|
-
return () => {
|
|
196
|
-
offReady();
|
|
197
|
-
offChange();
|
|
198
|
-
signal.abort();
|
|
199
|
-
};
|
|
200
|
-
}, "bundle-delivery: native registration");
|
|
201
|
-
}
|
|
202
|
-
var bundle_delivery_default = {
|
|
203
|
-
name,
|
|
204
|
-
inject,
|
|
205
|
-
apply
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
//#endregion
|
|
209
|
-
export { apply, bundle_delivery_default as default, inject, name, reconcileBundledDelivery };
|
package/lib/hmr.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import Hmr, { HmrConfig } from "@deepseek-ai/dsh-hmr";
|
|
2
|
-
import { Context } from "@deepseek-ai/cordis";
|
|
3
|
-
|
|
4
|
-
//#region src/hmr.d.ts
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Native runExclusive keeps a boolean in ALS. HTTP servers created while
|
|
8
|
-
* replacing webServer inherit that boolean after the transaction finishes,
|
|
9
|
-
* so subsequent native Plugin Manager requests are refused as nested.
|
|
10
|
-
*
|
|
11
|
-
* The public runExclusive/watchConfig APIs expose no selective context reset.
|
|
12
|
-
* AsyncLocalStorage.snapshot() also resets AgentRegistry initiator attribution
|
|
13
|
-
* and its teardown lineage, so it cannot safely repair this host transaction.
|
|
14
|
-
* This compatibility bridge reads the native TS-private `executing` storage
|
|
15
|
-
* after checking its runtime shape. It never replaces that storage or changes
|
|
16
|
-
* upstream files: only exit() clears its inherited value while the unchanged
|
|
17
|
-
* native method establishes the next transaction. Native queue, closing,
|
|
18
|
-
* exact watchers, module reload and disposal retain their original semantics.
|
|
19
|
-
*
|
|
20
|
-
* The mutable token rejects nesting only while its operation is still active;
|
|
21
|
-
* resources created by that operation can initiate later transactions.
|
|
22
|
-
* A native implementation change fails initialization explicitly instead of
|
|
23
|
-
* silently dropping queue or lifecycle guarantees.
|
|
24
|
-
*/
|
|
25
|
-
declare class BetterHmr extends Hmr {
|
|
26
|
-
private readonly nativeTransactions;
|
|
27
|
-
private readonly liveTransactions;
|
|
28
|
-
constructor(ctx: Context, config: HmrConfig);
|
|
29
|
-
runExclusive<T>(operation: () => Promise<T>): Promise<T>;
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
export { BetterHmr as default };
|
package/lib/hmr.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import Hmr from "@deepseek-ai/dsh-hmr";
|
|
3
|
-
|
|
4
|
-
//#region src/hmr.ts
|
|
5
|
-
/**
|
|
6
|
-
* Native runExclusive keeps a boolean in ALS. HTTP servers created while
|
|
7
|
-
* replacing webServer inherit that boolean after the transaction finishes,
|
|
8
|
-
* so subsequent native Plugin Manager requests are refused as nested.
|
|
9
|
-
*
|
|
10
|
-
* The public runExclusive/watchConfig APIs expose no selective context reset.
|
|
11
|
-
* AsyncLocalStorage.snapshot() also resets AgentRegistry initiator attribution
|
|
12
|
-
* and its teardown lineage, so it cannot safely repair this host transaction.
|
|
13
|
-
* This compatibility bridge reads the native TS-private `executing` storage
|
|
14
|
-
* after checking its runtime shape. It never replaces that storage or changes
|
|
15
|
-
* upstream files: only exit() clears its inherited value while the unchanged
|
|
16
|
-
* native method establishes the next transaction. Native queue, closing,
|
|
17
|
-
* exact watchers, module reload and disposal retain their original semantics.
|
|
18
|
-
*
|
|
19
|
-
* The mutable token rejects nesting only while its operation is still active;
|
|
20
|
-
* resources created by that operation can initiate later transactions.
|
|
21
|
-
* A native implementation change fails initialization explicitly instead of
|
|
22
|
-
* silently dropping queue or lifecycle guarantees.
|
|
23
|
-
*/
|
|
24
|
-
var BetterHmr = class extends Hmr {
|
|
25
|
-
nativeTransactions;
|
|
26
|
-
liveTransactions = new AsyncLocalStorage();
|
|
27
|
-
constructor(ctx, config) {
|
|
28
|
-
super(ctx, config);
|
|
29
|
-
const storage = this.executing;
|
|
30
|
-
if (!(storage instanceof AsyncLocalStorage) || typeof storage.getStore !== "function" || typeof storage.exit !== "function" || typeof storage.run !== "function") throw new Error("Better Dash HMR cannot reset this native HMR transaction context: expected executing AsyncLocalStorage");
|
|
31
|
-
this.nativeTransactions = storage;
|
|
32
|
-
}
|
|
33
|
-
runExclusive(operation) {
|
|
34
|
-
if (this.liveTransactions.getStore()?.active) return Promise.reject(/* @__PURE__ */ new Error("HMR transactions cannot be nested"));
|
|
35
|
-
return this.nativeTransactions.exit(() => super.runExclusive(() => {
|
|
36
|
-
const token = { active: true };
|
|
37
|
-
return this.liveTransactions.run(token, async () => {
|
|
38
|
-
try {
|
|
39
|
-
return await operation();
|
|
40
|
-
} finally {
|
|
41
|
-
token.active = false;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
}));
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
//#endregion
|
|
49
|
-
export { BetterHmr as default };
|
package/lib/kernel-bridge.py
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""stdio <-> ZeroMQ bridge for the DASHR REPL.
|
|
3
|
-
|
|
4
|
-
The host (Node/TS) owns the kernel lifecycle but must not link a native ZMQ
|
|
5
|
-
binding: `zeromq` was a hard npm dependency whose install script compiles C++
|
|
6
|
-
inside the consumer's install transaction (and whose top-level import killed
|
|
7
|
-
the whole plugin when the binding was missing). This process is the only ZMQ
|
|
8
|
-
speaker on the host side; it speaks JSON lines on stdio to Node.
|
|
9
|
-
|
|
10
|
-
Node ──stdio JSON lines──► kernel-bridge.py ──pyzmq/jupyter_client──► ipykernel
|
|
11
|
-
|
|
12
|
-
Requirements come from the kernel's own environment: `jupyter_client` and
|
|
13
|
-
`pyzmq` are transitive dependencies of the `ipykernel` we provision (see
|
|
14
|
-
`kernel-env.ts`), so nothing extra is fetched from any package manager.
|
|
15
|
-
|
|
16
|
-
Protocol
|
|
17
|
-
--------
|
|
18
|
-
stdin (one JSON object per line)::
|
|
19
|
-
|
|
20
|
-
{"op":"send","channel":"shell"|"control","header":{...},"content":{...}}
|
|
21
|
-
{"op":"interrupt"}
|
|
22
|
-
{"op":"shutdown"}
|
|
23
|
-
|
|
24
|
-
stdout (one JSON object per line)::
|
|
25
|
-
|
|
26
|
-
{"ev":"ready"}
|
|
27
|
-
{"ev":"msg","channel":"shell"|"iopub"|"control"|"stdin","msg":{...}}
|
|
28
|
-
{"ev":"fatal","message":"..."}
|
|
29
|
-
|
|
30
|
-
The caller-supplied `header` is passed to `Session.msg` verbatim, so the
|
|
31
|
-
message id the host generated is the id on the wire — parent-matching and
|
|
32
|
-
request/response correlation stay exactly as they were on the raw-socket path.
|
|
33
|
-
"""
|
|
34
|
-
from __future__ import annotations
|
|
35
|
-
|
|
36
|
-
import json
|
|
37
|
-
import queue
|
|
38
|
-
import sys
|
|
39
|
-
import threading
|
|
40
|
-
|
|
41
|
-
CHANNELS = ("iopub", "shell", "control", "stdin")
|
|
42
|
-
POLL_SECONDS = 0.2
|
|
43
|
-
|
|
44
|
-
# Protocol output goes to the real stdout; anything a library prints must never
|
|
45
|
-
# corrupt the byte stream the host parses, so sys.stdout is repointed to stderr.
|
|
46
|
-
_PROTOCOL_OUT = sys.stdout
|
|
47
|
-
sys.stdout = sys.stderr
|
|
48
|
-
|
|
49
|
-
_write_lock = threading.Lock()
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def emit(payload: dict) -> None:
|
|
53
|
-
"""Write one protocol line, atomically across the pump threads."""
|
|
54
|
-
# `default=str`: jupyter_client hands back parsed values (a header `date`
|
|
55
|
-
# arrives as `datetime`, for instance) that are not JSON-native; the host
|
|
56
|
-
# only reads msg_id/msg_type/content, so stringifying the rest is lossless
|
|
57
|
-
# for its purposes and keeps one pump thread from dying on a header field.
|
|
58
|
-
line = json.dumps(payload, ensure_ascii=False, default=str)
|
|
59
|
-
with _write_lock:
|
|
60
|
-
_PROTOCOL_OUT.write(line + "\n")
|
|
61
|
-
_PROTOCOL_OUT.flush()
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _clean(value: object) -> object:
|
|
65
|
-
"""Strip binary buffers: this protocol carries JSON only."""
|
|
66
|
-
if isinstance(value, dict):
|
|
67
|
-
return {key: _clean(item) for key, item in value.items() if key != "buffers"}
|
|
68
|
-
if isinstance(value, (list, tuple)):
|
|
69
|
-
return [_clean(item) for item in value]
|
|
70
|
-
if isinstance(value, bytes):
|
|
71
|
-
return value.decode("utf-8", "replace")
|
|
72
|
-
return value
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def pump(client, name: str, stop: threading.Event) -> None:
|
|
76
|
-
"""Relay one client channel onto stdout until `stop` is set."""
|
|
77
|
-
channel = getattr(client, f"{name}_channel")
|
|
78
|
-
while not stop.is_set():
|
|
79
|
-
try:
|
|
80
|
-
message = channel.get_msg(timeout=POLL_SECONDS)
|
|
81
|
-
except queue.Empty:
|
|
82
|
-
continue
|
|
83
|
-
except Exception as error: # channel torn down, or the socket died
|
|
84
|
-
if not stop.is_set():
|
|
85
|
-
emit({"ev": "fatal", "channel": name, "message": f"{type(error).__name__}: {error}"})
|
|
86
|
-
try:
|
|
87
|
-
emit({
|
|
88
|
-
"ev": "msg",
|
|
89
|
-
"channel": name,
|
|
90
|
-
"msg": {
|
|
91
|
-
"header": message.get("header") or {},
|
|
92
|
-
"parent_header": message.get("parent_header") or {},
|
|
93
|
-
"content": _clean(message.get("content") or {}),
|
|
94
|
-
"metadata": _clean(message.get("metadata") or {}),
|
|
95
|
-
},
|
|
96
|
-
})
|
|
97
|
-
except Exception as error: # never let one message kill a channel pump
|
|
98
|
-
sys.stderr.write(f"kernel-bridge: cannot relay {name} message: {type(error).__name__}: {error}\n")
|
|
99
|
-
sys.stderr.flush()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def main() -> int:
|
|
103
|
-
if len(sys.argv) != 2:
|
|
104
|
-
emit({"ev": "fatal", "message": "usage: kernel-bridge.py <connection-file>"})
|
|
105
|
-
return 2
|
|
106
|
-
|
|
107
|
-
try:
|
|
108
|
-
from jupyter_client import BlockingKernelClient
|
|
109
|
-
except Exception as error: # the kernel environment is incomplete
|
|
110
|
-
emit({"ev": "fatal", "message": f"jupyter_client unavailable: {type(error).__name__}: {error}"})
|
|
111
|
-
return 3
|
|
112
|
-
|
|
113
|
-
try:
|
|
114
|
-
client = BlockingKernelClient(connection_file=sys.argv[1])
|
|
115
|
-
client.load_connection_file()
|
|
116
|
-
client.start_channels()
|
|
117
|
-
except Exception as error:
|
|
118
|
-
emit({"ev": "fatal", "message": f"cannot connect to the kernel: {type(error).__name__}: {error}"})
|
|
119
|
-
return 4
|
|
120
|
-
|
|
121
|
-
stop = threading.Event()
|
|
122
|
-
for name in CHANNELS:
|
|
123
|
-
threading.Thread(target=pump, args=(client, name, stop), name=f"pump-{name}", daemon=True).start()
|
|
124
|
-
|
|
125
|
-
emit({"ev": "ready"})
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
for line in sys.stdin:
|
|
129
|
-
line = line.strip()
|
|
130
|
-
if not line:
|
|
131
|
-
continue
|
|
132
|
-
try:
|
|
133
|
-
request = json.loads(line)
|
|
134
|
-
except Exception:
|
|
135
|
-
continue
|
|
136
|
-
op = request.get("op")
|
|
137
|
-
if op == "send":
|
|
138
|
-
channel_name = request.get("channel")
|
|
139
|
-
if channel_name not in ("shell", "control"):
|
|
140
|
-
emit({"ev": "fatal", "message": f"unknown send channel {channel_name!r}"})
|
|
141
|
-
continue
|
|
142
|
-
channel = getattr(client, f"{channel_name}_channel")
|
|
143
|
-
try:
|
|
144
|
-
header = request.get("header") or None
|
|
145
|
-
message = client.session.msg(
|
|
146
|
-
header["msg_type"] if header else "comm_msg",
|
|
147
|
-
content=request.get("content") or {},
|
|
148
|
-
header=header,
|
|
149
|
-
)
|
|
150
|
-
channel.send(message)
|
|
151
|
-
except Exception as error:
|
|
152
|
-
emit({"ev": "fatal", "channel": channel_name, "message": f"send failed: {type(error).__name__}: {error}"})
|
|
153
|
-
elif op == "interrupt":
|
|
154
|
-
try:
|
|
155
|
-
client.interrupt_kernel()
|
|
156
|
-
except Exception as error:
|
|
157
|
-
emit({"ev": "fatal", "message": f"interrupt failed: {type(error).__name__}: {error}"})
|
|
158
|
-
elif op == "shutdown":
|
|
159
|
-
break
|
|
160
|
-
except (KeyboardInterrupt, BrokenPipeError):
|
|
161
|
-
pass
|
|
162
|
-
finally:
|
|
163
|
-
stop.set()
|
|
164
|
-
try:
|
|
165
|
-
client.stop_channels()
|
|
166
|
-
except Exception:
|
|
167
|
-
pass
|
|
168
|
-
return 0
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if __name__ == "__main__":
|
|
172
|
-
sys.exit(main())
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
|
-
|
|
7
|
-
//#region src/kernel-env.ts
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
|
-
/** The installed package root (where `lib/` and `src/` live): the default home of the managed venv. */
|
|
10
|
-
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
11
|
-
/** The config value meaning "no explicit interpreter — auto-manage a venv". */
|
|
12
|
-
const AUTO_PYTHON_SENTINEL = "python3";
|
|
13
|
-
/** Preferred CPython version for a managed venv. */
|
|
14
|
-
const DEFAULT_KERNEL_PYTHON_VERSION = "3.11";
|
|
15
|
-
/** Pinned, tested `ipykernel` version (2026-09-02 tested value; upgrade = explicit change + regression). */
|
|
16
|
-
const IPYKERNEL_VERSION = "7.3.0";
|
|
17
|
-
/** Pinned, tested `dill` version. */
|
|
18
|
-
const DILL_VERSION = "0.4.1";
|
|
19
|
-
/** A probe that never imports user modules and reports exactly what we need. */
|
|
20
|
-
const PROBE_CODE = [
|
|
21
|
-
"import json, sys",
|
|
22
|
-
"r = {\"version\": sys.version.split()[0], \"ipykernel\": False, \"dill\": False}",
|
|
23
|
-
"try:",
|
|
24
|
-
" import ipykernel # noqa: F401",
|
|
25
|
-
" r[\"ipykernel\"] = True",
|
|
26
|
-
"except Exception:",
|
|
27
|
-
" pass",
|
|
28
|
-
"try:",
|
|
29
|
-
" import dill # noqa: F401",
|
|
30
|
-
" r[\"dill\"] = True",
|
|
31
|
-
"except Exception:",
|
|
32
|
-
" pass",
|
|
33
|
-
"print(json.dumps(r))"
|
|
34
|
-
].join("\n");
|
|
35
|
-
async function probePython(python) {
|
|
36
|
-
try {
|
|
37
|
-
const { stdout } = await execFileAsync(python, ["-c", PROBE_CODE], { timeout: 15e3 });
|
|
38
|
-
const parsed = JSON.parse(stdout.trim());
|
|
39
|
-
return {
|
|
40
|
-
version: typeof parsed.version === "string" && parsed.version.length > 0 ? parsed.version : "unknown",
|
|
41
|
-
ipykernel: parsed.ipykernel === true,
|
|
42
|
-
dill: parsed.dill === true
|
|
43
|
-
};
|
|
44
|
-
} catch {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/** The venv interpreter path for this platform. */
|
|
49
|
-
function venvPythonPath(venvDir) {
|
|
50
|
-
return process.platform === "win32" ? join(venvDir, "Scripts", "python.exe") : join(venvDir, "bin", "python");
|
|
51
|
-
}
|
|
52
|
-
async function commandExists(cmd) {
|
|
53
|
-
try {
|
|
54
|
-
await execFileAsync(cmd, ["--version"], { timeout: 1e4 });
|
|
55
|
-
return true;
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
/** Tool-cache env for uv: caches live NEXT TO the venv (plugin territory), so a read-only home can never block provisioning. */
|
|
61
|
-
function uvToolEnv(venvDir) {
|
|
62
|
-
const cacheDir = join(dirname(venvDir), ".uv-cache");
|
|
63
|
-
return {
|
|
64
|
-
...process.env,
|
|
65
|
-
UV_CACHE_DIR: cacheDir,
|
|
66
|
-
UV_PYTHON_INSTALL_DIR: join(cacheDir, "python"),
|
|
67
|
-
UV_LINK_MODE: "copy"
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/** uv venv arguments (pure; unit-tested). */
|
|
71
|
-
function uvVenvArgs(venvDir, pythonVersion) {
|
|
72
|
-
return [
|
|
73
|
-
"venv",
|
|
74
|
-
venvDir,
|
|
75
|
-
"--python",
|
|
76
|
-
pythonVersion
|
|
77
|
-
];
|
|
78
|
-
}
|
|
79
|
-
/** python3 -m venv fallback arguments (pure; cache-free by construction). */
|
|
80
|
-
function venvFallbackArgs(venvDir) {
|
|
81
|
-
return [
|
|
82
|
-
"-m",
|
|
83
|
-
"venv",
|
|
84
|
-
venvDir
|
|
85
|
-
];
|
|
86
|
-
}
|
|
87
|
-
/** uv pip install arguments with pinned versions (pure; unit-tested). */
|
|
88
|
-
function uvInstallArgs(python) {
|
|
89
|
-
return [
|
|
90
|
-
"pip",
|
|
91
|
-
"install",
|
|
92
|
-
"--python",
|
|
93
|
-
python,
|
|
94
|
-
"--quiet",
|
|
95
|
-
`ipykernel==${IPYKERNEL_VERSION}`,
|
|
96
|
-
`dill==${DILL_VERSION}`
|
|
97
|
-
];
|
|
98
|
-
}
|
|
99
|
-
/** pip install arguments with pinned versions for the venv interpreter (pure). */
|
|
100
|
-
function pipInstallArgs() {
|
|
101
|
-
return [
|
|
102
|
-
"-m",
|
|
103
|
-
"pip",
|
|
104
|
-
"install",
|
|
105
|
-
"--quiet",
|
|
106
|
-
"--disable-pip-version-check",
|
|
107
|
-
`ipykernel==${IPYKERNEL_VERSION}`,
|
|
108
|
-
`dill==${DILL_VERSION}`
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
|
-
/** Create the venv (when absent) and return its interpreter path. */
|
|
112
|
-
async function createVenv(venvDir, pythonVersion) {
|
|
113
|
-
const python = venvPythonPath(venvDir);
|
|
114
|
-
if (existsSync(python)) return python;
|
|
115
|
-
if (await commandExists("uv")) {
|
|
116
|
-
mkdirSync(join(dirname(venvDir), ".uv-cache"), { recursive: true });
|
|
117
|
-
await execFileAsync("uv", uvVenvArgs(venvDir, pythonVersion), {
|
|
118
|
-
timeout: 12e4,
|
|
119
|
-
env: uvToolEnv(venvDir)
|
|
120
|
-
});
|
|
121
|
-
} else await execFileAsync("python3", venvFallbackArgs(venvDir), { timeout: 12e4 });
|
|
122
|
-
return python;
|
|
123
|
-
}
|
|
124
|
-
/** Install pinned `ipykernel` + `dill` into the venv interpreter. */
|
|
125
|
-
async function installDeps(python, venvDir) {
|
|
126
|
-
if (await commandExists("uv")) await execFileAsync("uv", uvInstallArgs(python), {
|
|
127
|
-
timeout: 18e4,
|
|
128
|
-
env: uvToolEnv(venvDir)
|
|
129
|
-
});
|
|
130
|
-
else await execFileAsync(python, pipInstallArgs(), { timeout: 18e4 });
|
|
131
|
-
}
|
|
132
|
-
/** Provision (or repair) the managed venv so it has `ipykernel` + `dill`. */
|
|
133
|
-
async function ensureVenv(venvDir, pythonVersion, log) {
|
|
134
|
-
const python = venvPythonPath(venvDir);
|
|
135
|
-
const existing = existsSync(python) ? await probePython(python) : null;
|
|
136
|
-
if (existing?.ipykernel === true && existing.dill === true) return python;
|
|
137
|
-
await createVenv(venvDir, pythonVersion);
|
|
138
|
-
if (!existsSync(python)) throw new Error(`dashr-repl: managed kernel venv was not created at ${venvDir} (no interpreter at ${python})`);
|
|
139
|
-
log?.("info", `dashr-repl: provisioning kernel venv at ${venvDir} (ipykernel + dill)`);
|
|
140
|
-
await installDeps(python, venvDir);
|
|
141
|
-
return python;
|
|
142
|
-
}
|
|
143
|
-
/**
|
|
144
|
-
* Resolve the kernel interpreter, provisioning a managed venv when no
|
|
145
|
-
* explicit interpreter is configured. Throws when the resolved interpreter
|
|
146
|
-
* cannot run `ipykernel`; a missing `dill` only disables snapshots (warned).
|
|
147
|
-
*/
|
|
148
|
-
async function resolveKernelEnv(options = {}) {
|
|
149
|
-
const log = options.log;
|
|
150
|
-
const explicit = options.python !== void 0 && options.python !== "" && options.python !== AUTO_PYTHON_SENTINEL ? options.python : void 0;
|
|
151
|
-
if (explicit !== void 0) {
|
|
152
|
-
const probe$1 = await probePython(explicit);
|
|
153
|
-
if (probe$1 === null) throw new Error(`dashr-repl: kernel python ${JSON.stringify(explicit)} could not be executed — check the path`);
|
|
154
|
-
if (!probe$1.ipykernel) throw new Error(`dashr-repl: kernel python ${JSON.stringify(explicit)} has no ipykernel — install it (pip install ipykernel) or set python to a managed venv`);
|
|
155
|
-
if (!probe$1.dill) log?.("warn", `dashr-repl: kernel python ${JSON.stringify(explicit)} has no dill — namespace snapshots are DISABLED (install it: pip install dill)`);
|
|
156
|
-
return {
|
|
157
|
-
python: explicit,
|
|
158
|
-
managed: false,
|
|
159
|
-
venvDir: void 0,
|
|
160
|
-
dill: probe$1.dill,
|
|
161
|
-
ipykernel: true,
|
|
162
|
-
version: probe$1.version
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
const venvDir = options.venvDir ?? join(PACKAGE_ROOT, ".venv-kernel");
|
|
166
|
-
const python = venvPythonPath(venvDir);
|
|
167
|
-
if (options.autoInstall === false) {
|
|
168
|
-
const probe$1 = await probePython(python);
|
|
169
|
-
if (probe$1 === null || !probe$1.ipykernel) throw new Error(`dashr-repl: managed kernel venv is missing or incomplete at ${venvDir} — run 'npm run kernel:venv' in the dashr package, or set kernelAutoInstall: true (or python to a prepared interpreter)`);
|
|
170
|
-
if (!probe$1.dill) log?.("warn", `dashr-repl: kernel venv at ${venvDir} has no dill — namespace snapshots are DISABLED (run 'npm run kernel:venv' to reinstall)`);
|
|
171
|
-
return {
|
|
172
|
-
python,
|
|
173
|
-
managed: true,
|
|
174
|
-
venvDir,
|
|
175
|
-
dill: probe$1.dill,
|
|
176
|
-
ipykernel: true,
|
|
177
|
-
version: probe$1.version
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const resolved = await ensureVenv(venvDir, options.pythonVersion ?? DEFAULT_KERNEL_PYTHON_VERSION, log);
|
|
181
|
-
const probe = await probePython(resolved);
|
|
182
|
-
if (probe === null || !probe.ipykernel) throw new Error(`dashr-repl: managed kernel venv at ${venvDir} still has no ipykernel after provisioning — check uv/python3-venv availability and retry`);
|
|
183
|
-
if (!probe.dill) log?.("warn", `dashr-repl: managed kernel venv at ${venvDir} has no dill after provisioning — namespace snapshots are DISABLED`);
|
|
184
|
-
return {
|
|
185
|
-
python: resolved,
|
|
186
|
-
managed: true,
|
|
187
|
-
venvDir,
|
|
188
|
-
dill: probe.dill,
|
|
189
|
-
ipykernel: true,
|
|
190
|
-
version: probe.version
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
//#endregion
|
|
195
|
-
export { PACKAGE_ROOT as a, uvInstallArgs as c, venvFallbackArgs as d, IPYKERNEL_VERSION as i, uvToolEnv as l, DEFAULT_KERNEL_PYTHON_VERSION as n, pipInstallArgs as o, DILL_VERSION as r, resolveKernelEnv as s, AUTO_PYTHON_SENTINEL as t, uvVenvArgs as u };
|
package/lib/webserver.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
-
import { WebServer } from "@deepseek-ai/dsh-host-webserver";
|
|
3
|
-
|
|
4
|
-
//#region src/webserver.d.ts
|
|
5
|
-
declare class BetterWebServer extends WebServer {
|
|
6
|
-
static inject: string[];
|
|
7
|
-
[Service.init](): Promise<void>;
|
|
8
|
-
}
|
|
9
|
-
//#endregion
|
|
10
|
-
export { BetterWebServer as default };
|