@xbrowser/cli 1.21.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.
Files changed (43) hide show
  1. package/README.md +21 -3
  2. package/dist/{browser-CNBSH53B.js → browser-BVRFSUXU.js} +3 -2
  3. package/dist/{browser-C2VEOJC2.js → browser-DSYUAE7H.js} +5 -4
  4. package/dist/{browser-VZOOFLAH.js → browser-Q67BJTI2.js} +4 -3
  5. package/dist/{cdp-driver-KFX4A6WR.js → cdp-driver-DC7EIUG5.js} +4 -3
  6. package/dist/{cdp-driver-7Z6ZAT5E.js → cdp-driver-T7D6H2RW.js} +335 -546
  7. package/dist/{cdp-driver-26LGCHXV.js → cdp-driver-WWO2UZDR.js} +3 -2
  8. package/dist/{chunk-Z5RTK4GW.js → chunk-2Q3GQRUP.js} +2 -2
  9. package/dist/{chunk-CLYZPSDR.js → chunk-4OU37CXP.js} +354 -576
  10. package/dist/{chunk-HWWG2ISJ.js → chunk-7OP2ISEY.js} +353 -575
  11. package/dist/{chunk-PIWNMC36.js → chunk-A3MORKYR.js} +1 -1
  12. package/dist/{chunk-3FWLW7FS.js → chunk-A7NEA4PA.js} +39 -1
  13. package/dist/{chunk-OMU63E6J.js → chunk-FBQZ3PVD.js} +5 -8
  14. package/dist/chunk-GYB5BOSP.js +602 -0
  15. package/dist/{chunk-ESJYANQO.js → chunk-IFKJO2UR.js} +360 -582
  16. package/dist/{chunk-NODRQGOK.js → chunk-IR6C24P7.js} +97 -0
  17. package/dist/{chunk-TEXCXIBW.js → chunk-LPW6QS4M.js} +5 -8
  18. package/dist/{chunk-3KZ34AUC.js → chunk-MYH6FIBZ.js} +2 -1
  19. package/dist/{chunk-AVSTNODR.js → chunk-T4WTKBWI.js} +2 -2
  20. package/dist/{chunk-HBHOKZPN.js → chunk-V6KGBU5J.js} +97 -0
  21. package/dist/{chunk-IWVG4XYZ.js → chunk-VCVDILH6.js} +2 -1
  22. package/dist/{chunk-NKW4A74J.js → chunk-XTUAUFL3.js} +5 -8
  23. package/dist/cli.js +497 -252
  24. package/dist/daemon-main.js +25 -19
  25. package/dist/{human-interaction-Y5IDH6LD.js → human-interaction-57VYIR56.js} +1 -1
  26. package/dist/{human-interaction-ZUTR5AC2.js → human-interaction-67K43B4O.js} +1 -1
  27. package/dist/{human-interaction-4YR2N6R6.js → human-interaction-MXST5ZQM.js} +1 -1
  28. package/dist/index.d.ts +52 -4
  29. package/dist/index.js +500 -256
  30. package/dist/keep-awake-M4KJS4B4.js +29 -0
  31. package/dist/{launcher-44STYRJC.js → launcher-IK2FKW6T.js} +1 -1
  32. package/dist/{launcher-EXJHHAUL.js → launcher-LUOMMGZC.js} +1 -1
  33. package/dist/{plugin-singleton-FCNDN3FX.js → plugin-singleton-GJO7EZXB.js} +1 -1
  34. package/dist/{recovery-NXC35EQN.js → recovery-6RI2FE3E.js} +1 -1
  35. package/dist/{recovery-MDWAVXE4.js → recovery-LGY25LIM.js} +1 -1
  36. package/dist/{recovery-L5GLDX4O.js → recovery-LMJW537X.js} +1 -1
  37. package/dist/{session-recorder-SLDBENVF.js → session-recorder-563ISSJK.js} +1 -1
  38. package/dist/{session-recorder-3BEVWHOK.js → session-recorder-GEZVQBKW.js} +1 -1
  39. package/dist/session-replayer-N665T3G5.js +932 -0
  40. package/dist/stealth-2AA2FSNN.js +29 -0
  41. package/dist/stealth-JOXGV34D.js +29 -0
  42. package/package.json +11 -7
  43. package/dist/session-replayer-EVD4HZBB.js +0 -413
package/dist/cli.js CHANGED
@@ -1,4 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ detectAntiBot,
4
+ formatDetectionMessage
5
+ } from "./chunk-JKVUFP3G.js";
2
6
  import {
3
7
  forwardCommandLog,
4
8
  forwardNetworkAnalyze,
@@ -45,7 +49,7 @@ import {
45
49
  } from "./chunk-Q2DFJQGS.js";
46
50
  import {
47
51
  SessionRecorder
48
- } from "./chunk-NODRQGOK.js";
52
+ } from "./chunk-IR6C24P7.js";
49
53
  import {
50
54
  addKnownIssue,
51
55
  getKnowledgePath,
@@ -66,21 +70,19 @@ import {
66
70
  getAllSessions,
67
71
  getBrowser,
68
72
  getSessionById,
69
- rand,
70
73
  resolveLaunchOpts,
71
74
  saveSessionDiskMeta,
72
- setActivePage,
75
+ setActivePage
76
+ } from "./chunk-IFKJO2UR.js";
77
+ import {
78
+ rand,
73
79
  sleep,
74
80
  wheelDelta
75
- } from "./chunk-ESJYANQO.js";
76
- import "./chunk-3KZ34AUC.js";
81
+ } from "./chunk-GYB5BOSP.js";
82
+ import "./chunk-MYH6FIBZ.js";
77
83
  import {
78
84
  errMsg
79
85
  } from "./chunk-GDKLH7ZY.js";
80
- import {
81
- detectAntiBot,
82
- formatDetectionMessage
83
- } from "./chunk-JKVUFP3G.js";
84
86
  import "./chunk-KFQGP6VL.js";
85
87
 
86
88
  // src/router.ts
@@ -7170,7 +7172,7 @@ import { join as join7 } from "path";
7170
7172
  import { execSync } from "child_process";
7171
7173
  var SHARED_PLUGIN_DEPENDENCIES = {
7172
7174
  "zod": "^3.24.0",
7173
- "@dyyz1993/xcli-core": "^0.12.1"
7175
+ "@dyyz1993/xcli-core": "^0.19.0"
7174
7176
  };
7175
7177
  function ensurePluginDependencies(pluginsDir) {
7176
7178
  const zodPath = join7(pluginsDir, "node_modules", "zod");
@@ -8248,7 +8250,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
8248
8250
  }
8249
8251
  let targetPageOverride = null;
