@thepixelhouse/cli 0.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +249 -0
  2. package/dist/cli.js +194 -47
  3. package/package.json +5 -3
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # @thepixelhouse/cli
2
+
3
+ Visual regression testing from the command line. Capture screenshots, diff them against baselines, run regression suites, and manage monitors — all from your terminal or CI pipeline.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @thepixelhouse/cli
9
+ ```
10
+
11
+ Or run without installing:
12
+
13
+ ```bash
14
+ npx @thepixelhouse/cli screenshot https://example.com
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ # 1. Create a config file
21
+ pixelhouse init
22
+
23
+ # 2. Set your API key
24
+ export PIXELHOUSE_API_KEY=ph_live_your_key_here
25
+
26
+ # 3. Capture your first screenshot
27
+ pixelhouse screenshot https://your-site.com --project prj_xxx
28
+
29
+ # 4. Promote it to a baseline
30
+ pixelhouse baseline create ss_abc123 --project prj_xxx --url https://your-site.com
31
+
32
+ # 5. Run a regression test
33
+ pixelhouse regression run --url https://your-site.com --baseline bl_xyz789
34
+ ```
35
+
36
+ ## Authentication
37
+
38
+ The CLI needs an API key. Provide it in any of these ways (highest priority first):
39
+
40
+ 1. **CLI flag:** `--api-key ph_live_xxx`
41
+ 2. **Environment variable:** `PIXELHOUSE_API_KEY=ph_live_xxx`
42
+ 3. **Config file:** `apiKey` field in `.pixelhouserc.json`
43
+
44
+ Get your API key from the [dashboard](https://thepixelhouse.co.uk/settings/api-keys).
45
+
46
+ ## Commands
47
+
48
+ ### `pixelhouse screenshot <url>`
49
+
50
+ Capture a screenshot of a URL.
51
+
52
+ ```bash
53
+ pixelhouse screenshot https://example.com
54
+ pixelhouse screenshot https://example.com --viewport mobile
55
+ pixelhouse screenshot https://example.com --viewport 1440x900 --full-page
56
+ pixelhouse screenshot https://example.com --output ./screenshot.png
57
+ ```
58
+
59
+ | Option | Description | Default |
60
+ |--------|-------------|---------|
61
+ | `--viewport <preset\|WxH>` | Viewport size (desktop, tablet, mobile, or custom) | `desktop` |
62
+ | `--full-page` | Capture the full scrollable page | `false` |
63
+ | `--project <id>` | Project ID | from config |
64
+ | `--output <path>` | Save PNG to a local file | - |
65
+ | `--wait-for <strategy>` | Load strategy (networkIdle, domContentLoaded, load) | `networkIdle` |
66
+
67
+ ### `pixelhouse compare <screenshot-a> <screenshot-b>`
68
+
69
+ Compare two screenshots for visual differences.
70
+
71
+ ```bash
72
+ pixelhouse compare ss_abc123 ss_def456
73
+ pixelhouse compare ss_abc123 ss_def456 --threshold 0.5 --output ./diff.png
74
+ ```
75
+
76
+ | Option | Description | Default |
77
+ |--------|-------------|---------|
78
+ | `--threshold <number>` | Diff threshold percentage (0-100) | `1.0` |
79
+ | `--output <path>` | Save diff image to a local file | - |
80
+
81
+ ### `pixelhouse regression run`
82
+
83
+ Run visual regression tests against baselines.
84
+
85
+ **Single URL mode:**
86
+
87
+ ```bash
88
+ pixelhouse regression run --url https://example.com --baseline bl_xyz789
89
+ ```
90
+
91
+ **Config-driven mode** (reads pages from `.pixelhouserc.json`):
92
+
93
+ ```bash
94
+ pixelhouse regression run
95
+ pixelhouse regression run --ci
96
+ pixelhouse regression run --fail-on-diff
97
+ ```
98
+
99
+ | Option | Description | Default |
100
+ |--------|-------------|---------|
101
+ | `--url <url>` | Single URL to test | - |
102
+ | `--baseline <id>` | Baseline ID to compare against | - |
103
+ | `--threshold <number>` | Diff threshold percentage | `1.0` |
104
+ | `--viewport <preset\|WxH>` | Viewport size | `desktop` |
105
+ | `--ci` | CI mode with exit codes | `false` |
106
+ | `--fail-on-diff` | Non-zero exit on any diff exceeding threshold | `false` |
107
+
108
+ **Exit codes (CI mode):**
109
+ - `0` — all tests passed
110
+ - `1` — visual regression detected
111
+ - `2` — error (capture failed, network error, etc.)
112
+
113
+ ### `pixelhouse baseline <subcommand>`
114
+
115
+ Manage screenshot baselines.
116
+
117
+ ```bash
118
+ # List baselines
119
+ pixelhouse baseline list --project prj_xxx
120
+ pixelhouse baseline list --project prj_xxx --branch staging
121
+
122
+ # Promote a screenshot to baseline
123
+ pixelhouse baseline create ss_abc123 --project prj_xxx --url https://example.com
124
+ pixelhouse baseline create ss_abc123 --project prj_xxx --url https://example.com --branch staging
125
+
126
+ # Delete a baseline
127
+ pixelhouse baseline delete bl_xyz789
128
+ ```
129
+
130
+ ### `pixelhouse monitor <subcommand>`
131
+
132
+ Manage scheduled visual monitors.
133
+
134
+ ```bash
135
+ # List monitors
136
+ pixelhouse monitor list --project prj_xxx
137
+
138
+ # Add a monitor (checks every hour)
139
+ pixelhouse monitor add https://example.com --project prj_xxx --every 1h
140
+
141
+ # Add with custom interval and threshold
142
+ pixelhouse monitor add https://example.com --project prj_xxx --every 30m --threshold 0.5
143
+
144
+ # Pause / resume / delete
145
+ pixelhouse monitor pause mon_abc123
146
+ pixelhouse monitor resume mon_abc123
147
+ pixelhouse monitor delete mon_abc123
148
+ ```
149
+
150
+ | Interval | Cron equivalent |
151
+ |----------|-----------------|
152
+ | `30m` | `*/30 * * * *` |
153
+ | `1h` | `0 */1 * * *` |
154
+ | `6h` | `0 */6 * * *` |
155
+ | `1d` | `0 0 */1 * *` |
156
+
157
+ You can also pass a raw cron expression: `--every "0 9 * * 1-5"`.
158
+
159
+ ### `pixelhouse init`
160
+
161
+ Create a `.pixelhouserc.json` configuration file.
162
+
163
+ ```bash
164
+ pixelhouse init
165
+ pixelhouse init --force # overwrite existing
166
+ ```
167
+
168
+ ## Configuration
169
+
170
+ Create a `.pixelhouserc.json` in your project root (or run `pixelhouse init`):
171
+
172
+ ```json
173
+ {
174
+ "apiKey": "ph_live_your_key_here",
175
+ "apiUrl": "https://api.thepixelhouse.co.uk",
176
+ "projectId": "prj_xxx",
177
+ "viewport": "desktop",
178
+ "threshold": 1.0,
179
+ "branch": "main",
180
+ "pages": [
181
+ {
182
+ "url": "https://your-site.com",
183
+ "baselineId": "bl_abc123",
184
+ "viewport": "desktop",
185
+ "threshold": 1.0
186
+ },
187
+ {
188
+ "url": "https://your-site.com/pricing",
189
+ "baselineId": "bl_def456",
190
+ "viewport": "mobile"
191
+ }
192
+ ]
193
+ }
194
+ ```
195
+
196
+ The CLI searches for this file starting from your current directory, walking up to the filesystem root.
197
+
198
+ ## CI/CD integration
199
+
200
+ ### GitHub Actions
201
+
202
+ ```yaml
203
+ - name: Run visual regression tests
204
+ env:
205
+ PIXELHOUSE_API_KEY: ${{ secrets.PIXELHOUSE_API_KEY }}
206
+ run: npx @thepixelhouse/cli regression run --ci
207
+ ```
208
+
209
+ ### GitLab CI
210
+
211
+ ```yaml
212
+ visual-regression:
213
+ script:
214
+ - npx @thepixelhouse/cli regression run --ci
215
+ variables:
216
+ PIXELHOUSE_API_KEY: $PIXELHOUSE_API_KEY
217
+ ```
218
+
219
+ ## Global options
220
+
221
+ These options apply to all commands:
222
+
223
+ | Option | Description |
224
+ |--------|-------------|
225
+ | `--api-key <key>` | API key (overrides env var and config) |
226
+ | `--api-url <url>` | API base URL |
227
+ | `--json` | Output results as JSON |
228
+ | `--no-color` | Disable coloured output |
229
+ | `-v, --version` | Print version |
230
+ | `-h, --help` | Print help |
231
+
232
+ ## JSON output
233
+
234
+ Pass `--json` to any command for machine-readable output:
235
+
236
+ ```bash
237
+ pixelhouse screenshot https://example.com --json | jq '.id'
238
+ pixelhouse regression run --json | jq '.summary'
239
+ ```
240
+
241
+ ## Links
242
+
243
+ - [Documentation](https://thepixelhouse.co.uk/docs)
244
+ - [Dashboard](https://thepixelhouse.co.uk)
245
+ - [API reference](https://api.thepixelhouse.co.uk/docs)
246
+
247
+ ## Licence
248
+
249
+ MIT - ToggleKit Ltd
package/dist/cli.js CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { Command as Command7 } from "commander";
5
- import chalk7 from "chalk";
5
+ import chalk8 from "chalk";
6
6
 
7
7
  // src/commands/screenshot.ts
8
8
  import { Command } from "commander";
9
- import * as fs2 from "fs";
10
- import * as path2 from "path";
9
+ import * as fs3 from "fs";
10
+ import * as path3 from "path";
11
11
  import ora from "ora";
12
+ import chalk2 from "chalk";
12
13
 
13
14
  // src/api/client.ts
14
15
  var ApiClient = class {
@@ -73,17 +74,17 @@ var ApiClient = class {
73
74
  return this.delete(`/v1/monitors/${id}`);
74
75
  }
75
76
  // ─── Internal helpers ─────────────────────────────
76
- async request(path5, init) {
77
- const url = `${this.baseUrl}${path5}`;
77
+ async request(path6, init) {
78
+ const url = `${this.baseUrl}${path6}`;
78
79
  const headers = new Headers(init.headers);
79
80
  headers.set("Authorization", `Bearer ${this.apiKey}`);
80
81
  headers.set("Accept", "application/json");
81
82
  headers.set("User-Agent", "@pixelhouse/cli");
82
83
  return fetch(url, { ...init, headers });
83
84
  }
84
- async get(path5) {
85
+ async get(path6) {
85
86
  try {
86
- const res = await this.request(path5, { method: "GET" });
87
+ const res = await this.request(path6, { method: "GET" });
87
88
  const body = await res.json();
88
89
  if (!res.ok || "error" in body) {
89
90
  const errBody = body;
@@ -94,9 +95,9 @@ var ApiClient = class {
94
95
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
95
96
  }
96
97
  }
97
- async getPaginated(path5) {
98
+ async getPaginated(path6) {
98
99
  try {
99
- const res = await this.request(path5, { method: "GET" });
100
+ const res = await this.request(path6, { method: "GET" });
100
101
  const body = await res.json();
101
102
  if (!res.ok || "error" in body) {
102
103
  const errBody = body;
@@ -107,9 +108,9 @@ var ApiClient = class {
107
108
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
108
109
  }
109
110
  }
110
- async post(path5, body) {
111
+ async post(path6, body) {
111
112
  try {
112
- const res = await this.request(path5, {
113
+ const res = await this.request(path6, {
113
114
  method: "POST",
114
115
  headers: { "Content-Type": "application/json" },
115
116
  body: JSON.stringify(body)
@@ -124,9 +125,9 @@ var ApiClient = class {
124
125
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
125
126
  }
126
127
  }
127
- async patch(path5, body) {
128
+ async patch(path6, body) {
128
129
  try {
129
- const res = await this.request(path5, {
130
+ const res = await this.request(path6, {
130
131
  method: "PATCH",
131
132
  headers: { "Content-Type": "application/json" },
132
133
  body: JSON.stringify(body)
@@ -141,9 +142,9 @@ var ApiClient = class {
141
142
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
142
143
  }
143
144
  }
144
- async delete(path5) {
145
+ async delete(path6) {
145
146
  try {
146
- const res = await this.request(path5, { method: "DELETE" });
147
+ const res = await this.request(path6, { method: "DELETE" });
147
148
  const json = await res.json();
148
149
  if (!res.ok || "error" in json) {
149
150
  const errBody = json;
@@ -154,9 +155,9 @@ var ApiClient = class {
154
155
  return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
155
156
  }
156
157
  }
157
- async getBlob(path5) {
158
+ async getBlob(path6) {
158
159
  try {
159
- const res = await this.request(path5, { method: "GET" });
160
+ const res = await this.request(path6, { method: "GET" });
160
161
  if (!res.ok) {
161
162
  let message = `API returned ${res.status}`;
162
163
  try {
@@ -174,6 +175,34 @@ var ApiClient = class {
174
175
  }
175
176
  };
176
177
 
178
+ // src/api/free-client.ts
179
+ var FreeClient = class {
180
+ baseUrl;
181
+ constructor(baseUrl) {
182
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
183
+ }
184
+ async captureScreenshot(params) {
185
+ try {
186
+ const res = await fetch(`${this.baseUrl}/v1/free/screenshot`, {
187
+ method: "POST",
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ "User-Agent": "@thepixelhouse/cli"
191
+ },
192
+ body: JSON.stringify(params)
193
+ });
194
+ const body = await res.json();
195
+ if (!res.ok || "error" in body) {
196
+ const errBody = body;
197
+ return { success: false, error: errBody.error.message };
198
+ }
199
+ return { success: true, data: body.data };
200
+ } catch (err) {
201
+ return { success: false, error: err instanceof Error ? err.message : "Unknown error" };
202
+ }
203
+ }
204
+ };
205
+
177
206
  // src/config/loader.ts
178
207
  import * as fs from "fs";
179
208
  import * as path from "path";
@@ -300,6 +329,58 @@ function output(text, jsonData, jsonMode) {
300
329
  }
301
330
  }
302
331
 
332
+ // src/lib/trial.ts
333
+ import * as fs2 from "fs";
334
+ import * as path2 from "path";
335
+ import * as os from "os";
336
+ var TRIAL_LIMIT = 10;
337
+ var TRIAL_FILE = "trial.json";
338
+ function defaultConfigDir() {
339
+ return path2.join(os.homedir(), ".config", "thepixelhouse");
340
+ }
341
+ function trialPath(configDir) {
342
+ return path2.join(configDir, TRIAL_FILE);
343
+ }
344
+ function readTrial(configDir) {
345
+ const filePath = trialPath(configDir);
346
+ try {
347
+ const raw = fs2.readFileSync(filePath, "utf-8");
348
+ return JSON.parse(raw);
349
+ } catch {
350
+ return { count: 0, firstUsed: (/* @__PURE__ */ new Date()).toISOString() };
351
+ }
352
+ }
353
+ function writeTrial(configDir, data) {
354
+ fs2.mkdirSync(configDir, { recursive: true });
355
+ fs2.writeFileSync(trialPath(configDir), JSON.stringify(data, null, 2) + "\n", "utf-8");
356
+ }
357
+ function getTrialUsage(configDir) {
358
+ const dir = configDir ?? defaultConfigDir();
359
+ const data = readTrial(dir);
360
+ return {
361
+ count: data.count,
362
+ remaining: Math.max(0, TRIAL_LIMIT - data.count)
363
+ };
364
+ }
365
+ function incrementTrialUsage(configDir) {
366
+ const dir = configDir ?? defaultConfigDir();
367
+ const data = readTrial(dir);
368
+ data.count += 1;
369
+ if (data.count === 1) {
370
+ data.firstUsed = (/* @__PURE__ */ new Date()).toISOString();
371
+ }
372
+ writeTrial(dir, data);
373
+ return {
374
+ count: data.count,
375
+ remaining: Math.max(0, TRIAL_LIMIT - data.count)
376
+ };
377
+ }
378
+ function isTrialExpired(configDir) {
379
+ const dir = configDir ?? defaultConfigDir();
380
+ const data = readTrial(dir);
381
+ return data.count >= TRIAL_LIMIT;
382
+ }
383
+
303
384
  // src/commands/screenshot.ts
304
385
  function createScreenshotCommand() {
305
386
  const cmd = new Command("screenshot").description("Capture a screenshot of a URL").argument("<url>", "URL to capture").option("--viewport <viewport>", "viewport preset or WxH (e.g. desktop, 1440x900)", "desktop").option("--full-page", "capture the full scrollable page", false).option("--project <id>", "project ID to associate the screenshot with").option("--output <path>", "save the screenshot PNG to a local file").option("--wait-for <strategy>", "page load strategy: networkIdle, domContentLoaded, load", "networkIdle").action(async (url, opts) => {
@@ -309,8 +390,12 @@ function createScreenshotCommand() {
309
390
  apiUrl: parentOpts?.apiUrl
310
391
  });
311
392
  const jsonMode = parentOpts?.json ?? false;
312
- const apiKey = requireApiKey(config);
313
- const client = new ApiClient(config.apiUrl ?? "https://api.thepixelhouse.co.uk", apiKey);
393
+ const apiUrl = config.apiUrl ?? "https://api.thepixelhouse.co.uk";
394
+ if (!config.apiKey) {
395
+ await handleTrialCapture(url, opts, apiUrl, jsonMode);
396
+ return;
397
+ }
398
+ const client = new ApiClient(apiUrl, config.apiKey);
314
399
  const spinner = jsonMode ? null : ora("Capturing screenshot...").start();
315
400
  const result = await client.captureScreenshot({
316
401
  url,
@@ -330,8 +415,8 @@ function createScreenshotCommand() {
330
415
  spinner?.warn("Screenshot captured but download failed");
331
416
  throw new CliError(`Failed to download image: ${downloadResult.error}`, EXIT_CODES.ERROR);
332
417
  }
333
- const outputPath = path2.resolve(opts.output);
334
- fs2.writeFileSync(outputPath, Buffer.from(downloadResult.data));
418
+ const outputPath = path3.resolve(opts.output);
419
+ fs3.writeFileSync(outputPath, Buffer.from(downloadResult.data));
335
420
  spinner?.succeed("Screenshot captured and saved");
336
421
  const text2 = [
337
422
  formatSuccess("Screenshot captured and saved"),
@@ -358,13 +443,75 @@ function createScreenshotCommand() {
358
443
  });
359
444
  return cmd;
360
445
  }
446
+ async function handleTrialCapture(url, opts, apiUrl, jsonMode) {
447
+ if (isTrialExpired()) {
448
+ const msg = [
449
+ chalk2.yellow(`You have used all ${String(TRIAL_LIMIT)} free trial screenshots.`),
450
+ "",
451
+ "Sign up for a free account to continue:",
452
+ chalk2.cyan(" https://thepixelhouse.co.uk/register"),
453
+ "",
454
+ "Then set your API key:",
455
+ chalk2.dim(" export PIXELHOUSE_API_KEY=ph_live_your_key")
456
+ ].join("\n");
457
+ throw new CliError(msg, EXIT_CODES.ERROR);
458
+ }
459
+ const viewport = opts.viewport;
460
+ if (!["desktop", "tablet", "mobile"].includes(viewport)) {
461
+ throw new CliError(
462
+ "Trial mode only supports viewport presets: desktop, tablet, mobile. Sign up for custom viewports.",
463
+ EXIT_CODES.ERROR
464
+ );
465
+ }
466
+ const { remaining } = getTrialUsage();
467
+ const spinner = jsonMode ? null : ora(`Capturing screenshot (trial: ${String(remaining)} of ${String(TRIAL_LIMIT)} remaining)...`).start();
468
+ const client = new FreeClient(apiUrl);
469
+ const result = await client.captureScreenshot({ url, viewport });
470
+ if (!result.success) {
471
+ spinner?.fail("Screenshot capture failed");
472
+ throw new CliError(result.error, EXIT_CODES.ERROR);
473
+ }
474
+ const usage = incrementTrialUsage();
475
+ if (opts.output) {
476
+ const outputPath = path3.resolve(opts.output);
477
+ const imageBuffer = Buffer.from(result.data.image, "base64");
478
+ fs3.writeFileSync(outputPath, imageBuffer);
479
+ spinner?.succeed("Screenshot captured and saved");
480
+ const text2 = [
481
+ formatSuccess("Screenshot captured and saved (trial)"),
482
+ formatInfo("URL", url),
483
+ formatInfo("Viewport", `${result.data.viewport} (${String(result.data.viewportWidth)}x${String(result.data.viewportHeight)})`),
484
+ formatInfo("Saved to", outputPath),
485
+ "",
486
+ chalk2.dim(`Trial: ${String(usage.remaining)} of ${String(TRIAL_LIMIT)} free screenshots remaining`)
487
+ ].join("\n");
488
+ output(text2, { ...result.data, trial: true, remaining: usage.remaining }, jsonMode);
489
+ return;
490
+ }
491
+ spinner?.succeed("Screenshot captured (trial)");
492
+ const text = [
493
+ formatSuccess("Screenshot captured (trial)"),
494
+ formatInfo("URL", url),
495
+ formatInfo("Viewport", `${result.data.viewport} (${String(result.data.viewportWidth)}x${String(result.data.viewportHeight)})`),
496
+ "",
497
+ chalk2.dim(`Trial: ${String(usage.remaining)} of ${String(TRIAL_LIMIT)} free screenshots remaining`),
498
+ chalk2.dim("Use --output <file.png> to save the image locally")
499
+ ].join("\n");
500
+ output(text, { ...result.data, trial: true, remaining: usage.remaining }, jsonMode);
501
+ if (usage.remaining <= 3 && usage.remaining > 0) {
502
+ console.error(
503
+ chalk2.yellow(`
504
+ Sign up for a free account to keep capturing: https://thepixelhouse.co.uk/register`)
505
+ );
506
+ }
507
+ }
361
508
 
