safelaunch 1.0.19 → 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 +222 -159
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safelaunch",
3
- "version": "1.0.19",
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,9 +1,8 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { execSync } = require('child_process');
3
4
  const { track, shutdown } = require('./telemetry');
4
5
 
5
- // ─── Impact messages ──────────────────────────────────────────────────────────
6
-
7
6
  function impactBlock(lines) {
8
7
  return ' Impact:\n' + lines.map(l => ' ' + l).join('\n');
9
8
  }
@@ -11,51 +10,77 @@ function impactBlock(lines) {
11
10
  const IMPACT = {
12
11
  missing: (key) => impactBlock([
13
12
  'Your app cannot use ' + key,
14
- ' Will crash or behave incorrectly at runtime'
13
+ '\u2192 Will crash or behave incorrectly at runtime'
15
14
  ]),
16
15
  empty: (key) => impactBlock([
17
16
  key + ' is defined but has no value',
18
- ' 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'
19
18
  ]),
20
19
  duplicate: (key) => impactBlock([
21
20
  key + ' is defined more than once',
22
- ' The last value silently wins likely not what you want'
21
+ '\u2192 The last value silently wins \u2014 likely not what you want'
23
22
  ]),
24
23
  depsNotInstalled: () => impactBlock([
25
24
  'node_modules is missing entirely',
26
- ' Your app will not start'
25
+ '\u2192 Your app will not start'
27
26
  ]),
28
27
  depsDrift: (dep) => impactBlock([
29
28
  dep + ' is in package.json but not installed',
30
- ' 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'
31
34
  ]),
32
35
  prefixVite: (key) => impactBlock([
33
36
  key + ' is missing the VITE_ prefix',
34
- ' 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'
35
38
  ]),
36
39
  prefixCra: (key) => impactBlock([
37
40
  key + ' is missing the REACT_APP_ prefix',
38
- ' 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'
39
42
  ]),
40
43
  noEnvFile: () => impactBlock([
41
44
  'No .env file found',
42
- ' 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'
43
74
  ]),
44
75
  };
45
76
 
46
- const DIVIDER = '────────────────────────────';
47
-
48
- // ─── 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';
49
78
 
50
79
  function detectProjectType(cwd) {
51
80
  if (fs.existsSync(path.join(cwd, 'vite.config.js')) ||
52
- fs.existsSync(path.join(cwd, 'vite.config.ts'))) {
53
- return 'vite';
54
- }
81
+ fs.existsSync(path.join(cwd, 'vite.config.ts'))) return 'vite';
55
82
  if (fs.existsSync(path.join(cwd, 'next.config.js')) ||
56
- fs.existsSync(path.join(cwd, 'next.config.ts'))) {
57
- return 'next';
58
- }
83
+ fs.existsSync(path.join(cwd, 'next.config.ts'))) return 'next';
59
84
  const packagePath = path.join(cwd, 'package.json');
60
85
  if (fs.existsSync(packagePath)) {
61
86
  const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
@@ -76,9 +101,7 @@ function scanFiles(dir, extensions, pattern, found = new Set()) {
76
101
  } else if (extensions.some(ext => item.endsWith(ext))) {
77
102
  const content = fs.readFileSync(full, 'utf8');
78
103
  const matches = content.matchAll(pattern);
79
- for (const match of matches) {
80
- found.add(match[1]);
81
- }
104
+ for (const match of matches) found.add(match[1]);
82
105
  }
83
106
  }
84
107
  return found;
@@ -118,23 +141,106 @@ function checkDependencies(cwd) {
118
141
  return { notInstalled: false, missing };
119
142
  }
120
143
 
121
- function renderIssue(number, title, impact) {
122
- return (
123
- number + '. ' + title + '\n' +
124
- '\n' +
125
- impact + '\n'
126
- );
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'));
127
148
  }
128
149
 
129
- // ─── Main ─────────────────────────────────────────────────────────────────────
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 []; }
156
+ }
130
157
 
