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-entry.mjs
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-entry - Find entry points in a codebase
|
|
5
|
+
*
|
|
6
|
+
* Locates main functions, route handlers, event listeners,
|
|
7
|
+
* and exported APIs - the "starting points" of the code.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-entry [path]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-entry',
|
|
16
|
+
desc: 'Find entry points (main, routes, handlers)',
|
|
17
|
+
when: 'before-read',
|
|
18
|
+
example: 'tl-entry src/'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { execSync } from 'child_process';
|
|
24
|
+
import { existsSync, readFileSync } from 'fs';
|
|
25
|
+
import { relative, resolve } from 'path';
|
|
26
|
+
import {
|
|
27
|
+
createOutput,
|
|
28
|
+
parseCommonArgs,
|
|
29
|
+
shellEscape,
|
|
30
|
+
COMMON_OPTIONS_HELP
|
|
31
|
+
} from '../src/output.mjs';
|
|
32
|
+
import { findProjectRoot } from '../src/project.mjs';
|
|
33
|
+
|
|
34
|
+
const HELP = `
|
|
35
|
+
tl-entry - Find entry points in a codebase
|
|
36
|
+
|
|
37
|
+
Usage: tl-entry [path] [options]
|
|
38
|
+
|
|
39
|
+
Options:
|
|
40
|
+
--type T, -t T Filter by type: main, routes, handlers, exports, cli
|
|
41
|
+
${COMMON_OPTIONS_HELP}
|
|
42
|
+
|
|
43
|
+
Entry point types:
|
|
44
|
+
main - Main functions, index files, app entry
|
|
45
|
+
routes - HTTP route handlers (Express, Fastify, etc.)
|
|
46
|
+
handlers - Event handlers, callbacks, listeners
|
|
47
|
+
exports - Public API (export default, module.exports)
|
|
48
|
+
cli - CLI entry points (bin scripts, commander)
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
tl-entry # All entry points in project
|
|
52
|
+
tl-entry src/ # Entry points in src/
|
|
53
|
+
tl-entry --type routes # Only route handlers
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
// ─────────────────────────────────────────────────────────────
|
|
57
|
+
// Entry Point Patterns
|
|
58
|
+
// ─────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const PATTERNS = {
|
|
61
|
+
main: {
|
|
62
|
+
label: 'Main / App Entry',
|
|
63
|
+
patterns: [
|
|
64
|
+
{ pattern: 'createApp\\(|new App\\(|express\\(\\)', desc: 'App initialization' },
|
|
65
|
+
{ pattern: '^\\s*app\\.listen\\(', desc: 'Server start' },
|
|
66
|
+
{ pattern: 'ReactDOM\\.render|createRoot|hydrateRoot', desc: 'React entry' },
|
|
67
|
+
{ pattern: 'main\\s*\\(|async function main', desc: 'Main function' },
|
|
68
|
+
],
|
|
69
|
+
files: ['index.ts', 'index.tsx', 'index.js', 'main.ts', 'main.tsx', 'app.ts', 'app.tsx', 'server.ts', 'server.js']
|
|
70
|
+
},
|
|
71
|
+
routes: {
|
|
72
|
+
label: 'Route Handlers',
|
|
73
|
+
patterns: [
|
|
74
|
+
{ pattern: 'app\\.(get|post|put|delete|patch)\\s*\\(', desc: 'Express routes' },
|
|
75
|
+
{ pattern: 'router\\.(get|post|put|delete|patch)\\s*\\(', desc: 'Router routes' },
|
|
76
|
+
{ pattern: 'fastify\\.(get|post|put|delete|patch)', desc: 'Fastify routes' },
|
|
77
|
+
{ pattern: '@(Get|Post|Put|Delete|Patch)\\(', desc: 'Decorator routes' },
|
|
78
|
+
{ pattern: 'export (async )?function (GET|POST|PUT|DELETE|PATCH)', desc: 'Next.js API routes' },
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
handlers: {
|
|
82
|
+
label: 'Event Handlers',
|
|
83
|
+
patterns: [
|
|
84
|
+
{ pattern: '\\.on\\([\'"]\\w+[\'"]', desc: 'Event listeners' },
|
|
85
|
+
{ pattern: '\\.addEventListener\\(', desc: 'DOM events' },
|
|
86
|
+
{ pattern: 'onClick|onChange|onSubmit|onLoad|onError', desc: 'React handlers' },
|
|
87
|
+
{ pattern: 'useEffect\\s*\\(', desc: 'React effects' },
|
|
88
|
+
{ pattern: '@Subscribe|@EventHandler|@Listener', desc: 'Event decorators' },
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
exports: {
|
|
92
|
+
label: 'Public API',
|
|
93
|
+
patterns: [
|
|
94
|
+
{ pattern: '^export default', desc: 'Default export' },
|
|
95
|
+
{ pattern: '^export (async )?(function|class|const) \\w+', desc: 'Named export' },
|
|
96
|
+
{ pattern: 'module\\.exports\\s*=', desc: 'CommonJS export' },
|
|
97
|
+
{ pattern: 'exports\\.\\w+\\s*=', desc: 'Named CommonJS' },
|
|
98
|
+
]
|
|
99
|
+
},
|
|
100
|
+
cli: {
|
|
101
|
+
label: 'CLI Entry Points',
|
|
102
|
+
patterns: [
|
|
103
|
+
{ pattern: '#!/usr/bin/env node', desc: 'Node CLI shebang' },
|
|
104
|
+
{ pattern: 'commander|yargs|meow|arg\\s*\\(', desc: 'CLI framework' },
|
|
105
|
+
{ pattern: 'process\\.argv', desc: 'Argument parsing' },
|
|
106
|
+
{ pattern: '\\.command\\(', desc: 'Command definition' },
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────────────────────────
|
|
112
|
+
// Entry Point Finding
|
|
113
|
+
// ─────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
function findEntryPoints(searchPath, projectRoot, filterType) {
|
|
116
|
+
const results = {};
|
|
117
|
+
|
|
118
|
+
const types = filterType ? [filterType] : Object.keys(PATTERNS);
|
|
119
|
+
|
|
120
|
+
for (const type of types) {
|
|
121
|
+
const config = PATTERNS[type];
|
|
122
|
+
if (!config) continue;
|
|
123
|
+
|
|
124
|
+
results[type] = {
|
|
125
|
+
label: config.label,
|
|
126
|
+
entries: []
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Search by patterns
|
|
130
|
+
for (const { pattern, desc } of config.patterns) {
|
|
131
|
+
try {
|
|
132
|
+
const cmd = `rg -n -g "*.{ts,tsx,js,jsx,mjs}" --no-heading -e "${shellEscape(pattern)}" "${shellEscape(searchPath)}" 2>/dev/null || true`;
|
|
133
|
+
const output = execSync(cmd, { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
|
|
134
|
+
|
|
135
|
+
for (const line of output.trim().split('\n')) {
|
|
136
|
+
if (!line) continue;
|
|
137
|
+
const match = line.match(/^([^:]+):(\d+):(.*)$/);
|
|
138
|
+
if (!match) continue;
|
|
139
|
+
|
|
140
|
+
const [, file, lineNum, content] = match;
|
|
141
|
+
if (file.includes('node_modules')) continue;
|
|
142
|
+
if (file.includes('.test.') || file.includes('.spec.')) continue;
|
|
143
|
+
|
|
144
|
+
results[type].entries.push({
|
|
145
|
+
file: relative(projectRoot, file),
|
|
146
|
+
line: parseInt(lineNum, 10),
|
|
147
|
+
desc,
|
|
148
|
+
content: content.trim().slice(0, 60)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// rg error
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Search for special files
|
|
157
|
+
if (config.files) {
|
|
158
|
+
for (const fileName of config.files) {
|
|
159
|
+
try {
|
|
160
|
+
const cmd = `find "${shellEscape(searchPath)}" -name "${fileName}" -not -path "*/node_modules/*" 2>/dev/null || true`;
|
|
161
|
+
const output = execSync(cmd, { encoding: 'utf-8' });
|
|
162
|
+
|
|
163
|
+
for (const file of output.trim().split('\n')) {
|
|
164
|
+
if (!file) continue;
|
|
165
|
+
results[type].entries.push({
|
|
166
|
+
file: relative(projectRoot, file),
|
|
167
|
+
line: 1,
|
|
168
|
+
desc: 'Entry file',
|
|
169
|
+
content: fileName
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
// find error
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Dedupe entries by file+line
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
results[type].entries = results[type].entries.filter(e => {
|
|
181
|
+
const key = `${e.file}:${e.line}`;
|
|
182
|
+
if (seen.has(key)) return false;
|
|
183
|
+
seen.add(key);
|
|
184
|
+
return true;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─────────────────────────────────────────────────────────────
|
|
192
|
+
// Main
|
|
193
|
+
// ─────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
const args = process.argv.slice(2);
|
|
196
|
+
const options = parseCommonArgs(args);
|
|
197
|
+
|
|
198
|
+
if (options.help) {
|
|
199
|
+
console.log(HELP);
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Parse tool-specific options
|
|
204
|
+
let filterType = null;
|
|
205
|
+
let searchPath = '.';
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
208
|
+
const arg = options.remaining[i];
|
|
209
|
+
if ((arg === '--type' || arg === '-t') && options.remaining[i + 1]) {
|
|
210
|
+
filterType = options.remaining[++i];
|
|
211
|
+
} else if (!arg.startsWith('-')) {
|
|
212
|
+
searchPath = arg;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const projectRoot = findProjectRoot();
|
|
217
|
+
const resolvedPath = resolve(searchPath);
|
|
218
|
+
|
|
219
|
+
if (!existsSync(resolvedPath)) {
|
|
220
|
+
console.error(`Path not found: ${searchPath}`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const out = createOutput(options);
|
|
225
|
+
|
|
226
|
+
out.header(`\n🚪 Entry points: ${searchPath === '.' ? 'project' : searchPath}`);
|
|
227
|
+
|
|
228
|
+
const results = findEntryPoints(resolvedPath, projectRoot, filterType);
|
|
229
|
+
|
|
230
|
+
let totalEntries = 0;
|
|
231
|
+
|
|
232
|
+
for (const [type, { label, entries }] of Object.entries(results)) {
|
|
233
|
+
if (entries.length === 0) continue;
|
|
234
|
+
|
|
235
|
+
totalEntries += entries.length;
|
|
236
|
+
|
|
237
|
+
out.add(`\n${label}:`);
|
|
238
|
+
|
|
239
|
+
for (const entry of entries.slice(0, 10)) {
|
|
240
|
+
out.add(` ${entry.file}:${entry.line} (${entry.desc})`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (entries.length > 10) {
|
|
244
|
+
out.add(` ... and ${entries.length - 10} more`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (totalEntries === 0) {
|
|
249
|
+
out.add('\n No entry points found.');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
out.add('');
|
|
253
|
+
out.stats(`📊 Found ${totalEntries} entry points`);
|
|
254
|
+
out.add('');
|
|
255
|
+
|
|
256
|
+
out.print();
|
package/bin/tl-env.mjs
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-env - Find environment variables and config used in the codebase
|
|
5
|
+
*
|
|
6
|
+
* Scans source files for process.env, import.meta.env, getenv(), os.environ,
|
|
7
|
+
* and similar patterns. Also finds .env files and config references.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-env [dir] [--show-files]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-env',
|
|
16
|
+
desc: 'Find environment variables used in codebase',
|
|
17
|
+
when: 'before-read',
|
|
18
|
+
example: 'tl-env src/'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
24
|
+
import { basename, extname, join, relative } from 'path';
|
|
25
|
+
import {
|
|
26
|
+
createOutput,
|
|
27
|
+
parseCommonArgs,
|
|
28
|
+
formatTable,
|
|
29
|
+
COMMON_OPTIONS_HELP
|
|
30
|
+
} from '../src/output.mjs';
|
|
31
|
+
import { findProjectRoot, shouldSkip, isCodeFile, detectLanguage } from '../src/project.mjs';
|
|
32
|
+
|
|
33
|
+
const HELP = `
|
|
34
|
+
tl-env - Find environment variables and config used in the codebase
|
|
35
|
+
|
|
36
|
+
Usage: tl-env [dir] [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--show-files, -f Show which files use each variable
|
|
40
|
+
--show-values Show values from .env files (careful with secrets!)
|
|
41
|
+
--required-only, -r Show only required/non-optional env vars
|
|
42
|
+
${COMMON_OPTIONS_HELP}
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
tl-env # Scan current directory
|
|
46
|
+
tl-env src/ # Scan specific directory
|
|
47
|
+
tl-env -f # Show which files use each var
|
|
48
|
+
tl-env -j # JSON output
|
|
49
|
+
|
|
50
|
+
Detects:
|
|
51
|
+
JavaScript/TypeScript: process.env.*, import.meta.env.*
|
|
52
|
+
Python: os.environ, os.getenv(), environ.get()
|
|
53
|
+
Ruby: ENV['*'], ENV.fetch()
|
|
54
|
+
Go: os.Getenv(), viper.Get*()
|
|
55
|
+
Config files: .env, .env.*, config/*.json
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
// ─────────────────────────────────────────────────────────────
|
|
59
|
+
// Environment Variable Patterns
|
|
60
|
+
// ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const ENV_PATTERNS = {
|
|
63
|
+
javascript: [
|
|
64
|
+
// process.env.VAR_NAME or process.env['VAR_NAME']
|
|
65
|
+
/process\.env\.([A-Z][A-Z0-9_]*)/g,
|
|
66
|
+
/process\.env\[['"]([A-Z][A-Z0-9_]*)['"]\]/g,
|
|
67
|
+
// import.meta.env.VITE_VAR
|
|
68
|
+
/import\.meta\.env\.([A-Z][A-Z0-9_]*)/g,
|
|
69
|
+
// Destructuring: const { VAR } = process.env
|
|
70
|
+
/const\s*\{\s*([A-Z][A-Z0-9_,\s]*)\s*\}\s*=\s*process\.env/g
|
|
71
|
+
],
|
|
72
|
+
typescript: [
|
|
73
|
+
/process\.env\.([A-Z][A-Z0-9_]*)/g,
|
|
74
|
+
/process\.env\[['"]([A-Z][A-Z0-9_]*)['"]\]/g,
|
|
75
|
+
/import\.meta\.env\.([A-Z][A-Z0-9_]*)/g,
|
|
76
|
+
/const\s*\{\s*([A-Z][A-Z0-9_,\s]*)\s*\}\s*=\s*process\.env/g
|
|
77
|
+
],
|
|
78
|
+
python: [
|
|
79
|
+
// os.environ['VAR'] or os.environ.get('VAR')
|
|
80
|
+
/os\.environ\[['"]([A-Z][A-Z0-9_]*)['"]\]/g,
|
|
81
|
+
/os\.environ\.get\(['"]([A-Z][A-Z0-9_]*)['"]/g,
|
|
82
|
+
/os\.getenv\(['"]([A-Z][A-Z0-9_]*)['"]/g,
|
|
83
|
+
// environ.get('VAR') after from os import environ
|
|
84
|
+
/environ\.get\(['"]([A-Z][A-Z0-9_]*)['"]/g,
|
|
85
|
+
/environ\[['"]([A-Z][A-Z0-9_]*)['"]\]/g
|
|
86
|
+
],
|
|
87
|
+
ruby: [
|
|
88
|
+
/ENV\[['"]([A-Z][A-Z0-9_]*)['"]\]/g,
|
|
89
|
+
/ENV\.fetch\(['"]([A-Z][A-Z0-9_]*)['"]/g
|
|
90
|
+
],
|
|
91
|
+
go: [
|
|
92
|
+
/os\.Getenv\(['"]([A-Z][A-Z0-9_]*)['"]\)/g,
|
|
93
|
+
/os\.LookupEnv\(['"]([A-Z][A-Z0-9_]*)['"]\)/g,
|
|
94
|
+
/viper\.Get(?:String|Int|Bool|Float64)?\(['"]([A-Za-z][A-Za-z0-9_.]*)['"]\)/g
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────────────────────
|
|
99
|
+
// File Discovery
|
|
100
|
+
// ─────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function findSourceFiles(dir, files = []) {
|
|
103
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
104
|
+
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const fullPath = join(dir, entry.name);
|
|
107
|
+
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
if (!shouldSkip(entry.name, true)) {
|
|
110
|
+
findSourceFiles(fullPath, files);
|
|
111
|
+
}
|
|
112
|
+
} else if (entry.isFile()) {
|
|
113
|
+
if (!shouldSkip(entry.name, false) && isCodeFile(fullPath)) {
|
|
114
|
+
files.push(fullPath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return files;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function findEnvFiles(dir, files = []) {
|
|
123
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
124
|
+
|
|
125
|
+
for (const entry of entries) {
|
|
126
|
+
const fullPath = join(dir, entry.name);
|
|
127
|
+
|
|
128
|
+
if (entry.isDirectory() && !shouldSkip(entry.name, true)) {
|
|
129
|
+
findEnvFiles(fullPath, files);
|
|
130
|
+
} else if (entry.isFile()) {
|
|
131
|
+
// .env, .env.local, .env.development, .env.example, .env.sample, etc.
|
|
132
|
+
if (entry.name === '.env' || entry.name.startsWith('.env.')) {
|
|
133
|
+
files.push(fullPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─────────────────────────────────────────────────────────────
|
|
142
|
+
// Extraction
|
|
143
|
+
// ─────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function extractEnvVars(content, language) {
|
|
146
|
+
const vars = new Set();
|
|
147
|
+
const patterns = ENV_PATTERNS[language];
|
|
148
|
+
|
|
149
|
+
if (!patterns) return vars;
|
|
150
|
+
|
|
151
|
+
for (const pattern of patterns) {
|
|
152
|
+
// Reset regex state
|
|
153
|
+
pattern.lastIndex = 0;
|
|
154
|
+
let match;
|
|
155
|
+
|
|
156
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
157
|
+
const captured = match[1];
|
|
158
|
+
|
|
159
|
+
// Handle destructuring pattern: { VAR1, VAR2 }
|
|
160
|
+
if (captured.includes(',')) {
|
|
161
|
+
const names = captured.split(',').map(n => n.trim()).filter(n => /^[A-Z]/.test(n));
|
|
162
|
+
names.forEach(n => vars.add(n));
|
|
163
|
+
} else {
|
|
164
|
+
vars.add(captured);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return vars;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseEnvFile(content) {
|
|
173
|
+
const vars = {};
|
|
174
|
+
const lines = content.split('\n');
|
|
175
|
+
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
const trimmed = line.trim();
|
|
178
|
+
|
|
179
|
+
// Skip empty lines and comments
|
|
180
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
181
|
+
|
|
182
|
+
// Parse KEY=value
|
|
183
|
+
const match = trimmed.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
|
|
184
|
+
if (match) {
|
|
185
|
+
const [, key, value] = match;
|
|
186
|
+
// Remove quotes if present
|
|
187
|
+
vars[key] = value.replace(/^["']|["']$/g, '');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return vars;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function detectRequired(content, varName, language) {
|
|
195
|
+
// Heuristics for detecting if a var is required
|
|
196
|
+
const patterns = [];
|
|
197
|
+
|
|
198
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
199
|
+
// Patterns that suggest the var is required (no fallback)
|
|
200
|
+
patterns.push(
|
|
201
|
+
new RegExp(`process\\.env\\.${varName}(?![\\s]*\\|\\|)(?![\\s]*\\?\\?)`, 'g'),
|
|
202
|
+
new RegExp(`throw.*${varName}`, 'gi'),
|
|
203
|
+
new RegExp(`required.*${varName}`, 'gi')
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// If we find patterns suggesting it's required without fallback
|
|
208
|
+
for (const pattern of patterns) {
|
|
209
|
+
if (pattern.test(content)) return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─────────────────────────────────────────────────────────────
|
|
216
|
+
// Main
|
|
217
|
+
// ─────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
const args = process.argv.slice(2);
|
|
220
|
+
const options = parseCommonArgs(args);
|
|
221
|
+
const showFiles = options.remaining.includes('--show-files') || options.remaining.includes('-f');
|
|
222
|
+
const showValues = options.remaining.includes('--show-values');
|
|
223
|
+
const requiredOnly = options.remaining.includes('--required-only') || options.remaining.includes('-r');
|
|
224
|
+
const targetDir = options.remaining.find(a => !a.startsWith('-')) || '.';
|
|
225
|
+
|
|
226
|
+
if (options.help) {
|
|
227
|
+
console.log(HELP);
|
|
228
|
+
process.exit(0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!existsSync(targetDir)) {
|
|
232
|
+
console.error(`Directory not found: ${targetDir}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const projectRoot = findProjectRoot(targetDir);
|
|
237
|
+
const out = createOutput(options);
|
|
238
|
+
|
|
239
|
+
// Track vars with their usage info
|
|
240
|
+
const envVars = new Map(); // varName -> { files: Set, values: Map, required: bool }
|
|
241
|
+
|
|
242
|
+
// 1. Scan source files
|
|
243
|
+
const sourceFiles = findSourceFiles(targetDir);
|
|
244
|
+
|
|
245
|
+
for (const filePath of sourceFiles) {
|
|
246
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
247
|
+
const lang = detectLanguage(filePath);
|
|
248
|
+
if (!lang) continue;
|
|
249
|
+
|
|
250
|
+
const vars = extractEnvVars(content, lang);
|
|
251
|
+
const relPath = relative(projectRoot, filePath);
|
|
252
|
+
|
|
253
|
+
for (const varName of vars) {
|
|
254
|
+
if (!envVars.has(varName)) {
|
|
255
|
+
envVars.set(varName, { files: new Set(), values: new Map(), required: false });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const info = envVars.get(varName);
|
|
259
|
+
info.files.add(relPath);
|
|
260
|
+
|
|
261
|
+
// Check if this usage suggests it's required
|
|
262
|
+
if (detectRequired(content, varName, lang)) {
|
|
263
|
+
info.required = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 2. Parse .env files for values
|
|
269
|
+
const envFiles = findEnvFiles(projectRoot);
|
|
270
|
+
const envFileVars = new Map(); // Track vars defined in env files
|
|
271
|
+
|
|
272
|
+
for (const envFile of envFiles) {
|
|
273
|
+
const content = readFileSync(envFile, 'utf-8');
|
|
274
|
+
const vars = parseEnvFile(content);
|
|
275
|
+
const relPath = relative(projectRoot, envFile);
|
|
276
|
+
|
|
277
|
+
for (const [varName, value] of Object.entries(vars)) {
|
|
278
|
+
envFileVars.set(varName, true);
|
|
279
|
+
|
|
280
|
+
if (!envVars.has(varName)) {
|
|
281
|
+
envVars.set(varName, { files: new Set(), values: new Map(), required: false });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const info = envVars.get(varName);
|
|
285
|
+
info.values.set(relPath, value);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Filter if required only
|
|
290
|
+
let varsToShow = [...envVars.entries()];
|
|
291
|
+
if (requiredOnly) {
|
|
292
|
+
varsToShow = varsToShow.filter(([, info]) => info.required);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Sort by name
|
|
296
|
+
varsToShow.sort((a, b) => a[0].localeCompare(b[0]));
|
|
297
|
+
|
|
298
|
+
// Build output
|
|
299
|
+
const usedInCode = varsToShow.filter(([, info]) => info.files.size > 0);
|
|
300
|
+
const definedOnly = varsToShow.filter(([, info]) => info.files.size === 0);
|
|
301
|
+
|
|
302
|
+
// Set JSON data
|
|
303
|
+
out.setData('variables', varsToShow.map(([name, info]) => ({
|
|
304
|
+
name,
|
|
305
|
+
usedIn: [...info.files],
|
|
306
|
+
definedIn: [...info.values.keys()],
|
|
307
|
+
required: info.required,
|
|
308
|
+
hasValue: info.values.size > 0
|
|
309
|
+
})));
|
|
310
|
+
out.setData('envFiles', envFiles.map(f => relative(projectRoot, f)));
|
|
311
|
+
out.setData('totalVars', varsToShow.length);
|
|
312
|
+
|
|
313
|
+
// Text output
|
|
314
|
+
if (usedInCode.length > 0) {
|
|
315
|
+
out.header('Environment Variables (used in code):');
|
|
316
|
+
out.blank();
|
|
317
|
+
|
|
318
|
+
const rows = [];
|
|
319
|
+
for (const [varName, info] of usedInCode) {
|
|
320
|
+
const status = [];
|
|
321
|
+
if (info.required) status.push('required');
|
|
322
|
+
if (info.values.size === 0) status.push('no default');
|
|
323
|
+
|
|
324
|
+
const statusStr = status.length > 0 ? ` (${status.join(', ')})` : '';
|
|
325
|
+
|
|
326
|
+
rows.push([` ${varName}`, statusStr]);
|
|
327
|
+
|
|
328
|
+
if (showFiles && info.files.size > 0) {
|
|
329
|
+
for (const file of info.files) {
|
|
330
|
+
rows.push([' ', `→ ${file}`]);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (showValues && info.values.size > 0) {
|
|
335
|
+
for (const [file, value] of info.values) {
|
|
336
|
+
const displayValue = value.length > 30 ? value.slice(0, 27) + '...' : value;
|
|
337
|
+
rows.push([' ', `= ${displayValue} (${basename(file)})`]);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
formatTable(rows).forEach(line => out.add(line));
|
|
343
|
+
out.blank();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (definedOnly.length > 0 && !requiredOnly) {
|
|
347
|
+
out.header('Defined in .env but not used in code:');
|
|
348
|
+
out.blank();
|
|
349
|
+
for (const [varName] of definedOnly) {
|
|
350
|
+
out.add(` ${varName}`);
|
|
351
|
+
}
|
|
352
|
+
out.blank();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Show env files found
|
|
356
|
+
if (envFiles.length > 0 && !options.quiet) {
|
|
357
|
+
out.add('Env files found:');
|
|
358
|
+
for (const file of envFiles) {
|
|
359
|
+
out.add(` ${relative(projectRoot, file)}`);
|
|
360
|
+
}
|
|
361
|
+
out.blank();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Summary
|
|
365
|
+
if (!options.quiet) {
|
|
366
|
+
const usedCount = usedInCode.length;
|
|
367
|
+
const definedCount = definedOnly.length;
|
|
368
|
+
const requiredCount = varsToShow.filter(([, info]) => info.required).length;
|
|
369
|
+
|
|
370
|
+
out.add(`Found ${usedCount} env vars used in code${requiredCount > 0 ? ` (${requiredCount} required)` : ''}`);
|
|
371
|
+
if (definedCount > 0 && !requiredOnly) {
|
|
372
|
+
out.add(`Found ${definedCount} env vars defined but not used`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
out.print();
|