@sharpe-jupyter/connect 0.4.2 → 0.4.4
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 +174 -42
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.tsx
|
|
4
4
|
import { render } from "ink";
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
6
|
+
import { join as join5 } from "path";
|
|
5
7
|
|
|
6
8
|
// src/app.tsx
|
|
7
9
|
import { useState, useEffect, useRef, useCallback } from "react";
|
|
@@ -333,6 +335,7 @@ function parseJupyterLine(line) {
|
|
|
333
335
|
// src/app.tsx
|
|
334
336
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
335
337
|
var MAX_EVENTS = 10;
|
|
338
|
+
var MAX_PROCESS_LOGS = 12;
|
|
336
339
|
var LOGO = ` ..
|
|
337
340
|
.:
|
|
338
341
|
.@.
|
|
@@ -386,11 +389,23 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
386
389
|
const [depsPrompt, setDepsPrompt] = useState(null);
|
|
387
390
|
const [depsStatus, setDepsStatus] = useState(null);
|
|
388
391
|
const depsResolverRef = useRef(null);
|
|
392
|
+
const [processLogs, setProcessLogs] = useState([]);
|
|
389
393
|
const didWeSpawnJupyter = useRef(false);
|
|
390
394
|
const jupyterProc = useRef(null);
|
|
391
395
|
const tunnelProc = useRef(null);
|
|
392
396
|
const cleanedUp = useRef(false);
|
|
397
|
+
const processLogsRef = useRef([]);
|
|
393
398
|
const uptime = useUptime(startedAt);
|
|
399
|
+
const pushProcessLog = useCallback((line) => {
|
|
400
|
+
const trimmed = line.trimEnd();
|
|
401
|
+
if (!trimmed) return;
|
|
402
|
+
setProcessLogs((prev) => {
|
|
403
|
+
const next = [...prev, trimmed];
|
|
404
|
+
const sliced = next.length > MAX_PROCESS_LOGS ? next.slice(-MAX_PROCESS_LOGS) : next;
|
|
405
|
+
processLogsRef.current = sliced;
|
|
406
|
+
return sliced;
|
|
407
|
+
});
|
|
408
|
+
}, []);
|
|
394
409
|
const pushEvent = useCallback((message) => {
|
|
395
410
|
setEvents((prev) => {
|
|
396
411
|
const next = [...prev, { time: formatTime(), message }];
|
|
@@ -439,20 +454,15 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
439
454
|
jupyterProc.current = null;
|
|
440
455
|
}
|
|
441
456
|
setJupyterRunning(false);
|
|
457
|
+
setProcessLogs([]);
|
|
458
|
+
processLogsRef.current = [];
|
|
442
459
|
pushEvent("Restarting notebook server...");
|
|
443
460
|
const proc = isUvProject() ? startJupyterWithUv(port2, getUvPath()) : startJupyter(port2);
|
|
444
461
|
jupyterProc.current = proc;
|
|
445
462
|
didWeSpawnJupyter.current = true;
|
|
446
|
-
|
|
447
|
-
proc.stderr
|
|
448
|
-
|
|
449
|
-
const lines = stderrBuf.split("\n");
|
|
450
|
-
stderrBuf = lines.pop() ?? "";
|
|
451
|
-
for (const line of lines) {
|
|
452
|
-
const msg = parseJupyterLine(line);
|
|
453
|
-
if (msg) pushEvent(msg);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
463
|
+
const pipeStream = createStreamPiper(pushProcessLog, pushEvent);
|
|
464
|
+
pipeStream(proc.stderr);
|
|
465
|
+
pipeStream(proc.stdout);
|
|
456
466
|
proc.on("error", () => {
|
|
457
467
|
setJupyterRunning(false);
|
|
458
468
|
pushEvent("Notebook server failed to start");
|
|
@@ -471,12 +481,12 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
471
481
|
clearInterval(poll);
|
|
472
482
|
setJupyterRunning(true);
|
|
473
483
|
pushEvent("Notebook server started");
|
|
474
|
-
} else if (attempts >=
|
|
484
|
+
} else if (attempts >= 40) {
|
|
475
485
|
clearInterval(poll);
|
|
476
486
|
pushEvent("Notebook server not responding");
|
|
477
487
|
}
|
|
478
488
|
}, 1e3);
|
|
479
|
-
}, [port2, pushEvent]);
|
|
489
|
+
}, [port2, pushEvent, pushProcessLog]);
|
|
480
490
|
useInput(
|
|
481
491
|
(_input, key) => {
|
|
482
492
|
if (_input === "r" || _input === "R") {
|
|
@@ -501,6 +511,8 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
501
511
|
setTunnelConnected(false);
|
|
502
512
|
setDepsPrompt(null);
|
|
503
513
|
setDepsStatus(null);
|
|
514
|
+
setProcessLogs([]);
|
|
515
|
+
processLogsRef.current = [];
|
|
504
516
|
depsResolverRef.current?.(false);
|
|
505
517
|
depsResolverRef.current = null;
|
|
506
518
|
}, []);
|
|
@@ -543,9 +555,15 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
543
555
|
const proc = startJupyterWithUv(port2, uvPath);
|
|
544
556
|
jupyterProc.current = proc;
|
|
545
557
|
didWeSpawnJupyter.current = true;
|
|
546
|
-
const spawnError = await waitForJupyterHealthy(
|
|
558
|
+
const spawnError = await waitForJupyterHealthy(
|
|
559
|
+
proc,
|
|
560
|
+
port2,
|
|
561
|
+
pushEvent,
|
|
562
|
+
pushProcessLog,
|
|
563
|
+
() => cancelled
|
|
564
|
+
);
|
|
547
565
|
if (cancelled) return;
|
|
548
|
-
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
566
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2, processLogsRef.current);
|
|
549
567
|
if (errorInfo) {
|
|
550
568
|
setJupyterStep("error");
|
|
551
569
|
setError(errorInfo);
|
|
@@ -629,9 +647,15 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
629
647
|
const proc = startJupyter(port2);
|
|
630
648
|
jupyterProc.current = proc;
|
|
631
649
|
didWeSpawnJupyter.current = true;
|
|
632
|
-
const spawnError = await waitForJupyterHealthy(
|
|
650
|
+
const spawnError = await waitForJupyterHealthy(
|
|
651
|
+
proc,
|
|
652
|
+
port2,
|
|
653
|
+
pushEvent,
|
|
654
|
+
pushProcessLog,
|
|
655
|
+
() => cancelled
|
|
656
|
+
);
|
|
633
657
|
if (cancelled) return;
|
|
634
|
-
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
658
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2, processLogsRef.current);
|
|
635
659
|
if (errorInfo) {
|
|
636
660
|
setJupyterStep("error");
|
|
637
661
|
setError(errorInfo);
|
|
@@ -819,6 +843,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
819
843
|
),
|
|
820
844
|
/* @__PURE__ */ jsx(Step, { status: tunnelStep, label: "Connecting to Sharpe" })
|
|
821
845
|
] }),
|
|
846
|
+
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)) }),
|
|
822
847
|
depsPrompt && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
823
848
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
824
849
|
"Found ",
|
|
@@ -904,43 +929,69 @@ function StatusBadge({ status }) {
|
|
|
904
929
|
] });
|
|
905
930
|
}
|
|
906
931
|
}
|
|
907
|
-
function waitForJupyterHealthy(proc, port2, pushEvent, isCancelled) {
|
|
932
|
+
function waitForJupyterHealthy(proc, port2, pushEvent, pushLog, isCancelled) {
|
|
908
933
|
return new Promise((resolve) => {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
|
|
934
|
+
let resolved = false;
|
|
935
|
+
const done = (result) => {
|
|
936
|
+
if (resolved) return;
|
|
937
|
+
resolved = true;
|
|
938
|
+
clearInterval(poll);
|
|
939
|
+
resolve(result);
|
|
940
|
+
};
|
|
941
|
+
proc.on("error", (err) => done(err.message));
|
|
942
|
+
proc.on("exit", (code, signal) => {
|
|
943
|
+
if (code !== null && code !== 0) {
|
|
944
|
+
done(`exit-${code}`);
|
|
945
|
+
} else if (signal) {
|
|
946
|
+
done(`signal-${signal}`);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
const pipeOutput = createStreamPiper(pushLog, pushEvent, (line) => {
|
|
950
|
+
if (line.includes("Address already in use")) {
|
|
951
|
+
done("port-conflict");
|
|
952
|
+
return true;
|
|
922
953
|
}
|
|
923
954
|
});
|
|
955
|
+
pipeOutput(proc.stderr);
|
|
956
|
+
pipeOutput(proc.stdout);
|
|
924
957
|
let attempts = 0;
|
|
925
958
|
const poll = setInterval(async () => {
|
|
926
959
|
if (isCancelled()) {
|
|
927
|
-
|
|
928
|
-
resolve("cancelled");
|
|
960
|
+
done("cancelled");
|
|
929
961
|
return;
|
|
930
962
|
}
|
|
931
963
|
attempts++;
|
|
932
964
|
const healthy = await checkJupyterHealth(port2);
|
|
933
965
|
if (healthy) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
clearInterval(poll);
|
|
938
|
-
resolve("timeout");
|
|
966
|
+
done(null);
|
|
967
|
+
} else if (attempts >= 40) {
|
|
968
|
+
done("timeout");
|
|
939
969
|
}
|
|
940
970
|
}, 1e3);
|
|
941
971
|
});
|
|
942
972
|
}
|
|
943
|
-
function
|
|
973
|
+
function createStreamPiper(pushLog, pushEvent, onLine) {
|
|
974
|
+
return (stream) => {
|
|
975
|
+
if (!stream) return;
|
|
976
|
+
let buf = "";
|
|
977
|
+
stream.on("data", (data) => {
|
|
978
|
+
buf += data.toString();
|
|
979
|
+
const lines = buf.split("\n");
|
|
980
|
+
buf = lines.pop() ?? "";
|
|
981
|
+
for (const line of lines) {
|
|
982
|
+
pushLog(line);
|
|
983
|
+
if (onLine?.(line)) return;
|
|
984
|
+
const msg = parseJupyterLine(line);
|
|
985
|
+
if (msg) pushEvent(msg);
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
function handleJupyterSpawnError(spawnError, port2, lastLogs) {
|
|
991
|
+
const logTail = lastLogs.length > 0 ? `
|
|
992
|
+
|
|
993
|
+
Process output:
|
|
994
|
+
${lastLogs.slice(-8).join("\n")}` : "";
|
|
944
995
|
if (spawnError === "port-conflict") {
|
|
945
996
|
return {
|
|
946
997
|
message: "Could not start the notebook server.",
|
|
@@ -953,13 +1004,20 @@ Close whatever is using it and try again, or use a different port:
|
|
|
953
1004
|
if (spawnError === "timeout") {
|
|
954
1005
|
return {
|
|
955
1006
|
message: "The notebook server isn't responding.",
|
|
956
|
-
hint: `
|
|
1007
|
+
hint: `Waited 40s but http://localhost:${port2}/api/status never returned OK.${logTail}`
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
if (spawnError?.startsWith("exit-")) {
|
|
1011
|
+
const code = spawnError.slice(5);
|
|
1012
|
+
return {
|
|
1013
|
+
message: `Notebook server process exited with code ${code}.`,
|
|
1014
|
+
hint: logTail || "No output was captured from the process."
|
|
957
1015
|
};
|
|
958
1016
|
}
|
|
959
1017
|
if (spawnError && spawnError !== "cancelled") {
|
|
960
1018
|
return {
|
|
961
1019
|
message: "Could not start the notebook server.",
|
|
962
|
-
hint:
|
|
1020
|
+
hint: `${spawnError}${logTail}`
|
|
963
1021
|
};
|
|
964
1022
|
}
|
|
965
1023
|
return null;
|
|
@@ -967,6 +1025,70 @@ Close whatever is using it and try again, or use a different port:
|
|
|
967
1025
|
|
|
968
1026
|
// src/index.tsx
|
|
969
1027
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1028
|
+
function getConfigDir() {
|
|
1029
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
1030
|
+
return join5(home, ".sharpe");
|
|
1031
|
+
}
|
|
1032
|
+
function getConfigPath() {
|
|
1033
|
+
return join5(getConfigDir(), "config.json");
|
|
1034
|
+
}
|
|
1035
|
+
function loadSavedToken() {
|
|
1036
|
+
try {
|
|
1037
|
+
const raw = readFileSync(getConfigPath(), "utf-8");
|
|
1038
|
+
const config = JSON.parse(raw);
|
|
1039
|
+
return config.connectionCode ?? null;
|
|
1040
|
+
} catch {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function saveToken(token) {
|
|
1045
|
+
try {
|
|
1046
|
+
mkdirSync4(getConfigDir(), { recursive: true });
|
|
1047
|
+
const config = { connectionCode: token };
|
|
1048
|
+
writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n");
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function ensureShellAlias() {
|
|
1053
|
+
const home = process.env.HOME;
|
|
1054
|
+
if (!home) return;
|
|
1055
|
+
const shell = process.env.SHELL ?? "";
|
|
1056
|
+
let rcFile;
|
|
1057
|
+
if (shell.endsWith("/zsh")) {
|
|
1058
|
+
rcFile = join5(home, ".zshrc");
|
|
1059
|
+
} else if (shell.endsWith("/bash")) {
|
|
1060
|
+
const bashrc = join5(home, ".bashrc");
|
|
1061
|
+
rcFile = existsSync5(bashrc) ? bashrc : join5(home, ".bash_profile");
|
|
1062
|
+
} else {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const aliasLine = 'alias sharpe="npx @sharpe-jupyter/connect@latest"';
|
|
1066
|
+
try {
|
|
1067
|
+
const contents = readFileSync(rcFile, "utf-8");
|
|
1068
|
+
if (contents.includes("alias sharpe=")) return;
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
writeFileSync(rcFile, readFileSync(rcFile, "utf-8").trimEnd() + `
|
|
1073
|
+
|
|
1074
|
+
# Sharpe CLI
|
|
1075
|
+
${aliasLine}
|
|
1076
|
+
`, "utf-8");
|
|
1077
|
+
console.log(`Added "sharpe" alias to ${rcFile}`);
|
|
1078
|
+
console.log(`Restart your terminal or run: source ${rcFile}
|
|
1079
|
+
`);
|
|
1080
|
+
} catch {
|
|
1081
|
+
try {
|
|
1082
|
+
writeFileSync(rcFile, `# Sharpe CLI
|
|
1083
|
+
${aliasLine}
|
|
1084
|
+
`, "utf-8");
|
|
1085
|
+
console.log(`Added "sharpe" alias to ${rcFile}`);
|
|
1086
|
+
console.log(`Restart your terminal or run: source ${rcFile}
|
|
1087
|
+
`);
|
|
1088
|
+
} catch {
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
970
1092
|
function parseArgs(argv) {
|
|
971
1093
|
const args = argv.slice(2);
|
|
972
1094
|
let connectionCode2 = null;
|
|
@@ -975,7 +1097,10 @@ function parseArgs(argv) {
|
|
|
975
1097
|
const arg = args[i];
|
|
976
1098
|
if (arg === "--help" || arg === "-h") {
|
|
977
1099
|
console.log(`
|
|
978
|
-
Usage: sharpe
|
|
1100
|
+
Usage: sharpe [connection-code] [options]
|
|
1101
|
+
|
|
1102
|
+
If a connection code is provided, it is saved to ~/.sharpe/config.json
|
|
1103
|
+
for future use. Run "sharpe" with no arguments to reuse the saved code.
|
|
979
1104
|
|
|
980
1105
|
Options:
|
|
981
1106
|
--port, -p Notebook port (default: 8888)
|
|
@@ -993,5 +1118,12 @@ Options:
|
|
|
993
1118
|
}
|
|
994
1119
|
return { connectionCode: connectionCode2, port: port2 };
|
|
995
1120
|
}
|
|
996
|
-
var { connectionCode, port } = parseArgs(process.argv);
|
|
1121
|
+
var { connectionCode: cliToken, port } = parseArgs(process.argv);
|
|
1122
|
+
var connectionCode = cliToken;
|
|
1123
|
+
if (connectionCode) {
|
|
1124
|
+
saveToken(connectionCode);
|
|
1125
|
+
ensureShellAlias();
|
|
1126
|
+
} else {
|
|
1127
|
+
connectionCode = loadSavedToken();
|
|
1128
|
+
}
|
|
997
1129
|
render(/* @__PURE__ */ jsx2(App, { connectionCode, port }));
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sharpe-jupyter/connect",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Connect a local JupyterHub to Sharpe via Cloudflare Tunnel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"sharpe": "dist/index.js",
|
|
7
8
|
"sharpe-connect": "dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|