tokenlean 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/README.md +248 -0
- package/bin/tl-api.mjs +515 -0
- package/bin/tl-blame.mjs +345 -0
- package/bin/tl-complexity.mjs +514 -0
- package/bin/tl-component.mjs +274 -0
- package/bin/tl-config.mjs +135 -0
- package/bin/tl-context.mjs +156 -0
- package/bin/tl-coverage.mjs +456 -0
- package/bin/tl-deps.mjs +474 -0
- package/bin/tl-diff.mjs +183 -0
- package/bin/tl-entry.mjs +256 -0
- package/bin/tl-env.mjs +376 -0
- package/bin/tl-exports.mjs +583 -0
- package/bin/tl-flow.mjs +324 -0
- package/bin/tl-history.mjs +289 -0
- package/bin/tl-hotspots.mjs +321 -0
- package/bin/tl-impact.mjs +345 -0
- package/bin/tl-prompt.mjs +175 -0
- package/bin/tl-related.mjs +227 -0
- package/bin/tl-routes.mjs +627 -0
- package/bin/tl-search.mjs +123 -0
- package/bin/tl-structure.mjs +161 -0
- package/bin/tl-symbols.mjs +430 -0
- package/bin/tl-todo.mjs +341 -0
- package/bin/tl-types.mjs +441 -0
- package/bin/tl-unused.mjs +494 -0
- package/package.json +55 -0
- package/src/config.mjs +271 -0
- package/src/output.mjs +251 -0
- package/src/project.mjs +277 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-component - React component analyzer
|
|
5
|
+
*
|
|
6
|
+
* Analyzes a React component to show props, hooks, dependencies,
|
|
7
|
+
* and structure without reading the full file.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-component <file.tsx>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-component',
|
|
16
|
+
desc: 'Analyze React component (props, hooks, imports)',
|
|
17
|
+
when: 'before-read',
|
|
18
|
+
example: 'tl-component src/Button.tsx'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync } from 'fs';
|
|
24
|
+
import { join, relative, dirname, basename } from 'path';
|
|
25
|
+
|
|
26
|
+
function findProjectRoot() {
|
|
27
|
+
let dir = process.cwd();
|
|
28
|
+
while (dir !== '/') {
|
|
29
|
+
if (existsSync(join(dir, 'package.json'))) return dir;
|
|
30
|
+
dir = dirname(dir);
|
|
31
|
+
}
|
|
32
|
+
return process.cwd();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractImports(content) {
|
|
36
|
+
const imports = {
|
|
37
|
+
react: [],
|
|
38
|
+
reactNative: [],
|
|
39
|
+
internal: [],
|
|
40
|
+
external: [],
|
|
41
|
+
types: []
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const importRegex = /import\s+(?:{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
|
|
45
|
+
let match;
|
|
46
|
+
|
|
47
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
48
|
+
const names = (match[1] || match[2]).split(',').map(s => s.trim());
|
|
49
|
+
const source = match[3];
|
|
50
|
+
|
|
51
|
+
if (source === 'react') {
|
|
52
|
+
imports.react.push(...names);
|
|
53
|
+
} else if (source.startsWith('react-native')) {
|
|
54
|
+
imports.reactNative.push(...names);
|
|
55
|
+
} else if (source.startsWith('.') || source.startsWith('@/')) {
|
|
56
|
+
imports.internal.push({ names, source });
|
|
57
|
+
} else if (source.includes('/types') || names.some(n => n.startsWith('type ') || /^[A-Z].*Props$/.test(n))) {
|
|
58
|
+
imports.types.push({ names, source });
|
|
59
|
+
} else {
|
|
60
|
+
imports.external.push({ names, source });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return imports;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractHooks(content) {
|
|
68
|
+
const hooks = [];
|
|
69
|
+
const hookRegex = /\b(use[A-Z]\w+)\s*\(/g;
|
|
70
|
+
let match;
|
|
71
|
+
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
while ((match = hookRegex.exec(content)) !== null) {
|
|
74
|
+
if (!seen.has(match[1])) {
|
|
75
|
+
seen.add(match[1]);
|
|
76
|
+
hooks.push(match[1]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return hooks;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractProps(content) {
|
|
84
|
+
// Look for Props interface/type
|
|
85
|
+
const propsMatch = content.match(/(?:interface|type)\s+(\w*Props)\s*(?:=\s*)?{([^}]+)}/);
|
|
86
|
+
if (!propsMatch) return null;
|
|
87
|
+
|
|
88
|
+
const name = propsMatch[1];
|
|
89
|
+
const body = propsMatch[2];
|
|
90
|
+
|
|
91
|
+
const props = [];
|
|
92
|
+
const propRegex = /(\w+)(\?)?:\s*([^;]+)/g;
|
|
93
|
+
let match;
|
|
94
|
+
|
|
95
|
+
while ((match = propRegex.exec(body)) !== null) {
|
|
96
|
+
props.push({
|
|
97
|
+
name: match[1],
|
|
98
|
+
optional: !!match[2],
|
|
99
|
+
type: match[3].trim()
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { name, props };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function extractComponents(content) {
|
|
107
|
+
const components = [];
|
|
108
|
+
|
|
109
|
+
// Function components
|
|
110
|
+
const funcRegex = /(?:export\s+)?(?:const|function)\s+(\w+)\s*(?::\s*React\.FC)?[^=]*=?\s*(?:\([^)]*\)|[^=])\s*(?:=>|{)/g;
|
|
111
|
+
let match;
|
|
112
|
+
|
|
113
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
114
|
+
const name = match[1];
|
|
115
|
+
// Check if it looks like a component (PascalCase, returns JSX)
|
|
116
|
+
if (/^[A-Z]/.test(name) && !name.endsWith('Props') && !name.endsWith('Type')) {
|
|
117
|
+
components.push(name);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [...new Set(components)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractStyles(content) {
|
|
125
|
+
const styles = [];
|
|
126
|
+
|
|
127
|
+
// StyleSheet.create
|
|
128
|
+
if (content.includes('StyleSheet.create')) {
|
|
129
|
+
styles.push('StyleSheet');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// styled-components / emotion
|
|
133
|
+
if (content.includes('styled.') || content.includes('styled(')) {
|
|
134
|
+
styles.push('styled-components');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Tailwind / NativeWind
|
|
138
|
+
if (content.includes('className=') || content.includes('tw`')) {
|
|
139
|
+
styles.push('Tailwind/NativeWind');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Inline styles
|
|
143
|
+
if (content.match(/style=\{\{/)) {
|
|
144
|
+
styles.push('inline styles');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return styles;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractRedux(content) {
|
|
151
|
+
const redux = {
|
|
152
|
+
selectors: [],
|
|
153
|
+
actions: [],
|
|
154
|
+
dispatch: false
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// useSelector calls
|
|
158
|
+
const selectorRegex = /useSelector\(\s*(?:\([^)]*\)\s*=>)?\s*(\w+)/g;
|
|
159
|
+
let match;
|
|
160
|
+
while ((match = selectorRegex.exec(content)) !== null) {
|
|
161
|
+
redux.selectors.push(match[1]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// useDispatch
|
|
165
|
+
if (content.includes('useDispatch')) {
|
|
166
|
+
redux.dispatch = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// dispatch calls
|
|
170
|
+
const dispatchRegex = /dispatch\(\s*(\w+)/g;
|
|
171
|
+
while ((match = dispatchRegex.exec(content)) !== null) {
|
|
172
|
+
redux.actions.push(match[1]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return redux;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function printAnalysis(analysis) {
|
|
179
|
+
const { file, lines, tokens, imports, hooks, propsInfo, components, styles, redux } = analysis;
|
|
180
|
+
|
|
181
|
+
console.log(`\n🧩 Component Analysis: ${file}`);
|
|
182
|
+
console.log(` ${lines} lines, ~${tokens} tokens\n`);
|
|
183
|
+
|
|
184
|
+
// Components
|
|
185
|
+
if (components.length > 0) {
|
|
186
|
+
console.log(`📦 Components: ${components.join(', ')}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Props
|
|
190
|
+
if (propsInfo) {
|
|
191
|
+
console.log(`\n📋 ${propsInfo.name}:`);
|
|
192
|
+
for (const p of propsInfo.props) {
|
|
193
|
+
const opt = p.optional ? '?' : '';
|
|
194
|
+
console.log(` ${p.name}${opt}: ${p.type}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Hooks
|
|
199
|
+
if (hooks.length > 0) {
|
|
200
|
+
console.log(`\n🪝 Hooks: ${hooks.join(', ')}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Redux
|
|
204
|
+
if (redux.dispatch || redux.selectors.length > 0) {
|
|
205
|
+
console.log(`\n📦 Redux:`);
|
|
206
|
+
if (redux.selectors.length > 0) {
|
|
207
|
+
console.log(` Selectors: ${redux.selectors.join(', ')}`);
|
|
208
|
+
}
|
|
209
|
+
if (redux.actions.length > 0) {
|
|
210
|
+
console.log(` Actions: ${redux.actions.join(', ')}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Imports summary
|
|
215
|
+
console.log(`\n📥 Imports:`);
|
|
216
|
+
if (imports.react.length > 0) {
|
|
217
|
+
console.log(` React: ${imports.react.join(', ')}`);
|
|
218
|
+
}
|
|
219
|
+
if (imports.reactNative.length > 0) {
|
|
220
|
+
console.log(` React Native: ${imports.reactNative.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
if (imports.internal.length > 0) {
|
|
223
|
+
console.log(` Internal: ${imports.internal.length} modules`);
|
|
224
|
+
for (const i of imports.internal.slice(0, 5)) {
|
|
225
|
+
console.log(` ${i.source}`);
|
|
226
|
+
}
|
|
227
|
+
if (imports.internal.length > 5) {
|
|
228
|
+
console.log(` ... and ${imports.internal.length - 5} more`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (imports.external.length > 0) {
|
|
232
|
+
console.log(` External: ${imports.external.map(i => i.source).join(', ')}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Styles
|
|
236
|
+
if (styles.length > 0) {
|
|
237
|
+
console.log(`\n🎨 Styling: ${styles.join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Main
|
|
244
|
+
const args = process.argv.slice(2);
|
|
245
|
+
const targetFile = args[0];
|
|
246
|
+
|
|
247
|
+
if (!targetFile) {
|
|
248
|
+
console.log('\nUsage: claude-component <file.tsx>\n');
|
|
249
|
+
console.log('Analyzes a React component to show props, hooks, and dependencies.');
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const fullPath = targetFile.startsWith('/') ? targetFile : join(process.cwd(), targetFile);
|
|
254
|
+
if (!existsSync(fullPath)) {
|
|
255
|
+
console.error(`File not found: ${targetFile}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
260
|
+
const projectRoot = findProjectRoot();
|
|
261
|
+
|
|
262
|
+
const analysis = {
|
|
263
|
+
file: relative(projectRoot, fullPath),
|
|
264
|
+
lines: content.split('\n').length,
|
|
265
|
+
tokens: Math.ceil(content.length / 4),
|
|
266
|
+
imports: extractImports(content),
|
|
267
|
+
hooks: extractHooks(content),
|
|
268
|
+
propsInfo: extractProps(content),
|
|
269
|
+
components: extractComponents(content),
|
|
270
|
+
styles: extractStyles(content),
|
|
271
|
+
redux: extractRedux(content)
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
printAnalysis(analysis);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-config - Show and manage tokenlean configuration
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* tl-config Show current merged config
|
|
8
|
+
* tl-config --paths Show config file locations
|
|
9
|
+
* tl-config --init Create a sample config file
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt (tl-config is a utility, not for AI agents)
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
process.exit(0); // No output - utility command
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
import { writeFileSync, existsSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import {
|
|
20
|
+
loadConfig,
|
|
21
|
+
getConfigPaths,
|
|
22
|
+
CONFIG_FILENAME,
|
|
23
|
+
GLOBAL_CONFIG_PATH
|
|
24
|
+
} from '../src/config.mjs';
|
|
25
|
+
|
|
26
|
+
const SAMPLE_CONFIG = {
|
|
27
|
+
output: {
|
|
28
|
+
maxLines: 100,
|
|
29
|
+
maxTokens: null,
|
|
30
|
+
format: 'text'
|
|
31
|
+
},
|
|
32
|
+
skipDirs: [],
|
|
33
|
+
skipExtensions: [],
|
|
34
|
+
importantDirs: [],
|
|
35
|
+
importantFiles: [],
|
|
36
|
+
searchPatterns: {
|
|
37
|
+
hooks: {
|
|
38
|
+
description: 'Find React hooks',
|
|
39
|
+
pattern: 'use[A-Z]\\w+',
|
|
40
|
+
glob: '**/*.{ts,tsx,js,jsx}'
|
|
41
|
+
},
|
|
42
|
+
todos: {
|
|
43
|
+
description: 'Find TODO comments',
|
|
44
|
+
pattern: 'TODO|FIXME|HACK',
|
|
45
|
+
glob: '**/*.{ts,tsx,js,jsx,mjs}'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
hotspots: {
|
|
49
|
+
days: 90,
|
|
50
|
+
top: 20
|
|
51
|
+
},
|
|
52
|
+
structure: {
|
|
53
|
+
depth: 3
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const HELP = `
|
|
58
|
+
tl-config - Show and manage tokenlean configuration
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
tl-config Show current merged config
|
|
62
|
+
tl-config --paths Show config file locations
|
|
63
|
+
tl-config --init Create a sample project config
|
|
64
|
+
tl-config --init-global Create a sample global config
|
|
65
|
+
|
|
66
|
+
Config file locations:
|
|
67
|
+
Project: ${CONFIG_FILENAME} (in project root or parent directories)
|
|
68
|
+
Global: ${GLOBAL_CONFIG_PATH}
|
|
69
|
+
|
|
70
|
+
Project config overrides global config.
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
const args = process.argv.slice(2);
|
|
74
|
+
|
|
75
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
76
|
+
console.log(HELP);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (args.includes('--paths')) {
|
|
81
|
+
const paths = getConfigPaths();
|
|
82
|
+
console.log('\nConfig file locations:\n');
|
|
83
|
+
|
|
84
|
+
if (paths.length === 0) {
|
|
85
|
+
console.log(' No config files found.');
|
|
86
|
+
console.log(`\n Create one with: tl-config --init`);
|
|
87
|
+
} else {
|
|
88
|
+
for (const { type, path } of paths) {
|
|
89
|
+
console.log(` ${type.padEnd(8)} ${path}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log();
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (args.includes('--init')) {
|
|
97
|
+
const configPath = join(process.cwd(), CONFIG_FILENAME);
|
|
98
|
+
|
|
99
|
+
if (existsSync(configPath)) {
|
|
100
|
+
console.error(`Config already exists: ${configPath}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
writeFileSync(configPath, JSON.stringify(SAMPLE_CONFIG, null, 2) + '\n');
|
|
105
|
+
console.log(`Created: ${configPath}`);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (args.includes('--init-global')) {
|
|
110
|
+
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
111
|
+
console.error(`Global config already exists: ${GLOBAL_CONFIG_PATH}`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(SAMPLE_CONFIG, null, 2) + '\n');
|
|
116
|
+
console.log(`Created: ${GLOBAL_CONFIG_PATH}`);
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Default: show merged config
|
|
121
|
+
const { config, projectRoot } = loadConfig();
|
|
122
|
+
const paths = getConfigPaths();
|
|
123
|
+
|
|
124
|
+
console.log('\nCurrent configuration:\n');
|
|
125
|
+
|
|
126
|
+
if (paths.length > 0) {
|
|
127
|
+
console.log('Loaded from:');
|
|
128
|
+
for (const { type, path } of paths) {
|
|
129
|
+
console.log(` ${type}: ${path}`);
|
|
130
|
+
}
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(JSON.stringify(config, null, 2));
|
|
135
|
+
console.log();
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-context - Estimate context token usage for files/directories
|
|
5
|
+
*
|
|
6
|
+
* Helps understand what contributes to context usage.
|
|
7
|
+
* Usage: tl-context [path] [--top N]
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Prompt info for tl-prompt
|
|
11
|
+
if (process.argv.includes('--prompt')) {
|
|
12
|
+
console.log(JSON.stringify({
|
|
13
|
+
name: 'tl-context',
|
|
14
|
+
desc: 'Estimate token usage for files/directories',
|
|
15
|
+
when: 'before-read',
|
|
16
|
+
example: 'tl-context src/'
|
|
17
|
+
}));
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
22
|
+
import { join, relative } from 'path';
|
|
23
|
+
|
|
24
|
+
const SKIP_DIRS = new Set([
|
|
25
|
+
'node_modules', '.git', 'android', 'ios', 'dist', 'build',
|
|
26
|
+
'.expo', '.next', 'coverage', '__pycache__', '.cache'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SKIP_EXTENSIONS = new Set([
|
|
30
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp',
|
|
31
|
+
'.woff', '.woff2', '.ttf', '.eot',
|
|
32
|
+
'.mp3', '.mp4', '.wav', '.ogg',
|
|
33
|
+
'.zip', '.tar', '.gz',
|
|
34
|
+
'.lock', '.log'
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Rough token estimate: ~4 chars per token for code
|
|
38
|
+
function estimateTokens(content) {
|
|
39
|
+
return Math.ceil(content.length / 4);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatTokens(tokens) {
|
|
43
|
+
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`;
|
|
44
|
+
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;
|
|
45
|
+
return String(tokens);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function shouldSkip(name, isDir) {
|
|
49
|
+
if (isDir && SKIP_DIRS.has(name)) return true;
|
|
50
|
+
if (!isDir) {
|
|
51
|
+
const ext = name.substring(name.lastIndexOf('.'));
|
|
52
|
+
if (SKIP_EXTENSIONS.has(ext)) return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function analyzeDir(dirPath, results = [], depth = 0) {
|
|
58
|
+
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
59
|
+
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (entry.name.startsWith('.') && entry.name !== '.claude') continue;
|
|
62
|
+
if (shouldSkip(entry.name, entry.isDirectory())) continue;
|
|
63
|
+
|
|
64
|
+
const fullPath = join(dirPath, entry.name);
|
|
65
|
+
|
|
66
|
+
if (entry.isDirectory()) {
|
|
67
|
+
analyzeDir(fullPath, results, depth + 1);
|
|
68
|
+
} else {
|
|
69
|
+
try {
|
|
70
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
71
|
+
const tokens = estimateTokens(content);
|
|
72
|
+
results.push({ path: fullPath, tokens, lines: content.split('\n').length });
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// Skip binary or unreadable files
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function printResults(results, rootPath, topN) {
|
|
83
|
+
// Sort by tokens descending
|
|
84
|
+
results.sort((a, b) => b.tokens - a.tokens);
|
|
85
|
+
|
|
86
|
+
const total = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
87
|
+
|
|
88
|
+
console.log(`\n📊 Context Estimate for: ${rootPath}\n`);
|
|
89
|
+
console.log(`Total: ~${formatTokens(total)} tokens across ${results.length} files\n`);
|
|
90
|
+
|
|
91
|
+
if (topN) {
|
|
92
|
+
console.log(`Top ${topN} largest files:\n`);
|
|
93
|
+
results = results.slice(0, topN);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const maxPathLen = Math.min(60, Math.max(...results.map(r => relative(rootPath, r.path).length)));
|
|
97
|
+
|
|
98
|
+
console.log(' Tokens Lines Path');
|
|
99
|
+
console.log(' ' + '-'.repeat(maxPathLen + 20));
|
|
100
|
+
|
|
101
|
+
for (const r of results) {
|
|
102
|
+
const relPath = relative(rootPath, r.path);
|
|
103
|
+
const truncPath = relPath.length > 60 ? '...' + relPath.slice(-57) : relPath;
|
|
104
|
+
console.log(` ${formatTokens(r.tokens).padStart(6)} ${String(r.lines).padStart(5)} ${truncPath}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log();
|
|
108
|
+
|
|
109
|
+
// Group by directory
|
|
110
|
+
const byDir = {};
|
|
111
|
+
for (const r of results) {
|
|
112
|
+
const rel = relative(rootPath, r.path);
|
|
113
|
+
const dir = rel.includes('/') ? rel.split('/')[0] : '.';
|
|
114
|
+
byDir[dir] = (byDir[dir] || 0) + r.tokens;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const sortedDirs = Object.entries(byDir).sort((a, b) => b[1] - a[1]);
|
|
118
|
+
|
|
119
|
+
console.log('By top-level directory:\n');
|
|
120
|
+
for (const [dir, tokens] of sortedDirs.slice(0, 10)) {
|
|
121
|
+
const pct = ((tokens / total) * 100).toFixed(1);
|
|
122
|
+
console.log(` ${formatTokens(tokens).padStart(6)} ${pct.padStart(5)}% ${dir}/`);
|
|
123
|
+
}
|
|
124
|
+
console.log();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Main
|
|
128
|
+
const args = process.argv.slice(2);
|
|
129
|
+
let targetPath = '.';
|
|
130
|
+
let topN = 20;
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < args.length; i++) {
|
|
133
|
+
if (args[i] === '--top' && args[i + 1]) {
|
|
134
|
+
topN = parseInt(args[i + 1], 10);
|
|
135
|
+
i++;
|
|
136
|
+
} else if (args[i] === '--all') {
|
|
137
|
+
topN = null;
|
|
138
|
+
} else if (!args[i].startsWith('-')) {
|
|
139
|
+
targetPath = args[i];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!existsSync(targetPath)) {
|
|
144
|
+
console.error(`Path not found: ${targetPath}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const stat = statSync(targetPath);
|
|
149
|
+
if (stat.isFile()) {
|
|
150
|
+
const content = readFileSync(targetPath, 'utf-8');
|
|
151
|
+
const tokens = estimateTokens(content);
|
|
152
|
+
console.log(`\n${targetPath}: ~${formatTokens(tokens)} tokens (${content.split('\n').length} lines)\n`);
|
|
153
|
+
} else {
|
|
154
|
+
const results = analyzeDir(targetPath);
|
|
155
|
+
printResults(results, targetPath, topN);
|
|
156
|
+
}
|