@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/dist/cli.js CHANGED
@@ -7172,7 +7172,7 @@ import { join as join7 } from "path";
7172
7172
  import { execSync } from "child_process";
7173
7173
  var SHARED_PLUGIN_DEPENDENCIES = {
7174
7174
  "zod": "^3.24.0",
7175
- "@dyyz1993/xcli-core": "^0.12.1"
7175
+ "@dyyz1993/xcli-core": "^0.19.0"
7176
7176
  };
7177
7177
  function ensurePluginDependencies(pluginsDir) {
7178
7178
  const zodPath = join7(pluginsDir, "node_modules", "zod");
@@ -8467,7 +8467,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
8467
8467
  const errorMessage = errMsg(err);
8468
8468
  if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
8469
8469
  try {
8470
- const { attemptRecovery } = await import("./recovery-NXC35EQN.js");
8470
+ const { attemptRecovery } = await import("./recovery-6RI2FE3E.js");
8471
8471
  const recovery = await attemptRecovery(
8472
8472
  session.page,
8473
8473
  sessionName,
@@ -9149,13 +9149,39 @@ async function installFromNpm(packageName, name, targetDir) {
9149
9149
  import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
9150
9150
  import { resolve as resolve5, join as join10 } from "path";
9151
9151
  import { tmpdir as tmpdir2 } from "os";
9152
- import { execSync as execSync2 } from "child_process";
9152
+ import { execFile as execFile2 } from "child_process";
9153
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
+ }
9154
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
+ }
9155
9181
  const tmpDir = join10(tmpdir2(), `xbrowser-git-${Date.now()}`);
9156
9182
  let warnings = [];
9157
9183
  try {
9158
- execSync2(`git clone --depth 1 "${gitUrl}" "${tmpDir}"`, { stdio: "pipe" });
9184
+ await execGit(["clone", "--depth", "1", "--", gitUrl, tmpDir]);
9159
9185
  const verify = verifyPlugin3(tmpDir, { metadataField: "xbrowser" });
9160
9186
  warnings = verify.warnings ?? [];
9161
9187
  if (!verify.valid) {
@@ -10990,7 +11016,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
10990
11016
  }
10991
11017
  outputResult({ command: cmdDef.name, description: cmdDef.description, scope: cmdDef.scope, parameters: paramsList }, mode);
10992
11018
  } else {
10993
- console.log(helpGenerator.generate(cmdDef, { color: true, emoji: false }));
11019
+ process.stdout.write(helpGenerator.generate(cmdDef, { color: true, emoji: false }));
10994
11020
  }
10995
11021
  } else {
10996
11022
  outputError(`Unknown command: ${command}`);
@@ -11391,7 +11417,7 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
11391
11417
  if (isEmptyResult) {
11392
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";
11393
11419
  outputResult(result.data, mode);
11394
- console.error(`
11420
+ outputError(`
11395
11421
  \u26A0\uFE0F ${hint}`);
11396
11422
  process.exit(1);
11397
11423
  } else {
@@ -11408,8 +11434,9 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
11408
11434
  try {
11409
11435
  mkdirSync10(dirname6(outputFile), { recursive: true });
11410
11436
  writeFileSync12(outputFile, content, "utf-8");
11411
- console.log(`
11412
- \u{1F4C4} Written to ${outputFile}`);
11437
+ process.stdout.write(`
11438
+ \u{1F4C4} Written to ${outputFile}
11439
+ `);
11413
11440
  } catch (err) {
11414
11441
  outputError(`Failed to write --output "${outputFile}": ${err instanceof Error ? err.message : String(err)}`);
11415
11442
  }
@@ -11507,7 +11534,7 @@ async function handleSession(args, options, mode, _cdpEndpoint) {
11507
11534
  break;
11508
11535
  }
11509
11536
  default:
11510
- console.log(handleSessionHelp());
11537
+ process.stdout.write(handleSessionHelp() + "\n");
11511
11538
  }
11512
11539
  }
11513
11540
 
@@ -11681,19 +11708,24 @@ async function handleSearch(args, options, mode) {
11681
11708
  outputEnvelope({ success: true, data: { results: deduped, total: deduped.length } }, { command: "plugin search" }, mode);
11682
11709
  } else {
11683
11710
  if (deduped.length === 0) {
11684
- console.log("No plugins found");
11711
+ process.stdout.write("No plugins found\n");
11685
11712
  return;
11686
11713
  }
11687
11714
  for (const r of deduped) {
11688
11715
  const src = r.source === "marketplace" ? "[marketplace]" : r.source === "local" ? "[local]" : "[npm]";
11689
11716
  const slug = r.slug ? ` (${r.slug})` : "";
11690
- console.log(` ${src} ${r.name}${slug}`);
11691
- if (r.description) console.log(` ${r.description}`);
11692
- if (r.version) console.log(` Version: ${r.version}`);
11693
- if (r.downloads) console.log(` Downloads: ${r.downloads}`);
11694
- 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");
11695
11726
  }
11696
- console.log(`Total: ${deduped.length} plugins`);
11727
+ process.stdout.write(`Total: ${deduped.length} plugins
11728
+ `);
11697
11729
  }
11698
11730
  }
11699
11731
  async function handlePluginInfo(args, options, mode) {
@@ -11710,14 +11742,22 @@ async function handlePluginInfo(args, options, mode) {
11710
11742
  outputEnvelope({ success: true, data: { source: "marketplace", ...d } }, { command: "plugin info" }, mode);
11711
11743
  return;
11712
11744
  }
11713
- console.log(`\u540D\u79F0: ${d.name || ""}`);
11714
- console.log(`\u7248\u672C: ${d.version || ""}`);
11715
- console.log(`\u63CF\u8FF0: ${d.description || ""}`);
11716
- console.log(`\u4F5C\u8005: ${d.author || ""}`);
11717
- console.log(`\u547D\u4EE4: ${(d.commands || []).join(", ")}`);
11718
- console.log(`\u4E0B\u8F7D\u91CF: ${d.downloads || 0}`);
11719
- console.log(`\u6807\u7B7E: ${(d.tags || []).join(", ")}`);
11720
- 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
+ `);
11721
11761
  return;
11722
11762
  }
11723
11763
  } catch {
@@ -11736,19 +11776,25 @@ async function handlePluginInfo(args, options, mode) {
11736
11776
  outputEnvelope({ success: true, data: { source: "npm", name: pkg.name, version: latest, description: pkg.description } }, { command: "plugin info" }, mode);
11737
11777
  return;
11738
11778
  }
11739
- console.log(`\u540D\u79F0: ${pkg.name || ""}`);
11740
- console.log(`\u7248\u672C: ${latest}`);
11741
- 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
+ `);
11742
11785
  const author = pkg.author;
11743
- console.log(`\u4F5C\u8005: ${typeof author === "string" ? author : author?.name || ""}`);
11744
- console.log(`\u5173\u952E\u8BCD: ${(pkg.keywords || []).join(", ")}`);
11745
- 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
+ `);
11746
11792
  return;
11747
11793
  }
