@ayronforge/envil 0.6.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/dist/cli/dotenv-codec.d.ts +3 -0
  4. package/dist/cli/fs-utils.d.ts +7 -0
  5. package/dist/cli/generate-env-ts.d.ts +2 -0
  6. package/dist/cli/generate-example.d.ts +2 -0
  7. package/dist/cli/index.d.ts +7 -0
  8. package/dist/cli/infer.d.ts +6 -0
  9. package/dist/cli/literals.d.ts +5 -0
  10. package/dist/cli/manifest-codec.d.ts +3 -0
  11. package/dist/cli/types.d.ts +61 -0
  12. package/dist/cli.d.ts +8 -0
  13. package/dist/cli.js +1249 -0
  14. package/dist/cli.js.map +18 -0
  15. package/dist/env.d.ts +14 -0
  16. package/dist/errors.d.ts +10 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.js +443 -0
  19. package/dist/index.js.map +17 -0
  20. package/dist/prefix.d.ts +6 -0
  21. package/dist/presets.d.ts +30 -0
  22. package/dist/presets.js +26 -0
  23. package/dist/presets.js.map +10 -0
  24. package/dist/resolvers/aws.d.ts +9 -0
  25. package/dist/resolvers/aws.js +146 -0
  26. package/dist/resolvers/aws.js.map +12 -0
  27. package/dist/resolvers/azure.d.ts +10 -0
  28. package/dist/resolvers/azure.js +85 -0
  29. package/dist/resolvers/azure.js.map +12 -0
  30. package/dist/resolvers/gcp.d.ts +10 -0
  31. package/dist/resolvers/gcp.js +88 -0
  32. package/dist/resolvers/gcp.js.map +12 -0
  33. package/dist/resolvers/onepassword.d.ts +9 -0
  34. package/dist/resolvers/onepassword.js +91 -0
  35. package/dist/resolvers/onepassword.js.map +12 -0
  36. package/dist/resolvers/remote.d.ts +9 -0
  37. package/dist/resolvers/types.d.ts +15 -0
  38. package/dist/resolvers/utils.d.ts +15 -0
  39. package/dist/safe-env.d.ts +23 -0
  40. package/dist/schemas.d.ts +30 -0
  41. package/dist/types.d.ts +41 -0
  42. package/package.json +109 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1249 @@
