agent-trellis 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -38
- package/dist/adapters/claude-code.js +3 -2
- package/dist/adapters/codex.js +3 -2
- package/dist/adapters/jsonMcp.d.ts +3 -1
- package/dist/adapters/jsonMcp.js +18 -4
- package/dist/adapters/kimi-code.d.ts +21 -0
- package/dist/adapters/kimi-code.js +107 -0
- package/dist/adapters/kiro.js +3 -2
- package/dist/adapters/mcpPlan.d.ts +6 -1
- package/dist/adapters/mcpPlan.js +106 -8
- package/dist/adapters/pi.js +22 -3
- package/dist/adapters/symlinkPlan.d.ts +7 -0
- package/dist/adapters/symlinkPlan.js +16 -7
- package/dist/cli.js +121 -21
- package/dist/commands/doctor.d.ts +7 -2
- package/dist/commands/doctor.js +38 -2
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +26 -13
- package/dist/commands/kimi.d.ts +16 -0
- package/dist/commands/kimi.js +64 -0
- package/dist/commands/manage.d.ts +39 -0
- package/dist/commands/manage.js +126 -0
- package/dist/commands/mcp.d.ts +22 -1
- package/dist/commands/mcp.js +65 -2
- package/dist/commands/mcpGateway.d.ts +6 -0
- package/dist/commands/mcpGateway.js +34 -14
- package/dist/commands/mcpImport.d.ts +27 -0
- package/dist/commands/mcpImport.js +336 -0
- package/dist/commands/memory.d.ts +5 -3
- package/dist/commands/memory.js +12 -8
- package/dist/commands/migrate.d.ts +16 -5
- package/dist/commands/migrate.js +90 -17
- package/dist/commands/onboard.d.ts +74 -2
- package/dist/commands/onboard.js +458 -71
- package/dist/commands/rollback.js +4 -1
- package/dist/commands/skill.d.ts +17 -0
- package/dist/commands/skill.js +47 -2
- package/dist/commands/sync.js +2 -0
- package/dist/core/canonical.d.ts +15 -7
- package/dist/core/canonical.js +101 -13
- package/dist/core/capabilitySelection.d.ts +45 -0
- package/dist/core/capabilitySelection.js +124 -0
- package/dist/core/types.d.ts +25 -2
- package/dist/core/types.js +7 -0
- package/dist/lib/backup.d.ts +2 -0
- package/dist/lib/backup.js +4 -0
- package/dist/lib/builtinSkills.d.ts +2 -0
- package/dist/lib/builtinSkills.js +4 -0
- package/dist/lib/cliMetadata.d.ts +1 -0
- package/dist/lib/cliMetadata.js +14 -0
- package/dist/lib/gatewayBackend.d.ts +28 -0
- package/dist/lib/gatewayBackend.js +72 -1
- package/dist/lib/installAgent.d.ts +7 -5
- package/dist/lib/installAgent.js +4 -3
- package/dist/lib/mcpConnect.js +5 -1
- package/dist/lib/mcpMigrateRead.d.ts +1 -0
- package/dist/lib/mcpMigrateRead.js +3 -0
- package/dist/lib/mcpRuntime.d.ts +46 -0
- package/dist/lib/mcpRuntime.js +185 -0
- package/dist/lib/mcpStatusProvider.d.ts +10 -0
- package/dist/lib/mcpStatusProvider.js +25 -0
- package/dist/lib/mcpToolRegistry.d.ts +18 -0
- package/dist/lib/mcpToolRegistry.js +107 -14
- package/dist/lib/memoryGraph.d.ts +1 -1
- package/dist/lib/memoryGraph.js +9 -3
- package/dist/lib/memoryProvider.d.ts +58 -0
- package/dist/lib/memoryProvider.js +175 -0
- package/dist/lib/memoryStatus.d.ts +17 -0
- package/dist/lib/memoryStatus.js +88 -0
- package/dist/lib/nativeMemory.d.ts +19 -0
- package/dist/lib/nativeMemory.js +129 -0
- package/dist/lib/runtimeControlProvider.d.ts +16 -0
- package/dist/lib/runtimeControlProvider.js +168 -0
- package/dist/lib/secretEnv.d.ts +2 -1
- package/dist/lib/secretEnv.js +6 -2
- package/dist/lib/skillProvider.d.ts +13 -0
- package/dist/lib/skillProvider.js +210 -0
- package/dist/lib/taskProvider.d.ts +7 -0
- package/dist/lib/taskProvider.js +43 -0
- package/dist/lib/taskStore.d.ts +46 -0
- package/dist/lib/taskStore.js +156 -0
- package/dist/lib/terminalPicker.d.ts +23 -49
- package/dist/lib/terminalPicker.js +53 -218
- package/dist/pi-bridge/bundle.js +475 -37
- package/dist/pi-bridge/index.js +42 -21
- package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
- package/dist/pi-bridge/schemaTranslate.js +4 -8
- package/dist/probes/kimi-code.d.ts +12 -0
- package/dist/probes/kimi-code.js +55 -0
- package/dist/sdk.d.ts +1 -1
- package/docs/architecture.md +224 -26
- package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
- package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
- package/docs/assets/trellis-mark.svg +12 -0
- package/docs/getting-started.md +328 -35
- package/docs/overview.md +131 -0
- package/docs/overview.zh-CN.md +100 -0
- package/docs/research.md +59 -0
- package/docs/roadmap.md +9 -5
- package/package.json +3 -2
- package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
- package/schema/capability-selection.example.yaml +23 -0
- package/schema/scope.example.yaml +1 -1
- package/schema/servers.example.yaml +15 -1
package/dist/commands/onboard.js
CHANGED
|
@@ -13,24 +13,32 @@
|
|
|
13
13
|
* judgment is made here.
|
|
14
14
|
*/
|
|
15
15
|
import { createInterface } from "node:readline/promises";
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
16
17
|
import { homedir } from "node:os";
|
|
17
|
-
import { writeFileSync } from "node:fs";
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
19
|
import { join } from "node:path";
|
|
19
|
-
import { canUseInteractivePicker, runMultiSelectPicker, runSingleSelectPicker } from "../lib/terminalPicker.js";
|
|
20
|
+
import { canUseInteractivePicker, runMultiSelectPicker, runSearchMultiSelectPicker, runSingleSelectPicker } from "../lib/terminalPicker.js";
|
|
20
21
|
import * as claudeCodeProbe from "../probes/claude-code.js";
|
|
21
22
|
import * as codexProbe from "../probes/codex.js";
|
|
22
23
|
import * as kiroProbe from "../probes/kiro.js";
|
|
23
24
|
import * as piProbe from "../probes/pi.js";
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
25
|
+
import * as kimiCodeProbe from "../probes/kimi-code.js";
|
|
26
|
+
import { ALL_AGENTS, capabilityDeliveryForAgent } from "../core/types.js";
|
|
27
|
+
import { loadCanonicalSource, removeServerYaml, upsertServerYaml, writeMcpModeYaml, writeMcpRoutesYaml, writeMcpRuntimeDeliveryYaml } from "../core/canonical.js";
|
|
28
|
+
import { buildCapabilityInventory, parseCapabilitySelectionFile, resolveSelectedNames, selectionContains } from "../core/capabilitySelection.js";
|
|
26
29
|
import { INSTALL_HINTS, collectInitReport } from "./init.js";
|
|
27
30
|
import { applyMigratePlan, collectMigratePlan, printPlan as printMigratePlan } from "./migrate.js";
|
|
31
|
+
import { isGatewayAgent } from "../adapters/mcpPlan.js";
|
|
32
|
+
import { isInScope } from "../core/adapter.js";
|
|
33
|
+
import { TRELLIS_RUNTIME_SKILL_NAME } from "../lib/builtinSkills.js";
|
|
34
|
+
import { discoverNativeMemory } from "../lib/nativeMemory.js";
|
|
28
35
|
import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
|
|
29
36
|
import { collectMcpSyncReport, printReport as printMcpSyncReport } from "./mcp.js";
|
|
30
37
|
import { collectSecretsAuditReport, printReport as printSecretsAuditReport } from "./secretsAudit.js";
|
|
31
38
|
import { applyMemorySync, collectMemorySyncResult, printMemorySyncResult, DEFAULT_MEMORY_SERVER_DEF, MEMORY_SERVER_NAME } from "./memory.js";
|
|
32
39
|
import { collectDoctorReport, printReport as printDoctorReport, resolveKnownHostInjected } from "./doctor.js";
|
|
33
40
|
import { openBackupSession } from "../lib/backup.js";
|
|
41
|
+
import { applyRollbackPlan, collectRollbackPlan, loadManifest } from "./rollback.js";
|
|
34
42
|
import { confirmAndInstall } from "../lib/installAgent.js";
|
|
35
43
|
import { normalizeDoctorVerdict, normalizeMcpSyncVerdict, normalizeMemorySyncVerdict, normalizeMigrateVerdict, normalizeSecretsAuditVerdict, normalizeSelfVerificationVerdict, normalizeSyncVerdict, printVerdict, } from "./onboardVerdict.js";
|
|
36
44
|
const PROBES = {
|
|
@@ -38,21 +46,78 @@ const PROBES = {
|
|
|
38
46
|
codex: (homeDir) => codexProbe.probe(homeDir),
|
|
39
47
|
kiro: (homeDir) => kiroProbe.probe(homeDir),
|
|
40
48
|
pi: (homeDir) => piProbe.probe(homeDir),
|
|
49
|
+
"kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
|
|
41
50
|
};
|
|
42
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Scores only real source-side content. The recommendation is a hint for a
|
|
53
|
+
* first-time user, never a filter: every candidate remains selectable.
|
|
54
|
+
*/
|
|
55
|
+
export function migrationSourceScore(summary) {
|
|
56
|
+
return summary.skillCount + summary.mcpServerCount + (summary.hasRealInstructions ? 1 : 0);
|
|
57
|
+
}
|
|
58
|
+
export function recommendedMigrationSource(summaries) {
|
|
59
|
+
return summaries.reduce((best, current) => {
|
|
60
|
+
if (!best || migrationSourceScore(current) > migrationSourceScore(best))
|
|
61
|
+
return current;
|
|
62
|
+
return best;
|
|
63
|
+
}, undefined)?.agent;
|
|
64
|
+
}
|
|
65
|
+
export const MCP_ROUTE_OPTIONS = [
|
|
66
|
+
{
|
|
67
|
+
mode: "gateway",
|
|
68
|
+
label: "gateway(推荐:本机统一托管) — Trellis 在本机会话中运行 MCP 入口,不需要额外部署服务",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
mode: "direct",
|
|
72
|
+
label: "direct(简单直连) — Agent 分别连接每个 MCP,适合不需要统一托管的情况",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
mode: "hub",
|
|
76
|
+
label: "hub(已有外部 Hub 时使用) — Agent 连接一个外部 HTTP MCP 服务",
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
export function interactiveMcpRouteOptions(hubConfigured) {
|
|
80
|
+
return MCP_ROUTE_OPTIONS.map((option) => option.mode === "hub" && !hubConfigured
|
|
81
|
+
? { ...option, label: "hub(暂不可选:需要先配置外部 Hub URL) — 不会自动部署 Hub", disabled: true }
|
|
82
|
+
: { ...option });
|
|
83
|
+
}
|
|
84
|
+
export async function collectOnboardSummary(homeDir = homedir(), workspaceDir = process.cwd()) {
|
|
43
85
|
return Promise.all(ALL_AGENTS.map(async (agent) => {
|
|
44
86
|
const snapshot = await PROBES[agent](homeDir);
|
|
45
87
|
if (!snapshot.present) {
|
|
46
|
-
return { agent, present: false, skillCount: 0, skillNames: [], hasRealInstructions: false, mcpServerCount: 0 };
|
|
88
|
+
return { agent, present: false, skillCount: 0, skillNames: [], hasRealInstructions: false, mcpServerCount: 0, mcpServerNames: [], nativeMemoryStatus: "unsupported", nativeMemoryCount: 0, nativeMemoryNames: [], nativeMemoryDetail: "agent is not installed" };
|
|
47
89
|
}
|
|
48
90
|
const skillNames = snapshot.skillRoots.flatMap((root) => root.skills.map((s) => s.name));
|
|
49
91
|
const hasRealInstructions = snapshot.instructionsFile !== undefined && !snapshot.instructionsFile.isSymlink;
|
|
50
92
|
const mcpPlan = await collectMigratePlan(agent, homeDir, ["mcp"]);
|
|
51
|
-
|
|
93
|
+
const nativeMemory = discoverNativeMemory(agent, homeDir, workspaceDir);
|
|
94
|
+
return { agent, present: true, skillCount: skillNames.length, skillNames, hasRealInstructions, mcpServerCount: mcpPlan.items.length, mcpServerNames: mcpPlan.items.map((item) => item.name), nativeMemoryStatus: nativeMemory.status, nativeMemoryCount: nativeMemory.candidates.length, nativeMemoryNames: nativeMemory.candidates.map((candidate) => candidate.displayName), nativeMemoryDetail: nativeMemory.detail };
|
|
52
95
|
}));
|
|
53
96
|
}
|
|
54
97
|
function hasContent(s) {
|
|
55
|
-
return s.skillCount > 0 || s.hasRealInstructions || s.mcpServerCount > 0;
|
|
98
|
+
return s.skillCount > 0 || s.hasRealInstructions || s.mcpServerCount > 0 || s.nativeMemoryCount > 0;
|
|
99
|
+
}
|
|
100
|
+
function hasSelectedSourceContent(s, selection) {
|
|
101
|
+
if (!selection)
|
|
102
|
+
return hasContent(s);
|
|
103
|
+
const hasSkills = selection.skills !== "none" && s.skillNames.some((name) => selectionContains(selection.skills, name));
|
|
104
|
+
const hasMcp = selection.mcpServers !== "none" && s.mcpServerNames.some((name) => selectionContains(selection.mcpServers, name));
|
|
105
|
+
const hasMemory = selection.memoryMigration !== "none" && s.nativeMemoryCount > 0;
|
|
106
|
+
return hasSkills || hasMcp || hasMemory || s.hasRealInstructions;
|
|
107
|
+
}
|
|
108
|
+
function filterMigratePlan(plan, selection) {
|
|
109
|
+
if (!selection)
|
|
110
|
+
return plan;
|
|
111
|
+
const items = plan.items.filter((item) => {
|
|
112
|
+
if (item.kind === "skill")
|
|
113
|
+
return selectionContains(selection.skills, item.name);
|
|
114
|
+
if (item.kind === "mcp")
|
|
115
|
+
return selectionContains(selection.mcpServers, item.name);
|
|
116
|
+
if (item.kind === "memory")
|
|
117
|
+
return selection.memoryMigration !== "none";
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
return { ...plan, items };
|
|
56
121
|
}
|
|
57
122
|
const PROGRESS_STAGES = ["migrate", "sync", "mcp sync", "memory sync", "secrets audit", "doctor"];
|
|
58
123
|
/** Transient status, not part of the report — printed to stderr so
|
|
@@ -74,18 +139,21 @@ function logProgress(opts, stage) {
|
|
|
74
139
|
* (found via a real mirasim terminal session). The full names are still
|
|
75
140
|
* available from `trellis doctor`/`--json`; a picker row's job is to
|
|
76
141
|
* let you tell agents apart at a glance, not enumerate everything. */
|
|
77
|
-
function agentSummaryLabel(s) {
|
|
78
|
-
|
|
142
|
+
function agentSummaryLabel(s, recommendedAgent) {
|
|
143
|
+
const recommendation = s.agent === recommendedAgent ? "(推荐:可迁移内容最多)" : "";
|
|
144
|
+
const memory = s.nativeMemoryCount > 0 ? `Memory: ${s.nativeMemoryCount}` : `Memory: ${s.nativeMemoryStatus}`;
|
|
145
|
+
return `${s.agent} — Skills: ${s.skillCount}, global instructions: ${s.hasRealInstructions ? "yes" : "no"}, MCP: ${s.mcpServerCount}, ${memory} ${recommendation}`;
|
|
79
146
|
}
|
|
80
147
|
/** Numbered-typing fallback (trellis-onboard-interactive-picker design.md
|
|
81
148
|
* D2) — used only when the terminal can't support the raw-mode picker
|
|
82
149
|
* (`canUseInteractivePicker()` false). Unchanged from before that change. */
|
|
83
150
|
async function promptForAgentNumbered(present) {
|
|
84
|
-
const rl = createInterface({ input: process.stdin, output: process.
|
|
151
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
152
|
+
const recommendedAgent = recommendedMigrationSource(present);
|
|
85
153
|
try {
|
|
86
|
-
console.
|
|
154
|
+
console.error("检测到多个 Agent。推荐选择可迁移内容最多的 Agent 作为来源:");
|
|
87
155
|
present.forEach((s, i) => {
|
|
88
|
-
console.
|
|
156
|
+
console.error(` ${i + 1}) ${agentSummaryLabel(s, recommendedAgent)}`);
|
|
89
157
|
});
|
|
90
158
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
91
159
|
const answer = (await rl.question(`Choose a migration source [1-${present.length}]: `)).trim();
|
|
@@ -97,7 +165,7 @@ async function promptForAgentNumbered(present) {
|
|
|
97
165
|
return byIndex.agent;
|
|
98
166
|
if (present.some((s) => s.agent === answer))
|
|
99
167
|
return answer;
|
|
100
|
-
console.
|
|
168
|
+
console.error(`Not a valid choice: enter a number from 1-${present.length}, or one of ${present.map((s) => s.agent).join(", ")}`);
|
|
101
169
|
}
|
|
102
170
|
throw new Error("no valid migration source chosen after 2 attempts");
|
|
103
171
|
}
|
|
@@ -116,10 +184,11 @@ async function promptForAgentReal(present) {
|
|
|
116
184
|
if (!canUseInteractivePicker()) {
|
|
117
185
|
return promptForAgentNumbered(present);
|
|
118
186
|
}
|
|
119
|
-
|
|
120
|
-
|
|
187
|
+
const recommendedAgent = recommendedMigrationSource(present);
|
|
188
|
+
console.error("选择迁移来源。推荐选择可迁移内容最多的 Agent;这只决定从哪里读取,不会自动托管它:");
|
|
189
|
+
const index = await runSingleSelectPicker(present.map((summary) => agentSummaryLabel(summary, recommendedAgent)), undefined, "迁移来源");
|
|
121
190
|
if (index === null) {
|
|
122
|
-
console.
|
|
191
|
+
console.error("cancelled, no changes made");
|
|
123
192
|
process.exit(1);
|
|
124
193
|
}
|
|
125
194
|
return present[index].agent;
|
|
@@ -127,13 +196,14 @@ async function promptForAgentReal(present) {
|
|
|
127
196
|
/** Numbered-typing fallback — unchanged from before this change (see
|
|
128
197
|
* `promptForAgentNumbered`'s doc comment). */
|
|
129
198
|
async function promptForManagedAgentsNumbered(candidates, alreadyManaged) {
|
|
130
|
-
const rl = createInterface({ input: process.stdin, output: process.
|
|
199
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
131
200
|
try {
|
|
132
|
-
console.
|
|
201
|
+
console.error("选择 Trellis 要托管的 Agent(推荐选择已安装的 Agent;直接回车表示本次不新增):");
|
|
133
202
|
candidates.forEach((s, i) => {
|
|
134
203
|
const status = s.present ? `present, ${s.skillCount} skill(s)` : "not installed";
|
|
135
204
|
const tag = alreadyManaged.includes(s.agent) ? " [already managed]" : "";
|
|
136
|
-
|
|
205
|
+
const recommendation = s.present ? "(推荐:已安装)" : "";
|
|
206
|
+
console.error(` ${i + 1}) ${s.agent} — ${status}${recommendation}${tag}`);
|
|
137
207
|
});
|
|
138
208
|
const answer = (await rl.question("Select: ")).trim();
|
|
139
209
|
return answer;
|
|
@@ -152,21 +222,23 @@ async function promptForManagedAgentsReal(candidates, alreadyManaged) {
|
|
|
152
222
|
if (!canUseInteractivePicker()) {
|
|
153
223
|
return promptForManagedAgentsNumbered(candidates, alreadyManaged);
|
|
154
224
|
}
|
|
155
|
-
console.
|
|
225
|
+
console.error("选择 Trellis 要托管的 Agent。推荐选择已安装的 Agent;选择未安装的 Agent 会先请求安装确认:");
|
|
156
226
|
const labels = candidates.map((s) => {
|
|
157
227
|
const status = s.present ? `present, ${s.skillCount} skill(s)` : "not installed";
|
|
158
|
-
|
|
228
|
+
const recommendation = s.present ? "(推荐:已安装)" : "";
|
|
229
|
+
const managed = alreadyManaged.includes(s.agent) ? "(已托管)" : "";
|
|
230
|
+
return `${s.agent} — ${status}${recommendation}${managed}`;
|
|
159
231
|
});
|
|
160
232
|
const initiallyChecked = candidates.map((s) => alreadyManaged.includes(s.agent));
|
|
161
233
|
const indices = await runMultiSelectPicker(labels, initiallyChecked);
|
|
162
234
|
if (indices === null) {
|
|
163
|
-
console.
|
|
235
|
+
console.error("cancelled, no changes made");
|
|
164
236
|
process.exit(1);
|
|
165
237
|
}
|
|
166
238
|
return indices.map((i) => candidates[i].agent).join(",");
|
|
167
239
|
}
|
|
168
240
|
/**
|
|
169
|
-
* Resolves which categories (
|
|
241
|
+
* Resolves which categories (Skills, instructions, MCP, native Memory) to migrate from a
|
|
170
242
|
* resolved source (trellis-migrate-category-selection, extended to a
|
|
171
243
|
* third category by trellis-onboard-mcp-memory design.md D2). Unlike the
|
|
172
244
|
* two pickers above, there is no numbered-text fallback to preserve
|
|
@@ -178,17 +250,32 @@ async function promptForManagedAgentsReal(candidates, alreadyManaged) {
|
|
|
178
250
|
* An empty result is a valid answer: "skip migrate for this run"
|
|
179
251
|
* (design.md D6), left for the caller to act on.
|
|
180
252
|
*/
|
|
181
|
-
async function resolveMigrateCategories(source, opts) {
|
|
253
|
+
async function resolveMigrateCategories(source, opts, selection) {
|
|
182
254
|
// Built dynamically, in skill/instructions/mcp display order — not a
|
|
183
|
-
// fixed
|
|
255
|
+
// fixed category structure, so future categories
|
|
184
256
|
// would only need an entry here, not a rewritten branch (design.md D2).
|
|
185
257
|
const candidates = [];
|
|
186
|
-
if (source.skillCount > 0)
|
|
187
|
-
candidates.push({ kind: "skill", label: "
|
|
258
|
+
if (source.skillCount > 0 && (!selection || selection.skills !== "none") && (!selection || source.skillNames.some((name) => selectionContains(selection.skills, name)))) {
|
|
259
|
+
candidates.push({ kind: "skill", label: "Skills" });
|
|
260
|
+
}
|
|
188
261
|
if (source.hasRealInstructions)
|
|
189
|
-
candidates.push({ kind: "instructions", label: "instructions" });
|
|
190
|
-
if (source.mcpServerCount > 0)
|
|
191
|
-
candidates.push({ kind: "mcp", label: "
|
|
262
|
+
candidates.push({ kind: "instructions", label: "global instructions" });
|
|
263
|
+
if (source.mcpServerCount > 0 && (!selection || selection.mcpServers !== "none") && (!selection || source.mcpServerNames.some((name) => selectionContains(selection.mcpServers, name)))) {
|
|
264
|
+
candidates.push({ kind: "mcp", label: "MCP servers" });
|
|
265
|
+
}
|
|
266
|
+
let migrateMemory = source.nativeMemoryCount > 0 && opts.memoryMigrate !== "off" && (!selection || selection.memoryMigration !== "none");
|
|
267
|
+
if (migrateMemory && opts.memoryMigrate === undefined && !opts.selectionFile && !opts.json && (opts.promptForMemoryMigration || canUseInteractivePicker())) {
|
|
268
|
+
const choice = opts.promptForMemoryMigration
|
|
269
|
+
? await opts.promptForMemoryMigration(source.agent, source.nativeMemoryCount)
|
|
270
|
+
: await runSingleSelectPicker([
|
|
271
|
+
`迁移支持的记忆(推荐:${source.nativeMemoryCount} 项)`,
|
|
272
|
+
"暂不迁移记忆",
|
|
273
|
+
], undefined, `选择是否从 ${source.agent} 迁移已有记忆`);
|
|
274
|
+
if (choice === null || choice === "off" || choice === 1)
|
|
275
|
+
migrateMemory = false;
|
|
276
|
+
}
|
|
277
|
+
if (migrateMemory)
|
|
278
|
+
candidates.push({ kind: "memory", label: `Memory(${source.nativeMemoryCount} 项)` });
|
|
192
279
|
// The choice is only meaningful when two or more kinds are real — same
|
|
193
280
|
// gate for the injected test seam as for the real picker, mirroring
|
|
194
281
|
// how `promptForAgent`/`promptForManagedAgents` are only ever
|
|
@@ -198,10 +285,10 @@ async function resolveMigrateCategories(source, opts) {
|
|
|
198
285
|
return opts.promptForMigrateCategories(source);
|
|
199
286
|
}
|
|
200
287
|
if (canUseInteractivePicker()) {
|
|
201
|
-
console.
|
|
202
|
-
const indices = await runMultiSelectPicker(candidates.map((c) => c.label), candidates.map(() => true));
|
|
288
|
+
console.error(`选择要从 ${source.agent} 迁移的内容(默认全选,推荐先完整迁移,之后再清理不需要的项):`);
|
|
289
|
+
const indices = await runMultiSelectPicker(candidates.map((c) => `${c.label}(推荐迁移)`), candidates.map(() => true), undefined, "迁移内容");
|
|
203
290
|
if (indices === null) {
|
|
204
|
-
console.
|
|
291
|
+
console.error("cancelled, no changes made");
|
|
205
292
|
process.exit(1);
|
|
206
293
|
}
|
|
207
294
|
return indices.map((i) => candidates[i].kind);
|
|
@@ -209,6 +296,75 @@ async function resolveMigrateCategories(source, opts) {
|
|
|
209
296
|
}
|
|
210
297
|
return candidates.map((c) => c.kind);
|
|
211
298
|
}
|
|
299
|
+
async function resolveInteractiveCapabilitySelection(source, opts) {
|
|
300
|
+
if (opts.json || opts.promptForMigrateCategories || !canUseInteractivePicker())
|
|
301
|
+
return undefined;
|
|
302
|
+
const skills = source.skillNames.length > 0
|
|
303
|
+
? await runSearchMultiSelectPicker(source.skillNames, undefined, "选择要迁移的 Skills(默认全选,推荐先全部保留)")
|
|
304
|
+
: [];
|
|
305
|
+
if (skills === null) {
|
|
306
|
+
console.error("cancelled, no changes made");
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
const mcpServers = source.mcpServerNames.length > 0
|
|
310
|
+
? await runSearchMultiSelectPicker(source.mcpServerNames, undefined, "选择要迁移的 MCP(默认全选,推荐先全部保留)")
|
|
311
|
+
: [];
|
|
312
|
+
if (mcpServers === null) {
|
|
313
|
+
console.error("cancelled, no changes made");
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
return { skills: skills ?? "none", mcpServers: mcpServers ?? "none", memories: "all", memoryMigration: "all", mcpRoutes: {}, runtimeDelivery: {} };
|
|
317
|
+
}
|
|
318
|
+
async function resolveInteractiveMemoryChoice(homeDir, opts) {
|
|
319
|
+
if (opts.memory !== undefined || opts.selectionFile || opts.json)
|
|
320
|
+
return undefined;
|
|
321
|
+
const current = loadCanonicalSource(homeDir).mcp.servers[MEMORY_SERVER_NAME] ? "on" : "off";
|
|
322
|
+
const canPrompt = Boolean(opts.promptForMemory) || (opts.isTTY ?? process.stdin.isTTY === true) && canUseInteractivePicker();
|
|
323
|
+
if (!canPrompt)
|
|
324
|
+
return undefined;
|
|
325
|
+
const choice = opts.promptForMemory
|
|
326
|
+
? await opts.promptForMemory(current)
|
|
327
|
+
: (() => {
|
|
328
|
+
const options = current === "on"
|
|
329
|
+
? ["保持开启(推荐:继续使用共享 Memory)", "关闭共享 Memory"]
|
|
330
|
+
: ["保持关闭(推荐:不改变现有环境)", "启用共享 Memory(推荐:Kimi/Pi 共享同一 graph)"];
|
|
331
|
+
return runSingleSelectPicker(options, undefined, "选择 Trellis Memory 托管方式").then((index) => {
|
|
332
|
+
if (index === null) {
|
|
333
|
+
console.error("cancelled, no changes made");
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
if (index === 0)
|
|
337
|
+
return "keep";
|
|
338
|
+
return current === "on" ? "off" : "on";
|
|
339
|
+
});
|
|
340
|
+
})();
|
|
341
|
+
if (choice === "on" || choice === "off")
|
|
342
|
+
return choice;
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
async function resolveInteractiveMcpRoutes(agentIds, serverNames, hubConfigured) {
|
|
346
|
+
const routes = {};
|
|
347
|
+
for (const agent of agentIds) {
|
|
348
|
+
const options = interactiveMcpRouteOptions(hubConfigured);
|
|
349
|
+
const modeIndex = await runSingleSelectPicker(options, undefined, `选择 ${agent} 的 MCP 模式(推荐 Gateway:Trellis 本机统一托管;Hub 只用于已有外部服务)`);
|
|
350
|
+
if (modeIndex === null) {
|
|
351
|
+
console.error("cancelled, no changes made");
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
const mode = options[modeIndex].mode;
|
|
355
|
+
if (mode === "hub") {
|
|
356
|
+
routes[agent] = { mode };
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const selected = await runSearchMultiSelectPicker(serverNames, undefined, `选择 ${agent} 的 MCP 服务器(默认全选,推荐先全部启用)`);
|
|
360
|
+
if (selected === null) {
|
|
361
|
+
console.error("cancelled, no changes made");
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
routes[agent] = { mode, servers: selected ?? [] };
|
|
365
|
+
}
|
|
366
|
+
return routes;
|
|
367
|
+
}
|
|
212
368
|
/** Shared by `--manage` and the interactive prompt's answer — same
|
|
213
369
|
* grammar either way (design.md D4): a real agent id list, comma- or
|
|
214
370
|
* whitespace-separated numbers referring to `candidates`' own order, or
|
|
@@ -300,6 +456,38 @@ function validateMemoryOption(memory) {
|
|
|
300
456
|
return { error: `--memory must be "on" or "off" (got "${memory}")` };
|
|
301
457
|
return { value: memory };
|
|
302
458
|
}
|
|
459
|
+
function validateMemoryMigrationOption(memoryMigrate) {
|
|
460
|
+
if (memoryMigrate === undefined)
|
|
461
|
+
return {};
|
|
462
|
+
if (memoryMigrate !== "on" && memoryMigrate !== "off")
|
|
463
|
+
return { error: `--memory-migrate must be "on" or "off" (got "${memoryMigrate}")` };
|
|
464
|
+
return { value: memoryMigrate };
|
|
465
|
+
}
|
|
466
|
+
function preflightMcpMode(mode) {
|
|
467
|
+
if (!mode)
|
|
468
|
+
return undefined;
|
|
469
|
+
if (mode.kind === "hub") {
|
|
470
|
+
try {
|
|
471
|
+
const parsed = new URL(mode.url);
|
|
472
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
473
|
+
return `Hub URL must use http:// or https:// (got "${mode.url}")`;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return `Hub URL is not valid: "${mode.url}"`;
|
|
478
|
+
}
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
if (mode.kind === "gateway") {
|
|
482
|
+
try {
|
|
483
|
+
execFileSync("which", ["trellis"], { stdio: "ignore" });
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return "gateway mode requires the `trellis` executable to be available on PATH before any Agent config is changed";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
303
491
|
function mcpModeValueOf(mcp) {
|
|
304
492
|
if (mcp.gateway?.enabled)
|
|
305
493
|
return "gateway";
|
|
@@ -348,6 +536,49 @@ function memoryStatusLine(memory) {
|
|
|
348
536
|
return `memory: ${memory.current} (unchanged) — pass --memory ${memory.current === "on" ? "off" : "on"} to change`;
|
|
349
537
|
return `memory: ${memory.current} (changed from ${memory.previous})`;
|
|
350
538
|
}
|
|
539
|
+
/** Converges explicit per-Agent route lists with the shared Memory toggle.
|
|
540
|
+
* Legacy/top-level routes without a `servers` list already expose every
|
|
541
|
+
* eligible server and need no rewrite. Hub routes are intentionally opaque:
|
|
542
|
+
* the external hub owns its upstream set. */
|
|
543
|
+
export function mergeMemoryIntoExplicitRoutes(mcp, managedAgents, memoryEnabled) {
|
|
544
|
+
const routes = mcp.routes;
|
|
545
|
+
if (!routes)
|
|
546
|
+
return { mcp, changed: false };
|
|
547
|
+
const memoryServer = mcp.servers[MEMORY_SERVER_NAME];
|
|
548
|
+
const changedRoutes = { ...routes };
|
|
549
|
+
let changed = false;
|
|
550
|
+
for (const agent of managedAgents) {
|
|
551
|
+
const route = routes[agent];
|
|
552
|
+
if (!route || route.mode === "hub" || !route.servers)
|
|
553
|
+
continue;
|
|
554
|
+
const inScope = Boolean(memoryServer && memoryServer.enabled !== false && isInScope(agent, memoryServer.agents, managedAgents));
|
|
555
|
+
const nextServers = memoryEnabled && inScope
|
|
556
|
+
? [...route.servers, ...(route.servers.includes(MEMORY_SERVER_NAME) ? [] : [MEMORY_SERVER_NAME])]
|
|
557
|
+
: route.servers.filter((name) => name !== MEMORY_SERVER_NAME);
|
|
558
|
+
if (nextServers.length === route.servers.length && nextServers.every((name, index) => name === route.servers[index]))
|
|
559
|
+
continue;
|
|
560
|
+
changedRoutes[agent] = { ...route, servers: nextServers };
|
|
561
|
+
changed = true;
|
|
562
|
+
}
|
|
563
|
+
return changed ? { mcp: { ...mcp, routes: changedRoutes }, changed } : { mcp, changed: false };
|
|
564
|
+
}
|
|
565
|
+
async function confirmMcpModeChange(previous, next, agents, opts) {
|
|
566
|
+
if (opts.promptForModeChange)
|
|
567
|
+
return opts.promptForModeChange(previous, next, agents);
|
|
568
|
+
if (!canUseInteractivePicker())
|
|
569
|
+
return true;
|
|
570
|
+
const affected = agents.length > 0 ? agents.join(", ") : "(none)";
|
|
571
|
+
console.error(`MCP 模式将从 ${previous} 切换到 ${next},影响托管 Agent:${affected}`);
|
|
572
|
+
if (next === "hub")
|
|
573
|
+
console.error("Hub 模式会让 Agent 连接外部 HTTP MCP 服务;Trellis 不会自动部署该服务。");
|
|
574
|
+
if (next === "gateway")
|
|
575
|
+
console.error("Gateway 模式会由 Trellis 在本机运行 MCP 入口并托管上游连接。");
|
|
576
|
+
const choice = await runSingleSelectPicker([
|
|
577
|
+
"取消切换(推荐:保持当前模式)",
|
|
578
|
+
"确认切换",
|
|
579
|
+
], undefined, "确认 MCP 模式切换");
|
|
580
|
+
return choice === 1;
|
|
581
|
+
}
|
|
351
582
|
async function resolveManagedAgents(opts, summary, alreadyManaged) {
|
|
352
583
|
if (opts.manage !== undefined) {
|
|
353
584
|
const parsed = parseManagedSelection(opts.manage, summary);
|
|
@@ -369,13 +600,35 @@ async function resolveManagedAgents(opts, summary, alreadyManaged) {
|
|
|
369
600
|
function readManagedYaml(homeDir) {
|
|
370
601
|
return loadCanonicalSource(homeDir).managedAgents;
|
|
371
602
|
}
|
|
372
|
-
function writeManagedYaml(homeDir, agents) {
|
|
373
|
-
|
|
603
|
+
function writeManagedYaml(homeDir, agents, backup) {
|
|
604
|
+
const content = `agents: [${agents.join(", ")}]\n`;
|
|
605
|
+
const path = join(homeDir, ".trellis", "managed.yaml");
|
|
606
|
+
if (existsSync(path) && readFileSync(path, "utf-8") === content)
|
|
607
|
+
return;
|
|
608
|
+
if (backup)
|
|
609
|
+
backup.writeFile(path, content);
|
|
610
|
+
else
|
|
611
|
+
writeFileSync(path, content);
|
|
374
612
|
}
|
|
375
613
|
export async function collectOnboardPlan(opts = {}) {
|
|
376
614
|
const homeDir = opts.homeDir ?? homedir();
|
|
377
|
-
|
|
378
|
-
|
|
615
|
+
if (opts.dryRun && !existsSync(join(homeDir, ".trellis"))) {
|
|
616
|
+
return {
|
|
617
|
+
summary: [],
|
|
618
|
+
refusal: "dry-run does not bootstrap a missing ~/.trellis — run `trellis init` first, then re-run the preview",
|
|
619
|
+
verdict: [],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
let capabilitySelection;
|
|
623
|
+
if (opts.selectionFile) {
|
|
624
|
+
const parsedSelection = parseCapabilitySelectionFile(opts.selectionFile);
|
|
625
|
+
if (!parsedSelection.ok)
|
|
626
|
+
return { summary: [], refusal: parsedSelection.error, verdict: [] };
|
|
627
|
+
capabilitySelection = parsedSelection.selection;
|
|
628
|
+
}
|
|
629
|
+
await collectInitReport(homeDir, { write: !opts.dryRun });
|
|
630
|
+
const workspaceDir = opts.workspaceDir ?? process.cwd();
|
|
631
|
+
const summary = await collectOnboardSummary(homeDir, workspaceDir);
|
|
379
632
|
const present = summary.filter((s) => s.present);
|
|
380
633
|
if (present.length === 0) {
|
|
381
634
|
return { summary, installHints: { ...INSTALL_HINTS }, verdict: [] };
|
|
@@ -390,11 +643,19 @@ export async function collectOnboardPlan(opts = {}) {
|
|
|
390
643
|
if ("error" in mcpModeValidation) {
|
|
391
644
|
return { summary, refusal: mcpModeValidation.error, verdict: [] };
|
|
392
645
|
}
|
|
393
|
-
const
|
|
394
|
-
if ("error" in
|
|
395
|
-
return { summary, refusal:
|
|
646
|
+
const explicitMemoryValidation = validateMemoryOption(opts.memory);
|
|
647
|
+
if ("error" in explicitMemoryValidation) {
|
|
648
|
+
return { summary, refusal: explicitMemoryValidation.error, verdict: [] };
|
|
649
|
+
}
|
|
650
|
+
const explicitMemoryMigrationValidation = validateMemoryMigrationOption(opts.memoryMigrate);
|
|
651
|
+
if ("error" in explicitMemoryMigrationValidation) {
|
|
652
|
+
return { summary, refusal: explicitMemoryMigrationValidation.error, verdict: [] };
|
|
396
653
|
}
|
|
397
|
-
const
|
|
654
|
+
const modePreflightError = preflightMcpMode(mcpModeValidation.mode);
|
|
655
|
+
if (modePreflightError) {
|
|
656
|
+
return { summary, refusal: modePreflightError, verdict: [] };
|
|
657
|
+
}
|
|
658
|
+
const sourceCandidates = present.filter((candidate) => hasSelectedSourceContent(candidate, capabilitySelection));
|
|
398
659
|
let source;
|
|
399
660
|
let sourceReason;
|
|
400
661
|
if (opts.agent) {
|
|
@@ -470,35 +731,65 @@ export async function collectOnboardPlan(opts = {}) {
|
|
|
470
731
|
// managed set, not an abort of the rest of the flow.
|
|
471
732
|
}
|
|
472
733
|
const managedAgents = [...new Set([...alreadyManaged, ...resolvedNew])];
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
// MCP mode + memory-server resolution (design.md D5/D11) — grouped
|
|
477
|
-
// with managed-set above as "how is this machine configured"
|
|
478
|
-
// decisions resolved once per run, ahead of migrate/sync/mcp-sync/
|
|
479
|
-
// memory-sync below, which act on the result. Both flags were already
|
|
480
|
-
// validated before managed-set was even touched; omitting either
|
|
481
|
-
// resolves to "no change requested" here too (design.md D1/D12) — no
|
|
482
|
-
// writer call at all, on either a first run or a later one.
|
|
734
|
+
// Resolve all mode/memory decisions before opening the transaction. A
|
|
735
|
+
// refusal here must leave both canonical and managed state untouched.
|
|
483
736
|
const canonicalForModeAndMemory = loadCanonicalSource(homeDir);
|
|
737
|
+
const interactiveMemory = await resolveInteractiveMemoryChoice(homeDir, opts);
|
|
738
|
+
const memoryValue = explicitMemoryValidation.value ?? interactiveMemory;
|
|
739
|
+
const memoryValidation = { value: memoryValue };
|
|
740
|
+
const sourceSummaryForInventory = source ? summary.find((item) => item.agent === source) : undefined;
|
|
741
|
+
const nativeMemory = sourceSummaryForInventory && sourceSummaryForInventory.nativeMemoryStatus !== "supported"
|
|
742
|
+
? [{ name: `${source}:native`, detail: sourceSummaryForInventory.nativeMemoryDetail }]
|
|
743
|
+
: [];
|
|
744
|
+
const inventory = sourceSummaryForInventory
|
|
745
|
+
? buildCapabilityInventory(sourceSummaryForInventory, canonicalForModeAndMemory, nativeMemory)
|
|
746
|
+
: undefined;
|
|
747
|
+
const capabilitySummary = {
|
|
748
|
+
awarenessSkill: canonicalForModeAndMemory.skills.some((skill) => skill.name === TRELLIS_RUNTIME_SKILL_NAME),
|
|
749
|
+
globalInstructions: { available: existsSync(canonicalForModeAndMemory.instructionsFile), native: true, runtimeReadable: true },
|
|
750
|
+
skills: canonicalForModeAndMemory.skills.length,
|
|
751
|
+
memories: canonicalForModeAndMemory.memories.length,
|
|
752
|
+
mcpServers: Object.keys(canonicalForModeAndMemory.mcp.servers).length,
|
|
753
|
+
};
|
|
754
|
+
const managedAgentRuntime = managedAgents.map((agent) => ({
|
|
755
|
+
agent,
|
|
756
|
+
delivery: capabilityDeliveryForAgent(agent, canonicalForModeAndMemory.mcp),
|
|
757
|
+
route: canonicalForModeAndMemory.mcp.routes?.[agent]?.mode
|
|
758
|
+
?? (isGatewayAgent(agent, canonicalForModeAndMemory.mcp, managedAgents) ? "gateway" : canonicalForModeAndMemory.mcp.hub ? "hub" : "direct"),
|
|
759
|
+
}));
|
|
484
760
|
const mcpModeResolution = resolveMcpModeChange(mcpModeValidation.mode, mcpModeValueOf(canonicalForModeAndMemory.mcp));
|
|
485
761
|
const mcpMode = { current: mcpModeResolution.current, previous: mcpModeResolution.previous, changed: mcpModeResolution.changed };
|
|
486
762
|
const serversYamlPath = join(homeDir, ".trellis", "mcp", "servers.yaml");
|
|
487
|
-
if (mcpModeResolution.write && !opts.dryRun) {
|
|
488
|
-
writeMcpModeYaml(serversYamlPath, mcpModeResolution.write);
|
|
489
|
-
}
|
|
490
763
|
const memoryConfiguredBefore = canonicalForModeAndMemory.mcp.servers[MEMORY_SERVER_NAME] !== undefined;
|
|
491
764
|
const memoryResolution = resolveMemoryToggle(memoryValidation.value, memoryConfiguredBefore, canonicalForModeAndMemory.mcp.knownHostInjected);
|
|
492
765
|
if ("refusal" in memoryResolution) {
|
|
493
766
|
return { summary, source, sourceReason, managedAgents, mcpMode, refusal: memoryResolution.refusal, verdict: [] };
|
|
494
767
|
}
|
|
495
768
|
const memory = { current: memoryResolution.current, previous: memoryResolution.previous, changed: memoryResolution.changed };
|
|
769
|
+
if (!opts.dryRun
|
|
770
|
+
&& !opts.json
|
|
771
|
+
&& mcpMode.changed
|
|
772
|
+
&& (opts.isTTY ?? process.stdin.isTTY === true)
|
|
773
|
+
&& (opts.promptForModeChange || canUseInteractivePicker())) {
|
|
774
|
+
const accepted = await confirmMcpModeChange(mcpMode.previous, mcpMode.current, managedAgents, opts);
|
|
775
|
+
if (!accepted)
|
|
776
|
+
return { summary, source, sourceReason, managedAgents, mcpMode, memory, refusal: "MCP mode change cancelled — current mode was preserved", verdict: [] };
|
|
777
|
+
}
|
|
778
|
+
// One transaction for the entire onboarding run, including canonical
|
|
779
|
+
// state. A dry run opens no session and a no-op session creates no backup.
|
|
780
|
+
const backupSession = opts.dryRun ? undefined : openBackupSession(homeDir, "onboard");
|
|
781
|
+
if (!opts.dryRun) {
|
|
782
|
+
writeManagedYaml(homeDir, managedAgents, backupSession);
|
|
783
|
+
}
|
|
784
|
+
if (mcpModeResolution.write && !opts.dryRun) {
|
|
785
|
+
writeMcpModeYaml(serversYamlPath, mcpModeResolution.write, backupSession);
|
|
786
|
+
}
|
|
496
787
|
if (memory.changed && !opts.dryRun) {
|
|
497
788
|
if (memory.current === "on") {
|
|
498
|
-
upsertServerYaml(serversYamlPath, MEMORY_SERVER_NAME, DEFAULT_MEMORY_SERVER_DEF);
|
|
789
|
+
upsertServerYaml(serversYamlPath, MEMORY_SERVER_NAME, DEFAULT_MEMORY_SERVER_DEF, backupSession);
|
|
499
790
|
}
|
|
500
791
|
else {
|
|
501
|
-
removeServerYaml(serversYamlPath, MEMORY_SERVER_NAME);
|
|
792
|
+
removeServerYaml(serversYamlPath, MEMORY_SERVER_NAME, backupSession);
|
|
502
793
|
}
|
|
503
794
|
}
|
|
504
795
|
let migratePlan;
|
|
@@ -506,22 +797,78 @@ export async function collectOnboardPlan(opts = {}) {
|
|
|
506
797
|
logProgress(opts, "migrate");
|
|
507
798
|
if (source) {
|
|
508
799
|
const sourceSummary = summary.find((s) => s.agent === source);
|
|
509
|
-
const
|
|
800
|
+
const interactiveSelection = capabilitySelection ?? await resolveInteractiveCapabilitySelection(sourceSummary, opts);
|
|
801
|
+
const categories = await resolveMigrateCategories(sourceSummary, opts, interactiveSelection);
|
|
510
802
|
if (categories.length > 0) {
|
|
511
|
-
migratePlan = await collectMigratePlan(source, homeDir, categories);
|
|
803
|
+
migratePlan = filterMigratePlan(await collectMigratePlan(source, homeDir, categories, workspaceDir), interactiveSelection);
|
|
512
804
|
if (!opts.dryRun) {
|
|
513
|
-
applyMigratePlan(migratePlan, homeDir);
|
|
805
|
+
applyMigratePlan(migratePlan, homeDir, backupSession);
|
|
514
806
|
}
|
|
515
807
|
}
|
|
516
808
|
else {
|
|
517
809
|
migrateSkipped = "migrate skipped — no categories selected";
|
|
518
810
|
}
|
|
519
811
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
812
|
+
const currentMcpAfterMigration = loadCanonicalSource(homeDir).mcp;
|
|
813
|
+
const migratedMcpNames = migratePlan?.items.filter((item) => item.kind === "mcp").map((item) => item.name) ?? [];
|
|
814
|
+
const availableMcpNames = [...new Set([...Object.keys(currentMcpAfterMigration.servers), ...migratedMcpNames])];
|
|
815
|
+
let requestedRoutes = capabilitySelection?.mcpRoutes;
|
|
816
|
+
const shouldPromptForRoutes = !opts.selectionFile
|
|
817
|
+
&& opts.mcpMode === undefined
|
|
818
|
+
&& !currentMcpAfterMigration.routes
|
|
819
|
+
&& !currentMcpAfterMigration.gateway
|
|
820
|
+
&& !currentMcpAfterMigration.hub
|
|
821
|
+
&& managedAgents.length > 0
|
|
822
|
+
&& availableMcpNames.length > 0
|
|
823
|
+
&& canUseInteractivePicker();
|
|
824
|
+
if (!requestedRoutes && shouldPromptForRoutes) {
|
|
825
|
+
requestedRoutes = await resolveInteractiveMcpRoutes(managedAgents, availableMcpNames, currentMcpAfterMigration.hub !== undefined);
|
|
826
|
+
}
|
|
827
|
+
const requestedRuntimeDelivery = capabilitySelection?.runtimeDelivery;
|
|
828
|
+
let mcpSyncOverride;
|
|
829
|
+
if ((requestedRoutes && Object.keys(requestedRoutes).length > 0) || (requestedRuntimeDelivery && Object.keys(requestedRuntimeDelivery).length > 0)) {
|
|
830
|
+
const currentMcp = currentMcpAfterMigration;
|
|
831
|
+
const routes = requestedRoutes && Object.keys(requestedRoutes).length > 0
|
|
832
|
+
? { ...(currentMcp.routes ?? {}), ...requestedRoutes }
|
|
833
|
+
: currentMcp.routes;
|
|
834
|
+
const delivery = requestedRuntimeDelivery && Object.keys(requestedRuntimeDelivery).length > 0
|
|
835
|
+
? { ...(currentMcp.runtime?.delivery ?? {}), ...requestedRuntimeDelivery }
|
|
836
|
+
: currentMcp.runtime?.delivery;
|
|
837
|
+
mcpSyncOverride = {
|
|
838
|
+
...currentMcp,
|
|
839
|
+
...(routes ? { routes } : {}),
|
|
840
|
+
...(delivery ? { runtime: { ...(currentMcp.runtime ?? {}), delivery } } : {}),
|
|
841
|
+
};
|
|
842
|
+
if (!opts.dryRun && requestedRoutes && Object.keys(requestedRoutes).length > 0) {
|
|
843
|
+
writeMcpRoutesYaml(join(homeDir, ".trellis", "mcp", "servers.yaml"), routes ?? {}, backupSession);
|
|
844
|
+
}
|
|
845
|
+
if (!opts.dryRun && requestedRuntimeDelivery && Object.keys(requestedRuntimeDelivery).length > 0) {
|
|
846
|
+
writeMcpRuntimeDeliveryYaml(join(homeDir, ".trellis", "mcp", "servers.yaml"), delivery ?? {}, backupSession);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
// Dry-run must plan the Memory server that this transaction would write;
|
|
850
|
+
// otherwise the following mcp-sync/memory-sync stages would inspect the
|
|
851
|
+
// pre-run canonical file and falsely report an unconfigured backend. The
|
|
852
|
+
// effective view is never persisted here — real writes above still use the
|
|
853
|
+
// normal backup transaction.
|
|
854
|
+
const effectiveMcpForRun = {
|
|
855
|
+
...(mcpSyncOverride ?? currentMcpAfterMigration),
|
|
856
|
+
servers: { ...(mcpSyncOverride ?? currentMcpAfterMigration).servers },
|
|
857
|
+
};
|
|
858
|
+
if (memory.current === "on") {
|
|
859
|
+
effectiveMcpForRun.servers[MEMORY_SERVER_NAME] = effectiveMcpForRun.servers[MEMORY_SERVER_NAME] ?? DEFAULT_MEMORY_SERVER_DEF;
|
|
860
|
+
}
|
|
861
|
+
else {
|
|
862
|
+
delete effectiveMcpForRun.servers[MEMORY_SERVER_NAME];
|
|
863
|
+
}
|
|
864
|
+
const mergedMemoryRoutes = mergeMemoryIntoExplicitRoutes(effectiveMcpForRun, managedAgents, memory.current === "on");
|
|
865
|
+
const effectiveMcpWithRoutes = mergedMemoryRoutes.mcp;
|
|
866
|
+
if (mergedMemoryRoutes.changed && !opts.dryRun && effectiveMcpWithRoutes.routes) {
|
|
867
|
+
writeMcpRoutesYaml(serversYamlPath, effectiveMcpWithRoutes.routes, backupSession);
|
|
868
|
+
}
|
|
869
|
+
// The same session now covers canonical and native writes. Neither
|
|
870
|
+
// collectSyncReport nor collectMcpSyncReport finalizes it; this caller
|
|
871
|
+
// finalizes once after every verification stage has completed.
|
|
525
872
|
logProgress(opts, "sync");
|
|
526
873
|
const syncReport = await collectSyncReport({ homeDir, dryRun: opts.dryRun, managedAgents, backupSession });
|
|
527
874
|
// The self-verification re-plan (design.md D2b): immediately after a
|
|
@@ -533,18 +880,18 @@ export async function collectOnboardPlan(opts = {}) {
|
|
|
533
880
|
// written, so there is nothing to verify.
|
|
534
881
|
const syncVerification = opts.dryRun ? undefined : await collectSyncReport({ homeDir, dryRun: true, managedAgents });
|
|
535
882
|
logProgress(opts, "mcp sync");
|
|
536
|
-
const mcpSyncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun, managedAgents, backupSession });
|
|
883
|
+
const mcpSyncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun, managedAgents, backupSession, mcp: effectiveMcpWithRoutes });
|
|
537
884
|
const mcpSyncVerification = opts.dryRun ? undefined : await collectMcpSyncReport({ homeDir, dryRun: true, managedAgents });
|
|
538
|
-
backupSession?.finalize();
|
|
539
885
|
// Independent of `managedAgents` — the shared memory server is not
|
|
540
886
|
// per-agent (design.md D4, trellis-onboard-mcp-memory), unlike
|
|
541
887
|
// sync/mcp-sync above. Runs unconditionally once reached; an
|
|
542
888
|
// unconfigured `memory` server is a legitimate "nothing to do yet"
|
|
543
889
|
// state (design.md D5), not gated on any prior step here.
|
|
544
890
|
logProgress(opts, "memory sync");
|
|
545
|
-
const
|
|
891
|
+
const selectedMemoryNames = capabilitySelection ? resolveSelectedNames(capabilitySelection.memories, loadCanonicalSource(homeDir).memories.map((memory) => memory.name)) : undefined;
|
|
892
|
+
const memorySyncResult = collectMemorySyncResult(homeDir, selectedMemoryNames, effectiveMcpWithRoutes);
|
|
546
893
|
if (!opts.dryRun) {
|
|
547
|
-
applyMemorySync(memorySyncResult);
|
|
894
|
+
applyMemorySync(memorySyncResult, backupSession);
|
|
548
895
|
}
|
|
549
896
|
// Read-only, no dryRun concept — same report either way, run last since
|
|
550
897
|
// it audits the config mcp sync just wrote (or, on --dry-run, whatever
|
|
@@ -572,8 +919,27 @@ export async function collectOnboardPlan(opts = {}) {
|
|
|
572
919
|
...normalizeSecretsAuditVerdict(secretsAuditReport),
|
|
573
920
|
...normalizeDoctorVerdict(doctorReport, managedAgents),
|
|
574
921
|
];
|
|
922
|
+
let rollback;
|
|
923
|
+
if (!opts.dryRun && backupSession?.hasOperations()) {
|
|
924
|
+
backupSession.finalize();
|
|
925
|
+
// Doctor intentionally scans the whole machine, including unmanaged
|
|
926
|
+
// Agents and pre-existing cross-agent drift. It is informative health
|
|
927
|
+
// state, not proof that this transaction failed. Roll back only stages
|
|
928
|
+
// whose writes/on-write verification belong to this onboard run.
|
|
929
|
+
const transactionBlocked = verdict.some((item) => item.severity === "blocked" && !item.stage.startsWith("doctor"));
|
|
930
|
+
if (transactionBlocked) {
|
|
931
|
+
rollback = await collectRollbackPlan(homeDir, backupSession.runId);
|
|
932
|
+
await applyRollbackPlan(homeDir, rollback.runId, loadManifest(homeDir, rollback.runId), rollback.items);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
backupSession?.finalize();
|
|
937
|
+
}
|
|
575
938
|
return {
|
|
576
939
|
summary,
|
|
940
|
+
inventory,
|
|
941
|
+
capabilitySummary,
|
|
942
|
+
managedAgentRuntime,
|
|
577
943
|
source,
|
|
578
944
|
sourceReason,
|
|
579
945
|
managedAgents,
|
|
@@ -587,6 +953,7 @@ export async function collectOnboardPlan(opts = {}) {
|
|
|
587
953
|
memorySyncResult,
|
|
588
954
|
secretsAuditReport,
|
|
589
955
|
doctorReport,
|
|
956
|
+
rollback,
|
|
590
957
|
mcpMode,
|
|
591
958
|
memory,
|
|
592
959
|
verdict,
|
|
@@ -604,8 +971,11 @@ async function offerToApply(opts) {
|
|
|
604
971
|
return opts.promptToApply();
|
|
605
972
|
if (!canUseInteractivePicker())
|
|
606
973
|
return false;
|
|
607
|
-
console.
|
|
608
|
-
const choice = await runSingleSelectPicker([
|
|
974
|
+
console.error("");
|
|
975
|
+
const choice = await runSingleSelectPicker([
|
|
976
|
+
"暂不应用(推荐:先检查上面的预览)",
|
|
977
|
+
"立即应用",
|
|
978
|
+
], undefined, "预览已完成,是否写入配置?");
|
|
609
979
|
return choice === 1;
|
|
610
980
|
}
|
|
611
981
|
export async function runOnboard(opts = {}) {
|
|
@@ -679,6 +1049,13 @@ function printResult(result, dryRun, showStatusLine) {
|
|
|
679
1049
|
: `${install.agent} has no npm package to install (see \`trellis init\`'s install hint) — left out of this run's managed set.`);
|
|
680
1050
|
}
|
|
681
1051
|
console.log(`Managed agents: ${result.managedAgents && result.managedAgents.length > 0 ? result.managedAgents.join(", ") : "(none)"}`);
|
|
1052
|
+
if (showStatusLine && result.managedAgentRuntime?.length) {
|
|
1053
|
+
console.log(`Agent delivery: ${result.managedAgentRuntime.map((item) => `${item.agent}=${item.delivery}/${item.route}`).join(", ")}`);
|
|
1054
|
+
}
|
|
1055
|
+
if (showStatusLine && result.capabilitySummary) {
|
|
1056
|
+
const summary = result.capabilitySummary;
|
|
1057
|
+
console.log(`Trellis capabilities: awareness=${summary.awarenessSkill ? "ready" : "missing"}, global instructions=${summary.globalInstructions.native ? "native" : "unavailable"} + Runtime read, Skills=${summary.skills}, Memory=${summary.memories}, MCP=${summary.mcpServers}`);
|
|
1058
|
+
}
|
|
682
1059
|
// Informational only, never a prompt (design.md D6/D12,
|
|
683
1060
|
// trellis-onboard-mcp-mode) — this is what keeps `--mcp-mode`/
|
|
684
1061
|
// `--memory` discoverable without costing every run a keystroke.
|
|
@@ -686,6 +1063,10 @@ function printResult(result, dryRun, showStatusLine) {
|
|
|
686
1063
|
// already uses.
|
|
687
1064
|
if (showStatusLine && result.mcpMode) {
|
|
688
1065
|
console.log(mcpModeStatusLine(result.mcpMode));
|
|
1066
|
+
if (result.mcpMode.changed) {
|
|
1067
|
+
const affected = result.managedAgents && result.managedAgents.length > 0 ? result.managedAgents.join(", ") : "(none)";
|
|
1068
|
+
console.log(`mode transition impact: ${result.mcpMode.previous} → ${result.mcpMode.current}; managed agents: ${affected}`);
|
|
1069
|
+
}
|
|
689
1070
|
}
|
|
690
1071
|
if (showStatusLine && result.memory) {
|
|
691
1072
|
console.log(memoryStatusLine(result.memory));
|
|
@@ -725,4 +1106,10 @@ function printResult(result, dryRun, showStatusLine) {
|
|
|
725
1106
|
console.log("\nhealth scan (trellis doctor)");
|
|
726
1107
|
printDoctorReport(result.doctorReport);
|
|
727
1108
|
}
|
|
1109
|
+
if (result.rollback) {
|
|
1110
|
+
console.log("\nautomatic rollback");
|
|
1111
|
+
const restored = result.rollback.items.filter((item) => item.action === "restore").length;
|
|
1112
|
+
const conflicts = result.rollback.items.filter((item) => item.action === "conflict").length;
|
|
1113
|
+
console.log(`↩️ restored ${restored} path(s), ${conflicts} rollback conflict(s); backup: ${result.rollback.runId}`);
|
|
1114
|
+
}
|
|
728
1115
|
}
|