projscan 0.10.0 → 0.12.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.
Files changed (242) hide show
  1. package/README.md +56 -19
  2. package/dist/analyzers/unusedDependencyCheck.js +69 -17
  3. package/dist/analyzers/unusedDependencyCheck.js.map +1 -1
  4. package/dist/cli/_shared.d.ts +16 -0
  5. package/dist/cli/_shared.js +210 -0
  6. package/dist/cli/_shared.js.map +1 -0
  7. package/dist/cli/commands/analyze.d.ts +1 -0
  8. package/dist/cli/commands/analyze.js +87 -0
  9. package/dist/cli/commands/analyze.js.map +1 -0
  10. package/dist/cli/commands/audit.d.ts +1 -0
  11. package/dist/cli/commands/audit.js +47 -0
  12. package/dist/cli/commands/audit.js.map +1 -0
  13. package/dist/cli/commands/badge.d.ts +1 -0
  14. package/dist/cli/commands/badge.js +45 -0
  15. package/dist/cli/commands/badge.js.map +1 -0
  16. package/dist/cli/commands/ci.d.ts +1 -0
  17. package/dist/cli/commands/ci.js +57 -0
  18. package/dist/cli/commands/ci.js.map +1 -0
  19. package/dist/cli/commands/coupling.d.ts +1 -0
  20. package/dist/cli/commands/coupling.js +83 -0
  21. package/dist/cli/commands/coupling.js.map +1 -0
  22. package/dist/cli/commands/coverage.d.ts +1 -0
  23. package/dist/cli/commands/coverage.js +63 -0
  24. package/dist/cli/commands/coverage.js.map +1 -0
  25. package/dist/cli/commands/dependencies.d.ts +1 -0
  26. package/dist/cli/commands/dependencies.js +45 -0
  27. package/dist/cli/commands/dependencies.js.map +1 -0
  28. package/dist/cli/commands/diagram.d.ts +1 -0
  29. package/dist/cli/commands/diagram.js +45 -0
  30. package/dist/cli/commands/diagram.js.map +1 -0
  31. package/dist/cli/commands/diff.d.ts +1 -0
  32. package/dist/cli/commands/diff.js +70 -0
  33. package/dist/cli/commands/diff.js.map +1 -0
  34. package/dist/cli/commands/doctor.d.ts +1 -0
  35. package/dist/cli/commands/doctor.js +62 -0
  36. package/dist/cli/commands/doctor.js.map +1 -0
  37. package/dist/cli/commands/explain.d.ts +1 -0
  38. package/dist/cli/commands/explain.js +42 -0
  39. package/dist/cli/commands/explain.js.map +1 -0
  40. package/dist/cli/commands/file.d.ts +1 -0
  41. package/dist/cli/commands/file.js +45 -0
  42. package/dist/cli/commands/file.js.map +1 -0
  43. package/dist/cli/commands/fix.d.ts +1 -0
  44. package/dist/cli/commands/fix.js +70 -0
  45. package/dist/cli/commands/fix.js.map +1 -0
  46. package/dist/cli/commands/help.d.ts +1 -0
  47. package/dist/cli/commands/help.js +11 -0
  48. package/dist/cli/commands/help.js.map +1 -0
  49. package/dist/cli/commands/hotspots.d.ts +1 -0
  50. package/dist/cli/commands/hotspots.js +74 -0
  51. package/dist/cli/commands/hotspots.js.map +1 -0
  52. package/dist/cli/commands/mcp.d.ts +1 -0
  53. package/dist/cli/commands/mcp.js +21 -0
  54. package/dist/cli/commands/mcp.js.map +1 -0
  55. package/dist/cli/commands/outdated.d.ts +1 -0
  56. package/dist/cli/commands/outdated.js +51 -0
  57. package/dist/cli/commands/outdated.js.map +1 -0
  58. package/dist/cli/commands/prDiff.d.ts +1 -0
  59. package/dist/cli/commands/prDiff.js +59 -0
  60. package/dist/cli/commands/prDiff.js.map +1 -0
  61. package/dist/cli/commands/search.d.ts +1 -0
  62. package/dist/cli/commands/search.js +233 -0
  63. package/dist/cli/commands/search.js.map +1 -0
  64. package/dist/cli/commands/structure.d.ts +1 -0
  65. package/dist/cli/commands/structure.js +58 -0
  66. package/dist/cli/commands/structure.js.map +1 -0
  67. package/dist/cli/commands/upgrade.d.ts +1 -0
  68. package/dist/cli/commands/upgrade.js +44 -0
  69. package/dist/cli/commands/upgrade.js.map +1 -0
  70. package/dist/cli/commands/workspaces.d.ts +1 -0
  71. package/dist/cli/commands/workspaces.js +35 -0
  72. package/dist/cli/commands/workspaces.js.map +1 -0
  73. package/dist/cli/index.js +45 -1132
  74. package/dist/cli/index.js.map +1 -1
  75. package/dist/core/ast.d.ts +2 -0
  76. package/dist/core/ast.js +35 -2
  77. package/dist/core/ast.js.map +1 -1
  78. package/dist/core/codeGraph.d.ts +2 -0
  79. package/dist/core/codeGraph.js +2 -0
  80. package/dist/core/codeGraph.js.map +1 -1
  81. package/dist/core/couplingAnalyzer.d.ts +18 -0
  82. package/dist/core/couplingAnalyzer.js +174 -0
  83. package/dist/core/couplingAnalyzer.js.map +1 -0
  84. package/dist/core/fileInspector.d.ts +1 -1
  85. package/dist/core/fileInspector.js +31 -1
  86. package/dist/core/fileInspector.js.map +1 -1
  87. package/dist/core/hotspotAnalyzer.d.ts +13 -0
  88. package/dist/core/hotspotAnalyzer.js +29 -6
  89. package/dist/core/hotspotAnalyzer.js.map +1 -1
  90. package/dist/core/indexCache.js +6 -3
  91. package/dist/core/indexCache.js.map +1 -1
  92. package/dist/core/languages/LanguageAdapter.d.ts +1 -1
  93. package/dist/core/languages/goAdapter.d.ts +2 -0
  94. package/dist/core/languages/goAdapter.js +138 -0
  95. package/dist/core/languages/goAdapter.js.map +1 -0
  96. package/dist/core/languages/goCallSites.d.ts +20 -0
  97. package/dist/core/languages/goCallSites.js +42 -0
  98. package/dist/core/languages/goCallSites.js.map +1 -0
  99. package/dist/core/languages/goCyclomatic.d.ts +21 -0
  100. package/dist/core/languages/goCyclomatic.js +55 -0
  101. package/dist/core/languages/goCyclomatic.js.map +1 -0
  102. package/dist/core/languages/goExports.d.ts +26 -0
  103. package/dist/core/languages/goExports.js +89 -0
  104. package/dist/core/languages/goExports.js.map +1 -0
  105. package/dist/core/languages/goImports.d.ts +26 -0
  106. package/dist/core/languages/goImports.js +64 -0
  107. package/dist/core/languages/goImports.js.map +1 -0
  108. package/dist/core/languages/goManifests.d.ts +19 -0
  109. package/dist/core/languages/goManifests.js +56 -0
  110. package/dist/core/languages/goManifests.js.map +1 -0
  111. package/dist/core/languages/javaAdapter.d.ts +2 -0
  112. package/dist/core/languages/javaAdapter.js +148 -0
  113. package/dist/core/languages/javaAdapter.js.map +1 -0
  114. package/dist/core/languages/javaCallSites.d.ts +16 -0
  115. package/dist/core/languages/javaCallSites.js +45 -0
  116. package/dist/core/languages/javaCallSites.js.map +1 -0
  117. package/dist/core/languages/javaCyclomatic.d.ts +21 -0
  118. package/dist/core/languages/javaCyclomatic.js +49 -0
  119. package/dist/core/languages/javaCyclomatic.js.map +1 -0
  120. package/dist/core/languages/javaExports.d.ts +25 -0
  121. package/dist/core/languages/javaExports.js +80 -0
  122. package/dist/core/languages/javaExports.js.map +1 -0
  123. package/dist/core/languages/javaImports.d.ts +25 -0
  124. package/dist/core/languages/javaImports.js +49 -0
  125. package/dist/core/languages/javaImports.js.map +1 -0
  126. package/dist/core/languages/javaManifests.d.ts +25 -0
  127. package/dist/core/languages/javaManifests.js +86 -0
  128. package/dist/core/languages/javaManifests.js.map +1 -0
  129. package/dist/core/languages/pythonAdapter.js +8 -1
  130. package/dist/core/languages/pythonAdapter.js.map +1 -1
  131. package/dist/core/languages/pythonCallSites.d.ts +19 -0
  132. package/dist/core/languages/pythonCallSites.js +40 -0
  133. package/dist/core/languages/pythonCallSites.js.map +1 -0
  134. package/dist/core/languages/pythonCyclomatic.d.ts +18 -0
  135. package/dist/core/languages/pythonCyclomatic.js +45 -0
  136. package/dist/core/languages/pythonCyclomatic.js.map +1 -0
  137. package/dist/core/languages/registry.js +4 -1
  138. package/dist/core/languages/registry.js.map +1 -1
  139. package/dist/core/languages/rubyAdapter.d.ts +2 -0
  140. package/dist/core/languages/rubyAdapter.js +131 -0
  141. package/dist/core/languages/rubyAdapter.js.map +1 -0
  142. package/dist/core/languages/rubyCallSites.d.ts +16 -0
  143. package/dist/core/languages/rubyCallSites.js +34 -0
  144. package/dist/core/languages/rubyCallSites.js.map +1 -0
  145. package/dist/core/languages/rubyCyclomatic.d.ts +19 -0
  146. package/dist/core/languages/rubyCyclomatic.js +47 -0
  147. package/dist/core/languages/rubyCyclomatic.js.map +1 -0
  148. package/dist/core/languages/rubyExports.d.ts +24 -0
  149. package/dist/core/languages/rubyExports.js +53 -0
  150. package/dist/core/languages/rubyExports.js.map +1 -0
  151. package/dist/core/languages/rubyImports.d.ts +12 -0
  152. package/dist/core/languages/rubyImports.js +75 -0
  153. package/dist/core/languages/rubyImports.js.map +1 -0
  154. package/dist/core/languages/rubyManifests.d.ts +20 -0
  155. package/dist/core/languages/rubyManifests.js +55 -0
  156. package/dist/core/languages/rubyManifests.js.map +1 -0
  157. package/dist/core/languages/treeSitterLoader.js +4 -1
  158. package/dist/core/languages/treeSitterLoader.js.map +1 -1
  159. package/dist/core/monorepo.d.ts +20 -0
  160. package/dist/core/monorepo.js +270 -0
  161. package/dist/core/monorepo.js.map +1 -0
  162. package/dist/core/outdatedDetector.d.ts +13 -2
  163. package/dist/core/outdatedDetector.js +86 -16
  164. package/dist/core/outdatedDetector.js.map +1 -1
  165. package/dist/core/prDiff.d.ts +43 -0
  166. package/dist/core/prDiff.js +298 -0
  167. package/dist/core/prDiff.js.map +1 -0
  168. package/dist/core/telemetry.d.ts +90 -0
  169. package/dist/core/telemetry.js +199 -0
  170. package/dist/core/telemetry.js.map +1 -0
  171. package/dist/grammars/tree-sitter-go.wasm +0 -0
  172. package/dist/grammars/tree-sitter-java.wasm +0 -0
  173. package/dist/grammars/tree-sitter-ruby.wasm +0 -0
  174. package/dist/mcp/tools/_shared.d.ts +24 -0
  175. package/dist/mcp/tools/_shared.js +82 -0
  176. package/dist/mcp/tools/_shared.js.map +1 -0
  177. package/dist/mcp/tools/analyze.d.ts +2 -0
  178. package/dist/mcp/tools/analyze.js +55 -0
  179. package/dist/mcp/tools/analyze.js.map +1 -0
  180. package/dist/mcp/tools/audit.d.ts +2 -0
  181. package/dist/mcp/tools/audit.js +32 -0
  182. package/dist/mcp/tools/audit.js.map +1 -0
  183. package/dist/mcp/tools/coupling.d.ts +2 -0
  184. package/dist/mcp/tools/coupling.js +67 -0
  185. package/dist/mcp/tools/coupling.js.map +1 -0
  186. package/dist/mcp/tools/coverage.d.ts +2 -0
  187. package/dist/mcp/tools/coverage.js +53 -0
  188. package/dist/mcp/tools/coverage.js.map +1 -0
  189. package/dist/mcp/tools/dependencies.d.ts +2 -0
  190. package/dist/mcp/tools/dependencies.js +16 -0
  191. package/dist/mcp/tools/dependencies.js.map +1 -0
  192. package/dist/mcp/tools/doctor.d.ts +2 -0
  193. package/dist/mcp/tools/doctor.js +30 -0
  194. package/dist/mcp/tools/doctor.js.map +1 -0
  195. package/dist/mcp/tools/explain.d.ts +2 -0
  196. package/dist/mcp/tools/explain.js +30 -0
  197. package/dist/mcp/tools/explain.js.map +1 -0
  198. package/dist/mcp/tools/file.d.ts +2 -0
  199. package/dist/mcp/tools/file.js +22 -0
  200. package/dist/mcp/tools/file.js.map +1 -0
  201. package/dist/mcp/tools/graph.d.ts +2 -0
  202. package/dist/mcp/tools/graph.js +69 -0
  203. package/dist/mcp/tools/graph.js.map +1 -0
  204. package/dist/mcp/tools/hotspots.d.ts +2 -0
  205. package/dist/mcp/tools/hotspots.js +64 -0
  206. package/dist/mcp/tools/hotspots.js.map +1 -0
  207. package/dist/mcp/tools/outdated.d.ts +2 -0
  208. package/dist/mcp/tools/outdated.js +36 -0
  209. package/dist/mcp/tools/outdated.js.map +1 -0
  210. package/dist/mcp/tools/prDiff.d.ts +2 -0
  211. package/dist/mcp/tools/prDiff.js +38 -0
  212. package/dist/mcp/tools/prDiff.js.map +1 -0
  213. package/dist/mcp/tools/search.d.ts +2 -0
  214. package/dist/mcp/tools/search.js +167 -0
  215. package/dist/mcp/tools/search.js.map +1 -0
  216. package/dist/mcp/tools/structure.d.ts +2 -0
  217. package/dist/mcp/tools/structure.js +34 -0
  218. package/dist/mcp/tools/structure.js.map +1 -0
  219. package/dist/mcp/tools/upgrade.d.ts +2 -0
  220. package/dist/mcp/tools/upgrade.js +38 -0
  221. package/dist/mcp/tools/upgrade.js.map +1 -0
  222. package/dist/mcp/tools/workspaces.d.ts +2 -0
  223. package/dist/mcp/tools/workspaces.js +13 -0
  224. package/dist/mcp/tools/workspaces.js.map +1 -0
  225. package/dist/mcp/tools.d.ts +12 -6
  226. package/dist/mcp/tools.js +40 -605
  227. package/dist/mcp/tools.js.map +1 -1
  228. package/dist/reporters/consoleReporter.d.ts +4 -1
  229. package/dist/reporters/consoleReporter.js +113 -0
  230. package/dist/reporters/consoleReporter.js.map +1 -1
  231. package/dist/reporters/jsonReporter.d.ts +4 -1
  232. package/dist/reporters/jsonReporter.js +9 -0
  233. package/dist/reporters/jsonReporter.js.map +1 -1
  234. package/dist/reporters/markdownReporter.d.ts +4 -1
  235. package/dist/reporters/markdownReporter.js +103 -3
  236. package/dist/reporters/markdownReporter.js.map +1 -1
  237. package/dist/tool-manifest.json +358 -0
  238. package/dist/types.d.ts +115 -0
  239. package/dist/utils/cache.d.ts +3 -0
  240. package/dist/utils/cache.js +51 -0
  241. package/dist/utils/cache.js.map +1 -0
  242. package/package.json +7 -3
