gsd-pi 2.31.2-dev.c8d7e03 → 2.32.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.
- package/dist/resources/extensions/gsd/auto-start.ts +4 -2
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
- package/dist/resources/extensions/gsd/doctor.ts +6 -0
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +6 -0
- package/dist/resources/extensions/gsd/progress-score.ts +273 -0
- package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-start.ts +4 -2
- package/src/resources/extensions/gsd/commands.ts +19 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/src/resources/extensions/gsd/doctor-types.ts +14 -1
- package/src/resources/extensions/gsd/doctor.ts +6 -0
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/progress-score.ts +273 -0
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Progress Score — Traffic Light Status Indicator (#1221)
|
|
3
|
+
*
|
|
4
|
+
* Combines existing health signals into a single at-a-glance status:
|
|
5
|
+
* - Green: progressing well
|
|
6
|
+
* - Yellow: struggling (retries, warnings)
|
|
7
|
+
* - Red: stuck (loops, persistent errors, no activity)
|
|
8
|
+
*
|
|
9
|
+
* Purely derived — no stored state. Reads from doctor-proactive health
|
|
10
|
+
* tracking, stuck detection counters, and working-tree activity.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getHealthTrend,
|
|
15
|
+
getConsecutiveErrorUnits,
|
|
16
|
+
getHealthHistory,
|
|
17
|
+
type HealthSnapshot,
|
|
18
|
+
} from "./doctor-proactive.js";
|
|
19
|
+
|
|
20
|
+
// ── Types ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export type ProgressLevel = "green" | "yellow" | "red";
|
|
23
|
+
|
|
24
|
+
export interface ProgressScore {
|
|
25
|
+
level: ProgressLevel;
|
|
26
|
+
summary: string;
|
|
27
|
+
signals: ProgressSignal[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProgressSignal {
|
|
31
|
+
name: string;
|
|
32
|
+
level: ProgressLevel;
|
|
33
|
+
detail: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Signal Evaluators ──────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function evaluateHealthTrend(): ProgressSignal {
|
|
39
|
+
const trend = getHealthTrend();
|
|
40
|
+
|
|
41
|
+
switch (trend) {
|
|
42
|
+
case "improving":
|
|
43
|
+
return { name: "health_trend", level: "green", detail: "Health improving" };
|
|
44
|
+
case "stable":
|
|
45
|
+
return { name: "health_trend", level: "green", detail: "Health stable" };
|
|
46
|
+
case "degrading":
|
|
47
|
+
return { name: "health_trend", level: "red", detail: "Health degrading" };
|
|
48
|
+
case "unknown":
|
|
49
|
+
return { name: "health_trend", level: "green", detail: "Insufficient data" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function evaluateErrorStreak(): ProgressSignal {
|
|
54
|
+
const streak = getConsecutiveErrorUnits();
|
|
55
|
+
|
|
56
|
+
if (streak === 0) {
|
|
57
|
+
return { name: "error_streak", level: "green", detail: "No consecutive errors" };
|
|
58
|
+
}
|
|
59
|
+
if (streak <= 2) {
|
|
60
|
+
return { name: "error_streak", level: "yellow", detail: `${streak} consecutive error unit(s)` };
|
|
61
|
+
}
|
|
62
|
+
return { name: "error_streak", level: "red", detail: `${streak} consecutive error units` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function evaluateRecentErrors(): ProgressSignal {
|
|
66
|
+
const history = getHealthHistory();
|
|
67
|
+
if (history.length === 0) {
|
|
68
|
+
return { name: "recent_errors", level: "green", detail: "No health data yet" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const latest = history[history.length - 1]!;
|
|
72
|
+
|
|
73
|
+
if (latest.errors === 0 && latest.warnings <= 1) {
|
|
74
|
+
return { name: "recent_errors", level: "green", detail: `${latest.errors}E/${latest.warnings}W` };
|
|
75
|
+
}
|
|
76
|
+
if (latest.errors === 0) {
|
|
77
|
+
return { name: "recent_errors", level: "yellow", detail: `${latest.warnings} warning(s)` };
|
|
78
|
+
}
|
|
79
|
+
if (latest.errors <= 2) {
|
|
80
|
+
return { name: "recent_errors", level: "yellow", detail: `${latest.errors} error(s), ${latest.warnings} warning(s)` };
|
|
81
|
+
}
|
|
82
|
+
return { name: "recent_errors", level: "red", detail: `${latest.errors} error(s), ${latest.warnings} warning(s)` };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function evaluateArtifactProduction(): ProgressSignal {
|
|
86
|
+
const history = getHealthHistory();
|
|
87
|
+
if (history.length < 2) {
|
|
88
|
+
return { name: "artifact_production", level: "green", detail: "Insufficient data" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const totalFixes = history.reduce((sum, s) => sum + s.fixesApplied, 0);
|
|
92
|
+
const recent = history.slice(-3);
|
|
93
|
+
const recentFixes = recent.reduce((sum, s) => sum + s.fixesApplied, 0);
|
|
94
|
+
|
|
95
|
+
// If recent units are all producing fixes but errors aren't decreasing,
|
|
96
|
+
// doctor is fighting fires but not making headway
|
|
97
|
+
if (recentFixes > 3 && recent.every(s => s.errors > 0)) {
|
|
98
|
+
return { name: "artifact_production", level: "yellow", detail: "Doctor applying fixes but errors persist" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { name: "artifact_production", level: "green", detail: `${totalFixes} total fixes applied` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function evaluateDispatchVelocity(): ProgressSignal {
|
|
105
|
+
const history = getHealthHistory();
|
|
106
|
+
if (history.length < 3) {
|
|
107
|
+
return { name: "dispatch_velocity", level: "green", detail: "Insufficient data" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check time between recent snapshots — are units completing at a reasonable rate?
|
|
111
|
+
const recent = history.slice(-5);
|
|
112
|
+
if (recent.length < 2) {
|
|
113
|
+
return { name: "dispatch_velocity", level: "green", detail: "Insufficient data" };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const timeDiffs: number[] = [];
|
|
117
|
+
for (let i = 1; i < recent.length; i++) {
|
|
118
|
+
timeDiffs.push(recent[i]!.timestamp - recent[i - 1]!.timestamp);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const avgTimeMs = timeDiffs.reduce((a, b) => a + b, 0) / timeDiffs.length;
|
|
122
|
+
const avgTimeMins = Math.round(avgTimeMs / 60_000);
|
|
123
|
+
|
|
124
|
+
// If average unit time is > 15 minutes, something might be wrong
|
|
125
|
+
if (avgTimeMins > 15) {
|
|
126
|
+
return { name: "dispatch_velocity", level: "yellow", detail: `Units averaging ${avgTimeMins}min each` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { name: "dispatch_velocity", level: "green", detail: `Units averaging ${avgTimeMins || "<1"}min each` };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Main API ───────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compute the current progress score by evaluating all available signals.
|
|
136
|
+
* Returns a composite score with individual signal details.
|
|
137
|
+
*/
|
|
138
|
+
export function computeProgressScore(): ProgressScore {
|
|
139
|
+
const signals: ProgressSignal[] = [
|
|
140
|
+
evaluateHealthTrend(),
|
|
141
|
+
evaluateErrorStreak(),
|
|
142
|
+
evaluateRecentErrors(),
|
|
143
|
+
evaluateArtifactProduction(),
|
|
144
|
+
evaluateDispatchVelocity(),
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
// Overall level: worst of all signals
|
|
148
|
+
const level = signals.some(s => s.level === "red")
|
|
149
|
+
? "red"
|
|
150
|
+
: signals.some(s => s.level === "yellow")
|
|
151
|
+
? "yellow"
|
|
152
|
+
: "green";
|
|
153
|
+
|
|
154
|
+
// Build summary from the most important signals
|
|
155
|
+
const summary = buildSummary(level, signals);
|
|
156
|
+
|
|
157
|
+
return { level, summary, signals };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Compute progress score with additional context from the current unit.
|
|
162
|
+
*/
|
|
163
|
+
export function computeProgressScoreWithContext(context: {
|
|
164
|
+
currentUnitType?: string;
|
|
165
|
+
currentUnitId?: string;
|
|
166
|
+
completedUnits?: number;
|
|
167
|
+
totalUnits?: number;
|
|
168
|
+
retryCount?: number;
|
|
169
|
+
maxRetries?: number;
|
|
170
|
+
}): ProgressScore {
|
|
171
|
+
const base = computeProgressScore();
|
|
172
|
+
|
|
173
|
+
// Add retry signal if available
|
|
174
|
+
if (context.retryCount !== undefined && context.maxRetries !== undefined) {
|
|
175
|
+
const retrySignal: ProgressSignal = context.retryCount === 0
|
|
176
|
+
? { name: "retry_count", level: "green", detail: "No retries" }
|
|
177
|
+
: context.retryCount <= 2
|
|
178
|
+
? { name: "retry_count", level: "yellow", detail: `Retry ${context.retryCount}/${context.maxRetries}` }
|
|
179
|
+
: { name: "retry_count", level: "red", detail: `Retry ${context.retryCount}/${context.maxRetries} — looping` };
|
|
180
|
+
|
|
181
|
+
base.signals.push(retrySignal);
|
|
182
|
+
|
|
183
|
+
// Re-evaluate level
|
|
184
|
+
if (retrySignal.level === "red") base.level = "red";
|
|
185
|
+
else if (retrySignal.level === "yellow" && base.level === "green") base.level = "yellow";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build richer summary with context
|
|
189
|
+
base.summary = buildSummaryWithContext(base.level, base.signals, context);
|
|
190
|
+
|
|
191
|
+
return base;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Formatting ─────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
function buildSummary(level: ProgressLevel, signals: ProgressSignal[]): string {
|
|
197
|
+
switch (level) {
|
|
198
|
+
case "green":
|
|
199
|
+
return "Progressing well";
|
|
200
|
+
case "yellow": {
|
|
201
|
+
const issues = signals.filter(s => s.level === "yellow").map(s => s.detail);
|
|
202
|
+
return `Struggling — ${issues[0] ?? "minor issues detected"}`;
|
|
203
|
+
}
|
|
204
|
+
case "red": {
|
|
205
|
+
const issues = signals.filter(s => s.level === "red").map(s => s.detail);
|
|
206
|
+
return `Stuck — ${issues[0] ?? "critical issues detected"}`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function buildSummaryWithContext(
|
|
212
|
+
level: ProgressLevel,
|
|
213
|
+
signals: ProgressSignal[],
|
|
214
|
+
context: {
|
|
215
|
+
currentUnitType?: string;
|
|
216
|
+
currentUnitId?: string;
|
|
217
|
+
completedUnits?: number;
|
|
218
|
+
totalUnits?: number;
|
|
219
|
+
retryCount?: number;
|
|
220
|
+
maxRetries?: number;
|
|
221
|
+
},
|
|
222
|
+
): string {
|
|
223
|
+
const unitLabel = context.currentUnitId
|
|
224
|
+
? ` ${context.currentUnitId}`
|
|
225
|
+
: "";
|
|
226
|
+
const progressLabel = context.completedUnits !== undefined && context.totalUnits !== undefined
|
|
227
|
+
? ` (${context.completedUnits} of ${context.totalUnits} done)`
|
|
228
|
+
: "";
|
|
229
|
+
|
|
230
|
+
switch (level) {
|
|
231
|
+
case "green":
|
|
232
|
+
return `Progressing well —${unitLabel}${progressLabel}`;
|
|
233
|
+
case "yellow": {
|
|
234
|
+
const issues = signals.filter(s => s.level === "yellow").map(s => s.detail);
|
|
235
|
+
const retryInfo = context.retryCount ? `, attempt ${context.retryCount}/${context.maxRetries}` : "";
|
|
236
|
+
return `Struggling —${unitLabel}${retryInfo}${progressLabel ? ` ${progressLabel}` : ""}, ${issues[0] ?? "issues detected"}`;
|
|
237
|
+
}
|
|
238
|
+
case "red": {
|
|
239
|
+
const issues = signals.filter(s => s.level === "red").map(s => s.detail);
|
|
240
|
+
return `Stuck —${unitLabel}${progressLabel ? ` ${progressLabel}` : ""}, ${issues[0] ?? "critical issues"}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Format progress score as a single-line traffic light for TUI display.
|
|
247
|
+
*/
|
|
248
|
+
export function formatProgressLine(score: ProgressScore): string {
|
|
249
|
+
const icon = score.level === "green" ? "\uD83D\uDFE2"
|
|
250
|
+
: score.level === "yellow" ? "\uD83D\uDFE1"
|
|
251
|
+
: "\uD83D\uDD34";
|
|
252
|
+
return `${icon} ${score.summary}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Format a detailed progress report showing all signals.
|
|
257
|
+
*/
|
|
258
|
+
export function formatProgressReport(score: ProgressScore): string {
|
|
259
|
+
const lines: string[] = [];
|
|
260
|
+
|
|
261
|
+
lines.push(formatProgressLine(score));
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("Signals:");
|
|
264
|
+
|
|
265
|
+
for (const signal of score.signals) {
|
|
266
|
+
const icon = signal.level === "green" ? "\u2705"
|
|
267
|
+
: signal.level === "yellow" ? "\u26A0\uFE0F"
|
|
268
|
+
: "\uD83D\uDED1";
|
|
269
|
+
lines.push(` ${icon} ${signal.name}: ${signal.detail}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor-environment.test.ts — Tests for environment health checks (#1221).
|
|
3
|
+
*
|
|
4
|
+
* Tests:
|
|
5
|
+
* - Node version detection
|
|
6
|
+
* - Dependencies installed check
|
|
7
|
+
* - Env file detection
|
|
8
|
+
* - Port conflict detection
|
|
9
|
+
* - Disk space check
|
|
10
|
+
* - Docker detection
|
|
11
|
+
* - Project tool detection
|
|
12
|
+
* - Doctor issue conversion
|
|
13
|
+
* - Report formatting
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
runEnvironmentChecks,
|
|
22
|
+
runFullEnvironmentChecks,
|
|
23
|
+
environmentResultsToDoctorIssues,
|
|
24
|
+
formatEnvironmentReport,
|
|
25
|
+
checkEnvironmentHealth,
|
|
26
|
+
type EnvironmentCheckResult,
|
|
27
|
+
} from "../doctor-environment.ts";
|
|
28
|
+
import { createTestContext } from "./test-helpers.ts";
|
|
29
|
+
|
|
30
|
+
const { assertEq, assertTrue, assertMatch, report } = createTestContext();
|
|
31
|
+
|
|
32
|
+
function createProjectDir(files: Record<string, string> = {}): string {
|
|
33
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-env-test-"));
|
|
34
|
+
for (const [name, content] of Object.entries(files)) {
|
|
35
|
+
const filePath = join(dir, name);
|
|
36
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
37
|
+
writeFileSync(filePath, content);
|
|
38
|
+
}
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main(): Promise<void> {
|
|
43
|
+
const cleanups: string[] = [];
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// ── Node Version Check ─────────────────────────────────────────────
|
|
47
|
+
console.log("\n=== env: no package.json returns empty ===");
|
|
48
|
+
{
|
|
49
|
+
const dir = createProjectDir();
|
|
50
|
+
cleanups.push(dir);
|
|
51
|
+
const results = runEnvironmentChecks(dir);
|
|
52
|
+
// No package.json → no node checks
|
|
53
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
54
|
+
assertEq(nodeCheck, undefined, "no node version check without package.json");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log("\n=== env: package.json without engines returns no node check ===");
|
|
58
|
+
{
|
|
59
|
+
const dir = createProjectDir({
|
|
60
|
+
"package.json": JSON.stringify({ name: "test", version: "1.0.0" }),
|
|
61
|
+
});
|
|
62
|
+
cleanups.push(dir);
|
|
63
|
+
const results = runEnvironmentChecks(dir);
|
|
64
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
65
|
+
assertEq(nodeCheck, undefined, "no node version check without engines field");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log("\n=== env: package.json with engines returns node check ===");
|
|
69
|
+
{
|
|
70
|
+
const dir = createProjectDir({
|
|
71
|
+
"package.json": JSON.stringify({
|
|
72
|
+
name: "test",
|
|
73
|
+
version: "1.0.0",
|
|
74
|
+
engines: { node: ">=18.0.0" },
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
cleanups.push(dir);
|
|
78
|
+
const results = runEnvironmentChecks(dir);
|
|
79
|
+
const nodeCheck = results.find(r => r.name === "node_version");
|
|
80
|
+
assertTrue(nodeCheck !== undefined, "node version check runs with engines field");
|
|
81
|
+
// Current node should be >= 18 in CI
|
|
82
|
+
assertEq(nodeCheck!.status, "ok", "node version meets requirement");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Dependencies Check ─────────────────────────────────────────────
|
|
86
|
+
console.log("\n=== env: missing node_modules detected ===");
|
|
87
|
+
{
|
|
88
|
+
const dir = createProjectDir({
|
|
89
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
90
|
+
});
|
|
91
|
+
cleanups.push(dir);
|
|
92
|
+
const results = runEnvironmentChecks(dir);
|
|
93
|
+
const depsCheck = results.find(r => r.name === "dependencies");
|
|
94
|
+
assertTrue(depsCheck !== undefined, "dependencies check runs");
|
|
95
|
+
assertEq(depsCheck!.status, "error", "missing node_modules is an error");
|
|
96
|
+
assertTrue(depsCheck!.message.includes("node_modules missing"), "reports missing node_modules");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log("\n=== env: existing node_modules detected ===");
|
|
100
|
+
{
|
|
101
|
+
const dir = createProjectDir({
|
|
102
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
103
|
+
});
|
|
104
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
105
|
+
cleanups.push(dir);
|
|
106
|
+
const results = runEnvironmentChecks(dir);
|
|
107
|
+
const depsCheck = results.find(r => r.name === "dependencies");
|
|
108
|
+
assertTrue(depsCheck !== undefined, "dependencies check runs");
|
|
109
|
+
assertEq(depsCheck!.status, "ok", "existing node_modules is ok");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Env File Check ─────────────────────────────────────────────────
|
|
113
|
+
console.log("\n=== env: .env.example without .env detected ===");
|
|
114
|
+
{
|
|
115
|
+
const dir = createProjectDir({
|
|
116
|
+
".env.example": "DB_URL=xxx\nAPI_KEY=xxx\n",
|
|
117
|
+
});
|
|
118
|
+
cleanups.push(dir);
|
|
119
|
+
const results = runEnvironmentChecks(dir);
|
|
120
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
121
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
122
|
+
assertEq(envCheck!.status, "warning", "missing .env is a warning");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log("\n=== env: .env.example with .env is ok ===");
|
|
126
|
+
{
|
|
127
|
+
const dir = createProjectDir({
|
|
128
|
+
".env.example": "DB_URL=xxx\n",
|
|
129
|
+
".env": "DB_URL=postgres://localhost/test\n",
|
|
130
|
+
});
|
|
131
|
+
cleanups.push(dir);
|
|
132
|
+
const results = runEnvironmentChecks(dir);
|
|
133
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
134
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
135
|
+
assertEq(envCheck!.status, "ok", "present .env is ok");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log("\n=== env: .env.example with .env.local is ok ===");
|
|
139
|
+
{
|
|
140
|
+
const dir = createProjectDir({
|
|
141
|
+
".env.example": "DB_URL=xxx\n",
|
|
142
|
+
".env.local": "DB_URL=postgres://localhost/test\n",
|
|
143
|
+
});
|
|
144
|
+
cleanups.push(dir);
|
|
145
|
+
const results = runEnvironmentChecks(dir);
|
|
146
|
+
const envCheck = results.find(r => r.name === "env_file");
|
|
147
|
+
assertTrue(envCheck !== undefined, "env file check runs");
|
|
148
|
+
assertEq(envCheck!.status, "ok", ".env.local counts as present");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Disk Space Check ───────────────────────────────────────────────
|
|
152
|
+
console.log("\n=== env: disk space check returns result ===");
|
|
153
|
+
if (process.platform !== "win32") {
|
|
154
|
+
const dir = createProjectDir();
|
|
155
|
+
cleanups.push(dir);
|
|
156
|
+
const results = runEnvironmentChecks(dir);
|
|
157
|
+
const diskCheck = results.find(r => r.name === "disk_space");
|
|
158
|
+
assertTrue(diskCheck !== undefined, "disk space check runs on unix");
|
|
159
|
+
// Should be ok on dev machines with reasonable disk
|
|
160
|
+
assertTrue(diskCheck!.status === "ok" || diskCheck!.status === "warning", "disk check returns valid status");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Project Tools Check ────────────────────────────────────────────
|
|
164
|
+
console.log("\n=== env: detects missing python when pyproject.toml exists ===");
|
|
165
|
+
{
|
|
166
|
+
const dir = createProjectDir({
|
|
167
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
168
|
+
"pyproject.toml": "[build-system]\nrequires = ['setuptools']\n",
|
|
169
|
+
});
|
|
170
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
171
|
+
cleanups.push(dir);
|
|
172
|
+
const results = runEnvironmentChecks(dir);
|
|
173
|
+
const pythonCheck = results.find(r => r.name === "python");
|
|
174
|
+
// Python is likely installed on CI/dev machines, so just verify the check runs
|
|
175
|
+
// without error — the result depends on the system
|
|
176
|
+
assertTrue(true, "python check runs without error");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log("\n=== env: detects Cargo.toml ===");
|
|
180
|
+
{
|
|
181
|
+
const dir = createProjectDir({
|
|
182
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
183
|
+
"Cargo.toml": "[package]\nname = 'test'\n",
|
|
184
|
+
});
|
|
185
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
186
|
+
cleanups.push(dir);
|
|
187
|
+
const results = runEnvironmentChecks(dir);
|
|
188
|
+
// Just verify it runs without error
|
|
189
|
+
assertTrue(true, "cargo check runs without error");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Docker Check ───────────────────────────────────────────────────
|
|
193
|
+
console.log("\n=== env: no docker check without Dockerfile ===");
|
|
194
|
+
{
|
|
195
|
+
const dir = createProjectDir({
|
|
196
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
197
|
+
});
|
|
198
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
199
|
+
cleanups.push(dir);
|
|
200
|
+
const results = runEnvironmentChecks(dir);
|
|
201
|
+
const dockerCheck = results.find(r => r.name === "docker");
|
|
202
|
+
assertEq(dockerCheck, undefined, "no docker check without Dockerfile");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log("\n=== env: docker check with Dockerfile ===");
|
|
206
|
+
{
|
|
207
|
+
const dir = createProjectDir({
|
|
208
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
209
|
+
"Dockerfile": "FROM node:22\n",
|
|
210
|
+
});
|
|
211
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
212
|
+
cleanups.push(dir);
|
|
213
|
+
const results = runEnvironmentChecks(dir);
|
|
214
|
+
const dockerCheck = results.find(r => r.name === "docker");
|
|
215
|
+
// Docker may or may not be installed on the test machine
|
|
216
|
+
assertTrue(dockerCheck !== undefined, "docker check runs when Dockerfile present");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Doctor Issue Conversion ────────────────────────────────────────
|
|
220
|
+
console.log("\n=== env: converts results to doctor issues ===");
|
|
221
|
+
{
|
|
222
|
+
const results: EnvironmentCheckResult[] = [
|
|
223
|
+
{ name: "node_version", status: "ok", message: "Node.js v22.0.0" },
|
|
224
|
+
{ name: "dependencies", status: "error", message: "node_modules missing" },
|
|
225
|
+
{ name: "env_file", status: "warning", message: ".env missing", detail: "Copy .env.example" },
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
const issues = environmentResultsToDoctorIssues(results);
|
|
229
|
+
assertEq(issues.length, 2, "only non-ok results converted");
|
|
230
|
+
assertEq(issues[0]!.severity, "error", "error severity preserved");
|
|
231
|
+
assertEq(issues[0]!.code, "env_dependencies", "code prefixed with env_");
|
|
232
|
+
assertEq(issues[1]!.severity, "warning", "warning severity preserved");
|
|
233
|
+
assertTrue(issues[1]!.message.includes("Copy .env.example"), "detail included in message");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── checkEnvironmentHealth integration ──────────────────────────────
|
|
237
|
+
console.log("\n=== env: checkEnvironmentHealth adds issues to array ===");
|
|
238
|
+
{
|
|
239
|
+
const dir = createProjectDir({
|
|
240
|
+
"package.json": JSON.stringify({ name: "test" }),
|
|
241
|
+
});
|
|
242
|
+
cleanups.push(dir);
|
|
243
|
+
|
|
244
|
+
const issues: any[] = [];
|
|
245
|
+
await checkEnvironmentHealth(dir, issues);
|
|
246
|
+
// Should have at least the missing node_modules issue
|
|
247
|
+
assertTrue(issues.some(i => i.code === "env_dependencies"), "environment issues added to array");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Report Formatting ──────────────────────────────────────────────
|
|
251
|
+
console.log("\n=== env: formatEnvironmentReport ===");
|
|
252
|
+
{
|
|
253
|
+
const results: EnvironmentCheckResult[] = [
|
|
254
|
+
{ name: "node_version", status: "ok", message: "Node.js v22.0.0" },
|
|
255
|
+
{ name: "dependencies", status: "error", message: "node_modules missing", detail: "Run npm install" },
|
|
256
|
+
{ name: "disk_space", status: "ok", message: "50.2GB free" },
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const report = formatEnvironmentReport(results);
|
|
260
|
+
assertTrue(report.includes("Environment Health:"), "has header");
|
|
261
|
+
assertTrue(report.includes("Node.js v22.0.0"), "includes ok result");
|
|
262
|
+
assertTrue(report.includes("node_modules missing"), "includes error result");
|
|
263
|
+
assertTrue(report.includes("Run npm install"), "includes detail for errors");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
console.log("\n=== env: formatEnvironmentReport empty ===");
|
|
267
|
+
{
|
|
268
|
+
const report = formatEnvironmentReport([]);
|
|
269
|
+
assertEq(report, "No environment checks applicable.", "empty report message");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Full environment checks include git remote ─────────────────────
|
|
273
|
+
console.log("\n=== env: runFullEnvironmentChecks includes git remote ===");
|
|
274
|
+
{
|
|
275
|
+
// runFullEnvironmentChecks adds git remote check
|
|
276
|
+
// We can't easily test this without a real git repo, but verify it doesn't throw
|
|
277
|
+
const dir = createProjectDir();
|
|
278
|
+
cleanups.push(dir);
|
|
279
|
+
const results = runFullEnvironmentChecks(dir);
|
|
280
|
+
// No git repo → no remote check, but should not throw
|
|
281
|
+
assertTrue(true, "runFullEnvironmentChecks does not throw on non-git dir");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Port Detection from package.json ───────────────────────────────
|
|
285
|
+
console.log("\n=== env: port detection from scripts ===");
|
|
286
|
+
if (process.platform !== "win32") {
|
|
287
|
+
const dir = createProjectDir({
|
|
288
|
+
"package.json": JSON.stringify({
|
|
289
|
+
name: "test",
|
|
290
|
+
scripts: {
|
|
291
|
+
dev: "next dev --port 3456",
|
|
292
|
+
start: "node server.js",
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
});
|
|
296
|
+
mkdirSync(join(dir, "node_modules"), { recursive: true });
|
|
297
|
+
cleanups.push(dir);
|
|
298
|
+
const results = runEnvironmentChecks(dir);
|
|
299
|
+
// Port 3456 is unlikely to be in use, so no conflicts expected
|
|
300
|
+
const portConflicts = results.filter(r => r.name === "port_conflict");
|
|
301
|
+
// Just verify it ran without error
|
|
302
|
+
assertTrue(true, "port check with script-detected ports runs without error");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
} finally {
|
|
306
|
+
for (const dir of cleanups) {
|
|
307
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
report();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
main();
|