@tlockney/cdp-kernel 0.0.0-stage → 0.1.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/dist/kernel.js ADDED
@@ -0,0 +1,362 @@
1
+ // Jupyter message handling: shell, control, stdin and iopub.
2
+ //
3
+ // Ported from startJupyterKernel in Deno's cli/js/jupyter_kernel.js
4
+ // (v2.9.6), which is Copyright 2018-2026 the Deno authors, MIT license.
5
+ // The thirteen op_jupyter_* calls into Deno's REPL isolate are replaced by
6
+ // a Target driven over CDP (src/target.ts).
7
+ import { Buffer } from "node:buffer";
8
+ import process from "node:process";
9
+ import { randomUUID } from "node:crypto";
10
+ import { decodeMessage, encodeMessage, PROTOCOL_VERSION, verify } from "./wire.js";
11
+ import { PubSocket, RouterSocket, startHeartbeat } from "./zmtp.js";
12
+ import { Interrupted, Target } from "./target.js";
13
+ import { CellSyntaxError, objectLiteralCandidate, transformCell } from "./transform.js";
14
+ import { checkIsComplete, codePointToUtf16Index, exprAtCursor, utf16ToCodePointIndex } from "./text.js";
15
+ export function parseConnectionInfo(json) {
16
+ const raw = JSON.parse(json);
17
+ if (typeof raw !== "object" || raw === null)
18
+ throw new Error("connection file is not an object");
19
+ const info = raw;
20
+ for (const port of ["shell_port", "control_port", "stdin_port", "iopub_port", "hb_port"]) {
21
+ if (typeof info[port] !== "number")
22
+ throw new Error(`connection file: missing ${port}`);
23
+ }
24
+ if (info.transport !== undefined && info.transport !== "tcp") {
25
+ throw new Error(`unsupported transport ${info.transport}; only tcp is implemented`);
26
+ }
27
+ if (info.signature_scheme !== undefined && info.signature_scheme !== "hmac-sha256" && info.key) {
28
+ throw new Error(`unsupported signature scheme ${info.signature_scheme}`);
29
+ }
30
+ return {
31
+ ip: info.ip ?? "127.0.0.1",
32
+ key: info.key ?? "",
33
+ transport: "tcp",
34
+ signature_scheme: "hmac-sha256",
35
+ shell_port: info.shell_port,
36
+ control_port: info.control_port,
37
+ stdin_port: info.stdin_port,
38
+ iopub_port: info.iopub_port,
39
+ hb_port: info.hb_port,
40
+ };
41
+ }
42
+ export class Kernel {
43
+ #conn;
44
+ #runtime;
45
+ #denoCompat;
46
+ #session = randomUUID();
47
+ #shell;
48
+ #control;
49
+ #stdin;
50
+ #iopub;
51
+ #target = null;
52
+ #targetStarting = null;
53
+ #executionCount = 0;
54
+ #parent = {};
55
+ #allowStdin = false;
56
+ #executing = false;
57
+ #shuttingDown = false;
58
+ constructor(conn, runtime, options = {}) {
59
+ this.#conn = conn;
60
+ this.#runtime = runtime;
61
+ this.#denoCompat = options.denoCompat ?? false;
62
+ startHeartbeat(conn.hb_port, conn.ip);
63
+ this.#shell = new RouterSocket(conn.shell_port, conn.ip);
64
+ this.#control = new RouterSocket(conn.control_port, conn.ip);
65
+ this.#stdin = new RouterSocket(conn.stdin_port, conn.ip);
66
+ this.#iopub = new PubSocket(conn.iopub_port, conn.ip);
67
+ }
68
+ async run() {
69
+ // Start the runtime eagerly so the first cell doesn't pay for it.
70
+ this.#ensureTarget().catch((err) => this.#log(`target failed to start: ${err}`));
71
+ process.on("SIGINT", () => void this.#interrupt());
72
+ await Promise.all([this.#loop(this.#shell, (m, p) => this.#handleShell(m, p)),
73
+ this.#loop(this.#control, (m, p) => this.#handleControl(m, p))]);
74
+ }
75
+ #log(text) {
76
+ process.stderr.write(`[cdp-kernel] ${text}\n`);
77
+ }
78
+ // --- target lifecycle -----------------------------------------------------
79
+ #ensureTarget() {
80
+ if (this.#target)
81
+ return Promise.resolve(this.#target);
82
+ this.#targetStarting ??= Target.launch(this.#runtime, this.#targetEvents(), { denoCompat: this.#denoCompat }).then((target) => {
83
+ this.#target = target;
84
+ this.#targetStarting = null;
85
+ return target;
86
+ }, (err) => {
87
+ this.#targetStarting = null;
88
+ throw err;
89
+ });
90
+ return this.#targetStarting;
91
+ }
92
+ #targetEvents() {
93
+ return {
94
+ stream: (name, text) => this.#publish("stream", { name, text }),
95
+ message: (msg) => void this.#onTargetMessage(msg),
96
+ exit: (code) => {
97
+ this.#target = null;
98
+ if (!this.#shuttingDown) {
99
+ this.#publish("stream", { name: "stderr", text: `\n[cdp-kernel] ${this.#runtime} exited (${code}); restarting on next cell\n` });
100
+ }
101
+ },
102
+ };
103
+ }
104
+ async #onTargetMessage(msg) {
105
+ if (msg.kind === "iopub") {
106
+ this.#publish(msg.msg_type, msg.content, {
107
+ metadata: msg.metadata,
108
+ buffers: msg.buffers.map((b) => new Uint8Array(Buffer.from(b, "base64"))),
109
+ });
110
+ return;
111
+ }
112
+ let value = null;
113
+ try {
114
+ value = await this.#requestInput(msg.prompt, msg.password);
115
+ }
116
+ finally {
117
+ // Always answer exactly once so the target's blocking read returns.
118
+ this.#target?.replyInput(value);
119
+ }
120
+ }
121
+ async #interrupt() {
122
+ if (this.#target && this.#executing)
123
+ await this.#target.interrupt();
124
+ }
125
+ // --- messaging --------------------------------------------------------------
126
+ #encode(msgType, content, parentHeader, extra = {}) {
127
+ return encodeMessage(this.#session, this.#conn.key, msgType, content, { parentHeader, ...extra });
128
+ }
129
+ #publish(msgType, content, extra = {}) {
130
+ this.#iopub.send(this.#encode(msgType, content, extra.parent ?? this.#parent, extra));
131
+ }
132
+ #status(state, parent) {
133
+ this.#publish("status", { execution_state: state }, { parent });
134
+ }
135
+ #reply(socket, peer, request, msgType, content) {
136
+ socket.send(peer, this.#encode(msgType, content, request.header));
137
+ }
138
+ async #loop(socket, handle) {
139
+ while (!this.#shuttingDown) {
140
+ const { peer, frames } = await socket.recv();
141
+ try {
142
+ const msg = decodeMessage(frames);
143
+ // Unsigned or forged messages are dropped, as in Deno.
144
+ if (!verify(this.#conn.key, msg.signedParts, msg.sig))
145
+ continue;
146
+ await handle(msg, peer);
147
+ }
148
+ catch (err) {
149
+ this.#log(`error handling message: ${err instanceof Error ? err.stack : err}`);
150
+ }
151
+ }
152
+ }
153
+ #kernelInfo() {
154
+ const versions = this.#target?.versions;
155
+ return {
156
+ status: "ok",
157
+ protocol_version: PROTOCOL_VERSION,
158
+ implementation: "cdp-kernel",
159
+ implementation_version: "0.1.0",
160
+ language_info: {
161
+ name: "typescript",
162
+ version: "5",
163
+ mimetype: "text/x.typescript",
164
+ file_extension: ".ts",
165
+ pygments_lexer: "typescript",
166
+ codemirror_mode: "typescript",
167
+ nbconvert_exporter: "script",
168
+ },
169
+ banner: `cdp-kernel on ${this.#runtime} ${versions?.runtime ?? ""} (V8 ${versions?.v8 ?? "?"})`,
170
+ help_links: [],
171
+ debugger: false,
172
+ };
173
+ }
174
+ // --- control channel ------------------------------------------------------
175
+ async #handleControl(msg, peer) {
176
+ switch (msg.header.msg_type) {
177
+ case "kernel_info_request":
178
+ this.#reply(this.#control, peer, msg, "kernel_info_reply", this.#kernelInfo());
179
+ break;
180
+ case "interrupt_request":
181
+ await this.#interrupt();
182
+ this.#reply(this.#control, peer, msg, "interrupt_reply", { status: "ok" });
183
+ break;
184
+ case "shutdown_request": {
185
+ const restart = msg.content.restart === true;
186
+ this.#reply(this.#control, peer, msg, "shutdown_reply", { status: "ok", restart });
187
+ this.#shuttingDown = true;
188
+ this.#target?.kill();
189
+ // Frontends spawn a fresh kernel even on restart; exit after the
190
+ // reply has had a moment to flush (denoland/deno#20556).
191
+ setTimeout(() => process.exit(0), 100);
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ // --- shell channel ----------------------------------------------------------
197
+ async #handleShell(msg, peer) {
198
+ const type = msg.header.msg_type;
199
+ if (type === "execute_request")
200
+ return this.#execute(msg, peer);
201
+ this.#status("busy", msg.header);
202
+ try {
203
+ switch (type) {
204
+ case "kernel_info_request":
205
+ await this.#ensureTarget().catch(() => null);
206
+ this.#reply(this.#shell, peer, msg, "kernel_info_reply", this.#kernelInfo());
207
+ break;
208
+ case "complete_request":
209
+ this.#reply(this.#shell, peer, msg, "complete_reply", await this.#complete(msg));
210
+ break;
211
+ case "is_complete_request":
212
+ this.#reply(this.#shell, peer, msg, "is_complete_reply", checkIsComplete(String(msg.content.code ?? "")));
213
+ break;
214
+ case "inspect_request":
215
+ this.#reply(this.#shell, peer, msg, "inspect_reply", { status: "ok", found: false, data: {}, metadata: {} });
216
+ break;
217
+ case "history_request":
218
+ this.#reply(this.#shell, peer, msg, "history_reply", { status: "ok", history: [] });
219
+ break;
220
+ case "comm_info_request":
221
+ this.#reply(this.#shell, peer, msg, "comm_info_reply", { status: "ok", comms: {} });
222
+ break;
223
+ case "comm_open":
224
+ // No comm targets (widgets) yet: close it straight away, like Deno.
225
+ this.#publish("comm_close", { comm_id: msg.content.comm_id ?? "", data: {} }, { parent: msg.header });
226
+ break;
227
+ }
228
+ }
229
+ finally {
230
+ this.#status("idle", msg.header);
231
+ }
232
+ }
233
+ async #complete(msg) {
234
+ const code = String(msg.content.code ?? "");
235
+ const cursorCp = typeof msg.content.cursor_pos === "number"
236
+ ? msg.content.cursor_pos
237
+ : utf16ToCodePointIndex(code, code.length);
238
+ const cursor = codePointToUtf16Index(code, cursorCp);
239
+ const expr = exprAtCursor(code, cursor);
240
+ const target = await this.#ensureTarget();
241
+ let matches;
242
+ let start;
243
+ const dot = expr.lastIndexOf(".");
244
+ if (dot !== -1) {
245
+ const prefix = expr.slice(dot + 1);
246
+ matches = (await target.propertyNames(expr.slice(0, dot)))
247
+ .filter((name) => name.startsWith(prefix) && !name.startsWith("Symbol("));
248
+ start = cursor - prefix.length;
249
+ }
250
+ else {
251
+ const names = new Set([...(await target.propertyNames("globalThis")), ...(await target.lexicalNames())]);
252
+ matches = [...names].filter((name) => name.startsWith(expr) && !name.startsWith("__cdpk")).sort();
253
+ start = cursor - expr.length;
254
+ }
255
+ return {
256
+ status: "ok",
257
+ matches,
258
+ cursor_start: utf16ToCodePointIndex(code, start),
259
+ cursor_end: cursorCp,
260
+ metadata: {},
261
+ };
262
+ }
263
+ async #requestInput(prompt, password) {
264
+ const parent = this.#parent;
265
+ if (!this.#allowStdin || typeof parent.msg_id !== "string")
266
+ return null;
267
+ if (this.#stdin.peers.size === 0)
268
+ await new Promise((r) => setTimeout(r, 100));
269
+ if (this.#stdin.peers.size === 0)
270
+ return null;
271
+ this.#stdin.sendAll(this.#encode("input_request", { prompt, password }, parent));
272
+ for (;;) {
273
+ const { frames } = await this.#stdin.recv();
274
+ const reply = decodeMessage(frames);
275
+ if (!verify(this.#conn.key, reply.signedParts, reply.sig))
276
+ continue;
277
+ if (reply.header.msg_type === "input_reply") {
278
+ return typeof reply.content.value === "string" ? reply.content.value : null;
279
+ }
280
+ }
281
+ }
282
+ async #evaluateCell(target, code) {
283
+ // Object-literal wrapping (Deno's evaluate_line_with_object_wrapping):
284
+ // try `({ ... })` first and fall back to the code as written.
285
+ const wrapped = objectLiteralCandidate(code);
286
+ if (wrapped !== null) {
287
+ try {
288
+ const result = await target.evaluate(transformCell(wrapped));
289
+ if (!result.exceptionDetails)
290
+ return result;
291
+ }
292
+ catch (err) {
293
+ if (!(err instanceof CellSyntaxError))
294
+ throw err;
295
+ }
296
+ }
297
+ return target.evaluate(transformCell(code));
298
+ }
299
+ async #execute(msg, peer) {
300
+ const parent = msg.header;
301
+ const code = String(msg.content.code ?? "");
302
+ const silent = msg.content.silent === true;
303
+ if (!silent && msg.content.store_history !== false)
304
+ this.#executionCount++;
305
+ const count = this.#executionCount;
306
+ this.#parent = parent;
307
+ this.#allowStdin = msg.content.allow_stdin === true;
308
+ this.#status("busy", parent);
309
+ this.#publish("execute_input", { code, execution_count: count });
310
+ let error = null;
311
+ this.#executing = true;
312
+ try {
313
+ const target = await this.#ensureTarget();
314
+ const result = await this.#evaluateCell(target, code);
315
+ if (result.exceptionDetails) {
316
+ const exception = result.exceptionDetails.exception;
317
+ const described = exception
318
+ ? await target.describeError(exception)
319
+ : { name: "Error", message: result.exceptionDetails.text, stack: "" };
320
+ error = {
321
+ ename: described.name || "Error",
322
+ evalue: described.message,
323
+ traceback: (described.stack || `${described.name}: ${described.message}`).split("\n"),
324
+ };
325
+ }
326
+ else if (!silent && result.result.type !== "undefined") {
327
+ await target.broadcastResult(count, result.result);
328
+ }
329
+ await target.flush();
330
+ }
331
+ catch (err) {
332
+ if (err instanceof CellSyntaxError) {
333
+ // Deno reports these as an opaque "Execution failed".
334
+ error = { ename: "SyntaxError", evalue: err.message, traceback: [`SyntaxError: ${err.message}`] };
335
+ }
336
+ else if (err instanceof Interrupted) {
337
+ error = { ename: "Interrupted", evalue: "Execution interrupted", traceback: ["Execution interrupted"] };
338
+ await this.#target?.flush().catch(() => { });
339
+ }
340
+ else {
341
+ const message = err instanceof Error ? err.message : String(err);
342
+ error = { ename: "KernelError", evalue: message, traceback: [message] };
343
+ }
344
+ }
345
+ finally {
346
+ this.#executing = false;
347
+ }
348
+ if (error) {
349
+ this.#publish("error", error);
350
+ this.#reply(this.#shell, peer, msg, "execute_reply", { status: "error", execution_count: count, ...error });
351
+ }
352
+ else {
353
+ this.#reply(this.#shell, peer, msg, "execute_reply", {
354
+ status: "ok",
355
+ execution_count: count,
356
+ payload: [],
357
+ user_expressions: {},
358
+ });
359
+ }
360
+ this.#status("idle", parent);
361
+ }
362
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ // cdp-kernel CLI.
3
+ // cdp-kernel install [--runtime node|deno] [--deno-compat] [--name NAME] [--display NAME]
4
+ // cdp-kernel kernel --runtime node|deno [--deno-compat] --conn CONNECTION_FILE
5
+ // cdp-kernel version
6
+ //
7
+ // --deno-compat (Node only) adds a `Deno` namespace for notebooks written for
8
+ // `deno jupyter`; see runtime/deno-compat.mjs.
9
+ import process from "node:process";
10
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { homedir, platform } from "node:os";
12
+ import { join } from "node:path";
13
+ import { parseArgs } from "node:util";
14
+ import { Kernel, parseConnectionInfo } from "./kernel.js";
15
+ import { hostRuntime, kernelArgv, moduleOrigin, VERSION } from "./package.js";
16
+ function isRuntime(value) {
17
+ return value === "node" || value === "deno";
18
+ }
19
+ function kernelsDir() {
20
+ if (process.env.JUPYTER_DATA_DIR)
21
+ return join(process.env.JUPYTER_DATA_DIR, "kernels");
22
+ if (platform() === "darwin")
23
+ return join(homedir(), "Library", "Jupyter", "kernels");
24
+ if (platform() === "win32")
25
+ return join(process.env.APPDATA ?? homedir(), "jupyter", "kernels");
26
+ return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "jupyter", "kernels");
27
+ }
28
+ function install(runtime, denoCompat, name, display) {
29
+ const dir = join(kernelsDir(), name);
30
+ mkdirSync(dir, { recursive: true });
31
+ const spec = {
32
+ argv: kernelArgv(hostRuntime(), process.execPath, moduleOrigin(import.meta.url), [
33
+ "kernel",
34
+ "--runtime",
35
+ runtime,
36
+ ...(denoCompat ? ["--deno-compat"] : []),
37
+ "--conn",
38
+ "{connection_file}",
39
+ ]),
40
+ display_name: display,
41
+ language: "typescript",
42
+ // Interrupts arrive as interrupt_request on the control channel rather
43
+ // than as SIGINT to this process.
44
+ interrupt_mode: "message",
45
+ metadata: { runtime, denoCompat, version: VERSION },
46
+ };
47
+ writeFileSync(join(dir, "kernel.json"), JSON.stringify(spec, null, 2) + "\n");
48
+ console.log(`installed kernelspec ${name} -> ${dir}`);
49
+ }
50
+ const { positionals, values } = parseArgs({
51
+ allowPositionals: true,
52
+ options: {
53
+ runtime: { type: "string", default: "node" },
54
+ conn: { type: "string" },
55
+ name: { type: "string" },
56
+ display: { type: "string" },
57
+ "deno-compat": { type: "boolean", default: false },
58
+ },
59
+ });
60
+ const runtime = values.runtime;
61
+ if (!isRuntime(runtime)) {
62
+ console.error(`unknown runtime ${runtime}; expected node or deno`);
63
+ process.exit(2);
64
+ }
65
+ // Deno targets have the real namespace; the shim is for Node.
66
+ const denoCompat = values["deno-compat"] === true && runtime === "node";
67
+ switch (positionals[0]) {
68
+ case "install": {
69
+ const suffix = denoCompat ? "-deno" : "";
70
+ install(runtime, denoCompat, values.name ?? `cdp-${runtime}${suffix}`, values.display ?? `TypeScript (cdp-kernel, ${runtime}${denoCompat ? ", Deno compat" : ""})`);
71
+ break;
72
+ }
73
+ case "kernel": {
74
+ if (!values.conn) {
75
+ console.error("kernel: --conn CONNECTION_FILE is required");
76
+ process.exit(2);
77
+ }
78
+ const kernel = new Kernel(parseConnectionInfo(readFileSync(values.conn, "utf8")), runtime, { denoCompat });
79
+ await kernel.run();
80
+ break;
81
+ }
82
+ case "version":
83
+ console.log(VERSION);
84
+ break;
85
+ default:
86
+ console.error("usage: cdp-kernel install [--runtime node|deno] [--deno-compat] | kernel --runtime node|deno --conn FILE | version");
87
+ process.exit(2);
88
+ }
package/dist/mod.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * cdp-kernel: Deno's Jupyter kernel, extracted to run notebook cells in any
3
+ * runtime whose inspector speaks the Chrome DevTools Protocol (Node and
4
+ * Deno today).
5
+ *
6
+ * Most people want the CLI (`cdp-kernel install`). This module exposes
7
+ * the kernel for embedding: give it a parsed connection file and a target
8
+ * runtime, then await {@link Kernel.run}.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { readFileSync } from "node:fs";
13
+ * import { Kernel, parseConnectionInfo } from "@tlockney/cdp-kernel";
14
+ *
15
+ * const conn = parseConnectionInfo(readFileSync("kernel-123.json", "utf8"));
16
+ * await new Kernel(conn, "node").run();
17
+ * ```
18
+ *
19
+ * @module
20
+ */
21
+ export { type ConnectionInfo, Kernel, parseConnectionInfo } from "./kernel.ts";
22
+ export type { RuntimeName } from "./target.ts";
23
+ export { VERSION } from "./package.ts";
package/dist/mod.js ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * cdp-kernel: Deno's Jupyter kernel, extracted to run notebook cells in any
3
+ * runtime whose inspector speaks the Chrome DevTools Protocol (Node and
4
+ * Deno today).
5
+ *
6
+ * Most people want the CLI (`cdp-kernel install`). This module exposes
7
+ * the kernel for embedding: give it a parsed connection file and a target
8
+ * runtime, then await {@link Kernel.run}.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { readFileSync } from "node:fs";
13
+ * import { Kernel, parseConnectionInfo } from "@tlockney/cdp-kernel";
14
+ *
15
+ * const conn = parseConnectionInfo(readFileSync("kernel-123.json", "utf8"));
16
+ * await new Kernel(conn, "node").run();
17
+ * ```
18
+ *
19
+ * @module
20
+ */
21
+ export { Kernel, parseConnectionInfo } from "./kernel.js";
22
+ export { VERSION } from "./package.js";
@@ -0,0 +1,29 @@
1
+ /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
2
+ export declare const VERSION = "0.1.1";
3
+ export declare const RUNTIME_FILES: readonly ["bootstrap.mjs", "display.mjs", "node-hooks.mjs", "deno-compat.mjs"];
4
+ export type HostRuntime = "node" | "deno";
5
+ export declare function hostRuntime(): HostRuntime;
6
+ /** Where the CLI module was loaded from. */
7
+ export type Origin = {
8
+ kind: "file";
9
+ path: string;
10
+ } | {
11
+ kind: "jsr";
12
+ specifier: string;
13
+ } | {
14
+ kind: "remote";
15
+ url: string;
16
+ };
17
+ /** `moduleUrl` is the CLI's import.meta.url. */
18
+ export declare function moduleOrigin(moduleUrl: string): Origin;
19
+ /** The command a kernelspec runs to start this kernel. */
20
+ export declare function kernelArgv(host: HostRuntime, execPath: string, origin: Origin, args: string[]): string[];
21
+ export declare function cacheRoot(): string;
22
+ export type FetchText = (url: string) => Promise<string>;
23
+ /**
24
+ * A local directory holding runtime/*.mjs. `baseUrl` is the runtime/
25
+ * directory's URL relative to the calling module. A file: URL is used in
26
+ * place; anything else is copied once into a cache keyed by version and
27
+ * URL.
28
+ */
29
+ export declare function runtimeDir(baseUrl: string, cache?: string, get?: FetchText): Promise<string>;
@@ -0,0 +1,95 @@
1
+ // How cdp-kernel was installed, and what follows from it.
2
+ //
3
+ // It ships three ways: an npm package run by Node (compiled dist/), a JSR
4
+ // package run by Deno (TypeScript fetched from https://jsr.io), and a local
5
+ // checkout run by either. Two things depend on which:
6
+ // - the kernelspec argv that relaunches the kernel, and
7
+ // - where the target runtime loads runtime/*.mjs from. A Node target can't
8
+ // import its bootstrap from an https URL, so a JSR install copies those
9
+ // files into a local cache first.
10
+ import { createHash } from "node:crypto";
11
+ import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { join } from "node:path";
14
+ import process from "node:process";
15
+ import { fileURLToPath } from "node:url";
16
+ /** Kept equal to package.json and deno.jsonc by test/package.test.ts. */
17
+ export const VERSION = "0.1.1";
18
+ export const RUNTIME_FILES = ["bootstrap.mjs", "display.mjs", "node-hooks.mjs", "deno-compat.mjs"];
19
+ export function hostRuntime() {
20
+ return process.versions.deno === undefined ? "node" : "deno";
21
+ }
22
+ const JSR_URL = /^https:\/\/jsr\.io\/(@[^/]+\/[^/]+)\/([^/]+)\//;
23
+ /** `moduleUrl` is the CLI's import.meta.url. */
24
+ export function moduleOrigin(moduleUrl) {
25
+ if (moduleUrl.startsWith("file:"))
26
+ return { kind: "file", path: fileURLToPath(moduleUrl) };
27
+ const jsr = JSR_URL.exec(moduleUrl);
28
+ // Pin the exact version so an installed kernelspec keeps running the
29
+ // release it was installed from.
30
+ if (jsr)
31
+ return { kind: "jsr", specifier: `jsr:${jsr[1]}@${jsr[2]}/cli` };
32
+ return { kind: "remote", url: moduleUrl };
33
+ }
34
+ /** The command a kernelspec runs to start this kernel. */
35
+ export function kernelArgv(host, execPath, origin, args) {
36
+ if (origin.kind === "remote") {
37
+ throw new Error(`cdp-kernel was loaded from ${origin.url}; install it from npm or JSR, or run a local checkout`);
38
+ }
39
+ if (host === "node") {
40
+ if (origin.kind !== "file")
41
+ throw new Error("a JSR install must be run with Deno");
42
+ return [execPath, origin.path, ...args];
43
+ }
44
+ // Deno: a checkout needs its deno.jsonc import map; a JSR package brings
45
+ // its own, so skip whatever config the notebook's directory has.
46
+ const flags = origin.kind === "jsr" ? ["--no-config", "--no-lock"] : ["--no-lock"];
47
+ const entry = origin.kind === "jsr" ? origin.specifier : origin.path;
48
+ return [execPath, "run", "--allow-all", ...flags, entry, ...args];
49
+ }
50
+ export function cacheRoot() {
51
+ const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
52
+ return join(base, "cdp-kernel");
53
+ }
54
+ async function fetchText(url) {
55
+ const res = await fetch(url);
56
+ if (!res.ok)
57
+ throw new Error(`fetching ${url}: ${res.status} ${res.statusText}`);
58
+ return await res.text();
59
+ }
60
+ /**
61
+ * A local directory holding runtime/*.mjs. `baseUrl` is the runtime/
62
+ * directory's URL relative to the calling module. A file: URL is used in
63
+ * place; anything else is copied once into a cache keyed by version and
64
+ * URL.
65
+ */
66
+ export async function runtimeDir(baseUrl, cache = cacheRoot(), get = fetchText) {
67
+ if (baseUrl.startsWith("file:"))
68
+ return fileURLToPath(baseUrl);
69
+ const key = createHash("sha256").update(baseUrl).digest("hex").slice(0, 12);
70
+ const dir = join(cache, "runtime", `${VERSION}-${key}`);
71
+ if (RUNTIME_FILES.every((name) => existsSync(join(dir, name))))
72
+ return dir;
73
+ // Fill a sibling directory, then rename, so a half-copied cache is never
74
+ // picked up by the check above.
75
+ const staging = `${dir}.${process.pid}.tmp`;
76
+ mkdirSync(staging, { recursive: true });
77
+ try {
78
+ for (const name of RUNTIME_FILES) {
79
+ writeFileSync(join(staging, name), await get(new URL(name, baseUrl).href));
80
+ }
81
+ }
82
+ catch (err) {
83
+ rmSync(staging, { recursive: true, force: true });
84
+ throw err;
85
+ }
86
+ try {
87
+ renameSync(staging, dir);
88
+ }
89
+ catch (err) {
90
+ // Another kernel finished first; theirs is complete.
91
+ if (!RUNTIME_FILES.every((name) => existsSync(join(dir, name))))
92
+ throw err;
93
+ }
94
+ return dir;
95
+ }