@xbrowser/cli 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -3
- package/dist/{chunk-PIWNMC36.js → chunk-A3MORKYR.js} +1 -1
- package/dist/{chunk-OMU63E6J.js → chunk-FBQZ3PVD.js} +5 -8
- package/dist/{chunk-TEXCXIBW.js → chunk-LPW6QS4M.js} +5 -8
- package/dist/{chunk-NKW4A74J.js → chunk-XTUAUFL3.js} +5 -8
- package/dist/cli.js +475 -242
- package/dist/daemon-main.js +6 -6
- package/dist/{human-interaction-Y5IDH6LD.js → human-interaction-57VYIR56.js} +1 -1
- package/dist/{human-interaction-ZUTR5AC2.js → human-interaction-67K43B4O.js} +1 -1
- package/dist/{human-interaction-4YR2N6R6.js → human-interaction-MXST5ZQM.js} +1 -1
- package/dist/index.d.ts +15 -3
- package/dist/index.js +477 -244
- package/dist/{plugin-singleton-FCNDN3FX.js → plugin-singleton-GJO7EZXB.js} +1 -1
- package/dist/{recovery-NXC35EQN.js → recovery-6RI2FE3E.js} +1 -1
- package/dist/{recovery-MDWAVXE4.js → recovery-LGY25LIM.js} +1 -1
- package/dist/{recovery-L5GLDX4O.js → recovery-LMJW537X.js} +1 -1
- package/dist/{session-replayer-XM5L7LFD.js → session-replayer-N665T3G5.js} +22 -17
- package/package.json +11 -7
package/dist/index.js
CHANGED
|
@@ -72,7 +72,7 @@ import {
|
|
|
72
72
|
resolveNpmPackageWithFallback,
|
|
73
73
|
resolveScreenshotsDir,
|
|
74
74
|
setConfigValue
|
|
75
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-XTUAUFL3.js";
|
|
76
76
|
import {
|
|
77
77
|
getDaemonConfig,
|
|
78
78
|
getDaemonProcessStatus,
|
|
@@ -7403,7 +7403,7 @@ function attachWaitForHuman(ctx, getOrCreateWSServer) {
|
|
|
7403
7403
|
if (!ctx.page) {
|
|
7404
7404
|
throw new Error("waitForHuman requires an active page");
|
|
7405
7405
|
}
|
|
7406
|
-
const { HumanInteractionManager: HumanInteractionManager2 } = await import("./human-interaction-
|
|
7406
|
+
const { HumanInteractionManager: HumanInteractionManager2 } = await import("./human-interaction-57VYIR56.js");
|
|
7407
7407
|
const wsServer2 = await getOrCreateWSServer(ctx.browserContext);
|
|
7408
7408
|
const manager = new HumanInteractionManager2(wsServer2, ctx.page);
|
|
7409
7409
|
return manager.waitForHuman(options);
|
|
@@ -7519,7 +7519,7 @@ import { join as join7 } from "path";
|
|
|
7519
7519
|
import { execSync } from "child_process";
|
|
7520
7520
|
var SHARED_PLUGIN_DEPENDENCIES = {
|
|
7521
7521
|
"zod": "^3.24.0",
|
|
7522
|
-
"@dyyz1993/xcli-core": "^0.
|
|
7522
|
+
"@dyyz1993/xcli-core": "^0.19.0"
|
|
7523
7523
|
};
|
|
7524
7524
|
function ensurePluginDependencies(pluginsDir) {
|
|
7525
7525
|
const zodPath = join7(pluginsDir, "node_modules", "zod");
|
|
@@ -8817,7 +8817,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
8817
8817
|
const errorMessage = errMsg(err);
|
|
8818
8818
|
if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
|
|
8819
8819
|
try {
|
|
8820
|
-
const { attemptRecovery } = await import("./recovery-
|
|
8820
|
+
const { attemptRecovery } = await import("./recovery-LMJW537X.js");
|
|
8821
8821
|
const recovery = await attemptRecovery(
|
|
8822
8822
|
session.page,
|
|
8823
8823
|
sessionName,
|
|
@@ -9532,13 +9532,39 @@ async function installFromNpm(packageName, name, targetDir) {
|
|
|
9532
9532
|
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
|
|
9533
9533
|
import { resolve as resolve5, join as join10 } from "path";
|
|
9534
9534
|
import { tmpdir as tmpdir2 } from "os";
|
|
9535
|
-
import {
|
|
9535
|
+
import { execFile as execFile2 } from "child_process";
|
|
9536
9536
|
import { verifyPlugin as verifyPlugin3, safeCleanup as safeCleanup3 } from "@dyyz1993/xcli-core";
|
|
9537
|
+
var GIT_URL_RE = /^(?:https|git|git\+https|git\+ssh|ssh):\/\/[^\s"'`\\;<>()&]+$/i;
|
|
9538
|
+
var GIT_SCP_RE = /^git@[\w.-]+:[\w./~+-]+$/;
|
|
9539
|
+
function isValidGitUrl(url) {
|
|
9540
|
+
if (typeof url !== "string" || url.length === 0) return false;
|
|
9541
|
+
if (url.startsWith("-")) return false;
|
|
9542
|
+
if (/[\r\n\0]/.test(url)) return false;
|
|
9543
|
+
return GIT_URL_RE.test(url) || GIT_SCP_RE.test(url);
|
|
9544
|
+
}
|
|
9545
|
+
function execGit(args) {
|
|
9546
|
+
return new Promise((resolve10, reject) => {
|
|
9547
|
+
execFile2("git", args, (err, _stdout, stderr) => {
|
|
9548
|
+
if (err) {
|
|
9549
|
+
const stderrText = typeof stderr === "string" ? stderr : stderr?.toString("utf-8") ?? "";
|
|
9550
|
+
const detail = stderrText.trim() || err.message;
|
|
9551
|
+
reject(new Error(`git clone failed: ${detail}`));
|
|
9552
|
+
} else {
|
|
9553
|
+
resolve10();
|
|
9554
|
+
}
|
|
9555
|
+
});
|
|
9556
|
+
});
|
|
9557
|
+
}
|
|
9537
9558
|
async function installFromGit(gitUrl, name, targetDir) {
|
|
9559
|
+
if (!isValidGitUrl(gitUrl)) {
|
|
9560
|
+
throw new Error(
|
|
9561
|
+
`Invalid git URL: ${JSON.stringify(gitUrl).slice(0, 120)} \u2014 allowed forms: https://, git://, git+https://, git+ssh://, ssh://, git@host:path`
|
|
9562
|
+
);
|
|
9563
|
+
}
|
|
9538
9564
|
const tmpDir = join10(tmpdir2(), `xbrowser-git-${Date.now()}`);
|
|
9539
9565
|
let warnings = [];
|
|
9540
9566
|
try {
|
|
9541
|
-
|
|
9567
|
+
await execGit(["clone", "--depth", "1", "--", gitUrl, tmpDir]);
|
|
9542
9568
|
const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
|
|
9543
9569
|
warnings = verify.warnings ?? [];
|
|
9544
9570
|
if (!verify.valid) {
|
|
@@ -11378,7 +11404,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
11378
11404
|
}
|
|
11379
11405
|
outputResult({ command: cmdDef.name, description: cmdDef.description, scope: cmdDef.scope, parameters: paramsList }, mode);
|
|
11380
11406
|
} else {
|
|
11381
|
-
|
|
11407
|
+
process.stdout.write(helpGenerator.generate(cmdDef, { color: true, emoji: false }));
|
|
11382
11408
|
}
|
|
11383
11409
|
} else {
|
|
11384
11410
|
outputError(`Unknown command: ${command}`);
|
|
@@ -11779,7 +11805,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
11779
11805
|
if (isEmptyResult) {
|
|
11780
11806
|
const hint = cdpEndpoint ? `\u53EF\u80FD\u672A\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\u3002\u8BF7\u786E\u8BA4 ${cdpEndpoint} \u4E0A\u6709 Chrome \u8FD0\u884C\uFF08--remote-debugging-port\uFF09\u3002` : "\u53EF\u80FD\u672A\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\u3002\u8BF7\u4F7F\u7528 --cdp <endpoint> \u8FDE\u63A5\uFF0C\u6216\u5B89\u88C5 cdp-tunnel \u590D\u7528\u5DF2\u6709 Chrome\u3002";
|
|
11781
11807
|
outputResult(result.data, mode);
|
|
11782
|
-
|
|
11808
|
+
outputError(`
|
|
11783
11809
|
\u26A0\uFE0F ${hint}`);
|
|
11784
11810
|
process.exit(1);
|
|
11785
11811
|
} else {
|
|
@@ -11796,8 +11822,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
|
|
|
11796
11822
|
try {
|
|
11797
11823
|
mkdirSync11(dirname7(outputFile), { recursive: true });
|
|
11798
11824
|
writeFileSync13(outputFile, content, "utf-8");
|
|
11799
|
-
|
|
11800
|
-
\u{1F4C4} Written to ${outputFile}
|
|
11825
|
+
process.stdout.write(`
|
|
11826
|
+
\u{1F4C4} Written to ${outputFile}
|
|
11827
|
+
`);
|
|
11801
11828
|
} catch (err) {
|
|
11802
11829
|
outputError(`Failed to write --output "${outputFile}": ${err instanceof Error ? err.message : String(err)}`);
|
|
11803
11830
|
}
|
|
@@ -11895,7 +11922,7 @@ async function handleSession(args, options, mode, _cdpEndpoint) {
|
|
|
11895
11922
|
break;
|
|
11896
11923
|
}
|
|
11897
11924
|
default:
|
|
11898
|
-
|
|
11925
|
+
process.stdout.write(handleSessionHelp() + "\n");
|
|
11899
11926
|
}
|
|
11900
11927
|
}
|
|
11901
11928
|
|
|
@@ -12069,19 +12096,24 @@ async function handleSearch(args, options, mode) {
|
|
|
12069
12096
|
outputEnvelope({ success: true, data: { results: deduped, total: deduped.length } }, { command: "plugin search" }, mode);
|
|
12070
12097
|
} else {
|
|
12071
12098
|
if (deduped.length === 0) {
|
|
12072
|
-
|
|
12099
|
+
process.stdout.write("No plugins found\n");
|
|
12073
12100
|
return;
|
|
12074
12101
|
}
|
|
12075
12102
|
for (const r of deduped) {
|
|
12076
12103
|
const src = r.source === "marketplace" ? "[marketplace]" : r.source === "local" ? "[local]" : "[npm]";
|
|
12077
12104
|
const slug = r.slug ? ` (${r.slug})` : "";
|
|
12078
|
-
|
|
12079
|
-
|
|
12080
|
-
if (r.
|
|
12081
|
-
|
|
12082
|
-
|
|
12105
|
+
process.stdout.write(` ${src} ${r.name}${slug}
|
|
12106
|
+
`);
|
|
12107
|
+
if (r.description) process.stdout.write(` ${r.description}
|
|
12108
|
+
`);
|
|
12109
|
+
if (r.version) process.stdout.write(` Version: ${r.version}
|
|
12110
|
+
`);
|
|
12111
|
+
if (r.downloads) process.stdout.write(` Downloads: ${r.downloads}
|
|
12112
|
+
`);
|
|
12113
|
+
process.stdout.write("\n");
|
|
12083
12114
|
}
|
|
12084
|
-
|
|
12115
|
+
process.stdout.write(`Total: ${deduped.length} plugins
|
|
12116
|
+
`);
|
|
12085
12117
|
}
|
|
12086
12118
|
}
|
|
12087
12119
|
async function handlePluginInfo(args, options, mode) {
|
|
@@ -12098,14 +12130,22 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
12098
12130
|
outputEnvelope({ success: true, data: { source: "marketplace", ...d } }, { command: "plugin info" }, mode);
|
|
12099
12131
|
return;
|
|
12100
12132
|
}
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12133
|
+
process.stdout.write(`\u540D\u79F0: ${d.name || ""}
|
|
12134
|
+
`);
|
|
12135
|
+
process.stdout.write(`\u7248\u672C: ${d.version || ""}
|
|
12136
|
+
`);
|
|
12137
|
+
process.stdout.write(`\u63CF\u8FF0: ${d.description || ""}
|
|
12138
|
+
`);
|
|
12139
|
+
process.stdout.write(`\u4F5C\u8005: ${d.author || ""}
|
|
12140
|
+
`);
|
|
12141
|
+
process.stdout.write(`\u547D\u4EE4: ${(d.commands || []).join(", ")}
|
|
12142
|
+
`);
|
|
12143
|
+
process.stdout.write(`\u4E0B\u8F7D\u91CF: ${d.downloads || 0}
|
|
12144
|
+
`);
|
|
12145
|
+
process.stdout.write(`\u6807\u7B7E: ${(d.tags || []).join(", ")}
|
|
12146
|
+
`);
|
|
12147
|
+
process.stdout.write(`\u7F51\u7AD9: ${(d.sites || []).join(", ")}
|
|
12148
|
+
`);
|
|
12109
12149
|
return;
|
|
12110
12150
|
}
|
|
12111
12151
|
} catch {
|
|
@@ -12124,19 +12164,25 @@ async function handlePluginInfo(args, options, mode) {
|
|
|
12124
12164
|
outputEnvelope({ success: true, data: { source: "npm", name: pkg.name, version: latest, description: pkg.description } }, { command: "plugin info" }, mode);
|
|
12125
12165
|
return;
|
|
12126
12166
|
}
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12167
|
+
process.stdout.write(`\u540D\u79F0: ${pkg.name || ""}
|
|
12168
|
+
`);
|
|
12169
|
+
process.stdout.write(`\u7248\u672C: ${latest}
|
|
12170
|
+
`);
|
|
12171
|
+
process.stdout.write(`\u63CF\u8FF0: ${pkg.description || ""}
|
|
12172
|
+
`);
|
|
12130
12173
|
const author = pkg.author;
|
|
12131
|
-
|
|
12132
|
-
|
|
12133
|
-
|
|
12174
|
+
process.stdout.write(`\u4F5C\u8005: ${typeof author === "string" ? author : author?.name || ""}
|
|
12175
|
+
`);
|
|
12176
|
+
process.stdout.write(`\u5173\u952E\u8BCD: ${(pkg.keywords || []).join(", ")}
|
|
12177
|
+
`);
|
|
12178
|
+
process.stdout.write(`\u8BB8\u53EF\u8BC1: ${pkg.license || ""}
|
|
12179
|
+
`);
|
|
12134
12180
|
return;
|
|
12135
12181
|
}
|
|
12136
12182
|
}
|
|
12137
|
-
|
|
12183
|
+
outputError(`\u63D2\u4EF6 '${slug}' \u672A\u627E\u5230`);
|
|
12138
12184
|
} catch (err) {
|
|
12139
|
-
|
|
12185
|
+
outputError("\u67E5\u8BE2\u5931\u8D25: " + errMsg(err));
|
|
12140
12186
|
}
|
|
12141
12187
|
}
|
|
12142
12188
|
async function handlePluginSchema(args, mode) {
|
|
@@ -12160,32 +12206,39 @@ async function handlePluginSchema(args, mode) {
|
|
|
12160
12206
|
}
|
|
12161
12207
|
}
|
|
12162
12208
|
function printPluginContract(contract) {
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12209
|
+
process.stdout.write(`${contract.plugin.name} contract v${contract.version}
|
|
12210
|
+
`);
|
|
12211
|
+
if (contract.plugin.description) process.stdout.write(contract.plugin.description + "\n");
|
|
12212
|
+
process.stdout.write("\n");
|
|
12166
12213
|
for (const command of contract.commands) {
|
|
12167
12214
|
printCommandContract(contract.plugin.name, command);
|
|
12168
12215
|
}
|
|
12169
12216
|
}
|
|
12170
12217
|
function printCommandContract(pluginName, command) {
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12218
|
+
process.stdout.write(`${pluginName} ${command.name}
|
|
12219
|
+
`);
|
|
12220
|
+
if (command.description) process.stdout.write(` ${command.description}
|
|
12221
|
+
`);
|
|
12222
|
+
process.stdout.write(` scope: ${command.scope}
|
|
12223
|
+
`);
|
|
12174
12224
|
if (command.capabilities.length > 0) {
|
|
12175
|
-
|
|
12225
|
+
process.stdout.write(` capabilities: ${command.capabilities.join(", ")}
|
|
12226
|
+
`);
|
|
12176
12227
|
}
|
|
12177
12228
|
if (command.positional.length > 0) {
|
|
12178
|
-
|
|
12229
|
+
process.stdout.write(` positional: ${command.positional.join(", ")}
|
|
12230
|
+
`);
|
|
12179
12231
|
}
|
|
12180
12232
|
if (command.form.fields.length > 0) {
|
|
12181
|
-
|
|
12233
|
+
process.stdout.write(" fields:\n");
|
|
12182
12234
|
for (const field of command.form.fields) {
|
|
12183
12235
|
const required = field.required ? "required" : "optional";
|
|
12184
12236
|
const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
|
|
12185
|
-
|
|
12237
|
+
process.stdout.write(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}
|
|
12238
|
+
`);
|
|
12186
12239
|
}
|
|
12187
12240
|
}
|
|
12188
|
-
|
|
12241
|
+
process.stdout.write("\n");
|
|
12189
12242
|
}
|
|
12190
12243
|
async function handlePlugin(args, options, mode) {
|
|
12191
12244
|
const sub = args[0];
|
|
@@ -12274,24 +12327,28 @@ async function handlePlugin(args, options, mode) {
|
|
|
12274
12327
|
outputEnvelope({ success: true, data: { plugins: enrichedPlugins } }, { command: "plugin list" }, mode);
|
|
12275
12328
|
} else {
|
|
12276
12329
|
if (enrichedPlugins.length === 0) {
|
|
12277
|
-
|
|
12330
|
+
process.stdout.write("No plugins installed\n");
|
|
12278
12331
|
return;
|
|
12279
12332
|
}
|
|
12280
12333
|
for (const p of enrichedPlugins) {
|
|
12281
12334
|
const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
|
|
12282
12335
|
if (p.version && p.description) {
|
|
12283
|
-
|
|
12336
|
+
process.stdout.write(`${p.name} (${p.version}) - ${p.description}${loginTag}
|
|
12337
|
+
`);
|
|
12284
12338
|
} else {
|
|
12285
|
-
|
|
12339
|
+
process.stdout.write(`${p.name}${loginTag}
|
|
12340
|
+
`);
|
|
12286
12341
|
}
|
|
12287
12342
|
if (p.commands && p.commands.length > 0) {
|
|
12288
|
-
|
|
12343
|
+
process.stdout.write(` ${p.commands.join(", ")}
|
|
12344
|
+
`);
|
|
12289
12345
|
}
|
|
12290
12346
|
if (p.requiresLoginCommands.length > 0) {
|
|
12291
|
-
|
|
12347
|
+
process.stdout.write(` requires login: ${p.requiresLoginCommands.join(", ")}
|
|
12348
|
+
`);
|
|
12292
12349
|
}
|
|
12293
12350
|
}
|
|
12294
|
-
|
|
12351
|
+
process.stdout.write(`
|
|
12295
12352
|
Total: ${enrichedPlugins.length} plugins`);
|
|
12296
12353
|
}
|
|
12297
12354
|
break;
|
|
@@ -12328,7 +12385,7 @@ See docs/plugin-guide.md for the publishing workflow.`
|
|
|
12328
12385
|
);
|
|
12329
12386
|
break;
|
|
12330
12387
|
default:
|
|
12331
|
-
|
|
12388
|
+
process.stdout.write(handlePluginHelp() + "\n");
|
|
12332
12389
|
}
|
|
12333
12390
|
}
|
|
12334
12391
|
function handleCreate(args, options) {
|
|
@@ -12362,7 +12419,7 @@ function handleDaemon(args, options, mode) {
|
|
|
12362
12419
|
break;
|
|
12363
12420
|
}
|
|
12364
12421
|
default:
|
|
12365
|
-
|
|
12422
|
+
process.stdout.write("Daemon starts automatically. No manual action needed.\n");
|
|
12366
12423
|
}
|
|
12367
12424
|
}
|
|
12368
12425
|
|
|
@@ -12431,8 +12488,8 @@ async function handleRecord(args, options, mode) {
|
|
|
12431
12488
|
}, mode);
|
|
12432
12489
|
const md = SessionRecorder.readMarkdownSummary(sessionName);
|
|
12433
12490
|
if (md) {
|
|
12434
|
-
|
|
12435
|
-
|
|
12491
|
+
process.stdout.write("\n");
|
|
12492
|
+
process.stdout.write(md + "\n");
|
|
12436
12493
|
} else {
|
|
12437
12494
|
const summary = SessionRecorder.readSummary(sessionName);
|
|
12438
12495
|
if (summary) {
|
|
@@ -12491,25 +12548,30 @@ async function handleRecord(args, options, mode) {
|
|
|
12491
12548
|
break;
|
|
12492
12549
|
}
|
|
12493
12550
|
default:
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12502
|
-
|
|
12551
|
+
process.stdout.write("Usage:\n");
|
|
12552
|
+
process.stdout.write(" xbrowser record start [--url <url>] [--session <name>]\n");
|
|
12553
|
+
process.stdout.write(" xbrowser record stop [--session <name>]\n");
|
|
12554
|
+
process.stdout.write(" xbrowser record status [--session <name>]\n");
|
|
12555
|
+
process.stdout.write(" xbrowser record summary [--session <name>] [--json]\n");
|
|
12556
|
+
process.stdout.write(' xbrowser record checkpoint --type <type> --hint "description" [--selector <sel>] [--session <name>]\n');
|
|
12557
|
+
process.stdout.write(" xbrowser record generate-plugin [--session <name>] [--name <plugin>] [--output <dir>]\n");
|
|
12558
|
+
process.stdout.write("\n");
|
|
12559
|
+
process.stdout.write("Checkpoint types: dialog, captcha, login, iframe, slider, custom\n");
|
|
12503
12560
|
}
|
|
12504
12561
|
}
|
|
12505
12562
|
function printRecordingSummary(summary, sessionName) {
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
12563
|
+
process.stdout.write("\n");
|
|
12564
|
+
process.stdout.write("=== Recording Summary ===\n");
|
|
12565
|
+
process.stdout.write(` Start URL: ${summary.startUrl}
|
|
12566
|
+
`);
|
|
12567
|
+
process.stdout.write(` Duration: ${Math.round(summary.durationMs / 1e3)}s
|
|
12568
|
+
`);
|
|
12569
|
+
process.stdout.write(` Actions: ${summary.totalActions}
|
|
12570
|
+
`);
|
|
12571
|
+
process.stdout.write(` Network: ${summary.totalNetworkRequests}
|
|
12572
|
+
`);
|
|
12573
|
+
process.stdout.write(` Steps: ${summary.steps.length}
|
|
12574
|
+
`);
|
|
12513
12575
|
for (const step of summary.steps) {
|
|
12514
12576
|
const a = step.action;
|
|
12515
12577
|
const el = a.element;
|
|
@@ -12531,20 +12593,24 @@ function printRecordingSummary(summary, sessionName) {
|
|
|
12531
12593
|
}
|
|
12532
12594
|
const navInfo = step.contextChanges.find((c) => c.type === "navigate");
|
|
12533
12595
|
if (navInfo) desc += ` \u2192 navigate to ${navInfo.url?.substring(0, 80)}`;
|
|
12534
|
-
|
|
12596
|
+
process.stdout.write(` ${step.step}. ${desc}
|
|
12597
|
+
`);
|
|
12535
12598
|
if (a.clickContext) {
|
|
12536
12599
|
const ctx = a.clickContext;
|
|
12537
12600
|
if (ctx.appeared?.length > 0) {
|
|
12538
12601
|
for (const popup of ctx.appeared) {
|
|
12539
12602
|
const roleStr = popup.role ? ` [${popup.role}]` : "";
|
|
12540
|
-
|
|
12603
|
+
process.stdout.write(` \u21B3 ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"
|
|
12604
|
+
`);
|
|
12541
12605
|
if (popup.items?.length > 0) {
|
|
12542
12606
|
for (const item of popup.items.slice(0, 10)) {
|
|
12543
12607
|
const disStr = item.disabled ? " [disabled]" : "";
|
|
12544
|
-
|
|
12608
|
+
process.stdout.write(` \u2022 ${item.text}${disStr}
|
|
12609
|
+
`);
|
|
12545
12610
|
}
|
|
12546
12611
|
if (popup.items.length > 10) {
|
|
12547
|
-
|
|
12612
|
+
process.stdout.write(` ... and ${popup.items.length - 10} more items
|
|
12613
|
+
`);
|
|
12548
12614
|
}
|
|
12549
12615
|
}
|
|
12550
12616
|
}
|
|
@@ -12557,31 +12623,41 @@ function printRecordingSummary(summary, sessionName) {
|
|
|
12557
12623
|
if (sc.ariaSelected !== void 0) parts.push(`selected=${sc.ariaSelected}`);
|
|
12558
12624
|
if (sc.dataState) parts.push(`state=${sc.dataState}`);
|
|
12559
12625
|
if (parts.length > 0) {
|
|
12560
|
-
|
|
12626
|
+
process.stdout.write(` \u21B3 state: <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${parts.join(", ")}
|
|
12627
|
+
`);
|
|
12561
12628
|
}
|
|
12562
12629
|
}
|
|
12563
12630
|
}
|
|
12564
12631
|
}
|
|
12565
12632
|
}
|
|
12566
|
-
|
|
12567
|
-
|
|
12633
|
+
process.stdout.write("\n");
|
|
12634
|
+
process.stdout.write(` Files: ${SessionRecorder.getRecordingsDir(sessionName)}/
|
|
12635
|
+
`);
|
|
12568
12636
|
if (summary.checkpoints && summary.checkpoints.length > 0) {
|
|
12569
|
-
|
|
12570
|
-
|
|
12637
|
+
process.stdout.write("\n");
|
|
12638
|
+
process.stdout.write(` Checkpoints (${summary.checkpoints.length}):
|
|
12639
|
+
`);
|
|
12571
12640
|
for (const cp of summary.checkpoints) {
|
|
12572
12641
|
const src = cp.source === "auto" ? "[auto]" : "[manual]";
|
|
12573
|
-
|
|
12574
|
-
|
|
12642
|
+
process.stdout.write(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}
|
|
12643
|
+
`);
|
|
12644
|
+
if (cp.selector) process.stdout.write(` selector: ${cp.selector}
|
|
12645
|
+
`);
|
|
12575
12646
|
}
|
|
12576
12647
|
}
|
|
12577
12648
|
}
|
|
12578
12649
|
function printHumanReadableSummary(summary) {
|
|
12579
|
-
|
|
12580
|
-
|
|
12581
|
-
|
|
12582
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12650
|
+
process.stdout.write(`Start URL: ${summary.startUrl}
|
|
12651
|
+
`);
|
|
12652
|
+
process.stdout.write(`Recorded: ${summary.recordedAt}
|
|
12653
|
+
`);
|
|
12654
|
+
process.stdout.write(`Duration: ${Math.round(summary.durationMs / 1e3)}s
|
|
12655
|
+
`);
|
|
12656
|
+
process.stdout.write(`Actions: ${summary.totalActions}
|
|
12657
|
+
`);
|
|
12658
|
+
process.stdout.write(`Network: ${summary.totalNetworkRequests}
|
|
12659
|
+
`);
|
|
12660
|
+
process.stdout.write("\n");
|
|
12585
12661
|
for (const step of summary.steps) {
|
|
12586
12662
|
const a = step.action;
|
|
12587
12663
|
const el = a.element;
|
|
@@ -12597,20 +12673,23 @@ function printHumanReadableSummary(summary) {
|
|
|
12597
12673
|
if (a.value) parts.push(`value="${a.value.substring(0, 50)}"`);
|
|
12598
12674
|
if (a.key) parts.push(`key=${a.key}`);
|
|
12599
12675
|
if (a.x !== void 0 && a.y !== void 0) parts.push(`@(${a.x},${a.y})`);
|
|
12600
|
-
|
|
12676
|
+
process.stdout.write(parts.join(" ") + "\n");
|
|
12601
12677
|
if (a.clickContext) {
|
|
12602
12678
|
const ctx = a.clickContext;
|
|
12603
12679
|
if (ctx.appeared?.length > 0) {
|
|
12604
12680
|
for (const popup of ctx.appeared) {
|
|
12605
12681
|
const roleStr = popup.role ? ` [${popup.role}]` : "";
|
|
12606
|
-
|
|
12682
|
+
process.stdout.write(` \u{1F4CB} ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"
|
|
12683
|
+
`);
|
|
12607
12684
|
if (popup.items?.length > 0) {
|
|
12608
12685
|
for (const item of popup.items.slice(0, 10)) {
|
|
12609
12686
|
const disStr = item.disabled ? " [disabled]" : "";
|
|
12610
|
-
|
|
12687
|
+
process.stdout.write(` \u2022 ${item.text}${disStr}
|
|
12688
|
+
`);
|
|
12611
12689
|
}
|
|
12612
12690
|
if (popup.items.length > 10) {
|
|
12613
|
-
|
|
12691
|
+
process.stdout.write(` ... and ${popup.items.length - 10} more items
|
|
12692
|
+
`);
|
|
12614
12693
|
}
|
|
12615
12694
|
}
|
|
12616
12695
|
}
|
|
@@ -12623,40 +12702,50 @@ function printHumanReadableSummary(summary) {
|
|
|
12623
12702
|
if (sc.ariaSelected !== void 0) stateParts.push(`selected=${sc.ariaSelected}`);
|
|
12624
12703
|
if (sc.dataState) stateParts.push(`state=${sc.dataState}`);
|
|
12625
12704
|
if (stateParts.length > 0) {
|
|
12626
|
-
|
|
12705
|
+
process.stdout.write(` \u{1F504} <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${stateParts.join(", ")}
|
|
12706
|
+
`);
|
|
12627
12707
|
}
|
|
12628
12708
|
}
|
|
12629
12709
|
}
|
|
12630
12710
|
}
|
|
12631
12711
|
for (const net of step.network) {
|
|
12632
|
-
|
|
12712
|
+
process.stdout.write(` \u2192 ${net.method} ${net.path} [${net.status}] ${net.resourceType}
|
|
12713
|
+
`);
|
|
12633
12714
|
if (net.requestBody && typeof net.requestBody === "object") {
|
|
12634
12715
|
const bodyStr = JSON.stringify(net.requestBody);
|
|
12635
12716
|
if (bodyStr.length <= 200) {
|
|
12636
|
-
|
|
12717
|
+
process.stdout.write(` body: ${bodyStr}
|
|
12718
|
+
`);
|
|
12637
12719
|
} else {
|
|
12638
|
-
|
|
12720
|
+
process.stdout.write(` body: ${bodyStr.substring(0, 200)}... (${bodyStr.length} bytes)
|
|
12721
|
+
`);
|
|
12639
12722
|
}
|
|
12640
12723
|
}
|
|
12641
12724
|
}
|
|
12642
12725
|
for (const match of step.matchedInputs) {
|
|
12643
|
-
|
|
12726
|
+
process.stdout.write(` \u{1F517} input "${match.inputValue}" \u2192 network #${match.networkId} param "${match.paramName}"
|
|
12727
|
+
`);
|
|
12644
12728
|
}
|
|
12645
12729
|
for (const ctx of step.contextChanges) {
|
|
12646
12730
|
if (ctx.type === "navigate") {
|
|
12647
|
-
|
|
12731
|
+
process.stdout.write(` \u2197 navigate \u2192 ${ctx.url}
|
|
12732
|
+
`);
|
|
12648
12733
|
} else if (ctx.type === "new_tab") {
|
|
12649
|
-
|
|
12734
|
+
process.stdout.write(` \u2197 new tab: ${ctx.url}
|
|
12735
|
+
`);
|
|
12650
12736
|
}
|
|
12651
12737
|
}
|
|
12652
12738
|
}
|
|
12653
12739
|
if (summary.checkpoints && summary.checkpoints.length > 0) {
|
|
12654
|
-
|
|
12655
|
-
|
|
12740
|
+
process.stdout.write("\n");
|
|
12741
|
+
process.stdout.write(`Checkpoints (${summary.checkpoints.length}):
|
|
12742
|
+
`);
|
|
12656
12743
|
for (const cp of summary.checkpoints) {
|
|
12657
12744
|
const src = cp.source === "auto" ? "[auto]" : "[manual]";
|
|
12658
|
-
|
|
12659
|
-
|
|
12745
|
+
process.stdout.write(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}
|
|
12746
|
+
`);
|
|
12747
|
+
if (cp.selector) process.stdout.write(` selector: ${cp.selector}
|
|
12748
|
+
`);
|
|
12660
12749
|
}
|
|
12661
12750
|
}
|
|
12662
12751
|
}
|
|
@@ -12685,10 +12774,12 @@ async function handleReplay(args, options, mode) {
|
|
|
12685
12774
|
if (result.ok && mode !== "json" && mode !== "yaml") {
|
|
12686
12775
|
const healed = typeof result.healed === "number" ? result.healed : 0;
|
|
12687
12776
|
if (healed > 0) {
|
|
12688
|
-
|
|
12689
|
-
Self-healed ${healed} action(s)
|
|
12777
|
+
process.stdout.write(`
|
|
12778
|
+
Self-healed ${healed} action(s):
|
|
12779
|
+
`);
|
|
12690
12780
|
for (const d of result.healedDetails ?? []) {
|
|
12691
|
-
|
|
12781
|
+
process.stdout.write(` - step ${d.index + 1}: ${d.strategy}
|
|
12782
|
+
`);
|
|
12692
12783
|
}
|
|
12693
12784
|
}
|
|
12694
12785
|
}
|
|
@@ -12698,7 +12789,7 @@ async function handleConvert(args, _mode) {
|
|
|
12698
12789
|
const filePath = args[0];
|
|
12699
12790
|
const outputPath = args[1];
|
|
12700
12791
|
if (!filePath || !outputPath) {
|
|
12701
|
-
|
|
12792
|
+
outputError("Usage: xbrowser convert <recording.yaml> <output.{js,py,sh}>");
|
|
12702
12793
|
process.exit(1);
|
|
12703
12794
|
}
|
|
12704
12795
|
const fs6 = await import("fs");
|
|
@@ -12710,11 +12801,11 @@ async function handleConvert(args, _mode) {
|
|
|
12710
12801
|
const content = fs6.readFileSync(filePath, "utf-8");
|
|
12711
12802
|
recording = yaml3.parse(content);
|
|
12712
12803
|
} catch (e) {
|
|
12713
|
-
|
|
12804
|
+
outputError(`Error: Failed to read "${filePath}": ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
|
|
12714
12805
|
process.exit(1);
|
|
12715
12806
|
}
|
|
12716
12807
|
if (recording === null || typeof recording !== "object" || Array.isArray(recording)) {
|
|
12717
|
-
|
|
12808
|
+
outputError(`Error: "${filePath}" does not contain a valid recording (expected a YAML/JSON object with events or actions).`);
|
|
12718
12809
|
process.exit(1);
|
|
12719
12810
|
}
|
|
12720
12811
|
const rawActions = recording.actions;
|
|
@@ -12748,24 +12839,28 @@ async function handleConvert(args, _mode) {
|
|
|
12748
12839
|
fs6.writeFileSync(outputPath, script);
|
|
12749
12840
|
fs6.chmodSync(outputPath, 493);
|
|
12750
12841
|
const eventCount = (recordingTyped.events || []).length;
|
|
12751
|
-
|
|
12752
|
-
|
|
12753
|
-
|
|
12842
|
+
process.stdout.write(`Converted ${filePath} -> ${outputPath}
|
|
12843
|
+
`);
|
|
12844
|
+
process.stdout.write(` Events: ${eventCount}, Start URL: ${recordingTyped.startUrl}
|
|
12845
|
+
`);
|
|
12846
|
+
process.stdout.write(` Run: ${ext === ".py" ? "python" : ext === ".sh" ? "./" : "node"} ${outputPath}
|
|
12847
|
+
`);
|
|
12754
12848
|
}
|
|
12755
12849
|
async function handleExtract(args, _mode) {
|
|
12756
12850
|
const filePath = args[0];
|
|
12757
12851
|
if (!filePath) {
|
|
12758
|
-
|
|
12852
|
+
outputError("Usage: xbrowser extract <recording.yaml>");
|
|
12759
12853
|
process.exit(1);
|
|
12760
12854
|
}
|
|
12761
12855
|
const { extractAndSave: extractAndSave2, printExtractSummary: printExtractSummary2 } = await import("./extract-DSU2RVMN.js");
|
|
12762
12856
|
try {
|
|
12763
12857
|
const { summary, outputPath } = extractAndSave2(filePath);
|
|
12764
12858
|
printExtractSummary2(summary);
|
|
12765
|
-
|
|
12766
|
-
Saved LLM summary: ${outputPath}
|
|
12859
|
+
process.stdout.write(`
|
|
12860
|
+
Saved LLM summary: ${outputPath}
|
|
12861
|
+
`);
|
|
12767
12862
|
} catch (e) {
|
|
12768
|
-
|
|
12863
|
+
outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
12769
12864
|
process.exit(1);
|
|
12770
12865
|
}
|
|
12771
12866
|
}
|
|
@@ -12773,7 +12868,7 @@ async function handleFilter(args, _mode, options) {
|
|
|
12773
12868
|
const filePath = args[0];
|
|
12774
12869
|
const outputPath = args[1];
|
|
12775
12870
|
if (!filePath || !outputPath) {
|
|
12776
|
-
|
|
12871
|
+
outputError("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude type1,type2]");
|
|
12777
12872
|
process.exit(1);
|
|
12778
12873
|
}
|
|
12779
12874
|
const { filterRecording: filterRecording2, parseExcludeTypes: parseExcludeTypes2 } = await import("./filter-3DXC6432.js");
|
|
@@ -12785,10 +12880,12 @@ async function handleFilter(args, _mode, options) {
|
|
|
12785
12880
|
const excludeTypes = parseExcludeTypes2(excludeArgs);
|
|
12786
12881
|
try {
|
|
12787
12882
|
const result = filterRecording2(filePath, outputPath, excludeTypes);
|
|
12788
|
-
|
|
12789
|
-
|
|
12883
|
+
process.stdout.write(`Filtered ${filePath} -> ${outputPath}
|
|
12884
|
+
`);
|
|
12885
|
+
process.stdout.write(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)
|
|
12886
|
+
`);
|
|
12790
12887
|
} catch (e) {
|
|
12791
|
-
|
|
12888
|
+
outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
12792
12889
|
process.exit(1);
|
|
12793
12890
|
}
|
|
12794
12891
|
}
|
|
@@ -12817,21 +12914,30 @@ async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
|
|
|
12817
12914
|
if (knowledgeMd) {
|
|
12818
12915
|
writeFileSync13(join16(finalOutputDir, "SITE_KNOWLEDGE.md"), knowledgeMd, "utf-8");
|
|
12819
12916
|
}
|
|
12820
|
-
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12917
|
+
process.stdout.write("\n");
|
|
12918
|
+
process.stdout.write("=== Plugin Generated ===\n");
|
|
12919
|
+
process.stdout.write(` Plugin: ${finalPluginName}
|
|
12920
|
+
`);
|
|
12921
|
+
process.stdout.write(` Domain: ${domain}
|
|
12922
|
+
`);
|
|
12923
|
+
process.stdout.write(` Output: ${finalOutputDir}/index.ts
|
|
12924
|
+
`);
|
|
12825
12925
|
if (knowledgeMd) {
|
|
12826
|
-
|
|
12926
|
+
process.stdout.write(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md
|
|
12927
|
+
`);
|
|
12827
12928
|
}
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12929
|
+
process.stdout.write(` Actions: ${data.actions.length}
|
|
12930
|
+
`);
|
|
12931
|
+
process.stdout.write(` APIs: ${data.network.filter((n) => n.contentType.includes("json") || n.url.includes("/api/")).length}
|
|
12932
|
+
`);
|
|
12933
|
+
process.stdout.write("\n");
|
|
12934
|
+
process.stdout.write("Next steps:\n");
|
|
12935
|
+
process.stdout.write(` 1. Review and edit: ${finalOutputDir}/index.ts
|
|
12936
|
+
`);
|
|
12937
|
+
process.stdout.write(` 2. Test: xbrowser ${finalPluginName} <command>
|
|
12938
|
+
`);
|
|
12939
|
+
process.stdout.write(` 3. Reference: ${finalOutputDir}/SITE_KNOWLEDGE.md (for LLM)
|
|
12940
|
+
`);
|
|
12835
12941
|
}
|
|
12836
12942
|
function generatePluginCode(pluginName, domain, data, _knowledgeMd) {
|
|
12837
12943
|
const pagePaths = /* @__PURE__ */ new Set();
|
|
@@ -13010,20 +13116,22 @@ async function handleRun(filePath, options) {
|
|
|
13010
13116
|
});
|
|
13011
13117
|
for (const step of chainResult.steps) {
|
|
13012
13118
|
if (step.success) {
|
|
13013
|
-
|
|
13119
|
+
process.stdout.write(`[OK] ${step.raw}
|
|
13120
|
+
`);
|
|
13014
13121
|
if (step.data && typeof step.data === "object") {
|
|
13015
13122
|
const d = step.data;
|
|
13016
13123
|
for (const [k, v] of Object.entries(d)) {
|
|
13017
13124
|
if (k !== "ok")
|
|
13018
|
-
|
|
13125
|
+
process.stdout.write(` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}
|
|
13126
|
+
`);
|
|
13019
13127
|
}
|
|
13020
13128
|
}
|
|
13021
13129
|
} else {
|
|
13022
|
-
|
|
13130
|
+
outputError(`[FAIL] ${step.raw}: ${step.message}`);
|
|
13023
13131
|
}
|
|
13024
13132
|
}
|
|
13025
13133
|
if (chainResult.stoppedReason) {
|
|
13026
|
-
|
|
13134
|
+
outputError(`Stopped: ${chainResult.stoppedReason}`);
|
|
13027
13135
|
}
|
|
13028
13136
|
if (!chainResult.success) process.exit(1);
|
|
13029
13137
|
}
|
|
@@ -13066,26 +13174,29 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13066
13174
|
if (mode === "json") {
|
|
13067
13175
|
outputResult(result, mode);
|
|
13068
13176
|
} else {
|
|
13069
|
-
|
|
13177
|
+
process.stdout.write(`
|
|
13070
13178
|
Network captures (session: ${netSession})`);
|
|
13071
|
-
|
|
13179
|
+
process.stdout.write(` Total: ${result.total}, Showing: ${result.captures.length}
|
|
13072
13180
|
`);
|
|
13073
13181
|
for (const c of result.captures) {
|
|
13074
13182
|
const statusColor = c.status < 300 ? "\x1B[32m" : c.status < 400 ? "\x1B[33m" : "\x1B[31m";
|
|
13075
13183
|
const reset = "\x1B[0m";
|
|
13076
|
-
|
|
13184
|
+
process.stdout.write(` #${c.id} ${c.method.padEnd(6)} ${statusColor}${c.status}${reset} ${c.resourceType.padEnd(10)} ${c.path}
|
|
13185
|
+
`);
|
|
13077
13186
|
if (c.size > 0) {
|
|
13078
13187
|
const sizeStr = c.size > 1024 ? `${(c.size / 1024).toFixed(1)}KB` : `${c.size}B`;
|
|
13079
|
-
|
|
13188
|
+
process.stdout.write(` ${c.contentType.split(";")[0]} ${sizeStr}
|
|
13189
|
+
`);
|
|
13080
13190
|
}
|
|
13081
13191
|
}
|
|
13082
|
-
|
|
13192
|
+
process.stdout.write("\n");
|
|
13083
13193
|
}
|
|
13084
13194
|
break;
|
|
13085
13195
|
}
|
|
13086
13196
|
case "clear": {
|
|
13087
13197
|
await forwardNetworkClear(netSession);
|
|
13088
|
-
|
|
13198
|
+
process.stdout.write(`Network captures cleared for session: ${netSession}
|
|
13199
|
+
`);
|
|
13089
13200
|
break;
|
|
13090
13201
|
}
|
|
13091
13202
|
case "top": {
|
|
@@ -13095,21 +13206,23 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13095
13206
|
if (mode === "json") {
|
|
13096
13207
|
outputResult(result, mode);
|
|
13097
13208
|
} else {
|
|
13098
|
-
|
|
13209
|
+
process.stdout.write(`
|
|
13099
13210
|
Top valued requests (session: ${netSession})`);
|
|
13100
|
-
|
|
13211
|
+
process.stdout.write(` Showing: ${result.entries.length}
|
|
13101
13212
|
`);
|
|
13102
13213
|
for (const e of result.entries) {
|
|
13103
13214
|
const scoreColor = e.score >= 50 ? "\x1B[32m" : e.score >= 20 ? "\x1B[33m" : "\x1B[90m";
|
|
13104
13215
|
const reset = "\x1B[0m";
|
|
13105
13216
|
const methodStr = e.method.padEnd(6);
|
|
13106
13217
|
const scoreStr = `${scoreColor}${e.score.toString().padStart(3)}${reset}`;
|
|
13107
|
-
|
|
13218
|
+
process.stdout.write(` ${scoreStr} ${methodStr} ${e.status} ${e.resourceType.padEnd(10)} ${e.path}
|
|
13219
|
+
`);
|
|
13108
13220
|
if (e.scoreBreakdown.content > 0) {
|
|
13109
|
-
|
|
13221
|
+
process.stdout.write(` ${e.contentType.split(";")[0]} ${e.size > 1024 ? (e.size / 1024).toFixed(1) + "KB" : e.size + "B"}
|
|
13222
|
+
`);
|
|
13110
13223
|
}
|
|
13111
13224
|
}
|
|
13112
|
-
|
|
13225
|
+
process.stdout.write("\n");
|
|
13113
13226
|
}
|
|
13114
13227
|
break;
|
|
13115
13228
|
}
|
|
@@ -13118,16 +13231,17 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13118
13231
|
if (mode === "json") {
|
|
13119
13232
|
outputResult(logResult, mode);
|
|
13120
13233
|
} else {
|
|
13121
|
-
|
|
13234
|
+
process.stdout.write(`
|
|
13122
13235
|
Command log (session: ${netSession})`);
|
|
13123
|
-
|
|
13236
|
+
process.stdout.write(` Total: ${logResult.commands.length}
|
|
13124
13237
|
`);
|
|
13125
13238
|
for (const cmd of logResult.commands) {
|
|
13126
13239
|
const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
|
|
13127
13240
|
const paramsStr = Object.entries(cmd.params).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
13128
|
-
|
|
13241
|
+
process.stdout.write(` #${cmd.id} [${ts}] ${cmd.command} ${paramsStr}
|
|
13242
|
+
`);
|
|
13129
13243
|
}
|
|
13130
|
-
|
|
13244
|
+
process.stdout.write("\n");
|
|
13131
13245
|
}
|
|
13132
13246
|
break;
|
|
13133
13247
|
}
|
|
@@ -13143,28 +13257,31 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13143
13257
|
outputResult(aroundResult, mode);
|
|
13144
13258
|
} else {
|
|
13145
13259
|
if (!aroundResult) {
|
|
13146
|
-
|
|
13260
|
+
process.stdout.write(" No command found with that ID\n");
|
|
13147
13261
|
break;
|
|
13148
13262
|
}
|
|
13149
13263
|
const cmd = aroundResult.command;
|
|
13150
13264
|
const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
|
|
13151
|
-
|
|
13265
|
+
process.stdout.write(`
|
|
13152
13266
|
Command: #${cmd.id} [${ts}] ${cmd.command}`);
|
|
13153
|
-
|
|
13267
|
+
process.stdout.write(` Window: \xB1${windowMs}ms
|
|
13154
13268
|
`);
|
|
13155
13269
|
const before = aroundResult.before;
|
|
13156
13270
|
const after = aroundResult.after;
|
|
13157
|
-
|
|
13271
|
+
process.stdout.write(` BEFORE (${before.length} requests):
|
|
13272
|
+
`);
|
|
13158
13273
|
for (const r of before.slice(0, 5)) {
|
|
13159
|
-
|
|
13274
|
+
process.stdout.write(` ${r.method} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}
|
|
13275
|
+
`);
|
|
13160
13276
|
}
|
|
13161
|
-
|
|
13277
|
+
process.stdout.write(`
|
|
13162
13278
|
AFTER (${aroundResult.afterCount} requests):`);
|
|
13163
13279
|
for (const r of after.slice(0, 10)) {
|
|
13164
13280
|
const highlight = r.method !== "GET" ? " \u2190" : "";
|
|
13165
|
-
|
|
13281
|
+
process.stdout.write(` ${String(r.method).padEnd(6)} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}${highlight}
|
|
13282
|
+
`);
|
|
13166
13283
|
}
|
|
13167
|
-
|
|
13284
|
+
process.stdout.write("\n");
|
|
13168
13285
|
}
|
|
13169
13286
|
break;
|
|
13170
13287
|
}
|
|
@@ -13173,9 +13290,9 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13173
13290
|
if (mode === "json") {
|
|
13174
13291
|
outputResult(result, mode);
|
|
13175
13292
|
} else {
|
|
13176
|
-
|
|
13293
|
+
process.stdout.write(`
|
|
13177
13294
|
API Reusability Analysis (session: ${netSession})`);
|
|
13178
|
-
|
|
13295
|
+
process.stdout.write(` Total: ${result.total}, Analyzed: ${result.analyzed.length}
|
|
13179
13296
|
`);
|
|
13180
13297
|
const groups = { high: [], medium: [], low: [], unknown: [] };
|
|
13181
13298
|
for (const e of result.analyzed) {
|
|
@@ -13186,16 +13303,20 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13186
13303
|
if (!items?.length) continue;
|
|
13187
13304
|
const color = level === "high" ? "\x1B[32m" : level === "medium" ? "\x1B[33m" : level === "low" ? "\x1B[31m" : "\x1B[90m";
|
|
13188
13305
|
const reset = "\x1B[0m";
|
|
13189
|
-
|
|
13306
|
+
process.stdout.write(` ${color}${level.toUpperCase()}${reset} (${items.length})
|
|
13307
|
+
`);
|
|
13190
13308
|
for (const e of items.slice(0, 5)) {
|
|
13191
13309
|
const scoreStr = `[${e.reusability.score.toString().padStart(3)}]`;
|
|
13192
|
-
|
|
13310
|
+
process.stdout.write(` ${e.method.padEnd(6)} ${e.status} ${scoreStr} ${e.path}
|
|
13311
|
+
`);
|
|
13193
13312
|
if (e.reusability.reasons.length > 0) {
|
|
13194
|
-
|
|
13313
|
+
process.stdout.write(` ${e.reusability.reasons.join(", ")}
|
|
13314
|
+
`);
|
|
13195
13315
|
}
|
|
13196
13316
|
}
|
|
13197
|
-
if (items.length > 5)
|
|
13198
|
-
|
|
13317
|
+
if (items.length > 5) process.stdout.write(` ... and ${items.length - 5} more
|
|
13318
|
+
`);
|
|
13319
|
+
process.stdout.write("\n");
|
|
13199
13320
|
}
|
|
13200
13321
|
}
|
|
13201
13322
|
break;
|
|
@@ -13214,12 +13335,12 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13214
13335
|
if (mode === "json") {
|
|
13215
13336
|
outputResult(result, mode);
|
|
13216
13337
|
} else {
|
|
13217
|
-
|
|
13338
|
+
process.stdout.write(`
|
|
13218
13339
|
${result.method} ${result.url}`);
|
|
13219
|
-
|
|
13340
|
+
process.stdout.write(` Headers: ${result.headerCount}, Body: ${result.hasBody}
|
|
13220
13341
|
`);
|
|
13221
|
-
|
|
13222
|
-
|
|
13342
|
+
process.stdout.write(result.command + "\n");
|
|
13343
|
+
process.stdout.write("\n");
|
|
13223
13344
|
}
|
|
13224
13345
|
break;
|
|
13225
13346
|
}
|
|
@@ -13237,27 +13358,33 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13237
13358
|
if (mode === "json") {
|
|
13238
13359
|
outputResult(result, mode);
|
|
13239
13360
|
} else {
|
|
13240
|
-
|
|
13361
|
+
process.stdout.write(`
|
|
13241
13362
|
Replay Result`);
|
|
13242
|
-
|
|
13363
|
+
process.stdout.write(` ${result.curlCommand?.split("\n")[0]?.trim()}
|
|
13243
13364
|
`);
|
|
13244
13365
|
const replay = result.replay;
|
|
13245
13366
|
if (replay?.error) {
|
|
13246
|
-
|
|
13367
|
+
process.stdout.write(` \x1B[31mFAILED\x1B[0m: ${replay.error}
|
|
13368
|
+
`);
|
|
13247
13369
|
} else if (replay) {
|
|
13248
13370
|
const statusColor = replay.status && replay.status < 300 ? "\x1B[32m" : "\x1B[31m";
|
|
13249
13371
|
const status = replay.status;
|
|
13250
13372
|
const size = replay.size;
|
|
13251
13373
|
const duration = replay.duration;
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13374
|
+
process.stdout.write(` Status: ${statusColor}${status}\x1B[0m ${replay.statusText}
|
|
13375
|
+
`);
|
|
13376
|
+
process.stdout.write(` Size: ${size > 1024 ? (size / 1024).toFixed(1) + "KB" : size + "B"}
|
|
13377
|
+
`);
|
|
13378
|
+
process.stdout.write(` Duration: ${duration}ms
|
|
13379
|
+
`);
|
|
13380
|
+
process.stdout.write(` Body Match: ${replay.bodyMatch ? "\x1B[32mYes\x1B[0m" : "\x1B[33mNo\x1B[0m"}
|
|
13381
|
+
`);
|
|
13256
13382
|
if (status && status >= 400) {
|
|
13257
|
-
|
|
13383
|
+
process.stdout.write(` \x1B[33m\u26A0 API may require fresh signature/token\x1B[0m
|
|
13384
|
+
`);
|
|
13258
13385
|
}
|
|
13259
13386
|
}
|
|
13260
|
-
|
|
13387
|
+
process.stdout.write("\n");
|
|
13261
13388
|
}
|
|
13262
13389
|
break;
|
|
13263
13390
|
}
|
|
@@ -13276,40 +13403,47 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13276
13403
|
outputResult(result, mode);
|
|
13277
13404
|
} else {
|
|
13278
13405
|
const c = result.capture;
|
|
13279
|
-
|
|
13406
|
+
process.stdout.write(`
|
|
13280
13407
|
Request #${c.id}`);
|
|
13281
|
-
|
|
13282
|
-
|
|
13283
|
-
|
|
13408
|
+
process.stdout.write(` ${c.method} ${c.url}
|
|
13409
|
+
`);
|
|
13410
|
+
process.stdout.write(` Status: ${c.status} | Size: ${c.size}B | Type: ${c.contentType}
|
|
13411
|
+
`);
|
|
13412
|
+
process.stdout.write(` Resource: ${c.resourceType}
|
|
13413
|
+
`);
|
|
13284
13414
|
if (c.requestHeaders) {
|
|
13285
|
-
|
|
13415
|
+
process.stdout.write(`
|
|
13286
13416
|
Request Headers:`);
|
|
13287
13417
|
for (const [k, v] of Object.entries(c.requestHeaders)) {
|
|
13288
|
-
|
|
13418
|
+
process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
|
|
13419
|
+
`);
|
|
13289
13420
|
}
|
|
13290
13421
|
}
|
|
13291
13422
|
if (c.requestBody !== void 0) {
|
|
13292
|
-
|
|
13423
|
+
process.stdout.write(`
|
|
13293
13424
|
Request Body:`);
|
|
13294
13425
|
const bodyStr = typeof c.requestBody === "string" ? c.requestBody : JSON.stringify(c.requestBody, null, 2);
|
|
13295
13426
|
const lines = bodyStr.split("\n").slice(0, 20);
|
|
13296
|
-
for (const line of lines)
|
|
13297
|
-
|
|
13427
|
+
for (const line of lines) process.stdout.write(` ${line}
|
|
13428
|
+
`);
|
|
13429
|
+
if (bodyStr.split("\n").length > 20) process.stdout.write(" ...");
|
|
13298
13430
|
}
|
|
13299
|
-
|
|
13431
|
+
process.stdout.write(`
|
|
13300
13432
|
Response Headers:`);
|
|
13301
13433
|
for (const [k, v] of Object.entries(c.headers)) {
|
|
13302
|
-
|
|
13434
|
+
process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
|
|
13435
|
+
`);
|
|
13303
13436
|
}
|
|
13304
13437
|
if (c.body !== void 0) {
|
|
13305
|
-
|
|
13438
|
+
process.stdout.write(`
|
|
13306
13439
|
Response Body:`);
|
|
13307
13440
|
const bodyStr = typeof c.body === "string" ? c.body : JSON.stringify(c.body, null, 2);
|
|
13308
13441
|
const lines = bodyStr.split("\n").slice(0, 20);
|
|
13309
|
-
for (const line of lines)
|
|
13310
|
-
|
|
13442
|
+
for (const line of lines) process.stdout.write(` ${line}
|
|
13443
|
+
`);
|
|
13444
|
+
if (bodyStr.split("\n").length > 20) process.stdout.write(" ...");
|
|
13311
13445
|
}
|
|
13312
|
-
|
|
13446
|
+
process.stdout.write("\n");
|
|
13313
13447
|
}
|
|
13314
13448
|
break;
|
|
13315
13449
|
}
|
|
@@ -13320,7 +13454,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13320
13454
|
break;
|
|
13321
13455
|
}
|
|
13322
13456
|
await forwardNetworkLike(netSession, id);
|
|
13323
|
-
|
|
13457
|
+
process.stdout.write(`Marked #${id} as useful
|
|
13458
|
+
`);
|
|
13324
13459
|
break;
|
|
13325
13460
|
}
|
|
13326
13461
|
case "dislike": {
|
|
@@ -13330,7 +13465,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13330
13465
|
break;
|
|
13331
13466
|
}
|
|
13332
13467
|
await forwardNetworkDislike(netSession, id);
|
|
13333
|
-
|
|
13468
|
+
process.stdout.write(`Marked #${id} as not useful
|
|
13469
|
+
`);
|
|
13334
13470
|
break;
|
|
13335
13471
|
}
|
|
13336
13472
|
case "export": {
|
|
@@ -13345,7 +13481,7 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13345
13481
|
outputError(result.error);
|
|
13346
13482
|
break;
|
|
13347
13483
|
}
|
|
13348
|
-
|
|
13484
|
+
process.stdout.write(result.code + "\n");
|
|
13349
13485
|
break;
|
|
13350
13486
|
}
|
|
13351
13487
|
default:
|
|
@@ -13357,9 +13493,25 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
13357
13493
|
}
|
|
13358
13494
|
|
|
13359
13495
|
// src/cli/test-routes.ts
|
|
13360
|
-
import {
|
|
13496
|
+
import { execFile as execFile3 } from "child_process";
|
|
13361
13497
|
import { readFileSync as readFileSync11 } from "fs";
|
|
13362
13498
|
import { resolve as resolve9 } from "path";
|
|
13499
|
+
function execCli(args) {
|
|
13500
|
+
return new Promise((resolvePromise) => {
|
|
13501
|
+
execFile3(
|
|
13502
|
+
"npx",
|
|
13503
|
+
args,
|
|
13504
|
+
{ timeout: 65e3, env: { ...process.env, FORCE_COLOR: "0" } },
|
|
13505
|
+
(err, stdout, stderr) => {
|
|
13506
|
+
resolvePromise({
|
|
13507
|
+
stdout: typeof stdout === "string" ? stdout : stdout?.toString("utf-8") ?? "",
|
|
13508
|
+
stderr: typeof stderr === "string" ? stderr : stderr?.toString("utf-8") ?? "",
|
|
13509
|
+
error: err ?? null
|
|
13510
|
+
});
|
|
13511
|
+
}
|
|
13512
|
+
);
|
|
13513
|
+
});
|
|
13514
|
+
}
|
|
13363
13515
|
function findPluginPath(plugin) {
|
|
13364
13516
|
const candidates = [
|
|
13365
13517
|
resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts"),
|
|
@@ -13424,20 +13576,21 @@ function extractSchema(plugin, command) {
|
|
|
13424
13576
|
}
|
|
13425
13577
|
async function runTest(plugin, command, cmdArgs, options) {
|
|
13426
13578
|
const cdp = options.cdp || options.cdpEndpoint || "http://localhost:9221";
|
|
13427
|
-
const
|
|
13579
|
+
const passthroughArgs = cmdArgs.filter((a) => !a.startsWith("--cdp"));
|
|
13428
13580
|
const schema = extractSchema(plugin, command);
|
|
13429
|
-
const
|
|
13430
|
-
|
|
13431
|
-
|
|
13432
|
-
|
|
13433
|
-
|
|
13434
|
-
|
|
13435
|
-
|
|
13436
|
-
|
|
13437
|
-
|
|
13438
|
-
|
|
13439
|
-
|
|
13440
|
-
|
|
13581
|
+
const cliArgs = [
|
|
13582
|
+
"xbrowser",
|
|
13583
|
+
plugin,
|
|
13584
|
+
command,
|
|
13585
|
+
...passthroughArgs,
|
|
13586
|
+
"--cdp",
|
|
13587
|
+
String(cdp),
|
|
13588
|
+
"--json",
|
|
13589
|
+
"--timeout",
|
|
13590
|
+
"60000"
|
|
13591
|
+
];
|
|
13592
|
+
const { stdout, stderr, error } = await execCli(cliArgs);
|
|
13593
|
+
if (error) {
|
|
13441
13594
|
const jsonLine = stdout.split("\n").find((l) => {
|
|
13442
13595
|
try {
|
|
13443
13596
|
JSON.parse(l);
|
|
@@ -13456,11 +13609,10 @@ async function runTest(plugin, command, cmdArgs, options) {
|
|
|
13456
13609
|
} catch {
|
|
13457
13610
|
}
|
|
13458
13611
|
}
|
|
13459
|
-
const stderr = err.stderr?.toString() || "";
|
|
13460
13612
|
if (stdout.includes("captcha") || stderr.includes("captcha") || stdout.includes("CAPTCHA")) {
|
|
13461
13613
|
return { status: "CAPTCHA", message: "\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
|
|
13462
13614
|
}
|
|
13463
|
-
return { status: "EXEC_ERROR", message: (
|
|
13615
|
+
return { status: "EXEC_ERROR", message: (error.message || "").slice(0, 200) || "\u6267\u884C\u5931\u8D25" };
|
|
13464
13616
|
}
|
|
13465
13617
|
const allLines = stdout.split("\n");
|
|
13466
13618
|
const jsonStart = allLines.findIndex((l) => l.trim().startsWith("{"));
|
|
@@ -13530,27 +13682,27 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
|
|
|
13530
13682
|
const plugin = cmdArgs[0];
|
|
13531
13683
|
const command = cmdArgs[1];
|
|
13532
13684
|
if (!plugin || !command) {
|
|
13533
|
-
|
|
13534
|
-
|
|
13685
|
+
outputError("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
|
|
13686
|
+
outputError("\u793A\u4F8B: xbrowser test doubao list --cdp 9221");
|
|
13535
13687
|
return;
|
|
13536
13688
|
}
|
|
13537
13689
|
const loader = await getPluginLoader();
|
|
13538
13690
|
const internalLoader = loader.getCore().loader;
|
|
13539
13691
|
const site = internalLoader.getSite(plugin);
|
|
13540
13692
|
if (!site) {
|
|
13541
|
-
|
|
13693
|
+
outputError(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
|
|
13542
13694
|
return;
|
|
13543
13695
|
}
|
|
13544
13696
|
const cmdEntry = site.getCommand(command);
|
|
13545
13697
|
if (!cmdEntry) {
|
|
13546
|
-
|
|
13698
|
+
outputError(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
|
|
13547
13699
|
return;
|
|
13548
13700
|
}
|
|
13549
13701
|
const testArgs = cmdArgs.slice(2);
|
|
13550
13702
|
const mergedOptions = { ...options, cdp: cdpEndpoint || options.cdp };
|
|
13551
13703
|
const result = await runTest(plugin, command, testArgs, mergedOptions);
|
|
13552
13704
|
if (mode === "json") {
|
|
13553
|
-
|
|
13705
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
13554
13706
|
return;
|
|
13555
13707
|
}
|
|
13556
13708
|
const r = result;
|
|
@@ -13565,23 +13717,32 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
|
|
|
13565
13717
|
};
|
|
13566
13718
|
const status = String(r.status);
|
|
13567
13719
|
const icon = icons[status] || "\u2753";
|
|
13568
|
-
|
|
13569
|
-
${icon} ${plugin}.${command}
|
|
13570
|
-
|
|
13720
|
+
process.stdout.write(`
|
|
13721
|
+
${icon} ${plugin}.${command}
|
|
13722
|
+
`);
|
|
13723
|
+
process.stdout.write(` \u72B6\u6001: ${status}
|
|
13724
|
+
`);
|
|
13571
13725
|
if (status === "OK") {
|
|
13572
|
-
if (r.count)
|
|
13573
|
-
|
|
13726
|
+
if (r.count) process.stdout.write(` \u6570\u636E: ${r.count} \u9879
|
|
13727
|
+
`);
|
|
13728
|
+
if (r.data) process.stdout.write(` \u9884\u89C8: ${String(r.data).slice(0, 150)}
|
|
13729
|
+
`);
|
|
13574
13730
|
} else if (status === "LOGIN_REQUIRED" || status === "CAPTCHA") {
|
|
13575
|
-
|
|
13576
|
-
|
|
13731
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13732
|
+
`);
|
|
13733
|
+
process.stdout.write(` Viewer: ${String(r.viewerUrl)}`);
|
|
13577
13734
|
} else if (status === "SCHEMA_ERROR") {
|
|
13578
13735
|
const errs = r.errors;
|
|
13579
|
-
if (errs)
|
|
13736
|
+
if (errs) process.stdout.write(` \u9519\u8BEF: ${errs.join("; ")}
|
|
13737
|
+
`);
|
|
13580
13738
|
} else if (["NO_DATA", "BLOCKED"].includes(status)) {
|
|
13581
|
-
|
|
13582
|
-
|
|
13739
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13740
|
+
`);
|
|
13741
|
+
if (r.viewerUrl) process.stdout.write(` Viewer: ${String(r.viewerUrl)}
|
|
13742
|
+
`);
|
|
13583
13743
|
} else {
|
|
13584
|
-
|
|
13744
|
+
process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
|
|
13745
|
+
`);
|
|
13585
13746
|
}
|
|
13586
13747
|
}
|
|
13587
13748
|
|
|
@@ -13679,7 +13840,7 @@ Commands:
|
|
|
13679
13840
|
plugin list List plugins
|
|
13680
13841
|
plugin reload <name> Reload plugin
|
|
13681
13842
|
create <name> --template <type> Create plugin
|
|
13682
|
-
serve [--port <
|
|
13843
|
+
serve [--port <p>] [--host <h>] [--token <t>] [--cors-origins <csv>] Start HTTP server (loopback by default)
|
|
13683
13844
|
remote <url> [command] [--token <t>] Execute on remote server
|
|
13684
13845
|
run <file> Execute commands from file
|
|
13685
13846
|
viewer [--name <n>] Generate viewer URL
|
|
@@ -13800,10 +13961,26 @@ function validateAuth(authHeader, validTokens) {
|
|
|
13800
13961
|
function isAuthRequired(validTokens) {
|
|
13801
13962
|
return validTokens.length > 0;
|
|
13802
13963
|
}
|
|
13964
|
+
var IPV6_LOOPBACK = /* @__PURE__ */ new Set(["::1", "0:0:0:0:0:0:0:1"]);
|
|
13965
|
+
function isLoopbackHost(host) {
|
|
13966
|
+
const h = host.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
|
|
13967
|
+
if (h === "localhost") return true;
|
|
13968
|
+
if (IPV6_LOOPBACK.has(h)) return true;
|
|
13969
|
+
const ipv4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
13970
|
+
return ipv4 !== null && ipv4[1] === "127";
|
|
13971
|
+
}
|
|
13972
|
+
function isLoopbackOrigin(origin) {
|
|
13973
|
+
try {
|
|
13974
|
+
const url = new URL(origin);
|
|
13975
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
|
|
13976
|
+
return isLoopbackHost(url.hostname);
|
|
13977
|
+
} catch {
|
|
13978
|
+
return false;
|
|
13979
|
+
}
|
|
13980
|
+
}
|
|
13803
13981
|
|
|
13804
13982
|
// src/server/router.ts
|
|
13805
|
-
var
|
|
13806
|
-
"Access-Control-Allow-Origin": "*",
|
|
13983
|
+
var CORS_BASE_HEADERS = {
|
|
13807
13984
|
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
|
13808
13985
|
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
13809
13986
|
};
|
|
@@ -13856,9 +14033,31 @@ function jsonResponse(statusCode, body, extraHeaders) {
|
|
|
13856
14033
|
return {
|
|
13857
14034
|
statusCode,
|
|
13858
14035
|
body,
|
|
13859
|
-
headers: {
|
|
14036
|
+
headers: { "Content-Type": "application/json", ...extraHeaders }
|
|
13860
14037
|
};
|
|
13861
14038
|
}
|
|
14039
|
+
function corsHeadersFor(origin, explicitOrigins) {
|
|
14040
|
+
if (!origin || !isOriginAllowed(origin, explicitOrigins)) return {};
|
|
14041
|
+
return {
|
|
14042
|
+
"Access-Control-Allow-Origin": origin,
|
|
14043
|
+
Vary: "Origin",
|
|
14044
|
+
...CORS_BASE_HEADERS
|
|
14045
|
+
};
|
|
14046
|
+
}
|
|
14047
|
+
function isOriginAllowed(origin, explicitOrigins) {
|
|
14048
|
+
if (explicitOrigins && explicitOrigins.length > 0) {
|
|
14049
|
+
return explicitOrigins.includes("*") || explicitOrigins.includes(origin);
|
|
14050
|
+
}
|
|
14051
|
+
return isLoopbackOrigin(origin);
|
|
14052
|
+
}
|
|
14053
|
+
function isJsonContentType(contentType) {
|
|
14054
|
+
return contentType.toLowerCase().split(";")[0].trim() === "application/json";
|
|
14055
|
+
}
|
|
14056
|
+
function applyCorsHeaders(response, origin, corsOrigins) {
|
|
14057
|
+
const cors = corsHeadersFor(origin, corsOrigins);
|
|
14058
|
+
if (Object.keys(cors).length === 0) return response;
|
|
14059
|
+
return { ...response, headers: { ...cors, ...response.headers ?? {} } };
|
|
14060
|
+
}
|
|
13862
14061
|
function errorResponse(statusCode, error, message) {
|
|
13863
14062
|
return jsonResponse(statusCode, { error, message, statusCode });
|
|
13864
14063
|
}
|
|
@@ -13976,13 +14175,30 @@ async function ensureSession(sessionName, url, cdpEndpoint) {
|
|
|
13976
14175
|
function isHealthCheckPath(pathname) {
|
|
13977
14176
|
return pathname === "/api/v1/health";
|
|
13978
14177
|
}
|
|
13979
|
-
async function route(method, url, headers, body) {
|
|
14178
|
+
async function route(method, url, headers, body, corsOrigins) {
|
|
14179
|
+
const response = await routeInner(method, url, headers, body, corsOrigins);
|
|
14180
|
+
return applyCorsHeaders(response, headers.origin, corsOrigins);
|
|
14181
|
+
}
|
|
14182
|
+
async function routeInner(method, url, headers, body, corsOrigins) {
|
|
13980
14183
|
const parsedUrl = new URL(url, "http://localhost");
|
|
13981
14184
|
const pathname = parsedUrl.pathname;
|
|
13982
14185
|
const query = parseQueryString(parsedUrl.search);
|
|
13983
14186
|
if (method === "OPTIONS") {
|
|
13984
14187
|
return jsonResponse(204, null);
|
|
13985
14188
|
}
|
|
14189
|
+
if (headers.origin && !isOriginAllowed(headers.origin, corsOrigins)) {
|
|
14190
|
+
return errorResponse(403, "FORBIDDEN", `Origin ${headers.origin} is not allowed to call this API`);
|
|
14191
|
+
}
|
|
14192
|
+
if (method === "POST" || method === "PUT") {
|
|
14193
|
+
const contentType = headers["content-type"];
|
|
14194
|
+
if (contentType && !isJsonContentType(contentType)) {
|
|
14195
|
+
return errorResponse(
|
|
14196
|
+
415,
|
|
14197
|
+
"UNSUPPORTED_MEDIA_TYPE",
|
|
14198
|
+
`Content-Type must be application/json, got "${contentType}"`
|
|
14199
|
+
);
|
|
14200
|
+
}
|
|
14201
|
+
}
|
|
13986
14202
|
const match = matchRoute(method, pathname);
|
|
13987
14203
|
if (!match) {
|
|
13988
14204
|
const pathMatch = matchRoute("GET", pathname) || matchRoute("POST", pathname) || matchRoute("DELETE", pathname);
|
|
@@ -14005,19 +14221,24 @@ async function route(method, url, headers, body) {
|
|
|
14005
14221
|
return errorResponse(500, "INTERNAL_ERROR", errMsg(err));
|
|
14006
14222
|
}
|
|
14007
14223
|
}
|
|
14008
|
-
async function handleRequest(req, res, validateAuthFn) {
|
|
14224
|
+
async function handleRequest(req, res, validateAuthFn, corsOrigins) {
|
|
14009
14225
|
const url = req.url || "/";
|
|
14010
14226
|
const method = (req.method || "GET").toUpperCase();
|
|
14011
14227
|
const pathname = new URL(url, "http://localhost").pathname;
|
|
14012
14228
|
if (method === "OPTIONS") {
|
|
14013
|
-
const response2 = await route(method, url, headersToObject(req.headers), null);
|
|
14229
|
+
const response2 = await route(method, url, headersToObject(req.headers), null, corsOrigins);
|
|
14014
14230
|
writeResponse(res, response2);
|
|
14015
14231
|
return;
|
|
14016
14232
|
}
|
|
14017
14233
|
if (validateAuthFn && !isHealthCheckPath(pathname)) {
|
|
14018
14234
|
const authHeader = req.headers["authorization"];
|
|
14019
14235
|
if (!validateAuthFn(authHeader)) {
|
|
14020
|
-
|
|
14236
|
+
const denied = applyCorsHeaders(
|
|
14237
|
+
errorResponse(401, "UNAUTHORIZED", "Invalid or missing authentication token"),
|
|
14238
|
+
req.headers.origin,
|
|
14239
|
+
corsOrigins
|
|
14240
|
+
);
|
|
14241
|
+
writeResponse(res, denied);
|
|
14021
14242
|
return;
|
|
14022
14243
|
}
|
|
14023
14244
|
}
|
|
@@ -14025,7 +14246,7 @@ async function handleRequest(req, res, validateAuthFn) {
|
|
|
14025
14246
|
if (method === "POST" || method === "PUT") {
|
|
14026
14247
|
body = await readBody(req);
|
|
14027
14248
|
}
|
|
14028
|
-
const response = await route(method, url, headersToObject(req.headers), body);
|
|
14249
|
+
const response = await route(method, url, headersToObject(req.headers), body, corsOrigins);
|
|
14029
14250
|
writeResponse(res, response);
|
|
14030
14251
|
}
|
|
14031
14252
|
function headersToObject(headers) {
|
|
@@ -14066,26 +14287,35 @@ var HTTPServer = class {
|
|
|
14066
14287
|
host;
|
|
14067
14288
|
server = null;
|
|
14068
14289
|
validTokens;
|
|
14290
|
+
corsOrigins;
|
|
14069
14291
|
constructor(config) {
|
|
14070
14292
|
this.port = config?.port ?? 9224;
|
|
14071
|
-
this.host = config?.host ?? "
|
|
14293
|
+
this.host = config?.host ?? "127.0.0.1";
|
|
14072
14294
|
this.validTokens = resolveTokens(config?.tokens);
|
|
14295
|
+
this.corsOrigins = config?.corsOrigins;
|
|
14073
14296
|
}
|
|
14074
14297
|
/**
|
|
14075
14298
|
* Start the HTTP server and begin listening for requests.
|
|
14076
14299
|
*
|
|
14077
14300
|
* @returns The actual port and host the server bound to.
|
|
14078
|
-
* @throws If the server is already running
|
|
14301
|
+
* @throws If the server is already running, if binding a non-loopback
|
|
14302
|
+
* host without an auth token, or if the port cannot be bound.
|
|
14079
14303
|
*/
|
|
14080
14304
|
async start() {
|
|
14081
14305
|
if (this.server) {
|
|
14082
14306
|
throw new Error("HTTP server is already running");
|
|
14083
14307
|
}
|
|
14308
|
+
if (!isLoopbackHost(this.host) && !isAuthRequired(this.validTokens)) {
|
|
14309
|
+
throw new Error(
|
|
14310
|
+
`Refusing to start on non-loopback host "${this.host}" without an auth token \u2014 the API exposes session creation and command execution. Bind loopback only (the default, or --host 127.0.0.1), or provide a token via --token or XBROWSER_SERVER_TOKEN.`
|
|
14311
|
+
);
|
|
14312
|
+
}
|
|
14084
14313
|
const authRequired = isAuthRequired(this.validTokens);
|
|
14085
14314
|
const tokens = this.validTokens;
|
|
14315
|
+
const corsOrigins = this.corsOrigins;
|
|
14086
14316
|
this.server = createServer((req, res) => {
|
|
14087
14317
|
const authFn = authRequired ? (authHeader) => validateAuth(authHeader, tokens) : void 0;
|
|
14088
|
-
handleRequest(req, res, authFn).catch((err) => {
|
|
14318
|
+
handleRequest(req, res, authFn, corsOrigins).catch((err) => {
|
|
14089
14319
|
const message = err instanceof Error ? err.message : String(err);
|
|
14090
14320
|
if (!res.headersSent) {
|
|
14091
14321
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -14556,7 +14786,7 @@ async function routeCommand(argvIn, stdinCommands) {
|
|
|
14556
14786
|
replay: "replay <file> [--slow-mo <ms>] [--stop-on-error]",
|
|
14557
14787
|
create: "create <name> [--template static|dynamic|login|api]",
|
|
14558
14788
|
run: "run <file>",
|
|
14559
|
-
serve: "serve [--port <port>] [--token <token>]",
|
|
14789
|
+
serve: "serve [--port <port>] [--host <host>] [--token <token>] [--cors-origins <csv>]",
|
|
14560
14790
|
remote: "remote <url> [command] [--token <token>]",
|
|
14561
14791
|
convert: "convert <file> [--to js|py|sh]",
|
|
14562
14792
|
extract: "extract <file> [--format json|yaml]",
|
|
@@ -14955,7 +15185,10 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
14955
15185
|
async function handleServe(_args, options, mode) {
|
|
14956
15186
|
const port = options.port ? Number(options.port) : void 0;
|
|
14957
15187
|
const token = options.token;
|
|
14958
|
-
const
|
|
15188
|
+
const host = options.host;
|
|
15189
|
+
const corsOriginsRaw = options["cors-origins"];
|
|
15190
|
+
const corsOrigins = corsOriginsRaw ? corsOriginsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
15191
|
+
const httpServer = new HTTPServer({ port, host, tokens: token ? [token] : void 0, corsOrigins });
|
|
14959
15192
|
process.on("SIGINT", async () => {
|
|
14960
15193
|
await httpServer.stop();
|
|
14961
15194
|
return;
|