11748
11794
  }
11749
- console.error(`\u63D2\u4EF6 '${slug}' \u672A\u627E\u5230`);
11795
+ outputError(`\u63D2\u4EF6 '${slug}' \u672A\u627E\u5230`);
11750
11796
  } catch (err) {
11751
- console.error("\u67E5\u8BE2\u5931\u8D25:", errMsg(err));
11797
+ outputError("\u67E5\u8BE2\u5931\u8D25: " + errMsg(err));
11752
11798
  }
11753
11799
  }
11754
11800
  async function handlePluginSchema(args, mode) {
@@ -11772,32 +11818,39 @@ async function handlePluginSchema(args, mode) {
11772
11818
  }
11773
11819
  }
11774
11820
  function printPluginContract(contract) {
11775
- console.log(`${contract.plugin.name} contract v${contract.version}`);
11776
- if (contract.plugin.description) console.log(contract.plugin.description);
11777
- 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");
11778
11825
  for (const command of contract.commands) {
11779
11826
  printCommandContract(contract.plugin.name, command);
11780
11827
  }
11781
11828
  }
11782
11829
  function printCommandContract(pluginName, command) {
11783
- console.log(`${pluginName} ${command.name}`);
11784
- if (command.description) console.log(` ${command.description}`);
11785
- 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
+ `);
11786
11836
  if (command.capabilities.length > 0) {
11787
- console.log(` capabilities: ${command.capabilities.join(", ")}`);
11837
+ process.stdout.write(` capabilities: ${command.capabilities.join(", ")}
11838
+ `);
11788
11839
  }
11789
11840
  if (command.positional.length > 0) {
11790
- console.log(` positional: ${command.positional.join(", ")}`);
11841
+ process.stdout.write(` positional: ${command.positional.join(", ")}
11842
+ `);
11791
11843
  }
11792
11844
  if (command.form.fields.length > 0) {
11793
- console.log(" fields:");
11845
+ process.stdout.write(" fields:\n");
11794
11846
  for (const field of command.form.fields) {
11795
11847
  const required = field.required ? "required" : "optional";
11796
11848
  const choices = field.enum ? ` [${field.enum.join("|")}]` : "";
11797
- console.log(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}`);
11849
+ process.stdout.write(` --${field.name}: ${field.type}/${field.widget} ${required}${choices}
11850
+ `);
11798
11851
  }
11799
11852
  }
11800
- console.log("");
11853
+ process.stdout.write("\n");
11801
11854
  }
11802
11855
  async function handlePlugin(args, options, mode) {
11803
11856
  const sub = args[0];
@@ -11886,24 +11939,28 @@ async function handlePlugin(args, options, mode) {
11886
11939
  outputEnvelope({ success: true, data: { plugins: enrichedPlugins } }, { command: "plugin list" }, mode);
11887
11940
  } else {
11888
11941
  if (enrichedPlugins.length === 0) {
11889
- console.log("No plugins installed");
11942
+ process.stdout.write("No plugins installed\n");
11890
11943
  return;
11891
11944
  }
11892
11945
  for (const p of enrichedPlugins) {
11893
11946
  const loginTag = p.hasLogin ? p.loggedIn ? " [logged in]" : " [need login]" : "";
11894
11947
  if (p.version && p.description) {
11895
- console.log(`${p.name} (${p.version}) - ${p.description}${loginTag}`);
11948
+ process.stdout.write(`${p.name} (${p.version}) - ${p.description}${loginTag}
11949
+ `);
11896
11950
  } else {
11897
- console.log(`${p.name}${loginTag}`);
11951
+ process.stdout.write(`${p.name}${loginTag}
11952
+ `);
11898
11953
  }
11899
11954
  if (p.commands && p.commands.length > 0) {
11900
- console.log(` ${p.commands.join(", ")}`);
11955
+ process.stdout.write(` ${p.commands.join(", ")}
11956
+ `);
11901
11957
  }
11902
11958
  if (p.requiresLoginCommands.length > 0) {
11903
- console.log(` requires login: ${p.requiresLoginCommands.join(", ")}`);
11959
+ process.stdout.write(` requires login: ${p.requiresLoginCommands.join(", ")}
11960
+ `);
11904
11961
  }
11905
11962
  }
11906
- console.log(`
11963
+ process.stdout.write(`
11907
11964
  Total: ${enrichedPlugins.length} plugins`);
11908
11965
  }
11909
11966
  break;
@@ -11940,7 +11997,7 @@ See docs/plugin-guide.md for the publishing workflow.`
11940
11997
  );
11941
11998
  break;
11942
11999
  default:
11943
- console.log(handlePluginHelp());
12000
+ process.stdout.write(handlePluginHelp() + "\n");
11944
12001
  }
11945
12002
  }
11946
12003
  function handleCreate(args, options) {
@@ -11974,7 +12031,7 @@ function handleDaemon(args, options, mode) {
11974
12031
  break;
11975
12032
  }
11976
12033
  default:
11977
- console.log("Daemon starts automatically. No manual action needed.");
12034
+ process.stdout.write("Daemon starts automatically. No manual action needed.\n");
11978
12035
  }
11979
12036
  }
11980
12037
 
@@ -12043,8 +12100,8 @@ async function handleRecord(args, options, mode) {
12043
12100
  }, mode);
12044
12101
  const md = SessionRecorder.readMarkdownSummary(sessionName);
12045
12102
  if (md) {
12046
- console.log("");
12047
- console.log(md);
12103
+ process.stdout.write("\n");
12104
+ process.stdout.write(md + "\n");
12048
12105
  } else {
12049
12106
  const summary = SessionRecorder.readSummary(sessionName);
12050
12107
  if (summary) {
@@ -12103,25 +12160,30 @@ async function handleRecord(args, options, mode) {
12103
12160
  break;
12104
12161
  }
12105
12162
  default:
12106
- console.log("Usage:");
12107
- console.log(" xbrowser record start [--url <url>] [--session <name>]");
12108
- console.log(" xbrowser record stop [--session <name>]");
12109
- console.log(" xbrowser record status [--session <name>]");
12110
- console.log(" xbrowser record summary [--session <name>] [--json]");
12111
- console.log(' xbrowser record checkpoint --type <type> --hint "description" [--selector <sel>] [--session <name>]');
12112
- console.log(" xbrowser record generate-plugin [--session <name>] [--name <plugin>] [--output <dir>]");
12113
- console.log("");
12114
- 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");
12115
12172
  }
12116
12173
  }
