safelaunch 1.0.18 → 1.0.20

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/safelaunch/src/scan.js +225 -210
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safelaunch",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Validate your environment before every push. Catch missing, empty, and misconfigured env variables before they break production.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1,62 +1,86 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const { track, shutdown } = require('./telemetry');
3
5
 
4
- // ─── Impact messages per check type ───────────────────────────────────────────
6
+ function impactBlock(lines) {
7
+ return ' Impact:\n' + lines.map(l => ' ' + l).join('\n');
8
+ }
5
9
 
6
10
  const IMPACT = {
7
11
  missing: (key) => impactBlock([
8
12
  'Your app cannot use ' + key,
9
- ' Will crash or behave incorrectly at runtime'
13
+ '\u2192 Will crash or behave incorrectly at runtime'
10
14
  ]),
11
15
  empty: (key) => impactBlock([
12
16
  key + ' is defined but has no value',
13
- ' Your app will treat it as blank this will break things'
17
+ '\u2192 Your app will treat it as blank \u2014 this will break things'
14
18
  ]),
15
19
  duplicate: (key) => impactBlock([
16
20
  key + ' is defined more than once',
17
- ' The last value silently wins likely not what you want'
21
+ '\u2192 The last value silently wins \u2014 likely not what you want'
18
22
  ]),
19
23
  depsNotInstalled: () => impactBlock([
20
24
  'node_modules is missing entirely',
21
- ' Your app will not start'
25
+ '\u2192 Your app will not start'
22
26
  ]),
23
27
  depsDrift: (dep) => impactBlock([
24
28
  dep + ' is in package.json but not installed',
25
- ' Any code that imports it will fail'
29
+ '\u2192 Any code that imports it will fail'
30
+ ]),
31
+ lockfileMissing: () => impactBlock([
32
+ 'package-lock.json is missing',
33
+ '\u2192 npm install will resolve different versions each time \u2014 inconsistent deploys'
26
34
  ]),
27
35
  prefixVite: (key) => impactBlock([
28
36
  key + ' is missing the VITE_ prefix',
29
- ' It will not be exposed to the browser your frontend will not see it'
37
+ '\u2192 It will not be exposed to the browser \u2014 your frontend will not see it'
30
38
  ]),
31
39
  prefixCra: (key) => impactBlock([
32
40
  key + ' is missing the REACT_APP_ prefix',
33
- ' It will not be exposed to the browser your frontend will not see it'
41
+ '\u2192 It will not be exposed to the browser \u2014 your frontend will not see it'
34
42
  ]),
35
43
  noEnvFile: () => impactBlock([
36
44
  'No .env file found',
37
- ' Every environment variable your app needs will be missing'
45
+ '\u2192 Every environment variable your app needs will be missing'
46
+ ]),
47
+ envInGit: (file) => impactBlock([
48
+ file + ' is committed to your git history',
49
+ '\u2192 Your secrets are exposed to anyone with repo access'
50
+ ]),
51
+ envNotIgnored: () => impactBlock([
52
+ '.env is not in your .gitignore',
53
+ '\u2192 You are one git add away from leaking your secrets'
54
+ ]),
55
+ envExampleOutOfSync: (keys) => impactBlock([
56
+ keys.join(', ') + ' are in .env but missing from .env.example',
57
+ '\u2192 Teammates cloning this repo will not know these vars exist'
58
+ ]),
59
+ hardcodedSecret: (file) => impactBlock([
60
+ 'Possible secret found hardcoded in ' + file,
61
+ '\u2192 Secrets in source code get committed and exposed in version history'
62
+ ]),
63
+ buildScriptMissing: () => impactBlock([
64
+ 'No build script found in package.json',
65
+ '\u2192 Your deploy pipeline will not know how to build this project'
66
+ ]),
67
+ uncommittedChanges: () => impactBlock([
68
+ 'You have uncommitted changes',
69
+ '\u2192 These changes will not be included in your deploy'
70
+ ]),
71
+ unpushedCommits: () => impactBlock([
72
+ 'You have commits that have not been pushed',
73
+ '\u2192 Your deploy may not include your latest changes'
38
74
  ]),
39
75
  };
