@katyella/legio 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 (219) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +555 -0
  4. package/agents/builder.md +141 -0
  5. package/agents/coordinator.md +351 -0
  6. package/agents/cto.md +196 -0
  7. package/agents/gateway.md +276 -0
  8. package/agents/lead.md +281 -0
  9. package/agents/merger.md +156 -0
  10. package/agents/monitor.md +212 -0
  11. package/agents/reviewer.md +142 -0
  12. package/agents/scout.md +131 -0
  13. package/agents/supervisor.md +416 -0
  14. package/bin/legio.mjs +38 -0
  15. package/package.json +77 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +102 -0
  18. package/src/agents/hooks-deployer.test.ts +1820 -0
  19. package/src/agents/hooks-deployer.ts +574 -0
  20. package/src/agents/identity.test.ts +614 -0
  21. package/src/agents/identity.ts +385 -0
  22. package/src/agents/lifecycle.test.ts +202 -0
  23. package/src/agents/lifecycle.ts +184 -0
  24. package/src/agents/manifest.test.ts +558 -0
  25. package/src/agents/manifest.ts +297 -0
  26. package/src/agents/overlay.test.ts +592 -0
  27. package/src/agents/overlay.ts +316 -0
  28. package/src/beads/client.test.ts +210 -0
  29. package/src/beads/client.ts +227 -0
  30. package/src/beads/molecules.test.ts +320 -0
  31. package/src/beads/molecules.ts +209 -0
  32. package/src/commands/agents.test.ts +325 -0
  33. package/src/commands/agents.ts +286 -0
  34. package/src/commands/clean.test.ts +730 -0
  35. package/src/commands/clean.ts +653 -0
  36. package/src/commands/completions.test.ts +346 -0
  37. package/src/commands/completions.ts +950 -0
  38. package/src/commands/coordinator.test.ts +1524 -0
  39. package/src/commands/coordinator.ts +880 -0
  40. package/src/commands/costs.test.ts +1015 -0
  41. package/src/commands/costs.ts +473 -0
  42. package/src/commands/dashboard.test.ts +94 -0
  43. package/src/commands/dashboard.ts +607 -0
  44. package/src/commands/doctor.test.ts +295 -0
  45. package/src/commands/doctor.ts +213 -0
  46. package/src/commands/down.test.ts +308 -0
  47. package/src/commands/down.ts +124 -0
  48. package/src/commands/errors.test.ts +648 -0
  49. package/src/commands/errors.ts +255 -0
  50. package/src/commands/feed.test.ts +579 -0
  51. package/src/commands/feed.ts +368 -0
  52. package/src/commands/gateway.test.ts +698 -0
  53. package/src/commands/gateway.ts +419 -0
  54. package/src/commands/group.test.ts +262 -0
  55. package/src/commands/group.ts +539 -0
  56. package/src/commands/hooks.test.ts +292 -0
  57. package/src/commands/hooks.ts +210 -0
  58. package/src/commands/init.test.ts +211 -0
  59. package/src/commands/init.ts +622 -0
  60. package/src/commands/inspect.test.ts +670 -0
  61. package/src/commands/inspect.ts +455 -0
  62. package/src/commands/log.test.ts +1556 -0
  63. package/src/commands/log.ts +752 -0
  64. package/src/commands/logs.test.ts +379 -0
  65. package/src/commands/logs.ts +544 -0
  66. package/src/commands/mail.test.ts +1726 -0
  67. package/src/commands/mail.ts +926 -0
  68. package/src/commands/merge.test.ts +676 -0
  69. package/src/commands/merge.ts +374 -0
  70. package/src/commands/metrics.test.ts +444 -0
  71. package/src/commands/metrics.ts +150 -0
  72. package/src/commands/monitor.test.ts +151 -0
  73. package/src/commands/monitor.ts +394 -0
  74. package/src/commands/nudge.test.ts +230 -0
  75. package/src/commands/nudge.ts +373 -0
  76. package/src/commands/prime.test.ts +467 -0
  77. package/src/commands/prime.ts +386 -0
  78. package/src/commands/replay.test.ts +742 -0
  79. package/src/commands/replay.ts +367 -0
  80. package/src/commands/run.test.ts +443 -0
  81. package/src/commands/run.ts +365 -0
  82. package/src/commands/server.test.ts +626 -0
  83. package/src/commands/server.ts +298 -0
  84. package/src/commands/sling.test.ts +810 -0
  85. package/src/commands/sling.ts +700 -0
  86. package/src/commands/spec.test.ts +206 -0
  87. package/src/commands/spec.ts +171 -0
  88. package/src/commands/status.test.ts +276 -0
  89. package/src/commands/status.ts +339 -0
  90. package/src/commands/stop.test.ts +357 -0
  91. package/src/commands/stop.ts +119 -0
  92. package/src/commands/supervisor.test.ts +186 -0
  93. package/src/commands/supervisor.ts +544 -0
  94. package/src/commands/trace.test.ts +746 -0
  95. package/src/commands/trace.ts +332 -0
  96. package/src/commands/up.test.ts +597 -0
  97. package/src/commands/up.ts +275 -0
  98. package/src/commands/watch.test.ts +152 -0
  99. package/src/commands/watch.ts +238 -0
  100. package/src/commands/worktree.test.ts +648 -0
  101. package/src/commands/worktree.ts +266 -0
  102. package/src/config.test.ts +496 -0
  103. package/src/config.ts +616 -0
  104. package/src/doctor/agents.test.ts +448 -0
  105. package/src/doctor/agents.ts +396 -0
  106. package/src/doctor/config-check.test.ts +184 -0
  107. package/src/doctor/config-check.ts +185 -0
  108. package/src/doctor/consistency.test.ts +645 -0
  109. package/src/doctor/consistency.ts +294 -0
  110. package/src/doctor/databases.test.ts +284 -0
  111. package/src/doctor/databases.ts +211 -0
  112. package/src/doctor/dependencies.test.ts +150 -0
  113. package/src/doctor/dependencies.ts +179 -0
  114. package/src/doctor/logs.test.ts +244 -0
  115. package/src/doctor/logs.ts +295 -0
  116. package/src/doctor/merge-queue.test.ts +210 -0
  117. package/src/doctor/merge-queue.ts +144 -0
  118. package/src/doctor/structure.test.ts +285 -0
  119. package/src/doctor/structure.ts +195 -0
  120. package/src/doctor/types.ts +37 -0
  121. package/src/doctor/version.test.ts +130 -0
  122. package/src/doctor/version.ts +131 -0
  123. package/src/e2e/chat-flow.test.ts +346 -0
  124. package/src/e2e/init-sling-lifecycle.test.ts +288 -0
  125. package/src/errors.test.ts +21 -0
  126. package/src/errors.ts +246 -0
  127. package/src/events/store.test.ts +660 -0
  128. package/src/events/store.ts +344 -0
  129. package/src/events/tool-filter.test.ts +330 -0
  130. package/src/events/tool-filter.ts +126 -0
  131. package/src/global-setup.ts +14 -0
  132. package/src/index.ts +339 -0
  133. package/src/insights/analyzer.test.ts +466 -0
  134. package/src/insights/analyzer.ts +203 -0
  135. package/src/logging/color.test.ts +118 -0
  136. package/src/logging/color.ts +71 -0
  137. package/src/logging/logger.test.ts +812 -0
  138. package/src/logging/logger.ts +266 -0
  139. package/src/logging/reporter.test.ts +258 -0
  140. package/src/logging/reporter.ts +109 -0
  141. package/src/logging/sanitizer.test.ts +190 -0
  142. package/src/logging/sanitizer.ts +57 -0
  143. package/src/mail/broadcast.test.ts +203 -0
  144. package/src/mail/broadcast.ts +92 -0
  145. package/src/mail/client.test.ts +873 -0
  146. package/src/mail/client.ts +236 -0
  147. package/src/mail/store.test.ts +815 -0
  148. package/src/mail/store.ts +402 -0
  149. package/src/merge/queue.test.ts +449 -0
  150. package/src/merge/queue.ts +262 -0
  151. package/src/merge/resolver.test.ts +1453 -0
  152. package/src/merge/resolver.ts +759 -0
  153. package/src/metrics/store.test.ts +1167 -0
  154. package/src/metrics/store.ts +511 -0
  155. package/src/metrics/summary.test.ts +397 -0
  156. package/src/metrics/summary.ts +178 -0
  157. package/src/metrics/transcript.test.ts +643 -0
  158. package/src/metrics/transcript.ts +351 -0
  159. package/src/mulch/client.test.ts +547 -0
  160. package/src/mulch/client.ts +416 -0
  161. package/src/server/audit-store.test.ts +384 -0
  162. package/src/server/audit-store.ts +257 -0
  163. package/src/server/headless.test.ts +180 -0
  164. package/src/server/headless.ts +151 -0
  165. package/src/server/index.test.ts +241 -0
  166. package/src/server/index.ts +317 -0
  167. package/src/server/public/app.js +187 -0
  168. package/src/server/public/apple-touch-icon.png +0 -0
  169. package/src/server/public/components/agent-badge.js +37 -0
  170. package/src/server/public/components/data-table.js +114 -0
  171. package/src/server/public/components/gateway-chat.js +256 -0
  172. package/src/server/public/components/issue-card.js +96 -0
  173. package/src/server/public/components/layout.js +88 -0
  174. package/src/server/public/components/message-bubble.js +120 -0
  175. package/src/server/public/components/stat-card.js +26 -0
  176. package/src/server/public/components/terminal-panel.js +140 -0
  177. package/src/server/public/favicon-16.png +0 -0
  178. package/src/server/public/favicon-32.png +0 -0
  179. package/src/server/public/favicon.ico +0 -0
  180. package/src/server/public/favicon.png +0 -0
  181. package/src/server/public/index.html +64 -0
  182. package/src/server/public/lib/api.js +35 -0
  183. package/src/server/public/lib/markdown.js +8 -0
  184. package/src/server/public/lib/preact-setup.js +8 -0
  185. package/src/server/public/lib/state.js +99 -0
  186. package/src/server/public/lib/utils.js +309 -0
  187. package/src/server/public/lib/ws.js +79 -0
  188. package/src/server/public/views/chat.js +983 -0
  189. package/src/server/public/views/costs.js +692 -0
  190. package/src/server/public/views/dashboard.js +781 -0
  191. package/src/server/public/views/gateway-chat.js +622 -0
  192. package/src/server/public/views/inspect.js +399 -0
  193. package/src/server/public/views/issues.js +470 -0
  194. package/src/server/public/views/setup.js +94 -0
  195. package/src/server/public/views/task-detail.js +422 -0
  196. package/src/server/routes.test.ts +3816 -0
  197. package/src/server/routes.ts +1964 -0
  198. package/src/server/websocket.test.ts +288 -0
  199. package/src/server/websocket.ts +196 -0
  200. package/src/sessions/compat.test.ts +109 -0
  201. package/src/sessions/compat.ts +17 -0
  202. package/src/sessions/store.test.ts +969 -0
  203. package/src/sessions/store.ts +480 -0
  204. package/src/test-helpers.test.ts +97 -0
  205. package/src/test-helpers.ts +143 -0
  206. package/src/types.ts +708 -0
  207. package/src/watchdog/daemon.test.ts +1233 -0
  208. package/src/watchdog/daemon.ts +533 -0
  209. package/src/watchdog/health.test.ts +371 -0
  210. package/src/watchdog/health.ts +248 -0
  211. package/src/watchdog/triage.test.ts +162 -0
  212. package/src/watchdog/triage.ts +193 -0
  213. package/src/worktree/manager.test.ts +444 -0
  214. package/src/worktree/manager.ts +224 -0
  215. package/src/worktree/tmux.test.ts +1238 -0
  216. package/src/worktree/tmux.ts +644 -0
  217. package/templates/CLAUDE.md.tmpl +89 -0
  218. package/templates/hooks.json.tmpl +132 -0
  219. package/templates/overlay.md.tmpl +79 -0
