@neonwatty/limner 0.1.0
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 +161 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +26 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/capture.d.ts +12 -0
- package/dist/commands/capture.js +66 -0
- package/dist/commands/capture.js.map +1 -0
- package/dist/commands/compare-image-app.d.ts +12 -0
- package/dist/commands/compare-image-app.js +45 -0
- package/dist/commands/compare-image-app.js.map +1 -0
- package/dist/commands/compare-image-reference.d.ts +28 -0
- package/dist/commands/compare-image-reference.js +82 -0
- package/dist/commands/compare-image-reference.js.map +1 -0
- package/dist/commands/compare.d.ts +15 -0
- package/dist/commands/compare.js +168 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.js +65 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/preview.d.ts +2 -0
- package/dist/commands/preview.js +27 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.js +14 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/runs.d.ts +2 -0
- package/dist/commands/runs.js +63 -0
- package/dist/commands/runs.js.map +1 -0
- package/dist/core/dom-metrics.d.ts +18 -0
- package/dist/core/dom-metrics.js +54 -0
- package/dist/core/dom-metrics.js.map +1 -0
- package/dist/core/playwright-capture.d.ts +27 -0
- package/dist/core/playwright-capture.js +46 -0
- package/dist/core/playwright-capture.js.map +1 -0
- package/dist/core/playwright-dom.d.ts +16 -0
- package/dist/core/playwright-dom.js +64 -0
- package/dist/core/playwright-dom.js.map +1 -0
- package/dist/core/reference-dom-facts.d.ts +65 -0
- package/dist/core/reference-dom-facts.js +71 -0
- package/dist/core/reference-dom-facts.js.map +1 -0
- package/dist/core/report-writer.d.ts +43 -0
- package/dist/core/report-writer.js +181 -0
- package/dist/core/report-writer.js.map +1 -0
- package/dist/core/run-logger.d.ts +22 -0
- package/dist/core/run-logger.js +67 -0
- package/dist/core/run-logger.js.map +1 -0
- package/dist/core/side-by-side.d.ts +8 -0
- package/dist/core/side-by-side.js +33 -0
- package/dist/core/side-by-side.js.map +1 -0
- package/dist/core/static-server.d.ts +6 -0
- package/dist/core/static-server.js +73 -0
- package/dist/core/static-server.js.map +1 -0
- package/dist/core/visual-spec-agent-pack.d.ts +27 -0
- package/dist/core/visual-spec-agent-pack.js +143 -0
- package/dist/core/visual-spec-agent-pack.js.map +1 -0
- package/dist/core/visual-spec-prompts.d.ts +32 -0
- package/dist/core/visual-spec-prompts.js +129 -0
- package/dist/core/visual-spec-prompts.js.map +1 -0
- package/dist/core/workspace.d.ts +31 -0
- package/dist/core/workspace.js +97 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/contract.d.ts +43 -0
- package/dist/schemas/contract.js +19 -0
- package/dist/schemas/contract.js.map +1 -0
- package/dist/schemas/events.d.ts +34 -0
- package/dist/schemas/events.js +29 -0
- package/dist/schemas/events.js.map +1 -0
- package/dist/schemas/visual-spec.d.ts +410 -0
- package/dist/schemas/visual-spec.js +201 -0
- package/dist/schemas/visual-spec.js.map +1 -0
- package/docs/agent-workflow.md +45 -0
- package/package.json +64 -0
- package/skills/limner/SKILL.md +51 -0
- package/templates/target/AGENT_GUIDE.md +24 -0
- package/templates/target/contract/acceptance.md +23 -0
- package/templates/target/contract/regions.json +11 -0
- package/templates/target/contract/tokens.json +21 -0
- package/templates/target/reference/index.html +22 -0
- package/templates/target/reference/styles.css +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Limner
|
|
2
|
+
|
|
3
|
+
Limner is an agent-guided visual fidelity workbench. It helps a coding agent turn an input image into a structured HTML reference, then compare that approved reference against a real app implementation.
|
|
4
|
+
|
|
5
|
+
Limner is model-agnostic. It does not call a vision model and it does not produce hard pass/fail scores. It produces artifacts an agent and human can inspect: screenshots, side-by-sides, DOM metrics, reports, contracts, and local JSONL run logs.
|
|
6
|
+
|
|
7
|
+
The optional visual spec workflow is agent-required. Limner can prepare the prompt pack and validate structured agent output, but it does not extract that spec on its own.
|
|
8
|
+
|
|
9
|
+
## Install Locally
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install
|
|
13
|
+
npm run build
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run from source during development:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run dev -- --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
After build:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node dist/cli.js --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run the full local gate before PRs:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm run check
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`npm run check` runs ESLint, TypeScript, Vitest, build, Knip, and a tracked-file line-count guard.
|
|
35
|
+
|
|
36
|
+
## Basic Workflow
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
limn init ./ideal.png --target replay-boundaries
|
|
40
|
+
limn preview --target replay-boundaries
|
|
41
|
+
limn capture reference --target replay-boundaries
|
|
42
|
+
limn compare image-reference --target replay-boundaries
|
|
43
|
+
limn compare image-app --target replay-boundaries --url http://localhost:3152/internal/optimization-lab/wedding-envelope#replay-boundaries --storage-state ./e2e/.auth/user.json
|
|
44
|
+
limn compare reference-app --target replay-boundaries --url http://localhost:3152/internal/optimization-lab/wedding-envelope#replay-boundaries --storage-state ./e2e/.auth/user.json
|
|
45
|
+
limn report --target replay-boundaries
|
|
46
|
+
limn runs list
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Workspace Shape
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
limner-workspace/
|
|
53
|
+
limner.config.ts
|
|
54
|
+
targets/
|
|
55
|
+
replay-boundaries/
|
|
56
|
+
source/ideal.png
|
|
57
|
+
contract/regions.json
|
|
58
|
+
contract/tokens.json
|
|
59
|
+
contract/acceptance.md
|
|
60
|
+
reference/index.html
|
|
61
|
+
reference/styles.css
|
|
62
|
+
captures/
|
|
63
|
+
reports/
|
|
64
|
+
AGENT_GUIDE.md
|
|
65
|
+
.limner/runs/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Modes
|
|
69
|
+
|
|
70
|
+
### Image To Reference
|
|
71
|
+
|
|
72
|
+
Use this while recreating the input image in standalone HTML/CSS.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
limn compare image-reference --target replay-boundaries
|
|
76
|
+
limn compare image-reference --target replay-boundaries --spec
|
|
77
|
+
limn compare image-reference --target replay-boundaries --spec --spec-instructions ./visual-spec-policy.md
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Outputs:
|
|
81
|
+
|
|
82
|
+
- `captures/image-reference/reference.png`
|
|
83
|
+
- `captures/image-reference/side-by-side.png`
|
|
84
|
+
- `reports/image-reference.md`
|
|
85
|
+
|
|
86
|
+
With `--spec`, Limner also writes an agent handoff pack under `captures/image-reference/spec/`:
|
|
87
|
+
|
|
88
|
+
- `agent-prompt.md`
|
|
89
|
+
- `agent-prompt.codex.md`
|
|
90
|
+
- `agent-prompt.claude.md`
|
|
91
|
+
- `reference-dom-facts.json`
|
|
92
|
+
- `agent-response.schema.json`
|
|
93
|
+
- `agent-response.example.json`
|
|
94
|
+
- expected agent output path: `agent-response.json`
|
|
95
|
+
|
|
96
|
+
Limner also creates `contract/visual-spec-instructions.md` if it does not exist. Edit that file to customize the generated Codex and Claude prompts for a target, or pass `--spec-instructions <path>` to use a shared prompt policy file.
|
|
97
|
+
|
|
98
|
+
After an agent writes a valid `agent-response.json`, rerun the same command and Limner will validate it and split out:
|
|
99
|
+
|
|
100
|
+
- `captures/image-reference/ideal-visual-spec.json`
|
|
101
|
+
- `captures/image-reference/reference-visual-spec.json`
|
|
102
|
+
- `captures/image-reference/visual-spec-diff.json`
|
|
103
|
+
|
|
104
|
+
The prompt pack is tuned for two common flows:
|
|
105
|
+
|
|
106
|
+
- `agent-prompt.codex.md`: for Codex reading local files and writing `agent-response.json` directly in the repo.
|
|
107
|
+
- `agent-prompt.claude.md`: for Claude with attached images and strict structured output, then copying the JSON into `agent-response.json`.
|
|
108
|
+
|
|
109
|
+
Agents should inspect the ideal image and reference screenshot separately. The side-by-side image is comparison context, not the source image to parse.
|
|
110
|
+
|
|
111
|
+
### Image To App
|
|
112
|
+
|
|
113
|
+
Use this for quick grounding checks against a real app before or during reconstruction.
|
|
114
|
+
It compares the source image directly to a captured app viewport.
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
limn compare image-app --target replay-boundaries --url http://localhost:3152/... --storage-state ./e2e/.auth/user.json
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Outputs:
|
|
121
|
+
|
|
122
|
+
- `captures/image-app/app.png`
|
|
123
|
+
- `captures/image-app/side-by-side.png`
|
|
124
|
+
- `reports/image-app.md`
|
|
125
|
+
|
|
126
|
+
### Reference To App
|
|
127
|
+
|
|
128
|
+
Use this after the reference HTML is approved and the real app needs to match it.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
limn compare reference-app --target replay-boundaries --url http://localhost:3152/... --storage-state ./e2e/.auth/user.json
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Outputs:
|
|
135
|
+
|
|
136
|
+
- `captures/reference-app/reference.png`
|
|
137
|
+
- `captures/reference-app/app.png`
|
|
138
|
+
- `captures/reference-app/side-by-side.png`
|
|
139
|
+
- `captures/reference-app/dom-metrics.json`
|
|
140
|
+
- `reports/reference-app.md`
|
|
141
|
+
|
|
142
|
+
Capture commands default to viewport-only screenshots. Add `--full-page` when the full scrollable page is the comparison target.
|
|
143
|
+
|
|
144
|
+
## Local Logs
|
|
145
|
+
|
|
146
|
+
Limner writes local structured logs only. There is no remote telemetry.
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
.limner/runs/<run-id>/
|
|
150
|
+
manifest.json
|
|
151
|
+
events.jsonl
|
|
152
|
+
agent-notes.md
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
limn runs list
|
|
159
|
+
limn runs show <run-id>
|
|
160
|
+
limn runs summarize
|
|
161
|
+
```
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerCaptureCommand } from './commands/capture.js';
|
|
4
|
+
import { registerCompareCommand } from './commands/compare.js';
|
|
5
|
+
import { registerInitCommand } from './commands/init.js';
|
|
6
|
+
import { registerPreviewCommand } from './commands/preview.js';
|
|
7
|
+
import { registerReportCommand } from './commands/report.js';
|
|
8
|
+
import { registerRunsCommand } from './commands/runs.js';
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('limn')
|
|
12
|
+
.description('Agent-guided visual fidelity workbench')
|
|
13
|
+
.version('0.1.0')
|
|
14
|
+
.option('-w, --workspace <path>', 'Limner workspace root', process.cwd());
|
|
15
|
+
registerInitCommand(program);
|
|
16
|
+
registerPreviewCommand(program);
|
|
17
|
+
registerCaptureCommand(program);
|
|
18
|
+
registerCompareCommand(program);
|
|
19
|
+
registerReportCommand(program);
|
|
20
|
+
registerRunsCommand(program);
|
|
21
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
console.error(`limn: ${message}`);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,MAAM,CAAC;KACZ,WAAW,CAAC,wCAAwC,CAAC;KACrD,OAAO,CAAC,OAAO,CAAC;KAChB,MAAM,CAAC,wBAAwB,EAAE,uBAAuB,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;AAE5E,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAC7B,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAChC,qBAAqB,CAAC,OAAO,CAAC,CAAC;AAC/B,mBAAmB,CAAC,OAAO,CAAC,CAAC;AAE7B,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IACxD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACvE,OAAO,CAAC,KAAK,CAAC,SAAS,OAAO,EAAE,CAAC,CAAC;IAClC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
export declare function captureReference(options: {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
target: string;
|
|
5
|
+
headed?: boolean;
|
|
6
|
+
fullPage?: boolean;
|
|
7
|
+
modeDir?: string;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
screenshotPath: string;
|
|
10
|
+
metricsPath: string;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function registerCaptureCommand(program: Command): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { capturePage } from '../core/playwright-capture.js';
|
|
4
|
+
import { createRunLogger } from '../core/run-logger.js';
|
|
5
|
+
import { startStaticServer } from '../core/static-server.js';
|
|
6
|
+
import { loadRegionContract } from '../core/dom-metrics.js';
|
|
7
|
+
import { resolveTarget, resolveWorkspace } from '../core/workspace.js';
|
|
8
|
+
export async function captureReference(options) {
|
|
9
|
+
const workspace = resolveWorkspace(options.workspaceRoot);
|
|
10
|
+
const target = resolveTarget(options.workspaceRoot, options.target);
|
|
11
|
+
const logger = await createRunLogger(workspace, { command: 'capture', target: options.target, mode: 'reference' });
|
|
12
|
+
const captureDir = path.join(target.capturesDir, options.modeDir ?? 'reference');
|
|
13
|
+
const screenshotPath = path.join(captureDir, 'reference.png');
|
|
14
|
+
const metricsPath = path.join(captureDir, 'dom-metrics.json');
|
|
15
|
+
try {
|
|
16
|
+
await logger.event({ type: 'command.started' });
|
|
17
|
+
const contract = await loadRegionContract(target.regionsPath);
|
|
18
|
+
await mkdir(captureDir, { recursive: true });
|
|
19
|
+
const server = await startStaticServer(target.referenceDir);
|
|
20
|
+
try {
|
|
21
|
+
const result = await capturePage({
|
|
22
|
+
url: server.url,
|
|
23
|
+
outputPath: screenshotPath,
|
|
24
|
+
contract,
|
|
25
|
+
side: 'reference',
|
|
26
|
+
headed: options.headed,
|
|
27
|
+
fullPage: options.fullPage,
|
|
28
|
+
});
|
|
29
|
+
await writeFile(metricsPath, JSON.stringify({ reference: result.metrics }, null, 2) + '\n');
|
|
30
|
+
await logger.artifact(screenshotPath, 'screenshot');
|
|
31
|
+
await logger.artifact(metricsPath, 'dom-metrics');
|
|
32
|
+
await logger.complete('ok');
|
|
33
|
+
return { screenshotPath, metricsPath };
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await server.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
await logger.event({ type: 'command.failed', status: 'failed', message: error instanceof Error ? error.message : String(error) });
|
|
41
|
+
await logger.complete('failed');
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function registerCaptureCommand(program) {
|
|
46
|
+
const capture = program.command('capture').description('Capture Limner artifacts');
|
|
47
|
+
capture
|
|
48
|
+
.command('reference')
|
|
49
|
+
.description('Capture the target reference HTML')
|
|
50
|
+
.requiredOption('-t, --target <name>', 'target name')
|
|
51
|
+
.option('--headed', 'show the browser while capturing')
|
|
52
|
+
.option('--full-page', 'capture the full scrollable page')
|
|
53
|
+
.option('--viewport-only', 'capture only the viewport (default)')
|
|
54
|
+
.action(async (options, command) => {
|
|
55
|
+
const globals = command.optsWithGlobals();
|
|
56
|
+
const result = await captureReference({
|
|
57
|
+
workspaceRoot: globals.workspace,
|
|
58
|
+
target: options.target,
|
|
59
|
+
headed: options.headed,
|
|
60
|
+
fullPage: options.fullPage,
|
|
61
|
+
});
|
|
62
|
+
console.log(`Reference screenshot: ${result.screenshotPath}`);
|
|
63
|
+
console.log(`DOM metrics: ${result.metricsPath}`);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../../src/commands/capture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function compareImageApp(options: {
|
|
2
|
+
workspaceRoot: string;
|
|
3
|
+
target: string;
|
|
4
|
+
url: string;
|
|
5
|
+
headed?: boolean;
|
|
6
|
+
storageState?: string;
|
|
7
|
+
fullPage?: boolean;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
sideBySidePath: string;
|
|
10
|
+
reportPath: string;
|
|
11
|
+
appPath: string;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { capturePage } from '../core/playwright-capture.js';
|
|
4
|
+
import { writeImageAppReport } from '../core/report-writer.js';
|
|
5
|
+
import { createRunLogger } from '../core/run-logger.js';
|
|
6
|
+
import { createSideBySideImage } from '../core/side-by-side.js';
|
|
7
|
+
import { resolveTarget, resolveWorkspace } from '../core/workspace.js';
|
|
8
|
+
export async function compareImageApp(options) {
|
|
9
|
+
const workspace = resolveWorkspace(options.workspaceRoot);
|
|
10
|
+
const target = resolveTarget(options.workspaceRoot, options.target);
|
|
11
|
+
const logger = await createRunLogger(workspace, { command: 'compare', target: options.target, mode: 'image-app' });
|
|
12
|
+
const captureDir = path.join(target.capturesDir, 'image-app');
|
|
13
|
+
const appPath = path.join(captureDir, 'app.png');
|
|
14
|
+
const sideBySidePath = path.join(captureDir, 'side-by-side.png');
|
|
15
|
+
try {
|
|
16
|
+
await logger.event({ type: 'command.started' });
|
|
17
|
+
await mkdir(captureDir, { recursive: true });
|
|
18
|
+
await capturePage({
|
|
19
|
+
url: options.url,
|
|
20
|
+
outputPath: appPath,
|
|
21
|
+
headed: options.headed,
|
|
22
|
+
storageState: options.storageState,
|
|
23
|
+
fullPage: options.fullPage,
|
|
24
|
+
});
|
|
25
|
+
await createSideBySideImage({ leftPath: target.sourceImagePath, rightPath: appPath, outputPath: sideBySidePath });
|
|
26
|
+
const reportPath = await writeImageAppReport({
|
|
27
|
+
target,
|
|
28
|
+
idealPath: target.sourceImagePath,
|
|
29
|
+
appPath,
|
|
30
|
+
sideBySidePath,
|
|
31
|
+
appUrl: options.url,
|
|
32
|
+
});
|
|
33
|
+
await logger.artifact(appPath, 'screenshot');
|
|
34
|
+
await logger.artifact(sideBySidePath, 'side-by-side');
|
|
35
|
+
await logger.artifact(reportPath, 'report');
|
|
36
|
+
await logger.complete('ok');
|
|
37
|
+
return { sideBySidePath, reportPath, appPath };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
await logger.event({ type: 'command.failed', status: 'failed', message: error instanceof Error ? error.message : String(error) });
|
|
41
|
+
await logger.complete('failed');
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=compare-image-app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-image-app.js","sourceRoot":"","sources":["../../src/commands/compare-image-app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAOrC;IACC,MAAM,SAAS,GAAG,gBAAgB,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;IACnH,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IACjD,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;IAEjE,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;QAChD,MAAM,KAAK,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,MAAM,WAAW,CAAC;YAChB,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,UAAU,EAAE,OAAO;YACnB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC,CAAC;QACH,MAAM,qBAAqB,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,eAAe,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC,CAAC;QAClH,MAAM,UAAU,GAAG,MAAM,mBAAmB,CAAC;YAC3C,MAAM;YACN,SAAS,EAAE,MAAM,CAAC,eAAe;YACjC,OAAO;YACP,cAAc;YACd,MAAM,EAAE,OAAO,CAAC,GAAG;SACpB,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAC7C,MAAM,MAAM,CAAC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC,CAAC;QACtD,MAAM,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC5C,MAAM,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC5B,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClI,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAChC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export declare function compareImageReference(options: {
|
|
2
|
+
workspaceRoot: string;
|
|
3
|
+
target: string;
|
|
4
|
+
headed?: boolean;
|
|
5
|
+
fullPage?: boolean;
|
|
6
|
+
spec?: boolean;
|
|
7
|
+
specInstructions?: string;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
sideBySidePath: string;
|
|
10
|
+
reportPath: string;
|
|
11
|
+
specPaths?: {
|
|
12
|
+
idealSpecPath: string;
|
|
13
|
+
referenceSpecPath: string;
|
|
14
|
+
diffPath: string;
|
|
15
|
+
};
|
|
16
|
+
agentSpec?: {
|
|
17
|
+
promptPath: string;
|
|
18
|
+
codexPromptPath: string;
|
|
19
|
+
claudePromptPath: string;
|
|
20
|
+
factsPath: string;
|
|
21
|
+
examplePath: string;
|
|
22
|
+
schemaPath: string;
|
|
23
|
+
instructionsPath: string;
|
|
24
|
+
responsePath: string;
|
|
25
|
+
status: 'awaiting-agent' | 'validated' | 'invalid';
|
|
26
|
+
validationErrors?: string[];
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeImageReferenceReport } from '../core/report-writer.js';
|
|
4
|
+
import { createRunLogger } from '../core/run-logger.js';
|
|
5
|
+
import { createSideBySideImage } from '../core/side-by-side.js';
|
|
6
|
+
import { startStaticServer } from '../core/static-server.js';
|
|
7
|
+
import { writeImageReferenceSpecPack } from '../core/visual-spec-agent-pack.js';
|
|
8
|
+
import { resolveTarget, resolveWorkspace } from '../core/workspace.js';
|
|
9
|
+
import { capturePage } from '../core/playwright-capture.js';
|
|
10
|
+
export async function compareImageReference(options) {
|
|
11
|
+
const workspace = resolveWorkspace(options.workspaceRoot);
|
|
12
|
+
const target = resolveTarget(options.workspaceRoot, options.target);
|
|
13
|
+
const logger = await createRunLogger(workspace, { command: 'compare', target: options.target, mode: 'image-reference' });
|
|
14
|
+
const captureDir = path.join(target.capturesDir, 'image-reference');
|
|
15
|
+
const referencePath = path.join(captureDir, 'reference.png');
|
|
16
|
+
const sideBySidePath = path.join(captureDir, 'side-by-side.png');
|
|
17
|
+
let specPaths;
|
|
18
|
+
let agentSpec;
|
|
19
|
+
try {
|
|
20
|
+
await logger.event({ type: 'command.started' });
|
|
21
|
+
await mkdir(captureDir, { recursive: true });
|
|
22
|
+
const server = await startStaticServer(target.referenceDir);
|
|
23
|
+
try {
|
|
24
|
+
await capturePage({
|
|
25
|
+
url: server.url,
|
|
26
|
+
outputPath: referencePath,
|
|
27
|
+
headed: options.headed,
|
|
28
|
+
fullPage: options.fullPage,
|
|
29
|
+
});
|
|
30
|
+
await createSideBySideImage({ leftPath: target.sourceImagePath, rightPath: referencePath, outputPath: sideBySidePath });
|
|
31
|
+
if (options.spec) {
|
|
32
|
+
const specPack = await writeImageReferenceSpecPack({
|
|
33
|
+
target,
|
|
34
|
+
captureDir,
|
|
35
|
+
referenceUrl: server.url,
|
|
36
|
+
referencePath,
|
|
37
|
+
sideBySidePath,
|
|
38
|
+
headed: options.headed,
|
|
39
|
+
instructionsPath: options.specInstructions ? path.resolve(options.workspaceRoot, options.specInstructions) : undefined,
|
|
40
|
+
});
|
|
41
|
+
specPaths = specPack.specPaths;
|
|
42
|
+
agentSpec = specPack;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await server.close();
|
|
47
|
+
}
|
|
48
|
+
const reportPath = await writeImageReferenceReport({
|
|
49
|
+
target,
|
|
50
|
+
idealPath: target.sourceImagePath,
|
|
51
|
+
referencePath,
|
|
52
|
+
sideBySidePath,
|
|
53
|
+
specPaths,
|
|
54
|
+
specHighlights: agentSpec?.status === 'validated' ? agentSpec.diffHighlights : undefined,
|
|
55
|
+
agentSpec,
|
|
56
|
+
});
|
|
57
|
+
await logger.artifact(referencePath, 'screenshot');
|
|
58
|
+
await logger.artifact(sideBySidePath, 'side-by-side');
|
|
59
|
+
if (agentSpec) {
|
|
60
|
+
await logger.artifact(agentSpec.promptPath, 'agent-prompt');
|
|
61
|
+
await logger.artifact(agentSpec.codexPromptPath, 'agent-prompt');
|
|
62
|
+
await logger.artifact(agentSpec.claudePromptPath, 'agent-prompt');
|
|
63
|
+
await logger.artifact(agentSpec.factsPath, 'dom-facts');
|
|
64
|
+
await logger.artifact(agentSpec.examplePath, 'spec-example');
|
|
65
|
+
await logger.artifact(agentSpec.schemaPath, 'spec-schema');
|
|
66
|
+
}
|
|
67
|
+
if (specPaths) {
|
|
68
|
+
await logger.artifact(specPaths.idealSpecPath, 'visual-spec');
|
|
69
|
+
await logger.artifact(specPaths.referenceSpecPath, 'visual-spec');
|
|
70
|
+
await logger.artifact(specPaths.diffPath, 'visual-spec-diff');
|
|
71
|
+
}
|
|
72
|
+
await logger.artifact(reportPath, 'report');
|
|
73
|
+
await logger.complete('ok');
|
|
74
|
+
return { sideBySidePath, reportPath, specPaths, agentSpec };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
await logger.event({ type: 'command.failed', status: 'failed', message: error instanceof Error ? error.message : String(error) });
|
|
78
|
+
await logger.complete('failed');
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=compare-image-reference.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare-image-reference.js","sourceRoot":"","sources":["../../src/commands/compare-image-reference.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
export { compareImageReference } from './compare-image-reference.js';
|
|
3
|
+
export declare function compareReferenceApp(options: {
|
|
4
|
+
workspaceRoot: string;
|
|
5
|
+
target: string;
|
|
6
|
+
url: string;
|
|
7
|
+
headed?: boolean;
|
|
8
|
+
storageState?: string;
|
|
9
|
+
fullPage?: boolean;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
sideBySidePath: string;
|
|
12
|
+
reportPath: string;
|
|
13
|
+
metricsPath: string;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function registerCompareCommand(program: Command): void;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { loadRegionContract } from '../core/dom-metrics.js';
|
|
4
|
+
import { capturePage } from '../core/playwright-capture.js';
|
|
5
|
+
import { writeReferenceAppReport } from '../core/report-writer.js';
|
|
6
|
+
import { createRunLogger } from '../core/run-logger.js';
|
|
7
|
+
import { createSideBySideImage } from '../core/side-by-side.js';
|
|
8
|
+
import { startStaticServer } from '../core/static-server.js';
|
|
9
|
+
import { resolveTarget, resolveWorkspace } from '../core/workspace.js';
|
|
10
|
+
import { compareImageApp } from './compare-image-app.js';
|
|
11
|
+
import { compareImageReference } from './compare-image-reference.js';
|
|
12
|
+
export { compareImageReference } from './compare-image-reference.js';
|
|
13
|
+
export async function compareReferenceApp(options) {
|
|
14
|
+
const workspace = resolveWorkspace(options.workspaceRoot);
|
|
15
|
+
const target = resolveTarget(options.workspaceRoot, options.target);
|
|
16
|
+
const logger = await createRunLogger(workspace, { command: 'compare', target: options.target, mode: 'reference-app' });
|
|
17
|
+
const captureDir = path.join(target.capturesDir, 'reference-app');
|
|
18
|
+
const referencePath = path.join(captureDir, 'reference.png');
|
|
19
|
+
const appPath = path.join(captureDir, 'app.png');
|
|
20
|
+
const sideBySidePath = path.join(captureDir, 'side-by-side.png');
|
|
21
|
+
const metricsPath = path.join(captureDir, 'dom-metrics.json');
|
|
22
|
+
try {
|
|
23
|
+
await logger.event({ type: 'command.started' });
|
|
24
|
+
await mkdir(captureDir, { recursive: true });
|
|
25
|
+
const contract = await loadRegionContract(target.regionsPath);
|
|
26
|
+
const server = await startStaticServer(target.referenceDir);
|
|
27
|
+
let referenceResult;
|
|
28
|
+
try {
|
|
29
|
+
referenceResult = await capturePage({
|
|
30
|
+
url: server.url,
|
|
31
|
+
outputPath: referencePath,
|
|
32
|
+
contract,
|
|
33
|
+
side: 'reference',
|
|
34
|
+
headed: options.headed,
|
|
35
|
+
fullPage: options.fullPage,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await server.close();
|
|
40
|
+
}
|
|
41
|
+
const appResult = await capturePage({
|
|
42
|
+
url: options.url,
|
|
43
|
+
outputPath: appPath,
|
|
44
|
+
contract,
|
|
45
|
+
side: 'app',
|
|
46
|
+
headed: options.headed,
|
|
47
|
+
storageState: options.storageState,
|
|
48
|
+
fullPage: options.fullPage,
|
|
49
|
+
});
|
|
50
|
+
await writeFile(metricsPath, JSON.stringify({
|
|
51
|
+
reference: referenceResult.metrics,
|
|
52
|
+
app: appResult.metrics,
|
|
53
|
+
appConsole: appResult.consoleMessages,
|
|
54
|
+
referenceConsole: referenceResult.consoleMessages,
|
|
55
|
+
}, null, 2) + '\n');
|
|
56
|
+
await createSideBySideImage({ leftPath: referencePath, rightPath: appPath, outputPath: sideBySidePath });
|
|
57
|
+
const reportPath = await writeReferenceAppReport({
|
|
58
|
+
target,
|
|
59
|
+
referencePath,
|
|
60
|
+
appPath,
|
|
61
|
+
sideBySidePath,
|
|
62
|
+
referenceMetrics: referenceResult.metrics,
|
|
63
|
+
appMetrics: appResult.metrics,
|
|
64
|
+
appUrl: options.url,
|
|
65
|
+
});
|
|
66
|
+
await logger.artifact(referencePath, 'screenshot');
|
|
67
|
+
await logger.artifact(appPath, 'screenshot');
|
|
68
|
+
await logger.artifact(sideBySidePath, 'side-by-side');
|
|
69
|
+
await logger.artifact(metricsPath, 'dom-metrics');
|
|
70
|
+
await logger.artifact(reportPath, 'report');
|
|
71
|
+
await logger.complete('ok');
|
|
72
|
+
return { sideBySidePath, reportPath, metricsPath };
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
await logger.event({ type: 'command.failed', status: 'failed', message: error instanceof Error ? error.message : String(error) });
|
|
76
|
+
await logger.complete('failed');
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function registerCompareCommand(program) {
|
|
81
|
+
const compare = program.command('compare').description('Compare Limner artifacts');
|
|
82
|
+
compare
|
|
83
|
+
.command('image-reference')
|
|
84
|
+
.description('Compare source image to reference HTML screenshot')
|
|
85
|
+
.requiredOption('-t, --target <name>', 'target name')
|
|
86
|
+
.option('--headed', 'show browser while capturing')
|
|
87
|
+
.option('--full-page', 'capture the full scrollable page')
|
|
88
|
+
.option('--spec', 'prepare or validate an agent-authored visual spec pack')
|
|
89
|
+
.option('--spec-instructions <path>', 'custom visual spec instructions markdown file')
|
|
90
|
+
.option('--viewport-only', 'capture only the viewport (default)')
|
|
91
|
+
.action(async (options, command) => {
|
|
92
|
+
const globals = command.optsWithGlobals();
|
|
93
|
+
const result = await compareImageReference({
|
|
94
|
+
workspaceRoot: globals.workspace,
|
|
95
|
+
target: options.target,
|
|
96
|
+
headed: options.headed,
|
|
97
|
+
fullPage: options.fullPage,
|
|
98
|
+
spec: options.spec,
|
|
99
|
+
specInstructions: options.specInstructions,
|
|
100
|
+
});
|
|
101
|
+
console.log(`Side-by-side: ${result.sideBySidePath}`);
|
|
102
|
+
if (result.agentSpec) {
|
|
103
|
+
console.log(`Shared agent prompt: ${result.agentSpec.promptPath}`);
|
|
104
|
+
console.log(`Codex prompt: ${result.agentSpec.codexPromptPath}`);
|
|
105
|
+
console.log(`Claude prompt: ${result.agentSpec.claudePromptPath}`);
|
|
106
|
+
console.log(`Reference DOM facts: ${result.agentSpec.factsPath}`);
|
|
107
|
+
console.log(`Agent response schema: ${result.agentSpec.schemaPath}`);
|
|
108
|
+
console.log(`Custom instructions: ${result.agentSpec.instructionsPath}`);
|
|
109
|
+
console.log(`Agent response target: ${result.agentSpec.responsePath}`);
|
|
110
|
+
}
|
|
111
|
+
if (result.specPaths) {
|
|
112
|
+
console.log(`Ideal visual spec: ${result.specPaths.idealSpecPath}`);
|
|
113
|
+
console.log(`Reference visual spec: ${result.specPaths.referenceSpecPath}`);
|
|
114
|
+
console.log(`Visual spec diff: ${result.specPaths.diffPath}`);
|
|
115
|
+
}
|
|
116
|
+
else if (options.spec) {
|
|
117
|
+
console.log('Visual spec status: awaiting validated agent response');
|
|
118
|
+
}
|
|
119
|
+
console.log(`Report: ${result.reportPath}`);
|
|
120
|
+
});
|
|
121
|
+
compare
|
|
122
|
+
.command('reference-app')
|
|
123
|
+
.description('Compare reference HTML to an app URL')
|
|
124
|
+
.requiredOption('-t, --target <name>', 'target name')
|
|
125
|
+
.requiredOption('-u, --url <url>', 'app URL')
|
|
126
|
+
.option('--headed', 'show browser while capturing')
|
|
127
|
+
.option('--storage-state <path>', 'Playwright storageState JSON for authenticated app captures')
|
|
128
|
+
.option('--full-page', 'capture the full scrollable page')
|
|
129
|
+
.option('--viewport-only', 'capture only the viewport (default)')
|
|
130
|
+
.action(async (options, command) => {
|
|
131
|
+
const globals = command.optsWithGlobals();
|
|
132
|
+
const result = await compareReferenceApp({
|
|
133
|
+
workspaceRoot: globals.workspace,
|
|
134
|
+
target: options.target,
|
|
135
|
+
url: options.url,
|
|
136
|
+
headed: options.headed,
|
|
137
|
+
storageState: options.storageState,
|
|
138
|
+
fullPage: options.fullPage,
|
|
139
|
+
});
|
|
140
|
+
console.log(`Side-by-side: ${result.sideBySidePath}`);
|
|
141
|
+
console.log(`Metrics: ${result.metricsPath}`);
|
|
142
|
+
console.log(`Report: ${result.reportPath}`);
|
|
143
|
+
});
|
|
144
|
+
compare
|
|
145
|
+
.command('image-app')
|
|
146
|
+
.description('Compare source image directly to an app URL screenshot')
|
|
147
|
+
.requiredOption('-t, --target <name>', 'target name')
|
|
148
|
+
.requiredOption('-u, --url <url>', 'app URL')
|
|
149
|
+
.option('--headed', 'show browser while capturing')
|
|
150
|
+
.option('--storage-state <path>', 'Playwright storageState JSON for authenticated app captures')
|
|
151
|
+
.option('--full-page', 'capture the full scrollable page')
|
|
152
|
+
.option('--viewport-only', 'capture only the viewport (default)')
|
|
153
|
+
.action(async (options, command) => {
|
|
154
|
+
const globals = command.optsWithGlobals();
|
|
155
|
+
const result = await compareImageApp({
|
|
156
|
+
workspaceRoot: globals.workspace,
|
|
157
|
+
target: options.target,
|
|
158
|
+
url: options.url,
|
|
159
|
+
headed: options.headed,
|
|
160
|
+
storageState: options.storageState,
|
|
161
|
+
fullPage: options.fullPage,
|
|
162
|
+
});
|
|
163
|
+
console.log(`App screenshot: ${result.appPath}`);
|
|
164
|
+
console.log(`Side-by-side: ${result.sideBySidePath}`);
|
|
165
|
+
console.log(`Report: ${result.reportPath}`);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=compare.js.map
|