@vizzly-testing/cli 0.39.1 → 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 +8 -0
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +41 -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} +4 -5
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +10 -41
  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/{utils → domains/updates}/cli-update.js +4 -6
  118. package/dist/{utils → domains/updates}/update-notifications.js +7 -7
  119. package/dist/domains/uploads/upload-defaults.js +4 -0
  120. package/dist/domains/uploads/upload-files.js +32 -0
  121. package/dist/domains/uploads/upload-settings.js +21 -0
  122. package/dist/domains/uploads/upload-timing.js +18 -0
  123. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  124. package/dist/domains/uploads/upload.normalizers.js +60 -0
  125. package/dist/domains/uploads/upload.serializers.js +179 -0
  126. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  127. package/dist/domains/uploads/upload.validations.js +77 -0
  128. package/dist/index.js +12 -12
  129. package/dist/reporter/reporter-bundle.iife.js +86 -86
  130. package/dist/reporter-ssr/ssr-entry.js +2 -2
  131. package/dist/shared/cloud-protocol.js +171 -0
  132. package/dist/{utils → shared}/file-changes.js +1 -1
  133. package/dist/shared/files.js +9 -0
  134. package/dist/{utils → shared}/global-config.js +7 -78
  135. package/dist/shared/log-level.js +7 -0
  136. package/dist/{utils → shared}/output.js +1 -1
  137. package/dist/{utils → shared}/package-info.js +5 -4
  138. package/dist/shared/paths.js +57 -0
  139. package/package.json +23 -22
  140. package/dist/api/core.js +0 -382
  141. package/dist/api/endpoints.js +0 -421
  142. package/dist/api/index.js +0 -19
  143. package/dist/auth/core.js +0 -237
  144. package/dist/auth/index.js +0 -31
  145. package/dist/commands/context.js +0 -1066
  146. package/dist/config/core.js +0 -439
  147. package/dist/config/operations.js +0 -327
  148. package/dist/project/core.js +0 -211
  149. package/dist/screenshot-server/core.js +0 -161
  150. package/dist/screenshot-server/index.js +0 -11
  151. package/dist/server/routers/assets.js +0 -95
  152. package/dist/server/routers/cloud-proxy.js +0 -146
  153. package/dist/server/routers/dashboard.js +0 -172
  154. package/dist/services/auth-service.js +0 -117
  155. package/dist/services/config-service.js +0 -313
  156. package/dist/services/project-service.js +0 -88
  157. package/dist/test-runner/core.js +0 -293
  158. package/dist/test-runner/index.js +0 -13
  159. package/dist/types/sdk.d.ts +0 -186
  160. package/dist/uploader/core.js +0 -432
  161. package/dist/uploader/index.js +0 -88
  162. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  163. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  164. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  166. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  167. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  168. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  169. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  170. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  171. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  172. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  173. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  174. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  175. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  176. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  177. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  178. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  179. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  180. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  181. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  182. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  183. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  184. /package/dist/{utils → domains/updates}/update-check.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,3 +1,10 @@
1
+ import { createAuthClient } from '../auth/auth-client.js';
2
+ import { getAuthTokens } from '../auth/token-store.js';
3
+ import { getApiUrl } from '../config/environment.js';
4
+ import { enrichProjectsWithOrg, extractBuilds, extractOrganizations, extractProject, extractProjects, extractToken, extractTokens } from './project.normalizers.js';
5
+ import { buildNoApiServiceError, buildNoAuthError, buildProjectFetchError, buildTokenCreateError, buildTokenRevokeError, buildTokensFetchError } from './project-errors.js';
6
+ import { buildBuildsUrl, buildOrgHeader, buildProjectUrl, buildTokensUrl } from './project-requests.js';
7
+
1
8
  /**
2
9
  * Project Operations - Project operations with dependency injection
3
10
  *
@@ -7,11 +14,89 @@
7
14
  * This makes them trivially testable without mocking modules.
8
15
  */
9
16
 
