trajectories-sh 1.5.1 → 1.6.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
@@ -81,7 +81,7 @@ program.command("upload <directory>").description("Upload a trajectory job direc
81
81
  Pushing ${directory} \u2192 ${chalk.cyan(slug)}
82
82
  `));
83
83
  try {
84
- const { uploadJob } = await import("./upload-3VEY2SEN.js");
84
+ const { uploadJob } = await import("./upload-NTDQN32Q.js");
85
85
  const result = await uploadJob(directory, {
86
86
  slug,
87
87
  name: opts.name,
@@ -107,7 +107,7 @@ program.command("download <url>").description("Download a trajectory job from tr
107
107
  Downloading trajectory...
108
108
  `));
109
109
  try {
110
- const { downloadJob, parseJobId } = await import("./download-YWW2BV6K.js");
110
+ const { downloadJob, parseJobId } = await import("./download-MFG34QU2.js");
111
111
  const outputDir = opts.output ?? parseJobId(url).replace(/^org:/, "").replace(/:/g, "-");
112
112
  const result = await downloadJob(url, outputDir);
113
113
  console.log(chalk.green.bold(` \u2713 Downloaded ${result.files} files`));
@@ -121,6 +121,68 @@ program.command("download <url>").description("Download a trajectory job from tr
121
121
  process.exit(1);
122
122
  }
123
123
  });
