@koishi-ce/plugin-market 1.1.0 → 1.1.2
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 +3 -4
- package/dist/style.css +1 -1
- package/lib/index.d.ts +9 -19
- package/lib/index.mjs +165 -119
- package/package.json +9 -7
- package/src/index.test.ts +3 -3
- package/src/node/deps.ts +1 -1
- package/src/node/index.ts +3 -3
- package/src/node/installer/exec.ts +53 -0
- package/src/node/installer/index.ts +337 -0
- package/src/node/installer/integrity.ts +104 -0
- package/src/node/installer/manifest.ts +108 -0
- package/src/node/installer/registry-config.ts +16 -0
- package/src/node/installer/versions.ts +73 -0
- package/src/node/installer.ts +0 -586
- package/src/node/locales/message.de-DE.yml +0 -29
- package/src/node/locales/message.en-US.yml +0 -29
- package/src/node/locales/message.fr-FR.yml +0 -29
- package/src/node/locales/message.ja-JP.yml +0 -29
- package/src/node/locales/message.ru-RU.yml +0 -29
- package/src/node/locales/message.zh-CN.yml +0 -32
- package/src/node/locales/message.zh-TW.yml +0 -29
- package/src/node/locales/schema.de-DE.yml +0 -13
- package/src/node/locales/schema.en-US.yml +0 -13
- package/src/node/locales/schema.fr-FR.yml +0 -13
- package/src/node/locales/schema.ja-JP.yml +0 -13
- package/src/node/locales/schema.ru-RU.yml +0 -13
- package/src/node/locales/schema.zh-CN.yml +0 -14
- package/src/node/locales/schema.zh-TW.yml +0 -13
- /package/src/node/{proc.ts → installer/proc.ts} +0 -0
package/src/node/installer.ts
DELETED
|
@@ -1,586 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
-
// Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
-
// Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
existsSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
realpathSync,
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import type {} from "@koishi-ce/console";
|
|
13
|
-
import {
|
|
14
|
-
type Context,
|
|
15
|
-
type Dict,
|
|
16
|
-
defineProperty,
|
|
17
|
-
type HTTP,
|
|
18
|
-
Logger,
|
|
19
|
-
pick,
|
|
20
|
-
Schema,
|
|
21
|
-
Service,
|
|
22
|
-
Time,
|
|
23
|
-
valueMap,
|
|
24
|
-
} from "@koishi-ce/koishi";
|
|
25
|
-
import type {} from "@koishi-ce/loader";
|
|
26
|
-
import type {} from "@koishi-ce/plugin-market";
|
|
27
|
-
import Scanner, {
|
|
28
|
-
type DependencyMetaKey,
|
|
29
|
-
isResidentInCache,
|
|
30
|
-
mapLimit,
|
|
31
|
-
type PackageJson,
|
|
32
|
-
type Registry,
|
|
33
|
-
type RemotePackage,
|
|
34
|
-
resolvePackageJson,
|
|
35
|
-
} from "@koishi-ce/registry";
|
|
36
|
-
import { compare, satisfies, valid } from "semver";
|
|
37
|
-
import { spawnBun } from "./proc.ts";
|
|
38
|
-
|
|
39
|
-
const logger = new Logger("market");
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 判断依赖声明是否受护栏保护、不可被安装清单覆盖或删除。两类:
|
|
43
|
-
* - `workspace:` 声明是本仓库(monorepo)对上游名的归属(如 koishi
|
|
44
|
-
* 裸名 shim,见 packages/shim/koishi);
|
|
45
|
-
* - `npm:@koishi-ce/...` alias 是下游脚手架生成项目对上游名的归属
|
|
46
|
-
* (如 "koishi": "npm:@koishi-ce/koishi-shim@^4.18.11",见
|
|
47
|
-
* packages/shim/koishi-shim)。
|
|
48
|
-
* 两者被覆盖或删除都会让 peer 解析失去归属,重新拉下 npm 官方包形成
|
|
49
|
-
* 第二份框架副本。
|
|
50
|
-
*/
|
|
51
|
-
function isGuardedRequest(
|
|
52
|
-
request: string | undefined,
|
|
53
|
-
): boolean {
|
|
54
|
-
return (
|
|
55
|
-
request?.startsWith("workspace:") === true ||
|
|
56
|
-
request?.startsWith("npm:@koishi-ce") === true
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 从 fromDir 沿 node_modules 链纯 fs 探测依赖包,命中返回其真实目录。
|
|
62
|
-
* realpath 化是 isolated 布局的关键:顶层条目是 symlink,包的依赖链接
|
|
63
|
-
* 挂在 .bun 真实目录的同级 node_modules,从链接路径向上爬会错误地查
|
|
64
|
-
* 顶层(isolated 不提升传递依赖)。
|
|
65
|
-
*/
|
|
66
|
-
function probePackage(
|
|
67
|
-
fromDir: string,
|
|
68
|
-
name: string,
|
|
69
|
-
): string | undefined {
|
|
70
|
-
let dir = resolve(fromDir);
|
|
71
|
-
for (;;) {
|
|
72
|
-
const manifestPath = join(
|
|
73
|
-
dir,
|
|
74
|
-
"node_modules",
|
|
75
|
-
name,
|
|
76
|
-
"package.json",
|
|
77
|
-
);
|
|
78
|
-
if (existsSync(manifestPath)) {
|
|
79
|
-
try {
|
|
80
|
-
return dirname(realpathSync(manifestPath));
|
|
81
|
-
} catch {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const parent = dirname(dir);
|
|
86
|
-
if (parent === dir) return undefined;
|
|
87
|
-
dir = parent;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** 从单个 .npmrc 文件提取 registry 配置项;文件不存在或读取出错一律视为未配置 */
|
|
92
|
-
function readNpmrcRegistry(
|
|
93
|
-
file: string,
|
|
94
|
-
): string | undefined {
|
|
95
|
-
try {
|
|
96
|
-
for (const line of readFileSync(file, "utf8").split(
|
|
97
|
-
/\r?\n/,
|
|
98
|
-
)) {
|
|
99
|
-
const matched = /^\s*registry\s*=\s*(\S+)\s*$/.exec(
|
|
100
|
-
line,
|
|
101
|
-
);
|
|
102
|
-
const value = matched?.[1];
|
|
103
|
-
if (value) return value;
|
|
104
|
-
}
|
|
105
|
-
} catch {}
|
|
106
|
-
return undefined;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 读取本机 npm registry 配置(优先级对齐 npm 自身:环境变量 > 项目
|
|
111
|
-
* .npmrc > 用户 ~/.npmrc)。刻意不 spawn 子进程探测——原依赖
|
|
112
|
-
* get-registry 按 user-agent 选 `bun config get registry`,而 Bun 没有
|
|
113
|
-
* config 子命令,Bun 运行时下子进程退出码 1 直接炸掉 market 服务启动;
|
|
114
|
-
* 读 npmrc 是零子进程的等价路径。任何一步都拿不到时回落 npm 官方
|
|
115
|
-
* 源(对齐被移除的 get-registry 的默认值),保证 http 恒有 endpoint
|
|
116
|
-
* 可拼接相对 URL——缺省时相对请求会在 resolveURL 抛 Invalid URL。
|
|
117
|
-
*/
|
|
118
|
-
function getLocalRegistry(
|
|
119
|
-
cwd: string,
|
|
120
|
-
userHome: string = homedir(),
|
|
121
|
-
): string {
|
|
122
|
-
const candidates = [
|
|
123
|
-
process.env["npm_config_registry"],
|
|
124
|
-
readNpmrcRegistry(join(cwd, ".npmrc")),
|
|
125
|
-
readNpmrcRegistry(join(userHome, ".npmrc")),
|
|
126
|
-
];
|
|
127
|
-
for (const candidate of candidates) {
|
|
128
|
-
if (
|
|
129
|
-
candidate?.startsWith("https://") ||
|
|
130
|
-
candidate?.startsWith("http://")
|
|
131
|
-
) {
|
|
132
|
-
return candidate;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return "https://registry.npmjs.org/";
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface Dependency {
|
|
139
|
-
/**
|
|
140
|
-
* requested semver range
|
|
141
|
-
* @example `^1.2.3` -> `1.2.3`
|
|
142
|
-
*/
|
|
143
|
-
request: string;
|
|
144
|
-
/**
|
|
145
|
-
* installed package version
|
|
146
|
-
* @example `1.2.5`
|
|
147
|
-
*/
|
|
148
|
-
resolved?: string | undefined;
|
|
149
|
-
/** whether it is a workspace package */
|
|
150
|
-
workspace?: boolean | undefined;
|
|
151
|
-
/** valid (unsupported) syntax */
|
|
152
|
-
invalid?: boolean | undefined;
|
|
153
|
-
/** latest version */
|
|
154
|
-
latest?: string | undefined;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
interface LocalPackage extends PackageJson {
|
|
158
|
-
private?: boolean;
|
|
159
|
-
$workspace?: boolean;
|
|
160
|
-
/** loadManifest 归一化保证 dependencies 必有 */
|
|
161
|
-
dependencies: Record<string, string>;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function loadManifest(name: string) {
|
|
165
|
-
// resolvePackageJson 以纯 fs 探测为主路径:市场安装流程在包落盘前的
|
|
166
|
-
// 探测不能触碰解析 API,否则触发 Bun 的父目录快照缓存(装完即失败)
|
|
167
|
-
const filename = resolvePackageJson(name);
|
|
168
|
-
const meta: LocalPackage = JSON.parse(
|
|
169
|
-
readFileSync(filename, "utf8"),
|
|
170
|
-
);
|
|
171
|
-
meta.dependencies ||= {};
|
|
172
|
-
defineProperty(
|
|
173
|
-
meta,
|
|
174
|
-
"$workspace",
|
|
175
|
-
!filename.includes("node_modules"),
|
|
176
|
-
);
|
|
177
|
-
return meta;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function getVersions(versions: RemotePackage[]) {
|
|
181
|
-
return Object.fromEntries(
|
|
182
|
-
versions
|
|
183
|
-
.map(
|
|
184
|
-
(item) =>
|
|
185
|
-
[
|
|
186
|
-
item.version,
|
|
187
|
-
pick(item, [
|
|
188
|
-
"peerDependencies",
|
|
189
|
-
"peerDependenciesMeta",
|
|
190
|
-
"deprecated",
|
|
191
|
-
]),
|
|
192
|
-
] as const,
|
|
193
|
-
)
|
|
194
|
-
.sort(([a], [b]) => compare(b, a)),
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
class Installer extends Service {
|
|
199
|
-
declare http: HTTP;
|
|
200
|
-
declare endpoint: string | undefined;
|
|
201
|
-
public fullCache: Dict<
|
|
202
|
-
Dict<Pick<RemotePackage, DependencyMetaKey>>
|
|
203
|
-
> = {};
|
|
204
|
-
public tempCache: Dict<
|
|
205
|
-
Dict<Pick<RemotePackage, DependencyMetaKey>>
|
|
206
|
-
> = {};
|
|
207
|
-
|
|
208
|
-
private pkgTasks: Dict<
|
|
209
|
-
Promise<Dict<Pick<RemotePackage, DependencyMetaKey>>>
|
|
210
|
-
> = {};
|
|
211
|
-
private manifest: LocalPackage;
|
|
212
|
-
declare private depTask: Promise<Dict<Dependency>>;
|
|
213
|
-
private flushData: () => void;
|
|
214
|
-
|
|
215
|
-
override config: Installer.Config;
|
|
216
|
-
|
|
217
|
-
constructor(ctx: Context, config: Installer.Config) {
|
|
218
|
-
super(ctx, "installer");
|
|
219
|
-
this.config = config;
|
|
220
|
-
this.manifest = loadManifest(this.cwd);
|
|
221
|
-
this.flushData = ctx.throttle(() => {
|
|
222
|
-
ctx
|
|
223
|
-
.get("console")
|
|
224
|
-
?.broadcast("market/registry", this.tempCache);
|
|
225
|
-
this.tempCache = {};
|
|
226
|
-
}, 500);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
get cwd() {
|
|
230
|
-
return this.ctx.baseDir;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
override async start() {
|
|
234
|
-
const { endpoint, timeout } = this.config;
|
|
235
|
-
this.endpoint = endpoint ?? getLocalRegistry(this.cwd);
|
|
236
|
-
const options: HTTP.Config = {};
|
|
237
|
-
if (this.endpoint) options.endpoint = this.endpoint;
|
|
238
|
-
if (timeout !== undefined) options.timeout = timeout;
|
|
239
|
-
this.http = this.ctx.http.extend(options);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
resolveName(name: string) {
|
|
243
|
-
if (name.startsWith("@koishijs/plugin-")) return [name];
|
|
244
|
-
if (name.match(/(^|\/)koishi-plugin-/)) return [name];
|
|
245
|
-
if (name[0] === "@") {
|
|
246
|
-
const [left, right] = name.split("/");
|
|
247
|
-
return [`${left}/koishi-plugin-${right}`];
|
|
248
|
-
} else {
|
|
249
|
-
return [
|
|
250
|
-
`@koishijs/plugin-${name}`,
|
|
251
|
-
`koishi-plugin-${name}`,
|
|
252
|
-
];
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async findVersion(names: string[]) {
|
|
257
|
-
const entries = await Promise.all(
|
|
258
|
-
names.map(async (name) => {
|
|
259
|
-
try {
|
|
260
|
-
const versions = Object.entries(
|
|
261
|
-
await this.getPackage(name),
|
|
262
|
-
);
|
|
263
|
-
const [latest] = versions;
|
|
264
|
-
if (!latest) return undefined;
|
|
265
|
-
return { [name]: latest[0] };
|
|
266
|
-
} catch {
|
|
267
|
-
return undefined;
|
|
268
|
-
}
|
|
269
|
-
}),
|
|
270
|
-
);
|
|
271
|
-
return entries.find(
|
|
272
|
-
(entry): entry is Dict<string> => entry !== undefined,
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private async _getPackage(name: string) {
|
|
277
|
-
try {
|
|
278
|
-
const registry = await this.http.get<Registry>(
|
|
279
|
-
`/${name}`,
|
|
280
|
-
);
|
|
281
|
-
const versions = getVersions(
|
|
282
|
-
Object.values(registry.versions).filter(
|
|
283
|
-
(remote) => {
|
|
284
|
-
if (name === "koishi")
|
|
285
|
-
return satisfies(remote.version, "4");
|
|
286
|
-
return (
|
|
287
|
-
!Scanner.isPlugin(name) ||
|
|
288
|
-
Scanner.isCompatible("4", remote)
|
|
289
|
-
);
|
|
290
|
-
},
|
|
291
|
-
),
|
|
292
|
-
);
|
|
293
|
-
this.fullCache[name] = this.tempCache[name] =
|
|
294
|
-
versions;
|
|
295
|
-
this.flushData();
|
|
296
|
-
return versions;
|
|
297
|
-
} catch (error) {
|
|
298
|
-
logger.warn(error);
|
|
299
|
-
return {};
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
setPackage(name: string, versions: RemotePackage[]) {
|
|
304
|
-
this.fullCache[name] = this.tempCache[name] =
|
|
305
|
-
getVersions(versions);
|
|
306
|
-
this.flushData();
|
|
307
|
-
this.pkgTasks[name] = Promise.resolve(
|
|
308
|
-
this.fullCache[name],
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
getPackage(name: string) {
|
|
313
|
-
return (this.pkgTasks[name] ||= this._getPackage(name));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
private async _getDeps() {
|
|
317
|
-
const result = valueMap(
|
|
318
|
-
this.manifest.dependencies,
|
|
319
|
-
(request) => {
|
|
320
|
-
return {
|
|
321
|
-
request: request.replace(/^[~^]/, ""),
|
|
322
|
-
} as Dependency;
|
|
323
|
-
},
|
|
324
|
-
);
|
|
325
|
-
await mapLimit(
|
|
326
|
-
Object.keys(result),
|
|
327
|
-
10,
|
|
328
|
-
async (name) => {
|
|
329
|
-
const dep = result[name];
|
|
330
|
-
if (!dep) return;
|
|
331
|
-
try {
|
|
332
|
-
// some dependencies may be left with no local installation
|
|
333
|
-
const meta = loadManifest(name);
|
|
334
|
-
dep.resolved = meta.version;
|
|
335
|
-
dep.workspace = meta.$workspace;
|
|
336
|
-
if (meta.$workspace) return;
|
|
337
|
-
} catch {}
|
|
338
|
-
|
|
339
|
-
if (!valid(dep.request)) {
|
|
340
|
-
dep.invalid = true;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const versions = await this.getPackage(name);
|
|
344
|
-
if (versions) dep.latest = Object.keys(versions)[0];
|
|
345
|
-
},
|
|
346
|
-
);
|
|
347
|
-
return result;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
getDeps() {
|
|
351
|
-
return (this.depTask ||= this._getDeps());
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
refreshData() {
|
|
355
|
-
this.ctx.get("console")?.refresh("registry");
|
|
356
|
-
this.ctx.get("console")?.refresh("packages");
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
refresh(refresh = false) {
|
|
360
|
-
this.pkgTasks = {};
|
|
361
|
-
this.fullCache = {};
|
|
362
|
-
this.tempCache = {};
|
|
363
|
-
this.depTask = this._getDeps();
|
|
364
|
-
if (!refresh) return;
|
|
365
|
-
this.refreshData();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
async exec(args: string[]) {
|
|
369
|
-
// Bun-first:CE 生态只存在 bun 这一种包管理器,直接驱动 bun 执行
|
|
370
|
-
// 安装(上游经 which-pm-runs 探测 npm/yarn/bun,此处固定为 bun)
|
|
371
|
-
args.unshift("install");
|
|
372
|
-
return new Promise<number>((resolve) => {
|
|
373
|
-
const child = spawnBun(args, this.cwd);
|
|
374
|
-
child.on("exit", (code) => resolve(code ?? -1));
|
|
375
|
-
child.on("error", () => resolve(-1));
|
|
376
|
-
|
|
377
|
-
let stderr = "";
|
|
378
|
-
child.stderr?.on("data", (data) => {
|
|
379
|
-
stderr += data.toString();
|
|
380
|
-
const lines = stderr.split("\n");
|
|
381
|
-
stderr = lines.pop() ?? "";
|
|
382
|
-
for (const line of lines) {
|
|
383
|
-
logger.warn(line);
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
let stdout = "";
|
|
388
|
-
child.stdout?.on("data", (data) => {
|
|
389
|
-
stdout += data.toString();
|
|
390
|
-
const lines = stdout.split("\n");
|
|
391
|
-
stdout = lines.pop() ?? "";
|
|
392
|
-
for (const line of lines) {
|
|
393
|
-
logger.info(line);
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
async override(deps: Dict<string | null>) {
|
|
400
|
-
const filename = resolve(this.cwd, "package.json");
|
|
401
|
-
// 现读现写:this.manifest 原为构造期的启动快照,运行期间根
|
|
402
|
-
// package.json 可能已被外部更新,基于快照整体重写会抹掉变更
|
|
403
|
-
this.manifest = JSON.parse(
|
|
404
|
-
readFileSync(filename, "utf8"),
|
|
405
|
-
) as LocalPackage;
|
|
406
|
-
this.manifest.dependencies ||= {};
|
|
407
|
-
for (const key in deps) {
|
|
408
|
-
if (deps[key]) {
|
|
409
|
-
if (
|
|
410
|
-
isGuardedRequest(this.manifest.dependencies[key])
|
|
411
|
-
) {
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
this.manifest.dependencies[key] = deps[key];
|
|
415
|
-
} else if (
|
|
416
|
-
!isGuardedRequest(this.manifest.dependencies[key])
|
|
417
|
-
) {
|
|
418
|
-
delete this.manifest.dependencies[key];
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
this.manifest.dependencies = Object.fromEntries(
|
|
422
|
-
Object.entries(this.manifest.dependencies).sort(
|
|
423
|
-
(a, b) => a[0].localeCompare(b[0]),
|
|
424
|
-
),
|
|
425
|
-
);
|
|
426
|
-
// 仓库格式权威是 biome(tab 缩进),按 tab 写出避免装插件后
|
|
427
|
-
// package.json 被重排成空格、lint 报格式漂移
|
|
428
|
-
await Bun.write(
|
|
429
|
-
filename,
|
|
430
|
-
`${JSON.stringify(this.manifest, null, "\t")}\n`,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
private _install() {
|
|
435
|
-
const args: string[] = [];
|
|
436
|
-
if (this.endpoint) {
|
|
437
|
-
args.push("--registry", this.endpoint);
|
|
438
|
-
}
|
|
439
|
-
return this.exec(args);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* 安装完整性校验:从本次安装的包出发递归纯 fs 探测依赖可达性,返回
|
|
444
|
-
* 「包: 缺失依赖」描述列表。背景是 Bun isolated 布局的增量安装缺陷
|
|
445
|
-
* (2026-09-07 实证):升级插件时包目录 hash 变化,而其依赖的解析
|
|
446
|
-
* 结果未变,bun 的增量安装跳过新包目录内的依赖链接建立——插件入口
|
|
447
|
-
* (顶层 symlink)可加载,内部依赖的 require 却炸 MODULE_NOT_FOUND;
|
|
448
|
-
* 此时补跑一次普通 bun install,bun 按磁盘实际状态检测到包不完整
|
|
449
|
-
* 即重建链接(秒级)。全程只走 existsSync / readFileSync /
|
|
450
|
-
* realpathSync,不触碰解析 API(防父目录快照缓存,与
|
|
451
|
-
* resolvePackageJson 同一纪律)。peer / optional 依赖由宿主或按需
|
|
452
|
-
* 提供不入校验;workspace / alias 等非 semver 声明的落盘名与键名无
|
|
453
|
-
* 直接映射,同样跳过。
|
|
454
|
-
*/
|
|
455
|
-
private _findMissingDeps(
|
|
456
|
-
deps: Dict<string | null>,
|
|
457
|
-
): string[] {
|
|
458
|
-
const problems: string[] = [];
|
|
459
|
-
const visited = new Set<string>();
|
|
460
|
-
const walk = (dir: string, label: string) => {
|
|
461
|
-
if (visited.has(dir)) return;
|
|
462
|
-
visited.add(dir);
|
|
463
|
-
let manifest: LocalPackage;
|
|
464
|
-
try {
|
|
465
|
-
manifest = JSON.parse(
|
|
466
|
-
readFileSync(join(dir, "package.json"), "utf8"),
|
|
467
|
-
) as LocalPackage;
|
|
468
|
-
} catch {
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
for (const [dep, request] of Object.entries(
|
|
472
|
-
manifest.dependencies ?? {},
|
|
473
|
-
)) {
|
|
474
|
-
if (request.includes(":")) continue;
|
|
475
|
-
const depDir = probePackage(dir, dep);
|
|
476
|
-
if (!depDir) {
|
|
477
|
-
problems.push(`${label} 的依赖 ${dep} 未被链接`);
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
walk(depDir, `${label} → ${dep}`);
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
for (const name in deps) {
|
|
484
|
-
const request = deps[name];
|
|
485
|
-
if (!request || request.includes(":")) continue;
|
|
486
|
-
const dir = probePackage(this.cwd, name);
|
|
487
|
-
// 顶层包未落盘属于安装失败,由退出码与后续加载告警兜底,
|
|
488
|
-
// 不属于「包已装但链接缺失」的补装场景
|
|
489
|
-
if (!dir) continue;
|
|
490
|
-
walk(dir, name);
|
|
491
|
-
}
|
|
492
|
-
return problems;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
private _getLocalDeps(override: Dict<string | null>) {
|
|
496
|
-
return valueMap(override, (request, name) => {
|
|
497
|
-
const dep = { request } as Dependency;
|
|
498
|
-
try {
|
|
499
|
-
const meta = loadManifest(name);
|
|
500
|
-
dep.resolved = meta.version;
|
|
501
|
-
dep.workspace = meta.$workspace;
|
|
502
|
-
} catch {}
|
|
503
|
-
return dep;
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async install(
|
|
508
|
-
deps: Dict<string | null>,
|
|
509
|
-
forced?: boolean,
|
|
510
|
-
) {
|
|
511
|
-
const localDeps = this._getLocalDeps(deps);
|
|
512
|
-
await this.override(deps);
|
|
513
|
-
|
|
514
|
-
let shouldInstall = forced === true;
|
|
515
|
-
for (const name in deps) {
|
|
516
|
-
const request = deps[name];
|
|
517
|
-
const local = localDeps[name];
|
|
518
|
-
if (
|
|
519
|
-
local?.workspace ||
|
|
520
|
-
(request &&
|
|
521
|
-
local?.resolved &&
|
|
522
|
-
satisfies(local.resolved, request, {
|
|
523
|
-
includePrerelease: true,
|
|
524
|
-
}))
|
|
525
|
-
)
|
|
526
|
-
continue;
|
|
527
|
-
shouldInstall = true;
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (shouldInstall) {
|
|
532
|
-
const code = await this._install();
|
|
533
|
-
if (code) return code;
|
|
534
|
-
// isolated 布局的增量安装可能漏建新包目录的依赖链接(机理
|
|
535
|
-
// 见 _findMissingDeps 注释):装完即校验,缺失时补跑一次
|
|
536
|
-
// 安装——bun 按磁盘实际状态重建链接,秒级自愈,无需人工
|
|
537
|
-
const missing = this._findMissingDeps(deps);
|
|
538
|
-
if (missing.length) {
|
|
539
|
-
logger.warn(
|
|
540
|
-
`检测到增量安装未建立部分依赖链接(isolated 布局已知缺陷),正在补装: ${missing.join("; ")}`,
|
|
541
|
-
);
|
|
542
|
-
const retry = await this._install();
|
|
543
|
-
if (retry) return retry;
|
|
544
|
-
if (this._findMissingDeps(deps).length) {
|
|
545
|
-
logger.warn(
|
|
546
|
-
"自动补装后依赖链接仍不完整,请在项目根目录运行 bun install --force 并重启 koishi",
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
this.refresh();
|
|
553
|
-
const newDeps = await this.getDeps();
|
|
554
|
-
for (const name in localDeps) {
|
|
555
|
-
const local = localDeps[name];
|
|
556
|
-
const newDep = newDeps[name];
|
|
557
|
-
if (!local || !newDep || local.workspace) continue;
|
|
558
|
-
if (newDep.resolved === local.resolved) continue;
|
|
559
|
-
// 版本变化且旧版本仍驻留内存时才需要整进程重载(判定不走
|
|
560
|
-
// 解析 API,规避 Bun 负缓存,见 isResidentInCache)
|
|
561
|
-
if (isResidentInCache(name))
|
|
562
|
-
this.ctx.loader.fullReload();
|
|
563
|
-
}
|
|
564
|
-
this.refreshData();
|
|
565
|
-
|
|
566
|
-
return 0;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// erasableSyntaxOnly 禁止含运行时值的 namespace,
|
|
570
|
-
// 原 namespace 内的 Config 常量移到此处的静态字段,对外形状不变
|
|
571
|
-
static Config: Schema<Installer.Config> = Schema.object({
|
|
572
|
-
endpoint: Schema.string().role("link"),
|
|
573
|
-
timeout: Schema.number()
|
|
574
|
-
.role("time")
|
|
575
|
-
.default(Time.second * 5),
|
|
576
|
-
}); // TODO .hidden()
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
declare namespace Installer {
|
|
580
|
-
export interface Config {
|
|
581
|
-
endpoint?: string;
|
|
582
|
-
timeout?: number;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
export default Installer;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
-
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
-
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
-
|
|
5
|
-
commands.plugin:
|
|
6
|
-
description: 插件管理
|
|
7
|
-
commands.plugin.install:
|
|
8
|
-
description: 安装插件
|
|
9
|
-
messages:
|
|
10
|
-
expect-name: 请输入插件名。
|
|
11
|
-
already-installed: 该插件已安装。
|
|
12
|
-
not-found: 未找到该插件。
|
|
13
|
-
success: 安装成功!
|
|
14
|
-
commands.plugin.uninstall:
|
|
15
|
-
description: 卸载插件
|
|
16
|
-
messages:
|
|
17
|
-
expect-name: 请输入插件名。
|
|
18
|
-
not-installed: 该插件未安装。
|
|
19
|
-
success: 卸载成功!
|
|
20
|
-
commands.plugin.upgrade:
|
|
21
|
-
description: 升级插件
|
|
22
|
-
options:
|
|
23
|
-
self: 升级 Koishi 本体
|
|
24
|
-
messages:
|
|
25
|
-
all-updated: 所有插件已是最新版本。
|
|
26
|
-
available: 有可用的依赖更新:
|
|
27
|
-
prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
|
|
28
|
-
cancelled: 已取消操作。
|
|
29
|
-
success: 升级成功!
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
-
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
-
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
-
|
|
5
|
-
commands.plugin:
|
|
6
|
-
description: Plugin management
|
|
7
|
-
commands.plugin.install:
|
|
8
|
-
description: Install Plugins
|
|
9
|
-
messages:
|
|
10
|
-
expect-name: 请输入插件名。
|
|
11
|
-
already-installed: 该插件已安装。
|
|
12
|
-
not-found: 未找到该插件。
|
|
13
|
-
success: Installation Successful!
|
|
14
|
-
commands.plugin.uninstall:
|
|
15
|
-
description: Uninstall plugin
|
|
16
|
-
messages:
|
|
17
|
-
expect-name: 请输入插件名。
|
|
18
|
-
not-installed: 该插件未安装。
|
|
19
|
-
success: 卸载成功!
|
|
20
|
-
commands.plugin.upgrade:
|
|
21
|
-
description: Upgrade Plugin
|
|
22
|
-
options:
|
|
23
|
-
self: Upgrade Koishi core
|
|
24
|
-
messages:
|
|
25
|
-
all-updated: 所有插件已是最新版本。
|
|
26
|
-
available: 有可用的依赖更新:
|
|
27
|
-
prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
|
|
28
|
-
cancelled: Operation canceled.
|
|
29
|
-
success: Upgrade Successful!
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
-
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
-
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
-
|
|
5
|
-
commands.plugin:
|
|
6
|
-
description: 插件管理
|
|
7
|
-
commands.plugin.install:
|
|
8
|
-
description: Installation de plugins
|
|
9
|
-
messages:
|
|
10
|
-
expect-name: 请输入插件名。
|
|
11
|
-
already-installed: 该插件已安装。
|
|
12
|
-
not-found: 未找到该插件。
|
|
13
|
-
success: 安装成功!
|
|
14
|
-
commands.plugin.uninstall:
|
|
15
|
-
description: 卸载插件
|
|
16
|
-
messages:
|
|
17
|
-
expect-name: 请输入插件名。
|
|
18
|
-
not-installed: 该插件未安装。
|
|
19
|
-
success: 卸载成功!
|
|
20
|
-
commands.plugin.upgrade:
|
|
21
|
-
description: 升级插件
|
|
22
|
-
options:
|
|
23
|
-
self: 升级 Koishi 本体
|
|
24
|
-
messages:
|
|
25
|
-
all-updated: 所有插件已是最新版本。
|
|
26
|
-
available: 有可用的依赖更新:
|
|
27
|
-
prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
|
|
28
|
-
cancelled: 已取消操作。
|
|
29
|
-
success: 升级成功!
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
-
# Copyright (c) 2019-present Shigma and Koishijs contributors.
|
|
3
|
-
# Copyright (c) 2026-present Koishi-CE contributors.
|
|
4
|
-
|
|
5
|
-
commands.plugin:
|
|
6
|
-
description: 插件管理
|
|
7
|
-
commands.plugin.install:
|
|
8
|
-
description: 安装插件
|
|
9
|
-
messages:
|
|
10
|
-
expect-name: 请输入插件名。
|
|
11
|
-
already-installed: 该插件已安装。
|
|
12
|
-
not-found: 未找到该插件。
|
|
13
|
-
success: 安装成功!
|
|
14
|
-
commands.plugin.uninstall:
|
|
15
|
-
description: 卸载插件
|
|
16
|
-
messages:
|
|
17
|
-
expect-name: 请输入插件名。
|
|
18
|
-
not-installed: 该插件未安装。
|
|
19
|
-
success: 卸载成功!
|
|
20
|
-
commands.plugin.upgrade:
|
|
21
|
-
description: 升级插件
|
|
22
|
-
options:
|
|
23
|
-
self: 升级 Koishi 本体
|
|
24
|
-
messages:
|
|
25
|
-
all-updated: 所有插件已是最新版本。
|
|
26
|
-
available: 有可用的依赖更新:
|
|
27
|
-
prompt: 输入「Y」升级全部依赖,输入「N」取消操作。
|
|
28
|
-
cancelled: 已取消操作。
|
|
29
|
-
success: 升级成功!
|