10
- import { buildBuildsUrl, buildNoApiServiceError, buildNoAuthError, buildOrgHeader, buildProjectFetchError, buildProjectUrl, buildTokenCreateError, buildTokenRevokeError, buildTokensFetchError, buildTokensUrl, enrichProjectsWithOrg, extractBuilds, extractOrganizations, extractProject, extractProjects, extractToken, extractTokens } from './core.js';
17
+ /**
18
+ * Project Service
19
+ * Wraps project operations for use by the HTTP server
20
+ *
21
+ * Provides the interface expected by src/domains/projects/projects.routes.js:
22
+ * - listProjects() - Returns [] if not authenticated
23
+ * - getRecentBuilds(projectSlug, organizationSlug, options) - Returns [] if not authenticated
24
+ *
25
+ * Error handling:
26
+ * - API methods (listProjects, getRecentBuilds) return empty arrays when not authenticated
27
+ */
28
+
29
+ /**
30
+ * Create a project service instance
31
+ * @param {Object} [options]
32
+ * @param {string} [options.apiUrl] - API base URL (defaults to VIZZLY_API_URL or https://app.vizzly.dev)
33
+ * @param {Object} [options.httpClient] - Injectable HTTP client (for testing)
34
+ * @param {Function} [options.getAuthTokens] - Injectable token getter (for testing)
35
+ * @returns {Object} Project service
36
+ */
37
+ export function createProjectService(options = {}) {
38
+ let apiUrl = options.apiUrl || getApiUrl();
39
+
40
+ // Create HTTP client once at service creation (not per-request)
41
+ // Allow injection for testing
42
+ let httpClient = options.httpClient || createAuthClient({
43
+ baseUrl: apiUrl
44
+ });
45
+
46
+ // Allow injection of getAuthTokens for testing
47
+ let tokenGetter = options.getAuthTokens || getAuthTokens;
11
48
 
12
- // ============================================================================
13
- // API Operations - List Projects
14
- // ============================================================================
49
+ /**
50
+ * Create an OAuth client with current access token
51
+ * @returns {Promise<Object|null>} OAuth client or null if not authenticated
52
+ */
53
+ async function createOAuthClient() {
54
+ let auth = await tokenGetter();
55
+ if (!auth?.accessToken) {
56
+ return null;
57
+ }
58
+
59
+ // Wrap authenticatedRequest to auto-inject the access token
60
+ return {
61
+ authenticatedRequest: (endpoint, fetchOptions = {}) => httpClient.authenticatedRequest(endpoint, auth.accessToken, fetchOptions)
62
+ };
63
+ }
64
+ return {
65
+ /**
66
+ * List all projects from API
67
+ * Returns empty array if not authenticated (projectOps handles null oauthClient)
68
+ * @returns {Promise<Array>} Array of projects, empty if not authenticated
69
+ */
70
+ async listProjects() {
71
+ let oauthClient = await createOAuthClient();
72
+ // listProjects handles null oauthClient by returning []
73
+ return listProjects({
74
+ oauthClient,
75
+ apiClient: null
76
+ });
77
+ },
78
+ /**
79
+ * Get recent builds for a project
80
+ * Returns empty array if not authenticated (projectOps handles null oauthClient)
81
+ * @param {string} projectSlug - Project slug
82
+ * @param {string} organizationSlug - Organization slug
83
+ * @param {Object} options - Query options
84
+ * @returns {Promise<Array>} Array of builds, empty if not authenticated
85
+ */
86
+ async getRecentBuilds(projectSlug, organizationSlug, options = {}) {
87
+ let oauthClient = await createOAuthClient();
88
+ // getRecentBuilds handles null oauthClient by returning []
89
+ return getRecentBuilds({
90
+ oauthClient,
91
+ apiClient: null,
92
+ projectSlug,
93
+ organizationSlug,
94
+ limit: options.limit,
95
+ branch: options.branch
96
+ });
97
+ }
98
+ };
99
+ }
15
100
 
