git-shots-cli 0.3.1 → 0.4.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/index.js +126 -98
- package/package.json +26 -26
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// src/config.ts
|
|
7
7
|
import { readFileSync, existsSync } from "fs";
|
|
8
8
|
import { resolve } from "path";
|
|
9
|
+
import chalk from "chalk";
|
|
9
10
|
var DEFAULT_CONFIG = {
|
|
10
11
|
project: "",
|
|
11
12
|
server: "https://git-shots.rijid356.workers.dev",
|
|
@@ -13,15 +14,29 @@ var DEFAULT_CONFIG = {
|
|
|
13
14
|
};
|
|
14
15
|
function loadConfig(cwd = process.cwd()) {
|
|
15
16
|
const configPath = resolve(cwd, ".git-shots.json");
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
let config = { ...DEFAULT_CONFIG };
|
|
18
|
+
if (existsSync(configPath)) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
config = { ...config, ...parsed };
|
|
23
|
+
} catch {
|
|
24
|
+
}
|
|
18
25
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const envKey = process.env.GIT_SHOTS_API_KEY;
|
|
27
|
+
if (envKey) {
|
|
28
|
+
config.apiKey = envKey;
|
|
29
|
+
}
|
|
30
|
+
return config;
|
|
31
|
+
}
|
|
32
|
+
function authHeaders(config) {
|
|
33
|
+
if (!config.apiKey) return {};
|
|
34
|
+
return { Authorization: `Bearer ${config.apiKey}` };
|
|
35
|
+
}
|
|
36
|
+
function checkAuthError(res) {
|
|
37
|
+
if (res.status === 401 || res.status === 403) {
|
|
38
|
+
console.error(chalk.red("Authentication failed. Set GIT_SHOTS_API_KEY or add apiKey to .git-shots.json"));
|
|
39
|
+
process.exit(1);
|
|
25
40
|
}
|
|
26
41
|
}
|
|
27
42
|
|
|
@@ -30,27 +45,27 @@ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
|
30
45
|
import { resolve as resolve2, basename, dirname } from "path";
|
|
31
46
|
import { execSync } from "child_process";
|
|
32
47
|
import { glob } from "glob";
|
|
33
|
-
import
|
|
48
|
+
import chalk2 from "chalk";
|
|
34
49
|
async function upload(config, options) {
|
|
35
50
|
const dir = resolve2(process.cwd(), config.directory);
|
|
36
51
|
if (!existsSync2(dir)) {
|
|
37
|
-
console.error(
|
|
52
|
+
console.error(chalk2.red(`Directory not found: ${dir}`));
|
|
38
53
|
process.exit(1);
|
|
39
54
|
}
|
|
40
55
|
const branch = options.branch ?? execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
|
|
41
56
|
const sha = options.sha ?? execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
42
|
-
console.log(
|
|
43
|
-
if (config.platform) console.log(
|
|
44
|
-
console.log(
|
|
45
|
-
console.log(
|
|
46
|
-
console.log(
|
|
57
|
+
console.log(chalk2.dim(`Project: ${config.project}`));
|
|
58
|
+
if (config.platform) console.log(chalk2.dim(`Platform: ${config.platform}`));
|
|
59
|
+
console.log(chalk2.dim(`Branch: ${branch}`));
|
|
60
|
+
console.log(chalk2.dim(`SHA: ${sha.slice(0, 7)}`));
|
|
61
|
+
console.log(chalk2.dim(`Dir: ${dir}`));
|
|
47
62
|
console.log();
|
|
48
63
|
const files = await glob("**/*.png", { cwd: dir });
|
|
49
64
|
if (files.length === 0) {
|
|
50
|
-
console.log(
|
|
65
|
+
console.log(chalk2.yellow("No PNG files found."));
|
|
51
66
|
return;
|
|
52
67
|
}
|
|
53
|
-
console.log(
|
|
68
|
+
console.log(chalk2.dim(`Found ${files.length} screenshots`));
|
|
54
69
|
const formData = new FormData();
|
|
55
70
|
formData.append("project", config.project);
|
|
56
71
|
formData.append("branch", branch);
|
|
@@ -65,27 +80,28 @@ async function upload(config, options) {
|
|
|
65
80
|
formData.append(fieldName, blob, basename(file));
|
|
66
81
|
}
|
|
67
82
|
const url = `${config.server}/api/upload`;
|
|
68
|
-
console.log(
|
|
83
|
+
console.log(chalk2.dim(`Uploading to ${url}...`));
|
|
69
84
|
try {
|
|
70
85
|
const res = await fetch(url, {
|
|
71
86
|
method: "POST",
|
|
72
87
|
body: formData,
|
|
73
|
-
headers: { Origin: config.server }
|
|
88
|
+
headers: { Origin: config.server, ...authHeaders(config) }
|
|
74
89
|
});
|
|
75
90
|
const data = await res.json();
|
|
91
|
+
checkAuthError(res);
|
|
76
92
|
if (!res.ok) {
|
|
77
|
-
console.error(
|
|
93
|
+
console.error(chalk2.red(`Upload failed: ${JSON.stringify(data)}`));
|
|
78
94
|
process.exit(1);
|
|
79
95
|
}
|
|
80
|
-
console.log(
|
|
96
|
+
console.log(chalk2.green(`Uploaded ${data.uploaded} screenshots`));
|
|
81
97
|
} catch (err) {
|
|
82
|
-
console.error(
|
|
98
|
+
console.error(chalk2.red(`Request failed: ${err}`));
|
|
83
99
|
process.exit(1);
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
// src/compare.ts
|
|
88
|
-
import
|
|
104
|
+
import chalk3 from "chalk";
|
|
89
105
|
async function compare(config, options) {
|
|
90
106
|
const url = `${config.server}/api/compare`;
|
|
91
107
|
const body = {
|
|
@@ -94,67 +110,71 @@ async function compare(config, options) {
|
|
|
94
110
|
head: options.head,
|
|
95
111
|
threshold: options.threshold ?? 0.1
|
|
96
112
|
};
|
|
97
|
-
console.log(
|
|
113
|
+
console.log(chalk3.dim(`Comparing ${body.base} vs ${body.head} for ${config.project}...`));
|
|
98
114
|
try {
|
|
99
115
|
const res = await fetch(url, {
|
|
100
116
|
method: "POST",
|
|
101
|
-
headers: { "Content-Type": "application/json", Origin: config.server },
|
|
117
|
+
headers: { "Content-Type": "application/json", Origin: config.server, ...authHeaders(config) },
|
|
102
118
|
body: JSON.stringify(body)
|
|
103
119
|
});
|
|
104
120
|
const data = await res.json();
|
|
121
|
+
checkAuthError(res);
|
|
105
122
|
if (!res.ok) {
|
|
106
|
-
console.error(
|
|
123
|
+
console.error(chalk3.red(`Compare failed: ${JSON.stringify(data)}`));
|
|
107
124
|
process.exit(1);
|
|
108
125
|
}
|
|
109
126
|
console.log();
|
|
110
|
-
console.log(`Compared ${
|
|
127
|
+
console.log(`Compared ${chalk3.bold(data.compared)} screens`);
|
|
111
128
|
console.log();
|
|
112
129
|
if (data.diffs.length === 0) {
|
|
113
|
-
console.log(
|
|
130
|
+
console.log(chalk3.green("No visual differences found!"));
|
|
114
131
|
return;
|
|
115
132
|
}
|
|
116
|
-
console.log(
|
|
117
|
-
console.log(
|
|
133
|
+
console.log(chalk3.dim("Screen".padEnd(30) + "Mismatch".padEnd(15) + "Pixels"));
|
|
134
|
+
console.log(chalk3.dim("-".repeat(55)));
|
|
118
135
|
for (const d of data.diffs) {
|
|
119
136
|
const pct = d.mismatchPercentage.toFixed(2) + "%";
|
|
120
|
-
const color = d.mismatchPercentage > 10 ?
|
|
137
|
+
const color = d.mismatchPercentage > 10 ? chalk3.red : d.mismatchPercentage > 1 ? chalk3.yellow : chalk3.green;
|
|
121
138
|
console.log(
|
|
122
|
-
d.screen.padEnd(30) + color(pct.padEnd(15)) +
|
|
139
|
+
d.screen.padEnd(30) + color(pct.padEnd(15)) + chalk3.dim(d.mismatchPixels.toLocaleString())
|
|
123
140
|
);
|
|
124
141
|
}
|
|
125
142
|
} catch (err) {
|
|
126
|
-
console.error(
|
|
143
|
+
console.error(chalk3.red(`Request failed: ${err}`));
|
|
127
144
|
process.exit(1);
|
|
128
145
|
}
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
// src/status.ts
|
|
132
|
-
import
|
|
149
|
+
import chalk4 from "chalk";
|
|
133
150
|
async function status(config) {
|
|
134
151
|
const url = `${config.server}/api/diffs?project=${encodeURIComponent(config.project)}`;
|
|
135
152
|
try {
|
|
136
|
-
const res = await fetch(url
|
|
153
|
+
const res = await fetch(url, {
|
|
154
|
+
headers: { ...authHeaders(config) }
|
|
155
|
+
});
|
|
137
156
|
const data = await res.json();
|
|
157
|
+
checkAuthError(res);
|
|
138
158
|
if (!res.ok) {
|
|
139
|
-
console.error(
|
|
159
|
+
console.error(chalk4.red(`Status failed: ${JSON.stringify(data)}`));
|
|
140
160
|
process.exit(1);
|
|
141
161
|
}
|
|
142
162
|
if (data.length === 0) {
|
|
143
|
-
console.log(
|
|
163
|
+
console.log(chalk4.dim("No diffs found for this project."));
|
|
144
164
|
return;
|
|
145
165
|
}
|
|
146
|
-
console.log(`${
|
|
166
|
+
console.log(`${chalk4.bold(data.length)} diffs for ${config.project}`);
|
|
147
167
|
console.log();
|
|
148
|
-
console.log(
|
|
149
|
-
console.log(
|
|
168
|
+
console.log(chalk4.dim("ID".padEnd(8) + "Status".padEnd(12) + "Mismatch".padEnd(12) + "Date"));
|
|
169
|
+
console.log(chalk4.dim("-".repeat(50)));
|
|
150
170
|
for (const { diff } of data) {
|
|
151
|
-
const statusColor = diff.status === "approved" ?
|
|
171
|
+
const statusColor = diff.status === "approved" ? chalk4.green : diff.status === "rejected" ? chalk4.red : chalk4.yellow;
|
|
152
172
|
console.log(
|
|
153
|
-
String(diff.id).padEnd(8) + statusColor(diff.status.padEnd(12)) + (diff.mismatch_percentage.toFixed(2) + "%").padEnd(12) +
|
|
173
|
+
String(diff.id).padEnd(8) + statusColor(diff.status.padEnd(12)) + (diff.mismatch_percentage.toFixed(2) + "%").padEnd(12) + chalk4.dim(new Date(diff.created_at * 1e3).toLocaleDateString())
|
|
154
174
|
);
|
|
155
175
|
}
|
|
156
176
|
} catch (err) {
|
|
157
|
-
console.error(
|
|
177
|
+
console.error(chalk4.red(`Request failed: ${err}`));
|
|
158
178
|
process.exit(1);
|
|
159
179
|
}
|
|
160
180
|
}
|
|
@@ -162,33 +182,36 @@ async function status(config) {
|
|
|
162
182
|
// src/pull-baselines.ts
|
|
163
183
|
import { mkdirSync, writeFileSync } from "fs";
|
|
164
184
|
import { resolve as resolve3, dirname as dirname2 } from "path";
|
|
165
|
-
import
|
|
185
|
+
import chalk5 from "chalk";
|
|
166
186
|
async function pullBaselines(config, options) {
|
|
167
187
|
const branch = options.branch ?? "main";
|
|
168
188
|
const outputDir = resolve3(process.cwd(), options.output ?? config.directory);
|
|
169
|
-
console.log(
|
|
170
|
-
console.log(
|
|
171
|
-
console.log(
|
|
189
|
+
console.log(chalk5.dim(`Project: ${config.project}`));
|
|
190
|
+
console.log(chalk5.dim(`Branch: ${branch}`));
|
|
191
|
+
console.log(chalk5.dim(`Output: ${outputDir}`));
|
|
172
192
|
console.log();
|
|
173
193
|
const manifestUrl = `${config.server}/api/projects/${encodeURIComponent(config.project)}/snapshots?branch=${encodeURIComponent(branch)}`;
|
|
174
194
|
let snapshots;
|
|
175
195
|
try {
|
|
176
|
-
const res = await fetch(manifestUrl
|
|
196
|
+
const res = await fetch(manifestUrl, {
|
|
197
|
+
headers: { ...authHeaders(config) }
|
|
198
|
+
});
|
|
177
199
|
const data = await res.json();
|
|
200
|
+
checkAuthError(res);
|
|
178
201
|
if (!res.ok) {
|
|
179
|
-
console.error(
|
|
202
|
+
console.error(chalk5.red(`Failed to fetch snapshots: ${JSON.stringify(data)}`));
|
|
180
203
|
process.exit(1);
|
|
181
204
|
}
|
|
182
205
|
snapshots = data.snapshots;
|
|
183
206
|
} catch (err) {
|
|
184
|
-
console.error(
|
|
207
|
+
console.error(chalk5.red(`Request failed: ${err}`));
|
|
185
208
|
process.exit(1);
|
|
186
209
|
}
|
|
187
210
|
if (snapshots.length === 0) {
|
|
188
|
-
console.log(
|
|
211
|
+
console.log(chalk5.yellow("No baseline snapshots found."));
|
|
189
212
|
return;
|
|
190
213
|
}
|
|
191
|
-
console.log(
|
|
214
|
+
console.log(chalk5.dim(`Found ${snapshots.length} baselines to download`));
|
|
192
215
|
console.log();
|
|
193
216
|
const batchSize = 5;
|
|
194
217
|
let downloaded = 0;
|
|
@@ -197,9 +220,11 @@ async function pullBaselines(config, options) {
|
|
|
197
220
|
await Promise.all(
|
|
198
221
|
batch.map(async (snap) => {
|
|
199
222
|
const imageUrl = `${config.server}/api/images/${snap.r2_key}`;
|
|
200
|
-
const res = await fetch(imageUrl
|
|
223
|
+
const res = await fetch(imageUrl, {
|
|
224
|
+
headers: { ...authHeaders(config) }
|
|
225
|
+
});
|
|
201
226
|
if (!res.ok) {
|
|
202
|
-
console.error(
|
|
227
|
+
console.error(chalk5.red(` Failed to download ${snap.screen_slug}: ${res.status}`));
|
|
203
228
|
return;
|
|
204
229
|
}
|
|
205
230
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
@@ -209,19 +234,19 @@ async function pullBaselines(config, options) {
|
|
|
209
234
|
writeFileSync(filePath, buffer);
|
|
210
235
|
downloaded++;
|
|
211
236
|
console.log(
|
|
212
|
-
|
|
237
|
+
chalk5.green(` [${downloaded}/${snapshots.length}]`) + ` ${subDir ? subDir + "/" : ""}${snap.screen_slug}.png`
|
|
213
238
|
);
|
|
214
239
|
})
|
|
215
240
|
);
|
|
216
241
|
}
|
|
217
242
|
console.log();
|
|
218
|
-
console.log(
|
|
243
|
+
console.log(chalk5.green(`Downloaded ${downloaded} baselines to ${outputDir}`));
|
|
219
244
|
}
|
|
220
245
|
|
|
221
246
|
// src/review.ts
|
|
222
247
|
import { execSync as execSync2 } from "child_process";
|
|
223
248
|
import { platform } from "os";
|
|
224
|
-
import
|
|
249
|
+
import chalk6 from "chalk";
|
|
225
250
|
function openBrowser(url) {
|
|
226
251
|
try {
|
|
227
252
|
const os = platform();
|
|
@@ -233,7 +258,7 @@ function openBrowser(url) {
|
|
|
233
258
|
execSync2(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
234
259
|
}
|
|
235
260
|
} catch {
|
|
236
|
-
console.log(
|
|
261
|
+
console.log(chalk6.dim(`Could not open browser. Visit the URL manually.`));
|
|
237
262
|
}
|
|
238
263
|
}
|
|
239
264
|
function sleep(ms) {
|
|
@@ -245,20 +270,20 @@ async function review(config, options) {
|
|
|
245
270
|
const timeoutSec = options.timeout ?? 300;
|
|
246
271
|
const branch = options.branch ?? execSync2("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
|
|
247
272
|
const sha = options.sha ?? execSync2("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
248
|
-
console.log(
|
|
249
|
-
console.log(
|
|
250
|
-
console.log(
|
|
273
|
+
console.log(chalk6.dim(`Project: ${config.project}`));
|
|
274
|
+
console.log(chalk6.dim(`Branch: ${branch}`));
|
|
275
|
+
console.log(chalk6.dim(`SHA: ${sha.slice(0, 7)}`));
|
|
251
276
|
console.log();
|
|
252
|
-
console.log(
|
|
277
|
+
console.log(chalk6.dim("Uploading screenshots..."));
|
|
253
278
|
await upload(config, { branch, sha });
|
|
254
279
|
console.log();
|
|
255
|
-
console.log(
|
|
280
|
+
console.log(chalk6.dim("Creating review session..."));
|
|
256
281
|
const reviewUrl = `${config.server}/api/reviews`;
|
|
257
282
|
let reviewData;
|
|
258
283
|
try {
|
|
259
284
|
const res = await fetch(reviewUrl, {
|
|
260
285
|
method: "POST",
|
|
261
|
-
headers: { "Content-Type": "application/json", Origin: config.server },
|
|
286
|
+
headers: { "Content-Type": "application/json", Origin: config.server, ...authHeaders(config) },
|
|
262
287
|
body: JSON.stringify({
|
|
263
288
|
project: config.project,
|
|
264
289
|
branch,
|
|
@@ -266,31 +291,32 @@ async function review(config, options) {
|
|
|
266
291
|
})
|
|
267
292
|
});
|
|
268
293
|
const data = await res.json();
|
|
294
|
+
checkAuthError(res);
|
|
269
295
|
if (!res.ok) {
|
|
270
|
-
console.error(
|
|
296
|
+
console.error(chalk6.red(`Failed to create review: ${JSON.stringify(data)}`));
|
|
271
297
|
process.exit(1);
|
|
272
298
|
}
|
|
273
299
|
reviewData = data;
|
|
274
300
|
} catch (err) {
|
|
275
|
-
console.error(
|
|
301
|
+
console.error(chalk6.red(`Request failed: ${err}`));
|
|
276
302
|
process.exit(1);
|
|
277
303
|
}
|
|
278
304
|
const allZero = reviewData.diffs.length === 0 || reviewData.diffs.every((d) => d.mismatchPercentage === 0);
|
|
279
305
|
if (allZero) {
|
|
280
|
-
console.log(
|
|
306
|
+
console.log(chalk6.green("No visual changes detected."));
|
|
281
307
|
process.exit(0);
|
|
282
308
|
}
|
|
283
309
|
const sessionUrl = `${config.server}/reviews/${reviewData.review.id}`;
|
|
284
310
|
console.log();
|
|
285
|
-
console.log(
|
|
311
|
+
console.log(chalk6.bold("Visual changes detected:"));
|
|
286
312
|
console.log();
|
|
287
313
|
for (const d of reviewData.diffs) {
|
|
288
314
|
const pct = d.mismatchPercentage.toFixed(2) + "%";
|
|
289
|
-
const color = d.mismatchPercentage > 10 ?
|
|
315
|
+
const color = d.mismatchPercentage > 10 ? chalk6.red : d.mismatchPercentage > 1 ? chalk6.yellow : chalk6.green;
|
|
290
316
|
console.log(` ${d.screen.padEnd(30)} ${color(pct)}`);
|
|
291
317
|
}
|
|
292
318
|
console.log();
|
|
293
|
-
console.log(`Review: ${
|
|
319
|
+
console.log(`Review: ${chalk6.cyan(sessionUrl)}`);
|
|
294
320
|
if (shouldOpen) {
|
|
295
321
|
openBrowser(sessionUrl);
|
|
296
322
|
}
|
|
@@ -298,7 +324,7 @@ async function review(config, options) {
|
|
|
298
324
|
return;
|
|
299
325
|
}
|
|
300
326
|
console.log();
|
|
301
|
-
console.log(
|
|
327
|
+
console.log(chalk6.dim(`Polling for verdict (timeout: ${timeoutSec}s)...`));
|
|
302
328
|
const pollInterval = 3e3;
|
|
303
329
|
const startTime = Date.now();
|
|
304
330
|
const deadline = startTime + timeoutSec * 1e3;
|
|
@@ -306,23 +332,23 @@ async function review(config, options) {
|
|
|
306
332
|
await sleep(pollInterval);
|
|
307
333
|
try {
|
|
308
334
|
const res = await fetch(`${config.server}/api/reviews/${reviewData.review.id}`, {
|
|
309
|
-
headers: { Origin: config.server }
|
|
335
|
+
headers: { Origin: config.server, ...authHeaders(config) }
|
|
310
336
|
});
|
|
311
337
|
const data = await res.json();
|
|
312
338
|
if (!res.ok) continue;
|
|
313
339
|
if (data.review.status === "approved") {
|
|
314
340
|
console.log();
|
|
315
|
-
console.log(
|
|
341
|
+
console.log(chalk6.green.bold("Review approved!"));
|
|
316
342
|
process.exit(0);
|
|
317
343
|
}
|
|
318
344
|
if (data.review.status === "rejected") {
|
|
319
345
|
console.log();
|
|
320
|
-
console.log(
|
|
346
|
+
console.log(chalk6.red.bold("Review rejected."));
|
|
321
347
|
const rejected = data.diffs.filter((d) => d.status === "rejected");
|
|
322
348
|
if (rejected.length > 0) {
|
|
323
|
-
console.log(
|
|
349
|
+
console.log(chalk6.dim("Rejected screens:"));
|
|
324
350
|
for (const d of rejected) {
|
|
325
|
-
console.log(
|
|
351
|
+
console.log(chalk6.red(` - ${d.base.screen_slug}`));
|
|
326
352
|
}
|
|
327
353
|
}
|
|
328
354
|
process.exit(1);
|
|
@@ -331,8 +357,8 @@ async function review(config, options) {
|
|
|
331
357
|
}
|
|
332
358
|
}
|
|
333
359
|
console.log();
|
|
334
|
-
console.log(
|
|
335
|
-
console.log(
|
|
360
|
+
console.log(chalk6.yellow(`Review timed out after ${timeoutSec}s.`));
|
|
361
|
+
console.log(chalk6.dim(`Visit ${sessionUrl} to complete the review.`));
|
|
336
362
|
process.exit(2);
|
|
337
363
|
}
|
|
338
364
|
|
|
@@ -340,7 +366,7 @@ async function review(config, options) {
|
|
|
340
366
|
import { existsSync as existsSync3, writeFileSync as writeFileSync2, unlinkSync, chmodSync, readFileSync as readFileSync3 } from "fs";
|
|
341
367
|
import { resolve as resolve4, join } from "path";
|
|
342
368
|
import { execSync as execSync3 } from "child_process";
|
|
343
|
-
import
|
|
369
|
+
import chalk7 from "chalk";
|
|
344
370
|
var HOOK_MARKER = "# git-shots-hook";
|
|
345
371
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
346
372
|
${HOOK_MARKER}
|
|
@@ -406,14 +432,14 @@ async function hookInstall(cwd = process.cwd()) {
|
|
|
406
432
|
if (existsSync3(hookPath)) {
|
|
407
433
|
const existing = readFileSync3(hookPath, "utf-8");
|
|
408
434
|
if (existing.includes(HOOK_MARKER)) {
|
|
409
|
-
console.log(
|
|
435
|
+
console.log(chalk7.yellow("git-shots pre-push hook is already installed."));
|
|
410
436
|
return;
|
|
411
437
|
}
|
|
412
438
|
console.error(
|
|
413
|
-
|
|
439
|
+
chalk7.red("A pre-push hook already exists at ") + chalk7.dim(hookPath)
|
|
414
440
|
);
|
|
415
441
|
console.error(
|
|
416
|
-
|
|
442
|
+
chalk7.dim("Remove or rename it first, then re-run this command.")
|
|
417
443
|
);
|
|
418
444
|
process.exit(1);
|
|
419
445
|
}
|
|
@@ -422,37 +448,39 @@ async function hookInstall(cwd = process.cwd()) {
|
|
|
422
448
|
chmodSync(hookPath, 493);
|
|
423
449
|
} catch {
|
|
424
450
|
}
|
|
425
|
-
console.log(
|
|
451
|
+
console.log(chalk7.green("Installed pre-push hook at ") + chalk7.dim(hookPath));
|
|
426
452
|
console.log();
|
|
427
|
-
console.log(
|
|
428
|
-
console.log(
|
|
453
|
+
console.log(chalk7.dim("The hook will run `git-shots review` before every push"));
|
|
454
|
+
console.log(chalk7.dim("when .git-shots.json is present and screenshots exist."));
|
|
455
|
+
console.log();
|
|
456
|
+
console.log(chalk7.dim("Set GIT_SHOTS_API_KEY in your environment or add apiKey to .git-shots.json"));
|
|
429
457
|
}
|
|
430
458
|
async function hookUninstall(cwd = process.cwd()) {
|
|
431
459
|
const gitDir = getGitDir(cwd);
|
|
432
460
|
const hooksDir = resolve4(cwd, gitDir, "hooks");
|
|
433
461
|
const hookPath = join(hooksDir, "pre-push");
|
|
434
462
|
if (!existsSync3(hookPath)) {
|
|
435
|
-
console.log(
|
|
463
|
+
console.log(chalk7.yellow("No pre-push hook found."));
|
|
436
464
|
return;
|
|
437
465
|
}
|
|
438
466
|
const existing = readFileSync3(hookPath, "utf-8");
|
|
439
467
|
if (!existing.includes(HOOK_MARKER)) {
|
|
440
|
-
console.error(
|
|
441
|
-
console.error(
|
|
468
|
+
console.error(chalk7.red("Pre-push hook exists but was not installed by git-shots."));
|
|
469
|
+
console.error(chalk7.dim("Remove it manually if you want: ") + chalk7.dim(hookPath));
|
|
442
470
|
process.exit(1);
|
|
443
471
|
}
|
|
444
472
|
unlinkSync(hookPath);
|
|
445
|
-
console.log(
|
|
473
|
+
console.log(chalk7.green("Removed git-shots pre-push hook."));
|
|
446
474
|
}
|
|
447
475
|
|
|
448
476
|
// src/flows.ts
|
|
449
|
-
import
|
|
477
|
+
import chalk8 from "chalk";
|
|
450
478
|
async function syncFlows(config) {
|
|
451
479
|
if (!config.flows || config.flows.length === 0) {
|
|
452
|
-
console.log(
|
|
480
|
+
console.log(chalk8.dim("No flows defined in config, skipping sync."));
|
|
453
481
|
return;
|
|
454
482
|
}
|
|
455
|
-
console.log(
|
|
483
|
+
console.log(chalk8.dim(`Syncing ${config.flows.length} flow(s)...`));
|
|
456
484
|
const listUrl = `${config.server}/api/projects/${config.project}/flows`;
|
|
457
485
|
let existingFlows = [];
|
|
458
486
|
try {
|
|
@@ -479,7 +507,7 @@ async function syncFlows(config) {
|
|
|
479
507
|
});
|
|
480
508
|
if (!patchRes.ok) {
|
|
481
509
|
const err = await patchRes.text();
|
|
482
|
-
console.error(
|
|
510
|
+
console.error(chalk8.red(` Failed to update flow ${flow.slug}: ${err}`));
|
|
483
511
|
continue;
|
|
484
512
|
}
|
|
485
513
|
const stepsUrl = `${config.server}/api/projects/${config.project}/flows/${flow.slug}/steps`;
|
|
@@ -489,10 +517,10 @@ async function syncFlows(config) {
|
|
|
489
517
|
body: JSON.stringify({ steps: flow.steps })
|
|
490
518
|
});
|
|
491
519
|
if (stepsRes.ok) {
|
|
492
|
-
console.log(
|
|
520
|
+
console.log(chalk8.green(` Updated flow: ${flow.name} (${flow.steps.length} steps)`));
|
|
493
521
|
} else {
|
|
494
522
|
const err = await stepsRes.text();
|
|
495
|
-
console.error(
|
|
523
|
+
console.error(chalk8.red(` Failed to update steps for ${flow.slug}: ${err}`));
|
|
496
524
|
}
|
|
497
525
|
} else {
|
|
498
526
|
const createUrl = `${config.server}/api/projects/${config.project}/flows`;
|
|
@@ -508,14 +536,14 @@ async function syncFlows(config) {
|
|
|
508
536
|
})
|
|
509
537
|
});
|
|
510
538
|
if (res.ok) {
|
|
511
|
-
console.log(
|
|
539
|
+
console.log(chalk8.green(` Created flow: ${flow.name} (${flow.steps.length} steps)`));
|
|
512
540
|
} else {
|
|
513
541
|
const err = await res.text();
|
|
514
|
-
console.error(
|
|
542
|
+
console.error(chalk8.red(` Failed to create flow ${flow.slug}: ${err}`));
|
|
515
543
|
}
|
|
516
544
|
}
|
|
517
545
|
}
|
|
518
|
-
console.log(
|
|
546
|
+
console.log(chalk8.green("Flows synced."));
|
|
519
547
|
}
|
|
520
548
|
|
|
521
549
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
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
|
-
"glob": "^11.0.0"
|
|
20
|
-
},
|
|
21
|
-
"devDependencies": {
|
|
22
|
-
"tsup": "^8.0.0",
|
|
23
|
-
"typescript": "^5.0.0",
|
|
24
|
-
"@types/node": "^22.0.0"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "git-shots-cli",
|
|
3
|
+
"version": "0.4.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
|
+
"glob": "^11.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"tsup": "^8.0.0",
|
|
23
|
+
"typescript": "^5.0.0",
|
|
24
|
+
"@types/node": "^22.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|