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-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 (let i = 0; i < args.length; i++) {
150
- if (args[i] === '--staged') {
123
+ for (const arg of options.remaining) {
124
+ if (arg === '--staged') {
151
125
  staged = true;
152
- } else if (args[i] === '--stat-only') {
126
+ } else if (arg === '--stat-only') {
153
127
  statOnly = true;
154
- } else if (!args[i].startsWith('-')) {
155
- ref = args[i];
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
- console.log('\nโœจ No changes detected\n');
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
- printSummary(files, categories, { statOnly });
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
- 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');
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();
@@ -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 { shellEscape } from '../src/output.mjs';
27
-
28
- try { execSync('which rg', { stdio: 'ignore' }); } catch {
29
- console.error('โš ๏ธ ripgrep (rg) not found. Install: brew install ripgrep');
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
- const SKIP_DIRS = new Set([
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
- const typeFiles = readdirSync(globalTypes).filter(f => f.endsWith('.ts'));
108
- for (const tf of typeFiles.slice(0, 5)) {
109
- types.push(join(globalTypes, tf));
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 (e) { /* rg not found or no matches */ }
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
- return statSync(fullPath).isFile() &&
157
- f !== basename(filePath) &&
158
- !f.includes('.test.') &&
159
- !f.includes('.spec.') &&
160
- (f.endsWith('.ts') || f.endsWith('.tsx'));
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 (e) { /* permission error */ }
172
+ } catch { /* permission error */ }
167
173
 
168
174
  return siblings;
169
175
  }
170
176
 
171
- function printSection(title, files, projectRoot) {
172
- if (files.length === 0) return;
173
-
174
- console.log(`\n${title}`);
175
- for (const f of files) {
176
- const rel = relative(projectRoot, f);
177
- const tokens = estimateTokens(f);
178
- console.log(` ${rel} (~${formatTokens(tokens)})`);
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 targetFile = args[0];
191
+ const options = parseCommonArgs(args);
192
+ const targetFile = options.remaining.find(a => !a.startsWith('-'));
185
193
 
186
- if (!targetFile) {
187
- console.log('\nUsage: claude-related <file>\n');
188
- console.log('Finds tests, types, and importers for a given file.');
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
- console.log(`\n๐Ÿ“Ž Related files for: ${relPath}`);
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
- printSection('๐Ÿงช Tests', tests, projectRoot);
209
- printSection('๐Ÿ“ Types', types, projectRoot);
210
- printSection('๐Ÿ“ฅ Imported by', importers.slice(0, 10), projectRoot);
211
- printSection('๐Ÿ‘ฅ Siblings', siblings, projectRoot);
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
- console.log('\n No related files found.');
251
+ out.add(' No related files found.');
252
+ out.blank();
216
253
  }
217
254
 
218
255
  // Summary
219
- const totalTokens = [...tests, ...types, ...importers.slice(0, 10), ...siblings]
220
- .reduce((sum, f) => sum + estimateTokens(f), 0);
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
- console.log(`\n๐Ÿ“Š Total: ${totalFiles} related files, ~${formatTokens(totalTokens)} tokens`);
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
- console.log(` (${importers.length - 10} more importers not shown)`);
265
+ out.header(`(${importers.length - 10} more importers not shown)`);
226
266
  }
227
- console.log();
267
+
268
+ out.print();