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
|
@@ -18,6 +18,7 @@ import * as claudeCodeProbe from "../probes/claude-code.js";
|
|
|
18
18
|
import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
19
19
|
import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
|
|
20
20
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
21
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
21
22
|
export class ClaudeCodeAdapter {
|
|
22
23
|
homeDir;
|
|
23
24
|
name = "Claude Code";
|
|
@@ -32,7 +33,7 @@ export class ClaudeCodeAdapter {
|
|
|
32
33
|
async plan(canonical) {
|
|
33
34
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
34
35
|
const skillsRoot = join(this.homeDir, ".claude", "skills");
|
|
35
|
-
const desiredSkills = canonical
|
|
36
|
+
const desiredSkills = globalLayerSkills(canonical)
|
|
36
37
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
37
38
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
38
39
|
const skillItems = planSymlinks({
|
|
@@ -54,7 +55,7 @@ export class ClaudeCodeAdapter {
|
|
|
54
55
|
const configPath = join(this.homeDir, ".claude.json");
|
|
55
56
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
56
57
|
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
|
57
|
-
return planJsonMcp({ configPath, parsed, mcp: canonical
|
|
58
|
+
return planJsonMcp({ configPath, parsed, mcp: globalLayerMcp(canonical), agentId: this.id, managedAgents: canonical.managedAgents, policy: canonical.secretsPolicy, ownership });
|
|
58
59
|
}
|
|
59
60
|
async apply(plan, backup) {
|
|
60
61
|
await applySymlinkPlan(plan.filter((item) => item.kind !== "mcp"), backup);
|
|
@@ -83,7 +84,7 @@ export class ClaudeCodeAdapter {
|
|
|
83
84
|
return { ok: false, mismatches: ["claude-code is not present on this machine"] };
|
|
84
85
|
}
|
|
85
86
|
const mismatches = [];
|
|
86
|
-
const desiredNames = new Set(canonical.
|
|
87
|
+
const desiredNames = new Set(globalLayerSkills(canonical).filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
87
88
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
88
89
|
for (const name of desiredNames) {
|
|
89
90
|
if (!actualSkills.has(name)) {
|
package/dist/adapters/codex.js
CHANGED
|
@@ -26,6 +26,7 @@ import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
|
26
26
|
import { resolveMcpPlan } from "./mcpPlan.js";
|
|
27
27
|
import { currentServerSectionText, removeSection, renderServerSection, upsertSection } from "../lib/tomlSection.js";
|
|
28
28
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
29
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
29
30
|
export class CodexAdapter {
|
|
30
31
|
homeDir;
|
|
31
32
|
name = "Codex";
|
|
@@ -40,7 +41,7 @@ export class CodexAdapter {
|
|
|
40
41
|
async plan(canonical) {
|
|
41
42
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
42
43
|
const skillsRoot = join(this.homeDir, ".agents", "skills");
|
|
43
|
-
const desiredSkills = canonical
|
|
44
|
+
const desiredSkills = globalLayerSkills(canonical)
|
|
44
45
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
45
46
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
46
47
|
const skillItems = planSymlinks({
|
|
@@ -67,7 +68,7 @@ export class CodexAdapter {
|
|
|
67
68
|
planMcp(canonical) {
|
|
68
69
|
const configTomlPath = join(this.homeDir, ".codex", "config.toml");
|
|
69
70
|
const content = existsSync(configTomlPath) ? readFileSync(configTomlPath, "utf-8") : "";
|
|
70
|
-
const { desired, conflicts } = resolveMcpPlan(this.id, canonical
|
|
71
|
+
const { desired, conflicts } = resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
|
|
71
72
|
const items = [];
|
|
72
73
|
for (const { name, def } of desired) {
|
|
73
74
|
const current = currentServerSectionText(content, name);
|
|
@@ -137,7 +138,7 @@ export class CodexAdapter {
|
|
|
137
138
|
return { ok: false, mismatches: ["codex is not present on this machine"] };
|
|
138
139
|
}
|
|
139
140
|
const mismatches = [];
|
|
140
|
-
const desiredNames = new Set(canonical.
|
|
141
|
+
const desiredNames = new Set(globalLayerSkills(canonical).filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
141
142
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
142
143
|
for (const name of desiredNames) {
|
|
143
144
|
if (!actualSkills.has(name)) {
|
|
@@ -16,6 +16,7 @@ import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
|
|
|
16
16
|
import { RUNTIME_COMMAND, RUNTIME_ENTRY_NAME } from "./mcpPlan.js";
|
|
17
17
|
import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
18
18
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
19
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
19
20
|
const KIMI_RUNTIME_STARTUP_TIMEOUT_MS = 30_000;
|
|
20
21
|
function renderKimiServerEntry(def, name) {
|
|
21
22
|
const entry = renderJsonServerEntry(def);
|
|
@@ -46,7 +47,7 @@ export class KimiCodeAdapter {
|
|
|
46
47
|
items.push(...planSymlinks({
|
|
47
48
|
rootDir: join(root, "skills"),
|
|
48
49
|
desired: native
|
|
49
|
-
? canonical
|
|
50
|
+
? globalLayerSkills(canonical)
|
|
50
51
|
.filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
51
52
|
.map((skill) => ({ name: skill.name, target: skill.dir }))
|
|
52
53
|
: [],
|
|
@@ -65,7 +66,7 @@ export class KimiCodeAdapter {
|
|
|
65
66
|
items.push(...planJsonMcp({
|
|
66
67
|
configPath,
|
|
67
68
|
parsed,
|
|
68
|
-
mcp: canonical
|
|
69
|
+
mcp: globalLayerMcp(canonical),
|
|
69
70
|
agentId: this.id,
|
|
70
71
|
managedAgents: canonical.managedAgents,
|
|
71
72
|
policy: canonical.secretsPolicy,
|
|
@@ -96,7 +97,7 @@ export class KimiCodeAdapter {
|
|
|
96
97
|
const snapshot = await kimiProbe.probe(this.homeDir);
|
|
97
98
|
if (!snapshot.present)
|
|
98
99
|
return { ok: false, mismatches: ["kimi-code is not present on this machine"] };
|
|
99
|
-
const desired = new Set(canonical.
|
|
100
|
+
const desired = new Set(globalLayerSkills(canonical).filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents)).map((skill) => skill.name));
|
|
100
101
|
const actual = new Set(snapshot.skillRoots.flatMap((root) => root.skills).filter((skill) => skill.dir.includes("/.kimi-code/skills/")).map((skill) => skill.name));
|
|
101
102
|
const mismatches = [
|
|
102
103
|
...[...desired].filter((name) => !actual.has(name)).map((name) => `skill "${name}" is missing on Kimi Code`),
|
package/dist/adapters/kiro.js
CHANGED
|
@@ -26,6 +26,7 @@ import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
|
|
|
26
26
|
import { resolveMcpPlan } from "./mcpPlan.js";
|
|
27
27
|
import { declaredEnvNames } from "../lib/envVarNames.js";
|
|
28
28
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
29
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
29
30
|
/** VS-Code-family global settings path. macOS only — see
|
|
30
31
|
* trellis-kiro-approved-env-vars proposal.md Non-Goals: Linux/Windows
|
|
31
32
|
* equivalents are the well-known convention but unverified against a
|
|
@@ -48,7 +49,7 @@ export class KiroAdapter {
|
|
|
48
49
|
async plan(canonical) {
|
|
49
50
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
50
51
|
const skillsRoot = join(this.homeDir, ".kiro", "skills");
|
|
51
|
-
const desiredSkills = canonical
|
|
52
|
+
const desiredSkills = globalLayerSkills(canonical)
|
|
52
53
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
53
54
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
54
55
|
const skillItems = planSymlinks({
|
|
@@ -71,13 +72,13 @@ export class KiroAdapter {
|
|
|
71
72
|
const configPath = join(this.homeDir, ".kiro", "settings", "mcp.json");
|
|
72
73
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
73
74
|
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
|
74
|
-
return planJsonMcp({ configPath, parsed, mcp: canonical
|
|
75
|
+
return planJsonMcp({ configPath, parsed, mcp: globalLayerMcp(canonical), agentId: this.id, managedAgents: canonical.managedAgents, policy: canonical.secretsPolicy, ownership });
|
|
75
76
|
}
|
|
76
77
|
/** Names come from `resolveMcpPlan`, not a raw scan of `canonical.mcp.servers`
|
|
77
78
|
* (design.md D2) — a server scoped away from Kiro, or refused for a
|
|
78
79
|
* known_host_injected collision, never contributes a name here either. */
|
|
79
80
|
desiredApprovedEnvVars(canonical) {
|
|
80
|
-
const { desired } = resolveMcpPlan(this.id, canonical
|
|
81
|
+
const { desired } = resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
|
|
81
82
|
const names = new Set();
|
|
82
83
|
for (const { def } of desired) {
|
|
83
84
|
for (const name of declaredEnvNames(def))
|
|
@@ -162,7 +163,7 @@ export class KiroAdapter {
|
|
|
162
163
|
return { ok: false, mismatches: ["kiro is not present on this machine"] };
|
|
163
164
|
}
|
|
164
165
|
const mismatches = [];
|
|
165
|
-
const desiredNames = new Set(canonical.
|
|
166
|
+
const desiredNames = new Set(globalLayerSkills(canonical).filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
166
167
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
167
168
|
for (const name of desiredNames) {
|
|
168
169
|
if (!actualSkills.has(name)) {
|
package/dist/adapters/pi.js
CHANGED
|
@@ -36,6 +36,7 @@ import { applyJsonMcp, planJsonMcp } from "./jsonMcp.js";
|
|
|
36
36
|
import { piRenderer, supportsNativeMcp } from "./piMcp.js";
|
|
37
37
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
38
38
|
import { currentLinkTarget } from "../lib/backup.js";
|
|
39
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
39
40
|
const BRIDGE_SYMLINK_NAME = "trellis-mcp-bridge.js";
|
|
40
41
|
/**
|
|
41
42
|
* A source-checkout bridge can survive a switch to the packaged CLI. Adopt it
|
|
@@ -97,7 +98,7 @@ export class PiAdapter {
|
|
|
97
98
|
async plan(canonical) {
|
|
98
99
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
99
100
|
const agentDir = join(this.homeDir, ".pi", "agent");
|
|
100
|
-
const desiredSkills = canonical
|
|
101
|
+
const desiredSkills = globalLayerSkills(canonical)
|
|
101
102
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
102
103
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
103
104
|
const skillItems = planSymlinks({
|
|
@@ -146,7 +147,7 @@ export class PiAdapter {
|
|
|
146
147
|
const items = planJsonMcp({
|
|
147
148
|
configPath,
|
|
148
149
|
parsed,
|
|
149
|
-
mcp: canonical
|
|
150
|
+
mcp: globalLayerMcp(canonical),
|
|
150
151
|
agentId: this.id,
|
|
151
152
|
managedAgents: canonical.managedAgents,
|
|
152
153
|
policy: canonical.secretsPolicy,
|
|
@@ -233,7 +234,7 @@ export class PiAdapter {
|
|
|
233
234
|
return { ok: false, mismatches: ["pi is not present on this machine"] };
|
|
234
235
|
}
|
|
235
236
|
const mismatches = [];
|
|
236
|
-
const desiredNames = new Set(canonical.
|
|
237
|
+
const desiredNames = new Set(globalLayerSkills(canonical).filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
237
238
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
238
239
|
for (const name of desiredNames) {
|
|
239
240
|
if (!actualSkills.has(name)) {
|
package/dist/adapters/zcode.d.ts
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
* native Skill switches through a ledger-backed, reversible config change.
|
|
7
7
|
*/
|
|
8
8
|
import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
|
|
9
|
-
import type { CanonicalSource } from "../core/types.js";
|
|
9
|
+
import type { CanonicalSource, McpConfig, SecretsPolicy } from "../core/types.js";
|
|
10
10
|
import type { BackupSession } from "../lib/backup.js";
|
|
11
|
+
type JsonRecord = Record<string, unknown>;
|
|
12
|
+
export declare function planZcodeMcp(opts: {
|
|
13
|
+
configPath: string;
|
|
14
|
+
parsed: JsonRecord | undefined;
|
|
15
|
+
mcp: McpConfig;
|
|
16
|
+
managedAgents: readonly import("../core/types.js").AgentId[];
|
|
17
|
+
policy: SecretsPolicy;
|
|
18
|
+
ownership: JsonRecord;
|
|
19
|
+
}): AdapterPlanItem[];
|
|
20
|
+
export declare function applyZcodeMcp(parsed: JsonRecord | undefined, items: readonly AdapterPlanItem[]): JsonRecord;
|
|
11
21
|
export declare class ZcodeAdapter implements TrellisAdapter {
|
|
12
22
|
private readonly homeDir;
|
|
13
23
|
private readonly env;
|
|
@@ -19,3 +29,4 @@ export declare class ZcodeAdapter implements TrellisAdapter {
|
|
|
19
29
|
apply(plan: AdapterPlanItem[], backup: BackupSession): Promise<void>;
|
|
20
30
|
verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
|
|
21
31
|
}
|
|
32
|
+
export {};
|
package/dist/adapters/zcode.js
CHANGED
|
@@ -17,6 +17,7 @@ import { deepEqual } from "../lib/deepEqual.js";
|
|
|
17
17
|
import { forgetOwned, loadMcpOwnership, ownedByAgent, recordOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
18
18
|
import { forgetZcodeSettingsOwnership, loadZcodeSettingsOwnership, saveZcodeSettingsOwnership } from "../lib/zcodeOwnership.js";
|
|
19
19
|
import * as zcodeProbe from "../probes/zcode.js";
|
|
20
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
20
21
|
function readConfig(path) {
|
|
21
22
|
if (!existsSync(path))
|
|
22
23
|
return undefined;
|
|
@@ -28,7 +29,7 @@ function record(value) {
|
|
|
28
29
|
function zcodeServers(parsed) {
|
|
29
30
|
return record(record(parsed?.mcp).servers);
|
|
30
31
|
}
|
|
31
|
-
function planZcodeMcp(opts) {
|
|
32
|
+
export function planZcodeMcp(opts) {
|
|
32
33
|
const { desired, conflicts } = resolveMcpPlan("zcode", opts.mcp, opts.managedAgents, opts.policy);
|
|
33
34
|
const existing = zcodeServers(opts.parsed);
|
|
34
35
|
const items = [];
|
|
@@ -65,7 +66,7 @@ function planZcodeMcp(opts) {
|
|
|
65
66
|
}
|
|
66
67
|
return items;
|
|
67
68
|
}
|
|
68
|
-
function applyZcodeMcp(parsed, items) {
|
|
69
|
+
export function applyZcodeMcp(parsed, items) {
|
|
69
70
|
const base = parsed ?? {};
|
|
70
71
|
const mcp = record(base.mcp);
|
|
71
72
|
const servers = { ...record(mcp.servers) };
|
|
@@ -124,7 +125,7 @@ export class ZcodeAdapter {
|
|
|
124
125
|
items.push(...planSymlinks({
|
|
125
126
|
rootDir: join(this.homeDir, ".zcode", "skills"),
|
|
126
127
|
desired: usesNativeCapabilityDelivery(this.id, canonical.mcp)
|
|
127
|
-
? canonical.
|
|
128
|
+
? globalLayerSkills(canonical).filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents)).map((skill) => ({ name: skill.name, target: skill.dir }))
|
|
128
129
|
: [],
|
|
129
130
|
canonicalRoot: join(canonicalRoot, "skills"),
|
|
130
131
|
kind: "skill",
|
|
@@ -138,7 +139,7 @@ export class ZcodeAdapter {
|
|
|
138
139
|
items.push(...planZcodeMcp({
|
|
139
140
|
configPath: profile.configPath,
|
|
140
141
|
parsed,
|
|
141
|
-
mcp: canonical
|
|
142
|
+
mcp: globalLayerMcp(canonical),
|
|
142
143
|
managedAgents: canonical.managedAgents,
|
|
143
144
|
policy: canonical.secretsPolicy,
|
|
144
145
|
ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
|
|
@@ -206,7 +207,7 @@ export class ZcodeAdapter {
|
|
|
206
207
|
if (!profile)
|
|
207
208
|
return { ok: false, mismatches: ["zcode profile cannot be selected"] };
|
|
208
209
|
const mismatches = [];
|
|
209
|
-
const desiredNativeSkills = canonical
|
|
210
|
+
const desiredNativeSkills = globalLayerSkills(canonical)
|
|
210
211
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
211
212
|
.map((skill) => skill.name);
|
|
212
213
|
const actualNativeSkills = new Set(snapshot.skillRoots.filter((root) => root.path === join(this.homeDir, ".zcode", "skills")).flatMap((root) => root.skills.map((skill) => skill.name)));
|
|
@@ -216,7 +217,7 @@ export class ZcodeAdapter {
|
|
|
216
217
|
const expectedMcp = planZcodeMcp({
|
|
217
218
|
configPath: profile.configPath,
|
|
218
219
|
parsed: readConfig(profile.configPath),
|
|
219
|
-
mcp: canonical
|
|
220
|
+
mcp: globalLayerMcp(canonical),
|
|
220
221
|
managedAgents: canonical.managedAgents,
|
|
221
222
|
policy: canonical.secretsPolicy,
|
|
222
223
|
ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { runDoctor } from "./commands/doctor.js";
|
|
8
8
|
import { runInit } from "./commands/init.js";
|
|
9
9
|
import { runMigrate } from "./commands/migrate.js";
|
|
10
|
+
import { runMigrateWorkspace } from "./commands/migrateWorkspace.js";
|
|
10
11
|
import { runOnboard } from "./commands/onboard.js";
|
|
11
12
|
import { runSync } from "./commands/sync.js";
|
|
12
13
|
import { runMcpSync, runMcpList, runMcpAdd, runMcpRemove, runMcpSetAuth, runMcpScope, parseMcpAddArgs, parseMcpSetArgs } from "./commands/mcp.js";
|
|
@@ -23,6 +24,7 @@ import { collectMemoryList, runMemoryExtraction, runMemorySync } from "./command
|
|
|
23
24
|
import { parseManageArgs, runManage } from "./commands/manage.js";
|
|
24
25
|
import { runKimi } from "./commands/kimi.js";
|
|
25
26
|
import { TRELLIS_VERSION } from "./lib/cliMetadata.js";
|
|
27
|
+
import { flagValue } from "./lib/cliArgs.js";
|
|
26
28
|
import { parseSyncArgs } from "./lib/syncArgs.js";
|
|
27
29
|
const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "mcp-gateway", "mcp-runtime", "skill", "add", "update", "memory", "manage", "kimi", "secrets", "rollback"];
|
|
28
30
|
function printUsage() {
|
|
@@ -74,6 +76,7 @@ Commands:
|
|
|
74
76
|
(never overwrites an existing file — fills in only what's
|
|
75
77
|
missing) and prints which agents are present
|
|
76
78
|
--json machine-readable output, no report text
|
|
79
|
+
--workspace scaffold .agents/ in the current project instead
|
|
77
80
|
migrate --from <agent>
|
|
78
81
|
Import an existing agent's real skills/instructions/MCP/memory
|
|
79
82
|
servers into canonical source (claude-code | codex | kiro |
|
|
@@ -84,6 +87,18 @@ Commands:
|
|
|
84
87
|
restrict to one category
|
|
85
88
|
--dry-run preview the plan, write nothing
|
|
86
89
|
--json machine-readable output, no report text
|
|
90
|
+
migrate --workspace
|
|
91
|
+
Absorb the repo's existing project-level agent config (all six
|
|
92
|
+
agents' MCP files + root AGENTS.md) into .agents/, then
|
|
93
|
+
re-render. Plaintext secrets are intercepted into the global
|
|
94
|
+
secret store as \${VAR} references, never written under
|
|
95
|
+
.agents/; *.local.* files are listed, never read.
|
|
96
|
+
--choose name=agentId pick a side for a cross-agent conflict
|
|
97
|
+
--dedup drop|override identical-to-global entries: skip
|
|
98
|
+
(default) or import as an intentional
|
|
99
|
+
shadow
|
|
100
|
+
--dry-run preview the plan, write nothing
|
|
101
|
+
--json machine-readable output, no report text
|
|
87
102
|
doctor Scan Claude Code / Codex / Kiro / pi for drift
|
|
88
103
|
--json machine-readable output, no table text
|
|
89
104
|
--probe-mcp also handshake every configured MCP server
|
|
@@ -123,6 +138,7 @@ Commands:
|
|
|
123
138
|
no overwrite): --command <cmd> [--args a,b] (stdio) or
|
|
124
139
|
--url <url> (http/sse); [--headers k=v,...]
|
|
125
140
|
[--auth oauth] [--env NAME,...] [--static-env k=v,...] [--agents id,...]
|
|
141
|
+
[--workspace] write to .agents/config.json instead of servers.yaml
|
|
126
142
|
[--enabled true|false]
|
|
127
143
|
--dry-run preview the plan, write nothing
|
|
128
144
|
--json machine-readable output, no report text
|
|
@@ -178,6 +194,9 @@ Commands:
|
|
|
178
194
|
Remove a canonical MCP server, then run the same sync as
|
|
179
195
|
mcp sync so every managed agent's Trellis-owned entry for
|
|
180
196
|
it is removed too
|
|
197
|
+
--workspace remove from .agents/config.json instead of
|
|
198
|
+
servers.yaml (a shadowed global entry takes
|
|
199
|
+
effect again, it is not deleted)
|
|
181
200
|
--dry-run preview the plan, write nothing
|
|
182
201
|
--json machine-readable output, no report text
|
|
183
202
|
skill list
|
|
@@ -186,6 +205,7 @@ Commands:
|
|
|
186
205
|
skill add <name> --from <path>
|
|
187
206
|
Import a real skill directory into canonical (refuses on
|
|
188
207
|
an existing name with different content, no overwrite)
|
|
208
|
+
--workspace copy into .agents/skills/ instead of canonical
|
|
189
209
|
--dry-run preview the plan, write nothing
|
|
190
210
|
--json machine-readable output, no report text
|
|
191
211
|
skill remove <name>
|
|
@@ -309,31 +329,15 @@ async function main(argv) {
|
|
|
309
329
|
return;
|
|
310
330
|
}
|
|
311
331
|
if (command === "onboard") {
|
|
312
|
-
const agentIndex = rest.indexOf("--agent");
|
|
313
|
-
const agent = agentIndex >= 0 ? rest[agentIndex + 1] : undefined;
|
|
314
|
-
const manageIndex = rest.indexOf("--manage");
|
|
315
|
-
const manage = manageIndex >= 0 ? rest[manageIndex + 1] : undefined;
|
|
316
|
-
const mcpModeIndex = rest.indexOf("--mcp-mode");
|
|
317
|
-
const mcpMode = mcpModeIndex >= 0 ? rest[mcpModeIndex + 1] : undefined;
|
|
318
|
-
const hubUrlIndex = rest.indexOf("--hub-url");
|
|
319
|
-
const hubUrl = hubUrlIndex >= 0 ? rest[hubUrlIndex + 1] : undefined;
|
|
320
|
-
const gatewayAgentsIndex = rest.indexOf("--gateway-agents");
|
|
321
|
-
const gatewayAgents = gatewayAgentsIndex >= 0 ? rest[gatewayAgentsIndex + 1] : undefined;
|
|
322
|
-
const memoryIndex = rest.indexOf("--memory");
|
|
323
|
-
const memory = memoryIndex >= 0 ? rest[memoryIndex + 1] : undefined;
|
|
324
|
-
const memoryMigrateIndex = rest.indexOf("--memory-migrate");
|
|
325
|
-
const memoryMigrate = memoryMigrateIndex >= 0 ? rest[memoryMigrateIndex + 1] : undefined;
|
|
326
|
-
const selectionIndex = rest.indexOf("--selection");
|
|
327
|
-
const selectionFile = selectionIndex >= 0 ? rest[selectionIndex + 1] : undefined;
|
|
328
332
|
const { exitCode } = await runOnboard({
|
|
329
|
-
agent,
|
|
330
|
-
manage,
|
|
331
|
-
mcpMode,
|
|
332
|
-
hubUrl,
|
|
333
|
-
gatewayAgents,
|
|
334
|
-
memory,
|
|
335
|
-
memoryMigrate,
|
|
336
|
-
selectionFile,
|
|
333
|
+
agent: flagValue(rest, "--agent"),
|
|
334
|
+
manage: flagValue(rest, "--manage"),
|
|
335
|
+
mcpMode: flagValue(rest, "--mcp-mode"),
|
|
336
|
+
hubUrl: flagValue(rest, "--hub-url"),
|
|
337
|
+
gatewayAgents: flagValue(rest, "--gateway-agents"),
|
|
338
|
+
memory: flagValue(rest, "--memory"),
|
|
339
|
+
memoryMigrate: flagValue(rest, "--memory-migrate"),
|
|
340
|
+
selectionFile: flagValue(rest, "--selection"),
|
|
337
341
|
dryRun: rest.includes("--dry-run"),
|
|
338
342
|
json: rest.includes("--json"),
|
|
339
343
|
});
|
|
@@ -341,16 +345,28 @@ async function main(argv) {
|
|
|
341
345
|
return;
|
|
342
346
|
}
|
|
343
347
|
if (command === "init") {
|
|
344
|
-
const { exitCode } = await runInit({ json: rest.includes("--json") });
|
|
348
|
+
const { exitCode } = await runInit({ json: rest.includes("--json"), workspace: rest.includes("--workspace") });
|
|
345
349
|
process.exitCode = exitCode;
|
|
346
350
|
return;
|
|
347
351
|
}
|
|
348
352
|
if (command === "migrate") {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
353
|
+
if (rest.includes("--workspace")) {
|
|
354
|
+
// Repeated `--choose name=agentId` flags collect into an array.
|
|
355
|
+
const choose = [];
|
|
356
|
+
for (let i = 0; i < rest.length; i++) {
|
|
357
|
+
if (rest[i] === "--choose" && rest[i + 1] !== undefined)
|
|
358
|
+
choose.push(rest[i + 1]);
|
|
359
|
+
}
|
|
360
|
+
const { exitCode } = await runMigrateWorkspace({
|
|
361
|
+
choose,
|
|
362
|
+
dedup: flagValue(rest, "--dedup"),
|
|
363
|
+
dryRun: rest.includes("--dry-run"),
|
|
364
|
+
json: rest.includes("--json"),
|
|
365
|
+
});
|
|
366
|
+
process.exitCode = exitCode;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const { exitCode } = await runMigrate({ from: flagValue(rest, "--from"), only: flagValue(rest, "--only"), dryRun: rest.includes("--dry-run"), json: rest.includes("--json") });
|
|
354
370
|
process.exitCode = exitCode;
|
|
355
371
|
return;
|
|
356
372
|
}
|
|
@@ -383,7 +399,7 @@ async function main(argv) {
|
|
|
383
399
|
process.exitCode = 1;
|
|
384
400
|
return;
|
|
385
401
|
}
|
|
386
|
-
const { exitCode } = await runSync({ target, json: rest.includes("--json"), dryRun: rest.includes("--dry-run") });
|
|
402
|
+
const { exitCode } = await runSync({ target, json: rest.includes("--json"), dryRun: rest.includes("--dry-run"), check: rest.includes("--check"), globalOnly: rest.includes("--global-only") });
|
|
387
403
|
process.exitCode = exitCode;
|
|
388
404
|
return;
|
|
389
405
|
}
|
|
@@ -443,7 +459,7 @@ async function main(argv) {
|
|
|
443
459
|
const json = mcpRest.includes("--json");
|
|
444
460
|
const dryRun = mcpRest.includes("--dry-run");
|
|
445
461
|
if (subcommand === "sync") {
|
|
446
|
-
const { exitCode } = await runMcpSync({ json, dryRun });
|
|
462
|
+
const { exitCode } = await runMcpSync({ json, dryRun, check: mcpRest.includes("--check"), globalOnly: mcpRest.includes("--global-only") });
|
|
447
463
|
process.exitCode = exitCode;
|
|
448
464
|
return;
|
|
449
465
|
}
|
|
@@ -453,7 +469,7 @@ async function main(argv) {
|
|
|
453
469
|
}
|
|
454
470
|
if (subcommand === "add") {
|
|
455
471
|
const [name] = mcpRest;
|
|
456
|
-
process.exitCode = (await runMcpAdd(name, parseMcpAddArgs(mcpRest), { json, dryRun })).exitCode;
|
|
472
|
+
process.exitCode = (await runMcpAdd(name, parseMcpAddArgs(mcpRest), { json, dryRun, workspace: mcpRest.includes("--workspace") })).exitCode;
|
|
457
473
|
return;
|
|
458
474
|
}
|
|
459
475
|
if (subcommand === "import") {
|
|
@@ -478,7 +494,7 @@ async function main(argv) {
|
|
|
478
494
|
process.exitCode = 1;
|
|
479
495
|
return;
|
|
480
496
|
}
|
|
481
|
-
process.exitCode = (await runMcpRemove(name, { json, dryRun })).exitCode;
|
|
497
|
+
process.exitCode = (await runMcpRemove(name, { json, dryRun, workspace: mcpRest.includes("--workspace") })).exitCode;
|
|
482
498
|
return;
|
|
483
499
|
}
|
|
484
500
|
if (subcommand === "renew") {
|
|
@@ -511,14 +527,13 @@ async function main(argv) {
|
|
|
511
527
|
}
|
|
512
528
|
if (subcommand === "add") {
|
|
513
529
|
const [name] = skillRest;
|
|
514
|
-
const
|
|
515
|
-
const from = fromIndex >= 0 ? skillRest[fromIndex + 1] : undefined;
|
|
530
|
+
const from = flagValue(skillRest, "--from");
|
|
516
531
|
if (!name || !from) {
|
|
517
532
|
console.error("Usage: trellis skill add <name> --from <path>");
|
|
518
533
|
process.exitCode = 1;
|
|
519
534
|
return;
|
|
520
535
|
}
|
|
521
|
-
process.exitCode = (await runSkillAdd(name, from, { json, dryRun })).exitCode;
|
|
536
|
+
process.exitCode = (await runSkillAdd(name, from, { json, dryRun, workspace: skillRest.includes("--workspace") })).exitCode;
|
|
522
537
|
return;
|
|
523
538
|
}
|
|
524
539
|
if (subcommand === "remove") {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
8
8
|
import type { AgentId, AgentSnapshot, CanonicalSource } from "../core/types.js";
|
|
9
|
+
import { type WorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
9
10
|
/**
|
|
10
11
|
* Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
|
|
11
12
|
* real `known_host_injected` field (P0 has no canonical source at all —
|
|
@@ -47,6 +48,10 @@ export interface RunDoctorOptions {
|
|
|
47
48
|
/** Defaults to the real `~`; overridable for tests only — same seam
|
|
48
49
|
* every other Trellis entry point uses. */
|
|
49
50
|
homeDir?: string;
|
|
51
|
+
/** Explicit workspace root for layer attribution — the test/sandbox
|
|
52
|
+
* seam. CLI discovery walks up from cwd via `findWorkspaceRoot`;
|
|
53
|
+
* null = no workspace layer. */
|
|
54
|
+
workspaceDir?: string | null;
|
|
50
55
|
}
|
|
51
56
|
export interface OAuthHealthEntry {
|
|
52
57
|
server: string;
|
|
@@ -61,6 +66,9 @@ export interface DoctorReport {
|
|
|
61
66
|
* server is OAuth-classified. Read-only by construction
|
|
62
67
|
* (trellis-mcp-oauth-renew design.md D4). */
|
|
63
68
|
oauth?: OAuthHealthEntry[];
|
|
69
|
+
/** Per-entry layer attribution when a `.agents/` workspace layer
|
|
70
|
+
* participated (trellis-workspace-scope task 3.5). */
|
|
71
|
+
workspace?: WorkspaceDoctorSection;
|
|
64
72
|
}
|
|
65
73
|
/** Exported so tests can assert on this resolution directly, without
|
|
66
74
|
* routing through real per-agent probes `runDoctor` also runs. */
|
|
@@ -76,7 +84,7 @@ export declare function runDoctor(opts?: RunDoctorOptions): Promise<{
|
|
|
76
84
|
* existing call site (which now passes `opts.homeDir`, itself normally
|
|
77
85
|
* `undefined`) is unaffected.
|
|
78
86
|
*/
|
|
79
|
-
export declare function collectDoctorReport(homeDir?: string, knownHostInjected?: readonly string[], probeMcp?: boolean): Promise<DoctorReport>;
|
|
87
|
+
export declare function collectDoctorReport(homeDir?: string, knownHostInjected?: readonly string[], probeMcp?: boolean, workspaceDir?: string | null): Promise<DoctorReport>;
|
|
80
88
|
/**
|
|
81
89
|
* Read-only OAuth credential health (trellis-mcp-oauth-renew design.md
|
|
82
90
|
* D4): classified from the stored token file alone — no token endpoint,
|
package/dist/commands/doctor.js
CHANGED
|
@@ -16,6 +16,8 @@ import { GATEWAY_ENTRY_NAME, RUNTIME_ENTRY_NAME, resolveMcpPlan } from "../adapt
|
|
|
16
16
|
import { ALL_AGENTS, isOAuthAuth, oauthOwner } from "../core/types.js";
|
|
17
17
|
import { classifyStoredToken, remediationFor } from "../lib/oauth/renew.js";
|
|
18
18
|
import { readToken } from "../lib/oauth/store.js";
|
|
19
|
+
import { findWorkspaceRoot } from "../lib/workspace.js";
|
|
20
|
+
import { collectWorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
19
21
|
/**
|
|
20
22
|
* Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
|
|
21
23
|
* real `known_host_injected` field (P0 has no canonical source at all —
|
|
@@ -58,7 +60,8 @@ export function resolveKnownHostInjected(opts) {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
export async function runDoctor(opts = {}) {
|
|
61
|
-
const
|
|
63
|
+
const workspaceDir = opts.workspaceDir !== undefined ? opts.workspaceDir : findWorkspaceRoot(process.cwd());
|
|
64
|
+
const report = await collectDoctorReport(opts.homeDir, resolveKnownHostInjected(opts), opts.probeMcp, workspaceDir);
|
|
62
65
|
const exitCode = report.findings.some((f) => EXIT_NONZERO_KINDS.has(f.kind)) ? 1 : 0;
|
|
63
66
|
if (opts.json) {
|
|
64
67
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -76,7 +79,7 @@ export async function runDoctor(opts = {}) {
|
|
|
76
79
|
* existing call site (which now passes `opts.homeDir`, itself normally
|
|
77
80
|
* `undefined`) is unaffected.
|
|
78
81
|
*/
|
|
79
|
-
export async function collectDoctorReport(homeDir = homedir(), knownHostInjected = DEFAULT_KNOWN_HOST_INJECTED, probeMcp = false) {
|
|
82
|
+
export async function collectDoctorReport(homeDir = homedir(), knownHostInjected = DEFAULT_KNOWN_HOST_INJECTED, probeMcp = false, workspaceDir) {
|
|
80
83
|
const probeList = [
|
|
81
84
|
{ agent: "claude-code", run: () => claudeCodeProbe.probe(homeDir, { probeMcp }) },
|
|
82
85
|
{ agent: "codex", run: () => codexProbe.probe(homeDir, { probeMcp }) },
|
|
@@ -103,15 +106,24 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
|
|
|
103
106
|
});
|
|
104
107
|
let canonical;
|
|
105
108
|
try {
|
|
106
|
-
|
|
109
|
+
// Workspace-merged when a layer is in play — doctor's attribution is
|
|
110
|
+
// the merged view, and a broken workspace config degrades doctor to
|
|
111
|
+
// the global-only view rather than failing the whole scan.
|
|
112
|
+
canonical = loadCanonicalSource({ homeDir, workspaceDir: workspaceDir ?? null });
|
|
107
113
|
}
|
|
108
114
|
catch {
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
try {
|
|
116
|
+
canonical = loadCanonicalSource(homeDir);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Doctor remains useful as a standalone cross-agent scanner when
|
|
120
|
+
// canonical has not been initialized yet.
|
|
121
|
+
}
|
|
111
122
|
}
|
|
123
|
+
const workspace = canonical?.workspace ? collectWorkspaceDoctorSection(canonical) : undefined;
|
|
112
124
|
const oauth = canonical ? collectOAuthHealth(homeDir, canonical) : [];
|
|
113
125
|
findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots), ...oauth.flatMap((entry) => (entry.status === "valid" ? [] : [oauthFinding(entry)])));
|
|
114
|
-
return { snapshots, findings, ...(oauth.length > 0 ? { oauth } : {}) };
|
|
126
|
+
return { snapshots, findings, ...(oauth.length > 0 ? { oauth } : {}), ...(workspace ? { workspace } : {}) };
|
|
115
127
|
}
|
|
116
128
|
/**
|
|
117
129
|
* Read-only OAuth credential health (trellis-mcp-oauth-renew design.md
|
|
@@ -362,4 +374,29 @@ export function printReport(report) {
|
|
|
362
374
|
console.log(`⚠️ ${finding.message}`);
|
|
363
375
|
}
|
|
364
376
|
}
|
|
377
|
+
if (report.workspace) {
|
|
378
|
+
const ws = report.workspace;
|
|
379
|
+
console.log(`\nWorkspace layer: ${ws.root} (.agents/)`);
|
|
380
|
+
for (const entry of ws.mcp) {
|
|
381
|
+
console.log(` mcp ${entry.name} — ${entry.layer}`);
|
|
382
|
+
}
|
|
383
|
+
for (const entry of ws.skills) {
|
|
384
|
+
console.log(` skill ${entry.name} — ${entry.layer}`);
|
|
385
|
+
}
|
|
386
|
+
if (ws.shadowed.length > 0) {
|
|
387
|
+
console.log(` shadowed by workspace: ${ws.shadowed.join(", ")}`);
|
|
388
|
+
}
|
|
389
|
+
if (ws.tombstoned.length > 0) {
|
|
390
|
+
console.log(` tombstoned (disabled for this project): ${ws.tombstoned.join(", ")}`);
|
|
391
|
+
}
|
|
392
|
+
for (const ref of ws.unresolvedVars) {
|
|
393
|
+
console.log(` ⚠ ${ref.server} ${ref.field}: \${${ref.name}} does not resolve under the current secrets policy`);
|
|
394
|
+
}
|
|
395
|
+
for (const hint of ws.unmigrated) {
|
|
396
|
+
console.log(` ⚠ ${hint.path} holds ${hint.entries.length} entr${hint.entries.length === 1 ? "y" : "ies"} Trellis didn't render (${hint.entries.join(", ")}) — run \`trellis migrate --workspace\``);
|
|
397
|
+
}
|
|
398
|
+
for (const warning of ws.precedenceWarnings) {
|
|
399
|
+
console.log(` ⚠ ${warning}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
365
402
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface RunInitOptions {
|
|
|
12
12
|
/** Defaults to the real `~`; overridable for tests only — same seam
|
|
13
13
|
* every other Trellis entry point uses. */
|
|
14
14
|
homeDir?: string;
|
|
15
|
+
/** `--workspace`: scaffold `.agents/` in the project instead of the
|
|
16
|
+
* global `~/.trellis/` (trellis-workspace-scope task 3.3). */
|
|
17
|
+
workspace?: boolean;
|
|
18
|
+
/** Workspace scaffold target — the test/sandbox seam. CLI discovery is
|
|
19
|
+
* simply `process.cwd()`. */
|
|
20
|
+
workspaceDir?: string;
|
|
15
21
|
}
|
|
16
22
|
export interface InitFileResult {
|
|
17
23
|
path: string;
|
package/dist/commands/init.js
CHANGED
|
@@ -18,6 +18,7 @@ import * as piProbe from "../probes/pi.js";
|
|
|
18
18
|
import * as kimiCodeProbe from "../probes/kimi-code.js";
|
|
19
19
|
import * as zcodeProbe from "../probes/zcode.js";
|
|
20
20
|
import { ALL_AGENTS } from "../core/types.js";
|
|
21
|
+
import { scaffoldWorkspace } from "../lib/workspaceConfig.js";
|
|
21
22
|
/** Exported so `migrate` can byte-compare canonical `agents.md` against
|
|
22
23
|
* this exact string (design.md D3, trellis-cli-migrate) rather than
|
|
23
24
|
* retyping an equivalent one that could silently drift from this. */
|
|
@@ -148,6 +149,29 @@ export async function collectInitReport(homeDir = homedir(), options = {}) {
|
|
|
148
149
|
return { files, agents };
|
|
149
150
|
}
|
|
150
151
|
export async function runInit(opts = {}) {
|
|
152
|
+
if (opts.workspace) {
|
|
153
|
+
const dir = opts.workspaceDir ?? process.cwd();
|
|
154
|
+
const files = scaffoldWorkspace(dir);
|
|
155
|
+
if (opts.json) {
|
|
156
|
+
console.log(JSON.stringify({ workspace: dir, files }, null, 2));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const created = files.filter((f) => f.action === "create");
|
|
160
|
+
if (created.length === 0) {
|
|
161
|
+
console.log(`${dir}/.agents already fully initialized — nothing to create.`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.log(`Created ${created.length} file(s) under ${dir}/.agents:`);
|
|
165
|
+
for (const f of created)
|
|
166
|
+
console.log(` - ${f.path}`);
|
|
167
|
+
}
|
|
168
|
+
for (const f of files.filter((f) => f.action === "already-present")) {
|
|
169
|
+
console.log(` - ${f.path} (already present, left untouched)`);
|
|
170
|
+
}
|
|
171
|
+
console.log("\nNext: commit .agents/ to share it, then `trellis sync` renders per-agent project files.");
|
|
172
|
+
}
|
|
173
|
+
return { exitCode: 0 };
|
|
174
|
+
}
|
|
151
175
|
const homeDir = opts.homeDir ?? homedir();
|
|
152
176
|
const report = await collectInitReport(homeDir);
|
|
153
177
|
if (opts.json) {
|