@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 CHANGED
@@ -168,9 +168,9 @@ Install the Vizzly skill and a short `AGENTS.md` note:
168
168
  pnpm exec vizzly init --agent-guidance
169
169
  ```
170
170
 
171
- This adds `.agents/skills/vizzly` and a note in `AGENTS.md`, keeping other
172
- guidance. Use `--agent-skill` to install only the skill during setup.
173
- These flags don't open the wizard or update an existing Vizzly setup.
171
+ This adds `.agents/skills/vizzly` and a note in `AGENTS.md`, keeping your
172
+ other instructions. Use `--agent-skill` for just the skill. Run either command
173
+ again to refresh it without changing your config.
174
174
  `--skip-agent-skill` skips agent setup unless you also request `--agent-guidance`.
175
175
 
176
176
  Use `context` to read local results. For cloud results, check the API schema
@@ -301,6 +301,7 @@ See their setup guides above for details.
301
301
  ## More
302
302
 
303
303
  [Full docs](https://docs.vizzly.dev) ·
304
+ [Updating Vizzly](docs/updates.md) ·
304
305
  [Report a bug or suggest a change](https://github.com/vizzly-testing/cli/issues)
305
306
 
306
307
  PRs welcome. MIT licensed.
package/dist/cli.js CHANGED
@@ -29,12 +29,14 @@ import { createPluginServices } from './plugin-api.js';
29
29
  import { loadPlugins } from './plugin-loader.js';
30
30
  import { createServices } from './services/index.js';
31
31
  import { withTimeout } from './utils/async-utils.js';
32
+ import { runUpdatedCli } from './utils/cli-update.js';
32
33
  import { colors } from './utils/colors.js';
33
34
  import { loadConfig } from './utils/config-loader.js';
34
35
  import { getContext } from './utils/context.js';
35
36
  import { saveUserPath } from './utils/global-config.js';
36
37
  import * as output from './utils/output.js';
37
38
  import { getPackageVersion } from './utils/package-info.js';
39
+ import { notifyUpdates } from './utils/update-notifications.js';
38
40
 
39
41
  // Custom help formatting with BearDen design system
40
42
  const formatHelp = (cmd, helper) => {
@@ -393,7 +395,7 @@ function writeCommanderError(text) {
393
395
  }
394
396
  }
395
397
  }
396
- program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json [fields]', 'JSON output, optionally specify fields (e.g., --json id,status,branch)').option('--color', 'Force colored output (even in non-TTY)').option('--no-color', 'Disable colored output').option('--strict', 'Fail on any error (default: be resilient, warn on non-critical issues)').configureHelp({
398
+ program.name('vizzly').description('Vizzly CLI for visual regression testing').version(getPackageVersion()).option('-c, --config <path>', 'Config file path').option('--token <token>', 'Vizzly API token').option('-v, --verbose', 'Verbose output (shorthand for --log-level debug)').option('--log-level <level>', 'Log level: debug, info, warn, error (default: info, or VIZZLY_LOG_LEVEL env var)').option('--json [fields]', 'JSON output, optionally specify fields (e.g., --json id,status,branch)').option('--color', 'Force colored output (even in non-TTY)').option('--no-color', 'Disable colored output').option('--no-update-notifier', 'Skip update checks and notices').option('--strict', 'Fail on any error (default: be resilient, warn on non-critical issues)').configureHelp({
397
399
  formatHelp
398
400
  });
399
401
  program.configureOutput({
@@ -402,6 +404,13 @@ program.configureOutput({
402
404
  program.showHelpAfterError('(Run vizzly --help for available commands)');
403
405
  program.showSuggestionAfterError();
404
406
  program.exitOverride();
407
+ program.hook('preAction', async (_command, actionCommand) => {
408
+ if (actionCommand.name() === 'init') return;
409
+ let updatedCli = await notifyUpdates(actionCommand.optsWithGlobals());
410
+ if (!updatedCli) return;
411
+ let code = await runUpdatedCli(updatedCli, process.argv.slice(2), process.cwd());
412
+ process.exit(code);
413
+ });
405
414
 
406
415
  // Load plugins before defining commands
407
416
  // We need to manually parse to get the config option early
@@ -456,7 +465,7 @@ if (needsRuntimeBootstrap) {
456
465
  output.debug(`Plugin loading failed: ${error.message}`);
457
466
  }
458
467
  }
459
- program.command('init').description('Initialize Vizzly in your project').option('--quick', 'Create an editable config without the setup wizard').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install the Vizzly agent skill during setup').option('--agent-guidance', 'Install the Vizzly skill and AGENTS.md guidance during setup').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
468
+ program.command('init').description('Initialize Vizzly in your project').option('--quick', 'Create an editable config without the setup wizard').option('--force', 'Overwrite existing configuration').option('--agent-skill', 'Install or refresh the Vizzly agent skill').option('--agent-guidance', 'Install or refresh the Vizzly skill and AGENTS.md guidance').option('--skip-agent-skill', 'Skip the Vizzly agent skill prompt').action(async options => {
460
469
  let globalOptions = getGlobalOptions();
461
470
  try {
462
471
  let result = await init({
@@ -119,9 +119,7 @@ export async function init(options = {}) {
119
119
  let prompts;
120
120
  try {
121
121
  if (initialized) {
122
- plan = {
123
- changes: []
124
- };
122
+ plan = await planAgentSetup(cwd, agentOptions(options));
125
123
  } else if (quick) {
126
124
  let configChange = planConfig(config);
127
125
  let agents = await planAgentSetup(cwd, agentOptions(options));
@@ -1,11 +1,20 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { readdir } from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { readOptionalFile } from '../utils/config-discovery.js';
5
6
  import { fileChange } from '../utils/file-changes.js';
7
+ import { getPackageVersion } from '../utils/package-info.js';
6
8
  let sourcePath = fileURLToPath(new URL('../../skills/vizzly/', import.meta.url));
7
9
  let start = '<!-- vizzly-agent-guidance -->';
8
10
  let end = '<!-- /vizzly-agent-guidance -->';
11
+ export function agentGuidancePath(cwd) {
12
+ return path.join(cwd, '.vizzly', 'agent-guidance.json');
13
+ }
14
+ export async function agentGuidanceFlag(cwd) {
15
+ let agents = await readOptionalFile(path.join(cwd, 'AGENTS.md'));
16
+ return agents?.includes(start) ? '--agent-guidance' : '--agent-skill';
17
+ }
9
18
  function guidanceContent() {
10
19
  return `${start}
