@danielszlaski/envguard 0.1.0 → 0.1.2

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,136 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Logger = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ /**
9
+ * Minimalist red-themed logger inspired by Serverless Framework v4
10
+ * Uses minimal colors and simple symbols for a clean, professional look
11
+ */
12
+ class Logger {
13
+ /**
14
+ * Start an animated spinner with a message
15
+ */
16
+ static startSpinner(message) {
17
+ this.spinnerMessage = message;
18
+ this.currentFrame = 0;
19
+ if (this.spinnerInterval) {
20
+ this.stopSpinner();
21
+ }
22
+ process.stdout.write('\n');
23
+ this.spinnerInterval = setInterval(() => {
24
+ const frame = this.spinnerFrames[this.currentFrame];
25
+ process.stdout.write(`\r${chalk_1.default.dim(frame)} ${this.spinnerMessage}`);
26
+ this.currentFrame = (this.currentFrame + 1) % this.spinnerFrames.length;
27
+ }, 80);
28
+ }
29
+ /**
30
+ * Stop the spinner and clear the line
31
+ */
32
+ static stopSpinner(finalMessage) {
33
+ if (this.spinnerInterval) {
34
+ clearInterval(this.spinnerInterval);
35
+ this.spinnerInterval = null;
36
+ }
37
+ if (finalMessage) {
38
+ process.stdout.write(`\r${finalMessage}\n`);
39
+ }
40
+ else {
41
+ process.stdout.write('\r\x1b[K'); // Clear line
42
+ }
43
+ }
44
+ /**
45
+ * Log a header/section message (minimalist, no icons)
46
+ */
47
+ static header(message) {
48
+ console.log(chalk_1.default.dim(message));
49
+ }
50
+ /**
51
+ * Log a success message with checkmark
52
+ */
53
+ static success(message, indent = false) {
54
+ const prefix = indent ? ' ' : '';
55
+ console.log(`${prefix}${chalk_1.default.dim('✔')} ${message}`);
56
+ }
57
+ /**
58
+ * Log an error message (red themed)
59
+ */
60
+ static error(message, indent = false) {
61
+ const prefix = indent ? ' ' : '';
62
+ console.log(`${prefix}${chalk_1.default.red('✖')} ${chalk_1.default.red(message)}`);
63
+ }
64
+ /**
65
+ * Log a warning message (yellow/amber)
66
+ */
67
+ static warning(message, indent = false) {
68
+ const prefix = indent ? ' ' : '';
69
+ console.log(`${prefix}${chalk_1.default.hex('#FFA500')('⚠')} ${chalk_1.default.hex('#FFA500')(message)}`);
70
+ }
71
+ /**
72
+ * Log an info message (very minimal)
73
+ */
74
+ static info(message, indent = false) {
75
+ const prefix = indent ? ' ' : '';
76
+ console.log(`${prefix}${chalk_1.default.dim(message)}`);
77
+ }
78
+ /**
79
+ * Log a list item (error themed in red)
80
+ */
81
+ static errorItem(message, indent = 1) {
82
+ const prefix = ' '.repeat(indent);
83
+ console.log(`${prefix}${chalk_1.default.red('•')} ${chalk_1.default.red(message)}`);
84
+ }
85
+ /**
86
+ * Log a list item (warning themed, yellow/amber)
87
+ */
88
+ static warningItem(message, indent = 1) {
89
+ const prefix = ' '.repeat(indent);
90
+ console.log(`${prefix}${chalk_1.default.hex('#FFA500')('•')} ${chalk_1.default.hex('#FFA500')(message)}`);
91
+ }
92
+ /**
93
+ * Log a list item (info themed, very dim)
94
+ */
95
+ static infoItem(message, indent = 1) {
96
+ const prefix = ' '.repeat(indent);
97
+ console.log(`${prefix}${chalk_1.default.dim('•')} ${chalk_1.default.dim(message)}`);
98
+ }
99
+ /**
100
+ * Log a divider
101
+ */
102
+ static divider() {
103
+ console.log(chalk_1.default.dim('─'.repeat(50)));
104
+ }
105
+ /**
106
+ * Log a blank line
107
+ */
108
+ static blank() {
109
+ console.log();
110
+ }
111
+ /**
112
+ * Log a final summary message (Serverless-style)
113
+ */
114
+ static summary(message) {
115
+ console.log(`\n${chalk_1.default.dim('✔')} ${message}\n`);
116
+ }
117
+ /**
118
+ * Log a path/file reference (dimmed)
119
+ */
120
+ static path(message, indent = false) {
121
+ const prefix = indent ? ' ' : '';
122
+ console.log(`${prefix}${chalk_1.default.dim(message)}`);
123
+ }
124
+ /**
125
+ * Log deployment-style message (like "Deploying to stage dev")
126
+ */
127
+ static deployment(message) {
128
+ console.log(`\n${message}\n`);
129
+ }
130
+ }
131
+ exports.Logger = Logger;
132
+ Logger.spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
133
+ Logger.spinnerInterval = null;
134
+ Logger.currentFrame = 0;
135
+ Logger.spinnerMessage = '';
136
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danielszlaski/envguard",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tool to keep environment variables in sync with your codebase",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -4,6 +4,7 @@ import { Command } from 'commander';
4
4
  import { scanCommand } from './commands/scan';
