frameshot-mcp 0.3.0 → 0.7.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,297 @@
1
+ import { Page } from 'playwright';
2
+
3
+ type Engine = "chromium" | "firefox" | "webkit";
4
+ type Framework = "html" | "react" | "vue" | "svelte";
5
+ type Theme = "light" | "dark";
6
+ interface Viewport {
7
+ width: number;
8
+ height: number;
9
+ }
10
+ interface RenderOptions {
11
+ viewport: Viewport;
12
+ deviceScaleFactor?: number;
13
+ engines: Engine[];
14
+ fullPage: boolean;
15
+ darkMode: boolean;
16
+ css: string;
17
+ tailwindVersion: "3" | "4";
18
+ waitFor: number;
19
+ autoFit: boolean;
20
+ }
21
+ interface ScreenshotResult {
22
+ engine: Engine;
23
+ image: string;
24
+ width: number;
25
+ height: number;
26
+ consoleErrors: string[];
27
+ }
28
+ interface DiffResult {
29
+ before: string;
30
+ after: string;
31
+ diff: string;
32
+ diffPixels: number;
33
+ totalPixels: number;
34
+ diffPercentage: number;
35
+ }
36
+ interface ReferenceDiffResult {
37
+ rendered: string;
38
+ diff: string;
39
+ diffPixels: number;
40
+ totalPixels: number;
41
+ diffPercentage: number;
42
+ passed: boolean;
43
+ }
44
+ interface A11yViolation {
45
+ id: string;
46
+ impact: string;
47
+ description: string;
48
+ helpUrl: string;
49
+ nodes: {
50
+ html: string;
51
+ target: string[];
52
+ }[];
53
+ }
54
+ interface A11yResult {
55
+ violations: A11yViolation[];
56
+ passes: number;
57
+ incomplete: number;
58
+ }
59
+ interface PerfMetrics {
60
+ renderTimeMs: number;
61
+ domElements: number;
62
+ domDepth: number;
63
+ scriptCount: number;
64
+ styleSheetCount: number;
65
+ imageCount: number;
66
+ totalDomSize: number;
67
+ }
68
+ interface AnimationFrame {
69
+ timestamp: number;
70
+ image: string;
71
+ }
72
+ interface Interaction {
73
+ action: "click" | "hover" | "focus" | "type" | "wait";
74
+ selector?: string;
75
+ value?: string;
76
+ ms?: number;
77
+ }
78
+ interface GridCell {
79
+ label: string;
80
+ code: string;
81
+ }
82
+ interface Snapshot {
83
+ image: string;
84
+ timestamp: number;
85
+ }
86
+ interface CatalogEntry {
87
+ path: string;
88
+ framework: Framework;
89
+ image: string;
90
+ width: number;
91
+ height: number;
92
+ consoleErrors: string[];
93
+ }
94
+ declare const DEVICE_PRESETS: Record<string, Viewport>;
95
+ declare const EXT_TO_FRAMEWORK: Record<string, Framework>;
96
+ interface FileRenderOptions extends RenderOptions {
97
+ props?: Record<string, unknown>;
98
+ projectRoot?: string;
99
+ }
100
+ interface ProjectConfig {
101
+ root: string;
102
+ viteConfigPath?: string;
103
+ framework: Framework;
104
+ hasVite: boolean;
105
+ }
106
+
107
+ declare class BrowserPool {
108
+ private pool;
109
+ warmup(engines: Engine[]): Promise<void>;
110
+ getPage(engine: Engine): Promise<Page>;
111
+ setViewport(engine: Engine, viewport: Viewport): Promise<void>;
112
+ shutdown(): Promise<void>;
113
+ private getSlot;
114
+ }
115
+
116
+ declare class HtmlBuilder {
117
+ build(code: string, framework: Framework, options: {
118
+ darkMode: boolean;
119
+ css: string;
120
+ tailwindVersion: "3" | "4";
121
+ }): string;
122
+ }
123
+
124
+ interface DiffComparison {
125
+ diff: string;
126
+ diffPixels: number;
127
+ totalPixels: number;
128
+ diffPercentage: number;
129
+ }
130
+ declare class ImageComparator {
131
+ diff(imageA: string, imageB: string, threshold?: number): DiffComparison;
132
+ composite(images: Buffer[], columns: number, labelHeight?: number): {
133
+ image: string;
134
+ width: number;
135
+ height: number;
136
+ };
137
+ }
138
+
139
+ declare class ProjectDetector {
140
+ detect(filePath: string): ProjectConfig;
141
+ private findProjectRoot;
142
+ private findViteConfig;
143
+ private detectFramework;
144
+ private checkViteAvailable;
145
+ }
146
+
147
+ declare class SnapshotStore {
148
+ private store;
149
+ save(key: string, image: string): void;
150
+ get(key: string): Snapshot | undefined;
151
+ list(): {
152
+ key: string;
153
+ timestamp: number;
154
+ }[];
155
+ }
156
+
157
+ declare class ViteBundler {
158
+ private servers;
159
+ private detector;
160
+ getUrl(filePath: string, options?: {
161
+ props?: Record<string, unknown>;
162
+ framework?: Framework;
163
+ projectRoot?: string;
164
+ }): Promise<{
165
+ url: string;
166
+ project: ProjectConfig;
167
+ }>;
168
+ shutdown(): Promise<void>;
169
+ private ensureServer;
170
+ private importVite;
171
+ private hasPackage;
172
+ private generateEntry;
173
+ private findGlobalCss;
174
+ private getOptimizeDepsInclude;
175
+ }
176
+
177
+ declare class AuditUseCase {
178
+ private readonly pool;
179
+ private readonly htmlBuilder;
180
+ constructor(pool: BrowserPool, htmlBuilder: HtmlBuilder);
181
+ auditA11y(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<A11yResult>;
182
+ perfAudit(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<PerfMetrics>;
183
+ }
184
+
185
+ declare class RenderUseCase {
186
+ private readonly pool;
187
+ private readonly htmlBuilder;
188
+ private readonly imageComparator;
189
+ private readonly viteBundler?;
190
+ constructor(pool: BrowserPool, htmlBuilder: HtmlBuilder, imageComparator: ImageComparator, viteBundler?: ViteBundler | undefined);
191
+ render(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<ScreenshotResult[]>;
192
+ renderFile(filePath: string, options?: Partial<FileRenderOptions>): Promise<{
193
+ results: ScreenshotResult[];
194
+ mode: "vite" | "cdn";
195
+ }>;
196
+ renderInteraction(code: string, framework: Framework, interactions: Interaction[], options?: Partial<RenderOptions>): Promise<{
197
+ image: string;
198
+ width: number;
199
+ height: number;
200
+ }>;
201
+ captureAnimation(code: string, framework: Framework, options?: Partial<RenderOptions> & {
202
+ frames?: number;
203
+ duration?: number;
204
+ }): Promise<AnimationFrame[]>;
205
+ renderGrid(cells: GridCell[], framework: Framework, options?: Partial<RenderOptions> & {
206
+ columns?: number;
207
+ }): Promise<{
208
+ image: string;
209
+ width: number;
210
+ height: number;
211
+ cells: number;
212
+ }>;
213
+ renderMatrix(code: string, framework: Framework, viewports: {
214
+ label: string;
215
+ width: number;
216
+ height: number;
217
+ }[], themes: Theme[], options?: Partial<Omit<RenderOptions, "viewport" | "darkMode">>): Promise<{
218
+ viewport: string;
219
+ theme: Theme;
220
+ image: string;
221
+ width: number;
222
+ height: number;
223
+ consoleErrors: string[];
224
+ }[]>;
225
+ private renderHtml;
226
+ private renderUrl;
227
+ private navigateAndWait;
228
+ private resolveAutoFitViewport;
229
+ private attachConsoleCapture;
230
+ private takeScreenshot;
231
+ private resolveOptions;
232
+ }
233
+
234
+ declare class CatalogUseCase {
235
+ private readonly renderUseCase;
236
+ constructor(renderUseCase: RenderUseCase);
237
+ renderCatalog(directory: string, options?: Partial<RenderOptions> & {
238
+ recursive?: boolean;
239
+ }): Promise<CatalogEntry[]>;
240
+ private scanDirectory;
241
+ private detectFramework;
242
+ }
243
+
244
+ interface Renderer {
245
+ render(code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<ScreenshotResult[]>;
246
+ }
247
+ interface Comparator {
248
+ diff(a: string, b: string, threshold?: number): DiffComparison;
249
+ }
250
+ declare class DiffUseCase {
251
+ private readonly renderUseCase;
252
+ private readonly imageComparator;
253
+ constructor(renderUseCase: Renderer, imageComparator: Comparator);
254
+ diffComponent(before: string, after: string, framework: Framework, options?: Partial<RenderOptions>): Promise<DiffResult>;
255
+ diffFromReference(code: string, framework: Framework, referenceImage: string, options?: Partial<RenderOptions> & {
256
+ threshold?: number;
257
+ }): Promise<ReferenceDiffResult>;
258
+ }
259
+
260
+ interface ScreenshotUrlOptions {
261
+ viewport?: {
262
+ width: number;
263
+ height: number;
264
+ };
265
+ engines?: Engine[];
266
+ fullPage?: boolean;
267
+ waitFor?: number;
268
+ waitForSelector?: string;
269
+ waitForNetworkIdle?: boolean;
270
+ }
271
+ declare class ScreenshotUseCase {
272
+ private readonly pool;
273
+ constructor(pool: BrowserPool);
274
+ screenshotUrl(url: string, options?: ScreenshotUrlOptions): Promise<ScreenshotResult[]>;
275
+ screenshotUrlWithRetry(url: string, options?: ScreenshotUrlOptions & {
276
+ retryCount?: number;
277
+ }): Promise<ScreenshotResult[]>;
278
+ }
279
+
280
+ declare class SnapshotUseCase {
281
+ private readonly store;
282
+ private readonly renderUseCase;
283
+ private readonly diffUseCase;
284
+ constructor(store: SnapshotStore, renderUseCase: RenderUseCase, diffUseCase: DiffUseCase);
285
+ save(key: string, code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<{
286
+ image: string;
287
+ width: number;
288
+ height: number;
289
+ }>;
290
+ check(key: string, code: string, framework: Framework, options?: Partial<RenderOptions>): Promise<ReferenceDiffResult | null>;
291
+ list(): {
292
+ key: string;
293
+ timestamp: number;
294
+ }[];
295
+ }
296
+
297
+ export { type A11yResult, type A11yViolation, type AnimationFrame, AuditUseCase, BrowserPool, type CatalogEntry, CatalogUseCase, DEVICE_PRESETS, type DiffResult, DiffUseCase, EXT_TO_FRAMEWORK, type Engine, type FileRenderOptions, type Framework, type GridCell, HtmlBuilder, ImageComparator, type Interaction, type PerfMetrics, type ProjectConfig, ProjectDetector, type ReferenceDiffResult, type RenderOptions, RenderUseCase, type ScreenshotResult, ScreenshotUseCase, type Snapshot, SnapshotStore, SnapshotUseCase, type Theme, type Viewport, ViteBundler };
@@ -0,0 +1,34 @@
1
+ import {
2
+ AuditUseCase,
3
+ ScreenshotUseCase,
4
+ SnapshotStore,
5
+ SnapshotUseCase
6
+ } from "./chunk-FTYTZW6D.js";
7
+ import {
8
+ BrowserPool,
9
+ CatalogUseCase,
10
+ DEVICE_PRESETS,
11
+ DiffUseCase,
12
+ EXT_TO_FRAMEWORK,
13
+ HtmlBuilder,
14
+ ImageComparator,
15
+ ProjectDetector,
16
+ RenderUseCase,
17
+ ViteBundler
18
+ } from "./chunk-Q7A3DLED.js";
19
+ export {
20
+ AuditUseCase,
21
+ BrowserPool,
22
+ CatalogUseCase,
23
+ DEVICE_PRESETS,
24
+ DiffUseCase,
25
+ EXT_TO_FRAMEWORK,
26
+ HtmlBuilder,
27
+ ImageComparator,
28
+ ProjectDetector,
29
+ RenderUseCase,
30
+ ScreenshotUseCase,
31
+ SnapshotStore,
32
+ SnapshotUseCase,
33
+ ViteBundler
34
+ };
package/package.json CHANGED
@@ -1,25 +1,39 @@
1
1
  {
2
2
  "name": "frameshot-mcp",
3
- "version": "0.3.0",
4
- "description": "Instant cross-browser component preview for AI agents. One MCP call, one screenshot no dev server, no Storybook, no ceremony.",
3
+ "version": "0.7.0",
4
+ "description": "Zero-config visual testing for AI agents. Render project components with full Vite dependency resolution no stories, no config.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "types": "./dist/index.d.ts"
11
+ },
12
+ "./renderer": {
13
+ "import": "./dist/renderer.js",
14
+ "types": "./dist/renderer.d.ts"
15
+ }
16
+ },
7
17
  "bin": {
8
- "frameshot-mcp": "dist/index.js"
18
+ "frameshot-mcp": "dist/index.js",
19
+ "frameshot": "dist/cli.js"
9
20
  },
10
21
  "files": [
11
22
  "dist",
23
+ "scripts",
24
+ "action.yml",
12
25
  "README.md",
13
26
  "LICENSE"
14
27
  ],
15
28
  "scripts": {
16
- "build": "tsup src/index.ts --format esm --dts",
17
- "dev": "tsup src/index.ts --format esm --watch",
29
+ "build": "tsup src/index.ts src/renderer.ts src/cli.ts --format esm --dts --external vite",
30
+ "dev": "tsup src/index.ts src/renderer.ts --format esm --watch",
18
31
  "start": "node dist/index.js",
19
32
  "typecheck": "tsc --noEmit",
20
33
  "lint": "biome check src/",
21
34
  "lint:fix": "biome check --write src/",
22
35
  "format": "biome format --write src/",
36
+ "test": "vitest run",
23
37
  "prepublishOnly": "npm run build"
24
38
  },
25
39
  "keywords": [
@@ -57,13 +71,24 @@
57
71
  "playwright": "^1.52.0",
58
72
  "pngjs": "^7.0.0"
59
73
  },
74
+ "peerDependencies": {
75
+ "vite": ">=5.0.0"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "vite": {
79
+ "optional": true
80
+ }
81
+ },
60
82
  "devDependencies": {
61
83
  "@biomejs/biome": "^2.5.0",
62
- "@types/node": "^22.0.0",
84
+ "@types/node": "^26.0.0",
63
85
  "@types/pngjs": "^6.0.5",
64
86
  "tsup": "^8.0.0",
65
- "typescript": "^5.7.0"
87
+ "typescript": "^5.7.0",
88
+ "vite": "^6.0.0",
89
+ "vitest": "^4.1.9"
66
90
  },
91
+ "mcpName": "io.github.kamegoro/frameshot-mcp",
67
92
  "engines": {
68
93
  "node": ">=20.0.0"
69
94
  }
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { mkdirSync, writeFileSync } from "node:fs";
5
+ import { basename, extname, join, relative } from "node:path";
6
+
7
+ const {
8
+ INPUT_PATHS = "",
9
+ INPUT_WIDTH = "",
10
+ INPUT_HEIGHT = "",
11
+ INPUT_BASE_REF = "",
12
+ GITHUB_WORKSPACE = process.cwd(),
13
+ GITHUB_OUTPUT = "/dev/null",
14
+ } = process.env;
15
+
16
+ const outputDir = join(GITHUB_WORKSPACE, ".frameshot-screenshots");
17
+ mkdirSync(outputDir, { recursive: true });
18
+
19
+ const patterns = INPUT_PATHS.split(/[,\n]/)
20
+ .map((p) => p.trim())
21
+ .filter(Boolean);
22
+
23
+ const COMPONENT_EXTS = new Set([
24
+ ".jsx",
25
+ ".tsx",
26
+ ".vue",
27
+ ".svelte",
28
+ ]);
29
+
30
+ function findFiles(workspace, patterns) {
31
+ const allFiles = [];
32
+ for (const pattern of patterns) {
33
+ try {
34
+ const result = execSync(
35
+ `find ${workspace} -path '${pattern}' -type f 2>/dev/null`,
36
+ { encoding: "utf-8" },
37
+ );
38
+ allFiles.push(...result.split("\n").filter(Boolean));
39
+ } catch {
40
+ // Pattern didn't match
41
+ }
42
+ }
43
+ return allFiles.filter((f) => COMPONENT_EXTS.has(extname(f).toLowerCase()));
44
+ }
45
+
46
+ function getChangedFiles(baseRef) {
47
+ if (!baseRef) return null;
48
+ try {
49
+ const result = execSync(`git diff --name-only ${baseRef}...HEAD`, {
50
+ encoding: "utf-8",
51
+ cwd: GITHUB_WORKSPACE,
52
+ });
53
+ return new Set(result.split("\n").filter(Boolean));
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function checkoutFileAtRef(filePath, ref) {
60
+ const relativePath = relative(GITHUB_WORKSPACE, filePath);
61
+ try {
62
+ execSync(`git checkout ${ref} -- ${relativePath}`, {
63
+ cwd: GITHUB_WORKSPACE,
64
+ stdio: "pipe",
65
+ });
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function restoreFile(filePath) {
73
+ const relativePath = relative(GITHUB_WORKSPACE, filePath);
74
+ execSync(`git checkout HEAD -- ${relativePath}`, {
75
+ cwd: GITHUB_WORKSPACE,
76
+ stdio: "pipe",
77
+ });
78
+ }
79
+
80
+ async function main() {
81
+ const { BrowserPool, HtmlBuilder, ImageComparator, RenderUseCase, ViteBundler } =
82
+ await import("frameshot-mcp/renderer");
83
+
84
+ const browserPool = new BrowserPool();
85
+ const htmlBuilder = new HtmlBuilder();
86
+ const imageComparator = new ImageComparator();
87
+ const viteBundler = new ViteBundler();
88
+ const renderUseCase = new RenderUseCase(
89
+ browserPool,
90
+ htmlBuilder,
91
+ imageComparator,
92
+ viteBundler,
93
+ );
94
+
95
+ await browserPool.warmup(["chromium"]);
96
+
97
+ const allFiles = findFiles(GITHUB_WORKSPACE, patterns);
98
+
99
+ if (allFiles.length === 0) {
100
+ console.log("No component files found matching the specified patterns.");
101
+ await viteBundler.shutdown();
102
+ await browserPool.shutdown();
103
+ process.exit(0);
104
+ }
105
+
106
+ const changedFiles = getChangedFiles(INPUT_BASE_REF);
107
+ const filesToRender = changedFiles
108
+ ? allFiles.filter((f) => {
109
+ const rel = relative(GITHUB_WORKSPACE, f);
110
+ return changedFiles.has(rel);
111
+ })
112
+ : allFiles;
113
+
114
+ if (filesToRender.length === 0) {
115
+ console.log("No changed component files to render.");
116
+ await viteBundler.shutdown();
117
+ await browserPool.shutdown();
118
+ process.exit(0);
119
+ }
120
+
121
+ console.log(`Found ${filesToRender.length} component file(s) to render...`);
122
+
123
+ const autoFit = !INPUT_WIDTH || !INPUT_HEIGHT;
124
+ const renderOpts = {
125
+ viewport: {
126
+ width: INPUT_WIDTH ? parseInt(INPUT_WIDTH) : 1280,
127
+ height: INPUT_HEIGHT ? parseInt(INPUT_HEIGHT) : 800,
128
+ },
129
+ autoFit,
130
+ fullPage: true,
131
+ engines: ["chromium"],
132
+ };
133
+
134
+ const results = [];
135
+
136
+ for (const filePath of filesToRender) {
137
+ const name = basename(filePath, extname(filePath));
138
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "_");
139
+
140
+ try {
141
+ // Render current (after) version
142
+ console.log(` Rendering (after): ${filePath}`);
143
+ const { results: afterResults, mode } = await renderUseCase.renderFile(
144
+ filePath,
145
+ renderOpts,
146
+ );
147
+ const afterResult = afterResults[0];
148
+
149
+ const afterPath = join(outputDir, `${safeName}_after.png`);
150
+ writeFileSync(afterPath, Buffer.from(afterResult.image, "base64"));
151
+
152
+ const entry = {
153
+ name: safeName,
154
+ after: afterResult.image,
155
+ before: null,
156
+ diff: null,
157
+ diffPercentage: 0,
158
+ mode,
159
+ };
160
+
161
+ // Render base (before) version if base_ref is provided
162
+ if (INPUT_BASE_REF) {
163
+ const checkedOut = checkoutFileAtRef(filePath, INPUT_BASE_REF);
164
+ if (checkedOut) {
165
+ try {
166
+ console.log(` Rendering (before): ${filePath}`);
167
+ const { results: beforeResults } = await renderUseCase.renderFile(
168
+ filePath,
169
+ renderOpts,
170
+ );
171
+ const beforeResult = beforeResults[0];
172
+ entry.before = beforeResult.image;
173
+
174
+ const beforePath = join(outputDir, `${safeName}_before.png`);
175
+ writeFileSync(
176
+ beforePath,
177
+ Buffer.from(beforeResult.image, "base64"),
178
+ );
179
+
180
+ const diffResult = imageComparator.diff(
181
+ beforeResult.image,
182
+ afterResult.image,
183
+ );
184
+ entry.diff = diffResult.diff;
185
+ entry.diffPercentage = diffResult.diffPercentage;
186
+
187
+ if (diffResult.diffPercentage > 0) {
188
+ const diffPath = join(outputDir, `${safeName}_diff.png`);
189
+ writeFileSync(diffPath, Buffer.from(diffResult.diff, "base64"));
190
+ }
191
+ } finally {
192
+ restoreFile(filePath);
193
+ }
194
+ }
195
+ }
196
+
197
+ results.push(entry);
198
+ console.log(
199
+ ` ✓ ${safeName} (${afterResult.width}x${afterResult.height}, ${mode})${entry.diffPercentage > 0 ? ` — ${entry.diffPercentage.toFixed(1)}% changed` : ""}`,
200
+ );
201
+ } catch (error) {
202
+ console.error(` ✗ Failed: ${filePath} — ${error.message}`);
203
+ }
204
+ }
205
+
206
+ await viteBundler.shutdown();
207
+ await browserPool.shutdown();
208
+
209
+ const manifest = JSON.stringify(
210
+ results.map((r) => ({
211
+ name: r.name,
212
+ hasBefore: r.before !== null,
213
+ diffPercentage: r.diffPercentage,
214
+ mode: r.mode,
215
+ })),
216
+ );
217
+ writeFileSync(join(outputDir, "manifest.json"), manifest);
218
+
219
+ const hasChanges = results.some((r) => r.diffPercentage > 0 || !r.before);
220
+ try {
221
+ const outputContent = `has_changes=${hasChanges}\ncount=${results.length}\n`;
222
+ writeFileSync(GITHUB_OUTPUT, outputContent, { flag: "a" });
223
+ } catch {
224
+ // Not in GitHub Actions
225
+ }
226
+
227
+ console.log(`\nDone. ${results.length} screenshot(s) saved to ${outputDir}`);
228
+ }
229
+
230
+ main().catch((e) => {
231
+ console.error(e);
232
+ process.exit(1);
233
+ });
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # Setup GitHub labels for the frameshot repository.
3
+ # Usage: ./scripts/setup-labels.sh [owner/repo]
4
+ # Requires: gh CLI authenticated
5
+
6
+ set -euo pipefail
7
+
8
+ REPO="${1:-kamegoro/frameshot}"
9
+
10
+ # Delete default labels that add noise
11
+ for label in "invalid" "wontfix" "help wanted" "question"; do
12
+ gh label delete "$label" --repo "$REPO" --yes 2>/dev/null || true
13
+ done
14
+
15
+ # --- Status labels (S-) ---
16
+ gh label create "S-needs triage" --color "D93F0B" --description "Awaiting maintainer review" --repo "$REPO" --force
17
+ gh label create "S-accepted" --color "0E8A16" --description "Confirmed and ready for work" --repo "$REPO" --force
18
+ gh label create "S-in progress" --color "1D76DB" --description "Currently being worked on" --repo "$REPO" --force
19
+ gh label create "S-blocked" --color "B60205" --description "Waiting on external dependency" --repo "$REPO" --force
20
+
21
+ # --- Priority labels (P-) ---
22
+ gh label create "P0-critical" --color "B60205" --description "Production broken, fix immediately" --repo "$REPO" --force
23
+ gh label create "P1-high" --color "D93F0B" --description "Important, fix this week" --repo "$REPO" --force
24
+ gh label create "P2-medium" --color "FBCA04" --description "Normal priority" --repo "$REPO" --force
25
+ gh label create "P3-low" --color "0075CA" --description "Nice to have, no urgency" --repo "$REPO" --force
26
+
27
+ # --- Type labels ---
28
+ gh label create "bug" --color "D73A4A" --description "Something isn't working" --repo "$REPO" --force
29
+ gh label create "enhancement" --color "A2EEEF" --description "New feature or improvement" --repo "$REPO" --force
30
+ gh label create "documentation" --color "0075CA" --description "Documentation improvements" --repo "$REPO" --force
31
+ gh label create "performance" --color "F9D0C4" --description "Performance improvement" --repo "$REPO" --force
32
+ gh label create "breaking change" --color "B60205" --description "Introduces a breaking change" --repo "$REPO" --force
33
+
34
+ # --- Area labels (A-) ---
35
+ gh label create "A-rendering" --color "C5DEF5" --description "Component rendering pipeline" --repo "$REPO" --force
36
+ gh label create "A-screenshot" --color "C5DEF5" --description "URL screenshot capture" --repo "$REPO" --force
37
+ gh label create "A-diff" --color "C5DEF5" --description "Visual regression diffing" --repo "$REPO" --force
38
+ gh label create "A-audit" --color "C5DEF5" --description "Accessibility & performance audits" --repo "$REPO" --force
39
+ gh label create "A-mcp-protocol" --color "C5DEF5" --description "MCP transport & tool definitions" --repo "$REPO" --force
40
+ gh label create "A-github-action" --color "C5DEF5" --description "GitHub Action integration" --repo "$REPO" --force
41
+ gh label create "A-config" --color "C5DEF5" --description "Configuration & setup" --repo "$REPO" --force
42
+
43
+ # --- Community labels ---
44
+ gh label create "good first issue" --color "7057FF" --description "Good for newcomers" --repo "$REPO" --force
45
+ gh label create "help wanted" --color "008672" --description "Extra attention is needed" --repo "$REPO" --force
46
+ gh label create "duplicate" --color "CFD3D7" --description "This issue already exists" --repo "$REPO" --force
47
+
48
+ echo "Labels setup complete for $REPO"