agent-trellis 0.1.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.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/dist/adapters/claude-code.d.ts +23 -0
  4. package/dist/adapters/claude-code.js +86 -0
  5. package/dist/adapters/codex.d.ts +27 -0
  6. package/dist/adapters/codex.js +119 -0
  7. package/dist/adapters/jsonMcp.d.ts +24 -0
  8. package/dist/adapters/jsonMcp.js +84 -0
  9. package/dist/adapters/kiro.d.ts +34 -0
  10. package/dist/adapters/kiro.js +175 -0
  11. package/dist/adapters/mcpPlan.d.ts +28 -0
  12. package/dist/adapters/mcpPlan.js +83 -0
  13. package/dist/adapters/pi.d.ts +23 -0
  14. package/dist/adapters/pi.js +108 -0
  15. package/dist/adapters/symlinkPlan.d.ts +33 -0
  16. package/dist/adapters/symlinkPlan.js +120 -0
  17. package/dist/cli.d.ts +7 -0
  18. package/dist/cli.js +135 -0
  19. package/dist/commands/doctor.d.ts +88 -0
  20. package/dist/commands/doctor.js +269 -0
  21. package/dist/commands/init.d.ts +44 -0
  22. package/dist/commands/init.js +150 -0
  23. package/dist/commands/mcp.d.ts +28 -0
  24. package/dist/commands/mcp.js +70 -0
  25. package/dist/commands/migrate.d.ts +38 -0
  26. package/dist/commands/migrate.js +132 -0
  27. package/dist/commands/onboard.d.ts +50 -0
  28. package/dist/commands/onboard.js +155 -0
  29. package/dist/commands/secretsAudit.d.ts +35 -0
  30. package/dist/commands/secretsAudit.js +115 -0
  31. package/dist/commands/sync.d.ts +40 -0
  32. package/dist/commands/sync.js +91 -0
  33. package/dist/core/adapter.d.ts +133 -0
  34. package/dist/core/adapter.js +16 -0
  35. package/dist/core/canonical.d.ts +16 -0
  36. package/dist/core/canonical.js +148 -0
  37. package/dist/core/types.d.ts +201 -0
  38. package/dist/core/types.js +15 -0
  39. package/dist/lib/dirEquals.d.ts +7 -0
  40. package/dist/lib/dirEquals.js +39 -0
  41. package/dist/lib/envVarNames.d.ts +35 -0
  42. package/dist/lib/envVarNames.js +79 -0
  43. package/dist/lib/fsIdentity.d.ts +16 -0
  44. package/dist/lib/fsIdentity.js +53 -0
  45. package/dist/lib/mcpProbe.d.ts +14 -0
  46. package/dist/lib/mcpProbe.js +96 -0
  47. package/dist/lib/probeCommon.d.ts +24 -0
  48. package/dist/lib/probeCommon.js +108 -0
  49. package/dist/lib/secretEnv.d.ts +19 -0
  50. package/dist/lib/secretEnv.js +46 -0
  51. package/dist/lib/skillFile.d.ts +12 -0
  52. package/dist/lib/skillFile.js +26 -0
  53. package/dist/lib/syncArgs.d.ts +16 -0
  54. package/dist/lib/syncArgs.js +17 -0
  55. package/dist/lib/tomlSection.d.ts +57 -0
  56. package/dist/lib/tomlSection.js +162 -0
  57. package/dist/pi-bridge/bundle.js +32074 -0
  58. package/dist/pi-bridge/index.d.ts +48 -0
  59. package/dist/pi-bridge/index.js +188 -0
  60. package/dist/pi-bridge/schemaTranslate.d.ts +55 -0
  61. package/dist/pi-bridge/schemaTranslate.js +40 -0
  62. package/dist/probes/claude-code.d.ts +13 -0
  63. package/dist/probes/claude-code.js +48 -0
  64. package/dist/probes/codex.d.ts +24 -0
  65. package/dist/probes/codex.js +78 -0
  66. package/dist/probes/kiro.d.ts +12 -0
  67. package/dist/probes/kiro.js +48 -0
  68. package/dist/probes/pi.d.ts +14 -0
  69. package/dist/probes/pi.js +53 -0
  70. package/dist/sdk.d.ts +14 -0
  71. package/dist/sdk.js +13 -0
  72. package/docs/architecture.md +367 -0
  73. package/docs/getting-started.md +235 -0
  74. package/docs/implementation-plan.md +341 -0
  75. package/docs/research.md +175 -0
  76. package/docs/roadmap.md +484 -0
  77. package/package.json +59 -0
  78. package/schema/scope.example.yaml +33 -0
  79. package/schema/secrets.policy.example.yaml +43 -0
  80. package/schema/servers.example.yaml +87 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `trellis init` — bootstraps a minimal, valid `~/.trellis/` if one
