@cod3vil/kount-cli 1.0.0 → 1.0.1
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/kount +0 -0
- package/dist/kount.js +44606 -0
- package/package.json +5 -5
- package/bin/kount.js +0 -2
- package/src/cli/.gitkeep +0 -0
- package/src/cli/config-resolver.ts +0 -175
- package/src/cli/parser.ts +0 -52
- package/src/core/.gitkeep +0 -0
- package/src/core/aggregator.ts +0 -204
- package/src/core/cache.ts +0 -130
- package/src/index.tsx +0 -167
- package/src/plugins/.gitkeep +0 -0
- package/src/plugins/built-in/blank-lines.ts +0 -26
- package/src/plugins/built-in/comment-lines.ts +0 -90
- package/src/plugins/built-in/file-size.ts +0 -20
- package/src/plugins/built-in/language-distribution.ts +0 -95
- package/src/plugins/built-in/largest-files.ts +0 -41
- package/src/plugins/built-in/total-files.ts +0 -18
- package/src/plugins/built-in/total-lines.ts +0 -21
- package/src/plugins/index.ts +0 -10
- package/src/plugins/types.ts +0 -58
- package/src/reporters/.gitkeep +0 -0
- package/src/reporters/html.ts +0 -385
- package/src/reporters/markdown.ts +0 -129
- package/src/reporters/terminal/Progress.tsx +0 -39
- package/src/reporters/terminal/Splash.tsx +0 -32
- package/src/reporters/terminal/Summary.tsx +0 -135
- package/src/reporters/terminal/Wizard.tsx +0 -125
- package/src/reporters/terminal/index.ts +0 -6
- package/src/scanner/.gitkeep +0 -0
- package/src/scanner/ignore-parser.ts +0 -168
- package/src/scanner/stream-reader.ts +0 -99
- package/src/utils/.gitkeep +0 -0
- package/src/utils/language-map.ts +0 -79
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
import { Box, Text } from 'ink';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import type { ProjectStats } from '../../plugins/types.js';
|
|
5
|
-
|
|
6
|
-
interface SummaryProps {
|
|
7
|
-
stats: ProjectStats;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Final summary display after scan completes.
|
|
12
|
-
* Strict color-coding: green for success-like values, no emojis.
|
|
13
|
-
* Follows ux-color-semantics and ux-next-steps guidelines.
|
|
14
|
-
*/
|
|
15
|
-
export function Summary({ stats }: SummaryProps): React.ReactElement {
|
|
16
|
-
const totalLines = stats.pluginResults.get('TotalLines')?.summaryValue ?? 0;
|
|
17
|
-
const blankLines = stats.pluginResults.get('BlankLines')?.summaryValue ?? 0;
|
|
18
|
-
const commentLines = stats.pluginResults.get('CommentLines')?.summaryValue ?? 0;
|
|
19
|
-
const totalBytes = stats.pluginResults.get('FileSize')?.summaryValue ?? 0;
|
|
20
|
-
const codeLines = totalLines - blankLines - commentLines;
|
|
21
|
-
const codeRatio = totalLines > 0 ? ((codeLines / totalLines) * 100).toFixed(1) : '0.0';
|
|
22
|
-
|
|
23
|
-
// Format bytes
|
|
24
|
-
const formatSize = (bytes: number): string => {
|
|
25
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
26
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
27
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
// Sort language distribution descending
|
|
31
|
-
const sortedLangs = [...stats.languageDistribution.entries()]
|
|
32
|
-
.sort((a, b) => b[1] - a[1]);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Box flexDirection="column" marginY={1}>
|
|
36
|
-
{/* Summary Section */}
|
|
37
|
-
<Box
|
|
38
|
-
flexDirection="column"
|
|
39
|
-
borderStyle="single"
|
|
40
|
-
borderColor="green"
|
|
41
|
-
paddingX={2}
|
|
42
|
-
paddingY={1}
|
|
43
|
-
>
|
|
44
|
-
<Text color="green" bold> SCAN RESULTS</Text>
|
|
45
|
-
<Box marginTop={1} flexDirection="column">
|
|
46
|
-
<Box>
|
|
47
|
-
<Box width={20}><Text color="white">Files</Text></Box>
|
|
48
|
-
<Text color="cyan" bold>{stats.totalFiles.toLocaleString()}</Text>
|
|
49
|
-
</Box>
|
|
50
|
-
<Box>
|
|
51
|
-
<Box width={20}><Text color="white">Total Lines</Text></Box>
|
|
52
|
-
<Text color="cyan" bold>{totalLines.toLocaleString()}</Text>
|
|
53
|
-
</Box>
|
|
54
|
-
<Box>
|
|
55
|
-
<Box width={20}><Text color="white">Code Lines</Text></Box>
|
|
56
|
-
<Text color="green" bold>{codeLines.toLocaleString()}</Text>
|
|
57
|
-
</Box>
|
|
58
|
-
<Box>
|
|
59
|
-
<Box width={20}><Text color="white">Comment Lines</Text></Box>
|
|
60
|
-
<Text color="yellow" bold>{commentLines.toLocaleString()}</Text>
|
|
61
|
-
</Box>
|
|
62
|
-
<Box>
|
|
63
|
-
<Box width={20}><Text color="white">Blank Lines</Text></Box>
|
|
64
|
-
<Text color="gray" bold>{blankLines.toLocaleString()}</Text>
|
|
65
|
-
</Box>
|
|
66
|
-
<Box>
|
|
67
|
-
<Box width={20}><Text color="white">Code Ratio</Text></Box>
|
|
68
|
-
<Text color="green" bold>{codeRatio}%</Text>
|
|
69
|
-
</Box>
|
|
70
|
-
<Box>
|
|
71
|
-
<Box width={20}><Text color="white">Total Size</Text></Box>
|
|
72
|
-
<Text color="cyan" bold>{formatSize(totalBytes)}</Text>
|
|
73
|
-
</Box>
|
|
74
|
-
</Box>
|
|
75
|
-
</Box>
|
|
76
|
-
|
|
77
|
-
{/* Language Distribution */}
|
|
78
|
-
{sortedLangs.length > 0 && (
|
|
79
|
-
<Box
|
|
80
|
-
flexDirection="column"
|
|
81
|
-
borderStyle="single"
|
|
82
|
-
borderColor="blue"
|
|
83
|
-
paddingX={2}
|
|
84
|
-
paddingY={1}
|
|
85
|
-
marginTop={1}
|
|
86
|
-
>
|
|
87
|
-
<Text color="blue" bold> LANGUAGE DISTRIBUTION</Text>
|
|
88
|
-
<Box marginTop={1} flexDirection="column">
|
|
89
|
-
{sortedLangs.map(([lang, count]) => {
|
|
90
|
-
const pct = ((count / stats.totalFiles) * 100).toFixed(1);
|
|
91
|
-
return (
|
|
92
|
-
<Box key={lang}>
|
|
93
|
-
<Box width={22}><Text color="white">{lang}</Text></Box>
|
|
94
|
-
<Box width={8}><Text color="cyan">{count} files</Text></Box>
|
|
95
|
-
<Text color="gray"> ({pct}%)</Text>
|
|
96
|
-
</Box>
|
|
97
|
-
);
|
|
98
|
-
})}
|
|
99
|
-
</Box>
|
|
100
|
-
</Box>
|
|
101
|
-
)}
|
|
102
|
-
|
|
103
|
-
{/* Largest Files */}
|
|
104
|
-
{stats.largestFiles.length > 0 && (
|
|
105
|
-
<Box
|
|
106
|
-
flexDirection="column"
|
|
107
|
-
borderStyle="single"
|
|
108
|
-
borderColor="magenta"
|
|
109
|
-
paddingX={2}
|
|
110
|
-
paddingY={1}
|
|
111
|
-
marginTop={1}
|
|
112
|
-
>
|
|
113
|
-
<Text color="magenta" bold> TOP {stats.largestFiles.length} LARGEST FILES</Text>
|
|
114
|
-
<Box marginTop={1} flexDirection="column">
|
|
115
|
-
{stats.largestFiles.map((file, i) => {
|
|
116
|
-
const relPath = path.relative(stats.rootDir, file.filePath);
|
|
117
|
-
return (
|
|
118
|
-
<Box key={file.filePath}>
|
|
119
|
-
<Box width={4}><Text color="gray">{i + 1}.</Text></Box>
|
|
120
|
-
<Box width={40}><Text color="white" wrap="truncate-end">{relPath}</Text></Box>
|
|
121
|
-
<Text color="yellow">{formatSize(file.size)}</Text>
|
|
122
|
-
</Box>
|
|
123
|
-
);
|
|
124
|
-
})}
|
|
125
|
-
</Box>
|
|
126
|
-
</Box>
|
|
127
|
-
)}
|
|
128
|
-
|
|
129
|
-
{/* Footer */}
|
|
130
|
-
<Box marginTop={1}>
|
|
131
|
-
<Text color="gray">Scanned at {stats.scannedAt.toLocaleString()}</Text>
|
|
132
|
-
</Box>
|
|
133
|
-
</Box>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import { Box, Text, useApp, useInput } from 'ink';
|
|
2
|
-
import React, { useCallback, useState } from 'react';
|
|
3
|
-
|
|
4
|
-
export interface WizardResult {
|
|
5
|
-
rootDir: string;
|
|
6
|
-
outputMode: 'terminal' | 'markdown' | 'html';
|
|
7
|
-
includeTests: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface WizardProps {
|
|
11
|
-
onComplete: (result: WizardResult) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
type Step = 'rootDir' | 'outputMode' | 'includeTests';
|
|
15
|
-
|
|
16
|
-
const OUTPUT_MODES = ['terminal', 'markdown', 'html'] as const;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Minimal interactive step-based wizard when no CLI flags are provided.
|
|
20
|
-
* Follows prompt-group-flow, input-useinput-hook, input-escape-routes guidelines.
|
|
21
|
-
*/
|
|
22
|
-
export function Wizard({ onComplete }: WizardProps): React.ReactElement {
|
|
23
|
-
const { exit } = useApp();
|
|
24
|
-
const [step, setStep] = useState<Step>('rootDir');
|
|
25
|
-
const [rootDir, setRootDir] = useState('.');
|
|
26
|
-
const [rootDirInput, setRootDirInput] = useState('.');
|
|
27
|
-
const [outputModeIndex, setOutputModeIndex] = useState(0);
|
|
28
|
-
const [includeTestsIndex, setIncludeTestsIndex] = useState(0);
|
|
29
|
-
|
|
30
|
-
const boolOptions = ['Yes', 'No'] as const;
|
|
31
|
-
|
|
32
|
-
useInput(useCallback((input: string, key: { return?: boolean; escape?: boolean; upArrow?: boolean; downArrow?: boolean; backspace?: boolean; delete?: boolean }) => {
|
|
33
|
-
// Escape route
|
|
34
|
-
if (key.escape) {
|
|
35
|
-
exit();
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (step === 'rootDir') {
|
|
40
|
-
if (key.return) {
|
|
41
|
-
setRootDir(rootDirInput || '.');
|
|
42
|
-
setStep('outputMode');
|
|
43
|
-
} else if (key.backspace || key.delete) {
|
|
44
|
-
setRootDirInput(prev => prev.slice(0, -1));
|
|
45
|
-
} else if (input && !key.upArrow && !key.downArrow) {
|
|
46
|
-
setRootDirInput(prev => prev + input);
|
|
47
|
-
}
|
|
48
|
-
} else if (step === 'outputMode') {
|
|
49
|
-
if (key.upArrow) {
|
|
50
|
-
setOutputModeIndex(prev => (prev - 1 + OUTPUT_MODES.length) % OUTPUT_MODES.length);
|
|
51
|
-
} else if (key.downArrow) {
|
|
52
|
-
setOutputModeIndex(prev => (prev + 1) % OUTPUT_MODES.length);
|
|
53
|
-
} else if (key.return) {
|
|
54
|
-
setStep('includeTests');
|
|
55
|
-
}
|
|
56
|
-
} else if (step === 'includeTests') {
|
|
57
|
-
if (key.upArrow || key.downArrow) {
|
|
58
|
-
setIncludeTestsIndex(prev => (prev + 1) % 2);
|
|
59
|
-
} else if (key.return) {
|
|
60
|
-
onComplete({
|
|
61
|
-
rootDir: rootDir,
|
|
62
|
-
outputMode: OUTPUT_MODES[outputModeIndex],
|
|
63
|
-
includeTests: includeTestsIndex === 0,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}, [step, rootDirInput, rootDir, outputModeIndex, includeTestsIndex, onComplete, exit]));
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
71
|
-
<Text color="cyan" bold>KOUNT Setup Wizard</Text>
|
|
72
|
-
<Text color="gray">Press ESC to cancel at any time.</Text>
|
|
73
|
-
<Box marginTop={1} flexDirection="column">
|
|
74
|
-
|
|
75
|
-
{/* Step 1: Root Directory */}
|
|
76
|
-
<Box flexDirection="column">
|
|
77
|
-
<Text color={step === 'rootDir' ? 'white' : 'green'} bold>
|
|
78
|
-
{step === 'rootDir' ? '>' : '✓'} Root directory:
|
|
79
|
-
{step !== 'rootDir' && <Text color="cyan"> {rootDir}</Text>}
|
|
80
|
-
</Text>
|
|
81
|
-
{step === 'rootDir' && (
|
|
82
|
-
<Box marginLeft={2}>
|
|
83
|
-
<Text color="cyan">{rootDirInput}</Text>
|
|
84
|
-
<Text color="gray">█</Text>
|
|
85
|
-
</Box>
|
|
86
|
-
)}
|
|
87
|
-
</Box>
|
|
88
|
-
|
|
89
|
-
{/* Step 2: Output Mode */}
|
|
90
|
-
{(step === 'outputMode' || step === 'includeTests') && (
|
|
91
|
-
<Box flexDirection="column" marginTop={1}>
|
|
92
|
-
<Text color={step === 'outputMode' ? 'white' : 'green'} bold>
|
|
93
|
-
{step === 'outputMode' ? '>' : '✓'} Output mode:
|
|
94
|
-
{step !== 'outputMode' && <Text color="cyan"> {OUTPUT_MODES[outputModeIndex]}</Text>}
|
|
95
|
-
</Text>
|
|
96
|
-
{step === 'outputMode' && (
|
|
97
|
-
<Box marginLeft={2} flexDirection="column">
|
|
98
|
-
{OUTPUT_MODES.map((mode, i) => (
|
|
99
|
-
<Text key={mode} color={i === outputModeIndex ? 'cyan' : 'gray'}>
|
|
100
|
-
{i === outputModeIndex ? '> ' : ' '}{mode}
|
|
101
|
-
</Text>
|
|
102
|
-
))}
|
|
103
|
-
</Box>
|
|
104
|
-
)}
|
|
105
|
-
</Box>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
{/* Step 3: Include Tests */}
|
|
109
|
-
{step === 'includeTests' && (
|
|
110
|
-
<Box flexDirection="column" marginTop={1}>
|
|
111
|
-
<Text color="white" bold>{'>'} Include test files?</Text>
|
|
112
|
-
<Box marginLeft={2} flexDirection="column">
|
|
113
|
-
{boolOptions.map((opt, i) => (
|
|
114
|
-
<Text key={opt} color={i === includeTestsIndex ? 'cyan' : 'gray'}>
|
|
115
|
-
{i === includeTestsIndex ? '> ' : ' '}{opt}
|
|
116
|
-
</Text>
|
|
117
|
-
))}
|
|
118
|
-
</Box>
|
|
119
|
-
</Box>
|
|
120
|
-
)}
|
|
121
|
-
|
|
122
|
-
</Box>
|
|
123
|
-
</Box>
|
|
124
|
-
);
|
|
125
|
-
}
|
package/src/scanner/.gitkeep
DELETED
|
File without changes
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import ignore, { Ignore } from 'ignore';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_IGNORES = [
|
|
6
|
-
'node_modules',
|
|
7
|
-
'dist',
|
|
8
|
-
'build',
|
|
9
|
-
'.git',
|
|
10
|
-
'.next',
|
|
11
|
-
'.nuxt',
|
|
12
|
-
'coverage',
|
|
13
|
-
'.kountcache.json', // Our own cache file
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
interface IgnoreContext {
|
|
17
|
-
ig: Ignore;
|
|
18
|
-
dir: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Parses ignore files and builds a matcher set for a given directory tree.
|
|
23
|
-
* Respects default ignores automatically.
|
|
24
|
-
*/
|
|
25
|
-
export class IgnoreParser {
|
|
26
|
-
// Store ignore instances keyed by directory path
|
|
27
|
-
private directoryIgnores = new Map<string, Ignore>();
|
|
28
|
-
private rootDir: string;
|
|
29
|
-
private respectGitignore: boolean;
|
|
30
|
-
|
|
31
|
-
// Track binary extensions to filter out automatically
|
|
32
|
-
private static readonly BINARY_EXTENSIONS = new Set([
|
|
33
|
-
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp', '.tiff', '.bmp',
|
|
34
|
-
'.mp4', '.mkv', '.avi', '.mov', '.wmv', '.webm', '.flv',
|
|
35
|
-
'.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a',
|
|
36
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
37
|
-
'.zip', '.tar', '.gz', '.7z', '.rar', '.bz2', '.xz',
|
|
38
|
-
'.exe', '.dll', '.so', '.dylib', '.bin', '.dat', '.db', '.sqlite',
|
|
39
|
-
'.app', '.dmg', '.iso', '.class', '.jar', '.war', '.ear',
|
|
40
|
-
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
41
|
-
'.pyc', '.pyo', '.pyd', '.o', '.a', '.lib', '.out',
|
|
42
|
-
'.wasm'
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
constructor(rootDir: string, respectGitignore: boolean = true) {
|
|
46
|
-
this.rootDir = path.resolve(rootDir);
|
|
47
|
-
this.respectGitignore = respectGitignore;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Initializes the base ignores (defaults + root .kountignore/.gitignore).
|
|
52
|
-
* Call this before walking.
|
|
53
|
-
*/
|
|
54
|
-
async init(): Promise<void> {
|
|
55
|
-
const rootIg = ignore().add(DEFAULT_IGNORES);
|
|
56
|
-
await this.loadIgnoreFiles(this.rootDir, rootIg);
|
|
57
|
-
this.directoryIgnores.set(this.rootDir, rootIg);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Loads `.gitignore` or `.kountignore` inside a specific directory.
|
|
62
|
-
*/
|
|
63
|
-
async loadIgnoreForDir(dirPath: string): Promise<void> {
|
|
64
|
-
if (this.directoryIgnores.has(dirPath)) return;
|
|
65
|
-
|
|
66
|
-
// Inherit from parent directory (if any) or fallback to root if not loaded somehow
|
|
67
|
-
const parentDir = path.dirname(dirPath);
|
|
68
|
-
const parentIg = this.directoryIgnores.get(parentDir) || this.directoryIgnores.get(this.rootDir);
|
|
69
|
-
|
|
70
|
-
// Create a new instance starting with parent's rules. Wait, ignore doesn't deep clone easily.
|
|
71
|
-
// Instead, we just maintain a stack of ig instances during walk, but for class-level,
|
|
72
|
-
// we can create a fresh ignore and add parent's rules if we tracked them, OR just test
|
|
73
|
-
// a path against all ignores from root to current dir.
|
|
74
|
-
// For simplicity and speed: We will test a relative path against an array of ignores.
|
|
75
|
-
|
|
76
|
-
// Actually, ignore is designed to be used by resolving paths relative to where the .gitignore is.
|
|
77
|
-
// Let's store rules per directory.
|
|
78
|
-
const ig = ignore();
|
|
79
|
-
const hasRules = await this.loadIgnoreFiles(dirPath, ig);
|
|
80
|
-
|
|
81
|
-
// If no new rules, we don't strictly need to store it, but let's store undefined or similar
|
|
82
|
-
// to mark it processed.
|
|
83
|
-
if (hasRules) {
|
|
84
|
-
this.directoryIgnores.set(dirPath, ig);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Helper to append rules from files to an Ignore instance.
|
|
90
|
-
* Returns true if rules were added.
|
|
91
|
-
*/
|
|
92
|
-
private async loadIgnoreFiles(dirPath: string, ig: Ignore): Promise<boolean> {
|
|
93
|
-
let hasRules = false;
|
|
94
|
-
|
|
95
|
-
// Load .kountignore first (custom rules overrides everything else if needed, though ignore merges them)
|
|
96
|
-
try {
|
|
97
|
-
const kountIgnorePath = path.join(dirPath, '.kountignore');
|
|
98
|
-
const kountRules = await fs.readFile(kountIgnorePath, 'utf8');
|
|
99
|
-
ig.add(kountRules);
|
|
100
|
-
hasRules = true;
|
|
101
|
-
} catch (e) { /* ignores missing file */ }
|
|
102
|
-
|
|
103
|
-
// Load .gitignore
|
|
104
|
-
if (this.respectGitignore) {
|
|
105
|
-
try {
|
|
106
|
-
const gitIgnorePath = path.join(dirPath, '.gitignore');
|
|
107
|
-
const gitRules = await fs.readFile(gitIgnorePath, 'utf8');
|
|
108
|
-
ig.add(gitRules);
|
|
109
|
-
hasRules = true;
|
|
110
|
-
} catch (e) { /* ignores missing file */ }
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
return hasRules;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Checks if an absolute path should be ignored.
|
|
118
|
-
*/
|
|
119
|
-
isIgnored(absolutePath: string, isDirectory: boolean): boolean {
|
|
120
|
-
// 1. Binary check
|
|
121
|
-
if (!isDirectory) {
|
|
122
|
-
const ext = path.extname(absolutePath).toLowerCase();
|
|
123
|
-
if (IgnoreParser.BINARY_EXTENSIONS.has(ext)) {
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 2. Resolve relative to root to check against ignores
|
|
129
|
-
const relativeToRoot = path.relative(this.rootDir, absolutePath);
|
|
130
|
-
|
|
131
|
-
// Edge case: if it evaluates to empty string (which means absolutePath == rootDir), don't ignore
|
|
132
|
-
if (relativeToRoot === '') return false;
|
|
133
|
-
|
|
134
|
-
// We need to check the path formatted for `ignore` package (forward slashes)
|
|
135
|
-
// and if it's a directory, adding a trailing slash helps `ignore` match dir-only rules eagerly.
|
|
136
|
-
const posixPath = relativeToRoot.split(path.sep).join('/');
|
|
137
|
-
const testPath = isDirectory ? `${posixPath}/` : posixPath;
|
|
138
|
-
|
|
139
|
-
// 3. Test against root ignores first (which contains defaults)
|
|
140
|
-
const rootIg = this.directoryIgnores.get(this.rootDir);
|
|
141
|
-
if (rootIg && rootIg.ignores(testPath)) {
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// 4. Test against nested directory ignores (from root down to the file's dir)
|
|
146
|
-
// E.g. for src/components/Button.tsx, check src/ and src/components/
|
|
147
|
-
const parts = relativeToRoot.split(path.sep);
|
|
148
|
-
let currentPath = this.rootDir;
|
|
149
|
-
|
|
150
|
-
for (let i = 0; i < parts.length - 1; i++) { // -1 because we don't need to check in the file's own name as a dir
|
|
151
|
-
currentPath = path.join(currentPath, parts[i]);
|
|
152
|
-
const dirIg = this.directoryIgnores.get(currentPath);
|
|
153
|
-
|
|
154
|
-
if (dirIg) {
|
|
155
|
-
// The ignore package checks paths relative to where the .gitignore file is located.
|
|
156
|
-
const relToIgnoreFile = path.relative(currentPath, absolutePath);
|
|
157
|
-
const relPosixPath = relToIgnoreFile.split(path.sep).join('/');
|
|
158
|
-
const nestedTestPath = isDirectory ? `${relPosixPath}/` : relPosixPath;
|
|
159
|
-
|
|
160
|
-
if (dirIg.ignores(nestedTestPath)) {
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import fsp from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { IgnoreParser } from './ignore-parser.js';
|
|
5
|
-
|
|
6
|
-
export interface ScannedFile {
|
|
7
|
-
filePath: string;
|
|
8
|
-
size: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type FileChunkCallback = (chunk: Buffer, isLast: boolean) => void;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Recursively walks directories and streams files chunk-by-chunk.
|
|
15
|
-
* Respects .gitignore and built-in binary exclusions via IgnoreParser.
|
|
16
|
-
*/
|
|
17
|
-
export class Scanner {
|
|
18
|
-
private parser: IgnoreParser;
|
|
19
|
-
|
|
20
|
-
constructor(rootDir: string, respectGitignore: boolean = true) {
|
|
21
|
-
this.parser = new IgnoreParser(rootDir, respectGitignore);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Discovers all files in the directory tree matching rules.
|
|
26
|
-
*/
|
|
27
|
-
async discover(dirPath: string): Promise<ScannedFile[]> {
|
|
28
|
-
await this.parser.init();
|
|
29
|
-
return this.walk(path.resolve(dirPath));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private async walk(currentDir: string): Promise<ScannedFile[]> {
|
|
33
|
-
const filesList: ScannedFile[] = [];
|
|
34
|
-
|
|
35
|
-
// Provide ignore rules for this specific directory level
|
|
36
|
-
await this.parser.loadIgnoreForDir(currentDir);
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const entries = await fsp.readdir(currentDir, { withFileTypes: true });
|
|
40
|
-
|
|
41
|
-
for (const entry of entries) {
|
|
42
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
43
|
-
|
|
44
|
-
// Check ignore rules immediately to avoid stat calls on ignored paths
|
|
45
|
-
if (this.parser.isIgnored(fullPath, entry.isDirectory())) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (entry.isDirectory()) {
|
|
50
|
-
const subFiles = await this.walk(fullPath);
|
|
51
|
-
filesList.push(...subFiles);
|
|
52
|
-
} else if (entry.isFile()) {
|
|
53
|
-
try {
|
|
54
|
-
// We need the size, though we could get it via stats here or let the caller do it.
|
|
55
|
-
// We'll calculate it so plugins can use it.
|
|
56
|
-
const stats = await fsp.stat(fullPath);
|
|
57
|
-
filesList.push({ filePath: fullPath, size: stats.size });
|
|
58
|
-
} catch (e) {
|
|
59
|
-
// Handle gracefully: e.g. broken symlink or permission denied
|
|
60
|
-
// Could hook a logger here later.
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} catch (e) {
|
|
65
|
-
// e.g. Permission denied on directory
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return filesList;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Streams a file in chunks without reading the whole file into memory.
|
|
73
|
-
*/
|
|
74
|
-
streamFile(filePath: string, onChunk: FileChunkCallback): Promise<void> {
|
|
75
|
-
return new Promise((resolve, reject) => {
|
|
76
|
-
// High water mark set to a reasonable chunk size (e.g. 64KB) to keep memory low
|
|
77
|
-
const stream = fs.createReadStream(filePath, { highWaterMark: 64 * 1024 });
|
|
78
|
-
|
|
79
|
-
stream.on('data', (chunk: Buffer | string) => {
|
|
80
|
-
// Enforce Buffer
|
|
81
|
-
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
82
|
-
// We pass false for `isLast` because we don't know yet.
|
|
83
|
-
// We will signal `isLast` on 'end' if needed, but typically passing the chunk is enough.
|
|
84
|
-
// For line counting plugins relying on chunks, we might need a signal to flush remaining lines.
|
|
85
|
-
onChunk(bufferChunk, false);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
stream.on('error', (err) => {
|
|
89
|
-
reject(err);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
stream.on('end', () => {
|
|
93
|
-
// Send a final empty chunk with isLast = true so plugins can flush state.
|
|
94
|
-
onChunk(Buffer.alloc(0), true);
|
|
95
|
-
resolve();
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
package/src/utils/.gitkeep
DELETED
|
File without changes
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Maps file extensions to their corresponding comment syntaxes.
|
|
3
|
-
* This is used by the comment-lines plugin to accurately count comments.
|
|
4
|
-
*/
|
|
5
|
-
export const LANGUAGE_COMMENT_MAP: Record<string, string[]> = {
|
|
6
|
-
// C-style (JS, TS, Java, C, C++, C#, Go, Swift, Rust, Kotlin, Dart, Scala, Objective-C)
|
|
7
|
-
'.js': ['//', '/* */'],
|
|
8
|
-
'.ts': ['//', '/* */'],
|
|
9
|
-
'.jsx': ['//', '/* */'],
|
|
10
|
-
'.tsx': ['//', '/* */'],
|
|
11
|
-
'.java': ['//', '/* */'],
|
|
12
|
-
'.c': ['//', '/* */'],
|
|
13
|
-
'.cpp': ['//', '/* */'],
|
|
14
|
-
'.cs': ['//', '/* */'],
|
|
15
|
-
'.go': ['//', '/* */'],
|
|
16
|
-
'.swift': ['//', '/* */'],
|
|
17
|
-
'.rs': ['//', '/* */'],
|
|
18
|
-
'.kt': ['//', '/* */'],
|
|
19
|
-
'.dart': ['//', '/* */'],
|
|
20
|
-
'.scala': ['//', '/* */'],
|
|
21
|
-
'.m': ['//', '/* */'],
|
|
22
|
-
'.mm': ['//', '/* */'],
|
|
23
|
-
'.css': ['/* */'],
|
|
24
|
-
'.scss': ['//', '/* */'],
|
|
25
|
-
'.less': ['//', '/* */'],
|
|
26
|
-
|
|
27
|
-
// Hash-style (Python, Ruby, Shell, YAML, Perl, PHP (also uses //), R, PowerShell, Makefile)
|
|
28
|
-
'.py': ['#'],
|
|
29
|
-
'.rb': ['#'],
|
|
30
|
-
'.sh': ['#'],
|
|
31
|
-
'.bash': ['#'],
|
|
32
|
-
'.zsh': ['#'],
|
|
33
|
-
'.yaml': ['#'],
|
|
34
|
-
'.yml': ['#'],
|
|
35
|
-
'.pl': ['#'],
|
|
36
|
-
'.pm': ['#'],
|
|
37
|
-
'.r': ['#'],
|
|
38
|
-
'.ps1': ['#'],
|
|
39
|
-
'makefile': ['#'], // Note: Makefile might not have an extension, handle separately if needed
|
|
40
|
-
|
|
41
|
-
// HTML/XML-style (HTML, XML, SVG, Markdown, Vue)
|
|
42
|
-
'.html': ['<!-- -->'],
|
|
43
|
-
'.htm': ['<!-- -->'],
|
|
44
|
-
'.xml': ['<!-- -->'],
|
|
45
|
-
'.svg': ['<!-- -->'],
|
|
46
|
-
'.md': ['<!-- -->'],
|
|
47
|
-
'.vue': ['<!-- -->', '//', '/* */'], // Vue can have JS/TS inside <script> and CSS inside <style>
|
|
48
|
-
|
|
49
|
-
// Dash-style (SQL, Lua, Haskell, Ada, VHDL)
|
|
50
|
-
'.sql': ['--'],
|
|
51
|
-
'.lua': ['--'],
|
|
52
|
-
'.hs': ['--'],
|
|
53
|
-
'.ada': ['--'],
|
|
54
|
-
'.vhdl': ['--'],
|
|
55
|
-
|
|
56
|
-
// Lisp-style (Lisp, Clojure, Scheme)
|
|
57
|
-
'.lisp': [';'],
|
|
58
|
-
'.clj': [';'],
|
|
59
|
-
'.scm': [';'],
|
|
60
|
-
|
|
61
|
-
// PHP
|
|
62
|
-
'.php': ['//', '#', '/* */'],
|
|
63
|
-
|
|
64
|
-
// JSON (Standard JSON has no comments, but JSONC allowing comments is common in tools)
|
|
65
|
-
'.jsonc': ['//', '/* */'],
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Returns the expected comment syntax array for a given file extension.
|
|
70
|
-
* If the extension is not mapped, returns an empty array.
|
|
71
|
-
*
|
|
72
|
-
* @param extension The file extension including the dot (e.g., '.ts')
|
|
73
|
-
* @returns Array of comment syntaxes (e.g., ['//', '/* *\/']) or empty array if not found.
|
|
74
|
-
*/
|
|
75
|
-
export function getCommentSyntax(extension: string): string[] {
|
|
76
|
-
// Ensure the extension starts with a dot if it's not a known full filename like Makefile
|
|
77
|
-
const normalizedExt = extension.toLowerCase();
|
|
78
|
-
return LANGUAGE_COMMENT_MAP[normalizedExt] || [];
|
|
79
|
-
}
|