1
+ #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined")
6
+ return require.apply(this, arguments);
7
+ throw Error('Dynamic require of "' + x + '" is not supported');
8
+ });
9
+
10
+ // src/cli/fs-utils.ts
11
+ import { access, mkdir, readFile, rename, writeFile } from "node:fs/promises";
12
+ import path from "node:path";
13
+ async function pathExists(filePath) {
14
+ try {
15
+ await access(filePath);
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ function resolveFromCwd(cwd, filePath) {
22
+ return path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
23
+ }
24
+ async function getDefaultEnvOutputPath(cwd) {
25
+ const srcDir = path.join(cwd, "src");
26
+ if (await pathExists(srcDir)) {
27
+ return path.join(srcDir, "env.ts");
28
+ }
29
+ return path.join(cwd, "env.ts");
30
+ }
31
+ async function getDefaultExampleInputPath(cwd) {
32
+ const rootEnv = path.join(cwd, "env.ts");
33
+ const srcEnv = path.join(cwd, "src", "env.ts");
34
+ if (await pathExists(rootEnv))
35
+ return rootEnv;
36
+ if (await pathExists(srcEnv))
37
+ return srcEnv;
38
+ const srcDir = path.join(cwd, "src");
39
+ if (await pathExists(srcDir)) {
40
+ return srcEnv;
41
+ }
42
+ return rootEnv;
43
+ }
44
+ async function ensureWritableTarget(targetPath, force) {
45
+ if (!force && await pathExists(targetPath)) {
46
+ throw new Error(`Target file "${targetPath}" already exists. Use --force to overwrite.`);
47
+ }
48
+ }
49
+ async function readTextFileOrThrow(filePath, label) {
50
+ try {
51
+ return await readFile(filePath, "utf8");
52
+ } catch (error) {
53
+ throw new Error(`Unable to read ${label} at "${filePath}": ${String(error)}`);
54
+ }
55
+ }
56
+ async function writeFileAtomic(targetPath, contents) {
57
+ const directory = path.dirname(targetPath);
58
+ await mkdir(directory, { recursive: true });
59
+ const tempPath = path.join(directory, `.envil-tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
60
+ await writeFile(tempPath, contents, "utf8");
61
+ await rename(tempPath, targetPath);
62
+ }
63
+
64
+ // src/cli/dotenv-codec.ts
65
+ import { Schema } from "effect";
66
+
67
+ // src/cli/literals.ts
68
+ function parseLiteral(input) {
69
+ const trimmed = input.trim();
70
+ if (trimmed.length === 0) {
71
+ return "";
72
+ }
73
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
74
+ try {
75
+ return JSON.parse(trimmed.replace(/^'/, '"').replace(/'$/, '"'));
76
+ } catch {
77
+ return trimmed.slice(1, -1);
78
+ }
79
+ }
80
+ if (trimmed === "true")
81
+ return true;
82
+ if (trimmed === "false")
83
+ return false;
84
+ if (trimmed === "null")
85
+ return null;
86
+ if (/^[+-]?\d+(?:\.\d+)?$/.test(trimmed)) {
87
+ const value = Number(trimmed);
88
+ if (!Number.isNaN(value)) {
89
+ return value;
90
+ }
91
+ }
92
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
93
+ try {
94
+ return JSON.parse(trimmed);
95
+ } catch {
96
+ return trimmed;
97
+ }
98
+ }
99
+ return trimmed;
100
+ }
101
+ function toCodeLiteral(value) {
102
+ if (typeof value === "string")
103
+ return JSON.stringify(value);
104
+ if (typeof value === "number" || typeof value === "boolean")
105
+ return String(value);
106
+ if (value === null)
107
+ return "null";
108
+ if (value === undefined)
109
+ return "undefined";
110
+ return JSON.stringify(value);
111
+ }
112
+ function toDirectiveLiteral(value) {
113
+ if (value === undefined)
114
+ return "";
115
+ return toCodeLiteral(value);
116
+ }
117
+ function toEnvValueLiteral(value) {
118
+ if (typeof value === "string")
119
+ return value;
120
+ if (typeof value === "number" || typeof value === "boolean")
121
+ return String(value);
122
+ if (value === null || value === undefined)
123
+ return "";
124
+ return JSON.stringify(value);
125
+ }
126
+ function parseBooleanDirective(value, defaultValue) {
127
+ if (value === undefined || value.length === 0) {
128
+ return defaultValue;
129
+ }
130
+ const normalized = value.trim().toLowerCase();
131
+ if (normalized === "true")
132
+ return true;
133
+ if (normalized === "false")
134
+ return false;
135
+ throw new Error(`Invalid boolean directive value "${value}"`);
136
+ }
137
+
138
+ // src/cli/types.ts
139
+ var BUCKETS = ["server", "client", "shared"];
140
+ var SCHEMA_KINDS = [
141
+ "requiredString",
142
+ "boolean",
143
+ "integer",
144
+ "number",
145
+ "port",
146
+ "url",
147
+ "postgresUrl",
148
+ "redisUrl",
149
+ "mongoUrl",
150
+ "mysqlUrl",
151
+ "commaSeparated",
152
+ "commaSeparatedNumbers",
153
+ "commaSeparatedUrls",
154
+ "json"
155
+ ];
156
+ var FRAMEWORKS = ["nextjs", "vite", "expo", "nuxt", "sveltekit", "astro"];
157
+ var MANIFEST_VERSION = 1;
158
+
159
+ // src/cli/dotenv-codec.ts
160
+ var SENTINEL_DIRECTIVE_PREFIX = "@";
161
+ var KEY_ASSIGNMENT_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
162
+ var DotenvCodec = Schema.transform(Schema.String, Schema.Unknown, {
163
+ decode: (text) => parseDotenvText(String(text)),
164
+ encode: (value) => stringifyDotenvDocument(value)
165
+ });
166
+ function decodeDotenvText(text) {
167
+ return Schema.decodeUnknownSync(DotenvCodec)(text);
168
+ }
169
+ function encodeDotenvText(document) {
170
+ return Schema.encodeSync(DotenvCodec)(document);
171
+ }
172
+ function parseDotenvText(text) {
173
+ const lines = text.split(/\r?\n/);
174
+ const entries = [];
175
+ let activeBucket;
176
+ let pendingDirectives = {};
177
+ for (const [index, line] of lines.entries()) {
178
+ const lineNumber = index + 1;
179
+ const trimmed = line.trim();
180
+ if (trimmed.length === 0) {
181
+ continue;
182
+ }
183
+ if (trimmed.startsWith("#")) {
184
+ const comment = trimmed.slice(1).trim();
185
+ if (comment.startsWith(SENTINEL_DIRECTIVE_PREFIX)) {
186
+ const directiveResult = parseDirectiveGroup(comment, lineNumber, pendingDirectives);
187
+ if (directiveResult.sectionBucket) {
188
+ activeBucket = directiveResult.sectionBucket;
189
+ pendingDirectives = {};
190
+ } else {
191
+ pendingDirectives = directiveResult.directives;
192
+ }
193
+ }
194
+ continue;
195
+ }
196
+ const assignmentMatch = KEY_ASSIGNMENT_PATTERN.exec(trimmed);
197
+ if (!assignmentMatch) {
198
+ throw new Error(`Malformed assignment at line ${lineNumber}: "${line}"`);
199
+ }
200
+ const key = assignmentMatch[1];
201
+ const rightHandSide = assignmentMatch[2];
202
+ const split = splitValueAndInlineComment(rightHandSide);
203
+ const value = normalizeAssignedValue(split.value.trim());
204
+ let directives = { ...pendingDirectives };
205
+ if (split.comment) {
206
+ const inlineComment = split.comment.trim();
207
+ if (inlineComment.startsWith(SENTINEL_DIRECTIVE_PREFIX)) {
208
+ const parsedInline = parseDirectiveGroup(inlineComment, lineNumber, directives);
209
+ if (parsedInline.sectionBucket) {
210
+ throw new Error(`Section directives are not allowed inline at line ${lineNumber}`);
211
+ }
212
+ directives = parsedInline.directives;
213
+ }
214
+ }
215
+ entries.push({
216
+ key,
217
+ value,
218
+ line: lineNumber,
219
+ sectionBucket: activeBucket,
220
+ directives
221
+ });
222
+ pendingDirectives = {};
223
+ }
224
+ return { entries };
225
+ }
226
+ function stringifyDotenvDocument(document) {
227
+ if (!document || !Array.isArray(document.entries)) {
228
+ throw new Error("Invalid dotenv document: expected an entries array");
229
+ }
230
+ const grouped = {
231
+ server: [],
232
+ client: [],
233
+ shared: []
234
+ };
235
+ const sortedEntries = [...document.entries].sort((left, right) => {
236
+ if (left.line !== right.line)
237
+ return left.line - right.line;
238
+ return left.key.localeCompare(right.key);
239
+ });
240
+ for (const entry of sortedEntries) {
241
+ const bucket = entry.directives.bucket ?? entry.sectionBucket ?? "server";
242
+ grouped[bucket].push(entry);
243
+ }
244
+ const lines = [];
245
+ for (const bucket of BUCKETS) {
246
+ lines.push(`# @${bucket}`);
247
+ lines.push("");
248
+ for (const entry of grouped[bucket]) {
249
+ const type = entry.directives.type ?? "requiredString";
250
+ const optional = entry.directives.optional ?? false;
251
+ const hasDefault = entry.directives.hasDefault ?? false;
252
+ const redacted = entry.directives.redacted ?? false;
253
+ const defaultLiteral = hasDefault ? ` ${toDirectiveLiteral(entry.directives.defaultValue)}` : "";
254
+ lines.push(`# @type ${type}`);
255
+ lines.push(`# @bucket ${bucket}`);
256
+ lines.push(`# @optional ${optional}`);
257
+ lines.push(`# @default${defaultLiteral}`);
258
+ lines.push(`# @redacted ${redacted}`);
259
+ lines.push(`${entry.key}=${serializeEnvValue(entry.value)}`);
260
+ lines.push("");
261
+ }
262
+ }
263
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
264
+ lines.pop();
265
+ }
266
+ return `${lines.join(`
267
+ `)}
268
+ `;
269
+ }
270
+ function splitValueAndInlineComment(value) {
271
+ let inSingleQuote = false;
272
+ let inDoubleQuote = false;
273
+ let escaped = false;
274
+ for (let index = 0;index < value.length; index += 1) {
275
+ const char = value[index];
276
+ if (escaped) {
277
+ escaped = false;
278
+ continue;
279
+ }
280
+ if (char === "\\") {
281
+ escaped = true;
282
+ continue;
283
+ }
284
+ if (char === "'" && !inDoubleQuote) {
285
+ inSingleQuote = !inSingleQuote;
286
+ continue;
287
+ }
288
+ if (char === '"' && !inSingleQuote) {
289
+ inDoubleQuote = !inDoubleQuote;
290
+ continue;
291
+ }
292
+ if (!inSingleQuote && !inDoubleQuote && char === "#") {
293
+ return {
294
+ value: value.slice(0, index).trimEnd(),
295
+ comment: value.slice(index + 1)
296
+ };
297
+ }
298
+ }
299
+ return { value };
300
+ }
301
+ function parseDirectiveGroup(directiveText, lineNumber, base) {
302
+ const directives = { ...base };
303
+ const tokens = directiveText.split(/\s+(?=@)/g).map((token) => token.trim()).filter(Boolean);
304
+ let sectionBucket;
305
+ for (const token of tokens) {
306
+ const parsed = parseDirectiveToken(token, lineNumber);
307
+ if ("type" in parsed)
308
+ directives.type = parsed.type;
309
+ if ("optional" in parsed)
310
+ directives.optional = parsed.optional;
311
+ if ("hasDefault" in parsed)
312
+ directives.hasDefault = parsed.hasDefault;
313
+ if ("defaultValue" in parsed)
314
+ directives.defaultValue = parsed.defaultValue;
315
+ if ("redacted" in parsed)
316
+ directives.redacted = parsed.redacted;
317
+ if ("bucket" in parsed)
318
+ directives.bucket = parsed.bucket;
319
+ if ("sectionBucket" in parsed)
320
+ sectionBucket = parsed.sectionBucket;
321
+ }
322
+ return { directives, sectionBucket };
323
+ }
324
+ function parseDirectiveToken(token, lineNumber) {
325
+ if (!token.startsWith("@")) {
326
+ throw new Error(`Malformed directive at line ${lineNumber}: "${token}"`);
327
+ }
328
+ const spaceIndex = token.indexOf(" ");
329
+ const name = (spaceIndex === -1 ? token.slice(1) : token.slice(1, spaceIndex)).trim();
330
+ const value = (spaceIndex === -1 ? "" : token.slice(spaceIndex + 1)).trim();
331
+ if (name === "server" || name === "client" || name === "shared") {
332
+ if (value.length > 0) {
333
+ throw new Error(`Section directive "@${name}" must not include a value (line ${lineNumber})`);
334
+ }
335
+ return { sectionBucket: name };
336
+ }
337
+ if (name === "type") {
338
+ if (value.length === 0) {
339
+ throw new Error(`Directive "@type" requires a value at line ${lineNumber}`);
340
+ }
341
+ return { type: parseSchemaKind(value, lineNumber) };
342
+ }
343
+ if (name === "optional") {
344
+ return { optional: parseBooleanDirective(value || undefined, true) };
345
+ }
346
+ if (name === "default") {
347
+ if (value.length === 0) {
348
+ return { hasDefault: false, defaultValue: undefined };
349
+ }
350
+ return { hasDefault: true, defaultValue: parseLiteral(value) };
351
+ }
352
+ if (name === "redacted") {
353
+ return { redacted: parseBooleanDirective(value || undefined, true) };
354
+ }
355
+ if (name === "bucket") {
356
+ if (value.length === 0) {
357
+ throw new Error(`Directive "@bucket" requires a value at line ${lineNumber}`);
358
+ }
359
+ if (!isBucket(value)) {
360
+ throw new Error(`Invalid bucket "${value}" at line ${lineNumber}`);
361
+ }
362
+ return { bucket: value };
363
+ }
364
+ throw new Error(`Unknown directive "@${name}" at line ${lineNumber}`);
365
+ }
366
+ function parseSchemaKind(rawValue, lineNumber) {
367
+ const normalized = rawValue.trim().toLowerCase();
368
+ const aliasMap = {
369
+ string: "requiredString",
370
+ requiredstring: "requiredString",
371
+ bool: "boolean",
372
+ boolean: "boolean",
373
+ int: "integer",
374
+ integer: "integer",
375
+ number: "number",
376
+ port: "port",
377
+ url: "url",
378
+ postgresurl: "postgresUrl",
379
+ redisurl: "redisUrl",
380
+ mongourl: "mongoUrl",
381
+ mysqlurl: "mysqlUrl",
382
+ commaseparated: "commaSeparated",
383
+ commaseparatednumbers: "commaSeparatedNumbers",
384
+ commaseparatedurls: "commaSeparatedUrls",
385
+ json: "json",
386
+ "json(schema.unknown)": "json"
387
+ };
388
+ const resolved = aliasMap[normalized];
389
+ if (!resolved || !SCHEMA_KINDS.includes(resolved)) {
390
+ throw new Error(`Invalid @type value "${rawValue}" at line ${lineNumber}`);
391
+ }
392
+ return resolved;
393
+ }
394
+ function normalizeAssignedValue(value) {
395
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
396
+ return value.slice(1, -1);
397
+ }
398
+ return value;
399
+ }
400
+ function serializeEnvValue(value) {
401
+ const literal = toEnvValueLiteral(value);
402
+ if (literal.length === 0)
403
+ return "";
404
+ if (/[\s#]/.test(literal)) {
405
+ return JSON.stringify(literal);
406
+ }
407
+ return literal;
408
+ }
409
+ function isBucket(value) {
410
+ return BUCKETS.includes(value);
411
+ }
412
+ // src/cli/generate-example.ts
413
+ var PLACEHOLDERS = {
414
+ requiredString: "CHANGE_ME",
415
+ boolean: "true",
416
+ integer: "123",
417
+ number: "3.14",
418
+ port: "3000",
419
+ url: "https://example.com",
420
+ postgresUrl: "postgres://user:pass@localhost:5432/app",
421
+ redisUrl: "redis://localhost:6379",
422
+ mongoUrl: "mongodb://localhost:27017/app",
423
+ mysqlUrl: "mysql://user:pass@localhost:3306/app",
424
+ commaSeparated: "alpha,beta,gamma",
425
+ commaSeparatedNumbers: "1,2,3",
426
+ commaSeparatedUrls: "https://one.example.com,https://two.example.com",
427
+ json: '{"key":"value"}'
428
+ };
429
+ function generateExample(manifest) {
430
+ const entries = [];
431
+ let line = 1;
432
+ for (const bucket of BUCKETS) {
433
+ const variables = manifest.variables.filter((variable) => variable.bucket === bucket).sort((left, right) => left.name.localeCompare(right.name));
434
+ for (const variable of variables) {
435
+ const runtimeKey = `${manifest.prefix[bucket]}${variable.name}`;
436
+ const value = variable.hasDefault ? renderDefaultValue(variable) : PLACEHOLDERS[variable.kind] ?? PLACEHOLDERS.requiredString;
437
+ entries.push({
438
+ key: runtimeKey,
439
+ value,
440
+ line,
441
+ sectionBucket: bucket,
442
+ directives: {
443
+ type: variable.kind,
444
+ bucket,
445
+ optional: variable.optional,
446
+ hasDefault: variable.hasDefault,
447
+ defaultValue: variable.defaultValue,
448
+ redacted: variable.redacted
449
+ }
450
+ });
451
+ line += 1;
452
+ }
453
+ }
454
+ return encodeDotenvText({ entries });
455
+ }
456
+ function renderDefaultValue(variable) {
457
+ if (!variable.hasDefault) {
458
+ return PLACEHOLDERS[variable.kind];
459
+ }
460
+ if (variable.kind === "commaSeparated") {
461
+ if (Array.isArray(variable.defaultValue)) {
462
+ return variable.defaultValue.map((item) => String(item)).join(",");
463
+ }
464
+ return String(variable.defaultValue ?? "");
465
+ }
466
+ if (variable.kind === "commaSeparatedNumbers") {
467
+ if (Array.isArray(variable.defaultValue)) {
468
+ return variable.defaultValue.map((item) => Number(item)).filter((item) => Number.isFinite(item)).join(",");
469
+ }
470
+ return String(variable.defaultValue ?? "0");
471
+ }
472
+ if (variable.kind === "commaSeparatedUrls") {
473
+ if (Array.isArray(variable.defaultValue)) {
474
+ return variable.defaultValue.map((item) => String(item)).join(",");
475
+ }
476
+ return String(variable.defaultValue ?? PLACEHOLDERS.commaSeparatedUrls);
477
+ }
478
+ if (variable.kind === "json") {
479
+ if (typeof variable.defaultValue === "string") {
480
+ return variable.defaultValue;
481
+ }
482
+ return JSON.stringify(variable.defaultValue ?? {});
483
+ }
484
+ return toEnvValueLiteral(variable.defaultValue);
485
+ }
486
+ // src/cli/manifest-codec.ts
487
+ import { Schema as Schema2 } from "effect";
488
+ var MANIFEST_SENTINEL = "@envil:manifest";
489
+ var MANIFEST_BLOCK_PATTERN = /\/\*\s*@envil:manifest\s*([\s\S]*?)\*\//m;
490
+ var ManifestCodec = Schema2.transform(Schema2.String, Schema2.Unknown, {
491
+ decode: (source) => decodeManifestFromSourceText(String(source)),
492
+ encode: (value) => encodeManifestToBlockComment(value)
493
+ });
494
+ function decodeManifestFromSource(source) {
495
+ return Schema2.decodeUnknownSync(ManifestCodec)(source);
496
+ }
497
+ function encodeManifestBlock(manifest) {
498
+ return Schema2.encodeSync(ManifestCodec)(manifest);
499
+ }
500
+ function decodeManifestFromSourceText(source) {
501
+ const match = MANIFEST_BLOCK_PATTERN.exec(source);
502
+ if (!match) {
503
+ throw new Error("Manifest block not found. Expected a top-level @envil:manifest block comment.");
504
+ }
505
+ const jsonText = match[1].trim();
506
+ let parsed;
507
+ try {
508
+ parsed = JSON.parse(jsonText);
509
+ } catch (error) {
510
+ throw new Error(`Malformed manifest JSON: ${String(error)}`);
511
+ }
512
+ return validateManifest(parsed);
513
+ }
514
+ function encodeManifestToBlockComment(manifest) {
515
+ const normalized = validateManifest(manifest);
516
+ return `/* ${MANIFEST_SENTINEL}
517
+ ${JSON.stringify(normalized, null, 2)}
518
+ */`;
519
+ }
520
+ function validateManifest(value) {
521
+ if (!value || typeof value !== "object") {
522
+ throw new Error("Manifest must be an object.");
523
+ }
524
+ const candidate = value;
525
+ if (candidate.version !== MANIFEST_VERSION) {
526
+ throw new Error(`Unsupported manifest version "${String(candidate.version)}".`);
527
+ }
528
+ const prefix = validatePrefix(candidate.prefix);
529
+ const variablesRaw = candidate.variables;
530
+ if (!Array.isArray(variablesRaw)) {
531
+ throw new Error('Manifest field "variables" must be an array.');
532
+ }
533
+ const variables = variablesRaw.map((item, index) => validateManifestVariable(item, index));
534
+ variables.sort((left, right) => {
535
+ if (left.bucket !== right.bucket)
536
+ return left.bucket.localeCompare(right.bucket);
537
+ return left.name.localeCompare(right.name);
538
+ });
539
+ return {
540
+ version: MANIFEST_VERSION,
541
+ prefix,
542
+ variables
543
+ };
544
+ }
545
+ function validatePrefix(prefixValue) {
546
+ if (!prefixValue || typeof prefixValue !== "object") {
547
+ throw new Error('Manifest field "prefix" must be an object.');
548
+ }
549
+ const prefix = prefixValue;
550
+ const server = typeof prefix.server === "string" ? prefix.server : "";
551
+ const client = typeof prefix.client === "string" ? prefix.client : "";
552
+ const shared = typeof prefix.shared === "string" ? prefix.shared : "";
553
+ return { server, client, shared };
554
+ }
555
+ function validateManifestVariable(value, index) {
556
+ if (!value || typeof value !== "object") {
557
+ throw new Error(`Manifest variable at index ${index} must be an object.`);
558
+ }
559
+ const variable = value;
560
+ if (typeof variable.name !== "string" || variable.name.length === 0) {
561
+ throw new Error(`Manifest variable at index ${index} has an invalid "name".`);
562
+ }
563
+ if (typeof variable.bucket !== "string" || !BUCKETS.includes(variable.bucket)) {
564
+ throw new Error(`Manifest variable "${variable.name}" has an invalid "bucket".`);
565
+ }
566
+ if (typeof variable.kind !== "string" || !SCHEMA_KINDS.includes(variable.kind)) {
567
+ throw new Error(`Manifest variable "${variable.name}" has an invalid "kind".`);
568
+ }
569
+ if (typeof variable.optional !== "boolean") {
570
+ throw new Error(`Manifest variable "${variable.name}" has an invalid "optional" flag.`);
571
+ }
572
+ if (typeof variable.hasDefault !== "boolean") {
573
+ throw new Error(`Manifest variable "${variable.name}" has an invalid "hasDefault" flag.`);
574
+ }
575
+ if (typeof variable.redacted !== "boolean") {
576
+ throw new Error(`Manifest variable "${variable.name}" has an invalid "redacted" flag.`);
577
+ }
578
+ return {
579
+ name: variable.name,
580
+ bucket: variable.bucket,
581
+ kind: variable.kind,
582
+ optional: variable.optional,
583
+ hasDefault: variable.hasDefault,
584
+ defaultValue: variable.defaultValue,
585
+ redacted: variable.redacted
586
+ };
587
+ }
588
+
589
+ // src/cli/generate-env-ts.ts
590
+ function generateEnvTs(model) {
591
+ const sortedVariables = [...model.variables].sort((left, right) => {
592
+ if (left.bucket !== right.bucket)
593
+ return left.bucket.localeCompare(right.bucket);
594
+ return left.schemaKey.localeCompare(right.schemaKey);
595
+ });
596
+ const grouped = {
597
+ server: [],
598
+ client: [],
599
+ shared: []
600
+ };
601
+ const helperImports = new Set(["createEnv"]);
602
+ let needsSchemaImport = false;
603
+ for (const variable of sortedVariables) {
604
+ const rendered = renderSchemaExpression(variable.kind, {
605
+ optional: variable.optional,
606
+ hasDefault: variable.hasDefault,
607
+ defaultValue: variable.defaultValue,
608
+ redacted: variable.redacted
609
+ });
610
+ for (const helper of rendered.helpers) {
611
+ helperImports.add(helper);
612
+ }
613
+ needsSchemaImport ||= rendered.needsSchemaImport;
614
+ grouped[variable.bucket].push({
615
+ key: quoteObjectKey(variable.schemaKey),
616
+ expression: rendered.expression
617
+ });
618
+ }
619
+ const importLine = `import { ${[...helperImports].sort().join(", ")} } from "@ayronforge/envil";`;
620
+ const schemaImportLine = needsSchemaImport ? `import { Schema } from "effect";` : "";
621
+ const manifestBlock = encodeManifestBlock(toManifest(model));
622
+ const sourceLines = [
623
+ manifestBlock,
624
+ "",
625
+ importLine,
626
+ schemaImportLine,
627
+ "",
628
+ "export const envDefinition = {",
629
+ " prefix: {",
630
+ ` server: ${JSON.stringify(model.prefix.server)},`,
631
+ ` client: ${JSON.stringify(model.prefix.client)},`,
632
+ ` shared: ${JSON.stringify(model.prefix.shared)},`,
633
+ " },",
634
+ " server: {",
635
+ ...renderBucketEntries(grouped.server),
636
+ " },",
637
+ " client: {",
638
+ ...renderBucketEntries(grouped.client),
639
+ " },",
640
+ " shared: {",
641
+ ...renderBucketEntries(grouped.shared),
642
+ " },",
643
+ "} as const;",
644
+ "",
645
+ "export const env = createEnv(envDefinition);",
646
+ ""
647
+ ];
648
+ return sourceLines.filter((line, index, all) => line !== "" || all[index - 1] !== "").join(`
649
+ `);
650
+ }
651
+ function renderBucketEntries(entries) {
652
+ if (entries.length === 0) {
653
+ return [];
654
+ }
655
+ return entries.map((entry) => ` ${entry.key}: ${entry.expression},`);
656
+ }
657
+ function renderSchemaExpression(kind, wrappers) {
658
+ let expression = renderBaseExpression(kind);
659
+ const helpers = new Set(requiredHelpersForKind(kind));
660
+ const needsSchemaImport = kind === "number" || kind === "json";
661
+ if (wrappers.optional && !wrappers.hasDefault) {
662
+ expression = `optional(${expression})`;
663
+ helpers.add("optional");
664
+ }
665
+ if (wrappers.hasDefault) {
666
+ expression = `withDefault(${expression}, ${toCodeLiteral(wrappers.defaultValue)})`;
667
+ helpers.add("withDefault");
668
+ }
669
+ if (wrappers.redacted) {
670
+ expression = `redacted(${expression})`;
671
+ helpers.add("redacted");
672
+ }
673
+ return { expression, helpers, needsSchemaImport };
674
+ }
675
+ function renderBaseExpression(kind) {
676
+ switch (kind) {
677
+ case "requiredString":
678
+ return "requiredString";
679
+ case "boolean":
680
+ return "boolean";
681
+ case "integer":
682
+ return "integer";
683
+ case "number":
684
+ return "Schema.NumberFromString";
685
+ case "port":
686
+ return "port";
687
+ case "url":
688
+ return "url";
689
+ case "postgresUrl":
690
+ return "postgresUrl";
691
+ case "redisUrl":
692
+ return "redisUrl";
693
+ case "mongoUrl":
694
+ return "mongoUrl";
695
+ case "mysqlUrl":
696
+ return "mysqlUrl";
697
+ case "commaSeparated":
698
+ return "commaSeparated";
699
+ case "commaSeparatedNumbers":
700
+ return "commaSeparatedNumbers";
701
+ case "commaSeparatedUrls":
702
+ return "commaSeparatedUrls";
703
+ case "json":
704
+ return "json(Schema.Unknown)";
705
+ default:
706
+ return "requiredString";
707
+ }
708
+ }
709
+ function requiredHelpersForKind(kind) {
710
+ switch (kind) {
711
+ case "requiredString":
712
+ return ["requiredString"];
713
+ case "boolean":
714
+ return ["boolean"];
715
+ case "integer":
716
+ return ["integer"];
717
+ case "number":
718
+ return [];
719
+ case "port":
720
+ return ["port"];
721
+ case "url":
722
+ return ["url"];
723
+ case "postgresUrl":
724
+ return ["postgresUrl"];
725
+ case "redisUrl":
726
+ return ["redisUrl"];
727
+ case "mongoUrl":
728
+ return ["mongoUrl"];
729
+ case "mysqlUrl":
730
+ return ["mysqlUrl"];
731
+ case "commaSeparated":
732
+ return ["commaSeparated"];
733
+ case "commaSeparatedNumbers":
734
+ return ["commaSeparatedNumbers"];
735
+ case "commaSeparatedUrls":
736
+ return ["commaSeparatedUrls"];
737
+ case "json":
738
+ return ["json"];
739
+ default:
740
+ return ["requiredString"];
741
+ }
742
+ }
743
+ function quoteObjectKey(value) {
744
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value) ? value : JSON.stringify(value);
745
+ }
746
+ function toManifest(model) {
747
+ const variables = [...model.variables].map((variable) => ({
748
+ name: variable.schemaKey,
749
+ bucket: variable.bucket,
750
+ kind: variable.kind,
751
+ optional: variable.optional,
752
+ hasDefault: variable.hasDefault,
753
+ defaultValue: variable.defaultValue,
754
+ redacted: variable.redacted
755
+ })).sort((left, right) => {
756
+ if (left.bucket !== right.bucket)
757
+ return left.bucket.localeCompare(right.bucket);
758
+ return left.name.localeCompare(right.name);
759
+ });
760
+ return {
761
+ version: MANIFEST_VERSION,
762
+ prefix: model.prefix,
763
+ variables
764
+ };
765
+ }
766
+ // src/cli/infer.ts
767
+ var STRING_KINDS = new Set([
768
+ "requiredString",
769
+ "url",
770
+ "postgresUrl",
771
+ "redisUrl",
772
+ "mongoUrl",
773
+ "mysqlUrl"
774
+ ]);
775
+ var BUCKET_ORDER = {
776
+ server: 0,
777
+ client: 1,
778
+ shared: 2
779
+ };
780
+ function inferModel(document, options) {
781
+ const runtimeEnv = {};
782
+ const inferredVariables = [];
783
+ for (const entry of document.entries) {
784
+ runtimeEnv[entry.key] = entry.value;
785
+ const { bucket, schemaKey } = resolveBucketAndSchemaKey(entry.key, options.prefix, {
786
+ explicitBucket: entry.directives.bucket,
787
+ sectionBucket: entry.sectionBucket
788
+ });
789
+ const resolvedKind = entry.directives.type ?? inferSchemaKind(entry.key, entry.value);
790
+ const hasDefault = entry.directives.hasDefault ?? false;
791
+ const defaultValue = hasDefault ? normalizeDefaultValue(resolvedKind, entry.directives.defaultValue) : undefined;
792
+ const optionalFlag = entry.directives.optional ?? false;
793
+ const redactedFlag = entry.directives.redacted ?? false;
794
+ const duplicate = inferredVariables.find((candidate) => candidate.bucket === bucket && candidate.schemaKey === schemaKey);
795
+ if (duplicate) {
796
+ throw new Error(`Duplicate schema key "${schemaKey}" in bucket "${bucket}" at line ${entry.line}.`);
797
+ }
798
+ inferredVariables.push({
799
+ schemaKey,
800
+ runtimeKey: entry.key,
801
+ bucket,
802
+ kind: resolvedKind,
803
+ optional: optionalFlag,
804
+ hasDefault,
805
+ defaultValue,
806
+ redacted: redactedFlag,
807
+ sourceLine: entry.line
808
+ });
809
+ }
810
+ inferredVariables.sort((left, right) => {
811
+ const bucketOrder = BUCKET_ORDER[left.bucket] - BUCKET_ORDER[right.bucket];
812
+ if (bucketOrder !== 0)
813
+ return bucketOrder;
814
+ return left.schemaKey.localeCompare(right.schemaKey);
815
+ });
816
+ return {
817
+ prefix: options.prefix,
818
+ variables: inferredVariables,
819
+ runtimeEnv
820
+ };
821
+ }
822
+ function resolveBucketAndSchemaKey(runtimeKey, prefix, options) {
823
+ const explicitBucket = options.explicitBucket ?? options.sectionBucket;
824
+ if (explicitBucket) {
825
+ return {
826
+ bucket: explicitBucket,
827
+ schemaKey: stripPrefix(runtimeKey, prefix[explicitBucket])
828
+ };
829
+ }
830
+ const inferredFromPrefix = inferBucketFromPrefix(runtimeKey, prefix);
831
+ if (inferredFromPrefix) {
832
+ return inferredFromPrefix;
833
+ }
834
+ return {
835
+ bucket: "server",
836
+ schemaKey: runtimeKey
837
+ };
838
+ }
839
+ function inferBucketFromPrefix(runtimeKey, prefix) {
840
+ const candidates = [
841
+ { bucket: "client", prefix: prefix.client },
842
+ { bucket: "server", prefix: prefix.server },
843
+ { bucket: "shared", prefix: prefix.shared }
844
+ ].filter((candidate) => candidate.prefix.length > 0 && runtimeKey.startsWith(candidate.prefix)).sort((left, right) => right.prefix.length - left.prefix.length);
845
+ const best = candidates[0];
846
+ if (!best)
847
+ return;
848
+ return {
849
+ bucket: best.bucket,
850
+ schemaKey: stripPrefix(runtimeKey, best.prefix)
851
+ };
852
+ }
853
+ function stripPrefix(key, prefix) {
854
+ if (!prefix || !key.startsWith(prefix)) {
855
+ return key;
856
+ }
857
+ const stripped = key.slice(prefix.length);
858
+ return stripped.length > 0 ? stripped : key;
859
+ }
860
+ function inferSchemaKind(key, rawValue) {
861
+ const value = rawValue.trim();
862
+ const lowerValue = value.toLowerCase();
863
+ const upperKey = key.toUpperCase();
864
+ if (isJsonCandidate(value)) {
865
+ return "json";
866
+ }
867
+ if (lowerValue.startsWith("postgres://") || lowerValue.startsWith("postgresql://")) {
868
+ return "postgresUrl";
869
+ }
870
+ if (lowerValue.startsWith("redis://") || lowerValue.startsWith("rediss://")) {
871
+ return "redisUrl";
872
+ }
873
+ if (lowerValue.startsWith("mongodb://") || lowerValue.startsWith("mongodb+srv://")) {
874
+ return "mongoUrl";
875
+ }
876
+ if (lowerValue.startsWith("mysql://") || lowerValue.startsWith("mysqls://")) {
877
+ return "mysqlUrl";
878
+ }
879
+ if (isCommaSeparatedCandidate(value)) {
880
+ const parts = splitCommaValues(value);
881
+ if (parts.length > 0 && parts.every((part) => isIntegerString(part) || isNumberString(part))) {
882
+ return "commaSeparatedNumbers";
883
+ }
884
+ if (parts.length > 0 && parts.every((part) => isHttpUrl(part))) {
885
+ return "commaSeparatedUrls";
886
+ }
887
+ return "commaSeparated";
888
+ }
889
+ if (isHttpUrl(value)) {
890
+ return "url";
891
+ }
892
+ if (isBooleanString(lowerValue)) {
893
+ return "boolean";
894
+ }
895
+ if (isIntegerString(value)) {
896
+ const numberValue = Number(value);
897
+ if (upperKey.includes("PORT") && numberValue >= 1 && numberValue <= 65535) {
898
+ return "port";
899
+ }
900
+ return "integer";
901
+ }
902
+ if (isNumberString(value)) {
903
+ return "number";
904
+ }
905
+ return "requiredString";
906
+ }
907
+ function isBooleanString(value) {
908
+ return value === "true" || value === "false" || value === "1" || value === "0";
909
+ }
910
+ function isIntegerString(value) {
911
+ return /^[+-]?\d+$/.test(value);
912
+ }
913
+ function isNumberString(value) {
914
+ return /^[+-]?(?:\d+\.?\d*|\.\d+)$/.test(value);
915
+ }
916
+ function isHttpUrl(value) {
917
+ try {
918
+ const parsed = new URL(value);
919
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
920
+ } catch {
921
+ return false;
922
+ }
923
+ }
924
+ function isCommaSeparatedCandidate(value) {
925
+ return value.includes(",") && !value.startsWith("{") && !value.startsWith("[");
926
+ }
927
+ function splitCommaValues(value) {
928
+ return value.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
929
+ }
930
+ function isJsonCandidate(value) {
931
+ const trimmed = value.trim();
932
+ if (!(trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]"))) {
933
+ return false;
934
+ }
935
+ try {
936
+ JSON.parse(trimmed);
937
+ return true;
938
+ } catch {
939
+ return false;
940
+ }
941
+ }
942
+ function normalizeDefaultValue(kind, value) {
943
+ if (kind === "boolean") {
944
+ if (typeof value === "boolean")
945
+ return value;
946
+ if (typeof value === "string") {
947
+ const normalized = value.toLowerCase();
948
+ return normalized === "true" || normalized === "1";
949
+ }
950
+ return Boolean(value);
951
+ }
952
+ if (kind === "integer") {
953
+ const numeric = Number(value);
954
+ if (!Number.isFinite(numeric))
955
+ return 0;
956
+ return Math.trunc(numeric);
957
+ }
958
+ if (kind === "number") {
959
+ const numeric = Number(value);
960
+ return Number.isFinite(numeric) ? numeric : 0;
961
+ }
962
+ if (kind === "port") {
963
+ const numeric = Math.trunc(Number(value));
964
+ if (Number.isFinite(numeric) && numeric >= 1 && numeric <= 65535) {
965
+ return numeric;
966
+ }
967
+ return 3000;
968
+ }
969
+ if (kind === "commaSeparated") {
970
+ if (Array.isArray(value)) {
971
+ return value.map((item) => String(item));
972
+ }
973
+ if (typeof value === "string") {
974
+ return splitCommaValues(value);
975
+ }
976
+ return [String(value ?? "")];
977
+ }
978
+ if (kind === "commaSeparatedNumbers") {
979
+ if (Array.isArray(value)) {
980
+ return value.map((item) => Number(item)).filter((item) => Number.isFinite(item));
981
+ }
982
+ if (typeof value === "string") {
983
+ return splitCommaValues(value).map((item) => Number(item)).filter((item) => Number.isFinite(item));
984
+ }
985
+ const numeric = Number(value);
986
+ return Number.isFinite(numeric) ? [numeric] : [0];
987
+ }
988
+ if (kind === "commaSeparatedUrls") {
989
+ if (Array.isArray(value)) {
990
+ return value.map((item) => String(item));
991
+ }
992
+ if (typeof value === "string") {
993
+ return splitCommaValues(value);
994
+ }
995
+ return [String(value ?? "https://example.com")];
996
+ }
997
+ if (kind === "json") {
998
+ return value ?? {};
999
+ }
1000
+ if (STRING_KINDS.has(kind)) {
1001
+ const stringValue = String(value ?? "");
1002
+ return stringValue.length > 0 ? stringValue : "value";
1003
+ }
1004
+ return value;
1005
+ }
1006
+ // src/cli.ts
1007
+ var DEFAULT_IO = {
1008
+ cwd: () => process.cwd(),
1009
+ stdout: (message) => process.stdout.write(message),
1010
+ stderr: (message) => process.stderr.write(message)
1011
+ };
1012
+ var FRAMEWORK_PREFIXES = {
1013
+ nextjs: { server: "", client: "NEXT_PUBLIC_", shared: "" },
1014
+ vite: { server: "", client: "VITE_", shared: "" },
1015
+ expo: { server: "", client: "EXPO_PUBLIC_", shared: "" },
1016
+ nuxt: { server: "", client: "NUXT_PUBLIC_", shared: "" },
1017
+ sveltekit: { server: "", client: "PUBLIC_", shared: "" },
1018
+ astro: { server: "", client: "PUBLIC_", shared: "" }
1019
+ };
1020
+ async function runCli(argv, io = {}) {
1021
+ const runtimeIO = {
1022
+ cwd: io.cwd ?? DEFAULT_IO.cwd,
1023
+ stdout: io.stdout ?? DEFAULT_IO.stdout,
1024
+ stderr: io.stderr ?? DEFAULT_IO.stderr
1025
+ };
1026
+ try {
1027
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
1028
+ runtimeIO.stdout(getRootHelpText());
1029
+ return 0;
1030
+ }
1031
+ if (argv[0] !== "add") {
1032
+ throw new Error(`Unknown command "${argv[0]}".
1033
+ ${getRootHelpText()}`);
1034
+ }
1035
+ const subcommand = argv[1];
1036
+ if (!subcommand || subcommand === "--help") {
1037
+ runtimeIO.stdout(getAddHelpText());
1038
+ return 0;
1039
+ }
1040
+ if (subcommand === "env") {
1041
+ const options = parseAddEnvOptions(argv.slice(2));
1042
+ if (options.help) {
1043
+ runtimeIO.stdout(getAddEnvHelpText());
1044
+ return 0;
1045
+ }
1046
+ await runAddEnv(options, runtimeIO);
1047
+ return 0;
1048
+ }
1049
+ if (subcommand === "example") {
1050
+ const options = parseAddExampleOptions(argv.slice(2));
1051
+ if (options.help) {
1052
+ runtimeIO.stdout(getAddExampleHelpText());
1053
+ return 0;
1054
+ }
1055
+ await runAddExample(options, runtimeIO);
1056
+ return 0;
1057
+ }
1058
+ throw new Error(`Unknown subcommand "add ${subcommand}".
1059
+ ${getAddHelpText()}`);
1060
+ } catch (error) {
1061
+ runtimeIO.stderr(`${formatErrorMessage(error)}
1062
+ `);
1063
+ return 1;
1064
+ }
1065
+ }
1066
+ async function runAddEnv(options, io) {
1067
+ const cwd = io.cwd();
1068
+ const inputPath = resolveFromCwd(cwd, options.input ?? ".env.example");
1069
+ const outputPath = resolveFromCwd(cwd, options.output ?? await getDefaultEnvOutputPath(cwd));
1070
+ const source = await readTextFileOrThrow(inputPath, "input file");
1071
+ const dotenv = decodeDotenvText(source);
1072
+ const prefix = resolvePrefix(options);
1073
+ const inferred = inferModel(dotenv, { prefix });
1074
+ const generated = generateEnvTs(inferred);
1075
+ await ensureWritableTarget(outputPath, options.force);
1076
+ await writeFileAtomic(outputPath, generated);
1077
+ io.stdout(`Generated ${outputPath}
1078
+ `);
1079
+ }
1080
+ async function runAddExample(options, io) {
1081
+ const cwd = io.cwd();
1082
+ const defaultInput = await getDefaultExampleInputPath(cwd);
1083
+ const inputPath = resolveFromCwd(cwd, options.input ?? defaultInput);
1084
+ const outputPath = resolveFromCwd(cwd, options.output ?? ".env.example");
1085
+ const source = await readTextFileOrThrow(inputPath, "input env.ts");
1086
+ const manifest = decodeManifestFromSource(source);
1087
+ const generated = generateExample(manifest);
1088
+ await ensureWritableTarget(outputPath, options.force);
1089
+ await writeFileAtomic(outputPath, generated);
1090
+ io.stdout(`Generated ${outputPath}
1091
+ `);
1092
+ }
1093
+ function resolvePrefix(options) {
1094
+ const fromFramework = options.framework ? FRAMEWORK_PREFIXES[options.framework] : undefined;
1095
+ return {
1096
+ server: options.serverPrefix ?? fromFramework?.server ?? "",
1097
+ client: options.clientPrefix ?? fromFramework?.client ?? "",
1098
+ shared: options.sharedPrefix ?? fromFramework?.shared ?? ""
1099
+ };
1100
+ }
1101
+ function parseFlags(args, spec) {
1102
+ const parsed = {};
1103
+ for (let index = 0;index < args.length; index += 1) {
1104
+ const token = args[index];
1105
+ if (!token.startsWith("--")) {
1106
+ throw new Error(`Unexpected argument "${token}"`);
1107
+ }
1108
+ const [nameWithPrefix, inlineValue] = token.split("=", 2);
1109
+ const name = nameWithPrefix.slice(2);
1110
+ const expected = spec[name];
1111
+ if (!expected) {
1112
+ throw new Error(`Unknown option "--${name}"`);
1113
+ }
1114
+ if (expected === "boolean") {
1115
+ if (inlineValue !== undefined) {
1116
+ parsed[name] = inlineValue !== "false";
1117
+ } else {
1118
+ parsed[name] = true;
1119
+ }
1120
+ continue;
1121
+ }
1122
+ if (inlineValue !== undefined) {
1123
+ parsed[name] = inlineValue;
1124
+ continue;
1125
+ }
1126
+ const next = args[index + 1];
1127
+ if (!next || next.startsWith("--")) {
1128
+ throw new Error(`Option "--${name}" requires a value`);
1129
+ }
1130
+ parsed[name] = next;
1131
+ index += 1;
1132
+ }
1133
+ return parsed;
1134
+ }
1135
+ function parseAddEnvOptions(args) {
1136
+ const parsed = parseFlags(args, {
1137
+ input: "string",
1138
+ output: "string",
1139
+ framework: "string",
1140
+ "client-prefix": "string",
1141
+ "server-prefix": "string",
1142
+ "shared-prefix": "string",
1143
+ force: "boolean",
1144
+ help: "boolean"
1145
+ });
1146
+ const frameworkValue = parsed.framework;
1147
+ if (frameworkValue !== undefined) {
1148
+ if (typeof frameworkValue !== "string" || !FRAMEWORKS.includes(frameworkValue)) {
1149
+ throw new Error(`Invalid value for --framework. Expected one of: ${FRAMEWORKS.join(", ")}.`);
1150
+ }
1151
+ }
1152
+ return {
1153
+ input: asOptionalString(parsed.input),
1154
+ output: asOptionalString(parsed.output),
1155
+ framework: frameworkValue,
1156
+ clientPrefix: asOptionalString(parsed["client-prefix"]),
1157
+ serverPrefix: asOptionalString(parsed["server-prefix"]),
1158
+ sharedPrefix: asOptionalString(parsed["shared-prefix"]),
1159
+ force: Boolean(parsed.force),
1160
+ help: Boolean(parsed.help)
1161
+ };
1162
+ }
1163
+ function parseAddExampleOptions(args) {
1164
+ const parsed = parseFlags(args, {
1165
+ input: "string",
1166
+ output: "string",
1167
+ force: "boolean",
1168
+ help: "boolean"
1169
+ });
1170
+ return {
1171
+ input: asOptionalString(parsed.input),
1172
+ output: asOptionalString(parsed.output),
1173
+ force: Boolean(parsed.force),
1174
+ help: Boolean(parsed.help)
1175
+ };
1176
+ }
1177
+ function asOptionalString(value) {
1178
+ return typeof value === "string" ? value : undefined;
1179
+ }
1180
+ function getRootHelpText() {
1181
+ return [
1182
+ "Usage:",
1183
+ " envil add env [options]",
1184
+ " envil add example [options]",
1185
+ "",
1186
+ getAddHelpText().trimEnd(),
1187
+ ""
1188
+ ].join(`
1189
+ `);
1190
+ }
1191
+ function getAddHelpText() {
1192
+ return [
1193
+ "Subcommands:",
1194
+ " envil add env Infer env.ts from .env.example",
1195
+ " envil add example Recreate .env.example from env.ts manifest",
1196
+ "",
1197
+ "Use --help on each subcommand for details.",
1198
+ ""
1199
+ ].join(`
1200
+ `);
1201
+ }
1202
+ function getAddEnvHelpText() {
1203
+ return [
1204
+ "Usage:",
1205
+ " envil add env [options]",
1206
+ "",
1207
+ "Options:",
1208
+ " --input <path> Input .env.example path (default: .env.example)",
1209
+ " --output <path> Output env.ts path (default: src/env.ts or env.ts)",
1210
+ " --framework <name> Prefix preset: nextjs|vite|expo|nuxt|sveltekit|astro",
1211
+ " --client-prefix <value> Client runtime prefix override",
1212
+ " --server-prefix <value> Server runtime prefix override",
1213
+ " --shared-prefix <value> Shared runtime prefix override",
1214
+ " --force Overwrite output file if it exists",
1215
+ " --help Show this help text",
1216
+ ""
1217
+ ].join(`
1218
+ `);
1219
+ }
1220
+ function getAddExampleHelpText() {
1221
+ return [
1222
+ "Usage:",
1223
+ " envil add example [options]",
1224
+ "",
1225
+ "Options:",
1226
+ " --input <path> Input env.ts path (default: env.ts, then src/env.ts fallback)",
1227
+ " --output <path> Output .env.example path (default: .env.example)",
1228
+ " --force Overwrite output file if it exists",
1229
+ " --help Show this help text",
1230
+ ""
1231
+ ].join(`
1232
+ `);
1233
+ }
1234
+ function formatErrorMessage(error) {
1235
+ if (error instanceof Error) {
1236
+ return error.message;
1237
+ }
1238
+ return String(error);
1239
+ }
1240
+ if (import.meta.main) {
1241
+ const exitCode = await runCli(process.argv.slice(2));
1242
+ process.exit(exitCode);
1243
+ }
1244
+ export {
1245
+ runCli
1246
+ };
1247
+
1248
+ //# debugId=B3DBC46CBE56A44864756E2164756E21
1249
+ //# sourceMappingURL=cli.js.map