124
+ program.command("delete <url>").description("Delete a trajectory job from trajectories.sh").option("-y, --yes", "Skip confirmation prompt").action(async (url, opts) => {
125
+ const chalk = (await import("chalk")).default;
126
+ const { parseJobId } = await import("./download-MFG34QU2.js");
127
+ const { getApiUrl, getAuthHeader } = await import("./config-NOIAMV2U.js");
128
+ let jobId = parseJobId(url);
129
+ if (jobId.startsWith("org:")) {
130
+ console.error(chalk.red("\n \u2717 Please use a direct job URL or UUID for deletion.\n"));
131
+ process.exit(1);
132
+ }
133
+ const auth2 = await getAuthHeader();
134
+ if (!auth2) {
135
+ console.error(chalk.red(" \u2717 Not authenticated. Run: trajectories auth login"));
136
+ process.exit(1);
137
+ }
138
+ const apiUrl = getApiUrl();
139
+ const headers = { Authorization: auth2 };
140
+ try {
141
+ const previewResp = await fetch(`${apiUrl}/api/cli/delete/${jobId}/preview`, { headers });
142
+ if (!previewResp.ok) {
143
+ const text = await previewResp.text();
144
+ throw new Error(text);
145
+ }
146
+ const preview = await previewResp.json();
147
+ const sizeMB = (preview.storage_bytes / 1024 / 1024).toFixed(1);
148
+ console.log(chalk.bold(`
149
+ About to delete:
150
+ `));
151
+ console.log(` Name: ${preview.name}`);
152
+ console.log(` Slug: ${preview.slug}`);
153
+ console.log(` Trials: ${preview.n_trials}`);
154
+ console.log(` Files: ${preview.file_count}`);
155
+ console.log(` Storage: ${sizeMB} MB
156
+ `);
157
+ if (!opts.yes) {
158
+ const readline = await import("readline");
159
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
160
+ const answer = await new Promise((resolve) => {
161
+ rl.question(chalk.yellow(" Are you sure? (y/N) "), resolve);
162
+ });
163
+ rl.close();
164
+ if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
165
+ console.log(chalk.dim("\n Cancelled.\n"));
166
+ process.exit(0);
167
+ }
168
+ }
169
+ const deleteResp = await fetch(`${apiUrl}/api/cli/delete/${jobId}`, {
170
+ method: "DELETE",
171
+ headers
172
+ });
173
+ if (!deleteResp.ok) throw new Error(await deleteResp.text());
174
+ const result = await deleteResp.json();
175
+ console.log(chalk.green.bold(`
176
+ \u2713 Deleted successfully`));
177
+ console.log(` Freed: ${(result.storage_freed_bytes / 1024 / 1024).toFixed(1)} MB
178
+ `);
179
+ } catch (e) {
180
+ console.error(chalk.red(`
181
+ \u2717 ${e.message}
182
+ `));
183
+ process.exit(1);
184
+ }
185
+ });
124
186
  program.command("whoami").description("Show current authentication status").action(async () => {
125
187
  const chalk = (await import("chalk")).default;
126
188
  const { status } = await import("./auth-UD3KTSHK.js");
@@ -0,0 +1,125 @@
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
+ var BATCH_SIZE = 30;
11
+ var CONCURRENCY = 12;
12
+ function parseJobId(input) {
13
+ const trimmed = input.trim();
14
+ if (UUID_RE.test(trimmed) && !trimmed.includes("/")) {
15
+ return trimmed.match(UUID_RE)[0];
16
+ }
17
+ const trajMatch = trimmed.match(/\/trajectories\/([0-9a-f-]{36})/i);
18
+ if (trajMatch) return trajMatch[1];
19
+ const trajViewerMatch = trimmed.match(/\/traj\/([0-9a-f-]{36})\/([^/]+)/i);
20
+ if (trajViewerMatch) {
21
+ return `org:${trajViewerMatch[1]}:${trajViewerMatch[2]}`;
22
+ }
23
+ const anyUuid = trimmed.match(UUID_RE);
24
+ if (anyUuid) return anyUuid[0];
25
+ throw new Error(`Could not find a trajectory job ID in: ${input}`);
26
+ }
27
+ async function downloadJob(input, outputDir) {
28
+ const apiUrl = getApiUrl();
29
+ const auth = await getAuthHeader();
30
+ const headers = {};
31
+ if (auth) headers["Authorization"] = auth;
32
+ let jobId = parseJobId(input);
33
+ if (jobId.startsWith("org:")) {
34
+ const [, orgId, slug2] = jobId.split(":");
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
+ }
45
+ const listResp = await fetch(`${apiUrl}/api/cli/download/${jobId}/files`, {
46
+ headers,
47
+ signal: AbortSignal.timeout(3e4)
48
+ });
49
+ if (!listResp.ok) {
50
+ throw new Error(`Failed to list files: ${await listResp.text()}`);
51
+ }
52
+ const { slug, files } = await listResp.json();
53
+ const totalBytes = files.reduce((s, f) => s + f.size, 0);
54
+ console.log(` Job: ${slug} (${jobId})`);
55
+ console.log(` ${files.length} files, ${(totalBytes / 1024 / 1024).toFixed(1)} MB`);
56
+ mkdirSync(outputDir, { recursive: true });
57
+ const { ProgressBar } = await import("./progress-2P5L7X24.js");
58
+ const bar = new ProgressBar({ total: files.length, totalBytes, label: "\u2193" });
59
+ let downloaded = 0;
60
+ let totalSize = 0;
61
+ for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
62
+ const batch = files.slice(batchStart, batchStart + BATCH_SIZE);
63
+ const urlResp = await fetch(`${apiUrl}/api/cli/download/${jobId}/signed-urls`, {
64
+ method: "POST",
65
+ headers: { ...headers, "Content-Type": "application/json" },
66
+ body: JSON.stringify({ paths: batch.map((f) => f.path) }),
67
+ signal: AbortSignal.timeout(3e4)
68
+ });
69
+ let urlMap = /* @__PURE__ */ new Map();
70
+ if (urlResp.ok) {
71
+ const { urls } = await urlResp.json();
72
+ for (const u of urls) {
73
+ if (u.signed_url) urlMap.set(u.path, u.signed_url);
74
+ }
75
+ }
76
+ const pending = [];
77
+ for (const file of batch) {
78
+ const signedUrl = urlMap.get(file.path);
79
+ const downloadUrl = signedUrl ?? `${apiUrl}/api/cli/download/${jobId}/file?path=${encodeURIComponent(file.path)}`;
80
+ const downloadHeaders = signedUrl ? {} : headers;
81
+ const task = (async () => {
82
+ for (let attempt = 0; attempt < 3; attempt++) {
83
+ try {
84
+ const resp = await fetch(downloadUrl, {
85
+ headers: downloadHeaders,
86
+ signal: AbortSignal.timeout(12e4)
87
+ });
88
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
89
+ const data = Buffer.from(await resp.arrayBuffer());
90
+ const outPath = join(outputDir, file.path);
91
+ mkdirSync(dirname(outPath), { recursive: true });
92
+ writeFileSync(outPath, data);
93
+ downloaded++;
94
+ totalSize += data.length;
95
+ bar.tick(data.length);
96
+ return;
97
+ } catch {
98
+ if (attempt < 2) await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
99
+ }
100
+ }
101
+ bar.tick(0);
102
+ })();
103
+ pending.push(task);
104
+ if (pending.length >= CONCURRENCY) {
105
+ await Promise.race(pending);
106
+ const next = [];
107
+ for (const p of pending) {
108
+ let settled = false;
109
+ p.then(() => settled = true, () => settled = true);
110
+ if (!settled) next.push(p);
111
+ }
112
+ if (next.length === pending.length) await Promise.race(pending);
113
+ pending.length = 0;
114
+ pending.push(...next);
115
+ }
116
+ }
117
+ await Promise.all(pending);
118
+ }
119
+ bar.done();
120
+ return { files: downloaded, totalSize };
121
+ }
122
+ export {
123
+ downloadJob,
124
+ parseJobId
125
+ };
@@ -0,0 +1,56 @@
1
+ // src/progress.ts
2
+ var ProgressBar = class {
3
+ total;
4
+ totalBytes;
5
+ current = 0;
6
+ currentBytes = 0;
7
+ startTime;
8
+ label;
9
+ constructor(opts) {
10
+ this.total = opts.total;
11
+ this.totalBytes = opts.totalBytes;
12
+ this.label = opts.label;
13
+ this.startTime = Date.now();
14
+ }
15
+ tick(bytes = 0) {
16
+ this.current++;
17
+ this.currentBytes += bytes;
18
+ this.render();
19
+ }
20
+ render() {
21
+ const pct = this.total > 0 ? Math.round(this.current / this.total * 100) : 0;
22
+ const elapsed = (Date.now() - this.startTime) / 1e3;
23
+ const bytesPerSec = elapsed > 0 ? this.currentBytes / elapsed : 0;
24
+ const sizeDone = formatBytes(this.currentBytes);
25
+ const sizeTotal = formatBytes(this.totalBytes);
26
+ const speed = formatBytes(bytesPerSec) + "/s";
27
+ let eta = "";
28
+ if (bytesPerSec > 0 && this.currentBytes < this.totalBytes) {
29
+ const remaining = (this.totalBytes - this.currentBytes) / bytesPerSec;
30
+ eta = remaining < 60 ? `${remaining.toFixed(0)}s` : `${(remaining / 60).toFixed(1)}m`;
31
+ }
32
+ const barWidth = 20;
33
+ const filled = Math.round(this.current / this.total * barWidth);
34
+ const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
35
+ process.stdout.write(
36
+ ` ${this.label} ${bar} ${pct}% ${this.current}/${this.total} ${sizeDone}/${sizeTotal} ${speed}${eta ? ` ETA ${eta}` : ""}\r`
37
+ );
38
+ }
39
+ done() {
40
+ const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
41
+ const size = formatBytes(this.currentBytes);
42
+ process.stdout.write(
43
+ ` ${this.label} ${"\u2588".repeat(20)} 100% ${this.current}/${this.total} ${size} in ${elapsed}s
44
+ `
45
+ );
46
+ }
47
+ };
48
+ function formatBytes(bytes) {
49
+ if (bytes < 1024) return `${bytes}B`;
50
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`;
51
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
52
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)}GB`;
53
+ }
54
+ export {
55
+ ProgressBar
56
+ };
@@ -206,7 +206,9 @@ async function uploadJob(dir, opts) {
206
206
  };
207
207
  }