16
101
  /**
17
102
  * List all projects from API using OAuth authentication
@@ -91,10 +176,6 @@ export async function listProjects({
91
176
  return [];
92
177
  }
93
178
 
94
- // ============================================================================
95
- // API Operations - Get Project
96
- // ============================================================================
97
-
98
179
  /**
99
180
  * Get project details using OAuth authentication
100
181
  * @param {Object} oauthClient - OAuth HTTP client
@@ -160,10 +241,6 @@ export async function getProject({
160
241
  throw buildNoAuthError();
161
242
  }
162
243
 
163
- // ============================================================================
164
- // API Operations - Recent Builds
165
- // ============================================================================
166
-
167
244
  /**
168
245
  * Get recent builds for a project using OAuth authentication
169
246
  * @param {Object} oauthClient - OAuth HTTP client
@@ -242,10 +319,6 @@ export async function getRecentBuilds({
242
319
  return [];
243
320
  }
244
321
 
245
- // ============================================================================
246
- // API Operations - Project Tokens
247
- // ============================================================================
248
-
249
322
  /**
250
323
  * Create a project token
251
324
  * @param {Object} apiClient - API HTTP client with request method
@@ -0,0 +1,31 @@
1
+ export function parseProjectSelector(selector, options = {}) {
2
+ let organizationSlug = options.org || null;
3
+ let projectSlug = options.project || null;
4
+ if (selector) {
5
+ let parts = selector.split('/');
6
+ if (parts.length === 2) {
7
+ organizationSlug = organizationSlug || parts[0];
8
+ projectSlug = projectSlug || parts[1];
9
+ } else if (parts.length === 1) {
10
+ projectSlug = projectSlug || parts[0];
11
+ }
12
+ }
13
+ return {
14
+ organizationSlug,
15
+ projectSlug
16
+ };
17
+ }
18
+ export function validateProjectLinkOptions(selector, options = {}) {
19
+ let errors = [];
20
+ let {
21
+ organizationSlug,
22
+ projectSlug
23
+ } = parseProjectSelector(selector, options);
24
+ if (!organizationSlug) {
25
+ errors.push('Organization is required. Use <org>/<project> or --org <slug>.');
26
+ }
27
+ if (!projectSlug) {
28
+ errors.push('Project is required. Use <org>/<project> or --project <slug>.');
29
+ }
30
+ return errors;
31
+ }
@@ -1,13 +1,12 @@
1
+ import * as defaultOutput from '../../shared/output.js';
2
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
3
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
4
+ import { getApiUrl as defaultGetApiUrl } from '../config/environment.js';
5
+
1
6
  /**
2
7
  * Projects command - List projects the user has access to
3
8
  */
4
9
 
5
- import { createApiClient as defaultCreateApiClient } from '../api/client.js';
6
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
7
- import { getApiUrl as defaultGetApiUrl } from '../utils/environment-config.js';
8
- import { getAccessToken as defaultGetAccessToken } from '../utils/global-config.js';
9
- import * as defaultOutput from '../utils/output.js';
10
-
11
10
  /**
12
11
  * Projects command implementation
13
12
  * @param {Object} options - Command options
@@ -19,7 +18,6 @@ export async function projectsCommand(options = {}, globalOptions = {}, deps = {
19
18
  loadConfig = defaultLoadConfig,
20
19
  createApiClient = defaultCreateApiClient,
21
20
  getApiUrl = defaultGetApiUrl,
22
- getAccessToken = defaultGetAccessToken,
23
21
  output = defaultOutput,
24
22
  exit = code => process.exit(code)
25
23
  } = deps;
@@ -33,7 +31,7 @@ export async function projectsCommand(options = {}, globalOptions = {}, deps = {
33
31
 
34
32
  // Prefer user auth token for listing projects (project tokens are org-scoped).
35
33
  // Falls back to config.apiKey which may be: VIZZLY_TOKEN, --token flag, or project token.
36
- let token = (await getAccessToken()) || config.apiKey;
34
+ let token = config.userToken || config.apiKey;
37
35
  if (!token) {
38
36
  output.error('API token required. Use --token, set VIZZLY_TOKEN, or run "vizzly login"');
39
37
  output.cleanup();
@@ -1,11 +1,11 @@
1
+ import { sendError, sendServiceUnavailable, sendSuccess } from '../../adapters/http/middleware/response.js';
2
+ import * as output from '../../shared/output.js';
3
+
1
4
  /**
2
5
  * Projects Router
3
6
  * Handles project listing and builds endpoints
4
7
  */
