@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.
- package/README.md +12 -3
- package/bin/vizzly.js +1 -1
- package/dist/{cli.js → adapters/cli/cli.js} +50 -35
- package/dist/adapters/cli/setup.js +34 -0
- package/dist/{client → adapters/client}/index.js +6 -7
- package/dist/adapters/http/cloud-proxy.js +29 -0
- package/dist/{server → adapters/http}/http-server.js +12 -13
- package/dist/{server-manager/operations.js → adapters/http/server-lifecycle.js} +10 -13
- package/dist/{server-manager/index.js → adapters/http/server-manager.js} +23 -27
- package/dist/{plugin-api.js → adapters/plugins/plugin-api.js} +1 -1
- package/dist/{plugin-loader.js → adapters/plugins/plugin-loader.js} +1 -1
- package/dist/{services/index.js → adapters/plugins/services.js} +4 -9
- package/dist/adapters/sdk/index.d.ts +89 -0
- package/dist/{sdk → adapters/sdk}/index.js +22 -19
- package/dist/{commands/api.js → domains/api/api.commands.js} +3 -3
- package/dist/{auth/client.js → domains/auth/auth-client.js} +4 -3
- package/dist/domains/auth/auth-errors.js +50 -0
- package/dist/domains/auth/auth-headers.js +41 -0
- package/dist/domains/auth/auth.normalizers.js +50 -0
- package/dist/{server/routers/auth.js → domains/auth/auth.routes.js} +9 -9
- package/dist/domains/auth/auth.rules.js +19 -0
- package/dist/domains/auth/auth.serializers.js +32 -0
- package/dist/{auth/operations.js → domains/auth/auth.service.js} +115 -17
- package/dist/domains/auth/auth.validations.js +21 -0
- package/dist/{api/client.js → domains/auth/cloud-client.js} +11 -18
- package/dist/{commands/login.js → domains/auth/login.commands.js} +11 -8
- package/dist/{commands/logout.js → domains/auth/logout.commands.js} +6 -4
- package/dist/domains/auth/token-store.js +81 -0
- package/dist/{commands/whoami.js → domains/auth/whoami.commands.js} +6 -3
- package/dist/domains/baselines/baseline-download.js +192 -0
- package/dist/domains/baselines/baseline-write.js +22 -0
- package/dist/{server/routers/baseline.js → domains/baselines/baseline.routes.js} +3 -3
- package/dist/{tdd/services/baseline-manager.js → domains/baselines/baseline.service.js} +1 -39
- package/dist/{commands/baselines.js → domains/baselines/baselines.commands.js} +3 -3
- package/dist/domains/baselines/cloud-baselines.js +9 -0
- package/dist/domains/baselines/cloud-baselines.routes.js +42 -0
- package/dist/domains/builds/build.normalizers.js +35 -0
- package/dist/domains/builds/build.serializers.js +164 -0
- package/dist/domains/builds/build.service.js +119 -0
- package/dist/domains/builds/build.validations.js +39 -0
- package/dist/{commands/builds.js → domains/builds/builds.commands.js} +6 -74
- package/dist/domains/builds/cloud-builds.routes.js +31 -0
- package/dist/{commands/finalize.js → domains/builds/finalize.commands.js} +5 -17
- package/dist/{commands/status.js → domains/builds/status.commands.js} +9 -120
- package/dist/domains/comparisons/cloud-comparisons.js +43 -0
- package/dist/{tdd/services/comparison-service.js → domains/comparisons/comparison.service.js} +1 -1
- package/dist/{commands/comparisons.js → domains/comparisons/comparisons.commands.js} +7 -5
- package/dist/domains/config/config-defaults.js +27 -0
- package/dist/{utils → domains/config}/config-discovery.js +1 -9
- package/dist/{utils → domains/config}/config-loader.js +10 -9
- package/dist/{commands/config-cmd.js → domains/config/config.commands.js} +4 -4
- package/dist/{types → domains/config}/config.d.ts +3 -3
- package/dist/domains/config/config.normalizers.js +126 -0
- package/dist/{server/routers/config.js → domains/config/config.routes.js} +13 -13
- package/dist/domains/config/config.rules.js +47 -0
- package/dist/domains/config/config.serializers.js +202 -0
- package/dist/domains/config/config.service.js +625 -0
- package/dist/{utils/config-schema.js → domains/config/config.validations.js} +9 -9
- package/dist/{utils/environment-config.js → domains/config/environment.js} +1 -8
- package/dist/domains/context/compact-context.validations.js +60 -0
- package/dist/domains/context/context-presentation.js +368 -0
- package/dist/domains/context/context.commands.js +385 -0
- package/dist/domains/context/context.serializers.js +157 -0
- package/dist/domains/context/context.service.js +76 -0
- package/dist/domains/context/context.validations.js +100 -0
- package/dist/{context → domains/context}/local-workspace-provider.js +1 -1
- package/dist/{commands/doctor.js → domains/diagnostics/doctor.commands.js} +6 -5
- package/dist/{server/routers/health.js → domains/diagnostics/health.routes.js} +11 -20
- package/dist/{commands/preview.js → domains/previews/preview.commands.js} +9 -7
- package/dist/domains/previews/preview.service.js +37 -0
- package/dist/domains/projects/cloud-projects.routes.js +28 -0
- package/dist/{commands/orgs.js → domains/projects/orgs.commands.js} +6 -8
- package/dist/{utils → domains/projects}/project-binding.js +3 -3
- package/dist/domains/projects/project-errors.js +61 -0
- package/dist/{utils → domains/projects}/project-link-store.js +2 -2
- package/dist/domains/projects/project-requests.js +61 -0
- package/dist/{commands/project.js → domains/projects/project.commands.js} +9 -41
- package/dist/domains/projects/project.normalizers.js +69 -0
- package/dist/{project/operations.js → domains/projects/project.service.js} +89 -16
- package/dist/domains/projects/project.validations.js +31 -0
- package/dist/{commands/projects.js → domains/projects/projects.commands.js} +6 -8
- package/dist/{server/routers/projects.js → domains/projects/projects.routes.js} +5 -5
- package/dist/domains/reports/dashboard.routes.js +96 -0
- package/dist/{server/routers/events.js → domains/reports/events.routes.js} +14 -36
- package/dist/domains/reports/report-assets.routes.js +81 -0
- package/dist/domains/reports/report-state.js +51 -0
- package/dist/domains/reports/report-store.js +85 -0
- package/dist/domains/reports/report.serializers.js +99 -0
- package/dist/{services/static-report-generator.js → domains/reports/report.service.js} +11 -33
- package/dist/{commands/review.js → domains/review/review.commands.js} +4 -4
- package/dist/domains/runs/run-environment.js +69 -0
- package/dist/{services/test-runner.js → domains/runs/run-events.js} +11 -9
- package/dist/{commands/run.js → domains/runs/run.commands.js} +16 -13
- package/dist/domains/runs/run.normalizers.js +8 -0
- package/dist/domains/runs/run.rules.js +56 -0
- package/dist/domains/runs/run.serializers.js +101 -0
- package/dist/{test-runner/operations.js → domains/runs/run.service.js} +35 -7
- package/dist/domains/runs/run.validations.js +17 -0
- package/dist/{utils/session.js → domains/runs/session-store.js} +3 -2
- package/dist/{server/handlers/api-handler.js → domains/screenshots/cloud-handler.js} +4 -4
- package/dist/domains/screenshots/cloud-screenshots.js +109 -0
- package/dist/{utils/file-helpers.js → domains/screenshots/image-files.js} +1 -1
- package/dist/domains/screenshots/screenshot-hash.js +53 -0
- package/dist/domains/screenshots/screenshot-server-options.js +28 -0
- package/dist/{screenshot-server/operations.js → domains/screenshots/screenshot-server.js} +14 -1
- package/dist/domains/screenshots/screenshot.normalizers.js +40 -0
- package/dist/{server/routers/screenshot.js → domains/screenshots/screenshot.routes.js} +4 -4
- package/dist/domains/screenshots/screenshot.serializers.js +146 -0
- package/dist/{utils/security.js → domains/screenshots/screenshot.validations.js} +20 -70
- package/dist/{init/agents.js → domains/setup/agent-setup.js} +54 -14
- package/dist/{commands/init.js → domains/setup/init.commands.js} +11 -44
- package/dist/{server/handlers/tdd-handler.js → domains/tdd/screenshot-handler.js} +46 -281
- package/dist/{commands/tdd-daemon.js → domains/tdd/tdd-daemon.commands.js} +6 -6
- package/dist/{commands/tdd-screenshots.js → domains/tdd/tdd-screenshots.commands.js} +5 -5
- package/dist/{commands/tdd.js → domains/tdd/tdd.commands.js} +16 -15
- package/dist/{tdd/tdd-service.js → domains/tdd/tdd.service.js} +81 -405
- package/dist/domains/updates/cli-update.js +103 -0
- package/dist/domains/updates/update-check.js +2 -0
- package/dist/domains/updates/update-notifications.js +175 -0
- package/dist/domains/uploads/upload-defaults.js +4 -0
- package/dist/domains/uploads/upload-files.js +32 -0
- package/dist/domains/uploads/upload-settings.js +21 -0
- package/dist/domains/uploads/upload-timing.js +18 -0
- package/dist/{commands/upload.js → domains/uploads/upload.commands.js} +11 -9
- package/dist/domains/uploads/upload.normalizers.js +60 -0
- package/dist/domains/uploads/upload.serializers.js +179 -0
- package/dist/{uploader/operations.js → domains/uploads/upload.service.js} +85 -28
- package/dist/domains/uploads/upload.validations.js +77 -0
- package/dist/index.js +12 -12
- package/dist/reporter/reporter-bundle.iife.js +86 -86
- package/dist/reporter-ssr/ssr-entry.js +3 -6
- package/dist/shared/cloud-protocol.js +171 -0
- package/dist/{utils → shared}/file-changes.js +1 -1
- package/dist/shared/files.js +9 -0
- package/dist/{utils → shared}/global-config.js +7 -78
- package/dist/shared/log-level.js +7 -0
- package/dist/{utils → shared}/output.js +1 -1
- package/dist/{utils → shared}/package-info.js +5 -4
- package/dist/shared/paths.js +57 -0
- package/package.json +37 -36
- package/dist/api/core.js +0 -382
- package/dist/api/endpoints.js +0 -421
- package/dist/api/index.js +0 -19
- package/dist/auth/core.js +0 -237
- package/dist/auth/index.js +0 -31
- package/dist/commands/context.js +0 -1066
- package/dist/config/core.js +0 -439
- package/dist/config/operations.js +0 -327
- package/dist/project/core.js +0 -211
- package/dist/screenshot-server/core.js +0 -161
- package/dist/screenshot-server/index.js +0 -11
- package/dist/server/routers/assets.js +0 -95
- package/dist/server/routers/cloud-proxy.js +0 -146
- package/dist/server/routers/dashboard.js +0 -172
- package/dist/services/auth-service.js +0 -117
- package/dist/services/config-service.js +0 -313
- package/dist/services/project-service.js +0 -88
- package/dist/test-runner/core.js +0 -293
- package/dist/test-runner/index.js +0 -13
- package/dist/types/sdk.d.ts +0 -186
- package/dist/uploader/core.js +0 -432
- package/dist/uploader/index.js +0 -88
- /package/dist/{types/client.d.ts → adapters/client/index.d.ts} +0 -0
- /package/dist/{server → adapters/http}/middleware/cors.js +0 -0
- /package/dist/{server → adapters/http}/middleware/json-parser.js +0 -0
- /package/dist/{server → adapters/http}/middleware/response.js +0 -0
- /package/dist/{server-manager/core.js → adapters/http/server-options.js} +0 -0
- /package/dist/{utils/login-poll-wait.js → domains/auth/login-wait.js} +0 -0
- /package/dist/{tdd/metadata → domains/baselines}/baseline-metadata.js +0 -0
- /package/dist/{services/build-manager.js → domains/builds/build-info.js} +0 -0
- /package/dist/{tdd/services/result-service.js → domains/comparisons/comparison-results.js} +0 -0
- /package/dist/{init/core.js → domains/config/config-plan.js} +0 -0
- /package/dist/{utils/config-helpers.js → domains/config/config.js} +0 -0
- /package/dist/{utils/visual-context-normalizers.js → domains/context/context.normalizers.js} +0 -0
- /package/dist/{context → domains/context}/provider-resolver.js +0 -0
- /package/dist/{utils/context.js → domains/diagnostics/environment-context.js} +0 -0
- /package/dist/{utils/report-data.js → domains/reports/report.normalizers.js} +0 -0
- /package/dist/{utils/image-input-detector.js → domains/screenshots/image-input.js} +0 -0
- /package/dist/{tdd/core/signature.js → domains/screenshots/screenshot-identity.js} +0 -0
- /package/dist/{utils → domains/screenshots}/screenshot-options.js +0 -0
- /package/dist/{init/prompts.js → domains/setup/setup-prompts.js} +0 -0
- /package/dist/{tdd → domains/tdd}/runtime-context.js +0 -0
- /package/dist/{tdd → domains/tdd}/server-registry.js +0 -0
- /package/dist/{commands/tdd-daemon-process.js → domains/tdd/tdd-daemon-process.commands.js} +0 -0
- /package/dist/{types/index.d.ts → index.d.ts} +0 -0
- /package/dist/{utils → shared}/api-url.js +0 -0
- /package/dist/{utils → shared}/async-utils.js +0 -0
- /package/dist/{utils → shared}/browser.js +0 -0
- /package/dist/{utils → shared}/ci-env.js +0 -0
- /package/dist/{utils → shared}/colors.js +0 -0
- /package/dist/{utils → shared}/fetch-utils.js +0 -0
- /package/dist/{utils → shared}/git.js +0 -0
- /package/dist/{utils → shared}/patterns.js +0 -0
- /package/dist/{utils → shared}/secret-store.js +0 -0
- /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,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,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 {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
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
|
-
|
|
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
|
+
}
|