11
20
  ## Visual testing with Vizzly
@@ -35,14 +44,18 @@ function addGuidance(source) {
35
44
  throw new Error('Repair the incomplete or duplicate Vizzly guidance markers in AGENTS.md before running init.');
36
45
  }
37
46
  let block = guidanceContent();
38
- if (starts) return content;
47
+ if (starts) {
48
+ return content.slice(0, begin) + block.trimEnd() + content.slice(finish + end.length);
49
+ }
39
50
  return content.trim() ? `${content.trimEnd()}\n\n${block}` : block;
40
51
  }
41
52
  async function skillFiles(directory, prefix = '') {
42
53
  let files = [];
43
- for (let entry of await readdir(directory, {
54
+ let entries = await readdir(directory, {
44
55
  withFileTypes: true
45
- })) {
56
+ });
57
+ entries.sort((a, b) => a.name.localeCompare(b.name));
58
+ for (let entry of entries) {
46
59
  let relative = path.join(prefix, entry.name);
47
60
  if (entry.isDirectory()) {
48
61
  files.push(...(await skillFiles(path.join(directory, entry.name), relative)));
@@ -52,6 +65,23 @@ async function skillFiles(directory, prefix = '') {
52
65
  }
53
66
  return files;
54
67
  }
68
+ export async function bundledAgentSkill() {
69
+ let files = [];
70
+ let hash = createHash('sha256');
71
+ for (let filename of await skillFiles(sourcePath)) {
72
+ let content = await readOptionalFile(path.join(sourcePath, filename));
73
+ files.push({
74
+ filename,
75
+ content
76
+ });
77
+ hash.update(JSON.stringify([filename.split(path.sep).join('/'), content]));
78
+ }
79
+ hash.update(guidanceContent());
80
+ return {
81
+ files,
82
+ version: hash.digest('hex')
83
+ };
84
+ }
55
85
  export async function planAgentSetup(cwd, {
56
86
  skill,
57
87
  guidance
@@ -61,17 +91,23 @@ export async function planAgentSetup(cwd, {
61
91
  };
62
92
  let targetPath = path.join(cwd, '.agents', 'skills', 'vizzly');
63
93
  let existing = await readOptionalFile(path.join(targetPath, 'SKILL.md'));
64
- let changes = [];
65
- if (!existing) {
66
- for (let filename of await skillFiles(sourcePath)) {
67
- changes.push(await fileChange(path.join(targetPath, filename), await readOptionalFile(path.join(sourcePath, filename))));
68
- }
94
+ let skillChanges = [];
95
+ let bundled = await bundledAgentSkill();
96
+ for (let {
97
+ filename,
98
+ content
99
+ } of bundled.files) {
100
+ skillChanges.push(await fileChange(path.join(targetPath, filename), content));
69
101
  }
102
+ let skillStatus = 'installed';
103
+ if (existing) skillStatus = skillChanges.some(Boolean) ? 'updated' : 'exists';
70
104
  let agentSkill = {
71
- status: existing ? 'exists' : 'installed',
105
+ status: skillStatus,
72
106
  sourcePath,
73
- targetPath
107
+ targetPath,
108
+ version: bundled.version
74
109
  };
110
+ let changes = [...skillChanges];
75
111
  let agentGuidance;
76
112
  if (guidance) {
77
113
  let agentsPath = path.join(cwd, 'AGENTS.md');
@@ -91,6 +127,11 @@ export async function planAgentSetup(cwd, {
91
127
  agentsPath
92
128
  };
93
129
  }
130
+ let installedGuidance = {
131
+ version: bundled.version,
132
+ cliVersion: getPackageVersion()
133
+ };
134
+ changes.push(await fileChange(agentGuidancePath(cwd), `${JSON.stringify(installedGuidance, null, 2)}\n`));
94
135
  return {
95
136
  changes: changes.filter(Boolean),
96
137
  agentSkill,