12117
12174
  function printRecordingSummary(summary, sessionName) {
12118
- console.log("");
12119
- console.log("=== Recording Summary ===");
12120
- console.log(` Start URL: ${summary.startUrl}`);
12121
- console.log(` Duration: ${Math.round(summary.durationMs / 1e3)}s`);
12122
- console.log(` Actions: ${summary.totalActions}`);
12123
- console.log(` Network: ${summary.totalNetworkRequests}`);
12124
- 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
+ `);
12125
12187
  for (const step of summary.steps) {
12126
12188
  const a = step.action;
12127
12189
  const el = a.element;
@@ -12143,20 +12205,24 @@ function printRecordingSummary(summary, sessionName) {
12143
12205
  }
12144
12206
  const navInfo = step.contextChanges.find((c) => c.type === "navigate");
12145
12207
  if (navInfo) desc += ` \u2192 navigate to ${navInfo.url?.substring(0, 80)}`;
12146
- console.log(` ${step.step}. ${desc}`);
12208
+ process.stdout.write(` ${step.step}. ${desc}
12209
+ `);
12147
12210
  if (a.clickContext) {
12148
12211
  const ctx = a.clickContext;
12149
12212
  if (ctx.appeared?.length > 0) {
12150
12213
  for (const popup of ctx.appeared) {
12151
12214
  const roleStr = popup.role ? ` [${popup.role}]` : "";
12152
- 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
+ `);
12153
12217
  if (popup.items?.length > 0) {
12154
12218
  for (const item of popup.items.slice(0, 10)) {
12155
12219
  const disStr = item.disabled ? " [disabled]" : "";
12156
- console.log(` \u2022 ${item.text}${disStr}`);
12220
+ process.stdout.write(` \u2022 ${item.text}${disStr}
12221
+ `);
12157
12222
  }
12158
12223
  if (popup.items.length > 10) {
12159
- console.log(` ... and ${popup.items.length - 10} more items`);
12224
+ process.stdout.write(` ... and ${popup.items.length - 10} more items
12225
+ `);
12160
12226
  }
12161
12227
  }
12162
12228
  }
@@ -12169,31 +12235,41 @@ function printRecordingSummary(summary, sessionName) {
12169
12235
  if (sc.ariaSelected !== void 0) parts.push(`selected=${sc.ariaSelected}`);
12170
12236
  if (sc.dataState) parts.push(`state=${sc.dataState}`);
12171
12237
  if (parts.length > 0) {
12172
- 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
+ `);
12173
12240
  }
12174
12241
  }
12175
12242
  }
12176
12243
  }
12177
12244
  }
12178
- console.log("");
12179
- console.log(` Files: ${SessionRecorder.getRecordingsDir(sessionName)}/`);
12245
+ process.stdout.write("\n");
12246
+ process.stdout.write(` Files: ${SessionRecorder.getRecordingsDir(sessionName)}/
12247
+ `);
12180
12248
  if (summary.checkpoints && summary.checkpoints.length > 0) {
12181
- console.log("");
12182
- console.log(` Checkpoints (${summary.checkpoints.length}):`);
12249
+ process.stdout.write("\n");
12250
+ process.stdout.write(` Checkpoints (${summary.checkpoints.length}):
12251
+ `);
12183
12252
  for (const cp of summary.checkpoints) {
12184
12253
  const src = cp.source === "auto" ? "[auto]" : "[manual]";
12185
- console.log(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}`);
12186
- 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
+ `);
12187
12258
  }
12188
12259
  }
12189
12260
  }
12190
12261
  function printHumanReadableSummary(summary) {
12191
- console.log(`Start URL: ${summary.startUrl}`);
12192
- console.log(`Recorded: ${summary.recordedAt}`);
12193
- console.log(`Duration: ${Math.round(summary.durationMs / 1e3)}s`);
12194
- console.log(`Actions: ${summary.totalActions}`);
12195
- console.log(`Network: ${summary.totalNetworkRequests}`);
12196
- 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");
12197
12273
  for (const step of summary.steps) {
12198
12274
  const a = step.action;
12199
12275
  const el = a.element;
@@ -12209,20 +12285,23 @@ function printHumanReadableSummary(summary) {
12209
12285
  if (a.value) parts.push(`value="${a.value.substring(0, 50)}"`);
12210
12286
  if (a.key) parts.push(`key=${a.key}`);
12211
12287
  if (a.x !== void 0 && a.y !== void 0) parts.push(`@(${a.x},${a.y})`);
12212
- console.log(parts.join(" "));
12288
+ process.stdout.write(parts.join(" ") + "\n");
12213
12289
  if (a.clickContext) {
12214
12290
  const ctx = a.clickContext;
12215
12291
  if (ctx.appeared?.length > 0) {
12216
12292
  for (const popup of ctx.appeared) {
12217
12293
  const roleStr = popup.role ? ` [${popup.role}]` : "";
12218
- 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
+ `);
12219
12296
  if (popup.items?.length > 0) {
12220
12297
  for (const item of popup.items.slice(0, 10)) {
12221
12298
  const disStr = item.disabled ? " [disabled]" : "";
12222
- console.log(` \u2022 ${item.text}${disStr}`);
12299
+ process.stdout.write(` \u2022 ${item.text}${disStr}
12300
+ `);
12223
12301
  }
12224
12302
  if (popup.items.length > 10) {
12225
- console.log(` ... and ${popup.items.length - 10} more items`);
12303
+ process.stdout.write(` ... and ${popup.items.length - 10} more items
12304
+ `);
12226
12305
  }
12227
12306
  }
12228
12307
  }
@@ -12235,40 +12314,50 @@ function printHumanReadableSummary(summary) {
12235
12314
  if (sc.ariaSelected !== void 0) stateParts.push(`selected=${sc.ariaSelected}`);
12236
12315
  if (sc.dataState) stateParts.push(`state=${sc.dataState}`);
12237
12316
  if (stateParts.length > 0) {
12238
- 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
+ `);
12239
12319
  }
12240
12320
  }
12241
12321
  }
12242
12322
  }
12243
12323
  for (const net of step.network) {
12244
- 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
+ `);
12245
12326
  if (net.requestBody && typeof net.requestBody === "object") {
12246
12327
  const bodyStr = JSON.stringify(net.requestBody);
12247
12328
  if (bodyStr.length <= 200) {
12248
- console.log(` body: ${bodyStr}`);
12329
+ process.stdout.write(` body: ${bodyStr}
12330
+ `);
12249
12331
  } else {
12250
- console.log(` body: ${bodyStr.substring(0, 200)}... (${bodyStr.length} bytes)`);
12332
+ process.stdout.write(` body: ${bodyStr.substring(0, 200)}... (${bodyStr.length} bytes)
12333
+ `);
12251
12334
  }
12252
12335
  }
12253
12336
  }
