dsh-plugin-manager-companion 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +144 -0
  3. package/README.md +142 -0
  4. package/cordis.patch.yml +9 -0
  5. package/dist/about.d.ts +77 -0
  6. package/dist/about.js +179 -0
  7. package/dist/cli.d.ts +226 -0
  8. package/dist/cli.js +856 -0
  9. package/dist/client/AboutPage.d.ts +75 -0
  10. package/dist/client/ConsolePage.d.ts +79 -0
  11. package/dist/client/KindsPage.d.ts +21 -0
  12. package/dist/client/MarketplacePage.d.ts +36 -0
  13. package/dist/client/OfficialSlots.d.ts +35 -0
  14. package/dist/client/UpgradeRow.d.ts +108 -0
  15. package/dist/client/index.d.ts +26 -0
  16. package/dist/client/locales.d.ts +475 -0
  17. package/dist/client/pmSelect.d.ts +38 -0
  18. package/dist/client/shared.d.ts +928 -0
  19. package/dist/client/upgradeView.d.ts +278 -0
  20. package/dist/client/wire.d.ts +401 -0
  21. package/dist/client.js +9194 -0
  22. package/dist/diagnostics.d.ts +332 -0
  23. package/dist/diagnostics.js +2631 -0
  24. package/dist/envManager.d.ts +1047 -0
  25. package/dist/envManager.js +3214 -0
  26. package/dist/fix.d.ts +60 -0
  27. package/dist/fix.js +168 -0
  28. package/dist/guard.d.ts +133 -0
  29. package/dist/guard.js +232 -0
  30. package/dist/index.d.ts +121 -0
  31. package/dist/index.js +1150 -0
  32. package/dist/installSession.d.ts +111 -0
  33. package/dist/installSession.js +150 -0
  34. package/dist/kinds.d.ts +464 -0
  35. package/dist/kinds.js +1029 -0
  36. package/dist/marketView.d.ts +261 -0
  37. package/dist/marketView.js +406 -0
  38. package/dist/marketplace.d.ts +248 -0
  39. package/dist/marketplace.js +500 -0
  40. package/dist/match.d.ts +67 -0
  41. package/dist/match.js +203 -0
  42. package/dist/net.d.ts +108 -0
  43. package/dist/net.js +163 -0
  44. package/dist/official.d.ts +145 -0
  45. package/dist/official.js +205 -0
  46. package/dist/paths.d.ts +108 -0
  47. package/dist/paths.js +236 -0
  48. package/dist/presets.d.ts +299 -0
  49. package/dist/presets.js +578 -0
  50. package/dist/qualityGate.d.ts +66 -0
  51. package/dist/qualityGate.js +247 -0
  52. package/dist/rank.d.ts +88 -0
  53. package/dist/rank.js +164 -0
  54. package/dist/registry.d.ts +295 -0
  55. package/dist/registry.js +686 -0
  56. package/dist/rest.d.ts +122 -0
  57. package/dist/rest.js +219 -0
  58. package/dist/scan.d.ts +134 -0
  59. package/dist/scan.js +396 -0
  60. package/dist/settings.d.ts +447 -0
  61. package/dist/settings.js +263 -0
  62. package/dist/tags.d.ts +119 -0
  63. package/dist/tags.js +166 -0
  64. package/dist/tools.d.ts +131 -0
  65. package/dist/tools.js +377 -0
  66. package/dist/types.d.ts +651 -0
  67. package/dist/types.js +13 -0
  68. package/dist/upgrade.d.ts +428 -0
  69. package/dist/upgrade.js +1100 -0
  70. package/dist/upgradeView.d.ts +313 -0
  71. package/dist/upgradeView.js +273 -0
  72. package/docs/images/readme/01-console-health.png +0 -0
  73. package/docs/images/readme/02-console-envs.png +0 -0
  74. package/docs/images/readme/03-marketplace.png +0 -0
  75. package/docs/images/readme/04-official-plugin-page.png +0 -0
  76. package/package.json +104 -0
