@sharpe-jupyter/connect 0.3.4 → 0.4.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/index.js +190 -99
- 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], {
|
|
@@ -189,41 +244,59 @@ function startJupyter(port2) {
|
|
|
189
244
|
"--ServerApp.password=",
|
|
190
245
|
"--ServerApp.allow_remote_access=True",
|
|
191
246
|
"--ServerApp.disable_check_xsrf=True",
|
|
247
|
+
"--ContentsManager.allow_hidden=True",
|
|
192
248
|
`--ServerApp.root_dir=${process.cwd()}`
|
|
193
249
|
],
|
|
194
250
|
{ stdio: ["ignore", "pipe", "pipe"], env }
|
|
195
251
|
);
|
|
196
252
|
}
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
253
|
+
function isUvProject() {
|
|
254
|
+
return existsSync4(join4(process.cwd(), "uv.lock"));
|
|
255
|
+
}
|
|
256
|
+
function startJupyterWithUv(port2, uvBin) {
|
|
257
|
+
const env = { ...process.env, PATH: `${SHARPE_BIN_DIR}:${process.env.PATH ?? ""}` };
|
|
258
|
+
return spawn(
|
|
259
|
+
uvBin,
|
|
260
|
+
[
|
|
261
|
+
"run",
|
|
262
|
+
"--with",
|
|
263
|
+
"jupyter",
|
|
264
|
+
"--with",
|
|
265
|
+
"jupyter-resource-usage",
|
|
266
|
+
"--with",
|
|
267
|
+
"sharpe-log-handler>=0.3.0",
|
|
268
|
+
"jupyter",
|
|
269
|
+
"notebook",
|
|
270
|
+
"--port",
|
|
271
|
+
String(port2),
|
|
272
|
+
"--no-browser",
|
|
273
|
+
"--ServerApp.token=",
|
|
274
|
+
"--ServerApp.password=",
|
|
275
|
+
"--ServerApp.allow_remote_access=True",
|
|
276
|
+
"--ServerApp.disable_check_xsrf=True",
|
|
277
|
+
"--ContentsManager.allow_hidden=True",
|
|
278
|
+
`--ServerApp.root_dir=${process.cwd()}`
|
|
279
|
+
],
|
|
280
|
+
{ stdio: ["ignore", "pipe", "pipe"], env }
|
|
281
|
+
);
|
|
204
282
|
}
|
|
205
283
|
function detectProjectDeps() {
|
|
206
284
|
const cwd = process.cwd();
|
|
207
|
-
const reqTxt =
|
|
208
|
-
if (
|
|
285
|
+
const reqTxt = join4(cwd, "requirements.txt");
|
|
286
|
+
if (existsSync4(reqTxt)) {
|
|
209
287
|
return { file: "requirements.txt", installArgs: ["-r", reqTxt] };
|
|
210
288
|
}
|
|
211
|
-
const pyproject =
|
|
212
|
-
if (
|
|
289
|
+
const pyproject = join4(cwd, "pyproject.toml");
|
|
290
|
+
if (existsSync4(pyproject)) {
|
|
213
291
|
return { file: "pyproject.toml", installArgs: ["-e", cwd] };
|
|
214
292
|
}
|
|
215
|
-
const setupPy =
|
|
216
|
-
if (
|
|
293
|
+
const setupPy = join4(cwd, "setup.py");
|
|
294
|
+
if (existsSync4(setupPy)) {
|
|
217
295
|
return { file: "setup.py", installArgs: ["-e", cwd] };
|
|
218
296
|
}
|
|
219
297
|
return null;
|
|
220
298
|
}
|
|
221
|
-
function installProjectDeps(deps
|
|
222
|
-
if (useUv) {
|
|
223
|
-
return spawn("uv", ["pip", "install", "--python", venvBin("python"), ...deps.installArgs], {
|
|
224
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
225
|
-
});
|
|
226
|
-
}
|
|
299
|
+
function installProjectDeps(deps) {
|
|
227
300
|
return spawn(venvBin("pip"), ["install", ...deps.installArgs], {
|
|
228
301
|
stdio: ["ignore", "pipe", "pipe"]
|
|
229
302
|
});
|
|
@@ -359,7 +432,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
359
432
|
}
|
|
360
433
|
setJupyterRunning(false);
|
|
361
434
|
pushEvent("Restarting notebook server...");
|
|
362
|
-
const proc = startJupyter(port2);
|
|
435
|
+
const proc = isUvProject() ? startJupyterWithUv(port2, getUvPath()) : startJupyter(port2);
|
|
363
436
|
jupyterProc.current = proc;
|
|
364
437
|
didWeSpawnJupyter.current = true;
|
|
365
438
|
let stderrBuf = "";
|
|
@@ -436,7 +509,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
436
509
|
let cancelled = false;
|
|
437
510
|
async function run() {
|
|
438
511
|
try {
|
|
439
|
-
await Promise.all([ensureCloudflared(), ensureRipgrep()]);
|
|
512
|
+
await Promise.all([ensureCloudflared(), ensureRipgrep(), ensureUv()]);
|
|
440
513
|
} catch {
|
|
441
514
|
if (cancelled) return;
|
|
442
515
|
setSetupStep("error");
|
|
@@ -456,6 +529,22 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
456
529
|
setJupyterRunning(true);
|
|
457
530
|
didWeSpawnJupyter.current = false;
|
|
458
531
|
pushEvent("Existing notebook server detected");
|
|
532
|
+
} else if (isUvProject()) {
|
|
533
|
+
pushEvent("Installing dependencies and starting notebook...");
|
|
534
|
+
const uvPath = getUvPath();
|
|
535
|
+
const proc = startJupyterWithUv(port2, uvPath);
|
|
536
|
+
jupyterProc.current = proc;
|
|
537
|
+
didWeSpawnJupyter.current = true;
|
|
538
|
+
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
|
|
539
|
+
if (cancelled) return;
|
|
540
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
541
|
+
if (errorInfo) {
|
|
542
|
+
setJupyterStep("error");
|
|
543
|
+
setError(errorInfo);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
setJupyterRunning(true);
|
|
547
|
+
pushEvent("Notebook server started");
|
|
459
548
|
} else {
|
|
460
549
|
const pythonCmd = findPython();
|
|
461
550
|
if (!pythonCmd) {
|
|
@@ -503,19 +592,17 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
503
592
|
pushEvent("Jupyter installed");
|
|
504
593
|
const detected = detectProjectDeps();
|
|
505
594
|
if (detected && !cancelled) {
|
|
506
|
-
|
|
507
|
-
setDepsPrompt({ file: detected.file, useUv });
|
|
595
|
+
setDepsPrompt({ file: detected.file });
|
|
508
596
|
const shouldInstall = await new Promise((resolve) => {
|
|
509
597
|
depsResolverRef.current = resolve;
|
|
510
598
|
});
|
|
511
599
|
setDepsPrompt(null);
|
|
512
600
|
if (cancelled) return;
|
|
513
601
|
if (shouldInstall) {
|
|
514
|
-
|
|
515
|
-
setDepsStatus(`Installing from ${detected.file} using ${toolLabel}...`);
|
|
602
|
+
setDepsStatus(`Installing from ${detected.file} using pip...`);
|
|
516
603
|
pushEvent(`Installing from ${detected.file}...`);
|
|
517
604
|
const depsResult = await new Promise((resolve) => {
|
|
518
|
-
const depsProc = installProjectDeps(detected
|
|
605
|
+
const depsProc = installProjectDeps(detected);
|
|
519
606
|
depsProc.on("error", (err) => resolve(err.message));
|
|
520
607
|
depsProc.on("exit", (code) => resolve(code === 0 ? null : "deps-install-failed"));
|
|
521
608
|
});
|
|
@@ -534,66 +621,12 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
|
|
|
534
621
|
const proc = startJupyter(port2);
|
|
535
622
|
jupyterProc.current = proc;
|
|
536
623
|
didWeSpawnJupyter.current = true;
|
|
537
|
-
const spawnError = await
|
|
538
|
-
proc.on("error", (err) => resolve(err.message));
|
|
539
|
-
let stderrBuf = "";
|
|
540
|
-
proc.stderr?.on("data", (data) => {
|
|
541
|
-
stderrBuf += data.toString();
|
|
542
|
-
const lines = stderrBuf.split("\n");
|
|
543
|
-
stderrBuf = lines.pop() ?? "";
|
|
544
|
-
for (const line of lines) {
|
|
545
|
-
if (line.includes("Address already in use")) {
|
|
546
|
-
resolve("port-conflict");
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
const msg = parseJupyterLine(line);
|
|
550
|
-
if (msg) pushEvent(msg);
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
let attempts = 0;
|
|
554
|
-
const poll = setInterval(async () => {
|
|
555
|
-
if (cancelled) {
|
|
556
|
-
clearInterval(poll);
|
|
557
|
-
resolve("cancelled");
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
attempts++;
|
|
561
|
-
const healthy = await checkJupyterHealth(port2);
|
|
562
|
-
if (healthy) {
|
|
563
|
-
clearInterval(poll);
|
|
564
|
-
resolve(null);
|
|
565
|
-
} else if (attempts >= 20) {
|
|
566
|
-
clearInterval(poll);
|
|
567
|
-
resolve("timeout");
|
|
568
|
-
}
|
|
569
|
-
}, 1e3);
|
|
570
|
-
});
|
|
624
|
+
const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
|
|
571
625
|
if (cancelled) return;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
setError({
|
|
575
|
-
message: "Could not start the notebook server.",
|
|
576
|
-
hint: `Port ${port2} is already in use.
|
|
577
|
-
Close whatever is using it and try again, or use a different port:
|
|
578
|
-
|
|
579
|
-
npx @sharpe-jupyter/connect CODE --port ${port2 + 1}`
|
|
580
|
-
});
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
if (spawnError === "timeout") {
|
|
584
|
-
setJupyterStep("error");
|
|
585
|
-
setError({
|
|
586
|
-
message: "The notebook server isn't responding.",
|
|
587
|
-
hint: `Try restarting or check if port ${port2} is available.`
|
|
588
|
-
});
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
if (spawnError && spawnError !== "cancelled") {
|
|
626
|
+
const errorInfo = handleJupyterSpawnError(spawnError, port2);
|
|
627
|
+
if (errorInfo) {
|
|
592
628
|
setJupyterStep("error");
|
|
593
|
-
setError(
|
|
594
|
-
message: "Could not start the notebook server.",
|
|
595
|
-
hint: "Something went wrong starting Jupyter. Try running again."
|
|
596
|
-
});
|
|
629
|
+
setError(errorInfo);
|
|
597
630
|
return;
|
|
598
631
|
}
|
|
599
632
|
setJupyterRunning(true);
|
|
@@ -785,9 +818,7 @@ Close whatever is using it and try again, or use a different port:
|
|
|
785
818
|
" in this directory."
|
|
786
819
|
] }),
|
|
787
820
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
788
|
-
"Install dependencies",
|
|
789
|
-
depsPrompt.useUv ? " using uv" : "",
|
|
790
|
-
"? ",
|
|
821
|
+
"Install dependencies? ",
|
|
791
822
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "[Y/n]" })
|
|
792
823
|
] })
|
|
793
824
|
] }),
|
|
@@ -865,6 +896,66 @@ function StatusBadge({ status }) {
|
|
|
865
896
|
] });
|
|
866
897
|
}
|
|
867
898
|
}
|
|
899
|
+
function waitForJupyterHealthy(proc, port2, pushEvent, isCancelled) {
|
|
900
|
+
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);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
let attempts = 0;
|
|
917
|
+
const poll = setInterval(async () => {
|
|
918
|
+
if (isCancelled()) {
|
|
919
|
+
clearInterval(poll);
|
|
920
|
+
resolve("cancelled");
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
attempts++;
|
|
924
|
+
const healthy = await checkJupyterHealth(port2);
|
|
925
|
+
if (healthy) {
|
|
926
|
+
clearInterval(poll);
|
|
927
|
+
resolve(null);
|
|
928
|
+
} else if (attempts >= 20) {
|
|
929
|
+
clearInterval(poll);
|
|
930
|
+
resolve("timeout");
|
|
931
|
+
}
|
|
932
|
+
}, 1e3);
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function handleJupyterSpawnError(spawnError, port2) {
|
|
936
|
+
if (spawnError === "port-conflict") {
|
|
937
|
+
return {
|
|
938
|
+
message: "Could not start the notebook server.",
|
|
939
|
+
hint: `Port ${port2} is already in use.
|
|
940
|
+
Close whatever is using it and try again, or use a different port:
|
|
941
|
+
|
|
942
|
+
npx @sharpe-jupyter/connect CODE --port ${port2 + 1}`
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
if (spawnError === "timeout") {
|
|
946
|
+
return {
|
|
947
|
+
message: "The notebook server isn't responding.",
|
|
948
|
+
hint: `Try restarting or check if port ${port2} is available.`
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
if (spawnError && spawnError !== "cancelled") {
|
|
952
|
+
return {
|
|
953
|
+
message: "Could not start the notebook server.",
|
|
954
|
+
hint: "Something went wrong starting Jupyter. Try running again."
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
868
959
|
|
|
869
960
|
// src/index.tsx
|
|
870
961
|
import { jsx as jsx2 } from "react/jsx-runtime";
|