trajectories-sh 1.3.1 → 1.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.
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-
|
|
84
|
+
const { uploadJob } = await import("./upload-WZGAR4S4.js");
|
|
85
85
|
const result = await uploadJob(directory, {
|
|
86
86
|
slug,
|
|
87
87
|
name: opts.name,
|
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
} from "./chunk-P6J5X7LP.js";
|
|
5
5
|
|
|
6
6
|
// src/upload.ts
|
|
7
|
-
import { readdirSync, statSync, readFileSync } from "fs";
|
|
8
|
-
import { join, relative } from "path";
|
|
7
|
+
import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
|
|
8
|
+
import { join, relative, dirname } from "path";
|
|
9
|
+
import { tmpdir } from "os";
|
|
10
|
+
import { randomBytes } from "crypto";
|
|
9
11
|
var SKIP = /* @__PURE__ */ new Set(["__pycache__", ".DS_Store", ".git", "node_modules"]);
|
|
10
12
|
var MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
11
13
|
var CONCURRENCY = 12;
|
|
@@ -39,11 +41,94 @@ async function apiPost(path, body, timeout = 3e5) {
|
|
|
39
41
|
signal: AbortSignal.timeout(timeout)
|
|
40
42
|
});
|
|
41
43
|
}
|
|
44
|
+
async function convertToWebP(dir) {
|
|
45
|
+
const files = collectFiles(dir);
|
|
46
|
+
const pngs = files.filter((f) => f.relPath.toLowerCase().endsWith(".png"));
|
|
47
|
+
if (pngs.length === 0) {
|
|
48
|
+
return { convertedDir: dir, pngsConverted: 0, savedBytes: 0 };
|
|
49
|
+
}
|
|
50
|
+
let sharp;
|
|
51
|
+
try {
|
|
52
|
+
sharp = (await import("sharp")).default;
|
|
53
|
+
} catch {
|
|
54
|
+
console.log(" \u26A0 sharp not available, skipping WebP conversion");
|
|
55
|
+
return { convertedDir: dir, pngsConverted: 0, savedBytes: 0 };
|
|
56
|
+
}
|
|
57
|
+
const tmpDir = join(tmpdir(), `trajectories-webp-${randomBytes(4).toString("hex")}`);
|
|
58
|
+
function copyDir(src, dest) {
|
|
59
|
+
mkdirSync(dest, { recursive: true });
|
|
60
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
61
|
+
if (SKIP.has(entry.name)) continue;
|
|
62
|
+
const srcPath = join(src, entry.name);
|
|
63
|
+
const destPath = join(dest, entry.name);
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
copyDir(srcPath, destPath);
|
|
66
|
+
} else {
|
|
67
|
+
copyFileSync(srcPath, destPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
copyDir(dir, tmpDir);
|
|
72
|
+
let converted = 0;
|
|
73
|
+
let savedBytes = 0;
|
|
74
|
+
const batchSize = 10;
|
|
75
|
+
for (let i = 0; i < pngs.length; i += batchSize) {
|
|
76
|
+
const batch = pngs.slice(i, i + batchSize);
|
|
77
|
+
await Promise.all(
|
|
78
|
+
batch.map(async (f) => {
|
|
79
|
+
const srcPath = join(tmpDir, f.relPath);
|
|
80
|
+
const webpPath = srcPath.replace(/\.png$/i, ".webp");
|
|
81
|
+
try {
|
|
82
|
+
await sharp(srcPath).webp({ quality: 80 }).toFile(webpPath);
|
|
83
|
+
const origSize = statSync(srcPath).size;
|
|
84
|
+
const newSize = statSync(webpPath).size;
|
|
85
|
+
if (newSize < origSize) {
|
|
86
|
+
const { unlinkSync } = await import("fs");
|
|
87
|
+
unlinkSync(srcPath);
|
|
88
|
+
savedBytes += origSize - newSize;
|
|
89
|
+
converted++;
|
|
90
|
+
} else {
|
|
91
|
+
const { unlinkSync } = await import("fs");
|
|
92
|
+
unlinkSync(webpPath);
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
process.stdout.write(` Converting: ${Math.min(i + batchSize, pngs.length)}/${pngs.length} PNGs\r`);
|
|
99
|
+
}
|
|
100
|
+
console.log("");
|
|
101
|
+
const trajFiles = collectFiles(tmpDir).filter((f) => f.relPath.endsWith("trajectory.json"));
|
|
102
|
+
for (const tf of trajFiles) {
|
|
103
|
+
try {
|
|
104
|
+
let content = readFileSync(tf.absPath, "utf-8");
|
|
105
|
+
const original = content;
|
|
106
|
+
content = content.replace(/screenshots\/([^"]+)\.png/g, (match, name) => {
|
|
107
|
+
const webpPath = join(dirname(tf.absPath), "screenshots", `${name}.webp`);
|
|
108
|
+
try {
|
|
109
|
+
statSync(webpPath);
|
|
110
|
+
return `screenshots/${name}.webp`;
|
|
111
|
+
} catch {
|
|
112
|
+
return match;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
if (content !== original) {
|
|
116
|
+
writeFileSync(tf.absPath, content);
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { convertedDir: tmpDir, pngsConverted: converted, savedBytes };
|
|
122
|
+
}
|
|
42
123
|
async function uploadJob(dir, opts) {
|
|
43
124
|
const slug = opts.slug ?? dir.replace(/\/+$/, "").split("/").pop();
|
|
44
125
|
const name = opts.name ?? slug;
|
|
45
126
|
const visibility = opts.visibility ?? "private";
|
|
46
|
-
const
|
|
127
|
+
const { convertedDir, pngsConverted, savedBytes } = await convertToWebP(dir);
|
|
128
|
+
if (pngsConverted > 0) {
|
|
129
|
+
console.log(` \u2713 Converted ${pngsConverted} PNGs to WebP (saved ${(savedBytes / 1024 / 1024).toFixed(1)} MB)`);
|
|
130
|
+
}
|
|
131
|
+
const files = collectFiles(convertedDir);
|
|
47
132
|
const totalSize = files.reduce((s, f) => s + f.size, 0);
|
|
48
133
|
console.log(` ${files.length} files, ${(totalSize / 1024 / 1024).toFixed(1)} MB`);
|
|
49
134
|
const initResp = await apiPost("/api/cli/push/init", { slug, name, visibility });
|
|
@@ -59,6 +144,13 @@ async function uploadJob(dir, opts) {
|
|
|
59
144
|
throw new Error(`Finalize failed: ${await finalResp.text()}`);
|
|
60
145
|
}
|
|
61
146
|
const result = await finalResp.json();
|
|
147
|
+
if (convertedDir !== dir) {
|
|
148
|
+
try {
|
|
149
|
+
const { rmSync } = await import("fs");
|
|
150
|
+
rmSync(convertedDir, { recursive: true, force: true });
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
62
154
|
const apiUrl = getApiUrl();
|
|
63
155
|
const siteUrl = apiUrl.includes("localhost") ? "http://localhost:3000" : apiUrl.replace("api.", "").replace(/\/$/, "");
|
|
64
156
|
return {
|
|
@@ -134,9 +226,7 @@ async function uploadOneFile(file, signedUrl) {
|
|
|
134
226
|
try {
|
|
135
227
|
const resp = await fetch(signedUrl, {
|
|
136
228
|
method: "PUT",
|
|
137
|
-
headers: {
|
|
138
|
-
"Content-Type": "application/octet-stream"
|
|
139
|
-
},
|
|
229
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
140
230
|
body: data,
|
|
141
231
|
signal: AbortSignal.timeout(12e4)
|
|
142
232
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trajectories-sh",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "CLI for uploading trajectory jobs to trajectories.sh",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
"node": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"chalk": "^5.0.0",
|
|
29
30
|
"commander": "^13.0.0",
|
|
31
|
+
"sharp": "^0.34.5",
|
|
30
32
|
"open": "^10.0.0",
|
|
31
33
|
"ora": "^8.0.0",
|
|
32
|
-
"chalk": "^5.0.0",
|
|
33
34
|
"tar": "^7.0.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|