8250
8252
  if (_target && extraOpts?.cdpEndpoint) {
8251
- const { findTargetPage } = await import("./browser-CNBSH53B.js");
8253
+ const { findTargetPage } = await import("./browser-BVRFSUXU.js");
8252
8254
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
8253
8255
  if (!targetPageOverride) {
8254
8256
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -8465,7 +8467,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
8465
8467
  const errorMessage = errMsg(err);
8466
8468
  if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
8467
8469
  try {
8468
- const { attemptRecovery } = await import("./recovery-NXC35EQN.js");
8470
+ const { attemptRecovery } = await import("./recovery-6RI2FE3E.js");
8469
8471
  const recovery = await attemptRecovery(
8470
8472
  session.page,
8471
8473
  sessionName,
@@ -9147,13 +9149,39 @@ async function installFromNpm(packageName, name, targetDir) {
9147
9149
  import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
9148
9150
  import { resolve as resolve5, join as join10 } from "path";
9149
9151
  import { tmpdir as tmpdir2 } from "os";
9150
- import { execSync as execSync2 } from "child_process";
9152
+ import { execFile as execFile2 } from "child_process";
9151
9153
  import { verifyPlugin as verifyPlugin3, safeCleanup as safeCleanup3 } from "@dyyz1993/xcli-core";
9154
+ var GIT_URL_RE = /^(?:https|git|git\+https|git\+ssh|ssh):\/\/[^\s"'`\\;<>()&]+$/i;
9155
+ var GIT_SCP_RE = /^git@[\w.-]+:[\w./~+-]+$/;
9156
+ function isValidGitUrl(url) {
9157
+ if (typeof url !== "string" || url.length === 0) return false;
9158
+ if (url.startsWith("-")) return false;
9159
+ if (/[\r\n\0]/.test(url)) return false;
9160
+ return GIT_URL_RE.test(url) || GIT_SCP_RE.test(url);
9161
+ }
9162
+ function execGit(args) {
9163
+ return new Promise((resolve10, reject) => {
9164
+ execFile2("git", args, (err, _stdout, stderr) => {
9165
+ if (err) {
9166
+ const stderrText = typeof stderr === "string" ? stderr : stderr?.toString("utf-8") ?? "";
9167
+ const detail = stderrText.trim() || err.message;
9168
+ reject(new Error(`git clone failed: ${detail}`));
9169
+ } else {
9170
+ resolve10();
9171
+ }
9172
+ });
9173
+ });
9174
+ }
9152
9175
  async function installFromGit(gitUrl, name, targetDir) {
9176
+ if (!isValidGitUrl(gitUrl)) {
9177
+ throw new Error(
9178
+ `Invalid git URL: ${JSON.stringify(gitUrl).slice(0, 120)} \u2014 allowed forms: https://, git://, git+https://, git+ssh://, ssh://, git@host:path`
9179
+ );
9180
+ }
9153
9181
  const tmpDir = join10(tmpdir2(), `xbrowser-git-${Date.now()}`);
9154
9182
  let warnings = [];
9155
9183
  try {
9156
- execSync2(`git clone --depth 1 "${gitUrl}" "${tmpDir}"`, { stdio: "pipe" });
9184
+ await execGit(["clone", "--depth", "1", "--", gitUrl, tmpDir]);
9157
9185
  const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
9158
9186
  warnings = verify.warnings ?? [];
9159
9187
  if (!verify.valid) {
@@ -10988,7 +11016,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
10988
11016
  }
10989
11017
  outputResult({ command: cmdDef.name, description: cmdDef.description, scope: cmdDef.scope, parameters: paramsList }, mode);
10990
11018
  } else {
10991
- console.log(helpGenerator.generate(cmdDef, { color: true, emoji: false }));
11019
+ process.stdout.write(helpGenerator.generate(cmdDef, { color: true, emoji: false }));
10992
11020
  }
10993
11021
  } else {
10994
11022
  outputError(`Unknown command: ${command}`);
@@ -11389,7 +11417,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
11389
11417
  if (isEmptyResult) {
11390
11418
  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";
11391
11419
  outputResult(result.data, mode);
11392
- console.error(`
11420
+ outputError(`
11393
11421
  \u26A0\uFE0F ${hint}`);
11394
11422
  process.exit(1);
11395
11423
  } else {
@@ -11406,8 +11434,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
11406
11434
  try {
11407
11435
  mkdirSync10(dirname6(outputFile), { recursive: true });
11408
11436
  writeFileSync12(outputFile, content, "utf-8");
11409
- console.log(`
11410
- \u{1F4C4} Written to ${outputFile}`);
11437
+ process.stdout.write(`
11438
+ \u{1F4C4} Written to ${outputFile}
11439
+ `);
11411
11440
  } catch (err) {
11412
11441
  outputError(`Failed to write --output "${outputFile}": ${err instanceof Error ? err.message : String(err)}`);
11413
11442
  }
@@ -11505,7 +11534,7 @@ async function handleSession(args, options, mode, _cdpEndpoint) {
11505
11534
  break;
11506
11535
  }
11507
11536
  default:
11508
- console.log(handleSessionHelp());
11537
+ process.stdout.write(handleSessionHelp() + "\n");
11509
11538
  }
11510
11539
  }
11511
11540
 
@@ -11679,19 +11708,24 @@ async function handleSearch(args, options, mode) {
11679
11708
  outputEnvelope({ success: true, data: { results: deduped, total: deduped.length } }, { command: "plugin search" }, mode);
11680
11709
  } else {
11681
11710
  if (deduped.length === 0) {
11682
- console.log("No plugins found");
11711
+ process.stdout.write("No plugins found\n");
11683
11712
  return;
11684
11713
  }
11685
11714
  for (const r of deduped) {
11686
11715
  const src = r.source === "marketplace" ? "[marketplace]" : r.source === "local" ? "[local]" : "[npm]";
11687
11716
  const slug = r.slug ? ` (${r.slug})` : "";
11688
- console.log(` ${src} ${r.name}${slug}`);
11689
- if (r.description) console.log(` ${r.description}`);
11690
- if (r.version) console.log(` Version: ${r.version}`);
11691
- if (r.downloads) console.log(` Downloads: ${r.downloads}`);
11692
- console.log("");
11717
+ process.stdout.write(` ${src} ${r.name}${slug}
11718
+ `);
11719
+ if (r.description) process.stdout.write(` ${r.description}
11720
+ `);
11721
+ if (r.version) process.stdout.write(` Version: ${r.version}
11722
+ `);
11723
+ if (r.downloads) process.stdout.write(` Downloads: ${r.downloads}
11724
+ `);
11725
+ process.stdout.write("\n");
11693
11726
  }
11694
- console.log(`Total: ${deduped.length} plugins`);
11727
+ process.stdout.write(`Total: ${deduped.length} plugins
11728
+ `);
11695
11729
  }
11696
11730
  }
11697
11731
  async function handlePluginInfo(args, options, mode) {
@@ -11708,14 +11742,22 @@ async function handlePluginInfo(args, options, mode) {
11708
11742
  outputEnvelope({ success: true, data: { source: "marketplace", ...d } }, { command: "plugin info" }, mode);
11709
11743
  return;
11710
11744
  }
11711
- console.log(`\u540D\u79F0: ${d.name || ""}`);
11712
- console.log(`\u7248\u672C: ${d.version || ""}`);
11713
- console.log(`\u63CF\u8FF0: ${d.description || ""}`);
11714
- console.log(`\u4F5C\u8005: ${d.author || ""}`);
11715
- console.log(`\u547D\u4EE4: ${(d.commands || []).join(", ")}`);
11716
- console.log(`\u4E0B\u8F7D\u91CF: ${d.downloads || 0}`);
11717
- console.log(`\u6807\u7B7E: ${(d.tags || []).join(", ")}`);
11718
- console.log(`\u7F51\u7AD9: ${(d.sites || []).join(", ")}`);
11745
+ process.stdout.write(`\u540D\u79F0: ${d.name || ""}
11746
+ `);
11747
+ process.stdout.write(`\u7248\u672C: ${d.version || ""}
11748
+ `);
11749
+ process.stdout.write(`\u63CF\u8FF0: ${d.description || ""}
11750
+ `);
11751
+ process.stdout.write(`\u4F5C\u8005: ${d.author || ""}
11752
+ `);
11753
+ process.stdout.write(`\u547D\u4EE4: ${(d.commands || []).join(", ")}
11754
+ `);
11755
+ process.stdout.write(`\u4E0B\u8F7D\u91CF: ${d.downloads || 0}
11756
+ `);
11757
+ process.stdout.write(`\u6807\u7B7E: ${(d.tags || []).join(", ")}
11758
+ `);
11759
+ process.stdout.write(`\u7F51\u7AD9: ${(d.sites || []).join(", ")}
11760
+ `);
11719
11761
  return;
11720
11762
  }
11721
11763
  } catch {
@@ -11734,19 +11776,25 @@ async function handlePluginInfo(args, options, mode) {
11734
11776
  outputEnvelope({ success: true, data: { source: "npm", name: pkg.name, version: latest, description: pkg.description } }, { command: "plugin info" }, mode);
11735
11777
  return;
11736
11778
  }
11737
- console.log(`\u540D\u79F0: ${pkg.name || ""}`);
11738
- console.log(`\u7248\u672C: ${latest}`);
11739
- console.log(`\u63CF\u8FF0: ${pkg.description || ""}`);
11779
+ process.stdout.write(`\u540D\u79F0: ${pkg.name || ""}
11780
+ `);
11781
+ process.stdout.write(`\u7248\u672C: ${latest}
11782
+ `);
11783
+ process.stdout.write(`\u63CF\u8FF0: ${pkg.description || ""}
11784
+ `);
11740
11785
  const author = pkg.author;
11741
- console.log(`\u4F5C\u8005: ${typeof author === "string" ? author : author?.name || ""}`);
11742
- console.log(`\u5173\u952E\u8BCD: ${(pkg.keywords || []).join(", ")}`);
11743
- console.log(`\u8BB8\u53EF\u8BC1: ${pkg.license || ""}`);
11786
+ process.stdout.write(`\u4F5C\u8005: ${typeof author === "string" ? author : author?.name || ""}
11787
+ `);
11788
+ process.stdout.write(`\u5173\u952E\u8BCD: ${(pkg.keywords || []).join(", ")}
11789
+ `);
11790
+ process.stdout.write(`\u8BB8\u53EF\u8BC1: ${pkg.license || ""}
11791
+ `);
11744
11792
  return;
11745
11793
  }
11746
11794
  }
11747
- console.error(`\u63D2\u4EF6 '${slug}' \u672A\u627E\u5230`);
11795
+ outputError(`\u63D2\u4EF6 '${slug}' \u672A\u627E\u5230`);
11748
11796
  } catch (err) {
11749
- console.error("\u67E5\u8BE2\u5931\u8D25:", errMsg(err));
11797
+ outputError("\u67E5\u8BE2\u5931\u8D25: " + errMsg(err));
11750
11798
  }
11751
11799
  }
11752
11800
  async function handlePluginSchema(args, mode) {
@@ -11770,32 +11818,39 @@ async function handlePluginSchema(args, mode) {
11770
11818
  }
11771
11819
  }
11772
11820
  function printPluginContract(contract) {
11773
- console.log(`${contract.plugin.name} contract v${contract.version}`);
11774
- if (contract.plugin.description) console.log(contract.plugin.description);
11775
- console.log("");
11821
+ process.stdout.write(`${contract.plugin.name} contract v${contract.version}
11822
+ `);
11823
+ if (contract.plugin.description) process.stdout.write(contract.plugin.description + "\n");
11824
+ process.stdout.write("\n");
11776
11825
  for (const command of contract.commands) {
11777
11826
  printCommandContract(contract.plugin.name, command);
11778
11827
  }
11779
11828
  }
11780
11829
  function printCommandContract(pluginName, command) {
11781
- console.log(`${pluginName} ${command.name}`);
11782
- if (command.description) console.log(` ${command.description}`);
11783
- console.log(` scope: ${command.scope}`);
11830
+ process.stdout.write(`${pluginName} ${command.name}
11831
+ `);
11832
+ if (command.description) process.stdout.write(` ${command.description}
11833
+ `);
11834
+ process.stdout.write(` scope: ${command.scope}
11835
+ `);
11784
11836
  if (command.capabilities.length > 0) {
11785
- console.log(` capabilities: ${command.capabilities.join(", ")}`);
11837
+ process.stdout.write(` capabilities: ${command.capabilities.join(", ")}
11838
+ `);
11786
11839
  }
11787
11840
  if (command.positional.length > 0) {
11788
- console.log(` positional: ${command.positional.join(", ")}`);
11841
+ process.stdout.write(` positional: ${command.positional.join(", ")}
11842
+ `);
11789
11843
  }
11790
11844
  if (command.form.fields.length > 0) {
11791
- console.log(" fields:");
11845
+ process.stdout.write(" fields:\n");
11792
11846
  for (const field of command.form.fields) {
11793
11847
  const required = field.required ? "required" : "optional";
11794
11848
  const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
11795
- console.log(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}`);
11849
+ process.stdout.write(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}
11850
+ `);
11796
11851
  }
11797
11852
  }
11798
- console.log("");
11853
+ process.stdout.write("\n");
11799
11854
  }
11800
11855
  async function handlePlugin(args, options, mode) {
11801
11856
  const sub = args[0];
@@ -11884,24 +11939,28 @@ async function handlePlugin(args, options, mode) {
11884
11939
  outputEnvelope({ success: true, data: { plugins: enrichedPlugins } }, { command: "plugin list" }, mode);
11885
11940
  } else {
11886
11941
  if (enrichedPlugins.length === 0) {
11887
- console.log("No plugins installed");
11942
+ process.stdout.write("No plugins installed\n");
11888
11943
  return;
11889
11944
  }
11890
11945
  for (const p of enrichedPlugins) {
11891
11946
  const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
11892
11947
  if (p.version && p.description) {
11893
- console.log(`${p.name} (${p.version}) - ${p.description}${loginTag}`);
11948
+ process.stdout.write(`${p.name} (${p.version}) - ${p.description}${loginTag}
11949
+ `);
11894
11950
  } else {
11895
- console.log(`${p.name}${loginTag}`);
11951
+ process.stdout.write(`${p.name}${loginTag}
11952
+ `);
11896
11953
  }
11897
11954
  if (p.commands && p.commands.length > 0) {
11898
- console.log(` ${p.commands.join(", ")}`);
11955
+ process.stdout.write(` ${p.commands.join(", ")}
11956
+ `);
11899
11957
  }
11900
11958
  if (p.requiresLoginCommands.length > 0) {
11901
- console.log(` requires login: ${p.requiresLoginCommands.join(", ")}`);
11959
+ process.stdout.write(` requires login: ${p.requiresLoginCommands.join(", ")}
11960
+ `);
11902
11961
  }
11903
11962
  }
11904
- console.log(`
11963
+ process.stdout.write(`
11905
11964
  Total: ${enrichedPlugins.length} plugins`);
11906
11965
  }
11907
11966
  break;
@@ -11938,7 +11997,7 @@ See docs/plugin-guide.md for the publishing workflow.`
11938
11997
  );
11939
11998
  break;
11940
11999
  default:
11941
- console.log(handlePluginHelp());
12000
+ process.stdout.write(handlePluginHelp() + "\n");
11942
12001
  }
11943
12002
  }
11944
12003
  function handleCreate(args, options) {
@@ -11972,7 +12031,7 @@ function handleDaemon(args, options, mode) {
11972
12031
  break;
11973
12032
  }
11974
12033
  default:
11975
- console.log("Daemon starts automatically. No manual action needed.");
12034
+ process.stdout.write("Daemon starts automatically. No manual action needed.\n");
11976
12035
  }
11977
12036
  }
11978
12037
 
@@ -12041,8 +12100,8 @@ async function handleRecord(args, options, mode) {
12041
12100
  }, mode);
12042
12101
  const md = SessionRecorder.readMarkdownSummary(sessionName);
12043
12102
  if (md) {
12044
- console.log("");
12045
- console.log(md);
12103
+ process.stdout.write("\n");
12104
+ process.stdout.write(md + "\n");
12046
12105
  } else {
12047
12106
  const summary = SessionRecorder.readSummary(sessionName);
12048
12107
  if (summary) {
@@ -12101,25 +12160,30 @@ async function handleRecord(args, options, mode) {
12101
12160
  break;
12102
12161
  }
12103
12162
  default:
12104
- console.log("Usage:");
12105
- console.log(" xbrowser record start [--url <url>] [--session <name>]");
12106
- console.log(" xbrowser record stop [--session <name>]");
12107
- console.log(" xbrowser record status [--session <name>]");
12108
- console.log(" xbrowser record summary [--session <name>] [--json]");
12109
- console.log(' xbrowser record checkpoint --type <type> --hint "description" [--selector <sel>] [--session <name>]');
12110
- console.log(" xbrowser record generate-plugin [--session <name>] [--name <plugin>] [--output <dir>]");
12111
- console.log("");
12112
- console.log("Checkpoint types: dialog, captcha, login, iframe, slider, custom");
12163
+ process.stdout.write("Usage:\n");
12164
+ process.stdout.write(" xbrowser record start [--url <url>] [--session <name>]\n");
12165
+ process.stdout.write(" xbrowser record stop [--session <name>]\n");
12166
+ process.stdout.write(" xbrowser record status [--session <name>]\n");
12167
+ process.stdout.write(" xbrowser record summary [--session <name>] [--json]\n");
12168
+ process.stdout.write(' xbrowser record checkpoint --type <type> --hint "description" [--selector <sel>] [--session <name>]\n');
12169
+ process.stdout.write(" xbrowser record generate-plugin [--session <name>] [--name <plugin>] [--output <dir>]\n");
12170
+ process.stdout.write("\n");
12171
+ process.stdout.write("Checkpoint types: dialog, captcha, login, iframe, slider, custom\n");
12113
12172
  }
12114
12173
  }
12115
12174
  function printRecordingSummary(summary, sessionName) {
12116
- console.log("");
12117
- console.log("=== Recording Summary ===");
12118
- console.log(` Start URL: ${summary.startUrl}`);
12119
- console.log(` Duration: ${Math.round(summary.durationMs / 1e3)}s`);
12120
- console.log(` Actions: ${summary.totalActions}`);
12121
- console.log(` Network: ${summary.totalNetworkRequests}`);
12122
- console.log(` Steps: ${summary.steps.length}`);
12175
+ process.stdout.write("\n");
12176
+ process.stdout.write("=== Recording Summary ===\n");
12177
+ process.stdout.write(` Start URL: ${summary.startUrl}
12178
+ `);
12179
+ process.stdout.write(` Duration: ${Math.round(summary.durationMs / 1e3)}s
12180
+ `);
12181
+ process.stdout.write(` Actions: ${summary.totalActions}
12182
+ `);
12183
+ process.stdout.write(` Network: ${summary.totalNetworkRequests}
12184
+ `);
12185
+ process.stdout.write(` Steps: ${summary.steps.length}
12186
+ `);
12123
12187
  for (const step of summary.steps) {
12124
12188
  const a = step.action;
12125
12189
  const el = a.element;
@@ -12141,20 +12205,24 @@ function printRecordingSummary(summary, sessionName) {
12141
12205
  }
12142
12206
  const navInfo = step.contextChanges.find((c) => c.type === "navigate");
12143
12207
  if (navInfo) desc += ` \u2192 navigate to ${navInfo.url?.substring(0, 80)}`;
12144
- console.log(` ${step.step}. ${desc}`);
12208
+ process.stdout.write(` ${step.step}. ${desc}
12209
+ `);
12145
12210
  if (a.clickContext) {
12146
12211
  const ctx = a.clickContext;
12147
12212
  if (ctx.appeared?.length > 0) {
12148
12213
  for (const popup of ctx.appeared) {
12149
12214
  const roleStr = popup.role ? ` [${popup.role}]` : "";
12150
- console.log(` \u21B3 ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"`);
12215
+ process.stdout.write(` \u21B3 ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"
12216
+ `);
12151
12217
  if (popup.items?.length > 0) {
12152
12218
  for (const item of popup.items.slice(0, 10)) {
12153
12219
  const disStr = item.disabled ? " [disabled]" : "";
12154
- console.log(` \u2022 ${item.text}${disStr}`);
12220
+ process.stdout.write(` \u2022 ${item.text}${disStr}
12221
+ `);
12155
12222
  }
12156
12223
  if (popup.items.length > 10) {
12157
- console.log(` ... and ${popup.items.length - 10} more items`);
12224
+ process.stdout.write(` ... and ${popup.items.length - 10} more items
12225
+ `);
12158
12226
  }
12159
12227
  }
12160
12228
  }
@@ -12167,31 +12235,41 @@ function printRecordingSummary(summary, sessionName) {
12167
12235
  if (sc.ariaSelected !== void 0) parts.push(`selected=${sc.ariaSelected}`);
12168
12236
  if (sc.dataState) parts.push(`state=${sc.dataState}`);
12169
12237
  if (parts.length > 0) {
12170
- console.log(` \u21B3 state: <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${parts.join(", ")}`);
12238
+ process.stdout.write(` \u21B3 state: <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${parts.join(", ")}
12239
+ `);
12171
12240
  }
12172
12241
  }
12173
12242
  }
12174
12243
  }
12175
12244
  }
12176
- console.log("");
12177
- console.log(` Files: ${SessionRecorder.getRecordingsDir(sessionName)}/`);
12245
+ process.stdout.write("\n");
12246
+ process.stdout.write(` Files: ${SessionRecorder.getRecordingsDir(sessionName)}/
12247
+ `);
12178
12248
  if (summary.checkpoints && summary.checkpoints.length > 0) {
12179
- console.log("");
12180
- console.log(` Checkpoints (${summary.checkpoints.length}):`);
12249
+ process.stdout.write("\n");
12250
+ process.stdout.write(` Checkpoints (${summary.checkpoints.length}):
12251
+ `);
12181
12252
  for (const cp of summary.checkpoints) {
12182
12253
  const src = cp.source === "auto" ? "[auto]" : "[manual]";
12183
- console.log(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}`);
12184
- if (cp.selector) console.log(` selector: ${cp.selector}`);
12254
+ process.stdout.write(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}
12255
+ `);
12256
+ if (cp.selector) process.stdout.write(` selector: ${cp.selector}
12257
+ `);
12185
12258
  }
12186
12259
  }
12187
12260
  }
12188
12261
  function printHumanReadableSummary(summary) {
12189
- console.log(`Start URL: ${summary.startUrl}`);
12190
- console.log(`Recorded: ${summary.recordedAt}`);
12191
- console.log(`Duration: ${Math.round(summary.durationMs / 1e3)}s`);
12192
- console.log(`Actions: ${summary.totalActions}`);
12193
- console.log(`Network: ${summary.totalNetworkRequests}`);
12194
- console.log("");
12262
+ process.stdout.write(`Start URL: ${summary.startUrl}
12263
+ `);
12264
+ process.stdout.write(`Recorded: ${summary.recordedAt}
12265
+ `);
12266
+ process.stdout.write(`Duration: ${Math.round(summary.durationMs / 1e3)}s
12267
+ `);
12268
+ process.stdout.write(`Actions: ${summary.totalActions}
12269
+ `);
12270
+ process.stdout.write(`Network: ${summary.totalNetworkRequests}
12271
+ `);
12272
+ process.stdout.write("\n");
12195
12273
  for (const step of summary.steps) {
12196
12274
  const a = step.action;
12197
12275
  const el = a.element;
@@ -12207,20 +12285,23 @@ function printHumanReadableSummary(summary) {
12207
12285
  if (a.value) parts.push(`value="${a.value.substring(0, 50)}"`);
12208
12286
  if (a.key) parts.push(`key=${a.key}`);
12209
12287
  if (a.x !== void 0 && a.y !== void 0) parts.push(`@(${a.x},${a.y})`);
12210
- console.log(parts.join(" "));
12288
+ process.stdout.write(parts.join(" ") + "\n");
12211
12289
  if (a.clickContext) {
12212
12290
  const ctx = a.clickContext;
12213
12291
  if (ctx.appeared?.length > 0) {
12214
12292
  for (const popup of ctx.appeared) {
12215
12293
  const roleStr = popup.role ? ` [${popup.role}]` : "";
12216
- console.log(` \u{1F4CB} ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"`);
12294
+ process.stdout.write(` \u{1F4CB} ${popup.tag}${roleStr} "${(popup.text || "").substring(0, 60)}"
12295
+ `);
12217
12296
  if (popup.items?.length > 0) {
12218
12297
  for (const item of popup.items.slice(0, 10)) {
12219
12298
  const disStr = item.disabled ? " [disabled]" : "";
12220
- console.log(` \u2022 ${item.text}${disStr}`);
12299
+ process.stdout.write(` \u2022 ${item.text}${disStr}
12300
+ `);
12221
12301
  }
12222
12302
  if (popup.items.length > 10) {
12223
- console.log(` ... and ${popup.items.length - 10} more items`);
12303
+ process.stdout.write(` ... and ${popup.items.length - 10} more items
12304
+ `);
12224
12305
  }
12225
12306
  }
12226
12307
  }
@@ -12233,40 +12314,50 @@ function printHumanReadableSummary(summary) {
12233
12314
  if (sc.ariaSelected !== void 0) stateParts.push(`selected=${sc.ariaSelected}`);
12234
12315
  if (sc.dataState) stateParts.push(`state=${sc.dataState}`);
12235
12316
  if (stateParts.length > 0) {
12236
- console.log(` \u{1F504} <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${stateParts.join(", ")}`);
12317
+ process.stdout.write(` \u{1F504} <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${stateParts.join(", ")}
12318
+ `);
12237
12319
  }
12238
12320
  }
12239
12321
  }
12240
12322
  }
12241
12323
  for (const net of step.network) {
12242
- console.log(` \u2192 ${net.method} ${net.path} [${net.status}] ${net.resourceType}`);
12324
+ process.stdout.write(` \u2192 ${net.method} ${net.path} [${net.status}] ${net.resourceType}
12325
+ `);
12243
12326
  if (net.requestBody && typeof net.requestBody === "object") {
12244
12327
  const bodyStr = JSON.stringify(net.requestBody);
12245
12328
  if (bodyStr.length <= 200) {
12246
- console.log(` body: ${bodyStr}`);
12329
+ process.stdout.write(` body: ${bodyStr}
12330
+ `);
12247
12331
  } else {
12248
- console.log(` body: ${bodyStr.substring(0, 200)}... (${bodyStr.length} bytes)`);
12332
+ process.stdout.write(` body: ${bodyStr.substring(0, 200)}... (${bodyStr.length} bytes)
12333
+ `);
12249
12334
  }
12250
12335
  }
12251
12336
  }
12252
12337
  for (const match of step.matchedInputs) {
12253
- console.log(` \u{1F517} input "${match.inputValue}" \u2192 network #${match.networkId} param "${match.paramName}"`);
12338
+ process.stdout.write(` \u{1F517} input "${match.inputValue}" \u2192 network #${match.networkId} param "${match.paramName}"
12339
+ `);
12254
12340
  }
12255
12341
  for (const ctx of step.contextChanges) {
12256
12342
  if (ctx.type === "navigate") {
12257
- console.log(` \u2197 navigate \u2192 ${ctx.url}`);
12343
+ process.stdout.write(` \u2197 navigate \u2192 ${ctx.url}
12344
+ `);
12258
12345
  } else if (ctx.type === "new_tab") {
12259
- console.log(` \u2197 new tab: ${ctx.url}`);
12346
+ process.stdout.write(` \u2197 new tab: ${ctx.url}
12347
+ `);
12260
12348
  }
12261
12349
  }
12262
12350
  }
12263
12351
  if (summary.checkpoints && summary.checkpoints.length > 0) {
12264
- console.log("");
12265
- console.log(`Checkpoints (${summary.checkpoints.length}):`);
12352
+ process.stdout.write("\n");
12353
+ process.stdout.write(`Checkpoints (${summary.checkpoints.length}):
12354
+ `);
12266
12355
  for (const cp of summary.checkpoints) {
12267
12356
  const src = cp.source === "auto" ? "[auto]" : "[manual]";
12268
- console.log(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}`);
12269
- if (cp.selector) console.log(` selector: ${cp.selector}`);
12357
+ process.stdout.write(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}
12358
+ `);
12359
+ if (cp.selector) process.stdout.write(` selector: ${cp.selector}
12360
+ `);
12270
12361
  }
12271
12362
  }
12272
12363
  }
@@ -12292,13 +12383,25 @@ async function handleReplay(args, options, mode) {
12292
12383
  }
12293
12384
  return;
12294
12385
  }
