UNPKG

@koishijs/plugin-market

Version:

Manage your bots and plugins with console

570 lines (559 loc) 21.8 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __commonJS = (cb, mod) => function __require() { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; var __export = (target, all) => { for (var name2 in all) __defProp(target, name2, { get: all[name2], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/node/locales/schema.zh-CN.yml var require_schema_zh_CN = __commonJS({ "src/node/locales/schema.zh-CN.yml"(exports2, module2) { module2.exports = { registry: { $description: "插件源设置", endpoint: "插件的下载源。默认跟随当前项目的 npm config。", timeout: "获取插件数据的超时时间。" }, search: { $description: "搜索设置", endpoint: "用于搜索插件市场的网址。默认跟随插件源设置。", timeout: "搜索插件市场的超时时间。", proxyAgent: "用于搜索插件市场的代理。" } }; } }); // src/node/locales/message.zh-CN.yml var require_message_zh_CN = __commonJS({ "src/node/locales/message.zh-CN.yml"(exports2, module2) { module2.exports = { "commands.plugin": { description: "插件管理" }, "commands.plugin.install": { description: "安装插件", messages: { "expect-name": "请输入插件名。", "already-installed": "该插件已安装。", "not-found": "未找到该插件。", success: "安装成功!" } }, "commands.plugin.uninstall": { description: "卸载插件", messages: { "expect-name": "请输入插件名。", "not-installed": "该插件未安装。", success: "卸载成功!" } }, "commands.plugin.upgrade": { description: "升级插件", options: { self: "升级 Koishi 本体" }, messages: { "all-updated": "所有插件已是最新版本。", available: "有可用的依赖更新:", prompt: "输入「Y」升级全部依赖,输入「N」取消操作。", cancelled: "已取消操作。", success: "升级成功!" } } }; } }); // src/node/index.ts var node_exports = {}; __export(node_exports, { Config: () => Config, Installer: () => installer_default, apply: () => apply, inject: () => inject, name: () => name, usage: () => usage }); module.exports = __toCommonJS(node_exports); var import_koishi3 = require("koishi"); var import_semver2 = require("semver"); var import_path2 = require("path"); // src/node/deps.ts var import_console = require("@koishijs/console"); var DependencyProvider = class extends import_console.DataService { constructor(ctx) { super(ctx, "dependencies", { authority: 4 }); this.ctx = ctx; } static { __name(this, "DependencyProvider"); } async get() { return this.ctx.installer.getDeps(); } }; var RegistryProvider = class extends import_console.DataService { constructor(ctx) { super(ctx, "registry", { authority: 4 }); this.ctx = ctx; } static { __name(this, "RegistryProvider"); } async get() { return this.ctx.installer.fullCache; } }; // src/node/installer.ts var import_koishi = require("koishi"); var import_registry = __toESM(require("@koishijs/registry")); var import_path = require("path"); var import_fs = require("fs"); var import_semver = require("semver"); var import_get_registry = __toESM(require("get-registry")); var import_which_pm_runs = __toESM(require("which-pm-runs")); var import_execa = __toESM(require("execa")); var import_p_map = __toESM(require("p-map")); var logger = new import_koishi.Logger("market"); var levelMap = { "info": "info", "warning": "debug", "error": "warn" }; function loadManifest(name2) { const filename = require.resolve(name2 + "/package.json"); const meta = JSON.parse((0, import_fs.readFileSync)(filename, "utf8")); meta.dependencies ||= {}; (0, import_koishi.defineProperty)(meta, "$workspace", !filename.includes("node_modules")); return meta; } __name(loadManifest, "loadManifest"); function getVersions(versions) { return Object.fromEntries(versions.map((item) => [item.version, (0, import_koishi.pick)(item, ["peerDependencies", "peerDependenciesMeta", "deprecated"])]).sort(([a], [b]) => (0, import_semver.compare)(b, a))); } __name(getVersions, "getVersions"); var Installer = class extends import_koishi.Service { constructor(ctx, config) { super(ctx, "installer"); this.ctx = ctx; this.config = config; this.manifest = loadManifest(this.cwd); this.flushData = ctx.throttle(() => { ctx.get("console")?.broadcast("market/registry", this.tempCache); this.tempCache = {}; }, 500); } static { __name(this, "Installer"); } http; endpoint; fullCache = {}; tempCache = {}; pkgTasks = {}; agent = (0, import_which_pm_runs.default)(); manifest; depTask; flushData; get cwd() { return this.ctx.baseDir; } async start() { const { endpoint, timeout } = this.config; this.endpoint = endpoint || await (0, import_get_registry.default)(); this.http = this.ctx.http.extend({ endpoint: this.endpoint, timeout }); } resolveName(name2) { if (name2.startsWith("@koishijs/plugin-")) return [name2]; if (name2.match(/(^|\/)koishi-plugin-/)) return [name2]; if (name2[0] === "@") { const [left, right] = name2.split("/"); return [`${left}/koishi-plugin-${right}`]; } else { return [`@koishijs/plugin-${name2}`, `koishi-plugin-${name2}`]; } } async findVersion(names) { const entries = await Promise.all(names.map(async (name2) => { try { const versions = Object.entries(await this.getPackage(name2)); if (!versions.length) return; return { [name2]: versions[0][0] }; } catch (e) { } })); return entries.find(Boolean); } async _getPackage(name2) { try { const registry = await this.http.get(`/${name2}`); this.fullCache[name2] = this.tempCache[name2] = getVersions(Object.values(registry.versions).filter((remote) => { return !import_registry.default.isPlugin(name2) || import_registry.default.isCompatible("4", remote); })); this.flushData(); return this.fullCache[name2]; } catch (e) { logger.warn(e.message); } } setPackage(name2, versions) { this.fullCache[name2] = this.tempCache[name2] = getVersions(versions); this.flushData(); this.pkgTasks[name2] = Promise.resolve(this.fullCache[name2]); } getPackage(name2) { return this.pkgTasks[name2] ||= this._getPackage(name2); } async _getDeps() { const result = (0, import_koishi.valueMap)(this.manifest.dependencies, (request) => { return { request: request.replace(/^[~^]/, "") }; }); await (0, import_p_map.default)(Object.keys(result), async (name2) => { try { const meta = loadManifest(name2); result[name2].resolved = meta.version; result[name2].workspace = meta.$workspace; if (meta.$workspace) return; } catch { } if (!(0, import_semver.valid)(result[name2].request)) { result[name2].invalid = true; } const versions = await this.getPackage(name2); if (versions) result[name2].latest = Object.keys(versions)[0]; }, { concurrency: 10 }); return result; } getDeps() { return this.depTask ||= this._getDeps(); } refreshData() { this.ctx.get("console")?.refresh("registry"); this.ctx.get("console")?.refresh("packages"); } refresh(refresh = false) { this.pkgTasks = {}; this.fullCache = {}; this.tempCache = {}; this.depTask = this._getDeps(); if (!refresh) return; this.refreshData(); } async exec(args) { const name2 = this.agent?.name ?? "npm"; const useJson = name2 === "yarn" && this.agent.version >= "2"; if (name2 !== "yarn") args.unshift("install"); return new Promise((resolve3) => { if (useJson) args.push("--json"); const child = (0, import_execa.default)(name2, args, { cwd: this.cwd }); child.on("exit", (code) => resolve3(code)); child.on("error", () => resolve3(-1)); let stderr = ""; child.stderr.on("data", (data) => { data = stderr + data.toString(); const lines = data.split("\n"); stderr = lines.pop(); for (const line of lines) { logger.warn(line); } }); let stdout = ""; child.stdout.on("data", (data) => { data = stdout + data.toString(); const lines = data.split("\n"); stdout = lines.pop(); for (const line of lines) { if (!useJson || line[0] !== "{") { logger.info(line); continue; } try { const { type, data: data2 } = JSON.parse(line); logger[levelMap[type] ?? "info"](data2); } catch (error) { logger.warn(line); logger.warn(error); } } }); }); } async override(deps) { const filename = (0, import_path.resolve)(this.cwd, "package.json"); for (const key in deps) { if (deps[key]) { this.manifest.dependencies[key] = deps[key]; } else { delete this.manifest.dependencies[key]; } } this.manifest.dependencies = Object.fromEntries(Object.entries(this.manifest.dependencies).sort((a, b) => a[0].localeCompare(b[0]))); await import_fs.promises.writeFile(filename, JSON.stringify(this.manifest, null, 2) + "\n"); } _install() { const args = []; if (this.config.endpoint) { args.push("--registry", this.endpoint); } return this.exec(args); } _getLocalDeps(override) { return (0, import_koishi.valueMap)(override, (request, name2) => { const dep = { request }; try { const meta = loadManifest(name2); dep.resolved = meta.version; dep.workspace = meta.$workspace; } catch { } return dep; }); } async install(deps, forced) { const localDeps = this._getLocalDeps(deps); await this.override(deps); for (const name2 in deps) { const { resolved, workspace } = localDeps[name2] || {}; if (workspace || deps[name2] && resolved && (0, import_semver.satisfies)(resolved, deps[name2], { includePrerelease: true })) continue; forced = true; break; } if (forced) { const code = await this._install(); if (code) return code; } this.refresh(); const newDeps = await this.getDeps(); for (const name2 in localDeps) { const { resolved, workspace } = localDeps[name2]; if (workspace || !newDeps[name2]) continue; if (newDeps[name2].resolved === resolved) continue; try { if (!(require.resolve(name2) in require.cache)) continue; } catch (error) { logger.error(error); } this.ctx.loader.fullReload(); } this.refreshData(); return 0; } }; ((Installer2) => { Installer2.Config = import_koishi.Schema.object({ endpoint: import_koishi.Schema.string().role("link"), timeout: import_koishi.Schema.number().role("time").default(import_koishi.Time.second * 5) }); })(Installer || (Installer = {})); var installer_default = Installer; // src/node/market.ts var import_koishi2 = require("koishi"); var import_registry2 = __toESM(require("@koishijs/registry")); var import_shared = require("../shared"); var MarketProvider = class extends import_shared.MarketProvider { constructor(ctx, config) { super(ctx); this.config = config; if (config.endpoint) this.http = ctx.http.extend(config); this.flushData = ctx.throttle(() => { ctx.console.broadcast("market/patch", { data: this.tempCache, failed: this.failed.length, total: this.scanner.total, progress: this.scanner.progress }); this.tempCache = {}; }, 500); } static { __name(this, "MarketProvider"); } http; failed = []; scanner; fullCache = {}; tempCache = {}; flushData; async start(refresh = false) { this.failed = []; this.fullCache = {}; this.tempCache = {}; if (refresh) this.ctx.installer.refresh(true); await this.prepare(); super.start(); } async collect() { const { timeout } = this.config; const registry = this.ctx.installer.http; this.failed = []; this.scanner = new import_registry2.default(registry.get); if (this.http) { const result = await this.http.get(""); this.scanner.objects = result.objects.filter((object) => !object.ignored); this.scanner.total = this.scanner.objects.length; this.scanner.version = result.version; } else { await this.scanner.collect({ timeout }); } if (!this.scanner.version) { this.scanner.analyze({ version: "4", onFailure: /* @__PURE__ */ __name((name2, reason) => { this.failed.push(name2); if (registry.config.endpoint.startsWith("https://registry.npmmirror.com")) { if (this.ctx.http.isError(reason) && reason.response?.status === 404) { } } }, "onFailure"), onRegistry: /* @__PURE__ */ __name((registry2, versions) => { this.ctx.installer.setPackage(registry2.name, versions); }, "onRegistry"), onSuccess: /* @__PURE__ */ __name((object, versions) => { object.package.links ||= { npm: `${registry.config.endpoint.replace("registry.", "www.")}/package/${object.package.name}` }; this.fullCache[object.package.name] = this.tempCache[object.package.name] = object; }, "onSuccess"), after: /* @__PURE__ */ __name(() => this.flushData(), "after") }); } return null; } async get() { await this.prepare(); if (this._error) return { data: {}, failed: 0, total: 0, progress: 0 }; return this.scanner.version ? { registry: this.ctx.installer.endpoint, data: Object.fromEntries(this.scanner.objects.map((item) => [item.package.name, item])), failed: 0, total: this.scanner.total, progress: this.scanner.total, gravatar: process.env.GRAVATAR_MIRROR } : { registry: this.ctx.installer.endpoint, data: this.fullCache, failed: this.failed.length, total: this.scanner.total, progress: this.scanner.progress, gravatar: process.env.GRAVATAR_MIRROR }; } }; ((MarketProvider2) => { MarketProvider2.Config = import_koishi2.Schema.object({ endpoint: import_koishi2.Schema.string().role("link"), timeout: import_koishi2.Schema.number().role("time").default(import_koishi2.Time.second * 30), proxyAgent: import_koishi2.Schema.string().role("link") }); })(MarketProvider || (MarketProvider = {})); var market_default = MarketProvider; // src/node/index.ts __reExport(node_exports, require("../shared"), module.exports); var name = "market"; var inject = ["http"]; var usage = ` 如果插件市场页面提示「无法连接到插件市场」,则可以选择一个 Koishi 社区提供的镜像地址,填入下方对应的配置项中。 ## 插件市场(填入 search.endpoint) - [t4wefan](https://k.ilharp.cc/2611)(大陆):https://registry.koishi.t4wefan.pub/index.json - [Lipraty](https://k.ilharp.cc/3530)(大陆):https://koi.nyan.zone/registry/index.json - [itzdrli](https://k.ilharp.cc/9975)(全球):https://kp.itzdrli.cc - [Q78KG](https://k.ilharp.cc/10042)(全球):https://koishi-registry.yumetsuki.moe/index.json - Koishi(全球):https://registry.koishi.chat/index.json 要浏览更多社区镜像,请访问 [Koishi 论坛上的镜像一览](https://k.ilharp.cc/4000)。`; var Config = import_koishi3.Schema.object({ registry: installer_default.Config, search: market_default.Config }).i18n({ "zh-CN": require_schema_zh_CN() }); function apply(ctx, config) { if (!ctx.loader?.writable) { return ctx.logger("app").warn("@koishijs/plugin-market is only available for json/yaml config file"); } ctx.plugin(installer_default, config.registry); ctx.inject(["installer"], (ctx2) => { ctx2.i18n.define("zh-CN", require_message_zh_CN()); ctx2.command("plugin.install <name>", { authority: 4 }).alias(".i").action(async ({ session }, name2) => { if (!name2) return session.text(".expect-name"); const names = ctx2.installer.resolveName(name2); const deps = await ctx2.installer.getDeps(); name2 = names.find((name3) => deps[name3]); if (name2) return session.text(".already-installed"); const result = await ctx2.installer.findVersion(names); if (!result) return session.text(".not-found"); ctx2.loader.envData.message = { ...(0, import_koishi3.pick)(session, ["sid", "channelId", "guildId", "isDirect"]), content: session.text(".success") }; await ctx2.installer.install(result); ctx2.loader.envData.message = null; return session.text(".success"); }); ctx2.command("plugin.uninstall <name>", { authority: 4 }).alias(".r").action(async ({ session }, name2) => { if (!name2) return session.text(".expect-name"); const names = ctx2.installer.resolveName(name2); const deps = await ctx2.installer.getDeps(); name2 = names.find((name3) => deps[name3]); if (!name2) return session.text(".not-installed"); await ctx2.installer.install({ [name2]: null }); return session.text(".success"); }); ctx2.command("plugin.upgrade [name...]", { authority: 4 }).alias(".update", ".up").option("self", "-s, --koishi").action(async ({ session, options }, ...names) => { async function getPackages(names2) { if (!names2.length) return Object.keys(deps); names2 = names2.map((name2) => { const names3 = ctx2.installer.resolveName(name2); return names3.find((name3) => deps[name3]); }).filter(Boolean); if (options.self) names2.push("koishi"); return names2; } __name(getPackages, "getPackages"); ctx2.installer.refresh(true); const deps = await ctx2.installer.getDeps(); names = await getPackages(names); names = names.filter((name2) => { const { latest, resolved, invalid } = deps[name2]; try { return !invalid && (0, import_semver2.gt)(latest, resolved); } catch { } }); if (!names.length) return session.text(".all-updated"); const output = names.map((name2) => { const { latest, resolved } = deps[name2]; return `${name2}: ${resolved} -> ${latest}`; }); output.unshift(session.text(".available")); output.push(session.text(".prompt")); await session.send(output.join("\n")); const result = await session.prompt(); if (!["Y", "y"].includes(result?.trim())) { return session.text(".cancelled"); } ctx2.loader.envData.message = { ...(0, import_koishi3.pick)(session, ["sid", "channelId", "guildId", "isDirect"]), content: session.text(".success") }; await ctx2.installer.install(names.reduce((result2, name2) => { result2[name2] = deps[name2].latest; return result2; }, {})); ctx2.loader.envData.message = null; return session.text(".success"); }); }); ctx.inject(["console", "installer"], (ctx2) => { ctx2.plugin(DependencyProvider); ctx2.plugin(RegistryProvider); ctx2.plugin(market_default, config.search); ctx2.console.addEntry({ dev: (0, import_path2.resolve)(__dirname, "../../client/index.ts"), prod: (0, import_path2.resolve)(__dirname, "../../dist") }); ctx2.console.addListener("market/install", async (deps, forced) => { const code = await ctx2.installer.install(deps, forced); ctx2.get("console")?.refresh("dependencies"); ctx2.get("console")?.refresh("registry"); ctx2.get("console")?.refresh("packages"); return code; }, { authority: 4 }); ctx2.console.addListener("market/registry", async (names) => { const meta = await Promise.all(names.map((name2) => ctx2.installer.getPackage(name2))); return Object.fromEntries(meta.map((meta2, index) => [names[index], meta2])); }, { authority: 4 }); }); } __name(apply, "apply"); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Config, Installer, apply, inject, name, usage, ...require("../shared") }); //# sourceMappingURL=index.js.map