@xbrowser/cli 1.9.0 → 1.9.2

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
@@ -50,11 +50,11 @@ import {
50
50
  extractAndSave,
51
51
  extractRecording,
52
52
  printExtractSummary
53
- } from "./chunk-MJFYLKGL.js";
53
+ } from "./chunk-X46HDAOT.js";
54
54
  import {
55
55
  filterRecording,
56
56
  parseExcludeTypes
57
- } from "./chunk-GJAV3QGG.js";
57
+ } from "./chunk-ANVL2ID2.js";
58
58
  import {
59
59
  SessionRecorder
60
60
  } from "./chunk-YSCY52UJ.js";
@@ -109,7 +109,7 @@ import {
109
109
  // src/executor.ts
110
110
  import {
111
111
  ok as ok25,
112
- fail as fail9,
112
+ fail as fail11,
113
113
  isCommandResult,
114
114
  CompositeStorage as CompositeStorage2,
115
115
  TipCollector as TipCollector2,
@@ -993,7 +993,7 @@ var evaluateCommand = registerCommand({
993
993
 
994
994
  // src/commands/storage.ts
995
995
  import { z as z8 } from "zod";
996
- import { ok as ok8 } from "@dyyz1993/xcli-core";
996
+ import { ok as ok8, fail as fail2 } from "@dyyz1993/xcli-core";
997
997
  var getCookiesCommand = registerCommand({
998
998
  name: "get-cookies",
999
999
  description: "Get all cookies for the current page",
@@ -1015,6 +1015,7 @@ var setCookieCommand = registerCommand({
1015
1015
  value: z8.coerce.string(),
1016
1016
  domain: z8.coerce.string().optional(),
1017
1017
  path: z8.coerce.string().optional(),
1018
+ url: z8.string().optional().describe("Cookie URL (alternative to domain)"),
1018
1019
  expires: z8.number().optional(),
1019
1020
  httpOnly: z8.boolean().optional(),
1020
1021
  secure: z8.boolean().optional(),
@@ -1034,6 +1035,9 @@ var setCookieCommand = registerCommand({
1034
1035
  }
1035
1036
  }
1036
1037
  }
1038
+ if (!cookie.domain && !cookie.url) {
1039
+ return fail2("set-cookie requires --domain or --url, or a non-blank page URL to infer from");
1040
+ }
1037
1041
  await ctx.browserContext.addCookies([cookie]);
1038
1042
  return ok8({ name: p.name });
1039
1043
  }
@@ -1060,19 +1064,23 @@ var getLocalStorageCommand = registerCommand({
1060
1064
  z8.object({ data: z8.record(z8.string()) })
1061
1065
  ]),
1062
1066
  handler: async (p, ctx) => {
1063
- if (p.key) {
1064
- const value = await ctx.page.evaluate((k) => localStorage.getItem(k), p.key);
1065
- return ok8({ key: p.key, value });
1067
+ try {
1068
+ if (p.key) {
1069
+ const value = await ctx.page.evaluate((k) => localStorage.getItem(k), p.key);
1070
+ return ok8({ key: p.key, value });
1071
+ }
1072
+ const data = await ctx.page.evaluate(() => {
1073
+ const entries = {};
1074
+ for (let i = 0; i < localStorage.length; i++) {
1075
+ const key = localStorage.key(i);
1076
+ if (key) entries[key] = localStorage.getItem(key) ?? "";
1077
+ }
1078
+ return entries;
1079
+ });
1080
+ return ok8({ data });
1081
+ } catch (e) {
1082
+ return fail2(`localStorage not accessible on this page: ${e instanceof Error ? e.message : String(e)}`);
1066
1083
  }
1067
- const data = await ctx.page.evaluate(() => {
1068
- const entries = {};
1069
- for (let i = 0; i < localStorage.length; i++) {
1070
- const key = localStorage.key(i);
1071
- if (key) entries[key] = localStorage.getItem(key) ?? "";
1072
- }
1073
- return entries;
1074
- });
1075
- return ok8({ data });
1076
1084
  }
1077
1085
  });
1078
1086
  var setLocalStorageCommand = registerCommand({
@@ -1085,13 +1093,17 @@ var setLocalStorageCommand = registerCommand({
1085
1093
  }),
1086
1094
  result: z8.object({ key: z8.string() }),
1087
1095
  handler: async (p, ctx) => {
1088
- await ctx.page.evaluate(
1089
- (args) => {
1090
- localStorage.setItem(args.key, args.value);
1091
- },
1092
- { key: p.key, value: p.value }
1093
- );
1094
- return ok8({ key: p.key });
1096
+ try {
1097
+ await ctx.page.evaluate(
1098
+ (args) => {
1099
+ localStorage.setItem(args.key, args.value);
1100
+ },
1101
+ { key: p.key, value: p.value }
1102
+ );
1103
+ return ok8({ key: p.key });
1104
+ } catch (e) {
1105
+ return fail2(`localStorage not accessible on this page: ${e instanceof Error ? e.message : String(e)}`);
1106
+ }
1095
1107
  }
1096
1108
  });
1097
1109
  var clearLocalStorageCommand = registerCommand({
@@ -1100,21 +1112,35 @@ var clearLocalStorageCommand = registerCommand({
1100
1112
  scope: "page",
1101
1113
  result: z8.object({ cleared: z8.boolean() }),
1102
1114
  handler: async (_p, ctx) => {
1103
- await ctx.page.evaluate(() => localStorage.clear());
1104
- return ok8({ cleared: true });
1115
+ try {
1116
+ await ctx.page.evaluate(() => localStorage.clear());
1117
+ return ok8({ cleared: true });
1118
+ } catch (e) {
1119
+ return fail2(`localStorage not accessible on this page: ${e instanceof Error ? e.message : String(e)}`);
1120
+ }
1105
1121
  }
1106
1122
  });
1107
1123
 
1108
1124
  // src/commands/screenshot.ts
1109
1125
  import { z as z9 } from "zod";
1110
- import { ok as ok9 } from "@dyyz1993/xcli-core";
1126
+ import { ok as ok9, fail as fail3 } from "@dyyz1993/xcli-core";
1111
1127
  import { writeFileSync, mkdirSync } from "fs";
1112
- import { join as join2 } from "path";
1128
+ import { dirname, join as join2 } from "path";
1113
1129
  import { homedir as homedir2 } from "os";
1114
1130
  var SCREENSHOTS_DIR = join2(homedir2(), ".xbrowser", "screenshots");
1115
1131
  function ensureScreenshotsDir() {
1116
1132
  mkdirSync(SCREENSHOTS_DIR, { recursive: true });
1117
1133
  }
1134
+ function ensureParentDir(filePath) {
1135
+ const dir = dirname(filePath);
1136
+ if (dir === "." || dir === "/") return null;
1137
+ try {
1138
+ mkdirSync(dir, { recursive: true });
1139
+ return null;
1140
+ } catch (err) {
1141
+ return err instanceof Error ? err.message : String(err);
1142
+ }
1143
+ }
1118
1144
  function generateScreenshotPath(format) {
1119
1145
  const timestamp = Date.now();
1120
1146
  const random = Math.random().toString(36).slice(2, 8);
@@ -1158,7 +1184,15 @@ var screenshotCommand = registerCommand({
1158
1184
  buffer = await ctx.page.screenshot(options);
1159
1185
  }
1160
1186
  if (p.output) {
1161
- writeFileSync(p.output, buffer, "binary");
1187
+ const dirErr = ensureParentDir(p.output);
1188
+ if (dirErr) {
1189
+ return fail3(`Cannot create directory for --output "${p.output}": ${dirErr}`);
1190
+ }
1191
+ try {
1192
+ writeFileSync(p.output, buffer, "binary");
1193
+ } catch (err) {
1194
+ return fail3(`Failed to write screenshot to "${p.output}": ${err instanceof Error ? err.message : String(err)}`);
1195
+ }
1162
1196
  return ok9({
1163
1197
  output: p.output,
1164
1198
  format,
@@ -1276,7 +1310,7 @@ var setViewportCommand = registerCommand({
1276
1310
 
1277
1311
  // src/commands/frame.ts
1278
1312
  import { z as z12 } from "zod";
1279
- import { ok as ok12, fail as fail2 } from "@dyyz1993/xcli-core";
1313
+ import { ok as ok12, fail as fail4 } from "@dyyz1993/xcli-core";
1280
1314
  var framesCommand = registerCommand({
1281
1315
  name: "frames",
1282
1316
  description: "List all frames in the current page",
@@ -1301,10 +1335,10 @@ var framesCommand = registerCommand({
1301
1335
  });
1302
1336
  var frameCommand = registerCommand({
1303
1337
  name: "frame",
1304
- description: "Switch to a frame by index or name",
1338
+ description: "Get frame info by index or name",
1305
1339
  scope: "page",
1306
1340
  parameters: z12.object({
1307
- index: z12.number().optional(),
1341
+ index: z12.number().int().min(0).optional(),
1308
1342
  name: z12.string().optional()
1309
1343
  }),
1310
1344
  result: z12.object({
@@ -1321,10 +1355,10 @@ var frameCommand = registerCommand({
1321
1355
  } else if (p.name !== void 0) {
1322
1356
  targetFrame = rawFrames.find((f) => f.name() === p.name);
1323
1357
  } else {
1324
- return fail2("Must provide index or name");
1358
+ return fail4("Must provide index or name");
1325
1359
  }
1326
1360
  if (!targetFrame) {
1327
- return fail2("Frame not found");
1361
+ return fail4("Frame not found");
1328
1362
  }
1329
1363
  return ok12({
1330
1364
  name: targetFrame.name(),
@@ -1970,7 +2004,7 @@ var actionsCommand = registerCommand({
1970
2004
 
1971
2005
  // src/commands/scrape.ts
1972
2006
  import { z as z15 } from "zod";
1973
- import { ok as ok15, fail as fail3 } from "@dyyz1993/xcli-core";
2007
+ import { ok as ok15, fail as fail5 } from "@dyyz1993/xcli-core";
1974
2008
 
1975
2009
  // src/lib/html-to-markdown.ts
1976
2010
  import * as cheerio from "cheerio";
@@ -2371,7 +2405,7 @@ var scrapeCommand = registerCommand({
2371
2405
  try {
2372
2406
  const targetUrl = p.url || page.url();
2373
2407
  if (!targetUrl || targetUrl === "about:blank") {
2374
- return fail3("URL is required. Provide a URL or connect to a browser (--cdp) with a non-blank page.");
2408
+ return fail5("URL is required. Provide a URL or connect to a browser (--cdp) with a non-blank page.");
2375
2409
  }
2376
2410
  let lastError;
2377
2411
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
@@ -2525,7 +2559,7 @@ var scrapeCommand = registerCommand({
2525
2559
  }
2526
2560
  }
2527
2561
  }
2528
- return fail3(`Scrape failed after ${maxAttempts} attempt(s): ${lastError?.message ?? "unknown error"}`);
2562
+ return fail5(`Scrape failed after ${maxAttempts} attempt(s): ${lastError?.message ?? "unknown error"}`);
2529
2563
  } finally {
2530
2564
  await closeEphemeralContext(context);
2531
2565
  }
@@ -2538,7 +2572,7 @@ var scrapeCommand = registerCommand({
2538
2572
 
2539
2573
  // src/commands/map.ts
2540
2574
  import { z as z16 } from "zod";
2541
- import { ok as ok16, fail as fail4 } from "@dyyz1993/xcli-core";
2575
+ import { ok as ok16, fail as fail6 } from "@dyyz1993/xcli-core";
2542
2576
 
2543
2577
  // src/utils/url.ts
2544
2578
  var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
@@ -2787,7 +2821,7 @@ var mapCommand = registerCommand({
2787
2821
  try {
2788
2822
  const targetUrl = p.url || page.url();
2789
2823
  if (!targetUrl || targetUrl === "about:blank") {
2790
- return fail4("URL is required. Provide a URL or connect to a browser (--cdp) with a non-blank page.");
2824
+ return fail6("URL is required. Provide a URL or connect to a browser (--cdp) with a non-blank page.");
2791
2825
  }
2792
2826
  const links = await discoverUrls(page, targetUrl, {
2793
2827
  sitemap: p.sitemap,
@@ -3632,7 +3666,7 @@ ${errors.map((e) => ` - ${e.engine}: ${e.error}`).join("\n")}`
3632
3666
 
3633
3667
  // src/commands/network.ts
3634
3668
  import { z as z19 } from "zod";
3635
- import { ok as ok19, fail as fail5 } from "@dyyz1993/xcli-core";
3669
+ import { ok as ok19, fail as fail7 } from "@dyyz1993/xcli-core";
3636
3670
  function extractPath2(url) {
3637
3671
  try {
3638
3672
  const u = new URL(url);
@@ -3797,7 +3831,7 @@ var networkCommand = registerCommand({
3797
3831
  };
3798
3832
  if (p.listen) {
3799
3833
  const page2 = ctx.page;
3800
- if (!page2) return fail5("No active page. Use --cdp to connect first.");
3834
+ if (!page2) return fail7("No active page. Use --cdp to connect first.");
3801
3835
  const captures = [];
3802
3836
  const consoleMessages = [];
3803
3837
  const wsCaptures = [];
@@ -4454,7 +4488,7 @@ function parseMarkdownResults(rawText) {
4454
4488
 
4455
4489
  // src/commands/snapshot.ts
4456
4490
  import { z as z21 } from "zod";
4457
- import { ok as ok20, fail as fail6, normalizeTips as normalizeTips3 } from "@dyyz1993/xcli-core";
4491
+ import { ok as ok20, fail as fail8, normalizeTips as normalizeTips3 } from "@dyyz1993/xcli-core";
4458
4492
 
4459
4493
  // src/runtime/ref-store.ts
4460
4494
  var sessions = /* @__PURE__ */ new Map();
@@ -4696,7 +4730,7 @@ async function resolveRefParams(page, params, selectorKeys, cache, sessionId) {
4696
4730
 
4697
4731
  // src/utils/site-semantics.ts
4698
4732
  import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync, readFileSync } from "fs";
4699
- import { join as join4, dirname } from "path";
4733
+ import { join as join4, dirname as dirname2 } from "path";
4700
4734
  import { homedir as homedir4 } from "os";
4701
4735
  import { stringify, parse } from "yaml";
4702
4736
  import { execFile } from "child_process";
@@ -4789,7 +4823,7 @@ function extractDomain2(url) {
4789
4823
  }
4790
4824
  function saveSemantics(domain, pagePath, url, elements) {
4791
4825
  const filePath = getSemanticsPath(domain);
4792
- const dir = dirname(filePath);
4826
+ const dir = dirname2(filePath);
4793
4827
  let site;
4794
4828
  if (existsSync(filePath)) {
4795
4829
  try {
@@ -5425,7 +5459,7 @@ var snapshotCommand = registerCommand({
5425
5459
  persistSemantics(url, aria);
5426
5460
  return ok20({ url, title, aria, text, dom }, normalizeTips3(tips));
5427
5461
  }
5428
- return fail6(`Unknown snapshot type: ${p.type}`);
5462
+ return fail8(`Unknown snapshot type: ${p.type}`);
5429
5463
  }
5430
5464
  });
5431
5465
  function persistSemantics(url, aria) {
@@ -5594,7 +5628,7 @@ var waitForCommand = registerCommand({
5594
5628
 
5595
5629
  // src/commands/tab.ts
5596
5630
  import { z as z23 } from "zod";
5597
- import { ok as ok22, fail as fail7 } from "@dyyz1993/xcli-core";
5631
+ import { ok as ok22, fail as fail9 } from "@dyyz1993/xcli-core";
5598
5632
  var TabParams = z23.object({
5599
5633
  subcommand: z23.enum(["list", "new", "close", "switch"]),
5600
5634
  url: z23.string().optional(),
@@ -5611,7 +5645,7 @@ var tabCommand = registerCommand({
5611
5645
  }),
5612
5646
  handler: async (p, ctx) => {
5613
5647
  if (!ctx.browserContext) {
5614
- return fail7("No browser context available. Use --cdp to connect to a browser first.");
5648
+ return fail9("No browser context available. Use --cdp to connect to a browser first.");
5615
5649
  }
5616
5650
  const pages = ctx.browserContext.pages();
5617
5651
  switch (p.subcommand) {
@@ -5620,50 +5654,45 @@ var tabCommand = registerCommand({
5620
5654
  case "new":
5621
5655
  return handleNew(p, pages, ctx);
5622
5656
  case "close":
5623
- return handleClose(p, pages, ctx);
5657
+ return handleClose(p, ctx);
5624
5658
  case "switch":
5625
5659
  return handleSwitch(p, pages, ctx);
5626
5660
  default:
5627
- return fail7(`Unknown subcommand: ${p.subcommand}`);
5661
+ return fail9(`Unknown subcommand: ${p.subcommand}`);
5628
5662
  }
5629
5663
  }
5630
5664
  });
5631
- function handleList(pages, ctx) {
5632
- const currentIndex = pages.indexOf(ctx.page);
5633
- const tabs = pages.map((page, i) => {
5665
+ async function handleList(pages, ctx) {
5666
+ const tabs = [];
5667
+ let activeIndex = -1;
5668
+ for (let i = 0; i < pages.length; i++) {
5669
+ const page = pages[i];
5634
5670
  const url = page.url();
5635
- let title = "";
5636
- try {
5637
- const t = page.title();
5638
- if (t instanceof Promise) {
5639
- void t.then((v) => {
5640
- title = v;
5641
- });
5642
- }
5643
- } catch {
5644
- title = "";
5645
- }
5646
- return {
5647
- index: i,
5648
- url,
5649
- title,
5650
- active: i === currentIndex
5651
- };
5652
- });
5653
- return ok22({ tabs, total: tabs.length, activeIndex: currentIndex });
5671
+ const title = await page.title().catch(() => "");
5672
+ const isActive = page === ctx.page;
5673
+ if (isActive) activeIndex = i;
5674
+ tabs.push({ index: i, url, title, active: isActive });
5675
+ }
5676
+ return ok22({ tabs, total: tabs.length, activeIndex });
5654
5677
  }
5655
5678
  async function handleNew(p, _pages, ctx) {
5656
5679
  const newPage = await ctx.browserContext.newPage();
5680
+ const warnings = [];
5657
5681
  if (p.url) {
5658
5682
  let url = p.url;
5659
- if (!/^https?:\/\//i.test(url)) {
5683
+ if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(url)) {
5660
5684
  url = "https://" + url;
5661
5685
  }
5662
- await newPage.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
5663
- });
5686
+ try {
5687
+ await newPage.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 });
5688
+ } catch (err) {
5689
+ warnings.push(`Navigation to "${url}" failed: ${err instanceof Error ? err.message : String(err)}`);
5690
+ }
5691
+ }
5692
+ try {
5693
+ await newPage.waitForLoadState("domcontentloaded");
5694
+ } catch {
5664
5695
  }
5665
- await newPage.waitForLoadState("domcontentloaded").catch(() => {
5666
- });
5667
5696
  const session = ctx.sessionId ? getSessionById(ctx.sessionId) : void 0;
5668
5697
  if (session) {
5669
5698
  setActivePage(session, newPage);
@@ -5676,18 +5705,20 @@ async function handleNew(p, _pages, ctx) {
5676
5705
  index: newIndex >= 0 ? newIndex : allPages.length - 1,
5677
5706
  url: newPage.url(),
5678
5707
  title,
5679
- total: allPages.length
5708
+ total: allPages.length,
5709
+ ...warnings.length > 0 ? { warning: warnings.join("; ") } : {}
5680
5710
  });
5681
5711
  }
5682
- async function handleClose(p, pages, ctx) {
5683
- if (pages.length <= 1) {
5684
- return fail7("Cannot close the last remaining tab");
5712
+ async function handleClose(p, ctx) {
5713
+ const currentPages = ctx.browserContext.pages();
5714
+ if (currentPages.length <= 1) {
5715
+ return fail9("Cannot close the last remaining tab");
5685
5716
  }
5686
- const closeIndex = p.index ?? pages.indexOf(ctx.page);
5687
- if (closeIndex < 0 || closeIndex >= pages.length) {
5688
- return fail7(`Invalid tab index: ${closeIndex}. Valid range: 0-${pages.length - 1}`);
5717
+ const closeIndex = p.index ?? currentPages.findIndex((pg) => pg === ctx.page);
5718
+ if (closeIndex < 0 || closeIndex >= currentPages.length) {
5719
+ return fail9(`Invalid tab index: ${closeIndex}. Valid range: 0-${currentPages.length - 1}`);
5689
5720
  }
5690
- const pageToClose = pages[closeIndex];
5721
+ const pageToClose = currentPages[closeIndex];
5691
5722
  const isActivePage = pageToClose === ctx.page;
5692
5723
  await pageToClose.close();
5693
5724
  const remainingPages = ctx.browserContext.pages();
@@ -5703,15 +5734,15 @@ async function handleClose(p, pages, ctx) {
5703
5734
  return ok22({
5704
5735
  closedIndex: closeIndex,
5705
5736
  total: remainingPages.length,
5706
- activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : pages.indexOf(ctx.page)
5737
+ activeIndex: isActivePage ? closeIndex < remainingPages.length ? closeIndex : remainingPages.length - 1 : remainingPages.findIndex((pg) => pg === ctx.page)
5707
5738
  });
5708
5739
  }
5709
5740
  async function handleSwitch(p, pages, ctx) {
5710
5741
  if (p.index === void 0) {
5711
- return fail7("Parameter --index is required for switch subcommand");
5742
+ return fail9("Parameter --index is required for switch subcommand");
5712
5743
  }
5713
5744
  if (p.index < 0 || p.index >= pages.length) {
5714
- return fail7(`Invalid tab index: ${p.index}. Valid range: 0-${pages.length - 1}`);
5745
+ return fail9(`Invalid tab index: ${p.index}. Valid range: 0-${pages.length - 1}`);
5715
5746
  }
5716
5747
  const targetPage = pages[p.index];
5717
5748
  await targetPage.bringToFront().catch(() => {
@@ -5963,7 +5994,7 @@ registerCommandDefinition("addinitscript", ["script"]);
5963
5994
 
5964
5995
  // src/commands/find.ts
5965
5996
  import { z as z25 } from "zod";
5966
- import { ok as ok24, fail as fail8, normalizeTips as normalizeTips5 } from "@dyyz1993/xcli-core";
5997
+ import { ok as ok24, fail as fail10, normalizeTips as normalizeTips5 } from "@dyyz1993/xcli-core";
5967
5998
  var actionSchema2 = z25.enum(["click", "fill", "type", "select", "hover", "check"]);
5968
5999
  var findCommand = registerCommand({
5969
6000
  name: "find",
@@ -6004,7 +6035,7 @@ var findCommand = registerCommand({
6004
6035
  });
6005
6036
  const count = await locator.count();
6006
6037
  if (count === 0) {
6007
- return fail8(`No element found with ${p.strategy}="${p.value}"`);
6038
+ return fail10(`No element found with ${p.strategy}="${p.value}"`);
6008
6039
  }
6009
6040
  const tips = [];
6010
6041
  const target = selectTarget(locator, p.strategy);
@@ -6016,15 +6047,15 @@ var findCommand = registerCommand({
6016
6047
  await target.click({ timeout: p.timeout, force: true });
6017
6048
  return okWithTips({ matched: count, selector, action: "click" }, tips);
6018
6049
  } else if (actionName === "fill") {
6019
- if (actionValue === void 0) return fail8("find fill requires a value");
6050
+ if (actionValue === void 0) return fail10("find fill requires a value");
6020
6051
  await target.fill(actionValue, { timeout: p.timeout, force: true });
6021
6052
  return okWithTips({ matched: count, selector, action: `fill("${actionValue}")` }, tips);
6022
6053
  } else if (actionName === "type") {
6023
- if (actionValue === void 0) return fail8("find type requires a value");
6054
+ if (actionValue === void 0) return fail10("find type requires a value");
6024
6055
  await target.type(actionValue, { delay: 10, timeout: p.timeout });
6025
6056
  return okWithTips({ matched: count, selector, action: `type("${actionValue}")` }, tips);
6026
6057
  } else if (actionName === "select") {
6027
- if (actionValue === void 0) return fail8("find select requires a value");
6058
+ if (actionValue === void 0) return fail10("find select requires a value");
6028
6059
  await target.selectOption(actionValue);
6029
6060
  return okWithTips({ matched: count, selector, action: `select("${actionValue}")` }, tips);
6030
6061
  } else if (actionName === "hover") {
@@ -6797,12 +6828,24 @@ var XBrowserPluginLoader = class {
6797
6828
  // src/utils/plugin-singleton.ts
6798
6829
  var pluginLoader = null;
6799
6830
  var pluginsScanned = false;
6831
+ function silenceSchemaWarnings(fn) {
6832
+ if (process.env.XBROWSER_DEBUG || process.env.VITEST_WORKER_ID) return fn();
6833
+ const originalWarn = console.warn;
6834
+ console.warn = (...args) => {
6835
+ const msg = typeof args[0] === "string" ? args[0] : "";
6836
+ if (msg.includes('has no "result" schema')) return;
6837
+ originalWarn(...args);
6838
+ };
6839
+ return fn().finally(() => {
6840
+ console.warn = originalWarn;
6841
+ });
6842
+ }
6800
6843
  async function getPluginLoader() {
6801
6844
  if (!pluginLoader) {
6802
6845
  pluginLoader = new XBrowserPluginLoader();
6803
6846
  }
6804
6847
  if (!pluginsScanned) {
6805
- await pluginLoader.scanAndLoad();
6848
+ await silenceSchemaWarnings(() => pluginLoader.scanAndLoad());
6806
6849
  pluginsScanned = true;
6807
6850
  }
6808
6851
  return pluginLoader;
@@ -7460,7 +7503,7 @@ async function guardCheck(commandName) {
7460
7503
  }
7461
7504
  }
7462
7505
  function errorResult(message) {
7463
- return { ...fail9(message), duration: 0 };
7506
+ return { ...fail11(message), duration: 0 };
7464
7507
  }
7465
7508
  function tipsToMessages(tips) {
7466
7509
  if (!tips || tips.length === 0) return [];
@@ -7764,7 +7807,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7764
7807
  duration,
7765
7808
  timestamp: start
7766
7809
  });
7767
- return { ...fail9(errorMessage), duration };
7810
+ return { ...fail11(errorMessage), duration };
7768
7811
  } finally {
7769
7812
  }
7770
7813
  }
@@ -7805,7 +7848,7 @@ async function executeChain(input, options) {
7805
7848
  results.push({
7806
7849
  command: cmdName,
7807
7850
  raw: cmdStr,
7808
- ...fail9(`Plugin "${cmdName}" requires a sub-command`),
7851
+ ...fail11(`Plugin "${cmdName}" requires a sub-command`),
7809
7852
  duration: 0
7810
7853
  });
7811
7854
  if (type === "and") {
@@ -7824,7 +7867,7 @@ async function executeChain(input, options) {
7824
7867
  results.push({
7825
7868
  command: cmdName,
7826
7869
  raw: cmdStr,
7827
- ...fail9(`Unknown command "${subCommand}" for plugin "${cmdName}"`),
7870
+ ...fail11(`Unknown command "${subCommand}" for plugin "${cmdName}"`),
7828
7871
  duration: 0
7829
7872
  });
7830
7873
  if (type === "and") {
@@ -7956,7 +7999,7 @@ async function executeChain(input, options) {
7956
7999
  results.push({
7957
8000
  command: `${cmdName} ${subCommand}`,
7958
8001
  raw: cmdStr,
7959
- ...fail9(errorMessage),
8002
+ ...fail11(errorMessage),
7960
8003
  duration: duration2
7961
8004
  });
7962
8005
  if (type === "and") {
@@ -8288,8 +8331,14 @@ var configBuiltin = {
8288
8331
  "preview.quality",
8289
8332
  "preview.fps"
8290
8333
  ]);
8291
- if (!knownKeys.has(key) && !key.startsWith("browser.") && !key.startsWith("captcha.") && !key.startsWith("preview.")) {
8292
- console.warn(`\u26A0\uFE0F Unknown config key: "${key}". Known keys: browser.*, captcha.*, preview.*`);
8334
+ const isKnownNamespace = key.startsWith("browser.") || key.startsWith("captcha.") || key.startsWith("preview.");
8335
+ if (!knownKeys.has(key) && !isKnownNamespace) {
8336
+ console.error(
8337
+ `Unknown config key: "${key}".
8338
+ Known namespaces: browser.*, captcha.*, preview.*
8339
+ Run "xbrowser config list" to see current keys.`
8340
+ );
8341
+ process.exit(1);
8293
8342
  }
8294
8343
  setConfigValue(key, value);
8295
8344
  console.log(`Set ${key} = ${value}`);
@@ -8312,7 +8361,7 @@ import {
8312
8361
  readFileSync as readFileSync8,
8313
8362
  writeFileSync as writeFileSync10
8314
8363
  } from "fs";
8315
- import { resolve as resolve8, basename as basename2, dirname as dirname3 } from "path";
8364
+ import { resolve as resolve8, basename as basename2, dirname as dirname4 } from "path";
8316
8365
  import { homedir as homedir7 } from "os";
8317
8366
 
8318
8367
  // src/plugin/install-sources/local.ts
@@ -8570,7 +8619,7 @@ import {
8570
8619
  rmSync as rmSync5,
8571
8620
  cpSync as cpSync5
8572
8621
  } from "fs";
8573
- import { resolve as resolve7, join as join10, dirname as dirname2 } from "path";
8622
+ import { resolve as resolve7, join as join10, dirname as dirname3 } from "path";
8574
8623
  import { tmpdir as tmpdir4 } from "os";
8575
8624
  import { gunzipSync } from "zlib";
8576
8625
  import {
@@ -8636,7 +8685,7 @@ function extractManifestToDir(manifest, targetDir) {
8636
8685
  mkdirSync7(targetDir, { recursive: true });
8637
8686
  for (const file of manifest) {
8638
8687
  const filePath = resolve7(targetDir, file.path);
8639
- mkdirSync7(dirname2(filePath), { recursive: true });
8688
+ mkdirSync7(dirname3(filePath), { recursive: true });
8640
8689
  writeFileSync9(filePath, Buffer.from(file.content, "base64"));
8641
8690
  }
8642
8691
  }
@@ -8885,7 +8934,7 @@ var PluginInstaller = class {
8885
8934
  if (resp.ok) {
8886
8935
  const content2 = await resp.text();
8887
8936
  const dst = resolve8(sharedDir, file);
8888
- const dstDir = dirname3(dst);
8937
+ const dstDir = dirname4(dst);
8889
8938
  if (!existsSync10(dstDir)) mkdirSync8(dstDir, { recursive: true });
8890
8939
  writeFileSync10(dst, content2, "utf-8");
8891
8940
  console.log(`\u2705 Downloaded shared/${file} from GitHub`);
@@ -8910,7 +8959,7 @@ var PluginInstaller = class {
8910
8959
  const dst = resolve8(sharedDir, file);
8911
8960
  if (existsSync10(src)) {
8912
8961
  try {
8913
- cpSync6(dirname3(src), dirname3(dst), { recursive: true });
8962
+ cpSync6(dirname4(src), dirname4(dst), { recursive: true });
8914
8963
  console.log(`\u2705 Copied shared/${file} for plugin "${basename2(pluginDir)}"`);
8915
8964
  } catch {
8916
8965
  try {
@@ -9053,7 +9102,7 @@ function outputResult(result, mode = "text") {
9053
9102
  function outputError(message) {
9054
9103
  const formatted = formatter.formatError(message, { color: true, emoji: false });
9055
9104
  console.error(formatted);
9056
- process.exit(1);
9105
+ process.exitCode = 1;
9057
9106
  }
9058
9107
  function outputEnvelope(result, meta, mode) {
9059
9108
  if (mode !== "json" && mode !== "yaml") {
@@ -10528,7 +10577,6 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
10528
10577
  break;
10529
10578
  }
10530
10579
  case "scrape":
10531
- if (!args[0]) outputError("Usage: xbrowser scrape <url> [--format markdown|html|text] [--mode raw|clean|compact] [--selector <sel>] [--timeout <ms>]");
10532
10580
  cmdName = "scrape";
10533
10581
  params = {
10534
10582
  url: args[0],
@@ -10540,7 +10588,6 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
10540
10588
  };
10541
10589
  break;
10542
10590
  case "map":
10543
- if (!args[0]) outputError("Usage: xbrowser map <url> [--search <query>] [--sitemap include|only] [--include-subdomains] [--limit <n>]");
10544
10591
  cmdName = "map";
10545
10592
  params = {
10546
10593
  url: args[0],
@@ -10675,12 +10722,20 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
10675
10722
  }
10676
10723
  }
10677
10724
  const outputFile = options.output;
10678
- if (outputFile && result.success && result.data) {
10679
- const { writeFileSync: writeFileSync13 } = await import("fs");
10680
- const content = typeof result.data === "string" ? result.data : result.data.content || result.data.text || JSON.stringify(result.data, null, 2);
10681
- writeFileSync13(outputFile, content, "utf-8");
10682
- console.log(`
10725
+ const dataObj = result.data;
10726
+ const handlerAlreadyWrote = !!dataObj && typeof dataObj.output === "string";
10727
+ if (outputFile && result.success && result.data && !handlerAlreadyWrote) {
10728
+ const { writeFileSync: writeFileSync13, mkdirSync: mkdirSync11 } = await import("fs");
10729
+ const { dirname: dirname7 } = await import("path");
10730
+ const content = typeof result.data === "string" ? result.data : dataObj.content || dataObj.text || JSON.stringify(result.data, null, 2);
10731
+ try {
10732
+ mkdirSync11(dirname7(outputFile), { recursive: true });
10733
+ writeFileSync13(outputFile, content, "utf-8");
10734
+ console.log(`
10683
10735
  \u{1F4C4} Written to ${outputFile}`);
10736
+ } catch (err) {
10737
+ outputError(`Failed to write --output "${outputFile}": ${err instanceof Error ? err.message : String(err)}`);
10738
+ }
10684
10739
  }
10685
10740
  }
10686
10741
 
@@ -11146,7 +11201,11 @@ Total: ${enrichedPlugins.length} plugins`);
11146
11201
  case "login":
11147
11202
  case "whoami":
11148
11203
  case "logout":
11149
- outputError(`"${sub}" has moved to the marketplace plugin. Use: xbrowser marketplace ${sub}`);
11204
+ outputError(
11205
+ `"plugin ${sub}" is no longer available: the built-in marketplace publisher was removed.
11206
+ To publish a plugin, use npm directly: \`npm publish\` from the plugin directory.
11207
+ See docs/plugin-guide.md for the publishing workflow.`
11208
+ );
11150
11209
  break;
11151
11210
  default:
11152
11211
  console.log(handlePluginHelp());
@@ -11507,23 +11566,51 @@ async function handleConvert(args, _mode) {
11507
11566
  const path5 = await import("path");
11508
11567
  const { default: yaml3 } = await import("yaml");
11509
11568
  const { generateJSScript: generateJSScript2, generatePythonScript: generatePythonScript2, generateBashScript: generateBashScript2 } = await import("./convert-R3XXYKC6.js");
11510
- const content = fs6.readFileSync(filePath, "utf-8");
11511
- const recording = yaml3.parse(content);
11512
- if (recording.actions && !recording.events) recording.events = recording.actions;
11569
+ let recording;
11570
+ try {
11571
+ const content = fs6.readFileSync(filePath, "utf-8");
11572
+ recording = yaml3.parse(content);
11573
+ } catch (e) {
11574
+ console.error(`Error: Failed to read "${filePath}": ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
11575
+ process.exit(1);
11576
+ }
11577
+ if (recording === null || typeof recording !== "object" || Array.isArray(recording)) {
11578
+ console.error(`Error: "${filePath}" does not contain a valid recording (expected a YAML/JSON object with events or actions).`);
11579
+ process.exit(1);
11580
+ }
11581
+ const rawActions = recording.actions;
11582
+ if (Array.isArray(rawActions) && !Array.isArray(recording.events)) {
11583
+ recording.events = rawActions.map((a) => {
11584
+ const action = a;
11585
+ const element = action.element ?? {};
11586
+ return {
11587
+ type: action.type,
11588
+ selector: element.selector ?? action.selector,
11589
+ data: {
11590
+ ...action.data,
11591
+ value: action.value ?? action.data?.value,
11592
+ key: action.key ?? action.data?.key,
11593
+ x: action.scrollX ?? action.data?.x,
11594
+ y: action.scrollY ?? action.data?.y
11595
+ }
11596
+ };
11597
+ });
11598
+ }
11513
11599
  const ext = path5.extname(outputPath).toLowerCase();
11600
+ const recordingTyped = recording;
11514
11601
  let script;
11515
11602
  if (ext === ".py") {
11516
- script = generatePythonScript2(recording);
11603
+ script = generatePythonScript2(recordingTyped);
11517
11604
  } else if (ext === ".sh") {
11518
- script = generateBashScript2(recording);
11605
+ script = generateBashScript2(recordingTyped);
11519
11606
  } else {
11520
- script = generateJSScript2(recording);
11607
+ script = generateJSScript2(recordingTyped);
11521
11608
  }
11522
11609
  fs6.writeFileSync(outputPath, script);
11523
11610
  fs6.chmodSync(outputPath, 493);
11524
- const eventCount = (recording.events || recording.actions || []).length;
11611
+ const eventCount = (recordingTyped.events || []).length;
11525
11612
  console.log(`Converted ${filePath} -> ${outputPath}`);
11526
- console.log(` Events: ${eventCount}, Start URL: ${recording.startUrl}`);
11613
+ console.log(` Events: ${eventCount}, Start URL: ${recordingTyped.startUrl}`);
11527
11614
  console.log(` Run: ${ext === ".py" ? "python" : ext === ".sh" ? "./" : "node"} ${outputPath}`);
11528
11615
  }
11529
11616
  async function handleExtract(args, _mode) {
@@ -11532,11 +11619,16 @@ async function handleExtract(args, _mode) {
11532
11619
  console.error("Usage: xbrowser extract <recording.yaml>");
11533
11620
  process.exit(1);
11534
11621
  }
11535
- const { extractAndSave: extractAndSave2, printExtractSummary: printExtractSummary2 } = await import("./extract-RM62AJXW.js");
11536
- const { summary, outputPath } = extractAndSave2(filePath);
11537
- printExtractSummary2(summary);
11538
- console.log(`
11622
+ const { extractAndSave: extractAndSave2, printExtractSummary: printExtractSummary2 } = await import("./extract-DSU2RVMN.js");
11623
+ try {
11624
+ const { summary, outputPath } = extractAndSave2(filePath);
11625
+ printExtractSummary2(summary);
11626
+ console.log(`
11539
11627
  Saved LLM summary: ${outputPath}`);
11628
+ } catch (e) {
11629
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
11630
+ process.exit(1);
11631
+ }
11540
11632
  }
11541
11633
  async function handleFilter(args, _mode, options) {
11542
11634
  const filePath = args[0];
@@ -11545,16 +11637,21 @@ async function handleFilter(args, _mode, options) {
11545
11637
  console.error("Usage: xbrowser filter <input.yaml> <output.yaml> [--exclude type1,type2]");
11546
11638
  process.exit(1);
11547
11639
  }
11548
- const { filterRecording: filterRecording2, parseExcludeTypes: parseExcludeTypes2 } = await import("./filter-K6FGRJQU.js");
11640
+ const { filterRecording: filterRecording2, parseExcludeTypes: parseExcludeTypes2 } = await import("./filter-3DXC6432.js");
11549
11641
  const excludeArgs = args.slice(2).concat(
11550
11642
  Object.entries(options || {}).flatMap(
11551
11643
  ([k, v]) => k.startsWith("exclude") ? [`--${k}${typeof v === "string" ? "=" + v : ""}`] : []
11552
11644
  )
11553
11645
  );
11554
11646
  const excludeTypes = parseExcludeTypes2(excludeArgs);
11555
- const result = filterRecording2(filePath, outputPath, excludeTypes);
11556
- console.log(`Filtered ${filePath} -> ${outputPath}`);
11557
- console.log(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)`);
11647
+ try {
11648
+ const result = filterRecording2(filePath, outputPath, excludeTypes);
11649
+ console.log(`Filtered ${filePath} -> ${outputPath}`);
11650
+ console.log(` Original: ${result.originalCount}, After: ${result.filteredCount}, Removed: ${result.removed} (${result.percentage}%)`);
11651
+ } catch (e) {
11652
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
11653
+ process.exit(1);
11654
+ }
11558
11655
  }
11559
11656
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
11560
11657
  const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-KPU4YQAE.js");
@@ -11725,8 +11822,21 @@ export default createSite({
11725
11822
  url: '${data.startUrl}',
11726
11823
  detect: async (ctx: CommandContext) => {
11727
11824
  const page = ensurePage(ctx);
11728
- // TODO: Add login detection logic
11729
- return false;
11825
+ // Best-effort login detection: check for common logged-in indicators.
11826
+ // Customize these selectors for your site's specific login state.
11827
+ try {
11828
+ const loggedIn = await page.evaluate(() => {
11829
+ const el = document.querySelector(
11830
+ '[class*="avatar"], [class*="user-info"], [class*="logged-in"], ' +
11831
+ '[data-testid*="user"], [data-testid*="avatar"], ' +
11832
+ 'a[href*="logout"], button[class*="logout"]'
11833
+ );
11834
+ return !!el;
11835
+ });
11836
+ return loggedIn;
11837
+ } catch {
11838
+ return false;
11839
+ }
11730
11840
  },
11731
11841
  },
11732
11842
 
@@ -11753,7 +11863,7 @@ async function handleRun(filePath, options) {
11753
11863
  outputError("No commands found in file");
11754
11864
  return;
11755
11865
  }
11756
- const chain = commands.join(" && ");
11866
+ const chain = commands.join(" ; ");
11757
11867
  const chainResult = await executeChain(chain, {
11758
11868
  cdpEndpoint: options?.cdpEndpoint,
11759
11869
  sessionName: options?.sessionName,
@@ -13088,6 +13198,7 @@ async function handleChainInput(input, argv) {
13088
13198
  }
13089
13199
  async function routeCommand(argvIn, stdinCommands) {
13090
13200
  let argv = argvIn;
13201
+ process.exitCode = 0;
13091
13202
  try {
13092
13203
  if (stdinCommands && stdinCommands.length > 0) {
13093
13204
  await handleStdinMode(stdinCommands, argv);
@@ -13485,6 +13596,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13485
13596
  const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
13486
13597
  const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
13487
13598
  if (result && result.success === false && resultData?.code === "LOGIN_REQUIRED") {
13599
+ process.exitCode = 1;
13488
13600
  outputLoginRequired(result, mode);
13489
13601
  return;
13490
13602
  }
@@ -13556,6 +13668,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13556
13668
  sessionName
13557
13669
  });
13558
13670
  if (!loginGuard.ok) {
13671
+ process.exitCode = 1;
13559
13672
  const result2 = {
13560
13673
  success: false,
13561
13674
  data: loginGuard.data ?? null,
@@ -13599,8 +13712,9 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13599
13712
  }
13600
13713
  const outputData = isCommandResult2(result) ? result.data : result && typeof result === "object" ? result.data ?? result : result;
13601
13714
  const tips = isCommandResult2(result) ? result.tips : result && typeof result === "object" ? result.tips : void 0;
13715
+ const resultSuccess = isCommandResult2(result) ? result.success !== false : true;
13716
+ if (!resultSuccess) process.exitCode = 1;
13602
13717
  if (mode === "json" || mode === "yaml") {
13603
- const resultSuccess = isCommandResult2(result) ? result.success !== false : true;
13604
13718
  const resultMsg = isCommandResult2(result) ? result.message : void 0;
13605
13719
  const duration = Date.now() - cmdStart;
13606
13720
  const envelopeMeta = { command: `${command} ${subCommand}` };