@xbrowser/cli 0.14.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 +858 -0
- package/dist/admin-6UTU2RZ2.js +281 -0
- package/dist/admin-MDGF4CET.js +285 -0
- package/dist/admin-RPJJ5CAF.js +282 -0
- package/dist/browser-GWBH6OJK.js +46 -0
- package/dist/browser-I2HJZ7IP.js +48 -0
- package/dist/browser-R7B255ML.js +46 -0
- package/dist/chunk-2ONMTDLK.js +2050 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-43VX3TYN.js +83 -0
- package/dist/chunk-ATFTAKMN.js +267 -0
- package/dist/chunk-DESA2KMG.js +77 -0
- package/dist/chunk-DTJRVA76.js +206 -0
- package/dist/chunk-F3ZWFCJJ.js +2051 -0
- package/dist/chunk-FF5WHQHN.js +135 -0
- package/dist/chunk-HINTG75P.js +77 -0
- package/dist/chunk-KDYXFLAC.js +1503 -0
- package/dist/chunk-KTSQU4QT.js +29 -0
- package/dist/chunk-L53IDAWK.js +68 -0
- package/dist/chunk-M7CMBPCA.js +100 -0
- package/dist/chunk-NFGO7J2I.js +29 -0
- package/dist/chunk-OLB6UJ25.js +438 -0
- package/dist/chunk-OPRXFZVE.js +52 -0
- package/dist/chunk-RS6YYWTK.js +685 -0
- package/dist/chunk-VEDJ5XSQ.js +196 -0
- package/dist/chunk-VEKPHQBR.js +47 -0
- package/dist/chunk-VUJDJCIN.js +437 -0
- package/dist/chunk-YEN2ODUI.js +14 -0
- package/dist/chunk-ZZ2TFWIV.js +1382 -0
- package/dist/cli.js +11012 -0
- package/dist/convert-4DUWZIKH.js +205 -0
- package/dist/convert-EKQVHKB4.js +11 -0
- package/dist/daemon-client-3IJD6X4B.js +59 -0
- package/dist/daemon-client-GX2UYIW4.js +241 -0
- package/dist/daemon-client-XWSSQBEA.js +58 -0
- package/dist/daemon-main.js +9910 -0
- package/dist/extract-EGRXZSSK.js +67 -0
- package/dist/extract-JUOQQX4V.js +11 -0
- package/dist/filter-OLAE26HN.js +51 -0
- package/dist/filter-VID2GGZ7.js +9 -0
- package/dist/human-interaction-QPHNDD76.js +8 -0
- package/dist/index.d.ts +2313 -0
- package/dist/index.js +13839 -0
- package/dist/marketplace-FCVN5OTZ.js +706 -0
- package/dist/marketplace-FPT5YLKB.js +351 -0
- package/dist/marketplace-W545W4FR.js +706 -0
- package/dist/network-store-2S5HATEV.js +194 -0
- package/dist/network-store-BN6QEZ7R.js +196 -0
- package/dist/network-store-YAF5OIBH.js +12 -0
- package/dist/parse-action-dsl-DRSPBALP.js +72 -0
- package/dist/parse-action-dsl-T3DYC33D.js +74 -0
- package/dist/proxy-WKGUCH2C.js +7 -0
- package/dist/session-recorder-ILSSV2UC.js +6 -0
- package/dist/session-recorder-XET3DNML.js +7 -0
- package/package.json +111 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__require
|
|
10
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readJsonFile
|
|
3
|
+
} from "./chunk-YEN2ODUI.js";
|
|
4
|
+
|
|
5
|
+
// src/config.ts
|
|
6
|
+
var DEFAULT_REGISTRY_URL = "https://xbrowser.dev";
|
|
7
|
+
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
8
|
+
function getRegistryUrl(options = {}, fallbackRegistry) {
|
|
9
|
+
return options["registry"] || process.env.XBROWSER_REGISTRY || fallbackRegistry || DEFAULT_REGISTRY_URL;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/plugin/builtins/shared.ts
|
|
13
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
14
|
+
import { resolve } from "path";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
function getAuthDir() {
|
|
17
|
+
return resolve(homedir(), ".xbrowser");
|
|
18
|
+
}
|
|
19
|
+
function getAuthFile() {
|
|
20
|
+
return resolve(getAuthDir(), "auth.json");
|
|
21
|
+
}
|
|
22
|
+
function loadAuth() {
|
|
23
|
+
const authFile = getAuthFile();
|
|
24
|
+
if (!existsSync(authFile)) return null;
|
|
25
|
+
return readJsonFile(authFile, null);
|
|
26
|
+
}
|
|
27
|
+
function saveAuth(config) {
|
|
28
|
+
const dir = getAuthDir();
|
|
29
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
30
|
+
writeFileSync(getAuthFile(), JSON.stringify(config, null, 2), "utf-8");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/utils/proxy-fetch.ts
|
|
34
|
+
var patched = false;
|
|
35
|
+
async function ensureProxyFetch() {
|
|
36
|
+
if (patched) return;
|
|
37
|
+
patched = true;
|
|
38
|
+
if (process.env.https_proxy && !process.env.HTTPS_PROXY) {
|
|
39
|
+
process.env.HTTPS_PROXY = process.env.https_proxy;
|
|
40
|
+
}
|
|
41
|
+
if (process.env.http_proxy && !process.env.HTTP_PROXY) {
|
|
42
|
+
process.env.HTTP_PROXY = process.env.http_proxy;
|
|
43
|
+
}
|
|
44
|
+
if (process.env.all_proxy && !process.env.ALL_PROXY) {
|
|
45
|
+
process.env.ALL_PROXY = process.env.all_proxy;
|
|
46
|
+
}
|
|
47
|
+
const proxyUrl = process.env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || process.env.all_proxy || process.env.ALL_PROXY;
|
|
48
|
+
if (!proxyUrl) return;
|
|
49
|
+
try {
|
|
50
|
+
const undici = await import("undici");
|
|
51
|
+
const EnvHttpProxyAgent = undici.EnvHttpProxyAgent;
|
|
52
|
+
const uFetch = undici.fetch;
|
|
53
|
+
const UFormData = undici.FormData;
|
|
54
|
+
if (EnvHttpProxyAgent && uFetch && UFormData) {
|
|
55
|
+
const agent = new EnvHttpProxyAgent();
|
|
56
|
+
globalThis.fetch = ((input, init) => {
|
|
57
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
58
|
+
const body = init?.body;
|
|
59
|
+
if (body instanceof globalThis.FormData && !(body instanceof UFormData)) {
|
|
60
|
+
const ufd = new UFormData();
|
|
61
|
+
body.forEach((value, key) => {
|
|
62
|
+
if (value instanceof Blob) {
|
|
63
|
+
ufd.append(key, value, value.name || "file");
|
|
64
|
+
} else {
|
|
65
|
+
ufd.append(key, value);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return uFetch(url, { ...init, body: ufd, dispatcher: agent });
|
|
69
|
+
}
|
|
70
|
+
return uFetch(url, { ...init, dispatcher: agent });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
NPM_REGISTRY_URL,
|
|
79
|
+
getRegistryUrl,
|
|
80
|
+
loadAuth,
|
|
81
|
+
saveAuth,
|
|
82
|
+
ensureProxyFetch
|
|
83
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/daemon/daemon.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { stopDaemon as xcliStopDaemon, isDaemonRunning, getDaemonStatus, killAllDaemon } from "@dyyz1993/xcli-core";
|
|
7
|
+
var CONFIG_DIR = join(homedir(), ".xbrowser");
|
|
8
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
var WORKER_PATH = join(__dirname, "daemon-main.js");
|
|
10
|
+
function getDaemonConfig() {
|
|
11
|
+
return {
|
|
12
|
+
configDir: CONFIG_DIR,
|
|
13
|
+
workerEntryPath: WORKER_PATH,
|
|
14
|
+
basePort: 9224
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async function startDaemonProcess(port = 9224) {
|
|
18
|
+
const config = getDaemonConfig();
|
|
19
|
+
if (isDaemonRunning(config)) {
|
|
20
|
+
const status = getDaemonStatus(config);
|
|
21
|
+
if (status.port === port && status.pid) {
|
|
22
|
+
return { pid: status.pid, port: status.port, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
23
|
+
}
|
|
24
|
+
await xcliStopDaemon(config);
|
|
25
|
+
}
|
|
26
|
+
const child = spawn("node", [WORKER_PATH], {
|
|
27
|
+
detached: true,
|
|
28
|
+
stdio: "ignore",
|
|
29
|
+
env: {
|
|
30
|
+
...process.env,
|
|
31
|
+
XBROWSER_DAEMON_PORT: String(port)
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
child.unref();
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let resolved = false;
|
|
37
|
+
const timeout = setTimeout(() => {
|
|
38
|
+
if (!resolved) {
|
|
39
|
+
resolved = true;
|
|
40
|
+
reject(new Error("Daemon start timeout after 15s"));
|
|
41
|
+
}
|
|
42
|
+
}, 15e3);
|
|
43
|
+
const checkInterval = setInterval(() => {
|
|
44
|
+
if (isDaemonRunning(config)) {
|
|
45
|
+
const s = getDaemonStatus(config);
|
|
46
|
+
if (s.port === port && s.pid) {
|
|
47
|
+
resolved = true;
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
clearInterval(checkInterval);
|
|
50
|
+
resolve({ pid: s.pid, port: s.port, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}, 200);
|
|
54
|
+
child.on("error", (err) => {
|
|
55
|
+
if (!resolved) {
|
|
56
|
+
resolved = true;
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
clearInterval(checkInterval);
|
|
59
|
+
reject(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function stopDaemonProcess() {
|
|
65
|
+
await xcliStopDaemon(getDaemonConfig());
|
|
66
|
+
}
|
|
67
|
+
async function killAllDaemonProcesses() {
|
|
68
|
+
await killAllDaemon(getDaemonConfig());
|
|
69
|
+
}
|
|
70
|
+
function getDaemonProcessStatus() {
|
|
71
|
+
const config = getDaemonConfig();
|
|
72
|
+
const running = isDaemonRunning(config);
|
|
73
|
+
if (!running) {
|
|
74
|
+
return { running: false, pid: 0, port: 0, info: null };
|
|
75
|
+
}
|
|
76
|
+
const status = getDaemonStatus(config);
|
|
77
|
+
return {
|
|
78
|
+
running: true,
|
|
79
|
+
pid: status.pid,
|
|
80
|
+
port: status.port,
|
|
81
|
+
info: { pid: status.pid, port: status.port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/client/daemon-client.ts
|
|
86
|
+
var DAEMON_PORT = 9224;
|
|
87
|
+
var DAEMON_BASE = `http://localhost:${DAEMON_PORT}`;
|
|
88
|
+
var _ensurePromise = null;
|
|
89
|
+
async function ensureDaemonRunning() {
|
|
90
|
+
if (_ensurePromise) {
|
|
91
|
+
try {
|
|
92
|
+
await _ensurePromise;
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
_ensurePromise = null;
|
|
96
|
+
}
|
|
97
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
98
|
+
const healthOk = await fetch(`${DAEMON_BASE}/health`, { signal: AbortSignal.timeout(2e3) }).then((r) => r.ok ? r.json() : null).then((d) => d?.status === "ok").catch(() => false);
|
|
99
|
+
if (healthOk) return;
|
|
100
|
+
if (attempt < 2) await new Promise((r) => setTimeout(r, 500));
|
|
101
|
+
}
|
|
102
|
+
_ensurePromise = startDaemonProcess(DAEMON_PORT).then(() => {
|
|
103
|
+
});
|
|
104
|
+
_ensurePromise.catch(() => {
|
|
105
|
+
_ensurePromise = null;
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
await _ensurePromise;
|
|
109
|
+
} catch {
|
|
110
|
+
_ensurePromise = null;
|
|
111
|
+
throw new Error("Daemon not available");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function rpcCall(method, params = {}, timeoutMs = 1e4) {
|
|
115
|
+
await ensureDaemonRunning();
|
|
116
|
+
const resp = await fetch(`${DAEMON_BASE}/rpc`, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
headers: { "Content-Type": "application/json" },
|
|
119
|
+
body: JSON.stringify({ method, params }),
|
|
120
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
121
|
+
});
|
|
122
|
+
if (!resp.ok) {
|
|
123
|
+
throw new Error(`Daemon error: ${resp.statusText}`);
|
|
124
|
+
}
|
|
125
|
+
return resp.json();
|
|
126
|
+
}
|
|
127
|
+
async function isDaemonRunning2() {
|
|
128
|
+
try {
|
|
129
|
+
const resp = await fetch(`${DAEMON_BASE}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
130
|
+
if (!resp.ok) return false;
|
|
131
|
+
const data = await resp.json();
|
|
132
|
+
return data.status === "ok";
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function daemonPing() {
|
|
138
|
+
try {
|
|
139
|
+
return await rpcCall("ping", {}, 2e3);
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function forwardSessionCreate(name, url, cdpEndpoint) {
|
|
145
|
+
const params = { name, url };
|
|
146
|
+
if (cdpEndpoint) params.cdpEndpoint = cdpEndpoint;
|
|
147
|
+
return rpcCall("session:create", params, 3e4);
|
|
148
|
+
}
|
|
149
|
+
async function forwardSessionClose(name) {
|
|
150
|
+
return rpcCall("session:close", { name }, 1e4);
|
|
151
|
+
}
|
|
152
|
+
async function forwardSessionList() {
|
|
153
|
+
return rpcCall("session:list", {}, 5e3);
|
|
154
|
+
}
|
|
155
|
+
async function forwardExec(command, params, session = "default", cdpEndpoint, timeoutMs = 12e4) {
|
|
156
|
+
const rpcParams = { command, params, session };
|
|
157
|
+
if (cdpEndpoint) rpcParams.cdpEndpoint = cdpEndpoint;
|
|
158
|
+
try {
|
|
159
|
+
return await rpcCall("exec", rpcParams, timeoutMs);
|
|
160
|
+
} catch {
|
|
161
|
+
return { success: false, data: null, message: `Daemon error: exec failed`, duration: 0 };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function forwardChain(input, session = "default", cdpEndpoint) {
|
|
165
|
+
const params = { chain: input, session };
|
|
166
|
+
if (cdpEndpoint) params.cdpEndpoint = cdpEndpoint;
|
|
167
|
+
try {
|
|
168
|
+
return await rpcCall("chain", params, 12e4);
|
|
169
|
+
} catch {
|
|
170
|
+
return { success: false, steps: [], totalDuration: 0, stoppedReason: "Daemon error" };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function forwardNetworkList(sessionName, options) {
|
|
174
|
+
return rpcCall("network:list", { session: sessionName, ...options }, 3e4);
|
|
175
|
+
}
|
|
176
|
+
async function forwardNetworkClear(sessionName) {
|
|
177
|
+
return rpcCall("network:clear", { session: sessionName }, 1e4);
|
|
178
|
+
}
|
|
179
|
+
async function forwardNetworkTop(sessionName, options) {
|
|
180
|
+
return rpcCall("network:top", { session: sessionName, ...options }, 3e4);
|
|
181
|
+
}
|
|
182
|
+
async function forwardCommandLog(sessionName, limit) {
|
|
183
|
+
return rpcCall("command:log", { session: sessionName, limit }, 1e4);
|
|
184
|
+
}
|
|
185
|
+
async function forwardNetworkAnalyze(sessionName) {
|
|
186
|
+
return rpcCall("network:analyze", { session: sessionName }, 3e4);
|
|
187
|
+
}
|
|
188
|
+
async function forwardNetworkAround(sessionName, commandId, windowMs) {
|
|
189
|
+
return rpcCall("network:around", { session: sessionName, commandId, window: windowMs }, 1e4);
|
|
190
|
+
}
|
|
191
|
+
async function forwardNetworkCurl(sessionName, id) {
|
|
192
|
+
return rpcCall("network:curl", { session: sessionName, id }, 1e4);
|
|
193
|
+
}
|
|
194
|
+
async function forwardNetworkReplay(sessionName, id) {
|
|
195
|
+
return rpcCall("network:replay", { session: sessionName, id }, 3e4);
|
|
196
|
+
}
|
|
197
|
+
async function forwardNetworkLike(sessionName, id) {
|
|
198
|
+
return rpcCall("network:like", { session: sessionName, id }, 5e3);
|
|
199
|
+
}
|
|
200
|
+
async function forwardNetworkDislike(sessionName, id) {
|
|
201
|
+
return rpcCall("network:dislike", { session: sessionName, id }, 5e3);
|
|
202
|
+
}
|
|
203
|
+
async function forwardNetworkExport(sessionName, id, lang) {
|
|
204
|
+
return rpcCall("network:export", { session: sessionName, id, lang }, 1e4);
|
|
205
|
+
}
|
|
206
|
+
async function forwardNetworkInspect(sessionName, id) {
|
|
207
|
+
return rpcCall("network:inspect", { session: sessionName, id }, 1e4);
|
|
208
|
+
}
|
|
209
|
+
async function forwardRecordStart(session, url) {
|
|
210
|
+
return rpcCall("record:start", { session, url }, 15e3);
|
|
211
|
+
}
|
|
212
|
+
async function forwardRecordStop(session) {
|
|
213
|
+
return rpcCall("record:stop", { session }, 1e4);
|
|
214
|
+
}
|
|
215
|
+
async function forwardRecordStatus(session) {
|
|
216
|
+
return rpcCall("record:status", { session }, 5e3);
|
|
217
|
+
}
|
|
218
|
+
async function forwardRecordSummary(session) {
|
|
219
|
+
return rpcCall("record:summary", { session }, 5e3);
|
|
220
|
+
}
|
|
221
|
+
async function forwardReplay(file, session, slowMo) {
|
|
222
|
+
return rpcCall("replay", { file, session, slowMo }, 12e4);
|
|
223
|
+
}
|
|
224
|
+
async function forwardRecordCheckpoint(session, type, hint, selector) {
|
|
225
|
+
return rpcCall("record:checkpoint", { session, type, hint, selector }, 1e4);
|
|
226
|
+
}
|
|
227
|
+
async function forwardReplayResume(session) {
|
|
228
|
+
return rpcCall("replay:resume", { session }, 1e4);
|
|
229
|
+
}
|
|
230
|
+
async function forwardViewerCheckSelector(name, selector) {
|
|
231
|
+
return rpcCall("viewer:check-selector", { name, selector }, 5e3);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
getDaemonConfig,
|
|
236
|
+
startDaemonProcess,
|
|
237
|
+
stopDaemonProcess,
|
|
238
|
+
killAllDaemonProcesses,
|
|
239
|
+
getDaemonProcessStatus,
|
|
240
|
+
isDaemonRunning2 as isDaemonRunning,
|
|
241
|
+
daemonPing,
|
|
242
|
+
forwardSessionCreate,
|
|
243
|
+
forwardSessionClose,
|
|
244
|
+
forwardSessionList,
|
|
245
|
+
forwardExec,
|
|
246
|
+
forwardChain,
|
|
247
|
+
forwardNetworkList,
|
|
248
|
+
forwardNetworkClear,
|
|
249
|
+
forwardNetworkTop,
|
|
250
|
+
forwardCommandLog,
|
|
251
|
+
forwardNetworkAnalyze,
|
|
252
|
+
forwardNetworkAround,
|
|
253
|
+
forwardNetworkCurl,
|
|
254
|
+
forwardNetworkReplay,
|
|
255
|
+
forwardNetworkLike,
|
|
256
|
+
forwardNetworkDislike,
|
|
257
|
+
forwardNetworkExport,
|
|
258
|
+
forwardNetworkInspect,
|
|
259
|
+
forwardRecordStart,
|
|
260
|
+
forwardRecordStop,
|
|
261
|
+
forwardRecordStatus,
|
|
262
|
+
forwardRecordSummary,
|
|
263
|
+
forwardReplay,
|
|
264
|
+
forwardRecordCheckpoint,
|
|
265
|
+
forwardReplayResume,
|
|
266
|
+
forwardViewerCheckSelector
|
|
267
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readJsonFile
|
|
3
|
+
} from "./chunk-YEN2ODUI.js";
|
|
4
|
+
|
|
5
|
+
// src/plugin/metadata-parser.ts
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
var PluginMetadataParser = class {
|
|
9
|
+
static XBROWSER_KEYWORDS = ["xbrowser", "xbrowser-plugin"];
|
|
10
|
+
static parseFromPackageJson(pluginPath) {
|
|
11
|
+
const packageJsonPath = resolve(pluginPath, "package.json");
|
|
12
|
+
if (!existsSync(packageJsonPath)) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const packageJson = readJsonFile(packageJsonPath, null);
|
|
16
|
+
if (!packageJson) return null;
|
|
17
|
+
if (!packageJson.xbrowser) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const xbrowser = packageJson.xbrowser;
|
|
21
|
+
const metadata = {
|
|
22
|
+
id: xbrowser.id || packageJson.name,
|
|
23
|
+
name: xbrowser.name || packageJson.name,
|
|
24
|
+
description: xbrowser.description || packageJson.description || "",
|
|
25
|
+
version: xbrowser.version || packageJson.version || "1.0.0",
|
|
26
|
+
author: xbrowser.author || this.extractAuthor(packageJson.author),
|
|
27
|
+
homepage: xbrowser.homepage || packageJson.homepage,
|
|
28
|
+
commands: xbrowser.commands,
|
|
29
|
+
sites: xbrowser.sites,
|
|
30
|
+
tags: xbrowser.tags,
|
|
31
|
+
screenshot: xbrowser.screenshot,
|
|
32
|
+
license: xbrowser.license || packageJson.license
|
|
33
|
+
};
|
|
34
|
+
return metadata;
|
|
35
|
+
}
|
|
36
|
+
static isXBrowserPlugin(packageJson) {
|
|
37
|
+
if (packageJson.xbrowser) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
const keywords = packageJson.keywords;
|
|
41
|
+
if (!keywords) return false;
|
|
42
|
+
return this.XBROWSER_KEYWORDS.some((kw) => keywords.includes(kw));
|
|
43
|
+
}
|
|
44
|
+
static fromNPMResult(result) {
|
|
45
|
+
const author = typeof result.author === "string" ? result.author : result.author?.name || "Unknown";
|
|
46
|
+
return {
|
|
47
|
+
id: result.name,
|
|
48
|
+
name: result.name.replace(/^xbrowser-plugin-/, "").replace(/^@[^/]+\//, ""),
|
|
49
|
+
description: result.description || "",
|
|
50
|
+
version: result.version,
|
|
51
|
+
author,
|
|
52
|
+
homepage: result.homepage || result.links?.homepage,
|
|
53
|
+
tags: result.keywords,
|
|
54
|
+
license: ""
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
static extractAuthor(author) {
|
|
58
|
+
if (typeof author === "string") return author;
|
|
59
|
+
if (typeof author === "object" && author !== null) {
|
|
60
|
+
const authorObj = author;
|
|
61
|
+
return authorObj.name || "Unknown";
|
|
62
|
+
}
|
|
63
|
+
return "Unknown";
|
|
64
|
+
}
|
|
65
|
+
static validateMetadata(metadata) {
|
|
66
|
+
const errors = [];
|
|
67
|
+
if (!metadata.id) errors.push("id is required");
|
|
68
|
+
if (!metadata.name) errors.push("name is required");
|
|
69
|
+
if (!metadata.description) errors.push("description is required");
|
|
70
|
+
if (!metadata.version) errors.push("version is required");
|
|
71
|
+
return errors;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
PluginMetadataParser
|
|
77
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/commands/convert.ts
|
|
2
|
+
function escapeString(str) {
|
|
3
|
+
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
4
|
+
}
|
|
5
|
+
function generateJSScript(recording) {
|
|
6
|
+
const events = aggregateEvents(recording.events || []);
|
|
7
|
+
let script = `#!/usr/bin/env node
|
|
8
|
+
// Auto-generated replay script from xbrowser
|
|
9
|
+
// Start URL: ${recording.startUrl}
|
|
10
|
+
// Events: ${events.length}
|
|
11
|
+
|
|
12
|
+
import { chromium } from 'playwright';
|
|
13
|
+
|
|
14
|
+
const START_URL = '${escapeString(recording.startUrl)}';
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const browser = await chromium.launch({ headless: true });
|
|
18
|
+
const context = await browser.newContext();
|
|
19
|
+
const page = await context.newPage();
|
|
20
|
+
|
|
21
|
+
console.log('Navigating to', START_URL);
|
|
22
|
+
await page.goto(START_URL, { waitUntil: 'domcontentloaded' });
|
|
23
|
+
await page.waitForTimeout(1000);
|
|
24
|
+
`;
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
script += generateJSEvent(event);
|
|
27
|
+
}
|
|
28
|
+
script += `
|
|
29
|
+
console.log('Replay completed!');
|
|
30
|
+
await browser.close();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main().catch((e) => { console.error(e); process.exit(1); });
|
|
34
|
+
`;
|
|
35
|
+
return script;
|
|
36
|
+
}
|
|
37
|
+
function generateJSEvent(event) {
|
|
38
|
+
switch (event.type) {
|
|
39
|
+
case "click":
|
|
40
|
+
return `
|
|
41
|
+
// Click: ${event.selector}
|
|
42
|
+
await page.click('${escapeString(event.selector || "body")}');
|
|
43
|
+
await page.waitForTimeout(100);
|
|
44
|
+
`;
|
|
45
|
+
case "type":
|
|
46
|
+
case "input":
|
|
47
|
+
return `
|
|
48
|
+
// Input: ${event.selector}
|
|
49
|
+
await page.fill('${escapeString(event.selector || "input")}', '${escapeString(event.data?.value || "")}');
|
|
50
|
+
await page.waitForTimeout(100);
|
|
51
|
+
`;
|
|
52
|
+
case "keydown":
|
|
53
|
+
case "keypress":
|
|
54
|
+
if (event.data?.key === "Enter") {
|
|
55
|
+
return `
|
|
56
|
+
// Press Enter
|
|
57
|
+
await page.keyboard.press('Enter');
|
|
58
|
+
await page.waitForTimeout(100);
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
case "scroll":
|
|
63
|
+
return `
|
|
64
|
+
// Scroll
|
|
65
|
+
await page.evaluate(() => window.scrollTo(${event.data?.x || 0}, ${event.data?.y || 0}));
|
|
66
|
+
await page.waitForTimeout(50);
|
|
67
|
+
`;
|
|
68
|
+
case "navigate":
|
|
69
|
+
case "page_load":
|
|
70
|
+
return "";
|
|
71
|
+
default:
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function generatePythonScript(recording) {
|
|
76
|
+
const events = recording.events || [];
|
|
77
|
+
let script = `#!/usr/bin/env python3
|
|
78
|
+
# Auto-generated replay script from xbrowser
|
|
79
|
+
# Start URL: ${recording.startUrl}
|
|
80
|
+
|
|
81
|
+
import asyncio
|
|
82
|
+
from playwright.async_api import async_playwright
|
|
83
|
+
|
|
84
|
+
START_URL = "${recording.startUrl}"
|
|
85
|
+
|
|
86
|
+
async def main():
|
|
87
|
+
async with async_playwright() as p:
|
|
88
|
+
browser = await p.chromium.launch(headless=True)
|
|
89
|
+
context = await browser.new_context()
|
|
90
|
+
page = await context.new_page()
|
|
91
|
+
|
|
92
|
+
print(f"Navigating to {START_URL}")
|
|
93
|
+
await page.goto(START_URL)
|
|
94
|
+
await asyncio.sleep(1)
|
|
95
|
+
`;
|
|
96
|
+
for (const event of events) {
|
|
97
|
+
script += generatePythonEvent(event);
|
|
98
|
+
}
|
|
99
|
+
script += `
|
|
100
|
+
print("Replay completed!")
|
|
101
|
+
await browser.close()
|
|
102
|
+
|
|
103
|
+
asyncio.run(main())
|
|
104
|
+
`;
|
|
105
|
+
return script;
|
|
106
|
+
}
|
|
107
|
+
function generatePythonEvent(event) {
|
|
108
|
+
switch (event.type) {
|
|
109
|
+
case "click":
|
|
110
|
+
return ` # Click: ${event.selector}
|
|
111
|
+
await page.click('${escapeString(event.selector || "body")}')
|
|
112
|
+
await asyncio.sleep(0.1)
|
|
113
|
+
`;
|
|
114
|
+
case "type":
|
|
115
|
+
case "input":
|
|
116
|
+
return ` # Input: ${event.selector}
|
|
117
|
+
await page.fill('${escapeString(event.selector || "input")}', '${escapeString(event.data?.value || "")}')
|
|
118
|
+
await asyncio.sleep(0.1)
|
|
119
|
+
`;
|
|
120
|
+
case "keydown":
|
|
121
|
+
case "keypress":
|
|
122
|
+
if (event.data?.key === "Enter") {
|
|
123
|
+
return ` # Press Enter
|
|
124
|
+
await page.keyboard.press('Enter')
|
|
125
|
+
await asyncio.sleep(0.1)
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
return "";
|
|
129
|
+
default:
|
|
130
|
+
return "";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function generateBashScript(recording) {
|
|
134
|
+
const events = recording.events || [];
|
|
135
|
+
let script = `#!/bin/bash
|
|
136
|
+
# Auto-generated replay script from xbrowser
|
|
137
|
+
# Start URL: ${recording.startUrl}
|
|
138
|
+
|
|
139
|
+
CDP_URL="\${CDP_URL:-http://localhost:9222}"
|
|
140
|
+
|
|
141
|
+
echo "Navigating to ${recording.startUrl}..."
|
|
142
|
+
curl -s "$CDP_URL/json/new?${encodeURIComponent(recording.startUrl)}" > /dev/null
|
|
143
|
+
sleep 2
|
|
144
|
+
`;
|
|
145
|
+
for (const event of events) {
|
|
146
|
+
script += generateBashEvent(event);
|
|
147
|
+
}
|
|
148
|
+
script += `
|
|
149
|
+
echo "Replay completed!"
|
|
150
|
+
`;
|
|
151
|
+
return script;
|
|
152
|
+
}
|
|
153
|
+
function generateBashEvent(event) {
|
|
154
|
+
switch (event.type) {
|
|
155
|
+
case "click":
|
|
156
|
+
return `# Click: ${event.selector}
|
|
157
|
+
curl -s "$CDP_URL/json/execute" -d '{
|
|
158
|
+
"method": "Runtime.evaluate",
|
|
159
|
+
"params": { "expression": "document.querySelector('${escapeString(event.selector || "body")}').click()" }
|
|
160
|
+
}' > /dev/null
|
|
161
|
+
sleep 0.1
|
|
162
|
+
`;
|
|
163
|
+
case "type":
|
|
164
|
+
case "input":
|
|
165
|
+
return `# Input: ${event.selector}
|
|
166
|
+
curl -s "$CDP_URL/json/execute" -d '{
|
|
167
|
+
"method": "Runtime.evaluate",
|
|
168
|
+
"params": { "expression": "document.querySelector('${escapeString(event.selector || "input")}').value = '${escapeString(event.data?.value || "")}'" }
|
|
169
|
+
}' > /dev/null
|
|
170
|
+
sleep 0.1
|
|
171
|
+
`;
|
|
172
|
+
default:
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function aggregateEvents(events) {
|
|
177
|
+
const aggregated = [];
|
|
178
|
+
let lastInput = null;
|
|
179
|
+
for (const event of events) {
|
|
180
|
+
if (event.type === "input" || event.type === "type") {
|
|
181
|
+
lastInput = event;
|
|
182
|
+
} else if (event.type === "keydown" || event.type === "keypress") {
|
|
183
|
+
if (lastInput) {
|
|
184
|
+
aggregated.push(lastInput);
|
|
185
|
+
lastInput = null;
|
|
186
|
+
}
|
|
187
|
+
aggregated.push(event);
|
|
188
|
+
} else {
|
|
189
|
+
if (lastInput) {
|
|
190
|
+
aggregated.push(lastInput);
|
|
191
|
+
lastInput = null;
|
|
192
|
+
}
|
|
193
|
+
aggregated.push(event);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (lastInput) {
|
|
197
|
+
aggregated.push(lastInput);
|
|
198
|
+
}
|
|
199
|
+
return aggregated;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
generateJSScript,
|
|
204
|
+
generatePythonScript,
|
|
205
|
+
generateBashScript
|
|
206
|
+
};
|