5
8
 
6
- import * as output from '../../utils/output.js';
7
- import { sendError, sendServiceUnavailable, sendSuccess } from '../middleware/response.js';
8
-
9
9
  /**
10
10
  * Create projects router
11
11
  * @param {Object} context - Router context
@@ -25,7 +25,7 @@ export function createProjectsRouter({
25
25
  // List all projects from API
26
26
  if (req.method === 'GET' && pathname === '/api/projects') {
27
27
  try {
28
- const projects = await projectService.listProjects();
28
+ let projects = await projectService.listProjects();
29
29
  sendSuccess(res, {
30
30
  projects
31
31
  });
@@ -40,7 +40,7 @@ export function createProjectsRouter({
40
40
  }
41
41
 
42
42
  // Get builds for a specific project (used by /builds page)
43
- const projectBuildsMatch = pathname.match(/^\/api\/projects\/([^/]+)\/([^/]+)\/builds$/);
43
+ let projectBuildsMatch = pathname.match(/^\/api\/projects\/([^/]+)\/([^/]+)\/builds$/);
44
44
  if (req.method === 'GET' && projectBuildsMatch) {
45
45
  try {
46
46
  let organizationSlug = decodeURIComponent(projectBuildsMatch[1]);
@@ -0,0 +1,96 @@
1
+ /** Dashboard HTTP projections and the reporter application shell. */
2
+ import { sendError, sendHtml, sendSuccess } from '../../adapters/http/middleware/response.js';
3
+ import * as output from '../../shared/output.js';
4
+ import { presentComparisonDetails } from './report.serializers.js';
5
+ import { createReportStore } from './report-store.js';
6
+ let SPA_ROUTES = ['/', '/stats', '/settings', '/projects', '/builds'];
7
+ export function createDashboardRouter(context) {
8
+ let {
9
+ workingDir = process.cwd()
10
+ } = context || {};
11
+ let reports = createReportStore(workingDir);
12
+ return async function handleDashboardRoute(req, res, pathname) {
13
+ if (req.method !== 'GET') return false;
14
+ if (pathname === '/api/report-data') {
15
+ try {
16
+ sendSuccess(res, reports.readSnapshot());
17
+ } catch (error) {
18
+ output.debug('Error reading report data:', {
19
+ error: error.message
20
+ });
21
+ sendError(res, 500, 'Failed to read report data');
22
+ }
23
+ return true;
24
+ }
25
+ let comparisonMatch = pathname.match(/^\/api\/comparison\/(.+)$/);
26
+ if (comparisonMatch) {
27
+ let comparisonId = decodeURIComponent(comparisonMatch[1]);
28
+ if (!comparisonId) {
29
+ sendError(res, 400, 'Comparison ID is required');
30
+ return true;
31
+ }
32
+ try {
33
+ let reportData = reports.readReport();
34
+ if (!reportData) {
35
+ sendError(res, 404, 'No report data found');
36
+ return true;
37
+ }
38
+ let comparison = (reportData.comparisons || []).find(candidate => candidate.id === comparisonId || candidate.signature === comparisonId || candidate.name === comparisonId);
39
+ if (!comparison) {
40
+ sendError(res, 404, 'Comparison not found');
41
+ return true;
42
+ }
43
+ let heavy = reports.readDetails()[comparison.id];
44
+ sendSuccess(res, heavy ? {
45
+ ...comparison,
46
+ ...presentComparisonDetails(heavy)
47
+ } : comparison);
48
+ } catch (error) {
49
+ output.debug('Error reading comparison data:', {
50
+ error: error.message
51
+ });
52
+ sendError(res, 500, 'Failed to read comparison data');
53
+ }
54
+ return true;
55
+ }
56
+ if (SPA_ROUTES.includes(pathname) || pathname.startsWith('/comparison/')) {
57
+ let reportData = null;
58
+ try {
59
+ reportData = reports.readSnapshot();
60
+ } catch (error) {
61
+ output.debug('Could not read report data:', {
62
+ error: error.message
63
+ });
64
+ }
65
+ let dashboardHtml = `
66
+ <!DOCTYPE html>
67
+ <html>
68
+ <head>
69
+ <title>Vizzly Dev Dashboard</title>
70
+ <meta charset="utf-8">
71
+ <meta name="viewport" content="width=device-width, initial-scale=1">
72
+ <link rel="stylesheet" href="/reporter-bundle.css">
73
+ </head>
74
+ <body>
75
+ <div id="vizzly-reporter-root">
76
+ <div class="reporter-loading">
77
+ <div>
78
+ <div class="spinner"></div>
79
+ <p>Loading Vizzly Dev Dashboard...</p>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <script>
85
+ // Inject report data if available
86
+ ${reportData ? `window.VIZZLY_REPORTER_DATA = ${JSON.stringify(reportData)};` : ''}
87
+ </script>
88
+ <script src="/reporter-bundle.js"></script>
89
+ </body>
90
+ </html>`;
91
+ sendHtml(res, 200, dashboardHtml);
92
+ return true;
93
+ }
94
+ return false;
95
+ };
96
+ }
@@ -3,8 +3,9 @@
3
3
  * Server-Sent Events endpoint for real-time dashboard updates
