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.
@@ -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();