@vizzly-testing/cli 0.30.0 → 0.32.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 (79) hide show
  1. package/README.md +9 -9
  2. package/dist/api/client.js +6 -3
  3. package/dist/cli.js +58 -29
  4. package/dist/client/index.js +21 -29
  5. package/dist/commands/api.js +145 -90
  6. package/dist/commands/baselines.js +78 -47
  7. package/dist/commands/builds.js +9 -8
  8. package/dist/commands/comparisons.js +7 -8
  9. package/dist/commands/config-cmd.js +15 -7
  10. package/dist/commands/context.js +329 -18
  11. package/dist/commands/doctor.js +116 -81
  12. package/dist/commands/init.js +202 -201
  13. package/dist/commands/login.js +134 -65
  14. package/dist/commands/logout.js +17 -6
  15. package/dist/commands/preview.js +29 -57
  16. package/dist/commands/project.js +120 -0
  17. package/dist/commands/projects.js +9 -2
  18. package/dist/commands/review.js +236 -202
  19. package/dist/commands/run.js +75 -72
  20. package/dist/commands/status.js +228 -150
  21. package/dist/commands/tdd-daemon-process.js +98 -0
  22. package/dist/commands/tdd-daemon.js +318 -212
  23. package/dist/commands/tdd.js +23 -8
  24. package/dist/commands/upload.js +10 -9
  25. package/dist/commands/whoami.js +63 -25
  26. package/dist/config/core.js +36 -35
  27. package/dist/context/local-workspace-provider.js +132 -12
  28. package/dist/context/provider-resolver.js +5 -1
  29. package/dist/index.js +5 -4
  30. package/dist/plugin-loader.js +55 -49
  31. package/dist/project/core.js +2 -2
  32. package/dist/project/operations.js +6 -3
  33. package/dist/reporter/reporter-bundle.css +1 -1
  34. package/dist/reporter/reporter-bundle.iife.js +7 -7
  35. package/dist/sdk/index.js +162 -107
  36. package/dist/server/handlers/tdd-handler.js +30 -54
  37. package/dist/server/routers/baseline.js +195 -178
  38. package/dist/server/routers/events.js +212 -174
  39. package/dist/server/routers/projects.js +14 -5
  40. package/dist/server/routers/screenshot.js +1 -31
  41. package/dist/services/build-manager.js +19 -97
  42. package/dist/services/config-service.js +26 -34
  43. package/dist/services/index.js +2 -4
  44. package/dist/services/static-report-generator.js +11 -12
  45. package/dist/services/test-runner.js +21 -27
  46. package/dist/tdd/core/region-coverage.js +109 -21
  47. package/dist/tdd/index.js +1 -1
  48. package/dist/tdd/metadata/region-metadata.js +0 -37
  49. package/dist/tdd/runtime-context.js +78 -0
  50. package/dist/tdd/server-registry.js +38 -17
  51. package/dist/tdd/services/baseline-manager.js +23 -0
  52. package/dist/tdd/services/comparison-service.js +10 -1
  53. package/dist/tdd/services/hotspot-service.js +0 -13
  54. package/dist/tdd/tdd-service.js +144 -138
  55. package/dist/types/client.d.ts +36 -1
  56. package/dist/types/index.d.ts +114 -22
  57. package/dist/types/sdk.d.ts +40 -0
  58. package/dist/uploader/core.js +5 -4
  59. package/dist/uploader/index.js +79 -2
  60. package/dist/uploader/operations.js +40 -5
  61. package/dist/utils/api-url.js +14 -0
  62. package/dist/utils/async-utils.js +26 -0
  63. package/dist/utils/ci-env.js +19 -12
  64. package/dist/utils/config-loader.js +39 -65
  65. package/dist/utils/config-schema.js +19 -55
  66. package/dist/utils/fetch-utils.js +12 -5
  67. package/dist/utils/git.js +35 -72
  68. package/dist/utils/global-config.js +20 -1
  69. package/dist/utils/output.js +19 -7
  70. package/dist/utils/patterns.js +14 -0
  71. package/dist/utils/project-link-store.js +105 -0
  72. package/dist/utils/screenshot-options.js +28 -0
  73. package/dist/utils/secret-store.js +65 -0
  74. package/package.json +14 -11
  75. package/dist/services/screenshot-server.js +0 -65
  76. package/dist/services/server-manager.js +0 -81
  77. package/dist/services/uploader.js +0 -100
  78. package/dist/tdd/services/region-service.js +0 -75
  79. package/dist/utils/build-history.js +0 -103
