agent-trellis 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-code.js +4 -3
- package/dist/adapters/codex.js +4 -3
- package/dist/adapters/kimi-code.js +4 -3
- package/dist/adapters/kiro.js +5 -4
- package/dist/adapters/pi.js +4 -3
- package/dist/adapters/piMcp.d.ts +6 -3
- package/dist/adapters/piMcp.js +10 -3
- package/dist/adapters/zcode.d.ts +12 -1
- package/dist/adapters/zcode.js +7 -6
- package/dist/cli.js +38 -5
- package/dist/commands/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +43 -6
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/mcp.d.ts +44 -1
- package/dist/commands/mcp.js +145 -33
- package/dist/commands/migrate.d.ts +12 -0
- package/dist/commands/migrate.js +8 -4
- package/dist/commands/migrateWorkspace.d.ts +40 -0
- package/dist/commands/migrateWorkspace.js +197 -0
- package/dist/commands/rollback.js +65 -35
- package/dist/commands/skill.d.ts +14 -0
- package/dist/commands/skill.js +71 -0
- package/dist/commands/sync.d.ts +16 -1
- package/dist/commands/sync.js +48 -15
- package/dist/core/adapter.d.ts +14 -0
- package/dist/core/canonical.d.ts +20 -32
- package/dist/core/canonical.js +18 -86
- package/dist/core/mcpServerDef.d.ts +39 -0
- package/dist/core/mcpServerDef.js +93 -0
- package/dist/core/types.d.ts +43 -4
- package/dist/lib/mcpMigrateRead.d.ts +25 -0
- package/dist/lib/mcpMigrateRead.js +9 -2
- package/dist/lib/skillProvider.js +18 -5
- package/dist/lib/workspace.d.ts +39 -0
- package/dist/lib/workspace.js +158 -0
- package/dist/lib/workspaceConfig.d.ts +45 -0
- package/dist/lib/workspaceConfig.js +98 -0
- package/dist/lib/workspaceDoctor.d.ts +44 -0
- package/dist/lib/workspaceDoctor.js +94 -0
- package/dist/lib/workspaceFiles.d.ts +38 -0
- package/dist/lib/workspaceFiles.js +264 -0
- package/dist/lib/workspaceMcp.d.ts +57 -0
- package/dist/lib/workspaceMcp.js +277 -0
- package/dist/lib/workspaceMerge.d.ts +37 -0
- package/dist/lib/workspaceMerge.js +98 -0
- package/dist/lib/workspaceMigrate.d.ts +118 -0
- package/dist/lib/workspaceMigrate.js +423 -0
- package/dist/pi-bridge/bundle.js +262 -96
- package/docs/architecture.md +111 -49
- package/docs/getting-started.md +102 -14
- package/docs/mcp-oauth-matrix.md +12 -1
- package/docs/overview.zh-CN.md +17 -0
- package/docs/research.md +28 -6
- package/docs/roadmap.md +17 -5
- package/package.json +1 -1
- package/docs/implementation-plan.md +0 -341
package/dist/commands/mcp.d.ts
CHANGED
|
@@ -33,14 +33,23 @@ export interface RunMcpSyncOptions {
|
|
|
33
33
|
/** Onboard-only seam — see RunSyncOptions.backupSession. Never a CLI
|
|
34
34
|
* flag. */
|
|
35
35
|
backupSession?: BackupSession;
|
|
36
|
+
/** Force global-only behavior even inside a workspace (`--global-only`). */
|
|
37
|
+
globalOnly?: boolean;
|
|
38
|
+
/** Explicit workspace root — the test/sandbox seam (see RunSyncOptions). */
|
|
39
|
+
workspaceDir?: string | null;
|
|
40
|
+
/** `--check`: plan only, write nothing, exit non-zero on any stale or
|
|
41
|
+
* missing rendered artifact (trellis-workspace-scope design D7). */
|
|
42
|
+
check?: boolean;
|
|
36
43
|
}
|
|
37
44
|
export interface AgentMcpSyncReport {
|
|
38
45
|
agent: AgentId;
|
|
39
46
|
present: boolean;
|
|
40
47
|
items: AdapterPlanItem[];
|
|
48
|
+
warnings?: string[];
|
|
41
49
|
}
|
|
42
50
|
export interface McpSyncReport {
|
|
43
51
|
reports: AgentMcpSyncReport[];
|
|
52
|
+
workspaceRoot?: string;
|
|
44
53
|
}
|
|
45
54
|
export declare function collectMcpSyncReport(opts?: RunMcpSyncOptions): Promise<McpSyncReport>;
|
|
46
55
|
export declare function runMcpSync(opts?: RunMcpSyncOptions): Promise<{
|
|
@@ -49,7 +58,7 @@ export declare function runMcpSync(opts?: RunMcpSyncOptions): Promise<{
|
|
|
49
58
|
/** Exported so `onboard` prints an mcp-sync report identically to running
|
|
50
59
|
* `mcp sync` standalone, instead of a second, easily-drifting copy of this
|
|
51
60
|
* formatting. */
|
|
52
|
-
export declare function printReport(report: McpSyncReport, dryRun: boolean): void;
|
|
61
|
+
export declare function printReport(report: McpSyncReport, dryRun: boolean, check?: boolean): void;
|
|
53
62
|
export interface McpListEntry {
|
|
54
63
|
name: string;
|
|
55
64
|
transport: Transport;
|
|
@@ -147,8 +156,30 @@ export interface McpAddPlan {
|
|
|
147
156
|
* write failure — design.md's open question, resolved in favor of
|
|
148
157
|
* refusing early with a specific reason. No `--force`: an existing name
|
|
149
158
|
* always refuses, ever (design.md D4).
|
|
159
|
+
*
|
|
160
|
+
* Split into two steps so `mcp add --workspace` reuses the identical
|
|
161
|
+
* flag validation/def construction and only swaps which file's existing
|
|
162
|
+
* entries it conflicts against (trellis-workspace-scope task 3.4):
|
|
163
|
+
* `buildMcpAddDef` is the pure part, `collectMcpAddPlan` adds the
|
|
164
|
+
* global-canonical existence check.
|
|
150
165
|
*/
|
|
166
|
+
export declare function buildMcpAddDef(name: string | undefined, raw: McpAddRawArgs): {
|
|
167
|
+
ok: true;
|
|
168
|
+
name: string;
|
|
169
|
+
def: McpServerDef;
|
|
170
|
+
} | {
|
|
171
|
+
ok: false;
|
|
172
|
+
plan: McpAddPlan;
|
|
173
|
+
};
|
|
151
174
|
export declare function collectMcpAddPlan(name: string | undefined, raw: McpAddRawArgs, homeDir?: string): McpAddPlan;
|
|
175
|
+
/** The workspace twin of `collectMcpAddPlan` (trellis-workspace-scope
|
|
176
|
+
* task 3.4): identical validation, but the existence check runs against
|
|
177
|
+
* `.agents/config.json` — compared through the same snake_case
|
|
178
|
+
* serialization the file stores, so a re-add of the same definition is
|
|
179
|
+
* a clean "already-present" no-op. A global same-name entry is NOT a
|
|
180
|
+
* conflict here (workspace shadowing a global entry is a legal,
|
|
181
|
+
* documented merge outcome), just noted in the detail. */
|
|
182
|
+
export declare function collectMcpAddWorkspacePlan(name: string | undefined, raw: McpAddRawArgs, workspaceRoot: string, homeDir?: string): McpAddPlan;
|
|
152
183
|
export type McpSetAuthAction = "updated" | "already-set" | "not-found" | "invalid-input";
|
|
153
184
|
export interface McpSetAuthPlan {
|
|
154
185
|
name: string;
|
|
@@ -269,10 +300,22 @@ export declare function applyMcpAddWithSync(plan: McpAddPlan, opts?: {
|
|
|
269
300
|
dryRun?: boolean;
|
|
270
301
|
backupSession?: BackupSession;
|
|
271
302
|
}): Promise<McpAddOutcome>;
|
|
303
|
+
/** Workspace twin of `applyMcpAddWithSync` (trellis-workspace-scope task
|
|
304
|
+
* 3.4): writes `.agents/config.json` instead of `servers.yaml`, then
|
|
305
|
+
* cascades the same mcp sync — which, run inside the workspace, renders
|
|
306
|
+
* only the project-level targets for the workspace layer (plus the usual
|
|
307
|
+
* global-layer targets; the agent does its own layering). */
|
|
308
|
+
export declare function applyMcpAddWorkspaceWithSync(plan: McpAddPlan, workspaceRoot: string, opts?: {
|
|
309
|
+
homeDir?: string;
|
|
310
|
+
dryRun?: boolean;
|
|
311
|
+
backupSession?: BackupSession;
|
|
312
|
+
}): Promise<McpAddOutcome>;
|
|
272
313
|
export declare function runMcpAdd(name: string | undefined, raw: McpAddRawArgs, opts?: {
|
|
273
314
|
homeDir?: string;
|
|
274
315
|
json?: boolean;
|
|
275
316
|
dryRun?: boolean;
|
|
317
|
+
workspace?: boolean;
|
|
318
|
+
workspaceDir?: string;
|
|
276
319
|
}): Promise<{
|
|
277
320
|
exitCode: number;
|
|
278
321
|
}>;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -27,6 +27,10 @@ import { PiAdapter } from "../adapters/pi.js";
|
|
|
27
27
|
import { KimiCodeAdapter } from "../adapters/kimi-code.js";
|
|
28
28
|
import { ZcodeAdapter } from "../adapters/zcode.js";
|
|
29
29
|
import { openBackupSession } from "../lib/backup.js";
|
|
30
|
+
import { findWorkspaceRoot } from "../lib/workspace.js";
|
|
31
|
+
import { applyWorkspaceMcp, planWorkspaceMcp, workspacePrecedenceWarnings } from "../lib/workspaceMcp.js";
|
|
32
|
+
import { readWorkspaceConfigJson, upsertWorkspaceServer } from "../lib/workspaceConfig.js";
|
|
33
|
+
import { toServerDefYaml } from "../core/mcpServerDef.js";
|
|
30
34
|
const ADAPTER_FACTORY = {
|
|
31
35
|
"claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
|
|
32
36
|
codex: (homeDir) => new CodexAdapter(homeDir),
|
|
@@ -42,14 +46,16 @@ function buildAdapters(homeDir, managedAgents) {
|
|
|
42
46
|
}
|
|
43
47
|
export async function collectMcpSyncReport(opts = {}) {
|
|
44
48
|
const homeDir = opts.homeDir ?? homedir();
|
|
45
|
-
const
|
|
49
|
+
const workspaceDir = opts.globalOnly ? null : (opts.workspaceDir !== undefined ? opts.workspaceDir : findWorkspaceRoot(process.cwd()));
|
|
50
|
+
const loaded = loadCanonicalSource({ homeDir, workspaceDir });
|
|
46
51
|
const canonical = {
|
|
47
52
|
...loaded,
|
|
48
53
|
...(opts.managedAgents ? { managedAgents: opts.managedAgents } : {}),
|
|
49
54
|
...(opts.mcp ? { mcp: opts.mcp } : {}),
|
|
50
55
|
};
|
|
51
56
|
const reports = [];
|
|
52
|
-
const
|
|
57
|
+
const writeMode = !opts.dryRun && !opts.check;
|
|
58
|
+
const ownSession = writeMode && !opts.backupSession ? openBackupSession(homeDir, "mcp-sync") : undefined;
|
|
53
59
|
const backup = opts.backupSession ?? ownSession;
|
|
54
60
|
for (const adapter of buildAdapters(homeDir, canonical.managedAgents)) {
|
|
55
61
|
const probeResult = await adapter.probe();
|
|
@@ -57,14 +63,17 @@ export async function collectMcpSyncReport(opts = {}) {
|
|
|
57
63
|
reports.push({ agent: adapter.id, present: false, items: [] });
|
|
58
64
|
continue;
|
|
59
65
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
const globalItems = (await adapter.plan(canonical)).filter((item) => item.kind === "mcp");
|
|
67
|
+
const wsItems = planWorkspaceMcp(adapter.id, canonical);
|
|
68
|
+
if (writeMode) {
|
|
69
|
+
await adapter.apply(globalItems, backup);
|
|
70
|
+
applyWorkspaceMcp(adapter.id, canonical, wsItems, backup);
|
|
63
71
|
}
|
|
64
|
-
|
|
72
|
+
const warnings = workspacePrecedenceWarnings(adapter.id, canonical);
|
|
73
|
+
reports.push({ agent: adapter.id, present: true, items: [...globalItems, ...wsItems], ...(warnings.length > 0 ? { warnings } : {}) });
|
|
65
74
|
}
|
|
66
75
|
ownSession?.finalize();
|
|
67
|
-
return { reports };
|
|
76
|
+
return { reports, ...(canonical.workspace ? { workspaceRoot: canonical.workspace.root } : {}) };
|
|
68
77
|
}
|
|
69
78
|
export async function runMcpSync(opts = {}) {
|
|
70
79
|
let report;
|
|
@@ -79,22 +88,31 @@ export async function runMcpSync(opts = {}) {
|
|
|
79
88
|
console.log(JSON.stringify(report, null, 2));
|
|
80
89
|
}
|
|
81
90
|
else {
|
|
82
|
-
printReport(report, opts.dryRun ?? false);
|
|
91
|
+
printReport(report, opts.dryRun ?? false, opts.check ?? false);
|
|
83
92
|
}
|
|
84
93
|
const hasConflict = report.reports.some((r) => r.items.some((i) => i.action === "conflict"));
|
|
94
|
+
if (opts.check) {
|
|
95
|
+
const hasPending = report.reports.some((r) => r.items.some((i) => i.action !== "conflict"));
|
|
96
|
+
return { exitCode: hasPending || hasConflict ? 1 : 0 };
|
|
97
|
+
}
|
|
85
98
|
return { exitCode: hasConflict ? 1 : 0 };
|
|
86
99
|
}
|
|
87
100
|
/** Exported so `onboard` prints an mcp-sync report identically to running
|
|
88
101
|
* `mcp sync` standalone, instead of a second, easily-drifting copy of this
|
|
89
102
|
* formatting. */
|
|
90
|
-
export function printReport(report, dryRun) {
|
|
103
|
+
export function printReport(report, dryRun, check = false) {
|
|
91
104
|
if (dryRun)
|
|
92
105
|
console.log("[dry run]");
|
|
106
|
+
if (check)
|
|
107
|
+
console.log("[check]");
|
|
108
|
+
if (report.workspaceRoot) {
|
|
109
|
+
console.log(`Workspace layer: ${report.workspaceRoot} (.agents/)`);
|
|
110
|
+
}
|
|
93
111
|
if (report.reports.length === 0) {
|
|
94
112
|
console.log("No managed agents yet — run `trellis onboard` or list agent ids in ~/.trellis/managed.yaml.");
|
|
95
113
|
return;
|
|
96
114
|
}
|
|
97
|
-
for (const { agent, present, items } of report.reports) {
|
|
115
|
+
for (const { agent, present, items, warnings } of report.reports) {
|
|
98
116
|
if (!present) {
|
|
99
117
|
console.log(`— ${agent} (not installed)`);
|
|
100
118
|
continue;
|
|
@@ -104,18 +122,27 @@ export function printReport(report, dryRun) {
|
|
|
104
122
|
const conflicts = items.filter((i) => i.action === "conflict");
|
|
105
123
|
if (created.length === 0 && removed.length === 0 && conflicts.length === 0) {
|
|
106
124
|
console.log(`✅ ${agent} — already in sync`);
|
|
107
|
-
continue;
|
|
108
125
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
else {
|
|
127
|
+
const icon = conflicts.length > 0 ? "⚠️ " : "✅";
|
|
128
|
+
console.log(`${icon} ${agent} — ${created.length} created/updated, ${removed.length} removed, ${conflicts.length} conflict(s)`);
|
|
129
|
+
for (const item of [...created, ...removed, ...conflicts]) {
|
|
130
|
+
console.log(` - [${item.action}]${item.layer === "workspace" ? " (workspace)" : ""} ${item.description}`);
|
|
131
|
+
// Conflicts carry the actionable fix (e.g. the unmarked-overwrite
|
|
132
|
+
// guard's "run migrate --workspace or remove the entry") — printing
|
|
133
|
+
// only the description would leave the user stuck at "left untouched".
|
|
134
|
+
if (item.remediation)
|
|
135
|
+
console.log(` → ${item.remediation}`);
|
|
136
|
+
}
|
|
137
|
+
// pi does not watch mcp.json (verified 2026-10-11): without this hint
|
|
138
|
+
// a changed config silently only takes effect on the next pi launch
|
|
139
|
+
// (trellis-pi-native-mcp-migration design.md D5).
|
|
140
|
+
if (agent === "pi" && (created.length > 0 || removed.length > 0)) {
|
|
141
|
+
console.log(" pi does not watch mcp.json — restart pi or run /reload to pick up MCP changes");
|
|
142
|
+
}
|
|
113
143
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// (trellis-pi-native-mcp-migration design.md D5).
|
|
117
|
-
if (agent === "pi" && (created.length > 0 || removed.length > 0)) {
|
|
118
|
-
console.log(" pi does not watch mcp.json — restart pi or run /reload to pick up MCP changes");
|
|
144
|
+
for (const warning of warnings ?? []) {
|
|
145
|
+
console.log(` ⚠ ${warning}`);
|
|
119
146
|
}
|
|
120
147
|
}
|
|
121
148
|
}
|
|
@@ -260,37 +287,43 @@ const TRANSPORTS = ["stdio", "http", "sse"];
|
|
|
260
287
|
* write failure — design.md's open question, resolved in favor of
|
|
261
288
|
* refusing early with a specific reason. No `--force`: an existing name
|
|
262
289
|
* always refuses, ever (design.md D4).
|
|
290
|
+
*
|
|
291
|
+
* Split into two steps so `mcp add --workspace` reuses the identical
|
|
292
|
+
* flag validation/def construction and only swaps which file's existing
|
|
293
|
+
* entries it conflicts against (trellis-workspace-scope task 3.4):
|
|
294
|
+
* `buildMcpAddDef` is the pure part, `collectMcpAddPlan` adds the
|
|
295
|
+
* global-canonical existence check.
|
|
263
296
|
*/
|
|
264
|
-
export function
|
|
297
|
+
export function buildMcpAddDef(name, raw) {
|
|
265
298
|
if (!name) {
|
|
266
|
-
return { name: "(none)", action: "invalid-input", detail: "a name is required: trellis mcp add <name> --transport ..." };
|
|
299
|
+
return { ok: false, plan: { name: "(none)", action: "invalid-input", detail: "a name is required: trellis mcp add <name> --transport ..." } };
|
|
267
300
|
}
|
|
268
301
|
if (!raw.transport || !TRANSPORTS.includes(raw.transport)) {
|
|
269
|
-
return { name, action: "invalid-input", detail: `--transport must be one of: ${TRANSPORTS.join(", ")}` };
|
|
302
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--transport must be one of: ${TRANSPORTS.join(", ")}` } };
|
|
270
303
|
}
|
|
271
304
|
const transport = raw.transport;
|
|
272
305
|
if (transport === "stdio" && !raw.command) {
|
|
273
|
-
return { name, action: "invalid-input", detail: "--transport stdio requires --command" };
|
|
306
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: "--transport stdio requires --command" } };
|
|
274
307
|
}
|
|
275
308
|
if ((transport === "http" || transport === "sse") && !raw.url) {
|
|
276
|
-
return { name, action: "invalid-input", detail: `--transport ${transport} requires --url` };
|
|
309
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--transport ${transport} requires --url` } };
|
|
277
310
|
}
|
|
278
311
|
if (raw.auth !== undefined && raw.auth !== "oauth") {
|
|
279
|
-
return { name, action: "invalid-input", detail: `--auth must be "oauth" when provided (got ${raw.auth})` };
|
|
312
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--auth must be "oauth" when provided (got ${raw.auth})` } };
|
|
280
313
|
}
|
|
281
314
|
if (raw.auth === "oauth" && transport === "stdio") {
|
|
282
|
-
return { name, action: "invalid-input", detail: "--auth oauth requires an http or sse MCP server" };
|
|
315
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: "--auth oauth requires an http or sse MCP server" } };
|
|
283
316
|
}
|
|
284
317
|
const agentsList = parseCsv(raw.agents);
|
|
285
318
|
if (agentsList) {
|
|
286
319
|
for (const id of agentsList) {
|
|
287
320
|
if (!ALL_AGENTS.includes(id)) {
|
|
288
|
-
return { name, action: "invalid-input", detail: `--agents "${id}" is not a recognized agent id (${ALL_AGENTS.join(", ")})` };
|
|
321
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--agents "${id}" is not a recognized agent id (${ALL_AGENTS.join(", ")})` } };
|
|
289
322
|
}
|
|
290
323
|
}
|
|
291
324
|
}
|
|
292
325
|
if (raw.enabled !== undefined && raw.enabled !== "true" && raw.enabled !== "false") {
|
|
293
|
-
return { name, action: "invalid-input", detail: `--enabled must be "true" or "false" (got ${raw.enabled})` };
|
|
326
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--enabled must be "true" or "false" (got ${raw.enabled})` } };
|
|
294
327
|
}
|
|
295
328
|
const def = {
|
|
296
329
|
transport,
|
|
@@ -304,15 +337,46 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
|
|
|
304
337
|
...(raw.enabled !== undefined ? { enabled: raw.enabled === "true" } : {}),
|
|
305
338
|
...(agentsList ? { agents: agentsList } : {}),
|
|
306
339
|
};
|
|
340
|
+
return { ok: true, name, def };
|
|
341
|
+
}
|
|
342
|
+
export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
|
|
343
|
+
const built = buildMcpAddDef(name, raw);
|
|
344
|
+
if (!built.ok)
|
|
345
|
+
return built.plan;
|
|
346
|
+
const def = built.def;
|
|
307
347
|
const canonical = loadCanonicalSource(homeDir);
|
|
308
|
-
const existing = canonical.mcp.servers[name];
|
|
348
|
+
const existing = canonical.mcp.servers[built.name];
|
|
309
349
|
if (existing) {
|
|
310
350
|
if (JSON.stringify(existing) === JSON.stringify(def)) {
|
|
311
|
-
return { name, action: "already-present", detail: "canonical entry is already identical" };
|
|
351
|
+
return { name: built.name, action: "already-present", detail: "canonical entry is already identical" };
|
|
312
352
|
}
|
|
313
|
-
return { name, action: "conflict", detail: `servers.yaml already has "${name}" with different settings — resolve by hand (no --force, design.md D4)` };
|
|
353
|
+
return { name: built.name, action: "conflict", detail: `servers.yaml already has "${built.name}" with different settings — resolve by hand (no --force, design.md D4)` };
|
|
314
354
|
}
|
|
315
|
-
return { name, action: "create", detail: "will add to servers.yaml", def };
|
|
355
|
+
return { name: built.name, action: "create", detail: "will add to servers.yaml", def };
|
|
356
|
+
}
|
|
357
|
+
/** The workspace twin of `collectMcpAddPlan` (trellis-workspace-scope
|
|
358
|
+
* task 3.4): identical validation, but the existence check runs against
|
|
359
|
+
* `.agents/config.json` — compared through the same snake_case
|
|
360
|
+
* serialization the file stores, so a re-add of the same definition is
|
|
361
|
+
* a clean "already-present" no-op. A global same-name entry is NOT a
|
|
362
|
+
* conflict here (workspace shadowing a global entry is a legal,
|
|
363
|
+
* documented merge outcome), just noted in the detail. */
|
|
364
|
+
export function collectMcpAddWorkspacePlan(name, raw, workspaceRoot, homeDir = homedir()) {
|
|
365
|
+
const built = buildMcpAddDef(name, raw);
|
|
366
|
+
if (!built.ok)
|
|
367
|
+
return built.plan;
|
|
368
|
+
const def = built.def;
|
|
369
|
+
const config = readWorkspaceConfigJson(workspaceRoot);
|
|
370
|
+
const existing = config.mcp?.servers?.[built.name];
|
|
371
|
+
if (existing) {
|
|
372
|
+
if (JSON.stringify(existing) === JSON.stringify(toServerDefYaml(def))) {
|
|
373
|
+
return { name: built.name, action: "already-present", detail: ".agents/config.json entry is already identical" };
|
|
374
|
+
}
|
|
375
|
+
return { name: built.name, action: "conflict", detail: `.agents/config.json already has "${built.name}" with different settings — resolve by hand (no --force)` };
|
|
376
|
+
}
|
|
377
|
+
const global = loadCanonicalSource(homeDir).mcp.servers[built.name];
|
|
378
|
+
const shadowNote = global ? ` (shadows the global "${built.name}" for this project)` : "";
|
|
379
|
+
return { name: built.name, action: "create", detail: `will add to .agents/config.json${shadowNote}`, def };
|
|
316
380
|
}
|
|
317
381
|
/**
|
|
318
382
|
* Sets or clears the OAuth classification, and optionally the
|
|
@@ -587,8 +651,56 @@ export async function applyMcpAddWithSync(plan, opts = {}) {
|
|
|
587
651
|
}
|
|
588
652
|
return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
|
|
589
653
|
}
|
|
654
|
+
/** Workspace twin of `applyMcpAddWithSync` (trellis-workspace-scope task
|
|
655
|
+
* 3.4): writes `.agents/config.json` instead of `servers.yaml`, then
|
|
656
|
+
* cascades the same mcp sync — which, run inside the workspace, renders
|
|
657
|
+
* only the project-level targets for the workspace layer (plus the usual
|
|
658
|
+
* global-layer targets; the agent does its own layering). */
|
|
659
|
+
export async function applyMcpAddWorkspaceWithSync(plan, workspaceRoot, opts = {}) {
|
|
660
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
661
|
+
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "mcp-add") : undefined;
|
|
662
|
+
const session = opts.backupSession ?? ownSession;
|
|
663
|
+
if (!opts.dryRun && plan.action === "create" && plan.def) {
|
|
664
|
+
upsertWorkspaceServer(workspaceRoot, plan.name, plan.def, session);
|
|
665
|
+
}
|
|
666
|
+
ownSession?.finalize();
|
|
667
|
+
const planFailed = plan.action === "conflict" || plan.action === "invalid-input";
|
|
668
|
+
let syncReport;
|
|
669
|
+
if (!planFailed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
|
|
670
|
+
syncReport = await collectMcpSyncReport({ homeDir, workspaceDir: workspaceRoot, dryRun: opts.dryRun });
|
|
671
|
+
}
|
|
672
|
+
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
673
|
+
}
|
|
590
674
|
export async function runMcpAdd(name, raw, opts = {}) {
|
|
591
675
|
const homeDir = opts.homeDir ?? homedir();
|
|
676
|
+
if (opts.workspace) {
|
|
677
|
+
const workspaceRoot = opts.workspaceDir ?? findWorkspaceRoot(process.cwd());
|
|
678
|
+
if (!workspaceRoot) {
|
|
679
|
+
console.error("no .agents/ found here or above — run `trellis init --workspace` first (or pass --workspace from inside the repo)");
|
|
680
|
+
return { exitCode: 1 };
|
|
681
|
+
}
|
|
682
|
+
let plan;
|
|
683
|
+
try {
|
|
684
|
+
plan = collectMcpAddWorkspacePlan(name, raw, workspaceRoot, homeDir);
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
688
|
+
return { exitCode: 1 };
|
|
689
|
+
}
|
|
690
|
+
const { sync: syncReport } = await applyMcpAddWorkspaceWithSync(plan, workspaceRoot, { homeDir, dryRun: opts.dryRun });
|
|
691
|
+
const planFailed = plan.action === "conflict" || plan.action === "invalid-input";
|
|
692
|
+
if (opts.json) {
|
|
693
|
+
console.log(JSON.stringify(syncReport ? { ...plan, workspace: workspaceRoot, sync: syncReport } : { ...plan, workspace: workspaceRoot }, null, 2));
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
console.log(`${opts.dryRun ? "[dry run] " : ""}mcp add ${plan.name} --workspace`);
|
|
697
|
+
console.log(` [${plan.action}] ${plan.detail}`);
|
|
698
|
+
if (syncReport)
|
|
699
|
+
printReport(syncReport, opts.dryRun ?? false);
|
|
700
|
+
}
|
|
701
|
+
const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
|
|
702
|
+
return { exitCode: planFailed || syncConflict ? 1 : 0 };
|
|
703
|
+
}
|
|
592
704
|
let plan;
|
|
593
705
|
try {
|
|
594
706
|
plan = collectMcpAddPlan(name, raw, homeDir);
|
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
import type { AgentId, McpServerDef } from "../core/types.js";
|
|
8
8
|
import type { BackupSession } from "../lib/backup.js";
|
|
9
9
|
import { type NativeMemoryCandidate, type NativeMemoryDiscovery } from "../lib/nativeMemory.js";
|
|
10
|
+
/** Where a `staticEnv` literal secret's real value goes when
|
|
11
|
+
* `secrets.policy.yaml` doesn't already have its own `env_file`
|
|
12
|
+
* (trellis-migrate-extract-static-env-secrets design.md D3) — a sibling
|
|
13
|
+
* of `servers.yaml`, so "the mcp stuff" stays one directory rather than
|
|
14
|
+
* a second, disconnected location. `~`-form for what gets written into
|
|
15
|
+
* `secrets.policy.yaml` itself (portable across machines); resolved form
|
|
16
|
+
* for real file I/O. */
|
|
17
|
+
export declare const DEFAULT_LOCAL_SECRETS_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
|
|
18
|
+
/** Exported for `migrate --workspace` (trellis-workspace-scope task 4.2) —
|
|
19
|
+
* its staticEnv extraction lands in the same default store under the same
|
|
20
|
+
* naming scheme, never a second location. */
|
|
21
|
+
export declare function defaultLocalSecretsEnvFilePath(homeDir: string): string;
|
|
10
22
|
export type MigrateAction = "create" | "skip-symlink" | "skip-case-broken" | "skip-unsupported" | "already-migrated" | "reclassify" | "conflict" | "extract-secret";
|
|
11
23
|
/** Internal kind naming, unchanged since before `--only` existed
|
|
12
24
|
* (trellis-migrate-category-selection design.md D2) — the CLI-facing
|
package/dist/commands/migrate.js
CHANGED
|
@@ -29,8 +29,11 @@ import { discoverNativeMemory, MAX_NATIVE_MEMORY_BYTES, renderNativeMemory } fro
|
|
|
29
29
|
* a second, disconnected location. `~`-form for what gets written into
|
|
30
30
|
* `secrets.policy.yaml` itself (portable across machines); resolved form
|
|
31
31
|
* for real file I/O. */
|
|
32
|
-
const DEFAULT_LOCAL_SECRETS_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
|
|
33
|
-
|
|
32
|
+
export const DEFAULT_LOCAL_SECRETS_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
|
|
33
|
+
/** Exported for `migrate --workspace` (trellis-workspace-scope task 4.2) —
|
|
34
|
+
* its staticEnv extraction lands in the same default store under the same
|
|
35
|
+
* naming scheme, never a second location. */
|
|
36
|
+
export function defaultLocalSecretsEnvFilePath(homeDir) {
|
|
34
37
|
return join(homeDir, ".trellis", "mcp", "servers.local.env");
|
|
35
38
|
}
|
|
36
39
|
/** Picks the rc file `ensureShellEnvSource` writes its one pointer block
|
|
@@ -54,8 +57,9 @@ const PROBES = {
|
|
|
54
57
|
"kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
|
|
55
58
|
zcode: (homeDir) => zcodeProbe.probe(homeDir),
|
|
56
59
|
};
|
|
57
|
-
/** pi
|
|
58
|
-
* trellis-migrate-mcp-servers
|
|
60
|
+
/** pi's mcp.json migrate-in is not implemented (roadmap.md P14/
|
|
61
|
+
* trellis-migrate-mcp-servers predates pi 1.1.0's native MCP config) —
|
|
62
|
+
* deliberately absent, not an oversight;
|
|
59
63
|
* `collectMigratePlan` skips the `mcp` category entirely for pi. */
|
|
60
64
|
const MCP_READERS = {
|
|
61
65
|
"claude-code": readClaudeCodeMcpDefs,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `trellis migrate --workspace` — absorbs a repo's existing project-level
|
|
3
|
+
* agent config (all six agents' MCP files + root AGENTS.md) into
|
|
4
|
+
* `.agents/`, then re-renders (trellis-workspace-scope group 4).
|
|
5
|
+
*
|
|
6
|
+
* Flow: plan (pure, `src/lib/workspaceMigrate.ts`) → refuse on unresolved
|
|
7
|
+
* conflicts → one backup session named `workspace-<repo-hash>` covering
|
|
8
|
+
* every write (task 4.6 — `.agents/` writes, the secret-store extraction,
|
|
9
|
+
* the marker-headed AGENTS.md rewrite, and the cascade render, so a
|
|
10
|
+
* single `trellis rollback` restores originals and removes generated
|
|
11
|
+
* `.agents/` content) → post-migration equivalence report (task 4.7).
|
|
12
|
+
*/
|
|
13
|
+
import type { AgentId } from "../core/types.js";
|
|
14
|
+
import type { WorkspaceMigratePlan } from "../lib/workspaceMigrate.js";
|
|
15
|
+
export interface RunMigrateWorkspaceOptions {
|
|
16
|
+
/** Test/sandbox seam — CLI resolves via findWorkspaceRoot(cwd), falling
|
|
17
|
+
* back to cwd itself so a legacy project without `.agents/` yet can
|
|
18
|
+
* still be scanned (apply then creates `.agents/` through backup). */
|
|
19
|
+
workspaceDir?: string;
|
|
20
|
+
/** Raw `name=agentId` strings from repeated `--choose`. */
|
|
21
|
+
choose?: string[];
|
|
22
|
+
/** Raw `--dedup` value: `drop` (default) or `override`. */
|
|
23
|
+
dedup?: string;
|
|
24
|
+
dryRun?: boolean;
|
|
25
|
+
json?: boolean;
|
|
26
|
+
homeDir?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface WorkspaceMigrateEquivalence {
|
|
29
|
+
/** Per agent: server names found in its project config before the
|
|
30
|
+
* migration vs. what the post-migration render now delivers. */
|
|
31
|
+
agents: {
|
|
32
|
+
agent: AgentId;
|
|
33
|
+
before: string[];
|
|
34
|
+
after: string[];
|
|
35
|
+
}[];
|
|
36
|
+
}
|
|
37
|
+
export declare function runMigrateWorkspace(opts?: RunMigrateWorkspaceOptions): Promise<{
|
|
38
|
+
exitCode: number;
|
|
39
|
+
}>;
|
|
40
|
+
export declare function printWorkspaceMigratePlan(plan: WorkspaceMigratePlan, dryRun: boolean): void;
|