@aalis/plugin-package-manager 0.8.1 → 0.10.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/README.md +5 -3
- package/dist/index.d.ts +45 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +285 -209
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts +70 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +70 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/texts.d.ts +139 -0
- package/dist/texts.d.ts.map +1 -0
- package/dist/texts.js +78 -0
- package/dist/texts.js.map +1 -0
- package/package.json +11 -9
package/dist/index.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
|
+
import { failureText, unmetDependencies } from '@aalis/api-doctor';
|
|
2
3
|
import { hostConfig } from '@aalis/api-host-config';
|
|
4
|
+
import { hostPaths } from '@aalis/api-host-paths';
|
|
3
5
|
import { packageManager, } from '@aalis/api-package-manager';
|
|
4
6
|
import { pluginDefinitionOf, pluginSource } from '@aalis/api-plugin-source';
|
|
5
|
-
import {
|
|
6
|
-
import { appService,
|
|
7
|
-
import {
|
|
7
|
+
import { processService } from '@aalis/api-process';
|
|
8
|
+
import { appService, definePlugin, logger, optional, pluginsService, provide, services, } from '@aalis/core';
|
|
9
|
+
import { TextError, textClauses, textOf, toSource } from '@aalis/schema-i18n';
|
|
8
10
|
import { classifyDepSpec, isRegistryDep, isUpgrade } from '@aalis/util-dep-spec';
|
|
11
|
+
import { t } from './texts.js';
|
|
9
12
|
// ===== 实现 =====
|
|
10
13
|
/** 短命令(cp、npm view 等)的超时。 */
|
|
11
14
|
const QUICK_TIMEOUT_MS = 120_000;
|
|
12
15
|
/**
|
|
13
|
-
* 安装类命令的超时。`npm install`
|
|
14
|
-
*
|
|
16
|
+
* 安装类命令的超时。`npm install` 要装全依赖树并跑依赖的安装脚本(比如 plugin-tool-browser
|
|
17
|
+
* 依赖的 puppeteer 安装时要下载 Chrome),快机快网下已达 95 秒量级,而超时行为是 SIGKILL——留足余量,
|
|
15
18
|
* 否则会在半装状态下被杀。
|
|
16
19
|
*/
|
|
17
20
|
const INSTALL_TIMEOUT_MS = 600_000;
|
|
@@ -75,8 +78,8 @@ export function declaresPlugin(pkgJson) {
|
|
|
75
78
|
}
|
|
76
79
|
/**
|
|
77
80
|
* 包类型是否归市场装卸:插件(判据同 {@link declaresPlugin})与前端界面(`aalis-interface`)
|
|
78
|
-
* 归市场管,返回 `undefined
|
|
79
|
-
*
|
|
81
|
+
* 归市场管,返回 `undefined`;其余按类型关键词返回种类码(没有类型关键词的是 other),拒绝时显示 KIND_NAMES 里的名字。
|
|
82
|
+
* 装与卸共用这一份判据——同一件事维护两份同义判据,正是本仓已出事四次的形态(依赖来源判据、定级数学、包名正则、关键词分类)。
|
|
80
83
|
* 纯函数,便于单测。
|
|
81
84
|
*/
|
|
82
85
|
export function nonMarketKind(keywords) {
|
|
@@ -84,37 +87,48 @@ export function nonMarketKind(keywords) {
|
|
|
84
87
|
if (declaresPlugin({ keywords: kw }) || kw.includes('aalis-interface'))
|
|
85
88
|
return undefined;
|
|
86
89
|
if (kw.includes('aalis-core'))
|
|
87
|
-
return '
|
|
90
|
+
return 'core';
|
|
88
91
|
if (kw.includes('aalis-runtime'))
|
|
89
|
-
return '
|
|
92
|
+
return 'runtime';
|
|
90
93
|
if (kw.includes('aalis-api'))
|
|
91
|
-
return '
|
|
94
|
+
return 'api';
|
|
92
95
|
if (kw.includes('aalis-schema'))
|
|
93
|
-
return '
|
|
96
|
+
return 'schema';
|
|
94
97
|
if (kw.includes('aalis-util'))
|
|
95
|
-
return '
|
|
96
|
-
return '
|
|
98
|
+
return 'util';
|
|
99
|
+
return 'other';
|
|
97
100
|
}
|
|
101
|
+
/** 不归市场装卸的包种类给人看的名字 */
|
|
102
|
+
const KIND_NAMES = {
|
|
103
|
+
core: t('kindCore'),
|
|
104
|
+
runtime: t('kindRuntime'),
|
|
105
|
+
api: t('kindApi'),
|
|
106
|
+
schema: t('kindSchema'),
|
|
107
|
+
util: t('kindUtil'),
|
|
108
|
+
other: t('kindOther'),
|
|
109
|
+
};
|
|
98
110
|
/**
|
|
99
|
-
* 找出「卸载 target 会断其服务依赖」的插件:target
|
|
100
|
-
*
|
|
101
|
-
*
|
|
111
|
+
* 找出「卸载 target 会断其服务依赖」的插件:target 的某个实例正在提供或马上就提供的某服务 S 没有别的插件正在提供,且有别的
|
|
112
|
+
* 插件 required 依赖 S(uses 里未包 optional)→ 这些插件会被打断。卸载连同 target 的全部实例一起卸下,所以按同名的每个实例判:算在提供的是
|
|
113
|
+
* active、activating,以及 pending 而只是在等的(按 api-doctor 的共享判据不缺什么,它要的在启动,起来就提供);disabled、
|
|
114
|
+
* error 与 pending 而缺服务的不在提供。别的提供者只算 active 与 activating(替代得是确定的)。已禁用(disabled / disposed)
|
|
115
|
+
* 的依赖者不算。available 是当前有提供者的服务名(services.names())。纯函数,便于单测。
|
|
102
116
|
*/
|
|
103
|
-
export function findServiceDependents(targetName, status) {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
117
|
+
export function findServiceDependents(targetName, status, available) {
|
|
118
|
+
const providing = (p) => p.state === 'active' || p.state === 'activating';
|
|
119
|
+
const unmet = unmetDependencies(status, available);
|
|
120
|
+
const live = status.filter(p => p.name === targetName &&
|
|
121
|
+
(providing(p) || (p.state === 'pending' && unmet.get(p.instanceId)?.lacking.length === 0)));
|
|
108
122
|
const dependents = new Set();
|
|
109
|
-
for (const svc of
|
|
110
|
-
const otherProvider = status.some(p => p.name !== targetName && (p
|
|
123
|
+
for (const svc of new Set(live.flatMap(p => p.provides ?? []))) {
|
|
124
|
+
const otherProvider = status.some(p => p.name !== targetName && providing(p) && (p.provides ?? []).includes(svc));
|
|
111
125
|
if (otherProvider)
|
|
112
126
|
continue; // 还有别的提供者在提供,删了不致命
|
|
113
127
|
for (const p of status) {
|
|
114
128
|
if (p.name !== targetName &&
|
|
115
129
|
p.state !== 'disabled' &&
|
|
116
130
|
p.state !== 'disposed' &&
|
|
117
|
-
|
|
131
|
+
p.uses.some(use => use.kind === 'required' && use.service === svc)) {
|
|
118
132
|
dependents.add(p.name);
|
|
119
133
|
}
|
|
120
134
|
}
|
|
@@ -155,25 +169,16 @@ const PKG_SPEC_RE = /^(@[a-z0-9][a-z0-9\-_.]*\/)?[a-z0-9][a-z0-9\-_.]*(@[a-z0-9]
|
|
|
155
169
|
*/
|
|
156
170
|
export function validatePackageSpec(spec) {
|
|
157
171
|
if (typeof spec !== 'string' || spec.length === 0)
|
|
158
|
-
return '
|
|
172
|
+
return t('emptyName');
|
|
159
173
|
if (!PKG_SPEC_RE.test(spec))
|
|
160
|
-
return
|
|
174
|
+
return t('badName', { name: JSON.stringify(spec) });
|
|
161
175
|
return undefined;
|
|
162
176
|
}
|
|
163
177
|
// ===== 插件入口 =====
|
|
164
|
-
const configSchema = defineConfig({
|
|
165
|
-
projectRoot: {
|
|
166
|
-
type: 'string',
|
|
167
|
-
label: '项目目录',
|
|
168
|
-
description: '安装插件的项目目录,留空使用当前工作目录;须与宿主的插件加载目录一致。',
|
|
169
|
-
default: '',
|
|
170
|
-
onInvalid: 'error',
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
178
|
const uses = {
|
|
174
179
|
proc: processService,
|
|
180
|
+
paths: hostPaths,
|
|
175
181
|
logger,
|
|
176
|
-
config,
|
|
177
182
|
provide,
|
|
178
183
|
services,
|
|
179
184
|
app: optional(appService),
|
|
@@ -182,32 +187,19 @@ const uses = {
|
|
|
182
187
|
source: optional(pluginSource),
|
|
183
188
|
};
|
|
184
189
|
function createService(caps) {
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const proc = createProcessGateway(caps.proc);
|
|
190
|
+
const log = caps.logger.use;
|
|
191
|
+
const proc = caps.proc.use;
|
|
188
192
|
function getApp() {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return app;
|
|
193
|
-
}
|
|
194
|
-
/** 把配置里的相对/绝对路径归一成绝对路径;未配置则用当前工作目录。 */
|
|
195
|
-
function projectRoot() {
|
|
196
|
-
const override = cfg.projectRoot;
|
|
197
|
-
if (override.length > 0) {
|
|
198
|
-
return override.startsWith('/') ? override : `${process.cwd()}/${override.replace(/^\.?\/+/, '')}`;
|
|
199
|
-
}
|
|
200
|
-
return process.cwd();
|
|
193
|
+
if (!caps.app.present)
|
|
194
|
+
throw new TextError(t('noApp'));
|
|
195
|
+
return caps.app.use;
|
|
201
196
|
}
|
|
202
197
|
return createPackageManager({
|
|
203
198
|
proc,
|
|
204
|
-
log,
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
// package.json
|
|
208
|
-
// 宿主若给 startAalis 传了非 cwd 的 projectDir(公开选项),必须用本配置项对齐。
|
|
209
|
-
projectRoot,
|
|
210
|
-
// 经 process 网关的 readExternalFile 读,而非 node:fs:目标(项目根 package.json、
|
|
199
|
+
log: log,
|
|
200
|
+
// 装到宿主的安装集:与插件加载器读的是同一份 package.json,否则装了永远不加载
|
|
201
|
+
projectRoot: () => caps.paths.use.app,
|
|
202
|
+
// 经 process 服务的 readExternalFile 读,而非 node:fs:目标(项目根 package.json、
|
|
211
203
|
// lockfile、node_modules/<pkg>/package.json)在 storage 沙盒之外,而 readExternalFile
|
|
212
204
|
// 正是文档给这一场景指定的「OS 直通读外部路径」通道——本插件也不在 biome 的 node:* 例外清单里。
|
|
213
205
|
readText: async (absPath) => {
|
|
@@ -219,17 +211,21 @@ function createService(caps) {
|
|
|
219
211
|
}
|
|
220
212
|
},
|
|
221
213
|
// 宿主不提供插件来源(打包宿主)时视同本次没有新插件;随后仍按注册表判定,声明为插件的包会报「未被加载」
|
|
222
|
-
rescanPlugins: async () => (await caps.source.
|
|
214
|
+
rescanPlugins: async () => (await (caps.source.present ? caps.source.use.rescan() : undefined)) ?? [],
|
|
223
215
|
// 判据取运行时注册表而非 rescan 返回值(理由见 PackageManagerDeps.isPluginRegistered)。
|
|
224
216
|
// plugins 服务缺席时保守返回 false——宁可让「声明为插件却没加载」的诊断多报一次,
|
|
225
217
|
// 也不要在真没装上时谎报成功。
|
|
226
|
-
isPluginRegistered: name => caps.plugins.
|
|
227
|
-
pluginStatus: () => caps.plugins.
|
|
218
|
+
isPluginRegistered: name => caps.plugins.present ? caps.plugins.use.getStatus().some(p => p.name === name) : false,
|
|
219
|
+
pluginStatus: () => (caps.plugins.present ? caps.plugins.use.getStatus() : []),
|
|
220
|
+
availableServices: () => caps.services.use.names(),
|
|
228
221
|
idle: async () => {
|
|
229
|
-
|
|
222
|
+
if (caps.plugins.present)
|
|
223
|
+
await caps.plugins.use.idle();
|
|
230
224
|
},
|
|
231
|
-
resolveDefinitionName: pkgName => resolveInstalledDefinitionName(
|
|
232
|
-
listPluginInstanceIds: definitionName => (caps.plugins.
|
|
225
|
+
resolveDefinitionName: pkgName => resolveInstalledDefinitionName(caps.paths.use.app, pkgName),
|
|
226
|
+
listPluginInstanceIds: definitionName => (caps.plugins.present ? caps.plugins.use.getStatus() : [])
|
|
227
|
+
.filter(p => p.name === definitionName)
|
|
228
|
+
.map(p => p.instanceId),
|
|
233
229
|
// 重启并交付回滚凭据。core 只透传 rollback(不解释形状),由 runtime 的重启策略
|
|
234
230
|
// 在「新实例 ready 前夭折」时消费——触发者与执行者同为父进程,全程内存不落盘。
|
|
235
231
|
//
|
|
@@ -250,23 +246,29 @@ function createService(caps) {
|
|
|
250
246
|
// 而这三个都没有,故也不需要为此加解析。
|
|
251
247
|
recoveryChannelProviders: () => [
|
|
252
248
|
...new Set(['webui-client', 'webui-server', packageManager.name].flatMap(service => {
|
|
253
|
-
const id = caps.services.inspect(service)[0]?.contextId;
|
|
249
|
+
const id = caps.services.use.inspect(service)[0]?.contextId;
|
|
254
250
|
return id ? [id] : [];
|
|
255
251
|
})),
|
|
256
252
|
],
|
|
257
253
|
unloadPlugin: async (name) => {
|
|
258
|
-
|
|
254
|
+
if (caps.plugins.present)
|
|
255
|
+
await caps.plugins.use.unload(name);
|
|
259
256
|
},
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const doc = caps.hostConfig.current;
|
|
264
|
-
if (!doc)
|
|
257
|
+
// 文档没有改动时不保存:没东西要写,也就不该因配置文件的状况报失败
|
|
258
|
+
cleanupConfig: async (instanceIds, removeConfig) => {
|
|
259
|
+
if (!caps.hostConfig.present)
|
|
265
260
|
return;
|
|
266
|
-
doc.
|
|
267
|
-
doc.
|
|
268
|
-
|
|
269
|
-
|
|
261
|
+
const doc = caps.hostConfig.use;
|
|
262
|
+
const plugins = doc.getAll().plugins;
|
|
263
|
+
const touched = instanceIds.filter(id => doc.isPluginDisabled(id) || (removeConfig && Object.hasOwn(plugins, id)));
|
|
264
|
+
if (touched.length === 0)
|
|
265
|
+
return;
|
|
266
|
+
for (const id of touched) {
|
|
267
|
+
doc.setPluginEnabled(id, true);
|
|
268
|
+
if (removeConfig)
|
|
269
|
+
doc.removePluginConfig(id);
|
|
270
|
+
}
|
|
271
|
+
await doc.save();
|
|
270
272
|
},
|
|
271
273
|
});
|
|
272
274
|
}
|
|
@@ -344,20 +346,20 @@ const SPEC_ARGV_BUDGET = 128 * 1024;
|
|
|
344
346
|
*/
|
|
345
347
|
export function buildUpdateSpecs(targets) {
|
|
346
348
|
if (!Array.isArray(targets) || targets.length === 0)
|
|
347
|
-
return { error: '
|
|
349
|
+
return { error: t('noTargets') };
|
|
348
350
|
const specs = [];
|
|
349
351
|
const seen = new Set();
|
|
350
352
|
let bytes = 0;
|
|
351
|
-
for (const
|
|
352
|
-
const name = typeof
|
|
353
|
-
const version = typeof
|
|
353
|
+
for (const target of targets) {
|
|
354
|
+
const name = typeof target?.name === 'string' ? target.name : '';
|
|
355
|
+
const version = typeof target?.version === 'string' ? target.version : '';
|
|
354
356
|
if (!PKG_NAME_ONLY_RE.test(name))
|
|
355
|
-
return { error:
|
|
357
|
+
return { error: t('badName', { name: JSON.stringify(name) }) };
|
|
356
358
|
if (!VERSION_RE.test(version))
|
|
357
|
-
return { error:
|
|
359
|
+
return { error: t('badVersion', { name, version: JSON.stringify(version) }) };
|
|
358
360
|
// 同名多版本会让 npm 取最后一个,静默丢弃前面的选择——宁可让用户重选。
|
|
359
361
|
if (seen.has(name))
|
|
360
|
-
return { error:
|
|
362
|
+
return { error: t('duplicateTarget', { name }) };
|
|
361
363
|
seen.add(name);
|
|
362
364
|
const spec = `${name}@${version}`;
|
|
363
365
|
// 按**字节**计而非按个数:这些 spec 逐条成为 argv,而 execve 的约束是参数块的总字节数,
|
|
@@ -365,7 +367,7 @@ export function buildUpdateSpecs(targets) {
|
|
|
365
367
|
// 超限即整批停下,与本函数其余校验同一策略:不猜、不截断。
|
|
366
368
|
bytes += Buffer.byteLength(spec) + 1; // +1 = argv 之间的 NUL 分隔
|
|
367
369
|
if (bytes > SPEC_ARGV_BUDGET) {
|
|
368
|
-
return { error:
|
|
370
|
+
return { error: t('tooManyTargets', { budget: SPEC_ARGV_BUDGET }) };
|
|
369
371
|
}
|
|
370
372
|
specs.push(spec);
|
|
371
373
|
}
|
|
@@ -380,6 +382,79 @@ export function extractPeerConflicts(output) {
|
|
|
380
382
|
const picked = lines.filter(l => /ERESOLVE|peer |Conflicting peer|Found:|Could not resolve|from the root project/i.test(l));
|
|
381
383
|
return picked.map(l => l.replace(/^npm (ERR!|error|warn)\s*/i, '').trim()).filter(l => l.length > 0);
|
|
382
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* npm 失败的几类与给用户看的一句(原因与修法)。浏览器下载与原生编译先于网络认:它们的输出里也可能带网络错误码,但修法
|
|
387
|
+
* 不同
|
|
388
|
+
*/
|
|
389
|
+
const NPM_FAILURES = [
|
|
390
|
+
{ kind: 'disk', match: /\bENOSPC\b/, describe: () => t('npmDisk') },
|
|
391
|
+
{ kind: 'permission', match: /\bEACCES\b/, describe: () => t('npmPermission') },
|
|
392
|
+
{
|
|
393
|
+
kind: 'platform',
|
|
394
|
+
match: /\bEBADPLATFORM\b/,
|
|
395
|
+
describe: output => {
|
|
396
|
+
const pkg = /Unsupported platform for (\S+?)@\d/.exec(output)?.[1];
|
|
397
|
+
return pkg === undefined ? t('npmPlatformUnknown') : t('npmPlatform', { pkg });
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
kind: 'peer',
|
|
402
|
+
match: /\bERESOLVE\b/,
|
|
403
|
+
describe: output => t('npmPeer', { conflicts: textClauses(extractPeerConflicts(output)) }),
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
kind: 'browser',
|
|
407
|
+
match: /Failed to set up (?:chrome|chrome-headless-shell|firefox)\b|node_modules[\\/]puppeteer\s*$/m,
|
|
408
|
+
describe: () => t('npmBrowser'),
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
kind: 'native',
|
|
412
|
+
match: /gyp ERR!|node-gyp|prebuild-install/,
|
|
413
|
+
describe: output => {
|
|
414
|
+
const pkg = /^npm (?:error|ERR!) path \S*node_modules[\\/]((?:@[^\\/\s]+[\\/])?[^\\/\s]+)\s*$/m.exec(output)?.[1];
|
|
415
|
+
return pkg === undefined ? t('npmNativeUnknown') : t('npmNative', { pkg });
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
kind: 'missing',
|
|
420
|
+
match: /\b(E404|ETARGET)\b/,
|
|
421
|
+
// 镜像源同步有延迟,刚发布的版本可能还没有
|
|
422
|
+
describe: (_output, [, code]) => t('npmMissing', { code }),
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
kind: 'network',
|
|
426
|
+
match: /\b(ENOTFOUND|ETIMEDOUT|ECONNRESET|ECONNREFUSED|EAI_AGAIN)\b/,
|
|
427
|
+
describe: (_output, [, code]) => t('npmNetwork', { code }),
|
|
428
|
+
},
|
|
429
|
+
];
|
|
430
|
+
/** 按先后认 npm 的失败输出:第一个对上的类别与匹配结果 */
|
|
431
|
+
function recognizeNpmFailure(output) {
|
|
432
|
+
for (const failure of NPM_FAILURES) {
|
|
433
|
+
const found = failure.match.exec(output);
|
|
434
|
+
if (found)
|
|
435
|
+
return { failure, found };
|
|
436
|
+
}
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
/** npm 的失败输出认出是哪一类,认不出时为 undefined。纯函数,便于单测 */
|
|
440
|
+
export function npmFailureKind(output) {
|
|
441
|
+
return recognizeNpmFailure(output)?.failure.kind;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* 把 npm 的失败输出归成一句给用户看的话:认得出的写原因与修法,认不出的写 npm 的最后一条报错,并指向日志与排障页。
|
|
445
|
+
* 整段输出由调用方记进日志,回执与提示里只放这一句。纯函数,便于单测。
|
|
446
|
+
*/
|
|
447
|
+
export function describeNpmFailure(output) {
|
|
448
|
+
const known = recognizeNpmFailure(output);
|
|
449
|
+
if (known)
|
|
450
|
+
return known.failure.describe(output, known.found);
|
|
451
|
+
const errors = output
|
|
452
|
+
.split('\n')
|
|
453
|
+
.map(line => /^npm (?:error|ERR!)\s*(.*)$/.exec(line.trim())?.[1]?.trim())
|
|
454
|
+
.filter((line) => !!line && !line.startsWith('A complete log of this run'));
|
|
455
|
+
const last = errors.at(-1) ?? output.trim().split('\n').at(-1)?.trim();
|
|
456
|
+
return t('npmOther', { error: String(last) });
|
|
457
|
+
}
|
|
383
458
|
/**
|
|
384
459
|
* 从 **成功**(exit 0)的 dry-run 输出里找出因本次目标而无法满足的 peer 依赖。
|
|
385
460
|
*
|
|
@@ -395,7 +470,7 @@ export function extractPeerConflicts(output) {
|
|
|
395
470
|
export function findUnmetPeers(output, targetNames) {
|
|
396
471
|
const names = new Set(targetNames);
|
|
397
472
|
// npm 会把同一条 peer 冲突在不同上下文里重复打印,去重后再给用户。
|
|
398
|
-
const out = new
|
|
473
|
+
const out = new Map();
|
|
399
474
|
for (const raw of output.split('\n')) {
|
|
400
475
|
const line = raw.replace(/^npm (ERR!|error|warn)\s*/i, '').trim();
|
|
401
476
|
const m = /^peer\s+(\S+?)@"([^"]+)"\s+from\s+(.+)$/.exec(line);
|
|
@@ -403,13 +478,13 @@ export function findUnmetPeers(output, targetNames) {
|
|
|
403
478
|
continue;
|
|
404
479
|
if (!names.has(m[1]))
|
|
405
480
|
continue;
|
|
406
|
-
out.
|
|
481
|
+
out.set(`${m[3]}\0${m[1]}@${m[2]}`, t('needsPeer', { dependent: m[3], peer: `${m[1]}@${m[2]}` }));
|
|
407
482
|
}
|
|
408
|
-
return [...out];
|
|
483
|
+
return [...out.values()];
|
|
409
484
|
}
|
|
410
485
|
/**
|
|
411
|
-
* 包管理核心:install/uninstall
|
|
412
|
-
* 所有文件操作走 process
|
|
486
|
+
* 包管理核心:install/uninstall 的纯依赖实现(不碰服务选择,可单测)。
|
|
487
|
+
* 所有文件操作走 process 服务(子进程:npm/cp/test),目标是项目根 package.json 与
|
|
413
488
|
* node_modules——不经 storage 沙盒。
|
|
414
489
|
* 能力组装层见 createService。
|
|
415
490
|
*/
|
|
@@ -443,7 +518,7 @@ export function createPackageManager(deps) {
|
|
|
443
518
|
*/
|
|
444
519
|
async function exclusive(what, run) {
|
|
445
520
|
if (inflight) {
|
|
446
|
-
return { ok: false, message:
|
|
521
|
+
return { ok: false, message: t('busy', { operation: inflight }) };
|
|
447
522
|
}
|
|
448
523
|
inflight = what;
|
|
449
524
|
try {
|
|
@@ -454,6 +529,17 @@ export function createPackageManager(deps) {
|
|
|
454
529
|
inflight = null;
|
|
455
530
|
}
|
|
456
531
|
}
|
|
532
|
+
/** 跑一条 npm 命令:失败时整段输出记进日志,抛出的错误只带 {@link describeNpmFailure} 认出的原因与修法 */
|
|
533
|
+
async function npm(args, cwd, timeout, extraEnv) {
|
|
534
|
+
try {
|
|
535
|
+
return (await execProcBoth(proc, 'npm', args, cwd, timeout, extraEnv)).stdout;
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
const output = err instanceof Error ? err.message : String(err);
|
|
539
|
+
log.error(`npm ${args.join(' ')} 失败: ${output}`);
|
|
540
|
+
throw new TextError(describeNpmFailure(output));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
457
543
|
/** 安装源查询的缓存:只留成功的结果,失败时清掉,下次调用重查 */
|
|
458
544
|
let registryUrl;
|
|
459
545
|
function registry() {
|
|
@@ -475,7 +561,7 @@ export function createPackageManager(deps) {
|
|
|
475
561
|
/** 路径存在性:`test -d|-f <abs>`(不存在 → exit 1 → 抛 → false)。绝对路径,cwd 无关。 */
|
|
476
562
|
async function pathExists(absPath, kind) {
|
|
477
563
|
try {
|
|
478
|
-
await proc.execFile('test', [`-${kind}`, absPath], { cwd:
|
|
564
|
+
await proc.execFile('test', [`-${kind}`, absPath], { cwd: deps.projectRoot(), timeout: 10_000 });
|
|
479
565
|
return true;
|
|
480
566
|
}
|
|
481
567
|
catch {
|
|
@@ -515,17 +601,14 @@ export function createPackageManager(deps) {
|
|
|
515
601
|
* HTTP 路由——本服务对外公开,任何插件都能绕过路由直接调。返回拒绝理由,`undefined` = 放行。
|
|
516
602
|
*/
|
|
517
603
|
async function installKindRefusal(npmPkg, root) {
|
|
518
|
-
const out = (await
|
|
604
|
+
const out = (await npm(['view', npmPkg, 'keywords', '--json'], root)).trim();
|
|
519
605
|
// 包没有 keywords 字段时 npm 什么都不输出;spec 是版本范围时按版本逐个给出、返回二维数组
|
|
520
606
|
const parsed = out ? JSON.parse(out) : [];
|
|
521
607
|
const perVersion = Array.isArray(parsed) && parsed.length > 0 && parsed.every(Array.isArray) ? parsed : [parsed];
|
|
522
608
|
const kind = perVersion.map(kw => nonMarketKind(kw)).find(k => k !== undefined);
|
|
523
609
|
if (!kind)
|
|
524
610
|
return undefined;
|
|
525
|
-
return (
|
|
526
|
-
(kind === '非插件包'
|
|
527
|
-
? '市场只安装带 aalis-plugin / aalis-interface 关键词的包。'
|
|
528
|
-
: '它随脚手架或依赖它的插件自动就位;要升级已装的版本,请在市场勾选它后用「更新所选」提交。'));
|
|
611
|
+
return t(kind === 'other' ? 'installUnmarked' : 'installBundled', { name: npmPkg, kind: KIND_NAMES[kind] });
|
|
529
612
|
}
|
|
530
613
|
/**
|
|
531
614
|
* 装完的统一判定:rescan 出新插件即成功;没出新插件时按目标是否**声明自己是插件**分流。
|
|
@@ -541,21 +624,21 @@ export function createPackageManager(deps) {
|
|
|
541
624
|
if (deps.isPluginRegistered(defName)) {
|
|
542
625
|
await deps.idle?.();
|
|
543
626
|
const main = deps.pluginStatus?.().find(p => p.instanceId === defName);
|
|
627
|
+
const settled = (message) => ({ ok: true, message, plugin: defName });
|
|
544
628
|
switch (main?.state) {
|
|
545
629
|
case 'error':
|
|
546
|
-
return
|
|
630
|
+
return settled(t('installedErrored', { target, error: failureText(main) ?? t('seeLogs') }));
|
|
547
631
|
case 'activating':
|
|
548
|
-
return
|
|
632
|
+
return settled(t('installedActivating', { target }));
|
|
549
633
|
case 'pending':
|
|
550
|
-
return
|
|
634
|
+
return settled(t('installedPending', { target }));
|
|
551
635
|
}
|
|
552
|
-
return
|
|
636
|
+
return settled(t('installedLoaded', { target }));
|
|
553
637
|
}
|
|
554
638
|
const meta = await readJson(installedPkgJsonPath);
|
|
555
|
-
if (declaresPlugin(meta))
|
|
556
|
-
return { ok: false, message:
|
|
557
|
-
}
|
|
558
|
-
return { ok: true, message: `已安装 ${target}(非插件包,不进入插件列表)` };
|
|
639
|
+
if (declaresPlugin(meta))
|
|
640
|
+
return { ok: false, message: t('installedNotLoaded', { where }) };
|
|
641
|
+
return { ok: true, message: t('installedNonPlugin', { target }) };
|
|
559
642
|
}
|
|
560
643
|
/**
|
|
561
644
|
* 安装:写根 `dependencies` —— 加载器**唯一**的发现来源。
|
|
@@ -563,7 +646,7 @@ export function createPackageManager(deps) {
|
|
|
563
646
|
* 不按「项目形态」分叉:检测到 `pnpm-workspace.yaml` 就把 npm 包解包成
|
|
564
647
|
* `packages/<dir>` 源码是错的——那个文件与真正生效的加载器**没有因果关系**,加载器是入口
|
|
565
648
|
* 文件传给 `startAalis` 的(默认读根依赖,本仓显式传 fs 加载器扫 packages/)。于是一个
|
|
566
|
-
* 用 pnpm
|
|
649
|
+
* 用 pnpm 工作区、却不给 `startAalis` 传插件加载器(走默认的 node_modules 加载器)的用户,包会被装进加载器永远不看的地方,静默失败。
|
|
567
650
|
*
|
|
568
651
|
* 只有一条路径。真正的 monorepo 由下面的 `hasWorkspaceProtocol` 守卫拦住并说清
|
|
569
652
|
* 该用什么工具——那才是开发者场景,不该用市场往自己仓里塞源码。
|
|
@@ -572,10 +655,7 @@ export function createPackageManager(deps) {
|
|
|
572
655
|
const root = deps.projectRoot();
|
|
573
656
|
const rootPkg = await readJson(`${root}/package.json`);
|
|
574
657
|
if (hasWorkspaceProtocol(rootPkg)) {
|
|
575
|
-
return {
|
|
576
|
-
ok: false,
|
|
577
|
-
message: '根依赖含 workspace: 协议,拒绝在此运行 npm install(会写出 package-lock.json 并搅坏 pnpm 工作区)',
|
|
578
|
-
};
|
|
658
|
+
return { ok: false, message: t('workspaceInstall') };
|
|
579
659
|
}
|
|
580
660
|
const refused = await installKindRefusal(npmPkg, root);
|
|
581
661
|
if (refused)
|
|
@@ -583,22 +663,13 @@ export function createPackageManager(deps) {
|
|
|
583
663
|
log.info(`正在安装: ${npmPkg} → 根依赖 + node_modules`);
|
|
584
664
|
// **刻意不加 `--ignore-scripts`**:依赖树的 preinstall/install/postinstall 会照常执行。
|
|
585
665
|
// 三条理由:脚手架自身就是裸 `npm install`(其生成的 README 也教用户裸装),加了会让
|
|
586
|
-
// 「同一个包、同一个用户、两条途径」行为分叉;会打穿 plugin-
|
|
587
|
-
// (better-sqlite3,脚手架默认勾选)与 plugin-tool-browser(puppeteer);而真正的增量
|
|
666
|
+
// 「同一个包、同一个用户、两条途径」行为分叉;会打穿 plugin-tool-browser(puppeteer);而真正的增量
|
|
588
667
|
// 风险只有传递依赖的 install 脚本这一片——入口是 owner 级、包名由用户自己点选、包本体
|
|
589
668
|
// 反正会被动态 import 执行(插件与内核同进程同权限,见 docs/concepts/security-model.md §1)。
|
|
590
669
|
//
|
|
591
670
|
// peer 护栏与更新预检相同(STRICT_PEERS_ENV)。代价:本就靠 legacy-peer-deps 容忍既有
|
|
592
|
-
// peer
|
|
593
|
-
|
|
594
|
-
await execProcBoth(proc, 'npm', ['install', npmPkg, '--no-audit', '--no-fund'], root, INSTALL_TIMEOUT_MS, STRICT_PEERS_ENV);
|
|
595
|
-
}
|
|
596
|
-
catch (err) {
|
|
597
|
-
const raw = err instanceof Error ? err.message : String(err);
|
|
598
|
-
if (!raw.includes('ERESOLVE'))
|
|
599
|
-
throw err;
|
|
600
|
-
throw new Error(`peer 依赖冲突,未安装(市场安装不沿用 .npmrc 里的 legacy-peer-deps):${extractPeerConflicts(raw).join(';')}`);
|
|
601
|
-
}
|
|
671
|
+
// peer 冲突的项目,市场安装会整体失败,故失败文案要点明原因(见 describeNpmFailure)。
|
|
672
|
+
await npm(['install', npmPkg, '--no-audit', '--no-fund'], root, INSTALL_TIMEOUT_MS, STRICT_PEERS_ENV);
|
|
602
673
|
const bare = stripVersion(npmPkg);
|
|
603
674
|
return settleInstall(npmPkg, `${root}/node_modules/${bare}/package.json`, 'node_modules');
|
|
604
675
|
}
|
|
@@ -629,23 +700,29 @@ export function createPackageManager(deps) {
|
|
|
629
700
|
async function preflightInSandbox(specs, root) {
|
|
630
701
|
const tmp = await proc.makeTempDir('preflight');
|
|
631
702
|
try {
|
|
632
|
-
// 直接 cp 现有文件,不重新写内容——与本插件其余文件操作同一路子(走
|
|
703
|
+
// 直接 cp 现有文件,不重新写内容——与本插件其余文件操作同一路子(走 process 服务的
|
|
633
704
|
// POSIX 命令),也省掉一个只为此处存在的写原语。
|
|
634
705
|
await execProc(proc, 'cp', [`${root}/package.json`, `${tmp.path}/package.json`], tmp.path);
|
|
635
706
|
const lock = `${root}/package-lock.json`;
|
|
636
707
|
if (await pathExists(lock, 'f')) {
|
|
637
708
|
await execProc(proc, 'cp', [lock, `${tmp.path}/package-lock.json`], tmp.path);
|
|
638
709
|
}
|
|
639
|
-
|
|
640
|
-
|
|
710
|
+
let output;
|
|
711
|
+
try {
|
|
712
|
+
output = await execProcBoth(proc, 'npm', ['install', ...specs, '--dry-run', '--no-audit', '--no-fund'], tmp.path, INSTALL_TIMEOUT_MS, STRICT_PEERS_ENV);
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
// 非零退出:npm 判定 peer 无解(ERESOLVE 等)时报冲突要点;别的失败(连不上源等)不是冲突,抛出原因与修法
|
|
716
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
717
|
+
const conflicts = extractPeerConflicts(raw);
|
|
718
|
+
if (conflicts.length > 0)
|
|
719
|
+
return conflicts;
|
|
720
|
+
log.error(`更新预检失败: ${raw}`);
|
|
721
|
+
throw new TextError(describeNpmFailure(raw));
|
|
722
|
+
}
|
|
723
|
+
const unmet = findUnmetPeers(`${output.stderr}\n${output.stdout}`, specs.map(stripVersion));
|
|
641
724
|
return unmet.length > 0 ? unmet : undefined;
|
|
642
725
|
}
|
|
643
|
-
catch (err) {
|
|
644
|
-
// 非零退出 = npm 自己判定无解(ERESOLVE 等)。原样上报,不猜。
|
|
645
|
-
const raw = err instanceof Error ? err.message : String(err);
|
|
646
|
-
const conflicts = extractPeerConflicts(raw);
|
|
647
|
-
return conflicts.length > 0 ? conflicts : [raw.slice(0, 2000)];
|
|
648
|
-
}
|
|
649
726
|
finally {
|
|
650
727
|
await tmp.cleanup().catch(() => { });
|
|
651
728
|
}
|
|
@@ -655,11 +732,12 @@ export function createPackageManager(deps) {
|
|
|
655
732
|
* 重启次数恒为 1,与改了多少个包无关(语义见 {@link PackageManagerService.update})。
|
|
656
733
|
*/
|
|
657
734
|
async function updateAll(targets) {
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
return { ok: false, message: error
|
|
735
|
+
const built = buildUpdateSpecs(targets);
|
|
736
|
+
if (built.error)
|
|
737
|
+
return { ok: false, message: built.error };
|
|
738
|
+
const { specs } = built;
|
|
661
739
|
if (!deps.restartApp)
|
|
662
|
-
return { ok: false, message: '
|
|
740
|
+
return { ok: false, message: t('noRestart') };
|
|
663
741
|
const root = deps.projectRoot();
|
|
664
742
|
// 没有「项目形态」这一层判断了:能不能更新是**每个包自己的来源**说了算(下面的
|
|
665
743
|
// isRegistryDep 闸),不是整个项目的属性。旧写法拿 `pnpm-workspace.yaml` 是否存在
|
|
@@ -667,34 +745,29 @@ export function createPackageManager(deps) {
|
|
|
667
745
|
const rootPkgPath = `${root}/package.json`;
|
|
668
746
|
const rootPkgText = await deps.readText(rootPkgPath);
|
|
669
747
|
if (rootPkgText === undefined)
|
|
670
|
-
return { ok: false, message:
|
|
748
|
+
return { ok: false, message: t('noRootPackage', { path: rootPkgPath }) };
|
|
671
749
|
const rootPkg = await readJson(rootPkgPath);
|
|
672
|
-
if (hasWorkspaceProtocol(rootPkg))
|
|
673
|
-
return { ok: false, message: '
|
|
674
|
-
}
|
|
750
|
+
if (hasWorkspaceProtocol(rootPkg))
|
|
751
|
+
return { ok: false, message: t('workspaceUpdate') };
|
|
675
752
|
// 只允许更新「根依赖里以 semver 范围声明」的包。闸放在服务层而非 HTTP 路由——
|
|
676
753
|
// 本服务对外公开,任何插件都能绕过路由直接调用(理由见 isRegistryDep)。
|
|
677
754
|
const rootDependencies = (rootPkg?.dependencies ?? {});
|
|
678
|
-
const notUpdatable = targets.filter(
|
|
755
|
+
const notUpdatable = targets.filter(target => !isRegistryDep(rootDependencies[target.name]));
|
|
679
756
|
if (notUpdatable.length > 0) {
|
|
680
|
-
return {
|
|
681
|
-
ok: false,
|
|
682
|
-
message: '以下包不在根依赖中(由其它插件带入)或不是 registry 来源,单独更新只会装出第二份副本;' +
|
|
683
|
-
`请改为更新带入它们的插件:${notUpdatable.map(t => t.name).join('、')}`,
|
|
684
|
-
};
|
|
757
|
+
return { ok: false, message: t('notUpdatable', { names: notUpdatable.map(target => target.name) }) };
|
|
685
758
|
}
|
|
686
759
|
// ── 读取每个目标的当前实装版本 ──
|
|
687
760
|
// 两处都要用:(a) 降级守卫;(b) 回滚凭据钉精确旧版。两者都不能靠根 package.json 的
|
|
688
761
|
// 声明——那是**范围**,回滚时按范围重解析只会又解到刚崩掉的新版。
|
|
689
762
|
const installedVersions = new Map();
|
|
690
|
-
for (const
|
|
691
|
-
const text = await deps.readText(`${root}/node_modules/${
|
|
763
|
+
for (const target of targets) {
|
|
764
|
+
const text = await deps.readText(`${root}/node_modules/${target.name}/package.json`);
|
|
692
765
|
if (text === undefined)
|
|
693
766
|
continue;
|
|
694
767
|
try {
|
|
695
768
|
const v = JSON.parse(text).version;
|
|
696
769
|
if (typeof v === 'string')
|
|
697
|
-
installedVersions.set(
|
|
770
|
+
installedVersions.set(target.name, v);
|
|
698
771
|
}
|
|
699
772
|
catch {
|
|
700
773
|
/* 坏 JSON:当作读不到,由下面的守卫拒掉 */
|
|
@@ -705,25 +778,26 @@ export function createPackageManager(deps) {
|
|
|
705
778
|
// 拒绝而非放行的理由:registry 的 dist-tags.latest 可以**低于**本地已装版本(发布事故后
|
|
706
779
|
// `npm dist-tag add pkg@旧版 latest` 回滚,或用户曾装过预发布版),此时「更新」会静默降级;
|
|
707
780
|
// 读不到已装版本同样拒——无从判断方向就不动,用户可显式卸载重装。
|
|
708
|
-
const notUpgrade = targets.filter(
|
|
781
|
+
const notUpgrade = targets.filter(target => !isUpgrade(installedVersions.get(target.name), target.version));
|
|
709
782
|
if (notUpgrade.length > 0) {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
message: '以下目标版本不高于本地已装版本(或本地版本号读不到),更新会变成降级,已拒绝:' +
|
|
713
|
-
notUpgrade
|
|
714
|
-
.map(t => `${t.name}(本地 ${installedVersions.get(t.name) ?? '未知'} → 目标 ${t.version})`)
|
|
715
|
-
.join('、'),
|
|
716
|
-
};
|
|
783
|
+
const items = notUpgrade.map(({ name, version }) => t('downgradeItem', { name, installed: installedVersions.get(name) ?? t('unknown'), version }));
|
|
784
|
+
return { ok: false, message: t('notUpgrade', { items }) };
|
|
717
785
|
}
|
|
718
786
|
// ── 预检:整组一次,不逐个 ──
|
|
719
787
|
// 逐个预检发现不了「A@new 要 core>=0.10、B@new 要 core<0.10」这类只在合并时冲突的组合。
|
|
720
788
|
log.info(`更新预检: ${specs.join(' ')}`);
|
|
721
789
|
const reject = (conflicts) => ({
|
|
722
790
|
ok: false,
|
|
723
|
-
message: '
|
|
791
|
+
message: t('preflightRejected'),
|
|
724
792
|
conflicts,
|
|
725
793
|
});
|
|
726
|
-
|
|
794
|
+
let verdict;
|
|
795
|
+
try {
|
|
796
|
+
verdict = await preflightInSandbox(specs, root);
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
return { ok: false, message: t('preflightFailed', { error: textOf(err) }) };
|
|
800
|
+
}
|
|
727
801
|
if (verdict)
|
|
728
802
|
return reject(verdict);
|
|
729
803
|
// ── 快照:预检过了才有必要 ──
|
|
@@ -756,12 +830,10 @@ export function createPackageManager(deps) {
|
|
|
756
830
|
// ── 提交 ──
|
|
757
831
|
// 预检在副本里做,live tree 此刻**未被任何 dry-run 触碰**,可以直接装。
|
|
758
832
|
try {
|
|
759
|
-
await
|
|
833
|
+
await npm(['install', ...specs, '--no-audit', '--no-fund'], root, INSTALL_TIMEOUT_MS);
|
|
760
834
|
}
|
|
761
835
|
catch (err) {
|
|
762
|
-
|
|
763
|
-
log.error(`更新安装失败: ${message}`);
|
|
764
|
-
return { ok: false, message: `安装失败(工程未重启,可重试): ${message}` };
|
|
836
|
+
return { ok: false, message: t('installFailedRetry', { error: textOf(err) }) };
|
|
765
837
|
}
|
|
766
838
|
// ── 重启接管 ──
|
|
767
839
|
// 回滚凭据交给重启策略:只有它能观察到「新实例 ready 前夭折」。插件级失败不在此列
|
|
@@ -772,19 +844,19 @@ export function createPackageManager(deps) {
|
|
|
772
844
|
// postRestore 钉**精确旧版**,不用裸 `npm install`。
|
|
773
845
|
// 裸 install 只能按还原后的 package.json 重解析,而声明通常是 caret 范围(`^1.0.0`),
|
|
774
846
|
// 刚崩掉的新版(1.1.0)本身就满足它——于是 npm 又把它装回来,node_modules 分毫未变,
|
|
775
|
-
//
|
|
847
|
+
// 而重启策略照样报告已回滚并拉起旧版本。实测:有锁时正常,无锁时静默失效。
|
|
776
848
|
// `--no-save` 保证 package.json 维持 restore 写回的原样,不被 npm 改写成 `^旧版`。
|
|
777
849
|
// 降级守卫已保证每个 target 都有已装版本(读不到的在那里就被拒了),此处的 undefined
|
|
778
850
|
// 过滤只是让类型收窄,不承担逻辑。
|
|
779
851
|
const pinned = targets
|
|
780
|
-
.map(
|
|
781
|
-
const v = installedVersions.get(
|
|
782
|
-
return v === undefined ? undefined : `${
|
|
852
|
+
.map(target => {
|
|
853
|
+
const v = installedVersions.get(target.name);
|
|
854
|
+
return v === undefined ? undefined : `${target.name}@${v}`;
|
|
783
855
|
})
|
|
784
856
|
.filter((s) => s !== undefined);
|
|
785
857
|
// 再把「被这次安装顺带改了版本」的包一并钉回旧版(理由见上面 treeBefore 处的注释)。
|
|
786
858
|
// 只回退**本来就在**的包:新增的包钉不回去(它更新前不存在),留给 npm 的 extraneous。
|
|
787
|
-
const named = new Set(targets.map(
|
|
859
|
+
const named = new Set(targets.map(target => target.name));
|
|
788
860
|
const treeAfter = await readInstalledTree(deps, root);
|
|
789
861
|
for (const [name, before] of treeBefore) {
|
|
790
862
|
if (named.has(name))
|
|
@@ -798,11 +870,11 @@ export function createPackageManager(deps) {
|
|
|
798
870
|
restore,
|
|
799
871
|
postRestore: { cmd: 'npm', args: ['install', ...pinned, '--no-save', '--no-audit', '--no-fund'], cwd: root },
|
|
800
872
|
});
|
|
801
|
-
return { ok: true, restarting: true, message:
|
|
873
|
+
return { ok: true, restarting: true, message: t('updated', { specs }) };
|
|
802
874
|
}
|
|
803
875
|
return {
|
|
804
|
-
update:
|
|
805
|
-
install: npmPkg => exclusive(
|
|
876
|
+
update: targets => exclusive(t('opUpdate', { names: targets.map(x => x.name) }), () => updateAll(targets)),
|
|
877
|
+
install: npmPkg => exclusive(t('opInstall', { name: npmPkg }), async () => {
|
|
806
878
|
const bad = validatePackageSpec(npmPkg);
|
|
807
879
|
if (bad)
|
|
808
880
|
return { ok: false, message: bad };
|
|
@@ -810,13 +882,13 @@ export function createPackageManager(deps) {
|
|
|
810
882
|
return await installTo(npmPkg);
|
|
811
883
|
}
|
|
812
884
|
catch (err) {
|
|
813
|
-
const message =
|
|
814
|
-
log.error(`安装 "${npmPkg}" 失败: ${message}`);
|
|
885
|
+
const message = textOf(err);
|
|
886
|
+
log.error(`安装 "${npmPkg}" 失败: ${toSource(message)}`);
|
|
815
887
|
return { ok: false, message };
|
|
816
888
|
}
|
|
817
889
|
}),
|
|
818
|
-
uninstall: pluginName => exclusive(
|
|
819
|
-
serviceDependents: name => findServiceDependents(name, deps.pluginStatus?.() ?? []),
|
|
890
|
+
uninstall: (pluginName, options) => exclusive(t('opUninstall', { name: pluginName }), () => uninstallOne(pluginName, options)),
|
|
891
|
+
serviceDependents: name => findServiceDependents(name, deps.pluginStatus?.() ?? [], deps.availableServices?.() ?? []),
|
|
820
892
|
registry,
|
|
821
893
|
};
|
|
822
894
|
/**
|
|
@@ -826,14 +898,14 @@ export function createPackageManager(deps) {
|
|
|
826
898
|
* (与 `buildUpdateSpecs`、`isRegistryDep` 同一理由)。前端早就有正确策略
|
|
827
899
|
* (只给 plugin/interface 卡片渲染卸载按钮),但那只是客户端校验,服务端不设防。
|
|
828
900
|
*/
|
|
829
|
-
async function uninstallOne(pluginName) {
|
|
901
|
+
async function uninstallOne(pluginName, { removeConfig = false } = {}) {
|
|
830
902
|
const bad = validatePackageSpec(pluginName);
|
|
831
903
|
if (bad)
|
|
832
904
|
return { ok: false, message: bad };
|
|
833
905
|
const root = deps.projectRoot();
|
|
834
906
|
const meta = await readJson(`${root}/node_modules/${pluginName}/package.json`);
|
|
835
907
|
if (!meta) {
|
|
836
|
-
return { ok: false, message:
|
|
908
|
+
return { ok: false, message: t('noPackageJson', { name: pluginName }) };
|
|
837
909
|
}
|
|
838
910
|
// ── 闸一:类型 ──
|
|
839
911
|
// 市场管的是**插件**。core / runtime / api / schema / util 不是插件,不在本工具职权内。
|
|
@@ -846,10 +918,10 @@ export function createPackageManager(deps) {
|
|
|
846
918
|
if (kind) {
|
|
847
919
|
return {
|
|
848
920
|
ok: false,
|
|
849
|
-
message:
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
921
|
+
message: t(kind === 'core' || kind === 'runtime' ? 'uninstallVital' : 'uninstallPruned', {
|
|
922
|
+
name: pluginName,
|
|
923
|
+
kind: KIND_NAMES[kind],
|
|
924
|
+
}),
|
|
853
925
|
};
|
|
854
926
|
}
|
|
855
927
|
// ── 闸一之二:承载「撤销通道」的服务提供者不能被卸掉 ──
|
|
@@ -866,27 +938,23 @@ export function createPackageManager(deps) {
|
|
|
866
938
|
// 那时它已不是提供者,本闸自然放行。
|
|
867
939
|
const lifeline = deps.recoveryChannelProviders?.() ?? [];
|
|
868
940
|
if (lifeline.includes(pluginName)) {
|
|
869
|
-
return {
|
|
870
|
-
ok: false,
|
|
871
|
-
message: `${pluginName} 正在承载市场/管理界面本身,卸掉它这个页面会立刻不可用,` +
|
|
872
|
-
'只能在终端里恢复。若确要更换实现,请先装上替代者并在「服务」页切换过去,再回来卸载它。',
|
|
873
|
-
};
|
|
941
|
+
return { ok: false, message: t('lifeline', { name: pluginName }) };
|
|
874
942
|
}
|
|
875
943
|
// ── 闸二:来源 ──
|
|
876
944
|
// 只有根依赖里以 semver 范围声明的才归市场管。workspace: 是用户自己的源码(删了不可逆,
|
|
877
945
|
// 且多半在 git 里);file:/link: 指向本地目录;git/URL 是外部源;不在根依赖里的是传递依赖。
|
|
878
946
|
const rootPkg = await readJson(`${root}/package.json`);
|
|
879
947
|
const request = (rootPkg?.dependencies ?? {})[pluginName];
|
|
880
|
-
|
|
881
|
-
|
|
948
|
+
const origin = classifyDepSpec(request);
|
|
949
|
+
if (origin !== 'registry') {
|
|
882
950
|
const why = {
|
|
883
|
-
workspace: '
|
|
884
|
-
link: '
|
|
885
|
-
git: '
|
|
886
|
-
alias: '
|
|
887
|
-
transitive: '
|
|
951
|
+
workspace: t('originWorkspace'),
|
|
952
|
+
link: t('originLink'),
|
|
953
|
+
git: t('originGit'),
|
|
954
|
+
alias: t('originAlias'),
|
|
955
|
+
transitive: t('originTransitive'),
|
|
888
956
|
};
|
|
889
|
-
return { ok: false, message:
|
|
957
|
+
return { ok: false, message: t('notFromMarket', { name: pluginName, why: why[origin] }) };
|
|
890
958
|
}
|
|
891
959
|
// 先解析定义 name 并枚举实例:npm uninstall 会拆掉 node_modules,之后就读不到入口了。
|
|
892
960
|
const defName = await definitionNameFor(pluginName);
|
|
@@ -894,39 +962,47 @@ export function createPackageManager(deps) {
|
|
|
894
962
|
// ── 闸三:服务依赖者 ──
|
|
895
963
|
// 某服务没有别的插件正在提供时,卸掉它的提供者会打断 required 该服务的插件(已禁用的不算)。
|
|
896
964
|
// 与市场卸载前的预警同一份判定(serviceDependents),按加载器解析出的定义 name 查。
|
|
897
|
-
const dependents = findServiceDependents(defName, deps.pluginStatus?.() ?? []);
|
|
965
|
+
const dependents = findServiceDependents(defName, deps.pluginStatus?.() ?? [], deps.availableServices?.() ?? []);
|
|
898
966
|
if (dependents.length > 0) {
|
|
899
|
-
return {
|
|
900
|
-
ok: false,
|
|
901
|
-
message: `卸载会破坏依赖:${dependents.join('、')} 依赖此插件提供的服务,且没有别的插件正在提供。请先停用它们,或启用 / 安装替代提供者。`,
|
|
902
|
-
};
|
|
967
|
+
return { ok: false, message: t('breaksDependents', { dependents }) };
|
|
903
968
|
}
|
|
904
969
|
try {
|
|
905
|
-
await
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
deps.
|
|
909
|
-
|
|
910
|
-
|
|
970
|
+
await npm(['uninstall', pluginName, '--no-audit', '--no-fund'], root, INSTALL_TIMEOUT_MS);
|
|
971
|
+
// 从运行时注册表彻底移除(dispose + delete),幂等
|
|
972
|
+
for (const id of instanceIds)
|
|
973
|
+
await deps.unloadPlugin(id);
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
return { ok: false, message: textOf(err) };
|
|
977
|
+
}
|
|
978
|
+
log.info(`${pluginName}: 已从根依赖与 node_modules 移除`);
|
|
979
|
+
// 包已经卸掉了,配置文件没写成不改变这一点:回执照样 ok,写明原因与残留
|
|
980
|
+
try {
|
|
981
|
+
await deps.cleanupConfig?.(instanceIds, removeConfig);
|
|
982
|
+
}
|
|
983
|
+
catch (err) {
|
|
911
984
|
return {
|
|
912
985
|
ok: true,
|
|
913
|
-
message:
|
|
986
|
+
message: t(removeConfig ? 'uninstalledConfigUnsaved' : 'uninstalledFlagUnsaved', {
|
|
987
|
+
name: pluginName,
|
|
988
|
+
error: textOf(err),
|
|
989
|
+
}),
|
|
914
990
|
};
|
|
915
991
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
992
|
+
return {
|
|
993
|
+
ok: true,
|
|
994
|
+
message: t(removeConfig ? 'uninstalledConfigRemoved' : 'uninstalledConfigKept', { name: pluginName }),
|
|
995
|
+
};
|
|
919
996
|
}
|
|
920
997
|
}
|
|
921
998
|
export default definePlugin({
|
|
922
999
|
name: '@aalis/plugin-package-manager',
|
|
923
|
-
displayName: '
|
|
1000
|
+
displayName: t('displayName'),
|
|
924
1001
|
subsystem: 'system',
|
|
925
|
-
configSchema,
|
|
926
1002
|
provides: [packageManager],
|
|
927
1003
|
uses,
|
|
928
1004
|
apply(caps) {
|
|
929
|
-
caps.provide(packageManager, createService(caps), { label: 'package-manager' });
|
|
1005
|
+
caps.provide.use(packageManager, createService(caps), { label: 'package-manager' });
|
|
930
1006
|
},
|
|
931
1007
|
});
|
|
932
1008
|
//# sourceMappingURL=index.js.map
|