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/src/index.ts
CHANGED
|
@@ -1,121 +1,140 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
.
|
|
21
|
-
.option('-
|
|
22
|
-
.option('-
|
|
23
|
-
.option('-
|
|
24
|
-
.option('--
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (options.
|
|
29
|
-
if (options.
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.option('-
|
|
43
|
-
.option('--
|
|
44
|
-
.option('
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (options.
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.option('-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (options.
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
.option('-
|
|
77
|
-
.option('-
|
|
78
|
-
.option('-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (options.
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.option('-
|
|
95
|
-
.option('-
|
|
96
|
-
.option('-
|
|
97
|
-
.option('--
|
|
98
|
-
.option('--
|
|
99
|
-
.option('--
|
|
100
|
-
.option('--
|
|
101
|
-
.option('--
|
|
102
|
-
.option('--
|
|
103
|
-
.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (options.
|
|
107
|
-
if (options.
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
import { hookInstall, hookUninstall } from './hook.js';
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('git-shots')
|
|
15
|
+
.description('CLI for git-shots visual regression platform')
|
|
16
|
+
.version('0.1.0');
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.command('upload')
|
|
20
|
+
.description('Upload screenshots to git-shots')
|
|
21
|
+
.option('-p, --project <slug>', 'Project slug')
|
|
22
|
+
.option('-s, --server <url>', 'Server URL')
|
|
23
|
+
.option('-d, --directory <path>', 'Screenshots directory')
|
|
24
|
+
.option('-b, --branch <name>', 'Git branch (auto-detected)')
|
|
25
|
+
.option('--sha <hash>', 'Git SHA (auto-detected)')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
if (options.project) config.project = options.project;
|
|
29
|
+
if (options.server) config.server = options.server;
|
|
30
|
+
if (options.directory) config.directory = options.directory;
|
|
31
|
+
if (!config.project) {
|
|
32
|
+
console.error('Error: project slug required. Use --project or .git-shots.json');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
await upload(config, { branch: options.branch, sha: options.sha });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command('compare')
|
|
40
|
+
.description('Compare screenshots between branches')
|
|
41
|
+
.requiredOption('--head <branch>', 'Head branch to compare')
|
|
42
|
+
.option('-p, --project <slug>', 'Project slug')
|
|
43
|
+
.option('-s, --server <url>', 'Server URL')
|
|
44
|
+
.option('--base <branch>', 'Base branch (default: main)')
|
|
45
|
+
.option('-t, --threshold <number>', 'Mismatch threshold 0-1', parseFloat)
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
const config = loadConfig();
|
|
48
|
+
if (options.project) config.project = options.project;
|
|
49
|
+
if (options.server) config.server = options.server;
|
|
50
|
+
if (!config.project) {
|
|
51
|
+
console.error('Error: project slug required. Use --project or .git-shots.json');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
await compare(config, { base: options.base, head: options.head, threshold: options.threshold });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command('status')
|
|
59
|
+
.description('Show current diff status')
|
|
60
|
+
.option('-p, --project <slug>', 'Project slug')
|
|
61
|
+
.option('-s, --server <url>', 'Server URL')
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
const config = loadConfig();
|
|
64
|
+
if (options.project) config.project = options.project;
|
|
65
|
+
if (options.server) config.server = options.server;
|
|
66
|
+
if (!config.project) {
|
|
67
|
+
console.error('Error: project slug required. Use --project or .git-shots.json');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
await status(config);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command('pull-baselines')
|
|
75
|
+
.description('Download baseline screenshots from git-shots')
|
|
76
|
+
.option('-p, --project <slug>', 'Project slug')
|
|
77
|
+
.option('-s, --server <url>', 'Server URL')
|
|
78
|
+
.option('-o, --output <path>', 'Output directory')
|
|
79
|
+
.option('-b, --branch <name>', 'Branch to pull baselines from (default: main)')
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
const config = loadConfig();
|
|
82
|
+
if (options.project) config.project = options.project;
|
|
83
|
+
if (options.server) config.server = options.server;
|
|
84
|
+
if (!config.project) {
|
|
85
|
+
console.error('Error: project slug required. Use --project or .git-shots.json');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
await pullBaselines(config, { branch: options.branch, output: options.output });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.command('review')
|
|
93
|
+
.description('Upload screenshots, create review session, and poll for verdict')
|
|
94
|
+
.option('-p, --project <slug>', 'Project slug')
|
|
95
|
+
.option('-s, --server <url>', 'Server URL')
|
|
96
|
+
.option('-d, --directory <path>', 'Screenshots directory')
|
|
97
|
+
.option('-b, --branch <name>', 'Git branch (auto-detected)')
|
|
98
|
+
.option('--sha <hash>', 'Git SHA (auto-detected)')
|
|
99
|
+
.option('--open', 'Open review URL in browser', true)
|
|
100
|
+
.option('--no-open', 'Do not open review URL in browser')
|
|
101
|
+
.option('--poll', 'Poll for verdict and exit with code', true)
|
|
102
|
+
.option('--no-poll', 'Do not poll for verdict')
|
|
103
|
+
.option('--timeout <seconds>', 'Polling timeout in seconds', parseInt, 300)
|
|
104
|
+
.action(async (options) => {
|
|
105
|
+
const config = loadConfig();
|
|
106
|
+
if (options.project) config.project = options.project;
|
|
107
|
+
if (options.server) config.server = options.server;
|
|
108
|
+
if (options.directory) config.directory = options.directory;
|
|
109
|
+
if (!config.project) {
|
|
110
|
+
console.error('Error: project slug required. Use --project or .git-shots.json');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
await review(config, {
|
|
114
|
+
branch: options.branch,
|
|
115
|
+
sha: options.sha,
|
|
116
|
+
open: options.open,
|
|
117
|
+
poll: options.poll,
|
|
118
|
+
timeout: options.timeout
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const hook = program
|
|
123
|
+
.command('hook')
|
|
124
|
+
.description('Manage git hooks for automatic visual review');
|
|
125
|
+
|
|
126
|
+
hook
|
|
127
|
+
.command('install')
|
|
128
|
+
.description('Install a pre-push hook that runs git-shots review before each push')
|
|
129
|
+
.action(async () => {
|
|
130
|
+
await hookInstall();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
hook
|
|
134
|
+
.command('uninstall')
|
|
135
|
+
.description('Remove the git-shots pre-push hook')
|
|
136
|
+
.action(async () => {
|
|
137
|
+
await hookUninstall();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
program.parse();
|
package/src/pull-baselines.ts
CHANGED
|
@@ -1,87 +1,87 @@
|
|
|
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
|
-
}
|
|
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
|
+
}
|