@xbrowser/cli 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2QQDTXDL.js +438 -0
- package/dist/{chunk-XYXCS7JW.js → chunk-JPSFUFPG.js} +4 -2
- package/dist/cli.js +109 -15
- package/dist/{daemon-client-R4QWHD7V.js → daemon-client-COJQESU2.js} +4 -2
- package/dist/{daemon-client-ZHO6NG36.js → daemon-client-XXKMJZZ7.js} +1 -1
- package/dist/daemon-main.js +102 -495
- package/dist/index.d.ts +10 -0
- package/dist/index.js +111 -17
- package/dist/plugin-singleton-SYJF6BD6.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// src/plugin/loader.ts
|
|
2
|
+
import {
|
|
3
|
+
Core
|
|
4
|
+
} from "@dyyz1993/xcli-core";
|
|
5
|
+
import { resolve as resolve2 } from "path";
|
|
6
|
+
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
|
|
9
|
+
// src/plugin/metadata-parser.ts
|
|
10
|
+
import { existsSync } from "fs";
|
|
11
|
+
import { resolve } from "path";
|
|
12
|
+
|
|
13
|
+
// src/utils/json-file.ts
|
|
14
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
15
|
+
function readJsonFile(filePath, defaultValue) {
|
|
16
|
+
try {
|
|
17
|
+
const content = readFileSync(filePath, "utf-8");
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
} catch {
|
|
20
|
+
return defaultValue;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/plugin/metadata-parser.ts
|
|
25
|
+
var PluginMetadataParser = class {
|
|
26
|
+
static XBROWSER_KEYWORDS = ["xbrowser", "xbrowser-plugin"];
|
|
27
|
+
static parseFromPackageJson(pluginPath) {
|
|
28
|
+
const packageJsonPath = resolve(pluginPath, "package.json");
|
|
29
|
+
if (!existsSync(packageJsonPath)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const packageJson = readJsonFile(packageJsonPath, null);
|
|
33
|
+
if (!packageJson) return null;
|
|
34
|
+
if (!packageJson.xbrowser) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const xbrowser = packageJson.xbrowser;
|
|
38
|
+
const metadata = {
|
|
39
|
+
id: xbrowser.id || packageJson.name,
|
|
40
|
+
name: xbrowser.name || packageJson.name,
|
|
41
|
+
description: xbrowser.description || packageJson.description || "",
|
|
42
|
+
version: xbrowser.version || packageJson.version || "1.0.0",
|
|
43
|
+
author: xbrowser.author || this.extractAuthor(packageJson.author),
|
|
44
|
+
homepage: xbrowser.homepage || packageJson.homepage,
|
|
45
|
+
commands: xbrowser.commands,
|
|
46
|
+
sites: xbrowser.sites,
|
|
47
|
+
tags: xbrowser.tags,
|
|
48
|
+
screenshot: xbrowser.screenshot,
|
|
49
|
+
license: xbrowser.license || packageJson.license
|
|
50
|
+
};
|
|
51
|
+
return metadata;
|
|
52
|
+
}
|
|
53
|
+
static isXBrowserPlugin(packageJson) {
|
|
54
|
+
if (packageJson.xbrowser) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const keywords = packageJson.keywords;
|
|
58
|
+
if (!keywords) return false;
|
|
59
|
+
return this.XBROWSER_KEYWORDS.some((kw) => keywords.includes(kw));
|
|
60
|
+
}
|
|
61
|
+
static fromNPMResult(result) {
|
|
62
|
+
const author = typeof result.author === "string" ? result.author : result.author?.name || "Unknown";
|
|
63
|
+
return {
|
|
64
|
+
id: result.name,
|
|
65
|
+
name: result.name.replace(/^xbrowser-plugin-/, "").replace(/^@[^/]+\//, ""),
|
|
66
|
+
description: result.description || "",
|
|
67
|
+
version: result.version,
|
|
68
|
+
author,
|
|
69
|
+
homepage: result.homepage || result.links?.homepage,
|
|
70
|
+
tags: result.keywords,
|
|
71
|
+
license: ""
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
static extractAuthor(author) {
|
|
75
|
+
if (typeof author === "string") return author;
|
|
76
|
+
if (typeof author === "object" && author !== null) {
|
|
77
|
+
const authorObj = author;
|
|
78
|
+
return authorObj.name || "Unknown";
|
|
79
|
+
}
|
|
80
|
+
return "Unknown";
|
|
81
|
+
}
|
|
82
|
+
static validateMetadata(metadata) {
|
|
83
|
+
const errors = [];
|
|
84
|
+
if (!metadata.id) errors.push("id is required");
|
|
85
|
+
if (!metadata.name) errors.push("name is required");
|
|
86
|
+
if (!metadata.description) errors.push("description is required");
|
|
87
|
+
if (!metadata.version) errors.push("version is required");
|
|
88
|
+
return errors;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/plugin/ensure-deps.ts
|
|
93
|
+
import { existsSync as existsSync2, mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
94
|
+
import { join } from "path";
|
|
95
|
+
import { execSync } from "child_process";
|
|
96
|
+
var SHARED_PLUGIN_DEPENDENCIES = {
|
|
97
|
+
"zod": "^3.24.0",
|
|
98
|
+
"@dyyz1993/xcli-core": "^0.12.1"
|
|
99
|
+
};
|
|
100
|
+
function ensurePluginDependencies(pluginsDir) {
|
|
101
|
+
const zodPath = join(pluginsDir, "node_modules", "zod");
|
|
102
|
+
if (existsSync2(zodPath)) return;
|
|
103
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
104
|
+
const pkgPath = join(pluginsDir, "package.json");
|
|
105
|
+
let pkg = {};
|
|
106
|
+
if (existsSync2(pkgPath)) {
|
|
107
|
+
try {
|
|
108
|
+
pkg = readJsonFile(pkgPath, {});
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const existingDeps = pkg.dependencies || {};
|
|
113
|
+
let needsInstall = false;
|
|
114
|
+
for (const [dep, version] of Object.entries(SHARED_PLUGIN_DEPENDENCIES)) {
|
|
115
|
+
if (!existingDeps[dep]) {
|
|
116
|
+
existingDeps[dep] = version;
|
|
117
|
+
needsInstall = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!needsInstall && existsSync2(join(pluginsDir, "node_modules"))) return;
|
|
121
|
+
pkg.dependencies = existingDeps;
|
|
122
|
+
pkg.private = true;
|
|
123
|
+
pkg.description = pkg.description || "xbrowser plugins \u2014 shared dependencies";
|
|
124
|
+
writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
|
|
125
|
+
try {
|
|
126
|
+
execSync("npm install --production --no-package-lock --no-fund --no-audit", {
|
|
127
|
+
cwd: pluginsDir,
|
|
128
|
+
stdio: "pipe",
|
|
129
|
+
timeout: 6e4,
|
|
130
|
+
env: { ...process.env, NODE_ENV: "production" }
|
|
131
|
+
});
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.warn(`\u26A0\uFE0F Failed to install shared plugin dependencies: ${err instanceof Error ? err.message : String(err)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/plugin/contract.ts
|
|
138
|
+
import {
|
|
139
|
+
unwrapZod,
|
|
140
|
+
fieldsFromZodObjectReflected,
|
|
141
|
+
zodTypeToContractType
|
|
142
|
+
} from "@dyyz1993/xcli-core";
|
|
143
|
+
function buildPluginContract(site) {
|
|
144
|
+
const commands = site.getAllCommands().map((command) => buildCommandContract(site.getCommand?.(command.name) || command, {
|
|
145
|
+
siteRequiresLogin: site.config?.requiresLogin
|
|
146
|
+
}));
|
|
147
|
+
return {
|
|
148
|
+
version: 2,
|
|
149
|
+
plugin: {
|
|
150
|
+
name: site.name,
|
|
151
|
+
url: site.url,
|
|
152
|
+
description: site.config?.description,
|
|
153
|
+
requiresLogin: site.config?.requiresLogin
|
|
154
|
+
},
|
|
155
|
+
commands
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function buildCommandContract(command, options = {}) {
|
|
159
|
+
const extension = command.xbrowser || {};
|
|
160
|
+
const inferredFields = fieldsFromZodObject(command.parameters);
|
|
161
|
+
const fields = mergeFields(inferredFields, extension.form?.fields || []);
|
|
162
|
+
const positional = extension.positional || fields.filter((field) => field.positional).map((field) => field.name);
|
|
163
|
+
const requiresLogin = command.requiresLogin === true || options.siteRequiresLogin === true && command.name !== "login" && command.name !== "logout";
|
|
164
|
+
const capabilities = extension.capabilities || inferCapabilities(command.scope || "project", requiresLogin);
|
|
165
|
+
const outputSchema = command.result ? summarizeZod(command.result) : void 0;
|
|
166
|
+
return {
|
|
167
|
+
name: command.name,
|
|
168
|
+
description: command.description || "",
|
|
169
|
+
scope: command.scope || "project",
|
|
170
|
+
requiresLogin,
|
|
171
|
+
category: extension.category,
|
|
172
|
+
capabilities,
|
|
173
|
+
positional,
|
|
174
|
+
form: {
|
|
175
|
+
title: extension.form?.title || command.description || command.name,
|
|
176
|
+
description: extension.form?.description,
|
|
177
|
+
submitLabel: extension.form?.submitLabel || "Run",
|
|
178
|
+
fields
|
|
179
|
+
},
|
|
180
|
+
output: extension.output || (outputSchema ? { schema: outputSchema } : void 0)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function fieldsFromZodObject(schema) {
|
|
184
|
+
const reflected = fieldsFromZodObjectReflected(schema);
|
|
185
|
+
return reflected.map((field) => {
|
|
186
|
+
const widget = widgetFor(field.type, field.enum);
|
|
187
|
+
return {
|
|
188
|
+
name: field.name,
|
|
189
|
+
label: toLabel(field.name),
|
|
190
|
+
type: field.type,
|
|
191
|
+
widget,
|
|
192
|
+
required: field.required,
|
|
193
|
+
...field.description ? { description: field.description } : {},
|
|
194
|
+
...field.default !== void 0 ? { default: field.default } : {},
|
|
195
|
+
...field.enum ? { enum: field.enum } : {},
|
|
196
|
+
...field.type === "array" ? { multiple: true } : {}
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
function mergeFields(inferred, overrides) {
|
|
201
|
+
if (overrides.length === 0) return inferred;
|
|
202
|
+
const byName = new Map(inferred.map((field) => [field.name, field]));
|
|
203
|
+
const seen = /* @__PURE__ */ new Set();
|
|
204
|
+
const merged = [];
|
|
205
|
+
for (const override of overrides) {
|
|
206
|
+
if (!override.name) continue;
|
|
207
|
+
const base = byName.get(override.name) || {
|
|
208
|
+
name: override.name,
|
|
209
|
+
label: toLabel(override.name),
|
|
210
|
+
type: "string",
|
|
211
|
+
widget: "text",
|
|
212
|
+
required: false
|
|
213
|
+
};
|
|
214
|
+
merged.push({ ...base, ...override, name: override.name });
|
|
215
|
+
seen.add(override.name);
|
|
216
|
+
}
|
|
217
|
+
for (const field of inferred) {
|
|
218
|
+
if (!seen.has(field.name)) merged.push(field);
|
|
219
|
+
}
|
|
220
|
+
return merged;
|
|
221
|
+
}
|
|
222
|
+
function inferCapabilities(scope, requiresLogin) {
|
|
223
|
+
const caps = [];
|
|
224
|
+
if (scope === "page") caps.push("browser.page");
|
|
225
|
+
if (scope === "browser") caps.push("browser.context");
|
|
226
|
+
if (requiresLogin) caps.push("auth.login");
|
|
227
|
+
return caps;
|
|
228
|
+
}
|
|
229
|
+
function widgetFor(type, enumValues) {
|
|
230
|
+
if (enumValues) return "select";
|
|
231
|
+
if (type === "boolean") return "checkbox";
|
|
232
|
+
if (type === "number") return "number";
|
|
233
|
+
if (type === "array") return "multi-select";
|
|
234
|
+
if (type === "object") return "json";
|
|
235
|
+
return "text";
|
|
236
|
+
}
|
|
237
|
+
function summarizeZod(schema) {
|
|
238
|
+
const unwrapped = unwrapZod(schema);
|
|
239
|
+
if (unwrapped.typeName === "ZodArray") {
|
|
240
|
+
const def = unwrapped.schema?._def;
|
|
241
|
+
return {
|
|
242
|
+
type: "array",
|
|
243
|
+
items: summarizeZod(def?.type || def?.innerType)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const shape = getObjectShape(schema);
|
|
247
|
+
if (!shape) {
|
|
248
|
+
return {
|
|
249
|
+
type: zodTypeToContractType(unwrapped.typeName),
|
|
250
|
+
required: !unwrapped.optional,
|
|
251
|
+
...unwrapped.description ? { description: unwrapped.description } : {}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return Object.fromEntries(
|
|
255
|
+
Object.entries(shape).map(([name, field]) => {
|
|
256
|
+
const inner = unwrapZod(field);
|
|
257
|
+
return [name, {
|
|
258
|
+
type: zodTypeToContractType(inner.typeName),
|
|
259
|
+
required: !inner.optional,
|
|
260
|
+
...inner.description ? { description: inner.description } : {}
|
|
261
|
+
}];
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
function getObjectShape(schema) {
|
|
266
|
+
const zod = schema;
|
|
267
|
+
const shapeOrFn = zod?.shape ?? zod?._def?.shape;
|
|
268
|
+
if (!shapeOrFn) return void 0;
|
|
269
|
+
return typeof shapeOrFn === "function" ? shapeOrFn() : shapeOrFn;
|
|
270
|
+
}
|
|
271
|
+
function toLabel(name) {
|
|
272
|
+
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim().replace(/^./, (char) => char.toUpperCase());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/plugin/login-required-patch.ts
|
|
276
|
+
import { SiteInstanceImpl } from "@dyyz1993/xcli-core";
|
|
277
|
+
var patched = false;
|
|
278
|
+
function patchLoginRequired() {
|
|
279
|
+
if (patched) return;
|
|
280
|
+
patched = true;
|
|
281
|
+
const target = SiteInstanceImpl.prototype;
|
|
282
|
+
const originalCommand = target.command;
|
|
283
|
+
const wrapped = function(...args) {
|
|
284
|
+
const result = originalCommand.apply(this, args);
|
|
285
|
+
const [name, cmd] = args;
|
|
286
|
+
const loginRequired = cmd.loginRequired;
|
|
287
|
+
if (loginRequired) {
|
|
288
|
+
const commands = this.commands;
|
|
289
|
+
const entry = commands?.get(name);
|
|
290
|
+
if (entry) {
|
|
291
|
+
entry.loginRequired = loginRequired;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
};
|
|
296
|
+
Object.defineProperty(target, "command", {
|
|
297
|
+
value: wrapped,
|
|
298
|
+
writable: true,
|
|
299
|
+
configurable: true
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/plugin/loader.ts
|
|
304
|
+
var DEFAULT_PLUGIN_DIRS = [".xcli/plugins", "../.xcli/plugins"];
|
|
305
|
+
var XBrowserPluginLoader = class {
|
|
306
|
+
core;
|
|
307
|
+
loader;
|
|
308
|
+
options;
|
|
309
|
+
constructor(options) {
|
|
310
|
+
patchLoginRequired();
|
|
311
|
+
this.options = options ?? {};
|
|
312
|
+
const cwd = this.options.cwd || process.cwd();
|
|
313
|
+
const coreConfig = {
|
|
314
|
+
name: "xbrowser",
|
|
315
|
+
version: "0.1.0",
|
|
316
|
+
description: "Browser automation CLI",
|
|
317
|
+
configDirName: ".xbrowser",
|
|
318
|
+
envPrefix: "XBROWSER",
|
|
319
|
+
pluginDirs: [
|
|
320
|
+
...DEFAULT_PLUGIN_DIRS,
|
|
321
|
+
resolve2(cwd, ".xcli/plugins")
|
|
322
|
+
]
|
|
323
|
+
};
|
|
324
|
+
this.core = new Core(coreConfig);
|
|
325
|
+
this.loader = this.core.loader;
|
|
326
|
+
}
|
|
327
|
+
getAPI() {
|
|
328
|
+
return this.loader.getAPI();
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Get the core instance for external use.
|
|
332
|
+
* @returns The xcli-core Core instance.
|
|
333
|
+
*/
|
|
334
|
+
getCore() {
|
|
335
|
+
return this.core;
|
|
336
|
+
}
|
|
337
|
+
getPlugin(id) {
|
|
338
|
+
return this.loader.getPlugin(id);
|
|
339
|
+
}
|
|
340
|
+
getPluginStatus(id) {
|
|
341
|
+
return this.loader.getPluginStatus(id);
|
|
342
|
+
}
|
|
343
|
+
getLoadedPlugins() {
|
|
344
|
+
return this.loader.getLoadedPlugins();
|
|
345
|
+
}
|
|
346
|
+
getPluginContract(siteName, commandName) {
|
|
347
|
+
const site = this.core.loader.getSite(siteName);
|
|
348
|
+
if (!site) return void 0;
|
|
349
|
+
const contract = buildPluginContract(site);
|
|
350
|
+
if (!commandName) return contract;
|
|
351
|
+
return contract.commands.find((command) => command.name === commandName);
|
|
352
|
+
}
|
|
353
|
+
async loadPlugin(pluginPath, id) {
|
|
354
|
+
return this.loader.loadPlugin(pluginPath, id);
|
|
355
|
+
}
|
|
356
|
+
async unloadPlugin(id) {
|
|
357
|
+
return this.loader.unloadPlugin(id);
|
|
358
|
+
}
|
|
359
|
+
async reloadPlugin(id) {
|
|
360
|
+
return this.loader.reloadPlugin(id);
|
|
361
|
+
}
|
|
362
|
+
async loadFromFunction(setup) {
|
|
363
|
+
return this.loader.loadFromFunction(setup);
|
|
364
|
+
}
|
|
365
|
+
async scanAndLoad() {
|
|
366
|
+
const cwd = this.options.cwd || process.cwd();
|
|
367
|
+
const globalDir = this.options.globalDir || resolve2(homedir(), ".xbrowser/plugins");
|
|
368
|
+
ensurePluginDependencies(globalDir);
|
|
369
|
+
const dirs = [
|
|
370
|
+
resolve2(cwd, ".xcli/plugins"),
|
|
371
|
+
resolve2(cwd, "../.xcli/plugins"),
|
|
372
|
+
this.options.userDir || resolve2(homedir(), ".xcli/plugins"),
|
|
373
|
+
globalDir
|
|
374
|
+
];
|
|
375
|
+
const loaded = [];
|
|
376
|
+
const seen = /* @__PURE__ */ new Set();
|
|
377
|
+
for (const dir of dirs) {
|
|
378
|
+
if (!existsSync3(dir)) continue;
|
|
379
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
if (!entry.isDirectory()) continue;
|
|
382
|
+
if (seen.has(entry.name)) continue;
|
|
383
|
+
seen.add(entry.name);
|
|
384
|
+
const pluginDir = resolve2(dir, entry.name);
|
|
385
|
+
let indexPath = resolve2(pluginDir, "index.js");
|
|
386
|
+
if (!existsSync3(indexPath)) {
|
|
387
|
+
indexPath = resolve2(pluginDir, "index.ts");
|
|
388
|
+
}
|
|
389
|
+
if (!existsSync3(indexPath)) continue;
|
|
390
|
+
try {
|
|
391
|
+
if (!existsSync3(resolve2(pluginDir, "package.json"))) {
|
|
392
|
+
console.warn(`\u26A0\uFE0F Plugin "${entry.name}" has no package.json. Use "xbrowser create ${entry.name} --template static" for proper structure.`);
|
|
393
|
+
} else {
|
|
394
|
+
const metadata = PluginMetadataParser.parseFromPackageJson(pluginDir);
|
|
395
|
+
if (!metadata) {
|
|
396
|
+
console.warn(`\u26A0\uFE0F Plugin "${entry.name}" has package.json but no xbrowser metadata. Add { "xbrowser": { "description": "..." } } to package.json.`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const instance = await this.loadPlugin(indexPath, entry.name);
|
|
400
|
+
loaded.push(instance);
|
|
401
|
+
} catch (err) {
|
|
402
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
403
|
+
console.warn(`\u26A0\uFE0F Plugin "${entry.name}" load failed: ${errMsg}`);
|
|
404
|
+
if (errMsg.includes("Cannot find module") && errMsg.includes("shared/")) {
|
|
405
|
+
console.warn(` \u{1F4A1} This plugin needs shared/ dependencies. Try: xbrowser plugin install shared`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return loaded;
|
|
411
|
+
}
|
|
412
|
+
async unload() {
|
|
413
|
+
return this.loader.unload();
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// src/utils/plugin-singleton.ts
|
|
418
|
+
var pluginLoader = null;
|
|
419
|
+
var pluginsScanned = false;
|
|
420
|
+
async function getPluginLoader() {
|
|
421
|
+
if (!pluginLoader) {
|
|
422
|
+
pluginLoader = new XBrowserPluginLoader();
|
|
423
|
+
}
|
|
424
|
+
if (!pluginsScanned) {
|
|
425
|
+
await pluginLoader.scanAndLoad();
|
|
426
|
+
pluginsScanned = true;
|
|
427
|
+
}
|
|
428
|
+
return pluginLoader;
|
|
429
|
+
}
|
|
430
|
+
function resetPluginLoader() {
|
|
431
|
+
pluginLoader = null;
|
|
432
|
+
pluginsScanned = false;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export {
|
|
436
|
+
getPluginLoader,
|
|
437
|
+
resetPluginLoader
|
|
438
|
+
};
|
|
@@ -279,8 +279,10 @@ async function forwardNetworkInspect(sessionName, id) {
|
|
|
279
279
|
async function forwardRecordStart(session, url, cdpEndpoint) {
|
|
280
280
|
return rpcCall("record:start", { session, url, cdpEndpoint }, 15e3);
|
|
281
281
|
}
|
|
282
|
-
async function forwardRecordStop(session) {
|
|
283
|
-
|
|
282
|
+
async function forwardRecordStop(session, output) {
|
|
283
|
+
const params = { session };
|
|
284
|
+
if (output) params.output = output;
|
|
285
|
+
return rpcCall("record:stop", params, 1e4);
|
|
284
286
|
}
|
|
285
287
|
async function forwardRecordStatus(session) {
|
|
286
288
|
return rpcCall("record:status", { session }, 5e3);
|
package/dist/cli.js
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
killAllDaemonProcesses,
|
|
55
55
|
startDaemonProcess,
|
|
56
56
|
stopDaemonProcess
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-JPSFUFPG.js";
|
|
58
58
|
import {
|
|
59
59
|
errMsg
|
|
60
60
|
} from "./chunk-GDKLH7ZY.js";
|
|
@@ -949,10 +949,10 @@ var setCookieCommand = registerCommand({
|
|
|
949
949
|
description: "Set a cookie",
|
|
950
950
|
scope: "page",
|
|
951
951
|
parameters: z8.object({
|
|
952
|
-
name: z8.string(),
|
|
953
|
-
value: z8.string(),
|
|
954
|
-
domain: z8.string().optional(),
|
|
955
|
-
path: z8.string().optional(),
|
|
952
|
+
name: z8.coerce.string(),
|
|
953
|
+
value: z8.coerce.string(),
|
|
954
|
+
domain: z8.coerce.string().optional(),
|
|
955
|
+
path: z8.coerce.string().optional(),
|
|
956
956
|
expires: z8.number().optional(),
|
|
957
957
|
httpOnly: z8.boolean().optional(),
|
|
958
958
|
secure: z8.boolean().optional(),
|
|
@@ -6331,8 +6331,10 @@ var XBrowserPluginLoader = class {
|
|
|
6331
6331
|
const instance = await this.loadPlugin(indexPath, entry.name);
|
|
6332
6332
|
loaded.push(instance);
|
|
6333
6333
|
} catch (err) {
|
|
6334
|
-
|
|
6335
|
-
|
|
6334
|
+
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
6335
|
+
console.warn(`\u26A0\uFE0F Plugin "${entry.name}" load failed: ${errMsg2}`);
|
|
6336
|
+
if (errMsg2.includes("Cannot find module") && errMsg2.includes("shared/")) {
|
|
6337
|
+
console.warn(` \u{1F4A1} This plugin needs shared/ dependencies. Try: xbrowser plugin install shared`);
|
|
6336
6338
|
}
|
|
6337
6339
|
}
|
|
6338
6340
|
}
|
|
@@ -7048,7 +7050,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
7048
7050
|
params = result.data;
|
|
7049
7051
|
}
|
|
7050
7052
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
7051
|
-
const { forwardExec } = await import("./daemon-client-
|
|
7053
|
+
const { forwardExec } = await import("./daemon-client-XXKMJZZ7.js");
|
|
7052
7054
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
7053
7055
|
if (result) return result;
|
|
7054
7056
|
}
|
|
@@ -7734,9 +7736,12 @@ import {
|
|
|
7734
7736
|
existsSync as existsSync10,
|
|
7735
7737
|
readdirSync as readdirSync2,
|
|
7736
7738
|
mkdirSync as mkdirSync8,
|
|
7737
|
-
rmSync as rmSync6
|
|
7739
|
+
rmSync as rmSync6,
|
|
7740
|
+
copyFileSync,
|
|
7741
|
+
cpSync as cpSync6,
|
|
7742
|
+
readFileSync as readFileSync8
|
|
7738
7743
|
} from "fs";
|
|
7739
|
-
import { resolve as resolve8, basename as basename2 } from "path";
|
|
7744
|
+
import { resolve as resolve8, basename as basename2, dirname as dirname3 } from "path";
|
|
7740
7745
|
import { homedir as homedir8 } from "os";
|
|
7741
7746
|
|
|
7742
7747
|
// src/plugin/install-sources/local.ts
|
|
@@ -8229,28 +8234,103 @@ var PluginInstaller = class {
|
|
|
8229
8234
|
switch (type) {
|
|
8230
8235
|
case "local":
|
|
8231
8236
|
return await installFromLocal(source, name, targetDir).then((r) => {
|
|
8237
|
+
this.fixSharedDeps(targetDir);
|
|
8232
8238
|
ensurePluginDependencies(this.pluginsDir);
|
|
8233
8239
|
return r;
|
|
8234
8240
|
});
|
|
8235
8241
|
case "npm":
|
|
8236
8242
|
return await installFromNpm(resolvedSource, name, targetDir).then((r) => {
|
|
8243
|
+
this.fixSharedDeps(targetDir);
|
|
8237
8244
|
ensurePluginDependencies(this.pluginsDir);
|
|
8238
8245
|
return r;
|
|
8239
8246
|
});
|
|
8240
8247
|
case "git":
|
|
8241
8248
|
return await installFromGit(source, name, targetDir).then((r) => {
|
|
8249
|
+
this.fixSharedDeps(targetDir);
|
|
8242
8250
|
ensurePluginDependencies(this.pluginsDir);
|
|
8243
8251
|
return r;
|
|
8244
8252
|
});
|
|
8245
8253
|
case "url":
|
|
8246
8254
|
return await installFromUrl(source, name, targetDir).then((r) => {
|
|
8255
|
+
this.fixSharedDeps(targetDir);
|
|
8247
8256
|
ensurePluginDependencies(this.pluginsDir);
|
|
8248
8257
|
return r;
|
|
8249
8258
|
});
|
|
8250
8259
|
}
|
|
8251
8260
|
}
|
|
8261
|
+
/**
|
|
8262
|
+
* Fix missing `../shared/` dependencies after installation.
|
|
8263
|
+
*
|
|
8264
|
+
* Some marketplace/npm packages import from `../shared/` (e.g. ssr-detect.js,
|
|
8265
|
+
* ai-chat-base.ts) but the `shared/` directory is not included in the package.
|
|
8266
|
+
* This method scans the installed plugin's index.ts for such imports and
|
|
8267
|
+
* copies the missing files from the local repository's `.xcli/plugins/shared/`
|
|
8268
|
+
* directory (if available).
|
|
8269
|
+
*/
|
|
8270
|
+
fixSharedDeps(pluginDir) {
|
|
8271
|
+
const indexPath = resolve8(pluginDir, "index.ts");
|
|
8272
|
+
if (!existsSync10(indexPath)) return;
|
|
8273
|
+
let content;
|
|
8274
|
+
try {
|
|
8275
|
+
content = readFileSync8(indexPath, "utf8");
|
|
8276
|
+
} catch {
|
|
8277
|
+
return;
|
|
8278
|
+
}
|
|
8279
|
+
const sharedImportRegex = /from\s+['"]\.\.\/shared\/([^'"]+)['"]/g;
|
|
8280
|
+
const missingFiles = [];
|
|
8281
|
+
let match;
|
|
8282
|
+
while ((match = sharedImportRegex.exec(content)) !== null) {
|
|
8283
|
+
missingFiles.push(match[1]);
|
|
8284
|
+
}
|
|
8285
|
+
if (missingFiles.length === 0) return;
|
|
8286
|
+
const sharedDir = resolve8(pluginDir, "..", "shared");
|
|
8287
|
+
const toCopy = [];
|
|
8288
|
+
for (const file of missingFiles) {
|
|
8289
|
+
const targetPath = resolve8(sharedDir, file);
|
|
8290
|
+
if (!existsSync10(targetPath)) {
|
|
8291
|
+
toCopy.push(file);
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
if (toCopy.length === 0) return;
|
|
8295
|
+
const repoSharedDirs = [
|
|
8296
|
+
resolve8(process.cwd(), ".xcli/plugins/shared"),
|
|
8297
|
+
resolve8(homedir8(), ".xbrowser/plugins/shared")
|
|
8298
|
+
];
|
|
8299
|
+
let sourceSharedDir = null;
|
|
8300
|
+
for (const dir of repoSharedDirs) {
|
|
8301
|
+
if (existsSync10(dir)) {
|
|
8302
|
+
sourceSharedDir = dir;
|
|
8303
|
+
break;
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
8306
|
+
if (!sourceSharedDir) {
|
|
8307
|
+
console.warn(`\u26A0\uFE0F Plugin "${basename2(pluginDir)}" imports shared files but they are missing: ${toCopy.join(", ")}`);
|
|
8308
|
+
console.warn(` To fix: install the "shared" plugin or copy .xcli/plugins/shared/ to ~/.xbrowser/plugins/shared/`);
|
|
8309
|
+
return;
|
|
8310
|
+
}
|
|
8311
|
+
mkdirSync8(sharedDir, { recursive: true });
|
|
8312
|
+
for (const file of toCopy) {
|
|
8313
|
+
const src = resolve8(sourceSharedDir, file);
|
|
8314
|
+
const dst = resolve8(sharedDir, file);
|
|
8315
|
+
if (existsSync10(src)) {
|
|
8316
|
+
try {
|
|
8317
|
+
cpSync6(dirname3(src), dirname3(dst), { recursive: true });
|
|
8318
|
+
console.log(`\u2705 Copied shared/${file} for plugin "${basename2(pluginDir)}"`);
|
|
8319
|
+
} catch {
|
|
8320
|
+
try {
|
|
8321
|
+
copyFileSync(src, dst);
|
|
8322
|
+
console.log(`\u2705 Copied shared/${file} for plugin "${basename2(pluginDir)}"`);
|
|
8323
|
+
} catch {
|
|
8324
|
+
console.warn(`\u26A0\uFE0F Could not copy shared/${file}`);
|
|
8325
|
+
}
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8252
8330
|
async installFromMarketplace(slug, options) {
|
|
8253
8331
|
const result = await installFromMarketplace(this.pluginsDir, slug, options);
|
|
8332
|
+
const targetDir = resolve8(this.pluginsDir, result.name);
|
|
8333
|
+
this.fixSharedDeps(targetDir);
|
|
8254
8334
|
ensurePluginDependencies(this.pluginsDir);
|
|
8255
8335
|
return result;
|
|
8256
8336
|
}
|
|
@@ -10272,6 +10352,18 @@ async function handlePlugin(args, options, mode) {
|
|
|
10272
10352
|
await (await getPluginLoader()).reloadPlugin(result.name);
|
|
10273
10353
|
} catch {
|
|
10274
10354
|
}
|
|
10355
|
+
try {
|
|
10356
|
+
const { daemonPing } = await import("./daemon-client-XXKMJZZ7.js");
|
|
10357
|
+
if (await daemonPing()) {
|
|
10358
|
+
await fetch("http://localhost:9224/rpc", {
|
|
10359
|
+
method: "POST",
|
|
10360
|
+
headers: { "Content-Type": "application/json" },
|
|
10361
|
+
body: JSON.stringify({ method: "plugins:reload", params: {} }),
|
|
10362
|
+
signal: AbortSignal.timeout(5e3)
|
|
10363
|
+
});
|
|
10364
|
+
}
|
|
10365
|
+
} catch {
|
|
10366
|
+
}
|
|
10275
10367
|
outputResult(
|
|
10276
10368
|
{ ok: true, name: result.name, source: result.source, path: result.path },
|
|
10277
10369
|
mode
|
|
@@ -10427,7 +10519,8 @@ async function handleRecord(args, options, mode) {
|
|
|
10427
10519
|
}
|
|
10428
10520
|
case "stop": {
|
|
10429
10521
|
const sessionName = options.session || "default";
|
|
10430
|
-
const
|
|
10522
|
+
const output = options.output || options.o;
|
|
10523
|
+
const result = await forwardRecordStop(sessionName, output);
|
|
10431
10524
|
if (!result.ok) {
|
|
10432
10525
|
outputError(String(result.error || "Failed to stop recording"));
|
|
10433
10526
|
return;
|
|
@@ -10436,6 +10529,7 @@ async function handleRecord(args, options, mode) {
|
|
|
10436
10529
|
ok: true,
|
|
10437
10530
|
message: "Recording stopped.",
|
|
10438
10531
|
sessionName,
|
|
10532
|
+
output: result.output || (output || SessionRecorder.getRecordingsDir(sessionName) + "/recording.json"),
|
|
10439
10533
|
actions: result.actions,
|
|
10440
10534
|
network: result.network,
|
|
10441
10535
|
durationMs: result.durationMs,
|
|
@@ -11299,7 +11393,7 @@ async function handleNetCommand(args, options, mode, sessionName) {
|
|
|
11299
11393
|
|
|
11300
11394
|
// src/cli/test-routes.ts
|
|
11301
11395
|
import { execSync as execSync3 } from "child_process";
|
|
11302
|
-
import { readFileSync as
|
|
11396
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
11303
11397
|
import { resolve as resolve9 } from "path";
|
|
11304
11398
|
function findPluginPath(plugin) {
|
|
11305
11399
|
const candidates = [
|
|
@@ -11308,7 +11402,7 @@ function findPluginPath(plugin) {
|
|
|
11308
11402
|
];
|
|
11309
11403
|
for (const p of candidates) {
|
|
11310
11404
|
try {
|
|
11311
|
-
|
|
11405
|
+
readFileSync9(p, "utf-8");
|
|
11312
11406
|
return p;
|
|
11313
11407
|
} catch {
|
|
11314
11408
|
}
|
|
@@ -11319,7 +11413,7 @@ function extractSchema(plugin, command) {
|
|
|
11319
11413
|
const pluginPath = findPluginPath(plugin);
|
|
11320
11414
|
let src;
|
|
11321
11415
|
try {
|
|
11322
|
-
src =
|
|
11416
|
+
src = readFileSync9(pluginPath, "utf-8");
|
|
11323
11417
|
} catch {
|
|
11324
11418
|
return null;
|
|
11325
11419
|
}
|
|
@@ -12549,7 +12643,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
|
|
|
12549
12643
|
}
|
|
12550
12644
|
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
12551
12645
|
if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
12552
|
-
const { forwardExec } = await import("./daemon-client-
|
|
12646
|
+
const { forwardExec } = await import("./daemon-client-XXKMJZZ7.js");
|
|
12553
12647
|
const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
|
|
12554
12648
|
const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
|
|
12555
12649
|
const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
|
|
@@ -162,8 +162,10 @@ async function forwardNetworkInspect(sessionName, id) {
|
|
|
162
162
|
async function forwardRecordStart(session, url, cdpEndpoint) {
|
|
163
163
|
return rpcCall("record:start", { session, url, cdpEndpoint }, 15e3);
|
|
164
164
|
}
|
|
165
|
-
async function forwardRecordStop(session) {
|
|
166
|
-
|
|
165
|
+
async function forwardRecordStop(session, output) {
|
|
166
|
+
const params = { session };
|
|
167
|
+
if (output) params.output = output;
|
|
168
|
+
return rpcCall("record:stop", params, 1e4);
|
|
167
169
|
}
|
|
168
170
|
async function forwardRecordStatus(session) {
|
|
169
171
|
return rpcCall("record:status", { session }, 5e3);
|