Skip to content

Commit 79e91ec

Browse files
committed
Improve doctor diagnostics
1 parent fea6e5a commit 79e91ec

6 files changed

Lines changed: 300 additions & 22 deletions

File tree

PRD.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ DevMap version 0.1.0 ✓
414414
Node.js version 20.11.0 ✓
415415
Provider Groq ✓
416416
API key valid ✓
417-
Model qwen-2.5-coder-32b
417+
Model openai/gpt-oss-20b
418418
Snapshot exists ✓
419419
420420
No issues found.

docs/for-me-personal/PROGRESS.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,19 @@ Terakhir diperbarui: 2026-06-11
6161
baru.
6262
- `--fresh` memaksa static analysis dan AI interpretation baru.
6363
- Jika AI gagal, static analysis dan snapshot tetap berhasil.
64-
- Automated test saat ini berjumlah 29 dan seluruhnya lulus.
64+
- Automated test AI memakai mock provider dan tidak menggunakan quota.
65+
66+
### Doctor Diagnostics
67+
68+
- `devmap doctor` sekarang menampilkan versi DevMap, Node.js, OS/arsitektur,
69+
lokasi project, framework, package manager, provider, config, dan snapshot.
70+
- Node.js di bawah versi 18 ditandai sebagai unsupported.
71+
- Model `auto` di-resolve ke model aktual `openai/gpt-oss-20b`.
72+
- API key divalidasi melalui endpoint daftar model Groq.
73+
- Availability selected model ikut diperiksa.
74+
- Snapshot dibedakan menjadi valid, missing, corrupt, dan unsupported schema.
75+
- API key dan raw stack trace tidak pernah ditampilkan.
76+
- Automated test saat ini berjumlah 32 dan seluruhnya lulus.
6577

6678
### Cross-Platform CI
6779

@@ -87,9 +99,9 @@ Terakhir diperbarui: 2026-06-11
8799
### Prioritas Berikutnya
88100

89101
1. Pastikan rerun GitHub Actions hijau pada seluruh 9 kombinasi.
90-
2. Tingkatkan `doctor` untuk memvalidasi provider dan selected model.
102+
2. Rapikan package npm dan verifikasi tarball sebelum publish.
91103
3. Tambahkan streaming output untuk jawaban AI.
92-
4. Rapikan package npm dan verifikasi tarball sebelum publish.
104+
4. Lakukan manual verification Groq pada project nyata.
93105

94106
## Status Saat Ini
95107

docs/for-me-personal/TEST.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ Saat ini test mencakup:
7878
Hasil minimum yang diharapkan:
7979

8080
```text
81-
tests 29
82-
pass 29
81+
tests 32
82+
pass 32
8383
fail 0
8484
```
8585

@@ -120,8 +120,8 @@ npx -p node@20 node packages\cli\node_modules\tsx\dist\cli.mjs packages\cli\test
120120
Hasil minimum yang diharapkan untuk keduanya:
121121

122122
```text
123-
tests 29
124-
pass 29
123+
tests 32
124+
pass 32
125125
fail 0
126126
```
127127

@@ -187,6 +187,27 @@ Pastikan:
187187
- `--deep --fresh` memakai model deep analysis;
188188
- static snapshot tetap tersimpan jika Groq gagal.
189189

190+
## Testing `devmap doctor`
191+
192+
Jalankan automated test dan hasil build:
193+
194+
```powershell
195+
pnpm test:cli
196+
pnpm build:cli
197+
node packages\cli\dist\index.js doctor
198+
```
199+
200+
Pastikan output menampilkan:
201+
202+
- versi DevMap dan Node.js;
203+
- OS dan arsitektur;
204+
- lokasi project, framework, dan package manager;
205+
- provider, status API key, dan selected model;
206+
- status snapshot;
207+
- warning actionable ketika config, key, model, atau snapshot bermasalah.
208+
209+
API key asli dan raw stack trace tidak boleh muncul pada output.
210+
190211
## GitHub Actions
191212

192213
Workflow:

