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
package/bin/tl-deps.mjs
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-deps - Show what a file imports/depends on
|
|
5
|
+
*
|
|
6
|
+
* Displays all imports and requires in a file, categorized by type
|
|
7
|
+
* (npm packages, local files, Node built-ins). Helps understand
|
|
8
|
+
* dependencies without reading the full file.
|
|
9
|
+
*
|
|
10
|
+
* Usage: tl-deps <file>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Prompt info for tl-prompt
|
|
14
|
+
if (process.argv.includes('--prompt')) {
|
|
15
|
+
console.log(JSON.stringify({
|
|
16
|
+
name: 'tl-deps',
|
|
17
|
+
desc: 'Show file imports and dependency tree',
|
|
18
|
+
when: 'before-read',
|
|
19
|
+
example: 'tl-deps src/index.ts --tree'
|
|
20
|
+
}));
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from 'fs';
|
|
25
|
+
import { basename, dirname, extname, resolve, relative } from 'path';
|
|
26
|
+
import {
|
|
27
|
+
createOutput,
|
|
28
|
+
parseCommonArgs,
|
|
29
|
+
estimateTokens,
|
|
30
|
+
formatTokens,
|
|
31
|
+
COMMON_OPTIONS_HELP
|
|
32
|
+
} from '../src/output.mjs';
|
|
33
|
+
import { findProjectRoot, detectLanguage } from '../src/project.mjs';
|
|
34
|
+
|
|
35
|
+
const HELP = `
|
|
36
|
+
tl-deps - Show what a file imports/depends on
|
|
37
|
+
|
|
38
|
+
Usage: tl-deps <file> [options]
|
|
39
|
+
|
|
40
|
+
Options:
|
|
41
|
+
--resolve, -r Show resolved paths for local imports
|
|
42
|
+
--tree, -t Show as dependency tree (follows local imports)
|
|
43
|
+
--depth N Max depth for tree mode (default: 2)
|
|
44
|
+
${COMMON_OPTIONS_HELP}
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
tl-deps src/app.ts # List all imports
|
|
48
|
+
tl-deps src/app.ts -r # Show resolved paths
|
|
49
|
+
tl-deps src/app.ts -t # Dependency tree
|
|
50
|
+
tl-deps src/app.ts -j # JSON output
|
|
51
|
+
|
|
52
|
+
Categories:
|
|
53
|
+
๐ฆ npm - node_modules packages
|
|
54
|
+
๐ local - relative imports (./file, ../file)
|
|
55
|
+
๐ง builtin - Node.js built-in modules
|
|
56
|
+
๐จ assets - CSS, images, etc.
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
// Node.js built-in modules
|
|
60
|
+
const NODE_BUILTINS = new Set([
|
|
61
|
+
'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants',
|
|
62
|
+
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https',
|
|
63
|
+
'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode',
|
|
64
|
+
'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers',
|
|
65
|
+
'tls', 'tty', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib'
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
69
|
+
// Import Extraction
|
|
70
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
71
|
+
|
|
72
|
+
function extractImports(content, lang) {
|
|
73
|
+
const imports = {
|
|
74
|
+
npm: [],
|
|
75
|
+
local: [],
|
|
76
|
+
builtin: [],
|
|
77
|
+
assets: [],
|
|
78
|
+
dynamic: []
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (lang === 'javascript' || lang === 'typescript') {
|
|
82
|
+
extractJsImports(content, imports);
|
|
83
|
+
} else if (lang === 'python') {
|
|
84
|
+
extractPythonImports(content, imports);
|
|
85
|
+
} else if (lang === 'go') {
|
|
86
|
+
extractGoImports(content, imports);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return imports;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractJsImports(content, imports) {
|
|
93
|
+
const lines = content.split('\n');
|
|
94
|
+
const seen = new Set(); // Avoid duplicates
|
|
95
|
+
|
|
96
|
+
// Track multi-line import state
|
|
97
|
+
let inMultiLineImport = false;
|
|
98
|
+
let multiLineBuffer = '';
|
|
99
|
+
let multiLineStart = 0;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const line = lines[i];
|
|
103
|
+
const lineNum = i + 1;
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
|
|
106
|
+
// Skip comments
|
|
107
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle multi-line imports
|
|
112
|
+
if (inMultiLineImport) {
|
|
113
|
+
multiLineBuffer += ' ' + trimmed;
|
|
114
|
+
if (trimmed.includes("'") || trimmed.includes('"')) {
|
|
115
|
+
// Try to extract the from clause
|
|
116
|
+
const fromMatch = multiLineBuffer.match(/from\s*['"]([^'"]+)['"]/);
|
|
117
|
+
if (fromMatch) {
|
|
118
|
+
const spec = fromMatch[1];
|
|
119
|
+
if (!seen.has(spec)) {
|
|
120
|
+
seen.add(spec);
|
|
121
|
+
const isTypeOnly = multiLineBuffer.includes('import type');
|
|
122
|
+
categorizeJsImport(spec, multiLineBuffer.substring(0, 100), multiLineStart, imports, isTypeOnly);
|
|
123
|
+
}
|
|
124
|
+
inMultiLineImport = false;
|
|
125
|
+
multiLineBuffer = '';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Start of import statement
|
|
132
|
+
if (trimmed.startsWith('import ')) {
|
|
133
|
+
// Check if it's complete on one line
|
|
134
|
+
const singleLineMatch = trimmed.match(/^import\s+(?:type\s+)?(?:.*?\s+from\s+)?['"]([^'"]+)['"]/);
|
|
135
|
+
if (singleLineMatch) {
|
|
136
|
+
const spec = singleLineMatch[1];
|
|
137
|
+
if (!seen.has(spec)) {
|
|
138
|
+
seen.add(spec);
|
|
139
|
+
categorizeJsImport(spec, trimmed, lineNum, imports, trimmed.includes('import type'));
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for from clause on same line
|
|
145
|
+
const fromMatch = trimmed.match(/from\s*['"]([^'"]+)['"]/);
|
|
146
|
+
if (fromMatch) {
|
|
147
|
+
const spec = fromMatch[1];
|
|
148
|
+
if (!seen.has(spec)) {
|
|
149
|
+
seen.add(spec);
|
|
150
|
+
categorizeJsImport(spec, trimmed, lineNum, imports, trimmed.includes('import type'));
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Multi-line import starting
|
|
156
|
+
inMultiLineImport = true;
|
|
157
|
+
multiLineBuffer = trimmed;
|
|
158
|
+
multiLineStart = lineNum;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// CommonJS: require('X')
|
|
163
|
+
const requireMatches = [...line.matchAll(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g)];
|
|
164
|
+
for (const match of requireMatches) {
|
|
165
|
+
const spec = match[1];
|
|
166
|
+
if (!seen.has(spec)) {
|
|
167
|
+
seen.add(spec);
|
|
168
|
+
categorizeJsImport(spec, trimmed, lineNum, imports, false);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Dynamic imports: import('X') - but not at start of line (that's regular import)
|
|
173
|
+
if (!trimmed.startsWith('import ')) {
|
|
174
|
+
const dynamicMatch = line.match(/import\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
175
|
+
if (dynamicMatch) {
|
|
176
|
+
const spec = dynamicMatch[1];
|
|
177
|
+
if (!seen.has('dynamic:' + spec)) {
|
|
178
|
+
seen.add('dynamic:' + spec);
|
|
179
|
+
imports.dynamic.push({
|
|
180
|
+
spec,
|
|
181
|
+
line: lineNum,
|
|
182
|
+
statement: trimmed
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function categorizeJsImport(spec, line, lineNum, imports, isTypeOnly) {
|
|
191
|
+
const entry = {
|
|
192
|
+
spec,
|
|
193
|
+
line: lineNum,
|
|
194
|
+
statement: line.trim().substring(0, 100),
|
|
195
|
+
isTypeOnly
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Asset imports
|
|
199
|
+
if (/\.(css|scss|sass|less|svg|png|jpg|jpeg|gif|webp|json)$/.test(spec)) {
|
|
200
|
+
imports.assets.push(entry);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Local imports
|
|
205
|
+
if (spec.startsWith('.') || spec.startsWith('/')) {
|
|
206
|
+
imports.local.push(entry);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Node built-ins (including node: prefix)
|
|
211
|
+
const modName = spec.replace(/^node:/, '').split('/')[0];
|
|
212
|
+
if (NODE_BUILTINS.has(modName)) {
|
|
213
|
+
imports.builtin.push(entry);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// npm packages
|
|
218
|
+
imports.npm.push(entry);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function extractPythonImports(content, imports) {
|
|
222
|
+
const lines = content.split('\n');
|
|
223
|
+
|
|
224
|
+
for (let i = 0; i < lines.length; i++) {
|
|
225
|
+
const line = lines[i];
|
|
226
|
+
const lineNum = i + 1;
|
|
227
|
+
const trimmed = line.trim();
|
|
228
|
+
|
|
229
|
+
// import X, from X import Y
|
|
230
|
+
const importMatch = trimmed.match(/^(?:from\s+(\S+)\s+)?import\s+(.+)$/);
|
|
231
|
+
if (importMatch) {
|
|
232
|
+
const module = importMatch[1] || importMatch[2].split(',')[0].split(' as ')[0].trim();
|
|
233
|
+
|
|
234
|
+
const entry = {
|
|
235
|
+
spec: module,
|
|
236
|
+
line: lineNum,
|
|
237
|
+
statement: trimmed.substring(0, 100)
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Relative imports
|
|
241
|
+
if (module.startsWith('.')) {
|
|
242
|
+
imports.local.push(entry);
|
|
243
|
+
} else {
|
|
244
|
+
imports.npm.push(entry); // Python doesn't distinguish npm/builtin easily
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function extractGoImports(content, imports) {
|
|
251
|
+
// Match import block or single imports
|
|
252
|
+
const importBlockMatch = content.match(/import\s*\(([\s\S]*?)\)/);
|
|
253
|
+
const singleImports = content.matchAll(/import\s+"([^"]+)"/g);
|
|
254
|
+
|
|
255
|
+
const processImport = (spec, lineNum = 0) => {
|
|
256
|
+
const entry = {
|
|
257
|
+
spec,
|
|
258
|
+
line: lineNum,
|
|
259
|
+
statement: `import "${spec}"`
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Standard library (no dots in path)
|
|
263
|
+
if (!spec.includes('.')) {
|
|
264
|
+
imports.builtin.push(entry);
|
|
265
|
+
} else {
|
|
266
|
+
imports.npm.push(entry);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
if (importBlockMatch) {
|
|
271
|
+
const lines = importBlockMatch[1].split('\n');
|
|
272
|
+
for (const line of lines) {
|
|
273
|
+
const match = line.match(/"([^"]+)"/);
|
|
274
|
+
if (match) {
|
|
275
|
+
processImport(match[1]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for (const match of singleImports) {
|
|
281
|
+
processImport(match[1]);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
286
|
+
// Path Resolution
|
|
287
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
288
|
+
|
|
289
|
+
function resolveLocalImport(spec, fileDir, projectRoot) {
|
|
290
|
+
const extensions = ['', '.js', '.jsx', '.ts', '.tsx', '.mjs', '/index.js', '/index.ts', '/index.tsx'];
|
|
291
|
+
|
|
292
|
+
for (const ext of extensions) {
|
|
293
|
+
const tryPath = resolve(fileDir, spec + ext);
|
|
294
|
+
if (existsSync(tryPath)) {
|
|
295
|
+
return relative(projectRoot, tryPath);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return spec; // Return original if can't resolve
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
303
|
+
// Tree Mode
|
|
304
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
305
|
+
|
|
306
|
+
function buildDependencyTree(filePath, projectRoot, maxDepth = 2, visited = new Set()) {
|
|
307
|
+
if (visited.has(filePath) || visited.size > 50) {
|
|
308
|
+
return { file: relative(projectRoot, filePath), circular: true };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
visited.add(filePath);
|
|
312
|
+
|
|
313
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
314
|
+
const lang = detectLanguage(filePath);
|
|
315
|
+
const imports = extractImports(content, lang);
|
|
316
|
+
const fileDir = dirname(filePath);
|
|
317
|
+
|
|
318
|
+
const tree = {
|
|
319
|
+
file: relative(projectRoot, filePath),
|
|
320
|
+
tokens: estimateTokens(content),
|
|
321
|
+
npm: imports.npm.map(i => i.spec),
|
|
322
|
+
builtin: imports.builtin.map(i => i.spec),
|
|
323
|
+
local: []
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
if (maxDepth > 0) {
|
|
327
|
+
for (const imp of imports.local) {
|
|
328
|
+
const resolved = resolveLocalImport(imp.spec, fileDir, projectRoot);
|
|
329
|
+
const fullPath = resolve(projectRoot, resolved);
|
|
330
|
+
|
|
331
|
+
if (existsSync(fullPath) && !visited.has(fullPath)) {
|
|
332
|
+
try {
|
|
333
|
+
const subtree = buildDependencyTree(fullPath, projectRoot, maxDepth - 1, visited);
|
|
334
|
+
tree.local.push(subtree);
|
|
335
|
+
} catch {
|
|
336
|
+
tree.local.push({ file: resolved, error: true });
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
tree.local.push({ file: resolved, circular: visited.has(fullPath) });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
tree.local = imports.local.map(i => ({ file: i.spec }));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return tree;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function printTree(tree, out, prefix = '', isLast = true) {
|
|
350
|
+
const connector = isLast ? 'โโโ ' : 'โโโ ';
|
|
351
|
+
const tokens = tree.tokens ? ` (~${formatTokens(tree.tokens)})` : '';
|
|
352
|
+
const marker = tree.circular ? ' โณ' : tree.error ? ' โ ' : '';
|
|
353
|
+
|
|
354
|
+
out.add(`${prefix}${connector}${tree.file}${tokens}${marker}`);
|
|
355
|
+
|
|
356
|
+
const newPrefix = prefix + (isLast ? ' ' : 'โ ');
|
|
357
|
+
|
|
358
|
+
// Print npm deps compactly
|
|
359
|
+
if (tree.npm && tree.npm.length > 0) {
|
|
360
|
+
out.add(`${newPrefix}๐ฆ ${tree.npm.join(', ')}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Print local deps recursively
|
|
364
|
+
if (tree.local && tree.local.length > 0) {
|
|
365
|
+
tree.local.forEach((child, i) => {
|
|
366
|
+
printTree(child, out, newPrefix, i === tree.local.length - 1);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
372
|
+
// Output
|
|
373
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
374
|
+
|
|
375
|
+
function printCategory(out, title, items, emoji, showResolved, fileDir, projectRoot) {
|
|
376
|
+
if (items.length === 0) return;
|
|
377
|
+
|
|
378
|
+
out.add(`${emoji} ${title} (${items.length}):`);
|
|
379
|
+
|
|
380
|
+
for (const item of items) {
|
|
381
|
+
let line = ` ${item.spec}`;
|
|
382
|
+
|
|
383
|
+
if (showResolved && item.spec.startsWith('.')) {
|
|
384
|
+
const resolved = resolveLocalImport(item.spec, fileDir, projectRoot);
|
|
385
|
+
line += ` โ ${resolved}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (item.isTypeOnly) {
|
|
389
|
+
line += ' [type]';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
line += ` :${item.line}`;
|
|
393
|
+
out.add(line);
|
|
394
|
+
}
|
|
395
|
+
out.blank();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
399
|
+
// Main
|
|
400
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
401
|
+
|
|
402
|
+
const args = process.argv.slice(2);
|
|
403
|
+
const options = parseCommonArgs(args);
|
|
404
|
+
|
|
405
|
+
// Parse tool-specific options
|
|
406
|
+
const showResolved = options.remaining.includes('--resolve') || options.remaining.includes('-r');
|
|
407
|
+
const treeMode = options.remaining.includes('--tree') || options.remaining.includes('-t');
|
|
408
|
+
let maxDepth = 2;
|
|
409
|
+
|
|
410
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
411
|
+
if (options.remaining[i] === '--depth' && options.remaining[i + 1]) {
|
|
412
|
+
maxDepth = parseInt(options.remaining[i + 1], 10);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const filePath = options.remaining.find(a => !a.startsWith('-'));
|
|
417
|
+
|
|
418
|
+
if (options.help || !filePath) {
|
|
419
|
+
console.log(HELP);
|
|
420
|
+
process.exit(options.help ? 0 : 1);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const resolvedPath = resolve(filePath);
|
|
424
|
+
|
|
425
|
+
if (!existsSync(resolvedPath)) {
|
|
426
|
+
console.error(`File not found: ${filePath}`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const projectRoot = findProjectRoot();
|
|
431
|
+
const relPath = relative(projectRoot, resolvedPath);
|
|
432
|
+
const content = readFileSync(resolvedPath, 'utf-8');
|
|
433
|
+
const lang = detectLanguage(resolvedPath);
|
|
434
|
+
|
|
435
|
+
if (!lang) {
|
|
436
|
+
console.error(`Unsupported file type: ${extname(resolvedPath)}`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const out = createOutput(options);
|
|
441
|
+
|
|
442
|
+
if (treeMode) {
|
|
443
|
+
// Tree mode
|
|
444
|
+
out.header(`\n๐ณ Dependency tree: ${relPath}`);
|
|
445
|
+
out.header(` Max depth: ${maxDepth}`);
|
|
446
|
+
out.blank();
|
|
447
|
+
|
|
448
|
+
const tree = buildDependencyTree(resolvedPath, projectRoot, maxDepth);
|
|
449
|
+
printTree(tree, out, '', true);
|
|
450
|
+
out.blank();
|
|
451
|
+
|
|
452
|
+
out.setData('tree', tree);
|
|
453
|
+
} else {
|
|
454
|
+
// List mode
|
|
455
|
+
const imports = extractImports(content, lang);
|
|
456
|
+
const fileDir = dirname(resolvedPath);
|
|
457
|
+
const totalImports = imports.npm.length + imports.local.length + imports.builtin.length + imports.assets.length + imports.dynamic.length;
|
|
458
|
+
|
|
459
|
+
out.header(`\n๐ฅ Dependencies: ${relPath}`);
|
|
460
|
+
out.header(` ${totalImports} imports found`);
|
|
461
|
+
out.blank();
|
|
462
|
+
|
|
463
|
+
printCategory(out, 'npm packages', imports.npm, '๐ฆ', false, fileDir, projectRoot);
|
|
464
|
+
printCategory(out, 'Local files', imports.local, '๐', showResolved, fileDir, projectRoot);
|
|
465
|
+
printCategory(out, 'Node built-ins', imports.builtin, '๐ง', false, fileDir, projectRoot);
|
|
466
|
+
printCategory(out, 'Assets', imports.assets, '๐จ', false, fileDir, projectRoot);
|
|
467
|
+
printCategory(out, 'Dynamic imports', imports.dynamic, 'โก', false, fileDir, projectRoot);
|
|
468
|
+
|
|
469
|
+
out.setData('file', relPath);
|
|
470
|
+
out.setData('imports', imports);
|
|
471
|
+
out.setData('totalImports', totalImports);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
out.print();
|
package/bin/tl-diff.mjs
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-diff - Token-efficient git diff summary
|
|
5
|
+
*
|
|
6
|
+
* Summarizes git changes without outputting full diff content.
|
|
7
|
+
* Great for understanding what changed before diving into details.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-diff [ref] [--staged] [--stat-only]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-diff',
|
|
16
|
+
desc: 'Summarize git changes with token estimates',
|
|
17
|
+
when: 'search',
|
|
18
|
+
example: 'tl-diff --staged'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { execSync } from 'child_process';
|
|
24
|
+
|
|
25
|
+
function run(cmd) {
|
|
26
|
+
try {
|
|
27
|
+
return execSync(cmd, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return e.stdout || '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function estimateTokens(content) {
|
|
34
|
+
return Math.ceil(content.length / 4);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatTokens(tokens) {
|
|
38
|
+
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;
|
|
39
|
+
return String(tokens);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseDiffStat(stat) {
|
|
43
|
+
const lines = stat.trim().split('\n');
|
|
44
|
+
const files = [];
|
|
45
|
+
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
// Match: " src/file.ts | 42 +++---"
|
|
48
|
+
const match = line.match(/^\s*(.+?)\s*\|\s*(\d+)\s*(\+*)(-*)/);
|
|
49
|
+
if (match) {
|
|
50
|
+
files.push({
|
|
51
|
+
path: match[1].trim(),
|
|
52
|
+
changes: parseInt(match[2]),
|
|
53
|
+
additions: match[3].length,
|
|
54
|
+
deletions: match[4].length
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return files;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function categorizeChanges(files) {
|
|
63
|
+
const categories = {
|
|
64
|
+
components: [],
|
|
65
|
+
hooks: [],
|
|
66
|
+
store: [],
|
|
67
|
+
types: [],
|
|
68
|
+
tests: [],
|
|
69
|
+
config: [],
|
|
70
|
+
manuscripts: [],
|
|
71
|
+
other: []
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const path = file.path.toLowerCase();
|
|
76
|
+
|
|
77
|
+
if (path.includes('.test.') || path.includes('.spec.') || path.includes('__tests__')) {
|
|
78
|
+
categories.tests.push(file);
|
|
79
|
+
} else if (path.includes('/components/') || path.endsWith('.tsx')) {
|
|
80
|
+
categories.components.push(file);
|
|
81
|
+
} else if (path.includes('/hooks/') || path.includes('use')) {
|
|
82
|
+
categories.hooks.push(file);
|
|
83
|
+
} else if (path.includes('/store/') || path.includes('slice') || path.includes('reducer')) {
|
|
84
|
+
categories.store.push(file);
|
|
85
|
+
} else if (path.includes('/types/') || path.endsWith('.d.ts')) {
|
|
86
|
+
categories.types.push(file);
|
|
87
|
+
} else if (path.includes('manuscripts') || path.endsWith('.json')) {
|
|
88
|
+
categories.manuscripts.push(file);
|
|
89
|
+
} else if (path.includes('config') || path.includes('package.json') || path.includes('tsconfig')) {
|
|
90
|
+
categories.config.push(file);
|
|
91
|
+
} else {
|
|
92
|
+
categories.other.push(file);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return categories;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function printSummary(files, categories, options) {
|
|
100
|
+
const totalChanges = files.reduce((sum, f) => sum + f.changes, 0);
|
|
101
|
+
const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
|
|
102
|
+
const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
|
103
|
+
|
|
104
|
+
console.log(`\n๐ Diff Summary`);
|
|
105
|
+
console.log(` ${files.length} files changed, ~${formatTokens(totalChanges * 4)} tokens of changes`);
|
|
106
|
+
console.log(` +${totalAdditions} additions, -${totalDeletions} deletions\n`);
|
|
107
|
+
|
|
108
|
+
const order = ['components', 'hooks', 'store', 'types', 'manuscripts', 'tests', 'config', 'other'];
|
|
109
|
+
const labels = {
|
|
110
|
+
components: '๐งฉ Components',
|
|
111
|
+
hooks: '๐ช Hooks',
|
|
112
|
+
store: '๐ฆ Store',
|
|
113
|
+
types: '๐ Types',
|
|
114
|
+
manuscripts: '๐ Manuscripts',
|
|
115
|
+
tests: '๐งช Tests',
|
|
116
|
+
config: 'โ๏ธ Config',
|
|
117
|
+
other: '๐ Other'
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
for (const cat of order) {
|
|
121
|
+
const catFiles = categories[cat];
|
|
122
|
+
if (catFiles.length === 0) continue;
|
|
123
|
+
|
|
124
|
+
console.log(`${labels[cat]} (${catFiles.length})`);
|
|
125
|
+
|
|
126
|
+
// Sort by changes descending
|
|
127
|
+
catFiles.sort((a, b) => b.changes - a.changes);
|
|
128
|
+
|
|
129
|
+
for (const f of catFiles.slice(0, 10)) {
|
|
130
|
+
const bar = '+'.repeat(Math.min(f.additions, 20)) + '-'.repeat(Math.min(f.deletions, 20));
|
|
131
|
+
console.log(` ${f.path}`);
|
|
132
|
+
console.log(` ${f.changes} changes ${bar}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (catFiles.length > 10) {
|
|
136
|
+
console.log(` ... and ${catFiles.length - 10} more`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Main
|
|
144
|
+
const args = process.argv.slice(2);
|
|
145
|
+
let ref = '';
|
|
146
|
+
let staged = false;
|
|
147
|
+
let statOnly = false;
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < args.length; i++) {
|
|
150
|
+
if (args[i] === '--staged') {
|
|
151
|
+
staged = true;
|
|
152
|
+
} else if (args[i] === '--stat-only') {
|
|
153
|
+
statOnly = true;
|
|
154
|
+
} else if (!args[i].startsWith('-')) {
|
|
155
|
+
ref = args[i];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Build git diff command
|
|
160
|
+
let diffCmd = 'git diff';
|
|
161
|
+
if (staged) {
|
|
162
|
+
diffCmd += ' --cached';
|
|
163
|
+
} else if (ref) {
|
|
164
|
+
diffCmd += ` ${ref}`;
|
|
165
|
+
}
|
|
166
|
+
diffCmd += ' --stat=200';
|
|
167
|
+
|
|
168
|
+
const stat = run(diffCmd);
|
|
169
|
+
|
|
170
|
+
if (!stat.trim()) {
|
|
171
|
+
console.log('\nโจ No changes detected\n');
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const files = parseDiffStat(stat);
|
|
176
|
+
const categories = categorizeChanges(files);
|
|
177
|
+
|
|
178
|
+
printSummary(files, categories, { statOnly });
|
|
179
|
+
|
|
180
|
+
if (!statOnly) {
|
|
181
|
+
console.log('๐ก Tip: Use --stat-only for just the summary, or check specific files with:');
|
|
182
|
+
console.log(' git diff [ref] -- path/to/file.ts\n');
|
|
183
|
+
}
|