@elyracode/doctor 0.4.4
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/README.md +63 -0
- package/extensions/checks.ts +313 -0
- package/extensions/index.ts +111 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @elyracode/doctor
|
|
2
|
+
|
|
3
|
+
Project health analysis for Elyra -- security audit, dependency check, code quality, and more.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
elyra install npm:@elyracode/doctor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `/doctor` -- Run all health checks and send the report to the agent for analysis
|
|
14
|
+
|
|
15
|
+
## Tools
|
|
16
|
+
|
|
17
|
+
| Tool | Description |
|
|
18
|
+
|------|-------------|
|
|
19
|
+
| `project_health_check` | Run health checks. Can filter by category: security, dependencies, config, code-debt, code-quality, git, project |
|
|
20
|
+
|
|
21
|
+
## Checks
|
|
22
|
+
|
|
23
|
+
| Check | What it does |
|
|
24
|
+
|-------|-------------|
|
|
25
|
+
| **Security** | Runs `npm audit` and `composer audit` for known vulnerabilities |
|
|
26
|
+
| **Dependencies** | Checks for outdated npm packages |
|
|
27
|
+
| **Configuration** | Compares .env with .env.example for missing keys |
|
|
28
|
+
| **Code Debt** | Scans for TODO, FIXME, HACK, WORKAROUND comments |
|
|
29
|
+
| **Code Quality** | Finds source files over 500 lines |
|
|
30
|
+
| **Git** | Reports uncommitted changes and untracked files |
|
|
31
|
+
| **Project** | Checks for essential files (.gitignore, README, configs) |
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
> Run a health check on this project
|
|
37
|
+
> Are there any security vulnerabilities?
|
|
38
|
+
> How many TODO comments are in the codebase?
|
|
39
|
+
> Check if my .env is missing any keys
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The agent runs the checks automatically and suggests fixes based on findings.
|
|
43
|
+
|
|
44
|
+
## Report Format
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
# Project Health Report
|
|
48
|
+
|
|
49
|
+
Errors: 1 | Warnings: 3 | Info: 5
|
|
50
|
+
|
|
51
|
+
## Errors
|
|
52
|
+
- [Security] 2 critical/high npm vulnerabilities found
|
|
53
|
+
|
|
54
|
+
## Warnings
|
|
55
|
+
- [Dependencies] 12 outdated npm packages
|
|
56
|
+
- [Code Debt] Found 8 debt markers: 5 TODO, 2 FIXME, 1 HACK
|
|
57
|
+
- [Code Quality] 3 files over 500 lines
|
|
58
|
+
|
|
59
|
+
## Info
|
|
60
|
+
- [Configuration] All .env.example keys present in .env
|
|
61
|
+
- [Git] Working tree clean
|
|
62
|
+
- [Project] All essential files present
|
|
63
|
+
```
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Individual health checks for elyra doctor.
|
|
3
|
+
* Each check runs without LLM -- pure file/process analysis.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { join, relative } from "node:path";
|
|
9
|
+
|
|
10
|
+
export interface Finding {
|
|
11
|
+
severity: "error" | "warning" | "info";
|
|
12
|
+
category: string;
|
|
13
|
+
message: string;
|
|
14
|
+
file?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── Check: Security audit (npm/composer) ──
|
|
18
|
+
|
|
19
|
+
export function checkSecurityAudit(cwd: string): Finding[] {
|
|
20
|
+
const findings: Finding[] = [];
|
|
21
|
+
|
|
22
|
+
// npm audit
|
|
23
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
24
|
+
try {
|
|
25
|
+
const result = execSync("npm audit --json 2>/dev/null", {
|
|
26
|
+
cwd,
|
|
27
|
+
timeout: 30000,
|
|
28
|
+
encoding: "utf-8",
|
|
29
|
+
});
|
|
30
|
+
const audit = JSON.parse(result);
|
|
31
|
+
const vulns = audit.metadata?.vulnerabilities ?? {};
|
|
32
|
+
const critical = (vulns.critical ?? 0) + (vulns.high ?? 0);
|
|
33
|
+
const moderate = vulns.moderate ?? 0;
|
|
34
|
+
const low = vulns.low ?? 0;
|
|
35
|
+
if (critical > 0) {
|
|
36
|
+
findings.push({ severity: "error", category: "Security", message: `${critical} critical/high npm vulnerabilities found` });
|
|
37
|
+
}
|
|
38
|
+
if (moderate > 0) {
|
|
39
|
+
findings.push({ severity: "warning", category: "Security", message: `${moderate} moderate npm vulnerabilities found` });
|
|
40
|
+
}
|
|
41
|
+
if (low > 0) {
|
|
42
|
+
findings.push({ severity: "info", category: "Security", message: `${low} low npm vulnerabilities found` });
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// npm audit returns non-zero when vulnerabilities exist
|
|
46
|
+
try {
|
|
47
|
+
const result = execSync("npm audit --json 2>/dev/null || true", {
|
|
48
|
+
cwd,
|
|
49
|
+
timeout: 30000,
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
});
|
|
52
|
+
if (result.includes('"critical"') || result.includes('"high"')) {
|
|
53
|
+
findings.push({ severity: "error", category: "Security", message: "npm security vulnerabilities detected. Run `npm audit` for details." });
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// Skip if npm audit fails entirely
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// composer audit
|
|
62
|
+
if (existsSync(join(cwd, "composer.json"))) {
|
|
63
|
+
try {
|
|
64
|
+
execSync("composer audit --format=json 2>/dev/null", {
|
|
65
|
+
cwd,
|
|
66
|
+
timeout: 30000,
|
|
67
|
+
encoding: "utf-8",
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error && typeof error === "object" && "status" in error && (error as { status: unknown }).status !== 0) {
|
|
71
|
+
findings.push({ severity: "warning", category: "Security", message: "Composer security vulnerabilities detected. Run `composer audit` for details." });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (findings.length === 0 && (existsSync(join(cwd, "package.json")) || existsSync(join(cwd, "composer.json")))) {
|
|
77
|
+
findings.push({ severity: "info", category: "Security", message: "No known vulnerabilities found" });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return findings;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Check: Outdated dependencies ──
|
|
84
|
+
|
|
85
|
+
export function checkOutdatedDeps(cwd: string): Finding[] {
|
|
86
|
+
const findings: Finding[] = [];
|
|
87
|
+
|
|
88
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
89
|
+
try {
|
|
90
|
+
const result = execSync("npm outdated --json 2>/dev/null || true", {
|
|
91
|
+
cwd,
|
|
92
|
+
timeout: 30000,
|
|
93
|
+
encoding: "utf-8",
|
|
94
|
+
});
|
|
95
|
+
const outdated = JSON.parse(result || "{}");
|
|
96
|
+
const count = Object.keys(outdated).length;
|
|
97
|
+
if (count > 10) {
|
|
98
|
+
findings.push({ severity: "warning", category: "Dependencies", message: `${count} outdated npm packages. Run \`npm outdated\` for details.` });
|
|
99
|
+
} else if (count > 0) {
|
|
100
|
+
findings.push({ severity: "info", category: "Dependencies", message: `${count} outdated npm packages` });
|
|
101
|
+
} else {
|
|
102
|
+
findings.push({ severity: "info", category: "Dependencies", message: "All npm packages are up to date" });
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Skip
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return findings;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Check: Missing .env keys ──
|
|
113
|
+
|
|
114
|
+
export function checkEnvFile(cwd: string): Finding[] {
|
|
115
|
+
const findings: Finding[] = [];
|
|
116
|
+
const examplePath = join(cwd, ".env.example");
|
|
117
|
+
const envPath = join(cwd, ".env");
|
|
118
|
+
|
|
119
|
+
if (!existsSync(examplePath)) return findings;
|
|
120
|
+
|
|
121
|
+
if (!existsSync(envPath)) {
|
|
122
|
+
findings.push({ severity: "error", category: "Configuration", message: ".env file is missing (but .env.example exists)" });
|
|
123
|
+
return findings;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const exampleKeys = parseEnvKeys(readFileSync(examplePath, "utf-8"));
|
|
127
|
+
const envKeys = parseEnvKeys(readFileSync(envPath, "utf-8"));
|
|
128
|
+
const missing = exampleKeys.filter((k) => !envKeys.includes(k));
|
|
129
|
+
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
findings.push({
|
|
132
|
+
severity: "warning",
|
|
133
|
+
category: "Configuration",
|
|
134
|
+
message: `${missing.length} keys in .env.example missing from .env: ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ` (+${missing.length - 5} more)` : ""}`,
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
findings.push({ severity: "info", category: "Configuration", message: "All .env.example keys present in .env" });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return findings;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parseEnvKeys(content: string): string[] {
|
|
144
|
+
return content
|
|
145
|
+
.split("\n")
|
|
146
|
+
.map((l) => l.trim())
|
|
147
|
+
.filter((l) => l && !l.startsWith("#"))
|
|
148
|
+
.map((l) => l.split("=")[0].trim())
|
|
149
|
+
.filter((k) => k.length > 0);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Check: TODO/FIXME/HACK comments ──
|
|
153
|
+
|
|
154
|
+
export function checkCodeDebt(cwd: string): Finding[] {
|
|
155
|
+
const findings: Finding[] = [];
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const result = execSync(
|
|
159
|
+
"grep -rn --include='*.php' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.vue' --include='*.blade.php' -E '(TODO|FIXME|HACK|XXX|WORKAROUND):?' . 2>/dev/null | head -100 || true",
|
|
160
|
+
{ cwd, timeout: 15000, encoding: "utf-8" },
|
|
161
|
+
);
|
|
162
|
+
const lines = result.trim().split("\n").filter((l) => l.length > 0);
|
|
163
|
+
if (lines.length > 0) {
|
|
164
|
+
const todoCount = lines.filter((l) => /TODO/i.test(l)).length;
|
|
165
|
+
const fixmeCount = lines.filter((l) => /FIXME/i.test(l)).length;
|
|
166
|
+
const hackCount = lines.filter((l) => /HACK|WORKAROUND/i.test(l)).length;
|
|
167
|
+
const parts: string[] = [];
|
|
168
|
+
if (todoCount > 0) parts.push(`${todoCount} TODO`);
|
|
169
|
+
if (fixmeCount > 0) parts.push(`${fixmeCount} FIXME`);
|
|
170
|
+
if (hackCount > 0) parts.push(`${hackCount} HACK/WORKAROUND`);
|
|
171
|
+
findings.push({
|
|
172
|
+
severity: hackCount > 0 || fixmeCount > 0 ? "warning" : "info",
|
|
173
|
+
category: "Code Debt",
|
|
174
|
+
message: `Found ${lines.length} debt markers: ${parts.join(", ")}`,
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
findings.push({ severity: "info", category: "Code Debt", message: "No TODO/FIXME/HACK comments found" });
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Skip
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Check: Large files ──
|
|
187
|
+
|
|
188
|
+
export function checkLargeFiles(cwd: string): Finding[] {
|
|
189
|
+
const findings: Finding[] = [];
|
|
190
|
+
const threshold = 500; // lines
|
|
191
|
+
const largeFiles: string[] = [];
|
|
192
|
+
|
|
193
|
+
function scan(dir: string, depth: number): void {
|
|
194
|
+
if (depth > 5) return;
|
|
195
|
+
try {
|
|
196
|
+
for (const entry of readdirSync(dir)) {
|
|
197
|
+
if (entry.startsWith(".") || entry === "node_modules" || entry === "vendor" || entry === "dist" || entry === "build") continue;
|
|
198
|
+
const fullPath = join(dir, entry);
|
|
199
|
+
const stat = statSync(fullPath);
|
|
200
|
+
if (stat.isDirectory()) {
|
|
201
|
+
scan(fullPath, depth + 1);
|
|
202
|
+
} else if (stat.isFile() && /\.(php|ts|tsx|js|jsx|vue)$/.test(entry)) {
|
|
203
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
204
|
+
const lineCount = content.split("\n").length;
|
|
205
|
+
if (lineCount > threshold) {
|
|
206
|
+
largeFiles.push(`${relative(cwd, fullPath)} (${lineCount} lines)`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// Permission errors etc
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
scan(cwd, 0);
|
|
216
|
+
|
|
217
|
+
if (largeFiles.length > 0) {
|
|
218
|
+
findings.push({
|
|
219
|
+
severity: "warning",
|
|
220
|
+
category: "Code Quality",
|
|
221
|
+
message: `${largeFiles.length} files over ${threshold} lines: ${largeFiles.slice(0, 5).join(", ")}${largeFiles.length > 5 ? ` (+${largeFiles.length - 5} more)` : ""}`,
|
|
222
|
+
});
|
|
223
|
+
} else {
|
|
224
|
+
findings.push({ severity: "info", category: "Code Quality", message: `No source files over ${threshold} lines` });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return findings;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Check: Git status ──
|
|
231
|
+
|
|
232
|
+
export function checkGitStatus(cwd: string): Finding[] {
|
|
233
|
+
const findings: Finding[] = [];
|
|
234
|
+
|
|
235
|
+
if (!existsSync(join(cwd, ".git"))) {
|
|
236
|
+
findings.push({ severity: "warning", category: "Git", message: "Not a git repository" });
|
|
237
|
+
return findings;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const status = execSync("git status --porcelain 2>/dev/null", { cwd, timeout: 10000, encoding: "utf-8" });
|
|
242
|
+
const lines = status.trim().split("\n").filter((l) => l.length > 0);
|
|
243
|
+
const untracked = lines.filter((l) => l.startsWith("??")).length;
|
|
244
|
+
const modified = lines.filter((l) => !l.startsWith("??")).length;
|
|
245
|
+
|
|
246
|
+
if (modified > 0) {
|
|
247
|
+
findings.push({ severity: "info", category: "Git", message: `${modified} uncommitted change${modified > 1 ? "s" : ""}` });
|
|
248
|
+
}
|
|
249
|
+
if (untracked > 0) {
|
|
250
|
+
findings.push({ severity: "info", category: "Git", message: `${untracked} untracked file${untracked > 1 ? "s" : ""}` });
|
|
251
|
+
}
|
|
252
|
+
if (modified === 0 && untracked === 0) {
|
|
253
|
+
findings.push({ severity: "info", category: "Git", message: "Working tree clean" });
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// Skip
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return findings;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Check: Missing project essentials ──
|
|
263
|
+
|
|
264
|
+
export function checkProjectEssentials(cwd: string): Finding[] {
|
|
265
|
+
const findings: Finding[] = [];
|
|
266
|
+
|
|
267
|
+
const checks: Array<{ file: string; label: string; severity: Finding["severity"] }> = [
|
|
268
|
+
{ file: ".gitignore", label: ".gitignore", severity: "warning" },
|
|
269
|
+
{ file: "README.md", label: "README.md", severity: "warning" },
|
|
270
|
+
{ file: ".env.example", label: ".env.example", severity: "info" },
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
// Laravel-specific
|
|
274
|
+
if (existsSync(join(cwd, "artisan"))) {
|
|
275
|
+
checks.push(
|
|
276
|
+
{ file: "phpunit.xml", label: "PHPUnit config (phpunit.xml)", severity: "info" },
|
|
277
|
+
{ file: "database/migrations", label: "Database migrations", severity: "info" },
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Node-specific
|
|
282
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
283
|
+
checks.push(
|
|
284
|
+
{ file: "tsconfig.json", label: "TypeScript config", severity: "info" },
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const check of checks) {
|
|
289
|
+
if (!existsSync(join(cwd, check.file))) {
|
|
290
|
+
findings.push({ severity: check.severity, category: "Project", message: `Missing ${check.label}` });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (findings.length === 0) {
|
|
295
|
+
findings.push({ severity: "info", category: "Project", message: "All essential files present" });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return findings;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Run all checks ──
|
|
302
|
+
|
|
303
|
+
export function runAllChecks(cwd: string): Finding[] {
|
|
304
|
+
return [
|
|
305
|
+
...checkSecurityAudit(cwd),
|
|
306
|
+
...checkOutdatedDeps(cwd),
|
|
307
|
+
...checkEnvFile(cwd),
|
|
308
|
+
...checkCodeDebt(cwd),
|
|
309
|
+
...checkLargeFiles(cwd),
|
|
310
|
+
...checkGitStatus(cwd),
|
|
311
|
+
...checkProjectEssentials(cwd),
|
|
312
|
+
];
|
|
313
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@elyracode/coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { type Finding, runAllChecks } from "./checks.js";
|
|
4
|
+
|
|
5
|
+
export default function (elyra: ExtensionAPI): void {
|
|
6
|
+
// ── Command: /doctor ──
|
|
7
|
+
elyra.registerCommand("doctor", {
|
|
8
|
+
description: "Run project health analysis",
|
|
9
|
+
handler: async (_args: string, ctx) => {
|
|
10
|
+
ctx.ui.notify("Running health checks...");
|
|
11
|
+
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const findings = runAllChecks(cwd);
|
|
14
|
+
const report = formatReport(findings);
|
|
15
|
+
|
|
16
|
+
elyra.sendUserMessage(
|
|
17
|
+
`Here is the project health report. Analyze the findings and suggest the most important fixes:\n\n${report}`,
|
|
18
|
+
);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ── Tool: project_health_check ──
|
|
23
|
+
elyra.registerTool({
|
|
24
|
+
name: "project_health_check",
|
|
25
|
+
label: "Project Health Check",
|
|
26
|
+
description:
|
|
27
|
+
"Analyze the current project for security vulnerabilities, outdated dependencies, " +
|
|
28
|
+
"missing configuration, code debt (TODO/FIXME/HACK), large files, and git status. " +
|
|
29
|
+
"Use this when the user asks about project health, code quality, or wants an audit.",
|
|
30
|
+
parameters: Type.Object({
|
|
31
|
+
category: Type.Optional(
|
|
32
|
+
Type.Union(
|
|
33
|
+
[
|
|
34
|
+
Type.Literal("all"),
|
|
35
|
+
Type.Literal("security"),
|
|
36
|
+
Type.Literal("dependencies"),
|
|
37
|
+
Type.Literal("config"),
|
|
38
|
+
Type.Literal("code-debt"),
|
|
39
|
+
Type.Literal("code-quality"),
|
|
40
|
+
Type.Literal("git"),
|
|
41
|
+
Type.Literal("project"),
|
|
42
|
+
],
|
|
43
|
+
{ description: "Check category to run (default: all)" },
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
}),
|
|
47
|
+
execute: async (_toolCallId, params) => {
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const findings = runAllChecks(cwd);
|
|
50
|
+
|
|
51
|
+
const filtered =
|
|
52
|
+
!params.category || params.category === "all"
|
|
53
|
+
? findings
|
|
54
|
+
: findings.filter((f) => f.category.toLowerCase().replace(/\s+/g, "-") === params.category);
|
|
55
|
+
|
|
56
|
+
const report = formatReport(filtered);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: report }],
|
|
60
|
+
details: {
|
|
61
|
+
errors: filtered.filter((f) => f.severity === "error").length,
|
|
62
|
+
warnings: filtered.filter((f) => f.severity === "warning").length,
|
|
63
|
+
info: filtered.filter((f) => f.severity === "info").length,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatReport(findings: Finding[]): string {
|
|
71
|
+
const errors = findings.filter((f) => f.severity === "error");
|
|
72
|
+
const warnings = findings.filter((f) => f.severity === "warning");
|
|
73
|
+
const infos = findings.filter((f) => f.severity === "info");
|
|
74
|
+
|
|
75
|
+
const lines: string[] = [
|
|
76
|
+
"# Project Health Report",
|
|
77
|
+
"",
|
|
78
|
+
`Errors: ${errors.length} | Warnings: ${warnings.length} | Info: ${infos.length}`,
|
|
79
|
+
"",
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
if (errors.length > 0) {
|
|
83
|
+
lines.push("## Errors");
|
|
84
|
+
for (const f of errors) {
|
|
85
|
+
lines.push(`- [${f.category}] ${f.message}${f.file ? ` (${f.file})` : ""}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push("");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (warnings.length > 0) {
|
|
91
|
+
lines.push("## Warnings");
|
|
92
|
+
for (const f of warnings) {
|
|
93
|
+
lines.push(`- [${f.category}] ${f.message}${f.file ? ` (${f.file})` : ""}`);
|
|
94
|
+
}
|
|
95
|
+
lines.push("");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (infos.length > 0) {
|
|
99
|
+
lines.push("## Info");
|
|
100
|
+
for (const f of infos) {
|
|
101
|
+
lines.push(`- [${f.category}] ${f.message}${f.file ? ` (${f.file})` : ""}`);
|
|
102
|
+
}
|
|
103
|
+
lines.push("");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (findings.length === 0) {
|
|
107
|
+
lines.push("No findings. Project looks healthy.");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elyracode/doctor",
|
|
3
|
+
"version": "0.4.4",
|
|
4
|
+
"description": "Elyra extension for project health analysis -- dependency audit, security checks, code quality, and more",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"elyra-package",
|
|
8
|
+
"doctor",
|
|
9
|
+
"health-check",
|
|
10
|
+
"audit",
|
|
11
|
+
"code-quality"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Knut W. Horne",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/kwhorne/elyra.git",
|
|
18
|
+
"directory": "packages/doctor"
|
|
19
|
+
},
|
|
20
|
+
"elyra": {
|
|
21
|
+
"extensions": [
|
|
22
|
+
"./extensions/index.ts"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@elyracode/coding-agent": "*",
|
|
27
|
+
"typebox": "*"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"clean": "echo 'nothing to clean'",
|
|
31
|
+
"build": "echo 'nothing to build'",
|
|
32
|
+
"check": "echo 'nothing to check'"
|
|
33
|
+
}
|
|
34
|
+
}
|