@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/generate-env-ts.d.ts +1 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/types.d.ts +4 -18
- package/dist/cli.js +327 -347
- package/dist/cli.js.map +10 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +585 -80
- package/dist/index.js.map +10 -6
- package/dist/introspect.d.ts +25 -0
- package/dist/schemas.d.ts +21 -16
- package/package.json +1 -1
- package/dist/cli/generate-example.d.ts +0 -2
- package/dist/cli/manifest-codec.d.ts +0 -3
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
|
-
|
|
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
|
-
|
|
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
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
659
|
-
const helpers = new Set(
|
|
660
|
-
const needsSchemaImport = kind === "
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
|
791
|
-
const
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
|
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=
|
|
1228
|
+
//# debugId=2598B6F54188F97964756E2164756E21
|
|
1249
1229
|
//# sourceMappingURL=cli.js.map
|