12386
+ if (result.ok && mode !== "json" && mode !== "yaml") {
12387
+ const healed = typeof result.healed === "number" ? result.healed : 0;
12388
+ if (healed > 0) {
12389
+ process.stdout.write(`
12390
+ Self-healed ${healed} action(s):
12391
+ `);
12392
+ for (const d of result.healedDetails ?? []) {
12393
+ process.stdout.write(` - step ${d.index + 1}: ${d.strategy}
12394
+ `);
12395
+ }
12396
+ }
12397
+ }
12295
12398
  outputResult(result, mode);
12296
12399
  }
12297
12400
  async function handleConvert(args, _mode) {
12298
12401
  const filePath = args[0];
12299
12402
  const outputPath = args[1];
12300
12403
  if (!filePath || !outputPath) {
12301
- console.error("Usage: xbrowser convert <recording.yaml> <output.{js,py,sh}>");
12404
+ outputError("Usage: xbrowser convert <recording.yaml> <output.{js,py,sh}>");
12302
12405
  process.exit(1);
12303
12406
  }
12304
12407
  const fs3 = await import("fs");
@@ -12310,11 +12413,11 @@ async function handleConvert(args, _mode) {
12310
12413
  const content = fs3.readFileSync(filePath, "utf-8");
12311
12414
  recording = yaml.parse(content);
12312
12415
  } catch (e) {
12313
- console.error(`Error: Failed to read "${filePath}": ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
12416
+ outputError(`Error: Failed to read "${filePath}": ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
12314
12417
  process.exit(1);
12315
12418
  }
12316
12419
  if (recording === null || typeof recording !== "object" || Array.isArray(recording)) {
12317
- console.error(`Error: "${filePath}" does not contain a valid recording (expected a YAML/JSON object with events or actions).`);
12420
+ outputError(`Error: "${filePath}" does not contain a valid recording (expected a YAML/JSON object with events or actions).`);
12318
12421
  process.exit(1);
12319
12422
  }
12320
12423
  const rawActions = recording.actions;
@@ -12348,24 +12451,28 @@ async function handleConvert(args, _mode) {
12348
12451
  fs3.writeFileSync(outputPath, script);
12349
12452
  fs3.chmodSync(outputPath, 493);
12350
12453
  const eventCount = (recordingTyped.events || []).length;
12351
- console.log(`Converted ${filePath} -> ${outputPath}`);
12352
- console.log(` Events: ${eventCount}, Start URL: ${recordingTyped.startUrl}`);
12353
- console.log(` Run: ${ext === ".py" ? "python" : ext === ".sh" ? "./" : "node"} ${outputPath}`);
12454
+ process.stdout.write(`Converted ${filePath} -> ${outputPath}
12455
+ `);
12456
+ process.stdout.write(` Events: ${eventCount}, Start URL: ${recordingTyped.startUrl}
12457
+ `);
12458
+ process.stdout.write(` Run: ${ext === ".py" ? "python" : ext === ".sh" ? "./" : "node"} ${outputPath}
12459
+ `);
12354
12460
  }
12355
12461
  async function handleExtract(args, _mode) {
12356
12462
  const filePath = args[0];
12357
12463
  if (!filePath) {
12358
- console.error("Usage: xbrowser extract <recording.yaml>");
12464
+ outputError("Usage: xbrowser extract <recording.yaml>");
12359
12465
  process.exit(1);
12360
12466
  }
12361
12467
  const { extractAndSave, printExtractSummary } = await import("./extract-EUWPRSKH.js");
12362
12468
  try {
12363
12469
  const { summary, outputPath } = extractAndSave(filePath);
12364
12470
  printExtractSummary(summary);
12365
- console.log(`
12366
- Saved LLM summary: ${outputPath}`);
12471
+ process.stdout.write(`
12472
+ Saved LLM summary: ${outputPath}
12473
+ `);
12367
12474
  } catch (e) {
12368
- console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
12475
+ outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
12369
12476
  process.exit(1);
12370
12477
  }
12371
12478
  }
@@ -12373,7 +12480,7 @@ async function handleFilter(args, _mode, options) {
12373
12480
  const filePath = args[0];
12374
12481
  const outputPath = args[1];
12375
12482
  if (!filePath || !outputPath) {
12376
- console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude type1,type2]");
12483
+ outputError("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude type1,type2]");
12377
12484
  process.exit(1);
12378
12485
  }
12379
12486
  const { filterRecording, parseExcludeTypes } = await import("./filter-7YOPVPVC.js");
@@ -12385,15 +12492,17 @@ async function handleFilter(args, _mode, options) {
12385
12492
  const excludeTypes = parseExcludeTypes(excludeArgs);
12386
12493
  try {
12387
12494
  const result = filterRecording(filePath, outputPath, excludeTypes);
12388
- console.log(`Filtered ${filePath} -> ${outputPath}`);
12389
- console.log(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)`);
12495
+ process.stdout.write(`Filtered ${filePath} -> ${outputPath}
12496
+ `);
12497
+ process.stdout.write(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)
12498
+ `);
12390
12499
  } catch (e) {
12391
- console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
12500
+ outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
12392
12501
  process.exit(1);
12393
12502
  }
12394
12503
  }
12395
12504
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
12396
- const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-SLDBENVF.js");
12505
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-563ISSJK.js");
12397
12506
  const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
12398
12507
  const { mkdirSync: mkdirSync10, writeFileSync: writeFileSync12 } = await import("fs");
12399
12508
  const { join: join15 } = await import("path");
@@ -12417,21 +12526,30 @@ async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
12417
12526
  if (knowledgeMd) {
12418
12527
  writeFileSync12(join15(finalOutputDir, "SITE_KNOWLEDGE.md"), knowledgeMd, "utf-8");
12419
12528
  }
12420
- console.log("");
12421
- console.log("=== Plugin Generated ===");
12422
- console.log(` Plugin: ${finalPluginName}`);
12423
- console.log(` Domain: ${domain}`);
12424
- console.log(` Output: ${finalOutputDir}/index.ts`);
12529
+ process.stdout.write("\n");
12530
+ process.stdout.write("=== Plugin Generated ===\n");
12531
+ process.stdout.write(` Plugin: ${finalPluginName}
12532
+ `);
12533
+ process.stdout.write(` Domain: ${domain}
12534
+ `);
12535
+ process.stdout.write(` Output: ${finalOutputDir}/index.ts
12536
+ `);
12425
12537
  if (knowledgeMd) {
12426
- console.log(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md`);
12538
+ process.stdout.write(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md
12539
+ `);
12427
12540
  }
12428
- console.log(` Actions: ${data.actions.length}`);
12429
- console.log(` APIs: ${data.network.filter((n) => n.contentType.includes("json") || n.url.includes("/api/")).length}`);
12430
- console.log("");
12431
- console.log("Next steps:");
12432
- console.log(` 1. Review and edit: ${finalOutputDir}/index.ts`);
12433
- console.log(` 2. Test: xbrowser ${finalPluginName} <command>`);
12434
- console.log(` 3. Reference: ${finalOutputDir}/SITE_KNOWLEDGE.md (for LLM)`);
12541
+ process.stdout.write(` Actions: ${data.actions.length}
12542
+ `);
12543
+ process.stdout.write(` APIs: ${data.network.filter((n) => n.contentType.includes("json") || n.url.includes("/api/")).length}
12544
+ `);
12545
+ process.stdout.write("\n");
12546
+ process.stdout.write("Next steps:\n");
12547
+ process.stdout.write(` 1. Review and edit: ${finalOutputDir}/index.ts
12548
+ `);
12549
+ process.stdout.write(` 2. Test: xbrowser ${finalPluginName} <command>
12550
+ `);
12551
+ process.stdout.write(` 3. Reference: ${finalOutputDir}/SITE_KNOWLEDGE.md (for LLM)
12552
+ `);
12435
12553
  }
