@xbrowser/cli 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- return rpcCall("record:stop", { session }, 1e4);
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);
@@ -0,0 +1,436 @@
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
+ if (process.env.XBROWSER_DEBUG) {
403
+ console.warn(`\u26A0\uFE0F Plugin "${entry.name}" load failed: ${err instanceof Error ? err.message : String(err)}`);
404
+ }
405
+ }
406
+ }
407
+ }
408
+ return loaded;
409
+ }
410
+ async unload() {
411
+ return this.loader.unload();
412
+ }
413
+ };
414
+
415
+ // src/utils/plugin-singleton.ts
416
+ var pluginLoader = null;
417
+ var pluginsScanned = false;
418
+ async function getPluginLoader() {
419
+ if (!pluginLoader) {
420
+ pluginLoader = new XBrowserPluginLoader();
421
+ }
422
+ if (!pluginsScanned) {
423
+ await pluginLoader.scanAndLoad();
424
+ pluginsScanned = true;
425
+ }
426
+ return pluginLoader;
427
+ }
428
+ function resetPluginLoader() {
429
+ pluginLoader = null;
430
+ pluginsScanned = false;
431
+ }
432
+
433
+ export {
434
+ getPluginLoader,
435
+ resetPluginLoader
436
+ };
package/dist/cli.js CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  killAllDaemonProcesses,
55
55
  startDaemonProcess,
56
56
  stopDaemonProcess
57
- } from "./chunk-XYXCS7JW.js";
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(),
@@ -7048,7 +7048,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7048
7048
  params = result.data;
7049
7049
  }
7050
7050
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
7051
- const { forwardExec } = await import("./daemon-client-ZHO6NG36.js");
7051
+ const { forwardExec } = await import("./daemon-client-XXKMJZZ7.js");
7052
7052
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
7053
7053
  if (result) return result;
7054
7054
  }
@@ -10272,6 +10272,18 @@ async function handlePlugin(args, options, mode) {
10272
10272
  await (await getPluginLoader()).reloadPlugin(result.name);
10273
10273
  } catch {
10274
10274
  }
10275
+ try {
10276
+ const { daemonPing } = await import("./daemon-client-XXKMJZZ7.js");
10277
+ if (await daemonPing()) {
10278
+ await fetch("http://localhost:9224/rpc", {
10279
+ method: "POST",
10280
+ headers: { "Content-Type": "application/json" },
10281
+ body: JSON.stringify({ method: "plugins:reload", params: {} }),
10282
+ signal: AbortSignal.timeout(5e3)
10283
+ });
10284
+ }
10285
+ } catch {
10286
+ }
10275
10287
  outputResult(
10276
10288
  { ok: true, name: result.name, source: result.source, path: result.path },
10277
10289
  mode
@@ -10427,7 +10439,8 @@ async function handleRecord(args, options, mode) {
10427
10439
  }
10428
10440
  case "stop": {
10429
10441
  const sessionName = options.session || "default";
10430
- const result = await forwardRecordStop(sessionName);
10442
+ const output = options.output || options.o;
10443
+ const result = await forwardRecordStop(sessionName, output);
10431
10444
  if (!result.ok) {
10432
10445
  outputError(String(result.error || "Failed to stop recording"));
10433
10446
  return;
@@ -10436,6 +10449,7 @@ async function handleRecord(args, options, mode) {
10436
10449
  ok: true,
10437
10450
  message: "Recording stopped.",
10438
10451
  sessionName,
10452
+ output: result.output || (output || SessionRecorder.getRecordingsDir(sessionName) + "/recording.json"),
10439
10453
  actions: result.actions,
10440
10454
  network: result.network,
10441
10455
  durationMs: result.durationMs,
@@ -12549,7 +12563,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
12549
12563
  }
12550
12564
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
12551
12565
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
12552
- const { forwardExec } = await import("./daemon-client-ZHO6NG36.js");
12566
+ const { forwardExec } = await import("./daemon-client-XXKMJZZ7.js");
12553
12567
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
12554
12568
  const result = await forwardExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
12555
12569
  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
- return rpcCall("record:stop", { session }, 1e4);
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);
@@ -29,7 +29,7 @@ import {
29
29
  forwardSessionList,
30
30
  forwardViewerCheckSelector,
31
31
  isDaemonRunning
32
- } from "./chunk-XYXCS7JW.js";
32
+ } from "./chunk-JPSFUFPG.js";
33
33
  import "./chunk-GDKLH7ZY.js";
34
34
  import "./chunk-KFQGP6VL.js";
35
35
  export {