@ayronforge/envil 0.6.0 → 0.7.2

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/cli.js CHANGED
@@ -65,39 +65,6 @@ async function writeFileAtomic(targetPath, contents) {
65
65
  import { Schema } from "effect";
66
66
 
67
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
68
  function toCodeLiteral(value) {
102
69
  if (typeof value === "string")
103
70
  return JSON.stringify(value);
@@ -109,11 +76,6 @@ function toCodeLiteral(value) {
109
76
  return "undefined";
110
77
  return JSON.stringify(value);
111
78
  }
112
- function toDirectiveLiteral(value) {
113
- if (value === undefined)
114
- return "";
115
- return toCodeLiteral(value);
116
- }
117
79
  function toEnvValueLiteral(value) {
118
80
  if (typeof value === "string")
119
81
  return value;
@@ -151,10 +113,10 @@ var SCHEMA_KINDS = [
151
113
  "commaSeparated",
152
114
  "commaSeparatedNumbers",
153
115
  "commaSeparatedUrls",
154
- "json"
116
+ "json",
117
+ "stringEnum"
155
118
  ];
156
119
  var FRAMEWORKS = ["nextjs", "vite", "expo", "nuxt", "sveltekit", "astro"];
157
- var MANIFEST_VERSION = 1;
158
120
 
159
121
  // src/cli/dotenv-codec.ts
160
122
  var SENTINEL_DIRECTIVE_PREFIX = "@";
@@ -172,6 +134,7 @@ function encodeDotenvText(document) {
172
134
  function parseDotenvText(text) {
173
135
  const lines = text.split(/\r?\n/);
174
136
  const entries = [];
137
+ const prefix = {};
175
138
  let activeBucket;
176
139
  let pendingDirectives = {};
177
140
  for (const [index, line] of lines.entries()) {
@@ -186,6 +149,9 @@ function parseDotenvText(text) {
186
149
  const directiveResult = parseDirectiveGroup(comment, lineNumber, pendingDirectives);
187
150
  if (directiveResult.sectionBucket) {
188
151
  activeBucket = directiveResult.sectionBucket;
152
+ if (directiveResult.sectionPrefix) {
153
+ prefix[directiveResult.sectionBucket] = directiveResult.sectionPrefix;
154
+ }
189
155
  pendingDirectives = {};
190
156
  } else {
191
157
  pendingDirectives = directiveResult.directives;
@@ -221,7 +187,8 @@ function parseDotenvText(text) {
221
187
  });
222
188
  pendingDirectives = {};
223
189
  }
224
- return { entries };
190
+ const hasPrefix = Object.values(prefix).some((v) => v && v.length > 0);
191
+ return { entries, ...hasPrefix ? { prefix } : {} };
225
192
  }
226
193
  function stringifyDotenvDocument(document) {
227
194
  if (!document || !Array.isArray(document.entries)) {
@@ -243,19 +210,22 @@ function stringifyDotenvDocument(document) {
243
210
  }
244
211
  const lines = [];
245
212
  for (const bucket of BUCKETS) {
246
- lines.push(`# @${bucket}`);
213
+ const pfx = document.prefix?.[bucket];
214
+ lines.push(pfx && pfx.length > 0 ? `# @${bucket} ${pfx}` : `# @${bucket}`);
247
215
  lines.push("");
248
216
  for (const entry of grouped[bucket]) {
249
- const type = entry.directives.type ?? "requiredString";
250
217
  const optional = entry.directives.optional ?? false;
251
- const hasDefault = entry.directives.hasDefault ?? false;
252
218
  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}`);
219
+ const noDefault = entry.directives.hasDefault === false;
220
+ if (entry.directives.type === "stringEnum" && entry.directives.stringEnumValues) {
221
+ lines.push(`# @type enum ${entry.directives.stringEnumValues.join(",")}`);
222
+ }
223
+ if (optional)
224
+ lines.push("# @optional");
225
+ if (noDefault)
226
+ lines.push("# @no-default");
227
+ if (redacted)
228
+ lines.push("# @redacted");
259
229
  lines.push(`${entry.key}=${serializeEnvValue(entry.value)}`);
260
230
  lines.push("");
261
231
  }
@@ -302,6 +272,7 @@ function parseDirectiveGroup(directiveText, lineNumber, base) {
302
272
  const directives = { ...base };
303
273
  const tokens = directiveText.split(/\s+(?=@)/g).map((token) => token.trim()).filter(Boolean);
304
274
  let sectionBucket;
275
+ let sectionPrefix;
305
276
  for (const token of tokens) {
306
277
  const parsed = parseDirectiveToken(token, lineNumber);
307
278
  if ("type" in parsed)
@@ -310,16 +281,18 @@ function parseDirectiveGroup(directiveText, lineNumber, base) {
310
281
  directives.optional = parsed.optional;
311
282
  if ("hasDefault" in parsed)
312
283
  directives.hasDefault = parsed.hasDefault;
313
- if ("defaultValue" in parsed)
314
- directives.defaultValue = parsed.defaultValue;
315
284
  if ("redacted" in parsed)
316
285
  directives.redacted = parsed.redacted;
317
286
  if ("bucket" in parsed)
318
287
  directives.bucket = parsed.bucket;
288
+ if ("stringEnumValues" in parsed)
289
+ directives.stringEnumValues = parsed.stringEnumValues;
319
290
  if ("sectionBucket" in parsed)
320
291
  sectionBucket = parsed.sectionBucket;
292
+ if ("sectionPrefix" in parsed)
293
+ sectionPrefix = parsed.sectionPrefix;
321
294
  }
322
- return { directives, sectionBucket };
295
+ return { directives, sectionBucket, sectionPrefix };
323
296
  }
324
297
  function parseDirectiveToken(token, lineNumber) {
325
298
  if (!token.startsWith("@")) {
@@ -329,25 +302,30 @@ function parseDirectiveToken(token, lineNumber) {
329
302
  const name = (spaceIndex === -1 ? token.slice(1) : token.slice(1, spaceIndex)).trim();
330
303
  const value = (spaceIndex === -1 ? "" : token.slice(spaceIndex + 1)).trim();
331
304
  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 };
305
+ return value.length > 0 ? { sectionBucket: name, sectionPrefix: value } : { sectionBucket: name };
336
306
  }
337
307
  if (name === "type") {
338
308
  if (value.length === 0) {
339
309
  throw new Error(`Directive "@type" requires a value at line ${lineNumber}`);
340
310
  }
311
+ if (value === "enum" || value.startsWith("enum ")) {
312
+ const raw = value.slice(4).trim();
313
+ if (raw.length === 0) {
314
+ throw new Error(`Directive "@type enum" requires comma-separated values at line ${lineNumber}`);
315
+ }
316
+ const stringEnumValues = raw.split(",").map((v) => v.trim()).filter(Boolean);
317
+ if (stringEnumValues.length === 0) {
318
+ throw new Error(`Directive "@type enum" requires at least one value at line ${lineNumber}`);
319
+ }
320
+ return { type: "stringEnum", stringEnumValues };
321
+ }
341
322
  return { type: parseSchemaKind(value, lineNumber) };
342
323
  }
343
324
  if (name === "optional") {
344
325
  return { optional: parseBooleanDirective(value || undefined, true) };
345
326
  }
346
- if (name === "default") {
347
- if (value.length === 0) {
348
- return { hasDefault: false, defaultValue: undefined };
349
- }
350
- return { hasDefault: true, defaultValue: parseLiteral(value) };
327
+ if (name === "no-default") {
328
+ return { hasDefault: false };
351
329
  }
352
330
  if (name === "redacted") {
353
331
  return { redacted: parseBooleanDirective(value || undefined, true) };
@@ -409,183 +387,6 @@ function serializeEnvValue(value) {
409
387
  function isBucket(value) {
410
388
  return BUCKETS.includes(value);
411
389
  }
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
390
  // src/cli/generate-env-ts.ts
590
391
  function generateEnvTs(model) {
591
392
  const sortedVariables = [...model.variables].sort((left, right) => {
@@ -605,7 +406,8 @@ function generateEnvTs(model) {
605
406
  optional: variable.optional,
606
407
  hasDefault: variable.hasDefault,
607
408
  defaultValue: variable.defaultValue,
608
- redacted: variable.redacted
409
+ redacted: variable.redacted,
410
+ stringEnumValues: variable.stringEnumValues
609
411
  });
610
412
  for (const helper of rendered.helpers) {
611
413
  helperImports.add(helper);
@@ -618,10 +420,7 @@ function generateEnvTs(model) {
618
420
  }
619
421
  const importLine = `import { ${[...helperImports].sort().join(", ")} } from "@ayronforge/envil";`;
620
422
  const schemaImportLine = needsSchemaImport ? `import { Schema } from "effect";` : "";
621
- const manifestBlock = encodeManifestBlock(toManifest(model));
622
423
  const sourceLines = [
623
- manifestBlock,
624
- "",
625
424
  importLine,
626
425
  schemaImportLine,
627
426
  "",
@@ -655,9 +454,9 @@ function renderBucketEntries(entries) {
655
454
  return entries.map((entry) => ` ${entry.key}: ${entry.expression},`);
656
455
  }
657
456
  function renderSchemaExpression(kind, wrappers) {
658
- let expression = renderBaseExpression(kind);
659
- const helpers = new Set(requiredHelpersForKind(kind));
660
- const needsSchemaImport = kind === "number" || kind === "json";
457
+ let expression = kind === "stringEnum" && wrappers.stringEnumValues ? `stringEnum([${wrappers.stringEnumValues.map((v) => JSON.stringify(v)).join(", ")}])` : SPECIAL_EXPRESSIONS[kind] ?? kind;
458
+ const helpers = new Set([kind]);
459
+ const needsSchemaImport = kind === "json";
661
460
  if (wrappers.optional && !wrappers.hasDefault) {
662
461
  expression = `optional(${expression})`;
663
462
  helpers.add("optional");
@@ -672,97 +471,13 @@ function renderSchemaExpression(kind, wrappers) {
672
471
  }
673
472
  return { expression, helpers, needsSchemaImport };
674
473
  }
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
- }
474
+ var SPECIAL_EXPRESSIONS = {
475
+ json: "json(Schema.Unknown)",
476
+ stringEnum: "stringEnum([])"
477
+ };
743
478
  function quoteObjectKey(value) {
744
479
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value) ? value : JSON.stringify(value);
745
480
  }
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
481
  // src/cli/infer.ts
767
482
  var STRING_KINDS = new Set([
768
483
  "requiredString",
@@ -787,8 +502,9 @@ function inferModel(document, options) {
787
502
  sectionBucket: entry.sectionBucket
788
503
  });
789
504
  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;
505
+ const hasValue = entry.value.trim().length > 0;
506
+ const hasDefault = entry.directives.hasDefault === false ? false : hasValue;
507
+ const defaultValue = hasDefault ? normalizeDefaultValue(resolvedKind, entry.value) : undefined;
792
508
  const optionalFlag = entry.directives.optional ?? false;
793
509
  const redactedFlag = entry.directives.redacted ?? false;
794
510
  const duplicate = inferredVariables.find((candidate) => candidate.bucket === bucket && candidate.schemaKey === schemaKey);
@@ -804,7 +520,8 @@ function inferModel(document, options) {
804
520
  hasDefault,
805
521
  defaultValue,
806
522
  redacted: redactedFlag,
807
- sourceLine: entry.line
523
+ sourceLine: entry.line,
524
+ stringEnumValues: entry.directives.stringEnumValues
808
525
  });
809
526
  }
810
527
  inferredVariables.sort((left, right) => {
@@ -995,14 +712,269 @@ function normalizeDefaultValue(kind, value) {
995
712
  return [String(value ?? "https://example.com")];
996
713
  }
997
714
  if (kind === "json") {
715
+ if (typeof value === "string") {
716
+ try {
717
+ return JSON.parse(value);
718
+ } catch {
719
+ return {};
720
+ }
721
+ }
998
722
  return value ?? {};
999
723
  }
724
+ if (kind === "stringEnum") {
725
+ return String(value ?? "");
726
+ }
1000
727
  if (STRING_KINDS.has(kind)) {
1001
728
  const stringValue = String(value ?? "");
1002
729
  return stringValue.length > 0 ? stringValue : "value";
1003
730
  }
1004
731
  return value;
1005
732
  }
733
+ // src/introspect.ts
734
+ import { Option, Redacted, Schema as Schema3, SchemaAST } from "effect";
735
+
736
+ // src/schemas.ts
737
+ import { Function, Schema as Schema2 } from "effect";
738
+ var DEFAULT_VALUE_ANNOTATION = Symbol.for("@ayronforge/envil/default-value");
739
+ var SCHEMA_KIND_ANNOTATION = Symbol.for("@ayronforge/envil/schema-kind");
740
+ var PLACEHOLDER_ANNOTATION = Symbol.for("@ayronforge/envil/placeholder");
741
+ var OPTIONAL_ANNOTATION = Symbol.for("@ayronforge/envil/optional");
742
+ var REDACTED_ANNOTATION = Symbol.for("@ayronforge/envil/redacted");
743
+ var STRING_ENUM_VALUES_ANNOTATION = Symbol.for("@ayronforge/envil/string-enum-values");
744
+ var withDefault = Function.dual(2, (schema, defaultValue) => {
745
+ const withDefaultSchema = Schema2.transform(Schema2.UndefinedOr(schema), Schema2.typeSchema(schema), {
746
+ decode: (value) => value ?? defaultValue,
747
+ encode: (value) => value
748
+ });
749
+ return withDefaultSchema.annotations({
750
+ [DEFAULT_VALUE_ANNOTATION]: defaultValue
751
+ });
752
+ });
753
+ var optional = (schema) => Schema2.UndefinedOr(schema).annotations({
754
+ [OPTIONAL_ANNOTATION]: true
755
+ });
756
+ var redacted = (schema) => Schema2.Redacted(schema).annotations({
757
+ [REDACTED_ANNOTATION]: true
758
+ });
759
+ var requiredString = Schema2.String.pipe(Schema2.minLength(1)).annotations({
760
+ identifier: "RequiredString",
761
+ [SCHEMA_KIND_ANNOTATION]: "requiredString",
762
+ [PLACEHOLDER_ANNOTATION]: "CHANGE_ME"
763
+ });
764
+ var boolean = Schema2.transform(Schema2.String.pipe(Schema2.filter((s) => ["true", "false", "1", "0"].includes(s.toLowerCase()), {
765
+ identifier: "BooleanString",
766
+ message: () => "Expected 'true', 'false', '1', or '0'"
767
+ })), Schema2.Boolean, {
768
+ decode: (s) => s.toLowerCase() === "true" || s === "1",
769
+ encode: (b) => b ? "true" : "false"
770
+ }).annotations({
771
+ [SCHEMA_KIND_ANNOTATION]: "boolean",
772
+ [PLACEHOLDER_ANNOTATION]: "true"
773
+ });
774
+ var integer = Schema2.NumberFromString.pipe(Schema2.int()).annotations({
775
+ identifier: "Integer",
776
+ [SCHEMA_KIND_ANNOTATION]: "integer",
777
+ [PLACEHOLDER_ANNOTATION]: "123"
778
+ });
779
+ var number = Schema2.NumberFromString.annotations({
780
+ identifier: "Number",
781
+ [SCHEMA_KIND_ANNOTATION]: "number",
782
+ [PLACEHOLDER_ANNOTATION]: "3.14"
783
+ });
784
+ var positiveNumber = Schema2.NumberFromString.pipe(Schema2.positive()).annotations({
785
+ identifier: "PositiveNumber"
786
+ });
787
+ var nonNegativeNumber = Schema2.NumberFromString.pipe(Schema2.nonNegative()).annotations({
788
+ identifier: "NonNegativeNumber"
789
+ });
790
+ var port = Schema2.NumberFromString.pipe(Schema2.int(), Schema2.between(1, 65535)).annotations({
791
+ identifier: "Port",
792
+ [SCHEMA_KIND_ANNOTATION]: "port",
793
+ [PLACEHOLDER_ANNOTATION]: "3000"
794
+ });
795
+ var url = Schema2.String.pipe(Schema2.filter((s) => {
796
+ try {
797
+ new URL(s);
798
+ return s.startsWith("http://") || s.startsWith("https://");
799
+ } catch {
800
+ return false;
801
+ }
802
+ }, { identifier: "Url", message: () => "Expected a valid HTTP or HTTPS URL" })).annotations({
803
+ [SCHEMA_KIND_ANNOTATION]: "url",
804
+ [PLACEHOLDER_ANNOTATION]: "https://example.com"
805
+ });
806
+ var postgresUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("postgres://") || s.startsWith("postgresql://"), {
807
+ identifier: "PostgresUrl",
808
+ message: () => "Expected a valid PostgreSQL connection URL"
809
+ }), Schema2.pattern(/^(postgres|postgresql):\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/)).annotations({
810
+ [SCHEMA_KIND_ANNOTATION]: "postgresUrl",
811
+ [PLACEHOLDER_ANNOTATION]: "postgres://user:pass@localhost:5432/app"
812
+ });
813
+ var redisUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("redis://") || s.startsWith("rediss://"), {
814
+ identifier: "RedisUrl",
815
+ message: () => "Expected a valid Redis connection URL"
816
+ }), Schema2.pattern(/^rediss?:\/\/(?:[^:]+:[^@]+@)?[^:]+(?::\d+)?(?:\/\d+)?$/)).annotations({
817
+ [SCHEMA_KIND_ANNOTATION]: "redisUrl",
818
+ [PLACEHOLDER_ANNOTATION]: "redis://localhost:6379"
819
+ });
820
+ var mongoUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("mongodb://") || s.startsWith("mongodb+srv://"), {
821
+ identifier: "MongoUrl",
822
+ message: () => "Expected a valid MongoDB connection URL"
823
+ }), Schema2.pattern(/^mongodb(\+srv)?:\/\/(?:[^:]+:[^@]+@)?[^/]+(?:\/[^?]*)?(?:\?.*)?$/)).annotations({
824
+ [SCHEMA_KIND_ANNOTATION]: "mongoUrl",
825
+ [PLACEHOLDER_ANNOTATION]: "mongodb://localhost:27017/app"
826
+ });
827
+ var mysqlUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("mysql://") || s.startsWith("mysqls://"), {
828
+ identifier: "MysqlUrl",
829
+ message: () => "Expected a valid MySQL connection URL"
830
+ }), Schema2.pattern(/^mysqls?:\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/)).annotations({
831
+ [SCHEMA_KIND_ANNOTATION]: "mysqlUrl",
832
+ [PLACEHOLDER_ANNOTATION]: "mysql://user:pass@localhost:3306/app"
833
+ });
834
+ var commaSeparated = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(Schema2.String)), {
835
+ decode: (s) => s.split(",").map((x) => x.trim()),
836
+ encode: (a) => a.join(",")
837
+ }).annotations({
838
+ [SCHEMA_KIND_ANNOTATION]: "commaSeparated",
839
+ [PLACEHOLDER_ANNOTATION]: "alpha,beta,gamma"
840
+ });
841
+ var commaSeparatedNumbers = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(Schema2.Number)), {
842
+ decode: (s) => s.split(",").map((x) => {
843
+ const n = Number(x.trim());
844
+ if (Number.isNaN(n))
845
+ throw new Error(`"${x.trim()}" is not a valid number`);
846
+ return n;
847
+ }),
848
+ encode: (a) => a.join(",")
849
+ }).annotations({
850
+ [SCHEMA_KIND_ANNOTATION]: "commaSeparatedNumbers",
851
+ [PLACEHOLDER_ANNOTATION]: "1,2,3"
852
+ });
853
+ var commaSeparatedUrls = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(url)), {
854
+ decode: (s) => s.split(",").map((x) => Schema2.decodeUnknownSync(url)(x.trim())),
855
+ encode: (a) => a.join(",")
856
+ }).annotations({
857
+ [SCHEMA_KIND_ANNOTATION]: "commaSeparatedUrls",
858
+ [PLACEHOLDER_ANNOTATION]: "https://one.example.com,https://two.example.com"
859
+ });
860
+ var stringEnum = (values) => Schema2.Literal(...values).annotations({
861
+ [SCHEMA_KIND_ANNOTATION]: "stringEnum",
862
+ [STRING_ENUM_VALUES_ANNOTATION]: values,
863
+ [PLACEHOLDER_ANNOTATION]: values[0]
864
+ });
865
+ var json = (schema) => Schema2.parseJson(schema).annotations({
866
+ [SCHEMA_KIND_ANNOTATION]: "json",
867
+ [PLACEHOLDER_ANNOTATION]: '{"key":"value"}'
868
+ });
869
+
870
+ // src/introspect.ts
871
+ var BUCKETS2 = ["server", "client", "shared"];
872
+ function getAnn(ast, key) {
873
+ return Option.getOrUndefined(SchemaAST.getAnnotation(ast, key));
874
+ }
875
+ function examineSchema(schema) {
876
+ let ast = schema.ast;
877
+ let isRedacted = false;
878
+ let isOptional = false;
879
+ let hasDefault = false;
880
+ let defaultValue = undefined;
881
+ if (SchemaAST.isTransformation(ast) && getAnn(ast, REDACTED_ANNOTATION)) {
882
+ isRedacted = true;
883
+ ast = ast.from;
884
+ }
885
+ if (SchemaAST.isTransformation(ast) && getAnn(ast, DEFAULT_VALUE_ANNOTATION) !== undefined) {
886
+ hasDefault = true;
887
+ defaultValue = getAnn(ast, DEFAULT_VALUE_ANNOTATION);
888
+ const fromAst = ast.from;
889
+ if (SchemaAST.isUnion(fromAst)) {
890
+ const nonUndefined = fromAst.types.find((t) => !SchemaAST.isUndefinedKeyword(t));
891
+ if (nonUndefined)
892
+ ast = nonUndefined;
893
+ }
894
+ }
895
+ if (SchemaAST.isUnion(ast) && getAnn(ast, OPTIONAL_ANNOTATION)) {
896
+ isOptional = true;
897
+ const nonUndefined = ast.types.find((t) => !SchemaAST.isUndefinedKeyword(t));
898
+ if (nonUndefined)
899
+ ast = nonUndefined;
900
+ }
901
+ const kind = getAnn(ast, SCHEMA_KIND_ANNOTATION);
902
+ const placeholder = getAnn(ast, PLACEHOLDER_ANNOTATION);
903
+ const stringEnumValues = kind === "stringEnum" ? getAnn(ast, STRING_ENUM_VALUES_ANNOTATION) : undefined;
904
+ return {
905
+ kind,
906
+ placeholder,
907
+ optional: isOptional,
908
+ hasDefault,
909
+ defaultValue,
910
+ redacted: isRedacted,
911
+ stringEnumValues
912
+ };
913
+ }
914
+ function buildEnvExample(definition) {
915
+ const prefix = {
916
+ server: definition.prefix?.server ?? "",
917
+ client: definition.prefix?.client ?? "",
918
+ shared: definition.prefix?.shared ?? ""
919
+ };
920
+ const entries = [];
921
+ let line = 1;
922
+ for (const bucket of BUCKETS2) {
923
+ const schemas = definition[bucket] ?? {};
924
+ const keys = Object.keys(schemas).sort();
925
+ for (const key of keys) {
926
+ const schema = schemas[key];
927
+ const examined = examineSchema(schema);
928
+ const runtimeKey = `${prefix[bucket]}${key}`;
929
+ let value;
930
+ if (examined.hasDefault) {
931
+ value = encodeDefault(schema, examined);
932
+ } else {
933
+ value = examined.placeholder ?? "CHANGE_ME";
934
+ }
935
+ entries.push({
936
+ key: runtimeKey,
937
+ value,
938
+ line,
939
+ sectionBucket: bucket,
940
+ directives: {
941
+ type: examined.kind,
942
+ bucket,
943
+ optional: examined.optional,
944
+ hasDefault: examined.hasDefault,
945
+ redacted: examined.redacted,
946
+ stringEnumValues: examined.stringEnumValues
947
+ }
948
+ });
949
+ line += 1;
950
+ }
951
+ }
952
+ const hasPrefix = Object.values(prefix).some((v) => v.length > 0);
953
+ return encodeDotenvText({ entries, ...hasPrefix ? { prefix } : {} });
954
+ }
955
+ function encodeDefault(schema, examined) {
956
+ try {
957
+ let valueToEncode = examined.defaultValue;
958
+ if (examined.redacted) {
959
+ valueToEncode = Redacted.make(valueToEncode);
960
+ }
961
+ const encoded = Schema3.encodeSync(schema)(valueToEncode);
962
+ return String(encoded ?? "");
963
+ } catch {
964
+ return stringifyDefault(examined);
965
+ }
966
+ }
967
+ function stringifyDefault(examined) {
968
+ const val = examined.defaultValue;
969
+ if (val === undefined || val === null)
970
+ return "";
971
+ if (Array.isArray(val))
972
+ return val.map(String).join(",");
973
+ if (typeof val === "object")
974
+ return JSON.stringify(val);
975
+ return String(val);
976
+ }
977
+
1006
978
  // src/cli.ts
