@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,94 @@
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-prime-test-"));
31
+ });
32
+
33
+ afterEach(async () => {
34
+ await rm(tmpDir, { recursive: true, force: true });
35
+ });
36
+
37
+ describe("sd prime", () => {
38
+ test("outputs full prime content without .seeds/ initialized", async () => {
39
+ const { stdout, exitCode } = await run(["prime"], tmpDir);
40
+ expect(exitCode).toBe(0);
41
+ expect(stdout).toContain("Seeds Workflow Context");
42
+ expect(stdout).toContain("Session Close Protocol");
43
+ expect(stdout).toContain("sd ready");
44
+ });
45
+
46
+ test("outputs compact content with --compact", async () => {
47
+ const { stdout, exitCode } = await run(["prime", "--compact"], tmpDir);
48
+ expect(exitCode).toBe(0);
49
+ expect(stdout).toContain("Seeds Quick Reference");
50
+ expect(stdout).not.toContain("Session Close Protocol");
51
+ });
52
+
53
+ test("outputs JSON with --json", async () => {
54
+ const { stdout, exitCode } = await run(["prime", "--json"], tmpDir);
55
+ expect(exitCode).toBe(0);
56
+ const result = JSON.parse(stdout) as { success: boolean; command: string; content: string };
57
+ expect(result.success).toBe(true);
58
+ expect(result.command).toBe("prime");
59
+ expect(result.content).toContain("Seeds Workflow Context");
60
+ });
61
+
62
+ test("--export outputs default template even with custom PRIME.md", async () => {
63
+ await initSeeds(tmpDir);
64
+ await Bun.write(join(tmpDir, ".seeds", "PRIME.md"), "custom prime content");
65
+ const { stdout, exitCode } = await run(["prime", "--export"], tmpDir);
66
+ expect(exitCode).toBe(0);
67
+ expect(stdout).toContain("Seeds Workflow Context");
68
+ expect(stdout).not.toContain("custom prime content");
69
+ });
70
+
71
+ test("uses custom PRIME.md when present", async () => {
72
+ await initSeeds(tmpDir);
73
+ await Bun.write(join(tmpDir, ".seeds", "PRIME.md"), "my custom agent context");
74
+ const { stdout, exitCode } = await run(["prime"], tmpDir);
75
+ expect(exitCode).toBe(0);
76
+ expect(stdout).toBe("my custom agent context");
77
+ });
78
+
79
+ test("full content includes essential command sections", async () => {
80
+ const { stdout } = await run(["prime"], tmpDir);
81
+ expect(stdout).toContain("Finding Work");
82
+ expect(stdout).toContain("Creating & Updating");
83
+ expect(stdout).toContain("Dependencies & Blocking");
84
+ expect(stdout).toContain("Common Workflows");
85
+ });
86
+
87
+ test("--export with --json returns JSON", async () => {
88
+ const { stdout, exitCode } = await run(["prime", "--export", "--json"], tmpDir);
89
+ expect(exitCode).toBe(0);
90
+ const result = JSON.parse(stdout) as { success: boolean; content: string };
91
+ expect(result.success).toBe(true);
92
+ expect(result.content).toContain("Seeds Workflow Context");
93
+ });
94
+ });
@@ -0,0 +1,146 @@
1
+ import { join } from "node:path";
2
+ import { findSeedsDir } from "../config.ts";
3
+ import { outputJson } from "../output.ts";
4
+
5
+ const PRIME_FILE = "PRIME.md";
6
+
7
+ function defaultPrimeContent(compact: boolean): string {
8
+ if (compact) {
9
+ return compactContent();
10
+ }
11
+ return fullContent();
12
+ }
13
+
14
+ function compactContent(): string {
15
+ return `# Seeds Quick Reference
16
+
17
+ \`\`\`
18
+ sd ready # Find unblocked work
19
+ sd show <id> # View issue details
20
+ sd create --title "..." # Create issue (--type, --priority)
21
+ sd update <id> --status in_progress # Claim work
22
+ sd close <id> # Complete work
23
+ sd dep add <a> <b> # a depends on b
24
+ sd blocked # Show blocked issues
25
+ sd sync # Stage + commit .seeds/
26
+ \`\`\`
27
+
28
+ **Before finishing:** \`sd close <ids> && sd sync && git push\`
29
+ `;
30
+ }
31
+
32
+ function fullContent(): string {
33
+ return `# Seeds Workflow Context
34
+
35
+ > **Context Recovery**: Run \`sd prime\` after compaction, clear, or new session
36
+
37
+ # Session Close Protocol
38
+
39
+ **CRITICAL**: Before saying "done" or "complete", you MUST run this checklist:
40
+
41
+ \`\`\`
42
+ [ ] 1. Close completed issues: sd close <id1> <id2> ...
43
+ [ ] 2. File issues for remaining: sd create --title "..."
44
+ [ ] 3. Run quality gates: bun test && bun run lint && bun run typecheck
45
+ [ ] 4. Sync and push: sd sync && git push
46
+ [ ] 5. Verify: git status (must show "up to date with origin")
47
+ \`\`\`
48
+
49
+ **NEVER skip this.** Work is not done until pushed.
50
+
51
+ ## Core Rules
52
+ - **Default**: Use seeds for ALL task tracking (\`sd create\`, \`sd ready\`, \`sd close\`)
53
+ - **Prohibited**: Do NOT use TodoWrite, TaskCreate, or markdown files for task tracking
54
+ - **Workflow**: Create issues BEFORE writing code, mark in_progress when starting
55
+ - Git workflow: run \`sd sync\` at session end
56
+
57
+ ## Essential Commands
58
+
59
+ ### Finding Work
60
+ - \`sd ready\` — Show issues ready to work (no blockers)
61
+ - \`sd list --status=open\` — All open issues
62
+ - \`sd list --status=in_progress\` — Your active work
63
+ - \`sd show <id>\` — Detailed issue view with dependencies
64
+
65
+ ### Creating & Updating
66
+ - \`sd create --title="..." --type=task|bug|feature|epic --priority=2\` — New issue
67
+ - Priority: 0-4 or P0-P4 (0=critical, 2=medium, 4=backlog)
68
+ - \`sd update <id> --status=in_progress\` — Claim work
69
+ - \`sd update <id> --assignee=username\` — Assign to someone
70
+ - \`sd close <id>\` — Mark complete
71
+ - \`sd close <id1> <id2> ...\` — Close multiple issues at once
72
+
73
+ ### Dependencies & Blocking
74
+ - \`sd dep add <issue> <depends-on>\` — Add dependency
75
+ - \`sd dep remove <issue> <depends-on>\` — Remove dependency
76
+ - \`sd blocked\` — Show all blocked issues
77
+
78
+ ### Sync & Project Health
79
+ - \`sd sync\` — Stage and commit .seeds/ changes
80
+ - \`sd sync --status\` — Check without committing
81
+ - \`sd stats\` — Project statistics
82
+ - \`sd doctor\` — Check for data integrity issues
83
+
84
+ ## Common Workflows
85
+
86
+ **Starting work:**
87
+ \`\`\`bash
88
+ sd ready # Find available work
89
+ sd show <id> # Review issue details
90
+ sd update <id> --status=in_progress # Claim it
91
+ \`\`\`
92
+
93
+ **Completing work:**
94
+ \`\`\`bash
95
+ sd close <id1> <id2> ... # Close all completed issues at once
96
+ sd sync # Stage + commit .seeds/
97
+ git push # Push to remote
98
+ \`\`\`
99
+
100
+ **Creating dependent work:**
101
+ \`\`\`bash
102
+ sd create --title="Implement feature X" --type=feature
103
+ sd create --title="Write tests for X" --type=task
104
+ sd dep add <test-id> <feature-id> # Tests depend on feature
105
+ \`\`\`
106
+ `;
107
+ }
108
+
109
+ export async function run(args: string[]): Promise<void> {
110
+ const jsonMode = args.includes("--json");
111
+ const compact = args.includes("--compact");
112
+ const exportMode = args.includes("--export");
113
+
114
+ // --export always outputs the default template
115
+ if (exportMode) {
116
+ const content = defaultPrimeContent(false);
117
+ if (jsonMode) {
118
+ outputJson({ success: true, command: "prime", content });
119
+ } else {
120
+ process.stdout.write(content);
121
+ }
122
+ return;
123
+ }
124
+
125
+ // Try to find seeds dir for custom PRIME.md
126
+ let content: string | null = null;
127
+ try {
128
+ const seedsDir = await findSeedsDir();
129
+ const customFile = Bun.file(join(seedsDir, PRIME_FILE));
130
+ if (await customFile.exists()) {
131
+ content = await customFile.text();
132
+ }
133
+ } catch {
134
+ // No seeds dir — that's fine, use default
135
+ }
136
+
137
+ if (!content) {
138
+ content = defaultPrimeContent(compact);
139
+ }
140
+
141
+ if (jsonMode) {
142
+ outputJson({ success: true, command: "prime", content });
143
+ } else {
144
+ process.stdout.write(content);
145
+ }
146
+ }
@@ -0,0 +1,31 @@
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
+ export async function run(args: string[], seedsDir?: string): Promise<void> {
7
+ const jsonMode = args.includes("--json");
8
+ const dir = seedsDir ?? (await findSeedsDir());
9
+ const issues = await readIssues(dir);
10
+
11
+ const closedIds = new Set(issues.filter((i: Issue) => i.status === "closed").map((i) => i.id));
12
+
13
+ const ready = issues.filter((i: Issue) => {
14
+ if (i.status !== "open") return false;
15
+ const blockers = i.blockedBy ?? [];
16
+ return blockers.every((bid) => closedIds.has(bid));
17
+ });
18
+
19
+ if (jsonMode) {
20
+ outputJson({ success: true, command: "ready", issues: ready, count: ready.length });
21
+ } else {
22
+ if (ready.length === 0) {
23
+ console.log("No ready issues.");
24
+ return;
25
+ }
26
+ for (const issue of ready) {
27
+ printIssueOneLine(issue);
28
+ }
29
+ console.log(`\n${ready.length} ready issue(s)`);
30
+ }
31
+ }
@@ -0,0 +1,20 @@
1
+ import { findSeedsDir } from "../config.ts";
2
+ import { outputJson, printIssueFull } from "../output.ts";
3
+ import { readIssues } from "../store.ts";
4
+
5
+ export async function run(args: string[], seedsDir?: string): Promise<void> {
6
+ const jsonMode = args.includes("--json");
7
+ const id = args.find((a) => !a.startsWith("--"));
8
+ if (!id) throw new Error("Usage: sd show <id>");
9
+
10
+ const dir = seedsDir ?? (await findSeedsDir());
11
+ const issues = await readIssues(dir);
12
+ const issue = issues.find((i) => i.id === id);
13
+ if (!issue) throw new Error(`Issue not found: ${id}`);
14
+
15
+ if (jsonMode) {
16
+ outputJson({ success: true, command: "show", issue });
17
+ } else {
18
+ printIssueFull(issue);
19
+ }
20
+ }
@@ -0,0 +1,58 @@
1
+ import { findSeedsDir } from "../config.ts";
2
+ import { c, outputJson } from "../output.ts";
3
+ import { readIssues } from "../store.ts";
4
+ import type { Issue } from "../types.ts";
5
+ import { PRIORITY_LABELS } from "../types.ts";
6
+
7
+ export async function run(args: string[], seedsDir?: string): Promise<void> {
8
+ const jsonMode = args.includes("--json");
9
+ const dir = seedsDir ?? (await findSeedsDir());
10
+ const issues = await readIssues(dir);
11
+
12
+ const total = issues.length;
13
+ const open = issues.filter((i: Issue) => i.status === "open").length;
14
+ const inProgress = issues.filter((i: Issue) => i.status === "in_progress").length;
15
+ const closed = issues.filter((i: Issue) => i.status === "closed").length;
16
+
17
+ const closedIds = new Set(issues.filter((i: Issue) => i.status === "closed").map((i) => i.id));
18
+ const blocked = issues.filter((i: Issue) => {
19
+ if (i.status === "closed") return false;
20
+ return (i.blockedBy ?? []).some((bid) => !closedIds.has(bid));
21
+ }).length;
22
+
23
+ const byType: Record<string, number> = {};
24
+ for (const issue of issues) {
25
+ byType[issue.type] = (byType[issue.type] ?? 0) + 1;
26
+ }
27
+
28
+ const byPriority: Record<number, number> = {};
29
+ for (const issue of issues) {
30
+ byPriority[issue.priority] = (byPriority[issue.priority] ?? 0) + 1;
31
+ }
32
+
33
+ if (jsonMode) {
34
+ outputJson({
35
+ success: true,
36
+ command: "stats",
37
+ stats: { total, open, inProgress, closed, blocked, byType, byPriority },
38
+ });
39
+ } else {
40
+ console.log(`${c.bold}Project Statistics${c.reset}`);
41
+ console.log(` Total: ${total}`);
42
+ console.log(` Open: ${open}`);
43
+ console.log(` In progress: ${inProgress}`);
44
+ console.log(` Closed: ${closed}`);
45
+ console.log(` Blocked: ${blocked}`);
46
+ console.log(`\n${c.bold}By Type${c.reset}`);
47
+ for (const [type, count] of Object.entries(byType)) {
48
+ console.log(` ${type.padEnd(10)} ${count}`);
49
+ }
50
+ if (Object.keys(byPriority).length > 0) {
51
+ console.log(`\n${c.bold}By Priority${c.reset}`);
52
+ for (const [p, count] of Object.entries(byPriority)) {
53
+ const label = PRIORITY_LABELS[Number(p)] ?? String(p);
54
+ console.log(` P${p} ${label.padEnd(10)} ${count}`);
55
+ }
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,76 @@
1
+ import { findSeedsDir, projectRootFromSeedsDir } from "../config.ts";
2
+ import { outputJson } from "../output.ts";
3
+ import { SEEDS_DIR_NAME } from "../types.ts";
4
+
5
+ function spawnSync(
6
+ cmd: string[],
7
+ cwd: string,
8
+ ): { stdout: string; stderr: string; exitCode: number } {
9
+ const result = Bun.spawnSync(cmd, { cwd, stdout: "pipe", stderr: "pipe" });
10
+ const stdout = new TextDecoder().decode(result.stdout);
11
+ const stderr = new TextDecoder().decode(result.stderr);
12
+ return { stdout, stderr, exitCode: result.exitCode ?? 0 };
13
+ }
14
+
15
+ export async function run(args: string[], seedsDir?: string): Promise<void> {
16
+ const jsonMode = args.includes("--json");
17
+ const statusOnly = args.includes("--status");
18
+
19
+ const dir = seedsDir ?? (await findSeedsDir());
20
+ const projectRoot = projectRootFromSeedsDir(dir);
21
+
22
+ const statusResult = spawnSync(
23
+ ["git", "-C", projectRoot, "status", "--porcelain", `${SEEDS_DIR_NAME}/`],
24
+ projectRoot,
25
+ );
26
+
27
+ const changed = statusResult.stdout.trim();
28
+
29
+ if (statusOnly) {
30
+ if (jsonMode) {
31
+ outputJson({ success: true, command: "sync", hasChanges: !!changed, changes: changed });
32
+ } else {
33
+ if (changed) {
34
+ console.log("Uncommitted .seeds/ changes:");
35
+ console.log(changed);
36
+ } else {
37
+ console.log("No uncommitted .seeds/ changes.");
38
+ }
39
+ }
40
+ return;
41
+ }
42
+
43
+ if (!changed) {
44
+ if (jsonMode) {
45
+ outputJson({
46
+ success: true,
47
+ command: "sync",
48
+ committed: false,
49
+ message: "Nothing to commit",
50
+ });
51
+ } else {
52
+ console.log("No changes to commit.");
53
+ }
54
+ return;
55
+ }
56
+
57
+ // Stage
58
+ const addResult = spawnSync(["git", "-C", projectRoot, "add", `${SEEDS_DIR_NAME}/`], projectRoot);
59
+ if (addResult.exitCode !== 0) {
60
+ throw new Error(`git add failed: ${addResult.stderr}`);
61
+ }
62
+
63
+ // Commit
64
+ const date = new Date().toISOString().slice(0, 10);
65
+ const msg = `seeds: sync ${date}`;
66
+ const commitResult = spawnSync(["git", "-C", projectRoot, "commit", "-m", msg], projectRoot);
67
+ if (commitResult.exitCode !== 0) {
68
+ throw new Error(`git commit failed: ${commitResult.stderr}`);
69
+ }
70
+
71
+ if (jsonMode) {
72
+ outputJson({ success: true, command: "sync", committed: true, message: msg });
73
+ } else {
74
+ console.log(`Committed: ${msg}`);
75
+ }
76
+ }