git-shots-cli 0.1.2 → 0.2.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 CHANGED
@@ -235,7 +235,7 @@ function openBrowser(url) {
235
235
  }
236
236
  }
237
237
  function sleep(ms) {
238
- return new Promise((resolve4) => setTimeout(resolve4, ms));
238
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
239
239
  }
240
240
  async function review(config, options) {
241
241
  const shouldOpen = options.open ?? true;
@@ -334,6 +334,115 @@ async function review(config, options) {
334
334
  process.exit(2);
335
335
  }
336
336
 
337
+ // src/hook.ts
338
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2, unlinkSync, chmodSync, readFileSync as readFileSync3 } from "fs";
339
+ import { resolve as resolve4, join } from "path";
340
+ import { execSync as execSync3 } from "child_process";
341
+ import chalk6 from "chalk";
342
+ var HOOK_MARKER = "# git-shots-hook";
343
+ var HOOK_SCRIPT = `#!/bin/sh
344
+ ${HOOK_MARKER}
345
+ # Pre-push hook: runs git-shots visual review before pushing.
346
+ # Installed by: git-shots hook install
347
+
348
+ # Skip if no .git-shots.json config
349
+ if [ ! -f ".git-shots.json" ]; then
350
+ exit 0
351
+ fi
352
+
353
+ # Read screenshots directory from config (default: docs/screenshots/current)
354
+ SCREENSHOTS_DIR=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('.git-shots.json','utf-8'));console.log(c.directory||'docs/screenshots/current')}catch{console.log('docs/screenshots/current')}" 2>/dev/null)
355
+
356
+ # Skip if no screenshots directory or no PNGs
357
+ if [ ! -d "$SCREENSHOTS_DIR" ]; then
358
+ exit 0
359
+ fi
360
+
361
+ PNG_COUNT=$(find "$SCREENSHOTS_DIR" -name "*.png" 2>/dev/null | head -1)
362
+ if [ -z "$PNG_COUNT" ]; then
363
+ exit 0
364
+ fi
365
+
366
+ # Skip on main/master \u2014 no base to diff against
367
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
368
+ if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
369
+ exit 0
370
+ fi
371
+
372
+ echo ""
373
+ echo "git-shots: Running visual review before push..."
374
+ echo ""
375
+
376
+ git-shots review
377
+ EXIT_CODE=$?
378
+
379
+ if [ $EXIT_CODE -eq 1 ]; then
380
+ echo ""
381
+ echo "git-shots: Visual review rejected. Push blocked."
382
+ exit 1
383
+ fi
384
+
385
+ if [ $EXIT_CODE -eq 2 ]; then
386
+ echo ""
387
+ echo "git-shots: Visual review timed out. Push allowed (review pending)."
388
+ exit 0
389
+ fi
390
+
391
+ exit 0
392
+ `;
393
+ function getGitDir(cwd) {
394
+ try {
395
+ return execSync3("git rev-parse --git-dir", { cwd, encoding: "utf-8" }).trim();
396
+ } catch {
397
+ throw new Error("Not a git repository");
398
+ }
399
+ }
400
+ async function hookInstall(cwd = process.cwd()) {
401
+ const gitDir = getGitDir(cwd);
402
+ const hooksDir = resolve4(cwd, gitDir, "hooks");
403
+ const hookPath = join(hooksDir, "pre-push");
404
+ if (existsSync3(hookPath)) {
405
+ const existing = readFileSync3(hookPath, "utf-8");
406
+ if (existing.includes(HOOK_MARKER)) {
407
+ console.log(chalk6.yellow("git-shots pre-push hook is already installed."));
408
+ return;
409
+ }
410
+ console.error(
411
+ chalk6.red("A pre-push hook already exists at ") + chalk6.dim(hookPath)
412
+ );
413
+ console.error(
414
+ chalk6.dim("Remove or rename it first, then re-run this command.")
415
+ );
416
+ process.exit(1);
417
+ }
418
+ writeFileSync2(hookPath, HOOK_SCRIPT, { mode: 493 });
419
+ try {
420
+ chmodSync(hookPath, 493);
421
+ } catch {
422
+ }
423
+ console.log(chalk6.green("Installed pre-push hook at ") + chalk6.dim(hookPath));
424
+ console.log();
425
+ console.log(chalk6.dim("The hook will run `git-shots review` before every push"));
426
+ console.log(chalk6.dim("when .git-shots.json is present and screenshots exist."));
427
+ }
428
+ async function hookUninstall(cwd = process.cwd()) {
429
+ const gitDir = getGitDir(cwd);
430
+ const hooksDir = resolve4(cwd, gitDir, "hooks");
431
+ const hookPath = join(hooksDir, "pre-push");
432
+ if (!existsSync3(hookPath)) {
433
+ console.log(chalk6.yellow("No pre-push hook found."));
434
+ return;
435
+ }
436
+ const existing = readFileSync3(hookPath, "utf-8");
437
+ if (!existing.includes(HOOK_MARKER)) {
438
+ console.error(chalk6.red("Pre-push hook exists but was not installed by git-shots."));
439
+ console.error(chalk6.dim("Remove it manually if you want: ") + chalk6.dim(hookPath));
440
+ process.exit(1);
441
+ }
442
+ unlinkSync(hookPath);
443
+ console.log(chalk6.green("Removed git-shots pre-push hook."));
444
+ }
445
+
337
446
  // src/index.ts
