@vizzly-testing/cli 0.39.0 → 0.39.2

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 (195) hide show
  1. package/README.md +12 -3
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +50 -35
  4. package/dist/adapters/cli/setup.js +34 -0
  5. package/dist/{client → adapters/client}/index.js +6 -7
  6. package/dist/adapters/http/cloud-proxy.js +29 -0
  7. package/dist/{server → adapters/http}/http-server.js +12 -13
  8. package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
  9. package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
  10. package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
  11. package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
  12. package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
  13. package/dist/adapters/sdk/index.d.ts +89 -0
  14. package/dist/{sdk → adapters/sdk}/index.js +22 -19
  15. package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
  16. package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
  17. package/dist/domains/auth/auth-errors.js +50 -0
  18. package/dist/domains/auth/auth-headers.js +41 -0
  19. package/dist/domains/auth/auth.normalizers.js +50 -0
  20. package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
  21. package/dist/domains/auth/auth.rules.js +19 -0
  22. package/dist/domains/auth/auth.serializers.js +32 -0
  23. package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
  24. package/dist/domains/auth/auth.validations.js +21 -0
  25. package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
  26. package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
  27. package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
  28. package/dist/domains/auth/token-store.js +81 -0
  29. package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
  30. package/dist/domains/baselines/baseline-download.js +192 -0
  31. package/dist/domains/baselines/baseline-write.js +22 -0
  32. package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
  33. package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
  34. package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
  35. package/dist/domains/baselines/cloud-baselines.js +9 -0
  36. package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
  37. package/dist/domains/builds/build.normalizers.js +35 -0
  38. package/dist/domains/builds/build.serializers.js +164 -0
  39. package/dist/domains/builds/build.service.js +119 -0
  40. package/dist/domains/builds/build.validations.js +39 -0
  41. package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
  42. package/dist/domains/builds/cloud-builds.routes.js +31 -0
  43. package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
  44. package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
  45. package/dist/domains/comparisons/cloud-comparisons.js +43 -0
  46. package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
  47. package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
  48. package/dist/domains/config/config-defaults.js +27 -0
  49. package/dist/{utils → domains/config}/config-discovery.js +1 -9
  50. package/dist/{utils → domains/config}/config-loader.js +10 -9
  51. package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
  52. package/dist/{types → domains/config}/config.d.ts +3 -3
  53. package/dist/domains/config/config.normalizers.js +126 -0
  54. package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
  55. package/dist/domains/config/config.rules.js +47 -0
  56. package/dist/domains/config/config.serializers.js +202 -0
  57. package/dist/domains/config/config.service.js +625 -0
  58. package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
  59. package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
  60. package/dist/domains/context/compact-context.validations.js +60 -0
  61. package/dist/domains/context/context-presentation.js +368 -0
  62. package/dist/domains/context/context.commands.js +385 -0
  63. package/dist/domains/context/context.serializers.js +157 -0
  64. package/dist/domains/context/context.service.js +76 -0
  65. package/dist/domains/context/context.validations.js +100 -0
  66. package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
  67. package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
  68. package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
  69. package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
  70. package/dist/domains/previews/preview.service.js +37 -0
  71. package/dist/domains/projects/cloud-projects.routes.js +28 -0
  72. package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
  73. package/dist/{utils → domains/projects}/project-binding.js +3 -3
  74. package/dist/domains/projects/project-errors.js +61 -0
  75. package/dist/{utils → domains/projects}/project-link-store.js +2 -2
  76. package/dist/domains/projects/project-requests.js +61 -0
  77. package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
  78. package/dist/domains/projects/project.normalizers.js +69 -0
  79. package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
  80. package/dist/domains/projects/project.validations.js +31 -0
  81. package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
  82. package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
  83. package/dist/domains/reports/dashboard.routes.js +96 -0
  84. package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
  85. package/dist/domains/reports/report-assets.routes.js +81 -0
  86. package/dist/domains/reports/report-state.js +51 -0
  87. package/dist/domains/reports/report-store.js +85 -0
  88. package/dist/domains/reports/report.serializers.js +99 -0
  89. package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
  90. package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
  91. package/dist/domains/runs/run-environment.js +69 -0
  92. package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
  93. package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
  94. package/dist/domains/runs/run.normalizers.js +8 -0
  95. package/dist/domains/runs/run.rules.js +56 -0
  96. package/dist/domains/runs/run.serializers.js +101 -0
  97. package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
  98. package/dist/domains/runs/run.validations.js +17 -0
  99. package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
  100. package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
  101. package/dist/domains/screenshots/cloud-screenshots.js +109 -0
  102. package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
  103. package/dist/domains/screenshots/screenshot-hash.js +53 -0
  104. package/dist/domains/screenshots/screenshot-server-options.js +28 -0
  105. package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
  106. package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
  107. package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
  108. package/dist/domains/screenshots/screenshot.serializers.js +146 -0
  109. package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
  110. package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
  112. package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
  113. package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
  114. package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
  115. package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
  116. package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
  117. package/dist/domains/updates/cli-update.js +103 -0
  118. package/dist/domains/updates/update-check.js +2 -0
  119. package/dist/domains/updates/update-notifications.js +175 -0
  120. package/dist/domains/uploads/upload-defaults.js +4 -0
  121. package/dist/domains/uploads/upload-files.js +32 -0
  122. package/dist/domains/uploads/upload-settings.js +21 -0
  123. package/dist/domains/uploads/upload-timing.js +18 -0
  124. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  125. package/dist/domains/uploads/upload.normalizers.js +60 -0
  126. package/dist/domains/uploads/upload.serializers.js +179 -0
  127. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  128. package/dist/domains/uploads/upload.validations.js +77 -0
  129. package/dist/index.js +12 -12
  130. package/dist/reporter/reporter-bundle.iife.js +86 -86
  131. package/dist/reporter-ssr/ssr-entry.js +3 -6
  132. package/dist/shared/cloud-protocol.js +171 -0
  133. package/dist/{utils → shared}/file-changes.js +1 -1
  134. package/dist/shared/files.js +9 -0
  135. package/dist/{utils → shared}/global-config.js +7 -78
  136. package/dist/shared/log-level.js +7 -0
  137. package/dist/{utils → shared}/output.js +1 -1
  138. package/dist/{utils → shared}/package-info.js +5 -4
  139. package/dist/shared/paths.js +57 -0
  140. package/package.json +37 -36
  141. package/dist/api/core.js +0 -382
  142. package/dist/api/endpoints.js +0 -421
  143. package/dist/api/index.js +0 -19
  144. package/dist/auth/core.js +0 -237
  145. package/dist/auth/index.js +0 -31
  146. package/dist/commands/context.js +0 -1066
  147. package/dist/config/core.js +0 -439
  148. package/dist/config/operations.js +0 -327
  149. package/dist/project/core.js +0 -211
  150. package/dist/screenshot-server/core.js +0 -161
  151. package/dist/screenshot-server/index.js +0 -11
  152. package/dist/server/routers/assets.js +0 -95
  153. package/dist/server/routers/cloud-proxy.js +0 -146
  154. package/dist/server/routers/dashboard.js +0 -172
  155. package/dist/services/auth-service.js +0 -117
  156. package/dist/services/config-service.js +0 -313
  157. package/dist/services/project-service.js +0 -88
  158. package/dist/test-runner/core.js +0 -293
  159. package/dist/test-runner/index.js +0 -13
  160. package/dist/types/sdk.d.ts +0 -186
  161. package/dist/uploader/core.js +0 -432
  162. package/dist/uploader/index.js +0 -88
  163. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  164. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  166. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  167. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  168. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  169. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  170. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  171. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  172. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  173. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  174. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  175. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  176. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  177. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  178. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  179. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  180. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  181. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  182. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  183. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  184. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  185. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  186. /package/dist/{utils → shared}/api-url.js +0 -0
  187. /package/dist/{utils → shared}/async-utils.js +0 -0
  188. /package/dist/{utils → shared}/browser.js +0 -0
  189. /package/dist/{utils → shared}/ci-env.js +0 -0
  190. /package/dist/{utils → shared}/colors.js +0 -0
  191. /package/dist/{utils → shared}/fetch-utils.js +0 -0
  192. /package/dist/{utils → shared}/git.js +0 -0
  193. /package/dist/{utils → shared}/patterns.js +0 -0
  194. /package/dist/{utils → shared}/secret-store.js +0 -0
  195. /package/dist/{errors → shared}/vizzly-error.js +0 -0
