git-shots-cli 0.1.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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/config.ts
7
+ import { readFileSync, existsSync } from "fs";
8
+ import { resolve } from "path";
9
+ var DEFAULT_CONFIG = {
10
+ project: "",
11
+ server: "https://git-shots.rijid356.workers.dev",
12
+ directory: "docs/screenshots/current"
13
+ };
14
+ function loadConfig(cwd = process.cwd()) {
15
+ const configPath = resolve(cwd, ".git-shots.json");
16
+ if (!existsSync(configPath)) {
17
+ return DEFAULT_CONFIG;
18
+ }
19
+ try {
20
+ const raw = readFileSync(configPath, "utf-8");
21
+ const parsed = JSON.parse(raw);
22
+ return { ...DEFAULT_CONFIG, ...parsed };
23
+ } catch {
24
+ return DEFAULT_CONFIG;
25
+ }
26
+ }
27
+
28
+ // src/upload.ts
29
+ import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
30
+ import { resolve as resolve2, basename, dirname } from "path";
31
+ import { execSync } from "child_process";
32
+ import { glob } from "glob";
33
+ import chalk from "chalk";
34
+ async function upload(config, options) {
35
+ const dir = resolve2(process.cwd(), config.directory);
36
+ if (!existsSync2(dir)) {
37
+ console.error(chalk.red(`Directory not found: ${dir}`));
38
+ process.exit(1);
39
+ }
40
+ const branch = options.branch ?? execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
41
+ const sha = options.sha ?? execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
42
+ console.log(chalk.dim(`Project: ${config.project}`));
43
+ console.log(chalk.dim(`Branch: ${branch}`));
44
+ console.log(chalk.dim(`SHA: ${sha.slice(0, 7)}`));
45
+ console.log(chalk.dim(`Dir: ${dir}`));
46
+ console.log();
47
+ const files = await glob("**/*.png", { cwd: dir });
48
+ if (files.length === 0) {
49
+ console.log(chalk.yellow("No PNG files found."));
50
+ return;
51
+ }
52
+ console.log(chalk.dim(`Found ${files.length} screenshots`));
53
+ const formData = new FormData();
54
+ formData.append("project", config.project);
55
+ formData.append("branch", branch);
56
+ formData.append("gitSha", sha);
57
+ for (const file of files) {
58
+ const fullPath = resolve2(dir, file);
59
+ const buffer = readFileSync2(fullPath);
60
+ const blob = new Blob([buffer], { type: "image/png" });
61
+ const dirName = dirname(file);
62
+ const fieldName = dirName !== "." ? `${dirName}/${basename(file)}` : basename(file);
63
+ formData.append(fieldName, blob, basename(file));
64
+ }
65
+ const url = `${config.server}/api/upload`;
66
+ console.log(chalk.dim(`Uploading to ${url}...`));
67
+ try {
68
+ const res = await fetch(url, {
69
+ method: "POST",
70
+ body: formData,
71
+ headers: { Origin: config.server }
72
+ });
73
+ const data = await res.json();
74
+ if (!res.ok) {
75
+ console.error(chalk.red(`Upload failed: ${JSON.stringify(data)}`));
76
+ process.exit(1);
77
+ }
78
+ console.log(chalk.green(`Uploaded ${data.uploaded} screenshots`));
79
+ } catch (err) {
80
+ console.error(chalk.red(`Request failed: ${err}`));
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ // src/compare.ts
86
+ import chalk2 from "chalk";
87
+ async function compare(config, options) {
88
+ const url = `${config.server}/api/compare`;
89
+ const body = {
90
+ project: config.project,
91
+ base: options.base ?? "main",
92
+ head: options.head,
93
+ threshold: options.threshold ?? 0.1
94
+ };
95
+ console.log(chalk2.dim(`Comparing ${body.base} vs ${body.head} for ${config.project}...`));
96
+ try {
97
+ const res = await fetch(url, {
98
+ method: "POST",
99
+ headers: { "Content-Type": "application/json", Origin: config.server },
100
+ body: JSON.stringify(body)
101
+ });
102
+ const data = await res.json();
103
+ if (!res.ok) {
104
+ console.error(chalk2.red(`Compare failed: ${JSON.stringify(data)}`));
105
+ process.exit(1);
106
+ }
107
+ console.log();
108
+ console.log(`Compared ${chalk2.bold(data.compared)} screens`);
109
+ console.log();
110
+ if (data.diffs.length === 0) {
111
+ console.log(chalk2.green("No visual differences found!"));
112
+ return;
113
+ }
114
+ console.log(chalk2.dim("Screen".padEnd(30) + "Mismatch".padEnd(15) + "Pixels"));
115
+ console.log(chalk2.dim("-".repeat(55)));
116
+ for (const d of data.diffs) {
117
+ const pct = d.mismatchPercentage.toFixed(2) + "%";
118
+ const color = d.mismatchPercentage > 10 ? chalk2.red : d.mismatchPercentage > 1 ? chalk2.yellow : chalk2.green;
119
+ console.log(
120
+ d.screen.padEnd(30) + color(pct.padEnd(15)) + chalk2.dim(d.mismatchPixels.toLocaleString())
121
+ );
122
+ }
123
+ } catch (err) {
124
+ console.error(chalk2.red(`Request failed: ${err}`));
125
+ process.exit(1);
126
+ }
127
+ }
128
+
129
+ // src/status.ts
130
+ import chalk3 from "chalk";
131
+ async function status(config) {
132
+ const url = `${config.server}/api/diffs?project=${encodeURIComponent(config.project)}`;
133
+ try {
134
+ const res = await fetch(url);
135
+ const data = await res.json();
136
+ if (!res.ok) {
137
+ console.error(chalk3.red(`Status failed: ${JSON.stringify(data)}`));
138
+ process.exit(1);
139
+ }
140
+ if (data.length === 0) {
141
+ console.log(chalk3.dim("No diffs found for this project."));
142
+ return;
143
+ }
144
+ console.log(`${chalk3.bold(data.length)} diffs for ${config.project}`);
145
+ console.log();
146
+ console.log(chalk3.dim("ID".padEnd(8) + "Status".padEnd(12) + "Mismatch".padEnd(12) + "Date"));
147
+ console.log(chalk3.dim("-".repeat(50)));
148
+ for (const { diff } of data) {
149
+ const statusColor = diff.status === "approved" ? chalk3.green : diff.status === "rejected" ? chalk3.red : chalk3.yellow;
150
+ console.log(
151
+ String(diff.id).padEnd(8) + statusColor(diff.status.padEnd(12)) + (diff.mismatch_percentage.toFixed(2) + "%").padEnd(12) + chalk3.dim(new Date(diff.created_at * 1e3).toLocaleDateString())
152
+ );
153
+ }
154
+ } catch (err) {
155
+ console.error(chalk3.red(`Request failed: ${err}`));
156
+ process.exit(1);
157
+ }
158
+ }
159
+
160
+ // src/index.ts
161
+ var program = new Command();
162
+ program.name("git-shots").description("CLI for git-shots visual regression platform").version("0.1.0");
163
+ 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)").action(async (options) => {
164
+ const config = loadConfig();
165
+ if (options.project) config.project = options.project;
166
+ if (options.server) config.server = options.server;
167
+ if (options.directory) config.directory = options.directory;
168
+ if (!config.project) {
169
+ console.error("Error: project slug required. Use --project or .git-shots.json");
170
+ process.exit(1);
171
+ }
172
+ await upload(config, { branch: options.branch, sha: options.sha });
173
+ });
174
+ program.command("compare").description("Compare screenshots between branches").requiredOption("--head <branch>", "Head branch to compare").option("-p, --project <slug>", "Project slug").option("-s, --server <url>", "Server URL").option("--base <branch>", "Base branch (default: main)").option("-t, --threshold <number>", "Mismatch threshold 0-1", parseFloat).action(async (options) => {
175
+ const config = loadConfig();
176
+ if (options.project) config.project = options.project;
177
+ if (options.server) config.server = options.server;
178
+ if (!config.project) {
179
+ console.error("Error: project slug required. Use --project or .git-shots.json");
180
+ process.exit(1);
181
+ }
182
+ await compare(config, { base: options.base, head: options.head, threshold: options.threshold });
183
+ });
184
+ program.command("status").description("Show current diff status").option("-p, --project <slug>", "Project slug").option("-s, --server <url>", "Server URL").action(async (options) => {
185
+ const config = loadConfig();
186
+ if (options.project) config.project = options.project;
187
+ if (options.server) config.server = options.server;
188
+ if (!config.project) {
189
+ console.error("Error: project slug required. Use --project or .git-shots.json");
190
+ process.exit(1);
191
+ }
192
+ await status(config);
193
+ });
194
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "git-shots-cli",
3
+ "version": "0.1.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 ADDED
@@ -0,0 +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
+ }
package/src/config.ts ADDED
@@ -0,0 +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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
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
+
8
+ const program = new Command();
9
+
10
+ program
11
+ .name('git-shots')
12
+ .description('CLI for git-shots visual regression platform')
13
+ .version('0.1.0');
14
+
15
+ program
16
+ .command('upload')
17
+ .description('Upload screenshots to git-shots')
18
+ .option('-p, --project <slug>', 'Project slug')
19
+ .option('-s, --server <url>', 'Server URL')
20
+ .option('-d, --directory <path>', 'Screenshots directory')
21
+ .option('-b, --branch <name>', 'Git branch (auto-detected)')
22
+ .option('--sha <hash>', 'Git SHA (auto-detected)')
23
+ .action(async (options) => {
24
+ const config = loadConfig();
25
+ if (options.project) config.project = options.project;
26
+ if (options.server) config.server = options.server;
27
+ if (options.directory) config.directory = options.directory;
28
+ if (!config.project) {
29
+ console.error('Error: project slug required. Use --project or .git-shots.json');
30
+ process.exit(1);
31
+ }
32
+ await upload(config, { branch: options.branch, sha: options.sha });
33
+ });
34
+
35
+ program
36
+ .command('compare')
37
+ .description('Compare screenshots between branches')
38
+ .requiredOption('--head <branch>', 'Head branch to compare')
39
+ .option('-p, --project <slug>', 'Project slug')
40
+ .option('-s, --server <url>', 'Server URL')
41
+ .option('--base <branch>', 'Base branch (default: main)')
42
+ .option('-t, --threshold <number>', 'Mismatch threshold 0-1', parseFloat)
43
+ .action(async (options) => {
44
+ const config = loadConfig();
45
+ if (options.project) config.project = options.project;
46
+ if (options.server) config.server = options.server;
47
+ if (!config.project) {
48
+ console.error('Error: project slug required. Use --project or .git-shots.json');
49
+ process.exit(1);
50
+ }
51
+ await compare(config, { base: options.base, head: options.head, threshold: options.threshold });
52
+ });
53
+
54
+ program
55
+ .command('status')
56
+ .description('Show current diff status')
57
+ .option('-p, --project <slug>', 'Project slug')
58
+ .option('-s, --server <url>', 'Server URL')
59
+ .action(async (options) => {
60
+ const config = loadConfig();
61
+ if (options.project) config.project = options.project;
62
+ if (options.server) config.server = options.server;
63
+ if (!config.project) {
64
+ console.error('Error: project slug required. Use --project or .git-shots.json');
65
+ process.exit(1);
66
+ }
67
+ await status(config);
68
+ });
69
+
70
+ program.parse();
package/src/status.ts ADDED
@@ -0,0 +1,42 @@
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 ADDED
@@ -0,0 +1,74 @@
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 ADDED
@@ -0,0 +1,14 @@
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
+ }