208
208
  async function uploadDirect(jobId, files) {
209
- let uploaded = 0;
209
+ const { ProgressBar } = await import("./progress-2P5L7X24.js");
210
+ const totalBytes = files.reduce((s, f) => s + f.size, 0);
211
+ const bar = new ProgressBar({ total: files.length, totalBytes, label: "\u2191" });
210
212
  let errors = 0;
211
213
  for (let batchStart = 0; batchStart < files.length; batchStart += BATCH_SIZE) {
212
214
  const batch = files.slice(batchStart, batchStart + BATCH_SIZE);
@@ -222,7 +224,7 @@ async function uploadDirect(jobId, files) {
222
224
  if (u.signed_url) urlMap.set(u.path, u.signed_url);
223
225
  else {
224
226
  errors++;
225
- console.log(` \u2717 ${u.path} (no signed URL: ${u.error})`);
227
+ bar.tick(0);
226
228
  }
227
229
  }
228
230
  const pending = [];
@@ -233,14 +235,10 @@ async function uploadDirect(jobId, files) {
233
235
  const task = (async () => {
234
236
  const ok = await uploadOneFile(file, signedUrl);
235
237
  if (ok) {
236
- uploaded++;
238
+ bar.tick(file.size);
237
239
  } else {
238
240
  errors++;
239
- console.log(` \u2717 ${file.relPath}`);
240
- }
241
- const total = uploaded + errors;
242
- if (total % 10 === 0 || total === files.length) {
243
- process.stdout.write(` \u2191 ${uploaded}/${files.length} uploaded\r`);
241
+ bar.tick(0);
244
242
  }
245
243
  })();
246
244
  pending.push(task);
@@ -257,11 +255,9 @@ async function uploadDirect(jobId, files) {
257
255
  }
258
256
  await Promise.all(pending);
259
257
  }
260
- console.log("");
258
+ bar.done();
261
259
  if (errors > 0) {
262
- console.log(` Uploaded: ${uploaded}, Errors: ${errors}`);
263
- } else {
264
- console.log(` \u2713 All ${uploaded} files uploaded directly to storage`);
260
+ console.log(` \u26A0 ${errors} file(s) failed to upload`);
265
261
  }
266
262
  }
267
263
  async function uploadOneFile(file, signedUrl) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trajectories-sh",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "description": "CLI for uploading trajectory jobs to trajectories.sh",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,111 +0,0 @@
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
- };