package/dist/cli.js ADDED
@@ -0,0 +1,856 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dshpmc —— dsh-plugin-manager-companion 的命令行入口。
4
+ *
5
+ * 归属:A 类·重写(可执行入口;旧 src/cli.ts 仅作意图参考,未复制代码)。
6
+ * 旧实现参考:dsh-web-plugin-manager/src/cli.ts(400 行:bin 名 dshpm、命令集合
7
+ * install/remove/update/mount/list/analyze/uninstall-kind、全局 flag
8
+ * --profile/--home/--env、ctx 可空时跳过 live 应用、git 源缺 env 时给出
9
+ * 续装命令)。
10
+ * 官方复用:**包操作全部走官方 @deepseek-ai/dsh-plugin-manager/operations 的
11
+ * runPluginCommand**——它自带 profile 写锁(withFileLock)、profile 初始化、
12
+ * 输出落盘与 bundle 激活,与 dsh plugin 命令、官方 plugin_manager 工具是同一条
13
+ * pnpm 通道。安装锚点优先取官方 @deepseek-ai/dsh/profile-boot 的 INSTALL_ANCHOR。
14
+ * 本文件不拼任何 pnpm 参数、不写 package.json、不碰 cordis.patch.yml。
15
+ * 前提检查:旧实现的前提(必须在 profile 里跑 pnpm、必须自己维护 insert 行)已消失:
16
+ * 0.1.6 的官方 operations 按 profile 参数化,所以"管理另一个环境"= 换一个
17
+ * profile 参数,不是自建第二条写路径。旧实现的 mount(自己往 patch 里写挂载行)
18
+ * 被**删除**:官方 plugin_manager 的 set_plugin/set_bundle 才是挂载与启停的唯一
19
+ * 入口,这里只做只读盘点并把正确动作报给用户。
20
+ *
21
+ * 退出码:0 成功;1 发现问题(analyze)/ 操作失败;2 用法或环境错误。
22
+ * bin 名刻意用 dshpmc(旧包用 dshpm),两个包共存期间不会互相抢 bin。
23
+ */
24
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
25
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
26
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
27
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
28
+ });
29
+ }
30
+ return path;
31
+ };
32
+ import { existsSync } from 'node:fs';
33
+ import { join, resolve } from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+ import { dshHome, environmentDir, isBuiltinEnvironment, isSafeEnvironmentName, readEnvironmentManifest, } from "./paths.js";
36
+ import { DEFAULT_CONFIG } from "./settings.js";
37
+ import { addBlockedRepo, loadKindRecords, loadBlockedRepos, presetsRoot, removeKindDir, removeKindRecord, skillsRoot, } from "./kinds.js";
38
+ import { cleanupOwnedPresets, formatCleanupResult, pluginInstalledInOtherEnvironments, } from "./presets.js";
39
+ import { formatMissingRequirements, isGitSource, isSensitiveEnvKey, scanRequirements, } from "./scan.js";
40
+ import { filterAnswers, sessionKey, withInstallSession } from "./installSession.js";
41
+ /** 默认 profile(与旧实现一致:web 是绝大多数用户的环境)。 */
42
+ export const DEFAULT_PROFILE = 'web';
43
+ /** 用法文本。 */
44
+ export const USAGE = [
45
+ 'dshpmc - companion CLI for the DSH plugin manager',
46
+ '',
47
+ 'Usage: dshpmc <command> [options]',
48
+ '',
49
+ 'Commands:',
50
+ ' install <spec> Install a package into a profile',
51
+ ' remove <name> Remove an installed package from a profile',
52
+ ' update <name> Re-resolve a package to its newest version (add <name>@latest)',
53
+ ' mount <name> Report whether a declared dependency is mounted, and what to do (read-only)',
54
+ ' list List the layer stack, dependencies and companion-installed skills/presets',
55
+ ' analyze Run the deep environment health check (exit 1 when issues are found)',
56
+ ' uninstall-kind <repo> Remove a companion-installed skill or agent preset (owner/repo)',
57
+ ' help Show this text',
58
+ ' version Print the CLI version',
59
+ '',
60
+ 'Options:',
61
+ ' --profile <name> Target profile (default: ' + DEFAULT_PROFILE + ')',
62
+ ' --home <dir> Override DSH_HOME for this invocation',
63
+ ' --env KEY=value Provide an environment variable the plugin needs (repeatable)',
64
+ ' --json Machine-readable output where supported',
65
+ ].join(String.fromCharCode(10));
66
+ /**
67
+ * 解析 argv。
68
+ *
69
+ * 全局 flag 可以出现在命令前或命令后(与官方 CLI 的解析器一致),因此解析一遍
70
+ * 就够,不需要位置状态机。未知 flag 也接受:CLI 是给工具与其他脚本调的,
71
+ * 对未知 flag 报错只会让上游版本升级变脆。
72
+ *
73
+ * @param argv - 进程参数(不含 node 与脚本路径),也可以用完整 process.argv。
74
+ * @returns 解析结果。
75
+ * @throws 缺少命令名或 --env 形式非法时。
76
+ */
77
+ export function parseArgs(argv) {
78
+ const tokens = [...argv];
79
+ // 传完整 process.argv 时丢掉 node 与脚本路径(两者都不以 - 开头,且是绝对路径)。
80
+ if (tokens.length >= 2 && /[\\/]node(\.exe)?$/.test(tokens[0]) && tokens[1].includes('cli'))
81
+ tokens.splice(0, 2);
82
+ const rest = [];
83
+ const env = {};
84
+ const flags = new Set();
85
+ let profile = DEFAULT_PROFILE;
86
+ let home;
87
+ let json = false;
88
+ for (let index = 0; index < tokens.length; index += 1) {
89
+ const token = tokens[index];
90
+ if (token === '--profile') {
91
+ const value = tokens[index + 1];
92
+ if (value === undefined || value.startsWith('-'))
93
+ throw new Error('--profile requires a value');
94
+ profile = value;
95
+ index += 1;
96
+ continue;
97
+ }
98
+ if (token.startsWith('--profile=')) {
99
+ profile = token.slice('--profile='.length);
100
+ continue;
101
+ }
102
+ if (token === '--home') {
103
+ const value = tokens[index + 1];
104
+ if (value === undefined || value.startsWith('-'))
105
+ throw new Error('--home requires a value');
106
+ home = value;
107
+ index += 1;
108
+ continue;
109
+ }
110
+ if (token.startsWith('--home=')) {
111
+ home = token.slice('--home='.length);
112
+ continue;
113
+ }
114
+ if (token === '--env') {
115
+ const value = tokens[index + 1];
116
+ if (value === undefined || !value.includes('='))
117
+ throw new Error('--env requires KEY=value');
118
+ const separator = value.indexOf('=');
119
+ env[value.slice(0, separator)] = value.slice(separator + 1);
120
+ index += 1;
121
+ continue;
122
+ }
123
+ if (token.startsWith('--env=')) {
124
+ const value = token.slice('--env='.length);
125
+ if (!value.includes('='))
126
+ throw new Error('--env requires KEY=value');
127
+ const separator = value.indexOf('=');
128
+ env[value.slice(0, separator)] = value.slice(separator + 1);
129
+ continue;
130
+ }
131
+ if (token === '--json') {
132
+ json = true;
133
+ continue;
134
+ }
135
+ if (token.startsWith('--')) {
136
+ flags.add(token.slice(2));
137
+ continue;
138
+ }
139
+ rest.push(token);
140
+ }
141
+ const command = rest.shift();
142
+ if (command === undefined || command === '')
143
+ throw new Error('missing command; run "dshpmc help"');
144
+ if (!isSafeEnvironmentName(profile))
145
+ throw new Error('unsafe profile name: ' + JSON.stringify(profile));
146
+ return { command, args: rest, profile, ...home === undefined ? {} : { home }, env, json, flags };
147
+ }
148
+ /** 环境目录(--home 覆盖时先改进程 DSH_HOME,让 paths/presets 与官方 operations 看到同一个 home)。 */
149
+ function prepareHome(options) {
150
+ if (options.home === undefined)
151
+ return;
152
+ process.env['DSH_HOME'] = resolve(options.home);
153
+ }
154
+ /**
155
+ * 解析官方 dsh 安装的 package.json 锚点。
156
+ *
157
+ * 三条路,按可靠性排序:
158
+ * 1. 注入(宿主进程里应传 ctx.profileContext.installAnchor);
159
+ * 2. 官方 @deepseek-ai/dsh/profile-boot 的 INSTALL_ANCHOR(CLI 跑在装了 dsh 的
160
+ * profile 里时可用);
161
+ * 3. 从 CLI 自身位置向上找 node_modules/@deepseek-ai/dsh/package.json。
162
+ * 三条都不通时**不猜**:返回 undefined,由调用方给出可执行的报错。
163
+ *
164
+ * @param injected - 注入的锚点。
165
+ * @returns 锚点绝对路径;解析不出时 undefined。
166
+ */
167
+ export async function resolveInstallAnchor(injected) {
168
+ // 空白注入按"没给"处理:调用方用一个空格表达"别用注入值"比传 undefined 更常见,
169
+ // 当成锚点会让引擎拿一个空白路径去解析,而报错文本里只有一个空格,查不出原因。
170
+ if (injected !== undefined && injected.trim() !== '')
171
+ return injected;
172
+ try {
173
+ // 说明符放在变量里:官方包是 peer(CLI 可能在没装它的环境里使用),
174
+ // 字面量说明符会让 tsc 去解析一个不保证存在的模块并让构建失败;
175
+ // 放在变量里解析失败会落到下面的 catch,正是我们要的行为。
176
+ const specifier = '@deepseek-ai/dsh/profile-boot';
177
+ const module = await import(__rewriteRelativeImportExtension(specifier));
178
+ if (typeof module.INSTALL_ANCHOR === 'string' && existsSync(module.INSTALL_ANCHOR))
179
+ return module.INSTALL_ANCHOR;
180
+ }
181
+ catch {
182
+ // 官方包不可解析(CLI 在 profile 之外跑):继续找别的路。
183
+ }
184
+ const fromEnv = process.env['DSH_INSTALL_ANCHOR'];
185
+ if (fromEnv !== undefined && existsSync(fromEnv))
186
+ return fromEnv;
187
+ return findAnchorFrom(process.cwd());
188
+ }
189
+ /** 从给定目录向上找 node_modules/@deepseek-ai/dsh/package.json。 */
190
+ function findAnchorFrom(start) {
191
+ let current = resolve(start);
192
+ for (let depth = 0; depth < 8; depth += 1) {
193
+ const candidate = join(current, 'node_modules', '@deepseek-ai', 'dsh', 'package.json');
194
+ if (existsSync(candidate))
195
+ return candidate;
196
+ const parent = resolve(current, '..');
197
+ if (parent === current)
198
+ break;
199
+ current = parent;
200
+ }
201
+ return undefined;
202
+ }
203
+ /**
204
+ * 取官方 runPluginCommand;注入优先,否则动态 import。
205
+ *
206
+ * 动态 import 而不是静态 import 的原因是构建顺序:静态 import 会让本文件的编译
207
+ * 依赖官方子路径的解析结果,而 CLI 可能在官方包不可用的环境里构建/使用。
208
+ * 解析失败不是一个崩溃点,而是一个可读的操作错误。
209
+ *
210
+ * @param injected - 注入的实现。
211
+ * @returns 官方实现。
212
+ * @throws 官方包不可解析时。
213
+ */
214
+ export async function loadRunPluginCommand(injected) {
215
+ if (injected !== undefined)
216
+ return injected;
217
+ try {
218
+ const module = await import('@deepseek-ai/dsh-plugin-manager/operations');
219
+ if (typeof module.runPluginCommand !== 'function') {
220
+ throw new Error('the official plugin-manager operations module exports no runPluginCommand');
221
+ }
222
+ return module.runPluginCommand;
223
+ }
224
+ catch (error) {
225
+ throw new Error('cannot load the official plugin operations (runPluginCommand): '
226
+ + (error instanceof Error ? error.message : String(error))
227
+ + '. Run this command from a shell where dsh is installed, or run "dshpmc" through the profile copy of the companion.');
228
+ }
229
+ }
230
+ /**
231
+ * 跑一次受保护的包操作。
232
+ *
233
+ * git 源会先扫描仓库需要的环境变量(见 scan.ts),把缺失清单报给用户,
234
+ * 并**只按扫描白名单**注入 --env 提供的值;子进程环境剔掉敏感键形态。
235
+ * 本函数不拼 pnpm 参数——args 直接交给官方 runPluginCommand。
236
+ *
237
+ * @param options - 解析后的全局 flag。
238
+ * @param args - 官方 pnpm 参数(如 ['add', spec])。
239
+ * @param deps - 注入的依赖。
240
+ * @returns 退出码。
241
+ */
242
+ async function runProtectedOperation(options, args, deps, out) {
243
+ const installAnchor = await resolveInstallAnchor(deps.installAnchor);
244
+ if (installAnchor === undefined) {
245
+ out.stderr('dshpmc: cannot locate the dsh installation (its package.json). '
246
+ + 'Run from an installed dsh, or set DSH_INSTALL_ANCHOR to that file.' + String.fromCharCode(10));
247
+ return 2;
248
+ }
249
+ const home = options.home === undefined ? dshHome() : resolve(options.home);
250
+ let env = {};
251
+ const spec = args[args.length - 1] ?? '';
252
+ if (isGitSource(spec)) {
253
+ // 会话键与 git 缓存目录由官方 clone 流程决定,这里只做"扫描 + 白名单校验",
254
+ // 不把 spec 当路径用——CLI 每次调用是新进程,没有会话可复用(见 installSession.ts)。
255
+ const scanRoot = resolve(process.cwd(), spec.replace(/^git\+/i, '').replace(/#.*$/, ''));
256
+ const report = existsSync(scanRoot)
257
+ ? await scanRequirements(scanRoot)
258
+ : undefined;
259
+ if (report === undefined) {
260
+ // 远程 git 源在官方 pnpm 流程里才 clone,安装前没有可扫的目录。如实说明而不是假装扫过了:
261
+ // 漏报的变量会在 pnpm 输出里以失败的形式出现。
262
+ out.stdout('dshpmc: note: a remote git source cannot be pre-scanned before pnpm clones it. '
263
+ + 'If this plugin needs credentials during install or build, pass them now: --env KEY=value.');
264
+ }
265
+ if (report !== undefined) {
266
+ const provided = filterAnswers(report.requirements, options.env);
267
+ const missing = formatMissingRequirements(report, provided);
268
+ if (missing.length > 0) {
269
+ out.stderr('dshpmc: the git source may need environment variables during install/build:' + String.fromCharCode(10));
270
+ for (const name of missing)
271
+ out.stderr(' - ' + name + String.fromCharCode(10));
272
+ if (report.truncated) {
273
+ out.stderr(' (scan limits reached: ' + report.truncatedReasons.join('; ')
274
+ + ' — the list may be incomplete)' + String.fromCharCode(10));
275
+ }
276
+ out.stderr('Re-run the same command with --env KEY=value for each one, for example:' + String.fromCharCode(10));
277
+ out.stderr(' dshpmc ' + options.command + ' ' + spec
278
+ + ' --profile ' + options.profile
279
+ + (missing.length === 0 ? '' : ' --env ' + (missing[0].split(' ')[0] ?? 'KEY') + '=...') + String.fromCharCode(10));
280
+ return 1;
281
+ }
282
+ env = provided;
283
+ }
284
+ }
285
+ const run = await loadRunPluginCommand(deps.runPluginCommand);
286
+ // 官方在 execution: 'cli' 下继承终端环境(pnpm 需要用户的 PATH/nvm),因此
287
+ // 敏感键不能靠 options.env 覆盖掉——它是在 process.env 之上合并的,删不掉。
288
+ // 做法是:调用期间把这些键从 process.env 里摘掉,调用结束还原。CLI 一次调用
289
+ // 只跑一个操作、且是单线程,窗口内没有别的读者;宿主进程不走这条路径。
290
+ const answers = filterAnswers(Object.keys(env), env);
291
+ const result = await withScrubbedEnvironment(async () => await run({
292
+ profile: options.profile,
293
+ installAnchor,
294
+ cwd: process.cwd(),
295
+ home,
296
+ }, args, {
297
+ execution: 'cli',
298
+ outputBytes: 16384,
299
+ lockWaitMs: 120_000,
300
+ // 只注入用户显式提供的白名单键(scan.ts 的扫描结果),其余宿主键不被
301
+ // 注入也不被改写——环境注入面由扫描结果决定,不由提交方的键名决定。
302
+ env: answers,
303
+ onOutput: (text, stream) => {
304
+ if (stream === 'stdout')
305
+ out.stdout(text);
306
+ else
307
+ out.stderr(text);
308
+ },
309
+ }));
310
+ if (result.exitCode === 127) {
311
+ out.stderr('dshpmc: pnpm was not found; install pnpm and make it available on PATH.' + String.fromCharCode(10));
312
+ }
313
+ if (result.kind !== undefined)
314
+ out.stderr('dshpmc: failure classified as ' + result.kind + String.fromCharCode(10));
315
+ if (result.exitCode !== 0 && result.logPath !== undefined) {
316
+ out.stderr('dshpmc: full log at ' + result.logPath + String.fromCharCode(10));
317
+ if (isGitSource(spec)) {
318
+ out.stderr('dshpmc: if pnpm printed an allowBuilds hint, approve the listed key in the '
319
+ + 'profile pnpm-workspace.yaml and re-run.' + String.fromCharCode(10));
320
+ }
321
+ }
322
+ return result.exitCode === null ? 1 : result.exitCode;
323
+ }
324
+ /**
325
+ * 在"摘掉敏感键"的环境里执行一段工作,结束后原样还原。
326
+ *
327
+ * 为什么需要它:官方 runPluginCommand 的 execution: 'cli' 模式继承终端环境(pnpm 要
328
+ * 用用户的 PATH/nvm),options.env 是在 process.env 之上**合并**的,无法用它删掉键。
329
+ * 而 git 源的插件会在 prepare/postinstall 里跑第三方代码,GITHUB_TOKEN 这类值不该
330
+ * 顺路交出去。窗口只覆盖一次包操作;还原在 finally 里,失败也不会把宿主的变量吃掉。
331
+ *
332
+ * @param task - 要在净化环境里执行的工作。
333
+ * @returns 工作结果。
334
+ */
335
+ async function withScrubbedEnvironment(task) {
336
+ const removed = [];
337
+ for (const key of Object.keys(process.env)) {
338
+ if (!isSensitiveEnvKey(key))
339
+ continue;
340
+ const value = process.env[key];
341
+ if (value === undefined)
342
+ continue;
343
+ removed.push([key, value]);
344
+ delete process.env[key];
345
+ }
346
+ try {
347
+ return await task();
348
+ }
349
+ finally {
350
+ for (const [key, value] of removed)
351
+ process.env[key] = value;
352
+ }
353
+ }
354
+ /** 一个安装记录的可读行。 */
355
+ function recordLine(key, record) {
356
+ const where = record.dir === '' ? '(unknown location)' : record.dir;
357
+ return '- ' + key + ' [' + record.kind + '] -> ' + where + ' installed ' + record.installedAt;
358
+ }
359
+ /** 列出环境事实 + 我们的安装记录(只读)。 */
360
+ async function runList(options, out) {
361
+ const dir = environmentDir(options.profile);
362
+ if (!existsSync(dir)) {
363
+ out.stdout('profile "' + options.profile + '" is not installed at ' + dir + String.fromCharCode(10));
364
+ }
365
+ else {
366
+ const manifest = readEnvironmentManifest(dir);
367
+ out.stdout('profile: ' + options.profile
368
+ + (isBuiltinEnvironment(options.profile) ? ' (builtin)' : '')
369
+ + String.fromCharCode(10) + ' dir: ' + dir + String.fromCharCode(10));
370
+ if (manifest.broken !== undefined) {
371
+ out.stdout(' manifest unreadable: ' + manifest.broken + String.fromCharCode(10));
372
+ }
373
+ out.stdout(' bundle layers (' + String(manifest.bundles.length) + '):'
374
+ + (manifest.bundles.length === 0 ? ' (none)' : '') + String.fromCharCode(10));
375
+ for (const bundle of manifest.bundles)
376
+ out.stdout(' - ' + bundle + String.fromCharCode(10));
377
+ out.stdout(' dependencies (' + String(manifest.dependencies.length) + '):'
378
+ + (manifest.dependencies.length === 0 ? ' (none)' : '') + String.fromCharCode(10));
379
+ for (const dependency of manifest.dependencies)
380
+ out.stdout(' - ' + dependency + String.fromCharCode(10));
381
+ }
382
+ const records = await loadKindRecords();
383
+ out.stdout('companion-installed skills/presets (' + String(records.size) + '):' + String.fromCharCode(10));
384
+ if (records.size === 0)
385
+ out.stdout(' (none)' + String.fromCharCode(10));
386
+ for (const [key, record] of records)
387
+ out.stdout(' ' + recordLine(key, record) + String.fromCharCode(10));
388
+ out.stdout('skill root: ' + skillsRoot() + String.fromCharCode(10));
389
+ out.stdout('preset root: ' + presetsRoot() + String.fromCharCode(10));
390
+ const blocked = await loadBlockedRepos();
391
+ if (blocked.size > 0) {
392
+ out.stdout('blocked repositories (' + String(blocked.size) + '): ' + [...blocked].sort().join(', ') + String.fromCharCode(10));
393
+ }
394
+ return 0;
395
+ }
396
+ /**
397
+ * mount:只读盘点一个依赖是否被挂载。
398
+ *
399
+ * 刻意**不做**挂载写入:挂载行属于 profile 组合,官方 plugin_manager 的
400
+ * set_plugin/set_bundle 是唯一入口。这里的价值是把"声明了但没挂载"这件事查出来
401
+ * 并给出可直接执行的下一步。
402
+ */
403
+ async function runMount(options, out) {
404
+ const name = options.args[0];
405
+ if (name === undefined || name === '') {
406
+ out.stderr('dshpmc mount: a package name is required' + String.fromCharCode(10));
407
+ return 2;
408
+ }
409
+ const dir = environmentDir(options.profile);
410
+ if (!existsSync(dir)) {
411
+ out.stderr('dshpmc mount: profile "' + options.profile + '" is not installed at ' + dir + String.fromCharCode(10));
412
+ return 2;
413
+ }
414
+ const manifest = readEnvironmentManifest(dir);
415
+ const declared = manifest.dependencies.includes(name);
416
+ const isLayer = manifest.bundles.includes(name);
417
+ out.stdout('package: ' + name + String.fromCharCode(10));
418
+ out.stdout(' declared in package.json: ' + (declared ? 'yes' : 'no') + String.fromCharCode(10));
419
+ out.stdout(' in the bundle layer stack: ' + (isLayer ? 'yes' : 'no') + String.fromCharCode(10));
420
+ if (!declared) {
421
+ out.stdout('Install it first: dshpmc install ' + name + ' --profile ' + options.profile + String.fromCharCode(10));
422
+ return 0;
423
+ }
424
+ out.stdout(String.fromCharCode(10)
425
+ + 'Declared, not necessarily mounted — check and mount it through the official manager:'
426
+ + String.fromCharCode(10)
427
+ + ' plugin_manager { action: "list_plugins" } # find the entry id and its current state'
428
+ + String.fromCharCode(10)
429
+ + ' plugin_manager { action: "set_plugin", target: "<entry id>", enabled: true }'
430
+ + String.fromCharCode(10)
431
+ + ' plugin_manager { action: "set_bundle", target: "' + name + '", enabled: true } # if it is a bundle'
432
+ + String.fromCharCode(10)
433
+ + '(those actions require danger-full-access permission or approval; the change affects every session '
434
+ + 'in profile ' + options.profile + ')' + String.fromCharCode(10));
435
+ return 0;
436
+ }
437
+ /**
438
+ * 最小 host 上下文。
439
+ *
440
+ * 引擎对 ctx 只做两件事:ctx.get('profileContext') 取安装锚点、ctx.logger 记账。
441
+ * CLI 进程里两样都没有:get 一律 undefined(引擎会把它记成"没有锚点"的 skipped,
442
+ * 而不是当成"包不存在"),logger 用空实现。**不假装自己是宿主**。
443
+ *
444
+ * @returns 结构式最小上下文。
445
+ */
446
+ export function minimalCliContext() {
447
+ const noop = () => { };
448
+ return { get: () => undefined, logger: { debug: noop, info: noop, warn: noop, error: noop } };
449
+ }
450
+ /**
451
+ * 动态加载诊断引擎。
452
+ *
453
+ * 刻意不用静态 import:CLI 要在引擎的官方依赖(app-boot / plugin-inventory 等)缺失时
454
+ * 也能报出**可读的原因**,而不是让整个 bin 加载失败。说明符放在变量里,避免打包/编译期
455
+ * 去解析一个不保证存在的模块。
456
+ *
457
+ * @returns 引擎模块或失败原因。
458
+ */
459
+ export async function loadAnalyzer() {
460
+ try {
461
+ const specifier = './diagnostics.js';
462
+ const module = await import(__rewriteRelativeImportExtension(specifier));
463
+ if (typeof module.analyzeEnvironment !== 'function') {
464
+ return { reason: 'the diagnostics module exposes no analyzeEnvironment export' };
465
+ }
466
+ return { analyzer: module };
467
+ }
468
+ catch (error) {
469
+ return { reason: error instanceof Error ? error.message : String(error) };
470
+ }
471
+ }
472
+ /**
473
+ * 由磁盘事实构造诊断目标(不依赖宿主)。
474
+ *
475
+ * @param name - 环境名。
476
+ * @param installAnchor - 已解析的安装锚点;解析不出时不传(引擎会如实记 skipped)。
477
+ * @returns 诊断目标。
478
+ */
479
+ export function environmentTargetFor(name, installAnchor) {
480
+ const dir = environmentDir(name);
481
+ const manifest = readEnvironmentManifest(dir);
482
+ return {
483
+ name,
484
+ dir,
485
+ current: false,
486
+ builtin: isBuiltinEnvironment(name),
487
+ bundles: manifest.bundles,
488
+ dependencies: manifest.dependencies,
489
+ runs: [],
490
+ ...installAnchor === undefined ? {} : { installAnchor },
491
+ };
492
+ }
493
+ /**
494
+ * 启动阶段硬失败的根因特征。
495
+ *
496
+ * 这三种"脏"会让 dsh 在启动阶段就退出:用户连界面都没有,诊断页也永远看不到。
497
+ * CLI 是唯一的逃生口,所以这里把根因文本显式挑出来单独展示——即便它在报告里的
498
+ * 身份只是"某层被跳过的原因"(官方 composeEntries / loadProfileDirectory 会抛错,
499
+ * 引擎把抛错原因记进 skipped)。
500
+ */
501
+ export const BOOT_BLOCKER_SIGNATURES = [
502
+ 'duplicate loader entry id',
503
+ 'cannot resolve profile bundle',
504
+ 'ERR_MODULE_NOT_FOUND',
505
+ 'plugin tree failed to load',
506
+ 'ERR_PACKAGE_PATH_NOT_EXPORTED',
507
+ ];
508
+ /**
509
+ * 关键核对项:这些没跑完,就不能说"环境没问题"。
510
+ *
511
+ * 与"结构性不可用"分开:CLI 里没有活 Loader,运行时层(runtime)永远跳过,那是**已知且预期**
512
+ * 的缺失(要显著告知,但不构成"有问题");而依赖/组合/一致性三层是只读磁盘就能跑的,
513
+ * 它们跳过说明这次诊断并没有真正覆盖到"环境能不能起来"。
514
+ */
515
+ export const CRITICAL_SKIP_CHECKS = [
516
+ 'environment-dir', 'dependency-layer', 'composition-official', 'composition-layer', 'consistency-layer',
517
+ ];
518
+ /**
519
+ * 一个 skipped 条目归属的层。
520
+ *
521
+ * 优先用引擎给的 `layers`(task-29 起 DiagnosticSkip 带复数层归属——单数表达不了
522
+ * "一次能力缺失同时废掉两层");老引擎没有这个字段时退回按 check 名推断。
523
+ *
524
+ * @param skip - 跳过项。
525
+ * @returns 层名列表。
526
+ */
527
+ function layersOfSkip(skip) {
528
+ if (Array.isArray(skip.layers)) {
529
+ return skip.layers.filter((layer) => typeof layer === 'string');
530
+ }
531
+ const named = ['dependency', 'composition', 'runtime', 'consistency', 'ecosystem']
532
+ .filter(layer => skip.check === layer + '-layer' || skip.check.startsWith(layer + '-'));
533
+ return named;
534
+ }
535
+ /**
536
+ * 这个跳过项是否让"环境没问题"这句话不能成立。
537
+ *
538
+ * 关键层是只读磁盘就能跑的三层(依赖/组合/一致性):它们跳过说明这次诊断没有真正覆盖
539
+ * "环境能不能起来"。运行时层在 CLI 里结构性不可用(没有活 Loader),要显著告知,但不构成结论。
540
+ *
541
+ * @param skip - 跳过项。
542
+ * @returns 是否属于关键核对缺失。
543
+ */
544
+ function isCriticalSkip(skip) {
545
+ if (CRITICAL_SKIP_CHECKS.includes(skip.check))
546
+ return true;
547
+ return layersOfSkip(skip).some(layer => CRITICAL_SKIP_CHECKS.includes(layer + '-layer'));
548
+ }
549
+ /** 文本里是否含启动阶段硬失败特征。 */
550
+ function blockerHit(text) {
551
+ for (const signature of BOOT_BLOCKER_SIGNATURES) {
552
+ if (text.includes(signature))
553
+ return signature;
554
+ }
555
+ return undefined;
556
+ }
557
+ /**
558
+ * 判定一次诊断的结果与退出码。
559
+ *
560
+ * 语义(刻意不让 issues=0 等于"健康"):
561
+ * - 有问题 → 'issues' / 1;
562
+ * - 没问题但**关键核对项没跑完**(组合层读不出来、缺安装锚点)→ 'incomplete' / 1,
563
+ * 由调用方在文案里说清楚"这次没能完成组合层核对";
564
+ * - 只有关键项都跑完且没问题才是 'clean' / 0。
565
+ *
566
+ * @param report - 引擎报告。
567
+ * @param notes - 调用方补充的事实(例如"没有安装锚点")。
568
+ * @returns 判定结果。
569
+ */
570
+ export function judgeAnalyze(report, notes = {}) {
571
+ const blockers = [];
572
+ const pushBlocker = (text) => {
573
+ if (text === '' || blockerHit(text) === undefined)
574
+ return;
575
+ if (!blockers.includes(text))
576
+ blockers.push(text);
577
+ };
578
+ for (const issue of report.issues) {
579
+ pushBlocker(issue.title);
580
+ pushBlocker(issue.detail);
581
+ for (const evidence of issue.evidence)
582
+ pushBlocker(evidence.at + ' — ' + evidence.note);
583
+ if (issue.fix !== undefined)
584
+ pushBlocker(issue.fix.summary);
585
+ }
586
+ for (const skip of report.skipped)
587
+ pushBlocker(skip.reason);
588
+ const incomplete = [];
589
+ for (const skip of report.skipped) {
590
+ if (!isCriticalSkip(skip))
591
+ continue;
592
+ incomplete.push(skip.check + ': ' + skip.reason);
593
+ }
594
+ if (notes.anchorMissing === true) {
595
+ incomplete.push('install-anchor: 没有安装锚点 → L1 dependency / L2 composition / L4 consistency 的模块解析缺安装侧根,本次结果不完整');
596
+ }
597
+ const disabled = new Set(notes.disabledLayers ?? []);
598
+ const skippedLayers = new Set();
599
+ for (const skip of report.skipped) {
600
+ for (const layer of layersOfSkip(skip))
601
+ skippedLayers.add(layer);
602
+ }
603
+ const layers = ['dependency', 'composition', 'runtime', 'consistency', 'ecosystem'].map(layer => ({
604
+ layer,
605
+ state: disabled.has(layer)
606
+ ? 'disabled'
607
+ : skippedLayers.has(layer) ? 'skipped' : 'ran',
608
+ }));
609
+ const verdict = report.issues.length > 0
610
+ ? 'issues'
611
+ : (incomplete.length > 0 || blockers.length > 0) ? 'incomplete' : 'clean';
612
+ return { layers, incomplete, blockers, verdict, code: verdict === 'clean' ? 0 : 1 };
613
+ }
614
+ /**
615
+ * analyze:跑诊断并打印。
616
+ *
617
+ * 两条路径:
618
+ * 1. 宿主注入的 deps.analyze(Web 侧接线)优先——那条路径有自己的活 Loader;
619
+ * 2. 没有注入时**自己加载引擎只读跑**:环境起不来(bundle 解析不到 / patch 行解析不到 /
620
+ * 两行同 id)时界面上没有任何入口,CLI 是唯一的逃生口,所以这条路径必须真的能跑。
621
+ *
622
+ * @param options - 解析后的全局 flag。
623
+ * @param deps - 注入依赖。
624
+ * @param out - 输出 sink。
625
+ * @returns 退出码:0 干净;1 有问题/没跑完;2 用法或环境错误。
626
+ */
627
+ async function runAnalyze(options, deps, out) {
628
+ const nl = String.fromCharCode(10);
629
+ let report;
630
+ let anchorMissing = false;
631
+ let anchorPath;
632
+ if (deps.analyze !== undefined) {
633
+ report = await deps.analyze(options.profile, DEFAULT_CONFIG);
634
+ }
635
+ else {
636
+ const loaded = await loadAnalyzer();
637
+ if (loaded.analyzer === undefined) {
638
+ out.stderr('dshpmc analyze: cannot load the diagnostics engine (' + (loaded.reason ?? 'unknown reason') + ').' + nl
639
+ + 'Install the companion package build (pnpm run build:host) or pass the host plugin path.' + nl);
640
+ return 2;
641
+ }
642
+ anchorPath = await resolveInstallAnchor(deps.installAnchor);
643
+ anchorMissing = anchorPath === undefined;
644
+ const target = environmentTargetFor(options.profile, anchorPath);
645
+ // 最小 ctx:没有活 Loader,运行时层会如实记 skipped(而不是假装查过)。
646
+ report = await loaded.analyzer.analyzeEnvironment(minimalCliContext(), { ...target }, DEFAULT_CONFIG);
647
+ }
648
+ const disabledLayers = Object.entries(DEFAULT_CONFIG.diagnostics)
649
+ .filter(([, enabled]) => enabled !== true)
650
+ .map(([layer]) => layer);
651
+ const judgement = judgeAnalyze(report, { anchorMissing, disabledLayers });
652
+ const counts = Object.entries(report.counts).filter(([, count]) => count > 0);
653
+ if (options.json) {
654
+ out.stdout(JSON.stringify(report, undefined, 2) + nl);
655
+ out.stderr('coverage: ' + judgement.layers.map(layer => layer.layer + '=' + layer.state).join(' ')
656
+ + ' verdict: ' + judgement.verdict + nl);
657
+ return judgement.code;
658
+ }
659
+ out.stdout('environment: ' + report.environment + ' checked at ' + report.generatedAt + nl);
660
+ out.stdout(' dir: ' + environmentDir(options.profile) + nl);
661
+ out.stdout(' coverage: ' + judgement.layers.map(layer => layer.layer + '=' + layer.state).join(' ') + nl);
662
+ out.stdout(' install anchor: ' + (anchorPath ?? (deps.analyze === undefined ? 'MISSING' : 'provided by the host')) + nl);
663
+ out.stdout('issues: ' + String(report.issues.length)
664
+ + (counts.length === 0 ? '' : ' (' + counts.map(([layer, count]) => layer + '=' + String(count)).join(', ') + ')')
665
+ + nl);
666
+ if (judgement.blockers.length > 0) {
667
+ out.stdout(nl + '!!! boot-blocking root cause detected — this environment cannot start:' + nl);
668
+ for (const blocker of judgement.blockers)
669
+ out.stdout(' - ' + blocker + nl);
670
+ }
671
+ if (judgement.verdict === 'incomplete') {
672
+ out.stdout(nl + 'INCOMPLETE: 这次没能完成关键核对——issues=0 **不代表环境健康**。' + nl);
673
+ for (const reason of judgement.incomplete)
674
+ out.stdout(' - ' + reason + nl);
675
+ for (const blocker of judgement.blockers) {
676
+ if (!judgement.incomplete.some(reason => reason.includes(blocker)))
677
+ out.stdout(' - 未提升为问题的根因:' + blocker + nl);
678
+ }
679
+ }
680
+ for (const issue of report.issues) {
681
+ out.stdout(nl + '[' + issue.severity + '] ' + issue.code + ': ' + issue.title + nl);
682
+ out.stdout(' ' + issue.detail + nl);
683
+ for (const evidence of issue.evidence)
684
+ out.stdout(' at ' + evidence.at + ' — ' + evidence.note + nl);
685
+ if (issue.fix !== undefined)
686
+ out.stdout(' fix: ' + issue.fix.action + ' — ' + issue.fix.summary + nl);
687
+ }
688
+ for (const skip of report.skipped)
689
+ out.stdout(nl + 'skipped: ' + skip.check + ' — ' + skip.reason + nl);
690
+ return judgement.code;
691
+ }
692
+ /** uninstall-kind:删除一次 skill/预设直装(含预设归属清理)。 */
693
+ async function runUninstallKind(options, out) {
694
+ const repo = options.args[0];
695
+ if (repo === undefined || repo === '') {
696
+ out.stderr('dshpmc uninstall-kind: an owner/repo reference is required' + String.fromCharCode(10));
697
+ return 2;
698
+ }
699
+ const records = await loadKindRecords();
700
+ const wanted = sessionKey(repo);
701
+ const match = [...records.entries()].find(([key, record]) => sessionKey(key) === wanted || sessionKey(record.repo) === wanted);
702
+ if (match === undefined) {
703
+ out.stderr('dshpmc uninstall-kind: no companion install record for ' + JSON.stringify(repo)
704
+ + '. Known records: ' + ([...records.keys()].join(', ') || '(none)') + String.fromCharCode(10));
705
+ return 2;
706
+ }
707
+ const [key, record] = match;
708
+ if (record.kind === 'cordis-plugin') {
709
+ out.stderr('dshpmc uninstall-kind: ' + key + ' is a cordis plugin, not a skill or preset. '
710
+ + 'Remove it through the official manager: plugin_manager { action: "remove_bundle", target: "'
711
+ + (record.repo || key) + '" }, or run dshpmc remove <package> --profile ' + options.profile
712
+ + String.fromCharCode(10));
713
+ return 2;
714
+ }
715
+ if (record.kind === 'agent-preset') {
716
+ const stillElsewhere = await pluginInstalledInOtherEnvironments(options.profile, record.repo);
717
+ const result = await cleanupOwnedPresets(undefined, record.repo, { stillInstalledElsewhere: stillElsewhere });
718
+ out.stdout(formatCleanupResult(record.repo, result) + String.fromCharCode(10));
719
+ if (result.removed.length === 0 && result.skipped.length > 0)
720
+ return 1;
721
+ }
722
+ else {
723
+ const root = skillsRoot();
724
+ if (record.dir !== '' && record.dir !== root) {
725
+ await removeKindDir(root, record.dir);
726
+ out.stdout('removed skill ' + record.dir + String.fromCharCode(10));
727
+ }
728
+ else {
729
+ out.stdout('skill "' + record.repo + '" was installed as a collection; individual directories under '
730
+ + root + ' were not removed automatically. Remove the ones you no longer need, then re-run.'
731
+ + String.fromCharCode(10));
732
+ return 1;
733
+ }
734
+ }
735
+ await removeKindRecord(key);
736
+ out.stdout('forgot install record ' + key + String.fromCharCode(10));
737
+ return 0;
738
+ }
739
+ /**
740
+ * CLI 主入口。
741
+ *
742
+ * @param argv - 参数(不含 node 与脚本路径;传 process.argv.slice(2))。
743
+ * @param deps - 可注入依赖(测试/宿主用)。
744
+ * @returns 退出码。
745
+ */
746
+ export async function main(argv, deps = {}) {
747
+ const stdout = deps.stdout ?? ((text) => { process.stdout.write(text); });
748
+ const stderr = deps.stderr ?? ((text) => { process.stderr.write(text); });
749
+ let options;
750
+ try {
751
+ options = parseArgs(argv);
752
+ }
753
+ catch (error) {
754
+ stderr('dshpmc: ' + (error instanceof Error ? error.message : String(error)) + String.fromCharCode(10));
755
+ return 2;
756
+ }
757
+ prepareHome(options);
758
+ try {
759
+ switch (options.command) {
760
+ case 'help':
761
+ case '--help':
762
+ stdout(USAGE + String.fromCharCode(10));
763
+ return 0;
764
+ case 'version':
765
+ stdout(await readVersion() + String.fromCharCode(10));
766
+ return 0;
767
+ case 'install': {
768
+ const spec = options.args[0];
769
+ if (spec === undefined || spec === '' || spec.startsWith('-')) {
770
+ stderr('dshpmc install: a package spec is required' + String.fromCharCode(10));
771
+ return 2;
772
+ }
773
+ const code = await withInstallSession(async () => await runProtectedOperation(options, ['add', spec], deps, { stdout, stderr }));
774
+ if (code === 0) {
775
+ stdout('dshpmc: installed ' + spec + ' into profile ' + options.profile
776
+ + ' (' + (isGitSource(spec) ? 'git source' : 'registry/local source') + ')' + String.fromCharCode(10));
777
+ }
778
+ return code;
779
+ }
780
+ case 'remove': {
781
+ const name = options.args[0];
782
+ if (name === undefined || name === '' || name.startsWith('-')) {
783
+ stderr('dshpmc remove: a package name is required' + String.fromCharCode(10));
784
+ return 2;
785
+ }
786
+ return await withInstallSession(async () => await runProtectedOperation(options, ['remove', name], deps, { stdout, stderr }));
787
+ }
788
+ case 'update': {
789
+ const name = options.args[0];
790
+ if (name === undefined || name === '' || name.startsWith('-')) {
791
+ stderr('dshpmc update: a package name is required' + String.fromCharCode(10));
792
+ return 2;
793
+ }
794
+ // 与旧实现同一条语义:重写 specifier 到 @latest(dsh plugin add 不带版本号
795
+ // 不会升级已声明的范围)。
796
+ const spec = /@[^/]+$/.test(name) ? name : name + '@latest';
797
+ return await withInstallSession(async () => await runProtectedOperation(options, ['add', spec], deps, { stdout, stderr }));
798
+ }
799
+ case 'mount':
800
+ return await runMount(options, { stdout, stderr });
801
+ case 'list':
802
+ return await runList(options, { stdout });
803
+ case 'analyze':
804
+ return await runAnalyze(options, deps, { stdout, stderr });
805
+ case 'uninstall-kind':
806
+ return await runUninstallKind(options, { stdout, stderr });
807
+ case 'block':
808
+ case 'unblock': {
809
+ const repo = options.args[0];
810
+ if (repo === undefined || repo === '') {
811
+ stderr('dshpmc ' + options.command + ': an owner/repo reference is required' + String.fromCharCode(10));
812
+ return 2;
813
+ }
814
+ if (options.command === 'block') {
815
+ const key = await addBlockedRepo(repo);
816
+ stdout('blocked ' + key + String.fromCharCode(10));
817
+ }
818
+ else {
819
+ const { removeBlockedRepo } = await import("./kinds.js");
820
+ const removed = await removeBlockedRepo(repo);
821
+ stdout((removed ? 'unblocked ' : 'not blocked: ') + repo + String.fromCharCode(10));
822
+ }
823
+ return 0;
824
+ }
825
+ default:
826
+ stderr('dshpmc: unknown command ' + JSON.stringify(options.command) + String.fromCharCode(10) + USAGE + String.fromCharCode(10));
827
+ return 2;
828
+ }
829
+ }
830
+ catch (error) {
831
+ stderr('dshpmc: ' + (error instanceof Error ? error.message : String(error)) + String.fromCharCode(10));
832
+ return 1;
833
+ }
834
+ }
835
+ /** 读本包版本(bin 在 dist/ 下,package.json 在上一层)。 */
836
+ async function readVersion() {
837
+ try {
838
+ const { readFile } = await import('node:fs/promises');
839
+ const here = fileURLToPath(new URL('../package.json', import.meta.url));
840
+ const manifest = JSON.parse(await readFile(here, 'utf8'));
841
+ return typeof manifest.version === 'string' ? manifest.version : 'unknown';
842
+ }
843
+ catch {
844
+ return 'unknown';
845
+ }
846
+ }
847
+ /**
848
+ * 作为 bin 直接运行时的入口。
849
+ *
850
+ * 只有在被当作可执行文件跑时才设退出码(被 import 时 main 是普通函数)。
851
+ */
852
+ const invokedPath = process.argv[1];
853
+ if (invokedPath !== undefined && /(?:^|[\\/])cli\.(?:js|ts|mjs)$/.test(invokedPath)) {
854
+ const code = await main(process.argv.slice(2));
855
+ process.exitCode = code;
856
+ }