@vscode/component-explorer-cli 0.1.1-5 → 0.1.1-7
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/README.md +177 -0
- package/dist/commands/acceptCommand.d.ts +2 -1
- package/dist/commands/acceptCommand.d.ts.map +1 -1
- package/dist/commands/acceptCommand.js +12 -6
- package/dist/commands/acceptCommand.js.map +1 -1
- package/dist/commands/compareCommand.d.ts +5 -1
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +43 -71
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/mcpCommand.d.ts +1 -1
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +9 -3
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/screenshotCommand.d.ts +7 -2
- package/dist/commands/screenshotCommand.d.ts.map +1 -1
- package/dist/commands/screenshotCommand.js +61 -14
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +1 -1
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +18 -11
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/watchCommand.d.ts +1 -2
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +18 -17
- package/dist/commands/watchCommand.js.map +1 -1
- package/dist/comparison.d.ts +60 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +250 -0
- package/dist/comparison.js.map +1 -0
- package/dist/componentExplorer.d.ts +33 -2
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +21 -2
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonService.d.ts +34 -1
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +116 -17
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/lifecycle.js +1 -1
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/httpServer.js +14 -8
- package/dist/httpServer.js.map +1 -1
- package/dist/mcp/McpServer.d.ts +1 -0
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +57 -1
- package/dist/mcp/McpServer.js.map +1 -1
- package/dist/resolveProject.d.ts +21 -0
- package/dist/resolveProject.d.ts.map +1 -0
- package/dist/resolveProject.js +39 -0
- package/dist/resolveProject.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +29 -0
- package/dist/utils.js.map +1 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# @vscode/component-explorer-cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for capturing, comparing, and managing component fixture screenshots.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vscode/component-explorer-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The binary is available as `component-explorer`.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
### `screenshot`
|
|
16
|
+
|
|
17
|
+
Capture screenshots of all fixtures using a headless browser.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
component-explorer screenshot [options]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Option | Description |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `-p, --project <path>` | Project: a directory, vite config file, or component-explorer.json (default: cwd) |
|
|
26
|
+
| `--filter <pattern>` | Filter fixtures by glob pattern |
|
|
27
|
+
| `--accept` | Write to baseline instead of current (mutually exclusive with `--target`) |
|
|
28
|
+
| `--target <dir>` | Screenshot output directory (default: `.screenshots/current`, or `.screenshots/baseline` with `--accept`) |
|
|
29
|
+
| `--compare` | Compare screenshots after capturing |
|
|
30
|
+
| `--compare-target <dir>` | Directory to compare against (default: `.screenshots/baseline`, or `.screenshots/current` with `--accept`) |
|
|
31
|
+
| `--report <dir>` | Output report folder with report.json, report.md, and changed screenshots (requires `--compare`) |
|
|
32
|
+
| `-v, --verbose` | Increase log verbosity (`-v` debug, `-vv` trace) |
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Capture all fixtures
|
|
36
|
+
component-explorer screenshot
|
|
37
|
+
|
|
38
|
+
# Capture and accept as baseline
|
|
39
|
+
component-explorer screenshot --accept
|
|
40
|
+
|
|
41
|
+
# Capture, then compare against baseline
|
|
42
|
+
component-explorer screenshot --compare
|
|
43
|
+
|
|
44
|
+
# Filter by glob
|
|
45
|
+
component-explorer screenshot --filter "Button/*"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `screenshot:compare`
|
|
49
|
+
|
|
50
|
+
Compare two screenshot directories (file-level hash comparison).
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
component-explorer screenshot:compare [options]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Option | Description |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `-p, --project <path>` | Project directory (default: cwd) |
|
|
59
|
+
| `--baseline <dir>` | Baseline screenshots directory (default: `.screenshots/baseline`) |
|
|
60
|
+
| `--current <dir>` | Current screenshots directory (default: `.screenshots/current`) |
|
|
61
|
+
| `--filter <pattern>` | Filter fixtures by glob pattern |
|
|
62
|
+
| `--report <dir>` | Output report folder (contains report.json, report.md, and changed screenshots) |
|
|
63
|
+
| `-v, --verbose` | Increase log verbosity |
|
|
64
|
+
|
|
65
|
+
Exits with code 1 if differences are found, 0 otherwise.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Compare default directories
|
|
69
|
+
component-explorer screenshot:compare
|
|
70
|
+
|
|
71
|
+
# Compare with custom paths
|
|
72
|
+
component-explorer screenshot:compare --baseline ./golden --current ./actual
|
|
73
|
+
|
|
74
|
+
# Generate report folder
|
|
75
|
+
component-explorer screenshot:compare --report ./report
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### `screenshot:accept`
|
|
79
|
+
|
|
80
|
+
Promote current screenshots to baseline by copying them from `.screenshots/current` to `.screenshots/baseline`.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
component-explorer screenshot:accept [options]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| Option | Description |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `-p, --project <path>` | Project directory (default: cwd) |
|
|
89
|
+
| `--filter <pattern>` | Filter fixtures by glob pattern |
|
|
90
|
+
| `--screenshot-dir <dir>` | Screenshots directory (default: `.screenshots`) |
|
|
91
|
+
| `-v, --verbose` | Increase log verbosity |
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Accept all current screenshots as baseline
|
|
95
|
+
component-explorer screenshot:accept
|
|
96
|
+
|
|
97
|
+
# Accept specific fixtures
|
|
98
|
+
component-explorer screenshot:accept --filter "Button/*"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `watch`
|
|
102
|
+
|
|
103
|
+
Watch for source changes and automatically re-capture/compare screenshots. Supports both simple mode (single project) and config mode (multiple sessions with git worktrees).
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
component-explorer watch [options]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
| Option | Description |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `-p, --project <path>` | Project: a directory, vite config file, or component-explorer.json (default: cwd) |
|
|
112
|
+
| `-v, --verbose` | Increase log verbosity |
|
|
113
|
+
|
|
114
|
+
In simple mode, starts a Vite dev server and re-captures screenshots on every HMR update. In config mode (when given a `component-explorer.json`), manages multiple sessions including git worktree-based baselines.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Watch all fixtures
|
|
118
|
+
component-explorer watch
|
|
119
|
+
|
|
120
|
+
# Watch with config
|
|
121
|
+
component-explorer watch -p component-explorer.json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `serve`
|
|
125
|
+
|
|
126
|
+
Start or attach to a Component Explorer daemon process. The daemon manages Vite servers and browser sessions in the background.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
component-explorer serve -p <config> [options]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Option | Description |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `-p, --project, -c <path>` | **(required)** Path to a component-explorer.json config file |
|
|
135
|
+
| `--background` | Spawn as a detached background process |
|
|
136
|
+
| `--attach` | Attach to a running daemon and stream events |
|
|
137
|
+
| `--kill` | Shut down a running daemon |
|
|
138
|
+
| `-v, --verbose` | Increase log verbosity |
|
|
139
|
+
|
|
140
|
+
Without flags, starts a foreground daemon. Combine `--background` and `--attach` to ensure a daemon is running and then stream its events.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Start in foreground
|
|
144
|
+
component-explorer serve -p config.json
|
|
145
|
+
|
|
146
|
+
# Start in background
|
|
147
|
+
component-explorer serve -p config.json --background
|
|
148
|
+
|
|
149
|
+
# Attach to running daemon
|
|
150
|
+
component-explorer serve -p config.json --attach
|
|
151
|
+
|
|
152
|
+
# Ensure daemon + attach
|
|
153
|
+
component-explorer serve -p config.json --background --attach
|
|
154
|
+
|
|
155
|
+
# Kill running daemon
|
|
156
|
+
component-explorer serve -p config.json --kill
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `mcp`
|
|
160
|
+
|
|
161
|
+
Start a [Model Context Protocol](https://modelcontextprotocol.io/) server over stdio. Auto-starts a daemon if one is not already running.
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
component-explorer mcp -p <config>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Option | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `-p, --project, -c <path>` | **(required)** Path to a component-explorer.json config file |
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
component-explorer mcp --project config.json
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Global Options
|
|
176
|
+
|
|
177
|
+
All commands support `-v` / `--verbose` for increased log output. Use `-vv` for trace-level logging.
|
|
@@ -4,7 +4,8 @@ export declare class AcceptCommand extends Command {
|
|
|
4
4
|
static usage: import("clipanion").Usage;
|
|
5
5
|
readonly verbose: number;
|
|
6
6
|
readonly filter: string | undefined;
|
|
7
|
-
readonly
|
|
7
|
+
readonly project: string;
|
|
8
|
+
readonly screenshotDir: string;
|
|
8
9
|
execute(): Promise<void>;
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=acceptCommand.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acceptCommand.d.ts","sourceRoot":"","sources":["../../src/commands/acceptCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"acceptCommand.d.ts","sourceRoot":"","sources":["../../src/commands/acceptCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAM5C,qBAAa,aAAc,SAAQ,OAAO;IACzC,OAAgB,KAAK,aAA2B;IAEhD,OAAgB,KAAK,4BAMlB;IAEH,QAAQ,CAAC,OAAO,SAAsG;IACtH,QAAQ,CAAC,MAAM,qBAAkG;IACjH,QAAQ,CAAC,OAAO,SAAuI;IACvJ,QAAQ,CAAC,aAAa,SAA+F;IAE/G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CA8B9B"}
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { Command, Option } from 'clipanion';
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import { ConsoleLogger, verbosityToLogLevel } from '../logger.js';
|
|
3
4
|
import { FileSystemStorage } from '../storage.js';
|
|
5
|
+
import { resolveProject } from '../resolveProject.js';
|
|
4
6
|
|
|
5
7
|
class AcceptCommand extends Command {
|
|
6
|
-
static paths = [['accept']];
|
|
8
|
+
static paths = [['screenshot:accept']];
|
|
7
9
|
static usage = Command.Usage({
|
|
8
|
-
description: 'Promote current screenshots
|
|
10
|
+
description: 'Promote current screenshots to baseline (file-level only)',
|
|
9
11
|
examples: [
|
|
10
|
-
['Accept all', '$0 accept'],
|
|
11
|
-
['Accept specific fixtures', '$0 accept --filter "Button/*"'],
|
|
12
|
+
['Accept all', '$0 screenshot:accept'],
|
|
13
|
+
['Accept specific fixtures', '$0 screenshot:accept --filter "Button/*"'],
|
|
12
14
|
],
|
|
13
15
|
});
|
|
14
16
|
verbose = Option.Counter('-v,--verbose', 0, { description: 'Increase log verbosity (-v debug, -vv trace)' });
|
|
15
17
|
filter = Option.String('--filter', { required: false, description: 'Filter fixtures by glob pattern' });
|
|
16
|
-
|
|
18
|
+
project = Option.String('-p,--project', process.cwd(), { description: 'Project: a directory, vite config file, or component-explorer.json' });
|
|
19
|
+
screenshotDir = Option.String('--screenshot-dir', '.screenshots', { description: 'Screenshots directory' });
|
|
17
20
|
async execute() {
|
|
21
|
+
const resolved = await resolveProject(this.project);
|
|
22
|
+
const projectDir = resolved.projectDir;
|
|
18
23
|
new ConsoleLogger('accept', this.context.stdout, verbosityToLogLevel(this.verbose));
|
|
19
|
-
const
|
|
24
|
+
const screenshotPath = path.isAbsolute(this.screenshotDir) ? this.screenshotDir : path.join(projectDir, this.screenshotDir);
|
|
25
|
+
const storage = new FileSystemStorage(screenshotPath);
|
|
20
26
|
let currentFiles = await storage.list('current');
|
|
21
27
|
if (this.filter) {
|
|
22
28
|
const regex = new RegExp('^current/' + this.filter.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acceptCommand.js","sources":["../../src/commands/acceptCommand.ts"],"sourcesContent":[null],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"acceptCommand.js","sources":["../../src/commands/acceptCommand.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;AAMM,MAAO,aAAc,SAAQ,OAAO,CAAA;IACzC,OAAgB,KAAK,GAAG,CAAC,CAAC,mBAAmB,CAAC,CAAC;AAE/C,IAAA,OAAgB,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;AACrC,QAAA,WAAW,EAAE,2DAA2D;AACxE,QAAA,QAAQ,EAAE;YACT,CAAC,YAAY,EAAE,sBAAsB,CAAC;YACtC,CAAC,0BAA0B,EAAE,0CAA0C,CAAC;AACxE,SAAA;AACD,KAAA,CAAC;AAEO,IAAA,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,8CAA8C,EAAE,CAAC;AAC5G,IAAA,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,iCAAiC,EAAE,CAAC;AACvG,IAAA,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,EAAE,WAAW,EAAE,oEAAoE,EAAE,CAAC;AAC7I,IAAA,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,kBAAkB,EAAE,cAAc,EAAE,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC;AAEpH,IAAA,MAAM,OAAO,GAAA;QACZ,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC;AACnD,QAAA,MAAM,UAAU,GAAG,QAAQ,CAAC,UAAU;QACtB,IAAI,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC;AAClG,QAAA,MAAM,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC;AAC3H,QAAA,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,cAAc,CAAC;QAErD,IAAI,YAAY,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC;AAEhD,QAAA,IAAI,IAAI,CAAC,MAAM,EAAE;YAChB,MAAM,KAAK,GAAG,IAAI,MAAM,CACvB,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,GAAG,CACxE;AACD,YAAA,YAAY,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACvD;AAEA,QAAA,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE;YAC9B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qCAAqC,CAAC;YAChE;QACD;AAEA,QAAA,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE;YAChC,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;YACjD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;YACrC,MAAM,OAAO,CAAC,KAAK,CAAC,CAAA,SAAA,EAAY,YAAY,CAAA,CAAE,EAAE,IAAI,CAAC;YACrD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA,IAAA,EAAO,YAAY,CAAA,EAAA,CAAI,CAAC;QACnD;AAEA,QAAA,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA,WAAA,EAAc,YAAY,CAAC,MAAM,CAAA,6BAAA,CAA+B,CAAC;IAC5F;;;;;"}
|
|
@@ -3,7 +3,11 @@ export declare class CompareCommand extends Command {
|
|
|
3
3
|
static paths: string[][];
|
|
4
4
|
static usage: import("clipanion").Usage;
|
|
5
5
|
readonly verbose: number;
|
|
6
|
-
readonly
|
|
6
|
+
readonly filter: string | undefined;
|
|
7
|
+
readonly project: string;
|
|
8
|
+
readonly baseline: string;
|
|
9
|
+
readonly current: string;
|
|
10
|
+
readonly report: string | undefined;
|
|
7
11
|
execute(): Promise<number>;
|
|
8
12
|
}
|
|
9
13
|
//# sourceMappingURL=compareCommand.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compareCommand.d.ts","sourceRoot":"","sources":["../../src/commands/compareCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"compareCommand.d.ts","sourceRoot":"","sources":["../../src/commands/compareCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAQ5C,qBAAa,cAAe,SAAQ,OAAO;IAC1C,OAAgB,KAAK,aAA4B;IAEjD,OAAgB,KAAK,4BAOlB;IAEH,QAAQ,CAAC,OAAO,SAAsG;IACtH,QAAQ,CAAC,MAAM,qBAAkG;IACjH,QAAQ,CAAC,OAAO,SAAuI;IACvJ,QAAQ,CAAC,QAAQ,SAA2G;IAC5H,QAAQ,CAAC,OAAO,SAAwG;IACxH,QAAQ,CAAC,MAAM,qBAAkJ;IAE3J,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;CA+ChC"}
|
|
@@ -1,89 +1,61 @@
|
|
|
1
1
|
import { Command, Option } from 'clipanion';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { compareScreenshotsOnDisk, printComparisonToConsole, writeComparisonReport } from '../comparison.js';
|
|
2
4
|
import { ConsoleLogger, verbosityToLogLevel } from '../logger.js';
|
|
3
5
|
import { FileSystemStorage } from '../storage.js';
|
|
6
|
+
import { resolveProject } from '../resolveProject.js';
|
|
7
|
+
import { listPngFiles, matchGlob } from '../utils.js';
|
|
4
8
|
|
|
5
9
|
class CompareCommand extends Command {
|
|
6
|
-
static paths = [['compare']];
|
|
10
|
+
static paths = [['screenshot:compare']];
|
|
7
11
|
static usage = Command.Usage({
|
|
8
|
-
description: 'Compare
|
|
12
|
+
description: 'Compare screenshot directories (file-level only)',
|
|
9
13
|
examples: [
|
|
10
|
-
['Compare default directories', '$0 compare'],
|
|
14
|
+
['Compare default directories', '$0 screenshot:compare'],
|
|
15
|
+
['Compare with custom paths', '$0 screenshot:compare --baseline ./golden --current ./actual'],
|
|
16
|
+
['Generate report folder', '$0 screenshot:compare --report ./report'],
|
|
11
17
|
],
|
|
12
18
|
});
|
|
13
19
|
verbose = Option.Counter('-v,--verbose', 0, { description: 'Increase log verbosity (-v debug, -vv trace)' });
|
|
14
|
-
|
|
20
|
+
filter = Option.String('--filter', { required: false, description: 'Filter fixtures by glob pattern' });
|
|
21
|
+
project = Option.String('-p,--project', process.cwd(), { description: 'Project: a directory, vite config file, or component-explorer.json' });
|
|
22
|
+
baseline = Option.String('--baseline', '.screenshots/baseline', { description: 'Baseline screenshots directory' });
|
|
23
|
+
current = Option.String('--current', '.screenshots/current', { description: 'Current screenshots directory' });
|
|
24
|
+
report = Option.String('--report', { required: false, description: 'Output report folder (contains report.json, report.md, and changed screenshots)' });
|
|
15
25
|
async execute() {
|
|
26
|
+
const resolved = await resolveProject(this.project);
|
|
27
|
+
const projectDir = resolved.projectDir;
|
|
16
28
|
new ConsoleLogger('compare', this.context.stdout, verbosityToLogLevel(this.verbose));
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
const baselineData = await storage.read(`baseline/${file}`);
|
|
32
|
-
const currentData = await storage.read(`current/${file}`);
|
|
33
|
-
if (buffersEqual(baselineData, currentData)) {
|
|
34
|
-
unchanged.push(file);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
changed.push(file);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
29
|
+
const baselineDir = path.isAbsolute(this.baseline) ? this.baseline : path.join(projectDir, this.baseline);
|
|
30
|
+
const currentDir = path.isAbsolute(this.current) ? this.current : path.join(projectDir, this.current);
|
|
31
|
+
const baselineStorage = new FileSystemStorage(baselineDir);
|
|
32
|
+
const currentStorage = new FileSystemStorage(currentDir);
|
|
33
|
+
// List all PNG files
|
|
34
|
+
let baselineFiles = await listPngFiles(baselineDir);
|
|
35
|
+
let currentFiles = await listPngFiles(currentDir);
|
|
36
|
+
// Apply filter if specified
|
|
37
|
+
if (this.filter) {
|
|
38
|
+
const filterFn = (file) => matchGlob(file.replace(/\.png$/, ''), this.filter);
|
|
39
|
+
baselineFiles = baselineFiles.filter(filterFn);
|
|
40
|
+
currentFiles = currentFiles.filter(filterFn);
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const result = await compareScreenshotsOnDisk({ list: async () => currentFiles, read: (id) => currentStorage.read(id), exists: (id) => currentStorage.exists(id) }, { list: async () => baselineFiles, read: (id) => baselineStorage.read(id), exists: (id) => baselineStorage.exists(id) }, currentFiles, baselineFiles);
|
|
43
|
+
printComparisonToConsole(result, this.context.stdout);
|
|
44
|
+
if (this.report) {
|
|
45
|
+
const reportDir = path.isAbsolute(this.report) ? this.report : path.join(process.cwd(), this.report);
|
|
46
|
+
await writeComparisonReport({
|
|
47
|
+
reportDir,
|
|
48
|
+
result,
|
|
49
|
+
baselinePath: this.baseline,
|
|
50
|
+
currentPath: this.current,
|
|
51
|
+
getBaselineData: (id) => baselineStorage.read(`${id}.png`),
|
|
52
|
+
getCurrentData: (id) => currentStorage.read(`${id}.png`),
|
|
53
|
+
});
|
|
54
|
+
this.context.stdout.write(`\nReport written to: ${this.report}/\n`);
|
|
45
55
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for (const f of added) {
|
|
49
|
-
this.context.stdout.write(` + ${f}\n`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (removed.length > 0) {
|
|
53
|
-
this.context.stdout.write(`Removed (${removed.length}):\n`);
|
|
54
|
-
for (const f of removed) {
|
|
55
|
-
this.context.stdout.write(` - ${f}\n`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (changed.length > 0) {
|
|
59
|
-
this.context.stdout.write(`Changed (${changed.length}):\n`);
|
|
60
|
-
for (const f of changed) {
|
|
61
|
-
this.context.stdout.write(` ~ ${f}\n`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (unchanged.length > 0) {
|
|
65
|
-
this.context.stdout.write(`Unchanged (${unchanged.length}):\n`);
|
|
66
|
-
for (const f of unchanged) {
|
|
67
|
-
this.context.stdout.write(` = ${f}\n`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const hasDiffs = added.length > 0 || removed.length > 0 || changed.length > 0;
|
|
71
|
-
if (hasDiffs) {
|
|
72
|
-
this.context.stdout.write('\nDifferences found.\n');
|
|
73
|
-
return 1;
|
|
74
|
-
}
|
|
75
|
-
this.context.stdout.write('\nNo differences.\n');
|
|
76
|
-
return 0;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
function buffersEqual(a, b) {
|
|
80
|
-
if (a.length !== b.length)
|
|
81
|
-
return false;
|
|
82
|
-
for (let i = 0; i < a.length; i++) {
|
|
83
|
-
if (a[i] !== b[i])
|
|
84
|
-
return false;
|
|
56
|
+
const hasDiffs = result.added.length > 0 || result.removed.length > 0 || result.changed.length > 0;
|
|
57
|
+
return hasDiffs ? 1 : 0;
|
|
85
58
|
}
|
|
86
|
-
return true;
|
|
87
59
|
}
|
|
88
60
|
|
|
89
61
|
export { CompareCommand };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compareCommand.js","sources":["../../src/commands/compareCommand.ts"],"sourcesContent":[null],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compareCommand.js","sources":["../../src/commands/compareCommand.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { Command } from 'clipanion';
|
|
|
2
2
|
export declare class McpCommand extends Command {
|
|
3
3
|
static paths: string[][];
|
|
4
4
|
static usage: import("clipanion").Usage;
|
|
5
|
-
readonly
|
|
5
|
+
readonly project: string;
|
|
6
6
|
execute(): Promise<void>;
|
|
7
7
|
}
|
|
8
8
|
//# sourceMappingURL=mcpCommand.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpCommand.d.ts","sourceRoot":"","sources":["../../src/commands/mcpCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"mcpCommand.d.ts","sourceRoot":"","sources":["../../src/commands/mcpCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAK5C,qBAAa,UAAW,SAAQ,OAAO;IACtC,OAAgB,KAAK,aAAa;IAElC,OAAgB,KAAK,4BAKlB;IAEH,QAAQ,CAAC,OAAO,SAA2I;IAErJ,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAc9B"}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { Command, Option } from 'clipanion';
|
|
2
2
|
import { ensureDaemon } from '../daemon/lifecycle.js';
|
|
3
3
|
import { ComponentExplorerMcpServer } from '../mcp/McpServer.js';
|
|
4
|
+
import { resolveProject } from '../resolveProject.js';
|
|
4
5
|
|
|
5
6
|
class McpCommand extends Command {
|
|
6
7
|
static paths = [['mcp']];
|
|
7
8
|
static usage = Command.Usage({
|
|
8
9
|
description: 'Start an MCP server over stdio. Auto-starts a daemon if not already running.',
|
|
9
10
|
examples: [
|
|
10
|
-
['Start MCP server', '$0 mcp
|
|
11
|
+
['Start MCP server', '$0 mcp --project config.json'],
|
|
11
12
|
],
|
|
12
13
|
});
|
|
13
|
-
|
|
14
|
+
project = Option.String('-p,--project,-c', { required: true, description: 'Project: a directory, vite config file, or component-explorer.json' });
|
|
14
15
|
async execute() {
|
|
16
|
+
const resolved = await resolveProject(this.project);
|
|
17
|
+
if (resolved.kind !== 'explorerConfig') {
|
|
18
|
+
this.context.stderr.write('Error: mcp requires a component-explorer.json config file.\n');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
15
21
|
// Ensure daemon is running (auto-starts in background if needed)
|
|
16
|
-
const daemon = await ensureDaemon(
|
|
22
|
+
const daemon = await ensureDaemon(resolved.configPath);
|
|
17
23
|
// Create and connect the MCP server over stdio
|
|
18
24
|
const mcpServer = new ComponentExplorerMcpServer(daemon);
|
|
19
25
|
await mcpServer.connect();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpCommand.js","sources":["../../src/commands/mcpCommand.ts"],"sourcesContent":[null],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mcpCommand.js","sources":["../../src/commands/mcpCommand.ts"],"sourcesContent":[null],"names":[],"mappings":";;;;;AAKM,MAAO,UAAW,SAAQ,OAAO,CAAA;IACtC,OAAgB,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;AAEjC,IAAA,OAAgB,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;AACrC,QAAA,WAAW,EAAE,8EAA8E;AAC3F,QAAA,QAAQ,EAAE;YACT,CAAC,kBAAkB,EAAE,8BAA8B,CAAC;AACpD,SAAA;AACD,KAAA,CAAC;AAEO,IAAA,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,iBAAiB,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,oEAAoE,EAAE,CAAC;AAE1J,IAAA,MAAM,OAAO,GAAA;QACZ,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC;AACnD,QAAA,IAAI,QAAQ,CAAC,IAAI,KAAK,gBAAgB,EAAE;YACvC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,8DAA8D,CAAC;YACzF;QACD;;QAGA,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,UAAU,CAAC;;AAGtD,QAAA,MAAM,SAAS,GAAG,IAAI,0BAA0B,CAAC,MAAM,CAAC;AACxD,QAAA,MAAM,SAAS,CAAC,OAAO,EAAE;IAC1B;;;;;"}
|
|
@@ -5,7 +5,12 @@ export declare class ScreenshotCommand extends Command {
|
|
|
5
5
|
readonly verbose: number;
|
|
6
6
|
readonly filter: string | undefined;
|
|
7
7
|
readonly accept: boolean;
|
|
8
|
-
readonly
|
|
9
|
-
|
|
8
|
+
readonly project: string;
|
|
9
|
+
readonly target: string | undefined;
|
|
10
|
+
readonly compare: boolean;
|
|
11
|
+
readonly compareTarget: string | undefined;
|
|
12
|
+
readonly report: string | undefined;
|
|
13
|
+
execute(): Promise<number>;
|
|
14
|
+
private _runComparison;
|
|
10
15
|
}
|
|
11
16
|
//# sourceMappingURL=screenshotCommand.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshotCommand.d.ts","sourceRoot":"","sources":["../../src/commands/screenshotCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"screenshotCommand.d.ts","sourceRoot":"","sources":["../../src/commands/screenshotCommand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAW5C,qBAAa,iBAAkB,SAAQ,OAAO;IAC7C,OAAgB,KAAK,aAAoB;IAEzC,OAAgB,KAAK,4BAOlB;IAEH,QAAQ,CAAC,OAAO,SAAsG;IACtH,QAAQ,CAAC,MAAM,qBAAkG;IACjH,QAAQ,CAAC,MAAM,UAAiI;IAChJ,QAAQ,CAAC,OAAO,SAAuI;IACvJ,QAAQ,CAAC,MAAM,qBAAsK;IACrL,QAAQ,CAAC,OAAO,UAA8F;IAC9G,QAAQ,CAAC,aAAa,qBAA+K;IACrM,QAAQ,CAAC,MAAM,qBAA4G;IAErH,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;YAqElB,cAAc;CA+B5B"}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { Command, Option } from 'clipanion';
|
|
2
|
+
import * as path from 'node:path';
|
|
2
3
|
import { PlaywrightBrowserPageFactory } from '../browserPage.js';
|
|
4
|
+
import { compareScreenshotsInMemory, printComparisonToConsole, writeComparisonReport } from '../comparison.js';
|
|
3
5
|
import { BrowserComponentExplorer } from '../componentExplorer.js';
|
|
4
6
|
import { DefaultComponentExplorerHttpServerFactory } from '../httpServer.js';
|
|
5
7
|
import { ConsoleLogger, verbosityToLogLevel } from '../logger.js';
|
|
8
|
+
import { resolveProject, resolvedProjectToViteProjectRef } from '../resolveProject.js';
|
|
6
9
|
import { FileSystemStorage } from '../storage.js';
|
|
7
|
-
import {
|
|
10
|
+
import { matchGlob } from '../utils.js';
|
|
8
11
|
|
|
9
12
|
class ScreenshotCommand extends Command {
|
|
10
13
|
static paths = [['screenshot']];
|
|
@@ -18,19 +21,39 @@ class ScreenshotCommand extends Command {
|
|
|
18
21
|
});
|
|
19
22
|
verbose = Option.Counter('-v,--verbose', 0, { description: 'Increase log verbosity (-v debug, -vv trace)' });
|
|
20
23
|
filter = Option.String('--filter', { required: false, description: 'Filter fixtures by glob pattern' });
|
|
21
|
-
accept = Option.Boolean('--accept', false, { description: 'Write
|
|
22
|
-
|
|
24
|
+
accept = Option.Boolean('--accept', false, { description: 'Write to baseline instead of current (mutually exclusive with --target)' });
|
|
25
|
+
project = Option.String('-p,--project', process.cwd(), { description: 'Project: a directory, vite config file, or component-explorer.json' });
|
|
26
|
+
target = Option.String('--target', { required: false, description: 'Screenshot output directory (default: .screenshots/current, or .screenshots/baseline with --accept)' });
|
|
27
|
+
compare = Option.Boolean('--compare', false, { description: 'Compare screenshots after capturing' });
|
|
28
|
+
compareTarget = Option.String('--compare-target', { required: false, description: 'Directory to compare against (default: .screenshots/baseline, or .screenshots/current with --accept)' });
|
|
29
|
+
report = Option.String('--report', { required: false, description: 'Output report folder (requires --compare)' });
|
|
23
30
|
async execute() {
|
|
31
|
+
// Validate mutually exclusive options
|
|
32
|
+
if (this.accept && this.target) {
|
|
33
|
+
this.context.stderr.write('Error: --accept and --target are mutually exclusive.\n');
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
24
36
|
const logger = new ConsoleLogger('screenshot', this.context.stdout, verbosityToLogLevel(this.verbose));
|
|
25
|
-
const
|
|
26
|
-
const
|
|
37
|
+
const resolved = await resolveProject(this.project);
|
|
38
|
+
const projectDir = resolved.projectDir;
|
|
39
|
+
const viteProject = resolvedProjectToViteProjectRef(resolved);
|
|
40
|
+
// Determine target directory
|
|
41
|
+
const defaultTarget = this.accept ? '.screenshots/baseline' : '.screenshots/current';
|
|
42
|
+
const targetDir = this.target ?? defaultTarget;
|
|
43
|
+
const targetPath = path.isAbsolute(targetDir) ? targetDir : path.join(projectDir, targetDir);
|
|
44
|
+
// Determine compare target directory
|
|
45
|
+
const defaultCompareTarget = this.accept ? '.screenshots/current' : '.screenshots/baseline';
|
|
46
|
+
const compareTargetDir = this.compareTarget ?? defaultCompareTarget;
|
|
47
|
+
const compareTargetPath = path.isAbsolute(compareTargetDir) ? compareTargetDir : path.join(projectDir, compareTargetDir);
|
|
48
|
+
const storage = new FileSystemStorage(targetPath);
|
|
27
49
|
const serverFactory = new DefaultComponentExplorerHttpServerFactory();
|
|
28
50
|
const browserFactory = new PlaywrightBrowserPageFactory();
|
|
51
|
+
const screenshots = new Map();
|
|
29
52
|
let server;
|
|
30
53
|
let explorer;
|
|
31
54
|
try {
|
|
32
55
|
this.context.stdout.write('Starting Vite dev server...\n');
|
|
33
|
-
server = await serverFactory.createViteServer(
|
|
56
|
+
server = await serverFactory.createViteServer(viteProject, { logger });
|
|
34
57
|
this.context.stdout.write(`Server running at ${server.url}\n`);
|
|
35
58
|
explorer = new BrowserComponentExplorer(browserFactory, server, logger);
|
|
36
59
|
const fixtures = await explorer.listFixtures();
|
|
@@ -39,23 +62,47 @@ class ScreenshotCommand extends Command {
|
|
|
39
62
|
: fixtures;
|
|
40
63
|
this.context.stdout.write(`Found ${filtered.length} fixture(s)\n`);
|
|
41
64
|
for (const fixture of filtered) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
const result = await explorer.screenshotFixture(fixture.fixtureId);
|
|
66
|
+
await storage.write(`${fixture.fixtureId}.png`, result.image);
|
|
67
|
+
screenshots.set(fixture.fixtureId, { data: result.image, background: fixture.background });
|
|
45
68
|
this.context.stdout.write(` ✓ ${fixture.fixtureId}\n`);
|
|
46
69
|
}
|
|
47
|
-
this.context.stdout.write(`\nScreenshots saved to
|
|
70
|
+
this.context.stdout.write(`\nScreenshots saved to ${targetDir}/\n`);
|
|
48
71
|
}
|
|
49
72
|
finally {
|
|
50
73
|
await explorer?.dispose();
|
|
51
74
|
await browserFactory.dispose();
|
|
52
75
|
await server?.dispose();
|
|
53
76
|
}
|
|
77
|
+
// Compare if requested
|
|
78
|
+
if (this.compare) {
|
|
79
|
+
this.context.stdout.write(`\nComparing against ${compareTargetDir}...\n`);
|
|
80
|
+
return this._runComparison(screenshots, targetPath, compareTargetPath, projectDir);
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
async _runComparison(screenshots, targetPath, compareTargetPath, projectDir) {
|
|
85
|
+
const compareStorage = new FileSystemStorage(compareTargetPath);
|
|
86
|
+
const result = await compareScreenshotsInMemory(screenshots, {
|
|
87
|
+
list: async () => [],
|
|
88
|
+
read: (id) => compareStorage.read(id),
|
|
89
|
+
exists: (id) => compareStorage.exists(id),
|
|
90
|
+
});
|
|
91
|
+
printComparisonToConsole(result, this.context.stdout);
|
|
92
|
+
if (this.report) {
|
|
93
|
+
const reportDir = path.isAbsolute(this.report) ? this.report : path.join(projectDir, this.report);
|
|
94
|
+
await writeComparisonReport({
|
|
95
|
+
reportDir,
|
|
96
|
+
result,
|
|
97
|
+
baselinePath: compareTargetPath,
|
|
98
|
+
currentPath: targetPath,
|
|
99
|
+
getBaselineData: (id) => compareStorage.read(`${id}.png`),
|
|
100
|
+
getCurrentData: async (id) => screenshots.get(id).data,
|
|
101
|
+
});
|
|
102
|
+
this.context.stdout.write(`\nReport written to: ${this.report}/\n`);
|
|
103
|
+
}
|
|
104
|
+
return (result.added.length > 0 || result.changed.length > 0) ? 1 : 0;
|
|
54
105
|
}
|
|
55
|
-
}
|
|
56
|
-
function matchGlob(fixtureId, pattern) {
|
|
57
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$');
|
|
58
|
-
return regex.test(fixtureId);
|
|
59
106
|
}
|
|
60
107
|
|
|
61
108
|
export { ScreenshotCommand };
|