@sharpe-jupyter/connect 0.3.3 → 0.4.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/dist/index.js +271 -74
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -130,6 +130,61 @@ async function ensureRipgrep() {
|
|
|
130
130
|
return { path: binPath, downloaded: true };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// src/uv.ts
|
|
134
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, chmodSync as chmodSync3, unlinkSync as unlinkSync3, createWriteStream as createWriteStream3 } from "fs";
|
|
135
|
+
import { join as join3 } from "path";
|
|
136
|
+
import { homedir as homedir3, arch as arch3, platform as platform3 } from "os";
|
|
137
|
+
import { pipeline as pipeline3 } from "stream/promises";
|
|
138
|
+
import { Readable as Readable3 } from "stream";
|
|
139
|
+
import { execSync as execSync3 } from "child_process";
|
|
140
|
+
var UV_VERSION = "0.10.4";
|
|
141
|
+
function getAssetName2() {
|
|
142
|
+
const os = platform3();
|
|
143
|
+
const cpu = arch3();
|
|
144
|
+
if (os === "darwin") {
|
|
145
|
+
const triple = cpu === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
|
|
146
|
+
return `uv-${triple}.tar.gz`;
|
|
147
|
+
}
|
|
148
|
+
if (os === "linux") {
|
|
149
|
+
const triple = cpu === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu";
|
|
150
|
+
return `uv-${triple}.tar.gz`;
|
|
151
|
+
}
|
|
152
|
+
throw new Error(`Unsupported platform: ${os}/${cpu}`);
|
|
153
|
+
}
|
|
154
|
+
function getBinDir3() {
|
|
155
|
+
return join3(homedir3(), ".sharpe", "bin");
|
|
156
|
+
}
|
|
157
|
+
function getUvPath() {
|
|
158
|
+
return join3(getBinDir3(), "uv");
|
|
159
|
+
}
|
|
160
|
+
async function ensureUv() {
|
|
161
|
+
const binPath = getUvPath();
|
|
162
|
+
if (existsSync3(binPath)) {
|
|
163
|
+
return { path: binPath, downloaded: false };
|
|
164
|
+
}
|
|
165
|
+
const binDir = getBinDir3();
|
|
166
|
+
mkdirSync3(binDir, { recursive: true });
|
|
167
|
+
const assetName = getAssetName2();
|
|
168
|
+
const url = `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/${assetName}`;
|
|
169
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
170
|
+
if (!response.ok || !response.body) {
|
|
171
|
+
throw new Error(`Failed to download uv: ${response.status} ${response.statusText}`);
|
|
172
|
+
}
|
|
173
|
+
const tgzPath = join3(binDir, assetName);
|
|
174
|
+
const fileStream = createWriteStream3(tgzPath);
|
|
175
|
+
await pipeline3(
|
|
176
|
+
Readable3.fromWeb(response.body),
|
|
177
|
+
fileStream
|
|
178
|
+
);
|
|
179
|
+
const dirName = assetName.replace(".tar.gz", "");
|
|
180
|
+
execSync3(`tar -xzf "${tgzPath}" -C "${binDir}" "${dirName}/uv"`, { stdio: "ignore" });
|
|
181
|
+
execSync3(`mv "${join3(binDir, dirName, "uv")}" "${binPath}"`, { stdio: "ignore" });
|
|
182
|
+
unlinkSync3(tgzPath);
|
|
183
|
+
execSync3(`rm -rf "${join3(binDir, dirName)}"`, { stdio: "ignore" });
|
|
184
|
+
chmodSync3(binPath, 493);
|
|
185
|
+
return { path: binPath, downloaded: true };
|
|
186
|
+
}
|
|
187
|
+
|
|
133
188
|
// src/health.ts
|
|
134
189
|
async function checkJupyterHealth(port2) {
|
|
135
190
|
try {
|
|
@@ -143,22 +198,22 @@ async function checkJupyterHealth(port2) {
|
|
|
143
198
|
}
|
|
144
199
|
|
|
145
200
|
// src/jupyter.ts
|
|
146
|
-
import { execSync as
|
|
147
|
-
import { existsSync as
|
|
148
|
-
import { join as
|
|
149
|
-
import { homedir as
|
|
150
|
-
var SHARPE_DIR =
|
|
151
|
-
var VENV_DIR =
|
|
152
|
-
var SHARPE_BIN_DIR =
|
|
153
|
-
var IS_WIN =
|
|
201
|
+
import { execSync as execSync4, spawn } from "child_process";
|
|
202
|
+
import { existsSync as existsSync4 } from "fs";
|
|
203
|
+
import { join as join4 } from "path";
|
|
204
|
+
import { homedir as homedir4, platform as platform4 } from "os";
|
|
205
|
+
var SHARPE_DIR = join4(homedir4(), ".sharpe");
|
|
206
|
+
var VENV_DIR = join4(SHARPE_DIR, "venv");
|
|
207
|
+
var SHARPE_BIN_DIR = join4(SHARPE_DIR, "bin");
|
|
208
|
+
var IS_WIN = platform4() === "win32";
|
|
154
209
|
var BIN_DIR = IS_WIN ? "Scripts" : "bin";
|
|
155
210
|
function venvBin(name) {
|
|
156
|
-
return
|
|
211
|
+
return join4(VENV_DIR, BIN_DIR, IS_WIN ? `${name}.exe` : name);
|
|
157
212
|
}
|
|
158
213
|
function findPython() {
|
|
159
214
|
for (const cmd of ["python3", "python"]) {
|
|
160
215
|
try {
|
|
161
|
-
const version =
|
|
216
|
+
const version = execSync4(`${cmd} --version`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
162
217
|
if (version.startsWith("Python 3")) return cmd;
|
|
163
218
|
} catch {
|
|
164
219
|
}
|
|
@@ -166,10 +221,10 @@ function findPython() {
|
|
|
166
221
|
return null;
|
|
167
222
|
}
|
|
168
223
|
function venvExists() {
|
|
169
|
-
return
|
|
224
|
+
return existsSync4(venvBin("python"));
|
|
170
225
|
}
|
|
171
226
|
function createVenv(pythonCmd) {
|
|
172
|
-
|
|
227
|
+
execSync4(`${pythonCmd} -m venv "${VENV_DIR}"`, { stdio: "ignore" });
|
|
173
228
|
}
|
|
174
229
|
function installPackages(packages) {
|
|
175
230
|
return spawn(venvBin("pip"), ["install", "--quiet", ...packages], {
|
|
@@ -194,6 +249,56 @@ function startJupyter(port2) {
|
|
|
194
249
|
{ stdio: ["ignore", "pipe", "pipe"], env }
|
|
195
250
|
);
|
|
196
251
|
}
|
|
252
|
+
function isUvProject() {
|
|
253
|
+
return existsSync4(join4(process.cwd(), "uv.lock"));
|
|
254
|
+
}
|
|
255
|
+
function startJupyterWithUv(port2, uvBin) {
|
|
256
|
+
const env = { ...process.env, PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}` };
|
|
257
|
+
return spawn(
|
|
258
|
+
uvBin,
|
|
259
|
+
[
|
|
260
|
+
"run",
|
|
261
|
+
"--with",
|
|
262
|
+
"jupyter",
|
|
263
|
+
"--with",
|
|
264
|
+
"jupyter-resource-usage",
|
|
265
|
+
"--with",
|
|
266
|
+
"sharpe-log-handler>=0.3.0",
|
|
267
|
+
"jupyter",
|
|
268
|
+
"notebook",
|
|
269
|
+
"--port",
|
|
270
|
+
String(port2),
|
|
271
|
+
"--no-browser",
|
|
272
|
+
"--ServerApp.token=",
|
|
273
|
+
"--ServerApp.password=",
|
|
274
|
+
"--ServerApp.allow_remote_access=True",
|
|
275
|
+
"--ServerApp.disable_check_xsrf=True",
|
|
276
|
+
`--ServerApp.root_dir=${process.cwd()}`
|
|
277
|
+
],
|
|
278
|
+
{ stdio: ["ignore", "pipe", "pipe"], env }
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
function detectProjectDeps() {
|
|
282
|
+
const cwd = process.cwd();
|
|
283
|
+
const reqTxt = join4(cwd, "requirements.txt");
|
|
284
|
+
if (existsSync4(reqTxt)) {
|
|
285
|
+
return { file: "requirements.txt", installArgs: ["-r", reqTxt] };
|
|
286
|
+
}
|
|
287
|
+
const pyproject = join4(cwd, "pyproject.toml");
|
|
288
|
+
if (existsSync4(pyproject)) {
|
|
289
|
+
return { file: "pyproject.toml", installArgs: ["-e", cwd] };
|
|
290
|
+
}
|
|
291
|
+
const setupPy = join4(cwd, "setup.py");
|
|
292
|
+
if (existsSync4(setupPy)) {
|
|
293
|
+
return { file: "setup.py", installArgs: ["-e", cwd] };
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
function installProjectDeps(deps) {
|
|
298
|
+
return spawn(venvBin("pip"), ["install", ...deps.installArgs], {
|
|
299
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
300
|
+
});
|
|
301
|
+
}
|
|
197
302
|
function parseJupyterLine(line) {
|
|
198
303
|
if (/Saving file at\s+(\S+)/.test(line)) {
|
|
199
304
|
const match = line.match(/Saving file at\s+(\S+)/);
|
|
@@ -268,6 +373,9 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
268
373
|
const [error, setError] = useState(null);
|
|
269
374
|
const [tokenInput, setTokenInput] = useState("");
|
|
270
375
|
const [activeToken, setActiveToken] = useState(connectionCode2);
|
|
376
|
+
const [depsPrompt, setDepsPrompt] = useState(null);
|
|
377
|
+
const [depsStatus, setDepsStatus] = useState(null);
|
|
378
|
+
const depsResolverRef = useRef(null);
|
|
271
379
|
const didWeSpawnJupyter = useRef(false);
|
|
272
380
|
const jupyterProc = useRef(null);
|
|
273
381
|
const tunnelProc = useRef(null);
|
|
@@ -300,6 +408,21 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
300
408
|
},
|
|
301
409
|
{ isActive: phase === "prompting" }
|
|
302
410
|
);
|
|
411
|
+
useInput(
|
|
412
|
+
(input, key) => {
|
|
413
|
+
if (key.return || input === "y" || input === "Y") {
|
|
414
|
+
depsResolverRef.current?.(true);
|
|
415
|
+
depsResolverRef.current = null;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (input === "n" || input === "N") {
|
|
419
|
+
depsResolverRef.current?.(false);
|
|
420
|
+
depsResolverRef.current = null;
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{ isActive: depsPrompt !== null }
|
|
425
|
+
);
|
|
303
426
|
const restartJupyter = useCallback(() => {
|
|
304
427
|
if (jupyterProc.current) {
|
|
305
428
|
jupyterProc.current.kill("SIGTERM");
|
|
@@ -307,7 +430,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
307
430
|
}
|
|
308
431
|
setJupyterRunning(false);
|
|
309
432
|
pushEvent("Restarting notebook server...");
|
|
310
|
-
const proc = startJupyter(port2);
|
|
433
|
+
const proc = isUvProject() ? startJupyterWithUv(port2, getUvPath()) : startJupyter(port2);
|
|
311
434
|
jupyterProc.current = proc;
|
|
312
435
|
didWeSpawnJupyter.current = true;
|
|
313
436
|
let stderrBuf = "";
|
|
@@ -366,6 +489,10 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
366
489
|
setStartedAt(null);
|
|
367
490
|
setJupyterRunning(false);
|
|
368
491
|
setTunnelConnected(false);
|
|
492
|
+
setDepsPrompt(null);
|
|
493
|
+
setDepsStatus(null);
|
|
494
|
+
depsResolverRef.current?.(false);
|
|
495
|
+
depsResolverRef.current = null;
|
|
369
496
|
}, []);
|
|
370
497
|
useInput(
|
|
371
498
|
(_input, key) => {
|
|
@@ -380,7 +507,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
380
507
|
let cancelled = false;
|
|
381
508
|
async function run() {
|
|
382
509
|
try {
|
|
383
|
-
await Promise.all([ensureCloudflared(), ensureRipgrep()]);
|
|
510
|
+
await Promise.all([ensureCloudflared(), ensureRipgrep(), ensureUv()]);
|
|
384
511
|
} catch {
|
|
385
512
|
if (cancelled) return;
|
|
386
513
|
setSetupStep("error");
|
|
@@ -400,6 +527,22 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
400
527
|
setJupyterRunning(true);
|
|
401
528
|
didWeSpawnJupyter.current = false;
|
|
402
529
|
pushEvent("Existing notebook server detected");
|
|
530
|
+
} else if (isUvProject()) {
|
|
531
|
+
pushEvent("Installing dependencies and starting notebook...");
|
|
532
|
+
const uvPath = getUvPath();
|
|
533
|
+
const proc = startJupyterWithUv(port2, uvPath);
|
|
534
|
+
jupyterProc.current = proc;
|
|
535
|
+
didWeSpawnJupyter.current = true;
|
|
536
|
+
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
|
|
537
|
+
if (cancelled) return;
|
|
538
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
539
|
+
if (errorInfo) {
|
|
540
|
+
setJupyterStep("error");
|
|
541
|
+
setError(errorInfo);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
setJupyterRunning(true);
|
|
545
|
+
pushEvent("Notebook server started");
|
|
403
546
|
} else {
|
|
404
547
|
const pythonCmd = findPython();
|
|
405
548
|
if (!pythonCmd) {
|
|
@@ -427,7 +570,11 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
427
570
|
if (cancelled) return;
|
|
428
571
|
pushEvent("Installing Jupyter...");
|
|
429
572
|
const installResult = await new Promise((resolve) => {
|
|
430
|
-
const proc2 = installPackages([
|
|
573
|
+
const proc2 = installPackages([
|
|
574
|
+
"jupyter",
|
|
575
|
+
"jupyter-resource-usage",
|
|
576
|
+
"sharpe-log-handler>=0.3.0"
|
|
577
|
+
]);
|
|
431
578
|
proc2.on("error", (err) => resolve(err.message));
|
|
432
579
|
proc2.on("exit", (code) => resolve(code === 0 ? null : "install-failed"));
|
|
433
580
|
});
|
|
@@ -441,69 +588,43 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
441
588
|
return;
|
|
442
589
|
}
|
|
443
590
|
pushEvent("Jupyter installed");
|
|
591
|
+
const detected = detectProjectDeps();
|
|
592
|
+
if (detected && !cancelled) {
|
|
593
|
+
setDepsPrompt({ file: detected.file });
|
|
594
|
+
const shouldInstall = await new Promise((resolve) => {
|
|
595
|
+
depsResolverRef.current = resolve;
|
|
596
|
+
});
|
|
597
|
+
setDepsPrompt(null);
|
|
598
|
+
if (cancelled) return;
|
|
599
|
+
if (shouldInstall) {
|
|
600
|
+
setDepsStatus(`Installing from ${detected.file} using pip...`);
|
|
601
|
+
pushEvent(`Installing from ${detected.file}...`);
|
|
602
|
+
const depsResult = await new Promise((resolve) => {
|
|
603
|
+
const depsProc = installProjectDeps(detected);
|
|
604
|
+
depsProc.on("error", (err) => resolve(err.message));
|
|
605
|
+
depsProc.on("exit", (code) => resolve(code === 0 ? null : "deps-install-failed"));
|
|
606
|
+
});
|
|
607
|
+
setDepsStatus(null);
|
|
608
|
+
if (cancelled) return;
|
|
609
|
+
if (depsResult) {
|
|
610
|
+
pushEvent(`Warning: failed to install from ${detected.file} (continuing anyway)`);
|
|
611
|
+
} else {
|
|
612
|
+
pushEvent(`Installed dependencies from ${detected.file}`);
|
|
613
|
+
}
|
|
614
|
+
} else {
|
|
615
|
+
pushEvent("Skipped dependency installation");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (cancelled) return;
|
|
444
619
|
const proc = startJupyter(port2);
|
|
445
620
|
jupyterProc.current = proc;
|
|
446
621
|
didWeSpawnJupyter.current = true;
|
|
447
|
-
const spawnError = await
|
|
448
|
-
proc.on("error", (err) => resolve(err.message));
|
|
449
|
-
let stderrBuf = "";
|
|
450
|
-
proc.stderr?.on("data", (data) => {
|
|
451
|
-
stderrBuf += data.toString();
|
|
452
|
-
const lines = stderrBuf.split("\n");
|
|
453
|
-
stderrBuf = lines.pop() ?? "";
|
|
454
|
-
for (const line of lines) {
|
|
455
|
-
if (line.includes("Address already in use")) {
|
|
456
|
-
resolve("port-conflict");
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
const msg = parseJupyterLine(line);
|
|
460
|
-
if (msg) pushEvent(msg);
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
let attempts = 0;
|
|
464
|
-
const poll = setInterval(async () => {
|
|
465
|
-
if (cancelled) {
|
|
466
|
-
clearInterval(poll);
|
|
467
|
-
resolve("cancelled");
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
attempts++;
|
|
471
|
-
const healthy = await checkJupyterHealth(port2);
|
|
472
|
-
if (healthy) {
|
|
473
|
-
clearInterval(poll);
|
|
474
|
-
resolve(null);
|
|
475
|
-
} else if (attempts >= 20) {
|
|
476
|
-
clearInterval(poll);
|
|
477
|
-
resolve("timeout");
|
|
478
|
-
}
|
|
479
|
-
}, 1e3);
|
|
480
|
-
});
|
|
622
|
+
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
|
|
481
623
|
if (cancelled) return;
|
|
482
|
-
|
|
624
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
625
|
+
if (errorInfo) {
|
|
483
626
|
setJupyterStep("error");
|
|
484
|
-
setError(
|
|
485
|
-
message: "Could not start the notebook server.",
|
|
486
|
-
hint: `Port ${port2} is already in use.
|
|
487
|
-
Close whatever is using it and try again, or use a different port:
|
|
488
|
-
|
|
489
|
-
npx @sharpe-jupyter/connect CODE --port ${port2 + 1}`
|
|
490
|
-
});
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
if (spawnError === "timeout") {
|
|
494
|
-
setJupyterStep("error");
|
|
495
|
-
setError({
|
|
496
|
-
message: "The notebook server isn't responding.",
|
|
497
|
-
hint: `Try restarting or check if port ${port2} is available.`
|
|
498
|
-
});
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
if (spawnError && spawnError !== "cancelled") {
|
|
502
|
-
setJupyterStep("error");
|
|
503
|
-
setError({
|
|
504
|
-
message: "Could not start the notebook server.",
|
|
505
|
-
hint: "Something went wrong starting Jupyter. Try running again."
|
|
506
|
-
});
|
|
627
|
+
setError(errorInfo);
|
|
507
628
|
return;
|
|
508
629
|
}
|
|
509
630
|
setJupyterRunning(true);
|
|
@@ -601,6 +722,8 @@ Close whatever is using it and try again, or use a different port:
|
|
|
601
722
|
cancelled = true;
|
|
602
723
|
cleanedUp.current = true;
|
|
603
724
|
setPhase("shutting-down");
|
|
725
|
+
depsResolverRef.current?.(false);
|
|
726
|
+
depsResolverRef.current = null;
|
|
604
727
|
if (didWeSpawnJupyter.current && jupyterProc.current) {
|
|
605
728
|
jupyterProc.current.kill("SIGTERM");
|
|
606
729
|
}
|
|
@@ -616,6 +739,8 @@ Close whatever is using it and try again, or use a different port:
|
|
|
616
739
|
process.on("SIGTERM", cleanup);
|
|
617
740
|
return () => {
|
|
618
741
|
cancelled = true;
|
|
742
|
+
depsResolverRef.current?.(false);
|
|
743
|
+
depsResolverRef.current = null;
|
|
619
744
|
process.removeListener("SIGINT", cleanup);
|
|
620
745
|
process.removeListener("SIGTERM", cleanup);
|
|
621
746
|
};
|
|
@@ -683,7 +808,19 @@ Close whatever is using it and try again, or use a different port:
|
|
|
683
808
|
}
|
|
684
809
|
),
|
|
685
810
|
/* @__PURE__ */ jsx(Step, { status: tunnelStep, label: "Connecting to Sharpe" })
|
|
686
|
-
] })
|
|
811
|
+
] }),
|
|
812
|
+
depsPrompt && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
813
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
814
|
+
"Found ",
|
|
815
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: depsPrompt.file }),
|
|
816
|
+
" in this directory."
|
|
817
|
+
] }),
|
|
818
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
819
|
+
"Install dependencies? ",
|
|
820
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[Y/n]" })
|
|
821
|
+
] })
|
|
822
|
+
] }),
|
|
823
|
+
depsStatus && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: depsStatus }) })
|
|
687
824
|
] });
|
|
688
825
|
}
|
|
689
826
|
const footerText = !jupyterRunning ? "Press R to restart notebook, or Ctrl+C to quit." : "Press Ctrl+C when you're done.";
|
|
@@ -757,6 +894,66 @@ function StatusBadge({ status }) {
|
|
|
757
894
|
] });
|
|
758
895
|
}
|
|
759
896
|
}
|
|
897
|
+
function waitForJupyterHealthy(proc, port2, pushEvent, isCancelled) {
|
|
898
|
+
return new Promise((resolve) => {
|
|
899
|
+
proc.on("error", (err) => resolve(err.message));
|
|
900
|
+
let stderrBuf = "";
|
|
901
|
+
proc.stderr?.on("data", (data) => {
|
|
902
|
+
stderrBuf += data.toString();
|
|
903
|
+
const lines = stderrBuf.split("\n");
|
|
904
|
+
stderrBuf = lines.pop() ?? "";
|
|
905
|
+
for (const line of lines) {
|
|
906
|
+
if (line.includes("Address already in use")) {
|
|
907
|
+
resolve("port-conflict");
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const msg = parseJupyterLine(line);
|
|
911
|
+
if (msg) pushEvent(msg);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
let attempts = 0;
|
|
915
|
+
const poll = setInterval(async () => {
|
|
916
|
+
if (isCancelled()) {
|
|
917
|
+
clearInterval(poll);
|
|
918
|
+
resolve("cancelled");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
attempts++;
|
|
922
|
+
const healthy = await checkJupyterHealth(port2);
|
|
923
|
+
if (healthy) {
|
|
924
|
+
clearInterval(poll);
|
|
925
|
+
resolve(null);
|
|
926
|
+
} else if (attempts >= 20) {
|
|
927
|
+
clearInterval(poll);
|
|
928
|
+
resolve("timeout");
|
|
929
|
+
}
|
|
930
|
+
}, 1e3);
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
function handleJupyterSpawnError(spawnError, port2) {
|
|
934
|
+
if (spawnError === "port-conflict") {
|
|
935
|
+
return {
|
|
936
|
+
message: "Could not start the notebook server.",
|
|
937
|
+
hint: `Port ${port2} is already in use.
|
|
938
|
+
Close whatever is using it and try again, or use a different port:
|
|
939
|
+
|
|
940
|
+
npx @sharpe-jupyter/connect CODE --port ${port2 + 1}`
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
if (spawnError === "timeout") {
|
|
944
|
+
return {
|
|
945
|
+
message: "The notebook server isn't responding.",
|
|
946
|
+
hint: `Try restarting or check if port ${port2} is available.`
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
if (spawnError && spawnError !== "cancelled") {
|
|
950
|
+
return {
|
|
951
|
+
message: "Could not start the notebook server.",
|
|
952
|
+
hint: "Something went wrong starting Jupyter. Try running again."
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
760
957
|
|
|
761
958
|
// src/index.tsx
|
|
762
959
|
import { jsx as jsx2 } from "react/jsx-runtime";
|