@xbrowser/cli 0.15.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{browser-R7B255ML.js → browser-53KUFEEM.js} +5 -1
- package/dist/{browser-I2HJZ7IP.js → browser-DSVV4GHS.js} +2 -2
- package/dist/{browser-GWBH6OJK.js → browser-GURRY444.js} +3 -1
- package/dist/cdp-driver-MNPR3HZH.js +2537 -0
- package/dist/cdp-driver-SSXUGXP6.js +47 -0
- package/dist/{chunk-2ONMTDLK.js → chunk-2BQZIT3S.js} +2535 -50
- package/dist/{chunk-KDYXFLAC.js → chunk-2MFXKN32.js} +2 -2
- package/dist/chunk-42RPMJ76.js +2530 -0
- package/dist/{chunk-F3ZWFCJJ.js → chunk-E4O5ZU3H.js} +2535 -50
- package/dist/{chunk-ATFTAKMN.js → chunk-IDVD44ED.js} +20 -0
- package/dist/chunk-T4J4C2NZ.js +250 -0
- package/dist/{chunk-RS6YYWTK.js → chunk-YKOHDEFV.js} +73 -38
- package/dist/cli.js +1176 -281
- package/dist/{convert-4DUWZIKH.js → convert-EGFYNICZ.js} +2 -0
- package/dist/{daemon-client-GX2UYIW4.js → daemon-client-3VM7VU7O.js} +22 -0
- package/dist/{daemon-client-3IJD6X4B.js → daemon-client-YAVQ343A.js} +7 -1
- package/dist/daemon-main.js +1004 -161
- package/dist/{extract-EGRXZSSK.js → extract-L2IW3IUB.js} +2 -0
- package/dist/{filter-OLAE26HN.js → filter-HC4RA7JY.js} +2 -0
- package/dist/index.d.ts +581 -41
- package/dist/index.js +1220 -321
- package/dist/launcher-KA7J32K5.js +19 -0
- package/dist/{network-store-YAF5OIBH.js → network-store-66A2RATI.js} +1 -0
- package/dist/{session-recorder-XET3DNML.js → session-recorder-MA75PKTQ.js} +1 -1
- package/package.json +3 -4
- package/dist/daemon-client-XWSSQBEA.js +0 -58
- package/dist/network-store-2S5HATEV.js +0 -194
- package/dist/parse-action-dsl-DRSPBALP.js +0 -72
- package/dist/screenshot-MB6R7RSS.js +0 -26
- package/dist/session-recorder-ILSSV2UC.js +0 -6
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
SessionRecorder
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2MFXKN32.js";
|
|
5
5
|
import {
|
|
6
6
|
closeAllSessions,
|
|
7
7
|
closeEphemeralContext,
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
resolveLaunchOpts,
|
|
18
18
|
saveSessionDiskMeta,
|
|
19
19
|
setActivePage
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-2BQZIT3S.js";
|
|
21
|
+
import "./chunk-T4J4C2NZ.js";
|
|
21
22
|
import {
|
|
22
23
|
forwardCommandLog,
|
|
23
24
|
forwardNetworkAnalyze,
|
|
@@ -46,7 +47,8 @@ import {
|
|
|
46
47
|
killAllDaemonProcesses,
|
|
47
48
|
startDaemonProcess,
|
|
48
49
|
stopDaemonProcess
|
|
49
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-IDVD44ED.js";
|
|
51
|
+
import "./chunk-3RG5ZIWI.js";
|
|
50
52
|
|
|
51
53
|
// src/router.ts
|
|
52
54
|
import { parseArgs, outputFormatter as outputFormatter2, isCommandResult as isCommandResult2, helpGenerator as helpGenerator2 } from "@dyyz1993/xcli-core";
|
|
@@ -169,12 +171,13 @@ var version = pkg.version;
|
|
|
169
171
|
|
|
170
172
|
// src/executor.ts
|
|
171
173
|
import {
|
|
172
|
-
ok as
|
|
174
|
+
ok as ok26,
|
|
173
175
|
fail as fail7,
|
|
174
176
|
isCommandResult,
|
|
175
177
|
configureArchiveStore,
|
|
176
178
|
appendCommandToArchive,
|
|
177
|
-
checkGuard
|
|
179
|
+
checkGuard,
|
|
180
|
+
PluginStorage
|
|
178
181
|
} from "@dyyz1993/xcli-core";
|
|
179
182
|
|
|
180
183
|
// src/commands/navigation.ts
|
|
@@ -534,7 +537,7 @@ var pressCommand = registerCommand({
|
|
|
534
537
|
key: z2.string()
|
|
535
538
|
}),
|
|
536
539
|
handler: async (p, ctx) => {
|
|
537
|
-
await ctx.page.press(p.selector || "body", p.key, {
|
|
540
|
+
await ctx.page.press(p.selector || "body", p.key, { timeout: 1e4 });
|
|
538
541
|
return ok2({ key: p.key });
|
|
539
542
|
}
|
|
540
543
|
});
|
|
@@ -553,7 +556,7 @@ var selectCommand = registerCommand({
|
|
|
553
556
|
}),
|
|
554
557
|
handler: async (p, ctx) => {
|
|
555
558
|
const values = typeof p.value === "string" ? [p.value] : p.value;
|
|
556
|
-
await ctx.page.selectOption(p.selector, values
|
|
559
|
+
await ctx.page.selectOption(p.selector, values);
|
|
557
560
|
return ok2({ selector: p.selector, value: p.value });
|
|
558
561
|
}
|
|
559
562
|
});
|
|
@@ -569,7 +572,7 @@ var checkCommand = registerCommand({
|
|
|
569
572
|
selector: z2.string()
|
|
570
573
|
}),
|
|
571
574
|
handler: async (p, ctx) => {
|
|
572
|
-
await ctx.page.check(p.selector, {
|
|
575
|
+
await ctx.page.check(p.selector, { timeout: 1e4 });
|
|
573
576
|
return ok2({ selector: p.selector });
|
|
574
577
|
}
|
|
575
578
|
});
|
|
@@ -586,7 +589,7 @@ var hoverCommand = registerCommand({
|
|
|
586
589
|
selector: z2.string()
|
|
587
590
|
}),
|
|
588
591
|
handler: async (p, ctx) => {
|
|
589
|
-
await ctx.page.hover(p.selector, {
|
|
592
|
+
await ctx.page.hover(p.selector, { timeout: 1e4 });
|
|
590
593
|
return ok2({ selector: p.selector });
|
|
591
594
|
}
|
|
592
595
|
});
|
|
@@ -2115,7 +2118,7 @@ var scrapeCommand = registerCommand({
|
|
|
2115
2118
|
await page.goto(p.url, { waitUntil: "commit", timeout: p.timeout });
|
|
2116
2119
|
await page.waitForSelector("body", { timeout: p.timeout }).catch(() => {
|
|
2117
2120
|
});
|
|
2118
|
-
await page.waitForLoadState("networkidle",
|
|
2121
|
+
await page.waitForLoadState("networkidle", Math.min(p.timeout, 8e3)).catch(() => {
|
|
2119
2122
|
});
|
|
2120
2123
|
await page.waitForTimeout(p.waitAfterLoad > 0 ? p.waitAfterLoad : 2e3);
|
|
2121
2124
|
if (p.selector) {
|
|
@@ -3120,7 +3123,7 @@ async function fetchFullContent(urls, timeout, cdpEndpoint) {
|
|
|
3120
3123
|
const pg = await context.newPage();
|
|
3121
3124
|
try {
|
|
3122
3125
|
await pg.goto(url, { waitUntil: "domcontentloaded", timeout });
|
|
3123
|
-
await pg.waitForLoadState("networkidle",
|
|
3126
|
+
await pg.waitForLoadState("networkidle", timeout).catch(() => {
|
|
3124
3127
|
});
|
|
3125
3128
|
const html = await pg.content();
|
|
3126
3129
|
contentMap.set(url, htmlToMarkdown(html, { onlyMainContent: true }));
|
|
@@ -3516,7 +3519,7 @@ var networkCommand = registerCommand({
|
|
|
3516
3519
|
waitUntil: "domcontentloaded",
|
|
3517
3520
|
timeout: p.timeout
|
|
3518
3521
|
});
|
|
3519
|
-
await page.waitForLoadState("networkidle",
|
|
3522
|
+
await page.waitForLoadState("networkidle", p.timeout).catch(() => {
|
|
3520
3523
|
});
|
|
3521
3524
|
await page.waitForTimeout(p.wait);
|
|
3522
3525
|
const totalCount = captures.length;
|
|
@@ -3827,6 +3830,24 @@ var ENGINE_KEY_ENUM = z20.enum(ALL_ENGINE_KEYS);
|
|
|
3827
3830
|
import { z as z21 } from "zod";
|
|
3828
3831
|
import { ok as ok20, fail as fail4 } from "@dyyz1993/xcli-core";
|
|
3829
3832
|
|
|
3833
|
+
// src/runtime/ref-store.ts
|
|
3834
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
3835
|
+
function normalizeAgentRef(ref) {
|
|
3836
|
+
return ref.startsWith("@") ? ref.slice(1) : ref;
|
|
3837
|
+
}
|
|
3838
|
+
function replaceRefs(sessionKey2, screenHash, targets) {
|
|
3839
|
+
sessions.set(sessionKey2, {
|
|
3840
|
+
screenHash,
|
|
3841
|
+
targets: new Map(targets.map((target) => [target.ref, target]))
|
|
3842
|
+
});
|
|
3843
|
+
}
|
|
3844
|
+
function getRefTarget(sessionKey2, ref) {
|
|
3845
|
+
const session = sessions.get(sessionKey2);
|
|
3846
|
+
const target = session?.targets.get(normalizeAgentRef(ref));
|
|
3847
|
+
if (!session || !target) return null;
|
|
3848
|
+
return { screenHash: session.screenHash, target };
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3830
3851
|
// src/utils/resolve-selector.ts
|
|
3831
3852
|
function buildElementSelector(el) {
|
|
3832
3853
|
function isUnique(sel) {
|
|
@@ -3998,9 +4019,9 @@ async function resolveSelectors(page, ariaSnapshot) {
|
|
|
3998
4019
|
}
|
|
3999
4020
|
return results;
|
|
4000
4021
|
}
|
|
4001
|
-
var REF_ONLY =
|
|
4022
|
+
var REF_ONLY = /^@?(e\d+)$/;
|
|
4002
4023
|
var refCache = /* @__PURE__ */ new Map();
|
|
4003
|
-
async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
4024
|
+
async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
|
|
4004
4025
|
const tips = [];
|
|
4005
4026
|
const newParams = { ...params };
|
|
4006
4027
|
if (!selectorKeys || selectorKeys.length === 0) {
|
|
@@ -4009,7 +4030,17 @@ async function resolveRefParams(page, params, selectorKeys, cache) {
|
|
|
4009
4030
|
for (const key of selectorKeys) {
|
|
4010
4031
|
const val = params[key];
|
|
4011
4032
|
if (typeof val !== "string" || !REF_ONLY.test(val)) continue;
|
|
4012
|
-
const
|
|
4033
|
+
const match = val.match(REF_ONLY);
|
|
4034
|
+
if (!match) continue;
|
|
4035
|
+
const ref = normalizeAgentRef(match[1]);
|
|
4036
|
+
if (sessionId) {
|
|
4037
|
+
const runtimeTarget = getRefTarget(sessionId, ref);
|
|
4038
|
+
if (runtimeTarget) {
|
|
4039
|
+
tips.push(`ref=@${ref} (${key}) => ${runtimeTarget.target.selector} (observe)`);
|
|
4040
|
+
newParams[key] = runtimeTarget.target.selector;
|
|
4041
|
+
continue;
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4013
4044
|
const activeCache = cache ?? refCache;
|
|
4014
4045
|
const cached = activeCache.get(ref);
|
|
4015
4046
|
if (cached) {
|
|
@@ -4290,6 +4321,404 @@ async function enhanceSemanticsWithLLM(url, ariaSnapshot, ruleBasedElements) {
|
|
|
4290
4321
|
saveSemantics(domain, pathKey, url, llmElements);
|
|
4291
4322
|
}
|
|
4292
4323
|
|
|
4324
|
+
// src/runtime/agent-runtime.ts
|
|
4325
|
+
function sessionKey(sessionId) {
|
|
4326
|
+
return sessionId || "default";
|
|
4327
|
+
}
|
|
4328
|
+
async function observePage(page, sessionId, options = {}) {
|
|
4329
|
+
const [title, raw] = await Promise.all([
|
|
4330
|
+
page.title().catch(() => ""),
|
|
4331
|
+
page.evaluate(
|
|
4332
|
+
({ includeHidden, limit }) => {
|
|
4333
|
+
function hash(input) {
|
|
4334
|
+
let h = 2166136261;
|
|
4335
|
+
for (let i = 0; i < input.length; i++) {
|
|
4336
|
+
h ^= input.charCodeAt(i);
|
|
4337
|
+
h = Math.imul(h, 16777619);
|
|
4338
|
+
}
|
|
4339
|
+
return (h >>> 0).toString(16);
|
|
4340
|
+
}
|
|
4341
|
+
function cssEscape(value) {
|
|
4342
|
+
const css = globalThis.CSS;
|
|
4343
|
+
return css?.escape ? css.escape(value) : value.replace(/["\\#.:,[\]>+~*]/g, "\\$&");
|
|
4344
|
+
}
|
|
4345
|
+
function isUnique(selector2) {
|
|
4346
|
+
try {
|
|
4347
|
+
return document.querySelectorAll(selector2).length === 1;
|
|
4348
|
+
} catch {
|
|
4349
|
+
return false;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
function nthOfType(el) {
|
|
4353
|
+
const tag = el.tagName.toLowerCase();
|
|
4354
|
+
const parent = el.parentElement;
|
|
4355
|
+
if (!parent) return tag;
|
|
4356
|
+
const same = Array.from(parent.children).filter((child) => child.tagName === el.tagName);
|
|
4357
|
+
if (same.length === 1) return tag;
|
|
4358
|
+
return `${tag}:nth-of-type(${same.indexOf(el) + 1})`;
|
|
4359
|
+
}
|
|
4360
|
+
function selectorFor(el) {
|
|
4361
|
+
const tag = el.tagName.toLowerCase();
|
|
4362
|
+
const id = el.getAttribute("id");
|
|
4363
|
+
if (id) {
|
|
4364
|
+
const selector2 = `#${cssEscape(id)}`;
|
|
4365
|
+
if (isUnique(selector2)) return selector2;
|
|
4366
|
+
}
|
|
4367
|
+
for (const attr of ["data-testid", "data-test", "data-qa", "name", "aria-label"]) {
|
|
4368
|
+
const value = el.getAttribute(attr);
|
|
4369
|
+
if (!value) continue;
|
|
4370
|
+
const selector2 = `[${attr}="${cssEscape(value)}"]`;
|
|
4371
|
+
if (isUnique(selector2)) return selector2;
|
|
4372
|
+
const tagged = `${tag}${selector2}`;
|
|
4373
|
+
if (isUnique(tagged)) return tagged;
|
|
4374
|
+
}
|
|
4375
|
+
const classes = Array.from(el.classList).slice(0, 3);
|
|
4376
|
+
for (const cls of classes) {
|
|
4377
|
+
const selector2 = `${tag}.${cssEscape(cls)}`;
|
|
4378
|
+
if (isUnique(selector2)) return selector2;
|
|
4379
|
+
}
|
|
4380
|
+
const parts = [];
|
|
4381
|
+
let cur = el;
|
|
4382
|
+
while (cur && cur !== document.body && cur !== document.documentElement && parts.length < 6) {
|
|
4383
|
+
parts.unshift(nthOfType(cur));
|
|
4384
|
+
const selector2 = parts.join(" > ");
|
|
4385
|
+
if (isUnique(selector2)) return selector2;
|
|
4386
|
+
cur = cur.parentElement;
|
|
4387
|
+
}
|
|
4388
|
+
return parts.join(" > ") || tag;
|
|
4389
|
+
}
|
|
4390
|
+
function roleFor(el) {
|
|
4391
|
+
const explicit = el.getAttribute("role");
|
|
4392
|
+
if (explicit) return explicit;
|
|
4393
|
+
const tag = el.tagName.toLowerCase();
|
|
4394
|
+
if (tag === "a") return "link";
|
|
4395
|
+
if (tag === "button") return "button";
|
|
4396
|
+
if (tag === "select") return "combobox";
|
|
4397
|
+
if (tag === "textarea") return "textbox";
|
|
4398
|
+
if (tag === "input") {
|
|
4399
|
+
const type = (el.getAttribute("type") || "text").toLowerCase();
|
|
4400
|
+
if (type === "checkbox") return "checkbox";
|
|
4401
|
+
if (type === "radio") return "radio";
|
|
4402
|
+
if (type === "button" || type === "submit" || type === "reset") return "button";
|
|
4403
|
+
return "textbox";
|
|
4404
|
+
}
|
|
4405
|
+
return tag;
|
|
4406
|
+
}
|
|
4407
|
+
function textName(el) {
|
|
4408
|
+
const input = el;
|
|
4409
|
+
const direct = el.getAttribute("aria-label") || el.getAttribute("placeholder") || el.getAttribute("title") || input.value || el.textContent || "";
|
|
4410
|
+
return direct.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
4411
|
+
}
|
|
4412
|
+
function actionsFor(role, editable) {
|
|
4413
|
+
const actions = [];
|
|
4414
|
+
if (editable) actions.push("fill", "type");
|
|
4415
|
+
if (role === "combobox") actions.push("select");
|
|
4416
|
+
if (role === "checkbox" || role === "radio") actions.push("check");
|
|
4417
|
+
actions.push("click", "hover");
|
|
4418
|
+
return Array.from(new Set(actions));
|
|
4419
|
+
}
|
|
4420
|
+
const selector = [
|
|
4421
|
+
"a[href]",
|
|
4422
|
+
"button",
|
|
4423
|
+
"input",
|
|
4424
|
+
"textarea",
|
|
4425
|
+
"select",
|
|
4426
|
+
"summary",
|
|
4427
|
+
"label",
|
|
4428
|
+
"[role]",
|
|
4429
|
+
"[tabindex]",
|
|
4430
|
+
'[contenteditable="true"]'
|
|
4431
|
+
].join(",");
|
|
4432
|
+
const candidates = Array.from(document.querySelectorAll(selector));
|
|
4433
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4434
|
+
const targets2 = [];
|
|
4435
|
+
for (const el of candidates) {
|
|
4436
|
+
const rect = el.getBoundingClientRect();
|
|
4437
|
+
const style = window.getComputedStyle(el);
|
|
4438
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4439
|
+
if (!includeHidden && !visible) continue;
|
|
4440
|
+
const selectorValue = selectorFor(el);
|
|
4441
|
+
if (!selectorValue || seen.has(selectorValue)) continue;
|
|
4442
|
+
seen.add(selectorValue);
|
|
4443
|
+
const input = el;
|
|
4444
|
+
const tag = el.tagName.toLowerCase();
|
|
4445
|
+
const role = roleFor(el);
|
|
4446
|
+
const editable = tag === "textarea" || tag === "select" || tag === "input" && !["button", "submit", "reset", "checkbox", "radio"].includes((input.type || "").toLowerCase()) || el.isContentEditable;
|
|
4447
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4448
|
+
const checked = typeof input.checked === "boolean" && ["checkbox", "radio"].includes((input.type || "").toLowerCase()) ? input.checked : void 0;
|
|
4449
|
+
targets2.push({
|
|
4450
|
+
selector: selectorValue,
|
|
4451
|
+
role,
|
|
4452
|
+
name: textName(el),
|
|
4453
|
+
tag,
|
|
4454
|
+
visible,
|
|
4455
|
+
enabled,
|
|
4456
|
+
editable,
|
|
4457
|
+
...checked !== void 0 ? { checked } : {},
|
|
4458
|
+
...editable && input.value ? { value: input.value.slice(0, 120) } : {},
|
|
4459
|
+
...visible ? { box: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height) } } : {},
|
|
4460
|
+
actions: actionsFor(role, editable)
|
|
4461
|
+
});
|
|
4462
|
+
if (targets2.length >= limit) break;
|
|
4463
|
+
}
|
|
4464
|
+
const stateText = [
|
|
4465
|
+
location.href,
|
|
4466
|
+
document.title,
|
|
4467
|
+
document.body?.innerText?.slice(0, 5e3) || ""
|
|
4468
|
+
].join("\n");
|
|
4469
|
+
return { screenHash: hash(stateText), targets: targets2 };
|
|
4470
|
+
},
|
|
4471
|
+
{ includeHidden: !!options.includeHidden, limit: options.limit ?? 80 }
|
|
4472
|
+
)
|
|
4473
|
+
]);
|
|
4474
|
+
const targets = raw.targets.map((target, index) => ({
|
|
4475
|
+
ref: `e${index + 1}`,
|
|
4476
|
+
...target
|
|
4477
|
+
}));
|
|
4478
|
+
replaceRefs(sessionKey(sessionId), raw.screenHash, targets);
|
|
4479
|
+
return {
|
|
4480
|
+
url: page.url(),
|
|
4481
|
+
title,
|
|
4482
|
+
screenHash: raw.screenHash,
|
|
4483
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4484
|
+
targets
|
|
4485
|
+
};
|
|
4486
|
+
}
|
|
4487
|
+
function quoteName(name) {
|
|
4488
|
+
const cleaned = name.replace(/\s+/g, " ").trim();
|
|
4489
|
+
if (!cleaned) return "";
|
|
4490
|
+
return ` "${cleaned.replace(/"/g, '\\"')}"`;
|
|
4491
|
+
}
|
|
4492
|
+
function targetFlags(target) {
|
|
4493
|
+
const flags = [target.role || target.tag];
|
|
4494
|
+
if (!target.enabled) flags.push("disabled");
|
|
4495
|
+
if (target.editable) flags.push("editable");
|
|
4496
|
+
if (target.checked !== void 0) flags.push(target.checked ? "checked" : "unchecked");
|
|
4497
|
+
return flags.join(" ");
|
|
4498
|
+
}
|
|
4499
|
+
function buildSelectorMap(observation) {
|
|
4500
|
+
return Object.fromEntries(observation.targets.map((target) => [target.ref, target.selector]));
|
|
4501
|
+
}
|
|
4502
|
+
function formatObservationCompact(observation, options = {}) {
|
|
4503
|
+
const lines = [
|
|
4504
|
+
`Page: ${observation.title || "(untitled)"}`,
|
|
4505
|
+
`URL: ${observation.url}`,
|
|
4506
|
+
`Screen: ${observation.screenHash}`,
|
|
4507
|
+
""
|
|
4508
|
+
];
|
|
4509
|
+
if (observation.targets.length === 0) {
|
|
4510
|
+
lines.push("(no interactive targets)");
|
|
4511
|
+
} else {
|
|
4512
|
+
for (const target of observation.targets) {
|
|
4513
|
+
lines.push(`@${target.ref} [${targetFlags(target)}]${quoteName(target.name)}`);
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
if (options.selectors && observation.targets.length > 0) {
|
|
4517
|
+
lines.push("", "## Selectors");
|
|
4518
|
+
lines.push(observation.targets.map((target) => `${target.ref}: ${target.selector}`).join(" | "));
|
|
4519
|
+
}
|
|
4520
|
+
return lines.join("\n");
|
|
4521
|
+
}
|
|
4522
|
+
async function getPageScreenHash(page) {
|
|
4523
|
+
const observation = await page.evaluate(() => {
|
|
4524
|
+
let h = 2166136261;
|
|
4525
|
+
const input = [location.href, document.title, document.body?.innerText?.slice(0, 5e3) || ""].join("\n");
|
|
4526
|
+
for (let i = 0; i < input.length; i++) {
|
|
4527
|
+
h ^= input.charCodeAt(i);
|
|
4528
|
+
h = Math.imul(h, 16777619);
|
|
4529
|
+
}
|
|
4530
|
+
return (h >>> 0).toString(16);
|
|
4531
|
+
});
|
|
4532
|
+
return observation;
|
|
4533
|
+
}
|
|
4534
|
+
async function actionability(page, selector) {
|
|
4535
|
+
return await page.evaluate((sel) => {
|
|
4536
|
+
const el = document.querySelector(sel);
|
|
4537
|
+
if (!el) return { ok: false, reason: "not_found" };
|
|
4538
|
+
const rect = el.getBoundingClientRect();
|
|
4539
|
+
const style = window.getComputedStyle(el);
|
|
4540
|
+
const visible = rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
4541
|
+
if (!visible) return { ok: false, reason: "not_visible" };
|
|
4542
|
+
const input = el;
|
|
4543
|
+
const enabled = !input.disabled && el.getAttribute("aria-disabled") !== "true";
|
|
4544
|
+
if (!enabled) return { ok: false, reason: "disabled" };
|
|
4545
|
+
const cx = rect.left + rect.width / 2;
|
|
4546
|
+
const cy = rect.top + rect.height / 2;
|
|
4547
|
+
const hit = document.elementFromPoint(cx, cy);
|
|
4548
|
+
if (hit && hit !== el && !el.contains(hit) && !hit.contains(el)) {
|
|
4549
|
+
return { ok: false, reason: "covered" };
|
|
4550
|
+
}
|
|
4551
|
+
return { ok: true };
|
|
4552
|
+
}, selector);
|
|
4553
|
+
}
|
|
4554
|
+
async function actOnPage(page, sessionId, input) {
|
|
4555
|
+
const normalizedRef = input.ref ? normalizeAgentRef(input.ref) : void 0;
|
|
4556
|
+
const refMatch = normalizedRef ? getRefTarget(sessionKey(sessionId), normalizedRef) : null;
|
|
4557
|
+
const selector = input.selector || refMatch?.target.selector;
|
|
4558
|
+
if (!selector) {
|
|
4559
|
+
return {
|
|
4560
|
+
action: input.action,
|
|
4561
|
+
selector: "",
|
|
4562
|
+
ref: normalizedRef,
|
|
4563
|
+
success: false,
|
|
4564
|
+
reason: input.ref ? "unknown_ref" : "missing_target",
|
|
4565
|
+
message: normalizedRef ? `Ref "${input.ref}" not found. Run observe again.` : "Provide ref or selector."
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
const hash = await getPageScreenHash(page).catch(() => void 0);
|
|
4569
|
+
const stale = !!(refMatch && hash && hash !== refMatch.screenHash);
|
|
4570
|
+
if (!input.force) {
|
|
4571
|
+
const check = await actionability(page, selector);
|
|
4572
|
+
if (!check.ok) {
|
|
4573
|
+
return {
|
|
4574
|
+
action: input.action,
|
|
4575
|
+
selector,
|
|
4576
|
+
ref: normalizedRef,
|
|
4577
|
+
success: false,
|
|
4578
|
+
reason: stale ? "stale_ref" : check.reason,
|
|
4579
|
+
message: stale ? `Ref "${input.ref}" may be stale. Run observe again.` : `Target is not actionable: ${check.reason}`,
|
|
4580
|
+
stale,
|
|
4581
|
+
screenHash: hash,
|
|
4582
|
+
target: refMatch?.target
|
|
4583
|
+
};
|
|
4584
|
+
}
|
|
4585
|
+
}
|
|
4586
|
+
const timeout = input.timeout ?? 1e4;
|
|
4587
|
+
try {
|
|
4588
|
+
switch (input.action) {
|
|
4589
|
+
case "click":
|
|
4590
|
+
await page.locator(selector).first().click({ timeout, force: !!input.force });
|
|
4591
|
+
break;
|
|
4592
|
+
case "fill":
|
|
4593
|
+
if (input.value === void 0) throw new Error("fill requires value");
|
|
4594
|
+
await page.locator(selector).first().fill(input.value, { timeout, force: !!input.force });
|
|
4595
|
+
break;
|
|
4596
|
+
case "type":
|
|
4597
|
+
if (input.value === void 0) throw new Error("type requires value");
|
|
4598
|
+
await page.locator(selector).first().pressSequentially(input.value, { timeout });
|
|
4599
|
+
break;
|
|
4600
|
+
case "press":
|
|
4601
|
+
if (!input.key) throw new Error("press requires key");
|
|
4602
|
+
await page.locator(selector).first().press(input.key, { timeout });
|
|
4603
|
+
break;
|
|
4604
|
+
case "select":
|
|
4605
|
+
if (input.value === void 0) throw new Error("select requires value");
|
|
4606
|
+
await page.locator(selector).first().selectOption(input.value);
|
|
4607
|
+
break;
|
|
4608
|
+
case "check":
|
|
4609
|
+
await page.locator(selector).first().check({ timeout });
|
|
4610
|
+
break;
|
|
4611
|
+
case "hover":
|
|
4612
|
+
await page.locator(selector).first().hover({ timeout });
|
|
4613
|
+
break;
|
|
4614
|
+
default: {
|
|
4615
|
+
const neverAction = input.action;
|
|
4616
|
+
throw new Error(`Unsupported action: ${neverAction}`);
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
} catch (error) {
|
|
4620
|
+
return {
|
|
4621
|
+
action: input.action,
|
|
4622
|
+
selector,
|
|
4623
|
+
ref: normalizedRef,
|
|
4624
|
+
success: false,
|
|
4625
|
+
reason: "browser_error",
|
|
4626
|
+
message: error.message,
|
|
4627
|
+
stale,
|
|
4628
|
+
screenHash: hash,
|
|
4629
|
+
target: refMatch?.target
|
|
4630
|
+
};
|
|
4631
|
+
}
|
|
4632
|
+
return {
|
|
4633
|
+
action: input.action,
|
|
4634
|
+
selector,
|
|
4635
|
+
ref: normalizedRef,
|
|
4636
|
+
success: true,
|
|
4637
|
+
stale,
|
|
4638
|
+
screenHash: hash,
|
|
4639
|
+
target: refMatch?.target
|
|
4640
|
+
};
|
|
4641
|
+
}
|
|
4642
|
+
function matchUrlPattern(url, pattern) {
|
|
4643
|
+
if (pattern.includes("*")) {
|
|
4644
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
4645
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
4646
|
+
}
|
|
4647
|
+
return url.includes(pattern);
|
|
4648
|
+
}
|
|
4649
|
+
async function pollUntil(timeout, pollInterval, predicate) {
|
|
4650
|
+
const startedAt = Date.now();
|
|
4651
|
+
while (Date.now() - startedAt <= timeout) {
|
|
4652
|
+
if (await predicate()) return true;
|
|
4653
|
+
await new Promise((resolve16) => setTimeout(resolve16, pollInterval));
|
|
4654
|
+
}
|
|
4655
|
+
return false;
|
|
4656
|
+
}
|
|
4657
|
+
async function waitForPage(page, input) {
|
|
4658
|
+
const timeout = input.timeout ?? 3e4;
|
|
4659
|
+
const pollInterval = input.pollInterval ?? 200;
|
|
4660
|
+
const startedAt = Date.now();
|
|
4661
|
+
try {
|
|
4662
|
+
if (input.selector) {
|
|
4663
|
+
const state = input.state ?? "visible";
|
|
4664
|
+
await page.locator(input.selector).first().waitFor({ state, timeout });
|
|
4665
|
+
return { success: true, matched: "selector", timeout, elapsed: Date.now() - startedAt };
|
|
4666
|
+
}
|
|
4667
|
+
if (input.text) {
|
|
4668
|
+
await page.getByText(input.text).first().waitFor({ state: "visible", timeout });
|
|
4669
|
+
return { success: true, matched: "text", timeout, elapsed: Date.now() - startedAt };
|
|
4670
|
+
}
|
|
4671
|
+
if (input.url) {
|
|
4672
|
+
const matched = await pollUntil(timeout, pollInterval, async () => matchUrlPattern(page.url(), input.url));
|
|
4673
|
+
return {
|
|
4674
|
+
success: matched,
|
|
4675
|
+
matched: "url",
|
|
4676
|
+
timeout,
|
|
4677
|
+
elapsed: Date.now() - startedAt,
|
|
4678
|
+
...matched ? {} : { message: `Timed out waiting for URL pattern: ${input.url}` }
|
|
4679
|
+
};
|
|
4680
|
+
}
|
|
4681
|
+
if (input.load) {
|
|
4682
|
+
await page.waitForLoadState(input.load, timeout);
|
|
4683
|
+
return { success: true, matched: "load", timeout, elapsed: Date.now() - startedAt };
|
|
4684
|
+
}
|
|
4685
|
+
if (input.fn) {
|
|
4686
|
+
await page.waitForFunction(input.fn, void 0, { timeout });
|
|
4687
|
+
return { success: true, matched: "fn", timeout, elapsed: Date.now() - startedAt };
|
|
4688
|
+
}
|
|
4689
|
+
if (input.screenHashChanged) {
|
|
4690
|
+
let screenHash = await getPageScreenHash(page);
|
|
4691
|
+
const matched = await pollUntil(timeout, pollInterval, async () => {
|
|
4692
|
+
screenHash = await getPageScreenHash(page);
|
|
4693
|
+
return screenHash !== input.screenHashChanged;
|
|
4694
|
+
});
|
|
4695
|
+
return {
|
|
4696
|
+
success: matched,
|
|
4697
|
+
matched: "screenHashChanged",
|
|
4698
|
+
timeout,
|
|
4699
|
+
elapsed: Date.now() - startedAt,
|
|
4700
|
+
screenHash,
|
|
4701
|
+
...matched ? {} : { message: `Timed out waiting for screen hash to change: ${input.screenHashChanged}` }
|
|
4702
|
+
};
|
|
4703
|
+
}
|
|
4704
|
+
} catch (error) {
|
|
4705
|
+
return {
|
|
4706
|
+
success: false,
|
|
4707
|
+
matched: input.selector ? "selector" : input.text ? "text" : input.url ? "url" : input.load ? "load" : input.fn ? "fn" : "screenHashChanged",
|
|
4708
|
+
timeout,
|
|
4709
|
+
elapsed: Date.now() - startedAt,
|
|
4710
|
+
message: error.message
|
|
4711
|
+
};
|
|
4712
|
+
}
|
|
4713
|
+
return {
|
|
4714
|
+
success: false,
|
|
4715
|
+
matched: "selector",
|
|
4716
|
+
timeout,
|
|
4717
|
+
elapsed: Date.now() - startedAt,
|
|
4718
|
+
message: "Provide one wait predicate: selector, text, url, load, fn, or screenHashChanged."
|
|
4719
|
+
};
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4293
4722
|
// src/commands/snapshot.ts
|
|
4294
4723
|
var snapshotCommand = registerCommand({
|
|
4295
4724
|
name: "snapshot",
|
|
@@ -4299,7 +4728,14 @@ var snapshotCommand = registerCommand({
|
|
|
4299
4728
|
parameters: z21.object({
|
|
4300
4729
|
type: z21.enum(["aria", "text", "dom", "all"]).default("aria").describe("Snapshot type: aria (accessibility tree), text (visible text), dom (element summary), all (combined)"),
|
|
4301
4730
|
selector: z21.string().optional().describe("Scope to a specific element"),
|
|
4302
|
-
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree")
|
|
4731
|
+
depth: z21.number().optional().default(6).describe("Max depth for DOM/aria tree"),
|
|
4732
|
+
interactive: z21.boolean().optional().default(false).describe("Return interactive agent refs only"),
|
|
4733
|
+
interactiveOnly: z21.boolean().optional().default(false).describe("Alias for interactive"),
|
|
4734
|
+
i: z21.boolean().optional().default(false).describe("Short alias for interactive"),
|
|
4735
|
+
compact: z21.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
|
|
4736
|
+
c: z21.boolean().optional().default(false).describe("Short alias for compact"),
|
|
4737
|
+
selectors: z21.boolean().optional().default(false).describe("Include ref to CSS selector map"),
|
|
4738
|
+
all: z21.boolean().optional().default(false).describe("Include hidden interactive targets when using interactive snapshot")
|
|
4303
4739
|
}),
|
|
4304
4740
|
result: z21.object({
|
|
4305
4741
|
url: z21.string(),
|
|
@@ -4312,6 +4748,18 @@ var snapshotCommand = registerCommand({
|
|
|
4312
4748
|
const page = ctx.page;
|
|
4313
4749
|
const url = page.url();
|
|
4314
4750
|
const title = await page.title().catch(() => "");
|
|
4751
|
+
if (p.interactive || p.interactiveOnly || p.i || p.compact || p.c || p.selectors) {
|
|
4752
|
+
const observation = await observePage(page, ctx.sessionId, {
|
|
4753
|
+
includeHidden: p.all
|
|
4754
|
+
});
|
|
4755
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4756
|
+
if (p.compact || p.c || p.interactive || p.interactiveOnly || p.i) {
|
|
4757
|
+
observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4758
|
+
}
|
|
4759
|
+
return ok20(observation, [
|
|
4760
|
+
`refs refreshed for ${observation.targets.length} targets; use click @e1 or fill @e2 "text"`
|
|
4761
|
+
]);
|
|
4762
|
+
}
|
|
4315
4763
|
if (p.type === "aria") {
|
|
4316
4764
|
const aria = await captureAriaSnapshot(page, p.selector, p.depth);
|
|
4317
4765
|
const tips = await buildRefTips(page, aria);
|
|
@@ -4362,10 +4810,10 @@ async function buildRefTips(page, aria) {
|
|
|
4362
4810
|
return [];
|
|
4363
4811
|
}
|
|
4364
4812
|
}
|
|
4365
|
-
async function captureAriaSnapshot(page, selector,
|
|
4813
|
+
async function captureAriaSnapshot(page, selector, _depth) {
|
|
4366
4814
|
try {
|
|
4367
4815
|
const locator = selector ? page.locator(selector).first() : page.locator("body");
|
|
4368
|
-
return await locator.ariaSnapshot(
|
|
4816
|
+
return await locator.ariaSnapshot();
|
|
4369
4817
|
} catch {
|
|
4370
4818
|
try {
|
|
4371
4819
|
return await page.locator("body").ariaSnapshot();
|
|
@@ -4376,7 +4824,7 @@ async function captureAriaSnapshot(page, selector, depth) {
|
|
|
4376
4824
|
}
|
|
4377
4825
|
async function captureTextSnapshot(page, selector) {
|
|
4378
4826
|
if (selector) {
|
|
4379
|
-
return await page.locator(selector).first().innerText(
|
|
4827
|
+
return await page.locator(selector).first().innerText().catch(() => "");
|
|
4380
4828
|
}
|
|
4381
4829
|
return await page.evaluate(() => document.body?.innerText || "").catch(() => "");
|
|
4382
4830
|
}
|
|
@@ -4412,22 +4860,108 @@ async function captureDomSnapshot(page, selector, maxDepth) {
|
|
|
4412
4860
|
).catch(() => ({ tag: "error" }));
|
|
4413
4861
|
}
|
|
4414
4862
|
|
|
4415
|
-
// src/commands/
|
|
4863
|
+
// src/commands/agent.ts
|
|
4416
4864
|
import { z as z22 } from "zod";
|
|
4417
|
-
import { ok as ok21
|
|
4418
|
-
var
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4865
|
+
import { ok as ok21 } from "@dyyz1993/xcli-core";
|
|
4866
|
+
var observeCommand = registerCommand({
|
|
4867
|
+
name: "observe",
|
|
4868
|
+
description: "Observe the current page as structured agent targets with session refs",
|
|
4869
|
+
scope: "page",
|
|
4870
|
+
parameters: z22.object({
|
|
4871
|
+
includeHidden: z22.boolean().optional().default(false).describe("Include hidden elements in the target list"),
|
|
4872
|
+
limit: z22.number().int().positive().max(300).optional().default(80).describe("Maximum number of targets to return"),
|
|
4873
|
+
compact: z22.boolean().optional().default(false).describe("Include compact agent-browser style snapshot text"),
|
|
4874
|
+
selectors: z22.boolean().optional().default(false).describe("Include ref to stable CSS selector map")
|
|
4875
|
+
}),
|
|
4876
|
+
handler: async (p, ctx) => {
|
|
4877
|
+
const observation = await observePage(ctx.page, ctx.sessionId, {
|
|
4878
|
+
includeHidden: p.includeHidden,
|
|
4879
|
+
limit: p.limit
|
|
4880
|
+
});
|
|
4881
|
+
if (p.selectors) observation.selectors = buildSelectorMap(observation);
|
|
4882
|
+
if (p.compact) observation.compact = formatObservationCompact(observation, { selectors: p.selectors });
|
|
4883
|
+
return ok21(observation, [
|
|
4884
|
+
`refs refreshed for ${observation.targets.length} targets; use act --ref @e1 --action click or click @e1`
|
|
4885
|
+
]);
|
|
4886
|
+
}
|
|
4887
|
+
});
|
|
4888
|
+
var actCommand = registerCommand({
|
|
4889
|
+
name: "act",
|
|
4890
|
+
description: "Perform an agent action using an observe ref or explicit selector",
|
|
4891
|
+
scope: "element",
|
|
4892
|
+
selectorParams: ["selector"],
|
|
4893
|
+
parameters: z22.object({
|
|
4894
|
+
action: z22.enum(["click", "fill", "type", "press", "select", "check", "hover"]).default("click"),
|
|
4895
|
+
ref: z22.string().optional().describe("Session-scoped ref returned by observe, such as e1"),
|
|
4896
|
+
selector: z22.string().optional().describe("CSS selector fallback when no ref is available"),
|
|
4897
|
+
value: z22.string().optional().describe("Value for fill/type/select"),
|
|
4898
|
+
key: z22.string().optional().describe("Key for press"),
|
|
4899
|
+
force: z22.boolean().optional().default(false).describe("Bypass actionability checks"),
|
|
4900
|
+
timeout: z22.number().optional().default(1e4).describe("Playwright action timeout in milliseconds")
|
|
4901
|
+
}).refine((p) => !!p.ref || !!p.selector, {
|
|
4902
|
+
message: "Either ref or selector is required"
|
|
4903
|
+
}),
|
|
4904
|
+
handler: async (p, ctx) => {
|
|
4905
|
+
const result = await actOnPage(ctx.page, ctx.sessionId, { ...p });
|
|
4906
|
+
if (!result.success) {
|
|
4907
|
+
return {
|
|
4908
|
+
success: false,
|
|
4909
|
+
data: result,
|
|
4910
|
+
message: result.message || result.reason || "Action failed",
|
|
4911
|
+
tips: result.stale ? ["run observe again to refresh refs"] : []
|
|
4912
|
+
};
|
|
4913
|
+
}
|
|
4914
|
+
return ok21(result, result.stale ? ["ref screen hash changed; run observe if the next action is uncertain"] : []);
|
|
4915
|
+
}
|
|
4916
|
+
});
|
|
4917
|
+
var waitForCommand = registerCommand({
|
|
4918
|
+
name: "waitFor",
|
|
4919
|
+
description: "Wait for agent predicates such as text, URL, load state, selector state, or screen hash changes",
|
|
4920
|
+
scope: "page",
|
|
4921
|
+
selectorParams: ["selector"],
|
|
4922
|
+
parameters: z22.object({
|
|
4923
|
+
selector: z22.string().optional().describe("CSS selector or observe ref to wait for"),
|
|
4924
|
+
state: z22.enum(["attached", "detached", "visible", "hidden"]).optional().default("visible"),
|
|
4925
|
+
text: z22.string().optional().describe("Visible text to wait for"),
|
|
4926
|
+
url: z22.string().optional().describe("URL substring or glob pattern to wait for"),
|
|
4927
|
+
load: z22.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Load state to wait for"),
|
|
4928
|
+
fn: z22.string().optional().describe("JavaScript predicate to wait for"),
|
|
4929
|
+
screenHashChanged: z22.string().optional().describe("Previous screenHash from observe"),
|
|
4930
|
+
timeout: z22.number().optional().default(3e4),
|
|
4931
|
+
pollInterval: z22.number().optional().default(200)
|
|
4932
|
+
}).refine((p) => [p.selector, p.text, p.url, p.load, p.fn, p.screenHashChanged].filter(Boolean).length === 1, {
|
|
4933
|
+
message: "Provide exactly one wait predicate: selector, text, url, load, fn, or screenHashChanged"
|
|
4934
|
+
}),
|
|
4935
|
+
handler: async (p, ctx) => {
|
|
4936
|
+
const result = await waitForPage(ctx.page, { ...p });
|
|
4937
|
+
if (!result.success) {
|
|
4938
|
+
return {
|
|
4939
|
+
success: false,
|
|
4940
|
+
data: result,
|
|
4941
|
+
message: result.message || `Timed out waiting for ${result.matched}`,
|
|
4942
|
+
tips: []
|
|
4943
|
+
};
|
|
4944
|
+
}
|
|
4945
|
+
return ok21(result);
|
|
4946
|
+
}
|
|
4947
|
+
});
|
|
4948
|
+
|
|
4949
|
+
// src/commands/tab.ts
|
|
4950
|
+
import { z as z23 } from "zod";
|
|
4951
|
+
import { ok as ok22, fail as fail5 } from "@dyyz1993/xcli-core";
|
|
4952
|
+
var TabParams = z23.object({
|
|
4953
|
+
subcommand: z23.enum(["list", "new", "close", "switch"]),
|
|
4954
|
+
url: z23.string().optional(),
|
|
4955
|
+
index: z23.number().int().min(0).optional()
|
|
4422
4956
|
});
|
|
4423
4957
|
var tabCommand = registerCommand({
|
|
4424
4958
|
name: "tab",
|
|
4425
4959
|
description: "Manage browser tabs: list, new, close, switch",
|
|
4426
4960
|
scope: "page",
|
|
4427
4961
|
parameters: TabParams,
|
|
4428
|
-
result:
|
|
4429
|
-
success:
|
|
4430
|
-
data:
|
|
4962
|
+
result: z23.object({
|
|
4963
|
+
success: z23.boolean(),
|
|
4964
|
+
data: z23.unknown()
|
|
4431
4965
|
}),
|
|
4432
4966
|
handler: async (p, ctx) => {
|
|
4433
4967
|
const pages = ctx.browserContext.pages();
|
|
@@ -4467,7 +5001,7 @@ function handleList(pages, ctx) {
|
|
|
4467
5001
|
active: i === currentIndex
|
|
4468
5002
|
};
|
|
4469
5003
|
});
|
|
4470
|
-
return
|
|
5004
|
+
return ok22({ tabs, total: tabs.length, activeIndex: currentIndex });
|
|
4471
5005
|
}
|
|
4472
5006
|
async function handleNew(p, _pages, ctx) {
|
|
4473
5007
|
const newPage = await ctx.browserContext.newPage();
|
|
@@ -4489,7 +5023,7 @@ async function handleNew(p, _pages, ctx) {
|
|
|
4489
5023
|
const title = await newPage.title().catch(() => "");
|
|
4490
5024
|
const allPages = ctx.browserContext.pages();
|
|
4491
5025
|
const newIndex = allPages.indexOf(newPage);
|
|
4492
|
-
return
|
|
5026
|
+
return ok22({
|
|
4493
5027
|
index: newIndex >= 0 ? newIndex : allPages.length - 1,
|
|
4494
5028
|
url: newPage.url(),
|
|
4495
5029
|
title,
|
|
@@ -4517,7 +5051,7 @@ async function handleClose(p, pages, ctx) {
|
|
|
4517
5051
|
}
|
|
4518
5052
|
ctx.page = newActivePage;
|
|
4519
5053
|
}
|
|
4520
|
-
return
|
|
5054
|
+
return ok22({
|
|
4521
5055
|
closedIndex: closeIndex,
|
|
4522
5056
|
total: remainingPages.length,
|
|
4523
5057
|
activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : pages.indexOf(ctx.page)
|
|
@@ -4539,7 +5073,7 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4539
5073
|
}
|
|
4540
5074
|
ctx.page = targetPage;
|
|
4541
5075
|
const title = await targetPage.title().catch(() => "");
|
|
4542
|
-
return
|
|
5076
|
+
return ok22({
|
|
4543
5077
|
index: p.index,
|
|
4544
5078
|
url: targetPage.url(),
|
|
4545
5079
|
title,
|
|
@@ -4548,8 +5082,8 @@ async function handleSwitch(p, pages, ctx) {
|
|
|
4548
5082
|
}
|
|
4549
5083
|
|
|
4550
5084
|
// src/commands/addinitscript.ts
|
|
4551
|
-
import { z as
|
|
4552
|
-
import { ok as
|
|
5085
|
+
import { z as z24 } from "zod";
|
|
5086
|
+
import { ok as ok23 } from "@dyyz1993/xcli-core";
|
|
4553
5087
|
import { readFileSync as readFileSync2 } from "fs";
|
|
4554
5088
|
|
|
4555
5089
|
// src/chain-parser.ts
|
|
@@ -4727,7 +5261,7 @@ function parseCommandArgs(name, args) {
|
|
|
4727
5261
|
} else {
|
|
4728
5262
|
if (positionalIndex < positionalKeys.length) {
|
|
4729
5263
|
const isLast = positionalIndex === positionalKeys.length - 1;
|
|
4730
|
-
if (isLast && name === "eval") {
|
|
5264
|
+
if (isLast && (name === "eval" || name === "find")) {
|
|
4731
5265
|
const remaining = args.slice(i).map(unquote2).join(" ");
|
|
4732
5266
|
params[positionalKeys[positionalIndex]] = remaining;
|
|
4733
5267
|
break;
|
|
@@ -4777,7 +5311,7 @@ var commandDefCache = {
|
|
|
4777
5311
|
frames: { positional: [] },
|
|
4778
5312
|
frame: { positional: ["selector"] },
|
|
4779
5313
|
actions: { positional: ["url"] },
|
|
4780
|
-
find: { positional: ["strategy", "value"] },
|
|
5314
|
+
find: { positional: ["strategy", "value", "operation"] },
|
|
4781
5315
|
addinitscript: { positional: ["script"] },
|
|
4782
5316
|
tab: { positional: ["subcommand"] }
|
|
4783
5317
|
};
|
|
@@ -4789,14 +5323,14 @@ function registerCommandDefinition(name, positional) {
|
|
|
4789
5323
|
}
|
|
4790
5324
|
|
|
4791
5325
|
// src/commands/addinitscript.ts
|
|
4792
|
-
var InitScriptParams =
|
|
4793
|
-
script:
|
|
4794
|
-
file:
|
|
4795
|
-
stdin:
|
|
4796
|
-
name:
|
|
4797
|
-
list:
|
|
4798
|
-
remove:
|
|
4799
|
-
base64:
|
|
5326
|
+
var InitScriptParams = z24.object({
|
|
5327
|
+
script: z24.string().optional(),
|
|
5328
|
+
file: z24.string().optional(),
|
|
5329
|
+
stdin: z24.boolean().optional(),
|
|
5330
|
+
name: z24.string().optional(),
|
|
5331
|
+
list: z24.boolean().optional(),
|
|
5332
|
+
remove: z24.string().optional(),
|
|
5333
|
+
base64: z24.string().optional()
|
|
4800
5334
|
});
|
|
4801
5335
|
var registeredScripts = /* @__PURE__ */ new Map();
|
|
4802
5336
|
function resolveScriptContent(params) {
|
|
@@ -4837,15 +5371,15 @@ var addInitScriptCommand = registerCommand({
|
|
|
4837
5371
|
size: content2.length,
|
|
4838
5372
|
preview: content2.slice(0, 80)
|
|
4839
5373
|
}));
|
|
4840
|
-
return
|
|
5374
|
+
return ok23({ scripts });
|
|
4841
5375
|
}
|
|
4842
5376
|
if (params.remove) {
|
|
4843
5377
|
const existed = registeredScripts.delete(params.remove);
|
|
4844
|
-
return
|
|
5378
|
+
return ok23({ removed: params.remove, existed });
|
|
4845
5379
|
}
|
|
4846
5380
|
let content = params.stdin ? await readStdin() : resolveScriptContent(params);
|
|
4847
5381
|
if (!content) {
|
|
4848
|
-
return
|
|
5382
|
+
return ok23({ error: "No script content provided. Use --script, --file, --stdin, or --base64" });
|
|
4849
5383
|
}
|
|
4850
5384
|
const scriptName = params.name ?? `script-${Date.now()}`;
|
|
4851
5385
|
registeredScripts.set(scriptName, content);
|
|
@@ -4853,74 +5387,134 @@ var addInitScriptCommand = registerCommand({
|
|
|
4853
5387
|
try {
|
|
4854
5388
|
await ctx.page.evaluate(content);
|
|
4855
5389
|
} catch {
|
|
4856
|
-
return
|
|
5390
|
+
return ok23({
|
|
4857
5391
|
registered: scriptName,
|
|
4858
5392
|
hint: "Script registered for future page loads; immediate execution skipped (page may not be ready)"
|
|
4859
5393
|
});
|
|
4860
5394
|
}
|
|
4861
|
-
return
|
|
5395
|
+
return ok23({ registered: scriptName, executedImmediately: true });
|
|
4862
5396
|
}
|
|
4863
5397
|
});
|
|
4864
5398
|
registerCommandDefinition("addinitscript", ["script"]);
|
|
4865
5399
|
|
|
4866
5400
|
// src/commands/find.ts
|
|
4867
|
-
import { z as
|
|
4868
|
-
import { ok as
|
|
5401
|
+
import { z as z25 } from "zod";
|
|
5402
|
+
import { ok as ok24, fail as fail6 } from "@dyyz1993/xcli-core";
|
|
5403
|
+
var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
|
|
4869
5404
|
var findCommand = registerCommand({
|
|
4870
5405
|
name: "find",
|
|
4871
5406
|
description: "Find elements by semantic strategy (text/role/label/placeholder/testid) and optionally perform an action",
|
|
4872
5407
|
scope: "page",
|
|
4873
|
-
parameters:
|
|
4874
|
-
strategy:
|
|
4875
|
-
value:
|
|
4876
|
-
name:
|
|
4877
|
-
exact:
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
5408
|
+
parameters: z25.object({
|
|
5409
|
+
strategy: z25.enum(["text", "role", "label", "placeholder", "testid", "alt", "title", "first", "last", "nth"]),
|
|
5410
|
+
value: z25.string(),
|
|
5411
|
+
name: z25.string().optional(),
|
|
5412
|
+
exact: z25.boolean().optional().default(false),
|
|
5413
|
+
operation: z25.string().optional().describe('Trailing operation syntax, e.g. click, fill "text", type "text"'),
|
|
5414
|
+
action: actionSchema2.optional().describe("Action to perform when not using trailing operation syntax"),
|
|
5415
|
+
actionValue: z25.string().optional().describe("Value for fill/type/select when using action"),
|
|
5416
|
+
index: z25.number().int().optional().describe("Index for nth strategy"),
|
|
5417
|
+
click: z25.boolean().optional().default(false),
|
|
5418
|
+
fill: z25.string().optional(),
|
|
5419
|
+
type: z25.string().optional(),
|
|
5420
|
+
select: z25.string().optional(),
|
|
5421
|
+
hover: z25.boolean().optional().default(false),
|
|
5422
|
+
check: z25.boolean().optional().default(false),
|
|
5423
|
+
timeout: z25.number().optional().default(1e4)
|
|
4883
5424
|
}),
|
|
4884
|
-
result:
|
|
4885
|
-
matched:
|
|
4886
|
-
selector:
|
|
4887
|
-
action:
|
|
5425
|
+
result: z25.object({
|
|
5426
|
+
matched: z25.number(),
|
|
5427
|
+
selector: z25.string(),
|
|
5428
|
+
action: z25.string().optional()
|
|
4888
5429
|
}),
|
|
4889
5430
|
handler: async (p, ctx) => {
|
|
4890
5431
|
const page = ctx.page;
|
|
4891
|
-
const
|
|
5432
|
+
const normalized = normalizeFindParams({ ...p });
|
|
5433
|
+
const parsedOperation = parseOperation(normalized.operation);
|
|
5434
|
+
const actionName = parsedOperation.action || p.action || inferLegacyAction(p);
|
|
5435
|
+
const actionValue = parsedOperation.value ?? p.actionValue ?? p.fill ?? p.type ?? p.select;
|
|
5436
|
+
const locator = buildLocator(page, normalized.strategy, normalized.value, {
|
|
4892
5437
|
name: p.name,
|
|
4893
|
-
exact: p.exact
|
|
5438
|
+
exact: p.exact,
|
|
5439
|
+
index: normalized.index
|
|
4894
5440
|
});
|
|
4895
5441
|
const count = await locator.count();
|
|
4896
5442
|
if (count === 0) {
|
|
4897
5443
|
return fail6(`No element found with ${p.strategy}="${p.value}"`);
|
|
4898
5444
|
}
|
|
4899
5445
|
const tips = [];
|
|
4900
|
-
const target = locator.
|
|
5446
|
+
const target = selectTarget(locator, p.strategy);
|
|
4901
5447
|
if (count > 1) {
|
|
4902
|
-
tips.push(`\u26A0\uFE0F Matched ${count} elements,
|
|
5448
|
+
tips.push(`\u26A0\uFE0F Matched ${count} elements, used first match. Use 'find nth <index> ${normalized.strategy} "${normalized.value}" ${actionName || "click"}' for a specific match.`);
|
|
4903
5449
|
}
|
|
4904
|
-
const selector = describeSelector(
|
|
4905
|
-
|
|
4906
|
-
if (p.click) {
|
|
5450
|
+
const selector = describeSelector(normalized.strategy, normalized.value, p.name);
|
|
5451
|
+
if (actionName === "click") {
|
|
4907
5452
|
await target.click({ timeout: p.timeout, force: true });
|
|
4908
|
-
action
|
|
4909
|
-
} else if (
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
5453
|
+
return okWithTips({ matched: count, selector, action: "click" }, tips);
|
|
5454
|
+
} else if (actionName === "fill") {
|
|
5455
|
+
if (actionValue === void 0) return fail6("find fill requires a value");
|
|
5456
|
+
await target.fill(actionValue, { timeout: p.timeout, force: true });
|
|
5457
|
+
return okWithTips({ matched: count, selector, action: `fill("${actionValue}")` }, tips);
|
|
5458
|
+
} else if (actionName === "type") {
|
|
5459
|
+
if (actionValue === void 0) return fail6("find type requires a value");
|
|
5460
|
+
await target.type(actionValue, { delay: 10, timeout: p.timeout });
|
|
5461
|
+
return okWithTips({ matched: count, selector, action: `type("${actionValue}")` }, tips);
|
|
5462
|
+
} else if (actionName === "select") {
|
|
5463
|
+
if (actionValue === void 0) return fail6("find select requires a value");
|
|
5464
|
+
await target.selectOption(actionValue);
|
|
5465
|
+
return okWithTips({ matched: count, selector, action: `select("${actionValue}")` }, tips);
|
|
5466
|
+
} else if (actionName === "hover") {
|
|
5467
|
+
await target.hover({ timeout: p.timeout, force: true });
|
|
5468
|
+
return okWithTips({ matched: count, selector, action: "hover" }, tips);
|
|
5469
|
+
} else if (actionName === "check") {
|
|
5470
|
+
await target.check({ timeout: p.timeout });
|
|
5471
|
+
return okWithTips({ matched: count, selector, action: "check" }, tips);
|
|
5472
|
+
}
|
|
5473
|
+
return okWithTips({ matched: count, selector }, tips);
|
|
4922
5474
|
}
|
|
4923
5475
|
});
|
|
5476
|
+
function okWithTips(data, tips) {
|
|
5477
|
+
const result = ok24(data);
|
|
5478
|
+
if (tips.length > 0) result.tips = tips;
|
|
5479
|
+
return result;
|
|
5480
|
+
}
|
|
5481
|
+
function parseOperation(operation) {
|
|
5482
|
+
if (!operation) return {};
|
|
5483
|
+
const match = operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5484
|
+
if (!match) return {};
|
|
5485
|
+
const maybeAction = match[1];
|
|
5486
|
+
const parsed = actionSchema2.safeParse(maybeAction);
|
|
5487
|
+
if (!parsed.success) return {};
|
|
5488
|
+
const rawValue = match[2];
|
|
5489
|
+
const value = rawValue?.replace(/^["']|["']$/g, "");
|
|
5490
|
+
return { action: parsed.data, ...value !== void 0 ? { value } : {} };
|
|
5491
|
+
}
|
|
5492
|
+
function normalizeFindParams(p) {
|
|
5493
|
+
if (p.strategy !== "nth") return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5494
|
+
const parsedIndex = Number(p.value);
|
|
5495
|
+
if (!Number.isInteger(parsedIndex) || !p.operation) {
|
|
5496
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: p.index };
|
|
5497
|
+
}
|
|
5498
|
+
const match = p.operation.trim().match(/^(\S+)(?:\s+([\s\S]+))?$/);
|
|
5499
|
+
if (!match) {
|
|
5500
|
+
return { strategy: p.strategy, value: p.value, operation: p.operation, index: parsedIndex };
|
|
5501
|
+
}
|
|
5502
|
+
return {
|
|
5503
|
+
strategy: p.strategy,
|
|
5504
|
+
value: match[1].replace(/^["']|["']$/g, ""),
|
|
5505
|
+
...match[2] ? { operation: match[2] } : {},
|
|
5506
|
+
index: parsedIndex
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
function inferLegacyAction(p) {
|
|
5510
|
+
if (p.click) return "click";
|
|
5511
|
+
if (p.fill !== void 0) return "fill";
|
|
5512
|
+
if (p.type !== void 0) return "type";
|
|
5513
|
+
if (p.select !== void 0) return "select";
|
|
5514
|
+
if (p.hover) return "hover";
|
|
5515
|
+
if (p.check) return "check";
|
|
5516
|
+
return void 0;
|
|
5517
|
+
}
|
|
4924
5518
|
function buildLocator(page, strategy, value, opts) {
|
|
4925
5519
|
switch (strategy) {
|
|
4926
5520
|
case "text":
|
|
@@ -4933,10 +5527,24 @@ function buildLocator(page, strategy, value, opts) {
|
|
|
4933
5527
|
return page.getByPlaceholder(value, { exact: opts.exact });
|
|
4934
5528
|
case "testid":
|
|
4935
5529
|
return page.getByTestId(value);
|
|
5530
|
+
case "alt":
|
|
5531
|
+
return page.getByAltText(value, { exact: opts.exact });
|
|
5532
|
+
case "title":
|
|
5533
|
+
return page.getByTitle(value, { exact: opts.exact });
|
|
5534
|
+
case "first":
|
|
5535
|
+
return page.locator(value).first();
|
|
5536
|
+
case "last":
|
|
5537
|
+
return page.locator(value).last();
|
|
5538
|
+
case "nth":
|
|
5539
|
+
return page.locator(value).nth(opts.index ?? 0);
|
|
4936
5540
|
default:
|
|
4937
5541
|
return page.getByText(value, { exact: opts.exact });
|
|
4938
5542
|
}
|
|
4939
5543
|
}
|
|
5544
|
+
function selectTarget(locator, strategy) {
|
|
5545
|
+
if (strategy === "first" || strategy === "last" || strategy === "nth") return locator;
|
|
5546
|
+
return locator.first();
|
|
5547
|
+
}
|
|
4940
5548
|
function describeSelector(strategy, value, name) {
|
|
4941
5549
|
switch (strategy) {
|
|
4942
5550
|
case "role":
|
|
@@ -4949,6 +5557,16 @@ function describeSelector(strategy, value, name) {
|
|
|
4949
5557
|
return `getByPlaceholder("${value}")`;
|
|
4950
5558
|
case "testid":
|
|
4951
5559
|
return `getByTestId("${value}")`;
|
|
5560
|
+
case "alt":
|
|
5561
|
+
return `getByAltText("${value}")`;
|
|
5562
|
+
case "title":
|
|
5563
|
+
return `getByTitle("${value}")`;
|
|
5564
|
+
case "first":
|
|
5565
|
+
return `first("${value}")`;
|
|
5566
|
+
case "last":
|
|
5567
|
+
return `last("${value}")`;
|
|
5568
|
+
case "nth":
|
|
5569
|
+
return `nth("${value}")`;
|
|
4952
5570
|
default:
|
|
4953
5571
|
return `${strategy}("${value}")`;
|
|
4954
5572
|
}
|
|
@@ -5071,7 +5689,7 @@ async function detectCaptcha2(page) {
|
|
|
5071
5689
|
}
|
|
5072
5690
|
async function detectWarningText(page) {
|
|
5073
5691
|
try {
|
|
5074
|
-
const pageText = await page.textContent("body"
|
|
5692
|
+
const pageText = await page.textContent("body").catch(() => "") || "";
|
|
5075
5693
|
const lowerText = pageText.toLowerCase();
|
|
5076
5694
|
for (const { text, severity } of WARNING_TEXTS) {
|
|
5077
5695
|
if (lowerText.includes(text.toLowerCase())) {
|
|
@@ -5202,8 +5820,8 @@ function formatDetectionMessage(result) {
|
|
|
5202
5820
|
}
|
|
5203
5821
|
|
|
5204
5822
|
// src/commands/promo.ts
|
|
5205
|
-
import { z as
|
|
5206
|
-
import { ok as
|
|
5823
|
+
import { z as z26 } from "zod";
|
|
5824
|
+
import { ok as ok25 } from "@dyyz1993/xcli-core";
|
|
5207
5825
|
import { existsSync as existsSync2, readFileSync as readFileSync8 } from "fs";
|
|
5208
5826
|
import { resolve as resolve6 } from "path";
|
|
5209
5827
|
|
|
@@ -5506,14 +6124,14 @@ async function dispatchPromo(config) {
|
|
|
5506
6124
|
}
|
|
5507
6125
|
|
|
5508
6126
|
// src/commands/promo.ts
|
|
5509
|
-
var promoParams =
|
|
5510
|
-
platform:
|
|
5511
|
-
file:
|
|
5512
|
-
tags:
|
|
5513
|
-
title:
|
|
5514
|
-
search:
|
|
5515
|
-
cdpEndpoint:
|
|
5516
|
-
session:
|
|
6127
|
+
var promoParams = z26.object({
|
|
6128
|
+
platform: z26.enum(["devto", "medium", "csdn", "juejin", "quora"]).describe("Target platform for promotion"),
|
|
6129
|
+
file: z26.string().describe("Path to Markdown file to publish"),
|
|
6130
|
+
tags: z26.string().optional().describe("Comma-separated tags"),
|
|
6131
|
+
title: z26.string().optional().describe("Custom title (default: extracted from file first heading)"),
|
|
6132
|
+
search: z26.string().optional().describe("Quora: search query to find questions"),
|
|
6133
|
+
cdpEndpoint: z26.string().optional().describe("CDP endpoint for agent-browser"),
|
|
6134
|
+
session: z26.string().optional().describe("agent-browser session name")
|
|
5517
6135
|
}).refine(
|
|
5518
6136
|
(data) => data.platform !== "quora" || !!data.search,
|
|
5519
6137
|
{ message: "Quora platform requires --search parameter" }
|
|
@@ -5526,17 +6144,17 @@ var promoCommand = registerCommand({
|
|
|
5526
6144
|
description: "Publish promotional articles to various platforms (devto, medium, csdn, juejin, quora)",
|
|
5527
6145
|
scope: "project",
|
|
5528
6146
|
parameters: promoParams,
|
|
5529
|
-
result:
|
|
5530
|
-
success:
|
|
5531
|
-
url:
|
|
5532
|
-
error:
|
|
5533
|
-
platform:
|
|
6147
|
+
result: z26.object({
|
|
6148
|
+
success: z26.boolean(),
|
|
6149
|
+
url: z26.string().optional(),
|
|
6150
|
+
error: z26.string().optional(),
|
|
6151
|
+
platform: z26.string()
|
|
5534
6152
|
}),
|
|
5535
6153
|
handler: async (p, _ctx) => {
|
|
5536
6154
|
const filePath = resolve6(p.file);
|
|
5537
6155
|
const content = readFileSync8(filePath, "utf-8");
|
|
5538
6156
|
if (content.trim().length === 0) {
|
|
5539
|
-
return
|
|
6157
|
+
return ok25({
|
|
5540
6158
|
success: false,
|
|
5541
6159
|
error: `File is empty: ${filePath}`,
|
|
5542
6160
|
platform: p.platform
|
|
@@ -5551,7 +6169,7 @@ var promoCommand = registerCommand({
|
|
|
5551
6169
|
cdpEndpoint: p.cdpEndpoint ?? _ctx.cdpEndpoint,
|
|
5552
6170
|
session: p.session ?? p.platform
|
|
5553
6171
|
});
|
|
5554
|
-
return
|
|
6172
|
+
return ok25(result);
|
|
5555
6173
|
}
|
|
5556
6174
|
});
|
|
5557
6175
|
|
|
@@ -5652,7 +6270,7 @@ import { join as join2 } from "path";
|
|
|
5652
6270
|
import { execSync as execSync6 } from "child_process";
|
|
5653
6271
|
var SHARED_PLUGIN_DEPENDENCIES = {
|
|
5654
6272
|
"zod": "^3.24.0",
|
|
5655
|
-
"@dyyz1993/xcli-core": "^0.
|
|
6273
|
+
"@dyyz1993/xcli-core": "^0.12.1"
|
|
5656
6274
|
};
|
|
5657
6275
|
function ensurePluginDependencies(pluginsDir) {
|
|
5658
6276
|
const zodPath = join2(pluginsDir, "node_modules", "zod");
|
|
@@ -5691,6 +6309,192 @@ function ensurePluginDependencies(pluginsDir) {
|
|
|
5691
6309
|
}
|
|
5692
6310
|
}
|
|
5693
6311
|
|
|
6312
|
+
// src/plugin/contract.ts
|
|
6313
|
+
function buildPluginContract(site) {
|
|
6314
|
+
const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command));
|
|
6315
|
+
return {
|
|
6316
|
+
version: 2,
|
|
6317
|
+
plugin: {
|
|
6318
|
+
name: site.name,
|
|
6319
|
+
url: site.url,
|
|
6320
|
+
description: site.config?.description,
|
|
6321
|
+
requiresLogin: site.config?.requiresLogin
|
|
6322
|
+
},
|
|
6323
|
+
commands
|
|
6324
|
+
};
|
|
6325
|
+
}
|
|
6326
|
+
function buildCommandContract(command) {
|
|
6327
|
+
const extension = command.xbrowser || {};
|
|
6328
|
+
const inferredFields = fieldsFromZodObject(command.parameters);
|
|
6329
|
+
const fields = mergeFields(inferredFields, extension.form?.fields || []);
|
|
6330
|
+
const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
|
|
6331
|
+
const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", command.requiresLogin);
|
|
6332
|
+
const outputSchema = command.result ? summarizeZod(command.result) : void 0;
|
|
6333
|
+
return {
|
|
6334
|
+
name: command.name,
|
|
6335
|
+
description: command.description || "",
|
|
6336
|
+
scope: command.scope || "project",
|
|
6337
|
+
requiresLogin: command.requiresLogin === true,
|
|
6338
|
+
category: extension.category,
|
|
6339
|
+
capabilities,
|
|
6340
|
+
positional,
|
|
6341
|
+
form: {
|
|
6342
|
+
title: extension.form?.title || command.description || command.name,
|
|
6343
|
+
description: extension.form?.description,
|
|
6344
|
+
submitLabel: extension.form?.submitLabel || "Run",
|
|
6345
|
+
fields
|
|
6346
|
+
},
|
|
6347
|
+
output: extension.output || (outputSchema ? { schema: outputSchema } : void 0)
|
|
6348
|
+
};
|
|
6349
|
+
}
|
|
6350
|
+
function fieldsFromZodObject(schema) {
|
|
6351
|
+
const shape = getShape(schema);
|
|
6352
|
+
if (!shape) return [];
|
|
6353
|
+
return Object.entries(shape).map(([name, field]) => fieldFromZod(name, field));
|
|
6354
|
+
}
|
|
6355
|
+
function fieldFromZod(name, schema) {
|
|
6356
|
+
const unwrapped = unwrapZod(schema);
|
|
6357
|
+
const type = zodTypeToContractType(unwrapped.typeName);
|
|
6358
|
+
const enumValues = extractEnumValues(unwrapped.schema);
|
|
6359
|
+
return {
|
|
6360
|
+
name,
|
|
6361
|
+
label: toLabel(name),
|
|
6362
|
+
type,
|
|
6363
|
+
widget: widgetFor(type, enumValues, unwrapped.schema),
|
|
6364
|
+
required: !unwrapped.optional,
|
|
6365
|
+
...unwrapped.description ? { description: unwrapped.description } : {},
|
|
6366
|
+
...unwrapped.defaultValue !== void 0 ? { default: unwrapped.defaultValue } : {},
|
|
6367
|
+
...enumValues ? { enum: enumValues } : {},
|
|
6368
|
+
...type === "array" ? { multiple: true } : {}
|
|
6369
|
+
};
|
|
6370
|
+
}
|
|
6371
|
+
function mergeFields(inferred, overrides) {
|
|
6372
|
+
if (overrides.length === 0) return inferred;
|
|
6373
|
+
const byName = new Map(inferred.map((field) => [field.name, field]));
|
|
6374
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6375
|
+
const merged = [];
|
|
6376
|
+
for (const override of overrides) {
|
|
6377
|
+
if (!override.name) continue;
|
|
6378
|
+
const base = byName.get(override.name) || {
|
|
6379
|
+
name: override.name,
|
|
6380
|
+
label: toLabel(override.name),
|
|
6381
|
+
type: "string",
|
|
6382
|
+
widget: "text",
|
|
6383
|
+
required: false
|
|
6384
|
+
};
|
|
6385
|
+
merged.push({ ...base, ...override, name: override.name });
|
|
6386
|
+
seen.add(override.name);
|
|
6387
|
+
}
|
|
6388
|
+
for (const field of inferred) {
|
|
6389
|
+
if (!seen.has(field.name)) merged.push(field);
|
|
6390
|
+
}
|
|
6391
|
+
return merged;
|
|
6392
|
+
}
|
|
6393
|
+
function inferCapabilities(scope, requiresLogin) {
|
|
6394
|
+
const caps = [];
|
|
6395
|
+
if (scope === "page") caps.push("browser.page");
|
|
6396
|
+
if (scope === "browser") caps.push("browser.context");
|
|
6397
|
+
if (requiresLogin) caps.push("auth.login");
|
|
6398
|
+
return caps;
|
|
6399
|
+
}
|
|
6400
|
+
function getShape(schema) {
|
|
6401
|
+
const zod = schema;
|
|
6402
|
+
const shapeOrFn = zod?.shape ?? zod?._def?.shape;
|
|
6403
|
+
if (!shapeOrFn) return void 0;
|
|
6404
|
+
return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
|
|
6405
|
+
}
|
|
6406
|
+
function unwrapZod(schema) {
|
|
6407
|
+
let current = schema;
|
|
6408
|
+
let optional = typeof current?.isOptional === "function" ? current.isOptional() : false;
|
|
6409
|
+
let description = current?._def?.description;
|
|
6410
|
+
let defaultValue;
|
|
6411
|
+
for (let i = 0; i < 8; i++) {
|
|
6412
|
+
const def = current?._def;
|
|
6413
|
+
const typeName = def?.typeName || "unknown";
|
|
6414
|
+
if (def?.description) description = def.description;
|
|
6415
|
+
if (!def) return { schema: current, typeName, optional, description, defaultValue };
|
|
6416
|
+
if (typeName === "ZodDefault") {
|
|
6417
|
+
optional = true;
|
|
6418
|
+
defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
6419
|
+
current = def.innerType || def.type;
|
|
6420
|
+
continue;
|
|
6421
|
+
}
|
|
6422
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable") {
|
|
6423
|
+
optional = true;
|
|
6424
|
+
current = def.innerType || def.type;
|
|
6425
|
+
continue;
|
|
6426
|
+
}
|
|
6427
|
+
return { schema: current, typeName, optional, description, defaultValue };
|
|
6428
|
+
}
|
|
6429
|
+
return { schema: current, typeName: current?._def?.typeName || "unknown", optional, description, defaultValue };
|
|
6430
|
+
}
|
|
6431
|
+
function zodTypeToContractType(typeName) {
|
|
6432
|
+
switch (typeName) {
|
|
6433
|
+
case "ZodString":
|
|
6434
|
+
return "string";
|
|
6435
|
+
case "ZodNumber":
|
|
6436
|
+
return "number";
|
|
6437
|
+
case "ZodBoolean":
|
|
6438
|
+
return "boolean";
|
|
6439
|
+
case "ZodEnum":
|
|
6440
|
+
case "ZodNativeEnum":
|
|
6441
|
+
return "enum";
|
|
6442
|
+
case "ZodArray":
|
|
6443
|
+
return "array";
|
|
6444
|
+
case "ZodObject":
|
|
6445
|
+
return "object";
|
|
6446
|
+
default:
|
|
6447
|
+
return typeName.replace(/^Zod/, "").toLowerCase() || "unknown";
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
function widgetFor(type, enumValues, schema) {
|
|
6451
|
+
if (enumValues) return "select";
|
|
6452
|
+
if (type === "boolean") return "checkbox";
|
|
6453
|
+
if (type === "number") return "number";
|
|
6454
|
+
if (type === "array") return "multi-select";
|
|
6455
|
+
if (type === "object") return "json";
|
|
6456
|
+
const checks = schema?._def?.checks;
|
|
6457
|
+
if (checks?.some((check) => check.kind === "url")) return "url";
|
|
6458
|
+
return "text";
|
|
6459
|
+
}
|
|
6460
|
+
function extractEnumValues(schema) {
|
|
6461
|
+
const def = schema?._def;
|
|
6462
|
+
const values = def?.values;
|
|
6463
|
+
if (Array.isArray(values)) return values.map(String);
|
|
6464
|
+
return void 0;
|
|
6465
|
+
}
|
|
6466
|
+
function summarizeZod(schema) {
|
|
6467
|
+
const unwrapped = unwrapZod(schema);
|
|
6468
|
+
if (unwrapped.typeName === "ZodArray") {
|
|
6469
|
+
const def = unwrapped.schema?._def;
|
|
6470
|
+
return {
|
|
6471
|
+
type: "array",
|
|
6472
|
+
items: summarizeZod(def?.type || def?.innerType)
|
|
6473
|
+
};
|
|
6474
|
+
}
|
|
6475
|
+
const shape = getShape(schema);
|
|
6476
|
+
if (!shape) {
|
|
6477
|
+
return {
|
|
6478
|
+
type: zodTypeToContractType(unwrapped.typeName),
|
|
6479
|
+
required: !unwrapped.optional,
|
|
6480
|
+
...unwrapped.description ? { description: unwrapped.description } : {}
|
|
6481
|
+
};
|
|
6482
|
+
}
|
|
6483
|
+
return Object.fromEntries(
|
|
6484
|
+
Object.entries(shape).map(([name, field]) => {
|
|
6485
|
+
const unwrapped2 = unwrapZod(field);
|
|
6486
|
+
return [name, {
|
|
6487
|
+
type: zodTypeToContractType(unwrapped2.typeName),
|
|
6488
|
+
required: !unwrapped2.optional,
|
|
6489
|
+
...unwrapped2.description ? { description: unwrapped2.description } : {}
|
|
6490
|
+
}];
|
|
6491
|
+
})
|
|
6492
|
+
);
|
|
6493
|
+
}
|
|
6494
|
+
function toLabel(name) {
|
|
6495
|
+
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
|
|
6496
|
+
}
|
|
6497
|
+
|
|
5694
6498
|
// src/plugin/loader.ts
|
|
5695
6499
|
var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
|
|
5696
6500
|
var XBrowserPluginLoader = class {
|
|
@@ -5733,6 +6537,13 @@ var XBrowserPluginLoader = class {
|
|
|
5733
6537
|
getLoadedPlugins() {
|
|
5734
6538
|
return this.loader.getLoadedPlugins();
|
|
5735
6539
|
}
|
|
6540
|
+
getPluginContract(siteName, commandName) {
|
|
6541
|
+
const site = this.core.loader.getSite(siteName);
|
|
6542
|
+
if (!site) return void 0;
|
|
6543
|
+
const contract = buildPluginContract(site);
|
|
6544
|
+
if (!commandName) return contract;
|
|
6545
|
+
return contract.commands.find((command) => command.name === commandName);
|
|
6546
|
+
}
|
|
5736
6547
|
async loadPlugin(pluginPath, id) {
|
|
5737
6548
|
return this.loader.loadPlugin(pluginPath, id);
|
|
5738
6549
|
}
|
|
@@ -6217,7 +7028,7 @@ function getTipsManager() {
|
|
|
6217
7028
|
|
|
6218
7029
|
// src/hooks/loader.ts
|
|
6219
7030
|
var builtinHooks = {
|
|
6220
|
-
screenshot: () => import("./screenshot-
|
|
7031
|
+
screenshot: () => import("./screenshot-CWAWMXVA.js").then((m) => m.screenshotHook)
|
|
6221
7032
|
};
|
|
6222
7033
|
async function loadHooks() {
|
|
6223
7034
|
const names = process.env.XBROWSER_HOOKS;
|
|
@@ -6236,45 +7047,13 @@ async function loadHooks() {
|
|
|
6236
7047
|
// src/executor.ts
|
|
6237
7048
|
import { homedir as homedir3 } from "os";
|
|
6238
7049
|
import { join as join3 } from "path";
|
|
6239
|
-
import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
6240
7050
|
var NAVIGATION_COMMANDS = /* @__PURE__ */ new Set(["goto", "back", "forward", "refresh"]);
|
|
6241
7051
|
var snapshotHintShown = /* @__PURE__ */ new WeakSet();
|
|
6242
7052
|
var STORAGE_DIR = join3(homedir3(), ".xbrowser", "storage");
|
|
6243
7053
|
var storageCache = /* @__PURE__ */ new Map();
|
|
6244
7054
|
function getPluginStorage(pluginName) {
|
|
6245
7055
|
if (!storageCache.has(pluginName)) {
|
|
6246
|
-
|
|
6247
|
-
let data = {};
|
|
6248
|
-
const load3 = () => {
|
|
6249
|
-
if (existsSync6(filePath)) {
|
|
6250
|
-
try {
|
|
6251
|
-
data = JSON.parse(readFileSync10(filePath, "utf-8"));
|
|
6252
|
-
} catch {
|
|
6253
|
-
data = {};
|
|
6254
|
-
}
|
|
6255
|
-
}
|
|
6256
|
-
};
|
|
6257
|
-
const save = () => {
|
|
6258
|
-
mkdirSync3(STORAGE_DIR, { recursive: true });
|
|
6259
|
-
writeFileSync5(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
6260
|
-
};
|
|
6261
|
-
load3();
|
|
6262
|
-
storageCache.set(pluginName, {
|
|
6263
|
-
get: async (key) => data[key] ?? null,
|
|
6264
|
-
set: async (key, value) => {
|
|
6265
|
-
data[key] = value;
|
|
6266
|
-
save();
|
|
6267
|
-
},
|
|
6268
|
-
delete: async (key) => {
|
|
6269
|
-
delete data[key];
|
|
6270
|
-
save();
|
|
6271
|
-
},
|
|
6272
|
-
clear: async () => {
|
|
6273
|
-
data = {};
|
|
6274
|
-
save();
|
|
6275
|
-
},
|
|
6276
|
-
keys: async () => Object.keys(data)
|
|
6277
|
-
});
|
|
7056
|
+
storageCache.set(pluginName, new PluginStorage(pluginName, STORAGE_DIR));
|
|
6278
7057
|
}
|
|
6279
7058
|
return storageCache.get(pluginName);
|
|
6280
7059
|
}
|
|
@@ -6335,7 +7114,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6335
7114
|
}
|
|
6336
7115
|
let targetPageOverride = null;
|
|
6337
7116
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
6338
|
-
const { findTargetPage } = await import("./browser-
|
|
7117
|
+
const { findTargetPage } = await import("./browser-DSVV4GHS.js");
|
|
6339
7118
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
6340
7119
|
if (!targetPageOverride) {
|
|
6341
7120
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -6352,7 +7131,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6352
7131
|
params = result.data;
|
|
6353
7132
|
}
|
|
6354
7133
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
6355
|
-
const { forwardExec } = await import("./daemon-client-
|
|
7134
|
+
const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
|
|
6356
7135
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
6357
7136
|
if (result) return result;
|
|
6358
7137
|
}
|
|
@@ -6360,7 +7139,18 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6360
7139
|
const existing = await findOrRestoreSession(sessionName, extraOpts?.cdpEndpoint);
|
|
6361
7140
|
if (existing) {
|
|
6362
7141
|
session = existing;
|
|
6363
|
-
if (
|
|
7142
|
+
if (session.page) {
|
|
7143
|
+
try {
|
|
7144
|
+
await Promise.race([
|
|
7145
|
+
session.page.evaluate(() => true),
|
|
7146
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3e3))
|
|
7147
|
+
]);
|
|
7148
|
+
} catch {
|
|
7149
|
+
await closeSessionByName(session.name);
|
|
7150
|
+
session = void 0;
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
if (session && targetPageOverride && session.page) {
|
|
6364
7154
|
const currentUrl = session.page.url();
|
|
6365
7155
|
if (currentUrl !== targetPageOverride.url) {
|
|
6366
7156
|
await session.page.goto(targetPageOverride.url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
@@ -6371,6 +7161,10 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6371
7161
|
session = await createSession(sessionName, params.url, {
|
|
6372
7162
|
cdpEndpoint: extraOpts?.cdpEndpoint
|
|
6373
7163
|
});
|
|
7164
|
+
} else if (command.scope === "browser") {
|
|
7165
|
+
session = await createSession(sessionName, void 0, {
|
|
7166
|
+
cdpEndpoint: extraOpts?.cdpEndpoint
|
|
7167
|
+
});
|
|
6374
7168
|
} else if (command.scope !== "project") {
|
|
6375
7169
|
return errorResult(
|
|
6376
7170
|
`Session '${sessionName}' not found. Run "xbrowser session open <url>" first.`
|
|
@@ -6381,7 +7175,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6381
7175
|
browser: session?.context.browser(),
|
|
6382
7176
|
browserContext: session?.context,
|
|
6383
7177
|
sessionId: session?.id,
|
|
6384
|
-
cdpEndpoint:
|
|
7178
|
+
cdpEndpoint: session?.cdpEndpoint || extraOpts?.cdpEndpoint,
|
|
6385
7179
|
args: [],
|
|
6386
7180
|
options: {},
|
|
6387
7181
|
cwd: process.cwd(),
|
|
@@ -6416,7 +7210,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6416
7210
|
let refTips = [];
|
|
6417
7211
|
if (session?.page && command.selectorParams && command.selectorParams.length > 0) {
|
|
6418
7212
|
const cache = /* @__PURE__ */ new Map();
|
|
6419
|
-
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache);
|
|
7213
|
+
const resolved = await resolveRefParams(session.page, params, command.selectorParams, cache, session.id);
|
|
6420
7214
|
if (resolved.tips.length > 0) {
|
|
6421
7215
|
refTips = resolved.tips;
|
|
6422
7216
|
params = resolved.params;
|
|
@@ -6486,7 +7280,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6486
7280
|
timestamp: start
|
|
6487
7281
|
});
|
|
6488
7282
|
if (isSuccess) {
|
|
6489
|
-
return { ...
|
|
7283
|
+
return { ...ok26(raw.data, merged.length > 0 ? merged : raw.tips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6490
7284
|
}
|
|
6491
7285
|
return { success: false, data: raw.data, message: raw.message, tips: merged.length > 0 ? merged : raw.tips || [], duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6492
7286
|
}
|
|
@@ -6499,7 +7293,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6499
7293
|
duration,
|
|
6500
7294
|
timestamp: start
|
|
6501
7295
|
});
|
|
6502
|
-
return { ...
|
|
7296
|
+
return { ...ok26(raw, smartTips), duration, ...hookOutputs ? { hookOutputs } : {} };
|
|
6503
7297
|
} catch (err) {
|
|
6504
7298
|
const end = Date.now();
|
|
6505
7299
|
const duration = end - start;
|
|
@@ -6647,7 +7441,7 @@ async function executeChain(input, options) {
|
|
|
6647
7441
|
results.push({
|
|
6648
7442
|
command: `${cmdName} ${subCommand}`,
|
|
6649
7443
|
raw: cmdStr,
|
|
6650
|
-
...
|
|
7444
|
+
...ok26(data),
|
|
6651
7445
|
duration: duration2,
|
|
6652
7446
|
...hookOutputs ? { hookOutputs } : {}
|
|
6653
7447
|
});
|
|
@@ -6864,13 +7658,13 @@ var sessionListBuiltin = {
|
|
|
6864
7658
|
},
|
|
6865
7659
|
execute: async () => {
|
|
6866
7660
|
try {
|
|
6867
|
-
const
|
|
6868
|
-
if (
|
|
7661
|
+
const sessions2 = await listSessions();
|
|
7662
|
+
if (sessions2.length === 0) {
|
|
6869
7663
|
console.log("No active sessions");
|
|
6870
7664
|
return;
|
|
6871
7665
|
}
|
|
6872
7666
|
console.log("Active sessions:");
|
|
6873
|
-
for (const s of
|
|
7667
|
+
for (const s of sessions2) {
|
|
6874
7668
|
console.log(` ${s.name} (${s.id})`);
|
|
6875
7669
|
}
|
|
6876
7670
|
} catch (e) {
|
|
@@ -6901,7 +7695,7 @@ var sessionKillBuiltin = {
|
|
|
6901
7695
|
};
|
|
6902
7696
|
|
|
6903
7697
|
// src/config.ts
|
|
6904
|
-
import { existsSync as
|
|
7698
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
6905
7699
|
import { join as join4 } from "path";
|
|
6906
7700
|
import { homedir as homedir4, tmpdir } from "os";
|
|
6907
7701
|
function getConfigFile() {
|
|
@@ -6909,14 +7703,14 @@ function getConfigFile() {
|
|
|
6909
7703
|
}
|
|
6910
7704
|
function loadConfig() {
|
|
6911
7705
|
const configFile = getConfigFile();
|
|
6912
|
-
if (!
|
|
7706
|
+
if (!existsSync6(configFile)) return {};
|
|
6913
7707
|
return readJsonFile(configFile, {});
|
|
6914
7708
|
}
|
|
6915
7709
|
function saveConfig(config) {
|
|
6916
7710
|
const dir = join4(homedir4() || tmpdir(), ".xbrowser");
|
|
6917
7711
|
const configFile = getConfigFile();
|
|
6918
|
-
if (!
|
|
6919
|
-
|
|
7712
|
+
if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
|
|
7713
|
+
writeFileSync5(configFile, JSON.stringify(config, null, 2), "utf-8");
|
|
6920
7714
|
}
|
|
6921
7715
|
function getConfigValue(key) {
|
|
6922
7716
|
return loadConfig()[key];
|
|
@@ -7024,26 +7818,26 @@ var configBuiltin = {
|
|
|
7024
7818
|
|
|
7025
7819
|
// src/plugin/installer.ts
|
|
7026
7820
|
import {
|
|
7027
|
-
existsSync as
|
|
7821
|
+
existsSync as existsSync13,
|
|
7028
7822
|
readdirSync as readdirSync3,
|
|
7029
|
-
mkdirSync as
|
|
7823
|
+
mkdirSync as mkdirSync8,
|
|
7030
7824
|
rmSync as rmSync7
|
|
7031
7825
|
} from "fs";
|
|
7032
7826
|
import { resolve as resolve15, basename as basename2 } from "path";
|
|
7033
7827
|
import { homedir as homedir5 } from "os";
|
|
7034
7828
|
|
|
7035
7829
|
// src/plugin/install-sources/local.ts
|
|
7036
|
-
import { existsSync as
|
|
7830
|
+
import { existsSync as existsSync8, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
|
|
7037
7831
|
import { resolve as resolve10 } from "path";
|
|
7038
7832
|
|
|
7039
7833
|
// src/plugin/install-utils.ts
|
|
7040
7834
|
import {
|
|
7041
|
-
existsSync as
|
|
7835
|
+
existsSync as existsSync7,
|
|
7042
7836
|
readdirSync as readdirSync2,
|
|
7043
7837
|
cpSync,
|
|
7044
7838
|
rmSync,
|
|
7045
|
-
mkdirSync as
|
|
7046
|
-
readFileSync as
|
|
7839
|
+
mkdirSync as mkdirSync4,
|
|
7840
|
+
readFileSync as readFileSync10,
|
|
7047
7841
|
createWriteStream
|
|
7048
7842
|
} from "fs";
|
|
7049
7843
|
import { resolve as resolve9 } from "path";
|
|
@@ -7114,7 +7908,7 @@ async function downloadToFile(url, destPath) {
|
|
|
7114
7908
|
await pipeline(nodeStream, createWriteStream(destPath));
|
|
7115
7909
|
}
|
|
7116
7910
|
function extractTarGz(tarballPath, targetDir) {
|
|
7117
|
-
|
|
7911
|
+
mkdirSync4(targetDir, { recursive: true });
|
|
7118
7912
|
execSync7(`tar -xzf "${tarballPath}" -C "${targetDir}"`, { stdio: "pipe" });
|
|
7119
7913
|
}
|
|
7120
7914
|
function flattenPackageRoot(targetDir) {
|
|
@@ -7135,18 +7929,18 @@ function flattenPackageRoot(targetDir) {
|
|
|
7135
7929
|
async function verifyPlugin(dir) {
|
|
7136
7930
|
const warnings = [];
|
|
7137
7931
|
const indexPath = resolve9(dir, "index.ts");
|
|
7138
|
-
if (!
|
|
7932
|
+
if (!existsSync7(indexPath)) {
|
|
7139
7933
|
const indexJs = resolve9(dir, "index.js");
|
|
7140
|
-
if (!
|
|
7934
|
+
if (!existsSync7(indexJs)) {
|
|
7141
7935
|
return { valid: false, error: "No index.ts or index.js entry point found", warnings };
|
|
7142
7936
|
}
|
|
7143
7937
|
}
|
|
7144
7938
|
const pkgPath = resolve9(dir, "package.json");
|
|
7145
|
-
if (!
|
|
7939
|
+
if (!existsSync7(pkgPath)) {
|
|
7146
7940
|
warnings.push("No package.json found");
|
|
7147
7941
|
} else {
|
|
7148
7942
|
try {
|
|
7149
|
-
const pkg2 = JSON.parse(
|
|
7943
|
+
const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
7150
7944
|
if (!pkg2.xbrowser) {
|
|
7151
7945
|
warnings.push("No xbrowser metadata in package.json");
|
|
7152
7946
|
}
|
|
@@ -7166,7 +7960,7 @@ function safeCleanup(dir) {
|
|
|
7166
7960
|
// src/plugin/install-sources/local.ts
|
|
7167
7961
|
async function installFromLocal(source, name, targetDir) {
|
|
7168
7962
|
const srcPath = resolve10(source);
|
|
7169
|
-
if (!
|
|
7963
|
+
if (!existsSync8(srcPath)) {
|
|
7170
7964
|
throw new Error(`Local path does not exist: ${srcPath}`);
|
|
7171
7965
|
}
|
|
7172
7966
|
const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
|
|
@@ -7179,7 +7973,7 @@ async function installFromLocal(source, name, targetDir) {
|
|
|
7179
7973
|
safeCleanup(tmpTarget);
|
|
7180
7974
|
throw new Error(`Invalid plugin: ${verify.error}`);
|
|
7181
7975
|
}
|
|
7182
|
-
if (
|
|
7976
|
+
if (existsSync8(targetDir)) {
|
|
7183
7977
|
rmSync2(targetDir, { recursive: true, force: true });
|
|
7184
7978
|
}
|
|
7185
7979
|
cpSync2(tmpTarget, targetDir, { recursive: true, force: true });
|
|
@@ -7199,7 +7993,7 @@ async function installFromLocal(source, name, targetDir) {
|
|
|
7199
7993
|
}
|
|
7200
7994
|
|
|
7201
7995
|
// src/plugin/install-sources/npm.ts
|
|
7202
|
-
import { existsSync as
|
|
7996
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
|
|
7203
7997
|
import { resolve as resolve11, join as join5 } from "path";
|
|
7204
7998
|
import { tmpdir as tmpdir2 } from "os";
|
|
7205
7999
|
async function installFromNpm(packageName, name, targetDir) {
|
|
@@ -7220,7 +8014,7 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
7220
8014
|
}
|
|
7221
8015
|
const tarballUrl = versionMeta.dist.tarball;
|
|
7222
8016
|
const tmpDir = join5(tmpdir2(), `xbrowser-npm-${Date.now()}`);
|
|
7223
|
-
|
|
8017
|
+
mkdirSync5(tmpDir, { recursive: true });
|
|
7224
8018
|
let warnings = [];
|
|
7225
8019
|
try {
|
|
7226
8020
|
const tarballPath = join5(tmpDir, `${name}.tgz`);
|
|
@@ -7233,16 +8027,16 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
7233
8027
|
if (!verify.valid) {
|
|
7234
8028
|
throw new Error(`Invalid npm plugin: ${verify.error}`);
|
|
7235
8029
|
}
|
|
7236
|
-
if (
|
|
8030
|
+
if (existsSync9(targetDir)) {
|
|
7237
8031
|
rmSync3(targetDir, { recursive: true, force: true });
|
|
7238
8032
|
}
|
|
7239
8033
|
cpSync3(extractDir, targetDir, { recursive: true, force: true });
|
|
7240
8034
|
const pkgPath = resolve11(targetDir, "package.json");
|
|
7241
|
-
if (
|
|
7242
|
-
const pkg2 = JSON.parse(
|
|
8035
|
+
if (existsSync9(pkgPath)) {
|
|
8036
|
+
const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
7243
8037
|
if (!pkg2._npmSource) {
|
|
7244
8038
|
pkg2._npmSource = { name: packageName, version: latestVersion };
|
|
7245
|
-
|
|
8039
|
+
writeFileSync6(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
7246
8040
|
}
|
|
7247
8041
|
}
|
|
7248
8042
|
} finally {
|
|
@@ -7259,7 +8053,7 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
7259
8053
|
}
|
|
7260
8054
|
|
|
7261
8055
|
// src/plugin/install-sources/git.ts
|
|
7262
|
-
import { existsSync as
|
|
8056
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync7, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
|
|
7263
8057
|
import { resolve as resolve12, join as join6 } from "path";
|
|
7264
8058
|
import { tmpdir as tmpdir3 } from "os";
|
|
7265
8059
|
import { execSync as execSync8 } from "child_process";
|
|
@@ -7273,17 +8067,17 @@ async function installFromGit(gitUrl, name, targetDir) {
|
|
|
7273
8067
|
if (!verify.valid) {
|
|
7274
8068
|
throw new Error(`Invalid git plugin: ${verify.error}`);
|
|
7275
8069
|
}
|
|
7276
|
-
if (
|
|
8070
|
+
if (existsSync10(targetDir)) {
|
|
7277
8071
|
rmSync4(targetDir, { recursive: true, force: true });
|
|
7278
8072
|
}
|
|
7279
8073
|
cpSync4(tmpDir, targetDir, { recursive: true, force: true });
|
|
7280
8074
|
rmSync4(resolve12(targetDir, ".git"), { recursive: true, force: true });
|
|
7281
8075
|
const pkgPath = resolve12(targetDir, "package.json");
|
|
7282
|
-
if (
|
|
7283
|
-
const pkg2 = JSON.parse(
|
|
8076
|
+
if (existsSync10(pkgPath)) {
|
|
8077
|
+
const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
7284
8078
|
if (!pkg2._gitSource) {
|
|
7285
8079
|
pkg2._gitSource = { url: gitUrl };
|
|
7286
|
-
|
|
8080
|
+
writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
7287
8081
|
}
|
|
7288
8082
|
}
|
|
7289
8083
|
} finally {
|
|
@@ -7300,12 +8094,12 @@ async function installFromGit(gitUrl, name, targetDir) {
|
|
|
7300
8094
|
}
|
|
7301
8095
|
|
|
7302
8096
|
// src/plugin/install-sources/url.ts
|
|
7303
|
-
import { existsSync as
|
|
8097
|
+
import { existsSync as existsSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, rmSync as rmSync5, cpSync as cpSync5 } from "fs";
|
|
7304
8098
|
import { resolve as resolve13, join as join7, basename } from "path";
|
|
7305
8099
|
import { tmpdir as tmpdir4 } from "os";
|
|
7306
8100
|
async function installFromUrl(url, name, targetDir) {
|
|
7307
8101
|
const tmpDir = join7(tmpdir4(), `xbrowser-url-${Date.now()}`);
|
|
7308
|
-
|
|
8102
|
+
mkdirSync6(tmpDir, { recursive: true });
|
|
7309
8103
|
let warnings = [];
|
|
7310
8104
|
try {
|
|
7311
8105
|
const fileName = basename(new URL(url).pathname) || "plugin.tar.gz";
|
|
@@ -7319,16 +8113,16 @@ async function installFromUrl(url, name, targetDir) {
|
|
|
7319
8113
|
if (!verify.valid) {
|
|
7320
8114
|
throw new Error(`Invalid plugin from URL: ${verify.error}`);
|
|
7321
8115
|
}
|
|
7322
|
-
if (
|
|
8116
|
+
if (existsSync11(targetDir)) {
|
|
7323
8117
|
rmSync5(targetDir, { recursive: true, force: true });
|
|
7324
8118
|
}
|
|
7325
8119
|
cpSync5(extractDir, targetDir, { recursive: true, force: true });
|
|
7326
8120
|
const pkgPath = resolve13(targetDir, "package.json");
|
|
7327
|
-
if (
|
|
7328
|
-
const pkg2 = JSON.parse(
|
|
8121
|
+
if (existsSync11(pkgPath)) {
|
|
8122
|
+
const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
7329
8123
|
if (!pkg2._urlSource) {
|
|
7330
8124
|
pkg2._urlSource = { url };
|
|
7331
|
-
|
|
8125
|
+
writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
|
|
7332
8126
|
}
|
|
7333
8127
|
}
|
|
7334
8128
|
} finally {
|
|
@@ -7346,10 +8140,10 @@ async function installFromUrl(url, name, targetDir) {
|
|
|
7346
8140
|
|
|
7347
8141
|
// src/plugin/install-sources/marketplace.ts
|
|
7348
8142
|
import {
|
|
7349
|
-
existsSync as
|
|
7350
|
-
mkdirSync as
|
|
7351
|
-
writeFileSync as
|
|
7352
|
-
readFileSync as
|
|
8143
|
+
existsSync as existsSync12,
|
|
8144
|
+
mkdirSync as mkdirSync7,
|
|
8145
|
+
writeFileSync as writeFileSync9,
|
|
8146
|
+
readFileSync as readFileSync14,
|
|
7353
8147
|
rmSync as rmSync6,
|
|
7354
8148
|
cpSync as cpSync6
|
|
7355
8149
|
} from "fs";
|
|
@@ -7371,12 +8165,12 @@ async function installFromMarketplace(pluginsDir, slug, options) {
|
|
|
7371
8165
|
const plugin = detailData.data;
|
|
7372
8166
|
const name = options?.name || String(plugin.slug || slug);
|
|
7373
8167
|
const targetDir = resolve14(pluginsDir, name);
|
|
7374
|
-
if (
|
|
8168
|
+
if (existsSync12(targetDir) && !options?.force) {
|
|
7375
8169
|
throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
|
|
7376
8170
|
}
|
|
7377
|
-
|
|
8171
|
+
mkdirSync7(targetDir, { recursive: true });
|
|
7378
8172
|
const tmpDir = join8(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
|
|
7379
|
-
|
|
8173
|
+
mkdirSync7(tmpDir, { recursive: true });
|
|
7380
8174
|
const realSlug = String(plugin.slug || slug);
|
|
7381
8175
|
try {
|
|
7382
8176
|
await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
|
|
@@ -7406,14 +8200,14 @@ function isManifestArray(data) {
|
|
|
7406
8200
|
return Array.isArray(data) && data.length > 0 && typeof data[0].path === "string" && typeof data[0].content === "string";
|
|
7407
8201
|
}
|
|
7408
8202
|
function extractManifestToDir(manifest, targetDir) {
|
|
7409
|
-
if (
|
|
8203
|
+
if (existsSync12(targetDir)) {
|
|
7410
8204
|
rmSync6(targetDir, { recursive: true, force: true });
|
|
7411
8205
|
}
|
|
7412
|
-
|
|
8206
|
+
mkdirSync7(targetDir, { recursive: true });
|
|
7413
8207
|
for (const file of manifest) {
|
|
7414
8208
|
const filePath = resolve14(targetDir, file.path);
|
|
7415
|
-
|
|
7416
|
-
|
|
8209
|
+
mkdirSync7(dirname2(filePath), { recursive: true });
|
|
8210
|
+
writeFileSync9(filePath, Buffer.from(file.content, "base64"));
|
|
7417
8211
|
}
|
|
7418
8212
|
}
|
|
7419
8213
|
function tryParseAsGzippedManifest(buffer) {
|
|
@@ -7440,7 +8234,7 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
|
|
|
7440
8234
|
const redirectUrl = tarballRes.headers.get("location");
|
|
7441
8235
|
const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
|
|
7442
8236
|
await downloadToFile(redirectUrl, tarballPath);
|
|
7443
|
-
const buffer =
|
|
8237
|
+
const buffer = readFileSync14(tarballPath);
|
|
7444
8238
|
const manifest = tryParseAsGzippedManifest(buffer);
|
|
7445
8239
|
if (manifest) {
|
|
7446
8240
|
extractManifestToDir(manifest, targetDir);
|
|
@@ -7449,7 +8243,7 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
|
|
|
7449
8243
|
const extractDir = join8(tmpDir, "extracted");
|
|
7450
8244
|
extractTarGz(tarballPath, extractDir);
|
|
7451
8245
|
flattenPackageRoot(extractDir);
|
|
7452
|
-
if (
|
|
8246
|
+
if (existsSync12(targetDir)) {
|
|
7453
8247
|
rmSync6(targetDir, { recursive: true, force: true });
|
|
7454
8248
|
}
|
|
7455
8249
|
cpSync6(extractDir, targetDir, { recursive: true, force: true });
|
|
@@ -7461,12 +8255,12 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
|
|
|
7461
8255
|
return;
|
|
7462
8256
|
}
|
|
7463
8257
|
const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
|
|
7464
|
-
|
|
8258
|
+
writeFileSync9(tarballPath, buffer);
|
|
7465
8259
|
try {
|
|
7466
8260
|
const extractDir = join8(tmpDir, "extracted");
|
|
7467
8261
|
extractTarGz(tarballPath, extractDir);
|
|
7468
8262
|
flattenPackageRoot(extractDir);
|
|
7469
|
-
if (
|
|
8263
|
+
if (existsSync12(targetDir)) {
|
|
7470
8264
|
rmSync6(targetDir, { recursive: true, force: true });
|
|
7471
8265
|
}
|
|
7472
8266
|
cpSync6(extractDir, targetDir, { recursive: true, force: true });
|
|
@@ -7502,24 +8296,24 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
|
|
|
7502
8296
|
}
|
|
7503
8297
|
};
|
|
7504
8298
|
const pkgPath = resolve14(targetDir, "package.json");
|
|
7505
|
-
if (!
|
|
7506
|
-
|
|
8299
|
+
if (!existsSync12(pkgPath)) {
|
|
8300
|
+
writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
7507
8301
|
} else {
|
|
7508
8302
|
try {
|
|
7509
|
-
const existing = JSON.parse(
|
|
8303
|
+
const existing = JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
7510
8304
|
const merged = {
|
|
7511
8305
|
...existing,
|
|
7512
8306
|
xbrowser: { ...existing.xbrowser, ...packageJson.xbrowser },
|
|
7513
8307
|
_marketplace: packageJson._marketplace
|
|
7514
8308
|
};
|
|
7515
|
-
|
|
8309
|
+
writeFileSync9(pkgPath, JSON.stringify(merged, null, 2));
|
|
7516
8310
|
} catch {
|
|
7517
|
-
|
|
8311
|
+
writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
7518
8312
|
}
|
|
7519
8313
|
}
|
|
7520
8314
|
}
|
|
7521
8315
|
function ensureIndexFile(plugin, name, targetDir) {
|
|
7522
|
-
if (!
|
|
8316
|
+
if (!existsSync12(resolve14(targetDir, "index.ts")) && !existsSync12(resolve14(targetDir, "index.js"))) {
|
|
7523
8317
|
const commands = plugin.commands || [];
|
|
7524
8318
|
const commandHandlers = commands.length > 0 ? commands.map((cmd) => {
|
|
7525
8319
|
return [
|
|
@@ -7534,7 +8328,7 @@ function ensureIndexFile(plugin, name, targetDir) {
|
|
|
7534
8328
|
` handler: async () => ({ data: { message: 'Hello from ${name}!' }, tips: [] }),`,
|
|
7535
8329
|
` });`
|
|
7536
8330
|
].join("\n");
|
|
7537
|
-
|
|
8331
|
+
writeFileSync9(
|
|
7538
8332
|
resolve14(targetDir, "index.ts"),
|
|
7539
8333
|
[
|
|
7540
8334
|
`import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
|
|
@@ -7573,10 +8367,10 @@ var PluginInstaller = class {
|
|
|
7573
8367
|
const type = this.detectSourceType(source);
|
|
7574
8368
|
const name = options?.name || this.deriveName(source, type);
|
|
7575
8369
|
const targetDir = resolve15(this.pluginsDir, name);
|
|
7576
|
-
if (
|
|
8370
|
+
if (existsSync13(targetDir) && !options?.force) {
|
|
7577
8371
|
throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
|
|
7578
8372
|
}
|
|
7579
|
-
|
|
8373
|
+
mkdirSync8(targetDir, { recursive: true });
|
|
7580
8374
|
const resolvedSource = type === "npm" ? await resolveNpmPackageWithFallback(source) : source;
|
|
7581
8375
|
switch (type) {
|
|
7582
8376
|
case "local":
|
|
@@ -7634,7 +8428,7 @@ var PluginInstaller = class {
|
|
|
7634
8428
|
*/
|
|
7635
8429
|
async uninstall(name) {
|
|
7636
8430
|
const targetDir = resolve15(this.pluginsDir, name);
|
|
7637
|
-
if (!
|
|
8431
|
+
if (!existsSync13(targetDir)) {
|
|
7638
8432
|
throw new Error(`Plugin "${name}" not found`);
|
|
7639
8433
|
}
|
|
7640
8434
|
rmSync7(targetDir, { recursive: true, force: true });
|
|
@@ -7645,7 +8439,7 @@ var PluginInstaller = class {
|
|
|
7645
8439
|
* @returns Array of installed plugin information.
|
|
7646
8440
|
*/
|
|
7647
8441
|
async list(_options) {
|
|
7648
|
-
if (!
|
|
8442
|
+
if (!existsSync13(this.pluginsDir)) return [];
|
|
7649
8443
|
const entries = readdirSync3(this.pluginsDir, { withFileTypes: true });
|
|
7650
8444
|
const plugins = [];
|
|
7651
8445
|
for (const entry of entries) {
|
|
@@ -7653,7 +8447,7 @@ var PluginInstaller = class {
|
|
|
7653
8447
|
const pluginPath = resolve15(this.pluginsDir, entry.name);
|
|
7654
8448
|
const indexPath = resolve15(pluginPath, "index.ts");
|
|
7655
8449
|
const indexJsPath = resolve15(pluginPath, "index.js");
|
|
7656
|
-
if (!
|
|
8450
|
+
if (!existsSync13(indexPath) && !existsSync13(indexJsPath)) continue;
|
|
7657
8451
|
const metadata = PluginMetadataParser.parseFromPackageJson(pluginPath);
|
|
7658
8452
|
let source = "local";
|
|
7659
8453
|
const pkg2 = readJsonFile(resolve15(pluginPath, "package.json"), {});
|
|
@@ -7680,10 +8474,10 @@ var PluginInstaller = class {
|
|
|
7680
8474
|
}
|
|
7681
8475
|
if (source.startsWith("file://")) {
|
|
7682
8476
|
const filePath = decodeURIComponent(new URL(source).pathname);
|
|
7683
|
-
if (
|
|
8477
|
+
if (existsSync13(filePath)) return "url";
|
|
7684
8478
|
}
|
|
7685
8479
|
if (source.endsWith(".git") || source.includes("github.com/")) return "git";
|
|
7686
|
-
if (
|
|
8480
|
+
if (existsSync13(resolve15(source))) return "local";
|
|
7687
8481
|
return "npm";
|
|
7688
8482
|
}
|
|
7689
8483
|
deriveName(source, type) {
|
|
@@ -7758,6 +8552,7 @@ function handlePluginHelp() {
|
|
|
7758
8552
|
" install <slug> --from-marketplace Install from marketplace",
|
|
7759
8553
|
" uninstall <name> Uninstall a plugin",
|
|
7760
8554
|
" list [--json] List installed plugins",
|
|
8555
|
+
" schema <name> [command] [--json] Show plugin contract and command forms",
|
|
7761
8556
|
" reload <name> Reload a plugin",
|
|
7762
8557
|
"",
|
|
7763
8558
|
"Examples:",
|
|
@@ -7766,6 +8561,7 @@ function handlePluginHelp() {
|
|
|
7766
8561
|
" xbrowser plugin install ./my-plugin",
|
|
7767
8562
|
" xbrowser plugin uninstall my-plugin",
|
|
7768
8563
|
" xbrowser plugin list",
|
|
8564
|
+
" xbrowser plugin schema my-plugin --json",
|
|
7769
8565
|
" xbrowser plugin reload my-plugin"
|
|
7770
8566
|
].join("\n");
|
|
7771
8567
|
}
|
|
@@ -8823,6 +9619,25 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8823
9619
|
cmdName = "text";
|
|
8824
9620
|
params = { selector: options.selector || options.s };
|
|
8825
9621
|
break;
|
|
9622
|
+
case "find": {
|
|
9623
|
+
if (!args[0] || !args[1]) {
|
|
9624
|
+
outputError("Usage: xbrowser find <text|role|label|placeholder|testid|alt|title|first|last|nth> <value> [action] [--name <name>]");
|
|
9625
|
+
}
|
|
9626
|
+
const strategy = args[0];
|
|
9627
|
+
const value = args[1];
|
|
9628
|
+
const operation = args.slice(2).join(" ") || void 0;
|
|
9629
|
+
cmdName = "find";
|
|
9630
|
+
params = {
|
|
9631
|
+
strategy,
|
|
9632
|
+
value,
|
|
9633
|
+
...operation ? { operation } : {},
|
|
9634
|
+
name: options.name,
|
|
9635
|
+
exact: !!options.exact,
|
|
9636
|
+
timeout: options.timeout ? Number(options.timeout) : void 0,
|
|
9637
|
+
index: options.index ? Number(options.index) : void 0
|
|
9638
|
+
};
|
|
9639
|
+
break;
|
|
9640
|
+
}
|
|
8826
9641
|
case "back":
|
|
8827
9642
|
cmdName = "back";
|
|
8828
9643
|
params = {};
|
|
@@ -8845,7 +9660,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
8845
9660
|
let parsedActions;
|
|
8846
9661
|
if (options.action) {
|
|
8847
9662
|
const actionList = Array.isArray(options.action) ? options.action : [options.action];
|
|
8848
|
-
const { parseActionDsl } = await import("./parse-action-dsl-
|
|
9663
|
+
const { parseActionDsl } = await import("./parse-action-dsl-T3DYC33D.js");
|
|
8849
9664
|
parsedActions = actionList.map((a) => parseActionDsl(a));
|
|
8850
9665
|
} else if (options["actions-file"]) {
|
|
8851
9666
|
const fs3 = await import("fs");
|
|
@@ -9014,11 +9829,11 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
9014
9829
|
case "list":
|
|
9015
9830
|
case "ls": {
|
|
9016
9831
|
try {
|
|
9017
|
-
const
|
|
9018
|
-
outputResult({ sessions }, mode);
|
|
9832
|
+
const sessions2 = await forwardSessionList();
|
|
9833
|
+
outputResult({ sessions: sessions2 }, mode);
|
|
9019
9834
|
} catch {
|
|
9020
|
-
const
|
|
9021
|
-
outputResult({ sessions }, mode);
|
|
9835
|
+
const sessions2 = await listSessions();
|
|
9836
|
+
outputResult({ sessions: sessions2 }, mode);
|
|
9022
9837
|
}
|
|
9023
9838
|
break;
|
|
9024
9839
|
}
|
|
@@ -9038,8 +9853,8 @@ async function handleSession(args, options, mode, cdpEndpoint) {
|
|
|
9038
9853
|
}
|
|
9039
9854
|
case "kill-all": {
|
|
9040
9855
|
try {
|
|
9041
|
-
const
|
|
9042
|
-
for (const s of
|
|
9856
|
+
const sessions2 = await forwardSessionList();
|
|
9857
|
+
for (const s of sessions2) {
|
|
9043
9858
|
try {
|
|
9044
9859
|
await forwardSessionClose(s.name);
|
|
9045
9860
|
} catch {
|
|
@@ -9066,15 +9881,28 @@ function getPluginLoader2() {
|
|
|
9066
9881
|
if (!pluginLoader3) pluginLoader3 = new XBrowserPluginLoader();
|
|
9067
9882
|
return pluginLoader3;
|
|
9068
9883
|
}
|
|
9069
|
-
async function
|
|
9884
|
+
async function buildRuntimePluginInfo() {
|
|
9070
9885
|
const loader = await getPluginLoader();
|
|
9071
9886
|
const sites = loader.getCore().loader.getSites();
|
|
9072
9887
|
const map = /* @__PURE__ */ new Map();
|
|
9073
9888
|
for (const site of sites) {
|
|
9074
|
-
const cmds = site.getAllCommands()
|
|
9075
|
-
|
|
9076
|
-
|
|
9889
|
+
const cmds = site.getAllCommands();
|
|
9890
|
+
const commandNames = cmds.map((c) => c.name);
|
|
9891
|
+
if (commandNames.length === 0) continue;
|
|
9892
|
+
const anySite = site;
|
|
9893
|
+
const hasLoginHandler = typeof anySite.hasLoginCommand === "function" && anySite.hasLoginCommand();
|
|
9894
|
+
const configRequiresLogin = !!site.config.requiresLogin;
|
|
9895
|
+
const hasLogin = hasLoginHandler || configRequiresLogin;
|
|
9896
|
+
let loggedIn = null;
|
|
9897
|
+
if (hasLogin) {
|
|
9898
|
+
try {
|
|
9899
|
+
loggedIn = await site.isLoggedIn();
|
|
9900
|
+
} catch {
|
|
9901
|
+
loggedIn = null;
|
|
9902
|
+
}
|
|
9077
9903
|
}
|
|
9904
|
+
const requiresLoginCommands = cmds.filter((c) => c.requiresLogin === true).map((c) => c.name);
|
|
9905
|
+
map.set(site.name, { commands: commandNames, hasLogin, loggedIn, requiresLoginCommands });
|
|
9078
9906
|
}
|
|
9079
9907
|
return map;
|
|
9080
9908
|
}
|
|
@@ -9238,6 +10066,54 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
9238
10066
|
console.error("\u67E5\u8BE2\u5931\u8D25:", err.message);
|
|
9239
10067
|
}
|
|
9240
10068
|
}
|
|
10069
|
+
async function handlePluginSchema(args, mode) {
|
|
10070
|
+
const pluginName = args[0];
|
|
10071
|
+
const commandName = args[1];
|
|
10072
|
+
if (!pluginName) outputError("Usage: xbrowser plugin schema <name> [command] [--json]");
|
|
10073
|
+
const loader = await getPluginLoader();
|
|
10074
|
+
const contract = loader.getPluginContract(pluginName, commandName);
|
|
10075
|
+
if (!contract) {
|
|
10076
|
+
outputError(commandName ? `Command "${commandName}" not found in plugin "${pluginName}"` : `Plugin "${pluginName}" not found`);
|
|
10077
|
+
return;
|
|
10078
|
+
}
|
|
10079
|
+
if (mode === "json") {
|
|
10080
|
+
outputResult(contract, mode);
|
|
10081
|
+
return;
|
|
10082
|
+
}
|
|
10083
|
+
if ("commands" in contract) {
|
|
10084
|
+
printPluginContract(contract);
|
|
10085
|
+
} else {
|
|
10086
|
+
printCommandContract(pluginName, contract);
|
|
10087
|
+
}
|
|
10088
|
+
}
|
|
10089
|
+
function printPluginContract(contract) {
|
|
10090
|
+
console.log(`${contract.plugin.name} contract v${contract.version}`);
|
|
10091
|
+
if (contract.plugin.description) console.log(contract.plugin.description);
|
|
10092
|
+
console.log("");
|
|
10093
|
+
for (const command of contract.commands) {
|
|
10094
|
+
printCommandContract(contract.plugin.name, command);
|
|
10095
|
+
}
|
|
10096
|
+
}
|
|
10097
|
+
function printCommandContract(pluginName, command) {
|
|
10098
|
+
console.log(`${pluginName} ${command.name}`);
|
|
10099
|
+
if (command.description) console.log(` ${command.description}`);
|
|
10100
|
+
console.log(` scope: ${command.scope}`);
|
|
10101
|
+
if (command.capabilities.length > 0) {
|
|
10102
|
+
console.log(` capabilities: ${command.capabilities.join(", ")}`);
|
|
10103
|
+
}
|
|
10104
|
+
if (command.positional.length > 0) {
|
|
10105
|
+
console.log(` positional: ${command.positional.join(", ")}`);
|
|
10106
|
+
}
|
|
10107
|
+
if (command.form.fields.length > 0) {
|
|
10108
|
+
console.log(" fields:");
|
|
10109
|
+
for (const field of command.form.fields) {
|
|
10110
|
+
const required = field.required ? "required" : "optional";
|
|
10111
|
+
const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
|
|
10112
|
+
console.log(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}`);
|
|
10113
|
+
}
|
|
10114
|
+
}
|
|
10115
|
+
console.log("");
|
|
10116
|
+
}
|
|
9241
10117
|
async function handlePlugin(args, options, mode) {
|
|
9242
10118
|
const sub = args[0];
|
|
9243
10119
|
const subArgs = args.slice(1);
|
|
@@ -9278,17 +10154,20 @@ async function handlePlugin(args, options, mode) {
|
|
|
9278
10154
|
}
|
|
9279
10155
|
case "list": {
|
|
9280
10156
|
const plugins = await installer.list();
|
|
9281
|
-
const
|
|
10157
|
+
const runtimeInfo = await buildRuntimePluginInfo();
|
|
9282
10158
|
const enrichedPlugins = plugins.map((p) => {
|
|
9283
10159
|
const metadata = p.metadata;
|
|
9284
10160
|
const staticCommands = metadata?.commands;
|
|
9285
|
-
const
|
|
9286
|
-
const commands =
|
|
10161
|
+
const rt = runtimeInfo.get(p.name);
|
|
10162
|
+
const commands = rt?.commands || staticCommands;
|
|
9287
10163
|
return {
|
|
9288
10164
|
...p,
|
|
9289
10165
|
commands,
|
|
9290
10166
|
version: metadata?.version,
|
|
9291
|
-
description: metadata?.description
|
|
10167
|
+
description: metadata?.description,
|
|
10168
|
+
hasLogin: rt?.hasLogin ?? false,
|
|
10169
|
+
loggedIn: rt?.loggedIn ?? null,
|
|
10170
|
+
requiresLoginCommands: rt?.requiresLoginCommands ?? []
|
|
9292
10171
|
};
|
|
9293
10172
|
});
|
|
9294
10173
|
if (mode === "json") {
|
|
@@ -9299,20 +10178,27 @@ async function handlePlugin(args, options, mode) {
|
|
|
9299
10178
|
return;
|
|
9300
10179
|
}
|
|
9301
10180
|
for (const p of enrichedPlugins) {
|
|
10181
|
+
const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
|
|
9302
10182
|
if (p.version && p.description) {
|
|
9303
|
-
console.log(`${p.name} (${p.version}) - ${p.description}`);
|
|
10183
|
+
console.log(`${p.name} (${p.version}) - ${p.description}${loginTag}`);
|
|
9304
10184
|
} else {
|
|
9305
|
-
console.log(p.name);
|
|
10185
|
+
console.log(`${p.name}${loginTag}`);
|
|
9306
10186
|
}
|
|
9307
10187
|
if (p.commands && p.commands.length > 0) {
|
|
9308
10188
|
console.log(` ${p.commands.join(", ")}`);
|
|
9309
10189
|
}
|
|
10190
|
+
if (p.requiresLoginCommands.length > 0) {
|
|
10191
|
+
console.log(` requires login: ${p.requiresLoginCommands.join(", ")}`);
|
|
10192
|
+
}
|
|
9310
10193
|
}
|
|
9311
10194
|
console.log(`
|
|
9312
10195
|
Total: ${enrichedPlugins.length} plugins`);
|
|
9313
10196
|
}
|
|
9314
10197
|
break;
|
|
9315
10198
|
}
|
|
10199
|
+
case "schema":
|
|
10200
|
+
await handlePluginSchema(subArgs, mode);
|
|
10201
|
+
break;
|
|
9316
10202
|
case "reload": {
|
|
9317
10203
|
const name = subArgs[0];
|
|
9318
10204
|
if (!name) outputError("Usage: xbrowser plugin reload <name>");
|
|
@@ -9660,7 +10546,7 @@ async function handleConvert(args, _mode) {
|
|
|
9660
10546
|
const fs3 = await import("fs");
|
|
9661
10547
|
const path3 = await import("path");
|
|
9662
10548
|
const { default: yaml } = await import("yaml");
|
|
9663
|
-
const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-
|
|
10549
|
+
const { generateJSScript, generatePythonScript, generateBashScript } = await import("./convert-EGFYNICZ.js");
|
|
9664
10550
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
9665
10551
|
const recording = yaml.parse(content);
|
|
9666
10552
|
const ext = path3.extname(outputPath).toLowerCase();
|
|
@@ -9685,7 +10571,7 @@ async function handleExtract(args, _mode) {
|
|
|
9685
10571
|
console.error("Usage: xbrowser extract <recording.yaml>");
|
|
9686
10572
|
process.exit(1);
|
|
9687
10573
|
}
|
|
9688
|
-
const { extractAndSave, printExtractSummary } = await import("./extract-
|
|
10574
|
+
const { extractAndSave, printExtractSummary } = await import("./extract-L2IW3IUB.js");
|
|
9689
10575
|
const { summary, outputPath } = extractAndSave(filePath);
|
|
9690
10576
|
printExtractSummary(summary);
|
|
9691
10577
|
console.log(`
|
|
@@ -9698,7 +10584,7 @@ async function handleFilter(args, _mode) {
|
|
|
9698
10584
|
console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude-types=type1,type2]");
|
|
9699
10585
|
process.exit(1);
|
|
9700
10586
|
}
|
|
9701
|
-
const { filterRecording, parseExcludeTypes } = await import("./filter-
|
|
10587
|
+
const { filterRecording, parseExcludeTypes } = await import("./filter-HC4RA7JY.js");
|
|
9702
10588
|
const excludeTypes = parseExcludeTypes(args.slice(2));
|
|
9703
10589
|
const result = filterRecording(filePath, outputPath, excludeTypes);
|
|
9704
10590
|
console.log(`Filtered ${filePath} -> ${outputPath}`);
|
|
@@ -9707,7 +10593,7 @@ async function handleFilter(args, _mode) {
|
|
|
9707
10593
|
|
|
9708
10594
|
// src/stdin.ts
|
|
9709
10595
|
import { createInterface } from "readline";
|
|
9710
|
-
import { readFileSync as
|
|
10596
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
9711
10597
|
async function readStdin2() {
|
|
9712
10598
|
if (process.stdin.isTTY) return [];
|
|
9713
10599
|
const lines = [];
|
|
@@ -9721,7 +10607,7 @@ async function readStdin2() {
|
|
|
9721
10607
|
return lines;
|
|
9722
10608
|
}
|
|
9723
10609
|
function readCommandFile(filePath) {
|
|
9724
|
-
const content =
|
|
10610
|
+
const content = readFileSync15(filePath, "utf-8");
|
|
9725
10611
|
return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
9726
10612
|
}
|
|
9727
10613
|
|
|
@@ -10353,14 +11239,14 @@ async function listCommands() {
|
|
|
10353
11239
|
return jsonResponse(200, { commands });
|
|
10354
11240
|
}
|
|
10355
11241
|
async function listSessions2() {
|
|
10356
|
-
const
|
|
11242
|
+
const sessions2 = getAllSessions().map((s) => ({
|
|
10357
11243
|
id: s.id,
|
|
10358
11244
|
name: s.name,
|
|
10359
11245
|
url: s.page?.url() ?? null,
|
|
10360
11246
|
createdAt: s.createdAt,
|
|
10361
11247
|
isCDP: s.isCDP ?? false
|
|
10362
11248
|
}));
|
|
10363
|
-
return jsonResponse(200, { sessions });
|
|
11249
|
+
return jsonResponse(200, { sessions: sessions2 });
|
|
10364
11250
|
}
|
|
10365
11251
|
async function createSessionHandler(req) {
|
|
10366
11252
|
const body = req.body;
|
|
@@ -10758,8 +11644,23 @@ async function handleEvalMode(argv) {
|
|
|
10758
11644
|
}
|
|
10759
11645
|
async function handleChainInput(input, argv) {
|
|
10760
11646
|
const cdpEndpoint = argv ? extractCdpFromArgv(argv) : void 0;
|
|
11647
|
+
const jsonMode = argv ? argv.includes("--json") || argv.includes("-j") : false;
|
|
10761
11648
|
const chainResult = await executeChain(input, { cdpEndpoint });
|
|
10762
|
-
|
|
11649
|
+
if (jsonMode) {
|
|
11650
|
+
const output = {
|
|
11651
|
+
success: chainResult.success,
|
|
11652
|
+
steps: chainResult.steps.map((s) => ({
|
|
11653
|
+
command: s.raw,
|
|
11654
|
+
success: s.success,
|
|
11655
|
+
data: s.data,
|
|
11656
|
+
duration: s.duration,
|
|
11657
|
+
...s.hookOutputs?.length ? { hooks: s.hookOutputs } : {}
|
|
11658
|
+
}))
|
|
11659
|
+
};
|
|
11660
|
+
console.log(JSON.stringify(output, null, 2));
|
|
11661
|
+
} else {
|
|
11662
|
+
printChainResult(chainResult);
|
|
11663
|
+
}
|
|
10763
11664
|
if (!chainResult.success) throw new Error("Command failed");
|
|
10764
11665
|
}
|
|
10765
11666
|
async function routeCommand(argv, stdinCommands) {
|
|
@@ -10784,21 +11685,6 @@ async function routeCommand(argv, stdinCommands) {
|
|
|
10784
11685
|
const { positional, options } = parsed;
|
|
10785
11686
|
const command = positional[0];
|
|
10786
11687
|
const cmdArgs = positional.slice(1);
|
|
10787
|
-
const isPluginCommand = command ? !allBuiltins.find((b) => b.name === command) && !getCommand(command) && command !== "help" : false;
|
|
10788
|
-
if (!isPluginCommand && command) {
|
|
10789
|
-
const unknownOptions = Object.keys(options).filter((k) => !KNOWN_GLOBAL_OPTIONS.has(k));
|
|
10790
|
-
if (unknownOptions.length > 0) {
|
|
10791
|
-
const unknown = unknownOptions.map((k) => `--${k}`).join(", ");
|
|
10792
|
-
outputError(
|
|
10793
|
-
`Unknown option: ${unknown}
|
|
10794
|
-
Did you mean to use a global flag? Global flags must come BEFORE the command:
|
|
10795
|
-
\u2717 xbrowser goto <url> --cdp-endpoint <endpoint> (treated as unknown)
|
|
10796
|
-
\u2713 xbrowser --cdp <endpoint> goto <url> (correct)
|
|
10797
|
-
Run "xbrowser --help" to see available global options.`
|
|
10798
|
-
);
|
|
10799
|
-
return;
|
|
10800
|
-
}
|
|
10801
|
-
}
|
|
10802
11688
|
const mode = options.json ? "json" : options.yaml ? "yaml" : "text";
|
|
10803
11689
|
const sessionName = options.session || process.env.XBROWSER_SESSION || "default";
|
|
10804
11690
|
const cdpEndpoint = options.cdp;
|
|
@@ -11047,11 +11933,11 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11047
11933
|
if (options.target && !params._target) {
|
|
11048
11934
|
params._target = options.target;
|
|
11049
11935
|
}
|
|
11050
|
-
const needsBrowser = cmdEntry.scope === "page";
|
|
11936
|
+
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
11051
11937
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
11052
|
-
const { forwardExec } = await import("./daemon-client-
|
|
11938
|
+
const { forwardExec } = await import("./daemon-client-YAVQ343A.js");
|
|
11053
11939
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
11054
|
-
const result = await forwardExec(command
|
|
11940
|
+
const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
|
|
11055
11941
|
if (result && result.success !== false) {
|
|
11056
11942
|
if (isCommandResult2(result)) {
|
|
11057
11943
|
if (mode === "json" || mode === "yaml") {
|
|
@@ -11084,6 +11970,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11084
11970
|
browser: needsBrowser ? session.context.browser() : null,
|
|
11085
11971
|
browserContext: needsBrowser ? session.context : null,
|
|
11086
11972
|
sessionId: needsBrowser ? session.id : "",
|
|
11973
|
+
cdpEndpoint: cdpEndpoint || (needsBrowser ? session?.cdpEndpoint : void 0),
|
|
11087
11974
|
storage: getPluginStorage(command),
|
|
11088
11975
|
output: { mode, showTips: true, color: true, emoji: true },
|
|
11089
11976
|
error: (msg) => {
|
|
@@ -11097,38 +11984,46 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
11097
11984
|
}
|
|
11098
11985
|
};
|
|
11099
11986
|
try {
|
|
11987
|
+
const cmdStart = Date.now();
|
|
11988
|
+
const cmdHooks = await loadHooks();
|
|
11989
|
+
if (cmdHooks.length > 0 && session?.page) {
|
|
11990
|
+
await Promise.all(cmdHooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${command} ${subCommand}`, params })));
|
|
11991
|
+
}
|
|
11100
11992
|
const result = await cmdEntry.handler(params, ctx);
|
|
11993
|
+
const hookOutputs = [];
|
|
11994
|
+
if (cmdHooks.length > 0 && session?.page) {
|
|
11995
|
+
for (const h of cmdHooks) {
|
|
11996
|
+
const output = await h.onAfterCommand?.({ page: session.page, command: `${command} ${subCommand}`, params, result, duration: Date.now() - cmdStart });
|
|
11997
|
+
if (output) hookOutputs.push({ _hook: h.name, ...output });
|
|
11998
|
+
}
|
|
11999
|
+
}
|
|
11101
12000
|
if (session && result && result.data) {
|
|
11102
12001
|
const convUrl = result.data.conversationUrl;
|
|
11103
12002
|
if (convUrl) {
|
|
11104
12003
|
saveSessionDiskMeta(sessionName, { conversationUrl: convUrl, cdpEndpoint });
|
|
11105
12004
|
}
|
|
11106
12005
|
}
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
if (result.tips?.length) {
|
|
11116
|
-
for (const tip of result.tips) console.log(` \u{1F4A1} ${tip}`);
|
|
11117
|
-
}
|
|
12006
|
+
const outputData = isCommandResult2(result) ? result.data : result && typeof result === "object" ? result.data ?? result : result;
|
|
12007
|
+
const tips = isCommandResult2(result) ? result.tips : result && typeof result === "object" ? result.tips : void 0;
|
|
12008
|
+
if (mode === "json" || mode === "yaml") {
|
|
12009
|
+
const finalOutput = {
|
|
12010
|
+
data: outputData
|
|
12011
|
+
};
|
|
12012
|
+
if (hookOutputs.length > 0) {
|
|
12013
|
+
finalOutput.hooks = hookOutputs;
|
|
11118
12014
|
}
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
|
|
11122
|
-
|
|
11123
|
-
|
|
11124
|
-
|
|
11125
|
-
|
|
11126
|
-
}
|
|
11127
|
-
}
|
|
11128
|
-
|
|
11129
|
-
const
|
|
11130
|
-
|
|
11131
|
-
for (const tip of tips) console.log(` \u{1F4A1} ${tip}`);
|
|
12015
|
+
console.log(outputFormatter2.format(finalOutput, { mode, color: false, emoji: false }));
|
|
12016
|
+
if (tips?.length) {
|
|
12017
|
+
for (const tip of tips) console.error(`\u{1F4A1} ${tip}`);
|
|
12018
|
+
}
|
|
12019
|
+
} else {
|
|
12020
|
+
console.log(outputFormatter2.format(outputData, { mode: "text", color: true, emoji: true }));
|
|
12021
|
+
if (tips?.length) {
|
|
12022
|
+
for (const tip of tips) console.log(` \u{1F4A1} ${tip}`);
|
|
12023
|
+
}
|
|
12024
|
+
if (hookOutputs.length > 0) {
|
|
12025
|
+
for (const ho of hookOutputs) {
|
|
12026
|
+
console.log(` \u{1F4F8} screenshot: ${ho.screenshot?.url || "captured"}`);
|
|
11132
12027
|
}
|
|
11133
12028
|
}
|
|
11134
12029
|
}
|
|
@@ -11302,7 +12197,7 @@ async function main() {
|
|
|
11302
12197
|
const command = process.argv[2];
|
|
11303
12198
|
const isLongRunning = command === "preview" || command === "serve";
|
|
11304
12199
|
if (!isLongRunning) {
|
|
11305
|
-
const { ensureProcessCanExit } = await import("./browser-
|
|
12200
|
+
const { ensureProcessCanExit } = await import("./browser-DSVV4GHS.js");
|
|
11306
12201
|
await ensureProcessCanExit().catch(() => {
|
|
11307
12202
|
});
|
|
11308
12203
|
process.exit(exitCode);
|