@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.
- package/README.md +8 -0
- package/bin/vizzly.js +1 -1
- package/dist/{cli.js → adapters/cli/cli.js} +41 -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} +4 -5
- package/dist/{commands/init.js → domains/setup/init.commands.js} +10 -41
- 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/{utils → domains/updates}/cli-update.js +4 -6
- package/dist/{utils → domains/updates}/update-notifications.js +7 -7
- 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 +2 -2
- 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 +23 -22
- 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/{utils → domains/updates}/update-check.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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as output from '../../shared/output.js';
|
|
2
|
+
import { buildScreenshotPayload, buildScreenshotResolvePayload, buildShaCheckPayload } from './screenshot.serializers.js';
|
|
3
|
+
import { computeSha256 } from './screenshot-hash.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if SHAs already exist on the server
|
|
7
|
+
* @param {Object} client - API client
|
|
8
|
+
* @param {Array} screenshots - Screenshots to check (objects with sha256, or string SHAs)
|
|
9
|
+
* @param {string} buildId - Build ID for screenshot record creation
|
|
10
|
+
* @returns {Promise<Object>} { existing, missing, screenshots }
|
|
11
|
+
*/
|
|
12
|
+
export async function checkShas(client, screenshots, buildId) {
|
|
13
|
+
try {
|
|
14
|
+
let payload = buildShaCheckPayload(screenshots, buildId);
|
|
15
|
+
return await client.request('/api/sdk/check-shas', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json'
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify(payload)
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Continue without deduplication on error
|
|
24
|
+
output.debug('sha-check', 'failed, continuing without deduplication', {
|
|
25
|
+
error: error.message
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Extract SHAs for fallback response
|
|
29
|
+
let shaList = Array.isArray(screenshots) && screenshots.length > 0 && typeof screenshots[0] === 'object' ? screenshots.map(s => s.sha256) : screenshots;
|
|
30
|
+
return {
|
|
31
|
+
existing: [],
|
|
32
|
+
missing: shaList,
|
|
33
|
+
screenshots: []
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function canUploadBytesAfterShaResolveError(error) {
|
|
38
|
+
let status = error?.context?.status;
|
|
39
|
+
|
|
40
|
+
// Network failures, older servers, and server errors should degrade to the
|
|
41
|
+
// slower byte upload path. Auth and ownership errors must still surface.
|
|
42
|
+
if (!status) return true;
|
|
43
|
+
if (status >= 500) return true;
|
|
44
|
+
if (status === 400 && error.message?.includes('image_data')) return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Upload a screenshot with SHA deduplication
|
|
50
|
+
* @param {Object} client - API client
|
|
51
|
+
* @param {string} buildId - Build ID
|
|
52
|
+
* @param {string} name - Screenshot name
|
|
53
|
+
* @param {Buffer} buffer - Screenshot data
|
|
54
|
+
* @param {Object} metadata - Additional metadata
|
|
55
|
+
* @param {boolean} skipDedup - Skip SHA deduplication (uploadAll mode)
|
|
56
|
+
* @returns {Promise<Object>} Upload result
|
|
57
|
+
*/
|
|
58
|
+
export async function uploadScreenshot(client, buildId, name, buffer, metadata = {}, skipDedup = false, screenshotOptions = {}) {
|
|
59
|
+
// Skip SHA deduplication if requested
|
|
60
|
+
if (skipDedup) {
|
|
61
|
+
let payload = buildScreenshotPayload(name, buffer, metadata, null, screenshotOptions);
|
|
62
|
+
return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/json'
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify(payload)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Normal flow with server-side SHA resolution.
|
|
72
|
+
let sha256 = computeSha256(buffer);
|
|
73
|
+
let resolvePayload = buildScreenshotResolvePayload(name, metadata, sha256, screenshotOptions);
|
|
74
|
+
try {
|
|
75
|
+
let resolveResult = await client.request(`/api/sdk/builds/${buildId}/screenshots`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json'
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify(resolvePayload)
|
|
81
|
+
});
|
|
82
|
+
if (resolveResult?.upload_required === false) {
|
|
83
|
+
return {
|
|
84
|
+
message: 'Screenshot already exists, skipped upload',
|
|
85
|
+
sha256,
|
|
86
|
+
skipped: true,
|
|
87
|
+
screenshot: resolveResult,
|
|
88
|
+
fromExisting: true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (!canUploadBytesAfterShaResolveError(error)) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
output.debug('sha-resolve', 'failed, uploading screenshot bytes', {
|
|
96
|
+
error: error.message
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// File doesn't exist, or resolve failed; proceed with upload.
|
|
101
|
+
let payload = buildScreenshotPayload(name, buffer, metadata, sha256, screenshotOptions);
|
|
102
|
+
return client.request(`/api/sdk/builds/${buildId}/screenshots`, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'application/json'
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify(payload)
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { resolve } from 'node:path';
|
|
8
|
-
import { VizzlyError } from '
|
|
8
|
+
import { VizzlyError } from '../../shared/vizzly-error.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Resolve image buffer from file path or return buffer as-is
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute SHA256 hash of buffer
|
|
5
|
+
* @param {Buffer} buffer - Data to hash
|
|
6
|
+
* @returns {string} Hex-encoded SHA256 hash
|
|
7
|
+
*/
|
|
8
|
+
export function computeSha256(buffer) {
|
|
9
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Partition screenshots by SHA existence
|
|
14
|
+
* @param {Array<Object>} screenshots - Screenshots with sha256 property
|
|
15
|
+
* @param {Set<string>|Array<string>} existingShas - SHAs that already exist
|
|
16
|
+
* @returns {Object} { toUpload, existing } partitioned arrays
|
|
17
|
+
*/
|
|
18
|
+
export function partitionByShaExistence(screenshots, existingShas) {
|
|
19
|
+
let existingSet = existingShas instanceof Set ? existingShas : new Set(existingShas);
|
|
20
|
+
let toUpload = [];
|
|
21
|
+
let existing = [];
|
|
22
|
+
for (let screenshot of screenshots) {
|
|
23
|
+
if (existingSet.has(screenshot.sha256)) {
|
|
24
|
+
existing.push(screenshot);
|
|
25
|
+
} else {
|
|
26
|
+
toUpload.push(screenshot);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
toUpload,
|
|
31
|
+
existing
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if SHA check result indicates file exists
|
|
37
|
+
* @param {Object} checkResult - Result from checkShas endpoint
|
|
38
|
+
* @param {string} sha256 - SHA to check
|
|
39
|
+
* @returns {boolean} True if file exists
|
|
40
|
+
*/
|
|
41
|
+
export function shaExists(checkResult, sha256) {
|
|
42
|
+
return checkResult?.existing?.includes(sha256) ?? false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Find screenshot record from SHA check result
|
|
47
|
+
* @param {Object} checkResult - Result from checkShas endpoint
|
|
48
|
+
* @param {string} sha256 - SHA to find
|
|
49
|
+
* @returns {Object|null} Screenshot record or null
|
|
50
|
+
*/
|
|
51
|
+
export function findScreenshotBySha(checkResult, sha256) {
|
|
52
|
+
return checkResult?.screenshots?.find(s => s.sha256 === sha256) ?? null;
|
|
53
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build server listen options
|
|
3
|
+
* @param {Object} config - Server configuration
|
|
4
|
+
* @returns {{ port: number, host: string }}
|
|
5
|
+
*/
|
|
6
|
+
export function buildServerListenOptions(config) {
|
|
7
|
+
return {
|
|
8
|
+
port: config?.server?.port || 3000,
|
|
9
|
+
host: '127.0.0.1'
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build server started message
|
|
15
|
+
* @param {number} port - Server port
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function buildServerStartedMessage(port) {
|
|
19
|
+
return `Screenshot server listening on http://127.0.0.1:${port}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build server stopped message
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function buildServerStoppedMessage() {
|
|
27
|
+
return 'Screenshot server stopped';
|
|
28
|
+
}
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Each operation takes its dependencies as parameters for testability.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { extractScreenshotData, getEffectiveBuildId } from './screenshot.normalizers.js';
|
|
8
|
+
import { buildBadRequestResponse, buildInternalErrorResponse, buildNotFoundResponse, buildSuccessResponse } from './screenshot.serializers.js';
|
|
9
|
+
import { validateScreenshotRequest } from './screenshot.validations.js';
|
|
10
|
+
import { buildServerListenOptions, buildServerStartedMessage, buildServerStoppedMessage } from './screenshot-server-options.js';
|
|
8
11
|
|
|
9
12
|
// ============================================================================
|
|
10
13
|
// Request Body Parsing
|
|
@@ -180,4 +183,14 @@ export function stopServer({
|
|
|
180
183
|
resolve();
|
|
181
184
|
});
|
|
182
185
|
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if request is for the screenshot endpoint
|
|
190
|
+
* @param {string} method - HTTP method
|
|
191
|
+
* @param {string} url - Request URL
|
|
192
|
+
* @returns {boolean}
|
|
193
|
+
*/
|
|
194
|
+
export function isScreenshotEndpoint(method, url) {
|
|
195
|
+
return method === 'POST' && url === '/screenshot';
|
|
183
196
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getScreenshotOptionsPayload } from './screenshot-options.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Get effective build ID (falls back to 'default')
|
|
5
|
+
* @param {string|null|undefined} buildId - Build ID from request
|
|
6
|
+
* @returns {string}
|
|
7
|
+
*/
|
|
8
|
+
export function getEffectiveBuildId(buildId) {
|
|
9
|
+
return buildId || 'default';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract screenshot data from request body
|
|
14
|
+
* @param {Object} body - Request body
|
|
15
|
+
* @returns {{ name: string, image: string, properties?: Object }}
|
|
16
|
+
*/
|
|
17
|
+
export function extractScreenshotData(body) {
|
|
18
|
+
let screenshotData = {
|
|
19
|
+
name: body.name,
|
|
20
|
+
image: body.image,
|
|
21
|
+
properties: body.properties
|
|
22
|
+
};
|
|
23
|
+
Object.assign(screenshotData, getScreenshotOptionsPayload(body));
|
|
24
|
+
return screenshotData;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the flat internal comparison shape. Width and height always come from
|
|
29
|
+
* the captured image. The untouched user bag remains available in metadata.
|
|
30
|
+
*/
|
|
31
|
+
export let extractProperties = (validatedProperties, imageDimensions = {}) => {
|
|
32
|
+
if (!validatedProperties) return {};
|
|
33
|
+
return {
|
|
34
|
+
...validatedProperties,
|
|
35
|
+
viewport_width: imageDimensions.width ?? null,
|
|
36
|
+
viewport_height: imageDimensions.height ?? null,
|
|
37
|
+
browser: validatedProperties.browser ?? null,
|
|
38
|
+
metadata: validatedProperties
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Handles screenshot uploads and flush requests.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
6
|
+
import { parseJsonBody } from '../../adapters/http/middleware/json-parser.js';
|
|
7
|
+
import { sendError, sendJson } from '../../adapters/http/middleware/response.js';
|
|
8
|
+
import * as output from '../../shared/output.js';
|
|
9
|
+
import { normalizeScreenshotOptions } from './screenshot-options.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Route local capture and flush requests to the active cloud or TDD handler.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createScreenshotProperties, getScreenshotOptionsPayload } from './screenshot-options.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build payload for screenshot upload or SHA resolution
|
|
5
|
+
* @param {string} name - Screenshot name
|
|
6
|
+
* @param {Buffer|null} buffer - Image data, or null when resolving by SHA
|
|
7
|
+
* @param {Object} metadata - User screenshot properties
|
|
8
|
+
* @param {string|null} sha256 - Pre-computed SHA256 hash (optional)
|
|
9
|
+
* @returns {Object} Screenshot upload payload
|
|
10
|
+
*/
|
|
11
|
+
export function buildScreenshotPayload(name, buffer, metadata = {}, sha256 = null, screenshotOptions = {}) {
|
|
12
|
+
let payload = {
|
|
13
|
+
name,
|
|
14
|
+
properties: createScreenshotProperties({
|
|
15
|
+
properties: metadata
|
|
16
|
+
})
|
|
17
|
+
};
|
|
18
|
+
Object.assign(payload, getScreenshotOptionsPayload(screenshotOptions));
|
|
19
|
+
if (buffer) {
|
|
20
|
+
payload.image_data = buffer.toString('base64');
|
|
21
|
+
}
|
|
22
|
+
if (sha256) {
|
|
23
|
+
payload.sha256 = sha256;
|
|
24
|
+
}
|
|
25
|
+
return payload;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build payload for server-side screenshot resolution without sending image bytes
|
|
30
|
+
* @param {string} name - Screenshot name
|
|
31
|
+
* @param {Object} metadata - User screenshot properties
|
|
32
|
+
* @param {string} sha256 - Pre-computed SHA256 hash
|
|
33
|
+
* @returns {Object} Screenshot resolution payload
|
|
34
|
+
*/
|
|
35
|
+
export function buildScreenshotResolvePayload(name, metadata = {}, sha256, screenshotOptions = {}) {
|
|
36
|
+
return buildScreenshotPayload(name, null, metadata, sha256, screenshotOptions);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build payload for SHA existence check (signature-based format)
|
|
41
|
+
* @param {Array<Object>} screenshots - Screenshots with sha256 and metadata
|
|
42
|
+
* @param {string} buildId - Build ID for screenshot record creation
|
|
43
|
+
* @returns {Object} SHA check request payload
|
|
44
|
+
*/
|
|
45
|
+
export function buildShaCheckPayload(screenshots, buildId) {
|
|
46
|
+
// Check if using new signature-based format or legacy SHA-only format
|
|
47
|
+
if (screenshots.length > 0 && typeof screenshots[0] === 'object' && screenshots[0].sha256) {
|
|
48
|
+
return {
|
|
49
|
+
buildId,
|
|
50
|
+
screenshots
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Legacy format: array of SHA strings
|
|
55
|
+
return {
|
|
56
|
+
shas: screenshots,
|
|
57
|
+
buildId
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build screenshot object for SHA checking
|
|
63
|
+
* @param {string} sha256 - SHA256 hash of image
|
|
64
|
+
* @param {string} name - Screenshot name
|
|
65
|
+
* @param {Object} metadata - Screenshot metadata
|
|
66
|
+
* @returns {Object} Screenshot check object
|
|
67
|
+
*/
|
|
68
|
+
export function buildScreenshotCheckObject(sha256, name, metadata = {}) {
|
|
69
|
+
let meta = metadata || {};
|
|
70
|
+
let properties = createScreenshotProperties({
|
|
71
|
+
properties: meta
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
sha256,
|
|
75
|
+
name,
|
|
76
|
+
properties
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build success response object
|
|
82
|
+
* @returns {{ status: number, body: Object }}
|
|
83
|
+
*/
|
|
84
|
+
export function buildSuccessResponse() {
|
|
85
|
+
return {
|
|
86
|
+
status: 200,
|
|
87
|
+
body: {
|
|
88
|
+
success: true
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build error response object
|
|
95
|
+
* @param {number} status - HTTP status code
|
|
96
|
+
* @param {string} message - Error message
|
|
97
|
+
* @returns {{ status: number, body: Object }}
|
|
98
|
+
*/
|
|
99
|
+
export function buildErrorResponse(status, message) {
|
|
100
|
+
return {
|
|
101
|
+
status,
|
|
102
|
+
body: {
|
|
103
|
+
error: message
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build not found response
|
|
110
|
+
* @returns {{ status: number, body: Object }}
|
|
111
|
+
*/
|
|
112
|
+
export function buildNotFoundResponse() {
|
|
113
|
+
return buildErrorResponse(404, 'Not found');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build bad request response
|
|
118
|
+
* @param {string} message - Error message
|
|
119
|
+
* @returns {{ status: number, body: Object }}
|
|
120
|
+
*/
|
|
121
|
+
export function buildBadRequestResponse(message) {
|
|
122
|
+
return buildErrorResponse(400, message);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build internal error response
|
|
127
|
+
* @returns {{ status: number, body: Object }}
|
|
128
|
+
*/
|
|
129
|
+
export function buildInternalErrorResponse() {
|
|
130
|
+
return buildErrorResponse(500, 'Internal server error');
|
|
131
|
+
}
|
|
132
|
+
export function serializeScreenshotRequest({
|
|
133
|
+
buildId,
|
|
134
|
+
name,
|
|
135
|
+
image,
|
|
136
|
+
type
|
|
137
|
+
}, options) {
|
|
138
|
+
return {
|
|
139
|
+
buildId,
|
|
140
|
+
name,
|
|
141
|
+
image,
|
|
142
|
+
type,
|
|
143
|
+
properties: options.properties,
|
|
144
|
+
...getScreenshotOptionsPayload(options)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
* Security utilities for path sanitization and validation
|
|
3
|
-
* Protects against path traversal attacks and ensures safe file operations
|
|
4
|
-
*/
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
5
2
|
|
|
6
|
-
import { isAbsolute, join, normalize, resolve } from 'node:path';
|
|
7
|
-
import * as output from './output.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Sanitizes a screenshot name to prevent path traversal and ensure safe file naming
|
|
11
|
-
* @param {string} name - Original screenshot name
|
|
12
|
-
* @param {number} maxLength - Maximum allowed length (default: 255)
|
|
13
|
-
* @param {boolean} allowSlashes - Whether to allow forward slashes (for browser version strings)
|
|
14
|
-
* @returns {string} Sanitized screenshot name
|
|
15
|
-
*/
|
|
16
3
|
/**
|
|
17
4
|
* Validate screenshot name for security (no transformations, just validation)
|
|
18
5
|
* Throws if name contains path traversal or other dangerous patterns
|
|
@@ -95,7 +82,7 @@ export function sanitizeScreenshotName(name, maxLength = 255, allowSlashes = fal
|
|
|
95
82
|
// Allow only safe characters: alphanumeric, hyphens, underscores, dots, spaces, and optionally slashes
|
|
96
83
|
// Spaces are allowed here and will be converted to hyphens in generateBaselineFilename() to match cloud behavior
|
|
97
84
|
// Replace other characters with underscores
|
|
98
|
-
|
|
85
|
+
let allowedChars = allowSlashes ? /[^a-zA-Z0-9._ /-]/g : /[^a-zA-Z0-9._ -]/g;
|
|
99
86
|
let sanitized = name.replace(allowedChars, '_');
|
|
100
87
|
|
|
101
88
|
// Prevent names that start with dots (hidden files)
|
|
@@ -110,61 +97,6 @@ export function sanitizeScreenshotName(name, maxLength = 255, allowSlashes = fal
|
|
|
110
97
|
return sanitized;
|
|
111
98
|
}
|
|
112
99
|
|
|
113
|
-
/**
|
|
114
|
-
* Validates that a path stays within the allowed working directory bounds
|
|
115
|
-
* @param {string} targetPath - Path to validate
|
|
116
|
-
* @param {string} workingDir - Working directory that serves as the security boundary
|
|
117
|
-
* @returns {string} Resolved and normalized path if valid
|
|
118
|
-
* @throws {Error} If path is invalid or outside bounds
|
|
119
|
-
*/
|
|
120
|
-
export function validatePathSecurity(targetPath, workingDir) {
|
|
121
|
-
if (typeof targetPath !== 'string' || targetPath.length === 0) {
|
|
122
|
-
throw new Error('Path must be a non-empty string');
|
|
123
|
-
}
|
|
124
|
-
if (typeof workingDir !== 'string' || workingDir.length === 0) {
|
|
125
|
-
throw new Error('Working directory must be a non-empty string');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Normalize and resolve both paths
|
|
129
|
-
const resolvedWorkingDir = resolve(normalize(workingDir));
|
|
130
|
-
const resolvedTargetPath = resolve(normalize(targetPath));
|
|
131
|
-
|
|
132
|
-
// Ensure the target path starts with the working directory
|
|
133
|
-
if (!resolvedTargetPath.startsWith(resolvedWorkingDir)) {
|
|
134
|
-
output.warn(`Path traversal attempt blocked: ${targetPath} (resolved: ${resolvedTargetPath}) is outside working directory: ${resolvedWorkingDir}`);
|
|
135
|
-
throw new Error('Path is outside the allowed working directory');
|
|
136
|
-
}
|
|
137
|
-
return resolvedTargetPath;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Safely constructs a path within the working directory
|
|
142
|
-
* @param {string} workingDir - Base working directory
|
|
143
|
-
* @param {...string} pathSegments - Path segments to join
|
|
144
|
-
* @returns {string} Safely constructed path
|
|
145
|
-
* @throws {Error} If resulting path would be outside working directory
|
|
146
|
-
*/
|
|
147
|
-
export function safePath(workingDir, ...pathSegments) {
|
|
148
|
-
if (pathSegments.length === 0) {
|
|
149
|
-
return validatePathSecurity(workingDir, workingDir);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Sanitize each path segment
|
|
153
|
-
const sanitizedSegments = pathSegments.map(segment => {
|
|
154
|
-
if (typeof segment !== 'string') {
|
|
155
|
-
throw new Error('Path segment must be a string');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Block directory traversal in segments
|
|
159
|
-
if (segment.includes('..')) {
|
|
160
|
-
throw new Error('Path segment contains directory traversal sequence');
|
|
161
|
-
}
|
|
162
|
-
return segment;
|
|
163
|
-
});
|
|
164
|
-
const targetPath = join(workingDir, ...sanitizedSegments);
|
|
165
|
-
return validatePathSecurity(targetPath, workingDir);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
100
|
/**
|
|
169
101
|
* Validate user metadata without interpreting names as Vizzly options or
|
|
170
102
|
* rewriting values. Rendering code must escape strings for its output context.
|
|
@@ -202,4 +134,22 @@ export function validateScreenshotProperties(properties = {}) {
|
|
|
202
134
|
return result;
|
|
203
135
|
}
|
|
204
136
|
return copy(properties);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Validate screenshot request body
|
|
141
|
+
* @param {Object} body - Request body
|
|
142
|
+
* @returns {{ valid: boolean, error: string|null }}
|
|
143
|
+
*/
|
|
144
|
+
export function validateScreenshotRequest(body) {
|
|
145
|
+
if (!body?.name || !body?.image) {
|
|
146
|
+
return {
|
|
147
|
+
valid: false,
|
|
148
|
+
error: 'name and image are required'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
valid: true,
|
|
153
|
+
error: null
|
|
154
|
+
};
|
|
205
155
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import { readOptionalFile } from '
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
let sourcePath = fileURLToPath(new URL('../../skills/vizzly/', import.meta.url));
|
|
4
|
+
import { fileChange } from '../../shared/file-changes.js';
|
|
5
|
+
import { readOptionalFile } from '../../shared/files.js';
|
|
6
|
+
import { getPackageRoot, getPackageVersion } from '../../shared/package-info.js';
|
|
7
|
+
let sourcePath = path.join(getPackageRoot(), 'skills', 'vizzly');
|
|
9
8
|
let start = '<!-- vizzly-agent-guidance -->';
|
|
10
9
|
let end = '<!-- /vizzly-agent-guidance -->';
|
|
11
10
|
export function agentGuidancePath(cwd) {
|
|
@@ -1,24 +1,11 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
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,
|