@sharpe-jupyter/connect 0.1.0 → 0.3.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 +683 -105
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.tsx
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
|
|
6
|
+
// src/app.tsx
|
|
7
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
8
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
6
9
|
import Spinner from "ink-spinner";
|
|
7
|
-
import { spawn } from "child_process";
|
|
10
|
+
import { spawn as spawn2 } from "child_process";
|
|
8
11
|
|
|
9
12
|
// src/cloudflared.ts
|
|
10
|
-
import { createWriteStream, existsSync, mkdirSync, chmodSync } from "fs";
|
|
13
|
+
import { createWriteStream, existsSync, mkdirSync, chmodSync, unlinkSync } from "fs";
|
|
11
14
|
import { join } from "path";
|
|
12
15
|
import { homedir, platform, arch } from "os";
|
|
13
16
|
import { pipeline } from "stream/promises";
|
|
@@ -58,7 +61,7 @@ async function ensureCloudflared() {
|
|
|
58
61
|
fileStream
|
|
59
62
|
);
|
|
60
63
|
execSync(`tar -xzf "${tgzPath}" -C "${binDir}" cloudflared`, { stdio: "ignore" });
|
|
61
|
-
|
|
64
|
+
unlinkSync(tgzPath);
|
|
62
65
|
} else {
|
|
63
66
|
const fileStream = createWriteStream(binPath);
|
|
64
67
|
await pipeline(
|
|
@@ -72,6 +75,53 @@ async function ensureCloudflared() {
|
|
|
72
75
|
return { path: binPath, downloaded: true };
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
// src/ripgrep.ts
|
|
79
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, chmodSync as chmodSync2, unlinkSync as unlinkSync2, createWriteStream as createWriteStream2 } from "fs";
|
|
80
|
+
import { join as join2 } from "path";
|
|
81
|
+
import { homedir as homedir2, arch as arch2 } from "os";
|
|
82
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
83
|
+
import { Readable as Readable2 } from "stream";
|
|
84
|
+
import { execSync as execSync2 } from "child_process";
|
|
85
|
+
var RIPGREP_VERSION = "15.1.0";
|
|
86
|
+
function getAssetName() {
|
|
87
|
+
const cpu = arch2();
|
|
88
|
+
const triple = cpu === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
|
|
89
|
+
return `ripgrep-${RIPGREP_VERSION}-${triple}.tar.gz`;
|
|
90
|
+
}
|
|
91
|
+
function getBinDir2() {
|
|
92
|
+
return join2(homedir2(), ".sharpe", "bin");
|
|
93
|
+
}
|
|
94
|
+
function getRipgrepPath() {
|
|
95
|
+
return join2(getBinDir2(), "rg");
|
|
96
|
+
}
|
|
97
|
+
async function ensureRipgrep() {
|
|
98
|
+
const binPath = getRipgrepPath();
|
|
99
|
+
if (existsSync2(binPath)) {
|
|
100
|
+
return { path: binPath, downloaded: false };
|
|
101
|
+
}
|
|
102
|
+
const binDir = getBinDir2();
|
|
103
|
+
mkdirSync2(binDir, { recursive: true });
|
|
104
|
+
const assetName = getAssetName();
|
|
105
|
+
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/${assetName}`;
|
|
106
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
107
|
+
if (!response.ok || !response.body) {
|
|
108
|
+
throw new Error(`Failed to download ripgrep: ${response.status} ${response.statusText}`);
|
|
109
|
+
}
|
|
110
|
+
const tgzPath = join2(binDir, assetName);
|
|
111
|
+
const fileStream = createWriteStream2(tgzPath);
|
|
112
|
+
await pipeline2(
|
|
113
|
+
Readable2.fromWeb(response.body),
|
|
114
|
+
fileStream
|
|
115
|
+
);
|
|
116
|
+
const dirName = assetName.replace(".tar.gz", "");
|
|
117
|
+
execSync2(`tar -xzf "${tgzPath}" -C "${binDir}" "${dirName}/rg"`, { stdio: "ignore" });
|
|
118
|
+
execSync2(`mv "${join2(binDir, dirName, "rg")}" "${binPath}"`, { stdio: "ignore" });
|
|
119
|
+
unlinkSync2(tgzPath);
|
|
120
|
+
execSync2(`rm -rf "${join2(binDir, dirName)}"`, { stdio: "ignore" });
|
|
121
|
+
chmodSync2(binPath, 493);
|
|
122
|
+
return { path: binPath, downloaded: true };
|
|
123
|
+
}
|
|
124
|
+
|
|
75
125
|
// src/health.ts
|
|
76
126
|
async function checkJupyterHealth(port2) {
|
|
77
127
|
try {
|
|
@@ -84,137 +134,665 @@ async function checkJupyterHealth(port2) {
|
|
|
84
134
|
}
|
|
85
135
|
}
|
|
86
136
|
|
|
87
|
-
// src/
|
|
137
|
+
// src/jupyter.ts
|
|
138
|
+
import { execSync as execSync3, spawn } from "child_process";
|
|
139
|
+
import { existsSync as existsSync3 } from "fs";
|
|
140
|
+
import { join as join3 } from "path";
|
|
141
|
+
import { homedir as homedir3, platform as platform2 } from "os";
|
|
142
|
+
var SHARPE_DIR = join3(homedir3(), ".sharpe");
|
|
143
|
+
var VENV_DIR = join3(SHARPE_DIR, "venv");
|
|
144
|
+
var SHARPE_BIN_DIR = join3(SHARPE_DIR, "bin");
|
|
145
|
+
var IS_WIN = platform2() === "win32";
|
|
146
|
+
var BIN_DIR = IS_WIN ? "Scripts" : "bin";
|
|
147
|
+
function venvBin(name) {
|
|
148
|
+
return join3(VENV_DIR, BIN_DIR, IS_WIN ? `${name}.exe` : name);
|
|
149
|
+
}
|
|
150
|
+
function findPython() {
|
|
151
|
+
for (const cmd of ["python3", "python"]) {
|
|
152
|
+
try {
|
|
153
|
+
const version = execSync3(`${cmd} --version`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
154
|
+
if (version.startsWith("Python 3")) return cmd;
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function venvExists() {
|
|
161
|
+
return existsSync3(venvBin("python"));
|
|
162
|
+
}
|
|
163
|
+
function createVenv(pythonCmd) {
|
|
164
|
+
execSync3(`${pythonCmd} -m venv "${VENV_DIR}"`, { stdio: "ignore" });
|
|
165
|
+
}
|
|
166
|
+
function installPackages(packages) {
|
|
167
|
+
return spawn(venvBin("pip"), ["install", "--quiet", ...packages], {
|
|
168
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function startJupyter(port2) {
|
|
172
|
+
const env = { ...process.env, PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}` };
|
|
173
|
+
return spawn(
|
|
174
|
+
venvBin("jupyter"),
|
|
175
|
+
[
|
|
176
|
+
"notebook",
|
|
177
|
+
"--port",
|
|
178
|
+
String(port2),
|
|
179
|
+
"--no-browser",
|
|
180
|
+
"--ServerApp.token=",
|
|
181
|
+
"--ServerApp.password=",
|
|
182
|
+
"--ServerApp.allow_remote_access=True",
|
|
183
|
+
"--ServerApp.disable_check_xsrf=True",
|
|
184
|
+
'--ServerApp.jpserver_extensions={"sharpe_log_handler.handler": true}'
|
|
185
|
+
],
|
|
186
|
+
{ stdio: ["ignore", "pipe", "pipe"], env }
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
function parseJupyterLine(line) {
|
|
190
|
+
if (/Saving file at\s+(\S+)/.test(line)) {
|
|
191
|
+
const match = line.match(/Saving file at\s+(\S+)/);
|
|
192
|
+
if (match) {
|
|
193
|
+
const filename = match[1].split("/").pop() ?? match[1];
|
|
194
|
+
return `Notebook saved: ${filename}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (/Kernel started/i.test(line)) {
|
|
198
|
+
return "Kernel started";
|
|
199
|
+
}
|
|
200
|
+
if (/KernelRestarter:\s*restarting/i.test(line)) {
|
|
201
|
+
return "Kernel restarted";
|
|
202
|
+
}
|
|
203
|
+
if (/\b(ERROR|CRITICAL)\b/.test(line)) {
|
|
204
|
+
const cleaned = line.replace(/^\[.*?\]\s*/, "").replace(/^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s*/, "").replace(/^(ERROR|CRITICAL)\s*[|:]\s*/i, "").trim();
|
|
205
|
+
return cleaned || null;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/app.tsx
|
|
88
211
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
89
|
-
|
|
212
|
+
var MAX_EVENTS = 10;
|
|
213
|
+
var LOGO = ` ..
|
|
214
|
+
.:
|
|
215
|
+
.@.
|
|
216
|
+
.@*
|
|
217
|
+
.@@+.
|
|
218
|
+
.@@@=
|
|
219
|
+
.@@@@*
|
|
220
|
+
.@@@@@@.
|
|
221
|
+
.@@@@@@@#.
|
|
222
|
+
.@@@@@@@@@@-.
|
|
223
|
+
.=@@@@@@@@@@.
|
|
224
|
+
.#@@@@@@@.
|
|
225
|
+
.@@@@@@.
|
|
226
|
+
*@@@@.
|
|
227
|
+
-@@@.
|
|
228
|
+
.+@@.
|
|
229
|
+
*@.
|
|
230
|
+
.@.
|
|
231
|
+
:.
|
|
232
|
+
..`;
|
|
233
|
+
function useUptime(startedAt) {
|
|
234
|
+
const [now, setNow] = useState(Date.now());
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (startedAt === null) return;
|
|
237
|
+
const id = setInterval(() => setNow(Date.now()), 1e3);
|
|
238
|
+
return () => clearInterval(id);
|
|
239
|
+
}, [startedAt]);
|
|
240
|
+
if (startedAt === null) return "0s";
|
|
241
|
+
const elapsed = Math.max(0, Math.floor((now - startedAt) / 1e3));
|
|
242
|
+
const m = Math.floor(elapsed / 60);
|
|
243
|
+
const s = elapsed % 60;
|
|
244
|
+
return m > 0 ? `${m}m ${s}s` : `${s}s`;
|
|
245
|
+
}
|
|
246
|
+
function formatTime() {
|
|
247
|
+
const d = /* @__PURE__ */ new Date();
|
|
248
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
249
|
+
}
|
|
250
|
+
function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
90
251
|
const { exit } = useApp();
|
|
91
|
-
const [phase, setPhase] = useState(
|
|
252
|
+
const [phase, setPhase] = useState(connectionCode2 ? "starting" : "prompting");
|
|
253
|
+
const [setupStep, setSetupStep] = useState(connectionCode2 ? "active" : "pending");
|
|
254
|
+
const [jupyterStep, setJupyterStep] = useState("pending");
|
|
255
|
+
const [tunnelStep, setTunnelStep] = useState("pending");
|
|
256
|
+
const [jupyterRunning, setJupyterRunning] = useState(false);
|
|
257
|
+
const [tunnelConnected, setTunnelConnected] = useState(false);
|
|
258
|
+
const [events, setEvents] = useState([]);
|
|
259
|
+
const [startedAt, setStartedAt] = useState(null);
|
|
260
|
+
const [error, setError] = useState(null);
|
|
261
|
+
const [tokenInput, setTokenInput] = useState("");
|
|
262
|
+
const [activeToken, setActiveToken] = useState(connectionCode2);
|
|
263
|
+
const didWeSpawnJupyter = useRef(false);
|
|
264
|
+
const jupyterProc = useRef(null);
|
|
265
|
+
const tunnelProc = useRef(null);
|
|
266
|
+
const cleanedUp = useRef(false);
|
|
267
|
+
const uptime = useUptime(startedAt);
|
|
268
|
+
const pushEvent = useCallback((message) => {
|
|
269
|
+
setEvents((prev) => {
|
|
270
|
+
const next = [...prev, { time: formatTime(), message }];
|
|
271
|
+
return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next;
|
|
272
|
+
});
|
|
273
|
+
}, []);
|
|
274
|
+
useInput(
|
|
275
|
+
(input, key) => {
|
|
276
|
+
if (key.return) {
|
|
277
|
+
const trimmed = tokenInput.trim();
|
|
278
|
+
if (trimmed.length > 0) {
|
|
279
|
+
setActiveToken(trimmed);
|
|
280
|
+
setPhase("starting");
|
|
281
|
+
setSetupStep("active");
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (key.backspace || key.delete) {
|
|
286
|
+
setTokenInput((prev) => prev.slice(0, -1));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (input && !key.ctrl && !key.meta) {
|
|
290
|
+
setTokenInput((prev) => prev + input.replace(/[\n\r\s]/g, ""));
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
{ isActive: phase === "prompting" }
|
|
294
|
+
);
|
|
295
|
+
const restartJupyter = useCallback(() => {
|
|
296
|
+
if (jupyterProc.current) {
|
|
297
|
+
jupyterProc.current.kill("SIGTERM");
|
|
298
|
+
jupyterProc.current = null;
|
|
299
|
+
}
|
|
300
|
+
setJupyterRunning(false);
|
|
301
|
+
pushEvent("Restarting notebook server...");
|
|
302
|
+
const proc = startJupyter(port2);
|
|
303
|
+
jupyterProc.current = proc;
|
|
304
|
+
didWeSpawnJupyter.current = true;
|
|
305
|
+
let stderrBuf = "";
|
|
306
|
+
proc.stderr?.on("data", (data) => {
|
|
307
|
+
stderrBuf += data.toString();
|
|
308
|
+
const lines = stderrBuf.split("\n");
|
|
309
|
+
stderrBuf = lines.pop() ?? "";
|
|
310
|
+
for (const line of lines) {
|
|
311
|
+
const msg = parseJupyterLine(line);
|
|
312
|
+
if (msg) pushEvent(msg);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
proc.on("error", () => {
|
|
316
|
+
setJupyterRunning(false);
|
|
317
|
+
pushEvent("Notebook server failed to start");
|
|
318
|
+
});
|
|
319
|
+
proc.on("exit", () => {
|
|
320
|
+
if (!cleanedUp.current) {
|
|
321
|
+
setJupyterRunning(false);
|
|
322
|
+
pushEvent("Notebook server stopped");
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
let attempts = 0;
|
|
326
|
+
const poll = setInterval(async () => {
|
|
327
|
+
attempts++;
|
|
328
|
+
const healthy = await checkJupyterHealth(port2);
|
|
329
|
+
if (healthy) {
|
|
330
|
+
clearInterval(poll);
|
|
331
|
+
setJupyterRunning(true);
|
|
332
|
+
pushEvent("Notebook server started");
|
|
333
|
+
} else if (attempts >= 20) {
|
|
334
|
+
clearInterval(poll);
|
|
335
|
+
pushEvent("Notebook server not responding");
|
|
336
|
+
}
|
|
337
|
+
}, 1e3);
|
|
338
|
+
}, [port2, pushEvent]);
|
|
339
|
+
useInput(
|
|
340
|
+
(_input, key) => {
|
|
341
|
+
if (_input === "r" || _input === "R") {
|
|
342
|
+
if (!key.ctrl && !key.meta) {
|
|
343
|
+
restartJupyter();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
{ isActive: phase === "ready" && !jupyterRunning }
|
|
348
|
+
);
|
|
349
|
+
const resetToPrompt = useCallback(() => {
|
|
350
|
+
setError(null);
|
|
351
|
+
setPhase("prompting");
|
|
352
|
+
setSetupStep("pending");
|
|
353
|
+
setJupyterStep("pending");
|
|
354
|
+
setTunnelStep("pending");
|
|
355
|
+
setTokenInput("");
|
|
356
|
+
setActiveToken(null);
|
|
357
|
+
setEvents([]);
|
|
358
|
+
setStartedAt(null);
|
|
359
|
+
setJupyterRunning(false);
|
|
360
|
+
setTunnelConnected(false);
|
|
361
|
+
}, []);
|
|
362
|
+
useInput(
|
|
363
|
+
(_input, key) => {
|
|
364
|
+
if (key.return) {
|
|
365
|
+
resetToPrompt();
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
{ isActive: error !== null }
|
|
369
|
+
);
|
|
92
370
|
useEffect(() => {
|
|
93
|
-
|
|
371
|
+
if (phase !== "starting" || !activeToken) return;
|
|
372
|
+
let cancelled = false;
|
|
94
373
|
async function run() {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
374
|
+
try {
|
|
375
|
+
await Promise.all([ensureCloudflared(), ensureRipgrep()]);
|
|
376
|
+
} catch {
|
|
377
|
+
if (cancelled) return;
|
|
378
|
+
setSetupStep("error");
|
|
379
|
+
setError({
|
|
380
|
+
message: "Could not set up required tools.",
|
|
381
|
+
hint: "Check your internet connection and try again."
|
|
382
|
+
});
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (cancelled) return;
|
|
386
|
+
setSetupStep("done");
|
|
387
|
+
setJupyterStep("active");
|
|
388
|
+
const alreadyHealthy = await checkJupyterHealth(port2);
|
|
389
|
+
if (cancelled) return;
|
|
390
|
+
if (alreadyHealthy) {
|
|
391
|
+
setJupyterStep("done");
|
|
392
|
+
setJupyterRunning(true);
|
|
393
|
+
didWeSpawnJupyter.current = false;
|
|
394
|
+
pushEvent("Existing notebook server detected");
|
|
395
|
+
} else {
|
|
396
|
+
const pythonCmd = findPython();
|
|
397
|
+
if (!pythonCmd) {
|
|
398
|
+
setJupyterStep("error");
|
|
399
|
+
setError({
|
|
400
|
+
message: "Python 3 is not installed.",
|
|
401
|
+
hint: "Install Python 3.11+ from https://python.org and try again."
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (!venvExists()) {
|
|
406
|
+
pushEvent("Creating Sharpe environment...");
|
|
407
|
+
try {
|
|
408
|
+
createVenv(pythonCmd);
|
|
409
|
+
} catch {
|
|
410
|
+
if (cancelled) return;
|
|
411
|
+
setJupyterStep("error");
|
|
412
|
+
setError({
|
|
413
|
+
message: "Could not create the Sharpe environment.",
|
|
414
|
+
hint: "Make sure Python 3.11+ is installed and try again."
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (cancelled) return;
|
|
420
|
+
pushEvent("Installing Jupyter...");
|
|
421
|
+
const installResult = await new Promise((resolve) => {
|
|
422
|
+
const proc2 = installPackages(["jupyter", "jupyter-resource-usage", "sharpe-log-handler"]);
|
|
423
|
+
proc2.on("error", (err) => resolve(err.message));
|
|
424
|
+
proc2.on("exit", (code) => resolve(code === 0 ? null : "install-failed"));
|
|
425
|
+
});
|
|
426
|
+
if (cancelled) return;
|
|
427
|
+
if (installResult) {
|
|
428
|
+
setJupyterStep("error");
|
|
429
|
+
setError({
|
|
430
|
+
message: "Could not install Jupyter.",
|
|
431
|
+
hint: "Check your internet connection and try again."
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
pushEvent("Jupyter installed");
|
|
436
|
+
const proc = startJupyter(port2);
|
|
437
|
+
jupyterProc.current = proc;
|
|
438
|
+
didWeSpawnJupyter.current = true;
|
|
439
|
+
const spawnError = await new Promise((resolve) => {
|
|
440
|
+
proc.on("error", (err) => resolve(err.message));
|
|
441
|
+
let stderrBuf = "";
|
|
442
|
+
proc.stderr?.on("data", (data) => {
|
|
443
|
+
stderrBuf += data.toString();
|
|
444
|
+
const lines = stderrBuf.split("\n");
|
|
445
|
+
stderrBuf = lines.pop() ?? "";
|
|
446
|
+
for (const line of lines) {
|
|
447
|
+
if (line.includes("Address already in use")) {
|
|
448
|
+
resolve("port-conflict");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const msg = parseJupyterLine(line);
|
|
452
|
+
if (msg) pushEvent(msg);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
let attempts = 0;
|
|
456
|
+
const poll = setInterval(async () => {
|
|
457
|
+
if (cancelled) {
|
|
458
|
+
clearInterval(poll);
|
|
459
|
+
resolve("cancelled");
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
attempts++;
|
|
463
|
+
const healthy = await checkJupyterHealth(port2);
|
|
464
|
+
if (healthy) {
|
|
465
|
+
clearInterval(poll);
|
|
466
|
+
resolve(null);
|
|
467
|
+
} else if (attempts >= 20) {
|
|
468
|
+
clearInterval(poll);
|
|
469
|
+
resolve("timeout");
|
|
470
|
+
}
|
|
471
|
+
}, 1e3);
|
|
472
|
+
});
|
|
473
|
+
if (cancelled) return;
|
|
474
|
+
if (spawnError === "port-conflict") {
|
|
475
|
+
setJupyterStep("error");
|
|
476
|
+
setError({
|
|
477
|
+
message: "Could not start the notebook server.",
|
|
478
|
+
hint: `Port ${port2} is already in use.
|
|
479
|
+
Close whatever is using it and try again, or use a different port:
|
|
480
|
+
|
|
481
|
+
npx @sharpe-jupyter/connect CODE --port ${port2 + 1}`
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (spawnError === "timeout") {
|
|
486
|
+
setJupyterStep("error");
|
|
487
|
+
setError({
|
|
488
|
+
message: "The notebook server isn't responding.",
|
|
489
|
+
hint: `Try restarting or check if port ${port2} is available.`
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (spawnError && spawnError !== "cancelled") {
|
|
494
|
+
setJupyterStep("error");
|
|
495
|
+
setError({
|
|
496
|
+
message: "Could not start the notebook server.",
|
|
497
|
+
hint: "Something went wrong starting Jupyter. Try running again."
|
|
498
|
+
});
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
setJupyterRunning(true);
|
|
502
|
+
pushEvent("Notebook server started");
|
|
503
|
+
}
|
|
504
|
+
if (cancelled) return;
|
|
505
|
+
setJupyterStep("done");
|
|
506
|
+
setTunnelStep("active");
|
|
507
|
+
const binPath = getCloudflaredPath();
|
|
508
|
+
const tunnelChild = spawn2(binPath, ["tunnel", "run", "--token", activeToken], {
|
|
103
509
|
stdio: ["ignore", "pipe", "pipe"]
|
|
104
510
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
511
|
+
tunnelProc.current = tunnelChild;
|
|
512
|
+
const tunnelResult = await new Promise((resolve) => {
|
|
513
|
+
let connectionCount = 0;
|
|
514
|
+
let resolved = false;
|
|
515
|
+
const timeout = setTimeout(() => {
|
|
516
|
+
if (!resolved) {
|
|
517
|
+
resolved = true;
|
|
518
|
+
resolve("timeout");
|
|
519
|
+
}
|
|
520
|
+
}, 3e4);
|
|
521
|
+
const handleData = (data) => {
|
|
522
|
+
const text = data.toString();
|
|
523
|
+
if (text.includes("Registered tunnel connection")) {
|
|
524
|
+
connectionCount++;
|
|
525
|
+
if (connectionCount === 1 && !resolved) {
|
|
526
|
+
resolved = true;
|
|
527
|
+
clearTimeout(timeout);
|
|
528
|
+
resolve(null);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
tunnelChild.stderr?.on("data", handleData);
|
|
533
|
+
tunnelChild.stdout?.on("data", handleData);
|
|
534
|
+
tunnelChild.on("error", (err) => {
|
|
535
|
+
if (!resolved) {
|
|
536
|
+
resolved = true;
|
|
537
|
+
clearTimeout(timeout);
|
|
538
|
+
resolve(err.message);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
tunnelChild.on("exit", (code) => {
|
|
542
|
+
if (!resolved) {
|
|
543
|
+
resolved = true;
|
|
544
|
+
clearTimeout(timeout);
|
|
545
|
+
resolve(code !== 0 ? "exit-nonzero" : null);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
110
548
|
});
|
|
111
|
-
|
|
549
|
+
if (cancelled) return;
|
|
550
|
+
if (tunnelResult === "timeout") {
|
|
551
|
+
setTunnelStep("error");
|
|
552
|
+
setError({
|
|
553
|
+
message: "Could not connect to Sharpe.",
|
|
554
|
+
hint: "Check your internet connection and try again."
|
|
555
|
+
});
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (tunnelResult === "exit-nonzero") {
|
|
559
|
+
setTunnelStep("error");
|
|
560
|
+
setError({
|
|
561
|
+
message: "Could not connect to Sharpe.",
|
|
562
|
+
hint: "Make sure your connection code is correct.\nGet a new code from the Sharpe dashboard."
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (tunnelResult) {
|
|
567
|
+
setTunnelStep("error");
|
|
568
|
+
setError({ message: "Could not connect to Sharpe.", hint: tunnelResult });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
setTunnelStep("done");
|
|
572
|
+
setTunnelConnected(true);
|
|
573
|
+
pushEvent("Connected to Sharpe");
|
|
574
|
+
setPhase("ready");
|
|
575
|
+
setStartedAt(Date.now());
|
|
576
|
+
if (jupyterProc.current) {
|
|
577
|
+
jupyterProc.current.on("exit", () => {
|
|
578
|
+
if (!cleanedUp.current) {
|
|
579
|
+
setJupyterRunning(false);
|
|
580
|
+
pushEvent("Notebook server stopped");
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
let wasDisconnected = false;
|
|
585
|
+
const monitorTunnel = (data) => {
|
|
112
586
|
const text = data.toString();
|
|
113
|
-
if (
|
|
114
|
-
|
|
587
|
+
if (/Retrying|connection.*failed|ERR/i.test(text) && !wasDisconnected) {
|
|
588
|
+
wasDisconnected = true;
|
|
589
|
+
setTunnelConnected(false);
|
|
590
|
+
pushEvent("Connection interrupted, reconnecting...");
|
|
115
591
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
exit();
|
|
592
|
+
if (text.includes("Registered tunnel connection") && wasDisconnected) {
|
|
593
|
+
wasDisconnected = false;
|
|
594
|
+
setTunnelConnected(true);
|
|
595
|
+
pushEvent("Reconnected to Sharpe");
|
|
121
596
|
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
597
|
+
};
|
|
598
|
+
tunnelChild.stderr?.on("data", monitorTunnel);
|
|
599
|
+
tunnelChild.stdout?.on("data", monitorTunnel);
|
|
600
|
+
tunnelChild.on("exit", () => {
|
|
601
|
+
if (!cleanedUp.current) {
|
|
602
|
+
setTunnelConnected(false);
|
|
603
|
+
pushEvent("Tunnel disconnected");
|
|
129
604
|
}
|
|
130
605
|
});
|
|
131
|
-
const cleanup = () => {
|
|
132
|
-
killed = true;
|
|
133
|
-
child.kill("SIGTERM");
|
|
134
|
-
};
|
|
135
|
-
process.on("SIGINT", cleanup);
|
|
136
|
-
process.on("SIGTERM", cleanup);
|
|
137
606
|
}
|
|
138
|
-
run()
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
607
|
+
run();
|
|
608
|
+
const cleanup = () => {
|
|
609
|
+
cancelled = true;
|
|
610
|
+
cleanedUp.current = true;
|
|
611
|
+
setPhase("shutting-down");
|
|
612
|
+
if (didWeSpawnJupyter.current && jupyterProc.current) {
|
|
613
|
+
jupyterProc.current.kill("SIGTERM");
|
|
614
|
+
}
|
|
615
|
+
if (tunnelProc.current) {
|
|
616
|
+
tunnelProc.current.kill("SIGTERM");
|
|
617
|
+
}
|
|
618
|
+
setTimeout(() => {
|
|
619
|
+
exit();
|
|
620
|
+
process.exit(0);
|
|
621
|
+
}, 1500);
|
|
622
|
+
};
|
|
623
|
+
process.on("SIGINT", cleanup);
|
|
624
|
+
process.on("SIGTERM", cleanup);
|
|
625
|
+
return () => {
|
|
626
|
+
cancelled = true;
|
|
627
|
+
process.removeListener("SIGINT", cleanup);
|
|
628
|
+
process.removeListener("SIGTERM", cleanup);
|
|
629
|
+
};
|
|
630
|
+
}, [phase, activeToken, port2, exit, pushEvent]);
|
|
631
|
+
const overallStatus = !jupyterRunning && !tunnelConnected ? "offline" : !tunnelConnected ? "reconnecting" : !jupyterRunning ? "notebook-stopped" : "connected";
|
|
632
|
+
if (phase === "shutting-down") {
|
|
633
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
|
|
634
|
+
/* @__PURE__ */ jsx(Header, {}),
|
|
635
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Shutting down..." }) })
|
|
636
|
+
] });
|
|
637
|
+
}
|
|
638
|
+
if (phase === "prompting") {
|
|
639
|
+
const display = tokenInput.length > 50 ? `${tokenInput.slice(0, 20)}...${tokenInput.slice(-10)}` : tokenInput;
|
|
640
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
|
|
641
|
+
/* @__PURE__ */ jsx(Header, {}),
|
|
642
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { children: "Paste your connection code from the Sharpe dashboard:" }) }),
|
|
162
643
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
163
|
-
/* @__PURE__ */ jsx(Text, {
|
|
164
|
-
/* @__PURE__ */
|
|
644
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "> " }),
|
|
645
|
+
tokenInput.length > 0 ? /* @__PURE__ */ jsxs(Text, { children: [
|
|
646
|
+
display,
|
|
647
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
648
|
+
" (",
|
|
649
|
+
tokenInput.length,
|
|
650
|
+
" chars)"
|
|
651
|
+
] })
|
|
652
|
+
] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "paste here and press Enter" })
|
|
653
|
+
] })
|
|
654
|
+
] });
|
|
655
|
+
}
|
|
656
|
+
if (error) {
|
|
657
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
|
|
658
|
+
/* @__PURE__ */ jsx(Header, {}),
|
|
659
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
660
|
+
setupStep === "done" && /* @__PURE__ */ jsx(Step, { status: "done", label: "Setting up" }),
|
|
661
|
+
setupStep === "error" && /* @__PURE__ */ jsx(Step, { status: "error", label: "Setting up" }),
|
|
662
|
+
jupyterStep === "done" && /* @__PURE__ */ jsx(
|
|
663
|
+
Step,
|
|
664
|
+
{
|
|
665
|
+
status: "done",
|
|
666
|
+
label: "Notebook server running",
|
|
667
|
+
detail: `http://localhost:${port2}`
|
|
668
|
+
}
|
|
669
|
+
),
|
|
670
|
+
jupyterStep === "error" && /* @__PURE__ */ jsx(Step, { status: "error", label: "Could not start notebook server" }),
|
|
671
|
+
tunnelStep === "error" && /* @__PURE__ */ jsx(Step, { status: "error", label: "Connecting to Sharpe" })
|
|
672
|
+
] }),
|
|
673
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
674
|
+
/* @__PURE__ */ jsx(Text, { color: "red", children: error.message }),
|
|
675
|
+
error.hint && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: error.hint }) })
|
|
165
676
|
] }),
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
677
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press Enter to try again or Ctrl+C to quit." }) })
|
|
678
|
+
] });
|
|
679
|
+
}
|
|
680
|
+
if (phase === "starting") {
|
|
681
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
|
|
682
|
+
/* @__PURE__ */ jsx(Header, {}),
|
|
683
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
684
|
+
/* @__PURE__ */ jsx(Step, { status: setupStep, label: "Setting up" }),
|
|
685
|
+
/* @__PURE__ */ jsx(
|
|
686
|
+
Step,
|
|
687
|
+
{
|
|
688
|
+
status: jupyterStep,
|
|
689
|
+
label: "Notebook server running",
|
|
690
|
+
detail: jupyterStep === "done" ? `http://localhost:${port2}` : void 0
|
|
691
|
+
}
|
|
692
|
+
),
|
|
693
|
+
/* @__PURE__ */ jsx(Step, { status: tunnelStep, label: "Connecting to Sharpe" })
|
|
169
694
|
] })
|
|
170
|
-
] })
|
|
171
|
-
|
|
695
|
+
] });
|
|
696
|
+
}
|
|
697
|
+
const footerText = !jupyterRunning ? "Press R to restart notebook, or Ctrl+C to quit." : "Press Ctrl+C when you're done.";
|
|
698
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
|
|
699
|
+
/* @__PURE__ */ jsx(Header, {}),
|
|
700
|
+
/* @__PURE__ */ jsx(Box, { justifyContent: "space-between", marginTop: 1, children: /* @__PURE__ */ jsx(StatusBadge, { status: overallStatus }) }),
|
|
701
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
172
702
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
173
|
-
/* @__PURE__ */ jsx(Text, {
|
|
174
|
-
/* @__PURE__ */ jsx(Text, { children:
|
|
175
|
-
] }),
|
|
176
|
-
phase.warning && /* @__PURE__ */ jsxs(Box, { children: [
|
|
177
|
-
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "Warning: " }),
|
|
178
|
-
/* @__PURE__ */ jsx(Text, { dimColor: true, children: phase.warning })
|
|
703
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Notebook " }),
|
|
704
|
+
jupyterRunning ? /* @__PURE__ */ jsx(Text, { children: `http://localhost:${port2}` }) : /* @__PURE__ */ jsx(Text, { color: "red", children: "Not running" })
|
|
179
705
|
] }),
|
|
180
|
-
/* @__PURE__ */
|
|
706
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
707
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Uptime " }),
|
|
708
|
+
/* @__PURE__ */ jsx(Text, { children: uptime })
|
|
709
|
+
] })
|
|
181
710
|
] }),
|
|
182
|
-
|
|
183
|
-
/* @__PURE__ */
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
711
|
+
events.length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
712
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }),
|
|
713
|
+
events.map((evt, i) => /* @__PURE__ */ jsxs(Box, { children: [
|
|
714
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
715
|
+
evt.time,
|
|
716
|
+
" "
|
|
717
|
+
] }),
|
|
718
|
+
/* @__PURE__ */ jsx(Text, { children: evt.message })
|
|
719
|
+
] }, i))
|
|
720
|
+
] }),
|
|
721
|
+
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: footerText }) })
|
|
722
|
+
] });
|
|
723
|
+
}
|
|
724
|
+
function Header() {
|
|
725
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
726
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: LOGO }),
|
|
727
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "Sharpe Connect" })
|
|
728
|
+
] });
|
|
729
|
+
}
|
|
730
|
+
function Step({ status, label, detail }) {
|
|
731
|
+
const icon = status === "done" ? /* @__PURE__ */ jsx(Text, { color: "green", children: "v" }) : status === "active" ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: /* @__PURE__ */ jsx(Spinner, { type: "dots" }) }) : status === "error" ? /* @__PURE__ */ jsx(Text, { color: "red", children: "x" }) : status === "skipped" ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "-" }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "." });
|
|
732
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
733
|
+
icon,
|
|
734
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
735
|
+
" ",
|
|
736
|
+
label
|
|
737
|
+
] }),
|
|
738
|
+
detail && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
739
|
+
" ",
|
|
740
|
+
detail
|
|
188
741
|
] })
|
|
189
742
|
] });
|
|
190
743
|
}
|
|
744
|
+
function StatusBadge({ status }) {
|
|
745
|
+
switch (status) {
|
|
746
|
+
case "connected":
|
|
747
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
748
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "\u25CF " }),
|
|
749
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "Connected" })
|
|
750
|
+
] });
|
|
751
|
+
case "reconnecting":
|
|
752
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
753
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25CF " }),
|
|
754
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "Reconnecting..." })
|
|
755
|
+
] });
|
|
756
|
+
case "notebook-stopped":
|
|
757
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
758
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u25CF " }),
|
|
759
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: "Notebook stopped" })
|
|
760
|
+
] });
|
|
761
|
+
case "offline":
|
|
762
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
763
|
+
/* @__PURE__ */ jsx(Text, { color: "red", children: "\u25CF " }),
|
|
764
|
+
/* @__PURE__ */ jsx(Text, { color: "red", children: "Offline" })
|
|
765
|
+
] });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/index.tsx
|
|
770
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
191
771
|
function parseArgs(argv) {
|
|
192
772
|
const args = argv.slice(2);
|
|
193
|
-
let
|
|
194
|
-
let port2 =
|
|
773
|
+
let connectionCode2 = null;
|
|
774
|
+
let port2 = 8888;
|
|
195
775
|
for (let i = 0; i < args.length; i++) {
|
|
196
776
|
const arg = args[i];
|
|
197
|
-
if (
|
|
198
|
-
token2 = args[++i];
|
|
199
|
-
} else if ((arg === "--port" || arg === "-p") && args[i + 1]) {
|
|
200
|
-
port2 = parseInt(args[++i], 10);
|
|
201
|
-
} else if (arg === "--help" || arg === "-h") {
|
|
777
|
+
if (arg === "--help" || arg === "-h") {
|
|
202
778
|
console.log(`
|
|
203
|
-
Usage: sharpe-connect
|
|
779
|
+
Usage: sharpe-connect [connection-code] [options]
|
|
204
780
|
|
|
205
781
|
Options:
|
|
206
|
-
--
|
|
207
|
-
--
|
|
208
|
-
--help, -h Show this help message
|
|
782
|
+
--port, -p Notebook port (default: 8888)
|
|
783
|
+
--help, -h Show help
|
|
209
784
|
`);
|
|
210
785
|
process.exit(0);
|
|
211
786
|
}
|
|
787
|
+
if ((arg === "--port" || arg === "-p") && args[i + 1]) {
|
|
788
|
+
port2 = parseInt(args[++i], 10);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
if (!arg.startsWith("-") && connectionCode2 === null) {
|
|
792
|
+
connectionCode2 = arg;
|
|
793
|
+
}
|
|
212
794
|
}
|
|
213
|
-
|
|
214
|
-
console.error("Error: --token is required. Run with --help for usage.");
|
|
215
|
-
process.exit(1);
|
|
216
|
-
}
|
|
217
|
-
return { token: token2, port: port2 };
|
|
795
|
+
return { connectionCode: connectionCode2, port: port2 };
|
|
218
796
|
}
|
|
219
|
-
var {
|
|
220
|
-
render(/* @__PURE__ */
|
|
797
|
+
var { connectionCode, port } = parseArgs(process.argv);
|
|
798
|
+
render(/* @__PURE__ */ jsx2(App, { connectionCode, port }));
|