@hieuzest/koishi-plugin-market 2.12.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.
Files changed (41) hide show
  1. package/dist/index.js +1 -0
  2. package/dist/style.css +1 -0
  3. package/lib/browser/index.d.ts +12 -0
  4. package/lib/browser/index.mjs +86 -0
  5. package/lib/browser/index.mjs.map +6 -0
  6. package/lib/browser/market.d.ts +12 -0
  7. package/lib/index.d.ts +1 -0
  8. package/lib/node/deps.d.ts +15 -0
  9. package/lib/node/index.d.ts +33 -0
  10. package/lib/node/index.js +715 -0
  11. package/lib/node/index.js.map +6 -0
  12. package/lib/node/installer.d.ts +108 -0
  13. package/lib/node/market.d.ts +40 -0
  14. package/lib/shared/index.d.ts +32 -0
  15. package/lib/shared/index.js +63 -0
  16. package/lib/shared/index.js.map +6 -0
  17. package/lib/shared/index.mjs +41 -0
  18. package/lib/shared/index.mjs.map +6 -0
  19. package/package.json +88 -0
  20. package/src/browser/index.ts +27 -0
  21. package/src/browser/market.ts +19 -0
  22. package/src/index.ts +2 -0
  23. package/src/node/deps.ts +26 -0
  24. package/src/node/index.ts +192 -0
  25. package/src/node/installer.ts +488 -0
  26. package/src/node/locales/message.de-DE.yml +25 -0
  27. package/src/node/locales/message.en-US.yml +25 -0
  28. package/src/node/locales/message.fr-FR.yml +25 -0
  29. package/src/node/locales/message.ja-JP.yml +25 -0
  30. package/src/node/locales/message.ru-RU.yml +25 -0
  31. package/src/node/locales/message.zh-CN.yml +28 -0
  32. package/src/node/locales/message.zh-TW.yml +25 -0
  33. package/src/node/locales/schema.de-DE.yml +9 -0
  34. package/src/node/locales/schema.en-US.yml +9 -0
  35. package/src/node/locales/schema.fr-FR.yml +9 -0
  36. package/src/node/locales/schema.ja-JP.yml +9 -0
  37. package/src/node/locales/schema.ru-RU.yml +9 -0
  38. package/src/node/locales/schema.zh-CN.yml +10 -0
  39. package/src/node/locales/schema.zh-TW.yml +9 -0
  40. package/src/node/market.ts +113 -0
  41. package/src/shared/index.ts +63 -0
