@vizzly-testing/cli 0.39.0 → 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 +4 -3
- package/dist/cli.js +11 -2
- package/dist/commands/init.js +1 -3
- package/dist/init/agents.js +51 -10
- package/dist/reporter/reporter-bundle.iife.js +2 -2
- package/dist/reporter-ssr/ssr-entry.js +3 -6
- package/dist/utils/cli-update.js +105 -0
- package/dist/utils/update-check.js +2 -0
- package/dist/utils/update-notifications.js +175 -0
- package/package.json +15 -15
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { renderToString } from "react-dom/server";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
|
|
3
|
+
// src/utils/status-utils.js
|
|
4
4
|
function isNewComparisonStatus(status) {
|
|
5
5
|
return status === "new" || status === "baseline-created";
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
//#region src/components/static-report-view.jsx
|
|
7
|
+
// src/components/static-report-view.jsx
|
|
9
8
|
/**
|
|
10
9
|
* Static Report View
|
|
11
10
|
*
|
|
@@ -637,8 +636,7 @@ function StaticReportView({ reportData }) {
|
|
|
637
636
|
})]
|
|
638
637
|
});
|
|
639
638
|
}
|
|
640
|
-
|
|
641
|
-
//#region src/ssr-entry.jsx
|
|
639
|
+
// src/ssr-entry.jsx
|
|
642
640
|
/**
|
|
643
641
|
* SSR Entry Point
|
|
644
642
|
*
|
|
@@ -653,5 +651,4 @@ function StaticReportView({ reportData }) {
|
|
|
653
651
|
function renderStaticReport(reportData) {
|
|
654
652
|
return renderToString(/* @__PURE__ */ jsx(StaticReportView, { reportData }));
|
|
655
653
|
}
|
|
656
|
-
//#endregion
|
|
657
654
|
export { renderStaticReport };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { access, readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { getPackageName } from './package-info.js';
|
|
8
|
+
let exec = promisify(execFile);
|
|
9
|
+
let bundledRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
10
|
+
let installCommands = {
|
|
11
|
+
npm: 'install',
|
|
12
|
+
pnpm: 'add'
|
|
13
|
+
};
|
|
14
|
+
let saveFlags = {
|
|
15
|
+
dependencies: '--save-prod',
|
|
16
|
+
devDependencies: '--save-dev',
|
|
17
|
+
optionalDependencies: '--save-optional'
|
|
18
|
+
};
|
|
19
|
+
async function exists(filename) {
|
|
20
|
+
try {
|
|
21
|
+
await access(filename);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Only offer an installer when the running package can be matched to that
|
|
29
|
+
// installation. A checkout, npx cache, or unknown manager gets manual guidance.
|
|
30
|
+
export async function findCliUpdatePlan(cwd, version, packageRoot = bundledRoot) {
|
|
31
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) return null;
|
|
32
|
+
let installed = await realpath(packageRoot);
|
|
33
|
+
if (!installed.split(path.sep).includes('node_modules')) return null;
|
|
34
|
+
let name = getPackageName();
|
|
35
|
+
let spec = `${name}@${version}`;
|
|
36
|
+
for (let directory = cwd;; directory = path.dirname(directory)) {
|
|
37
|
+
let manifest = path.join(directory, 'package.json');
|
|
38
|
+
try {
|
|
39
|
+
let pkg = JSON.parse(await readFile(manifest, 'utf8'));
|
|
40
|
+
let group = Object.keys(saveFlags).find(key => pkg[key]?.[name]);
|
|
41
|
+
if (group) {
|
|
42
|
+
let resolved = createRequire(manifest).resolve(name);
|
|
43
|
+
if ((await realpath(resolved)) !== path.join(installed, 'dist', 'index.js')) return null;
|
|
44
|
+
if (/^(workspace:|file:|link:|git|https?:|npm:)/.test(pkg[group][name])) return null;
|
|
45
|
+
let manager = pkg.packageManager?.split('@')[0];
|
|
46
|
+
if (!manager) {
|
|
47
|
+
if (await exists(path.join(directory, 'pnpm-lock.yaml'))) manager = 'pnpm';else if (await exists(path.join(directory, 'package-lock.json'))) manager = 'npm';
|
|
48
|
+
}
|
|
49
|
+
if (!['npm', 'pnpm'].includes(manager)) return null;
|
|
50
|
+
let args = [installCommands[manager], saveFlags[group]];
|
|
51
|
+
let preserveExactVersion = /^\d+\.\d+\.\d+(?:[-+].*)?$/.test(pkg[group][name]);
|
|
52
|
+
if (preserveExactVersion) args.push('--save-exact');
|
|
53
|
+
args.push(spec);
|
|
54
|
+
return {
|
|
55
|
+
command: manager,
|
|
56
|
+
args,
|
|
57
|
+
cwd: directory,
|
|
58
|
+
cliPath: path.join(directory, 'node_modules', name, 'dist', 'cli.js')
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Keep looking for the workspace manifest that owns the dependency.
|
|
63
|
+
}
|
|
64
|
+
if (directory === path.dirname(directory)) break;
|
|
65
|
+
}
|
|
66
|
+
for (let manager of ['npm', 'pnpm']) {
|
|
67
|
+
try {
|
|
68
|
+
let {
|
|
69
|
+
stdout
|
|
70
|
+
} = await exec(manager, ['root', '--global'], {
|
|
71
|
+
timeout: 3000
|
|
72
|
+
});
|
|
73
|
+
let globalPackage = path.join(stdout.trim(), name);
|
|
74
|
+
if ((await realpath(globalPackage)) === installed) {
|
|
75
|
+
return {
|
|
76
|
+
command: manager,
|
|
77
|
+
args: [installCommands[manager], '--global', spec],
|
|
78
|
+
cwd,
|
|
79
|
+
cliPath: path.join(globalPackage, 'dist', 'cli.js')
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// An unavailable manager cannot own an update we offer to install.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function runProcess(command, args, cwd, stdio) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
let child = spawn(command, args, {
|
|
91
|
+
cwd,
|
|
92
|
+
stdio,
|
|
93
|
+
shell: process.platform === 'win32' && ['npm', 'pnpm'].includes(command)
|
|
94
|
+
});
|
|
95
|
+
child.once('error', reject);
|
|
96
|
+
child.once('close', (code, signal) => resolve(code ?? (signal === 'SIGINT' ? 130 : 1)));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export async function runUpdateProcess(command, args, cwd) {
|
|
100
|
+
let code = await runProcess(command, args, cwd, ['inherit', process.stderr, process.stderr]);
|
|
101
|
+
if (code !== 0) throw new Error(`${command} exited with status ${code}.`);
|
|
102
|
+
}
|
|
103
|
+
export function runUpdatedCli(cliPath, args, cwd) {
|
|
104
|
+
return runProcess(process.execPath, [cliPath, '--no-update-notifier', ...args], cwd, 'inherit');
|
|
105
|
+
}
|
|
@@ -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.39.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"description": "Visual regression testing from your terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm-context",
|
|
@@ -99,11 +99,11 @@
|
|
|
99
99
|
"ansis": "^4.4.0",
|
|
100
100
|
"commander": "^15.0.0",
|
|
101
101
|
"cosmiconfig": "^10.0.1",
|
|
102
|
-
"dotenv": "^18.0.
|
|
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.
|
|
105
|
+
"react": "^19.3.1",
|
|
106
|
+
"react-dom": "^19.3.1",
|
|
107
107
|
"zod": "^4.6.7"
|
|
108
108
|
},
|
|
109
109
|
"tsd": {
|
|
@@ -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
|
}
|