tokenlean 0.1.0 โ 0.2.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/bin/tl-component.mjs +109 -83
- package/bin/tl-context.mjs +147 -98
- package/bin/tl-diff.mjs +95 -62
- package/bin/tl-related.mjs +112 -71
- package/bin/tl-search.mjs +142 -27
- package/bin/tl-structure.mjs +152 -83
- package/package.json +1 -1
package/bin/tl-diff.mjs
CHANGED
|
@@ -21,6 +21,30 @@ if (process.argv.includes('--prompt')) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
import { execSync } from 'child_process';
|
|
24
|
+
import {
|
|
25
|
+
createOutput,
|
|
26
|
+
parseCommonArgs,
|
|
27
|
+
formatTokens,
|
|
28
|
+
COMMON_OPTIONS_HELP
|
|
29
|
+
} from '../src/output.mjs';
|
|
30
|
+
|
|
31
|
+
const HELP = `
|
|
32
|
+
tl-diff - Token-efficient git diff summary
|
|
33
|
+
|
|
34
|
+
Usage: tl-diff [ref] [options]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--staged Show staged changes only
|
|
38
|
+
--stat-only Show just the summary (no file list)
|
|
39
|
+
${COMMON_OPTIONS_HELP}
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
tl-diff # Working directory changes
|
|
43
|
+
tl-diff --staged # Staged changes
|
|
44
|
+
tl-diff HEAD~3 # Last 3 commits
|
|
45
|
+
tl-diff main # Changes vs main branch
|
|
46
|
+
tl-diff -j # JSON output
|
|
47
|
+
`;
|
|
24
48
|
|
|
25
49
|
function run(cmd) {
|
|
26
50
|
try {
|
|
@@ -30,15 +54,6 @@ function run(cmd) {
|
|
|
30
54
|
}
|
|
31
55
|
}
|
|
32
56
|
|
|
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
57
|
function parseDiffStat(stat) {
|
|
43
58
|
const lines = stat.trim().split('\n');
|
|
44
59
|
const files = [];
|
|
@@ -96,66 +111,30 @@ function categorizeChanges(files) {
|
|
|
96
111
|
return categories;
|
|
97
112
|
}
|
|
98
113
|
|
|
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
114
|
// Main
|
|
144
115
|
const args = process.argv.slice(2);
|
|
116
|
+
const options = parseCommonArgs(args);
|
|
117
|
+
|
|
118
|
+
// Parse tool-specific options
|
|
145
119
|
let ref = '';
|
|
146
120
|
let staged = false;
|
|
147
121
|
let statOnly = false;
|
|
148
122
|
|
|
149
|
-
for (
|
|
150
|
-
if (
|
|
123
|
+
for (const arg of options.remaining) {
|
|
124
|
+
if (arg === '--staged') {
|
|
151
125
|
staged = true;
|
|
152
|
-
} else if (
|
|
126
|
+
} else if (arg === '--stat-only') {
|
|
153
127
|
statOnly = true;
|
|
154
|
-
} else if (!
|
|
155
|
-
ref =
|
|
128
|
+
} else if (!arg.startsWith('-')) {
|
|
129
|
+
ref = arg;
|
|
156
130
|
}
|
|
157
131
|
}
|
|
158
132
|
|
|
133
|
+
if (options.help) {
|
|
134
|
+
console.log(HELP);
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
|
|
159
138
|
// Build git diff command
|
|
160
139
|
let diffCmd = 'git diff';
|
|
161
140
|
if (staged) {
|
|
@@ -167,17 +146,71 @@ diffCmd += ' --stat=200';
|
|
|
167
146
|
|
|
168
147
|
const stat = run(diffCmd);
|
|
169
148
|
|
|
149
|
+
const out = createOutput(options);
|
|
150
|
+
|
|
170
151
|
if (!stat.trim()) {
|
|
171
|
-
|
|
152
|
+
out.header('No changes detected');
|
|
153
|
+
out.print();
|
|
172
154
|
process.exit(0);
|
|
173
155
|
}
|
|
174
156
|
|
|
175
157
|
const files = parseDiffStat(stat);
|
|
176
158
|
const categories = categorizeChanges(files);
|
|
177
159
|
|
|
178
|
-
|
|
160
|
+
const totalChanges = files.reduce((sum, f) => sum + f.changes, 0);
|
|
161
|
+
const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
|
|
162
|
+
const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
|
163
|
+
|
|
164
|
+
// Set JSON data
|
|
165
|
+
out.setData('files', files);
|
|
166
|
+
out.setData('categories', categories);
|
|
167
|
+
out.setData('totalFiles', files.length);
|
|
168
|
+
out.setData('totalChanges', totalChanges);
|
|
169
|
+
out.setData('estimatedTokens', totalChanges * 4);
|
|
170
|
+
|
|
171
|
+
// Summary header
|
|
172
|
+
out.header('Diff Summary');
|
|
173
|
+
out.header(`${files.length} files changed, ~${formatTokens(totalChanges * 4)} tokens of changes`);
|
|
174
|
+
out.header(`+${totalAdditions} additions, -${totalDeletions} deletions`);
|
|
175
|
+
out.blank();
|
|
179
176
|
|
|
180
177
|
if (!statOnly) {
|
|
181
|
-
|
|
182
|
-
|
|
178
|
+
const order = ['components', 'hooks', 'store', 'types', 'manuscripts', 'tests', 'config', 'other'];
|
|
179
|
+
const labels = {
|
|
180
|
+
components: 'Components',
|
|
181
|
+
hooks: 'Hooks',
|
|
182
|
+
store: 'Store',
|
|
183
|
+
types: 'Types',
|
|
184
|
+
manuscripts: 'Manuscripts',
|
|
185
|
+
tests: 'Tests',
|
|
186
|
+
config: 'Config',
|
|
187
|
+
other: 'Other'
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
for (const cat of order) {
|
|
191
|
+
const catFiles = categories[cat];
|
|
192
|
+
if (catFiles.length === 0) continue;
|
|
193
|
+
|
|
194
|
+
out.add(`${labels[cat]} (${catFiles.length})`);
|
|
195
|
+
|
|
196
|
+
// Sort by changes descending
|
|
197
|
+
catFiles.sort((a, b) => b.changes - a.changes);
|
|
198
|
+
|
|
199
|
+
for (const f of catFiles.slice(0, 10)) {
|
|
200
|
+
const bar = '+'.repeat(Math.min(f.additions, 20)) + '-'.repeat(Math.min(f.deletions, 20));
|
|
201
|
+
out.add(` ${f.path}`);
|
|
202
|
+
out.add(` ${f.changes} changes ${bar}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (catFiles.length > 10) {
|
|
206
|
+
out.add(` ... and ${catFiles.length - 10} more`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
out.blank();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
out.header('Tip: Use --stat-only for just the summary, or check specific files with:');
|
|
213
|
+
out.header(' git diff [ref] -- path/to/file.ts');
|
|
183
214
|
}
|
|
215
|
+
|
|
216
|
+
out.print();
|
package/bin/tl-related.mjs
CHANGED
|
@@ -23,41 +23,37 @@ if (process.argv.includes('--prompt')) {
|
|
|
23
23
|
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
24
24
|
import { join, dirname, basename, relative, extname } from 'path';
|
|
25
25
|
import { execSync } from 'child_process';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
import {
|
|
27
|
+
createOutput,
|
|
28
|
+
parseCommonArgs,
|
|
29
|
+
estimateTokens,
|
|
30
|
+
formatTokens,
|
|
31
|
+
shellEscape,
|
|
32
|
+
COMMON_OPTIONS_HELP
|
|
33
|
+
} from '../src/output.mjs';
|
|
34
|
+
import { findProjectRoot } from '../src/project.mjs';
|
|
35
|
+
|
|
36
|
+
const HELP = `
|
|
37
|
+
tl-related - Find related files (tests, types, usages)
|
|
38
|
+
|
|
39
|
+
Usage: tl-related <file> [options]
|
|
40
|
+
${COMMON_OPTIONS_HELP}
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
tl-related src/Button.tsx # Find tests, types, importers
|
|
44
|
+
tl-related src/api.ts -j # JSON output
|
|
45
|
+
tl-related src/utils.ts -q # Quiet (file paths only)
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
// Check for ripgrep
|
|
49
|
+
try {
|
|
50
|
+
execSync('which rg', { stdio: 'ignore' });
|
|
51
|
+
} catch {
|
|
52
|
+
console.error('ripgrep (rg) not found. Install: brew install ripgrep');
|
|
30
53
|
process.exit(1);
|
|
31
54
|
}
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
'node_modules', '.git', 'android', 'ios', 'dist', 'build', '.expo', '.next'
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
function findProjectRoot() {
|
|
38
|
-
let dir = process.cwd();
|
|
39
|
-
while (dir !== '/') {
|
|
40
|
-
if (existsSync(join(dir, 'package.json'))) return dir;
|
|
41
|
-
dir = dirname(dir);
|
|
42
|
-
}
|
|
43
|
-
return process.cwd();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function estimateTokens(filePath) {
|
|
47
|
-
try {
|
|
48
|
-
const content = readFileSync(filePath, 'utf-8');
|
|
49
|
-
return Math.ceil(content.length / 4);
|
|
50
|
-
} catch {
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function formatTokens(tokens) {
|
|
56
|
-
if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;
|
|
57
|
-
return String(tokens);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function findTestFiles(filePath, projectRoot) {
|
|
56
|
+
function findTestFiles(filePath) {
|
|
61
57
|
const dir = dirname(filePath);
|
|
62
58
|
const name = basename(filePath, extname(filePath));
|
|
63
59
|
const tests = [];
|
|
@@ -66,10 +62,15 @@ function findTestFiles(filePath, projectRoot) {
|
|
|
66
62
|
const patterns = [
|
|
67
63
|
join(dir, `${name}.test.ts`),
|
|
68
64
|
join(dir, `${name}.test.tsx`),
|
|
65
|
+
join(dir, `${name}.test.js`),
|
|
66
|
+
join(dir, `${name}.test.jsx`),
|
|
69
67
|
join(dir, `${name}.spec.ts`),
|
|
70
68
|
join(dir, `${name}.spec.tsx`),
|
|
69
|
+
join(dir, `${name}.spec.js`),
|
|
70
|
+
join(dir, `${name}.spec.jsx`),
|
|
71
71
|
join(dir, '__tests__', `${name}.test.ts`),
|
|
72
72
|
join(dir, '__tests__', `${name}.test.tsx`),
|
|
73
|
+
join(dir, '__tests__', `${name}.test.js`),
|
|
73
74
|
join(dir, '__tests__', `${name}.spec.ts`),
|
|
74
75
|
join(dir, '__tests__', `${name}.spec.tsx`),
|
|
75
76
|
];
|
|
@@ -104,10 +105,12 @@ function findTypeFiles(filePath, projectRoot) {
|
|
|
104
105
|
// Check project-wide types directory
|
|
105
106
|
const globalTypes = join(projectRoot, 'src', 'types');
|
|
106
107
|
if (existsSync(globalTypes)) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
try {
|
|
109
|
+
const typeFiles = readdirSync(globalTypes).filter(f => f.endsWith('.ts'));
|
|
110
|
+
for (const tf of typeFiles.slice(0, 5)) {
|
|
111
|
+
types.push(join(globalTypes, tf));
|
|
112
|
+
}
|
|
113
|
+
} catch { /* permission error */ }
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
return types;
|
|
@@ -115,7 +118,6 @@ function findTypeFiles(filePath, projectRoot) {
|
|
|
115
118
|
|
|
116
119
|
function findImporters(filePath, projectRoot) {
|
|
117
120
|
const name = basename(filePath, extname(filePath));
|
|
118
|
-
|
|
119
121
|
const importers = new Set();
|
|
120
122
|
|
|
121
123
|
// Search for files that might import this module
|
|
@@ -141,7 +143,7 @@ function findImporters(filePath, projectRoot) {
|
|
|
141
143
|
}
|
|
142
144
|
} catch { /* skip unreadable files */ }
|
|
143
145
|
}
|
|
144
|
-
} catch
|
|
146
|
+
} catch { /* rg not found or no matches */ }
|
|
145
147
|
|
|
146
148
|
return Array.from(importers);
|
|
147
149
|
}
|
|
@@ -153,43 +155,48 @@ function findSiblings(filePath) {
|
|
|
153
155
|
try {
|
|
154
156
|
const files = readdirSync(dir).filter(f => {
|
|
155
157
|
const fullPath = join(dir, f);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
try {
|
|
159
|
+
return statSync(fullPath).isFile() &&
|
|
160
|
+
f !== basename(filePath) &&
|
|
161
|
+
!f.includes('.test.') &&
|
|
162
|
+
!f.includes('.spec.') &&
|
|
163
|
+
(f.endsWith('.ts') || f.endsWith('.tsx') || f.endsWith('.js') || f.endsWith('.jsx'));
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
161
167
|
});
|
|
162
168
|
|
|
163
169
|
for (const f of files.slice(0, 5)) {
|
|
164
170
|
siblings.push(join(dir, f));
|
|
165
171
|
}
|
|
166
|
-
} catch
|
|
172
|
+
} catch { /* permission error */ }
|
|
167
173
|
|
|
168
174
|
return siblings;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
function getFileInfo(filePath) {
|
|
178
|
+
try {
|
|
179
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
180
|
+
return {
|
|
181
|
+
tokens: estimateTokens(content),
|
|
182
|
+
lines: content.split('\n').length
|
|
183
|
+
};
|
|
184
|
+
} catch {
|
|
185
|
+
return { tokens: 0, lines: 0 };
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
// Main
|
|
183
190
|
const args = process.argv.slice(2);
|
|
184
|
-
const
|
|
191
|
+
const options = parseCommonArgs(args);
|
|
192
|
+
const targetFile = options.remaining.find(a => !a.startsWith('-'));
|
|
185
193
|
|
|
186
|
-
if (!targetFile) {
|
|
187
|
-
console.log(
|
|
188
|
-
|
|
189
|
-
process.exit(1);
|
|
194
|
+
if (options.help || !targetFile) {
|
|
195
|
+
console.log(HELP);
|
|
196
|
+
process.exit(options.help ? 0 : 1);
|
|
190
197
|
}
|
|
191
198
|
|
|
192
|
-
const fullPath = join(process.cwd(), targetFile);
|
|
199
|
+
const fullPath = targetFile.startsWith('/') ? targetFile : join(process.cwd(), targetFile);
|
|
193
200
|
if (!existsSync(fullPath)) {
|
|
194
201
|
console.error(`File not found: ${targetFile}`);
|
|
195
202
|
process.exit(1);
|
|
@@ -197,31 +204,65 @@ if (!existsSync(fullPath)) {
|
|
|
197
204
|
|
|
198
205
|
const projectRoot = findProjectRoot();
|
|
199
206
|
const relPath = relative(projectRoot, fullPath);
|
|
207
|
+
const out = createOutput(options);
|
|
200
208
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const tests = findTestFiles(fullPath, projectRoot);
|
|
209
|
+
const tests = findTestFiles(fullPath);
|
|
204
210
|
const types = findTypeFiles(fullPath, projectRoot);
|
|
205
211
|
const importers = findImporters(fullPath, projectRoot);
|
|
206
212
|
const siblings = findSiblings(fullPath);
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
// Collect file info for JSON
|
|
215
|
+
const testsInfo = tests.map(f => ({ path: relative(projectRoot, f), ...getFileInfo(f) }));
|
|
216
|
+
const typesInfo = types.map(f => ({ path: relative(projectRoot, f), ...getFileInfo(f) }));
|
|
217
|
+
const importersInfo = importers.slice(0, 10).map(f => ({ path: relative(projectRoot, f), ...getFileInfo(f) }));
|
|
218
|
+
const siblingsInfo = siblings.map(f => ({ path: relative(projectRoot, f), ...getFileInfo(f) }));
|
|
219
|
+
|
|
220
|
+
// Set JSON data
|
|
221
|
+
out.setData('file', relPath);
|
|
222
|
+
out.setData('tests', testsInfo);
|
|
223
|
+
out.setData('types', typesInfo);
|
|
224
|
+
out.setData('importers', importersInfo);
|
|
225
|
+
out.setData('siblings', siblingsInfo);
|
|
226
|
+
out.setData('totalImporters', importers.length);
|
|
227
|
+
|
|
228
|
+
// Header
|
|
229
|
+
out.header(`Related files for: ${relPath}`);
|
|
230
|
+
out.blank();
|
|
231
|
+
|
|
232
|
+
// Sections
|
|
233
|
+
function addSection(title, files) {
|
|
234
|
+
if (files.length === 0) return;
|
|
235
|
+
out.add(title);
|
|
236
|
+
for (const f of files) {
|
|
237
|
+
const rel = relative(projectRoot, f);
|
|
238
|
+
const info = getFileInfo(f);
|
|
239
|
+
out.add(` ${rel} (~${formatTokens(info.tokens)})`);
|
|
240
|
+
}
|
|
241
|
+
out.blank();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
addSection('Tests:', tests);
|
|
245
|
+
addSection('Types:', types);
|
|
246
|
+
addSection('Imported by:', importers.slice(0, 10));
|
|
247
|
+
addSection('Siblings:', siblings);
|
|
212
248
|
|
|
213
249
|
const totalFiles = tests.length + types.length + Math.min(importers.length, 10) + siblings.length;
|
|
214
250
|
if (totalFiles === 0) {
|
|
215
|
-
|
|
251
|
+
out.add(' No related files found.');
|
|
252
|
+
out.blank();
|
|
216
253
|
}
|
|
217
254
|
|
|
218
255
|
// Summary
|
|
219
|
-
const
|
|
220
|
-
|
|
256
|
+
const allFiles = [...tests, ...types, ...importers.slice(0, 10), ...siblings];
|
|
257
|
+
const totalTokens = allFiles.reduce((sum, f) => sum + getFileInfo(f).tokens, 0);
|
|
221
258
|
|
|
222
|
-
|
|
259
|
+
out.setData('totalFiles', totalFiles);
|
|
260
|
+
out.setData('totalTokens', totalTokens);
|
|
261
|
+
|
|
262
|
+
out.header(`Total: ${totalFiles} related files, ~${formatTokens(totalTokens)} tokens`);
|
|
223
263
|
|
|
224
264
|
if (importers.length > 10) {
|
|
225
|
-
|
|
265
|
+
out.header(`(${importers.length - 10} more importers not shown)`);
|
|
226
266
|
}
|
|
227
|
-
|
|
267
|
+
|
|
268
|
+
out.print();
|