@cod3vil/kount-cli 1.0.0 → 1.0.3
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.js +44618 -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
package/package.json
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cod3vil/kount-cli",
|
|
3
3
|
"packageManager": "bun@1.3.5",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.3",
|
|
5
5
|
"description": "Project Intelligence for Codebases — analyze your code with precision.",
|
|
6
6
|
"module": "src/index.tsx",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
|
-
"kount": "
|
|
9
|
+
"kount": "dist/kount.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"
|
|
13
|
-
"bin",
|
|
12
|
+
"dist",
|
|
14
13
|
"README.md"
|
|
15
14
|
],
|
|
16
15
|
"publishConfig": {
|
|
@@ -21,7 +20,7 @@
|
|
|
21
20
|
"start": "bun run src/index.tsx",
|
|
22
21
|
"test": "vitest run",
|
|
23
22
|
"test:watch": "vitest",
|
|
24
|
-
"build": "bun build ./src/index.tsx --
|
|
23
|
+
"build": "rm -rf dist && bun build ./src/index.tsx --outfile dist/kount.js --target node",
|
|
25
24
|
"prepublishOnly": "bun run build",
|
|
26
25
|
"release": "np"
|
|
27
26
|
},
|
|
@@ -50,6 +49,7 @@
|
|
|
50
49
|
"@types/node": "^25.3.2",
|
|
51
50
|
"@types/react": "^19.2.14",
|
|
52
51
|
"np": "^11.0.2",
|
|
52
|
+
"react-devtools-core": "^7.0.1",
|
|
53
53
|
"vitest": "^4.0.18"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
package/bin/kount.js
DELETED
package/src/cli/.gitkeep
DELETED
|
File without changes
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import fsp from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolved configuration used throughout the application.
|
|
6
|
-
*/
|
|
7
|
-
export interface KountConfig {
|
|
8
|
-
rootDir: string;
|
|
9
|
-
outputMode: 'terminal' | 'markdown' | 'html';
|
|
10
|
-
includeTests: boolean;
|
|
11
|
-
respectGitignore: boolean;
|
|
12
|
-
cache: {
|
|
13
|
-
enabled: boolean;
|
|
14
|
-
clearFirst: boolean;
|
|
15
|
-
};
|
|
16
|
-
force: boolean;
|
|
17
|
-
outputPath?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Shape of the .kountrc.json / .kountrc.yaml config file.
|
|
22
|
-
*/
|
|
23
|
-
interface ConfigFile {
|
|
24
|
-
rootDir?: string;
|
|
25
|
-
outputMode?: string;
|
|
26
|
-
includeTests?: boolean;
|
|
27
|
-
respectGitignore?: boolean;
|
|
28
|
-
cache?: {
|
|
29
|
-
enabled?: boolean;
|
|
30
|
-
clearFirst?: boolean;
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* CLI flags that can override the config file.
|
|
36
|
-
*/
|
|
37
|
-
export interface CliFlags {
|
|
38
|
-
rootDir?: string;
|
|
39
|
-
outputMode?: string;
|
|
40
|
-
includeTests?: boolean;
|
|
41
|
-
respectGitignore?: boolean;
|
|
42
|
-
cache?: boolean;
|
|
43
|
-
clearCache?: boolean;
|
|
44
|
-
force?: boolean;
|
|
45
|
-
output?: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const DEFAULTS: KountConfig = {
|
|
49
|
-
rootDir: '.',
|
|
50
|
-
outputMode: 'terminal',
|
|
51
|
-
includeTests: false,
|
|
52
|
-
respectGitignore: true,
|
|
53
|
-
cache: {
|
|
54
|
-
enabled: true,
|
|
55
|
-
clearFirst: false,
|
|
56
|
-
},
|
|
57
|
-
force: false,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Attempts to load a config file from the given directory.
|
|
62
|
-
* Checks for .kountrc.json first, then .kountrc.yaml.
|
|
63
|
-
*/
|
|
64
|
-
async function loadConfigFile(dir: string): Promise<ConfigFile> {
|
|
65
|
-
const jsonPath = path.join(dir, '.kountrc.json');
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
const raw = await fsp.readFile(jsonPath, 'utf8');
|
|
69
|
-
return JSON.parse(raw) as ConfigFile;
|
|
70
|
-
} catch {
|
|
71
|
-
// JSON not found or invalid — try YAML
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// YAML support: we'll parse manually for the simple flat structure
|
|
75
|
-
// rather than adding a heavy dependency. This handles basic key: value pairs.
|
|
76
|
-
const yamlPath = path.join(dir, '.kountrc.yaml');
|
|
77
|
-
try {
|
|
78
|
-
const raw = await fsp.readFile(yamlPath, 'utf8');
|
|
79
|
-
return parseSimpleYaml(raw);
|
|
80
|
-
} catch {
|
|
81
|
-
// No config file found — use defaults
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Minimal YAML parser for flat config files.
|
|
89
|
-
* Handles: string, boolean, and nested single-level objects.
|
|
90
|
-
*/
|
|
91
|
-
function parseSimpleYaml(raw: string): ConfigFile {
|
|
92
|
-
const result: Record<string, unknown> = {};
|
|
93
|
-
let currentObject: Record<string, unknown> | null = null;
|
|
94
|
-
let currentKey = '';
|
|
95
|
-
|
|
96
|
-
for (const line of raw.split('\n')) {
|
|
97
|
-
const trimmed = line.trim();
|
|
98
|
-
if (trimmed === '' || trimmed.startsWith('#')) continue;
|
|
99
|
-
|
|
100
|
-
const indent = line.length - line.trimStart().length;
|
|
101
|
-
|
|
102
|
-
if (indent > 0 && currentObject !== null) {
|
|
103
|
-
// Nested key
|
|
104
|
-
const match = trimmed.match(/^(\w+)\s*:\s*(.+)$/);
|
|
105
|
-
if (match) {
|
|
106
|
-
currentObject[match[1]] = parseYamlValue(match[2]);
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
// Top-level key
|
|
110
|
-
const match = trimmed.match(/^(\w+)\s*:\s*(.*)$/);
|
|
111
|
-
if (match) {
|
|
112
|
-
const key = match[1];
|
|
113
|
-
const value = match[2].trim();
|
|
114
|
-
|
|
115
|
-
if (value === '') {
|
|
116
|
-
// Start of nested object
|
|
117
|
-
currentKey = key;
|
|
118
|
-
currentObject = {};
|
|
119
|
-
result[key] = currentObject;
|
|
120
|
-
} else {
|
|
121
|
-
currentObject = null;
|
|
122
|
-
result[key] = parseYamlValue(value);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return result as unknown as ConfigFile;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function parseYamlValue(val: string): string | boolean | number {
|
|
132
|
-
if (val === 'true') return true;
|
|
133
|
-
if (val === 'false') return false;
|
|
134
|
-
const num = Number(val);
|
|
135
|
-
if (!isNaN(num) && val !== '') return num;
|
|
136
|
-
// Strip quotes
|
|
137
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
138
|
-
return val.slice(1, -1);
|
|
139
|
-
}
|
|
140
|
-
return val;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Resolves the final configuration from:
|
|
145
|
-
* CLI flags > Config file > Defaults
|
|
146
|
-
*
|
|
147
|
-
* @param cliFlags Parsed CLI arguments.
|
|
148
|
-
* @param cwd Current working directory (for finding config files).
|
|
149
|
-
*/
|
|
150
|
-
export async function resolveConfig(cliFlags: CliFlags, cwd: string = process.cwd()): Promise<KountConfig> {
|
|
151
|
-
const fileConfig = await loadConfigFile(cwd);
|
|
152
|
-
|
|
153
|
-
const outputMode = validateOutputMode(
|
|
154
|
-
cliFlags.outputMode ?? fileConfig.outputMode ?? DEFAULTS.outputMode
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
rootDir: path.resolve(cwd, cliFlags.rootDir ?? fileConfig.rootDir ?? DEFAULTS.rootDir),
|
|
159
|
-
outputMode,
|
|
160
|
-
includeTests: cliFlags.includeTests ?? fileConfig.includeTests ?? DEFAULTS.includeTests,
|
|
161
|
-
respectGitignore: cliFlags.respectGitignore ?? fileConfig.respectGitignore ?? DEFAULTS.respectGitignore,
|
|
162
|
-
cache: {
|
|
163
|
-
enabled: cliFlags.cache ?? fileConfig.cache?.enabled ?? DEFAULTS.cache.enabled,
|
|
164
|
-
clearFirst: cliFlags.clearCache ?? fileConfig.cache?.clearFirst ?? DEFAULTS.cache.clearFirst,
|
|
165
|
-
},
|
|
166
|
-
force: cliFlags.force ?? DEFAULTS.force,
|
|
167
|
-
outputPath: cliFlags.output,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function validateOutputMode(mode: string): 'terminal' | 'markdown' | 'html' {
|
|
172
|
-
const valid = ['terminal', 'markdown', 'html'];
|
|
173
|
-
if (valid.includes(mode)) return mode as 'terminal' | 'markdown' | 'html';
|
|
174
|
-
return 'terminal';
|
|
175
|
-
}
|
package/src/cli/parser.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import type { CliFlags } from './config-resolver.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates and configures the Commander CLI program.
|
|
6
|
-
* Returns parsed CLI flags for the config resolver.
|
|
7
|
-
*
|
|
8
|
-
* Follows tuicfg-sensible-defaults, tuicfg-flags-over-args,
|
|
9
|
-
* and tuicfg-help-system guidelines from terminal-ui skill.
|
|
10
|
-
*/
|
|
11
|
-
export function createCli(argv: string[]): CliFlags {
|
|
12
|
-
const program = new Command();
|
|
13
|
-
|
|
14
|
-
program
|
|
15
|
-
.name('kount')
|
|
16
|
-
.description('Project Intelligence for Codebases — analyze your code with precision.')
|
|
17
|
-
.version('1.0.0')
|
|
18
|
-
.option('-d, --root-dir <path>', 'Root directory to scan (default: current directory)')
|
|
19
|
-
.option(
|
|
20
|
-
'-o, --output-mode <mode>',
|
|
21
|
-
'Output mode: terminal, markdown, or html (default: terminal)'
|
|
22
|
-
)
|
|
23
|
-
.option('-t, --include-tests', 'Include test files in the analysis')
|
|
24
|
-
.option('--no-gitignore', 'Ignore .gitignore rules')
|
|
25
|
-
.option('--no-cache', 'Disable caching')
|
|
26
|
-
.option('--clear-cache', 'Clear the cache before scanning')
|
|
27
|
-
.option('-f, --force', 'Force overwrite output files (for markdown mode)')
|
|
28
|
-
.option('--output <path>', 'Output file path (for markdown mode)')
|
|
29
|
-
.parse(argv);
|
|
30
|
-
|
|
31
|
-
const opts = program.opts<{
|
|
32
|
-
rootDir?: string;
|
|
33
|
-
outputMode?: string;
|
|
34
|
-
includeTests?: boolean;
|
|
35
|
-
gitignore?: boolean;
|
|
36
|
-
cache?: boolean;
|
|
37
|
-
clearCache?: boolean;
|
|
38
|
-
force?: boolean;
|
|
39
|
-
output?: string;
|
|
40
|
-
}>();
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
rootDir: opts.rootDir,
|
|
44
|
-
outputMode: opts.outputMode,
|
|
45
|
-
includeTests: opts.includeTests,
|
|
46
|
-
respectGitignore: opts.gitignore, // Commander converts --no-gitignore to gitignore: false
|
|
47
|
-
cache: opts.cache, // Commander converts --no-cache to cache: false
|
|
48
|
-
clearCache: opts.clearCache,
|
|
49
|
-
force: opts.force,
|
|
50
|
-
output: opts.output,
|
|
51
|
-
};
|
|
52
|
-
}
|
package/src/core/.gitkeep
DELETED
|
File without changes
|
package/src/core/aggregator.ts
DELETED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import fsp from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
BlankLinesPlugin,
|
|
5
|
-
CommentLinesPlugin,
|
|
6
|
-
FileSizePlugin,
|
|
7
|
-
LanguageDistributionPlugin,
|
|
8
|
-
LargestFilesPlugin,
|
|
9
|
-
TotalFilesPlugin,
|
|
10
|
-
TotalLinesPlugin,
|
|
11
|
-
} from '../plugins/index.js';
|
|
12
|
-
import type { AnalyzedFileData, AnalyzerPlugin, PluginResult, ProjectStats } from '../plugins/types.js';
|
|
13
|
-
import type { ScannedFile } from '../scanner/stream-reader.js';
|
|
14
|
-
import { Scanner } from '../scanner/stream-reader.js';
|
|
15
|
-
import { CacheManager } from './cache.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Default set of built-in v1 plugins.
|
|
19
|
-
*/
|
|
20
|
-
function getDefaultPlugins(): AnalyzerPlugin[] {
|
|
21
|
-
return [
|
|
22
|
-
new TotalLinesPlugin(),
|
|
23
|
-
new BlankLinesPlugin(),
|
|
24
|
-
new CommentLinesPlugin(),
|
|
25
|
-
new FileSizePlugin(),
|
|
26
|
-
new TotalFilesPlugin(),
|
|
27
|
-
new LanguageDistributionPlugin(),
|
|
28
|
-
new LargestFilesPlugin(),
|
|
29
|
-
];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface AggregatorOptions {
|
|
33
|
-
respectGitignore?: boolean;
|
|
34
|
-
plugins?: AnalyzerPlugin[];
|
|
35
|
-
cacheEnabled?: boolean;
|
|
36
|
-
clearCache?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Orchestrator: connects the Scanner to the Plugin pipeline.
|
|
41
|
-
* Streams each file, builds AnalyzedFileData, then runs all plugins.
|
|
42
|
-
* Integrates the CacheManager to skip unchanged files.
|
|
43
|
-
*/
|
|
44
|
-
export class Aggregator {
|
|
45
|
-
private scanner: Scanner;
|
|
46
|
-
private plugins: AnalyzerPlugin[];
|
|
47
|
-
private rootDir: string;
|
|
48
|
-
private cache: CacheManager;
|
|
49
|
-
private clearCacheFirst: boolean;
|
|
50
|
-
|
|
51
|
-
constructor(rootDir: string, options?: AggregatorOptions) {
|
|
52
|
-
this.rootDir = path.resolve(rootDir);
|
|
53
|
-
this.scanner = new Scanner(this.rootDir, options?.respectGitignore ?? true);
|
|
54
|
-
this.plugins = options?.plugins ?? getDefaultPlugins();
|
|
55
|
-
this.cache = new CacheManager(this.rootDir, options?.cacheEnabled ?? true);
|
|
56
|
-
this.clearCacheFirst = options?.clearCache ?? false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Runs the full pipeline: discover → cache check → stream → analyze → aggregate → save cache.
|
|
61
|
-
* Returns a ProjectStats payload ready for reporters.
|
|
62
|
-
*
|
|
63
|
-
* @param onProgress Optional callback fired per file for progress tracking.
|
|
64
|
-
*/
|
|
65
|
-
async run(onProgress?: (current: number, total: number, filePath: string) => void): Promise<ProjectStats> {
|
|
66
|
-
// 0. Cache setup
|
|
67
|
-
if (this.clearCacheFirst) {
|
|
68
|
-
await this.cache.clear();
|
|
69
|
-
}
|
|
70
|
-
await this.cache.load();
|
|
71
|
-
|
|
72
|
-
// 1. Discover files
|
|
73
|
-
const scannedFiles = await this.scanner.discover(this.rootDir);
|
|
74
|
-
|
|
75
|
-
// 2. Stream each file (or use cache) and build enriched data
|
|
76
|
-
const analyzedFiles: AnalyzedFileData[] = [];
|
|
77
|
-
let current = 0;
|
|
78
|
-
|
|
79
|
-
for (const scannedFile of scannedFiles) {
|
|
80
|
-
const fileData = await this.processFile(scannedFile);
|
|
81
|
-
analyzedFiles.push(fileData);
|
|
82
|
-
current++;
|
|
83
|
-
|
|
84
|
-
if (onProgress) {
|
|
85
|
-
onProgress(current, scannedFiles.length, scannedFile.filePath);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 3. Run each plugin against the full analyzed data
|
|
90
|
-
const pluginResults = new Map<string, PluginResult>();
|
|
91
|
-
for (const plugin of this.plugins) {
|
|
92
|
-
const result = plugin.analyze(analyzedFiles);
|
|
93
|
-
pluginResults.set(plugin.name, result);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 4. Update cache with fresh per-file metrics from plugins
|
|
97
|
-
for (const file of analyzedFiles) {
|
|
98
|
-
const metrics: Record<string, number> = {};
|
|
99
|
-
for (const [pluginName, result] of pluginResults) {
|
|
100
|
-
const value = result.perFile.get(file.filePath);
|
|
101
|
-
if (value !== undefined) {
|
|
102
|
-
metrics[pluginName] = value;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
const stat = await fsp.stat(file.filePath);
|
|
108
|
-
this.cache.set(file.filePath, stat.mtimeMs, stat.size, metrics);
|
|
109
|
-
} catch {
|
|
110
|
-
// File may have been deleted between discover and cache save — skip
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 5. Save cache to disk
|
|
115
|
-
await this.cache.save();
|
|
116
|
-
|
|
117
|
-
// 6. Compute language distribution
|
|
118
|
-
const langPlugin = this.plugins.find(
|
|
119
|
-
(p): p is LanguageDistributionPlugin => p.name === 'LanguageDistribution'
|
|
120
|
-
) as LanguageDistributionPlugin | undefined;
|
|
121
|
-
const languageDistribution = langPlugin
|
|
122
|
-
? langPlugin.getDistribution(analyzedFiles)
|
|
123
|
-
: new Map<string, number>();
|
|
124
|
-
|
|
125
|
-
// 7. Compute largest files
|
|
126
|
-
const largestPlugin = this.plugins.find(
|
|
127
|
-
(p): p is LargestFilesPlugin => p.name === 'LargestFiles'
|
|
128
|
-
) as LargestFilesPlugin | undefined;
|
|
129
|
-
const largestFiles = largestPlugin
|
|
130
|
-
? largestPlugin.getTopFiles(analyzedFiles)
|
|
131
|
-
: [];
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
rootDir: this.rootDir,
|
|
135
|
-
totalFiles: scannedFiles.length,
|
|
136
|
-
pluginResults,
|
|
137
|
-
languageDistribution,
|
|
138
|
-
largestFiles,
|
|
139
|
-
scannedAt: new Date(),
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Processes a single file: checks cache first, streams if cache miss.
|
|
145
|
-
*/
|
|
146
|
-
private async processFile(scannedFile: ScannedFile): Promise<AnalyzedFileData> {
|
|
147
|
-
// Check cache (we need the current stat to compare)
|
|
148
|
-
try {
|
|
149
|
-
const stat = await fsp.stat(scannedFile.filePath);
|
|
150
|
-
const cached = this.cache.lookup(scannedFile.filePath, stat.mtimeMs, stat.size);
|
|
151
|
-
|
|
152
|
-
if (cached !== null) {
|
|
153
|
-
// Cache hit — we still need AnalyzedFileData for the plugins.
|
|
154
|
-
// Since plugins need `lines`, we need to re-stream on cache miss.
|
|
155
|
-
// For cache hits, we can't avoid re-reading if plugins need full line data.
|
|
156
|
-
// However the cache stores per-file metrics directly, so for a future
|
|
157
|
-
// optimization we could bypass plugins entirely. For now, we still stream
|
|
158
|
-
// but the cache mechanism is in place for the next optimization pass.
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
// stat failed — proceed with streaming
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return this.streamAndParse(scannedFile);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Streams a single file and collects its lines.
|
|
169
|
-
* Uses the Scanner's streaming API to avoid loading the entire file at once.
|
|
170
|
-
*/
|
|
171
|
-
private async streamAndParse(scannedFile: ScannedFile): Promise<AnalyzedFileData> {
|
|
172
|
-
const lines: string[] = [];
|
|
173
|
-
let remainder = '';
|
|
174
|
-
|
|
175
|
-
await this.scanner.streamFile(scannedFile.filePath, (chunk, isLast) => {
|
|
176
|
-
if (chunk.length === 0 && isLast) {
|
|
177
|
-
// Flush any remaining partial line
|
|
178
|
-
if (remainder.length > 0) {
|
|
179
|
-
lines.push(remainder);
|
|
180
|
-
remainder = '';
|
|
181
|
-
}
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const text = remainder + chunk.toString('utf8');
|
|
186
|
-
const parts = text.split('\n');
|
|
187
|
-
|
|
188
|
-
// All complete lines (everything except the last element which may be partial)
|
|
189
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
190
|
-
lines.push(parts[i]);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Keep the last part as remainder (may be partial line)
|
|
194
|
-
remainder = parts[parts.length - 1];
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
return {
|
|
198
|
-
filePath: scannedFile.filePath,
|
|
199
|
-
size: scannedFile.size,
|
|
200
|
-
extension: path.extname(scannedFile.filePath),
|
|
201
|
-
lines,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
}
|
package/src/core/cache.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import fsp from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Per-file cached entry storing metrics and invalidation keys.
|
|
6
|
-
*/
|
|
7
|
-
export interface CacheEntry {
|
|
8
|
-
/** Last modified time in ms (from stat.mtimeMs). */
|
|
9
|
-
mtimeMs: number;
|
|
10
|
-
/** File size in bytes (from stat.size). */
|
|
11
|
-
size: number;
|
|
12
|
-
/** Cached plugin results keyed by plugin name. */
|
|
13
|
-
metrics: Record<string, number>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Shape of the .kountcache.json file on disk.
|
|
18
|
-
*/
|
|
19
|
-
interface CacheFile {
|
|
20
|
-
version: number;
|
|
21
|
-
entries: Record<string, CacheEntry>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const CACHE_VERSION = 1;
|
|
25
|
-
const CACHE_FILENAME = '.kountcache.json';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Manages the .kountcache.json file for incremental scanning.
|
|
29
|
-
* Uses mtime + size to determine if a file needs re-scanning.
|
|
30
|
-
*/
|
|
31
|
-
export class CacheManager {
|
|
32
|
-
private cachePath: string;
|
|
33
|
-
private entries: Map<string, CacheEntry> = new Map();
|
|
34
|
-
private enabled: boolean;
|
|
35
|
-
private dirty = false;
|
|
36
|
-
|
|
37
|
-
constructor(rootDir: string, enabled: boolean = true) {
|
|
38
|
-
this.cachePath = path.join(path.resolve(rootDir), CACHE_FILENAME);
|
|
39
|
-
this.enabled = enabled;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Loads the cache from disk. If it doesn't exist or is corrupt, starts fresh.
|
|
44
|
-
*/
|
|
45
|
-
async load(): Promise<void> {
|
|
46
|
-
if (!this.enabled) return;
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
const raw = await fsp.readFile(this.cachePath, 'utf8');
|
|
50
|
-
const parsed: CacheFile = JSON.parse(raw);
|
|
51
|
-
|
|
52
|
-
if (parsed.version !== CACHE_VERSION) {
|
|
53
|
-
// Version mismatch — discard and start fresh
|
|
54
|
-
this.entries = new Map();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
this.entries = new Map(Object.entries(parsed.entries));
|
|
59
|
-
} catch {
|
|
60
|
-
// File doesn't exist or is corrupt — start with empty cache
|
|
61
|
-
this.entries = new Map();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Checks whether a file's cached entry is still valid by comparing
|
|
67
|
-
* mtime and size from the current stat against the stored values.
|
|
68
|
-
*
|
|
69
|
-
* @returns The cached metrics if valid, or null if the file needs re-scanning.
|
|
70
|
-
*/
|
|
71
|
-
lookup(filePath: string, currentMtimeMs: number, currentSize: number): Record<string, number> | null {
|
|
72
|
-
if (!this.enabled) return null;
|
|
73
|
-
|
|
74
|
-
const entry = this.entries.get(filePath);
|
|
75
|
-
if (!entry) return null;
|
|
76
|
-
|
|
77
|
-
if (entry.mtimeMs === currentMtimeMs && entry.size === currentSize) {
|
|
78
|
-
return entry.metrics;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Invalidated — mtime or size changed
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Stores or updates a file's cache entry after scanning.
|
|
87
|
-
*/
|
|
88
|
-
set(filePath: string, mtimeMs: number, size: number, metrics: Record<string, number>): void {
|
|
89
|
-
if (!this.enabled) return;
|
|
90
|
-
|
|
91
|
-
this.entries.set(filePath, { mtimeMs, size, metrics });
|
|
92
|
-
this.dirty = true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Persists the cache to disk if any entries were updated.
|
|
97
|
-
*/
|
|
98
|
-
async save(): Promise<void> {
|
|
99
|
-
if (!this.enabled || !this.dirty) return;
|
|
100
|
-
|
|
101
|
-
const cacheFile: CacheFile = {
|
|
102
|
-
version: CACHE_VERSION,
|
|
103
|
-
entries: Object.fromEntries(this.entries),
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
await fsp.writeFile(this.cachePath, JSON.stringify(cacheFile, null, 2), 'utf8');
|
|
107
|
-
this.dirty = false;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Removes the cache file from disk.
|
|
112
|
-
*/
|
|
113
|
-
async clear(): Promise<void> {
|
|
114
|
-
this.entries = new Map();
|
|
115
|
-
this.dirty = false;
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
await fsp.unlink(this.cachePath);
|
|
119
|
-
} catch {
|
|
120
|
-
// File didn't exist — that's fine
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Returns the number of cached entries (for diagnostics).
|
|
126
|
-
*/
|
|
127
|
-
get size(): number {
|
|
128
|
-
return this.entries.size;
|
|
129
|
-
}
|
|
130
|
-
}
|