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.
- package/README.md +56 -19
- package/dist/analyzers/unusedDependencyCheck.js +69 -17
- package/dist/analyzers/unusedDependencyCheck.js.map +1 -1
- package/dist/cli/_shared.d.ts +16 -0
- package/dist/cli/_shared.js +210 -0
- package/dist/cli/_shared.js.map +1 -0
- package/dist/cli/commands/analyze.d.ts +1 -0
- package/dist/cli/commands/analyze.js +87 -0
- package/dist/cli/commands/analyze.js.map +1 -0
- package/dist/cli/commands/audit.d.ts +1 -0
- package/dist/cli/commands/audit.js +47 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/badge.d.ts +1 -0
- package/dist/cli/commands/badge.js +45 -0
- package/dist/cli/commands/badge.js.map +1 -0
- package/dist/cli/commands/ci.d.ts +1 -0
- package/dist/cli/commands/ci.js +57 -0
- package/dist/cli/commands/ci.js.map +1 -0
- package/dist/cli/commands/coupling.d.ts +1 -0
- package/dist/cli/commands/coupling.js +83 -0
- package/dist/cli/commands/coupling.js.map +1 -0
- package/dist/cli/commands/coverage.d.ts +1 -0
- package/dist/cli/commands/coverage.js +63 -0
- package/dist/cli/commands/coverage.js.map +1 -0
- package/dist/cli/commands/dependencies.d.ts +1 -0
- package/dist/cli/commands/dependencies.js +45 -0
- package/dist/cli/commands/dependencies.js.map +1 -0
- package/dist/cli/commands/diagram.d.ts +1 -0
- package/dist/cli/commands/diagram.js +45 -0
- package/dist/cli/commands/diagram.js.map +1 -0
- package/dist/cli/commands/diff.d.ts +1 -0
- package/dist/cli/commands/diff.js +70 -0
- package/dist/cli/commands/diff.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +62 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/explain.d.ts +1 -0
- package/dist/cli/commands/explain.js +42 -0
- package/dist/cli/commands/explain.js.map +1 -0
- package/dist/cli/commands/file.d.ts +1 -0
- package/dist/cli/commands/file.js +45 -0
- package/dist/cli/commands/file.js.map +1 -0
- package/dist/cli/commands/fix.d.ts +1 -0
- package/dist/cli/commands/fix.js +70 -0
- package/dist/cli/commands/fix.js.map +1 -0
- package/dist/cli/commands/help.d.ts +1 -0
- package/dist/cli/commands/help.js +11 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/hotspots.d.ts +1 -0
- package/dist/cli/commands/hotspots.js +74 -0
- package/dist/cli/commands/hotspots.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +1 -0
- package/dist/cli/commands/mcp.js +21 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/outdated.d.ts +1 -0
- package/dist/cli/commands/outdated.js +51 -0
- package/dist/cli/commands/outdated.js.map +1 -0
- package/dist/cli/commands/prDiff.d.ts +1 -0
- package/dist/cli/commands/prDiff.js +59 -0
- package/dist/cli/commands/prDiff.js.map +1 -0
- package/dist/cli/commands/search.d.ts +1 -0
- package/dist/cli/commands/search.js +233 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/structure.d.ts +1 -0
- package/dist/cli/commands/structure.js +58 -0
- package/dist/cli/commands/structure.js.map +1 -0
- package/dist/cli/commands/upgrade.d.ts +1 -0
- package/dist/cli/commands/upgrade.js +44 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/commands/workspaces.d.ts +1 -0
- package/dist/cli/commands/workspaces.js +35 -0
- package/dist/cli/commands/workspaces.js.map +1 -0
- package/dist/cli/index.js +45 -1132
- package/dist/cli/index.js.map +1 -1
- package/dist/core/ast.d.ts +2 -0
- package/dist/core/ast.js +35 -2
- package/dist/core/ast.js.map +1 -1
- package/dist/core/codeGraph.d.ts +2 -0
- package/dist/core/codeGraph.js +2 -0
- package/dist/core/codeGraph.js.map +1 -1
- package/dist/core/couplingAnalyzer.d.ts +18 -0
- package/dist/core/couplingAnalyzer.js +174 -0
- package/dist/core/couplingAnalyzer.js.map +1 -0
- package/dist/core/fileInspector.d.ts +1 -1
- package/dist/core/fileInspector.js +31 -1
- package/dist/core/fileInspector.js.map +1 -1
- package/dist/core/hotspotAnalyzer.d.ts +13 -0
- package/dist/core/hotspotAnalyzer.js +29 -6
- package/dist/core/hotspotAnalyzer.js.map +1 -1
- package/dist/core/indexCache.js +6 -3
- package/dist/core/indexCache.js.map +1 -1
- package/dist/core/languages/LanguageAdapter.d.ts +1 -1
- package/dist/core/languages/goAdapter.d.ts +2 -0
- package/dist/core/languages/goAdapter.js +138 -0
- package/dist/core/languages/goAdapter.js.map +1 -0
- package/dist/core/languages/goCallSites.d.ts +20 -0
- package/dist/core/languages/goCallSites.js +42 -0
- package/dist/core/languages/goCallSites.js.map +1 -0
- package/dist/core/languages/goCyclomatic.d.ts +21 -0
- package/dist/core/languages/goCyclomatic.js +55 -0
- package/dist/core/languages/goCyclomatic.js.map +1 -0
- package/dist/core/languages/goExports.d.ts +26 -0
- package/dist/core/languages/goExports.js +89 -0
- package/dist/core/languages/goExports.js.map +1 -0
- package/dist/core/languages/goImports.d.ts +26 -0
- package/dist/core/languages/goImports.js +64 -0
- package/dist/core/languages/goImports.js.map +1 -0
- package/dist/core/languages/goManifests.d.ts +19 -0
- package/dist/core/languages/goManifests.js +56 -0
- package/dist/core/languages/goManifests.js.map +1 -0
- package/dist/core/languages/javaAdapter.d.ts +2 -0
- package/dist/core/languages/javaAdapter.js +148 -0
- package/dist/core/languages/javaAdapter.js.map +1 -0
- package/dist/core/languages/javaCallSites.d.ts +16 -0
- package/dist/core/languages/javaCallSites.js +45 -0
- package/dist/core/languages/javaCallSites.js.map +1 -0
- package/dist/core/languages/javaCyclomatic.d.ts +21 -0
- package/dist/core/languages/javaCyclomatic.js +49 -0
- package/dist/core/languages/javaCyclomatic.js.map +1 -0
- package/dist/core/languages/javaExports.d.ts +25 -0
- package/dist/core/languages/javaExports.js +80 -0
- package/dist/core/languages/javaExports.js.map +1 -0
- package/dist/core/languages/javaImports.d.ts +25 -0
- package/dist/core/languages/javaImports.js +49 -0
- package/dist/core/languages/javaImports.js.map +1 -0
- package/dist/core/languages/javaManifests.d.ts +25 -0
- package/dist/core/languages/javaManifests.js +86 -0
- package/dist/core/languages/javaManifests.js.map +1 -0
- package/dist/core/languages/pythonAdapter.js +8 -1
- package/dist/core/languages/pythonAdapter.js.map +1 -1
- package/dist/core/languages/pythonCallSites.d.ts +19 -0
- package/dist/core/languages/pythonCallSites.js +40 -0
- package/dist/core/languages/pythonCallSites.js.map +1 -0
- package/dist/core/languages/pythonCyclomatic.d.ts +18 -0
- package/dist/core/languages/pythonCyclomatic.js +45 -0
- package/dist/core/languages/pythonCyclomatic.js.map +1 -0
- package/dist/core/languages/registry.js +4 -1
- package/dist/core/languages/registry.js.map +1 -1
- package/dist/core/languages/rubyAdapter.d.ts +2 -0
- package/dist/core/languages/rubyAdapter.js +131 -0
- package/dist/core/languages/rubyAdapter.js.map +1 -0
- package/dist/core/languages/rubyCallSites.d.ts +16 -0
- package/dist/core/languages/rubyCallSites.js +34 -0
- package/dist/core/languages/rubyCallSites.js.map +1 -0
- package/dist/core/languages/rubyCyclomatic.d.ts +19 -0
- package/dist/core/languages/rubyCyclomatic.js +47 -0
- package/dist/core/languages/rubyCyclomatic.js.map +1 -0
- package/dist/core/languages/rubyExports.d.ts +24 -0
- package/dist/core/languages/rubyExports.js +53 -0
- package/dist/core/languages/rubyExports.js.map +1 -0
- package/dist/core/languages/rubyImports.d.ts +12 -0
- package/dist/core/languages/rubyImports.js +75 -0
- package/dist/core/languages/rubyImports.js.map +1 -0
- package/dist/core/languages/rubyManifests.d.ts +20 -0
- package/dist/core/languages/rubyManifests.js +55 -0
- package/dist/core/languages/rubyManifests.js.map +1 -0
- package/dist/core/languages/treeSitterLoader.js +4 -1
- package/dist/core/languages/treeSitterLoader.js.map +1 -1
- package/dist/core/monorepo.d.ts +20 -0
- package/dist/core/monorepo.js +270 -0
- package/dist/core/monorepo.js.map +1 -0
- package/dist/core/outdatedDetector.d.ts +13 -2
- package/dist/core/outdatedDetector.js +86 -16
- package/dist/core/outdatedDetector.js.map +1 -1
- package/dist/core/prDiff.d.ts +43 -0
- package/dist/core/prDiff.js +298 -0
- package/dist/core/prDiff.js.map +1 -0
- package/dist/core/telemetry.d.ts +90 -0
- package/dist/core/telemetry.js +199 -0
- package/dist/core/telemetry.js.map +1 -0
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-java.wasm +0 -0
- package/dist/grammars/tree-sitter-ruby.wasm +0 -0
- package/dist/mcp/tools/_shared.d.ts +24 -0
- package/dist/mcp/tools/_shared.js +82 -0
- package/dist/mcp/tools/_shared.js.map +1 -0
- package/dist/mcp/tools/analyze.d.ts +2 -0
- package/dist/mcp/tools/analyze.js +55 -0
- package/dist/mcp/tools/analyze.js.map +1 -0
- package/dist/mcp/tools/audit.d.ts +2 -0
- package/dist/mcp/tools/audit.js +32 -0
- package/dist/mcp/tools/audit.js.map +1 -0
- package/dist/mcp/tools/coupling.d.ts +2 -0
- package/dist/mcp/tools/coupling.js +67 -0
- package/dist/mcp/tools/coupling.js.map +1 -0
- package/dist/mcp/tools/coverage.d.ts +2 -0
- package/dist/mcp/tools/coverage.js +53 -0
- package/dist/mcp/tools/coverage.js.map +1 -0
- package/dist/mcp/tools/dependencies.d.ts +2 -0
- package/dist/mcp/tools/dependencies.js +16 -0
- package/dist/mcp/tools/dependencies.js.map +1 -0
- package/dist/mcp/tools/doctor.d.ts +2 -0
- package/dist/mcp/tools/doctor.js +30 -0
- package/dist/mcp/tools/doctor.js.map +1 -0
- package/dist/mcp/tools/explain.d.ts +2 -0
- package/dist/mcp/tools/explain.js +30 -0
- package/dist/mcp/tools/explain.js.map +1 -0
- package/dist/mcp/tools/file.d.ts +2 -0
- package/dist/mcp/tools/file.js +22 -0
- package/dist/mcp/tools/file.js.map +1 -0
- package/dist/mcp/tools/graph.d.ts +2 -0
- package/dist/mcp/tools/graph.js +69 -0
- package/dist/mcp/tools/graph.js.map +1 -0
- package/dist/mcp/tools/hotspots.d.ts +2 -0
- package/dist/mcp/tools/hotspots.js +64 -0
- package/dist/mcp/tools/hotspots.js.map +1 -0
- package/dist/mcp/tools/outdated.d.ts +2 -0
- package/dist/mcp/tools/outdated.js +36 -0
- package/dist/mcp/tools/outdated.js.map +1 -0
- package/dist/mcp/tools/prDiff.d.ts +2 -0
- package/dist/mcp/tools/prDiff.js +38 -0
- package/dist/mcp/tools/prDiff.js.map +1 -0
- package/dist/mcp/tools/search.d.ts +2 -0
- package/dist/mcp/tools/search.js +167 -0
- package/dist/mcp/tools/search.js.map +1 -0
- package/dist/mcp/tools/structure.d.ts +2 -0
- package/dist/mcp/tools/structure.js +34 -0
- package/dist/mcp/tools/structure.js.map +1 -0
- package/dist/mcp/tools/upgrade.d.ts +2 -0
- package/dist/mcp/tools/upgrade.js +38 -0
- package/dist/mcp/tools/upgrade.js.map +1 -0
- package/dist/mcp/tools/workspaces.d.ts +2 -0
- package/dist/mcp/tools/workspaces.js +13 -0
- package/dist/mcp/tools/workspaces.js.map +1 -0
- package/dist/mcp/tools.d.ts +12 -6
- package/dist/mcp/tools.js +40 -605
- package/dist/mcp/tools.js.map +1 -1
- package/dist/reporters/consoleReporter.d.ts +4 -1
- package/dist/reporters/consoleReporter.js +113 -0
- package/dist/reporters/consoleReporter.js.map +1 -1
- package/dist/reporters/jsonReporter.d.ts +4 -1
- package/dist/reporters/jsonReporter.js +9 -0
- package/dist/reporters/jsonReporter.js.map +1 -1
- package/dist/reporters/markdownReporter.d.ts +4 -1
- package/dist/reporters/markdownReporter.js +103 -3
- package/dist/reporters/markdownReporter.js.map +1 -1
- package/dist/tool-manifest.json +358 -0
- package/dist/types.d.ts +115 -0
- package/dist/utils/cache.d.ts +3 -0
- package/dist/utils/cache.js +51 -0
- package/dist/utils/cache.js.map +1 -0
- package/package.json +7 -3
package/dist/cli/index.js
CHANGED
|
@@ -1,1135 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|