git-shots-cli 0.7.0 → 0.8.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/index.js +92 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ function checkAuthError(res) {
|
|
|
44
44
|
|
|
45
45
|
// src/upload.ts
|
|
46
46
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
47
|
+
import { createHash } from "crypto";
|
|
47
48
|
import { resolve as resolve2, basename, dirname } from "path";
|
|
48
49
|
import { execSync } from "child_process";
|
|
49
50
|
import { glob } from "glob";
|
|
@@ -56,6 +57,25 @@ function chunk(arr, size) {
|
|
|
56
57
|
}
|
|
57
58
|
return chunks;
|
|
58
59
|
}
|
|
60
|
+
function computeFileHash(buffer) {
|
|
61
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
62
|
+
}
|
|
63
|
+
async function fetchServerHashes(config, branch) {
|
|
64
|
+
try {
|
|
65
|
+
const params = new URLSearchParams({ branch });
|
|
66
|
+
if (config.platform) params.set("platform", config.platform);
|
|
67
|
+
const url = `${config.server}/api/projects/${config.project}/hashes?${params}`;
|
|
68
|
+
const res = await fetch(url, {
|
|
69
|
+
headers: { ...authHeaders(config) }
|
|
70
|
+
});
|
|
71
|
+
checkAuthError(res);
|
|
72
|
+
if (!res.ok) return null;
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
return data.hashes;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
59
79
|
async function upload(config, options) {
|
|
60
80
|
const dir = resolve2(process.cwd(), config.directory);
|
|
61
81
|
if (!existsSync2(dir)) {
|
|
@@ -77,22 +97,81 @@ async function upload(config, options) {
|
|
|
77
97
|
}
|
|
78
98
|
const files = rawFiles.map((file) => {
|
|
79
99
|
const dirName = dirname(file);
|
|
100
|
+
const fileName = basename(file);
|
|
80
101
|
return {
|
|
81
|
-
fieldName: dirName !== "." ? `${dirName}/${
|
|
82
|
-
fileName
|
|
102
|
+
fieldName: dirName !== "." ? `${dirName}/${fileName}` : fileName,
|
|
103
|
+
fileName,
|
|
104
|
+
slug: fileName.replace(/\.png$/, ""),
|
|
83
105
|
fullPath: resolve2(dir, file)
|
|
84
106
|
};
|
|
85
107
|
});
|
|
86
|
-
const
|
|
87
|
-
const allSlugs = [.../* @__PURE__ */ new Set([...options.baseManifest ?? [], ...localSlugs])];
|
|
108
|
+
const allSlugs = [.../* @__PURE__ */ new Set([...options.baseManifest ?? [], ...files.map((f) => f.slug)])];
|
|
88
109
|
if (options.force) {
|
|
89
110
|
console.log(chalk2.yellow("Force mode: bypassing content hash dedup"));
|
|
90
111
|
}
|
|
91
112
|
console.log(chalk2.dim(`Found ${files.length} screenshots`));
|
|
113
|
+
let filesToUpload = files;
|
|
114
|
+
let clientDedupSkipped = 0;
|
|
115
|
+
if (!options.force) {
|
|
116
|
+
const [branchHashes, mainHashes] = await Promise.all([
|
|
117
|
+
fetchServerHashes(config, branch),
|
|
118
|
+
branch !== "main" ? fetchServerHashes(config, "main") : Promise.resolve(null)
|
|
119
|
+
]);
|
|
120
|
+
if (branchHashes || mainHashes) {
|
|
121
|
+
console.log(chalk2.dim("Computing local hashes..."));
|
|
122
|
+
filesToUpload = files.filter((file) => {
|
|
123
|
+
file.buffer = readFileSync2(file.fullPath);
|
|
124
|
+
const localHash = computeFileHash(file.buffer);
|
|
125
|
+
const branchHash = branchHashes?.[file.slug];
|
|
126
|
+
const mainHash = mainHashes?.[file.slug];
|
|
127
|
+
if (branchHash && localHash === branchHash) return false;
|
|
128
|
+
if (mainHash && localHash === mainHash) return false;
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
clientDedupSkipped = files.length - filesToUpload.length;
|
|
132
|
+
if (clientDedupSkipped > 0) {
|
|
133
|
+
console.log(
|
|
134
|
+
chalk2.cyan(
|
|
135
|
+
`Client dedup: ${clientDedupSkipped} unchanged, ${filesToUpload.length} to upload`
|
|
136
|
+
)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
console.log(chalk2.dim("Hash endpoint unavailable, uploading all files"));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
92
143
|
const url = `${config.server}/api/upload`;
|
|
93
|
-
|
|
144
|
+
if (filesToUpload.length === 0) {
|
|
145
|
+
console.log(chalk2.dim("All files unchanged, sending manifest only"));
|
|
146
|
+
const formData = new FormData();
|
|
147
|
+
formData.append("project", config.project);
|
|
148
|
+
formData.append("branch", branch);
|
|
149
|
+
formData.append("gitSha", sha);
|
|
150
|
+
if (config.platform) formData.append("platform", config.platform);
|
|
151
|
+
formData.append("allSlugs", JSON.stringify(allSlugs));
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(url, {
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: formData,
|
|
156
|
+
headers: { Origin: config.server, ...authHeaders(config) }
|
|
157
|
+
});
|
|
158
|
+
checkAuthError(res);
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const data = await res.json();
|
|
161
|
+
console.error(chalk2.red(`Manifest update failed: ${JSON.stringify(data)}`));
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(chalk2.red(`Manifest request failed: ${err}`));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
console.log(`
|
|
169
|
+
${chalk2.dim(`${clientDedupSkipped} unchanged`)} (manifest updated)`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const batches = chunk(filesToUpload, BATCH_SIZE);
|
|
94
173
|
let totalUploaded = 0;
|
|
95
|
-
let
|
|
174
|
+
let serverSkipped = 0;
|
|
96
175
|
const totalSkippedScreens = [];
|
|
97
176
|
for (let i = 0; i < batches.length; i++) {
|
|
98
177
|
const batch = batches[i];
|
|
@@ -107,7 +186,7 @@ async function upload(config, options) {
|
|
|
107
186
|
formData.append("allSlugs", JSON.stringify(allSlugs));
|
|
108
187
|
}
|
|
109
188
|
for (const file of batch) {
|
|
110
|
-
const buffer = readFileSync2(file.fullPath);
|
|
189
|
+
const buffer = file.buffer ?? readFileSync2(file.fullPath);
|
|
111
190
|
const blob = new Blob([buffer], { type: "image/png" });
|
|
112
191
|
formData.append(file.fieldName, blob, file.fileName);
|
|
113
192
|
}
|
|
@@ -125,7 +204,7 @@ async function upload(config, options) {
|
|
|
125
204
|
process.exit(1);
|
|
126
205
|
}
|
|
127
206
|
totalUploaded += data.uploaded ?? 0;
|
|
128
|
-
|
|
207
|
+
serverSkipped += data.skipped ?? 0;
|
|
129
208
|
if (data.skippedScreens) totalSkippedScreens.push(...data.skippedScreens);
|
|
130
209
|
if (batches.length > 1) {
|
|
131
210
|
const parts2 = [];
|
|
@@ -140,9 +219,13 @@ async function upload(config, options) {
|
|
|
140
219
|
}
|
|
141
220
|
const parts = [];
|
|
142
221
|
if (totalUploaded > 0) parts.push(chalk2.green(`${totalUploaded} uploaded`));
|
|
143
|
-
|
|
222
|
+
const totalUnchanged = clientDedupSkipped + serverSkipped;
|
|
223
|
+
if (totalUnchanged > 0) parts.push(chalk2.dim(`${totalUnchanged} unchanged`));
|
|
144
224
|
console.log(`
|
|
145
225
|
${parts.join(", ") || chalk2.dim("nothing to do")}`);
|
|
226
|
+
if (clientDedupSkipped > 0) {
|
|
227
|
+
console.log(chalk2.dim(` (${clientDedupSkipped} skipped client-side, ${serverSkipped} server-side)`));
|
|
228
|
+
}
|
|
146
229
|
if (totalSkippedScreens.length > 0) {
|
|
147
230
|
const MAX_DISPLAY = 20;
|
|
148
231
|
if (totalSkippedScreens.length <= MAX_DISPLAY) {
|