5
5
  import { fixCommand } from './commands/fix';
6
6
  import { checkCommand } from './commands/check';
7
+ import { Logger } from './utils/logger';
7
8
 
8
9
  const program = new Command();
9
10
 
@@ -21,7 +22,7 @@ program
21
22
  try {
22
23
  await scanCommand(options);
23
24
  } catch (error) {
24
- console.error('Error:', error);
25
+ Logger.error(`${error}`);
25
26
  process.exit(1);
26
27
  }
27
28
  });
@@ -33,7 +34,7 @@ program
33
34
  try {
34
35
  await fixCommand();
35
36
  } catch (error) {
36
- console.error('Error:', error);
37
+ Logger.error(`${error}`);
37
38
  process.exit(1);
38
39
  }
39
40
  });
@@ -46,7 +47,7 @@ program
46
47
  try {
47
48
  await scanCommand({ ci: true, strict: options.strict });
48
49
  } catch (error) {
49
- console.error('Error:', error);
50
+ Logger.error(`${error}`);
50
51
  process.exit(1);
51
52
  }
52
53
  });
@@ -1,25 +1,29 @@
1
1
  import * as path from 'path';
2
2
  import * as fs from 'fs';
3
- import chalk from 'chalk';
4
3
  import { CodeScanner } from '../scanner/codeScanner';
5
4
  import { EnvParser } from '../parser/envParser';
6
5
  import { EnvAnalyzer } from '../analyzer/envAnalyzer';
6
+ import { Logger } from '../utils/logger';
7
7
 