@@ -1,24 +1,11 @@
1
- import { spawn } from 'node:child_process';
2
1
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { planAgentSetup } from '../init/agents.js';
5
- import { planConfig } from '../init/core.js';
6
- import { createPrompts } from '../init/prompts.js';
7
- import { discoverConfig } from '../utils/config-discovery.js';
8
- import { applyChanges } from '../utils/file-changes.js';
9
- import * as output from '../utils/output.js';
10
- import { projectLinkCommand, validateProjectLinkOptions } from './project.js';
11
- function runProcess(command, args) {
12
- return new Promise((resolve, reject) => {
13
- let child = spawn(command, args, {
14
- stdio: 'inherit'
15
- });
16
- child.once('error', reject);
17
- child.once('close', (code, signal) => {
18
- if (code === 0) resolve();else reject(new Error(`${command} did not finish successfully (${signal || code}).`));
19
- });
20
- });
21
- }
2
+ import { applyChanges } from '../../shared/file-changes.js';
3
+ import * as output from '../../shared/output.js';
4
+ import { discoverConfig } from '../config/config-discovery.js';
5
+ import { planConfig } from '../config/config-plan.js';
6
+ import { validateProjectLinkOptions } from '../projects/project.validations.js';
7
+ import { planAgentSetup } from './agent-setup.js';
8
+ import { createPrompts } from './setup-prompts.js';
22
9
  async function guidedPlan(cwd, config, prompts) {
23
10
  output.hint(`Project: ${cwd}`);
24
11
  let guidance = await prompts.confirm('Add the Vizzly skill and AGENTS.md instructions?');
@@ -57,26 +44,6 @@ function agentOptions(options) {
57
44
  skill: guidance || !options.skipAgentSkill && Boolean(options.agentSkill)
58
45
  };
59
46
  }
