@xbrowser/cli 0.14.0 → 0.14.1
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-VUJDJCIN.js → chunk-ITKPSIP7.js} +93 -4
- package/dist/cli.js +512 -305
- package/dist/daemon-main.js +172 -80
- package/dist/{human-interaction-QPHNDD76.js → human-interaction-W753RVJB.js} +1 -2
- package/dist/index.d.ts +7 -68
- package/dist/index.js +407 -271
- package/dist/screenshot-CWAWMXVA.js +28 -0
- package/dist/screenshot-MB6R7RSS.js +26 -0
- package/package.json +1 -1
- package/dist/admin-6UTU2RZ2.js +0 -281
- package/dist/admin-MDGF4CET.js +0 -285
- package/dist/admin-RPJJ5CAF.js +0 -282
- package/dist/chunk-43VX3TYN.js +0 -83
- package/dist/chunk-DESA2KMG.js +0 -77
- package/dist/chunk-FF5WHQHN.js +0 -135
- package/dist/chunk-HINTG75P.js +0 -77
- package/dist/chunk-KTSQU4QT.js +0 -29
- package/dist/chunk-M7CMBPCA.js +0 -100
- package/dist/chunk-NFGO7J2I.js +0 -29
- package/dist/chunk-OLB6UJ25.js +0 -438
- package/dist/chunk-VEKPHQBR.js +0 -47
- package/dist/chunk-YEN2ODUI.js +0 -14
- package/dist/marketplace-FCVN5OTZ.js +0 -706
- package/dist/marketplace-FPT5YLKB.js +0 -351
- package/dist/marketplace-W545W4FR.js +0 -706
|
@@ -1,706 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
PluginMetadataParser
|
|
3
|
-
} from "./chunk-DESA2KMG.js";
|
|
4
|
-
import {
|
|
5
|
-
NPM_REGISTRY_URL,
|
|
6
|
-
ensureProxyFetch,
|
|
7
|
-
getRegistryUrl,
|
|
8
|
-
loadAuth,
|
|
9
|
-
saveAuth
|
|
10
|
-
} from "./chunk-43VX3TYN.js";
|
|
11
|
-
import {
|
|
12
|
-
readJsonFile
|
|
13
|
-
} from "./chunk-YEN2ODUI.js";
|
|
14
|
-
|
|
15
|
-
// src/plugin/builtins/marketplace.ts
|
|
16
|
-
import { z } from "zod";
|
|
17
|
-
import { createInterface } from "readline";
|
|
18
|
-
|
|
19
|
-
// src/plugin/publisher.ts
|
|
20
|
-
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
21
|
-
import { resolve, relative, basename, posix } from "path";
|
|
22
|
-
import { createHash } from "crypto";
|
|
23
|
-
import { gzipSync } from "zlib";
|
|
24
|
-
var IGNORE_PATTERNS = [
|
|
25
|
-
"node_modules",
|
|
26
|
-
".git",
|
|
27
|
-
".DS_Store",
|
|
28
|
-
"dist",
|
|
29
|
-
".env",
|
|
30
|
-
".env.local",
|
|
31
|
-
"*.log"
|
|
32
|
-
];
|
|
33
|
-
function shouldIgnore(name) {
|
|
34
|
-
return IGNORE_PATTERNS.some((pattern) => {
|
|
35
|
-
if (pattern.startsWith("*")) return name.endsWith(pattern.slice(1));
|
|
36
|
-
return name === pattern;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function collectFiles(dir, base = dir) {
|
|
40
|
-
const files = [];
|
|
41
|
-
if (!existsSync(dir)) return files;
|
|
42
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
if (shouldIgnore(entry.name)) continue;
|
|
45
|
-
const fullPath = resolve(dir, entry.name);
|
|
46
|
-
if (entry.isDirectory()) {
|
|
47
|
-
files.push(...collectFiles(fullPath, base));
|
|
48
|
-
} else if (entry.isFile()) {
|
|
49
|
-
const relPath = relative(base, fullPath).split("/").join(posix.sep);
|
|
50
|
-
files.push({ path: relPath, content: readFileSync(fullPath) });
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return files;
|
|
54
|
-
}
|
|
55
|
-
function extractCommandDocsFromCode(code) {
|
|
56
|
-
const docs = [];
|
|
57
|
-
const commandStartRegex = /\b\w+\s*\.\s*command\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*\{/g;
|
|
58
|
-
let startMatch;
|
|
59
|
-
while ((startMatch = commandStartRegex.exec(code)) !== null) {
|
|
60
|
-
const cmdName = startMatch[1];
|
|
61
|
-
const startPos = startMatch.index + startMatch[0].length;
|
|
62
|
-
const header = extractCommandHeader(code, startPos);
|
|
63
|
-
const descMatch = header.match(/description\s*:\s*['"`]([\s\S]*?)['"`]/);
|
|
64
|
-
const description = descMatch ? descMatch[1].trim() : "";
|
|
65
|
-
const parameters = extractParameters(header);
|
|
66
|
-
const examples = extractExamples(header);
|
|
67
|
-
docs.push({
|
|
68
|
-
name: cmdName,
|
|
69
|
-
description,
|
|
70
|
-
parameters,
|
|
71
|
-
examples: examples.length > 0 ? examples : void 0
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
return docs;
|
|
75
|
-
}
|
|
76
|
-
function extractCommandHeader(code, startPos) {
|
|
77
|
-
let depth = 1;
|
|
78
|
-
let i = startPos;
|
|
79
|
-
while (i < code.length && depth > 0) {
|
|
80
|
-
const ch = code[i];
|
|
81
|
-
if (ch === "{") depth++;
|
|
82
|
-
else if (ch === "}") {
|
|
83
|
-
depth--;
|
|
84
|
-
if (depth === 0) break;
|
|
85
|
-
}
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
const fullBlock = code.slice(startPos, i);
|
|
89
|
-
const handlerIdx = fullBlock.search(/\bhandler\s*:/);
|
|
90
|
-
if (handlerIdx !== -1) {
|
|
91
|
-
return fullBlock.slice(0, handlerIdx);
|
|
92
|
-
}
|
|
93
|
-
return fullBlock;
|
|
94
|
-
}
|
|
95
|
-
function extractParameters(block) {
|
|
96
|
-
const params = [];
|
|
97
|
-
const zodObjectRegex = /z\.object\s*\(\s*\{([\s\S]*?)\}\s*\)/;
|
|
98
|
-
const objMatch = block.match(zodObjectRegex);
|
|
99
|
-
if (!objMatch) return params;
|
|
100
|
-
const objBody = objMatch[1];
|
|
101
|
-
const normalized = objBody.replace(/\n\s*/g, " ").replace(/\s+/g, " ").replace(/z\s*\.\s*/g, "z.");
|
|
102
|
-
const fieldRegex = /(\w+)\s*:\s*z\.(\w+)([^(]*)/g;
|
|
103
|
-
let fieldMatch;
|
|
104
|
-
while ((fieldMatch = fieldRegex.exec(normalized)) !== null) {
|
|
105
|
-
const pName = fieldMatch[1];
|
|
106
|
-
const zType = fieldMatch[2];
|
|
107
|
-
const restStart = fieldMatch.index + fieldMatch[0].length;
|
|
108
|
-
let rest = "";
|
|
109
|
-
let depth = 0;
|
|
110
|
-
let j = restStart;
|
|
111
|
-
while (j < normalized.length) {
|
|
112
|
-
if (normalized[j] === "(") depth++;
|
|
113
|
-
else if (normalized[j] === ")") {
|
|
114
|
-
if (depth === 0) break;
|
|
115
|
-
depth--;
|
|
116
|
-
} else if (depth === 0 && (normalized[j] === "," || normalized[j] === "}")) {
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
rest += normalized[j];
|
|
120
|
-
j++;
|
|
121
|
-
}
|
|
122
|
-
const isOptional = rest.includes(".optional()");
|
|
123
|
-
const defaultMatch = rest.match(/\.default\s*\(\s*([^)]+)\s*\)/);
|
|
124
|
-
let defaultValue = void 0;
|
|
125
|
-
if (defaultMatch) {
|
|
126
|
-
const raw = defaultMatch[1].trim();
|
|
127
|
-
if (raw === "true") defaultValue = true;
|
|
128
|
-
else if (raw === "false") defaultValue = false;
|
|
129
|
-
else if (raw.startsWith("'") || raw.startsWith('"')) defaultValue = raw.slice(1, -1);
|
|
130
|
-
else if (!isNaN(Number(raw))) defaultValue = Number(raw);
|
|
131
|
-
else defaultValue = raw;
|
|
132
|
-
}
|
|
133
|
-
const descMatch = rest.match(/\.describe\s*\(\s*['"`]([\s\S]*?)['"`]\s*\)/);
|
|
134
|
-
const pDesc = descMatch ? descMatch[1].trim() : "";
|
|
135
|
-
let pType = zType;
|
|
136
|
-
const enumMatch = rest.match(/\.enum\s*\(\s*\[([^\]]+)\]\s*\)/);
|
|
137
|
-
if (enumMatch) {
|
|
138
|
-
pType = `enum: ${enumMatch[1].trim()}`;
|
|
139
|
-
}
|
|
140
|
-
params.push({
|
|
141
|
-
name: pName,
|
|
142
|
-
type: pType,
|
|
143
|
-
description: pDesc,
|
|
144
|
-
required: !isOptional,
|
|
145
|
-
...defaultValue !== void 0 ? { default: defaultValue } : {}
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
return params;
|
|
149
|
-
}
|
|
150
|
-
function extractExamples(block) {
|
|
151
|
-
const examples = [];
|
|
152
|
-
const examplesRegex = /examples\s*:\s*\[([\s\S]*?)\]\s*,?\s*(?:result|handler|\}\s*\)|$)/;
|
|
153
|
-
const exMatch = block.match(examplesRegex);
|
|
154
|
-
if (!exMatch) {
|
|
155
|
-
const fallbackRegex = /examples\s*:\s*\[([\s\S]*?)\]\s*[,}\n]/;
|
|
156
|
-
const fbMatch = block.match(fallbackRegex);
|
|
157
|
-
if (!fbMatch) return examples;
|
|
158
|
-
const exBody = fbMatch[1];
|
|
159
|
-
const objRegex2 = /\{\s*cmd\s*:\s*'([^']+)'\s*,\s*description\s*:\s*'([^']+)'\s*\}/g;
|
|
160
|
-
let objMatch2;
|
|
161
|
-
while ((objMatch2 = objRegex2.exec(exBody)) !== null) {
|
|
162
|
-
examples.push({ cmd: objMatch2[1], description: objMatch2[2].trim() });
|
|
163
|
-
}
|
|
164
|
-
if (examples.length === 0) {
|
|
165
|
-
const objRegex22 = /\{\s*cmd\s*:\s*"([^"]+)"\s*,\s*description\s*:\s*"([^"]+)"\s*\}/g;
|
|
166
|
-
let objMatch22;
|
|
167
|
-
while ((objMatch22 = objRegex22.exec(exBody)) !== null) {
|
|
168
|
-
examples.push({ cmd: objMatch22[1], description: objMatch22[2].trim() });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return examples;
|
|
172
|
-
}
|
|
173
|
-
const exBody2 = exMatch[1];
|
|
174
|
-
const objRegex = /\{\s*cmd\s*:\s*'([^']+)'\s*,\s*description\s*:\s*'([^']+)'\s*\}/g;
|
|
175
|
-
let objMatch;
|
|
176
|
-
while ((objMatch = objRegex.exec(exBody2)) !== null) {
|
|
177
|
-
examples.push({ cmd: objMatch[1], description: objMatch[2].trim() });
|
|
178
|
-
}
|
|
179
|
-
if (examples.length === 0) {
|
|
180
|
-
const objRegex2 = /\{\s*cmd\s*:\s*"([^"]+)"\s*,\s*description\s*:\s*"([^"]+)"\s*\}/g;
|
|
181
|
-
let objMatch2;
|
|
182
|
-
while ((objMatch2 = objRegex2.exec(exBody2)) !== null) {
|
|
183
|
-
examples.push({ cmd: objMatch2[1], description: objMatch2[2].trim() });
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return examples;
|
|
187
|
-
}
|
|
188
|
-
function readReadme(pluginDir) {
|
|
189
|
-
const candidates = ["README.md", "readme.md", "Readme.md"];
|
|
190
|
-
for (const name of candidates) {
|
|
191
|
-
const path = resolve(pluginDir, name);
|
|
192
|
-
if (existsSync(path)) {
|
|
193
|
-
return readFileSync(path, "utf-8");
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
198
|
-
var SITE_TAG_MAP = {
|
|
199
|
-
"baidu.com": ["baidu", "search-engine"],
|
|
200
|
-
"google.com": ["google", "search-engine"],
|
|
201
|
-
"bing.com": ["bing", "search-engine"],
|
|
202
|
-
"douyin.com": ["douyin", "social-media", "video"],
|
|
203
|
-
"tiktok.com": ["tiktok", "social-media", "video"],
|
|
204
|
-
"instagram.com": ["instagram", "social-media", "photo"],
|
|
205
|
-
"twitter.com": ["twitter", "social-media"],
|
|
206
|
-
"x.com": ["twitter", "social-media"],
|
|
207
|
-
"facebook.com": ["facebook", "social-media"],
|
|
208
|
-
"weibo.com": ["weibo", "social-media"],
|
|
209
|
-
"zhihu.com": ["zhihu", "q&a", "knowledge"],
|
|
210
|
-
"xiaohongshu.com": ["xiaohongshu", "social-media", "lifestyle"],
|
|
211
|
-
"csdn.net": ["csdn", "developer", "knowledge"],
|
|
212
|
-
"juejin.cn": ["juejin", "developer"],
|
|
213
|
-
"github.com": ["github", "developer", "code"],
|
|
214
|
-
"reddit.com": ["reddit", "social-media", "forum"],
|
|
215
|
-
"medium.com": ["medium", "blog", "knowledge"],
|
|
216
|
-
"taobao.com": ["taobao", "e-commerce"],
|
|
217
|
-
"jd.com": ["jd", "e-commerce"],
|
|
218
|
-
"pinterest.com": ["pinterest", "social-media", "photo"],
|
|
219
|
-
"youtube.com": ["youtube", "video", "social-media"],
|
|
220
|
-
"bilibili.com": ["bilibili", "video", "social-media"]
|
|
221
|
-
};
|
|
222
|
-
function extractTagsFromCode(code, existingTags) {
|
|
223
|
-
const tags = new Set(existingTags);
|
|
224
|
-
const urlMatch = code.match(/url\s*:\s*['"`](https?:\/\/[^'"`]+)['"`]/);
|
|
225
|
-
if (urlMatch) {
|
|
226
|
-
try {
|
|
227
|
-
const hostname = new URL(urlMatch[1]).hostname.replace(/^www\./, "");
|
|
228
|
-
const mapped = SITE_TAG_MAP[hostname];
|
|
229
|
-
if (mapped) {
|
|
230
|
-
for (const tag of mapped) tags.add(tag);
|
|
231
|
-
} else {
|
|
232
|
-
const domain = hostname.split(".")[0];
|
|
233
|
-
tags.add(domain);
|
|
234
|
-
}
|
|
235
|
-
} catch {
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
const nameMatch = code.match(/name\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
239
|
-
if (nameMatch && !tags.has(nameMatch[1])) {
|
|
240
|
-
tags.add(nameMatch[1]);
|
|
241
|
-
}
|
|
242
|
-
return Array.from(tags);
|
|
243
|
-
}
|
|
244
|
-
function extractSitesFromCode(code, existingSites) {
|
|
245
|
-
const sites = new Set(existingSites);
|
|
246
|
-
const urlMatch = code.match(/url\s*:\s*['"`](https?:\/\/[^'"`]+)['"`]/);
|
|
247
|
-
if (urlMatch) {
|
|
248
|
-
try {
|
|
249
|
-
const hostname = new URL(urlMatch[1]).hostname.replace(/^www\./, "");
|
|
250
|
-
sites.add(hostname);
|
|
251
|
-
} catch {
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return Array.from(sites);
|
|
255
|
-
}
|
|
256
|
-
function slugify(name) {
|
|
257
|
-
return name.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
258
|
-
}
|
|
259
|
-
async function validateNpmPackageExists(packageName, version) {
|
|
260
|
-
await ensureProxyFetch();
|
|
261
|
-
const encodedName = encodeURIComponent(packageName);
|
|
262
|
-
const res = await fetch(`${NPM_REGISTRY_URL}/${encodedName}/${version}`);
|
|
263
|
-
if (!res.ok) {
|
|
264
|
-
throw new Error(
|
|
265
|
-
`Package ${packageName}@${version} not found on npm. Publish to npm first, or use --storage r2 to upload directly.`
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
async function createTarball(pluginDir, options) {
|
|
270
|
-
const indexPath = resolve(pluginDir, "index.ts");
|
|
271
|
-
const pkgPath = resolve(pluginDir, "package.json");
|
|
272
|
-
if (!existsSync(indexPath)) {
|
|
273
|
-
throw new Error("No index.ts found. A plugin must have an index.ts entry file.");
|
|
274
|
-
}
|
|
275
|
-
let packageJson = {};
|
|
276
|
-
if (existsSync(pkgPath)) {
|
|
277
|
-
packageJson = readJsonFile(pkgPath, {});
|
|
278
|
-
}
|
|
279
|
-
const xbrowserMeta = packageJson.xbrowser || {};
|
|
280
|
-
const name = xbrowserMeta.name || packageJson.name || basename(pluginDir);
|
|
281
|
-
const version = xbrowserMeta.version || packageJson.version || "1.0.0";
|
|
282
|
-
const description = xbrowserMeta.description || packageJson.description || "";
|
|
283
|
-
const author = PluginMetadataParser.extractAuthor(packageJson.author);
|
|
284
|
-
const slug = slugify(xbrowserMeta.slug || name);
|
|
285
|
-
if (!description) {
|
|
286
|
-
throw new Error('Plugin must have a description. Add "description" to package.json or xbrowser metadata.');
|
|
287
|
-
}
|
|
288
|
-
const indexCode = readFileSync(indexPath, "utf-8");
|
|
289
|
-
const commandsDocs = extractCommandDocsFromCode(indexCode);
|
|
290
|
-
const detectedCommands = commandsDocs.map((d) => d.name);
|
|
291
|
-
const commands = xbrowserMeta.commands || detectedCommands.length > 0 ? xbrowserMeta.commands || detectedCommands : detectedCommands;
|
|
292
|
-
const readme = readReadme(pluginDir);
|
|
293
|
-
const tags = extractTagsFromCode(indexCode, xbrowserMeta.tags || []);
|
|
294
|
-
const sites = extractSitesFromCode(indexCode, xbrowserMeta.sites || []);
|
|
295
|
-
const storage = options.storage || "r2";
|
|
296
|
-
if (storage === "npm") {
|
|
297
|
-
const packageName = packageJson.name || name;
|
|
298
|
-
await validateNpmPackageExists(packageName, version);
|
|
299
|
-
}
|
|
300
|
-
const formData = new FormData();
|
|
301
|
-
const metadata = {
|
|
302
|
-
name,
|
|
303
|
-
slug,
|
|
304
|
-
version,
|
|
305
|
-
description,
|
|
306
|
-
author,
|
|
307
|
-
commands,
|
|
308
|
-
commandsDocs,
|
|
309
|
-
readme,
|
|
310
|
-
tags,
|
|
311
|
-
sites,
|
|
312
|
-
license: xbrowserMeta.license || packageJson.license || "MIT",
|
|
313
|
-
homepageUrl: xbrowserMeta.homepage || packageJson.homepage || null,
|
|
314
|
-
repositoryUrl: xbrowserMeta.repository || packageJson.repository?.url || null,
|
|
315
|
-
npmPackage: packageJson.name || null,
|
|
316
|
-
storageType: storage
|
|
317
|
-
};
|
|
318
|
-
const metadataBlob = new Blob([JSON.stringify(metadata)], { type: "application/json" });
|
|
319
|
-
formData.append("metadata", metadataBlob, "metadata.json");
|
|
320
|
-
let totalSize = 0;
|
|
321
|
-
let checksum = "";
|
|
322
|
-
if (storage === "r2") {
|
|
323
|
-
const files = collectFiles(pluginDir);
|
|
324
|
-
const manifest = files.map((f) => ({
|
|
325
|
-
path: f.path,
|
|
326
|
-
content: f.content.toString("base64")
|
|
327
|
-
}));
|
|
328
|
-
const manifestJson = JSON.stringify(manifest);
|
|
329
|
-
const gzipped = gzipSync(Buffer.from(manifestJson));
|
|
330
|
-
totalSize = gzipped.length;
|
|
331
|
-
const hash = createHash("sha256");
|
|
332
|
-
hash.update(gzipped);
|
|
333
|
-
checksum = `sha256-${hash.digest("hex").slice(0, 16)}`;
|
|
334
|
-
const blob = new Blob([new Uint8Array(gzipped)]);
|
|
335
|
-
formData.append("files", blob, `${slug}-${version}.tar.gz`);
|
|
336
|
-
formData.append("checksum", checksum);
|
|
337
|
-
return {
|
|
338
|
-
name,
|
|
339
|
-
version,
|
|
340
|
-
slug,
|
|
341
|
-
description,
|
|
342
|
-
author,
|
|
343
|
-
commands,
|
|
344
|
-
commandsDocs,
|
|
345
|
-
readme,
|
|
346
|
-
tags,
|
|
347
|
-
sites,
|
|
348
|
-
fileCount: files.length,
|
|
349
|
-
size: totalSize,
|
|
350
|
-
checksum,
|
|
351
|
-
formData
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
formData.append("checksum", "npm-managed");
|
|
355
|
-
return {
|
|
356
|
-
name,
|
|
357
|
-
version,
|
|
358
|
-
slug,
|
|
359
|
-
description,
|
|
360
|
-
author,
|
|
361
|
-
commands,
|
|
362
|
-
commandsDocs,
|
|
363
|
-
readme,
|
|
364
|
-
tags,
|
|
365
|
-
sites,
|
|
366
|
-
fileCount: 0,
|
|
367
|
-
size: 0,
|
|
368
|
-
checksum: "npm-managed",
|
|
369
|
-
formData
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// src/plugin/builtins/marketplace.ts
|
|
374
|
-
var marketResult = z.object({
|
|
375
|
-
success: z.boolean(),
|
|
376
|
-
data: z.record(z.unknown()).optional(),
|
|
377
|
-
message: z.string().optional()
|
|
378
|
-
});
|
|
379
|
-
function prompt(rl, question, hidden = false) {
|
|
380
|
-
return new Promise((resolve2) => {
|
|
381
|
-
if (hidden) {
|
|
382
|
-
const stdin = process.stdin;
|
|
383
|
-
const wasRaw = stdin.isRaw;
|
|
384
|
-
if (stdin.isTTY) stdin.setRawMode(true);
|
|
385
|
-
let input = "";
|
|
386
|
-
process.stdout.write(question);
|
|
387
|
-
const onData = (char) => {
|
|
388
|
-
const c = char.toString();
|
|
389
|
-
switch (c) {
|
|
390
|
-
case "\n":
|
|
391
|
-
case "\r":
|
|
392
|
-
if (stdin.isTTY) stdin.setRawMode(wasRaw ?? false);
|
|
393
|
-
stdin.removeListener("data", onData);
|
|
394
|
-
console.log();
|
|
395
|
-
resolve2(input);
|
|
396
|
-
break;
|
|
397
|
-
case "":
|
|
398
|
-
process.exit();
|
|
399
|
-
break;
|
|
400
|
-
case "\x7F":
|
|
401
|
-
input = input.slice(0, -1);
|
|
402
|
-
break;
|
|
403
|
-
default:
|
|
404
|
-
input += c;
|
|
405
|
-
process.stdout.write("*");
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
stdin.on("data", onData);
|
|
409
|
-
} else {
|
|
410
|
-
rl.question(question, resolve2);
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
function setupMarketplacePlugin(xcli) {
|
|
415
|
-
const site = xcli.createSite({
|
|
416
|
-
name: "marketplace",
|
|
417
|
-
url: "https://xbrowser.dev",
|
|
418
|
-
description: "xbrowser marketplace \u2014 publish, login, register, whoami, logout"
|
|
419
|
-
});
|
|
420
|
-
const cmd = (name, config) => site.command(name, config);
|
|
421
|
-
cmd("publish", {
|
|
422
|
-
description: "Publish a plugin to the marketplace",
|
|
423
|
-
scope: "global",
|
|
424
|
-
parameters: z.object({
|
|
425
|
-
dir: z.string().optional().describe("Plugin directory (defaults to cwd)"),
|
|
426
|
-
registry: z.string().optional().describe("Custom registry URL"),
|
|
427
|
-
dryRun: z.boolean().optional().default(false).describe("Validate without publishing")
|
|
428
|
-
}),
|
|
429
|
-
result: marketResult,
|
|
430
|
-
handler: async (params) => {
|
|
431
|
-
const pluginDir = params.dir || process.cwd();
|
|
432
|
-
const auth = loadAuth();
|
|
433
|
-
if (!auth?.token) {
|
|
434
|
-
return { success: false, message: "Not logged in. Run: xbrowser marketplace login" };
|
|
435
|
-
}
|
|
436
|
-
const registryUrl = getRegistryUrl(params, auth.registry);
|
|
437
|
-
await ensureProxyFetch();
|
|
438
|
-
try {
|
|
439
|
-
const result = await createTarball(pluginDir, {
|
|
440
|
-
registry: registryUrl,
|
|
441
|
-
token: auth.token,
|
|
442
|
-
dryRun: !!params.dryRun
|
|
443
|
-
});
|
|
444
|
-
if (params.dryRun) {
|
|
445
|
-
const lines = [
|
|
446
|
-
"Dry run - validation passed:",
|
|
447
|
-
` Name: ${result.name}`,
|
|
448
|
-
` Version: ${result.version}`,
|
|
449
|
-
` Slug: ${result.slug}`,
|
|
450
|
-
` Description: ${result.description}`
|
|
451
|
-
];
|
|
452
|
-
if (result.commands?.length) lines.push(` Commands: ${result.commands.join(", ")}`);
|
|
453
|
-
if (result.tags?.length) lines.push(` Tags: ${result.tags.join(", ")}`);
|
|
454
|
-
if (result.sites?.length) lines.push(` Sites: ${result.sites.join(", ")}`);
|
|
455
|
-
if (result.commandsDocs?.length) {
|
|
456
|
-
lines.push(" Command Docs:");
|
|
457
|
-
for (const c of result.commandsDocs) {
|
|
458
|
-
lines.push(` ${c.name}: ${c.description}`);
|
|
459
|
-
for (const p of c.parameters) {
|
|
460
|
-
const req = p.required ? "required" : "optional";
|
|
461
|
-
const def = p.default !== void 0 ? `, default: ${JSON.stringify(p.default)}` : "";
|
|
462
|
-
lines.push(` - ${p.name} (${p.type}, ${req}${def}): ${p.description}`);
|
|
463
|
-
}
|
|
464
|
-
if (c.examples?.length) {
|
|
465
|
-
for (const ex of c.examples) lines.push(` example: ${ex.cmd} \u2014 ${ex.description}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
if (result.readme) {
|
|
470
|
-
const preview = result.readme.slice(0, 200).replace(/\n/g, "\\n");
|
|
471
|
-
lines.push(` README: ${preview.length < result.readme.length ? preview + "..." : preview}`);
|
|
472
|
-
}
|
|
473
|
-
lines.push(` Files: ${result.fileCount} files, ${(result.size / 1024).toFixed(1)}KB`);
|
|
474
|
-
return { success: true, data: { text: lines.join("\n"), name: result.name, version: result.version, slug: result.slug } };
|
|
475
|
-
}
|
|
476
|
-
const response = await fetch(`${registryUrl}/api/plugins/publish`, {
|
|
477
|
-
method: "POST",
|
|
478
|
-
headers: { Authorization: `Bearer ${auth.token}` },
|
|
479
|
-
body: result.formData
|
|
480
|
-
});
|
|
481
|
-
if (!response.ok) {
|
|
482
|
-
const errBody = await response.json().catch(() => ({}));
|
|
483
|
-
const errMsg = errBody.error || response.statusText;
|
|
484
|
-
if (errMsg.includes("R2 storage")) {
|
|
485
|
-
return { success: false, message: "R2 storage unavailable. Cannot publish plugin without source code.\n Please configure R2 bucket or use --storage local" };
|
|
486
|
-
}
|
|
487
|
-
return { success: false, message: `Publish failed (${response.status}): ${errMsg}` };
|
|
488
|
-
}
|
|
489
|
-
const body = await response.json();
|
|
490
|
-
const slug = body.data?.slug || result.slug;
|
|
491
|
-
const text = `
|
|
492
|
-
Published: ${result.name}@${result.version}
|
|
493
|
-
URL: ${registryUrl}/plugins/${slug}`;
|
|
494
|
-
return { success: true, data: { ok: true, name: result.name, version: result.version, slug, text } };
|
|
495
|
-
} catch (e) {
|
|
496
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
497
|
-
const cause = e instanceof Error && e.cause instanceof Error ? ` (${e.cause.message})` : "";
|
|
498
|
-
return { success: false, message: `Error: ${msg}${cause}` };
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
cmd("login", {
|
|
503
|
-
description: "Login to the marketplace",
|
|
504
|
-
scope: "global",
|
|
505
|
-
parameters: z.object({
|
|
506
|
-
email: z.string().optional().describe("Email (skip interactive prompt)"),
|
|
507
|
-
password: z.string().optional().describe("Password (skip interactive prompt)"),
|
|
508
|
-
token: z.string().optional().describe("Auth token (skip interactive login)"),
|
|
509
|
-
registry: z.string().optional().describe("Custom registry URL")
|
|
510
|
-
}),
|
|
511
|
-
result: marketResult,
|
|
512
|
-
handler: async (params) => {
|
|
513
|
-
const registryUrl = getRegistryUrl(params);
|
|
514
|
-
await ensureProxyFetch();
|
|
515
|
-
if (params.token) {
|
|
516
|
-
saveAuth({ token: params.token, registry: registryUrl });
|
|
517
|
-
const auth = loadAuth();
|
|
518
|
-
let username = "unknown";
|
|
519
|
-
if (auth?.token) {
|
|
520
|
-
try {
|
|
521
|
-
const resp = await fetch(`${registryUrl}/api/auth/verify`, {
|
|
522
|
-
headers: { Authorization: `Bearer ${auth.token}` }
|
|
523
|
-
});
|
|
524
|
-
if (resp.ok) {
|
|
525
|
-
const body = await resp.json();
|
|
526
|
-
username = body.data?.username || body.data?.email || "unknown";
|
|
527
|
-
}
|
|
528
|
-
} catch {
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
return { success: true, data: { ok: true, text: `Token saved.
|
|
532
|
-
Logged in as ${username}` } };
|
|
533
|
-
}
|
|
534
|
-
let email = params.email;
|
|
535
|
-
let password = params.password;
|
|
536
|
-
if (!email || !password) {
|
|
537
|
-
console.log(`
|
|
538
|
-
Login to ${registryUrl}
|
|
539
|
-
`);
|
|
540
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
541
|
-
if (!email) email = await prompt(rl, "Email: ");
|
|
542
|
-
if (!password) password = await prompt(rl, "Password: ", true);
|
|
543
|
-
rl.close();
|
|
544
|
-
console.log();
|
|
545
|
-
}
|
|
546
|
-
if (!email || !password) {
|
|
547
|
-
return { success: false, message: "Email and password are required" };
|
|
548
|
-
}
|
|
549
|
-
try {
|
|
550
|
-
const res = await fetch(`${registryUrl}/api/auth/login`, {
|
|
551
|
-
method: "POST",
|
|
552
|
-
headers: { "Content-Type": "application/json" },
|
|
553
|
-
body: JSON.stringify({ email, password })
|
|
554
|
-
});
|
|
555
|
-
if (!res.ok) {
|
|
556
|
-
const errBody = await res.json().catch(() => ({}));
|
|
557
|
-
return { success: false, message: `Login failed: ${errBody.error || errBody.message || res.statusText}` };
|
|
558
|
-
}
|
|
559
|
-
const body = await res.json();
|
|
560
|
-
const result = body.data;
|
|
561
|
-
if (!result?.token) {
|
|
562
|
-
return { success: false, message: "No token received from server" };
|
|
563
|
-
}
|
|
564
|
-
saveAuth({ token: result.token, registry: registryUrl });
|
|
565
|
-
const username = result.profile?.username || result.profile?.email || "unknown";
|
|
566
|
-
return { success: true, data: { ok: true, text: `Logged in as ${username}` } };
|
|
567
|
-
} catch (e) {
|
|
568
|
-
return { success: false, message: e instanceof Error ? e.message : String(e) };
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
cmd("register", {
|
|
573
|
-
description: "Register a new marketplace account",
|
|
574
|
-
scope: "global",
|
|
575
|
-
parameters: z.object({
|
|
576
|
-
username: z.string().optional().describe("Username (2-50 chars)"),
|
|
577
|
-
email: z.string().optional().describe("Email"),
|
|
578
|
-
password: z.string().optional().describe("Password (min 6 chars)"),
|
|
579
|
-
registry: z.string().optional().describe("Custom registry URL")
|
|
580
|
-
}),
|
|
581
|
-
result: marketResult,
|
|
582
|
-
handler: async (params) => {
|
|
583
|
-
const registryUrl = getRegistryUrl(params);
|
|
584
|
-
await ensureProxyFetch();
|
|
585
|
-
let username = params.username;
|
|
586
|
-
let email = params.email;
|
|
587
|
-
let password = params.password;
|
|
588
|
-
const needsInteractive = !username || !email || !password;
|
|
589
|
-
let confirmPassword = "";
|
|
590
|
-
if (needsInteractive) {
|
|
591
|
-
console.log("\nRegister for xbrowser developer account\n");
|
|
592
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
593
|
-
if (!username) username = await prompt(rl, "Username (2-50 chars): ");
|
|
594
|
-
if (!email) email = await prompt(rl, "Email: ");
|
|
595
|
-
if (!password) password = await prompt(rl, "Password (min 6 chars): ", true);
|
|
596
|
-
confirmPassword = await prompt(rl, "Confirm password: ", true);
|
|
597
|
-
rl.close();
|
|
598
|
-
}
|
|
599
|
-
if (!username || username.length < 2 || username.length > 50) {
|
|
600
|
-
return { success: false, message: "Username must be 2-50 characters" };
|
|
601
|
-
}
|
|
602
|
-
if (!email || !email.includes("@")) {
|
|
603
|
-
return { success: false, message: "Invalid email" };
|
|
604
|
-
}
|
|
605
|
-
if (!password || password.length < 6) {
|
|
606
|
-
return { success: false, message: "Password must be at least 6 characters" };
|
|
607
|
-
}
|
|
608
|
-
if (needsInteractive && password !== confirmPassword) {
|
|
609
|
-
return { success: false, message: "Passwords do not match" };
|
|
610
|
-
}
|
|
611
|
-
try {
|
|
612
|
-
const regRes = await fetch(`${registryUrl}/api/auth/register`, {
|
|
613
|
-
method: "POST",
|
|
614
|
-
headers: { "Content-Type": "application/json" },
|
|
615
|
-
body: JSON.stringify({ username, email, password })
|
|
616
|
-
});
|
|
617
|
-
if (!regRes.ok) {
|
|
618
|
-
const errBody = await regRes.json().catch(() => ({}));
|
|
619
|
-
return { success: false, message: `Registration failed: ${errBody.error || errBody.message || regRes.statusText}` };
|
|
620
|
-
}
|
|
621
|
-
const loginRes = await fetch(`${registryUrl}/api/auth/login`, {
|
|
622
|
-
method: "POST",
|
|
623
|
-
headers: { "Content-Type": "application/json" },
|
|
624
|
-
body: JSON.stringify({ email, password })
|
|
625
|
-
});
|
|
626
|
-
if (!loginRes.ok) {
|
|
627
|
-
return { success: false, message: "Registration succeeded but auto-login failed. Run: xbrowser marketplace login" };
|
|
628
|
-
}
|
|
629
|
-
const loginBody = await loginRes.json();
|
|
630
|
-
const token = loginBody.data?.token;
|
|
631
|
-
const profile = loginBody.data?.profile;
|
|
632
|
-
if (!token) {
|
|
633
|
-
return { success: false, message: "Registration succeeded but no token received. Run: xbrowser marketplace login" };
|
|
634
|
-
}
|
|
635
|
-
saveAuth({ token, registry: registryUrl });
|
|
636
|
-
const text = [
|
|
637
|
-
"\n Registered successfully!",
|
|
638
|
-
` Username: ${profile?.username || username}`,
|
|
639
|
-
` Email: ${profile?.email || email}`,
|
|
640
|
-
` Saved to ~/.xbrowser/auth.json`,
|
|
641
|
-
"\nYou can now publish plugins with: xbrowser marketplace publish"
|
|
642
|
-
].join("\n");
|
|
643
|
-
return { success: true, data: { ok: true, username: profile?.username, text } };
|
|
644
|
-
} catch (e) {
|
|
645
|
-
return { success: false, message: e instanceof Error ? e.message : String(e) };
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
cmd("whoami", {
|
|
650
|
-
description: "Show current logged-in user",
|
|
651
|
-
scope: "global",
|
|
652
|
-
parameters: z.object({
|
|
653
|
-
registry: z.string().optional().describe("Custom registry URL")
|
|
654
|
-
}),
|
|
655
|
-
result: marketResult,
|
|
656
|
-
handler: async (params) => {
|
|
657
|
-
const auth = loadAuth();
|
|
658
|
-
if (!auth?.token) {
|
|
659
|
-
return { success: false, message: "Not logged in. Run: xbrowser marketplace login" };
|
|
660
|
-
}
|
|
661
|
-
const registryUrl = getRegistryUrl(params, auth.registry);
|
|
662
|
-
await ensureProxyFetch();
|
|
663
|
-
try {
|
|
664
|
-
const resp = await fetch(`${registryUrl}/api/auth/verify`, {
|
|
665
|
-
headers: { Authorization: `Bearer ${auth.token}` }
|
|
666
|
-
});
|
|
667
|
-
if (!resp.ok) {
|
|
668
|
-
return { success: false, message: "Token invalid or expired. Run: xbrowser marketplace login" };
|
|
669
|
-
}
|
|
670
|
-
const body = await resp.json();
|
|
671
|
-
return {
|
|
672
|
-
success: true,
|
|
673
|
-
data: {
|
|
674
|
-
username: body.data?.username,
|
|
675
|
-
email: body.data?.email,
|
|
676
|
-
role: body.data?.role,
|
|
677
|
-
registry: registryUrl,
|
|
678
|
-
text: `Username: ${body.data?.username || "unknown"}
|
|
679
|
-
Email: ${body.data?.email || "unknown"}
|
|
680
|
-
Role: ${body.data?.role || "user"}
|
|
681
|
-
Registry: ${registryUrl}`
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
} catch (e) {
|
|
685
|
-
return { success: false, message: e instanceof Error ? e.message : String(e) };
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
cmd("logout", {
|
|
690
|
-
description: "Logout from the marketplace",
|
|
691
|
-
scope: "global",
|
|
692
|
-
parameters: z.object({}),
|
|
693
|
-
result: marketResult,
|
|
694
|
-
handler: async (_params) => {
|
|
695
|
-
const auth = loadAuth();
|
|
696
|
-
if (!auth?.token) {
|
|
697
|
-
return { success: false, message: "Not logged in" };
|
|
698
|
-
}
|
|
699
|
-
saveAuth({ token: "", registry: "" });
|
|
700
|
-
return { success: true, data: { ok: true, text: "Logged out" } };
|
|
701
|
-
}
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
export {
|
|
705
|
-
setupMarketplacePlugin as default
|
|
706
|
-
};
|