40
76
 
41
- function impactBlock(lines) {
42
- return ' Impact:\n' + lines.map(l => ' ' + l).join('\n');
43
- }
44
-
45
- // ─── Divider ──────────────────────────────────────────────────────────────────
46
-
47
- const DIVIDER = '────────────────────────────';
48
-
49
- // ─── Helpers ──────────────────────────────────────────────────────────────────
77
+ const DIVIDER = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500';
50
78
 
51
79
  function detectProjectType(cwd) {
52
80
  if (fs.existsSync(path.join(cwd, 'vite.config.js')) ||
53
- fs.existsSync(path.join(cwd, 'vite.config.ts'))) {
54
- return 'vite';
55
- }
81
+ fs.existsSync(path.join(cwd, 'vite.config.ts'))) return 'vite';
56
82
  if (fs.existsSync(path.join(cwd, 'next.config.js')) ||
57
- fs.existsSync(path.join(cwd, 'next.config.ts'))) {
58
- return 'next';
59
- }
83
+ fs.existsSync(path.join(cwd, 'next.config.ts'))) return 'next';
60
84
  const packagePath = path.join(cwd, 'package.json');
61
85
  if (fs.existsSync(packagePath)) {
62
86
  const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
@@ -77,9 +101,7 @@ function scanFiles(dir, extensions, pattern, found = new Set()) {
77
101
  } else if (extensions.some(ext => item.endsWith(ext))) {
78
102
  const content = fs.readFileSync(full, 'utf8');
79
103
  const matches = content.matchAll(pattern);
80
- for (const match of matches) {
81
- found.add(match[1]);
82
- }
104
+ for (const match of matches) found.add(match[1]);
83
105
  }
84
106
  }
85
107
  return found;
@@ -119,28 +141,100 @@ function checkDependencies(cwd) {
119
141
  return { notInstalled: false, missing };
120
142
  }
121
143
 
122
- // ─── Render helpers ───────────────────────────────────────────────────────────
144
+ function checkLockfile(cwd) {
145
+ return !fs.existsSync(path.join(cwd, 'package-lock.json')) &&
146
+ !fs.existsSync(path.join(cwd, 'yarn.lock')) &&
147
+ !fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'));
148
+ }
123
149
 