4
4
  */
5
5
 
6
- import { existsSync, readFileSync, statSync, watch } from 'node:fs';
6
+ import { existsSync, statSync, watch } from 'node:fs';
7
7
  import { join } from 'node:path';
8
+ import { createReportStore } from './report-store.js';
8
9
  let FILE_WATCH_DEBOUNCE_MS = 25;
9
10
  let FILE_POLL_INTERVAL_MS = 50;
10
11
  let REPORT_READ_RETRY_MS = 25;
@@ -16,37 +17,6 @@ let defaultTimers = {
16
17
  clearInterval
17
18
  };
18
19
 
19
- /**
20
- * Read and parse JSON from disk, returning null on missing or invalid files.
21
- */
22
- function readJsonFile(path) {
23
- if (!existsSync(path)) {
24
- return null;
25
- }
26
- try {
27
- return JSON.parse(readFileSync(path, 'utf8'));
28
- } catch {
29
- return null;
30
- }
31
- }
32
-
33
- /**
34
- * Read report data and attach baseline metadata for the stats view.
35
- */
36
- function readReportDataFile({
37
- reportDataPath,
38
- baselineMetadataPath
39
- }) {
40
- let data = readJsonFile(reportDataPath);
41
- if (!data) {
42
- return null;
43
- }
44
- return {
45
- ...data,
46
- baseline: readJsonFile(baselineMetadataPath)
47
- };
48
- }
49
-
50
20
  /**
51
21
  * Build a lookup map from comparisons array keyed by id
52
22
  */