338
447
  var program = new Command();
339
448
  program.name("git-shots").description("CLI for git-shots visual regression platform").version("0.1.0");
@@ -395,4 +504,11 @@ program.command("review").description("Upload screenshots, create review session
395
504
  timeout: options.timeout
396
505
  });
397
506
  });
507
+ var hook = program.command("hook").description("Manage git hooks for automatic visual review");
508
+ hook.command("install").description("Install a pre-push hook that runs git-shots review before each push").action(async () => {
509
+ await hookInstall();
510
+ });
511
+ hook.command("uninstall").description("Remove the git-shots pre-push hook").action(async () => {
512
+ await hookUninstall();
513
+ });
398
514
  program.parse();
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "git-shots-cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "CLI for git-shots visual regression platform",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "git-shots": "./dist/index.js"
8
8
  },
9
+ "files": [
10
+ "dist"
11
+ ],
9
12
  "scripts": {
10
13
  "build": "tsup src/index.ts --format esm --dts",
11
14
  "dev": "tsup src/index.ts --format esm --watch"
@@ -1,9 +0,0 @@
1
- Stack trace:
2
- Frame Function Args
3
- 000FFFFBED8 0018006137E (0018026DF0D, 0018024E186, 000FFFFBED8, 000FFFFADD0)
4
- 000FFFFBED8 00180049229 (00000000000, 00000000000, 00000000000, 00000000000)
5
- 000FFFFBED8 00180049262 (0018026DFC9, 000FFFFBD88, 000FFFFBED8, 00000000000)
6
- 000FFFFBED8 001800B5C58 (00000000000, 00000000000, 00000000000, 00000000000)
7
- 000FFFFBED8 001800B5DDD (000FFFFBEF0, 00000000000, 00000000000, 00000000000)
8
- 000FFFFC1A0 001800B740C (000FFFFBEF0, 00000000000, 00000000000, 00000000000)
9
- End of stack trace
package/src/compare.ts DELETED
@@ -1,57 +0,0 @@
1
- import chalk from 'chalk';
2
- import type { GitShotsConfig } from './config.js';
3
-
4
- export async function compare(
5
- config: GitShotsConfig,
6
- options: { base?: string; head: string; threshold?: number }
7
- ) {
8
- const url = `${config.server}/api/compare`;
9
- const body = {
10
- project: config.project,
11
- base: options.base ?? 'main',
12
- head: options.head,
13
- threshold: options.threshold ?? 0.1
14
- };
15
-
16
- console.log(chalk.dim(`Comparing ${body.base} vs ${body.head} for ${config.project}...`));
17
-
18
- try {
19
- const res = await fetch(url, {
20
- method: 'POST',
21
- headers: { 'Content-Type': 'application/json', Origin: config.server },
22
- body: JSON.stringify(body)
23
- });
24
- const data = await res.json();
25
-
26
- if (!res.ok) {
27
- console.error(chalk.red(`Compare failed: ${JSON.stringify(data)}`));
28
- process.exit(1);
29
- }
30
-
31
- console.log();
32
- console.log(`Compared ${chalk.bold(data.compared)} screens`);
33
- console.log();
34
-
35
- if (data.diffs.length === 0) {
36
- console.log(chalk.green('No visual differences found!'));
37
- return;
38
- }
39
-
40
- // Print table
41
- console.log(chalk.dim('Screen'.padEnd(30) + 'Mismatch'.padEnd(15) + 'Pixels'));
42
- console.log(chalk.dim('-'.repeat(55)));
43
-
44
- for (const d of data.diffs) {
45
- const pct = d.mismatchPercentage.toFixed(2) + '%';
46
- const color = d.mismatchPercentage > 10 ? chalk.red : d.mismatchPercentage > 1 ? chalk.yellow : chalk.green;
47
- console.log(
48
- d.screen.padEnd(30) +
49
- color(pct.padEnd(15)) +
50
- chalk.dim(d.mismatchPixels.toLocaleString())
51
- );
52
- }
53
- } catch (err) {
54
- console.error(chalk.red(`Request failed: ${err}`));
55
- process.exit(1);
56
- }
57
- }
package/src/config.ts DELETED
@@ -1,28 +0,0 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
-
4
- export interface GitShotsConfig {
5
- project: string;
6
- server: string;
7
- directory: string;
8
- }
9
-
10
- const DEFAULT_CONFIG: GitShotsConfig = {
11
- project: '',
12
- server: 'https://git-shots.rijid356.workers.dev',
13
- directory: 'docs/screenshots/current'
14
- };
15
-
16
- export function loadConfig(cwd: string = process.cwd()): GitShotsConfig {
17
- const configPath = resolve(cwd, '.git-shots.json');
18
- if (!existsSync(configPath)) {
19
- return DEFAULT_CONFIG;
20
- }
21
- try {
22
- const raw = readFileSync(configPath, 'utf-8');
23
- const parsed = JSON.parse(raw);
24
- return { ...DEFAULT_CONFIG, ...parsed };
25
- } catch {
26
- return DEFAULT_CONFIG;
27
- }
28
- }
package/src/index.ts DELETED
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import { loadConfig } from './config.js';
4
- import { upload } from './upload.js';
5
- import { compare } from './compare.js';
6
- import { status } from './status.js';
7
- import { pullBaselines } from './pull-baselines.js';
8
- import { review } from './review.js';
9
-
10
- const program = new Command();
11
-
12
- program
13
- .name('git-shots')
14
- .description('CLI for git-shots visual regression platform')
15
- .version('0.1.0');
16
-
17
- program
18
- .command('upload')
19
- .description('Upload screenshots to git-shots')
20
- .option('-p, --project <slug>', 'Project slug')
21
- .option('-s, --server <url>', 'Server URL')
22
- .option('-d, --directory <path>', 'Screenshots directory')
23
- .option('-b, --branch <name>', 'Git branch (auto-detected)')
24
- .option('--sha <hash>', 'Git SHA (auto-detected)')
25
- .action(async (options) => {
26
- const config = loadConfig();
27
- if (options.project) config.project = options.project;
28
- if (options.server) config.server = options.server;
29
- if (options.directory) config.directory = options.directory;
30
- if (!config.project) {
31
- console.error('Error: project slug required. Use --project or .git-shots.json');
32
- process.exit(1);
33
- }
34
- await upload(config, { branch: options.branch, sha: options.sha });
35
- });
36
-
37
- program
38
- .command('compare')
39
- .description('Compare screenshots between branches')
40
- .requiredOption('--head <branch>', 'Head branch to compare')
41
- .option('-p, --project <slug>', 'Project slug')
42
- .option('-s, --server <url>', 'Server URL')
43
- .option('--base <branch>', 'Base branch (default: main)')
44
- .option('-t, --threshold <number>', 'Mismatch threshold 0-1', parseFloat)
45
- .action(async (options) => {
46
- const config = loadConfig();
47
- if (options.project) config.project = options.project;
48
- if (options.server) config.server = options.server;
49
- if (!config.project) {
50
- console.error('Error: project slug required. Use --project or .git-shots.json');
51
- process.exit(1);
52
- }
53
- await compare(config, { base: options.base, head: options.head, threshold: options.threshold });
54
- });
55
-
56
- program
57
- .command('status')
58
- .description('Show current diff status')
59
- .option('-p, --project <slug>', 'Project slug')
60
- .option('-s, --server <url>', 'Server URL')
61
- .action(async (options) => {
62
- const config = loadConfig();
63
- if (options.project) config.project = options.project;
64
- if (options.server) config.server = options.server;
65
- if (!config.project) {
66
- console.error('Error: project slug required. Use --project or .git-shots.json');
67
- process.exit(1);
68
- }
69
- await status(config);
70
- });
71
-
72
- program
73
- .command('pull-baselines')
74
- .description('Download baseline screenshots from git-shots')
75
- .option('-p, --project <slug>', 'Project slug')
76
- .option('-s, --server <url>', 'Server URL')
77
- .option('-o, --output <path>', 'Output directory')
78
- .option('-b, --branch <name>', 'Branch to pull baselines from (default: main)')
79
- .action(async (options) => {
80
- const config = loadConfig();
81
- if (options.project) config.project = options.project;
82
- if (options.server) config.server = options.server;
83
- if (!config.project) {
84
- console.error('Error: project slug required. Use --project or .git-shots.json');
85
- process.exit(1);
86
- }
87
- await pullBaselines(config, { branch: options.branch, output: options.output });
88
- });
89
-
90
- program
91
- .command('review')
92
- .description('Upload screenshots, create review session, and poll for verdict')
93
- .option('-p, --project <slug>', 'Project slug')
94
- .option('-s, --server <url>', 'Server URL')
95
- .option('-d, --directory <path>', 'Screenshots directory')
96
- .option('-b, --branch <name>', 'Git branch (auto-detected)')
97
- .option('--sha <hash>', 'Git SHA (auto-detected)')
98
- .option('--open', 'Open review URL in browser', true)
99
- .option('--no-open', 'Do not open review URL in browser')
100
- .option('--poll', 'Poll for verdict and exit with code', true)
101
- .option('--no-poll', 'Do not poll for verdict')
102
- .option('--timeout <seconds>', 'Polling timeout in seconds', parseInt, 300)
103
- .action(async (options) => {
104
- const config = loadConfig();
105
- if (options.project) config.project = options.project;
106
- if (options.server) config.server = options.server;
107
- if (options.directory) config.directory = options.directory;
108
- if (!config.project) {
109
- console.error('Error: project slug required. Use --project or .git-shots.json');
110
- process.exit(1);
111
- }
112
- await review(config, {
113
- branch: options.branch,
114
- sha: options.sha,
115
- open: options.open,
116
- poll: options.poll,
117
- timeout: options.timeout
118
- });
119
- });
120
-
121
- program.parse();
@@ -1,87 +0,0 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
- import { resolve, dirname } from 'node:path';
3
- import chalk from 'chalk';
4
- import type { GitShotsConfig } from './config.js';
5
-
6
- interface Snapshot {
7
- screen_slug: string;
8
- category: string | null;
9
- r2_key: string;
10
- git_sha: string;
11
- width: number;
12
- height: number;
13
- }
14
-
15
- export async function pullBaselines(
16
- config: GitShotsConfig,
17
- options: { branch?: string; output?: string }
18
- ) {
19
- const branch = options.branch ?? 'main';
20
- const outputDir = resolve(process.cwd(), options.output ?? config.directory);
21
-
22
- console.log(chalk.dim(`Project: ${config.project}`));
23
- console.log(chalk.dim(`Branch: ${branch}`));
24
- console.log(chalk.dim(`Output: ${outputDir}`));
25
- console.log();
26
-
27
- // Fetch manifest
28
- const manifestUrl = `${config.server}/api/projects/${encodeURIComponent(config.project)}/snapshots?branch=${encodeURIComponent(branch)}`;
29
- let snapshots: Snapshot[];
30
-
31
- try {
32
- const res = await fetch(manifestUrl);
33
- const data = await res.json();
34
-
35
- if (!res.ok) {
36
- console.error(chalk.red(`Failed to fetch snapshots: ${JSON.stringify(data)}`));
37
- process.exit(1);
38
- }
39
-
40
- snapshots = data.snapshots;
41
- } catch (err) {
42
- console.error(chalk.red(`Request failed: ${err}`));
43
- process.exit(1);
44
- }
45
-
46
- if (snapshots.length === 0) {
47
- console.log(chalk.yellow('No baseline snapshots found.'));
48
- return;
49
- }
50
-
51
- console.log(chalk.dim(`Found ${snapshots.length} baselines to download`));
52
- console.log();
53
-
54
- // Download in batches of 5
55
- const batchSize = 5;
56
- let downloaded = 0;
57
-
58
- for (let i = 0; i < snapshots.length; i += batchSize) {
59
- const batch = snapshots.slice(i, i + batchSize);
60
- await Promise.all(
61
- batch.map(async (snap) => {
62
- const imageUrl = `${config.server}/api/images/${snap.r2_key}`;
63
- const res = await fetch(imageUrl);
64
- if (!res.ok) {
65
- console.error(chalk.red(` Failed to download ${snap.screen_slug}: ${res.status}`));
66
- return;
67
- }
68
-
69
- const buffer = Buffer.from(await res.arrayBuffer());
70
- const subDir = snap.category ?? '';
71
- const filePath = resolve(outputDir, subDir, `${snap.screen_slug}.png`);
72
-
73
- mkdirSync(dirname(filePath), { recursive: true });
74
- writeFileSync(filePath, buffer);
75
-
76
- downloaded++;
77
- console.log(
78
- chalk.green(` [${downloaded}/${snapshots.length}]`) +
79
- ` ${subDir ? subDir + '/' : ''}${snap.screen_slug}.png`
80
- );
81
- })
82
- );
83
- }
84
-
85
- console.log();
86
- console.log(chalk.green(`Downloaded ${downloaded} baselines to ${outputDir}`));
87
- }
package/src/review.ts DELETED
@@ -1,199 +0,0 @@
1
- import { execSync } from 'node:child_process';
2
- import { platform } from 'node:os';
3
- import chalk from 'chalk';
4
- import type { GitShotsConfig } from './config.js';
5
- import { upload } from './upload.js';
6
-
7
- interface ReviewOptions {
8
- branch?: string;
9
- sha?: string;
10
- open?: boolean;
11
- poll?: boolean;
12
- timeout?: number;
13
- }
14
-
15
- interface ReviewResponse {
16
- review: {
17
- id: number;
18
- status: string;
19
- url: string;
20
- };
21
- diffs: Array<{
22
- screen: string;
23
- mismatchPercentage: number;
24
- mismatchPixels: number;
25
- }>;
26
- }
27
-
28
- interface ReviewStatusResponse {
29
- review: {
30
- id: number;
31
- status: string;
32
- };
33
- diffs: Array<{
34
- base: { screen_slug: string };
35
- mismatch_percentage: number;
36
- status: string;
37
- }>;
38
- }
39
-
40
- function openBrowser(url: string): void {
41
- try {
42
- const os = platform();
43
- if (os === 'win32') {
44
- execSync(`start "" "${url}"`, { stdio: 'ignore' });
45
- } else if (os === 'darwin') {
46
- execSync(`open "${url}"`, { stdio: 'ignore' });
47
- } else {
48
- execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
49
- }
50
- } catch {
51
- console.log(chalk.dim(`Could not open browser. Visit the URL manually.`));
52
- }
53
- }
54
-
55
- function sleep(ms: number): Promise<void> {
56
- return new Promise((resolve) => setTimeout(resolve, ms));
57
- }
58
-
59
- export async function review(config: GitShotsConfig, options: ReviewOptions) {
60
- const shouldOpen = options.open ?? true;
61
- const shouldPoll = options.poll ?? true;
62
- const timeoutSec = options.timeout ?? 300;
63
-
64
- // Get git info
65
- const branch =
66
- options.branch ??
67
- execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
68
- const sha =
69
- options.sha ?? execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
70
-
71
- console.log(chalk.dim(`Project: ${config.project}`));
72
- console.log(chalk.dim(`Branch: ${branch}`));
73
- console.log(chalk.dim(`SHA: ${sha.slice(0, 7)}`));
74
- console.log();
75
-
76
- // Step 1: Upload screenshots
77
- console.log(chalk.dim('Uploading screenshots...'));
78
- await upload(config, { branch, sha });
79
- console.log();
80
-
81
- // Step 2: Create review session via POST /api/reviews
82
- console.log(chalk.dim('Creating review session...'));
83
-
84
- const reviewUrl = `${config.server}/api/reviews`;
85
- let reviewData: ReviewResponse;
86
-
87
- try {
88
- const res = await fetch(reviewUrl, {
89
- method: 'POST',
90
- headers: { 'Content-Type': 'application/json', Origin: config.server },
91
- body: JSON.stringify({
92
- project: config.project,
93
- branch,
94
- gitSha: sha
95
- })
96
- });
97
- const data = await res.json();
98
-
99
- if (!res.ok) {
100
- console.error(chalk.red(`Failed to create review: ${JSON.stringify(data)}`));
101
- process.exit(1);
102
- }
103
-
104
- reviewData = data as ReviewResponse;
105
- } catch (err) {
106
- console.error(chalk.red(`Request failed: ${err}`));
107
- process.exit(1);
108
- }
109
-
110
- // Step 3: Check if all diffs are 0% mismatch
111
- const allZero =
112
- reviewData.diffs.length === 0 ||
113
- reviewData.diffs.every((d) => d.mismatchPercentage === 0);
114
-
115
- if (allZero) {
116
- console.log(chalk.green('No visual changes detected.'));
117
- process.exit(0);
118
- }
119
-
120
- // Step 4: Print review URL and diff summary
121
- const sessionUrl = `${config.server}/reviews/${reviewData.review.id}`;
122
- console.log();
123
- console.log(chalk.bold('Visual changes detected:'));
124
- console.log();
125
-
126
- for (const d of reviewData.diffs) {
127
- const pct = d.mismatchPercentage.toFixed(2) + '%';
128
- const color =
129
- d.mismatchPercentage > 10
130
- ? chalk.red
131
- : d.mismatchPercentage > 1
132
- ? chalk.yellow
133
- : chalk.green;
134
- console.log(` ${d.screen.padEnd(30)} ${color(pct)}`);
135
- }
136
-
137
- console.log();
138
- console.log(`Review: ${chalk.cyan(sessionUrl)}`);
139
-
140
- // Step 5: Open in browser
141
- if (shouldOpen) {
142
- openBrowser(sessionUrl);
143
- }
144
-
145
- // Step 6: Poll for verdict
146
- if (!shouldPoll) {
147
- return;
148
- }
149
-
150
- console.log();
151
- console.log(chalk.dim(`Polling for verdict (timeout: ${timeoutSec}s)...`));
152
-
153
- const pollInterval = 3000;
154
- const startTime = Date.now();
155
- const deadline = startTime + timeoutSec * 1000;
156
-
157
- while (Date.now() < deadline) {
158
- await sleep(pollInterval);
159
-
160
- try {
161
- const res = await fetch(`${config.server}/api/reviews/${reviewData.review.id}`, {
162
- headers: { Origin: config.server }
163
- });
164
- const data = (await res.json()) as ReviewStatusResponse;
165
-
166
- if (!res.ok) continue;
167
-
168
- if (data.review.status === 'approved') {
169
- console.log();
170
- console.log(chalk.green.bold('Review approved!'));
171
- process.exit(0);
172
- }
173
-
174
- if (data.review.status === 'rejected') {
175
- console.log();
176
- console.log(chalk.red.bold('Review rejected.'));
177
-
178
- // Show which screens had issues
179
- const rejected = data.diffs.filter((d) => d.status === 'rejected');
180
- if (rejected.length > 0) {
181
- console.log(chalk.dim('Rejected screens:'));
182
- for (const d of rejected) {
183
- console.log(chalk.red(` - ${d.base.screen_slug}`));
184
- }
185
- }
186
-
187
- process.exit(1);
188
- }
189
- } catch {
190
- // Network error — keep polling
191
- }
192
- }
193
-
194
- // Timeout
195
- console.log();
196
- console.log(chalk.yellow(`Review timed out after ${timeoutSec}s.`));
197
- console.log(chalk.dim(`Visit ${sessionUrl} to complete the review.`));
198
- process.exit(2);
199
- }
package/src/status.ts DELETED
@@ -1,42 +0,0 @@
1
- import chalk from 'chalk';
2
- import type { GitShotsConfig } from './config.js';
3
-
4
- export async function status(config: GitShotsConfig) {
5
- const url = `${config.server}/api/diffs?project=${encodeURIComponent(config.project)}`;
6
-
7
- try {
8
- const res = await fetch(url);
9
- const data = await res.json();
10
-
11
- if (!res.ok) {
12
- console.error(chalk.red(`Status failed: ${JSON.stringify(data)}`));
13
- process.exit(1);
14
- }
15
-
16
- if (data.length === 0) {
17
- console.log(chalk.dim('No diffs found for this project.'));
18
- return;
19
- }
20
-
21
- console.log(`${chalk.bold(data.length)} diffs for ${config.project}`);
22
- console.log();
23
- console.log(chalk.dim('ID'.padEnd(8) + 'Status'.padEnd(12) + 'Mismatch'.padEnd(12) + 'Date'));
24
- console.log(chalk.dim('-'.repeat(50)));
25
-
26
- for (const { diff } of data) {
27
- const statusColor =
28
- diff.status === 'approved' ? chalk.green :
29
- diff.status === 'rejected' ? chalk.red :
30
- chalk.yellow;
31
- console.log(
32
- String(diff.id).padEnd(8) +
33
- statusColor(diff.status.padEnd(12)) +
34
- (diff.mismatch_percentage.toFixed(2) + '%').padEnd(12) +
35
- chalk.dim(new Date(diff.created_at * 1000).toLocaleDateString())
36
- );
37
- }
38
- } catch (err) {
39
- console.error(chalk.red(`Request failed: ${err}`));
40
- process.exit(1);
41
- }
42
- }
package/src/upload.ts DELETED
@@ -1,74 +0,0 @@
1
- import { readFileSync, existsSync } from 'node:fs';
2
- import { resolve, basename, relative, dirname } from 'node:path';
3
- import { execSync } from 'node:child_process';
4
- import { glob } from 'glob';
5
- import chalk from 'chalk';
6
- import type { GitShotsConfig } from './config.js';
7
-
8
- export async function upload(config: GitShotsConfig, options: { branch?: string; sha?: string }) {
9
- const dir = resolve(process.cwd(), config.directory);
10
- if (!existsSync(dir)) {
11
- console.error(chalk.red(`Directory not found: ${dir}`));
12
- process.exit(1);
13
- }
14
-
15
- // Get git info
16
- const branch = options.branch ?? execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
17
- const sha = options.sha ?? execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
18
-
19
- console.log(chalk.dim(`Project: ${config.project}`));
20
- console.log(chalk.dim(`Branch: ${branch}`));
21
- console.log(chalk.dim(`SHA: ${sha.slice(0, 7)}`));
22
- console.log(chalk.dim(`Dir: ${dir}`));
23
- console.log();
24
-
25
- // Find all PNGs
26
- const files = await glob('**/*.png', { cwd: dir });
27
- if (files.length === 0) {
28
- console.log(chalk.yellow('No PNG files found.'));
29
- return;
30
- }
31
-
32
- console.log(chalk.dim(`Found ${files.length} screenshots`));
33
-
34
- // Build multipart form
35
- const formData = new FormData();
36
- formData.append('project', config.project);
37
- formData.append('branch', branch);
38
- formData.append('gitSha', sha);
39
-
40
- for (const file of files) {
41
- const fullPath = resolve(dir, file);
42
- const buffer = readFileSync(fullPath);
43
- const blob = new Blob([buffer], { type: 'image/png' });
44
-
45
- // Use directory as field name for category derivation
46
- const dirName = dirname(file);
47
- const fieldName = dirName !== '.' ? `${dirName}/${basename(file)}` : basename(file);
48
-
49
- formData.append(fieldName, blob, basename(file));
50
- }
51
-
52
- // Upload
53
- const url = `${config.server}/api/upload`;
54
- console.log(chalk.dim(`Uploading to ${url}...`));
55
-
56
- try {
57
- const res = await fetch(url, {
58
- method: 'POST',
59
- body: formData,
60
- headers: { Origin: config.server },
61
- });
62
- const data = await res.json();
63
-
64
- if (!res.ok) {
65
- console.error(chalk.red(`Upload failed: ${JSON.stringify(data)}`));
66
- process.exit(1);
67
- }
68
-
69
- console.log(chalk.green(`Uploaded ${data.uploaded} screenshots`));
70
- } catch (err) {
71
- console.error(chalk.red(`Request failed: ${err}`));
72
- process.exit(1);
73
- }
74
- }
package/tsconfig.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "esModuleInterop": true,
7
- "strict": true,
8
- "outDir": "dist",
9
- "rootDir": "src",
10
- "declaration": true,
11
- "skipLibCheck": true
12
- },
13
- "include": ["src"]
14
- }