git-shots-cli 0.6.1 → 0.7.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 +98 -36
- package/package.json +27 -27
package/dist/index.js
CHANGED
|
@@ -48,6 +48,14 @@ import { resolve as resolve2, basename, dirname } from "path";
|
|
|
48
48
|
import { execSync } from "child_process";
|
|
49
49
|
import { glob } from "glob";
|
|
50
50
|
import chalk2 from "chalk";
|
|
51
|
+
var BATCH_SIZE = 3;
|
|
52
|
+
function chunk(arr, size) {
|
|
53
|
+
const chunks = [];
|
|
54
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
55
|
+
chunks.push(arr.slice(i, i + size));
|
|
56
|
+
}
|
|
57
|
+
return chunks;
|
|
58
|
+
}
|
|
51
59
|
async function upload(config, options) {
|
|
52
60
|
const dir = resolve2(process.cwd(), config.directory);
|
|
53
61
|
if (!existsSync2(dir)) {
|
|
@@ -62,47 +70,87 @@ async function upload(config, options) {
|
|
|
62
70
|
console.log(chalk2.dim(`SHA: ${sha.slice(0, 7)}`));
|
|
63
71
|
console.log(chalk2.dim(`Dir: ${dir}`));
|
|
64
72
|
console.log();
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
73
|
+
const rawFiles = await glob("**/*.png", { cwd: dir });
|
|
74
|
+
if (rawFiles.length === 0) {
|
|
67
75
|
console.log(chalk2.yellow("No PNG files found."));
|
|
68
76
|
return;
|
|
69
77
|
}
|
|
70
|
-
|
|
71
|
-
const formData = new FormData();
|
|
72
|
-
formData.append("project", config.project);
|
|
73
|
-
formData.append("branch", branch);
|
|
74
|
-
formData.append("gitSha", sha);
|
|
75
|
-
if (config.platform) formData.append("platform", config.platform);
|
|
76
|
-
for (const file of files) {
|
|
77
|
-
const fullPath = resolve2(dir, file);
|
|
78
|
-
const buffer = readFileSync2(fullPath);
|
|
79
|
-
const blob = new Blob([buffer], { type: "image/png" });
|
|
78
|
+
const files = rawFiles.map((file) => {
|
|
80
79
|
const dirName = dirname(file);
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
return {
|
|
81
|
+
fieldName: dirName !== "." ? `${dirName}/${basename(file)}` : basename(file),
|
|
82
|
+
fileName: basename(file),
|
|
83
|
+
fullPath: resolve2(dir, file)
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
const localSlugs = files.map((f) => f.fileName.replace(/\.png$/, ""));
|
|
87
|
+
const allSlugs = [.../* @__PURE__ */ new Set([...options.baseManifest ?? [], ...localSlugs])];
|
|
88
|
+
if (options.force) {
|
|
89
|
+
console.log(chalk2.yellow("Force mode: bypassing content hash dedup"));
|
|
83
90
|
}
|
|
91
|
+
console.log(chalk2.dim(`Found ${files.length} screenshots`));
|
|
84
92
|
const url = `${config.server}/api/upload`;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const batches = chunk(files, BATCH_SIZE);
|
|
94
|
+
let totalUploaded = 0;
|
|
95
|
+
let totalSkipped = 0;
|
|
96
|
+
const totalSkippedScreens = [];
|
|
97
|
+
for (let i = 0; i < batches.length; i++) {
|
|
98
|
+
const batch = batches[i];
|
|
99
|
+
const isLastBatch = i === batches.length - 1;
|
|
100
|
+
const formData = new FormData();
|
|
101
|
+
formData.append("project", config.project);
|
|
102
|
+
formData.append("branch", branch);
|
|
103
|
+
formData.append("gitSha", sha);
|
|
104
|
+
if (config.platform) formData.append("platform", config.platform);
|
|
105
|
+
if (options.force) formData.append("forceUpload", "true");
|
|
106
|
+
if (isLastBatch) {
|
|
107
|
+
formData.append("allSlugs", JSON.stringify(allSlugs));
|
|
108
|
+
}
|
|
109
|
+
for (const file of batch) {
|
|
110
|
+
const buffer = readFileSync2(file.fullPath);
|
|
111
|
+
const blob = new Blob([buffer], { type: "image/png" });
|
|
112
|
+
formData.append(file.fieldName, blob, file.fileName);
|
|
113
|
+
}
|
|
114
|
+
const batchLabel = batches.length > 1 ? chalk2.dim(`[${i + 1}/${batches.length}] `) : "";
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetch(url, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: formData,
|
|
119
|
+
headers: { Origin: config.server, ...authHeaders(config) }
|
|
120
|
+
});
|
|
121
|
+
checkAuthError(res);
|
|
122
|
+
const data = await res.json();
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
console.error(chalk2.red(`${batchLabel}Upload failed: ${JSON.stringify(data)}`));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
totalUploaded += data.uploaded ?? 0;
|
|
128
|
+
totalSkipped += data.skipped ?? 0;
|
|
129
|
+
if (data.skippedScreens) totalSkippedScreens.push(...data.skippedScreens);
|
|
130
|
+
if (batches.length > 1) {
|
|
131
|
+
const parts2 = [];
|
|
132
|
+
if (data.uploaded > 0) parts2.push(chalk2.green(`${data.uploaded} uploaded`));
|
|
133
|
+
if (data.skipped > 0) parts2.push(chalk2.dim(`${data.skipped} skipped`));
|
|
134
|
+
console.log(`${batchLabel}${parts2.join(", ")}`);
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error(chalk2.red(`${batchLabel}Request failed: ${err}`));
|
|
96
138
|
process.exit(1);
|
|
97
139
|
}
|
|
98
|
-
|
|
99
|
-
|
|
140
|
+
}
|
|
141
|
+
const parts = [];
|
|
142
|
+
if (totalUploaded > 0) parts.push(chalk2.green(`${totalUploaded} uploaded`));
|
|
143
|
+
if (totalSkipped > 0) parts.push(chalk2.dim(`${totalSkipped} unchanged`));
|
|
144
|
+
console.log(`
|
|
145
|
+
${parts.join(", ") || chalk2.dim("nothing to do")}`);
|
|
146
|
+
if (totalSkippedScreens.length > 0) {
|
|
147
|
+
const MAX_DISPLAY = 20;
|
|
148
|
+
if (totalSkippedScreens.length <= MAX_DISPLAY) {
|
|
149
|
+
console.log(chalk2.dim(`Skipped: ${totalSkippedScreens.join(", ")}`));
|
|
100
150
|
} else {
|
|
101
|
-
|
|
151
|
+
const shown = totalSkippedScreens.slice(0, MAX_DISPLAY).join(", ");
|
|
152
|
+
console.log(chalk2.dim(`Skipped: ${shown} ... and ${totalSkippedScreens.length - MAX_DISPLAY} more`));
|
|
102
153
|
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
console.error(chalk2.red(`Request failed: ${err}`));
|
|
105
|
-
process.exit(1);
|
|
106
154
|
}
|
|
107
155
|
}
|
|
108
156
|
|
|
@@ -286,8 +334,21 @@ async function review(config, options) {
|
|
|
286
334
|
console.log(chalk6.dim(`Branch: ${branch}`));
|
|
287
335
|
console.log(chalk6.dim(`SHA: ${sha.slice(0, 7)}`));
|
|
288
336
|
console.log();
|
|
337
|
+
let baseManifest;
|
|
338
|
+
try {
|
|
339
|
+
const manifestUrl = `${config.server}/api/projects/${encodeURIComponent(config.project)}/manifest?branch=main`;
|
|
340
|
+
const manifestRes = await fetch(manifestUrl, {
|
|
341
|
+
headers: { Origin: config.server, ...authHeaders(config) }
|
|
342
|
+
});
|
|
343
|
+
if (manifestRes.ok) {
|
|
344
|
+
const data = await manifestRes.json();
|
|
345
|
+
baseManifest = data.screenSlugs;
|
|
346
|
+
console.log(chalk6.dim(`Base manifest: ${baseManifest.length} screens on main`));
|
|
347
|
+
}
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
289
350
|
console.log(chalk6.dim("Uploading screenshots..."));
|
|
290
|
-
await upload(config, { branch, sha });
|
|
351
|
+
await upload(config, { branch, sha, baseManifest, force: options.force });
|
|
291
352
|
console.log();
|
|
292
353
|
console.log(chalk6.dim("Creating review session..."));
|
|
293
354
|
const reviewUrl = `${config.server}/api/reviews`;
|
|
@@ -641,7 +702,7 @@ var require2 = createRequire(import.meta.url);
|
|
|
641
702
|
var { version } = require2("../package.json");
|
|
642
703
|
var program = new Command();
|
|
643
704
|
program.name("git-shots").description("CLI for git-shots visual regression platform").version(version);
|
|
644
|
-
program.command("upload").description("Upload screenshots to git-shots").option("-p, --project <slug>", "Project slug").option("-s, --server <url>", "Server URL").option("-d, --directory <path>", "Screenshots directory").option("-b, --branch <name>", "Git branch (auto-detected)").option("--sha <hash>", "Git SHA (auto-detected)").option("--platform <name>", "Platform tag (e.g., android, web)").action(async (options) => {
|
|
705
|
+
program.command("upload").description("Upload screenshots to git-shots").option("-p, --project <slug>", "Project slug").option("-s, --server <url>", "Server URL").option("-d, --directory <path>", "Screenshots directory").option("-b, --branch <name>", "Git branch (auto-detected)").option("--sha <hash>", "Git SHA (auto-detected)").option("--platform <name>", "Platform tag (e.g., android, web)").option("-f, --force", "Re-upload all files, bypassing content hash dedup").action(async (options) => {
|
|
645
706
|
const config = loadConfig();
|
|
646
707
|
if (options.project) config.project = options.project;
|
|
647
708
|
if (options.server) config.server = options.server;
|
|
@@ -651,7 +712,7 @@ program.command("upload").description("Upload screenshots to git-shots").option(
|
|
|
651
712
|
console.error("Error: project slug required. Use --project or .git-shots.json");
|
|
652
713
|
process.exit(1);
|
|
653
714
|
}
|
|
654
|
-
await upload(config, { branch: options.branch, sha: options.sha });
|
|
715
|
+
await upload(config, { branch: options.branch, sha: options.sha, force: options.force });
|
|
655
716
|
if (config.flows && config.flows.length > 0) {
|
|
656
717
|
await syncFlows(config);
|
|
657
718
|
}
|
|
@@ -688,7 +749,7 @@ program.command("pull-baselines").description("Download baseline screenshots fro
|
|
|
688
749
|
}
|
|
689
750
|
await pullBaselines(config, { branch: options.branch, output: options.output });
|
|
690
751
|
});
|
|
691
|
-
program.command("review").description("Upload screenshots, create review session, and poll for verdict").option("-p, --project <slug>", "Project slug").option("-s, --server <url>", "Server URL").option("-d, --directory <path>", "Screenshots directory").option("-b, --branch <name>", "Git branch (auto-detected)").option("--sha <hash>", "Git SHA (auto-detected)").option("--platform <name>", "Platform tag (e.g., android, web)").option("--open", "Open review URL in browser", true).option("--no-open", "Do not open review URL in browser").option("--poll", "Poll for verdict and exit with code", true).option("--no-poll", "Do not poll for verdict").option("--timeout <seconds>", "Polling timeout in seconds", parseInt, 300).option("--screens <patterns>", "Screen slug patterns to compare (comma-separated, supports *)").action(async (options) => {
|
|
752
|
+
program.command("review").description("Upload screenshots, create review session, and poll for verdict").option("-p, --project <slug>", "Project slug").option("-s, --server <url>", "Server URL").option("-d, --directory <path>", "Screenshots directory").option("-b, --branch <name>", "Git branch (auto-detected)").option("--sha <hash>", "Git SHA (auto-detected)").option("--platform <name>", "Platform tag (e.g., android, web)").option("--open", "Open review URL in browser", true).option("--no-open", "Do not open review URL in browser").option("--poll", "Poll for verdict and exit with code", true).option("--no-poll", "Do not poll for verdict").option("--timeout <seconds>", "Polling timeout in seconds", parseInt, 300).option("--screens <patterns>", "Screen slug patterns to compare (comma-separated, supports *)").option("-f, --force", "Re-upload all files, bypassing content hash dedup").action(async (options) => {
|
|
692
753
|
const config = loadConfig();
|
|
693
754
|
if (options.project) config.project = options.project;
|
|
694
755
|
if (options.server) config.server = options.server;
|
|
@@ -705,7 +766,8 @@ program.command("review").description("Upload screenshots, create review session
|
|
|
705
766
|
open: options.open,
|
|
706
767
|
poll: options.poll,
|
|
707
768
|
timeout: options.timeout,
|
|
708
|
-
screens
|
|
769
|
+
screens,
|
|
770
|
+
force: options.force
|
|
709
771
|
});
|
|
710
772
|
});
|
|
711
773
|
program.command("rename <old-slug> <new-slug>").description("Rename a screen slug (moves R2 objects server-side)").option("-p, --project <slug>", "Project slug").option("-s, --server <url>", "Server URL").option("--platform <name>", "Platform to disambiguate (e.g., android, web)").option("--category <value>", "Set new category (use empty string to clear)").action(async (oldSlug, newSlug, options) => {
|
package/package.json
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "git-shots-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for git-shots visual regression platform",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"git-shots": "./dist/index.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist"
|
|
11
|
-
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsup src/index.ts --format esm --dts",
|
|
14
|
-
"dev": "tsup src/index.ts --format esm --watch"
|
|
15
|
-
},
|
|
16
|
-
"dependencies": {
|
|
17
|
-
"commander": "^12.0.0",
|
|
18
|
-
"chalk": "^5.3.0",
|
|
19
|
-
"dotenv": "^16.4.0",
|
|
20
|
-
"glob": "^11.0.0"
|
|
21
|
-
},
|
|
22
|
-
"devDependencies": {
|
|
23
|
-
"tsup": "^8.0.0",
|
|
24
|
-
"typescript": "^5.0.0",
|
|
25
|
-
"@types/node": "^22.0.0"
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "git-shots-cli",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "CLI for git-shots visual regression platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"git-shots": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
14
|
+
"dev": "tsup src/index.ts --format esm --watch"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"commander": "^12.0.0",
|
|
18
|
+
"chalk": "^5.3.0",
|
|
19
|
+
"dotenv": "^16.4.0",
|
|
20
|
+
"glob": "^11.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.0.0",
|
|
24
|
+
"typescript": "^5.0.0",
|
|
25
|
+
"@types/node": "^22.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|