@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.
- package/README.md +12 -3
- package/bin/vizzly.js +1 -1
- package/dist/{cli.js → adapters/cli/cli.js} +50 -35
- package/dist/adapters/cli/setup.js +34 -0
- package/dist/{client → adapters/client}/index.js +6 -7
- package/dist/adapters/http/cloud-proxy.js +29 -0
- package/dist/{server → adapters/http}/http-server.js +12 -13
- package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
- package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
- package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
- package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
- package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
- package/dist/adapters/sdk/index.d.ts +89 -0
- package/dist/{sdk → adapters/sdk}/index.js +22 -19
- package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
- package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
- package/dist/domains/auth/auth-errors.js +50 -0
- package/dist/domains/auth/auth-headers.js +41 -0
- package/dist/domains/auth/auth.normalizers.js +50 -0
- package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
- package/dist/domains/auth/auth.rules.js +19 -0
- package/dist/domains/auth/auth.serializers.js +32 -0
- package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
- package/dist/domains/auth/auth.validations.js +21 -0
- package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
- package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
- package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
- package/dist/domains/auth/token-store.js +81 -0
- package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
- package/dist/domains/baselines/baseline-download.js +192 -0
- package/dist/domains/baselines/baseline-write.js +22 -0
- package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
- package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
- package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
- package/dist/domains/baselines/cloud-baselines.js +9 -0
- package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
- package/dist/domains/builds/build.normalizers.js +35 -0
- package/dist/domains/builds/build.serializers.js +164 -0
- package/dist/domains/builds/build.service.js +119 -0
- package/dist/domains/builds/build.validations.js +39 -0
- package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
- package/dist/domains/builds/cloud-builds.routes.js +31 -0
- package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
- package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
- package/dist/domains/comparisons/cloud-comparisons.js +43 -0
- package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
- package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
- package/dist/domains/config/config-defaults.js +27 -0
- package/dist/{utils → domains/config}/config-discovery.js +1 -9
- package/dist/{utils → domains/config}/config-loader.js +10 -9
- package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
- package/dist/{types → domains/config}/config.d.ts +3 -3
- package/dist/domains/config/config.normalizers.js +126 -0
- package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
- package/dist/domains/config/config.rules.js +47 -0
- package/dist/domains/config/config.serializers.js +202 -0
- package/dist/domains/config/config.service.js +625 -0
- package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
- package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
- package/dist/domains/context/compact-context.validations.js +60 -0
- package/dist/domains/context/context-presentation.js +368 -0
- package/dist/domains/context/context.commands.js +385 -0
- package/dist/domains/context/context.serializers.js +157 -0
- package/dist/domains/context/context.service.js +76 -0
- package/dist/domains/context/context.validations.js +100 -0
- package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
- package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
- package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
- package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
- package/dist/domains/previews/preview.service.js +37 -0
- package/dist/domains/projects/cloud-projects.routes.js +28 -0
- package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
- package/dist/{utils → domains/projects}/project-binding.js +3 -3
- package/dist/domains/projects/project-errors.js +61 -0
- package/dist/{utils → domains/projects}/project-link-store.js +2 -2
- package/dist/domains/projects/project-requests.js +61 -0
- package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
- package/dist/domains/projects/project.normalizers.js +69 -0
- package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
- package/dist/domains/projects/project.validations.js +31 -0
- package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
- package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
- package/dist/domains/reports/dashboard.routes.js +96 -0
- package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
- package/dist/domains/reports/report-assets.routes.js +81 -0
- package/dist/domains/reports/report-state.js +51 -0
- package/dist/domains/reports/report-store.js +85 -0
- package/dist/domains/reports/report.serializers.js +99 -0
- package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
- package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
- package/dist/domains/runs/run-environment.js +69 -0
- package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
- package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
- package/dist/domains/runs/run.normalizers.js +8 -0
- package/dist/domains/runs/run.rules.js +56 -0
- package/dist/domains/runs/run.serializers.js +101 -0
- package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
- package/dist/domains/runs/run.validations.js +17 -0
- package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
- package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
- package/dist/domains/screenshots/cloud-screenshots.js +109 -0
- package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
- package/dist/domains/screenshots/screenshot-hash.js +53 -0
- package/dist/domains/screenshots/screenshot-server-options.js +28 -0
- package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
- package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
- package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
- package/dist/domains/screenshots/screenshot.serializers.js +146 -0
- package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
- package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
- package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
- package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
- package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
- package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
- package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
- package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
- package/dist/domains/updates/cli-update.js +103 -0
- package/dist/domains/updates/update-check.js +2 -0
- package/dist/domains/updates/update-notifications.js +175 -0
- package/dist/domains/uploads/upload-defaults.js +4 -0
- package/dist/domains/uploads/upload-files.js +32 -0
- package/dist/domains/uploads/upload-settings.js +21 -0
- package/dist/domains/uploads/upload-timing.js +18 -0
- package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
- package/dist/domains/uploads/upload.normalizers.js +60 -0
- package/dist/domains/uploads/upload.serializers.js +179 -0
- package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
- package/dist/domains/uploads/upload.validations.js +77 -0
- package/dist/index.js +12 -12
- package/dist/reporter/reporter-bundle.iife.js +86 -86
- package/dist/reporter-ssr/ssr-entry.js +3 -6
- package/dist/shared/cloud-protocol.js +171 -0
- package/dist/{utils → shared}/file-changes.js +1 -1
- package/dist/shared/files.js +9 -0
- package/dist/{utils → shared}/global-config.js +7 -78
- package/dist/shared/log-level.js +7 -0
- package/dist/{utils → shared}/output.js +1 -1
- package/dist/{utils → shared}/package-info.js +5 -4
- package/dist/shared/paths.js +57 -0
- package/package.json +37 -36
- package/dist/api/core.js +0 -382
- package/dist/api/endpoints.js +0 -421
- package/dist/api/index.js +0 -19
- package/dist/auth/core.js +0 -237
- package/dist/auth/index.js +0 -31
- package/dist/commands/context.js +0 -1066
- package/dist/config/core.js +0 -439
- package/dist/config/operations.js +0 -327
- package/dist/project/core.js +0 -211
- package/dist/screenshot-server/core.js +0 -161
- package/dist/screenshot-server/index.js +0 -11
- package/dist/server/routers/assets.js +0 -95
- package/dist/server/routers/cloud-proxy.js +0 -146
- package/dist/server/routers/dashboard.js +0 -172
- package/dist/services/auth-service.js +0 -117
- package/dist/services/config-service.js +0 -313
- package/dist/services/project-service.js +0 -88
- package/dist/test-runner/core.js +0 -293
- package/dist/test-runner/index.js +0 -13
- package/dist/types/sdk.d.ts +0 -186
- package/dist/uploader/core.js +0 -432
- package/dist/uploader/index.js +0 -88
- /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
- /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
- /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
- /package/dist/{server → adapters/http}/middleware/response.js +0 -0
- /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
- /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
- /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
- /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
- /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
- /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
- /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
- /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
- /package/dist/{context → domains/context}/provider-resolver.js +0 -0
- /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
- /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
- /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
- /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
- /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
- /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
- /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
- /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
- /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
- /package/dist/{types/index.d.ts → index.d.ts} +0 -0
- /package/dist/{utils → shared}/api-url.js +0 -0
- /package/dist/{utils → shared}/async-utils.js +0 -0
- /package/dist/{utils → shared}/browser.js +0 -0
- /package/dist/{utils → shared}/ci-env.js +0 -0
- /package/dist/{utils → shared}/colors.js +0 -0
- /package/dist/{utils → shared}/fetch-utils.js +0 -0
- /package/dist/{utils → shared}/git.js +0 -0
- /package/dist/{utils → shared}/patterns.js +0 -0
- /package/dist/{utils → shared}/secret-store.js +0 -0
- /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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
198
|
-
readReportData = readReportDataFile,
|
|
176
|
+
readReportData = readSnapshot,
|
|
199
177
|
watchReportData = watchReportDataFile,
|
|
200
178
|
timers = defaultTimers
|
|
201
179
|
} = context || {};
|
|
202
|
-
let reportDataPath =
|
|
203
|
-
let baselineMetadataPath =
|
|
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
|
+
}
|