@xbrowser/cli 0.14.3 → 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);
@@ -6597,7 +6630,7 @@ async function executeChain(input, options) {
6597
6630
  const outputs = [];
6598
6631
  for (const h of hooks) {
6599
6632
  const output = await h.onAfterCommand?.({ page: session.page, command: `${cmdName} ${subCommand}`, params: pluginParams, result: raw, duration: duration2 });
6600
- if (output) outputs.push(output);
6633
+ if (output) outputs.push({ _hook: h.name, ...output });
6601
6634
  }
6602
6635
  if (outputs.length > 0) hookOutputs = outputs;
6603
6636
  }
@@ -6868,7 +6901,7 @@ var sessionKillBuiltin = {
6868
6901
  };
6869
6902
 
6870
6903
  // src/config.ts
6871
- 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";
6872
6905
  import { join as join4 } from "path";
6873
6906
  import { homedir as homedir4, tmpdir } from "os";
6874
6907
  function getConfigFile() {
@@ -6876,14 +6909,14 @@ function getConfigFile() {
6876
6909
  }
6877
6910
  function loadConfig() {
6878
6911
  const configFile = getConfigFile();
6879
- if (!existsSync6(configFile)) return {};
6912
+ if (!existsSync7(configFile)) return {};
6880
6913
  return readJsonFile(configFile, {});
6881
6914
  }
6882
6915
  function saveConfig(config) {
6883
6916
  const dir = join4(homedir4() || tmpdir(), ".xbrowser");
6884
6917
  const configFile = getConfigFile();
6885
- if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
6886
- 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");
6887
6920
  }
6888
6921
  function getConfigValue(key) {
6889
6922
  return loadConfig()[key];
@@ -6991,26 +7024,26 @@ var configBuiltin = {
6991
7024
 
6992
7025
  // src/plugin/installer.ts
6993
7026
  import {
6994
- existsSync as existsSync13,
7027
+ existsSync as existsSync14,
6995
7028
  readdirSync as readdirSync3,
6996
- mkdirSync as mkdirSync8,
7029
+ mkdirSync as mkdirSync9,
6997
7030
  rmSync as rmSync7
6998
7031
  } from "fs";
6999
7032
  import { resolve as resolve15, basename as basename2 } from "path";
7000
7033
  import { homedir as homedir5 } from "os";
7001
7034
 
7002
7035
  // src/plugin/install-sources/local.ts
7003
- 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";
7004
7037
  import { resolve as resolve10 } from "path";
7005
7038
 
7006
7039
  // src/plugin/install-utils.ts
7007
7040
  import {
7008
- existsSync as existsSync7,
7041
+ existsSync as existsSync8,
7009
7042
  readdirSync as readdirSync2,
7010
7043
  cpSync,
7011
7044
  rmSync,
7012
- mkdirSync as mkdirSync4,
7013
- readFileSync as readFileSync10,
7045
+ mkdirSync as mkdirSync5,
7046
+ readFileSync as readFileSync11,
7014
7047
  createWriteStream
7015
7048
  } from "fs";
7016
7049
  import { resolve as resolve9 } from "path";
@@ -7081,7 +7114,7 @@ async function downloadToFile(url, destPath) {
7081
7114
  await pipeline(nodeStream, createWriteStream(destPath));
7082
7115
  }
7083
7116
  function extractTarGz(tarballPath, targetDir) {
7084
- mkdirSync4(targetDir, { recursive: true });
7117
+ mkdirSync5(targetDir, { recursive: true });
7085
7118
  execSync7(`tar -xzf "${tarballPath}" -C "${targetDir}"`, { stdio: "pipe" });
7086
7119
  }
7087
7120
  function flattenPackageRoot(targetDir) {
@@ -7102,18 +7135,18 @@ function flattenPackageRoot(targetDir) {
7102
7135
  async function verifyPlugin(dir) {
7103
7136
  const warnings = [];
7104
7137
  const indexPath = resolve9(dir, "index.ts");
7105
- if (!existsSync7(indexPath)) {
7138
+ if (!existsSync8(indexPath)) {
7106
7139
  const indexJs = resolve9(dir, "index.js");
7107
- if (!existsSync7(indexJs)) {
7140
+ if (!existsSync8(indexJs)) {
7108
7141
  return { valid: false, error: "No index.ts or index.js entry point found", warnings };
7109
7142
  }
7110
7143
  }
7111
7144
  const pkgPath = resolve9(dir, "package.json");
7112
- if (!existsSync7(pkgPath)) {
7145
+ if (!existsSync8(pkgPath)) {
7113
7146
  warnings.push("No package.json found");
7114
7147
  } else {
7115
7148
  try {
7116
- const pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
7149
+ const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7117
7150
  if (!pkg2.xbrowser) {
7118
7151
  warnings.push("No xbrowser metadata in package.json");
7119
7152
  }
@@ -7133,7 +7166,7 @@ function safeCleanup(dir) {
7133
7166
  // src/plugin/install-sources/local.ts
7134
7167
  async function installFromLocal(source, name, targetDir) {
7135
7168
  const srcPath = resolve10(source);
7136
- if (!existsSync8(srcPath)) {
7169
+ if (!existsSync9(srcPath)) {
7137
7170
  throw new Error(`Local path does not exist: ${srcPath}`);
7138
7171
  }
7139
7172
  const tmpTarget = `${targetDir}-tmp-${Date.now()}`;
@@ -7146,7 +7179,7 @@ async function installFromLocal(source, name, targetDir) {
7146
7179
  safeCleanup(tmpTarget);
7147
7180
  throw new Error(`Invalid plugin: ${verify.error}`);
7148
7181
  }
7149
- if (existsSync8(targetDir)) {
7182
+ if (existsSync9(targetDir)) {
7150
7183
  rmSync2(targetDir, { recursive: true, force: true });
7151
7184
  }
7152
7185
  cpSync2(tmpTarget, targetDir, { recursive: true, force: true });
@@ -7166,7 +7199,7 @@ async function installFromLocal(source, name, targetDir) {
7166
7199
  }
7167
7200
 
7168
7201
  // src/plugin/install-sources/npm.ts
7169
- 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";
7170
7203
  import { resolve as resolve11, join as join5 } from "path";
7171
7204
  import { tmpdir as tmpdir2 } from "os";
7172
7205
  async function installFromNpm(packageName, name, targetDir) {
@@ -7187,7 +7220,7 @@ async function installFromNpm(packageName, name, targetDir) {
7187
7220
  }
7188
7221
  const tarballUrl = versionMeta.dist.tarball;
7189
7222
  const tmpDir = join5(tmpdir2(), `xbrowser-npm-${Date.now()}`);
7190
- mkdirSync5(tmpDir, { recursive: true });
7223
+ mkdirSync6(tmpDir, { recursive: true });
7191
7224
  let warnings = [];
7192
7225
  try {
7193
7226
  const tarballPath = join5(tmpDir, `${name}.tgz`);
@@ -7200,16 +7233,16 @@ async function installFromNpm(packageName, name, targetDir) {
7200
7233
  if (!verify.valid) {
7201
7234
  throw new Error(`Invalid npm plugin: ${verify.error}`);
7202
7235
  }
7203
- if (existsSync9(targetDir)) {
7236
+ if (existsSync10(targetDir)) {
7204
7237
  rmSync3(targetDir, { recursive: true, force: true });
7205
7238
  }
7206
7239
  cpSync3(extractDir, targetDir, { recursive: true, force: true });
7207
7240
  const pkgPath = resolve11(targetDir, "package.json");
7208
- if (existsSync9(pkgPath)) {
7209
- const pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
7241
+ if (existsSync10(pkgPath)) {
7242
+ const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7210
7243
  if (!pkg2._npmSource) {
7211
7244
  pkg2._npmSource = { name: packageName, version: latestVersion };
7212
- writeFileSync6(pkgPath, JSON.stringify(pkg2, null, 2));
7245
+ writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
7213
7246
  }
7214
7247
  }
7215
7248
  } finally {
@@ -7226,7 +7259,7 @@ async function installFromNpm(packageName, name, targetDir) {
7226
7259
  }
7227
7260
 
7228
7261
  // src/plugin/install-sources/git.ts
7229
- 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";
7230
7263
  import { resolve as resolve12, join as join6 } from "path";
7231
7264
  import { tmpdir as tmpdir3 } from "os";
7232
7265
  import { execSync as execSync8 } from "child_process";
@@ -7240,17 +7273,17 @@ async function installFromGit(gitUrl, name, targetDir) {
7240
7273
  if (!verify.valid) {
7241
7274
  throw new Error(`Invalid git plugin: ${verify.error}`);
7242
7275
  }
7243
- if (existsSync10(targetDir)) {
7276
+ if (existsSync11(targetDir)) {
7244
7277
  rmSync4(targetDir, { recursive: true, force: true });
7245
7278
  }
7246
7279
  cpSync4(tmpDir, targetDir, { recursive: true, force: true });
7247
7280
  rmSync4(resolve12(targetDir, ".git"), { recursive: true, force: true });
7248
7281
  const pkgPath = resolve12(targetDir, "package.json");
7249
- if (existsSync10(pkgPath)) {
7250
- const pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
7282
+ if (existsSync11(pkgPath)) {
7283
+ const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
7251
7284
  if (!pkg2._gitSource) {
7252
7285
  pkg2._gitSource = { url: gitUrl };
7253
- writeFileSync7(pkgPath, JSON.stringify(pkg2, null, 2));
7286
+ writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
7254
7287
  }
7255
7288
  }
7256
7289
  } finally {
@@ -7267,12 +7300,12 @@ async function installFromGit(gitUrl, name, targetDir) {
7267
7300
  }
7268
7301
 
7269
7302
  // src/plugin/install-sources/url.ts
7270
- 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";
7271
7304
  import { resolve as resolve13, join as join7, basename } from "path";
7272
7305
  import { tmpdir as tmpdir4 } from "os";
7273
7306
  async function installFromUrl(url, name, targetDir) {
7274
7307
  const tmpDir = join7(tmpdir4(), `xbrowser-url-${Date.now()}`);
7275
- mkdirSync6(tmpDir, { recursive: true });
7308
+ mkdirSync7(tmpDir, { recursive: true });
7276
7309
  let warnings = [];
7277
7310
  try {
7278
7311
  const fileName = basename(new URL(url).pathname) || "plugin.tar.gz";
@@ -7286,16 +7319,16 @@ async function installFromUrl(url, name, targetDir) {
7286
7319
  if (!verify.valid) {
7287
7320
  throw new Error(`Invalid plugin from URL: ${verify.error}`);
7288
7321
  }
7289
- if (existsSync11(targetDir)) {
7322
+ if (existsSync12(targetDir)) {
7290
7323
  rmSync5(targetDir, { recursive: true, force: true });
7291
7324
  }
7292
7325
  cpSync5(extractDir, targetDir, { recursive: true, force: true });
7293
7326
  const pkgPath = resolve13(targetDir, "package.json");
7294
- if (existsSync11(pkgPath)) {
7295
- const pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
7327
+ if (existsSync12(pkgPath)) {
7328
+ const pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
7296
7329
  if (!pkg2._urlSource) {
7297
7330
  pkg2._urlSource = { url };
7298
- writeFileSync8(pkgPath, JSON.stringify(pkg2, null, 2));
7331
+ writeFileSync9(pkgPath, JSON.stringify(pkg2, null, 2));
7299
7332
  }
7300
7333
  }
7301
7334
  } finally {
@@ -7313,10 +7346,10 @@ async function installFromUrl(url, name, targetDir) {
7313
7346
 
7314
7347
  // src/plugin/install-sources/marketplace.ts
7315
7348
  import {
7316
- existsSync as existsSync12,
7317
- mkdirSync as mkdirSync7,
7318
- writeFileSync as writeFileSync9,
7319
- readFileSync as readFileSync14,
7349
+ existsSync as existsSync13,
7350
+ mkdirSync as mkdirSync8,
7351
+ writeFileSync as writeFileSync10,
7352
+ readFileSync as readFileSync15,
7320
7353
  rmSync as rmSync6,
7321
7354
  cpSync as cpSync6
7322
7355
  } from "fs";
@@ -7338,12 +7371,12 @@ async function installFromMarketplace(pluginsDir, slug, options) {
7338
7371
  const plugin = detailData.data;
7339
7372
  const name = options?.name || String(plugin.slug || slug);
7340
7373
  const targetDir = resolve14(pluginsDir, name);
7341
- if (existsSync12(targetDir) && !options?.force) {
7374
+ if (existsSync13(targetDir) && !options?.force) {
7342
7375
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
7343
7376
  }
7344
- mkdirSync7(targetDir, { recursive: true });
7377
+ mkdirSync8(targetDir, { recursive: true });
7345
7378
  const tmpDir = join8(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
7346
- mkdirSync7(tmpDir, { recursive: true });
7379
+ mkdirSync8(tmpDir, { recursive: true });
7347
7380
  const realSlug = String(plugin.slug || slug);
7348
7381
  try {
7349
7382
  await downloadAndExtractMarketplaceTarball(baseUrl, realSlug, tmpDir, targetDir);
@@ -7373,14 +7406,14 @@ function isManifestArray(data) {
7373
7406
  return Array.isArray(data) && data.length > 0 && typeof data[0].path === "string" && typeof data[0].content === "string";
7374
7407
  }
7375
7408
  function extractManifestToDir(manifest, targetDir) {
7376
- if (existsSync12(targetDir)) {
7409
+ if (existsSync13(targetDir)) {
7377
7410
  rmSync6(targetDir, { recursive: true, force: true });
7378
7411
  }
7379
- mkdirSync7(targetDir, { recursive: true });
7412
+ mkdirSync8(targetDir, { recursive: true });
7380
7413
  for (const file of manifest) {
7381
7414
  const filePath = resolve14(targetDir, file.path);
7382
- mkdirSync7(dirname2(filePath), { recursive: true });
7383
- writeFileSync9(filePath, Buffer.from(file.content, "base64"));
7415
+ mkdirSync8(dirname2(filePath), { recursive: true });
7416
+ writeFileSync10(filePath, Buffer.from(file.content, "base64"));
7384
7417
  }
7385
7418
  }
7386
7419
  function tryParseAsGzippedManifest(buffer) {
@@ -7407,7 +7440,7 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7407
7440
  const redirectUrl = tarballRes.headers.get("location");
7408
7441
  const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
7409
7442
  await downloadToFile(redirectUrl, tarballPath);
7410
- const buffer = readFileSync14(tarballPath);
7443
+ const buffer = readFileSync15(tarballPath);
7411
7444
  const manifest = tryParseAsGzippedManifest(buffer);
7412
7445
  if (manifest) {
7413
7446
  extractManifestToDir(manifest, targetDir);
@@ -7416,7 +7449,7 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7416
7449
  const extractDir = join8(tmpDir, "extracted");
7417
7450
  extractTarGz(tarballPath, extractDir);
7418
7451
  flattenPackageRoot(extractDir);
7419
- if (existsSync12(targetDir)) {
7452
+ if (existsSync13(targetDir)) {
7420
7453
  rmSync6(targetDir, { recursive: true, force: true });
7421
7454
  }
7422
7455
  cpSync6(extractDir, targetDir, { recursive: true, force: true });
@@ -7428,12 +7461,12 @@ async function downloadAndExtractMarketplaceTarball(baseUrl, slug, tmpDir, targe
7428
7461
  return;
7429
7462
  }
7430
7463
  const tarballPath = join8(tmpDir, `${slug}.tar.gz`);
7431
- writeFileSync9(tarballPath, buffer);
7464
+ writeFileSync10(tarballPath, buffer);
7432
7465
  try {
7433
7466
  const extractDir = join8(tmpDir, "extracted");
7434
7467
  extractTarGz(tarballPath, extractDir);
7435
7468
  flattenPackageRoot(extractDir);
7436
- if (existsSync12(targetDir)) {
7469
+ if (existsSync13(targetDir)) {
7437
7470
  rmSync6(targetDir, { recursive: true, force: true });
7438
7471
  }
7439
7472
  cpSync6(extractDir, targetDir, { recursive: true, force: true });
@@ -7469,24 +7502,24 @@ function writeMarketplacePackageJson(plugin, slug, name, baseUrl, targetDir) {
7469
7502
  }
7470
7503
  };
7471
7504
  const pkgPath = resolve14(targetDir, "package.json");
7472
- if (!existsSync12(pkgPath)) {
7473
- writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
7505
+ if (!existsSync13(pkgPath)) {
7506
+ writeFileSync10(pkgPath, JSON.stringify(packageJson, null, 2));
7474
7507
  } else {
7475
7508
  try {
7476
- const existing = JSON.parse(readFileSync14(pkgPath, "utf-8"));
7509
+ const existing = JSON.parse(readFileSync15(pkgPath, "utf-8"));
7477
7510
  const merged = {
7478
7511
  ...existing,
7479
7512
  xbrowser: { ...existing.xbrowser, ...packageJson.xbrowser },
7480
7513
  _marketplace: packageJson._marketplace
7481
7514
  };
7482
- writeFileSync9(pkgPath, JSON.stringify(merged, null, 2));
7515
+ writeFileSync10(pkgPath, JSON.stringify(merged, null, 2));
7483
7516
  } catch {
7484
- writeFileSync9(pkgPath, JSON.stringify(packageJson, null, 2));
7517
+ writeFileSync10(pkgPath, JSON.stringify(packageJson, null, 2));
7485
7518
  }
7486
7519
  }
7487
7520
  }
7488
7521
  function ensureIndexFile(plugin, name, targetDir) {
7489
- if (!existsSync12(resolve14(targetDir, "index.ts")) && !existsSync12(resolve14(targetDir, "index.js"))) {
7522
+ if (!existsSync13(resolve14(targetDir, "index.ts")) && !existsSync13(resolve14(targetDir, "index.js"))) {
7490
7523
  const commands = plugin.commands || [];
7491
7524
  const commandHandlers = commands.length > 0 ? commands.map((cmd) => {
7492
7525
  return [
@@ -7501,7 +7534,7 @@ function ensureIndexFile(plugin, name, targetDir) {
7501
7534
  ` handler: async () => ({ data: { message: 'Hello from ${name}!' }, tips: [] }),`,
7502
7535
  ` });`
7503
7536
  ].join("\n");
7504
- writeFileSync9(
7537
+ writeFileSync10(
7505
7538
  resolve14(targetDir, "index.ts"),
7506
7539
  [
7507
7540
  `import type { XCLIAPI } from '@dyyz1993/xcli-core';`,
@@ -7540,10 +7573,10 @@ var PluginInstaller = class {
7540
7573
  const type = this.detectSourceType(source);
7541
7574
  const name = options?.name || this.deriveName(source, type);
7542
7575
  const targetDir = resolve15(this.pluginsDir, name);
7543
- if (existsSync13(targetDir) && !options?.force) {
7576
+ if (existsSync14(targetDir) && !options?.force) {
7544
7577
  throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
7545
7578
  }
7546
- mkdirSync8(targetDir, { recursive: true });
7579
+ mkdirSync9(targetDir, { recursive: true });
7547
7580
  const resolvedSource = type === "npm" ? await resolveNpmPackageWithFallback(source) : source;
7548
7581
  switch (type) {
7549
7582
  case "local":
@@ -7601,7 +7634,7 @@ var PluginInstaller = class {
7601
7634
  */
7602
7635
  async uninstall(name) {
7603
7636
  const targetDir = resolve15(this.pluginsDir, name);
7604
- if (!existsSync13(targetDir)) {
7637
+ if (!existsSync14(targetDir)) {
7605
7638
  throw new Error(`Plugin "${name}" not found`);
7606
7639
  }
7607
7640
  rmSync7(targetDir, { recursive: true, force: true });
@@ -7612,7 +7645,7 @@ var PluginInstaller = class {
7612
7645
  * @returns Array of installed plugin information.
7613
7646
  */
7614
7647
  async list(_options) {
7615
- if (!existsSync13(this.pluginsDir)) return [];
7648
+ if (!existsSync14(this.pluginsDir)) return [];
7616
7649
  const entries = readdirSync3(this.pluginsDir, { withFileTypes: true });
7617
7650
  const plugins = [];
7618
7651
  for (const entry of entries) {
@@ -7620,7 +7653,7 @@ var PluginInstaller = class {
7620
7653
  const pluginPath = resolve15(this.pluginsDir, entry.name);
7621
7654
  const indexPath = resolve15(pluginPath, "index.ts");
7622
7655
  const indexJsPath = resolve15(pluginPath, "index.js");
7623
- if (!existsSync13(indexPath) && !existsSync13(indexJsPath)) continue;
7656
+ if (!existsSync14(indexPath) && !existsSync14(indexJsPath)) continue;
7624
7657
  const metadata = PluginMetadataParser.parseFromPackageJson(pluginPath);
7625
7658
  let source = "local";
7626
7659
  const pkg2 = readJsonFile(resolve15(pluginPath, "package.json"), {});
@@ -7647,10 +7680,10 @@ var PluginInstaller = class {
7647
7680
  }
7648
7681
  if (source.startsWith("file://")) {
7649
7682
  const filePath = decodeURIComponent(new URL(source).pathname);
7650
- if (existsSync13(filePath)) return "url";
7683
+ if (existsSync14(filePath)) return "url";
7651
7684
  }
7652
7685
  if (source.endsWith(".git") || source.includes("github.com/")) return "git";
7653
- if (existsSync13(resolve15(source))) return "local";
7686
+ if (existsSync14(resolve15(source))) return "local";
7654
7687
  return "npm";
7655
7688
  }
7656
7689
  deriveName(source, type) {
@@ -9674,7 +9707,7 @@ async function handleFilter(args, _mode) {
9674
9707
 
9675
9708
  // src/stdin.ts
9676
9709
  import { createInterface } from "readline";
9677
- import { readFileSync as readFileSync15 } from "fs";
9710
+ import { readFileSync as readFileSync16 } from "fs";
9678
9711
  async function readStdin2() {
9679
9712
  if (process.stdin.isTTY) return [];
9680
9713
  const lines = [];
@@ -9688,7 +9721,7 @@ async function readStdin2() {
9688
9721
  return lines;
9689
9722
  }
9690
9723
  function readCommandFile(filePath) {
9691
- const content = readFileSync15(filePath, "utf-8");
9724
+ const content = readFileSync16(filePath, "utf-8");
9692
9725
  return content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
9693
9726
  }
9694
9727
 
@@ -10956,11 +10989,14 @@ Run "xbrowser ${command} --help" to see available commands.`
10956
10989
  showCommandHelp(command, cmdEntry, { description: site.config.description, name: site.name, url: site.url }, mode);
10957
10990
  return;
10958
10991
  }
10959
- 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);
10960
10996
  if (options.target && !params._target) {
10961
10997
  params._target = options.target;
10962
10998
  }
10963
- const needsBrowser = cmdEntry.scope !== "global";
10999
+ const needsBrowser = cmdEntry.scope === "page";
10964
11000
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
10965
11001
  const { forwardExec } = await import("./daemon-client-XWSSQBEA.js");
10966
11002
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
@@ -10997,16 +11033,7 @@ Run "xbrowser ${command} --help" to see available commands.`
10997
11033
  browser: needsBrowser ? session.context.browser() : null,
10998
11034
  browserContext: needsBrowser ? session.context : null,
10999
11035
  sessionId: needsBrowser ? session.id : "",
11000
- storage: {
11001
- get: async (_key) => null,
11002
- set: async (_key, _value) => {
11003
- },
11004
- delete: async (_key) => {
11005
- },
11006
- clear: async () => {
11007
- },
11008
- keys: async () => []
11009
- },
11036
+ storage: getPluginStorage(command),
11010
11037
  output: { mode, showTips: true, color: true, emoji: true },
11011
11038
  error: (msg) => {
11012
11039
  outputError(msg);