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.
Files changed (2) hide show
  1. package/dist/index.js +98 -36
  2. 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 files = await glob("**/*.png", { cwd: dir });
66
- if (files.length === 0) {
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
- console.log(chalk2.dim(`Found ${files.length} screenshots`));
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
- const fieldName = dirName !== "." ? `${dirName}/${basename(file)}` : basename(file);
82
- formData.append(fieldName, blob, basename(file));
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
- console.log(chalk2.dim(`Uploading to ${url}...`));
86
- try {
87
- const res = await fetch(url, {
88
- method: "POST",
89
- body: formData,
90
- headers: { Origin: config.server, ...authHeaders(config) }
91
- });
92
- const data = await res.json();
93
- checkAuthError(res);
94
- if (!res.ok) {
95
- console.error(chalk2.red(`Upload failed: ${JSON.stringify(data)}`));
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
- if (data.skipped > 0) {
99
- console.log(chalk2.green(`Uploaded ${data.uploaded} screenshots`) + chalk2.dim(` (${data.skipped} unchanged, skipped)`));
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
- console.log(chalk2.green(`Uploaded ${data.uploaded} screenshots`));
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.6.1",
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
+ }