1007
979
  var DEFAULT_IO = {
1008
980
  cwd: () => process.cwd(),
@@ -1069,7 +1041,7 @@ async function runAddEnv(options, io) {
1069
1041
  const outputPath = resolveFromCwd(cwd, options.output ?? await getDefaultEnvOutputPath(cwd));
1070
1042
  const source = await readTextFileOrThrow(inputPath, "input file");
1071
1043
  const dotenv = decodeDotenvText(source);
1072
- const prefix = resolvePrefix(options);
1044
+ const prefix = resolvePrefix(options, dotenv.prefix);
1073
1045
  const inferred = inferModel(dotenv, { prefix });
1074
1046
  const generated = generateEnvTs(inferred);
1075
1047
  await ensureWritableTarget(outputPath, options.force);
@@ -1082,20 +1054,28 @@ async function runAddExample(options, io) {
1082
1054
  const defaultInput = await getDefaultExampleInputPath(cwd);
1083
1055
  const inputPath = resolveFromCwd(cwd, options.input ?? defaultInput);
1084
1056
  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);
1057
+ process.env.ENVIL_INTROSPECT_ONLY = "1";
1058
+ let mod;
1059
+ try {
1060
+ mod = await import(inputPath);
1061
+ } finally {
1062
+ delete process.env.ENVIL_INTROSPECT_ONLY;
1063
+ }
1064
+ if (!mod.envDefinition || typeof mod.envDefinition !== "object") {
1065
+ throw new Error(`Expected "envDefinition" export in ${inputPath}. Make sure the file exports an envDefinition object.`);
1066
+ }
1067
+ const generated = buildEnvExample(mod.envDefinition);
1088
1068
  await ensureWritableTarget(outputPath, options.force);
1089
1069
  await writeFileAtomic(outputPath, generated);
1090
1070
  io.stdout(`Generated ${outputPath}
1091
1071
  `);
1092
1072
  }
1093
- function resolvePrefix(options) {
1073
+ function resolvePrefix(options, fromDocument) {
1094
1074
  const fromFramework = options.framework ? FRAMEWORK_PREFIXES[options.framework] : undefined;
1095
1075
  return {
1096
- server: options.serverPrefix ?? fromFramework?.server ?? "",
1097
- client: options.clientPrefix ?? fromFramework?.client ?? "",
1098
- shared: options.sharedPrefix ?? fromFramework?.shared ?? ""
1076
+ server: options.serverPrefix ?? fromFramework?.server ?? fromDocument?.server ?? "",
1077
+ client: options.clientPrefix ?? fromFramework?.client ?? fromDocument?.client ?? "",
1078
+ shared: options.sharedPrefix ?? fromFramework?.shared ?? fromDocument?.shared ?? ""
1099
1079
  };
1100
1080
  }
1101
1081
  function parseFlags(args, spec) {
@@ -1192,7 +1172,7 @@ function getAddHelpText() {
1192
1172
  return [
1193
1173
  "Subcommands:",
1194
1174
  " envil add env Infer env.ts from .env.example",
1195
- " envil add example Recreate .env.example from env.ts manifest",
1175
+ " envil add example Recreate .env.example from env.ts",
1196
1176
  "",
1197
1177
  "Use --help on each subcommand for details.",
1198
1178
  ""
@@ -1245,5 +1225,5 @@ export {
1245
1225
  runCli
1246
1226
  };
1247
1227
 
1248
- //# debugId=B3DBC46CBE56A44864756E2164756E21
1228
+ //# debugId=2598B6F54188F97964756E2164756E21
1249
1229
  //# sourceMappingURL=cli.js.map