@vizzly-testing/cli 0.29.6 → 0.31.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 (87) hide show
  1. package/README.md +34 -6
  2. package/dist/api/client.js +6 -3
  3. package/dist/api/endpoints.js +59 -0
  4. package/dist/api/index.js +1 -1
  5. package/dist/auth/core.js +1 -1
  6. package/dist/auth/index.js +2 -1
  7. package/dist/cli.js +206 -51
  8. package/dist/client/index.js +21 -29
  9. package/dist/commands/api.js +145 -90
  10. package/dist/commands/baselines.js +78 -47
  11. package/dist/commands/builds.js +9 -8
  12. package/dist/commands/comparisons.js +7 -8
  13. package/dist/commands/config-cmd.js +15 -7
  14. package/dist/commands/context.js +870 -0
  15. package/dist/commands/doctor.js +116 -81
  16. package/dist/commands/init.js +202 -201
  17. package/dist/commands/login.js +134 -65
  18. package/dist/commands/logout.js +18 -7
  19. package/dist/commands/preview.js +29 -57
  20. package/dist/commands/project.js +120 -0
  21. package/dist/commands/projects.js +9 -2
  22. package/dist/commands/review.js +236 -202
  23. package/dist/commands/run.js +75 -72
  24. package/dist/commands/status.js +228 -150
  25. package/dist/commands/tdd-daemon-process.js +98 -0
  26. package/dist/commands/tdd-daemon.js +318 -212
  27. package/dist/commands/tdd.js +23 -8
  28. package/dist/commands/upload.js +10 -9
  29. package/dist/commands/whoami.js +64 -26
  30. package/dist/config/core.js +37 -36
  31. package/dist/context/local-workspace-provider.js +532 -0
  32. package/dist/context/provider-resolver.js +36 -0
  33. package/dist/index.js +5 -4
  34. package/dist/plugin-loader.js +55 -49
  35. package/dist/project/core.js +2 -2
  36. package/dist/project/operations.js +6 -3
  37. package/dist/reporter/reporter-bundle.css +2 -1
  38. package/dist/reporter/reporter-bundle.iife.js +119 -119
  39. package/dist/reporter-ssr/ssr-entry.js +605 -509
  40. package/dist/sdk/index.js +162 -107
  41. package/dist/server/handlers/tdd-handler.js +30 -54
  42. package/dist/server/routers/assets.js +4 -0
  43. package/dist/server/routers/baseline.js +195 -178
  44. package/dist/server/routers/events.js +226 -148
  45. package/dist/server/routers/projects.js +14 -5
  46. package/dist/server/routers/screenshot.js +1 -31
  47. package/dist/services/build-manager.js +19 -97
  48. package/dist/services/config-service.js +26 -34
  49. package/dist/services/index.js +2 -4
  50. package/dist/services/static-report-generator.js +11 -12
  51. package/dist/services/test-runner.js +21 -27
  52. package/dist/tdd/core/hotspot-coverage.js +1 -1
  53. package/dist/tdd/index.js +1 -1
  54. package/dist/tdd/metadata/region-metadata.js +0 -37
  55. package/dist/tdd/runtime-context.js +78 -0
  56. package/dist/tdd/server-registry.js +38 -17
  57. package/dist/tdd/services/baseline-manager.js +23 -0
  58. package/dist/tdd/services/hotspot-service.js +1 -14
  59. package/dist/tdd/tdd-service.js +82 -113
  60. package/dist/types/client.d.ts +36 -1
  61. package/dist/types/index.d.ts +114 -22
  62. package/dist/types/sdk.d.ts +47 -7
  63. package/dist/uploader/core.js +5 -4
  64. package/dist/uploader/index.js +79 -2
  65. package/dist/uploader/operations.js +40 -5
  66. package/dist/utils/api-url.js +14 -0
  67. package/dist/utils/async-utils.js +26 -0
  68. package/dist/utils/ci-env.js +19 -12
  69. package/dist/utils/colors.js +46 -44
  70. package/dist/utils/config-loader.js +39 -65
  71. package/dist/utils/config-schema.js +19 -55
  72. package/dist/utils/fetch-utils.js +12 -5
  73. package/dist/utils/git.js +35 -72
  74. package/dist/utils/global-config.js +22 -3
  75. package/dist/utils/output.js +19 -7
  76. package/dist/utils/patterns.js +14 -0
  77. package/dist/utils/project-link-store.js +105 -0
  78. package/dist/utils/report-data.js +31 -0
  79. package/dist/utils/screenshot-options.js +28 -0
  80. package/dist/utils/secret-store.js +65 -0
  81. package/dist/utils/security.js +3 -2
  82. package/package.json +14 -11
  83. package/dist/services/screenshot-server.js +0 -65
  84. package/dist/services/server-manager.js +0 -81
  85. package/dist/services/uploader.js +0 -100
  86. package/dist/tdd/services/region-service.js +0 -75
  87. package/dist/utils/build-history.js +0 -103
