@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.
@@ -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
- 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);
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(),
@@ -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
- if (process.env.XBROWSER_DEBUG) {
6335
- console.warn(`\u26A0\uFE0F Plugin "${entry.name}" load failed: ${err instanceof Error ? err.message : String(err)}`);
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-ZHO6NG36.js");
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 result = await forwardRecordStop(sessionName);
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 readFileSync8 } from "fs";
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
- readFileSync8(p, "utf-8");
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 = readFileSync8(pluginPath, "utf-8");
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-ZHO6NG36.js");
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
- 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);