@wchen.ai/env-from-example 1.0.2 → 1.0.3
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/dist/env-from-example.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wchen.ai/env-from-example",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Interactive and non-interactive CLI to set up .env from .env.example",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"env",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"schema.json"
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
|
-
"build": "tsc",
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
36
|
"prepublishOnly": "pnpm run build",
|
|
37
37
|
"start": "node dist/env-from-example.js",
|
|
38
38
|
"dev": "tsx env-from-example.ts",
|
package/dist/setup-env.js
DELETED
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import crypto from 'crypto';
|
|
6
|
-
import { Command } from 'commander';
|
|
7
|
-
import { input, confirm, select } from '@inquirer/prompts';
|
|
8
|
-
import pc from 'picocolors';
|
|
9
|
-
import dotenv from 'dotenv';
|
|
10
|
-
export function getRootDirFromArgv() {
|
|
11
|
-
const argv = process.argv;
|
|
12
|
-
const cwdIdx = argv.indexOf('--cwd');
|
|
13
|
-
if (cwdIdx !== -1 && argv[cwdIdx + 1]) {
|
|
14
|
-
return path.resolve(argv[cwdIdx + 1]);
|
|
15
|
-
}
|
|
16
|
-
return process.cwd();
|
|
17
|
-
}
|
|
18
|
-
export function parseEnvExample(rootDir) {
|
|
19
|
-
const envExamplePath = path.join(rootDir, '.env.example');
|
|
20
|
-
if (!fs.existsSync(envExamplePath)) {
|
|
21
|
-
throw new Error(`.env.example not found at ${envExamplePath}`);
|
|
22
|
-
}
|
|
23
|
-
const content = fs.readFileSync(envExamplePath, 'utf-8');
|
|
24
|
-
const lines = content.split('\n');
|
|
25
|
-
const variables = [];
|
|
26
|
-
let currentComments = [];
|
|
27
|
-
let version = null;
|
|
28
|
-
for (const line of lines) {
|
|
29
|
-
const trimmed = line.trim();
|
|
30
|
-
if (trimmed.startsWith('# ENV_SCHEMA_VERSION=')) {
|
|
31
|
-
const match = trimmed.match(/# ENV_SCHEMA_VERSION="?([^"]+)"?/);
|
|
32
|
-
if (match)
|
|
33
|
-
version = match[1];
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (trimmed.startsWith('#')) {
|
|
37
|
-
const maybeVarMatch = trimmed.match(/^#\s*([A-Z0-9_]+)=(.*)$/);
|
|
38
|
-
if (maybeVarMatch && !trimmed.includes('------')) {
|
|
39
|
-
let val = maybeVarMatch[2].trim();
|
|
40
|
-
// Remove trailing comments in the value if any (e.g., "value" # comment)
|
|
41
|
-
val = val.split(' #')[0].trim();
|
|
42
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
43
|
-
val = val.slice(1, -1);
|
|
44
|
-
}
|
|
45
|
-
variables.push({
|
|
46
|
-
key: maybeVarMatch[1],
|
|
47
|
-
defaultValue: val,
|
|
48
|
-
comment: currentComments.join('\n'),
|
|
49
|
-
required: false,
|
|
50
|
-
isCommentedOut: true,
|
|
51
|
-
});
|
|
52
|
-
currentComments = [];
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (trimmed.includes('------')) {
|
|
56
|
-
currentComments = [trimmed]; // Keep section header
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
currentComments.push(trimmed.replace(/^#\s*/, ''));
|
|
60
|
-
}
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (!trimmed) {
|
|
64
|
-
currentComments = [];
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const match = trimmed.match(/^([A-Z0-9_]+)=(.*)$/);
|
|
68
|
-
if (match) {
|
|
69
|
-
let val = match[2].trim();
|
|
70
|
-
val = val.split(' #')[0].trim(); // Remove inline comments
|
|
71
|
-
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
72
|
-
val = val.slice(1, -1);
|
|
73
|
-
}
|
|
74
|
-
const fullComment = currentComments.join(' ');
|
|
75
|
-
const required = fullComment.toUpperCase().includes('[REQUIRED]');
|
|
76
|
-
variables.push({
|
|
77
|
-
key: match[1],
|
|
78
|
-
defaultValue: val,
|
|
79
|
-
comment: currentComments.join('\n'),
|
|
80
|
-
required,
|
|
81
|
-
isCommentedOut: false,
|
|
82
|
-
});
|
|
83
|
-
currentComments = [];
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
currentComments = [];
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return { version, variables };
|
|
90
|
-
}
|
|
91
|
-
export function getExistingEnvVersion(content) {
|
|
92
|
-
const match = content.match(/# ENV_SCHEMA_VERSION="?([^"\n]+)"?/);
|
|
93
|
-
return match ? match[1] : null;
|
|
94
|
-
}
|
|
95
|
-
const SETUP_ENV_CREDIT = '# setup-env (https://www.npmjs.com/package/setup-env)';
|
|
96
|
-
/** Serialize variables and optional version to .env.example content (polished format). */
|
|
97
|
-
export function serializeEnvExample(version, variables) {
|
|
98
|
-
const lines = [SETUP_ENV_CREDIT, ''];
|
|
99
|
-
if (version !== null && version !== undefined) {
|
|
100
|
-
lines.push(`# ENV_SCHEMA_VERSION="${version}"`);
|
|
101
|
-
lines.push('');
|
|
102
|
-
}
|
|
103
|
-
let lastSection = '';
|
|
104
|
-
for (const v of variables) {
|
|
105
|
-
const sectionMatch = v.comment.split('\n').find((l) => l.includes('------'));
|
|
106
|
-
if (sectionMatch && sectionMatch !== lastSection) {
|
|
107
|
-
if (lines.length > 0 && lines[lines.length - 1] !== '')
|
|
108
|
-
lines.push('');
|
|
109
|
-
lines.push(sectionMatch);
|
|
110
|
-
lastSection = sectionMatch;
|
|
111
|
-
}
|
|
112
|
-
const commentLines = v.comment
|
|
113
|
-
.split('\n')
|
|
114
|
-
.filter((l) => l && !l.includes('------'));
|
|
115
|
-
for (const c of commentLines) {
|
|
116
|
-
lines.push('# ' + c.replace(/^#\s*/, ''));
|
|
117
|
-
}
|
|
118
|
-
const needsQuotes = /[\s#"']/.test(v.defaultValue) || v.defaultValue === '';
|
|
119
|
-
const value = needsQuotes && v.defaultValue !== ''
|
|
120
|
-
? `"${v.defaultValue.replace(/"/g, '\\"')}"`
|
|
121
|
-
: v.defaultValue;
|
|
122
|
-
if (v.isCommentedOut) {
|
|
123
|
-
lines.push(`# ${v.key}=${value}`);
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
lines.push(`${v.key}=${value}`);
|
|
127
|
-
}
|
|
128
|
-
lines.push('');
|
|
129
|
-
}
|
|
130
|
-
return lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
131
|
-
}
|
|
132
|
-
/** Deduplicate variables by key (keep first occurrence to preserve section order). */
|
|
133
|
-
function dedupeVariables(variables) {
|
|
134
|
-
const seen = new Set();
|
|
135
|
-
return variables.filter((v) => {
|
|
136
|
-
if (seen.has(v.key))
|
|
137
|
-
return false;
|
|
138
|
-
seen.add(v.key);
|
|
139
|
-
return true;
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
/** Turn UPPER_SNAKE_KEY into "Upper snake key" for use as default comment. */
|
|
143
|
-
function humanizeEnvKey(key) {
|
|
144
|
-
return key
|
|
145
|
-
.split('_')
|
|
146
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
147
|
-
.join(' ');
|
|
148
|
-
}
|
|
149
|
-
/** Ensure each variable has a comment (description), [REQUIRED] if needed, and default documented. */
|
|
150
|
-
function enrichVariablesForPolish(variables) {
|
|
151
|
-
return variables.map((v) => {
|
|
152
|
-
const commentLines = v.comment
|
|
153
|
-
.split('\n')
|
|
154
|
-
.filter((l) => l && !l.includes('------'));
|
|
155
|
-
const sectionHeader = v.comment.split('\n').find((l) => l.includes('------'));
|
|
156
|
-
let description = commentLines.find((l) => !l.toUpperCase().includes('[REQUIRED]'))?.trim() || '';
|
|
157
|
-
if (!description) {
|
|
158
|
-
description = humanizeEnvKey(v.key);
|
|
159
|
-
}
|
|
160
|
-
const parts = [description];
|
|
161
|
-
if (v.required) {
|
|
162
|
-
parts.push('[REQUIRED]');
|
|
163
|
-
}
|
|
164
|
-
const defaultLabel = v.defaultValue === '' ? 'Default: (empty)' : `Default: ${v.defaultValue}`;
|
|
165
|
-
parts.push(defaultLabel);
|
|
166
|
-
const newComment = [sectionHeader, parts.join(' ').trim()]
|
|
167
|
-
.filter(Boolean)
|
|
168
|
-
.join('\n');
|
|
169
|
-
return { ...v, comment: newComment };
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
/** Default ENV_SCHEMA_VERSION when missing: use package.json version or "1.0.0". */
|
|
173
|
-
function getDefaultSchemaVersion(rootDir) {
|
|
174
|
-
const pkgPath = path.join(rootDir, 'package.json');
|
|
175
|
-
if (fs.existsSync(pkgPath)) {
|
|
176
|
-
try {
|
|
177
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
178
|
-
if (pkg.version && typeof pkg.version === 'string') {
|
|
179
|
-
return pkg.version;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
// ignore
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return '1.0.0';
|
|
187
|
-
}
|
|
188
|
-
/** Polish .env.example: add ENV_SCHEMA_VERSION if missing, add comments and default docs per variable, normalize format, dedupe keys. */
|
|
189
|
-
export function polishEnvExample(rootDir) {
|
|
190
|
-
const envExamplePath = path.join(rootDir, '.env.example');
|
|
191
|
-
if (!fs.existsSync(envExamplePath)) {
|
|
192
|
-
throw new Error(`.env.example not found at ${envExamplePath}`);
|
|
193
|
-
}
|
|
194
|
-
const { version, variables } = parseEnvExample(rootDir);
|
|
195
|
-
const deduped = dedupeVariables(variables);
|
|
196
|
-
const enriched = enrichVariablesForPolish(deduped);
|
|
197
|
-
const effectiveVersion = version ?? getDefaultSchemaVersion(rootDir);
|
|
198
|
-
const content = serializeEnvExample(effectiveVersion, enriched);
|
|
199
|
-
fs.writeFileSync(envExamplePath, content, 'utf-8');
|
|
200
|
-
}
|
|
201
|
-
/** Parse a simple semver string (e.g. "1.0.0" or "1.0") and return [major, minor, patch]. */
|
|
202
|
-
function parseSemver(s) {
|
|
203
|
-
const parts = s.replace(/^v/i, '').split('.');
|
|
204
|
-
const major = Math.max(0, parseInt(parts[0] || '0', 10) || 0);
|
|
205
|
-
const minor = Math.max(0, parseInt(parts[1] || '0', 10) || 0);
|
|
206
|
-
const patch = Math.max(0, parseInt(parts[2] || '0', 10) || 0);
|
|
207
|
-
return [major, minor, patch];
|
|
208
|
-
}
|
|
209
|
-
/** Bump semver: patch -> 1.0.0 => 1.0.1, minor => 1.1.0, major => 2.0.0. */
|
|
210
|
-
export function bumpSemver(current, bump) {
|
|
211
|
-
const [major, minor, patch] = parseSemver(current);
|
|
212
|
-
if (bump === 'major')
|
|
213
|
-
return `${major + 1}.0.0`;
|
|
214
|
-
if (bump === 'minor')
|
|
215
|
-
return `${major}.${minor + 1}.0`;
|
|
216
|
-
return `${major}.${minor}.${patch + 1}`;
|
|
217
|
-
}
|
|
218
|
-
/** Update ENV_SCHEMA_VERSION in .env.example and optionally package.json version. */
|
|
219
|
-
export function updateEnvSchemaVersion(rootDir, newVersion, options = {}) {
|
|
220
|
-
const envExamplePath = path.join(rootDir, '.env.example');
|
|
221
|
-
if (!fs.existsSync(envExamplePath)) {
|
|
222
|
-
throw new Error(`.env.example not found at ${envExamplePath}`);
|
|
223
|
-
}
|
|
224
|
-
const { version, variables } = parseEnvExample(rootDir);
|
|
225
|
-
const content = serializeEnvExample(newVersion, variables);
|
|
226
|
-
fs.writeFileSync(envExamplePath, content, 'utf-8');
|
|
227
|
-
if (options.syncPackage) {
|
|
228
|
-
const pkgPath = path.join(rootDir, 'package.json');
|
|
229
|
-
if (fs.existsSync(pkgPath)) {
|
|
230
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
231
|
-
pkg.version = newVersion;
|
|
232
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
export function getExistingEnvVariables(envPath) {
|
|
237
|
-
if (fs.existsSync(envPath)) {
|
|
238
|
-
const content = fs.readFileSync(envPath, 'utf-8');
|
|
239
|
-
return dotenv.parse(content);
|
|
240
|
-
}
|
|
241
|
-
return {};
|
|
242
|
-
}
|
|
243
|
-
async function run() {
|
|
244
|
-
const initialRoot = getRootDirFromArgv();
|
|
245
|
-
let schemaVersion;
|
|
246
|
-
let variables;
|
|
247
|
-
try {
|
|
248
|
-
const result = parseEnvExample(initialRoot);
|
|
249
|
-
schemaVersion = result.version;
|
|
250
|
-
variables = result.variables;
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
console.error(pc.red(err instanceof Error ? err.message : String(err)));
|
|
254
|
-
process.exit(1);
|
|
255
|
-
}
|
|
256
|
-
const program = new Command();
|
|
257
|
-
program
|
|
258
|
-
.name('setup-env')
|
|
259
|
-
.description('Interactive and non-interactive CLI to setup .env file')
|
|
260
|
-
.option('-y, --yes', 'Non-interactive mode: accept existing .env or defaults automatically')
|
|
261
|
-
.option('-f, --force', 'Force run even if .env is up-to-date')
|
|
262
|
-
.option('-e, --env <environment>', 'Target environment (e.g., local, test, production)')
|
|
263
|
-
.option('--cwd <path>', 'Project root directory (default: current working directory)')
|
|
264
|
-
.option('--polish', 'Polish .env.example (normalize format, dedupe keys, preserve sections)')
|
|
265
|
-
.option('--version [bump]', 'Bump or set ENV_SCHEMA_VERSION (patch|minor|major or exact semver like 1.2.0)')
|
|
266
|
-
.option('--sync-package', 'With --version: also set package.json version to match');
|
|
267
|
-
// Register dynamic options
|
|
268
|
-
variables.forEach((v) => {
|
|
269
|
-
const optName = `--${v.key.toLowerCase().replace(/_/g, '-')}`;
|
|
270
|
-
program.option(`${optName} <value>`, v.comment.split('\n')[0] || `Set ${v.key}`);
|
|
271
|
-
});
|
|
272
|
-
program.parse();
|
|
273
|
-
const options = program.opts();
|
|
274
|
-
if (options.polish) {
|
|
275
|
-
const root = path.resolve(options.cwd || process.cwd());
|
|
276
|
-
try {
|
|
277
|
-
polishEnvExample(root);
|
|
278
|
-
console.log(pc.green(pc.bold('✅ .env.example polished.')));
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
catch (e) {
|
|
282
|
-
console.error(pc.red(e instanceof Error ? e.message : String(e)));
|
|
283
|
-
process.exit(1);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (options.version !== undefined) {
|
|
287
|
-
const root = path.resolve(options.cwd || process.cwd());
|
|
288
|
-
try {
|
|
289
|
-
const { version } = parseEnvExample(root);
|
|
290
|
-
const current = version || '1.0.0';
|
|
291
|
-
const bump = options.version === true ? undefined : options.version;
|
|
292
|
-
let newVersion;
|
|
293
|
-
if (bump === 'patch' || bump === 'minor' || bump === 'major') {
|
|
294
|
-
newVersion = bumpSemver(current, bump);
|
|
295
|
-
}
|
|
296
|
-
else if (bump && typeof bump === 'string' && /^\d+(\.\d+)*(\.\d+)?$/.test(bump)) {
|
|
297
|
-
newVersion = bump;
|
|
298
|
-
}
|
|
299
|
-
else if (bump && typeof bump === 'string') {
|
|
300
|
-
newVersion = bump;
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
newVersion = bumpSemver(current, 'patch');
|
|
304
|
-
}
|
|
305
|
-
updateEnvSchemaVersion(root, newVersion, { syncPackage: options.syncPackage });
|
|
306
|
-
console.log(pc.green(pc.bold(`✅ ENV_SCHEMA_VERSION set to ${newVersion}`)));
|
|
307
|
-
if (options.syncPackage) {
|
|
308
|
-
console.log(pc.gray('package.json version updated.'));
|
|
309
|
-
}
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
catch (e) {
|
|
313
|
-
console.error(pc.red(e instanceof Error ? e.message : String(e)));
|
|
314
|
-
process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
const ROOT_DIR = path.resolve(options.cwd || process.cwd());
|
|
318
|
-
if (ROOT_DIR !== initialRoot) {
|
|
319
|
-
try {
|
|
320
|
-
const result = parseEnvExample(ROOT_DIR);
|
|
321
|
-
schemaVersion = result.version;
|
|
322
|
-
variables = result.variables;
|
|
323
|
-
}
|
|
324
|
-
catch (err) {
|
|
325
|
-
console.error(pc.red(err instanceof Error ? err.message : String(err)));
|
|
326
|
-
process.exit(1);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
console.log(pc.cyan(pc.bold('Environment Setup CLI')));
|
|
330
|
-
let targetEnv = options.env;
|
|
331
|
-
if (!targetEnv && !options.yes) {
|
|
332
|
-
const envChoice = await select({
|
|
333
|
-
message: 'Select target environment to generate:',
|
|
334
|
-
choices: [
|
|
335
|
-
{ name: 'default (.env)', value: 'default' },
|
|
336
|
-
{ name: 'local (.env.local)', value: 'local' },
|
|
337
|
-
{ name: 'test (.env.test)', value: 'test' },
|
|
338
|
-
{ name: 'staging (.env.stage)', value: 'stage' },
|
|
339
|
-
{ name: 'production (.env.production)', value: 'production' },
|
|
340
|
-
{ name: 'custom', value: 'custom' },
|
|
341
|
-
],
|
|
342
|
-
});
|
|
343
|
-
if (envChoice === 'custom') {
|
|
344
|
-
targetEnv = await input({
|
|
345
|
-
message: 'Enter custom environment name (e.g., ci, demo):',
|
|
346
|
-
validate: (val) => val.trim().length > 0 || 'Environment name is required',
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
targetEnv = envChoice === 'default' ? '' : envChoice;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
else if (!targetEnv && options.yes) {
|
|
354
|
-
targetEnv = ''; // fallback to default in non-interactive if not specified
|
|
355
|
-
}
|
|
356
|
-
const envFileName = targetEnv ? `.env.${targetEnv}` : '.env';
|
|
357
|
-
const envPath = path.join(ROOT_DIR, envFileName);
|
|
358
|
-
const existingEnvExists = fs.existsSync(envPath);
|
|
359
|
-
const existingVars = getExistingEnvVariables(envPath);
|
|
360
|
-
let existingVersion = null;
|
|
361
|
-
if (existingEnvExists) {
|
|
362
|
-
const content = fs.readFileSync(envPath, 'utf-8');
|
|
363
|
-
existingVersion = getExistingEnvVersion(content);
|
|
364
|
-
}
|
|
365
|
-
// Check if up-to-date
|
|
366
|
-
if (existingEnvExists && existingVersion === schemaVersion && !options.force && !options.yes) {
|
|
367
|
-
const proceed = await confirm({
|
|
368
|
-
message: pc.green(`${envFileName} is already up-to-date (Version ${schemaVersion}). Do you want to re-run setup?`),
|
|
369
|
-
default: false,
|
|
370
|
-
});
|
|
371
|
-
if (!proceed) {
|
|
372
|
-
console.log(pc.gray('Exiting...'));
|
|
373
|
-
process.exit(0);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const finalValues = {};
|
|
377
|
-
for (const v of variables) {
|
|
378
|
-
const cliOptKey = v.key.toLowerCase().replace(/([a-z])([A-Z])/g, '$1-$2').replace(/_/g, '');
|
|
379
|
-
const providedOptValue = options[v.key.toLowerCase().replace(/_/g, '')] || options[v.key.toLowerCase()]; // Command parses --database-url to databaseUrl
|
|
380
|
-
// 1. From CLI option overrides
|
|
381
|
-
let valFromCli = null;
|
|
382
|
-
for (const opt of Object.keys(options)) {
|
|
383
|
-
if (opt.toLowerCase() === v.key.toLowerCase().replace(/_/g, '')) {
|
|
384
|
-
valFromCli = options[opt];
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
if (valFromCli !== null && valFromCli !== undefined) {
|
|
389
|
-
finalValues[v.key] = valFromCli;
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
// 2. Determine default
|
|
393
|
-
let currentDefault = existingVars[v.key] ?? v.defaultValue;
|
|
394
|
-
// Auto-generate SESSION_SECRET if empty
|
|
395
|
-
if (v.key === 'SESSION_SECRET' && !currentDefault) {
|
|
396
|
-
currentDefault = crypto.randomBytes(64).toString('base64');
|
|
397
|
-
}
|
|
398
|
-
// Skip commented out variables from interactive prompt
|
|
399
|
-
if (v.isCommentedOut) {
|
|
400
|
-
finalValues[v.key] = currentDefault;
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
// Non-interactive Mode: take existing or default
|
|
404
|
-
if (options.yes) {
|
|
405
|
-
if (v.required && !currentDefault) {
|
|
406
|
-
console.warn(pc.yellow(`Warning: [REQUIRED] ${v.key} has no default or existing value. Please set it manually later.`));
|
|
407
|
-
}
|
|
408
|
-
finalValues[v.key] = currentDefault;
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
// Interactive Mode
|
|
412
|
-
console.log(''); // Blank line for readability
|
|
413
|
-
if (v.comment) {
|
|
414
|
-
console.log(pc.gray(v.comment));
|
|
415
|
-
}
|
|
416
|
-
let promptMsg = pc.cyan(`Set ${v.key}`);
|
|
417
|
-
if (v.required)
|
|
418
|
-
promptMsg = pc.bold(`${promptMsg} [REQUIRED]`);
|
|
419
|
-
const answer = await input({
|
|
420
|
-
message: promptMsg,
|
|
421
|
-
default: currentDefault,
|
|
422
|
-
validate: (value) => {
|
|
423
|
-
if (v.required && !value.trim()) {
|
|
424
|
-
return `${v.key} is required.`;
|
|
425
|
-
}
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
finalValues[v.key] = answer;
|
|
430
|
-
}
|
|
431
|
-
// Construct the new .env content
|
|
432
|
-
let newEnvContent = `# ==============================================\n`;
|
|
433
|
-
newEnvContent += `# Environment Variables\n`;
|
|
434
|
-
newEnvContent += `# ==============================================\n`;
|
|
435
|
-
if (schemaVersion) {
|
|
436
|
-
newEnvContent += `# ENV_SCHEMA_VERSION="${schemaVersion}"\n`;
|
|
437
|
-
}
|
|
438
|
-
newEnvContent += `# Generated on ${new Date().toISOString()}\n`;
|
|
439
|
-
newEnvContent += `# Generated by setup-env (https://www.npmjs.com/package/setup-env)\n`;
|
|
440
|
-
newEnvContent += `# ==============================================\n\n`;
|
|
441
|
-
let lastSection = '';
|
|
442
|
-
for (const v of variables) {
|
|
443
|
-
// Attempt to parse out the section header if available in the comments
|
|
444
|
-
const sectionMatch = v.comment.split('\n').find(l => l.includes('------'));
|
|
445
|
-
if (sectionMatch && sectionMatch !== lastSection) {
|
|
446
|
-
newEnvContent += `\n${sectionMatch}\n`;
|
|
447
|
-
lastSection = sectionMatch;
|
|
448
|
-
}
|
|
449
|
-
if (finalValues[v.key]) {
|
|
450
|
-
// Quote value if it contains spaces or special characters
|
|
451
|
-
const needsQuotes = /[\s#"']/.test(finalValues[v.key]) || finalValues[v.key] === '';
|
|
452
|
-
const safeValue = needsQuotes ? `"${finalValues[v.key].replace(/"/g, '\\"')}"` : finalValues[v.key];
|
|
453
|
-
newEnvContent += `${v.key}=${safeValue}\n`;
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
newEnvContent += `# ${v.key}=\n`;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
fs.writeFileSync(envPath, newEnvContent, 'utf-8');
|
|
460
|
-
console.log('');
|
|
461
|
-
console.log(pc.green(pc.bold(`✅ ${envFileName} file successfully created/updated!`)));
|
|
462
|
-
if (schemaVersion) {
|
|
463
|
-
console.log(pc.gray(`Schema Version: ${schemaVersion}`));
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
467
|
-
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename);
|
|
468
|
-
if (isMain) {
|
|
469
|
-
run().catch((err) => {
|
|
470
|
-
console.error(pc.red('Error:'), err);
|
|
471
|
-
process.exit(1);
|
|
472
|
-
});
|
|
473
|
-
}
|