oh-my-opencode 4.14.0 → 4.14.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/.agents/skills/codex-qa/references/isolation.md +1 -1
- package/README.md +1 -1
- package/dist/cli/doctor/framework/types.d.ts +2 -0
- package/dist/cli/index.js +223 -1447
- package/dist/cli-node/index.js +223 -1447
- package/dist/index.js +4255 -714
- package/dist/plugin/native-skills.d.ts +9 -0
- package/dist/plugin/system-transform.d.ts +1 -1
- package/dist/skills/frontend/ATTRIBUTION.md +7 -1
- package/dist/skills/frontend/SKILL.md +4 -3
- package/dist/skills/frontend/references/design/README.md +4 -3
- package/dist/skills/frontend/references/design/_INDEX.md +5 -3
- package/dist/skills/frontend/references/design/aside.md +209 -0
- package/dist/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/dist/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/dist/skills/lcx-doctor/SKILL.md +48 -13
- package/dist/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/dist/skills/lcx-report-bug/SKILL.md +44 -13
- package/dist/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/dist/skills/visual-qa/SKILL.md +5 -5
- package/dist/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/dist/skills/visual-qa/scripts/cli.test.ts +7 -2
- package/dist/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
- package/dist/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/dist/tools/delegate-task/skill-resolver.d.ts +1 -0
- package/dist/tools/delegate-task/types.d.ts +2 -0
- package/dist/tools/skill/types.d.ts +1 -0
- package/package.json +26 -13
- package/packages/git-bash-mcp/dist/cli.js +2 -0
- package/packages/lsp-daemon/dist/cli.js +15 -4
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +1 -0
- package/packages/lsp-daemon/dist/ensure-daemon.js +9 -3
- package/packages/lsp-daemon/dist/index.js +15 -4
- package/packages/lsp-daemon/dist/paths.d.ts +1 -0
- package/packages/lsp-daemon/dist/paths.js +6 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +4 -7
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/src/setup.ts +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +123 -10
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +123 -10
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +126 -9
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +141 -0
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +90 -25
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +2 -0
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +2 -0
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +56 -0
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +2 -5
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +2 -0
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +65 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +6 -1
- package/packages/omo-codex/plugin/components/rules/dist/cli.js +8 -106
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +4 -17
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +17 -5
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +63 -1
- package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +80 -50
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +4 -12
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +7 -33
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +4 -12
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-quality-gate.json +7 -7
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +3 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-doc.test.ts +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +7 -6
- package/packages/omo-codex/plugin/package-lock.json +16 -56
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/frontend/ATTRIBUTION.md +7 -1
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +4 -3
- package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +4 -3
- package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +5 -3
- package/packages/omo-codex/plugin/skills/frontend/references/design/aside.md +209 -0
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/packages/omo-codex/plugin/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-doctor/SKILL.md +48 -13
- package/packages/omo-codex/plugin/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +44 -13
- package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/omo-codex/plugin/skills/teammode/SKILL.md +17 -5
- package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +63 -1
- package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +80 -50
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -4
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +5 -5
- package/packages/omo-codex/plugin/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/packages/omo-codex/plugin/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/packages/omo-codex/plugin/test/aggregate-skills.test.mjs +23 -0
- package/packages/omo-codex/plugin/test/bootstrap-binlinks.test.mjs +38 -2
- package/packages/omo-codex/plugin/test/component-bin-names.test.mjs +12 -11
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +99 -1
- package/packages/omo-codex/plugin/test/lcx-bug-skills.test.mjs +62 -8
- package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/teammode-archive-ambiguity.test.mjs +102 -0
- package/packages/omo-codex/plugin/test/teammode-safety-fixture.mjs +41 -1
- package/packages/omo-codex/plugin/test/teammode-safety.test.mjs +93 -0
- package/packages/omo-codex/scripts/install-bin-links.test.mjs +6 -0
- package/packages/omo-codex/scripts/install-cache-copy.test.mjs +28 -0
- package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +43 -3
- package/packages/omo-codex/scripts/install-delegated-command.test.mjs +93 -0
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +169 -41
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +21 -2
- package/packages/omo-codex/scripts/install-local.test.mjs +0 -1
- package/packages/shared-skills/skills/frontend/ATTRIBUTION.md +7 -1
- package/packages/shared-skills/skills/frontend/SKILL.md +4 -3
- package/packages/shared-skills/skills/frontend/references/design/README.md +4 -3
- package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +5 -3
- package/packages/shared-skills/skills/frontend/references/design/aside.md +209 -0
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/SKILL.md +44 -13
- package/packages/shared-skills/skills/lcx-contribute-bug-fix/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/lcx-doctor/SKILL.md +48 -13
- package/packages/shared-skills/skills/lcx-doctor/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +44 -13
- package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +1 -1
- package/packages/shared-skills/skills/visual-qa/SKILL.md +5 -5
- package/packages/shared-skills/skills/visual-qa/references/agent-browser-setup.md +5 -4
- package/packages/shared-skills/skills/visual-qa/scripts/cli.test.ts +7 -2
- package/packages/shared-skills/skills/visual-qa/scripts/skill-prompt-contract.test.ts +10 -2
- package/packages/shared-skills/skills/visual-qa/scripts/visual-qa.mjs +530 -0
- package/dist/cli/sparkshell-appserver-websocket.d.ts +0 -4
- package/dist/cli/sparkshell-appserver.d.ts +0 -16
- package/dist/cli/sparkshell-condense.d.ts +0 -10
- package/dist/cli/sparkshell-parse.d.ts +0 -24
- package/dist/cli/sparkshell-session-context.d.ts +0 -20
- package/dist/cli/sparkshell-spark.d.ts +0 -23
- package/dist/cli/sparkshell.d.ts +0 -33
- package/dist/shared/sparkshell-awareness.d.ts +0 -5
- package/packages/omo-codex/plugin/components/rules/src/sparkshell-awareness.ts +0 -106
- package/packages/omo-codex/plugin/components/rules/test/sparkshell-awareness.test.ts +0 -355
|
@@ -13,7 +13,7 @@ Verify a rendered UI against intent using objective script evidence plus two par
|
|
|
13
13
|
- Use when output must match a mock, a baseline, or a stated design intent; when you suspect a regression; when CJK (Korean/Japanese/Chinese) text may clip, misalign, or wrap awkwardly; when a claimed design system might actually be a flat image; when a terminal layout may overflow or its borders may break.
|
|
14
14
|
- Skip when there is no rendered surface (pure backend or library logic with no visual or terminal output). For broad post-implementation review use review-work; this skill is the visual specialist.
|
|
15
15
|
|
|
16
|
-
In the commands below, `$SKILL_DIR` is this skill's own directory (the folder containing this SKILL.md). The bundled
|
|
16
|
+
In the commands below, `$SKILL_DIR` is this skill's own directory (the folder containing this SKILL.md). The bundled Node evidence CLI lives at `scripts/visual-qa.mjs` inside it; the TypeScript source in `scripts/cli.ts` is for development.
|
|
17
17
|
|
|
18
18
|
## Step 1 - Detect the surface
|
|
19
19
|
|
|
@@ -44,11 +44,11 @@ Every gate runs on captures produced AFTER the last edit to the rendered source.
|
|
|
44
44
|
### Web
|
|
45
45
|
|
|
46
46
|
1. Capture a REFERENCE image: the user's mock/target, generated page snapshot, Figma export, source-site capture, or known-good baseline. Save as PNG. If the user provided overview text or annotations, save them next to the image and treat them as part of the reference packet.
|
|
47
|
-
2. Capture the ACTUAL rendered screenshot at the same viewport size. In Codex, when `browser:control-in-app-browser` is available and the page does not need an authenticated user browser session, use that Browser plugin first for navigation, page state inspection, and screenshots. If it is unavailable or lacks the needed capture action, use the project's configured browser tooling (the playwright, agent-browser, or dev-browser skill). Save as PNG. If none is configured or available, install [agent-browser](https://github.com/vercel-labs/agent-browser) (`
|
|
47
|
+
2. Capture the ACTUAL rendered screenshot at the same viewport size. In Codex, when `browser:control-in-app-browser` is available and the page does not need an authenticated user browser session, use that Browser plugin first for navigation, page state inspection, and screenshots. If it is unavailable or lacks the needed capture action, use the project's configured browser tooling (the playwright, agent-browser, or dev-browser skill). Save as PNG. If none is configured or available, install [agent-browser](https://github.com/vercel-labs/agent-browser) (`npm install -g agent-browser && agent-browser install`) and capture with it — see `$SKILL_DIR/references/agent-browser-setup.md` for the full setup, including how to shoot a fixed-viewport screenshot.
|
|
48
48
|
3. Run the diff and keep the JSON:
|
|
49
49
|
|
|
50
50
|
```
|
|
51
|
-
|
|
51
|
+
node "$SKILL_DIR/scripts/visual-qa.mjs" image-diff <reference.png> <actual.png>
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Key fields: `dimensionsMatch`, `diffRatio` (0..1), `similarityScore` (0..100), `alphaChannelIntact`, `hotspots[]` (grid regions ranked by `diffRatio`).
|
|
@@ -82,7 +82,7 @@ metadata with cleanup receipt.
|
|
|
82
82
|
3. Run the check with the REAL terminal width and keep the JSON:
|
|
83
83
|
|
|
84
84
|
```
|
|
85
|
-
|
|
85
|
+
node "$SKILL_DIR/scripts/visual-qa.mjs" tui-check capture.txt --cols <N>
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
Key fields: `maxWidth`, `overflowLines[]`, `borderMisaligned`, `wideCharColumns[]`, `hasAnsi`.
|
|
@@ -245,7 +245,7 @@ Run this step IN ADDITION to Steps 1-4 when the original user task has a concret
|
|
|
245
245
|
1. Pixel-perfect design-compare subagent (visual oracle). Dispatch a focused, read-only design-compare reviewer (recommend `gpt-5.5` with medium reasoning). It must crop/zoom BOTH the reference (target / Figma export / source-site screenshot / generated page snapshot) and the ACTUAL screenshot into matching regions and read them **pixel-by-pixel** - header, nav, each card, spacing, type ramp, color tokens - not at a glance. It must also compare the overview text or annotations against the rendered content and DOM text. Anchor every claim with the bundled tool:
|
|
246
246
|
|
|
247
247
|
```
|
|
248
|
-
|
|
248
|
+
node "$SKILL_DIR/scripts/visual-qa.mjs" image-diff <reference.png> <actual.png>
|
|
249
249
|
```
|
|
250
250
|
|
|
251
251
|
It judges whether layout geometry, spacing, design tokens (color, type, radius, shadow), and the design itself are identical to the target, region by region. Anything off by more than rounding is a finding.
|
|
@@ -11,11 +11,12 @@ Repo: https://github.com/vercel-labs/agent-browser
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```
|
|
14
|
-
|
|
14
|
+
npm install -g agent-browser && agent-browser install
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`
|
|
18
|
-
managed browser it drives.
|
|
17
|
+
`npm install -g agent-browser` installs the CLI globally; `agent-browser install` downloads the
|
|
18
|
+
managed browser it drives. If Bun is already available, `bun add -g agent-browser && agent-browser install`
|
|
19
|
+
is equivalent, but npm is the default because LazyCodex/Codex installations do not require Bun.
|
|
19
20
|
|
|
20
21
|
Confirm it is ready and discover the current flags:
|
|
21
22
|
|
|
@@ -36,7 +37,7 @@ agent-browser screenshot actual.png # add --full for full-page, --screensho
|
|
|
36
37
|
Then feed the PNG into the diff exactly as in Step 2:
|
|
37
38
|
|
|
38
39
|
```
|
|
39
|
-
|
|
40
|
+
node "$SKILL_DIR/scripts/visual-qa.mjs" image-diff <reference.png> actual.png
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
Match the viewport numbers to whatever the reference/mock was captured at; mismatched
|
|
@@ -55,15 +55,20 @@ describe("run dispatch", () => {
|
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
describe("cli entry", () => {
|
|
58
|
-
test("#given the
|
|
58
|
+
test("#given the Node bundle is spawned for image-diff without Bun on PATH #when it runs #then it prints JSON and exits zero", () => {
|
|
59
59
|
// given
|
|
60
60
|
const dir = tempDir()
|
|
61
61
|
const reference = join(dir, "reference.png")
|
|
62
62
|
const actual = join(dir, "actual.png")
|
|
63
63
|
writeFileSync(reference, encodeRgbaPng(2, 2, solidRgba(2, 2, [0, 0, 0, 255])))
|
|
64
64
|
writeFileSync(actual, encodeRgbaPng(2, 2, solidRgba(2, 2, [255, 255, 255, 255])))
|
|
65
|
+
const nodePath = Bun.which("node")
|
|
66
|
+
if (nodePath === null) throw new Error("node not found on PATH")
|
|
65
67
|
// when
|
|
66
|
-
const proc = Bun.spawnSync(
|
|
68
|
+
const proc = Bun.spawnSync({
|
|
69
|
+
cmd: [nodePath, join(import.meta.dir, "visual-qa.mjs"), "image-diff", reference, actual],
|
|
70
|
+
env: { ...process.env, PATH: "/usr/bin:/bin" },
|
|
71
|
+
})
|
|
67
72
|
// then
|
|
68
73
|
expect(proc.exitCode).toBe(0)
|
|
69
74
|
const parsed: Record<string, unknown> = JSON.parse(proc.stdout.toString())
|
|
@@ -98,7 +98,7 @@ describe("visual-qa skill prompt contract", () => {
|
|
|
98
98
|
const web = sectionBetween(fixture.text, "### Web", "### TUI")
|
|
99
99
|
|
|
100
100
|
expect(web, fixture.label).toContain("agent-browser")
|
|
101
|
-
expect(fixture.text, fixture.label).toContain("
|
|
101
|
+
expect(fixture.text, fixture.label).toContain("npm install -g agent-browser")
|
|
102
102
|
expect(fixture.text, fixture.label).toContain("https://github.com/vercel-labs/agent-browser")
|
|
103
103
|
expect(fixture.text, fixture.label).toContain("references/agent-browser-setup.md")
|
|
104
104
|
}
|
|
@@ -108,12 +108,20 @@ describe("visual-qa skill prompt contract", () => {
|
|
|
108
108
|
expect(existsSync(referencesPath)).toBe(true)
|
|
109
109
|
const doc = readFileSync(referencesPath, "utf8")
|
|
110
110
|
|
|
111
|
-
expect(doc).toContain("
|
|
111
|
+
expect(doc).toContain("npm install -g agent-browser")
|
|
112
112
|
expect(doc).toContain("agent-browser install")
|
|
113
113
|
expect(doc).toContain("https://github.com/vercel-labs/agent-browser")
|
|
114
114
|
expect(doc).toContain("agent-browser --help")
|
|
115
115
|
})
|
|
116
116
|
|
|
117
|
+
test("#given the visual QA evidence commands #when documented #then they use the Node bundle, not a Bun-only TypeScript launcher", () => {
|
|
118
|
+
for (const fixture of fixtures()) {
|
|
119
|
+
expect(fixture.text, fixture.label).toContain('node "$SKILL_DIR/scripts/visual-qa.mjs" image-diff')
|
|
120
|
+
expect(fixture.text, fixture.label).toContain('node "$SKILL_DIR/scripts/visual-qa.mjs" tui-check')
|
|
121
|
+
expect(fixture.text, fixture.label).not.toContain('bun "$SKILL_DIR/scripts/cli.ts"')
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
117
125
|
test("#given a concrete visual target #when in reference-fidelity mode #then dual pixel + code-fidelity verification loops until both pass", () => {
|
|
118
126
|
for (const fixture of fixtures()) {
|
|
119
127
|
const cloneMode = sectionBetween(fixture.text, "## Step 5", "## Reference evidence is not the verdict")
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// packages/shared-skills/skills/visual-qa/scripts/cli.ts
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
// packages/shared-skills/skills/visual-qa/scripts/image-diff.ts
|
|
8
|
+
var GRID_SIZE = 8;
|
|
9
|
+
function round4(value) {
|
|
10
|
+
return Math.round(value * 1e4) / 1e4;
|
|
11
|
+
}
|
|
12
|
+
function pixelsDiffer(ref, refOffset, act, actOffset) {
|
|
13
|
+
return ref[refOffset] !== act[actOffset] || ref[refOffset + 1] !== act[actOffset + 1] || ref[refOffset + 2] !== act[actOffset + 2] || ref[refOffset + 3] !== act[actOffset + 3];
|
|
14
|
+
}
|
|
15
|
+
function buildHotspots(cellDiff, cellTotal, cols, rows, overlapWidth, overlapHeight) {
|
|
16
|
+
const hotspots = [];
|
|
17
|
+
for (let gridY = 0;gridY < rows; gridY++) {
|
|
18
|
+
for (let gridX = 0;gridX < cols; gridX++) {
|
|
19
|
+
const index = gridY * cols + gridX;
|
|
20
|
+
const diff = cellDiff[index] ?? 0;
|
|
21
|
+
const total = cellTotal[index] ?? 0;
|
|
22
|
+
if (diff === 0 || total === 0)
|
|
23
|
+
continue;
|
|
24
|
+
const left = Math.floor(gridX * overlapWidth / cols);
|
|
25
|
+
const right = Math.floor((gridX + 1) * overlapWidth / cols);
|
|
26
|
+
const top = Math.floor(gridY * overlapHeight / rows);
|
|
27
|
+
const bottom = Math.floor((gridY + 1) * overlapHeight / rows);
|
|
28
|
+
hotspots.push({
|
|
29
|
+
gridX,
|
|
30
|
+
gridY,
|
|
31
|
+
x: left,
|
|
32
|
+
y: top,
|
|
33
|
+
width: right - left,
|
|
34
|
+
height: bottom - top,
|
|
35
|
+
diffRatio: round4(diff / total)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
hotspots.sort((a, b) => b.diffRatio - a.diffRatio);
|
|
40
|
+
return hotspots;
|
|
41
|
+
}
|
|
42
|
+
function buildSummary(similarityScore, diffPixels, totalPixels, dimensionsMatch, hotspotCount) {
|
|
43
|
+
const parts = [`${similarityScore}/100 similarity`, `${diffPixels}/${totalPixels} pixels differ`];
|
|
44
|
+
if (!dimensionsMatch)
|
|
45
|
+
parts.push("dimensions differ");
|
|
46
|
+
if (hotspotCount > 0)
|
|
47
|
+
parts.push(`${hotspotCount} hotspot region(s)`);
|
|
48
|
+
return `${parts.join("; ")}.`;
|
|
49
|
+
}
|
|
50
|
+
function diffImages(reference, actual) {
|
|
51
|
+
const overlapWidth = Math.min(reference.width, actual.width);
|
|
52
|
+
const overlapHeight = Math.min(reference.height, actual.height);
|
|
53
|
+
const totalPixels = overlapWidth * overlapHeight;
|
|
54
|
+
const cols = Math.max(1, Math.min(GRID_SIZE, overlapWidth));
|
|
55
|
+
const rows = Math.max(1, Math.min(GRID_SIZE, overlapHeight));
|
|
56
|
+
const cellDiff = new Array(cols * rows).fill(0);
|
|
57
|
+
const cellTotal = new Array(cols * rows).fill(0);
|
|
58
|
+
let diffPixels = 0;
|
|
59
|
+
for (let y = 0;y < overlapHeight; y++) {
|
|
60
|
+
const cellY = Math.min(rows - 1, Math.floor(y * rows / overlapHeight));
|
|
61
|
+
for (let x = 0;x < overlapWidth; x++) {
|
|
62
|
+
const cellX = Math.min(cols - 1, Math.floor(x * cols / overlapWidth));
|
|
63
|
+
const cellIndex = cellY * cols + cellX;
|
|
64
|
+
cellTotal[cellIndex] = (cellTotal[cellIndex] ?? 0) + 1;
|
|
65
|
+
const refOffset = (y * reference.width + x) * 4;
|
|
66
|
+
const actOffset = (y * actual.width + x) * 4;
|
|
67
|
+
if (pixelsDiffer(reference.rgba, refOffset, actual.rgba, actOffset)) {
|
|
68
|
+
diffPixels++;
|
|
69
|
+
cellDiff[cellIndex] = (cellDiff[cellIndex] ?? 0) + 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const diffRatio = totalPixels === 0 ? 0 : diffPixels / totalPixels;
|
|
74
|
+
const similarityScore = Math.round((1 - diffRatio) * 100);
|
|
75
|
+
const hotspots = buildHotspots(cellDiff, cellTotal, cols, rows, overlapWidth, overlapHeight);
|
|
76
|
+
const dimensionsMatch = reference.width === actual.width && reference.height === actual.height;
|
|
77
|
+
const alphaChannelIntact = !(reference.hasTransparentPixels && !actual.hasTransparentPixels);
|
|
78
|
+
return {
|
|
79
|
+
command: "image-diff",
|
|
80
|
+
dimensionsMatch,
|
|
81
|
+
reference: { width: reference.width, height: reference.height },
|
|
82
|
+
actual: { width: actual.width, height: actual.height },
|
|
83
|
+
totalPixels,
|
|
84
|
+
diffPixels,
|
|
85
|
+
diffRatio: round4(diffRatio),
|
|
86
|
+
similarityScore,
|
|
87
|
+
alphaChannelIntact,
|
|
88
|
+
hotspots,
|
|
89
|
+
summary: buildSummary(similarityScore, diffPixels, totalPixels, dimensionsMatch, hotspots.length)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// packages/shared-skills/skills/visual-qa/scripts/png-decode.ts
|
|
94
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
95
|
+
import { inflateSync } from "node:zlib";
|
|
96
|
+
|
|
97
|
+
// packages/shared-skills/skills/visual-qa/scripts/png-crc.ts
|
|
98
|
+
import { Buffer } from "node:buffer";
|
|
99
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
100
|
+
function buildCrcTable() {
|
|
101
|
+
const table = new Uint32Array(256);
|
|
102
|
+
for (let n = 0;n < 256; n++) {
|
|
103
|
+
let c = n;
|
|
104
|
+
for (let k = 0;k < 8; k++) {
|
|
105
|
+
c = (c & 1) === 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
106
|
+
}
|
|
107
|
+
table[n] = c >>> 0;
|
|
108
|
+
}
|
|
109
|
+
return table;
|
|
110
|
+
}
|
|
111
|
+
var CRC_TABLE = buildCrcTable();
|
|
112
|
+
|
|
113
|
+
// packages/shared-skills/skills/visual-qa/scripts/png-decode.ts
|
|
114
|
+
class PngDecodeError extends Error {
|
|
115
|
+
name = "PngDecodeError";
|
|
116
|
+
}
|
|
117
|
+
function readChunks(buffer) {
|
|
118
|
+
const chunks = [];
|
|
119
|
+
let offset = 8;
|
|
120
|
+
while (offset + 8 <= buffer.length) {
|
|
121
|
+
const length = buffer.readUInt32BE(offset);
|
|
122
|
+
const type = buffer.toString("ascii", offset + 4, offset + 8);
|
|
123
|
+
const dataStart = offset + 8;
|
|
124
|
+
const dataEnd = dataStart + length;
|
|
125
|
+
if (dataEnd + 4 > buffer.length)
|
|
126
|
+
break;
|
|
127
|
+
chunks.push({ type, data: buffer.subarray(dataStart, dataEnd) });
|
|
128
|
+
offset = dataEnd + 4;
|
|
129
|
+
}
|
|
130
|
+
return chunks;
|
|
131
|
+
}
|
|
132
|
+
function channelsForColorType(colorType) {
|
|
133
|
+
switch (colorType) {
|
|
134
|
+
case 0:
|
|
135
|
+
return 1;
|
|
136
|
+
case 2:
|
|
137
|
+
return 3;
|
|
138
|
+
case 4:
|
|
139
|
+
return 2;
|
|
140
|
+
case 6:
|
|
141
|
+
return 4;
|
|
142
|
+
default:
|
|
143
|
+
throw new PngDecodeError(`unsupported color type ${colorType}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function parseHeader(data) {
|
|
147
|
+
if (data.length < 13) {
|
|
148
|
+
throw new PngDecodeError("invalid IHDR chunk length");
|
|
149
|
+
}
|
|
150
|
+
const colorType = data[9] ?? 0;
|
|
151
|
+
return {
|
|
152
|
+
width: data.readUInt32BE(0),
|
|
153
|
+
height: data.readUInt32BE(4),
|
|
154
|
+
bitDepth: data[8] ?? 0,
|
|
155
|
+
colorType,
|
|
156
|
+
channels: channelsForColorType(colorType)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function paeth(a, b, c) {
|
|
160
|
+
const p = a + b - c;
|
|
161
|
+
const pa = Math.abs(p - a);
|
|
162
|
+
const pb = Math.abs(p - b);
|
|
163
|
+
const pc = Math.abs(p - c);
|
|
164
|
+
if (pa <= pb && pa <= pc)
|
|
165
|
+
return a;
|
|
166
|
+
if (pb <= pc)
|
|
167
|
+
return b;
|
|
168
|
+
return c;
|
|
169
|
+
}
|
|
170
|
+
function unfilterRow(filterType, row, prev, bpp) {
|
|
171
|
+
const out = Buffer2.alloc(row.length);
|
|
172
|
+
for (let i = 0;i < row.length; i++) {
|
|
173
|
+
const raw = row[i] ?? 0;
|
|
174
|
+
const a = i >= bpp ? out[i - bpp] ?? 0 : 0;
|
|
175
|
+
const b = prev ? prev[i] ?? 0 : 0;
|
|
176
|
+
const c = i >= bpp && prev ? prev[i - bpp] ?? 0 : 0;
|
|
177
|
+
switch (filterType) {
|
|
178
|
+
case 0:
|
|
179
|
+
out[i] = raw;
|
|
180
|
+
break;
|
|
181
|
+
case 1:
|
|
182
|
+
out[i] = raw + a & 255;
|
|
183
|
+
break;
|
|
184
|
+
case 2:
|
|
185
|
+
out[i] = raw + b & 255;
|
|
186
|
+
break;
|
|
187
|
+
case 3:
|
|
188
|
+
out[i] = raw + (a + b >> 1) & 255;
|
|
189
|
+
break;
|
|
190
|
+
case 4:
|
|
191
|
+
out[i] = raw + paeth(a, b, c) & 255;
|
|
192
|
+
break;
|
|
193
|
+
default:
|
|
194
|
+
throw new PngDecodeError(`unsupported filter type ${filterType}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
function decodePixels(idat, width, height, bpp) {
|
|
200
|
+
const inflated = inflateSync(idat);
|
|
201
|
+
const rowBytes = width * bpp;
|
|
202
|
+
if (inflated.length < height * (rowBytes + 1)) {
|
|
203
|
+
throw new PngDecodeError("truncated image data");
|
|
204
|
+
}
|
|
205
|
+
const pixels = Buffer2.alloc(width * height * bpp);
|
|
206
|
+
let prev = null;
|
|
207
|
+
for (let y = 0;y < height; y++) {
|
|
208
|
+
const rowStart = y * (rowBytes + 1);
|
|
209
|
+
const filterType = inflated[rowStart] ?? 0;
|
|
210
|
+
const filtered = inflated.subarray(rowStart + 1, rowStart + 1 + rowBytes);
|
|
211
|
+
const row = unfilterRow(filterType, filtered, prev, bpp);
|
|
212
|
+
row.copy(pixels, y * rowBytes);
|
|
213
|
+
prev = row;
|
|
214
|
+
}
|
|
215
|
+
return pixels;
|
|
216
|
+
}
|
|
217
|
+
function normalizeToRgba(pixels, pixelCount, channels) {
|
|
218
|
+
const rgba = new Uint8Array(pixelCount * 4);
|
|
219
|
+
let hasTransparent = false;
|
|
220
|
+
for (let i = 0;i < pixelCount; i++) {
|
|
221
|
+
const src = i * channels;
|
|
222
|
+
let r = 0;
|
|
223
|
+
let g = 0;
|
|
224
|
+
let b = 0;
|
|
225
|
+
let a = 255;
|
|
226
|
+
switch (channels) {
|
|
227
|
+
case 1: {
|
|
228
|
+
const v = pixels[src] ?? 0;
|
|
229
|
+
r = v;
|
|
230
|
+
g = v;
|
|
231
|
+
b = v;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 2: {
|
|
235
|
+
const v = pixels[src] ?? 0;
|
|
236
|
+
r = v;
|
|
237
|
+
g = v;
|
|
238
|
+
b = v;
|
|
239
|
+
a = pixels[src + 1] ?? 255;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case 3: {
|
|
243
|
+
r = pixels[src] ?? 0;
|
|
244
|
+
g = pixels[src + 1] ?? 0;
|
|
245
|
+
b = pixels[src + 2] ?? 0;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
default: {
|
|
249
|
+
r = pixels[src] ?? 0;
|
|
250
|
+
g = pixels[src + 1] ?? 0;
|
|
251
|
+
b = pixels[src + 2] ?? 0;
|
|
252
|
+
a = pixels[src + 3] ?? 255;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const dst = i * 4;
|
|
256
|
+
rgba[dst] = r;
|
|
257
|
+
rgba[dst + 1] = g;
|
|
258
|
+
rgba[dst + 2] = b;
|
|
259
|
+
rgba[dst + 3] = a;
|
|
260
|
+
if (a < 255)
|
|
261
|
+
hasTransparent = true;
|
|
262
|
+
}
|
|
263
|
+
return { rgba, hasTransparent };
|
|
264
|
+
}
|
|
265
|
+
function decodePng(buffer) {
|
|
266
|
+
if (buffer.length < 8 || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
267
|
+
throw new PngDecodeError("not a PNG file (bad signature)");
|
|
268
|
+
}
|
|
269
|
+
const chunks = readChunks(buffer);
|
|
270
|
+
const ihdr = chunks.find((chunk) => chunk.type === "IHDR");
|
|
271
|
+
if (ihdr === undefined) {
|
|
272
|
+
throw new PngDecodeError("missing IHDR chunk");
|
|
273
|
+
}
|
|
274
|
+
const header = parseHeader(ihdr.data);
|
|
275
|
+
if (header.bitDepth !== 8) {
|
|
276
|
+
throw new PngDecodeError(`unsupported bit depth ${header.bitDepth}`);
|
|
277
|
+
}
|
|
278
|
+
const idatChunks = chunks.filter((chunk) => chunk.type === "IDAT");
|
|
279
|
+
if (idatChunks.length === 0) {
|
|
280
|
+
throw new PngDecodeError("missing IDAT chunk");
|
|
281
|
+
}
|
|
282
|
+
const idat = Buffer2.concat(idatChunks.map((chunk) => chunk.data));
|
|
283
|
+
const pixels = decodePixels(idat, header.width, header.height, header.channels);
|
|
284
|
+
const normalized = normalizeToRgba(pixels, header.width * header.height, header.channels);
|
|
285
|
+
return {
|
|
286
|
+
width: header.width,
|
|
287
|
+
height: header.height,
|
|
288
|
+
rgba: normalized.rgba,
|
|
289
|
+
hasAlphaChannel: header.colorType === 4 || header.colorType === 6,
|
|
290
|
+
hasTransparentPixels: normalized.hasTransparent
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// packages/shared-skills/skills/visual-qa/scripts/ansi.ts
|
|
295
|
+
var ESC = String.fromCharCode(27);
|
|
296
|
+
var CSI = String.fromCharCode(155);
|
|
297
|
+
var ANSI_PATTERN = new RegExp(`[${ESC}${CSI}][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]`, "g");
|
|
298
|
+
function stripAnsi(input) {
|
|
299
|
+
return input.replace(ANSI_PATTERN, "");
|
|
300
|
+
}
|
|
301
|
+
function hasAnsi(input) {
|
|
302
|
+
return stripAnsi(input) !== input;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// packages/shared-skills/skills/visual-qa/scripts/east-asian-width.ts
|
|
306
|
+
var ZERO_WIDTH_RANGES = [
|
|
307
|
+
{ start: 768, end: 879 },
|
|
308
|
+
{ start: 1155, end: 1161 },
|
|
309
|
+
{ start: 1425, end: 1469 },
|
|
310
|
+
{ start: 1552, end: 1562 },
|
|
311
|
+
{ start: 1611, end: 1631 },
|
|
312
|
+
{ start: 1648, end: 1648 },
|
|
313
|
+
{ start: 1750, end: 1756 },
|
|
314
|
+
{ start: 4448, end: 4607 },
|
|
315
|
+
{ start: 8203, end: 8207 },
|
|
316
|
+
{ start: 8234, end: 8238 },
|
|
317
|
+
{ start: 8288, end: 8292 },
|
|
318
|
+
{ start: 8400, end: 8447 },
|
|
319
|
+
{ start: 65056, end: 65071 },
|
|
320
|
+
{ start: 65279, end: 65279 }
|
|
321
|
+
];
|
|
322
|
+
var WIDE_RANGES = [
|
|
323
|
+
{ start: 4352, end: 4447 },
|
|
324
|
+
{ start: 8986, end: 8987 },
|
|
325
|
+
{ start: 11904, end: 12350 },
|
|
326
|
+
{ start: 12353, end: 13311 },
|
|
327
|
+
{ start: 13312, end: 19903 },
|
|
328
|
+
{ start: 19968, end: 40959 },
|
|
329
|
+
{ start: 40960, end: 42191 },
|
|
330
|
+
{ start: 43360, end: 43391 },
|
|
331
|
+
{ start: 44032, end: 55203 },
|
|
332
|
+
{ start: 63744, end: 64255 },
|
|
333
|
+
{ start: 65040, end: 65049 },
|
|
334
|
+
{ start: 65072, end: 65135 },
|
|
335
|
+
{ start: 65280, end: 65376 },
|
|
336
|
+
{ start: 65504, end: 65510 },
|
|
337
|
+
{ start: 110592, end: 110959 },
|
|
338
|
+
{ start: 127488, end: 127743 },
|
|
339
|
+
{ start: 127744, end: 129791 },
|
|
340
|
+
{ start: 131072, end: 262141 }
|
|
341
|
+
];
|
|
342
|
+
function inRanges(codePoint, ranges) {
|
|
343
|
+
for (const range of ranges) {
|
|
344
|
+
if (codePoint >= range.start && codePoint <= range.end) {
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
function charWidth(codePoint) {
|
|
351
|
+
if (codePoint === 0)
|
|
352
|
+
return 0;
|
|
353
|
+
if (codePoint < 32)
|
|
354
|
+
return 0;
|
|
355
|
+
if (codePoint >= 127 && codePoint <= 159)
|
|
356
|
+
return 0;
|
|
357
|
+
if (inRanges(codePoint, ZERO_WIDTH_RANGES))
|
|
358
|
+
return 0;
|
|
359
|
+
if (inRanges(codePoint, WIDE_RANGES))
|
|
360
|
+
return 2;
|
|
361
|
+
return 1;
|
|
362
|
+
}
|
|
363
|
+
function stringWidth(text) {
|
|
364
|
+
let total = 0;
|
|
365
|
+
for (const char of text) {
|
|
366
|
+
const codePoint = char.codePointAt(0);
|
|
367
|
+
if (codePoint === undefined)
|
|
368
|
+
continue;
|
|
369
|
+
total += charWidth(codePoint);
|
|
370
|
+
}
|
|
371
|
+
return total;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// packages/shared-skills/skills/visual-qa/scripts/tui-grid.ts
|
|
375
|
+
var BOX_DRAWING_START = 9472;
|
|
376
|
+
var BOX_DRAWING_END = 9599;
|
|
377
|
+
var MAX_WIDE_COLUMNS = 64;
|
|
378
|
+
function splitLines(text) {
|
|
379
|
+
const lines = text.split(/\r?\n/);
|
|
380
|
+
if (lines.length > 1 && lines[lines.length - 1] === "")
|
|
381
|
+
lines.pop();
|
|
382
|
+
return lines;
|
|
383
|
+
}
|
|
384
|
+
function isFrameLine(plain) {
|
|
385
|
+
for (const char of plain) {
|
|
386
|
+
const codePoint = char.codePointAt(0);
|
|
387
|
+
if (codePoint !== undefined && codePoint >= BOX_DRAWING_START && codePoint <= BOX_DRAWING_END) {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
function wideStartColumns(plain) {
|
|
394
|
+
const columns = [];
|
|
395
|
+
let column = 0;
|
|
396
|
+
for (const char of plain) {
|
|
397
|
+
const codePoint = char.codePointAt(0);
|
|
398
|
+
if (codePoint === undefined)
|
|
399
|
+
continue;
|
|
400
|
+
const width = charWidth(codePoint);
|
|
401
|
+
if (width === 2)
|
|
402
|
+
columns.push(column);
|
|
403
|
+
column += width;
|
|
404
|
+
}
|
|
405
|
+
return columns;
|
|
406
|
+
}
|
|
407
|
+
function summarize(lineCount, maxWidth, expectedColumns, overflowCount, borderMisaligned, containsAnsi) {
|
|
408
|
+
const parts = [`${lineCount} line(s)`, `max width ${maxWidth}/${expectedColumns}`];
|
|
409
|
+
if (overflowCount > 0)
|
|
410
|
+
parts.push(`${overflowCount} overflow line(s)`);
|
|
411
|
+
if (borderMisaligned)
|
|
412
|
+
parts.push("borders misaligned");
|
|
413
|
+
if (containsAnsi)
|
|
414
|
+
parts.push("contains ANSI");
|
|
415
|
+
return `${parts.join("; ")}.`;
|
|
416
|
+
}
|
|
417
|
+
function checkTui(text, expectedColumns) {
|
|
418
|
+
const lines = splitLines(text);
|
|
419
|
+
const lineWidths = [];
|
|
420
|
+
const overflowLines = [];
|
|
421
|
+
const wideColumns = new Set;
|
|
422
|
+
const frameWidths = new Set;
|
|
423
|
+
for (let index = 0;index < lines.length; index++) {
|
|
424
|
+
const plain = stripAnsi(lines[index] ?? "");
|
|
425
|
+
const width = stringWidth(plain);
|
|
426
|
+
lineWidths.push(width);
|
|
427
|
+
if (expectedColumns > 0 && width > expectedColumns) {
|
|
428
|
+
overflowLines.push({ line: index + 1, width });
|
|
429
|
+
}
|
|
430
|
+
if (isFrameLine(plain))
|
|
431
|
+
frameWidths.add(width);
|
|
432
|
+
for (const column of wideStartColumns(plain)) {
|
|
433
|
+
if (wideColumns.size < MAX_WIDE_COLUMNS)
|
|
434
|
+
wideColumns.add(column);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const maxWidth = lineWidths.reduce((max, width) => width > max ? width : max, 0);
|
|
438
|
+
const borderMisaligned = frameWidths.size > 1;
|
|
439
|
+
const containsAnsi = hasAnsi(text);
|
|
440
|
+
return {
|
|
441
|
+
command: "tui-check",
|
|
442
|
+
expectedColumns,
|
|
443
|
+
lineCount: lines.length,
|
|
444
|
+
lineWidths,
|
|
445
|
+
maxWidth,
|
|
446
|
+
overflowLines,
|
|
447
|
+
borderMisaligned,
|
|
448
|
+
wideCharColumns: [...wideColumns].sort((a, b) => a - b),
|
|
449
|
+
hasAnsi: containsAnsi,
|
|
450
|
+
summary: summarize(lines.length, maxWidth, expectedColumns, overflowLines.length, borderMisaligned, containsAnsi)
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// packages/shared-skills/skills/visual-qa/scripts/cli.ts
|
|
455
|
+
class CliError extends Error {
|
|
456
|
+
name = "CliError";
|
|
457
|
+
}
|
|
458
|
+
var DEFAULT_COLUMNS = 80;
|
|
459
|
+
var COLS_FLAG = "--cols";
|
|
460
|
+
function runImageDiff(args) {
|
|
461
|
+
const referencePath = args[0];
|
|
462
|
+
const actualPath = args[1];
|
|
463
|
+
if (referencePath === undefined || actualPath === undefined) {
|
|
464
|
+
throw new CliError("usage: image-diff <reference.png> <actual.png>");
|
|
465
|
+
}
|
|
466
|
+
const reference = decodePng(readFileSync(referencePath));
|
|
467
|
+
const actual = decodePng(readFileSync(actualPath));
|
|
468
|
+
return diffImages(reference, actual);
|
|
469
|
+
}
|
|
470
|
+
function parseColumns(args) {
|
|
471
|
+
for (let index = 0;index < args.length; index++) {
|
|
472
|
+
const arg = args[index] ?? "";
|
|
473
|
+
if (arg === COLS_FLAG) {
|
|
474
|
+
const parsed = Number(args[index + 1]);
|
|
475
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
476
|
+
throw new CliError(`${COLS_FLAG} requires a positive integer`);
|
|
477
|
+
}
|
|
478
|
+
return parsed;
|
|
479
|
+
}
|
|
480
|
+
if (arg.startsWith(`${COLS_FLAG}=`)) {
|
|
481
|
+
const parsed = Number(arg.slice(COLS_FLAG.length + 1));
|
|
482
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
483
|
+
throw new CliError(`${COLS_FLAG} requires a positive integer`);
|
|
484
|
+
}
|
|
485
|
+
return parsed;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return DEFAULT_COLUMNS;
|
|
489
|
+
}
|
|
490
|
+
function runTuiCheck(args) {
|
|
491
|
+
const capturePath = args[0];
|
|
492
|
+
if (capturePath === undefined || capturePath.startsWith("--")) {
|
|
493
|
+
throw new CliError("usage: tui-check <capture.txt> [--cols N]");
|
|
494
|
+
}
|
|
495
|
+
const text = readFileSync(capturePath, "utf8");
|
|
496
|
+
return checkTui(text, parseColumns(args.slice(1)));
|
|
497
|
+
}
|
|
498
|
+
function run(argv) {
|
|
499
|
+
const command = argv[0];
|
|
500
|
+
const rest = argv.slice(1);
|
|
501
|
+
switch (command) {
|
|
502
|
+
case "image-diff":
|
|
503
|
+
return runImageDiff(rest);
|
|
504
|
+
case "tui-check":
|
|
505
|
+
return runTuiCheck(rest);
|
|
506
|
+
default:
|
|
507
|
+
throw new CliError(`unknown command "${command ?? ""}"; expected "image-diff" or "tui-check"`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function main(argv) {
|
|
511
|
+
try {
|
|
512
|
+
const result = run(argv);
|
|
513
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
514
|
+
`);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
517
|
+
process.stderr.write(`visual-qa error: ${message}
|
|
518
|
+
`);
|
|
519
|
+
process.exitCode = 1;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (__require.main == __require.module) {
|
|
523
|
+
main(process.argv.slice(2));
|
|
524
|
+
}
|
|
525
|
+
export {
|
|
526
|
+
runTuiCheck,
|
|
527
|
+
runImageDiff,
|
|
528
|
+
run,
|
|
529
|
+
CliError
|
|
530
|
+
};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { type Socket } from "node:net";
|
|
2
|
-
export declare function connectUnixWebSocket(socketPath: string): Promise<Socket>;
|
|
3
|
-
export declare function writeWebSocketText(socket: Socket, payload: string): void;
|
|
4
|
-
export declare function readWebSocketText(socket: Socket): Promise<string>;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export type RuntimeEnv = Readonly<Record<string, string | undefined>>;
|
|
2
|
-
export type SparkShellAppServerCommand = {
|
|
3
|
-
readonly argv: readonly string[];
|
|
4
|
-
readonly cwd: string;
|
|
5
|
-
readonly env: RuntimeEnv;
|
|
6
|
-
};
|
|
7
|
-
export type SparkShellAppServerResult = {
|
|
8
|
-
readonly exitCode: number;
|
|
9
|
-
readonly stdout: string;
|
|
10
|
-
readonly stderr: string;
|
|
11
|
-
};
|
|
12
|
-
export interface SparkShellAppServerClient {
|
|
13
|
-
getPlatform(): Promise<NodeJS.Platform>;
|
|
14
|
-
exec(command: SparkShellAppServerCommand): Promise<SparkShellAppServerResult>;
|
|
15
|
-
}
|
|
16
|
-
export declare function createDefaultSparkShellAppServerClient(env: RuntimeEnv): SparkShellAppServerClient | null;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export type CondenseOptions = {
|
|
2
|
-
readonly budgetChars: number;
|
|
3
|
-
readonly hints: readonly string[];
|
|
4
|
-
};
|
|
5
|
-
export type CondenseResult = {
|
|
6
|
-
readonly output: string;
|
|
7
|
-
readonly condensed: boolean;
|
|
8
|
-
};
|
|
9
|
-
export declare function condenseOutput(text: string, options: CondenseOptions): CondenseResult;
|
|
10
|
-
export declare function extractContextHints(requests: readonly string[]): readonly string[];
|