@vizzly-testing/cli 0.38.1 → 0.39.1
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 +222 -195
- package/dist/cli.js +55 -54
- package/dist/commands/init.js +154 -483
- package/dist/commands/project.js +128 -44
- package/dist/commands/run.js +2 -2
- package/dist/commands/tdd.js +89 -38
- package/dist/commands/upload.js +2 -2
- package/dist/init/agents.js +140 -0
- package/dist/init/core.js +31 -0
- package/dist/init/prompts.js +44 -0
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +30 -61
- package/dist/reporter-ssr/ssr-entry.js +3 -6
- package/dist/server/handlers/tdd-handler.js +33 -2
- package/dist/services/index.js +8 -2
- package/dist/tdd/services/comparison-service.js +4 -1
- package/dist/tdd/tdd-service.js +14 -2
- package/dist/test-runner/core.js +28 -3
- package/dist/test-runner/operations.js +41 -20
- package/dist/types/index.d.ts +8 -0
- package/dist/uploader/core.js +1 -0
- package/dist/utils/cli-update.js +105 -0
- package/dist/utils/config-discovery.js +73 -0
- package/dist/utils/config-loader.js +29 -8
- package/dist/utils/config-schema.js +5 -0
- package/dist/utils/file-changes.js +105 -0
- package/dist/utils/project-binding.js +96 -0
- package/dist/utils/project-link-store.js +13 -7
- package/dist/utils/update-check.js +2 -0
- package/dist/utils/update-notifications.js +175 -0
- package/package.json +16 -16
- package/skills/vizzly/SKILL.md +4 -3
- package/skills/vizzly/references/ci.md +33 -0
- package/skills/vizzly/references/cli-context.md +81 -71
- package/skills/vizzly/references/dynamic-content.md +16 -17
- package/skills/vizzly/references/sdks.md +22 -22
- package/skills/vizzly/references/setup-ci.md +0 -44
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access, lstat, mkdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { readOptionalFile } from './config-discovery.js';
|
|
6
|
+
export async function fileChange(filename, after) {
|
|
7
|
+
let before = await readOptionalFile(filename);
|
|
8
|
+
return before === after ? null : {
|
|
9
|
+
path: filename,
|
|
10
|
+
before,
|
|
11
|
+
after
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function writableParent(filename) {
|
|
15
|
+
let parent = path.dirname(filename);
|
|
16
|
+
while (true) {
|
|
17
|
+
try {
|
|
18
|
+
await access(parent, constants.W_OK);
|
|
19
|
+
return;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code !== 'ENOENT' || path.dirname(parent) === parent) {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
parent = path.dirname(parent);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function checkChange(change) {
|
|
29
|
+
// Reject redirects in parent folders too, including explicit config paths.
|
|
30
|
+
// Top-level OS aliases such as /tmp on macOS are safe to follow.
|
|
31
|
+
for (let parent = path.dirname(change.path); path.dirname(parent) !== parent; parent = path.dirname(parent)) {
|
|
32
|
+
try {
|
|
33
|
+
if (path.dirname(parent) !== path.parse(parent).root && (await lstat(parent)).isSymbolicLink()) {
|
|
34
|
+
throw new Error(`Setup will not write through a symlink: ${parent}`);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code !== 'ENOENT') throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let info;
|
|
41
|
+
try {
|
|
42
|
+
info = await lstat(change.path);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.code !== 'ENOENT') throw error;
|
|
45
|
+
}
|
|
46
|
+
if (info?.isSymbolicLink()) {
|
|
47
|
+
throw new Error(`Setup will not replace a symlink: ${change.path}`);
|
|
48
|
+
}
|
|
49
|
+
let current = await readOptionalFile(change.path);
|
|
50
|
+
if (current !== change.before) {
|
|
51
|
+
throw new Error(`${change.path} changed during setup; run init again.`);
|
|
52
|
+
}
|
|
53
|
+
if (current !== null) {
|
|
54
|
+
if (!info.isFile()) {
|
|
55
|
+
throw new Error(`Setup needs a regular file: ${change.path}`);
|
|
56
|
+
}
|
|
57
|
+
await access(change.path, constants.W_OK);
|
|
58
|
+
}
|
|
59
|
+
await writableParent(change.path);
|
|
60
|
+
}
|
|
61
|
+
export async function preflightChanges(changes) {
|
|
62
|
+
for (let change of changes.filter(Boolean)) await checkChange(change);
|
|
63
|
+
}
|
|
64
|
+
export async function applyChanges(changes) {
|
|
65
|
+
let planned = changes.filter(Boolean);
|
|
66
|
+
await preflightChanges(planned);
|
|
67
|
+
let completed = [];
|
|
68
|
+
try {
|
|
69
|
+
for (let change of planned) {
|
|
70
|
+
await checkChange(change);
|
|
71
|
+
await mkdir(path.dirname(change.path), {
|
|
72
|
+
recursive: true
|
|
73
|
+
});
|
|
74
|
+
if (change.before === null) {
|
|
75
|
+
await writeFile(change.path, change.after, {
|
|
76
|
+
flag: 'wx'
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
let temporary = `${change.path}.${randomUUID()}.tmp`;
|
|
80
|
+
try {
|
|
81
|
+
let {
|
|
82
|
+
mode
|
|
83
|
+
} = await lstat(change.path);
|
|
84
|
+
await writeFile(temporary, change.after, {
|
|
85
|
+
flag: 'wx',
|
|
86
|
+
mode
|
|
87
|
+
});
|
|
88
|
+
await rename(temporary, change.path);
|
|
89
|
+
} finally {
|
|
90
|
+
await rm(temporary, {
|
|
91
|
+
force: true
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
completed.push({
|
|
96
|
+
path: change.path,
|
|
97
|
+
status: 'written'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
error.completed = completed;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
return completed;
|
|
105
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
3
|
+
import { discoverConfig } from './config-discovery.js';
|
|
4
|
+
import { applyChanges } from './file-changes.js';
|
|
5
|
+
function sameProject(left, right) {
|
|
6
|
+
return left?.organizationSlug === right.organizationSlug && left?.projectSlug === right.projectSlug;
|
|
7
|
+
}
|
|
8
|
+
function bindingValues(project, apiUrl) {
|
|
9
|
+
return {
|
|
10
|
+
...(apiUrl !== 'https://app.vizzly.dev' ? {
|
|
11
|
+
apiUrl
|
|
12
|
+
} : {}),
|
|
13
|
+
project
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function javascriptBinding(values) {
|
|
17
|
+
return `export default ${JSON.stringify(values, null, 2)};\n`;
|
|
18
|
+
}
|
|
19
|
+
export async function planProjectBinding({
|
|
20
|
+
cwd = process.cwd(),
|
|
21
|
+
configPath = null,
|
|
22
|
+
project,
|
|
23
|
+
apiUrl = 'https://app.vizzly.dev',
|
|
24
|
+
currentProject,
|
|
25
|
+
currentApiUrl
|
|
26
|
+
}) {
|
|
27
|
+
let config = await discoverConfig({
|
|
28
|
+
cwd,
|
|
29
|
+
configPath
|
|
30
|
+
});
|
|
31
|
+
let filename = config.configPath || path.join(cwd, 'vizzly.config.js');
|
|
32
|
+
let values = bindingValues(project, apiUrl);
|
|
33
|
+
let plan = {
|
|
34
|
+
status: 'ready',
|
|
35
|
+
configPath: filename,
|
|
36
|
+
before: config.source,
|
|
37
|
+
after: null
|
|
38
|
+
};
|
|
39
|
+
if (config.exists && sameProject(currentProject, project) && currentApiUrl === apiUrl) {
|
|
40
|
+
return plan;
|
|
41
|
+
}
|
|
42
|
+
if (!config.exists) {
|
|
43
|
+
if (['javascript', 'typescript'].includes(config.format)) {
|
|
44
|
+
plan.after = javascriptBinding(values);
|
|
45
|
+
} else if (config.format === 'json') {
|
|
46
|
+
plan.after = `${JSON.stringify(values, null, 2)}\n`;
|
|
47
|
+
} else {
|
|
48
|
+
throw new VizzlyError(`Cannot create a project binding in ${filename}. Use a JavaScript, TypeScript, or JSON config.`, 'PROJECT_CONFIG_FORMAT');
|
|
49
|
+
}
|
|
50
|
+
return plan;
|
|
51
|
+
}
|
|
52
|
+
if (config.format === 'package' || config.format === 'json') {
|
|
53
|
+
let document = JSON.parse(config.source);
|
|
54
|
+
let existing = config.format === 'package' ? document.vizzly || {} : document;
|
|
55
|
+
let updated = {
|
|
56
|
+
...existing,
|
|
57
|
+
...values
|
|
58
|
+
};
|
|
59
|
+
if (apiUrl === 'https://app.vizzly.dev' && existing.apiUrl) {
|
|
60
|
+
updated.apiUrl = apiUrl;
|
|
61
|
+
}
|
|
62
|
+
let result = config.format === 'package' ? {
|
|
63
|
+
...document,
|
|
64
|
+
vizzly: updated
|
|
65
|
+
} : updated;
|
|
66
|
+
let indent = config.source.match(/\n([ \t]+)\S/)?.[1] || ' ';
|
|
67
|
+
plan.after = `${JSON.stringify(result, null, indent)}\n`;
|
|
68
|
+
return plan;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A freshly scaffolded config contains only comments. Add the binding without
|
|
72
|
+
// interpreting or rewriting executable configuration supplied by the user.
|
|
73
|
+
let comments = String.raw`(?:\s|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*`;
|
|
74
|
+
let emptyModule = new RegExp(`^${comments}export\\s+default\\s*\\{${comments}\\}\\s*;?\\s*$`);
|
|
75
|
+
if (['javascript', 'typescript'].includes(config.format) && emptyModule.test(config.source)) {
|
|
76
|
+
let propertyText = JSON.stringify(values, null, 2).slice(1, -1);
|
|
77
|
+
let closingBrace = config.source.lastIndexOf('}');
|
|
78
|
+
plan.after = `${config.source.slice(0, closingBrace).trimEnd()}\n${propertyText}\n${config.source.slice(closingBrace)}`;
|
|
79
|
+
return plan;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
...plan,
|
|
83
|
+
status: 'manual',
|
|
84
|
+
instructions: `Add these fields to ${filename}, then run project link again:\n${JSON.stringify(values, null, 2)}`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function writeProjectBinding(plan) {
|
|
88
|
+
if (plan.status !== 'ready' || plan.after === null) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
await applyChanges([{
|
|
92
|
+
path: plan.configPath,
|
|
93
|
+
before: plan.before,
|
|
94
|
+
after: plan.after
|
|
95
|
+
}]);
|
|
96
|
+
}
|
|
@@ -49,25 +49,31 @@ export async function saveProjectLink(link, deps = {}) {
|
|
|
49
49
|
token: link.token
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
export async function
|
|
52
|
+
export async function getProjectLink(options = {}, deps = {}) {
|
|
53
53
|
let {
|
|
54
54
|
loadConfig = loadGlobalConfig,
|
|
55
|
-
getSecret = defaultGetSecret
|
|
55
|
+
getSecret = defaultGetSecret,
|
|
56
|
+
now = Date.now
|
|
56
57
|
} = deps;
|
|
58
|
+
if (!options.account && !(options.organizationSlug && options.projectSlug)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
57
61
|
let config = await loadConfig();
|
|
58
62
|
let projectLink = getProjectLinkConfig(config);
|
|
59
|
-
let account = options.account ||
|
|
63
|
+
let account = options.account || buildProjectLinkAccount(options);
|
|
60
64
|
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
65
|
if (!link) {
|
|
66
66
|
return null;
|
|
67
67
|
}
|
|
68
68
|
if (options.apiUrl && link.apiUrl && link.apiUrl !== options.apiUrl) {
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
|
+
if (link.expiresAt) {
|
|
72
|
+
let expiresAt = Date.parse(link.expiresAt);
|
|
73
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now()) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
71
77
|
let token = link.storage === 'keychain' ? await getSecret(account) : link.token;
|
|
72
78
|
if (!token) {
|
|
73
79
|
return null;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { agentGuidanceFlag, agentGuidancePath, bundledAgentSkill, planAgentSetup } from '../init/agents.js';
|
|
5
|
+
import { createPrompts } from '../init/prompts.js';
|
|
6
|
+
import { findCliUpdatePlan, runUpdateProcess } from './cli-update.js';
|
|
7
|
+
import { readOptionalFile } from './config-discovery.js';
|
|
8
|
+
import { applyChanges, fileChange, preflightChanges } from './file-changes.js';
|
|
9
|
+
import { getGlobalConfigDir } from './global-config.js';
|
|
10
|
+
import * as output from './output.js';
|
|
11
|
+
import { getPackageName, getPackageVersion } from './package-info.js';
|
|
12
|
+
let day = 24 * 60 * 60 * 1000;
|
|
13
|
+
async function readState(filename) {
|
|
14
|
+
try {
|
|
15
|
+
let state = JSON.parse(await readOptionalFile(filename));
|
|
16
|
+
return state && typeof state === 'object' && !Array.isArray(state) ? state : {};
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function stateChange(filename, state) {
|
|
22
|
+
return fileChange(filename, `${JSON.stringify(state, null, 2)}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The registry's latest tag contains stable releases. Never suggest a downgrade
|
|
26
|
+
// or a prerelease, even when an unexpected registry response is cached.
|
|
27
|
+
export function isNewerStableVersion(candidate, current) {
|
|
28
|
+
let latest = /^(\d+)\.(\d+)\.(\d+)$/.exec(candidate);
|
|
29
|
+
let installed = /^(\d+)\.(\d+)\.(\d+)(-[\w.-]+)?(?:\+[\w.-]+)?$/.exec(current);
|
|
30
|
+
if (!latest || !installed) return false;
|
|
31
|
+
for (let index = 1; index <= 3; index++) {
|
|
32
|
+
let difference = Number(latest[index]) - Number(installed[index]);
|
|
33
|
+
if (difference) return difference > 0;
|
|
34
|
+
}
|
|
35
|
+
return Boolean(installed[4]);
|
|
36
|
+
}
|
|
37
|
+
export async function checkForUpdate() {
|
|
38
|
+
try {
|
|
39
|
+
let filename = path.join(getGlobalConfigDir(), 'updates.json');
|
|
40
|
+
let response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(getPackageName())}/latest`, {
|
|
41
|
+
signal: AbortSignal.timeout(3000)
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) return;
|
|
44
|
+
let {
|
|
45
|
+
version
|
|
46
|
+
} = await response.json();
|
|
47
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) return;
|
|
48
|
+
// Keep notice history saved while the network request was in flight.
|
|
49
|
+
let state = await readState(filename);
|
|
50
|
+
await applyChanges([await stateChange(filename, {
|
|
51
|
+
...state,
|
|
52
|
+
latestVersion: version
|
|
53
|
+
})]);
|
|
54
|
+
} catch {
|
|
55
|
+
// Update checks never affect the command being run.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function offerUpdate(cwd, {
|
|
59
|
+
cliVersion,
|
|
60
|
+
guidanceFlag
|
|
61
|
+
}) {
|
|
62
|
+
if (!process.stdin.isTTY) return;
|
|
63
|
+
let cliPlan = cliVersion ? await findCliUpdatePlan(cwd, cliVersion) : null;
|
|
64
|
+
if (!cliPlan && !guidanceFlag) return;
|
|
65
|
+
let updatedCli;
|
|
66
|
+
try {
|
|
67
|
+
let guidancePlan;
|
|
68
|
+
if (!cliPlan && guidanceFlag) {
|
|
69
|
+
guidancePlan = await planAgentSetup(cwd, {
|
|
70
|
+
skill: true,
|
|
71
|
+
guidance: guidanceFlag === '--agent-guidance'
|
|
72
|
+
});
|
|
73
|
+
await preflightChanges(guidancePlan.changes);
|
|
74
|
+
}
|
|
75
|
+
if (cliPlan) {
|
|
76
|
+
output.printErr(` Apply: ${cliPlan.command} ${cliPlan.args.join(' ')} (in ${cliPlan.cwd})`);
|
|
77
|
+
// A CLI upgrade can bring new guidance even if the current bundle matches.
|
|
78
|
+
if (!guidanceFlag && (await readOptionalFile(path.join(cwd, '.agents', 'skills', 'vizzly', 'SKILL.md')))) {
|
|
79
|
+
guidanceFlag = await agentGuidanceFlag(cwd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (guidanceFlag) {
|
|
83
|
+
output.printErr(` Refresh: vizzly init ${guidanceFlag} (replaces bundled skill files${guidanceFlag === '--agent-guidance' ? ' and the Vizzly AGENTS.md note' : ''})`);
|
|
84
|
+
}
|
|
85
|
+
if (!(await confirmUpdate())) return;
|
|
86
|
+
if (cliPlan) {
|
|
87
|
+
await runUpdateProcess(cliPlan.command, cliPlan.args, cliPlan.cwd);
|
|
88
|
+
updatedCli = cliPlan.cliPath;
|
|
89
|
+
if (guidanceFlag) {
|
|
90
|
+
await runUpdateProcess(process.execPath, [cliPlan.cliPath, 'init', guidanceFlag, '--no-update-notifier'], cwd);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
await applyChanges(guidancePlan.changes);
|
|
94
|
+
}
|
|
95
|
+
output.printErr(' Vizzly update applied.');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (error.code !== 'INIT_CANCELLED') output.printErr(` Could not apply the Vizzly update: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
return updatedCli;
|
|
100
|
+
}
|
|
101
|
+
async function confirmUpdate() {
|
|
102
|
+
let prompts = createPrompts();
|
|
103
|
+
try {
|
|
104
|
+
return await prompts.confirm('Apply available Vizzly updates now?');
|
|
105
|
+
} finally {
|
|
106
|
+
prompts.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function planGuidanceNotice(cwd, cliVersion) {
|
|
110
|
+
let installed = await readOptionalFile(path.join(cwd, '.agents', 'skills', 'vizzly', 'SKILL.md'));
|
|
111
|
+
let guidance = await readState(agentGuidancePath(cwd));
|
|
112
|
+
if (!installed || isNewerStableVersion(guidance.cliVersion, cliVersion)) return null;
|
|
113
|
+
let {
|
|
114
|
+
version,
|
|
115
|
+
files
|
|
116
|
+
} = await bundledAgentSkill();
|
|
117
|
+
if (!guidance.version) {
|
|
118
|
+
let contents = await Promise.all(files.map(({
|
|
119
|
+
filename
|
|
120
|
+
}) => readOptionalFile(path.join(cwd, '.agents', 'skills', 'vizzly', filename))));
|
|
121
|
+
if (files.every(({
|
|
122
|
+
content
|
|
123
|
+
}, index) => content === contents[index])) guidance.version = version;
|
|
124
|
+
}
|
|
125
|
+
if (guidance.version === version || guidance.notifiedVersion === version) return null;
|
|
126
|
+
let flag = await agentGuidanceFlag(cwd);
|
|
127
|
+
return {
|
|
128
|
+
flag,
|
|
129
|
+
message: `Vizzly agent guidance has an update. Run vizzly init ${flag} to refresh it.`,
|
|
130
|
+
change: await stateChange(agentGuidancePath(cwd), {
|
|
131
|
+
...guidance,
|
|
132
|
+
notifiedVersion: version
|
|
133
|
+
})
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function startUpdateCheck() {
|
|
137
|
+
output.printErr(' Checking for Vizzly updates in the background…');
|
|
138
|
+
let child = spawn(process.execPath, [fileURLToPath(new URL('./update-check.js', import.meta.url))], {
|
|
139
|
+
detached: true,
|
|
140
|
+
stdio: 'ignore'
|
|
141
|
+
});
|
|
142
|
+
child.on('error', () => {});
|
|
143
|
+
child.unref();
|
|
144
|
+
}
|
|
145
|
+
export async function notifyUpdates(options = {}) {
|
|
146
|
+
if (options.updateNotifier === false || options.json || output.isJson() || !process.stderr.isTTY || !process.stdout.isTTY || process.env.CI || 'NO_UPDATE_NOTIFIER' in process.env || 'VIZZLY_NO_UPDATE_NOTIFIER' in process.env || !['info', 'debug'].includes(output.getLogLevel())) return;
|
|
147
|
+
try {
|
|
148
|
+
let cwd = process.cwd();
|
|
149
|
+
let filename = path.join(getGlobalConfigDir(), 'updates.json');
|
|
150
|
+
let state = await readState(filename);
|
|
151
|
+
let messages = [];
|
|
152
|
+
let available = {};
|
|
153
|
+
let cliVersion = getPackageVersion();
|
|
154
|
+
if (isNewerStableVersion(state.latestVersion, cliVersion) && state.notifiedCliVersion !== state.latestVersion) {
|
|
155
|
+
messages.push(`Vizzly ${state.latestVersion} is available (installed: ${cliVersion}). Update ${getPackageName()} with the package manager that installed it.`);
|
|
156
|
+
state.notifiedCliVersion = state.latestVersion;
|
|
157
|
+
available.cliVersion = state.latestVersion;
|
|
158
|
+
}
|
|
159
|
+
let guidanceNotice = await planGuidanceNotice(cwd, cliVersion);
|
|
160
|
+
if (guidanceNotice) {
|
|
161
|
+
messages.push(guidanceNotice.message);
|
|
162
|
+
available.guidanceFlag = guidanceNotice.flag;
|
|
163
|
+
}
|
|
164
|
+
let checkDue = !Number.isFinite(state.lastCheckedAt) || state.lastCheckedAt > Date.now() || Date.now() - state.lastCheckedAt >= day;
|
|
165
|
+
if (checkDue) state.lastCheckedAt = Date.now();
|
|
166
|
+
// If state cannot be saved, stay quiet instead of repeating a notice on
|
|
167
|
+
// every invocation. Setup's file preflight also protects against symlinks.
|
|
168
|
+
await applyChanges([guidanceNotice?.change, await stateChange(filename, state)]);
|
|
169
|
+
for (let message of messages) output.printErr(` ${message}`);
|
|
170
|
+
if (checkDue) startUpdateCheck();
|
|
171
|
+
return await offerUpdate(cwd, available);
|
|
172
|
+
} catch {
|
|
173
|
+
// Missing, malformed, or unwritable metadata must not break CLI commands.
|
|
174
|
+
}
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vizzly-testing/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"description": "Visual regression testing from your terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm-context",
|
|
@@ -99,12 +99,12 @@
|
|
|
99
99
|
"ansis": "^4.4.0",
|
|
100
100
|
"commander": "^15.0.0",
|
|
101
101
|
"cosmiconfig": "^10.0.1",
|
|
102
|
-
"dotenv": "^
|
|
102
|
+
"dotenv": "^18.0.9",
|
|
103
103
|
"form-data": "^4.0.6",
|
|
104
104
|
"glob": "^13.0.6",
|
|
105
|
-
"react": "^19.3.
|
|
106
|
-
"react-dom": "^19.3.
|
|
107
|
-
"zod": "^4.6.
|
|
105
|
+
"react": "^19.3.1",
|
|
106
|
+
"react-dom": "^19.3.1",
|
|
107
|
+
"zod": "^4.6.7"
|
|
108
108
|
},
|
|
109
109
|
"tsd": {
|
|
110
110
|
"directory": "test-d",
|
|
@@ -118,28 +118,28 @@
|
|
|
118
118
|
},
|
|
119
119
|
"devDependencies": {
|
|
120
120
|
"@babel/cli": "^8.0.6",
|
|
121
|
-
"@babel/core": "^8.0.
|
|
122
|
-
"@babel/preset-env": "^8.0.
|
|
121
|
+
"@babel/core": "^8.0.7",
|
|
122
|
+
"@babel/preset-env": "^8.0.7",
|
|
123
123
|
"@babel/preset-react": "^8.0.1",
|
|
124
|
-
"@babel/preset-typescript": "^8.0.
|
|
125
|
-
"@biomejs/biome": "^2.
|
|
124
|
+
"@babel/preset-typescript": "^8.0.7",
|
|
125
|
+
"@biomejs/biome": "^2.6.2",
|
|
126
126
|
"@heroicons/react": "^2.2.0",
|
|
127
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
128
|
-
"@playwright/test": "^1.
|
|
127
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
128
|
+
"@playwright/test": "^1.64.1",
|
|
129
129
|
"@tailwindcss/postcss": "^4.3.3",
|
|
130
|
-
"@tanstack/react-query": "^5.104.
|
|
131
|
-
"@types/node": "^26.6.
|
|
130
|
+
"@tanstack/react-query": "^5.104.5",
|
|
131
|
+
"@types/node": "^26.6.5",
|
|
132
132
|
"@vitejs/plugin-react": "^6.1.2",
|
|
133
133
|
"autoprefixer": "^10.6.1",
|
|
134
134
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
135
135
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
|
136
|
-
"postcss": "^8.5.
|
|
136
|
+
"postcss": "^8.5.31",
|
|
137
137
|
"rimraf": "^6.1.3",
|
|
138
138
|
"tailwindcss": "^4.3.3",
|
|
139
139
|
"tsd": "^0.33.0",
|
|
140
140
|
"tui-driver": "github:vizzly-testing/tui-driver",
|
|
141
141
|
"typescript": "^7.0.2",
|
|
142
|
-
"vite": "^8.3.
|
|
143
|
-
"wouter": "^3.
|
|
142
|
+
"vite": "^8.3.5",
|
|
143
|
+
"wouter": "^3.13.0"
|
|
144
144
|
}
|
|
145
145
|
}
|
package/skills/vizzly/SKILL.md
CHANGED
|
@@ -41,8 +41,9 @@ output for guided inspection.
|
|
|
41
41
|
pagination before drawing conclusions. Follow the schema's page instructions
|
|
42
42
|
for cloud data. Missing fields remain unknown.
|
|
43
43
|
4. For cloud data, discover the comparison and image operations from the schema.
|
|
44
|
-
View the
|
|
45
|
-
|
|
44
|
+
View the current image with the baseline and diff when they're available.
|
|
45
|
+
A new screenshot may have no baseline or diff. Say which images you inspected;
|
|
46
|
+
if you couldn't open any images, label the result metadata-only.
|
|
46
47
|
5. Read image dimensions, viewport, browser, diff regions, fingerprint, and
|
|
47
48
|
relevant history alongside the images. A prior approval is supporting
|
|
48
49
|
evidence, not permission to approve the current comparison.
|
|
@@ -69,4 +70,4 @@ output for guided inspection.
|
|
|
69
70
|
- [SDK capture](references/sdks.md): add or change screenshot capture code.
|
|
70
71
|
- [Dynamic content](references/dynamic-content.md): investigate unstable
|
|
71
72
|
content and screenshot-specific tolerances.
|
|
72
|
-
- [
|
|
73
|
+
- [CI](references/ci.md): run visual tests with committed config and CI secrets.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# CI
|
|
2
|
+
|
|
3
|
+
CI uses the config committed to the repository. Set up Vizzly locally first.
|
|
4
|
+
|
|
5
|
+
## Cloud CI
|
|
6
|
+
|
|
7
|
+
Use a project token from the repo's existing secret store. Never print it or
|
|
8
|
+
save it in the repo.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
vizzly run "<existing visual test command>" --json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Add `--wait` when you want CI to wait for cloud comparisons and fail on visual
|
|
15
|
+
differences. Changed comparisons then make the command exit with code `1`.
|
|
16
|
+
|
|
17
|
+
For parallel CI, give every shard the same ID for that CI run. Run shards
|
|
18
|
+
without `--wait`, then finalize once they all finish:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
vizzly run "<shard test command>" --parallel-id "<shared-ci-run-id>" --json
|
|
22
|
+
vizzly finalize "<shared-ci-run-id>" --json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Troubleshoot
|
|
26
|
+
|
|
27
|
+
- `vizzly doctor`: check local config.
|
|
28
|
+
- `vizzly status <build-id> --json`: check cloud build status.
|
|
29
|
+
- `vizzly api schema --json`: find cloud review requests and their inputs
|
|
30
|
+
and outputs.
|
|
31
|
+
|
|
32
|
+
If screenshots are missing, check the integration and the test that should
|
|
33
|
+
capture them. If auth is missing, check the `VIZZLY_TOKEN` CI secret.
|