@modootoday/envs 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/.agent/skills/env-value-store/SKILL.md +350 -0
- package/LICENSE +93 -0
- package/NOTICE +24 -0
- package/README.md +97 -0
- package/dist/chunk-47XO3SDE.js +4591 -0
- package/dist/chunk-STHFIZRP.js +1310 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +10 -0
- package/dist/config-DKLCgBZX.d.cts +116 -0
- package/dist/config-DKLCgBZX.d.ts +116 -0
- package/dist/config.cjs +940 -0
- package/dist/config.d.cts +5 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +10 -0
- package/dist/index.cjs +5915 -0
- package/dist/index.d.cts +418 -0
- package/dist/index.d.ts +418 -0
- package/dist/index.js +112 -0
- package/package.json +98 -0
package/dist/config.cjs
ADDED
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/config.ts
|
|
21
|
+
var config_exports = {};
|
|
22
|
+
__export(config_exports, {
|
|
23
|
+
result: () => result
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(config_exports);
|
|
26
|
+
|
|
27
|
+
// src/loader/config.ts
|
|
28
|
+
var import_node_fs2 = require("fs");
|
|
29
|
+
|
|
30
|
+
// src/format/parse.ts
|
|
31
|
+
var KEY_PATTERN = /^[\w.-]+$/;
|
|
32
|
+
var EXPORT_PREFIX = /^export\s+/;
|
|
33
|
+
function expandDoubleQuoted(raw) {
|
|
34
|
+
return raw.replace(/\\n/g, "\n").replace(/\\r/g, "\r");
|
|
35
|
+
}
|
|
36
|
+
function isQuote(ch) {
|
|
37
|
+
return ch === '"' || ch === "'" || ch === "`";
|
|
38
|
+
}
|
|
39
|
+
function scanQuoted(text, start, quote) {
|
|
40
|
+
let i = start + 1;
|
|
41
|
+
let out = "";
|
|
42
|
+
while (i < text.length) {
|
|
43
|
+
const ch = text[i];
|
|
44
|
+
if (ch === "\\" && i + 1 < text.length) {
|
|
45
|
+
out += ch + text[i + 1];
|
|
46
|
+
i += 2;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (ch === quote) {
|
|
50
|
+
return {
|
|
51
|
+
value: quote === '"' ? expandDoubleQuoted(out) : out,
|
|
52
|
+
quote,
|
|
53
|
+
next: i + 1,
|
|
54
|
+
unterminated: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
out += ch;
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
return { value: out, quote, next: text.length, unterminated: true };
|
|
61
|
+
}
|
|
62
|
+
function scanBare(text, start) {
|
|
63
|
+
let i = start;
|
|
64
|
+
while (i < text.length && text[i] !== "\n" && text[i] !== "#") i += 1;
|
|
65
|
+
return {
|
|
66
|
+
value: text.slice(start, i).trim(),
|
|
67
|
+
quote: null,
|
|
68
|
+
next: i,
|
|
69
|
+
unterminated: false
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function countLines(text, from, to) {
|
|
73
|
+
let n = 0;
|
|
74
|
+
for (let i = from; i < to; i += 1) if (text[i] === "\n") n += 1;
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
function restOfLineIsClean(text, from) {
|
|
78
|
+
let i = from;
|
|
79
|
+
while (i < text.length && text[i] !== "\n") {
|
|
80
|
+
const ch = text[i];
|
|
81
|
+
if (ch === "#") return true;
|
|
82
|
+
if (ch !== " " && ch !== " ") return false;
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function parseEnv(input) {
|
|
88
|
+
const text = input.replace(/\r\n?/g, "\n");
|
|
89
|
+
const entries = [];
|
|
90
|
+
const findings = [];
|
|
91
|
+
let i = 0;
|
|
92
|
+
let line = 1;
|
|
93
|
+
const skipToNextLine = (from) => {
|
|
94
|
+
const nl = text.indexOf("\n", from);
|
|
95
|
+
return nl === -1 ? text.length : nl + 1;
|
|
96
|
+
};
|
|
97
|
+
while (i < text.length) {
|
|
98
|
+
const eol = text.indexOf("\n", i);
|
|
99
|
+
const lineEnd = eol === -1 ? text.length : eol;
|
|
100
|
+
const raw = text.slice(i, lineEnd);
|
|
101
|
+
const trimmed = raw.trim();
|
|
102
|
+
if (trimmed === "") {
|
|
103
|
+
entries.push({ kind: "blank", line, endLine: line });
|
|
104
|
+
i = lineEnd + 1;
|
|
105
|
+
line += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (trimmed.startsWith("#")) {
|
|
109
|
+
entries.push({ kind: "comment", line, endLine: line });
|
|
110
|
+
i = lineEnd + 1;
|
|
111
|
+
line += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const lead = raw.length - raw.trimStart().length;
|
|
115
|
+
const body = raw.slice(lead);
|
|
116
|
+
const exported = EXPORT_PREFIX.test(body);
|
|
117
|
+
const afterExport = exported ? body.replace(EXPORT_PREFIX, "") : body;
|
|
118
|
+
const keyOffset = i + lead + (body.length - afterExport.length);
|
|
119
|
+
const eq = afterExport.indexOf("=");
|
|
120
|
+
if (eq === -1) {
|
|
121
|
+
findings.push({ code: "NOT_ENV_LINE", line });
|
|
122
|
+
i = lineEnd + 1;
|
|
123
|
+
line += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const key = afterExport.slice(0, eq).trim();
|
|
127
|
+
if (key === "") {
|
|
128
|
+
findings.push({ code: "EMPTY_KEY", line });
|
|
129
|
+
i = lineEnd + 1;
|
|
130
|
+
line += 1;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!KEY_PATTERN.test(key)) {
|
|
134
|
+
findings.push({ code: "INVALID_KEY", line });
|
|
135
|
+
i = lineEnd + 1;
|
|
136
|
+
line += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
let cursor = keyOffset + eq + 1;
|
|
140
|
+
while (cursor < text.length && (text[cursor] === " " || text[cursor] === " ")) {
|
|
141
|
+
cursor += 1;
|
|
142
|
+
}
|
|
143
|
+
const ch = text[cursor];
|
|
144
|
+
const scan = ch !== void 0 && isQuote(ch) ? scanQuoted(text, cursor, ch) : scanBare(text, cursor);
|
|
145
|
+
if (scan.unterminated) {
|
|
146
|
+
findings.push({ code: "UNTERMINATED_QUOTE", line });
|
|
147
|
+
i = text.length;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
const endLine = line + countLines(text, i, scan.next);
|
|
151
|
+
if (!restOfLineIsClean(text, scan.next)) {
|
|
152
|
+
findings.push({ code: "TRAILING_CONTENT", line: endLine });
|
|
153
|
+
i = skipToNextLine(scan.next);
|
|
154
|
+
line = endLine + 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
entries.push({
|
|
158
|
+
kind: "assignment",
|
|
159
|
+
line,
|
|
160
|
+
endLine,
|
|
161
|
+
key,
|
|
162
|
+
value: scan.value,
|
|
163
|
+
quote: scan.quote,
|
|
164
|
+
exported
|
|
165
|
+
});
|
|
166
|
+
i = skipToNextLine(scan.next);
|
|
167
|
+
line = endLine + 1;
|
|
168
|
+
}
|
|
169
|
+
return { ok: findings.length === 0, entries, findings };
|
|
170
|
+
}
|
|
171
|
+
function toRecord(result2) {
|
|
172
|
+
if (!result2.ok) return null;
|
|
173
|
+
const out = {};
|
|
174
|
+
for (const entry of result2.entries) {
|
|
175
|
+
if (entry.kind === "assignment") out[entry.key] = entry.value;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/sqlite/open.ts
|
|
181
|
+
var import_node_module = require("module");
|
|
182
|
+
|
|
183
|
+
// src/sqlite/provider.ts
|
|
184
|
+
var onBun = () => typeof globalThis["Bun"] !== "undefined";
|
|
185
|
+
var PROVIDERS = [
|
|
186
|
+
{
|
|
187
|
+
backend: "bun",
|
|
188
|
+
specifier: "bun:sqlite",
|
|
189
|
+
exportName: "Database",
|
|
190
|
+
builtIn: true,
|
|
191
|
+
// Rejects {} and {readonly:false}; a writable handle needs create.
|
|
192
|
+
openOptions: (readOnly) => readOnly ? { readonly: true } : { readonly: false, create: true },
|
|
193
|
+
eligible: onBun,
|
|
194
|
+
bindKey: (_bare, token) => token
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
backend: "node",
|
|
198
|
+
specifier: "node:sqlite",
|
|
199
|
+
exportName: "DatabaseSync",
|
|
200
|
+
builtIn: true,
|
|
201
|
+
// Spells it readOnly and silently ignores the lowercase name, which would
|
|
202
|
+
// hand back a writable database.
|
|
203
|
+
openOptions: (readOnly) => ({ readOnly }),
|
|
204
|
+
eligible: () => !onBun(),
|
|
205
|
+
bindKey: (_bare, token) => token
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
backend: "better-sqlite3",
|
|
209
|
+
specifier: "better-sqlite3",
|
|
210
|
+
exportName: "default",
|
|
211
|
+
builtIn: false,
|
|
212
|
+
// Wants the lowercase name and rejects the other as a misspelling.
|
|
213
|
+
openOptions: (readOnly) => ({ readonly: readOnly }),
|
|
214
|
+
// Never attempted on bun: the load is a process panic, not an exception.
|
|
215
|
+
eligible: () => !onBun(),
|
|
216
|
+
bindKey: (bare) => bare
|
|
217
|
+
}
|
|
218
|
+
];
|
|
219
|
+
function providerFor(backend) {
|
|
220
|
+
return PROVIDERS.find((provider) => provider.backend === backend);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/sqlite/open.ts
|
|
224
|
+
var import_meta = {};
|
|
225
|
+
var SqliteUnavailableError = class extends Error {
|
|
226
|
+
constructor(attempts) {
|
|
227
|
+
super(
|
|
228
|
+
`no sqlite backend available; tried ${attempts.map(([spec, reason]) => `${spec} (${reason})`).join(
|
|
229
|
+
", "
|
|
230
|
+
)}. On a runtime without node:sqlite, install better-sqlite3.`
|
|
231
|
+
);
|
|
232
|
+
this.attempts = attempts;
|
|
233
|
+
this.name = "SqliteUnavailableError";
|
|
234
|
+
}
|
|
235
|
+
attempts;
|
|
236
|
+
};
|
|
237
|
+
var SqliteBindError = class extends Error {
|
|
238
|
+
constructor(message) {
|
|
239
|
+
super(message);
|
|
240
|
+
this.name = "SqliteBindError";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
var NUL = String.fromCharCode(0);
|
|
244
|
+
var bindings = /* @__PURE__ */ new Map();
|
|
245
|
+
function readEnv(name) {
|
|
246
|
+
return globalThis.process?.env?.[name];
|
|
247
|
+
}
|
|
248
|
+
function shortlist(pinned) {
|
|
249
|
+
const name = pinned ?? readEnv("ENVS_SQLITE_BACKEND");
|
|
250
|
+
if (name === void 0 || name === "") return PROVIDERS;
|
|
251
|
+
const provider = providerFor(name);
|
|
252
|
+
if (provider === void 0) {
|
|
253
|
+
throw new SqliteUnavailableError([
|
|
254
|
+
[
|
|
255
|
+
"backend pin",
|
|
256
|
+
`"${name}" is not one of ${PROVIDERS.map((p) => p.backend).join(", ")}`
|
|
257
|
+
]
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
return [provider];
|
|
261
|
+
}
|
|
262
|
+
function normalise(value, where) {
|
|
263
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
264
|
+
if (value === void 0) {
|
|
265
|
+
throw new SqliteBindError(
|
|
266
|
+
`undefined bound at ${where}; pass null to mean NULL`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (typeof value === "string" && value.includes(NUL)) {
|
|
270
|
+
throw new SqliteBindError(
|
|
271
|
+
`value at ${where} contains a NUL byte, which sqlite TEXT truncates; store it as bytes instead`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
275
|
+
}
|
|
276
|
+
function scanPlaceholders(sql) {
|
|
277
|
+
const found = /* @__PURE__ */ new Map();
|
|
278
|
+
let i = 0;
|
|
279
|
+
while (i < sql.length) {
|
|
280
|
+
const ch = sql[i];
|
|
281
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
282
|
+
i += 1;
|
|
283
|
+
while (i < sql.length && sql[i] !== ch) i += 1;
|
|
284
|
+
i += 1;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (ch === "-" && sql[i + 1] === "-") {
|
|
288
|
+
while (i < sql.length && sql[i] !== "\n") i += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (ch === "/" && sql[i + 1] === "*") {
|
|
292
|
+
i += 2;
|
|
293
|
+
while (i < sql.length && !(sql[i] === "*" && sql[i + 1] === "/")) i += 1;
|
|
294
|
+
i += 2;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (ch === "$" || ch === ":" || ch === "@") {
|
|
298
|
+
let j = i + 1;
|
|
299
|
+
while (j < sql.length && /[A-Za-z0-9_]/.test(sql[j])) j += 1;
|
|
300
|
+
if (j > i + 1) {
|
|
301
|
+
const token = sql.slice(i, j);
|
|
302
|
+
found.set(token.slice(1), token);
|
|
303
|
+
i = j;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
i += 1;
|
|
308
|
+
}
|
|
309
|
+
return found;
|
|
310
|
+
}
|
|
311
|
+
function bindArgs(params, placeholders, provider) {
|
|
312
|
+
if (params === void 0) return [];
|
|
313
|
+
if (Array.isArray(params)) {
|
|
314
|
+
return params.map(
|
|
315
|
+
(value, index) => normalise(value, `position ${index + 1}`)
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const out = {};
|
|
319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
320
|
+
for (const [key, value] of Object.entries(
|
|
321
|
+
params
|
|
322
|
+
)) {
|
|
323
|
+
const bare = key.replace(/^[$:@]/, "");
|
|
324
|
+
const token = placeholders.get(bare);
|
|
325
|
+
if (token === void 0) {
|
|
326
|
+
throw new SqliteBindError(
|
|
327
|
+
`named parameter "${key}" has no placeholder in the statement`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
if (seen.has(bare)) {
|
|
331
|
+
throw new SqliteBindError(`named parameter "${bare}" given twice`);
|
|
332
|
+
}
|
|
333
|
+
seen.add(bare);
|
|
334
|
+
out[provider.bindKey(bare, token)] = normalise(value, `parameter ${key}`);
|
|
335
|
+
}
|
|
336
|
+
for (const bare of placeholders.keys()) {
|
|
337
|
+
if (!seen.has(bare)) {
|
|
338
|
+
throw new SqliteBindError(`placeholder "${bare}" has no value`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return [out];
|
|
342
|
+
}
|
|
343
|
+
function wrapStatement(raw, sql, provider) {
|
|
344
|
+
const placeholders = scanPlaceholders(sql);
|
|
345
|
+
const args = (params) => bindArgs(params, placeholders, provider);
|
|
346
|
+
return {
|
|
347
|
+
// Measured: with no matching row bun returns null and node undefined, so a
|
|
348
|
+
// caller's `!== undefined` check passes on one runtime and dereferences
|
|
349
|
+
// null on the other. One absent value for both.
|
|
350
|
+
get: (params) => raw.get(...args(params)) ?? void 0,
|
|
351
|
+
all: (params) => raw.all(...args(params)),
|
|
352
|
+
run: (params) => raw.run(...args(params))
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
var IN_MEMORY = /* @__PURE__ */ new Set([":memory:", ""]);
|
|
356
|
+
var JOURNAL_SQL = Object.freeze({
|
|
357
|
+
WAL: "PRAGMA journal_mode = WAL",
|
|
358
|
+
DELETE: "PRAGMA journal_mode = DELETE",
|
|
359
|
+
MEMORY: "PRAGMA journal_mode = MEMORY"
|
|
360
|
+
});
|
|
361
|
+
function build(binding, path, options) {
|
|
362
|
+
const { provider, ctor } = binding;
|
|
363
|
+
const readOnly = options.readOnly ?? false;
|
|
364
|
+
const raw = new ctor(path, provider.openOptions(readOnly));
|
|
365
|
+
const journalMode = options.journalMode ?? (IN_MEMORY.has(path) ? "MEMORY" : "WAL");
|
|
366
|
+
const journalSql = JOURNAL_SQL[journalMode];
|
|
367
|
+
if (journalSql === void 0) {
|
|
368
|
+
throw new SqliteBindError(`unknown journal mode ${String(journalMode)}`);
|
|
369
|
+
}
|
|
370
|
+
if (!readOnly) raw.exec(journalSql);
|
|
371
|
+
let inTransaction = false;
|
|
372
|
+
return {
|
|
373
|
+
backend: provider.backend,
|
|
374
|
+
exec: (sql) => raw.exec(sql),
|
|
375
|
+
prepare: (sql) => wrapStatement(raw.prepare(sql), sql, provider),
|
|
376
|
+
transaction(fn) {
|
|
377
|
+
if (inTransaction) throw new Error("transaction() is not nestable");
|
|
378
|
+
inTransaction = true;
|
|
379
|
+
raw.exec("BEGIN");
|
|
380
|
+
try {
|
|
381
|
+
const result2 = fn();
|
|
382
|
+
raw.exec("COMMIT");
|
|
383
|
+
return result2;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
raw.exec("ROLLBACK");
|
|
386
|
+
throw error;
|
|
387
|
+
} finally {
|
|
388
|
+
inTransaction = false;
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
close: () => raw.close()
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function moduleBase() {
|
|
395
|
+
return typeof __filename === "string" ? __filename : import_meta.url;
|
|
396
|
+
}
|
|
397
|
+
function loadBindingSync(pinned) {
|
|
398
|
+
const attempts = [];
|
|
399
|
+
const require2 = (0, import_node_module.createRequire)(moduleBase());
|
|
400
|
+
for (const provider of shortlist(pinned)) {
|
|
401
|
+
if (!provider.eligible()) {
|
|
402
|
+
attempts.push([provider.specifier, "not eligible in this runtime"]);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const already = bindings.get(provider.backend);
|
|
406
|
+
if (already) return already;
|
|
407
|
+
try {
|
|
408
|
+
const mod = require2(provider.specifier);
|
|
409
|
+
const ctor = provider.exportName === "default" ? mod.default ?? mod : mod[provider.exportName];
|
|
410
|
+
if (typeof ctor !== "function") {
|
|
411
|
+
attempts.push([provider.specifier, `no ${provider.exportName} export`]);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const binding = { provider, ctor };
|
|
415
|
+
bindings.set(provider.backend, binding);
|
|
416
|
+
return binding;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
attempts.push([provider.specifier, error.message]);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
throw new SqliteUnavailableError(attempts);
|
|
422
|
+
}
|
|
423
|
+
function openDatabaseSync(path, options = {}) {
|
|
424
|
+
return build(loadBindingSync(options.backend), path, options);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/loader/locate.ts
|
|
428
|
+
var import_node_fs = require("fs");
|
|
429
|
+
var import_node_os = require("os");
|
|
430
|
+
var import_node_path = require("path");
|
|
431
|
+
var DIR_NAME = ".envs";
|
|
432
|
+
var CATALOG_FILE = "catalog.sqlite";
|
|
433
|
+
var ROOT_MARKERS = [
|
|
434
|
+
"bun.lock",
|
|
435
|
+
"bun.lockb",
|
|
436
|
+
"pnpm-workspace.yaml",
|
|
437
|
+
"package-lock.json",
|
|
438
|
+
"yarn.lock",
|
|
439
|
+
".git",
|
|
440
|
+
"package.json"
|
|
441
|
+
];
|
|
442
|
+
function findProjectRoot(from) {
|
|
443
|
+
let dir = (0, import_node_path.resolve)(from);
|
|
444
|
+
for (; ; ) {
|
|
445
|
+
for (const marker of ROOT_MARKERS) {
|
|
446
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, marker))) return dir;
|
|
447
|
+
}
|
|
448
|
+
const parent = (0, import_node_path.dirname)(dir);
|
|
449
|
+
if (parent === dir) return void 0;
|
|
450
|
+
dir = parent;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function globalDir(home = (0, import_node_os.homedir)()) {
|
|
454
|
+
return (0, import_node_path.join)(home, DIR_NAME);
|
|
455
|
+
}
|
|
456
|
+
function locateCatalogs(options = {}) {
|
|
457
|
+
const env = options.env ?? process.env;
|
|
458
|
+
const cwd = options.cwd ?? process.cwd();
|
|
459
|
+
const home = options.home ?? env["HOME"] ?? (0, import_node_os.homedir)();
|
|
460
|
+
const explicit = env["ENVS_CATALOG_PATH"];
|
|
461
|
+
if (explicit !== void 0 && explicit !== "") {
|
|
462
|
+
return { project: (0, import_node_path.resolve)(explicit), source: "explicit" };
|
|
463
|
+
}
|
|
464
|
+
const globalPath = (0, import_node_path.join)(globalDir(home), CATALOG_FILE);
|
|
465
|
+
const root = findProjectRoot(cwd);
|
|
466
|
+
if (root === void 0) {
|
|
467
|
+
return { project: globalPath, source: "global" };
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
project: (0, import_node_path.join)(root, DIR_NAME, CATALOG_FILE),
|
|
471
|
+
global: globalPath,
|
|
472
|
+
projectRoot: root,
|
|
473
|
+
source: "project"
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/catalog/schema.ts
|
|
478
|
+
var SCHEMA_VERSION = 4;
|
|
479
|
+
var CatalogVersionError = class extends Error {
|
|
480
|
+
constructor(found, supported, message) {
|
|
481
|
+
super(message);
|
|
482
|
+
this.found = found;
|
|
483
|
+
this.supported = supported;
|
|
484
|
+
this.name = "CatalogVersionError";
|
|
485
|
+
}
|
|
486
|
+
found;
|
|
487
|
+
supported;
|
|
488
|
+
};
|
|
489
|
+
function tableExists(db, name) {
|
|
490
|
+
const row = db.prepare(
|
|
491
|
+
"SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = $name"
|
|
492
|
+
).get({ name });
|
|
493
|
+
return (row?.n ?? 0) > 0;
|
|
494
|
+
}
|
|
495
|
+
function readMeta(db) {
|
|
496
|
+
if (!tableExists(db, "schema_meta")) return void 0;
|
|
497
|
+
const row = db.prepare(
|
|
498
|
+
"SELECT version, catalog_id, created_at FROM schema_meta WHERE id = 1"
|
|
499
|
+
).get();
|
|
500
|
+
if (row === void 0) return void 0;
|
|
501
|
+
return {
|
|
502
|
+
version: Number(row.version),
|
|
503
|
+
catalogId: row.catalog_id,
|
|
504
|
+
createdAt: row.created_at
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/crypto/digest.ts
|
|
509
|
+
function toHex(bytes) {
|
|
510
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
511
|
+
""
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/crypto/envelope.ts
|
|
516
|
+
var import_node_crypto = require("crypto");
|
|
517
|
+
var MAGIC = new Uint8Array([69, 78, 86, 67]);
|
|
518
|
+
var FORMAT_AES_256_GCM = 1;
|
|
519
|
+
var TAG_BYTES = 16;
|
|
520
|
+
var KEK_BYTES = 32;
|
|
521
|
+
var HEADER_FIXED = MAGIC.length + 1 + 4 + 1;
|
|
522
|
+
var EnvelopeFormatError = class extends Error {
|
|
523
|
+
constructor(message) {
|
|
524
|
+
super(message);
|
|
525
|
+
this.name = "EnvelopeFormatError";
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
var EnvelopeAuthError = class extends Error {
|
|
529
|
+
constructor(message) {
|
|
530
|
+
super(message);
|
|
531
|
+
this.name = "EnvelopeAuthError";
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
function concat(parts) {
|
|
535
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
536
|
+
const out = new Uint8Array(total);
|
|
537
|
+
let at = 0;
|
|
538
|
+
for (const part of parts) {
|
|
539
|
+
out.set(part, at);
|
|
540
|
+
at += part.length;
|
|
541
|
+
}
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
function bytesEqual(a, b) {
|
|
545
|
+
if (a.length !== b.length) return false;
|
|
546
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
function checkKek(kek) {
|
|
550
|
+
if (kek.length !== KEK_BYTES) {
|
|
551
|
+
throw new EnvelopeFormatError(
|
|
552
|
+
`KEK must be ${KEK_BYTES} bytes for AES-256-GCM, got ${kek.length}`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function decodeHeader(blob) {
|
|
557
|
+
if (blob.length < HEADER_FIXED) {
|
|
558
|
+
throw new EnvelopeFormatError("blob shorter than the envelope header");
|
|
559
|
+
}
|
|
560
|
+
if (!bytesEqual(blob.subarray(0, MAGIC.length), MAGIC)) {
|
|
561
|
+
throw new EnvelopeFormatError("not an ENVC envelope");
|
|
562
|
+
}
|
|
563
|
+
const format = blob[MAGIC.length];
|
|
564
|
+
if (format !== FORMAT_AES_256_GCM) {
|
|
565
|
+
throw new EnvelopeFormatError(`unknown envelope format ${format}`);
|
|
566
|
+
}
|
|
567
|
+
const view = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
568
|
+
const kekVersion = view.getUint32(MAGIC.length + 1, false);
|
|
569
|
+
const ivLen = blob[MAGIC.length + 5];
|
|
570
|
+
const headerLength = HEADER_FIXED + ivLen;
|
|
571
|
+
if (blob.length < headerLength + TAG_BYTES) {
|
|
572
|
+
throw new EnvelopeFormatError("blob too short for its declared iv and tag");
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
header: {
|
|
576
|
+
format,
|
|
577
|
+
kekVersion,
|
|
578
|
+
iv: blob.subarray(HEADER_FIXED, headerLength)
|
|
579
|
+
},
|
|
580
|
+
headerBytes: blob.subarray(0, headerLength),
|
|
581
|
+
// The tag trails the ciphertext, as Web Crypto lays it out; node:crypto
|
|
582
|
+
// keeps the two apart, so the split happens here and nowhere else.
|
|
583
|
+
ciphertext: blob.subarray(headerLength, blob.length - TAG_BYTES),
|
|
584
|
+
tag: blob.subarray(blob.length - TAG_BYTES)
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function open(input) {
|
|
588
|
+
checkKek(input.kek);
|
|
589
|
+
const { header, headerBytes, ciphertext, tag } = decodeHeader(input.blob);
|
|
590
|
+
try {
|
|
591
|
+
const decipher = (0, import_node_crypto.createDecipheriv)("aes-256-gcm", input.kek, header.iv);
|
|
592
|
+
decipher.setAAD(concat([headerBytes, input.context]));
|
|
593
|
+
decipher.setAuthTag(tag);
|
|
594
|
+
return new Uint8Array(
|
|
595
|
+
Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
|
596
|
+
);
|
|
597
|
+
} catch {
|
|
598
|
+
throw new EnvelopeAuthError(
|
|
599
|
+
"envelope did not authenticate: wrong key, wrong context, or altered bytes"
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/crypto/keyring.ts
|
|
605
|
+
var import_node_crypto2 = require("crypto");
|
|
606
|
+
var DEK_BYTES = 32;
|
|
607
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
608
|
+
function kdfFor(method) {
|
|
609
|
+
return method === "recovery" ? "scrypt" : "hkdf";
|
|
610
|
+
}
|
|
611
|
+
var RecoveryCodeError = class extends Error {
|
|
612
|
+
constructor(message) {
|
|
613
|
+
super(message);
|
|
614
|
+
this.name = "RecoveryCodeError";
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
var KeyringLockedError = class extends Error {
|
|
618
|
+
constructor(tried) {
|
|
619
|
+
super(
|
|
620
|
+
`none of the ${tried} wraps opened with the secret given; try another recovery code or the KEK`
|
|
621
|
+
);
|
|
622
|
+
this.tried = tried;
|
|
623
|
+
this.name = "KeyringLockedError";
|
|
624
|
+
}
|
|
625
|
+
tried;
|
|
626
|
+
};
|
|
627
|
+
var SCRYPT = Object.freeze({ n: 1 << 15, r: 8, p: 1 });
|
|
628
|
+
function normaliseRecoveryCode(input) {
|
|
629
|
+
const cleaned = input.toUpperCase().replace(/[\s-]/g, "").replace(/[IL]/g, "1").replace(/O/g, "0");
|
|
630
|
+
if (cleaned.length === 0) {
|
|
631
|
+
throw new RecoveryCodeError("recovery code is empty");
|
|
632
|
+
}
|
|
633
|
+
for (const ch of cleaned) {
|
|
634
|
+
if (!ALPHABET.includes(ch)) {
|
|
635
|
+
throw new RecoveryCodeError(
|
|
636
|
+
`recovery code contains "${ch}", which is not in the alphabet`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return cleaned;
|
|
641
|
+
}
|
|
642
|
+
function wrapKeyFrom(secret, salt, method, cost) {
|
|
643
|
+
if (kdfFor(method) === "hkdf") {
|
|
644
|
+
return new Uint8Array(
|
|
645
|
+
(0, import_node_crypto2.hkdfSync)("sha256", secret, salt, "envs:dek-wrap:v1", DEK_BYTES)
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
return new Uint8Array(
|
|
649
|
+
(0, import_node_crypto2.scryptSync)(secret, salt, DEK_BYTES, {
|
|
650
|
+
N: cost.n,
|
|
651
|
+
r: cost.r,
|
|
652
|
+
p: cost.p,
|
|
653
|
+
// scrypt's default cap is below what N=32768 needs.
|
|
654
|
+
maxmem: 256 * cost.n * cost.r
|
|
655
|
+
})
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
var encoder = new TextEncoder();
|
|
659
|
+
function wrapContext(wrapId, method) {
|
|
660
|
+
return encoder.encode(`envs:dek-wrap:v1:${method}:${wrapId}`);
|
|
661
|
+
}
|
|
662
|
+
function unlockDek(wraps, unlock) {
|
|
663
|
+
const method = "kek" in unlock ? "kek" : "recovery";
|
|
664
|
+
const secret = "kek" in unlock ? unlock.kek : encoder.encode(normaliseRecoveryCode(unlock.recoveryCode));
|
|
665
|
+
const candidates = wraps.filter((wrap) => wrap.method === method);
|
|
666
|
+
for (const wrap of candidates) {
|
|
667
|
+
const key = wrapKeyFrom(secret, wrap.salt, wrap.method, wrap);
|
|
668
|
+
try {
|
|
669
|
+
return open({
|
|
670
|
+
kek: key,
|
|
671
|
+
blob: wrap.envelope,
|
|
672
|
+
context: wrapContext(wrap.wrapId, wrap.method)
|
|
673
|
+
});
|
|
674
|
+
} catch (error) {
|
|
675
|
+
if (!(error instanceof EnvelopeAuthError)) throw error;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
throw new KeyringLockedError(candidates.length);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/loader/read.ts
|
|
682
|
+
var NoReleaseError = class extends Error {
|
|
683
|
+
constructor(message) {
|
|
684
|
+
super(message);
|
|
685
|
+
this.name = "NoReleaseError";
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
var decoder = new TextDecoder();
|
|
689
|
+
var encoder2 = new TextEncoder();
|
|
690
|
+
function itemContext(sourceId, keyHash, revisionId) {
|
|
691
|
+
return encoder2.encode(
|
|
692
|
+
`envs:item:v1:${sourceId}:${toHex(keyHash)}:${revisionId}`
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
function assertReadable(db) {
|
|
696
|
+
const meta = readMeta(db);
|
|
697
|
+
if (meta !== void 0 && meta.version > SCHEMA_VERSION) {
|
|
698
|
+
throw new CatalogVersionError(
|
|
699
|
+
meta.version,
|
|
700
|
+
SCHEMA_VERSION,
|
|
701
|
+
`this catalog was written by a newer envs (schema ${String(meta.version)}, this build reads ${String(SCHEMA_VERSION)}); upgrade @modootoday/envs`
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function readWraps(db) {
|
|
706
|
+
assertReadable(db);
|
|
707
|
+
return db.prepare(
|
|
708
|
+
"SELECT wrap_id, method, salt, scrypt_n, scrypt_r, scrypt_p, envelope FROM dek_wraps WHERE retired_at IS NULL"
|
|
709
|
+
).all().map((row) => ({
|
|
710
|
+
wrapId: row.wrap_id,
|
|
711
|
+
method: row.method === "kek" ? "kek" : "recovery",
|
|
712
|
+
salt: new Uint8Array(row.salt),
|
|
713
|
+
n: Number(row.scrypt_n),
|
|
714
|
+
r: Number(row.scrypt_r),
|
|
715
|
+
p: Number(row.scrypt_p),
|
|
716
|
+
envelope: new Uint8Array(row.envelope)
|
|
717
|
+
}));
|
|
718
|
+
}
|
|
719
|
+
function currentRevision(db) {
|
|
720
|
+
return db.prepare(
|
|
721
|
+
"SELECT revision_id FROM pointer WHERE id = 1"
|
|
722
|
+
).get()?.revision_id;
|
|
723
|
+
}
|
|
724
|
+
function readEntries(db, options) {
|
|
725
|
+
const revision = options.revisionId ?? currentRevision(db);
|
|
726
|
+
if (revision === void 0) {
|
|
727
|
+
throw new NoReleaseError(
|
|
728
|
+
'catalog has no current release; run "envs pull" or "envs load" first'
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const dek = unlockDek(readWraps(db), options.unlock);
|
|
732
|
+
const rows = db.prepare(
|
|
733
|
+
`SELECT i.source_id, s.alias, s.path, i.key_hash, i.envelope
|
|
734
|
+
FROM items i
|
|
735
|
+
JOIN sources s ON s.source_id = i.source_id
|
|
736
|
+
WHERE i.revision_id = $revision
|
|
737
|
+
AND s.retired_at IS NULL
|
|
738
|
+
ORDER BY s.added_at, s.source_id, i.rowid`
|
|
739
|
+
).all({ revision });
|
|
740
|
+
const wanted = options.aliases === void 0 ? void 0 : new Set(options.aliases);
|
|
741
|
+
const entries = [];
|
|
742
|
+
for (const row of rows) {
|
|
743
|
+
if (wanted !== void 0 && !wanted.has(row.alias)) continue;
|
|
744
|
+
const plain = open({
|
|
745
|
+
kek: dek,
|
|
746
|
+
blob: new Uint8Array(row.envelope),
|
|
747
|
+
context: itemContext(
|
|
748
|
+
row.source_id,
|
|
749
|
+
new Uint8Array(row.key_hash),
|
|
750
|
+
revision
|
|
751
|
+
)
|
|
752
|
+
});
|
|
753
|
+
const text = decoder.decode(plain);
|
|
754
|
+
const split = text.indexOf("=");
|
|
755
|
+
if (split <= 0) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
`item in source ${row.alias} does not carry a key name; the catalog is corrupt`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
entries.push({
|
|
761
|
+
key: text.slice(0, split),
|
|
762
|
+
value: text.slice(split + 1),
|
|
763
|
+
sourceId: row.source_id,
|
|
764
|
+
alias: row.alias,
|
|
765
|
+
path: row.path
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
if (options.aliases === void 0) return entries;
|
|
769
|
+
const rank = new Map(options.aliases.map((alias, index) => [alias, index]));
|
|
770
|
+
return [...entries].sort(
|
|
771
|
+
(a, b) => (rank.get(a.alias) ?? 0) - (rank.get(b.alias) ?? 0)
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/loader/config.ts
|
|
776
|
+
var ConflictError = class extends Error {
|
|
777
|
+
constructor(key, first, second) {
|
|
778
|
+
super(
|
|
779
|
+
`key "${key}" is declared by ${first.layer}:${first.from} and ${second.layer}:${second.from}`
|
|
780
|
+
);
|
|
781
|
+
this.key = key;
|
|
782
|
+
this.first = first;
|
|
783
|
+
this.second = second;
|
|
784
|
+
this.name = "ConflictError";
|
|
785
|
+
}
|
|
786
|
+
key;
|
|
787
|
+
first;
|
|
788
|
+
second;
|
|
789
|
+
};
|
|
790
|
+
var KekMissingError = class extends Error {
|
|
791
|
+
constructor() {
|
|
792
|
+
super(
|
|
793
|
+
"no key available: set ENVS_KEK, or pass unlock explicitly. Values stay sealed without it."
|
|
794
|
+
);
|
|
795
|
+
this.name = "KekMissingError";
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
function decodeKek(raw) {
|
|
799
|
+
const bytes = new Uint8Array(Buffer.from(raw, "base64"));
|
|
800
|
+
if (bytes.length !== 32) {
|
|
801
|
+
throw new KekMissingError();
|
|
802
|
+
}
|
|
803
|
+
return bytes;
|
|
804
|
+
}
|
|
805
|
+
function resolveUnlock(options, env) {
|
|
806
|
+
if (options.unlock !== void 0) return options.unlock;
|
|
807
|
+
const raw = env["ENVS_KEK"];
|
|
808
|
+
if (raw === void 0 || raw === "") throw new KekMissingError();
|
|
809
|
+
return { kek: decodeKek(raw) };
|
|
810
|
+
}
|
|
811
|
+
function isTruthy(value) {
|
|
812
|
+
return value !== void 0 && value !== "" && value !== "0" && value !== "false";
|
|
813
|
+
}
|
|
814
|
+
function fileEntries(paths, encoding) {
|
|
815
|
+
const entries = [];
|
|
816
|
+
for (const path of paths) {
|
|
817
|
+
if (!(0, import_node_fs2.existsSync)(path)) continue;
|
|
818
|
+
const parsed = parseEnv((0, import_node_fs2.readFileSync)(path, encoding));
|
|
819
|
+
const record = toRecord(parsed);
|
|
820
|
+
if (record === null) {
|
|
821
|
+
return {
|
|
822
|
+
entries,
|
|
823
|
+
error: new Error(
|
|
824
|
+
`${path} is not env format: ${parsed.findings.map((f) => `${f.code} at line ${f.line}`).join(", ")}`
|
|
825
|
+
)
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
for (const [key, value] of Object.entries(record)) {
|
|
829
|
+
entries.push({ key, value, sourceId: path, alias: path, path });
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return { entries };
|
|
833
|
+
}
|
|
834
|
+
function catalogEntries(path, options, unlock) {
|
|
835
|
+
if (!(0, import_node_fs2.existsSync)(path)) return [];
|
|
836
|
+
let db;
|
|
837
|
+
try {
|
|
838
|
+
db = openDatabaseSync(path, { readOnly: true });
|
|
839
|
+
return readEntries(db, {
|
|
840
|
+
unlock,
|
|
841
|
+
...options.aliases ? { aliases: options.aliases } : {}
|
|
842
|
+
});
|
|
843
|
+
} finally {
|
|
844
|
+
db?.close();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function merge(ordered, override, onConflict, debug) {
|
|
848
|
+
const parsed = {};
|
|
849
|
+
const seen = /* @__PURE__ */ new Map();
|
|
850
|
+
for (const { entry, layer } of ordered) {
|
|
851
|
+
const here = { key: entry.key, layer, from: entry.alias };
|
|
852
|
+
const earlier = seen.get(entry.key);
|
|
853
|
+
if (earlier === void 0) {
|
|
854
|
+
parsed[entry.key] = entry.value;
|
|
855
|
+
seen.set(entry.key, here);
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
if (onConflict === "throw")
|
|
859
|
+
throw new ConflictError(entry.key, earlier, here);
|
|
860
|
+
if (onConflict === "warn") {
|
|
861
|
+
debug(
|
|
862
|
+
`[envs] "${entry.key}" declared by ${earlier.layer}:${earlier.from} and ${here.layer}:${here.from}`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
if (override) {
|
|
866
|
+
parsed[entry.key] = entry.value;
|
|
867
|
+
seen.set(entry.key, here);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return { parsed, provenance: [...seen.values()] };
|
|
871
|
+
}
|
|
872
|
+
function config(options = {}) {
|
|
873
|
+
const env = options.env ?? process.env;
|
|
874
|
+
const target = options.processEnv ?? process.env;
|
|
875
|
+
const override = options.override ?? false;
|
|
876
|
+
const onConflict = options.onConflict ?? "ignore";
|
|
877
|
+
const quiet = options.quiet ?? false;
|
|
878
|
+
const debugOn = options.debug ?? false;
|
|
879
|
+
const debug = (message) => {
|
|
880
|
+
if (debugOn || !quiet) process.stderr.write(`${message}
|
|
881
|
+
`);
|
|
882
|
+
};
|
|
883
|
+
try {
|
|
884
|
+
const locateOptions = {
|
|
885
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
886
|
+
...options.home ? { home: options.home } : {},
|
|
887
|
+
env
|
|
888
|
+
};
|
|
889
|
+
const located = locateCatalogs(locateOptions);
|
|
890
|
+
const useGlobal = (options.global ?? true) && !isTruthy(env["ENVS_NO_GLOBAL"]);
|
|
891
|
+
const ordered = [];
|
|
892
|
+
if (options.path !== void 0) {
|
|
893
|
+
const paths = typeof options.path === "string" ? [options.path] : options.path;
|
|
894
|
+
const read = fileEntries(paths, options.encoding ?? "utf8");
|
|
895
|
+
if (read.error) return { error: read.error };
|
|
896
|
+
for (const entry of read.entries) ordered.push({ entry, layer: "file" });
|
|
897
|
+
}
|
|
898
|
+
const sawCatalog = (0, import_node_fs2.existsSync)(located.project) || useGlobal && located.global !== void 0 && (0, import_node_fs2.existsSync)(located.global);
|
|
899
|
+
if (!sawCatalog && options.path === void 0) {
|
|
900
|
+
return {
|
|
901
|
+
error: new Error(
|
|
902
|
+
`no catalog at ${located.project}; run "envs init" first`
|
|
903
|
+
)
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
if (sawCatalog) {
|
|
907
|
+
const unlock = resolveUnlock(options, env);
|
|
908
|
+
for (const entry of catalogEntries(located.project, options, unlock)) {
|
|
909
|
+
ordered.push({ entry, layer: "project" });
|
|
910
|
+
}
|
|
911
|
+
if (useGlobal && located.global !== void 0) {
|
|
912
|
+
for (const entry of catalogEntries(located.global, options, unlock)) {
|
|
913
|
+
ordered.push({ entry, layer: "global" });
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const { parsed, provenance } = merge(ordered, override, onConflict, debug);
|
|
918
|
+
const applied = [];
|
|
919
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
920
|
+
const present = Object.prototype.hasOwnProperty.call(target, key);
|
|
921
|
+
if (present && !override) {
|
|
922
|
+
applied.push({ key, layer: "process", from: "process.env" });
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
target[key] = value;
|
|
926
|
+
applied.push(provenance.find((p) => p.key === key));
|
|
927
|
+
}
|
|
928
|
+
return { parsed, provenance: applied };
|
|
929
|
+
} catch (error) {
|
|
930
|
+
return { error };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/config.ts
|
|
935
|
+
var result = config();
|
|
936
|
+
if (result.error) throw result.error;
|
|
937
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
938
|
+
0 && (module.exports = {
|
|
939
|
+
result
|
|
940
|
+
});
|