agentweaver 0.1.11 → 0.1.13

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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/dist/artifacts.js +9 -0
  4. package/dist/doctor/checks/agentweaver-home.js +57 -0
  5. package/dist/doctor/checks/category.js +9 -0
  6. package/dist/doctor/checks/cwd-context.js +69 -0
  7. package/dist/doctor/checks/env-diagnostics.js +171 -0
  8. package/dist/doctor/checks/executors.js +106 -0
  9. package/dist/doctor/checks/flow-readiness.js +305 -0
  10. package/dist/doctor/checks/node-version.js +79 -0
  11. package/dist/doctor/checks/register.js +18 -0
  12. package/dist/doctor/checks/system.js +91 -0
  13. package/dist/doctor/index.js +4 -0
  14. package/dist/doctor/orchestrator.js +78 -0
  15. package/dist/doctor/registry.js +50 -0
  16. package/dist/doctor/runner.js +89 -0
  17. package/dist/doctor/types.js +12 -0
  18. package/dist/executors/codex-executor.js +2 -1
  19. package/dist/executors/jira-fetch-executor.js +1 -0
  20. package/dist/executors/opencode-executor.js +22 -11
  21. package/dist/executors/process-executor.js +3 -0
  22. package/dist/index.js +59 -35
  23. package/dist/interactive-ui.js +579 -159
  24. package/dist/jira.js +57 -0
  25. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +105 -21
  26. package/dist/pipeline/flow-specs/review/review-fix.json +1 -0
  27. package/dist/pipeline/flow-specs/review/review-loop.json +2 -0
  28. package/dist/pipeline/flow-specs/task-describe.json +64 -3
  29. package/dist/pipeline/nodes/jira-fetch-node.js +3 -0
  30. package/dist/pipeline/nodes/review-findings-form-node.js +33 -3
  31. package/dist/pipeline/nodes/user-input-node.js +18 -4
  32. package/dist/pipeline/prompt-registry.js +2 -1
  33. package/dist/pipeline/spec-types.js +2 -0
  34. package/dist/pipeline/value-resolver.js +11 -1
  35. package/dist/prompts.js +17 -2
  36. package/dist/runtime/process-runner.js +9 -3
  37. package/dist/structured-artifact-schema-registry.js +1 -0
  38. package/dist/structured-artifact-schemas.json +22 -0
  39. package/dist/user-input.js +8 -1
  40. package/package.json +4 -2
  41. package/dist/pipeline/flow-model-settings.js +0 -77