362
509
  // src/commands/compare.ts
363
510
  import { Command as Command2 } from "commander";
364
- import * as fs3 from "fs";
365
- import * as path3 from "path";
511
+ import * as fs4 from "fs";
512
+ import * as path4 from "path";
366
513
  import ora2 from "ora";
367
- import chalk2 from "chalk";
514
+ import chalk3 from "chalk";
368
515
  function createCompareCommand() {
369
516
  const cmd = new Command2("compare").description("Compare two screenshots for visual differences").argument("<screenshot-a>", "ID of the first screenshot (before)").argument("<screenshot-b>", "ID of the second screenshot (after)").option("--threshold <number>", "diff threshold percentage (0-100)", "1.0").option("--output <path>", "save the diff image to a local file").action(async (screenshotA, screenshotB, opts) => {
370
517
  const parentOpts = cmd.parent?.opts();
@@ -393,14 +540,14 @@ function createCompareCommand() {
393
540
  if (opts.output && comparison.diffImageUrl) {
394
541
  const downloadResult = await client.downloadDiffImage(comparison.id);
395
542
  if (downloadResult.success) {
396
- const outputPath = path3.resolve(opts.output);
397
- fs3.writeFileSync(outputPath, Buffer.from(downloadResult.data));
543
+ const outputPath = path4.resolve(opts.output);
544
+ fs4.writeFileSync(outputPath, Buffer.from(downloadResult.data));
398
545
  }
399
546
  }
400
547
  spinner?.succeed("Comparison complete");
401
- const statusLabel = comparison.status === "pass" ? chalk2.green("PASS") : comparison.status === "fail" ? chalk2.red("FAIL") : chalk2.yellow(comparison.status.toUpperCase());
548
+ const statusLabel = comparison.status === "pass" ? chalk3.green("PASS") : comparison.status === "fail" ? chalk3.red("FAIL") : chalk3.yellow(comparison.status.toUpperCase());
402
549
  const lines = [
403
- `${chalk2.bold("Result:")} ${statusLabel}`,
550
+ `${chalk3.bold("Result:")} ${statusLabel}`,
404
551
  formatInfo("Comparison ID", comparison.id),
405
552
  formatInfo("Diff percentage", comparison.diffPercentage !== null ? `${String(comparison.diffPercentage)}%` : "N/A"),
406
553
  formatInfo("SSIM score", comparison.ssimScore !== null ? String(comparison.ssimScore) : "N/A"),
@@ -410,7 +557,7 @@ function createCompareCommand() {
410
557
  lines.push(formatInfo("Diff image", comparison.diffImageUrl));
411
558
  }
412
559
  if (opts.output && comparison.diffImageUrl) {
413
- lines.push(formatInfo("Saved to", path3.resolve(opts.output)));
560
+ lines.push(formatInfo("Saved to", path4.resolve(opts.output)));
414
561
  }
415
562
  output(lines.join("\n"), comparison, jsonMode);
416
563
  });
@@ -420,7 +567,7 @@ function createCompareCommand() {
420
567
  // src/commands/regression.ts
421
568
  import { Command as Command3 } from "commander";
422
569
  import ora3 from "ora";
423
- import chalk3 from "chalk";
570
+ import chalk4 from "chalk";
424
571
  function toTableRow(url, result) {
425
572
  return {
426
573
  url,
@@ -497,10 +644,10 @@ function createRegressionCommand() {
497
644
  }
498
645
  const colouredRows = rows.map((row) => ({
499
646
  ...row,
500
- status: row.status === "pass" ? chalk3.green("pass") : row.status === "fail" ? chalk3.red("fail") : chalk3.yellow(row.status)
647
+ status: row.status === "pass" ? chalk4.green("pass") : row.status === "fail" ? chalk4.red("fail") : chalk4.yellow(row.status)
501
648
  }));
502
649
  spinner?.stop();
503
- const summary = hasFailure ? formatError(`${String(rows.filter((r) => r.status === "fail" || r.status === chalk3.red("fail")).length)} test(s) failed`) : hasError ? formatError(`${String(rows.filter((r) => r.status === "error" || r.status === chalk3.yellow("error")).length)} test(s) errored`) : formatSuccess(`All ${String(rows.length)} test(s) passed`);
650
+ const summary = hasFailure ? formatError(`${String(rows.filter((r) => r.status === "fail" || r.status === chalk4.red("fail")).length)} test(s) failed`) : hasError ? formatError(`${String(rows.filter((r) => r.status === "error" || r.status === chalk4.yellow("error")).length)} test(s) errored`) : formatSuccess(`All ${String(rows.length)} test(s) passed`);
504
651
  const tableOutput = formatTable(colouredRows, ["url", "status", "diff", "ssim", "threshold"]);
505
652
  const text = `${tableOutput}
506
653
 
@@ -543,7 +690,7 @@ function buildPageList(opts, config) {
543
690
  // src/commands/baseline.ts
544
691
  import { Command as Command4 } from "commander";
545
692
  import ora4 from "ora";
546
- import chalk4 from "chalk";
693
+ import chalk5 from "chalk";
547
694
  function createBaselineCommand() {
548
695
  const baseline = new Command4("baseline").description("Manage screenshot baselines");
549
696
  baseline.command("list").description("List baselines for a project").requiredOption("--project <id>", "project ID").option("--branch <name>", "filter by branch name").option("--limit <number>", "max results to return", "25").action(async (opts) => {
@@ -571,7 +718,7 @@ function createBaselineCommand() {
571
718
  url: b.url,
572
719
  viewport: b.viewport,
573
720
  branch: b.branch,
574
- active: b.isActive ? chalk4.green("yes") : chalk4.dim("no"),
721
+ active: b.isActive ? chalk5.green("yes") : chalk5.dim("no"),
575
722
  created: b.createdAt
576
723
  }));
577
724
  const tableText = formatTable(rows, ["id", "url", "viewport", "branch", "active", "created"]);
@@ -633,7 +780,7 @@ function createBaselineCommand() {
633
780
  // src/commands/monitor.ts
634
781
  import { Command as Command5 } from "commander";
635
782
  import ora5 from "ora";
636
- import chalk5 from "chalk";
783
+ import chalk6 from "chalk";
637
784
  function intervalToCron(interval) {
638
785
  if (interval.includes(" ")) {
639
786
  return interval;
@@ -682,7 +829,7 @@ function createMonitorCommand() {
682
829
  viewport: m.viewport,
683
830
  schedule: m.cronExpression,
684
831
  threshold: `${String(m.threshold)}%`,
685
- active: m.isActive ? chalk5.green("yes") : chalk5.dim("no"),
832
+ active: m.isActive ? chalk6.green("yes") : chalk6.dim("no"),
686
833
  lastRun: m.lastRunAt ?? "never",
687
834
  lastStatus: m.lastStatus ?? "-"
688
835
  }));
@@ -788,9 +935,9 @@ function createMonitorCommand() {
788
935
 
789
936
  // src/commands/init.ts
790
937
  import { Command as Command6 } from "commander";
791
- import * as fs4 from "fs";
792
- import * as path4 from "path";
793
- import chalk6 from "chalk";
938
+ import * as fs5 from "fs";
939
+ import * as path5 from "path";
940
+ import chalk7 from "chalk";
794
941
  var DEFAULT_CONFIG = {
795
942
  apiUrl: "https://api.thepixelhouse.co.uk",
796
943
  viewport: "desktop",
@@ -802,8 +949,8 @@ function createInitCommand() {
802
949
  const cmd = new Command6("init").description("Create a .pixelhouserc.json configuration file").option("--force", "overwrite existing configuration file", false).action(async (opts) => {
803
950
  const parentOpts = cmd.parent?.opts();
804
951
  const jsonMode = parentOpts?.json ?? false;
805
- const configPath = path4.join(process.cwd(), CONFIG_FILE_NAME);
806
- if (fs4.existsSync(configPath) && !opts.force) {
952
+ const configPath = path5.join(process.cwd(), CONFIG_FILE_NAME);
953
+ if (fs5.existsSync(configPath) && !opts.force) {
807
954
  throw new CliError(
808
955
  `${CONFIG_FILE_NAME} already exists. Use --force to overwrite.`,
809
956
  EXIT_CODES.ERROR
@@ -814,15 +961,15 @@ function createInitCommand() {
814
961
  if (apiKey) {
815
962
  config.apiKey = apiKey;
816
963
  }
817
- fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
964
+ fs5.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
818
965
  const text = [
819
966
  formatSuccess("Configuration file created"),
820
967
  formatInfo("Path", configPath),
821
968
  "",
822
- chalk6.dim("Next steps:"),
823
- chalk6.dim(` 1. Add your API key to ${CONFIG_FILE_NAME} or set PIXELHOUSE_API_KEY`),
824
- chalk6.dim(' 2. Add pages to the "pages" array for regression testing'),
825
- chalk6.dim(' 3. Run "pixelhouse screenshot <url>" to capture your first screenshot')
969
+ chalk7.dim("Next steps:"),
970
+ chalk7.dim(` 1. Add your API key to ${CONFIG_FILE_NAME} or set PIXELHOUSE_API_KEY`),
971
+ chalk7.dim(' 2. Add pages to the "pages" array for regression testing'),
972
+ chalk7.dim(' 3. Run "pixelhouse screenshot <url>" to capture your first screenshot')
826
973
  ].join("\n");
827
974
  output(text, { path: configPath, config }, jsonMode);
828
975
  });
@@ -836,7 +983,7 @@ function createProgram() {
836
983
  program2.name("pixelhouse").description("Visual regression testing CLI for The Pixel House").version(VERSION, "-v, --version").option("--api-key <key>", "API key (overrides PIXELHOUSE_API_KEY env var)").option("--api-url <url>", "API base URL (default: https://api.thepixelhouse.co.uk)").option("--json", "output results as JSON", false).option("--no-color", "disable coloured output").hook("preAction", (_thisCommand, actionCommand) => {
837
984
  const rootOpts = actionCommand.optsWithGlobals();
838
985
  if (rootOpts.color === false) {
839
- chalk7.level = 0;
986
+ chalk8.level = 0;
840
987
  }
841
988
  });
842
989
  program2.addCommand(createScreenshotCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thepixelhouse/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Visual regression testing CLI for The Pixel House. Capture screenshots, diff them, manage baselines, and run monitors from your terminal.",
5
5
  "license": "MIT",
6
6
  "author": "ToggleKit Ltd <hello@thepixelhouse.co.uk>",
@@ -29,7 +29,8 @@
29
29
  },
30
30
  "type": "module",
31
31
  "files": [
32
- "dist"
32
+ "dist",
33
+ "README.md"
33
34
  ],
34
35
  "engines": {
35
36
  "node": ">=18.0.0"
@@ -53,6 +54,7 @@
53
54
  "@pixelhouse/shared": "workspace:*",
54
55
  "@types/node": "^25.5.0",
55
56
  "tsup": "^8.0.0",
56
- "typescript": "^5.7.0"
57
+ "typescript": "^5.7.0",
58
+ "vitest": "^2.1.0"
57
59
  }
58
60
  }