git-shots-cli 0.1.2 → 0.2.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 +117 -1
- package/package.json +23 -23
- package/src/compare.ts +57 -57
- package/src/config.ts +28 -28
- package/src/hook.ts +124 -0
- package/src/index.ts +140 -121
- package/src/pull-baselines.ts +87 -87
- package/src/review.ts +199 -199
- package/src/status.ts +42 -42
- package/src/upload.ts +74 -74
- package/tsconfig.json +14 -14
- package/bash.exe.stackdump +0 -9
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((
|
|
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,23 +1,23 @@
|
|
|
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
|
-
"scripts": {
|
|
10
|
-
"build": "tsup src/index.ts --format esm --dts",
|
|
11
|
-
"dev": "tsup src/index.ts --format esm --watch"
|
|
12
|
-
},
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"commander": "^12.0.0",
|
|
15
|
-
"chalk": "^5.3.0",
|
|
16
|
-
"glob": "^11.0.0"
|
|
17
|
-
},
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"tsup": "^8.0.0",
|
|
20
|
-
"typescript": "^5.0.0",
|
|
21
|
-
"@types/node": "^22.0.0"
|
|
22
|
-
}
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "git-shots-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI for git-shots visual regression platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"git-shots": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
11
|
+
"dev": "tsup src/index.ts --format esm --watch"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"commander": "^12.0.0",
|
|
15
|
+
"chalk": "^5.3.0",
|
|
16
|
+
"glob": "^11.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"tsup": "^8.0.0",
|
|
20
|
+
"typescript": "^5.0.0",
|
|
21
|
+
"@types/node": "^22.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/compare.ts
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
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
|
-
}
|
|
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
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
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
|
-
}
|
|
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/hook.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { existsSync, writeFileSync, unlinkSync, chmodSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
const HOOK_MARKER = '# git-shots-hook';
|
|
7
|
+
|
|
8
|
+
const HOOK_SCRIPT = `#!/bin/sh
|
|
9
|
+
${HOOK_MARKER}
|
|
10
|
+
# Pre-push hook: runs git-shots visual review before pushing.
|
|
11
|
+
# Installed by: git-shots hook install
|
|
12
|
+
|
|
13
|
+
# Skip if no .git-shots.json config
|
|
14
|
+
if [ ! -f ".git-shots.json" ]; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Read screenshots directory from config (default: docs/screenshots/current)
|
|
19
|
+
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)
|
|
20
|
+
|
|
21
|
+
# Skip if no screenshots directory or no PNGs
|
|
22
|
+
if [ ! -d "$SCREENSHOTS_DIR" ]; then
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
PNG_COUNT=$(find "$SCREENSHOTS_DIR" -name "*.png" 2>/dev/null | head -1)
|
|
27
|
+
if [ -z "$PNG_COUNT" ]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Skip on main/master — no base to diff against
|
|
32
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
33
|
+
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
echo ""
|
|
38
|
+
echo "git-shots: Running visual review before push..."
|
|
39
|
+
echo ""
|
|
40
|
+
|
|
41
|
+
git-shots review
|
|
42
|
+
EXIT_CODE=$?
|
|
43
|
+
|
|
44
|
+
if [ $EXIT_CODE -eq 1 ]; then
|
|
45
|
+
echo ""
|
|
46
|
+
echo "git-shots: Visual review rejected. Push blocked."
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
if [ $EXIT_CODE -eq 2 ]; then
|
|
51
|
+
echo ""
|
|
52
|
+
echo "git-shots: Visual review timed out. Push allowed (review pending)."
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
exit 0
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
function getGitDir(cwd: string): string {
|
|
60
|
+
try {
|
|
61
|
+
return execSync('git rev-parse --git-dir', { cwd, encoding: 'utf-8' }).trim();
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error('Not a git repository');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function hookInstall(cwd: string = process.cwd()) {
|
|
68
|
+
const gitDir = getGitDir(cwd);
|
|
69
|
+
const hooksDir = resolve(cwd, gitDir, 'hooks');
|
|
70
|
+
const hookPath = join(hooksDir, 'pre-push');
|
|
71
|
+
|
|
72
|
+
// Check if a pre-push hook already exists
|
|
73
|
+
if (existsSync(hookPath)) {
|
|
74
|
+
const existing = readFileSync(hookPath, 'utf-8');
|
|
75
|
+
if (existing.includes(HOOK_MARKER)) {
|
|
76
|
+
console.log(chalk.yellow('git-shots pre-push hook is already installed.'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Another hook exists — don't overwrite
|
|
81
|
+
console.error(
|
|
82
|
+
chalk.red('A pre-push hook already exists at ') + chalk.dim(hookPath)
|
|
83
|
+
);
|
|
84
|
+
console.error(
|
|
85
|
+
chalk.dim('Remove or rename it first, then re-run this command.')
|
|
86
|
+
);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeFileSync(hookPath, HOOK_SCRIPT, { mode: 0o755 });
|
|
91
|
+
|
|
92
|
+
// chmod on non-Windows (writeFileSync mode doesn't always stick)
|
|
93
|
+
try {
|
|
94
|
+
chmodSync(hookPath, 0o755);
|
|
95
|
+
} catch {
|
|
96
|
+
// Windows — mode already set via writeFileSync
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.green('Installed pre-push hook at ') + chalk.dim(hookPath));
|
|
100
|
+
console.log();
|
|
101
|
+
console.log(chalk.dim('The hook will run `git-shots review` before every push'));
|
|
102
|
+
console.log(chalk.dim('when .git-shots.json is present and screenshots exist.'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function hookUninstall(cwd: string = process.cwd()) {
|
|
106
|
+
const gitDir = getGitDir(cwd);
|
|
107
|
+
const hooksDir = resolve(cwd, gitDir, 'hooks');
|
|
108
|
+
const hookPath = join(hooksDir, 'pre-push');
|
|
109
|
+
|
|
110
|
+
if (!existsSync(hookPath)) {
|
|
111
|
+
console.log(chalk.yellow('No pre-push hook found.'));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const existing = readFileSync(hookPath, 'utf-8');
|
|
116
|
+
if (!existing.includes(HOOK_MARKER)) {
|
|
117
|
+
console.error(chalk.red('Pre-push hook exists but was not installed by git-shots.'));
|
|
118
|
+
console.error(chalk.dim('Remove it manually if you want: ') + chalk.dim(hookPath));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
unlinkSync(hookPath);
|
|
123
|
+
console.log(chalk.green('Removed git-shots pre-push hook.'));
|
|
124
|
+
}
|