dotenvx-ui 0.1.2 → 0.2.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.
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/scanner.ts
4
+ import { readdirSync, readFileSync, statSync } from "fs";
5
+ import { basename, dirname, join, relative } from "path";
6
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
7
+ "node_modules",
8
+ ".git",
9
+ "dist",
10
+ ".next",
11
+ ".turbo",
12
+ "build",
13
+ ".cache"
14
+ ]);
15
+ function detectRoot(cwd) {
16
+ let dir = cwd;
17
+ while (true) {
18
+ try {
19
+ statSync(join(dir, ".git"));
20
+ return dir;
21
+ } catch {
22
+ }
23
+ const parent = dirname(dir);
24
+ if (parent === dir) break;
25
+ dir = parent;
26
+ }
27
+ return cwd;
28
+ }
29
+ function scanForEnvFiles(root) {
30
+ const results = [];
31
+ function walk(dir) {
32
+ let entries;
33
+ try {
34
+ entries = readdirSync(dir);
35
+ } catch {
36
+ return;
37
+ }
38
+ for (const entry of entries) {
39
+ if (SKIP_DIRS.has(entry)) continue;
40
+ const full = join(dir, entry);
41
+ try {
42
+ const stat = statSync(full);
43
+ if (stat.isDirectory()) {
44
+ walk(full);
45
+ } else if (isEnvFile(entry)) {
46
+ results.push(full);
47
+ }
48
+ } catch {
49
+ }
50
+ }
51
+ }
52
+ walk(root);
53
+ return results;
54
+ }
55
+ function isEnvFile(name) {
56
+ if (name === ".env.keys") return false;
57
+ return name === ".env" || name.startsWith(".env.");
58
+ }
59
+ function parseEnvironmentFromFilename(filename) {
60
+ const name = basename(filename);
61
+ if (name === ".env") return "default";
62
+ const suffix = name.slice(".env.".length);
63
+ return suffix || "default";
64
+ }
65
+ function scan(cwd) {
66
+ const root = detectRoot(cwd);
67
+ const paths = scanForEnvFiles(root);
68
+ return paths.map((filePath) => {
69
+ const rel = relative(root, filePath);
70
+ const pkg = relative(root, dirname(filePath)) || ".";
71
+ const environment = parseEnvironmentFromFilename(basename(filePath));
72
+ let content = "";
73
+ try {
74
+ content = readFileSync(filePath, "utf8");
75
+ } catch {
76
+ }
77
+ const hasPublicKey = content.includes("DOTENV_PUBLIC_KEY=");
78
+ const encrypted = /encrypted:/.test(content);
79
+ return {
80
+ path: filePath,
81
+ relativePath: rel,
82
+ package: pkg,
83
+ environment,
84
+ encrypted,
85
+ hasPublicKey,
86
+ keys: []
87
+ };
88
+ });
89
+ }
90
+
91
+ // src/core/parser/values.ts
92
+ function isEncryptedValue(value) {
93
+ return value.startsWith("encrypted:");
94
+ }
95
+ function parseValue(rawValue, allLines, nextLineIdx) {
96
+ const trimmed = rawValue.trim();
97
+ if (trimmed.startsWith('"')) {
98
+ const inner = trimmed.slice(1);
99
+ const closeIdx = findClosingQuote(inner);
100
+ if (closeIdx !== -1) {
101
+ return { value: unescape(inner.slice(0, closeIdx)), extraLines: [] };
102
+ }
103
+ const valueLines = [inner];
104
+ let idx = nextLineIdx;
105
+ while (idx < allLines.length) {
106
+ const continuation = allLines[idx];
107
+ const close = findClosingQuote(continuation);
108
+ if (close !== -1) {
109
+ valueLines.push(continuation.slice(0, close));
110
+ return {
111
+ value: valueLines.join("\n"),
112
+ extraLines: allLines.slice(nextLineIdx, idx + 1)
113
+ };
114
+ }
115
+ valueLines.push(continuation);
116
+ idx++;
117
+ }
118
+ return {
119
+ value: valueLines.join("\n"),
120
+ extraLines: allLines.slice(nextLineIdx, idx)
121
+ };
122
+ }
123
+ if (trimmed.startsWith("'")) {
124
+ const inner = trimmed.slice(1);
125
+ const closeIdx = inner.indexOf("'");
126
+ return {
127
+ value: closeIdx !== -1 ? inner.slice(0, closeIdx) : inner,
128
+ extraLines: []
129
+ };
130
+ }
131
+ const commentIdx = trimmed.indexOf(" #");
132
+ const bare = commentIdx !== -1 ? trimmed.slice(0, commentIdx) : trimmed;
133
+ return { value: bare, extraLines: [] };
134
+ }
135
+ function serializeKeyValue(key, value) {
136
+ if (value.includes("\n")) {
137
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
138
+ return `${key}="${escaped}"`;
139
+ }
140
+ if (value === "" || /[\s#"'`]/.test(value)) {
141
+ return `${key}="${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
142
+ }
143
+ return `${key}=${value}`;
144
+ }
145
+ function findClosingQuote(s) {
146
+ for (let i = 0; i < s.length; i++) {
147
+ if (s[i] === "\\") {
148
+ i++;
149
+ continue;
150
+ }
151
+ if (s[i] === '"') return i;
152
+ }
153
+ return -1;
154
+ }
155
+ function unescape(s) {
156
+ return s.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\r").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
157
+ }
158
+
159
+ // src/core/parser/io.ts
160
+ import { randomBytes } from "crypto";
161
+ import { readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
162
+ import { dirname as dirname2, join as join2 } from "path";
163
+ function readEnvFile(filePath) {
164
+ const content = readFileSync2(filePath, "utf8");
165
+ return parse(content).filter((e) => e.type === "key").map((e) => ({
166
+ key: e.key,
167
+ value: e.value,
168
+ encrypted: isEncryptedValue(e.value),
169
+ comment: extractLeadingComment(e.lines)
170
+ }));
171
+ }
172
+ function writeEnvFile(filePath, keys) {
173
+ const content = readFileSync2(filePath, "utf8");
174
+ const entries = parse(content);
175
+ const updates = new Map(keys.map((k) => [k.key, k]));
176
+ const outLines = [];
177
+ const written = /* @__PURE__ */ new Set();
178
+ for (const entry of entries) {
179
+ if (entry.type === "raw") {
180
+ outLines.push(entry.text);
181
+ continue;
182
+ }
183
+ const update = updates.get(entry.key);
184
+ if (!update) continue;
185
+ written.add(entry.key);
186
+ const leadingComments = getLeadingCommentLines(entry.lines);
187
+ outLines.push(...leadingComments);
188
+ if (update.value === entry.value) {
189
+ const keyLines = entry.lines.filter(
190
+ (l) => !l.trimStart().startsWith("#")
191
+ );
192
+ outLines.push(...keyLines);
193
+ } else {
194
+ outLines.push(serializeKeyValue(entry.key, update.value));
195
+ }
196
+ }
197
+ for (const k of keys) {
198
+ if (!written.has(k.key)) {
199
+ if (k.comment) outLines.push(`# ${k.comment}`);
200
+ outLines.push(serializeKeyValue(k.key, k.value));
201
+ }
202
+ }
203
+ const output = outLines.join("\n") + (content.endsWith("\n") ? "\n" : "");
204
+ atomicWrite(filePath, output);
205
+ }
206
+ function addKey(filePath, key, value) {
207
+ const keys = readEnvFile(filePath);
208
+ if (keys.some((k) => k.key === key)) {
209
+ throw new Error(`Key "${key}" already exists in ${filePath}`);
210
+ }
211
+ keys.push({ key, value, encrypted: isEncryptedValue(value) });
212
+ writeEnvFile(filePath, keys);
213
+ }
214
+ function updateKey(filePath, key, value) {
215
+ const keys = readEnvFile(filePath);
216
+ const idx = keys.findIndex((k) => k.key === key);
217
+ if (idx === -1) throw new Error(`Key "${key}" not found in ${filePath}`);
218
+ keys[idx] = { ...keys[idx], key, value, encrypted: isEncryptedValue(value) };
219
+ writeEnvFile(filePath, keys);
220
+ }
221
+ function removeKey(filePath, key) {
222
+ const keys = readEnvFile(filePath).filter((k) => k.key !== key);
223
+ writeEnvFile(filePath, keys);
224
+ }
225
+ function parse(content) {
226
+ const entries = [];
227
+ const lines = content.split("\n");
228
+ if (lines[lines.length - 1] === "") lines.pop();
229
+ let i = 0;
230
+ let pendingComments = [];
231
+ while (i < lines.length) {
232
+ const line = lines[i];
233
+ if (line.trim() === "") {
234
+ for (const c of pendingComments) entries.push({ type: "raw", text: c });
235
+ pendingComments = [];
236
+ entries.push({ type: "raw", text: line });
237
+ i++;
238
+ continue;
239
+ }
240
+ if (line.trimStart().startsWith("#")) {
241
+ pendingComments.push(line);
242
+ i++;
243
+ continue;
244
+ }
245
+ const eqIdx = line.indexOf("=");
246
+ if (eqIdx === -1) {
247
+ for (const c of pendingComments) entries.push({ type: "raw", text: c });
248
+ pendingComments = [];
249
+ entries.push({ type: "raw", text: line });
250
+ i++;
251
+ continue;
252
+ }
253
+ const key = line.slice(0, eqIdx).trim();
254
+ const rawValue = line.slice(eqIdx + 1);
255
+ const { value, extraLines } = parseValue(rawValue, lines, i + 1);
256
+ entries.push({
257
+ type: "key",
258
+ key,
259
+ value,
260
+ lines: [...pendingComments, line, ...extraLines]
261
+ });
262
+ pendingComments = [];
263
+ i += 1 + extraLines.length;
264
+ }
265
+ for (const c of pendingComments) entries.push({ type: "raw", text: c });
266
+ return entries;
267
+ }
268
+ function getLeadingCommentLines(lines) {
269
+ const result = [];
270
+ for (const l of lines) {
271
+ if (l.trimStart().startsWith("#")) result.push(l);
272
+ else break;
273
+ }
274
+ return result;
275
+ }
276
+ function extractLeadingComment(lines) {
277
+ const comments = getLeadingCommentLines(lines).map(
278
+ (l) => l.trimStart().slice(1).trim()
279
+ );
280
+ return comments.length > 0 ? comments.join("\n") : void 0;
281
+ }
282
+ function atomicWrite(filePath, content) {
283
+ const tmp = join2(
284
+ dirname2(filePath),
285
+ `.dotenvx-ui-tmp-${randomBytes(6).toString("hex")}`
286
+ );
287
+ try {
288
+ writeFileSync(tmp, content, { encoding: "utf8", flag: "wx" });
289
+ renameSync(tmp, filePath);
290
+ } catch (err) {
291
+ try {
292
+ writeFileSync(tmp, "");
293
+ } catch {
294
+ }
295
+ throw new Error(`Failed to write ${filePath}: ${err.message}`);
296
+ }
297
+ }
298
+
299
+ // src/core/dotenvx.ts
300
+ import { createRequire } from "module";
301
+ import { existsSync, readFileSync as readFileSync3 } from "fs";
302
+ import { dirname as dirname3, join as join3 } from "path";
303
+ var dotenvx = createRequire(import.meta.url)("@dotenvx/dotenvx");
304
+ function decryptValue(encryptedValue, envFilePath) {
305
+ if (!isEncryptedValue(encryptedValue)) return encryptedValue;
306
+ const keyName = findKeyForValue(encryptedValue, envFilePath);
307
+ if (!keyName) return null;
308
+ const keysFile = findKeysFile(envFilePath);
309
+ return silenced(() => {
310
+ const result = dotenvx.get(keyName, {
311
+ path: envFilePath,
312
+ ...keysFile ? { envKeysFile: keysFile } : {},
313
+ logLevel: "error"
314
+ });
315
+ return result ?? null;
316
+ });
317
+ }
318
+ function decryptAllValues(envFilePath) {
319
+ let raw;
320
+ try {
321
+ raw = readFileSync3(envFilePath, "utf8");
322
+ } catch {
323
+ return {};
324
+ }
325
+ let privateKey = process.env.DOTENV_PRIVATE_KEY ?? null;
326
+ if (!privateKey) {
327
+ const keysFile = findKeysFile(envFilePath);
328
+ if (keysFile) {
329
+ try {
330
+ const keypairs = dotenvx.keypair(
331
+ envFilePath,
332
+ void 0,
333
+ keysFile
334
+ );
335
+ for (const [name, value] of Object.entries(keypairs)) {
336
+ if (name.startsWith("DOTENV_PRIVATE_KEY") && value) {
337
+ privateKey = value;
338
+ break;
339
+ }
340
+ }
341
+ } catch {
342
+ }
343
+ }
344
+ }
345
+ return silenced(() => {
346
+ try {
347
+ return dotenvx.parse(raw, {
348
+ ...privateKey ? { privateKey } : {},
349
+ processEnv: {}
350
+ });
351
+ } catch {
352
+ return {};
353
+ }
354
+ });
355
+ }
356
+ var DOTENVX_INTERNAL_KEYS = /* @__PURE__ */ new Set([
357
+ "DOTENV_PUBLIC_KEY",
358
+ "DOTENV_PRIVATE_KEY"
359
+ ]);
360
+ function encryptFile(envFilePath) {
361
+ const keys = readEnvFile(envFilePath);
362
+ for (const k of keys) {
363
+ if (!isEncryptedValue(k.value) && !DOTENVX_INTERNAL_KEYS.has(k.key)) {
364
+ dotenvx.set(k.key, k.value, {
365
+ path: envFilePath,
366
+ encrypt: true,
367
+ logLevel: "error"
368
+ });
369
+ }
370
+ }
371
+ }
372
+ function encryptKey(envFilePath, keyName, plainValue) {
373
+ dotenvx.set(keyName, plainValue, {
374
+ path: envFilePath,
375
+ encrypt: true,
376
+ logLevel: "error"
377
+ });
378
+ }
379
+ function decryptFile(envFilePath) {
380
+ const keys = readEnvFile(envFilePath);
381
+ const decrypted = decryptAllValues(envFilePath);
382
+ for (const k of keys) {
383
+ if (isEncryptedValue(k.value)) {
384
+ const plain = decrypted[k.key];
385
+ if (plain !== void 0 && !isEncryptedValue(plain))
386
+ updateKey(envFilePath, k.key, plain);
387
+ }
388
+ }
389
+ }
390
+ function silenced(fn) {
391
+ const origWrite = process.stderr.write.bind(process.stderr);
392
+ const origOut = process.stdout.write.bind(process.stdout);
393
+ const mute = (chunk) => {
394
+ const s = String(chunk);
395
+ if (s.includes("[MISSING_PRIVATE_KEY]") || s.includes("could not decrypt") || s.includes("\u2620"))
396
+ return true;
397
+ return false;
398
+ };
399
+ process.stderr.write = ((chunk, ...args) => mute(chunk) ? true : origWrite(
400
+ chunk,
401
+ ...args
402
+ ));
403
+ process.stdout.write = ((chunk, ...args) => mute(chunk) ? true : origOut(
404
+ chunk,
405
+ ...args
406
+ ));
407
+ try {
408
+ return fn();
409
+ } finally {
410
+ process.stderr.write = origWrite;
411
+ process.stdout.write = origOut;
412
+ }
413
+ }
414
+ function findKeysFile(envFilePath) {
415
+ let dir = dirname3(envFilePath);
416
+ while (true) {
417
+ const candidate = join3(dir, ".env.keys");
418
+ if (existsSync(candidate)) return candidate;
419
+ const parent = dirname3(dir);
420
+ if (parent === dir) break;
421
+ dir = parent;
422
+ }
423
+ return null;
424
+ }
425
+ function findKeyForValue(encryptedValue, envFilePath) {
426
+ let raw;
427
+ try {
428
+ raw = readFileSync3(envFilePath, "utf8");
429
+ } catch {
430
+ return null;
431
+ }
432
+ for (const line of raw.split("\n")) {
433
+ const eqIdx = line.indexOf("=");
434
+ if (eqIdx === -1) continue;
435
+ const keyName = line.slice(0, eqIdx).trim();
436
+ const rawVal = line.slice(eqIdx + 1).trim();
437
+ const unquoted = rawVal.startsWith('"') && rawVal.endsWith('"') ? rawVal.slice(1, -1) : rawVal;
438
+ if (unquoted === encryptedValue || rawVal === encryptedValue)
439
+ return keyName;
440
+ }
441
+ return null;
442
+ }
443
+
444
+ export {
445
+ scan,
446
+ isEncryptedValue,
447
+ readEnvFile,
448
+ addKey,
449
+ updateKey,
450
+ removeKey,
451
+ decryptValue,
452
+ decryptAllValues,
453
+ encryptFile,
454
+ encryptKey,
455
+ decryptFile
456
+ };