packages/cli/src/ai/groq.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export type GroqClientDependencies = {
2323
sleep?: (milliseconds: number) => Promise<void>;
2424
};
2525

26+
export type GroqProviderInspection = {
27+
reachable: true;
28+
modelAvailable: boolean;
29+
};
30+
2631
export class GroqClient implements AiClient {
2732
private readonly fetchImplementation: typeof fetch;
2833
private readonly sleep: (milliseconds: number) => Promise<void>;
@@ -130,10 +135,19 @@ export class GroqClient implements AiClient {
130135
}
131136

132137
export async function validateGroqApiKey(apiKey: string): Promise<void> {
138+
await inspectGroqProvider(apiKey);
139+
}
140+
141+
export async function inspectGroqProvider(
142+
apiKey: string,
143+
model?: string,
144+
dependencies: Pick<GroqClientDependencies, "fetch"> = {}
145+
): Promise<GroqProviderInspection> {
146+
const fetchImplementation = dependencies.fetch ?? fetch;
133147
let response: Response;
134148

135149
try {
136-
response = await fetch(GROQ_MODELS_URL, {
150+
response = await fetchImplementation(GROQ_MODELS_URL, {
137151
headers: {
138152
Authorization: `Bearer ${apiKey}`
139153
}
@@ -158,6 +172,12 @@ export async function validateGroqApiKey(apiKey: string): Promise<void> {
158172
"Try again shortly or check https://status.groq.com."
159173
);
160174
}
175+
176+
const modelIds = await readModelIds(response);
177+
return {
178+
reachable: true,
179+
modelAvailable: !model || modelIds.includes(model)
180+
};
161181
}
162182

163183
type GroqCompletionPayload = {
@@ -265,3 +285,20 @@ function normalizeUsage(usage: NonNullable<GroqCompletionPayload["usage"]>): AiT
265285
function wait(milliseconds: number): Promise<void> {
266286
return new Promise((resolve) => setTimeout(resolve, milliseconds));
267287
}
288+
289+
async function readModelIds(response: Response): Promise<string[]> {
290+
try {
291+
const payload = await response.json() as {
292+
data?: Array<{ id?: unknown }>;
293+
};
294+
295+
return (payload.data ?? [])
296+
.map((model) => model.id)
297+
.filter((id): id is string => typeof id === "string");
298+
} catch {
299+
throw new DevmapError(
300+
"Groq returned an unreadable model list.",
301+
"Try again shortly or check https://status.groq.com."
302+
);
303+
}
304+
}
Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,110 @@
1-
import { existsSync } from "node:fs";
2-
import { readConfig, getConfigPath } from "../utils/config.js";
3-
import { getSnapshotPath } from "../cache/snapshot.js";
1+
import { arch, platform } from "node:os";
2+
import { resolve } from "node:path";
3+
import {
4+
DEFAULT_AI_MODELS,
5+
inspectGroqProvider,
6+
type GroqProviderInspection
7+
} from "../ai/groq.js";
8+
import { scanFiles } from "../analyzers/fileScanner.js";
9+
import { detectFramework } from "../analyzers/frameworkDetector.js";
10+
import { detectProjectMetadata } from "../analyzers/projectMetadata.js";
11+
import { inspectSnapshot } from "../cache/snapshot.js";
12+
import { readConfig, type DevmapConfig } from "../utils/config.js";
13+
import { DevmapError } from "../utils/errors.js";
414
import { output } from "../utils/output.js";
515

6-
export async function doctorCommand(): Promise<void> {
7-
const config = await readConfig();
16+
const DEVMAP_VERSION = "0.1.0";
17+
const MINIMUM_NODE_MAJOR = 18;
18+
19+
export type DoctorDependencies = {
20+
projectRoot?: string;
21+
loadConfig?: () => Promise<DevmapConfig | null>;
22+
inspectProvider?: (
23+
apiKey: string,
24+
model: string
25+
) => Promise<GroqProviderInspection>;
26+
};
27+
28+
export async function doctorCommand(
29+
dependencies: DoctorDependencies = {}
30+
): Promise<void> {
31+
const projectRoot = resolve(dependencies.projectRoot ?? process.cwd());
32+
const loadConfig = dependencies.loadConfig ?? readConfig;
33+
const inspectProvider = dependencies.inspectProvider ?? inspectGroqProvider;
34+
const [config, snapshotResult, files] = await Promise.all([
35+
loadConfig(),
36+
inspectSnapshot(projectRoot),
37+
scanFiles(projectRoot)
38+
]);
39+
const framework = detectFramework(files);
40+
const project = detectProjectMetadata(projectRoot, framework, files);
41+
const selectedModel = config?.model === "auto"
42+
? DEFAULT_AI_MODELS.ask
43+
: config?.model;
44+
const issues: string[] = [];
45+
const nodeSupported = readNodeMajor(process.version) >= MINIMUM_NODE_MAJOR;
846

947
output.section("DevMap Doctor");
10-
output.keyValue("Node.js", process.version);
48+
output.keyValue("DevMap", DEVMAP_VERSION);
49+
output.keyValue("Node.js", `${process.version} (${nodeSupported ? "supported" : "unsupported"})`);
50+
output.keyValue("OS", `${platform()}/${arch()}`);
51+
output.keyValue("Project", project.name);
52+
output.keyValue("Framework", framework);
53+
output.keyValue("Package Manager", project.packageManager);
1154
output.keyValue("Provider", config?.provider ?? "not configured");
12-
output.keyValue("API key", config?.apiKey ? "set" : "not set");
13-
output.keyValue("Config", existsSync(getConfigPath()) ? "exists" : "missing");
14-
output.keyValue("Snapshot", existsSync(getSnapshotPath(process.cwd())) ? "exists" : "missing");
55+
output.keyValue("Config", config ? "exists" : "missing");
56+
output.keyValue("Snapshot", snapshotResult.status);
1557

1658
if (!config) {
17-
output.warning("Run devmap init to create ~/.devmap/config.json");
18-
} else if (!config.apiKey) {
19-
output.warning("Groq API key is not set yet. Add it before Phase 2 AI commands.");
59+
issues.push("Run devmap init to create ~/.devmap/config.json.");
60+
output.keyValue("API key", "not configured");
61+
output.keyValue("Model", "not configured");
62+
} else if (!config.apiKey || !selectedModel) {
63+
issues.push("Run devmap init again to configure Groq.");
64+
output.keyValue("API key", "missing");
65+
output.keyValue("Model", selectedModel ?? "not configured");
2066
} else {
21-
output.success("Base configuration looks ready");
67+
try {
68+
const provider = await inspectProvider(config.apiKey, selectedModel);
69+
output.keyValue("API key", provider.reachable ? "valid" : "unreachable");
70+
output.keyValue(
71+
"Model",
72+
provider.modelAvailable ? selectedModel : `unavailable: ${selectedModel}`
73+
);
74+
75+
if (!provider.modelAvailable) {
76+
issues.push("Run devmap init or choose an available Groq model.");
77+
}
78+
} catch (error) {
79+
const message = error instanceof DevmapError
80+
? error.message
81+
: "Provider diagnostics failed.";
82+
output.keyValue("API key", "invalid or unreachable");
83+
output.keyValue("Model", selectedModel);
84+
issues.push(message);
85+
}
86+
}
87+
88+
if (!nodeSupported) {
89+
issues.push(`Install Node.js ${MINIMUM_NODE_MAJOR} or newer.`);
90+
}
91+
92+
if (snapshotResult.status === "corrupt" || snapshotResult.status === "unsupported") {
93+
issues.push("Run devmap analyze --fresh to regenerate the snapshot.");
2294
}
95+
96+
if (issues.length === 0) {
97+
output.success("No issues found");
98+
return;
99+
}
100+
101+
output.section("Issues");
102+
for (const issue of issues) {
103+
output.warning(issue);
104+
}
105+
}
106+
107+
function readNodeMajor(version: string): number {
108+
const match = version.match(/^v?(\d+)/);
109+
return match ? Number(match[1]) : 0;
23110
}

0 commit comments

Comments
 (0)