trajectories-sh 1.9.9 → 1.10.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 @@ async function uploadTrajectoryAction(directory, opts) {
|
|
|
81
81
|
Uploading trajectory ${directory} \u2192 ${chalk.cyan(slug)}
|
|
82
82
|
`));
|
|
83
83
|
try {
|
|
84
|
-
const { uploadJob } = await import("./upload-
|
|
84
|
+
const { uploadJob } = await import("./upload-IBWVHRL2.js");
|
|
85
85
|
const result = await uploadJob(directory, {
|
|
86
86
|
slug,
|
|
87
87
|
name: opts.name,
|
|
@@ -145,7 +145,7 @@ async function downloadTrajectoryAction(url, opts) {
|
|
|
145
145
|
Downloading trajectory...
|
|
146
146
|
`));
|
|
147
147
|
try {
|
|
148
|
-
const { downloadJob, parseJobId } = await import("./download-
|
|
148
|
+
const { downloadJob, parseJobId } = await import("./download-O3JONHXT.js");
|
|
149
149
|
const outputDir = opts.output ?? parseJobId(url).replace(/^org:/, "").replace(/:/g, "-");
|
|
150
150
|
const result = await downloadJob(url, outputDir);
|
|
151
151
|
console.log(chalk.green.bold(` \u2713 Downloaded ${result.files} files`));
|
|
@@ -161,7 +161,7 @@ async function downloadTrajectoryAction(url, opts) {
|
|
|
161
161
|
}
|
|
162
162
|
async function deleteTrajectoryAction(url, opts) {
|
|
163
163
|
const chalk = (await import("chalk")).default;
|
|
164
|
-
const { parseJobId } = await import("./download-
|
|
164
|
+
const { parseJobId } = await import("./download-O3JONHXT.js");
|
|
165
165
|
const { getApiUrl, getAuthHeader } = await import("./config-NOIAMV2U.js");
|
|
166
166
|
let jobId = parseJobId(url);
|
|
167
167
|
if (jobId.startsWith("org:")) {
|
|
@@ -56,7 +56,7 @@ async function downloadJob(input, outputDir) {
|
|
|
56
56
|
console.log(` Job: ${slug} (${jobId})`);
|
|
57
57
|
console.log(` ${files.length} files, ${(totalBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
58
58
|
mkdirSync(outputDir, { recursive: true });
|
|
59
|
-
const { ProgressBar } = await import("./progress-
|
|
59
|
+
const { ProgressBar } = await import("./progress-H7WNKHDR.js");
|
|
60
60
|
const bar = new ProgressBar({ total: files.length, totalBytes, label: "\u2193" });
|
|
61
61
|
let downloaded = 0;
|
|
62
62
|
let totalSize = 0;
|
|
@@ -17,6 +17,16 @@ var ProgressBar = class {
|
|
|
17
17
|
this.currentBytes += bytes;
|
|
18
18
|
this.render();
|
|
19
19
|
}
|
|
20
|
+
/** Set absolute byte count (does not change `current`). Useful for streaming a single large file. */
|
|
21
|
+
setBytes(bytes) {
|
|
22
|
+
this.currentBytes = bytes;
|
|
23
|
+
this.render();
|
|
24
|
+
}
|
|
25
|
+
/** Set absolute file count (does not change `currentBytes`). */
|
|
26
|
+
setCount(count) {
|
|
27
|
+
this.current = count;
|
|
28
|
+
this.render();
|
|
29
|
+
}
|
|
20
30
|
render() {
|
|
21
31
|
const pct = this.total > 0 ? Math.round(this.current / this.total * 100) : 0;
|
|
22
32
|
const elapsed = (Date.now() - this.startTime) / 1e3;
|
|
@@ -11,7 +11,6 @@ import { randomBytes, createHash } from "crypto";
|
|
|
11
11
|
var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
|
|
12
12
|
var MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
13
13
|
var CONCURRENCY = Number(process.env.TRAJECTORIES_CLI_CONCURRENCY ?? "8");
|
|
14
|
-
var BATCH_SIZE = 100;
|
|
15
14
|
function collectFiles(dir) {
|
|
16
15
|
const files = [];
|
|
17
16
|
function walk(current) {
|
|
@@ -323,7 +322,7 @@ async function uploadJob(dir, opts) {
|
|
|
323
322
|
}
|
|
324
323
|
const { job_id: jobId } = await initResp.json();
|
|
325
324
|
console.log(` Job ID: ${jobId}`);
|
|
326
|
-
await
|
|
325
|
+
await uploadArchive(jobId, convertedDir);
|
|
327
326
|
console.log(" Finalizing...");
|
|
328
327
|
const finalResp = await apiPost(`/api/cli/push/${jobId}/finalize`, {});
|
|
329
328
|
if (!finalResp.ok) {
|
|
@@ -354,96 +353,149 @@ async function uploadJob(dir, opts) {
|
|
|
354
353
|
viewerUrl: `${siteUrl}/t/${jobId}`
|
|
355
354
|
};
|
|
356
355
|
}
|
|
357
|
-
async function
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
path: u.path,
|
|
379
|
-
reason: `no signed URL from server: ${u.error ?? "unknown"}`
|
|
380
|
-
});
|
|
381
|
-
bar.tick(0);
|
|
356
|
+
async function uploadArchive(jobId, dir) {
|
|
357
|
+
const tar = await import("tar");
|
|
358
|
+
const tmpArchive = join(tmpdir(), `trajectories-archive-${randomBytes(4).toString("hex")}.tar.gz`);
|
|
359
|
+
const entries = collectFiles(dir).map((f) => f.relPath);
|
|
360
|
+
await tar.create(
|
|
361
|
+
{
|
|
362
|
+
gzip: true,
|
|
363
|
+
file: tmpArchive,
|
|
364
|
+
cwd: dir,
|
|
365
|
+
portable: true
|
|
366
|
+
},
|
|
367
|
+
entries
|
|
368
|
+
);
|
|
369
|
+
const archiveSize = statSync(tmpArchive).size;
|
|
370
|
+
console.log(` Archive: ${(archiveSize / 1024 / 1024).toFixed(1)} MB (gzipped, ${entries.length} entries)`);
|
|
371
|
+
try {
|
|
372
|
+
const maxAttempts = 3;
|
|
373
|
+
let lastError = null;
|
|
374
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
375
|
+
if (attempt > 1) {
|
|
376
|
+
console.log(` Retrying archive upload (attempt ${attempt}/${maxAttempts})\u2026`);
|
|
382
377
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
bar.tick(0);
|
|
378
|
+
try {
|
|
379
|
+
await uploadArchiveOnce(jobId, tmpArchive, archiveSize, entries.length);
|
|
380
|
+
return;
|
|
381
|
+
} catch (e) {
|
|
382
|
+
lastError = e;
|
|
383
|
+
const msg = e?.message ?? String(e);
|
|
384
|
+
if (!/failed to store|extraction failed|extraction timed out/i.test(msg)) {
|
|
385
|
+
throw e;
|
|
386
|
+
}
|
|
387
|
+
if (attempt < maxAttempts) {
|
|
388
|
+
console.log(` \u26A0 ${msg}`);
|
|
389
|
+
await sleep(3e3 * attempt);
|
|
396
390
|
}
|
|
397
|
-
})().finally(() => {
|
|
398
|
-
pending.delete(task);
|
|
399
|
-
});
|
|
400
|
-
pending.add(task);
|
|
401
|
-
if (pending.size >= CONCURRENCY) {
|
|
402
|
-
await Promise.race(pending);
|
|
403
391
|
}
|
|
404
392
|
}
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
for (const { reason } of failureReasons) {
|
|
411
|
-
byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
|
|
393
|
+
throw lastError instanceof Error ? lastError : new Error(`Archive upload failed after ${maxAttempts} attempts`);
|
|
394
|
+
} finally {
|
|
395
|
+
try {
|
|
396
|
+
unlinkSync(tmpArchive);
|
|
397
|
+
} catch {
|
|
412
398
|
}
|
|
413
|
-
const reasonLines = [...byReason.entries()].sort((a, b) => b[1] - a[1]).map(([reason, count]) => ` ${count}\xD7 ${reason}`).join("\n");
|
|
414
|
-
const samplePath = failedFiles[0];
|
|
415
|
-
throw new Error(
|
|
416
|
-
`Upload incomplete: ${failedFiles.length}/${files.length} file(s) failed. First failed file: ${samplePath}.
|
|
417
|
-
Failure breakdown:
|
|
418
|
-
${reasonLines}
|
|
419
|
-
Retry the upload or check your network connection.`
|
|
420
|
-
);
|
|
421
399
|
}
|
|
422
400
|
}
|
|
423
|
-
async function
|
|
424
|
-
const
|
|
425
|
-
|
|
401
|
+
async function uploadArchiveOnce(jobId, archivePath, archiveSize, entryCount) {
|
|
402
|
+
const { createReadStream } = await import("fs");
|
|
403
|
+
const { ProgressBar } = await import("./progress-H7WNKHDR.js");
|
|
404
|
+
const urlResp = await apiPost(`/api/cli/push/${jobId}/upload-url`, {});
|
|
405
|
+
if (!urlResp.ok) {
|
|
406
|
+
throw new Error(`Failed to get archive upload URL: ${await urlResp.text()}`);
|
|
407
|
+
}
|
|
408
|
+
const { signed_url: signedUrl } = await urlResp.json();
|
|
409
|
+
if (!signedUrl) {
|
|
410
|
+
throw new Error("Server returned no signed URL for archive upload");
|
|
411
|
+
}
|
|
412
|
+
let putError = null;
|
|
426
413
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
414
|
+
const bar = new ProgressBar({ total: 1, totalBytes: archiveSize, label: "\u2191 archive" });
|
|
415
|
+
const stream = createReadStream(archivePath);
|
|
416
|
+
let bytesSent = 0;
|
|
417
|
+
stream.on("data", (chunk) => {
|
|
418
|
+
bytesSent += chunk.length;
|
|
419
|
+
bar.setBytes(bytesSent);
|
|
420
|
+
});
|
|
427
421
|
try {
|
|
428
422
|
const resp = await fetch(signedUrl, {
|
|
429
423
|
method: "PUT",
|
|
430
|
-
headers: {
|
|
431
|
-
|
|
432
|
-
|
|
424
|
+
headers: {
|
|
425
|
+
"Content-Type": "application/gzip",
|
|
426
|
+
"Content-Length": String(archiveSize)
|
|
427
|
+
},
|
|
428
|
+
// undici accepts Node streams here
|
|
429
|
+
body: stream,
|
|
430
|
+
duplex: "half",
|
|
431
|
+
signal: AbortSignal.timeout(30 * 60 * 1e3)
|
|
433
432
|
});
|
|
434
|
-
|
|
435
|
-
|
|
433
|
+
bar.setCount(1);
|
|
434
|
+
bar.done();
|
|
435
|
+
if (resp.ok) {
|
|
436
|
+
putError = null;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
putError = `HTTP ${resp.status} ${resp.statusText}`;
|
|
436
440
|
try {
|
|
437
441
|
const body = await resp.text();
|
|
438
|
-
if (body)
|
|
442
|
+
if (body) putError += ` \u2014 ${body.slice(0, 200).replace(/\s+/g, " ")}`;
|
|
439
443
|
} catch {
|
|
440
444
|
}
|
|
441
445
|
} catch (e) {
|
|
442
|
-
|
|
446
|
+
bar.done();
|
|
447
|
+
putError = `fetch error: ${e?.name ?? "Error"} ${e?.message ?? e}`;
|
|
443
448
|
}
|
|
444
|
-
|
|
449
|
+
try {
|
|
450
|
+
stream.destroy?.();
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
if (attempt < 2) await sleep(2e3 * (attempt + 1));
|
|
454
|
+
}
|
|
455
|
+
if (putError) {
|
|
456
|
+
throw new Error(`Archive upload failed: ${putError}`);
|
|
457
|
+
}
|
|
458
|
+
const extractResp = await apiPost(`/api/cli/push/${jobId}/extract`, {});
|
|
459
|
+
if (!extractResp.ok) {
|
|
460
|
+
throw new Error(`Failed to start extraction: ${await extractResp.text()}`);
|
|
461
|
+
}
|
|
462
|
+
const extractBar = new ProgressBar({ total: entryCount, totalBytes: 0, label: "\u2699 extract" });
|
|
463
|
+
const pollStart = Date.now();
|
|
464
|
+
const pollTimeoutMs = 30 * 60 * 1e3;
|
|
465
|
+
let lastStatus = null;
|
|
466
|
+
while (Date.now() - pollStart < pollTimeoutMs) {
|
|
467
|
+
await sleep(1500);
|
|
468
|
+
const statusResp = await apiGet(`/api/cli/push/${jobId}/extract-status`);
|
|
469
|
+
if (!statusResp.ok) {
|
|
470
|
+
throw new Error(`Failed to poll extract status: ${await statusResp.text()}`);
|
|
471
|
+
}
|
|
472
|
+
lastStatus = await statusResp.json();
|
|
473
|
+
const uploaded = Number(lastStatus?.uploaded ?? 0);
|
|
474
|
+
const errors = Number(lastStatus?.errors ?? 0);
|
|
475
|
+
extractBar.setCount(uploaded + errors);
|
|
476
|
+
if (lastStatus?.status === "done") break;
|
|
477
|
+
if (lastStatus?.status === "error") {
|
|
478
|
+
throw new Error(`Archive extraction failed: ${lastStatus?.detail ?? "unknown"}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
extractBar.done();
|
|
482
|
+
if (!lastStatus || lastStatus.status !== "done") {
|
|
483
|
+
throw new Error("Archive extraction timed out");
|
|
445
484
|
}
|
|
446
|
-
|
|
485
|
+
if (lastStatus.errors && lastStatus.errors > 0) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`Archive extraction completed but ${lastStatus.errors} file(s) failed to store. Retry the upload or check server logs.`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async function apiGet(path, timeout = 6e4) {
|
|
492
|
+
const auth = await getAuthHeader();
|
|
493
|
+
if (!auth) throw new Error("Not authenticated. Run: trajectories auth login");
|
|
494
|
+
return fetch(`${getApiUrl()}${path}`, {
|
|
495
|
+
method: "GET",
|
|
496
|
+
headers: { Authorization: auth },
|
|
497
|
+
signal: AbortSignal.timeout(timeout)
|
|
498
|
+
});
|
|
447
499
|
}
|
|
448
500
|
function sleep(ms) {
|
|
449
501
|
return new Promise((r) => setTimeout(r, ms));
|