@vizzly-testing/cli 0.30.0 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/api/client.js +6 -3
- package/dist/cli.js +58 -29
- package/dist/client/index.js +21 -29
- package/dist/commands/api.js +145 -90
- package/dist/commands/baselines.js +78 -47
- package/dist/commands/builds.js +9 -8
- package/dist/commands/comparisons.js +7 -8
- package/dist/commands/config-cmd.js +15 -7
- package/dist/commands/context.js +329 -18
- package/dist/commands/doctor.js +116 -81
- package/dist/commands/init.js +202 -201
- package/dist/commands/login.js +134 -65
- package/dist/commands/logout.js +17 -6
- package/dist/commands/preview.js +29 -57
- package/dist/commands/project.js +120 -0
- package/dist/commands/projects.js +9 -2
- package/dist/commands/review.js +236 -202
- package/dist/commands/run.js +75 -72
- package/dist/commands/status.js +228 -150
- package/dist/commands/tdd-daemon-process.js +98 -0
- package/dist/commands/tdd-daemon.js +318 -212
- package/dist/commands/tdd.js +23 -8
- package/dist/commands/upload.js +10 -9
- package/dist/commands/whoami.js +63 -25
- package/dist/config/core.js +36 -35
- package/dist/context/local-workspace-provider.js +132 -12
- package/dist/context/provider-resolver.js +5 -1
- package/dist/index.js +5 -4
- package/dist/plugin-loader.js +55 -49
- package/dist/project/core.js +2 -2
- package/dist/project/operations.js +6 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +7 -7
- package/dist/sdk/index.js +162 -107
- package/dist/server/handlers/tdd-handler.js +30 -54
- package/dist/server/routers/baseline.js +195 -178
- package/dist/server/routers/events.js +212 -174
- package/dist/server/routers/projects.js +14 -5
- package/dist/server/routers/screenshot.js +1 -31
- package/dist/services/build-manager.js +19 -97
- package/dist/services/config-service.js +26 -34
- package/dist/services/index.js +2 -4
- package/dist/services/static-report-generator.js +11 -12
- package/dist/services/test-runner.js +21 -27
- package/dist/tdd/core/region-coverage.js +109 -21
- package/dist/tdd/index.js +1 -1
- package/dist/tdd/metadata/region-metadata.js +0 -37
- package/dist/tdd/runtime-context.js +78 -0
- package/dist/tdd/server-registry.js +38 -17
- package/dist/tdd/services/baseline-manager.js +23 -0
- package/dist/tdd/services/comparison-service.js +10 -1
- package/dist/tdd/services/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +144 -138
- package/dist/types/client.d.ts +36 -1
- package/dist/types/index.d.ts +114 -22
- package/dist/types/sdk.d.ts +40 -0
- package/dist/uploader/core.js +5 -4
- package/dist/uploader/index.js +79 -2
- package/dist/uploader/operations.js +40 -5
- package/dist/utils/api-url.js +14 -0
- package/dist/utils/async-utils.js +26 -0
- package/dist/utils/ci-env.js +19 -12
- package/dist/utils/config-loader.js +39 -65
- package/dist/utils/config-schema.js +19 -55
- package/dist/utils/fetch-utils.js +12 -5
- package/dist/utils/git.js +35 -72
- package/dist/utils/global-config.js +20 -1
- package/dist/utils/output.js +19 -7
- package/dist/utils/patterns.js +14 -0
- package/dist/utils/project-link-store.js +105 -0
- package/dist/utils/screenshot-options.js +28 -0
- package/dist/utils/secret-store.js +65 -0
- package/package.json +14 -11
- package/dist/services/screenshot-server.js +0 -65
- package/dist/services/server-manager.js +0 -81
- package/dist/services/uploader.js +0 -100
- package/dist/tdd/services/region-service.js +0 -75
- package/dist/utils/build-history.js +0 -103
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { loadGlobalConfig, saveGlobalConfig } from './global-config.js';
|
|
2
|
+
import { deleteSecret as defaultDeleteSecret, getSecret as defaultGetSecret, saveSecret as defaultSaveSecret } from './secret-store.js';
|
|
3
|
+
export function buildProjectLinkAccount({
|
|
4
|
+
apiUrl,
|
|
5
|
+
organizationSlug,
|
|
6
|
+
projectSlug
|
|
7
|
+
}) {
|
|
8
|
+
return `${apiUrl || 'https://app.vizzly.dev'}|${organizationSlug}/${projectSlug}`;
|
|
9
|
+
}
|
|
10
|
+
function getProjectLinkConfig(config) {
|
|
11
|
+
return config.projectLink || {
|
|
12
|
+
active: null,
|
|
13
|
+
links: {}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export async function saveProjectLink(link, deps = {}) {
|
|
17
|
+
let {
|
|
18
|
+
loadConfig = loadGlobalConfig,
|
|
19
|
+
saveConfig = saveGlobalConfig,
|
|
20
|
+
saveSecret = defaultSaveSecret,
|
|
21
|
+
now = () => new Date()
|
|
22
|
+
} = deps;
|
|
23
|
+
let config = await loadConfig();
|
|
24
|
+
let projectLink = getProjectLinkConfig(config);
|
|
25
|
+
let account = buildProjectLinkAccount(link);
|
|
26
|
+
let storedInKeychain = await saveSecret(account, link.token);
|
|
27
|
+
projectLink.active = account;
|
|
28
|
+
projectLink.links = {
|
|
29
|
+
...projectLink.links,
|
|
30
|
+
[account]: {
|
|
31
|
+
apiUrl: link.apiUrl,
|
|
32
|
+
organizationSlug: link.organizationSlug,
|
|
33
|
+
organizationName: link.organizationName,
|
|
34
|
+
projectSlug: link.projectSlug,
|
|
35
|
+
projectName: link.projectName,
|
|
36
|
+
tokenId: link.tokenId,
|
|
37
|
+
tokenPrefix: link.tokenPrefix,
|
|
38
|
+
expiresAt: link.expiresAt || null,
|
|
39
|
+
createdAt: link.createdAt || now().toISOString(),
|
|
40
|
+
storage: storedInKeychain ? 'keychain' : 'file',
|
|
41
|
+
token: storedInKeychain ? undefined : link.token
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
config.projectLink = projectLink;
|
|
45
|
+
await saveConfig(config);
|
|
46
|
+
return {
|
|
47
|
+
...projectLink.links[account],
|
|
48
|
+
account,
|
|
49
|
+
token: link.token
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export async function getActiveProjectLink(options = {}, deps = {}) {
|
|
53
|
+
let {
|
|
54
|
+
loadConfig = loadGlobalConfig,
|
|
55
|
+
getSecret = defaultGetSecret
|
|
56
|
+
} = deps;
|
|
57
|
+
let config = await loadConfig();
|
|
58
|
+
let projectLink = getProjectLinkConfig(config);
|
|
59
|
+
let account = options.account || projectLink.active;
|
|
60
|
+
let link = account ? projectLink.links?.[account] : null;
|
|
61
|
+
if (!link && options.apiUrl && options.organizationSlug && options.projectSlug) {
|
|
62
|
+
account = buildProjectLinkAccount(options);
|
|
63
|
+
link = projectLink.links?.[account] || null;
|
|
64
|
+
}
|
|
65
|
+
if (!link) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (options.apiUrl && link.apiUrl && link.apiUrl !== options.apiUrl) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
let token = link.storage === 'keychain' ? await getSecret(account) : link.token;
|
|
72
|
+
if (!token) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
...link,
|
|
77
|
+
account,
|
|
78
|
+
token
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export async function clearActiveProjectLink(deps = {}) {
|
|
82
|
+
let {
|
|
83
|
+
loadConfig = loadGlobalConfig,
|
|
84
|
+
saveConfig = saveGlobalConfig,
|
|
85
|
+
deleteSecret = defaultDeleteSecret
|
|
86
|
+
} = deps;
|
|
87
|
+
let config = await loadConfig();
|
|
88
|
+
let projectLink = getProjectLinkConfig(config);
|
|
89
|
+
let account = projectLink.active;
|
|
90
|
+
if (!account || !projectLink.links?.[account]) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
let link = projectLink.links[account];
|
|
94
|
+
if (link.storage === 'keychain') {
|
|
95
|
+
await deleteSecret(account);
|
|
96
|
+
}
|
|
97
|
+
delete projectLink.links[account];
|
|
98
|
+
projectLink.active = null;
|
|
99
|
+
config.projectLink = projectLink;
|
|
100
|
+
await saveConfig(config);
|
|
101
|
+
return {
|
|
102
|
+
...link,
|
|
103
|
+
account
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize screenshot SDK options into the properties payload consumed by the
|
|
3
|
+
* local TDD server and cloud-compatible comparison path.
|
|
4
|
+
*/
|
|
5
|
+
export function createScreenshotProperties(options = {}) {
|
|
6
|
+
let {
|
|
7
|
+
buildId: _buildId,
|
|
8
|
+
properties = {},
|
|
9
|
+
threshold,
|
|
10
|
+
minClusterSize,
|
|
11
|
+
fullPage,
|
|
12
|
+
...topLevelProperties
|
|
13
|
+
} = options;
|
|
14
|
+
let normalizedProperties = {
|
|
15
|
+
...topLevelProperties,
|
|
16
|
+
...properties
|
|
17
|
+
};
|
|
18
|
+
if (threshold !== undefined) {
|
|
19
|
+
normalizedProperties.threshold = threshold;
|
|
20
|
+
}
|
|
21
|
+
if (minClusterSize !== undefined) {
|
|
22
|
+
normalizedProperties.minClusterSize = minClusterSize;
|
|
23
|
+
}
|
|
24
|
+
if (fullPage !== undefined) {
|
|
25
|
+
normalizedProperties.fullPage = fullPage;
|
|
26
|
+
}
|
|
27
|
+
return normalizedProperties;
|
|
28
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
let execFileAsync = promisify(execFile);
|
|
4
|
+
let KEYCHAIN_SERVICE = 'vizzly-cli';
|
|
5
|
+
export function canUseKeychain(platform = process.platform, env = process.env) {
|
|
6
|
+
return platform === 'darwin' && env.VIZZLY_DISABLE_KEYCHAIN !== 'true';
|
|
7
|
+
}
|
|
8
|
+
export async function saveSecret(account, secret, options = {}) {
|
|
9
|
+
let {
|
|
10
|
+
service = KEYCHAIN_SERVICE,
|
|
11
|
+
platform = process.platform,
|
|
12
|
+
env = process.env,
|
|
13
|
+
execFileFn = execFileAsync
|
|
14
|
+
} = options;
|
|
15
|
+
if (!canUseKeychain(platform, env)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
try {
|
|
20
|
+
await execFileFn('security', ['delete-generic-password', '-s', service, '-a', account]);
|
|
21
|
+
} catch {
|
|
22
|
+
// Missing secrets are fine; add-generic-password creates the current value.
|
|
23
|
+
}
|
|
24
|
+
await execFileFn('security', ['add-generic-password', '-s', service, '-a', account, '-w', secret, '-U']);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function getSecret(account, options = {}) {
|
|
31
|
+
let {
|
|
32
|
+
service = KEYCHAIN_SERVICE,
|
|
33
|
+
platform = process.platform,
|
|
34
|
+
env = process.env,
|
|
35
|
+
execFileFn = execFileAsync
|
|
36
|
+
} = options;
|
|
37
|
+
if (!canUseKeychain(platform, env)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
let {
|
|
42
|
+
stdout
|
|
43
|
+
} = await execFileFn('security', ['find-generic-password', '-s', service, '-a', account, '-w']);
|
|
44
|
+
return stdout.trim() || null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function deleteSecret(account, options = {}) {
|
|
50
|
+
let {
|
|
51
|
+
service = KEYCHAIN_SERVICE,
|
|
52
|
+
platform = process.platform,
|
|
53
|
+
env = process.env,
|
|
54
|
+
execFileFn = execFileAsync
|
|
55
|
+
} = options;
|
|
56
|
+
if (!canUseKeychain(platform, env)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await execFileFn('security', ['delete-generic-password', '-s', service, '-a', account]);
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Visual review
|
|
3
|
+
"version": "0.32.0",
|
|
4
|
+
"description": "Visual review for agents and teams",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"llm-context",
|
|
7
|
+
"visual-context",
|
|
8
|
+
"approved-baselines",
|
|
6
9
|
"visual-testing",
|
|
7
10
|
"screenshot-testing",
|
|
8
|
-
"visual-regression",
|
|
9
11
|
"visual-review",
|
|
10
12
|
"ui-testing",
|
|
11
13
|
"collaboration",
|
|
@@ -51,7 +53,6 @@
|
|
|
51
53
|
"files": [
|
|
52
54
|
"bin",
|
|
53
55
|
"dist",
|
|
54
|
-
"claude-plugin",
|
|
55
56
|
"README.md",
|
|
56
57
|
"LICENSE"
|
|
57
58
|
],
|
|
@@ -71,12 +72,14 @@
|
|
|
71
72
|
"test:watch": "node --test --test-reporter=spec --watch $(find tests -name '*.test.js')",
|
|
72
73
|
"test:reporter": "playwright test --config=tests/reporter/playwright.config.js",
|
|
73
74
|
"test:reporter:visual": "node bin/vizzly.js run \"npm run test:reporter\"",
|
|
75
|
+
"test:swift:e2e": "npm run build && node clients/swift/scripts/run-e2e.js",
|
|
74
76
|
"test:tui": "node --test --test-reporter=spec tests/tui/*.test.js",
|
|
75
77
|
"test:tui:docker": "./tests/tui/run-tui-tests.sh",
|
|
76
|
-
"
|
|
77
|
-
"lint
|
|
78
|
-
"
|
|
79
|
-
"format
|
|
78
|
+
"smoke:prod:pack": "node scripts/smoke-prod-pack.js",
|
|
79
|
+
"lint": "biome check src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
|
|
80
|
+
"lint:fix": "biome check --write --unsafe src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
|
|
81
|
+
"format": "biome format --write src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
|
|
82
|
+
"format:check": "biome format src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
|
|
80
83
|
"fix": "npm run format && npm run lint:fix"
|
|
81
84
|
},
|
|
82
85
|
"engines": {
|
|
@@ -87,13 +90,15 @@
|
|
|
87
90
|
"registry": "https://registry.npmjs.org/"
|
|
88
91
|
},
|
|
89
92
|
"dependencies": {
|
|
90
|
-
"@vizzly-testing/honeydiff": "^0.10.
|
|
93
|
+
"@vizzly-testing/honeydiff": "^0.10.3",
|
|
91
94
|
"ansis": "^4.2.0",
|
|
92
95
|
"commander": "^14.0.0",
|
|
93
96
|
"cosmiconfig": "^9.0.0",
|
|
94
97
|
"dotenv": "^17.2.1",
|
|
95
98
|
"form-data": "^4.0.0",
|
|
96
99
|
"glob": "^13.0.0",
|
|
100
|
+
"react": "^19.1.1",
|
|
101
|
+
"react-dom": "^19.1.1",
|
|
97
102
|
"zod": "^4.1.12"
|
|
98
103
|
},
|
|
99
104
|
"tsd": {
|
|
@@ -125,8 +130,6 @@
|
|
|
125
130
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
126
131
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
|
127
132
|
"postcss": "^8.5.6",
|
|
128
|
-
"react": "^19.1.1",
|
|
129
|
-
"react-dom": "^19.1.1",
|
|
130
133
|
"rimraf": "^6.0.1",
|
|
131
134
|
"tailwindcss": "^4.1.13",
|
|
132
135
|
"tsd": "^0.33.0",
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Screenshot Server Service
|
|
3
|
-
* Listens for and processes screenshots from the test runner
|
|
4
|
-
*
|
|
5
|
-
* This class is a thin wrapper around the functional operations in
|
|
6
|
-
* src/screenshot-server/. It maintains backwards compatibility while
|
|
7
|
-
* delegating to pure functions for testability.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { createServer } from 'node:http';
|
|
11
|
-
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
12
|
-
import { handleRequest, parseRequestBody, startServer, stopServer } from '../screenshot-server/index.js';
|
|
13
|
-
import * as output from '../utils/output.js';
|
|
14
|
-
export class ScreenshotServer {
|
|
15
|
-
constructor(config, buildManager, options = {}) {
|
|
16
|
-
this.config = config;
|
|
17
|
-
this.buildManager = buildManager;
|
|
18
|
-
this.server = null;
|
|
19
|
-
|
|
20
|
-
// Dependency injection for testing
|
|
21
|
-
this.deps = options.deps || {
|
|
22
|
-
createHttpServer: createServer,
|
|
23
|
-
output,
|
|
24
|
-
createError: (message, code) => new VizzlyError(message, code)
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
async start() {
|
|
28
|
-
this.server = await startServer({
|
|
29
|
-
config: this.config,
|
|
30
|
-
requestHandler: this.handleRequest.bind(this),
|
|
31
|
-
deps: {
|
|
32
|
-
createHttpServer: this.deps.createHttpServer,
|
|
33
|
-
createError: this.deps.createError,
|
|
34
|
-
output: this.deps.output
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
async stop() {
|
|
39
|
-
await stopServer({
|
|
40
|
-
server: this.server,
|
|
41
|
-
deps: {
|
|
42
|
-
output: this.deps.output
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
async handleRequest(req, res) {
|
|
47
|
-
await handleRequest({
|
|
48
|
-
req,
|
|
49
|
-
res,
|
|
50
|
-
deps: {
|
|
51
|
-
buildManager: this.buildManager,
|
|
52
|
-
createError: this.deps.createError,
|
|
53
|
-
output: this.deps.output
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
async parseRequestBody(req) {
|
|
58
|
-
return parseRequestBody({
|
|
59
|
-
req,
|
|
60
|
-
deps: {
|
|
61
|
-
createError: this.deps.createError
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server Manager Service
|
|
3
|
-
* Manages the HTTP server with functional handlers
|
|
4
|
-
*
|
|
5
|
-
* This class is a thin wrapper around the functional operations in
|
|
6
|
-
* src/server-manager/. It maintains backwards compatibility while
|
|
7
|
-
* delegating to pure functions for testability.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
11
|
-
import { createApiClient } from '../api/index.js';
|
|
12
|
-
import { createApiHandler } from '../server/handlers/api-handler.js';
|
|
13
|
-
import { createTddHandler } from '../server/handlers/tdd-handler.js';
|
|
14
|
-
import { createHttpServer } from '../server/http-server.js';
|
|
15
|
-
import { buildServerInterface, getTddResults, startServer, stopServer } from '../server-manager/index.js';
|
|
16
|
-
export class ServerManager {
|
|
17
|
-
constructor(config, options = {}) {
|
|
18
|
-
this.config = config;
|
|
19
|
-
this.httpServer = null;
|
|
20
|
-
this.handler = null;
|
|
21
|
-
this.services = options.services || {};
|
|
22
|
-
this.tddMode = false;
|
|
23
|
-
|
|
24
|
-
// Dependency injection for testing - defaults to real implementations
|
|
25
|
-
this.deps = options.deps || {
|
|
26
|
-
createHttpServer,
|
|
27
|
-
createTddHandler,
|
|
28
|
-
createApiHandler,
|
|
29
|
-
createApiClient,
|
|
30
|
-
fs: {
|
|
31
|
-
mkdirSync,
|
|
32
|
-
writeFileSync,
|
|
33
|
-
existsSync,
|
|
34
|
-
unlinkSync
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
async start(buildId = null, tddMode = false, setBaseline = false) {
|
|
39
|
-
this.buildId = buildId;
|
|
40
|
-
this.tddMode = tddMode;
|
|
41
|
-
this.setBaseline = setBaseline;
|
|
42
|
-
let result = await startServer({
|
|
43
|
-
config: this.config,
|
|
44
|
-
buildId,
|
|
45
|
-
tddMode,
|
|
46
|
-
setBaseline,
|
|
47
|
-
projectRoot: process.cwd(),
|
|
48
|
-
services: this.services,
|
|
49
|
-
deps: this.deps
|
|
50
|
-
});
|
|
51
|
-
this.httpServer = result.httpServer;
|
|
52
|
-
this.handler = result.handler;
|
|
53
|
-
}
|
|
54
|
-
async stop() {
|
|
55
|
-
await stopServer({
|
|
56
|
-
httpServer: this.httpServer,
|
|
57
|
-
handler: this.handler,
|
|
58
|
-
projectRoot: process.cwd(),
|
|
59
|
-
deps: this.deps
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Expose server interface for compatibility
|
|
64
|
-
get server() {
|
|
65
|
-
return buildServerInterface({
|
|
66
|
-
handler: this.handler,
|
|
67
|
-
httpServer: this.httpServer
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get TDD results (comparisons, screenshot count, etc.)
|
|
73
|
-
* Only available in TDD mode after tests have run
|
|
74
|
-
*/
|
|
75
|
-
async getTddResults() {
|
|
76
|
-
return getTddResults({
|
|
77
|
-
tddMode: this.tddMode,
|
|
78
|
-
handler: this.handler
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vizzly Screenshot Uploader
|
|
3
|
-
* Handles screenshot uploads to the Vizzly platform
|
|
4
|
-
*
|
|
5
|
-
* This module is a thin wrapper around the functional operations in
|
|
6
|
-
* src/uploader/. It maintains backwards compatibility while
|
|
7
|
-
* delegating to pure functions for testability.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFile, stat } from 'node:fs/promises';
|
|
11
|
-
import { glob } from 'glob';
|
|
12
|
-
import { checkShas, createApiClient, createBuild } from '../api/index.js';
|
|
13
|
-
import { TimeoutError, UploadError, ValidationError } from '../errors/vizzly-error.js';
|
|
14
|
-
import { resolveBatchSize, resolveTimeout } from '../uploader/index.js';
|
|
15
|
-
import { upload as uploadOperation, waitForBuild as waitForBuildOperation } from '../uploader/operations.js';
|
|
16
|
-
import { getDefaultBranch } from '../utils/git.js';
|
|
17
|
-
import * as output from '../utils/output.js';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create a new uploader instance
|
|
21
|
-
*/
|
|
22
|
-
export function createUploader({
|
|
23
|
-
apiKey,
|
|
24
|
-
apiUrl,
|
|
25
|
-
userAgent,
|
|
26
|
-
command,
|
|
27
|
-
upload: uploadConfig = {}
|
|
28
|
-
} = {}, options = {}) {
|
|
29
|
-
let signal = options.signal || new AbortController().signal;
|
|
30
|
-
let client = createApiClient({
|
|
31
|
-
baseUrl: apiUrl,
|
|
32
|
-
token: apiKey,
|
|
33
|
-
command: command || 'upload',
|
|
34
|
-
sdkUserAgent: userAgent,
|
|
35
|
-
allowNoToken: true
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
// Resolve tunable parameters
|
|
39
|
-
let batchSize = resolveBatchSize(options, uploadConfig);
|
|
40
|
-
let timeout = resolveTimeout(options, uploadConfig);
|
|
41
|
-
|
|
42
|
-
// Dependency injection for testing
|
|
43
|
-
let deps = options.deps || {
|
|
44
|
-
client,
|
|
45
|
-
createBuild,
|
|
46
|
-
getDefaultBranch,
|
|
47
|
-
glob,
|
|
48
|
-
readFile,
|
|
49
|
-
stat,
|
|
50
|
-
checkShas,
|
|
51
|
-
createError: (message, code, context) => {
|
|
52
|
-
let error = new UploadError(message, context);
|
|
53
|
-
error.code = code;
|
|
54
|
-
return error;
|
|
55
|
-
},
|
|
56
|
-
createValidationError: (message, context) => new ValidationError(message, context),
|
|
57
|
-
createUploadError: (message, context) => new UploadError(message, context),
|
|
58
|
-
createTimeoutError: (message, context) => new TimeoutError(message, context),
|
|
59
|
-
output
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Upload screenshots to Vizzly
|
|
64
|
-
*/
|
|
65
|
-
async function upload(uploadOptions) {
|
|
66
|
-
return uploadOperation({
|
|
67
|
-
uploadOptions,
|
|
68
|
-
config: {
|
|
69
|
-
apiKey,
|
|
70
|
-
apiUrl
|
|
71
|
-
},
|
|
72
|
-
signal,
|
|
73
|
-
batchSize,
|
|
74
|
-
deps: {
|
|
75
|
-
...deps,
|
|
76
|
-
client: deps.client || client
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Wait for a build to complete
|
|
83
|
-
*/
|
|
84
|
-
async function waitForBuild(buildId, waitTimeout = timeout) {
|
|
85
|
-
return waitForBuildOperation({
|
|
86
|
-
buildId,
|
|
87
|
-
timeout: waitTimeout,
|
|
88
|
-
signal,
|
|
89
|
-
client: deps.client || client,
|
|
90
|
-
deps: {
|
|
91
|
-
createError: deps.createError,
|
|
92
|
-
createTimeoutError: deps.createTimeoutError
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
upload,
|
|
98
|
-
waitForBuild
|
|
99
|
-
};
|
|
100
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Region Service
|
|
3
|
-
*
|
|
4
|
-
* Functions for downloading and managing user-defined hotspot regions from the cloud.
|
|
5
|
-
* Regions are 2D bounding boxes that users have confirmed as dynamic content areas.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { saveRegionMetadata } from '../metadata/region-metadata.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Download user-defined regions from cloud API
|
|
12
|
-
*
|
|
13
|
-
* @param {Object} options
|
|
14
|
-
* @param {Object} options.api - ApiService instance
|
|
15
|
-
* @param {string} options.workingDir - Working directory
|
|
16
|
-
* @param {string[]} options.screenshotNames - Names of screenshots to get regions for
|
|
17
|
-
* @param {boolean} options.includeCandidates - Include candidate regions (default: false)
|
|
18
|
-
* @returns {Promise<{ success: boolean, count: number, regionCount: number, error?: string }>}
|
|
19
|
-
*/
|
|
20
|
-
export async function downloadRegions(options) {
|
|
21
|
-
let {
|
|
22
|
-
api,
|
|
23
|
-
workingDir,
|
|
24
|
-
screenshotNames,
|
|
25
|
-
includeCandidates = false
|
|
26
|
-
} = options;
|
|
27
|
-
if (!screenshotNames || screenshotNames.length === 0) {
|
|
28
|
-
return {
|
|
29
|
-
success: true,
|
|
30
|
-
count: 0,
|
|
31
|
-
regionCount: 0
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
let response = await api.getRegions(screenshotNames, {
|
|
36
|
-
includeCandidates
|
|
37
|
-
});
|
|
38
|
-
if (!response?.regions) {
|
|
39
|
-
return {
|
|
40
|
-
success: false,
|
|
41
|
-
error: 'API returned no region data'
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Save regions to disk
|
|
46
|
-
saveRegionMetadata(workingDir, response.regions, response.summary);
|
|
47
|
-
|
|
48
|
-
// Calculate stats
|
|
49
|
-
let count = Object.keys(response.regions).length;
|
|
50
|
-
let regionCount = response.summary?.total_regions || 0;
|
|
51
|
-
return {
|
|
52
|
-
success: true,
|
|
53
|
-
count,
|
|
54
|
-
regionCount
|
|
55
|
-
};
|
|
56
|
-
} catch (error) {
|
|
57
|
-
return {
|
|
58
|
-
success: false,
|
|
59
|
-
error: error.message
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Extract screenshot names from a list of screenshots
|
|
66
|
-
*
|
|
67
|
-
* @param {Array} screenshots - Screenshots with name property
|
|
68
|
-
* @returns {string[]}
|
|
69
|
-
*/
|
|
70
|
-
export function extractScreenshotNames(screenshots) {
|
|
71
|
-
if (!screenshots || !Array.isArray(screenshots)) {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
return screenshots.map(s => s.name).filter(Boolean);
|
|
75
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Archive a build to history directory
|
|
6
|
-
* @param {string} workingDir - Working directory
|
|
7
|
-
* @param {string} buildId - Build ID to archive
|
|
8
|
-
* @param {Array} builds - Build data
|
|
9
|
-
* @param {Array} comparisons - Comparison data
|
|
10
|
-
* @param {Object} summary - Summary stats
|
|
11
|
-
* @param {number} maxHistory - Maximum number of builds to keep (default: 3)
|
|
12
|
-
*/
|
|
13
|
-
export function archiveBuild(workingDir, buildId, builds, comparisons, summary, maxHistory = 3) {
|
|
14
|
-
const historyDir = join(workingDir, '.vizzly', 'history');
|
|
15
|
-
|
|
16
|
-
// Create history directory if it doesn't exist
|
|
17
|
-
if (!existsSync(historyDir)) {
|
|
18
|
-
mkdirSync(historyDir, {
|
|
19
|
-
recursive: true
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Save current build to history
|
|
24
|
-
const buildDir = join(historyDir, buildId);
|
|
25
|
-
if (!existsSync(buildDir)) {
|
|
26
|
-
mkdirSync(buildDir, {
|
|
27
|
-
recursive: true
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
const buildData = {
|
|
31
|
-
buildId,
|
|
32
|
-
timestamp: Date.now(),
|
|
33
|
-
builds,
|
|
34
|
-
comparisons,
|
|
35
|
-
summary
|
|
36
|
-
};
|
|
37
|
-
writeFileSync(join(buildDir, 'report.json'), JSON.stringify(buildData, null, 2));
|
|
38
|
-
|
|
39
|
-
// Clean up old builds (keep last N)
|
|
40
|
-
cleanupOldBuilds(historyDir, maxHistory);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Get list of archived builds
|
|
45
|
-
* @param {string} workingDir - Working directory
|
|
46
|
-
* @returns {Array} Array of build metadata
|
|
47
|
-
*/
|
|
48
|
-
export function getArchivedBuilds(workingDir) {
|
|
49
|
-
const historyDir = join(workingDir, '.vizzly', 'history');
|
|
50
|
-
if (!existsSync(historyDir)) {
|
|
51
|
-
return [];
|
|
52
|
-
}
|
|
53
|
-
try {
|
|
54
|
-
const buildDirs = readdirSync(historyDir, {
|
|
55
|
-
withFileTypes: true
|
|
56
|
-
}).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name).sort().reverse(); // Newest first
|
|
57
|
-
|
|
58
|
-
return buildDirs.map(buildId => {
|
|
59
|
-
const reportPath = join(historyDir, buildId, 'report.json');
|
|
60
|
-
if (existsSync(reportPath)) {
|
|
61
|
-
try {
|
|
62
|
-
const data = JSON.parse(require('node:fs').readFileSync(reportPath, 'utf8'));
|
|
63
|
-
return {
|
|
64
|
-
buildId: data.buildId,
|
|
65
|
-
timestamp: data.timestamp,
|
|
66
|
-
summary: data.summary
|
|
67
|
-
};
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}).filter(Boolean);
|
|
74
|
-
} catch {
|
|
75
|
-
return [];
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Remove old builds, keeping only the last N
|
|
81
|
-
* @private
|
|
82
|
-
*/
|
|
83
|
-
function cleanupOldBuilds(historyDir, maxHistory) {
|
|
84
|
-
try {
|
|
85
|
-
const buildDirs = readdirSync(historyDir, {
|
|
86
|
-
withFileTypes: true
|
|
87
|
-
}).filter(dirent => dirent.isDirectory()).map(dirent => dirent.name).sort().reverse(); // Newest first
|
|
88
|
-
|
|
89
|
-
// Remove builds beyond maxHistory
|
|
90
|
-
if (buildDirs.length > maxHistory) {
|
|
91
|
-
const toRemove = buildDirs.slice(maxHistory);
|
|
92
|
-
toRemove.forEach(buildId => {
|
|
93
|
-
const buildDir = join(historyDir, buildId);
|
|
94
|
-
rmSync(buildDir, {
|
|
95
|
-
recursive: true,
|
|
96
|
-
force: true
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
// Ignore cleanup errors
|
|
102
|
-
}
|
|
103
|
-
}
|