@sharpe-jupyter/connect 0.3.4 → 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.
Files changed (2) hide show
  1. package/dist/index.js +188 -99
  2. 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 execSync3, spawn } from "child_process";
147
- import { existsSync as existsSync3 } from "fs";
148
- import { join as join3 } from "path";
149
- import { homedir as homedir3, platform as platform3 } from "os";
150
- var SHARPE_DIR = join3(homedir3(), ".sharpe");
151
- var VENV_DIR = join3(SHARPE_DIR, "venv");
152
- var SHARPE_BIN_DIR = join3(SHARPE_DIR, "bin");
153
- var IS_WIN = platform3() === "win32";
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 join3(VENV_DIR, BIN_DIR, IS_WIN ? `${name}.exe` : name);
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 = execSync3(`${cmd} --version`, { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
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 existsSync3(venvBin("python"));
224
+ return existsSync4(venvBin("python"));
170
225
  }
171
226
  function createVenv(pythonCmd) {
172
- execSync3(`${pythonCmd} -m venv "${VENV_DIR}"`, { stdio: "ignore" });
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,36 +249,52 @@ function startJupyter(port2) {
194
249
  { stdio: ["ignore", "pipe", "pipe"], env }
195
250
  );
196
251
  }
197
- function findUv() {
198
- try {
199
- execSync3("uv --version", { stdio: ["ignore", "pipe", "ignore"] });
200
- return true;
201
- } catch {
202
- return false;
203
- }
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
+ );
204
280
  }
205
281
  function detectProjectDeps() {
206
282
  const cwd = process.cwd();
207
- const reqTxt = join3(cwd, "requirements.txt");
208
- if (existsSync3(reqTxt)) {
283
+ const reqTxt = join4(cwd, "requirements.txt");
284
+ if (existsSync4(reqTxt)) {
209
285
  return { file: "requirements.txt", installArgs: ["-r", reqTxt] };
210
286
  }
211
- const pyproject = join3(cwd, "pyproject.toml");
212
- if (existsSync3(pyproject)) {
287
+ const pyproject = join4(cwd, "pyproject.toml");
288
+ if (existsSync4(pyproject)) {
213
289
  return { file: "pyproject.toml", installArgs: ["-e", cwd] };
214
290
  }
215
- const setupPy = join3(cwd, "setup.py");
216
- if (existsSync3(setupPy)) {
291
+ const setupPy = join4(cwd, "setup.py");
292
+ if (existsSync4(setupPy)) {
217
293
  return { file: "setup.py", installArgs: ["-e", cwd] };
218
294
  }
219
295
  return null;
220
296
  }
221
- function installProjectDeps(deps, useUv) {
222
- if (useUv) {
223
- return spawn("uv", ["pip", "install", "--python", venvBin("python"), ...deps.installArgs], {
224
- stdio: ["ignore", "pipe", "pipe"]
225
- });
226
- }
297
+ function installProjectDeps(deps) {
227
298
  return spawn(venvBin("pip"), ["install", ...deps.installArgs], {
228
299
  stdio: ["ignore", "pipe", "pipe"]
229
300
  });
@@ -359,7 +430,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
359
430
  }
360
431
  setJupyterRunning(false);
361
432
  pushEvent("Restarting notebook server...");
362
- const proc = startJupyter(port2);
433
+ const proc = isUvProject() ? startJupyterWithUv(port2, getUvPath()) : startJupyter(port2);
363
434
  jupyterProc.current = proc;
364
435
  didWeSpawnJupyter.current = true;
365
436
  let stderrBuf = "";
@@ -436,7 +507,7 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
436
507
  let cancelled = false;
437
508
  async function run() {
438
509
  try {
439
- await Promise.all([ensureCloudflared(), ensureRipgrep()]);
510
+ await Promise.all([ensureCloudflared(), ensureRipgrep(), ensureUv()]);
440
511
  } catch {
441
512
  if (cancelled) return;
442
513
  setSetupStep("error");
@@ -456,6 +527,22 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
456
527
  setJupyterRunning(true);
457
528
  didWeSpawnJupyter.current = false;
458
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");
459
546
  } else {
460
547
  const pythonCmd = findPython();
461
548
  if (!pythonCmd) {
@@ -503,19 +590,17 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
503
590
  pushEvent("Jupyter installed");
504
591
  const detected = detectProjectDeps();
505
592
  if (detected && !cancelled) {
506
- const useUv = findUv();
507
- setDepsPrompt({ file: detected.file, useUv });
593
+ setDepsPrompt({ file: detected.file });
508
594
  const shouldInstall = await new Promise((resolve) => {
509
595
  depsResolverRef.current = resolve;
510
596
  });
511
597
  setDepsPrompt(null);
512
598
  if (cancelled) return;
513
599
  if (shouldInstall) {
514
- const toolLabel = useUv ? "uv" : "pip";
515
- setDepsStatus(`Installing from ${detected.file} using ${toolLabel}...`);
600
+ setDepsStatus(`Installing from ${detected.file} using pip...`);
516
601
  pushEvent(`Installing from ${detected.file}...`);
517
602
  const depsResult = await new Promise((resolve) => {
518
- const depsProc = installProjectDeps(detected, useUv);
603
+ const depsProc = installProjectDeps(detected);
519
604
  depsProc.on("error", (err) => resolve(err.message));
520
605
  depsProc.on("exit", (code) => resolve(code === 0 ? null : "deps-install-failed"));
521
606
  });
@@ -534,66 +619,12 @@ function App({ connectionCode: connectionCode2, port: port2 }) {
534
619
  const proc = startJupyter(port2);
535
620
  jupyterProc.current = proc;
536
621
  didWeSpawnJupyter.current = true;
537
- const spawnError = await new Promise((resolve) => {
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
- });
622
+ const spawnError = await waitForJupyterHealthy(proc, port2, pushEvent, () => cancelled);
571
623
  if (cancelled) return;
572
- if (spawnError === "port-conflict") {
573
- setJupyterStep("error");
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") {
624
+ const errorInfo = handleJupyterSpawnError(spawnError, port2);
625
+ if (errorInfo) {
592
626
  setJupyterStep("error");
593
- setError({
594
- message: "Could not start the notebook server.",
595
- hint: "Something went wrong starting Jupyter. Try running again."
596
- });
627
+ setError(errorInfo);
597
628
  return;
598
629
  }
599
630
  setJupyterRunning(true);
@@ -785,9 +816,7 @@ Close whatever is using it and try again, or use a different port:
785
816
  " in this directory."
786
817
  ] }),
787
818
  /* @__PURE__ */ jsxs(Text, { children: [
788
- "Install dependencies",
789
- depsPrompt.useUv ? " using uv" : "",
790
- "? ",
819
+ "Install dependencies? ",
791
820
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[Y/n]" })
792
821
  ] })
793
822
  ] }),
@@ -865,6 +894,66 @@ function StatusBadge({ status }) {
865
894
  ] });
866
895
  }
867
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
+ }
868
957
 
869
958
  // src/index.tsx
870
959
  import { jsx as jsx2 } from "react/jsx-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sharpe-jupyter/connect",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Connect a local JupyterHub to Sharpe via Cloudflare Tunnel",
5
5
  "type": "module",
6
6
  "bin": {