12254
12337
  for (const match of step.matchedInputs) {
12255
- 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
+ `);
12256
12340
  }
12257
12341
  for (const ctx of step.contextChanges) {
12258
12342
  if (ctx.type === "navigate") {
12259
- console.log(` \u2197 navigate \u2192 ${ctx.url}`);
12343
+ process.stdout.write(` \u2197 navigate \u2192 ${ctx.url}
12344
+ `);
12260
12345
  } else if (ctx.type === "new_tab") {
12261
- console.log(` \u2197 new tab: ${ctx.url}`);
12346
+ process.stdout.write(` \u2197 new tab: ${ctx.url}
12347
+ `);
12262
12348
  }
12263
12349
  }
12264
12350
  }
12265
12351
  if (summary.checkpoints && summary.checkpoints.length > 0) {
12266
- console.log("");
12267
- console.log(`Checkpoints (${summary.checkpoints.length}):`);
12352
+ process.stdout.write("\n");
12353
+ process.stdout.write(`Checkpoints (${summary.checkpoints.length}):
12354
+ `);
12268
12355
  for (const cp of summary.checkpoints) {
12269
12356
  const src = cp.source === "auto" ? "[auto]" : "[manual]";
12270
- console.log(` ${cp.id}. ${src} [${cp.type}] ${cp.hint}`);
12271
- 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
+ `);
12272
12361
  }
12273
12362
  }
12274
12363
  }
@@ -12297,10 +12386,12 @@ async function handleReplay(args, options, mode) {
12297
12386
  if (result.ok && mode !== "json" && mode !== "yaml") {
12298
12387
  const healed = typeof result.healed === "number" ? result.healed : 0;
12299
12388
  if (healed > 0) {
12300
- console.log(`
12301
- Self-healed ${healed} action(s):`);
12389
+ process.stdout.write(`
12390
+ Self-healed ${healed} action(s):
12391
+ `);
12302
12392
  for (const d of result.healedDetails ?? []) {
12303
- console.log(` - step ${d.index + 1}: ${d.strategy}`);
12393
+ process.stdout.write(` - step ${d.index + 1}: ${d.strategy}
12394
+ `);
12304
12395
  }
12305
12396
  }
12306
12397
  }
@@ -12310,7 +12401,7 @@ async function handleConvert(args, _mode) {
12310
12401
  const filePath = args[0];
12311
12402
  const outputPath = args[1];
12312
12403
  if (!filePath || !outputPath) {
12313
- console.error("Usage: xbrowser convert <recording.yaml> <output.{js,py,sh}>");
12404
+ outputError("Usage: xbrowser convert <recording.yaml> <output.{js,py,sh}>");
12314
12405
  process.exit(1);
12315
12406
  }
12316
12407
  const fs3 = await import("fs");
@@ -12322,11 +12413,11 @@ async function handleConvert(args, _mode) {
12322
12413
  const content = fs3.readFileSync(filePath, "utf-8");
12323
12414
  recording = yaml.parse(content);
12324
12415
  } catch (e) {
12325
- 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)}`);
12326
12417
  process.exit(1);
12327
12418
  }
12328
12419
  if (recording === null || typeof recording !== "object" || Array.isArray(recording)) {
12329
- 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).`);
12330
12421
  process.exit(1);
12331
12422
  }
12332
12423
  const rawActions = recording.actions;
@@ -12360,24 +12451,28 @@ async function handleConvert(args, _mode) {
12360
12451
  fs3.writeFileSync(outputPath, script);
12361
12452
  fs3.chmodSync(outputPath, 493);
12362
12453
  const eventCount = (recordingTyped.events || []).length;
12363
- console.log(`Converted ${filePath} -> ${outputPath}`);
12364
- console.log(` Events: ${eventCount}, Start URL: ${recordingTyped.startUrl}`);
12365
- 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
+ `);
12366
12460
  }
12367
12461
  async function handleExtract(args, _mode) {
12368
12462
  const filePath = args[0];
12369
12463
  if (!filePath) {
12370
- console.error("Usage: xbrowser extract <recording.yaml>");
12464
+ outputError("Usage: xbrowser extract <recording.yaml>");
12371
12465
  process.exit(1);
12372
12466
  }
12373
12467
  const { extractAndSave, printExtractSummary } = await import("./extract-EUWPRSKH.js");
12374
12468
  try {
12375
12469
  const { summary, outputPath } = extractAndSave(filePath);
12376
12470
  printExtractSummary(summary);
12377
- console.log(`
12378
- Saved LLM summary: ${outputPath}`);
12471
+ process.stdout.write(`
12472
+ Saved LLM summary: ${outputPath}
12473
+ `);
12379
12474
  } catch (e) {
12380
- console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
12475
+ outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
12381
12476
  process.exit(1);
12382
12477
  }
12383
12478
  }
@@ -12385,7 +12480,7 @@ async function handleFilter(args, _mode, options) {
12385
12480
  const filePath = args[0];
12386
12481
  const outputPath = args[1];
12387
12482
  if (!filePath || !outputPath) {
12388
- console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude type1,type2]");
12483
+ outputError("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude type1,type2]");
12389
12484
  process.exit(1);
12390
12485
  }
12391
12486
  const { filterRecording, parseExcludeTypes } = await import("./filter-7YOPVPVC.js");
@@ -12397,10 +12492,12 @@ async function handleFilter(args, _mode, options) {
12397
12492
  const excludeTypes = parseExcludeTypes(excludeArgs);
12398
12493
  try {
12399
12494
  const result = filterRecording(filePath, outputPath, excludeTypes);
12400
- console.log(`Filtered ${filePath} -> ${outputPath}`);
12401
- 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
+ `);
12402
12499
  } catch (e) {
12403
- console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
12500
+ outputError(`Error: ${e instanceof Error ? e.message : String(e)}`);
12404
12501
  process.exit(1);
12405
12502
  }
12406
12503
  }
@@ -12429,21 +12526,30 @@ async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
12429
12526
  if (knowledgeMd) {
12430
12527
  writeFileSync12(join15(finalOutputDir, "SITE_KNOWLEDGE.md"), knowledgeMd, "utf-8");
12431
12528
  }
12432
- console.log("");
12433
- console.log("=== Plugin Generated ===");
12434
- console.log(` Plugin: ${finalPluginName}`);
12435
- console.log(` Domain: ${domain}`);
12436
- 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
+ `);
12437
12537
  if (knowledgeMd) {
12438
- console.log(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md`);
12538
+ process.stdout.write(` Knowledge: ${finalOutputDir}/SITE_KNOWLEDGE.md
12539
+ `);
12439
12540
  }
12440
- console.log(` Actions: ${data.actions.length}`);
12441
- console.log(` APIs: ${data.network.filter((n) => n.contentType.includes("json") || n.url.includes("/api/")).length}`);
12442
- console.log("");
12443
- console.log("Next steps:");
12444
- console.log(` 1. Review and edit: ${finalOutputDir}/index.ts`);
12445
- console.log(` 2. Test: xbrowser ${finalPluginName} <command>`);
12446
- 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
+ `);
12447
12553
  }
12448
12554
  function generatePluginCode(pluginName, domain, data, _knowledgeMd) {
12449
12555
  const pagePaths = /* @__PURE__ */ new Set();
@@ -12622,20 +12728,22 @@ async function handleRun(filePath, options) {
12622
12728
  });
12623
12729
  for (const step of chainResult.steps) {
12624
12730
  if (step.success) {
12625
- console.log(`[OK] ${step.raw}`);
12731
+ process.stdout.write(`[OK] ${step.raw}
12732
+ `);
12626
12733
  if (step.data && typeof step.data === "object") {
12627
12734
  const d = step.data;
12628
12735
  for (const [k, v] of Object.entries(d)) {
12629
12736
  if (k !== "ok")
12630
- console.log(` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
12737
+ process.stdout.write(` ${k}: ${typeof v === "string" ? v : JSON.stringify(v)}
12738
+ `);
12631
12739
  }