@@ -0,0 +1,211 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
5
+
6
+ /**
7
+ * Database integrity checks.
8
+ * Validates SQLite databases (mail.db, metrics.db, sessions.db) exist and have correct schema.
9
+ */
10
+ export const checkDatabases: DoctorCheckFn = (_config, legioDir): DoctorCheck[] => {
11
+ const checks: DoctorCheck[] = [];
12
+
13
+ // Define expected databases and their required tables
14
+ const databases = [
15
+ {
16
+ name: "mail.db",
17
+ tables: ["messages"],
18
+ requiredColumns: {
19
+ messages: [
20
+ "id",
21
+ "from_agent",
22
+ "to_agent",
23
+ "subject",
24
+ "body",
25
+ "type",
26
+ "priority",
27
+ "thread_id",
28
+ "payload",
29
+ "read",
30
+ "created_at",
31
+ ],
32
+ },
33
+ },
34
+ {
35
+ name: "metrics.db",
36
+ tables: ["sessions"],
37
+ requiredColumns: {
38
+ sessions: [
39
+ "agent_name",
40
+ "bead_id",
41
+ "capability",
42
+ "started_at",
43
+ "completed_at",
44
+ "duration_ms",
45
+ "exit_code",
46
+ "merge_result",
47
+ "parent_agent",
48
+ "input_tokens",
49
+ "output_tokens",
50
+ "cache_read_tokens",
51
+ "cache_creation_tokens",
52
+ "estimated_cost_usd",
53
+ "model_used",
54
+ ],
55
+ },
56
+ },
57
+ {
58
+ name: "sessions.db",
59
+ tables: ["sessions", "runs"],
60
+ requiredColumns: {
61
+ sessions: [
62
+ "id",
63
+ "agent_name",
64
+ "capability",
65
+ "worktree_path",
66
+ "branch_name",
67
+ "bead_id",
68
+ "tmux_session",
69
+ "state",
70
+ "pid",
71
+ "parent_agent",
72
+ "depth",
73
+ "run_id",
74
+ "started_at",
75
+ "last_activity",
76
+ "escalation_level",
77
+ "stalled_since",
78
+ ],
79
+ runs: [
80
+ "id",
81
+ "started_at",
82
+ "completed_at",
83
+ "agent_count",
84
+ "coordinator_session_id",
85
+ "status",
86
+ ],
87
+ },
88
+ },
89
+ ];
90
+
91
+ for (const dbSpec of databases) {
92
+ const dbPath = join(legioDir, dbSpec.name);
93
+
94
+ // Check if database file exists
95
+ if (!existsSync(dbPath)) {
96
+ checks.push({
97
+ name: `${dbSpec.name} exists`,
98
+ category: "databases",
99
+ status: "fail",
100
+ message: `Database file ${dbSpec.name} does not exist`,
101
+ details: [`Expected at: ${dbPath}`],
102
+ });
103
+ continue;
104
+ }
105
+
106
+ // Try to open the database
107
+ let db: InstanceType<typeof Database> | null = null;
108
+ try {
109
+ db = new Database(dbPath);
110
+
111
+ // Check WAL mode is enabled
112
+ const journalMode = db.prepare("PRAGMA journal_mode").get() as
113
+ | { journal_mode: string }
114
+ | undefined;
115
+ const walEnabled = journalMode?.journal_mode?.toLowerCase() === "wal";
116
+
117
+ // Check for required tables
118
+ const missingTables: string[] = [];
119
+ const schemaIssues: string[] = [];
120
+
121
+ for (const tableName of dbSpec.tables) {
122
+ const tableExists = db
123
+ .prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name=?")
124
+ .get(tableName) as { count: number } | undefined;
125
+
126
+ if (!tableExists || tableExists.count === 0) {
127
+ missingTables.push(tableName);
128
+ continue;
129
+ }
130
+
131
+ // Check columns if table exists
132
+ const requiredCols =
133
+ dbSpec.requiredColumns[tableName as keyof typeof dbSpec.requiredColumns];
134
+ if (requiredCols) {
135
+ const columns = db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
136
+ name: string;
137
+ }>;
138
+ const existingCols = new Set(columns.map((c) => c.name));
139
+
140
+ for (const reqCol of requiredCols) {
141
+ if (!existingCols.has(reqCol)) {
142
+ schemaIssues.push(`Table ${tableName} missing column: ${reqCol}`);
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ // Determine check status
149
+ if (missingTables.length > 0 || schemaIssues.length > 0) {
150
+ const details: string[] = [];
151
+ if (missingTables.length > 0) {
152
+ details.push(`Missing tables: ${missingTables.join(", ")}`);
153
+ }
154
+ if (schemaIssues.length > 0) {
155
+ details.push(...schemaIssues);
156
+ }
157
+ if (!walEnabled) {
158
+ details.push("WAL mode not enabled");
159
+ }
160
+
161
+ checks.push({
162
+ name: `${dbSpec.name} schema`,
163
+ category: "databases",
164
+ status: "fail",
165
+ message: `Database ${dbSpec.name} has schema issues`,
166
+ details,
167
+ fixable: true,
168
+ });
169
+ } else if (!walEnabled) {
170
+ checks.push({
171
+ name: `${dbSpec.name} WAL mode`,
172
+ category: "databases",
173
+ status: "warn",
174
+ message: `Database ${dbSpec.name} is not using WAL mode`,
175
+ details: ["WAL mode improves concurrent access performance"],
176
+ fixable: true,
177
+ });
178
+ } else {
179
+ checks.push({
180
+ name: `${dbSpec.name} health`,
181
+ category: "databases",
182
+ status: "pass",
183
+ message: `Database ${dbSpec.name} is healthy`,
184
+ });
185
+ }
186
+
187
+ db.close();
188
+ } catch (err) {
189
+ if (db) {
190
+ try {
191
+ db.close();
192
+ } catch {
193
+ // Ignore close errors
194
+ }
195
+ }
196
+
197
+ checks.push({
198
+ name: `${dbSpec.name} integrity`,
199
+ category: "databases",
200
+ status: "fail",
201
+ message: `Failed to open or validate ${dbSpec.name}`,
202
+ details: [
203
+ err instanceof Error ? err.message : String(err),
204
+ "Database may be corrupted or locked",
205
+ ],
206
+ });
207
+ }
208
+ }
209
+
210
+ return checks;
211
+ };
@@ -0,0 +1,150 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { LegioConfig } from "../types.ts";
3
+ import { checkDependencies } from "./dependencies.ts";
4
+
5
+ // Minimal config for testing
6
+ const mockConfig: LegioConfig = {
7
+ project: {
8
+ name: "test-project",
9
+ root: "/tmp/test",
10
+ canonicalBranch: "main",
11
+ },
12
+ agents: {
13
+ manifestPath: "/tmp/.legio/agent-manifest.json",
14
+ baseDir: "/tmp/.legio/agents",
15
+ maxConcurrent: 5,
16
+ staggerDelayMs: 1000,
17
+ maxDepth: 2,
18
+ },
19
+ worktrees: {
20
+ baseDir: "/tmp/.legio/worktrees",
21
+ },
22
+ beads: {
23
+ enabled: false,
24
+ },
25
+ mulch: {
26
+ enabled: false,
27
+ domains: [],
28
+ primeFormat: "markdown",
29
+ },
30
+ merge: {
31
+ aiResolveEnabled: false,
32
+ reimagineEnabled: false,
33
+ },
34
+ watchdog: {
35
+ tier0Enabled: false,
36
+ tier0IntervalMs: 30000,
37
+ tier1Enabled: false,
38
+ tier2Enabled: false,
39
+ zombieThresholdMs: 600000,
40
+ nudgeIntervalMs: 60000,
41
+ },
42
+ models: {},
43
+ logging: {
44
+ verbose: false,
45
+ redactSecrets: true,
46
+ },
47
+ };
48
+
49
+ describe("checkDependencies", () => {
50
+ test("returns checks for all required tools", async () => {
51
+ const checks = await checkDependencies(mockConfig, "/tmp/.legio");
52
+
53
+ expect(Array.isArray(checks)).toBe(true);
54
+ expect(checks.length).toBeGreaterThanOrEqual(5);
55
+
56
+ // Verify we have checks for each required tool
57
+ const toolNames = checks.map((c) => c.name);
58
+ expect(toolNames).toContain("git availability");
59
+ expect(toolNames).toContain("node availability");
60
+ expect(toolNames).toContain("tmux availability");
61
+ expect(toolNames).toContain("bd availability");
62
+ expect(toolNames).toContain("mulch availability");
63
+ });
64
+
65
+ test("includes bd CGO support check when bd is available", async () => {
66
+ const checks = await checkDependencies(mockConfig, "/tmp/.legio");
67
+
68
+ const bdCheck = checks.find((c) => c.name === "bd availability");
69
+ if (bdCheck?.status === "pass") {
70
+ const cgoCheck = checks.find((c) => c.name === "bd CGO support");
71
+ expect(cgoCheck).toBeDefined();
72
+ expect(cgoCheck?.category).toBe("dependencies");
73
+ expect(["pass", "warn", "fail"]).toContain(cgoCheck?.status ?? "");
74
+ }
75
+ });
76
+
77
+ test("all checks have required DoctorCheck fields", async () => {
78
+ const checks = await checkDependencies(mockConfig, "/tmp/.legio");
79
+
80
+ for (const check of checks) {
81
+ expect(check).toHaveProperty("name");
82
+ expect(check).toHaveProperty("category");
83
+ expect(check).toHaveProperty("status");
84
+ expect(check).toHaveProperty("message");
85
+
86
+ expect(check.category).toBe("dependencies");
87
+ expect(["pass", "warn", "fail"]).toContain(check.status);
88
+ expect(typeof check.name).toBe("string");
89
+ expect(typeof check.message).toBe("string");
90
+
91
+ if (check.details !== undefined) {
92
+ expect(Array.isArray(check.details)).toBe(true);
93
+ }
94
+
95
+ if (check.fixable !== undefined) {
96
+ expect(typeof check.fixable).toBe("boolean");
97
+ }
98
+ }
99
+ });
100
+
101
+ test("checks for commonly available tools should pass", async () => {
102
+ const checks = await checkDependencies(mockConfig, "/tmp/.legio");
103
+
104
+ // git and node should definitely be available in this environment
105
+ const gitCheck = checks.find((c) => c.name === "git availability");
106
+ const nodeCheck = checks.find((c) => c.name === "node availability");
107
+
108
+ expect(gitCheck).toBeDefined();
109
+ expect(nodeCheck).toBeDefined();
110
+
111
+ // These should pass in a normal development environment
112
+ expect(gitCheck?.status).toBe("pass");
113
+ expect(nodeCheck?.status).toBe("pass");
114
+
115
+ // Passing checks should include version info
116
+ if (gitCheck?.status === "pass") {
117
+ expect(Array.isArray(gitCheck.details)).toBe(true);
118
+ expect(gitCheck.details?.length).toBeGreaterThan(0);
119
+ }
120
+ });
121
+
122
+ test("checks include version details for available tools", async () => {
123
+ const checks = await checkDependencies(mockConfig, "/tmp/.legio");
124
+
125
+ const passingChecks = checks.filter((c) => c.status === "pass");
126
+
127
+ for (const check of passingChecks) {
128
+ expect(check.details).toBeDefined();
129
+ expect(Array.isArray(check.details)).toBe(true);
130
+ expect(check.details?.length).toBeGreaterThan(0);
131
+
132
+ // Version string should not be empty
133
+ const version = check.details?.[0];
134
+ expect(version).toBeDefined();
135
+ expect(typeof version).toBe("string");
136
+ expect(version?.length).toBeGreaterThan(0);
137
+ }
138
+ });
139
+
140
+ test("failing checks are marked as fixable", async () => {
141
+ const checks = await checkDependencies(mockConfig, "/tmp/.legio");
142
+
143
+ const failingChecks = checks.filter((c) => c.status === "fail" || c.status === "warn");
144
+
145
+ // If there are any failing checks, they should be marked fixable
146
+ for (const check of failingChecks) {
147
+ expect(check.fixable).toBe(true);
148
+ }
149
+ });
150
+ });
@@ -0,0 +1,179 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
3
+
4
+ /**
5
+ * External dependency checks.
6
+ * Validates that required CLI tools (git, node, 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
+ _legioDir,
12
+ ): Promise<DoctorCheck[]> => {
13
+ const requiredTools = [
14
+ { name: "git", versionFlag: "--version", required: true },
15
+ { name: "node", versionFlag: "--version", required: true },
16
+ { name: "tmux", versionFlag: "-V", required: true },
17
+ { name: "bd", versionFlag: "--version", required: true },
18
+ { name: "mulch", versionFlag: "--version", required: true },
19
+ ];
20
+
21
+ const checks: DoctorCheck[] = [];
22
+
23
+ for (const tool of requiredTools) {
24
+ const check = await checkTool(tool.name, tool.versionFlag, tool.required);
25
+ checks.push(check);
26
+ }
27
+
28
+ // If bd is available, probe for CGO/Dolt backend functionality
29
+ const bdCheck = checks.find((c) => c.name === "bd availability");
30
+ if (bdCheck?.status === "pass") {
31
+ const cgoCheck = await checkBdCgoSupport();
32
+ checks.push(cgoCheck);
33
+ }
34
+
35
+ return checks;
36
+ };
37
+
38
+ /**
39
+ * Probe whether bd's Dolt database backend is functional.
40
+ * The npm-distributed bd binary may be built without CGO, which causes
41
+ * `bd init` and all database operations to fail even though `bd --version` succeeds.
42
+ * We detect this by running `bd status` in a temp directory and checking for
43
+ * the characteristic "without CGO support" error message.
44
+ */
45
+ async function checkBdCgoSupport(): Promise<DoctorCheck> {
46
+ const { mkdtemp, rm } = await import("node:fs/promises");
47
+ const { join } = await import("node:path");
48
+ const { tmpdir } = await import("node:os");
49
+
50
+ let tempDir: string | undefined;
51
+ try {
52
+ tempDir = await mkdtemp(join(tmpdir(), "legio-bd-cgo-"));
53
+ const { exitCode, stderr } = await new Promise<{ exitCode: number; stderr: string }>(
54
+ (resolve, reject) => {
55
+ const proc = spawn("bd", ["status"], {
56
+ cwd: tempDir,
57
+ stdio: ["ignore", "ignore", "pipe"],
58
+ });
59
+ let stderr = "";
60
+ proc.stderr?.on("data", (chunk: Buffer) => {
61
+ stderr += chunk.toString();
62
+ });
63
+ proc.on("close", (code) => resolve({ exitCode: code ?? 1, stderr }));
64
+ proc.on("error", reject);
65
+ },
66
+ );
67
+
68
+ if (stderr.includes("without CGO support")) {
69
+ return {
70
+ name: "bd CGO support",
71
+ category: "dependencies",
72
+ status: "fail",
73
+ message: "bd binary was built without CGO — Dolt database operations will fail",
74
+ details: [
75
+ "The installed bd binary lacks CGO support required by its Dolt backend.",
76
+ "Workaround: rebuild bd from source with CGO_ENABLED=1 and ICU headers.",
77
+ "See: https://github.com/katyella/legio/issues/10",
78
+ ],
79
+ fixable: true,
80
+ };
81
+ }
82
+
83
+ // Any other exit code is fine — bd status may fail for other reasons
84
+ // (no .beads/ dir, etc.) but those aren't CGO issues
85
+ if (exitCode === 0 || !stderr.includes("CGO")) {
86
+ return {
87
+ name: "bd CGO support",
88
+ category: "dependencies",
89
+ status: "pass",
90
+ message: "bd has functional database backend",
91
+ details: ["Dolt backend operational"],
92
+ };
93
+ }
94
+
95
+ return {
96
+ name: "bd CGO support",
97
+ category: "dependencies",
98
+ status: "warn",
99
+ message: `bd status returned unexpected error (exit code ${exitCode})`,
100
+ details: [stderr.trim().split("\n")[0] || "unknown error"],
101
+ };
102
+ } catch (error) {
103
+ return {
104
+ name: "bd CGO support",
105
+ category: "dependencies",
106
+ status: "warn",
107
+ message: "Could not verify bd CGO support",
108
+ details: [error instanceof Error ? error.message : String(error)],
109
+ };
110
+ } finally {
111
+ if (tempDir) {
112
+ await rm(tempDir, { recursive: true }).catch(() => {});
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Check if a CLI tool is available by attempting to run it with a version flag.
119
+ */
120
+ async function checkTool(
121
+ name: string,
122
+ versionFlag: string,
123
+ required: boolean,
124
+ ): Promise<DoctorCheck> {
125
+ try {
126
+ const { exitCode, stdout, stderr } = await new Promise<{
127
+ exitCode: number;
128
+ stdout: string;
129
+ stderr: string;
130
+ }>((resolve, reject) => {
131
+ const proc = spawn(name, [versionFlag], { stdio: ["ignore", "pipe", "pipe"] });
132
+ let stdout = "";
133
+ let stderr = "";
134
+ proc.stdout?.on("data", (chunk: Buffer) => {
135
+ stdout += chunk.toString();
136
+ });
137
+ proc.stderr?.on("data", (chunk: Buffer) => {
138
+ stderr += chunk.toString();
139
+ });
140
+ proc.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
141
+ proc.on("error", reject);
142
+ });
143
+
144
+ if (exitCode === 0) {
145
+ const version = stdout.split("\n")[0]?.trim() || "version unknown";
146
+
147
+ return {
148
+ name: `${name} availability`,
149
+ category: "dependencies",
150
+ status: "pass",
151
+ message: `${name} is available`,
152
+ details: [version],
153
+ };
154
+ }
155
+
156
+ // Non-zero exit code
157
+ return {
158
+ name: `${name} availability`,
159
+ category: "dependencies",
160
+ status: required ? "fail" : "warn",
161
+ message: `${name} command failed (exit code ${exitCode})`,
162
+ details: stderr ? [stderr.trim()] : undefined,
163
+ fixable: true,
164
+ };
165
+ } catch (error) {
166
+ // Command not found or spawn failed
167
+ return {
168
+ name: `${name} availability`,
169
+ category: "dependencies",
170
+ status: required ? "fail" : "warn",
171
+ message: `${name} is not installed or not in PATH`,
172
+ details: [
173
+ `Install ${name} or ensure it is in your PATH`,
174
+ error instanceof Error ? error.message : String(error),
175
+ ],
176
+ fixable: true,
177
+ };
178
+ }
179
+ }