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.
@@ -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();
@@ -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
+ }