@wchen.ai/env-from-example 1.0.0

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 ADDED
@@ -0,0 +1,240 @@
1
+ # env-from-example
2
+
3
+ Interactive and non-interactive CLI to set up `.env` from `.env.example`.
4
+
5
+ Walks you through each variable, validates types, auto-generates secrets, and prints a summary of what was configured. All type detection and validation are driven by a bundled [`schema.json`](schema.json).
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # Run without installing (npx fetches and runs)
11
+ npx @wchen.ai/env-from-example
12
+
13
+ # After installing in your project, use the short form:
14
+ npx env-from-example
15
+
16
+ # Don't have a .env.example yet? Create one:
17
+ npx env-from-example --init # starter template, add env vars, then polish
18
+ npx env-from-example --init .env # from your existing .env
19
+
20
+ # Don't have a .env? Generate one from .env.example
21
+ npx env-from-example # Generate .env
22
+ npx env-from-example -e staging # Generate .env.staging
23
+ ```
24
+
25
+ ## Installation
26
+
27
+ Install in your project so you can run `npx env-from-example` from the project root:
28
+
29
+ ```bash
30
+ npm install @wchen.ai/env-from-example
31
+ # or
32
+ pnpm add @wchen.ai/env-from-example
33
+ # or as a dev dependency (recommended for CLI tools)
34
+ npm install -D @wchen.ai/env-from-example
35
+ pnpm add -D @wchen.ai/env-from-example
36
+ ```
37
+
38
+ Then run from your project root: **`npx env-from-example`** (or add it to your `package.json` scripts).
39
+
40
+ ## Setup
41
+
42
+ Add a `.env.example` in your project root (or run `env-from-example --init` to create one).
43
+
44
+ ```bash
45
+ npx env-from-example --init # starter template
46
+ npx env-from-example --init .env # from your existing .env
47
+ ```
48
+
49
+ ### Before and after (first run)
50
+
51
+ If you start with only a `.env.example` and no `.env` (or an empty one), running the tool fills in `.env` from your answers and defaults.
52
+
53
+ **Before** — you have `.env.example` and no `.env` (or `.env` is empty):
54
+
55
+ ```env
56
+ # .env.example (excerpt)
57
+ DATABASE_URL=postgres://localhost:5432/myapp
58
+ API_KEY=
59
+ NODE_ENV=development
60
+ SESSION_SECRET=
61
+ ```
62
+
63
+ ```env
64
+ # .env — missing or empty
65
+ ```
66
+
67
+ **After** — run `env-from-example` (or `env-from-example -y` to accept defaults). The CLI prompts for required values, can auto-generate secrets (e.g. `SESSION_SECRET`), and writes:
68
+
69
+ ```env
70
+ # .env — created/updated by env-from-example
71
+ DATABASE_URL=postgres://localhost:5432/myapp
72
+ API_KEY=your-api-key-here
73
+ NODE_ENV=development
74
+ SESSION_SECRET=a1b2c3d4e5f6...
75
+ ```
76
+
77
+ Use `env-from-example -y --dry-run` to preview the result without writing files.
78
+
79
+ ## Usage
80
+
81
+ Run from your project root (where `.env.example` lives):
82
+
83
+ ```bash
84
+ env-from-example
85
+ ```
86
+
87
+ **Options:**
88
+
89
+ | Flag | Description |
90
+ | ---------------------- | ----------------------------------------------------------------------------------- |
91
+ | `-y, --yes` | Non-interactive: accept existing values or defaults without prompting |
92
+ | `-f, --force` | Force re-run even if `.env` is already up-to-date |
93
+ | `-e, --env <name>` | Target environment (e.g., `local`, `test`, `production`) |
94
+ | `--cwd <path>` | Project root directory (default: current working directory) |
95
+ | `--init [source]` | Create `.env.example` from an existing env file or from scratch |
96
+ | `--polish` | Polish `.env.example`: add descriptions, types, defaults (`-y` for non-interactive) |
97
+ | `--version [bump]` | Bump or set `ENV_SCHEMA_VERSION` (`patch`, `minor`, `major`, or exact semver) |
98
+ | `--sync-package` | With `--version`: also update `package.json` version |
99
+ | `--validate [envFile]` | Validate `.env` against `.env.example` schema (exit 1 if invalid) |
100
+ | `--dry-run` | Preview what would be written without creating/modifying files |
101
+
102
+ **Examples:**
103
+
104
+ ```bash
105
+ # Interactive setup (default .env)
106
+ env-from-example
107
+
108
+ # Non-interactive: create/update .env with defaults or existing values
109
+ env-from-example -y
110
+
111
+ # Preview what would be generated (no files written)
112
+ env-from-example -y --dry-run
113
+
114
+ # Create .env.local with prompts
115
+ env-from-example -e local
116
+
117
+ # Create .env.test without prompts (e.g. CI)
118
+ env-from-example -y -e test
119
+
120
+ # Force re-run even if .env is up-to-date
121
+ env-from-example -f
122
+
123
+ # Run from another directory (e.g. monorepo package)
124
+ env-from-example --cwd ./apps/api
125
+
126
+ # Override a variable via CLI
127
+ env-from-example --database-url "postgres://prod:5432/db" -y
128
+
129
+ # Bump ENV_SCHEMA_VERSION
130
+ env-from-example --version patch
131
+ env-from-example --version minor --sync-package
132
+
133
+ # Validate .env against .env.example schema
134
+ env-from-example --validate
135
+ ```
136
+
137
+ ## Annotations
138
+
139
+ Each variable in `.env.example` can be annotated with structured tags in the comment line above it:
140
+
141
+ ```env
142
+ # <description> [REQUIRED] [TYPE: <schema-type>] [CONSTRAINTS: key=value,...] Default: <value>
143
+ VARIABLE_NAME=default_value
144
+ ```
145
+
146
+ | Annotation | Syntax | Purpose |
147
+ | ----------- | ------------------------------ | ---------------------------------------------------- |
148
+ | Description | Free text at start of comment | Human-readable explanation |
149
+ | Required | `[REQUIRED]` | Variable must be non-empty |
150
+ | Type | `[TYPE: <schema-type>]` | Type from `schema.json` for validation and detection |
151
+ | Constraints | `[CONSTRAINTS: key=value,...]` | Constraints (min, max, pattern, etc.) |
152
+ | Default | `Default: <value>` | Documented default (informational) |
153
+
154
+ All annotations are optional. The `--polish` command auto-detects and adds them.
155
+
156
+ ### Polish: before and after
157
+
158
+ `env-from-example --polish` (or `--polish -y` for non-interactive) updates your `.env.example` with inferred descriptions, types, and default annotations.
159
+
160
+ **Before** — minimal `.env.example`:
161
+
162
+ ```env
163
+ DATABASE_URL=postgres://localhost:5432/myapp
164
+ PORT=3000
165
+ NODE_ENV=development
166
+ API_KEY=
167
+ ```
168
+
169
+ **After** — run `env-from-example --polish -y`:
170
+
171
+ ```env
172
+ # env-from-example (https://www.npmjs.com/package/@wchen.ai/env-from-example)
173
+
174
+ # ENV_SCHEMA_VERSION="1.0.0"
175
+
176
+ # ========================================
177
+ # Database
178
+ # ========================================
179
+
180
+ # Database Url
181
+ # [REQUIRED] [TYPE: network/uri] Default: postgres://localhost:5432/myapp
182
+ DATABASE_URL=postgres://localhost:5432/myapp
183
+
184
+ # ========================================
185
+ # App
186
+ # ========================================
187
+
188
+ # Application port
189
+ # [REQUIRED] [TYPE: integer] [CONSTRAINTS: min=3000,max=10000] Default: 3000
190
+ PORT=3000
191
+
192
+ # Node Env
193
+ # [TYPE: structured/enum] [CONSTRAINTS: pattern=^(development|test|staging|production|ci)$] Default: development
194
+ NODE_ENV=development
195
+
196
+ # ========================================
197
+ # Other
198
+ # ========================================
199
+
200
+ # OPEN AI API KEY
201
+ # [REQUIRED] [TYPE: credentials/secret] Default: (empty)
202
+ API_KEY=
203
+ ```
204
+
205
+ Use `env-from-example --polish --dry-run` to preview changes without writing the file.
206
+
207
+ ## CI / CD
208
+
209
+ **GitHub Actions** -- generate `.env.test` before running tests:
210
+
211
+ ```yaml
212
+ # .github/workflows/test.yml
213
+ jobs:
214
+ test:
215
+ runs-on: ubuntu-latest
216
+ steps:
217
+ - uses: actions/checkout@v4
218
+ - uses: actions/setup-node@v4
219
+ with:
220
+ node-version: 20
221
+ - run: npm ci
222
+ - run: npx env-from-example -y -e test
223
+ - run: npm test
224
+ ```
225
+
226
+ **Pre-commit hook** -- validate `.env` stays in sync:
227
+
228
+ ```bash
229
+ # .husky/pre-commit
230
+ npx env-from-example --validate
231
+ ```
232
+
233
+ ## Requirements
234
+
235
+ - A `.env.example` file in the project root (or the path given by `--cwd`). Run `env-from-example --init` to create one.
236
+ - `schema.json` is bundled with the package and used automatically.
237
+
238
+ ## License
239
+
240
+ ISC
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env node
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { Command } from "commander";
6
+ import { input, confirm, select } from "@inquirer/prompts";
7
+ import pc from "picocolors";
8
+ import { findSchemaType, parseEnumChoices, } from "./src/schema.js";
9
+ import { parseEnvExample, getExistingEnvVersion, getExistingEnvVariables, groupVariablesBySection, getGroup, inferDescription, stripMetaFromComment, initEnvExample, } from "./src/parse.js";
10
+ import { validateValue, validateEnv, coerceToType, generateAutoValue, } from "./src/validate.js";
11
+ import { polishEnvExample, polishEnvExampleInteractive } from "./src/polish.js";
12
+ import { bumpSemver, updateEnvSchemaVersion } from "./src/version.js";
13
+ // ─── Re-exports (public API consumed by tests and external users) ────────────
14
+ export { loadSchema, resetSchemaCache, getSchemaTypes, findSchemaType, parseEnumChoices, getAvailableConstraints, } from "./src/schema.js";
15
+ export { parseEnvExample, getExistingEnvVersion, getExistingEnvVariables, serializeEnvExample, inferDescription, initEnvExample, } from "./src/parse.js";
16
+ export { detectType, matchesSchemaType, validateValue, validateEnv, coerceToType, generateAutoValue, } from "./src/validate.js";
17
+ export { polishEnvExample, polishEnvExampleInteractive } from "./src/polish.js";
18
+ export { bumpSemver, updateEnvSchemaVersion } from "./src/version.js";
19
+ // ─── CLI helpers ─────────────────────────────────────────────────────────────
20
+ export function getRootDirFromArgv() {
21
+ const argv = process.argv;
22
+ const cwdIdx = argv.indexOf("--cwd");
23
+ if (cwdIdx !== -1 && argv[cwdIdx + 1]) {
24
+ return path.resolve(argv[cwdIdx + 1]);
25
+ }
26
+ return process.cwd();
27
+ }
28
+ function renderGroupBanner(groupName) {
29
+ const W = 40;
30
+ const bar = "# " + "=".repeat(W);
31
+ const padLen = Math.max(1, Math.floor((W - groupName.length) / 2));
32
+ const center = "#" + " ".repeat(padLen) + groupName;
33
+ return [bar, center, bar];
34
+ }
35
+ function buildEnvContent(schemaVersion, variables, finalValues) {
36
+ let content = `# ==============================================\n`;
37
+ content += `# Environment Variables\n`;
38
+ content += `# ==============================================\n`;
39
+ if (schemaVersion) {
40
+ content += `# ENV_SCHEMA_VERSION="${schemaVersion}"\n`;
41
+ }
42
+ content += `# Generated on ${new Date().toISOString()}\n`;
43
+ content += `# Generated by env-from-example (https://www.npmjs.com/package/env-from-example)\n`;
44
+ content += `# ==============================================\n\n`;
45
+ const grouped = groupVariablesBySection(variables);
46
+ let lastGroup = "";
47
+ for (const v of grouped) {
48
+ const group = getGroup(v);
49
+ if (group && group !== lastGroup) {
50
+ content += "\n" + renderGroupBanner(group).join("\n") + "\n\n";
51
+ lastGroup = group;
52
+ }
53
+ const desc = inferDescription(v);
54
+ if (desc) {
55
+ content += `# ${desc}\n`;
56
+ }
57
+ if (v.key in finalValues) {
58
+ const val = finalValues[v.key];
59
+ const needsQuotes = /[\s#"']/.test(val) || val === "";
60
+ const safeValue = needsQuotes && val !== "" ? `"${val.replace(/"/g, '\\"')}"` : val;
61
+ content += `${v.key}=${safeValue}\n`;
62
+ }
63
+ else {
64
+ content += `# ${v.key}=\n`;
65
+ }
66
+ }
67
+ return content;
68
+ }
69
+ function printSummary(summary, envFileName, schemaVersion) {
70
+ console.log("");
71
+ console.log(pc.green(pc.bold(`✅ ${envFileName} successfully created/updated!`)));
72
+ if (schemaVersion) {
73
+ console.log(pc.gray(` Schema version: ${schemaVersion}`));
74
+ }
75
+ console.log("");
76
+ const total = summary.fromExisting.length +
77
+ summary.fromDefault.length +
78
+ summary.autoGenerated.length +
79
+ summary.fromCli.length +
80
+ summary.skippedCommented.length;
81
+ console.log(pc.bold(` ${total} variables configured:`));
82
+ if (summary.fromCli.length > 0) {
83
+ console.log(pc.cyan(` ⮑ ${summary.fromCli.length} from CLI flags: `) +
84
+ pc.dim(summary.fromCli.join(", ")));
85
+ }
86
+ if (summary.fromExisting.length > 0) {
87
+ console.log(pc.green(` ⮑ ${summary.fromExisting.length} from existing ${envFileName}: `) + pc.dim(summary.fromExisting.join(", ")));
88
+ }
89
+ if (summary.fromDefault.length > 0) {
90
+ console.log(pc.blue(` ⮑ ${summary.fromDefault.length} from defaults: `) +
91
+ pc.dim(summary.fromDefault.join(", ")));
92
+ }
93
+ if (summary.autoGenerated.length > 0) {
94
+ console.log(pc.magenta(` ⮑ ${summary.autoGenerated.length} auto-generated: `) +
95
+ pc.dim(summary.autoGenerated.join(", ")));
96
+ }
97
+ if (summary.skippedCommented.length > 0) {
98
+ console.log(pc.gray(` ⮑ ${summary.skippedCommented.length} commented-out (kept as-is): `) + pc.dim(summary.skippedCommented.join(", ")));
99
+ }
100
+ if (summary.requiredMissing.length > 0) {
101
+ console.log(pc.yellow(` ⚠ ${summary.requiredMissing.length} required but empty: `) + pc.dim(summary.requiredMissing.join(", ")));
102
+ }
103
+ console.log("");
104
+ }
105
+ // ─── Main CLI ────────────────────────────────────────────────────────────────
106
+ async function run() {
107
+ const program = new Command();
108
+ program
109
+ .name("env-from-example")
110
+ .description("Interactive and non-interactive CLI to set up .env from .env.example")
111
+ .option("-y, --yes", "Non-interactive: accept existing values or defaults without prompting")
112
+ .option("-f, --force", "Force re-run even if .env is already up-to-date")
113
+ .option("-e, --env <environment>", "Target environment (e.g., local, test, production)")
114
+ .option("--cwd <path>", "Project root directory (default: current working directory)")
115
+ .option("--init [source]", "Create .env.example from an existing env file (default: .env) or from scratch")
116
+ .option("--polish", "Polish .env.example: add descriptions, types, defaults (use -y for non-interactive)")
117
+ .option("--version [bump]", "Bump or set ENV_SCHEMA_VERSION (patch|minor|major or exact semver)")
118
+ .option("--sync-package", "With --version: also update package.json version")
119
+ .option("--validate [envFile]", "Validate .env against .env.example schema (exit 1 if invalid)")
120
+ .option("--dry-run", "Preview what would be written without creating/modifying files");
121
+ const earlyRoot = getRootDirFromArgv();
122
+ try {
123
+ const { variables: earlyVars } = parseEnvExample(earlyRoot);
124
+ earlyVars.forEach((v) => {
125
+ const optName = `--${v.key.toLowerCase().replace(/_/g, "-")}`;
126
+ const desc = stripMetaFromComment(v.comment) || `Set ${v.key}`;
127
+ program.option(`${optName} <value>`, desc);
128
+ });
129
+ }
130
+ catch {
131
+ /* .env.example may not exist yet */
132
+ }
133
+ program.parse();
134
+ const options = program.opts();
135
+ const ROOT_DIR = path.resolve(options.cwd || process.cwd());
136
+ if (options.init !== undefined) {
137
+ try {
138
+ const source = typeof options.init === "string" ? options.init : undefined;
139
+ initEnvExample(ROOT_DIR, { from: source });
140
+ console.log(pc.green(pc.bold("✅ .env.example created.")));
141
+ if (!source) {
142
+ return;
143
+ }
144
+ if (options.yes) {
145
+ polishEnvExample(ROOT_DIR);
146
+ console.log(pc.green(pc.bold("✅ .env.example polished (non-interactive).")));
147
+ }
148
+ else {
149
+ await polishEnvExampleInteractive(ROOT_DIR);
150
+ console.log(pc.green(pc.bold("✅ .env.example polished.")));
151
+ }
152
+ return;
153
+ }
154
+ catch (e) {
155
+ console.error(pc.red(e instanceof Error ? e.message : String(e)));
156
+ process.exit(1);
157
+ }
158
+ }
159
+ if (options.polish) {
160
+ try {
161
+ if (options.yes) {
162
+ polishEnvExample(ROOT_DIR);
163
+ console.log(pc.green(pc.bold("✅ .env.example polished (non-interactive).")));
164
+ }
165
+ else {
166
+ console.log(pc.cyan(pc.bold("Interactive polish: conform .env.example to convention (description, default, type, etc.)\n")));
167
+ await polishEnvExampleInteractive(ROOT_DIR);
168
+ console.log(pc.green(pc.bold("✅ .env.example polished.")));
169
+ }
170
+ return;
171
+ }
172
+ catch (e) {
173
+ console.error(pc.red(e instanceof Error ? e.message : String(e)));
174
+ process.exit(1);
175
+ }
176
+ }
177
+ if (options.validate !== undefined) {
178
+ const envFile = options.validate === true ? ".env" : `.env.${options.validate}`;
179
+ try {
180
+ const result = validateEnv(ROOT_DIR, { envFile });
181
+ if (result.warnings.length > 0) {
182
+ result.warnings.forEach((w) => console.warn(pc.yellow("Warning:"), w));
183
+ }
184
+ if (result.valid) {
185
+ console.log(pc.green(pc.bold(`✅ ${envFile} is valid against .env.example schema.`)));
186
+ return;
187
+ }
188
+ result.errors.forEach((e) => console.error(pc.red("Error:"), e));
189
+ process.exit(1);
190
+ }
191
+ catch (e) {
192
+ console.error(pc.red(e instanceof Error ? e.message : String(e)));
193
+ process.exit(1);
194
+ }
195
+ }
196
+ if (options.version !== undefined) {
197
+ try {
198
+ const { version } = parseEnvExample(ROOT_DIR);
199
+ const current = version || "1.0.0";
200
+ const bump = options.version === true ? undefined : options.version;
201
+ let newVersion;
202
+ if (bump === "patch" || bump === "minor" || bump === "major") {
203
+ newVersion = bumpSemver(current, bump);
204
+ }
205
+ else if (bump && typeof bump === "string") {
206
+ newVersion = bump;
207
+ }
208
+ else {
209
+ newVersion = bumpSemver(current, "patch");
210
+ }
211
+ updateEnvSchemaVersion(ROOT_DIR, newVersion, {
212
+ syncPackage: options.syncPackage,
213
+ });
214
+ console.log(pc.green(pc.bold(`✅ ENV_SCHEMA_VERSION set to ${newVersion}`)));
215
+ if (options.syncPackage) {
216
+ console.log(pc.gray(" package.json version updated."));
217
+ }
218
+ return;
219
+ }
220
+ catch (e) {
221
+ console.error(pc.red(e instanceof Error ? e.message : String(e)));
222
+ process.exit(1);
223
+ }
224
+ }
225
+ // ─── Main: generate .env ─────────────────────────────────────────────────
226
+ let schemaVersion;
227
+ let variables;
228
+ try {
229
+ const result = parseEnvExample(ROOT_DIR);
230
+ schemaVersion = result.version;
231
+ variables = result.variables;
232
+ }
233
+ catch {
234
+ const examplePath = path.join(ROOT_DIR, ".env.example");
235
+ console.error(pc.red(`No .env.example found at ${examplePath}`));
236
+ console.error("");
237
+ console.error(pc.bold("To get started:"));
238
+ console.error(pc.cyan(" env-from-example --init") +
239
+ pc.gray(" Create a starter .env.example"));
240
+ console.error(pc.cyan(" env-from-example --init .env") +
241
+ pc.gray(" Create .env.example from existing .env"));
242
+ console.error("");
243
+ console.error(pc.gray("Or create .env.example manually — see https://www.npmjs.com/package/env-from-example"));
244
+ process.exit(1);
245
+ }
246
+ const activeVars = variables.filter((v) => !v.isCommentedOut);
247
+ const totalPromptable = activeVars.length;
248
+ console.log("");
249
+ console.log(pc.cyan(pc.bold(" env-from-example")) +
250
+ pc.gray(` — ${totalPromptable} variables from .env.example`));
251
+ console.log("");
252
+ let targetEnv = options.env;
253
+ if (!targetEnv && !options.yes) {
254
+ const envChoice = await select({
255
+ message: "Select target environment to generate:",
256
+ choices: [
257
+ { name: "default (.env)", value: "default" },
258
+ { name: "local (.env.local)", value: "local" },
259
+ { name: "test (.env.test)", value: "test" },
260
+ { name: "staging (.env.stage)", value: "stage" },
261
+ { name: "production (.env.production)", value: "production" },
262
+ { name: "custom", value: "custom" },
263
+ ],
264
+ });
265
+ if (envChoice === "custom") {
266
+ targetEnv = await input({
267
+ message: "Enter custom environment name (e.g., ci, demo):",
268
+ validate: (val) => val.trim().length > 0 || "Environment name is required",
269
+ });
270
+ }
271
+ else {
272
+ targetEnv = envChoice === "default" ? "" : envChoice;
273
+ }
274
+ }
275
+ else if (!targetEnv) {
276
+ targetEnv = "";
277
+ }
278
+ const envFileName = targetEnv ? `.env.${targetEnv}` : ".env";
279
+ const envPath = path.join(ROOT_DIR, envFileName);
280
+ const existingEnvExists = fs.existsSync(envPath);
281
+ const existingVars = getExistingEnvVariables(envPath);
282
+ let existingVersion = null;
283
+ if (existingEnvExists) {
284
+ const content = fs.readFileSync(envPath, "utf-8");
285
+ existingVersion = getExistingEnvVersion(content);
286
+ }
287
+ if (existingEnvExists &&
288
+ existingVersion === schemaVersion &&
289
+ !options.force &&
290
+ !options.yes) {
291
+ const proceed = await confirm({
292
+ message: pc.green(`${envFileName} is already up-to-date (v${schemaVersion}). Re-run setup?`),
293
+ default: false,
294
+ });
295
+ if (!proceed) {
296
+ console.log(pc.gray("Nothing changed."));
297
+ process.exit(0);
298
+ }
299
+ }
300
+ const finalValues = {};
301
+ const summary = {
302
+ fromExisting: [],
303
+ fromDefault: [],
304
+ autoGenerated: [],
305
+ fromCli: [],
306
+ skippedCommented: [],
307
+ requiredMissing: [],
308
+ };
309
+ let promptIndex = 0;
310
+ for (const v of variables) {
311
+ const camelKey = v.key
312
+ .toLowerCase()
313
+ .replace(/_([a-z0-9])/gi, (_, c) => c.toUpperCase());
314
+ const valFromCli = options[camelKey];
315
+ if (valFromCli !== undefined &&
316
+ valFromCli !== null &&
317
+ typeof valFromCli === "string") {
318
+ finalValues[v.key] = valFromCli;
319
+ summary.fromCli.push(v.key);
320
+ continue;
321
+ }
322
+ const hasExisting = v.key in existingVars;
323
+ let currentDefault = existingVars[v.key] ?? v.defaultValue;
324
+ const schemaType = v.type ? findSchemaType(v.type) : undefined;
325
+ const autoGen = schemaType?.auto_generate;
326
+ let wasAutoGenerated = false;
327
+ if (autoGen && !currentDefault) {
328
+ currentDefault = generateAutoValue(autoGen);
329
+ wasAutoGenerated = true;
330
+ }
331
+ if (v.isCommentedOut) {
332
+ finalValues[v.key] = currentDefault;
333
+ summary.skippedCommented.push(v.key);
334
+ continue;
335
+ }
336
+ if (options.yes) {
337
+ if (v.required && !currentDefault) {
338
+ console.warn(pc.yellow(` ⚠ [REQUIRED] ${v.key} has no value — set it manually in ${envFileName}`));
339
+ summary.requiredMissing.push(v.key);
340
+ }
341
+ finalValues[v.key] = currentDefault;
342
+ if (wasAutoGenerated)
343
+ summary.autoGenerated.push(v.key);
344
+ else if (hasExisting)
345
+ summary.fromExisting.push(v.key);
346
+ else
347
+ summary.fromDefault.push(v.key);
348
+ continue;
349
+ }
350
+ promptIndex++;
351
+ const progress = pc.dim(`[${promptIndex}/${totalPromptable}]`);
352
+ const desc = stripMetaFromComment(v.comment);
353
+ if (desc) {
354
+ console.log(pc.gray(` ${desc}`));
355
+ }
356
+ let answer;
357
+ const isEnum = v.type === "structured/enum" && v.constraints?.pattern;
358
+ const enumChoices = isEnum ? parseEnumChoices(v.constraints.pattern) : [];
359
+ if (enumChoices.length > 0) {
360
+ const choiceValue = currentDefault && enumChoices.includes(currentDefault)
361
+ ? currentDefault
362
+ : enumChoices[0];
363
+ answer = await select({
364
+ message: `${progress} ${v.key}` +
365
+ (v.required ? pc.bold(pc.yellow(" REQUIRED")) : ""),
366
+ choices: enumChoices.map((c) => ({ name: c, value: c })),
367
+ default: choiceValue,
368
+ });
369
+ }
370
+ else {
371
+ const hint = wasAutoGenerated ? pc.dim(" (auto-generated)") : "";
372
+ answer = await input({
373
+ message: `${progress} ${v.key}` +
374
+ (v.required ? pc.bold(pc.yellow(" REQUIRED")) : "") +
375
+ hint,
376
+ default: currentDefault,
377
+ validate: (val) => validateValue(val, v) ?? true,
378
+ });
379
+ }
380
+ const coerced = coerceToType(answer, v.type);
381
+ finalValues[v.key] = coerced;
382
+ if (wasAutoGenerated && coerced === currentDefault)
383
+ summary.autoGenerated.push(v.key);
384
+ else if (hasExisting && coerced === existingVars[v.key])
385
+ summary.fromExisting.push(v.key);
386
+ else
387
+ summary.fromDefault.push(v.key);
388
+ }
389
+ const newEnvContent = buildEnvContent(schemaVersion, variables, finalValues);
390
+ if (options.dryRun) {
391
+ console.log("");
392
+ console.log(pc.bold(pc.cyan(`--- Dry run: ${envFileName} (not written) ---`)));
393
+ console.log(pc.dim(newEnvContent));
394
+ console.log(pc.bold(pc.cyan("--- End dry run ---")));
395
+ printSummary(summary, envFileName, schemaVersion);
396
+ return;
397
+ }
398
+ fs.writeFileSync(envPath, newEnvContent, "utf-8");
399
+ printSummary(summary, envFileName, schemaVersion);
400
+ }
401
+ // ─── Entry ───────────────────────────────────────────────────────────────────
402
+ const __filename = fileURLToPath(import.meta.url);
403
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename);
404
+ if (isMain) {
405
+ run().catch((err) => {
406
+ if (err &&
407
+ typeof err === "object" &&
408
+ "name" in err &&
409
+ err.name === "ExitPromptError") {
410
+ console.log(pc.gray("\nCancelled."));
411
+ process.exit(0);
412
+ }
413
+ console.error(pc.red("Error:"), err);
414
+ process.exit(1);
415
+ });
416
+ }