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