@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/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 { createProcessGateway, processService } from '@aalis/api-process';
6
- import { appService, config, definePlugin, logger, optional, pluginsService, provide, services, } from '@aalis/core';
7
- import { defineConfig, parseConfig } from '@aalis/schema-config';
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` 要装全依赖树 + 原生编译(better-sqlite3、puppeteer
14
- * 都在脚手架默认依赖里),快机快网下已达 95 秒量级,而超时行为是 SIGKILL——留足余量,
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 提供的某服务 S 没有别的插件正在提供(只认 active 与
100
- * activating;disabled / error / pending 的同类提供者不在提供),且有别的插件 requiredServices 含 S → 这些插件
101
- * 会被打断。已禁用(disabled / disposed)的依赖者不算。纯函数,便于单测。
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 target = status.find(p => p.name === targetName);
105
- const provided = target?.provides ?? [];
106
- if (provided.length === 0)
107
- return [];
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 provided) {
110
- const otherProvider = status.some(p => p.name !== targetName && (p.state === 'active' || p.state === 'activating') && (p.provides ?? []).includes(svc));
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
- (p.requiredServices ?? []).includes(svc)) {
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 `非法包名: ${JSON.stringify(spec)}`;
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 cfg = parseConfig(configSchema, caps.config, caps.logger);
186
- const log = caps.logger;
187
- const proc = createProcessGateway(caps.proc);
190
+ const log = caps.logger.use;
191
+ const proc = caps.proc.use;
188
192
  function getApp() {
189
- const app = caps.app.current;
190
- if (!app)
191
- throw new Error('app 服务不可用,无法执行包管理操作');
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
- // 项目根:默认 cwd,与 createNodeModulesPluginLoader(projectDir = process.cwd()) 的
206
- // 默认值同源——两者必须指同一处,否则「我们写的 package.json」与「加载器读的
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.current?.rescan()) ?? [],
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.current?.getStatus().some(p => p.name === name) ?? false,
227
- pluginStatus: () => caps.plugins.current?.getStatus() ?? [],
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
- await caps.plugins.current?.idle();
222
+ if (caps.plugins.present)
223
+ await caps.plugins.use.idle();
230
224
  },
231
- resolveDefinitionName: pkgName => resolveInstalledDefinitionName(projectRoot(), pkgName),
232
- listPluginInstanceIds: definitionName => (caps.plugins.current?.getStatus() ?? []).filter(p => p.name === definitionName).map(p => p.instanceId),
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
- await caps.plugins.current?.unload(name);
254
+ if (caps.plugins.present)
255
+ await caps.plugins.use.unload(name);
259
256
  },
260
- // 卸载后清残留配置:删 plugins.<name> 配置块 + 从 disabledPlugins 移除
261
- // (否则重装会被"上次禁用"标记带成已禁用状态),并持久化。宿主没提供配置文档时无可清理。
262
- cleanupConfig: name => {
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.removePluginConfig(name);
267
- doc.setPluginEnabled(name, true);
268
- // cleanupConfig 契约返 void;落盘失败只 warn,内存态已清、下次保存会带上
269
- doc.save().catch(err => log.warn(`${name}: 卸载后配置清理落盘失败:`, err));
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 t of targets) {
352
- const name = typeof t?.name === 'string' ? t.name : '';
353
- const version = typeof t?.version === 'string' ? t.version : '';
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: `非法包名: ${JSON.stringify(name)}` };
357
+ return { error: t('badName', { name: JSON.stringify(name) }) };
356
358
  if (!VERSION_RE.test(version))
357
- return { error: `非法版本号: ${name}@${JSON.stringify(version)}` };
359
+ return { error: t('badVersion', { name, version: JSON.stringify(version) }) };
358
360
  // 同名多版本会让 npm 取最后一个,静默丢弃前面的选择——宁可让用户重选。
359
361
  if (seen.has(name))
360
- return { error: `同一包被指定了多次: ${name}` };
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: `更新目标过多:参数总长已超 ${SPEC_ARGV_BUDGET} 字节,请分批提交` };
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 Set();
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.add(`${m[3]} 需要 ${m[1]}@${m[2]}`);
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 网关(子进程:npm/cp/test),目标是项目根 package.json 与
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: `有包管理操作正在进行中(${inflight}),请稍候重试` };
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: process.cwd(), timeout: 10_000 });
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 execProc(proc, 'npm', ['view', npmPkg, 'keywords', '--json'], root)).trim();
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 (`${npmPkg} 是${kind},不是可装卸的插件,市场不负责它的安装。` +
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 { ok: true, message: `已安装 ${target},但激活失败,已转为 error 态(${main.error ?? '详见日志'})` };
630
+ return settled(t('installedErrored', { target, error: failureText(main) ?? t('seeLogs') }));
547
631
  case 'activating':
548
- return { ok: true, message: `已安装 ${target},仍在激活(超过慢激活阈值,已转入后台),结果以插件列表为准` };
632
+ return settled(t('installedActivating', { target }));
549
633
  case 'pending':
550
- return { ok: true, message: `已安装 ${target},尚未激活,正在等待 required 依赖满足` };
634
+ return settled(t('installedPending', { target }));
551
635
  }
552
- return { ok: true, message: `已安装并加载: ${target}` };
636
+ return settled(t('installedLoaded', { target }));
553
637
  }
554
638
  const meta = await readJson(installedPkgJsonPath);
555
- if (declaresPlugin(meta)) {
556
- return { ok: false, message: `已装到 ${where},但它声明为插件却未被加载——请检查其 keywords 与入口导出` };
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 工作区但走默认 `startAalis()` 的用户,包会被装进加载器永远不看的地方,静默失败。
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-memory-sqlite
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
- try {
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 现有文件,不重新写内容——与本插件其余文件操作同一路子(走 proc 网关的
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
- const { stdout, stderr } = await execProcBoth(proc, 'npm', ['install', ...specs, '--dry-run', '--no-audit', '--no-fund'], tmp.path, INSTALL_TIMEOUT_MS, STRICT_PEERS_ENV);
640
- const unmet = findUnmetPeers(`${stderr}\n${stdout}`, specs.map(stripVersion));
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 { specs, error } = buildUpdateSpecs(targets);
659
- if (!specs)
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: `读不到根 package.json: ${rootPkgPath}` };
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: '根依赖含 workspace: 协议,拒绝在此运行 npm install' };
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(t => !isRegistryDep(rootDependencies[t.name]));
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 t of targets) {
691
- const text = await deps.readText(`${root}/node_modules/${t.name}/package.json`);
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(t.name, v);
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(t => !isUpgrade(installedVersions.get(t.name), t.version));
781
+ const notUpgrade = targets.filter(target => !isUpgrade(installedVersions.get(target.name), target.version));
709
782
  if (notUpgrade.length > 0) {
710
- return {
711
- ok: false,
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: '依赖预检未通过,未改动任何文件。若是 peer 版本要求,请把被依赖方一并勾选后重试。',
791
+ message: t('preflightRejected'),
724
792
  conflicts,
725
793
  });
726
- const verdict = await preflightInSandbox(specs, root);
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 execProc(proc, 'npm', ['install', ...specs, '--no-audit', '--no-fund'], root, INSTALL_TIMEOUT_MS);
833
+ await npm(['install', ...specs, '--no-audit', '--no-fund'], root, INSTALL_TIMEOUT_MS);
760
834
  }
761
835
  catch (err) {
762
- const message = err instanceof Error ? err.message : String(err);
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(t => {
781
- const v = installedVersions.get(t.name);
782
- return v === undefined ? undefined : `${t.name}@${v}`;
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(t => t.name));
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: `已更新 ${specs.join('、')},正在重启…` };
873
+ return { ok: true, restarting: true, message: t('updated', { specs }) };
802
874
  }
803
875
  return {
804
- update: t => exclusive(`更新 ${t.map(x => x.name).join('、')}`, () => updateAll(t)),
805
- install: npmPkg => exclusive(`安装 ${npmPkg}`, async () => {
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 = err instanceof Error ? err.message : String(err);
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(`卸载 ${pluginName}`, () => uninstallOne(pluginName)),
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: `读不到 ${pluginName} 的 package.json,无法确认它是什么,已拒绝卸载` };
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: `${pluginName} 是${kind},不是可装卸的插件,市场不负责它的卸载。` +
850
- (kind === '内核' || kind === '宿主'
851
- ? '删掉它实例将无法启动、市场也随之消失,只能在终端里恢复;确需如此请手动执行 npm uninstall。'
852
- : '它随依赖它的插件被 npm 自动剪枝;确需单独删除请手动执行 npm uninstall。'),
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
- if (!isRegistryDep(request)) {
881
- const origin = classifyDepSpec(request);
948
+ const origin = classifyDepSpec(request);
949
+ if (origin !== 'registry') {
882
950
  const why = {
883
- workspace: '它是工作区里的本地源码包,删除请走 git',
884
- link: '它由 file:/link: 指向本地目录,删除请改那份源码与依赖声明',
885
- git: '它由 git/URL/user-repo 简写安装,删除请改依赖声明',
886
- alias: '它由 npm: 别名指向另一个包,删除请改依赖声明',
887
- transitive: '它不在根依赖里(由其它插件带入),会随带入它的插件被 npm 自动剪枝',
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: `${pluginName} 不是经市场安装的:${why[origin] ?? '来源不明'}。` };
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 execProc(proc, 'npm', ['uninstall', pluginName, '--no-audit', '--no-fund'], root, INSTALL_TIMEOUT_MS);
906
- for (const id of instanceIds) {
907
- await deps.unloadPlugin(id); // 从运行时注册表彻底移除(dispose + delete),幂等
908
- deps.cleanupConfig?.(id); // 清残留配置(含禁用标记)
909
- }
910
- log.info(`${pluginName}: 已从根依赖与 node_modules 移除`);
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: `插件 ${pluginName} 已卸载(已从根依赖与 node_modules 移除)。它写入 data/ 等存储根的数据不会删除,需要时请手动清理。`,
986
+ message: t(removeConfig ? 'uninstalledConfigUnsaved' : 'uninstalledFlagUnsaved', {
987
+ name: pluginName,
988
+ error: textOf(err),
989
+ }),
914
990
  };
915
991
  }
916
- catch (err) {
917
- return { ok: false, message: err instanceof Error ? err.message : String(err) };
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