@@ -0,0 +1,305 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { DoctorStatus } from "../types.js";
4
+ import { CATEGORY } from "./category.js";
5
+ import { BUILT_IN_COMMAND_FLOW_IDS } from "../../pipeline/flow-catalog.js";
6
+ import { validateStructuredArtifact } from "../../structured-artifacts.js";
7
+ import { designJsonFile, planJsonFile, scopeArtifactsDir } from "../../artifacts.js";
8
+ const GO_BINARY_PATTERNS = ["go", "go.exe"];
9
+ const GIT_BINARY_PATTERNS = ["git", "git.exe"];
10
+ function findBinary(name) {
11
+ const envPath = process.env.PATH ?? "";
12
+ const pathDirs = envPath.split(path.delimiter);
13
+ for (const dir of pathDirs) {
14
+ for (const pattern of name === "go" ? GO_BINARY_PATTERNS : GIT_BINARY_PATTERNS) {
15
+ const fullPath = path.join(dir, pattern);
16
+ if (existsSync(fullPath)) {
17
+ return fullPath;
18
+ }
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ function checkBinaryPresence(flowId) {
24
+ const goFlows = new Set([
25
+ "run-go-tests-loop",
26
+ "run-go-linter-loop",
27
+ "auto-golang",
28
+ ]);
29
+ const gitFlows = new Set(["git-commit", "gitlab-review", "gitlab-diff-review", "mr-description"]);
30
+ if (goFlows.has(flowId)) {
31
+ return findBinary("go");
32
+ }
33
+ if (gitFlows.has(flowId)) {
34
+ return findBinary("git");
35
+ }
36
+ return null;
37
+ }
38
+ function isJiraConfigured() {
39
+ return !!(process.env.JIRA_API_KEY && process.env.JIRA_BASE_URL);
40
+ }
41
+ function isGitLabConfigured() {
42
+ return !!process.env.GITLAB_TOKEN;
43
+ }
44
+ function getLatestDesignIteration(scopeKey) {
45
+ const artifactsDir = scopeArtifactsDir(scopeKey);
46
+ if (!existsSync(artifactsDir)) {
47
+ return null;
48
+ }
49
+ const files = readdirSync(artifactsDir);
50
+ const designFiles = files.filter((f) => /^design-.*-\d+\.json$/.test(f));
51
+ if (designFiles.length === 0) {
52
+ return null;
53
+ }
54
+ const iterations = designFiles.map((f) => {
55
+ const match = f.match(/^design-.*-(\d+)\.json$/);
56
+ return match?.[1] ? parseInt(match[1], 10) : 0;
57
+ });
58
+ return Math.max(...iterations);
59
+ }
60
+ function getLatestPlanIteration(scopeKey) {
61
+ const artifactsDir = scopeArtifactsDir(scopeKey);
62
+ if (!existsSync(artifactsDir)) {
63
+ return null;
64
+ }
65
+ const files = readdirSync(artifactsDir);
66
+ const planFiles = files.filter((f) => /^plan-.*-\d+\.json$/.test(f));
67
+ if (planFiles.length === 0) {
68
+ return null;
69
+ }
70
+ const iterations = planFiles.map((f) => {
71
+ const match = f.match(/^plan-.*-(\d+)\.json$/);
72
+ return match?.[1] ? parseInt(match[1], 10) : 0;
73
+ });
74
+ return Math.max(...iterations);
75
+ }
76
+ function checkImplementFlowReadiness(scopeKey) {
77
+ const blockers = [];
78
+ const latestDesignIteration = getLatestDesignIteration(scopeKey);
79
+ const latestPlanIteration = getLatestPlanIteration(scopeKey);
80
+ if (latestDesignIteration === null) {
81
+ blockers.push("design artifact not found");
82
+ }
83
+ else {
84
+ const designPath = designJsonFile(scopeKey, latestDesignIteration);
85
+ if (!existsSync(designPath)) {
86
+ blockers.push(`design artifact not found at ${designPath}`);
87
+ }
88
+ else {
89
+ try {
90
+ validateStructuredArtifact(designPath, "implementation-design/v1");
91
+ }
92
+ catch (error) {
93
+ blockers.push(`design artifact schema invalid: ${error.message}`);
94
+ }
95
+ }
96
+ }
97
+ if (latestPlanIteration === null) {
98
+ blockers.push("plan artifact not found");
99
+ }
100
+ else {
101
+ const planPath = planJsonFile(scopeKey, latestPlanIteration);
102
+ if (!existsSync(planPath)) {
103
+ blockers.push(`plan artifact not found at ${planPath}`);
104
+ }
105
+ else {
106
+ try {
107
+ validateStructuredArtifact(planPath, "implementation-plan/v1");
108
+ }
109
+ catch (error) {
110
+ blockers.push(`plan artifact schema invalid: ${error.message}`);
111
+ }
112
+ }
113
+ }
114
+ return {
115
+ flowId: "implement",
116
+ status: blockers.length === 0 ? "ready" : "not_ready",
117
+ blockers,
118
+ };
119
+ }
120
+ function checkReviewProjectFlowReadiness(scopeKey) {
121
+ const blockers = [];
122
+ const latestDesignIteration = getLatestDesignIteration(scopeKey);
123
+ if (latestDesignIteration === null) {
124
+ blockers.push("design artifact not found");
125
+ }
126
+ else {
127
+ const designPath = designJsonFile(scopeKey, latestDesignIteration);
128
+ if (!existsSync(designPath)) {
129
+ blockers.push(`design artifact not found at ${designPath}`);
130
+ }
131
+ }
132
+ return {
133
+ flowId: "review",
134
+ mode: "project",
135
+ status: blockers.length === 0 ? "ready" : "not_ready",
136
+ blockers,
137
+ };
138
+ }
139
+ function checkReviewJiraFlowReadiness(scopeKey) {
140
+ const blockers = [];
141
+ if (!isJiraConfigured()) {
142
+ blockers.push("Jira not configured (JIRA_API_KEY or JIRA_BASE_URL missing)");
143
+ }
144
+ const latestDesignIteration = getLatestDesignIteration(scopeKey);
145
+ if (latestDesignIteration === null) {
146
+ blockers.push("design artifact not found");
147
+ }
148
+ else {
149
+ const designPath = designJsonFile(scopeKey, latestDesignIteration);
150
+ if (!existsSync(designPath)) {
151
+ blockers.push(`design artifact not found at ${designPath}`);
152
+ }
153
+ }
154
+ const latestPlanIteration = getLatestPlanIteration(scopeKey);
155
+ if (latestPlanIteration === null) {
156
+ blockers.push("plan artifact not found");
157
+ }
158
+ else {
159
+ const planPath = planJsonFile(scopeKey, latestPlanIteration);
160
+ if (!existsSync(planPath)) {
161
+ blockers.push(`plan artifact not found at ${planPath}`);
162
+ }
163
+ }
164
+ return {
165
+ flowId: "review",
166
+ mode: "jira",
167
+ status: blockers.length === 0 ? "ready" : "not_ready",
168
+ blockers,
169
+ };
170
+ }
171
+ function checkPlanJiraFlowReadiness(scopeKey) {
172
+ const blockers = [];
173
+ if (!isJiraConfigured()) {
174
+ blockers.push("Jira not configured (JIRA_API_KEY or JIRA_BASE_URL missing)");
175
+ }
176
+ if (!process.env.JIRA_ISSUE_KEY && !scopeKey.includes("@")) {
177
+ blockers.push("Jira issue key not available");
178
+ }
179
+ return {
180
+ flowId: "plan",
181
+ mode: "jira",
182
+ status: blockers.length === 0 ? "ready" : "not_ready",
183
+ blockers,
184
+ };
185
+ }
186
+ function checkPlanProjectFlowReadiness(scopeKey) {
187
+ const blockers = [];
188
+ const latestDesignIteration = getLatestDesignIteration(scopeKey);
189
+ if (latestDesignIteration === null) {
190
+ blockers.push("design artifact not found (task-describe output required)");
191
+ }
192
+ else {
193
+ const designPath = designJsonFile(scopeKey, latestDesignIteration);
194
+ if (!existsSync(designPath)) {
195
+ blockers.push("design artifact not found");
196
+ }
197
+ }
198
+ return {
199
+ flowId: "plan",
200
+ mode: "project",
201
+ status: blockers.length === 0 ? "ready" : "not_ready",
202
+ blockers,
203
+ };
204
+ }
205
+ function checkGenericFlowReadiness(flowId, scopeKey) {
206
+ const blockers = [];
207
+ const binaryPath = checkBinaryPresence(flowId);
208
+ if (binaryPath === null) {
209
+ const requiredBinaries = {
210
+ "run-go-tests-loop": "go",
211
+ "run-go-linter-loop": "go",
212
+ "auto-golang": "go",
213
+ "git_commit": "git",
214
+ "gitlab-review": "git",
215
+ "gitlab-diff-review": "git",
216
+ "mr-description": "git",
217
+ };
218
+ const required = requiredBinaries[flowId];
219
+ if (required) {
220
+ blockers.push(`${required} binary not found in PATH`);
221
+ }
222
+ }
223
+ const jiraFlows = new Set(["gitlab-review", "gitlab-diff-review", "mr-description"]);
224
+ if (jiraFlows.has(flowId) && isGitLabConfigured()) {
225
+ // GitLab configured - could add connectivity check here
226
+ }
227
+ return {
228
+ flowId,
229
+ status: blockers.length === 0 ? "ready" : "not_ready",
230
+ blockers,
231
+ };
232
+ }
233
+ function determineFlowReadiness(flowId, scopeKey) {
234
+ if (flowId === "implement") {
235
+ return [checkImplementFlowReadiness(scopeKey)];
236
+ }
237
+ if (flowId === "review") {
238
+ return [
239
+ checkReviewProjectFlowReadiness(scopeKey),
240
+ checkReviewJiraFlowReadiness(scopeKey),
241
+ ];
242
+ }
243
+ if (flowId === "plan") {
244
+ return [
245
+ checkPlanJiraFlowReadiness(scopeKey),
246
+ checkPlanProjectFlowReadiness(scopeKey),
247
+ ];
248
+ }
249
+ return [checkGenericFlowReadiness(flowId, scopeKey)];
250
+ }
251
+ function getScopeKey() {
252
+ const cwd = process.cwd();
253
+ const scopesRoot = path.join(cwd, ".agentweaver", "scopes");
254
+ if (!existsSync(scopesRoot)) {
255
+ return "unknown";
256
+ }
257
+ const entries = readdirSync(scopesRoot);
258
+ if (entries.length === 0) {
259
+ return "unknown";
260
+ }
261
+ return entries[0];
262
+ }
263
+ function performFlowReadinessCheck() {
264
+ const scopeKey = getScopeKey();
265
+ const allEntries = [];
266
+ for (const flowId of BUILT_IN_COMMAND_FLOW_IDS) {
267
+ const entries = determineFlowReadiness(flowId, scopeKey);
268
+ allEntries.push(...entries);
269
+ }
270
+ const readyCount = allEntries.filter((e) => e.status === "ready").length;
271
+ const notReadyCount = allEntries.filter((e) => e.status === "not_ready").length;
272
+ const notConfiguredCount = allEntries.filter((e) => e.status === "not_configured").length;
273
+ const lines = [];
274
+ for (const entry of allEntries) {
275
+ const modeStr = entry.mode ? `:${entry.mode}` : "";
276
+ const statusStr = entry.status === "ready" ? "✓ ready" : entry.status === "not_configured" ? "⚠ not configured" : "✗ not ready";
277
+ lines.push(` ${entry.flowId}${modeStr}: ${statusStr}`);
278
+ for (const blocker of entry.blockers) {
279
+ lines.push(` - ${blocker}`);
280
+ }
281
+ }
282
+ const overallStatus = notReadyCount > 0 ? DoctorStatus.Fail : notConfiguredCount > 0 ? DoctorStatus.Warn : DoctorStatus.Ok;
283
+ const message = `${readyCount} flows ready, ${notReadyCount} not ready, ${notConfiguredCount} not configured`;
284
+ return {
285
+ status: overallStatus,
286
+ message,
287
+ details: lines.join("\n"),
288
+ };
289
+ }
290
+ export const flowReadinessCheck = {
291
+ id: "flow-readiness-01",
292
+ category: CATEGORY.FLOW_READINESS,
293
+ title: "flow-readiness",
294
+ dependencies: ["env-diagnostics-01", "cwd-context-01"],
295
+ execute: async () => {
296
+ const result = performFlowReadinessCheck();
297
+ return {
298
+ id: "flow-readiness-01",
299
+ status: result.status,
300
+ title: "flow-readiness",
301
+ message: result.message,
302
+ details: result.details,
303
+ };
304
+ },
305
+ };
@@ -0,0 +1,79 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import semver from "semver";
4
+ import { DoctorStatus } from "../types.js";
5
+ import { CATEGORY } from "./category.js";
6
+ const PACKAGE_ROOT = process.cwd();
7
+ export const nodeVersionCheck = {
8
+ id: "node-version-01",
9
+ category: CATEGORY.NODE_VERSION,
10
+ title: "node-version",
11
+ dependencies: ["system-01"],
12
+ execute: async () => {
13
+ const packageJsonPath = path.join(PACKAGE_ROOT, "package.json");
14
+ if (!existsSync(packageJsonPath)) {
15
+ return {
16
+ id: "node-version-01",
17
+ status: DoctorStatus.Warn,
18
+ title: "node-version",
19
+ message: "package.json not found in project root",
20
+ hint: "Cannot verify Node.js compatibility without package.json",
21
+ };
22
+ }
23
+ let enginesNode;
24
+ try {
25
+ const raw = readFileSync(packageJsonPath, "utf8");
26
+ const pkg = JSON.parse(raw);
27
+ if (!pkg.engines?.node) {
28
+ return {
29
+ id: "node-version-01",
30
+ status: DoctorStatus.Warn,
31
+ title: "node-version",
32
+ message: "engines.node not specified in package.json",
33
+ hint: "Consider adding engines.node requirement to package.json",
34
+ };
35
+ }
36
+ enginesNode = pkg.engines.node;
37
+ }
38
+ catch {
39
+ return {
40
+ id: "node-version-01",
41
+ status: DoctorStatus.Warn,
42
+ title: "node-version",
43
+ message: "Failed to read package.json",
44
+ hint: "Ensure package.json is valid JSON",
45
+ };
46
+ }
47
+ const currentVersion = process.version.slice(1);
48
+ const validRange = semver.validRange(enginesNode);
49
+ if (!validRange) {
50
+ return {
51
+ id: "node-version-01",
52
+ status: DoctorStatus.Warn,
53
+ title: "node-version",
54
+ message: `Invalid semver range in engines.node: ${enginesNode}`,
55
+ hint: "Ensure engines.node contains a valid semver range",
56
+ };
57
+ }
58
+ const satisfied = semver.satisfies(currentVersion, validRange);
59
+ if (satisfied) {
60
+ return {
61
+ id: "node-version-01",
62
+ status: DoctorStatus.Ok,
63
+ title: "node-version",
64
+ message: `Compatible: ${currentVersion} satisfies ${enginesNode}`,
65
+ details: `engine requirement: ${enginesNode}, current: ${currentVersion}`,
66
+ };
67
+ }
68
+ else {
69
+ return {
70
+ id: "node-version-01",
71
+ status: DoctorStatus.Warn,
72
+ title: "node-version",
73
+ message: `Incompatible: ${currentVersion} does not satisfy ${enginesNode}`,
74
+ hint: "Consider using a Node.js version that matches the engines.node requirement",
75
+ details: `engine requirement: ${enginesNode}, current: ${currentVersion}`,
76
+ };
77
+ }
78
+ },
79
+ };
@@ -0,0 +1,18 @@
1
+ import { REGISTRY } from "../registry.js";
2
+ import { systemChecks } from "./system.js";
3
+ import { nodeVersionCheck } from "./node-version.js";
4
+ import { agentweaverHomeCheck } from "./agentweaver-home.js";
5
+ import { cwdContextCheck } from "./cwd-context.js";
6
+ import { codexExecutorCheck, opencodeExecutorCheck } from "./executors.js";
7
+ import { envDiagnosticsCheck } from "./env-diagnostics.js";
8
+ import { flowReadinessCheck } from "./flow-readiness.js";
9
+ REGISTRY.register(nodeVersionCheck);
10
+ for (const check of systemChecks) {
11
+ REGISTRY.register(check);
12
+ }
13
+ REGISTRY.register(agentweaverHomeCheck);
14
+ REGISTRY.register(cwdContextCheck);
15
+ REGISTRY.register(codexExecutorCheck);
16
+ REGISTRY.register(opencodeExecutorCheck);
17
+ REGISTRY.register(envDiagnosticsCheck);
18
+ REGISTRY.register(flowReadinessCheck);
@@ -0,0 +1,91 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { DoctorStatus } from "../types.js";
3
+ import { CATEGORY } from "./category.js";
4
+ import { resolveCmd } from "../../runtime/command-resolution.js";
5
+ const BINARY_CHECKS = [
6
+ {
7
+ id: "system-01",
8
+ title: "node",
9
+ command: "node",
10
+ versionArgs: ["--version"],
11
+ versionEnvVar: "",
12
+ parseVersion: (stdout) => stdout.trim(),
13
+ },
14
+ {
15
+ id: "system-02",
16
+ title: "npm",
17
+ command: "npm",
18
+ versionArgs: ["--version"],
19
+ versionEnvVar: "",
20
+ parseVersion: (stdout) => stdout.trim(),
21
+ },
22
+ {
23
+ id: "system-03",
24
+ title: "git",
25
+ command: "git",
26
+ versionArgs: ["--version"],
27
+ versionEnvVar: "",
28
+ parseVersion: (stdout) => stdout.trim(),
29
+ },
30
+ {
31
+ id: "system-04",
32
+ title: "bash",
33
+ command: "bash",
34
+ versionArgs: ["--version"],
35
+ versionEnvVar: "",
36
+ parseVersion: (stdout) => (stdout.split("\n")[0] ?? stdout).trim(),
37
+ },
38
+ ];
39
+ function runVersionCheck(spec) {
40
+ let cmdPath;
41
+ try {
42
+ cmdPath = resolveCmd(spec.command, "");
43
+ }
44
+ catch {
45
+ return {
46
+ status: DoctorStatus.Fail,
47
+ message: `${spec.title} binary not found`,
48
+ hint: `Install ${spec.title} or ensure it is available in PATH`,
49
+ };
50
+ }
51
+ const result = spawnSync(cmdPath, spec.versionArgs, { encoding: "utf8", stdio: "pipe" });
52
+ if (result.status !== 0 || !result.stdout) {
53
+ return {
54
+ status: DoctorStatus.Fail,
55
+ message: `Failed to get ${spec.title} version`,
56
+ hint: `${spec.title} --version did not return expected output`,
57
+ };
58
+ }
59
+ const stdout = result.stdout;
60
+ const version = spec.parseVersion(stdout);
61
+ const okResult = {
62
+ status: DoctorStatus.Ok,
63
+ message: version,
64
+ };
65
+ if (spec.title === "node") {
66
+ okResult.details = `process.version: ${process.version}`;
67
+ }
68
+ return okResult;
69
+ }
70
+ export const systemChecks = BINARY_CHECKS.map((spec) => ({
71
+ id: spec.id,
72
+ category: CATEGORY.SYSTEM,
73
+ title: spec.title,
74
+ dependencies: [],
75
+ execute: async () => {
76
+ const result = runVersionCheck(spec);
77
+ const checkResult = {
78
+ id: spec.id,
79
+ status: result.status,
80
+ title: spec.title,
81
+ message: result.message,
82
+ };
83
+ if (result.hint) {
84
+ checkResult.hint = result.hint;
85
+ }
86
+ if (result.details) {
87
+ checkResult.details = result.details;
88
+ }
89
+ return checkResult;
90
+ },
91
+ }));
@@ -0,0 +1,4 @@
1
+ export { DoctorStatus, ReadinessStatus } from "./types.js";
2
+ export { CheckRegistry, REGISTRY } from "./registry.js";
3
+ export { DoctorOrchestrator } from "./orchestrator.js";
4
+ export { runDoctorCommand } from "./runner.js";
@@ -0,0 +1,78 @@
1
+ import { DoctorStatus, ReadinessStatus } from "./types.js";
2
+ import { REGISTRY } from "./registry.js";
3
+ import { SOFT_CHECK_IDS } from "./checks/cwd-context.js";
4
+ class DoctorOrchestrator {
5
+ async run(checks, filter) {
6
+ let checksToRun;
7
+ if (checks) {
8
+ checksToRun = checks;
9
+ }
10
+ else if (filter) {
11
+ const byCategory = REGISTRY.getByCategory(filter);
12
+ if (byCategory.length > 0) {
13
+ checksToRun = byCategory;
14
+ }
15
+ else {
16
+ const byId = REGISTRY.getById(filter);
17
+ if (byId) {
18
+ checksToRun = [byId];
19
+ }
20
+ else {
21
+ const byTitle = REGISTRY.getByTitle(filter);
22
+ if (byTitle) {
23
+ checksToRun = [byTitle];
24
+ }
25
+ else {
26
+ checksToRun = [];
27
+ }
28
+ }
29
+ }
30
+ }
31
+ else {
32
+ checksToRun = REGISTRY.getDependencyOrder();
33
+ }
34
+ const results = [];
35
+ for (const check of checksToRun) {
36
+ const result = await this.executeCheck(check);
37
+ results.push(result);
38
+ if (result.status === DoctorStatus.Fail) {
39
+ break;
40
+ }
41
+ }
42
+ const overall = this.aggregateReadiness(results);
43
+ return {
44
+ overall,
45
+ checks: results,
46
+ timestamp: new Date().toISOString(),
47
+ };
48
+ }
49
+ async executeCheck(check) {
50
+ const timeout = check.timeout ?? 30000;
51
+ try {
52
+ const timeoutPromise = new Promise((_, reject) => {
53
+ setTimeout(() => reject(new Error(`Check '${check.id}' timed out after ${timeout}ms`)), timeout);
54
+ });
55
+ const result = await Promise.race([check.execute(), timeoutPromise]);
56
+ return result;
57
+ }
58
+ catch (error) {
59
+ return {
60
+ id: check.id,
61
+ status: DoctorStatus.Fail,
62
+ title: check.title,
63
+ message: error instanceof Error ? error.message : "Unknown error occurred",
64
+ hint: `Check execution failed: ${check.id}`,
65
+ };
66
+ }
67
+ }
68
+ aggregateReadiness(results) {
69
+ if (results.some((r) => r.status === DoctorStatus.Fail)) {
70
+ return ReadinessStatus.NotReady;
71
+ }
72
+ if (results.some((r) => r.status === DoctorStatus.Warn && !SOFT_CHECK_IDS.includes(r.id))) {
73
+ return ReadinessStatus.ReadyWithWarnings;
74
+ }
75
+ return ReadinessStatus.Ready;
76
+ }
77
+ }
78
+ export { DoctorOrchestrator };
@@ -0,0 +1,50 @@
1
+ class CheckRegistry {
2
+ checks = [];
3
+ register(check) {
4
+ this.checks.push(check);
5
+ }
6
+ getAll() {
7
+ return [...this.checks];
8
+ }
9
+ getByCategory(category) {
10
+ return this.checks.filter((check) => check.category === category);
11
+ }
12
+ getById(id) {
13
+ return this.checks.find((check) => check.id === id);
14
+ }
15
+ getByTitle(title) {
16
+ return this.checks.find((check) => check.title === title);
17
+ }
18
+ getDependencyOrder() {
19
+ const result = [];
20
+ const visited = new Set();
21
+ const visiting = new Set();
22
+ const visit = (check) => {
23
+ if (visited.has(check.id)) {
24
+ return;
25
+ }
26
+ if (visiting.has(check.id)) {
27
+ throw new Error(`Cycle detected in check dependencies: ${check.id}`);
28
+ }
29
+ visiting.add(check.id);
30
+ for (const depId of check.dependencies) {
31
+ const dep = this.getById(depId);
32
+ if (dep) {
33
+ visit(dep);
34
+ }
35
+ }
36
+ visiting.delete(check.id);
37
+ visited.add(check.id);
38
+ result.push(check);
39
+ };
40
+ for (const check of this.checks) {
41
+ if (!visited.has(check.id)) {
42
+ visit(check);
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ }
48
+ export { CheckRegistry };
49
+ const REGISTRY = new CheckRegistry();
50
+ export { REGISTRY };