@os-eco/overstory-cli 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Database integrity checks.
|
|
7
|
+
* Validates SQLite databases (mail.db, metrics.db, sessions.db) exist and have correct schema.
|
|
8
|
+
*/
|
|
9
|
+
export const checkDatabases: DoctorCheckFn = (_config, overstoryDir): DoctorCheck[] => {
|
|
10
|
+
const checks: DoctorCheck[] = [];
|
|
11
|
+
|
|
12
|
+
// Define expected databases and their required tables
|
|
13
|
+
const databases = [
|
|
14
|
+
{
|
|
15
|
+
name: "mail.db",
|
|
16
|
+
tables: ["messages"],
|
|
17
|
+
requiredColumns: {
|
|
18
|
+
messages: [
|
|
19
|
+
"id",
|
|
20
|
+
"from_agent",
|
|
21
|
+
"to_agent",
|
|
22
|
+
"subject",
|
|
23
|
+
"body",
|
|
24
|
+
"type",
|
|
25
|
+
"priority",
|
|
26
|
+
"thread_id",
|
|
27
|
+
"payload",
|
|
28
|
+
"read",
|
|
29
|
+
"created_at",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "metrics.db",
|
|
35
|
+
tables: ["sessions"],
|
|
36
|
+
requiredColumns: {
|
|
37
|
+
sessions: [
|
|
38
|
+
"agent_name",
|
|
39
|
+
"task_id",
|
|
40
|
+
"capability",
|
|
41
|
+
"started_at",
|
|
42
|
+
"completed_at",
|
|
43
|
+
"duration_ms",
|
|
44
|
+
"exit_code",
|
|
45
|
+
"merge_result",
|
|
46
|
+
"parent_agent",
|
|
47
|
+
"input_tokens",
|
|
48
|
+
"output_tokens",
|
|
49
|
+
"cache_read_tokens",
|
|
50
|
+
"cache_creation_tokens",
|
|
51
|
+
"estimated_cost_usd",
|
|
52
|
+
"model_used",
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "sessions.db",
|
|
58
|
+
tables: ["sessions", "runs"],
|
|
59
|
+
requiredColumns: {
|
|
60
|
+
sessions: [
|
|
61
|
+
"id",
|
|
62
|
+
"agent_name",
|
|
63
|
+
"capability",
|
|
64
|
+
"worktree_path",
|
|
65
|
+
"branch_name",
|
|
66
|
+
"task_id",
|
|
67
|
+
"tmux_session",
|
|
68
|
+
"state",
|
|
69
|
+
"pid",
|
|
70
|
+
"parent_agent",
|
|
71
|
+
"depth",
|
|
72
|
+
"run_id",
|
|
73
|
+
"started_at",
|
|
74
|
+
"last_activity",
|
|
75
|
+
"escalation_level",
|
|
76
|
+
"stalled_since",
|
|
77
|
+
],
|
|
78
|
+
runs: [
|
|
79
|
+
"id",
|
|
80
|
+
"started_at",
|
|
81
|
+
"completed_at",
|
|
82
|
+
"agent_count",
|
|
83
|
+
"coordinator_session_id",
|
|
84
|
+
"status",
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const dbSpec of databases) {
|
|
91
|
+
const dbPath = join(overstoryDir, dbSpec.name);
|
|
92
|
+
|
|
93
|
+
// Check if database file exists
|
|
94
|
+
if (!existsSync(dbPath)) {
|
|
95
|
+
checks.push({
|
|
96
|
+
name: `${dbSpec.name} exists`,
|
|
97
|
+
category: "databases",
|
|
98
|
+
status: "fail",
|
|
99
|
+
message: `Database file ${dbSpec.name} does not exist`,
|
|
100
|
+
details: [`Expected at: ${dbPath}`],
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Try to open the database
|
|
106
|
+
let db: Database | null = null;
|
|
107
|
+
try {
|
|
108
|
+
db = new Database(dbPath);
|
|
109
|
+
|
|
110
|
+
// Check WAL mode is enabled
|
|
111
|
+
const journalMode = db.prepare<{ journal_mode: string }, []>("PRAGMA journal_mode").get();
|
|
112
|
+
const walEnabled = journalMode?.journal_mode?.toLowerCase() === "wal";
|
|
113
|
+
|
|
114
|
+
// Check for required tables
|
|
115
|
+
const missingTables: string[] = [];
|
|
116
|
+
const schemaIssues: string[] = [];
|
|
117
|
+
|
|
118
|
+
for (const tableName of dbSpec.tables) {
|
|
119
|
+
const tableExists = db
|
|
120
|
+
.prepare<{ count: number }, [string]>(
|
|
121
|
+
"SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name=?",
|
|
122
|
+
)
|
|
123
|
+
.get(tableName);
|
|
124
|
+
|
|
125
|
+
if (!tableExists || tableExists.count === 0) {
|
|
126
|
+
missingTables.push(tableName);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check columns if table exists
|
|
131
|
+
const requiredCols =
|
|
132
|
+
dbSpec.requiredColumns[tableName as keyof typeof dbSpec.requiredColumns];
|
|
133
|
+
if (requiredCols) {
|
|
134
|
+
const columns = db.prepare<{ name: string }, []>(`PRAGMA table_info(${tableName})`).all();
|
|
135
|
+
const existingCols = new Set(columns.map((c) => c.name));
|
|
136
|
+
|
|
137
|
+
for (const reqCol of requiredCols) {
|
|
138
|
+
if (!existingCols.has(reqCol)) {
|
|
139
|
+
schemaIssues.push(`Table ${tableName} missing column: ${reqCol}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Determine check status
|
|
146
|
+
if (missingTables.length > 0 || schemaIssues.length > 0) {
|
|
147
|
+
const details: string[] = [];
|
|
148
|
+
if (missingTables.length > 0) {
|
|
149
|
+
details.push(`Missing tables: ${missingTables.join(", ")}`);
|
|
150
|
+
}
|
|
151
|
+
if (schemaIssues.length > 0) {
|
|
152
|
+
details.push(...schemaIssues);
|
|
153
|
+
}
|
|
154
|
+
if (!walEnabled) {
|
|
155
|
+
details.push("WAL mode not enabled");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
checks.push({
|
|
159
|
+
name: `${dbSpec.name} schema`,
|
|
160
|
+
category: "databases",
|
|
161
|
+
status: "fail",
|
|
162
|
+
message: `Database ${dbSpec.name} has schema issues`,
|
|
163
|
+
details,
|
|
164
|
+
fixable: true,
|
|
165
|
+
});
|
|
166
|
+
} else if (!walEnabled) {
|
|
167
|
+
checks.push({
|
|
168
|
+
name: `${dbSpec.name} WAL mode`,
|
|
169
|
+
category: "databases",
|
|
170
|
+
status: "warn",
|
|
171
|
+
message: `Database ${dbSpec.name} is not using WAL mode`,
|
|
172
|
+
details: ["WAL mode improves concurrent access performance"],
|
|
173
|
+
fixable: true,
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
checks.push({
|
|
177
|
+
name: `${dbSpec.name} health`,
|
|
178
|
+
category: "databases",
|
|
179
|
+
status: "pass",
|
|
180
|
+
message: `Database ${dbSpec.name} is healthy`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
db.close();
|
|
185
|
+
} catch (err) {
|
|
186
|
+
if (db) {
|
|
187
|
+
try {
|
|
188
|
+
db.close();
|
|
189
|
+
} catch {
|
|
190
|
+
// Ignore close errors
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
checks.push({
|
|
195
|
+
name: `${dbSpec.name} integrity`,
|
|
196
|
+
category: "databases",
|
|
197
|
+
status: "fail",
|
|
198
|
+
message: `Failed to open or validate ${dbSpec.name}`,
|
|
199
|
+
details: [
|
|
200
|
+
err instanceof Error ? err.message : String(err),
|
|
201
|
+
"Database may be corrupted or locked",
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return checks;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** Helper to check if file exists (synchronous). */
|
|
211
|
+
function existsSync(path: string): boolean {
|
|
212
|
+
try {
|
|
213
|
+
const { existsSync } = require("node:fs");
|
|
214
|
+
return existsSync(path);
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { OverstoryConfig } from "../types.ts";
|
|
3
|
+
import { checkDependencies } from "./dependencies.ts";
|
|
4
|
+
|
|
5
|
+
// Minimal config for testing
|
|
6
|
+
const mockConfig: OverstoryConfig = {
|
|
7
|
+
project: {
|
|
8
|
+
name: "test-project",
|
|
9
|
+
root: "/tmp/test",
|
|
10
|
+
canonicalBranch: "main",
|
|
11
|
+
},
|
|
12
|
+
agents: {
|
|
13
|
+
manifestPath: "/tmp/.overstory/agent-manifest.json",
|
|
14
|
+
baseDir: "/tmp/.overstory/agents",
|
|
15
|
+
maxConcurrent: 5,
|
|
16
|
+
staggerDelayMs: 1000,
|
|
17
|
+
maxDepth: 2,
|
|
18
|
+
maxSessionsPerRun: 0,
|
|
19
|
+
},
|
|
20
|
+
worktrees: {
|
|
21
|
+
baseDir: "/tmp/.overstory/worktrees",
|
|
22
|
+
},
|
|
23
|
+
taskTracker: {
|
|
24
|
+
backend: "auto",
|
|
25
|
+
enabled: false,
|
|
26
|
+
},
|
|
27
|
+
mulch: {
|
|
28
|
+
enabled: false,
|
|
29
|
+
domains: [],
|
|
30
|
+
primeFormat: "markdown",
|
|
31
|
+
},
|
|
32
|
+
merge: {
|
|
33
|
+
aiResolveEnabled: false,
|
|
34
|
+
reimagineEnabled: false,
|
|
35
|
+
},
|
|
36
|
+
providers: {
|
|
37
|
+
anthropic: { type: "native" },
|
|
38
|
+
},
|
|
39
|
+
watchdog: {
|
|
40
|
+
tier0Enabled: false,
|
|
41
|
+
tier0IntervalMs: 30000,
|
|
42
|
+
tier1Enabled: false,
|
|
43
|
+
tier2Enabled: false,
|
|
44
|
+
staleThresholdMs: 300000,
|
|
45
|
+
zombieThresholdMs: 600000,
|
|
46
|
+
nudgeIntervalMs: 60000,
|
|
47
|
+
},
|
|
48
|
+
models: {},
|
|
49
|
+
logging: {
|
|
50
|
+
verbose: false,
|
|
51
|
+
redactSecrets: true,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe("checkDependencies", () => {
|
|
56
|
+
test("returns checks for all required tools", async () => {
|
|
57
|
+
const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
|
|
58
|
+
|
|
59
|
+
expect(checks).toBeArray();
|
|
60
|
+
expect(checks.length).toBeGreaterThanOrEqual(5);
|
|
61
|
+
|
|
62
|
+
// Verify we have checks for each required tool
|
|
63
|
+
const toolNames = checks.map((c) => c.name);
|
|
64
|
+
expect(toolNames).toContain("git availability");
|
|
65
|
+
expect(toolNames).toContain("bun availability");
|
|
66
|
+
expect(toolNames).toContain("tmux availability");
|
|
67
|
+
expect(toolNames).toContain("sd availability");
|
|
68
|
+
expect(toolNames).toContain("mulch availability");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("includes bd CGO support check when bd is available", async () => {
|
|
72
|
+
const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
|
|
73
|
+
|
|
74
|
+
const bdCheck = checks.find((c) => c.name === "bd availability");
|
|
75
|
+
if (bdCheck?.status === "pass") {
|
|
76
|
+
const cgoCheck = checks.find((c) => c.name === "bd CGO support");
|
|
77
|
+
expect(cgoCheck).toBeDefined();
|
|
78
|
+
expect(cgoCheck?.category).toBe("dependencies");
|
|
79
|
+
expect(["pass", "warn", "fail"]).toContain(cgoCheck?.status ?? "");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("all checks have required DoctorCheck fields", async () => {
|
|
84
|
+
const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
|
|
85
|
+
|
|
86
|
+
for (const check of checks) {
|
|
87
|
+
expect(check).toHaveProperty("name");
|
|
88
|
+
expect(check).toHaveProperty("category");
|
|
89
|
+
expect(check).toHaveProperty("status");
|
|
90
|
+
expect(check).toHaveProperty("message");
|
|
91
|
+
|
|
92
|
+
expect(check.category).toBe("dependencies");
|
|
93
|
+
expect(["pass", "warn", "fail"]).toContain(check.status);
|
|
94
|
+
expect(typeof check.name).toBe("string");
|
|
95
|
+
expect(typeof check.message).toBe("string");
|
|
96
|
+
|
|
97
|
+
if (check.details !== undefined) {
|
|
98
|
+
expect(check.details).toBeArray();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (check.fixable !== undefined) {
|
|
102
|
+
expect(typeof check.fixable).toBe("boolean");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("checks for commonly available tools should pass", async () => {
|
|
108
|
+
const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
|
|
109
|
+
|
|
110
|
+
// git and bun should definitely be available in this environment
|
|
111
|
+
const gitCheck = checks.find((c) => c.name === "git availability");
|
|
112
|
+
const bunCheck = checks.find((c) => c.name === "bun availability");
|
|
113
|
+
|
|
114
|
+
expect(gitCheck).toBeDefined();
|
|
115
|
+
expect(bunCheck).toBeDefined();
|
|
116
|
+
|
|
117
|
+
// These should pass in a normal development environment
|
|
118
|
+
expect(gitCheck?.status).toBe("pass");
|
|
119
|
+
expect(bunCheck?.status).toBe("pass");
|
|
120
|
+
|
|
121
|
+
// Passing checks should include version info
|
|
122
|
+
if (gitCheck?.status === "pass") {
|
|
123
|
+
expect(gitCheck.details).toBeArray();
|
|
124
|
+
expect(gitCheck.details?.length).toBeGreaterThan(0);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("checks include version details for available tools", async () => {
|
|
129
|
+
const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
|
|
130
|
+
|
|
131
|
+
const passingChecks = checks.filter((c) => c.status === "pass");
|
|
132
|
+
|
|
133
|
+
for (const check of passingChecks) {
|
|
134
|
+
expect(check.details).toBeDefined();
|
|
135
|
+
expect(check.details).toBeArray();
|
|
136
|
+
expect(check.details?.length).toBeGreaterThan(0);
|
|
137
|
+
|
|
138
|
+
// Version string should not be empty
|
|
139
|
+
const version = check.details?.[0];
|
|
140
|
+
expect(version).toBeDefined();
|
|
141
|
+
expect(typeof version).toBe("string");
|
|
142
|
+
expect(version?.length).toBeGreaterThan(0);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("failing checks are marked as fixable", async () => {
|
|
147
|
+
const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
|
|
148
|
+
|
|
149
|
+
const failingChecks = checks.filter((c) => c.status === "fail" || c.status === "warn");
|
|
150
|
+
|
|
151
|
+
// If there are any failing checks, they should be marked fixable
|
|
152
|
+
for (const check of failingChecks) {
|
|
153
|
+
expect(check.fixable).toBe(true);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("checks for sd when backend is seeds", async () => {
|
|
158
|
+
const seedsConfig: typeof mockConfig = {
|
|
159
|
+
...mockConfig,
|
|
160
|
+
taskTracker: { backend: "seeds", enabled: true },
|
|
161
|
+
};
|
|
162
|
+
const checks = await checkDependencies(seedsConfig, "/tmp/.overstory");
|
|
163
|
+
const toolNames = checks.map((c) => c.name);
|
|
164
|
+
expect(toolNames).toContain("sd availability");
|
|
165
|
+
expect(toolNames).not.toContain("bd availability");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("checks for sd when backend is auto (seeds is default)", async () => {
|
|
169
|
+
const checks = await checkDependencies(mockConfig, "/tmp/.overstory");
|
|
170
|
+
const toolNames = checks.map((c) => c.name);
|
|
171
|
+
expect(toolNames).toContain("sd availability");
|
|
172
|
+
expect(toolNames).not.toContain("bd availability");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("skips bd CGO check when backend is seeds", async () => {
|
|
176
|
+
const seedsConfig: typeof mockConfig = {
|
|
177
|
+
...mockConfig,
|
|
178
|
+
taskTracker: { backend: "seeds", enabled: true },
|
|
179
|
+
};
|
|
180
|
+
const checks = await checkDependencies(seedsConfig, "/tmp/.overstory");
|
|
181
|
+
const cgoCheck = checks.find((c) => c.name === "bd CGO support");
|
|
182
|
+
expect(cgoCheck).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { resolveBackend, trackerCliName } from "../tracker/factory.ts";
|
|
2
|
+
import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* External dependency checks.
|
|
6
|
+
* Validates that required CLI tools (git, bun, tmux, bd, mulch) are available
|
|
7
|
+
* and that bd has functional CGO support for its Dolt database backend.
|
|
8
|
+
*/
|
|
9
|
+
export const checkDependencies: DoctorCheckFn = async (
|
|
10
|
+
config,
|
|
11
|
+
_overstoryDir,
|
|
12
|
+
): Promise<DoctorCheck[]> => {
|
|
13
|
+
// Determine which tracker CLI to check based on config backend (resolve "auto")
|
|
14
|
+
const resolvedBackend = await resolveBackend(config.taskTracker.backend, config.project.root);
|
|
15
|
+
const trackerTool = {
|
|
16
|
+
name: trackerCliName(resolvedBackend),
|
|
17
|
+
versionFlag: "--version",
|
|
18
|
+
required: true,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const requiredTools = [
|
|
22
|
+
{ name: "git", versionFlag: "--version", required: true },
|
|
23
|
+
{ name: "bun", versionFlag: "--version", required: true },
|
|
24
|
+
{ name: "tmux", versionFlag: "-V", required: true },
|
|
25
|
+
trackerTool,
|
|
26
|
+
{ name: "mulch", versionFlag: "--version", required: true },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const checks: DoctorCheck[] = [];
|
|
30
|
+
|
|
31
|
+
for (const tool of requiredTools) {
|
|
32
|
+
const check = await checkTool(tool.name, tool.versionFlag, tool.required);
|
|
33
|
+
checks.push(check);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// If bd is available, probe for CGO/Dolt backend functionality.
|
|
37
|
+
// Only run for beads backend (CGO check is beads-specific).
|
|
38
|
+
if (trackerTool.name === "bd") {
|
|
39
|
+
const bdCheck = checks.find((c) => c.name === "bd availability");
|
|
40
|
+
if (bdCheck?.status === "pass") {
|
|
41
|
+
const cgoCheck = await checkBdCgoSupport();
|
|
42
|
+
checks.push(cgoCheck);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return checks;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Probe whether bd's Dolt database backend is functional.
|
|
51
|
+
* The npm-distributed bd binary may be built without CGO, which causes
|
|
52
|
+
* `bd init` and all database operations to fail even though `bd --version` succeeds.
|
|
53
|
+
* We detect this by running `bd status` in a temp directory and checking for
|
|
54
|
+
* the characteristic "without CGO support" error message.
|
|
55
|
+
*/
|
|
56
|
+
async function checkBdCgoSupport(): Promise<DoctorCheck> {
|
|
57
|
+
const { mkdtemp, rm } = await import("node:fs/promises");
|
|
58
|
+
const { join } = await import("node:path");
|
|
59
|
+
const { tmpdir } = await import("node:os");
|
|
60
|
+
|
|
61
|
+
let tempDir: string | undefined;
|
|
62
|
+
try {
|
|
63
|
+
tempDir = await mkdtemp(join(tmpdir(), "overstory-bd-cgo-"));
|
|
64
|
+
const proc = Bun.spawn(["bd", "status"], {
|
|
65
|
+
cwd: tempDir,
|
|
66
|
+
stdout: "pipe",
|
|
67
|
+
stderr: "pipe",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const exitCode = await proc.exited;
|
|
71
|
+
const stderr = await new Response(proc.stderr).text();
|
|
72
|
+
|
|
73
|
+
if (stderr.includes("without CGO support")) {
|
|
74
|
+
return {
|
|
75
|
+
name: "bd CGO support",
|
|
76
|
+
category: "dependencies",
|
|
77
|
+
status: "fail",
|
|
78
|
+
message: "bd binary was built without CGO — Dolt database operations will fail",
|
|
79
|
+
details: [
|
|
80
|
+
"The installed bd binary lacks CGO support required by its Dolt backend.",
|
|
81
|
+
"Workaround: rebuild bd from source with CGO_ENABLED=1 and ICU headers.",
|
|
82
|
+
"See: https://github.com/jayminwest/overstory/issues/10",
|
|
83
|
+
],
|
|
84
|
+
fixable: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Any other exit code is fine — bd status may fail for other reasons
|
|
89
|
+
// (no .beads/ dir, etc.) but those aren't CGO issues
|
|
90
|
+
if (exitCode === 0 || !stderr.includes("CGO")) {
|
|
91
|
+
return {
|
|
92
|
+
name: "bd CGO support",
|
|
93
|
+
category: "dependencies",
|
|
94
|
+
status: "pass",
|
|
95
|
+
message: "bd has functional database backend",
|
|
96
|
+
details: ["Dolt backend operational"],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
name: "bd CGO support",
|
|
102
|
+
category: "dependencies",
|
|
103
|
+
status: "warn",
|
|
104
|
+
message: `bd status returned unexpected error (exit code ${exitCode})`,
|
|
105
|
+
details: [stderr.trim().split("\n")[0] || "unknown error"],
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
name: "bd CGO support",
|
|
110
|
+
category: "dependencies",
|
|
111
|
+
status: "warn",
|
|
112
|
+
message: "Could not verify bd CGO support",
|
|
113
|
+
details: [error instanceof Error ? error.message : String(error)],
|
|
114
|
+
};
|
|
115
|
+
} finally {
|
|
116
|
+
if (tempDir) {
|
|
117
|
+
await rm(tempDir, { recursive: true }).catch(() => {});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if a CLI tool is available by attempting to run it with a version flag.
|
|
124
|
+
*/
|
|
125
|
+
async function checkTool(
|
|
126
|
+
name: string,
|
|
127
|
+
versionFlag: string,
|
|
128
|
+
required: boolean,
|
|
129
|
+
): Promise<DoctorCheck> {
|
|
130
|
+
try {
|
|
131
|
+
const proc = Bun.spawn([name, versionFlag], {
|
|
132
|
+
stdout: "pipe",
|
|
133
|
+
stderr: "pipe",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const exitCode = await proc.exited;
|
|
137
|
+
|
|
138
|
+
if (exitCode === 0) {
|
|
139
|
+
const stdout = await new Response(proc.stdout).text();
|
|
140
|
+
const version = stdout.split("\n")[0]?.trim() || "version unknown";
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
name: `${name} availability`,
|
|
144
|
+
category: "dependencies",
|
|
145
|
+
status: "pass",
|
|
146
|
+
message: `${name} is available`,
|
|
147
|
+
details: [version],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Non-zero exit code
|
|
152
|
+
const stderr = await new Response(proc.stderr).text();
|
|
153
|
+
return {
|
|
154
|
+
name: `${name} availability`,
|
|
155
|
+
category: "dependencies",
|
|
156
|
+
status: required ? "fail" : "warn",
|
|
157
|
+
message: `${name} command failed (exit code ${exitCode})`,
|
|
158
|
+
details: stderr ? [stderr.trim()] : undefined,
|
|
159
|
+
fixable: true,
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Command not found or spawn failed
|
|
163
|
+
return {
|
|
164
|
+
name: `${name} availability`,
|
|
165
|
+
category: "dependencies",
|
|
166
|
+
status: required ? "fail" : "warn",
|
|
167
|
+
message: `${name} is not installed or not in PATH`,
|
|
168
|
+
details: [
|
|
169
|
+
`Install ${name} or ensure it is in your PATH`,
|
|
170
|
+
error instanceof Error ? error.message : String(error),
|
|
171
|
+
],
|
|
172
|
+
fixable: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|