@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/index.js CHANGED
@@ -197,6 +197,11 @@ function buildEnv(opts) {
197
197
  return env;
198
198
  }
199
199
  function createEnv(opts) {
200
+ if (process.env.ENVIL_INTROSPECT_ONLY === "1") {
201
+ return new Proxy({}, { get: () => {
202
+ return;
203
+ } });
204
+ }
200
205
  if (opts.resolvers?.length) {
201
206
  const shouldAutoRedact = opts.autoRedactResolver !== false;
202
207
  return Effect.all(opts.resolvers, { concurrency: "unbounded" }).pipe(Effect.map((results) => {
@@ -225,6 +230,577 @@ function createEnv(opts) {
225
230
  }
226
231
  return buildEnv(opts);
227
232
  }
233
+ // src/introspect.ts
234
+ import { Option, Redacted as Redacted2, Schema as Schema4, SchemaAST } from "effect";
235
+
236
+ // src/cli/dotenv-codec.ts
237
+ import { Schema as Schema2 } from "effect";
238
+
239
+ // src/cli/literals.ts
240
+ function toCodeLiteral(value) {
241
+ if (typeof value === "string")
242
+ return JSON.stringify(value);
243
+ if (typeof value === "number" || typeof value === "boolean")
244
+ return String(value);
245
+ if (value === null)
246
+ return "null";
247
+ if (value === undefined)
248
+ return "undefined";
249
+ return JSON.stringify(value);
250
+ }
251
+ function toEnvValueLiteral(value) {
252
+ if (typeof value === "string")
253
+ return value;
254
+ if (typeof value === "number" || typeof value === "boolean")
255
+ return String(value);
256
+ if (value === null || value === undefined)
257
+ return "";
258
+ return JSON.stringify(value);
259
+ }
260
+ function parseBooleanDirective(value, defaultValue) {
261
+ if (value === undefined || value.length === 0) {
262
+ return defaultValue;
263
+ }
264
+ const normalized = value.trim().toLowerCase();
265
+ if (normalized === "true")
266
+ return true;
267
+ if (normalized === "false")
268
+ return false;
269
+ throw new Error(`Invalid boolean directive value "${value}"`);
270
+ }
271
+
272
+ // src/cli/types.ts
273
+ var BUCKETS = ["server", "client", "shared"];
274
+ var SCHEMA_KINDS = [
275
+ "requiredString",
276
+ "boolean",
277
+ "integer",
278
+ "number",
279
+ "port",
280
+ "url",
281
+ "postgresUrl",
282
+ "redisUrl",
283
+ "mongoUrl",
284
+ "mysqlUrl",
285
+ "commaSeparated",
286
+ "commaSeparatedNumbers",
287
+ "commaSeparatedUrls",
288
+ "json",
289
+ "stringEnum"
290
+ ];
291
+ var FRAMEWORKS = ["nextjs", "vite", "expo", "nuxt", "sveltekit", "astro"];
292
+
293
+ // src/cli/dotenv-codec.ts
294
+ var SENTINEL_DIRECTIVE_PREFIX = "@";
295
+ var KEY_ASSIGNMENT_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
296
+ var DotenvCodec = Schema2.transform(Schema2.String, Schema2.Unknown, {
297
+ decode: (text) => parseDotenvText(String(text)),
298
+ encode: (value) => stringifyDotenvDocument(value)
299
+ });
300
+ function decodeDotenvText(text) {
301
+ return Schema2.decodeUnknownSync(DotenvCodec)(text);
302
+ }
303
+ function encodeDotenvText(document) {
304
+ return Schema2.encodeSync(DotenvCodec)(document);
305
+ }
306
+ function parseDotenvText(text) {
307
+ const lines = text.split(/\r?\n/);
308
+ const entries = [];
309
+ const prefix = {};
310
+ let activeBucket;
311
+ let pendingDirectives = {};
312
+ for (const [index, line] of lines.entries()) {
313
+ const lineNumber = index + 1;
314
+ const trimmed = line.trim();
315
+ if (trimmed.length === 0) {
316
+ continue;
317
+ }
318
+ if (trimmed.startsWith("#")) {
319
+ const comment = trimmed.slice(1).trim();
320
+ if (comment.startsWith(SENTINEL_DIRECTIVE_PREFIX)) {
321
+ const directiveResult = parseDirectiveGroup(comment, lineNumber, pendingDirectives);
322
+ if (directiveResult.sectionBucket) {
323
+ activeBucket = directiveResult.sectionBucket;
324
+ if (directiveResult.sectionPrefix) {
325
+ prefix[directiveResult.sectionBucket] = directiveResult.sectionPrefix;
326
+ }
327
+ pendingDirectives = {};
328
+ } else {
329
+ pendingDirectives = directiveResult.directives;
330
+ }
331
+ }
332
+ continue;
333
+ }
334
+ const assignmentMatch = KEY_ASSIGNMENT_PATTERN.exec(trimmed);
335
+ if (!assignmentMatch) {
336
+ throw new Error(`Malformed assignment at line ${lineNumber}: "${line}"`);
337
+ }
338
+ const key = assignmentMatch[1];
339
+ const rightHandSide = assignmentMatch[2];
340
+ const split = splitValueAndInlineComment(rightHandSide);
341
+ const value = normalizeAssignedValue(split.value.trim());
342
+ let directives = { ...pendingDirectives };
343
+ if (split.comment) {
344
+ const inlineComment = split.comment.trim();
345
+ if (inlineComment.startsWith(SENTINEL_DIRECTIVE_PREFIX)) {
346
+ const parsedInline = parseDirectiveGroup(inlineComment, lineNumber, directives);
347
+ if (parsedInline.sectionBucket) {
348
+ throw new Error(`Section directives are not allowed inline at line ${lineNumber}`);
349
+ }
350
+ directives = parsedInline.directives;
351
+ }
352
+ }
353
+ entries.push({
354
+ key,
355
+ value,
356
+ line: lineNumber,
357
+ sectionBucket: activeBucket,
358
+ directives
359
+ });
360
+ pendingDirectives = {};
361
+ }
362
+ const hasPrefix = Object.values(prefix).some((v) => v && v.length > 0);
363
+ return { entries, ...hasPrefix ? { prefix } : {} };
364
+ }
365
+ function stringifyDotenvDocument(document) {
366
+ if (!document || !Array.isArray(document.entries)) {
367
+ throw new Error("Invalid dotenv document: expected an entries array");
368
+ }
369
+ const grouped = {
370
+ server: [],
371
+ client: [],
372
+ shared: []
373
+ };
374
+ const sortedEntries = [...document.entries].sort((left, right) => {
375
+ if (left.line !== right.line)
376
+ return left.line - right.line;
377
+ return left.key.localeCompare(right.key);
378
+ });
379
+ for (const entry of sortedEntries) {
380
+ const bucket = entry.directives.bucket ?? entry.sectionBucket ?? "server";
381
+ grouped[bucket].push(entry);
382
+ }
383
+ const lines = [];
384
+ for (const bucket of BUCKETS) {
385
+ const pfx = document.prefix?.[bucket];
386
+ lines.push(pfx && pfx.length > 0 ? `# @${bucket} ${pfx}` : `# @${bucket}`);
387
+ lines.push("");
388
+ for (const entry of grouped[bucket]) {
389
+ const optional = entry.directives.optional ?? false;
390
+ const redacted = entry.directives.redacted ?? false;
391
+ const noDefault = entry.directives.hasDefault === false;
392
+ if (entry.directives.type === "stringEnum" && entry.directives.stringEnumValues) {
393
+ lines.push(`# @type enum ${entry.directives.stringEnumValues.join(",")}`);
394
+ }
395
+ if (optional)
396
+ lines.push("# @optional");
397
+ if (noDefault)
398
+ lines.push("# @no-default");
399
+ if (redacted)
400
+ lines.push("# @redacted");
401
+ lines.push(`${entry.key}=${serializeEnvValue(entry.value)}`);
402
+ lines.push("");
403
+ }
404
+ }
405
+ while (lines.length > 0 && lines[lines.length - 1] === "") {
406
+ lines.pop();
407
+ }
408
+ return `${lines.join(`
409
+ `)}
410
+ `;
411
+ }
412
+ function splitValueAndInlineComment(value) {
413
+ let inSingleQuote = false;
414
+ let inDoubleQuote = false;
415
+ let escaped = false;
416
+ for (let index = 0;index < value.length; index += 1) {
417
+ const char = value[index];
418
+ if (escaped) {
419
+ escaped = false;
420
+ continue;
421
+ }
422
+ if (char === "\\") {
423
+ escaped = true;
424
+ continue;
425
+ }
426
+ if (char === "'" && !inDoubleQuote) {
427
+ inSingleQuote = !inSingleQuote;
428
+ continue;
429
+ }
430
+ if (char === '"' && !inSingleQuote) {
431
+ inDoubleQuote = !inDoubleQuote;
432
+ continue;
433
+ }
434
+ if (!inSingleQuote && !inDoubleQuote && char === "#") {
435
+ return {
436
+ value: value.slice(0, index).trimEnd(),
437
+ comment: value.slice(index + 1)
438
+ };
439
+ }
440
+ }
441
+ return { value };
442
+ }
443
+ function parseDirectiveGroup(directiveText, lineNumber, base) {
444
+ const directives = { ...base };
445
+ const tokens = directiveText.split(/\s+(?=@)/g).map((token) => token.trim()).filter(Boolean);
446
+ let sectionBucket;
447
+ let sectionPrefix;
448
+ for (const token of tokens) {
449
+ const parsed = parseDirectiveToken(token, lineNumber);
450
+ if ("type" in parsed)
451
+ directives.type = parsed.type;
452
+ if ("optional" in parsed)
453
+ directives.optional = parsed.optional;
454
+ if ("hasDefault" in parsed)
455
+ directives.hasDefault = parsed.hasDefault;
456
+ if ("redacted" in parsed)
457
+ directives.redacted = parsed.redacted;
458
+ if ("bucket" in parsed)
459
+ directives.bucket = parsed.bucket;
460
+ if ("stringEnumValues" in parsed)
461
+ directives.stringEnumValues = parsed.stringEnumValues;
462
+ if ("sectionBucket" in parsed)
463
+ sectionBucket = parsed.sectionBucket;
464
+ if ("sectionPrefix" in parsed)
465
+ sectionPrefix = parsed.sectionPrefix;
466
+ }
467
+ return { directives, sectionBucket, sectionPrefix };
468
+ }
469
+ function parseDirectiveToken(token, lineNumber) {
470
+ if (!token.startsWith("@")) {
471
+ throw new Error(`Malformed directive at line ${lineNumber}: "${token}"`);
472
+ }
473
+ const spaceIndex = token.indexOf(" ");
474
+ const name = (spaceIndex === -1 ? token.slice(1) : token.slice(1, spaceIndex)).trim();
475
+ const value = (spaceIndex === -1 ? "" : token.slice(spaceIndex + 1)).trim();
476
+ if (name === "server" || name === "client" || name === "shared") {
477
+ return value.length > 0 ? { sectionBucket: name, sectionPrefix: value } : { sectionBucket: name };
478
+ }
479
+ if (name === "type") {
480
+ if (value.length === 0) {
481
+ throw new Error(`Directive "@type" requires a value at line ${lineNumber}`);
482
+ }
483
+ if (value === "enum" || value.startsWith("enum ")) {
484
+ const raw = value.slice(4).trim();
485
+ if (raw.length === 0) {
486
+ throw new Error(`Directive "@type enum" requires comma-separated values at line ${lineNumber}`);
487
+ }
488
+ const stringEnumValues = raw.split(",").map((v) => v.trim()).filter(Boolean);
489
+ if (stringEnumValues.length === 0) {
490
+ throw new Error(`Directive "@type enum" requires at least one value at line ${lineNumber}`);
491
+ }
492
+ return { type: "stringEnum", stringEnumValues };
493
+ }
494
+ return { type: parseSchemaKind(value, lineNumber) };
495
+ }
496
+ if (name === "optional") {
497
+ return { optional: parseBooleanDirective(value || undefined, true) };
498
+ }
499
+ if (name === "no-default") {
500
+ return { hasDefault: false };
501
+ }
502
+ if (name === "redacted") {
503
+ return { redacted: parseBooleanDirective(value || undefined, true) };
504
+ }
505
+ if (name === "bucket") {
506
+ if (value.length === 0) {
507
+ throw new Error(`Directive "@bucket" requires a value at line ${lineNumber}`);
508
+ }
509
+ if (!isBucket(value)) {
510
+ throw new Error(`Invalid bucket "${value}" at line ${lineNumber}`);
511
+ }
512
+ return { bucket: value };
513
+ }
514
+ throw new Error(`Unknown directive "@${name}" at line ${lineNumber}`);
515
+ }
516
+ function parseSchemaKind(rawValue, lineNumber) {
517
+ const normalized = rawValue.trim().toLowerCase();
518
+ const aliasMap = {
519
+ string: "requiredString",
520
+ requiredstring: "requiredString",
521
+ bool: "boolean",
522
+ boolean: "boolean",
523
+ int: "integer",
524
+ integer: "integer",
525
+ number: "number",
526
+ port: "port",
527
+ url: "url",
528
+ postgresurl: "postgresUrl",
529
+ redisurl: "redisUrl",
530
+ mongourl: "mongoUrl",
531
+ mysqlurl: "mysqlUrl",
532
+ commaseparated: "commaSeparated",
533
+ commaseparatednumbers: "commaSeparatedNumbers",
534
+ commaseparatedurls: "commaSeparatedUrls",
535
+ json: "json",
536
+ "json(schema.unknown)": "json"
537
+ };
538
+ const resolved = aliasMap[normalized];
539
+ if (!resolved || !SCHEMA_KINDS.includes(resolved)) {
540
+ throw new Error(`Invalid @type value "${rawValue}" at line ${lineNumber}`);
541
+ }
542
+ return resolved;
543
+ }
544
+ function normalizeAssignedValue(value) {
545
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
546
+ return value.slice(1, -1);
547
+ }
548
+ return value;
549
+ }
550
+ function serializeEnvValue(value) {
551
+ const literal = toEnvValueLiteral(value);
552
+ if (literal.length === 0)
553
+ return "";
554
+ if (/[\s#]/.test(literal)) {
555
+ return JSON.stringify(literal);
556
+ }
557
+ return literal;
558
+ }
559
+ function isBucket(value) {
560
+ return BUCKETS.includes(value);
561
+ }
562
+
563
+ // src/schemas.ts
564
+ import { Function, Schema as Schema3 } from "effect";
565
+ var DEFAULT_VALUE_ANNOTATION = Symbol.for("@ayronforge/envil/default-value");
566
+ var SCHEMA_KIND_ANNOTATION = Symbol.for("@ayronforge/envil/schema-kind");
567
+ var PLACEHOLDER_ANNOTATION = Symbol.for("@ayronforge/envil/placeholder");
568
+ var OPTIONAL_ANNOTATION = Symbol.for("@ayronforge/envil/optional");
569
+ var REDACTED_ANNOTATION = Symbol.for("@ayronforge/envil/redacted");
570
+ var STRING_ENUM_VALUES_ANNOTATION = Symbol.for("@ayronforge/envil/string-enum-values");
571
+ var withDefault = Function.dual(2, (schema, defaultValue) => {
572
+ const withDefaultSchema = Schema3.transform(Schema3.UndefinedOr(schema), Schema3.typeSchema(schema), {
573
+ decode: (value) => value ?? defaultValue,
574
+ encode: (value) => value
575
+ });
576
+ return withDefaultSchema.annotations({
577
+ [DEFAULT_VALUE_ANNOTATION]: defaultValue
578
+ });
579
+ });
580
+ var optional = (schema) => Schema3.UndefinedOr(schema).annotations({
581
+ [OPTIONAL_ANNOTATION]: true
582
+ });
583
+ var redacted = (schema) => Schema3.Redacted(schema).annotations({
584
+ [REDACTED_ANNOTATION]: true
585
+ });
586
+ var requiredString = Schema3.String.pipe(Schema3.minLength(1)).annotations({
587
+ identifier: "RequiredString",
588
+ [SCHEMA_KIND_ANNOTATION]: "requiredString",
589
+ [PLACEHOLDER_ANNOTATION]: "CHANGE_ME"
590
+ });
591
+ var boolean = Schema3.transform(Schema3.String.pipe(Schema3.filter((s) => ["true", "false", "1", "0"].includes(s.toLowerCase()), {
592
+ identifier: "BooleanString",
593
+ message: () => "Expected 'true', 'false', '1', or '0'"
594
+ })), Schema3.Boolean, {
595
+ decode: (s) => s.toLowerCase() === "true" || s === "1",
596
+ encode: (b) => b ? "true" : "false"
597
+ }).annotations({
598
+ [SCHEMA_KIND_ANNOTATION]: "boolean",
599
+ [PLACEHOLDER_ANNOTATION]: "true"
600
+ });
601
+ var integer = Schema3.NumberFromString.pipe(Schema3.int()).annotations({
602
+ identifier: "Integer",
603
+ [SCHEMA_KIND_ANNOTATION]: "integer",
604
+ [PLACEHOLDER_ANNOTATION]: "123"
605
+ });
606
+ var number = Schema3.NumberFromString.annotations({
607
+ identifier: "Number",
608
+ [SCHEMA_KIND_ANNOTATION]: "number",
609
+ [PLACEHOLDER_ANNOTATION]: "3.14"
610
+ });
611
+ var positiveNumber = Schema3.NumberFromString.pipe(Schema3.positive()).annotations({
612
+ identifier: "PositiveNumber"
613
+ });
614
+ var nonNegativeNumber = Schema3.NumberFromString.pipe(Schema3.nonNegative()).annotations({
615
+ identifier: "NonNegativeNumber"
616
+ });
617
+ var port = Schema3.NumberFromString.pipe(Schema3.int(), Schema3.between(1, 65535)).annotations({
618
+ identifier: "Port",
619
+ [SCHEMA_KIND_ANNOTATION]: "port",
620
+ [PLACEHOLDER_ANNOTATION]: "3000"
621
+ });
622
+ var url = Schema3.String.pipe(Schema3.filter((s) => {
623
+ try {
624
+ new URL(s);
625
+ return s.startsWith("http://") || s.startsWith("https://");
626
+ } catch {
627
+ return false;
628
+ }
629
+ }, { identifier: "Url", message: () => "Expected a valid HTTP or HTTPS URL" })).annotations({
630
+ [SCHEMA_KIND_ANNOTATION]: "url",
631
+ [PLACEHOLDER_ANNOTATION]: "https://example.com"
632
+ });
633
+ var postgresUrl = Schema3.String.pipe(Schema3.filter((s) => s.startsWith("postgres://") || s.startsWith("postgresql://"), {
634
+ identifier: "PostgresUrl",
635
+ message: () => "Expected a valid PostgreSQL connection URL"
636
+ }), Schema3.pattern(/^(postgres|postgresql):\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/)).annotations({
637
+ [SCHEMA_KIND_ANNOTATION]: "postgresUrl",
638
+ [PLACEHOLDER_ANNOTATION]: "postgres://user:pass@localhost:5432/app"
639
+ });
640
+ var redisUrl = Schema3.String.pipe(Schema3.filter((s) => s.startsWith("redis://") || s.startsWith("rediss://"), {
641
+ identifier: "RedisUrl",
642
+ message: () => "Expected a valid Redis connection URL"
643
+ }), Schema3.pattern(/^rediss?:\/\/(?:[^:]+:[^@]+@)?[^:]+(?::\d+)?(?:\/\d+)?$/)).annotations({
644
+ [SCHEMA_KIND_ANNOTATION]: "redisUrl",
645
+ [PLACEHOLDER_ANNOTATION]: "redis://localhost:6379"
646
+ });
647
+ var mongoUrl = Schema3.String.pipe(Schema3.filter((s) => s.startsWith("mongodb://") || s.startsWith("mongodb+srv://"), {
648
+ identifier: "MongoUrl",
649
+ message: () => "Expected a valid MongoDB connection URL"
650
+ }), Schema3.pattern(/^mongodb(\+srv)?:\/\/(?:[^:]+:[^@]+@)?[^/]+(?:\/[^?]*)?(?:\?.*)?$/)).annotations({
651
+ [SCHEMA_KIND_ANNOTATION]: "mongoUrl",
652
+ [PLACEHOLDER_ANNOTATION]: "mongodb://localhost:27017/app"
653
+ });
654
+ var mysqlUrl = Schema3.String.pipe(Schema3.filter((s) => s.startsWith("mysql://") || s.startsWith("mysqls://"), {
655
+ identifier: "MysqlUrl",
656
+ message: () => "Expected a valid MySQL connection URL"
657
+ }), Schema3.pattern(/^mysqls?:\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/)).annotations({
658
+ [SCHEMA_KIND_ANNOTATION]: "mysqlUrl",
659
+ [PLACEHOLDER_ANNOTATION]: "mysql://user:pass@localhost:3306/app"
660
+ });
661
+ var commaSeparated = Schema3.transform(Schema3.String, Schema3.mutable(Schema3.Array(Schema3.String)), {
662
+ decode: (s) => s.split(",").map((x) => x.trim()),
663
+ encode: (a) => a.join(",")
664
+ }).annotations({
665
+ [SCHEMA_KIND_ANNOTATION]: "commaSeparated",
666
+ [PLACEHOLDER_ANNOTATION]: "alpha,beta,gamma"
667
+ });
668
+ var commaSeparatedNumbers = Schema3.transform(Schema3.String, Schema3.mutable(Schema3.Array(Schema3.Number)), {
669
+ decode: (s) => s.split(",").map((x) => {
670
+ const n = Number(x.trim());
671
+ if (Number.isNaN(n))
672
+ throw new Error(`"${x.trim()}" is not a valid number`);
673
+ return n;
674
+ }),
675
+ encode: (a) => a.join(",")
676
+ }).annotations({
677
+ [SCHEMA_KIND_ANNOTATION]: "commaSeparatedNumbers",
678
+ [PLACEHOLDER_ANNOTATION]: "1,2,3"
679
+ });
680
+ var commaSeparatedUrls = Schema3.transform(Schema3.String, Schema3.mutable(Schema3.Array(url)), {
681
+ decode: (s) => s.split(",").map((x) => Schema3.decodeUnknownSync(url)(x.trim())),
682
+ encode: (a) => a.join(",")
683
+ }).annotations({
684
+ [SCHEMA_KIND_ANNOTATION]: "commaSeparatedUrls",
685
+ [PLACEHOLDER_ANNOTATION]: "https://one.example.com,https://two.example.com"
686
+ });
687
+ var stringEnum = (values) => Schema3.Literal(...values).annotations({
688
+ [SCHEMA_KIND_ANNOTATION]: "stringEnum",
689
+ [STRING_ENUM_VALUES_ANNOTATION]: values,
690
+ [PLACEHOLDER_ANNOTATION]: values[0]
691
+ });
692
+ var json = (schema) => Schema3.parseJson(schema).annotations({
693
+ [SCHEMA_KIND_ANNOTATION]: "json",
694
+ [PLACEHOLDER_ANNOTATION]: '{"key":"value"}'
695
+ });
696
+
697
+ // src/introspect.ts
698
+ var BUCKETS2 = ["server", "client", "shared"];
699
+ function getAnn(ast, key) {
700
+ return Option.getOrUndefined(SchemaAST.getAnnotation(ast, key));
701
+ }
702
+ function examineSchema(schema) {
703
+ let ast = schema.ast;
704
+ let isRedacted = false;
705
+ let isOptional = false;
706
+ let hasDefault = false;
707
+ let defaultValue = undefined;
708
+ if (SchemaAST.isTransformation(ast) && getAnn(ast, REDACTED_ANNOTATION)) {
709
+ isRedacted = true;
710
+ ast = ast.from;
711
+ }
712
+ if (SchemaAST.isTransformation(ast) && getAnn(ast, DEFAULT_VALUE_ANNOTATION) !== undefined) {
713
+ hasDefault = true;
714
+ defaultValue = getAnn(ast, DEFAULT_VALUE_ANNOTATION);
715
+ const fromAst = ast.from;
716
+ if (SchemaAST.isUnion(fromAst)) {
717
+ const nonUndefined = fromAst.types.find((t) => !SchemaAST.isUndefinedKeyword(t));
718
+ if (nonUndefined)
719
+ ast = nonUndefined;
720
+ }
721
+ }
722
+ if (SchemaAST.isUnion(ast) && getAnn(ast, OPTIONAL_ANNOTATION)) {
723
+ isOptional = true;
724
+ const nonUndefined = ast.types.find((t) => !SchemaAST.isUndefinedKeyword(t));
725
+ if (nonUndefined)
726
+ ast = nonUndefined;
727
+ }
728
+ const kind = getAnn(ast, SCHEMA_KIND_ANNOTATION);
729
+ const placeholder = getAnn(ast, PLACEHOLDER_ANNOTATION);
730
+ const stringEnumValues = kind === "stringEnum" ? getAnn(ast, STRING_ENUM_VALUES_ANNOTATION) : undefined;
731
+ return {
732
+ kind,
733
+ placeholder,
734
+ optional: isOptional,
735
+ hasDefault,
736
+ defaultValue,
737
+ redacted: isRedacted,
738
+ stringEnumValues
739
+ };
740
+ }
741
+ function buildEnvExample(definition) {
742
+ const prefix = {
743
+ server: definition.prefix?.server ?? "",
744
+ client: definition.prefix?.client ?? "",
745
+ shared: definition.prefix?.shared ?? ""
746
+ };
747
+ const entries = [];
748
+ let line = 1;
749
+ for (const bucket of BUCKETS2) {
750
+ const schemas = definition[bucket] ?? {};
751
+ const keys = Object.keys(schemas).sort();
752
+ for (const key of keys) {
753
+ const schema = schemas[key];
754
+ const examined = examineSchema(schema);
755
+ const runtimeKey = `${prefix[bucket]}${key}`;
756
+ let value;
757
+ if (examined.hasDefault) {
758
+ value = encodeDefault(schema, examined);
759
+ } else {
760
+ value = examined.placeholder ?? "CHANGE_ME";
761
+ }
762
+ entries.push({
763
+ key: runtimeKey,
764
+ value,
765
+ line,
766
+ sectionBucket: bucket,
767
+ directives: {
768
+ type: examined.kind,
769
+ bucket,
770
+ optional: examined.optional,
771
+ hasDefault: examined.hasDefault,
772
+ redacted: examined.redacted,
773
+ stringEnumValues: examined.stringEnumValues
774
+ }
775
+ });
776
+ line += 1;
777
+ }
778
+ }
779
+ const hasPrefix = Object.values(prefix).some((v) => v.length > 0);
780
+ return encodeDotenvText({ entries, ...hasPrefix ? { prefix } : {} });
781
+ }
782
+ function encodeDefault(schema, examined) {
783
+ try {
784
+ let valueToEncode = examined.defaultValue;
785
+ if (examined.redacted) {
786
+ valueToEncode = Redacted2.make(valueToEncode);
787
+ }
788
+ const encoded = Schema4.encodeSync(schema)(valueToEncode);
789
+ return String(encoded ?? "");
790
+ } catch {
791
+ return stringifyDefault(examined);
792
+ }
793
+ }
794
+ function stringifyDefault(examined) {
795
+ const val = examined.defaultValue;
796
+ if (val === undefined || val === null)
797
+ return "";
798
+ if (Array.isArray(val))
799
+ return val.map(String).join(",");
800
+ if (typeof val === "object")
801
+ return JSON.stringify(val);
802
+ return String(val);
803
+ }
228
804
  // src/resolvers/remote.ts
229
805
  import { Effect as Effect3 } from "effect";
230
806
 
@@ -332,84 +908,6 @@ function safeCreateEnv(opts) {
332
908
  return { success: false, error: normalizeEnvValidationError(error) };
333
909
  }
334
910
  }
335
- // src/schemas.ts
336
- import { Function, Schema as Schema2 } from "effect";
337
- var DEFAULT_VALUE_ANNOTATION = Symbol.for("@ayronforge/envil/default-value");
338
- var withDefault = Function.dual(2, (schema, defaultValue) => {
339
- const withDefaultSchema = Schema2.transform(Schema2.UndefinedOr(schema), Schema2.typeSchema(schema), {
340
- decode: (value) => value ?? defaultValue,
341
- encode: (value) => value
342
- });
343
- return withDefaultSchema.annotations({
344
- [DEFAULT_VALUE_ANNOTATION]: defaultValue
345
- });
346
- });
347
- var optional = (schema) => Schema2.UndefinedOr(schema);
348
- var redacted = (schema) => Schema2.Redacted(schema);
349
- var requiredString = Schema2.String.pipe(Schema2.minLength(1)).annotations({
350
- identifier: "RequiredString"
351
- });
352
- var optionalString = Schema2.UndefinedOr(Schema2.String);
353
- var positiveNumber = Schema2.NumberFromString.pipe(Schema2.positive()).annotations({
354
- identifier: "PositiveNumber"
355
- });
356
- var commaSeparated = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(Schema2.String)), {
357
- decode: (s) => s.split(",").map((x) => x.trim()),
358
- encode: (a) => a.join(",")
359
- });
360
- var commaSeparatedNumbers = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(Schema2.Number)), {
361
- decode: (s) => s.split(",").map((x) => {
362
- const n = Number(x.trim());
363
- if (Number.isNaN(n))
364
- throw new Error(`"${x.trim()}" is not a valid number`);
365
- return n;
366
- }),
367
- encode: (a) => a.join(",")
368
- });
369
- var url = Schema2.String.pipe(Schema2.filter((s) => {
370
- try {
371
- new URL(s);
372
- return s.startsWith("http://") || s.startsWith("https://");
373
- } catch {
374
- return false;
375
- }
376
- }, { identifier: "Url", message: () => "Expected a valid HTTP or HTTPS URL" }));
377
- var postgresUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("postgres://") || s.startsWith("postgresql://"), {
378
- identifier: "PostgresUrl",
379
- message: () => "Expected a valid PostgreSQL connection URL"
380
- }), Schema2.pattern(/^(postgres|postgresql):\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/));
381
- var redisUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("redis://") || s.startsWith("rediss://"), {
382
- identifier: "RedisUrl",
383
- message: () => "Expected a valid Redis connection URL"
384
- }), Schema2.pattern(/^rediss?:\/\/(?:[^:]+:[^@]+@)?[^:]+(?::\d+)?(?:\/\d+)?$/));
385
- var commaSeparatedUrls = Schema2.transform(Schema2.String, Schema2.mutable(Schema2.Array(url)), {
386
- decode: (s) => s.split(",").map((x) => Schema2.decodeUnknownSync(url)(x.trim())),
387
- encode: (a) => a.join(",")
388
- });
389
- var boolean = Schema2.transform(Schema2.String.pipe(Schema2.filter((s) => ["true", "false", "1", "0"].includes(s.toLowerCase()), {
390
- identifier: "BooleanString",
391
- message: () => "Expected 'true', 'false', '1', or '0'"
392
- })), Schema2.Boolean, {
393
- decode: (s) => s.toLowerCase() === "true" || s === "1",
394
- encode: (b) => b ? "true" : "false"
395
- });
396
- var integer = Schema2.NumberFromString.pipe(Schema2.int()).annotations({
397
- identifier: "Integer"
398
- });
399
- var nonNegativeNumber = Schema2.NumberFromString.pipe(Schema2.nonNegative()).annotations({
400
- identifier: "NonNegativeNumber"
401
- });
402
- var port = Schema2.NumberFromString.pipe(Schema2.int(), Schema2.between(1, 65535)).annotations({ identifier: "Port" });
403
- var stringEnum = (values) => Schema2.Literal(...values);
404
- var json = (schema) => Schema2.parseJson(schema);
405
- var mongoUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("mongodb://") || s.startsWith("mongodb+srv://"), {
406
- identifier: "MongoUrl",
407
- message: () => "Expected a valid MongoDB connection URL"
408
- }), Schema2.pattern(/^mongodb(\+srv)?:\/\/(?:[^:]+:[^@]+@)?[^/]+(?:\/[^?]*)?(?:\?.*)?$/));
409
- var mysqlUrl = Schema2.String.pipe(Schema2.filter((s) => s.startsWith("mysql://") || s.startsWith("mysqls://"), {
410
- identifier: "MysqlUrl",
411
- message: () => "Expected a valid MySQL connection URL"
412
- }), Schema2.pattern(/^mysqls?:\/\/[^:]+:[^@]+@[^:]+:\d+\/.+$/));
413
911
  export {
414
912
  withDefault,
415
913
  url,
@@ -421,23 +919,30 @@ export {
421
919
  postgresUrl,
422
920
  positiveNumber,
423
921
  port,
424
- optionalString,
425
922
  optional,
923
+ number,
426
924
  nonNegativeNumber,
427
925
  mysqlUrl,
428
926
  mongoUrl,
429
927
  json,
430
928
  integer,
431
929
  fromRemoteSecrets,
930
+ examineSchema,
432
931
  createEnv,
433
932
  commaSeparatedUrls,
434
933
  commaSeparatedNumbers,
435
934
  commaSeparated,
935
+ buildEnvExample,
436
936
  boolean,
937
+ STRING_ENUM_VALUES_ANNOTATION,
938
+ SCHEMA_KIND_ANNOTATION,
939
+ REDACTED_ANNOTATION,
940
+ PLACEHOLDER_ANNOTATION,
941
+ OPTIONAL_ANNOTATION,
437
942
  EnvValidationError,
438
943
  DEFAULT_VALUE_ANNOTATION,
439
944
  ClientAccessError
440
945
  };
441
946
 
442
- //# debugId=83E78DCC4BD19C8E64756E2164756E21
947
+ //# debugId=D41C1808EDE7815C64756E2164756E21
443
948
  //# sourceMappingURL=index.js.map