@cydm/pie 2.1.0 → 2.1.1
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 +6 -3
- package/dist/builtin/extensions/ask-user/index.js +1 -1
- package/dist/builtin/extensions/computer-use/index.js +3993 -107
- package/dist/builtin/extensions/plan-mode/index.js +1 -1
- package/dist/builtin/extensions/subagent/index.js +2 -2
- package/dist/builtin/extensions/todo/index.js +1 -1
- package/dist/builtin/skills/chrome-devtools-axi/LICENSE +21 -0
- package/dist/builtin/skills/chrome-devtools-axi/SKILL.md +20 -1
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-bridge.js +20034 -0
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-cli.js +8 -2
- package/dist/builtin/skills/chrome-devtools-axi/chrome-devtools-axi-runtime.js +26 -0
- package/dist/builtin/skills/chrome-devtools-axi/chunk-42SGITKJ.js +11 -0
- package/dist/builtin/skills/chrome-devtools-axi/chunk-OJ6N23UZ.js +47 -0
- package/dist/builtin/skills/chrome-devtools-axi/chunk-UPATXALW.js +215 -0
- package/dist/builtin/skills/chrome-devtools-axi/cli-F2VCSVL4.js +4424 -0
- package/dist/builtin/skills/skill-creator/SKILL.md +5 -2
- package/dist/builtin/skills/skill-creator/eval-viewer/generate_review.mjs +7 -20
- package/dist/builtin/skills/skill-creator/eval-viewer/viewer.html +15 -15
- package/dist/builtin/skills/skill-creator/scripts/aggregate_benchmark.mjs +20 -19
- package/dist/builtin/skills/skill-creator/scripts/generate_report.mjs +3 -3
- package/dist/builtin/skills/skill-creator/scripts/improve_description.mjs +12 -8
- package/dist/builtin/skills/skill-creator/scripts/package_skill.mjs +1 -0
- package/dist/builtin/skills/skill-creator/scripts/pie_runner.mjs +77 -31
- package/dist/builtin/skills/skill-creator/scripts/run_eval.mjs +38 -11
- package/dist/builtin/skills/skill-creator/scripts/run_loop.mjs +12 -6
- package/dist/builtin/skills/skill-creator/scripts/skill_metadata.mjs +12 -1
- package/dist/builtin/skills/tui-use/LICENSE +21 -0
- package/dist/builtin/skills/tui-use/README.md +13 -3
- package/dist/builtin/skills/tui-use/SKILL.md +87 -39
- package/dist/builtin/skills/tui-use/tui-use-cli.cjs +10608 -0
- package/dist/builtin/skills/tui-use/tui-use-cli.js +39 -0
- package/dist/builtin/skills/tui-use/tui-use-daemon.cjs +7916 -0
- package/dist/chunks/{chunk-PMPD56N2.js → chunk-LU7RKALH.js} +82 -24
- package/dist/chunks/{chunk-G2POWIA5.js → chunk-TJ2F4FL5.js} +64 -16
- package/dist/cli.js +7503 -87
- package/package.json +26 -18
- package/scripts/repair-node-pty.mjs +45 -0
|
@@ -0,0 +1,4424 @@
|
|
|
1
|
+
import { createRequire as __axiRequire } from "node:module"; const require = __axiRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
VERSION
|
|
4
|
+
} from "./chunk-42SGITKJ.js";
|
|
5
|
+
import {
|
|
6
|
+
BRIDGE_PORT_IN_USE_EXIT_CODE,
|
|
7
|
+
PAGE_IDENTITY_CHANGED_ERROR,
|
|
8
|
+
clearSelectedPageId,
|
|
9
|
+
getSelectedPageId,
|
|
10
|
+
overlaySessionSelected,
|
|
11
|
+
rememberToolRouting,
|
|
12
|
+
resolveBridgeScript,
|
|
13
|
+
resolveSessionName,
|
|
14
|
+
resolveSessionPidFile,
|
|
15
|
+
resolveSessionPort,
|
|
16
|
+
resolveSessionStateDir
|
|
17
|
+
} from "./chunk-UPATXALW.js";
|
|
18
|
+
import "./chunk-OJ6N23UZ.js";
|
|
19
|
+
|
|
20
|
+
// ../../node_modules/chrome-devtools-axi/node_modules/@toon-format/toon/dist/index.mjs
|
|
21
|
+
var NULL_LITERAL = "null";
|
|
22
|
+
var DELIMITERS = {
|
|
23
|
+
comma: ",",
|
|
24
|
+
tab: " ",
|
|
25
|
+
pipe: "|"
|
|
26
|
+
};
|
|
27
|
+
var DEFAULT_DELIMITER = DELIMITERS.comma;
|
|
28
|
+
function escapeString(value) {
|
|
29
|
+
return value.replace(/\\/g, `\\\\`).replace(/"/g, `\\"`).replace(/\n/g, `\\n`).replace(/\r/g, `\\r`).replace(/\t/g, `\\t`).replace(/[\u0000-\u001F]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
30
|
+
}
|
|
31
|
+
function isBooleanOrNullLiteral(token) {
|
|
32
|
+
return token === "true" || token === "false" || token === "null";
|
|
33
|
+
}
|
|
34
|
+
function setOwnProperty(target, key, value) {
|
|
35
|
+
if (key === "__proto__") {
|
|
36
|
+
Object.defineProperty(target, key, {
|
|
37
|
+
value,
|
|
38
|
+
enumerable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
configurable: true
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
target[key] = value;
|
|
45
|
+
}
|
|
46
|
+
function normalizeValue(value) {
|
|
47
|
+
if (value === null) return null;
|
|
48
|
+
if (typeof value === "object" && value !== null && "toJSON" in value && typeof value.toJSON === "function") {
|
|
49
|
+
const next = value.toJSON();
|
|
50
|
+
if (next !== value) return normalizeValue(next);
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
53
|
+
if (typeof value === "number") {
|
|
54
|
+
if (Object.is(value, -0)) return 0;
|
|
55
|
+
if (!Number.isFinite(value)) return null;
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "bigint") {
|
|
59
|
+
if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER) return Number(value);
|
|
60
|
+
return value.toString();
|
|
61
|
+
}
|
|
62
|
+
if (value instanceof Date) return value.toISOString();
|
|
63
|
+
if (Array.isArray(value)) return value.map(normalizeValue);
|
|
64
|
+
if (value instanceof Set) return Array.from(value).map(normalizeValue);
|
|
65
|
+
if (value instanceof Map) return Object.fromEntries(Array.from(value, ([k, v]) => [String(k), normalizeValue(v)]));
|
|
66
|
+
if (isPlainObject(value)) {
|
|
67
|
+
const encodedValues = {};
|
|
68
|
+
for (const key in value) if (Object.hasOwn(value, key)) setOwnProperty(encodedValues, key, normalizeValue(value[key]));
|
|
69
|
+
return encodedValues;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function isJsonPrimitive(value) {
|
|
74
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
75
|
+
}
|
|
76
|
+
function isJsonArray(value) {
|
|
77
|
+
return Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
function isJsonObject(value) {
|
|
80
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
81
|
+
}
|
|
82
|
+
function isEmptyObject(value) {
|
|
83
|
+
return Object.keys(value).length === 0;
|
|
84
|
+
}
|
|
85
|
+
function isPlainObject(value) {
|
|
86
|
+
if (value === null || typeof value !== "object") return false;
|
|
87
|
+
const prototype = Object.getPrototypeOf(value);
|
|
88
|
+
return prototype === null || prototype === Object.prototype;
|
|
89
|
+
}
|
|
90
|
+
function isArrayOfPrimitives(value) {
|
|
91
|
+
return value.length === 0 || value.every((item) => isJsonPrimitive(item));
|
|
92
|
+
}
|
|
93
|
+
function isArrayOfArrays(value) {
|
|
94
|
+
return value.length === 0 || value.every((item) => isJsonArray(item));
|
|
95
|
+
}
|
|
96
|
+
function isArrayOfObjects(value) {
|
|
97
|
+
return value.length === 0 || value.every((item) => isJsonObject(item));
|
|
98
|
+
}
|
|
99
|
+
var NUMERIC_LIKE_PATTERN = /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i;
|
|
100
|
+
var LEADING_ZERO_PATTERN = /^0\d+$/;
|
|
101
|
+
function isValidUnquotedKey(key) {
|
|
102
|
+
return /^[A-Z_][\w.]*$/i.test(key);
|
|
103
|
+
}
|
|
104
|
+
function isIdentifierSegment(key) {
|
|
105
|
+
return /^[A-Z_]\w*$/i.test(key);
|
|
106
|
+
}
|
|
107
|
+
function isSafeUnquoted(value, delimiter2 = DEFAULT_DELIMITER) {
|
|
108
|
+
if (!value) return false;
|
|
109
|
+
if (value !== value.trim()) return false;
|
|
110
|
+
if (isBooleanOrNullLiteral(value) || isNumericLike(value)) return false;
|
|
111
|
+
if (value.includes(":")) return false;
|
|
112
|
+
if (value.includes('"') || value.includes("\\")) return false;
|
|
113
|
+
if (/[[\]{}]/.test(value)) return false;
|
|
114
|
+
if (/[\u0000-\u001F]/.test(value)) return false;
|
|
115
|
+
if (value.includes(delimiter2)) return false;
|
|
116
|
+
if (value.startsWith("-")) return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
function isNumericLike(value) {
|
|
120
|
+
return NUMERIC_LIKE_PATTERN.test(value) || LEADING_ZERO_PATTERN.test(value);
|
|
121
|
+
}
|
|
122
|
+
function tryFoldKeyChain(key, value, siblings, options, rootLiteralKeys, pathPrefix, flattenDepth) {
|
|
123
|
+
if (options.keyFolding !== "safe") return;
|
|
124
|
+
if (!isJsonObject(value)) return;
|
|
125
|
+
const { segments, tail, leafValue } = collectSingleKeyChain(key, value, flattenDepth ?? options.flattenDepth);
|
|
126
|
+
if (segments.length < 2) return;
|
|
127
|
+
if (!segments.every((seg) => isIdentifierSegment(seg))) return;
|
|
128
|
+
const foldedKey = buildFoldedKey(segments);
|
|
129
|
+
const absolutePath = pathPrefix ? `${pathPrefix}.${foldedKey}` : foldedKey;
|
|
130
|
+
if (siblings.includes(foldedKey)) return;
|
|
131
|
+
if (rootLiteralKeys && rootLiteralKeys.has(absolutePath)) return;
|
|
132
|
+
return {
|
|
133
|
+
foldedKey,
|
|
134
|
+
remainder: tail,
|
|
135
|
+
leafValue,
|
|
136
|
+
segmentCount: segments.length
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function collectSingleKeyChain(startKey, startValue, maxDepth) {
|
|
140
|
+
const segments = [startKey];
|
|
141
|
+
let currentValue = startValue;
|
|
142
|
+
while (segments.length < maxDepth) {
|
|
143
|
+
if (!isJsonObject(currentValue)) break;
|
|
144
|
+
const keys = Object.keys(currentValue);
|
|
145
|
+
if (keys.length !== 1) break;
|
|
146
|
+
const nextKey = keys[0];
|
|
147
|
+
const nextValue = currentValue[nextKey];
|
|
148
|
+
segments.push(nextKey);
|
|
149
|
+
currentValue = nextValue;
|
|
150
|
+
}
|
|
151
|
+
if (!isJsonObject(currentValue) || isEmptyObject(currentValue)) return {
|
|
152
|
+
segments,
|
|
153
|
+
tail: void 0,
|
|
154
|
+
leafValue: currentValue
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
segments,
|
|
158
|
+
tail: currentValue,
|
|
159
|
+
leafValue: currentValue
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function buildFoldedKey(segments) {
|
|
163
|
+
return segments.join(".");
|
|
164
|
+
}
|
|
165
|
+
function encodePrimitive(value, delimiter2) {
|
|
166
|
+
if (value === null) return NULL_LITERAL;
|
|
167
|
+
if (typeof value === "boolean") return String(value);
|
|
168
|
+
if (typeof value === "number") return String(value);
|
|
169
|
+
return encodeStringLiteral(value, delimiter2);
|
|
170
|
+
}
|
|
171
|
+
function encodeStringLiteral(value, delimiter2 = DEFAULT_DELIMITER) {
|
|
172
|
+
if (isSafeUnquoted(value, delimiter2)) return value;
|
|
173
|
+
return `"${escapeString(value)}"`;
|
|
174
|
+
}
|
|
175
|
+
function encodeKey(key) {
|
|
176
|
+
if (isValidUnquotedKey(key)) return key;
|
|
177
|
+
return `"${escapeString(key)}"`;
|
|
178
|
+
}
|
|
179
|
+
function encodeAndJoinPrimitives(values, delimiter2 = DEFAULT_DELIMITER) {
|
|
180
|
+
return values.map((v) => encodePrimitive(v, delimiter2)).join(delimiter2);
|
|
181
|
+
}
|
|
182
|
+
function formatHeader(length, options) {
|
|
183
|
+
const key = options?.key;
|
|
184
|
+
const fields = options?.fields;
|
|
185
|
+
const delimiter2 = options?.delimiter ?? ",";
|
|
186
|
+
let header = "";
|
|
187
|
+
if (key != null) header += encodeKey(key);
|
|
188
|
+
header += `[${length}${delimiter2 !== DEFAULT_DELIMITER ? delimiter2 : ""}]`;
|
|
189
|
+
if (fields) {
|
|
190
|
+
const quotedFields = fields.map((f) => encodeKey(f));
|
|
191
|
+
header += `{${quotedFields.join(delimiter2)}}`;
|
|
192
|
+
}
|
|
193
|
+
header += ":";
|
|
194
|
+
return header;
|
|
195
|
+
}
|
|
196
|
+
function* encodeJsonValue(value, options, depth) {
|
|
197
|
+
if (isJsonPrimitive(value)) {
|
|
198
|
+
const encodedPrimitive = encodePrimitive(value, options.delimiter);
|
|
199
|
+
if (encodedPrimitive !== "") yield encodedPrimitive;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (isJsonArray(value)) yield* encodeArrayLines(void 0, value, depth, options);
|
|
203
|
+
else if (isJsonObject(value)) yield* encodeObjectLines(value, depth, options);
|
|
204
|
+
}
|
|
205
|
+
function* encodeObjectLines(value, depth, options, rootLiteralKeys, pathPrefix, remainingDepth) {
|
|
206
|
+
const keys = Object.keys(value);
|
|
207
|
+
if (depth === 0 && !rootLiteralKeys) rootLiteralKeys = new Set(keys.filter((k) => k.includes(".")));
|
|
208
|
+
const effectiveFlattenDepth = remainingDepth ?? options.flattenDepth;
|
|
209
|
+
for (const [key, val] of Object.entries(value)) yield* encodeKeyValuePairLines(key, val, depth, options, keys, rootLiteralKeys, pathPrefix, effectiveFlattenDepth);
|
|
210
|
+
}
|
|
211
|
+
function* encodeKeyValuePairLines(key, value, depth, options, siblings, rootLiteralKeys, pathPrefix, flattenDepth) {
|
|
212
|
+
const currentPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
213
|
+
const effectiveFlattenDepth = flattenDepth ?? options.flattenDepth;
|
|
214
|
+
if (options.keyFolding === "safe" && siblings) {
|
|
215
|
+
const foldResult = tryFoldKeyChain(key, value, siblings, options, rootLiteralKeys, pathPrefix, effectiveFlattenDepth);
|
|
216
|
+
if (foldResult) {
|
|
217
|
+
const { foldedKey, remainder, leafValue, segmentCount } = foldResult;
|
|
218
|
+
const encodedFoldedKey = encodeKey(foldedKey);
|
|
219
|
+
if (remainder === void 0) {
|
|
220
|
+
if (isJsonPrimitive(leafValue)) {
|
|
221
|
+
yield indentedLine(depth, `${encodedFoldedKey}: ${encodePrimitive(leafValue, options.delimiter)}`, options.indent);
|
|
222
|
+
return;
|
|
223
|
+
} else if (isJsonArray(leafValue)) {
|
|
224
|
+
yield* encodeArrayLines(foldedKey, leafValue, depth, options);
|
|
225
|
+
return;
|
|
226
|
+
} else if (isJsonObject(leafValue) && isEmptyObject(leafValue)) {
|
|
227
|
+
yield indentedLine(depth, `${encodedFoldedKey}:`, options.indent);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (isJsonObject(remainder)) {
|
|
232
|
+
yield indentedLine(depth, `${encodedFoldedKey}:`, options.indent);
|
|
233
|
+
const remainingDepth = effectiveFlattenDepth - segmentCount;
|
|
234
|
+
const foldedPath = pathPrefix ? `${pathPrefix}.${foldedKey}` : foldedKey;
|
|
235
|
+
yield* encodeObjectLines(remainder, depth + 1, options, rootLiteralKeys, foldedPath, remainingDepth);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const encodedKey = encodeKey(key);
|
|
241
|
+
if (isJsonPrimitive(value)) yield indentedLine(depth, `${encodedKey}: ${encodePrimitive(value, options.delimiter)}`, options.indent);
|
|
242
|
+
else if (isJsonArray(value)) yield* encodeArrayLines(key, value, depth, options);
|
|
243
|
+
else if (isJsonObject(value)) {
|
|
244
|
+
yield indentedLine(depth, `${encodedKey}:`, options.indent);
|
|
245
|
+
if (!isEmptyObject(value)) yield* encodeObjectLines(value, depth + 1, options, rootLiteralKeys, currentPath, effectiveFlattenDepth);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function* encodeArrayLines(key, value, depth, options) {
|
|
249
|
+
if (value.length === 0) {
|
|
250
|
+
yield indentedLine(depth, key != null ? `${encodeKey(key)}: []` : "[]", options.indent);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (isArrayOfPrimitives(value)) {
|
|
254
|
+
yield indentedLine(depth, encodeInlineArrayLine(value, options.delimiter, key), options.indent);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (isArrayOfArrays(value)) {
|
|
258
|
+
if (value.every((arr) => isArrayOfPrimitives(arr))) {
|
|
259
|
+
yield* encodeArrayOfArraysAsListItemsLines(key, value, depth, options);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (isArrayOfObjects(value)) {
|
|
264
|
+
const header = extractTabularHeader(value);
|
|
265
|
+
if (header) yield* encodeArrayOfObjectsAsTabularLines(key, value, header, depth, options);
|
|
266
|
+
else yield* encodeMixedArrayAsListItemsLines(key, value, depth, options);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
yield* encodeMixedArrayAsListItemsLines(key, value, depth, options);
|
|
270
|
+
}
|
|
271
|
+
function* encodeArrayOfArraysAsListItemsLines(prefix, values, depth, options) {
|
|
272
|
+
yield indentedLine(depth, formatHeader(values.length, {
|
|
273
|
+
key: prefix,
|
|
274
|
+
delimiter: options.delimiter
|
|
275
|
+
}), options.indent);
|
|
276
|
+
for (const arr of values) if (isArrayOfPrimitives(arr)) {
|
|
277
|
+
const arrayLine = encodeInlineArrayLine(arr, options.delimiter);
|
|
278
|
+
yield indentedListItem(depth + 1, arrayLine, options.indent);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function encodeInlineArrayLine(values, delimiter2, prefix) {
|
|
282
|
+
const header = formatHeader(values.length, {
|
|
283
|
+
key: prefix,
|
|
284
|
+
delimiter: delimiter2
|
|
285
|
+
});
|
|
286
|
+
const joinedValue = encodeAndJoinPrimitives(values, delimiter2);
|
|
287
|
+
if (values.length === 0) return header;
|
|
288
|
+
return `${header} ${joinedValue}`;
|
|
289
|
+
}
|
|
290
|
+
function* encodeArrayOfObjectsAsTabularLines(prefix, rows, header, depth, options) {
|
|
291
|
+
yield indentedLine(depth, formatHeader(rows.length, {
|
|
292
|
+
key: prefix,
|
|
293
|
+
fields: header,
|
|
294
|
+
delimiter: options.delimiter
|
|
295
|
+
}), options.indent);
|
|
296
|
+
yield* writeTabularRowsLines(rows, header, depth + 1, options);
|
|
297
|
+
}
|
|
298
|
+
function extractTabularHeader(rows) {
|
|
299
|
+
if (rows.length === 0) return;
|
|
300
|
+
const firstRow = rows[0];
|
|
301
|
+
const firstKeys = Object.keys(firstRow);
|
|
302
|
+
if (firstKeys.length === 0) return;
|
|
303
|
+
if (isTabularArray(rows, firstKeys)) return firstKeys;
|
|
304
|
+
}
|
|
305
|
+
function isTabularArray(rows, header) {
|
|
306
|
+
for (const row of rows) {
|
|
307
|
+
if (Object.keys(row).length !== header.length) return false;
|
|
308
|
+
for (const key of header) {
|
|
309
|
+
if (!Object.hasOwn(row, key)) return false;
|
|
310
|
+
if (!isJsonPrimitive(row[key])) return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
function* writeTabularRowsLines(rows, header, depth, options) {
|
|
316
|
+
for (const row of rows) yield indentedLine(depth, encodeAndJoinPrimitives(header.map((key) => row[key]), options.delimiter), options.indent);
|
|
317
|
+
}
|
|
318
|
+
function* encodeMixedArrayAsListItemsLines(prefix, items, depth, options) {
|
|
319
|
+
yield indentedLine(depth, formatHeader(items.length, {
|
|
320
|
+
key: prefix,
|
|
321
|
+
delimiter: options.delimiter
|
|
322
|
+
}), options.indent);
|
|
323
|
+
for (const item of items) yield* encodeListItemValueLines(item, depth + 1, options);
|
|
324
|
+
}
|
|
325
|
+
function* encodeObjectAsListItemLines(obj, depth, options) {
|
|
326
|
+
if (isEmptyObject(obj)) {
|
|
327
|
+
yield indentedLine(depth, "-", options.indent);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const entries = Object.entries(obj);
|
|
331
|
+
const [firstKey, firstValue] = entries[0];
|
|
332
|
+
const restEntries = entries.slice(1);
|
|
333
|
+
if (isJsonArray(firstValue) && isArrayOfObjects(firstValue)) {
|
|
334
|
+
const header = extractTabularHeader(firstValue);
|
|
335
|
+
if (header) {
|
|
336
|
+
yield indentedListItem(depth, formatHeader(firstValue.length, {
|
|
337
|
+
key: firstKey,
|
|
338
|
+
fields: header,
|
|
339
|
+
delimiter: options.delimiter
|
|
340
|
+
}), options.indent);
|
|
341
|
+
yield* writeTabularRowsLines(firstValue, header, depth + 2, options);
|
|
342
|
+
if (restEntries.length > 0) yield* encodeObjectLines(Object.fromEntries(restEntries), depth + 1, options);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const encodedKey = encodeKey(firstKey);
|
|
347
|
+
if (isJsonPrimitive(firstValue)) yield indentedListItem(depth, `${encodedKey}: ${encodePrimitive(firstValue, options.delimiter)}`, options.indent);
|
|
348
|
+
else if (isJsonArray(firstValue)) if (firstValue.length === 0) yield indentedListItem(depth, `${encodedKey}: []`, options.indent);
|
|
349
|
+
else if (isArrayOfPrimitives(firstValue)) yield indentedListItem(depth, `${encodedKey}${encodeInlineArrayLine(firstValue, options.delimiter)}`, options.indent);
|
|
350
|
+
else {
|
|
351
|
+
yield indentedListItem(depth, `${encodedKey}${formatHeader(firstValue.length, { delimiter: options.delimiter })}`, options.indent);
|
|
352
|
+
for (const item of firstValue) yield* encodeListItemValueLines(item, depth + 2, options);
|
|
353
|
+
}
|
|
354
|
+
else if (isJsonObject(firstValue)) {
|
|
355
|
+
yield indentedListItem(depth, `${encodedKey}:`, options.indent);
|
|
356
|
+
if (!isEmptyObject(firstValue)) yield* encodeObjectLines(firstValue, depth + 2, options);
|
|
357
|
+
}
|
|
358
|
+
if (restEntries.length > 0) yield* encodeObjectLines(Object.fromEntries(restEntries), depth + 1, options);
|
|
359
|
+
}
|
|
360
|
+
function* encodeListItemValueLines(value, depth, options) {
|
|
361
|
+
if (isJsonPrimitive(value)) yield indentedListItem(depth, encodePrimitive(value, options.delimiter), options.indent);
|
|
362
|
+
else if (isJsonArray(value)) if (isArrayOfPrimitives(value)) yield indentedListItem(depth, encodeInlineArrayLine(value, options.delimiter), options.indent);
|
|
363
|
+
else {
|
|
364
|
+
yield indentedListItem(depth, formatHeader(value.length, { delimiter: options.delimiter }), options.indent);
|
|
365
|
+
for (const item of value) yield* encodeListItemValueLines(item, depth + 1, options);
|
|
366
|
+
}
|
|
367
|
+
else if (isJsonObject(value)) yield* encodeObjectAsListItemLines(value, depth, options);
|
|
368
|
+
}
|
|
369
|
+
function indentedLine(depth, content, indentSize) {
|
|
370
|
+
return " ".repeat(indentSize * depth) + content;
|
|
371
|
+
}
|
|
372
|
+
function indentedListItem(depth, content, indentSize) {
|
|
373
|
+
return indentedLine(depth, "- " + content, indentSize);
|
|
374
|
+
}
|
|
375
|
+
function applyReplacer(root, replacer) {
|
|
376
|
+
const replacedRoot = replacer("", root, []);
|
|
377
|
+
if (replacedRoot === void 0) return transformChildren(root, replacer, []);
|
|
378
|
+
return transformChildren(normalizeValue(replacedRoot), replacer, []);
|
|
379
|
+
}
|
|
380
|
+
function transformChildren(value, replacer, path) {
|
|
381
|
+
if (isJsonObject(value)) return transformObject(value, replacer, path);
|
|
382
|
+
if (isJsonArray(value)) return transformArray(value, replacer, path);
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
function transformObject(obj, replacer, path) {
|
|
386
|
+
const result = {};
|
|
387
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
388
|
+
const childPath = [...path, key];
|
|
389
|
+
const replacedValue = replacer(key, value, childPath);
|
|
390
|
+
if (replacedValue === void 0) continue;
|
|
391
|
+
setOwnProperty(result, key, transformChildren(normalizeValue(replacedValue), replacer, childPath));
|
|
392
|
+
}
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
function transformArray(arr, replacer, path) {
|
|
396
|
+
const result = [];
|
|
397
|
+
for (let i = 0; i < arr.length; i++) {
|
|
398
|
+
const value = arr[i];
|
|
399
|
+
const childPath = [...path, i];
|
|
400
|
+
const replacedValue = replacer(String(i), value, childPath);
|
|
401
|
+
if (replacedValue === void 0) continue;
|
|
402
|
+
const normalizedValue = normalizeValue(replacedValue);
|
|
403
|
+
result.push(transformChildren(normalizedValue, replacer, childPath));
|
|
404
|
+
}
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
function encode(input, options) {
|
|
408
|
+
return Array.from(encodeLines(input, options)).join("\n");
|
|
409
|
+
}
|
|
410
|
+
function encodeLines(input, options) {
|
|
411
|
+
const normalizedValue = normalizeValue(input);
|
|
412
|
+
const resolvedOptions = resolveOptions(options);
|
|
413
|
+
return encodeJsonValue(resolvedOptions.replacer ? applyReplacer(normalizedValue, resolvedOptions.replacer) : normalizedValue, resolvedOptions, 0);
|
|
414
|
+
}
|
|
415
|
+
function resolveOptions(options) {
|
|
416
|
+
return {
|
|
417
|
+
indent: options?.indent ?? 2,
|
|
418
|
+
delimiter: options?.delimiter ?? DEFAULT_DELIMITER,
|
|
419
|
+
keyFolding: options?.keyFolding ?? "off",
|
|
420
|
+
flattenDepth: options?.flattenDepth ?? Number.POSITIVE_INFINITY,
|
|
421
|
+
replacer: options?.replacer
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ../../node_modules/axi-sdk-js/dist/cli.js
|
|
426
|
+
import { basename as basename2 } from "node:path";
|
|
427
|
+
|
|
428
|
+
// ../../node_modules/axi-sdk-js/dist/errors.js
|
|
429
|
+
var AxiError = class extends Error {
|
|
430
|
+
code;
|
|
431
|
+
suggestions;
|
|
432
|
+
constructor(message, code, suggestions = []) {
|
|
433
|
+
super(message);
|
|
434
|
+
this.code = code;
|
|
435
|
+
this.suggestions = suggestions;
|
|
436
|
+
this.name = "AxiError";
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
function exitCodeForError(error) {
|
|
440
|
+
if (error instanceof AxiError && error.code === "VALIDATION_ERROR") {
|
|
441
|
+
return 2;
|
|
442
|
+
}
|
|
443
|
+
return 1;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ../../node_modules/axi-sdk-js/dist/output.js
|
|
447
|
+
import { homedir } from "node:os";
|
|
448
|
+
function collapseHomeDirectory(path, homeDir = homedir()) {
|
|
449
|
+
if (!path.startsWith(homeDir)) {
|
|
450
|
+
return path;
|
|
451
|
+
}
|
|
452
|
+
return `~${path.slice(homeDir.length)}`;
|
|
453
|
+
}
|
|
454
|
+
function homeHeaderOutput(options) {
|
|
455
|
+
return {
|
|
456
|
+
bin: collapseHomeDirectory(options.execPath ?? process.argv[1] ?? "", options.homeDir),
|
|
457
|
+
description: options.description
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function errorOutput(message, code, suggestions = []) {
|
|
461
|
+
const output = {
|
|
462
|
+
error: message,
|
|
463
|
+
code
|
|
464
|
+
};
|
|
465
|
+
if (suggestions.length > 0) {
|
|
466
|
+
output.help = suggestions;
|
|
467
|
+
}
|
|
468
|
+
return output;
|
|
469
|
+
}
|
|
470
|
+
function renderOutput(output) {
|
|
471
|
+
if (typeof output === "string") {
|
|
472
|
+
return output;
|
|
473
|
+
}
|
|
474
|
+
return encode(output);
|
|
475
|
+
}
|
|
476
|
+
function renderError(message, code, suggestions = []) {
|
|
477
|
+
return renderOutput(errorOutput(message, code, suggestions));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ../../node_modules/axi-sdk-js/dist/update.js
|
|
481
|
+
import { spawn } from "node:child_process";
|
|
482
|
+
import { execFile } from "node:child_process";
|
|
483
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
484
|
+
import { basename, dirname, join } from "node:path";
|
|
485
|
+
import { promisify } from "node:util";
|
|
486
|
+
var execFileAsync = promisify(execFile);
|
|
487
|
+
var REGISTRY_BASE = "https://registry.npmjs.org";
|
|
488
|
+
var REGISTRY_FETCH_TIMEOUT_MS = 2e4;
|
|
489
|
+
function parseSemver(version) {
|
|
490
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-.]+))?(?:\+[0-9A-Za-z-.]+)?$/.exec(version.trim());
|
|
491
|
+
if (!match) {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
major: Number(match[1]),
|
|
496
|
+
minor: Number(match[2]),
|
|
497
|
+
patch: Number(match[3]),
|
|
498
|
+
prerelease: match[4] ? match[4].split(".") : []
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function comparePrerelease(a, b) {
|
|
502
|
+
if (a.length === 0 && b.length === 0)
|
|
503
|
+
return 0;
|
|
504
|
+
if (a.length === 0)
|
|
505
|
+
return 1;
|
|
506
|
+
if (b.length === 0)
|
|
507
|
+
return -1;
|
|
508
|
+
const length = Math.max(a.length, b.length);
|
|
509
|
+
for (let index = 0; index < length; index += 1) {
|
|
510
|
+
if (index >= a.length)
|
|
511
|
+
return -1;
|
|
512
|
+
if (index >= b.length)
|
|
513
|
+
return 1;
|
|
514
|
+
const left = a[index];
|
|
515
|
+
const right = b[index];
|
|
516
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
517
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
518
|
+
if (leftNumeric && rightNumeric) {
|
|
519
|
+
const delta = Number(left) - Number(right);
|
|
520
|
+
if (delta !== 0)
|
|
521
|
+
return delta < 0 ? -1 : 1;
|
|
522
|
+
} else if (leftNumeric) {
|
|
523
|
+
return -1;
|
|
524
|
+
} else if (rightNumeric) {
|
|
525
|
+
return 1;
|
|
526
|
+
} else if (left !== right) {
|
|
527
|
+
return left < right ? -1 : 1;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
function compareSemver(a, b) {
|
|
533
|
+
const parsedA = parseSemver(a);
|
|
534
|
+
const parsedB = parseSemver(b);
|
|
535
|
+
if (!parsedA || !parsedB) {
|
|
536
|
+
if (a === b)
|
|
537
|
+
return 0;
|
|
538
|
+
return a < b ? -1 : 1;
|
|
539
|
+
}
|
|
540
|
+
if (parsedA.major !== parsedB.major) {
|
|
541
|
+
return parsedA.major < parsedB.major ? -1 : 1;
|
|
542
|
+
}
|
|
543
|
+
if (parsedA.minor !== parsedB.minor) {
|
|
544
|
+
return parsedA.minor < parsedB.minor ? -1 : 1;
|
|
545
|
+
}
|
|
546
|
+
if (parsedA.patch !== parsedB.patch) {
|
|
547
|
+
return parsedA.patch < parsedB.patch ? -1 : 1;
|
|
548
|
+
}
|
|
549
|
+
return comparePrerelease(parsedA.prerelease, parsedB.prerelease);
|
|
550
|
+
}
|
|
551
|
+
function isUpdateAvailable(current, latest) {
|
|
552
|
+
return compareSemver(latest, current) > 0;
|
|
553
|
+
}
|
|
554
|
+
var nodeFs = {
|
|
555
|
+
existsSync,
|
|
556
|
+
readFileSync: (path, encoding) => readFileSync(path, encoding)
|
|
557
|
+
};
|
|
558
|
+
function readNearestPackageJson(startPath, fs = nodeFs) {
|
|
559
|
+
let dir = dirname(startPath);
|
|
560
|
+
let previous = "";
|
|
561
|
+
while (dir !== previous) {
|
|
562
|
+
const packageJsonPath = join(dir, "package.json");
|
|
563
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
564
|
+
try {
|
|
565
|
+
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
566
|
+
if (typeof parsed.name === "string" && parsed.name.length > 0) {
|
|
567
|
+
return {
|
|
568
|
+
packageName: parsed.name,
|
|
569
|
+
version: typeof parsed.version === "string" ? parsed.version : void 0,
|
|
570
|
+
packageJsonPath
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
previous = dir;
|
|
577
|
+
dir = dirname(dir);
|
|
578
|
+
}
|
|
579
|
+
return {};
|
|
580
|
+
}
|
|
581
|
+
function detectInstallMethod(options) {
|
|
582
|
+
const env = options.env ?? process.env;
|
|
583
|
+
const path = options.entry.replaceAll("\\", "/");
|
|
584
|
+
if (path.includes("/_npx/") || /\/dlx-[^/]+\//.test(path) || path.includes("/pnpm/dlx/") || path.includes("/bun/install/cache/")) {
|
|
585
|
+
return { kind: "npx" };
|
|
586
|
+
}
|
|
587
|
+
const homebrewFormula = homebrewFormulaFromPath(path, env);
|
|
588
|
+
if (homebrewFormula) {
|
|
589
|
+
return { kind: "homebrew", formula: homebrewFormula };
|
|
590
|
+
}
|
|
591
|
+
const pnpmHome = normalizePathRoot(env.PNPM_HOME);
|
|
592
|
+
if (isPathInsideRoot(path, pnpmHome) || isKnownPnpmGlobalStore(path, env)) {
|
|
593
|
+
return { kind: "pnpm-global" };
|
|
594
|
+
}
|
|
595
|
+
if (isKnownNpmGlobalInstall(path, env)) {
|
|
596
|
+
return { kind: "npm-global" };
|
|
597
|
+
}
|
|
598
|
+
return { kind: "unknown" };
|
|
599
|
+
}
|
|
600
|
+
function normalizePathRoot(path) {
|
|
601
|
+
const normalized = path?.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
602
|
+
return normalized && normalized.length > 0 ? normalized : void 0;
|
|
603
|
+
}
|
|
604
|
+
function isPathInsideRoot(path, root) {
|
|
605
|
+
return root !== void 0 && (path === root || path.startsWith(`${root}/`));
|
|
606
|
+
}
|
|
607
|
+
function homebrewFormulaFromPath(path, env) {
|
|
608
|
+
for (const root of homebrewCellarRoots(env)) {
|
|
609
|
+
if (!isPathInsideRoot(path, root)) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const relative = path.slice(root.length).replace(/^\/+/, "");
|
|
613
|
+
const formula = relative.split("/")[0];
|
|
614
|
+
if (formula) {
|
|
615
|
+
return formula;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
function homebrewCellarRoots(env) {
|
|
621
|
+
const roots = [];
|
|
622
|
+
const explicitCellar = normalizePathRoot(env.HOMEBREW_CELLAR);
|
|
623
|
+
if (explicitCellar) {
|
|
624
|
+
roots.push(explicitCellar);
|
|
625
|
+
}
|
|
626
|
+
const prefixes = [
|
|
627
|
+
env.HOMEBREW_PREFIX,
|
|
628
|
+
"/opt/homebrew",
|
|
629
|
+
"/usr/local",
|
|
630
|
+
"/home/linuxbrew/.linuxbrew"
|
|
631
|
+
];
|
|
632
|
+
for (const prefix of prefixes) {
|
|
633
|
+
const normalized = normalizePathRoot(prefix);
|
|
634
|
+
if (normalized) {
|
|
635
|
+
roots.push(`${normalized}/Cellar`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return [...new Set(roots)];
|
|
639
|
+
}
|
|
640
|
+
function isKnownPnpmGlobalStore(path, env) {
|
|
641
|
+
return pnpmGlobalStoreRoots(env).some((root) => {
|
|
642
|
+
if (!isPathInsideRoot(path, root)) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
const relative = path.slice(root.length).replace(/^\/+/, "");
|
|
646
|
+
return /^\d+\/\.pnpm\//.test(relative);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function pnpmGlobalStoreRoots(env) {
|
|
650
|
+
const roots = [];
|
|
651
|
+
const home = normalizePathRoot(env.HOME ?? env.USERPROFILE);
|
|
652
|
+
if (home) {
|
|
653
|
+
roots.push(`${home}/Library/pnpm/global`);
|
|
654
|
+
roots.push(`${home}/.local/share/pnpm/global`);
|
|
655
|
+
roots.push(`${home}/AppData/Local/pnpm/global`);
|
|
656
|
+
}
|
|
657
|
+
const localAppData = normalizePathRoot(env.LOCALAPPDATA);
|
|
658
|
+
if (localAppData) {
|
|
659
|
+
roots.push(`${localAppData}/pnpm/global`);
|
|
660
|
+
}
|
|
661
|
+
return [...new Set(roots)];
|
|
662
|
+
}
|
|
663
|
+
function isKnownNpmGlobalInstall(path, env) {
|
|
664
|
+
return npmGlobalNodeModulesRoots(env).some((root) => isPathInsideRoot(path, root)) || isKnownVersionManagerNpmGlobal(path, env);
|
|
665
|
+
}
|
|
666
|
+
function npmGlobalNodeModulesRoots(env) {
|
|
667
|
+
const roots = [
|
|
668
|
+
"/usr/local/lib/node_modules",
|
|
669
|
+
"/usr/lib/node_modules",
|
|
670
|
+
"/opt/homebrew/lib/node_modules",
|
|
671
|
+
"/opt/local/lib/node_modules"
|
|
672
|
+
];
|
|
673
|
+
const prefixes = [env.npm_config_prefix, env.NPM_CONFIG_PREFIX];
|
|
674
|
+
for (const prefix of prefixes) {
|
|
675
|
+
const normalized = normalizePathRoot(prefix);
|
|
676
|
+
if (normalized) {
|
|
677
|
+
roots.push(`${normalized}/lib/node_modules`, `${normalized}/node_modules`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const appData = normalizePathRoot(env.APPDATA);
|
|
681
|
+
if (appData) {
|
|
682
|
+
roots.push(`${appData}/npm/node_modules`);
|
|
683
|
+
}
|
|
684
|
+
const home = normalizePathRoot(env.HOME ?? env.USERPROFILE);
|
|
685
|
+
if (home) {
|
|
686
|
+
roots.push(`${home}/.npm-global/lib/node_modules`, `${home}/.npm-packages/lib/node_modules`);
|
|
687
|
+
}
|
|
688
|
+
return [...new Set(roots)];
|
|
689
|
+
}
|
|
690
|
+
function isKnownVersionManagerNpmGlobal(path, env) {
|
|
691
|
+
return versionManagerNodeRoots(env).some((root) => isPathInsideRoot(path, root) && path.includes("/lib/node_modules/"));
|
|
692
|
+
}
|
|
693
|
+
function versionManagerNodeRoots(env) {
|
|
694
|
+
const roots = [];
|
|
695
|
+
const home = normalizePathRoot(env.HOME ?? env.USERPROFILE);
|
|
696
|
+
if (home) {
|
|
697
|
+
roots.push(`${home}/.nvm/versions/node`, `${home}/.local/share/fnm/node-versions`, `${home}/.asdf/installs/nodejs`, `${home}/.nodenv/versions`, `${home}/.local/share/mise/installs/node`, `${home}/.volta/tools/image/node`);
|
|
698
|
+
}
|
|
699
|
+
const nvmDir = normalizePathRoot(env.NVM_DIR);
|
|
700
|
+
if (nvmDir) {
|
|
701
|
+
roots.push(`${nvmDir}/versions/node`);
|
|
702
|
+
}
|
|
703
|
+
const fnmDir = normalizePathRoot(env.FNM_DIR);
|
|
704
|
+
if (fnmDir) {
|
|
705
|
+
roots.push(`${fnmDir}/node-versions`);
|
|
706
|
+
}
|
|
707
|
+
return [...new Set(roots)];
|
|
708
|
+
}
|
|
709
|
+
function planUpgrade(method, packageName) {
|
|
710
|
+
switch (method.kind) {
|
|
711
|
+
case "npm-global":
|
|
712
|
+
return {
|
|
713
|
+
method: method.kind,
|
|
714
|
+
command: `npm install -g ${packageName}@latest`,
|
|
715
|
+
argv: ["npm", "install", "-g", `${packageName}@latest`]
|
|
716
|
+
};
|
|
717
|
+
case "pnpm-global":
|
|
718
|
+
return {
|
|
719
|
+
method: method.kind,
|
|
720
|
+
command: `pnpm add -g ${packageName}@latest`,
|
|
721
|
+
argv: ["pnpm", "add", "-g", `${packageName}@latest`]
|
|
722
|
+
};
|
|
723
|
+
case "homebrew":
|
|
724
|
+
if (method.formula) {
|
|
725
|
+
return {
|
|
726
|
+
method: method.kind,
|
|
727
|
+
command: `brew upgrade ${method.formula}`,
|
|
728
|
+
argv: ["brew", "upgrade", method.formula]
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
method: method.kind,
|
|
733
|
+
command: `brew upgrade ${packageName}`,
|
|
734
|
+
argv: null,
|
|
735
|
+
note: "Could not determine the Homebrew formula automatically"
|
|
736
|
+
};
|
|
737
|
+
case "npx":
|
|
738
|
+
return {
|
|
739
|
+
method: method.kind,
|
|
740
|
+
command: `npx -y ${packageName}@latest`,
|
|
741
|
+
argv: null,
|
|
742
|
+
note: "npx always runs the latest published version, so no install is needed"
|
|
743
|
+
};
|
|
744
|
+
case "unknown":
|
|
745
|
+
return {
|
|
746
|
+
method: method.kind,
|
|
747
|
+
command: `npm install -g ${packageName}@latest`,
|
|
748
|
+
argv: null,
|
|
749
|
+
note: "Could not determine how this tool was installed"
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function packageManagerExecutable(command, platform) {
|
|
754
|
+
if (platform === "win32" && (command === "npm" || command === "pnpm" || command === "npx")) {
|
|
755
|
+
return `${command}.cmd`;
|
|
756
|
+
}
|
|
757
|
+
return command;
|
|
758
|
+
}
|
|
759
|
+
function shouldUseWindowsPackageManagerShell(command, platform) {
|
|
760
|
+
return platform === "win32" && (command === "npm" || command === "pnpm" || command === "npx");
|
|
761
|
+
}
|
|
762
|
+
async function npmViewVersion(packageName, platform = process.platform) {
|
|
763
|
+
try {
|
|
764
|
+
const command = packageManagerExecutable("npm", platform);
|
|
765
|
+
const { stdout } = await execFileAsync(command, ["view", packageName, "version"], {
|
|
766
|
+
timeout: 2e4,
|
|
767
|
+
shell: shouldUseWindowsPackageManagerShell("npm", platform)
|
|
768
|
+
});
|
|
769
|
+
const version = stdout.trim();
|
|
770
|
+
return version.length > 0 ? version : null;
|
|
771
|
+
} catch {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function registryPath(packageName) {
|
|
776
|
+
return packageName.startsWith("@") ? packageName.replace("/", "%2f") : packageName;
|
|
777
|
+
}
|
|
778
|
+
function notPublishedError(packageName) {
|
|
779
|
+
return new AxiError(`${packageName} is not published to the npm registry`, "UPDATE_ERROR", [
|
|
780
|
+
"Confirm the package name is correct",
|
|
781
|
+
`Run \`npm view ${packageName} version\` to check manually`
|
|
782
|
+
]);
|
|
783
|
+
}
|
|
784
|
+
var RegistryNotFoundError = class extends Error {
|
|
785
|
+
};
|
|
786
|
+
async function withRegistryTimeout(timeoutMs, operation) {
|
|
787
|
+
const controller = new AbortController();
|
|
788
|
+
let timer;
|
|
789
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
790
|
+
timer = setTimeout(() => {
|
|
791
|
+
controller.abort();
|
|
792
|
+
reject(new Error(`Registry fetch timed out after ${timeoutMs}ms`));
|
|
793
|
+
}, timeoutMs);
|
|
794
|
+
timer.unref?.();
|
|
795
|
+
});
|
|
796
|
+
try {
|
|
797
|
+
return await Promise.race([operation(controller.signal), timeout]);
|
|
798
|
+
} finally {
|
|
799
|
+
if (timer) {
|
|
800
|
+
clearTimeout(timer);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
async function fetchRegistryVersion(fetchImpl, packageName, timeoutMs) {
|
|
805
|
+
return withRegistryTimeout(timeoutMs, async (signal) => {
|
|
806
|
+
const response = await fetchImpl(`${REGISTRY_BASE}/${registryPath(packageName)}/latest`, { headers: { accept: "application/json" }, signal });
|
|
807
|
+
if (response.ok) {
|
|
808
|
+
const data = await response.json();
|
|
809
|
+
if (typeof data.version === "string" && data.version.length > 0) {
|
|
810
|
+
return data.version;
|
|
811
|
+
}
|
|
812
|
+
} else if (response.status === 404) {
|
|
813
|
+
throw new RegistryNotFoundError();
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
async function fetchLatestVersion(packageName, options = {}) {
|
|
819
|
+
const fetchImpl = options.fetchImpl === void 0 ? globalThis.fetch : options.fetchImpl ?? void 0;
|
|
820
|
+
let registryNotFound = false;
|
|
821
|
+
if (typeof fetchImpl === "function") {
|
|
822
|
+
try {
|
|
823
|
+
const version = await fetchRegistryVersion(fetchImpl, packageName, options.fetchTimeoutMs ?? REGISTRY_FETCH_TIMEOUT_MS);
|
|
824
|
+
if (version) {
|
|
825
|
+
return version;
|
|
826
|
+
}
|
|
827
|
+
} catch (error) {
|
|
828
|
+
if (error instanceof RegistryNotFoundError) {
|
|
829
|
+
registryNotFound = true;
|
|
830
|
+
} else if (error instanceof AxiError) {
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const viewed = await (options.npmView ?? ((name) => npmViewVersion(name, options.platform)))(packageName);
|
|
836
|
+
if (viewed) {
|
|
837
|
+
return viewed;
|
|
838
|
+
}
|
|
839
|
+
if (registryNotFound) {
|
|
840
|
+
throw notPublishedError(packageName);
|
|
841
|
+
}
|
|
842
|
+
throw new AxiError(`Could not reach the npm registry to check for updates to ${packageName}`, "UPDATE_ERROR", [
|
|
843
|
+
"Check your network connection and try again",
|
|
844
|
+
`Run \`npm view ${packageName} version\` to check manually`
|
|
845
|
+
]);
|
|
846
|
+
}
|
|
847
|
+
async function defaultRunInstall(plan, stdout, context) {
|
|
848
|
+
const argv = plan.argv;
|
|
849
|
+
if (!argv || argv.length === 0) {
|
|
850
|
+
return { ok: false, message: "No runnable upgrade command" };
|
|
851
|
+
}
|
|
852
|
+
stdout.write(`running: ${plan.command}
|
|
853
|
+
`);
|
|
854
|
+
return new Promise((resolve3) => {
|
|
855
|
+
const [command, ...args] = argv;
|
|
856
|
+
const child = spawn(packageManagerExecutable(command, context.platform), args, {
|
|
857
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
858
|
+
shell: shouldUseWindowsPackageManagerShell(command, context.platform)
|
|
859
|
+
});
|
|
860
|
+
child.stdout?.on("data", (chunk) => {
|
|
861
|
+
process.stderr.write(chunk);
|
|
862
|
+
});
|
|
863
|
+
child.stderr?.on("data", (chunk) => {
|
|
864
|
+
process.stderr.write(chunk);
|
|
865
|
+
});
|
|
866
|
+
child.on("error", (error) => {
|
|
867
|
+
resolve3({ ok: false, message: error.message });
|
|
868
|
+
});
|
|
869
|
+
child.on("close", (code) => {
|
|
870
|
+
resolve3(code === 0 ? { ok: true } : { ok: false, message: `${plan.command} exited with code ${code}` });
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
function binNameFromArgv(invokedAs) {
|
|
875
|
+
return basename(invokedAs ?? "tool") || "tool";
|
|
876
|
+
}
|
|
877
|
+
function resolveEntry(invokedAs, realpath) {
|
|
878
|
+
if (!invokedAs) {
|
|
879
|
+
return void 0;
|
|
880
|
+
}
|
|
881
|
+
try {
|
|
882
|
+
return realpath(invokedAs);
|
|
883
|
+
} catch {
|
|
884
|
+
return invokedAs;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function resolveInstalledVersion(invokedAs, realpath, fs) {
|
|
888
|
+
const installedEntry = resolveEntry(invokedAs, realpath);
|
|
889
|
+
return installedEntry ? readNearestPackageJson(installedEntry, fs).version : void 0;
|
|
890
|
+
}
|
|
891
|
+
function homebrewUpgradeOutput(options) {
|
|
892
|
+
const update = {
|
|
893
|
+
package: options.packageName,
|
|
894
|
+
previous: options.current,
|
|
895
|
+
latest: options.latest
|
|
896
|
+
};
|
|
897
|
+
if (options.installedVersion) {
|
|
898
|
+
update.installed = options.installedVersion;
|
|
899
|
+
update.available = isUpdateAvailable(options.installedVersion, options.latest);
|
|
900
|
+
} else {
|
|
901
|
+
update.action = "upgrade-command-ran";
|
|
902
|
+
update.result = "installed version unknown";
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
update,
|
|
906
|
+
command: options.command
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
function parseUpdateArgs(args, binName) {
|
|
910
|
+
if (args.length === 0) {
|
|
911
|
+
return "install";
|
|
912
|
+
}
|
|
913
|
+
if (args.length === 1 && (args[0] === "--check" || args[0] === "--dry-run")) {
|
|
914
|
+
return "check";
|
|
915
|
+
}
|
|
916
|
+
const unknown = args.find((arg) => arg !== "--check" && arg !== "--dry-run");
|
|
917
|
+
throw new AxiError(unknown ? `Unknown update option: ${unknown}` : "Invalid update arguments", "VALIDATION_ERROR", [
|
|
918
|
+
`Run \`${binName} update --help\``,
|
|
919
|
+
`Use \`${binName} update --check\` to check without installing`
|
|
920
|
+
]);
|
|
921
|
+
}
|
|
922
|
+
async function runUpdate(options) {
|
|
923
|
+
const invokedAs = options.invokedAs ?? process.argv[1];
|
|
924
|
+
const binName = binNameFromArgv(invokedAs);
|
|
925
|
+
const mode = parseUpdateArgs(options.args, binName);
|
|
926
|
+
const platform = options.platform ?? process.platform;
|
|
927
|
+
const realpath = options.realpath ?? ((path) => realpathSync(path));
|
|
928
|
+
const entry = resolveEntry(invokedAs, realpath);
|
|
929
|
+
const fs = options.fs ?? nodeFs;
|
|
930
|
+
const fromPackageJson = entry ? readNearestPackageJson(entry, fs) : {};
|
|
931
|
+
const packageName = options.packageName ?? fromPackageJson.packageName;
|
|
932
|
+
const current = options.version ?? fromPackageJson.version;
|
|
933
|
+
if (!packageName) {
|
|
934
|
+
throw new AxiError("Could not determine the package name to update", "UPDATE_ERROR", [
|
|
935
|
+
"Reinstall the tool from npm so its package.json is available",
|
|
936
|
+
"Tool authors can pass `packageName` to runAxiCli()"
|
|
937
|
+
]);
|
|
938
|
+
}
|
|
939
|
+
if (!current) {
|
|
940
|
+
throw new AxiError(`Could not determine the current version of ${packageName}`, "UPDATE_ERROR", [
|
|
941
|
+
"Reinstall the tool from npm so its version is available",
|
|
942
|
+
"Tool authors can pass `version` to runAxiCli()"
|
|
943
|
+
]);
|
|
944
|
+
}
|
|
945
|
+
const fetchLatest = options.fetchLatest ?? ((name) => fetchLatestVersion(name, { platform }));
|
|
946
|
+
const latest = await fetchLatest(packageName);
|
|
947
|
+
const available = isUpdateAvailable(current, latest);
|
|
948
|
+
if (mode === "check") {
|
|
949
|
+
const output = {
|
|
950
|
+
update: { package: packageName, current, latest, available }
|
|
951
|
+
};
|
|
952
|
+
if (available) {
|
|
953
|
+
output.help = [`Run \`${binName} update\` to upgrade`];
|
|
954
|
+
}
|
|
955
|
+
return output;
|
|
956
|
+
}
|
|
957
|
+
if (!available) {
|
|
958
|
+
return {
|
|
959
|
+
update: `${packageName} is already on the latest version (${current})`
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
const method = entry ? detectInstallMethod({ entry, env: options.env }) : { kind: "unknown" };
|
|
963
|
+
const plan = planUpgrade(method, packageName);
|
|
964
|
+
if (!plan.argv) {
|
|
965
|
+
const help = method.kind === "npx" ? `Re-run with \`${plan.command}\` to use the latest version` : `Run \`${plan.command}\` to upgrade`;
|
|
966
|
+
return {
|
|
967
|
+
update: {
|
|
968
|
+
package: packageName,
|
|
969
|
+
current,
|
|
970
|
+
latest,
|
|
971
|
+
available: true,
|
|
972
|
+
action: "manual",
|
|
973
|
+
...plan.note ? { reason: plan.note } : {},
|
|
974
|
+
run: plan.command
|
|
975
|
+
},
|
|
976
|
+
help: [help]
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
const runInstall = options.runInstall ?? defaultRunInstall;
|
|
980
|
+
const result = await runInstall(plan, options.stdout, { platform });
|
|
981
|
+
if (!result.ok) {
|
|
982
|
+
throw new AxiError(`Failed to upgrade ${packageName}`, "UPDATE_ERROR", [
|
|
983
|
+
`Run \`${plan.command}\` manually`,
|
|
984
|
+
...result.message ? [result.message] : []
|
|
985
|
+
]);
|
|
986
|
+
}
|
|
987
|
+
if (method.kind === "homebrew") {
|
|
988
|
+
return homebrewUpgradeOutput({
|
|
989
|
+
packageName,
|
|
990
|
+
current,
|
|
991
|
+
latest,
|
|
992
|
+
installedVersion: resolveInstalledVersion(invokedAs, realpath, fs),
|
|
993
|
+
command: plan.command
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
update: `${packageName} upgraded ${current} -> ${latest}`,
|
|
998
|
+
command: plan.command
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ../../node_modules/axi-sdk-js/dist/cli.js
|
|
1003
|
+
var stdoutWithErrorHandler = /* @__PURE__ */ new WeakSet();
|
|
1004
|
+
function defaultFormatError(error) {
|
|
1005
|
+
if (error instanceof AxiError) {
|
|
1006
|
+
return {
|
|
1007
|
+
output: `${renderError(error.message, error.code, error.suggestions)}
|
|
1008
|
+
`,
|
|
1009
|
+
exitCode: exitCodeForError(error)
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1013
|
+
return {
|
|
1014
|
+
output: `${renderError(message, "UNKNOWN")}
|
|
1015
|
+
`,
|
|
1016
|
+
exitCode: 1
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function defaultUnknownCommand(command) {
|
|
1020
|
+
return `${renderError(`Unknown command: ${command}`, "VALIDATION_ERROR", [
|
|
1021
|
+
"Run `--help` to see available commands"
|
|
1022
|
+
])}
|
|
1023
|
+
`;
|
|
1024
|
+
}
|
|
1025
|
+
async function runAxiCli(options) {
|
|
1026
|
+
const stdout = options.stdout ?? process.stdout;
|
|
1027
|
+
handleStdoutErrors(stdout);
|
|
1028
|
+
try {
|
|
1029
|
+
await options.initialize?.();
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
writeFormattedError(error, stdout, options);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const argv = options.argv ?? process.argv.slice(2);
|
|
1035
|
+
if (argv.length === 1 && argv[0] === "--help") {
|
|
1036
|
+
stdout.write(options.topLevelHelp);
|
|
1037
|
+
if (!options.commands.update) {
|
|
1038
|
+
if (options.topLevelHelp.length > 0 && !options.topLevelHelp.endsWith("\n")) {
|
|
1039
|
+
stdout.write("\n");
|
|
1040
|
+
}
|
|
1041
|
+
stdout.write(builtinCommandsHelp());
|
|
1042
|
+
}
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (argv.length === 1 && isVersionFlag(argv[0])) {
|
|
1046
|
+
if (!options.version) {
|
|
1047
|
+
stdout.write(`${renderError("Version is not configured for this tool", "VALIDATION_ERROR")}
|
|
1048
|
+
`);
|
|
1049
|
+
process.exitCode = 2;
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
stdout.write(`${options.version}
|
|
1053
|
+
`);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const command = argv[0];
|
|
1057
|
+
if (!command) {
|
|
1058
|
+
await runHandler(options.home, [], { command: void 0, args: [] }, stdout, options, true);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (command.startsWith("-")) {
|
|
1062
|
+
stdout.write(renderLeadingFlagError(command));
|
|
1063
|
+
process.exitCode = 2;
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const args = argv.slice(1);
|
|
1067
|
+
if (command === "update" && !options.commands.update) {
|
|
1068
|
+
await runBuiltinUpdate(args, stdout, options);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (args.includes("--help")) {
|
|
1072
|
+
const help = options.getCommandHelp?.(command);
|
|
1073
|
+
if (help) {
|
|
1074
|
+
stdout.write(help);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const handler = options.commands[command];
|
|
1079
|
+
if (!handler) {
|
|
1080
|
+
stdout.write((options.renderUnknownCommand ?? defaultUnknownCommand)(command));
|
|
1081
|
+
process.exitCode = 2;
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
await runHandler(handler, args, { command, args }, stdout, options, false);
|
|
1085
|
+
}
|
|
1086
|
+
function handleStdoutErrors(stdout) {
|
|
1087
|
+
const errorObservable = stdout;
|
|
1088
|
+
if (typeof errorObservable.on !== "function" || stdoutWithErrorHandler.has(stdout)) {
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
stdoutWithErrorHandler.add(stdout);
|
|
1092
|
+
errorObservable.on("error", (error) => {
|
|
1093
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "EPIPE") {
|
|
1094
|
+
process.exitCode = 0;
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
throw error;
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
async function runHandler(handler, args, contextInput, stdout, options, isHomeView) {
|
|
1101
|
+
try {
|
|
1102
|
+
const context = await options.resolveContext?.(contextInput);
|
|
1103
|
+
const output = await handler(args, context);
|
|
1104
|
+
stdout.write(`${renderCommandOutput(output, options, isHomeView)}
|
|
1105
|
+
`);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
writeFormattedError(error, stdout, options);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
async function runBuiltinUpdate(args, stdout, options) {
|
|
1111
|
+
if (args.length === 1 && args[0] === "--help") {
|
|
1112
|
+
stdout.write(builtinUpdateHelp());
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
try {
|
|
1116
|
+
const output = await runUpdate({
|
|
1117
|
+
args,
|
|
1118
|
+
stdout,
|
|
1119
|
+
packageName: options.packageName,
|
|
1120
|
+
version: options.version
|
|
1121
|
+
});
|
|
1122
|
+
stdout.write(`${renderOutput(output)}
|
|
1123
|
+
`);
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
writeFormattedError(error, stdout, options);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function writeFormattedError(error, stdout, options) {
|
|
1129
|
+
const formatted = (options.formatError ?? defaultFormatError)(error);
|
|
1130
|
+
stdout.write(formatted.output);
|
|
1131
|
+
process.exitCode = formatted.exitCode;
|
|
1132
|
+
}
|
|
1133
|
+
function resolveBinName() {
|
|
1134
|
+
return basename2(process.argv[1] ?? "tool") || "tool";
|
|
1135
|
+
}
|
|
1136
|
+
function builtinCommandsHelp() {
|
|
1137
|
+
const bin = resolveBinName();
|
|
1138
|
+
return `${renderOutput({
|
|
1139
|
+
"built-in": {
|
|
1140
|
+
update: `Upgrade \`${bin}\` to the latest published version`,
|
|
1141
|
+
"update --check": "Report current vs latest without installing"
|
|
1142
|
+
}
|
|
1143
|
+
})}
|
|
1144
|
+
`;
|
|
1145
|
+
}
|
|
1146
|
+
function builtinUpdateHelp() {
|
|
1147
|
+
const bin = resolveBinName();
|
|
1148
|
+
return `${renderOutput({
|
|
1149
|
+
command: "update",
|
|
1150
|
+
description: `Upgrade \`${bin}\` to the latest published npm version`,
|
|
1151
|
+
flags: {
|
|
1152
|
+
"--check": "Report current vs latest and exit without installing"
|
|
1153
|
+
},
|
|
1154
|
+
examples: [`${bin} update`, `${bin} update --check`]
|
|
1155
|
+
})}
|
|
1156
|
+
`;
|
|
1157
|
+
}
|
|
1158
|
+
function renderLeadingFlagError(flag) {
|
|
1159
|
+
const bin = basename2(process.argv[1] ?? "tool") || "tool";
|
|
1160
|
+
return `${renderError("Flags must come after the command", "VALIDATION_ERROR", [
|
|
1161
|
+
`Run \`${bin} <command> [args] [flags]\``,
|
|
1162
|
+
`Move \`${flag}\` after the command instead of before it`
|
|
1163
|
+
])}
|
|
1164
|
+
`;
|
|
1165
|
+
}
|
|
1166
|
+
function isVersionFlag(flag) {
|
|
1167
|
+
return flag === "-v" || flag === "-V" || flag === "--version";
|
|
1168
|
+
}
|
|
1169
|
+
function renderCommandOutput(output, options, isHomeView) {
|
|
1170
|
+
if (!isHomeView) {
|
|
1171
|
+
return renderOutput(output);
|
|
1172
|
+
}
|
|
1173
|
+
const header = homeHeaderOutput({ description: options.description });
|
|
1174
|
+
if (typeof output === "string") {
|
|
1175
|
+
return `${renderOutput(header)}
|
|
1176
|
+
${output}`;
|
|
1177
|
+
}
|
|
1178
|
+
return renderOutput(mergeHomeHeader(header, output));
|
|
1179
|
+
}
|
|
1180
|
+
function mergeHomeHeader(header, output) {
|
|
1181
|
+
const rest = { ...output };
|
|
1182
|
+
delete rest.bin;
|
|
1183
|
+
delete rest.description;
|
|
1184
|
+
return {
|
|
1185
|
+
...header,
|
|
1186
|
+
...rest
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// ../../node_modules/axi-sdk-js/dist/hooks.js
|
|
1191
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, realpathSync as realpathSync2, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1192
|
+
import { homedir as homedir2 } from "node:os";
|
|
1193
|
+
import { basename as basename3, delimiter, dirname as dirname2, join as join2, resolve } from "node:path";
|
|
1194
|
+
var OPENCODE_PLUGIN_MANAGED_PREFIX = "axi-sdk-js managed opencode plugin:";
|
|
1195
|
+
function isManagedHook(hook, marker) {
|
|
1196
|
+
return typeof hook?.command === "string" && hook.command.includes(marker);
|
|
1197
|
+
}
|
|
1198
|
+
function computeSessionStartHookUpdate(settings, spec) {
|
|
1199
|
+
const updated = structuredClone(settings);
|
|
1200
|
+
let changed = false;
|
|
1201
|
+
if (!updated.hooks) {
|
|
1202
|
+
updated.hooks = {};
|
|
1203
|
+
changed = true;
|
|
1204
|
+
}
|
|
1205
|
+
if (Array.isArray(updated.hooks.session_start)) {
|
|
1206
|
+
const legacyHooks = updated.hooks.session_start.filter((hook) => !isManagedHook(hook, spec.marker));
|
|
1207
|
+
if (legacyHooks.length !== updated.hooks.session_start.length) {
|
|
1208
|
+
changed = true;
|
|
1209
|
+
if (legacyHooks.length === 0) {
|
|
1210
|
+
delete updated.hooks.session_start;
|
|
1211
|
+
} else {
|
|
1212
|
+
updated.hooks.session_start = legacyHooks;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (!Array.isArray(updated.hooks.SessionStart)) {
|
|
1217
|
+
updated.hooks.SessionStart = [];
|
|
1218
|
+
changed = true;
|
|
1219
|
+
}
|
|
1220
|
+
for (const group of updated.hooks.SessionStart) {
|
|
1221
|
+
if (!Array.isArray(group.hooks)) {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
for (const hook of group.hooks) {
|
|
1225
|
+
if (!isManagedHook(hook, spec.marker)) {
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
const timeout = spec.timeoutSeconds ?? 10;
|
|
1229
|
+
const isCorrect = hook.command === spec.command && hook.type === "command" && hook.timeout === timeout;
|
|
1230
|
+
if (isCorrect && !changed) {
|
|
1231
|
+
return [settings, false];
|
|
1232
|
+
}
|
|
1233
|
+
hook.command = spec.command;
|
|
1234
|
+
hook.type = "command";
|
|
1235
|
+
hook.timeout = timeout;
|
|
1236
|
+
return [updated, true];
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
updated.hooks.SessionStart.push({
|
|
1240
|
+
matcher: "",
|
|
1241
|
+
hooks: [
|
|
1242
|
+
{
|
|
1243
|
+
type: "command",
|
|
1244
|
+
command: spec.command,
|
|
1245
|
+
timeout: spec.timeoutSeconds ?? 10
|
|
1246
|
+
}
|
|
1247
|
+
]
|
|
1248
|
+
});
|
|
1249
|
+
return [updated, true];
|
|
1250
|
+
}
|
|
1251
|
+
function computeCodexConfigUpdate(content) {
|
|
1252
|
+
const newline = content.includes("\r\n") ? "\r\n" : "\n";
|
|
1253
|
+
const normalized = content.length === 0 ? "" : content;
|
|
1254
|
+
if (normalized.trim().length === 0) {
|
|
1255
|
+
return [`[features]${newline}hooks = true${newline}`, true];
|
|
1256
|
+
}
|
|
1257
|
+
const lines = normalized.split(/\r?\n/);
|
|
1258
|
+
const updated = [...lines];
|
|
1259
|
+
let inFeatures = false;
|
|
1260
|
+
let sawFeatures = false;
|
|
1261
|
+
for (let index = 0; index < updated.length; index++) {
|
|
1262
|
+
const line = updated[index];
|
|
1263
|
+
const section = line.match(/^\s*(\[{1,2})([^\]]+)(\]{1,2})\s*(?:#.*)?$/);
|
|
1264
|
+
if (section) {
|
|
1265
|
+
const isTableHeader = section[1] === "[" && section[3] === "]" || section[1] === "[[" && section[3] === "]]";
|
|
1266
|
+
if (!isTableHeader) {
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
const sectionName = section[2].trim();
|
|
1270
|
+
if (inFeatures) {
|
|
1271
|
+
updated.splice(index, 0, "hooks = true");
|
|
1272
|
+
return [updated.join(newline), true];
|
|
1273
|
+
}
|
|
1274
|
+
inFeatures = sectionName === "features";
|
|
1275
|
+
sawFeatures ||= inFeatures;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (!inFeatures) {
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const flag = line.match(/^\s*hooks\s*=\s*(true|false)\s*(?:#.*)?$/);
|
|
1282
|
+
if (!flag) {
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (flag[1] === "true") {
|
|
1286
|
+
return [content, false];
|
|
1287
|
+
}
|
|
1288
|
+
updated[index] = line.replace(/false/, "true");
|
|
1289
|
+
return [updated.join(newline), true];
|
|
1290
|
+
}
|
|
1291
|
+
if (sawFeatures) {
|
|
1292
|
+
const suffix = normalized.endsWith(newline) ? "" : newline;
|
|
1293
|
+
return [`${normalized}${suffix}hooks = true${newline}`, true];
|
|
1294
|
+
}
|
|
1295
|
+
const separator = normalized.endsWith(newline) ? newline : `${newline}${newline}`;
|
|
1296
|
+
return [
|
|
1297
|
+
`${normalized}${separator}[features]${newline}hooks = true${newline}`,
|
|
1298
|
+
true
|
|
1299
|
+
];
|
|
1300
|
+
}
|
|
1301
|
+
function sanitizeOpenCodePluginFilePart(marker) {
|
|
1302
|
+
return marker.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
1303
|
+
}
|
|
1304
|
+
function sanitizeOpenCodeExportName(marker) {
|
|
1305
|
+
const name = marker.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join("");
|
|
1306
|
+
return `Axi${name || "Plugin"}AmbientContextPlugin`;
|
|
1307
|
+
}
|
|
1308
|
+
function buildOpenCodeAmbientPluginSource(marker, command, timeoutSeconds) {
|
|
1309
|
+
const exportName = sanitizeOpenCodeExportName(marker);
|
|
1310
|
+
const managedMarker = `${OPENCODE_PLUGIN_MANAGED_PREFIX} ${marker}`;
|
|
1311
|
+
return `// ${managedMarker}
|
|
1312
|
+
// This file is generated by axi-sdk-js. It is safe to edit only if you remove the managed marker above.
|
|
1313
|
+
import { spawn } from "node:child_process";
|
|
1314
|
+
|
|
1315
|
+
const command = ${JSON.stringify(command)};
|
|
1316
|
+
const marker = ${JSON.stringify(marker)};
|
|
1317
|
+
const ambientHeader = ${JSON.stringify(`## AXI ambient context: ${marker}`)};
|
|
1318
|
+
const timeoutMs = ${JSON.stringify(timeoutSeconds * 1e3)};
|
|
1319
|
+
|
|
1320
|
+
function runAxiHomeView(cwd) {
|
|
1321
|
+
return new Promise((resolve) => {
|
|
1322
|
+
const child = spawn(command, [], {
|
|
1323
|
+
cwd: directoryOrFallback(cwd),
|
|
1324
|
+
env: process.env,
|
|
1325
|
+
shell: false,
|
|
1326
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
let stdout = "";
|
|
1330
|
+
let stderr = "";
|
|
1331
|
+
let settled = false;
|
|
1332
|
+
|
|
1333
|
+
const timer = setTimeout(() => {
|
|
1334
|
+
if (settled) return;
|
|
1335
|
+
settled = true;
|
|
1336
|
+
child.kill("SIGTERM");
|
|
1337
|
+
resolve("error: " + marker + " ambient context timed out after " + timeoutMs + "ms");
|
|
1338
|
+
}, timeoutMs);
|
|
1339
|
+
|
|
1340
|
+
child.stdout?.setEncoding("utf-8");
|
|
1341
|
+
child.stderr?.setEncoding("utf-8");
|
|
1342
|
+
child.stdout?.on("data", (chunk) => {
|
|
1343
|
+
stdout += chunk;
|
|
1344
|
+
});
|
|
1345
|
+
child.stderr?.on("data", (chunk) => {
|
|
1346
|
+
stderr += chunk;
|
|
1347
|
+
});
|
|
1348
|
+
child.on("error", (error) => {
|
|
1349
|
+
if (settled) return;
|
|
1350
|
+
settled = true;
|
|
1351
|
+
clearTimeout(timer);
|
|
1352
|
+
resolve("error: " + marker + " ambient context failed: " + error.message);
|
|
1353
|
+
});
|
|
1354
|
+
child.on("close", (code) => {
|
|
1355
|
+
if (settled) return;
|
|
1356
|
+
settled = true;
|
|
1357
|
+
clearTimeout(timer);
|
|
1358
|
+
if (code === 0) {
|
|
1359
|
+
resolve(stdout.trim());
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const message = (stderr || stdout || marker + " exited with code " + code).trim();
|
|
1363
|
+
resolve("error: " + marker + " ambient context failed: " + message);
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function directoryOrFallback(directory) {
|
|
1369
|
+
return typeof directory === "string" && directory.length > 0
|
|
1370
|
+
? directory
|
|
1371
|
+
: process.cwd();
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
export const ${exportName} = async ({ directory }) => {
|
|
1375
|
+
const sessionCache = new Map();
|
|
1376
|
+
|
|
1377
|
+
return {
|
|
1378
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
1379
|
+
const sessionID = input.sessionID ?? "__global__";
|
|
1380
|
+
let homeView = sessionCache.get(sessionID);
|
|
1381
|
+
if (homeView === undefined) {
|
|
1382
|
+
homeView = await runAxiHomeView(directory);
|
|
1383
|
+
sessionCache.set(sessionID, homeView);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (homeView.length === 0) return;
|
|
1387
|
+
output.system.push(ambientHeader + "\\n" + homeView);
|
|
1388
|
+
},
|
|
1389
|
+
};
|
|
1390
|
+
};
|
|
1391
|
+
`;
|
|
1392
|
+
}
|
|
1393
|
+
function openCodePluginFileName(marker) {
|
|
1394
|
+
return `axi-${sanitizeOpenCodePluginFilePart(marker)}.js`;
|
|
1395
|
+
}
|
|
1396
|
+
function openCodePluginDir(scope, home, root) {
|
|
1397
|
+
return scope === "project" ? join2(root, ".opencode", "plugins") : join2(home, ".config", "opencode", "plugins");
|
|
1398
|
+
}
|
|
1399
|
+
function resolveHookScopeTargets(marker, options) {
|
|
1400
|
+
const home = options.homeDir ?? homedir2();
|
|
1401
|
+
const scope = options.scope ?? "user";
|
|
1402
|
+
const root = scope === "project" ? resolve(options.projectDir ?? process.cwd()) : home;
|
|
1403
|
+
return {
|
|
1404
|
+
scope,
|
|
1405
|
+
home,
|
|
1406
|
+
root,
|
|
1407
|
+
claudeSettingsPath: join2(root, ".claude", "settings.json"),
|
|
1408
|
+
codexHooksPath: join2(root, ".codex", "hooks.json"),
|
|
1409
|
+
codexConfigPath: join2(home, ".codex", "config.toml"),
|
|
1410
|
+
openCodePluginPath: join2(openCodePluginDir(scope, home, root), openCodePluginFileName(marker))
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
function installOpenCodeAmbientPlugin(pluginPath, marker, command, timeoutSeconds, onError) {
|
|
1414
|
+
const managedMarker = `${OPENCODE_PLUGIN_MANAGED_PREFIX} ${marker}`;
|
|
1415
|
+
const next = buildOpenCodeAmbientPluginSource(marker, command, timeoutSeconds);
|
|
1416
|
+
try {
|
|
1417
|
+
mkdirSync(dirname2(pluginPath), { recursive: true });
|
|
1418
|
+
const current = existsSync2(pluginPath) ? readFileSync2(pluginPath, "utf-8") : void 0;
|
|
1419
|
+
if (current !== void 0 && !current.includes(managedMarker)) {
|
|
1420
|
+
onError?.(`${pluginPath}: refusing to overwrite unmanaged OpenCode plugin`);
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (current !== next) {
|
|
1424
|
+
writeFileSync(pluginPath, next, "utf-8");
|
|
1425
|
+
}
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1428
|
+
onError?.(`${pluginPath}: ${message}`);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
function resolvePortableHookCommand(execPath, binaryNames, marker, context) {
|
|
1432
|
+
if (binaryNames.length === 0) {
|
|
1433
|
+
return execPath;
|
|
1434
|
+
}
|
|
1435
|
+
const resolvedExec = context.resolveRealPath(execPath);
|
|
1436
|
+
if (!resolvedExec) {
|
|
1437
|
+
return execPath;
|
|
1438
|
+
}
|
|
1439
|
+
for (const name of binaryNames) {
|
|
1440
|
+
if (!name.includes(marker)) {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
for (const dir of context.pathEntries) {
|
|
1444
|
+
if (!dir)
|
|
1445
|
+
continue;
|
|
1446
|
+
for (const ext of context.pathExtensions) {
|
|
1447
|
+
const candidate = join2(dir, `${name}${ext}`);
|
|
1448
|
+
const resolvedCandidate = context.resolveRealPath(candidate);
|
|
1449
|
+
if (resolvedCandidate && resolvedCandidate === resolvedExec) {
|
|
1450
|
+
return name;
|
|
1451
|
+
}
|
|
1452
|
+
const shimTarget = context.resolveShimTarget?.(candidate);
|
|
1453
|
+
if (shimTarget && shimTarget === resolvedExec) {
|
|
1454
|
+
return name;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
return execPath;
|
|
1460
|
+
}
|
|
1461
|
+
var NPM_SHIM_SCRIPT_PATTERNS = [
|
|
1462
|
+
// Git Bash / POSIX shim: exec node "$basedir/node_modules/<pkg>/.../cli.js"
|
|
1463
|
+
/"\$basedir\/([^"]+\.[cm]?js)"/,
|
|
1464
|
+
// Windows .cmd shim: "%dp0%\node_modules\<pkg>\...\cli.js"
|
|
1465
|
+
/%~?dp0%?[\\/]([^"\r\n]+\.[cm]?js)/i
|
|
1466
|
+
];
|
|
1467
|
+
function extractNpmShimScriptPath(content) {
|
|
1468
|
+
for (const pattern of NPM_SHIM_SCRIPT_PATTERNS) {
|
|
1469
|
+
const match = content.match(pattern);
|
|
1470
|
+
if (match?.[1]) {
|
|
1471
|
+
return match[1];
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return void 0;
|
|
1475
|
+
}
|
|
1476
|
+
function buildDefaultPortableCommandContext() {
|
|
1477
|
+
const rawPath = process.env.PATH ?? process.env.Path ?? "";
|
|
1478
|
+
const pathEntries = rawPath.split(delimiter).filter(Boolean);
|
|
1479
|
+
const pathExtensions = process.platform === "win32" ? (
|
|
1480
|
+
// Git Bash resolves the extensionless shim, which PATHEXT omits.
|
|
1481
|
+
["", ...(process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";")]
|
|
1482
|
+
) : [""];
|
|
1483
|
+
return {
|
|
1484
|
+
pathEntries,
|
|
1485
|
+
pathExtensions,
|
|
1486
|
+
resolveRealPath: (absolutePath) => {
|
|
1487
|
+
try {
|
|
1488
|
+
const stat = statSync(absolutePath);
|
|
1489
|
+
if (!stat.isFile()) {
|
|
1490
|
+
return void 0;
|
|
1491
|
+
}
|
|
1492
|
+
return realpathSync2(absolutePath);
|
|
1493
|
+
} catch {
|
|
1494
|
+
return void 0;
|
|
1495
|
+
}
|
|
1496
|
+
},
|
|
1497
|
+
resolveShimTarget: (shimPath) => {
|
|
1498
|
+
try {
|
|
1499
|
+
const relative = extractNpmShimScriptPath(readFileSync2(shimPath, "utf-8"));
|
|
1500
|
+
if (!relative) {
|
|
1501
|
+
return void 0;
|
|
1502
|
+
}
|
|
1503
|
+
return realpathSync2(resolve(dirname2(shimPath), relative));
|
|
1504
|
+
} catch {
|
|
1505
|
+
return void 0;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
function shouldInstallHooksForNodeAxiExecPath(execPath, policy) {
|
|
1511
|
+
const normalized = resolve(execPath).replaceAll("\\", "/");
|
|
1512
|
+
if (!normalized.includes(policy.marker) || normalized.endsWith(".ts")) {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
const fileName = basename3(normalized);
|
|
1516
|
+
if (policy.binaryNames?.includes(fileName)) {
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
return policy.distEntrypoints?.some((entrypoint) => normalized.endsWith(entrypoint.replaceAll("\\", "/"))) ?? false;
|
|
1520
|
+
}
|
|
1521
|
+
function inferHookOptions(execPath) {
|
|
1522
|
+
if (!execPath) {
|
|
1523
|
+
return void 0;
|
|
1524
|
+
}
|
|
1525
|
+
const normalized = execPath.replaceAll("\\", "/");
|
|
1526
|
+
const match = normalized.match(/(?:^|\/)dist\/bin\/([^/]+)\.js$/);
|
|
1527
|
+
if (match?.[1]) {
|
|
1528
|
+
const marker = match[1];
|
|
1529
|
+
return {
|
|
1530
|
+
execPath,
|
|
1531
|
+
marker,
|
|
1532
|
+
binaryNames: [marker],
|
|
1533
|
+
distEntrypoints: [`dist/bin/${marker}.js`]
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
const fileName = normalized.split("/").pop() ?? "";
|
|
1537
|
+
if (!fileName || fileName.includes(".") || fileName === "node") {
|
|
1538
|
+
return void 0;
|
|
1539
|
+
}
|
|
1540
|
+
return {
|
|
1541
|
+
execPath,
|
|
1542
|
+
marker: fileName,
|
|
1543
|
+
binaryNames: [fileName],
|
|
1544
|
+
distEntrypoints: [`dist/bin/${fileName}.js`]
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
function buildInferredHookInstallPolicy(marker, options, inferred) {
|
|
1548
|
+
const binaryNames = options.binaryNames ?? inferred.binaryNames;
|
|
1549
|
+
const distEntrypoints = options.distEntrypoints ?? inferred.distEntrypoints;
|
|
1550
|
+
return (execPath) => shouldInstallHooksForNodeAxiExecPath(execPath, {
|
|
1551
|
+
marker,
|
|
1552
|
+
binaryNames,
|
|
1553
|
+
distEntrypoints
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
function installSessionStartHooks(options = {}) {
|
|
1557
|
+
const inferred = inferHookOptions(options.execPath ?? process.argv[1]);
|
|
1558
|
+
const marker = options.marker ?? inferred?.marker;
|
|
1559
|
+
if (!marker) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const execPath = resolve(options.execPath ?? inferred?.execPath ?? process.argv[1] ?? "");
|
|
1563
|
+
if (!execPath) {
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const defaultPolicyOptions = inferred ?? {
|
|
1567
|
+
execPath,
|
|
1568
|
+
marker,
|
|
1569
|
+
binaryNames: [marker],
|
|
1570
|
+
distEntrypoints: [`dist/bin/${marker}.js`]
|
|
1571
|
+
};
|
|
1572
|
+
const shouldInstall = options.shouldInstall ?? buildInferredHookInstallPolicy(marker, options, defaultPolicyOptions);
|
|
1573
|
+
if (shouldInstall && !shouldInstall(execPath)) {
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const binaryNames = options.binaryNames ?? inferred?.binaryNames ?? [];
|
|
1577
|
+
const command = resolvePortableHookCommand(execPath, binaryNames, marker, buildDefaultPortableCommandContext());
|
|
1578
|
+
const targets = resolveHookScopeTargets(marker, options);
|
|
1579
|
+
const jsonTargets = [targets.claudeSettingsPath, targets.codexHooksPath];
|
|
1580
|
+
const codexConfigPath = targets.codexConfigPath;
|
|
1581
|
+
installOpenCodeAmbientPlugin(targets.openCodePluginPath, marker, command, options.timeoutSeconds ?? 10, options.onError);
|
|
1582
|
+
for (const target of jsonTargets) {
|
|
1583
|
+
try {
|
|
1584
|
+
mkdirSync(dirname2(target), { recursive: true });
|
|
1585
|
+
const current = existsSync2(target) ? JSON.parse(readFileSync2(target, "utf-8")) : {};
|
|
1586
|
+
const [updated, changed] = computeSessionStartHookUpdate(current, {
|
|
1587
|
+
marker,
|
|
1588
|
+
command,
|
|
1589
|
+
timeoutSeconds: options.timeoutSeconds
|
|
1590
|
+
});
|
|
1591
|
+
if (changed) {
|
|
1592
|
+
writeFileSync(target, `${JSON.stringify(updated, null, 2)}
|
|
1593
|
+
`, "utf-8");
|
|
1594
|
+
}
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1597
|
+
options.onError?.(`${target}: ${message}`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
try {
|
|
1601
|
+
mkdirSync(dirname2(codexConfigPath), { recursive: true });
|
|
1602
|
+
const current = existsSync2(codexConfigPath) ? readFileSync2(codexConfigPath, "utf-8") : "";
|
|
1603
|
+
const [updated, changed] = computeCodexConfigUpdate(current);
|
|
1604
|
+
if (changed) {
|
|
1605
|
+
writeFileSync(codexConfigPath, updated, "utf-8");
|
|
1606
|
+
}
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1609
|
+
options.onError?.(`${codexConfigPath}: ${message}`);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// vendor/chrome-devtools-axi/src/client.ts
|
|
1614
|
+
import { execFileSync, spawn as spawn2 } from "node:child_process";
|
|
1615
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
|
|
1616
|
+
import { request } from "node:http";
|
|
1617
|
+
import { dirname as dirname3 } from "node:path";
|
|
1618
|
+
|
|
1619
|
+
// vendor/chrome-devtools-axi/src/pages.ts
|
|
1620
|
+
var PAGE_SCOPED_TOOLS = /* @__PURE__ */ new Set([
|
|
1621
|
+
"click",
|
|
1622
|
+
"drag",
|
|
1623
|
+
"emulate",
|
|
1624
|
+
"evaluate_script",
|
|
1625
|
+
"fill",
|
|
1626
|
+
"fill_form",
|
|
1627
|
+
"get_console_message",
|
|
1628
|
+
"get_network_request",
|
|
1629
|
+
"handle_dialog",
|
|
1630
|
+
"hover",
|
|
1631
|
+
"lighthouse_audit",
|
|
1632
|
+
"list_console_messages",
|
|
1633
|
+
"list_network_requests",
|
|
1634
|
+
"navigate_page",
|
|
1635
|
+
"performance_analyze_insight",
|
|
1636
|
+
"performance_start_trace",
|
|
1637
|
+
"performance_stop_trace",
|
|
1638
|
+
"press_key",
|
|
1639
|
+
"resize_page",
|
|
1640
|
+
"take_heapsnapshot",
|
|
1641
|
+
"take_screenshot",
|
|
1642
|
+
"take_snapshot",
|
|
1643
|
+
"type_text",
|
|
1644
|
+
"upload_file",
|
|
1645
|
+
"wait_for"
|
|
1646
|
+
]);
|
|
1647
|
+
function needsPageId(name, args = {}) {
|
|
1648
|
+
if (typeof args.pageId === "number") return false;
|
|
1649
|
+
if (name === "evaluate_script" && typeof args.serviceWorkerId === "string" && args.serviceWorkerId.length > 0) {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
return PAGE_SCOPED_TOOLS.has(name);
|
|
1653
|
+
}
|
|
1654
|
+
var GENERIC_SCHEME_SLASH_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
1655
|
+
var COLON_ONLY_SCHEME_URL = /^(?:about:|data:|view-source:|blob:)/i;
|
|
1656
|
+
function isPageSchemeUrl(label) {
|
|
1657
|
+
return GENERIC_SCHEME_SLASH_URL.test(label) || COLON_ONLY_SCHEME_URL.test(label);
|
|
1658
|
+
}
|
|
1659
|
+
var MCP_SECTION_HEADER = /^##\s+(Pages|Extension Pages|Extension Service Workers|Third-party developer tools|WebMCP tools)$/;
|
|
1660
|
+
var PAGE_ID_LINE = /^\d+:(?:\s+|$)/;
|
|
1661
|
+
var OPEN_DIALOG_HEADER = /^# Open dialog$/;
|
|
1662
|
+
var HANDLE_DIALOG_FOOTER = /^Call handle_dialog to handle it before continuing\.$/;
|
|
1663
|
+
function stripTrailingIsolatedContext(label) {
|
|
1664
|
+
const match = label.match(/^(.*)\s+isolatedContext=.*$/);
|
|
1665
|
+
if (!match) return label;
|
|
1666
|
+
const before = match[1];
|
|
1667
|
+
const withoutSelected = before.replace(/\s*\[selected\]\s*$/, "").trimEnd();
|
|
1668
|
+
if (matchTrailingUrl(withoutSelected) !== null) return before;
|
|
1669
|
+
if (isPageSchemeUrl(withoutSelected)) return before;
|
|
1670
|
+
return label;
|
|
1671
|
+
}
|
|
1672
|
+
function stripPageSuffixes(rest) {
|
|
1673
|
+
let label = stripTrailingIsolatedContext(rest);
|
|
1674
|
+
if (/(?:^|\s)\[selected\]\s*$/.test(label)) {
|
|
1675
|
+
label = label.replace(/\s*\[selected\]\s*$/, "").trimEnd();
|
|
1676
|
+
}
|
|
1677
|
+
return label;
|
|
1678
|
+
}
|
|
1679
|
+
function matchTrailingUrl(label) {
|
|
1680
|
+
const trimmed = label.trimEnd();
|
|
1681
|
+
if (!trimmed.endsWith(")")) return null;
|
|
1682
|
+
const close = trimmed.length - 1;
|
|
1683
|
+
let from = close;
|
|
1684
|
+
while (from > 0) {
|
|
1685
|
+
const open = trimmed.lastIndexOf(" (", from - 1);
|
|
1686
|
+
if (open === -1) return null;
|
|
1687
|
+
const url = trimmed.slice(open + 2, close);
|
|
1688
|
+
if (isPageSchemeUrl(url)) {
|
|
1689
|
+
return { title: trimmed.slice(0, open), url };
|
|
1690
|
+
}
|
|
1691
|
+
from = open;
|
|
1692
|
+
}
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
function hasSchemeUrl(label) {
|
|
1696
|
+
const rest = stripPageSuffixes(label);
|
|
1697
|
+
return matchTrailingUrl(rest) !== null || isPageSchemeUrl(rest);
|
|
1698
|
+
}
|
|
1699
|
+
function isCompletePageRow(row) {
|
|
1700
|
+
return hasSchemeUrl(row.replace(/^\d+:\s*/, ""));
|
|
1701
|
+
}
|
|
1702
|
+
function isTitleContinuationLine(line) {
|
|
1703
|
+
const m = line.match(/^(\d+):\s+(.+)$/);
|
|
1704
|
+
if (!m) return false;
|
|
1705
|
+
const rest = stripPageSuffixes(m[2]);
|
|
1706
|
+
if (matchTrailingUrl(rest) !== null) return false;
|
|
1707
|
+
if (isPageSchemeUrl(rest)) return false;
|
|
1708
|
+
return /(?:^|\s)\[selected\]\s*$/.test(stripTrailingIsolatedContext(m[2]));
|
|
1709
|
+
}
|
|
1710
|
+
function extractPageUrl(label) {
|
|
1711
|
+
const match = matchTrailingUrl(label);
|
|
1712
|
+
return match ? match.url : label.trim();
|
|
1713
|
+
}
|
|
1714
|
+
function pageRowRest(row) {
|
|
1715
|
+
return stripPageSuffixes(row.replace(/^\d+:\s*/, ""));
|
|
1716
|
+
}
|
|
1717
|
+
function stripDialogPreamble(text) {
|
|
1718
|
+
const lines = text.split(/\r?\n/);
|
|
1719
|
+
const kept = [];
|
|
1720
|
+
for (let i = 0; i < lines.length; ) {
|
|
1721
|
+
if (OPEN_DIALOG_HEADER.test(lines[i].trim())) {
|
|
1722
|
+
let end = -1;
|
|
1723
|
+
for (let idx = i + 1; idx < lines.length; idx++) {
|
|
1724
|
+
if (HANDLE_DIALOG_FOOTER.test(lines[idx].trim())) end = idx;
|
|
1725
|
+
}
|
|
1726
|
+
if (end !== -1) {
|
|
1727
|
+
i = end + 1;
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
kept.push(lines[i]);
|
|
1732
|
+
i++;
|
|
1733
|
+
}
|
|
1734
|
+
return kept.join("\n");
|
|
1735
|
+
}
|
|
1736
|
+
function followingLinesCompleteTitle(rawLines, headerIndex, currentRow) {
|
|
1737
|
+
let acc = `${currentRow} ## Pages`;
|
|
1738
|
+
for (let i = headerIndex + 1; i < rawLines.length; i++) {
|
|
1739
|
+
const next = rawLines[i].trim();
|
|
1740
|
+
if (next.length === 0) continue;
|
|
1741
|
+
if (next.match(MCP_SECTION_HEADER) !== null) return false;
|
|
1742
|
+
if (PAGE_ID_LINE.test(next)) {
|
|
1743
|
+
const rest = stripPageSuffixes(next.replace(/^\d+:\s*/, ""));
|
|
1744
|
+
if (isPageSchemeUrl(rest) && matchTrailingUrl(rest) === null) {
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
acc += ` ${next}`;
|
|
1749
|
+
if (matchTrailingUrl(pageRowRest(acc)) !== null) return true;
|
|
1750
|
+
}
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
function collapsePageRows(text) {
|
|
1754
|
+
const rows = [];
|
|
1755
|
+
let inPageBlock = false;
|
|
1756
|
+
const rawLines = stripDialogPreamble(text).split(/\r?\n/);
|
|
1757
|
+
for (let index = 0; index < rawLines.length; index++) {
|
|
1758
|
+
const line = rawLines[index].trim();
|
|
1759
|
+
if (line.length === 0) continue;
|
|
1760
|
+
const section = line.match(MCP_SECTION_HEADER)?.[1];
|
|
1761
|
+
if (section !== void 0) {
|
|
1762
|
+
const prev = rows[rows.length - 1];
|
|
1763
|
+
if (section === "Pages" && inPageBlock && prev !== void 0 && !isCompletePageRow(prev) && followingLinesCompleteTitle(rawLines, index, prev)) {
|
|
1764
|
+
rows[rows.length - 1] += ` ${line}`;
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (section === "Pages") {
|
|
1768
|
+
rows.length = 0;
|
|
1769
|
+
inPageBlock = true;
|
|
1770
|
+
} else if (section === "Extension Pages") {
|
|
1771
|
+
inPageBlock = true;
|
|
1772
|
+
} else {
|
|
1773
|
+
inPageBlock = false;
|
|
1774
|
+
}
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
if (!inPageBlock) continue;
|
|
1778
|
+
if (PAGE_ID_LINE.test(line)) {
|
|
1779
|
+
const prev = rows[rows.length - 1];
|
|
1780
|
+
if (prev !== void 0 && (!isCompletePageRow(prev) || isTitleContinuationLine(line))) {
|
|
1781
|
+
rows[rows.length - 1] += ` ${line}`;
|
|
1782
|
+
} else {
|
|
1783
|
+
rows.push(line);
|
|
1784
|
+
}
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
if (rows.length > 0) {
|
|
1788
|
+
rows[rows.length - 1] += ` ${line}`;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return rows.map((row) => row.replace(/\s+/g, " "));
|
|
1792
|
+
}
|
|
1793
|
+
function parsePagesList(text) {
|
|
1794
|
+
const pages = [];
|
|
1795
|
+
for (const line of collapsePageRows(text)) {
|
|
1796
|
+
const m = line.match(/^(\d+):\s+(.+)$/);
|
|
1797
|
+
if (!m) continue;
|
|
1798
|
+
const id = Number.parseInt(m[1], 10);
|
|
1799
|
+
let rest = stripTrailingIsolatedContext(m[2]);
|
|
1800
|
+
const selected = /(?:^|\s)\[selected\]\s*$/.test(rest);
|
|
1801
|
+
if (selected) {
|
|
1802
|
+
rest = rest.replace(/\s*\[selected\]\s*$/, "").trimEnd();
|
|
1803
|
+
}
|
|
1804
|
+
const url = extractPageUrl(rest);
|
|
1805
|
+
pages.push({ id, url, selected });
|
|
1806
|
+
}
|
|
1807
|
+
return pages;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// vendor/chrome-devtools-axi/src/client.ts
|
|
1811
|
+
var DEFAULT_BRIDGE_TIMEOUT_MS = 3e4;
|
|
1812
|
+
var MIN_BRIDGE_TIMEOUT_MS = 1e3;
|
|
1813
|
+
var HEALTH_TIMEOUT_MS = 2e3;
|
|
1814
|
+
var DEEP_HEALTH_TIMEOUT_MS = 5e3;
|
|
1815
|
+
function resolveBridgeTimeoutMs() {
|
|
1816
|
+
const raw = process.env.CHROME_DEVTOOLS_AXI_BRIDGE_TIMEOUT_MS;
|
|
1817
|
+
if (!raw) return DEFAULT_BRIDGE_TIMEOUT_MS;
|
|
1818
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1819
|
+
if (Number.isNaN(parsed) || parsed <= 0) return DEFAULT_BRIDGE_TIMEOUT_MS;
|
|
1820
|
+
return Math.max(parsed, MIN_BRIDGE_TIMEOUT_MS);
|
|
1821
|
+
}
|
|
1822
|
+
var CdpError = class extends AxiError {
|
|
1823
|
+
constructor(message, code, suggestions = []) {
|
|
1824
|
+
super(message, code, suggestions);
|
|
1825
|
+
this.code = code;
|
|
1826
|
+
this.suggestions = suggestions;
|
|
1827
|
+
this.name = "CdpError";
|
|
1828
|
+
}
|
|
1829
|
+
code;
|
|
1830
|
+
suggestions;
|
|
1831
|
+
};
|
|
1832
|
+
function readPidFile(pidFile = resolveSessionPidFile()) {
|
|
1833
|
+
try {
|
|
1834
|
+
if (!existsSync3(pidFile)) return null;
|
|
1835
|
+
const data = JSON.parse(readFileSync3(pidFile, "utf-8"));
|
|
1836
|
+
if (typeof data.pid === "number" && typeof data.port === "number") {
|
|
1837
|
+
return data;
|
|
1838
|
+
}
|
|
1839
|
+
return null;
|
|
1840
|
+
} catch {
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
function isProcessAlive(pid) {
|
|
1845
|
+
try {
|
|
1846
|
+
process.kill(pid, 0);
|
|
1847
|
+
return true;
|
|
1848
|
+
} catch {
|
|
1849
|
+
return false;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
function httpGet(port, path, timeoutMs = 2e3) {
|
|
1853
|
+
return new Promise((resolve3, reject) => {
|
|
1854
|
+
const req = request(
|
|
1855
|
+
{ hostname: "127.0.0.1", port, path, method: "GET", timeout: timeoutMs },
|
|
1856
|
+
(res) => {
|
|
1857
|
+
res.setEncoding("utf8");
|
|
1858
|
+
let data = "";
|
|
1859
|
+
res.on("data", (chunk) => data += chunk);
|
|
1860
|
+
res.on("end", () => resolve3(data));
|
|
1861
|
+
}
|
|
1862
|
+
);
|
|
1863
|
+
req.on("error", reject);
|
|
1864
|
+
req.on("timeout", () => {
|
|
1865
|
+
req.destroy();
|
|
1866
|
+
reject(new Error("timeout"));
|
|
1867
|
+
});
|
|
1868
|
+
req.end();
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
function httpPost(port, path, body, timeoutMs = 12e4) {
|
|
1872
|
+
return new Promise((resolve3, reject) => {
|
|
1873
|
+
const payload = JSON.stringify(body);
|
|
1874
|
+
const req = request(
|
|
1875
|
+
{
|
|
1876
|
+
hostname: "127.0.0.1",
|
|
1877
|
+
port,
|
|
1878
|
+
path,
|
|
1879
|
+
method: "POST",
|
|
1880
|
+
timeout: timeoutMs,
|
|
1881
|
+
headers: {
|
|
1882
|
+
"Content-Type": "application/json",
|
|
1883
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
1884
|
+
}
|
|
1885
|
+
},
|
|
1886
|
+
(res) => {
|
|
1887
|
+
res.setEncoding("utf8");
|
|
1888
|
+
let data = "";
|
|
1889
|
+
res.on("data", (chunk) => data += chunk);
|
|
1890
|
+
res.on("end", () => {
|
|
1891
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
1892
|
+
reject(new Error(data));
|
|
1893
|
+
} else {
|
|
1894
|
+
resolve3(data);
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
);
|
|
1899
|
+
req.on("error", reject);
|
|
1900
|
+
req.on("timeout", () => {
|
|
1901
|
+
req.destroy();
|
|
1902
|
+
reject(new Error("timeout"));
|
|
1903
|
+
});
|
|
1904
|
+
req.write(payload);
|
|
1905
|
+
req.end();
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
async function checkBridgeHealth(port, opts = {}) {
|
|
1909
|
+
try {
|
|
1910
|
+
const path = opts.deep ? "/health?deep=1" : "/health";
|
|
1911
|
+
const timeoutMs = opts.deep ? DEEP_HEALTH_TIMEOUT_MS : HEALTH_TIMEOUT_MS;
|
|
1912
|
+
const resp = await httpGet(port, path, timeoutMs);
|
|
1913
|
+
const data = JSON.parse(resp);
|
|
1914
|
+
if (data.status !== "ok") return false;
|
|
1915
|
+
if (opts.expectedSession !== void 0 && typeof data.session === "string" && data.session !== opts.expectedSession) {
|
|
1916
|
+
return false;
|
|
1917
|
+
}
|
|
1918
|
+
if (opts.deep && opts.notice) {
|
|
1919
|
+
opts.notice.pageIdentityChanged = data.pageIdentityChanged === true;
|
|
1920
|
+
}
|
|
1921
|
+
return true;
|
|
1922
|
+
} catch {
|
|
1923
|
+
return false;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
function sleep(ms) {
|
|
1927
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1928
|
+
}
|
|
1929
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
1930
|
+
const deadline = Date.now() + timeoutMs;
|
|
1931
|
+
while (Date.now() < deadline) {
|
|
1932
|
+
if (!isProcessAlive(pid)) return true;
|
|
1933
|
+
await sleep(50);
|
|
1934
|
+
}
|
|
1935
|
+
return !isProcessAlive(pid);
|
|
1936
|
+
}
|
|
1937
|
+
function isBridgeProcess(pid) {
|
|
1938
|
+
try {
|
|
1939
|
+
const command = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
1940
|
+
encoding: "utf-8",
|
|
1941
|
+
timeout: 1e3
|
|
1942
|
+
});
|
|
1943
|
+
return command.includes("chrome-devtools-axi-bridge");
|
|
1944
|
+
} catch {
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
async function terminateBridgeProcess(pid, opts = {}) {
|
|
1949
|
+
if (!isProcessAlive(pid)) return;
|
|
1950
|
+
const killProcessGroup = opts.killProcessGroup === true;
|
|
1951
|
+
try {
|
|
1952
|
+
process.kill(pid, "SIGTERM");
|
|
1953
|
+
} catch {
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
if (await waitForProcessExit(pid, 2e3)) {
|
|
1957
|
+
if (killProcessGroup) {
|
|
1958
|
+
try {
|
|
1959
|
+
process.kill(-pid, "SIGKILL");
|
|
1960
|
+
} catch {
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
if (killProcessGroup) {
|
|
1966
|
+
try {
|
|
1967
|
+
process.kill(-pid, "SIGKILL");
|
|
1968
|
+
} catch {
|
|
1969
|
+
try {
|
|
1970
|
+
process.kill(pid, "SIGKILL");
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
} else {
|
|
1975
|
+
try {
|
|
1976
|
+
process.kill(pid, "SIGKILL");
|
|
1977
|
+
} catch {
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
await waitForProcessExit(pid, 1e3);
|
|
1981
|
+
}
|
|
1982
|
+
function spawnBridgeProcess(port, sessionName) {
|
|
1983
|
+
const bridgeScript = resolveBridgeScript(import.meta.dirname);
|
|
1984
|
+
const script = existsSync3(bridgeScript.replace(/\.js$/, ".ts")) ? bridgeScript.replace(/\.js$/, ".ts") : bridgeScript;
|
|
1985
|
+
const runner = script.endsWith(".ts") ? "tsx" : "node";
|
|
1986
|
+
const child = spawn2(
|
|
1987
|
+
runner === "tsx" ? "npx" : "node",
|
|
1988
|
+
runner === "tsx" ? ["tsx", script] : [script],
|
|
1989
|
+
{
|
|
1990
|
+
stdio: "ignore",
|
|
1991
|
+
env: {
|
|
1992
|
+
...process.env,
|
|
1993
|
+
CHROME_DEVTOOLS_AXI_PORT: String(port),
|
|
1994
|
+
CHROME_DEVTOOLS_AXI_SESSION: sessionName
|
|
1995
|
+
},
|
|
1996
|
+
detached: true
|
|
1997
|
+
}
|
|
1998
|
+
);
|
|
1999
|
+
child.unref();
|
|
2000
|
+
return child;
|
|
2001
|
+
}
|
|
2002
|
+
function resolveSharedMcpMode() {
|
|
2003
|
+
const sharedServerUrl = process.env.CHROME_DEVTOOLS_AXI_MCP_SERVER_URL?.trim();
|
|
2004
|
+
if (!sharedServerUrl) return null;
|
|
2005
|
+
return process.env.CHROME_DEVTOOLS_AXI_MCP_PATH?.trim() ? "proxy" : "direct";
|
|
2006
|
+
}
|
|
2007
|
+
function sharedMcpSuggestions(mode) {
|
|
2008
|
+
if (mode === "direct") {
|
|
2009
|
+
return [
|
|
2010
|
+
"Shared MCP direct mode is enabled by CHROME_DEVTOOLS_AXI_MCP_SERVER_URL; AXI connects directly to that Streamable HTTP endpoint without launching a local MCP process.",
|
|
2011
|
+
"Verify CHROME_DEVTOOLS_AXI_MCP_SERVER_URL is an absolute http(s) MCP endpoint and that the service is running and reachable there."
|
|
2012
|
+
];
|
|
2013
|
+
}
|
|
2014
|
+
return [
|
|
2015
|
+
"Shared MCP proxy mode is enabled by CHROME_DEVTOOLS_AXI_MCP_SERVER_URL; this mode does not launch Chrome locally.",
|
|
2016
|
+
"Set CHROME_DEVTOOLS_AXI_MCP_PATH to a runnable chrome-devtools-mcp build that advertises --serverUrl in --help.",
|
|
2017
|
+
"Use the proxy-capable MCP build from the companion shared-server change, then restart this AXI session."
|
|
2018
|
+
];
|
|
2019
|
+
}
|
|
2020
|
+
function buildBridgeEarlyExitError(sessionName, port, code, signal) {
|
|
2021
|
+
const how = signal != null ? `was killed by ${signal}` : `exited with code ${code ?? "unknown"}`;
|
|
2022
|
+
const message = `Bridge for session "${sessionName}" ${how} before becoming ready on port ${port}`;
|
|
2023
|
+
if (code === BRIDGE_PORT_IN_USE_EXIT_CODE) {
|
|
2024
|
+
return new CdpError(message, "BRIDGE_NOT_READY", [
|
|
2025
|
+
`Port ${port} is already in use. It may be held by another chrome-devtools-axi session's bridge (a hashed-port collision, or a globally-exported CHROME_DEVTOOLS_AXI_PORT forcing every session onto one port), by a stale or crashed bridge that could not be reused, or by an unrelated process.`,
|
|
2026
|
+
"Set a distinct CHROME_DEVTOOLS_AXI_PORT for this session, unset a global CHROME_DEVTOOLS_AXI_PORT so every session derives its own, or free whatever is holding the port."
|
|
2027
|
+
]);
|
|
2028
|
+
}
|
|
2029
|
+
const sharedMcpMode = resolveSharedMcpMode();
|
|
2030
|
+
if (sharedMcpMode) {
|
|
2031
|
+
return new CdpError(
|
|
2032
|
+
message,
|
|
2033
|
+
"BRIDGE_NOT_READY",
|
|
2034
|
+
sharedMcpSuggestions(sharedMcpMode)
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
const suggestions = [
|
|
2038
|
+
"Check that chrome-devtools-mcp can start: npx chrome-devtools-mcp@latest --help"
|
|
2039
|
+
];
|
|
2040
|
+
if (process.env.CHROME_DEVTOOLS_AXI_MCP_PATH?.trim()) {
|
|
2041
|
+
suggestions.push(
|
|
2042
|
+
"Verify CHROME_DEVTOOLS_AXI_MCP_PATH points to a valid chrome-devtools-mcp build."
|
|
2043
|
+
);
|
|
2044
|
+
} else {
|
|
2045
|
+
suggestions.push(
|
|
2046
|
+
"`npx -y chrome-devtools-mcp@latest` may have failed to resolve/download the package (offline, or a slow cold first run); install it globally and set:",
|
|
2047
|
+
' export CHROME_DEVTOOLS_AXI_MCP_PATH="$(npm prefix -g)/lib/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js"'
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
suggestions.push(
|
|
2051
|
+
"Or Chrome failed to launch; confirm a usable Chrome is installed."
|
|
2052
|
+
);
|
|
2053
|
+
return new CdpError(message, "BRIDGE_NOT_READY", suggestions);
|
|
2054
|
+
}
|
|
2055
|
+
async function ensureBridge(spawnBridge = spawnBridgeProcess, notice) {
|
|
2056
|
+
const sessionName = resolveSessionName();
|
|
2057
|
+
const port = resolveSessionPort(sessionName);
|
|
2058
|
+
const pidFile = resolveSessionPidFile(sessionName);
|
|
2059
|
+
const pidInfo = readPidFile(pidFile);
|
|
2060
|
+
if (pidInfo && isProcessAlive(pidInfo.pid)) {
|
|
2061
|
+
if (await checkBridgeHealth(pidInfo.port, {
|
|
2062
|
+
deep: true,
|
|
2063
|
+
expectedSession: sessionName,
|
|
2064
|
+
notice
|
|
2065
|
+
})) {
|
|
2066
|
+
return pidInfo.port;
|
|
2067
|
+
}
|
|
2068
|
+
await terminateBridgeProcess(pidInfo.pid, {
|
|
2069
|
+
killProcessGroup: isBridgeProcess(pidInfo.pid)
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
clearSelectedPageId();
|
|
2073
|
+
const child = spawnBridge(port, sessionName);
|
|
2074
|
+
let childExited = false;
|
|
2075
|
+
let exitCode = null;
|
|
2076
|
+
let exitSignal = null;
|
|
2077
|
+
child.on("exit", (code, signal) => {
|
|
2078
|
+
childExited = true;
|
|
2079
|
+
exitCode = code;
|
|
2080
|
+
exitSignal = signal;
|
|
2081
|
+
});
|
|
2082
|
+
const timeoutMs = resolveBridgeTimeoutMs();
|
|
2083
|
+
const deadline = Date.now() + timeoutMs;
|
|
2084
|
+
let sawShallowReady = false;
|
|
2085
|
+
while (Date.now() < deadline) {
|
|
2086
|
+
if (await checkBridgeHealth(port, {
|
|
2087
|
+
deep: true,
|
|
2088
|
+
expectedSession: sessionName,
|
|
2089
|
+
notice
|
|
2090
|
+
})) {
|
|
2091
|
+
return port;
|
|
2092
|
+
}
|
|
2093
|
+
if (childExited) {
|
|
2094
|
+
if (await checkBridgeHealth(port, {
|
|
2095
|
+
deep: true,
|
|
2096
|
+
expectedSession: sessionName,
|
|
2097
|
+
notice
|
|
2098
|
+
})) {
|
|
2099
|
+
return port;
|
|
2100
|
+
}
|
|
2101
|
+
throw buildBridgeEarlyExitError(sessionName, port, exitCode, exitSignal);
|
|
2102
|
+
}
|
|
2103
|
+
if (!sawShallowReady && await checkBridgeHealth(port, { expectedSession: sessionName })) {
|
|
2104
|
+
sawShallowReady = true;
|
|
2105
|
+
}
|
|
2106
|
+
await sleep(500);
|
|
2107
|
+
}
|
|
2108
|
+
const seconds = Math.round(timeoutMs / 1e3);
|
|
2109
|
+
const sharedMcpMode = resolveSharedMcpMode();
|
|
2110
|
+
if (sawShallowReady) {
|
|
2111
|
+
const suggestions2 = sharedMcpMode ? [
|
|
2112
|
+
...sharedMcpSuggestions(sharedMcpMode),
|
|
2113
|
+
"The shared MCP service may be reachable while its attached Chrome target is unavailable."
|
|
2114
|
+
] : [
|
|
2115
|
+
"The Chrome/Electron instance the bridge was attached to may have exited.",
|
|
2116
|
+
"Verify the target is still listening on its remote-debugging port, then re-run the command.",
|
|
2117
|
+
"If the target was restarted, the bridge has already been recycled \u2014 this run will succeed once the target is reachable."
|
|
2118
|
+
];
|
|
2119
|
+
throw new CdpError(
|
|
2120
|
+
"Bridge is running but the attached CDP target appears to have gone away",
|
|
2121
|
+
"BRIDGE_NOT_READY",
|
|
2122
|
+
suggestions2
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
const suggestions = sharedMcpMode === "direct" ? sharedMcpSuggestions("direct") : sharedMcpMode === "proxy" ? sharedMcpSuggestions("proxy") : [
|
|
2126
|
+
"Check that chrome-devtools-mcp is installed: npx chrome-devtools-mcp@latest --help"
|
|
2127
|
+
];
|
|
2128
|
+
if (!sharedMcpMode && !process.env.CHROME_DEVTOOLS_AXI_MCP_PATH?.trim()) {
|
|
2129
|
+
suggestions.push(
|
|
2130
|
+
"If `npx -y chrome-devtools-mcp@latest` is slow on this machine, install mcp globally and set:",
|
|
2131
|
+
' export CHROME_DEVTOOLS_AXI_MCP_PATH="$(npm prefix -g)/lib/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js"'
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
suggestions.push(
|
|
2135
|
+
"Or extend the deadline: export CHROME_DEVTOOLS_AXI_BRIDGE_TIMEOUT_MS=60000"
|
|
2136
|
+
);
|
|
2137
|
+
throw new CdpError(
|
|
2138
|
+
`Bridge failed to start within ${seconds}s`,
|
|
2139
|
+
"BRIDGE_NOT_READY",
|
|
2140
|
+
suggestions
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
function parseCallResponse(resp) {
|
|
2144
|
+
const data = JSON.parse(resp);
|
|
2145
|
+
if (data.error) {
|
|
2146
|
+
throw new Error(data.error);
|
|
2147
|
+
}
|
|
2148
|
+
return data.result ?? "";
|
|
2149
|
+
}
|
|
2150
|
+
async function postTool(port, name, args, opts = {}) {
|
|
2151
|
+
const body = { name, args };
|
|
2152
|
+
if (opts.roots && opts.roots.length > 0) body.roots = opts.roots;
|
|
2153
|
+
const resp = await httpPost(port, "/call", body, opts.timeoutMs);
|
|
2154
|
+
return parseCallResponse(resp);
|
|
2155
|
+
}
|
|
2156
|
+
var FILE_OUTPUT_ARGS_BY_TOOL = /* @__PURE__ */ new Map([
|
|
2157
|
+
["take_screenshot", ["filePath"]],
|
|
2158
|
+
["get_network_request", ["responseFilePath", "requestFilePath"]],
|
|
2159
|
+
["performance_start_trace", ["filePath"]],
|
|
2160
|
+
["performance_stop_trace", ["filePath"]],
|
|
2161
|
+
["take_heapsnapshot", ["filePath"]]
|
|
2162
|
+
]);
|
|
2163
|
+
var DIR_OUTPUT_ARGS_BY_TOOL = /* @__PURE__ */ new Map([
|
|
2164
|
+
["lighthouse_audit", ["outputDirPath"]]
|
|
2165
|
+
]);
|
|
2166
|
+
function nearestExistingAncestor(path) {
|
|
2167
|
+
let candidate = path;
|
|
2168
|
+
while (!existsSync3(candidate)) {
|
|
2169
|
+
const parent = dirname3(candidate);
|
|
2170
|
+
if (parent === candidate) return candidate;
|
|
2171
|
+
candidate = parent;
|
|
2172
|
+
}
|
|
2173
|
+
return candidate;
|
|
2174
|
+
}
|
|
2175
|
+
function collectRootDirs(name, args) {
|
|
2176
|
+
const dirs = /* @__PURE__ */ new Set([process.cwd()]);
|
|
2177
|
+
for (const key of FILE_OUTPUT_ARGS_BY_TOOL.get(name) ?? []) {
|
|
2178
|
+
const value = args[key];
|
|
2179
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2180
|
+
dirs.add(nearestExistingAncestor(dirname3(value)));
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
for (const key of DIR_OUTPUT_ARGS_BY_TOOL.get(name) ?? []) {
|
|
2184
|
+
const value = args[key];
|
|
2185
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2186
|
+
dirs.add(nearestExistingAncestor(value));
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
if (name === "upload_file" && Array.isArray(args.filePaths)) {
|
|
2190
|
+
for (const file of args.filePaths) {
|
|
2191
|
+
if (typeof file === "string") dirs.add(dirname3(file));
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return [...dirs];
|
|
2195
|
+
}
|
|
2196
|
+
function resolveSelectedPageId(reconnected) {
|
|
2197
|
+
const pageId = getSelectedPageId();
|
|
2198
|
+
if (pageId === null) {
|
|
2199
|
+
if (reconnected) throw pageIdentityClearedError();
|
|
2200
|
+
throw new CdpError("No page is currently selected", "BROWSER_ERROR", [
|
|
2201
|
+
"Run `chrome-devtools-axi open <url>` to open a page",
|
|
2202
|
+
"Run `chrome-devtools-axi pages` to list tabs",
|
|
2203
|
+
"Run `chrome-devtools-axi selectpage <id>` to select a tab"
|
|
2204
|
+
]);
|
|
2205
|
+
}
|
|
2206
|
+
return pageId;
|
|
2207
|
+
}
|
|
2208
|
+
function resolveToolArgs(name, args, reconnected) {
|
|
2209
|
+
if (!needsPageId(name, args)) return args;
|
|
2210
|
+
return { ...args, pageId: resolveSelectedPageId(reconnected) };
|
|
2211
|
+
}
|
|
2212
|
+
async function callTool(name, args = {}) {
|
|
2213
|
+
const notice = { pageIdentityChanged: false };
|
|
2214
|
+
const port = await ensureBridge(void 0, notice);
|
|
2215
|
+
let resolved = args;
|
|
2216
|
+
try {
|
|
2217
|
+
resolved = resolveToolArgs(name, args, notice.pageIdentityChanged);
|
|
2218
|
+
const result = await postTool(port, name, resolved, {
|
|
2219
|
+
roots: collectRootDirs(name, resolved)
|
|
2220
|
+
});
|
|
2221
|
+
rememberToolRouting(name, resolved, result);
|
|
2222
|
+
return result;
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
if (err instanceof CdpError) throw err;
|
|
2225
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2226
|
+
if (isMissingPageError(message)) {
|
|
2227
|
+
const pageId = typeof resolved.pageId === "number" ? resolved.pageId : null;
|
|
2228
|
+
if (pageId !== null && getSelectedPageId() === pageId) {
|
|
2229
|
+
clearSelectedPageId();
|
|
2230
|
+
}
|
|
2231
|
+
throw missingPageError(pageId);
|
|
2232
|
+
}
|
|
2233
|
+
throw mapErrorMessage(message);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
var MCP_MISSING_PAGE_LINE = "No page found";
|
|
2237
|
+
var MCP_CLOSED_PAGE_LINE_PREFIX = "The selected page has been closed.";
|
|
2238
|
+
function isMissingPageError(message) {
|
|
2239
|
+
const line = lastNonEmptyLine(message).replace(/^Error:\s*/, "");
|
|
2240
|
+
return line === MCP_MISSING_PAGE_LINE || line.startsWith(MCP_CLOSED_PAGE_LINE_PREFIX);
|
|
2241
|
+
}
|
|
2242
|
+
function lastNonEmptyLine(message) {
|
|
2243
|
+
const lines = message.split(/\r?\n/);
|
|
2244
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
2245
|
+
const line = lines[i].trim();
|
|
2246
|
+
if (line) return line;
|
|
2247
|
+
}
|
|
2248
|
+
return "";
|
|
2249
|
+
}
|
|
2250
|
+
function missingPageError(pageId) {
|
|
2251
|
+
return new CdpError(
|
|
2252
|
+
pageId === null ? "The selected page is no longer available" : `Page ${pageId} is no longer available`,
|
|
2253
|
+
"BROWSER_ERROR",
|
|
2254
|
+
[
|
|
2255
|
+
"Run `chrome-devtools-axi pages` to list the remaining tabs",
|
|
2256
|
+
"Run `chrome-devtools-axi selectpage <id>` to select a tab, or `chrome-devtools-axi open <url>` to open one"
|
|
2257
|
+
]
|
|
2258
|
+
);
|
|
2259
|
+
}
|
|
2260
|
+
var PAGE_IDENTITY_SUGGESTIONS = [
|
|
2261
|
+
"Run `chrome-devtools-axi pages` to list the current tabs and their new ids",
|
|
2262
|
+
"Run `chrome-devtools-axi selectpage <id>` to re-select a tab after the reconnect, then retry"
|
|
2263
|
+
];
|
|
2264
|
+
function pageIdentityChangedError() {
|
|
2265
|
+
return new CdpError(
|
|
2266
|
+
PAGE_IDENTITY_CHANGED_ERROR,
|
|
2267
|
+
"BROWSER_ERROR",
|
|
2268
|
+
PAGE_IDENTITY_SUGGESTIONS
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
function pageIdentityClearedError() {
|
|
2272
|
+
return new CdpError(
|
|
2273
|
+
"The browser reconnected and every page id changed, so no page is currently selected",
|
|
2274
|
+
"BROWSER_ERROR",
|
|
2275
|
+
PAGE_IDENTITY_SUGGESTIONS
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
function mapErrorMessage(message) {
|
|
2279
|
+
if (message === PAGE_IDENTITY_CHANGED_ERROR) {
|
|
2280
|
+
return pageIdentityChangedError();
|
|
2281
|
+
}
|
|
2282
|
+
if (isMissingPageError(message)) return missingPageError(null);
|
|
2283
|
+
if (message.includes("ECONNREFUSED") || message.includes("ECONNRESET")) {
|
|
2284
|
+
return new CdpError("Bridge is not running", "BRIDGE_NOT_READY", [
|
|
2285
|
+
"Run `chrome-devtools-axi open <url>` \u2014 the bridge starts automatically"
|
|
2286
|
+
]);
|
|
2287
|
+
}
|
|
2288
|
+
if ((message.includes("uid") || message.includes("element")) && (message.includes("not found") || message.includes("invalid"))) {
|
|
2289
|
+
return new CdpError(message, "REF_NOT_FOUND", [
|
|
2290
|
+
"Run `chrome-devtools-axi snapshot` to see available elements and their @uid refs"
|
|
2291
|
+
]);
|
|
2292
|
+
}
|
|
2293
|
+
if (message.includes("timeout") || message.includes("timed out")) {
|
|
2294
|
+
return new CdpError(message, "TIMEOUT", [
|
|
2295
|
+
"Run `chrome-devtools-axi snapshot` to see current page state"
|
|
2296
|
+
]);
|
|
2297
|
+
}
|
|
2298
|
+
try {
|
|
2299
|
+
const parsed = JSON.parse(message);
|
|
2300
|
+
if (parsed.error) {
|
|
2301
|
+
return new CdpError(parsed.error, "BROWSER_ERROR", [
|
|
2302
|
+
"Run `chrome-devtools-axi snapshot` to see current page state"
|
|
2303
|
+
]);
|
|
2304
|
+
}
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2307
|
+
return new CdpError(message, "UNKNOWN");
|
|
2308
|
+
}
|
|
2309
|
+
async function getSessionSnapshotIfRunning() {
|
|
2310
|
+
let sessionName;
|
|
2311
|
+
let pidInfo;
|
|
2312
|
+
try {
|
|
2313
|
+
sessionName = resolveSessionName();
|
|
2314
|
+
pidInfo = readPidFile(resolveSessionPidFile(sessionName));
|
|
2315
|
+
} catch {
|
|
2316
|
+
return null;
|
|
2317
|
+
}
|
|
2318
|
+
if (!pidInfo || !isProcessAlive(pidInfo.pid)) {
|
|
2319
|
+
return null;
|
|
2320
|
+
}
|
|
2321
|
+
if (!await checkBridgeHealth(pidInfo.port, { expectedSession: sessionName })) {
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
try {
|
|
2325
|
+
const pageId = getSelectedPageId();
|
|
2326
|
+
if (pageId === null) return null;
|
|
2327
|
+
return await postTool(
|
|
2328
|
+
pidInfo.port,
|
|
2329
|
+
"take_snapshot",
|
|
2330
|
+
{ pageId },
|
|
2331
|
+
{ timeoutMs: 5e3 }
|
|
2332
|
+
);
|
|
2333
|
+
} catch {
|
|
2334
|
+
return null;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
async function stopBridge() {
|
|
2338
|
+
const pidInfo = readPidFile();
|
|
2339
|
+
if (!pidInfo) return false;
|
|
2340
|
+
if (!isProcessAlive(pidInfo.pid)) return false;
|
|
2341
|
+
await terminateBridgeProcess(pidInfo.pid, {
|
|
2342
|
+
killProcessGroup: isBridgeProcess(pidInfo.pid)
|
|
2343
|
+
});
|
|
2344
|
+
clearSelectedPageId();
|
|
2345
|
+
return true;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// vendor/chrome-devtools-axi/src/generation.ts
|
|
2349
|
+
import {
|
|
2350
|
+
existsSync as existsSync4,
|
|
2351
|
+
mkdirSync as mkdirSync2,
|
|
2352
|
+
readFileSync as readFileSync4,
|
|
2353
|
+
unlinkSync,
|
|
2354
|
+
writeFileSync as writeFileSync2
|
|
2355
|
+
} from "node:fs";
|
|
2356
|
+
import { dirname as dirname4, join as join3 } from "node:path";
|
|
2357
|
+
function genFile() {
|
|
2358
|
+
return join3(resolveSessionStateDir(), "snapshot-generation");
|
|
2359
|
+
}
|
|
2360
|
+
function getCurrentGeneration() {
|
|
2361
|
+
const file = genFile();
|
|
2362
|
+
try {
|
|
2363
|
+
if (!existsSync4(file)) return 0;
|
|
2364
|
+
const parsed = Number.parseInt(readFileSync4(file, "utf-8").trim(), 10);
|
|
2365
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
2366
|
+
} catch {
|
|
2367
|
+
return 0;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
function bumpGeneration() {
|
|
2371
|
+
const next = getCurrentGeneration() + 1;
|
|
2372
|
+
const file = genFile();
|
|
2373
|
+
try {
|
|
2374
|
+
mkdirSync2(dirname4(file), { recursive: true });
|
|
2375
|
+
writeFileSync2(file, String(next));
|
|
2376
|
+
} catch {
|
|
2377
|
+
}
|
|
2378
|
+
return next;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// vendor/chrome-devtools-axi/src/run.ts
|
|
2382
|
+
import { mkdtempSync, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, rmdirSync } from "node:fs";
|
|
2383
|
+
import { join as join4 } from "node:path";
|
|
2384
|
+
import { tmpdir } from "node:os";
|
|
2385
|
+
import { pathToFileURL } from "node:url";
|
|
2386
|
+
|
|
2387
|
+
// vendor/chrome-devtools-axi/src/snapshot.ts
|
|
2388
|
+
function countRefs(snapshot) {
|
|
2389
|
+
const matches = snapshot.match(/\buid=\S+/g);
|
|
2390
|
+
return matches ? matches.length : 0;
|
|
2391
|
+
}
|
|
2392
|
+
function extractRefs(snapshot) {
|
|
2393
|
+
const refs = [];
|
|
2394
|
+
for (const line of snapshot.split("\n")) {
|
|
2395
|
+
const m = line.match(/\buid=(\S+)\s+(\w+)\s+"([^"]*)"/);
|
|
2396
|
+
if (!m) continue;
|
|
2397
|
+
refs.push({ ref: m[1], type: m[2], label: m[3] });
|
|
2398
|
+
}
|
|
2399
|
+
return refs;
|
|
2400
|
+
}
|
|
2401
|
+
function parseStampedUid(arg) {
|
|
2402
|
+
const stripped = arg.startsWith("@") ? arg.slice(1) : arg;
|
|
2403
|
+
const m = stripped.match(/^g(\d+):(.+)$/);
|
|
2404
|
+
if (m) return { uid: m[2], generation: Number.parseInt(m[1], 10) };
|
|
2405
|
+
return { uid: stripped, generation: null };
|
|
2406
|
+
}
|
|
2407
|
+
function stampSnapshotGeneration(snapshot, generation) {
|
|
2408
|
+
return snapshot.replace(/\buid=(\S+)/g, (match, uid) => {
|
|
2409
|
+
if (/^g\d+:/.test(uid)) return match;
|
|
2410
|
+
return `uid=g${generation}:${uid}`;
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
function checkUidGeneration(arg, currentGeneration) {
|
|
2414
|
+
const { uid, generation } = parseStampedUid(arg);
|
|
2415
|
+
return {
|
|
2416
|
+
uid,
|
|
2417
|
+
stale: generation !== null && generation !== currentGeneration,
|
|
2418
|
+
refGeneration: generation
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
function extractTitle(snapshot) {
|
|
2422
|
+
const rootMatch = snapshot.match(/RootWebArea\s+"([^"]+)"/);
|
|
2423
|
+
if (rootMatch) return rootMatch[1];
|
|
2424
|
+
const headingMatch = snapshot.match(/\bheading\s+"([^"]+)"/);
|
|
2425
|
+
if (headingMatch) return headingMatch[1];
|
|
2426
|
+
return "";
|
|
2427
|
+
}
|
|
2428
|
+
function truncateSnapshot(snapshot, full, limit = 16e3) {
|
|
2429
|
+
const totalLength = snapshot.length;
|
|
2430
|
+
if (full || totalLength <= limit) {
|
|
2431
|
+
return { text: snapshot, truncated: false, totalLength };
|
|
2432
|
+
}
|
|
2433
|
+
const cut = snapshot.lastIndexOf("\n", limit);
|
|
2434
|
+
const text = cut > 0 ? snapshot.slice(0, cut) : snapshot.slice(0, limit);
|
|
2435
|
+
return { text, truncated: true, totalLength };
|
|
2436
|
+
}
|
|
2437
|
+
var MARKER_OVERHEAD = 50;
|
|
2438
|
+
function truncateText(text, limit = 8e3) {
|
|
2439
|
+
const totalLength = text.length;
|
|
2440
|
+
if (totalLength <= limit) {
|
|
2441
|
+
return { text, truncated: false, totalLength };
|
|
2442
|
+
}
|
|
2443
|
+
if (totalLength <= limit + MARKER_OVERHEAD) {
|
|
2444
|
+
return { text, truncated: false, totalLength };
|
|
2445
|
+
}
|
|
2446
|
+
const headBudget = Math.floor(limit * 0.4);
|
|
2447
|
+
const tailBudget = limit - headBudget;
|
|
2448
|
+
const headCut = text.lastIndexOf("\n", headBudget);
|
|
2449
|
+
const head = headCut > 0 ? text.slice(0, headCut) : text.slice(0, headBudget);
|
|
2450
|
+
const tailStart = text.indexOf("\n", totalLength - tailBudget);
|
|
2451
|
+
const tail = tailStart > 0 && tailStart < totalLength ? text.slice(tailStart + 1) : text.slice(totalLength - tailBudget);
|
|
2452
|
+
const omitted = totalLength - head.length - tail.length;
|
|
2453
|
+
const result = `${head}
|
|
2454
|
+
|
|
2455
|
+
... (${omitted} chars omitted, ${totalLength} total) ...
|
|
2456
|
+
|
|
2457
|
+
${tail}`;
|
|
2458
|
+
return { text: result, truncated: true, totalLength };
|
|
2459
|
+
}
|
|
2460
|
+
var INPUT_TYPES = ["textbox", "searchbox", "input", "combobox", "textarea"];
|
|
2461
|
+
function isInputType(type) {
|
|
2462
|
+
return INPUT_TYPES.includes(type);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// vendor/chrome-devtools-axi/src/uid-freshness.ts
|
|
2466
|
+
var PAGE_GENERATION_KEY = "__chromeDevtoolsAxiSnapshotGeneration";
|
|
2467
|
+
async function captureFreshSnapshot(caller, capture) {
|
|
2468
|
+
const first = await captureSnapshot(caller, capture);
|
|
2469
|
+
const fresh = first.state?.generation === first.generation && first.state.mutations > 0 ? await captureSnapshot(caller, capture) : first;
|
|
2470
|
+
return stampSnapshotGeneration(fresh.snapshot, fresh.generation);
|
|
2471
|
+
}
|
|
2472
|
+
function throwStaleRef(arg, refGeneration, currentGeneration) {
|
|
2473
|
+
const refRaw = arg.startsWith("@") ? arg.slice(1) : arg;
|
|
2474
|
+
const current = currentGeneration ?? "unavailable";
|
|
2475
|
+
throw new CdpError(
|
|
2476
|
+
`Stale ref @${refRaw}: from snapshot generation ${refGeneration}, current is ${current}. Re-snapshot to get fresh refs.`,
|
|
2477
|
+
"STALE_REF",
|
|
2478
|
+
[
|
|
2479
|
+
"Run `chrome-devtools-axi snapshot` to capture current refs, then retry the action"
|
|
2480
|
+
]
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
async function markPageSnapshotGeneration(generation, caller) {
|
|
2484
|
+
const key = JSON.stringify(PAGE_GENERATION_KEY);
|
|
2485
|
+
try {
|
|
2486
|
+
await caller("evaluate_script", {
|
|
2487
|
+
function: `() => {
|
|
2488
|
+
const key = ${key};
|
|
2489
|
+
const previous = globalThis[key];
|
|
2490
|
+
if (previous && previous.observer) previous.observer.disconnect();
|
|
2491
|
+
const state = { generation: ${generation}, mutations: 0, observer: null };
|
|
2492
|
+
const observer = new MutationObserver(() => { state.mutations += 1; });
|
|
2493
|
+
observer.observe(document.documentElement || document, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
2494
|
+
state.observer = observer;
|
|
2495
|
+
globalThis[key] = state;
|
|
2496
|
+
return state.generation;
|
|
2497
|
+
}`
|
|
2498
|
+
});
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
async function captureSnapshot(caller, capture) {
|
|
2503
|
+
const generation = bumpGeneration();
|
|
2504
|
+
await markPageSnapshotGeneration(generation, caller);
|
|
2505
|
+
const snapshot = await capture();
|
|
2506
|
+
return { generation, snapshot, state: await getPageRefState(caller) };
|
|
2507
|
+
}
|
|
2508
|
+
async function getPageRefState(caller) {
|
|
2509
|
+
const key = JSON.stringify(PAGE_GENERATION_KEY);
|
|
2510
|
+
try {
|
|
2511
|
+
const output = await caller("evaluate_script", {
|
|
2512
|
+
function: `() => {
|
|
2513
|
+
const state = globalThis[${key}];
|
|
2514
|
+
if (!state || typeof state.generation !== 'number' || typeof state.mutations !== 'number' || !state.observer) return null;
|
|
2515
|
+
return { generation: state.generation, mutations: state.mutations };
|
|
2516
|
+
}`
|
|
2517
|
+
});
|
|
2518
|
+
const jsonBlock = output.match(/```json\n([\s\S]*?)\n```/);
|
|
2519
|
+
const raw = jsonBlock?.[1] ?? output.replace(/^Script ran on page and returned:\s*/, "");
|
|
2520
|
+
const parsed = JSON.parse(raw.trim());
|
|
2521
|
+
if (typeof parsed !== "object" || parsed === null || typeof parsed.generation !== "number" || typeof parsed.mutations !== "number") {
|
|
2522
|
+
return null;
|
|
2523
|
+
}
|
|
2524
|
+
return parsed;
|
|
2525
|
+
} catch {
|
|
2526
|
+
return null;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
async function parseUidFresh(arg, caller) {
|
|
2530
|
+
const snapshotGeneration = getCurrentGeneration();
|
|
2531
|
+
const check = checkUidGeneration(arg, snapshotGeneration);
|
|
2532
|
+
if (check.stale) {
|
|
2533
|
+
throwStaleRef(arg, check.refGeneration, snapshotGeneration);
|
|
2534
|
+
}
|
|
2535
|
+
const state = await getPageRefState(caller);
|
|
2536
|
+
if (!state || state.generation !== snapshotGeneration || state.mutations !== 0) {
|
|
2537
|
+
throwStaleRef(
|
|
2538
|
+
arg,
|
|
2539
|
+
parseStampedUid(arg).generation ?? snapshotGeneration,
|
|
2540
|
+
state ? state.generation + state.mutations : null
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
return check.uid;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// vendor/chrome-devtools-axi/src/run.ts
|
|
2547
|
+
function unwrapNoArgIIFE(s) {
|
|
2548
|
+
const candidate = s.endsWith(";") ? s.slice(0, -1).trimEnd() : s;
|
|
2549
|
+
if (candidate.length < 4 || candidate[0] !== "(" || !candidate.endsWith(")")) {
|
|
2550
|
+
return s;
|
|
2551
|
+
}
|
|
2552
|
+
let depth = 0;
|
|
2553
|
+
let inString = null;
|
|
2554
|
+
let escape = false;
|
|
2555
|
+
let closeIdx = -1;
|
|
2556
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
2557
|
+
const c = candidate[i];
|
|
2558
|
+
if (escape) {
|
|
2559
|
+
escape = false;
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
if (inString) {
|
|
2563
|
+
if (c === "\\") {
|
|
2564
|
+
escape = true;
|
|
2565
|
+
continue;
|
|
2566
|
+
}
|
|
2567
|
+
if (c === inString) inString = null;
|
|
2568
|
+
continue;
|
|
2569
|
+
}
|
|
2570
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
2571
|
+
inString = c;
|
|
2572
|
+
continue;
|
|
2573
|
+
}
|
|
2574
|
+
if (c === "(") depth++;
|
|
2575
|
+
else if (c === ")") {
|
|
2576
|
+
depth--;
|
|
2577
|
+
if (depth === 0) {
|
|
2578
|
+
closeIdx = i;
|
|
2579
|
+
break;
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
if (closeIdx < 0) return s;
|
|
2584
|
+
const rest = candidate.slice(closeIdx + 1).trim();
|
|
2585
|
+
if (rest !== "()") return s;
|
|
2586
|
+
const inner = candidate.slice(1, closeIdx).trim();
|
|
2587
|
+
if (/^(async\s*)?(\(.*?\)\s*=>|[a-zA-Z_$][a-zA-Z0-9_$]*\s*=>|function[\s*(])/.test(
|
|
2588
|
+
inner
|
|
2589
|
+
)) {
|
|
2590
|
+
return inner;
|
|
2591
|
+
}
|
|
2592
|
+
return s;
|
|
2593
|
+
}
|
|
2594
|
+
function wrapJsExpression(js) {
|
|
2595
|
+
const trimmed = unwrapNoArgIIFE(js.trim());
|
|
2596
|
+
if (/^(async\s*)?(\(.*?\)\s*=>|[a-zA-Z_$][a-zA-Z0-9_$]*\s*=>|function[\s*(])/.test(
|
|
2597
|
+
trimmed
|
|
2598
|
+
)) {
|
|
2599
|
+
return trimmed;
|
|
2600
|
+
}
|
|
2601
|
+
return `() => (${trimmed})`;
|
|
2602
|
+
}
|
|
2603
|
+
function parseEvalOutput(output) {
|
|
2604
|
+
const jsonBlock = output.match(/```json\n([\s\S]*?)\n```/);
|
|
2605
|
+
if (jsonBlock) {
|
|
2606
|
+
try {
|
|
2607
|
+
return JSON.parse(jsonBlock[1].trim());
|
|
2608
|
+
} catch {
|
|
2609
|
+
return jsonBlock[1].trim();
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
const preamble = "Script ran on page and returned:";
|
|
2613
|
+
if (output.includes(preamble)) {
|
|
2614
|
+
const raw = output.slice(output.indexOf(preamble) + preamble.length).trim();
|
|
2615
|
+
try {
|
|
2616
|
+
return JSON.parse(raw);
|
|
2617
|
+
} catch {
|
|
2618
|
+
return raw;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
return output.trim();
|
|
2622
|
+
}
|
|
2623
|
+
function stripSnapshotHeader(text) {
|
|
2624
|
+
const lines = text.split("\n");
|
|
2625
|
+
const treeStart = lines.findIndex((l) => /\bRootWebArea\b|\buid=/.test(l));
|
|
2626
|
+
if (treeStart > 0) return lines.slice(treeStart).join("\n");
|
|
2627
|
+
return text.replace(/^[\s\S]*?##\s+Latest page snapshot\s*\n/, "");
|
|
2628
|
+
}
|
|
2629
|
+
function isRecoverableOpenError(error) {
|
|
2630
|
+
if (!(error instanceof CdpError)) return false;
|
|
2631
|
+
if (error.code !== "BROWSER_ERROR") return false;
|
|
2632
|
+
return /not connected|session (?:closed|not found)|no page/i.test(
|
|
2633
|
+
error.message
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
var UID_RE = /^@?(?:\d[\d_]*|g\d+:.+)$/;
|
|
2637
|
+
function isUidRef(s) {
|
|
2638
|
+
return UID_RE.test(s);
|
|
2639
|
+
}
|
|
2640
|
+
var DEFAULT_WAIT_TIMEOUT = 3e4;
|
|
2641
|
+
function createPageHelper(callTool2) {
|
|
2642
|
+
function callFunction(code) {
|
|
2643
|
+
return callTool2("evaluate_script", { function: wrapJsExpression(code) });
|
|
2644
|
+
}
|
|
2645
|
+
async function evalJs(code) {
|
|
2646
|
+
return parseEvalOutput(await callFunction(code));
|
|
2647
|
+
}
|
|
2648
|
+
return {
|
|
2649
|
+
async open(url) {
|
|
2650
|
+
if (!url) {
|
|
2651
|
+
throw new CdpError("Missing URL", "VALIDATION_ERROR", [
|
|
2652
|
+
'Start with `await page.open("https://example.com")`'
|
|
2653
|
+
]);
|
|
2654
|
+
}
|
|
2655
|
+
try {
|
|
2656
|
+
await callTool2("navigate_page", { type: "url", url });
|
|
2657
|
+
} catch (error) {
|
|
2658
|
+
if (!isRecoverableOpenError(error)) throw error;
|
|
2659
|
+
await callTool2("new_page", { url });
|
|
2660
|
+
}
|
|
2661
|
+
const info = await evalJs(
|
|
2662
|
+
`() => ({ url: location.href, status: performance.getEntriesByType('navigation').pop()?.responseStatus ?? null })`
|
|
2663
|
+
);
|
|
2664
|
+
const result = info;
|
|
2665
|
+
return {
|
|
2666
|
+
url: String(result?.url ?? url),
|
|
2667
|
+
status: typeof result?.status === "number" ? result.status : null
|
|
2668
|
+
};
|
|
2669
|
+
},
|
|
2670
|
+
async eval(jsOrFn) {
|
|
2671
|
+
if (typeof jsOrFn === "function") {
|
|
2672
|
+
return parseEvalOutput(
|
|
2673
|
+
await callTool2("evaluate_script", { function: String(jsOrFn) })
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
return evalJs(jsOrFn);
|
|
2677
|
+
},
|
|
2678
|
+
async wait(msOrSelector, timeout) {
|
|
2679
|
+
if (typeof msOrSelector === "number") {
|
|
2680
|
+
await callFunction(`new Promise(r => setTimeout(r, ${msOrSelector}))`);
|
|
2681
|
+
} else {
|
|
2682
|
+
const ms = timeout ?? DEFAULT_WAIT_TIMEOUT;
|
|
2683
|
+
const sel = JSON.stringify(msOrSelector);
|
|
2684
|
+
await callFunction(`new Promise((resolve, reject) => {
|
|
2685
|
+
const sel = ${sel};
|
|
2686
|
+
if (document.querySelector(sel)) { resolve(); return; }
|
|
2687
|
+
const observer = new MutationObserver(() => {
|
|
2688
|
+
if (document.querySelector(sel)) {
|
|
2689
|
+
observer.disconnect();
|
|
2690
|
+
clearTimeout(timer);
|
|
2691
|
+
resolve();
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
const timer = setTimeout(() => {
|
|
2695
|
+
observer.disconnect();
|
|
2696
|
+
reject(new Error('Timeout waiting for: ' + sel));
|
|
2697
|
+
}, ${ms});
|
|
2698
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
2699
|
+
})`);
|
|
2700
|
+
}
|
|
2701
|
+
},
|
|
2702
|
+
async snapshot() {
|
|
2703
|
+
return captureFreshSnapshot(
|
|
2704
|
+
callTool2,
|
|
2705
|
+
async () => stripSnapshotHeader(await callTool2("take_snapshot"))
|
|
2706
|
+
);
|
|
2707
|
+
},
|
|
2708
|
+
async click(refOrSelector) {
|
|
2709
|
+
if (isUidRef(refOrSelector)) {
|
|
2710
|
+
await callTool2("click", {
|
|
2711
|
+
uid: await parseUidFresh(refOrSelector, callTool2)
|
|
2712
|
+
});
|
|
2713
|
+
} else {
|
|
2714
|
+
const sel = JSON.stringify(refOrSelector);
|
|
2715
|
+
await callFunction(`() => {
|
|
2716
|
+
const el = document.querySelector(${sel});
|
|
2717
|
+
if (!el) throw new Error('Element not found: ' + ${sel});
|
|
2718
|
+
el.scrollIntoView({ block: 'center' });
|
|
2719
|
+
el.click();
|
|
2720
|
+
}`);
|
|
2721
|
+
}
|
|
2722
|
+
},
|
|
2723
|
+
async fill(refOrSelector, text) {
|
|
2724
|
+
if (isUidRef(refOrSelector)) {
|
|
2725
|
+
await callTool2("fill", {
|
|
2726
|
+
uid: await parseUidFresh(refOrSelector, callTool2),
|
|
2727
|
+
value: text
|
|
2728
|
+
});
|
|
2729
|
+
} else {
|
|
2730
|
+
const sel = JSON.stringify(refOrSelector);
|
|
2731
|
+
const val = JSON.stringify(text);
|
|
2732
|
+
await callFunction(`() => {
|
|
2733
|
+
const el = document.querySelector(${sel});
|
|
2734
|
+
if (!el) throw new Error('Element not found: ' + ${sel});
|
|
2735
|
+
el.focus();
|
|
2736
|
+
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype
|
|
2737
|
+
: el instanceof HTMLSelectElement ? HTMLSelectElement.prototype
|
|
2738
|
+
: HTMLInputElement.prototype;
|
|
2739
|
+
Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, ${val});
|
|
2740
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2741
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
2742
|
+
}`);
|
|
2743
|
+
}
|
|
2744
|
+
},
|
|
2745
|
+
async type(text) {
|
|
2746
|
+
await callTool2("type_text", { text });
|
|
2747
|
+
},
|
|
2748
|
+
async press(key) {
|
|
2749
|
+
await callTool2("press_key", { key });
|
|
2750
|
+
},
|
|
2751
|
+
async back() {
|
|
2752
|
+
await callTool2("navigate_page", { type: "back" });
|
|
2753
|
+
}
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
async function readStdin() {
|
|
2757
|
+
const chunks = [];
|
|
2758
|
+
for await (const chunk of process.stdin) {
|
|
2759
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
2760
|
+
}
|
|
2761
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
2762
|
+
}
|
|
2763
|
+
async function runScript(content, callTool2) {
|
|
2764
|
+
const page = createPageHelper(callTool2);
|
|
2765
|
+
const tmpDir = mkdtempSync(join4(tmpdir(), "cda-run-"));
|
|
2766
|
+
const tmpFile = join4(tmpDir, "script.mjs");
|
|
2767
|
+
writeFileSync3(tmpFile, content, "utf-8");
|
|
2768
|
+
const lines = [];
|
|
2769
|
+
const origLog = console.log;
|
|
2770
|
+
const captureLog = (...args) => {
|
|
2771
|
+
lines.push(args.map(String).join(" "));
|
|
2772
|
+
};
|
|
2773
|
+
const prevPage = globalThis.page;
|
|
2774
|
+
globalThis.page = page;
|
|
2775
|
+
console.log = captureLog;
|
|
2776
|
+
try {
|
|
2777
|
+
const mod = await import(pathToFileURL(tmpFile).href);
|
|
2778
|
+
if (typeof mod.default === "function") {
|
|
2779
|
+
await mod.default();
|
|
2780
|
+
}
|
|
2781
|
+
} finally {
|
|
2782
|
+
console.log = origLog;
|
|
2783
|
+
if (prevPage === void 0) {
|
|
2784
|
+
delete globalThis.page;
|
|
2785
|
+
} else {
|
|
2786
|
+
globalThis.page = prevPage;
|
|
2787
|
+
}
|
|
2788
|
+
try {
|
|
2789
|
+
unlinkSync2(tmpFile);
|
|
2790
|
+
rmdirSync(tmpDir);
|
|
2791
|
+
} catch {
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
const stdout = lines.length > 0 ? lines.join("\n") + "\n" : "";
|
|
2795
|
+
return { stdout };
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// vendor/chrome-devtools-axi/src/suggestions.ts
|
|
2799
|
+
function getSuggestions(ctx) {
|
|
2800
|
+
if (ctx.command === "wait" || ctx.command === "eval") {
|
|
2801
|
+
return ["Run `chrome-devtools-axi snapshot` to see current page state"];
|
|
2802
|
+
}
|
|
2803
|
+
const refs = ctx.snapshot ? extractRefs(ctx.snapshot) : [];
|
|
2804
|
+
const links = refs.filter((r) => r.type === "link");
|
|
2805
|
+
const buttons = refs.filter((r) => r.type === "button");
|
|
2806
|
+
const inputs = refs.filter((r) => isInputType(r.type));
|
|
2807
|
+
const lines = [];
|
|
2808
|
+
if (ctx.command === "fill") {
|
|
2809
|
+
const submitBtn = buttons.find(
|
|
2810
|
+
(r) => /submit|search|go|send|login|sign|ok/i.test(r.label)
|
|
2811
|
+
);
|
|
2812
|
+
if (submitBtn) {
|
|
2813
|
+
lines.push(
|
|
2814
|
+
`Run \`chrome-devtools-axi click @${submitBtn.ref}\` to click "${submitBtn.label}"`
|
|
2815
|
+
);
|
|
2816
|
+
} else {
|
|
2817
|
+
lines.push("Run `chrome-devtools-axi press Enter` to submit the form");
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (inputs.length > 0 && ctx.command !== "fill") {
|
|
2821
|
+
const inp = inputs[0];
|
|
2822
|
+
const label = inp.label ? `the "${inp.label}" field` : "the input field";
|
|
2823
|
+
lines.push(
|
|
2824
|
+
`Run \`chrome-devtools-axi fill @${inp.ref} "text"\` to fill ${label}`
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
if (buttons.length > 0) {
|
|
2828
|
+
const btn = ctx.command === "fill" ? buttons.find(
|
|
2829
|
+
(r) => !/submit|search|go|send|login|sign|ok/i.test(r.label)
|
|
2830
|
+
) ?? buttons[0] : buttons[0];
|
|
2831
|
+
if (btn && !lines.some((l) => l.includes(`@${btn.ref}`))) {
|
|
2832
|
+
const label = btn.label ? `"${btn.label}" ` : "";
|
|
2833
|
+
lines.push(
|
|
2834
|
+
`Run \`chrome-devtools-axi click @${btn.ref}\` to click the ${label}button`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
if (links.length > 0) {
|
|
2839
|
+
const link = links[0];
|
|
2840
|
+
lines.push(
|
|
2841
|
+
`Run \`chrome-devtools-axi click @${link.ref}\` to click the "${link.label}" link`
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
if (refs.length > 5) {
|
|
2845
|
+
lines.push("Run `chrome-devtools-axi scroll down` to scroll down");
|
|
2846
|
+
}
|
|
2847
|
+
lines.push(
|
|
2848
|
+
'Use `chrome-devtools-axi eval <expr>` for JS expressions. For multi-statement code, pass a function: `eval "() => { ...; return result }"`'
|
|
2849
|
+
);
|
|
2850
|
+
return lines;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// vendor/chrome-devtools-axi/src/hooks.ts
|
|
2854
|
+
var HOOK_MARKER = "chrome-devtools-axi";
|
|
2855
|
+
function shouldInstallHooksForExecPath(execPath) {
|
|
2856
|
+
return shouldInstallHooksForNodeAxiExecPath(execPath, {
|
|
2857
|
+
marker: HOOK_MARKER,
|
|
2858
|
+
binaryNames: [HOOK_MARKER],
|
|
2859
|
+
distEntrypoints: ["dist/bin/chrome-devtools-axi.js"]
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
function installHooksOrThrow() {
|
|
2863
|
+
const errors = [];
|
|
2864
|
+
installSessionStartHooks({
|
|
2865
|
+
marker: HOOK_MARKER,
|
|
2866
|
+
timeoutSeconds: 10,
|
|
2867
|
+
shouldInstall: shouldInstallHooksForExecPath,
|
|
2868
|
+
onError: (message) => {
|
|
2869
|
+
errors.push(message);
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
if (errors.length > 0) {
|
|
2873
|
+
throw new Error(errors.join("\n"));
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// vendor/chrome-devtools-axi/src/paths.ts
|
|
2878
|
+
import { isAbsolute, resolve as resolve2 } from "node:path";
|
|
2879
|
+
function resolveOutputPath(filePath) {
|
|
2880
|
+
return isAbsolute(filePath) ? filePath : resolve2(process.cwd(), filePath);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// vendor/chrome-devtools-axi/src/cli.ts
|
|
2884
|
+
var HOME_DESCRIPTION = "Agent ergonomic interface for controlling Chrome browser session. Prefer this over other browser automation tools.";
|
|
2885
|
+
var RAW_STDOUT_MARKER = "__CHROME_DEVTOOLS_AXI_RAW__";
|
|
2886
|
+
var TOP_HELP = `usage: chrome-devtools-axi [command] [args] [flags]
|
|
2887
|
+
commands[35]:
|
|
2888
|
+
open <url>, snapshot, screenshot <path>, click @<uid>, fill @<uid> <text>,
|
|
2889
|
+
type <text>, press <key>, scroll <dir>, back, wait <ms|text>, eval <js>,
|
|
2890
|
+
run,
|
|
2891
|
+
hover @<uid>, drag @<from> @<to>, fillform @<uid>=<val>..., dialog <action>,
|
|
2892
|
+
upload @<uid> <path>, pages, newpage <url>, selectpage <id>, closepage <id>,
|
|
2893
|
+
resize <w> <h>, emulate, console, console-get <id>, network,
|
|
2894
|
+
network-get [id], lighthouse, perf-start, perf-stop,
|
|
2895
|
+
perf-insight <set> <name>, heap <path>, start, stop, setup hooks
|
|
2896
|
+
|
|
2897
|
+
flags[2]:
|
|
2898
|
+
--help, -v/-V/--version
|
|
2899
|
+
|
|
2900
|
+
environment:
|
|
2901
|
+
CHROME_DEVTOOLS_AXI_AUTO_CONNECT Set to 1 to connect to the user's running Chrome (144+)
|
|
2902
|
+
via chrome://inspect/#remote-debugging instead of launching
|
|
2903
|
+
a new browser. Requires remote debugging enabled in Chrome.
|
|
2904
|
+
CHROME_DEVTOOLS_AXI_CHANNEL Chrome release channel to target: stable (default), beta,
|
|
2905
|
+
canary, or dev. Selects which installed Chrome --autoConnect
|
|
2906
|
+
attaches to, and which one is launched in the default and
|
|
2907
|
+
USER_DATA_DIR modes. Ignored with CHROME_DEVTOOLS_AXI_BROWSER_URL.
|
|
2908
|
+
CHROME_DEVTOOLS_AXI_HEADED Set to 1 to run Chrome in headed (visible) mode
|
|
2909
|
+
CHROME_DEVTOOLS_AXI_CHROME_ARGS Whitespace-separated Chrome flags forwarded to the browser
|
|
2910
|
+
(no shell-style quoting; flags with spaces are not supported)
|
|
2911
|
+
e.g. "--enable-gpu --ignore-gpu-blocklist"
|
|
2912
|
+
CHROME_DEVTOOLS_AXI_PORT Bridge server port (default: 9224)
|
|
2913
|
+
CHROME_DEVTOOLS_AXI_SESSION Named session for concurrent isolation. Each session name gets
|
|
2914
|
+
its own bridge process, port (auto-derived from the name, or set
|
|
2915
|
+
CHROME_DEVTOOLS_AXI_PORT), and on-disk state, so multiple sessions
|
|
2916
|
+
run at once without colliding. Connection mode and profile are
|
|
2917
|
+
unchanged. Defaults to "default" (port 9224, legacy state paths).
|
|
2918
|
+
e.g. CHROME_DEVTOOLS_AXI_SESSION=worker-1
|
|
2919
|
+
CHROME_DEVTOOLS_AXI_BROWSER_URL Connect to an existing Chrome instance instead of launching one.
|
|
2920
|
+
http(s):// uses --browserUrl (fetches /json/version).
|
|
2921
|
+
ws(s):// uses --wsEndpoint (direct WebSocket).
|
|
2922
|
+
e.g. "http://127.0.0.1:9222" or "wss://cluster.example/launch"
|
|
2923
|
+
CHROME_DEVTOOLS_AXI_WS_HEADERS JSON headers for ws(s):// endpoints (only with BROWSER_URL=wss?://)
|
|
2924
|
+
e.g. '{"Authorization":"Bearer token"}'
|
|
2925
|
+
CHROME_DEVTOOLS_AXI_USER_DATA_DIR Persistent Chrome profile directory (skips --isolated mode)
|
|
2926
|
+
e.g. "/path/to/.chrome-profile"
|
|
2927
|
+
CHROME_DEVTOOLS_AXI_MCP_PATH Absolute path to a chrome-devtools-mcp script. When set
|
|
2928
|
+
without a shared URL, the bridge spawns 'node $MCP_PATH'
|
|
2929
|
+
directly instead of 'npx -y chrome-devtools-mcp@latest'.
|
|
2930
|
+
With MCP_SERVER_URL also set, selects stdio proxy mode:
|
|
2931
|
+
the executable must advertise --serverUrl in --help.
|
|
2932
|
+
Local-mode recommendation:
|
|
2933
|
+
npm install -g chrome-devtools-mcp
|
|
2934
|
+
export CHROME_DEVTOOLS_AXI_MCP_PATH="$(npm prefix -g)/lib/node_modules/chrome-devtools-mcp/build/src/bin/chrome-devtools-mcp.js"
|
|
2935
|
+
CHROME_DEVTOOLS_AXI_MCP_SERVER_URL
|
|
2936
|
+
Shared MCP service URL. With MCP_PATH, starts a verified
|
|
2937
|
+
stdio proxy and passes --server-url=<URL>. Without
|
|
2938
|
+
MCP_PATH, connects directly over Streamable HTTP (no
|
|
2939
|
+
local MCP child); use an absolute http(s) MCP endpoint.
|
|
2940
|
+
If unset or blank, the bridge uses standalone stdio mode.
|
|
2941
|
+
CHROME_DEVTOOLS_AXI_BRIDGE_TIMEOUT_MS
|
|
2942
|
+
Bridge readiness deadline in ms (default: 30000, min: 1000)
|
|
2943
|
+
|
|
2944
|
+
gpu:
|
|
2945
|
+
Headless Chrome cannot access hardware GPU on most Linux systems.
|
|
2946
|
+
For GPU-accelerated WebGL, use headed mode with GPU flags:
|
|
2947
|
+
CHROME_DEVTOOLS_AXI_HEADED=1
|
|
2948
|
+
CHROME_DEVTOOLS_AXI_CHROME_ARGS="--enable-gpu --ignore-gpu-blocklist"
|
|
2949
|
+
For WebGPU, Vulkan must also be enabled (required for the Dawn backend):
|
|
2950
|
+
CHROME_DEVTOOLS_AXI_CHROME_ARGS="--enable-gpu --ignore-gpu-blocklist --enable-unsafe-webgpu --enable-features=Vulkan"
|
|
2951
|
+
|
|
2952
|
+
tips:
|
|
2953
|
+
Pipe output through grep/head to extract specific data from large pages.
|
|
2954
|
+
`;
|
|
2955
|
+
var COMMAND_HELP = {
|
|
2956
|
+
open: `usage: chrome-devtools-axi open <url> [--full]
|
|
2957
|
+
Navigate to a URL and capture an accessibility snapshot.
|
|
2958
|
+
|
|
2959
|
+
args:
|
|
2960
|
+
<url> URL to navigate to (required)
|
|
2961
|
+
|
|
2962
|
+
flags:
|
|
2963
|
+
--full Show complete snapshot without truncation
|
|
2964
|
+
|
|
2965
|
+
examples:
|
|
2966
|
+
chrome-devtools-axi open https://example.com
|
|
2967
|
+
chrome-devtools-axi open https://example.com --full`,
|
|
2968
|
+
screenshot: `usage: chrome-devtools-axi screenshot <path> [--uid @<uid>] [--full-page] [--format png|jpeg|webp]
|
|
2969
|
+
Save a screenshot to a file.
|
|
2970
|
+
|
|
2971
|
+
args:
|
|
2972
|
+
<path> File path to save the screenshot (required)
|
|
2973
|
+
|
|
2974
|
+
Relative output paths resolve against the directory where you run the CLI.
|
|
2975
|
+
Output reports the resolved absolute path.
|
|
2976
|
+
|
|
2977
|
+
flags:
|
|
2978
|
+
--uid @<uid> Capture a specific element instead of the full viewport.
|
|
2979
|
+
Refs are generation-tagged (e.g. @g3:12) - pass them back
|
|
2980
|
+
exactly as printed. A stale ref returns STALE_REF.
|
|
2981
|
+
--full-page Capture the entire scrollable page
|
|
2982
|
+
--format <fmt> Image format: png (default), jpeg, or webp
|
|
2983
|
+
|
|
2984
|
+
examples:
|
|
2985
|
+
chrome-devtools-axi screenshot ./page.png
|
|
2986
|
+
chrome-devtools-axi screenshot ./element.png --uid @g1:3
|
|
2987
|
+
chrome-devtools-axi screenshot ./full.png --full-page --format jpeg`,
|
|
2988
|
+
snapshot: `usage: chrome-devtools-axi snapshot [--full]
|
|
2989
|
+
Capture the current page accessibility snapshot.
|
|
2990
|
+
|
|
2991
|
+
flags:
|
|
2992
|
+
--full Show complete snapshot without truncation
|
|
2993
|
+
|
|
2994
|
+
examples:
|
|
2995
|
+
chrome-devtools-axi snapshot
|
|
2996
|
+
chrome-devtools-axi snapshot --full`,
|
|
2997
|
+
click: `usage: chrome-devtools-axi click @<uid> [--full]
|
|
2998
|
+
Click an interactive element by its ref from the snapshot.
|
|
2999
|
+
|
|
3000
|
+
args:
|
|
3001
|
+
@<uid> Element ref from snapshot (required). Refs are generation-tagged
|
|
3002
|
+
(e.g. @g3:12) - pass them back exactly as printed. A stale ref
|
|
3003
|
+
(older generation) returns a STALE_REF error so you know to re-snapshot.
|
|
3004
|
+
|
|
3005
|
+
flags:
|
|
3006
|
+
--full Show complete snapshot without truncation
|
|
3007
|
+
|
|
3008
|
+
examples:
|
|
3009
|
+
chrome-devtools-axi click @g1:1
|
|
3010
|
+
chrome-devtools-axi click @g2:12 --full`,
|
|
3011
|
+
fill: `usage: chrome-devtools-axi fill @<uid> <text> [--full]
|
|
3012
|
+
Fill a form field with text.
|
|
3013
|
+
|
|
3014
|
+
args:
|
|
3015
|
+
@<uid> Element ref from snapshot (required). Refs are generation-tagged
|
|
3016
|
+
(e.g. @g3:12) - pass them back exactly as printed. A stale ref
|
|
3017
|
+
returns a STALE_REF error so you know to re-snapshot.
|
|
3018
|
+
<text> Text to fill (required)
|
|
3019
|
+
|
|
3020
|
+
flags:
|
|
3021
|
+
--full Show complete snapshot without truncation
|
|
3022
|
+
|
|
3023
|
+
examples:
|
|
3024
|
+
chrome-devtools-axi fill @g1:3 "hello world"
|
|
3025
|
+
chrome-devtools-axi fill @g2:3 "search query" --full`,
|
|
3026
|
+
type: `usage: chrome-devtools-axi type <text> [--full]
|
|
3027
|
+
Type text at the currently focused element.
|
|
3028
|
+
|
|
3029
|
+
args:
|
|
3030
|
+
<text> Text to type (required)
|
|
3031
|
+
|
|
3032
|
+
flags:
|
|
3033
|
+
--full Show complete snapshot without truncation
|
|
3034
|
+
|
|
3035
|
+
examples:
|
|
3036
|
+
chrome-devtools-axi type "hello"
|
|
3037
|
+
chrome-devtools-axi type "search query" --full`,
|
|
3038
|
+
press: `usage: chrome-devtools-axi press <key> [--full]
|
|
3039
|
+
Press a keyboard key.
|
|
3040
|
+
|
|
3041
|
+
args:
|
|
3042
|
+
<key> Key name, e.g. Enter, Tab, Escape, ArrowDown (required)
|
|
3043
|
+
|
|
3044
|
+
flags:
|
|
3045
|
+
--full Show complete snapshot without truncation
|
|
3046
|
+
|
|
3047
|
+
examples:
|
|
3048
|
+
chrome-devtools-axi press Enter
|
|
3049
|
+
chrome-devtools-axi press Tab --full`,
|
|
3050
|
+
scroll: `usage: chrome-devtools-axi scroll <direction> [--full]
|
|
3051
|
+
Scroll the page in a direction.
|
|
3052
|
+
|
|
3053
|
+
args:
|
|
3054
|
+
<direction> up, down, top, or bottom (default: down)
|
|
3055
|
+
|
|
3056
|
+
flags:
|
|
3057
|
+
--full Show complete snapshot without truncation
|
|
3058
|
+
|
|
3059
|
+
examples:
|
|
3060
|
+
chrome-devtools-axi scroll down
|
|
3061
|
+
chrome-devtools-axi scroll top --full`,
|
|
3062
|
+
back: `usage: chrome-devtools-axi back [--full]
|
|
3063
|
+
Navigate back in browser history.
|
|
3064
|
+
|
|
3065
|
+
flags:
|
|
3066
|
+
--full Show complete snapshot without truncation
|
|
3067
|
+
|
|
3068
|
+
examples:
|
|
3069
|
+
chrome-devtools-axi back
|
|
3070
|
+
chrome-devtools-axi back --full`,
|
|
3071
|
+
wait: `usage: chrome-devtools-axi wait <ms|text>
|
|
3072
|
+
Wait for a duration or for text to appear on the page.
|
|
3073
|
+
|
|
3074
|
+
args:
|
|
3075
|
+
<ms> Milliseconds to wait (numeric)
|
|
3076
|
+
<text> Text to wait for (string)
|
|
3077
|
+
|
|
3078
|
+
examples:
|
|
3079
|
+
chrome-devtools-axi wait 2000
|
|
3080
|
+
chrome-devtools-axi wait "Submit"`,
|
|
3081
|
+
eval: `usage: chrome-devtools-axi eval <js>
|
|
3082
|
+
Evaluate a JavaScript expression in the page context and return the result.
|
|
3083
|
+
A bare expression is wrapped as () => (<js>); pass a function (arrow or
|
|
3084
|
+
function-keyword) for multi-statement logic. No-arg IIFE form (...)() is
|
|
3085
|
+
also accepted and unwrapped automatically.
|
|
3086
|
+
|
|
3087
|
+
args:
|
|
3088
|
+
<js> JavaScript expression (required)
|
|
3089
|
+
|
|
3090
|
+
examples:
|
|
3091
|
+
chrome-devtools-axi eval "document.title"
|
|
3092
|
+
chrome-devtools-axi eval "document.querySelectorAll('a').length"
|
|
3093
|
+
chrome-devtools-axi eval "() => { const rows = [...document.querySelectorAll('tr')]; return rows.map(r => r.textContent) }"`,
|
|
3094
|
+
run: `usage: chrome-devtools-axi run <<'EOF'
|
|
3095
|
+
...script...
|
|
3096
|
+
EOF
|
|
3097
|
+
|
|
3098
|
+
Execute a JavaScript script from stdin against the current browser session.
|
|
3099
|
+
The script gets a global \`page\` object. Only the script's stdout is returned.
|
|
3100
|
+
Pipe a script via heredoc or stdin \u2014 no file path needed.
|
|
3101
|
+
|
|
3102
|
+
script API (available as global \`page\`):
|
|
3103
|
+
await page.open(url) Navigate, returns { url, status }
|
|
3104
|
+
await page.eval(jsOrFn) Evaluate JS in the page, returns the value
|
|
3105
|
+
await page.snapshot() Get the generation-tagged accessibility tree
|
|
3106
|
+
await page.wait(ms) Wait by duration
|
|
3107
|
+
await page.wait(selector) Wait for CSS selector (30s timeout)
|
|
3108
|
+
await page.wait(selector, ms) Wait for CSS selector with timeout
|
|
3109
|
+
await page.click("@uid") Click an element by fresh ref
|
|
3110
|
+
await page.click(selector) Click via CSS selector
|
|
3111
|
+
await page.fill("@uid", text) Fill a form field by fresh ref
|
|
3112
|
+
await page.fill(selector, text) Fill via CSS selector, including controlled fields
|
|
3113
|
+
await page.type(text) Type at the focused element
|
|
3114
|
+
await page.press(key) Press a keyboard key
|
|
3115
|
+
await page.back() Navigate back
|
|
3116
|
+
|
|
3117
|
+
click and fill accept either @uid refs (from snapshot) or CSS selectors. UID actions verify that the page has not mutated since the snapshot and fail with STALE_REF when freshness cannot be confirmed, including for legacy untagged refs.
|
|
3118
|
+
page.eval accepts functions, arrow functions, and bare expression strings; no-arg IIFE strings are unwrapped automatically.
|
|
3119
|
+
|
|
3120
|
+
examples:
|
|
3121
|
+
chrome-devtools-axi run <<'EOF'
|
|
3122
|
+
await page.open("https://example.com");
|
|
3123
|
+
console.log(await page.eval(() => document.title));
|
|
3124
|
+
EOF
|
|
3125
|
+
|
|
3126
|
+
chrome-devtools-axi run <<'EOF'
|
|
3127
|
+
await page.open("https://en.wikipedia.org/wiki/Ada_Lovelace");
|
|
3128
|
+
await page.click("a[href='/wiki/Charles_Babbage']");
|
|
3129
|
+
await page.wait(".mw-page-title-main");
|
|
3130
|
+
console.log(await page.eval(() => document.title));
|
|
3131
|
+
EOF
|
|
3132
|
+
|
|
3133
|
+
chrome-devtools-axi run <<'EOF'
|
|
3134
|
+
const { status } = await page.open("https://httpbin.org/status/404");
|
|
3135
|
+
console.log("status:", status);
|
|
3136
|
+
EOF`,
|
|
3137
|
+
start: `usage: chrome-devtools-axi start
|
|
3138
|
+
Start the bridge server (launches headless Chrome).
|
|
3139
|
+
|
|
3140
|
+
examples:
|
|
3141
|
+
chrome-devtools-axi start`,
|
|
3142
|
+
stop: `usage: chrome-devtools-axi stop
|
|
3143
|
+
Stop the bridge server and close the browser.
|
|
3144
|
+
|
|
3145
|
+
examples:
|
|
3146
|
+
chrome-devtools-axi stop`,
|
|
3147
|
+
// Page management
|
|
3148
|
+
pages: `usage: chrome-devtools-axi pages
|
|
3149
|
+
List all open pages/tabs in the browser.
|
|
3150
|
+
|
|
3151
|
+
examples:
|
|
3152
|
+
chrome-devtools-axi pages`,
|
|
3153
|
+
newpage: `usage: chrome-devtools-axi newpage <url> [--background] [--full]
|
|
3154
|
+
Open a new tab and navigate to a URL.
|
|
3155
|
+
|
|
3156
|
+
args:
|
|
3157
|
+
<url> URL to open (required)
|
|
3158
|
+
|
|
3159
|
+
flags:
|
|
3160
|
+
--background Open in background without bringing to front
|
|
3161
|
+
--full Show complete snapshot without truncation
|
|
3162
|
+
|
|
3163
|
+
examples:
|
|
3164
|
+
chrome-devtools-axi newpage https://example.com
|
|
3165
|
+
chrome-devtools-axi newpage https://example.com --background`,
|
|
3166
|
+
selectpage: `usage: chrome-devtools-axi selectpage <id> [--full]
|
|
3167
|
+
Switch to a tab by page ID.
|
|
3168
|
+
|
|
3169
|
+
args:
|
|
3170
|
+
<id> Page ID from the pages command (required)
|
|
3171
|
+
|
|
3172
|
+
flags:
|
|
3173
|
+
--full Show complete snapshot without truncation
|
|
3174
|
+
|
|
3175
|
+
examples:
|
|
3176
|
+
chrome-devtools-axi selectpage 1`,
|
|
3177
|
+
closepage: `usage: chrome-devtools-axi closepage <id>
|
|
3178
|
+
Close a tab by page ID. The last open page cannot be closed.
|
|
3179
|
+
|
|
3180
|
+
args:
|
|
3181
|
+
<id> Page ID from the pages command (required)
|
|
3182
|
+
|
|
3183
|
+
examples:
|
|
3184
|
+
chrome-devtools-axi closepage 2`,
|
|
3185
|
+
resize: `usage: chrome-devtools-axi resize <width> <height>
|
|
3186
|
+
Resize the browser window; report requested and actual page dimensions.
|
|
3187
|
+
For an exact CSS viewport use emulate --viewport "390x844x1".
|
|
3188
|
+
|
|
3189
|
+
args:
|
|
3190
|
+
<width> Width in pixels (required)
|
|
3191
|
+
<height> Height in pixels (required)
|
|
3192
|
+
|
|
3193
|
+
examples:
|
|
3194
|
+
chrome-devtools-axi resize 1280 720
|
|
3195
|
+
chrome-devtools-axi resize 390 844`,
|
|
3196
|
+
// Interaction
|
|
3197
|
+
hover: `usage: chrome-devtools-axi hover @<uid> [--full]
|
|
3198
|
+
Hover over an element to trigger hover states.
|
|
3199
|
+
|
|
3200
|
+
args:
|
|
3201
|
+
@<uid> Element ref from snapshot (required). Refs are generation-tagged
|
|
3202
|
+
(e.g. @g3:12) - pass them back exactly as printed. A stale ref
|
|
3203
|
+
returns a STALE_REF error so you know to re-snapshot.
|
|
3204
|
+
|
|
3205
|
+
flags:
|
|
3206
|
+
--full Show complete snapshot without truncation
|
|
3207
|
+
|
|
3208
|
+
examples:
|
|
3209
|
+
chrome-devtools-axi hover @g1:5`,
|
|
3210
|
+
drag: `usage: chrome-devtools-axi drag @<from> @<to> [--full]
|
|
3211
|
+
Drag an element onto another element.
|
|
3212
|
+
|
|
3213
|
+
args:
|
|
3214
|
+
@<from> Element to drag (required). Use refs from the latest snapshot.
|
|
3215
|
+
@<to> Element to drop onto (required). Stale refs return STALE_REF.
|
|
3216
|
+
|
|
3217
|
+
flags:
|
|
3218
|
+
--full Show complete snapshot without truncation
|
|
3219
|
+
|
|
3220
|
+
examples:
|
|
3221
|
+
chrome-devtools-axi drag @g1:3 @g1:7`,
|
|
3222
|
+
fillform: `usage: chrome-devtools-axi fillform @<uid>=<value>... [--full]
|
|
3223
|
+
Fill multiple form fields at once.
|
|
3224
|
+
|
|
3225
|
+
args:
|
|
3226
|
+
@<uid>=<value> One or more field entries from the latest snapshot (required).
|
|
3227
|
+
Stale refs return STALE_REF.
|
|
3228
|
+
|
|
3229
|
+
flags:
|
|
3230
|
+
--full Show complete snapshot without truncation
|
|
3231
|
+
|
|
3232
|
+
examples:
|
|
3233
|
+
chrome-devtools-axi fillform @g1:1="hello" @g1:2="world"
|
|
3234
|
+
chrome-devtools-axi fillform @g2:3="user@email.com" @g2:4="password123"`,
|
|
3235
|
+
dialog: `usage: chrome-devtools-axi dialog <accept|dismiss> [text]
|
|
3236
|
+
Handle a browser dialog (alert, confirm, prompt).
|
|
3237
|
+
|
|
3238
|
+
args:
|
|
3239
|
+
<action> accept or dismiss (required)
|
|
3240
|
+
[text] Optional text to enter into a prompt dialog
|
|
3241
|
+
|
|
3242
|
+
examples:
|
|
3243
|
+
chrome-devtools-axi dialog accept
|
|
3244
|
+
chrome-devtools-axi dialog dismiss
|
|
3245
|
+
chrome-devtools-axi dialog accept "confirmed"`,
|
|
3246
|
+
upload: `usage: chrome-devtools-axi upload @<uid> <path> [--full]
|
|
3247
|
+
Upload a file through a file input element.
|
|
3248
|
+
|
|
3249
|
+
args:
|
|
3250
|
+
@<uid> File input element ref from snapshot (required). Refs are
|
|
3251
|
+
generation-tagged; stale refs return STALE_REF.
|
|
3252
|
+
<path> Local file path to upload (required)
|
|
3253
|
+
|
|
3254
|
+
flags:
|
|
3255
|
+
--full Show complete snapshot without truncation
|
|
3256
|
+
|
|
3257
|
+
examples:
|
|
3258
|
+
chrome-devtools-axi upload @g1:5 ./photo.jpg`,
|
|
3259
|
+
// Emulation
|
|
3260
|
+
emulate: `usage: chrome-devtools-axi emulate [flags]
|
|
3261
|
+
Emulate device features on the selected page.
|
|
3262
|
+
|
|
3263
|
+
flags:
|
|
3264
|
+
--viewport <spec> Viewport like "390x844x3,mobile,touch"
|
|
3265
|
+
--color-scheme <value> dark | light | auto
|
|
3266
|
+
--network <condition> Offline | Slow 3G | Fast 3G | Slow 4G | Fast 4G
|
|
3267
|
+
--cpu <rate> CPU throttling rate 1-20
|
|
3268
|
+
--geolocation <lat>x<lon> Geolocation like "37.7749x-122.4194"
|
|
3269
|
+
--user-agent <string> Custom user agent string
|
|
3270
|
+
|
|
3271
|
+
examples:
|
|
3272
|
+
chrome-devtools-axi emulate --viewport "390x844x3,mobile" --color-scheme dark
|
|
3273
|
+
chrome-devtools-axi emulate --network "Slow 3G" --cpu 4`,
|
|
3274
|
+
// DevTools debugging
|
|
3275
|
+
console: `usage: chrome-devtools-axi console [--type <type>] [--limit <n>] [--page <n>]
|
|
3276
|
+
List console messages for the current page.
|
|
3277
|
+
|
|
3278
|
+
flags:
|
|
3279
|
+
--type <type> Filter by message type. Valid values:
|
|
3280
|
+
log, debug, info, error, warn, dir, dirxml, table, trace,
|
|
3281
|
+
clear, startGroup, startGroupCollapsed, endGroup, assert,
|
|
3282
|
+
profile, profileEnd, count, timeEnd, verbose, issue, all
|
|
3283
|
+
("all" or omitted returns every message.)
|
|
3284
|
+
--limit <n> Maximum messages to return
|
|
3285
|
+
--page <n> Page number (0-based)
|
|
3286
|
+
|
|
3287
|
+
examples:
|
|
3288
|
+
chrome-devtools-axi console
|
|
3289
|
+
chrome-devtools-axi console --type error --limit 50
|
|
3290
|
+
chrome-devtools-axi console --type all`,
|
|
3291
|
+
"console-get": `usage: chrome-devtools-axi console-get <id>
|
|
3292
|
+
Get a specific console message by ID.
|
|
3293
|
+
|
|
3294
|
+
args:
|
|
3295
|
+
<id> Message ID from the console command (required)
|
|
3296
|
+
|
|
3297
|
+
examples:
|
|
3298
|
+
chrome-devtools-axi console-get 3`,
|
|
3299
|
+
network: `usage: chrome-devtools-axi network [--type <type>] [--limit <n>] [--page <n>]
|
|
3300
|
+
List network requests for the current page.
|
|
3301
|
+
|
|
3302
|
+
flags:
|
|
3303
|
+
--type <type> Filter by resource type. Valid values:
|
|
3304
|
+
document, stylesheet, image, media, font, script, texttrack,
|
|
3305
|
+
xhr, fetch, prefetch, eventsource, websocket, manifest,
|
|
3306
|
+
signedexchange, ping, cspviolationreport, preflight, fedcm,
|
|
3307
|
+
other, all
|
|
3308
|
+
("all" or omitted returns every request.)
|
|
3309
|
+
--limit <n> Maximum requests to return
|
|
3310
|
+
--page <n> Page number (0-based)
|
|
3311
|
+
|
|
3312
|
+
examples:
|
|
3313
|
+
chrome-devtools-axi network
|
|
3314
|
+
chrome-devtools-axi network --type fetch --limit 50
|
|
3315
|
+
chrome-devtools-axi network --type all`,
|
|
3316
|
+
"network-get": `usage: chrome-devtools-axi network-get [id] [--response-file <path>] [--request-file <path>]
|
|
3317
|
+
Get a specific network request. If id is omitted, gets the selected request.
|
|
3318
|
+
|
|
3319
|
+
args:
|
|
3320
|
+
[id] Request ID from the network command (optional)
|
|
3321
|
+
|
|
3322
|
+
flags:
|
|
3323
|
+
--response-file <path> Save response body to file
|
|
3324
|
+
--request-file <path> Save request body to file
|
|
3325
|
+
|
|
3326
|
+
Relative output paths resolve against the directory where you run the CLI.
|
|
3327
|
+
|
|
3328
|
+
examples:
|
|
3329
|
+
chrome-devtools-axi network-get 42
|
|
3330
|
+
chrome-devtools-axi network-get 42 --response-file ./response.json`,
|
|
3331
|
+
// Performance
|
|
3332
|
+
lighthouse: `usage: chrome-devtools-axi lighthouse [--device <device>] [--mode <mode>] [--output-dir <path>]
|
|
3333
|
+
Run a Lighthouse audit for accessibility, SEO, and best practices.
|
|
3334
|
+
|
|
3335
|
+
flags:
|
|
3336
|
+
--device <device> desktop (default) or mobile
|
|
3337
|
+
--mode <mode> navigation (default) or snapshot
|
|
3338
|
+
--output-dir <path> Directory for reports
|
|
3339
|
+
|
|
3340
|
+
Relative output paths resolve against the directory where you run the CLI.
|
|
3341
|
+
|
|
3342
|
+
examples:
|
|
3343
|
+
chrome-devtools-axi lighthouse
|
|
3344
|
+
chrome-devtools-axi lighthouse --device mobile --output-dir ./reports`,
|
|
3345
|
+
"perf-start": `usage: chrome-devtools-axi perf-start [--no-reload] [--no-auto-stop] [--file <path>]
|
|
3346
|
+
Start a performance trace recording.
|
|
3347
|
+
|
|
3348
|
+
flags:
|
|
3349
|
+
--no-reload Don't reload the page when starting
|
|
3350
|
+
--no-auto-stop Don't automatically stop the trace
|
|
3351
|
+
--file <path> Save raw trace data to file
|
|
3352
|
+
|
|
3353
|
+
Relative output paths resolve against the directory where you run the CLI.
|
|
3354
|
+
Output reports the resolved absolute path.
|
|
3355
|
+
|
|
3356
|
+
examples:
|
|
3357
|
+
chrome-devtools-axi perf-start
|
|
3358
|
+
chrome-devtools-axi perf-start --no-reload --file trace.json.gz`,
|
|
3359
|
+
"perf-stop": `usage: chrome-devtools-axi perf-stop [--file <path>]
|
|
3360
|
+
Stop the active performance trace recording.
|
|
3361
|
+
|
|
3362
|
+
flags:
|
|
3363
|
+
--file <path> Save raw trace data to file
|
|
3364
|
+
|
|
3365
|
+
Relative output paths resolve against the directory where you run the CLI.
|
|
3366
|
+
|
|
3367
|
+
examples:
|
|
3368
|
+
chrome-devtools-axi perf-stop
|
|
3369
|
+
chrome-devtools-axi perf-stop --file trace.json.gz`,
|
|
3370
|
+
"perf-insight": `usage: chrome-devtools-axi perf-insight <set-id> <insight-name>
|
|
3371
|
+
Analyze a specific performance insight from a trace.
|
|
3372
|
+
|
|
3373
|
+
args:
|
|
3374
|
+
<set-id> Insight set ID from trace results (required)
|
|
3375
|
+
<insight-name> Insight name, e.g. "DocumentLatency" (required)
|
|
3376
|
+
|
|
3377
|
+
examples:
|
|
3378
|
+
chrome-devtools-axi perf-insight set1 DocumentLatency
|
|
3379
|
+
chrome-devtools-axi perf-insight set1 LCPBreakdown`,
|
|
3380
|
+
heap: `usage: chrome-devtools-axi heap <path>
|
|
3381
|
+
Capture a heap snapshot for memory leak debugging.
|
|
3382
|
+
|
|
3383
|
+
args:
|
|
3384
|
+
<path> File path to save the .heapsnapshot file (required)
|
|
3385
|
+
|
|
3386
|
+
Relative output paths resolve against the directory where you run the CLI.
|
|
3387
|
+
Output reports the resolved absolute path.
|
|
3388
|
+
|
|
3389
|
+
examples:
|
|
3390
|
+
chrome-devtools-axi heap ./snapshot.heapsnapshot`,
|
|
3391
|
+
setup: `usage: chrome-devtools-axi setup hooks
|
|
3392
|
+
Install or repair agent SessionStart hooks for chrome-devtools-axi ambient context.
|
|
3393
|
+
|
|
3394
|
+
examples:
|
|
3395
|
+
chrome-devtools-axi setup hooks`
|
|
3396
|
+
};
|
|
3397
|
+
function getCommandHelp(command) {
|
|
3398
|
+
return COMMAND_HELP[command] ?? null;
|
|
3399
|
+
}
|
|
3400
|
+
function parseScreenshotArgs(args) {
|
|
3401
|
+
let filePath = null;
|
|
3402
|
+
let uid;
|
|
3403
|
+
let fullPage = false;
|
|
3404
|
+
let format;
|
|
3405
|
+
for (let i = 0; i < args.length; i++) {
|
|
3406
|
+
const a = args[i];
|
|
3407
|
+
if (a === "--uid" && i + 1 < args.length) {
|
|
3408
|
+
const raw = args[++i];
|
|
3409
|
+
uid = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
3410
|
+
} else if (a === "--full-page") {
|
|
3411
|
+
fullPage = true;
|
|
3412
|
+
} else if (a === "--format" && i + 1 < args.length) {
|
|
3413
|
+
format = args[++i];
|
|
3414
|
+
} else if (!a.startsWith("--")) {
|
|
3415
|
+
filePath = a;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
return { filePath, uid, fullPage, format };
|
|
3419
|
+
}
|
|
3420
|
+
function formatScreenshotOutput(filePath) {
|
|
3421
|
+
return encode({ screenshot: filePath });
|
|
3422
|
+
}
|
|
3423
|
+
function parseScreenshotOutputPath(result) {
|
|
3424
|
+
const match = result.match(/^Saved screenshot to ([^\r\n]+)\.\s*$/m);
|
|
3425
|
+
if (!match) {
|
|
3426
|
+
throw new CdpError(
|
|
3427
|
+
"chrome-devtools-mcp did not report a saved screenshot path",
|
|
3428
|
+
"BROWSER_ERROR"
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
return match[1];
|
|
3432
|
+
}
|
|
3433
|
+
function formatMcpResult(label, text, suggestions) {
|
|
3434
|
+
const blocks = [];
|
|
3435
|
+
const tr = truncateSnapshot(text, false, 2e3);
|
|
3436
|
+
blocks.push(`${label}:
|
|
3437
|
+
${tr.text.trimEnd()}`);
|
|
3438
|
+
if (tr.truncated) {
|
|
3439
|
+
blocks[0] += `
|
|
3440
|
+
... (truncated, ${tr.totalLength} chars total)`;
|
|
3441
|
+
}
|
|
3442
|
+
if (suggestions.length > 0) {
|
|
3443
|
+
blocks.push(renderHelp(suggestions));
|
|
3444
|
+
}
|
|
3445
|
+
return renderOutput2(blocks);
|
|
3446
|
+
}
|
|
3447
|
+
function parseFillFormArgs(args) {
|
|
3448
|
+
const entries = [];
|
|
3449
|
+
for (const arg of args) {
|
|
3450
|
+
if (arg === "--full") continue;
|
|
3451
|
+
const match = arg.match(/^@([^=]+)=(.+)$/);
|
|
3452
|
+
if (!match) continue;
|
|
3453
|
+
const uid = match[1];
|
|
3454
|
+
let value = match[2];
|
|
3455
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
3456
|
+
value = value.slice(1, -1);
|
|
3457
|
+
}
|
|
3458
|
+
entries.push({ uid, value });
|
|
3459
|
+
}
|
|
3460
|
+
return { entries };
|
|
3461
|
+
}
|
|
3462
|
+
function parseOptionalInteger(value) {
|
|
3463
|
+
if (value === void 0) return void 0;
|
|
3464
|
+
const parsed = Number.parseInt(value, 10);
|
|
3465
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
3466
|
+
}
|
|
3467
|
+
function parseEmulateArgs(args) {
|
|
3468
|
+
const result = {};
|
|
3469
|
+
let i = 0;
|
|
3470
|
+
while (i < args.length) {
|
|
3471
|
+
switch (args[i]) {
|
|
3472
|
+
case "--viewport":
|
|
3473
|
+
result.viewport = args[++i];
|
|
3474
|
+
break;
|
|
3475
|
+
case "--color-scheme":
|
|
3476
|
+
result.colorScheme = args[++i];
|
|
3477
|
+
break;
|
|
3478
|
+
case "--network":
|
|
3479
|
+
result.networkConditions = args[++i];
|
|
3480
|
+
break;
|
|
3481
|
+
case "--cpu": {
|
|
3482
|
+
const cpuThrottlingRate = parseOptionalInteger(args[++i]);
|
|
3483
|
+
if (cpuThrottlingRate !== void 0) {
|
|
3484
|
+
result.cpuThrottlingRate = cpuThrottlingRate;
|
|
3485
|
+
}
|
|
3486
|
+
break;
|
|
3487
|
+
}
|
|
3488
|
+
case "--geolocation":
|
|
3489
|
+
result.geolocation = args[++i];
|
|
3490
|
+
break;
|
|
3491
|
+
case "--user-agent":
|
|
3492
|
+
result.userAgent = args[++i];
|
|
3493
|
+
break;
|
|
3494
|
+
}
|
|
3495
|
+
i++;
|
|
3496
|
+
}
|
|
3497
|
+
return result;
|
|
3498
|
+
}
|
|
3499
|
+
function parseConsoleArgs(args) {
|
|
3500
|
+
const result = {};
|
|
3501
|
+
for (let i = 0; i < args.length; i++) {
|
|
3502
|
+
if (args[i] === "--type" && i + 1 < args.length) {
|
|
3503
|
+
const value = args[++i];
|
|
3504
|
+
if (value.toLowerCase() === "all") delete result.types;
|
|
3505
|
+
else result.types = [value];
|
|
3506
|
+
} else if (args[i] === "--limit" && i + 1 < args.length) {
|
|
3507
|
+
const pageSize = parseOptionalInteger(args[++i]);
|
|
3508
|
+
if (pageSize !== void 0) result.pageSize = pageSize;
|
|
3509
|
+
} else if (args[i] === "--page" && i + 1 < args.length) {
|
|
3510
|
+
const pageIdx = parseOptionalInteger(args[++i]);
|
|
3511
|
+
if (pageIdx !== void 0) result.pageIdx = pageIdx;
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return result;
|
|
3515
|
+
}
|
|
3516
|
+
function parseNetworkArgs(args) {
|
|
3517
|
+
const result = {};
|
|
3518
|
+
for (let i = 0; i < args.length; i++) {
|
|
3519
|
+
if (args[i] === "--type" && i + 1 < args.length) {
|
|
3520
|
+
const value = args[++i];
|
|
3521
|
+
if (value.toLowerCase() === "all") delete result.resourceTypes;
|
|
3522
|
+
else result.resourceTypes = [value];
|
|
3523
|
+
} else if (args[i] === "--limit" && i + 1 < args.length) {
|
|
3524
|
+
const pageSize = parseOptionalInteger(args[++i]);
|
|
3525
|
+
if (pageSize !== void 0) result.pageSize = pageSize;
|
|
3526
|
+
} else if (args[i] === "--page" && i + 1 < args.length) {
|
|
3527
|
+
const pageIdx = parseOptionalInteger(args[++i]);
|
|
3528
|
+
if (pageIdx !== void 0) result.pageIdx = pageIdx;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
return result;
|
|
3532
|
+
}
|
|
3533
|
+
function parseNetworkGetArgs(args) {
|
|
3534
|
+
const result = {};
|
|
3535
|
+
for (let i = 0; i < args.length; i++) {
|
|
3536
|
+
if (args[i] === "--response-file" && i + 1 < args.length) {
|
|
3537
|
+
result.responseFilePath = args[++i];
|
|
3538
|
+
} else if (args[i] === "--request-file" && i + 1 < args.length) {
|
|
3539
|
+
result.requestFilePath = args[++i];
|
|
3540
|
+
} else if (!args[i].startsWith("--")) {
|
|
3541
|
+
const reqid = parseOptionalInteger(args[i]);
|
|
3542
|
+
if (reqid !== void 0) result.reqid = reqid;
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
return result;
|
|
3546
|
+
}
|
|
3547
|
+
function parseLighthouseArgs(args) {
|
|
3548
|
+
const result = {};
|
|
3549
|
+
for (let i = 0; i < args.length; i++) {
|
|
3550
|
+
switch (args[i]) {
|
|
3551
|
+
case "--device":
|
|
3552
|
+
result.device = args[++i];
|
|
3553
|
+
break;
|
|
3554
|
+
case "--mode":
|
|
3555
|
+
result.mode = args[++i];
|
|
3556
|
+
break;
|
|
3557
|
+
case "--output-dir":
|
|
3558
|
+
result.outputDirPath = args[++i];
|
|
3559
|
+
break;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
return result;
|
|
3563
|
+
}
|
|
3564
|
+
function parsePerfStartArgs(args) {
|
|
3565
|
+
const result = {};
|
|
3566
|
+
for (let i = 0; i < args.length; i++) {
|
|
3567
|
+
switch (args[i]) {
|
|
3568
|
+
case "--no-reload":
|
|
3569
|
+
result.reload = false;
|
|
3570
|
+
break;
|
|
3571
|
+
case "--no-auto-stop":
|
|
3572
|
+
result.autoStop = false;
|
|
3573
|
+
break;
|
|
3574
|
+
case "--file":
|
|
3575
|
+
result.filePath = args[++i];
|
|
3576
|
+
break;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return result;
|
|
3580
|
+
}
|
|
3581
|
+
function renderHelp(lines) {
|
|
3582
|
+
if (lines.length === 0) return "";
|
|
3583
|
+
const indented = lines.map((l) => ` ${l}`).join("\n");
|
|
3584
|
+
return `help[${lines.length}]:
|
|
3585
|
+
${indented}`;
|
|
3586
|
+
}
|
|
3587
|
+
function renderError2(message, code, suggestions = []) {
|
|
3588
|
+
const blocks = [encode({ error: message, code })];
|
|
3589
|
+
if (suggestions.length > 0) {
|
|
3590
|
+
blocks.push(renderHelp(suggestions));
|
|
3591
|
+
}
|
|
3592
|
+
return blocks.join("\n");
|
|
3593
|
+
}
|
|
3594
|
+
function renderOutput2(blocks) {
|
|
3595
|
+
return blocks.filter(Boolean).join("\n");
|
|
3596
|
+
}
|
|
3597
|
+
function splitFullFlag(args) {
|
|
3598
|
+
return {
|
|
3599
|
+
args: args.filter((arg) => arg !== "--full"),
|
|
3600
|
+
full: args.includes("--full")
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
function trimSingleTrailingNewline(text) {
|
|
3604
|
+
return text.endsWith("\n") ? text.slice(0, -1) : text;
|
|
3605
|
+
}
|
|
3606
|
+
function wrapsRawStdout(argv) {
|
|
3607
|
+
return (argv ?? process.argv.slice(2))[0] === "run";
|
|
3608
|
+
}
|
|
3609
|
+
function wrapStdout(stdout, argv) {
|
|
3610
|
+
const target = stdout ?? process.stdout;
|
|
3611
|
+
if (!wrapsRawStdout(argv)) {
|
|
3612
|
+
return stdout;
|
|
3613
|
+
}
|
|
3614
|
+
return {
|
|
3615
|
+
write(chunk) {
|
|
3616
|
+
if (!chunk.startsWith(RAW_STDOUT_MARKER)) {
|
|
3617
|
+
return target.write(chunk);
|
|
3618
|
+
}
|
|
3619
|
+
const raw = chunk.slice(RAW_STDOUT_MARKER.length);
|
|
3620
|
+
if (raw === "\n") {
|
|
3621
|
+
return true;
|
|
3622
|
+
}
|
|
3623
|
+
return target.write(raw);
|
|
3624
|
+
}
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
3627
|
+
function renderUnknownCommand(command) {
|
|
3628
|
+
return renderError2(`Unknown command: ${command}`, "VALIDATION_ERROR", [
|
|
3629
|
+
"Run `chrome-devtools-axi --help` to see available commands"
|
|
3630
|
+
]) + "\n";
|
|
3631
|
+
}
|
|
3632
|
+
function normalizeMainOptions(options) {
|
|
3633
|
+
if (Array.isArray(options)) {
|
|
3634
|
+
return { argv: options };
|
|
3635
|
+
}
|
|
3636
|
+
return options ?? {};
|
|
3637
|
+
}
|
|
3638
|
+
function resolveArgv(argv) {
|
|
3639
|
+
return argv ?? process.argv.slice(2);
|
|
3640
|
+
}
|
|
3641
|
+
function shouldRenderFullHome(argv) {
|
|
3642
|
+
return argv.length === 1 && argv[0] === "--full";
|
|
3643
|
+
}
|
|
3644
|
+
function formatPageOutput(snapshot, command, url, full = false) {
|
|
3645
|
+
const title = extractTitle(snapshot);
|
|
3646
|
+
const refs = countRefs(snapshot);
|
|
3647
|
+
const blocks = [];
|
|
3648
|
+
const page = {};
|
|
3649
|
+
if (title) page.title = title;
|
|
3650
|
+
if (url) page.url = url;
|
|
3651
|
+
page.refs = refs;
|
|
3652
|
+
blocks.push(encode({ page }));
|
|
3653
|
+
const tr = truncateSnapshot(snapshot, full);
|
|
3654
|
+
let snapshotBlock = `snapshot:
|
|
3655
|
+
${tr.text.trimEnd()}`;
|
|
3656
|
+
if (tr.truncated) {
|
|
3657
|
+
snapshotBlock += `
|
|
3658
|
+
... (truncated, ${tr.totalLength} chars total)`;
|
|
3659
|
+
}
|
|
3660
|
+
blocks.push(snapshotBlock);
|
|
3661
|
+
const suggestions = getSuggestions({ command, url, snapshot });
|
|
3662
|
+
if (tr.truncated) {
|
|
3663
|
+
suggestions.push(
|
|
3664
|
+
`Run \`chrome-devtools-axi ${command}${url ? " " + url : ""} --full\` to see complete snapshot`
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
if (suggestions.length > 0) {
|
|
3668
|
+
blocks.push(renderHelp(suggestions));
|
|
3669
|
+
}
|
|
3670
|
+
return renderOutput2(blocks);
|
|
3671
|
+
}
|
|
3672
|
+
function stripSnapshotHeader2(text) {
|
|
3673
|
+
const lines = text.split("\n");
|
|
3674
|
+
const treeStart = lines.findIndex((l) => /\bRootWebArea\b|\buid=/.test(l));
|
|
3675
|
+
if (treeStart > 0) return lines.slice(treeStart).join("\n");
|
|
3676
|
+
return text.replace(/^[\s\S]*?##\s+Latest page snapshot\s*\n/, "");
|
|
3677
|
+
}
|
|
3678
|
+
function parseUid(arg) {
|
|
3679
|
+
const current = getCurrentGeneration();
|
|
3680
|
+
const check = checkUidGeneration(arg, current);
|
|
3681
|
+
if (check.stale) {
|
|
3682
|
+
throwStaleRef2(arg, check.refGeneration, current);
|
|
3683
|
+
}
|
|
3684
|
+
return check.uid;
|
|
3685
|
+
}
|
|
3686
|
+
async function stampFresh() {
|
|
3687
|
+
return captureFreshSnapshot(
|
|
3688
|
+
callTool,
|
|
3689
|
+
async () => stripSnapshotHeader2(await callTool("take_snapshot"))
|
|
3690
|
+
);
|
|
3691
|
+
}
|
|
3692
|
+
function throwStaleRef2(arg, refGeneration, currentGeneration) {
|
|
3693
|
+
const refRaw = arg.startsWith("@") ? arg.slice(1) : arg;
|
|
3694
|
+
throw new CdpError(
|
|
3695
|
+
`Stale ref @${refRaw}: from snapshot generation ${refGeneration}, current is ${currentGeneration}. Re-snapshot to get fresh refs.`,
|
|
3696
|
+
"STALE_REF",
|
|
3697
|
+
[
|
|
3698
|
+
"Run `chrome-devtools-axi snapshot` to capture current refs, then retry the action"
|
|
3699
|
+
]
|
|
3700
|
+
);
|
|
3701
|
+
}
|
|
3702
|
+
async function parseUidFresh2(arg, caller = callTool) {
|
|
3703
|
+
return parseUidFresh(arg, caller);
|
|
3704
|
+
}
|
|
3705
|
+
function isRecoverableOpenError2(error) {
|
|
3706
|
+
if (!(error instanceof CdpError)) return false;
|
|
3707
|
+
if (error.code !== "BROWSER_ERROR") return false;
|
|
3708
|
+
return /not connected|session (?:closed|not found)|no page/i.test(
|
|
3709
|
+
error.message
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
async function callWithSnapshot(name, args) {
|
|
3713
|
+
const result = await callTool(name, args);
|
|
3714
|
+
if (/^# Open dialog\b/m.test(result)) {
|
|
3715
|
+
throw new CdpError("The action opened a dialog. Do not replay it; handle the dialog first.", "DIALOG_PENDING", [
|
|
3716
|
+
"Run `chrome-devtools-axi dialog accept` or `chrome-devtools-axi dialog dismiss` in the same session"
|
|
3717
|
+
]);
|
|
3718
|
+
}
|
|
3719
|
+
return stampFresh();
|
|
3720
|
+
}
|
|
3721
|
+
var SCROLL_FUNCTIONS = {
|
|
3722
|
+
up: "() => window.scrollBy(0, -500)",
|
|
3723
|
+
down: "() => window.scrollBy(0, 500)",
|
|
3724
|
+
top: "() => window.scrollTo(0, 0)",
|
|
3725
|
+
bottom: "() => window.scrollTo(0, document.body.scrollHeight)"
|
|
3726
|
+
};
|
|
3727
|
+
async function handleOpen(args, full) {
|
|
3728
|
+
const url = args[0];
|
|
3729
|
+
if (!url) {
|
|
3730
|
+
throw new CdpError("Missing URL", "VALIDATION_ERROR", [
|
|
3731
|
+
"Run `chrome-devtools-axi open https://example.com` to navigate to a page"
|
|
3732
|
+
]);
|
|
3733
|
+
}
|
|
3734
|
+
try {
|
|
3735
|
+
await callTool("navigate_page", { type: "url", url });
|
|
3736
|
+
} catch (error) {
|
|
3737
|
+
if (!isRecoverableOpenError2(error)) {
|
|
3738
|
+
throw error;
|
|
3739
|
+
}
|
|
3740
|
+
await callTool("new_page", { url });
|
|
3741
|
+
}
|
|
3742
|
+
const snapshot = await stampFresh();
|
|
3743
|
+
return formatPageOutput(snapshot, "open", url, full);
|
|
3744
|
+
}
|
|
3745
|
+
async function handleSnapshot(full) {
|
|
3746
|
+
const snapshot = await stampFresh();
|
|
3747
|
+
return formatPageOutput(snapshot, "snapshot", void 0, full);
|
|
3748
|
+
}
|
|
3749
|
+
async function handleScreenshot(args) {
|
|
3750
|
+
const parsed = parseScreenshotArgs(args);
|
|
3751
|
+
if (!parsed.filePath) {
|
|
3752
|
+
throw new CdpError("Missing file path", "VALIDATION_ERROR", [
|
|
3753
|
+
"Run `chrome-devtools-axi screenshot ./page.png` to save a screenshot"
|
|
3754
|
+
]);
|
|
3755
|
+
}
|
|
3756
|
+
const filePath = resolveOutputPath(parsed.filePath);
|
|
3757
|
+
const toolArgs = { filePath };
|
|
3758
|
+
if (parsed.uid) toolArgs.uid = await parseUidFresh2(parsed.uid);
|
|
3759
|
+
if (parsed.fullPage) toolArgs.fullPage = true;
|
|
3760
|
+
if (parsed.format) toolArgs.format = parsed.format;
|
|
3761
|
+
const result = await callTool("take_screenshot", toolArgs);
|
|
3762
|
+
return formatScreenshotOutput(parseScreenshotOutputPath(result));
|
|
3763
|
+
}
|
|
3764
|
+
async function handleClick(args, full) {
|
|
3765
|
+
const uid = args[0];
|
|
3766
|
+
if (!uid) {
|
|
3767
|
+
throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
|
|
3768
|
+
"Run `chrome-devtools-axi click @<uid>` \u2014 get uid from snapshot"
|
|
3769
|
+
]);
|
|
3770
|
+
}
|
|
3771
|
+
const snapshot = await callWithSnapshot("click", {
|
|
3772
|
+
uid: await parseUidFresh2(uid)
|
|
3773
|
+
});
|
|
3774
|
+
return formatPageOutput(snapshot, "click", void 0, full);
|
|
3775
|
+
}
|
|
3776
|
+
async function handleFill(args, full) {
|
|
3777
|
+
const uid = args[0];
|
|
3778
|
+
const value = args.slice(1).join(" ");
|
|
3779
|
+
if (!uid) {
|
|
3780
|
+
throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
|
|
3781
|
+
'Run `chrome-devtools-axi fill @<uid> "text"` \u2014 get uid from snapshot'
|
|
3782
|
+
]);
|
|
3783
|
+
}
|
|
3784
|
+
if (!value) {
|
|
3785
|
+
throw new CdpError("Missing fill text", "VALIDATION_ERROR", [
|
|
3786
|
+
'Run `chrome-devtools-axi fill @<uid> "text"` to fill the field'
|
|
3787
|
+
]);
|
|
3788
|
+
}
|
|
3789
|
+
const snapshot = await callWithSnapshot("fill", {
|
|
3790
|
+
uid: await parseUidFresh2(uid),
|
|
3791
|
+
value
|
|
3792
|
+
});
|
|
3793
|
+
return formatPageOutput(snapshot, "fill", void 0, full);
|
|
3794
|
+
}
|
|
3795
|
+
async function handlePress(args, full) {
|
|
3796
|
+
const key = args[0];
|
|
3797
|
+
if (!key) {
|
|
3798
|
+
throw new CdpError("Missing key name", "VALIDATION_ERROR", [
|
|
3799
|
+
"Run `chrome-devtools-axi press Enter` to press a key"
|
|
3800
|
+
]);
|
|
3801
|
+
}
|
|
3802
|
+
const snapshot = await callWithSnapshot("press_key", { key });
|
|
3803
|
+
return formatPageOutput(snapshot, "press", void 0, full);
|
|
3804
|
+
}
|
|
3805
|
+
async function handleType(args, full) {
|
|
3806
|
+
const text = args.join(" ");
|
|
3807
|
+
if (!text) {
|
|
3808
|
+
throw new CdpError("Missing text", "VALIDATION_ERROR", [
|
|
3809
|
+
'Run `chrome-devtools-axi type "hello"` to type text'
|
|
3810
|
+
]);
|
|
3811
|
+
}
|
|
3812
|
+
await callTool("type_text", { text });
|
|
3813
|
+
const snapshot = await stampFresh();
|
|
3814
|
+
return formatPageOutput(snapshot, "type", void 0, full);
|
|
3815
|
+
}
|
|
3816
|
+
async function handleScroll(args, full) {
|
|
3817
|
+
const dir = (args[0] ?? "down").toLowerCase();
|
|
3818
|
+
const fn = SCROLL_FUNCTIONS[dir];
|
|
3819
|
+
if (!fn) {
|
|
3820
|
+
throw new CdpError(`Unknown scroll direction: ${dir}`, "VALIDATION_ERROR", [
|
|
3821
|
+
"Run `chrome-devtools-axi scroll down` \u2014 directions: up, down, top, bottom"
|
|
3822
|
+
]);
|
|
3823
|
+
}
|
|
3824
|
+
await callTool("evaluate_script", { function: fn });
|
|
3825
|
+
const snapshot = await stampFresh();
|
|
3826
|
+
return formatPageOutput(snapshot, "scroll", void 0, full);
|
|
3827
|
+
}
|
|
3828
|
+
async function handleBack(full) {
|
|
3829
|
+
await callTool("navigate_page", { type: "back" });
|
|
3830
|
+
const snapshot = await stampFresh();
|
|
3831
|
+
return formatPageOutput(snapshot, "back", void 0, full);
|
|
3832
|
+
}
|
|
3833
|
+
async function handleWait(args) {
|
|
3834
|
+
const target = args[0];
|
|
3835
|
+
if (!target) {
|
|
3836
|
+
throw new CdpError(
|
|
3837
|
+
"Missing wait target (milliseconds or text)",
|
|
3838
|
+
"VALIDATION_ERROR",
|
|
3839
|
+
[
|
|
3840
|
+
"Run `chrome-devtools-axi wait 2000` to wait 2 seconds",
|
|
3841
|
+
'Run `chrome-devtools-axi wait "Submit"` to wait for text to appear'
|
|
3842
|
+
]
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3845
|
+
const isNumeric = /^\d+$/.test(target);
|
|
3846
|
+
if (isNumeric) {
|
|
3847
|
+
await callTool("evaluate_script", {
|
|
3848
|
+
function: wrapJsExpression(`new Promise(r => setTimeout(r, ${target}))`)
|
|
3849
|
+
});
|
|
3850
|
+
} else {
|
|
3851
|
+
await callTool("wait_for", { text: [target] });
|
|
3852
|
+
}
|
|
3853
|
+
const blocks = [];
|
|
3854
|
+
blocks.push(encode({ waited: target }));
|
|
3855
|
+
const suggestions = getSuggestions({ command: "wait" });
|
|
3856
|
+
if (suggestions.length > 0) blocks.push(renderHelp(suggestions));
|
|
3857
|
+
return renderOutput2(blocks);
|
|
3858
|
+
}
|
|
3859
|
+
function parseEvalResult(output) {
|
|
3860
|
+
const jsonBlock = output.match(/```json\n([\s\S]*?)\n```/);
|
|
3861
|
+
if (jsonBlock) return jsonBlock[1].trim();
|
|
3862
|
+
const preamble = "Script ran on page and returned:";
|
|
3863
|
+
if (output.includes(preamble))
|
|
3864
|
+
return output.slice(output.indexOf(preamble) + preamble.length).trim();
|
|
3865
|
+
return output.trim();
|
|
3866
|
+
}
|
|
3867
|
+
async function handleEval(args, full) {
|
|
3868
|
+
const js = args.join(" ");
|
|
3869
|
+
if (!js) {
|
|
3870
|
+
throw new CdpError("Missing JavaScript expression", "VALIDATION_ERROR", [
|
|
3871
|
+
'Run `chrome-devtools-axi eval "document.title"` to evaluate JavaScript'
|
|
3872
|
+
]);
|
|
3873
|
+
}
|
|
3874
|
+
const output = await callTool("evaluate_script", {
|
|
3875
|
+
function: wrapJsExpression(js)
|
|
3876
|
+
});
|
|
3877
|
+
const blocks = [];
|
|
3878
|
+
const raw = parseEvalResult(output);
|
|
3879
|
+
const tr = full ? { text: raw, truncated: false, totalLength: raw.length } : truncateText(raw);
|
|
3880
|
+
blocks.push(encode({ result: tr.text }));
|
|
3881
|
+
const suggestions = getSuggestions({ command: "eval" });
|
|
3882
|
+
if (tr.truncated) {
|
|
3883
|
+
suggestions.push(
|
|
3884
|
+
"Result was truncated \u2014 re-run with --full flag, or use .slice() / filter in your JS expression"
|
|
3885
|
+
);
|
|
3886
|
+
}
|
|
3887
|
+
if (suggestions.length > 0) blocks.push(renderHelp(suggestions));
|
|
3888
|
+
return renderOutput2(blocks);
|
|
3889
|
+
}
|
|
3890
|
+
async function handleStart() {
|
|
3891
|
+
const port = await ensureBridge();
|
|
3892
|
+
return encode({ status: "ready", port });
|
|
3893
|
+
}
|
|
3894
|
+
function formatStopOutput(wasStopped) {
|
|
3895
|
+
return encode({ status: wasStopped ? "stopped" : "stopped (no-op)" });
|
|
3896
|
+
}
|
|
3897
|
+
async function handleStop() {
|
|
3898
|
+
const wasStopped = await stopBridge();
|
|
3899
|
+
return formatStopOutput(wasStopped);
|
|
3900
|
+
}
|
|
3901
|
+
async function handlePages() {
|
|
3902
|
+
const result = await callTool("list_pages");
|
|
3903
|
+
const pages = overlaySessionSelected(parsePagesList(result));
|
|
3904
|
+
if (pages.length === 0) {
|
|
3905
|
+
return "pages: 0 pages open";
|
|
3906
|
+
}
|
|
3907
|
+
const blocks = [];
|
|
3908
|
+
const header = `pages[${pages.length}]{id,url,selected}:`;
|
|
3909
|
+
const rows = pages.map((p) => ` ${p.id},${p.url},${p.selected}`);
|
|
3910
|
+
blocks.push(`${header}
|
|
3911
|
+
${rows.join("\n")}`);
|
|
3912
|
+
blocks.push(
|
|
3913
|
+
renderHelp([
|
|
3914
|
+
"Run `chrome-devtools-axi selectpage <id>` to switch tabs",
|
|
3915
|
+
"Run `chrome-devtools-axi newpage <url>` to open a new tab"
|
|
3916
|
+
])
|
|
3917
|
+
);
|
|
3918
|
+
return renderOutput2(blocks);
|
|
3919
|
+
}
|
|
3920
|
+
async function handleNewPage(args, full) {
|
|
3921
|
+
const url = args.filter((a) => !a.startsWith("--"))[0];
|
|
3922
|
+
if (!url) {
|
|
3923
|
+
throw new CdpError("Missing URL", "VALIDATION_ERROR", [
|
|
3924
|
+
"Run `chrome-devtools-axi newpage https://example.com` to open a new tab"
|
|
3925
|
+
]);
|
|
3926
|
+
}
|
|
3927
|
+
const background = args.includes("--background");
|
|
3928
|
+
const toolArgs = { url };
|
|
3929
|
+
if (background) toolArgs.background = true;
|
|
3930
|
+
await callTool("new_page", toolArgs);
|
|
3931
|
+
const snapshot = await stampFresh();
|
|
3932
|
+
return formatPageOutput(snapshot, "newpage", url, full);
|
|
3933
|
+
}
|
|
3934
|
+
async function handleSelectPage(args, full) {
|
|
3935
|
+
const id = args[0];
|
|
3936
|
+
if (!id) {
|
|
3937
|
+
throw new CdpError("Missing page ID", "VALIDATION_ERROR", [
|
|
3938
|
+
"Run `chrome-devtools-axi selectpage <id>` \u2014 get ID from `pages` command"
|
|
3939
|
+
]);
|
|
3940
|
+
}
|
|
3941
|
+
const pageId = parseInt(id, 10);
|
|
3942
|
+
if (isNaN(pageId)) {
|
|
3943
|
+
throw new CdpError(`Invalid page ID: ${id}`, "VALIDATION_ERROR", [
|
|
3944
|
+
"Run `chrome-devtools-axi pages` to list available page IDs"
|
|
3945
|
+
]);
|
|
3946
|
+
}
|
|
3947
|
+
await callTool("select_page", { pageId });
|
|
3948
|
+
const snapshot = await stampFresh();
|
|
3949
|
+
return formatPageOutput(snapshot, "selectpage", void 0, full);
|
|
3950
|
+
}
|
|
3951
|
+
async function handleClosePage(args) {
|
|
3952
|
+
const id = args[0];
|
|
3953
|
+
if (!id) {
|
|
3954
|
+
throw new CdpError("Missing page ID", "VALIDATION_ERROR", [
|
|
3955
|
+
"Run `chrome-devtools-axi closepage <id>` \u2014 get ID from `pages` command"
|
|
3956
|
+
]);
|
|
3957
|
+
}
|
|
3958
|
+
const pageId = parseInt(id, 10);
|
|
3959
|
+
if (isNaN(pageId)) {
|
|
3960
|
+
throw new CdpError(`Invalid page ID: ${id}`, "VALIDATION_ERROR", [
|
|
3961
|
+
"Run `chrome-devtools-axi pages` to list available page IDs"
|
|
3962
|
+
]);
|
|
3963
|
+
}
|
|
3964
|
+
const beforeResult = await callTool("list_pages");
|
|
3965
|
+
const pagesBefore = parsePagesList(beforeResult);
|
|
3966
|
+
if (pagesBefore.length <= 1) {
|
|
3967
|
+
const blocks = [
|
|
3968
|
+
encode({ status: "cannot close the last open page (no-op)" })
|
|
3969
|
+
];
|
|
3970
|
+
blocks.push(
|
|
3971
|
+
renderHelp([
|
|
3972
|
+
"Run `chrome-devtools-axi newpage <url>` to open another tab first",
|
|
3973
|
+
"Run `chrome-devtools-axi stop` to shut down the browser entirely"
|
|
3974
|
+
])
|
|
3975
|
+
);
|
|
3976
|
+
return renderOutput2(blocks);
|
|
3977
|
+
}
|
|
3978
|
+
await callTool("close_page", { pageId });
|
|
3979
|
+
return encode({ status: "closed", pageId });
|
|
3980
|
+
}
|
|
3981
|
+
async function handleResize(args) {
|
|
3982
|
+
const [widthStr, heightStr] = args;
|
|
3983
|
+
if (!widthStr || !heightStr) {
|
|
3984
|
+
throw new CdpError("Missing width and/or height", "VALIDATION_ERROR", [
|
|
3985
|
+
"Run `chrome-devtools-axi resize 1280 720` to resize the viewport"
|
|
3986
|
+
]);
|
|
3987
|
+
}
|
|
3988
|
+
const width = parseInt(widthStr, 10);
|
|
3989
|
+
const height = parseInt(heightStr, 10);
|
|
3990
|
+
if (isNaN(width) || isNaN(height)) {
|
|
3991
|
+
throw new CdpError("Width and height must be numbers", "VALIDATION_ERROR", [
|
|
3992
|
+
"Run `chrome-devtools-axi resize 1280 720` to resize the viewport"
|
|
3993
|
+
]);
|
|
3994
|
+
}
|
|
3995
|
+
await callTool("resize_page", { width, height });
|
|
3996
|
+
const result = await callTool("evaluate_script", {
|
|
3997
|
+
function: "() => ({width: innerWidth, height: innerHeight, devicePixelRatio})"
|
|
3998
|
+
});
|
|
3999
|
+
const match = result.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
4000
|
+
let actual;
|
|
4001
|
+
try {
|
|
4002
|
+
actual = match ? JSON.parse(match[1]) : void 0;
|
|
4003
|
+
} catch {
|
|
4004
|
+
}
|
|
4005
|
+
return encode({
|
|
4006
|
+
resized: {
|
|
4007
|
+
requested: { width, height },
|
|
4008
|
+
actual: actual ?? null,
|
|
4009
|
+
mode: "window",
|
|
4010
|
+
exact: actual?.width === width && actual?.height === height
|
|
4011
|
+
}
|
|
4012
|
+
});
|
|
4013
|
+
}
|
|
4014
|
+
async function handleHover(args, full) {
|
|
4015
|
+
const uid = args[0];
|
|
4016
|
+
if (!uid) {
|
|
4017
|
+
throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
|
|
4018
|
+
"Run `chrome-devtools-axi hover @<uid>` \u2014 get uid from snapshot"
|
|
4019
|
+
]);
|
|
4020
|
+
}
|
|
4021
|
+
const snapshot = await callWithSnapshot("hover", {
|
|
4022
|
+
uid: await parseUidFresh2(uid)
|
|
4023
|
+
});
|
|
4024
|
+
return formatPageOutput(snapshot, "hover", void 0, full);
|
|
4025
|
+
}
|
|
4026
|
+
async function handleDrag(args, full) {
|
|
4027
|
+
const from = args[0];
|
|
4028
|
+
const to = args[1];
|
|
4029
|
+
if (!from || !to) {
|
|
4030
|
+
throw new CdpError("Missing element refs", "VALIDATION_ERROR", [
|
|
4031
|
+
"Run `chrome-devtools-axi drag @<from> @<to>` \u2014 get uids from snapshot"
|
|
4032
|
+
]);
|
|
4033
|
+
}
|
|
4034
|
+
const snapshot = await callWithSnapshot("drag", {
|
|
4035
|
+
from_uid: await parseUidFresh2(from),
|
|
4036
|
+
to_uid: await parseUidFresh2(to)
|
|
4037
|
+
});
|
|
4038
|
+
return formatPageOutput(snapshot, "drag", void 0, full);
|
|
4039
|
+
}
|
|
4040
|
+
async function handleFillForm(args, full) {
|
|
4041
|
+
const { entries } = parseFillFormArgs(args);
|
|
4042
|
+
if (entries.length === 0) {
|
|
4043
|
+
throw new CdpError("No valid field entries", "VALIDATION_ERROR", [
|
|
4044
|
+
'Run `chrome-devtools-axi fillform @g1:1="hello" @g1:2="world"` to fill multiple fields'
|
|
4045
|
+
]);
|
|
4046
|
+
}
|
|
4047
|
+
const validated = await Promise.all(
|
|
4048
|
+
entries.map(async (e) => ({
|
|
4049
|
+
uid: await parseUidFresh2(e.uid),
|
|
4050
|
+
value: e.value
|
|
4051
|
+
}))
|
|
4052
|
+
);
|
|
4053
|
+
const snapshot = await callWithSnapshot("fill_form", { elements: validated });
|
|
4054
|
+
return formatPageOutput(snapshot, "fillform", void 0, full);
|
|
4055
|
+
}
|
|
4056
|
+
async function handleDialog(args) {
|
|
4057
|
+
const action = args[0];
|
|
4058
|
+
if (!action || action !== "accept" && action !== "dismiss") {
|
|
4059
|
+
throw new CdpError("Missing or invalid action", "VALIDATION_ERROR", [
|
|
4060
|
+
"Run `chrome-devtools-axi dialog accept` or `chrome-devtools-axi dialog dismiss`"
|
|
4061
|
+
]);
|
|
4062
|
+
}
|
|
4063
|
+
const params = { action };
|
|
4064
|
+
const promptText = args.slice(1).join(" ");
|
|
4065
|
+
if (promptText) params.promptText = promptText;
|
|
4066
|
+
const snapshot = await callWithSnapshot("handle_dialog", params);
|
|
4067
|
+
return encode({ dialog: action }) + "\n" + formatPageOutput(snapshot, "dialog");
|
|
4068
|
+
}
|
|
4069
|
+
async function handleUpload(args, full) {
|
|
4070
|
+
const uid = args[0];
|
|
4071
|
+
const filePath = args[1];
|
|
4072
|
+
if (!uid) {
|
|
4073
|
+
throw new CdpError("Missing element ref", "VALIDATION_ERROR", [
|
|
4074
|
+
"Run `chrome-devtools-axi upload @<uid> <path>` \u2014 get uid from snapshot"
|
|
4075
|
+
]);
|
|
4076
|
+
}
|
|
4077
|
+
if (!filePath) {
|
|
4078
|
+
throw new CdpError("Missing file path", "VALIDATION_ERROR", [
|
|
4079
|
+
"Run `chrome-devtools-axi upload @<uid> /path/to/file` to upload a file"
|
|
4080
|
+
]);
|
|
4081
|
+
}
|
|
4082
|
+
const snapshot = await callWithSnapshot("upload_file", {
|
|
4083
|
+
uid: await parseUidFresh2(uid),
|
|
4084
|
+
filePaths: [resolveOutputPath(filePath)]
|
|
4085
|
+
});
|
|
4086
|
+
return formatPageOutput(snapshot, "upload", void 0, full);
|
|
4087
|
+
}
|
|
4088
|
+
async function handleEmulate(args) {
|
|
4089
|
+
const parsed = parseEmulateArgs(args);
|
|
4090
|
+
await callTool("emulate", parsed);
|
|
4091
|
+
return encode({ emulated: parsed });
|
|
4092
|
+
}
|
|
4093
|
+
async function handleConsole(args) {
|
|
4094
|
+
const parsed = parseConsoleArgs(args);
|
|
4095
|
+
const result = await callTool("list_console_messages", parsed);
|
|
4096
|
+
return formatMcpResult("console", result, [
|
|
4097
|
+
"Run `chrome-devtools-axi console-get <id>` to see a specific message",
|
|
4098
|
+
"Run `chrome-devtools-axi console --type error` to filter by type"
|
|
4099
|
+
]);
|
|
4100
|
+
}
|
|
4101
|
+
async function handleConsoleGet(args) {
|
|
4102
|
+
const id = args[0];
|
|
4103
|
+
if (!id) {
|
|
4104
|
+
throw new CdpError("Missing console message id", "VALIDATION_ERROR", [
|
|
4105
|
+
"Run `chrome-devtools-axi console-get <id>` \u2014 get id from `chrome-devtools-axi console`"
|
|
4106
|
+
]);
|
|
4107
|
+
}
|
|
4108
|
+
const msgid = parseOptionalInteger(id);
|
|
4109
|
+
if (msgid === void 0) {
|
|
4110
|
+
throw new CdpError(
|
|
4111
|
+
`Invalid console message id: ${id}`,
|
|
4112
|
+
"VALIDATION_ERROR",
|
|
4113
|
+
["Run `chrome-devtools-axi console` to list available message ids"]
|
|
4114
|
+
);
|
|
4115
|
+
}
|
|
4116
|
+
const result = await callTool("get_console_message", { msgid });
|
|
4117
|
+
return formatMcpResult("message", result, []);
|
|
4118
|
+
}
|
|
4119
|
+
async function handleNetwork(args) {
|
|
4120
|
+
const parsed = parseNetworkArgs(args);
|
|
4121
|
+
const result = await callTool("list_network_requests", parsed);
|
|
4122
|
+
return formatMcpResult("network", result, [
|
|
4123
|
+
"Run `chrome-devtools-axi network-get <id>` to see request details",
|
|
4124
|
+
"Run `chrome-devtools-axi network --type fetch` to filter by type"
|
|
4125
|
+
]);
|
|
4126
|
+
}
|
|
4127
|
+
async function handleNetworkGet(args) {
|
|
4128
|
+
const parsed = parseNetworkGetArgs(args);
|
|
4129
|
+
const toolArgs = { ...parsed };
|
|
4130
|
+
if (toolArgs.responseFilePath) {
|
|
4131
|
+
toolArgs.responseFilePath = resolveOutputPath(toolArgs.responseFilePath);
|
|
4132
|
+
}
|
|
4133
|
+
if (toolArgs.requestFilePath) {
|
|
4134
|
+
toolArgs.requestFilePath = resolveOutputPath(toolArgs.requestFilePath);
|
|
4135
|
+
}
|
|
4136
|
+
const result = await callTool("get_network_request", toolArgs);
|
|
4137
|
+
return formatMcpResult("request", result, []);
|
|
4138
|
+
}
|
|
4139
|
+
async function handleLighthouse(args) {
|
|
4140
|
+
const opts = parseLighthouseArgs(args);
|
|
4141
|
+
if (opts.outputDirPath) {
|
|
4142
|
+
opts.outputDirPath = resolveOutputPath(opts.outputDirPath);
|
|
4143
|
+
}
|
|
4144
|
+
const result = await callTool("lighthouse_audit", opts);
|
|
4145
|
+
return formatMcpResult("lighthouse", result, []);
|
|
4146
|
+
}
|
|
4147
|
+
async function handlePerfStart(args) {
|
|
4148
|
+
const opts = parsePerfStartArgs(args);
|
|
4149
|
+
if (opts.filePath) opts.filePath = resolveOutputPath(opts.filePath);
|
|
4150
|
+
await callTool("performance_start_trace", opts);
|
|
4151
|
+
return encode({ trace: "started", ...opts });
|
|
4152
|
+
}
|
|
4153
|
+
async function handlePerfStop(args) {
|
|
4154
|
+
const toolArgs = {};
|
|
4155
|
+
for (let i = 0; i < args.length; i++) {
|
|
4156
|
+
if (args[i] === "--file" && i + 1 < args.length) {
|
|
4157
|
+
toolArgs.filePath = resolveOutputPath(args[++i]);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
const result = await callTool("performance_stop_trace", toolArgs);
|
|
4161
|
+
return formatMcpResult("trace", result, [
|
|
4162
|
+
"Run `chrome-devtools-axi perf-insight <set-id> <insight-name>` to analyze insights"
|
|
4163
|
+
]);
|
|
4164
|
+
}
|
|
4165
|
+
async function handlePerfInsight(args) {
|
|
4166
|
+
const [setId, insightName] = args;
|
|
4167
|
+
if (!setId || !insightName) {
|
|
4168
|
+
throw new CdpError("Missing required arguments", "VALIDATION_ERROR", [
|
|
4169
|
+
"Run `chrome-devtools-axi perf-insight <set-id> <insight-name>` to analyze an insight"
|
|
4170
|
+
]);
|
|
4171
|
+
}
|
|
4172
|
+
const result = await callTool("performance_analyze_insight", {
|
|
4173
|
+
insightSetId: setId,
|
|
4174
|
+
insightName
|
|
4175
|
+
});
|
|
4176
|
+
return formatMcpResult("insight", result, []);
|
|
4177
|
+
}
|
|
4178
|
+
async function handleHeap(args) {
|
|
4179
|
+
const rawPath = args[0];
|
|
4180
|
+
if (!rawPath) {
|
|
4181
|
+
throw new CdpError("Missing file path", "VALIDATION_ERROR", [
|
|
4182
|
+
"Run `chrome-devtools-axi heap ./snapshot.heapsnapshot` to take a heap snapshot"
|
|
4183
|
+
]);
|
|
4184
|
+
}
|
|
4185
|
+
const filePath = resolveOutputPath(rawPath);
|
|
4186
|
+
await callTool("take_heapsnapshot", { filePath });
|
|
4187
|
+
return encode({ heap: filePath });
|
|
4188
|
+
}
|
|
4189
|
+
async function handleRun() {
|
|
4190
|
+
if (process.stdin.isTTY) {
|
|
4191
|
+
throw new CdpError("No script provided on stdin", "VALIDATION_ERROR", [
|
|
4192
|
+
"Pipe a script: chrome-devtools-axi run <<'EOF'\\n...\\nEOF"
|
|
4193
|
+
]);
|
|
4194
|
+
}
|
|
4195
|
+
const content = await readStdin();
|
|
4196
|
+
if (!content.trim()) {
|
|
4197
|
+
throw new CdpError("Empty script on stdin", "VALIDATION_ERROR", [
|
|
4198
|
+
"Pipe a script: chrome-devtools-axi run <<'EOF'\\n...\\nEOF"
|
|
4199
|
+
]);
|
|
4200
|
+
}
|
|
4201
|
+
const result = await runScript(content, callTool);
|
|
4202
|
+
return RAW_STDOUT_MARKER + trimSingleTrailingNewline(result.stdout);
|
|
4203
|
+
}
|
|
4204
|
+
async function handleSetup(args) {
|
|
4205
|
+
if (args.length !== 1 || args[0] !== "hooks") {
|
|
4206
|
+
throw new CdpError("Unknown setup action", "VALIDATION_ERROR", [
|
|
4207
|
+
"Run `chrome-devtools-axi setup hooks`"
|
|
4208
|
+
]);
|
|
4209
|
+
}
|
|
4210
|
+
installHooksOrThrow();
|
|
4211
|
+
return renderOutput2([
|
|
4212
|
+
"hooks:\n status: installed\n integrations: Claude Code, Codex, OpenCode",
|
|
4213
|
+
renderHelp([
|
|
4214
|
+
"Restart your agent session to receive chrome-devtools-axi ambient context"
|
|
4215
|
+
])
|
|
4216
|
+
]);
|
|
4217
|
+
}
|
|
4218
|
+
async function handleHome(_full) {
|
|
4219
|
+
const result = await getSessionSnapshotIfRunning();
|
|
4220
|
+
if (!result) {
|
|
4221
|
+
return renderOutput2([
|
|
4222
|
+
encode({ browser: "no active session" }),
|
|
4223
|
+
renderHelp(["Run `chrome-devtools-axi open <url>` to start browsing"])
|
|
4224
|
+
]);
|
|
4225
|
+
}
|
|
4226
|
+
const snapshot = stripSnapshotHeader2(result);
|
|
4227
|
+
const title = extractTitle(snapshot);
|
|
4228
|
+
const refs = countRefs(snapshot);
|
|
4229
|
+
const page = {};
|
|
4230
|
+
if (title) page.title = title;
|
|
4231
|
+
page.refs = refs;
|
|
4232
|
+
const help = [
|
|
4233
|
+
"Run `chrome-devtools-axi snapshot` to see page content",
|
|
4234
|
+
"Run `chrome-devtools-axi open <url>` to navigate to a URL",
|
|
4235
|
+
"Run `chrome-devtools-axi --help` to see full command list"
|
|
4236
|
+
];
|
|
4237
|
+
return renderOutput2([encode({ page }), renderHelp(help)]);
|
|
4238
|
+
}
|
|
4239
|
+
function withFullFlag(handler) {
|
|
4240
|
+
return (args) => {
|
|
4241
|
+
const parsed = splitFullFlag(args);
|
|
4242
|
+
return handler(parsed.args, parsed.full);
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4245
|
+
function withoutFullFlag(handler) {
|
|
4246
|
+
return (args) => handler(splitFullFlag(args).args);
|
|
4247
|
+
}
|
|
4248
|
+
function validateCommandFlags(command, args, allowedFlags, valueFlags, dashPositionalSlots, positionalArgsBeforeText = args.length) {
|
|
4249
|
+
let positionalArgs = 0;
|
|
4250
|
+
for (let i = 0; i < args.length; i++) {
|
|
4251
|
+
const arg = args[i];
|
|
4252
|
+
if (positionalArgs === positionalArgsBeforeText) return;
|
|
4253
|
+
if (arg === "--help" || allowedFlags.includes(arg)) {
|
|
4254
|
+
if (valueFlags.includes(arg)) i += 1;
|
|
4255
|
+
continue;
|
|
4256
|
+
}
|
|
4257
|
+
if (arg !== "-" && arg.startsWith("-") && (arg.startsWith("--") || !dashPositionalSlots.includes(positionalArgs))) {
|
|
4258
|
+
throw new CdpError(
|
|
4259
|
+
`Unknown flag ${arg} for \`${command}\``,
|
|
4260
|
+
"VALIDATION_ERROR",
|
|
4261
|
+
[`Run \`chrome-devtools-axi ${command} --help\` to see valid flags`]
|
|
4262
|
+
);
|
|
4263
|
+
}
|
|
4264
|
+
positionalArgs += 1;
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
var COMMAND_HANDLERS = {
|
|
4268
|
+
open: withFullFlag(handleOpen),
|
|
4269
|
+
snapshot: async (args) => handleSnapshot(splitFullFlag(args).full),
|
|
4270
|
+
screenshot: withoutFullFlag(handleScreenshot),
|
|
4271
|
+
click: withFullFlag(handleClick),
|
|
4272
|
+
fill: withFullFlag(handleFill),
|
|
4273
|
+
type: withFullFlag(handleType),
|
|
4274
|
+
press: withFullFlag(handlePress),
|
|
4275
|
+
scroll: withFullFlag(handleScroll),
|
|
4276
|
+
back: async (args) => handleBack(splitFullFlag(args).full),
|
|
4277
|
+
wait: withoutFullFlag(handleWait),
|
|
4278
|
+
eval: withFullFlag(handleEval),
|
|
4279
|
+
run: async () => handleRun(),
|
|
4280
|
+
hover: withFullFlag(handleHover),
|
|
4281
|
+
drag: withFullFlag(handleDrag),
|
|
4282
|
+
fillform: withFullFlag(handleFillForm),
|
|
4283
|
+
dialog: withoutFullFlag(handleDialog),
|
|
4284
|
+
upload: withFullFlag(handleUpload),
|
|
4285
|
+
pages: async () => handlePages(),
|
|
4286
|
+
newpage: withFullFlag(handleNewPage),
|
|
4287
|
+
selectpage: withFullFlag(handleSelectPage),
|
|
4288
|
+
closepage: withoutFullFlag(handleClosePage),
|
|
4289
|
+
resize: withoutFullFlag(handleResize),
|
|
4290
|
+
emulate: withoutFullFlag(handleEmulate),
|
|
4291
|
+
console: withoutFullFlag(handleConsole),
|
|
4292
|
+
"console-get": withoutFullFlag(handleConsoleGet),
|
|
4293
|
+
network: withoutFullFlag(handleNetwork),
|
|
4294
|
+
"network-get": withoutFullFlag(handleNetworkGet),
|
|
4295
|
+
lighthouse: withoutFullFlag(handleLighthouse),
|
|
4296
|
+
"perf-start": withoutFullFlag(handlePerfStart),
|
|
4297
|
+
"perf-stop": withoutFullFlag(handlePerfStop),
|
|
4298
|
+
"perf-insight": withoutFullFlag(handlePerfInsight),
|
|
4299
|
+
heap: withoutFullFlag(handleHeap),
|
|
4300
|
+
start: async () => handleStart(),
|
|
4301
|
+
stop: async () => handleStop(),
|
|
4302
|
+
setup: withoutFullFlag(handleSetup)
|
|
4303
|
+
};
|
|
4304
|
+
var COMMAND_FLAGS = {
|
|
4305
|
+
open: ["--full"],
|
|
4306
|
+
screenshot: ["--uid", "--full-page", "--format"],
|
|
4307
|
+
snapshot: ["--full"],
|
|
4308
|
+
click: ["--full"],
|
|
4309
|
+
fill: ["--full"],
|
|
4310
|
+
type: ["--full"],
|
|
4311
|
+
press: ["--full"],
|
|
4312
|
+
scroll: ["--full"],
|
|
4313
|
+
back: ["--full"],
|
|
4314
|
+
wait: [],
|
|
4315
|
+
eval: ["--full"],
|
|
4316
|
+
run: [],
|
|
4317
|
+
hover: ["--full"],
|
|
4318
|
+
drag: ["--full"],
|
|
4319
|
+
fillform: ["--full"],
|
|
4320
|
+
dialog: [],
|
|
4321
|
+
upload: ["--full"],
|
|
4322
|
+
pages: [],
|
|
4323
|
+
newpage: ["--background", "--full"],
|
|
4324
|
+
selectpage: ["--full"],
|
|
4325
|
+
closepage: [],
|
|
4326
|
+
resize: [],
|
|
4327
|
+
emulate: [
|
|
4328
|
+
"--viewport",
|
|
4329
|
+
"--color-scheme",
|
|
4330
|
+
"--network",
|
|
4331
|
+
"--cpu",
|
|
4332
|
+
"--geolocation",
|
|
4333
|
+
"--user-agent"
|
|
4334
|
+
],
|
|
4335
|
+
console: ["--type", "--limit", "--page"],
|
|
4336
|
+
"console-get": [],
|
|
4337
|
+
network: ["--type", "--limit", "--page"],
|
|
4338
|
+
"network-get": ["--response-file", "--request-file"],
|
|
4339
|
+
lighthouse: ["--device", "--mode", "--output-dir"],
|
|
4340
|
+
"perf-start": ["--no-reload", "--no-auto-stop", "--file"],
|
|
4341
|
+
"perf-stop": ["--file"],
|
|
4342
|
+
"perf-insight": [],
|
|
4343
|
+
heap: [],
|
|
4344
|
+
start: [],
|
|
4345
|
+
stop: [],
|
|
4346
|
+
setup: []
|
|
4347
|
+
};
|
|
4348
|
+
var COMMAND_VALUE_FLAGS = {
|
|
4349
|
+
screenshot: ["--uid", "--format"],
|
|
4350
|
+
emulate: COMMAND_FLAGS.emulate,
|
|
4351
|
+
console: COMMAND_FLAGS.console,
|
|
4352
|
+
network: COMMAND_FLAGS.network,
|
|
4353
|
+
"network-get": COMMAND_FLAGS["network-get"],
|
|
4354
|
+
lighthouse: COMMAND_FLAGS.lighthouse,
|
|
4355
|
+
"perf-start": ["--file"],
|
|
4356
|
+
"perf-stop": COMMAND_FLAGS["perf-stop"]
|
|
4357
|
+
};
|
|
4358
|
+
var COMMAND_POSITIONAL_TEXT_START = {
|
|
4359
|
+
fill: 1,
|
|
4360
|
+
type: 0,
|
|
4361
|
+
wait: 0,
|
|
4362
|
+
eval: 0,
|
|
4363
|
+
dialog: 1
|
|
4364
|
+
};
|
|
4365
|
+
var COMMAND_DASH_POSITIONAL_SLOTS = {
|
|
4366
|
+
heap: [0],
|
|
4367
|
+
upload: [1],
|
|
4368
|
+
screenshot: [0]
|
|
4369
|
+
};
|
|
4370
|
+
var COMMANDS = Object.fromEntries(
|
|
4371
|
+
Object.entries(COMMAND_HANDLERS).map(([command, handler]) => [
|
|
4372
|
+
command,
|
|
4373
|
+
(args) => {
|
|
4374
|
+
validateCommandFlags(
|
|
4375
|
+
command,
|
|
4376
|
+
args,
|
|
4377
|
+
COMMAND_FLAGS[command] ?? [],
|
|
4378
|
+
COMMAND_VALUE_FLAGS[command] ?? [],
|
|
4379
|
+
COMMAND_DASH_POSITIONAL_SLOTS[command] ?? [],
|
|
4380
|
+
COMMAND_POSITIONAL_TEXT_START[command]
|
|
4381
|
+
);
|
|
4382
|
+
return handler(args);
|
|
4383
|
+
}
|
|
4384
|
+
])
|
|
4385
|
+
);
|
|
4386
|
+
async function main(options = {}) {
|
|
4387
|
+
const normalized = normalizeMainOptions(options);
|
|
4388
|
+
const requestedArgv = resolveArgv(normalized.argv);
|
|
4389
|
+
const homeFull = shouldRenderFullHome(requestedArgv);
|
|
4390
|
+
const argv = homeFull ? [] : normalized.argv;
|
|
4391
|
+
const stdout = wrapStdout(normalized.stdout, argv);
|
|
4392
|
+
await runAxiCli({
|
|
4393
|
+
...argv ? { argv } : {},
|
|
4394
|
+
...stdout ? { stdout } : {},
|
|
4395
|
+
description: HOME_DESCRIPTION,
|
|
4396
|
+
version: VERSION,
|
|
4397
|
+
topLevelHelp: TOP_HELP,
|
|
4398
|
+
home: async (args) => handleHome(homeFull || splitFullFlag(args).full),
|
|
4399
|
+
commands: COMMANDS,
|
|
4400
|
+
getCommandHelp,
|
|
4401
|
+
renderUnknownCommand
|
|
4402
|
+
});
|
|
4403
|
+
}
|
|
4404
|
+
export {
|
|
4405
|
+
HOME_DESCRIPTION,
|
|
4406
|
+
TOP_HELP,
|
|
4407
|
+
formatMcpResult,
|
|
4408
|
+
formatScreenshotOutput,
|
|
4409
|
+
formatStopOutput,
|
|
4410
|
+
getCommandHelp,
|
|
4411
|
+
main,
|
|
4412
|
+
parseConsoleArgs,
|
|
4413
|
+
parseEmulateArgs,
|
|
4414
|
+
parseFillFormArgs,
|
|
4415
|
+
parseLighthouseArgs,
|
|
4416
|
+
parseNetworkArgs,
|
|
4417
|
+
parseNetworkGetArgs,
|
|
4418
|
+
parsePagesList,
|
|
4419
|
+
parsePerfStartArgs,
|
|
4420
|
+
parseScreenshotArgs,
|
|
4421
|
+
parseUid,
|
|
4422
|
+
parseUidFresh2 as parseUidFresh,
|
|
4423
|
+
wrapJsExpression
|
|
4424
|
+
};
|