@crouton-kit/capture 1.4.2 → 1.4.11
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 +187 -0
- package/bin/capture +31278 -7312
- package/package.json +3 -1
- package/src/capture.ts +21 -18
- package/src/cdp/args.ts +269 -49
- package/src/cdp/bridge/server.ts +6 -5
- package/src/cdp/bridge/spawn.ts +41 -97
- package/src/cdp/commands/bridge-serve.ts +11 -21
- package/src/cdp/commands/cdp.ts +44 -22
- package/src/cdp/commands/heap/census.ts +61 -0
- package/src/cdp/commands/heap/common.ts +77 -0
- package/src/cdp/commands/heap/diff.ts +92 -0
- package/src/cdp/commands/heap/index.ts +38 -0
- package/src/cdp/commands/heap/objects.ts +42 -0
- package/src/cdp/commands/heap/retainers.ts +52 -0
- package/src/cdp/commands/heap/snapshot.ts +94 -0
- package/src/cdp/commands/lib.ts +9 -17
- package/src/cdp/commands/lighthouse.ts +414 -0
- package/src/cdp/commands/measure/census.ts +2 -1
- package/src/cdp/commands/measure/check.ts +70 -16
- package/src/cdp/commands/measure/diff.ts +1 -2
- package/src/cdp/commands/measure/explain.ts +44 -7
- package/src/cdp/commands/measure/index.ts +24 -32
- package/src/cdp/commands/measure/map-ax.ts +2 -1
- package/src/cdp/commands/measure/map-focus.ts +2 -1
- package/src/cdp/commands/measure/map-layers.ts +5 -3
- package/src/cdp/commands/measure/map-scroll.ts +2 -1
- package/src/cdp/commands/measure/snap.ts +5 -4
- package/src/cdp/commands/measure/sweep.ts +16 -11
- package/src/cdp/commands/measure/text.ts +147 -0
- package/src/cdp/commands/motion/index.ts +10 -18
- package/src/cdp/commands/motion/jank.ts +13 -22
- package/src/cdp/commands/motion/mask.ts +10 -9
- package/src/cdp/commands/motion/rec.ts +228 -26
- package/src/cdp/commands/motion/timeline.ts +1 -1
- package/src/cdp/commands/page/click.ts +15 -5
- package/src/cdp/commands/page/elements.ts +43 -11
- package/src/cdp/commands/page/exec.ts +53 -18
- package/src/cdp/commands/page/index.ts +20 -20
- package/src/cdp/commands/page/inspect.ts +338 -0
- package/src/cdp/commands/page/navigate.ts +83 -38
- package/src/cdp/commands/page/repeat.ts +176 -0
- package/src/cdp/commands/page/scroll.ts +10 -5
- package/src/cdp/commands/page/shot.ts +66 -31
- package/src/cdp/commands/page/type.ts +6 -2
- package/src/cdp/commands/perf/index.ts +32 -0
- package/src/cdp/commands/perf/insights.ts +214 -0
- package/src/cdp/commands/perf/trace.ts +235 -0
- package/src/cdp/commands/perf/vitals.ts +76 -0
- package/src/cdp/commands/session/collectors.ts +54 -0
- package/src/cdp/commands/stub.ts +10 -0
- package/src/cdp/commands/tab/close.ts +2 -2
- package/src/cdp/commands/tab/index.ts +16 -23
- package/src/cdp/commands/tab/launch.ts +1 -1
- package/src/cdp/commands/tab/mock/index.ts +24 -0
- package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
- package/src/cdp/commands/tab/mock/start.ts +65 -0
- package/src/cdp/commands/tab/mock/stop.ts +39 -0
- package/src/cdp/commands/tab/network.ts +22 -13
- package/src/cdp/commands/tab/reset.ts +11 -12
- package/src/cdp/connection.ts +19 -39
- package/src/cdp/detect.ts +27 -7
- package/src/cdp/dispatch.ts +25 -4
- package/src/cdp/domains.ts +2 -2
- package/src/cdp/har-recorder.ts +0 -1
- package/src/cdp/heap-snapshot.ts +647 -0
- package/src/cdp/host/client.ts +22 -0
- package/src/cdp/host/collector.ts +74 -0
- package/src/cdp/host/collectors/heap.ts +79 -0
- package/src/cdp/host/collectors/intercept.ts +247 -0
- package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
- package/src/cdp/host/collectors/trace.ts +108 -0
- package/src/cdp/host/core.ts +217 -0
- package/src/cdp/host/handle.ts +47 -0
- package/src/cdp/host/held-client.ts +91 -0
- package/src/cdp/host/kinds.ts +97 -0
- package/src/cdp/host/lifecycle.ts +123 -0
- package/src/cdp/host/server.ts +88 -0
- package/src/cdp/measure/check.ts +181 -16
- package/src/cdp/measure/collectors/hittest.ts +16 -10
- package/src/cdp/measure/collectors/scroll.ts +15 -7
- package/src/cdp/measure/collectors/style-provenance.ts +11 -0
- package/src/cdp/measure/collectors/styles.ts +4 -4
- package/src/cdp/measure/explain.ts +14 -2
- package/src/cdp/measure/map-layers.ts +52 -8
- package/src/cdp/measure/snapshot.ts +5 -3
- package/src/cdp/measure/sweep.ts +29 -6
- package/src/cdp/measure/text.ts +227 -0
- package/src/cdp/motion/jank.ts +29 -83
- package/src/cdp/motion/mask.ts +18 -9
- package/src/cdp/motion/recorder.ts +86 -731
- package/src/cdp/scope-lock.ts +9 -9
- package/src/cdp/screenshot.ts +51 -21
- package/src/cdp/timing.ts +5 -5
- package/src/cdp/trace-analysis.ts +320 -0
- package/src/cdp/types.ts +31 -1
- package/src/cdp.ts +4 -6
- package/src/har-manager.ts +2 -4
- package/src/interact.ts +50 -23
- package/src/output/artifact.ts +49 -5
- package/src/output/render.ts +60 -9
- package/src/output/selection.ts +6 -0
- package/src/session/artifacts.ts +122 -12
- package/src/session/commands.ts +348 -113
- package/src/session/coordinator.ts +0 -93
- package/src/session/har-redact.ts +29 -23
- package/src/session-context.ts +13 -9
- package/src/cdp/bridge/protocol.ts +0 -224
- package/src/cdp/recorder-client.ts +0 -326
- package/src/cdp/tab-lock.ts +0 -132
package/src/cdp/args.ts
CHANGED
|
@@ -15,34 +15,44 @@ export function expandEqualsFlags(argv: string[]): string[] {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const VALUE_FLAGS = new Set([
|
|
18
|
-
'--port', '--out', '--duration', '--settle', '--file', '--target', '--url', '--into', '--viewport', '--color-scheme', '--session',
|
|
19
|
-
'--filter', '--name', '--filter-url', '--filter-status', '--filter-method', '--limit', '--params', '--wait-event',
|
|
18
|
+
'--port', '--out', '--artifact-dir', '--duration', '--settle', '--file', '--target', '--url', '--into', '--viewport', '--color-scheme', '--behavior', '--session',
|
|
19
|
+
'--filter', '--name', '--filter-url', '--body', '--filter-status', '--filter-method', '--limit', '--params', '--wait-event',
|
|
20
20
|
'--timeout', '--socket', '--settle-timeout', '--state', '--for', '--before', '--after', '--snap', '--set-file',
|
|
21
21
|
'--axis', '--from', '--to', '--viewport-height', '--rec-id', '--selector', '--do', '--element', '--prop', '--action', '--occurrence',
|
|
22
|
-
'--state-padding', '--crop', '--crop-selector', '--pad', '--zoom',
|
|
22
|
+
'--state-padding', '--crop', '--crop-selector', '--pad', '--zoom', '--constructor', '--node', '--paths', '--sort', '--rules', '--categories', '--preset',
|
|
23
|
+
'--scope', '--event', '--steps', '--iterations',
|
|
23
24
|
]);
|
|
24
25
|
|
|
25
|
-
function
|
|
26
|
-
|
|
26
|
+
function syntaxSchemaInput(command: string | undefined, received: string, expected: string, field: string): ReturnType<typeof invalidInput> {
|
|
27
|
+
return command
|
|
28
|
+
? schemaInput(command, received, expected, field)
|
|
29
|
+
: invalidInput(`received: ${received}\nexpected: ${expected}\nfield: ${field}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function schemaValueFor(command: string | undefined, flag: string, next: string | undefined): string {
|
|
33
|
+
if (next === undefined || next.startsWith('--')) throw syntaxSchemaInput(command, `${flag}=(missing)`, `a value for ${flag}`, flag);
|
|
27
34
|
return next;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
function
|
|
37
|
+
function schemaInteger(command: string | undefined, token: string, flag: string, min: number, max: number, safe = false): number {
|
|
31
38
|
// Full unsigned decimal integer grammar (leading zeros allowed); no sign,
|
|
32
39
|
// exponent, decimal point, partial token, or non-finite value.
|
|
33
|
-
|
|
40
|
+
const expected = `${safe ? 'a safe ' : ''}integer from ${min} to ${max}`;
|
|
41
|
+
const received = `${flag}=${token}`;
|
|
42
|
+
if (!/^\d+$/.test(token)) throw syntaxSchemaInput(command, received, expected, flag);
|
|
34
43
|
const value = Number(token);
|
|
35
44
|
if (!Number.isInteger(value) || value < min || value > max || (safe && !Number.isSafeInteger(value))) {
|
|
36
|
-
throw
|
|
45
|
+
throw syntaxSchemaInput(command, received, expected, flag);
|
|
37
46
|
}
|
|
38
47
|
return value;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
function
|
|
50
|
+
function schemaDuration(command: string | undefined, token: string): number {
|
|
42
51
|
// Full unsigned decimal-seconds grammar (`1`, `1.`, `.5`, `00.5`); no sign,
|
|
43
52
|
// exponent, partial token, or non-finite value. Compare the decimal token
|
|
44
53
|
// before Number conversion so precision rounding cannot admit an overflow.
|
|
45
|
-
|
|
54
|
+
const expected = 'an unsigned decimal seconds value from 0 through 2147483.647';
|
|
55
|
+
if (!/^(?:\d+\.?\d*|\.\d+)$/.test(token)) throw syntaxSchemaInput(command, `--duration=${token}`, expected, '--duration');
|
|
46
56
|
const [whole = '', fraction = ''] = token.split('.');
|
|
47
57
|
const normalizedWhole = whole.replace(/^0+/, '') || '0';
|
|
48
58
|
const maxWhole = '2147483';
|
|
@@ -50,7 +60,7 @@ function durationMs(token: string): number {
|
|
|
50
60
|
const maxFraction = '647';
|
|
51
61
|
const fractionComparison = fraction.slice(0, maxFraction.length).padEnd(maxFraction.length, '0').localeCompare(maxFraction);
|
|
52
62
|
if (wholeComparison > 0 || (wholeComparison === 0 && (fractionComparison > 0 || (fractionComparison === 0 && /[1-9]/.test(fraction.slice(maxFraction.length)))))) {
|
|
53
|
-
throw
|
|
63
|
+
throw syntaxSchemaInput(command, `--duration=${token}`, expected, '--duration');
|
|
54
64
|
}
|
|
55
65
|
return Number(token) * 1000;
|
|
56
66
|
}
|
|
@@ -59,91 +69,212 @@ function validateKnownLeaf(command: string, positional: readonly string[]): void
|
|
|
59
69
|
const leaf = positional[0];
|
|
60
70
|
if (leaf === undefined) return;
|
|
61
71
|
const branches: Record<string, readonly string[]> = {
|
|
62
|
-
session: ['start', 'stop', 'list', 'view', 'har', 'log'],
|
|
63
|
-
page: ['click', 'type', 'scroll', 'navigate', 'exec', 'shot', 'elements'],
|
|
64
|
-
tab: ['launch', 'quit', 'list', 'open', 'close', 'reset', 'network'],
|
|
65
|
-
measure: ['snap', 'check', 'diff', 'census', 'explain', 'sweep', 'map'],
|
|
72
|
+
session: ['start', 'stop', 'list', 'view', 'har', 'log', 'collectors'],
|
|
73
|
+
page: ['click', 'type', 'scroll', 'navigate', 'exec', 'shot', 'elements', 'inspect', 'repeat'],
|
|
74
|
+
tab: ['launch', 'quit', 'list', 'open', 'close', 'reset', 'network', 'mock'],
|
|
75
|
+
measure: ['snap', 'check', 'diff', 'census', 'explain', 'text', 'sweep', 'map'],
|
|
66
76
|
motion: ['rec', 'mask', 'timeline', 'jank', 'response'],
|
|
77
|
+
perf: ['trace', 'vitals', 'insights'],
|
|
78
|
+
heap: ['snapshot', 'census', 'objects', 'retainers', 'diff'],
|
|
67
79
|
lib: ['list', 'search', 'show', 'read'],
|
|
68
80
|
};
|
|
69
81
|
const known = branches[command];
|
|
70
|
-
if (known && !known.includes(leaf))
|
|
82
|
+
if (known && !known.includes(leaf)) {
|
|
83
|
+
if (command === 'perf' || command === 'heap') throw schemaInput(command, leaf, `one of ${known.join(', ')}`, '<leaf>');
|
|
84
|
+
throw invalidInput(`Unknown ${command} leaf: ${leaf}.`, 'unknown_command');
|
|
85
|
+
}
|
|
86
|
+
if (command === 'page' && leaf === 'inspect' && positional[1] !== undefined) {
|
|
87
|
+
const inspectLeaf = positional[1];
|
|
88
|
+
if (!['listeners', 'cardinality', 'frames', 'resources'].includes(inspectLeaf)) throw invalidInput(`Unknown page inspect leaf: ${inspectLeaf}.`, 'unknown_command');
|
|
89
|
+
}
|
|
71
90
|
if (command === 'measure' && leaf === 'map' && positional[1] !== undefined) {
|
|
72
91
|
const mapLeaf = positional[1];
|
|
73
92
|
if (!['focus', 'scroll', 'layers', 'ax', 'paint'].includes(mapLeaf)) throw invalidInput(`Unknown measure map leaf: ${mapLeaf}.`, 'unknown_command');
|
|
74
93
|
}
|
|
94
|
+
if (command === 'tab' && leaf === 'mock' && positional[1] !== undefined) {
|
|
95
|
+
const mockLeaf = positional[1];
|
|
96
|
+
if (!['start', 'stop'].includes(mockLeaf)) throw schemaInput('tab mock', mockLeaf, 'start or stop', '<leaf>');
|
|
97
|
+
}
|
|
75
98
|
}
|
|
76
99
|
|
|
77
|
-
function requireCount(values: readonly string[], min: number, max: number, command: string): void {
|
|
100
|
+
function requireCount(values: readonly string[], min: number, max: number, command: string, field = '<positional>'): void {
|
|
101
|
+
if (values.length < min || values.length > max) {
|
|
102
|
+
const expected = min === max ? `exactly ${min} positional argument(s)` : `${min}..${max} positional argument(s)`;
|
|
103
|
+
throw schemaInput(command, `${values.length} positional argument(s)`, expected, field);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function schemaInput(command: string, received: string, expected: string, field: string): ReturnType<typeof invalidInput> {
|
|
108
|
+
return invalidInput(`received: ${received}\nexpected: ${expected}\nfield: ${field}\nNext: Run \`capture ${command} -h\` and read the schema before re-issuing.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function leafCommand(command: string, positional: readonly string[]): string {
|
|
112
|
+
if (command === 'lighthouse' || command === 'cdp') return command;
|
|
113
|
+
if (command === 'tab' && positional[0] === 'mock') return positional[1] ? `tab mock ${positional[1]}` : 'tab mock';
|
|
114
|
+
if (command === 'page' && positional[0] === 'inspect') return positional[1] ? `page inspect ${positional[1]}` : 'page inspect';
|
|
115
|
+
if (command === 'measure' && positional[0] === 'map') return positional[1] ? `measure map ${positional[1]}` : 'measure map';
|
|
116
|
+
return positional[0] ? `${command} ${positional[0]}` : command;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function syntaxCommand(argv: readonly string[]): string {
|
|
120
|
+
const command = argv[0] ?? 'capture';
|
|
121
|
+
const positional: string[] = [];
|
|
122
|
+
for (let index = 1; index < argv.length; index++) {
|
|
123
|
+
const token = argv[index]!;
|
|
124
|
+
if (VALUE_FLAGS.has(token)) { index++; continue; }
|
|
125
|
+
if (!token.startsWith('-')) positional.push(token);
|
|
126
|
+
}
|
|
127
|
+
return leafCommand(command, positional);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function requireSchemaCount(values: readonly string[], min: number, max: number, command: string, field: string): void {
|
|
78
131
|
if (values.length < min || values.length > max) {
|
|
79
132
|
const expected = min === max ? `exactly ${min}` : `${min}..${max}`;
|
|
80
|
-
throw
|
|
133
|
+
throw schemaInput(command, `${values.length} positional argument(s)`, expected, field);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assertSchemaUrl(value: string, command: string, field: string): void {
|
|
138
|
+
try {
|
|
139
|
+
assertParseableUrl(value, `${command} URL`);
|
|
140
|
+
} catch {
|
|
141
|
+
throw schemaInput(command, value, 'an absolute parseable URL', field);
|
|
81
142
|
}
|
|
82
143
|
}
|
|
83
144
|
|
|
145
|
+
function cdpMember(value: string): boolean {
|
|
146
|
+
return /^[A-Za-z_$][\w$]*\.[A-Za-z_$][\w$]*$/.test(value);
|
|
147
|
+
}
|
|
148
|
+
|
|
84
149
|
/** Pure branch/leaf validation. It must stay before session and environment resolution. */
|
|
85
150
|
export function validateCliInvocation(parsed: ParsedArgs): void {
|
|
86
151
|
validateKnownLeaf(parsed.command, parsed.positional);
|
|
87
|
-
if (parsed.help) return;
|
|
152
|
+
if (parsed.help && parsed.command !== 'cdp') return;
|
|
153
|
+
if (parsed.body !== undefined && !(parsed.command === 'session' && parsed.positional[0] === 'har')) {
|
|
154
|
+
throw schemaInput(leafCommand(parsed.command, parsed.positional), `--body=${parsed.body}`, '--body only with capture session har', '--body');
|
|
155
|
+
}
|
|
88
156
|
if (parsed.command === 'cdp') {
|
|
89
157
|
requireCount(parsed.positional, 0, 1, 'cdp');
|
|
90
|
-
if (parsed.positional
|
|
158
|
+
if (parsed.positional[0] !== undefined && !cdpMember(parsed.positional[0])) {
|
|
159
|
+
throw schemaInput('cdp', parsed.positional[0], 'a <Domain.method> member or --wait-event <Domain.event>', '<Domain.method>');
|
|
160
|
+
}
|
|
161
|
+
if (parsed.waitEvent !== undefined && !cdpMember(parsed.waitEvent)) {
|
|
162
|
+
throw schemaInput('cdp', parsed.waitEvent, 'a <Domain.event> member', '--wait-event');
|
|
163
|
+
}
|
|
164
|
+
if (parsed.help) return;
|
|
165
|
+
if (parsed.positional.length === 0 && !parsed.waitEvent) throw schemaInput('cdp', '(missing)', 'a <Domain.method> positional or --wait-event <Domain.event>', '<Domain.method> or --wait-event');
|
|
91
166
|
if (parsed.params !== undefined) {
|
|
92
167
|
try {
|
|
93
168
|
const value = JSON.parse(parsed.params) as unknown;
|
|
94
169
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('not an object');
|
|
95
170
|
} catch {
|
|
96
|
-
throw
|
|
171
|
+
throw schemaInput('cdp', parsed.params, 'a JSON-encoded params object', '--params');
|
|
97
172
|
}
|
|
98
173
|
}
|
|
99
174
|
return;
|
|
100
175
|
}
|
|
176
|
+
if (parsed.command === 'lighthouse') {
|
|
177
|
+
requireSchemaCount(parsed.positional, 1, 1, 'lighthouse', '<url>');
|
|
178
|
+
assertSchemaUrl(parsed.positional[0], 'lighthouse', '<url>');
|
|
179
|
+
if (parsed.categories !== undefined) {
|
|
180
|
+
const categories = parsed.categories.split(',');
|
|
181
|
+
if (categories.length === 0 || categories.some(category => !['performance', 'accessibility', 'best-practices', 'seo'].includes(category))) {
|
|
182
|
+
throw schemaInput('lighthouse', parsed.categories, 'a comma-separated list of performance, accessibility, best-practices, or seo', '--categories');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (parsed.preset !== undefined && !['mobile', 'desktop'].includes(parsed.preset)) throw schemaInput('lighthouse', parsed.preset, 'mobile or desktop', '--preset');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
101
188
|
if (parsed.positional[0] === undefined) return;
|
|
102
189
|
|
|
103
190
|
const leaf = parsed.positional[0];
|
|
104
191
|
const values = parsed.positional.slice(1);
|
|
105
192
|
if (parsed.command === 'session') {
|
|
193
|
+
if (leaf === 'collectors') {
|
|
194
|
+
requireSchemaCount(values, 0, 0, 'session collectors', '(none)');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
106
197
|
const ranges: Record<string, readonly [number, number]> = {
|
|
107
|
-
start: [0, 0], stop: [1, 1], list: [0, 0], view: [1, 1], har: [0, 1], log: [1, 1],
|
|
198
|
+
start: [0, 0], stop: [1, 1], list: [0, 0], view: [1, 1], har: [0, 1], log: [1, 1], collectors: [0, 0],
|
|
108
199
|
};
|
|
109
200
|
const [min, max] = ranges[leaf];
|
|
110
201
|
requireCount(values, min, max, `session ${leaf}`);
|
|
111
202
|
if (leaf === 'start' && parsed.url !== undefined && parsed.target !== undefined) {
|
|
112
|
-
throw
|
|
203
|
+
throw schemaInput('session start', `--url=${parsed.url}, --target=${parsed.target}`, 'at most one target source', '--url or --target');
|
|
113
204
|
}
|
|
114
205
|
return;
|
|
115
206
|
}
|
|
116
207
|
|
|
117
208
|
if (parsed.command === 'page') {
|
|
118
209
|
if (leaf === 'click' || leaf === 'type' || leaf === 'navigate') requireCount(values, 1, 1, `page ${leaf}`);
|
|
119
|
-
if (leaf === 'navigate')
|
|
210
|
+
if (leaf === 'navigate') assertSchemaUrl(values[0], 'page navigate', '<url>');
|
|
120
211
|
if (leaf === 'scroll') {
|
|
121
212
|
requireCount(values, 1, 1, 'page scroll');
|
|
122
|
-
if (parsed.to === undefined) throw
|
|
123
|
-
assertScrollDestination(parsed.to);
|
|
213
|
+
if (parsed.to === undefined) throw schemaInput('page scroll', '(missing)', 'a --to <top|bottom|px> destination', '--to');
|
|
214
|
+
try { assertScrollDestination(parsed.to); } catch { throw schemaInput('page scroll', parsed.to, 'top, bottom, or a non-negative pixel offset', '--to'); }
|
|
124
215
|
}
|
|
125
216
|
if (leaf === 'exec') requireCount(values, parsed.file === undefined ? 1 : 0, parsed.file === undefined ? 1 : 0, 'page exec');
|
|
126
217
|
if (leaf === 'shot' || leaf === 'elements') requireCount(values, 0, 0, `page ${leaf}`);
|
|
218
|
+
if (leaf === 'repeat') {
|
|
219
|
+
requireCount(values, 0, 0, 'page repeat');
|
|
220
|
+
if (parsed.iterations === undefined) throw schemaInput('page repeat', '(missing)', 'an iteration count from 1 to 100', '--iterations');
|
|
221
|
+
if (parsed.steps === undefined) throw schemaInput('page repeat', '(missing)', 'a JSON array of 1..20 click/type action objects', '--steps');
|
|
222
|
+
try {
|
|
223
|
+
const steps = JSON.parse(parsed.steps) as unknown;
|
|
224
|
+
const valid = parsed.steps.length <= 10_000 && Array.isArray(steps) && steps.length >= 1 && steps.length <= 20 && steps.every((step) => {
|
|
225
|
+
if (!step || typeof step !== 'object' || Array.isArray(step)) return false;
|
|
226
|
+
const record = step as Record<string, unknown>;
|
|
227
|
+
if (record.action === 'click') return Object.keys(record).length === 2 && typeof record.target === 'string' && record.target.length > 0;
|
|
228
|
+
if (record.action === 'type') return Object.keys(record).every(key => key === 'action' || key === 'text' || key === 'into') && typeof record.text === 'string' && (record.into === undefined || (typeof record.into === 'string' && record.into.trim().length > 0));
|
|
229
|
+
return false;
|
|
230
|
+
});
|
|
231
|
+
if (!valid) throw new Error('invalid steps');
|
|
232
|
+
} catch {
|
|
233
|
+
throw schemaInput('page repeat', parsed.steps, 'a JSON array of 1..20 {"action":"click","target":"…"} or {"action":"type","text":"…","into":"…"} objects', '--steps');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (leaf === 'inspect') {
|
|
237
|
+
const inspectLeaf = values[0];
|
|
238
|
+
if (inspectLeaf === undefined) return;
|
|
239
|
+
requireCount(values.slice(1), 0, 0, `page inspect ${inspectLeaf}`);
|
|
240
|
+
if (inspectLeaf === 'listeners' && (parsed.scope ?? 'window') === 'target' && !parsed.selector?.trim()) {
|
|
241
|
+
throw schemaInput('page inspect listeners', '(missing)', 'a live selector/backend target when --scope target', '--selector');
|
|
242
|
+
}
|
|
243
|
+
if (inspectLeaf === 'listeners' && (parsed.scope ?? 'window') !== 'target' && parsed.selector !== undefined) {
|
|
244
|
+
throw schemaInput('page inspect listeners', `--scope=${parsed.scope ?? 'window'}, --selector=${parsed.selector}`, '--selector only with --scope target', '--scope or --selector');
|
|
245
|
+
}
|
|
246
|
+
if (inspectLeaf === 'frames' && parsed.duration !== undefined && parsed.duration > 60_000) {
|
|
247
|
+
throw schemaInput('page inspect frames', `--duration=${parsed.duration / 1000}`, 'a duration from 0 through 60 seconds', '--duration');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
127
250
|
if (leaf === 'shot' && parsed.viewport !== undefined) {
|
|
128
251
|
const match = /^([1-9]\d*)x([1-9]\d*)$/.exec(parsed.viewport);
|
|
129
252
|
if (!match || !Number.isSafeInteger(Number(match[1])) || !Number.isSafeInteger(Number(match[2]))) {
|
|
130
|
-
throw
|
|
253
|
+
throw schemaInput('page shot', parsed.viewport, '<positive-safe-int>x<positive-safe-int>', '--viewport');
|
|
131
254
|
}
|
|
132
255
|
}
|
|
133
256
|
if (leaf === 'shot' && parsed.colorScheme !== undefined && parsed.colorScheme !== 'dark' && parsed.colorScheme !== 'light') {
|
|
134
|
-
throw
|
|
257
|
+
throw schemaInput('page shot', parsed.colorScheme, 'dark or light', '--color-scheme');
|
|
135
258
|
}
|
|
136
259
|
return;
|
|
137
260
|
}
|
|
138
261
|
|
|
139
262
|
if (parsed.command === 'tab') {
|
|
263
|
+
if (leaf === 'mock') {
|
|
264
|
+
const mockLeaf = values[0];
|
|
265
|
+
if (mockLeaf === undefined) return;
|
|
266
|
+
const mockValues = values.slice(1);
|
|
267
|
+
requireSchemaCount(mockValues, 0, 0, `tab mock ${mockLeaf}`, '(none)');
|
|
268
|
+
if (mockLeaf === 'start' && !parsed.rules?.trim()) throw schemaInput('tab mock start', parsed.rules ?? '(missing)', 'a JSON rule document path', '--rules');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
140
271
|
// `launch` takes no target at all (its browser does not exist yet) and
|
|
141
272
|
// `quit` takes an optional port; every other tab leaf names exactly one.
|
|
142
273
|
if (leaf === 'launch' || leaf === 'list') requireCount(values, 0, 0, `tab ${leaf}`);
|
|
143
274
|
else if (leaf === 'quit') requireCount(values, 0, 1, 'tab quit');
|
|
144
275
|
else requireCount(values, 1, 1, `tab ${leaf}`);
|
|
145
276
|
if (leaf === 'network' && values[0] !== 'offline' && values[0] !== 'online') {
|
|
146
|
-
throw
|
|
277
|
+
throw schemaInput('tab network', values[0] ?? '(missing)', 'offline or online', '<mode>');
|
|
147
278
|
}
|
|
148
279
|
return;
|
|
149
280
|
}
|
|
@@ -151,30 +282,90 @@ export function validateCliInvocation(parsed: ParsedArgs): void {
|
|
|
151
282
|
if (parsed.command === 'measure') {
|
|
152
283
|
if (leaf === 'snap' || leaf === 'check' || leaf === 'sweep') requireCount(values, 0, 1, `measure ${leaf}`);
|
|
153
284
|
if (leaf === 'snap' && parsed.colorScheme !== undefined && parsed.colorScheme !== 'dark' && parsed.colorScheme !== 'light') {
|
|
154
|
-
throw
|
|
285
|
+
throw schemaInput('measure snap', parsed.colorScheme, 'dark or light', '--color-scheme');
|
|
155
286
|
}
|
|
156
287
|
if (leaf === 'diff' || leaf === 'census') requireCount(values, 0, 0, `measure ${leaf}`);
|
|
157
|
-
if (leaf === 'diff' && (!parsed.before || !parsed.after)) throw
|
|
288
|
+
if (leaf === 'diff' && (!parsed.before || !parsed.after)) throw schemaInput('measure diff', `--before=${parsed.before ?? '(missing)'}, --after=${parsed.after ?? '(missing)'}`, 'both settled snapshot references', '--before and --after');
|
|
158
289
|
if (leaf === 'census' && !['color', 'font', 'spacing', 'radius', 'shadow', 'animation', 'geometry', 'media', 'queries'].includes(parsed.axis ?? '')) {
|
|
159
|
-
throw
|
|
290
|
+
throw schemaInput('measure census', parsed.axis ?? '(missing)', 'color, font, spacing, radius, shadow, animation, geometry, media, or queries', '--axis');
|
|
160
291
|
}
|
|
161
292
|
if (leaf === 'explain') {
|
|
162
293
|
requireCount(values, 1, 1, 'measure explain');
|
|
163
|
-
if (!parsed.selector?.trim()) throw
|
|
294
|
+
if (!parsed.selector?.trim()) throw schemaInput('measure explain', '(missing)', 'a recorded selector input', '--selector');
|
|
295
|
+
}
|
|
296
|
+
if (leaf === 'text') {
|
|
297
|
+
requireCount(values, 1, 1, 'measure text');
|
|
298
|
+
if (!parsed.selector?.trim()) throw schemaInput('measure text', '(missing)', 'a recorded selector input', '--selector');
|
|
164
299
|
}
|
|
165
300
|
if (leaf === 'sweep') {
|
|
166
301
|
if (!['width', 'dpr', 'zoom', 'color-scheme', 'reduced-motion'].includes(parsed.axis ?? '')) {
|
|
167
|
-
throw
|
|
302
|
+
throw schemaInput('measure sweep', parsed.axis ?? '(missing)', 'width, dpr, zoom, color-scheme, or reduced-motion', '--axis');
|
|
303
|
+
}
|
|
304
|
+
try { assertSweepBounds(parsed.axis as SweepAxisName, parsed.from, parsed.to); } catch { throw schemaInput('measure sweep', `--from=${parsed.from ?? '(missing)'}, --to=${parsed.to ?? '(missing)'}`, `valid ${parsed.axis} bounds`, '--from and --to'); }
|
|
305
|
+
if (parsed.viewportHeight !== undefined) {
|
|
306
|
+
try { parsePositiveNumber(parsed.viewportHeight, 1, '--viewport-height'); } catch { throw schemaInput('measure sweep', parsed.viewportHeight, 'a positive number', '--viewport-height'); }
|
|
168
307
|
}
|
|
169
|
-
assertSweepBounds(parsed.axis as SweepAxisName, parsed.from, parsed.to);
|
|
170
|
-
if (parsed.viewportHeight !== undefined) parsePositiveNumber(parsed.viewportHeight, 1, '--viewport-height');
|
|
171
308
|
}
|
|
172
309
|
if (leaf === 'map') {
|
|
173
310
|
const mapLeaf = values[0];
|
|
174
311
|
if (mapLeaf === undefined) return;
|
|
175
312
|
const targets = values.slice(1);
|
|
176
313
|
requireCount(targets, ['focus', 'ax', 'paint'].includes(mapLeaf) ? 1 : 0, 1, `measure map ${mapLeaf}`);
|
|
177
|
-
if (mapLeaf === 'paint' && !parsed.selector?.trim()) throw
|
|
314
|
+
if (mapLeaf === 'paint' && !parsed.selector?.trim()) throw schemaInput('measure map paint', '(missing)', 'a recorded selector input', '--selector');
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (parsed.command === 'perf') {
|
|
320
|
+
if (leaf === 'trace') {
|
|
321
|
+
requireSchemaCount(values, 0, 1, 'perf trace', '[url]');
|
|
322
|
+
if (parsed.start && parsed.stop) throw schemaInput('perf trace', '--start --stop', 'one lifecycle flag', '--start|--stop');
|
|
323
|
+
if (parsed.start || parsed.stop) {
|
|
324
|
+
if (values.length || parsed.do || parsed.duration !== undefined) throw schemaInput('perf trace', 'lifecycle and one-shot arguments combined', '--start or --stop alone', '[url]|--do|--duration');
|
|
325
|
+
} else {
|
|
326
|
+
if (values[0] !== undefined) assertSchemaUrl(values[0], 'perf trace', '[url]');
|
|
327
|
+
if (parsed.do !== undefined) {
|
|
328
|
+
try {
|
|
329
|
+
parseDoAction(parsed.do);
|
|
330
|
+
} catch {
|
|
331
|
+
throw schemaInput('perf trace', parsed.do, 'the motion rec --do action grammar', '--do');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
requireSchemaCount(values, 1, 1, `perf ${leaf}`, '<trace>');
|
|
337
|
+
if (leaf === 'insights' && parsed.full && !parsed.name) throw schemaInput('perf insights', '(missing --name)', 'one engine insight name before --full event detail', '--name');
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (parsed.command === 'heap') {
|
|
343
|
+
if (leaf === 'snapshot') {
|
|
344
|
+
requireSchemaCount(values, 0, 1, 'heap snapshot', '[url]');
|
|
345
|
+
if (values[0] !== undefined) assertSchemaUrl(values[0], 'heap snapshot', '[url]');
|
|
346
|
+
}
|
|
347
|
+
if (leaf === 'census') {
|
|
348
|
+
requireSchemaCount(values, 1, 1, 'heap census', '<snapshot>');
|
|
349
|
+
if (parsed.axis !== undefined && !['constructor', 'string'].includes(parsed.axis)) throw schemaInput('heap census', parsed.axis, 'constructor or string', '--axis');
|
|
350
|
+
}
|
|
351
|
+
if (leaf === 'objects') {
|
|
352
|
+
requireSchemaCount(values, 1, 1, 'heap objects', '<snapshot>');
|
|
353
|
+
const constructor = typeof parsed.constructor === 'string' ? parsed.constructor.trim() : '';
|
|
354
|
+
if (!constructor) throw schemaInput('heap objects', '(missing)', 'an exact constructor name reported by capture heap census', '--constructor');
|
|
355
|
+
if (parsed.sort !== undefined && !['retained', 'self'].includes(parsed.sort)) throw schemaInput('heap objects', parsed.sort, 'retained or self', '--sort');
|
|
356
|
+
}
|
|
357
|
+
if (leaf === 'retainers') {
|
|
358
|
+
requireSchemaCount(values, 1, 1, 'heap retainers', '<snapshot>');
|
|
359
|
+
if (parsed.node === undefined) throw schemaInput('heap retainers', '(missing)', 'a Chrome snapshot object id', '--node');
|
|
360
|
+
try {
|
|
361
|
+
schemaInteger(undefined, parsed.node, '--node', 1, Number.MAX_SAFE_INTEGER, true);
|
|
362
|
+
} catch {
|
|
363
|
+
throw schemaInput('heap retainers', parsed.node, 'a positive safe-integer Chrome snapshot object id', '--node');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (leaf === 'diff') {
|
|
367
|
+
requireSchemaCount(values, 0, 0, 'heap diff', '(none)');
|
|
368
|
+
if (!parsed.before || !parsed.after) throw schemaInput('heap diff', `--before=${parsed.before ?? '(missing)'}, --after=${parsed.after ?? '(missing)'}`, 'both heap snapshot references', '--before and --after');
|
|
178
369
|
}
|
|
179
370
|
return;
|
|
180
371
|
}
|
|
@@ -183,29 +374,29 @@ export function validateCliInvocation(parsed: ParsedArgs): void {
|
|
|
183
374
|
if (leaf === 'mask' || leaf === 'jank' || leaf === 'response') requireCount(values, 1, 1, `motion ${leaf}`);
|
|
184
375
|
if (leaf === 'timeline') {
|
|
185
376
|
requireCount(values, 1, 1, 'motion timeline');
|
|
186
|
-
if (!parsed.element?.trim()) throw
|
|
377
|
+
if (!parsed.element?.trim()) throw schemaInput('motion timeline', '(missing)', 'an element selector input', '--element');
|
|
187
378
|
}
|
|
188
379
|
if (leaf === 'rec') {
|
|
189
|
-
if (parsed.start && parsed.stop) throw
|
|
190
|
-
if (!parsed.stop && parsed.recId) throw
|
|
380
|
+
if (parsed.start && parsed.stop) throw schemaInput('motion rec', '--start --stop', 'one lifecycle flag', '--start or --stop');
|
|
381
|
+
if (!parsed.stop && parsed.recId) throw schemaInput('motion rec', `--rec-id=${parsed.recId}`, '--rec-id only with --stop', '--rec-id');
|
|
191
382
|
if (parsed.start || parsed.stop) {
|
|
192
383
|
requireCount(values, 0, 0, 'motion rec lifecycle');
|
|
193
384
|
if (parsed.do || parsed.duration !== undefined || (parsed.stop && parsed.viewport !== undefined)) {
|
|
194
|
-
throw
|
|
385
|
+
throw schemaInput('motion rec', 'lifecycle and one-shot arguments combined', '--start or --stop alone', '[url]|--do|--duration|--viewport');
|
|
195
386
|
}
|
|
196
387
|
} else {
|
|
197
388
|
// The URL is optional: omitting it records the active session tab
|
|
198
389
|
// (enforced downstream once session state is known — this pure
|
|
199
390
|
// validator only bounds cardinality at 0..1 and checks a supplied URL).
|
|
200
391
|
requireCount(values, 0, 1, 'motion rec');
|
|
201
|
-
if (!parsed.do) throw
|
|
202
|
-
if (values[0] !== undefined)
|
|
203
|
-
parseDoAction(parsed.do);
|
|
392
|
+
if (!parsed.do) throw schemaInput('motion rec', '(missing)', 'a --do action for one-shot recording', '--do');
|
|
393
|
+
if (values[0] !== undefined) assertSchemaUrl(values[0], 'motion rec', '[url]');
|
|
394
|
+
try { parseDoAction(parsed.do); } catch { throw schemaInput('motion rec', parsed.do, 'the documented --do action grammar', '--do'); }
|
|
204
395
|
}
|
|
205
396
|
if (parsed.viewport !== undefined) {
|
|
206
397
|
const match = /^([1-9]\d*)x([1-9]\d*)$/.exec(parsed.viewport);
|
|
207
398
|
if (!match || !Number.isSafeInteger(Number(match[1])) || !Number.isSafeInteger(Number(match[2]))) {
|
|
208
|
-
throw
|
|
399
|
+
throw schemaInput('motion rec', parsed.viewport, '<positive-safe-int>x<positive-safe-int>', '--viewport');
|
|
209
400
|
}
|
|
210
401
|
}
|
|
211
402
|
}
|
|
@@ -225,14 +416,19 @@ export function parseCliSyntax(argv: string[]): ParsedArgs {
|
|
|
225
416
|
const rest = expandEqualsFlags(argv.slice(1));
|
|
226
417
|
const positional: string[] = [];
|
|
227
418
|
const parsed: Partial<ParsedArgs> = { command, positional };
|
|
419
|
+
const commandPath = syntaxCommand(argv);
|
|
420
|
+
const valueFor = (flag: string, next: string | undefined) => schemaValueFor(commandPath, flag, next);
|
|
421
|
+
const integer = (token: string, flag: string, min: number, max: number, safe = false) => schemaInteger(commandPath, token, flag, min, max, safe);
|
|
422
|
+
const durationMs = (token: string) => schemaDuration(commandPath, token);
|
|
228
423
|
|
|
229
424
|
for (let i = 0; i < rest.length; i++) {
|
|
230
425
|
const arg = rest[i];
|
|
231
426
|
const next = rest[i + 1];
|
|
232
|
-
if (VALUE_FLAGS.has(arg)) valueFor(arg, next);
|
|
427
|
+
if (VALUE_FLAGS.has(arg) && !(arg === '--crop' && commandPath === 'measure text')) valueFor(arg, next);
|
|
233
428
|
|
|
234
429
|
if (arg === '--port') { parsed.port = integer(valueFor(arg, next), '--port', 1, 65535); parsed.portSource = 'flag'; i++; }
|
|
235
430
|
else if (arg === '--out') { parsed.out = valueFor(arg, next); i++; }
|
|
431
|
+
else if (arg === '--artifact-dir') { parsed.artifactDir = valueFor(arg, next); i++; }
|
|
236
432
|
else if (arg === '--json') parsed.json = true;
|
|
237
433
|
else if (arg === '--duration') { parsed.duration = durationMs(valueFor(arg, next)); i++; }
|
|
238
434
|
else if (arg === '--settle') { parsed.settle = integer(valueFor(arg, next), '--settle', 0, 2_147_483_647); i++; }
|
|
@@ -245,6 +441,12 @@ export function parseCliSyntax(argv: string[]): ParsedArgs {
|
|
|
245
441
|
else if (arg === '--no-screenshot') parsed.noScreenshot = true;
|
|
246
442
|
else if (arg === '--viewport') { const value = valueFor(arg, next); parsed.viewport = value; (parsed.viewports ??= []).push(value); i++; }
|
|
247
443
|
else if (arg === '--color-scheme') { parsed.colorScheme = valueFor(arg, next); i++; }
|
|
444
|
+
else if (arg === '--behavior') {
|
|
445
|
+
const behavior = valueFor(arg, next);
|
|
446
|
+
if (behavior !== 'instant' && behavior !== 'smooth') throw syntaxSchemaInput(commandPath, `--behavior=${behavior}`, 'instant or smooth', '--behavior');
|
|
447
|
+
parsed.behavior = behavior;
|
|
448
|
+
i++;
|
|
449
|
+
}
|
|
248
450
|
else if (arg === '--full-page') parsed.fullPage = true;
|
|
249
451
|
else if (arg === '--all') parsed.all = true;
|
|
250
452
|
else if (arg === '--session') { parsed.session = valueFor(arg, next); i++; }
|
|
@@ -252,6 +454,7 @@ export function parseCliSyntax(argv: string[]): ParsedArgs {
|
|
|
252
454
|
else if (arg === '--filter') { parsed.filter = valueFor(arg, next); i++; }
|
|
253
455
|
else if (arg === '--name') { parsed.name = valueFor(arg, next); i++; }
|
|
254
456
|
else if (arg === '--filter-url') { parsed.filterUrl = valueFor(arg, next); i++; }
|
|
457
|
+
else if (arg === '--body') { parsed.body = valueFor(arg, next); i++; }
|
|
255
458
|
else if (arg === '--filter-status') { parsed.filterStatus = valueFor(arg, next); i++; }
|
|
256
459
|
else if (arg === '--filter-method') { parsed.filterMethod = valueFor(arg, next).toUpperCase(); i++; }
|
|
257
460
|
else if (arg === '--limit') { parsed.limit = integer(valueFor(arg, next), '--limit', 1, Number.MAX_SAFE_INTEGER, true); i++; }
|
|
@@ -268,6 +471,7 @@ export function parseCliSyntax(argv: string[]): ParsedArgs {
|
|
|
268
471
|
else if (arg === '--state') { (parsed.state ??= []).push(valueFor(arg, next)); i++; }
|
|
269
472
|
else if (arg === '--state-padding') { parsed.statePadding = integer(valueFor(arg, next), '--state-padding', 0, 16_384); i++; }
|
|
270
473
|
else if (arg === '--list-crops') parsed.listCrops = true;
|
|
474
|
+
else if (arg === '--crop' && commandPath === 'measure text') parsed.textCrop = true;
|
|
271
475
|
else if (arg === '--crop') { parsed.crop = valueFor(arg, next); i++; }
|
|
272
476
|
else if (arg === '--crop-selector') { parsed.cropSelector = valueFor(arg, next); i++; }
|
|
273
477
|
else if (arg === '--pad') { parsed.pad = integer(valueFor(arg, next), '--pad', 0, 16_384); i++; }
|
|
@@ -295,7 +499,23 @@ export function parseCliSyntax(argv: string[]): ParsedArgs {
|
|
|
295
499
|
else if (arg === '--prop') { parsed.prop = valueFor(arg, next); i++; }
|
|
296
500
|
else if (arg === '--action') { parsed.action = valueFor(arg, next); i++; }
|
|
297
501
|
else if (arg === '--occurrence') { parsed.occurrence = integer(valueFor(arg, next), '--occurrence', 1, Number.MAX_SAFE_INTEGER, true); i++; }
|
|
298
|
-
else if (arg === '
|
|
502
|
+
else if (arg === '--constructor') { parsed.constructor = valueFor(arg, next); i++; }
|
|
503
|
+
else if (arg === '--node') { parsed.node = valueFor(arg, next); i++; }
|
|
504
|
+
else if (arg === '--paths') { parsed.paths = integer(valueFor(arg, next), '--paths', 1, Number.MAX_SAFE_INTEGER, true); i++; }
|
|
505
|
+
else if (arg === '--sort') { parsed.sort = valueFor(arg, next); i++; }
|
|
506
|
+
else if (arg === '--rules') { parsed.rules = valueFor(arg, next); i++; }
|
|
507
|
+
else if (arg === '--categories') { parsed.categories = valueFor(arg, next); i++; }
|
|
508
|
+
else if (arg === '--preset') { parsed.preset = valueFor(arg, next); i++; }
|
|
509
|
+
else if (arg === '--scope') {
|
|
510
|
+
const scope = valueFor(arg, next);
|
|
511
|
+
if (scope !== 'window' && scope !== 'document' && scope !== 'target') throw syntaxSchemaInput(commandPath, `--scope=${scope}`, 'window, document, or target', '--scope');
|
|
512
|
+
parsed.scope = scope;
|
|
513
|
+
i++;
|
|
514
|
+
}
|
|
515
|
+
else if (arg === '--event') { parsed.event = valueFor(arg, next); i++; }
|
|
516
|
+
else if (arg === '--steps') { parsed.steps = valueFor(arg, next); i++; }
|
|
517
|
+
else if (arg === '--iterations') { parsed.iterations = integer(valueFor(arg, next), '--iterations', 1, 100, true); i++; }
|
|
518
|
+
else if (arg === '-h' || arg === '--help') parsed.help = true;
|
|
299
519
|
else if (arg.startsWith('--')) throw invalidInput(`Unknown flag: ${arg}.`, 'unknown_flag');
|
|
300
520
|
else positional.push(arg);
|
|
301
521
|
}
|
|
@@ -314,7 +534,7 @@ export function parseCliSyntax(argv: string[]): ParsedArgs {
|
|
|
314
534
|
*/
|
|
315
535
|
function needsEndpointResolution(parsed: ParsedArgs): boolean {
|
|
316
536
|
if (parsed.help || parsed.command === 'lib') return false;
|
|
317
|
-
const branchOnly = ['session', 'page', 'tab', 'measure', 'motion'].includes(parsed.command)
|
|
537
|
+
const branchOnly = ['session', 'page', 'tab', 'measure', 'motion', 'perf', 'heap'].includes(parsed.command)
|
|
318
538
|
&& (parsed.positional.length === 0 || (parsed.command === 'measure' && parsed.positional.length === 1 && parsed.positional[0] === 'map'));
|
|
319
539
|
if (branchOnly) return false;
|
|
320
540
|
if (parsed.command === 'session' && parsed.positional[0] !== 'start') return false;
|
|
@@ -353,7 +573,7 @@ export function resolveCliContext(parsed: ParsedArgs): ParsedArgs {
|
|
|
353
573
|
if (parsed.port === undefined && session.port !== undefined && session.port !== null) { parsed.port = session.port; parsed.portSource = 'session'; }
|
|
354
574
|
}
|
|
355
575
|
if (parsed.port === undefined && process.env.CDP_PORT !== undefined) {
|
|
356
|
-
parsed.port =
|
|
576
|
+
parsed.port = schemaInteger(leafCommand(parsed.command, parsed.positional), process.env.CDP_PORT, 'CDP_PORT', 1, 65535);
|
|
357
577
|
parsed.portSource = 'env';
|
|
358
578
|
}
|
|
359
579
|
// Preserve historical stale-index cleanup once endpoint validation has
|
package/src/cdp/bridge/server.ts
CHANGED
|
@@ -218,17 +218,18 @@ export async function runBridgeServer(socketPath: string, port?: number): Promis
|
|
|
218
218
|
export function prepareSocketPath(socketPath: string): void {
|
|
219
219
|
fs.mkdirSync(path.dirname(socketPath), { recursive: true });
|
|
220
220
|
try {
|
|
221
|
+
if (!fs.lstatSync(socketPath).isSocket()) throw new Error(`socket path is not a Unix socket: ${socketPath}`);
|
|
221
222
|
fs.unlinkSync(socketPath);
|
|
222
|
-
} catch {
|
|
223
|
-
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
/**
|
|
228
229
|
* Binds a Unix-domain `net.Server` at `socketPath` that frames each
|
|
229
230
|
* connection's input as newline-delimited JSON, invoking `handleLine` once
|
|
230
|
-
* per complete line. This is the wire framing
|
|
231
|
-
* browser-level bridge
|
|
231
|
+
* per complete line. This is the wire framing both bridge modes (plain
|
|
232
|
+
* browser-level bridge and collector host) share — "one request per
|
|
232
233
|
* connection, one response, then the socket closes" is a convention the
|
|
233
234
|
* caller's `handleLine` implements by writing exactly one response per
|
|
234
235
|
* line and letting the client end the connection; the server itself is
|
|
@@ -269,7 +270,7 @@ export function closeNdjsonSocket(server: net.Server, socketPath: string): void
|
|
|
269
270
|
// Already closed.
|
|
270
271
|
}
|
|
271
272
|
try {
|
|
272
|
-
fs.unlinkSync(socketPath);
|
|
273
|
+
if (fs.lstatSync(socketPath).isSocket()) fs.unlinkSync(socketPath);
|
|
273
274
|
} catch {
|
|
274
275
|
// Already gone.
|
|
275
276
|
}
|