@@ -193,14 +163,22 @@ function watchReportDataFile({
193
163
  * @returns {Function} Route handler
194
164
  */
195
165
  export function createEventsRouter(context) {
166
+ let workingDir = context?.workingDir || process.cwd();
167
+ let reports = createReportStore(workingDir);
168
+ let readSnapshot = () => {
169
+ try {
170
+ return reports.readSnapshot();
171
+ } catch {
172
+ return null;
173
+ }
174
+ };
196
175
  let {
197
- workingDir = process.cwd(),
198
- readReportData = readReportDataFile,
176
+ readReportData = readSnapshot,
199
177
  watchReportData = watchReportDataFile,
200
178
  timers = defaultTimers
201
179
  } = context || {};
202
- let reportDataPath = join(workingDir, '.vizzly', 'report-data.json');
203
- let baselineMetadataPath = join(workingDir, '.vizzly', 'baselines', 'metadata.json');
180
+ let reportDataPath = reports.paths.report;
181
+ let baselineMetadataPath = reports.paths.baseline;
204
182
 
205
183
  /**
206
184
  * Send SSE event to response
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Assets Router
3
+ * Serves static assets (bundle files, images)
4
+ */
5
+
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { sendError, sendFile, sendNotFound } from '../../adapters/http/middleware/response.js';
9
+ import * as output from '../../shared/output.js';
10
+ import { getPackageRoot } from '../../shared/package-info.js';
11
+ let reporterAssets = new Map([['/reporter-bundle.js', {
12
+ filename: 'reporter-bundle.iife.js',
13
+ contentType: 'application/javascript',
14
+ label: 'reporter bundle',
15
+ missing: 'Reporter bundle not found'
16
+ }], ['/reporter-bundle.css', {
17
+ filename: 'reporter-bundle.css',
18
+ contentType: 'text/css',
19
+ label: 'reporter CSS',
20
+ missing: 'Reporter CSS not found'
21
+ }]]);
22
+
23
+ /**
24
+ * Create assets router for a workspace's report and packaged reporter build.
25
+ * @param {Object} [context] - Router context
26
+ * @param {string} [context.workingDir] - Workspace containing report artifacts
27
+ * @returns {Function} Route handler
28
+ */
29
+ export function createAssetsRouter({
30
+ workingDir = process.cwd()
31
+ } = {}) {
32
+ return async function handleAssetsRoute(req, res, pathname) {
33
+ if (req.method !== 'GET') {
34
+ return false;
35
+ }
36
+ let asset = reporterAssets.get(pathname);
37
+ if (asset) {
38
+ let filename = join(getPackageRoot(), 'dist', 'reporter', asset.filename);
39
+ if (!existsSync(filename)) {
40
+ sendNotFound(res, asset.missing);
41
+ return true;
42
+ }
43
+ try {
44
+ sendFile(res, readFileSync(filename, 'utf8'), asset.contentType);
45
+ } catch (error) {
46
+ output.debug(`Error serving ${asset.label}:`, {
47
+ error: error.message
48
+ });
49
+ sendError(res, 500, `Error loading ${asset.label}`);
50
+ }
51
+ return true;
52
+ }
53
+
54
+ // Serve images from .vizzly directory
55
+ if (pathname.startsWith('/images/')) {
56
+ let imagePath = pathname.replace('/images/', '');
57
+ let fullImagePath = join(workingDir, '.vizzly', imagePath);
58
+ if (existsSync(fullImagePath)) {
59
+ try {
60
+ let imageData = readFileSync(fullImagePath);
61
+ // Images are rewritten in place between TDD runs, so disable browser caching.
62
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
63
+ res.setHeader('Pragma', 'no-cache');
64
+ res.setHeader('Expires', '0');
65
+ sendFile(res, imageData, 'image/png');
66
+ return true;
67
+ } catch (error) {
68
+ output.debug('Error serving image:', {
69
+ error: error.message
70
+ });
71
+ sendError(res, 500, 'Error loading image');
72
+ return true;
73
+ }
74
+ } else {
75
+ sendNotFound(res, 'Image not found');
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ };
81
+ }
@@ -0,0 +1,51 @@
1
+ /** Local review summaries intentionally count new baselines as passing captures. */
2
+ export function summarizeReportComparisons(comparisons = []) {
3
+ let summary = {
4
+ total: comparisons.length,
5
+ passed: 0,
6
+ failed: 0,
7
+ rejected: 0,
8
+ errors: 0
9
+ };
10
+ for (let comparison of comparisons) {
11
+ if (['passed', 'baseline-created', 'new'].includes(comparison.status)) {
12
+ summary.passed++;
13
+ } else if (comparison.status === 'failed') {
14
+ summary.failed++;
15
+ } else if (comparison.status === 'rejected') {
16
+ summary.rejected++;
17
+ } else if (comparison.status === 'error') {
18
+ summary.errors++;
19
+ }
20
+ }
21
+ return summary;
22
+ }
23
+ export function createEmptyReport(timestamp) {
24
+ return {
25
+ timestamp,
26
+ comparisons: [],
27
+ summary: summarizeReportComparisons()
28
+ };
29
+ }
30
+ function withComparisons(report, comparisons, timestamp) {
31
+ return {
32
+ ...report,
33
+ timestamp,
34
+ comparisons,
35
+ summary: summarizeReportComparisons(comparisons)
36
+ };
37
+ }
38
+ export function upsertReportComparison(report, comparison, timestamp) {
39
+ let previous = report.comparisons || [];
40
+ let existing = previous.find(candidate => candidate.id === comparison.id);
41
+ let updated = {
42
+ ...comparison,
43
+ initialStatus: existing?.initialStatus || comparison.status
44
+ };
45
+ let comparisons = existing ? previous.map(candidate => candidate.id === comparison.id ? updated : candidate) : [...previous, updated];
46
+ return withComparisons(report, comparisons, timestamp);
47
+ }
48
+ export function removeReportComparison(report, id, timestamp) {
49
+ let comparisons = (report.comparisons || []).filter(comparison => comparison.id !== id);
50
+ return withComparisons(report, comparisons, timestamp);
51
+ }
@@ -0,0 +1,85 @@
1
+ import { existsSync as defaultExistsSync, readFileSync as defaultReadFileSync, unlinkSync as defaultUnlinkSync, writeFileSync as defaultWriteFileSync } from 'node:fs';
2
+ import { join as defaultJoin } from 'node:path';
3
+ import * as defaultOutput from '../../shared/output.js';
4
+ import { createEmptyReport } from './report-state.js';
5
+
6
+ /** The local report has one lightweight watched file and one heavy-details file. */
7
+ export function createReportStore(workingDir, io = {}) {
8
+ let {
9
+ existsSync = defaultExistsSync,
10
+ readFileSync = defaultReadFileSync,
11
+ writeFileSync = defaultWriteFileSync,
12
+ unlinkSync = defaultUnlinkSync,
13
+ join = defaultJoin,
14
+ output = defaultOutput
15
+ } = io;
16
+ let paths = {
17
+ report: join(workingDir, '.vizzly', 'report-data.json'),
18
+ details: join(workingDir, '.vizzly', 'comparison-details.json'),
19
+ baseline: join(workingDir, '.vizzly', 'baselines', 'metadata.json')
20
+ };
21
+ function readJson(filename) {
22
+ return existsSync(filename) ? JSON.parse(readFileSync(filename, 'utf8')) : null;
23
+ }
24
+ function readReport() {
25
+ return readJson(paths.report);
26
+ }
27
+ function readBaselineMetadata() {
28
+ try {
29
+ return readJson(paths.baseline);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function readSnapshot() {
35
+ let report = readReport();
36
+ return report ? {
37
+ ...report,
38
+ baseline: readBaselineMetadata()
39
+ } : null;
40
+ }
41
+ function readDetails() {
42
+ try {
43
+ return readJson(paths.details) || {};
44
+ } catch (error) {
45
+ output.debug('Failed to read comparison details:', error);
46
+ return {};
47
+ }
48
+ }
49
+ function writeReport(report) {
50
+ writeFileSync(paths.report, JSON.stringify(report));
51
+ }
52
+ function updateDetails(id, fields) {
53
+ let hasData = Object.values(fields).some(value => value != null && (!Array.isArray(value) || value.length > 0));
54
+ if (!hasData) return;
55
+ let details = readDetails();
56
+ details[id] = fields;
57
+ writeFileSync(paths.details, JSON.stringify(details));
58
+ }
59
+ function removeDetails(id) {
60
+ let details = readDetails();
61
+ delete details[id];
62
+ writeFileSync(paths.details, JSON.stringify(details));
63
+ }
64
+ function reset() {
65
+ writeReport(createEmptyReport(Date.now()));
66
+ if (existsSync(paths.details)) writeFileSync(paths.details, JSON.stringify({}));
67
+ }
68
+ function clear() {
69
+ for (let filename of [paths.report, paths.details]) {
70
+ if (existsSync(filename)) unlinkSync(filename);
71
+ }
72
+ }
73
+ return {
74
+ paths,
75
+ readReport,
76
+ readSnapshot,
77
+ readBaselineMetadata,
78
+ readDetails,
79
+ writeReport,
80
+ updateDetails,
81
+ removeDetails,
82
+ reset,
83
+ clear
84
+ };
85
+ }