safelaunch 1.0.16 → 1.0.18

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.
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # safelaunch
2
2
 
3
- > Stop breaking production over environment variables.
3
+ > Ship without breaking production.
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/safelaunch.svg)](https://www.npmjs.com/package/safelaunch)
6
6
 
7
- safelaunch validates your environment before every push. It catches missing variables, misconfigurations, and dependency issues before they reach production — in seconds.
7
+ safelaunch validates your environment before every push. Catches missing variables, empty values, duplicates, dependency drift, and prefix misconfigurations — before they reach production.
8
8
 
9
9
  Works with **Node.js, Next.js, Vite, and Create React App**. Not just backend. Any JavaScript project.
10
10
 
11
- 🌐 [safelaunch.dev](https://karthicedric7-cloud.github.io/Orches)
11
+ 🌐 [orches.dev](https://karthicedric7-cloud.github.io/Orches)
12
12
 
13
13
  ---
14
14
 
@@ -18,21 +18,39 @@ safelaunch runs **entirely on your machine**. It never sends your environment va
18
18
 
19
19
  ---
20
20
 
21
- ## Two commands. That's it.
22
-
23
- **Step 1 — Generate your environment manifest**
21
+ ## Try it right now — zero setup
24
22
  ```bash
25
- safelaunch init
23
+ npx safelaunch scan
26
24
  ```
27
25
 
28
- Scans your codebase, finds every environment variable your app uses, and generates an `env.manifest.json` file automatically.
26
+ No installation. No config. Just run it in any JavaScript project.
29
27
 
30
- **Step 2 Validate before you push**
31
- ```bash
32
- safelaunch validate
28
+ Scans your entire codebase, checks your `.env`, and tells you exactly what would break your next deploy.
33
29
  ```
30
+ Scanning your project...
31
+
32
+ Detected: Next.js
33
+
34
+ Found 8 env variables in your codebase
35
+
36
+ ⚠️ DUPLICATE VARIABLES (1 found)
37
+
38
+ DATABASE_URL defined more than once in .env
39
+
40
+ ❌ MISSING VARIABLES (2 found)
34
41
 
35
- Runs 11 checks against your `.env` file and tells you exactly what will break before it does.
42
+ NEXTAUTH_SECRET missing from .env
43
+ STRIPE_SECRET_KEY missing from .env
44
+
45
+ ✅ PASSING (6)
46
+
47
+ API_KEY present
48
+ NODE_ENV present
49
+
50
+ Your next deploy would have failed.
51
+
52
+ Run safelaunch init to lock this in permanently.
53
+ ```
36
54
 
37
55
  ---
38
56
 
@@ -43,7 +61,27 @@ npm install -g safelaunch
43
61
 
44
62
  ---
45
63
 
46
- ## Never break a push again — install the git hook
64
+ ## Lock it in permanently
65
+
66
+ Once you've seen what scan finds, lock it in with two commands:
67
+
68
+ **Step 1 — Generate your environment manifest**
69
+ ```bash
70
+ safelaunch init
71
+ ```
72
+
73
+ Scans your codebase and generates an `env.manifest.json` contract file automatically.
74
+
75
+ **Step 2 — Validate before every push**
76
+ ```bash
77
+ safelaunch validate
78
+ ```
79
+
80
+ Runs 11 checks against your `.env` and tells you exactly what will break before it does.
81
+
82
+ ---
83
+
84
+ ## Never think about it again — install the git hook
47
85
  ```bash
48
86
  safelaunch hook install
49
87
  ```
@@ -52,7 +90,17 @@ Blocks `git push` automatically if validation fails. Set it once, forget about i
52
90
 
53
91
  ---
54
92
 
55
- ## What it checks
93
+ ## What scan checks
94
+
95
+ 1. Missing variables
96
+ 2. Empty variables
97
+ 3. Duplicate variables
98
+ 4. Dependencies not installed
99
+ 5. Dependency drift
100
+ 6. `VITE_` prefix validation
101
+ 7. `REACT_APP_` prefix validation
102
+
103
+ ## What validate checks
56
104
 
57
105
  1. Missing required variables
58
106
  2. Empty variables
@@ -68,28 +116,9 @@ Blocks `git push` automatically if validation fails. Set it once, forget about i
68
116
 
69
117
  ---
70
118
 
71
- ## Example output
72
- ```
73
- Running safelaunch...
74
-
75
- ❌ MISSING VARIABLES (2 found)
76
- DATABASE_URL required but missing from .env
77
- REDIS_URL required but missing from .env
78
-
79
- ✅ PASSING (9)
80
- API_KEY present
81
- NODE_ENV present
82
- ...
83
-
84
- Your environment is not ready for production.
85
- Fix the issues above and run safelaunch again.
86
- ```
87
-
88
- ---
89
-
90
119
  ## CI Integration
91
120
 
92
- Add this to your GitHub Actions workflow to block deployments automatically:
121
+ Block deployments automatically by adding this to your GitHub Actions workflow:
93
122
  ```yaml
94
123
  - name: Install safelaunch
95
124
  run: npm install -g safelaunch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safelaunch",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
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": {
@@ -31,4 +31,4 @@
31
31
  "dependencies": {
32
32
  "posthog-node": "^5.28.2"
33
33
  }
34
- }
34
+ }
@@ -1,6 +1,52 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { track, shutdown } = require('./telemetry');
3
+
4
+ // ─── Impact messages per check type ───────────────────────────────────────────
5
+
6
+ const IMPACT = {
7
+ missing: (key) => impactBlock([
8
+ 'Your app cannot use ' + key,
9
+ '→ Will crash or behave incorrectly at runtime'
10
+ ]),
11
+ empty: (key) => impactBlock([
12
+ key + ' is defined but has no value',
13
+ '→ Your app will treat it as blank — this will break things'
14
+ ]),
15
+ duplicate: (key) => impactBlock([
16
+ key + ' is defined more than once',
17
+ '→ The last value silently wins — likely not what you want'
18
+ ]),
19
+ depsNotInstalled: () => impactBlock([
20
+ 'node_modules is missing entirely',
21
+ '→ Your app will not start'
22
+ ]),
23
+ depsDrift: (dep) => impactBlock([
24
+ dep + ' is in package.json but not installed',
25
+ '→ Any code that imports it will fail'
26
+ ]),
27
+ prefixVite: (key) => impactBlock([
28
+ key + ' is missing the VITE_ prefix',
29
+ '→ It will not be exposed to the browser — your frontend will not see it'
30
+ ]),
31
+ prefixCra: (key) => impactBlock([
32
+ key + ' is missing the REACT_APP_ prefix',
33
+ '→ It will not be exposed to the browser — your frontend will not see it'
34
+ ]),
35
+ noEnvFile: () => impactBlock([
36
+ 'No .env file found',
37
+ '→ Every environment variable your app needs will be missing'
38
+ ]),
39
+ };
40
+
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 ──────────────────────────────────────────────────────────────────
4
50
 
5
51
  function detectProjectType(cwd) {
6
52
  if (fs.existsSync(path.join(cwd, 'vite.config.js')) ||
@@ -59,20 +105,6 @@ function readEnvDetailed(cwd) {
59
105
  return { envVars, duplicates: [...duplicates] };
60
106
  }
61
107
 
62
- function checkPrefixes(foundVars, envVars, projectType) {
63
- const warnings = [];
64
- for (const key of foundVars) {
65
- if (key === 'NODE_ENV') continue;
66
- if (projectType === 'vite' && !key.startsWith('VITE_')) {
67
- warnings.push(key + ' missing VITE_ prefix (won\'t be exposed to client)');
68
- }
69
- if (projectType === 'cra' && !key.startsWith('REACT_APP_')) {
70
- warnings.push(key + ' missing REACT_APP_ prefix (won\'t be exposed to client)');
71
- }
72
- }
73
- return warnings;
74
- }
75
-
76
108
  function checkDependencies(cwd) {
77
109
  const packagePath = path.join(cwd, 'package.json');
78
110
  const modulesPath = path.join(cwd, 'node_modules');
@@ -87,17 +119,41 @@ function checkDependencies(cwd) {
87
119
  return { notInstalled: false, missing };
88
120
  }
89
121
 
122
+ // ─── Render helpers ───────────────────────────────────────────────────────────
123
+
124
+ function renderIssue(number, title, impactLines) {
125
+ return (
126
+ number + '. ' + title + '\n' +
127
+ '\n' +
128
+ impactLines + '\n'
129
+ );
130
+ }
131
+
132
+ // ─── Main ─────────────────────────────────────────────────────────────────────
133
+
90
134
  async function scan() {
91
- console.log('\nScanning your project...\n');
135
+ // Lazy-load telemetry so it doesn't affect output
136
+ let track = async () => {};
137
+ let shutdown = async () => {};
138
+ try {
139
+ const telemetry = require('./safelaunch/src/telemetry');
140
+ track = telemetry.track;
141
+ shutdown = telemetry.shutdown;
142
+ } catch (_) {}
92
143
 
93
144
  const cwd = process.cwd();
94
145
  const projectType = detectProjectType(cwd);
95
146
  const typeLabels = { vite: 'Vite', next: 'Next.js', cra: 'Create React App', node: 'Node.js' };
96
- console.log('Detected: ' + typeLabels[projectType] + '\n');
147
+
148
+ console.log('');
149
+ console.log('Scanning your project...');
150
+ console.log('Detected: ' + typeLabels[projectType]);
151
+ console.log('');
152
+
153
+ // ── Collect env vars used in codebase ──
97
154
 
98
155
  const extensions = ['.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte'];
99
156
  let pattern;
100
-
101
157
  if (projectType === 'vite') {
102
158
  pattern = /import\.meta\.env\.([A-Z_][A-Z0-9_]*)/g;
103
159
  } else if (projectType === 'cra') {
@@ -108,29 +164,77 @@ async function scan() {
108
164
 
109
165
  const found = scanFiles(cwd, extensions, pattern);
110
166
 
167
+ // ── No env vars in codebase at all ──
168
+
111
169
  if (found.size === 0) {
112
- console.log('No environment variables found in your codebase.\n');
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('');
113
180
  await track('safelaunch_scan_run', { project_type: projectType, vars_found: 0 });
114
181
  await shutdown();
115
182
  return;
116
183
  }
117
184
 
118
- console.log('Found ' + found.size + ' env variables in your codebase\n');
185
+ // ── No .env file found ──
119
186
 
120
187
  const result = readEnvDetailed(cwd);
121
188
 
122
189
  if (!result) {
123
- console.log('❌ No .env file found — all variables would be missing.\n');
124
- for (const key of [...found].sort()) {
125
- console.log(' ' + key);
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)));
126
202
  }
127
- console.log('\nYour next deploy would have failed.\n');
128
- console.log('Run safelaunch init to lock this in permanently.\n');
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('');
129
231
  await track('safelaunch_scan_run', { project_type: projectType, vars_found: found.size, missing: found.size, no_env: true });
130
232
  await shutdown();
131
233
  return;
132
234
  }
133
235
 
236
+ // ── Full scan ──
237
+
134
238
  const { envVars, duplicates } = result;
135
239
  const missing = [];
136
240
  const empty = [];
@@ -146,76 +250,167 @@ async function scan() {
146
250
  }
147
251
  }
148
252
 
149
- const prefixWarnings = checkPrefixes(found, envVars, projectType);
150
253
  const drift = checkDependencies(cwd);
151
254
 
152
- let hasFailed = false;
153
-
154
- if (duplicates.length > 0) {
155
- hasFailed = true;
156
- console.log('⚠️ DUPLICATE VARIABLES (' + duplicates.length + ' found)\n');
157
- for (const key of duplicates) {
158
- console.log(' ' + key + ' defined more than once in .env');
255
+ // Prefix issues (warnings only)
256
+ const prefixWarnings = [];
257
+ for (const key of found) {
258
+ 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' });
159
264
  }
160
- console.log('');
161
265
  }
162
266
 
163
- if (drift) {
164
- if (drift.notInstalled) {
165
- hasFailed = true;
166
- console.log('⚠️ DEPENDENCIES NOT INSTALLED\n');
167
- console.log(' node_modules not found. Run npm install.\n');
168
- } else if (drift.missing.length > 0) {
169
- hasFailed = true;
170
- console.log('⚠️ DEPENDENCY DRIFT (' + drift.missing.length + ' found)\n');
171
- for (const dep of drift.missing) {
172
- console.log(' ' + dep + ' in package.json but not installed');
173
- }
174
- console.log('');
175
- }
267
+ // ── Build issue lists ──
268
+
269
+ const criticalIssues = []; // { title, impact }
270
+ const warningIssues = []; // { title, impact }
271
+ const passingChecks = []; // string[]
272
+
273
+ // Criticals: missing vars
274
+ for (const key of missing) {
275
+ criticalIssues.push({ title: key + ' is missing', impact: IMPACT.missing(key) });
176
276
  }
177
277
 
178
- if (prefixWarnings.length > 0) {
179
- hasFailed = true;
180
- console.log('⚠️ PREFIX WARNINGS (' + prefixWarnings.length + ' found)\n');
181
- for (const w of prefixWarnings) {
182
- console.log(' ' + w);
183
- }
184
- console.log('');
278
+ // Criticals: empty vars
279
+ for (const key of empty) {
280
+ criticalIssues.push({ title: key + ' is empty', impact: IMPACT.empty(key) });
185
281
  }
186
282
 
187
- if (empty.length > 0) {
188
- hasFailed = true;
189
- console.log(' EMPTY VARIABLES (' + empty.length + ' found)\n');
190
- for (const key of empty) {
191
- console.log(' ' + key + ' present but empty in .env');
192
- }
193
- console.log('');
283
+ // Criticals: deps not installed at all
284
+ if (drift && drift.notInstalled) {
285
+ criticalIssues.push({ title: 'Dependencies are not installed', impact: IMPACT.depsNotInstalled() });
194
286
  }
195
287
 
196
- if (missing.length > 0) {
197
- hasFailed = true;
198
- console.log('❌ MISSING VARIABLES (' + missing.length + ' found)\n');
199
- for (const key of missing) {
200
- console.log(' ' + key + ' missing from .env');
288
+ // Warnings: dependency drift
289
+ 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) });
201
292
  }
202
- console.log('');
203
293
  }
204
294
 
205
- if (present.length > 0) {
206
- console.log('✅ PASSING (' + present.length + ')\n');
207
- for (const key of present) {
208
- console.log(' ' + key + ' present');
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
301
+ 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) });
209
306
  }
210
- console.log('');
211
307
  }
212
308
 
309
+ // Passing checks
310
+ if (result) passingChecks.push('Environment file detected');
311
+ if (!drift || (!drift.notInstalled && drift.missing.length === 0)) passingChecks.push('Dependencies installed');
312
+ if (duplicates.length === 0) passingChecks.push('No duplicate variables');
313
+ for (const key of present) {
314
+ passingChecks.push(key + ' configured');
315
+ }
316
+
317
+ const totalIssues = criticalIssues.length + warningIssues.length;
318
+ const hasFailed = totalIssues > 0;
319
+
320
+ // ─── Render ───────────────────────────────────────────────────────────────
321
+
213
322
  if (hasFailed) {
214
- console.log('Your next deploy would have failed.\n');
215
- console.log('Run safelaunch init to lock this in permanently.\n');
323
+ const critCount = criticalIssues.length;
324
+ const warnCount = warningIssues.length;
325
+
326
+ let summary = totalIssues + ' issue' + (totalIssues !== 1 ? 's' : '') + ' found';
327
+ 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(' · ');
331
+
332
+ console.log('🚨 Safelaunch Scan Report');
333
+ console.log('');
334
+ console.log('This project is NOT safe to deploy');
335
+ console.log('');
336
+ console.log(summary);
337
+ console.log('');
338
+ console.log(DIVIDER);
339
+ console.log('');
340
+
341
+ if (criticalIssues.length > 0) {
342
+ console.log('🚨 CRITICAL (will break your app)');
343
+ console.log('');
344
+ criticalIssues.forEach((issue, i) => {
345
+ console.log(renderIssue(i + 1, issue.title, issue.impact));
346
+ if (i < criticalIssues.length - 1) {
347
+ console.log('---');
348
+ console.log('');
349
+ }
350
+ });
351
+ console.log('');
352
+ console.log(DIVIDER);
353
+ console.log('');
354
+ }
355
+
356
+ if (warningIssues.length > 0) {
357
+ console.log('⚠️ WARNINGS (may cause issues)');
358
+ console.log('');
359
+ const offset = criticalIssues.length;
360
+ warningIssues.forEach((issue, i) => {
361
+ console.log(renderIssue(offset + i + 1, issue.title, issue.impact));
362
+ if (i < warningIssues.length - 1) {
363
+ console.log('---');
364
+ console.log('');
365
+ }
366
+ });
367
+ console.log('');
368
+ console.log(DIVIDER);
369
+ console.log('');
370
+ }
371
+
372
+ if (passingChecks.length > 0) {
373
+ console.log('✅ What\'s working (' + passingChecks.length + ' checks passed)');
374
+ console.log('');
375
+ for (const check of passingChecks) {
376
+ console.log('- ' + check);
377
+ }
378
+ console.log('');
379
+ console.log(DIVIDER);
380
+ console.log('');
381
+ }
382
+
383
+ console.log('💡 Next step');
384
+ console.log('');
385
+ console.log(' Run:');
386
+ console.log(' safelaunch init');
387
+ console.log('');
388
+ console.log(' Lock this configuration and prevent future breakage');
389
+ console.log('');
390
+
216
391
  } else {
217
- console.log('Your project is clean. Nothing would break your next deploy.\n');
218
- console.log('Run safelaunch init to lock this in permanently.\n');
392
+ // ── All clean ──
393
+
394
+ console.log('🛡️ Safelaunch Scan Report');
395
+ console.log('');
396
+ console.log('Your project is safe to deploy');
397
+ console.log('');
398
+ console.log('No issues found');
399
+ console.log('');
400
+ console.log(DIVIDER);
401
+ console.log('');
402
+ console.log('✅ All checks passed');
403
+ console.log('');
404
+ for (const check of passingChecks) {
405
+ console.log('- ' + check);
406
+ }
407
+ console.log('');
408
+ console.log(DIVIDER);
409
+ console.log('');
410
+ console.log('🎉 You\'re good to go');
411
+ console.log('');
412
+ console.log(' Deploy with confidence');
413
+ console.log('');
219
414
  }
220
415
 
221
416
  await track('safelaunch_scan_run', {
@@ -2,6 +2,59 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { track, shutdown } = require('./telemetry');
4
4
 
5
+ // ─── Impact messages ──────────────────────────────────────────────────────────
6
+
7
+ function impactBlock(lines) {
8
+ return ' Impact:\n' + lines.map(l => ' ' + l).join('\n');
9
+ }
10
+
11
+ const IMPACT = {
12
+ missing: (key) => impactBlock([
13
+ key + ' is required but not in your .env',
14
+ '→ Your app will crash or behave incorrectly at runtime'
15
+ ]),
16
+ empty: (key) => impactBlock([
17
+ key + ' is present but has no value',
18
+ '→ Your app will treat it as blank — this will break things'
19
+ ]),
20
+ runtimeMismatch: (required, actual) => impactBlock([
21
+ 'Expected Node ' + required + ', found Node ' + actual,
22
+ '→ Unexpected runtime errors may occur'
23
+ ]),
24
+ duplicate: (key) => impactBlock([
25
+ key + ' is defined more than once in .env',
26
+ '→ The last value silently wins — likely not what you want'
27
+ ]),
28
+ depsNotInstalled: () => impactBlock([
29
+ 'node_modules is missing entirely',
30
+ '→ Your app will not start'
31
+ ]),
32
+ depsDrift: (dep) => impactBlock([
33
+ dep + ' is in package.json but not installed',
34
+ '→ Any code that imports it will fail'
35
+ ]),
36
+ exampleMissing: (key) => impactBlock([
37
+ key + ' is in .env.example but missing from your .env',
38
+ '→ This was likely required — your app may break'
39
+ ]),
40
+ prefixVite: (key) => impactBlock([
41
+ key + ' is missing the VITE_ prefix',
42
+ '→ It will not be exposed to the browser — your frontend will not see it'
43
+ ]),
44
+ prefixCra: (key) => impactBlock([
45
+ key + ' is missing the REACT_APP_ prefix',
46
+ '→ It will not be exposed to the browser — your frontend will not see it'
47
+ ]),
48
+ priority: (key) => impactBlock([
49
+ key + ' exists in both .env and .env.local',
50
+ '→ .env.local takes priority — .env value will be ignored'
51
+ ]),
52
+ };
53
+
54
+ const DIVIDER = '────────────────────────────';
55
+
56
+ // ─── File helpers ─────────────────────────────────────────────────────────────
57
+
5
58
  function detectProjectType(cwd) {
6
59
  if (fs.existsSync(path.join(cwd, 'vite.config.js')) ||
7
60
  fs.existsSync(path.join(cwd, 'vite.config.ts'))) {
@@ -23,7 +76,11 @@ function detectProjectType(cwd) {
23
76
  function readManifest() {
24
77
  const manifestPath = path.join(process.cwd(), 'env.manifest.json');
25
78
  if (!fs.existsSync(manifestPath)) {
26
- console.log('\nNo env.manifest.json found. Run safelaunch init first.\n');
79
+ console.log('');
80
+ console.log('No env.manifest.json found.');
81
+ console.log('');
82
+ console.log('Run safelaunch init to generate one.');
83
+ console.log('');
27
84
  process.exit(1);
28
85
  }
29
86
  return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
@@ -32,7 +89,9 @@ function readManifest() {
32
89
  function readEnv() {
33
90
  const envPath = path.join(process.cwd(), '.env');
34
91
  if (!fs.existsSync(envPath)) {
35
- console.log('\nNo .env file found.\n');
92
+ console.log('');
93
+ console.log('No .env file found.');
94
+ console.log('');
36
95
  process.exit(1);
37
96
  }
38
97
  const envVars = {};
@@ -101,13 +160,10 @@ function checkPrefixes(envVars, projectType) {
101
160
  for (const key of Object.keys(envVars)) {
102
161
  if (key === 'NODE_ENV') continue;
103
162
  if (projectType === 'vite' && !key.startsWith('VITE_')) {
104
- warnings.push(key + ' missing VITE_ prefix (won\'t be exposed to client)');
163
+ warnings.push({ key, type: 'vite' });
105
164
  }
106
165
  if (projectType === 'cra' && !key.startsWith('REACT_APP_')) {
107
- warnings.push(key + ' missing REACT_APP_ prefix (won\'t be exposed to client)');
108
- }
109
- if (projectType === 'next' && key.startsWith('NEXT_PUBLIC_') === false) {
110
- // server-side only — no warning needed
166
+ warnings.push({ key, type: 'cra' });
111
167
  }
112
168
  }
113
169
  return warnings;
@@ -132,15 +188,29 @@ function checkEnvPriority(cwd, projectType) {
132
188
  if (fileVars['.env'] && fileVars['.env.local']) {
133
189
  for (const key of Object.keys(fileVars['.env'])) {
134
190
  if (fileVars['.env.local'][key]) {
135
- warnings.push(key + ' in both .env and .env.local (.env.local takes priority)');
191
+ warnings.push(key);
136
192
  }
137
193
  }
138
194
  }
139
195
  return warnings;
140
196
  }
141
197
 
198
+ // ─── Render helpers ───────────────────────────────────────────────────────────
199
+
200
+ function renderIssue(number, title, impact) {
201
+ return (
202
+ number + '. ' + title + '\n' +
203
+ '\n' +
204
+ impact + '\n'
205
+ );
206
+ }
207
+
208
+ // ─── Main ─────────────────────────────────────────────────────────────────────
209
+
142
210
  async function validate() {
143
- console.log('\nRunning safelaunch...\n');
211
+ console.log('');
212
+ console.log('Running safelaunch...');
213
+ console.log('');
144
214
 
145
215
  const cwd = process.cwd();
146
216
  const manifest = readManifest();
@@ -148,7 +218,10 @@ async function validate() {
148
218
 
149
219
  const projectType = manifest.projectType || detectProjectType(cwd);
150
220
  const typeLabels = { vite: 'Vite', next: 'Next.js', cra: 'Create React App', node: 'Node.js' };
151
- console.log('project type: ' + (typeLabels[projectType] || 'Node.js') + '\n');
221
+ console.log('Detected: ' + (typeLabels[projectType] || 'Node.js'));
222
+ console.log('');
223
+
224
+ // ── Collect results ──
152
225
 
153
226
  const missing = [];
154
227
  const empty = [];
@@ -170,89 +243,136 @@ async function validate() {
170
243
  const prefixWarnings = checkPrefixes(envVars, projectType);
171
244
  const priorityWarnings = checkEnvPriority(cwd, projectType);
172
245
 
173
- if (runtimeMismatch) {
174
- console.log('⚠️ RUNTIME MISMATCH\n');
175
- console.log(' Node required: ' + runtimeMismatch.required);
176
- console.log(' Node actual: ' + runtimeMismatch.actual);
177
- console.log('');
178
- }
246
+ // ── Build issue lists ──
179
247
 
180
- if (duplicates.length > 0) {
181
- console.log('⚠️ DUPLICATE VARIABLES (' + duplicates.length + ' found)\n');
182
- for (const key of duplicates) {
183
- console.log(' ' + key + ' defined more than once in .env');
184
- }
185
- console.log('');
248
+ const criticalIssues = [];
249
+ const warningIssues = [];
250
+ const passingChecks = [];
251
+
252
+ // Criticals
253
+ for (const key of missing) {
254
+ criticalIssues.push({ title: key + ' is missing', impact: IMPACT.missing(key) });
255
+ }
256
+ for (const key of empty) {
257
+ criticalIssues.push({ title: key + ' is empty', impact: IMPACT.empty(key) });
258
+ }
259
+ if (drift && drift.notInstalled) {
260
+ criticalIssues.push({ title: 'Dependencies are not installed', impact: IMPACT.depsNotInstalled() });
186
261
  }
187
262
 
188
- if (drift) {
189
- if (drift.notInstalled) {
190
- console.log('⚠️ DEPENDENCIES NOT INSTALLED\n');
191
- console.log(' node_modules not found. Run npm install.\n');
192
- } else if (drift.missing.length > 0) {
193
- console.log('⚠️ DEPENDENCY DRIFT (' + drift.missing.length + ' found)\n');
194
- for (const dep of drift.missing) {
195
- console.log(' ' + dep + ' in package.json but not installed');
196
- }
197
- console.log('');
263
+ // Warnings
264
+ if (runtimeMismatch) {
265
+ warningIssues.push({
266
+ title: 'Node.js version mismatch',
267
+ impact: IMPACT.runtimeMismatch(runtimeMismatch.required, runtimeMismatch.actual)
268
+ });
269
+ }
270
+ for (const key of duplicates) {
271
+ warningIssues.push({ title: key + ' is defined more than once', impact: IMPACT.duplicate(key) });
272
+ }
273
+ if (drift && !drift.notInstalled) {
274
+ for (const dep of drift.missing) {
275
+ warningIssues.push({ title: dep + ' is not installed', impact: IMPACT.depsDrift(dep) });
198
276
  }
199
277
  }
200
-
201
- if (exampleMissing.length > 0) {
202
- console.log('⚠️ MISSING FROM .env.example (' + exampleMissing.length + ' found)\n');
203
- for (const key of exampleMissing) {
204
- console.log(' ' + key + ' in .env.example but missing from .env');
278
+ for (const key of exampleMissing) {
279
+ warningIssues.push({ title: key + ' is missing (from .env.example)', impact: IMPACT.exampleMissing(key) });
280
+ }
281
+ for (const { key, type } of prefixWarnings) {
282
+ if (type === 'vite') {
283
+ warningIssues.push({ title: key + ' is missing VITE_ prefix', impact: IMPACT.prefixVite(key) });
284
+ } else {
285
+ warningIssues.push({ title: key + ' is missing REACT_APP_ prefix', impact: IMPACT.prefixCra(key) });
205
286
  }
206
- console.log('');
287
+ }
288
+ for (const key of priorityWarnings) {
289
+ warningIssues.push({ title: key + ' has an env file priority conflict', impact: IMPACT.priority(key) });
207
290
  }
208
291
 
209
- if (prefixWarnings.length > 0) {
210
- console.log('⚠️ PREFIX WARNINGS (' + prefixWarnings.length + ' found)\n');
211
- for (const w of prefixWarnings) {
212
- console.log(' ' + w);
213
- }
214
- console.log('');
292
+ // Passing checks
293
+ passingChecks.push('env.manifest.json found');
294
+ passingChecks.push('Environment file detected');
295
+ if (!drift || (!drift.notInstalled && drift.missing.length === 0)) passingChecks.push('Dependencies installed');
296
+ if (duplicates.length === 0) passingChecks.push('No duplicate variables');
297
+ if (!runtimeMismatch && manifest.runtime) passingChecks.push('Runtime version matches');
298
+ for (const key of passing) {
299
+ passingChecks.push(key + ' configured');
215
300
  }
216
301
 
217
- if (priorityWarnings.length > 0) {
218
- console.log('⚠️ ENV FILE PRIORITY (' + priorityWarnings.length + ' found)\n');
219
- for (const w of priorityWarnings) {
220
- console.log(' ' + w);
221
- }
302
+ const totalIssues = criticalIssues.length + warningIssues.length;
303
+ const hasFailed = totalIssues > 0;
304
+
305
+ // ─── Render ───────────────────────────────────────────────────────────────
306
+
307
+ if (hasFailed) {
308
+ const critCount = criticalIssues.length;
309
+ const warnCount = warningIssues.length;
310
+
311
+ const parts = [];
312
+ if (critCount > 0) parts.push(critCount + ' critical');
313
+ if (warnCount > 0) parts.push(warnCount + ' warning' + (warnCount !== 1 ? 's' : ''));
314
+
315
+ console.log('🚨 Safelaunch Scan Report');
316
+ console.log('');
317
+ console.log('This project is NOT safe to deploy');
318
+ console.log('');
319
+ console.log(totalIssues + ' issue' + (totalIssues !== 1 ? 's' : '') + ' found');
320
+ console.log(parts.join(' · '));
321
+ console.log('');
322
+ console.log(DIVIDER);
222
323
  console.log('');
223
- }
224
324
 
225
- if (empty.length > 0) {
226
- console.log(' EMPTY VARIABLES (' + empty.length + ' found)\n');
227
- for (const key of empty) {
228
- console.log(' ' + key + ' required but empty in .env');
325
+ if (criticalIssues.length > 0) {
326
+ console.log('🚨 CRITICAL (will break your app)');
327
+ console.log('');
328
+ criticalIssues.forEach((issue, i) => {
329
+ console.log(renderIssue(i + 1, issue.title, issue.impact));
330
+ if (i < criticalIssues.length - 1) {
331
+ console.log('---');
332
+ console.log('');
333
+ }
334
+ });
335
+ console.log('');
336
+ console.log(DIVIDER);
337
+ console.log('');
229
338
  }
230
- console.log('');
231
- }
232
339
 
233
- if (missing.length > 0) {
234
- console.log(' MISSING VARIABLES (' + missing.length + ' found)\n');
235
- for (const key of missing) {
236
- console.log(' ' + key + ' required but missing from .env');
340
+ if (warningIssues.length > 0) {
341
+ console.log('⚠️ WARNINGS (may cause issues)');
342
+ console.log('');
343
+ const offset = criticalIssues.length;
344
+ warningIssues.forEach((issue, i) => {
345
+ console.log(renderIssue(offset + i + 1, issue.title, issue.impact));
346
+ if (i < warningIssues.length - 1) {
347
+ console.log('---');
348
+ console.log('');
349
+ }
350
+ });
351
+ console.log('');
352
+ console.log(DIVIDER);
353
+ console.log('');
237
354
  }
238
- console.log('');
239
- }
240
355
 
241
- if (passing.length > 0) {
242
- console.log('PASSING (' + passing.length + ')\n');
243
- for (const key of passing) {
244
- console.log(' ' + key + ' present');
356
+ if (passingChecks.length > 0) {
357
+ console.log("What's working (" + passingChecks.length + ' checks passed)');
358
+ console.log('');
359
+ for (const check of passingChecks) {
360
+ console.log('- ' + check);
361
+ }
362
+ console.log('');
363
+ console.log(DIVIDER);
364
+ console.log('');
245
365
  }
246
- console.log('');
247
- }
248
366
 
249
- const hasFailed = runtimeMismatch || missing.length > 0 || empty.length > 0 ||
250
- duplicates.length > 0 || exampleMissing.length > 0 ||
251
- prefixWarnings.length > 0 || priorityWarnings.length > 0 ||
252
- (drift && (drift.notInstalled || drift.missing.length > 0));
367
+ console.log('💡 Next step');
368
+ console.log('');
369
+ console.log(' Fix the issues above, then run:');
370
+ console.log(' safelaunch scan');
371
+ console.log('');
372
+ console.log(' Or re-run the contract lock:');
373
+ console.log(' safelaunch init');
374
+ console.log('');
253
375
 
254
- if (hasFailed) {
255
- console.log('Your environment is not ready for production.\n');
256
376
  await track('safelaunch_validate_run', {
257
377
  project_type: projectType,
258
378
  passed: false,
@@ -266,8 +386,31 @@ async function validate() {
266
386
  });
267
387
  await shutdown();
268
388
  process.exit(1);
389
+
269
390
  } else {
270
- console.log('Your environment is ready for production.\n');
391
+ // ── All clean ──
392
+
393
+ console.log('🛡️ Safelaunch Scan Report');
394
+ console.log('');
395
+ console.log('Your project is safe to deploy');
396
+ console.log('');
397
+ console.log('No issues found');
398
+ console.log('');
399
+ console.log(DIVIDER);
400
+ console.log('');
401
+ console.log('✅ All checks passed');
402
+ console.log('');
403
+ for (const check of passingChecks) {
404
+ console.log('- ' + check);
405
+ }
406
+ console.log('');
407
+ console.log(DIVIDER);
408
+ console.log('');
409
+ console.log("🎉 You're good to go");
410
+ console.log('');
411
+ console.log(' Deploy with confidence');
412
+ console.log('');
413
+
271
414
  await track('safelaunch_validate_run', {
272
415
  project_type: projectType,
273
416
  passed: true,