@vizzly-testing/cli 0.39.0 → 0.39.2

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.
Files changed (195) hide show
  1. package/README.md +12 -3
  2. package/bin/vizzly.js +1 -1
  3. package/dist/{cli.js → adapters/cli/cli.js} +50 -35
  4. package/dist/adapters/cli/setup.js +34 -0
  5. package/dist/{client → adapters/client}/index.js +6 -7
  6. package/dist/adapters/http/cloud-proxy.js +29 -0
  7. package/dist/{server → adapters/http}/http-server.js +12 -13
  8. package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
  9. package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
  10. package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
  11. package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
  12. package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
  13. package/dist/adapters/sdk/index.d.ts +89 -0
  14. package/dist/{sdk → adapters/sdk}/index.js +22 -19
  15. package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
  16. package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
  17. package/dist/domains/auth/auth-errors.js +50 -0
  18. package/dist/domains/auth/auth-headers.js +41 -0
  19. package/dist/domains/auth/auth.normalizers.js +50 -0
  20. package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
  21. package/dist/domains/auth/auth.rules.js +19 -0
  22. package/dist/domains/auth/auth.serializers.js +32 -0
  23. package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
  24. package/dist/domains/auth/auth.validations.js +21 -0
  25. package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
  26. package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
  27. package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
  28. package/dist/domains/auth/token-store.js +81 -0
  29. package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
  30. package/dist/domains/baselines/baseline-download.js +192 -0
  31. package/dist/domains/baselines/baseline-write.js +22 -0
  32. package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
  33. package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
  34. package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
  35. package/dist/domains/baselines/cloud-baselines.js +9 -0
  36. package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
  37. package/dist/domains/builds/build.normalizers.js +35 -0
  38. package/dist/domains/builds/build.serializers.js +164 -0
  39. package/dist/domains/builds/build.service.js +119 -0
  40. package/dist/domains/builds/build.validations.js +39 -0
  41. package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
  42. package/dist/domains/builds/cloud-builds.routes.js +31 -0
  43. package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
  44. package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
  45. package/dist/domains/comparisons/cloud-comparisons.js +43 -0
  46. package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
  47. package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
  48. package/dist/domains/config/config-defaults.js +27 -0
  49. package/dist/{utils → domains/config}/config-discovery.js +1 -9
  50. package/dist/{utils → domains/config}/config-loader.js +10 -9
  51. package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
  52. package/dist/{types → domains/config}/config.d.ts +3 -3
  53. package/dist/domains/config/config.normalizers.js +126 -0
  54. package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
  55. package/dist/domains/config/config.rules.js +47 -0
  56. package/dist/domains/config/config.serializers.js +202 -0
  57. package/dist/domains/config/config.service.js +625 -0
  58. package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
  59. package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
  60. package/dist/domains/context/compact-context.validations.js +60 -0
  61. package/dist/domains/context/context-presentation.js +368 -0
  62. package/dist/domains/context/context.commands.js +385 -0
  63. package/dist/domains/context/context.serializers.js +157 -0
  64. package/dist/domains/context/context.service.js +76 -0
  65. package/dist/domains/context/context.validations.js +100 -0
  66. package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
  67. package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
  68. package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
  69. package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
  70. package/dist/domains/previews/preview.service.js +37 -0
  71. package/dist/domains/projects/cloud-projects.routes.js +28 -0
  72. package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
  73. package/dist/{utils → domains/projects}/project-binding.js +3 -3
  74. package/dist/domains/projects/project-errors.js +61 -0
  75. package/dist/{utils → domains/projects}/project-link-store.js +2 -2
  76. package/dist/domains/projects/project-requests.js +61 -0
  77. package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
  78. package/dist/domains/projects/project.normalizers.js +69 -0
  79. package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
  80. package/dist/domains/projects/project.validations.js +31 -0
  81. package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
  82. package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
  83. package/dist/domains/reports/dashboard.routes.js +96 -0
  84. package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
  85. package/dist/domains/reports/report-assets.routes.js +81 -0
  86. package/dist/domains/reports/report-state.js +51 -0
  87. package/dist/domains/reports/report-store.js +85 -0
  88. package/dist/domains/reports/report.serializers.js +99 -0
  89. package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
  90. package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
  91. package/dist/domains/runs/run-environment.js +69 -0
  92. package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
  93. package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
  94. package/dist/domains/runs/run.normalizers.js +8 -0
  95. package/dist/domains/runs/run.rules.js +56 -0
  96. package/dist/domains/runs/run.serializers.js +101 -0
  97. package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
  98. package/dist/domains/runs/run.validations.js +17 -0
  99. package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
  100. package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
  101. package/dist/domains/screenshots/cloud-screenshots.js +109 -0
  102. package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
  103. package/dist/domains/screenshots/screenshot-hash.js +53 -0
  104. package/dist/domains/screenshots/screenshot-server-options.js +28 -0
  105. package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
  106. package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
  107. package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
  108. package/dist/domains/screenshots/screenshot.serializers.js +146 -0
  109. package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
  110. package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
  111. package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
  112. package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
  113. package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
  114. package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
  115. package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
  116. package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
  117. package/dist/domains/updates/cli-update.js +103 -0
  118. package/dist/domains/updates/update-check.js +2 -0
  119. package/dist/domains/updates/update-notifications.js +175 -0
  120. package/dist/domains/uploads/upload-defaults.js +4 -0
  121. package/dist/domains/uploads/upload-files.js +32 -0
  122. package/dist/domains/uploads/upload-settings.js +21 -0
  123. package/dist/domains/uploads/upload-timing.js +18 -0
  124. package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
  125. package/dist/domains/uploads/upload.normalizers.js +60 -0
  126. package/dist/domains/uploads/upload.serializers.js +179 -0
  127. package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
  128. package/dist/domains/uploads/upload.validations.js +77 -0
  129. package/dist/index.js +12 -12
  130. package/dist/reporter/reporter-bundle.iife.js +86 -86
  131. package/dist/reporter-ssr/ssr-entry.js +3 -6
  132. package/dist/shared/cloud-protocol.js +171 -0
  133. package/dist/{utils → shared}/file-changes.js +1 -1
  134. package/dist/shared/files.js +9 -0
  135. package/dist/{utils → shared}/global-config.js +7 -78
  136. package/dist/shared/log-level.js +7 -0
  137. package/dist/{utils → shared}/output.js +1 -1
  138. package/dist/{utils → shared}/package-info.js +5 -4
  139. package/dist/shared/paths.js +57 -0
  140. package/package.json +37 -36
  141. package/dist/api/core.js +0 -382
  142. package/dist/api/endpoints.js +0 -421
  143. package/dist/api/index.js +0 -19
  144. package/dist/auth/core.js +0 -237
  145. package/dist/auth/index.js +0 -31
  146. package/dist/commands/context.js +0 -1066
  147. package/dist/config/core.js +0 -439
  148. package/dist/config/operations.js +0 -327
  149. package/dist/project/core.js +0 -211
  150. package/dist/screenshot-server/core.js +0 -161
  151. package/dist/screenshot-server/index.js +0 -11
  152. package/dist/server/routers/assets.js +0 -95
  153. package/dist/server/routers/cloud-proxy.js +0 -146
  154. package/dist/server/routers/dashboard.js +0 -172
  155. package/dist/services/auth-service.js +0 -117
  156. package/dist/services/config-service.js +0 -313
  157. package/dist/services/project-service.js +0 -88
  158. package/dist/test-runner/core.js +0 -293
  159. package/dist/test-runner/index.js +0 -13
  160. package/dist/types/sdk.d.ts +0 -186
  161. package/dist/uploader/core.js +0 -432
  162. package/dist/uploader/index.js +0 -88
  163. /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
  164. /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
  165. /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
  166. /package/dist/{server → adapters/http}/middleware/response.js +0 -0
  167. /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
  168. /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
  169. /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
  170. /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
  171. /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
  172. /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
  173. /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
  174. /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
  175. /package/dist/{context → domains/context}/provider-resolver.js +0 -0
  176. /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
  177. /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
  178. /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
  179. /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
  180. /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
  181. /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
  182. /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
  183. /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
  184. /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
  185. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  186. /package/dist/{utils → shared}/api-url.js +0 -0
  187. /package/dist/{utils → shared}/async-utils.js +0 -0
  188. /package/dist/{utils → shared}/browser.js +0 -0
  189. /package/dist/{utils → shared}/ci-env.js +0 -0
  190. /package/dist/{utils → shared}/colors.js +0 -0
  191. /package/dist/{utils → shared}/fetch-utils.js +0 -0
  192. /package/dist/{utils → shared}/git.js +0 -0
  193. /package/dist/{utils → shared}/patterns.js +0 -0
  194. /package/dist/{utils → shared}/secret-store.js +0 -0
  195. /package/dist/{errors → shared}/vizzly-error.js +0 -0
