trajectories-sh 1.2.2 → 1.3.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/cli.js CHANGED
@@ -101,6 +101,26 @@ program.command("upload <directory>").description("Upload a trajectory job direc
101
101
  process.exit(1);
102
102
  }
103
103
  });
104
+ program.command("download <url>").description("Download a trajectory job from trajectories.sh").option("-o, --output <dir>", "Output directory (default: slug name)").action(async (url, opts) => {
105
+ const chalk = (await import("chalk")).default;
106
+ console.log(chalk.bold(`
107
+ Downloading trajectory...
108
+ `));
109
+ try {
110
+ const { downloadJob, parseJobId } = await import("./download-YWW2BV6K.js");
111
+ const outputDir = opts.output ?? parseJobId(url).replace(/^org:/, "").replace(/:/g, "-");
112
+ const result = await downloadJob(url, outputDir);
113
+ console.log(chalk.green.bold(` \u2713 Downloaded ${result.files} files`));
114
+ console.log(` Size: ${(result.totalSize / 1024 / 1024).toFixed(1)} MB`);
115
+ console.log(` Output: ${outputDir}
116
+ `);
117
+ } catch (e) {
118
+ console.error(chalk.red(`
119
+ \u2717 ${e.message}
120
+ `));
121
+ process.exit(1);
122
+ }
123
+ });
104
124
  program.command("whoami").description("Show current authentication status").action(async () => {
105
125
  const chalk = (await import("chalk")).default;
106
126
  const { status } = await import("./auth-UD3KTSHK.js");
@@ -0,0 +1,111 @@
1
+ import {
2
+ getApiUrl,
3
+ getAuthHeader
4
+ } from "./chunk-P6J5X7LP.js";
5
+
6
+ // src/download.ts
7
+ import { mkdirSync, writeFileSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ var UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
10
+ function parseJobId(input) {
11
+ const trimmed = input.trim();
12
+ if (UUID_RE.test(trimmed) && !trimmed.includes("/")) {
13
+ return trimmed.match(UUID_RE)[0];
14
+ }
15
+ const trajMatch = trimmed.match(/\/trajectories\/([0-9a-f-]{36})/i);
16
+ if (trajMatch) return trajMatch[1];
17
+ const trajViewerMatch = trimmed.match(/\/traj\/([0-9a-f-]{36})\/([^/]+)/i);
18
+ if (trajViewerMatch) {
19
+ return `org:${trajViewerMatch[1]}:${trajViewerMatch[2]}`;
20
+ }
21
+ const anyUuid = trimmed.match(UUID_RE);
22
+ if (anyUuid) return anyUuid[0];
23
+ throw new Error(`Could not find a trajectory job ID in: ${input}`);
24
+ }
25
+ async function downloadJob(input, outputDir) {
26
+ const apiUrl = getApiUrl();
27
+ const auth = await getAuthHeader();
28
+ const headers = {};
29
+ if (auth) headers["Authorization"] = auth;
30
+ let jobId = parseJobId(input);
31
+ if (jobId.startsWith("org:")) {
32
+ const [, orgId, slug2] = jobId.split(":");
33
+ const resp = await fetch(`${apiUrl}/api/org/trajectories/public/${orgId}?slug=${slug2}`, { headers });
34
+ if (!resp.ok) {
35
+ const configResp = await fetch(`${apiUrl}/api/viewer/${orgId}/${slug2}/api/jobs`, { headers });
36
+ if (!configResp.ok) throw new Error(`Could not resolve job: ${orgId}/${slug2}`);
37
+ const configData = await configResp.json();
38
+ const items = configData.items || [];
39
+ if (items.length > 0 && items[0].id) {
40
+ jobId = items[0].id;
41
+ } else {
42
+ throw new Error(`Could not resolve job ID for ${orgId}/${slug2}`);
43
+ }
44
+ } else {
45
+ const data = await resp.json();
46
+ jobId = data.id;
47
+ }
48
+ }
49
+ const listResp = await fetch(`${apiUrl}/api/cli/download/${jobId}/files`, {
50
+ headers,
51
+ signal: AbortSignal.timeout(3e4)
52
+ });
53
+ if (!listResp.ok) {
54
+ const text = await listResp.text();
55
+ throw new Error(`Failed to list files: ${text}`);
56
+ }
57
+ const { slug, files } = await listResp.json();
58
+ console.log(` Job: ${slug} (${jobId})`);
59
+ console.log(` ${files.length} files to download`);
60
+ mkdirSync(outputDir, { recursive: true });
61
+ let downloaded = 0;
62
+ let totalSize = 0;
63
+ const concurrency = 8;
64
+ const pending = [];
65
+ for (const file of files) {
66
+ const task = (async () => {
67
+ for (let attempt = 0; attempt < 3; attempt++) {
68
+ try {
69
+ const fileResp = await fetch(
70
+ `${apiUrl}/api/cli/download/${jobId}/file?path=${encodeURIComponent(file.path)}`,
71
+ { headers, signal: AbortSignal.timeout(6e4) }
72
+ );
73
+ if (!fileResp.ok) throw new Error(`HTTP ${fileResp.status}`);
74
+ const data = Buffer.from(await fileResp.arrayBuffer());
75
+ const outPath = join(outputDir, file.path);
76
+ mkdirSync(dirname(outPath), { recursive: true });
77
+ writeFileSync(outPath, data);
78
+ downloaded++;
79
+ totalSize += data.length;
80
+ if (downloaded % 10 === 0 || downloaded === files.length) {
81
+ process.stdout.write(` \u2193 ${downloaded}/${files.length} downloaded\r`);
82
+ }
83
+ return;
84
+ } catch {
85
+ if (attempt < 2) await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
86
+ }
87
+ }
88
+ console.log(` \u2717 ${file.path}`);
89
+ })();
90
+ pending.push(task);
91
+ if (pending.length >= concurrency) {
92
+ await Promise.race(pending);
93
+ const next = [];
94
+ for (const p of pending) {
95
+ let settled = false;
96
+ p.then(() => settled = true, () => settled = true);
97
+ if (!settled) next.push(p);
98
+ }
99
+ if (next.length === pending.length) await Promise.race(pending);
100
+ pending.length = 0;
101
+ pending.push(...next);
102
+ }
103
+ }
104
+ await Promise.all(pending);
105
+ console.log("");
106
+ return { files: downloaded, totalSize };
107
+ }
108
+ export {
109
+ downloadJob,
110
+ parseJobId
111
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {