projscan 0.11.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 (203) hide show
  1. package/README.md +45 -26
  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 -1416
  74. package/dist/cli/index.js.map +1 -1
  75. package/dist/core/couplingAnalyzer.d.ts +1 -1
  76. package/dist/core/couplingAnalyzer.js +3 -3
  77. package/dist/core/hotspotAnalyzer.js +2 -2
  78. package/dist/core/languages/LanguageAdapter.d.ts +1 -1
  79. package/dist/core/languages/goAdapter.js +7 -5
  80. package/dist/core/languages/goAdapter.js.map +1 -1
  81. package/dist/core/languages/goCallSites.d.ts +20 -0
  82. package/dist/core/languages/goCallSites.js +42 -0
  83. package/dist/core/languages/goCallSites.js.map +1 -0
  84. package/dist/core/languages/goCyclomatic.d.ts +1 -1
  85. package/dist/core/languages/goCyclomatic.js +2 -2
  86. package/dist/core/languages/goExports.d.ts +1 -1
  87. package/dist/core/languages/goExports.js +1 -1
  88. package/dist/core/languages/goManifests.d.ts +1 -1
  89. package/dist/core/languages/goManifests.js +2 -2
  90. package/dist/core/languages/javaAdapter.d.ts +2 -0
  91. package/dist/core/languages/javaAdapter.js +148 -0
  92. package/dist/core/languages/javaAdapter.js.map +1 -0
  93. package/dist/core/languages/javaCallSites.d.ts +16 -0
  94. package/dist/core/languages/javaCallSites.js +45 -0
  95. package/dist/core/languages/javaCallSites.js.map +1 -0
  96. package/dist/core/languages/javaCyclomatic.d.ts +21 -0
  97. package/dist/core/languages/javaCyclomatic.js +49 -0
  98. package/dist/core/languages/javaCyclomatic.js.map +1 -0
  99. package/dist/core/languages/javaExports.d.ts +25 -0
  100. package/dist/core/languages/javaExports.js +80 -0
  101. package/dist/core/languages/javaExports.js.map +1 -0
  102. package/dist/core/languages/javaImports.d.ts +25 -0
  103. package/dist/core/languages/javaImports.js +49 -0
  104. package/dist/core/languages/javaImports.js.map +1 -0
  105. package/dist/core/languages/javaManifests.d.ts +25 -0
  106. package/dist/core/languages/javaManifests.js +86 -0
  107. package/dist/core/languages/javaManifests.js.map +1 -0
  108. package/dist/core/languages/pythonAdapter.js +3 -1
  109. package/dist/core/languages/pythonAdapter.js.map +1 -1
  110. package/dist/core/languages/pythonCallSites.d.ts +19 -0
  111. package/dist/core/languages/pythonCallSites.js +40 -0
  112. package/dist/core/languages/pythonCallSites.js.map +1 -0
  113. package/dist/core/languages/registry.js +3 -1
  114. package/dist/core/languages/registry.js.map +1 -1
  115. package/dist/core/languages/rubyAdapter.d.ts +2 -0
  116. package/dist/core/languages/rubyAdapter.js +131 -0
  117. package/dist/core/languages/rubyAdapter.js.map +1 -0
  118. package/dist/core/languages/rubyCallSites.d.ts +16 -0
  119. package/dist/core/languages/rubyCallSites.js +34 -0
  120. package/dist/core/languages/rubyCallSites.js.map +1 -0
  121. package/dist/core/languages/rubyCyclomatic.d.ts +19 -0
  122. package/dist/core/languages/rubyCyclomatic.js +47 -0
  123. package/dist/core/languages/rubyCyclomatic.js.map +1 -0
  124. package/dist/core/languages/rubyExports.d.ts +24 -0
  125. package/dist/core/languages/rubyExports.js +53 -0
  126. package/dist/core/languages/rubyExports.js.map +1 -0
  127. package/dist/core/languages/rubyImports.d.ts +12 -0
  128. package/dist/core/languages/rubyImports.js +75 -0
  129. package/dist/core/languages/rubyImports.js.map +1 -0
  130. package/dist/core/languages/rubyManifests.d.ts +20 -0
  131. package/dist/core/languages/rubyManifests.js +55 -0
  132. package/dist/core/languages/rubyManifests.js.map +1 -0
  133. package/dist/core/languages/treeSitterLoader.js +3 -1
  134. package/dist/core/languages/treeSitterLoader.js.map +1 -1
  135. package/dist/core/monorepo.js +5 -5
  136. package/dist/core/outdatedDetector.d.ts +13 -2
  137. package/dist/core/outdatedDetector.js +86 -16
  138. package/dist/core/outdatedDetector.js.map +1 -1
  139. package/dist/core/prDiff.d.ts +1 -1
  140. package/dist/core/prDiff.js +2 -2
  141. package/dist/grammars/tree-sitter-java.wasm +0 -0
  142. package/dist/grammars/tree-sitter-ruby.wasm +0 -0
  143. package/dist/mcp/server.js +0 -22
  144. package/dist/mcp/server.js.map +1 -1
  145. package/dist/mcp/tools/_shared.d.ts +24 -0
  146. package/dist/mcp/tools/_shared.js +82 -0
  147. package/dist/mcp/tools/_shared.js.map +1 -0
  148. package/dist/mcp/tools/analyze.d.ts +2 -0
  149. package/dist/mcp/tools/analyze.js +55 -0
  150. package/dist/mcp/tools/analyze.js.map +1 -0
  151. package/dist/mcp/tools/audit.d.ts +2 -0
  152. package/dist/mcp/tools/audit.js +32 -0
  153. package/dist/mcp/tools/audit.js.map +1 -0
  154. package/dist/mcp/tools/coupling.d.ts +2 -0
  155. package/dist/mcp/tools/coupling.js +67 -0
  156. package/dist/mcp/tools/coupling.js.map +1 -0
  157. package/dist/mcp/tools/coverage.d.ts +2 -0
  158. package/dist/mcp/tools/coverage.js +53 -0
  159. package/dist/mcp/tools/coverage.js.map +1 -0
  160. package/dist/mcp/tools/dependencies.d.ts +2 -0
  161. package/dist/mcp/tools/dependencies.js +16 -0
  162. package/dist/mcp/tools/dependencies.js.map +1 -0
  163. package/dist/mcp/tools/doctor.d.ts +2 -0
  164. package/dist/mcp/tools/doctor.js +30 -0
  165. package/dist/mcp/tools/doctor.js.map +1 -0
  166. package/dist/mcp/tools/explain.d.ts +2 -0
  167. package/dist/mcp/tools/explain.js +30 -0
  168. package/dist/mcp/tools/explain.js.map +1 -0
  169. package/dist/mcp/tools/file.d.ts +2 -0
  170. package/dist/mcp/tools/file.js +22 -0
  171. package/dist/mcp/tools/file.js.map +1 -0
  172. package/dist/mcp/tools/graph.d.ts +2 -0
  173. package/dist/mcp/tools/graph.js +69 -0
  174. package/dist/mcp/tools/graph.js.map +1 -0
  175. package/dist/mcp/tools/hotspots.d.ts +2 -0
  176. package/dist/mcp/tools/hotspots.js +64 -0
  177. package/dist/mcp/tools/hotspots.js.map +1 -0
  178. package/dist/mcp/tools/outdated.d.ts +2 -0
  179. package/dist/mcp/tools/outdated.js +36 -0
  180. package/dist/mcp/tools/outdated.js.map +1 -0
  181. package/dist/mcp/tools/prDiff.d.ts +2 -0
  182. package/dist/mcp/tools/prDiff.js +38 -0
  183. package/dist/mcp/tools/prDiff.js.map +1 -0
  184. package/dist/mcp/tools/search.d.ts +2 -0
  185. package/dist/mcp/tools/search.js +167 -0
  186. package/dist/mcp/tools/search.js.map +1 -0
  187. package/dist/mcp/tools/structure.d.ts +2 -0
  188. package/dist/mcp/tools/structure.js +34 -0
  189. package/dist/mcp/tools/structure.js.map +1 -0
  190. package/dist/mcp/tools/upgrade.d.ts +2 -0
  191. package/dist/mcp/tools/upgrade.js +38 -0
  192. package/dist/mcp/tools/upgrade.js.map +1 -0
  193. package/dist/mcp/tools/workspaces.d.ts +2 -0
  194. package/dist/mcp/tools/workspaces.js +13 -0
  195. package/dist/mcp/tools/workspaces.js.map +1 -0
  196. package/dist/mcp/tools.d.ts +12 -6
  197. package/dist/mcp/tools.js +40 -854
  198. package/dist/mcp/tools.js.map +1 -1
  199. package/dist/tool-manifest.json +358 -0
  200. package/dist/types.d.ts +8 -6
  201. package/dist/utils/config.js +0 -10
  202. package/dist/utils/config.js.map +1 -1
  203. package/package.json +6 -3