60
- async function connectProject(project, options) {
61
- let exitCode = 0;
62
- let link = () => projectLinkCommand(project, {}, options, {
63
- exit: code => {
64
- exitCode = code;
65
- }
66
- });
67
- let result = await link();
68
- if (result?.reason === 'auth_required') {
69
- let cli = fileURLToPath(new URL('../cli.js', import.meta.url));
70
- await runProcess(process.execPath, [cli, 'login']);
71
- result = await link();
72
- }
73
- let status = result?.linked ? 'linked' : 'failed';
74
- if (!result?.linked && exitCode === 1) status = 'auth_required';
75
- return {
76
- status,
77
- project
78
- };
79
- }
80
47
  function showResult(result, json) {
81
48
  if (json) {
82
49
  output.data(result);
@@ -94,7 +61,9 @@ function showResult(result, json) {
94
61
  output.hint('Use a Vizzly client to add screenshot capture.');
95
62
  }
96
63
  }
97
- export async function init(options = {}) {
64
+ export async function init(options = {}, {
65
+ connectProject
66
+ } = {}) {
98
67
  output.configure({
99
68
  json: options.json || false,
100
69
  verbose: options.verbose,
@@ -119,9 +88,7 @@ export async function init(options = {}) {
119
88
  let prompts;
120
89
  try {
121
90
  if (initialized) {
122
- plan = {
123
- changes: []
124
- };
91
+ plan = await planAgentSetup(cwd, agentOptions(options));
125
92
  } else if (quick) {
126
93
  let configChange = planConfig(config);
127
94
  let agents = await planAgentSetup(cwd, agentOptions(options));
@@ -2,119 +2,15 @@ import { Buffer as defaultBuffer } from 'node:buffer';
2
2
  import { existsSync as defaultExistsSync, readFileSync as defaultReadFileSync, unlinkSync as defaultUnlinkSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
3
3
  import { join as defaultJoin, resolve as defaultResolve } from 'node:path';
4
4
  import { getDimensionsSync as defaultGetDimensionsSync } from '@vizzly-testing/honeydiff';
5
- import { TddService as DefaultTddService } from '../../tdd/tdd-service.js';
6
- import { detectImageInputType as defaultDetectImageInputType } from '../../utils/image-input-detector.js';
7
- import * as defaultOutput from '../../utils/output.js';
8
- import { safePath as defaultSafePath, sanitizeScreenshotName as defaultSanitizeScreenshotName, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../../utils/security.js';
9
-
10
- /**
11
- * Build the flat internal comparison shape. Width and height always come from
12
- * the captured image. The untouched user bag remains available in metadata.
13
- */
14
- export const extractProperties = (validatedProperties, imageDimensions = {}) => {
15
- if (!validatedProperties) return {};
16
- return {
17
- ...validatedProperties,
18
- viewport_width: imageDimensions.width ?? null,
19
- viewport_height: imageDimensions.height ?? null,
20
- browser: validatedProperties.browser ?? null,
21
- metadata: validatedProperties
22
- };
23
- };
24
-
25
- /**
26
- * Convert absolute file paths to web-accessible URLs
27
- */
28
- export const convertPathToUrl = (filePath, vizzlyDir) => {
29
- if (!filePath) return null;
30
- // Convert absolute path to relative path from .vizzly directory
31
- if (filePath.startsWith(vizzlyDir)) {
32
- let relativePath = filePath.substring(vizzlyDir.length + 1);
33
- return `/images/${relativePath}`;
34
- }
35
- return filePath;
36
- };
37
-
38
- /**
39
- * Group comparisons by screenshot name with variant structure
40
- * Matches cloud product's grouping logic from comparison.js
41
- */
42
- export const groupComparisons = comparisons => {
43
- const groups = new Map();
44
-
45
- // Group by screenshot name
46
- for (const comp of comparisons) {
47
- if (!groups.has(comp.name)) {
48
- groups.set(comp.name, {
49
- name: comp.name,
50
- comparisons: [],
51
- browsers: new Set(),
52
- viewports: new Set(),
53
- devices: new Set(),
54
- totalVariants: 0
55
- });
56
- }
57
- const group = groups.get(comp.name);
58
- group.comparisons.push(comp);
59
- group.totalVariants++;
60
-
61
- // Track unique browsers, viewports, devices
62
- if (comp.properties?.browser) {
63
- group.browsers.add(comp.properties.browser);
64
- }
65
- if (comp.properties?.viewport_width && comp.properties?.viewport_height) {
66
- group.viewports.add(`${comp.properties.viewport_width}x${comp.properties.viewport_height}`);
67
- }
68
- if (comp.properties?.device) {
69
- group.devices.add(comp.properties.device);
70
- }
71
- }
72
-
73
- // Convert to final structure
74
- return Array.from(groups.values()).map(group => {
75
- const browsers = Array.from(group.browsers);
76
- const viewports = Array.from(group.viewports);
77
- const devices = Array.from(group.devices);
78
-
79
- // Build variants structure (browser -> viewport -> comparisons)
80
- const variants = {};
81
- group.comparisons.forEach(comp => {
82
- const browser = comp.properties?.browser || null;
83
- const viewport = comp.properties?.viewport_width && comp.properties?.viewport_height ? `${comp.properties.viewport_width}x${comp.properties.viewport_height}` : null;
84
- if (!variants[browser]) variants[browser] = {};
85
- if (!variants[browser][viewport]) variants[browser][viewport] = [];
86
- variants[browser][viewport].push(comp);
87
- });
88
-
89
- // Determine grouping strategy
90
- let groupingStrategy = 'flat';
91
- if (browsers.length > 1) groupingStrategy = 'browser';else if (viewports.length > 1) groupingStrategy = 'viewport';
92
-
93
- // Sort comparisons by viewport area (largest first)
94
- group.comparisons.sort((a, b) => {
95
- const aArea = (a.properties?.viewport_width || 0) * (a.properties?.viewport_height || 0);
96
- const bArea = (b.properties?.viewport_width || 0) * (b.properties?.viewport_height || 0);
97
- if (bArea !== aArea) return bArea - aArea;
98
- return (b.properties?.viewport_width || 0) - (a.properties?.viewport_width || 0);
99
- });
100
- return {
101
- ...group,
102
- browsers,
103
- viewports,
104
- devices: Array.from(devices),
105
- variants,
106
- groupingStrategy
107
- };
108
- }).sort((a, b) => {
109
- // Sort groups: multi-variant first (by variant count), then singles alphabetically
110
- if (a.totalVariants > 1 && b.totalVariants === 1) return -1;
111
- if (a.totalVariants === 1 && b.totalVariants > 1) return 1;
112
- if (a.totalVariants > 1 && b.totalVariants > 1) {
113
- return b.totalVariants - a.totalVariants;
114
- }
115
- return a.name.localeCompare(b.name);
116
- });
117
- };
5
+ import * as defaultOutput from '../../shared/output.js';
6
+ import { safePath as defaultSafePath } from '../../shared/paths.js';
7
+ import { convertPathToUrl } from '../reports/report.serializers.js';
8
+ import { createEmptyReport, removeReportComparison, upsertReportComparison } from '../reports/report-state.js';
9
+ import { createReportStore } from '../reports/report-store.js';
10
+ import { detectImageInputType as defaultDetectImageInputType } from '../screenshots/image-input.js';
11
+ import { extractProperties } from '../screenshots/screenshot.normalizers.js';
12
+ import { sanitizeScreenshotName as defaultSanitizeScreenshotName, validateScreenshotProperties as defaultValidateScreenshotProperties } from '../screenshots/screenshot.validations.js';
13
+ import { TddService as DefaultTddService } from './tdd.service.js';
118
14
  export const createTddHandler = (config, workingDir, baselineBuild, baselineComparison, setBaseline = false, deps = {}) => {
119
15
  // Inject dependencies with defaults
120
16
  let {
@@ -133,133 +29,45 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
133
29
  validateScreenshotProperties = defaultValidateScreenshotProperties,
134
30
  output = defaultOutput
135
31
  } = deps;
136
- const tddService = new TddService(config, workingDir, setBaseline);
137
- const reportPath = join(workingDir, '.vizzly', 'report-data.json');
138
- const detailsPath = join(workingDir, '.vizzly', 'comparison-details.json');
32
+ let tddService = new TddService(config, workingDir, setBaseline);
33
+ let reportStore = createReportStore(workingDir, {
34
+ existsSync,
35
+ readFileSync,
36
+ writeFileSync,
37
+ unlinkSync,
38
+ join,
39
+ output
40
+ });
139
41
  let capture = {
140
42
  accepted: 0,
141
43
  rejected: 0
142
44
  };
143
- let clearJsonFile = path => {
144
- if (existsSync(path)) {
145
- unlinkSync(path);
146
- }
147
- };
45
+ let {
46
+ updateDetails: updateComparisonDetails,
47
+ removeDetails: removeComparisonDetails
48
+ } = reportStore;
148
49
  let prepareRunArtifacts = () => {
149
50
  capture = {
150
51
  accepted: 0,
151
52
  rejected: 0
152
53
  };
153
- clearJsonFile(reportPath);
154
- clearJsonFile(detailsPath);
54
+ reportStore.clear();
155
55
  tddService.clearRunData?.();
156
56
  };
157
-
158
- /**
159
- * Read heavy comparison details from comparison-details.json
160
- * Returns a map of comparison ID -> heavy fields
161
- */
162
- const readComparisonDetails = () => {
163
- try {
164
- if (!existsSync(detailsPath)) return {};
165
- return JSON.parse(readFileSync(detailsPath, 'utf8'));
166
- } catch (error) {
167
- output.debug('Failed to read comparison details:', error);
168
- return {};
169
- }
170
- };
171
-
172
- /**
173
- * Persist heavy fields for a comparison to comparison-details.json
174
- * This file is NOT watched by SSE, so writes here don't trigger broadcasts
175
- * Skips writing if all heavy fields are empty (passed comparisons)
176
- */
177
- const updateComparisonDetails = (id, heavyFields) => {
178
- let hasData = Object.values(heavyFields).some(v => v != null && (!Array.isArray(v) || v.length > 0));
179
- if (!hasData) return;
180
- let details = readComparisonDetails();
181
- details[id] = heavyFields;
182
- writeFileSync(detailsPath, JSON.stringify(details));
183
- };
184
-
185
- /**
186
- * Remove a comparison's heavy fields from comparison-details.json
187
- */
188
- const removeComparisonDetails = id => {
189
- let details = readComparisonDetails();
190
- delete details[id];
191
- writeFileSync(detailsPath, JSON.stringify(details));
192
- };
193
- const readReportData = () => {
57
+ let readReportData = () => {
194
58
  try {
195
- if (!existsSync(reportPath)) {
196
- return {
197
- timestamp: Date.now(),
198
- comparisons: [],
199
- summary: {
200
- total: 0,
201
- passed: 0,
202
- failed: 0,
203
- errors: 0
204
- }
205
- };
206
- }
207
- const data = readFileSync(reportPath, 'utf8');
208
- return JSON.parse(data);
59
+ return reportStore.readReport() || createEmptyReport(Date.now());
209
60
  } catch (error) {
210
61
  output.error('Failed to read report data:', error);
211
- return {
212
- timestamp: Date.now(),
213
- comparisons: [],
214
- summary: {
215
- total: 0,
216
- passed: 0,
217
- failed: 0,
218
- errors: 0
219
- }
220
- };
62
+ return createEmptyReport(Date.now());
221
63
  }
222
64
  };
223
- const updateComparison = newComparison => {
65
+ let updateComparison = comparison => {
224
66
  try {
225
- const reportData = readReportData();
226
-
227
- // Ensure comparisons array exists (backward compatibility)
228
- if (!reportData.comparisons) {
229
- reportData.comparisons = [];
230
- }
231
-
232
- // Find existing comparison by unique ID
233
- // This ensures we update the correct variant even with same name
234
- const existingIndex = reportData.comparisons.findIndex(c => c.id === newComparison.id);
235
- if (existingIndex >= 0) {
236
- // Preserve initialStatus from the original comparison
237
- // This keeps sort order stable when status changes (e.g., after approval)
238
- const initialStatus = reportData.comparisons[existingIndex].initialStatus;
239
- reportData.comparisons[existingIndex] = {
240
- ...newComparison,
241
- initialStatus: initialStatus || newComparison.status
242
- };
243
- } else {
244
- // New comparison - set initialStatus to current status
245
- reportData.comparisons.push({
246
- ...newComparison,
247
- initialStatus: newComparison.status
248
- });
249
- }
250
-
251
- // Update summary (groups computed client-side from comparisons)
252
- reportData.timestamp = Date.now();
253
- reportData.summary = {
254
- total: reportData.comparisons.length,
255
- passed: reportData.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'new').length,
256
- failed: reportData.comparisons.filter(c => c.status === 'failed').length,
257
- rejected: reportData.comparisons.filter(c => c.status === 'rejected').length,
258
- errors: reportData.comparisons.filter(c => c.status === 'error').length
259
- };
260
- writeFileSync(reportPath, JSON.stringify(reportData));
67
+ reportStore.writeReport(upsertReportComparison(readReportData(), comparison, Date.now()));
261
68
  } catch (error) {
262
69
  output.error('Failed to update comparison:', error);
70
+ throw error;
263
71
  }
264
72
  };
265
73
  const initialize = async () => {
@@ -281,13 +89,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
281
89
  }
282
90
  const baseline = await tddService.loadBaseline();
283
91
  if (!baseline) {
284
- // Only download baselines if explicitly requested via baseline flags
285
- if ((baselineBuild || baselineComparison) && config.apiKey) {
286
- output.debug('tdd', 'downloading baselines');
287
- await tddService.downloadBaselines(config.build?.environment || 'test', config.build?.branch || null, baselineBuild, baselineComparison);
288
- } else {
289
- output.debug('tdd', 'no baselines yet');
290
- }
92
+ output.debug('tdd', 'no baselines yet');
291
93
  } else {
292
94
  output.debug('tdd', `baseline: ${baseline.buildName}`);
293
95
  }
@@ -637,27 +439,16 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
637
439
  let deletedBaselines = 0;
638
440
  let deletedCurrents = 0;
639
441
  let deletedDiffs = 0;
640
- let deleteResetFile = (imagePath, label, name) => {
641
- if (!imagePath) return 0;
642
- try {
643
- let filePath = safePath(workingDir, '.vizzly', imagePath.replace('/images/', ''));
644
- if (!existsSync(filePath)) return 0;
645
- unlinkSync(filePath);
646
- if (label === 'diff') {
647
- output.debug(`Deleted diff for ${name}`);
648
- }
649
- return 1;
650
- } catch (error) {
651
- output.warn(`Failed to delete ${label} for ${name}: ${error.message}`);
652
- return 0;
653
- }
654
- };
655
442
 
656
443
  // Delete all baseline, current, and diff images
657
444
  for (let comparison of reportData.comparisons) {
658
- deletedBaselines += deleteResetFile(comparison.baseline, 'baseline', comparison.name);
659
- deletedCurrents += deleteResetFile(comparison.current, 'current screenshot', comparison.name);
660
- deletedDiffs += deleteResetFile(comparison.diff, 'diff', comparison.name);
445
+ deletedBaselines += safeDeleteFile(comparison.baseline, 'baseline', comparison.name, {
446
+ logDeletion: false
447
+ });
448
+ deletedCurrents += safeDeleteFile(comparison.current, 'current screenshot', comparison.name, {
449
+ logDeletion: false
450
+ });
451
+ deletedDiffs += safeDeleteFile(comparison.diff, 'diff', comparison.name);
661
452
  }
662
453
 
663
454
  // Delete baseline metadata
@@ -670,24 +461,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
670
461
  output.warn(`Failed to delete baseline metadata: ${error.message}`);
671
462
  }
672
463
  }
673
-
674
- // Clear the report data entirely - fresh start
675
- const freshReportData = {
676
- timestamp: Date.now(),
677
- comparisons: [],
678
- summary: {
679
- total: 0,
680
- passed: 0,
681
- failed: 0,
682
- errors: 0
683
- }
684
- };
685
- writeFileSync(reportPath, JSON.stringify(freshReportData));
686
-
687
- // Clear comparison details
688
- if (existsSync(detailsPath)) {
689
- writeFileSync(detailsPath, JSON.stringify({}));
690
- }
464
+ reportStore.reset();
691
465
  output.info(`Baselines reset - ${deletedBaselines} baselines deleted, ${deletedCurrents} current screenshots deleted, ${deletedDiffs} diffs deleted`);
692
466
  return {
693
467
  success: true,
@@ -707,19 +481,23 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
707
481
  * @param {string} label - Label for logging (e.g., "baseline", "current", "diff")
708
482
  * @param {string} name - Screenshot name for logging
709
483
  */
710
- const safeDeleteFile = (imagePath, label, name) => {
711
- if (!imagePath) return;
484
+ let safeDeleteFile = (imagePath, label, name, {
485
+ logDeletion = true
486
+ } = {}) => {
487
+ if (!imagePath) return 0;
712
488
  try {
713
489
  // Use safePath to validate the path stays within workingDir
714
490
  const filePath = safePath(workingDir, '.vizzly', imagePath.replace('/images/', ''));
715
491
  if (existsSync(filePath)) {
716
492
  unlinkSync(filePath);
717
- output.debug(`Deleted ${label} for ${name}`);
493
+ if (logDeletion) output.debug(`Deleted ${label} for ${name}`);
494
+ return 1;
718
495
  }
719
496
  } catch (error) {
720
497
  // safePath throws if path traversal is attempted
721
498
  output.warn(`Failed to delete ${label} for ${name}: ${error.message}`);
722
499
  }
500
+ return 0;
723
501
  };
724
502
  const deleteComparison = async comparisonId => {
725
503
  const reportData = readReportData();
@@ -755,20 +533,7 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
755
533
 
756
534
  // Remove heavy fields from comparison-details.json
757
535
  removeComparisonDetails(comparisonId);
758
-
759
- // Remove comparison from report data
760
- reportData.comparisons = reportData.comparisons.filter(c => c.id !== comparisonId);
761
-
762
- // Regenerate summary (groups computed client-side)
763
- reportData.timestamp = Date.now();
764
- reportData.summary = {
765
- total: reportData.comparisons.length,
766
- passed: reportData.comparisons.filter(c => c.status === 'passed' || c.status === 'baseline-created' || c.status === 'new').length,
767
- failed: reportData.comparisons.filter(c => c.status === 'failed').length,
768
- rejected: reportData.comparisons.filter(c => c.status === 'rejected').length,
769
- errors: reportData.comparisons.filter(c => c.status === 'error').length
770
- };
771
- writeFileSync(reportPath, JSON.stringify(reportData));
536
+ reportStore.writeReport(removeReportComparison(reportData, comparisonId, Date.now()));
772
537
  output.info(`Deleted comparison ${comparisonId} (${comparison.name})`);
773
538
  return {
774
539
  success: true,
@@ -1,12 +1,12 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { basename, join } from 'node:path';
4
- import { getServerRegistry } from '../tdd/server-registry.js';
5
- import { withTimeout } from '../utils/async-utils.js';
6
- import * as output from '../utils/output.js';
7
- import { tddCommand } from './tdd.js';
8
- import { parsePositiveInteger, readDaemonPidFile, resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.js';
9
- export { findDaemonPidByPort, readDaemonPidFile, resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.js';
4
+ import { withTimeout } from '../../shared/async-utils.js';
5
+ import * as output from '../../shared/output.js';
6
+ import { getServerRegistry } from './server-registry.js';
7
+ import { tddCommand } from './tdd.commands.js';
8
+ import { parsePositiveInteger, readDaemonPidFile, resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.commands.js';
9
+ export { findDaemonPidByPort, readDaemonPidFile, resolveDaemonPid, waitForProcessExit } from './tdd-daemon-process.commands.js';
10
10
  let defaultTimers = {
11
11
  setTimeout,
12
12
  clearTimeout
@@ -1,10 +1,10 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { basename, join, resolve } from 'node:path';
3
- import { generateScreenshotSignature } from '../tdd/core/signature.js';
4
- import { TddService as defaultTddService } from '../tdd/tdd-service.js';
5
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
6
- import * as output from '../utils/output.js';
7
- import { safePath } from '../utils/security.js';
3
+ import * as output from '../../shared/output.js';
4
+ import { safePath } from '../../shared/paths.js';
5
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
6
+ import { generateScreenshotSignature } from '../screenshots/screenshot-identity.js';
7
+ import { TddService as defaultTddService } from './tdd.service.js';
8
8
  let DEFAULT_PAGE_SIZE = 20;
9
9
  let MAX_PAGE_SIZE = 100;
10
10
  let TDD_SCREENSHOT_STATUSES = new Set(['baseline-created', 'baseline-updated', 'error', 'failed', 'new', 'passed', 'rejected']);
@@ -4,21 +4,22 @@
4
4
  */
5
5
 
6
6
  import { spawn as defaultSpawn } from 'node:child_process';
7
- import { createBuild as defaultCreateApiBuild, createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild } from '../api/index.js';
8
- import { CONFIG_DEFAULTS } from '../config/core.js';
9
- import { VizzlyError } from '../errors/vizzly-error.js';
10
- import { createServerManager as defaultCreateServerManager } from '../server-manager/index.js';
11
- import { createAuthService as defaultCreateAuthService } from '../services/auth-service.js';
12
- import { createBuildObject as defaultCreateBuildObject } from '../services/build-manager.js';
13
- import { createConfigService as defaultCreateConfigService } from '../services/config-service.js';
14
- import { createProjectService as defaultCreateProjectService } from '../services/project-service.js';
15
- import { generateStaticReport as defaultGenerateStaticReport, getReportFileUrl } from '../services/static-report-generator.js';
16
- import { getServerRegistry } from '../tdd/server-registry.js';
17
- import { initializeDaemon as defaultInitializeDaemon, runTests as defaultRunTests } from '../test-runner/index.js';
18
- import { openBrowser as defaultOpenBrowser } from '../utils/browser.js';
19
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
20
- import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitAuthor as defaultDetectCommitAuthor, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
21
- import * as defaultOutput from '../utils/output.js';
7
+ import { createServerManager as defaultCreateServerManager } from '../../adapters/http/server-manager.js';
8
+ import { openBrowser as defaultOpenBrowser } from '../../shared/browser.js';
9
+ import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitAuthor as defaultDetectCommitAuthor, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../../shared/git.js';
10
+ import * as defaultOutput from '../../shared/output.js';
11
+ import { VizzlyError } from '../../shared/vizzly-error.js';
12
+ import { createAuthService as defaultCreateAuthService } from '../auth/auth.service.js';
13
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
14
+ import { createBuild as defaultCreateApiBuild, finalizeBuild as defaultFinalizeApiBuild, getBuild as defaultGetBuild } from '../builds/build.service.js';
15
+ import { createBuildObject as defaultCreateBuildObject } from '../builds/build-info.js';
16
+ import { createConfigService as defaultCreateConfigService } from '../config/config.service.js';
17
+ import { CONFIG_DEFAULTS } from '../config/config-defaults.js';
18
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
19
+ import { createProjectService as defaultCreateProjectService } from '../projects/project.service.js';
20
+ import { generateStaticReport as defaultGenerateStaticReport, getReportFileUrl } from '../reports/report.service.js';
21
+ import { initializeDaemon as defaultInitializeDaemon, runTests as defaultRunTests } from '../runs/run.service.js';
22
+ import { getServerRegistry } from './server-registry.js';
22
23
 
23
24
  /**
24
25
  * Build the follow-up command for local TDD evidence.