8
8
  export async function fixCommand() {
9
9
  const rootDir = process.cwd();
10
10
 
11
- console.log(chalk.blue('🔧 Generating .env.example files...\n'));
11
+ Logger.startSpinner('Generating .env.example files...');
12
12
 
13
13
  // Step 1: Find all .env files
14
14
  const scanner = new CodeScanner(rootDir);
15
15
  const envFiles = await scanner.findEnvFiles();
16
16
 
17
+ Logger.stopSpinner();
18
+
17
19
  if (envFiles.length === 0) {
18
- console.log(chalk.yellow('⚠️ No .env files found in the project\n'));
20
+ Logger.warning('No .env files found in the project');
21
+ Logger.blank();
19
22
  return { success: false };
20
23
  }
21
24
 
22
- console.log(chalk.green(`✓ Found ${envFiles.length} .env file(s)\n`));
25
+ Logger.success(`Found ${envFiles.length} .env file(s)`);
26
+ Logger.blank();
23
27
 
24
28
  const parser = new EnvParser();
25
29
  const analyzer = new EnvAnalyzer();
@@ -31,17 +35,19 @@ export async function fixCommand() {
31
35
  const relativePath = path.relative(rootDir, envDir);
32
36
  const displayPath = relativePath || '.';
33
37
 
34
- console.log(chalk.cyan(`📂 Processing ${displayPath}/`));
38
+ Logger.path(`Processing ${displayPath}/`);
35
39
 
36
40
  // Step 3: Scan code files in this directory and subdirectories
37
41
  const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
38
42
 
39
43
  if (usedVars.size === 0) {
40
- console.log(chalk.gray(` No environment variables found in code\n`));
44
+ Logger.info('No environment variables found in code', true);
45
+ Logger.blank();
41
46
  continue;
42
47
  }
43
48
 
44
- console.log(chalk.green(`Found ${usedVars.size} variable(s) used in this directory\n`));
49
+ Logger.success(`Found ${usedVars.size} variable(s) used in this directory`, true);
50
+ Logger.blank();
45
51
 
46
52
  // Step 4: Parse existing .env.example to preserve comments
47
53
  const examplePath = path.join(envDir, '.env.example');
@@ -53,7 +59,8 @@ export async function fixCommand() {
53
59
  // Step 6: Write to .env.example
54
60
  fs.writeFileSync(examplePath, newContent, 'utf-8');
55
61
 
56
- console.log(chalk.green(`Generated ${path.relative(rootDir, examplePath)}\n`));
62
+ Logger.success(`Generated ${path.relative(rootDir, examplePath)}`, true);
63
+ Logger.blank();
57
64
 
58
65
  totalVars += usedVars.size;
59
66
 
@@ -62,11 +69,12 @@ export async function fixCommand() {
62
69
  const missingFromEnv = Array.from(usedVars.keys()).filter(v => !definedVars.has(v));
63
70
 
64
71
  if (missingFromEnv.length > 0) {
65
- console.log(chalk.yellow(` ⚠️ Missing from .env: ${missingFromEnv.join(', ')}\n`));
72
+ Logger.warning(`Missing from .env: ${missingFromEnv.join(', ')}`, true);
73
+ Logger.blank();
66
74
  }
67
75
  }
68
76
 
69
- console.log(chalk.green(`🎉 Done! Generated ${envFiles.length} .env.example file(s) with ${totalVars} total variables\n`));
77
+ Logger.summary(`Generated ${envFiles.length} .env.example file(s) with ${totalVars} total variables`);
70
78
 
71
79
  return { success: true };
72
80
  }
@@ -1,6 +1,5 @@
1
1
  import * as path from 'path';
2
2
  import * as fs from 'fs';
3
- import chalk from 'chalk';
4
3
  import { glob } from 'glob';
5
4
  import { CodeScanner } from '../scanner/codeScanner';
6
5
  import { EnvParser, EnvEntry } from '../parser/envParser';
@@ -9,6 +8,7 @@ import { EnvAnalyzer } from '../analyzer/envAnalyzer';
9
8
  import { Issue } from '../types';
10
9
  import { isKnownRuntimeVar, getRuntimeVarCategory } from '../constants/knownEnvVars';
11
10
  import { ConfigLoader } from '../config/configLoader';
11
+ import { Logger } from '../utils/logger';
12
12
 
13
13
  export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
14
14
  const rootDir = process.cwd();
@@ -19,19 +19,23 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
19
19
  // CLI options override config file
20
20
  const strictMode = options.strict !== undefined ? options.strict : config.strict;
21
21
 
22
- console.log(chalk.blue('🔍 Scanning codebase for environment variables...\n'));
22
+ Logger.startSpinner('Scanning codebase for environment variables...');
23
23
 
24
24
  // Step 1: Find all .env files and serverless.yml files
25
25
  const scanner = new CodeScanner(rootDir);
26
26
  const envFiles = await scanner.findEnvFiles();
27
27
  const serverlessFiles = await scanner.findServerlessFiles();
28
28
 
29
+ Logger.stopSpinner();
30
+
29
31
  if (envFiles.length === 0 && serverlessFiles.length === 0) {
30
- console.log(chalk.yellow('⚠️ No .env or serverless.yml files found in the project\n'));
32
+ Logger.warning('No .env or serverless.yml files found in the project');
33
+ Logger.blank();
31
34
  return { success: false, issues: [] };
32
35
  }
33
36
 
34
- console.log(chalk.green(`✓ Found ${envFiles.length} .env file(s) and ${serverlessFiles.length} serverless.yml file(s)\n`));
37
+ Logger.success(`Found ${envFiles.length} .env file(s) and ${serverlessFiles.length} serverless.yml file(s)`);
38
+ Logger.blank();
35
39
 
36
40
  const parser = new EnvParser();
37
41
  const serverlessParser = new ServerlessParser();
@@ -43,15 +47,17 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
43
47
  const serverlessDir = path.dirname(serverlessFilePath);
44
48
  const relativePath = path.relative(rootDir, serverlessFilePath);
45
49
 
46
- console.log(chalk.cyan(`📂 Checking ${relativePath}\n`));
50
+ Logger.path(`Checking ${relativePath}`);
51
+ Logger.blank();
47
52
 
48
53
  // Parse serverless.yml
49
54
  const serverlessVars = serverlessParser.parse(serverlessFilePath);
50
- console.log(chalk.gray(` Found ${serverlessVars.size} variable(s) in serverless.yml`));
55
+ Logger.info(`Found ${serverlessVars.size} variable(s) in serverless.yml`, true);
51
56
 
52
57
  // Scan code files in this directory to see what's actually used
53
58
  const usedVars = await scanDirectoryForCodeVars(rootDir, serverlessDir, scanner);
54
- console.log(chalk.gray(` Found ${usedVars.size} variable(s) used in code\n`));
59
+ Logger.info(`Found ${usedVars.size} variable(s) used in code`, true);
60
+ Logger.blank();
55
61
 
56
62
  // Check for unused variables in serverless.yml
57
63
  const unusedServerlessVars: string[] = [];
@@ -87,24 +93,24 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
87
93
  }
88
94
 
89
95
  if (unusedServerlessVars.length > 0) {
90
- console.log(chalk.yellow.bold(' ⚠️ Unused variables in serverless.yml:'));
91
- unusedServerlessVars.forEach((varName, index) => {
92
- console.log(chalk.yellow(` ${index + 1}. ${varName}`));
96
+ Logger.warning('Unused variables in serverless.yml:', true);
97
+ unusedServerlessVars.forEach((varName) => {
98
+ Logger.warningItem(varName, 2);
93
99
  allIssues.push({
94
100
  type: 'unused',
95
101
  varName,
96
102
  details: `Defined in serverless.yml but never used in code`,
97
103
  });
98
104
  });
99
- console.log();
105
+ Logger.blank();
100
106
  }
101
107
 
102
108
  if (missingFromServerless.length > 0) {
103
- console.log(chalk.red.bold(' 🚨 Missing from serverless.yml:'));
104
- missingFromServerless.forEach((item, index) => {
105
- console.log(chalk.red(` ${index + 1}. ${item.varName}`));
109
+ Logger.error('Missing from serverless.yml:', true);
110
+ missingFromServerless.forEach((item) => {
111
+ Logger.errorItem(item.varName, 2);
106
112
  if (item.locations && item.locations.length > 0) {
107
- console.log(chalk.gray(` Used in: ${item.locations.slice(0, 2).join(', ')}`));
113
+ Logger.info(`Used in: ${item.locations.slice(0, 2).join(', ')}`, true);
108
114
  }
109
115
  allIssues.push({
110
116
  type: 'missing',
@@ -113,16 +119,17 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
113
119
  locations: item.locations,
114
120
  });
115
121
  });
116
- console.log();
122
+ Logger.blank();
117
123
  }
118
124
 
119
125
  if (unusedServerlessVars.length === 0 && missingFromServerless.length === 0) {
120
- console.log(chalk.green('No issues in this serverless.yml\n'));
126
+ Logger.success('No issues in this serverless.yml', true);
127
+ Logger.blank();
121
128
  }
122
129
 
123
130
  // Show skipped runtime variables in non-strict mode
124
131
  if (!strictMode && skippedRuntimeVars.length > 0) {
125
- console.log(chalk.gray(' ℹ️ Skipped known runtime variables (use --strict to show):'));
132
+ Logger.info('Skipped known runtime variables (use --strict to show):', true);
126
133
  // Group by category
127
134
  const grouped = new Map<string, string[]>();
128
135
  for (const { varName, category } of skippedRuntimeVars) {
@@ -132,9 +139,9 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
132
139
  grouped.get(category)!.push(varName);
133
140
  }
134
141
  for (const [category, vars] of grouped.entries()) {
135
- console.log(chalk.gray(` ${category}: ${vars.join(', ')}`));
142
+ Logger.info(`${category}: ${vars.join(', ')}`, true);
136
143
  }
137
- console.log();
144
+ Logger.blank();
138
145
  }
139
146
  }
140
147
 
@@ -144,21 +151,23 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
144
151
  const relativePath = path.relative(rootDir, envDir);
145
152
  const displayPath = relativePath || '.';
146
153
 
147
- console.log(chalk.cyan(`📂 Checking ${displayPath}/\n`));
154
+ Logger.path(`Checking ${displayPath}/`);
155
+ Logger.blank();
148
156
 
149
157
  // Step 3: Scan code files in this directory and subdirectories
150
158
  const usedVars = await scanDirectoryForVars(rootDir, envDir, scanner);
151
159
 
152
- console.log(chalk.gray(` Found ${usedVars.size} variable(s) used in this scope`));
160
+ Logger.info(`Found ${usedVars.size} variable(s) used in this scope`, true);
153
161
 
154
162
  // Step 4: Parse .env file
155
163
  const definedVars = parser.parse(envFilePath);
156
- console.log(chalk.gray(` Found ${definedVars.size} variable(s) in .env`));
164
+ Logger.info(`Found ${definedVars.size} variable(s) in .env`, true);
157
165
 
158
166
  // Step 5: Parse .env.example
159
167
  const examplePath = path.join(envDir, '.env.example');
160
168
  const exampleVars = parser.parseExample(examplePath);
161
- console.log(chalk.gray(` Found ${exampleVars.size} variable(s) in .env.example\n`));
169
+ Logger.info(`Found ${exampleVars.size} variable(s) in .env.example`, true);
170
+ Logger.blank();
162
171
 
163
172
  // Step 6: Analyze and find issues
164
173
  const result = analyzer.analyze(usedVars, definedVars, exampleVars);
@@ -170,55 +179,60 @@ export async function scanCommand(options: { ci?: boolean; strict?: boolean }) {
170
179
  const undocumentedIssues = result.issues.filter(i => i.type === 'undocumented');
171
180
 
172
181
  if (missingIssues.length > 0) {
173
- console.log(chalk.red.bold(' 🚨 Missing from .env:'));
174
- missingIssues.forEach((issue, index) => {
175
- console.log(chalk.red(` ${index + 1}. ${issue.varName}`));
182
+ Logger.error('Missing from .env:', true);
183
+ missingIssues.forEach((issue) => {
184
+ Logger.errorItem(issue.varName, 2);
176
185
  if (issue.locations && issue.locations.length > 0) {
177
- console.log(chalk.gray(` Used in: ${issue.locations.slice(0, 2).join(', ')}`));
186
+ Logger.info(`Used in: ${issue.locations.slice(0, 2).join(', ')}`, true);
178
187
  }
179
188
  });
180
- console.log();
189
+ Logger.blank();
181
190
  }
182
191
 
183
192
  if (unusedIssues.length > 0) {
184
- console.log(chalk.yellow.bold(' ⚠️ Unused variables:'));
185
- unusedIssues.forEach((issue, index) => {
186
- console.log(chalk.yellow(` ${index + 1}. ${issue.varName}`));
193
+ Logger.warning('Unused variables:', true);
194
+ unusedIssues.forEach((issue) => {
195
+ Logger.warningItem(issue.varName, 2);
187
196
  });
188
- console.log();
197
+ Logger.blank();
189
198
  }
190
199
 
191
200
  if (undocumentedIssues.length > 0) {
192
- console.log(chalk.blue.bold(' 📝 Missing from .env.example:'));
193
- undocumentedIssues.forEach((issue, index) => {
194
- console.log(chalk.blue(` ${index + 1}. ${issue.varName}`));
201
+ Logger.info('Missing from .env.example:', true);
202
+ undocumentedIssues.forEach((issue) => {
203
+ Logger.infoItem(issue.varName, 2);
195
204
  });
196
- console.log();
205
+ Logger.blank();
197
206
  }
198
207
 
199
208
  allIssues.push(...result.issues);
200
209
  } else {
201
- console.log(chalk.green('No issues in this directory\n'));
210
+ Logger.success('No issues in this directory', true);
211
+ Logger.blank();
202
212
  }
203
213
  }
204
214
 
205
215
  // Display summary
206
- console.log(chalk.bold('─'.repeat(50)));
216
+ Logger.divider();
207
217
  if (allIssues.length === 0) {
208
- console.log(chalk.green('\n✅ No issues found! All environment variables are in sync.\n'));
218
+ Logger.summary('No issues found! All environment variables are in sync.');
209
219
  return { success: true, issues: [] };
210
220
  }
211
221
 
212
- console.log(chalk.yellow(`\n⚠️ Total: ${allIssues.length} issue(s) across ${envFiles.length} location(s)\n`));
222
+ Logger.blank();
223
+ Logger.warning(`Total: ${allIssues.length} issue(s) across ${envFiles.length} location(s)`);
224
+ Logger.blank();
213
225
 
214
226
  // Suggest fix
215
227
  if (!options.ci) {
216
- console.log(chalk.cyan('💡 Run `envguard fix` to auto-generate .env.example files\n'));
228
+ Logger.info('Run `envguard fix` to auto-generate .env.example files');
229
+ Logger.blank();
217
230
  }
218
231
 
219
232
  // Exit with error code in CI mode
220
233
  if (options.ci) {
221
- console.log(chalk.red('Issues found. Exiting with error code 1.\n'));
234
+ Logger.error('Issues found. Exiting with error code 1.');
235
+ Logger.blank();
222
236
  process.exit(1);
223
237
  }
224
238
 
@@ -0,0 +1,146 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Minimalist red-themed logger inspired by Serverless Framework v4
5
+ * Uses minimal colors and simple symbols for a clean, professional look
6
+ */
7
+ export class Logger {
8
+ private static spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
9
+ private static spinnerInterval: NodeJS.Timeout | null = null;
10
+ private static currentFrame = 0;
11
+ private static spinnerMessage = '';
12
+
13
+ /**
14
+ * Start an animated spinner with a message
15
+ */
16
+ static startSpinner(message: string) {
17
+ this.spinnerMessage = message;
18
+ this.currentFrame = 0;
19
+
20
+ if (this.spinnerInterval) {
21
+ this.stopSpinner();
22
+ }
23
+
24
+ process.stdout.write('\n');
25
+ this.spinnerInterval = setInterval(() => {
26
+ const frame = this.spinnerFrames[this.currentFrame];
27
+ process.stdout.write(`\r${chalk.dim(frame)} ${this.spinnerMessage}`);
28
+ this.currentFrame = (this.currentFrame + 1) % this.spinnerFrames.length;
29
+ }, 80);
30
+ }
31
+
32
+ /**
33
+ * Stop the spinner and clear the line
34
+ */
35
+ static stopSpinner(finalMessage?: string) {
36
+ if (this.spinnerInterval) {
37
+ clearInterval(this.spinnerInterval);
38
+ this.spinnerInterval = null;
39
+ }
40
+
41
+ if (finalMessage) {
42
+ process.stdout.write(`\r${finalMessage}\n`);
43
+ } else {
44
+ process.stdout.write('\r\x1b[K'); // Clear line
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Log a header/section message (minimalist, no icons)
50
+ */
51
+ static header(message: string) {
52
+ console.log(chalk.dim(message));
53
+ }
54
+
55
+ /**
56
+ * Log a success message with checkmark
57
+ */
58
+ static success(message: string, indent = false) {
59
+ const prefix = indent ? ' ' : '';
60
+ console.log(`${prefix}${chalk.dim('✔')} ${message}`);
61
+ }
62
+
63
+ /**
64
+ * Log an error message (red themed)
65
+ */
66
+ static error(message: string, indent = false) {
67
+ const prefix = indent ? ' ' : '';
68
+ console.log(`${prefix}${chalk.red('✖')} ${chalk.red(message)}`);
69
+ }
70
+
71
+ /**
72
+ * Log a warning message (yellow/amber)
73
+ */
74
+ static warning(message: string, indent = false) {
75
+ const prefix = indent ? ' ' : '';
76
+ console.log(`${prefix}${chalk.hex('#FFA500')('⚠')} ${chalk.hex('#FFA500')(message)}`);
77
+ }
78
+
79
+ /**
80
+ * Log an info message (very minimal)
81
+ */
82
+ static info(message: string, indent = false) {
83
+ const prefix = indent ? ' ' : '';
84
+ console.log(`${prefix}${chalk.dim(message)}`);
85
+ }
86
+
87
+ /**
88
+ * Log a list item (error themed in red)
89
+ */
90
+ static errorItem(message: string, indent = 1) {
91
+ const prefix = ' '.repeat(indent);
92
+ console.log(`${prefix}${chalk.red('•')} ${chalk.red(message)}`);
93
+ }
94
+
95
+ /**
96
+ * Log a list item (warning themed, yellow/amber)
97
+ */
98
+ static warningItem(message: string, indent = 1) {
99
+ const prefix = ' '.repeat(indent);
100
+ console.log(`${prefix}${chalk.hex('#FFA500')('•')} ${chalk.hex('#FFA500')(message)}`);
101
+ }
102
+
103
+ /**
104
+ * Log a list item (info themed, very dim)
105
+ */
106
+ static infoItem(message: string, indent = 1) {
107
+ const prefix = ' '.repeat(indent);
108
+ console.log(`${prefix}${chalk.dim('•')} ${chalk.dim(message)}`);
109
+ }
110
+
111
+ /**
112
+ * Log a divider
113
+ */
114
+ static divider() {
115
+ console.log(chalk.dim('─'.repeat(50)));
116
+ }
117
+
118
+ /**
119
+ * Log a blank line
120
+ */
121
+ static blank() {
122
+ console.log();
123
+ }
124
+
125
+ /**
126
+ * Log a final summary message (Serverless-style)
127
+ */
128
+ static summary(message: string) {
129
+ console.log(`\n${chalk.dim('✔')} ${message}\n`);
130
+ }
131
+
132
+ /**
133
+ * Log a path/file reference (dimmed)
134
+ */
135
+ static path(message: string, indent = false) {
136
+ const prefix = indent ? ' ' : '';
137
+ console.log(`${prefix}${chalk.dim(message)}`);
138
+ }
139
+
140
+ /**
141
+ * Log deployment-style message (like "Deploying to stage dev")
142
+ */
143
+ static deployment(message: string) {
144
+ console.log(`\n${message}\n`);
145
+ }
146
+ }
@@ -1,11 +1,11 @@
1
1
  # Auto-generated by envguard
2
2
  # Do not put actual secrets in this file - use .env instead
3
3
 
4
- # Used in: test-project/src/lambda1/handler.js
4
+ # Used in: src/lambda1/handler.js
5
5
  # Format: your-secret-here
6
6
  CUSTOM_KEY=
7
7
 
8
- # Used in: test-project/src/lambda1/handler.js
8
+ # Used in: src/lambda1/handler.js
9
9
  # Format: your-secret-here
10
10
  STRIPE_SECRET_KEY=
11
11