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 +63 -34
- package/package.json +2 -2
- package/safelaunch/src/scan.js +271 -76
- package/safelaunch/src/validate.js +218 -75
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# safelaunch
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Ship without breaking production.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/safelaunch)
|
|
6
6
|
|
|
7
|
-
safelaunch validates your environment before every push.
|
|
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
|
-
🌐 [
|
|
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
|
-
##
|
|
22
|
-
|
|
23
|
-
**Step 1 — Generate your environment manifest**
|
|
21
|
+
## Try it right now — zero setup
|
|
24
22
|
```bash
|
|
25
|
-
safelaunch
|
|
23
|
+
npx safelaunch scan
|
|
26
24
|
```
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
No installation. No config. Just run it in any JavaScript project.
|
|
29
27
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|
package/safelaunch/src/scan.js
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
185
|
+
// ── No .env file found ──
|
|
119
186
|
|
|
120
187
|
const result = readEnvDetailed(cwd);
|
|
121
188
|
|
|
122
189
|
if (!result) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
128
|
-
console.log('
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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('
|
|
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('
|
|
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
|
|
163
|
+
warnings.push({ key, type: 'vite' });
|
|
105
164
|
}
|
|
106
165
|
if (projectType === 'cra' && !key.startsWith('REACT_APP_')) {
|
|
107
|
-
warnings.push(key
|
|
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
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
(
|
|
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
|
-
|
|
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,
|