@compr/contextengine-mcp 1.9.44

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -0
  3. package/dist/agents.d.ts +127 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/agents.js +1162 -0
  6. package/dist/agents.js.map +1 -0
  7. package/dist/cache.d.ts +15 -0
  8. package/dist/cache.d.ts.map +1 -0
  9. package/dist/cache.js +117 -0
  10. package/dist/cache.js.map +1 -0
  11. package/dist/cli.d.ts +10 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +227 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/code-chunker.d.ts +12 -0
  16. package/dist/code-chunker.d.ts.map +1 -0
  17. package/dist/code-chunker.js +263 -0
  18. package/dist/code-chunker.js.map +1 -0
  19. package/dist/collectors.d.ts +63 -0
  20. package/dist/collectors.d.ts.map +1 -0
  21. package/dist/collectors.js +617 -0
  22. package/dist/collectors.js.map +1 -0
  23. package/dist/config.d.ts +59 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +213 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/embeddings.d.ts +31 -0
  28. package/dist/embeddings.d.ts.map +1 -0
  29. package/dist/embeddings.js +91 -0
  30. package/dist/embeddings.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +557 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/ingest.d.ts +23 -0
  36. package/dist/ingest.d.ts.map +1 -0
  37. package/dist/ingest.js +79 -0
  38. package/dist/ingest.js.map +1 -0
  39. package/dist/search.d.ts +11 -0
  40. package/dist/search.d.ts.map +1 -0
  41. package/dist/search.js +58 -0
  42. package/dist/search.js.map +1 -0
  43. package/dist/sessions.d.ts +46 -0
  44. package/dist/sessions.d.ts.map +1 -0
  45. package/dist/sessions.js +143 -0
  46. package/dist/sessions.js.map +1 -0
  47. package/dist/test-sessions.d.ts +2 -0
  48. package/dist/test-sessions.d.ts.map +1 -0
  49. package/dist/test-sessions.js.map +1 -0
  50. package/dist/test.d.ts +2 -0
  51. package/dist/test.d.ts.map +1 -0
  52. package/dist/test.js +52 -0
  53. package/dist/test.js.map +1 -0
  54. package/package.json +58 -0
