@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 +63 -1
- package/dist/cli.js +142 -100
- package/dist/daemon-main.js +83 -35
- package/dist/index.js +140 -98
- package/package.json +19 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# xbrowser
|
|
2
2
|
|
|
3
|
-
>
|
|
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
|
[](https://github.com/dyyz1993/xbrowser/actions)
|
|
6
6
|
[](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
|
|
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
|
|
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 (!
|
|
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 (!
|
|
6871
|
-
|
|
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
|
|
7027
|
+
existsSync as existsSync14,
|
|
6980
7028
|
readdirSync as readdirSync3,
|
|
6981
|
-
mkdirSync as
|
|
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
|
|
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
|
|
7041
|
+
existsSync as existsSync8,
|
|
6994
7042
|
readdirSync as readdirSync2,
|
|
6995
7043
|
cpSync,
|
|
6996
7044
|
rmSync,
|
|
6997
|
-
mkdirSync as
|
|
6998
|
-
readFileSync as
|
|
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
|
-
|
|
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 (!
|
|
7138
|
+
if (!existsSync8(indexPath)) {
|
|
7091
7139
|
const indexJs = resolve9(dir, "index.js");
|
|
7092
|
-
if (!
|
|
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 (!
|
|
7145
|
+
if (!existsSync8(pkgPath)) {
|
|
7098
7146
|
warnings.push("No package.json found");
|
|
7099
7147
|
} else {
|
|
7100
7148
|
try {
|
|
7101
|
-
const pkg2 = JSON.parse(
|
|
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 (!
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
7194
|
-
const pkg2 = JSON.parse(
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
7235
|
-
const pkg2 = JSON.parse(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
7280
|
-
const pkg2 = JSON.parse(
|
|
7327
|
+
if (existsSync12(pkgPath)) {
|
|
7328
|
+
const pkg2 = JSON.parse(readFileSync14(pkgPath, "utf-8"));
|
|
7281
7329
|
if (!pkg2._urlSource) {
|
|
7282
7330
|
pkg2._urlSource = { url };
|
|
7283
|
-
|
|
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
|
|
7302
|
-
mkdirSync as
|
|
7303
|
-
writeFileSync as
|
|
7304
|
-
readFileSync as
|
|
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 (
|
|
7374
|
+
if (existsSync13(targetDir) && !options?.force) {
|
|
7327
7375
|
throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
|
|
7328
7376
|
}
|
|
7329
|
-
|
|
7377
|
+
mkdirSync8(targetDir, { recursive: true });
|
|
7330
7378
|
const tmpDir = join8(tmpdir5(), `xbrowser-marketplace-${Date.now()}`);
|
|
7331
|
-
|
|
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 (
|
|
7409
|
+
if (existsSync13(targetDir)) {
|
|
7362
7410
|
rmSync6(targetDir, { recursive: true, force: true });
|
|
7363
7411
|
}
|
|
7364
|
-
|
|
7412
|
+
mkdirSync8(targetDir, { recursive: true });
|
|
7365
7413
|
for (const file of manifest) {
|
|
7366
7414
|
const filePath = resolve14(targetDir, file.path);
|
|
7367
|
-
|
|
7368
|
-
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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 (!
|
|
7458
|
-
|
|
7505
|
+
if (!existsSync13(pkgPath)) {
|
|
7506
|
+
writeFileSync10(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
7459
7507
|
} else {
|
|
7460
7508
|
try {
|
|
7461
|
-
const existing = JSON.parse(
|
|
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
|
-
|
|
7515
|
+
writeFileSync10(pkgPath, JSON.stringify(merged, null, 2));
|
|
7468
7516
|
} catch {
|
|
7469
|
-
|
|
7517
|
+
writeFileSync10(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
7470
7518
|
}
|
|
7471
7519
|
}
|
|
7472
7520
|
}
|
|
7473
7521
|
function ensureIndexFile(plugin, name, targetDir) {
|
|
7474
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
7576
|
+
if (existsSync14(targetDir) && !options?.force) {
|
|
7529
7577
|
throw new Error(`Plugin "${name}" already exists. Use --force to overwrite.`);
|
|
7530
7578
|
}
|
|
7531
|
-
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (
|
|
7683
|
+
if (existsSync14(filePath)) return "url";
|
|
7636
7684
|
}
|
|
7637
7685
|
if (source.endsWith(".git") || source.includes("github.com/")) return "git";
|
|
7638
|
-
if (
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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);
|