@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.
@@ -1,11 +1,10 @@
1
1
  import { renderToString } from "react-dom/server";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- //#region src/utils/status-utils.js
3
+ // src/utils/status-utils.js
4
4
  function isNewComparisonStatus(status) {
5
5
  return status === "new" || status === "baseline-created";
6
6
  }
7
- //#endregion
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
- //#endregion
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,2 @@
1
+ import { checkForUpdate } from './update-notifications.js';
2
+ await checkForUpdate();
@@ -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.0",
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.3",
102
+ "dotenv": "^18.0.9",
103
103
  "form-data": "^4.0.6",
104
104
  "glob": "^13.0.6",
105
- "react": "^19.3.0",
106
- "react-dom": "^19.3.0",
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.6",
122
- "@babel/preset-env": "^8.0.6",
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.1",
125
- "@biomejs/biome": "^2.5.15",
124
+ "@babel/preset-typescript": "^8.0.7",
125
+ "@biomejs/biome": "^2.6.2",
126
126
  "@heroicons/react": "^2.2.0",
127
- "@modelcontextprotocol/sdk": "^1.31.0",
128
- "@playwright/test": "^1.63.0",
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.0",
131
- "@types/node": "^26.6.3",
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.29",
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.1",
143
- "wouter": "^3.11.1"
142
+ "vite": "^8.3.5",
143
+ "wouter": "^3.13.0"
144
144
  }
145
145
  }