package/dist/cli/index.js CHANGED
@@ -1,1419 +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 { computeCoupling, filterCoupling } from '../core/couplingAnalyzer.js';
26
- import { computePrDiff } from '../core/prDiff.js';
27
- import { detectWorkspaces, filterFilesByPackage } from '../core/monorepo.js';
28
- import { describeTelemetryConfig, aggregateTelemetry } from '../core/telemetry.js';
29
- import { buildSearchIndex, search as searchIndex, attachExcerpts, expandQuery } from '../core/searchIndex.js';
30
- import { buildSemanticIndex, semanticSearch, reciprocalRankFusion, } from '../core/semanticSearch.js';
31
- import { isSemanticAvailable } from '../core/embeddings.js';
32
- import { inspectFile, extractImports, extractExports, inferPurpose, detectFileIssues, } from '../core/fileInspector.js';
33
- import { getAllAvailableFixes } from '../fixes/fixRegistry.js';
34
- import { setLogLevel } from '../utils/logger.js';
35
- import { calculateScore, badgeUrl, badgeMarkdown } from '../utils/scoreCalculator.js';
36
- import { showBanner, showCompactBanner, showHelp } from '../utils/banner.js';
37
- import { saveBaseline, loadBaseline, computeDiff } from '../utils/baseline.js';
38
- import { loadConfig, applyConfigToIssues } from '../utils/config.js';
39
- import { getChangedFiles } from '../utils/changedFiles.js';
40
- import { runMcpServer } from '../mcp/server.js';
41
- import { reportAnalysis, reportHealth, reportCi, reportDiff, reportDetectedIssues, reportExplanation, reportDiagram, reportStructure, reportDependencies, reportHotspots, reportFileInspection, reportOutdated, reportAudit, reportUpgrade, reportCoverage, reportCoupling, reportPrDiff, reportWorkspaces, } from '../reporters/consoleReporter.js';
42
- import { reportAnalysisJson, reportHealthJson, reportCiJson, reportDiffJson, reportExplanationJson, reportDiagramJson, reportStructureJson, reportDependenciesJson, reportHotspotsJson, reportFileJson, reportOutdatedJson, reportAuditJson, reportUpgradeJson, reportCoverageJson, reportCouplingJson, reportPrDiffJson, reportWorkspacesJson, } from '../reporters/jsonReporter.js';
43
- import { reportAnalysisMarkdown, reportHealthMarkdown, reportCiMarkdown, reportDiffMarkdown, reportExplanationMarkdown, reportDiagramMarkdown, reportStructureMarkdown, reportDependenciesMarkdown, reportHotspotsMarkdown, reportFileMarkdown, reportOutdatedMarkdown, reportAuditMarkdown, reportUpgradeMarkdown, reportCoverageMarkdown, reportCouplingMarkdown, reportPrDiffMarkdown, reportWorkspacesMarkdown, } from '../reporters/markdownReporter.js';
44
- import { reportAnalysisSarif, reportHealthSarif, reportCiSarif, issuesToSarif, } from '../reporters/sarifReporter.js';
45
- // ── CLI Setup ─────────────────────────────────────────────
46
- const program = new Command();
47
- program
48
- .name('projscan')
49
- .description('Instant codebase insights - doctor, x-ray, and architecture map for any repository')
50
- .version(pkg.version)
51
- .option('--format <type>', 'output format: console, json, markdown, sarif', 'console')
52
- .option('--config <path>', 'path to .projscanrc config file')
53
- .option('--verbose', 'enable verbose output')
54
- .option('--quiet', 'suppress non-essential output');
55
- function getFormat() {
56
- const opts = program.opts();
57
- const f = opts.format;
58
- if (f === 'json' || f === 'markdown' || f === 'sarif')
59
- return f;
60
- return 'console';
61
- }
62
- function getRootPath() {
63
- return process.cwd();
64
- }
65
- async function loadProjectConfig() {
66
- const opts = program.opts();
67
- const explicit = typeof opts.config === 'string' ? opts.config : undefined;
68
- try {
69
- const { config, source } = await loadConfig(getRootPath(), explicit);
70
- if (source && !opts.quiet && getFormat() === 'console') {
71
- console.error(chalk.dim(` [config: ${path.relative(getRootPath(), source) || source}]`));
72
- }
73
- return config;
74
- }
75
- catch (err) {
76
- const msg = err instanceof Error ? err.message : String(err);
77
- console.error(chalk.red(` Config error: ${msg}`));
78
- process.exit(1);
79
- }
80
- }
81
- async function filterIssuesByChangedFiles(issues, rootPath, baseRef) {
82
- const result = await getChangedFiles(rootPath, baseRef);
83
- if (!result.available) {
84
- if (getFormat() === 'console' && !program.opts().quiet) {
85
- console.error(chalk.yellow(` [--changed-only: ${result.reason ?? 'unavailable'} - reporting all issues]`));
86
- }
87
- return issues;
88
- }
89
- if (getFormat() === 'console' && !program.opts().quiet) {
90
- console.error(chalk.dim(` [--changed-only: base=${result.baseRef}, ${result.files.length} file(s)]`));
91
- }
92
- const set = new Set(result.files);
93
- const filtered = issues.filter((issue) => {
94
- if (!issue.locations || issue.locations.length === 0)
95
- return false;
96
- return issue.locations.some((loc) => set.has(loc.file));
97
- });
98
- const dropped = issues.length - filtered.length;
99
- if (dropped > 0 && !program.opts().quiet) {
100
- const unlocated = issues.filter((i) => !i.locations || i.locations.length === 0).length;
101
- const message = unlocated > 0
102
- ? ` [--changed-only: ${dropped} issue(s) filtered out; ${unlocated} had no file location]`
103
- : ` [--changed-only: ${dropped} issue(s) outside the changed-file set]`;
104
- if (getFormat() === 'console') {
105
- console.error(chalk.dim(message));
106
- }
107
- else {
108
- // For non-console formats, still emit to stderr so the count is visible
109
- // without corrupting machine-readable stdout.
110
- console.error(message.trim());
111
- }
112
- }
113
- return filtered;
114
- }
115
- function setupLogLevel() {
116
- const opts = program.opts();
117
- if (opts.verbose)
118
- setLogLevel('debug');
119
- else if (opts.quiet)
120
- setLogLevel('quiet');
121
- }
122
- function maybeBanner() {
123
- const opts = program.opts();
124
- if (!opts.quiet && getFormat() === 'console') {
125
- try {
126
- showBanner();
127
- }
128
- catch (err) {
129
- console.error(chalk.dim(` [banner error: ${err instanceof Error ? err.message : String(err)}]`));
130
- }
131
- }
132
- }
133
- function maybeCompactBanner() {
134
- const opts = program.opts();
135
- if (!opts.quiet && getFormat() === 'console') {
136
- try {
137
- showCompactBanner();
138
- }
139
- catch (err) {
140
- console.error(chalk.dim(` [banner error: ${err instanceof Error ? err.message : String(err)}]`));
141
- }
142
- }
143
- }
144
- /** Walk a DirectoryNode to find the node whose `path` matches targetPath. */
145
- function sliceCliTree(node, targetPath) {
146
- if (node.path === targetPath)
147
- return node;
148
- for (const child of node.children) {
149
- const hit = sliceCliTree(child, targetPath);
150
- if (hit)
151
- return hit;
152
- }
153
- return null;
154
- }
155
- // ── Command: analyze (default) ────────────────────────────
156
- program
157
- .command('analyze', { isDefault: true })
158
- .description('Analyze repository and show project report')
159
- .option('--changed-only', 'only report issues on files changed vs base ref')
160
- .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
161
- .option('--package <name>', 'monorepo: scope issues to a single workspace package')
162
- .action(async (cmdOpts) => {
163
- setupLogLevel();
164
- maybeBanner();
165
- const rootPath = getRootPath();
166
- const format = getFormat();
167
- const config = await loadProjectConfig();
168
- const spinner = format === 'console' ? ora('Scanning repository...').start() : null;
169
- try {
170
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
171
- if (spinner)
172
- spinner.text = 'Detecting languages...';
173
- const languages = detectLanguages(scan.files);
174
- if (spinner)
175
- spinner.text = 'Detecting frameworks...';
176
- const frameworks = await detectFrameworks(rootPath, scan.files);
177
- if (spinner)
178
- spinner.text = 'Analyzing dependencies...';
179
- const dependencies = await analyzeDependencies(rootPath);
180
- if (spinner)
181
- spinner.text = 'Checking for issues...';
182
- let issues = await collectIssues(rootPath, scan.files);
183
- issues = applyConfigToIssues(issues, config);
184
- if (cmdOpts.changedOnly) {
185
- issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
186
- }
187
- if (cmdOpts.package) {
188
- const ws = await detectWorkspaces(rootPath);
189
- const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, scan.files.map((f) => f.relativePath)));
190
- issues = issues.filter((i) => (i.locations ?? []).some((l) => l.file && allowed.has(l.file)));
191
- }
192
- if (spinner)
193
- spinner.stop();
194
- const report = {
195
- projectName: path.basename(rootPath),
196
- rootPath,
197
- scan,
198
- languages,
199
- frameworks,
200
- dependencies,
201
- issues,
202
- timestamp: new Date().toISOString(),
203
- };
204
- switch (format) {
205
- case 'json':
206
- reportAnalysisJson(report);
207
- break;
208
- case 'markdown':
209
- reportAnalysisMarkdown(report);
210
- break;
211
- case 'sarif':
212
- reportAnalysisSarif(issues, pkg.version);
213
- break;
214
- default:
215
- reportAnalysis(report);
216
- }
217
- }
218
- catch (error) {
219
- if (spinner)
220
- spinner.fail('Analysis failed');
221
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
222
- process.exit(1);
223
- }
224
- });
225
- // ── Command: doctor ───────────────────────────────────────
226
- program
227
- .command('doctor')
228
- .description('Evaluate project health and detect issues')
229
- .option('--changed-only', 'only report issues on files changed vs base ref')
230
- .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
231
- .option('--package <name>', 'monorepo: scope issues to a single workspace package')
232
- .action(async (cmdOpts) => {
233
- setupLogLevel();
234
- maybeCompactBanner();
235
- const rootPath = getRootPath();
236
- const format = getFormat();
237
- const config = await loadProjectConfig();
238
- const spinner = format === 'console' ? ora('Running health checks...').start() : null;
239
- try {
240
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
241
- let issues = await collectIssues(rootPath, scan.files);
242
- issues = applyConfigToIssues(issues, config);
243
- if (cmdOpts.changedOnly) {
244
- issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
245
- }
246
- if (cmdOpts.package) {
247
- const ws = await detectWorkspaces(rootPath);
248
- const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, scan.files.map((f) => f.relativePath)));
249
- issues = issues.filter((i) => (i.locations ?? []).some((l) => l.file && allowed.has(l.file)));
250
- }
251
- if (spinner)
252
- spinner.stop();
253
- switch (format) {
254
- case 'json':
255
- reportHealthJson(issues);
256
- break;
257
- case 'markdown':
258
- reportHealthMarkdown(issues);
259
- break;
260
- case 'sarif':
261
- reportHealthSarif(issues, pkg.version);
262
- break;
263
- default:
264
- reportHealth(issues, scan.scanDurationMs);
265
- }
266
- }
267
- catch (error) {
268
- if (spinner)
269
- spinner.fail('Health check failed');
270
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
271
- process.exit(1);
272
- }
273
- });
274
- // ── Command: ci ──────────────────────────────────────────
275
- program
276
- .command('ci')
277
- .description('Run health check for CI pipelines (exits 1 if score below threshold)')
278
- .option('--min-score <score>', 'minimum passing score (0-100)')
279
- .option('--changed-only', 'gate only on issues in files changed vs base ref')
280
- .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
281
- .action(async (cmdOpts) => {
282
- setupLogLevel();
283
- maybeCompactBanner();
284
- const rootPath = getRootPath();
285
- const format = getFormat();
286
- const config = await loadProjectConfig();
287
- try {
288
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
289
- let issues = await collectIssues(rootPath, scan.files);
290
- issues = applyConfigToIssues(issues, config);
291
- if (cmdOpts.changedOnly) {
292
- issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
293
- }
294
- const rawThreshold = cmdOpts.minScore ?? config.minScore ?? 70;
295
- const threshold = Math.max(0, Math.min(100, typeof rawThreshold === 'string' ? parseInt(rawThreshold, 10) || 70 : rawThreshold));
296
- const { score } = calculateScore(issues);
297
- switch (format) {
298
- case 'json':
299
- reportCiJson(issues, threshold);
300
- break;
301
- case 'markdown':
302
- reportCiMarkdown(issues, threshold);
303
- break;
304
- case 'sarif':
305
- reportCiSarif(issues, pkg.version);
306
- break;
307
- default:
308
- reportCi(issues, threshold);
309
- }
310
- if (score < threshold) {
311
- process.exit(1);
312
- }
313
- }
314
- catch (error) {
315
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
316
- process.exit(1);
317
- }
318
- });
319
- // ── Command: diff ─────────────────────────────────────────
320
- program
321
- .command('diff')
322
- .description('Compare health against a saved baseline')
323
- .option('--save-baseline', 'save current health as the baseline')
324
- .option('--baseline <path>', 'path to baseline file (default: .projscan-baseline.json)')
325
- .action(async (cmdOpts) => {
326
- setupLogLevel();
327
- maybeCompactBanner();
328
- const rootPath = getRootPath();
329
- const format = getFormat();
330
- const config = await loadProjectConfig();
331
- try {
332
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
333
- let issues = await collectIssues(rootPath, scan.files);
334
- issues = applyConfigToIssues(issues, config);
335
- const hotspotReport = await analyzeHotspots(rootPath, scan.files, issues, { limit: 20 });
336
- if (cmdOpts.saveBaseline) {
337
- const filePath = await saveBaseline(rootPath, issues, hotspotReport);
338
- const { score, grade } = calculateScore(issues);
339
- console.log(chalk.green(`\n Baseline saved to ${filePath}`));
340
- console.log(` Score: ${chalk.bold(`${grade} (${score}/100)`)}`);
341
- console.log(` Issues: ${issues.length}`);
342
- if (hotspotReport.available) {
343
- console.log(` Hotspots snapshotted: ${hotspotReport.hotspots.length}\n`);
344
- }
345
- else {
346
- console.log('');
347
- }
348
- return;
349
- }
350
- let baseline;
351
- try {
352
- baseline = await loadBaseline(cmdOpts.baseline, rootPath);
353
- }
354
- catch {
355
- console.error(chalk.yellow('\n No baseline found.'));
356
- console.error(` Run ${chalk.bold.cyan('projscan diff --save-baseline')} first to create one.\n`);
357
- process.exit(1);
358
- }
359
- const diff = computeDiff(baseline, issues, hotspotReport);
360
- switch (format) {
361
- case 'json':
362
- reportDiffJson(diff);
363
- break;
364
- case 'markdown':
365
- reportDiffMarkdown(diff);
366
- break;
367
- default:
368
- reportDiff(diff);
369
- }
370
- }
371
- catch (error) {
372
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
373
- process.exit(1);
374
- }
375
- });
376
- // ── Command: fix ──────────────────────────────────────────
377
- program
378
- .command('fix')
379
- .description('Auto-fix detected project issues')
380
- .option('-y, --yes', 'apply fixes without prompting')
381
- .action(async (cmdOpts) => {
382
- setupLogLevel();
383
- maybeCompactBanner();
384
- const rootPath = getRootPath();
385
- const spinner = ora('Detecting issues...').start();
386
- const config = await loadProjectConfig();
387
- try {
388
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
389
- let issues = await collectIssues(rootPath, scan.files);
390
- issues = applyConfigToIssues(issues, config);
391
- const fixes = getAllAvailableFixes(issues);
392
- spinner.stop();
393
- if (fixes.length === 0) {
394
- console.log(`\n ${chalk.green('✓')} ${chalk.bold('No fixable issues found!')}\n`);
395
- return;
396
- }
397
- reportDetectedIssues(issues, fixes);
398
- // Prompt for confirmation
399
- if (!cmdOpts.yes) {
400
- const proceed = await promptYesNo(` Apply ${fixes.length} fix${fixes.length > 1 ? 'es' : ''}? (y/n) `);
401
- if (!proceed) {
402
- console.log(chalk.dim('\n Aborted.\n'));
403
- return;
404
- }
405
- }
406
- // Apply fixes
407
- const results = [];
408
- for (const fix of fixes) {
409
- const fixSpinner = ora(` Applying: ${fix.title}...`).start();
410
- try {
411
- await fix.apply(rootPath);
412
- fixSpinner.succeed(` ${fix.title}`);
413
- results.push({ fix, success: true });
414
- }
415
- catch (error) {
416
- const msg = error instanceof Error ? error.message : String(error);
417
- fixSpinner.fail(` ${fix.title}`);
418
- results.push({ fix, success: false, error: msg });
419
- }
420
- }
421
- const succeeded = results.filter((r) => r.success).length;
422
- const failed = results.filter((r) => !r.success).length;
423
- console.log('');
424
- if (succeeded > 0) {
425
- console.log(` ${chalk.green('✓')} ${succeeded} fix${succeeded > 1 ? 'es' : ''} applied successfully`);
426
- }
427
- if (failed > 0) {
428
- console.log(` ${chalk.red('✗')} ${failed} fix${failed > 1 ? 'es' : ''} failed`);
429
- }
430
- console.log('');
431
- }
432
- catch (error) {
433
- spinner.fail('Fix detection failed');
434
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
435
- process.exit(1);
436
- }
437
- });
438
- // ── Command: file ─────────────────────────────────────────
439
- program
440
- .command('file <file>')
441
- .description('Drill into a file - purpose, risk, ownership, related issues')
442
- .action(async (filePath) => {
443
- setupLogLevel();
444
- maybeCompactBanner();
445
- const rootPath = getRootPath();
446
- const format = getFormat();
447
- const spinner = format === 'console' ? ora('Inspecting file...').start() : null;
448
- try {
449
- const inspection = await inspectFile(rootPath, filePath);
450
- if (spinner)
451
- spinner.stop();
452
- if (!inspection.exists) {
453
- console.error(chalk.red(`\n ${inspection.reason ?? 'File unavailable'}: ${filePath}\n`));
454
- process.exit(1);
455
- }
456
- switch (format) {
457
- case 'json':
458
- reportFileJson(inspection);
459
- break;
460
- case 'markdown':
461
- reportFileMarkdown(inspection);
462
- break;
463
- default:
464
- reportFileInspection(inspection);
465
- }
466
- }
467
- catch (error) {
468
- if (spinner)
469
- spinner.fail('File inspection failed');
470
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
471
- process.exit(1);
472
- }
473
- });
474
- // ── Command: explain ──────────────────────────────────────
475
- program
476
- .command('explain <file>')
477
- .description('Explain a file - its purpose, dependencies, and exports')
478
- .action(async (filePath) => {
479
- setupLogLevel();
480
- maybeCompactBanner();
481
- const format = getFormat();
482
- const absolutePath = path.resolve(filePath);
483
- try {
484
- const content = await fs.readFile(absolutePath, 'utf-8');
485
- const explanation = analyzeFile(absolutePath, content);
486
- switch (format) {
487
- case 'json':
488
- reportExplanationJson(explanation);
489
- break;
490
- case 'markdown':
491
- reportExplanationMarkdown(explanation);
492
- break;
493
- default:
494
- reportExplanation(explanation);
495
- }
496
- }
497
- catch (error) {
498
- if (error.code === 'ENOENT') {
499
- console.error(chalk.red(`File not found: ${filePath}`));
500
- }
501
- else {
502
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
503
- }
504
- process.exit(1);
505
- }
506
- });
507
- // ── Command: diagram ──────────────────────────────────────
508
- program
509
- .command('diagram')
510
- .description('Generate architecture overview diagram')
511
- .action(async () => {
512
- setupLogLevel();
513
- maybeCompactBanner();
514
- const rootPath = getRootPath();
515
- const format = getFormat();
516
- const config = await loadProjectConfig();
517
- const spinner = format === 'console' ? ora('Analyzing architecture...').start() : null;
518
- try {
519
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
520
- const frameworks = await detectFrameworks(rootPath, scan.files);
521
- const layers = buildArchitectureLayers(scan.files, frameworks.frameworks.map((f) => f.name));
522
- if (spinner)
523
- spinner.stop();
524
- switch (format) {
525
- case 'json':
526
- reportDiagramJson(layers);
527
- break;
528
- case 'markdown':
529
- reportDiagramMarkdown(layers);
530
- break;
531
- default:
532
- reportDiagram(layers);
533
- }
534
- }
535
- catch (error) {
536
- if (spinner)
537
- spinner.fail('Diagram generation failed');
538
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
539
- process.exit(1);
540
- }
541
- });
542
- // ── Command: structure ────────────────────────────────────
543
- program
544
- .command('structure')
545
- .description('Show project directory structure')
546
- .option('--package <name>', 'monorepo: scope tree to a single workspace package')
547
- .action(async (cmdOpts) => {
548
- setupLogLevel();
549
- maybeCompactBanner();
550
- const rootPath = getRootPath();
551
- const format = getFormat();
552
- const config = await loadProjectConfig();
553
- const spinner = format === 'console' ? ora('Scanning...').start() : null;
554
- try {
555
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
556
- let tree = scan.directoryTree;
557
- let title = path.basename(rootPath);
558
- if (cmdOpts.package) {
559
- const ws = await detectWorkspaces(rootPath);
560
- const pkg = ws.packages.find((p) => p.name === cmdOpts.package);
561
- if (pkg && !pkg.isRoot && pkg.relativePath) {
562
- const sliced = sliceCliTree(tree, pkg.relativePath);
563
- if (sliced) {
564
- tree = sliced;
565
- title = pkg.name;
566
- }
567
- }
568
- }
569
- if (spinner)
570
- spinner.stop();
571
- switch (format) {
572
- case 'json':
573
- reportStructureJson(tree);
574
- break;
575
- case 'markdown':
576
- reportStructureMarkdown(tree);
577
- break;
578
- default:
579
- reportStructure(tree, title);
580
- }
581
- }
582
- catch (error) {
583
- if (spinner)
584
- spinner.fail('Structure scan failed');
585
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
586
- process.exit(1);
587
- }
588
- });
589
- // ── Command: dependencies ─────────────────────────────────
590
- program
591
- .command('dependencies')
592
- .description('Analyze project dependencies')
593
- .action(async () => {
594
- setupLogLevel();
595
- maybeCompactBanner();
596
- const rootPath = getRootPath();
597
- const format = getFormat();
598
- const spinner = format === 'console' ? ora('Analyzing dependencies...').start() : null;
599
- try {
600
- const report = await analyzeDependencies(rootPath);
601
- if (spinner)
602
- spinner.stop();
603
- if (!report) {
604
- console.log(chalk.yellow('\n No package.json found in this directory.\n'));
605
- return;
606
- }
607
- switch (format) {
608
- case 'json':
609
- reportDependenciesJson(report);
610
- break;
611
- case 'markdown':
612
- reportDependenciesMarkdown(report);
613
- break;
614
- default:
615
- reportDependencies(report);
616
- }
617
- }
618
- catch (error) {
619
- if (spinner)
620
- spinner.fail('Dependency analysis failed');
621
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
622
- process.exit(1);
623
- }
624
- });
625
- // ── Command: hotspots ─────────────────────────────────────
626
- program
627
- .command('hotspots')
628
- .description('Rank files by risk (git churn × AST cyclomatic complexity × open issues)')
629
- .option('--limit <n>', 'number of hotspots to show')
630
- .option('--since <when>', 'git history window (e.g. "6 months ago", "2024-01-01")')
631
- .option('--package <name>', 'monorepo: scope to a single workspace package')
632
- .action(async (cmdOpts) => {
633
- setupLogLevel();
634
- maybeCompactBanner();
635
- const rootPath = getRootPath();
636
- const format = getFormat();
637
- const config = await loadProjectConfig();
638
- const spinner = format === 'console' ? ora('Analyzing hotspots...').start() : null;
639
- try {
640
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
641
- let issues = await collectIssues(rootPath, scan.files);
642
- issues = applyConfigToIssues(issues, config);
643
- const limitRaw = cmdOpts.limit ?? config.hotspots?.limit ?? 10;
644
- const limit = Math.max(1, Math.min(100, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 10 : limitRaw));
645
- const since = cmdOpts.since ?? config.hotspots?.since ?? '12 months ago';
646
- const coverageReport = await parseCoverage(rootPath);
647
- // Build the code graph so the risk score uses AST cyclomatic complexity
648
- // instead of LOC. Cache hit makes this nearly free on repeat runs.
649
- const cached = await loadCachedGraph(rootPath);
650
- const graph = await buildCodeGraph(rootPath, scan.files, cached);
651
- await saveCachedGraph(rootPath, graph);
652
- const report = await analyzeHotspots(rootPath, scan.files, issues, {
653
- since,
654
- limit,
655
- coverage: coverageReport.available ? coverageMap(coverageReport) : undefined,
656
- graph,
657
- });
658
- if (cmdOpts.package) {
659
- const ws = await detectWorkspaces(rootPath);
660
- const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, report.hotspots.map((h) => h.relativePath)));
661
- report.hotspots = report.hotspots.filter((h) => allowed.has(h.relativePath));
662
- }
663
- if (spinner)
664
- spinner.stop();
665
- switch (format) {
666
- case 'json':
667
- reportHotspotsJson(report);
668
- break;
669
- case 'markdown':
670
- reportHotspotsMarkdown(report);
671
- break;
672
- default:
673
- reportHotspots(report);
674
- }
675
- }
676
- catch (error) {
677
- if (spinner)
678
- spinner.fail('Hotspot analysis failed');
679
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
680
- process.exit(1);
681
- }
682
- });
683
- // ── Command: coupling ─────────────────────────────────────
684
- program
685
- .command('coupling')
686
- .description('Per-file fan-in / fan-out / instability and circular-import cycles (AST-derived)')
687
- .option('--limit <n>', 'number of files to show (default 25)')
688
- .option('--cycles-only', 'only show files participating in import cycles')
689
- .option('--high-fan-in', 'sort by fan-in (most-depended-on first)')
690
- .option('--high-fan-out', 'sort by fan-out (most-coupled first)')
691
- .option('--file <path>', 'restrict output to a single file')
692
- .option('--package <name>', 'monorepo: scope to a single workspace package')
693
- .action(async (cmdOpts) => {
694
- setupLogLevel();
695
- maybeCompactBanner();
696
- const rootPath = getRootPath();
697
- const format = getFormat();
698
- const config = await loadProjectConfig();
699
- const spinner = format === 'console' ? ora('Computing coupling + cycles...').start() : null;
700
- try {
701
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
702
- const cached = await loadCachedGraph(rootPath);
703
- const graph = await buildCodeGraph(rootPath, scan.files, cached);
704
- await saveCachedGraph(rootPath, graph);
705
- const ws = await detectWorkspaces(rootPath);
706
- const report = computeCoupling(graph, ws);
707
- const direction = cmdOpts.cyclesOnly
708
- ? 'cycles_only'
709
- : cmdOpts.highFanIn
710
- ? 'high_fan_in'
711
- : cmdOpts.highFanOut
712
- ? 'high_fan_out'
713
- : 'all';
714
- const limitRaw = cmdOpts.limit ?? 25;
715
- const limit = Math.max(1, Math.min(500, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 25 : limitRaw));
716
- let files = filterCoupling(report, direction);
717
- if (cmdOpts.file)
718
- files = files.filter((f) => f.relativePath === cmdOpts.file);
719
- if (cmdOpts.package) {
720
- const ws = await detectWorkspaces(rootPath);
721
- const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, files.map((f) => f.relativePath)));
722
- files = files.filter((f) => allowed.has(f.relativePath));
723
- }
724
- files = files.slice(0, limit);
725
- const filtered = {
726
- files,
727
- cycles: report.cycles,
728
- crossPackageEdges: report.crossPackageEdges,
729
- totalFiles: report.totalFiles,
730
- totalCycles: report.totalCycles,
731
- totalCrossPackageEdges: report.totalCrossPackageEdges,
732
- };
733
- if (spinner)
734
- spinner.stop();
735
- switch (format) {
736
- case 'json':
737
- reportCouplingJson(filtered);
738
- break;
739
- case 'markdown':
740
- reportCouplingMarkdown(filtered);
741
- break;
742
- default:
743
- reportCoupling(filtered);
744
- }
745
- }
746
- catch (error) {
747
- if (spinner)
748
- spinner.fail('Coupling analysis failed');
749
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
750
- process.exit(1);
751
- }
752
- });
753
- // ── Command: pr-diff ──────────────────────────────────────
754
- program
755
- .command('pr-diff')
756
- .description('Structural (AST) diff between two refs - what changed in exports, imports, calls, CC, fan-in')
757
- .option('--base <ref>', 'base ref (default: origin/main, falling back to main/master/HEAD~1)')
758
- .option('--head <ref>', 'head ref (default: HEAD)')
759
- .option('--package <name>', 'monorepo: scope diff to a single workspace package')
760
- .action(async (cmdOpts) => {
761
- setupLogLevel();
762
- maybeCompactBanner();
763
- const rootPath = getRootPath();
764
- const format = getFormat();
765
- const spinner = format === 'console' ? ora('Computing structural PR diff...').start() : null;
766
- try {
767
- const report = await computePrDiff(rootPath, { base: cmdOpts.base, head: cmdOpts.head });
768
- if (cmdOpts.package) {
769
- const ws = await detectWorkspaces(rootPath);
770
- const collected = [
771
- ...report.filesAdded,
772
- ...report.filesRemoved,
773
- ...report.filesModified.map((f) => f.relativePath),
774
- ];
775
- const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, collected));
776
- report.filesAdded = report.filesAdded.filter((f) => allowed.has(f));
777
- report.filesRemoved = report.filesRemoved.filter((f) => allowed.has(f));
778
- report.filesModified = report.filesModified.filter((f) => allowed.has(f.relativePath));
779
- report.totalFilesChanged =
780
- report.filesAdded.length + report.filesRemoved.length + report.filesModified.length;
781
- }
782
- if (spinner)
783
- spinner.stop();
784
- switch (format) {
785
- case 'json':
786
- reportPrDiffJson(report);
787
- break;
788
- case 'markdown':
789
- reportPrDiffMarkdown(report);
790
- break;
791
- default:
792
- reportPrDiff(report);
793
- }
794
- }
795
- catch (error) {
796
- if (spinner)
797
- spinner.fail('PR diff failed');
798
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
799
- process.exit(1);
800
- }
801
- });
802
- // ── Command: workspaces ───────────────────────────────────
803
- program
804
- .command('workspaces')
805
- .description('List monorepo workspace packages (npm/yarn workspaces, pnpm-workspace.yaml, Nx/Turbo/Lerna fallback)')
806
- .action(async () => {
807
- setupLogLevel();
808
- maybeCompactBanner();
809
- const rootPath = getRootPath();
810
- const format = getFormat();
811
- try {
812
- const info = await detectWorkspaces(rootPath);
813
- switch (format) {
814
- case 'json':
815
- reportWorkspacesJson(info);
816
- break;
817
- case 'markdown':
818
- reportWorkspacesMarkdown(info);
819
- break;
820
- default:
821
- reportWorkspaces(info);
822
- }
823
- }
824
- catch (error) {
825
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
826
- process.exit(1);
827
- }
828
- });
829
- // ── Command: telemetry ────────────────────────────────────
830
- program
831
- .command('telemetry')
832
- .description('Inspect projscan opt-in telemetry: config state, or per-tool histograms with --aggregate')
833
- .option('--aggregate', 'read the local sink and print per-tool latency histograms (count, p50/p95/p99, error rate)')
834
- .action(async (cmdOpts) => {
835
- setupLogLevel();
836
- maybeCompactBanner();
837
- const rootPath = getRootPath();
838
- const format = getFormat();
839
- try {
840
- const { config } = await loadConfig(rootPath);
841
- const out = cmdOpts.aggregate
842
- ? await aggregateTelemetry(config.telemetry)
843
- : describeTelemetryConfig(config.telemetry);
844
- if (format === 'json') {
845
- console.log(JSON.stringify(out, null, 2));
846
- return;
847
- }
848
- // Console: hand-formatted summary so users don't have to read raw JSON.
849
- if (cmdOpts.aggregate) {
850
- const agg = out;
851
- if (!agg.available) {
852
- console.log(chalk.yellow(`\n ${agg.reason ?? 'No telemetry available.'}\n`));
853
- return;
854
- }
855
- console.log(chalk.bold('\n Telemetry histograms'));
856
- console.log(chalk.dim(` sink: ${agg.sink}`));
857
- console.log(chalk.dim(` ${agg.totalEvents} event(s) · ${agg.windowFrom ?? '?'} → ${agg.windowTo ?? '?'}\n`));
858
- if (agg.byTool.length === 0) {
859
- console.log(chalk.dim(' (no events recorded yet)\n'));
860
- return;
861
- }
862
- const colHead = ` ${'count'.padStart(6)} ${'err%'.padStart(5)} ${'p50'.padStart(6)} ${'p95'.padStart(6)} ${'p99'.padStart(6)} tool`;
863
- console.log(chalk.dim(colHead));
864
- for (const t of agg.byTool) {
865
- const errPct = (t.errorRate * 100).toFixed(1) + '%';
866
- console.log(` ${String(t.count).padStart(6)} ${errPct.padStart(5)} ${(t.p50Ms ?? 0).toString().padStart(6)} ${(t.p95Ms ?? 0).toString().padStart(6)} ${(t.p99Ms ?? 0).toString().padStart(6)} ${chalk.cyan(t.tool)}`);
867
- }
868
- console.log('');
869
- }
870
- else {
871
- const cfg = out;
872
- console.log(chalk.bold('\n Telemetry'));
873
- console.log(` enabled: ${cfg.enabled ? chalk.green('yes') : chalk.dim('no (default)')}`);
874
- console.log(` sink: ${cfg.sink}`);
875
- console.log(` default: ${cfg.defaultSink}`);
876
- console.log(` PROJSCAN_TELEMETRY env: ${cfg.envOverride ?? chalk.dim('(unset)')}`);
877
- console.log(chalk.dim('\n Records: tool, durationMs, ok, version, ts. Never source/paths/args.'));
878
- console.log(chalk.dim(' Re-run with --aggregate to see histograms over the recorded events.\n'));
879
- }
880
- }
881
- catch (error) {
882
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
883
- process.exit(1);
884
- }
885
- });
886
- // ── Command: outdated ─────────────────────────────────────
887
- program
888
- .command('outdated')
889
- .description('Detect outdated dependencies (offline - compares declared vs installed)')
890
- .action(async () => {
891
- setupLogLevel();
892
- maybeCompactBanner();
893
- const rootPath = getRootPath();
894
- const format = getFormat();
895
- const spinner = format === 'console' ? ora('Checking dependencies...').start() : null;
896
- try {
897
- const report = await detectOutdated(rootPath);
898
- if (spinner)
899
- spinner.stop();
900
- switch (format) {
901
- case 'json':
902
- reportOutdatedJson(report);
903
- break;
904
- case 'markdown':
905
- reportOutdatedMarkdown(report);
906
- break;
907
- case 'sarif':
908
- console.log(JSON.stringify(issuesToSarif([], pkg.version), null, 2));
909
- break;
910
- default:
911
- reportOutdated(report);
912
- }
913
- }
914
- catch (error) {
915
- if (spinner)
916
- spinner.fail('Outdated check failed');
917
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
918
- process.exit(1);
919
- }
920
- });
921
- // ── Command: audit ────────────────────────────────────────
922
- program
923
- .command('audit')
924
- .description('Run npm audit and surface vulnerabilities (SARIF supported)')
925
- .option('--timeout <ms>', 'override npm audit timeout (default 60000)')
926
- .action(async (cmdOpts) => {
927
- setupLogLevel();
928
- maybeCompactBanner();
929
- const rootPath = getRootPath();
930
- const format = getFormat();
931
- const spinner = format === 'console' ? ora('Running npm audit...').start() : null;
932
- try {
933
- const timeoutMs = cmdOpts.timeout ? Math.max(5_000, parseInt(cmdOpts.timeout, 10)) : undefined;
934
- const report = await runAudit(rootPath, timeoutMs !== undefined ? { timeoutMs } : {});
935
- if (spinner)
936
- spinner.stop();
937
- switch (format) {
938
- case 'json':
939
- reportAuditJson(report);
940
- break;
941
- case 'markdown':
942
- reportAuditMarkdown(report);
943
- break;
944
- case 'sarif':
945
- console.log(JSON.stringify(issuesToSarif(auditFindingsToIssues(report), pkg.version), null, 2));
946
- break;
947
- default:
948
- reportAudit(report);
949
- }
950
- }
951
- catch (error) {
952
- if (spinner)
953
- spinner.fail('Audit failed');
954
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
955
- process.exit(1);
956
- }
957
- });
958
- // ── Command: upgrade ──────────────────────────────────────
959
- program
960
- .command('upgrade <package>')
961
- .description('Preview the impact of upgrading a package (offline - reads local CHANGELOG + importers)')
962
- .action(async (pkgName) => {
963
- setupLogLevel();
964
- maybeCompactBanner();
965
- const rootPath = getRootPath();
966
- const format = getFormat();
967
- const config = await loadProjectConfig();
968
- const spinner = format === 'console' ? ora(`Previewing ${pkgName}...`).start() : null;
969
- try {
970
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
971
- const preview = await previewUpgrade(rootPath, pkgName, scan.files);
972
- if (spinner)
973
- spinner.stop();
974
- switch (format) {
975
- case 'json':
976
- reportUpgradeJson(preview);
977
- break;
978
- case 'markdown':
979
- reportUpgradeMarkdown(preview);
980
- break;
981
- default:
982
- reportUpgrade(preview);
983
- }
984
- }
985
- catch (error) {
986
- if (spinner)
987
- spinner.fail('Upgrade preview failed');
988
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
989
- process.exit(1);
990
- }
991
- });
992
- // ── Command: search ───────────────────────────────────────
993
- program
994
- .command('search <query...>')
995
- .description('Ranked search - BM25 by default, semantic or hybrid when @xenova/transformers peer is installed')
996
- .option('--scope <scope>', 'auto | content | symbols | files', 'auto')
997
- .option('--mode <mode>', 'lexical | semantic | hybrid (content/auto scope only)', 'lexical')
998
- .option('--semantic', 'shortcut for --mode semantic')
999
- .option('--limit <n>', 'max results', '15')
1000
- .option('--package <name>', 'monorepo: scope to a single workspace package')
1001
- .action(async (queryParts, cmdOpts) => {
1002
- setupLogLevel();
1003
- maybeCompactBanner();
1004
- const rootPath = getRootPath();
1005
- const format = getFormat();
1006
- const config = await loadProjectConfig();
1007
- const query = queryParts.join(' ').trim();
1008
- if (!query) {
1009
- console.error(chalk.red('\n search requires a non-empty query\n'));
1010
- process.exit(1);
1011
- }
1012
- const limitRaw = cmdOpts.limit ?? 15;
1013
- const limit = Math.max(1, Math.min(200, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 15 : limitRaw));
1014
- const scope = String(cmdOpts.scope ?? 'auto');
1015
- const spinner = format === 'console' ? ora('Indexing repository...').start() : null;
1016
- try {
1017
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
1018
- const cached = await loadCachedGraph(rootPath);
1019
- const graph = await buildCodeGraph(rootPath, scan.files, cached);
1020
- await saveCachedGraph(rootPath, graph);
1021
- // Build a (file -> bool) filter once if --package is set; reused below.
1022
- let passes = null;
1023
- if (cmdOpts.package) {
1024
- const ws = await detectWorkspaces(rootPath);
1025
- const pkg = ws.packages.find((p) => p.name === cmdOpts.package);
1026
- if (!pkg) {
1027
- passes = () => false;
1028
- }
1029
- else if (pkg.isRoot) {
1030
- passes = () => true;
1031
- }
1032
- else {
1033
- const prefix = pkg.relativePath + '/';
1034
- passes = (f) => f === pkg.relativePath || f.startsWith(prefix);
1035
- }
1036
- }
1037
- if (spinner)
1038
- spinner.text = 'Searching...';
1039
- let results;
1040
- if (scope === 'symbols') {
1041
- const q = query.toLowerCase();
1042
- const matches = [];
1043
- for (const [file, entry] of graph.files) {
1044
- if (passes && !passes(file))
1045
- continue;
1046
- for (const exp of entry.exports) {
1047
- if (exp.name.toLowerCase().includes(q)) {
1048
- matches.push({ symbol: exp.name, kind: exp.kind, file, line: exp.line });
1049
- }
1050
- }
1051
- }
1052
- matches.sort((a, b) => {
1053
- const aExact = a.symbol.toLowerCase() === q ? 0 : a.symbol.toLowerCase().startsWith(q) ? 1 : 2;
1054
- const bExact = b.symbol.toLowerCase() === q ? 0 : b.symbol.toLowerCase().startsWith(q) ? 1 : 2;
1055
- return aExact - bExact;
1056
- });
1057
- results = { scope, query, matches: matches.slice(0, limit), total: matches.length };
1058
- }
1059
- else if (scope === 'files') {
1060
- const q = query.toLowerCase();
1061
- const matches = scan.files
1062
- .filter((f) => f.relativePath.toLowerCase().includes(q))
1063
- .filter((f) => !passes || passes(f.relativePath))
1064
- .slice(0, limit)
1065
- .map((f) => ({ file: f.relativePath, sizeBytes: f.sizeBytes }));
1066
- results = { scope, query, matches, total: matches.length };
1067
- }
1068
- else {
1069
- const mode = cmdOpts.semantic ? 'semantic' : String(cmdOpts.mode ?? 'lexical');
1070
- const index = await buildSearchIndex(rootPath, scan.files, graph);
1071
- const lexicalHitsAll = searchIndex(index, query, { limit });
1072
- const lexicalHits = passes ? lexicalHitsAll.filter((h) => passes(h.file)) : lexicalHitsAll;
1073
- const tokens = expandQuery(query);
1074
- if (mode === 'lexical') {
1075
- const withExcerpts = await attachExcerpts(rootPath, lexicalHits, tokens);
1076
- results = {
1077
- scope: scope === 'auto' ? 'content' : scope,
1078
- mode: 'lexical',
1079
- query,
1080
- queryTokens: tokens,
1081
- matches: withExcerpts,
1082
- total: withExcerpts.length,
1083
- };
1084
- }
1085
- else {
1086
- const available = await isSemanticAvailable();
1087
- if (!available) {
1088
- if (spinner)
1089
- spinner.stop();
1090
- console.error(chalk.red(`\n Semantic search requires the optional peer @xenova/transformers.\n Install it with: ${chalk.bold('npm install @xenova/transformers')}\n`));
1091
- process.exit(1);
1092
- }
1093
- if (spinner)
1094
- spinner.text = 'Building semantic index (first run may take ~10s + model download)...';
1095
- const semIndex = await buildSemanticIndex(rootPath, scan.files, {
1096
- onFirstLoad: (m) => spinner?.text && (spinner.text = m),
1097
- onProgress: (d, t) => {
1098
- if (spinner)
1099
- spinner.text = `Embedding files... ${d}/${t}`;
1100
- },
1101
- });
1102
- if (!semIndex) {
1103
- if (spinner)
1104
- spinner.fail('Semantic index build failed');
1105
- process.exit(1);
1106
- }
1107
- if (spinner)
1108
- spinner.text = 'Searching...';
1109
- const semHitsAll = await semanticSearch(semIndex, query, { limit });
1110
- const semHits = passes ? semHitsAll.filter((h) => passes(h.file)) : semHitsAll;
1111
- if (mode === 'semantic') {
1112
- const enriched = await attachExcerpts(rootPath, semHits.map((h) => ({
1113
- file: h.file,
1114
- score: h.score,
1115
- matched: [],
1116
- symbolMatch: false,
1117
- pathMatch: false,
1118
- excerpt: '',
1119
- line: 0,
1120
- })), tokens);
1121
- results = {
1122
- scope: scope === 'auto' ? 'content' : scope,
1123
- mode: 'semantic',
1124
- query,
1125
- model: semIndex.model,
1126
- matches: enriched,
1127
- total: enriched.length,
1128
- };
1129
- }
1130
- else {
1131
- // hybrid
1132
- const fused = reciprocalRankFusion([lexicalHits, semHits]).slice(0, limit);
1133
- const enriched = await attachExcerpts(rootPath, fused.map((f) => ({
1134
- file: f.file,
1135
- score: f.score,
1136
- matched: [],
1137
- symbolMatch: false,
1138
- pathMatch: false,
1139
- excerpt: '',
1140
- line: 0,
1141
- })), tokens);
1142
- results = {
1143
- scope: scope === 'auto' ? 'content' : scope,
1144
- mode: 'hybrid',
1145
- query,
1146
- queryTokens: tokens,
1147
- model: semIndex.model,
1148
- matches: enriched,
1149
- total: enriched.length,
1150
- };
1151
- }
1152
- }
1153
- }
1154
- if (spinner)
1155
- spinner.stop();
1156
- if (format === 'json') {
1157
- console.log(JSON.stringify({ search: results }, null, 2));
1158
- return;
1159
- }
1160
- if (format === 'markdown') {
1161
- const r = results;
1162
- console.log(`# Search - \`${r.query}\` (${r.scope})\n`);
1163
- if (r.matches.length === 0) {
1164
- console.log('_No matches._');
1165
- return;
1166
- }
1167
- for (const m of r.matches) {
1168
- if ('symbol' in m)
1169
- console.log(`- \`${m.symbol}\` (${m.kind}) → \`${m.file}:${m.line}\``);
1170
- else if ('score' in m)
1171
- console.log(`- \`${m.file}:${m.line}\` - score ${m.score} - ${m.excerpt ?? ''}`);
1172
- else
1173
- console.log(`- \`${m.file}\``);
1174
- }
1175
- return;
1176
- }
1177
- // Console
1178
- const r = results;
1179
- console.log(`\n ${chalk.bold(`Search - "${query}"`)} ${chalk.dim(`[${r.scope}]`)}`);
1180
- if (r.queryTokens)
1181
- console.log(chalk.dim(` tokens: ${r.queryTokens.join(', ')}`));
1182
- console.log(chalk.dim(' ─'.repeat(20)));
1183
- if (r.matches.length === 0) {
1184
- console.log(chalk.yellow('\n No matches.\n'));
1185
- return;
1186
- }
1187
- for (const m of r.matches) {
1188
- if ('symbol' in m) {
1189
- console.log(` ${chalk.bold(String(m.symbol))} ${chalk.dim(`(${m.kind})`)} → ${chalk.dim(`${m.file}:${m.line}`)}`);
1190
- }
1191
- else if ('score' in m) {
1192
- const score = typeof m.score === 'number' ? m.score.toFixed(1) : String(m.score);
1193
- console.log(` ${chalk.bold(score.padStart(5))} ${chalk.cyan(String(m.file))}${m.line ? chalk.dim(`:${m.line}`) : ''}`);
1194
- if (m.excerpt)
1195
- console.log(` ${chalk.dim(String(m.excerpt))}`);
1196
- }
1197
- else {
1198
- console.log(` ${chalk.cyan(String(m.file))}`);
1199
- }
1200
- }
1201
- console.log('');
1202
- }
1203
- catch (error) {
1204
- if (spinner)
1205
- spinner.fail('Search failed');
1206
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
1207
- process.exit(1);
1208
- }
1209
- });
1210
- // ── Command: coverage ─────────────────────────────────────
1211
- program
1212
- .command('coverage')
1213
- .description('Join test coverage with hotspots - surface the scariest untested files')
1214
- .option('--limit <n>', 'limit number of entries shown', '30')
1215
- .option('--package <name>', 'monorepo: scope to a single workspace package')
1216
- .action(async (cmdOpts) => {
1217
- setupLogLevel();
1218
- maybeCompactBanner();
1219
- const rootPath = getRootPath();
1220
- const format = getFormat();
1221
- const config = await loadProjectConfig();
1222
- const spinner = format === 'console' ? ora('Parsing coverage...').start() : null;
1223
- try {
1224
- const coverage = await parseCoverage(rootPath);
1225
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
1226
- const issues = await collectIssues(rootPath, scan.files);
1227
- const limitRaw = cmdOpts.limit ?? 30;
1228
- const limit = Math.max(1, Math.min(200, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 30 : limitRaw));
1229
- const hotspots = await analyzeHotspots(rootPath, scan.files, issues, {
1230
- limit,
1231
- coverage: coverage.available ? coverageMap(coverage) : undefined,
1232
- });
1233
- const joined = joinCoverageWithHotspots(hotspots, coverage);
1234
- if (cmdOpts.package && joined.available) {
1235
- const ws = await detectWorkspaces(rootPath);
1236
- const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, joined.entries.map((e) => e.relativePath)));
1237
- joined.entries = joined.entries.filter((e) => allowed.has(e.relativePath));
1238
- }
1239
- if (spinner)
1240
- spinner.stop();
1241
- switch (format) {
1242
- case 'json':
1243
- reportCoverageJson(joined);
1244
- break;
1245
- case 'markdown':
1246
- reportCoverageMarkdown(joined);
1247
- break;
1248
- default:
1249
- reportCoverage(joined);
1250
- }
1251
- }
1252
- catch (error) {
1253
- if (spinner)
1254
- spinner.fail('Coverage analysis failed');
1255
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
1256
- process.exit(1);
1257
- }
1258
- });
1259
- // ── Command: mcp ──────────────────────────────────────────
1260
- program
1261
- .command('mcp')
1262
- .description('Run projscan as an MCP server (stdio) for AI coding agents')
1263
- .action(async () => {
1264
- setLogLevel('quiet');
1265
- const rootPath = getRootPath();
1266
- try {
1267
- await runMcpServer(rootPath);
1268
- }
1269
- catch (error) {
1270
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
1271
- process.exit(1);
1272
- }
1273
- });
1274
- // ── Command: badge ────────────────────────────────────────
1275
- program
1276
- .command('badge')
1277
- .description('Generate a health badge for your README')
1278
- .option('--markdown', 'output as markdown image link')
1279
- .action(async (cmdOpts) => {
1280
- setupLogLevel();
1281
- maybeCompactBanner();
1282
- const rootPath = getRootPath();
1283
- const spinner = ora('Calculating health score...').start();
1284
- const config = await loadProjectConfig();
1285
- try {
1286
- const scan = await scanRepository(rootPath, { ignore: config.ignore });
1287
- let issues = await collectIssues(rootPath, scan.files);
1288
- issues = applyConfigToIssues(issues, config);
1289
- const { score, grade } = calculateScore(issues);
1290
- spinner.stop();
1291
- const gradeColor = grade === 'A' || grade === 'B' ? chalk.green : grade === 'C' ? chalk.yellow : chalk.red;
1292
- console.log(`\n Health Score: ${gradeColor(chalk.bold(`${grade} (${score}/100)`))}\n`);
1293
- if (cmdOpts.markdown) {
1294
- console.log(` ${badgeMarkdown(grade)}\n`);
1295
- }
1296
- else {
1297
- console.log(` ${chalk.bold('Badge URL:')}`);
1298
- console.log(` ${badgeUrl(grade)}\n`);
1299
- console.log(` ${chalk.bold('Markdown:')}`);
1300
- console.log(` ${badgeMarkdown(grade)}\n`);
1301
- }
1302
- console.log(chalk.dim(' Add this to your README to show your project health score.\n'));
1303
- }
1304
- catch (error) {
1305
- spinner.fail('Badge generation failed');
1306
- console.error(chalk.red(error instanceof Error ? error.message : String(error)));
1307
- process.exit(1);
1308
- }
1309
- });
1310
- // ── File Analysis (for explain command) ───────────────────
1311
- function analyzeFile(filePath, content) {
1312
- const lines = content.split('\n');
1313
- const imports = extractImports(content);
1314
- const exports = extractExports(content);
1315
- const purpose = inferPurpose(filePath, exports);
1316
- const potentialIssues = detectFileIssues(content, lines.length);
1317
- return {
1318
- filePath: path.relative(process.cwd(), filePath),
1319
- purpose,
1320
- imports,
1321
- exports,
1322
- potentialIssues,
1323
- lineCount: lines.length,
1324
- };
1325
- }
1326
- // ── Architecture Layer Detection ──────────────────────────
1327
- function buildArchitectureLayers(files, frameworkNames) {
1328
- const layers = [];
1329
- const dirs = new Set(files.map((f) => f.directory.split(path.sep)[0]).filter(Boolean));
1330
- // Frontend layer
1331
- const frontendDirs = ['pages', 'components', 'views', 'layouts', 'public', 'app', 'styles'];
1332
- const frontendMatches = frontendDirs.filter((d) => dirs.has(d) || dirs.has(`src/${d}`));
1333
- const frontendFrameworks = frameworkNames.filter((f) => ['React', 'Next.js', 'Vue.js', 'Nuxt.js', 'Svelte', 'SvelteKit', 'Angular', 'Solid.js'].includes(f));
1334
- if (frontendMatches.length > 0 || frontendFrameworks.length > 0) {
1335
- layers.push({
1336
- name: 'Frontend',
1337
- technologies: frontendFrameworks.length > 0 ? frontendFrameworks : ['Static'],
1338
- directories: frontendMatches,
1339
- });
1340
- }
1341
- // API layer
1342
- const apiDirs = ['api', 'routes', 'controllers', 'endpoints'];
1343
- const apiMatches = apiDirs.filter((d) => dirs.has(d) || dirs.has(`src/${d}`));
1344
- const apiFrameworks = frameworkNames.filter((f) => ['Express', 'Fastify', 'NestJS', 'Hono', 'Koa', 'Apollo Server', 'tRPC'].includes(f));
1345
- if (apiMatches.length > 0 || apiFrameworks.length > 0) {
1346
- layers.push({
1347
- name: 'API Layer',
1348
- technologies: apiFrameworks.length > 0 ? apiFrameworks : ['HTTP'],
1349
- directories: apiMatches,
1350
- });
1351
- }
1352
- // Services layer
1353
- const serviceDirs = ['services', 'lib', 'core', 'domain', 'modules'];
1354
- const serviceMatches = serviceDirs.filter((d) => dirs.has(d) || dirs.has(`src/${d}`));
1355
- if (serviceMatches.length > 0) {
1356
- layers.push({
1357
- name: 'Services',
1358
- technologies: inferServiceTech(files, serviceMatches),
1359
- directories: serviceMatches,
1360
- });
1361
- }
1362
- // Database layer
1363
- const dbDirs = ['db', 'database', 'prisma', 'migrations', 'models', 'entities'];
1364
- const dbMatches = dbDirs.filter((d) => dirs.has(d) || dirs.has(`src/${d}`));
1365
- const dbFrameworks = frameworkNames.filter((f) => ['Prisma', 'Drizzle ORM', 'Mongoose', 'TypeORM', 'Sequelize'].includes(f));
1366
- if (dbMatches.length > 0 || dbFrameworks.length > 0) {
1367
- layers.push({
1368
- name: 'Database',
1369
- technologies: dbFrameworks.length > 0 ? dbFrameworks : ['Database'],
1370
- directories: dbMatches,
1371
- });
1372
- }
1373
- // If no layers detected, show a generic one
1374
- if (layers.length === 0) {
1375
- const topDirs = [...dirs].slice(0, 5);
1376
- layers.push({
1377
- name: 'Application',
1378
- technologies: frameworkNames.length > 0 ? frameworkNames : ['Unknown'],
1379
- directories: topDirs,
1380
- });
1381
- }
1382
- return layers;
1383
- }
1384
- function inferServiceTech(files, serviceDirs) {
1385
- const techs = [];
1386
- const serviceFiles = files.filter((f) => serviceDirs.some((d) => f.directory.startsWith(d)));
1387
- const hasTsFiles = serviceFiles.some((f) => f.extension === '.ts' || f.extension === '.tsx');
1388
- const hasJsFiles = serviceFiles.some((f) => f.extension === '.js' || f.extension === '.jsx');
1389
- if (hasTsFiles)
1390
- techs.push('TypeScript');
1391
- else if (hasJsFiles)
1392
- techs.push('JavaScript');
1393
- if (techs.length === 0)
1394
- techs.push('Mixed');
1395
- return techs;
1396
- }
1397
- // ── Helpers ───────────────────────────────────────────────
1398
- function promptYesNo(question) {
1399
- return new Promise((resolve) => {
1400
- const rl = readline.createInterface({
1401
- input: process.stdin,
1402
- output: process.stdout,
1403
- });
1404
- rl.question(question, (answer) => {
1405
- rl.close();
1406
- resolve(answer.toLowerCase().startsWith('y'));
1407
- });
1408
- });
1409
- }
1410
- // ── Command: help ─────────────────────────────────────────
1411
- program
1412
- .command('help')
1413
- .description('Show detailed help with all commands and options')
1414
- .action(() => {
1415
- showHelp();
1416
- });
1417
- // ── 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();
1418
47
  program.parse();
1419
48
  //# sourceMappingURL=index.js.map