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.
@@ -21,16 +21,27 @@ if (process.argv.includes('--prompt')) {
21
21
  }
22
22
 
23
23
  import { readFileSync, existsSync } from 'fs';
24
- import { join, relative, dirname, basename } from 'path';
25
-
26
- function findProjectRoot() {
27
- let dir = process.cwd();
28
- while (dir !== '/') {
29
- if (existsSync(join(dir, 'package.json'))) return dir;
30
- dir = dirname(dir);
31
- }
32
- return process.cwd();
33
- }
24
+ import { join, relative } from 'path';
25
+ import {
26
+ createOutput,
27
+ parseCommonArgs,
28
+ estimateTokens,
29
+ formatTokens,
30
+ COMMON_OPTIONS_HELP
31
+ } from '../src/output.mjs';
32
+ import { findProjectRoot } from '../src/project.mjs';
33
+
34
+ const HELP = `
35
+ tl-component - React component analyzer
36
+
37
+ Usage: tl-component <file.tsx> [options]
38
+ ${COMMON_OPTIONS_HELP}
39
+
40
+ Examples:
41
+ tl-component src/Button.tsx # Analyze component
42
+ tl-component src/App.tsx -j # JSON output
43
+ tl-component src/Modal.tsx -q # Quiet (minimal)
44
+ `;
34
45
 
