@xbrowser/cli 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # xbrowser
2
2
 
3
- > Self-contained browser automation CLI navigate, click, fill forms, screenshot, web scraping, record & replay. A Playwright/Puppeteer alternative for CLI-first workflows.
3
+ > **Browser automation CLI** for web scraping, headless browsing, SEO analysis, and AI agent workflows. 35+ commands, 67+ plugins. A command-line alternative to Playwright, Puppeteer, and Selenium **no code required**.
4
4
 
5
5
  [![CI Status](https://github.com/dyyz1993/xbrowser/workflows/CI/badge.svg)](https://github.com/dyyz1993/xbrowser/actions)
6
6
  [![codecov](https://codecov.io/gh/dyyz1993/xbrowser/branch/master/graph/badge.svg)](https://codecov.io/gh/dyyz1993/xbrowser)
@@ -18,6 +18,42 @@
18
18
  - **会话管理** — 多会话并行,独立上下文
19
19
  - **Daemon 模式** — 后台常驻,快速响应
20
20
 
21
+ ## Use Cases
22
+
23
+ **Web Scraping & Data Extraction**
24
+ ```bash
25
+ # Scrape any page to Markdown — no Playwright scripts needed
26
+ xbrowser scrape https://news.ycombinator.com --mode smart --output markdown
27
+
28
+ # Crawl an entire site
29
+ xbrowser crawl https://example.com --max-depth 3 --output json
30
+ ```
31
+
32
+ **Search Engine Automation**
33
+ ```bash
34
+ # Search Google, Bing, Baidu from the command line
35
+ xbrowser search "best headless browser CLI" --engine google --limit 10 --json
36
+
37
+ # Time-filtered search
38
+ xbrowser search "playwright alternative 2025" --engine bing --time-filter month
39
+ ```
40
+
41
+ **Browser Automation for AI Agents**
42
+ ```bash
43
+ # Record browser actions, replay in CI/CD
44
+ xbrowser record --output workflow.yaml
45
+ xbrowser replay workflow.yaml
46
+
47
+ # Chain commands for complex workflows
48
+ xbrowser "goto https://example.com && wait .content && text --selector .content"
49
+ ```
50
+
51
+ **SEO & Backlink Automation**
52
+ ```bash
53
+ # Auto-publish articles to 13+ SEO platforms
54
+ xbrowser publish --platform devto,juejin --file article.md
55
+ ```
56
+
21
57
  ## 快速开始
22
58
 
23
59
  ### 安装
@@ -841,6 +877,32 @@ xbrowser/
841
877
  └── docs/ # 文档
842
878
  ```
843
879
 
880
+ ## xbrowser vs Other Browser Automation Tools
881
+
882
+ | Feature | xbrowser | Playwright | Puppeteer | Selenium |
883
+ |---------|----------|------------|-----------|----------|
884
+ | **Interface** | CLI (command line) | Node.js library | Node.js library | Multi-language library |
885
+ | **Setup** | `npm i -g` — 0 config | Install + browser download | Install + browser download | Install + WebDriver + drivers |
886
+ | **Web Scraping** | Built-in (`scrape`, `crawl`, `map`) | Write custom scripts | Write custom scripts | Write custom scripts |
887
+ | **Search** | Built-in multi-engine (`search`) | No | No | No |
888
+ | **Plugin Ecosystem** | 67+ plugins | Limited | Limited | No |
889
+ | **No Code Required** | ✅ CLI commands | ❌ Must write JS/TS | ❌ Must write JS/TS | ❌ Must write code |
890
+ | **Headless Mode** | ✅ Default | ✅ | ✅ | ✅ |
891
+ | **Record/Replay** | ✅ Built-in (`record`/`replay`) | ✅ Codegen | ❌ | ❌ |
892
+ | **Anti-Detection** | ✅ CDP firewall | ❌ | ❌ | ❌ |
893
+ | **AI Agent Integration** | ✅ Built-in plugins | Manual integration | Manual integration | Manual integration |
894
+
895
+ **When to use xbrowser:**
896
+ - You want to **automate browsers from the command line** without writing code
897
+ - You need **web scraping, SEO automation, or AI agent workflows**
898
+ - You want a **Playwright/Puppeteer alternative** that works with one-liners
899
+ - You're building **CLI tools that need browser automation**
900
+
901
+ **When to use Playwright/Puppeteer:**
902
+ - You're writing **test suites** for web applications
903
+ - You need **fine-grained programmatic control** of browser behavior
904
+ - You're building a **Node.js application** that embeds browser automation
905
+
844
906
  ## 与相关项目的关系
845
907
 
846
908
  | 项目 | 定位 | 关系 |
package/dist/cli.js CHANGED
@@ -132,7 +132,18 @@ function parsePluginParams(args, schema, base = {}) {
132
132
  if (value === "true") params[key] = true;
133
133
  else if (value === "false") params[key] = false;
134
134
  else if (/^\d+$/.test(value)) params[key] = parseInt(value, 10);
135
- else params[key] = value;
135
+ else {
136
+ try {
137
+ const parsed = JSON.parse(value);
138
+ if (Array.isArray(parsed) || typeof parsed === "object" && parsed !== null) {
139
+ params[key] = parsed;
140
+ } else {
141
+ params[key] = value;
142
+ }
143
+ } catch {
144
+ params[key] = value;
145
+ }
146
+ }
136
147
  i++;
137
148
  } else {
138
149
  params[key] = true;
@@ -6225,8 +6236,48 @@ async function loadHooks() {
6225
6236
  // src/executor.ts
6226
6237
  import { homedir as homedir3 } from "os";
6227
6238
  import { join as join3 } from "path";
6239
+ import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5, mkdirSync as mkdirSync3 } from "fs";
6228
6240
  var NAVIGATION_COMMANDS = /* @__PURE__ */ new Set(["goto", "back", "forward", "refresh"]);
6229
6241
  var snapshotHintShown = /* @__PURE__ */ new WeakSet();
6242
+ var STORAGE_DIR = join3(homedir3(), ".xbrowser", "storage");
6243
+ var storageCache = /* @__PURE__ */ new Map();
6244
+ function getPluginStorage(pluginName) {
6245
+ if (!storageCache.has(pluginName)) {
6246
+ const filePath = join3(STORAGE_DIR, `${pluginName}.json`);
6247
+ let data = {};
6248
+ const load3 = () => {
6249
+ if (existsSync6(filePath)) {
6250
+ try {
6251
+ data = JSON.parse(readFileSync10(filePath, "utf-8"));
6252
+ } catch {
6253
+ data = {};
6254
+ }
6255
+ }
6256
+ };
6257
+ const save = () => {
6258
+ mkdirSync3(STORAGE_DIR, { recursive: true });
6259
+ writeFileSync5(filePath, JSON.stringify(data, null, 2), "utf-8");
6260
+ };
6261
+ load3();
6262
+ storageCache.set(pluginName, {
6263
+ get: async (key) => data[key] ?? null,
6264
+ set: async (key, value) => {
6265
+ data[key] = value;
6266
+ save();
6267
+ },
6268
+ delete: async (key) => {
6269
+ delete data[key];
6270
+ save();
6271
+ },
6272
+ clear: async () => {
6273
+ data = {};
6274
+ save();
6275
+ },
6276
+ keys: async () => Object.keys(data)
6277
+ });
6278
+ }
6279
+ return storageCache.get(pluginName);
6280
+ }
6230
6281
  var archiveInitialized = false;
6231
6282
  function ensureArchiveInit() {
6232
6283
  if (!archiveInitialized) {
@@ -6334,16 +6385,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
6334
6385
  args: [],
6335
6386
  options: {},
6336
6387
  cwd: process.cwd(),
6337
- storage: {
6338
- get: async () => null,
6339
- set: async () => {
6340
- },
6341
- delete: async () => {
6342
- },
6343
- clear: async () => {
6344
- },
6345
- keys: async () => []
6346
- },
6388
+ storage: getPluginStorage(commandName),
6347
6389
  output: {
6348
6390
  mode: "text",
6349
6391
  showTips: false,
@@ -6566,16 +6608,7 @@ async function executeChain(input, options) {
6566
6608
  browser: session.context.browser(),
6567
6609
  browserContext: session.context,
6568
6610
  sessionId: session.id,
6569
- storage: {
6570
- get: async (_key) => null,
6571
- set: async (_key, _value) => {
6572
- },
6573
- delete: async (_key) => {
6574
- },
6575
- clear: async () => {
6576
- },
6577
- keys: async () => []
6578
- },
6611
+ storage: getPluginStorage(cmdName),
6579
6612
  output: { mode: "text", showTips: false, color: false, emoji: false },
6580
6613
  error: (msg) => {
6581
6614
  throw new Error(msg);
@@ -6586,8 +6619,21 @@ async function executeChain(input, options) {
6586
6619
  };
6587
6620
  const start2 = Date.now();
6588
6621
  try {
6622
+ const hooks = await loadHooks();
6623
+ if (hooks.length > 0) {
6624
+ await Promise.all(hooks.map((h) => h.onBeforeCommand?.({ page: session.page, command: `${cmdName} ${subCommand}`, params: pluginParams })));
6625
+ }
6589
6626
  const raw = await cmdEntry.handler(pluginParams, pluginCtx);
6590
6627
  const duration2 = Date.now() - start2;
6628
+ let hookOutputs;
6629
+ if (hooks.length > 0) {
6630
+ const outputs = [];
6631
+ for (const h of hooks) {
6632
+ const output = await h.onAfterCommand?.({ page: session.page, command: `${cmdName} ${subCommand}`, params: pluginParams, result: raw, duration: duration2 });
6633
+ if (output) outputs.push({ _hook: h.name, ...output });
6634
+ }
6635
+ if (outputs.length > 0) hookOutputs = outputs;
6636
+ }
6591
6637
  const data = raw?.data ?? raw;
6592
6638
  recordArchive(session.id, sessionName, {
6593
6639
  step: results.length,
@@ -6602,7 +6648,8 @@ async function executeChain(input, options) {
6602
6648
  command: `${cmdName} ${subCommand}`,
6603
6649
  raw: cmdStr,
6604
6650
  ...ok25(data),
6605
- duration: duration2
6651
+ duration: duration2,
6652
+ ...hookOutputs ? { hookOutputs } : {}
6606
6653
  });
6607
6654
  if (type === "or") {
6608
6655
  return {
@@ -6666,7 +6713,8 @@ async function executeChain(input, options) {
6666
6713
  data: result.data,
6667
6714
  message: result.message,
6668
6715
  duration,
6669
- tips: result.tips
6716
+ tips: result.tips,
6717
+ ...result.hookOutputs ? { hookOutputs: result.hookOutputs } : {}
6670
6718
  };
6671
6719
  results.push(stepResult);
6672
6720
  if (type === "and" && !result.success) {
@@ -6853,7 +6901,7 @@ var sessionKillBuiltin = {
6853
6901
  };
6854
6902
 
6855
6903
  // src/config.ts
6856
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, writeFileSync as writeFileSync5 } from "fs";
6904
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
6857
6905
  import { join as join4 } from "path";
6858
6906
  import { homedir as homedir4, tmpdir } from "os";
6859
6907
  function getConfigFile() {
@@ -6861,14 +6909,14 @@ function getConfigFile() {
6861
6909
  }
6862
6910
  function loadConfig() {
6863
6911
  const configFile = getConfigFile();
6864
- if (!existsSync6(configFile)) return {};
6912
+ if (!existsSync7(configFile)) return {};
6865
6913
  return readJsonFile(configFile, {});
6866
6914
  }
6867
6915
  function saveConfig(config) {
6868
6916
  const dir = join4(homedir4() || tmpdir(), ".xbrowser");
6869
6917
  const configFile = getConfigFile();
6870
- if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
6871
- writeFileSync5(configFile, JSON.stringify(config, null, 2), "utf-8");
6918
+ if (!existsSync7(dir)) mkdirSync4(dir, { recursive: true });
6919
+ writeFileSync6(configFile, JSON.stringify(config, null, 2), "utf-8");
6872
6920
  }
6873
6921
  function getConfigValue(key) {
6874
6922
  return loadConfig()[key];
@@ -6976,26 +7024,26 @@ var configBuiltin = {
6976
7024
 
6977
7025
  // src/plugin/installer.ts
6978
7026
  import {
6979
- existsSync as existsSync13,
7027
+ existsSync as existsSync14,
6980
7028
  readdirSync as readdirSync3,
6981
- mkdirSync as mkdirSync8,
7029
+ mkdirSync as mkdirSync9,
6982
7030
  rmSync as rmSync7
6983
7031
  } from "fs";
6984
7032
  import { resolve as resolve15, basename as basename2 } from "path";
6985
7033
  import { homedir as homedir5 } from "os";
6986
7034
 
6987
7035
  // src/plugin/install-sources/local.ts
6988
- import { existsSync as existsSync8, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
7036
+ import { existsSync as existsSync9, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
6989
7037
  import { resolve as resolve10 } from "path";
6990
7038
 
6991
7039
  // src/plugin/install-utils.ts
6992
7040
  import {
6993
- existsSync as existsSync7,
7041
+ existsSync as existsSync8,
6994
7042
  readdirSync as readdirSync2,
6995
7043
  cpSync,
6996
7044
  rmSync,
6997
- mkdirSync as mkdirSync4,
6998
- readFileSync as readFileSync10,
7045
+ mkdirSync as mkdirSync5,
7046
+ readFileSync as readFileSync11,
6999
7047
  createWriteStream
7000
7048
  } from "fs";
7001
7049
  import { resolve as resolve9 } from "path";
@@ -7066,7 +7114,7 @@ async function downloadToFile(url, destPath) {
7066
7114
  await pipeline(nodeStream, createWriteStream(destPath));
7067
7115
  }
7068
7116
  function extractTarGz(tarballPath, targetDir) {
7069
- mkdirSync4(targetDir, { recursive: true });
7117
+ mkdirSync5(targetDir, { recursive: true });
7070
7118
  execSync7(`tar -xzf "${tarballPath}" -C "${targetDir}"`, { stdio: "pipe" });
7071
7119
  }
7072
7120
  function flattenPackageRoot(targetDir) {
@@ -7087,18 +7135,18 @@ function flattenPackageRoot(targetDir) {
7087
7135
  async function verifyPlugin(dir) {
7088
7136
  const warnings = [];
7089
7137
  const indexPath = resolve9(dir, "index.ts");
7090
- if (!existsSync7(indexPath)) {
7138
+ if (!existsSync8(indexPath)) {
7091
7139
  const indexJs = resolve9(dir, "index.js");
7092
- if (!existsSync7(indexJs)) {
7140
+ if (!existsSync8(indexJs)) {
7093
7141
  return { valid: false, error: "No index.ts or index.js entry point found", warnings };
7094
7142
  }
7095
7143
  }
7096
7144
  const pkgPath = resolve9(dir, "package.json");
7097
- if (!existsSync7(pkgPath)) {
7145
+ if (!existsSync8(pkgPath)) {
7098
7146
  warnings.push("No package.json found");
7099
7147
  } else {
7100
7148
  try {
7101
- const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
7149
+ const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7102
7150
  if (!pkg2.xbrowser) {
7103
7151
  warnings.push("No xbrowser metadata in package.json");
7104
7152
  }
@@ -7118,7 +7166,7 @@ function safeCleanup(dir) {
7118
7166
  // src/plugin/install-sources/local.ts
7119
7167
  async function installFromLocal(source, name, targetDir) {
7120
7168
  const srcPath = resolve10(source);
7121
- if (!existsSync8(srcPath)) {
7169
+ if (!existsSync9(srcPath)) {
7122
7170
  throw new Error(`Local path does not exist: ${srcPath}`);
7123
7171
  }
7124
7172
  const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
@@ -7131,7 +7179,7 @@ async function installFromLocal(source, name, targetDir) {
7131
7179
  safeCleanup(tmpTarget);
7132
7180
  throw new Error(`Invalid plugin: ${verify.error}`);
7133
7181
  }
7134
- if (existsSync8(targetDir)) {
7182
+ if (existsSync9(targetDir)) {
7135
7183
  rmSync2(targetDir, { recursive: true, force: true });
7136
7184
  }
7137
7185
  cpSync2(tmpTarget, targetDir, { recursive: true, force: true });
@@ -7151,7 +7199,7 @@ async function installFromLocal(source, name, targetDir) {
7151
7199
  }
7152
7200
 
7153
7201
  // src/plugin/install-sources/npm.ts
7154
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync6, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
7202
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, writeFileSync as writeFileSync7, rmSync as rmSync3, cpSync as cpSync3 } from "fs";
7155
7203
  import { resolve as resolve11, join as join5 } from "path";
7156
7204
  import { tmpdir as tmpdir2 } from "os";
7157
7205
  async function installFromNpm(packageName, name, targetDir) {
@@ -7172,7 +7220,7 @@ async function installFromNpm(packageName, name, targetDir) {
7172
7220
  }
7173
7221
  const tarballUrl = versionMeta.dist.tarball;
7174
7222
  const tmpDir = join5(tmpdir2(), `xbrowser-npm-${Date.now()}`);
7175
- mkdirSync5(tmpDir, { recursive: true });
7223
+ mkdirSync6(tmpDir, { recursive: true });
7176
7224
  let warnings = [];
7177
7225
  try {
7178
7226
  const tarballPath = join5(tmpDir, `${name}.tgz`);
@@ -7185,16 +7233,16 @@ async function installFromNpm(packageName, name, targetDir) {
7185
7233
  if (!verify.valid) {
7186
7234
  throw new Error(`Invalid npm plugin: ${verify.error}`);
7187
7235
  }
7188
- if (existsSync9(targetDir)) {
7236
+ if (existsSync10(targetDir)) {
7189
7237
  rmSync3(targetDir, { recursive: true, force: true });
7190
7238
  }
7191
7239
  cpSync3(extractDir, targetDir, { recursive: true, force: true });
7192
7240
  const pkgPath = resolve11(targetDir, "package.json");
7193
- if (existsSync9(pkgPath)) {
7194
- const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7241
+ if (existsSync10(pkgPath)) {
7242
+ const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7195
7243
  if (!pkg2._npmSource) {
7196
7244
  pkg2._npmSource = { name: packageName, version: latestVersion };
7197
- writeFileSync6(pkgPath, JSON.stringify(pkg2, null, 2));
7245
+ writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
7198
7246
  }
7199
7247
  }
7200
7248
  } finally {
@@ -7211,7 +7259,7 @@ async function installFromNpm(packageName, name, targetDir) {
7211
7259
  }
7212
7260
 
7213
7261
  // src/plugin/install-sources/git.ts
7214
- import { existsSync as existsSync10, readFileSync as readFileSync12, writeFileSync as writeFileSync7, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
7262
+ import { existsSync as existsSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8, rmSync as rmSync4, cpSync as cpSync4 } from "fs";
7215
7263
  import { resolve as resolve12, join as join6 } from "path";
7216
7264
  import { tmpdir as tmpdir3 } from "os";
7217
7265
  import { execSync as execSync8 } from "child_process";
@@ -7225,17 +7273,17 @@ async function installFromGit(gitUrl, name, targetDir) {
7225
7273
  if (!verify.valid) {
7226
7274
  throw new Error(`Invalid git plugin: ${verify.error}`);
7227
7275
  }
7228
- if (existsSync10(targetDir)) {
7276
+ if (existsSync11(targetDir)) {
7229
7277
  rmSync4(targetDir, { recursive: true, force: true });
7230
7278
  }
7231
7279
  cpSync4(tmpDir, targetDir, { recursive: true, force: true });
7232
7280
  rmSync4(resolve12(targetDir, ".git"), { recursive: true, force: true });
7233
7281
  const pkgPath = resolve12(targetDir, "package.json");
7234
- if (existsSync10(pkgPath)) {
7235
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7282
+ if (existsSync11(pkgPath)) {
7283
+ const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
7236
7284
  if (!pkg2._gitSource) {
7237
7285
  pkg2._gitSource = { url: gitUrl };
7238
- writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
7286
+ writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
7239
7287
  }
7240
7288
  }
7241
7289
  } finally {
@@ -7252,12 +7300,12 @@ async function installFromGit(gitUrl, name, targetDir) {
7252
7300
  }
7253
7301
 
7254
7302
  // src/plugin/install-sources/url.ts
7255
- import { existsSync as existsSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync8, mkdirSync as mkdirSync6, rmSync as rmSync5, cpSync as cpSync5 } from "fs";
7303
+ import { existsSync as existsSync12, readFileSync as readFileSync14, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, rmSync as rmSync5, cpSync as cpSync5 } from "fs";
7256
7304
  import { resolve as resolve13, join as join7, basename } from "path";
7257
7305
  import { tmpdir as tmpdir4 } from "os";
7258
7306
  async function installFromUrl(url, name, targetDir) {
7259
7307
  const tmpDir = join7(tmpdir4(), `xbrowser-url-${Date.now()}`);
7260
- mkdirSync6(tmpDir, { recursive: true });
7308
+ mkdirSync7(tmpDir, { recursive: true });
7261
7309
  let warnings = [];
7262
7310
  try {
7263
7311
  const fileName = basename(new URL(url).pathname) || "plugin.tar.gz";
@@ -7271,16 +7319,16 @@ async function installFromUrl(url, name, targetDir) {
7271
7319
  if (!verify.valid) {
7272
7320
  throw new Error(`Invalid plugin from URL: ${verify.error}`);
7273
7321
  }
7274
- if (existsSync11(targetDir)) {
7322
+ if (existsSync12(targetDir)) {
7275
7323
  rmSync5(targetDir, { recursive: true, force: true });
7276
7324
  }
7277
7325
  cpSync5(extractDir, targetDir, { recursive: true, force: true });
7278
7326
  const pkgPath = resolve13(targetDir, "package.json");
7279
- if (existsSync11(pkgPath)) {
7280
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
7327
+ if (existsSync12(pkgPath)) {
7328
+ const pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
7281
7329
  if (!pkg2._urlSource) {
7282
7330
  pkg2._urlSource = { url };
7283
- writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
7331
+ writeFileSync9(pkgPath, JSON.stringify(pkg2, null, 2));
7284
7332
  }
7285
7333
  }
7286
7334
  } finally {
@@ -7298,10 +7346,10 @@ async function installFromUrl(url, name, targetDir) {
7298
7346
 
7299
7347
  // src/plugin/install-sources/marketplace.ts
7300
7348
  import {
7301
- existsSync as existsSync12,
7302
- mkdirSync as mkdirSync7,
7303
- writeFileSync as writeFileSync9,
7304
- readFileSync as readFileSync14,
7349
+ existsSync as existsSync13,
7350
+ mkdirSync as mkdirSync8,
7351
+ writeFileSync as writeFileSync10,
7352
+ readFileSync as readFileSync15,
7305
7353
  rmSync as rmSync6,
7306
7354
  cpSync as cpSync6
7307
7355
  } from "fs";
@@ -7323,12 +7371,12 @@ async function installFromMarketplace(pluginsDir, slug, options) {
7323
7371
  const plugin = detailData.data;
7324
7372
  const name = options?.name || String(plugin.slug || slug);
7325
7373
  const targetDir = resolve14(pluginsDir, name);
7326
- if (existsSync12(targetDir) && !options?.force) {
7374
+ if (existsSync13(targetDir) && !options?.force) {
7327
7375
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
7328
7376
  }
7329
- mkdirSync7(targetDir, { recursive: true });
7377
+ mkdirSync8(targetDir, { recursive: true });
7330
7378
  const tmpDir = join8(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
7331
- mkdirSync7(tmpDir, { recursive: true });
7379
+ mkdirSync8(tmpDir, { recursive: true });
7332
7380
  const realSlug = String(plugin.slug || slug);
7333
7381
  try {
7334
7382
  await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
@@ -7358,14 +7406,14 @@ function isManifestArray(data) {
7358
7406
  return Array.isArray(data) && data.length > 0 && typeof data[0].path === "string" && typeof data[0].content === "string";
7359
7407
  }
7360
7408
  function extractManifestToDir(manifest, targetDir) {
7361
- if (existsSync12(targetDir)) {
7409
+ if (existsSync13(targetDir)) {
7362
7410
  rmSync6(targetDir, { recursive: true, force: true });
7363
7411
  }
7364
- mkdirSync7(targetDir, { recursive: true });
7412
+ mkdirSync8(targetDir, { recursive: true });
7365
7413
  for (const file of manifest) {
7366
7414
  const filePath = resolve14(targetDir, file.path);
7367
- mkdirSync7(dirname2(filePath), { recursive: true });
7368
- writeFileSync9(filePath, Buffer.from(file.content, "base64"));
7415
+ mkdirSync8(dirname2(filePath), { recursive: true });
7416
+ writeFileSync10(filePath, Buffer.from(file.content, "base64"));
7369
7417
  }
7370
7418
  }
7371
7419
  function tryParseAsGzippedManifest(buffer) {
@@ -7392,7 +7440,7 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7392
7440
  const redirectUrl = tarballRes.headers.get("location");
7393
7441
  const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
7394
7442
  await downloadToFile(redirectUrl, tarballPath);
7395
- const buffer = readFileSync14(tarballPath);
7443
+ const buffer = readFileSync15(tarballPath);
7396
7444
  const manifest = tryParseAsGzippedManifest(buffer);
7397
7445
  if (manifest) {
7398
7446
  extractManifestToDir(manifest, targetDir);
@@ -7401,7 +7449,7 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7401
7449
  const extractDir = join8(tmpDir, "extracted");
7402
7450
  extractTarGz(tarballPath, extractDir);
7403
7451
  flattenPackageRoot(extractDir);
7404
- if (existsSync12(targetDir)) {
7452
+ if (existsSync13(targetDir)) {
7405
7453
  rmSync6(targetDir, { recursive: true, force: true });
7406
7454
  }
7407
7455
  cpSync6(extractDir, targetDir, { recursive: true, force: true });
@@ -7413,12 +7461,12 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7413
7461
  return;
7414
7462
  }
7415
7463
  const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
7416
- writeFileSync9(tarballPath, buffer);
7464
+ writeFileSync10(tarballPath, buffer);
7417
7465
  try {
7418
7466
  const extractDir = join8(tmpDir, "extracted");
7419
7467
  extractTarGz(tarballPath, extractDir);
7420
7468
  flattenPackageRoot(extractDir);
7421
- if (existsSync12(targetDir)) {
7469
+ if (existsSync13(targetDir)) {
7422
7470
  rmSync6(targetDir, { recursive: true, force: true });
7423
7471
  }
7424
7472
  cpSync6(extractDir, targetDir, { recursive: true, force: true });
@@ -7454,24 +7502,24 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
7454
7502
  }
7455
7503
  };
7456
7504
  const pkgPath = resolve14(targetDir, "package.json");
7457
- if (!existsSync12(pkgPath)) {
7458
- writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
7505
+ if (!existsSync13(pkgPath)) {
7506
+ writeFileSync10(pkgPath, JSON.stringify(packageJson, null, 2));
7459
7507
  } else {
7460
7508
  try {
7461
- const existing = JSON.parse(readFileSync14(pkgPath, "utf-8"));
7509
+ const existing = JSON.parse(readFileSync15(pkgPath, "utf-8"));
7462
7510
  const merged = {
7463
7511
  ...existing,
7464
7512
  xbrowser: { ...existing.xbrowser, ...packageJson.xbrowser },
7465
7513
  _marketplace: packageJson._marketplace
7466
7514
  };
7467
- writeFileSync9(pkgPath, JSON.stringify(merged, null, 2));
7515
+ writeFileSync10(pkgPath, JSON.stringify(merged, null, 2));
7468
7516
  } catch {
7469
- writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
7517
+ writeFileSync10(pkgPath, JSON.stringify(packageJson, null, 2));
7470
7518
  }
7471
7519
  }
7472
7520
  }
7473
7521
  function ensureIndexFile(plugin, name, targetDir) {
7474
- if (!existsSync12(resolve14(targetDir, "index.ts")) && !existsSync12(resolve14(targetDir, "index.js"))) {
7522
+ if (!existsSync13(resolve14(targetDir, "index.ts")) && !existsSync13(resolve14(targetDir, "index.js"))) {
7475
7523
  const commands = plugin.commands || [];
7476
7524
  const commandHandlers = commands.length > 0 ? commands.map((cmd) => {
7477
7525
  return [
@@ -7486,7 +7534,7 @@ function ensureIndexFile(plugin, name, targetDir) {
7486
7534
  ` handler: async () => ({ data: { message: 'Hello from ${name}!' }, tips: [] }),`,
7487
7535
  ` });`
7488
7536
  ].join("\n");
7489
- writeFileSync9(
7537
+ writeFileSync10(
7490
7538
  resolve14(targetDir, "index.ts"),
7491
7539
  [
7492
7540
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
@@ -7525,10 +7573,10 @@ var PluginInstaller = class {
7525
7573
  const type = this.detectSourceType(source);
7526
7574
  const name = options?.name || this.deriveName(source, type);
7527
7575
  const targetDir = resolve15(this.pluginsDir, name);
7528
- if (existsSync13(targetDir) && !options?.force) {
7576
+ if (existsSync14(targetDir) && !options?.force) {
7529
7577
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
7530
7578
  }
7531
- mkdirSync8(targetDir, { recursive: true });
7579
+ mkdirSync9(targetDir, { recursive: true });
7532
7580
  const resolvedSource = type === "npm" ? await resolveNpmPackageWithFallback(source) : source;
7533
7581
  switch (type) {
7534
7582
  case "local":
@@ -7586,7 +7634,7 @@ var PluginInstaller = class {
7586
7634
  */
7587
7635
  async uninstall(name) {
7588
7636
  const targetDir = resolve15(this.pluginsDir, name);
7589
- if (!existsSync13(targetDir)) {
7637
+ if (!existsSync14(targetDir)) {
7590
7638
  throw new Error(`Plugin "${name}" not found`);
7591
7639
  }
7592
7640
  rmSync7(targetDir, { recursive: true, force: true });
@@ -7597,7 +7645,7 @@ var PluginInstaller = class {
7597
7645
  * @returns Array of installed plugin information.
7598
7646
  */
7599
7647
  async list(_options) {
7600
- if (!existsSync13(this.pluginsDir)) return [];
7648
+ if (!existsSync14(this.pluginsDir)) return [];
7601
7649
  const entries = readdirSync3(this.pluginsDir, { withFileTypes: true });
7602
7650
  const plugins = [];
7603
7651
  for (const entry of entries) {
@@ -7605,7 +7653,7 @@ var PluginInstaller = class {
7605
7653
  const pluginPath = resolve15(this.pluginsDir, entry.name);
7606
7654
  const indexPath = resolve15(pluginPath, "index.ts");
7607
7655
  const indexJsPath = resolve15(pluginPath, "index.js");
7608
- if (!existsSync13(indexPath) && !existsSync13(indexJsPath)) continue;
7656
+ if (!existsSync14(indexPath) && !existsSync14(indexJsPath)) continue;
7609
7657
  const metadata = PluginMetadataParser.parseFromPackageJson(pluginPath);
7610
7658
  let source = "local";
7611
7659
  const pkg2 = readJsonFile(resolve15(pluginPath, "package.json"), {});
@@ -7632,10 +7680,10 @@ var PluginInstaller = class {
7632
7680
  }
7633
7681
  if (source.startsWith("file://")) {
7634
7682
  const filePath = decodeURIComponent(new URL(source).pathname);
7635
- if (existsSync13(filePath)) return "url";
7683
+ if (existsSync14(filePath)) return "url";
7636
7684
  }
7637
7685
  if (source.endsWith(".git") || source.includes("github.com/")) return "git";
7638
- if (existsSync13(resolve15(source))) return "local";
7686
+ if (existsSync14(resolve15(source))) return "local";
7639
7687
  return "npm";
7640
7688
  }
7641
7689
  deriveName(source, type) {
@@ -9659,7 +9707,7 @@ async function handleFilter(args, _mode) {
9659
9707
 
9660
9708
  // src/stdin.ts
9661
9709
  import { createInterface } from "readline";
9662
- import { readFileSync as readFileSync15 } from "fs";
9710
+ import { readFileSync as readFileSync16 } from "fs";
9663
9711
  async function readStdin2() {
9664
9712
  if (process.stdin.isTTY) return [];
9665
9713
  const lines = [];
@@ -9673,7 +9721,7 @@ async function readStdin2() {
9673
9721
  return lines;
9674
9722
  }
9675
9723
  function readCommandFile(filePath) {
9676
- const content = readFileSync15(filePath, "utf-8");
9724
+ const content = readFileSync16(filePath, "utf-8");
9677
9725
  return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
9678
9726
  }
9679
9727
 
@@ -10941,11 +10989,14 @@ Run "xbrowser ${command} --help" to see available commands.`
10941
10989
  showCommandHelp(command, cmdEntry, { description: site.config.description, name: site.name, url: site.url }, mode);
10942
10990
  return;
10943
10991
  }
10944
- const params = parsePluginParams(cmdArgsForPlugin, cmdEntry.parameters, options);
10992
+ const pluginNameIdx = argv.indexOf(command);
10993
+ const subCmdIdx = pluginNameIdx >= 0 ? argv.indexOf(subCommand, pluginNameIdx + 1) : -1;
10994
+ const rawPluginArgs = subCmdIdx >= 0 ? argv.slice(subCmdIdx + 1) : [];
10995
+ const params = parsePluginParams(rawPluginArgs, cmdEntry.parameters);
10945
10996
  if (options.target && !params._target) {
10946
10997
  params._target = options.target;
10947
10998
  }
10948
- const needsBrowser = cmdEntry.scope !== "global";
10999
+ const needsBrowser = cmdEntry.scope === "page";
10949
11000
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
10950
11001
  const { forwardExec } = await import("./daemon-client-XWSSQBEA.js");
10951
11002
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
@@ -10982,16 +11033,7 @@ Run "xbrowser ${command} --help" to see available commands.`
10982
11033
  browser: needsBrowser ? session.context.browser() : null,
10983
11034
  browserContext: needsBrowser ? session.context : null,
10984
11035
  sessionId: needsBrowser ? session.id : "",
10985
- storage: {
10986
- get: async (_key) => null,
10987
- set: async (_key, _value) => {
10988
- },
10989
- delete: async (_key) => {
10990
- },
10991
- clear: async () => {
10992
- },
10993
- keys: async () => []
10994
- },
11036
+ storage: getPluginStorage(command),
10995
11037
  output: { mode, showTips: true, color: true, emoji: true },
10996
11038
  error: (msg) => {
10997
11039
  outputError(msg);