agent-trellis 0.14.1 → 0.15.1
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/zcode.d.ts +12 -1
- package/dist/adapters/zcode.js +7 -6
- package/dist/cli.js +52 -37
- 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 +69 -3
- package/dist/commands/mcp.js +255 -56
- package/dist/commands/mcpGateway.js +3 -3
- 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 -34
- package/dist/core/canonical.js +18 -92
- package/dist/core/mcpServerDef.d.ts +39 -0
- package/dist/core/mcpServerDef.js +93 -0
- package/dist/core/types.d.ts +39 -4
- package/dist/lib/cliArgs.d.ts +17 -0
- package/dist/lib/cliArgs.js +36 -0
- package/dist/lib/mcpMigrateRead.d.ts +25 -0
- package/dist/lib/mcpMigrateRead.js +9 -2
- package/dist/lib/scopeSelection.d.ts +1 -1
- package/dist/lib/scopeSelection.js +4 -3
- 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 +51 -0
- package/dist/lib/workspaceConfig.js +117 -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 +258 -96
- package/docs/architecture.md +111 -49
- package/docs/getting-started.md +86 -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;
|
|
@@ -111,8 +120,9 @@ export declare function runMcpList(opts?: {
|
|
|
111
120
|
};
|
|
112
121
|
/** Raw, unvalidated CLI flag values — `collectMcpAddPlan` does the actual
|
|
113
122
|
* parsing/validation. Kept as plain strings here so `parseMcpAddArgs`
|
|
114
|
-
* stays a dumb `--flag value` reader
|
|
115
|
-
*
|
|
123
|
+
* stays a dumb `--flag value` reader. Both `--flag value` and
|
|
124
|
+
* `--flag=value` are accepted (see src/lib/cliArgs.ts — the `=` form used
|
|
125
|
+
* to be silently dropped). */
|
|
116
126
|
export interface McpAddRawArgs {
|
|
117
127
|
transport?: string;
|
|
118
128
|
auth?: string;
|
|
@@ -130,6 +140,9 @@ export interface McpAddRawArgs {
|
|
|
130
140
|
agents?: string;
|
|
131
141
|
/** `"true"` or `"false"`. */
|
|
132
142
|
enabled?: string;
|
|
143
|
+
/** `--*` tokens no flag claimed (typos, wrong command); `runMcpAdd`
|
|
144
|
+
* warns about them instead of letting them vanish silently. */
|
|
145
|
+
unknown?: string[];
|
|
133
146
|
}
|
|
134
147
|
export declare function parseMcpAddArgs(argv: readonly string[]): McpAddRawArgs;
|
|
135
148
|
export declare function parseMcpSetArgs(argv: readonly string[]): McpSetAuthRawArgs;
|
|
@@ -147,8 +160,30 @@ export interface McpAddPlan {
|
|
|
147
160
|
* write failure — design.md's open question, resolved in favor of
|
|
148
161
|
* refusing early with a specific reason. No `--force`: an existing name
|
|
149
162
|
* always refuses, ever (design.md D4).
|
|
163
|
+
*
|
|
164
|
+
* Split into two steps so `mcp add --workspace` reuses the identical
|
|
165
|
+
* flag validation/def construction and only swaps which file's existing
|
|
166
|
+
* entries it conflicts against (trellis-workspace-scope task 3.4):
|
|
167
|
+
* `buildMcpAddDef` is the pure part, `collectMcpAddPlan` adds the
|
|
168
|
+
* global-canonical existence check.
|
|
150
169
|
*/
|
|
170
|
+
export declare function buildMcpAddDef(name: string | undefined, raw: McpAddRawArgs): {
|
|
171
|
+
ok: true;
|
|
172
|
+
name: string;
|
|
173
|
+
def: McpServerDef;
|
|
174
|
+
} | {
|
|
175
|
+
ok: false;
|
|
176
|
+
plan: McpAddPlan;
|
|
177
|
+
};
|
|
151
178
|
export declare function collectMcpAddPlan(name: string | undefined, raw: McpAddRawArgs, homeDir?: string): McpAddPlan;
|
|
179
|
+
/** The workspace twin of `collectMcpAddPlan` (trellis-workspace-scope
|
|
180
|
+
* task 3.4): identical validation, but the existence check runs against
|
|
181
|
+
* `.agents/config.json` — compared through the same snake_case
|
|
182
|
+
* serialization the file stores, so a re-add of the same definition is
|
|
183
|
+
* a clean "already-present" no-op. A global same-name entry is NOT a
|
|
184
|
+
* conflict here (workspace shadowing a global entry is a legal,
|
|
185
|
+
* documented merge outcome), just noted in the detail. */
|
|
186
|
+
export declare function collectMcpAddWorkspacePlan(name: string | undefined, raw: McpAddRawArgs, workspaceRoot: string, homeDir?: string): McpAddPlan;
|
|
152
187
|
export type McpSetAuthAction = "updated" | "already-set" | "not-found" | "invalid-input";
|
|
153
188
|
export interface McpSetAuthPlan {
|
|
154
189
|
name: string;
|
|
@@ -168,6 +203,8 @@ export interface McpSetAuthRawArgs {
|
|
|
168
203
|
clientId?: string;
|
|
169
204
|
/** A variable NAME, never a secret value. */
|
|
170
205
|
clientSecretEnv?: string;
|
|
206
|
+
/** See `McpAddRawArgs.unknown`. */
|
|
207
|
+
unknown?: string[];
|
|
171
208
|
}
|
|
172
209
|
/**
|
|
173
210
|
* Sets or clears the OAuth classification, and optionally the
|
|
@@ -269,10 +306,22 @@ export declare function applyMcpAddWithSync(plan: McpAddPlan, opts?: {
|
|
|
269
306
|
dryRun?: boolean;
|
|
270
307
|
backupSession?: BackupSession;
|
|
271
308
|
}): Promise<McpAddOutcome>;
|
|
309
|
+
/** Workspace twin of `applyMcpAddWithSync` (trellis-workspace-scope task
|
|
310
|
+
* 3.4): writes `.agents/config.json` instead of `servers.yaml`, then
|
|
311
|
+
* cascades the same mcp sync — which, run inside the workspace, renders
|
|
312
|
+
* only the project-level targets for the workspace layer (plus the usual
|
|
313
|
+
* global-layer targets; the agent does its own layering). */
|
|
314
|
+
export declare function applyMcpAddWorkspaceWithSync(plan: McpAddPlan, workspaceRoot: string, opts?: {
|
|
315
|
+
homeDir?: string;
|
|
316
|
+
dryRun?: boolean;
|
|
317
|
+
backupSession?: BackupSession;
|
|
318
|
+
}): Promise<McpAddOutcome>;
|
|
272
319
|
export declare function runMcpAdd(name: string | undefined, raw: McpAddRawArgs, opts?: {
|
|
273
320
|
homeDir?: string;
|
|
274
321
|
json?: boolean;
|
|
275
322
|
dryRun?: boolean;
|
|
323
|
+
workspace?: boolean;
|
|
324
|
+
workspaceDir?: string;
|
|
276
325
|
}): Promise<{
|
|
277
326
|
exitCode: number;
|
|
278
327
|
}>;
|
|
@@ -299,10 +348,27 @@ export declare function applyMcpRemoveWithSync(plan: McpRemovePlan, opts?: {
|
|
|
299
348
|
dryRun?: boolean;
|
|
300
349
|
backupSession?: BackupSession;
|
|
301
350
|
}): Promise<McpRemoveOutcome>;
|
|
351
|
+
/** Workspace twin of `collectMcpRemovePlan`: presence is judged against
|
|
352
|
+
* `.agents/config.json` only — a global entry of the same name is this
|
|
353
|
+
* entry's shadow target, not a reason to touch the global file. */
|
|
354
|
+
export declare function collectMcpRemoveWorkspacePlan(name: string, workspaceRoot: string): McpRemovePlan;
|
|
355
|
+
/** Workspace twin of `applyMcpRemoveWithSync`: deletes the entry from
|
|
356
|
+
* `.agents/config.json` (backup-session wrapped), then cascades the same
|
|
357
|
+
* mcp sync — run inside the workspace, so `planWorkspaceMcp` sees the
|
|
358
|
+
* entry is gone and removes the rendered project-level copies (the
|
|
359
|
+
* ledger-gated propagation already exists; this command was the missing
|
|
360
|
+
* corner). A shadowed global entry simply takes effect again. */
|
|
361
|
+
export declare function applyMcpRemoveWorkspaceWithSync(plan: McpRemovePlan, workspaceRoot: string, opts?: {
|
|
362
|
+
homeDir?: string;
|
|
363
|
+
dryRun?: boolean;
|
|
364
|
+
backupSession?: BackupSession;
|
|
365
|
+
}): Promise<McpRemoveOutcome>;
|
|
302
366
|
export declare function runMcpRemove(name: string, opts?: {
|
|
303
367
|
homeDir?: string;
|
|
304
368
|
json?: boolean;
|
|
305
369
|
dryRun?: boolean;
|
|
370
|
+
workspace?: boolean;
|
|
371
|
+
workspaceDir?: string;
|
|
306
372
|
}): Promise<{
|
|
307
373
|
exitCode: number;
|
|
308
374
|
}>;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -27,6 +27,11 @@ 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, removeWorkspaceServer, upsertWorkspaceServer } from "../lib/workspaceConfig.js";
|
|
33
|
+
import { toServerDefYaml } from "../core/mcpServerDef.js";
|
|
34
|
+
import { flagValue, unknownFlags } from "../lib/cliArgs.js";
|
|
30
35
|
const ADAPTER_FACTORY = {
|
|
31
36
|
"claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
|
|
32
37
|
codex: (homeDir) => new CodexAdapter(homeDir),
|
|
@@ -42,14 +47,16 @@ function buildAdapters(homeDir, managedAgents) {
|
|
|
42
47
|
}
|
|
43
48
|
export async function collectMcpSyncReport(opts = {}) {
|
|
44
49
|
const homeDir = opts.homeDir ?? homedir();
|
|
45
|
-
const
|
|
50
|
+
const workspaceDir = opts.globalOnly ? null : (opts.workspaceDir !== undefined ? opts.workspaceDir : findWorkspaceRoot(process.cwd()));
|
|
51
|
+
const loaded = loadCanonicalSource({ homeDir, workspaceDir });
|
|
46
52
|
const canonical = {
|
|
47
53
|
...loaded,
|
|
48
54
|
...(opts.managedAgents ? { managedAgents: opts.managedAgents } : {}),
|
|
49
55
|
...(opts.mcp ? { mcp: opts.mcp } : {}),
|
|
50
56
|
};
|
|
51
57
|
const reports = [];
|
|
52
|
-
const
|
|
58
|
+
const writeMode = !opts.dryRun && !opts.check;
|
|
59
|
+
const ownSession = writeMode && !opts.backupSession ? openBackupSession(homeDir, "mcp-sync") : undefined;
|
|
53
60
|
const backup = opts.backupSession ?? ownSession;
|
|
54
61
|
for (const adapter of buildAdapters(homeDir, canonical.managedAgents)) {
|
|
55
62
|
const probeResult = await adapter.probe();
|
|
@@ -57,14 +64,17 @@ export async function collectMcpSyncReport(opts = {}) {
|
|
|
57
64
|
reports.push({ agent: adapter.id, present: false, items: [] });
|
|
58
65
|
continue;
|
|
59
66
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
const globalItems = (await adapter.plan(canonical)).filter((item) => item.kind === "mcp");
|
|
68
|
+
const wsItems = planWorkspaceMcp(adapter.id, canonical);
|
|
69
|
+
if (writeMode) {
|
|
70
|
+
await adapter.apply(globalItems, backup);
|
|
71
|
+
applyWorkspaceMcp(adapter.id, canonical, wsItems, backup);
|
|
63
72
|
}
|
|
64
|
-
|
|
73
|
+
const warnings = workspacePrecedenceWarnings(adapter.id, canonical);
|
|
74
|
+
reports.push({ agent: adapter.id, present: true, items: [...globalItems, ...wsItems], ...(warnings.length > 0 ? { warnings } : {}) });
|
|
65
75
|
}
|
|
66
76
|
ownSession?.finalize();
|
|
67
|
-
return { reports };
|
|
77
|
+
return { reports, ...(canonical.workspace ? { workspaceRoot: canonical.workspace.root } : {}) };
|
|
68
78
|
}
|
|
69
79
|
export async function runMcpSync(opts = {}) {
|
|
70
80
|
let report;
|
|
@@ -79,22 +89,31 @@ export async function runMcpSync(opts = {}) {
|
|
|
79
89
|
console.log(JSON.stringify(report, null, 2));
|
|
80
90
|
}
|
|
81
91
|
else {
|
|
82
|
-
printReport(report, opts.dryRun ?? false);
|
|
92
|
+
printReport(report, opts.dryRun ?? false, opts.check ?? false);
|
|
83
93
|
}
|
|
84
94
|
const hasConflict = report.reports.some((r) => r.items.some((i) => i.action === "conflict"));
|
|
95
|
+
if (opts.check) {
|
|
96
|
+
const hasPending = report.reports.some((r) => r.items.some((i) => i.action !== "conflict"));
|
|
97
|
+
return { exitCode: hasPending || hasConflict ? 1 : 0 };
|
|
98
|
+
}
|
|
85
99
|
return { exitCode: hasConflict ? 1 : 0 };
|
|
86
100
|
}
|
|
87
101
|
/** Exported so `onboard` prints an mcp-sync report identically to running
|
|
88
102
|
* `mcp sync` standalone, instead of a second, easily-drifting copy of this
|
|
89
103
|
* formatting. */
|
|
90
|
-
export function printReport(report, dryRun) {
|
|
104
|
+
export function printReport(report, dryRun, check = false) {
|
|
91
105
|
if (dryRun)
|
|
92
106
|
console.log("[dry run]");
|
|
107
|
+
if (check)
|
|
108
|
+
console.log("[check]");
|
|
109
|
+
if (report.workspaceRoot) {
|
|
110
|
+
console.log(`Workspace layer: ${report.workspaceRoot} (.agents/)`);
|
|
111
|
+
}
|
|
93
112
|
if (report.reports.length === 0) {
|
|
94
113
|
console.log("No managed agents yet — run `trellis onboard` or list agent ids in ~/.trellis/managed.yaml.");
|
|
95
114
|
return;
|
|
96
115
|
}
|
|
97
|
-
for (const { agent, present, items } of report.reports) {
|
|
116
|
+
for (const { agent, present, items, warnings } of report.reports) {
|
|
98
117
|
if (!present) {
|
|
99
118
|
console.log(`— ${agent} (not installed)`);
|
|
100
119
|
continue;
|
|
@@ -104,18 +123,27 @@ export function printReport(report, dryRun) {
|
|
|
104
123
|
const conflicts = items.filter((i) => i.action === "conflict");
|
|
105
124
|
if (created.length === 0 && removed.length === 0 && conflicts.length === 0) {
|
|
106
125
|
console.log(`✅ ${agent} — already in sync`);
|
|
107
|
-
continue;
|
|
108
126
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
127
|
+
else {
|
|
128
|
+
const icon = conflicts.length > 0 ? "⚠️ " : "✅";
|
|
129
|
+
console.log(`${icon} ${agent} — ${created.length} created/updated, ${removed.length} removed, ${conflicts.length} conflict(s)`);
|
|
130
|
+
for (const item of [...created, ...removed, ...conflicts]) {
|
|
131
|
+
console.log(` - [${item.action}]${item.layer === "workspace" ? " (workspace)" : ""} ${item.description}`);
|
|
132
|
+
// Conflicts carry the actionable fix (e.g. the unmarked-overwrite
|
|
133
|
+
// guard's "run migrate --workspace or remove the entry") — printing
|
|
134
|
+
// only the description would leave the user stuck at "left untouched".
|
|
135
|
+
if (item.remediation)
|
|
136
|
+
console.log(` → ${item.remediation}`);
|
|
137
|
+
}
|
|
138
|
+
// pi does not watch mcp.json (verified 2026-10-11): without this hint
|
|
139
|
+
// a changed config silently only takes effect on the next pi launch
|
|
140
|
+
// (trellis-pi-native-mcp-migration design.md D5).
|
|
141
|
+
if (agent === "pi" && (created.length > 0 || removed.length > 0)) {
|
|
142
|
+
console.log(" pi does not watch mcp.json — restart pi or run /reload to pick up MCP changes");
|
|
143
|
+
}
|
|
113
144
|
}
|
|
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");
|
|
145
|
+
for (const warning of warnings ?? []) {
|
|
146
|
+
console.log(` ⚠ ${warning}`);
|
|
119
147
|
}
|
|
120
148
|
}
|
|
121
149
|
}
|
|
@@ -205,34 +233,39 @@ export function runMcpList(opts = {}) {
|
|
|
205
233
|
}
|
|
206
234
|
return { exitCode: 0 };
|
|
207
235
|
}
|
|
236
|
+
const MCP_ADD_KNOWN_FLAGS = [
|
|
237
|
+
"--transport", "--auth", "--command", "--args", "--url", "--headers",
|
|
238
|
+
"--env", "--static-env", "--agents", "--enabled",
|
|
239
|
+
"--workspace", "--dry-run", "--json",
|
|
240
|
+
];
|
|
208
241
|
export function parseMcpAddArgs(argv) {
|
|
209
|
-
const
|
|
210
|
-
const i = argv.indexOf(name);
|
|
211
|
-
return i >= 0 ? argv[i + 1] : undefined;
|
|
212
|
-
};
|
|
242
|
+
const unknown = unknownFlags(argv, MCP_ADD_KNOWN_FLAGS);
|
|
213
243
|
return {
|
|
214
|
-
transport:
|
|
215
|
-
auth:
|
|
216
|
-
command:
|
|
217
|
-
args:
|
|
218
|
-
url:
|
|
219
|
-
headers:
|
|
220
|
-
env:
|
|
221
|
-
staticEnv:
|
|
222
|
-
agents:
|
|
223
|
-
enabled:
|
|
244
|
+
transport: flagValue(argv, "--transport"),
|
|
245
|
+
auth: flagValue(argv, "--auth"),
|
|
246
|
+
command: flagValue(argv, "--command"),
|
|
247
|
+
args: flagValue(argv, "--args"),
|
|
248
|
+
url: flagValue(argv, "--url"),
|
|
249
|
+
headers: flagValue(argv, "--headers"),
|
|
250
|
+
env: flagValue(argv, "--env"),
|
|
251
|
+
staticEnv: flagValue(argv, "--static-env"),
|
|
252
|
+
agents: flagValue(argv, "--agents"),
|
|
253
|
+
enabled: flagValue(argv, "--enabled"),
|
|
254
|
+
...(unknown.length > 0 ? { unknown } : {}),
|
|
224
255
|
};
|
|
225
256
|
}
|
|
257
|
+
const MCP_SET_KNOWN_FLAGS = [
|
|
258
|
+
"--auth", "--client-id", "--client-secret-env", "--auth-owner",
|
|
259
|
+
"--dry-run", "--json",
|
|
260
|
+
];
|
|
226
261
|
export function parseMcpSetArgs(argv) {
|
|
227
|
-
const
|
|
228
|
-
const i = argv.indexOf(name);
|
|
229
|
-
return i >= 0 ? argv[i + 1] : undefined;
|
|
230
|
-
};
|
|
262
|
+
const unknown = unknownFlags(argv, MCP_SET_KNOWN_FLAGS);
|
|
231
263
|
return {
|
|
232
|
-
auth:
|
|
233
|
-
clientId:
|
|
234
|
-
clientSecretEnv:
|
|
235
|
-
authOwner:
|
|
264
|
+
auth: flagValue(argv, "--auth"),
|
|
265
|
+
clientId: flagValue(argv, "--client-id"),
|
|
266
|
+
clientSecretEnv: flagValue(argv, "--client-secret-env"),
|
|
267
|
+
authOwner: flagValue(argv, "--auth-owner"),
|
|
268
|
+
...(unknown.length > 0 ? { unknown } : {}),
|
|
236
269
|
};
|
|
237
270
|
}
|
|
238
271
|
function parseCsv(raw) {
|
|
@@ -260,37 +293,43 @@ const TRANSPORTS = ["stdio", "http", "sse"];
|
|
|
260
293
|
* write failure — design.md's open question, resolved in favor of
|
|
261
294
|
* refusing early with a specific reason. No `--force`: an existing name
|
|
262
295
|
* always refuses, ever (design.md D4).
|
|
296
|
+
*
|
|
297
|
+
* Split into two steps so `mcp add --workspace` reuses the identical
|
|
298
|
+
* flag validation/def construction and only swaps which file's existing
|
|
299
|
+
* entries it conflicts against (trellis-workspace-scope task 3.4):
|
|
300
|
+
* `buildMcpAddDef` is the pure part, `collectMcpAddPlan` adds the
|
|
301
|
+
* global-canonical existence check.
|
|
263
302
|
*/
|
|
264
|
-
export function
|
|
303
|
+
export function buildMcpAddDef(name, raw) {
|
|
265
304
|
if (!name) {
|
|
266
|
-
return { name: "(none)", action: "invalid-input", detail: "a name is required: trellis mcp add <name> --transport ..." };
|
|
305
|
+
return { ok: false, plan: { name: "(none)", action: "invalid-input", detail: "a name is required: trellis mcp add <name> --transport ..." } };
|
|
267
306
|
}
|
|
268
307
|
if (!raw.transport || !TRANSPORTS.includes(raw.transport)) {
|
|
269
|
-
return { name, action: "invalid-input", detail: `--transport must be one of: ${TRANSPORTS.join(", ")}` };
|
|
308
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--transport must be one of: ${TRANSPORTS.join(", ")}` } };
|
|
270
309
|
}
|
|
271
310
|
const transport = raw.transport;
|
|
272
311
|
if (transport === "stdio" && !raw.command) {
|
|
273
|
-
return { name, action: "invalid-input", detail: "--transport stdio requires --command" };
|
|
312
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: "--transport stdio requires --command" } };
|
|
274
313
|
}
|
|
275
314
|
if ((transport === "http" || transport === "sse") && !raw.url) {
|
|
276
|
-
return { name, action: "invalid-input", detail: `--transport ${transport} requires --url` };
|
|
315
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--transport ${transport} requires --url` } };
|
|
277
316
|
}
|
|
278
317
|
if (raw.auth !== undefined && raw.auth !== "oauth") {
|
|
279
|
-
return { name, action: "invalid-input", detail: `--auth must be "oauth" when provided (got ${raw.auth})` };
|
|
318
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--auth must be "oauth" when provided (got ${raw.auth})` } };
|
|
280
319
|
}
|
|
281
320
|
if (raw.auth === "oauth" && transport === "stdio") {
|
|
282
|
-
return { name, action: "invalid-input", detail: "--auth oauth requires an http or sse MCP server" };
|
|
321
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: "--auth oauth requires an http or sse MCP server" } };
|
|
283
322
|
}
|
|
284
323
|
const agentsList = parseCsv(raw.agents);
|
|
285
324
|
if (agentsList) {
|
|
286
325
|
for (const id of agentsList) {
|
|
287
326
|
if (!ALL_AGENTS.includes(id)) {
|
|
288
|
-
return { name, action: "invalid-input", detail: `--agents "${id}" is not a recognized agent id (${ALL_AGENTS.join(", ")})` };
|
|
327
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--agents "${id}" is not a recognized agent id (${ALL_AGENTS.join(", ")})` } };
|
|
289
328
|
}
|
|
290
329
|
}
|
|
291
330
|
}
|
|
292
331
|
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})` };
|
|
332
|
+
return { ok: false, plan: { name, action: "invalid-input", detail: `--enabled must be "true" or "false" (got ${raw.enabled})` } };
|
|
294
333
|
}
|
|
295
334
|
const def = {
|
|
296
335
|
transport,
|
|
@@ -304,15 +343,46 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
|
|
|
304
343
|
...(raw.enabled !== undefined ? { enabled: raw.enabled === "true" } : {}),
|
|
305
344
|
...(agentsList ? { agents: agentsList } : {}),
|
|
306
345
|
};
|
|
346
|
+
return { ok: true, name, def };
|
|
347
|
+
}
|
|
348
|
+
export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
|
|
349
|
+
const built = buildMcpAddDef(name, raw);
|
|
350
|
+
if (!built.ok)
|
|
351
|
+
return built.plan;
|
|
352
|
+
const def = built.def;
|
|
307
353
|
const canonical = loadCanonicalSource(homeDir);
|
|
308
|
-
const existing = canonical.mcp.servers[name];
|
|
354
|
+
const existing = canonical.mcp.servers[built.name];
|
|
309
355
|
if (existing) {
|
|
310
356
|
if (JSON.stringify(existing) === JSON.stringify(def)) {
|
|
311
|
-
return { name, action: "already-present", detail: "canonical entry is already identical" };
|
|
357
|
+
return { name: built.name, action: "already-present", detail: "canonical entry is already identical" };
|
|
312
358
|
}
|
|
313
|
-
return { name, action: "conflict", detail: `servers.yaml already has "${name}" with different settings — resolve by hand (no --force, design.md D4)` };
|
|
359
|
+
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
360
|
}
|
|
315
|
-
return { name, action: "create", detail: "will add to servers.yaml", def };
|
|
361
|
+
return { name: built.name, action: "create", detail: "will add to servers.yaml", def };
|
|
362
|
+
}
|
|
363
|
+
/** The workspace twin of `collectMcpAddPlan` (trellis-workspace-scope
|
|
364
|
+
* task 3.4): identical validation, but the existence check runs against
|
|
365
|
+
* `.agents/config.json` — compared through the same snake_case
|
|
366
|
+
* serialization the file stores, so a re-add of the same definition is
|
|
367
|
+
* a clean "already-present" no-op. A global same-name entry is NOT a
|
|
368
|
+
* conflict here (workspace shadowing a global entry is a legal,
|
|
369
|
+
* documented merge outcome), just noted in the detail. */
|
|
370
|
+
export function collectMcpAddWorkspacePlan(name, raw, workspaceRoot, homeDir = homedir()) {
|
|
371
|
+
const built = buildMcpAddDef(name, raw);
|
|
372
|
+
if (!built.ok)
|
|
373
|
+
return built.plan;
|
|
374
|
+
const def = built.def;
|
|
375
|
+
const config = readWorkspaceConfigJson(workspaceRoot);
|
|
376
|
+
const existing = config.mcp?.servers?.[built.name];
|
|
377
|
+
if (existing) {
|
|
378
|
+
if (JSON.stringify(existing) === JSON.stringify(toServerDefYaml(def))) {
|
|
379
|
+
return { name: built.name, action: "already-present", detail: ".agents/config.json entry is already identical" };
|
|
380
|
+
}
|
|
381
|
+
return { name: built.name, action: "conflict", detail: `.agents/config.json already has "${built.name}" with different settings — resolve by hand (no --force)` };
|
|
382
|
+
}
|
|
383
|
+
const global = loadCanonicalSource(homeDir).mcp.servers[built.name];
|
|
384
|
+
const shadowNote = global ? ` (shadows the global "${built.name}" for this project)` : "";
|
|
385
|
+
return { name: built.name, action: "create", detail: `will add to .agents/config.json${shadowNote}`, def };
|
|
316
386
|
}
|
|
317
387
|
/**
|
|
318
388
|
* Sets or clears the OAuth classification, and optionally the
|
|
@@ -427,6 +497,7 @@ export function collectMcpSetAuthPlan(name, raw, homeDir = homedir()) {
|
|
|
427
497
|
}
|
|
428
498
|
export function runMcpSetAuth(name, raw, opts = {}) {
|
|
429
499
|
const homeDir = opts.homeDir ?? homedir();
|
|
500
|
+
warnUnknownFlags("mcp set", raw.unknown);
|
|
430
501
|
let plan;
|
|
431
502
|
try {
|
|
432
503
|
plan = collectMcpSetAuthPlan(name, raw, homeDir);
|
|
@@ -587,8 +658,64 @@ export async function applyMcpAddWithSync(plan, opts = {}) {
|
|
|
587
658
|
}
|
|
588
659
|
return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
|
|
589
660
|
}
|
|
661
|
+
/** Workspace twin of `applyMcpAddWithSync` (trellis-workspace-scope task
|
|
662
|
+
* 3.4): writes `.agents/config.json` instead of `servers.yaml`, then
|
|
663
|
+
* cascades the same mcp sync — which, run inside the workspace, renders
|
|
664
|
+
* only the project-level targets for the workspace layer (plus the usual
|
|
665
|
+
* global-layer targets; the agent does its own layering). */
|
|
666
|
+
export async function applyMcpAddWorkspaceWithSync(plan, workspaceRoot, opts = {}) {
|
|
667
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
668
|
+
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "mcp-add") : undefined;
|
|
669
|
+
const session = opts.backupSession ?? ownSession;
|
|
670
|
+
if (!opts.dryRun && plan.action === "create" && plan.def) {
|
|
671
|
+
upsertWorkspaceServer(workspaceRoot, plan.name, plan.def, session);
|
|
672
|
+
}
|
|
673
|
+
ownSession?.finalize();
|
|
674
|
+
const planFailed = plan.action === "conflict" || plan.action === "invalid-input";
|
|
675
|
+
let syncReport;
|
|
676
|
+
if (!planFailed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
|
|
677
|
+
syncReport = await collectMcpSyncReport({ homeDir, workspaceDir: workspaceRoot, dryRun: opts.dryRun });
|
|
678
|
+
}
|
|
679
|
+
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
680
|
+
}
|
|
681
|
+
/** Warn about `--*` tokens the parser didn't claim — a typo like
|
|
682
|
+
* `--tranport http` must not exit 0 in silence. */
|
|
683
|
+
function warnUnknownFlags(commandLabel, unknown) {
|
|
684
|
+
if (unknown && unknown.length > 0) {
|
|
685
|
+
console.error(`warning: ${commandLabel}: unrecognized flag(s) ${unknown.map((f) => `"${f}"`).join(", ")} — ignored`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
590
688
|
export async function runMcpAdd(name, raw, opts = {}) {
|
|
591
689
|
const homeDir = opts.homeDir ?? homedir();
|
|
690
|
+
warnUnknownFlags("mcp add", raw.unknown);
|
|
691
|
+
if (opts.workspace) {
|
|
692
|
+
const workspaceRoot = opts.workspaceDir ?? findWorkspaceRoot(process.cwd());
|
|
693
|
+
if (!workspaceRoot) {
|
|
694
|
+
console.error("no .agents/ found here or above — run `trellis init --workspace` first (or pass --workspace from inside the repo)");
|
|
695
|
+
return { exitCode: 1 };
|
|
696
|
+
}
|
|
697
|
+
let plan;
|
|
698
|
+
try {
|
|
699
|
+
plan = collectMcpAddWorkspacePlan(name, raw, workspaceRoot, homeDir);
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
703
|
+
return { exitCode: 1 };
|
|
704
|
+
}
|
|
705
|
+
const { sync: syncReport } = await applyMcpAddWorkspaceWithSync(plan, workspaceRoot, { homeDir, dryRun: opts.dryRun });
|
|
706
|
+
const planFailed = plan.action === "conflict" || plan.action === "invalid-input";
|
|
707
|
+
if (opts.json) {
|
|
708
|
+
console.log(JSON.stringify(syncReport ? { ...plan, workspace: workspaceRoot, sync: syncReport } : { ...plan, workspace: workspaceRoot }, null, 2));
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
console.log(`${opts.dryRun ? "[dry run] " : ""}mcp add ${plan.name} --workspace`);
|
|
712
|
+
console.log(` [${plan.action}] ${plan.detail}`);
|
|
713
|
+
if (syncReport)
|
|
714
|
+
printReport(syncReport, opts.dryRun ?? false);
|
|
715
|
+
}
|
|
716
|
+
const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
|
|
717
|
+
return { exitCode: planFailed || syncConflict ? 1 : 0 };
|
|
718
|
+
}
|
|
592
719
|
let plan;
|
|
593
720
|
try {
|
|
594
721
|
plan = collectMcpAddPlan(name, raw, homeDir);
|
|
@@ -655,8 +782,65 @@ export async function applyMcpRemoveWithSync(plan, opts = {}) {
|
|
|
655
782
|
}
|
|
656
783
|
return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
|
|
657
784
|
}
|
|
785
|
+
/** Workspace twin of `collectMcpRemovePlan`: presence is judged against
|
|
786
|
+
* `.agents/config.json` only — a global entry of the same name is this
|
|
787
|
+
* entry's shadow target, not a reason to touch the global file. */
|
|
788
|
+
export function collectMcpRemoveWorkspacePlan(name, workspaceRoot) {
|
|
789
|
+
const config = readWorkspaceConfigJson(workspaceRoot);
|
|
790
|
+
return { name, action: config.mcp?.servers?.[name] ? "removed" : "not-found" };
|
|
791
|
+
}
|
|
792
|
+
/** Workspace twin of `applyMcpRemoveWithSync`: deletes the entry from
|
|
793
|
+
* `.agents/config.json` (backup-session wrapped), then cascades the same
|
|
794
|
+
* mcp sync — run inside the workspace, so `planWorkspaceMcp` sees the
|
|
795
|
+
* entry is gone and removes the rendered project-level copies (the
|
|
796
|
+
* ledger-gated propagation already exists; this command was the missing
|
|
797
|
+
* corner). A shadowed global entry simply takes effect again. */
|
|
798
|
+
export async function applyMcpRemoveWorkspaceWithSync(plan, workspaceRoot, opts = {}) {
|
|
799
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
800
|
+
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "removed" ? openBackupSession(homeDir, "mcp-remove") : undefined;
|
|
801
|
+
const session = opts.backupSession ?? ownSession;
|
|
802
|
+
if (!opts.dryRun && plan.action === "removed") {
|
|
803
|
+
removeWorkspaceServer(workspaceRoot, plan.name, session);
|
|
804
|
+
}
|
|
805
|
+
ownSession?.finalize();
|
|
806
|
+
const planFailed = plan.action === "not-found";
|
|
807
|
+
let syncReport;
|
|
808
|
+
if (!planFailed && existsSync(join(homeDir, ".trellis"))) {
|
|
809
|
+
syncReport = await collectMcpSyncReport({ homeDir, workspaceDir: workspaceRoot, dryRun: opts.dryRun });
|
|
810
|
+
}
|
|
811
|
+
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
812
|
+
}
|
|
658
813
|
export async function runMcpRemove(name, opts = {}) {
|
|
659
814
|
const homeDir = opts.homeDir ?? homedir();
|
|
815
|
+
if (opts.workspace) {
|
|
816
|
+
const workspaceRoot = opts.workspaceDir ?? findWorkspaceRoot(process.cwd());
|
|
817
|
+
if (!workspaceRoot) {
|
|
818
|
+
console.error("no .agents/ found here or above — run `trellis init --workspace` first (or pass --workspace from inside the repo)");
|
|
819
|
+
return { exitCode: 1 };
|
|
820
|
+
}
|
|
821
|
+
let plan;
|
|
822
|
+
try {
|
|
823
|
+
plan = collectMcpRemoveWorkspacePlan(name, workspaceRoot);
|
|
824
|
+
}
|
|
825
|
+
catch (err) {
|
|
826
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
827
|
+
return { exitCode: 1 };
|
|
828
|
+
}
|
|
829
|
+
const { sync: syncReport } = await applyMcpRemoveWorkspaceWithSync(plan, workspaceRoot, { homeDir, dryRun: opts.dryRun });
|
|
830
|
+
if (opts.json) {
|
|
831
|
+
console.log(JSON.stringify(syncReport ? { ...plan, workspace: workspaceRoot, sync: syncReport } : { ...plan, workspace: workspaceRoot }, null, 2));
|
|
832
|
+
}
|
|
833
|
+
else if (plan.action === "not-found") {
|
|
834
|
+
console.error(`"${name}" is not in the workspace layer (${workspaceRoot}/.agents/config.json) — nothing to remove.`);
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
console.log(`${opts.dryRun ? "[dry run] " : ""}removed MCP server "${name}" from ${workspaceRoot}/.agents/config.json.`);
|
|
838
|
+
if (syncReport)
|
|
839
|
+
printReport(syncReport, opts.dryRun ?? false);
|
|
840
|
+
}
|
|
841
|
+
const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
|
|
842
|
+
return { exitCode: plan.action === "not-found" || syncConflict ? 1 : 0 };
|
|
843
|
+
}
|
|
660
844
|
let plan;
|
|
661
845
|
try {
|
|
662
846
|
plan = collectMcpRemovePlan(name, homeDir);
|
|
@@ -680,7 +864,22 @@ export async function runMcpRemove(name, opts = {}) {
|
|
|
680
864
|
console.log(JSON.stringify(syncReport ? { ...payload, sync: syncReport } : payload, null, 2));
|
|
681
865
|
}
|
|
682
866
|
else if (plan.action === "not-found") {
|
|
683
|
-
|
|
867
|
+
// A name that lives only in the workspace layer used to get this same
|
|
868
|
+
// blunt message (found by the v0.15.0 real-machine smoke test) —
|
|
869
|
+
// point at the flag instead of implying the entry doesn't exist.
|
|
870
|
+
let workspaceHint = "";
|
|
871
|
+
const workspaceRoot = opts.workspaceDir !== undefined ? opts.workspaceDir : findWorkspaceRoot(process.cwd());
|
|
872
|
+
if (workspaceRoot) {
|
|
873
|
+
try {
|
|
874
|
+
if (readWorkspaceConfigJson(workspaceRoot).mcp?.servers?.[name]) {
|
|
875
|
+
workspaceHint = ` here, but it exists in the workspace layer — re-run with --workspace to remove it from ${workspaceRoot}/.agents/config.json`;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
// An unreadable workspace config must not mask the primary answer.
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
console.error(`"${name}" is not a canonical MCP server${workspaceHint} — nothing to remove.`);
|
|
684
883
|
}
|
|
685
884
|
else if (writeError) {
|
|
686
885
|
console.error(` write failed: ${writeError}`);
|
|
@@ -28,16 +28,16 @@ import { AgentBridgeProvider } from "../lib/agentBridgeProvider.js";
|
|
|
28
28
|
import { RuntimeMemoryProvider } from "../lib/memoryProvider.js";
|
|
29
29
|
import { SkillProvider } from "../lib/skillProvider.js";
|
|
30
30
|
import { ALL_AGENTS, isOAuthAuth } from "../core/types.js";
|
|
31
|
+
import { flagValue } from "../lib/cliArgs.js";
|
|
31
32
|
const CLIENT_INFO = { name: "trellis-mcp-gateway", version: "0.0.0" };
|
|
32
33
|
const SERVER_INFO = { name: "trellis-mcp-gateway", version: "0.0.0" };
|
|
33
34
|
const RUNTIME_CLIENT_INFO = { name: "trellis-mcp-runtime", version: "0.0.0" };
|
|
34
35
|
const RUNTIME_SERVER_INFO = { name: "trellis-mcp-runtime", version: "0.0.0" };
|
|
35
36
|
export function parseMcpGatewayArgs(argv) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
37
|
+
const value = flagValue(argv, "--agent");
|
|
38
|
+
if (value === undefined) {
|
|
38
39
|
return { error: `mcp-gateway requires --agent <${ALL_AGENTS.join("|")}>` };
|
|
39
40
|
}
|
|
40
|
-
const value = argv[index + 1];
|
|
41
41
|
if (!ALL_AGENTS.includes(value)) {
|
|
42
42
|
return { error: `unknown agent "${value}" — expected one of ${ALL_AGENTS.join(", ")}` };
|
|
43
43
|
}
|
|
@@ -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
|