@@ -0,0 +1,192 @@
1
+ import { Context, Dict, pick, Schema } from 'koishi'
2
+ import { DependencyMetaKey, RemotePackage } from '@koishijs/registry'
3
+ import { gt } from 'semver'
4
+ import { resolve } from 'path'
5
+ import { DependencyProvider, RegistryProvider } from './deps'
6
+ import Installer from './installer'
7
+ import MarketProvider from './market'
8
+
9
+ export * from '../shared'
10
+
11
+ export { Installer }
12
+
13
+ declare module 'koishi' {
14
+ interface Context {
15
+ installer: Installer
16
+ }
17
+ }
18
+
19
+ declare module '@koishijs/console' {
20
+ namespace Console {
21
+ interface Services {
22
+ dependencies: DependencyProvider
23
+ registry: RegistryProvider
24
+ }
25
+ }
26
+
27
+ interface Events {
28
+ 'market/install'(deps: Dict<string>, forced?: boolean): Promise<number>
29
+ 'market/registry'(names: string[]): Promise<Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>>>
30
+ }
31
+ }
32
+
33
+ export const name = 'market'
34
+ export const inject = ['http']
35
+
36
+ export const usage = `
37
+ 如果插件市场页面提示「无法连接到插件市场」,则可以选择一个 Koishi 社区提供的镜像地址,填入下方对应的配置项中。
38
+
39
+ ## 插件市场(填入 search.endpoint)
40
+
41
+ - [t4wefan](https://k.ilharp.cc/2611)(大陆):https://registry.koishi.t4wefan.pub/index.json
42
+ - [Lipraty](https://k.ilharp.cc/3530)(大陆):https://koi.nyan.zone/registry/index.json
43
+ - [itzdrli](https://k.ilharp.cc/9975)(全球):https://kp.itzdrli.cc
44
+ - [Q78KG](https://k.ilharp.cc/10042)(全球):https://koishi-registry.yumetsuki.moe/index.json
45
+ - Koishi(全球):https://registry.koishi.chat/index.json
46
+
47
+ 要浏览更多社区镜像,请访问 [Koishi 论坛上的镜像一览](https://k.ilharp.cc/4000)。`
48
+
49
+ // ## 软件源(填入 npmRegistryServer)
50
+
51
+ // - 淘宝(大陆):https://registry.npmmirror.com
52
+ // - 腾讯(大陆):https://mirrors.cloud.tencent.com/npm
53
+ // - npm(全球):https://registry.npmjs.org
54
+ // - yarn(全球):https://registry.yarnpkg.com
55
+
56
+ export interface Config {
57
+ registry?: Installer.Config
58
+ search?: MarketProvider.Config
59
+ }
60
+
61
+ export const Config: Schema<Config> = Schema.object({
62
+ registry: Installer.Config,
63
+ search: MarketProvider.Config,
64
+ }).i18n({
65
+ 'zh-CN': require('./locales/schema.zh-CN'),
66
+ })
67
+
68
+ export function apply(ctx: Context, config: Config) {
69
+ if (!ctx.loader?.writable) {
70
+ return ctx.logger('app').warn('@koishijs/plugin-market is only available for json/yaml config file')
71
+ }
72
+
73
+ ctx.plugin(Installer, config.registry)
74
+
75
+ ctx.inject(['installer'], (ctx) => {
76
+ ctx.i18n.define('zh-CN', require('./locales/message.zh-CN'))
77
+
78
+ ctx.command('plugin.install <name>', { authority: 4 })
79
+ .alias('.i')
80
+ .action(async ({ session }, name) => {
81
+ if (!name) return session.text('.expect-name')
82
+
83
+ // check local dependencies
84
+ const names = ctx.installer.resolveName(name)
85
+ const deps = await ctx.installer.getDeps()
86
+ name = names.find((name) => deps[name])
87
+ if (name) return session.text('.already-installed')
88
+
89
+ // find proper version
90
+ const result = await ctx.installer.findVersion(names)
91
+ if (!result) return session.text('.not-found')
92
+
93
+ // set restart message
94
+ ctx.loader.envData.message = {
95
+ ...pick(session, ['sid', 'channelId', 'guildId', 'isDirect']),
96
+ content: session.text('.success'),
97
+ }
98
+ await ctx.installer.install(result)
99
+ ctx.loader.envData.message = null
100
+ return session.text('.success')
101
+ })
102
+
103
+ ctx.command('plugin.uninstall <name>', { authority: 4 })
104
+ .alias('.r')
105
+ .action(async ({ session }, name) => {
106
+ if (!name) return session.text('.expect-name')
107
+
108
+ // check local dependencies
109
+ const names = ctx.installer.resolveName(name)
110
+ const deps = await ctx.installer.getDeps()
111
+ name = names.find((name) => deps[name])
112
+ if (!name) return session.text('.not-installed')
113
+
114
+ await ctx.installer.install({ [name]: null })
115
+ return session.text('.success')
116
+ })
117
+
118
+ ctx.command('plugin.upgrade [name...]', { authority: 4 })
119
+ .alias('.update', '.up')
120
+ .option('self', '-s, --koishi')
121
+ .action(async ({ session, options }, ...names) => {
122
+ async function getPackages(names: string[]) {
123
+ if (!names.length) return Object.keys(deps)
124
+ names = names.map((name) => {
125
+ const names = ctx.installer.resolveName(name)
126
+ return names.find((name) => deps[name])
127
+ }).filter(Boolean)
128
+ if (options.self) names.push('koishi')
129
+ return names
130
+ }
131
+
132
+ // refresh dependencies
133
+ ctx.installer.refresh(true)
134
+ const deps = await ctx.installer.getDeps()
135
+ names = await getPackages(names)
136
+ names = names.filter((name) => {
137
+ const { latest, resolved, invalid } = deps[name]
138
+ try {
139
+ return !invalid && gt(latest, resolved)
140
+ } catch {}
141
+ })
142
+ if (!names.length) return session.text('.all-updated')
143
+
144
+ const output = names.map((name) => {
145
+ const { latest, resolved } = deps[name]
146
+ return `${name}: ${resolved} -> ${latest}`
147
+ })
148
+ output.unshift(session.text('.available'))
149
+ output.push(session.text('.prompt'))
150
+ await session.send(output.join('\n'))
151
+ const result = await session.prompt()
152
+ if (!['Y', 'y'].includes(result?.trim())) {
153
+ return session.text('.cancelled')
154
+ }
155
+
156
+ ctx.loader.envData.message = {
157
+ ...pick(session, ['sid', 'channelId', 'guildId', 'isDirect']),
158
+ content: session.text('.success'),
159
+ }
160
+ await ctx.installer.install(names.reduce((result, name) => {
161
+ result[name] = deps[name].latest
162
+ return result
163
+ }, {}))
164
+ ctx.loader.envData.message = null
165
+ return session.text('.success')
166
+ })
167
+ })
168
+
169
+ ctx.inject(['console', 'installer'], (ctx) => {
170
+ ctx.plugin(DependencyProvider)
171
+ ctx.plugin(RegistryProvider)
172
+ ctx.plugin(MarketProvider, config.search)
173
+
174
+ ctx.console.addEntry({
175
+ dev: resolve(__dirname, '../../client/index.ts'),
176
+ prod: resolve(__dirname, '../../dist'),
177
+ })
178
+
179
+ ctx.console.addListener('market/install', async (deps, forced) => {
180
+ const code = await ctx.installer.install(deps, forced)
181
+ ctx.get('console')?.refresh('dependencies')
182
+ ctx.get('console')?.refresh('registry')
183
+ ctx.get('console')?.refresh('packages')
184
+ return code
185
+ }, { authority: 4 })
186
+
187
+ ctx.console.addListener('market/registry', async (names) => {
188
+ const meta = await Promise.all(names.map(name => ctx.installer.getPackage(name)))
189
+ return Object.fromEntries(meta.map((meta, index) => [names[index], meta]))
190
+ }, { authority: 4 })
191
+ })
192
+ }
@@ -0,0 +1,488 @@
1
+ import { Context, defineProperty, Dict, filterKeys, HTTP, Logger, mapValues, pick, Schema, Service, Time, valueMap } from 'koishi'
2
+ import Scanner, { DependencyMetaKey, PackageJson, Registry, RemotePackage } from '@koishijs/registry'
3
+ import { dirname, join, parse, resolve } from 'path'
4
+ import { SimpleGit, simpleGit } from 'simple-git'
5
+ import { existsSync, promises as fsp, readFileSync } from 'fs'
6
+ import { compare, satisfies, valid } from 'semver'
7
+ import {} from '@koishijs/console'
8
+ import {} from '@koishijs/loader'
9
+ import getRegistry from 'get-registry'
10
+ import which from 'which-pm-runs'
11
+ import spawn from 'execa'
12
+ import pMap from 'p-map'
13
+ import {} from '.'
14
+
15
+ declare module '@koishijs/registry' {
16
+ interface PackageJson {
17
+ resolutions?: Dict<string>
18
+ }
19
+ }
20
+
21
+ const logger = new Logger('market')
22
+
23
+ export interface Dependency {
24
+ /** name */
25
+ name: string
26
+ /**
27
+ * yarn protocol
28
+ * @example `workspace`, `npm`, `git`
29
+ */
30
+ protocol: string
31
+ /**
32
+ * override package name, empty for default name
33
+ *
34
+ * git: url for git protocol
35
+ */
36
+ path?: string
37
+ /** workspace name for monorepo, default to name */
38
+ workspaceName?: string
39
+ /**
40
+ * requested semver range
41
+ *
42
+ * git: requested tag
43
+ * @example `^1.2.3` -> `1.2.3`
44
+ * @example `v1.2.3`
45
+ */
46
+ request: string
47
+ /**
48
+ * installed package version
49
+ *
50
+ * git: same as request
51
+ * @example `1.2.5`
52
+ * @example `v1.2.3`
53
+ */
54
+ resolved?: string
55
+ /** whether it is a workspace package */
56
+ workspace?: boolean
57
+ /** valid (unsupported) syntax */
58
+ invalid?: boolean
59
+ /** latest version */
60
+ latest?: string
61
+ }
62
+
63
+ export namespace Dependency {
64
+ export const RESOLUTION_PREFIX = '▶'
65
+
66
+ export function isResolution(name: string) {
67
+ return name.startsWith(RESOLUTION_PREFIX)
68
+ }
69
+
70
+ export function asResolution(name: string) {
71
+ return name.startsWith(RESOLUTION_PREFIX) ? name : RESOLUTION_PREFIX + name
72
+ }
73
+
74
+ export function asDependency(name: string) {
75
+ return name.startsWith(RESOLUTION_PREFIX) ? name.slice(RESOLUTION_PREFIX.length) : name
76
+ }
77
+
78
+ export function parse(name: string, request: string): Dependency {
79
+ const workspaceMatch = request.match(/^workspace:(.+)/)
80
+ if (workspaceMatch) {
81
+ return {
82
+ name,
83
+ protocol: 'workspace',
84
+ request,
85
+ invalid: true,
86
+ }
87
+ }
88
+
89
+ const npmMatch = request.match(/^npm:(.+)/)
90
+ if (npmMatch) {
91
+ return {
92
+ name,
93
+ protocol: 'npm',
94
+ path: npmMatch[1].startsWith('@') ? ('@' + npmMatch[1].split('@')[1]) : npmMatch[1].split('@')[0],
95
+ request: npmMatch[1].split('@')[npmMatch[1].startsWith('@') ? 2 : 1]?.replace(/^[~^]/, '') ?? '',
96
+ }
97
+ }
98
+
99
+ const gitMatch = request.match(/^(git[@\+].+)/)
100
+ if (gitMatch) {
101
+ return {
102
+ name,
103
+ protocol: 'git',
104
+ path: gitMatch[1].split('#')[0],
105
+ request: gitMatch[1].split('#')[1]?.split('&').find(item => item.startsWith('tag='))?.replace('tag=', '') || '',
106
+ workspaceName: gitMatch[1].split('#')[1]?.split('&').find(item => item.startsWith('workspace='))?.replace('workspace=', '') || undefined,
107
+ }
108
+ }
109
+
110
+ if (valid(request.replace(/^[~^]/, ''))) {
111
+ return {
112
+ name,
113
+ protocol: 'npm',
114
+ request: request.replace(/^[~^]/, ''),
115
+ }
116
+ }
117
+
118
+ return {
119
+ name,
120
+ protocol: 'invalid',
121
+ request,
122
+ invalid: true,
123
+ }
124
+ }
125
+
126
+ export function stringify(dep: Dependency, target: string) {
127
+ if (!target) return ''
128
+ switch (dep.protocol) {
129
+ case 'workspace':
130
+ return `workspace:${target}`
131
+ case 'npm':
132
+ if (dep.path) {
133
+ return `npm:${dep.path}@${target}`
134
+ }
135
+ return target
136
+ case 'git':
137
+ return `${dep.path}#tag=${target}` + (dep.workspaceName ? `&workspace=${dep.workspaceName}` : '')
138
+ }
139
+ }
140
+ }
141
+
142
+ export interface YarnLog {
143
+ type: 'warning' | 'info' | 'error' | string
144
+ name: number | null
145
+ displayName: string
146
+ indent?: string
147
+ data: string
148
+ }
149
+
150
+ const levelMap = {
151
+ 'info': 'info',
152
+ 'warning': 'debug',
153
+ 'error': 'warn',
154
+ }
155
+
156
+ export interface LocalPackage extends PackageJson {
157
+ private?: boolean
158
+ $workspace?: boolean
159
+ }
160
+
161
+ export function loadManifest(name: string) {
162
+ const filename = require.resolve(name + '/package.json')
163
+ const meta: LocalPackage = JSON.parse(readFileSync(filename, 'utf8'))
164
+ meta.dependencies ||= {}
165
+ defineProperty(meta, '$workspace', !filename.includes('node_modules'))
166
+ return meta
167
+ }
168
+
169
+ function getVersions(versions: RemotePackage[]) {
170
+ return Object.fromEntries(versions
171
+ .map(item => [item.version, pick(item, ['peerDependencies', 'peerDependenciesMeta', 'deprecated'])] as const)
172
+ .sort(([a], [b]) => compare(b, a)))
173
+ }
174
+
175
+ class Installer extends Service {
176
+ public http: HTTP
177
+ public endpoint: string
178
+ public fullCache: Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>> = {}
179
+ public tempCache: Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>> = {}
180
+
181
+ private pkgTasks: Dict<Promise<Dict<Pick<RemotePackage, DependencyMetaKey>>>> = {}
182
+ private agent = which()
183
+ private manifest: PackageJson
184
+ private depTask: Promise<Dict<Dependency>>
185
+ private flushData: () => void
186
+ private git: SimpleGit = simpleGit()
187
+
188
+ constructor(public ctx: Context, public config: Installer.Config) {
189
+ super(ctx, 'installer')
190
+ this.manifest = loadManifest(this.cwd)
191
+ this.flushData = ctx.throttle(() => {
192
+ ctx.get('console')?.broadcast('market/registry', this.tempCache)
193
+ this.tempCache = {}
194
+ }, 500)
195
+ }
196
+
197
+ get cwd() {
198
+ return this.ctx.baseDir
199
+ }
200
+
201
+ async start() {
202
+ const { endpoint, timeout } = this.config
203
+ this.endpoint = endpoint || await getRegistry()
204
+ this.http = this.ctx.http.extend({
205
+ endpoint: this.endpoint,
206
+ timeout,
207
+ })
208
+ }
209
+
210
+ resolveName(name: string) {
211
+ if (name.startsWith('@koishijs/plugin-')) return [name]
212
+ if (name.match(/(^|\/)koishi-plugin-/)) return [name]
213
+ if (name[0] === '@') {
214
+ const [left, right] = name.split('/')
215
+ return [`${left}/koishi-plugin-${right}`]
216
+ } else {
217
+ return [`@koishijs/plugin-${name}`, `koishi-plugin-${name}`]
218
+ }
219
+ }
220
+
221
+ async findVersion(names: string[]) {
222
+ const entries = await Promise.all(names.map(async (name) => {
223
+ try {
224
+ const versions = Object.entries(await this.getPackage(name))
225
+ if (!versions.length) return
226
+ return { [name]: versions[0][0] }
227
+ } catch (e) {}
228
+ }))
229
+ return entries.find(Boolean)
230
+ }
231
+
232
+ private async _getPackage(name: string, path: string) {
233
+ try {
234
+ const registry = await this.http.get<Registry>(`/${path}`)
235
+ this.fullCache[name] = this.tempCache[name] = getVersions(Object.values(registry.versions).filter((remote) => {
236
+ return !Scanner.isPlugin(path) || Scanner.isCompatible('4', remote)
237
+ }))
238
+ this.flushData()
239
+ return this.fullCache[name]
240
+ } catch (e) {
241
+ logger.warn(`Cannot get package ${name} with ${path}: ${e.message}`)
242
+ }
243
+ }
244
+
245
+ private async _getGitPackage(name: string, path: string) {
246
+ try {
247
+ const output = await this.git.raw([
248
+ 'ls-remote',
249
+ '--tags',
250
+ '--sort=-version:refname', // sort by semver descending
251
+ path.replace(/^git\+/, ''),
252
+ ])
253
+ const lines = output.trim().split('\n')
254
+ const tags = []
255
+
256
+ for (const line of lines) {
257
+ if (!line || !line.includes('refs/tags/')) continue
258
+
259
+ const [, ref] = line.split('\t')
260
+ if (ref && ref.startsWith('refs/tags/') && !ref.endsWith('^{}')) {
261
+ const tag = ref.replace('refs/tags/', '')
262
+ tags.push(tag)
263
+ }
264
+ }
265
+
266
+ const versions = Object.fromEntries(tags.map(tag => [tag, {}]))
267
+ this.fullCache[name] = this.tempCache[name] = versions
268
+ this.flushData()
269
+ return this.fullCache[name]
270
+ } catch (e) {
271
+ logger.warn(e.message)
272
+ }
273
+ }
274
+
275
+ setPackage(name: string, versions: RemotePackage[]) {
276
+ this.fullCache[name] = this.tempCache[name] = getVersions(versions)
277
+ this.flushData()
278
+ this.pkgTasks[name] = Promise.resolve(this.fullCache[name])
279
+ }
280
+
281
+ getPackage(name: string, dep?: Dependency) {
282
+ switch (dep?.protocol) {
283
+ case 'git':
284
+ return this.pkgTasks[name] ||= this._getGitPackage(name, dep.path)
285
+ default:
286
+ return this.pkgTasks[name] ||= this._getPackage(name, dep?.path ?? dep?.name ?? name)
287
+ }
288
+ }
289
+
290
+ _loadManifest2(name: string) {
291
+ const packagePath = resolve(process.cwd(), require.resolve(name))
292
+ if (!packagePath.startsWith(process.cwd())) throw new Error(`Package ${name} not in workspace`)
293
+ let currentDir = dirname(packagePath)
294
+ while (currentDir !== parse(currentDir).root) {
295
+ const filename = join(currentDir, 'package.json')
296
+ if (existsSync(filename)) {
297
+ try {
298
+ const meta: LocalPackage = JSON.parse(readFileSync(filename, 'utf8'))
299
+ meta.dependencies ||= {}
300
+ defineProperty(meta, '$workspace', !filename.includes('node_modules'))
301
+ return meta
302
+ } catch (e) {}
303
+ }
304
+ currentDir = dirname(currentDir)
305
+ }
306
+ throw new Error(`Cannot find package.json for ${name}`)
307
+ }
308
+
309
+ private async _getDeps(local: boolean = false) {
310
+ const deps = valueMap(this.manifest.dependencies ?? {}, (request, name) => Dependency.parse(name, request))
311
+ const resolutions = Object.fromEntries(Object.entries(this.manifest.resolutions ?? {})
312
+ .map(([name, request]) => [Dependency.asResolution(name), Dependency.parse(name, request)]))
313
+ const result = { ...deps, ...resolutions }
314
+ await pMap(Object.entries(result), async ([name, dep]) => {
315
+ if (dep.protocol === 'git') {
316
+ result[name].resolved = result[name].request
317
+ } else {
318
+ try {
319
+ // some dependencies may be left with no local installation
320
+ const meta = loadManifest(dep.name)
321
+ result[name].resolved = meta.version
322
+ result[name].workspace = meta.$workspace
323
+ if (meta.$workspace) return
324
+ } catch {
325
+ try {
326
+ const meta = this._loadManifest2(dep.name)
327
+ result[name].resolved = meta.version
328
+ result[name].workspace = meta.$workspace
329
+ if (meta.$workspace) return
330
+ } catch (e) {
331
+ logger.warn(`Cannot load local manifest for ${name}: ${e.message}`)
332
+ }
333
+ }
334
+ }
335
+
336
+ if (!local) {
337
+ const versions = await this.getPackage(name, dep)
338
+ if (versions) result[name].latest = Object.keys(versions)[0]
339
+ }
340
+ }, { concurrency: 10 })
341
+ return result
342
+ }
343
+
344
+ getDeps() {
345
+ return this.depTask ||= this._getDeps()
346
+ }
347
+
348
+ refreshData() {
349
+ this.ctx.get('console')?.refresh('registry')
350
+ this.ctx.get('console')?.refresh('packages')
351
+ }
352
+
353
+ refresh(refresh = false) {
354
+ this.pkgTasks = {}
355
+ this.fullCache = {}
356
+ this.tempCache = {}
357
+ this.depTask = this._getDeps()
358
+ if (!refresh) return
359
+ this.refreshData()
360
+ }
361
+
362
+ async exec(args: string[]) {
363
+ const name = this.agent?.name ?? 'npm'
364
+ const useJson = name === 'yarn' && this.agent.version >= '2'
365
+ if (name !== 'yarn') args.unshift('install')
366
+ return new Promise<number>((resolve) => {
367
+ if (useJson) args.push('--json')
368
+ const child = spawn(name, args, { cwd: this.cwd })
369
+ child.on('exit', (code) => resolve(code))
370
+ child.on('error', () => resolve(-1))
371
+
372
+ let stderr = ''
373
+ child.stderr.on('data', (data) => {
374
+ data = stderr + data.toString()
375
+ const lines = data.split('\n')
376
+ stderr = lines.pop()!
377
+ for (const line of lines) {
378
+ logger.warn(line)
379
+ }
380
+ })
381
+
382
+ let stdout = ''
383
+ child.stdout.on('data', (data) => {
384
+ data = stdout + data.toString()
385
+ const lines = data.split('\n')
386
+ stdout = lines.pop()!
387
+ for (const line of lines) {
388
+ if (!useJson || line[0] !== '{') {
389
+ logger.info(line)
390
+ continue
391
+ }
392
+ try {
393
+ const { type, data } = JSON.parse(line) as YarnLog
394
+ logger[levelMap[type] ?? 'info'](data)
395
+ } catch (error) {
396
+ logger.warn(line)
397
+ logger.warn(error)
398
+ }
399
+ }
400
+ })
401
+ })
402
+ }
403
+
404
+ async override(deps: Dict<string>) {
405
+ const filename = resolve(this.cwd, 'package.json')
406
+ for (const key in deps) {
407
+ if (Dependency.isResolution(key)) {
408
+ const realKey = Dependency.asDependency(key)
409
+ this.manifest.resolutions ||= {}
410
+ if (deps[key]) {
411
+ this.manifest.resolutions[realKey] = deps[key]
412
+ } else {
413
+ delete this.manifest.resolutions[realKey]
414
+ }
415
+ } else {
416
+ if (deps[key]) {
417
+ this.manifest.dependencies[key] = deps[key]
418
+ } else {
419
+ delete this.manifest.dependencies[key]
420
+ }
421
+ }
422
+ }
423
+ this.manifest.dependencies = Object.fromEntries(Object.entries(this.manifest.dependencies).sort((a, b) => a[0].localeCompare(b[0])))
424
+ if (this.manifest.resolutions) {
425
+ this.manifest.resolutions = Object.fromEntries(Object.entries(this.manifest.resolutions ?? {}).sort((a, b) => a[0].localeCompare(b[0])))
426
+ }
427
+ await fsp.writeFile(filename, JSON.stringify(this.manifest, null, 2) + '\n')
428
+ }
429
+
430
+ private _install() {
431
+ const args: string[] = []
432
+ if (this.config.endpoint) {
433
+ args.push('--registry', this.endpoint)
434
+ }
435
+ return this.exec(args)
436
+ }
437
+
438
+ async install(deps: Dict<string>, forced?: boolean) {
439
+ const localDeps = await this._getDeps(true).then((res) => filterKeys(res, (name) => Object.hasOwn(deps, name)))
440
+ deps = mapValues(deps, (request, name) => Object.hasOwn(localDeps, name) ? Dependency.stringify(localDeps[name], request) : request)
441
+ await this.override(deps)
442
+
443
+ for (const name in deps) {
444
+ const { resolved, workspace, protocol } = localDeps[name] || {}
445
+ if (protocol !== 'git' && workspace || deps[name] && resolved && satisfies(resolved, deps[name], { includePrerelease: true })) continue
446
+ forced = true
447
+ break
448
+ }
449
+
450
+ if (forced) {
451
+ const code = await this._install()
452
+ if (code) return code
453
+ }
454
+
455
+ this.refresh()
456
+ const newDeps = await this.getDeps()
457
+ for (const key in localDeps) {
458
+ const { name, resolved, workspace } = localDeps[key]
459
+ if (workspace || !newDeps[key]) continue
460
+ if (newDeps[key].resolved === resolved) continue
461
+ try {
462
+ if (!(require.resolve(name) in require.cache)) continue
463
+ } catch (error) {
464
+ // FIXME https://github.com/koishijs/webui/issues/273
465
+ // I have no idea why this happens and how to fix it.
466
+ logger.error(error)
467
+ }
468
+ this.ctx.loader.fullReload()
469
+ }
470
+ this.refreshData()
471
+
472
+ return 0
473
+ }
474
+ }
475
+
476
+ namespace Installer {
477
+ export interface Config {
478
+ endpoint?: string
479
+ timeout?: number
480
+ }
481
+
482
+ export const Config: Schema<Config> = Schema.object({
483
+ endpoint: Schema.string().role('link'),
484
+ timeout: Schema.number().role('time').default(Time.second * 5),
485
+ }) // TODO .hidden()
486
+ }
487
+
488
+ export default Installer
@@ -0,0 +1,25 @@
1
+ commands.plugin:
2
+ description: 插件管理
3
+ commands.plugin.install:
4
+ description: 安装插件
5
+ messages:
6
+ expect-name: 请输入插件名。
7
+ already-installed: 该插件已安装。
8
+ not-found: 未找到该插件。
9
+ success: 安装成功!
10
+ commands.plugin.uninstall:
11
+ description: 卸载插件
12
+ messages:
13
+ expect-name: 请输入插件名。
14
+ not-installed: 该插件未安装。
15
+ success: 卸载成功!
16
+ commands.plugin.upgrade:
17
+ description: 升级插件
18
+ options:
19
+ self: 升级 Koishi 本体
20
+ messages:
21
+ all-updated: 所有插件已是最新版本。
22
+ available: 有可用的依赖更新:
23
+ prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
24
+ cancelled: 已取消操作。
25
+ success: 升级成功!