3
+ * doesn't already exist, per-file (never overwrites an existing file;
4
+ * fills in only what's missing — trellis-cli-init). `loadCanonicalSource`
5
+ * only actually requires the `.trellis/` directory itself to exist; this
6
+ * command exists because an empty directory gives a new user nothing to
7
+ * act on.
8
+ */
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import { parse as parseYaml } from "yaml";
14
+ import * as claudeCodeProbe from "../probes/claude-code.js";
15
+ import * as codexProbe from "../probes/codex.js";
16
+ import * as kiroProbe from "../probes/kiro.js";
17
+ import * as piProbe from "../probes/pi.js";
18
+ import { ALL_AGENTS } from "../core/types.js";
19
+ /** Exported so `migrate` can byte-compare canonical `agents.md` against
20
+ * this exact string (design.md D3, trellis-cli-migrate) rather than
21
+ * retyping an equivalent one that could silently drift from this. */
22
+ export const AGENTS_MD_TEMPLATE = `# Shared instructions
23
+
24
+ Write what every agent should know here — communication preferences,
25
+ project conventions, anything you'd otherwise repeat per agent.
26
+
27
+ Already using Claude Code, Codex, Kiro, or pi? \`trellis migrate --from
28
+ <agent>\` imports its real instructions and skills instead of starting
29
+ from this placeholder.
30
+ `;
31
+ /**
32
+ * `known_host_injected` starts empty deliberately (design.md D3, D1):
33
+ * seeding it with any specific runtime's connector names (e.g. this
34
+ * project's own mirasim) would be actively wrong on a machine without
35
+ * that runtime. \`trellis doctor\`'s collision check reads this file's
36
+ * real value once it exists, so leaving it empty is not just a stub —
37
+ * it accurately means "nothing known to be host-injected here yet."
38
+ */
39
+ function serversYamlTemplate() {
40
+ return `# See schema/servers.example.yaml for the full documented shape
41
+ # (transports, env references, agent scoping, hub mode).
42
+ servers: {}
43
+
44
+ # Names a host environment (e.g. mirasim) injects into an agent process
45
+ # at runtime. A same-name static definition here would collide with it —
46
+ # on Codex that crashes the whole process at startup, not just that one
47
+ # server. Leave empty until you know what your own host injects; run
48
+ # \`trellis doctor\` to find out. See docs/research.md "Codex — three
49
+ # hard constraints".
50
+ known_host_injected: []
51
+ `;
52
+ }
53
+ /**
54
+ * Reads schema/secrets.policy.example.yaml's own \`reject_patterns\`
55
+ * rather than retyping them — those patterns are reviewed, shipped
56
+ * content; duplicating them here would let the two drift (design.md D1).
57
+ */
58
+ function secretsPolicyYamlTemplate() {
59
+ const here = dirname(fileURLToPath(import.meta.url));
60
+ const examplePath = join(here, "..", "..", "schema", "secrets.policy.example.yaml");
61
+ const parsed = parseYaml(readFileSync(examplePath, "utf-8"));
62
+ const patterns = parsed.reject_patterns ?? [];
63
+ const patternLines = patterns.map((p) => ` - '${p}'`).join("\n");
64
+ return `# See schema/secrets.policy.example.yaml for the full documented shape
65
+ # (env_file, what it does and doesn't protect).
66
+ allowed_vars: []
67
+
68
+ # Seeded from schema/secrets.policy.example.yaml's own reviewed list —
69
+ # generic credential shapes, not specific to any one project.
70
+ reject_patterns:
71
+ ${patternLines}
72
+ `;
73
+ }
74
+ /**
75
+ * Trellis never spawns an installer itself (global package installs are
76
+ * exactly the kind of irreversible, system-wide action that needs the
77
+ * user's own explicit "yes", not a silent trellis-init side effect) —
78
+ * these are printed as a pointer only. Verified against each project's
79
+ * own current install docs, same discipline as everything else here.
80
+ */
81
+ export const INSTALL_HINTS = {
82
+ "claude-code": "npm install -g @anthropic-ai/claude-code",
83
+ codex: "npm install -g @openai/codex",
84
+ kiro: "https://kiro.dev/downloads/",
85
+ pi: "npm install -g @earendil-works/pi-coding-agent",
86
+ };
87
+ function ensureFile(path, template) {
88
+ if (existsSync(path)) {
89
+ return { path, action: "already-present" };
90
+ }
91
+ writeFileSync(path, template);
92
+ return { path, action: "create" };
93
+ }
94
+ export async function collectInitReport(homeDir = homedir()) {
95
+ const root = join(homeDir, ".trellis");
96
+ mkdirSync(root, { recursive: true });
97
+ mkdirSync(join(root, "mcp"), { recursive: true });
98
+ mkdirSync(join(root, "skills"), { recursive: true });
99
+ mkdirSync(join(root, "agents"), { recursive: true });
100
+ mkdirSync(join(root, "memories"), { recursive: true });
101
+ const files = [
102
+ ensureFile(join(root, "agents.md"), AGENTS_MD_TEMPLATE),
103
+ ensureFile(join(root, "mcp", "servers.yaml"), serversYamlTemplate()),
104
+ ensureFile(join(root, "secrets.policy.yaml"), secretsPolicyYamlTemplate()),
105
+ ];
106
+ const probes = [
107
+ { agent: "claude-code", run: () => claudeCodeProbe.probe(homeDir) },
108
+ { agent: "codex", run: () => codexProbe.probe(homeDir) },
109
+ { agent: "kiro", run: () => kiroProbe.probe(homeDir) },
110
+ { agent: "pi", run: () => piProbe.probe(homeDir) },
111
+ ];
112
+ const settled = await Promise.allSettled(probes.map((p) => p.run()));
113
+ const agents = settled.map((result, index) => {
114
+ const agent = ALL_AGENTS[index];
115
+ const present = result.status === "fulfilled" && result.value.present;
116
+ const message = present
117
+ ? `${agent} is present — run \`trellis migrate --from ${agent}\` to import its skills and instructions`
118
+ : `${agent} — not detected on this machine. Install: ${INSTALL_HINTS[agent]}`;
119
+ return { agent, present, message };
120
+ });
121
+ return { files, agents };
122
+ }
123
+ export async function runInit(opts = {}) {
124
+ const homeDir = opts.homeDir ?? homedir();
125
+ const report = await collectInitReport(homeDir);
126
+ if (opts.json) {
127
+ console.log(JSON.stringify(report, null, 2));
128
+ }
129
+ else {
130
+ printReport(report);
131
+ }
132
+ return { exitCode: 0 };
133
+ }
134
+ function printReport(report) {
135
+ const created = report.files.filter((f) => f.action === "create");
136
+ if (created.length === 0) {
137
+ console.log("~/.trellis already fully initialized — nothing to create.");
138
+ }
139
+ else {
140
+ console.log(`Created ${created.length} file(s):`);
141
+ for (const f of created)
142
+ console.log(` - ${f.path}`);
143
+ }
144
+ const alreadyPresent = report.files.filter((f) => f.action === "already-present");
145
+ for (const f of alreadyPresent)
146
+ console.log(` - ${f.path} (already present, left untouched)`);
147
+ console.log("");
148
+ for (const a of report.agents)
149
+ console.log(a.message);
150
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `trellis mcp sync` — loads canonical once, runs every present agent's
3
+ * adapter, applies only the "mcp" slice of its plan, and reports what
4
+ * happened. Same create/repair/refuse shape as `src/commands/sync.ts`,
5
+ * kept as a separate command because MCP has no ownership marker for
6
+ * automatic removal yet (trellis-mcp-sync-p2 design.md D7) — folding it
7
+ * into bare `trellis sync` would blur that distinction.
8
+ */
9
+ import type { AdapterPlanItem } from "../core/adapter.js";
10
+ import type { AgentId } from "../core/types.js";
11
+ export interface RunMcpSyncOptions {
12
+ json?: boolean;
13
+ /** Same test/sandbox-only seam as `RunSyncOptions.homeDir` — never a CLI
14
+ * flag. See docs/architecture.md's testing philosophy. */
15
+ homeDir?: string;
16
+ }
17
+ export interface AgentMcpSyncReport {
18
+ agent: AgentId;
19
+ present: boolean;
20
+ items: AdapterPlanItem[];
21
+ }
22
+ export interface McpSyncReport {
23
+ reports: AgentMcpSyncReport[];
24
+ }
25
+ export declare function collectMcpSyncReport(opts?: RunMcpSyncOptions): Promise<McpSyncReport>;
26
+ export declare function runMcpSync(opts?: RunMcpSyncOptions): Promise<{
27
+ exitCode: number;
28
+ }>;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `trellis mcp sync` — loads canonical once, runs every present agent's
3
+ * adapter, applies only the "mcp" slice of its plan, and reports what
4
+ * happened. Same create/repair/refuse shape as `src/commands/sync.ts`,
5
+ * kept as a separate command because MCP has no ownership marker for
6
+ * automatic removal yet (trellis-mcp-sync-p2 design.md D7) — folding it
7
+ * into bare `trellis sync` would blur that distinction.
8
+ */
9
+ import { homedir } from "node:os";
10
+ import { loadCanonicalSource } from "../core/canonical.js";
11
+ import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
12
+ import { CodexAdapter } from "../adapters/codex.js";
13
+ import { KiroAdapter } from "../adapters/kiro.js";
14
+ import { PiAdapter } from "../adapters/pi.js";
15
+ function buildAdapters(homeDir) {
16
+ return [new ClaudeCodeAdapter(homeDir), new CodexAdapter(homeDir), new KiroAdapter(homeDir), new PiAdapter(homeDir)];
17
+ }
18
+ export async function collectMcpSyncReport(opts = {}) {
19
+ const homeDir = opts.homeDir ?? homedir();
20
+ const canonical = loadCanonicalSource(homeDir);
21
+ const reports = [];
22
+ for (const adapter of buildAdapters(homeDir)) {
23
+ const probeResult = await adapter.probe();
24
+ if (!probeResult.present) {
25
+ reports.push({ agent: adapter.id, present: false, items: [] });
26
+ continue;
27
+ }
28
+ const items = (await adapter.plan(canonical)).filter((item) => item.kind === "mcp");
29
+ await adapter.apply(items);
30
+ reports.push({ agent: adapter.id, present: true, items });
31
+ }
32
+ return { reports };
33
+ }
34
+ export async function runMcpSync(opts = {}) {
35
+ let report;
36
+ try {
37
+ report = await collectMcpSyncReport(opts);
38
+ }
39
+ catch (err) {
40
+ console.error(err instanceof Error ? err.message : String(err));
41
+ return { exitCode: 1 };
42
+ }
43
+ if (opts.json) {
44
+ console.log(JSON.stringify(report, null, 2));
45
+ }
46
+ else {
47
+ printReport(report);
48
+ }
49
+ const hasConflict = report.reports.some((r) => r.items.some((i) => i.action === "conflict"));
50
+ return { exitCode: hasConflict ? 1 : 0 };
51
+ }
52
+ function printReport(report) {
53
+ for (const { agent, present, items } of report.reports) {
54
+ if (!present) {
55
+ console.log(`— ${agent} (not installed)`);
56
+ continue;
57
+ }
58
+ const created = items.filter((i) => i.action === "create");
59
+ const conflicts = items.filter((i) => i.action === "conflict");
60
+ if (created.length === 0 && conflicts.length === 0) {
61
+ console.log(`✅ ${agent} — already in sync`);
62
+ continue;
63
+ }
64
+ const icon = conflicts.length > 0 ? "⚠️ " : "✅";
65
+ console.log(`${icon} ${agent} — ${created.length} created/updated, ${conflicts.length} conflict(s)`);
66
+ for (const item of [...created, ...conflicts]) {
67
+ console.log(` - [${item.action}] ${item.description}`);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `trellis migrate --from <agent>` — imports an existing agent's real
3
+ * skills and instructions into canonical source (trellis-cli-migrate).
4
+ * Pure `collectMigratePlan` / effectful `applyMigratePlan`, same
5
+ * plan-then-apply split every adapter already uses.
6
+ */
7
+ import type { AgentId } from "../core/types.js";
8
+ export type MigrateAction = "create" | "skip-symlink" | "skip-case-broken" | "already-migrated" | "conflict";
9
+ export interface MigratePlanItem {
10
+ kind: "skill" | "instructions";
11
+ name: string;
12
+ action: MigrateAction;
13
+ detail: string;
14
+ /** Only set when action === "create"; consumed by applyMigratePlan. */
15
+ sourceDir?: string;
16
+ sourceContent?: string;
17
+ }
18
+ export interface MigratePlan {
19
+ agent: AgentId;
20
+ present: boolean;
21
+ items: MigratePlanItem[];
22
+ }
23
+ export declare function collectMigratePlan(agent: AgentId, homeDir?: string): Promise<MigratePlan>;
24
+ export declare function applyMigratePlan(plan: MigratePlan, homeDir?: string): void;
25
+ export interface RunMigrateOptions {
26
+ from?: string;
27
+ dryRun?: boolean;
28
+ json?: boolean;
29
+ /** Defaults to the real `~`; overridable for tests only. */
30
+ homeDir?: string;
31
+ }
32
+ export declare function runMigrate(opts?: RunMigrateOptions): Promise<{
33
+ exitCode: number;
34
+ }>;
35
+ /** Exported so `onboard` prints a migrate plan identically to running
36
+ * `migrate` standalone, instead of a second, easily-drifting copy of
37
+ * this formatting (including the empty-plan "nothing to migrate" case). */
38
+ export declare function printPlan(plan: MigratePlan, dryRun: boolean): void;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `trellis migrate --from <agent>` — imports an existing agent's real
3
+ * skills and instructions into canonical source (trellis-cli-migrate).
4
+ * Pure `collectMigratePlan` / effectful `applyMigratePlan`, same
5
+ * plan-then-apply split every adapter already uses.
6
+ */
7
+ import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import * as claudeCodeProbe from "../probes/claude-code.js";
11
+ import * as codexProbe from "../probes/codex.js";
12
+ import * as kiroProbe from "../probes/kiro.js";
13
+ import * as piProbe from "../probes/pi.js";
14
+ import { AGENTS_MD_TEMPLATE } from "./init.js";
15
+ import { dirContentsEqual } from "../lib/dirEquals.js";
16
+ import { ALL_AGENTS } from "../core/types.js";
17
+ const PROBES = {
18
+ "claude-code": (homeDir) => claudeCodeProbe.probe(homeDir),
19
+ codex: (homeDir) => codexProbe.probe(homeDir),
20
+ kiro: (homeDir) => kiroProbe.probe(homeDir),
21
+ pi: (homeDir) => piProbe.probe(homeDir),
22
+ };
23
+ function planSkill(name, sourceDir, isSymlink, caseCorrect, canonicalDir) {
24
+ if (isSymlink) {
25
+ return { kind: "skill", name, action: "skip-symlink", detail: "shared in from elsewhere, not this agent's own content" };
26
+ }
27
+ if (!caseCorrect) {
28
+ return { kind: "skill", name, action: "skip-case-broken", detail: "already undiscoverable on at least one other agent — fix on the source before migrating" };
29
+ }
30
+ if (!existsSync(canonicalDir)) {
31
+ return { kind: "skill", name, action: "create", detail: `will copy from ${sourceDir}`, sourceDir };
32
+ }
33
+ if (dirContentsEqual(sourceDir, canonicalDir)) {
34
+ return { kind: "skill", name, action: "already-migrated", detail: "canonical content is byte-identical" };
35
+ }
36
+ return { kind: "skill", name, action: "conflict", detail: `canonical skills/${name}/ already exists with different content — resolve by hand` };
37
+ }
38
+ function planInstructions(snapshot, canonicalAgentsMd) {
39
+ if (!snapshot.instructionsFile)
40
+ return undefined;
41
+ if (snapshot.instructionsFile.isSymlink) {
42
+ return { kind: "instructions", name: "agents.md", action: "skip-symlink", detail: "source agent's instructions file is itself a symlink, nothing real to read" };
43
+ }
44
+ let sourceContent;
45
+ try {
46
+ sourceContent = readFileSync(snapshot.instructionsFile.path, "utf-8");
47
+ }
48
+ catch (err) {
49
+ return { kind: "instructions", name: "agents.md", action: "conflict", detail: `could not read source instructions: ${err instanceof Error ? err.message : String(err)}` };
50
+ }
51
+ if (!existsSync(canonicalAgentsMd)) {
52
+ return { kind: "instructions", name: "agents.md", action: "create", detail: "canonical agents.md does not exist yet", sourceContent };
53
+ }
54
+ const current = readFileSync(canonicalAgentsMd, "utf-8");
55
+ if (current === AGENTS_MD_TEMPLATE) {
56
+ return { kind: "instructions", name: "agents.md", action: "create", detail: "canonical agents.md is still trellis init's placeholder", sourceContent };
57
+ }
58
+ if (current === sourceContent) {
59
+ return { kind: "instructions", name: "agents.md", action: "already-migrated", detail: "canonical content is byte-identical" };
60
+ }
61
+ return { kind: "instructions", name: "agents.md", action: "conflict", detail: "canonical agents.md already has different real content — resolve by hand" };
62
+ }
63
+ export async function collectMigratePlan(agent, homeDir = homedir()) {
64
+ const snapshot = await PROBES[agent](homeDir);
65
+ if (!snapshot.present) {
66
+ return { agent, present: false, items: [] };
67
+ }
68
+ const canonicalRoot = join(homeDir, ".trellis");
69
+ const items = [];
70
+ for (const root of snapshot.skillRoots) {
71
+ for (const skill of root.skills) {
72
+ items.push(planSkill(skill.name, skill.dir, skill.isSymlink, skill.caseCorrect, join(canonicalRoot, "skills", skill.name)));
73
+ }
74
+ }
75
+ const instructionsItem = planInstructions(snapshot, join(canonicalRoot, "agents.md"));
76
+ if (instructionsItem)
77
+ items.push(instructionsItem);
78
+ return { agent, present: true, items };
79
+ }
80
+ export function applyMigratePlan(plan, homeDir = homedir()) {
81
+ const canonicalRoot = join(homeDir, ".trellis");
82
+ for (const item of plan.items) {
83
+ if (item.action !== "create")
84
+ continue;
85
+ if (item.kind === "skill" && item.sourceDir) {
86
+ const dest = join(canonicalRoot, "skills", item.name);
87
+ mkdirSync(dest, { recursive: true });
88
+ cpSync(item.sourceDir, dest, { recursive: true });
89
+ }
90
+ else if (item.kind === "instructions" && item.sourceContent !== undefined) {
91
+ mkdirSync(canonicalRoot, { recursive: true });
92
+ writeFileSync(join(canonicalRoot, "agents.md"), item.sourceContent);
93
+ }
94
+ }
95
+ }
96
+ export async function runMigrate(opts = {}) {
97
+ const homeDir = opts.homeDir ?? homedir();
98
+ if (!opts.from || !ALL_AGENTS.includes(opts.from)) {
99
+ console.error(`--from must be one of: ${ALL_AGENTS.join(", ")} (got ${opts.from ?? "(none)"})`);
100
+ return { exitCode: 1 };
101
+ }
102
+ const agent = opts.from;
103
+ const plan = await collectMigratePlan(agent, homeDir);
104
+ if (!plan.present) {
105
+ console.error(`${agent} is not present on this machine — nothing to migrate.`);
106
+ return { exitCode: 1 };
107
+ }
108
+ if (!opts.dryRun) {
109
+ applyMigratePlan(plan, homeDir);
110
+ }
111
+ if (opts.json) {
112
+ console.log(JSON.stringify(plan, null, 2));
113
+ }
114
+ else {
115
+ printPlan(plan, opts.dryRun ?? false);
116
+ }
117
+ const hasConflict = plan.items.some((i) => i.action === "conflict");
118
+ return { exitCode: hasConflict ? 1 : 0 };
119
+ }
120
+ /** Exported so `onboard` prints a migrate plan identically to running
121
+ * `migrate` standalone, instead of a second, easily-drifting copy of
122
+ * this formatting (including the empty-plan "nothing to migrate" case). */
123
+ export function printPlan(plan, dryRun) {
124
+ console.log(`${dryRun ? "[dry run] " : ""}migrate --from ${plan.agent}`);
125
+ if (plan.items.length === 0) {
126
+ console.log(" nothing to migrate");
127
+ return;
128
+ }
129
+ for (const item of plan.items) {
130
+ console.log(` [${item.action}] ${item.kind === "skill" ? `skill "${item.name}"` : "instructions"} — ${item.detail}`);
131
+ }
132
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `trellis onboard` — chains `init` → agent detection → base-agent
3
+ * resolution → `migrate` → `sync` into one guided flow
4
+ * (trellis-cli-onboard). Orchestrates existing commands' own
5
+ * plan/apply logic; no new skill-copy, symlink, or conflict-detection
6
+ * judgment is made here.
7
+ */
8
+ import type { AgentId } from "../core/types.js";
9
+ import type { MigratePlan } from "./migrate.js";
10
+ import type { SyncReport } from "./sync.js";
11
+ export interface OnboardAgentSummary {
12
+ agent: AgentId;
13
+ present: boolean;
14
+ skillCount: number;
15
+ skillNames: string[];
16
+ /** instructionsFile exists and isn't itself a symlink — real content
17
+ * of that agent's own to potentially migrate, not a judgment about
18
+ * its quality or length. */
19
+ hasRealInstructions: boolean;
20
+ }
21
+ export declare function collectOnboardSummary(homeDir?: string): Promise<OnboardAgentSummary[]>;
22
+ export interface RunOnboardOptions {
23
+ agent?: string;
24
+ dryRun?: boolean;
25
+ json?: boolean;
26
+ /** Defaults to the real `~`; overridable for tests only. */
27
+ homeDir?: string;
28
+ /** Test-only: overrides the real `process.stdin.isTTY` check so the
29
+ * interactive-vs-refuse branch is exercisable without a real terminal. */
30
+ isTTY?: boolean;
31
+ /** Test-only: replaces the real readline prompt with a scripted
32
+ * answer, so the prompt path is exercisable without a real terminal. */
33
+ promptForAgent?: (present: OnboardAgentSummary[]) => Promise<string>;
34
+ }
35
+ export interface OnboardResult {
36
+ summary: OnboardAgentSummary[];
37
+ base?: AgentId;
38
+ baseReason?: "auto-selected" | "flag" | "prompt";
39
+ migratePlan?: MigratePlan;
40
+ syncReport?: SyncReport;
41
+ refusal?: string;
42
+ /** Only set when no agent is present — the same values `--json` and
43
+ * text output both surface, so a machine caller doesn't have to
44
+ * hardcode them a second time. */
45
+ installHints?: Record<AgentId, string>;
46
+ }
47
+ export declare function collectOnboardPlan(opts?: RunOnboardOptions): Promise<OnboardResult>;
48
+ export declare function runOnboard(opts?: RunOnboardOptions): Promise<{
49
+ exitCode: number;
50
+ }>;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * `trellis onboard` — chains `init` → agent detection → base-agent
3
+ * resolution → `migrate` → `sync` into one guided flow
4
+ * (trellis-cli-onboard). Orchestrates existing commands' own
5
+ * plan/apply logic; no new skill-copy, symlink, or conflict-detection
6
+ * judgment is made here.
7
+ */
8
+ import { createInterface } from "node:readline/promises";
9
+ import { homedir } from "node:os";
10
+ import * as claudeCodeProbe from "../probes/claude-code.js";
11
+ import * as codexProbe from "../probes/codex.js";
12
+ import * as kiroProbe from "../probes/kiro.js";
13
+ import * as piProbe from "../probes/pi.js";
14
+ import { ALL_AGENTS } from "../core/types.js";
15
+ import { INSTALL_HINTS, collectInitReport } from "./init.js";
16
+ import { applyMigratePlan, collectMigratePlan, printPlan as printMigratePlan } from "./migrate.js";
17
+ import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
18
+ const PROBES = {
19
+ "claude-code": (homeDir) => claudeCodeProbe.probe(homeDir),
20
+ codex: (homeDir) => codexProbe.probe(homeDir),
21
+ kiro: (homeDir) => kiroProbe.probe(homeDir),
22
+ pi: (homeDir) => piProbe.probe(homeDir),
23
+ };
24
+ export async function collectOnboardSummary(homeDir = homedir()) {
25
+ return Promise.all(ALL_AGENTS.map(async (agent) => {
26
+ const snapshot = await PROBES[agent](homeDir);
27
+ if (!snapshot.present) {
28
+ return { agent, present: false, skillCount: 0, skillNames: [], hasRealInstructions: false };
29
+ }
30
+ const skillNames = snapshot.skillRoots.flatMap((root) => root.skills.map((s) => s.name));
31
+ const hasRealInstructions = snapshot.instructionsFile !== undefined && !snapshot.instructionsFile.isSymlink;
32
+ return { agent, present: true, skillCount: skillNames.length, skillNames, hasRealInstructions };
33
+ }));
34
+ }
35
+ async function promptForAgentReal(present) {
36
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
37
+ try {
38
+ console.log("Multiple agents detected:");
39
+ for (const s of present) {
40
+ const skills = s.skillCount > 0 ? ` (${s.skillNames.join(", ")})` : "";
41
+ console.log(` ${s.agent} — ${s.skillCount} skill(s)${skills}, instructions: ${s.hasRealInstructions ? "yes" : "no"}`);
42
+ }
43
+ for (let attempt = 0; attempt < 2; attempt++) {
44
+ const answer = (await rl.question("Choose a base agent to migrate from: ")).trim();
45
+ if (present.some((s) => s.agent === answer))
46
+ return answer;
47
+ console.log(`Not one of the present agents: ${present.map((s) => s.agent).join(", ")}`);
48
+ }
49
+ throw new Error("no valid agent chosen after 2 attempts");
50
+ }
51
+ finally {
52
+ rl.close();
53
+ }
54
+ }
55
+ export async function collectOnboardPlan(opts = {}) {
56
+ const homeDir = opts.homeDir ?? homedir();
57
+ await collectInitReport(homeDir);
58
+ const summary = await collectOnboardSummary(homeDir);
59
+ const present = summary.filter((s) => s.present);
60
+ if (present.length === 0) {
61
+ return { summary, installHints: { ...INSTALL_HINTS } };
62
+ }
63
+ let base;
64
+ let baseReason;
65
+ if (opts.agent) {
66
+ const match = present.find((s) => s.agent === opts.agent);
67
+ if (!match) {
68
+ return {
69
+ summary,
70
+ refusal: `"${opts.agent}" is not one of the present agents (${present.map((s) => s.agent).join(", ")})`,
71
+ };
72
+ }
73
+ base = match.agent;
74
+ baseReason = "flag";
75
+ }
76
+ else if (present.length === 1) {
77
+ base = present[0].agent;
78
+ baseReason = "auto-selected";
79
+ }
80
+ else {
81
+ const canPrompt = !opts.json && (opts.isTTY ?? process.stdin.isTTY === true);
82
+ if (!canPrompt) {
83
+ return {
84
+ summary,
85
+ refusal: `multiple agents detected (${present.map((s) => s.agent).join(", ")}) and no terminal to prompt in — pass --agent <id>`,
86
+ };
87
+ }
88
+ const prompt = opts.promptForAgent ?? promptForAgentReal;
89
+ try {
90
+ base = (await prompt(present));
91
+ }
92
+ catch (err) {
93
+ return { summary, refusal: err instanceof Error ? err.message : String(err) };
94
+ }
95
+ baseReason = "prompt";
96
+ }
97
+ const migratePlan = await collectMigratePlan(base, homeDir);
98
+ if (!opts.dryRun) {
99
+ applyMigratePlan(migratePlan, homeDir);
100
+ }
101
+ const syncReport = await collectSyncReport({ homeDir, dryRun: opts.dryRun });
102
+ return { summary, base, baseReason, migratePlan, syncReport };
103
+ }
104
+ export async function runOnboard(opts = {}) {
105
+ const result = await collectOnboardPlan(opts);
106
+ if (opts.json) {
107
+ console.log(JSON.stringify(result, null, 2));
108
+ }
109
+ else {
110
+ printResult(result, opts.dryRun ?? false);
111
+ }
112
+ if (result.refusal)
113
+ return { exitCode: 1 };
114
+ const hasConflict = (result.migratePlan?.items.some((i) => i.action === "conflict") ?? false) ||
115
+ (result.syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false);
116
+ return { exitCode: hasConflict ? 1 : 0 };
117
+ }
118
+ function printResult(result, dryRun) {
119
+ if (dryRun)
120
+ console.log("[dry run]");
121
+ const present = result.summary.filter((s) => s.present);
122
+ if (present.length === 0) {
123
+ console.log("No agent detected on this machine yet. Install one, then re-run `trellis onboard`:");
124
+ for (const agent of ALL_AGENTS) {
125
+ console.log(` ${agent}: ${INSTALL_HINTS[agent]}`);
126
+ }
127
+ return;
128
+ }
129
+ if (result.refusal) {
130
+ console.error(result.refusal);
131
+ return;
132
+ }
133
+ if (result.baseReason === "auto-selected") {
134
+ console.log(`Only ${result.base} detected — using it as the migration base.`);
135
+ }
136
+ else if (result.baseReason === "flag") {
137
+ console.log(`Using ${result.base} as the migration base (--agent).`);
138
+ }
139
+ else if (result.baseReason === "prompt") {
140
+ console.log(`Using ${result.base} as the migration base.`);
141
+ }
142
+ // Reuse `migrate`/`sync`'s own printing verbatim (including the
143
+ // "nothing to migrate" / "already in sync" cases) rather than a second,
144
+ // easily-drifting copy of this formatting. `dryRun: false` here since
145
+ // this function's own leading "[dry run]" line already said so once.
146
+ if (result.migratePlan) {
147
+ console.log("");
148
+ printMigratePlan(result.migratePlan, false);
149
+ }
150
+ if (result.syncReport) {
151
+ console.log("\nsync");
152
+ printSyncReport(result.syncReport, false);
153
+ }
154
+ console.log("\nNext: `trellis mcp sync` to distribute MCP servers, `trellis secrets audit` to check for leaked credentials.");
155
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `trellis secrets audit` — reads every present, MCP-capable agent's real
3
+ * config file (never the canonical source) and checks it against
4
+ * `CanonicalSource.secretsPolicy`: a literal-value scan against
5
+ * `rejectPatterns`, and a declared-env-var-name check against
6
+ * `allowedVars`. Also checks, agent-agnostically, whether every env var
7
+ * name declared across canonical `mcp.servers[*].env` actually resolves
8
+ * to a value via the same `resolveSecretEnv` the pi bridge uses
9
+ * (trellis-secrets-env-management) — authoritative for pi, a best-effort
10
+ * proxy for the other three (design.md D3 in that change). Read-only —
11
+ * never writes anything. Fails non-zero on any finding
12
+ * (trellis-secrets-audit-p3).
13
+ */
14
+ import type { AgentId } from "../core/types.js";
15
+ export interface RunSecretsAuditOptions {
16
+ json?: boolean;
17
+ /** Same test/sandbox-only seam as every other command — never a CLI
18
+ * flag. See docs/architecture.md's testing philosophy. */
19
+ homeDir?: string;
20
+ }
21
+ export interface SecretsFinding {
22
+ /** "environment" for `missing-env-value` — that check isn't scoped to
23
+ * any single agent's config file (see module doc comment). */
24
+ agent: AgentId | "environment";
25
+ file: string;
26
+ kind: "literal-secret" | "unexpected-var-name" | "missing-env-value";
27
+ detail: string;
28
+ }
29
+ export interface SecretsAuditReport {
30
+ findings: SecretsFinding[];
31
+ }
32
+ export declare function collectSecretsAuditReport(opts?: RunSecretsAuditOptions): Promise<SecretsAuditReport>;
33
+ export declare function runSecretsAudit(opts?: RunSecretsAuditOptions): Promise<{
34
+ exitCode: number;
35
+ }>;