12436
12554
  function generatePluginCode(pluginName, domain, data, _knowledgeMd) {
12437
12555
  const pagePaths = /* @__PURE__ */ new Set();
@@ -12610,20 +12728,22 @@ async function handleRun(filePath, options) {
12610
12728
  });
12611
12729
  for (const step of chainResult.steps) {
12612
12730
  if (step.success) {
12613
- console.log(`[OK] ${step.raw}`);
12731
+ process.stdout.write(`[OK] ${step.raw}
12732
+ `);
12614
12733
  if (step.data && typeof step.data === "object") {
12615
12734
  const d = step.data;
12616
12735
  for (const [k, v] of Object.entries(d)) {
12617
12736
  if (k !== "ok")
12618
- console.log(` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
12737
+ process.stdout.write(` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}
12738
+ `);
12619
12739
  }
12620
12740
  }
12621
12741
  } else {
12622
- console.error(`[FAIL] ${step.raw}: ${step.message}`);
12742
+ outputError(`[FAIL] ${step.raw}: ${step.message}`);
12623
12743
  }
12624
12744
  }
12625
12745
  if (chainResult.stoppedReason) {
12626
- console.error(`Stopped: ${chainResult.stoppedReason}`);
12746
+ outputError(`Stopped: ${chainResult.stoppedReason}`);
12627
12747
  }
12628
12748
  if (!chainResult.success) process.exit(1);
12629
12749
  }
@@ -12666,26 +12786,29 @@ async function handleNetCommand(args, options, mode, sessionName) {
12666
12786
  if (mode === "json") {
12667
12787
  outputResult(result, mode);
12668
12788
  } else {
12669
- console.log(`
12789
+ process.stdout.write(`
12670
12790
  Network captures (session: ${netSession})`);
12671
- console.log(` Total: ${result.total}, Showing: ${result.captures.length}
12791
+ process.stdout.write(` Total: ${result.total}, Showing: ${result.captures.length}
12672
12792
  `);
12673
12793
  for (const c of result.captures) {
12674
12794
  const statusColor = c.status < 300 ? "\x1B[32m" : c.status < 400 ? "\x1B[33m" : "\x1B[31m";
12675
12795
  const reset = "\x1B[0m";
12676
- console.log(` #${c.id} ${c.method.padEnd(6)} ${statusColor}${c.status}${reset} ${c.resourceType.padEnd(10)} ${c.path}`);
12796
+ process.stdout.write(` #${c.id} ${c.method.padEnd(6)} ${statusColor}${c.status}${reset} ${c.resourceType.padEnd(10)} ${c.path}
12797
+ `);
12677
12798
  if (c.size > 0) {
12678
12799
  const sizeStr = c.size > 1024 ? `${(c.size / 1024).toFixed(1)}KB` : `${c.size}B`;
12679
- console.log(` ${c.contentType.split(";")[0]} ${sizeStr}`);
12800
+ process.stdout.write(` ${c.contentType.split(";")[0]} ${sizeStr}
12801
+ `);
12680
12802
  }
12681
12803
  }
12682
- console.log("");
12804
+ process.stdout.write("\n");
12683
12805
  }
12684
12806
  break;
12685
12807
  }
12686
12808
  case "clear": {
12687
12809
  await forwardNetworkClear(netSession);
12688
- console.log(`Network captures cleared for session: ${netSession}`);
12810
+ process.stdout.write(`Network captures cleared for session: ${netSession}
12811
+ `);
12689
12812
  break;
12690
12813
  }
12691
12814
  case "top": {
@@ -12695,21 +12818,23 @@ async function handleNetCommand(args, options, mode, sessionName) {
12695
12818
  if (mode === "json") {
12696
12819
  outputResult(result, mode);
12697
12820
  } else {
12698
- console.log(`
12821
+ process.stdout.write(`
12699
12822
  Top valued requests (session: ${netSession})`);
12700
- console.log(` Showing: ${result.entries.length}
12823
+ process.stdout.write(` Showing: ${result.entries.length}
12701
12824
  `);
12702
12825
  for (const e of result.entries) {
12703
12826
  const scoreColor = e.score >= 50 ? "\x1B[32m" : e.score >= 20 ? "\x1B[33m" : "\x1B[90m";
12704
12827
  const reset = "\x1B[0m";
12705
12828
  const methodStr = e.method.padEnd(6);
12706
12829
  const scoreStr = `${scoreColor}${e.score.toString().padStart(3)}${reset}`;
12707
- console.log(` ${scoreStr} ${methodStr} ${e.status} ${e.resourceType.padEnd(10)} ${e.path}`);
12830
+ process.stdout.write(` ${scoreStr} ${methodStr} ${e.status} ${e.resourceType.padEnd(10)} ${e.path}
12831
+ `);
12708
12832
  if (e.scoreBreakdown.content > 0) {
12709
- console.log(` ${e.contentType.split(";")[0]} ${e.size > 1024 ? (e.size / 1024).toFixed(1) + "KB" : e.size + "B"}`);
12833
+ process.stdout.write(` ${e.contentType.split(";")[0]} ${e.size > 1024 ? (e.size / 1024).toFixed(1) + "KB" : e.size + "B"}
12834
+ `);
12710
12835
  }
12711
12836
  }
12712
- console.log("");
12837
+ process.stdout.write("\n");
12713
12838
  }
12714
12839
  break;
12715
12840
  }
@@ -12718,16 +12843,17 @@ async function handleNetCommand(args, options, mode, sessionName) {
12718
12843
  if (mode === "json") {
12719
12844
  outputResult(logResult, mode);
12720
12845
  } else {
12721
- console.log(`
12846
+ process.stdout.write(`
12722
12847
  Command log (session: ${netSession})`);
12723
- console.log(` Total: ${logResult.commands.length}
12848
+ process.stdout.write(` Total: ${logResult.commands.length}
12724
12849
  `);
12725
12850
  for (const cmd of logResult.commands) {
12726
12851
  const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
12727
12852
  const paramsStr = Object.entries(cmd.params).map(([k, v]) => `${k}=${v}`).join(" ");
12728
- console.log(` #${cmd.id} [${ts}] ${cmd.command} ${paramsStr}`);
12853
+ process.stdout.write(` #${cmd.id} [${ts}] ${cmd.command} ${paramsStr}
12854
+ `);
12729
12855
  }
12730
- console.log("");
12856
+ process.stdout.write("\n");
12731
12857
  }
12732
12858
  break;
12733
12859
  }
@@ -12743,28 +12869,31 @@ async function handleNetCommand(args, options, mode, sessionName) {
12743
12869
  outputResult(aroundResult, mode);
12744
12870
  } else {
12745
12871
  if (!aroundResult) {
12746
- console.log(" No command found with that ID");
12872
+ process.stdout.write(" No command found with that ID\n");
12747
12873
  break;
12748
12874
  }
12749
12875
  const cmd = aroundResult.command;
12750
12876
  const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
12751
- console.log(`
12877
+ process.stdout.write(`
12752
12878
  Command: #${cmd.id} [${ts}] ${cmd.command}`);
12753
- console.log(` Window: \xB1${windowMs}ms
12879
+ process.stdout.write(` Window: \xB1${windowMs}ms
12754
12880
  `);
12755
12881
  const before = aroundResult.before;
12756
12882
  const after = aroundResult.after;
12757
- console.log(` BEFORE (${before.length} requests):`);
12883
+ process.stdout.write(` BEFORE (${before.length} requests):
12884
+ `);
12758
12885
  for (const r of before.slice(0, 5)) {
12759
- console.log(` ${r.method} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}`);
12886
+ process.stdout.write(` ${r.method} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}
12887
+ `);
12760
12888
  }
12761
- console.log(`
12889
+ process.stdout.write(`
12762
12890
  AFTER (${aroundResult.afterCount} requests):`);
12763
12891
  for (const r of after.slice(0, 10)) {
12764
12892
  const highlight = r.method !== "GET" ? " \u2190" : "";
12765
- console.log(` ${String(r.method).padEnd(6)} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}${highlight}`);
12893
+ process.stdout.write(` ${String(r.method).padEnd(6)} ${r.status} ${String(r.resourceType).padEnd(10)} ${r.path}${highlight}
12894
+ `);
12766
12895
  }
12767
- console.log("");
12896
+ process.stdout.write("\n");
12768
12897
  }
12769
12898
  break;
12770
12899
  }
@@ -12773,9 +12902,9 @@ async function handleNetCommand(args, options, mode, sessionName) {
12773
12902
  if (mode === "json") {
12774
12903
  outputResult(result, mode);
12775
12904
  } else {
12776
- console.log(`
12905
+ process.stdout.write(`
12777
12906
  API Reusability Analysis (session: ${netSession})`);
12778
- console.log(` Total: ${result.total}, Analyzed: ${result.analyzed.length}
12907
+ process.stdout.write(` Total: ${result.total}, Analyzed: ${result.analyzed.length}
12779
12908
  `);
12780
12909
  const groups = { high: [], medium: [], low: [], unknown: [] };
12781
12910
  for (const e of result.analyzed) {
@@ -12786,16 +12915,20 @@ async function handleNetCommand(args, options, mode, sessionName) {
12786
12915
  if (!items?.length) continue;
12787
12916
  const color = level === "high" ? "\x1B[32m" : level === "medium" ? "\x1B[33m" : level === "low" ? "\x1B[31m" : "\x1B[90m";
12788
12917
  const reset = "\x1B[0m";
12789
- console.log(` ${color}${level.toUpperCase()}${reset} (${items.length})`);
12918
+ process.stdout.write(` ${color}${level.toUpperCase()}${reset} (${items.length})
12919
+ `);
12790
12920
  for (const e of items.slice(0, 5)) {
12791
12921
  const scoreStr = `[${e.reusability.score.toString().padStart(3)}]`;
12792
- console.log(` ${e.method.padEnd(6)} ${e.status} ${scoreStr} ${e.path}`);
12922
+ process.stdout.write(` ${e.method.padEnd(6)} ${e.status} ${scoreStr} ${e.path}
12923
+ `);
12793
12924
  if (e.reusability.reasons.length > 0) {
12794
- console.log(` ${e.reusability.reasons.join(", ")}`);
12925
+ process.stdout.write(` ${e.reusability.reasons.join(", ")}
12926
+ `);
12795
12927
  }
12796
12928
  }
12797
- if (items.length > 5) console.log(` ... and ${items.length - 5} more`);
12798
- console.log("");
12929
+ if (items.length > 5) process.stdout.write(` ... and ${items.length - 5} more
12930
+ `);
12931
+ process.stdout.write("\n");
12799
12932
  }
12800
12933
  }
12801
12934
  break;
@@ -12814,12 +12947,12 @@ async function handleNetCommand(args, options, mode, sessionName) {
12814
12947
  if (mode === "json") {
12815
12948
  outputResult(result, mode);
12816
12949
  } else {
12817
- console.log(`
12950
+ process.stdout.write(`
12818
12951
  ${result.method} ${result.url}`);
12819
- console.log(` Headers: ${result.headerCount}, Body: ${result.hasBody}
12952
+ process.stdout.write(` Headers: ${result.headerCount}, Body: ${result.hasBody}
12820
12953
  `);
12821
- console.log(result.command);
12822
- console.log("");
12954
+ process.stdout.write(result.command + "\n");
12955
+ process.stdout.write("\n");
12823
12956
  }
12824
12957
  break;
12825
12958
  }
@@ -12837,27 +12970,33 @@ async function handleNetCommand(args, options, mode, sessionName) {
12837
12970
  if (mode === "json") {
12838
12971
  outputResult(result, mode);
12839
12972
  } else {
12840
- console.log(`
12973
+ process.stdout.write(`
12841
12974
  Replay Result`);
12842
- console.log(` ${result.curlCommand?.split("\n")[0]?.trim()}
12975
+ process.stdout.write(` ${result.curlCommand?.split("\n")[0]?.trim()}
12843
12976
  `);
12844
12977
  const replay = result.replay;
12845
12978
  if (replay?.error) {
12846
- console.log(` \x1B[31mFAILED\x1B[0m: ${replay.error}`);
12979
+ process.stdout.write(` \x1B[31mFAILED\x1B[0m: ${replay.error}
12980
+ `);
12847
12981
  } else if (replay) {
12848
12982
  const statusColor = replay.status && replay.status < 300 ? "\x1B[32m" : "\x1B[31m";
12849
12983
  const status = replay.status;
12850
12984
  const size = replay.size;
12851
12985
  const duration = replay.duration;
12852
- console.log(` Status: ${statusColor}${status}\x1B[0m ${replay.statusText}`);
12853
- console.log(` Size: ${size > 1024 ? (size / 1024).toFixed(1) + "KB" : size + "B"}`);
12854
- console.log(` Duration: ${duration}ms`);
12855
- console.log(` Body Match: ${replay.bodyMatch ? "\x1B[32mYes\x1B[0m" : "\x1B[33mNo\x1B[0m"}`);
12986
+ process.stdout.write(` Status: ${statusColor}${status}\x1B[0m ${replay.statusText}
12987
+ `);
12988
+ process.stdout.write(` Size: ${size > 1024 ? (size / 1024).toFixed(1) + "KB" : size + "B"}
12989
+ `);
12990
+ process.stdout.write(` Duration: ${duration}ms
12991
+ `);
12992
+ process.stdout.write(` Body Match: ${replay.bodyMatch ? "\x1B[32mYes\x1B[0m" : "\x1B[33mNo\x1B[0m"}
12993
+ `);
12856
12994
  if (status && status >= 400) {
12857
- console.log(` \x1B[33m\u26A0 API may require fresh signature/token\x1B[0m`);
12995
+ process.stdout.write(` \x1B[33m\u26A0 API may require fresh signature/token\x1B[0m
12996
+ `);
12858
12997
  }
12859
12998
  }
12860
- console.log("");
12999
+ process.stdout.write("\n");
12861
13000
  }
12862
13001
  break;
12863
13002
  }
@@ -12876,40 +13015,47 @@ async function handleNetCommand(args, options, mode, sessionName) {
12876
13015
  outputResult(result, mode);
12877
13016
  } else {
12878
13017
  const c = result.capture;
12879
- console.log(`
13018
+ process.stdout.write(`
12880
13019
  Request #${c.id}`);
12881
- console.log(` ${c.method} ${c.url}`);
12882
- console.log(` Status: ${c.status} | Size: ${c.size}B | Type: ${c.contentType}`);
12883
- console.log(` Resource: ${c.resourceType}`);
13020
+ process.stdout.write(` ${c.method} ${c.url}
13021
+ `);
13022
+ process.stdout.write(` Status: ${c.status} | Size: ${c.size}B | Type: ${c.contentType}
13023
+ `);
13024
+ process.stdout.write(` Resource: ${c.resourceType}
13025
+ `);
12884
13026
  if (c.requestHeaders) {
12885
- console.log(`
13027
+ process.stdout.write(`
12886
13028
  Request Headers:`);
12887
13029
  for (const [k, v] of Object.entries(c.requestHeaders)) {
12888
- console.log(` ${k}: ${String(v).substring(0, 100)}`);
13030
+ process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
13031
+ `);
12889
13032
  }
12890
13033
  }
12891
13034
  if (c.requestBody !== void 0) {
12892
- console.log(`
13035
+ process.stdout.write(`
12893
13036
  Request Body:`);
12894
13037
  const bodyStr = typeof c.requestBody === "string" ? c.requestBody : JSON.stringify(c.requestBody, null, 2);
12895
13038
  const lines = bodyStr.split("\n").slice(0, 20);
12896
- for (const line of lines) console.log(` ${line}`);
12897
- if (bodyStr.split("\n").length > 20) console.log(" ...");
13039
+ for (const line of lines) process.stdout.write(` ${line}
13040
+ `);
13041
+ if (bodyStr.split("\n").length > 20) process.stdout.write(" ...");
12898
13042
  }
12899
- console.log(`
13043
+ process.stdout.write(`
12900
13044
  Response Headers:`);
12901
13045
  for (const [k, v] of Object.entries(c.headers)) {
12902
- console.log(` ${k}: ${String(v).substring(0, 100)}`);
13046
+ process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
13047
+ `);
12903
13048
  }
12904
13049
  if (c.body !== void 0) {
12905
- console.log(`
13050
+ process.stdout.write(`
12906
13051
  Response Body:`);
12907
13052
  const bodyStr = typeof c.body === "string" ? c.body : JSON.stringify(c.body, null, 2);
12908
13053
  const lines = bodyStr.split("\n").slice(0, 20);
12909
- for (const line of lines) console.log(` ${line}`);
12910
- if (bodyStr.split("\n").length > 20) console.log(" ...");
13054
+ for (const line of lines) process.stdout.write(` ${line}
13055
+ `);
13056
+ if (bodyStr.split("\n").length > 20) process.stdout.write(" ...");
12911
13057
  }
12912
- console.log("");
13058
+ process.stdout.write("\n");
12913
13059
  }
12914
13060
  break;
12915
13061
  }
@@ -12920,7 +13066,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
12920
13066
  break;
12921
13067
  }
12922
13068
  await forwardNetworkLike(netSession, id);
12923
- console.log(`Marked #${id} as useful`);
13069
+ process.stdout.write(`Marked #${id} as useful
13070
+ `);
12924
13071
  break;
12925
13072
  }
12926
13073
  case "dislike": {
@@ -12930,7 +13077,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
12930
13077
  break;
12931
13078
  }
12932
13079
  await forwardNetworkDislike(netSession, id);
12933
- console.log(`Marked #${id} as not useful`);
13080
+ process.stdout.write(`Marked #${id} as not useful
13081
+ `);
12934
13082
  break;
12935
13083
  }
12936
13084
  case "export": {
@@ -12945,7 +13093,7 @@ async function handleNetCommand(args, options, mode, sessionName) {
12945
13093
  outputError(result.error);
12946
13094
  break;
12947
13095
  }
12948
- console.log(result.code);
13096
+ process.stdout.write(result.code + "\n");
12949
13097
  break;
12950
13098
  }
12951
13099
  default:
@@ -12957,9 +13105,25 @@ async function handleNetCommand(args, options, mode, sessionName) {
12957
13105
  }
12958
13106
 
12959
13107
  // src/cli/test-routes.ts
12960
- import { execSync as execSync3 } from "child_process";
13108
+ import { execFile as execFile3 } from "child_process";
12961
13109
  import { readFileSync as readFileSync11 } from "fs";
12962
13110
  import { resolve as resolve9 } from "path";
13111
+ function execCli(args) {
13112
+ return new Promise((resolvePromise) => {
13113
+ execFile3(
13114
+ "npx",
13115
+ args,
13116
+ { timeout: 65e3, env: { ...process.env, FORCE_COLOR: "0" } },
13117
+ (err, stdout, stderr) => {
13118
+ resolvePromise({
13119
+ stdout: typeof stdout === "string" ? stdout : stdout?.toString("utf-8") ?? "",
13120
+ stderr: typeof stderr === "string" ? stderr : stderr?.toString("utf-8") ?? "",
13121
+ error: err ?? null
13122
+ });
13123
+ }
13124
+ );
13125
+ });
13126
+ }
12963
13127
  function findPluginPath(plugin) {
12964
13128
  const candidates = [
12965
13129
  resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts"),
@@ -13024,20 +13188,21 @@ function extractSchema(plugin, command) {
13024
13188
  }
13025
13189
  async function runTest(plugin, command, cmdArgs, options) {
13026
13190
  const cdp = options.cdp || options.cdpEndpoint || "http://localhost:9221";
13027
- const argsStr = cmdArgs.filter((a) => !a.startsWith("--cdp")).join(" ");
13191
+ const passthroughArgs = cmdArgs.filter((a) => !a.startsWith("--cdp"));
13028
13192
  const schema = extractSchema(plugin, command);
13029
- const fullCmd = `npx xbrowser ${plugin} ${command} ${argsStr} --cdp ${cdp} --json --timeout 60000`;
13030
- let stdout = "";
13031
- try {
13032
- stdout = execSync3(fullCmd, {
13033
- timeout: 65e3,
13034
- encoding: "utf-8",
13035
- stdio: ["pipe", "pipe", "pipe"],
13036
- env: { ...process.env, FORCE_COLOR: "0" }
13037
- });
13038
- } catch (e) {
13039
- const err = e;
13040
- stdout = err.stdout?.toString() || "";
13193
+ const cliArgs = [
13194
+ "xbrowser",
13195
+ plugin,
13196
+ command,
13197
+ ...passthroughArgs,
13198
+ "--cdp",
13199
+ String(cdp),
13200
+ "--json",
13201
+ "--timeout",
13202
+ "60000"
13203
+ ];
13204
+ const { stdout, stderr, error } = await execCli(cliArgs);
13205
+ if (error) {
13041
13206
  const jsonLine = stdout.split("\n").find((l) => {
13042
13207
  try {
13043
13208
  JSON.parse(l);
@@ -13056,11 +13221,10 @@ async function runTest(plugin, command, cmdArgs, options) {
13056
13221
  } catch {
13057
13222
  }
13058
13223
  }
13059
- const stderr = err.stderr?.toString() || "";
13060
13224
  if (stdout.includes("captcha") || stderr.includes("captcha") || stdout.includes("CAPTCHA")) {
13061
13225
  return { status: "CAPTCHA", message: "\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
13062
13226
  }
13063
- return { status: "EXEC_ERROR", message: (err.message || "").slice(0, 200) || "\u6267\u884C\u5931\u8D25" };
13227
+ return { status: "EXEC_ERROR", message: (error.message || "").slice(0, 200) || "\u6267\u884C\u5931\u8D25" };
13064
13228
  }
13065
13229
  const allLines = stdout.split("\n");
13066
13230
  const jsonStart = allLines.findIndex((l) => l.trim().startsWith("{"));
@@ -13130,27 +13294,27 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
13130
13294
  const plugin = cmdArgs[0];
13131
13295
  const command = cmdArgs[1];
13132
13296
  if (!plugin || !command) {
13133
- console.error("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
13134
- console.error("\u793A\u4F8B: xbrowser test doubao list --cdp 9221");
13297
+ outputError("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
13298
+ outputError("\u793A\u4F8B: xbrowser test doubao list --cdp 9221");
13135
13299
  return;
13136
13300
  }
13137
13301
  const loader = await getPluginLoader();
13138
13302
  const internalLoader = loader.getCore().loader;
13139
13303
  const site = internalLoader.getSite(plugin);
13140
13304
  if (!site) {
13141
- console.error(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
13305
+ outputError(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
13142
13306
  return;
13143
13307
  }
13144
13308
  const cmdEntry = site.getCommand(command);
13145
13309
  if (!cmdEntry) {
13146
- console.error(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
13310
+ outputError(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
13147
13311
  return;
13148
13312
  }
13149
13313
  const testArgs = cmdArgs.slice(2);
13150
13314
  const mergedOptions = { ...options, cdp: cdpEndpoint || options.cdp };
13151
13315
  const result = await runTest(plugin, command, testArgs, mergedOptions);
13152
13316
  if (mode === "json") {
13153
- console.log(JSON.stringify(result, null, 2));
13317
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
13154
13318
  return;
13155
13319
  }
13156
13320
  const r = result;
@@ -13165,23 +13329,32 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
13165
13329
  };
13166
13330
  const status = String(r.status);
13167
13331
  const icon = icons[status] || "\u2753";
13168
- console.log(`
13169
- ${icon} ${plugin}.${command}`);
13170
- console.log(` \u72B6\u6001: ${status}`);
13332
+ process.stdout.write(`
13333
+ ${icon} ${plugin}.${command}
13334
+ `);
13335
+ process.stdout.write(` \u72B6\u6001: ${status}
13336
+ `);
13171
13337
  if (status === "OK") {
13172
- if (r.count) console.log(` \u6570\u636E: ${r.count} \u9879`);
13173
- if (r.data) console.log(` \u9884\u89C8: ${String(r.data).slice(0, 150)}`);
13338
+ if (r.count) process.stdout.write(` \u6570\u636E: ${r.count} \u9879
13339
+ `);
13340
+ if (r.data) process.stdout.write(` \u9884\u89C8: ${String(r.data).slice(0, 150)}
13341
+ `);
13174
13342
  } else if (status === "LOGIN_REQUIRED" || status === "CAPTCHA") {
13175
- console.log(` \u4FE1\u606F: ${String(r.message)}`);
13176
- console.log(` Viewer: ${String(r.viewerUrl)}`);
13343
+ process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
13344
+ `);
13345
+ process.stdout.write(` Viewer: ${String(r.viewerUrl)}`);
13177
13346
  } else if (status === "SCHEMA_ERROR") {
13178
13347
  const errs = r.errors;
13179
- if (errs) console.log(` \u9519\u8BEF: ${errs.join("; ")}`);
13348
+ if (errs) process.stdout.write(` \u9519\u8BEF: ${errs.join("; ")}
13349
+ `);
13180
13350
  } else if (["NO_DATA", "BLOCKED"].includes(status)) {
13181
- console.log(` \u4FE1\u606F: ${String(r.message)}`);
13182
- if (r.viewerUrl) console.log(` Viewer: ${String(r.viewerUrl)}`);
13351
+ process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
13352
+ `);
13353
+ if (r.viewerUrl) process.stdout.write(` Viewer: ${String(r.viewerUrl)}
13354
+ `);
13183
13355
  } else {
13184
- console.log(` \u4FE1\u606F: ${String(r.message)}`);
13356
+ process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
13357
+ `);
13185
13358
  }
13186
13359
  }
13187
13360
 
@@ -13279,7 +13452,7 @@ Commands:
13279
13452
  plugin list List plugins
13280
13453
  plugin reload <name> Reload plugin
13281
13454
  create <name> --template <type> Create plugin
13282
- serve [--port <port>] [--token <t>] Start HTTP server
13455
+ serve [--port <p>] [--host <h>] [--token <t>] [--cors-origins <csv>] Start HTTP server (loopback by default)
13283
13456
  remote <url> [command] [--token <t>] Execute on remote server
13284
13457
  run <file> Execute commands from file
13285
13458
  viewer [--name <n>] Generate viewer URL
@@ -13400,10 +13573,26 @@ function validateAuth(authHeader, validTokens) {
13400
13573
  function isAuthRequired(validTokens) {
13401
13574
  return validTokens.length > 0;
13402
13575
  }
13576
+ var IPV6_LOOPBACK = /* @__PURE__ */ new Set(["::1", "0:0:0:0:0:0:0:1"]);
13577
+ function isLoopbackHost(host) {
13578
+ const h = host.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "");
13579
+ if (h === "localhost") return true;
13580
+ if (IPV6_LOOPBACK.has(h)) return true;
13581
+ const ipv4 = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
13582
+ return ipv4 !== null && ipv4[1] === "127";
13583
+ }
13584
+ function isLoopbackOrigin(origin) {
13585
+ try {
13586
+ const url = new URL(origin);
13587
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
13588
+ return isLoopbackHost(url.hostname);
13589
+ } catch {
13590
+ return false;
13591
+ }
13592
+ }
13403
13593
 
13404
13594
  // src/server/router.ts
13405
- var CORS_HEADERS = {
13406
- "Access-Control-Allow-Origin": "*",
13595
+ var CORS_BASE_HEADERS = {
13407
13596
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
13408
13597
  "Access-Control-Allow-Headers": "Content-Type, Authorization"
13409
13598
  };
@@ -13456,9 +13645,31 @@ function jsonResponse(statusCode, body, extraHeaders) {
13456
13645
  return {
13457
13646
  statusCode,
13458
13647
  body,
13459
- headers: { ...CORS_HEADERS, "Content-Type": "application/json", ...extraHeaders }
13648
+ headers: { "Content-Type": "application/json", ...extraHeaders }
13649
+ };
13650
+ }
13651
+ function corsHeadersFor(origin, explicitOrigins) {
13652
+ if (!origin || !isOriginAllowed(origin, explicitOrigins)) return {};
13653
+ return {
13654
+ "Access-Control-Allow-Origin": origin,
13655
+ Vary: "Origin",
13656
+ ...CORS_BASE_HEADERS
13460
13657
  };
13461
13658
  }
13659
+ function isOriginAllowed(origin, explicitOrigins) {
13660
+ if (explicitOrigins && explicitOrigins.length > 0) {
13661
+ return explicitOrigins.includes("*") || explicitOrigins.includes(origin);
13662
+ }
13663
+ return isLoopbackOrigin(origin);
13664
+ }
13665
+ function isJsonContentType(contentType) {
13666
+ return contentType.toLowerCase().split(";")[0].trim() === "application/json";
13667
+ }
13668
+ function applyCorsHeaders(response, origin, corsOrigins) {
13669
+ const cors = corsHeadersFor(origin, corsOrigins);
13670
+ if (Object.keys(cors).length === 0) return response;
13671
+ return { ...response, headers: { ...cors, ...response.headers ?? {} } };
13672
+ }
13462
13673
  function errorResponse(statusCode, error, message) {
13463
13674
  return jsonResponse(statusCode, { error, message, statusCode });
13464
13675
  }
@@ -13576,13 +13787,30 @@ async function ensureSession(sessionName, url, cdpEndpoint) {
13576
13787
  function isHealthCheckPath(pathname) {
13577
13788
  return pathname === "/api/v1/health";
13578
13789
  }
13579
- async function route(method, url, headers, body) {
13790
+ async function route(method, url, headers, body, corsOrigins) {
13791
+ const response = await routeInner(method, url, headers, body, corsOrigins);
13792
+ return applyCorsHeaders(response, headers.origin, corsOrigins);
13793
+ }
13794
+ async function routeInner(method, url, headers, body, corsOrigins) {
13580
13795
  const parsedUrl = new URL(url, "http://localhost");
13581
13796
  const pathname = parsedUrl.pathname;
13582
13797
  const query = parseQueryString(parsedUrl.search);
13583
13798
  if (method === "OPTIONS") {
13584
13799
  return jsonResponse(204, null);
13585
13800
  }
13801
+ if (headers.origin && !isOriginAllowed(headers.origin, corsOrigins)) {
13802
+ return errorResponse(403, "FORBIDDEN", `Origin ${headers.origin} is not allowed to call this API`);
13803
+ }
13804
+ if (method === "POST" || method === "PUT") {
13805
+ const contentType = headers["content-type"];
13806
+ if (contentType && !isJsonContentType(contentType)) {
13807
+ return errorResponse(
13808
+ 415,
13809
+ "UNSUPPORTED_MEDIA_TYPE",
13810
+ `Content-Type must be application/json, got "${contentType}"`
13811
+ );
13812
+ }
13813
+ }
13586
13814
  const match = matchRoute(method, pathname);
13587
13815
  if (!match) {
13588
13816
  const pathMatch = matchRoute("GET", pathname) || matchRoute("POST", pathname) || matchRoute("DELETE", pathname);
@@ -13605,19 +13833,24 @@ async function route(method, url, headers, body) {
13605
13833
  return errorResponse(500, "INTERNAL_ERROR", errMsg(err));
13606
13834
  }
13607
13835
  }
13608
- async function handleRequest(req, res, validateAuthFn) {
13836
+ async function handleRequest(req, res, validateAuthFn, corsOrigins) {
13609
13837
  const url = req.url || "/";
13610
13838
  const method = (req.method || "GET").toUpperCase();
13611
13839
  const pathname = new URL(url, "http://localhost").pathname;
13612
13840
  if (method === "OPTIONS") {
13613
- const response2 = await route(method, url, headersToObject(req.headers), null);
13841
+ const response2 = await route(method, url, headersToObject(req.headers), null, corsOrigins);
13614
13842
  writeResponse(res, response2);
13615
13843
  return;
13616
13844
  }
13617
13845
  if (validateAuthFn && !isHealthCheckPath(pathname)) {
13618
13846
  const authHeader = req.headers["authorization"];
13619
13847
  if (!validateAuthFn(authHeader)) {
13620
- writeResponse(res, errorResponse(401, "UNAUTHORIZED", "Invalid or missing authentication token"));
13848
+ const denied = applyCorsHeaders(
13849
+ errorResponse(401, "UNAUTHORIZED", "Invalid or missing authentication token"),
13850
+ req.headers.origin,
13851
+ corsOrigins
13852
+ );
13853
+ writeResponse(res, denied);
13621
13854
  return;
13622
13855
  }
13623
13856
  }
@@ -13625,7 +13858,7 @@ async function handleRequest(req, res, validateAuthFn) {
13625
13858
  if (method === "POST" || method === "PUT") {
13626
13859
  body = await readBody(req);
13627
13860
  }
13628
- const response = await route(method, url, headersToObject(req.headers), body);
13861
+ const response = await route(method, url, headersToObject(req.headers), body, corsOrigins);
13629
13862
  writeResponse(res, response);
13630
13863
  }
13631
13864
  function headersToObject(headers) {
@@ -13666,26 +13899,35 @@ var HTTPServer = class {
13666
13899
  host;
13667
13900
  server = null;
13668
13901
  validTokens;
13902
+ corsOrigins;
13669
13903
  constructor(config) {
13670
13904
  this.port = config?.port ?? 9224;
13671
- this.host = config?.host ?? "0.0.0.0";
13905
+ this.host = config?.host ?? "127.0.0.1";
13672
13906
  this.validTokens = resolveTokens(config?.tokens);
13907
+ this.corsOrigins = config?.corsOrigins;
13673
13908
  }
13674
13909
  /**
13675
13910
  * Start the HTTP server and begin listening for requests.
13676
13911
  *
13677
13912
  * @returns The actual port and host the server bound to.
13678
- * @throws If the server is already running or fails to start.
13913
+ * @throws If the server is already running, if binding a non-loopback
13914
+ * host without an auth token, or if the port cannot be bound.
13679
13915
  */
13680
13916
  async start() {
13681
13917
  if (this.server) {
13682
13918
  throw new Error("HTTP server is already running");
13683
13919
  }
13920
+ if (!isLoopbackHost(this.host) && !isAuthRequired(this.validTokens)) {
13921
+ throw new Error(
13922
+ `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.`
13923
+ );
13924
+ }
13684
13925
  const authRequired = isAuthRequired(this.validTokens);
13685
13926
  const tokens = this.validTokens;
13927
+ const corsOrigins = this.corsOrigins;
13686
13928
  this.server = createServer((req, res) => {
13687
13929
  const authFn = authRequired ? (authHeader) => validateAuth(authHeader, tokens) : void 0;
13688
- handleRequest(req, res, authFn).catch((err) => {
13930
+ handleRequest(req, res, authFn, corsOrigins).catch((err) => {
13689
13931
  const message = err instanceof Error ? err.message : String(err);
13690
13932
  if (!res.headersSent) {
13691
13933
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -14156,7 +14398,7 @@ async function routeCommand(argvIn, stdinCommands) {
14156
14398
  replay: "replay <file> [--slow-mo <ms>] [--stop-on-error]",
14157
14399
  create: "create <name> [--template static|dynamic|login|api]",
14158
14400
  run: "run <file>",
14159
- serve: "serve [--port <port>] [--token <token>]",
14401
+ serve: "serve [--port <port>] [--host <host>] [--token <token>] [--cors-origins <csv>]",
14160
14402
  remote: "remote <url> [command] [--token <token>]",
14161
14403
  convert: "convert <file> [--to js|py|sh]",
14162
14404
  extract: "extract <file> [--format json|yaml]",
@@ -14433,7 +14675,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
14433
14675
  const targetPage = pages[cmdTabIndex];
14434
14676
  await targetPage.bringToFront().catch(() => {
14435
14677
  });
14436
- const { setActivePage: setActivePage2 } = await import("./browser-CNBSH53B.js");
14678
+ const { setActivePage: setActivePage2 } = await import("./browser-BVRFSUXU.js");
14437
14679
  setActivePage2(session, targetPage);
14438
14680
  }
14439
14681
  }
@@ -14555,7 +14797,10 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
14555
14797
  async function handleServe(_args, options, mode) {
14556
14798
  const port = options.port ? Number(options.port) : void 0;
14557
14799
  const token = options.token;
14558
- const httpServer = new HTTPServer({ port, tokens: token ? [token] : void 0 });
14800
+ const host = options.host;
14801
+ const corsOriginsRaw = options["cors-origins"];
14802
+ const corsOrigins = corsOriginsRaw ? corsOriginsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
14803
+ const httpServer = new HTTPServer({ port, host, tokens: token ? [token] : void 0, corsOrigins });
14559
14804
  process.on("SIGINT", async () => {
14560
14805
  await httpServer.stop();
14561
14806
  return;
@@ -14709,7 +14954,7 @@ async function main() {
14709
14954
  const command = process.argv[2];
14710
14955
  const isLongRunning = command === "preview" || command === "serve";
14711
14956
  if (!isLongRunning) {
14712
- const { ensureProcessCanExit } = await import("./browser-CNBSH53B.js");
14957
+ const { ensureProcessCanExit } = await import("./browser-BVRFSUXU.js");
14713
14958
  await ensureProcessCanExit().catch(() => {
14714
14959
  });
14715
14960
  process.exit(process.exitCode || exitCode);