package/dist/cli/index.js CHANGED
@@ -1,1135 +1,48 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import ora from 'ora';
4
- import chalk from 'chalk';
5
- import path from 'node:path';
6
- import fs from 'node:fs/promises';
7
- import { readFileSync } from 'node:fs';
8
- import { fileURLToPath } from 'node:url';
9
- import readline from 'node:readline';
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
- const pkg = JSON.parse(readFileSync(path.resolve(__dirname, '../../package.json'), 'utf-8'));
12
- import { scanRepository } from '../core/repositoryScanner.js';
13
- import { detectLanguages } from '../core/languageDetector.js';
14
- import { detectFrameworks } from '../core/frameworkDetector.js';
15
- import { analyzeDependencies } from '../core/dependencyAnalyzer.js';
16
- import { collectIssues } from '../core/issueEngine.js';
17
- import { analyzeHotspots } from '../core/hotspotAnalyzer.js';
18
- import { detectOutdated } from '../core/outdatedDetector.js';
19
- import { runAudit, auditFindingsToIssues } from '../core/auditRunner.js';
20
- import { previewUpgrade } from '../core/upgradePreview.js';
21
- import { parseCoverage, coverageMap } from '../core/coverageParser.js';
22
- import { joinCoverageWithHotspots } from '../core/coverageJoin.js';
23
- import { buildCodeGraph } from '../core/codeGraph.js';
24
- import { loadCachedGraph, saveCachedGraph } from '../core/indexCache.js';
25
- import { buildSearchIndex, search as searchIndex, attachExcerpts, expandQuery } from '../core/searchIndex.js';
26
- import { buildSemanticIndex, semanticSearch, reciprocalRankFusion, } from '../core/semanticSearch.js';
27
- import { isSemanticAvailable } from '../core/embeddings.js';
28
- import { inspectFile, extractImports, extractExports, inferPurpose, detectFileIssues, } from '../core/fileInspector.js';
29
- import { getAllAvailableFixes } from '../fixes/fixRegistry.js';
30
- import { setLogLevel } from '../utils/logger.js';
31
- import { calculateScore, badgeUrl, badgeMarkdown } from '../utils/scoreCalculator.js';
32
- import { showBanner, showCompactBanner, showHelp } from '../utils/banner.js';
33
- import { saveBaseline, loadBaseline, computeDiff } from '../utils/baseline.js';
34
- import { loadConfig, applyConfigToIssues } from '../utils/config.js';
35
- import { getChangedFiles } from '../utils/changedFiles.js';
36
- import { runMcpServer } from '../mcp/server.js';
37
- import { reportAnalysis, reportHealth, reportCi, reportDiff, reportDetectedIssues, reportExplanation, reportDiagram, reportStructure, reportDependencies, reportHotspots, reportFileInspection, reportOutdated, reportAudit, reportUpgrade, reportCoverage, } from '../reporters/consoleReporter.js';
38
- import { reportAnalysisJson, reportHealthJson, reportCiJson, reportDiffJson, reportExplanationJson, reportDiagramJson, reportStructureJson, reportDependenciesJson, reportHotspotsJson, reportFileJson, reportOutdatedJson, reportAuditJson, reportUpgradeJson, reportCoverageJson, } from '../reporters/jsonReporter.js';
39
- import { reportAnalysisMarkdown, reportHealthMarkdown, reportCiMarkdown, reportDiffMarkdown, reportExplanationMarkdown, reportDiagramMarkdown, reportStructureMarkdown, reportDependenciesMarkdown, reportHotspotsMarkdown, reportFileMarkdown, reportOutdatedMarkdown, reportAuditMarkdown, reportUpgradeMarkdown, reportCoverageMarkdown, } from '../reporters/markdownReporter.js';
40
- import { reportAnalysisSarif, reportHealthSarif, reportCiSarif, issuesToSarif, } from '../reporters/sarifReporter.js';
41
- // ── CLI Setup ─────────────────────────────────────────────
42
- const program = new Command();
43
- program
44
- .name('projscan')
45
- .description('Instant codebase insights - doctor, x-ray, and architecture map for any repository')
46
- .version(pkg.version)
47
- .option('--format <type>', 'output format: console, json, markdown, sarif', 'console')
48
- .option('--config <path>', 'path to .projscanrc config file')
49
- .option('--verbose', 'enable verbose output')
50
- .option('--quiet', 'suppress non-essential output');
51
- function getFormat() {
52
- const opts = program.opts();
53
- const f = opts.format;
54
- if (f === 'json' || f === 'markdown' || f === 'sarif')
55
- return f;
56
- return 'console';
57
- }
58
- function getRootPath() {
59
- return process.cwd();
60
- }
61
- async function loadProjectConfig() {
62
- const opts = program.opts();
63
- const explicit = typeof opts.config === 'string' ? opts.config : undefined;
64
- try {
65
- const { config, source } = await loadConfig(getRootPath(), explicit);
66
- if (source && !opts.quiet && getFormat() === 'console') {
67
- console.error(chalk.dim(` [config: ${path.relative(getRootPath(), source) || source}]`));
68
- }
69
- return config;
70
- }
71
- catch (err) {
72
- const msg = err instanceof Error ? err.message : String(err);
73
- console.error(chalk.red(` Config error: ${msg}`));
74
- process.exit(1);
75
- }
76
- }
77
- async function filterIssuesByChangedFiles(issues, rootPath, baseRef) {
78
- const result = await getChangedFiles(rootPath, baseRef);
79
- if (!result.available) {
80
- if (getFormat() === 'console' && !program.opts().quiet) {
81
- console.error(chalk.yellow(` [--changed-only: ${result.reason ?? 'unavailable'} - reporting all issues]`));
82
- }
83
- return issues;
84
- }
85
- if (getFormat() === 'console' && !program.opts().quiet) {
86
- console.error(chalk.dim(` [--changed-only: base=${result.baseRef}, ${result.files.length} file(s)]`));
87
- }
88
- const set = new Set(result.files);
89
- const filtered = issues.filter((issue) => {
90
- if (!issue.locations || issue.locations.length === 0)
91
- return false;
92
- return issue.locations.some((loc) => set.has(loc.file));
93
- });
94
- const dropped = issues.length - filtered.length;
95
- if (dropped > 0 && !program.opts().quiet) {
96
- const unlocated = issues.filter((i) => !i.locations || i.locations.length === 0).length;
97
- const message = unlocated > 0
98
- ? ` [--changed-only: ${dropped} issue(s) filtered out; ${unlocated} had no file location]`
99
- : ` [--changed-only: ${dropped} issue(s) outside the changed-file set]`;
100
- if (getFormat() === 'console') {
101
- console.error(chalk.dim(message));
102
- }
103
- else {
104
- // For non-console formats, still emit to stderr so the count is visible
105
- // without corrupting machine-readable stdout.
106
- console.error(message.trim());
107
- }
108
- }
109
- return filtered;
110
- }
111
- function setupLogLevel() {
112
- const opts = program.opts();
113
- if (opts.verbose)
114
- setLogLevel('debug');
115
- else if (opts.quiet)
116
- setLogLevel('quiet');
117
- }
118
- function maybeBanner() {
119
- const opts = program.opts();
120
- if (!opts.quiet && getFormat() === 'console') {
121
- try {
122
- showBanner();
123
- }
124
- catch (err) {
125
- console.error(chalk.dim(` [banner error: ${err instanceof Error ? err.message : String(err)}]`));
126
- }
127
- }
128
- }
129
- function maybeCompactBanner() {
130
- const opts = program.opts();
131
- if (!opts.quiet && getFormat() === 'console') {
132
- try {
133
- showCompactBanner();
134
- }
135
- catch (err) {
136
- console.error(chalk.dim(` [banner error: ${err instanceof Error ? err.message : String(err)}]`));
137
- }
138
- }
139
- }
140
- // ── Command: analyze (default) ────────────────────────────
141
- program
142
- .command('analyze', { isDefault: true })
143
- .description('Analyze repository and show project report')
144
- .option('--changed-only', 'only report issues on files changed vs base ref')
145
- .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
146
- .action(async (cmdOpts) => {
147
- setupLogLevel();
148
- maybeBanner();
149
- const rootPath = getRootPath();
150
- const format = getFormat();
151
- const config = await loadProjectConfig();
152
- const spinner = format === 'console' ? ora('Scanning repository...').start() : null;
153
- try {
154
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
155
- if (spinner)
156
- spinner.text = 'Detecting languages...';
157
- const languages = detectLanguages(scan.files);
158
- if (spinner)
159
- spinner.text = 'Detecting frameworks...';
160
- const frameworks = await detectFrameworks(rootPath, scan.files);
161
- if (spinner)
162
- spinner.text = 'Analyzing dependencies...';
163
- const dependencies = await analyzeDependencies(rootPath);
164
- if (spinner)
165
- spinner.text = 'Checking for issues...';
166
- let issues = await collectIssues(rootPath, scan.files);
167
- issues = applyConfigToIssues(issues, config);
168
- if (cmdOpts.changedOnly) {
169
- issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
170
- }
171
- if (spinner)
172
- spinner.stop();
173
- const report = {
174
- projectName: path.basename(rootPath),
175
- rootPath,
176
- scan,
177
- languages,
178
- frameworks,
179
- dependencies,
180
- issues,
181
- timestamp: new Date().toISOString(),
182
- };
183
- switch (format) {
184
- case 'json':
185
- reportAnalysisJson(report);
186
- break;
187
- case 'markdown':
188
- reportAnalysisMarkdown(report);
189
- break;
190
- case 'sarif':
191
- reportAnalysisSarif(issues, pkg.version);
192
- break;
193
- default:
194
- reportAnalysis(report);
195
- }
196
- }
197
- catch (error) {
198
- if (spinner)
199
- spinner.fail('Analysis failed');
200
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
201
- process.exit(1);
202
- }
203
- });
204
- // ── Command: doctor ───────────────────────────────────────
205
- program
206
- .command('doctor')
207
- .description('Evaluate project health and detect issues')
208
- .option('--changed-only', 'only report issues on files changed vs base ref')
209
- .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
210
- .action(async (cmdOpts) => {
211
- setupLogLevel();
212
- maybeCompactBanner();
213
- const rootPath = getRootPath();
214
- const format = getFormat();
215
- const config = await loadProjectConfig();
216
- const spinner = format === 'console' ? ora('Running health checks...').start() : null;
217
- try {
218
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
219
- let issues = await collectIssues(rootPath, scan.files);
220
- issues = applyConfigToIssues(issues, config);
221
- if (cmdOpts.changedOnly) {
222
- issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
223
- }
224
- if (spinner)
225
- spinner.stop();
226
- switch (format) {
227
- case 'json':
228
- reportHealthJson(issues);
229
- break;
230
- case 'markdown':
231
- reportHealthMarkdown(issues);
232
- break;
233
- case 'sarif':
234
- reportHealthSarif(issues, pkg.version);
235
- break;
236
- default:
237
- reportHealth(issues, scan.scanDurationMs);
238
- }
239
- }
240
- catch (error) {
241
- if (spinner)
242
- spinner.fail('Health check failed');
243
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
244
- process.exit(1);
245
- }
246
- });
247
- // ── Command: ci ──────────────────────────────────────────
248
- program
249
- .command('ci')
250
- .description('Run health check for CI pipelines (exits 1 if score below threshold)')
251
- .option('--min-score <score>', 'minimum passing score (0-100)')
252
- .option('--changed-only', 'gate only on issues in files changed vs base ref')
253
- .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
254
- .action(async (cmdOpts) => {
255
- setupLogLevel();
256
- maybeCompactBanner();
257
- const rootPath = getRootPath();
258
- const format = getFormat();
259
- const config = await loadProjectConfig();
260
- try {
261
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
262
- let issues = await collectIssues(rootPath, scan.files);
263
- issues = applyConfigToIssues(issues, config);
264
- if (cmdOpts.changedOnly) {
265
- issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
266
- }
267
- const rawThreshold = cmdOpts.minScore ?? config.minScore ?? 70;
268
- const threshold = Math.max(0, Math.min(100, typeof rawThreshold === 'string' ? parseInt(rawThreshold, 10) || 70 : rawThreshold));
269
- const { score } = calculateScore(issues);
270
- switch (format) {
271
- case 'json':
272
- reportCiJson(issues, threshold);
273
- break;
274
- case 'markdown':
275
- reportCiMarkdown(issues, threshold);
276
- break;
277
- case 'sarif':
278
- reportCiSarif(issues, pkg.version);
279
- break;
280
- default:
281
- reportCi(issues, threshold);
282
- }
283
- if (score < threshold) {
284
- process.exit(1);
285
- }
286
- }
287
- catch (error) {
288
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
289
- process.exit(1);
290
- }
291
- });
292
- // ── Command: diff ─────────────────────────────────────────
293
- program
294
- .command('diff')
295
- .description('Compare health against a saved baseline')
296
- .option('--save-baseline', 'save current health as the baseline')
297
- .option('--baseline <path>', 'path to baseline file (default: .projscan-baseline.json)')
298
- .action(async (cmdOpts) => {
299
- setupLogLevel();
300
- maybeCompactBanner();
301
- const rootPath = getRootPath();
302
- const format = getFormat();
303
- const config = await loadProjectConfig();
304
- try {
305
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
306
- let issues = await collectIssues(rootPath, scan.files);
307
- issues = applyConfigToIssues(issues, config);
308
- const hotspotReport = await analyzeHotspots(rootPath, scan.files, issues, { limit: 20 });
309
- if (cmdOpts.saveBaseline) {
310
- const filePath = await saveBaseline(rootPath, issues, hotspotReport);
311
- const { score, grade } = calculateScore(issues);
312
- console.log(chalk.green(`\n Baseline saved to ${filePath}`));
313
- console.log(` Score: ${chalk.bold(`${grade} (${score}/100)`)}`);
314
- console.log(` Issues: ${issues.length}`);
315
- if (hotspotReport.available) {
316
- console.log(` Hotspots snapshotted: ${hotspotReport.hotspots.length}\n`);
317
- }
318
- else {
319
- console.log('');
320
- }
321
- return;
322
- }
323
- let baseline;
324
- try {
325
- baseline = await loadBaseline(cmdOpts.baseline, rootPath);
326
- }
327
- catch {
328
- console.error(chalk.yellow('\n No baseline found.'));
329
- console.error(` Run ${chalk.bold.cyan('projscan diff --save-baseline')} first to create one.\n`);
330
- process.exit(1);
331
- }
332
- const diff = computeDiff(baseline, issues, hotspotReport);
333
- switch (format) {
334
- case 'json':
335
- reportDiffJson(diff);
336
- break;
337
- case 'markdown':
338
- reportDiffMarkdown(diff);
339
- break;
340
- default:
341
- reportDiff(diff);
342
- }
343
- }
344
- catch (error) {
345
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
346
- process.exit(1);
347
- }
348
- });
349
- // ── Command: fix ──────────────────────────────────────────
350
- program
351
- .command('fix')
352
- .description('Auto-fix detected project issues')
353
- .option('-y, --yes', 'apply fixes without prompting')
354
- .action(async (cmdOpts) => {
355
- setupLogLevel();
356
- maybeCompactBanner();
357
- const rootPath = getRootPath();
358
- const spinner = ora('Detecting issues...').start();
359
- const config = await loadProjectConfig();
360
- try {
361
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
362
- let issues = await collectIssues(rootPath, scan.files);
363
- issues = applyConfigToIssues(issues, config);
364
- const fixes = getAllAvailableFixes(issues);
365
- spinner.stop();
366
- if (fixes.length === 0) {
367
- console.log(`\n ${chalk.green('✓')} ${chalk.bold('No fixable issues found!')}\n`);
368
- return;
369
- }
370
- reportDetectedIssues(issues, fixes);
371
- // Prompt for confirmation
372
- if (!cmdOpts.yes) {
373
- const proceed = await promptYesNo(` Apply ${fixes.length} fix${fixes.length > 1 ? 'es' : ''}? (y/n) `);
374
- if (!proceed) {
375
- console.log(chalk.dim('\n Aborted.\n'));
376
- return;
377
- }
378
- }
379
- // Apply fixes
380
- const results = [];
381
- for (const fix of fixes) {
382
- const fixSpinner = ora(` Applying: ${fix.title}...`).start();
383
- try {
384
- await fix.apply(rootPath);
385
- fixSpinner.succeed(` ${fix.title}`);
386
- results.push({ fix, success: true });
387
- }
388
- catch (error) {
389
- const msg = error instanceof Error ? error.message : String(error);
390
- fixSpinner.fail(` ${fix.title}`);
391
- results.push({ fix, success: false, error: msg });
392
- }
393
- }
394
- const succeeded = results.filter((r) => r.success).length;
395
- const failed = results.filter((r) => !r.success).length;
396
- console.log('');
397
- if (succeeded > 0) {
398
- console.log(` ${chalk.green('✓')} ${succeeded} fix${succeeded > 1 ? 'es' : ''} applied successfully`);
399
- }
400
- if (failed > 0) {
401
- console.log(` ${chalk.red('✗')} ${failed} fix${failed > 1 ? 'es' : ''} failed`);
402
- }
403
- console.log('');
404
- }
405
- catch (error) {
406
- spinner.fail('Fix detection failed');
407
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
408
- process.exit(1);
409
- }
410
- });
411
- // ── Command: file ─────────────────────────────────────────
412
- program
413
- .command('file <file>')
414
- .description('Drill into a file - purpose, risk, ownership, related issues')
415
- .action(async (filePath) => {
416
- setupLogLevel();
417
- maybeCompactBanner();
418
- const rootPath = getRootPath();
419
- const format = getFormat();
420
- const spinner = format === 'console' ? ora('Inspecting file...').start() : null;
421
- try {
422
- const inspection = await inspectFile(rootPath, filePath);
423
- if (spinner)
424
- spinner.stop();
425
- if (!inspection.exists) {
426
- console.error(chalk.red(`\n ${inspection.reason ?? 'File unavailable'}: ${filePath}\n`));
427
- process.exit(1);
428
- }
429
- switch (format) {
430
- case 'json':
431
- reportFileJson(inspection);
432
- break;
433
- case 'markdown':
434
- reportFileMarkdown(inspection);
435
- break;
436
- default:
437
- reportFileInspection(inspection);
438
- }
439
- }
440
- catch (error) {
441
- if (spinner)
442
- spinner.fail('File inspection failed');
443
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
444
- process.exit(1);
445
- }
446
- });
447
- // ── Command: explain ──────────────────────────────────────
448
- program
449
- .command('explain <file>')
450
- .description('Explain a file - its purpose, dependencies, and exports')
451
- .action(async (filePath) => {
452
- setupLogLevel();
453
- maybeCompactBanner();
454
- const format = getFormat();
455
- const absolutePath = path.resolve(filePath);
456
- try {
457
- const content = await fs.readFile(absolutePath, 'utf-8');
458
- const explanation = analyzeFile(absolutePath, content);
459
- switch (format) {
460
- case 'json':
461
- reportExplanationJson(explanation);
462
- break;
463
- case 'markdown':
464
- reportExplanationMarkdown(explanation);
465
- break;
466
- default:
467
- reportExplanation(explanation);
468
- }
469
- }
470
- catch (error) {
471
- if (error.code === 'ENOENT') {
472
- console.error(chalk.red(`File not found: ${filePath}`));
473
- }
474
- else {
475
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
476
- }
477
- process.exit(1);
478
- }
479
- });
480
- // ── Command: diagram ──────────────────────────────────────
481
- program
482
- .command('diagram')
483
- .description('Generate architecture overview diagram')
484
- .action(async () => {
485
- setupLogLevel();
486
- maybeCompactBanner();
487
- const rootPath = getRootPath();
488
- const format = getFormat();
489
- const config = await loadProjectConfig();
490
- const spinner = format === 'console' ? ora('Analyzing architecture...').start() : null;
491
- try {
492
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
493
- const frameworks = await detectFrameworks(rootPath, scan.files);
494
- const layers = buildArchitectureLayers(scan.files, frameworks.frameworks.map((f) => f.name));
495
- if (spinner)
496
- spinner.stop();
497
- switch (format) {
498
- case 'json':
499
- reportDiagramJson(layers);
500
- break;
501
- case 'markdown':
502
- reportDiagramMarkdown(layers);
503
- break;
504
- default:
505
- reportDiagram(layers);
506
- }
507
- }
508
- catch (error) {
509
- if (spinner)
510
- spinner.fail('Diagram generation failed');
511
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
512
- process.exit(1);
513
- }
514
- });
515
- // ── Command: structure ────────────────────────────────────
516
- program
517
- .command('structure')
518
- .description('Show project directory structure')
519
- .action(async () => {
520
- setupLogLevel();
521
- maybeCompactBanner();
522
- const rootPath = getRootPath();
523
- const format = getFormat();
524
- const config = await loadProjectConfig();
525
- const spinner = format === 'console' ? ora('Scanning...').start() : null;
526
- try {
527
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
528
- if (spinner)
529
- spinner.stop();
530
- switch (format) {
531
- case 'json':
532
- reportStructureJson(scan.directoryTree);
533
- break;
534
- case 'markdown':
535
- reportStructureMarkdown(scan.directoryTree);
536
- break;
537
- default:
538
- reportStructure(scan.directoryTree, path.basename(rootPath));
539
- }
540
- }
541
- catch (error) {
542
- if (spinner)
543
- spinner.fail('Structure scan failed');
544
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
545
- process.exit(1);
546
- }
547
- });
548
- // ── Command: dependencies ─────────────────────────────────
549
- program
550
- .command('dependencies')
551
- .description('Analyze project dependencies')
552
- .action(async () => {
553
- setupLogLevel();
554
- maybeCompactBanner();
555
- const rootPath = getRootPath();
556
- const format = getFormat();
557
- const spinner = format === 'console' ? ora('Analyzing dependencies...').start() : null;
558
- try {
559
- const report = await analyzeDependencies(rootPath);
560
- if (spinner)
561
- spinner.stop();
562
- if (!report) {
563
- console.log(chalk.yellow('\n No package.json found in this directory.\n'));
564
- return;
565
- }
566
- switch (format) {
567
- case 'json':
568
- reportDependenciesJson(report);
569
- break;
570
- case 'markdown':
571
- reportDependenciesMarkdown(report);
572
- break;
573
- default:
574
- reportDependencies(report);
575
- }
576
- }
577
- catch (error) {
578
- if (spinner)
579
- spinner.fail('Dependency analysis failed');
580
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
581
- process.exit(1);
582
- }
583
- });
584
- // ── Command: hotspots ─────────────────────────────────────
585
- program
586
- .command('hotspots')
587
- .description('Rank files by risk (git churn × complexity × open issues)')
588
- .option('--limit <n>', 'number of hotspots to show')
589
- .option('--since <when>', 'git history window (e.g. "6 months ago", "2024-01-01")')
590
- .action(async (cmdOpts) => {
591
- setupLogLevel();
592
- maybeCompactBanner();
593
- const rootPath = getRootPath();
594
- const format = getFormat();
595
- const config = await loadProjectConfig();
596
- const spinner = format === 'console' ? ora('Analyzing hotspots...').start() : null;
597
- try {
598
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
599
- let issues = await collectIssues(rootPath, scan.files);
600
- issues = applyConfigToIssues(issues, config);
601
- const limitRaw = cmdOpts.limit ?? config.hotspots?.limit ?? 10;
602
- const limit = Math.max(1, Math.min(100, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 10 : limitRaw));
603
- const since = cmdOpts.since ?? config.hotspots?.since ?? '12 months ago';
604
- const coverageReport = await parseCoverage(rootPath);
605
- const report = await analyzeHotspots(rootPath, scan.files, issues, {
606
- since,
607
- limit,
608
- coverage: coverageReport.available ? coverageMap(coverageReport) : undefined,
609
- });
610
- if (spinner)
611
- spinner.stop();
612
- switch (format) {
613
- case 'json':
614
- reportHotspotsJson(report);
615
- break;
616
- case 'markdown':
617
- reportHotspotsMarkdown(report);
618
- break;
619
- default:
620
- reportHotspots(report);
621
- }
622
- }
623
- catch (error) {
624
- if (spinner)
625
- spinner.fail('Hotspot analysis failed');
626
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
627
- process.exit(1);
628
- }
629
- });
630
- // ── Command: outdated ─────────────────────────────────────
631
- program
632
- .command('outdated')
633
- .description('Detect outdated dependencies (offline - compares declared vs installed)')
634
- .action(async () => {
635
- setupLogLevel();
636
- maybeCompactBanner();
637
- const rootPath = getRootPath();
638
- const format = getFormat();
639
- const spinner = format === 'console' ? ora('Checking dependencies...').start() : null;
640
- try {
641
- const report = await detectOutdated(rootPath);
642
- if (spinner)
643
- spinner.stop();
644
- switch (format) {
645
- case 'json':
646
- reportOutdatedJson(report);
647
- break;
648
- case 'markdown':
649
- reportOutdatedMarkdown(report);
650
- break;
651
- case 'sarif':
652
- console.log(JSON.stringify(issuesToSarif([], pkg.version), null, 2));
653
- break;
654
- default:
655
- reportOutdated(report);
656
- }
657
- }
658
- catch (error) {
659
- if (spinner)
660
- spinner.fail('Outdated check failed');
661
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
662
- process.exit(1);
663
- }
664
- });
665
- // ── Command: audit ────────────────────────────────────────
666
- program
667
- .command('audit')
668
- .description('Run npm audit and surface vulnerabilities (SARIF supported)')
669
- .option('--timeout <ms>', 'override npm audit timeout (default 60000)')
670
- .action(async (cmdOpts) => {
671
- setupLogLevel();
672
- maybeCompactBanner();
673
- const rootPath = getRootPath();
674
- const format = getFormat();
675
- const spinner = format === 'console' ? ora('Running npm audit...').start() : null;
676
- try {
677
- const timeoutMs = cmdOpts.timeout ? Math.max(5_000, parseInt(cmdOpts.timeout, 10)) : undefined;
678
- const report = await runAudit(rootPath, timeoutMs !== undefined ? { timeoutMs } : {});
679
- if (spinner)
680
- spinner.stop();
681
- switch (format) {
682
- case 'json':
683
- reportAuditJson(report);
684
- break;
685
- case 'markdown':
686
- reportAuditMarkdown(report);
687
- break;
688
- case 'sarif':
689
- console.log(JSON.stringify(issuesToSarif(auditFindingsToIssues(report), pkg.version), null, 2));
690
- break;
691
- default:
692
- reportAudit(report);
693
- }
694
- }
695
- catch (error) {
696
- if (spinner)
697
- spinner.fail('Audit failed');
698
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
699
- process.exit(1);
700
- }
701
- });
702
- // ── Command: upgrade ──────────────────────────────────────
703
- program
704
- .command('upgrade <package>')
705
- .description('Preview the impact of upgrading a package (offline - reads local CHANGELOG + importers)')
706
- .action(async (pkgName) => {
707
- setupLogLevel();
708
- maybeCompactBanner();
709
- const rootPath = getRootPath();
710
- const format = getFormat();
711
- const config = await loadProjectConfig();
712
- const spinner = format === 'console' ? ora(`Previewing ${pkgName}...`).start() : null;
713
- try {
714
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
715
- const preview = await previewUpgrade(rootPath, pkgName, scan.files);
716
- if (spinner)
717
- spinner.stop();
718
- switch (format) {
719
- case 'json':
720
- reportUpgradeJson(preview);
721
- break;
722
- case 'markdown':
723
- reportUpgradeMarkdown(preview);
724
- break;
725
- default:
726
- reportUpgrade(preview);
727
- }
728
- }
729
- catch (error) {
730
- if (spinner)
731
- spinner.fail('Upgrade preview failed');
732
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
733
- process.exit(1);
734
- }
735
- });
736
- // ── Command: search ───────────────────────────────────────
737
- program
738
- .command('search <query...>')
739
- .description('Ranked search - BM25 by default, semantic or hybrid when @xenova/transformers peer is installed')
740
- .option('--scope <scope>', 'auto | content | symbols | files', 'auto')
741
- .option('--mode <mode>', 'lexical | semantic | hybrid (content/auto scope only)', 'lexical')
742
- .option('--semantic', 'shortcut for --mode semantic')
743
- .option('--limit <n>', 'max results', '15')
744
- .action(async (queryParts, cmdOpts) => {
745
- setupLogLevel();
746
- maybeCompactBanner();
747
- const rootPath = getRootPath();
748
- const format = getFormat();
749
- const config = await loadProjectConfig();
750
- const query = queryParts.join(' ').trim();
751
- if (!query) {
752
- console.error(chalk.red('\n search requires a non-empty query\n'));
753
- process.exit(1);
754
- }
755
- const limitRaw = cmdOpts.limit ?? 15;
756
- const limit = Math.max(1, Math.min(200, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 15 : limitRaw));
757
- const scope = String(cmdOpts.scope ?? 'auto');
758
- const spinner = format === 'console' ? ora('Indexing repository...').start() : null;
759
- try {
760
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
761
- const cached = await loadCachedGraph(rootPath);
762
- const graph = await buildCodeGraph(rootPath, scan.files, cached);
763
- await saveCachedGraph(rootPath, graph);
764
- if (spinner)
765
- spinner.text = 'Searching...';
766
- let results;
767
- if (scope === 'symbols') {
768
- const q = query.toLowerCase();
769
- const matches = [];
770
- for (const [file, entry] of graph.files) {
771
- for (const exp of entry.exports) {
772
- if (exp.name.toLowerCase().includes(q)) {
773
- matches.push({ symbol: exp.name, kind: exp.kind, file, line: exp.line });
774
- }
775
- }
776
- }
777
- matches.sort((a, b) => {
778
- const aExact = a.symbol.toLowerCase() === q ? 0 : a.symbol.toLowerCase().startsWith(q) ? 1 : 2;
779
- const bExact = b.symbol.toLowerCase() === q ? 0 : b.symbol.toLowerCase().startsWith(q) ? 1 : 2;
780
- return aExact - bExact;
781
- });
782
- results = { scope, query, matches: matches.slice(0, limit), total: matches.length };
783
- }
784
- else if (scope === 'files') {
785
- const q = query.toLowerCase();
786
- const matches = scan.files
787
- .filter((f) => f.relativePath.toLowerCase().includes(q))
788
- .slice(0, limit)
789
- .map((f) => ({ file: f.relativePath, sizeBytes: f.sizeBytes }));
790
- results = { scope, query, matches, total: matches.length };
791
- }
792
- else {
793
- const mode = cmdOpts.semantic ? 'semantic' : String(cmdOpts.mode ?? 'lexical');
794
- const index = await buildSearchIndex(rootPath, scan.files, graph);
795
- const lexicalHits = searchIndex(index, query, { limit });
796
- const tokens = expandQuery(query);
797
- if (mode === 'lexical') {
798
- const withExcerpts = await attachExcerpts(rootPath, lexicalHits, tokens);
799
- results = {
800
- scope: scope === 'auto' ? 'content' : scope,
801
- mode: 'lexical',
802
- query,
803
- queryTokens: tokens,
804
- matches: withExcerpts,
805
- total: withExcerpts.length,
806
- };
807
- }
808
- else {
809
- const available = await isSemanticAvailable();
810
- if (!available) {
811
- if (spinner)
812
- spinner.stop();
813
- console.error(chalk.red(`\n Semantic search requires the optional peer @xenova/transformers.\n Install it with: ${chalk.bold('npm install @xenova/transformers')}\n`));
814
- process.exit(1);
815
- }
816
- if (spinner)
817
- spinner.text = 'Building semantic index (first run may take ~10s + model download)...';
818
- const semIndex = await buildSemanticIndex(rootPath, scan.files, {
819
- onFirstLoad: (m) => spinner?.text && (spinner.text = m),
820
- onProgress: (d, t) => {
821
- if (spinner)
822
- spinner.text = `Embedding files... ${d}/${t}`;
823
- },
824
- });
825
- if (!semIndex) {
826
- if (spinner)
827
- spinner.fail('Semantic index build failed');
828
- process.exit(1);
829
- }
830
- if (spinner)
831
- spinner.text = 'Searching...';
832
- const semHits = await semanticSearch(semIndex, query, { limit });
833
- if (mode === 'semantic') {
834
- const enriched = await attachExcerpts(rootPath, semHits.map((h) => ({
835
- file: h.file,
836
- score: h.score,
837
- matched: [],
838
- symbolMatch: false,
839
- pathMatch: false,
840
- excerpt: '',
841
- line: 0,
842
- })), tokens);
843
- results = {
844
- scope: scope === 'auto' ? 'content' : scope,
845
- mode: 'semantic',
846
- query,
847
- model: semIndex.model,
848
- matches: enriched,
849
- total: enriched.length,
850
- };
851
- }
852
- else {
853
- // hybrid
854
- const fused = reciprocalRankFusion([lexicalHits, semHits]).slice(0, limit);
855
- const enriched = await attachExcerpts(rootPath, fused.map((f) => ({
856
- file: f.file,
857
- score: f.score,
858
- matched: [],
859
- symbolMatch: false,
860
- pathMatch: false,
861
- excerpt: '',
862
- line: 0,
863
- })), tokens);
864
- results = {
865
- scope: scope === 'auto' ? 'content' : scope,
866
- mode: 'hybrid',
867
- query,
868
- queryTokens: tokens,
869
- model: semIndex.model,
870
- matches: enriched,
871
- total: enriched.length,
872
- };
873
- }
874
- }
875
- }
876
- if (spinner)
877
- spinner.stop();
878
- if (format === 'json') {
879
- console.log(JSON.stringify({ search: results }, null, 2));
880
- return;
881
- }
882
- if (format === 'markdown') {
883
- const r = results;
884
- console.log(`# Search - \`${r.query}\` (${r.scope})\n`);
885
- if (r.matches.length === 0) {
886
- console.log('_No matches._');
887
- return;
888
- }
889
- for (const m of r.matches) {
890
- if ('symbol' in m)
891
- console.log(`- \`${m.symbol}\` (${m.kind}) → \`${m.file}:${m.line}\``);
892
- else if ('score' in m)
893
- console.log(`- \`${m.file}:${m.line}\` - score ${m.score} - ${m.excerpt ?? ''}`);
894
- else
895
- console.log(`- \`${m.file}\``);
896
- }
897
- return;
898
- }
899
- // Console
900
- const r = results;
901
- console.log(`\n ${chalk.bold(`Search - "${query}"`)} ${chalk.dim(`[${r.scope}]`)}`);
902
- if (r.queryTokens)
903
- console.log(chalk.dim(` tokens: ${r.queryTokens.join(', ')}`));
904
- console.log(chalk.dim(' ─'.repeat(20)));
905
- if (r.matches.length === 0) {
906
- console.log(chalk.yellow('\n No matches.\n'));
907
- return;
908
- }
909
- for (const m of r.matches) {
910
- if ('symbol' in m) {
911
- console.log(` ${chalk.bold(String(m.symbol))} ${chalk.dim(`(${m.kind})`)} → ${chalk.dim(`${m.file}:${m.line}`)}`);
912
- }
913
- else if ('score' in m) {
914
- const score = typeof m.score === 'number' ? m.score.toFixed(1) : String(m.score);
915
- console.log(` ${chalk.bold(score.padStart(5))} ${chalk.cyan(String(m.file))}${m.line ? chalk.dim(`:${m.line}`) : ''}`);
916
- if (m.excerpt)
917
- console.log(` ${chalk.dim(String(m.excerpt))}`);
918
- }
919
- else {
920
- console.log(` ${chalk.cyan(String(m.file))}`);
921
- }
922
- }
923
- console.log('');
924
- }
925
- catch (error) {
926
- if (spinner)
927
- spinner.fail('Search failed');
928
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
929
- process.exit(1);
930
- }
931
- });
932
- // ── Command: coverage ─────────────────────────────────────
933
- program
934
- .command('coverage')
935
- .description('Join test coverage with hotspots - surface the scariest untested files')
936
- .option('--limit <n>', 'limit number of entries shown', '30')
937
- .action(async (cmdOpts) => {
938
- setupLogLevel();
939
- maybeCompactBanner();
940
- const rootPath = getRootPath();
941
- const format = getFormat();
942
- const config = await loadProjectConfig();
943
- const spinner = format === 'console' ? ora('Parsing coverage...').start() : null;
944
- try {
945
- const coverage = await parseCoverage(rootPath);
946
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
947
- const issues = await collectIssues(rootPath, scan.files);
948
- const limitRaw = cmdOpts.limit ?? 30;
949
- const limit = Math.max(1, Math.min(200, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 30 : limitRaw));
950
- const hotspots = await analyzeHotspots(rootPath, scan.files, issues, {
951
- limit,
952
- coverage: coverage.available ? coverageMap(coverage) : undefined,
953
- });
954
- const joined = joinCoverageWithHotspots(hotspots, coverage);
955
- if (spinner)
956
- spinner.stop();
957
- switch (format) {
958
- case 'json':
959
- reportCoverageJson(joined);
960
- break;
961
- case 'markdown':
962
- reportCoverageMarkdown(joined);
963
- break;
964
- default:
965
- reportCoverage(joined);
966
- }
967
- }
968
- catch (error) {
969
- if (spinner)
970
- spinner.fail('Coverage analysis failed');
971
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
972
- process.exit(1);
973
- }
974
- });
975
- // ── Command: mcp ──────────────────────────────────────────
976
- program
977
- .command('mcp')
978
- .description('Run projscan as an MCP server (stdio) for AI coding agents')
979
- .action(async () => {
980
- setLogLevel('quiet');
981
- const rootPath = getRootPath();
982
- try {
983
- await runMcpServer(rootPath);
984
- }
985
- catch (error) {
986
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
987
- process.exit(1);
988
- }
989
- });
990
- // ── Command: badge ────────────────────────────────────────
991
- program
992
- .command('badge')
993
- .description('Generate a health badge for your README')
994
- .option('--markdown', 'output as markdown image link')
995
- .action(async (cmdOpts) => {
996
- setupLogLevel();
997
- maybeCompactBanner();
998
- const rootPath = getRootPath();
999
- const spinner = ora('Calculating health score...').start();
1000
- const config = await loadProjectConfig();
1001
- try {
1002
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
1003
- let issues = await collectIssues(rootPath, scan.files);
1004
- issues = applyConfigToIssues(issues, config);
1005
- const { score, grade } = calculateScore(issues);
1006
- spinner.stop();
1007
- const gradeColor = grade === 'A' || grade === 'B' ? chalk.green : grade === 'C' ? chalk.yellow : chalk.red;
1008
- console.log(`\n Health Score: ${gradeColor(chalk.bold(`${grade} (${score}/100)`))}\n`);
1009
- if (cmdOpts.markdown) {
1010
- console.log(` ${badgeMarkdown(grade)}\n`);
1011
- }
1012
- else {
1013
- console.log(` ${chalk.bold('Badge URL:')}`);
1014
- console.log(` ${badgeUrl(grade)}\n`);
1015
- console.log(` ${chalk.bold('Markdown:')}`);
1016
- console.log(` ${badgeMarkdown(grade)}\n`);
1017
- }
1018
- console.log(chalk.dim(' Add this to your README to show your project health score.\n'));
1019
- }
1020
- catch (error) {
1021
- spinner.fail('Badge generation failed');
1022
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
1023
- process.exit(1);
1024
- }
1025
- });
1026
- // ── File Analysis (for explain command) ───────────────────
1027
- function analyzeFile(filePath, content) {
1028
- const lines = content.split('\n');
1029
- const imports = extractImports(content);
1030
- const exports = extractExports(content);
1031
- const purpose = inferPurpose(filePath, exports);
1032
- const potentialIssues = detectFileIssues(content, lines.length);
1033
- return {
1034
- filePath: path.relative(process.cwd(), filePath),
1035
- purpose,
1036
- imports,
1037
- exports,
1038
- potentialIssues,
1039
- lineCount: lines.length,
1040
- };
1041
- }
1042
- // ── Architecture Layer Detection ──────────────────────────
1043
- function buildArchitectureLayers(files, frameworkNames) {
1044
- const layers = [];
1045
- const dirs = new Set(files.map((f) => f.directory.split(path.sep)[0]).filter(Boolean));
1046
- // Frontend layer
1047
- const frontendDirs = ['pages', 'components', 'views', 'layouts', 'public', 'app', 'styles'];
1048
- const frontendMatches = frontendDirs.filter((d) => dirs.has(d) || dirs.has(`src/${d}`));
1049
- const frontendFrameworks = frameworkNames.filter((f) => ['React', 'Next.js', 'Vue.js', 'Nuxt.js', 'Svelte', 'SvelteKit', 'Angular', 'Solid.js'].includes(f));
1050
- if (frontendMatches.length > 0 || frontendFrameworks.length > 0) {
1051
- layers.push({
1052
- name: 'Frontend',
1053
- technologies: frontendFrameworks.length > 0 ? frontendFrameworks : ['Static'],
1054
- directories: frontendMatches,
1055
- });
1056
- }
1057
- // API layer
1058
- const apiDirs = ['api', 'routes', 'controllers', 'endpoints'];
1059
- const apiMatches = apiDirs.filter((d) => dirs.has(d) || dirs.has(`src/${d}`));
1060
- const apiFrameworks = frameworkNames.filter((f) => ['Express', 'Fastify', 'NestJS', 'Hono', 'Koa', 'Apollo Server', 'tRPC'].includes(f));
1061
- if (apiMatches.length > 0 || apiFrameworks.length > 0) {
1062
- layers.push({
1063
- name: 'API Layer',
1064
- technologies: apiFrameworks.length > 0 ? apiFrameworks : ['HTTP'],
1065
- directories: apiMatches,
1066
- });
1067
- }
1068
- // Services layer
1069
- const serviceDirs = ['services', 'lib', 'core', 'domain', 'modules'];
1070
- const serviceMatches = serviceDirs.filter((d) => dirs.has(d) || dirs.has(`src/${d}`));
1071
- if (serviceMatches.length > 0) {
1072
- layers.push({
1073
- name: 'Services',
1074
- technologies: inferServiceTech(files, serviceMatches),
1075
- directories: serviceMatches,
1076
- });
1077
- }
1078
- // Database layer
1079
- const dbDirs = ['db', 'database', 'prisma', 'migrations', 'models', 'entities'];
1080
- const dbMatches = dbDirs.filter((d) => dirs.has(d) || dirs.has(`src/${d}`));
1081
- const dbFrameworks = frameworkNames.filter((f) => ['Prisma', 'Drizzle ORM', 'Mongoose', 'TypeORM', 'Sequelize'].includes(f));
1082
- if (dbMatches.length > 0 || dbFrameworks.length > 0) {
1083
- layers.push({
1084
- name: 'Database',
1085
- technologies: dbFrameworks.length > 0 ? dbFrameworks : ['Database'],
1086
- directories: dbMatches,
1087
- });
1088
- }
1089
- // If no layers detected, show a generic one
1090
- if (layers.length === 0) {
1091
- const topDirs = [...dirs].slice(0, 5);
1092
- layers.push({
1093
- name: 'Application',
1094
- technologies: frameworkNames.length > 0 ? frameworkNames : ['Unknown'],
1095
- directories: topDirs,
1096
- });
1097
- }
1098
- return layers;
1099
- }
1100
- function inferServiceTech(files, serviceDirs) {
1101
- const techs = [];
1102
- const serviceFiles = files.filter((f) => serviceDirs.some((d) => f.directory.startsWith(d)));
1103
- const hasTsFiles = serviceFiles.some((f) => f.extension === '.ts' || f.extension === '.tsx');
1104
- const hasJsFiles = serviceFiles.some((f) => f.extension === '.js' || f.extension === '.jsx');
1105
- if (hasTsFiles)
1106
- techs.push('TypeScript');
1107
- else if (hasJsFiles)
1108
- techs.push('JavaScript');
1109
- if (techs.length === 0)
1110
- techs.push('Mixed');
1111
- return techs;
1112
- }
1113
- // ── Helpers ───────────────────────────────────────────────
1114
- function promptYesNo(question) {
1115
- return new Promise((resolve) => {
1116
- const rl = readline.createInterface({
1117
- input: process.stdin,
1118
- output: process.stdout,
1119
- });
1120
- rl.question(question, (answer) => {
1121
- rl.close();
1122
- resolve(answer.toLowerCase().startsWith('y'));
1123
- });
1124
- });
1125
- }
1126
- // ── Command: help ─────────────────────────────────────────
1127
- program
1128
- .command('help')
1129
- .description('Show detailed help with all commands and options')
1130
- .action(() => {
1131
- showHelp();
1132
- });
1133
- // ── Run ───────────────────────────────────────────────────
2
+ import { program } from './_shared.js';
3
+ import { registerAnalyze } from './commands/analyze.js';
4
+ import { registerDoctor } from './commands/doctor.js';
5
+ import { registerCi } from './commands/ci.js';
6
+ import { registerDiff } from './commands/diff.js';
7
+ import { registerFix } from './commands/fix.js';
8
+ import { registerFile } from './commands/file.js';
9
+ import { registerExplain } from './commands/explain.js';
10
+ import { registerDiagram } from './commands/diagram.js';
11
+ import { registerStructure } from './commands/structure.js';
12
+ import { registerDependencies } from './commands/dependencies.js';
13
+ import { registerHotspots } from './commands/hotspots.js';
14
+ import { registerCoupling } from './commands/coupling.js';
15
+ import { registerPrDiff } from './commands/prDiff.js';
16
+ import { registerWorkspaces } from './commands/workspaces.js';
17
+ import { registerOutdated } from './commands/outdated.js';
18
+ import { registerAudit } from './commands/audit.js';
19
+ import { registerUpgrade } from './commands/upgrade.js';
20
+ import { registerSearch } from './commands/search.js';
21
+ import { registerCoverage } from './commands/coverage.js';
22
+ import { registerMcp } from './commands/mcp.js';
23
+ import { registerBadge } from './commands/badge.js';
24
+ import { registerHelp } from './commands/help.js';
25
+ registerAnalyze();
26
+ registerDoctor();
27
+ registerCi();
28
+ registerDiff();
29
+ registerFix();
30
+ registerFile();
31
+ registerExplain();
32
+ registerDiagram();
33
+ registerStructure();
34
+ registerDependencies();
35
+ registerHotspots();
36
+ registerCoupling();
37
+ registerPrDiff();
38
+ registerWorkspaces();
39
+ registerOutdated();
40
+ registerAudit();
41
+ registerUpgrade();
42
+ registerSearch();
43
+ registerCoverage();
44
+ registerMcp();
45
+ registerBadge();
46
+ registerHelp();
1134
47
  program.parse();
1135
48
  //# sourceMappingURL=index.js.map