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,494 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-unused - Find potentially unused exports and files
|
|
5
|
+
*
|
|
6
|
+
* Scans the codebase to find exports that aren't imported anywhere
|
|
7
|
+
* and files that aren't referenced by other files.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-unused [dir] [--exports-only]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-unused',
|
|
16
|
+
desc: 'Find unused exports and unreferenced files',
|
|
17
|
+
when: 'before-modify',
|
|
18
|
+
example: 'tl-unused src/'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
24
|
+
import { join, relative, dirname, basename, extname } from 'path';
|
|
25
|
+
import { spawnSync } from 'child_process';
|
|
26
|
+
import {
|
|
27
|
+
createOutput,
|
|
28
|
+
parseCommonArgs,
|
|
29
|
+
COMMON_OPTIONS_HELP
|
|
30
|
+
} from '../src/output.mjs';
|
|
31
|
+
import { findProjectRoot, shouldSkip, isCodeFile } from '../src/project.mjs';
|
|
32
|
+
|
|
33
|
+
const HELP = `
|
|
34
|
+
tl-unused - Find potentially unused exports and unreferenced files
|
|
35
|
+
|
|
36
|
+
Usage: tl-unused [dir] [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--exports-only, -e Only check for unused exports
|
|
40
|
+
--files-only, -f Only check for unreferenced files
|
|
41
|
+
--ignore <pattern> Ignore files matching pattern (can use multiple times)
|
|
42
|
+
--include-tests Include test files in analysis (default: excluded)
|
|
43
|
+
${COMMON_OPTIONS_HELP}
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
tl-unused # Full analysis
|
|
47
|
+
tl-unused src/ # Analyze src/ only
|
|
48
|
+
tl-unused -e # Unused exports only
|
|
49
|
+
tl-unused --ignore "*.d.ts" # Ignore type definitions
|
|
50
|
+
|
|
51
|
+
Note: This is a heuristic analysis. Some "unused" exports might be:
|
|
52
|
+
- Used dynamically (require(), dynamic imports)
|
|
53
|
+
- Public API exports
|
|
54
|
+
- Entry points
|
|
55
|
+
- Used by external packages
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────
|
|
59
|
+
// File Discovery
|
|
60
|
+
// ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const CODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.mts']);
|
|
63
|
+
|
|
64
|
+
function isCodeExtension(filePath) {
|
|
65
|
+
return CODE_EXTENSIONS.has(extname(filePath).toLowerCase());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findCodeFiles(dir, files = [], options = {}) {
|
|
69
|
+
const { includeTests = false, ignorePatterns = [] } = options;
|
|
70
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const fullPath = join(dir, entry.name);
|
|
74
|
+
|
|
75
|
+
// Check ignore patterns
|
|
76
|
+
if (ignorePatterns.some(p => entry.name.includes(p) || fullPath.includes(p))) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
if (!shouldSkip(entry.name, true)) {
|
|
82
|
+
findCodeFiles(fullPath, files, options);
|
|
83
|
+
}
|
|
84
|
+
} else if (entry.isFile() && isCodeExtension(fullPath)) {
|
|
85
|
+
if (!shouldSkip(entry.name, false)) {
|
|
86
|
+
// Skip test files unless includeTests
|
|
87
|
+
if (!includeTests) {
|
|
88
|
+
const lower = entry.name.toLowerCase();
|
|
89
|
+
if (lower.includes('.test.') || lower.includes('.spec.') ||
|
|
90
|
+
lower.includes('__tests__') || lower.includes('__mocks__')) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
files.push(fullPath);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return files;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─────────────────────────────────────────────────────────────
|
|
103
|
+
// Export Extraction
|
|
104
|
+
// ─────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
function extractExports(content, filePath) {
|
|
107
|
+
const exports = [];
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
const trimmed = line.trim();
|
|
113
|
+
|
|
114
|
+
if (!trimmed.startsWith('export ')) continue;
|
|
115
|
+
|
|
116
|
+
// Skip re-exports (these are pass-through)
|
|
117
|
+
if (trimmed.includes(' from ')) continue;
|
|
118
|
+
|
|
119
|
+
// Named exports: export { a, b }
|
|
120
|
+
const namedMatch = trimmed.match(/^export\s+\{([^}]+)\}/);
|
|
121
|
+
if (namedMatch) {
|
|
122
|
+
const names = namedMatch[1].split(',').map(n => {
|
|
123
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
124
|
+
return parts[parts.length - 1].trim(); // Use alias if present
|
|
125
|
+
});
|
|
126
|
+
names.forEach(name => exports.push({ name, line: i + 1 }));
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Default export
|
|
131
|
+
if (trimmed.startsWith('export default ')) {
|
|
132
|
+
exports.push({ name: 'default', line: i + 1 });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// export interface/type/enum
|
|
137
|
+
const typeMatch = trimmed.match(/^export\s+(?:interface|type|enum|const\s+enum)\s+(\w+)/);
|
|
138
|
+
if (typeMatch) {
|
|
139
|
+
exports.push({ name: typeMatch[1], line: i + 1, isType: true });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// export function/class/const
|
|
144
|
+
const valueMatch = trimmed.match(/^export\s+(?:async\s+)?(?:function|class|const|let|var)\s+(\w+)/);
|
|
145
|
+
if (valueMatch) {
|
|
146
|
+
exports.push({ name: valueMatch[1], line: i + 1 });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// export abstract class
|
|
151
|
+
const abstractMatch = trimmed.match(/^export\s+abstract\s+class\s+(\w+)/);
|
|
152
|
+
if (abstractMatch) {
|
|
153
|
+
exports.push({ name: abstractMatch[1], line: i + 1 });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return exports;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─────────────────────────────────────────────────────────────
|
|
162
|
+
// Import/Reference Detection
|
|
163
|
+
// ─────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
function extractImports(content) {
|
|
166
|
+
const imports = {
|
|
167
|
+
named: new Set(), // Named imports
|
|
168
|
+
files: new Set() // Import paths (for file reference checking)
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Named imports: import { a, b } from './x'
|
|
172
|
+
const namedRegex = /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
173
|
+
let match;
|
|
174
|
+
while ((match = namedRegex.exec(content)) !== null) {
|
|
175
|
+
const names = match[1].split(',').map(n => {
|
|
176
|
+
const parts = n.trim().split(/\s+as\s+/);
|
|
177
|
+
return parts[0].trim(); // Use original name, not alias
|
|
178
|
+
});
|
|
179
|
+
names.forEach(name => imports.named.add(name));
|
|
180
|
+
imports.files.add(match[2]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Default imports: import X from './x'
|
|
184
|
+
const defaultRegex = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g;
|
|
185
|
+
while ((match = defaultRegex.exec(content)) !== null) {
|
|
186
|
+
imports.named.add('default');
|
|
187
|
+
imports.files.add(match[2]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Namespace imports: import * as X from './x'
|
|
191
|
+
const namespaceRegex = /import\s+\*\s+as\s+\w+\s+from\s+['"]([^'"]+)['"]/g;
|
|
192
|
+
while ((match = namespaceRegex.exec(content)) !== null) {
|
|
193
|
+
imports.files.add(match[1]);
|
|
194
|
+
// Namespace import means all exports are potentially used
|
|
195
|
+
imports.named.add('*');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Dynamic imports: import('./x')
|
|
199
|
+
const dynamicRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
200
|
+
while ((match = dynamicRegex.exec(content)) !== null) {
|
|
201
|
+
imports.files.add(match[1]);
|
|
202
|
+
imports.named.add('*'); // Dynamic import might use anything
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// require(): require('./x')
|
|
206
|
+
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
207
|
+
while ((match = requireRegex.exec(content)) !== null) {
|
|
208
|
+
imports.files.add(match[1]);
|
|
209
|
+
imports.named.add('*');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Type imports: import type { X } from './x'
|
|
213
|
+
const typeRegex = /import\s+type\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
214
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
215
|
+
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0].trim());
|
|
216
|
+
names.forEach(name => imports.named.add(name));
|
|
217
|
+
imports.files.add(match[2]);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return imports;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function findReferencesWithGrep(name, projectRoot, excludeFile) {
|
|
224
|
+
// Use ripgrep for fast reference counting
|
|
225
|
+
const args = [
|
|
226
|
+
'-l', // Files only
|
|
227
|
+
'--type', 'js',
|
|
228
|
+
'--type', 'ts',
|
|
229
|
+
'-w', // Word boundary
|
|
230
|
+
name,
|
|
231
|
+
'.'
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const result = spawnSync('rg', args, {
|
|
235
|
+
cwd: projectRoot,
|
|
236
|
+
encoding: 'utf-8'
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (result.error || result.status !== 0) {
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const files = result.stdout.trim().split('\n').filter(Boolean);
|
|
244
|
+
// Exclude the file that exports it
|
|
245
|
+
const relExclude = relative(projectRoot, excludeFile);
|
|
246
|
+
const otherFiles = files.filter(f => f !== relExclude && !f.includes(relExclude));
|
|
247
|
+
|
|
248
|
+
return otherFiles.length;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ─────────────────────────────────────────────────────────────
|
|
252
|
+
// Analysis
|
|
253
|
+
// ─────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
function analyzeUnusedExports(files, projectRoot) {
|
|
256
|
+
const allImports = {
|
|
257
|
+
named: new Set(),
|
|
258
|
+
files: new Set()
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// First pass: collect all imports
|
|
262
|
+
for (const file of files) {
|
|
263
|
+
const content = readFileSync(file, 'utf-8');
|
|
264
|
+
const imports = extractImports(content);
|
|
265
|
+
|
|
266
|
+
imports.named.forEach(n => allImports.named.add(n));
|
|
267
|
+
imports.files.forEach(f => allImports.files.add(f));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check if namespace imports are used (means everything is potentially used)
|
|
271
|
+
const hasWildcardImport = allImports.named.has('*');
|
|
272
|
+
|
|
273
|
+
// Second pass: find unused exports
|
|
274
|
+
const unused = [];
|
|
275
|
+
|
|
276
|
+
for (const file of files) {
|
|
277
|
+
const content = readFileSync(file, 'utf-8');
|
|
278
|
+
const exports = extractExports(content, file);
|
|
279
|
+
const relPath = relative(projectRoot, file);
|
|
280
|
+
|
|
281
|
+
for (const exp of exports) {
|
|
282
|
+
// Skip default exports (often intentionally exported)
|
|
283
|
+
if (exp.name === 'default') continue;
|
|
284
|
+
|
|
285
|
+
// If there's a wildcard import somewhere, we can't be sure it's unused
|
|
286
|
+
if (hasWildcardImport) {
|
|
287
|
+
// Do a more thorough grep-based check
|
|
288
|
+
const refs = findReferencesWithGrep(exp.name, projectRoot, file);
|
|
289
|
+
if (refs === 0) {
|
|
290
|
+
unused.push({
|
|
291
|
+
file: relPath,
|
|
292
|
+
name: exp.name,
|
|
293
|
+
line: exp.line,
|
|
294
|
+
isType: exp.isType
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// Simple check: is the name in our imports set?
|
|
299
|
+
if (!allImports.named.has(exp.name)) {
|
|
300
|
+
// Double-check with grep for common names
|
|
301
|
+
if (exp.name.length <= 3 || /^[A-Z]/.test(exp.name)) {
|
|
302
|
+
const refs = findReferencesWithGrep(exp.name, projectRoot, file);
|
|
303
|
+
if (refs > 0) continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
unused.push({
|
|
307
|
+
file: relPath,
|
|
308
|
+
name: exp.name,
|
|
309
|
+
line: exp.line,
|
|
310
|
+
isType: exp.isType
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return unused;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function analyzeUnreferencedFiles(files, projectRoot) {
|
|
321
|
+
const importedPaths = new Set();
|
|
322
|
+
|
|
323
|
+
// Collect all imported paths
|
|
324
|
+
for (const file of files) {
|
|
325
|
+
const content = readFileSync(file, 'utf-8');
|
|
326
|
+
const imports = extractImports(content);
|
|
327
|
+
|
|
328
|
+
for (const importPath of imports.files) {
|
|
329
|
+
// Resolve relative imports
|
|
330
|
+
if (importPath.startsWith('.')) {
|
|
331
|
+
const resolved = join(dirname(file), importPath);
|
|
332
|
+
// Try with various extensions
|
|
333
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '/index.ts', '/index.tsx', '/index.js'];
|
|
334
|
+
for (const ext of extensions) {
|
|
335
|
+
importedPaths.add(resolved + ext);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Find unreferenced files
|
|
342
|
+
const unreferenced = [];
|
|
343
|
+
const entryPatterns = ['index.', 'main.', 'app.', 'server.', 'cli.', 'bin/'];
|
|
344
|
+
|
|
345
|
+
for (const file of files) {
|
|
346
|
+
const relPath = relative(projectRoot, file);
|
|
347
|
+
|
|
348
|
+
// Skip likely entry points
|
|
349
|
+
if (entryPatterns.some(p => relPath.includes(p))) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Check if this file is imported
|
|
354
|
+
const isImported = [...importedPaths].some(p => {
|
|
355
|
+
return file.startsWith(p) || file === p;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (!isImported) {
|
|
359
|
+
// Double-check: look for any reference to the file basename
|
|
360
|
+
const name = basename(file, extname(file));
|
|
361
|
+
const refs = findReferencesWithGrep(name, projectRoot, file);
|
|
362
|
+
|
|
363
|
+
if (refs === 0) {
|
|
364
|
+
unreferenced.push(relPath);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return unreferenced;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─────────────────────────────────────────────────────────────
|
|
373
|
+
// Main
|
|
374
|
+
// ─────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
const args = process.argv.slice(2);
|
|
377
|
+
const options = parseCommonArgs(args);
|
|
378
|
+
|
|
379
|
+
// Parse custom options
|
|
380
|
+
let exportsOnly = false;
|
|
381
|
+
let filesOnly = false;
|
|
382
|
+
let includeTests = false;
|
|
383
|
+
const ignorePatterns = [];
|
|
384
|
+
|
|
385
|
+
const remaining = [];
|
|
386
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
387
|
+
const arg = options.remaining[i];
|
|
388
|
+
|
|
389
|
+
if (arg === '--exports-only' || arg === '-e') {
|
|
390
|
+
exportsOnly = true;
|
|
391
|
+
} else if (arg === '--files-only' || arg === '-f') {
|
|
392
|
+
filesOnly = true;
|
|
393
|
+
} else if (arg === '--include-tests') {
|
|
394
|
+
includeTests = true;
|
|
395
|
+
} else if (arg === '--ignore') {
|
|
396
|
+
ignorePatterns.push(options.remaining[++i]);
|
|
397
|
+
} else if (!arg.startsWith('-')) {
|
|
398
|
+
remaining.push(arg);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const targetDir = remaining[0] || '.';
|
|
403
|
+
|
|
404
|
+
if (options.help) {
|
|
405
|
+
console.log(HELP);
|
|
406
|
+
process.exit(0);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!existsSync(targetDir)) {
|
|
410
|
+
console.error(`Directory not found: ${targetDir}`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const projectRoot = findProjectRoot();
|
|
415
|
+
const out = createOutput(options);
|
|
416
|
+
|
|
417
|
+
// Find all code files
|
|
418
|
+
const files = findCodeFiles(targetDir, [], { includeTests, ignorePatterns });
|
|
419
|
+
|
|
420
|
+
if (files.length === 0) {
|
|
421
|
+
console.error('No code files found');
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
out.header(`🔍 Analyzing ${files.length} files for unused code...`);
|
|
426
|
+
out.blank();
|
|
427
|
+
|
|
428
|
+
const results = {
|
|
429
|
+
unusedExports: [],
|
|
430
|
+
unreferencedFiles: []
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Analyze unused exports
|
|
434
|
+
if (!filesOnly) {
|
|
435
|
+
results.unusedExports = analyzeUnusedExports(files, projectRoot);
|
|
436
|
+
|
|
437
|
+
if (results.unusedExports.length > 0) {
|
|
438
|
+
out.add(`Potentially unused exports (${results.unusedExports.length}):`);
|
|
439
|
+
out.blank();
|
|
440
|
+
|
|
441
|
+
// Group by file
|
|
442
|
+
const byFile = new Map();
|
|
443
|
+
for (const exp of results.unusedExports) {
|
|
444
|
+
if (!byFile.has(exp.file)) {
|
|
445
|
+
byFile.set(exp.file, []);
|
|
446
|
+
}
|
|
447
|
+
byFile.get(exp.file).push(exp);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
for (const [file, exports] of byFile) {
|
|
451
|
+
out.add(` ${file}`);
|
|
452
|
+
for (const exp of exports) {
|
|
453
|
+
const typeIndicator = exp.isType ? ' (type)' : '';
|
|
454
|
+
out.add(` L${exp.line}: ${exp.name}${typeIndicator}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
out.blank();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Analyze unreferenced files
|
|
462
|
+
if (!exportsOnly) {
|
|
463
|
+
results.unreferencedFiles = analyzeUnreferencedFiles(files, projectRoot);
|
|
464
|
+
|
|
465
|
+
if (results.unreferencedFiles.length > 0) {
|
|
466
|
+
out.add(`Potentially unreferenced files (${results.unreferencedFiles.length}):`);
|
|
467
|
+
out.blank();
|
|
468
|
+
|
|
469
|
+
for (const file of results.unreferencedFiles) {
|
|
470
|
+
out.add(` ${file}`);
|
|
471
|
+
}
|
|
472
|
+
out.blank();
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Set JSON data
|
|
477
|
+
out.setData('unusedExports', results.unusedExports);
|
|
478
|
+
out.setData('unreferencedFiles', results.unreferencedFiles);
|
|
479
|
+
|
|
480
|
+
// Summary
|
|
481
|
+
if (!options.quiet) {
|
|
482
|
+
const totalIssues = results.unusedExports.length + results.unreferencedFiles.length;
|
|
483
|
+
|
|
484
|
+
if (totalIssues === 0) {
|
|
485
|
+
out.add('✓ No obviously unused code found');
|
|
486
|
+
} else {
|
|
487
|
+
out.add(`Found ${results.unusedExports.length} potentially unused exports, ${results.unreferencedFiles.length} unreferenced files`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
out.blank();
|
|
491
|
+
out.add('Note: Review carefully - some exports may be public API or dynamically used');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
out.print();
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tokenlean",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Lean CLI tools for AI agents and developers - reduce context, save tokens",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.0.0"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/edimuj/tokenlean.git"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/edimuj/tokenlean#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/edimuj/tokenlean/issues"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"src/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"bin": {
|
|
23
|
+
"tl-api": "./bin/tl-api.mjs",
|
|
24
|
+
"tl-blame": "./bin/tl-blame.mjs",
|
|
25
|
+
"tl-config": "./bin/tl-config.mjs",
|
|
26
|
+
"tl-context": "./bin/tl-context.mjs",
|
|
27
|
+
"tl-coverage": "./bin/tl-coverage.mjs",
|
|
28
|
+
"tl-complexity": "./bin/tl-complexity.mjs",
|
|
29
|
+
"tl-component": "./bin/tl-component.mjs",
|
|
30
|
+
"tl-deps": "./bin/tl-deps.mjs",
|
|
31
|
+
"tl-diff": "./bin/tl-diff.mjs",
|
|
32
|
+
"tl-entry": "./bin/tl-entry.mjs",
|
|
33
|
+
"tl-env": "./bin/tl-env.mjs",
|
|
34
|
+
"tl-exports": "./bin/tl-exports.mjs",
|
|
35
|
+
"tl-flow": "./bin/tl-flow.mjs",
|
|
36
|
+
"tl-history": "./bin/tl-history.mjs",
|
|
37
|
+
"tl-hotspots": "./bin/tl-hotspots.mjs",
|
|
38
|
+
"tl-impact": "./bin/tl-impact.mjs",
|
|
39
|
+
"tl-prompt": "./bin/tl-prompt.mjs",
|
|
40
|
+
"tl-related": "./bin/tl-related.mjs",
|
|
41
|
+
"tl-routes": "./bin/tl-routes.mjs",
|
|
42
|
+
"tl-search": "./bin/tl-search.mjs",
|
|
43
|
+
"tl-structure": "./bin/tl-structure.mjs",
|
|
44
|
+
"tl-symbols": "./bin/tl-symbols.mjs",
|
|
45
|
+
"tl-todo": "./bin/tl-todo.mjs",
|
|
46
|
+
"tl-types": "./bin/tl-types.mjs",
|
|
47
|
+
"tl-unused": "./bin/tl-unused.mjs"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"link": "npm link"
|
|
51
|
+
},
|
|
52
|
+
"keywords": ["ai", "agents", "cli", "tools", "tokens", "context", "developer"],
|
|
53
|
+
"author": "Edin Mujkanovic",
|
|
54
|
+
"license": "MIT"
|
|
55
|
+
}
|