@vizzly-testing/cli 0.30.0 → 0.31.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/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/hotspot-service.js +0 -13
- package/dist/tdd/tdd-service.js +82 -113
- 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 +11 -8
- 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
package/dist/utils/git.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { getBranch as getCIBranch, getCommit as getCICommit, getCommitMessage as getCICommitMessage, getPullRequestNumber } from './ci-env.js';
|
|
4
|
-
|
|
4
|
+
let execFileAsync = promisify(execFile);
|
|
5
|
+
async function runGit(args, cwd = process.cwd()) {
|
|
6
|
+
let {
|
|
7
|
+
stdout
|
|
8
|
+
} = await execFileAsync('git', args, {
|
|
9
|
+
cwd
|
|
10
|
+
});
|
|
11
|
+
return stdout.trim();
|
|
12
|
+
}
|
|
5
13
|
export async function getCommonAncestor(commit1, commit2, cwd = process.cwd()) {
|
|
6
14
|
try {
|
|
7
|
-
|
|
8
|
-
stdout
|
|
9
|
-
} = await execAsync(`git merge-base ${commit1} ${commit2}`, {
|
|
10
|
-
cwd
|
|
11
|
-
});
|
|
12
|
-
return stdout.trim();
|
|
15
|
+
return await runGit(['merge-base', commit1, commit2], cwd);
|
|
13
16
|
} catch {
|
|
14
17
|
// If merge-base fails (e.g., no common ancestor), return null
|
|
15
18
|
return null;
|
|
@@ -17,24 +20,14 @@ export async function getCommonAncestor(commit1, commit2, cwd = process.cwd()) {
|
|
|
17
20
|
}
|
|
18
21
|
export async function getCurrentCommitSha(cwd = process.cwd()) {
|
|
19
22
|
try {
|
|
20
|
-
|
|
21
|
-
stdout
|
|
22
|
-
} = await execAsync('git rev-parse HEAD', {
|
|
23
|
-
cwd
|
|
24
|
-
});
|
|
25
|
-
return stdout.trim();
|
|
23
|
+
return await runGit(['rev-parse', 'HEAD'], cwd);
|
|
26
24
|
} catch {
|
|
27
25
|
return null;
|
|
28
26
|
}
|
|
29
27
|
}
|
|
30
28
|
export async function getCurrentBranch(cwd = process.cwd()) {
|
|
31
29
|
try {
|
|
32
|
-
|
|
33
|
-
stdout
|
|
34
|
-
} = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
|
35
|
-
cwd
|
|
36
|
-
});
|
|
37
|
-
return stdout.trim();
|
|
30
|
+
return await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
38
31
|
} catch {
|
|
39
32
|
// Fallback strategy: use a simple, non-recursive approach
|
|
40
33
|
// to avoid circular dependency with getDefaultBranch()
|
|
@@ -48,24 +41,18 @@ export async function getCurrentBranch(cwd = process.cwd()) {
|
|
|
48
41
|
*/
|
|
49
42
|
async function getCurrentBranchFallback(cwd = process.cwd()) {
|
|
50
43
|
// Try common default branches in order of likelihood
|
|
51
|
-
|
|
52
|
-
for (
|
|
44
|
+
let commonBranches = ['main', 'master', 'develop', 'dev'];
|
|
45
|
+
for (let branch of commonBranches) {
|
|
53
46
|
try {
|
|
54
|
-
await
|
|
55
|
-
cwd
|
|
56
|
-
});
|
|
47
|
+
await runGit(['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], cwd);
|
|
57
48
|
return branch;
|
|
58
49
|
} catch {}
|
|
59
50
|
}
|
|
60
51
|
|
|
61
52
|
// If none of the common branches exist, try to get any local branch
|
|
62
53
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
} = await execAsync('git branch --format="%(refname:short)"', {
|
|
66
|
-
cwd
|
|
67
|
-
});
|
|
68
|
-
const branches = stdout.trim().split('\n').filter(b => b.trim());
|
|
54
|
+
let stdout = await runGit(['branch', '--format=%(refname:short)'], cwd);
|
|
55
|
+
let branches = stdout.trim().split('\n').filter(b => b.trim());
|
|
69
56
|
if (branches.length > 0) {
|
|
70
57
|
return branches[0]; // Return the first available branch
|
|
71
58
|
}
|
|
@@ -80,35 +67,22 @@ async function getCurrentBranchFallback(cwd = process.cwd()) {
|
|
|
80
67
|
export async function getDefaultBranch(cwd = process.cwd()) {
|
|
81
68
|
try {
|
|
82
69
|
// Try to get the default branch from remote origin
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
} = await execAsync('git symbolic-ref refs/remotes/origin/HEAD', {
|
|
86
|
-
cwd
|
|
87
|
-
});
|
|
88
|
-
const defaultBranch = stdout.trim().replace('refs/remotes/origin/', '');
|
|
70
|
+
let stdout = await runGit(['symbolic-ref', 'refs/remotes/origin/HEAD'], cwd);
|
|
71
|
+
let defaultBranch = stdout.replace('refs/remotes/origin/', '');
|
|
89
72
|
return defaultBranch;
|
|
90
73
|
} catch {
|
|
91
74
|
try {
|
|
92
75
|
// Fallback: try to get default branch from git config
|
|
93
|
-
|
|
94
|
-
stdout
|
|
95
|
-
} = await execAsync('git config --get init.defaultBranch', {
|
|
96
|
-
cwd
|
|
97
|
-
});
|
|
98
|
-
return stdout.trim();
|
|
76
|
+
return await runGit(['config', '--get', 'init.defaultBranch'], cwd);
|
|
99
77
|
} catch {
|
|
100
78
|
try {
|
|
101
79
|
// Fallback: check if main exists
|
|
102
|
-
await
|
|
103
|
-
cwd
|
|
104
|
-
});
|
|
80
|
+
await runGit(['show-ref', '--verify', '--quiet', 'refs/heads/main'], cwd);
|
|
105
81
|
return 'main';
|
|
106
82
|
} catch {
|
|
107
83
|
try {
|
|
108
84
|
// Fallback: check if master exists
|
|
109
|
-
await
|
|
110
|
-
cwd
|
|
111
|
-
});
|
|
85
|
+
await runGit(['show-ref', '--verify', '--quiet', 'refs/heads/master'], cwd);
|
|
112
86
|
return 'master';
|
|
113
87
|
} catch {
|
|
114
88
|
// If we're not in a git repo or no branches exist, return null
|
|
@@ -120,7 +94,7 @@ export async function getDefaultBranch(cwd = process.cwd()) {
|
|
|
120
94
|
}
|
|
121
95
|
}
|
|
122
96
|
export function generateBuildName() {
|
|
123
|
-
|
|
97
|
+
let timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
124
98
|
return `Build ${timestamp}`;
|
|
125
99
|
}
|
|
126
100
|
|
|
@@ -131,12 +105,7 @@ export function generateBuildName() {
|
|
|
131
105
|
*/
|
|
132
106
|
export async function getCommitMessage(cwd = process.cwd()) {
|
|
133
107
|
try {
|
|
134
|
-
|
|
135
|
-
stdout
|
|
136
|
-
} = await execAsync('git log -1 --pretty=%B', {
|
|
137
|
-
cwd
|
|
138
|
-
});
|
|
139
|
-
return stdout.trim();
|
|
108
|
+
return await runGit(['log', '-1', '--pretty=%B'], cwd);
|
|
140
109
|
} catch {
|
|
141
110
|
return null;
|
|
142
111
|
}
|
|
@@ -152,7 +121,7 @@ export async function detectCommitMessage(override = null, cwd = process.cwd())
|
|
|
152
121
|
if (override) return override;
|
|
153
122
|
|
|
154
123
|
// Try CI environment variables first
|
|
155
|
-
|
|
124
|
+
let ciCommitMessage = getCICommitMessage();
|
|
156
125
|
if (ciCommitMessage) return ciCommitMessage;
|
|
157
126
|
|
|
158
127
|
// Fallback to regular git log
|
|
@@ -166,9 +135,7 @@ export async function detectCommitMessage(override = null, cwd = process.cwd())
|
|
|
166
135
|
*/
|
|
167
136
|
export async function isGitRepository(cwd = process.cwd()) {
|
|
168
137
|
try {
|
|
169
|
-
await
|
|
170
|
-
cwd
|
|
171
|
-
});
|
|
138
|
+
await runGit(['rev-parse', '--git-dir'], cwd);
|
|
172
139
|
return true;
|
|
173
140
|
} catch {
|
|
174
141
|
return false;
|
|
@@ -182,12 +149,8 @@ export async function isGitRepository(cwd = process.cwd()) {
|
|
|
182
149
|
*/
|
|
183
150
|
export async function getGitStatus(cwd = process.cwd()) {
|
|
184
151
|
try {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
} = await execAsync('git status --porcelain', {
|
|
188
|
-
cwd
|
|
189
|
-
});
|
|
190
|
-
const changes = stdout.trim().split('\n').filter(line => line);
|
|
152
|
+
let stdout = await runGit(['status', '--porcelain'], cwd);
|
|
153
|
+
let changes = stdout.trim().split('\n').filter(line => line);
|
|
191
154
|
return {
|
|
192
155
|
hasChanges: changes.length > 0,
|
|
193
156
|
changes: changes.map(line => ({
|
|
@@ -210,11 +173,11 @@ export async function detectBranch(override = null, cwd = process.cwd()) {
|
|
|
210
173
|
if (override) return override;
|
|
211
174
|
|
|
212
175
|
// Try CI environment variables first
|
|
213
|
-
|
|
176
|
+
let ciBranch = getCIBranch();
|
|
214
177
|
if (ciBranch) return ciBranch;
|
|
215
178
|
|
|
216
179
|
// Fallback to git command when no CI environment variables
|
|
217
|
-
|
|
180
|
+
let currentBranch = await getCurrentBranch(cwd);
|
|
218
181
|
return currentBranch || 'unknown';
|
|
219
182
|
}
|
|
220
183
|
|
|
@@ -228,7 +191,7 @@ export async function detectCommit(override = null, cwd = process.cwd()) {
|
|
|
228
191
|
if (override) return override;
|
|
229
192
|
|
|
230
193
|
// Try CI environment variables first
|
|
231
|
-
|
|
194
|
+
let ciCommit = getCICommit();
|
|
232
195
|
if (ciCommit) return ciCommit;
|
|
233
196
|
|
|
234
197
|
// Fallback to git command when no CI environment variables
|
|
@@ -243,10 +206,10 @@ export async function detectCommit(override = null, cwd = process.cwd()) {
|
|
|
243
206
|
*/
|
|
244
207
|
export async function generateBuildNameWithGit(override = null, cwd = process.cwd()) {
|
|
245
208
|
if (override) return override;
|
|
246
|
-
|
|
247
|
-
|
|
209
|
+
let branch = await getCurrentBranch(cwd);
|
|
210
|
+
let shortSha = await getCurrentCommitSha(cwd);
|
|
248
211
|
if (branch && shortSha) {
|
|
249
|
-
|
|
212
|
+
let shortCommit = shortSha.substring(0, 7);
|
|
250
213
|
return `${branch}-${shortCommit}`;
|
|
251
214
|
}
|
|
252
215
|
return generateBuildName();
|
|
@@ -8,6 +8,25 @@ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import * as output from './output.js';
|
|
11
|
+
export function expandHomePath(path) {
|
|
12
|
+
if (!path) {
|
|
13
|
+
return path;
|
|
14
|
+
}
|
|
15
|
+
let home = homedir();
|
|
16
|
+
if (path === '$HOME') {
|
|
17
|
+
return home;
|
|
18
|
+
}
|
|
19
|
+
if (path.startsWith('$HOME/')) {
|
|
20
|
+
return join(home, path.slice('$HOME/'.length));
|
|
21
|
+
}
|
|
22
|
+
if (path === '~') {
|
|
23
|
+
return home;
|
|
24
|
+
}
|
|
25
|
+
if (path.startsWith('~/')) {
|
|
26
|
+
return join(home, path.slice(2));
|
|
27
|
+
}
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
11
30
|
|
|
12
31
|
/**
|
|
13
32
|
* Get the path to the global Vizzly directory
|
|
@@ -15,7 +34,7 @@ import * as output from './output.js';
|
|
|
15
34
|
*/
|
|
16
35
|
export function getGlobalConfigDir() {
|
|
17
36
|
if (process.env.VIZZLY_HOME) {
|
|
18
|
-
return process.env.VIZZLY_HOME;
|
|
37
|
+
return expandHomePath(process.env.VIZZLY_HOME);
|
|
19
38
|
}
|
|
20
39
|
return join(homedir(), '.vizzly');
|
|
21
40
|
}
|
package/dist/utils/output.js
CHANGED
|
@@ -603,15 +603,27 @@ function getComponentColor(component) {
|
|
|
603
603
|
};
|
|
604
604
|
return componentColors[component] || colors.brand.info;
|
|
605
605
|
}
|
|
606
|
-
export function
|
|
607
|
-
if (!shouldLog('debug')) return;
|
|
608
|
-
|
|
609
|
-
// Handle legacy calls: debug('message') or debug('message', {data})
|
|
606
|
+
export function normalizeDebugArgs(component, message, data = {}) {
|
|
610
607
|
if (typeof message === 'object' || message === undefined) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
608
|
+
return {
|
|
609
|
+
component: null,
|
|
610
|
+
message: component,
|
|
611
|
+
data: message || {}
|
|
612
|
+
};
|
|
614
613
|
}
|
|
614
|
+
return {
|
|
615
|
+
component,
|
|
616
|
+
message,
|
|
617
|
+
data
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
export function debug(component, message, data = {}) {
|
|
621
|
+
if (!shouldLog('debug')) return;
|
|
622
|
+
({
|
|
623
|
+
component,
|
|
624
|
+
message,
|
|
625
|
+
data
|
|
626
|
+
} = normalizeDebugArgs(component, message, data));
|
|
615
627
|
let elapsed = getElapsedTime();
|
|
616
628
|
if (config.json) {
|
|
617
629
|
console.error(JSON.stringify({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function escapeRegExp(value) {
|
|
2
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3
|
+
}
|
|
4
|
+
export function createWildcardMatcher(pattern, options = {}) {
|
|
5
|
+
let {
|
|
6
|
+
anchored = false
|
|
7
|
+
} = options;
|
|
8
|
+
let regexPattern = String(pattern).split('*').map(escapeRegExp).join('.*');
|
|
9
|
+
if (anchored) {
|
|
10
|
+
regexPattern = `^${regexPattern}$`;
|
|
11
|
+
}
|
|
12
|
+
let regex = new RegExp(regexPattern, 'i');
|
|
13
|
+
return value => regex.test(String(value));
|
|
14
|
+
}
|
|
@@ -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.31.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": {
|
|
@@ -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
|
-
}
|