@os-eco/seeds-cli 0.2.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.
@@ -0,0 +1,86 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ let tmpDir: string;
7
+
8
+ // Path to the CLI entry point (relative to repo root)
9
+ const CLI = join(import.meta.dir, "../../src/index.ts");
10
+
11
+ async function run(
12
+ args: string[],
13
+ cwd: string,
14
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
15
+ const proc = Bun.spawn(["bun", "run", CLI, ...args], {
16
+ cwd,
17
+ stdout: "pipe",
18
+ stderr: "pipe",
19
+ });
20
+ const stdout = await new Response(proc.stdout).text();
21
+ const stderr = await new Response(proc.stderr).text();
22
+ const exitCode = await proc.exited;
23
+ return { stdout, stderr, exitCode };
24
+ }
25
+
26
+ beforeEach(async () => {
27
+ tmpDir = await mkdtemp(join(tmpdir(), "seeds-init-test-"));
28
+ });
29
+
30
+ afterEach(async () => {
31
+ await rm(tmpDir, { recursive: true, force: true });
32
+ });
33
+
34
+ describe("sd init", () => {
35
+ test("creates .seeds directory", async () => {
36
+ const { exitCode } = await run(["init"], tmpDir);
37
+ expect(exitCode).toBe(0);
38
+ const stat = await Bun.file(join(tmpDir, ".seeds", "config.yaml")).exists();
39
+ expect(stat).toBe(true);
40
+ });
41
+
42
+ test("creates config.yaml with project name", async () => {
43
+ await run(["init"], tmpDir);
44
+ const config = await Bun.file(join(tmpDir, ".seeds", "config.yaml")).text();
45
+ expect(config).toContain("project:");
46
+ expect(config).toContain("version:");
47
+ });
48
+
49
+ test("creates empty issues.jsonl", async () => {
50
+ await run(["init"], tmpDir);
51
+ const exists = await Bun.file(join(tmpDir, ".seeds", "issues.jsonl")).exists();
52
+ expect(exists).toBe(true);
53
+ });
54
+
55
+ test("creates empty templates.jsonl", async () => {
56
+ await run(["init"], tmpDir);
57
+ const exists = await Bun.file(join(tmpDir, ".seeds", "templates.jsonl")).exists();
58
+ expect(exists).toBe(true);
59
+ });
60
+
61
+ test("creates .gitignore ignoring lock files", async () => {
62
+ await run(["init"], tmpDir);
63
+ const gitignore = await Bun.file(join(tmpDir, ".seeds", ".gitignore")).text();
64
+ expect(gitignore).toContain("*.lock");
65
+ });
66
+
67
+ test("appends gitattributes to project root", async () => {
68
+ await run(["init"], tmpDir);
69
+ const gitattributes = await Bun.file(join(tmpDir, ".gitattributes")).text();
70
+ expect(gitattributes).toContain(".seeds/issues.jsonl merge=union");
71
+ expect(gitattributes).toContain(".seeds/templates.jsonl merge=union");
72
+ });
73
+
74
+ test("is idempotent — second init does not fail", async () => {
75
+ await run(["init"], tmpDir);
76
+ const { exitCode } = await run(["init"], tmpDir);
77
+ expect(exitCode).toBe(0);
78
+ });
79
+
80
+ test("--json flag returns success JSON", async () => {
81
+ const { stdout, exitCode } = await run(["init", "--json"], tmpDir);
82
+ expect(exitCode).toBe(0);
83
+ const result = JSON.parse(stdout) as { success: boolean };
84
+ expect(result.success).toBe(true);
85
+ });
86
+ });
@@ -0,0 +1,49 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { outputJson, printSuccess } from "../output.ts";
4
+ import { CONFIG_FILE, ISSUES_FILE, SEEDS_DIR_NAME, TEMPLATES_FILE } from "../types.ts";
5
+
6
+ export async function run(args: string[]): Promise<void> {
7
+ const jsonMode = args.includes("--json");
8
+ const cwd = process.cwd();
9
+ const seedsDir = join(cwd, SEEDS_DIR_NAME);
10
+
11
+ if (existsSync(join(seedsDir, CONFIG_FILE))) {
12
+ if (jsonMode) {
13
+ outputJson({ success: true, command: "init", dir: seedsDir });
14
+ } else {
15
+ printSuccess(`Already initialized: ${seedsDir}`);
16
+ }
17
+ return;
18
+ }
19
+
20
+ mkdirSync(seedsDir, { recursive: true });
21
+
22
+ // config.yaml
23
+ writeFileSync(join(seedsDir, CONFIG_FILE), 'project: "seeds"\nversion: "1"\n');
24
+
25
+ // empty JSONL files
26
+ writeFileSync(join(seedsDir, ISSUES_FILE), "");
27
+ writeFileSync(join(seedsDir, TEMPLATES_FILE), "");
28
+
29
+ // .gitignore inside .seeds/
30
+ writeFileSync(join(seedsDir, ".gitignore"), "*.lock\n");
31
+
32
+ // Append .gitattributes to project root
33
+ const gitattrsPath = join(cwd, ".gitattributes");
34
+ const entry = ".seeds/issues.jsonl merge=union\n.seeds/templates.jsonl merge=union\n";
35
+ if (existsSync(gitattrsPath)) {
36
+ const existing = readFileSync(gitattrsPath, "utf8");
37
+ if (!existing.includes(".seeds/issues.jsonl")) {
38
+ writeFileSync(gitattrsPath, `${existing}\n${entry}`);
39
+ }
40
+ } else {
41
+ writeFileSync(gitattrsPath, entry);
42
+ }
43
+
44
+ if (jsonMode) {
45
+ outputJson({ success: true, command: "init", dir: seedsDir });
46
+ } else {
47
+ printSuccess(`Initialized .seeds/ in ${cwd}`);
48
+ }
49
+ }
@@ -0,0 +1,69 @@
1
+ import { findSeedsDir } from "../config.ts";
2
+ import { outputJson, printIssueOneLine } from "../output.ts";
3
+ import { readIssues } from "../store.ts";
4
+ import type { Issue } from "../types.ts";
5
+
6
+ function parseArgs(args: string[]) {
7
+ const flags: Record<string, string | boolean> = {};
8
+ let i = 0;
9
+ while (i < args.length) {
10
+ const arg = args[i];
11
+ if (!arg) {
12
+ i++;
13
+ continue;
14
+ }
15
+ if (arg.startsWith("--")) {
16
+ const key = arg.slice(2);
17
+ const eqIdx = key.indexOf("=");
18
+ if (eqIdx !== -1) {
19
+ flags[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
20
+ i++;
21
+ } else {
22
+ const next = args[i + 1];
23
+ if (next !== undefined && !next.startsWith("--")) {
24
+ flags[key] = next;
25
+ i += 2;
26
+ } else {
27
+ flags[key] = true;
28
+ i++;
29
+ }
30
+ }
31
+ } else {
32
+ i++;
33
+ }
34
+ }
35
+ return flags;
36
+ }
37
+
38
+ export async function run(args: string[], seedsDir?: string): Promise<void> {
39
+ const jsonMode = args.includes("--json");
40
+ const flags = parseArgs(args);
41
+
42
+ const statusFilter = typeof flags.status === "string" ? flags.status : undefined;
43
+ const typeFilter = typeof flags.type === "string" ? flags.type : undefined;
44
+ const assigneeFilter = typeof flags.assignee === "string" ? flags.assignee : undefined;
45
+ const limitStr = typeof flags.limit === "string" ? flags.limit : "50";
46
+ const limit = Number.parseInt(limitStr, 10) || 50;
47
+
48
+ const dir = seedsDir ?? (await findSeedsDir());
49
+ let issues = await readIssues(dir);
50
+
51
+ if (statusFilter) issues = issues.filter((i: Issue) => i.status === statusFilter);
52
+ if (typeFilter) issues = issues.filter((i: Issue) => i.type === typeFilter);
53
+ if (assigneeFilter) issues = issues.filter((i: Issue) => i.assignee === assigneeFilter);
54
+
55
+ issues = issues.slice(0, limit);
56
+
57
+ if (jsonMode) {
58
+ outputJson({ success: true, command: "list", issues, count: issues.length });
59
+ } else {
60
+ if (issues.length === 0) {
61
+ console.log("No issues found.");
62
+ return;
63
+ }
64
+ for (const issue of issues) {
65
+ printIssueOneLine(issue);
66
+ }
67
+ console.log(`\n${issues.length} issue(s)`);
68
+ }
69
+ }
@@ -0,0 +1,117 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { findSeedsDir, projectRootFromSeedsDir } from "../config.ts";
4
+ import { outputJson } from "../output.ts";
5
+ import { issuesPath, readIssues, withLock, writeIssues } from "../store.ts";
6
+ import type { Issue } from "../types.ts";
7
+
8
+ interface BeadsIssue {
9
+ id?: string;
10
+ title?: string;
11
+ status?: string;
12
+ issue_type?: string;
13
+ type?: string;
14
+ priority?: number;
15
+ owner?: string;
16
+ assignee?: string;
17
+ description?: string;
18
+ close_reason?: string;
19
+ closeReason?: string;
20
+ blocks?: string[];
21
+ blocked_by?: string[];
22
+ blockedBy?: string[];
23
+ created_at?: string;
24
+ createdAt?: string;
25
+ updated_at?: string;
26
+ updatedAt?: string;
27
+ closed_at?: string;
28
+ closedAt?: string;
29
+ }
30
+
31
+ function mapStatus(s: string | undefined): Issue["status"] {
32
+ if (s === "in_progress" || s === "in-progress") return "in_progress";
33
+ if (s === "closed" || s === "done" || s === "complete") return "closed";
34
+ return "open";
35
+ }
36
+
37
+ function mapType(t: string | undefined): Issue["type"] {
38
+ if (t === "bug") return "bug";
39
+ if (t === "feature") return "feature";
40
+ if (t === "epic") return "epic";
41
+ return "task";
42
+ }
43
+
44
+ function mapBeadsIssue(b: BeadsIssue): Issue | null {
45
+ if (!b.id || !b.title) return null;
46
+ const now = new Date().toISOString();
47
+ const issue: Issue = {
48
+ id: b.id,
49
+ title: b.title,
50
+ status: mapStatus(b.status),
51
+ type: mapType(b.issue_type ?? b.type),
52
+ priority: b.priority ?? 2,
53
+ createdAt: b.created_at ?? b.createdAt ?? now,
54
+ updatedAt: b.updated_at ?? b.updatedAt ?? now,
55
+ };
56
+ const assignee = b.owner ?? b.assignee;
57
+ if (assignee) issue.assignee = assignee;
58
+ if (b.description) issue.description = b.description;
59
+ const closeReason = b.close_reason ?? b.closeReason;
60
+ if (closeReason) issue.closeReason = closeReason;
61
+ const blockedBy = b.blocked_by ?? b.blockedBy;
62
+ if (blockedBy?.length) issue.blockedBy = blockedBy;
63
+ if (b.blocks?.length) issue.blocks = b.blocks;
64
+ const closedAt = b.closed_at ?? b.closedAt;
65
+ if (closedAt) issue.closedAt = closedAt;
66
+ return issue;
67
+ }
68
+
69
+ export async function run(args: string[], seedsDir?: string): Promise<void> {
70
+ const jsonMode = args.includes("--json");
71
+ const dir = seedsDir ?? (await findSeedsDir());
72
+ const projectRoot = projectRootFromSeedsDir(dir);
73
+
74
+ const beadsPath = join(projectRoot, ".beads", "issues.jsonl");
75
+ if (!existsSync(beadsPath)) {
76
+ throw new Error(`Beads issues not found at: ${beadsPath}`);
77
+ }
78
+
79
+ const file = Bun.file(beadsPath);
80
+ const content = await file.text();
81
+ const lines = content.split("\n").filter((l) => l.trim());
82
+
83
+ const beadsIssues: BeadsIssue[] = [];
84
+ for (const line of lines) {
85
+ try {
86
+ beadsIssues.push(JSON.parse(line) as BeadsIssue);
87
+ } catch {
88
+ // skip malformed lines
89
+ }
90
+ }
91
+
92
+ const mapped: Issue[] = [];
93
+ const skipped: string[] = [];
94
+ for (const b of beadsIssues) {
95
+ const issue = mapBeadsIssue(b);
96
+ if (issue) mapped.push(issue);
97
+ else skipped.push(b.id ?? "(unknown)");
98
+ }
99
+
100
+ let written = 0;
101
+ await withLock(issuesPath(dir), async () => {
102
+ const existing = await readIssues(dir);
103
+ const existingIds = new Set(existing.map((i) => i.id));
104
+ const newIssues = mapped.filter((i) => !existingIds.has(i.id));
105
+ await writeIssues(dir, [...existing, ...newIssues]);
106
+ written = newIssues.length;
107
+ });
108
+
109
+ if (jsonMode) {
110
+ outputJson({ success: true, command: "migrate-from-beads", written, skipped: skipped.length });
111
+ } else {
112
+ console.log(`Migrated ${written} issues from beads.`);
113
+ if (skipped.length > 0) {
114
+ console.log(`Skipped ${skipped.length} malformed issues.`);
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,155 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ let tmpDir: string;
7
+
8
+ const CLI = join(import.meta.dir, "../../src/index.ts");
9
+
10
+ async function run(
11
+ args: string[],
12
+ cwd: string,
13
+ ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
14
+ const proc = Bun.spawn(["bun", "run", CLI, ...args], {
15
+ cwd,
16
+ stdout: "pipe",
17
+ stderr: "pipe",
18
+ });
19
+ const stdout = await new Response(proc.stdout).text();
20
+ const stderr = await new Response(proc.stderr).text();
21
+ const exitCode = await proc.exited;
22
+ return { stdout, stderr, exitCode };
23
+ }
24
+
25
+ async function initSeeds(cwd: string): Promise<void> {
26
+ await run(["init"], cwd);
27
+ }
28
+
29
+ beforeEach(async () => {
30
+ tmpDir = await mkdtemp(join(tmpdir(), "seeds-onboard-test-"));
31
+ });
32
+
33
+ afterEach(async () => {
34
+ await rm(tmpDir, { recursive: true, force: true });
35
+ });
36
+
37
+ describe("sd onboard", () => {
38
+ test("fails without .seeds/ initialized", async () => {
39
+ const { exitCode, stderr } = await run(["onboard"], tmpDir);
40
+ expect(exitCode).toBe(1);
41
+ expect(stderr).toContain("Not in a seeds project");
42
+ });
43
+
44
+ test("creates CLAUDE.md when no target file exists", async () => {
45
+ await initSeeds(tmpDir);
46
+ const { exitCode } = await run(["onboard"], tmpDir);
47
+ expect(exitCode).toBe(0);
48
+ const content = await Bun.file(join(tmpDir, "CLAUDE.md")).text();
49
+ expect(content).toContain("<!-- seeds:start -->");
50
+ expect(content).toContain("<!-- seeds:end -->");
51
+ expect(content).toContain("Issue Tracking (Seeds)");
52
+ expect(content).toContain("sd prime");
53
+ });
54
+
55
+ test("appends to existing CLAUDE.md", async () => {
56
+ await initSeeds(tmpDir);
57
+ await Bun.write(join(tmpDir, "CLAUDE.md"), "# My Project\n\nExisting content.\n");
58
+ const { exitCode } = await run(["onboard"], tmpDir);
59
+ expect(exitCode).toBe(0);
60
+ const content = await Bun.file(join(tmpDir, "CLAUDE.md")).text();
61
+ expect(content).toContain("# My Project");
62
+ expect(content).toContain("Existing content.");
63
+ expect(content).toContain("<!-- seeds:start -->");
64
+ expect(content).toContain("Issue Tracking (Seeds)");
65
+ });
66
+
67
+ test("is idempotent — second onboard does not duplicate", async () => {
68
+ await initSeeds(tmpDir);
69
+ await run(["onboard"], tmpDir);
70
+ const first = await Bun.file(join(tmpDir, "CLAUDE.md")).text();
71
+ await run(["onboard"], tmpDir);
72
+ const second = await Bun.file(join(tmpDir, "CLAUDE.md")).text();
73
+ expect(second).toBe(first);
74
+ });
75
+
76
+ test("--check reports missing when no file exists", async () => {
77
+ await initSeeds(tmpDir);
78
+ const { stdout, exitCode } = await run(["onboard", "--check"], tmpDir);
79
+ expect(exitCode).toBe(0);
80
+ expect(stdout).toContain("missing");
81
+ });
82
+
83
+ test("--check reports current after onboard", async () => {
84
+ await initSeeds(tmpDir);
85
+ await run(["onboard"], tmpDir);
86
+ const { stdout, exitCode } = await run(["onboard", "--check"], tmpDir);
87
+ expect(exitCode).toBe(0);
88
+ expect(stdout).toContain("current");
89
+ });
90
+
91
+ test("--check with --json returns structured output", async () => {
92
+ await initSeeds(tmpDir);
93
+ await run(["onboard"], tmpDir);
94
+ const { stdout, exitCode } = await run(["onboard", "--check", "--json"], tmpDir);
95
+ expect(exitCode).toBe(0);
96
+ const result = JSON.parse(stdout) as { success: boolean; status: string };
97
+ expect(result.success).toBe(true);
98
+ expect(result.status).toBe("current");
99
+ });
100
+
101
+ test("--stdout prints snippet without writing", async () => {
102
+ await initSeeds(tmpDir);
103
+ const { stdout, exitCode } = await run(["onboard", "--stdout"], tmpDir);
104
+ expect(exitCode).toBe(0);
105
+ expect(stdout).toContain("<!-- seeds:start -->");
106
+ expect(stdout).toContain("Issue Tracking (Seeds)");
107
+ // Should not have created the file
108
+ const exists = await Bun.file(join(tmpDir, "CLAUDE.md")).exists();
109
+ expect(exists).toBe(false);
110
+ });
111
+
112
+ test("detects existing CLAUDE.md in .claude/ subdirectory", async () => {
113
+ await initSeeds(tmpDir);
114
+ const claudeDir = join(tmpDir, ".claude");
115
+ await Bun.write(join(claudeDir, "CLAUDE.md"), "# Agent Instructions\n");
116
+ const { exitCode } = await run(["onboard"], tmpDir);
117
+ expect(exitCode).toBe(0);
118
+ const content = await Bun.file(join(claudeDir, "CLAUDE.md")).text();
119
+ expect(content).toContain("<!-- seeds:start -->");
120
+ // Root CLAUDE.md should NOT have been created
121
+ const rootExists = await Bun.file(join(tmpDir, "CLAUDE.md")).exists();
122
+ expect(rootExists).toBe(false);
123
+ });
124
+
125
+ test("updates outdated section when version changes", async () => {
126
+ await initSeeds(tmpDir);
127
+ // Write a seeds section with an old version marker
128
+ const oldContent =
129
+ "# Project\n\n<!-- seeds:start -->\n## Old Seeds Section\n<!-- seeds-onboard-v:0 -->\nold content\n<!-- seeds:end -->\n";
130
+ await Bun.write(join(tmpDir, "CLAUDE.md"), oldContent);
131
+ const { exitCode } = await run(["onboard"], tmpDir);
132
+ expect(exitCode).toBe(0);
133
+ const content = await Bun.file(join(tmpDir, "CLAUDE.md")).text();
134
+ expect(content).toContain("# Project");
135
+ expect(content).toContain("seeds-onboard-v:1");
136
+ expect(content).not.toContain("seeds-onboard-v:0");
137
+ expect(content).not.toContain("Old Seeds Section");
138
+ });
139
+
140
+ test("--json output on create", async () => {
141
+ await initSeeds(tmpDir);
142
+ const { stdout, exitCode } = await run(["onboard", "--json"], tmpDir);
143
+ expect(exitCode).toBe(0);
144
+ const result = JSON.parse(stdout) as { success: boolean; action: string };
145
+ expect(result.success).toBe(true);
146
+ expect(result.action).toBe("created");
147
+ });
148
+
149
+ test("includes version marker in output", async () => {
150
+ await initSeeds(tmpDir);
151
+ await run(["onboard"], tmpDir);
152
+ const content = await Bun.file(join(tmpDir, "CLAUDE.md")).text();
153
+ expect(content).toContain("seeds-onboard-v:1");
154
+ });
155
+ });
@@ -0,0 +1,140 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { findSeedsDir, projectRootFromSeedsDir } from "../config.ts";
4
+ import { hasMarkerSection, replaceMarkerSection, wrapInMarkers } from "../markers.ts";
5
+ import { outputJson, printSuccess } from "../output.ts";
6
+
7
+ const ONBOARD_VERSION = 1;
8
+ const VERSION_MARKER = `<!-- seeds-onboard-v:${String(ONBOARD_VERSION)} -->`;
9
+
10
+ const CANDIDATE_FILES = ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md"] as const;
11
+
12
+ function onboardSnippet(): string {
13
+ return `## Issue Tracking (Seeds)
14
+ ${VERSION_MARKER}
15
+
16
+ This project uses [Seeds](https://github.com/jayminwest/seeds) for git-native issue tracking.
17
+
18
+ **At the start of every session**, run:
19
+ \`\`\`
20
+ sd prime
21
+ \`\`\`
22
+
23
+ This injects session context: rules, command reference, and workflows.
24
+
25
+ **Quick reference:**
26
+ - \`sd ready\` — Find unblocked work
27
+ - \`sd create --title "..." --type task --priority 2\` — Create issue
28
+ - \`sd update <id> --status in_progress\` — Claim work
29
+ - \`sd close <id>\` — Complete work
30
+ - \`sd sync\` — Sync with git (run before pushing)
31
+
32
+ ### Before You Finish
33
+ 1. Close completed issues: \`sd close <id>\`
34
+ 2. File issues for remaining work: \`sd create --title "..."\`
35
+ 3. Sync and push: \`sd sync && git push\``;
36
+ }
37
+
38
+ function findTargetFile(projectRoot: string): string | null {
39
+ for (const candidate of CANDIDATE_FILES) {
40
+ const fullPath = join(projectRoot, candidate);
41
+ if (existsSync(fullPath)) {
42
+ return fullPath;
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function detectStatus(content: string): "missing" | "current" | "outdated" {
49
+ if (!hasMarkerSection(content)) return "missing";
50
+ if (content.includes(VERSION_MARKER)) return "current";
51
+ return "outdated";
52
+ }
53
+
54
+ export async function run(args: string[]): Promise<void> {
55
+ const jsonMode = args.includes("--json");
56
+ const stdoutMode = args.includes("--stdout");
57
+ const checkMode = args.includes("--check");
58
+
59
+ const seedsDir = await findSeedsDir();
60
+ const projectRoot = projectRootFromSeedsDir(seedsDir);
61
+
62
+ const targetPath = findTargetFile(projectRoot);
63
+ const snippet = onboardSnippet();
64
+
65
+ // --check mode: report status only
66
+ if (checkMode) {
67
+ if (!targetPath) {
68
+ if (jsonMode) {
69
+ outputJson({ success: true, command: "onboard", status: "missing", file: null });
70
+ } else {
71
+ console.log("Status: missing (no CLAUDE.md found)");
72
+ }
73
+ return;
74
+ }
75
+ const content = await Bun.file(targetPath).text();
76
+ const status = detectStatus(content);
77
+ if (jsonMode) {
78
+ outputJson({ success: true, command: "onboard", status, file: targetPath });
79
+ } else {
80
+ console.log(`Status: ${status} (${targetPath})`);
81
+ }
82
+ return;
83
+ }
84
+
85
+ // --stdout mode: print what would be written
86
+ if (stdoutMode) {
87
+ process.stdout.write(wrapInMarkers(snippet));
88
+ process.stdout.write("\n");
89
+ return;
90
+ }
91
+
92
+ // Default mode: write to file
93
+ const filePath = targetPath ?? join(projectRoot, "CLAUDE.md");
94
+ const fileExists = existsSync(filePath);
95
+ const wrappedSnippet = wrapInMarkers(snippet);
96
+
97
+ if (!fileExists) {
98
+ await Bun.write(filePath, `${wrappedSnippet}\n`);
99
+ if (jsonMode) {
100
+ outputJson({ success: true, command: "onboard", action: "created", file: filePath });
101
+ } else {
102
+ printSuccess(`Created ${filePath} with seeds section`);
103
+ }
104
+ return;
105
+ }
106
+
107
+ const content = await Bun.file(filePath).text();
108
+ const status = detectStatus(content);
109
+
110
+ if (status === "current") {
111
+ if (jsonMode) {
112
+ outputJson({ success: true, command: "onboard", action: "unchanged", file: filePath });
113
+ } else {
114
+ printSuccess("Seeds section is already up to date");
115
+ }
116
+ return;
117
+ }
118
+
119
+ if (status === "outdated") {
120
+ const updated = replaceMarkerSection(content, snippet);
121
+ if (updated) {
122
+ await Bun.write(filePath, updated);
123
+ if (jsonMode) {
124
+ outputJson({ success: true, command: "onboard", action: "updated", file: filePath });
125
+ } else {
126
+ printSuccess(`Updated seeds section in ${filePath}`);
127
+ }
128
+ }
129
+ return;
130
+ }
131
+
132
+ // status === "missing": append
133
+ const separator = content.endsWith("\n") ? "\n" : "\n\n";
134
+ await Bun.write(filePath, `${content}${separator}${wrappedSnippet}\n`);
135
+ if (jsonMode) {
136
+ outputJson({ success: true, command: "onboard", action: "appended", file: filePath });
137
+ } else {
138
+ printSuccess(`Added seeds section to ${filePath}`);
139
+ }
140
+ }