@@ -0,0 +1,103 @@
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 { promisify } from 'node:util';
6
+ import { getPackageName, getPackageRoot } from '../../shared/package-info.js';
7
+ let exec = promisify(execFile);
8
+ let installCommands = {
9
+ npm: 'install',
10
+ pnpm: 'add'
11
+ };
12
+ let saveFlags = {
13
+ dependencies: '--save-prod',
14
+ devDependencies: '--save-dev',
15
+ optionalDependencies: '--save-optional'
16
+ };
17
+ async function exists(filename) {
18
+ try {
19
+ await access(filename);
20
+ return true;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ // Only offer an installer when the running package can be matched to that
27
+ // installation. A checkout, npx cache, or unknown manager gets manual guidance.
28
+ export async function findCliUpdatePlan(cwd, version, packageRoot = getPackageRoot()) {
29
+ if (!/^\d+\.\d+\.\d+$/.test(version)) return null;
30
+ let installed = await realpath(packageRoot);
31
+ if (!installed.split(path.sep).includes('node_modules')) return null;
32
+ let name = getPackageName();
33
+ let spec = `${name}@${version}`;
34
+ for (let directory = cwd;; directory = path.dirname(directory)) {
35
+ let manifest = path.join(directory, 'package.json');
36
+ try {
37
+ let pkg = JSON.parse(await readFile(manifest, 'utf8'));
38
+ let group = Object.keys(saveFlags).find(key => pkg[key]?.[name]);
39
+ if (group) {
40
+ let resolved = createRequire(manifest).resolve(name);
41
+ if ((await realpath(resolved)) !== path.join(installed, 'dist', 'index.js')) return null;
42
+ if (/^(workspace:|file:|link:|git|https?:|npm:)/.test(pkg[group][name])) return null;
43
+ let manager = pkg.packageManager?.split('@')[0];
44
+ if (!manager) {
45
+ if (await exists(path.join(directory, 'pnpm-lock.yaml'))) manager = 'pnpm';else if (await exists(path.join(directory, 'package-lock.json'))) manager = 'npm';
46
+ }
47
+ if (!['npm', 'pnpm'].includes(manager)) return null;
48
+ let args = [installCommands[manager], saveFlags[group]];
49
+ let preserveExactVersion = /^\d+\.\d+\.\d+(?:[-+].*)?$/.test(pkg[group][name]);
50
+ if (preserveExactVersion) args.push('--save-exact');
51
+ args.push(spec);
52
+ return {
53
+ command: manager,
54
+ args,
55
+ cwd: directory,
56
+ cliPath: path.join(directory, 'node_modules', name, 'bin', 'vizzly.js')
57
+ };
58
+ }
59
+ } catch {
60
+ // Keep looking for the workspace manifest that owns the dependency.
61
+ }
62
+ if (directory === path.dirname(directory)) break;
63
+ }
64
+ for (let manager of ['npm', 'pnpm']) {
65
+ try {
66
+ let {
67
+ stdout
68
+ } = await exec(manager, ['root', '--global'], {
69
+ timeout: 3000
70
+ });
71
+ let globalPackage = path.join(stdout.trim(), name);
72
+ if ((await realpath(globalPackage)) === installed) {
73
+ return {
74
+ command: manager,
75
+ args: [installCommands[manager], '--global', spec],
76
+ cwd,
77
+ cliPath: path.join(globalPackage, 'bin', 'vizzly.js')
78
+ };
79
+ }
80
+ } catch {
81
+ // An unavailable manager cannot own an update we offer to install.
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ function runProcess(command, args, cwd, stdio) {
87
+ return new Promise((resolve, reject) => {
88
+ let child = spawn(command, args, {
89
+ cwd,
90
+ stdio,
91
+ shell: process.platform === 'win32' && ['npm', 'pnpm'].includes(command)
92
+ });
93
+ child.once('error', reject);
94
+ child.once('close', (code, signal) => resolve(code ?? (signal === 'SIGINT' ? 130 : 1)));
95
+ });
96
+ }
97
+ export async function runUpdateProcess(command, args, cwd) {
98
+ let code = await runProcess(command, args, cwd, ['inherit', process.stderr, process.stderr]);
99
+ if (code !== 0) throw new Error(`${command} exited with status ${code}.`);
100
+ }
101
+ export function runUpdatedCli(cliPath, args, cwd) {
102
+ return runProcess(process.execPath, [cliPath, '--no-update-notifier', ...args], cwd, 'inherit');
103
+ }
@@ -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 { applyChanges, fileChange, preflightChanges } from '../../shared/file-changes.js';
5
+ import { readOptionalFile } from '../../shared/files.js';
6
+ import { getGlobalConfigDir } from '../../shared/global-config.js';
7
+ import * as output from '../../shared/output.js';
8
+ import { getPackageName, getPackageVersion } from '../../shared/package-info.js';
9
+ import { agentGuidanceFlag, agentGuidancePath, bundledAgentSkill, planAgentSetup } from '../setup/agent-setup.js';
10
+ import { createPrompts } from '../setup/setup-prompts.js';
11
+ import { findCliUpdatePlan, runUpdateProcess } from './cli-update.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
+ }
@@ -0,0 +1,4 @@
1
+ export let DEFAULT_BATCH_SIZE = 50;
2
+ export let DEFAULT_SHA_CHECK_BATCH_SIZE = 100;
3
+ export let DEFAULT_TIMEOUT = 30000;
4
+ export let DEFAULT_BUILD_POLL_INTERVAL = 1000;
@@ -0,0 +1,32 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ /**
4
+ * Compute SHA256 hash of a buffer
5
+ * @param {Buffer} buffer - File buffer
6
+ * @returns {string} Hex-encoded SHA256 hash
7
+ */
8
+ export function computeSha256(buffer) {
9
+ return crypto.createHash('sha256').update(buffer).digest('hex');
10
+ }
11
+
12
+ /**
13
+ * Partition files into those that need upload and those that exist
14
+ * @param {Array} fileMetadata - All file metadata
15
+ * @param {Set} existingShas - Set of SHAs that already exist
16
+ * @returns {{ toUpload: Array, existing: Array }}
17
+ */
18
+ export function partitionFilesByExistence(fileMetadata, existingShas) {
19
+ return {
20
+ toUpload: fileMetadata.filter(f => !existingShas.has(f.sha256)),
21
+ existing: fileMetadata.filter(f => existingShas.has(f.sha256))
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Build glob pattern for screenshots
27
+ * @param {string} directory - Base directory
28
+ * @returns {string} Glob pattern
29
+ */
30
+ export function buildScreenshotPattern(directory) {
31
+ return `${directory}/**/*.png`;
32
+ }
@@ -0,0 +1,21 @@
1
+ import { DEFAULT_BATCH_SIZE, DEFAULT_TIMEOUT } from './upload-defaults.js';
2
+
3
+ /**
4
+ * Resolve batch size from options and config
5
+ * @param {Object} options - Runtime options
6
+ * @param {Object} uploadConfig - Upload configuration
7
+ * @returns {number} Resolved batch size
8
+ */
9
+ export function resolveBatchSize(options, uploadConfig) {
10
+ return Number(options?.batchSize ?? uploadConfig?.batchSize ?? DEFAULT_BATCH_SIZE);
11
+ }
12
+
13
+ /**
14
+ * Resolve timeout from options and config
15
+ * @param {Object} options - Runtime options
16
+ * @param {Object} uploadConfig - Upload configuration
17
+ * @returns {number} Resolved timeout in ms
18
+ */
19
+ export function resolveTimeout(options, uploadConfig) {
20
+ return Number(options?.timeout ?? uploadConfig?.timeout ?? DEFAULT_TIMEOUT);
21
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Check if timeout has been exceeded
3
+ * @param {number} startTime - Start timestamp
4
+ * @param {number} timeout - Timeout in ms
5
+ * @returns {boolean} True if timed out
6
+ */
7
+ export function isTimedOut(startTime, timeout, now = Date.now()) {
8
+ return now - startTime >= timeout;
9
+ }
10
+
11
+ /**
12
+ * Get elapsed time since start
13
+ * @param {number} startTime - Start timestamp
14
+ * @returns {number} Elapsed time in ms
15
+ */
16
+ export function getElapsedTime(startTime, now = Date.now()) {
17
+ return now - startTime;
18
+ }
@@ -1,11 +1,13 @@
1
- import { createApiClient as defaultCreateApiClient, finalizeBuild as defaultFinalizeBuild, getTokenContext as defaultGetTokenContext } from '../api/index.js';
2
- import { CONFIG_DEFAULTS } from '../config/core.js';
3
- import { createUploader as defaultCreateUploader } from '../uploader/index.js';
4
- import { getAppBaseUrl } from '../utils/api-url.js';
5
- import { loadConfig as defaultLoadConfig } from '../utils/config-loader.js';
6
- import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitAuthor as defaultDetectCommitAuthor, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../utils/git.js';
7
- import * as defaultOutput from '../utils/output.js';
8
- import { writeSession as defaultWriteSession } from '../utils/session.js';
1
+ import { getAppBaseUrl } from '../../shared/api-url.js';
2
+ import { detectBranch as defaultDetectBranch, detectCommit as defaultDetectCommit, detectCommitAuthor as defaultDetectCommitAuthor, detectCommitMessage as defaultDetectCommitMessage, detectPullRequestNumber as defaultDetectPullRequestNumber, generateBuildNameWithGit as defaultGenerateBuildNameWithGit } from '../../shared/git.js';
3
+ import * as defaultOutput from '../../shared/output.js';
4
+ import { createApiClient as defaultCreateApiClient } from '../auth/cloud-client.js';
5
+ import { finalizeBuild as defaultFinalizeBuild } from '../builds/build.service.js';
6
+ import { CONFIG_DEFAULTS } from '../config/config-defaults.js';
7
+ import { loadConfig as defaultLoadConfig } from '../config/config-loader.js';
8
+ import { getTokenContext as defaultGetTokenContext } from '../context/context.service.js';
9
+ import { writeSession as defaultWriteSession } from '../runs/session-store.js';
10
+ import { createUploader as defaultCreateUploader } from './upload.service.js';
9
11
 
10
12
  /**
11
13
  * Construct proper build URL with org/project context
@@ -380,7 +382,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
380
382
  * @param {Object} options - Command options
381
383
  */
382
384
  export function validateUploadOptions(screenshotsPath, options) {
383
- const errors = [];
385
+ let errors = [];
384
386
  if (!screenshotsPath) {
385
387
  errors.push('Screenshots path is required');
386
388
  }
@@ -0,0 +1,60 @@
1
+ import { basename } from 'node:path';
2
+ import { computeSha256 } from './upload-files.js';
3
+ let KNOWN_BROWSERS = ['chrome', 'firefox', 'safari', 'edge', 'webkit'];
4
+
5
+ /**
6
+ * Extract browser name from filename
7
+ * @param {string} filename - The screenshot filename
8
+ * @returns {string|null} Browser name or null if not found
9
+ */
10
+ export function extractBrowserFromFilename(filename) {
11
+ let lowerFilename = filename.toLowerCase();
12
+ for (let browser of KNOWN_BROWSERS) {
13
+ if (lowerFilename.includes(browser)) {
14
+ return browser;
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+
20
+ /**
21
+ * Build file metadata object
22
+ * @param {string} filePath - Path to file
23
+ * @param {Buffer} buffer - File contents
24
+ * @returns {Object} File metadata
25
+ */
26
+ export function buildFileMetadata(filePath, buffer) {
27
+ return {
28
+ path: filePath,
29
+ filename: basename(filePath),
30
+ buffer,
31
+ sha256: computeSha256(buffer)
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Convert file metadata to screenshot check format
37
+ * @param {Object} file - File metadata
38
+ * @returns {Object} Screenshot format for SHA check
39
+ */
40
+ export function fileToScreenshotFormat(file) {
41
+ let browser = extractBrowserFromFilename(file.filename);
42
+ return {
43
+ sha256: file.sha256,
44
+ name: file.filename.replace(/\.png$/, ''),
45
+ filename: file.filename,
46
+ properties: browser ? {
47
+ browser
48
+ } : {}
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Extract status code from error message
54
+ * @param {string} errorMessage - Error message
55
+ * @returns {string} Status code or 'unknown'
56
+ */
57
+ export function extractStatusCodeFromError(errorMessage) {
58
+ let match = String(errorMessage || '').match(/API request failed: (\d+)/);
59
+ return match ? match[1] : 'unknown';
60
+ }
@@ -0,0 +1,179 @@
1
+ function buildUploadMetadata(options) {
2
+ let metadata = {
3
+ ...(options.metadata || {})
4
+ };
5
+ let comparison = {
6
+ ...(metadata.comparison || {})
7
+ };
8
+ if (options.threshold !== undefined) {
9
+ comparison.threshold = options.threshold;
10
+ }
11
+ if (options.minClusterSize !== undefined) {
12
+ comparison.minClusterSize = options.minClusterSize;
13
+ }
14
+ if (Object.keys(comparison).length > 0) {
15
+ metadata.comparison = comparison;
16
+ }
17
+ return Object.keys(metadata).length > 0 ? metadata : undefined;
18
+ }
19
+
20
+ /**
21
+ * Build API build info payload
22
+ * @param {Object} options - Upload options
23
+ * @param {string} [defaultBranch] - Default branch to use
24
+ * @returns {Object} Build info payload
25
+ */
26
+ export function buildBuildInfo(options, defaultBranch = 'main') {
27
+ let buildInfo = {
28
+ name: options.buildName || `Upload ${new Date().toISOString()}`,
29
+ branch: options.branch || defaultBranch || 'main',
30
+ commit_sha: options.commit,
31
+ commit_message: options.message,
32
+ commit_author_name: options.commitAuthorName,
33
+ commit_author_email: options.commitAuthorEmail,
34
+ environment: options.environment || 'production',
35
+ threshold: options.threshold,
36
+ github_pull_request_number: options.pullRequestNumber,
37
+ parallel_id: options.parallelId
38
+ };
39
+ let metadata = buildUploadMetadata(options);
40
+ if (metadata) {
41
+ buildInfo.metadata = metadata;
42
+ }
43
+ return buildInfo;
44
+ }
45
+
46
+ /**
47
+ * Build scanning phase progress
48
+ * @param {number} total - Total files found
49
+ * @returns {Object} Progress object
50
+ */
51
+ export function buildScanningProgress(total) {
52
+ return {
53
+ phase: 'scanning',
54
+ message: `Found ${total} screenshots`,
55
+ total
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Build processing phase progress
61
+ * @param {number} current - Current file number
62
+ * @param {number} total - Total files
63
+ * @returns {Object} Progress object
64
+ */
65
+ export function buildProcessingProgress(current, total) {
66
+ return {
67
+ phase: 'processing',
68
+ message: 'Processing files',
69
+ current,
70
+ total
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Build deduplication phase progress
76
+ * @param {number} toUpload - Files to upload
77
+ * @param {number} existing - Existing files
78
+ * @param {number} total - Total files
79
+ * @returns {Object} Progress object
80
+ */
81
+ export function buildDeduplicationProgress(toUpload, existing, total) {
82
+ return {
83
+ phase: 'deduplication',
84
+ message: `Checking for duplicates (${toUpload} to upload, ${existing} existing)`,
85
+ toUpload,
86
+ existing,
87
+ total
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Build uploading phase progress
93
+ * @param {number} current - Current upload number
94
+ * @param {number} total - Total to upload
95
+ * @returns {Object} Progress object
96
+ */
97
+ export function buildUploadingProgress(current, total) {
98
+ return {
99
+ phase: 'uploading',
100
+ message: 'Uploading screenshots',
101
+ current,
102
+ total
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Build completed phase progress
108
+ * @param {string} buildId - Build ID
109
+ * @param {string|null} url - Build URL
110
+ * @returns {Object} Progress object
111
+ */
112
+ export function buildCompletedProgress(buildId, url) {
113
+ return {
114
+ phase: 'completed',
115
+ message: 'Upload completed',
116
+ buildId,
117
+ url
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Build successful upload result
123
+ * @param {Object} options - Options
124
+ * @param {string} options.buildId - Build ID
125
+ * @param {string|null} options.url - Build URL
126
+ * @param {number} options.total - Total files
127
+ * @param {number} options.uploaded - Files uploaded
128
+ * @param {number} options.skipped - Files skipped
129
+ * @returns {Object} Upload result
130
+ */
131
+ export function buildUploadResult({
132
+ buildId,
133
+ url,
134
+ total,
135
+ uploaded,
136
+ skipped
137
+ }) {
138
+ return {
139
+ success: true,
140
+ buildId,
141
+ url,
142
+ stats: {
143
+ total,
144
+ uploaded,
145
+ skipped
146
+ }
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Build wait result from build response
152
+ * @param {Object} build - Build object from API
153
+ * @returns {Object} Wait result
154
+ */
155
+ export function buildWaitResult(build) {
156
+ let totalComparisons = build.total_comparisons;
157
+ let passedComparisons = build.identical_comparisons;
158
+ let failedComparisons = build.changed_comparisons;
159
+ let newComparisons = build.new_comparisons;
160
+ let identicalComparisons = build.identical_comparisons;
161
+ let hasComparisonCounts = build.total_comparisons !== undefined || build.new_comparisons !== undefined || build.changed_comparisons !== undefined || build.identical_comparisons !== undefined;
162
+ let result = {
163
+ status: build.status,
164
+ build,
165
+ passedComparisons,
166
+ failedComparisons,
167
+ newComparisons,
168
+ identicalComparisons,
169
+ visual_review: build.visual_review || null,
170
+ totalComparisons
171
+ };
172
+ if (hasComparisonCounts) {
173
+ result.comparisons = totalComparisons;
174
+ }
175
+ if (build.url) {
176
+ result.url = build.url;
177
+ }
178
+ return result;
179
+ }