131
- async function scan() {
132
- console.log('');
133
- console.log('Scanning your project...');
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
+ }
164
+
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) {
221
+ try {
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
+ }
134
236
 
237
+ async function scan() {
135
238
  const cwd = process.cwd();
136
239
  const projectType = detectProjectType(cwd);
137
240
  const typeLabels = { vite: 'Vite', next: 'Next.js', cra: 'Create React App', node: 'Node.js' };
241
+
242
+ console.log('');
243
+ console.log('Scanning your project...');
138
244
  console.log('Detected: ' + typeLabels[projectType]);
139
245
  console.log('');
140
246
 
@@ -149,158 +255,115 @@ async function scan() {
149
255
  }
150
256
 
151
257
  const found = scanFiles(cwd, extensions, pattern);
152
-
153
- if (found.size === 0) {
154
- console.log('🛡️ Safelaunch Scan Report');
155
- console.log('');
156
- console.log('No environment variables found in your codebase.');
157
- console.log('');
158
- console.log(DIVIDER);
159
- console.log('');
160
- console.log('✅ Nothing to check');
161
- console.log('');
162
- console.log(' Your project does not appear to use process.env');
163
- console.log('');
164
- await track('safelaunch_scan_run', { project_type: projectType, vars_found: 0 });
165
- await shutdown();
166
- return;
167
- }
168
-
169
258
  const result = readEnvDetailed(cwd);
170
-
171
- if (!result) {
172
- const varList = [...found].sort();
173
- const criticals = [];
174
- let issueNum = 1;
175
-
176
- criticals.push(renderIssue(issueNum++, '.env file is missing', IMPACT.noEnvFile()));
177
- for (const key of varList) {
178
- criticals.push(renderIssue(issueNum++, key + ' is missing', IMPACT.missing(key)));
179
- }
180
-
181
- console.log('🚨 Safelaunch Scan Report');
182
- console.log('');
183
- console.log('This project is NOT safe to deploy');
184
- console.log('');
185
- console.log((varList.length + 1) + ' issues found');
186
- console.log((varList.length + 1) + ' critical');
187
- console.log('');
188
- console.log(DIVIDER);
189
- console.log('');
190
- console.log('🚨 CRITICAL (will break your app)');
191
- console.log('');
192
- for (const block of criticals) {
193
- console.log(block);
194
- console.log('---');
195
- console.log('');
196
- }
197
- console.log(DIVIDER);
198
- console.log('');
199
- console.log('💡 Next step');
200
- console.log('');
201
- console.log(' Create a .env file with your environment variables,');
202
- console.log(' then run:');
203
- console.log('');
204
- console.log(' safelaunch init');
205
- console.log('');
206
- console.log(' Lock this configuration and prevent future breakage');
207
- console.log('');
208
- await track('safelaunch_scan_run', { project_type: projectType, vars_found: found.size, missing: found.size, no_env: true });
209
- await shutdown();
210
- return;
211
- }
212
-
213
- const { envVars, duplicates } = result;
214
- const missing = [];
215
- const empty = [];
216
- const present = [];
217
-
218
- for (const key of [...found].sort()) {
219
- if (!(key in envVars)) {
220
- missing.push(key);
221
- } else if (envVars[key] === '') {
222
- empty.push(key);
223
- } else {
224
- 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);
225
277
  }
278
+ } else {
279
+ missing = [...found].sort();
226
280
  }
227
281
 
228
- const drift = checkDependencies(cwd);
229
-
230
282
  const prefixWarnings = [];
231
283
  for (const key of found) {
232
284
  if (key === 'NODE_ENV') continue;
233
- if (projectType === 'vite' && !key.startsWith('VITE_')) {
234
- prefixWarnings.push({ key, type: 'vite' });
235
- }
236
- if (projectType === 'cra' && !key.startsWith('REACT_APP_')) {
237
- prefixWarnings.push({ key, type: 'cra' });
238
- }
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' });
239
287
  }
240
288
 
241
289
  const criticalIssues = [];
242
290
  const warningIssues = [];
243
291
  const passingChecks = [];
244
292
 
245
- for (const key of missing) {
246
- criticalIssues.push({ title: key + ' is missing', impact: IMPACT.missing(key) });
247
- }
248
- for (const key of empty) {
249
- criticalIssues.push({ title: key + ' is empty', impact: IMPACT.empty(key) });
250
- }
251
- if (drift && drift.notInstalled) {
252
- criticalIssues.push({ title: 'Dependencies are not installed', impact: IMPACT.depsNotInstalled() });
253
- }
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) });
299
+
254
300
  if (drift && !drift.notInstalled && drift.missing.length > 0) {
255
- for (const dep of drift.missing) {
256
- warningIssues.push({ title: dep + ' is not installed', impact: IMPACT.depsDrift(dep) });
257
- }
258
- }
259
- for (const key of duplicates) {
260
- warningIssues.push({ title: key + ' is defined more than once', impact: IMPACT.duplicate(key) });
301
+ for (const dep of drift.missing) warningIssues.push({ title: dep + ' is not installed', impact: IMPACT.depsDrift(dep) });
261
302
  }
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) });
262
305
  for (const { key, type } of prefixWarnings) {
263
- if (type === 'vite') {
264
- warningIssues.push({ title: key + ' is missing VITE_ prefix', impact: IMPACT.prefixVite(key) });
265
- } else {
266
- warningIssues.push({ title: key + ' is missing REACT_APP_ prefix', impact: IMPACT.prefixCra(key) });
267
- }
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) });
268
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() });
269
314
 
270
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');
271
320
  if (!drift || (!drift.notInstalled && drift.missing.length === 0)) passingChecks.push('Dependencies installed');
272
321
  if (duplicates.length === 0) passingChecks.push('No duplicate variables');
