@numa-tech/numa 1.26.7 → 1.26.9
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 +13 -2
- package/dist/ai-registry/commands.js +192 -129
- package/dist/ai-registry/commands.js.map +1 -1
- package/dist/ai-registry/install-conflicts.d.ts +9 -0
- package/dist/ai-registry/install-conflicts.js +59 -0
- package/dist/ai-registry/install-conflicts.js.map +1 -0
- package/dist/ai-registry/install-defaults.js +3 -2
- package/dist/ai-registry/install-defaults.js.map +1 -1
- package/dist/cli.js +39 -28
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.js +10 -3
- package/dist/command-catalog.js.map +1 -1
- package/dist/interactive/entry.js +1 -1
- package/dist/interactive/entry.js.map +1 -1
- package/dist/interactive/prompts.js +1 -1
- package/dist/interactive/prompts.js.map +1 -1
- package/dist/interactive/registry.d.ts +15 -0
- package/dist/interactive/registry.js +51 -2
- package/dist/interactive/registry.js.map +1 -1
- package/dist/jev/client.d.ts +17 -0
- package/dist/jev/client.js +127 -0
- package/dist/jev/client.js.map +1 -0
- package/dist/jev/commands.d.ts +8 -0
- package/dist/jev/commands.js +40 -0
- package/dist/jev/commands.js.map +1 -0
- package/dist/jev/errors.d.ts +39 -0
- package/dist/jev/errors.js +48 -0
- package/dist/jev/errors.js.map +1 -0
- package/dist/jev/input.d.ts +11 -0
- package/dist/jev/input.js +126 -0
- package/dist/jev/input.js.map +1 -0
- package/dist/jev/profile.d.ts +3 -0
- package/dist/jev/profile.js +26 -0
- package/dist/jev/profile.js.map +1 -0
- package/dist/jev/response.d.ts +3 -0
- package/dist/jev/response.js +38 -0
- package/dist/jev/response.js.map +1 -0
- package/dist/kb/config.js +4 -2
- package/dist/kb/config.js.map +1 -1
- package/dist/kb/environment-display.d.ts +5 -0
- package/dist/kb/environment-display.js +7 -0
- package/dist/kb/environment-display.js.map +1 -0
- package/dist/kb/web-page.js +81 -15
- package/dist/kb/web-page.js.map +1 -1
- package/dist/kb/web-queue.d.ts +3 -0
- package/dist/kb/web-queue.js +21 -1
- package/dist/kb/web-queue.js.map +1 -1
- package/dist/kb/web.js +14 -1
- package/dist/kb/web.js.map +1 -1
- package/dist/mcp-gateway/runtime.d.ts +95 -0
- package/dist/mcp-gateway/runtime.js +25 -3
- package/dist/mcp-gateway/runtime.js.map +1 -1
- package/docs/design/kb-web-workspace.md +28 -1
- package/docs/module-maintenance-and-skills-updates.md +18 -0
- package/examples/jev/README.md +6 -2
- package/examples/oss/__pycache__/roundtrip.cpython-314.pyc +0 -0
- package/examples/oss/__pycache__/test_roundtrip.cpython-314.pyc +0 -0
- package/package.json +4 -4
- package/skills/numa-cli/SKILL.md +10 -6
- package/skills/numa-cli/evals/evals.json +27 -0
- package/skills/numa-cli/numa-skill.json +1 -1
- package/skills/numa-cli/references/authoring.md +29 -0
- package/skills/numa-cli/references/cli-bootstrap.md +3 -3
- package/skills/numa-jev/SKILL.md +11 -9
- package/skills/numa-jev/numa-skill.json +5 -2
- package/skills/numa-jev/references/application-integration.md +2 -2
- package/skills/numa-jev/references/cli-usage.md +62 -0
- package/skills/numa-jev/references/mcp-usage.md +3 -1
package/README.md
CHANGED
|
@@ -38,7 +38,15 @@ numa notion page create --grant 41 --file page.json --json
|
|
|
38
38
|
|
|
39
39
|
## Jev 企业决策(开发中)
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
`numa jev` 直接调用平台 HTTP API,复用当前平台身份。正常任务一次提交业务请求;输入、权限、可用性和结果校验由 CLI 处理,无需先查帮助、capabilities 或执行试探性推理。
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
numa jev decide --file request.json --json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
输出单个 JSON,包含 `status`、`availability` 和结果或 `next_action`。输入错误返回 `needs_input`,身份/服务问题返回 `blocked`;请求中断或响应异常返回 `pending`,可能已计费,不自动重放。`--check` 与 `capabilities` 仅供主动诊断。
|
|
48
|
+
|
|
49
|
+
目前是未发布源码,不能假定 npm latest 已包含该命令。用法见 [CLI 指引](skills/numa-jev/references/cli-usage.md),三类接入边界见 [Jev 设计](docs/design/jev-integration.md) 与 [numa-jev Skill](skills/numa-jev/SKILL.md)。平台管理上游密钥和代理;ChatGPT 远程 MCP/OAuth 为独立接入路径,CLI 不回退到 MCP。
|
|
42
50
|
|
|
43
51
|
## 显式用户授权
|
|
44
52
|
|
|
@@ -615,7 +623,10 @@ numa maintenance run --force --json
|
|
|
615
623
|
未下发默认列表时给出 profile 配置提示;不会猜测或硬编码默认客户端。
|
|
616
624
|
`skillBootstrap.enabled` 只控制登录后的自动初始化,不影响用户主动执行 install。
|
|
617
625
|
多客户端共用一次下载,同目录目标去重,已有文件保留并报告 `SKIPPED_EXISTING`;
|
|
618
|
-
|
|
626
|
+
已登记且未修改的 Skill 会提示使用更新;本地自建或修改过的同名 Skill 在交互模式下询问
|
|
627
|
+
是否备份覆盖,非交互需显式 `--force`。显式单客户端保留原有 JSON 成功结果结构。
|
|
628
|
+
安装向导支持同时勾选多个 Skills 和 Agents;命令行也可使用
|
|
629
|
+
`numa skills install numa-cli numa-notion --agent claude,codex --interactive`。
|
|
619
630
|
|
|
620
631
|
| 客户端 | Skill | MCP Server |
|
|
621
632
|
| --- | --- | --- |
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { addGatewayOptions, executeGatewayCommand, registerGatewayCommands, useGatewayMode } from "../mcp-gateway/commands.js";
|
|
2
2
|
import { appendEnvironment, collectEnvironment, parseEnvironment, safeMcpOutput } from "../mcp/environment.js";
|
|
3
|
-
import { completeRegistryInstall } from "../interactive/registry.js";
|
|
3
|
+
import { completeSkillBatchInstall, completeRegistryInstall } from "../interactive/registry.js";
|
|
4
4
|
import { terminalUi, useInteractive, requireInput, confirmInstall, InstallCancelled, chooseEnvironment } from "../interactive/prompts.js";
|
|
5
|
-
import {
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import process from "node:process";
|
|
8
8
|
import { loadAppConfig } from "../app-config.js";
|
|
9
|
+
import { requireProfileService } from "../platform-profile.js";
|
|
10
|
+
import { prepareRegistryInstanceMigration, applyRegistryInstanceMigration } from "../compatibility/index.js";
|
|
9
11
|
import { loadConfig as loadPlatformConfig } from "../config.js";
|
|
10
12
|
import { AiRegistryClient } from "./client.js";
|
|
11
13
|
import { readMcpSpecification } from "./mcp-management.js";
|
|
12
14
|
import { registerMcpRuntimeCommands } from "./mcp-runtime-commands.js";
|
|
13
15
|
import { runMcpBridge } from "./bridge.js";
|
|
14
16
|
import { bootstrapSkills } from "./bootstrap.js";
|
|
17
|
+
import { decideSkillInstall } from "./install-conflicts.js";
|
|
15
18
|
import { resolveSkillInstallTargets } from "./install-defaults.js";
|
|
16
19
|
import { chooseSkillUpdates } from "./update-prompt.js";
|
|
17
20
|
import { recordManagedSkill, updateSkillPreferences } from "./updates.js";
|
|
@@ -23,7 +26,7 @@ import { maintenanceIdentity } from "../maintenance/identity.js";
|
|
|
23
26
|
import { AiRegistryError, sanitizeRegistryOutput } from "./errors.js";
|
|
24
27
|
import { installMcpClientDefinition, mcpDefinitionFromNacos } from "./mcp-installer.js";
|
|
25
28
|
import { resolveRegistryNamespace } from "./namespace.js";
|
|
26
|
-
import { installSkillArchive, skillTargetRoot } from "./skill-installer.js";
|
|
29
|
+
import { installSkillArchive, safeSkillName, skillTargetRoot } from "./skill-installer.js";
|
|
27
30
|
import { assertConsumersCompatible, assertInstalledDependencies, confirmSkillDependencies, resolveSkillDependencies } from "./skill-dependencies.js";
|
|
28
31
|
import { assertRouterHealth, lockRegistryWorkspace, portableInstallPath, resolveVersion, routerMcpDefinition, syncRegistryWorkspace } from "./sync.js";
|
|
29
32
|
import { runRegistryTui } from "./tui.js";
|
|
@@ -275,10 +278,10 @@ export function registerAiRegistryCommands(program, dependencies = {}) {
|
|
|
275
278
|
const result = await client.getSkill(targetNamespace.id, name);
|
|
276
279
|
await output(command, { ok: true, namespace: targetNamespace, skill: result }, JSON.stringify(sanitizeRegistryOutput(result), null, 2));
|
|
277
280
|
}), ["numa skills inspect release-helper --namespace team-ai --json"]);
|
|
278
|
-
addExamples(skills.command("install [
|
|
281
|
+
addExamples(skills.command("install [names...]")
|
|
279
282
|
.option("-n, --namespace <key>", "namespaceKey")
|
|
280
283
|
.option("-i, --instance <id-or-code>", "同名 namespace 的实例消歧")
|
|
281
|
-
.option("-a, --agent <agent>", "
|
|
284
|
+
.option("-a, --agent <agent>", "逗号分隔的目标客户端,覆盖 profile 默认:chatgpt|codex|claude|cursor|workbuddy|openclaw|agents|custom")
|
|
282
285
|
.option("--scope <scope>", "user(所有工作空间)|project(当前项目)", "user")
|
|
283
286
|
.option("--version <semver>", "精确版本")
|
|
284
287
|
.option("--label <label>", "路由标签,例如 latest")
|
|
@@ -291,146 +294,191 @@ export function registerAiRegistryCommands(program, dependencies = {}) {
|
|
|
291
294
|
.option("--adopt-dependencies", "与 --with-dependencies 配合,确认接管已核验的未登记旧依赖")
|
|
292
295
|
.option("--interactive", "交互选择来源、Skill、版本、Agent 和安装范围")
|
|
293
296
|
.option("--no-interactive", "不询问;依赖安装须显式 --with-dependencies")
|
|
294
|
-
.action(async (
|
|
297
|
+
.action(async (names, options, command) => {
|
|
295
298
|
const json = jsonEnabled(command, (await loadAppConfig()).output), ui = dependencies.ui ?? terminalUi;
|
|
296
|
-
const interactive = useInteractive(options, !
|
|
299
|
+
const interactive = useInteractive(options, !names.length || !options.agent || Boolean(options.agent?.split(",").map(value => value.trim()).includes("custom") && !options.customRoot), json, dependencies.tty);
|
|
297
300
|
if (!interactive)
|
|
298
|
-
requireInput(
|
|
301
|
+
requireInput(names.length, "缺少 Skill 名称");
|
|
299
302
|
await dependencies.initializeProfile?.();
|
|
300
303
|
const client = dependencies.clientFactory?.() ?? new AiRegistryClient(loadConfig("aiRegistry"));
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const existing = await lstat(rootPath).catch((e) => { if (e.code === "ENOENT")
|
|
327
|
-
return undefined; throw e; });
|
|
328
|
-
if (existing && !options.force && !options.plan)
|
|
329
|
-
throw new AiRegistryError("Root Skill already exists; inspect local changes and explicitly approve --force before installing dependencies.", "ai_registry_install_exists");
|
|
330
|
-
const root = { ...source, identity: identity ?? "", name, version, agent, scope: targets.scope,
|
|
331
|
-
path: rootPath, workspace: options.workspace, customRoot: options.customRoot, id: "", archiveSha256: download.sha256,
|
|
332
|
-
contentDigest: "", updatedAt: 0, policy: "pinned" };
|
|
333
|
-
dependencyPlans.set(agent, { root, prepared: await prepareSkillGraph(root, dependencyNodes, client, undefined, graph.find(n => n.name === name)) });
|
|
304
|
+
const selections = interactive
|
|
305
|
+
? await completeSkillBatchInstall(names, { ...options, scope: command.getOptionValueSource("scope") === "default" ? undefined : options.scope }, client, ui)
|
|
306
|
+
: [...new Set(names)].map(name => ({ name, options }));
|
|
307
|
+
const results = [];
|
|
308
|
+
const installOne = async (name, options) => {
|
|
309
|
+
const emit = async (_command, result, textValue) => {
|
|
310
|
+
if (selections.length === 1)
|
|
311
|
+
await output(command, result, textValue);
|
|
312
|
+
else
|
|
313
|
+
results.push({ name, result, text: textValue });
|
|
314
|
+
};
|
|
315
|
+
name = safeSkillName(name);
|
|
316
|
+
const targets = await resolveSkillInstallTargets(options);
|
|
317
|
+
if (targets.scope === "project" && options.updates === "current")
|
|
318
|
+
throw new Error("Project Skills remain locked; use registry upgrade.");
|
|
319
|
+
const context = await resolveContext(client, options, "install");
|
|
320
|
+
const detail = await client.getSkill(context.namespace.id, name);
|
|
321
|
+
const trackedLabel = options.version ? undefined : options.label ?? "latest";
|
|
322
|
+
const version = resolveVersion(detail, options.version, trackedLabel, name);
|
|
323
|
+
const decisions = new Map();
|
|
324
|
+
const conflictUi = !options.plan && useInteractive(options, true, json, dependencies.tty) ? ui : undefined;
|
|
325
|
+
for (const agent of targets.agents) {
|
|
326
|
+
const target = { agent, scope: targets.scope, workspace: options.workspace, skillName: name, force: options.force };
|
|
327
|
+
decisions.set(agent, options.plan ? { force: Boolean(options.force) }
|
|
328
|
+
: await decideSkillInstall(resolve(skillTargetRoot({ ...target, customRoot: options.customRoot }), name), target, conflictUi));
|
|
334
329
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (options.plan) {
|
|
341
|
-
await output(command, { ok: true, plan }, JSON.stringify(plan, null, 2));
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
const adoptsDependencies = plan.dependencyTargets.some(p => p.packages.some(d => d.adopt));
|
|
345
|
-
// Complete arguments skip discovery, but a TTY can still confirm dependencies.
|
|
346
|
-
const promptDependencies = dependencyNodes.length > 0 && (!options.withDependencies || adoptsDependencies && !options.adoptDependencies)
|
|
347
|
-
&& useInteractive(options, true, json, dependencies.tty);
|
|
348
|
-
if (interactive || promptDependencies) {
|
|
349
|
-
if (dependencyNodes.length) {
|
|
350
|
-
ui.note(plan.dependencyTargets.map(formatSkillUpdatePlan).join("\n")
|
|
351
|
-
+ `\n${options.force ? "备份并替换" : "安装"} ${name}@${version} · ${targets.scope}\n${plan.packages.find(p => p.name === name).targets.join("\n")}`, "安装计划");
|
|
352
|
-
if (!(await ui.confirm(adoptsDependencies ? "确认接管上述旧依赖并安装此 Skill?" : "是否安装上述 Skill 及其依赖?")))
|
|
353
|
-
throw new InstallCancelled();
|
|
330
|
+
const activeAgents = targets.agents.filter(agent => !decisions.get(agent)?.skip);
|
|
331
|
+
if (!activeAgents.length) {
|
|
332
|
+
const installations = targets.agents.map(agent => ({ agent, status: "SKIPPED_EXISTING", message: decisions.get(agent).skip, updateCommand: decisions.get(agent).updateCommand }));
|
|
333
|
+
await emit(command, { ok: true, installations }, installations.map(item => `${item.agent}: ${item.message}`).join("\n"));
|
|
334
|
+
return;
|
|
354
335
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
throw new AiRegistryError("检测到未登记的旧依赖,已核验其发布版本;先查看 --plan,确认接管后使用 --with-dependencies --adopt-dependencies。", "skills_dependency_adoption_required", undefined, undefined, { plan });
|
|
364
|
-
const choice = await chooseSkillUpdates({ choice: options.updates,
|
|
365
|
-
eligible: targets.scope === "user" && !options.version && (!options.label || options.label === "latest"),
|
|
366
|
-
json: !interactive || json });
|
|
367
|
-
if (dependencyNodes.length && !identity)
|
|
368
|
-
throw new AiRegistryError("Dependency provenance needs a verified current identity; login and retry.", "skills_dependency_identity_required");
|
|
369
|
-
if (choice && !identity)
|
|
370
|
-
throw new Error("Cannot associate update settings with the current identity; run numa login and retry.");
|
|
371
|
-
const installations = [];
|
|
372
|
-
for (const agent of targets.agents) {
|
|
373
|
-
const dependencyInstallations = [];
|
|
374
|
-
try {
|
|
336
|
+
const download = await client.downloadSkill(context.namespace.id, name, { version });
|
|
337
|
+
const graph = await resolveSkillDependencies(client, context.namespace.id, { name, version, download, dependencies: [] });
|
|
338
|
+
const dependencyNodes = graph.filter(node => node.name !== name);
|
|
339
|
+
const source = { namespace: context.namespace.namespaceKey, instance: context.namespace.instanceCode,
|
|
340
|
+
namespaceId: context.namespace.id, instanceId: context.namespace.instanceId };
|
|
341
|
+
const identity = await maintenanceIdentity(loadConfig("aiRegistry"));
|
|
342
|
+
const dependencyPlans = new Map();
|
|
343
|
+
for (const agent of activeAgents) {
|
|
375
344
|
const target = { agent, scope: targets.scope, workspace: options.workspace, customRoot: options.customRoot, skillName: name };
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
345
|
+
const rootPath = resolve(skillTargetRoot(target), name);
|
|
346
|
+
await assertConsumersCompatible(name, version, rootPath);
|
|
347
|
+
if (dependencyNodes.length) {
|
|
348
|
+
const root = { ...source, identity: identity ?? "", name, version, agent, scope: targets.scope,
|
|
349
|
+
path: rootPath, workspace: options.workspace, customRoot: options.customRoot, id: "", archiveSha256: download.sha256,
|
|
350
|
+
contentDigest: "", updatedAt: 0, policy: "pinned" };
|
|
351
|
+
dependencyPlans.set(agent, { root, prepared: await prepareSkillGraph(root, dependencyNodes, client, undefined, graph.find(n => n.name === name)) });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const plan = { source, scope: targets.scope, agents: activeAgents, packages: graph.map(node => ({
|
|
355
|
+
name: node.name, version: node.version, sha256: node.download.sha256, dependencies: node.dependencies,
|
|
356
|
+
targets: activeAgents.map(agent => resolve(skillTargetRoot({ agent, scope: targets.scope, workspace: options.workspace, customRoot: options.customRoot }), node.name))
|
|
357
|
+
})), dependencyTargets: [...dependencyPlans.values()].map(p => p.prepared.plan) };
|
|
358
|
+
if (options.plan) {
|
|
359
|
+
await emit(command, { ok: true, plan }, JSON.stringify(plan, null, 2));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Validate/migrate registration before any Skill or dependency is written.
|
|
363
|
+
await registryProjectForWrite(context, options.workspace);
|
|
364
|
+
const adoptsDependencies = plan.dependencyTargets.some(p => p.packages.some(d => d.adopt));
|
|
365
|
+
// Complete arguments skip discovery, but a TTY can still confirm dependencies.
|
|
366
|
+
const promptDependencies = dependencyNodes.length > 0 && (!options.withDependencies || adoptsDependencies && !options.adoptDependencies)
|
|
367
|
+
&& useInteractive(options, true, json, dependencies.tty);
|
|
368
|
+
if (interactive || promptDependencies) {
|
|
369
|
+
if (dependencyNodes.length) {
|
|
370
|
+
ui.note(plan.dependencyTargets.map(formatSkillUpdatePlan).join("\n")
|
|
371
|
+
+ `\n${activeAgents.some(agent => decisions.get(agent)?.force) ? "备份并替换已有目录并安装" : "安装"} ${name}@${version} · ${targets.scope}\n${plan.packages.find(p => p.name === name).targets.join("\n")}`, "安装计划");
|
|
372
|
+
if (!(await ui.confirm(adoptsDependencies ? "确认接管上述旧依赖并安装此 Skill?" : "是否安装上述 Skill 及其依赖?")))
|
|
373
|
+
throw new InstallCancelled();
|
|
374
|
+
}
|
|
375
|
+
else
|
|
376
|
+
await confirmInstall(ui, { ...plan, overwriteTargets: activeAgents.filter(agent => decisions.get(agent)?.force) });
|
|
377
|
+
options.withDependencies = true;
|
|
378
|
+
options.adoptDependencies = true;
|
|
379
|
+
}
|
|
380
|
+
if (dependencyNodes.length)
|
|
381
|
+
await confirmSkillDependencies(plan, { confirmed: options.withDependencies, json: true });
|
|
382
|
+
if (adoptsDependencies && !options.adoptDependencies)
|
|
383
|
+
throw new AiRegistryError("检测到未登记的旧依赖,已核验其发布版本;先查看 --plan,确认接管后使用 --with-dependencies --adopt-dependencies。", "skills_dependency_adoption_required", undefined, undefined, { plan });
|
|
384
|
+
const choice = await chooseSkillUpdates({ choice: options.updates,
|
|
385
|
+
eligible: targets.scope === "user" && !options.version && (!options.label || options.label === "latest"),
|
|
386
|
+
json: !interactive || json });
|
|
387
|
+
if (dependencyNodes.length && !identity)
|
|
388
|
+
throw new AiRegistryError("Dependency provenance needs a verified current identity; login and retry.", "skills_dependency_identity_required");
|
|
389
|
+
if (choice && !identity)
|
|
390
|
+
throw new Error("Cannot associate update settings with the current identity; run numa login and retry.");
|
|
391
|
+
const installations = [];
|
|
392
|
+
for (const agent of targets.agents) {
|
|
393
|
+
const decision = decisions.get(agent);
|
|
394
|
+
if (decision.skip) {
|
|
395
|
+
installations.push({ agent, status: "SKIPPED_EXISTING", message: decision.skip, updateCommand: decision.updateCommand });
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const dependencyInstallations = [];
|
|
399
|
+
try {
|
|
400
|
+
const target = { agent, scope: targets.scope, workspace: options.workspace, customRoot: options.customRoot, skillName: name };
|
|
401
|
+
const dependencyPlan = dependencyPlans.get(agent);
|
|
402
|
+
if (dependencyPlan) {
|
|
403
|
+
await applySkillUpdate(dependencyPlan.prepared, dependencyPlan.root, undefined, {});
|
|
404
|
+
for (const dep of dependencyPlan.prepared.plan.packages) {
|
|
405
|
+
dependencyInstallations.push({ name: dep.name, version: dep.version, path: dep.path, backupPath: dep.backupPath,
|
|
406
|
+
status: dep.adopt ? dep.action === "REUSE" ? "ADOPTED" : "ADOPTED_UPDATED" : dep.action === "REUSE" ? "REUSED" : dep.action === "UPDATE" ? "UPDATED" : "INSTALLED" });
|
|
407
|
+
await recordSkillInstallation(context, options.workspace, { name: dep.name, version: dep.version, agent,
|
|
408
|
+
scope: targets.scope, customRoot: options.customRoot }, dep.version, dep.sha256, dep.path);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
await assertInstalledDependencies(download.bytes, resolve(skillTargetRoot(target), name), source);
|
|
412
|
+
const result = await installSkillArchive(download.bytes, {
|
|
413
|
+
dependencyPlanVerified: true,
|
|
414
|
+
agent, scope: targets.scope, skillName: name,
|
|
415
|
+
workspace: options.workspace, customRoot: options.customRoot, force: decision.force
|
|
416
|
+
});
|
|
417
|
+
const lock = await recordSkillInstallation(context, options.workspace, {
|
|
418
|
+
name, version: options.version, label: trackedLabel, agent,
|
|
419
|
+
scope: targets.scope, customRoot: options.customRoot
|
|
420
|
+
}, version, download.sha256, result.installedPath);
|
|
421
|
+
const tracking = identity ? await recordManagedSkill({ identity, name, namespace: context.namespace.namespaceKey,
|
|
422
|
+
instance: context.namespace.instanceCode, namespaceId: context.namespace.id, instanceId: context.namespace.instanceId,
|
|
423
|
+
agent, scope: targets.scope, path: result.installedPath, workspace: options.workspace, customRoot: options.customRoot,
|
|
424
|
+
version, archiveSha256: download.sha256, contentDigest: result.contentDigest,
|
|
425
|
+
preservePolicy: !choice && !options.version && (!options.label || options.label === "latest"),
|
|
426
|
+
policy: choice === "current" ? "latest" : choice === "never" ? "off"
|
|
427
|
+
: options.version || options.label && options.label !== "latest" ? "pinned" : "inherit" }) : undefined;
|
|
428
|
+
if (choice === "all" || choice === "never")
|
|
429
|
+
await updateSkillPreferences(choice);
|
|
430
|
+
if (targets.source === "explicit" && targets.agents.length === 1) {
|
|
431
|
+
await emit(command, { ok: true, namespace: context.namespace, resolvedVersion: version, dependencyPlan: plan, dependencyInstallations, download: { sha256: download.sha256, fileName: download.fileName }, installation: result, lock,
|
|
432
|
+
updateTracking: tracking ? { id: tracking.id, policy: tracking.policy, choice } : { policy: "unregistered" } }, `已安装 ${name}@${version}: ${result.installedPath}${dependencyInstallations.map(d => `\n依赖 ${d.name}@${d.version}: ${{ REUSED: "复用", INSTALLED: "已安装", UPDATED: "已升级", ADOPTED: "已接管", ADOPTED_UPDATED: "已接管并升级" }[d.status]}${d.backupPath ? `(备份:${d.backupPath})` : ""}`).join("")}${result.backupPath ? `\n原版本备份: ${result.backupPath}` : ""}\n更新策略: ${tracking?.policy ?? "未登记"}${choice ? ` (${choice})` : ""};numa skills updates status 查看。`);
|
|
433
|
+
return;
|
|
384
434
|
}
|
|
435
|
+
installations.push({ agent, status: "INSTALLED", installation: result, lock, dependencyInstallations,
|
|
436
|
+
updateTracking: tracking ? { id: tracking.id, policy: tracking.policy, choice } : { policy: "unregistered" } });
|
|
385
437
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
instance: context.namespace.instanceCode, namespaceId: context.namespace.id, instanceId: context.namespace.instanceId,
|
|
398
|
-
agent, scope: targets.scope, path: result.installedPath, workspace: options.workspace, customRoot: options.customRoot,
|
|
399
|
-
version, archiveSha256: download.sha256, contentDigest: result.contentDigest,
|
|
400
|
-
preservePolicy: !choice && !options.version && (!options.label || options.label === "latest"),
|
|
401
|
-
policy: choice === "current" ? "latest" : choice === "never" ? "off"
|
|
402
|
-
: options.version || options.label && options.label !== "latest" ? "pinned" : "inherit" }) : undefined;
|
|
403
|
-
if (choice === "all" || choice === "never")
|
|
404
|
-
await updateSkillPreferences(choice);
|
|
405
|
-
if (targets.source === "explicit") {
|
|
406
|
-
await output(command, { ok: true, namespace: context.namespace, resolvedVersion: version, dependencyPlan: plan, dependencyInstallations, download: { sha256: download.sha256, fileName: download.fileName }, installation: result, lock,
|
|
407
|
-
updateTracking: tracking ? { id: tracking.id, policy: tracking.policy, choice } : { policy: "unregistered" } }, `已安装 ${name}@${version}: ${result.installedPath}${dependencyInstallations.map(d => `\n依赖 ${d.name}@${d.version}: ${{ REUSED: "复用", INSTALLED: "已安装", UPDATED: "已升级", ADOPTED: "已接管", ADOPTED_UPDATED: "已接管并升级" }[d.status]}${d.backupPath ? `(备份:${d.backupPath})` : ""}`).join("")}${result.backupPath ? `\n原版本备份: ${result.backupPath}` : ""}\n更新策略: ${tracking?.policy ?? "未登记"}${choice ? ` (${choice})` : ""};numa skills updates status 查看。`);
|
|
408
|
-
return;
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (targets.source === "explicit" && targets.agents.length === 1) {
|
|
440
|
+
if (!dependencyInstallations.length)
|
|
441
|
+
throw error;
|
|
442
|
+
throw new AiRegistryError(error instanceof AiRegistryError ? error.message : "Skill installation failed; some dependencies may already be installed.", error instanceof AiRegistryError ? error.code : "skills_install_failed", undefined, undefined, { plan, dependencyInstallations, partial: true });
|
|
443
|
+
}
|
|
444
|
+
const existing = error instanceof AiRegistryError && error.code === "ai_registry_install_exists";
|
|
445
|
+
installations.push({ agent, status: existing ? "SKIPPED_EXISTING" : "FAILED", dependencyInstallations,
|
|
446
|
+
code: error instanceof AiRegistryError ? error.code : "skills_install_failed",
|
|
447
|
+
message: existing ? "已保留已有文件,未验证其版本;使用 --force 可备份后替换。"
|
|
448
|
+
: error instanceof AiRegistryError ? error.message : "安装失败,请检查目标目录后重试。" });
|
|
409
449
|
}
|
|
410
|
-
|
|
411
|
-
|
|
450
|
+
}
|
|
451
|
+
const ok = installations.every((item) => item.status !== "FAILED");
|
|
452
|
+
await emit(command, { ok, namespace: context.namespace, resolvedVersion: version, dependencyPlan: plan,
|
|
453
|
+
download: { sha256: download.sha256, fileName: download.fileName }, source: targets.source,
|
|
454
|
+
scope: targets.scope, installations }, `${name}@${version}(${targets.source === "profile" ? "profile 默认客户端" : "所选客户端"},${targets.scope})\n${installations.map((item) => `${item.agent}\t${item.status}\t${"installation" in item ? item.installation?.installedPath : item.message}`).join("\n")}`);
|
|
455
|
+
if (!ok)
|
|
456
|
+
process.exitCode = 1;
|
|
457
|
+
};
|
|
458
|
+
for (const selected of selections) {
|
|
459
|
+
try {
|
|
460
|
+
await installOne(selected.name, { ...options, ...selected.options, scope: selected.options.scope ?? options.scope });
|
|
412
461
|
}
|
|
413
462
|
catch (error) {
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
463
|
+
if (selections.length === 1)
|
|
464
|
+
throw error;
|
|
465
|
+
if (error instanceof InstallCancelled) {
|
|
466
|
+
results.push({ name: selected.name, result: { ok: false, code: error.code, message: error.message }, text: `${selected.name}: ${error.message}` });
|
|
467
|
+
process.exitCode = 1;
|
|
468
|
+
break;
|
|
418
469
|
}
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
message: existing ? "已保留已有文件,未验证其版本;使用 --force 可备份后替换。"
|
|
423
|
-
: error instanceof AiRegistryError ? error.message : "安装失败,请检查目标目录后重试。" });
|
|
470
|
+
const message = error instanceof AiRegistryError ? error.message : "安装失败,请检查目标目录后重试。";
|
|
471
|
+
results.push({ name: selected.name, result: { ok: false, code: error instanceof AiRegistryError ? error.code : "skills_install_failed", message }, text: `${selected.name}: ${message}` });
|
|
472
|
+
process.exitCode = 1;
|
|
424
473
|
}
|
|
425
474
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (!ok)
|
|
431
|
-
process.exitCode = 1;
|
|
475
|
+
if (selections.length > 1) {
|
|
476
|
+
const ok = results.every(item => item.result.ok !== false);
|
|
477
|
+
await output(command, { ok, results: results.map(({ name, result }) => ({ name, result })) }, results.map(item => item.text).join("\n\n"));
|
|
478
|
+
}
|
|
432
479
|
}), [
|
|
433
480
|
"numa skills install numa-cli",
|
|
481
|
+
"numa skills install numa-cli numa-notion --agent claude,codex --interactive",
|
|
434
482
|
"numa skills install release-helper --namespace team-ai --agent claude",
|
|
435
483
|
"numa skills install release-helper --namespace team-ai --instance cluster-b --agent cursor --scope user --label latest --json"
|
|
436
484
|
]);
|
|
@@ -635,6 +683,7 @@ export function registerAiRegistryCommands(program, dependencies = {}) {
|
|
|
635
683
|
await output(command, safeMcpOutput({ ok: true, plan }), JSON.stringify(safeMcpOutput(plan), null, 2));
|
|
636
684
|
return;
|
|
637
685
|
}
|
|
686
|
+
await registryProjectForWrite(context, options.workspace);
|
|
638
687
|
if (interactive)
|
|
639
688
|
await confirmInstall(ui, plan);
|
|
640
689
|
const result = await installMcpClientDefinition(name, definition, {
|
|
@@ -918,7 +967,21 @@ async function registryProjectForWrite(context, workspace) {
|
|
|
918
967
|
mcpServers: []
|
|
919
968
|
};
|
|
920
969
|
if (manifest.namespaceKey !== context.namespace.namespaceKey || (manifest.instanceCode && manifest.instanceCode !== context.namespace.instanceCode)) {
|
|
921
|
-
|
|
970
|
+
// Explicit/interactive selection bypasses project-source resolution. Still
|
|
971
|
+
// migrate a confirmed historical alias when it matches this authorized target.
|
|
972
|
+
const migration = manifest.namespaceKey === context.namespace.namespaceKey
|
|
973
|
+
? prepareRegistryInstanceMigration({
|
|
974
|
+
baseUrl: requireProfileService("aiRegistry")?.service.baseUrl,
|
|
975
|
+
namespace: manifest.namespaceKey, instance: manifest.instanceCode,
|
|
976
|
+
source: "project", capabilities: context.capabilities
|
|
977
|
+
}) : undefined;
|
|
978
|
+
if (existing && migration && migration.to === context.namespace.instanceCode) {
|
|
979
|
+
await applyRegistryInstanceMigration(existing.files, context.namespace, migration);
|
|
980
|
+
return registryProjectForWrite(context, workspace);
|
|
981
|
+
}
|
|
982
|
+
throw new AiRegistryError(`Registry manifest ${files.manifestPath} binds namespace=${manifest.namespaceKey}, instance=${manifest.instanceCode ?? "(unspecified)"}; ` +
|
|
983
|
+
`selected namespace=${context.namespace.namespaceKey}, instance=${context.namespace.instanceCode}. ` +
|
|
984
|
+
"No verified legacy migration matches; existing configuration was preserved.", "ai_registry_manifest_namespace_mismatch");
|
|
922
985
|
}
|
|
923
986
|
const lock = await readRegistryLock(files, false) ?? {
|
|
924
987
|
schemaVersion: 1,
|