@michaelmagan/dnumb 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/AGENTS.md +180 -0
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/analysis/cluster.cjs +114 -0
- package/dist/analysis/cluster.d.cts +68 -0
- package/dist/analysis/cluster.d.ts +68 -0
- package/dist/analysis/cluster.js +109 -0
- package/dist/analysis/distances.cjs +124 -0
- package/dist/analysis/distances.d.cts +58 -0
- package/dist/analysis/distances.d.ts +58 -0
- package/dist/analysis/distances.js +119 -0
- package/dist/analysis/geometry.cjs +60 -0
- package/dist/analysis/geometry.d.cts +34 -0
- package/dist/analysis/geometry.d.ts +34 -0
- package/dist/analysis/geometry.js +53 -0
- package/dist/analysis/layout-tree.cjs +245 -0
- package/dist/analysis/layout-tree.d.cts +45 -0
- package/dist/analysis/layout-tree.d.ts +45 -0
- package/dist/analysis/layout-tree.js +242 -0
- package/dist/analysis/region-query.cjs +71 -0
- package/dist/analysis/region-query.d.cts +25 -0
- package/dist/analysis/region-query.d.ts +25 -0
- package/dist/analysis/region-query.js +67 -0
- package/dist/analysis/types.cjs +7 -0
- package/dist/analysis/types.d.cts +143 -0
- package/dist/analysis/types.d.ts +143 -0
- package/dist/analysis/types.js +6 -0
- package/dist/bundle.cjs +182 -0
- package/dist/bundle.d.cts +91 -0
- package/dist/bundle.d.ts +91 -0
- package/dist/bundle.js +175 -0
- package/dist/cli.cjs +755 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +719 -0
- package/dist/diff/changes.cjs +656 -0
- package/dist/diff/changes.d.cts +2 -0
- package/dist/diff/changes.d.ts +2 -0
- package/dist/diff/changes.js +653 -0
- package/dist/diff/identity.cjs +167 -0
- package/dist/diff/identity.d.cts +62 -0
- package/dist/diff/identity.d.ts +62 -0
- package/dist/diff/identity.js +159 -0
- package/dist/diff/index.cjs +84 -0
- package/dist/diff/index.d.cts +76 -0
- package/dist/diff/index.d.ts +76 -0
- package/dist/diff/index.js +68 -0
- package/dist/diff/match.cjs +156 -0
- package/dist/diff/match.d.cts +17 -0
- package/dist/diff/match.d.ts +17 -0
- package/dist/diff/match.js +152 -0
- package/dist/diff/render.cjs +540 -0
- package/dist/diff/render.d.cts +2 -0
- package/dist/diff/render.d.ts +2 -0
- package/dist/diff/render.js +537 -0
- package/dist/diff/types.cjs +2 -0
- package/dist/diff/types.d.cts +211 -0
- package/dist/diff/types.d.ts +211 -0
- package/dist/diff/types.js +1 -0
- package/dist/digest/index.cjs +8 -0
- package/dist/digest/index.d.cts +4 -0
- package/dist/digest/index.d.ts +4 -0
- package/dist/digest/index.js +2 -0
- package/dist/digest/parse.cjs +163 -0
- package/dist/digest/parse.d.cts +80 -0
- package/dist/digest/parse.d.ts +80 -0
- package/dist/digest/parse.js +160 -0
- package/dist/digest/render.cjs +1575 -0
- package/dist/digest/render.d.cts +41 -0
- package/dist/digest/render.d.ts +41 -0
- package/dist/digest/render.js +1571 -0
- package/dist/digest/types.cjs +10 -0
- package/dist/digest/types.d.cts +86 -0
- package/dist/digest/types.d.ts +86 -0
- package/dist/digest/types.js +9 -0
- package/dist/index.cjs +61 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +19 -0
- package/dist/page.cjs +35 -0
- package/dist/page.d.cts +40 -0
- package/dist/page.d.ts +40 -0
- package/dist/page.js +31 -0
- package/dist/playwright.cjs +138 -0
- package/dist/playwright.d.cts +69 -0
- package/dist/playwright.d.ts +69 -0
- package/dist/playwright.js +131 -0
- package/dist/query.cjs +1152 -0
- package/dist/query.d.cts +47 -0
- package/dist/query.d.ts +47 -0
- package/dist/query.js +1146 -0
- package/dist/snapshot/capture.cjs +80 -0
- package/dist/snapshot/capture.d.cts +34 -0
- package/dist/snapshot/capture.d.ts +34 -0
- package/dist/snapshot/capture.js +77 -0
- package/dist/snapshot/cdp.cjs +450 -0
- package/dist/snapshot/cdp.d.cts +36 -0
- package/dist/snapshot/cdp.d.ts +36 -0
- package/dist/snapshot/cdp.js +447 -0
- package/dist/snapshot/clip.cjs +100 -0
- package/dist/snapshot/clip.d.cts +26 -0
- package/dist/snapshot/clip.d.ts +26 -0
- package/dist/snapshot/clip.js +96 -0
- package/dist/snapshot/collect.cjs +358 -0
- package/dist/snapshot/collect.d.cts +18 -0
- package/dist/snapshot/collect.d.ts +18 -0
- package/dist/snapshot/collect.js +355 -0
- package/dist/snapshot/color.cjs +1039 -0
- package/dist/snapshot/color.d.cts +316 -0
- package/dist/snapshot/color.d.ts +316 -0
- package/dist/snapshot/color.js +1026 -0
- package/dist/snapshot/facts.cjs +167 -0
- package/dist/snapshot/facts.d.cts +48 -0
- package/dist/snapshot/facts.d.ts +48 -0
- package/dist/snapshot/facts.js +161 -0
- package/dist/snapshot/limits.cjs +69 -0
- package/dist/snapshot/limits.d.cts +59 -0
- package/dist/snapshot/limits.d.ts +59 -0
- package/dist/snapshot/limits.js +65 -0
- package/dist/snapshot/not-measured.cjs +44 -0
- package/dist/snapshot/not-measured.d.cts +14 -0
- package/dist/snapshot/not-measured.d.ts +14 -0
- package/dist/snapshot/not-measured.js +41 -0
- package/dist/snapshot/types.cjs +2 -0
- package/dist/snapshot/types.d.cts +238 -0
- package/dist/snapshot/types.d.ts +238 -0
- package/dist/snapshot/types.js +1 -0
- package/dist/spec/index.cjs +9 -0
- package/dist/spec/index.d.cts +15 -0
- package/dist/spec/index.d.ts +15 -0
- package/dist/spec/index.js +6 -0
- package/package.json +141 -0
- package/skills/dnumb/SKILL.md +136 -0
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.main = main;
|
|
38
|
+
/**
|
|
39
|
+
* `dnumb` — the bin entrypoint.
|
|
40
|
+
*
|
|
41
|
+
* Arg parsing is `node:util`'s `parseArgs`, not a dependency and not
|
|
42
|
+
* hand-rolled. The prototype hand-rolled it; `parseArgs` has been stable since
|
|
43
|
+
* Node 18 and this package's floor is 22.12.
|
|
44
|
+
*
|
|
45
|
+
* This file does three things and delegates everything else: turn argv into
|
|
46
|
+
* options, get a `Bundle` off disk, and print one of the two renderings
|
|
47
|
+
* `src/query.ts` already produced. It holds no formatting of its own, because a
|
|
48
|
+
* second place that decides what a measurement looks like is a second place for
|
|
49
|
+
* `--json` and the text to disagree. `capture` is held to the same rule: it
|
|
50
|
+
* launches a browser and navigates, but the measuring and the serializing are
|
|
51
|
+
* `dnumb/playwright`'s `capture()` and `serializeBundle`, called here rather
|
|
52
|
+
* than reimplemented — a second place that decides what a bundle looks like is
|
|
53
|
+
* the same mistake as a second place that decides what a digest looks like.
|
|
54
|
+
*
|
|
55
|
+
* `playwright-core` is loaded with a dynamic `import()` inside the `capture`
|
|
56
|
+
* verb only, never at module scope. It is an optional peer dependency, and a
|
|
57
|
+
* static top-level import would make every other verb — `look`, `q`, `diff`,
|
|
58
|
+
* none of which touch a browser — fail to load for a consumer who never
|
|
59
|
+
* installed it. Verified empirically, not assumed: a module with a static
|
|
60
|
+
* `import ... from 'playwright-core'` throws `ERR_MODULE_NOT_FOUND` on load
|
|
61
|
+
* before any of its exports run, while a module that only imports it inside
|
|
62
|
+
* one function loads fine and fails only when that function is called.
|
|
63
|
+
*
|
|
64
|
+
* Exit codes are the contract a CI script reads: 0 answered, 1 the command was
|
|
65
|
+
* wrong, 2 the bundle was unreadable, 3 a live capture could not be produced
|
|
66
|
+
* (no browser installed, DNS failure, connection refused, navigation timeout),
|
|
67
|
+
* 4 a capture succeeded and then could not be saved (permissions, a full
|
|
68
|
+
* disk, two capture names colliding on one path — `writeBundle` in
|
|
69
|
+
* `src/playwright.ts`). 3 and 4 exist as two codes, not one, because they are
|
|
70
|
+
* opposite failures a CI script needs to tell apart: 3 means nothing was
|
|
71
|
+
* measured, re-run the capture; 4 means the measurement worked and only the
|
|
72
|
+
* write did not, so re-running the same capture is exactly the right fix, not
|
|
73
|
+
* a wasted round trip. Before 4 existed, a write failure after a successful
|
|
74
|
+
* capture had no wrapped path at all — it escaped as an unhandled promise
|
|
75
|
+
* rejection with a raw stack trace and exit 1, misreporting an I/O problem as
|
|
76
|
+
* a bad command after the measurement had already been taken and thrown away.
|
|
77
|
+
* There is no "found problems" code, and there will not be one — dnumb does
|
|
78
|
+
* not adjudicate.
|
|
79
|
+
*/
|
|
80
|
+
const node_fs_1 = require("node:fs");
|
|
81
|
+
const node_path_1 = require("node:path");
|
|
82
|
+
const node_url_1 = require("node:url");
|
|
83
|
+
const node_util_1 = require("node:util");
|
|
84
|
+
const bundle_js_1 = require("./bundle.cjs");
|
|
85
|
+
const index_js_1 = require("./diff/index.cjs");
|
|
86
|
+
const index_js_2 = require("./digest/index.cjs");
|
|
87
|
+
const playwright_js_1 = require("./playwright.cjs");
|
|
88
|
+
const query_js_1 = require("./query.cjs");
|
|
89
|
+
const limits_js_1 = require("./snapshot/limits.cjs");
|
|
90
|
+
const not_measured_js_1 = require("./snapshot/not-measured.cjs");
|
|
91
|
+
const index_js_3 = require("./spec/index.cjs");
|
|
92
|
+
/**
|
|
93
|
+
* `../package.json` from this file's own location, which is the package root
|
|
94
|
+
* either way this file runs: `src/cli.ts` in dev/test, `dist/cli.cjs` after
|
|
95
|
+
* build. Read lazily, only when `--version` is actually asked for, rather
|
|
96
|
+
* than on every invocation.
|
|
97
|
+
*/
|
|
98
|
+
const PACKAGE_JSON_PATH = (0, node_url_1.fileURLToPath)(new URL('../package.json', require("url").pathToFileURL(__filename)));
|
|
99
|
+
/** `--help` promises "the package and spec versions"; this is the first half. */
|
|
100
|
+
function packageVersion() {
|
|
101
|
+
try {
|
|
102
|
+
const pkg = JSON.parse((0, node_fs_1.readFileSync)(PACKAGE_JSON_PATH, 'utf8'));
|
|
103
|
+
const version = pkg.version;
|
|
104
|
+
return typeof version === 'string' && version !== '' ? version : 'unknown';
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// A missing or unreadable package.json should not take down `--version`
|
|
108
|
+
// itself -- the spec version, at least, is still answerable.
|
|
109
|
+
return 'unknown';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const OK = 0;
|
|
113
|
+
/** The command was wrong: unknown verb, missing argument, unknown element id. */
|
|
114
|
+
const USAGE_ERROR = 1;
|
|
115
|
+
/** The bundle was wrong: not there, not readable, not a dnumb bundle. */
|
|
116
|
+
const BUNDLE_ERROR = 2;
|
|
117
|
+
/**
|
|
118
|
+
* `capture` could not produce a bundle: no `playwright-core` installed, no
|
|
119
|
+
* browser binary downloaded, DNS failure, connection refused, or a navigation
|
|
120
|
+
* timeout. None of these are a bad command (1) or a bad bundle (2) — the
|
|
121
|
+
* command was right and there was no bundle to be wrong about yet.
|
|
122
|
+
*/
|
|
123
|
+
const CAPTURE_ERROR = 3;
|
|
124
|
+
/**
|
|
125
|
+
* `capture` measured the page and then could not save it: a filesystem error
|
|
126
|
+
* writing the bundle (permissions, a full disk) or `writeBundle`'s own "two
|
|
127
|
+
* capture names write to one path" collision (`src/playwright.ts`). Distinct
|
|
128
|
+
* from `CAPTURE_ERROR` on purpose — see the file header for why.
|
|
129
|
+
*/
|
|
130
|
+
const WRITE_ERROR = 4;
|
|
131
|
+
const USAGE = `dnumb — design, in numbers.
|
|
132
|
+
|
|
133
|
+
Turns a rendered page into measurements an agent can read. It reports facts and
|
|
134
|
+
does not decide whether anything is wrong.
|
|
135
|
+
|
|
136
|
+
USAGE
|
|
137
|
+
dnumb capture <url> [--name <name>] [--dir <dir>] [--viewport <WxH>] [--wait <state>]
|
|
138
|
+
[--strategy <auto|walk|cdp>]
|
|
139
|
+
dnumb --from <bundle> look [--focus <id|tag>] [--edges] [--budget-chars <n>] [--budget <n>]
|
|
140
|
+
dnumb --from <bundle> q <verb> [args]
|
|
141
|
+
dnumb --from <before> diff <after>
|
|
142
|
+
|
|
143
|
+
VERBS
|
|
144
|
+
capture <url> Launch a browser, navigate, and write a bundle.
|
|
145
|
+
look Render the digest: every element, geometry, facts.
|
|
146
|
+
diff <bundle> What changed between two captures, and why.
|
|
147
|
+
q describe <id> Everything the capture knows about one element.
|
|
148
|
+
q find <text|tag|role> Elements matching, as digest lines.
|
|
149
|
+
q at <x> <y> What is at a point. Innermost first.
|
|
150
|
+
q within <x> <y> <w> <h> What overlaps a region. Largest overlap first.
|
|
151
|
+
q distance <idA> <idB> Edge-to-edge and centre-to-centre distance.
|
|
152
|
+
q nearest <id> The closest layout nodes by edge distance.
|
|
153
|
+
q facts Every element carrying a fact, one per line.
|
|
154
|
+
q color fg/bg pairs, ratio ascending, unmeasured last.
|
|
155
|
+
q color <id> One element's full composite chain.
|
|
156
|
+
q color --all Every gated element's color line.
|
|
157
|
+
|
|
158
|
+
Capture reports whatever is on screen the instant it runs — a skeleton
|
|
159
|
+
screen, a mid-animation frame, a half-fetched list — with no way to tell
|
|
160
|
+
settled from loading, so choose --wait deliberately and expect to re-run it.
|
|
161
|
+
|
|
162
|
+
OPTIONS
|
|
163
|
+
--from <bundle> The capture bundle to read. Required by every verb but capture.
|
|
164
|
+
--json Machine-readable output. Every verb supports it.
|
|
165
|
+
--limit <n> Rows the list verbs print (default 50; 0 for all).
|
|
166
|
+
--k <n> Neighbours \`q nearest\` returns (default 4).
|
|
167
|
+
--focus <id> \`look\` only: scope the digest to one subtree.
|
|
168
|
+
--edges \`look\` only: add the edge-cluster histogram header.
|
|
169
|
+
--budget-chars <n> \`look\` only: soft target for total digest body characters
|
|
170
|
+
(default 32768). The standing budget meter -- always active.
|
|
171
|
+
--budget <n> \`look\` only: soft target for digest body lines. Additional to
|
|
172
|
+
--budget-chars, never a replacement for it -- passing --budget
|
|
173
|
+
does not turn the character budget off.
|
|
174
|
+
--all \`q color\` only: per-element lines instead of the pair table.
|
|
175
|
+
--name <name> \`capture\` only: name for the bundle (default "capture").
|
|
176
|
+
--dir <dir> \`capture\` only: directory the bundle is written to (default .dnumb).
|
|
177
|
+
--viewport <WxH> \`capture\` only: e.g. 375x812 (default: the browser's own default).
|
|
178
|
+
--wait <state> \`capture\` only: load|domcontentloaded|networkidle|commit before
|
|
179
|
+
capturing (default load). See the hazard above before trusting this.
|
|
180
|
+
--timeout <ms> \`capture\` only: navigation timeout (default: Playwright's own, 30s).
|
|
181
|
+
--strategy <s> \`capture\` only: auto|walk|cdp (default auto). \`docs/digest.md\` walks
|
|
182
|
+
through comparing a cdp capture against a walk capture of the same
|
|
183
|
+
page for both the color and the shadow-DOM asymmetry between the two
|
|
184
|
+
paths -- this is what lets that comparison happen from the CLI alone.
|
|
185
|
+
\`cdp\` on a non-Chromium/non-CDP page is a usage error, not a fallback.
|
|
186
|
+
-h, --help Print this and exit.
|
|
187
|
+
-v, --version Print the package and spec versions and exit.
|
|
188
|
+
|
|
189
|
+
Put \`--\` before a negative coordinate: dnumb --from b.json q at -- -8 120
|
|
190
|
+
|
|
191
|
+
Element ids like e13 are stable within one capture only. They are not DOM ids.
|
|
192
|
+
https://github.com/michaelmagan/dnumb#readme
|
|
193
|
+
`;
|
|
194
|
+
const OPTIONS = {
|
|
195
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
196
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
197
|
+
json: { type: 'boolean', default: false },
|
|
198
|
+
from: { type: 'string' },
|
|
199
|
+
focus: { type: 'string' },
|
|
200
|
+
edges: { type: 'boolean', default: false },
|
|
201
|
+
all: { type: 'boolean', default: false },
|
|
202
|
+
budget: { type: 'string' },
|
|
203
|
+
'budget-chars': { type: 'string' },
|
|
204
|
+
k: { type: 'string' },
|
|
205
|
+
limit: { type: 'string' },
|
|
206
|
+
name: { type: 'string' },
|
|
207
|
+
dir: { type: 'string' },
|
|
208
|
+
viewport: { type: 'string' },
|
|
209
|
+
wait: { type: 'string' },
|
|
210
|
+
timeout: { type: 'string' },
|
|
211
|
+
strategy: { type: 'string' },
|
|
212
|
+
};
|
|
213
|
+
const out = (text) => {
|
|
214
|
+
process.stdout.write(text);
|
|
215
|
+
};
|
|
216
|
+
function fail(code, message) {
|
|
217
|
+
process.stderr.write(`${message}\n`);
|
|
218
|
+
return code;
|
|
219
|
+
}
|
|
220
|
+
/** A flag value that must be a whole number, with the flag named in the complaint. */
|
|
221
|
+
function integer(raw, flag, minimum) {
|
|
222
|
+
if (raw === undefined)
|
|
223
|
+
return undefined;
|
|
224
|
+
const value = Number.parseInt(raw, 10);
|
|
225
|
+
if (!Number.isFinite(value) || String(value) !== raw.trim() || value < minimum) {
|
|
226
|
+
throw new query_js_1.QueryError(`--${flag} takes a whole number ≥ ${minimum}; got "${raw}".`);
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
/** The states Playwright's `page.goto` can wait on before we take the snapshot. */
|
|
231
|
+
const WAIT_STATES = ['load', 'domcontentloaded', 'networkidle', 'commit'];
|
|
232
|
+
/** An error's first line, so a long CDP call-log doesn't drown the one sentence that matters. */
|
|
233
|
+
function firstLine(error) {
|
|
234
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
235
|
+
return (message.split('\n')[0] ?? message).trim();
|
|
236
|
+
}
|
|
237
|
+
/** `writeFileSync`/`mkdirSync` failure codes `writeBundle` (`src/playwright.ts`) can surface. */
|
|
238
|
+
const FS_ERROR_CODES = new Set([
|
|
239
|
+
'ENOENT',
|
|
240
|
+
'EACCES',
|
|
241
|
+
'EPERM',
|
|
242
|
+
'ENOSPC',
|
|
243
|
+
'EROFS',
|
|
244
|
+
'EEXIST',
|
|
245
|
+
'EISDIR',
|
|
246
|
+
'ENOTDIR',
|
|
247
|
+
'EMFILE',
|
|
248
|
+
'ENFILE',
|
|
249
|
+
]);
|
|
250
|
+
/**
|
|
251
|
+
* True for the two failures `writeBundle` can throw: a filesystem error
|
|
252
|
+
* writing the file, or its own "two capture names write to one path"
|
|
253
|
+
* collision. Both happen strictly *after* `captureSnapshot` already
|
|
254
|
+
* succeeded — the measurement was taken — which is what earns them
|
|
255
|
+
* `WRITE_ERROR` instead of `CAPTURE_ERROR`: the latter means nothing was
|
|
256
|
+
* measured, and by the time either of these fires, something was. Anything
|
|
257
|
+
* else `captureToDisk` throws (a page crash mid-measurement, a lost CDP
|
|
258
|
+
* connection) still means `CAPTURE_ERROR` and falls through to it below.
|
|
259
|
+
*/
|
|
260
|
+
function isWriteFailure(error) {
|
|
261
|
+
if (error instanceof Error && /^dnumb: capture .+ both write to /.test(error.message)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
const code = error?.code;
|
|
265
|
+
return typeof code === 'string' && FS_ERROR_CODES.has(code);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* `capture <url>` needs an absolute URL, not a bare host, because there is no
|
|
269
|
+
* `baseURL` for a relative one to resolve against. Caught here, before a
|
|
270
|
+
* browser is launched, it is a usage error (1); caught by the navigation
|
|
271
|
+
* itself it would be a capture error (3) with no clean way to tell the two
|
|
272
|
+
* apart from the caller's side.
|
|
273
|
+
*/
|
|
274
|
+
function parseCaptureUrl(raw) {
|
|
275
|
+
if (raw === undefined || raw === '') {
|
|
276
|
+
throw new query_js_1.QueryError('capture needs a URL: dnumb capture <url> [options]');
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
new URL(raw);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
throw new query_js_1.QueryError(`"${raw}" is not a URL capture can load — it needs a scheme. Did you mean https://${raw}?`);
|
|
283
|
+
}
|
|
284
|
+
return raw;
|
|
285
|
+
}
|
|
286
|
+
/** `--viewport 375x812` → `{ width: 375, height: 812 }`, or a usage error naming what was typed. */
|
|
287
|
+
function parseViewport(raw) {
|
|
288
|
+
if (raw === undefined)
|
|
289
|
+
return undefined;
|
|
290
|
+
const match = /^(\d+)x(\d+)$/.exec(raw.trim());
|
|
291
|
+
if (!match?.[1] || !match[2]) {
|
|
292
|
+
throw new query_js_1.QueryError(`--viewport takes WIDTHxHEIGHT, e.g. 375x812; got "${raw}".`);
|
|
293
|
+
}
|
|
294
|
+
const width = Number.parseInt(match[1], 10);
|
|
295
|
+
const height = Number.parseInt(match[2], 10);
|
|
296
|
+
if (width < 1 || height < 1) {
|
|
297
|
+
throw new query_js_1.QueryError(`--viewport dimensions must be ≥ 1px; got "${raw}".`);
|
|
298
|
+
}
|
|
299
|
+
return { width, height };
|
|
300
|
+
}
|
|
301
|
+
function parseWait(raw) {
|
|
302
|
+
if (raw === undefined)
|
|
303
|
+
return 'load';
|
|
304
|
+
if (!WAIT_STATES.includes(raw)) {
|
|
305
|
+
throw new query_js_1.QueryError(`--wait takes one of ${WAIT_STATES.join(', ')}; got "${raw}".`);
|
|
306
|
+
}
|
|
307
|
+
return raw;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* P2-2: `dnumb/playwright`'s `capture()` has always taken a `strategy`
|
|
311
|
+
* (`auto | walk | cdp`, `snapshot/capture.ts`), but the CLI never exposed it
|
|
312
|
+
* -- `doCapture` called `captureToDisk` with no strategy at all, always
|
|
313
|
+
* `auto`. `docs/digest.md` walks a reader through comparing a `cdp` capture
|
|
314
|
+
* against a `walk` capture of the same page twice over -- once for the color
|
|
315
|
+
* overlay-detection asymmetry (`centre` sample vs `whole-rect`), once for
|
|
316
|
+
* shadow DOM (`cdp` can flatten shadow content in, unreliably; `walk` cannot
|
|
317
|
+
* see into a shadow root at all) -- and a CLI-only user, with no Playwright
|
|
318
|
+
* suite to write `{ strategy: 'walk' }` into, had no way to run that
|
|
319
|
+
* comparison. `'auto'` is not in `CaptureOptions['strategy']`'s type as an
|
|
320
|
+
* omittable default by accident here either: passing it explicitly is
|
|
321
|
+
* identical to passing nothing, so this validates it without threading a
|
|
322
|
+
* fourth branch through `doCapture` for no behavioural difference.
|
|
323
|
+
*/
|
|
324
|
+
const CAPTURE_STRATEGIES = ['auto', 'walk', 'cdp'];
|
|
325
|
+
function parseStrategy(raw) {
|
|
326
|
+
if (raw === undefined)
|
|
327
|
+
return undefined;
|
|
328
|
+
if (!CAPTURE_STRATEGIES.includes(raw)) {
|
|
329
|
+
throw new query_js_1.QueryError(`--strategy takes one of ${CAPTURE_STRATEGIES.join(', ')}; got "${raw}".`);
|
|
330
|
+
}
|
|
331
|
+
return raw;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Launch a browser, navigate, and hand the page to `dnumb/playwright`'s own
|
|
335
|
+
* `capture()` — the same function a Playwright suite calls. Everything below
|
|
336
|
+
* this line that is not "launch" or "navigate" is that function's job, not
|
|
337
|
+
* this one's, so a second bundle-writer never drifts from the first.
|
|
338
|
+
*
|
|
339
|
+
* `playwright-core` is imported here, inside the verb, and nowhere else in
|
|
340
|
+
* this file — see the file header for why a top-level import would be wrong.
|
|
341
|
+
*/
|
|
342
|
+
async function doCapture(url, opts, json) {
|
|
343
|
+
let playwrightCore;
|
|
344
|
+
try {
|
|
345
|
+
playwrightCore = await Promise.resolve().then(() => __importStar(require('playwright-core')));
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return fail(CAPTURE_ERROR, 'dnumb: capture needs playwright-core, an optional peer dependency that is not installed. ' +
|
|
349
|
+
"Run `npm install playwright-core` (or your package manager's equivalent), then " +
|
|
350
|
+
'`npx playwright install chromium` to download a browser.');
|
|
351
|
+
}
|
|
352
|
+
let browser;
|
|
353
|
+
try {
|
|
354
|
+
browser = await playwrightCore.chromium.launch();
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
return fail(CAPTURE_ERROR, `dnumb: could not launch a browser: ${firstLine(error)}\n` +
|
|
358
|
+
'Run `npx playwright install chromium` to download one.');
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const page = await browser.newPage(opts.viewport ? { viewport: opts.viewport } : {});
|
|
362
|
+
try {
|
|
363
|
+
await page.goto(url, {
|
|
364
|
+
waitUntil: opts.waitUntil,
|
|
365
|
+
...(opts.timeoutMs !== undefined ? { timeout: opts.timeoutMs } : {}),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
return fail(CAPTURE_ERROR, `dnumb: could not load ${url}: ${firstLine(error)}`);
|
|
370
|
+
}
|
|
371
|
+
let outcome;
|
|
372
|
+
try {
|
|
373
|
+
outcome = await (0, playwright_js_1.capture)(page, opts.name, {
|
|
374
|
+
outputDir: opts.outputDir,
|
|
375
|
+
...(opts.strategy !== undefined ? { strategy: opts.strategy } : {}),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
// Whatever comes next, the browser measured the page: `page.goto`
|
|
380
|
+
// already succeeded above. If this is a save failure, name it as one
|
|
381
|
+
// and report the path capture was trying to write, computed the same
|
|
382
|
+
// way `writeBundle` computes it, so nothing about a completed capture
|
|
383
|
+
// is silently lost — only unresolved. Anything else here is treated as
|
|
384
|
+
// a genuine capture failure (`CAPTURE_ERROR`) rather than left to
|
|
385
|
+
// escape as an unhandled rejection, which is what used to turn every
|
|
386
|
+
// one of these into a misreported exit 1.
|
|
387
|
+
if (isWriteFailure(error)) {
|
|
388
|
+
let intended = null;
|
|
389
|
+
try {
|
|
390
|
+
intended = (0, node_path_1.resolve)((0, playwright_js_1.bundlePath)(opts.outputDir, opts.name));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// `safeName` already validated `opts.name` in `runCapture`; this
|
|
394
|
+
// is unreachable in practice, and `intended` staying null just
|
|
395
|
+
// drops the path from the message rather than throwing again.
|
|
396
|
+
}
|
|
397
|
+
return fail(WRITE_ERROR, `dnumb: captured ${url} but could not save the bundle` +
|
|
398
|
+
(intended ? ` to ${intended}` : '') +
|
|
399
|
+
`: ${firstLine(error)}\n` +
|
|
400
|
+
'The measurement was taken; only the write failed. Fix the write problem ' +
|
|
401
|
+
'(permissions, disk space, or rename one of two colliding capture names) and run ' +
|
|
402
|
+
'this same capture again — nothing about the page needs to change.');
|
|
403
|
+
}
|
|
404
|
+
return fail(CAPTURE_ERROR, `dnumb: captured ${url} but could not finish producing a bundle: ${firstLine(error)}`);
|
|
405
|
+
}
|
|
406
|
+
const { bundle, path } = outcome;
|
|
407
|
+
// `capture` is the first thing a new user runs, so it is also the first
|
|
408
|
+
// moment the tool discloses its own limits -- the moment where a caller
|
|
409
|
+
// could still re-run with a different `--strategy` or a narrower page.
|
|
410
|
+
// Both `truncated` and `notMeasured` were already written into the
|
|
411
|
+
// bundle either way; only the write's own report of them was missing.
|
|
412
|
+
const notMeasuredLine = (0, not_measured_js_1.formatNotMeasured)(bundle.notMeasured);
|
|
413
|
+
const textLines = [`dnumb: wrote ${bundle.elements.length} elements to ${path}`];
|
|
414
|
+
if (bundle.truncated) {
|
|
415
|
+
// Named directly from the constant this same process just capped the
|
|
416
|
+
// walk at, not inferred from `bundle.elements.length` -- the count and
|
|
417
|
+
// the cap coincide for a bundle this process just wrote, but naming
|
|
418
|
+
// the cap directly says what is actually true regardless of that, and
|
|
419
|
+
// matches what a caller needs to know: this is the ceiling to raise.
|
|
420
|
+
textLines.push(`# TRUNCATED: this capture stopped at the ${limits_js_1.MAX_ELEMENTS}-element cap; the page has more elements than were captured`);
|
|
421
|
+
}
|
|
422
|
+
if (notMeasuredLine)
|
|
423
|
+
textLines.push(`# ${notMeasuredLine}`);
|
|
424
|
+
out(json
|
|
425
|
+
? `${JSON.stringify({
|
|
426
|
+
verb: 'capture',
|
|
427
|
+
name: bundle.name,
|
|
428
|
+
url: bundle.url,
|
|
429
|
+
path,
|
|
430
|
+
viewport: bundle.viewport,
|
|
431
|
+
// P2-3: `look --json` has always called this `records` (`src/
|
|
432
|
+
// query.ts`'s `look()`), matching what the digest header itself
|
|
433
|
+
// calls it (`# page: WxH (<N> records → <M> layout nodes ...)`,
|
|
434
|
+
// `docs/digest.md`). `capture --json` called the identical
|
|
435
|
+
// count `elements` -- same value, different name, across the
|
|
436
|
+
// two verbs a caller is most likely to pipe straight into each
|
|
437
|
+
// other. `records` is the name that already has two other
|
|
438
|
+
// callers agreeing with it.
|
|
439
|
+
records: bundle.elements.length,
|
|
440
|
+
truncated: bundle.truncated,
|
|
441
|
+
notMeasured: bundle.notMeasured,
|
|
442
|
+
}, null, 2)}\n`
|
|
443
|
+
: `${textLines.join('\n')}\n`);
|
|
444
|
+
return OK;
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
await browser.close();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* The synchronous half of `capture`: parse and validate argv. This never
|
|
452
|
+
* touches a browser, so a bad `--viewport` or a missing URL fails instantly
|
|
453
|
+
* and exits 1 without the dynamic import of `playwright-core` ever running —
|
|
454
|
+
* which matters for a caller who does not have it installed and is only here
|
|
455
|
+
* because they typo'd a flag.
|
|
456
|
+
*/
|
|
457
|
+
function runCapture(positionals, values, json) {
|
|
458
|
+
let url;
|
|
459
|
+
let name;
|
|
460
|
+
let outputDir;
|
|
461
|
+
let viewport;
|
|
462
|
+
let waitUntil;
|
|
463
|
+
let timeoutMs;
|
|
464
|
+
let strategy;
|
|
465
|
+
try {
|
|
466
|
+
url = parseCaptureUrl(positionals[0]);
|
|
467
|
+
name = typeof values.name === 'string' && values.name !== '' ? values.name : 'capture';
|
|
468
|
+
(0, playwright_js_1.safeName)(name); // Throws a "dnumb: capture name ..." Error if unusable as a filename.
|
|
469
|
+
outputDir =
|
|
470
|
+
typeof values.dir === 'string' && values.dir !== '' ? values.dir : playwright_js_1.DEFAULT_OUTPUT_DIR;
|
|
471
|
+
viewport = parseViewport(typeof values.viewport === 'string' ? values.viewport : undefined);
|
|
472
|
+
waitUntil = parseWait(typeof values.wait === 'string' ? values.wait : undefined);
|
|
473
|
+
timeoutMs = integer(typeof values.timeout === 'string' ? values.timeout : undefined, 'timeout', 1);
|
|
474
|
+
strategy = parseStrategy(typeof values.strategy === 'string' ? values.strategy : undefined);
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
if (error instanceof query_js_1.QueryError)
|
|
478
|
+
return fail(USAGE_ERROR, `dnumb: ${error.message}`);
|
|
479
|
+
if (error instanceof Error) {
|
|
480
|
+
return fail(USAGE_ERROR, error.message.startsWith('dnumb:') ? error.message : `dnumb: ${error.message}`);
|
|
481
|
+
}
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
return doCapture(url, {
|
|
485
|
+
name,
|
|
486
|
+
outputDir,
|
|
487
|
+
...(viewport ? { viewport } : {}),
|
|
488
|
+
waitUntil,
|
|
489
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
490
|
+
...(strategy !== undefined ? { strategy } : {}),
|
|
491
|
+
}, json);
|
|
492
|
+
}
|
|
493
|
+
function optionsFrom(values) {
|
|
494
|
+
const budgetLines = integer(values.budget, 'budget', 1);
|
|
495
|
+
// No default threaded through here: an absent `--budget-chars` leaves
|
|
496
|
+
// `budgetChars` undefined all the way to `renderDigest`, which supplies
|
|
497
|
+
// `DEFAULT_BUDGET_CHARS` itself (`digest/render.ts`). Hardcoding that
|
|
498
|
+
// number here would be a second place claiming to know the default, and
|
|
499
|
+
// the two would silently drift the next time the library's own changed.
|
|
500
|
+
const budgetChars = integer(values['budget-chars'], 'budget-chars', 1);
|
|
501
|
+
const k = integer(values.k, 'k', 1);
|
|
502
|
+
const limit = integer(values.limit, 'limit', 0);
|
|
503
|
+
return {
|
|
504
|
+
...(values.focus !== undefined ? { focus: values.focus } : {}),
|
|
505
|
+
...(values.edges ? { edges: true } : {}),
|
|
506
|
+
...(values.all ? { all: true } : {}),
|
|
507
|
+
...(budgetLines !== undefined ? { budgetLines } : {}),
|
|
508
|
+
...(budgetChars !== undefined ? { budgetChars } : {}),
|
|
509
|
+
...(k !== undefined ? { k } : {}),
|
|
510
|
+
...(limit !== undefined ? { limit } : {}),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Read the bundle, or say which path was tried.
|
|
515
|
+
*
|
|
516
|
+
* `--from` is the only thing standing between an agent and six-month-old
|
|
517
|
+
* measurements, so a miss names the resolved absolute path rather than echoing
|
|
518
|
+
* back whatever relative string was typed — the two differ exactly when the
|
|
519
|
+
* caller is in the wrong directory, which is the case worth diagnosing.
|
|
520
|
+
*/
|
|
521
|
+
function readBundle(from) {
|
|
522
|
+
const path = (0, node_path_1.resolve)(from);
|
|
523
|
+
let text;
|
|
524
|
+
try {
|
|
525
|
+
text = (0, node_fs_1.readFileSync)(path, 'utf8');
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
const code = error.code;
|
|
529
|
+
if (code === 'ENOENT') {
|
|
530
|
+
return fail(BUNDLE_ERROR, `dnumb: no bundle at ${path}. --from takes a file written by capture() (dnumb/playwright), not a URL or a directory.`);
|
|
531
|
+
}
|
|
532
|
+
if (code === 'EISDIR') {
|
|
533
|
+
return fail(BUNDLE_ERROR, `dnumb: ${path} is a directory. --from takes one bundle file.`);
|
|
534
|
+
}
|
|
535
|
+
return fail(BUNDLE_ERROR, `dnumb: could not read ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
return (0, bundle_js_1.parseBundle)(text, path);
|
|
539
|
+
}
|
|
540
|
+
catch (error) {
|
|
541
|
+
if (error instanceof bundle_js_1.BundleFormatError)
|
|
542
|
+
return fail(BUNDLE_ERROR, `dnumb: ${error.message}`);
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Mirrors `src/diff/render.ts`'s own (unexported) `RANK` and `documentOrder`
|
|
548
|
+
* -- `renderDiff` decides the text's line order and does not export the
|
|
549
|
+
* pieces that do, and this file owns the `--json` payload's own ordering
|
|
550
|
+
* decision (B5). Kept in the same shape on purpose: `--json` and the text
|
|
551
|
+
* describe the same diff, and a caller comparing the Nth JSON entry against
|
|
552
|
+
* the Nth text line should never find them disagreeing about what comes
|
|
553
|
+
* first. `Change['kind']` values are fixed by `diff/types.ts`; this table
|
|
554
|
+
* cannot silently miss one the compiler wouldn't catch.
|
|
555
|
+
*/
|
|
556
|
+
const DIFF_KIND_RANK = {
|
|
557
|
+
added: 0,
|
|
558
|
+
removed: 0,
|
|
559
|
+
appeared: 1,
|
|
560
|
+
disappeared: 1,
|
|
561
|
+
text: 2,
|
|
562
|
+
attr: 2,
|
|
563
|
+
clip: 3,
|
|
564
|
+
overflow: 3,
|
|
565
|
+
box: 4,
|
|
566
|
+
style: 5,
|
|
567
|
+
};
|
|
568
|
+
/** Document order, read off the id -- same convention as `render.ts`'s own. */
|
|
569
|
+
function diffDocumentOrder(change) {
|
|
570
|
+
const id = change.after?.id ?? change.before?.id ?? '';
|
|
571
|
+
const digits = /^e(\d+)$/.exec(id)?.[1];
|
|
572
|
+
return digits === undefined ? 0 : Number(digits);
|
|
573
|
+
}
|
|
574
|
+
const diffRank = (change) => change.cause === 'derived' ? DIFF_KIND_RANK.style + 1 : DIFF_KIND_RANK[change.kind];
|
|
575
|
+
/**
|
|
576
|
+
* The `Change[]` `--json` actually reports: `renderDiff` never prints a
|
|
577
|
+
* `subsumed` change (`src/diff/render.ts`'s own `live` filter) -- it exists
|
|
578
|
+
* only to explain the change that absorbed it -- so including it here let
|
|
579
|
+
* `--json` report entries the text can never produce, and slicing the raw,
|
|
580
|
+
* unsorted array by `--limit` capped a different, differently-ordered list
|
|
581
|
+
* than the one `--limit` (as `maxLines`) capped in the text (B5). Filtering
|
|
582
|
+
* to the same "live" set and sorting it the same way the text is sorted
|
|
583
|
+
* fixes both: the two now describe the same set of changes, in the same
|
|
584
|
+
* order, even though the text goes on to fold some of them onto shared
|
|
585
|
+
* lines (`foldDerived`) where `--json` keeps one entry per change.
|
|
586
|
+
*/
|
|
587
|
+
function reportableChanges(changes) {
|
|
588
|
+
return [...changes]
|
|
589
|
+
.filter((change) => change.subsumed === undefined)
|
|
590
|
+
.sort((a, b) => diffRank(a) - diffRank(b) || diffDocumentOrder(a) - diffDocumentOrder(b));
|
|
591
|
+
}
|
|
592
|
+
function main(argv = process.argv.slice(2)) {
|
|
593
|
+
let values;
|
|
594
|
+
let positionals;
|
|
595
|
+
try {
|
|
596
|
+
({ values, positionals } = (0, node_util_1.parseArgs)({
|
|
597
|
+
args: argv,
|
|
598
|
+
options: OPTIONS,
|
|
599
|
+
allowPositionals: true,
|
|
600
|
+
strict: true,
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
605
|
+
const hint = error.code === 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
|
|
606
|
+
? 'If that was a negative coordinate, put `--` before it: dnumb --from b.json q at -- -8 120'
|
|
607
|
+
: 'Run `dnumb --help` for the verbs and their arguments.';
|
|
608
|
+
return fail(USAGE_ERROR, `dnumb: ${message}\n${hint}`);
|
|
609
|
+
}
|
|
610
|
+
if (values.version === true) {
|
|
611
|
+
const version = packageVersion();
|
|
612
|
+
out(values.json === true
|
|
613
|
+
? `${JSON.stringify({ package: version, spec: index_js_3.SPEC_VERSION })}\n`
|
|
614
|
+
: `dnumb ${version} (spec ${index_js_3.SPEC_VERSION})\n`);
|
|
615
|
+
return OK;
|
|
616
|
+
}
|
|
617
|
+
const verb = positionals[0];
|
|
618
|
+
if (values.help === true || verb === undefined) {
|
|
619
|
+
out(USAGE);
|
|
620
|
+
return values.help === true ? OK : USAGE_ERROR;
|
|
621
|
+
}
|
|
622
|
+
if (verb !== 'look' && verb !== 'q' && verb !== 'diff' && verb !== 'capture') {
|
|
623
|
+
return fail(USAGE_ERROR, `dnumb: unknown verb "${verb}". Known: look, diff, capture, q <${query_js_1.Q_VERBS.join(' | ')}>.`);
|
|
624
|
+
}
|
|
625
|
+
// `capture` is the one verb with a browser in it and no bundle to read, so
|
|
626
|
+
// it is dispatched before `--from` is required of everything else.
|
|
627
|
+
if (verb === 'capture') {
|
|
628
|
+
return runCapture(positionals.slice(1), values, values.json === true);
|
|
629
|
+
}
|
|
630
|
+
let options;
|
|
631
|
+
try {
|
|
632
|
+
options = optionsFrom(values);
|
|
633
|
+
}
|
|
634
|
+
catch (error) {
|
|
635
|
+
if (error instanceof query_js_1.QueryError)
|
|
636
|
+
return fail(USAGE_ERROR, `dnumb: ${error.message}`);
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
if (typeof values.from !== 'string' || values.from === '') {
|
|
640
|
+
return fail(USAGE_ERROR, 'dnumb: --from <bundle> is required. Every verb but `capture` reads a capture bundle; ' +
|
|
641
|
+
'there is no browser in this command. Use `dnumb capture <url>` to create one.');
|
|
642
|
+
}
|
|
643
|
+
const bundle = readBundle(values.from);
|
|
644
|
+
if (typeof bundle === 'number')
|
|
645
|
+
return bundle;
|
|
646
|
+
if (verb === 'diff') {
|
|
647
|
+
const otherPath = positionals[1];
|
|
648
|
+
if (otherPath === undefined) {
|
|
649
|
+
return fail(USAGE_ERROR, 'dnumb: diff needs a second bundle: dnumb --from before.json diff after.json');
|
|
650
|
+
}
|
|
651
|
+
const other = readBundle(otherPath);
|
|
652
|
+
if (typeof other === 'number')
|
|
653
|
+
return other;
|
|
654
|
+
let result;
|
|
655
|
+
try {
|
|
656
|
+
result = (0, index_js_1.diffCaptures)(bundle, other, {
|
|
657
|
+
beforeName: bundle.name,
|
|
658
|
+
afterName: other.name,
|
|
659
|
+
// `--help` documents `--limit 0` as "0 for all", matching `query.ts`'s
|
|
660
|
+
// own `cap()` precedent (`limit <= 0` shows everything). `renderDiff`
|
|
661
|
+
// (`src/diff/render.ts`) does not itself treat 0 that way -- it takes
|
|
662
|
+
// `maxLines` at face value, so `maxLines: 0` caps the body to zero
|
|
663
|
+
// lines and prints only the "not listed" notice (B6). `Infinity` is a
|
|
664
|
+
// cap `body.length` can never exceed, so it reads as "no cap" to the
|
|
665
|
+
// exact same `shown.length > maxLines` check without this file
|
|
666
|
+
// reaching into that module to change what it does with the number.
|
|
667
|
+
...(options.limit === undefined
|
|
668
|
+
? {}
|
|
669
|
+
: { maxLines: options.limit <= 0 ? Number.POSITIVE_INFINITY : options.limit }),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
// Comparing two viewports measures the viewport, not the change --
|
|
674
|
+
// `diffCaptures` itself refuses this now (`DiffViewportMismatchError`,
|
|
675
|
+
// worded identically to what this file used to check for itself), so a
|
|
676
|
+
// second guard here would be a second place deciding what this
|
|
677
|
+
// measurement means, which is the exact mistake this file's own header
|
|
678
|
+
// comment rules out.
|
|
679
|
+
if (error instanceof index_js_1.DiffViewportMismatchError) {
|
|
680
|
+
return fail(USAGE_ERROR, error.message);
|
|
681
|
+
}
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
// `--limit` already caps `result.text`'s body (`diffCaptures` ->
|
|
685
|
+
// `renderDiff`); `--json` used to ignore it and always emit every change
|
|
686
|
+
// regardless, so an agent that passed `--limit` to bound its context got
|
|
687
|
+
// an unbounded payload back with no sign the flag did nothing.
|
|
688
|
+
// `result.truncated` already means something else -- whether either
|
|
689
|
+
// *source capture* hit the 5000-element cap -- so the count `--limit`
|
|
690
|
+
// itself left out gets its own key, `changesOmitted`, rather than
|
|
691
|
+
// overloading a key that already answers a different question.
|
|
692
|
+
//
|
|
693
|
+
// `reportableChanges` (above) is the fix for B5: capping the raw,
|
|
694
|
+
// unsorted `result.changes` -- which still carries every `subsumed`
|
|
695
|
+
// change the text never prints -- against a `maxLines` that bounds text
|
|
696
|
+
// *lines* gave `--limit 1` a "complete" text diff and a `changesOmitted`
|
|
697
|
+
// in the double digits on the same response. Capping the same
|
|
698
|
+
// live-and-sorted list the text is built from keeps the two answering
|
|
699
|
+
// the same question about the same diff.
|
|
700
|
+
const reportable = reportableChanges(result.changes);
|
|
701
|
+
const limit = options.limit;
|
|
702
|
+
const capChanges = limit !== undefined && limit > 0;
|
|
703
|
+
const changes = capChanges ? reportable.slice(0, limit) : reportable;
|
|
704
|
+
const changesOmitted = capChanges ? reportable.length - changes.length : 0;
|
|
705
|
+
out(values.json === true
|
|
706
|
+
? `${JSON.stringify({
|
|
707
|
+
verb: 'diff',
|
|
708
|
+
before: bundle.name,
|
|
709
|
+
after: other.name,
|
|
710
|
+
truncated: result.truncated,
|
|
711
|
+
changesOmitted,
|
|
712
|
+
changes,
|
|
713
|
+
}, null, 2)}\n`
|
|
714
|
+
: `${result.text}\n`);
|
|
715
|
+
return OK;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const result = verb === 'look'
|
|
719
|
+
? (0, query_js_1.look)(bundle, options)
|
|
720
|
+
: (0, query_js_1.query)(bundle, positionals[1] ?? '', positionals.slice(2), options);
|
|
721
|
+
// `docs/notes/07-refusal.md` §2/§7: `look --json` gains `sparseCapture`
|
|
722
|
+
// so a programmatic caller can branch without string-matching the
|
|
723
|
+
// digest text. Read off the rendered text's own marker rather than
|
|
724
|
+
// recomputing the gate here -- `renderDigest` already decided this once
|
|
725
|
+
// (`elements >= 20 && dropped/total >= 0.9 && page.height <= 2`), and
|
|
726
|
+
// re-deriving it a second time in this file is exactly the "two places
|
|
727
|
+
// that decide what a bundle looks like" failure mode this file's own
|
|
728
|
+
// header comment warns against, one level up. `query`'s other verbs
|
|
729
|
+
// (`q ...`) are untouched -- the gate governs `look`'s default body
|
|
730
|
+
// only, not what `q facts`/`q describe`/`q find` return.
|
|
731
|
+
const json = verb === 'look' && result.json !== null && typeof result.json === 'object'
|
|
732
|
+
? { ...result.json, sparseCapture: result.text.includes(index_js_2.SPARSE_CAPTURE_MARKER) }
|
|
733
|
+
: result.json;
|
|
734
|
+
out(values.json === true ? `${JSON.stringify(json, null, 2)}\n` : `${result.text}\n`);
|
|
735
|
+
return OK;
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
if (error instanceof query_js_1.QueryError)
|
|
739
|
+
return fail(USAGE_ERROR, `dnumb: ${error.message}`);
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// `main` returns a plain number for every verb except `capture`, which needs
|
|
744
|
+
// a browser and a network round trip. Handling both shapes here — rather than
|
|
745
|
+
// making every verb async — keeps the exit code synchronous for the common
|
|
746
|
+
// case and lets tests call `main()` and read `.code` back immediately.
|
|
747
|
+
const mainResult = main();
|
|
748
|
+
if (mainResult instanceof Promise) {
|
|
749
|
+
mainResult.then((code) => {
|
|
750
|
+
process.exitCode = code;
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
process.exitCode = mainResult;
|
|
755
|
+
}
|