package/dist/agents.js ADDED
@@ -0,0 +1,1162 @@
1
+ import { execSync } from "child_process";
2
+ import { readFileSync, existsSync, readdirSync } from "fs";
3
+ import { resolve, join } from "path";
4
+ // ---------------------------------------------------------------------------
5
+ // Helpers
6
+ // ---------------------------------------------------------------------------
7
+ function exec(cmd, cwd) {
8
+ try {
9
+ return execSync(cmd, {
10
+ cwd,
11
+ encoding: "utf-8",
12
+ timeout: 10_000,
13
+ stdio: ["pipe", "pipe", "pipe"],
14
+ }).trim();
15
+ }
16
+ catch {
17
+ return "";
18
+ }
19
+ }
20
+ // ---------------------------------------------------------------------------
21
+ // Project Discovery & Analysis
22
+ // ---------------------------------------------------------------------------
23
+ /**
24
+ * Analyze a project directory and determine its type, framework, runtime.
25
+ */
26
+ export function analyzeProject(dir) {
27
+ const info = {
28
+ name: dir.name,
29
+ path: dir.path,
30
+ type: "unknown",
31
+ framework: "unknown",
32
+ runtime: "unknown",
33
+ hasGit: existsSync(join(dir.path, ".git")),
34
+ gitRemotes: [],
35
+ hasDocker: existsSync(join(dir.path, "Dockerfile")) || existsSync(join(dir.path, "docker-compose.yml")),
36
+ hasPm2: existsSync(join(dir.path, "ecosystem.config.js")) || existsSync(join(dir.path, "ecosystem.config.cjs")),
37
+ deps: {},
38
+ };
39
+ // Git remotes
40
+ if (info.hasGit) {
41
+ const remotes = exec("git remote -v", dir.path);
42
+ info.gitRemotes = [...new Set(remotes.split("\n").map(l => l.split(/\s+/)[0]).filter(Boolean))];
43
+ }
44
+ // Node.js project
45
+ const pkgPath = join(dir.path, "package.json");
46
+ if (existsSync(pkgPath)) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
49
+ info.type = "node";
50
+ info.runtime = `node`;
51
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
52
+ // Detect framework
53
+ if (allDeps["next"]) {
54
+ info.framework = "next.js";
55
+ info.deps["next"] = allDeps["next"];
56
+ }
57
+ else if (allDeps["expo"]) {
58
+ info.framework = "expo";
59
+ info.deps["expo"] = allDeps["expo"];
60
+ }
61
+ else if (allDeps["react-scripts"]) {
62
+ info.framework = "react-cra";
63
+ info.deps["react-scripts"] = allDeps["react-scripts"];
64
+ }
65
+ else if (allDeps["vite"]) {
66
+ info.framework = "vite";
67
+ info.deps["vite"] = allDeps["vite"];
68
+ }
69
+ else if (allDeps["@modelcontextprotocol/sdk"]) {
70
+ info.framework = "mcp-server";
71
+ info.deps["@modelcontextprotocol/sdk"] = allDeps["@modelcontextprotocol/sdk"];
72
+ }
73
+ else if (allDeps["express"]) {
74
+ info.framework = "express";
75
+ info.deps["express"] = allDeps["express"];
76
+ }
77
+ else if (allDeps["fastify"]) {
78
+ info.framework = "fastify";
79
+ info.deps["fastify"] = allDeps["fastify"];
80
+ }
81
+ // Key deps
82
+ if (allDeps["react"])
83
+ info.deps["react"] = allDeps["react"];
84
+ if (allDeps["typescript"])
85
+ info.deps["typescript"] = allDeps["typescript"];
86
+ if (allDeps["vue"])
87
+ info.deps["vue"] = allDeps["vue"];
88
+ if (allDeps["@mui/material"])
89
+ info.deps["@mui/material"] = allDeps["@mui/material"];
90
+ if (allDeps["@material-ui/core"])
91
+ info.deps["@material-ui/core"] = allDeps["@material-ui/core"];
92
+ }
93
+ catch { /* ignore */ }
94
+ }
95
+ // PHP project
96
+ const composerPath = join(dir.path, "composer.json");
97
+ if (existsSync(composerPath)) {
98
+ try {
99
+ const composer = JSON.parse(readFileSync(composerPath, "utf-8"));
100
+ info.type = "php";
101
+ info.runtime = "php";
102
+ const allDeps = { ...composer.require, ...composer["require-dev"] };
103
+ if (allDeps["laravel/framework"]) {
104
+ info.framework = "laravel";
105
+ info.deps["laravel/framework"] = allDeps["laravel/framework"];
106
+ }
107
+ else if (allDeps["symfony/framework-bundle"]) {
108
+ info.framework = "symfony";
109
+ }
110
+ }
111
+ catch { /* ignore */ }
112
+ }
113
+ // Python project
114
+ const pyProjectPath = join(dir.path, "pyproject.toml");
115
+ const requirementsPath = join(dir.path, "requirements.txt");
116
+ if (existsSync(pyProjectPath) || existsSync(requirementsPath)) {
117
+ info.type = "python";
118
+ info.runtime = "python";
119
+ if (existsSync(requirementsPath)) {
120
+ const reqs = readFileSync(requirementsPath, "utf-8");
121
+ if (reqs.includes("fastapi"))
122
+ info.framework = "fastapi";
123
+ else if (reqs.includes("django"))
124
+ info.framework = "django";
125
+ else if (reqs.includes("flask"))
126
+ info.framework = "flask";
127
+ }
128
+ }
129
+ // Flutter project
130
+ if (existsSync(join(dir.path, "pubspec.yaml"))) {
131
+ info.type = "flutter";
132
+ info.framework = "flutter";
133
+ info.runtime = "dart";
134
+ }
135
+ // Flutter web build (compiled only)
136
+ if (existsSync(join(dir.path, "main.dart.js")) && existsSync(join(dir.path, "flutter_service_worker.js"))) {
137
+ info.type = "flutter";
138
+ info.framework = "flutter-web-build";
139
+ info.runtime = "static";
140
+ }
141
+ return info;
142
+ }
143
+ /**
144
+ * List all projects with tech analysis.
145
+ */
146
+ export function listProjects(projectDirs) {
147
+ return projectDirs.map(analyzeProject);
148
+ }
149
+ /**
150
+ * Scan all projects for port declarations and detect conflicts.
151
+ */
152
+ export function checkPorts(projectDirs) {
153
+ const allPorts = [];
154
+ for (const dir of projectDirs) {
155
+ // ecosystem.config.js — parse port from args or env
156
+ for (const ecFile of ["ecosystem.config.js", "ecosystem.config.cjs"]) {
157
+ const ecPath = join(dir.path, ecFile);
158
+ if (!existsSync(ecPath))
159
+ continue;
160
+ const content = readFileSync(ecPath, "utf-8");
161
+ // Match port patterns: --port 8000, PORT=8000, port: 8000, WEB_PORT=19012
162
+ const portPatterns = [
163
+ /--port\s+(\d+)/g,
164
+ /PORT[=:]\s*['"]?(\d+)/gi,
165
+ /WEB_PORT[=:]\s*['"]?(\d+)/gi,
166
+ /RCT_METRO_PORT[=:]\s*['"]?(\d+)/gi,
167
+ /port:\s*(\d+)/g,
168
+ ];
169
+ for (const regex of portPatterns) {
170
+ let match;
171
+ while ((match = regex.exec(content)) !== null) {
172
+ const port = parseInt(match[1], 10);
173
+ if (port > 0 && port < 65536) {
174
+ // Try to extract the app name from context
175
+ const lines = content.split("\n");
176
+ const matchLine = content.substring(0, match.index).split("\n").length - 1;
177
+ let appName = dir.name;
178
+ for (let i = matchLine; i >= Math.max(0, matchLine - 10); i--) {
179
+ const nameMatch = lines[i]?.match(/name:\s*['"]([^'"]+)['"]/);
180
+ if (nameMatch) {
181
+ appName = nameMatch[1];
182
+ break;
183
+ }
184
+ }
185
+ allPorts.push({
186
+ port,
187
+ project: dir.name,
188
+ source: ecFile,
189
+ details: appName,
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
195
+ // docker-compose.yml — published ports
196
+ for (const dcFile of ["docker-compose.yml", "docker-compose.yaml", "docker-compose.prod.yml"]) {
197
+ const dcPath = join(dir.path, dcFile);
198
+ if (!existsSync(dcPath))
199
+ continue;
200
+ const content = readFileSync(dcPath, "utf-8");
201
+ const portMatches = content.matchAll(/["']?(\d+):(\d+)["']?/g);
202
+ for (const m of portMatches) {
203
+ const hostPort = parseInt(m[1], 10);
204
+ if (hostPort > 0 && hostPort < 65536) {
205
+ allPorts.push({
206
+ port: hostPort,
207
+ project: dir.name,
208
+ source: dcFile,
209
+ details: `host:${m[1]}→container:${m[2]}`,
210
+ });
211
+ }
212
+ }
213
+ }
214
+ // .env — PORT= or APP_PORT= or DB_PORT=
215
+ const envPath = join(dir.path, ".env");
216
+ if (existsSync(envPath)) {
217
+ const content = readFileSync(envPath, "utf-8");
218
+ const portLines = content.match(/^(?:APP_)?(?:PORT|DB_PORT|REDIS_PORT)\s*=\s*(\d+)$/gm);
219
+ if (portLines) {
220
+ for (const line of portLines) {
221
+ const [key, val] = line.split("=").map(s => s.trim());
222
+ const port = parseInt(val, 10);
223
+ if (port > 0 && port < 65536) {
224
+ allPorts.push({
225
+ port,
226
+ project: dir.name,
227
+ source: ".env",
228
+ details: key,
229
+ });
230
+ }
231
+ }
232
+ }
233
+ }
234
+ // package.json — "start" script with --port
235
+ const pkgPath = join(dir.path, "package.json");
236
+ if (existsSync(pkgPath)) {
237
+ try {
238
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
239
+ const scripts = pkg.scripts || {};
240
+ for (const [scriptName, scriptCmd] of Object.entries(scripts)) {
241
+ const cmd = String(scriptCmd);
242
+ const portMatch = cmd.match(/--port\s+(\d+)/);
243
+ if (portMatch) {
244
+ allPorts.push({
245
+ port: parseInt(portMatch[1], 10),
246
+ project: dir.name,
247
+ source: "package.json",
248
+ details: `script: ${scriptName}`,
249
+ });
250
+ }
251
+ }
252
+ }
253
+ catch { /* ignore */ }
254
+ }
255
+ }
256
+ // Deduplicate (same port+project+source = one entry)
257
+ const seen = new Set();
258
+ const dedupPorts = allPorts.filter(p => {
259
+ const key = `${p.port}:${p.project}:${p.source}`;
260
+ if (seen.has(key))
261
+ return false;
262
+ seen.add(key);
263
+ return true;
264
+ });
265
+ // Detect conflicts (same port used by different projects)
266
+ const portMap = new Map();
267
+ for (const p of dedupPorts) {
268
+ const list = portMap.get(p.port) || [];
269
+ list.push(p);
270
+ portMap.set(p.port, list);
271
+ }
272
+ const conflicts = [];
273
+ for (const [port, usages] of portMap) {
274
+ const uniqueProjects = new Set(usages.map(u => u.project));
275
+ if (uniqueProjects.size > 1) {
276
+ conflicts.push({ port, usages });
277
+ }
278
+ }
279
+ return { ports: dedupPorts, conflicts };
280
+ }
281
+ // ---------------------------------------------------------------------------
282
+ // Compliance Agent — Audits
283
+ // ---------------------------------------------------------------------------
284
+ /**
285
+ * Check git remotes: every project should have both 'origin' (GitHub) and 'gdrive'.
286
+ */
287
+ function auditGitRemotes(projects) {
288
+ const findings = [];
289
+ for (const p of projects) {
290
+ if (!p.hasGit) {
291
+ findings.push({
292
+ check: "git-remotes",
293
+ status: "warn",
294
+ message: `${p.name}: Not a git repository`,
295
+ project: p.name,
296
+ severity: "medium",
297
+ remediation: `cd ${p.path} && git init && git remote add origin <url>`,
298
+ });
299
+ continue;
300
+ }
301
+ const hasOrigin = p.gitRemotes.includes("origin");
302
+ const hasGdrive = p.gitRemotes.includes("gdrive");
303
+ if (hasOrigin && hasGdrive) {
304
+ findings.push({
305
+ check: "git-remotes",
306
+ status: "pass",
307
+ message: `${p.name}: origin ✅ gdrive ✅`,
308
+ project: p.name,
309
+ severity: "info",
310
+ });
311
+ }
312
+ else {
313
+ const missing = [];
314
+ if (!hasOrigin)
315
+ missing.push("origin");
316
+ if (!hasGdrive)
317
+ missing.push("gdrive");
318
+ findings.push({
319
+ check: "git-remotes",
320
+ status: "fail",
321
+ message: `${p.name}: Missing remotes: ${missing.join(", ")}`,
322
+ project: p.name,
323
+ severity: "high",
324
+ remediation: missing.map(r => `cd ${p.path} && git remote add ${r} <url>`).join("\n"),
325
+ });
326
+ }
327
+ }
328
+ return findings;
329
+ }
330
+ /**
331
+ * Check for post-commit hook (auto-push to all remotes).
332
+ * Also audits hook quality: error handling, gdrive best-effort pattern.
333
+ */
334
+ function auditGitHooks(projects) {
335
+ const findings = [];
336
+ for (const p of projects) {
337
+ if (!p.hasGit)
338
+ continue;
339
+ // Check both .git/hooks/ and hooks/ (custom dir)
340
+ const hookPaths = [
341
+ join(p.path, ".git", "hooks", "post-commit"),
342
+ join(p.path, "hooks", "post-commit"),
343
+ ];
344
+ let foundHook = false;
345
+ let hookContent = "";
346
+ for (const hookPath of hookPaths) {
347
+ if (existsSync(hookPath)) {
348
+ hookContent = readFileSync(hookPath, "utf-8");
349
+ if (hookContent.includes("push") || hookContent.includes("remote")) {
350
+ foundHook = true;
351
+ break;
352
+ }
353
+ }
354
+ }
355
+ // Also check git config for core.hooksPath
356
+ const hooksPath = exec("git config core.hooksPath", p.path);
357
+ if (hooksPath) {
358
+ const customHook = resolve(p.path, hooksPath, "post-commit");
359
+ if (existsSync(customHook)) {
360
+ foundHook = true;
361
+ if (!hookContent)
362
+ hookContent = readFileSync(customHook, "utf-8");
363
+ }
364
+ }
365
+ findings.push({
366
+ check: "git-hooks",
367
+ status: foundHook ? "pass" : "warn",
368
+ message: `${p.name}: post-commit auto-push ${foundHook ? "✅" : "⚠ not found"}`,
369
+ project: p.name,
370
+ severity: foundHook ? "info" : "low",
371
+ remediation: foundHook ? undefined : `Add post-commit hook: cp hooks/post-commit ${p.path}/.git/hooks/`,
372
+ });
373
+ // Hook quality checks (only if hook exists)
374
+ if (foundHook && hookContent) {
375
+ // Check: gdrive push should be best-effort (error-suppressed)
376
+ const pushesGdrive = hookContent.includes("gdrive");
377
+ const gdriveErrorSuppressed = hookContent.includes("2>/dev/null") ||
378
+ hookContent.includes("2>&1") ||
379
+ hookContent.includes("|| true") ||
380
+ hookContent.includes("|| echo");
381
+ if (pushesGdrive && !gdriveErrorSuppressed) {
382
+ findings.push({
383
+ check: "hook-quality",
384
+ status: "warn",
385
+ message: `${p.name}: gdrive push has no error suppression — FUSE failures will produce noisy output`,
386
+ project: p.name,
387
+ severity: "low",
388
+ remediation: `Update hook: git push gdrive "$BRANCH" 2>/dev/null && echo "✅ gdrive" || echo "⚠️ gdrive (best-effort)"`,
389
+ });
390
+ }
391
+ else if (pushesGdrive && gdriveErrorSuppressed) {
392
+ findings.push({
393
+ check: "hook-quality",
394
+ status: "pass",
395
+ message: `${p.name}: gdrive push is best-effort ✅ (errors suppressed)`,
396
+ project: p.name,
397
+ severity: "info",
398
+ });
399
+ }
400
+ // Check: hook should use BRANCH variable, not hardcoded branch name
401
+ const usesBranchVar = hookContent.includes("$BRANCH") ||
402
+ hookContent.includes("$(git") ||
403
+ hookContent.includes("`git");
404
+ const hardcodesBranch = hookContent.includes("push origin main") ||
405
+ hookContent.includes("push origin master") ||
406
+ hookContent.includes("push gdrive main") ||
407
+ hookContent.includes("push gdrive master");
408
+ if (hardcodesBranch && !usesBranchVar) {
409
+ findings.push({
410
+ check: "hook-quality",
411
+ status: "warn",
412
+ message: `${p.name}: hook hardcodes branch name — won't work on feature branches`,
413
+ project: p.name,
414
+ severity: "low",
415
+ remediation: `Use BRANCH=$(git rev-parse --abbrev-ref HEAD) then git push origin "$BRANCH"`,
416
+ });
417
+ }
418
+ // Check: hook should have shebang
419
+ if (!hookContent.startsWith("#!")) {
420
+ findings.push({
421
+ check: "hook-quality",
422
+ status: "warn",
423
+ message: `${p.name}: post-commit hook missing shebang (#!/bin/zsh or #!/bin/bash)`,
424
+ project: p.name,
425
+ severity: "low",
426
+ remediation: `Add #!/bin/zsh as the first line of the hook`,
427
+ });
428
+ }
429
+ }
430
+ }
431
+ return findings;
432
+ }
433
+ /**
434
+ * Validate .env files exist for projects that need them.
435
+ */
436
+ function auditEnvFiles(projects) {
437
+ const findings = [];
438
+ for (const p of projects) {
439
+ // Only check projects that should have .env
440
+ const needsEnv = ["laravel", "fastapi", "django", "express", "fastify", "next.js", "vite"].includes(p.framework)
441
+ || existsSync(join(p.path, ".env.example"));
442
+ if (!needsEnv)
443
+ continue;
444
+ const envPath = join(p.path, ".env");
445
+ const envExamplePath = join(p.path, ".env.example");
446
+ if (!existsSync(envPath)) {
447
+ findings.push({
448
+ check: "env-file",
449
+ status: "fail",
450
+ message: `${p.name}: .env file missing (framework: ${p.framework})`,
451
+ project: p.name,
452
+ severity: "high",
453
+ remediation: existsSync(envExamplePath)
454
+ ? `cd ${p.path} && cp .env.example .env`
455
+ : `Create ${p.path}/.env from project documentation`,
456
+ });
457
+ }
458
+ else {
459
+ // Check .env is in .gitignore
460
+ const giPath = join(p.path, ".gitignore");
461
+ if (existsSync(giPath)) {
462
+ const gi = readFileSync(giPath, "utf-8");
463
+ if (!gi.includes(".env")) {
464
+ findings.push({
465
+ check: "env-file",
466
+ status: "fail",
467
+ message: `${p.name}: .env exists but NOT in .gitignore — secrets could be committed!`,
468
+ project: p.name,
469
+ severity: "critical",
470
+ remediation: `echo ".env" >> ${p.path}/.gitignore`,
471
+ });
472
+ }
473
+ else {
474
+ findings.push({
475
+ check: "env-file",
476
+ status: "pass",
477
+ message: `${p.name}: .env ✅ (in .gitignore)`,
478
+ project: p.name,
479
+ severity: "info",
480
+ });
481
+ }
482
+ }
483
+ else {
484
+ findings.push({
485
+ check: "env-file",
486
+ status: "warn",
487
+ message: `${p.name}: .env exists but no .gitignore found`,
488
+ project: p.name,
489
+ severity: "medium",
490
+ });
491
+ }
492
+ }
493
+ }
494
+ return findings;
495
+ }
496
+ /**
497
+ * Check Docker configuration consistency.
498
+ */
499
+ function auditDocker(projects) {
500
+ const findings = [];
501
+ for (const p of projects) {
502
+ if (!p.hasDocker)
503
+ continue;
504
+ // Check Dockerfile exists
505
+ const dockerfilePath = join(p.path, "Dockerfile");
506
+ if (existsSync(dockerfilePath)) {
507
+ const content = readFileSync(dockerfilePath, "utf-8");
508
+ // Check platform specification for production builds
509
+ // (important: Apple Silicon → amd64)
510
+ findings.push({
511
+ check: "docker-config",
512
+ status: "pass",
513
+ message: `${p.name}: Dockerfile found`,
514
+ project: p.name,
515
+ severity: "info",
516
+ });
517
+ // Check for WORKDIR
518
+ const workdirMatch = content.match(/WORKDIR\s+(\S+)/);
519
+ if (workdirMatch) {
520
+ findings.push({
521
+ check: "docker-workdir",
522
+ status: "pass",
523
+ message: `${p.name}: WORKDIR = ${workdirMatch[1]}`,
524
+ project: p.name,
525
+ severity: "info",
526
+ });
527
+ }
528
+ }
529
+ // Check docker-compose volume mounts
530
+ for (const dcFile of ["docker-compose.yml", "docker-compose.prod.yml"]) {
531
+ const dcPath = join(p.path, dcFile);
532
+ if (!existsSync(dcPath))
533
+ continue;
534
+ const content = readFileSync(dcPath, "utf-8");
535
+ // Check for restart policy
536
+ if (content.includes("restart:")) {
537
+ findings.push({
538
+ check: "docker-restart",
539
+ status: "pass",
540
+ message: `${p.name} (${dcFile}): restart policy configured`,
541
+ project: p.name,
542
+ severity: "info",
543
+ });
544
+ }
545
+ else {
546
+ findings.push({
547
+ check: "docker-restart",
548
+ status: "warn",
549
+ message: `${p.name} (${dcFile}): No restart policy — containers won't auto-restart`,
550
+ project: p.name,
551
+ severity: "medium",
552
+ remediation: `Add 'restart: unless-stopped' to services in ${dcPath}`,
553
+ });
554
+ }
555
+ }
556
+ }
557
+ return findings;
558
+ }
559
+ /**
560
+ * Check PM2 ecosystem configs for best practices.
561
+ */
562
+ function auditPm2(projects) {
563
+ const findings = [];
564
+ for (const p of projects) {
565
+ if (!p.hasPm2)
566
+ continue;
567
+ for (const ecFile of ["ecosystem.config.js", "ecosystem.config.cjs"]) {
568
+ const ecPath = join(p.path, ecFile);
569
+ if (!existsSync(ecPath))
570
+ continue;
571
+ const content = readFileSync(ecPath, "utf-8");
572
+ // Check for bash wrapper anti-pattern
573
+ if (content.includes("bash -c") || content.includes("bash -i")) {
574
+ findings.push({
575
+ check: "pm2-no-bash",
576
+ status: "fail",
577
+ message: `${p.name}: PM2 uses bash wrapper — causes orphan processes on restart`,
578
+ project: p.name,
579
+ severity: "high",
580
+ remediation: `Remove 'bash -c' wrapper in ${ecPath} — use npx/node directly`,
581
+ });
582
+ }
583
+ // Check treekill
584
+ if (!content.includes("treekill")) {
585
+ findings.push({
586
+ check: "pm2-treekill",
587
+ status: "warn",
588
+ message: `${p.name}: PM2 config missing treekill: true — child processes may orphan on restart`,
589
+ project: p.name,
590
+ severity: "medium",
591
+ remediation: `Add 'treekill: true' to ${ecPath}`,
592
+ });
593
+ }
594
+ // Check kill_timeout
595
+ if (!content.includes("kill_timeout")) {
596
+ findings.push({
597
+ check: "pm2-kill-timeout",
598
+ status: "warn",
599
+ message: `${p.name}: PM2 config missing kill_timeout — processes may hang on stop`,
600
+ project: p.name,
601
+ severity: "low",
602
+ remediation: `Add 'kill_timeout: 10000' to ${ecPath}`,
603
+ });
604
+ }
605
+ // Check autorestart
606
+ if (content.includes("autorestart: false") || content.includes("autorestart:false")) {
607
+ findings.push({
608
+ check: "pm2-autorestart",
609
+ status: "pass",
610
+ message: `${p.name}: PM2 autorestart disabled (intentional for dev servers)`,
611
+ project: p.name,
612
+ severity: "info",
613
+ });
614
+ }
615
+ }
616
+ }
617
+ return findings;
618
+ }
619
+ /**
620
+ * Check for version issues — EOL runtimes, outdated deps, MUI v4/v5 coexistence.
621
+ */
622
+ function auditVersions(projects) {
623
+ const findings = [];
624
+ // Known EOL dates (approximate)
625
+ const eolRuntimes = {
626
+ "php 7.4": { eol: "Nov 2022", replacement: "PHP 8.2+" },
627
+ "node 14": { eol: "Apr 2023", replacement: "Node 20 LTS" },
628
+ "node 16": { eol: "Sep 2023", replacement: "Node 20 LTS" },
629
+ "python 3.7": { eol: "Jun 2023", replacement: "Python 3.11+" },
630
+ "python 3.8": { eol: "Oct 2024", replacement: "Python 3.11+" },
631
+ };
632
+ for (const p of projects) {
633
+ // Check MUI v4/v5 coexistence
634
+ if (p.deps["@mui/material"] && p.deps["@material-ui/core"]) {
635
+ findings.push({
636
+ check: "dep-conflict",
637
+ status: "warn",
638
+ message: `${p.name}: MUI v4 AND v5 both installed — should consolidate to v5`,
639
+ project: p.name,
640
+ severity: "medium",
641
+ remediation: `Migrate all @material-ui/* imports to @mui/* equivalents`,
642
+ });
643
+ }
644
+ // Check for very old react-scripts
645
+ if (p.deps["react-scripts"]) {
646
+ const version = p.deps["react-scripts"].replace(/[^0-9.]/g, "");
647
+ const major = parseInt(version.split(".")[0], 10);
648
+ if (major < 5) {
649
+ findings.push({
650
+ check: "dep-outdated",
651
+ status: "warn",
652
+ message: `${p.name}: react-scripts v${version} (CRA is deprecated — consider Vite migration)`,
653
+ project: p.name,
654
+ severity: "medium",
655
+ remediation: `Migrate to Vite: npm create vite@latest -- --template react-ts`,
656
+ });
657
+ }
658
+ }
659
+ }
660
+ return findings;
661
+ }
662
+ // ---------------------------------------------------------------------------
663
+ // Compliance Agent — Full Audit
664
+ // ---------------------------------------------------------------------------
665
+ /**
666
+ * Run the full compliance audit across all projects.
667
+ * Returns a structured plan document.
668
+ */
669
+ export function runComplianceAudit(projectDirs) {
670
+ const projects = listProjects(projectDirs);
671
+ const portResult = checkPorts(projectDirs);
672
+ const findings = [];
673
+ // Port conflicts
674
+ for (const conflict of portResult.conflicts) {
675
+ findings.push({
676
+ check: "port-conflict",
677
+ status: "fail",
678
+ message: `Port ${conflict.port} used by: ${conflict.usages.map(u => `${u.project} (${u.source}: ${u.details})`).join(", ")}`,
679
+ severity: "high",
680
+ });
681
+ }
682
+ // All individual audit checks
683
+ findings.push(...auditGitRemotes(projects));
684
+ findings.push(...auditGitHooks(projects));
685
+ findings.push(...auditEnvFiles(projects));
686
+ findings.push(...auditDocker(projects));
687
+ findings.push(...auditPm2(projects));
688
+ findings.push(...auditVersions(projects));
689
+ // Generate remediation steps for failures
690
+ const steps = [];
691
+ for (const f of findings) {
692
+ if (f.status === "fail" && f.remediation) {
693
+ steps.push({
694
+ action: `Fix: ${f.check}`,
695
+ description: f.message,
696
+ command: f.command || f.remediation,
697
+ risk: f.severity === "critical" ? "red" : "yellow",
698
+ reversible: f.check !== "env-file", // most are reversible
699
+ estimatedTime: "30s",
700
+ });
701
+ }
702
+ }
703
+ const summary = {
704
+ pass: findings.filter(f => f.status === "pass").length,
705
+ warn: findings.filter(f => f.status === "warn").length,
706
+ fail: findings.filter(f => f.status === "fail").length,
707
+ critical: findings.filter(f => f.severity === "critical").length,
708
+ };
709
+ return {
710
+ agent: "Compliance Agent",
711
+ timestamp: new Date().toISOString(),
712
+ trigger: "manual",
713
+ scope: `${projectDirs.length} projects`,
714
+ findings,
715
+ steps,
716
+ summary,
717
+ };
718
+ }
719
+ // ---------------------------------------------------------------------------
720
+ // Plan Document Formatter
721
+ // ---------------------------------------------------------------------------
722
+ /**
723
+ * Format an audit plan as a readable Markdown document.
724
+ */
725
+ export function formatPlan(plan) {
726
+ const lines = [];
727
+ lines.push(`## 📋 Agent Plan: ${plan.agent} — ${plan.timestamp.split("T")[0]}`);
728
+ lines.push("");
729
+ lines.push(`### Context`);
730
+ lines.push(`- **Trigger**: ${plan.trigger}`);
731
+ lines.push(`- **Scope**: ${plan.scope}`);
732
+ lines.push(`- **Summary**: ✅ ${plan.summary.pass} pass | ⚠ ${plan.summary.warn} warn | ❌ ${plan.summary.fail} fail | 🔴 ${plan.summary.critical} critical`);
733
+ lines.push("");
734
+ // Findings grouped by status
735
+ const failures = plan.findings.filter(f => f.status === "fail");
736
+ const warnings = plan.findings.filter(f => f.status === "warn");
737
+ const passes = plan.findings.filter(f => f.status === "pass");
738
+ if (failures.length > 0) {
739
+ lines.push(`### ❌ Failures (${failures.length})`);
740
+ for (const f of failures) {
741
+ const severity = f.severity === "critical" ? "🔴 CRITICAL" : `⚠ ${f.severity}`;
742
+ lines.push(`- **[${severity}] ${f.check}**: ${f.message}`);
743
+ if (f.remediation) {
744
+ lines.push(` - Fix: \`${f.remediation}\``);
745
+ }
746
+ }
747
+ lines.push("");
748
+ }
749
+ if (warnings.length > 0) {
750
+ lines.push(`### ⚠ Warnings (${warnings.length})`);
751
+ for (const f of warnings) {
752
+ lines.push(`- **${f.check}**: ${f.message}`);
753
+ if (f.remediation) {
754
+ lines.push(` - Fix: \`${f.remediation}\``);
755
+ }
756
+ }
757
+ lines.push("");
758
+ }
759
+ if (passes.length > 0) {
760
+ lines.push(`### ✅ Passed (${passes.length})`);
761
+ for (const f of passes) {
762
+ lines.push(`- ${f.check}: ${f.message}`);
763
+ }
764
+ lines.push("");
765
+ }
766
+ // Remediation steps
767
+ if (plan.steps.length > 0) {
768
+ lines.push(`### 🛠 Proposed Remediation Steps`);
769
+ lines.push("");
770
+ for (let i = 0; i < plan.steps.length; i++) {
771
+ const s = plan.steps[i];
772
+ const risk = s.risk === "red" ? "🔴 High" : s.risk === "yellow" ? "🟡 Medium" : "🟢 Low";
773
+ lines.push(`${i + 1}. **${s.action}** — ${s.description}`);
774
+ if (s.command) {
775
+ lines.push(` - Command: \`${s.command}\``);
776
+ }
777
+ lines.push(` - Risk: ${risk} | Reversible: ${s.reversible ? "Yes" : "No"} | Time: ${s.estimatedTime}`);
778
+ }
779
+ lines.push("");
780
+ lines.push(`> ⚠ Review each step before approving. Only approved steps will be executed.`);
781
+ }
782
+ return lines.join("\n");
783
+ }
784
+ /**
785
+ * Format project list as a readable summary.
786
+ */
787
+ export function formatProjectList(projects) {
788
+ const lines = [];
789
+ lines.push(`## 📂 FASTPROD Projects — ${projects.length} discovered`);
790
+ lines.push("");
791
+ // Group by type
792
+ const grouped = new Map();
793
+ for (const p of projects) {
794
+ const key = p.type;
795
+ const list = grouped.get(key) || [];
796
+ list.push(p);
797
+ grouped.set(key, list);
798
+ }
799
+ for (const [type, projs] of grouped) {
800
+ lines.push(`### ${type.toUpperCase()} (${projs.length})`);
801
+ for (const p of projs) {
802
+ const remotes = p.gitRemotes.length > 0 ? p.gitRemotes.join(", ") : "none";
803
+ const depList = Object.entries(p.deps).map(([k, v]) => `${k}@${v}`).join(", ");
804
+ const flags = [
805
+ p.hasGit ? "git" : null,
806
+ p.hasDocker ? "docker" : null,
807
+ p.hasPm2 ? "pm2" : null,
808
+ ].filter(Boolean).join("+");
809
+ lines.push(`- **${p.name}** — ${p.framework} (${p.runtime})`);
810
+ lines.push(` - Path: ${p.path}`);
811
+ if (depList)
812
+ lines.push(` - Key deps: ${depList}`);
813
+ lines.push(` - Infra: ${flags || "none"} | Remotes: ${remotes}`);
814
+ }
815
+ lines.push("");
816
+ }
817
+ return lines.join("\n");
818
+ }
819
+ /**
820
+ * Format port map as a readable table.
821
+ */
822
+ export function formatPortMap(ports, conflicts) {
823
+ const lines = [];
824
+ lines.push(`## 🔌 Port Allocation Map`);
825
+ lines.push("");
826
+ if (conflicts.length > 0) {
827
+ lines.push(`### ⚠ Conflicts (${conflicts.length})`);
828
+ for (const c of conflicts) {
829
+ lines.push(`- **Port ${c.port}**: ${c.usages.map(u => `${u.project}/${u.source} (${u.details})`).join(" vs ")}`);
830
+ }
831
+ lines.push("");
832
+ }
833
+ // Sort by port number
834
+ const sorted = [...ports].sort((a, b) => a.port - b.port);
835
+ lines.push("| Port | Project | Source | Details |");
836
+ lines.push("|------|---------|--------|---------|");
837
+ for (const p of sorted) {
838
+ lines.push(`| ${p.port} | ${p.project} | ${p.source} | ${p.details} |`);
839
+ }
840
+ return lines.join("\n");
841
+ }
842
+ /**
843
+ * Score a project's AI-readiness (0-100%).
844
+ * Checks how well-prepared a project is for AI coding agents.
845
+ */
846
+ export function scoreProject(dir) {
847
+ const checks = [];
848
+ const p = dir.path;
849
+ // --- Documentation (30 points max) ---
850
+ // copilot-instructions.md (10 pts)
851
+ const copilotPath = join(p, ".github", "copilot-instructions.md");
852
+ if (existsSync(copilotPath)) {
853
+ const content = readFileSync(copilotPath, "utf-8");
854
+ const lines = content.split("\n").length;
855
+ if (lines > 50) {
856
+ checks.push({ name: "copilot-instructions.md", category: "Documentation", points: 10, maxPoints: 10, status: "pass", detail: `${lines} lines — comprehensive` });
857
+ }
858
+ else {
859
+ checks.push({ name: "copilot-instructions.md", category: "Documentation", points: 6, maxPoints: 10, status: "partial", detail: `${lines} lines — could be more detailed` });
860
+ }
861
+ }
862
+ else {
863
+ checks.push({ name: "copilot-instructions.md", category: "Documentation", points: 0, maxPoints: 10, status: "fail", detail: "Missing — AI agents lack project context" });
864
+ }
865
+ // README.md (8 pts)
866
+ const readmePath = join(p, "README.md");
867
+ if (existsSync(readmePath)) {
868
+ const content = readFileSync(readmePath, "utf-8");
869
+ const readmeLines = content.split("\n").length;
870
+ if (readmeLines > 30) {
871
+ checks.push({ name: "README.md", category: "Documentation", points: 8, maxPoints: 8, status: "pass", detail: `${readmeLines} lines` });
872
+ }
873
+ else {
874
+ checks.push({ name: "README.md", category: "Documentation", points: 4, maxPoints: 8, status: "partial", detail: `${readmeLines} lines — sparse` });
875
+ }
876
+ }
877
+ else {
878
+ checks.push({ name: "README.md", category: "Documentation", points: 0, maxPoints: 8, status: "fail", detail: "Missing" });
879
+ }
880
+ // CLAUDE.md / .cursorrules / AGENTS.md (6 pts)
881
+ const altPatterns = ["CLAUDE.md", ".cursorrules", ".cursor/rules", "AGENTS.md"];
882
+ const foundAlt = altPatterns.filter(pat => existsSync(join(p, pat)));
883
+ if (foundAlt.length >= 2) {
884
+ checks.push({ name: "Multi-agent patterns", category: "Documentation", points: 6, maxPoints: 6, status: "pass", detail: `Found: ${foundAlt.join(", ")}` });
885
+ }
886
+ else if (foundAlt.length === 1) {
887
+ checks.push({ name: "Multi-agent patterns", category: "Documentation", points: 3, maxPoints: 6, status: "partial", detail: `Found: ${foundAlt[0]} only` });
888
+ }
889
+ else {
890
+ checks.push({ name: "Multi-agent patterns", category: "Documentation", points: 0, maxPoints: 6, status: "fail", detail: "No CLAUDE.md, .cursorrules, or AGENTS.md" });
891
+ }
892
+ // .github/SKILLS.md (3 pts)
893
+ const skillsPath = join(p, ".github", "SKILLS.md");
894
+ if (existsSync(skillsPath)) {
895
+ checks.push({ name: "SKILLS.md", category: "Documentation", points: 3, maxPoints: 3, status: "pass", detail: "Present" });
896
+ }
897
+ else {
898
+ checks.push({ name: "SKILLS.md", category: "Documentation", points: 0, maxPoints: 3, status: "fail", detail: "Missing — agents can't discover capabilities" });
899
+ }
900
+ // .env.example (3 pts)
901
+ const envExamplePath = join(p, ".env.example");
902
+ if (existsSync(envExamplePath)) {
903
+ checks.push({ name: ".env.example", category: "Documentation", points: 3, maxPoints: 3, status: "pass", detail: "Present — agents know which env vars to set" });
904
+ }
905
+ else {
906
+ checks.push({ name: ".env.example", category: "Documentation", points: 0, maxPoints: 3, status: "fail", detail: "Missing — agents can't set up env" });
907
+ }
908
+ // --- Infrastructure (30 points max) ---
909
+ // Git repo (5 pts)
910
+ if (existsSync(join(p, ".git"))) {
911
+ checks.push({ name: "Git repository", category: "Infrastructure", points: 5, maxPoints: 5, status: "pass", detail: "Initialized" });
912
+ }
913
+ else {
914
+ checks.push({ name: "Git repository", category: "Infrastructure", points: 0, maxPoints: 5, status: "fail", detail: "Not a git repo" });
915
+ }
916
+ // .gitignore (3 pts)
917
+ if (existsSync(join(p, ".gitignore"))) {
918
+ checks.push({ name: ".gitignore", category: "Infrastructure", points: 3, maxPoints: 3, status: "pass", detail: "Present" });
919
+ }
920
+ else {
921
+ checks.push({ name: ".gitignore", category: "Infrastructure", points: 0, maxPoints: 3, status: "fail", detail: "Missing" });
922
+ }
923
+ // Git hooks (5 pts)
924
+ const hookDir = join(p, "hooks");
925
+ const gitHookDir = join(p, ".git", "hooks");
926
+ const hasPostCommit = existsSync(join(hookDir, "post-commit")) || existsSync(join(gitHookDir, "post-commit"));
927
+ if (hasPostCommit) {
928
+ checks.push({ name: "Git hooks", category: "Infrastructure", points: 5, maxPoints: 5, status: "pass", detail: "post-commit hook configured" });
929
+ }
930
+ else {
931
+ checks.push({ name: "Git hooks", category: "Infrastructure", points: 0, maxPoints: 5, status: "fail", detail: "No hooks — consider auto-push" });
932
+ }
933
+ // Docker / containerization (5 pts)
934
+ const hasDockerfile = existsSync(join(p, "Dockerfile"));
935
+ const hasCompose = existsSync(join(p, "docker-compose.yml")) || existsSync(join(p, "docker-compose.prod.yml"));
936
+ if (hasDockerfile && hasCompose) {
937
+ checks.push({ name: "Docker", category: "Infrastructure", points: 5, maxPoints: 5, status: "pass", detail: "Dockerfile + compose" });
938
+ }
939
+ else if (hasDockerfile || hasCompose) {
940
+ checks.push({ name: "Docker", category: "Infrastructure", points: 3, maxPoints: 5, status: "partial", detail: hasDockerfile ? "Dockerfile only" : "Compose only" });
941
+ }
942
+ else {
943
+ checks.push({ name: "Docker", category: "Infrastructure", points: 0, maxPoints: 5, status: "fail", detail: "Not containerized" });
944
+ }
945
+ // CI config (5 pts)
946
+ const ciPaths = [".github/workflows", ".gitlab-ci.yml", "Jenkinsfile", ".circleci", ".travis.yml"];
947
+ const foundCI = ciPaths.filter(ci => existsSync(join(p, ci)));
948
+ if (foundCI.length > 0) {
949
+ checks.push({ name: "CI/CD", category: "Infrastructure", points: 5, maxPoints: 5, status: "pass", detail: foundCI.join(", ") });
950
+ }
951
+ else {
952
+ checks.push({ name: "CI/CD", category: "Infrastructure", points: 0, maxPoints: 5, status: "fail", detail: "No CI pipeline" });
953
+ }
954
+ // Deploy script (4 pts)
955
+ const deployPaths = ["deploy.sh", "deploy.js", "Makefile"];
956
+ const foundDeploy = deployPaths.filter(d => existsSync(join(p, d)));
957
+ if (foundDeploy.length > 0) {
958
+ checks.push({ name: "Deploy script", category: "Infrastructure", points: 4, maxPoints: 4, status: "pass", detail: foundDeploy.join(", ") });
959
+ }
960
+ else {
961
+ checks.push({ name: "Deploy script", category: "Infrastructure", points: 0, maxPoints: 4, status: "fail", detail: "No deploy automation" });
962
+ }
963
+ // PM2 / process manager (3 pts)
964
+ if (existsSync(join(p, "ecosystem.config.js")) || existsSync(join(p, "ecosystem.config.cjs"))) {
965
+ checks.push({ name: "Process manager", category: "Infrastructure", points: 3, maxPoints: 3, status: "pass", detail: "PM2 ecosystem config" });
966
+ }
967
+ else {
968
+ checks.push({ name: "Process manager", category: "Infrastructure", points: 0, maxPoints: 3, status: "fail", detail: "No PM2 config" });
969
+ }
970
+ // --- Code Quality (20 points max) ---
971
+ // Tests directory (8 pts)
972
+ const testDirs = ["tests", "test", "__tests__", "spec", "src/__tests__"];
973
+ const foundTests = testDirs.filter(td => existsSync(join(p, td)));
974
+ if (foundTests.length > 0) {
975
+ const testDir = join(p, foundTests[0]);
976
+ try {
977
+ const hasFiles = readdirSync(testDir).length > 0;
978
+ if (hasFiles) {
979
+ checks.push({ name: "Tests", category: "Code Quality", points: 8, maxPoints: 8, status: "pass", detail: `${foundTests[0]}/ directory with files` });
980
+ }
981
+ else {
982
+ checks.push({ name: "Tests", category: "Code Quality", points: 3, maxPoints: 8, status: "partial", detail: `${foundTests[0]}/ exists but empty` });
983
+ }
984
+ }
985
+ catch {
986
+ checks.push({ name: "Tests", category: "Code Quality", points: 3, maxPoints: 8, status: "partial", detail: `${foundTests[0]}/ exists` });
987
+ }
988
+ }
989
+ else {
990
+ checks.push({ name: "Tests", category: "Code Quality", points: 0, maxPoints: 8, status: "fail", detail: "No test directory" });
991
+ }
992
+ // TypeScript / type checking (5 pts)
993
+ if (existsSync(join(p, "tsconfig.json"))) {
994
+ checks.push({ name: "TypeScript", category: "Code Quality", points: 5, maxPoints: 5, status: "pass", detail: "tsconfig.json present" });
995
+ }
996
+ else if (existsSync(join(p, "jsconfig.json"))) {
997
+ checks.push({ name: "Type checking", category: "Code Quality", points: 2, maxPoints: 5, status: "partial", detail: "jsconfig.json only" });
998
+ }
999
+ else {
1000
+ checks.push({ name: "Type checking", category: "Code Quality", points: 0, maxPoints: 5, status: "fail", detail: "No tsconfig/jsconfig" });
1001
+ }
1002
+ // Linting config (4 pts)
1003
+ const lintConfigs = [".eslintrc.js", ".eslintrc.json", ".eslintrc.yml", "eslint.config.js", "eslint.config.mjs", ".prettierrc", "phpcs.xml"];
1004
+ const foundLint = lintConfigs.filter(l => existsSync(join(p, l)));
1005
+ if (foundLint.length > 0) {
1006
+ checks.push({ name: "Linting", category: "Code Quality", points: 4, maxPoints: 4, status: "pass", detail: foundLint.join(", ") });
1007
+ }
1008
+ else {
1009
+ checks.push({ name: "Linting", category: "Code Quality", points: 0, maxPoints: 4, status: "fail", detail: "No lint config" });
1010
+ }
1011
+ // Package scripts / build commands (3 pts)
1012
+ const pkgPath = join(p, "package.json");
1013
+ if (existsSync(pkgPath)) {
1014
+ try {
1015
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1016
+ const scripts = Object.keys(pkg.scripts || {});
1017
+ const hasUseful = scripts.some(s => ["build", "dev", "start", "test", "lint"].includes(s));
1018
+ if (hasUseful) {
1019
+ checks.push({ name: "npm scripts", category: "Code Quality", points: 3, maxPoints: 3, status: "pass", detail: scripts.slice(0, 6).join(", ") });
1020
+ }
1021
+ else {
1022
+ checks.push({ name: "npm scripts", category: "Code Quality", points: 1, maxPoints: 3, status: "partial", detail: `Has scripts but no build/dev/test: ${scripts.join(", ")}` });
1023
+ }
1024
+ }
1025
+ catch { /* ignore */ }
1026
+ }
1027
+ // --- Security (20 points max) ---
1028
+ // .env in .gitignore (8 pts)
1029
+ const gitignorePath = join(p, ".gitignore");
1030
+ if (existsSync(gitignorePath)) {
1031
+ const gitignore = readFileSync(gitignorePath, "utf-8");
1032
+ if (gitignore.includes(".env")) {
1033
+ checks.push({ name: ".env in .gitignore", category: "Security", points: 8, maxPoints: 8, status: "pass", detail: ".env is gitignored" });
1034
+ }
1035
+ else {
1036
+ checks.push({ name: ".env in .gitignore", category: "Security", points: 0, maxPoints: 8, status: "fail", detail: ".env NOT in .gitignore — secrets at risk!" });
1037
+ }
1038
+ }
1039
+ else {
1040
+ checks.push({ name: ".env in .gitignore", category: "Security", points: 0, maxPoints: 8, status: "fail", detail: "No .gitignore at all" });
1041
+ }
1042
+ // No secrets in tracked files (6 pts)
1043
+ if (existsSync(join(p, ".env")) && existsSync(join(p, ".git"))) {
1044
+ const tracked = exec("git ls-files .env", p);
1045
+ if (tracked === ".env") {
1046
+ checks.push({ name: "Secrets exposure", category: "Security", points: 0, maxPoints: 6, status: "fail", detail: ".env is tracked by git!" });
1047
+ }
1048
+ else {
1049
+ checks.push({ name: "Secrets exposure", category: "Security", points: 6, maxPoints: 6, status: "pass", detail: ".env not tracked" });
1050
+ }
1051
+ }
1052
+ else {
1053
+ checks.push({ name: "Secrets exposure", category: "Security", points: 6, maxPoints: 6, status: "pass", detail: "No .env or not a git repo" });
1054
+ }
1055
+ // Lockfile present (3 pts)
1056
+ const lockfiles = ["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "composer.lock"];
1057
+ const foundLock = lockfiles.filter(l => existsSync(join(p, l)));
1058
+ if (foundLock.length > 0) {
1059
+ checks.push({ name: "Lockfile", category: "Security", points: 3, maxPoints: 3, status: "pass", detail: foundLock.join(", ") });
1060
+ }
1061
+ else if (existsSync(pkgPath) || existsSync(join(p, "composer.json"))) {
1062
+ checks.push({ name: "Lockfile", category: "Security", points: 0, maxPoints: 3, status: "fail", detail: "No lockfile — deps not pinned" });
1063
+ }
1064
+ // node_modules in .gitignore (3 pts)
1065
+ if (existsSync(gitignorePath)) {
1066
+ const gitignore = readFileSync(gitignorePath, "utf-8");
1067
+ if (gitignore.includes("node_modules") || gitignore.includes("vendor")) {
1068
+ checks.push({ name: "Deps gitignored", category: "Security", points: 3, maxPoints: 3, status: "pass", detail: "node_modules/vendor gitignored" });
1069
+ }
1070
+ else if (!existsSync(join(p, "package.json")) && !existsSync(join(p, "composer.json"))) {
1071
+ checks.push({ name: "Deps gitignored", category: "Security", points: 3, maxPoints: 3, status: "pass", detail: "N/A — no package manager" });
1072
+ }
1073
+ else {
1074
+ checks.push({ name: "Deps gitignored", category: "Security", points: 0, maxPoints: 3, status: "fail", detail: "node_modules/vendor not in .gitignore" });
1075
+ }
1076
+ }
1077
+ // --- Calculate totals ---
1078
+ const totalScore = checks.reduce((sum, c) => sum + c.points, 0);
1079
+ const maxScore = checks.reduce((sum, c) => sum + c.maxPoints, 0);
1080
+ const percentage = Math.round((totalScore / maxScore) * 100);
1081
+ let grade;
1082
+ if (percentage >= 90)
1083
+ grade = "A+";
1084
+ else if (percentage >= 80)
1085
+ grade = "A";
1086
+ else if (percentage >= 70)
1087
+ grade = "B";
1088
+ else if (percentage >= 60)
1089
+ grade = "C";
1090
+ else if (percentage >= 50)
1091
+ grade = "D";
1092
+ else
1093
+ grade = "F";
1094
+ return {
1095
+ project: dir.name,
1096
+ path: dir.path,
1097
+ score: totalScore,
1098
+ maxScore,
1099
+ percentage,
1100
+ grade,
1101
+ checks,
1102
+ };
1103
+ }
1104
+ /**
1105
+ * Format a project score report as Markdown.
1106
+ */
1107
+ export function formatScoreReport(scores) {
1108
+ const lines = [];
1109
+ // Summary table
1110
+ lines.push("# 🎯 AI-Readiness Scores\n");
1111
+ lines.push("| Project | Score | Grade | Doc | Infra | Quality | Security |");
1112
+ lines.push("|---------|-------|-------|-----|-------|---------|----------|");
1113
+ for (const s of scores) {
1114
+ const byCategory = new Map();
1115
+ for (const c of s.checks) {
1116
+ const cat = byCategory.get(c.category) || { pts: 0, max: 0 };
1117
+ cat.pts += c.points;
1118
+ cat.max += c.maxPoints;
1119
+ byCategory.set(c.category, cat);
1120
+ }
1121
+ const doc = byCategory.get("Documentation") || { pts: 0, max: 0 };
1122
+ const infra = byCategory.get("Infrastructure") || { pts: 0, max: 0 };
1123
+ const quality = byCategory.get("Code Quality") || { pts: 0, max: 0 };
1124
+ const security = byCategory.get("Security") || { pts: 0, max: 0 };
1125
+ const gradeEmoji = s.percentage >= 80 ? "🟢" : s.percentage >= 60 ? "🟡" : "🔴";
1126
+ lines.push(`| ${s.project} | **${s.percentage}%** | ${gradeEmoji} ${s.grade} | ${doc.pts}/${doc.max} | ${infra.pts}/${infra.max} | ${quality.pts}/${quality.max} | ${security.pts}/${security.max} |`);
1127
+ }
1128
+ // Sort by score descending
1129
+ const sorted = [...scores].sort((a, b) => b.percentage - a.percentage);
1130
+ // Top performers
1131
+ const top3 = sorted.slice(0, 3);
1132
+ lines.push("\n## 🏆 Top Performers");
1133
+ for (const s of top3) {
1134
+ lines.push(`- **${s.project}** — ${s.percentage}% (${s.grade})`);
1135
+ }
1136
+ // Needs work
1137
+ const needsWork = sorted.filter(s => s.percentage < 60);
1138
+ if (needsWork.length > 0) {
1139
+ lines.push("\n## ⚠️ Needs Work");
1140
+ for (const s of needsWork) {
1141
+ const fails = s.checks.filter(c => c.status === "fail");
1142
+ lines.push(`- **${s.project}** — ${s.percentage}% (${s.grade}): ${fails.map(f => f.name).join(", ")}`);
1143
+ }
1144
+ }
1145
+ // Detailed per-project breakdown (top 5 only to keep output manageable)
1146
+ lines.push("\n## 📋 Detailed Breakdown\n");
1147
+ for (const s of sorted.slice(0, 5)) {
1148
+ lines.push(`### ${s.project} — ${s.percentage}% (${s.grade})\n`);
1149
+ lines.push("| Check | Category | Score | Status | Detail |");
1150
+ lines.push("|-------|----------|-------|--------|--------|");
1151
+ for (const c of s.checks) {
1152
+ const icon = c.status === "pass" ? "✅" : c.status === "partial" ? "🟡" : "❌";
1153
+ lines.push(`| ${c.name} | ${c.category} | ${c.points}/${c.maxPoints} | ${icon} | ${c.detail} |`);
1154
+ }
1155
+ lines.push("");
1156
+ }
1157
+ // Overall stats
1158
+ const avgScore = Math.round(scores.reduce((sum, s) => sum + s.percentage, 0) / scores.length);
1159
+ lines.push(`\n---\n**${scores.length} projects scanned** | Average AI-readiness: **${avgScore}%**`);
1160
+ return lines.join("\n");
1161
+ }
1162
+ //# sourceMappingURL=agents.js.map