@sharpe-jupyter/connect 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +102 -42
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -232,7 +232,11 @@ function installPackages(packages) {
232
232
  });
233
233
  }
234
234
  function startJupyter(port2) {
235
- const env = { ...process.env, PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}` };
235
+ const env = {
236
+ ...process.env,
237
+ PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}`,
238
+ SHARPE_ENV_DIR: process.cwd()
239
+ };
236
240
  return spawn(
237
241
  venvBin("jupyter"),
238
242
  [
@@ -254,7 +258,11 @@ function isUvProject() {
254
258
  return existsSync4(join4(process.cwd(), "uv.lock"));
255
259
  }
256
260
  function startJupyterWithUv(port2, uvBin) {
257
- const env = { ...process.env, PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}` };
261
+ const env = {
262
+ ...process.env,
263
+ PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}`,
264
+ SHARPE_ENV_DIR: process.cwd()
265
+ };
258
266
  return spawn(
259
267
  uvBin,
260
268
  [
@@ -325,6 +333,7 @@ function parseJupyterLine(line) {
325
333
  // src/app.tsx
326
334
  import { jsx, jsxs } from "react/jsx-runtime";
327
335
  var MAX_EVENTS = 10;
336
+ var MAX_PROCESS_LOGS = 12;
328
337
  var LOGO = ` ..
329
338
  .:
330
339
  .@.
@@ -378,11 +387,23 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
378
387
  const [depsPrompt, setDepsPrompt] = useState(null);
379
388
  const [depsStatus, setDepsStatus] = useState(null);
380
389
  const depsResolverRef = useRef(null);
390
+ const [processLogs, setProcessLogs] = useState([]);
381
391
  const didWeSpawnJupyter = useRef(false);
382
392
  const jupyterProc = useRef(null);
383
393
  const tunnelProc = useRef(null);
384
394
  const cleanedUp = useRef(false);
395
+ const processLogsRef = useRef([]);
385
396
  const uptime = useUptime(startedAt);
397
+ const pushProcessLog = useCallback((line) => {
398
+ const trimmed = line.trimEnd();
399
+ if (!trimmed) return;
400
+ setProcessLogs((prev) => {
401
+ const next = [...prev, trimmed];
402
+ const sliced = next.length > MAX_PROCESS_LOGS ? next.slice(-MAX_PROCESS_LOGS) : next;
403
+ processLogsRef.current = sliced;
404
+ return sliced;
405
+ });
406
+ }, []);
386
407
  const pushEvent = useCallback((message) => {
387
408
  setEvents((prev) => {
388
409
  const next = [...prev, { time: formatTime(), message }];
@@ -431,20 +452,28 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
431
452
  jupyterProc.current = null;
432
453
  }
433
454
  setJupyterRunning(false);
455
+ setProcessLogs([]);
456
+ processLogsRef.current = [];
434
457
  pushEvent("Restarting notebook server...");
435
458
  const proc = isUvProject() ? startJupyterWithUv(port2, getUvPath()) : startJupyter(port2);
436
459
  jupyterProc.current = proc;
437
460
  didWeSpawnJupyter.current = true;
438
- let stderrBuf = "";
439
- proc.stderr?.on("data", (data) => {
440
- stderrBuf += data.toString();
441
- const lines = stderrBuf.split("\n");
442
- stderrBuf = lines.pop() ?? "";
443
- for (const line of lines) {
444
- const msg = parseJupyterLine(line);
445
- if (msg) pushEvent(msg);
446
- }
447
- });
461
+ const pipeStream = (stream) => {
462
+ if (!stream) return;
463
+ let buf = "";
464
+ stream.on("data", (data) => {
465
+ buf += data.toString();
466
+ const lines = buf.split("\n");
467
+ buf = lines.pop() ?? "";
468
+ for (const line of lines) {
469
+ pushProcessLog(line);
470
+ const msg = parseJupyterLine(line);
471
+ if (msg) pushEvent(msg);
472
+ }
473
+ });
474
+ };
475
+ pipeStream(proc.stderr);
476
+ pipeStream(proc.stdout);
448
477
  proc.on("error", () => {
449
478
  setJupyterRunning(false);
450
479
  pushEvent("Notebook server failed to start");
@@ -463,12 +492,12 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
463
492
  clearInterval(poll);
464
493
  setJupyterRunning(true);
465
494
  pushEvent("Notebook server started");
466
- } else if (attempts >= 20) {
495
+ } else if (attempts >= 40) {
467
496
  clearInterval(poll);
468
497
  pushEvent("Notebook server not responding");
469
498
  }
470
499
  }, 1e3);
471
- }, [port2, pushEvent]);
500
+ }, [port2, pushEvent, pushProcessLog]);
472
501
  useInput(
473
502
  (_input, key) => {
474
503
  if (_input === "r" || _input === "R") {
@@ -493,6 +522,8 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
493
522
  setTunnelConnected(false);
494
523
  setDepsPrompt(null);
495
524
  setDepsStatus(null);
525
+ setProcessLogs([]);
526
+ processLogsRef.current = [];
496
527
  depsResolverRef.current?.(false);
497
528
  depsResolverRef.current = null;
498
529
  }, []);
@@ -535,9 +566,9 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
535
566
  const proc = startJupyterWithUv(port2, uvPath);
536
567
  jupyterProc.current = proc;
537
568
  didWeSpawnJupyter.current = true;
538
- const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
569
+ const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, pushProcessLog, () => cancelled);
539
570
  if (cancelled) return;
540
- const errorInfo = handleJupyterSpawnError(spawnError, port2);
571
+ const errorInfo = handleJupyterSpawnError(spawnError, port2, processLogsRef.current);
541
572
  if (errorInfo) {
542
573
  setJupyterStep("error");
543
574
  setError(errorInfo);
@@ -621,9 +652,9 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
621
652
  const proc = startJupyter(port2);
622
653
  jupyterProc.current = proc;
623
654
  didWeSpawnJupyter.current = true;
624
- const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
655
+ const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, pushProcessLog, () => cancelled);
625
656
  if (cancelled) return;
626
- const errorInfo = handleJupyterSpawnError(spawnError, port2);
657
+ const errorInfo = handleJupyterSpawnError(spawnError, port2, processLogsRef.current);
627
658
  if (errorInfo) {
628
659
  setJupyterStep("error");
629
660
  setError(errorInfo);
@@ -811,6 +842,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
811
842
  ),
812
843
  /* @__PURE__ */ jsx(Step, { status: tunnelStep, label: "Connecting to Sharpe" })
813
844
  ] }),
845
+ processLogs.length > 0 && /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children: processLogs.map((line, i) => /* @__PURE__ */ jsx(Text, { dimColor: true, wrap: "truncate", children: line }, i)) }),
814
846
  depsPrompt && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
815
847
  /* @__PURE__ */ jsxs(Text, { children: [
816
848
  "Found ",
@@ -896,43 +928,64 @@ function StatusBadge({ status }) {
896
928
  ] });
897
929
  }
898
930
  }
899
- function waitForJupyterHealthy(proc, port2, pushEvent, isCancelled) {
931
+ function waitForJupyterHealthy(proc, port2, pushEvent, pushLog, isCancelled) {
900
932
  return new Promise((resolve) => {
901
- proc.on("error", (err) => resolve(err.message));
902
- let stderrBuf = "";
903
- proc.stderr?.on("data", (data) => {
904
- stderrBuf += data.toString();
905
- const lines = stderrBuf.split("\n");
906
- stderrBuf = lines.pop() ?? "";
907
- for (const line of lines) {
908
- if (line.includes("Address already in use")) {
909
- resolve("port-conflict");
910
- return;
911
- }
912
- const msg = parseJupyterLine(line);
913
- if (msg) pushEvent(msg);
933
+ let resolved = false;
934
+ const done = (result) => {
935
+ if (resolved) return;
936
+ resolved = true;
937
+ clearInterval(poll);
938
+ resolve(result);
939
+ };
940
+ proc.on("error", (err) => done(err.message));
941
+ proc.on("exit", (code, signal) => {
942
+ if (code !== null && code !== 0) {
943
+ done(`exit-${code}`);
944
+ } else if (signal) {
945
+ done(`signal-${signal}`);
914
946
  }
915
947
  });
948
+ const pipeOutput = (stream) => {
949
+ if (!stream) return;
950
+ let buf = "";
951
+ stream.on("data", (data) => {
952
+ buf += data.toString();
953
+ const lines = buf.split("\n");
954
+ buf = lines.pop() ?? "";
955
+ for (const line of lines) {
956
+ pushLog(line);
957
+ if (line.includes("Address already in use")) {
958
+ done("port-conflict");
959
+ return;
960
+ }
961
+ const msg = parseJupyterLine(line);
962
+ if (msg) pushEvent(msg);
963
+ }
964
+ });
965
+ };
966
+ pipeOutput(proc.stderr);
967
+ pipeOutput(proc.stdout);
916
968
  let attempts = 0;
917
969
  const poll = setInterval(async () => {
918
970
  if (isCancelled()) {
919
- clearInterval(poll);
920
- resolve("cancelled");
971
+ done("cancelled");
921
972
  return;
922
973
  }
923
974
  attempts++;
924
975
  const healthy = await checkJupyterHealth(port2);
925
976
  if (healthy) {
926
- clearInterval(poll);
927
- resolve(null);
928
- } else if (attempts >= 20) {
929
- clearInterval(poll);
930
- resolve("timeout");
977
+ done(null);
978
+ } else if (attempts >= 40) {
979
+ done("timeout");
931
980
  }
932
981
  }, 1e3);
933
982
  });
934
983
  }
935
- function handleJupyterSpawnError(spawnError, port2) {
984
+ function handleJupyterSpawnError(spawnError, port2, lastLogs) {
985
+ const logTail = lastLogs.length > 0 ? `
986
+
987
+ Process output:
988
+ ${lastLogs.slice(-8).join("\n")}` : "";
936
989
  if (spawnError === "port-conflict") {
937
990
  return {
938
991
  message: "Could not start the notebook server.",
@@ -945,13 +998,20 @@ Close whatever is using it and try again, or use a different port:
945
998
  if (spawnError === "timeout") {
946
999
  return {
947
1000
  message: "The notebook server isn't responding.",
948
- hint: `Try restarting or check if port ${port2} is available.`
1001
+ hint: `Waited 40s but http://localhost:${port2}/api/status never returned OK.${logTail}`
1002
+ };
1003
+ }
1004
+ if (spawnError?.startsWith("exit-")) {
1005
+ const code = spawnError.slice(5);
1006
+ return {
1007
+ message: `Notebook server process exited with code ${code}.`,
1008
+ hint: logTail || "No output was captured from the process."
949
1009
  };
950
1010
  }
951
1011
  if (spawnError && spawnError !== "cancelled") {
952
1012
  return {
953
1013
  message: "Could not start the notebook server.",
954
- hint: "Something went wrong starting Jupyter. Try running again."
1014
+ hint: `${spawnError}${logTail}`
955
1015
  };
956
1016
  }
957
1017
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sharpe-jupyter/connect",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Connect a local JupyterHub to Sharpe via Cloudflare Tunnel",
5
5
  "type": "module",
6
6
  "bin": {