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-todo.mjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-todo - Extract TODOs, FIXMEs, and other markers from codebase
|
|
5
|
+
*
|
|
6
|
+
* Quickly find all task markers in your code. Helps prioritize work
|
|
7
|
+
* and find forgotten tasks without reading entire files.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-todo [path]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-todo',
|
|
16
|
+
desc: 'Find TODOs, FIXMEs in codebase',
|
|
17
|
+
when: 'search',
|
|
18
|
+
example: 'tl-todo --priority'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { execSync } from 'child_process';
|
|
24
|
+
import { existsSync, readFileSync } from 'fs';
|
|
25
|
+
import { basename, dirname, relative, resolve } from 'path';
|
|
26
|
+
import {
|
|
27
|
+
createOutput,
|
|
28
|
+
parseCommonArgs,
|
|
29
|
+
estimateTokens,
|
|
30
|
+
formatTokens,
|
|
31
|
+
shellEscape,
|
|
32
|
+
COMMON_OPTIONS_HELP
|
|
33
|
+
} from '../src/output.mjs';
|
|
34
|
+
import { findProjectRoot, shouldSkip, SKIP_DIRS } from '../src/project.mjs';
|
|
35
|
+
|
|
36
|
+
const HELP = `
|
|
37
|
+
tl-todo - Extract TODOs, FIXMEs, and other markers from codebase
|
|
38
|
+
|
|
39
|
+
Usage: tl-todo [path] [options]
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--type T, -t T Filter by type (todo, fixme, hack, xxx, note)
|
|
43
|
+
--author A Filter by author (e.g., "@john")
|
|
44
|
+
--priority, -p Sort by priority (FIXME > TODO > HACK > NOTE)
|
|
45
|
+
--context N, -c N Show N lines of context (default: 0)
|
|
46
|
+
${COMMON_OPTIONS_HELP}
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
tl-todo # All markers in project
|
|
50
|
+
tl-todo src/ # Only in src/
|
|
51
|
+
tl-todo -t fixme # Only FIXMEs
|
|
52
|
+
tl-todo -p # Sort by priority
|
|
53
|
+
tl-todo -c 2 # Show 2 lines of context
|
|
54
|
+
|
|
55
|
+
Markers detected:
|
|
56
|
+
š“ FIXME - Bugs or critical issues
|
|
57
|
+
š” TODO - Tasks to complete
|
|
58
|
+
š HACK - Temporary workarounds
|
|
59
|
+
āŖ XXX - Warnings/concerns
|
|
60
|
+
šµ NOTE - Important information
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
// Marker types with priority (lower = higher priority)
|
|
64
|
+
const MARKERS = {
|
|
65
|
+
FIXME: { emoji: 'š“', priority: 1 },
|
|
66
|
+
FIX: { emoji: 'š“', priority: 1 },
|
|
67
|
+
BUG: { emoji: 'š“', priority: 1 },
|
|
68
|
+
TODO: { emoji: 'š”', priority: 2 },
|
|
69
|
+
HACK: { emoji: 'š ', priority: 3 },
|
|
70
|
+
WORKAROUND: { emoji: 'š ', priority: 3 },
|
|
71
|
+
XXX: { emoji: 'āŖ', priority: 4 },
|
|
72
|
+
WARN: { emoji: 'āŖ', priority: 4 },
|
|
73
|
+
NOTE: { emoji: 'šµ', priority: 5 },
|
|
74
|
+
INFO: { emoji: 'šµ', priority: 5 },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
78
|
+
// Todo Extraction
|
|
79
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
80
|
+
|
|
81
|
+
function findTodos(searchPath, projectRoot) {
|
|
82
|
+
const todos = [];
|
|
83
|
+
const markerPattern = Object.keys(MARKERS).join('|');
|
|
84
|
+
|
|
85
|
+
// Comment prefixes to detect actual comment lines
|
|
86
|
+
// Must be preceded by comment syntax or be in a comment context
|
|
87
|
+
const commentPrefixes = [
|
|
88
|
+
'//', // C-style line comments
|
|
89
|
+
'#', // Shell, Python, Ruby
|
|
90
|
+
'--', // SQL, Lua, Haskell
|
|
91
|
+
'\\*', // Inside /* */ blocks
|
|
92
|
+
'<!--', // HTML comments
|
|
93
|
+
'\\{#', // Jinja/Django templates (escaped brace)
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Build exclude patterns for ripgrep
|
|
98
|
+
const excludes = [...SKIP_DIRS].map(d => `--glob=!${d}`).join(' ');
|
|
99
|
+
|
|
100
|
+
// Search for markers that look like they're in comments
|
|
101
|
+
// Pattern: comment prefix followed by optional whitespace, then marker with colon/parens
|
|
102
|
+
// Require : or ( after marker to avoid false positives like "Todo Extraction"
|
|
103
|
+
const commentPattern = `(${commentPrefixes.join('|')})\\s*(${markerPattern})[:(]`;
|
|
104
|
+
const cmd = `rg -n --no-heading -i "${commentPattern}" "${shellEscape(searchPath)}" ${excludes} 2>/dev/null || true`;
|
|
105
|
+
const output = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
|
106
|
+
|
|
107
|
+
if (!output.trim()) {
|
|
108
|
+
return todos;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const lines = output.trim().split('\n');
|
|
112
|
+
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
// Format: file:line:content
|
|
115
|
+
const match = line.match(/^([^:]+):(\d+):(.*)$/);
|
|
116
|
+
if (!match) continue;
|
|
117
|
+
|
|
118
|
+
const [, file, lineNum, content] = match;
|
|
119
|
+
|
|
120
|
+
// Skip if file should be skipped
|
|
121
|
+
if (shouldSkip(basename(file), false)) continue;
|
|
122
|
+
|
|
123
|
+
// Extract marker type and message
|
|
124
|
+
const markerMatch = content.match(new RegExp(`(${markerPattern})[:\\s(]+(.*)`, 'i'));
|
|
125
|
+
if (!markerMatch) continue;
|
|
126
|
+
|
|
127
|
+
const type = markerMatch[1].toUpperCase();
|
|
128
|
+
let message = markerMatch[2].trim();
|
|
129
|
+
|
|
130
|
+
// Clean up common comment endings
|
|
131
|
+
message = message.replace(/\*\/\s*$/, '').replace(/-->\s*$/, '').replace(/\)\s*$/, '').trim();
|
|
132
|
+
|
|
133
|
+
// Skip if message is too short or looks like code/section header
|
|
134
|
+
if (message.length < 3) continue;
|
|
135
|
+
if (/^[{(\[;,=]/.test(message)) continue;
|
|
136
|
+
if (/^ā+$/.test(message)) continue; // Section dividers
|
|
137
|
+
|
|
138
|
+
// Extract author if present: TODO(@author): message or TODO(author): message
|
|
139
|
+
// Only match if explicitly has @ or parens around author name
|
|
140
|
+
let author = null;
|
|
141
|
+
const authorMatch = message.match(/^[@(](\w+)\)?[:\s]+(.+)$/);
|
|
142
|
+
if (authorMatch && authorMatch[1].length < 15 && authorMatch[2].length > 5) {
|
|
143
|
+
author = authorMatch[1];
|
|
144
|
+
message = authorMatch[2];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const relFile = relative(projectRoot, resolve(searchPath, file));
|
|
148
|
+
|
|
149
|
+
todos.push({
|
|
150
|
+
file: relFile,
|
|
151
|
+
line: parseInt(lineNum, 10),
|
|
152
|
+
type: MARKERS[type] ? type : 'TODO',
|
|
153
|
+
message: message.substring(0, 200),
|
|
154
|
+
author,
|
|
155
|
+
priority: MARKERS[type]?.priority || 2
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// ripgrep error
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return todos;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getContext(file, lineNum, contextLines, projectRoot) {
|
|
166
|
+
if (contextLines <= 0) return null;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const fullPath = resolve(projectRoot, file);
|
|
170
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
171
|
+
const lines = content.split('\n');
|
|
172
|
+
|
|
173
|
+
const start = Math.max(0, lineNum - 1 - contextLines);
|
|
174
|
+
const end = Math.min(lines.length, lineNum + contextLines);
|
|
175
|
+
|
|
176
|
+
return lines.slice(start, end).map((l, i) => ({
|
|
177
|
+
num: start + i + 1,
|
|
178
|
+
content: l.substring(0, 120),
|
|
179
|
+
isTodo: start + i + 1 === lineNum
|
|
180
|
+
}));
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
187
|
+
// Output
|
|
188
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
189
|
+
|
|
190
|
+
function formatTodos(todos, out, contextLines, projectRoot) {
|
|
191
|
+
// Group by file
|
|
192
|
+
const byFile = new Map();
|
|
193
|
+
for (const todo of todos) {
|
|
194
|
+
if (!byFile.has(todo.file)) {
|
|
195
|
+
byFile.set(todo.file, []);
|
|
196
|
+
}
|
|
197
|
+
byFile.get(todo.file).push(todo);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (const [file, fileTodos] of byFile) {
|
|
201
|
+
out.add(`š ${file}`);
|
|
202
|
+
|
|
203
|
+
for (const todo of fileTodos) {
|
|
204
|
+
const marker = MARKERS[todo.type] || MARKERS.TODO;
|
|
205
|
+
let line = ` ${marker.emoji} L${todo.line}: ${todo.message}`;
|
|
206
|
+
if (todo.author) {
|
|
207
|
+
line += ` (@${todo.author})`;
|
|
208
|
+
}
|
|
209
|
+
out.add(line);
|
|
210
|
+
|
|
211
|
+
// Add context if requested
|
|
212
|
+
if (contextLines > 0) {
|
|
213
|
+
const context = getContext(file, todo.line, contextLines, projectRoot);
|
|
214
|
+
if (context) {
|
|
215
|
+
for (const ctx of context) {
|
|
216
|
+
const prefix = ctx.isTodo ? ' ā ' : ' ';
|
|
217
|
+
out.add(` ${ctx.num.toString().padStart(4)}${prefix}${ctx.content}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
out.blank();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatSummary(todos, out) {
|
|
227
|
+
const counts = {};
|
|
228
|
+
for (const marker of Object.keys(MARKERS)) {
|
|
229
|
+
counts[marker] = 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const todo of todos) {
|
|
233
|
+
counts[todo.type] = (counts[todo.type] || 0) + 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const parts = [];
|
|
237
|
+
for (const [type, count] of Object.entries(counts)) {
|
|
238
|
+
if (count > 0) {
|
|
239
|
+
parts.push(`${MARKERS[type].emoji} ${count} ${type}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return parts.join(' ');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
247
|
+
// Main
|
|
248
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
249
|
+
|
|
250
|
+
const args = process.argv.slice(2);
|
|
251
|
+
const options = parseCommonArgs(args);
|
|
252
|
+
|
|
253
|
+
// Parse tool-specific options
|
|
254
|
+
let filterType = null;
|
|
255
|
+
let filterAuthor = null;
|
|
256
|
+
let sortByPriority = false;
|
|
257
|
+
let contextLines = 0;
|
|
258
|
+
|
|
259
|
+
const consumedIndices = new Set();
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
262
|
+
const arg = options.remaining[i];
|
|
263
|
+
if ((arg === '--type' || arg === '-t') && options.remaining[i + 1]) {
|
|
264
|
+
filterType = options.remaining[i + 1].toUpperCase();
|
|
265
|
+
consumedIndices.add(i);
|
|
266
|
+
consumedIndices.add(i + 1);
|
|
267
|
+
i++;
|
|
268
|
+
} else if (arg === '--author' && options.remaining[i + 1]) {
|
|
269
|
+
filterAuthor = options.remaining[i + 1].replace(/^@/, '');
|
|
270
|
+
consumedIndices.add(i);
|
|
271
|
+
consumedIndices.add(i + 1);
|
|
272
|
+
i++;
|
|
273
|
+
} else if (arg === '--priority' || arg === '-p') {
|
|
274
|
+
sortByPriority = true;
|
|
275
|
+
consumedIndices.add(i);
|
|
276
|
+
} else if ((arg === '--context' || arg === '-c') && options.remaining[i + 1]) {
|
|
277
|
+
contextLines = parseInt(options.remaining[i + 1], 10);
|
|
278
|
+
consumedIndices.add(i);
|
|
279
|
+
consumedIndices.add(i + 1);
|
|
280
|
+
i++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const targetPath = options.remaining.find((a, i) => !a.startsWith('-') && !consumedIndices.has(i)) || '.';
|
|
285
|
+
|
|
286
|
+
if (options.help) {
|
|
287
|
+
console.log(HELP);
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const projectRoot = findProjectRoot();
|
|
292
|
+
const resolvedPath = resolve(targetPath);
|
|
293
|
+
const relPath = relative(projectRoot, resolvedPath) || '.';
|
|
294
|
+
|
|
295
|
+
if (!existsSync(resolvedPath)) {
|
|
296
|
+
console.error(`Path not found: ${targetPath}`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const out = createOutput(options);
|
|
301
|
+
|
|
302
|
+
out.header(`\nš TODOs: ${relPath === '.' ? basename(projectRoot) : relPath}`);
|
|
303
|
+
|
|
304
|
+
let todos = findTodos(resolvedPath, projectRoot);
|
|
305
|
+
|
|
306
|
+
// Apply filters
|
|
307
|
+
if (filterType) {
|
|
308
|
+
todos = todos.filter(t => t.type === filterType);
|
|
309
|
+
}
|
|
310
|
+
if (filterAuthor) {
|
|
311
|
+
todos = todos.filter(t => t.author && t.author.toLowerCase() === filterAuthor.toLowerCase());
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Sort
|
|
315
|
+
if (sortByPriority) {
|
|
316
|
+
todos.sort((a, b) => a.priority - b.priority || a.file.localeCompare(b.file));
|
|
317
|
+
} else {
|
|
318
|
+
todos.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (todos.length === 0) {
|
|
322
|
+
out.header(' No markers found! āØ');
|
|
323
|
+
out.print();
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
out.header(` ${todos.length} markers found`);
|
|
328
|
+
out.blank();
|
|
329
|
+
|
|
330
|
+
formatTodos(todos, out, contextLines, projectRoot);
|
|
331
|
+
|
|
332
|
+
out.stats('ā'.repeat(50));
|
|
333
|
+
out.stats(`š ${formatSummary(todos, out)}`);
|
|
334
|
+
out.blank();
|
|
335
|
+
|
|
336
|
+
// JSON data
|
|
337
|
+
out.setData('path', relPath);
|
|
338
|
+
out.setData('count', todos.length);
|
|
339
|
+
out.setData('todos', todos);
|
|
340
|
+
|
|
341
|
+
out.print();
|