12632
12740
  }
12633
12741
  } else {
12634
- console.error(`[FAIL] ${step.raw}: ${step.message}`);
12742
+ outputError(`[FAIL] ${step.raw}: ${step.message}`);
12635
12743
  }
12636
12744
  }
12637
12745
  if (chainResult.stoppedReason) {
12638
- console.error(`Stopped: ${chainResult.stoppedReason}`);
12746
+ outputError(`Stopped: ${chainResult.stoppedReason}`);
12639
12747
  }
12640
12748
  if (!chainResult.success) process.exit(1);
12641
12749
  }
@@ -12678,26 +12786,29 @@ async function handleNetCommand(args, options, mode, sessionName) {
12678
12786
  if (mode === "json") {
12679
12787
  outputResult(result, mode);
12680
12788
  } else {
12681
- console.log(`
12789
+ process.stdout.write(`
12682
12790
  Network captures (session: ${netSession})`);
12683
- console.log(` Total: ${result.total}, Showing: ${result.captures.length}
12791
+ process.stdout.write(` Total: ${result.total}, Showing: ${result.captures.length}
12684
12792
  `);
12685
12793
  for (const c of result.captures) {
12686
12794
  const statusColor = c.status < 300 ? "\x1B[32m" : c.status < 400 ? "\x1B[33m" : "\x1B[31m";
12687
12795
  const reset = "\x1B[0m";
12688
- 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
+ `);
12689
12798
  if (c.size > 0) {
12690
12799
  const sizeStr = c.size > 1024 ? `${(c.size / 1024).toFixed(1)}KB` : `${c.size}B`;
12691
- console.log(` ${c.contentType.split(";")[0]} ${sizeStr}`);
12800
+ process.stdout.write(` ${c.contentType.split(";")[0]} ${sizeStr}
12801
+ `);
12692
12802
  }
12693
12803
  }
12694
- console.log("");
12804
+ process.stdout.write("\n");
12695
12805
  }
12696
12806
  break;
12697
12807
  }
12698
12808
  case "clear": {
12699
12809
  await forwardNetworkClear(netSession);
12700
- console.log(`Network captures cleared for session: ${netSession}`);
12810
+ process.stdout.write(`Network captures cleared for session: ${netSession}
12811
+ `);
12701
12812
  break;
12702
12813
  }
12703
12814
  case "top": {
@@ -12707,21 +12818,23 @@ async function handleNetCommand(args, options, mode, sessionName) {
12707
12818
  if (mode === "json") {
12708
12819
  outputResult(result, mode);
12709
12820
  } else {
12710
- console.log(`
12821
+ process.stdout.write(`
12711
12822
  Top valued requests (session: ${netSession})`);
12712
- console.log(` Showing: ${result.entries.length}
12823
+ process.stdout.write(` Showing: ${result.entries.length}
12713
12824
  `);
12714
12825
  for (const e of result.entries) {
12715
12826
  const scoreColor = e.score >= 50 ? "\x1B[32m" : e.score >= 20 ? "\x1B[33m" : "\x1B[90m";
12716
12827
  const reset = "\x1B[0m";
12717
12828
  const methodStr = e.method.padEnd(6);
12718
12829
  const scoreStr = `${scoreColor}${e.score.toString().padStart(3)}${reset}`;
12719
- 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
+ `);
12720
12832
  if (e.scoreBreakdown.content > 0) {
12721
- 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
+ `);
12722
12835
  }
12723
12836
  }
12724
- console.log("");
12837
+ process.stdout.write("\n");
12725
12838
  }
12726
12839
  break;
12727
12840
  }
@@ -12730,16 +12843,17 @@ async function handleNetCommand(args, options, mode, sessionName) {
12730
12843
  if (mode === "json") {
12731
12844
  outputResult(logResult, mode);
12732
12845
  } else {
12733
- console.log(`
12846
+ process.stdout.write(`
12734
12847
  Command log (session: ${netSession})`);
12735
- console.log(` Total: ${logResult.commands.length}
12848
+ process.stdout.write(` Total: ${logResult.commands.length}
12736
12849
  `);
12737
12850
  for (const cmd of logResult.commands) {
12738
12851
  const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
12739
12852
  const paramsStr = Object.entries(cmd.params).map(([k, v]) => `${k}=${v}`).join(" ");
12740
- console.log(` #${cmd.id} [${ts}] ${cmd.command} ${paramsStr}`);
12853
+ process.stdout.write(` #${cmd.id} [${ts}] ${cmd.command} ${paramsStr}
12854
+ `);
12741
12855
  }
12742
- console.log("");
12856
+ process.stdout.write("\n");
12743
12857
  }
12744
12858
  break;
12745
12859
  }
@@ -12755,28 +12869,31 @@ async function handleNetCommand(args, options, mode, sessionName) {
12755
12869
  outputResult(aroundResult, mode);
12756
12870
  } else {
12757
12871
  if (!aroundResult) {
12758
- console.log(" No command found with that ID");
12872
+ process.stdout.write(" No command found with that ID\n");
12759
12873
  break;
12760
12874
  }
12761
12875
  const cmd = aroundResult.command;
12762
12876
  const ts = new Date(cmd.timestamp).toISOString().substring(11, 19);
12763
- console.log(`
12877
+ process.stdout.write(`
12764
12878
  Command: #${cmd.id} [${ts}] ${cmd.command}`);
12765
- console.log(` Window: \xB1${windowMs}ms
12879
+ process.stdout.write(` Window: \xB1${windowMs}ms
12766
12880
  `);
12767
12881
  const before = aroundResult.before;
12768
12882
  const after = aroundResult.after;
12769
- console.log(` BEFORE (${before.length} requests):`);
12883
+ process.stdout.write(` BEFORE (${before.length} requests):
12884
+ `);
12770
12885
  for (const r of before.slice(0, 5)) {
12771
- 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
+ `);
12772
12888
  }
12773
- console.log(`
12889
+ process.stdout.write(`
12774
12890
  AFTER (${aroundResult.afterCount} requests):`);
12775
12891
  for (const r of after.slice(0, 10)) {
12776
12892
  const highlight = r.method !== "GET" ? " \u2190" : "";
12777
- 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
+ `);
12778
12895
  }
12779
- console.log("");
12896
+ process.stdout.write("\n");
12780
12897
  }
12781
12898
  break;
12782
12899
  }
@@ -12785,9 +12902,9 @@ async function handleNetCommand(args, options, mode, sessionName) {
12785
12902
  if (mode === "json") {
12786
12903
  outputResult(result, mode);
12787
12904
  } else {
12788
- console.log(`
12905
+ process.stdout.write(`
12789
12906
  API Reusability Analysis (session: ${netSession})`);
12790
- console.log(` Total: ${result.total}, Analyzed: ${result.analyzed.length}
12907
+ process.stdout.write(` Total: ${result.total}, Analyzed: ${result.analyzed.length}
12791
12908
  `);
12792
12909
  const groups = { high: [], medium: [], low: [], unknown: [] };
12793
12910
  for (const e of result.analyzed) {
@@ -12798,16 +12915,20 @@ async function handleNetCommand(args, options, mode, sessionName) {
12798
12915
  if (!items?.length) continue;
12799
12916
  const color = level === "high" ? "\x1B[32m" : level === "medium" ? "\x1B[33m" : level === "low" ? "\x1B[31m" : "\x1B[90m";
12800
12917
  const reset = "\x1B[0m";
12801
- console.log(` ${color}${level.toUpperCase()}${reset} (${items.length})`);
12918
+ process.stdout.write(` ${color}${level.toUpperCase()}${reset} (${items.length})
12919
+ `);
12802
12920
  for (const e of items.slice(0, 5)) {
12803
12921
  const scoreStr = `[${e.reusability.score.toString().padStart(3)}]`;
12804
- 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
+ `);
12805
12924
  if (e.reusability.reasons.length > 0) {
12806
- console.log(` ${e.reusability.reasons.join(", ")}`);
12925
+ process.stdout.write(` ${e.reusability.reasons.join(", ")}
12926
+ `);
12807
12927
  }
12808
12928
  }
12809
- if (items.length > 5) console.log(` ... and ${items.length - 5} more`);
12810
- console.log("");
12929
+ if (items.length > 5) process.stdout.write(` ... and ${items.length - 5} more
12930
+ `);
12931
+ process.stdout.write("\n");
12811
12932
  }
12812
12933
  }
12813
12934
  break;
@@ -12826,12 +12947,12 @@ async function handleNetCommand(args, options, mode, sessionName) {
12826
12947
  if (mode === "json") {
12827
12948
  outputResult(result, mode);
12828
12949
  } else {
12829
- console.log(`
12950
+ process.stdout.write(`
12830
12951
  ${result.method} ${result.url}`);
12831
- console.log(` Headers: ${result.headerCount}, Body: ${result.hasBody}
12952
+ process.stdout.write(` Headers: ${result.headerCount}, Body: ${result.hasBody}
12832
12953
  `);
12833
- console.log(result.command);
12834
- console.log("");
12954
+ process.stdout.write(result.command + "\n");
12955
+ process.stdout.write("\n");
12835
12956
  }
12836
12957
  break;
12837
12958
  }
@@ -12849,27 +12970,33 @@ async function handleNetCommand(args, options, mode, sessionName) {
12849
12970
  if (mode === "json") {
12850
12971
  outputResult(result, mode);
12851
12972
  } else {
12852
- console.log(`
12973
+ process.stdout.write(`
12853
12974
  Replay Result`);
12854
- console.log(` ${result.curlCommand?.split("\n")[0]?.trim()}
12975
+ process.stdout.write(` ${result.curlCommand?.split("\n")[0]?.trim()}
12855
12976
  `);
12856
12977
  const replay = result.replay;
12857
12978
  if (replay?.error) {
12858
- console.log(` \x1B[31mFAILED\x1B[0m: ${replay.error}`);
12979
+ process.stdout.write(` \x1B[31mFAILED\x1B[0m: ${replay.error}
12980
+ `);
12859
12981
  } else if (replay) {
12860
12982
  const statusColor = replay.status && replay.status < 300 ? "\x1B[32m" : "\x1B[31m";
12861
12983
  const status = replay.status;
12862
12984
  const size = replay.size;
12863
12985
  const duration = replay.duration;
12864
- console.log(` Status: ${statusColor}${status}\x1B[0m ${replay.statusText}`);
12865
- console.log(` Size: ${size > 1024 ? (size / 1024).toFixed(1) + "KB" : size + "B"}`);
12866
- console.log(` Duration: ${duration}ms`);
12867
- 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
+ `);
12868
12994
  if (status && status >= 400) {
12869
- 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
+ `);
12870
12997
  }
12871
12998
  }
12872
- console.log("");
12999
+ process.stdout.write("\n");
12873
13000
  }
12874
13001
  break;
12875
13002
  }
@@ -12888,40 +13015,47 @@ async function handleNetCommand(args, options, mode, sessionName) {
12888
13015
  outputResult(result, mode);
12889
13016
  } else {
12890
13017
  const c = result.capture;
12891
- console.log(`
13018
+ process.stdout.write(`
12892
13019
  Request #${c.id}`);
12893
- console.log(` ${c.method} ${c.url}`);
12894
- console.log(` Status: ${c.status} | Size: ${c.size}B | Type: ${c.contentType}`);
12895
- 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
+ `);
12896
13026
  if (c.requestHeaders) {
12897
- console.log(`
13027
+ process.stdout.write(`
12898
13028
  Request Headers:`);
12899
13029
  for (const [k, v] of Object.entries(c.requestHeaders)) {
12900
- console.log(` ${k}: ${String(v).substring(0, 100)}`);
13030
+ process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
13031
+ `);
12901
13032
  }
12902
13033
  }
12903
13034
  if (c.requestBody !== void 0) {
12904
- console.log(`
13035
+ process.stdout.write(`
12905
13036
  Request Body:`);
12906
13037
  const bodyStr = typeof c.requestBody === "string" ? c.requestBody : JSON.stringify(c.requestBody, null, 2);
12907
13038
  const lines = bodyStr.split("\n").slice(0, 20);
12908
- for (const line of lines) console.log(` ${line}`);
12909
- 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(" ...");
12910
13042
  }
12911
- console.log(`
13043
+ process.stdout.write(`
12912
13044
  Response Headers:`);
12913
13045
  for (const [k, v] of Object.entries(c.headers)) {
12914
- console.log(` ${k}: ${String(v).substring(0, 100)}`);
13046
+ process.stdout.write(` ${k}: ${String(v).substring(0, 100)}
13047
+ `);
12915
13048
  }
12916
13049
  if (c.body !== void 0) {
12917
- console.log(`
13050
+ process.stdout.write(`
12918
13051
  Response Body:`);
12919
13052
  const bodyStr = typeof c.body === "string" ? c.body : JSON.stringify(c.body, null, 2);
12920
13053
  const lines = bodyStr.split("\n").slice(0, 20);
12921
- for (const line of lines) console.log(` ${line}`);
12922
- 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(" ...");
12923
13057
  }
12924
- console.log("");
13058
+ process.stdout.write("\n");
12925
13059
  }
12926
13060
  break;
12927
13061
  }
@@ -12932,7 +13066,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
12932
13066
  break;
12933
13067
  }
12934
13068
  await forwardNetworkLike(netSession, id);
12935
- console.log(`Marked #${id} as useful`);
13069
+ process.stdout.write(`Marked #${id} as useful
13070
+ `);
12936
13071
  break;
12937
13072
  }
12938
13073
  case "dislike": {
@@ -12942,7 +13077,8 @@ async function handleNetCommand(args, options, mode, sessionName) {
12942
13077
  break;
12943
13078
  }
12944
13079
  await forwardNetworkDislike(netSession, id);
12945
- console.log(`Marked #${id} as not useful`);
13080
+ process.stdout.write(`Marked #${id} as not useful
13081
+ `);
12946
13082
  break;
12947
13083
  }
12948
13084
  case "export": {
@@ -12957,7 +13093,7 @@ async function handleNetCommand(args, options, mode, sessionName) {
12957
13093
  outputError(result.error);
12958
13094
  break;
12959
13095
  }
12960
- console.log(result.code);
13096
+ process.stdout.write(result.code + "\n");
12961
13097
  break;
12962
13098
  }
12963
13099
  default:
@@ -12969,9 +13105,25 @@ async function handleNetCommand(args, options, mode, sessionName) {
12969
13105
  }
12970
13106
 
12971
13107
  // src/cli/test-routes.ts
12972
- import { execSync as execSync3 } from "child_process";
13108
+ import { execFile as execFile3 } from "child_process";
12973
13109
  import { readFileSync as readFileSync11 } from "fs";
12974
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
+ }
12975
13127
  function findPluginPath(plugin) {
12976
13128
  const candidates = [
12977
13129
  resolve9(process.cwd(), ".xcli/plugins", plugin, "index.ts"),
@@ -13036,20 +13188,21 @@ function extractSchema(plugin, command) {
13036
13188
  }
13037
13189
  async function runTest(plugin, command, cmdArgs, options) {
13038
13190
  const cdp = options.cdp || options.cdpEndpoint || "http://localhost:9221";
13039
- const argsStr = cmdArgs.filter((a) => !a.startsWith("--cdp")).join(" ");
13191
+ const passthroughArgs = cmdArgs.filter((a) => !a.startsWith("--cdp"));
13040
13192
  const schema = extractSchema(plugin, command);
13041
- const fullCmd = `npx xbrowser ${plugin} ${command} ${argsStr} --cdp ${cdp} --json --timeout 60000`;
13042
- let stdout = "";
13043
- try {
13044
- stdout = execSync3(fullCmd, {
13045
- timeout: 65e3,
13046
- encoding: "utf-8",
13047
- stdio: ["pipe", "pipe", "pipe"],
13048
- env: { ...process.env, FORCE_COLOR: "0" }
13049
- });
13050
- } catch (e) {
13051
- const err = e;
13052
- 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) {
13053
13206
  const jsonLine = stdout.split("\n").find((l) => {
13054
13207
  try {
13055
13208
  JSON.parse(l);
@@ -13068,11 +13221,10 @@ async function runTest(plugin, command, cmdArgs, options) {
13068
13221
  } catch {
13069
13222
  }
13070
13223
  }
13071
- const stderr = err.stderr?.toString() || "";
13072
13224
  if (stdout.includes("captcha") || stderr.includes("captcha") || stdout.includes("CAPTCHA")) {
13073
13225
  return { status: "CAPTCHA", message: "\u68C0\u6D4B\u5230\u9A8C\u8BC1\u7801", viewerUrl: "http://localhost:9224/preview/default" };
13074
13226
  }
13075
- 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" };
13076
13228
  }
13077
13229
  const allLines = stdout.split("\n");
13078
13230
  const jsonStart = allLines.findIndex((l) => l.trim().startsWith("{"));
@@ -13142,27 +13294,27 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
13142
13294
  const plugin = cmdArgs[0];
13143
13295
  const command = cmdArgs[1];
13144
13296
  if (!plugin || !command) {
13145
- console.error("\u7528\u6CD5: xbrowser test <plugin> <command> [\u53C2\u6570...]");
13146
- 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");
13147
13299
  return;
13148
13300
  }
13149
13301
  const loader = await getPluginLoader();
13150
13302
  const internalLoader = loader.getCore().loader;
13151
13303
  const site = internalLoader.getSite(plugin);
13152
13304
  if (!site) {
13153
- console.error(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
13305
+ outputError(`\u63D2\u4EF6 "${plugin}" \u4E0D\u5B58\u5728`);
13154
13306
  return;
13155
13307
  }
13156
13308
  const cmdEntry = site.getCommand(command);
13157
13309
  if (!cmdEntry) {
13158
- console.error(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
13310
+ outputError(`\u6307\u4EE4 "${command}" \u4E0D\u5B58\u5728`);
13159
13311
  return;
13160
13312
  }
13161
13313
  const testArgs = cmdArgs.slice(2);
13162
13314
  const mergedOptions = { ...options, cdp: cdpEndpoint || options.cdp };
13163
13315
  const result = await runTest(plugin, command, testArgs, mergedOptions);
13164
13316
  if (mode === "json") {
13165
- console.log(JSON.stringify(result, null, 2));
13317
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
13166
13318
  return;
13167
13319
  }
13168
13320
  const r = result;
@@ -13177,23 +13329,32 @@ async function handleTest(cmdArgs, options, mode, cdpEndpoint) {
13177
13329
  };
13178
13330
  const status = String(r.status);
13179
13331
  const icon = icons[status] || "\u2753";
13180
- console.log(`
13181
- ${icon} ${plugin}.${command}`);
13182
- console.log(` \u72B6\u6001: ${status}`);
13332
+ process.stdout.write(`
13333
+ ${icon} ${plugin}.${command}
13334
+ `);
13335
+ process.stdout.write(` \u72B6\u6001: ${status}
13336
+ `);
13183
13337
  if (status === "OK") {
13184
- if (r.count) console.log(` \u6570\u636E: ${r.count} \u9879`);
13185
- 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
+ `);
13186
13342
  } else if (status === "LOGIN_REQUIRED" || status === "CAPTCHA") {
13187
- console.log(` \u4FE1\u606F: ${String(r.message)}`);
13188
- console.log(` Viewer: ${String(r.viewerUrl)}`);
13343
+ process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
13344
+ `);
13345
+ process.stdout.write(` Viewer: ${String(r.viewerUrl)}`);
13189
13346
  } else if (status === "SCHEMA_ERROR") {
13190
13347
  const errs = r.errors;
13191
- if (errs) console.log(` \u9519\u8BEF: ${errs.join("; ")}`);
13348
+ if (errs) process.stdout.write(` \u9519\u8BEF: ${errs.join("; ")}
13349
+ `);
13192
13350
  } else if (["NO_DATA", "BLOCKED"].includes(status)) {
13193
- console.log(` \u4FE1\u606F: ${String(r.message)}`);
13194
- 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
+ `);
13195
13355
  } else {
13196
- console.log(` \u4FE1\u606F: ${String(r.message)}`);
13356
+ process.stdout.write(` \u4FE1\u606F: ${String(r.message)}
13357
+ `);
13197
13358
  }
13198
13359
  }
13199
13360
 
@@ -13291,7 +13452,7 @@ Commands:
13291
13452
  plugin list List plugins
13292
13453
  plugin reload <name> Reload plugin
13293
13454
  create <name> --template <type> Create plugin
13294
- 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)
13295
13456
  remote <url> [command] [--token <t>] Execute on remote server
13296
13457
  run <file> Execute commands from file
13297
13458
  viewer [--name <n>] Generate viewer URL
@@ -13412,10 +13573,26 @@ function validateAuth(authHeader, validTokens) {
13412
13573
  function isAuthRequired(validTokens) {
13413
13574
  return validTokens.length > 0;
13414
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
+ }
13415
13593
 
13416
13594
  // src/server/router.ts
13417
- var CORS_HEADERS = {
13418
- "Access-Control-Allow-Origin": "*",
13595
+ var CORS_BASE_HEADERS = {
13419
13596
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
13420
13597
  "Access-Control-Allow-Headers": "Content-Type, Authorization"
13421
13598
  };
@@ -13468,9 +13645,31 @@ function jsonResponse(statusCode, body, extraHeaders) {
13468
13645
  return {
13469
13646
  statusCode,
13470
13647
  body,
13471
- headers: { ...CORS_HEADERS, "Content-Type": "application/json", ...extraHeaders }
13648
+ headers: { "Content-Type": "application/json", ...extraHeaders }
13472
13649
  };
13473
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
13657
+ };
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
+ }
13474
13673
  function errorResponse(statusCode, error, message) {
13475
13674
  return jsonResponse(statusCode, { error, message, statusCode });
13476
13675
  }
@@ -13588,13 +13787,30 @@ async function ensureSession(sessionName, url, cdpEndpoint) {
13588
13787
  function isHealthCheckPath(pathname) {
13589
13788
  return pathname === "/api/v1/health";
13590
13789
  }
13591
- 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) {
13592
13795
  const parsedUrl = new URL(url, "http://localhost");
13593
13796
  const pathname = parsedUrl.pathname;
13594
13797
  const query = parseQueryString(parsedUrl.search);
13595
13798
  if (method === "OPTIONS") {
13596
13799
  return jsonResponse(204, null);
13597
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
+ }
13598
13814
  const match = matchRoute(method, pathname);
13599
13815
  if (!match) {
13600
13816
  const pathMatch = matchRoute("GET", pathname) || matchRoute("POST", pathname) || matchRoute("DELETE", pathname);
@@ -13617,19 +13833,24 @@ async function route(method, url, headers, body) {
13617
13833
  return errorResponse(500, "INTERNAL_ERROR", errMsg(err));
13618
13834
  }
13619
13835
  }
13620
- async function handleRequest(req, res, validateAuthFn) {
13836
+ async function handleRequest(req, res, validateAuthFn, corsOrigins) {
13621
13837
  const url = req.url || "/";
13622
13838
  const method = (req.method || "GET").toUpperCase();
13623
13839
  const pathname = new URL(url, "http://localhost").pathname;
13624
13840
  if (method === "OPTIONS") {
13625
- const response2 = await route(method, url, headersToObject(req.headers), null);
13841
+ const response2 = await route(method, url, headersToObject(req.headers), null, corsOrigins);
13626
13842
  writeResponse(res, response2);
13627
13843
  return;
13628
13844
  }
13629
13845
  if (validateAuthFn && !isHealthCheckPath(pathname)) {
13630
13846
  const authHeader = req.headers["authorization"];
13631
13847
  if (!validateAuthFn(authHeader)) {
13632
- 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);
13633
13854
  return;
13634
13855
  }
13635
13856
  }
@@ -13637,7 +13858,7 @@ async function handleRequest(req, res, validateAuthFn) {
13637
13858
  if (method === "POST" || method === "PUT") {
13638
13859
  body = await readBody(req);
13639
13860
  }
13640
- const response = await route(method, url, headersToObject(req.headers), body);
13861
+ const response = await route(method, url, headersToObject(req.headers), body, corsOrigins);
13641
13862
  writeResponse(res, response);
13642
13863
  }
13643
13864
  function headersToObject(headers) {
@@ -13678,26 +13899,35 @@ var HTTPServer = class {
13678
13899
  host;
13679
13900
  server = null;
13680
13901
  validTokens;
13902
+ corsOrigins;
13681
13903
  constructor(config) {
13682
13904
  this.port = config?.port ?? 9224;
13683
- this.host = config?.host ?? "0.0.0.0";
13905
+ this.host = config?.host ?? "127.0.0.1";
13684
13906
  this.validTokens = resolveTokens(config?.tokens);
13907
+ this.corsOrigins = config?.corsOrigins;
13685
13908
  }
13686
13909
  /**
13687
13910
  * Start the HTTP server and begin listening for requests.
13688
13911
  *
13689
13912
  * @returns The actual port and host the server bound to.
13690
- * @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.
13691
13915
  */
13692
13916
  async start() {
13693
13917
  if (this.server) {
13694
13918
  throw new Error("HTTP server is already running");
13695
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
+ }
13696
13925
  const authRequired = isAuthRequired(this.validTokens);
13697
13926
  const tokens = this.validTokens;
13927
+ const corsOrigins = this.corsOrigins;
13698
13928
  this.server = createServer((req, res) => {
13699
13929
  const authFn = authRequired ? (authHeader) => validateAuth(authHeader, tokens) : void 0;
13700
- handleRequest(req, res, authFn).catch((err) => {
13930
+ handleRequest(req, res, authFn, corsOrigins).catch((err) => {
13701
13931
  const message = err instanceof Error ? err.message : String(err);
13702
13932
  if (!res.headersSent) {
13703
13933
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -14168,7 +14398,7 @@ async function routeCommand(argvIn, stdinCommands) {
14168
14398
  replay: "replay <file> [--slow-mo <ms>] [--stop-on-error]",
14169
14399
  create: "create <name> [--template static|dynamic|login|api]",
14170
14400
  run: "run <file>",
14171
- serve: "serve [--port <port>] [--token <token>]",
14401
+ serve: "serve [--port <port>] [--host <host>] [--token <token>] [--cors-origins <csv>]",
14172
14402
  remote: "remote <url> [command] [--token <token>]",
14173
14403
  convert: "convert <file> [--to js|py|sh]",
14174
14404
  extract: "extract <file> [--format json|yaml]",
@@ -14567,7 +14797,10 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
14567
14797
  async function handleServe(_args, options, mode) {
14568
14798
  const port = options.port ? Number(options.port) : void 0;
14569
14799
  const token = options.token;
14570
- 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 });
14571
14804
  process.on("SIGINT", async () => {
14572
14805
  await httpServer.stop();
14573
14806
  return;