273
- for (const key of present) {
274
- passingChecks.push(key + ' configured');
275
- }
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');
276
326
 
277
327
  const totalIssues = criticalIssues.length + warningIssues.length;
278
328
  const hasFailed = totalIssues > 0;
279
329
 
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
+ }
345
+
280
346
  if (hasFailed) {
281
347
  const parts = [];
282
348
  if (criticalIssues.length > 0) parts.push(criticalIssues.length + ' critical');
283
349
  if (warningIssues.length > 0) parts.push(warningIssues.length + ' warning' + (warningIssues.length !== 1 ? 's' : ''));
284
350
 
285
- console.log('🚨 Safelaunch Scan Report');
351
+ console.log('\uD83D\uDEA8 Safelaunch Scan Report');
286
352
  console.log('');
287
353
  console.log('This project is NOT safe to deploy');
288
354
  console.log('');
289
355
  console.log(totalIssues + ' issue' + (totalIssues !== 1 ? 's' : '') + ' found');
290
- console.log(parts.join(' · '));
356
+ console.log(parts.join(' \u00B7 '));
291
357
  console.log('');
292
358
  console.log(DIVIDER);
293
359
  console.log('');
294
360
 
295
361
  if (criticalIssues.length > 0) {
296
- console.log('🚨 CRITICAL (will break your app)');
362
+ console.log('\uD83D\uDEA8 CRITICAL (will break your app)');
297
363
  console.log('');
298
364
  criticalIssues.forEach((issue, i) => {
299
365
  console.log(renderIssue(i + 1, issue.title, issue.impact));
300
- if (i < criticalIssues.length - 1) {
301
- console.log('---');
302
- console.log('');
303
- }
366
+ if (i < criticalIssues.length - 1) { console.log('---'); console.log(''); }
304
367
  });
305
368
  console.log('');
306
369
  console.log(DIVIDER);
@@ -308,15 +371,12 @@ async function scan() {
308
371
  }
309
372
 
310
373
  if (warningIssues.length > 0) {
311
- console.log('⚠️ WARNINGS (may cause issues)');
374
+ console.log('\u26A0\uFE0F WARNINGS (may cause issues)');
312
375
  console.log('');
313
376
  const offset = criticalIssues.length;
314
377
  warningIssues.forEach((issue, i) => {
315
378
  console.log(renderIssue(offset + i + 1, issue.title, issue.impact));
316
- if (i < warningIssues.length - 1) {
317
- console.log('---');
318
- console.log('');
319
- }
379
+ if (i < warningIssues.length - 1) { console.log('---'); console.log(''); }
320
380
  });
321
381
  console.log('');
322
382
  console.log(DIVIDER);
@@ -324,17 +384,15 @@ async function scan() {
324
384
  }
325
385
 
326
386
  if (passingChecks.length > 0) {
327
- console.log("✅ What's working (" + passingChecks.length + ' checks passed)');
387
+ console.log('\u2705 What\'s working (' + passingChecks.length + ' checks passed)');
328
388
  console.log('');
329
- for (const check of passingChecks) {
330
- console.log('- ' + check);
331
- }
389
+ for (const check of passingChecks) console.log('- ' + check);
332
390
  console.log('');
333
391
  console.log(DIVIDER);
334
392
  console.log('');
335
393
  }
336
394
 
337
- console.log('💡 Next step');
395
+ console.log('\uD83D\uDCA1 Next step');
338
396
  console.log('');
339
397
  console.log(' Run:');
340
398
  console.log(' safelaunch init');
@@ -343,7 +401,7 @@ async function scan() {
343
401
  console.log('');
344
402
 
345
403
  } else {
346
- console.log('🛡️ Safelaunch Scan Report');
404
+ console.log('\uD83D\uDEE1\uFE0F Safelaunch Scan Report');
347
405
  console.log('');
348
406
  console.log('Your project is safe to deploy');
349
407
  console.log('');
@@ -351,15 +409,13 @@ async function scan() {
351
409
  console.log('');
352
410
  console.log(DIVIDER);
353
411
  console.log('');
354
- console.log(' All checks passed');
412
+ console.log('\u2705 All checks passed');
355
413
  console.log('');
356
- for (const check of passingChecks) {
357
- console.log('- ' + check);
358
- }
414
+ for (const check of passingChecks) console.log('- ' + check);
359
415
  console.log('');
360
416
  console.log(DIVIDER);
361
417
  console.log('');
362
- console.log("🎉 You're good to go");
418
+ console.log('\uD83C\uDF89 You\'re good to go');
363
419
  console.log('');
364
420
  console.log(' Deploy with confidence');
365
421
  console.log('');
@@ -373,7 +429,14 @@ async function scan() {
373
429
  empty: empty.length,
374
430
  duplicates: duplicates.length,
375
431
  prefix_warnings: prefixWarnings.length,
376
- 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
377
440
  });
378
441
 
379
442
  await shutdown();