35
46
  function extractImports(content) {
36
47
  const imports = {
@@ -175,79 +186,14 @@ function extractRedux(content) {
175
186
  return redux;
176
187
  }
177
188
 
178
- function printAnalysis(analysis) {
179
- const { file, lines, tokens, imports, hooks, propsInfo, components, styles, redux } = analysis;
180
-
181
- console.log(`\n🧩 Component Analysis: ${file}`);
182
- console.log(` ${lines} lines, ~${tokens} tokens\n`);
183
-
184
- // Components
185
- if (components.length > 0) {
186
- console.log(`šŸ“¦ Components: ${components.join(', ')}`);
187
- }
188
-
189
- // Props
190
- if (propsInfo) {
191
- console.log(`\nšŸ“‹ ${propsInfo.name}:`);
192
- for (const p of propsInfo.props) {
193
- const opt = p.optional ? '?' : '';
194
- console.log(` ${p.name}${opt}: ${p.type}`);
195
- }
196
- }
197
-
198
- // Hooks
199
- if (hooks.length > 0) {
200
- console.log(`\nšŸŖ Hooks: ${hooks.join(', ')}`);
201
- }
202
-
203
- // Redux
204
- if (redux.dispatch || redux.selectors.length > 0) {
205
- console.log(`\nšŸ“¦ Redux:`);
206
- if (redux.selectors.length > 0) {
207
- console.log(` Selectors: ${redux.selectors.join(', ')}`);
208
- }
209
- if (redux.actions.length > 0) {
210
- console.log(` Actions: ${redux.actions.join(', ')}`);
211
- }
212
- }
213
-
214
- // Imports summary
215
- console.log(`\nšŸ“„ Imports:`);
216
- if (imports.react.length > 0) {
217
- console.log(` React: ${imports.react.join(', ')}`);
218
- }
219
- if (imports.reactNative.length > 0) {
220
- console.log(` React Native: ${imports.reactNative.join(', ')}`);
221
- }
222
- if (imports.internal.length > 0) {
223
- console.log(` Internal: ${imports.internal.length} modules`);
224
- for (const i of imports.internal.slice(0, 5)) {
225
- console.log(` ${i.source}`);
226
- }
227
- if (imports.internal.length > 5) {
228
- console.log(` ... and ${imports.internal.length - 5} more`);
229
- }
230
- }
231
- if (imports.external.length > 0) {
232
- console.log(` External: ${imports.external.map(i => i.source).join(', ')}`);
233
- }
234
-
235
- // Styles
236
- if (styles.length > 0) {
237
- console.log(`\nšŸŽØ Styling: ${styles.join(', ')}`);
238
- }
239
-
240
- console.log();
241
- }
242
-
243
189
  // Main
244
190
  const args = process.argv.slice(2);
245
- const targetFile = args[0];
191
+ const options = parseCommonArgs(args);
192
+ const targetFile = options.remaining.find(a => !a.startsWith('-'));
246
193
 
247
- if (!targetFile) {
248
- console.log('\nUsage: claude-component <file.tsx>\n');
249
- console.log('Analyzes a React component to show props, hooks, and dependencies.');
250
- process.exit(1);
194
+ if (options.help || !targetFile) {
195
+ console.log(HELP);
196
+ process.exit(options.help ? 0 : 1);
251
197
  }
252
198
 
253
199
  const fullPath = targetFile.startsWith('/') ? targetFile : join(process.cwd(), targetFile);
@@ -258,11 +204,12 @@ if (!existsSync(fullPath)) {
258
204
 
259
205
  const content = readFileSync(fullPath, 'utf-8');
260
206
  const projectRoot = findProjectRoot();
207
+ const relPath = relative(projectRoot, fullPath);
261
208
 
262
209
  const analysis = {
263
- file: relative(projectRoot, fullPath),
210
+ file: relPath,
264
211
  lines: content.split('\n').length,
265
- tokens: Math.ceil(content.length / 4),
212
+ tokens: estimateTokens(content),
266
213
  imports: extractImports(content),
267
214
  hooks: extractHooks(content),
268
215
  propsInfo: extractProps(content),
@@ -271,4 +218,83 @@ const analysis = {
271
218
  redux: extractRedux(content)
272
219
  };
273
220
 
274
- printAnalysis(analysis);
221
+ const out = createOutput(options);
222
+
223
+ // Set JSON data
224
+ out.setData('file', analysis.file);
225
+ out.setData('lines', analysis.lines);
226
+ out.setData('tokens', analysis.tokens);
227
+ out.setData('components', analysis.components);
228
+ out.setData('props', analysis.propsInfo);
229
+ out.setData('hooks', analysis.hooks);
230
+ out.setData('imports', analysis.imports);
231
+ out.setData('styles', analysis.styles);
232
+ out.setData('redux', analysis.redux);
233
+
234
+ // Headers
235
+ out.header(`Component Analysis: ${analysis.file}`);
236
+ out.header(`${analysis.lines} lines, ~${formatTokens(analysis.tokens)} tokens`);
237
+ out.blank();
238
+
239
+ // Components
240
+ if (analysis.components.length > 0) {
241
+ out.add(`Components: ${analysis.components.join(', ')}`);
242
+ }
243
+
244
+ // Props
245
+ if (analysis.propsInfo) {
246
+ out.blank();
247
+ out.add(`${analysis.propsInfo.name}:`);
248
+ for (const p of analysis.propsInfo.props) {
249
+ const opt = p.optional ? '?' : '';
250
+ out.add(` ${p.name}${opt}: ${p.type}`);
251
+ }
252
+ }
253
+
254
+ // Hooks
255
+ if (analysis.hooks.length > 0) {
256
+ out.blank();
257
+ out.add(`Hooks: ${analysis.hooks.join(', ')}`);
258
+ }
259
+
260
+ // Redux
261
+ if (analysis.redux.dispatch || analysis.redux.selectors.length > 0) {
262
+ out.blank();
263
+ out.add('Redux:');
264
+ if (analysis.redux.selectors.length > 0) {
265
+ out.add(` Selectors: ${analysis.redux.selectors.join(', ')}`);
266
+ }
267
+ if (analysis.redux.actions.length > 0) {
268
+ out.add(` Actions: ${analysis.redux.actions.join(', ')}`);
269
+ }
270
+ }
271
+
272
+ // Imports summary
273
+ out.blank();
274
+ out.add('Imports:');
275
+ if (analysis.imports.react.length > 0) {
276
+ out.add(` React: ${analysis.imports.react.join(', ')}`);
277
+ }
278
+ if (analysis.imports.reactNative.length > 0) {
279
+ out.add(` React Native: ${analysis.imports.reactNative.join(', ')}`);
280
+ }
281
+ if (analysis.imports.internal.length > 0) {
282
+ out.add(` Internal: ${analysis.imports.internal.length} modules`);
283
+ for (const i of analysis.imports.internal.slice(0, 5)) {
284
+ out.add(` ${i.source}`);
285
+ }
286
+ if (analysis.imports.internal.length > 5) {
287
+ out.add(` ... and ${analysis.imports.internal.length - 5} more`);
288
+ }
289
+ }
290
+ if (analysis.imports.external.length > 0) {
291
+ out.add(` External: ${analysis.imports.external.map(i => i.source).join(', ')}`);
292
+ }
293
+
294
+ // Styles
295
+ if (analysis.styles.length > 0) {
296
+ out.blank();
297
+ out.add(`Styling: ${analysis.styles.join(', ')}`);
298
+ }
299
+
300
+ out.print();
@@ -20,137 +20,186 @@ if (process.argv.includes('--prompt')) {
20
20
 
21
21
  import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
22
22
  import { join, relative } from 'path';
23
+ import {
24
+ createOutput,
25
+ parseCommonArgs,
26
+ estimateTokens,
27
+ formatTokens,
28
+ formatTable,
29
+ COMMON_OPTIONS_HELP
30
+ } from '../src/output.mjs';
31
+ import {
32
+ findProjectRoot,
33
+ shouldSkip,
34
+ getSkipDirs,
35
+ getImportantDirs
36
+ } from '../src/project.mjs';
37
+
38
+ const HELP = `
39
+ tl-context - Estimate context token usage for files/directories
40
+
41
+ Usage: tl-context [path] [options]
42
+
43
+ Options:
44
+ --top N, -n N Show top N files (default: 20, use --all for all)
45
+ --all Show all files
46
+ ${COMMON_OPTIONS_HELP}
47
+
48
+ Examples:
49
+ tl-context src/ # Estimate tokens for src directory
50
+ tl-context src/ --top 10 # Show top 10 largest files
51
+ tl-context src/ --all # Show all files
52
+ tl-context package.json # Single file estimate
53
+ tl-context -j # JSON output for scripting
54
+ `;
55
+
56
+ function analyzeDir(dirPath, results = [], skipDirs, importantDirs) {
57
+ try {
58
+ const entries = readdirSync(dirPath, { withFileTypes: true });
59
+
60
+ for (const entry of entries) {
61
+ if (entry.name.startsWith('.') && !importantDirs.has(entry.name)) continue;
62
+ if (shouldSkip(entry.name, entry.isDirectory())) continue;
63
+
64
+ const fullPath = join(dirPath, entry.name);
65
+
66
+ if (entry.isDirectory()) {
67
+ analyzeDir(fullPath, results, skipDirs, importantDirs);
68
+ } else {
69
+ try {
70
+ const content = readFileSync(fullPath, 'utf-8');
71
+ const tokens = estimateTokens(content);
72
+ results.push({ path: fullPath, tokens, lines: content.split('\n').length });
73
+ } catch {
74
+ // Skip binary or unreadable files
75
+ }
76
+ }
77
+ }
78
+ } catch {
79
+ // Permission error
80
+ }
23
81
 
24
- const SKIP_DIRS = new Set([
25
- 'node_modules', '.git', 'android', 'ios', 'dist', 'build',
26
- '.expo', '.next', 'coverage', '__pycache__', '.cache'
27
- ]);
28
-
29
- const SKIP_EXTENSIONS = new Set([
30
- '.png', '.jpg', '.jpeg', '.gif', '.ico', '.svg', '.webp',
31
- '.woff', '.woff2', '.ttf', '.eot',
32
- '.mp3', '.mp4', '.wav', '.ogg',
33
- '.zip', '.tar', '.gz',
34
- '.lock', '.log'
35
- ]);
36
-
37
- // Rough token estimate: ~4 chars per token for code
38
- function estimateTokens(content) {
39
- return Math.ceil(content.length / 4);
82
+ return results;
40
83
  }
41
84
 
42
- function formatTokens(tokens) {
43
- if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`;
44
- if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`;
45
- return String(tokens);
46
- }
85
+ // Main
86
+ const args = process.argv.slice(2);
87
+ const options = parseCommonArgs(args);
47
88
 
48
- function shouldSkip(name, isDir) {
49
- if (isDir && SKIP_DIRS.has(name)) return true;
50
- if (!isDir) {
51
- const ext = name.substring(name.lastIndexOf('.'));
52
- if (SKIP_EXTENSIONS.has(ext)) return true;
89
+ // Parse tool-specific options
90
+ let topN = 20;
91
+ let targetPath = '.';
92
+
93
+ for (let i = 0; i < options.remaining.length; i++) {
94
+ const arg = options.remaining[i];
95
+ if ((arg === '--top' || arg === '-n') && options.remaining[i + 1]) {
96
+ topN = parseInt(options.remaining[i + 1], 10);
97
+ i++;
98
+ } else if (arg === '--all') {
99
+ topN = null;
100
+ } else if (!arg.startsWith('-')) {
101
+ targetPath = arg;
53
102
  }
54
- return false;
55
103
  }
56
104
 
57
- function analyzeDir(dirPath, results = [], depth = 0) {
58
- const entries = readdirSync(dirPath, { withFileTypes: true });
105
+ if (options.help) {
106
+ console.log(HELP);
107
+ process.exit(0);
108
+ }
59
109
 
60
- for (const entry of entries) {
61
- if (entry.name.startsWith('.') && entry.name !== '.claude') continue;
62
- if (shouldSkip(entry.name, entry.isDirectory())) continue;
110
+ if (!existsSync(targetPath)) {
111
+ console.error(`Path not found: ${targetPath}`);
112
+ process.exit(1);
113
+ }
63
114
 
64
- const fullPath = join(dirPath, entry.name);
115
+ const projectRoot = findProjectRoot();
116
+ const skipDirs = getSkipDirs();
117
+ const importantDirs = getImportantDirs();
118
+ const out = createOutput(options);
65
119
 
66
- if (entry.isDirectory()) {
67
- analyzeDir(fullPath, results, depth + 1);
68
- } else {
69
- try {
70
- const content = readFileSync(fullPath, 'utf-8');
71
- const tokens = estimateTokens(content);
72
- results.push({ path: fullPath, tokens, lines: content.split('\n').length });
73
- } catch (e) {
74
- // Skip binary or unreadable files
75
- }
76
- }
77
- }
120
+ const stat = statSync(targetPath);
121
+ if (stat.isFile()) {
122
+ // Single file
123
+ const content = readFileSync(targetPath, 'utf-8');
124
+ const tokens = estimateTokens(content);
125
+ const lines = content.split('\n').length;
78
126
 
79
- return results;
80
- }
127
+ out.setData('file', targetPath);
128
+ out.setData('tokens', tokens);
129
+ out.setData('lines', lines);
130
+
131
+ out.header(`${targetPath}: ~${formatTokens(tokens)} tokens (${lines} lines)`);
132
+ out.print();
133
+ } else {
134
+ // Directory
135
+ const results = analyzeDir(targetPath, [], skipDirs, importantDirs);
81
136
 
82
- function printResults(results, rootPath, topN) {
83
137
  // Sort by tokens descending
84
138
  results.sort((a, b) => b.tokens - a.tokens);
85
139
 
86
140
  const total = results.reduce((sum, r) => sum + r.tokens, 0);
141
+ const totalLines = results.reduce((sum, r) => sum + r.lines, 0);
87
142
 
88
- console.log(`\nšŸ“Š Context Estimate for: ${rootPath}\n`);
89
- console.log(`Total: ~${formatTokens(total)} tokens across ${results.length} files\n`);
90
-
91
- if (topN) {
92
- console.log(`Top ${topN} largest files:\n`);
93
- results = results.slice(0, topN);
94
- }
143
+ // Set JSON data
144
+ out.setData('path', targetPath);
145
+ out.setData('totalTokens', total);
146
+ out.setData('totalLines', totalLines);
147
+ out.setData('fileCount', results.length);
95
148
 
96
- const maxPathLen = Math.min(60, Math.max(...results.map(r => relative(rootPath, r.path).length)));
149
+ // Header
150
+ out.header(`Context Estimate: ${targetPath}`);
151
+ out.header(`Total: ~${formatTokens(total)} tokens across ${results.length} files`);
152
+ out.blank();
97
153
 
98
- console.log(' Tokens Lines Path');
99
- console.log(' ' + '-'.repeat(maxPathLen + 20));
154
+ // File list
155
+ const displayResults = topN ? results.slice(0, topN) : results;
100
156
 
101
- for (const r of results) {
102
- const relPath = relative(rootPath, r.path);
103
- const truncPath = relPath.length > 60 ? '...' + relPath.slice(-57) : relPath;
104
- console.log(` ${formatTokens(r.tokens).padStart(6)} ${String(r.lines).padStart(5)} ${truncPath}`);
157
+ if (displayResults.length > 0) {
158
+ if (topN) {
159
+ out.header(`Top ${Math.min(topN, results.length)} largest files:`);
160
+ }
161
+ out.blank();
162
+
163
+ // Format as table
164
+ const rows = displayResults.map(r => {
165
+ const relPath = relative(targetPath, r.path);
166
+ const truncPath = relPath.length > 60 ? '...' + relPath.slice(-57) : relPath;
167
+ return [formatTokens(r.tokens), r.lines, truncPath];
168
+ });
169
+
170
+ const tableLines = formatTable(rows, { indent: ' ', separator: ' ' });
171
+ out.add(' Tokens Lines Path');
172
+ out.add(' ' + '-'.repeat(70));
173
+ out.addLines(tableLines);
105
174
  }
106
175
 
107
- console.log();
108
-
109
176
  // Group by directory
110
177
  const byDir = {};
111
178
  for (const r of results) {
112
- const rel = relative(rootPath, r.path);
179
+ const rel = relative(targetPath, r.path);
113
180
  const dir = rel.includes('/') ? rel.split('/')[0] : '.';
114
181
  byDir[dir] = (byDir[dir] || 0) + r.tokens;
115
182
  }
116
183
 
117
184
  const sortedDirs = Object.entries(byDir).sort((a, b) => b[1] - a[1]);
118
185
 
119
- console.log('By top-level directory:\n');
120
- for (const [dir, tokens] of sortedDirs.slice(0, 10)) {
121
- const pct = ((tokens / total) * 100).toFixed(1);
122
- console.log(` ${formatTokens(tokens).padStart(6)} ${pct.padStart(5)}% ${dir}/`);
123
- }
124
- console.log();
125
- }
186
+ out.blank();
187
+ out.header('By top-level directory:');
188
+ out.blank();
126
189
 
127
- // Main
128
- const args = process.argv.slice(2);
129
- let targetPath = '.';
130
- let topN = 20;
190
+ const dirRows = sortedDirs.slice(0, 10).map(([dir, tokens]) => {
191
+ const pct = ((tokens / total) * 100).toFixed(1) + '%';
192
+ return [formatTokens(tokens), pct, dir + '/'];
193
+ });
131
194
 
132
- for (let i = 0; i < args.length; i++) {
133
- if (args[i] === '--top' && args[i + 1]) {
134
- topN = parseInt(args[i + 1], 10);
135
- i++;
136
- } else if (args[i] === '--all') {
137
- topN = null;
138
- } else if (!args[i].startsWith('-')) {
139
- targetPath = args[i];
140
- }
141
- }
195
+ out.addLines(formatTable(dirRows, { indent: ' ', separator: ' ' }));
142
196
 
143
- if (!existsSync(targetPath)) {
144
- console.error(`Path not found: ${targetPath}`);
145
- process.exit(1);
146
- }
197
+ out.setData('byDirectory', Object.fromEntries(sortedDirs));
198
+ out.setData('files', results.slice(0, 100).map(r => ({
199
+ path: relative(targetPath, r.path),
200
+ tokens: r.tokens,
201
+ lines: r.lines
202
+ })));
147
203
 
148
- const stat = statSync(targetPath);
149
- if (stat.isFile()) {
150
- const content = readFileSync(targetPath, 'utf-8');
151
- const tokens = estimateTokens(content);
152
- console.log(`\n${targetPath}: ~${formatTokens(tokens)} tokens (${content.split('\n').length} lines)\n`);
153
- } else {
154
- const results = analyzeDir(targetPath);
155
- printResults(results, targetPath, topN);
204
+ out.print();
156
205
  }