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/review.ts
CHANGED
|
@@ -1,199 +1,199 @@
|
|
|
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
|
-
}
|
|
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
CHANGED
|
@@ -1,42 +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
|
-
}
|
|
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
CHANGED
|
@@ -1,74 +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
|
-
}
|
|
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
CHANGED
|
@@ -1,14 +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
|
-
}
|
|
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
|
+
}
|
package/bash.exe.stackdump
DELETED
|
@@ -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
|