dotenvx-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1295 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.tsx
4
+ import { render } from "ink";
5
+
6
+ // src/core/scanner.ts
7
+ import { readdirSync, readFileSync, statSync } from "fs";
8
+ import { join, relative, basename, dirname } from "path";
9
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
10
+ "node_modules",
11
+ ".git",
12
+ "dist",
13
+ ".next",
14
+ ".turbo",
15
+ "build",
16
+ ".cache"
17
+ ]);
18
+ function detectRoot(cwd) {
19
+ let dir = cwd;
20
+ while (true) {
21
+ try {
22
+ statSync(join(dir, ".git"));
23
+ return dir;
24
+ } catch {
25
+ }
26
+ const parent = dirname(dir);
27
+ if (parent === dir) break;
28
+ dir = parent;
29
+ }
30
+ return cwd;
31
+ }
32
+ function scanForEnvFiles(root) {
33
+ const results = [];
34
+ function walk(dir) {
35
+ let entries;
36
+ try {
37
+ entries = readdirSync(dir);
38
+ } catch {
39
+ return;
40
+ }
41
+ for (const entry of entries) {
42
+ if (SKIP_DIRS.has(entry)) continue;
43
+ const full = join(dir, entry);
44
+ try {
45
+ const stat = statSync(full);
46
+ if (stat.isDirectory()) {
47
+ walk(full);
48
+ } else if (isEnvFile(entry)) {
49
+ results.push(full);
50
+ }
51
+ } catch {
52
+ }
53
+ }
54
+ }
55
+ walk(root);
56
+ return results;
57
+ }
58
+ function isEnvFile(name) {
59
+ if (name === ".env.keys") return false;
60
+ return name === ".env" || name.startsWith(".env.");
61
+ }
62
+ function parseEnvironmentFromFilename(filename) {
63
+ const name = basename(filename);
64
+ if (name === ".env") return "default";
65
+ const suffix = name.slice(".env.".length);
66
+ return suffix || "default";
67
+ }
68
+ function scan(cwd) {
69
+ const root = detectRoot(cwd);
70
+ const paths = scanForEnvFiles(root);
71
+ return paths.map((filePath) => {
72
+ const rel = relative(root, filePath);
73
+ const pkg = relative(root, dirname(filePath)) || ".";
74
+ const environment = parseEnvironmentFromFilename(basename(filePath));
75
+ let content = "";
76
+ try {
77
+ content = readFileSync(filePath, "utf8");
78
+ } catch {
79
+ }
80
+ const hasPublicKey = content.includes("DOTENV_PUBLIC_KEY=");
81
+ const encrypted = /encrypted:/.test(content);
82
+ return {
83
+ path: filePath,
84
+ relativePath: rel,
85
+ package: pkg,
86
+ environment,
87
+ encrypted,
88
+ hasPublicKey,
89
+ keys: []
90
+ };
91
+ });
92
+ }
93
+
94
+ // src/tui/App.tsx
95
+ import { useState as useState3 } from "react";
96
+ import { Box as Box7, Text as Text7, useApp, useInput as useInput6, useStdin as useStdin6 } from "ink";
97
+ import clipboard from "clipboardy";
98
+
99
+ // src/tui/FileList.tsx
100
+ import { Box, Text, useInput, useStdin } from "ink";
101
+ import { jsx, jsxs } from "react/jsx-runtime";
102
+ function FileList({ files, selectedIndex, focused, onSelect }) {
103
+ const { isRawModeSupported } = useStdin();
104
+ useInput((_, key) => {
105
+ if (!focused) return;
106
+ if (key.upArrow) onSelect(Math.max(0, selectedIndex - 1));
107
+ if (key.downArrow) onSelect(Math.min(files.length - 1, selectedIndex + 1));
108
+ }, { isActive: isRawModeSupported });
109
+ const byPkg = Map.groupBy(files, (f) => f.package);
110
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: 24, borderStyle: "single", borderRight: true, borderTop: false, borderBottom: false, borderLeft: false, children: Array.from(byPkg.entries()).map(([pkg, pkgFiles]) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
111
+ /* @__PURE__ */ jsxs(Text, { bold: true, dimColor: true, children: [
112
+ " ",
113
+ pkg
114
+ ] }),
115
+ pkgFiles.map((f) => {
116
+ const idx = files.indexOf(f);
117
+ const selected = idx === selectedIndex;
118
+ return /* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: /* @__PURE__ */ jsxs(
119
+ Text,
120
+ {
121
+ backgroundColor: selected && focused ? "blue" : void 0,
122
+ color: selected && focused ? "white" : selected ? "cyan" : void 0,
123
+ children: [
124
+ f.encrypted ? "\u{1F512} " : " ",
125
+ f.environment
126
+ ]
127
+ }
128
+ ) }, f.path);
129
+ })
130
+ ] }, pkg)) });
131
+ }
132
+
133
+ // src/tui/KeyTable.tsx
134
+ import { Box as Box2, Text as Text2, useInput as useInput2, useStdin as useStdin2 } from "ink";
135
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
136
+ var SECRET_PATTERN = /secret|password|token|key|private|api_?key/i;
137
+ function maskValue(k, revealed) {
138
+ if (revealed.has(k.key)) return revealed.get(k.key);
139
+ if (k.encrypted) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
140
+ if (SECRET_PATTERN.test(k.key)) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
141
+ return k.value.length > 48 ? k.value.slice(0, 48) + "\u2026" : k.value;
142
+ }
143
+ function KeyTable({ file, keys, selectedIndex, focused, revealed, onSelect }) {
144
+ const { isRawModeSupported } = useStdin2();
145
+ useInput2((_, key) => {
146
+ if (!focused) return;
147
+ if (key.upArrow) onSelect(Math.max(0, selectedIndex - 1));
148
+ if (key.downArrow) onSelect(Math.min(keys.length - 1, selectedIndex + 1));
149
+ }, { isActive: isRawModeSupported });
150
+ const encBadge = file.encrypted ? /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: " encrypted" }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " plain" });
151
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, children: [
152
+ /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, children: [
153
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: file.relativePath }),
154
+ encBadge,
155
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
156
+ " ",
157
+ keys.length,
158
+ " key",
159
+ keys.length === 1 ? "" : "s"
160
+ ] })
161
+ ] }),
162
+ keys.length === 0 ? /* @__PURE__ */ jsx2(Box2, { paddingX: 2, marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
163
+ "No keys found. Press ",
164
+ /* @__PURE__ */ jsx2(Text2, { bold: true, children: "a" }),
165
+ " to add one."
166
+ ] }) }) : keys.map((k, idx) => {
167
+ const selected = idx === selectedIndex;
168
+ const value = maskValue(k, revealed);
169
+ const lockIcon = k.encrypted && !revealed.has(k.key) ? " \u{1F512}" : "";
170
+ return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsxs2(
171
+ Text2,
172
+ {
173
+ backgroundColor: selected && focused ? "blue" : void 0,
174
+ color: selected && focused ? "white" : selected ? "cyan" : void 0,
175
+ children: [
176
+ k.key.padEnd(24),
177
+ /* @__PURE__ */ jsx2(Text2, { dimColor: !selected, children: value }),
178
+ lockIcon
179
+ ]
180
+ }
181
+ ) }, k.key);
182
+ })
183
+ ] });
184
+ }
185
+
186
+ // src/tui/StatusBar.tsx
187
+ import { Box as Box3, Text as Text3 } from "ink";
188
+ import { jsx as jsx3 } from "react/jsx-runtime";
189
+ var FILE_HINTS = "\u2191\u2193 navigate tab switch panel ? help q quit";
190
+ var KEY_HINTS = "\u2191\u2193 navigate tab switch enter edit y copy r reveal a add D del d diff ? help q quit";
191
+ function StatusBar({ message, focus }) {
192
+ return /* @__PURE__ */ jsx3(Box3, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingX: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: message ?? (focus === "files" ? FILE_HINTS : KEY_HINTS) }) });
193
+ }
194
+
195
+ // src/tui/DiffView.tsx
196
+ import { useState } from "react";
197
+ import { Box as Box4, Text as Text4, useInput as useInput3, useStdin as useStdin3 } from "ink";
198
+
199
+ // src/core/parser/io.ts
200
+ import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
201
+ import { join as join2, dirname as dirname2 } from "path";
202
+ import { randomBytes } from "crypto";
203
+
204
+ // src/core/parser/values.ts
205
+ function isEncryptedValue(value) {
206
+ return value.startsWith("encrypted:");
207
+ }
208
+ function parseValue(rawValue, allLines, nextLineIdx) {
209
+ const trimmed = rawValue.trim();
210
+ if (trimmed.startsWith('"')) {
211
+ const inner = trimmed.slice(1);
212
+ const closeIdx = findClosingQuote(inner);
213
+ if (closeIdx !== -1) {
214
+ return { value: unescape(inner.slice(0, closeIdx)), extraLines: [] };
215
+ }
216
+ const valueLines = [inner];
217
+ let idx = nextLineIdx;
218
+ while (idx < allLines.length) {
219
+ const continuation = allLines[idx];
220
+ const close = findClosingQuote(continuation);
221
+ if (close !== -1) {
222
+ valueLines.push(continuation.slice(0, close));
223
+ return {
224
+ value: valueLines.join("\n"),
225
+ extraLines: allLines.slice(nextLineIdx, idx + 1)
226
+ };
227
+ }
228
+ valueLines.push(continuation);
229
+ idx++;
230
+ }
231
+ return { value: valueLines.join("\n"), extraLines: allLines.slice(nextLineIdx, idx) };
232
+ }
233
+ if (trimmed.startsWith("'")) {
234
+ const inner = trimmed.slice(1);
235
+ const closeIdx = inner.indexOf("'");
236
+ return {
237
+ value: closeIdx !== -1 ? inner.slice(0, closeIdx) : inner,
238
+ extraLines: []
239
+ };
240
+ }
241
+ const commentIdx = trimmed.indexOf(" #");
242
+ const bare = commentIdx !== -1 ? trimmed.slice(0, commentIdx) : trimmed;
243
+ return { value: bare, extraLines: [] };
244
+ }
245
+ function serializeKeyValue(key, value) {
246
+ if (value.includes("\n")) {
247
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
248
+ return `${key}="${escaped}"`;
249
+ }
250
+ if (value === "" || /[\s#"'`]/.test(value)) {
251
+ return `${key}="${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
252
+ }
253
+ return `${key}=${value}`;
254
+ }
255
+ function findClosingQuote(s) {
256
+ for (let i = 0; i < s.length; i++) {
257
+ if (s[i] === "\\") {
258
+ i++;
259
+ continue;
260
+ }
261
+ if (s[i] === '"') return i;
262
+ }
263
+ return -1;
264
+ }
265
+ function unescape(s) {
266
+ return s.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\r").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
267
+ }
268
+
269
+ // src/core/parser/io.ts
270
+ function readEnvFile(filePath) {
271
+ const content = readFileSync2(filePath, "utf8");
272
+ return parse(content).filter((e) => e.type === "key").map((e) => ({
273
+ key: e.key,
274
+ value: e.value,
275
+ encrypted: isEncryptedValue(e.value),
276
+ comment: extractLeadingComment(e.lines)
277
+ }));
278
+ }
279
+ function writeEnvFile(filePath, keys) {
280
+ const content = readFileSync2(filePath, "utf8");
281
+ const entries = parse(content);
282
+ const updates = new Map(keys.map((k) => [k.key, k]));
283
+ const outLines = [];
284
+ const written = /* @__PURE__ */ new Set();
285
+ for (const entry of entries) {
286
+ if (entry.type === "raw") {
287
+ outLines.push(entry.text);
288
+ continue;
289
+ }
290
+ const update = updates.get(entry.key);
291
+ if (!update) continue;
292
+ written.add(entry.key);
293
+ const leadingComments = getLeadingCommentLines(entry.lines);
294
+ outLines.push(...leadingComments);
295
+ if (update.value === entry.value) {
296
+ const keyLines = entry.lines.filter((l) => !l.trimStart().startsWith("#"));
297
+ outLines.push(...keyLines);
298
+ } else {
299
+ outLines.push(serializeKeyValue(entry.key, update.value));
300
+ }
301
+ }
302
+ for (const k of keys) {
303
+ if (!written.has(k.key)) {
304
+ if (k.comment) outLines.push(`# ${k.comment}`);
305
+ outLines.push(serializeKeyValue(k.key, k.value));
306
+ }
307
+ }
308
+ const output = outLines.join("\n") + (content.endsWith("\n") ? "\n" : "");
309
+ atomicWrite(filePath, output);
310
+ }
311
+ function addKey(filePath, key, value) {
312
+ const keys = readEnvFile(filePath);
313
+ if (keys.some((k) => k.key === key)) {
314
+ throw new Error(`Key "${key}" already exists in ${filePath}`);
315
+ }
316
+ keys.push({ key, value, encrypted: isEncryptedValue(value) });
317
+ writeEnvFile(filePath, keys);
318
+ }
319
+ function updateKey(filePath, key, value) {
320
+ const keys = readEnvFile(filePath);
321
+ const idx = keys.findIndex((k) => k.key === key);
322
+ if (idx === -1) throw new Error(`Key "${key}" not found in ${filePath}`);
323
+ keys[idx] = { ...keys[idx], key, value, encrypted: isEncryptedValue(value) };
324
+ writeEnvFile(filePath, keys);
325
+ }
326
+ function removeKey(filePath, key) {
327
+ const keys = readEnvFile(filePath).filter((k) => k.key !== key);
328
+ writeEnvFile(filePath, keys);
329
+ }
330
+ function parse(content) {
331
+ const entries = [];
332
+ const lines = content.split("\n");
333
+ if (lines[lines.length - 1] === "") lines.pop();
334
+ let i = 0;
335
+ let pendingComments = [];
336
+ while (i < lines.length) {
337
+ const line = lines[i];
338
+ if (line.trim() === "") {
339
+ for (const c of pendingComments) entries.push({ type: "raw", text: c });
340
+ pendingComments = [];
341
+ entries.push({ type: "raw", text: line });
342
+ i++;
343
+ continue;
344
+ }
345
+ if (line.trimStart().startsWith("#")) {
346
+ pendingComments.push(line);
347
+ i++;
348
+ continue;
349
+ }
350
+ const eqIdx = line.indexOf("=");
351
+ if (eqIdx === -1) {
352
+ for (const c of pendingComments) entries.push({ type: "raw", text: c });
353
+ pendingComments = [];
354
+ entries.push({ type: "raw", text: line });
355
+ i++;
356
+ continue;
357
+ }
358
+ const key = line.slice(0, eqIdx).trim();
359
+ const rawValue = line.slice(eqIdx + 1);
360
+ const { value, extraLines } = parseValue(rawValue, lines, i + 1);
361
+ entries.push({ type: "key", key, value, lines: [...pendingComments, line, ...extraLines] });
362
+ pendingComments = [];
363
+ i += 1 + extraLines.length;
364
+ }
365
+ for (const c of pendingComments) entries.push({ type: "raw", text: c });
366
+ return entries;
367
+ }
368
+ function getLeadingCommentLines(lines) {
369
+ const result = [];
370
+ for (const l of lines) {
371
+ if (l.trimStart().startsWith("#")) result.push(l);
372
+ else break;
373
+ }
374
+ return result;
375
+ }
376
+ function extractLeadingComment(lines) {
377
+ const comments = getLeadingCommentLines(lines).map(
378
+ (l) => l.trimStart().slice(1).trim()
379
+ );
380
+ return comments.length > 0 ? comments.join("\n") : void 0;
381
+ }
382
+ function atomicWrite(filePath, content) {
383
+ const tmp = join2(
384
+ dirname2(filePath),
385
+ `.dotenvx-ui-tmp-${randomBytes(6).toString("hex")}`
386
+ );
387
+ try {
388
+ writeFileSync(tmp, content, { encoding: "utf8", flag: "wx" });
389
+ renameSync(tmp, filePath);
390
+ } catch (err) {
391
+ try {
392
+ writeFileSync(tmp, "");
393
+ } catch {
394
+ }
395
+ throw new Error(`Failed to write ${filePath}: ${err.message}`);
396
+ }
397
+ }
398
+
399
+ // src/core/dotenvx.ts
400
+ import { createRequire } from "module";
401
+ import { existsSync, readFileSync as readFileSync3 } from "fs";
402
+ import { join as join3, dirname as dirname3 } from "path";
403
+ var dotenvx = createRequire(import.meta.url)("@dotenvx/dotenvx");
404
+ function decryptValue(encryptedValue, envFilePath) {
405
+ if (!isEncryptedValue(encryptedValue)) return encryptedValue;
406
+ const keyName = findKeyForValue(encryptedValue, envFilePath);
407
+ if (!keyName) return null;
408
+ const keysFile = findKeysFile(envFilePath);
409
+ try {
410
+ const result = dotenvx.get(keyName, {
411
+ path: envFilePath,
412
+ ...keysFile ? { envKeysFile: keysFile } : {},
413
+ logLevel: "error"
414
+ });
415
+ return result ?? null;
416
+ } catch {
417
+ return null;
418
+ }
419
+ }
420
+ var DOTENVX_INTERNAL_KEYS = /* @__PURE__ */ new Set(["DOTENV_PUBLIC_KEY", "DOTENV_PRIVATE_KEY"]);
421
+ function encryptFile(envFilePath) {
422
+ const keys = readEnvFile(envFilePath);
423
+ for (const k of keys) {
424
+ if (!isEncryptedValue(k.value) && !DOTENVX_INTERNAL_KEYS.has(k.key)) {
425
+ dotenvx.set(k.key, k.value, {
426
+ path: envFilePath,
427
+ encrypt: true,
428
+ logLevel: "error"
429
+ });
430
+ }
431
+ }
432
+ }
433
+ function encryptKey(envFilePath, keyName, plainValue) {
434
+ dotenvx.set(keyName, plainValue, {
435
+ path: envFilePath,
436
+ encrypt: true,
437
+ logLevel: "error"
438
+ });
439
+ }
440
+ function decryptFile(envFilePath) {
441
+ const keys = readEnvFile(envFilePath);
442
+ for (const k of keys) {
443
+ if (isEncryptedValue(k.value)) {
444
+ const plain = decryptValue(k.value, envFilePath);
445
+ if (plain !== null) updateKey(envFilePath, k.key, plain);
446
+ }
447
+ }
448
+ }
449
+ function findKeysFile(envFilePath) {
450
+ let dir = dirname3(envFilePath);
451
+ while (true) {
452
+ const candidate = join3(dir, ".env.keys");
453
+ if (existsSync(candidate)) return candidate;
454
+ const parent = dirname3(dir);
455
+ if (parent === dir) break;
456
+ dir = parent;
457
+ }
458
+ return null;
459
+ }
460
+ function findKeyForValue(encryptedValue, envFilePath) {
461
+ let raw;
462
+ try {
463
+ raw = readFileSync3(envFilePath, "utf8");
464
+ } catch {
465
+ return null;
466
+ }
467
+ for (const line of raw.split("\n")) {
468
+ const eqIdx = line.indexOf("=");
469
+ if (eqIdx === -1) continue;
470
+ const lineValue = line.slice(eqIdx + 1).trim();
471
+ if (lineValue === encryptedValue) return line.slice(0, eqIdx).trim();
472
+ }
473
+ return null;
474
+ }
475
+
476
+ // src/tui/DiffView.tsx
477
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
478
+ var COL_VAL = 26;
479
+ function buildDisplayMap(keys, filePath) {
480
+ const out = /* @__PURE__ */ new Map();
481
+ for (const k of keys) {
482
+ if (k.encrypted) {
483
+ const plain = decryptValue(k.value, filePath);
484
+ out.set(k.key, plain !== null ? plain : "\u{1F512}");
485
+ } else {
486
+ out.set(k.key, k.value);
487
+ }
488
+ }
489
+ return out;
490
+ }
491
+ function buildRows(leftMap, rightMap) {
492
+ const allKeys = Array.from(/* @__PURE__ */ new Set([...leftMap.keys(), ...rightMap.keys()]));
493
+ return allKeys.map((key) => {
494
+ const l = leftMap.get(key) ?? null;
495
+ const r = rightMap.get(key) ?? null;
496
+ let status;
497
+ if (l === null) status = "right-only";
498
+ else if (r === null) status = "left-only";
499
+ else if (l === r) status = "same";
500
+ else status = "diff";
501
+ return {
502
+ key,
503
+ leftDisplay: l !== null ? trunc(l, COL_VAL) : "",
504
+ rightDisplay: r !== null ? trunc(r, COL_VAL) : "",
505
+ status
506
+ };
507
+ });
508
+ }
509
+ function trunc(s, max) {
510
+ const first = s.split("\n")[0];
511
+ return first.length > max ? first.slice(0, max - 1) + "\u2026" : first;
512
+ }
513
+ function safeRead(file) {
514
+ try {
515
+ return readEnvFile(file.path);
516
+ } catch {
517
+ return [];
518
+ }
519
+ }
520
+ function DiffView({ left, files, onClose }) {
521
+ const { isRawModeSupported } = useStdin3();
522
+ const others = files.filter((f) => f.path !== left.path);
523
+ const [pickerIndex, setPickerIndex] = useState(0);
524
+ const rightFile = others[pickerIndex] ?? null;
525
+ const leftKeys = safeRead(left);
526
+ const rightKeys = rightFile ? safeRead(rightFile) : [];
527
+ const leftMap = buildDisplayMap(leftKeys, left.path);
528
+ const rightMap = rightFile ? buildDisplayMap(rightKeys, rightFile.path) : /* @__PURE__ */ new Map();
529
+ const rows = rightFile ? buildRows(leftMap, rightMap) : [];
530
+ useInput3((input, key) => {
531
+ if (key.escape || input === "q") {
532
+ onClose();
533
+ return;
534
+ }
535
+ if (key.upArrow) setPickerIndex((i) => Math.max(0, i - 1));
536
+ if (key.downArrow) setPickerIndex((i) => Math.min(others.length - 1, i + 1));
537
+ }, { isActive: isRawModeSupported });
538
+ const leftName = trunc(left.relativePath, COL_VAL);
539
+ const rightName = rightFile ? trunc(rightFile.relativePath, COL_VAL) : "\u2014";
540
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
541
+ /* @__PURE__ */ jsxs3(Box4, { paddingX: 1, children: [
542
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "dotenvx-ui " }),
543
+ /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
544
+ "diff ",
545
+ left.relativePath,
546
+ " \u2194 ",
547
+ rightFile?.relativePath ?? "\u2014"
548
+ ] })
549
+ ] }),
550
+ /* @__PURE__ */ jsxs3(
551
+ Box4,
552
+ {
553
+ flexDirection: "column",
554
+ paddingX: 1,
555
+ borderStyle: "single",
556
+ borderBottom: true,
557
+ borderTop: false,
558
+ borderLeft: false,
559
+ borderRight: false,
560
+ children: [
561
+ /* @__PURE__ */ jsx4(Text4, { bold: true, dimColor: true, children: "compare with" }),
562
+ others.map((f, i) => {
563
+ const selected = i === pickerIndex;
564
+ return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs3(
565
+ Text4,
566
+ {
567
+ backgroundColor: selected ? "blue" : void 0,
568
+ color: selected ? "white" : void 0,
569
+ children: [
570
+ selected ? "\u25B6 " : " ",
571
+ f.relativePath
572
+ ]
573
+ }
574
+ ) }, f.path);
575
+ }),
576
+ others.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " no other files" })
577
+ ]
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
581
+ /* @__PURE__ */ jsxs3(Box4, { borderStyle: "single", borderBottom: true, borderTop: false, borderLeft: false, borderRight: false, children: [
582
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "KEY".padEnd(22) }),
583
+ /* @__PURE__ */ jsxs3(Text4, { bold: true, children: [
584
+ " ",
585
+ leftName.padEnd(COL_VAL + 2)
586
+ ] }),
587
+ /* @__PURE__ */ jsx4(Text4, { bold: true, children: rightName })
588
+ ] }),
589
+ rows.map((row) => /* @__PURE__ */ jsx4(DiffRow, { row }, row.key)),
590
+ rows.length === 0 && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Select a file to compare." }) })
591
+ ] }),
592
+ /* @__PURE__ */ jsxs3(Box4, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingX: 1, children: [
593
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 pick file esc close " }),
594
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u25CF same" })
595
+ ] })
596
+ ] });
597
+ }
598
+ function DiffRow({ row }) {
599
+ const { key, leftDisplay, rightDisplay, status } = row;
600
+ const color = status === "same" ? "green" : void 0;
601
+ return /* @__PURE__ */ jsxs3(Box4, { children: [
602
+ /* @__PURE__ */ jsx4(Text4, { color, children: key.padEnd(22) }),
603
+ /* @__PURE__ */ jsxs3(Text4, { color, children: [
604
+ " ",
605
+ (leftDisplay || "\u2014").padEnd(COL_VAL + 2)
606
+ ] }),
607
+ /* @__PURE__ */ jsx4(Text4, { color, children: rightDisplay || "\u2014" })
608
+ ] });
609
+ }
610
+
611
+ // src/tui/HelpOverlay.tsx
612
+ import { Box as Box5, Text as Text5, useInput as useInput4, useStdin as useStdin4 } from "ink";
613
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
614
+ var SECTIONS = [
615
+ {
616
+ title: "Navigation",
617
+ rows: [
618
+ ["\u2191 \u2193", "Move up / down"],
619
+ ["Tab", "Switch between file list and key table"],
620
+ ["q / Esc", "Quit"]
621
+ ]
622
+ },
623
+ {
624
+ title: "Key actions",
625
+ rows: [
626
+ ["Enter", "Edit selected key"],
627
+ ["a", "Add new key"],
628
+ ["D", "Delete selected key (confirmation required)"],
629
+ ["y", "Copy value to clipboard"],
630
+ ["r", "Reveal / hide selected key value"],
631
+ ["R", "Reveal / hide all key values"]
632
+ ]
633
+ },
634
+ {
635
+ title: "File actions",
636
+ rows: [
637
+ ["e", "Encrypt / decrypt entire file"],
638
+ ["d", "Open diff view"]
639
+ ]
640
+ },
641
+ {
642
+ title: "Diff view",
643
+ rows: [
644
+ ["\u2191 \u2193", "Pick file to compare"],
645
+ ["Esc / q", "Close diff view"]
646
+ ]
647
+ }
648
+ ];
649
+ var KEY_WIDTH = 10;
650
+ function HelpOverlay({ onClose }) {
651
+ const { isRawModeSupported } = useStdin4();
652
+ useInput4((input, key) => {
653
+ if (input === "?" || input === "q" || key.escape) onClose();
654
+ }, { isActive: isRawModeSupported });
655
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
656
+ /* @__PURE__ */ jsxs4(Box5, { marginBottom: 1, children: [
657
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "dotenvx-ui " }),
658
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "keyboard shortcuts" })
659
+ ] }),
660
+ SECTIONS.map((section) => /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", marginBottom: 1, children: [
661
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: section.title }),
662
+ section.rows.map(([key, desc]) => /* @__PURE__ */ jsxs4(Box5, { children: [
663
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: key.padEnd(KEY_WIDTH) }),
664
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: desc })
665
+ ] }, key))
666
+ ] }, section.title)),
667
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "? / q / esc close help" }) })
668
+ ] });
669
+ }
670
+
671
+ // src/tui/InlineForm.tsx
672
+ import { useState as useState2 } from "react";
673
+ import { Box as Box6, Text as Text6, useInput as useInput5, useStdin as useStdin5 } from "ink";
674
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
675
+ function InlineForm({ label, initialValue = "", onSubmit, onCancel }) {
676
+ const { isRawModeSupported } = useStdin5();
677
+ const [value, setValue] = useState2(initialValue);
678
+ const [cursor, setCursor] = useState2(initialValue.length);
679
+ useInput5((input, key) => {
680
+ if (key.escape) {
681
+ onCancel();
682
+ return;
683
+ }
684
+ if (key.return) {
685
+ onSubmit(value);
686
+ return;
687
+ }
688
+ if (key.leftArrow) {
689
+ setCursor((c) => Math.max(0, c - 1));
690
+ return;
691
+ }
692
+ if (key.rightArrow) {
693
+ setCursor((c) => Math.min(value.length, c + 1));
694
+ return;
695
+ }
696
+ if (key.ctrl && input === "a" || key.home) {
697
+ setCursor(0);
698
+ return;
699
+ }
700
+ if (key.ctrl && input === "e" || key.end) {
701
+ setCursor(value.length);
702
+ return;
703
+ }
704
+ if (key.ctrl && input === "k") {
705
+ setValue((v) => v.slice(0, cursor));
706
+ return;
707
+ }
708
+ if (key.ctrl && input === "u") {
709
+ setValue((v) => v.slice(cursor));
710
+ setCursor(0);
711
+ return;
712
+ }
713
+ if (key.backspace) {
714
+ if (cursor === 0) return;
715
+ setValue((v) => v.slice(0, cursor - 1) + v.slice(cursor));
716
+ setCursor((c) => c - 1);
717
+ return;
718
+ }
719
+ if (key.delete) {
720
+ setValue((v) => v.slice(0, cursor) + v.slice(cursor + 1));
721
+ return;
722
+ }
723
+ if (input && !key.ctrl && !key.meta) {
724
+ setValue((v) => v.slice(0, cursor) + input + v.slice(cursor));
725
+ setCursor((c) => c + input.length);
726
+ }
727
+ }, { isActive: isRawModeSupported });
728
+ const before = value.slice(0, cursor);
729
+ const at = value[cursor] ?? " ";
730
+ const after = value.slice(cursor + 1);
731
+ return /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingX: 1, paddingTop: 0, children: /* @__PURE__ */ jsxs5(Box6, { children: [
732
+ /* @__PURE__ */ jsxs5(Text6, { bold: true, color: "cyan", children: [
733
+ label,
734
+ " "
735
+ ] }),
736
+ /* @__PURE__ */ jsx6(Text6, { children: before }),
737
+ /* @__PURE__ */ jsx6(Text6, { inverse: true, children: at }),
738
+ /* @__PURE__ */ jsx6(Text6, { children: after }),
739
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \u21B5 confirm esc cancel" })
740
+ ] }) });
741
+ }
742
+
743
+ // src/tui/App.tsx
744
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
745
+ function App({ files }) {
746
+ const { exit } = useApp();
747
+ const { isRawModeSupported } = useStdin6();
748
+ const [fileIndex, setFileIndex] = useState3(0);
749
+ const [keyIndex, setKeyIndex] = useState3(0);
750
+ const [focus, setFocus] = useState3("files");
751
+ const [revealed, setRevealed] = useState3(/* @__PURE__ */ new Map());
752
+ const [mode, setMode] = useState3({ type: "normal" });
753
+ const [statusMsg, setStatusMsg] = useState3();
754
+ const [keys, setKeys] = useState3(() => loadKeys(files[0]));
755
+ const selectedFile = files[fileIndex];
756
+ function loadKeys(file) {
757
+ try {
758
+ return readEnvFile(file.path);
759
+ } catch {
760
+ return [];
761
+ }
762
+ }
763
+ function refreshKeys() {
764
+ setKeys(loadKeys(selectedFile));
765
+ }
766
+ function selectFile(idx) {
767
+ setFileIndex(idx);
768
+ setKeyIndex(0);
769
+ setRevealed(/* @__PURE__ */ new Map());
770
+ setKeys(loadKeys(files[idx]));
771
+ }
772
+ function flash(msg, ms = 1500) {
773
+ setStatusMsg(msg);
774
+ setTimeout(() => setStatusMsg(void 0), ms);
775
+ }
776
+ useInput6((input, key) => {
777
+ if (mode.type !== "normal") return;
778
+ if (input === "q" || key.escape) {
779
+ exit();
780
+ return;
781
+ }
782
+ if (input === "?") {
783
+ setMode({ type: "help" });
784
+ return;
785
+ }
786
+ if (key.tab) {
787
+ setFocus((f) => f === "files" ? "keys" : "files");
788
+ return;
789
+ }
790
+ if (focus !== "keys") return;
791
+ const k = keys[keyIndex];
792
+ if (input === "r") {
793
+ if (!k) return;
794
+ if (revealed.has(k.key)) {
795
+ setRevealed((prev) => {
796
+ const next = new Map(prev);
797
+ next.delete(k.key);
798
+ return next;
799
+ });
800
+ return;
801
+ }
802
+ if (k.encrypted) {
803
+ const plain = decryptValue(k.value, selectedFile.path);
804
+ if (plain === null) {
805
+ flash("\u{1F512} Private key not found in environment");
806
+ return;
807
+ }
808
+ setRevealed((prev) => new Map(prev).set(k.key, plain));
809
+ } else {
810
+ setRevealed((prev) => new Map(prev).set(k.key, k.value));
811
+ }
812
+ return;
813
+ }
814
+ if (input === "R") {
815
+ if (revealed.size > 0) {
816
+ setRevealed(/* @__PURE__ */ new Map());
817
+ return;
818
+ }
819
+ const next = /* @__PURE__ */ new Map();
820
+ for (const entry of keys) {
821
+ if (entry.encrypted) {
822
+ const plain = decryptValue(entry.value, selectedFile.path);
823
+ if (plain === null) {
824
+ flash("\u{1F512} Private key not found \u2014 cannot reveal all");
825
+ return;
826
+ }
827
+ next.set(entry.key, plain);
828
+ } else {
829
+ next.set(entry.key, entry.value);
830
+ }
831
+ }
832
+ setRevealed(next);
833
+ return;
834
+ }
835
+ if (input === "y") {
836
+ if (!k) return;
837
+ const value = k.encrypted ? decryptValue(k.value, selectedFile.path) : k.value;
838
+ if (value === null) {
839
+ flash("\u{1F512} Private key not found \u2014 cannot copy");
840
+ return;
841
+ }
842
+ clipboard.writeSync(value);
843
+ flash(`Copied ${k.key}`);
844
+ return;
845
+ }
846
+ if (key.return) {
847
+ if (!k) return;
848
+ if (k.encrypted) {
849
+ const plain = decryptValue(k.value, selectedFile.path);
850
+ if (plain === null) {
851
+ flash("\u{1F512} Private key not found \u2014 cannot edit");
852
+ return;
853
+ }
854
+ setMode({ type: "edit", key: { ...k, value: plain } });
855
+ } else {
856
+ setMode({ type: "edit", key: k });
857
+ }
858
+ return;
859
+ }
860
+ if (input === "a") {
861
+ setMode({ type: "add-key" });
862
+ return;
863
+ }
864
+ if (input === "D") {
865
+ if (!k) return;
866
+ setMode({ type: "confirm-delete", key: k });
867
+ return;
868
+ }
869
+ if (input === "d") {
870
+ setMode({ type: "diff" });
871
+ return;
872
+ }
873
+ if (input === "e") {
874
+ setMode({ type: "confirm-encrypt" });
875
+ return;
876
+ }
877
+ }, { isActive: isRawModeSupported && mode.type === "normal" });
878
+ if (mode.type === "edit") {
879
+ const editing = mode.key;
880
+ return /* @__PURE__ */ jsx7(
881
+ Layout,
882
+ {
883
+ files,
884
+ fileIndex,
885
+ keys,
886
+ keyIndex,
887
+ focus,
888
+ revealed,
889
+ onSelectFile: selectFile,
890
+ onSelectKey: setKeyIndex,
891
+ statusMsg,
892
+ focus2: focus,
893
+ extra: /* @__PURE__ */ jsx7(
894
+ InlineForm,
895
+ {
896
+ label: `Edit ${editing.key}`,
897
+ initialValue: editing.value,
898
+ onSubmit: (val) => {
899
+ if (editing.encrypted) {
900
+ encryptKey(selectedFile.path, editing.key, val);
901
+ } else {
902
+ updateKey(selectedFile.path, editing.key, val);
903
+ }
904
+ setRevealed((prev) => {
905
+ const next = new Map(prev);
906
+ next.delete(editing.key);
907
+ return next;
908
+ });
909
+ refreshKeys();
910
+ setMode({ type: "normal" });
911
+ flash(`Saved ${editing.key}`);
912
+ },
913
+ onCancel: () => setMode({ type: "normal" })
914
+ }
915
+ )
916
+ }
917
+ );
918
+ }
919
+ if (mode.type === "add-key") {
920
+ return /* @__PURE__ */ jsx7(
921
+ Layout,
922
+ {
923
+ files,
924
+ fileIndex,
925
+ keys,
926
+ keyIndex,
927
+ focus,
928
+ revealed,
929
+ onSelectFile: selectFile,
930
+ onSelectKey: setKeyIndex,
931
+ statusMsg,
932
+ focus2: focus,
933
+ extra: /* @__PURE__ */ jsx7(
934
+ InlineForm,
935
+ {
936
+ label: "New key name",
937
+ onSubmit: (keyName) => {
938
+ if (!keyName.trim()) {
939
+ setMode({ type: "normal" });
940
+ return;
941
+ }
942
+ setMode({ type: "add-value", keyName: keyName.trim() });
943
+ },
944
+ onCancel: () => setMode({ type: "normal" })
945
+ },
946
+ "add-key"
947
+ )
948
+ }
949
+ );
950
+ }
951
+ if (mode.type === "add-value") {
952
+ const { keyName } = mode;
953
+ return /* @__PURE__ */ jsx7(
954
+ Layout,
955
+ {
956
+ files,
957
+ fileIndex,
958
+ keys,
959
+ keyIndex,
960
+ focus,
961
+ revealed,
962
+ onSelectFile: selectFile,
963
+ onSelectKey: setKeyIndex,
964
+ statusMsg,
965
+ focus2: focus,
966
+ extra: /* @__PURE__ */ jsx7(
967
+ InlineForm,
968
+ {
969
+ label: `Value for ${keyName}`,
970
+ onSubmit: (val) => {
971
+ if (selectedFile.encrypted) {
972
+ setMode({ type: "confirm-add-encrypt", keyName, value: val });
973
+ } else {
974
+ addKey(selectedFile.path, keyName, val);
975
+ refreshKeys();
976
+ setKeyIndex(keys.length);
977
+ setMode({ type: "normal" });
978
+ flash(`Added ${keyName}`);
979
+ }
980
+ },
981
+ onCancel: () => setMode({ type: "normal" })
982
+ },
983
+ "add-value"
984
+ )
985
+ }
986
+ );
987
+ }
988
+ if (mode.type === "confirm-add-encrypt") {
989
+ const { keyName, value: newVal } = mode;
990
+ const commit = (encrypt) => {
991
+ addKey(selectedFile.path, keyName, newVal);
992
+ if (encrypt) encryptKey(selectedFile.path, keyName, newVal);
993
+ refreshKeys();
994
+ setKeyIndex(keys.length);
995
+ setMode({ type: "normal" });
996
+ flash(`Added ${keyName}${encrypt ? " (encrypted)" : ""}`);
997
+ };
998
+ return /* @__PURE__ */ jsx7(
999
+ Layout,
1000
+ {
1001
+ files,
1002
+ fileIndex,
1003
+ keys,
1004
+ keyIndex,
1005
+ focus,
1006
+ revealed,
1007
+ onSelectFile: selectFile,
1008
+ onSelectKey: setKeyIndex,
1009
+ statusMsg,
1010
+ focus2: focus,
1011
+ extra: /* @__PURE__ */ jsx7(ConfirmAddEncrypt, { keyName, onEncrypt: () => commit(true), onPlain: () => commit(false), onCancel: () => setMode({ type: "normal" }) })
1012
+ }
1013
+ );
1014
+ }
1015
+ if (mode.type === "confirm-delete") {
1016
+ const { key: k } = mode;
1017
+ return /* @__PURE__ */ jsx7(
1018
+ Layout,
1019
+ {
1020
+ files,
1021
+ fileIndex,
1022
+ keys,
1023
+ keyIndex,
1024
+ focus,
1025
+ revealed,
1026
+ onSelectFile: selectFile,
1027
+ onSelectKey: setKeyIndex,
1028
+ statusMsg,
1029
+ focus2: focus,
1030
+ extra: /* @__PURE__ */ jsx7(
1031
+ ConfirmDelete,
1032
+ {
1033
+ keyName: k.key,
1034
+ onConfirm: () => {
1035
+ removeKey(selectedFile.path, k.key);
1036
+ refreshKeys();
1037
+ setKeyIndex(Math.max(0, keyIndex - 1));
1038
+ setMode({ type: "normal" });
1039
+ flash(`Deleted ${k.key}`);
1040
+ },
1041
+ onCancel: () => setMode({ type: "normal" })
1042
+ }
1043
+ )
1044
+ }
1045
+ );
1046
+ }
1047
+ if (mode.type === "confirm-encrypt") {
1048
+ const isEncrypted = selectedFile.encrypted;
1049
+ return /* @__PURE__ */ jsx7(
1050
+ Layout,
1051
+ {
1052
+ files,
1053
+ fileIndex,
1054
+ keys,
1055
+ keyIndex,
1056
+ focus,
1057
+ revealed,
1058
+ onSelectFile: selectFile,
1059
+ onSelectKey: setKeyIndex,
1060
+ statusMsg,
1061
+ focus2: focus,
1062
+ extra: /* @__PURE__ */ jsx7(
1063
+ ConfirmEncrypt,
1064
+ {
1065
+ decrypt: isEncrypted,
1066
+ fileName: selectedFile.relativePath,
1067
+ onConfirm: () => {
1068
+ try {
1069
+ if (isEncrypted) {
1070
+ decryptFile(selectedFile.path);
1071
+ flash(`Decrypted ${selectedFile.relativePath}`);
1072
+ } else {
1073
+ encryptFile(selectedFile.path);
1074
+ flash(`Encrypted ${selectedFile.relativePath}`);
1075
+ }
1076
+ } catch (err) {
1077
+ flash(`Error: ${err instanceof Error ? err.message : String(err)}`);
1078
+ }
1079
+ refreshKeys();
1080
+ setRevealed(/* @__PURE__ */ new Map());
1081
+ setMode({ type: "normal" });
1082
+ },
1083
+ onCancel: () => setMode({ type: "normal" })
1084
+ }
1085
+ )
1086
+ }
1087
+ );
1088
+ }
1089
+ if (mode.type === "help") {
1090
+ return /* @__PURE__ */ jsx7(HelpOverlay, { onClose: () => setMode({ type: "normal" }) });
1091
+ }
1092
+ if (mode.type === "diff") {
1093
+ return /* @__PURE__ */ jsx7(
1094
+ DiffView,
1095
+ {
1096
+ left: selectedFile,
1097
+ files,
1098
+ onClose: () => setMode({ type: "normal" })
1099
+ }
1100
+ );
1101
+ }
1102
+ return /* @__PURE__ */ jsx7(
1103
+ Layout,
1104
+ {
1105
+ files,
1106
+ fileIndex,
1107
+ keys,
1108
+ keyIndex,
1109
+ focus,
1110
+ revealed,
1111
+ onSelectFile: selectFile,
1112
+ onSelectKey: setKeyIndex,
1113
+ statusMsg,
1114
+ focus2: focus
1115
+ }
1116
+ );
1117
+ }
1118
+ function Layout({
1119
+ files,
1120
+ fileIndex,
1121
+ keys,
1122
+ keyIndex,
1123
+ focus,
1124
+ revealed,
1125
+ onSelectFile,
1126
+ onSelectKey,
1127
+ statusMsg,
1128
+ extra
1129
+ }) {
1130
+ const selectedFile = files[fileIndex];
1131
+ const encCount = files.filter((f) => f.encrypted).length;
1132
+ return /* @__PURE__ */ jsxs6(Box7, { flexDirection: "column", height: "100%", children: [
1133
+ /* @__PURE__ */ jsxs6(Box7, { paddingX: 1, children: [
1134
+ /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "dotenvx-ui" }),
1135
+ /* @__PURE__ */ jsxs6(Text7, { dimColor: true, children: [
1136
+ " ",
1137
+ selectedFile.relativePath,
1138
+ " \xB7 ",
1139
+ files.length,
1140
+ " files \xB7 ",
1141
+ encCount,
1142
+ " enc"
1143
+ ] })
1144
+ ] }),
1145
+ /* @__PURE__ */ jsxs6(Box7, { flexGrow: 1, children: [
1146
+ /* @__PURE__ */ jsx7(FileList, { files, selectedIndex: fileIndex, focused: focus === "files", onSelect: onSelectFile }),
1147
+ /* @__PURE__ */ jsx7(
1148
+ KeyTable,
1149
+ {
1150
+ file: selectedFile,
1151
+ keys,
1152
+ selectedIndex: keyIndex,
1153
+ focused: focus === "keys",
1154
+ revealed,
1155
+ onSelect: onSelectKey
1156
+ }
1157
+ )
1158
+ ] }),
1159
+ extra,
1160
+ /* @__PURE__ */ jsx7(StatusBar, { focus, message: statusMsg })
1161
+ ] });
1162
+ }
1163
+ function ConfirmDelete({ keyName, onConfirm, onCancel }) {
1164
+ const { isRawModeSupported } = useStdin6();
1165
+ useInput6((input) => {
1166
+ if (input === "y" || input === "Y") onConfirm();
1167
+ else onCancel();
1168
+ }, { isActive: isRawModeSupported });
1169
+ return /* @__PURE__ */ jsxs6(Box7, { paddingX: 1, children: [
1170
+ /* @__PURE__ */ jsxs6(Text7, { color: "red", children: [
1171
+ "Delete ",
1172
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: keyName }),
1173
+ "? "
1174
+ ] }),
1175
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y confirm any other key cancel" })
1176
+ ] });
1177
+ }
1178
+ function ConfirmEncrypt({ decrypt, fileName, onConfirm, onCancel }) {
1179
+ const { isRawModeSupported } = useStdin6();
1180
+ useInput6((input) => {
1181
+ if (input === "y" || input === "Y") onConfirm();
1182
+ else onCancel();
1183
+ }, { isActive: isRawModeSupported });
1184
+ const action = decrypt ? "Decrypt" : "Encrypt";
1185
+ const color = decrypt ? "yellow" : "green";
1186
+ return /* @__PURE__ */ jsxs6(Box7, { paddingX: 1, children: [
1187
+ /* @__PURE__ */ jsxs6(Text7, { color, children: [
1188
+ action,
1189
+ " ",
1190
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: fileName }),
1191
+ "? "
1192
+ ] }),
1193
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y confirm any other key cancel" })
1194
+ ] });
1195
+ }
1196
+ function ConfirmAddEncrypt({ keyName, onEncrypt, onPlain, onCancel }) {
1197
+ const { isRawModeSupported } = useStdin6();
1198
+ useInput6((input, key) => {
1199
+ if (key.escape) {
1200
+ onCancel();
1201
+ return;
1202
+ }
1203
+ if (input === "y" || input === "Y") {
1204
+ onEncrypt();
1205
+ return;
1206
+ }
1207
+ if (input === "n" || input === "N" || key.return) {
1208
+ onPlain();
1209
+ return;
1210
+ }
1211
+ }, { isActive: isRawModeSupported });
1212
+ return /* @__PURE__ */ jsxs6(Box7, { paddingX: 1, children: [
1213
+ /* @__PURE__ */ jsxs6(Text7, { children: [
1214
+ "Encrypt ",
1215
+ /* @__PURE__ */ jsx7(Text7, { bold: true, children: keyName }),
1216
+ "? "
1217
+ ] }),
1218
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y encrypt n plain esc cancel" })
1219
+ ] });
1220
+ }
1221
+
1222
+ // src/tui/ErrorBoundary.tsx
1223
+ import React4 from "react";
1224
+ import { Box as Box8, Text as Text8 } from "ink";
1225
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
1226
+ var ErrorBoundary = class extends React4.Component {
1227
+ state = { error: null };
1228
+ static getDerivedStateFromError(error) {
1229
+ return { error };
1230
+ }
1231
+ render() {
1232
+ if (this.state.error) {
1233
+ return /* @__PURE__ */ jsxs7(Box8, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
1234
+ /* @__PURE__ */ jsx8(Text8, { bold: true, color: "red", children: "dotenvx-ui crashed" }),
1235
+ /* @__PURE__ */ jsx8(Text8, { children: this.state.error.message }),
1236
+ /* @__PURE__ */ jsx8(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text8, { dimColor: true, children: "Please report this at https://github.com/alxbrla/dotenvx-ui/issues" }) })
1237
+ ] });
1238
+ }
1239
+ return this.props.children;
1240
+ }
1241
+ };
1242
+
1243
+ // src/cli.tsx
1244
+ import { createRequire as createRequire2 } from "module";
1245
+ import { jsx as jsx9 } from "react/jsx-runtime";
1246
+ var { version } = createRequire2(import.meta.url)("../package.json");
1247
+ var HELP = `
1248
+ dotenvx-ui \u2014 terminal and web UI for dotenvx environment files
1249
+
1250
+ Usage:
1251
+ dotenvx-ui Launch TUI
1252
+ dotenvx-ui ui Launch web UI in browser
1253
+
1254
+ Options:
1255
+ -v, --version Print version
1256
+ -h, --help Show this help
1257
+ `;
1258
+ var commands = {
1259
+ "--version": () => {
1260
+ console.log(version);
1261
+ process.exit(0);
1262
+ },
1263
+ "-v": () => {
1264
+ console.log(version);
1265
+ process.exit(0);
1266
+ },
1267
+ "--help": () => {
1268
+ console.log(HELP);
1269
+ process.exit(0);
1270
+ },
1271
+ "-h": () => {
1272
+ console.log(HELP);
1273
+ process.exit(0);
1274
+ },
1275
+ "ui": runWebUI
1276
+ };
1277
+ var [, , command] = process.argv;
1278
+ if (command !== void 0 && !(command in commands)) {
1279
+ console.error(`Unknown command: ${command}
1280
+ Run dotenvx-ui --help for usage.`);
1281
+ process.exit(1);
1282
+ }
1283
+ commands[command ?? ""]?.() ?? runTUI();
1284
+ function runTUI() {
1285
+ const files = scan(process.cwd());
1286
+ if (files.length === 0) {
1287
+ console.error("No .env files found in this directory.");
1288
+ process.exit(1);
1289
+ }
1290
+ render(/* @__PURE__ */ jsx9(ErrorBoundary, { children: /* @__PURE__ */ jsx9(App, { files }) }));
1291
+ }
1292
+ function runWebUI() {
1293
+ console.log("Web UI \u2014 coming soon");
1294
+ process.exit(0);
1295
+ }