@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.
- package/dist/index.js +1 -0
- package/dist/style.css +1 -0
- package/lib/browser/index.d.ts +12 -0
- package/lib/browser/index.mjs +86 -0
- package/lib/browser/index.mjs.map +6 -0
- package/lib/browser/market.d.ts +12 -0
- package/lib/index.d.ts +1 -0
- package/lib/node/deps.d.ts +15 -0
- package/lib/node/index.d.ts +33 -0
- package/lib/node/index.js +715 -0
- package/lib/node/index.js.map +6 -0
- package/lib/node/installer.d.ts +108 -0
- package/lib/node/market.d.ts +40 -0
- package/lib/shared/index.d.ts +32 -0
- package/lib/shared/index.js +63 -0
- package/lib/shared/index.js.map +6 -0
- package/lib/shared/index.mjs +41 -0
- package/lib/shared/index.mjs.map +6 -0
- package/package.json +88 -0
- package/src/browser/index.ts +27 -0
- package/src/browser/market.ts +19 -0
- package/src/index.ts +2 -0
- package/src/node/deps.ts +26 -0
- package/src/node/index.ts +192 -0
- package/src/node/installer.ts +488 -0
- package/src/node/locales/message.de-DE.yml +25 -0
- package/src/node/locales/message.en-US.yml +25 -0
- package/src/node/locales/message.fr-FR.yml +25 -0
- package/src/node/locales/message.ja-JP.yml +25 -0
- package/src/node/locales/message.ru-RU.yml +25 -0
- package/src/node/locales/message.zh-CN.yml +28 -0
- package/src/node/locales/message.zh-TW.yml +25 -0
- package/src/node/locales/schema.de-DE.yml +9 -0
- package/src/node/locales/schema.en-US.yml +9 -0
- package/src/node/locales/schema.fr-FR.yml +9 -0
- package/src/node/locales/schema.ja-JP.yml +9 -0
- package/src/node/locales/schema.ru-RU.yml +9 -0
- package/src/node/locales/schema.zh-CN.yml +10 -0
- package/src/node/locales/schema.zh-TW.yml +9 -0
- package/src/node/market.ts +113 -0
- 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: 升级成功!
|