@@ -0,0 +1,65 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ let execFileAsync = promisify(execFile);
4
+ let KEYCHAIN_SERVICE = 'vizzly-cli';
5
+ export function canUseKeychain(platform = process.platform, env = process.env) {
6
+ return platform === 'darwin' && env.VIZZLY_DISABLE_KEYCHAIN !== 'true';
7
+ }
8
+ export async function saveSecret(account, secret, options = {}) {
9
+ let {
10
+ service = KEYCHAIN_SERVICE,
11
+ platform = process.platform,
12
+ env = process.env,
13
+ execFileFn = execFileAsync
14
+ } = options;
15
+ if (!canUseKeychain(platform, env)) {
16
+ return false;
17
+ }
18
+ try {
19
+ try {
20
+ await execFileFn('security', ['delete-generic-password', '-s', service, '-a', account]);
21
+ } catch {
22
+ // Missing secrets are fine; add-generic-password creates the current value.
23
+ }
24
+ await execFileFn('security', ['add-generic-password', '-s', service, '-a', account, '-w', secret, '-U']);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ export async function getSecret(account, options = {}) {
31
+ let {
32
+ service = KEYCHAIN_SERVICE,
33
+ platform = process.platform,
34
+ env = process.env,
35
+ execFileFn = execFileAsync
36
+ } = options;
37
+ if (!canUseKeychain(platform, env)) {
38
+ return null;
39
+ }
40
+ try {
41
+ let {
42
+ stdout
43
+ } = await execFileFn('security', ['find-generic-password', '-s', service, '-a', account, '-w']);
44
+ return stdout.trim() || null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+ export async function deleteSecret(account, options = {}) {
50
+ let {
51
+ service = KEYCHAIN_SERVICE,
52
+ platform = process.platform,
53
+ env = process.env,
54
+ execFileFn = execFileAsync
55
+ } = options;
56
+ if (!canUseKeychain(platform, env)) {
57
+ return false;
58
+ }
59
+ try {
60
+ await execFileFn('security', ['delete-generic-password', '-s', service, '-a', account]);
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
@@ -206,8 +206,9 @@ export function validateScreenshotProperties(properties = {}) {
206
206
 
207
207
  if (typeof key === 'string' && key.length <= 50 && /^[a-zA-Z0-9_-]+$/.test(key)) {
208
208
  if (typeof value === 'string' && value.length <= 200) {
209
- // Store sanitized version of string values
210
- validated[key] = value.replace(/[<>&"']/g, ''); // Basic HTML entity prevention
209
+ // Preserve safe URL/query characters like '&' in metadata values.
210
+ // Rendering layers should escape for HTML instead of mutating payload data here.
211
+ validated[key] = value.replace(/[<>"']/g, '');
211
212
  } else if (typeof value === 'number' && !Number.isNaN(value) && Number.isFinite(value)) {
212
213
  validated[key] = value;
213
214
  } else if (typeof value === 'boolean') {
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.29.6",
4
- "description": "Visual review platform for UI developers and designers",
3
+ "version": "0.31.0",
4
+ "description": "Visual review for agents and teams",
5
5
  "keywords": [
6
+ "llm-context",
7
+ "visual-context",
8
+ "approved-baselines",
6
9
  "visual-testing",
7
10
  "screenshot-testing",
8
- "visual-regression",
9
11
  "visual-review",
10
12
  "ui-testing",
11
13
  "collaboration",
@@ -51,7 +53,6 @@
51
53
  "files": [
52
54
  "bin",
53
55
  "dist",
54
- "claude-plugin",
55
56
  "README.md",
56
57
  "LICENSE"
57
58
  ],
@@ -71,12 +72,14 @@
71
72
  "test:watch": "node --test --test-reporter=spec --watch $(find tests -name '*.test.js')",
72
73
  "test:reporter": "playwright test --config=tests/reporter/playwright.config.js",
73
74
  "test:reporter:visual": "node bin/vizzly.js run \"npm run test:reporter\"",
75
+ "test:swift:e2e": "npm run build && node clients/swift/scripts/run-e2e.js",
74
76
  "test:tui": "node --test --test-reporter=spec tests/tui/*.test.js",
75
77
  "test:tui:docker": "./tests/tui/run-tui-tests.sh",
76
- "lint": "biome check src tests",
77
- "lint:fix": "biome check --write --unsafe src tests",
78
- "format": "biome format --write src tests",
79
- "format:check": "biome format src tests",
78
+ "smoke:prod:pack": "node scripts/smoke-prod-pack.js",
79
+ "lint": "biome check src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
80
+ "lint:fix": "biome check --write --unsafe src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
81
+ "format": "biome format --write src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
82
+ "format:check": "biome format src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
80
83
  "fix": "npm run format && npm run lint:fix"
81
84
  },
82
85
  "engines": {
@@ -119,7 +122,7 @@
119
122
  "@tailwindcss/postcss": "^4.1.13",
120
123
  "@tanstack/react-query": "^5.90.11",
121
124
  "@types/node": "^25.0.2",
122
- "@vitejs/plugin-react": "^5.0.3",
125
+ "@vitejs/plugin-react": "^6.0.1",
123
126
  "@vizzly-testing/observatory": "^0.3.3",
124
127
  "autoprefixer": "^10.4.21",
125
128
  "babel-plugin-react-compiler": "^1.0.0",
@@ -131,8 +134,8 @@
131
134
  "tailwindcss": "^4.1.13",
132
135
  "tsd": "^0.33.0",
133
136
  "tui-driver": "github:vizzly-testing/tui-driver",
134
- "typescript": "^5.0.4",
135
- "vite": "^7.1.7",
137
+ "typescript": "^6.0.3",
138
+ "vite": "^8.0.0",
136
139
  "wouter": "^3.7.1"
137
140
  }
138
141
  }
@@ -1,65 +0,0 @@
1
- /**
2
- * Screenshot Server Service
3
- * Listens for and processes screenshots from the test runner
4
- *
5
- * This class is a thin wrapper around the functional operations in
6
- * src/screenshot-server/. It maintains backwards compatibility while
7
- * delegating to pure functions for testability.
8
- */
9
-
10
- import { createServer } from 'node:http';
11
- import { VizzlyError } from '../errors/vizzly-error.js';
12
- import { handleRequest, parseRequestBody, startServer, stopServer } from '../screenshot-server/index.js';
13
- import * as output from '../utils/output.js';
14
- export class ScreenshotServer {
15
- constructor(config, buildManager, options = {}) {
16
- this.config = config;
17
- this.buildManager = buildManager;
18
- this.server = null;
19
-
20
- // Dependency injection for testing
21
- this.deps = options.deps || {
22
- createHttpServer: createServer,
23
- output,
24
- createError: (message, code) => new VizzlyError(message, code)
25
- };
26
- }
27
- async start() {
28
- this.server = await startServer({
29
- config: this.config,
30
- requestHandler: this.handleRequest.bind(this),
31
- deps: {
32
- createHttpServer: this.deps.createHttpServer,
33
- createError: this.deps.createError,
34
- output: this.deps.output
35
- }
36
- });
37
- }
38
- async stop() {
39
- await stopServer({
40
- server: this.server,
41
- deps: {
42
- output: this.deps.output
43
- }
44
- });
45
- }
46
- async handleRequest(req, res) {
47
- await handleRequest({
48
- req,
49
- res,
50
- deps: {
51
- buildManager: this.buildManager,
52
- createError: this.deps.createError,
53
- output: this.deps.output
54
- }
55
- });
56
- }
57
- async parseRequestBody(req) {
58
- return parseRequestBody({
59
- req,
60
- deps: {
61
- createError: this.deps.createError
62
- }
63
- });
64
- }
65
- }
@@ -1,81 +0,0 @@
1
- /**
2
- * Server Manager Service
3
- * Manages the HTTP server with functional handlers
4
- *
5
- * This class is a thin wrapper around the functional operations in
6
- * src/server-manager/. It maintains backwards compatibility while
7
- * delegating to pure functions for testability.
8
- */
9
-
10
- import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
11
- import { createApiClient } from '../api/index.js';
12
- import { createApiHandler } from '../server/handlers/api-handler.js';
13
- import { createTddHandler } from '../server/handlers/tdd-handler.js';
14
- import { createHttpServer } from '../server/http-server.js';
15
- import { buildServerInterface, getTddResults, startServer, stopServer } from '../server-manager/index.js';
16
- export class ServerManager {
17
- constructor(config, options = {}) {
18
- this.config = config;
19
- this.httpServer = null;
20
- this.handler = null;
21
- this.services = options.services || {};
22
- this.tddMode = false;
23
-
24
- // Dependency injection for testing - defaults to real implementations
25
- this.deps = options.deps || {
26
- createHttpServer,
27
- createTddHandler,
28
- createApiHandler,
29
- createApiClient,
30
- fs: {
31
- mkdirSync,
32
- writeFileSync,
33
- existsSync,
34
- unlinkSync
35
- }
36
- };
37
- }
38
- async start(buildId = null, tddMode = false, setBaseline = false) {
39
- this.buildId = buildId;
40
- this.tddMode = tddMode;
41
- this.setBaseline = setBaseline;
42
- let result = await startServer({
43
- config: this.config,
44
- buildId,
45
- tddMode,
46
- setBaseline,
47
- projectRoot: process.cwd(),
48
- services: this.services,
49
- deps: this.deps
50
- });
51
- this.httpServer = result.httpServer;
52
- this.handler = result.handler;
53
- }
54
- async stop() {
55
- await stopServer({
56
- httpServer: this.httpServer,
57
- handler: this.handler,
58
- projectRoot: process.cwd(),
59
- deps: this.deps
60
- });
61
- }
62
-
63
- // Expose server interface for compatibility
64
- get server() {
65
- return buildServerInterface({
66
- handler: this.handler,
67
- httpServer: this.httpServer
68
- });
69
- }
70
-
71
- /**
72
- * Get TDD results (comparisons, screenshot count, etc.)
73
- * Only available in TDD mode after tests have run
74
- */
75
- async getTddResults() {
76
- return getTddResults({
77
- tddMode: this.tddMode,
78
- handler: this.handler
79
- });
80
- }
81
- }
@@ -1,100 +0,0 @@
1
- /**
2
- * Vizzly Screenshot Uploader
3
- * Handles screenshot uploads to the Vizzly platform
4
- *
5
- * This module is a thin wrapper around the functional operations in
6
- * src/uploader/. It maintains backwards compatibility while
7
- * delegating to pure functions for testability.
8
- */
9
-
10
- import { readFile, stat } from 'node:fs/promises';
11
- import { glob } from 'glob';
12
- import { checkShas, createApiClient, createBuild } from '../api/index.js';
13
- import { TimeoutError, UploadError, ValidationError } from '../errors/vizzly-error.js';
14
- import { resolveBatchSize, resolveTimeout } from '../uploader/index.js';
15
- import { upload as uploadOperation, waitForBuild as waitForBuildOperation } from '../uploader/operations.js';
16
- import { getDefaultBranch } from '../utils/git.js';
17
- import * as output from '../utils/output.js';
18
-
19
- /**
20
- * Create a new uploader instance
21
- */
22
- export function createUploader({
23
- apiKey,
24
- apiUrl,
25
- userAgent,
26
- command,
27
- upload: uploadConfig = {}
28
- } = {}, options = {}) {
29
- let signal = options.signal || new AbortController().signal;
30
- let client = createApiClient({
31
- baseUrl: apiUrl,
32
- token: apiKey,
33
- command: command || 'upload',
34
- sdkUserAgent: userAgent,
35
- allowNoToken: true
36
- });
37
-
38
- // Resolve tunable parameters
39
- let batchSize = resolveBatchSize(options, uploadConfig);
40
- let timeout = resolveTimeout(options, uploadConfig);
41
-
42
- // Dependency injection for testing
43
- let deps = options.deps || {
44
- client,
45
- createBuild,
46
- getDefaultBranch,
47
- glob,
48
- readFile,
49
- stat,
50
- checkShas,
51
- createError: (message, code, context) => {
52
- let error = new UploadError(message, context);
53
- error.code = code;
54
- return error;
55
- },
56
- createValidationError: (message, context) => new ValidationError(message, context),
57
- createUploadError: (message, context) => new UploadError(message, context),
58
- createTimeoutError: (message, context) => new TimeoutError(message, context),
59
- output
60
- };
61
-
62
- /**
63
- * Upload screenshots to Vizzly
64
- */
65
- async function upload(uploadOptions) {
66
- return uploadOperation({
67
- uploadOptions,
68
- config: {
69
- apiKey,
70
- apiUrl
71
- },
72
- signal,
73
- batchSize,
74
- deps: {
75
- ...deps,
76
- client: deps.client || client
77
- }
78
- });
79
- }
80
-
81
- /**
82
- * Wait for a build to complete
83
- */
84
- async function waitForBuild(buildId, waitTimeout = timeout) {
85
- return waitForBuildOperation({
86
- buildId,
87
- timeout: waitTimeout,
88
- signal,
89
- client: deps.client || client,
90
- deps: {
91
- createError: deps.createError,
92
- createTimeoutError: deps.createTimeoutError
93
- }
94
- });
95
- }
96
- return {
97
- upload,
98
- waitForBuild
99
- };
100
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * Region Service
3
- *
4
- * Functions for downloading and managing user-defined hotspot regions from the cloud.
5
- * Regions are 2D bounding boxes that users have confirmed as dynamic content areas.
6
- */
7
-
8
- import { saveRegionMetadata } from '../metadata/region-metadata.js';
9
-
10
- /**
11
- * Download user-defined regions from cloud API
12
- *
13
- * @param {Object} options
14
- * @param {Object} options.api - ApiService instance
15
- * @param {string} options.workingDir - Working directory
16
- * @param {string[]} options.screenshotNames - Names of screenshots to get regions for
17
- * @param {boolean} options.includeCandidates - Include candidate regions (default: false)
18
- * @returns {Promise<{ success: boolean, count: number, regionCount: number, error?: string }>}
19
- */
20
- export async function downloadRegions(options) {
21
- let {
22
- api,
23
- workingDir,
24
- screenshotNames,
25
- includeCandidates = false
26
- } = options;
27
- if (!screenshotNames || screenshotNames.length === 0) {
28
- return {
29
- success: true,
30
- count: 0,
31
- regionCount: 0
32
- };
33
- }
34
- try {
35
- let response = await api.getRegions(screenshotNames, {
36
- includeCandidates
37
- });
38
- if (!response || !response.regions) {
39
- return {
40
- success: false,
41
- error: 'API returned no region data'
42
- };
43
- }
44
-
45
- // Save regions to disk
46
- saveRegionMetadata(workingDir, response.regions, response.summary);
47
-
48
- // Calculate stats
49
- let count = Object.keys(response.regions).length;
50
- let regionCount = response.summary?.total_regions || 0;
51
- return {
52
- success: true,
53
- count,
54
- regionCount
55
- };
56
- } catch (error) {
57
- return {
58
- success: false,
59
- error: error.message
60
- };
61
- }
62
- }
63
-
64
- /**
65
- * Extract screenshot names from a list of screenshots
66
- *
67
- * @param {Array} screenshots - Screenshots with name property
68
- * @returns {string[]}
69
- */
70
- export function extractScreenshotNames(screenshots) {
71
- if (!screenshots || !Array.isArray(screenshots)) {
72
- return [];
73
- }
74
- return screenshots.map(s => s.name).filter(Boolean);
75
- }
@@ -1,103 +0,0 @@
1
- import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- /**
5
- * Archive a build to history directory
6
- * @param {string} workingDir - Working directory
7
- * @param {string} buildId - Build ID to archive
8
- * @param {Array} builds - Build data
9
- * @param {Array} comparisons - Comparison data
10
- * @param {Object} summary - Summary stats
11
- * @param {number} maxHistory - Maximum number of builds to keep (default: 3)
12
- */
13
- export function archiveBuild(workingDir, buildId, builds, comparisons, summary, maxHistory = 3) {
14
- const historyDir = join(workingDir, '.vizzly', 'history');
15
-
16
- // Create history directory if it doesn't exist
17
- if (!existsSync(historyDir)) {
18
- mkdirSync(historyDir, {
19
- recursive: true
20
- });
21
- }
22
-
23
- // Save current build to history
24
- const buildDir = join(historyDir, buildId);
25
- if (!existsSync(buildDir)) {
26
- mkdirSync(buildDir, {
27
- recursive: true
28
- });
29
- }
30
- const buildData = {
31
- buildId,
32
- timestamp: Date.now(),
33
- builds,
34
- comparisons,
35
- summary
36
- };
37
- writeFileSync(join(buildDir, 'report.json'), JSON.stringify(buildData, null, 2));
38
-
39
- // Clean up old builds (keep last N)
40
- cleanupOldBuilds(historyDir, maxHistory);
41
- }
42
-
43
- /**
44
- * Get list of archived builds
45
- * @param {string} workingDir - Working directory
46
- * @returns {Array} Array of build metadata
47
- */
48
- export function getArchivedBuilds(workingDir) {
49
- const historyDir = join(workingDir, '.vizzly', 'history');
50
- if (!existsSync(historyDir)) {
51
- return [];
52
- }
53
- try {
54
- const buildDirs = readdirSync(historyDir, {
55
- withFileTypes: true
56
- }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name).sort().reverse(); // Newest first
57
-
58
- return buildDirs.map(buildId => {
59
- const reportPath = join(historyDir, buildId, 'report.json');
60
- if (existsSync(reportPath)) {
61
- try {
62
- const data = JSON.parse(require('node:fs').readFileSync(reportPath, 'utf8'));
63
- return {
64
- buildId: data.buildId,
65
- timestamp: data.timestamp,
66
- summary: data.summary
67
- };
68
- } catch {
69
- return null;
70
- }
71
- }
72
- return null;
73
- }).filter(Boolean);
74
- } catch {
75
- return [];
76
- }
77
- }
78
-
79
- /**
80
- * Remove old builds, keeping only the last N
81
- * @private
82
- */
83
- function cleanupOldBuilds(historyDir, maxHistory) {
84
- try {
85
- const buildDirs = readdirSync(historyDir, {
86
- withFileTypes: true
87
- }).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name).sort().reverse(); // Newest first
88
-
89
- // Remove builds beyond maxHistory
90
- if (buildDirs.length > maxHistory) {
91
- const toRemove = buildDirs.slice(maxHistory);
92
- toRemove.forEach(buildId => {
93
- const buildDir = join(historyDir, buildId);
94
- rmSync(buildDir, {
95
- recursive: true,
96
- force: true
97
- });
98
- });
99
- }
100
- } catch {
101
- // Ignore cleanup errors
102
- }
103
- }