124
- function renderIssue(number, title, impactLines) {
125
- return (
126
- number + '. ' + title + '\n' +
127
- '\n' +
128
- impactLines + '\n'
129
- );
150
+ function checkEnvInGit(cwd) {
151
+ try {
152
+ const tracked = execSync('git ls-files', { cwd, stdio: ['pipe', 'pipe', 'ignore'] }).toString();
153
+ const files = tracked.split('\n');
154
+ return files.filter(f => f === '.env' || /^\.env\.(production|staging|live)$/.test(f));
155
+ } catch (_) { return []; }
130
156
  }
131
157
 
132
- // ─── Main ─────────────────────────────────────────────────────────────────────
158
+ function checkEnvInGitignore(cwd) {
159
+ const gitignorePath = path.join(cwd, '.gitignore');
160
+ if (!fs.existsSync(gitignorePath)) return false;
161
+ const content = fs.readFileSync(gitignorePath, 'utf8');
162
+ return content.split('\n').some(line => line.trim() === '.env');
163
+ }
133
164
 
134
- async function scan() {
135
- // Lazy-load telemetry so it doesn't affect output
136
- let track = async () => {};
137
- let shutdown = async () => {};
165
+ function checkEnvExampleSync(cwd, envVars) {
166
+ const examplePath = path.join(cwd, '.env.example');
167
+ if (!fs.existsSync(examplePath)) return [];
168
+ const exampleContent = fs.readFileSync(examplePath, 'utf8');
169
+ const exampleKeys = new Set();
170
+ for (const line of exampleContent.split('\n')) {
171
+ const match = line.match(/^([^=]+)=/);
172
+ if (match) exampleKeys.add(match[1].trim());
173
+ }
174
+ return Object.keys(envVars).filter(k => !exampleKeys.has(k));
175
+ }
176
+
177
+ function checkHardcodedSecrets(cwd) {
178
+ const extensions = ['.js', '.ts', '.jsx', '.tsx'];
179
+ const secretPatterns = [
180
+ /sk_live_[a-zA-Z0-9]{20,}/,
181
+ /pk_live_[a-zA-Z0-9]{20,}/,
182
+ /AKIA[0-9A-Z]{16}/,
183
+ /-----BEGIN (RSA |EC )?PRIVATE KEY-----/,
184
+ /ghp_[a-zA-Z0-9]{36}/,
185
+ /xox[baprs]-[0-9a-zA-Z-]{10,}/,
186
+ ];
187
+ const hits = [];
188
+ function walk(dir) {
189
+ try {
190
+ const items = fs.readdirSync(dir);
191
+ for (const item of items) {
192
+ if (item === 'node_modules' || item === '.git') continue;
193
+ const full = path.join(dir, item);
194
+ const stat = fs.statSync(full);
195
+ if (stat.isDirectory()) {
196
+ walk(full);
197
+ } else if (extensions.some(ext => item.endsWith(ext))) {
198
+ const content = fs.readFileSync(full, 'utf8');
199
+ for (const pattern of secretPatterns) {
200
+ if (pattern.test(content)) {
201
+ hits.push(path.relative(cwd, full));
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ }
207
+ } catch (_) {}
208
+ }
209
+ walk(cwd);
210
+ return hits;
211
+ }
212
+
213
+ function checkBuildScript(cwd) {
214
+ const packagePath = path.join(cwd, 'package.json');
215
+ if (!fs.existsSync(packagePath)) return false;
216
+ const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
217
+ return !!(pkg.scripts && pkg.scripts.build);
218
+ }
219
+
220
+ function checkGitStatus(cwd) {
138
221
  try {
139
- const telemetry = require('./safelaunch/src/telemetry');
140
- track = telemetry.track;
141
- shutdown = telemetry.shutdown;
142
- } catch (_) {}
222
+ const status = execSync('git status --porcelain', { cwd, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
223
+ const uncommitted = status.length > 0;
224
+ let unpushed = false;
225
+ try {
226
+ const ahead = execSync('git log @{u}.. --oneline', { cwd, stdio: ['pipe', 'pipe', 'ignore'] }).toString().trim();
227
+ unpushed = ahead.length > 0;
228
+ } catch (_) {}
229
+ return { uncommitted, unpushed };
230
+ } catch (_) { return { uncommitted: false, unpushed: false }; }
231
+ }
232
+
233
+ function renderIssue(number, title, impact) {
234
+ return number + '. ' + title + '\n\n' + impact + '\n';
235
+ }
143
236
 
237
+ async function scan() {
144
238
  const cwd = process.cwd();
145
239
  const projectType = detectProjectType(cwd);
146
240
  const typeLabels = { vite: 'Vite', next: 'Next.js', cra: 'Create React App', node: 'Node.js' };
@@ -150,8 +244,6 @@ async function scan() {
150
244
  console.log('Detected: ' + typeLabels[projectType]);
151
245
  console.log('');
152
246
 
153
- // ── Collect env vars used in codebase ──
154
-
155
247
  const extensions = ['.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte'];
156
248
  let pattern;
157
249
  if (projectType === 'vite') {
@@ -163,190 +255,115 @@ async function scan() {
163
255
  }
164
256
 
165
257
  const found = scanFiles(cwd, extensions, pattern);
166
-
167
- // ── No env vars in codebase at all ──
168
-
169
- if (found.size === 0) {
170
- console.log('🛡️ Safelaunch Scan Report');
171
- console.log('');
172
- console.log('No environment variables found in your codebase.');
173
- console.log('');
174
- console.log(DIVIDER);
175
- console.log('');
176
- console.log('✅ Nothing to check');
177
- console.log('');
178
- console.log(' Your project does not appear to use process.env');
179
- console.log('');
180
- await track('safelaunch_scan_run', { project_type: projectType, vars_found: 0 });
181
- await shutdown();
182
- return;
183
- }
184
-
185
- // ── No .env file found ──
186
-
187
258
  const result = readEnvDetailed(cwd);
188
-
189
- if (!result) {
190
- const varList = [...found].sort();
191
- const criticals = [];
192
- let issueNum = 1;
193
-
194
- criticals.push(renderIssue(
195
- issueNum++,
196
- '.env file is missing',
197
- IMPACT.noEnvFile()
198
- ));
199
-
200
- for (const key of varList) {
201
- criticals.push(renderIssue(issueNum++, key + ' is missing', IMPACT.missing(key)));
202
- }
203
-
204
- console.log('🚨 Safelaunch Scan Report');
205
- console.log('');
206
- console.log('This project is NOT safe to deploy');
207
- console.log('');
208
- console.log((varList.length + 1) + ' issues found');
209
- console.log((varList.length + 1) + ' critical');
210
- console.log('');
211
- console.log(DIVIDER);
212
- console.log('');
213
- console.log('🚨 CRITICAL (will break your app)');
214
- console.log('');
215
- for (const block of criticals) {
216
- console.log(block);
217
- console.log('---');
218
- console.log('');
219
- }
220
- console.log(DIVIDER);
221
- console.log('');
222
- console.log('💡 Next step');
223
- console.log('');
224
- console.log(' Create a .env file with your environment variables,');
225
- console.log(' then run:');
226
- console.log('');
227
- console.log(' safelaunch init');
228
- console.log('');
229
- console.log(' Lock this configuration and prevent future breakage');
230
- console.log('');
231
- await track('safelaunch_scan_run', { project_type: projectType, vars_found: found.size, missing: found.size, no_env: true });
232
- await shutdown();
233
- return;
234
- }
235
-
236
- // ── Full scan ──
237
-
238
- const { envVars, duplicates } = result;
239
- const missing = [];
240
- const empty = [];
241
- const present = [];
242
-
243
- for (const key of [...found].sort()) {
244
- if (!(key in envVars)) {
245
- missing.push(key);
246
- } else if (envVars[key] === '') {
247
- empty.push(key);
248
- } else {
249
- present.push(key);
259
+ const drift = checkDependencies(cwd);
260
+ const lockfileMissing = checkLockfile(cwd);
261
+ const envInGit = checkEnvInGit(cwd);
262
+ const envIgnored = checkEnvInGitignore(cwd);
263
+ const hardcodedSecrets = checkHardcodedSecrets(cwd);
264
+ const hasBuildScript = checkBuildScript(cwd);
265
+ const gitStatus = checkGitStatus(cwd);
266
+
267
+ let missing = [], empty = [], present = [], duplicates = [], envExampleOutOfSync = [];
268
+
269
+ if (result) {
270
+ const { envVars, duplicates: dups } = result;
271
+ duplicates = dups;
272
+ envExampleOutOfSync = checkEnvExampleSync(cwd, envVars);
273
+ for (const key of [...found].sort()) {
274
+ if (!(key in envVars)) missing.push(key);
275
+ else if (envVars[key] === '') empty.push(key);
276
+ else present.push(key);
250
277
  }
278
+ } else {
279
+ missing = [...found].sort();
251
280
  }
252
281
 
253
- const drift = checkDependencies(cwd);
254
-
255
- // Prefix issues (warnings only)
256
282
  const prefixWarnings = [];
257
283
  for (const key of found) {
258
284
  if (key === 'NODE_ENV') continue;
259
- if (projectType === 'vite' && !key.startsWith('VITE_')) {
260
- prefixWarnings.push({ key, type: 'vite' });
261
- }
262
- if (projectType === 'cra' && !key.startsWith('REACT_APP_')) {
263
- prefixWarnings.push({ key, type: 'cra' });
264
- }
285
+ if (projectType === 'vite' && !key.startsWith('VITE_')) prefixWarnings.push({ key, type: 'vite' });
286
+ if (projectType === 'cra' && !key.startsWith('REACT_APP_')) prefixWarnings.push({ key, type: 'cra' });
265
287
  }
266
288
 
267
- // ── Build issue lists ──
289
+ const criticalIssues = [];
290
+ const warningIssues = [];
291
+ const passingChecks = [];
268
292
 
269
- const criticalIssues = []; // { title, impact }
270
- const warningIssues = []; // { title, impact }
271
- const passingChecks = []; // string[]
293
+ if (!result) criticalIssues.push({ title: '.env file is missing', impact: IMPACT.noEnvFile() });
294
+ for (const key of missing) criticalIssues.push({ title: key + ' is missing', impact: IMPACT.missing(key) });
295
+ for (const key of empty) criticalIssues.push({ title: key + ' is empty', impact: IMPACT.empty(key) });
296
+ if (drift && drift.notInstalled) criticalIssues.push({ title: 'Dependencies are not installed', impact: IMPACT.depsNotInstalled() });
297
+ for (const file of envInGit) criticalIssues.push({ title: file + ' is tracked by git', impact: IMPACT.envInGit(file) });
298
+ for (const file of hardcodedSecrets) criticalIssues.push({ title: 'Hardcoded secret in ' + file, impact: IMPACT.hardcodedSecret(file) });
272
299
 
273
- // Criticals: missing vars
274
- for (const key of missing) {
275
- criticalIssues.push({ title: key + ' is missing', impact: IMPACT.missing(key) });
276
- }
277
-
278
- // Criticals: empty vars
279
- for (const key of empty) {
280
- criticalIssues.push({ title: key + ' is empty', impact: IMPACT.empty(key) });
281
- }
282
-
283
- // Criticals: deps not installed at all
284
- if (drift && drift.notInstalled) {
285
- criticalIssues.push({ title: 'Dependencies are not installed', impact: IMPACT.depsNotInstalled() });
286
- }
287
-
288
- // Warnings: dependency drift
289
300
  if (drift && !drift.notInstalled && drift.missing.length > 0) {
290
- for (const dep of drift.missing) {
291
- warningIssues.push({ title: dep + ' is not installed', impact: IMPACT.depsDrift(dep) });
292
- }
301
+ for (const dep of drift.missing) warningIssues.push({ title: dep + ' is not installed', impact: IMPACT.depsDrift(dep) });
293
302
  }
294
-
295
- // Warnings: duplicate vars
296
- for (const key of duplicates) {
297
- warningIssues.push({ title: key + ' is defined more than once', impact: IMPACT.duplicate(key) });
298
- }
299
-
300
- // Warnings: prefix issues
303
+ if (lockfileMissing) warningIssues.push({ title: 'No lockfile found', impact: IMPACT.lockfileMissing() });
304
+ for (const key of duplicates) warningIssues.push({ title: key + ' is defined more than once', impact: IMPACT.duplicate(key) });
301
305
  for (const { key, type } of prefixWarnings) {
302
- if (type === 'vite') {
303
- warningIssues.push({ title: key + ' is missing VITE_ prefix', impact: IMPACT.prefixVite(key) });
304
- } else {
305
- warningIssues.push({ title: key + ' is missing REACT_APP_ prefix', impact: IMPACT.prefixCra(key) });
306
- }
306
+ if (type === 'vite') warningIssues.push({ title: key + ' is missing VITE_ prefix', impact: IMPACT.prefixVite(key) });
307
+ else warningIssues.push({ title: key + ' is missing REACT_APP_ prefix', impact: IMPACT.prefixCra(key) });
307
308
  }
309
+ if (!envIgnored && fs.existsSync(path.join(cwd, '.env'))) warningIssues.push({ title: '.env is not in .gitignore', impact: IMPACT.envNotIgnored() });
310
+ if (envExampleOutOfSync.length > 0) warningIssues.push({ title: '.env.example is out of sync', impact: IMPACT.envExampleOutOfSync(envExampleOutOfSync) });
311
+ if (!hasBuildScript && fs.existsSync(path.join(cwd, 'package.json'))) warningIssues.push({ title: 'No build script in package.json', impact: IMPACT.buildScriptMissing() });
312
+ if (gitStatus.uncommitted) warningIssues.push({ title: 'You have uncommitted changes', impact: IMPACT.uncommittedChanges() });
313
+ if (gitStatus.unpushed) warningIssues.push({ title: 'You have unpushed commits', impact: IMPACT.unpushedCommits() });
308
314
 
309
- // Passing checks
310
315
  if (result) passingChecks.push('Environment file detected');
316
+ if (envIgnored) passingChecks.push('.env is in .gitignore');
317
+ if (envInGit.length === 0) passingChecks.push('No env files tracked by git');
318
+ if (hardcodedSecrets.length === 0) passingChecks.push('No hardcoded secrets found');
319
+ if (!lockfileMissing) passingChecks.push('Lockfile present');
311
320
  if (!drift || (!drift.notInstalled && drift.missing.length === 0)) passingChecks.push('Dependencies installed');
312
321
  if (duplicates.length === 0) passingChecks.push('No duplicate variables');
313
- for (const key of present) {
314
- passingChecks.push(key + ' configured');
315
- }
322
+ if (hasBuildScript) passingChecks.push('Build script found');
323
+ if (!gitStatus.uncommitted) passingChecks.push('No uncommitted changes');
324
+ if (!gitStatus.unpushed) passingChecks.push('No unpushed commits');
325
+ for (const key of present) passingChecks.push(key + ' configured');
316
326
 
317
327
  const totalIssues = criticalIssues.length + warningIssues.length;
318
328
  const hasFailed = totalIssues > 0;
319
329
 
320
- // ─── Render ───────────────────────────────────────────────────────────────
330
+ if (found.size === 0 && totalIssues === 0) {
331
+ console.log('\uD83D\uDEE1\uFE0F Safelaunch Scan Report');
332
+ console.log('');
333
+ console.log('No environment variables found in your codebase.');
334
+ console.log('');
335
+ console.log(DIVIDER);
336
+ console.log('');
337
+ console.log('\u2705 Nothing to check');
338
+ console.log('');
339
+ console.log(' Your project does not appear to use process.env');
340
+ console.log('');
341
+ await track('safelaunch_scan_run', { project_type: projectType, vars_found: 0 });
342
+ await shutdown();
343
+ return;
344
+ }
321
345
 
322
346
  if (hasFailed) {
323
- const critCount = criticalIssues.length;
324
- const warnCount = warningIssues.length;
325
-
326
- let summary = totalIssues + ' issue' + (totalIssues !== 1 ? 's' : '') + ' found';
327
347
  const parts = [];
328
- if (critCount > 0) parts.push(critCount + ' critical');
329
- if (warnCount > 0) parts.push(warnCount + ' warning' + (warnCount !== 1 ? 's' : ''));
330
- summary += '\n' + parts.join(' · ');
348
+ if (criticalIssues.length > 0) parts.push(criticalIssues.length + ' critical');
349
+ if (warningIssues.length > 0) parts.push(warningIssues.length + ' warning' + (warningIssues.length !== 1 ? 's' : ''));
331
350
 
332
- console.log('🚨 Safelaunch Scan Report');
351
+ console.log('\uD83D\uDEA8 Safelaunch Scan Report');
333
352
  console.log('');
334
353
  console.log('This project is NOT safe to deploy');
335
354
  console.log('');
336
- console.log(summary);
355
+ console.log(totalIssues + ' issue' + (totalIssues !== 1 ? 's' : '') + ' found');
356
+ console.log(parts.join(' \u00B7 '));
337
357
  console.log('');
338
358
  console.log(DIVIDER);
339
359
  console.log('');
340
360
 
341
361
  if (criticalIssues.length > 0) {
342
- console.log('🚨 CRITICAL (will break your app)');
362
+ console.log('\uD83D\uDEA8 CRITICAL (will break your app)');
343
363
  console.log('');
344
364
  criticalIssues.forEach((issue, i) => {
345
365
  console.log(renderIssue(i + 1, issue.title, issue.impact));
346
- if (i < criticalIssues.length - 1) {
347
- console.log('---');
348
- console.log('');
349
- }
366
+ if (i < criticalIssues.length - 1) { console.log('---'); console.log(''); }
350
367
  });
351
368
  console.log('');
352
369
  console.log(DIVIDER);
@@ -354,15 +371,12 @@ async function scan() {
354
371
  }
355
372
 
356
373
  if (warningIssues.length > 0) {
357
- console.log('⚠️ WARNINGS (may cause issues)');
374
+ console.log('\u26A0\uFE0F WARNINGS (may cause issues)');
358
375
  console.log('');
359
376
  const offset = criticalIssues.length;
360
377
  warningIssues.forEach((issue, i) => {
361
378
  console.log(renderIssue(offset + i + 1, issue.title, issue.impact));
362
- if (i < warningIssues.length - 1) {
363
- console.log('---');
364
- console.log('');
365
- }
379
+ if (i < warningIssues.length - 1) { console.log('---'); console.log(''); }
366
380
  });
367
381
  console.log('');
368
382
  console.log(DIVIDER);
@@ -370,17 +384,15 @@ async function scan() {
370
384
  }
371
385
 
372
386
  if (passingChecks.length > 0) {
373
- console.log(' What\'s working (' + passingChecks.length + ' checks passed)');
387
+ console.log('\u2705 What\'s working (' + passingChecks.length + ' checks passed)');
374
388
  console.log('');
375
- for (const check of passingChecks) {
376
- console.log('- ' + check);
377
- }
389
+ for (const check of passingChecks) console.log('- ' + check);
378
390
  console.log('');
379
391
  console.log(DIVIDER);
380
392
  console.log('');
381
393
  }
382
394
 
383
- console.log('💡 Next step');
395
+ console.log('\uD83D\uDCA1 Next step');
384
396
  console.log('');
385
397
  console.log(' Run:');
386
398
  console.log(' safelaunch init');
@@ -389,9 +401,7 @@ async function scan() {
389
401
  console.log('');
390
402
 
391
403
  } else {
392
- // ── All clean ──
393
-
394
- console.log('🛡️ Safelaunch Scan Report');
404
+ console.log('\uD83D\uDEE1\uFE0F Safelaunch Scan Report');
395
405
  console.log('');
396
406
  console.log('Your project is safe to deploy');
397
407
  console.log('');
@@ -399,15 +409,13 @@ async function scan() {
399
409
  console.log('');
400
410
  console.log(DIVIDER);
401
411
  console.log('');
402
- console.log(' All checks passed');
412
+ console.log('\u2705 All checks passed');
403
413
  console.log('');
404
- for (const check of passingChecks) {
405
- console.log('- ' + check);
406
- }
414
+ for (const check of passingChecks) console.log('- ' + check);
407
415
  console.log('');
408
416
  console.log(DIVIDER);
409
417
  console.log('');
410
- console.log('🎉 You\'re good to go');
418
+ console.log('\uD83C\uDF89 You\'re good to go');
411
419
  console.log('');
412
420
  console.log(' Deploy with confidence');
413
421
  console.log('');
@@ -421,7 +429,14 @@ async function scan() {
421
429
  empty: empty.length,
422
430
  duplicates: duplicates.length,
423
431
  prefix_warnings: prefixWarnings.length,
424
- dependency_drift: !!(drift && drift.missing.length > 0)
432
+ dependency_drift: !!(drift && drift.missing.length > 0),
433
+ env_in_git: envInGit.length,
434
+ hardcoded_secrets: hardcodedSecrets.length,
435
+ lockfile_missing: lockfileMissing,
436
+ env_not_ignored: !envIgnored,
437
+ build_script_missing: !hasBuildScript,
438
+ uncommitted: gitStatus.uncommitted,
439
+ unpushed: gitStatus.unpushed
425
440
  });
426
441
 
427
442
  await shutdown();