envprobe 1.0.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,181 @@
1
+ /**
2
+ * Progress indicators and spinners for CLI operations
3
+ */
4
+
5
+ /**
6
+ * Simple spinner for long-running operations
7
+ */
8
+ export class Spinner {
9
+ constructor(message = 'Processing...') {
10
+ this.message = message;
11
+ this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
12
+ this.currentFrame = 0;
13
+ this.interval = null;
14
+ this.isSpinning = false;
15
+ }
16
+
17
+ start() {
18
+ if (this.isSpinning) return;
19
+
20
+ this.isSpinning = true;
21
+ this.currentFrame = 0;
22
+
23
+ // Hide cursor
24
+ process.stdout.write('\x1B[?25l');
25
+
26
+ this.interval = setInterval(() => {
27
+ const frame = this.frames[this.currentFrame];
28
+ process.stdout.write(`\r${frame} ${this.message}`);
29
+ this.currentFrame = (this.currentFrame + 1) % this.frames.length;
30
+ }, 80);
31
+ }
32
+
33
+ update(message) {
34
+ this.message = message;
35
+ }
36
+
37
+ succeed(message) {
38
+ this.stop();
39
+ process.stdout.write(`\r✓ ${message || this.message}\n`);
40
+ }
41
+
42
+ fail(message) {
43
+ this.stop();
44
+ process.stdout.write(`\r✗ ${message || this.message}\n`);
45
+ }
46
+
47
+ warn(message) {
48
+ this.stop();
49
+ process.stdout.write(`\r⚠ ${message || this.message}\n`);
50
+ }
51
+
52
+ info(message) {
53
+ this.stop();
54
+ process.stdout.write(`\rℹ ${message || this.message}\n`);
55
+ }
56
+
57
+ stop() {
58
+ if (!this.isSpinning) return;
59
+
60
+ this.isSpinning = false;
61
+
62
+ if (this.interval) {
63
+ clearInterval(this.interval);
64
+ this.interval = null;
65
+ }
66
+
67
+ // Clear line and show cursor
68
+ process.stdout.write('\r\x1B[K');
69
+ process.stdout.write('\x1B[?25h');
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Progress bar for file scanning
75
+ */
76
+ export class ProgressBar {
77
+ constructor(total, message = 'Progress') {
78
+ this.total = total;
79
+ this.current = 0;
80
+ this.message = message;
81
+ this.width = 40;
82
+ this.startTime = Date.now();
83
+ }
84
+
85
+ update(current, message) {
86
+ this.current = current;
87
+ if (message) this.message = message;
88
+ this.render();
89
+ }
90
+
91
+ increment(message) {
92
+ this.current++;
93
+ if (message) this.message = message;
94
+ this.render();
95
+ }
96
+
97
+ render() {
98
+ const percentage = Math.min(100, Math.floor((this.current / this.total) * 100));
99
+ const filled = Math.floor((this.current / this.total) * this.width);
100
+ const empty = this.width - filled;
101
+
102
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
103
+ const elapsed = Math.floor((Date.now() - this.startTime) / 1000);
104
+ const rate = this.current / elapsed || 0;
105
+ const eta = this.current > 0 ? Math.floor((this.total - this.current) / rate) : 0;
106
+
107
+ process.stdout.write(
108
+ `\r${this.message}: [${bar}] ${percentage}% (${this.current}/${this.total}) ETA: ${eta}s`
109
+ );
110
+ }
111
+
112
+ complete(message) {
113
+ this.current = this.total;
114
+ this.render();
115
+ process.stdout.write(`\n✓ ${message || 'Complete'}\n`);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Multi-line progress display for concurrent operations
121
+ */
122
+ export class MultiProgress {
123
+ constructor() {
124
+ this.tasks = new Map();
125
+ this.lineCount = 0;
126
+ }
127
+
128
+ addTask(id, message) {
129
+ this.tasks.set(id, {
130
+ message,
131
+ status: 'pending',
132
+ spinner: 0,
133
+ });
134
+ this.render();
135
+ }
136
+
137
+ updateTask(id, status, message) {
138
+ const task = this.tasks.get(id);
139
+ if (task) {
140
+ task.status = status;
141
+ if (message) task.message = message;
142
+ this.render();
143
+ }
144
+ }
145
+
146
+ render() {
147
+ // Move cursor up to overwrite previous output
148
+ if (this.lineCount > 0) {
149
+ process.stdout.write(`\x1B[${this.lineCount}A`);
150
+ }
151
+
152
+ const lines = [];
153
+ for (const [id, task] of this.tasks) {
154
+ const icon = this.getStatusIcon(task.status);
155
+ lines.push(`${icon} ${task.message}`);
156
+ }
157
+
158
+ this.lineCount = lines.length;
159
+ process.stdout.write(lines.join('\n') + '\n');
160
+ }
161
+
162
+ getStatusIcon(status) {
163
+ const icons = {
164
+ pending: '⋯',
165
+ running: '⠿',
166
+ success: '✓',
167
+ error: '✗',
168
+ warning: '⚠',
169
+ };
170
+ return icons[status] || '•';
171
+ }
172
+
173
+ clear() {
174
+ if (this.lineCount > 0) {
175
+ process.stdout.write(`\x1B[${this.lineCount}A`);
176
+ process.stdout.write('\x1B[J');
177
+ }
178
+ this.tasks.clear();
179
+ this.lineCount = 0;
180
+ }
181
+ }
package/src/repl.js ADDED
@@ -0,0 +1,416 @@
1
+ import { createInterface } from 'readline';
2
+ import { stdin as input, stdout as output } from 'process';
3
+ import { dirname } from 'path';
4
+ import { parseArguments, run } from './cli.js';
5
+ import { setupAutocomplete } from './autocomplete.js';
6
+ import { saveConfig, loadConfig } from './config.js';
7
+
8
+ /**
9
+ * REPL (Read-Eval-Print Loop) for interactive envcheck sessions
10
+ * Provides an interactive shell for running envcheck commands
11
+ */
12
+
13
+ /**
14
+ * Session state management
15
+ */
16
+ export class Session {
17
+ constructor() {
18
+ this.history = [];
19
+ this.results = [];
20
+ this.config = {
21
+ path: '.',
22
+ envFile: '.env.example',
23
+ format: 'text',
24
+ failOn: 'none',
25
+ ignore: [],
26
+ noColor: false,
27
+ quiet: false,
28
+ };
29
+ this.startTime = Date.now();
30
+ }
31
+
32
+ addCommand(command) {
33
+ this.history.push({
34
+ command,
35
+ timestamp: Date.now(),
36
+ });
37
+ }
38
+
39
+ addResult(result) {
40
+ this.results.push({
41
+ result,
42
+ timestamp: Date.now(),
43
+ });
44
+ }
45
+
46
+ getConfig() {
47
+ return { ...this.config };
48
+ }
49
+
50
+ setConfig(key, value) {
51
+ if (key in this.config) {
52
+ this.config[key] = value;
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+
58
+ getHistory() {
59
+ return [...this.history];
60
+ }
61
+
62
+ getResults() {
63
+ return [...this.results];
64
+ }
65
+
66
+ getDuration() {
67
+ return Date.now() - this.startTime;
68
+ }
69
+
70
+ clear() {
71
+ this.history = [];
72
+ this.results = [];
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Command parser for REPL special commands
78
+ */
79
+ export class CommandParser {
80
+ constructor(session) {
81
+ this.session = session;
82
+ this.commands = new Map([
83
+ ['help', this.helpCommand.bind(this)],
84
+ ['exit', this.exitCommand.bind(this)],
85
+ ['quit', this.exitCommand.bind(this)],
86
+ ['history', this.historyCommand.bind(this)],
87
+ ['clear', this.clearCommand.bind(this)],
88
+ ['config', this.configCommand.bind(this)],
89
+ ['set', this.setCommand.bind(this)],
90
+ ['get', this.getCommand.bind(this)],
91
+ ['results', this.resultsCommand.bind(this)],
92
+ ['last', this.lastCommand.bind(this)],
93
+ ['watch', this.watchCommand.bind(this)],
94
+ ['save', this.saveCommand.bind(this)],
95
+ ['load', this.loadCommand.bind(this)],
96
+ ['fix', this.fixCommand.bind(this)],
97
+ ['suggest', this.suggestCommand.bind(this)],
98
+ ]);
99
+ }
100
+
101
+ isCommand(input) {
102
+ const trimmed = input.trim();
103
+ return trimmed.startsWith(':') || trimmed.startsWith('.');
104
+ }
105
+
106
+ async execute(input) {
107
+ const trimmed = input.trim();
108
+ const prefix = trimmed[0];
109
+ const commandLine = trimmed.slice(1).trim();
110
+ const [command, ...args] = commandLine.split(/\s+/);
111
+
112
+ if (!this.commands.has(command)) {
113
+ return `Unknown command: ${command}. Type :help for available commands.`;
114
+ }
115
+
116
+ return await this.commands.get(command)(args);
117
+ }
118
+
119
+ helpCommand() {
120
+ return `
121
+ Available REPL Commands:
122
+ :help, .help Show this help message
123
+ :exit, :quit Exit the REPL
124
+ :history Show command history
125
+ :clear Clear history and results
126
+ :config Show current configuration
127
+ :set <key> <value> Set a configuration value
128
+ :get <key> Get a configuration value
129
+ :results Show all previous results
130
+ :last Show the last result
131
+ :watch Start watch mode
132
+ :save [file] Save current config to file
133
+ :load [file] Load config from file
134
+ :fix Auto-fix issues in .env.example
135
+ :suggest Show intelligent suggestions
136
+
137
+ Configuration Keys:
138
+ path, envFile, format, failOn, noColor, quiet, suggestions, progress
139
+
140
+ Examples:
141
+ :set path ./src
142
+ :set format json
143
+ :get envFile
144
+ :save .envcheckrc.json
145
+ envcheck . --format json
146
+ . --fail-on missing
147
+ :fix
148
+ `;
149
+ }
150
+
151
+ exitCommand() {
152
+ return { exit: true };
153
+ }
154
+
155
+ historyCommand() {
156
+ const history = this.session.getHistory();
157
+ if (history.length === 0) {
158
+ return 'No command history.';
159
+ }
160
+
161
+ return history
162
+ .map((entry, index) => {
163
+ const time = new Date(entry.timestamp).toLocaleTimeString();
164
+ return `${index + 1}. [${time}] ${entry.command}`;
165
+ })
166
+ .join('\n');
167
+ }
168
+
169
+ clearCommand() {
170
+ this.session.clear();
171
+ return 'History and results cleared.';
172
+ }
173
+
174
+ configCommand() {
175
+ const config = this.session.getConfig();
176
+ return Object.entries(config)
177
+ .map(([key, value]) => {
178
+ const displayValue = Array.isArray(value) ? `[${value.join(', ')}]` : value;
179
+ return `${key}: ${displayValue}`;
180
+ })
181
+ .join('\n');
182
+ }
183
+
184
+ setCommand(args) {
185
+ if (args.length < 2) {
186
+ return 'Usage: :set <key> <value>';
187
+ }
188
+
189
+ const [key, ...valueParts] = args;
190
+ const value = valueParts.join(' ');
191
+
192
+ // Handle special types
193
+ let parsedValue = value;
194
+ if (value === 'true') parsedValue = true;
195
+ else if (value === 'false') parsedValue = false;
196
+ else if (!isNaN(value) && value !== '') parsedValue = Number(value);
197
+
198
+ if (this.session.setConfig(key, parsedValue)) {
199
+ return `Set ${key} = ${parsedValue}`;
200
+ } else {
201
+ return `Unknown configuration key: ${key}`;
202
+ }
203
+ }
204
+
205
+ getCommand(args) {
206
+ if (args.length === 0) {
207
+ return 'Usage: :get <key>';
208
+ }
209
+
210
+ const key = args[0];
211
+ const config = this.session.getConfig();
212
+
213
+ if (key in config) {
214
+ const value = config[key];
215
+ const displayValue = Array.isArray(value) ? `[${value.join(', ')}]` : value;
216
+ return `${key}: ${displayValue}`;
217
+ } else {
218
+ return `Unknown configuration key: ${key}`;
219
+ }
220
+ }
221
+
222
+ resultsCommand() {
223
+ const results = this.session.getResults();
224
+ if (results.length === 0) {
225
+ return 'No results yet.';
226
+ }
227
+
228
+ return results
229
+ .map((entry, index) => {
230
+ const time = new Date(entry.timestamp).toLocaleTimeString();
231
+ const summary = entry.result.summary || {};
232
+ return `${index + 1}. [${time}] Missing: ${summary.missingCount || 0}, Unused: ${summary.unusedCount || 0}, Undocumented: ${summary.undocumentedCount || 0}`;
233
+ })
234
+ .join('\n');
235
+ }
236
+
237
+ lastCommand() {
238
+ const results = this.session.getResults();
239
+ if (results.length === 0) {
240
+ return 'No results yet.';
241
+ }
242
+
243
+ const last = results[results.length - 1];
244
+ return JSON.stringify(last.result, null, 2);
245
+ }
246
+
247
+ async watchCommand(args) {
248
+ return 'Watch mode not available in REPL. Use: envcheck . --watch';
249
+ }
250
+
251
+ async saveCommand(args) {
252
+ const filename = args[0] || '.envcheckrc.json';
253
+
254
+ try {
255
+ const config = this.session.getConfig();
256
+ const path = saveConfig(config, '.', filename);
257
+ return `Configuration saved to ${path}`;
258
+ } catch (error) {
259
+ return `Failed to save config: ${error.message}`;
260
+ }
261
+ }
262
+
263
+ async loadCommand(args) {
264
+ const filename = args[0];
265
+
266
+ try {
267
+ const config = loadConfig(filename ? dirname(filename) : '.');
268
+
269
+ if (!config) {
270
+ return 'No configuration file found';
271
+ }
272
+
273
+ // Update session config
274
+ for (const [key, value] of Object.entries(config)) {
275
+ this.session.setConfig(key, value);
276
+ }
277
+
278
+ return 'Configuration loaded successfully';
279
+ } catch (error) {
280
+ return `Failed to load config: ${error.message}`;
281
+ }
282
+ }
283
+
284
+ async fixCommand(args) {
285
+ return 'Auto-fix: Run envcheck with --fix flag to update .env.example';
286
+ }
287
+
288
+ async suggestCommand(args) {
289
+ const results = this.session.getResults();
290
+ if (results.length === 0) {
291
+ return 'No results to analyze. Run a check first.';
292
+ }
293
+
294
+ const last = results[results.length - 1];
295
+ const { generateSuggestions } = await import('./suggestions.js');
296
+ const suggestions = generateSuggestions(last.result);
297
+
298
+ if (suggestions.length === 0) {
299
+ return 'No suggestions - everything looks good! ✨';
300
+ }
301
+
302
+ return suggestions
303
+ .map(s => {
304
+ const items = s.items.map(i => ` • ${i.suggestion}`).join('\n');
305
+ return `${s.message}\n${s.action}\n${items}`;
306
+ })
307
+ .join('\n\n');
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Start the REPL
313
+ */
314
+ export async function startREPL() {
315
+ const session = new Session();
316
+ const commandParser = new CommandParser(session);
317
+
318
+ const rl = createInterface({
319
+ input,
320
+ output,
321
+ prompt: 'envcheck> ',
322
+ historySize: 100,
323
+ completer: (line) => {
324
+ const { getCompletions } = require('./autocomplete.js');
325
+ const completions = getCompletions(line);
326
+ return [completions, line];
327
+ },
328
+ });
329
+
330
+ console.log('╔════════════════════════════════════════════════════════════╗');
331
+ console.log('║ envcheck REPL - Interactive Environment Variable Checker ║');
332
+ console.log('╚════════════════════════════════════════════════════════════╝');
333
+ console.log('\n💡 Type :help for available commands, :exit to quit\n');
334
+
335
+ rl.prompt();
336
+
337
+ rl.on('line', async (line) => {
338
+ const input = line.trim();
339
+
340
+ if (!input) {
341
+ rl.prompt();
342
+ return;
343
+ }
344
+
345
+ session.addCommand(input);
346
+
347
+ try {
348
+ // Check if it's a special command
349
+ if (commandParser.isCommand(input)) {
350
+ const result = await commandParser.execute(input);
351
+
352
+ if (result && typeof result === 'object' && result.exit) {
353
+ console.log('Goodbye!');
354
+ rl.close();
355
+ return;
356
+ }
357
+
358
+ if (result) {
359
+ console.log(result);
360
+ }
361
+ } else {
362
+ // Parse as envcheck command
363
+ let args;
364
+
365
+ // Handle shorthand: if line starts with '.', treat as 'envcheck .'
366
+ if (input.startsWith('.')) {
367
+ args = input.split(/\s+/);
368
+ } else if (input.startsWith('envcheck')) {
369
+ // Remove 'envcheck' prefix
370
+ args = input.slice('envcheck'.length).trim().split(/\s+/).filter(Boolean);
371
+ } else {
372
+ // Treat as arguments to envcheck
373
+ args = input.split(/\s+/);
374
+ }
375
+
376
+ // Merge with session config
377
+ const config = session.getConfig();
378
+ const fullArgs = [
379
+ config.path,
380
+ '--env-file', config.envFile,
381
+ '--format', config.format,
382
+ '--fail-on', config.failOn,
383
+ ...config.ignore.flatMap(pattern => ['--ignore', pattern]),
384
+ ...(config.noColor ? ['--no-color'] : []),
385
+ ...(config.quiet ? ['--quiet'] : []),
386
+ ...args,
387
+ ];
388
+
389
+ // Run the command
390
+ const exitCode = await run(fullArgs);
391
+
392
+ // Store result (we'd need to modify run() to return the actual result)
393
+ session.addResult({ exitCode });
394
+
395
+ if (exitCode === 0) {
396
+ console.log('\n✓ Check completed successfully');
397
+ } else if (exitCode === 1) {
398
+ console.log('\n✗ Validation failed');
399
+ } else {
400
+ console.log('\n✗ Error occurred');
401
+ }
402
+ }
403
+ } catch (error) {
404
+ console.error(`Error: ${error.message}`);
405
+ }
406
+
407
+ rl.prompt();
408
+ });
409
+
410
+ rl.on('close', () => {
411
+ const duration = Math.round(session.getDuration() / 1000);
412
+ console.log(`\nSession duration: ${duration}s`);
413
+ console.log(`Commands executed: ${session.getHistory().length}`);
414
+ process.exit(0);
415
+ });
416
+ }