@@ -0,0 +1,105 @@
1
+ import { loadGlobalConfig, saveGlobalConfig } from './global-config.js';
2
+ import { deleteSecret as defaultDeleteSecret, getSecret as defaultGetSecret, saveSecret as defaultSaveSecret } from './secret-store.js';
3
+ export function buildProjectLinkAccount({
4
+ apiUrl,
5
+ organizationSlug,
6
+ projectSlug
7
+ }) {
8
+ return `${apiUrl || 'https://app.vizzly.dev'}|${organizationSlug}/${projectSlug}`;
9
+ }
10
+ function getProjectLinkConfig(config) {
11
+ return config.projectLink || {
12
+ active: null,
13
+ links: {}
14
+ };
15
+ }
16
+ export async function saveProjectLink(link, deps = {}) {
17
+ let {
18
+ loadConfig = loadGlobalConfig,
19
+ saveConfig = saveGlobalConfig,
20
+ saveSecret = defaultSaveSecret,
21
+ now = () => new Date()
22
+ } = deps;
23
+ let config = await loadConfig();
24
+ let projectLink = getProjectLinkConfig(config);
25
+ let account = buildProjectLinkAccount(link);
26
+ let storedInKeychain = await saveSecret(account, link.token);
27
+ projectLink.active = account;
28
+ projectLink.links = {
29
+ ...projectLink.links,
30
+ [account]: {
31
+ apiUrl: link.apiUrl,
32
+ organizationSlug: link.organizationSlug,
33
+ organizationName: link.organizationName,
34
+ projectSlug: link.projectSlug,
35
+ projectName: link.projectName,
36
+ tokenId: link.tokenId,
37
+ tokenPrefix: link.tokenPrefix,
38
+ expiresAt: link.expiresAt || null,
39
+ createdAt: link.createdAt || now().toISOString(),
40
+ storage: storedInKeychain ? 'keychain' : 'file',
41
+ token: storedInKeychain ? undefined : link.token
42
+ }
43
+ };
44
+ config.projectLink = projectLink;
45
+ await saveConfig(config);
46
+ return {
47
+ ...projectLink.links[account],
48
+ account,
49
+ token: link.token
50
+ };
51
+ }
52
+ export async function getActiveProjectLink(options = {}, deps = {}) {
53
+ let {
54
+ loadConfig = loadGlobalConfig,
55
+ getSecret = defaultGetSecret
56
+ } = deps;
57
+ let config = await loadConfig();
58
+ let projectLink = getProjectLinkConfig(config);
59
+ let account = options.account || projectLink.active;
60
+ let link = account ? projectLink.links?.[account] : null;
61
+ if (!link && options.apiUrl && options.organizationSlug && options.projectSlug) {
62
+ account = buildProjectLinkAccount(options);
63
+ link = projectLink.links?.[account] || null;
64
+ }
65
+ if (!link) {
66
+ return null;
67
+ }
68
+ if (options.apiUrl && link.apiUrl && link.apiUrl !== options.apiUrl) {
69
+ return null;
70
+ }
71
+ let token = link.storage === 'keychain' ? await getSecret(account) : link.token;
72
+ if (!token) {
73
+ return null;
74
+ }
75
+ return {
76
+ ...link,
77
+ account,
78
+ token
79
+ };
80
+ }
81
+ export async function clearActiveProjectLink(deps = {}) {
82
+ let {
83
+ loadConfig = loadGlobalConfig,
84
+ saveConfig = saveGlobalConfig,
85
+ deleteSecret = defaultDeleteSecret
86
+ } = deps;
87
+ let config = await loadConfig();
88
+ let projectLink = getProjectLinkConfig(config);
89
+ let account = projectLink.active;
90
+ if (!account || !projectLink.links?.[account]) {
91
+ return null;
92
+ }
93
+ let link = projectLink.links[account];
94
+ if (link.storage === 'keychain') {
95
+ await deleteSecret(account);
96
+ }
97
+ delete projectLink.links[account];
98
+ projectLink.active = null;
99
+ config.projectLink = projectLink;
100
+ await saveConfig(config);
101
+ return {
102
+ ...link,
103
+ account
104
+ };
105
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Normalize screenshot SDK options into the properties payload consumed by the
3
+ * local TDD server and cloud-compatible comparison path.
4
+ */
5
+ export function createScreenshotProperties(options = {}) {
6
+ let {
7
+ buildId: _buildId,
8
+ properties = {},
9
+ threshold,
10
+ minClusterSize,
11
+ fullPage,
12
+ ...topLevelProperties
13
+ } = options;
14
+ let normalizedProperties = {
15
+ ...topLevelProperties,
16
+ ...properties
17
+ };
18
+ if (threshold !== undefined) {
19
+ normalizedProperties.threshold = threshold;
20
+ }
21
+ if (minClusterSize !== undefined) {
22
+ normalizedProperties.minClusterSize = minClusterSize;
23
+ }
24
+ if (fullPage !== undefined) {
25
+ normalizedProperties.fullPage = fullPage;
26
+ }
27
+ return normalizedProperties;
28
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.30.0",
4
- "description": "Visual review platform for UI developers and designers",
3
+ "version": "0.32.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": {
@@ -87,13 +90,15 @@
87
90
  "registry": "https://registry.npmjs.org/"
88
91
  },
89
92
  "dependencies": {
90
- "@vizzly-testing/honeydiff": "^0.10.0",
93
+ "@vizzly-testing/honeydiff": "^0.10.3",
91
94
  "ansis": "^4.2.0",
92
95
  "commander": "^14.0.0",
93
96
  "cosmiconfig": "^9.0.0",
94
97
  "dotenv": "^17.2.1",
95
98
  "form-data": "^4.0.0",
96
99
  "glob": "^13.0.0",
100
+ "react": "^19.1.1",
101
+ "react-dom": "^19.1.1",
97
102
  "zod": "^4.1.12"
98
103
  },
99
104
  "tsd": {
@@ -125,8 +130,6 @@
125
130
  "babel-plugin-react-compiler": "^1.0.0",
126
131
  "babel-plugin-transform-remove-console": "^6.9.4",
127
132
  "postcss": "^8.5.6",
128
- "react": "^19.1.1",
129
- "react-dom": "^19.1.1",
130
133
  "rimraf": "^6.0.1",
131
134
  "tailwindcss": "^4.1.13",
132
135
  "tsd": "^0.33.0",
@@ -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?.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
- }