agent-trellis 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -17
- package/dist/adapters/claude-code.d.ts +5 -3
- package/dist/adapters/claude-code.js +27 -14
- package/dist/adapters/codex.d.ts +8 -4
- package/dist/adapters/codex.js +47 -16
- package/dist/adapters/jsonMcp.d.ts +16 -5
- package/dist/adapters/jsonMcp.js +38 -29
- package/dist/adapters/kiro.d.ts +5 -3
- package/dist/adapters/kiro.js +29 -16
- package/dist/adapters/mcpPlan.d.ts +11 -6
- package/dist/adapters/mcpPlan.js +40 -7
- package/dist/adapters/pi.d.ts +2 -1
- package/dist/adapters/pi.js +4 -4
- package/dist/adapters/symlinkPlan.d.ts +7 -3
- package/dist/adapters/symlinkPlan.js +42 -16
- package/dist/cli.js +161 -18
- package/dist/commands/init.js +11 -0
- package/dist/commands/mcp.d.ts +114 -7
- package/dist/commands/mcp.js +258 -17
- package/dist/commands/memory.d.ts +39 -0
- package/dist/commands/memory.js +78 -0
- package/dist/commands/migrate.d.ts +30 -4
- package/dist/commands/migrate.js +83 -16
- package/dist/commands/onboard.d.ts +52 -7
- package/dist/commands/onboard.js +318 -35
- package/dist/commands/rollback.d.ts +44 -0
- package/dist/commands/rollback.js +201 -0
- package/dist/commands/secretsAudit.d.ts +7 -0
- package/dist/commands/secretsAudit.js +14 -7
- package/dist/commands/skill.d.ts +51 -0
- package/dist/commands/skill.js +104 -0
- package/dist/commands/sync.d.ts +13 -0
- package/dist/commands/sync.js +31 -5
- package/dist/core/adapter.d.ts +28 -11
- package/dist/core/adapter.js +2 -2
- package/dist/core/canonical.d.ts +26 -1
- package/dist/core/canonical.js +103 -3
- package/dist/core/types.d.ts +29 -1
- package/dist/core/types.js +11 -2
- package/dist/lib/backup.d.ts +56 -0
- package/dist/lib/backup.js +98 -0
- package/dist/lib/deepEqual.d.ts +8 -0
- package/dist/lib/deepEqual.js +26 -0
- package/dist/lib/dirEquals.d.ts +9 -0
- package/dist/lib/dirEquals.js +15 -1
- package/dist/lib/installAgent.d.ts +26 -0
- package/dist/lib/installAgent.js +46 -0
- package/dist/lib/mcpMigrateRead.d.ts +69 -0
- package/dist/lib/mcpMigrateRead.js +188 -0
- package/dist/lib/mcpOwnership.d.ts +25 -0
- package/dist/lib/mcpOwnership.js +50 -0
- package/dist/lib/memoryGraph.d.ts +60 -0
- package/dist/lib/memoryGraph.js +101 -0
- package/dist/lib/realHomeSnapshot.d.ts +26 -0
- package/dist/lib/realHomeSnapshot.js +77 -0
- package/dist/lib/terminalPicker.d.ts +45 -0
- package/dist/lib/terminalPicker.js +193 -0
- package/dist/lib/tomlSection.d.ts +20 -6
- package/dist/lib/tomlSection.js +78 -12
- package/dist/pi-bridge/bundle.js +100 -51
- package/dist/pi-bridge/index.js +14 -2
- package/dist/probes/codex.js +10 -2
- package/docs/architecture.md +7 -4
- package/docs/getting-started.md +267 -33
- package/docs/roadmap.md +444 -0
- package/package.json +1 -1
- package/schema/servers.example.yaml +39 -2
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
* each adapter turns this into plan items using its own read/write
|
|
7
7
|
* mechanism (JSON merge vs. TOML section splice).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* skill's symlink, a plain key has no ownership marker, so
|
|
11
|
-
* canonical anymore" can't be distinguished from "the user
|
|
12
|
-
* this directly
|
|
9
|
+
* Removal is NOT decided here (design.md D7, trellis-mcp-sync-p2): unlike
|
|
10
|
+
* a skill's symlink, a plain key has no ownership marker of its own, so
|
|
11
|
+
* "not in canonical anymore" can't be distinguished from "the user
|
|
12
|
+
* configured this directly" from this function's storage-agnostic view
|
|
13
|
+
* alone. Each format-specific caller (`jsonMcp.ts`'s `planJsonMcp`,
|
|
14
|
+
* `codex.ts`'s own `planMcp`) computes removal candidates itself, against
|
|
15
|
+
* `src/lib/mcpOwnership.ts`'s ledger and that format's own current
|
|
16
|
+
* on-disk content (trellis-mcp-lifecycle-parity) — this function only
|
|
17
|
+
* ever returns create/repair + refuse.
|
|
13
18
|
*/
|
|
14
|
-
import type { AgentId, McpConfig, McpServerDef } from "../core/types.js";
|
|
19
|
+
import type { AgentId, McpConfig, McpServerDef, SecretsPolicy } from "../core/types.js";
|
|
15
20
|
export declare const HUB_ENTRY_NAME = "trellis-hub";
|
|
16
21
|
export interface DesiredMcpEntry {
|
|
17
22
|
name: string;
|
|
@@ -25,4 +30,4 @@ export interface McpPlanResult {
|
|
|
25
30
|
desired: DesiredMcpEntry[];
|
|
26
31
|
conflicts: McpConflict[];
|
|
27
32
|
}
|
|
28
|
-
export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig): McpPlanResult;
|
|
33
|
+
export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig, managedAgents: readonly AgentId[], policy: SecretsPolicy): McpPlanResult;
|
package/dist/adapters/mcpPlan.js
CHANGED
|
@@ -6,13 +6,19 @@
|
|
|
6
6
|
* each adapter turns this into plan items using its own read/write
|
|
7
7
|
* mechanism (JSON merge vs. TOML section splice).
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* skill's symlink, a plain key has no ownership marker, so
|
|
11
|
-
* canonical anymore" can't be distinguished from "the user
|
|
12
|
-
* this directly
|
|
9
|
+
* Removal is NOT decided here (design.md D7, trellis-mcp-sync-p2): unlike
|
|
10
|
+
* a skill's symlink, a plain key has no ownership marker of its own, so
|
|
11
|
+
* "not in canonical anymore" can't be distinguished from "the user
|
|
12
|
+
* configured this directly" from this function's storage-agnostic view
|
|
13
|
+
* alone. Each format-specific caller (`jsonMcp.ts`'s `planJsonMcp`,
|
|
14
|
+
* `codex.ts`'s own `planMcp`) computes removal candidates itself, against
|
|
15
|
+
* `src/lib/mcpOwnership.ts`'s ledger and that format's own current
|
|
16
|
+
* on-disk content (trellis-mcp-lifecycle-parity) — this function only
|
|
17
|
+
* ever returns create/repair + refuse.
|
|
13
18
|
*/
|
|
14
19
|
import { isInScope } from "../core/adapter.js";
|
|
15
20
|
import { codexBearerTokenEnvVar } from "../lib/tomlSection.js";
|
|
21
|
+
import { resolveSecretEnv } from "../lib/secretEnv.js";
|
|
16
22
|
export const HUB_ENTRY_NAME = "trellis-hub";
|
|
17
23
|
const CODEX_STDIO_URL_CRASH_NOTE = ' On Codex specifically, this crashes the entire process at startup ("url is not supported for stdio"), not just this one server — see docs/research.md.';
|
|
18
24
|
function collisionMessage(name, agentId) {
|
|
@@ -30,7 +36,7 @@ const DANGEROUS_LITERAL_PATTERNS = [
|
|
|
30
36
|
{ label: "mcp-router token (mcpr_)", pattern: /mcpr_/ },
|
|
31
37
|
];
|
|
32
38
|
function findLiteralSecret(def) {
|
|
33
|
-
const candidates = [def.command, def.url, ...(def.args ?? []), ...Object.values(def.headers ?? {})].filter((v) => typeof v === "string");
|
|
39
|
+
const candidates = [def.command, def.url, ...(def.args ?? []), ...Object.values(def.headers ?? {}), ...Object.values(def.staticEnv ?? {})].filter((v) => typeof v === "string");
|
|
34
40
|
for (const candidate of candidates) {
|
|
35
41
|
for (const { label, pattern } of DANGEROUS_LITERAL_PATTERNS) {
|
|
36
42
|
if (pattern.test(candidate)) {
|
|
@@ -40,7 +46,22 @@ function findLiteralSecret(def) {
|
|
|
40
46
|
}
|
|
41
47
|
return undefined;
|
|
42
48
|
}
|
|
43
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Name-only `env` entries that don't resolve to anything would otherwise
|
|
51
|
+
* be written and silently break the agent's connection to that server —
|
|
52
|
+
* found via real-machine dogfooding (trellis-mcp-static-env-and-disabled-servers
|
|
53
|
+
* proposal.md "Why"). Uses the identical `resolveSecretEnv` `secrets
|
|
54
|
+
* audit`/the pi bridge already call, so this and a later `secrets audit`
|
|
55
|
+
* run can never disagree about what resolves.
|
|
56
|
+
*/
|
|
57
|
+
function findUnresolvedEnvName(def, policy) {
|
|
58
|
+
const names = def.env ?? [];
|
|
59
|
+
if (names.length === 0)
|
|
60
|
+
return undefined;
|
|
61
|
+
const resolved = resolveSecretEnv(names, policy);
|
|
62
|
+
return names.find((name) => !resolved[name]);
|
|
63
|
+
}
|
|
64
|
+
export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
44
65
|
if (mcp.hub) {
|
|
45
66
|
if (mcp.knownHostInjected.includes(HUB_ENTRY_NAME)) {
|
|
46
67
|
return { desired: [], conflicts: [{ name: HUB_ENTRY_NAME, message: collisionMessage(HUB_ENTRY_NAME, agentId) }] };
|
|
@@ -50,7 +71,10 @@ export function resolveMcpPlan(agentId, mcp) {
|
|
|
50
71
|
const desired = [];
|
|
51
72
|
const conflicts = [];
|
|
52
73
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
53
|
-
if (
|
|
74
|
+
if (def.enabled === false) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!isInScope(agentId, def.agents, managedAgents)) {
|
|
54
78
|
continue;
|
|
55
79
|
}
|
|
56
80
|
if (mcp.knownHostInjected.includes(name)) {
|
|
@@ -77,6 +101,15 @@ export function resolveMcpPlan(agentId, mcp) {
|
|
|
77
101
|
});
|
|
78
102
|
continue;
|
|
79
103
|
}
|
|
104
|
+
const unresolvedName = findUnresolvedEnvName(def, policy);
|
|
105
|
+
if (unresolvedName) {
|
|
106
|
+
const source = policy.envFile ?? "process environment";
|
|
107
|
+
conflicts.push({
|
|
108
|
+
name,
|
|
109
|
+
message: `refusing to write MCP server "${name}": its declared env var "${unresolvedName}" has no resolvable value in ${source} — writing it now would silently break this server's connection once the agent starts it (trellis-mcp-static-env-and-disabled-servers)`,
|
|
110
|
+
});
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
80
113
|
desired.push({ name, def });
|
|
81
114
|
}
|
|
82
115
|
return { desired, conflicts };
|
package/dist/adapters/pi.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
|
|
13
13
|
import type { CanonicalSource } from "../core/types.js";
|
|
14
|
+
import type { BackupSession } from "../lib/backup.js";
|
|
14
15
|
export declare class PiAdapter implements TrellisAdapter {
|
|
15
16
|
private readonly homeDir;
|
|
16
17
|
readonly name = "pi";
|
|
@@ -18,6 +19,6 @@ export declare class PiAdapter implements TrellisAdapter {
|
|
|
18
19
|
constructor(homeDir?: string);
|
|
19
20
|
probe(): Promise<AdapterProbeResult>;
|
|
20
21
|
plan(canonical: CanonicalSource): Promise<AdapterPlanItem[]>;
|
|
21
|
-
apply(plan: AdapterPlanItem[]): Promise<void>;
|
|
22
|
+
apply(plan: AdapterPlanItem[], backup: BackupSession): Promise<void>;
|
|
22
23
|
verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
|
|
23
24
|
}
|
package/dist/adapters/pi.js
CHANGED
|
@@ -56,7 +56,7 @@ export class PiAdapter {
|
|
|
56
56
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
57
57
|
const agentDir = join(this.homeDir, ".pi", "agent");
|
|
58
58
|
const desiredSkills = canonical.skills
|
|
59
|
-
.filter((skill) => isInScope(this.id, skill.scope))
|
|
59
|
+
.filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
60
60
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
61
61
|
const skillItems = planSymlinks({
|
|
62
62
|
rootDir: join(agentDir, "skills"),
|
|
@@ -82,8 +82,8 @@ export class PiAdapter {
|
|
|
82
82
|
});
|
|
83
83
|
return [...skillItems, ...instructionsItems, ...extensionItems];
|
|
84
84
|
}
|
|
85
|
-
async apply(plan) {
|
|
86
|
-
await applySymlinkPlan(plan);
|
|
85
|
+
async apply(plan, backup) {
|
|
86
|
+
await applySymlinkPlan(plan, backup);
|
|
87
87
|
}
|
|
88
88
|
async verify(canonical) {
|
|
89
89
|
const snapshot = await piProbe.probe(this.homeDir);
|
|
@@ -91,7 +91,7 @@ export class PiAdapter {
|
|
|
91
91
|
return { ok: false, mismatches: ["pi is not present on this machine"] };
|
|
92
92
|
}
|
|
93
93
|
const mismatches = [];
|
|
94
|
-
const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope)).map((s) => s.name));
|
|
94
|
+
const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
95
95
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
96
96
|
for (const name of desiredNames) {
|
|
97
97
|
if (!actualSkills.has(name)) {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* trellis-sync-p1/specs/skill-instructions-sync/spec.md) — built once,
|
|
5
5
|
* reused by every adapter rather than reimplemented per agent.
|
|
6
6
|
*/
|
|
7
|
+
import { type BackupSession } from "../lib/backup.js";
|
|
7
8
|
import type { AdapterPlanItem } from "../core/adapter.js";
|
|
8
9
|
export interface DesiredSymlink {
|
|
9
10
|
/** Basename this entry should have under `rootDir`. */
|
|
@@ -28,6 +29,9 @@ export declare function planSymlinks(opts: {
|
|
|
28
29
|
canonicalRoot: string;
|
|
29
30
|
kind: "skill" | "instructions" | "extension";
|
|
30
31
|
}): AdapterPlanItem[];
|
|
31
|
-
/** Executes a `planSymlinks` result
|
|
32
|
-
*
|
|
33
|
-
|
|
32
|
+
/** Executes a `planSymlinks` result through the run's backup session
|
|
33
|
+
* (trellis-backup-rollback) — every create/repair/remove is recorded
|
|
34
|
+
* before it happens; `backup` performs the actual filesystem write, this
|
|
35
|
+
* function never calls `fs/promises` itself. "conflict" is report-only —
|
|
36
|
+
* see src/core/adapter.ts's `apply()` doc for why this never throws. */
|
|
37
|
+
export declare function applySymlinkPlan(plan: AdapterPlanItem[], backup: BackupSession): Promise<void>;
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* reused by every adapter rather than reimplemented per agent.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, lstatSync, readdirSync, readlinkSync } from "node:fs";
|
|
8
|
-
import { mkdir
|
|
8
|
+
import { mkdir } from "node:fs/promises";
|
|
9
9
|
import { join, resolve, sep } from "node:path";
|
|
10
10
|
import { isSymlinkTo } from "../lib/fsIdentity.js";
|
|
11
|
+
import { currentLinkTarget } from "../lib/backup.js";
|
|
11
12
|
function isUnderRoot(path, root) {
|
|
12
13
|
const normalizedRoot = resolve(root);
|
|
13
14
|
const normalizedPath = resolve(path);
|
|
@@ -37,14 +38,27 @@ export function planSymlinks(opts) {
|
|
|
37
38
|
if (isSymlinkTo(path, target)) {
|
|
38
39
|
continue; // already correct — no-op
|
|
39
40
|
}
|
|
40
|
-
if (existsSync(path)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
if (existsSync(path)) {
|
|
42
|
+
const isSymlink = lstatSync(path).isSymbolicLink();
|
|
43
|
+
// A symlink whose stored target resolves outside canonicalRoot is
|
|
44
|
+
// owned by something else (e.g. a user's own dotfile-management
|
|
45
|
+
// setup) — repairing it as if it were a stale Trellis entry would
|
|
46
|
+
// silently steal that ownership. Raw readlink, not realpath: a
|
|
47
|
+
// symlink Trellis itself left pointing at a since-removed canonical
|
|
48
|
+
// entry is broken by construction and must still be treated as
|
|
49
|
+
// ours to repair, not thrown out to a conflict by a realpath error.
|
|
50
|
+
const isForeign = isSymlink && !isUnderRoot(readlinkSync(path), canonicalRootResolved);
|
|
51
|
+
if (!isSymlink || isForeign) {
|
|
52
|
+
items.push({
|
|
53
|
+
action: "conflict",
|
|
54
|
+
kind,
|
|
55
|
+
target: path,
|
|
56
|
+
description: isForeign
|
|
57
|
+
? `${path} exists as a symlink to ${readlinkSync(path)}, not owned by Trellis — left untouched`
|
|
58
|
+
: `${path} exists and is not a Trellis-managed symlink — left untouched`,
|
|
59
|
+
});
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
48
62
|
}
|
|
49
63
|
items.push({
|
|
50
64
|
action: "create",
|
|
@@ -100,21 +114,33 @@ export function planSymlinks(opts) {
|
|
|
100
114
|
}
|
|
101
115
|
return items;
|
|
102
116
|
}
|
|
103
|
-
/** Executes a `planSymlinks` result
|
|
104
|
-
*
|
|
105
|
-
|
|
117
|
+
/** Executes a `planSymlinks` result through the run's backup session
|
|
118
|
+
* (trellis-backup-rollback) — every create/repair/remove is recorded
|
|
119
|
+
* before it happens; `backup` performs the actual filesystem write, this
|
|
120
|
+
* function never calls `fs/promises` itself. "conflict" is report-only —
|
|
121
|
+
* see src/core/adapter.ts's `apply()` doc for why this never throws. */
|
|
122
|
+
export async function applySymlinkPlan(plan, backup) {
|
|
106
123
|
for (const item of plan) {
|
|
107
124
|
if (item.action === "conflict")
|
|
108
125
|
continue;
|
|
109
126
|
if (item.action === "remove") {
|
|
110
|
-
await
|
|
127
|
+
const oldTarget = await currentLinkTarget(item.target);
|
|
128
|
+
if (oldTarget === undefined)
|
|
129
|
+
continue; // already gone — no-op, nothing to record
|
|
130
|
+
await backup.removeSymlink(item.target, oldTarget);
|
|
111
131
|
continue;
|
|
112
132
|
}
|
|
113
|
-
// "create"
|
|
133
|
+
// "create" also covers repair — rootDir may not exist yet (first
|
|
134
|
+
// sync ever for this agent).
|
|
114
135
|
if (!item.linkTarget)
|
|
115
136
|
continue;
|
|
116
137
|
await mkdir(resolve(item.target, ".."), { recursive: true });
|
|
117
|
-
await
|
|
118
|
-
|
|
138
|
+
const oldTarget = await currentLinkTarget(item.target);
|
|
139
|
+
if (oldTarget === undefined) {
|
|
140
|
+
await backup.createSymlink(item.target, item.linkTarget);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
await backup.repairSymlink(item.target, oldTarget, item.linkTarget);
|
|
144
|
+
}
|
|
119
145
|
}
|
|
120
146
|
}
|
package/dist/cli.js
CHANGED
|
@@ -9,10 +9,13 @@ import { runInit } from "./commands/init.js";
|
|
|
9
9
|
import { runMigrate } from "./commands/migrate.js";
|
|
10
10
|
import { runOnboard } from "./commands/onboard.js";
|
|
11
11
|
import { runSync } from "./commands/sync.js";
|
|
12
|
-
import { runMcpSync } from "./commands/mcp.js";
|
|
12
|
+
import { runMcpSync, runMcpList, runMcpAdd, runMcpRemove, parseMcpAddArgs } from "./commands/mcp.js";
|
|
13
13
|
import { runSecretsAudit } from "./commands/secretsAudit.js";
|
|
14
|
+
import { runRollback } from "./commands/rollback.js";
|
|
15
|
+
import { runSkillList, runSkillAdd, runSkillRemove } from "./commands/skill.js";
|
|
16
|
+
import { runMemorySync } from "./commands/memory.js";
|
|
14
17
|
import { parseSyncArgs } from "./lib/syncArgs.js";
|
|
15
|
-
const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "secrets"];
|
|
18
|
+
const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "skill", "memory", "secrets", "rollback"];
|
|
16
19
|
function printUsage() {
|
|
17
20
|
console.log(`trellis - a single source of capability for every coding agent
|
|
18
21
|
|
|
@@ -20,21 +23,32 @@ Usage:
|
|
|
20
23
|
trellis <command>
|
|
21
24
|
|
|
22
25
|
Commands:
|
|
23
|
-
onboard Guided flow: init -> detect agents -> pick a
|
|
24
|
-
migrate -> sync
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
--
|
|
29
|
-
|
|
26
|
+
onboard Guided flow: init -> detect agents -> pick a migration source ->
|
|
27
|
+
pick which agents to manage -> migrate -> sync -> mcp sync ->
|
|
28
|
+
secrets audit, in one command. Source (read from) and managed
|
|
29
|
+
set (written to) are independent; the source is not managed
|
|
30
|
+
by default.
|
|
31
|
+
--agent <agent> non-interactive migration-source
|
|
32
|
+
choice (required with 2+ candidates
|
|
33
|
+
and no terminal to prompt in)
|
|
34
|
+
--manage <ids|none> non-interactive managed-set choice,
|
|
35
|
+
e.g. --manage pi,codex ; --manage none
|
|
36
|
+
means "add nothing new this run"
|
|
37
|
+
(required with no terminal to prompt
|
|
38
|
+
in, e.g. --json)
|
|
39
|
+
--dry-run preview the whole flow, write nothing
|
|
40
|
+
--json machine-readable output, no report text
|
|
30
41
|
init Create ~/.trellis/ with a minimal valid skeleton if missing
|
|
31
42
|
(never overwrites an existing file — fills in only what's
|
|
32
43
|
missing) and prints which agents are present
|
|
33
44
|
--json machine-readable output, no report text
|
|
34
45
|
migrate --from <agent>
|
|
35
|
-
Import an existing agent's real skills/instructions
|
|
36
|
-
canonical source (claude-code | codex | kiro |
|
|
37
|
-
|
|
46
|
+
Import an existing agent's real skills/instructions/MCP
|
|
47
|
+
servers into canonical source (claude-code | codex | kiro |
|
|
48
|
+
pi — pi has no static MCP config, mcp migrate-in is a no-op
|
|
49
|
+
for it). Never overwrites differing content — reports a
|
|
50
|
+
conflict instead.
|
|
51
|
+
--only skills|instructions|mcp restrict to one category
|
|
38
52
|
--dry-run preview the plan, write nothing
|
|
39
53
|
--json machine-readable output, no report text
|
|
40
54
|
doctor Scan Claude Code / Codex / Kiro / pi for drift
|
|
@@ -47,12 +61,62 @@ Commands:
|
|
|
47
61
|
--dry-run preview the plan, write nothing
|
|
48
62
|
--json machine-readable output, no report text
|
|
49
63
|
mcp sync Distribute MCP servers to each agent's native config
|
|
50
|
-
(create/repair
|
|
64
|
+
(create/repair, plus ownership-ledger-gated removal — an
|
|
65
|
+
entry is only ever removed when it's still exactly what
|
|
66
|
+
Trellis itself last wrote there; see docs/roadmap.md)
|
|
67
|
+
--dry-run preview the plan, write nothing
|
|
68
|
+
--json machine-readable output, no report text
|
|
69
|
+
mcp list List canonical MCP servers (transport, scope, enabled;
|
|
70
|
+
never prints resolved secret values)
|
|
71
|
+
--json machine-readable output, no report text
|
|
72
|
+
mcp add <name> --transport stdio|http|sse ...
|
|
73
|
+
Add a canonical MCP server (refuses on an existing name,
|
|
74
|
+
no overwrite): --command <cmd> [--args a,b] (stdio) or
|
|
75
|
+
--url <url> (http/sse); [--headers k=v,...]
|
|
76
|
+
[--env NAME,...] [--static-env k=v,...] [--agents id,...]
|
|
77
|
+
[--enabled true|false]
|
|
78
|
+
--dry-run preview the plan, write nothing
|
|
79
|
+
--json machine-readable output, no report text
|
|
80
|
+
mcp remove <name>
|
|
81
|
+
Remove a canonical MCP server (canonical-side only — does
|
|
82
|
+
not touch any agent's already-synced native config)
|
|
83
|
+
--dry-run preview the plan, write nothing
|
|
84
|
+
--json machine-readable output, no report text
|
|
85
|
+
skill list
|
|
86
|
+
List canonical skills with resolved scope
|
|
51
87
|
--json machine-readable output, no report text
|
|
88
|
+
skill add <name> --from <path>
|
|
89
|
+
Import a real skill directory into canonical (refuses on
|
|
90
|
+
an existing name with different content, no overwrite)
|
|
91
|
+
--dry-run preview the plan, write nothing
|
|
92
|
+
--json machine-readable output, no report text
|
|
93
|
+
skill remove <name>
|
|
94
|
+
Remove a canonical skill (the next sync auto-removes the
|
|
95
|
+
now-stale symlink on every managed agent)
|
|
96
|
+
--dry-run preview the plan, write nothing
|
|
97
|
+
--json machine-readable output, no report text
|
|
98
|
+
memory sync
|
|
99
|
+
Ingest canonical memories/*.md into the shared-memory MCP
|
|
100
|
+
server's own on-disk knowledge-graph file (requires a
|
|
101
|
+
"memory" server with static_env.MEMORY_FILE_PATH set in
|
|
102
|
+
servers.yaml — see schema/servers.example.yaml; a no-op,
|
|
103
|
+
not an error, if unconfigured). Never touches an entity
|
|
104
|
+
or relation this didn't create.
|
|
105
|
+
--dry-run preview the plan, write nothing
|
|
106
|
+
--json machine-readable output, no report text
|
|
52
107
|
secrets audit
|
|
53
108
|
Scan each present agent's real MCP config for leaked
|
|
54
109
|
credentials and unexpected env var names
|
|
55
110
|
--json machine-readable output, no report text
|
|
111
|
+
rollback [<run-id>]
|
|
112
|
+
Undo one recorded sync/mcp-sync/onboard run (every real write
|
|
113
|
+
it performed is backed up first, under
|
|
114
|
+
~/.trellis/backups/) — omit <run-id> for the most recent
|
|
115
|
+
run. Refuses per-path (a conflict, not overwritten) if the
|
|
116
|
+
path changed since that run.
|
|
117
|
+
--list show available backup runs, don't restore
|
|
118
|
+
--dry-run preview what would be restored, write nothing
|
|
119
|
+
--json machine-readable output, no report text
|
|
56
120
|
|
|
57
121
|
See docs/roadmap.md for what's built vs. planned.`);
|
|
58
122
|
}
|
|
@@ -71,7 +135,9 @@ async function main(argv) {
|
|
|
71
135
|
if (command === "onboard") {
|
|
72
136
|
const agentIndex = rest.indexOf("--agent");
|
|
73
137
|
const agent = agentIndex >= 0 ? rest[agentIndex + 1] : undefined;
|
|
74
|
-
const
|
|
138
|
+
const manageIndex = rest.indexOf("--manage");
|
|
139
|
+
const manage = manageIndex >= 0 ? rest[manageIndex + 1] : undefined;
|
|
140
|
+
const { exitCode } = await runOnboard({ agent, manage, dryRun: rest.includes("--dry-run"), json: rest.includes("--json") });
|
|
75
141
|
process.exitCode = exitCode;
|
|
76
142
|
return;
|
|
77
143
|
}
|
|
@@ -83,7 +149,9 @@ async function main(argv) {
|
|
|
83
149
|
if (command === "migrate") {
|
|
84
150
|
const fromIndex = rest.indexOf("--from");
|
|
85
151
|
const from = fromIndex >= 0 ? rest[fromIndex + 1] : undefined;
|
|
86
|
-
const
|
|
152
|
+
const onlyIndex = rest.indexOf("--only");
|
|
153
|
+
const only = onlyIndex >= 0 ? rest[onlyIndex + 1] : undefined;
|
|
154
|
+
const { exitCode } = await runMigrate({ from, only, dryRun: rest.includes("--dry-run"), json: rest.includes("--json") });
|
|
87
155
|
process.exitCode = exitCode;
|
|
88
156
|
return;
|
|
89
157
|
}
|
|
@@ -105,13 +173,79 @@ async function main(argv) {
|
|
|
105
173
|
return;
|
|
106
174
|
}
|
|
107
175
|
if (command === "mcp") {
|
|
108
|
-
const [subcommand] = rest;
|
|
176
|
+
const [subcommand, ...mcpRest] = rest;
|
|
177
|
+
const json = mcpRest.includes("--json");
|
|
178
|
+
const dryRun = mcpRest.includes("--dry-run");
|
|
179
|
+
if (subcommand === "sync") {
|
|
180
|
+
const { exitCode } = await runMcpSync({ json, dryRun });
|
|
181
|
+
process.exitCode = exitCode;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (subcommand === "list") {
|
|
185
|
+
process.exitCode = runMcpList({ json }).exitCode;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (subcommand === "add") {
|
|
189
|
+
const [name] = mcpRest;
|
|
190
|
+
process.exitCode = runMcpAdd(name, parseMcpAddArgs(mcpRest), { json, dryRun }).exitCode;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (subcommand === "remove") {
|
|
194
|
+
const [name] = mcpRest;
|
|
195
|
+
if (!name) {
|
|
196
|
+
console.error("Usage: trellis mcp remove <name>");
|
|
197
|
+
process.exitCode = 1;
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
process.exitCode = runMcpRemove(name, { json, dryRun }).exitCode;
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
console.error(`Unknown mcp subcommand: ${subcommand ?? "(none)"}\nUsage: trellis mcp sync|list|add <name>|remove <name>\n`);
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (command === "skill") {
|
|
208
|
+
const [subcommand, ...skillRest] = rest;
|
|
209
|
+
const json = skillRest.includes("--json");
|
|
210
|
+
const dryRun = skillRest.includes("--dry-run");
|
|
211
|
+
if (subcommand === "list") {
|
|
212
|
+
process.exitCode = runSkillList({ json }).exitCode;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (subcommand === "add") {
|
|
216
|
+
const [name] = skillRest;
|
|
217
|
+
const fromIndex = skillRest.indexOf("--from");
|
|
218
|
+
const from = fromIndex >= 0 ? skillRest[fromIndex + 1] : undefined;
|
|
219
|
+
if (!name || !from) {
|
|
220
|
+
console.error("Usage: trellis skill add <name> --from <path>");
|
|
221
|
+
process.exitCode = 1;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
process.exitCode = runSkillAdd(name, from, { json, dryRun }).exitCode;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (subcommand === "remove") {
|
|
228
|
+
const [name] = skillRest;
|
|
229
|
+
if (!name) {
|
|
230
|
+
console.error("Usage: trellis skill remove <name>");
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
process.exitCode = runSkillRemove(name, { json, dryRun }).exitCode;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.error(`Unknown skill subcommand: ${subcommand ?? "(none)"}\nUsage: trellis skill list|add <name> --from <path>|remove <name>\n`);
|
|
238
|
+
process.exitCode = 1;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (command === "memory") {
|
|
242
|
+
const [subcommand, ...memoryRest] = rest;
|
|
109
243
|
if (subcommand !== "sync") {
|
|
110
|
-
console.error(`Unknown
|
|
244
|
+
console.error(`Unknown memory subcommand: ${subcommand ?? "(none)"}\nUsage: trellis memory sync\n`);
|
|
111
245
|
process.exitCode = 1;
|
|
112
246
|
return;
|
|
113
247
|
}
|
|
114
|
-
const { exitCode } =
|
|
248
|
+
const { exitCode } = runMemorySync({ json: memoryRest.includes("--json"), dryRun: memoryRest.includes("--dry-run") });
|
|
115
249
|
process.exitCode = exitCode;
|
|
116
250
|
return;
|
|
117
251
|
}
|
|
@@ -126,6 +260,15 @@ async function main(argv) {
|
|
|
126
260
|
process.exitCode = exitCode;
|
|
127
261
|
return;
|
|
128
262
|
}
|
|
263
|
+
if (command === "rollback") {
|
|
264
|
+
const list = rest.includes("--list");
|
|
265
|
+
const dryRun = rest.includes("--dry-run");
|
|
266
|
+
const json = rest.includes("--json");
|
|
267
|
+
const runId = rest.find((arg) => !arg.startsWith("--"));
|
|
268
|
+
const { exitCode } = await runRollback({ runId, list, dryRun, json });
|
|
269
|
+
process.exitCode = exitCode;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
129
272
|
console.error(`\`trellis ${command}\` is not implemented yet — this is a pre-alpha scaffold.\nSee docs/roadmap.md for status.`);
|
|
130
273
|
process.exitCode = 1;
|
|
131
274
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -71,6 +71,16 @@ reject_patterns:
|
|
|
71
71
|
${patternLines}
|
|
72
72
|
`;
|
|
73
73
|
}
|
|
74
|
+
/** Zero managed agents is the correct starting point (trellis-managed-agents
|
|
75
|
+
* design.md D1) — `trellis onboard`'s managed-set selection is what
|
|
76
|
+
* populates this, never `init` guessing on its behalf. */
|
|
77
|
+
function managedYamlTemplate() {
|
|
78
|
+
return `# Agents Trellis is authorized to write to. Empty means none yet —
|
|
79
|
+
# run \`trellis onboard\` or list agent ids here yourself, e.g.:
|
|
80
|
+
# agents: [pi, codex]
|
|
81
|
+
agents: []
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
74
84
|
/**
|
|
75
85
|
* Trellis never spawns an installer itself (global package installs are
|
|
76
86
|
* exactly the kind of irreversible, system-wide action that needs the
|
|
@@ -102,6 +112,7 @@ export async function collectInitReport(homeDir = homedir()) {
|
|
|
102
112
|
ensureFile(join(root, "agents.md"), AGENTS_MD_TEMPLATE),
|
|
103
113
|
ensureFile(join(root, "mcp", "servers.yaml"), serversYamlTemplate()),
|
|
104
114
|
ensureFile(join(root, "secrets.policy.yaml"), secretsPolicyYamlTemplate()),
|
|
115
|
+
ensureFile(join(root, "managed.yaml"), managedYamlTemplate()),
|
|
105
116
|
];
|
|
106
117
|
const probes = [
|
|
107
118
|
{ agent: "claude-code", run: () => claudeCodeProbe.probe(homeDir) },
|