node-safe-env 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1019 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // src/cli/index.ts
5
+ import { pathToFileURL as pathToFileURL2 } from "url";
6
+
7
+ // src/cli/utils/parseArgs.ts
8
+ function parseArgs(argv) {
9
+ const [command, ...rest] = argv;
10
+ const flags = {};
11
+ const positionals = [];
12
+ for (let index = 0; index < rest.length; index += 1) {
13
+ const token = rest[index];
14
+ if (!token.startsWith("--")) {
15
+ positionals.push(token);
16
+ continue;
17
+ }
18
+ const flagName = token.slice(2);
19
+ const next = rest[index + 1];
20
+ if (!next || next.startsWith("--")) {
21
+ flags[flagName] = true;
22
+ continue;
23
+ }
24
+ flags[flagName] = next;
25
+ index += 1;
26
+ }
27
+ return {
28
+ command,
29
+ flags,
30
+ positionals
31
+ };
32
+ }
33
+
34
+ // src/errors/EnvValidationError.ts
35
+ var EnvValidationError = class extends Error {
36
+ constructor(issues) {
37
+ super(
38
+ [
39
+ "Environment validation failed:",
40
+ ...issues.map((issue) => `- [${issue.code}] ${issue.message}`)
41
+ ].join("\n")
42
+ );
43
+ this.name = "EnvValidationError";
44
+ this.issues = issues;
45
+ }
46
+ };
47
+
48
+ // src/createEnv.ts
49
+ import path2 from "path";
50
+
51
+ // src/flattenSchema.ts
52
+ function isEnvRule(value) {
53
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
54
+ }
55
+ function toEnvKey(path4) {
56
+ return path4.map((segment) => segment.toUpperCase()).join("_");
57
+ }
58
+ function flattenSchema(schema, parentPath = []) {
59
+ const entries = [];
60
+ for (const [key, value] of Object.entries(schema)) {
61
+ const currentPath = [...parentPath, key];
62
+ if (isEnvRule(value)) {
63
+ entries.push({
64
+ path: currentPath,
65
+ envKey: toEnvKey(currentPath),
66
+ rule: value
67
+ });
68
+ continue;
69
+ }
70
+ if (typeof value === "object" && value !== null) {
71
+ entries.push(
72
+ ...flattenSchema(value, currentPath)
73
+ );
74
+ }
75
+ }
76
+ return entries;
77
+ }
78
+
79
+ // src/findUnknownEnvKeys.ts
80
+ function findUnknownEnvKeys(schema, source) {
81
+ const knownKeys = new Set(flattenSchema(schema).map((entry) => entry.envKey));
82
+ const issues = [];
83
+ for (const key of Object.keys(source)) {
84
+ if (source[key] === void 0) {
85
+ continue;
86
+ }
87
+ if (knownKeys.has(key)) {
88
+ continue;
89
+ }
90
+ issues.push({
91
+ key,
92
+ code: "unknown_key",
93
+ message: `Environment variable "${key}" is not defined in the schema.`
94
+ });
95
+ }
96
+ return issues;
97
+ }
98
+
99
+ // src/loadEnvFiles.ts
100
+ import fs from "fs";
101
+ import path from "path";
102
+ var ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
103
+ function stripInlineComment(input) {
104
+ let inSingleQuote = false;
105
+ let inDoubleQuote = false;
106
+ let isEscaped = false;
107
+ for (let index = 0; index < input.length; index += 1) {
108
+ const char = input[index];
109
+ if (isEscaped) {
110
+ isEscaped = false;
111
+ continue;
112
+ }
113
+ if (char === "\\") {
114
+ isEscaped = true;
115
+ continue;
116
+ }
117
+ if (char === '"' && !inSingleQuote) {
118
+ inDoubleQuote = !inDoubleQuote;
119
+ continue;
120
+ }
121
+ if (char === "'" && !inDoubleQuote) {
122
+ inSingleQuote = !inSingleQuote;
123
+ continue;
124
+ }
125
+ if (char === "#" && !inSingleQuote && !inDoubleQuote && (index === 0 || /\s/.test(input[index - 1] ?? ""))) {
126
+ return input.slice(0, index).trimEnd();
127
+ }
128
+ }
129
+ return input.trimEnd();
130
+ }
131
+ function parseEnvFile(filePath) {
132
+ if (!fs.existsSync(filePath)) {
133
+ return /* @__PURE__ */ Object.create(null);
134
+ }
135
+ const content = fs.readFileSync(filePath, "utf8");
136
+ const result = /* @__PURE__ */ Object.create(null);
137
+ for (const rawLine of content.split(/\r?\n/)) {
138
+ let line = rawLine.trim();
139
+ if (!line || line.startsWith("#")) {
140
+ continue;
141
+ }
142
+ if (line.startsWith("export ")) {
143
+ line = line.slice("export ".length).trim();
144
+ if (!line) {
145
+ continue;
146
+ }
147
+ }
148
+ const equalIndex = line.indexOf("=");
149
+ if (equalIndex <= 0) {
150
+ continue;
151
+ }
152
+ const key = line.slice(0, equalIndex).trim();
153
+ if (!ENV_KEY_PATTERN.test(key)) {
154
+ continue;
155
+ }
156
+ let value = stripInlineComment(line.slice(equalIndex + 1)).trim();
157
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
158
+ value = value.slice(1, -1);
159
+ }
160
+ result[key] = value;
161
+ }
162
+ return result;
163
+ }
164
+ function loadEnvFiles(options = {}) {
165
+ const cwd = options.cwd ?? process.cwd();
166
+ const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV ?? "development";
167
+ const base = parseEnvFile(path.join(cwd, ".env"));
168
+ const local = parseEnvFile(path.join(cwd, ".env.local"));
169
+ const environment = parseEnvFile(path.join(cwd, `.env.${nodeEnv}`));
170
+ const custom = options.envFile ? parseEnvFile(path.resolve(cwd, options.envFile)) : /* @__PURE__ */ Object.create(null);
171
+ return {
172
+ base,
173
+ local,
174
+ environment,
175
+ custom
176
+ };
177
+ }
178
+
179
+ // src/mergeSources.ts
180
+ function mergeSources(files, runtimeValues = process.env) {
181
+ return Object.assign(
182
+ /* @__PURE__ */ Object.create(null),
183
+ files.base,
184
+ files.local,
185
+ files.environment,
186
+ files.custom,
187
+ runtimeValues
188
+ );
189
+ }
190
+
191
+ // src/applyTransform.ts
192
+ function applyTransform(key, rule, result) {
193
+ if (result.issue || result.value === void 0 || !rule.transform) {
194
+ return result;
195
+ }
196
+ try {
197
+ return {
198
+ value: rule.transform(result.value)
199
+ };
200
+ } catch (error) {
201
+ const message = error instanceof Error && error.message ? error.message : `Environment variable "${key}" failed transform.`;
202
+ return {
203
+ issue: {
204
+ key,
205
+ code: "invalid_custom",
206
+ message
207
+ }
208
+ };
209
+ }
210
+ }
211
+
212
+ // src/validators/boolean.ts
213
+ var TRUE_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
214
+ var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "0", "no", "off"]);
215
+ var validateBoolean = ({ key, rawValue }) => {
216
+ const normalized = rawValue.trim().toLowerCase();
217
+ if (TRUE_VALUES.has(normalized)) {
218
+ return { value: true };
219
+ }
220
+ if (FALSE_VALUES.has(normalized)) {
221
+ return { value: false };
222
+ }
223
+ return {
224
+ issue: {
225
+ key,
226
+ code: "invalid_boolean",
227
+ message: `Environment variable "${key}" must be a valid boolean.`
228
+ }
229
+ };
230
+ };
231
+
232
+ // src/validators/enum.ts
233
+ var validateEnum = ({ key, rawValue, rule }) => {
234
+ const enumRule = rule;
235
+ if (!enumRule.values.includes(rawValue)) {
236
+ return {
237
+ issue: {
238
+ key,
239
+ code: "invalid_enum",
240
+ message: `Environment variable "${key}" must be one of: ${enumRule.values.join(", ")}.`
241
+ }
242
+ };
243
+ }
244
+ return { value: rawValue };
245
+ };
246
+
247
+ // src/validators/json.ts
248
+ var validateJson = ({ key, rawValue }) => {
249
+ try {
250
+ const parsed = JSON.parse(rawValue);
251
+ return { value: parsed };
252
+ } catch {
253
+ return {
254
+ issue: {
255
+ key,
256
+ code: "invalid_json",
257
+ message: `Environment variable "${key}" must contain valid JSON.`
258
+ }
259
+ };
260
+ }
261
+ };
262
+
263
+ // src/validators/number.ts
264
+ var validateNumber = ({ key, rawValue }) => {
265
+ const parsed = Number(rawValue);
266
+ if (!Number.isFinite(parsed)) {
267
+ return {
268
+ issue: {
269
+ key,
270
+ code: "invalid_number",
271
+ message: `Environment variable "${key}" must be a valid number.`
272
+ }
273
+ };
274
+ }
275
+ return { value: parsed };
276
+ };
277
+
278
+ // src/validators/port.ts
279
+ var validatePort = ({ key, rawValue }) => {
280
+ const num = Number(rawValue);
281
+ if (!Number.isInteger(num) || num < 1 || num > 65535) {
282
+ return {
283
+ issue: {
284
+ key,
285
+ code: "invalid_port",
286
+ message: `Environment variable "${key}" must be a valid port (1\u201365535).`
287
+ }
288
+ };
289
+ }
290
+ return { value: num };
291
+ };
292
+
293
+ // src/validators/string.ts
294
+ var validateString = ({ rawValue }) => {
295
+ return { value: rawValue };
296
+ };
297
+
298
+ // src/validators/url.ts
299
+ var validateUrl = ({ key, rawValue }) => {
300
+ try {
301
+ new URL(rawValue);
302
+ return { value: rawValue };
303
+ } catch {
304
+ return {
305
+ issue: {
306
+ key,
307
+ code: "invalid_url",
308
+ message: `Environment variable "${key}" must be a valid URL.`
309
+ }
310
+ };
311
+ }
312
+ };
313
+
314
+ // src/validators/int.ts
315
+ var validateInt = ({ key, rawValue }) => {
316
+ const parsed = Number(rawValue);
317
+ if (!Number.isInteger(parsed)) {
318
+ return {
319
+ issue: {
320
+ key,
321
+ code: "invalid_number",
322
+ message: `Environment variable "${key}" must be a valid integer.`
323
+ }
324
+ };
325
+ }
326
+ return { value: parsed };
327
+ };
328
+
329
+ // src/validators/float.ts
330
+ var validateFloat = ({ key, rawValue }) => {
331
+ const parsed = Number(rawValue);
332
+ if (!Number.isFinite(parsed)) {
333
+ return {
334
+ issue: {
335
+ key,
336
+ code: "invalid_number",
337
+ message: `Environment variable "${key}" must be a valid float.`
338
+ }
339
+ };
340
+ }
341
+ return { value: parsed };
342
+ };
343
+
344
+ // src/validators/array.ts
345
+ var validateArray = ({ key, rawValue, rule }) => {
346
+ const arrayRule = rule;
347
+ const separator = arrayRule.separator ?? ",";
348
+ const trimItems = arrayRule.trimItems ?? true;
349
+ const allowEmptyItems = arrayRule.allowEmptyItems ?? false;
350
+ const splitValues = rawValue.split(separator);
351
+ const parsed = trimItems ? splitValues.map((item) => item.trim()) : splitValues;
352
+ if (!allowEmptyItems && parsed.some((item) => item === "")) {
353
+ const issue = {
354
+ key,
355
+ code: "invalid_array",
356
+ message: `Environment variable "${key}" cannot contain empty array items`
357
+ };
358
+ return { issue };
359
+ }
360
+ return { value: parsed };
361
+ };
362
+
363
+ // src/validators/custom.ts
364
+ var validateCustom = ({ key, rawValue, rule }) => {
365
+ const customRule = rule;
366
+ try {
367
+ const parsed = customRule.parse(rawValue);
368
+ return { value: parsed };
369
+ } catch (error) {
370
+ const message = error instanceof Error && error.message ? error.message : `Environment variable "${key}" failed custom validation.`;
371
+ return {
372
+ issue: {
373
+ key,
374
+ code: "invalid_custom",
375
+ message
376
+ }
377
+ };
378
+ }
379
+ };
380
+
381
+ // src/validators/email.ts
382
+ var validateEmail = ({ key, rawValue, rule }) => {
383
+ const emailRule = rule;
384
+ void emailRule;
385
+ const value = rawValue.trim();
386
+ if (!value) {
387
+ const issue = {
388
+ key,
389
+ code: "invalid_email",
390
+ message: `Environment variable "${key}" must be a valid email address`
391
+ };
392
+ return { issue };
393
+ }
394
+ if (/\s/.test(value)) {
395
+ const issue = {
396
+ key,
397
+ code: "invalid_email",
398
+ message: `Environment variable "${key}" must be a valid email address`
399
+ };
400
+ return { issue };
401
+ }
402
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
403
+ if (!emailRegex.test(value)) {
404
+ const issue = {
405
+ key,
406
+ code: "invalid_email",
407
+ message: `Environment variable "${key}" must be a valid email address`
408
+ };
409
+ return { issue };
410
+ }
411
+ return {
412
+ value
413
+ };
414
+ };
415
+
416
+ // src/validators/date.ts
417
+ function isValidDate(date) {
418
+ return !Number.isNaN(date.getTime());
419
+ }
420
+ var validateDate = ({ key, rawValue, rule }) => {
421
+ const dateRule = rule;
422
+ void dateRule;
423
+ const value = rawValue.trim();
424
+ if (!value) {
425
+ const issue = {
426
+ key,
427
+ code: "invalid_date",
428
+ message: `Environment variable "${key}" must be a valid ISO date`
429
+ };
430
+ return { issue };
431
+ }
432
+ const isoDateRegex = /^\d{4}-\d{2}-\d{2}$/;
433
+ const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})$/;
434
+ if (!isoDateRegex.test(value) && !isoDateTimeRegex.test(value)) {
435
+ const issue = {
436
+ key,
437
+ code: "invalid_date",
438
+ message: `Environment variable "${key}" must be a valid ISO date string`
439
+ };
440
+ return { issue };
441
+ }
442
+ const parsed = new Date(value);
443
+ if (!isValidDate(parsed)) {
444
+ const issue = {
445
+ key,
446
+ code: "invalid_date",
447
+ message: `Environment variable "${key}" must be a valid date`
448
+ };
449
+ return { issue };
450
+ }
451
+ if (isoDateRegex.test(value)) {
452
+ const [year, month, day] = value.split("-").map(Number);
453
+ if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() + 1 !== month || parsed.getUTCDate() !== day) {
454
+ const issue = {
455
+ key,
456
+ code: "invalid_date",
457
+ message: `Environment variable "${key}" must be a real calendar date`
458
+ };
459
+ return { issue };
460
+ }
461
+ }
462
+ return {
463
+ value: parsed
464
+ };
465
+ };
466
+
467
+ // src/validators/index.ts
468
+ var validators = {
469
+ string: validateString,
470
+ number: validateNumber,
471
+ boolean: validateBoolean,
472
+ enum: validateEnum,
473
+ url: validateUrl,
474
+ port: validatePort,
475
+ json: validateJson,
476
+ int: validateInt,
477
+ float: validateFloat,
478
+ array: validateArray,
479
+ custom: validateCustom,
480
+ email: validateEmail,
481
+ date: validateDate
482
+ };
483
+
484
+ // src/parseValue.ts
485
+ function parseValue(key, rawValue, rule) {
486
+ const validator = validators[rule.type];
487
+ const result = validator({
488
+ key,
489
+ rawValue,
490
+ rule
491
+ });
492
+ return applyTransform(key, rule, result);
493
+ }
494
+
495
+ // src/setNestedValue.ts
496
+ function setNestedValue(target, path4, value) {
497
+ let current = target;
498
+ for (let index = 0; index < path4.length - 1; index += 1) {
499
+ const segment = path4[index];
500
+ const existing = current[segment];
501
+ if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
502
+ current[segment] = {};
503
+ }
504
+ current = current[segment];
505
+ }
506
+ current[path4[path4.length - 1]] = value;
507
+ }
508
+
509
+ // src/createEnv.ts
510
+ function isEmptyString(value) {
511
+ return value.trim() === "";
512
+ }
513
+ function defaultToRawValue(value) {
514
+ if (typeof value === "string") {
515
+ return value;
516
+ }
517
+ if (typeof value === "number" || typeof value === "boolean") {
518
+ return String(value);
519
+ }
520
+ if (value instanceof Date) {
521
+ return value.toISOString();
522
+ }
523
+ return JSON.stringify(value);
524
+ }
525
+ function resolveDefaultValue(value) {
526
+ return typeof value === "function" ? value() : value;
527
+ }
528
+ function parseDefaultValue(envKey, rule) {
529
+ const defaultKind = typeof rule.default === "function" ? "function" : "static";
530
+ let resolvedDefault;
531
+ try {
532
+ resolvedDefault = resolveDefaultValue(rule.default);
533
+ } catch (err) {
534
+ const message = err instanceof Error ? err.message : String(err);
535
+ return {
536
+ issue: {
537
+ key: envKey,
538
+ code: "invalid_default",
539
+ message: `Default function for "${envKey}" threw an error: ${message}`
540
+ },
541
+ defaultKind
542
+ };
543
+ }
544
+ const rawDefault = defaultToRawValue(resolvedDefault);
545
+ return {
546
+ ...parseValue(envKey, rawDefault, rule),
547
+ defaultKind,
548
+ rawDefault
549
+ };
550
+ }
551
+ function countKeys(source) {
552
+ return Object.values(source).filter((value) => typeof value === "string").length;
553
+ }
554
+ function buildLoadedFileReport(loadedFiles, cwd, nodeEnv, envFile) {
555
+ return [
556
+ {
557
+ source: ".env",
558
+ path: path2.join(cwd, ".env"),
559
+ keyCount: countKeys(loadedFiles.base)
560
+ },
561
+ {
562
+ source: ".env.local",
563
+ path: path2.join(cwd, ".env.local"),
564
+ keyCount: countKeys(loadedFiles.local)
565
+ },
566
+ {
567
+ source: ".env.environment",
568
+ path: path2.join(cwd, `.env.${nodeEnv}`),
569
+ keyCount: countKeys(loadedFiles.environment)
570
+ },
571
+ {
572
+ source: "custom",
573
+ path: envFile ? path2.resolve(cwd, envFile) : void 0,
574
+ keyCount: countKeys(loadedFiles.custom)
575
+ }
576
+ ];
577
+ }
578
+ function buildSourceTrace(loadedFiles, runtimeValues) {
579
+ const trace = /* @__PURE__ */ Object.create(null);
580
+ const applySource = (source, label) => {
581
+ for (const [key, raw] of Object.entries(source)) {
582
+ if (typeof raw === "string") {
583
+ trace[key] = { source: label, raw };
584
+ }
585
+ }
586
+ };
587
+ applySource(loadedFiles.base, ".env");
588
+ applySource(loadedFiles.local, ".env.local");
589
+ applySource(loadedFiles.environment, ".env.environment");
590
+ applySource(loadedFiles.custom, "custom");
591
+ applySource(runtimeValues, "process.env");
592
+ return trace;
593
+ }
594
+ function maskDebugValue(value, sensitive) {
595
+ if (value === void 0) {
596
+ return void 0;
597
+ }
598
+ return sensitive ? "***" : value;
599
+ }
600
+ function resolveDebugLogger(debug) {
601
+ if (debug === true) {
602
+ return (report) => {
603
+ console.info(report);
604
+ };
605
+ }
606
+ if (debug && typeof debug === "object" && debug.logger) {
607
+ return debug.logger;
608
+ }
609
+ return void 0;
610
+ }
611
+ function createEnv(schema, options = {}) {
612
+ const debugEnabled = options.debug !== void 0 && options.debug !== false;
613
+ const debugLogger = debugEnabled ? resolveDebugLogger(options.debug) : void 0;
614
+ const debugKeys = [];
615
+ let source;
616
+ let loadedFileReport = [];
617
+ let sourceTrace = /* @__PURE__ */ Object.create(null);
618
+ if (options.source) {
619
+ source = options.source;
620
+ if (debugEnabled) {
621
+ for (const [key, raw] of Object.entries(source)) {
622
+ if (typeof raw === "string") {
623
+ sourceTrace[key] = { source: "process.env", raw };
624
+ }
625
+ }
626
+ }
627
+ } else {
628
+ const loadedFiles = loadEnvFiles({
629
+ cwd: options.cwd,
630
+ nodeEnv: options.nodeEnv,
631
+ envFile: options.envFile
632
+ });
633
+ source = mergeSources(loadedFiles, process.env);
634
+ if (debugEnabled) {
635
+ const cwd = options.cwd ?? process.cwd();
636
+ const nodeEnv = options.nodeEnv ?? process.env.NODE_ENV ?? "development";
637
+ loadedFileReport = buildLoadedFileReport(
638
+ loadedFiles,
639
+ cwd,
640
+ nodeEnv,
641
+ options.envFile
642
+ );
643
+ sourceTrace = buildSourceTrace(loadedFiles, process.env);
644
+ }
645
+ }
646
+ const issues = [];
647
+ const result = {};
648
+ const flattenedSchema = flattenSchema(schema);
649
+ if (options.strict) {
650
+ issues.push(
651
+ ...findUnknownEnvKeys(schema, source)
652
+ );
653
+ }
654
+ for (const entry of flattenedSchema) {
655
+ const { path: path4, envKey, rule } = entry;
656
+ const currentValue = source[envKey];
657
+ const sourceInfo = sourceTrace[envKey];
658
+ const sensitive = rule.sensitive === true;
659
+ const defaultKind = rule.default === void 0 ? void 0 : typeof rule.default === "function" ? "function" : "static";
660
+ const pushDebugEntry = (status, values) => {
661
+ if (!debugEnabled) {
662
+ return;
663
+ }
664
+ debugKeys.push({
665
+ key: envKey,
666
+ ruleType: rule.type,
667
+ source: values.source,
668
+ usedDefault: values.usedDefault,
669
+ defaultKind: values.defaultKind,
670
+ raw: maskDebugValue(values.raw, sensitive),
671
+ parsed: maskDebugValue(values.parsed, sensitive),
672
+ status,
673
+ issue: values.issue
674
+ });
675
+ };
676
+ if (typeof currentValue !== "string") {
677
+ if (rule.default !== void 0) {
678
+ const parsedDefault = parseDefaultValue(envKey, rule);
679
+ if (parsedDefault.issue) {
680
+ issues.push(parsedDefault.issue);
681
+ pushDebugEntry("issue", {
682
+ source: "default",
683
+ usedDefault: true,
684
+ defaultKind: parsedDefault.defaultKind,
685
+ raw: parsedDefault.rawDefault,
686
+ issue: parsedDefault.issue
687
+ });
688
+ continue;
689
+ }
690
+ pushDebugEntry("defaulted", {
691
+ source: "default",
692
+ usedDefault: true,
693
+ defaultKind: parsedDefault.defaultKind,
694
+ raw: parsedDefault.rawDefault,
695
+ parsed: parsedDefault.value
696
+ });
697
+ setNestedValue(result, path4, parsedDefault.value);
698
+ continue;
699
+ }
700
+ if (rule.required) {
701
+ const issue = {
702
+ key: envKey,
703
+ code: "missing",
704
+ message: `Missing required environment variable "${envKey}".`
705
+ };
706
+ issues.push(issue);
707
+ pushDebugEntry("missing", {
708
+ source: "missing",
709
+ usedDefault: false,
710
+ defaultKind,
711
+ issue
712
+ });
713
+ } else {
714
+ pushDebugEntry("missing", {
715
+ source: "missing",
716
+ usedDefault: false,
717
+ defaultKind
718
+ });
719
+ }
720
+ continue;
721
+ }
722
+ const rawValue = currentValue;
723
+ if (!rule.allowEmpty && isEmptyString(rawValue)) {
724
+ if (rule.default !== void 0) {
725
+ const parsedDefault = parseDefaultValue(envKey, rule);
726
+ if (parsedDefault.issue) {
727
+ issues.push(parsedDefault.issue);
728
+ pushDebugEntry("issue", {
729
+ source: "default",
730
+ usedDefault: true,
731
+ defaultKind: parsedDefault.defaultKind,
732
+ raw: parsedDefault.rawDefault,
733
+ issue: parsedDefault.issue
734
+ });
735
+ continue;
736
+ }
737
+ pushDebugEntry("defaulted", {
738
+ source: "default",
739
+ usedDefault: true,
740
+ defaultKind: parsedDefault.defaultKind,
741
+ raw: parsedDefault.rawDefault,
742
+ parsed: parsedDefault.value
743
+ });
744
+ setNestedValue(result, path4, parsedDefault.value);
745
+ continue;
746
+ }
747
+ const issue = {
748
+ key: envKey,
749
+ code: "empty",
750
+ message: `Environment variable "${envKey}" cannot be empty.`
751
+ };
752
+ issues.push(issue);
753
+ pushDebugEntry("empty", {
754
+ source: sourceInfo?.source ?? "process.env",
755
+ usedDefault: false,
756
+ defaultKind,
757
+ raw: rawValue,
758
+ issue
759
+ });
760
+ continue;
761
+ }
762
+ const parsed = parseValue(envKey, rawValue, rule);
763
+ if (parsed.issue) {
764
+ issues.push(parsed.issue);
765
+ pushDebugEntry("issue", {
766
+ source: sourceInfo?.source ?? "process.env",
767
+ usedDefault: false,
768
+ defaultKind,
769
+ raw: rawValue,
770
+ issue: parsed.issue
771
+ });
772
+ continue;
773
+ }
774
+ pushDebugEntry("parsed", {
775
+ source: sourceInfo?.source ?? "process.env",
776
+ usedDefault: false,
777
+ defaultKind,
778
+ raw: rawValue,
779
+ parsed: parsed.value
780
+ });
781
+ setNestedValue(result, path4, parsed.value);
782
+ }
783
+ if (debugLogger) {
784
+ debugLogger({
785
+ loadedFiles: loadedFileReport,
786
+ keys: debugKeys
787
+ });
788
+ }
789
+ if (issues.length > 0) {
790
+ throw new EnvValidationError(issues);
791
+ }
792
+ return result;
793
+ }
794
+
795
+ // src/cli/utils/formatIssues.ts
796
+ function formatIssues(issues) {
797
+ return issues.map((issue) => `- ${issue.key}: ${issue.message}`).join("\n");
798
+ }
799
+
800
+ // src/cli/utils/loadSchemaModule.ts
801
+ import path3 from "path";
802
+ import { pathToFileURL } from "url";
803
+ function isObject(value) {
804
+ return typeof value === "object" && value !== null;
805
+ }
806
+ async function loadSchemaModule(schemaPath) {
807
+ const absolutePath = path3.resolve(schemaPath);
808
+ const moduleUrl = pathToFileURL(absolutePath).href;
809
+ const mod = await import(moduleUrl);
810
+ const schemaCandidate = mod.default ?? mod.schema;
811
+ if (!isObject(schemaCandidate)) {
812
+ throw new Error(
813
+ `Schema module "${schemaPath}" must export a schema as default export or named export "schema".`
814
+ );
815
+ }
816
+ return schemaCandidate;
817
+ }
818
+
819
+ // src/cli/commands/validate.ts
820
+ async function runValidateCommand(options) {
821
+ try {
822
+ const schema = await loadSchemaModule(options.schemaPath);
823
+ createEnv(schema, {
824
+ cwd: options.cwd,
825
+ envFile: options.envFile,
826
+ nodeEnv: options.nodeEnv,
827
+ strict: options.strict
828
+ });
829
+ console.log("\u2713 Environment validation passed.");
830
+ return 0;
831
+ } catch (error) {
832
+ if (error instanceof EnvValidationError) {
833
+ console.error("Environment validation failed:\n");
834
+ console.error(formatIssues(error.issues));
835
+ return 1;
836
+ }
837
+ const message = error instanceof Error ? error.message : "Unknown CLI error";
838
+ console.error(`CLI error: ${message}`);
839
+ return 1;
840
+ }
841
+ }
842
+
843
+ // src/readEnvFileSource.ts
844
+ import { existsSync, readFileSync } from "fs";
845
+ import { resolve } from "path";
846
+ function stripQuotes(value) {
847
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
848
+ return value.slice(1, -1);
849
+ }
850
+ return value;
851
+ }
852
+ function readEnvFileSource(filePath) {
853
+ if (!existsSync(filePath)) {
854
+ return {};
855
+ }
856
+ const content = readFileSync(filePath, "utf8");
857
+ const source = {};
858
+ for (const line of content.split(/\r?\n/)) {
859
+ const trimmed = line.trim();
860
+ if (!trimmed || trimmed.startsWith("#")) {
861
+ continue;
862
+ }
863
+ const equalsIndex = trimmed.indexOf("=");
864
+ if (equalsIndex === -1) {
865
+ continue;
866
+ }
867
+ const key = trimmed.slice(0, equalsIndex).trim();
868
+ const rawValue = trimmed.slice(equalsIndex + 1).trim();
869
+ if (!key) {
870
+ continue;
871
+ }
872
+ source[key] = stripQuotes(rawValue);
873
+ }
874
+ return source;
875
+ }
876
+ function resolveExampleEnvPath(cwd = process.cwd(), exampleFile = ".env.example") {
877
+ return resolve(cwd, exampleFile);
878
+ }
879
+
880
+ // src/validateExampleEnv.ts
881
+ function validateExampleEnv(schema, exampleSource) {
882
+ const issues = [];
883
+ const flattenedSchema = flattenSchema(schema);
884
+ const expectedKeys = new Set(flattenedSchema.map((entry) => entry.envKey));
885
+ for (const entry of flattenedSchema) {
886
+ const { envKey } = entry;
887
+ if (!(envKey in exampleSource)) {
888
+ issues.push({
889
+ key: envKey,
890
+ code: "missing_example_key",
891
+ message: `Environment variable "${envKey}" is missing from .env.example.`
892
+ });
893
+ }
894
+ }
895
+ for (const key of Object.keys(exampleSource)) {
896
+ if (exampleSource[key] === void 0) {
897
+ continue;
898
+ }
899
+ if (expectedKeys.has(key)) {
900
+ continue;
901
+ }
902
+ issues.push({
903
+ key,
904
+ code: "unknown_example_key",
905
+ message: `Environment variable "${key}" in .env.example is not defined in the schema.`
906
+ });
907
+ }
908
+ return issues;
909
+ }
910
+
911
+ // src/validateExampleEnvFile.ts
912
+ function validateExampleEnvFile(schema, options = {}) {
913
+ const filePath = resolveExampleEnvPath(options.cwd, options.exampleFile);
914
+ const exampleSource = readEnvFileSource(filePath);
915
+ return validateExampleEnv(schema, exampleSource);
916
+ }
917
+
918
+ // src/cli/commands/validateExample.ts
919
+ async function runValidateExampleCommand(options) {
920
+ try {
921
+ const schema = await loadSchemaModule(options.schemaPath);
922
+ const issues = validateExampleEnvFile(schema, {
923
+ cwd: options.cwd,
924
+ exampleFile: options.exampleFile
925
+ });
926
+ if (issues.length > 0) {
927
+ console.error(".env.example validation failed:\n");
928
+ console.error(formatIssues(issues));
929
+ return 1;
930
+ }
931
+ console.log("\u2713 .env.example validation passed.");
932
+ return 0;
933
+ } catch (error) {
934
+ const message = error instanceof Error ? error.message : "Unknown CLI error";
935
+ console.error(`CLI error: ${message}`);
936
+ return 1;
937
+ }
938
+ }
939
+
940
+ // src/cli/index.ts
941
+ function printHelp(io) {
942
+ io.log(`node-safe-env CLI
943
+
944
+ Usage:
945
+ node-safe-env validate --schema <path> [--cwd <path>] [--env-file <path>] [--node-env <value>] [--strict]
946
+ node-safe-env validate-example --schema <path> [--cwd <path>] [--example-file <path>]
947
+
948
+ Commands:
949
+ validate Validate environment variables using a schema
950
+ validate-example Validate a .env.example file against a schema
951
+
952
+ Options:
953
+ --schema <path> Path to schema module
954
+ --cwd <path> Working directory for env file loading
955
+ --env-file <path> Custom env file path for validate
956
+ --node-env <value> NODE_ENV override for validate
957
+ --strict Enable strict mode for unknown keys
958
+ --example-file <path> Custom example file path for validate-example
959
+ --help Show this help
960
+ `);
961
+ }
962
+ function getStringFlag(flags, name) {
963
+ const value = flags[name];
964
+ return typeof value === "string" ? value : void 0;
965
+ }
966
+ async function runCli(argv, io = {
967
+ log: console.log,
968
+ error: console.error
969
+ }) {
970
+ const parsed = parseArgs(argv);
971
+ if (!parsed.command || parsed.flags.help || parsed.command === "help") {
972
+ printHelp(io);
973
+ return 0;
974
+ }
975
+ const schemaPath = getStringFlag(parsed.flags, "schema");
976
+ if (!schemaPath) {
977
+ io.error('Missing required flag "--schema".\n');
978
+ printHelp(io);
979
+ return 1;
980
+ }
981
+ if (parsed.command === "validate") {
982
+ return runValidateCommand({
983
+ schemaPath,
984
+ cwd: getStringFlag(parsed.flags, "cwd"),
985
+ envFile: getStringFlag(parsed.flags, "env-file"),
986
+ nodeEnv: getStringFlag(parsed.flags, "node-env"),
987
+ strict: parsed.flags.strict === true
988
+ });
989
+ }
990
+ if (parsed.command === "validate-example") {
991
+ return runValidateExampleCommand({
992
+ schemaPath,
993
+ cwd: getStringFlag(parsed.flags, "cwd"),
994
+ exampleFile: getStringFlag(parsed.flags, "example-file")
995
+ });
996
+ }
997
+ io.error(`Unknown command "${parsed.command}".
998
+ `);
999
+ printHelp(io);
1000
+ return 1;
1001
+ }
1002
+ async function main() {
1003
+ const exitCode = await runCli(process.argv.slice(2));
1004
+ process.exitCode = exitCode;
1005
+ }
1006
+ function isDirectRun() {
1007
+ const entry = process.argv[1];
1008
+ if (!entry) {
1009
+ return false;
1010
+ }
1011
+ return import.meta.url === pathToFileURL2(entry).href;
1012
+ }
1013
+ if (isDirectRun()) {
1014
+ void main();
1015
+ }
1016
+ export {
1017
+ runCli
1018
+ };
1019
+ //# sourceMappingURL=cli.js.map