driftdetect 0.1.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/dist/bin/drift.d.ts +11 -0
- package/dist/bin/drift.d.ts.map +1 -0
- package/dist/bin/drift.js +83 -0
- package/dist/bin/drift.js.map +1 -0
- package/dist/commands/approve.d.ts +18 -0
- package/dist/commands/approve.d.ts.map +1 -0
- package/dist/commands/approve.js +271 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/check.d.ts +39 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +268 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/export.d.ts +14 -0
- package/dist/commands/export.d.ts.map +1 -0
- package/dist/commands/export.js +109 -0
- package/dist/commands/export.js.map +1 -0
- package/dist/commands/files.d.ts +13 -0
- package/dist/commands/files.d.ts.map +1 -0
- package/dist/commands/files.js +88 -0
- package/dist/commands/files.js.map +1 -0
- package/dist/commands/ignore.d.ts +18 -0
- package/dist/commands/ignore.d.ts.map +1 -0
- package/dist/commands/ignore.js +200 -0
- package/dist/commands/ignore.js.map +1 -0
- package/dist/commands/index.d.ts +16 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +16 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/init.d.ts +19 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +320 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/report.d.ts +20 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +202 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/scan.d.ts +27 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +444 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/status.d.ts +18 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +199 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/where.d.ts +13 -0
- package/dist/commands/where.d.ts.map +1 -0
- package/dist/commands/where.js +80 -0
- package/dist/commands/where.js.map +1 -0
- package/dist/git/hooks.d.ts +108 -0
- package/dist/git/hooks.d.ts.map +1 -0
- package/dist/git/hooks.js +389 -0
- package/dist/git/hooks.js.map +1 -0
- package/dist/git/index.d.ts +6 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/index.js +6 -0
- package/dist/git/index.js.map +1 -0
- package/dist/git/staged-files.d.ts +41 -0
- package/dist/git/staged-files.d.ts.map +1 -0
- package/dist/git/staged-files.js +118 -0
- package/dist/git/staged-files.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/reporters/github-reporter.d.ts +13 -0
- package/dist/reporters/github-reporter.d.ts.map +1 -0
- package/dist/reporters/github-reporter.js +59 -0
- package/dist/reporters/github-reporter.js.map +1 -0
- package/dist/reporters/gitlab-reporter.d.ts +16 -0
- package/dist/reporters/gitlab-reporter.d.ts.map +1 -0
- package/dist/reporters/gitlab-reporter.js +62 -0
- package/dist/reporters/gitlab-reporter.js.map +1 -0
- package/dist/reporters/index.d.ts +9 -0
- package/dist/reporters/index.d.ts.map +1 -0
- package/dist/reporters/index.js +9 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/json-reporter.d.ts +13 -0
- package/dist/reporters/json-reporter.d.ts.map +1 -0
- package/dist/reporters/json-reporter.js +44 -0
- package/dist/reporters/json-reporter.js.map +1 -0
- package/dist/reporters/text-reporter.d.ts +13 -0
- package/dist/reporters/text-reporter.d.ts.map +1 -0
- package/dist/reporters/text-reporter.js +96 -0
- package/dist/reporters/text-reporter.js.map +1 -0
- package/dist/reporters/types.d.ts +42 -0
- package/dist/reporters/types.d.ts.map +1 -0
- package/dist/reporters/types.js +5 -0
- package/dist/reporters/types.js.map +1 -0
- package/dist/services/scanner-service.d.ts +166 -0
- package/dist/services/scanner-service.d.ts.map +1 -0
- package/dist/services/scanner-service.js +453 -0
- package/dist/services/scanner-service.js.map +1 -0
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/ui/index.d.ts +11 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/progress.d.ts +115 -0
- package/dist/ui/progress.d.ts.map +1 -0
- package/dist/ui/progress.js +232 -0
- package/dist/ui/progress.js.map +1 -0
- package/dist/ui/prompts.d.ts +91 -0
- package/dist/ui/prompts.d.ts.map +1 -0
- package/dist/ui/prompts.js +160 -0
- package/dist/ui/prompts.js.map +1 -0
- package/dist/ui/spinner.d.ts +109 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +179 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/table.d.ts +118 -0
- package/dist/ui/table.d.ts.map +1 -0
- package/dist/ui/table.js +235 -0
- package/dist/ui/table.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report Command - drift report
|
|
3
|
+
*
|
|
4
|
+
* Generate reports in various formats.
|
|
5
|
+
*
|
|
6
|
+
* @requirements 29.7
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import * as fs from 'node:fs/promises';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { PatternStore, } from 'driftdetect-core';
|
|
13
|
+
import { createSpinner, status } from '../ui/spinner.js';
|
|
14
|
+
import { promptReportFormat, promptCategorySelection } from '../ui/prompts.js';
|
|
15
|
+
import { TextReporter, JsonReporter, GitHubReporter, GitLabReporter, } from '../reporters/index.js';
|
|
16
|
+
/** Directory name for drift configuration */
|
|
17
|
+
const DRIFT_DIR = '.drift';
|
|
18
|
+
/**
|
|
19
|
+
* Check if drift is initialized
|
|
20
|
+
*/
|
|
21
|
+
async function isDriftInitialized(rootDir) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(path.join(rootDir, DRIFT_DIR));
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get reporter based on format
|
|
32
|
+
*/
|
|
33
|
+
function getReporter(format) {
|
|
34
|
+
switch (format) {
|
|
35
|
+
case 'json':
|
|
36
|
+
return new JsonReporter();
|
|
37
|
+
case 'github':
|
|
38
|
+
return new GitHubReporter();
|
|
39
|
+
case 'gitlab':
|
|
40
|
+
return new GitLabReporter();
|
|
41
|
+
case 'text':
|
|
42
|
+
default:
|
|
43
|
+
return new TextReporter();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get file extension for format
|
|
48
|
+
*/
|
|
49
|
+
function getExtension(format) {
|
|
50
|
+
switch (format) {
|
|
51
|
+
case 'json':
|
|
52
|
+
case 'gitlab':
|
|
53
|
+
return 'json';
|
|
54
|
+
case 'github':
|
|
55
|
+
case 'text':
|
|
56
|
+
default:
|
|
57
|
+
return 'txt';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Report command implementation
|
|
62
|
+
*/
|
|
63
|
+
async function reportAction(options) {
|
|
64
|
+
const rootDir = process.cwd();
|
|
65
|
+
const verbose = options.verbose ?? false;
|
|
66
|
+
console.log();
|
|
67
|
+
console.log(chalk.bold('🔍 Drift - Generate Report'));
|
|
68
|
+
console.log();
|
|
69
|
+
// Check if initialized
|
|
70
|
+
if (!(await isDriftInitialized(rootDir))) {
|
|
71
|
+
status.error('Drift is not initialized. Run `drift init` first.');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
// Initialize pattern store
|
|
75
|
+
const spinner = createSpinner('Loading patterns...');
|
|
76
|
+
spinner.start();
|
|
77
|
+
const store = new PatternStore({ rootDir });
|
|
78
|
+
await store.initialize();
|
|
79
|
+
spinner.succeed('Patterns loaded');
|
|
80
|
+
// Get format
|
|
81
|
+
let format = options.format;
|
|
82
|
+
if (!format) {
|
|
83
|
+
format = await promptReportFormat();
|
|
84
|
+
}
|
|
85
|
+
// Get categories to include
|
|
86
|
+
let categories = options.categories;
|
|
87
|
+
if (!categories || categories.length === 0) {
|
|
88
|
+
const stats = store.getStats();
|
|
89
|
+
const availableCategories = Object.entries(stats.byCategory)
|
|
90
|
+
.filter(([_, count]) => count > 0)
|
|
91
|
+
.map(([category]) => category);
|
|
92
|
+
if (availableCategories.length > 1 && !options.output) {
|
|
93
|
+
// Interactive category selection
|
|
94
|
+
categories = await promptCategorySelection(availableCategories);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
categories = availableCategories;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Filter patterns by category
|
|
101
|
+
let patterns = store.getApproved();
|
|
102
|
+
if (categories && categories.length > 0) {
|
|
103
|
+
patterns = patterns.filter((p) => categories.includes(p.category));
|
|
104
|
+
}
|
|
105
|
+
if (patterns.length === 0) {
|
|
106
|
+
status.info('No approved patterns to report on');
|
|
107
|
+
console.log(chalk.gray('Run `drift scan` and `drift approve` to add patterns'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Collect violations from outliers
|
|
111
|
+
const violations = [];
|
|
112
|
+
for (const pattern of patterns) {
|
|
113
|
+
for (const outlier of pattern.outliers) {
|
|
114
|
+
const violation = {
|
|
115
|
+
id: `${pattern.id}-${outlier.file}-${outlier.line}`,
|
|
116
|
+
patternId: pattern.id,
|
|
117
|
+
severity: pattern.severity,
|
|
118
|
+
file: outlier.file,
|
|
119
|
+
range: {
|
|
120
|
+
start: { line: outlier.line, character: outlier.column },
|
|
121
|
+
end: { line: outlier.endLine ?? outlier.line, character: outlier.endColumn ?? outlier.column },
|
|
122
|
+
},
|
|
123
|
+
message: `Deviation from pattern: ${pattern.name}`,
|
|
124
|
+
explanation: outlier.reason,
|
|
125
|
+
expected: pattern.description,
|
|
126
|
+
actual: `Code at line ${outlier.line} deviates from the established pattern`,
|
|
127
|
+
aiExplainAvailable: true,
|
|
128
|
+
aiFixAvailable: pattern.autoFixable,
|
|
129
|
+
firstSeen: new Date(),
|
|
130
|
+
occurrences: 1,
|
|
131
|
+
};
|
|
132
|
+
violations.push(violation);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Calculate summary
|
|
136
|
+
const errorCount = violations.filter((v) => v.severity === 'error').length;
|
|
137
|
+
const warningCount = violations.filter((v) => v.severity === 'warning').length;
|
|
138
|
+
const infoCount = violations.filter((v) => v.severity === 'info').length;
|
|
139
|
+
const hintCount = violations.filter((v) => v.severity === 'hint').length;
|
|
140
|
+
// Prepare report data
|
|
141
|
+
const reportData = {
|
|
142
|
+
violations,
|
|
143
|
+
summary: {
|
|
144
|
+
total: violations.length,
|
|
145
|
+
errors: errorCount,
|
|
146
|
+
warnings: warningCount,
|
|
147
|
+
infos: infoCount,
|
|
148
|
+
hints: hintCount,
|
|
149
|
+
},
|
|
150
|
+
patterns,
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
rootDir,
|
|
153
|
+
};
|
|
154
|
+
// Generate report
|
|
155
|
+
const generateSpinner = createSpinner('Generating report...');
|
|
156
|
+
generateSpinner.start();
|
|
157
|
+
const reporter = getReporter(format);
|
|
158
|
+
const report = reporter.generate(reportData);
|
|
159
|
+
generateSpinner.succeed('Report generated');
|
|
160
|
+
// Output report
|
|
161
|
+
if (options.output) {
|
|
162
|
+
// Write to file
|
|
163
|
+
const outputPath = path.resolve(rootDir, options.output);
|
|
164
|
+
const outputDir = path.dirname(outputPath);
|
|
165
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
166
|
+
await fs.writeFile(outputPath, report);
|
|
167
|
+
status.success(`Report saved to ${path.relative(rootDir, outputPath)}`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Print to console
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(report);
|
|
173
|
+
}
|
|
174
|
+
// Save to reports directory
|
|
175
|
+
const reportsDir = path.join(rootDir, DRIFT_DIR, 'reports');
|
|
176
|
+
await fs.mkdir(reportsDir, { recursive: true });
|
|
177
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
178
|
+
const reportPath = path.join(reportsDir, `report-${timestamp}.${getExtension(format)}`);
|
|
179
|
+
await fs.writeFile(reportPath, report);
|
|
180
|
+
if (verbose) {
|
|
181
|
+
status.info(`Report also saved to ${path.relative(rootDir, reportPath)}`);
|
|
182
|
+
}
|
|
183
|
+
// Summary
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(chalk.bold('Report Summary'));
|
|
186
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
187
|
+
console.log(` Format: ${format}`);
|
|
188
|
+
console.log(` Patterns: ${patterns.length}`);
|
|
189
|
+
console.log(` Violations: ${violations.length}`);
|
|
190
|
+
if (categories && categories.length > 0) {
|
|
191
|
+
console.log(` Categories: ${categories.join(', ')}`);
|
|
192
|
+
}
|
|
193
|
+
console.log();
|
|
194
|
+
}
|
|
195
|
+
export const reportCommand = new Command('report')
|
|
196
|
+
.description('Generate a report')
|
|
197
|
+
.option('-f, --format <format>', 'Output format (text, json, github, gitlab)')
|
|
198
|
+
.option('-o, --output <path>', 'Output file path')
|
|
199
|
+
.option('-c, --categories <categories...>', 'Include only specific categories')
|
|
200
|
+
.option('--verbose', 'Enable verbose output')
|
|
201
|
+
.action(reportAction);
|
|
202
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/commands/report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan Command - drift scan
|
|
3
|
+
*
|
|
4
|
+
* Perform a full codebase scan to discover patterns using
|
|
5
|
+
* enterprise-grade detectors from driftdetect-detectors.
|
|
6
|
+
*
|
|
7
|
+
* @requirements 29.2
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
export interface ScanCommandOptions {
|
|
11
|
+
/** Specific paths to scan */
|
|
12
|
+
paths?: string[];
|
|
13
|
+
/** Enable verbose output */
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
/** Force rescan even if cache is valid */
|
|
16
|
+
force?: boolean;
|
|
17
|
+
/** Only run critical detectors */
|
|
18
|
+
critical?: boolean;
|
|
19
|
+
/** Filter by categories */
|
|
20
|
+
categories?: string[];
|
|
21
|
+
/** Generate manifest with semantic locations */
|
|
22
|
+
manifest?: boolean;
|
|
23
|
+
/** Incremental scan (only changed files) */
|
|
24
|
+
incremental?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare const scanCommand: Command;
|
|
27
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../src/commands/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBpC,MAAM,WAAW,kBAAkB;IACjC,6BAA6B;IAC7B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2BAA2B;IAC3B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,gDAAgD;IAChD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA4dD,eAAO,MAAM,WAAW,SASH,CAAC"}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan Command - drift scan
|
|
3
|
+
*
|
|
4
|
+
* Perform a full codebase scan to discover patterns using
|
|
5
|
+
* enterprise-grade detectors from driftdetect-detectors.
|
|
6
|
+
*
|
|
7
|
+
* @requirements 29.2
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from 'commander';
|
|
10
|
+
import * as fs from 'node:fs/promises';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import * as crypto from 'node:crypto';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { PatternStore, FileWalker, } from 'driftdetect-core';
|
|
15
|
+
import { createSpinner, status } from '../ui/spinner.js';
|
|
16
|
+
import { createPatternsTable } from '../ui/table.js';
|
|
17
|
+
import { createScannerService } from '../services/scanner-service.js';
|
|
18
|
+
/** Directory name for drift configuration */
|
|
19
|
+
const DRIFT_DIR = '.drift';
|
|
20
|
+
/**
|
|
21
|
+
* Check if drift is initialized
|
|
22
|
+
*/
|
|
23
|
+
async function isDriftInitialized(rootDir) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(path.join(rootDir, DRIFT_DIR));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Load ignore patterns from .driftignore
|
|
34
|
+
*/
|
|
35
|
+
async function loadIgnorePatterns(rootDir) {
|
|
36
|
+
const defaultIgnores = [
|
|
37
|
+
'node_modules/**',
|
|
38
|
+
'.git/**',
|
|
39
|
+
'dist/**',
|
|
40
|
+
'build/**',
|
|
41
|
+
'coverage/**',
|
|
42
|
+
'.drift/**',
|
|
43
|
+
'__pycache__/**',
|
|
44
|
+
'.venv/**',
|
|
45
|
+
'venv/**',
|
|
46
|
+
];
|
|
47
|
+
try {
|
|
48
|
+
const driftignorePath = path.join(rootDir, '.driftignore');
|
|
49
|
+
const content = await fs.readFile(driftignorePath, 'utf-8');
|
|
50
|
+
const patterns = content
|
|
51
|
+
.split('\n')
|
|
52
|
+
.map((line) => line.trim())
|
|
53
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
54
|
+
return [...defaultIgnores, ...patterns];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return defaultIgnores;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get file extension
|
|
62
|
+
*/
|
|
63
|
+
function getExtension(filePath) {
|
|
64
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
65
|
+
return ext.startsWith('.') ? ext.slice(1) : ext;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if file is scannable
|
|
69
|
+
*/
|
|
70
|
+
function isScannableFile(filePath) {
|
|
71
|
+
const scannableExtensions = [
|
|
72
|
+
'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
|
|
73
|
+
'py', 'pyw',
|
|
74
|
+
'css', 'scss', 'sass', 'less',
|
|
75
|
+
'json', 'yaml', 'yml',
|
|
76
|
+
'md', 'mdx',
|
|
77
|
+
'html', 'htm',
|
|
78
|
+
'vue', 'svelte',
|
|
79
|
+
];
|
|
80
|
+
const ext = getExtension(filePath);
|
|
81
|
+
return scannableExtensions.includes(ext);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Group files by type for reporting
|
|
85
|
+
*/
|
|
86
|
+
function groupFilesByType(files) {
|
|
87
|
+
const groups = new Map();
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const ext = getExtension(file) || 'other';
|
|
90
|
+
groups.set(ext, (groups.get(ext) ?? 0) + 1);
|
|
91
|
+
}
|
|
92
|
+
return groups;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Map detector category to PatternCategory
|
|
96
|
+
*/
|
|
97
|
+
function mapToPatternCategory(category) {
|
|
98
|
+
const mapping = {
|
|
99
|
+
'api': 'api',
|
|
100
|
+
'auth': 'auth',
|
|
101
|
+
'security': 'security',
|
|
102
|
+
'errors': 'errors',
|
|
103
|
+
'structural': 'structural',
|
|
104
|
+
'components': 'components',
|
|
105
|
+
'styling': 'styling',
|
|
106
|
+
'logging': 'logging',
|
|
107
|
+
'testing': 'testing',
|
|
108
|
+
'data-access': 'data-access',
|
|
109
|
+
'config': 'config',
|
|
110
|
+
'types': 'types',
|
|
111
|
+
'performance': 'performance',
|
|
112
|
+
'accessibility': 'accessibility',
|
|
113
|
+
'documentation': 'documentation',
|
|
114
|
+
};
|
|
115
|
+
return mapping[category] || 'structural';
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Convert aggregated pattern to Pattern for storage
|
|
119
|
+
*/
|
|
120
|
+
function convertToPattern(aggPattern, violations, rootDir) {
|
|
121
|
+
const now = new Date().toISOString();
|
|
122
|
+
// Generate unique ID
|
|
123
|
+
const id = crypto.createHash('sha256')
|
|
124
|
+
.update(`${aggPattern.patternId}-${rootDir}`)
|
|
125
|
+
.digest('hex')
|
|
126
|
+
.slice(0, 16);
|
|
127
|
+
// Calculate confidence
|
|
128
|
+
const spread = new Set(aggPattern.locations.map(l => l.file)).size;
|
|
129
|
+
const confidenceScore = Math.min(0.95, aggPattern.confidence);
|
|
130
|
+
const confidenceInfo = {
|
|
131
|
+
frequency: Math.min(1, aggPattern.occurrences / 100),
|
|
132
|
+
consistency: 0.9,
|
|
133
|
+
age: 0,
|
|
134
|
+
spread,
|
|
135
|
+
score: confidenceScore,
|
|
136
|
+
level: confidenceScore >= 0.85 ? 'high' : confidenceScore >= 0.65 ? 'medium' : confidenceScore >= 0.45 ? 'low' : 'uncertain',
|
|
137
|
+
};
|
|
138
|
+
// Create detector config
|
|
139
|
+
const detectorConfig = {
|
|
140
|
+
type: 'regex', // Most detectors are regex-based
|
|
141
|
+
config: {
|
|
142
|
+
detectorId: aggPattern.detectorId,
|
|
143
|
+
patternId: aggPattern.patternId,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
// Get violations for this pattern as outliers
|
|
147
|
+
const patternViolations = violations.filter(v => v.detectorId === aggPattern.detectorId);
|
|
148
|
+
const outliers = patternViolations.map(v => ({
|
|
149
|
+
file: v.file,
|
|
150
|
+
line: v.line,
|
|
151
|
+
column: v.column,
|
|
152
|
+
reason: v.message,
|
|
153
|
+
deviationScore: v.severity === 'error' ? 1.0 : v.severity === 'warning' ? 0.7 : 0.4,
|
|
154
|
+
}));
|
|
155
|
+
// Limit locations to prevent huge files
|
|
156
|
+
const locations = aggPattern.locations.slice(0, 100);
|
|
157
|
+
return {
|
|
158
|
+
id,
|
|
159
|
+
category: mapToPatternCategory(aggPattern.category),
|
|
160
|
+
subcategory: aggPattern.subcategory,
|
|
161
|
+
name: aggPattern.name,
|
|
162
|
+
description: aggPattern.description,
|
|
163
|
+
detector: detectorConfig,
|
|
164
|
+
confidence: confidenceInfo,
|
|
165
|
+
locations,
|
|
166
|
+
outliers,
|
|
167
|
+
metadata: {
|
|
168
|
+
firstSeen: now,
|
|
169
|
+
lastSeen: now,
|
|
170
|
+
source: 'auto-detected',
|
|
171
|
+
tags: [aggPattern.category, aggPattern.subcategory],
|
|
172
|
+
},
|
|
173
|
+
severity: patternViolations.length > 0 ?
|
|
174
|
+
(patternViolations.some(v => v.severity === 'error') ? 'error' : 'warning') :
|
|
175
|
+
'info',
|
|
176
|
+
autoFixable: false,
|
|
177
|
+
status: 'discovered',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Scan command implementation
|
|
182
|
+
*/
|
|
183
|
+
async function scanAction(options) {
|
|
184
|
+
const rootDir = process.cwd();
|
|
185
|
+
const verbose = options.verbose ?? false;
|
|
186
|
+
console.log();
|
|
187
|
+
console.log(chalk.bold('🔍 Drift - Enterprise Pattern Scanner'));
|
|
188
|
+
console.log();
|
|
189
|
+
// Check if initialized
|
|
190
|
+
if (!(await isDriftInitialized(rootDir))) {
|
|
191
|
+
status.error('Drift is not initialized. Run `drift init` first.');
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
// Initialize pattern store
|
|
195
|
+
const store = new PatternStore({ rootDir });
|
|
196
|
+
await store.initialize();
|
|
197
|
+
// Load ignore patterns
|
|
198
|
+
const ignorePatterns = await loadIgnorePatterns(rootDir);
|
|
199
|
+
if (verbose) {
|
|
200
|
+
status.info(`Loaded ${ignorePatterns.length} ignore patterns`);
|
|
201
|
+
}
|
|
202
|
+
// Initialize file walker
|
|
203
|
+
const walker = new FileWalker();
|
|
204
|
+
// Discover files
|
|
205
|
+
const discoverSpinner = createSpinner('Discovering files...');
|
|
206
|
+
discoverSpinner.start();
|
|
207
|
+
let files;
|
|
208
|
+
try {
|
|
209
|
+
const scanOptions = {
|
|
210
|
+
rootDir,
|
|
211
|
+
ignorePatterns,
|
|
212
|
+
respectGitignore: true,
|
|
213
|
+
respectDriftignore: true,
|
|
214
|
+
followSymlinks: false,
|
|
215
|
+
maxDepth: 50,
|
|
216
|
+
};
|
|
217
|
+
// If specific paths provided, use those
|
|
218
|
+
if (options.paths && options.paths.length > 0) {
|
|
219
|
+
files = [];
|
|
220
|
+
for (const p of options.paths) {
|
|
221
|
+
const fullPath = path.resolve(rootDir, p);
|
|
222
|
+
const stat = await fs.stat(fullPath);
|
|
223
|
+
if (stat.isDirectory()) {
|
|
224
|
+
const subResult = await walker.walk({
|
|
225
|
+
...scanOptions,
|
|
226
|
+
rootDir: fullPath,
|
|
227
|
+
});
|
|
228
|
+
files.push(...subResult.files.map((f) => path.relative(rootDir, f.path)));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
files.push(path.relative(rootDir, fullPath));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
const result = await walker.walk(scanOptions);
|
|
237
|
+
files = result.files.map((f) => f.relativePath);
|
|
238
|
+
}
|
|
239
|
+
// Filter to scannable files
|
|
240
|
+
files = files.filter(isScannableFile);
|
|
241
|
+
discoverSpinner.succeed(`Discovered ${files.length} files`);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
discoverSpinner.fail('Failed to discover files');
|
|
245
|
+
console.error(chalk.red(error.message));
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
// Show file type breakdown
|
|
249
|
+
if (verbose) {
|
|
250
|
+
const fileGroups = groupFilesByType(files);
|
|
251
|
+
console.log();
|
|
252
|
+
console.log(chalk.gray('File types:'));
|
|
253
|
+
for (const [ext, count] of Array.from(fileGroups.entries()).sort((a, b) => b[1] - a[1])) {
|
|
254
|
+
console.log(chalk.gray(` .${ext}: ${count}`));
|
|
255
|
+
}
|
|
256
|
+
console.log();
|
|
257
|
+
}
|
|
258
|
+
// Initialize scanner service with real detectors
|
|
259
|
+
const scannerService = createScannerService({
|
|
260
|
+
rootDir,
|
|
261
|
+
verbose,
|
|
262
|
+
criticalOnly: options.critical ?? false,
|
|
263
|
+
categories: options.categories ?? [],
|
|
264
|
+
generateManifest: options.manifest ?? false,
|
|
265
|
+
incremental: options.incremental ?? false,
|
|
266
|
+
});
|
|
267
|
+
const initSpinner = createSpinner('Loading detectors...');
|
|
268
|
+
initSpinner.start();
|
|
269
|
+
try {
|
|
270
|
+
await scannerService.initialize();
|
|
271
|
+
const counts = scannerService.getDetectorCounts();
|
|
272
|
+
initSpinner.succeed(`Loaded ${scannerService.getDetectorCount()} detectors (${counts.total} available)`);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
initSpinner.fail('Failed to load detectors');
|
|
276
|
+
console.error(chalk.red(error.message));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
// Create project context
|
|
280
|
+
const projectContext = {
|
|
281
|
+
rootDir,
|
|
282
|
+
files,
|
|
283
|
+
config: {},
|
|
284
|
+
};
|
|
285
|
+
// Scan files with progress
|
|
286
|
+
const scanSpinner = createSpinner('Analyzing patterns with enterprise detectors...');
|
|
287
|
+
scanSpinner.start();
|
|
288
|
+
const startTime = Date.now();
|
|
289
|
+
try {
|
|
290
|
+
const scanResults = await scannerService.scanFiles(files, projectContext);
|
|
291
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
292
|
+
scanSpinner.succeed(`Analyzed ${scanResults.totalFiles} files in ${duration}s ` +
|
|
293
|
+
`(${scanResults.patterns.length} pattern types, ${scanResults.totalViolations} violations)`);
|
|
294
|
+
if (verbose) {
|
|
295
|
+
console.log(chalk.gray(` Detectors ran: ${scanResults.detectorStats.ran}`));
|
|
296
|
+
console.log(chalk.gray(` Detectors skipped: ${scanResults.detectorStats.skipped}`));
|
|
297
|
+
if (scanResults.errors.length > 0) {
|
|
298
|
+
console.log(chalk.yellow(` Warnings: ${scanResults.errors.length}`));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// Show detected patterns by category
|
|
302
|
+
if (scanResults.patterns.length > 0) {
|
|
303
|
+
console.log();
|
|
304
|
+
console.log(chalk.bold('Patterns detected by category:'));
|
|
305
|
+
const byCategory = new Map();
|
|
306
|
+
for (const pattern of scanResults.patterns) {
|
|
307
|
+
byCategory.set(pattern.category, (byCategory.get(pattern.category) ?? 0) + pattern.occurrences);
|
|
308
|
+
}
|
|
309
|
+
for (const [category, count] of Array.from(byCategory.entries()).sort((a, b) => b[1] - a[1])) {
|
|
310
|
+
console.log(` ${chalk.cyan(category)}: ${count} occurrences`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// Show violations (HIGH VALUE)
|
|
314
|
+
if (scanResults.violations.length > 0) {
|
|
315
|
+
console.log();
|
|
316
|
+
console.log(chalk.bold.red(`⚠️ ${scanResults.violations.length} Violations Found:`));
|
|
317
|
+
// Group by severity
|
|
318
|
+
const errors = scanResults.violations.filter(v => v.severity === 'error');
|
|
319
|
+
const warnings = scanResults.violations.filter(v => v.severity === 'warning');
|
|
320
|
+
const infos = scanResults.violations.filter(v => v.severity === 'info');
|
|
321
|
+
if (errors.length > 0) {
|
|
322
|
+
console.log();
|
|
323
|
+
console.log(chalk.red(` Errors (${errors.length}):`));
|
|
324
|
+
for (const v of errors.slice(0, 5)) {
|
|
325
|
+
console.log(chalk.red(` ${v.file}:${v.line} - ${v.message}`));
|
|
326
|
+
}
|
|
327
|
+
if (errors.length > 5) {
|
|
328
|
+
console.log(chalk.gray(` ... and ${errors.length - 5} more errors`));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (warnings.length > 0) {
|
|
332
|
+
console.log();
|
|
333
|
+
console.log(chalk.yellow(` Warnings (${warnings.length}):`));
|
|
334
|
+
for (const v of warnings.slice(0, 5)) {
|
|
335
|
+
console.log(chalk.yellow(` ${v.file}:${v.line} - ${v.message}`));
|
|
336
|
+
}
|
|
337
|
+
if (warnings.length > 5) {
|
|
338
|
+
console.log(chalk.gray(` ... and ${warnings.length - 5} more warnings`));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (verbose && infos.length > 0) {
|
|
342
|
+
console.log();
|
|
343
|
+
console.log(chalk.blue(` Info (${infos.length}):`));
|
|
344
|
+
for (const v of infos.slice(0, 3)) {
|
|
345
|
+
console.log(chalk.blue(` ${v.file}:${v.line} - ${v.message}`));
|
|
346
|
+
}
|
|
347
|
+
if (infos.length > 3) {
|
|
348
|
+
console.log(chalk.gray(` ... and ${infos.length - 3} more`));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Convert and store patterns
|
|
353
|
+
const saveSpinner = createSpinner('Saving patterns...');
|
|
354
|
+
saveSpinner.start();
|
|
355
|
+
let addedCount = 0;
|
|
356
|
+
let skippedCount = 0;
|
|
357
|
+
for (const aggPattern of scanResults.patterns) {
|
|
358
|
+
const pattern = convertToPattern(aggPattern, scanResults.violations, rootDir);
|
|
359
|
+
// Skip if pattern already exists
|
|
360
|
+
if (store.has(pattern.id)) {
|
|
361
|
+
skippedCount++;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
store.add(pattern);
|
|
366
|
+
addedCount++;
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
if (verbose) {
|
|
370
|
+
console.log(chalk.yellow(` Warning: Could not add pattern ${aggPattern.patternId}: ${e.message}`));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
await store.saveAll();
|
|
375
|
+
saveSpinner.succeed(`Saved ${addedCount} new patterns (${skippedCount} already existed)`);
|
|
376
|
+
// Show manifest info if generated
|
|
377
|
+
if (options.manifest && scanResults.manifest) {
|
|
378
|
+
console.log();
|
|
379
|
+
console.log(chalk.bold('📋 Manifest Generated:'));
|
|
380
|
+
console.log(chalk.gray(` Location: .drift/index/manifest.json`));
|
|
381
|
+
console.log(chalk.gray(` Patterns: ${scanResults.manifest.summary.totalPatterns}`));
|
|
382
|
+
console.log(chalk.gray(` Files: ${scanResults.manifest.summary.totalFiles}`));
|
|
383
|
+
console.log(chalk.gray(` Locations: ${scanResults.manifest.summary.totalLocations}`));
|
|
384
|
+
console.log();
|
|
385
|
+
console.log(chalk.gray('Use these commands to explore:'));
|
|
386
|
+
console.log(chalk.cyan(' drift export --format ai-context'));
|
|
387
|
+
console.log(chalk.cyan(' drift where <pattern>'));
|
|
388
|
+
console.log(chalk.cyan(' drift files <path>'));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
scanSpinner.fail('Scan failed');
|
|
393
|
+
console.error(chalk.red(error.message));
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
// Summary
|
|
397
|
+
console.log();
|
|
398
|
+
const stats = store.getStats();
|
|
399
|
+
console.log(chalk.bold('Scan Summary'));
|
|
400
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
401
|
+
console.log(` Files scanned: ${chalk.cyan(files.length)}`);
|
|
402
|
+
console.log(` Total patterns: ${chalk.cyan(stats.totalPatterns)}`);
|
|
403
|
+
console.log(` Discovered: ${chalk.yellow(stats.byStatus.discovered)}`);
|
|
404
|
+
console.log(` Approved: ${chalk.green(stats.byStatus.approved)}`);
|
|
405
|
+
console.log(` Ignored: ${chalk.gray(stats.byStatus.ignored)}`);
|
|
406
|
+
console.log();
|
|
407
|
+
// Show discovered patterns if any
|
|
408
|
+
if (stats.byStatus.discovered > 0) {
|
|
409
|
+
const discovered = store.getDiscovered();
|
|
410
|
+
const highConfidence = discovered.filter((p) => p.confidence.level === 'high');
|
|
411
|
+
if (highConfidence.length > 0) {
|
|
412
|
+
console.log(chalk.bold('High Confidence Patterns (ready for approval):'));
|
|
413
|
+
console.log();
|
|
414
|
+
const rows = highConfidence.slice(0, 10).map((p) => ({
|
|
415
|
+
id: p.id.slice(0, 13),
|
|
416
|
+
name: p.name.slice(0, 28),
|
|
417
|
+
category: p.category,
|
|
418
|
+
confidence: p.confidence.score,
|
|
419
|
+
locations: p.locations.length,
|
|
420
|
+
outliers: p.outliers.length,
|
|
421
|
+
}));
|
|
422
|
+
console.log(createPatternsTable(rows));
|
|
423
|
+
if (highConfidence.length > 10) {
|
|
424
|
+
console.log(chalk.gray(` ... and ${highConfidence.length - 10} more`));
|
|
425
|
+
}
|
|
426
|
+
console.log();
|
|
427
|
+
}
|
|
428
|
+
console.log(chalk.gray('To review and approve patterns:'));
|
|
429
|
+
console.log(chalk.cyan(' drift status'));
|
|
430
|
+
console.log(chalk.cyan(' drift approve <pattern-id>'));
|
|
431
|
+
}
|
|
432
|
+
console.log();
|
|
433
|
+
}
|
|
434
|
+
export const scanCommand = new Command('scan')
|
|
435
|
+
.description('Scan codebase for patterns using enterprise detectors')
|
|
436
|
+
.option('-p, --paths <paths...>', 'Specific paths to scan')
|
|
437
|
+
.option('--force', 'Force rescan even if cache is valid')
|
|
438
|
+
.option('--verbose', 'Enable verbose output')
|
|
439
|
+
.option('--critical', 'Only run critical/high-value detectors')
|
|
440
|
+
.option('-c, --categories <categories...>', 'Filter by categories (api, auth, security, etc.)')
|
|
441
|
+
.option('--manifest', 'Generate manifest with semantic locations')
|
|
442
|
+
.option('--incremental', 'Only scan changed files')
|
|
443
|
+
.action(scanAction);
|
|
444
|
+
//# sourceMappingURL=scan.js.map
|