dotenvx-ui 0.1.1 → 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.
package/dist/cli.js CHANGED
@@ -1,141 +1,34 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ addKey,
4
+ decryptAllValues,
5
+ decryptFile,
6
+ decryptValue,
7
+ encryptFile,
8
+ encryptKey,
9
+ isEncryptedValue,
10
+ readEnvFile,
11
+ removeKey,
12
+ scan,
13
+ updateKey
14
+ } from "./chunk-GSBFG44K.js";
2
15
 
3
16
  // src/cli.tsx
17
+ import { createRequire } from "module";
4
18
  import { render } from "ink";
5
19
 
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
20
  // src/tui/App.tsx
95
- import { useState as useState4 } from "react";
96
- import { Box as Box7, Text as Text7, useApp, useInput as useInput6, useStdin as useStdin6 } from "ink";
97
21
  import clipboard from "clipboardy";
22
+ import { Box as Box7, Text as Text7, useApp, useInput as useInput6, useStdin as useStdin6 } from "ink";
23
+ import { useState as useState4 } from "react";
98
24
 
99
- // src/tui/FileList.tsx
25
+ // src/tui/DiffView.tsx
100
26
  import { Box, Text, useInput, useStdin } from "ink";
101
- import { jsx, jsxs } from "react/jsx-runtime";
102
- function FileList({ files, selectedIndex, focused, interactive, 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 && interactive });
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";
27
+ import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
135
28
 
136
29
  // src/tui/useTerminalRows.ts
137
- import { useEffect, useState } from "react";
138
30
  import { useStdout } from "ink";
31
+ import { useEffect, useState } from "react";
139
32
  function useTerminalRows() {
140
33
  const { stdout } = useStdout();
141
34
  const [rows, setRows] = useState(stdout?.rows ?? 24);
@@ -163,7 +56,8 @@ function useTerminalCols() {
163
56
  return cols;
164
57
  }
165
58
  function scrollWindow(length, selectedIndex, maxVisible) {
166
- if (length <= maxVisible) return { start: 0, end: length, above: 0, below: 0 };
59
+ if (length <= maxVisible)
60
+ return { start: 0, end: length, above: 0, below: 0 };
167
61
  const start = Math.min(
168
62
  Math.max(0, selectedIndex - Math.floor(maxVisible / 2)),
169
63
  length - maxVisible
@@ -172,444 +66,8 @@ function scrollWindow(length, selectedIndex, maxVisible) {
172
66
  return { start, end, above: start, below: length - end };
173
67
  }
174
68
 
175
- // src/tui/KeyTable.tsx
176
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
177
- var SECRET_PATTERN = /secret|password|token|key|private|api_?key/i;
178
- var MAX_INLINE = 48;
179
- function truncate(s) {
180
- const first = s.split("\n")[0];
181
- return first.length > MAX_INLINE ? first.slice(0, MAX_INLINE - 1) + "\u2026" : first;
182
- }
183
- function maskValue(k, revealed) {
184
- if (revealed.has(k.key)) return truncate(revealed.get(k.key));
185
- if (k.encrypted) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
186
- if (SECRET_PATTERN.test(k.key)) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
187
- return truncate(k.value);
188
- }
189
- function KeyTable({ file, keys, selectedIndex, focused, interactive, revealed, onSelect, maxRows }) {
190
- const { isRawModeSupported } = useStdin2();
191
- useInput2((_, key) => {
192
- if (!focused) return;
193
- if (key.upArrow) onSelect(Math.max(0, selectedIndex - 1));
194
- if (key.downArrow) onSelect(Math.min(keys.length - 1, selectedIndex + 1));
195
- }, { isActive: isRawModeSupported && interactive });
196
- const maxVisible = Math.max(3, maxRows - 3);
197
- const { start, end, above, below } = scrollWindow(keys.length, selectedIndex, maxVisible);
198
- const visibleKeys = keys.slice(start, end);
199
- const keyColWidth = Math.min(48, Math.max(16, ...keys.map((k) => k.key.length))) + 2;
200
- const encBadge = file.encrypted ? /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: " encrypted" }) : /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " plain" });
201
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", flexGrow: 1, children: [
202
- /* @__PURE__ */ jsxs2(Box2, { paddingX: 1, children: [
203
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: file.relativePath }),
204
- encBadge,
205
- /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
206
- " ",
207
- keys.length,
208
- " key",
209
- keys.length === 1 ? "" : "s"
210
- ] })
211
- ] }),
212
- keys.length === 0 ? /* @__PURE__ */ jsx2(Box2, { paddingX: 2, marginTop: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
213
- "No keys found. Press ",
214
- /* @__PURE__ */ jsx2(Text2, { bold: true, children: "a" }),
215
- " to add one."
216
- ] }) }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
217
- above > 0 && /* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
218
- "\u2191 ",
219
- above,
220
- " more"
221
- ] }) }),
222
- visibleKeys.map((k, i) => {
223
- const idx = start + i;
224
- const selected = idx === selectedIndex;
225
- const value = maskValue(k, revealed);
226
- const lockIcon = k.encrypted && !revealed.has(k.key) ? " \u{1F512}" : "";
227
- return /* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsxs2(
228
- Text2,
229
- {
230
- backgroundColor: selected && focused ? "blue" : void 0,
231
- color: selected && focused ? "white" : selected ? "cyan" : void 0,
232
- children: [
233
- k.key.padEnd(keyColWidth),
234
- /* @__PURE__ */ jsx2(Text2, { dimColor: !selected, children: value }),
235
- lockIcon
236
- ]
237
- }
238
- ) }, k.key);
239
- }),
240
- below > 0 && /* @__PURE__ */ jsx2(Box2, { paddingX: 1, children: /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
241
- "\u2193 ",
242
- below,
243
- " more"
244
- ] }) })
245
- ] })
246
- ] });
247
- }
248
-
249
- // src/tui/StatusBar.tsx
250
- import { Box as Box3, Text as Text3 } from "ink";
251
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
252
- var FILE_HINTS = [
253
- { key: "\u2191\u2193", action: "navigate" },
254
- { key: "tab", action: "switch panel" },
255
- { key: "?", action: "help" },
256
- { key: "q", action: "quit" }
257
- ];
258
- var KEY_HINTS = [
259
- { key: "\u2191\u2193", action: "navigate" },
260
- { key: "tab", action: "switch" },
261
- { key: "enter", action: "edit" },
262
- { key: "y", action: "copy" },
263
- { key: "r", action: "reveal" },
264
- { key: "a", action: "add" },
265
- { key: "D", action: "delete" },
266
- { key: "d", action: "diff" },
267
- { key: "?", action: "help" },
268
- { key: "q", action: "quit" }
269
- ];
270
- function Hints({ hints }) {
271
- return /* @__PURE__ */ jsx3(Box3, { gap: 2, children: hints.map(({ key, action }) => /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
272
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: key }),
273
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: action })
274
- ] }, key)) });
275
- }
276
- function StatusBar({ message, focus }) {
277
- return /* @__PURE__ */ jsx3(Box3, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingX: 1, children: message ? /* @__PURE__ */ jsx3(Text3, { children: message }) : /* @__PURE__ */ jsx3(Hints, { hints: focus === "files" ? FILE_HINTS : KEY_HINTS }) });
278
- }
279
-
280
69
  // src/tui/DiffView.tsx
281
- import { useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
282
- import { Box as Box4, Text as Text4, useInput as useInput3, useStdin as useStdin3 } from "ink";
283
-
284
- // src/core/parser/io.ts
285
- import { readFileSync as readFileSync2, writeFileSync, renameSync } from "fs";
286
- import { join as join2, dirname as dirname2 } from "path";
287
- import { randomBytes } from "crypto";
288
-
289
- // src/core/parser/values.ts
290
- function isEncryptedValue(value) {
291
- return value.startsWith("encrypted:");
292
- }
293
- function parseValue(rawValue, allLines, nextLineIdx) {
294
- const trimmed = rawValue.trim();
295
- if (trimmed.startsWith('"')) {
296
- const inner = trimmed.slice(1);
297
- const closeIdx = findClosingQuote(inner);
298
- if (closeIdx !== -1) {
299
- return { value: unescape(inner.slice(0, closeIdx)), extraLines: [] };
300
- }
301
- const valueLines = [inner];
302
- let idx = nextLineIdx;
303
- while (idx < allLines.length) {
304
- const continuation = allLines[idx];
305
- const close = findClosingQuote(continuation);
306
- if (close !== -1) {
307
- valueLines.push(continuation.slice(0, close));
308
- return {
309
- value: valueLines.join("\n"),
310
- extraLines: allLines.slice(nextLineIdx, idx + 1)
311
- };
312
- }
313
- valueLines.push(continuation);
314
- idx++;
315
- }
316
- return { value: valueLines.join("\n"), extraLines: allLines.slice(nextLineIdx, idx) };
317
- }
318
- if (trimmed.startsWith("'")) {
319
- const inner = trimmed.slice(1);
320
- const closeIdx = inner.indexOf("'");
321
- return {
322
- value: closeIdx !== -1 ? inner.slice(0, closeIdx) : inner,
323
- extraLines: []
324
- };
325
- }
326
- const commentIdx = trimmed.indexOf(" #");
327
- const bare = commentIdx !== -1 ? trimmed.slice(0, commentIdx) : trimmed;
328
- return { value: bare, extraLines: [] };
329
- }
330
- function serializeKeyValue(key, value) {
331
- if (value.includes("\n")) {
332
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\r/g, "\\r");
333
- return `${key}="${escaped}"`;
334
- }
335
- if (value === "" || /[\s#"'`]/.test(value)) {
336
- return `${key}="${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
337
- }
338
- return `${key}=${value}`;
339
- }
340
- function findClosingQuote(s) {
341
- for (let i = 0; i < s.length; i++) {
342
- if (s[i] === "\\") {
343
- i++;
344
- continue;
345
- }
346
- if (s[i] === '"') return i;
347
- }
348
- return -1;
349
- }
350
- function unescape(s) {
351
- return s.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\r/g, "\r").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
352
- }
353
-
354
- // src/core/parser/io.ts
355
- function readEnvFile(filePath) {
356
- const content = readFileSync2(filePath, "utf8");
357
- return parse(content).filter((e) => e.type === "key").map((e) => ({
358
- key: e.key,
359
- value: e.value,
360
- encrypted: isEncryptedValue(e.value),
361
- comment: extractLeadingComment(e.lines)
362
- }));
363
- }
364
- function writeEnvFile(filePath, keys) {
365
- const content = readFileSync2(filePath, "utf8");
366
- const entries = parse(content);
367
- const updates = new Map(keys.map((k) => [k.key, k]));
368
- const outLines = [];
369
- const written = /* @__PURE__ */ new Set();
370
- for (const entry of entries) {
371
- if (entry.type === "raw") {
372
- outLines.push(entry.text);
373
- continue;
374
- }
375
- const update = updates.get(entry.key);
376
- if (!update) continue;
377
- written.add(entry.key);
378
- const leadingComments = getLeadingCommentLines(entry.lines);
379
- outLines.push(...leadingComments);
380
- if (update.value === entry.value) {
381
- const keyLines = entry.lines.filter((l) => !l.trimStart().startsWith("#"));
382
- outLines.push(...keyLines);
383
- } else {
384
- outLines.push(serializeKeyValue(entry.key, update.value));
385
- }
386
- }
387
- for (const k of keys) {
388
- if (!written.has(k.key)) {
389
- if (k.comment) outLines.push(`# ${k.comment}`);
390
- outLines.push(serializeKeyValue(k.key, k.value));
391
- }
392
- }
393
- const output = outLines.join("\n") + (content.endsWith("\n") ? "\n" : "");
394
- atomicWrite(filePath, output);
395
- }
396
- function addKey(filePath, key, value) {
397
- const keys = readEnvFile(filePath);
398
- if (keys.some((k) => k.key === key)) {
399
- throw new Error(`Key "${key}" already exists in ${filePath}`);
400
- }
401
- keys.push({ key, value, encrypted: isEncryptedValue(value) });
402
- writeEnvFile(filePath, keys);
403
- }
404
- function updateKey(filePath, key, value) {
405
- const keys = readEnvFile(filePath);
406
- const idx = keys.findIndex((k) => k.key === key);
407
- if (idx === -1) throw new Error(`Key "${key}" not found in ${filePath}`);
408
- keys[idx] = { ...keys[idx], key, value, encrypted: isEncryptedValue(value) };
409
- writeEnvFile(filePath, keys);
410
- }
411
- function removeKey(filePath, key) {
412
- const keys = readEnvFile(filePath).filter((k) => k.key !== key);
413
- writeEnvFile(filePath, keys);
414
- }
415
- function parse(content) {
416
- const entries = [];
417
- const lines = content.split("\n");
418
- if (lines[lines.length - 1] === "") lines.pop();
419
- let i = 0;
420
- let pendingComments = [];
421
- while (i < lines.length) {
422
- const line = lines[i];
423
- if (line.trim() === "") {
424
- for (const c of pendingComments) entries.push({ type: "raw", text: c });
425
- pendingComments = [];
426
- entries.push({ type: "raw", text: line });
427
- i++;
428
- continue;
429
- }
430
- if (line.trimStart().startsWith("#")) {
431
- pendingComments.push(line);
432
- i++;
433
- continue;
434
- }
435
- const eqIdx = line.indexOf("=");
436
- if (eqIdx === -1) {
437
- for (const c of pendingComments) entries.push({ type: "raw", text: c });
438
- pendingComments = [];
439
- entries.push({ type: "raw", text: line });
440
- i++;
441
- continue;
442
- }
443
- const key = line.slice(0, eqIdx).trim();
444
- const rawValue = line.slice(eqIdx + 1);
445
- const { value, extraLines } = parseValue(rawValue, lines, i + 1);
446
- entries.push({ type: "key", key, value, lines: [...pendingComments, line, ...extraLines] });
447
- pendingComments = [];
448
- i += 1 + extraLines.length;
449
- }
450
- for (const c of pendingComments) entries.push({ type: "raw", text: c });
451
- return entries;
452
- }
453
- function getLeadingCommentLines(lines) {
454
- const result = [];
455
- for (const l of lines) {
456
- if (l.trimStart().startsWith("#")) result.push(l);
457
- else break;
458
- }
459
- return result;
460
- }
461
- function extractLeadingComment(lines) {
462
- const comments = getLeadingCommentLines(lines).map(
463
- (l) => l.trimStart().slice(1).trim()
464
- );
465
- return comments.length > 0 ? comments.join("\n") : void 0;
466
- }
467
- function atomicWrite(filePath, content) {
468
- const tmp = join2(
469
- dirname2(filePath),
470
- `.dotenvx-ui-tmp-${randomBytes(6).toString("hex")}`
471
- );
472
- try {
473
- writeFileSync(tmp, content, { encoding: "utf8", flag: "wx" });
474
- renameSync(tmp, filePath);
475
- } catch (err) {
476
- try {
477
- writeFileSync(tmp, "");
478
- } catch {
479
- }
480
- throw new Error(`Failed to write ${filePath}: ${err.message}`);
481
- }
482
- }
483
-
484
- // src/core/dotenvx.ts
485
- import { createRequire } from "module";
486
- import { existsSync, readFileSync as readFileSync3 } from "fs";
487
- import { join as join3, dirname as dirname3 } from "path";
488
- var dotenvx = createRequire(import.meta.url)("@dotenvx/dotenvx");
489
- function decryptValue(encryptedValue, envFilePath) {
490
- if (!isEncryptedValue(encryptedValue)) return encryptedValue;
491
- const keyName = findKeyForValue(encryptedValue, envFilePath);
492
- if (!keyName) return null;
493
- const keysFile = findKeysFile(envFilePath);
494
- return silenced(() => {
495
- const result = dotenvx.get(keyName, {
496
- path: envFilePath,
497
- ...keysFile ? { envKeysFile: keysFile } : {},
498
- logLevel: "error"
499
- });
500
- return result ?? null;
501
- });
502
- }
503
- function decryptAllValues(envFilePath) {
504
- let raw;
505
- try {
506
- raw = readFileSync3(envFilePath, "utf8");
507
- } catch {
508
- return {};
509
- }
510
- let privateKey = process.env["DOTENV_PRIVATE_KEY"] ?? null;
511
- if (!privateKey) {
512
- const keysFile = findKeysFile(envFilePath);
513
- if (keysFile) {
514
- try {
515
- const keypairs = dotenvx.keypair(envFilePath, void 0, keysFile);
516
- for (const [name, value] of Object.entries(keypairs)) {
517
- if (name.startsWith("DOTENV_PRIVATE_KEY") && value) {
518
- privateKey = value;
519
- break;
520
- }
521
- }
522
- } catch {
523
- }
524
- }
525
- }
526
- return silenced(() => {
527
- try {
528
- return dotenvx.parse(raw, {
529
- ...privateKey ? { privateKey } : {},
530
- processEnv: {}
531
- });
532
- } catch {
533
- return {};
534
- }
535
- });
536
- }
537
- var DOTENVX_INTERNAL_KEYS = /* @__PURE__ */ new Set(["DOTENV_PUBLIC_KEY", "DOTENV_PRIVATE_KEY"]);
538
- function encryptFile(envFilePath) {
539
- const keys = readEnvFile(envFilePath);
540
- for (const k of keys) {
541
- if (!isEncryptedValue(k.value) && !DOTENVX_INTERNAL_KEYS.has(k.key)) {
542
- dotenvx.set(k.key, k.value, {
543
- path: envFilePath,
544
- encrypt: true,
545
- logLevel: "error"
546
- });
547
- }
548
- }
549
- }
550
- function encryptKey(envFilePath, keyName, plainValue) {
551
- dotenvx.set(keyName, plainValue, {
552
- path: envFilePath,
553
- encrypt: true,
554
- logLevel: "error"
555
- });
556
- }
557
- function decryptFile(envFilePath) {
558
- const keys = readEnvFile(envFilePath);
559
- const decrypted = decryptAllValues(envFilePath);
560
- for (const k of keys) {
561
- if (isEncryptedValue(k.value)) {
562
- const plain = decrypted[k.key];
563
- if (plain !== void 0 && !isEncryptedValue(plain)) updateKey(envFilePath, k.key, plain);
564
- }
565
- }
566
- }
567
- function silenced(fn) {
568
- const origWrite = process.stderr.write.bind(process.stderr);
569
- const origOut = process.stdout.write.bind(process.stdout);
570
- const mute = (chunk) => {
571
- const s = String(chunk);
572
- if (s.includes("[MISSING_PRIVATE_KEY]") || s.includes("could not decrypt") || s.includes("\u2620")) return true;
573
- return false;
574
- };
575
- process.stderr.write = ((chunk, ...args) => mute(chunk) ? true : origWrite(chunk, ...args));
576
- process.stdout.write = ((chunk, ...args) => mute(chunk) ? true : origOut(chunk, ...args));
577
- try {
578
- return fn();
579
- } finally {
580
- process.stderr.write = origWrite;
581
- process.stdout.write = origOut;
582
- }
583
- }
584
- function findKeysFile(envFilePath) {
585
- let dir = dirname3(envFilePath);
586
- while (true) {
587
- const candidate = join3(dir, ".env.keys");
588
- if (existsSync(candidate)) return candidate;
589
- const parent = dirname3(dir);
590
- if (parent === dir) break;
591
- dir = parent;
592
- }
593
- return null;
594
- }
595
- function findKeyForValue(encryptedValue, envFilePath) {
596
- let raw;
597
- try {
598
- raw = readFileSync3(envFilePath, "utf8");
599
- } catch {
600
- return null;
601
- }
602
- for (const line of raw.split("\n")) {
603
- const eqIdx = line.indexOf("=");
604
- if (eqIdx === -1) continue;
605
- const lineValue = line.slice(eqIdx + 1).trim();
606
- if (lineValue === encryptedValue) return line.slice(0, eqIdx).trim();
607
- }
608
- return null;
609
- }
610
-
611
- // src/tui/DiffView.tsx
612
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
70
+ import { jsx, jsxs } from "react/jsx-runtime";
613
71
  var COL_VAL = 26;
614
72
  var PICKER_MAX = 5;
615
73
  function buildDisplayMap(keys, filePath) {
@@ -618,7 +76,10 @@ function buildDisplayMap(keys, filePath) {
618
76
  for (const k of keys) {
619
77
  if (k.encrypted) {
620
78
  const plain = decrypted[k.key];
621
- out.set(k.key, plain !== void 0 && !isEncryptedValue(plain) ? plain : "\u{1F512}");
79
+ out.set(
80
+ k.key,
81
+ plain !== void 0 && !isEncryptedValue(plain) ? plain : "\u{1F512}"
82
+ );
622
83
  } else {
623
84
  out.set(k.key, k.value);
624
85
  }
@@ -645,7 +106,7 @@ function buildRows(leftMap, rightMap) {
645
106
  }
646
107
  function trunc(s, max) {
647
108
  const first = s.split("\n")[0];
648
- return first.length > max ? first.slice(0, max - 1) + "\u2026" : first;
109
+ return first.length > max ? `${first.slice(0, max - 1)}\u2026` : first;
649
110
  }
650
111
  function safeRead(file) {
651
112
  try {
@@ -655,40 +116,43 @@ function safeRead(file) {
655
116
  }
656
117
  }
657
118
  function DiffView({ left, files, onClose }) {
658
- const { isRawModeSupported } = useStdin3();
119
+ const { isRawModeSupported } = useStdin();
659
120
  const termRows = useTerminalRows();
660
121
  const others = files.filter((f) => f.path !== left.path);
661
122
  const [pickerIndex, setPickerIndex] = useState2(0);
662
123
  const [rowScroll, setRowScroll] = useState2(0);
663
124
  const rightFile = others[pickerIndex] ?? null;
664
125
  const mapsCache = useRef(/* @__PURE__ */ new Map());
665
- const [cacheVersion, setCacheVersion] = useState2(0);
126
+ const [_cacheVersion, setCacheVersion] = useState2(0);
666
127
  function getMap(file) {
667
128
  return mapsCache.current.get(file.path) ?? null;
668
129
  }
669
130
  function buildAndCache(file) {
670
131
  if (!mapsCache.current.has(file.path)) {
671
- mapsCache.current.set(file.path, buildDisplayMap(safeRead(file), file.path));
132
+ mapsCache.current.set(
133
+ file.path,
134
+ buildDisplayMap(safeRead(file), file.path)
135
+ );
672
136
  setCacheVersion((v) => v + 1);
673
137
  }
674
138
  }
675
139
  useEffect2(() => {
676
140
  buildAndCache(left);
677
141
  if (others[0]) buildAndCache(others[0]);
678
- }, []);
142
+ }, [left, others[0], buildAndCache]);
679
143
  useEffect2(() => {
680
144
  const next = others[pickerIndex + 1];
681
145
  const prev = others[pickerIndex - 1];
682
146
  if (next) buildAndCache(next);
683
147
  if (prev) buildAndCache(prev);
684
- }, [pickerIndex]);
148
+ }, [pickerIndex, others, buildAndCache]);
685
149
  const rows = useMemo(() => {
686
150
  if (!rightFile) return [];
687
151
  const leftMap = getMap(left);
688
152
  const rightMap = getMap(rightFile);
689
153
  if (!leftMap || !rightMap) return [];
690
154
  return buildRows(leftMap, rightMap);
691
- }, [left.path, rightFile?.path, cacheVersion]);
155
+ }, [left.path, rightFile?.path, getMap, left, rightFile]);
692
156
  const pickerVisible = Math.min(others.length, PICKER_MAX);
693
157
  const picker = scrollWindow(others.length, pickerIndex, PICKER_MAX);
694
158
  const chrome = 9 + pickerVisible + (picker.above > 0 || picker.below > 0 ? 2 : 0);
@@ -698,40 +162,47 @@ function DiffView({ left, files, onClose }) {
698
162
  const visibleRows = rows.slice(scroll, scroll + maxRows);
699
163
  const rowsAbove = scroll;
700
164
  const rowsBelow = rows.length - (scroll + visibleRows.length);
701
- useInput3((input, key) => {
702
- if (key.escape || input === "q") {
703
- onClose();
704
- return;
705
- }
706
- if (key.upArrow) {
707
- setPickerIndex((i) => Math.max(0, i - 1));
708
- setRowScroll(0);
709
- return;
710
- }
711
- if (key.downArrow) {
712
- setPickerIndex((i) => Math.min(others.length - 1, i + 1));
713
- setRowScroll(0);
714
- return;
715
- }
716
- if (input === "k") setRowScroll(Math.max(0, scroll - 1));
717
- if (input === "j") setRowScroll(Math.min(maxScroll, scroll + 1));
718
- if (key.pageUp) setRowScroll(Math.max(0, scroll - maxRows));
719
- if (key.pageDown) setRowScroll(Math.min(maxScroll, scroll + maxRows));
720
- }, { isActive: isRawModeSupported });
165
+ useInput(
166
+ (input, key) => {
167
+ if (key.escape || input === "q") {
168
+ onClose();
169
+ return;
170
+ }
171
+ if (key.upArrow) {
172
+ setPickerIndex((i) => Math.max(0, i - 1));
173
+ setRowScroll(0);
174
+ return;
175
+ }
176
+ if (key.downArrow) {
177
+ setPickerIndex((i) => Math.min(others.length - 1, i + 1));
178
+ setRowScroll(0);
179
+ return;
180
+ }
181
+ if (input === "k") setRowScroll(Math.max(0, scroll - 1));
182
+ if (input === "j") setRowScroll(Math.min(maxScroll, scroll + 1));
183
+ if (key.pageUp) setRowScroll(Math.max(0, scroll - maxRows));
184
+ if (key.pageDown) setRowScroll(Math.min(maxScroll, scroll + maxRows));
185
+ },
186
+ { isActive: isRawModeSupported }
187
+ );
188
+ const keyColWidth = Math.min(48, Math.max(16, ...rows.map((r) => r.key.length))) + 2;
721
189
  const leftName = trunc(left.relativePath, COL_VAL);
722
190
  const rightName = rightFile ? trunc(rightFile.relativePath, COL_VAL) : "\u2014";
723
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
724
- /* @__PURE__ */ jsxs4(Box4, { paddingX: 1, children: [
725
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "dotenvx-ui " }),
726
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
727
- "diff ",
191
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
192
+ /* @__PURE__ */ jsxs(Box, { paddingX: 1, children: [
193
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
194
+ "dotenvx-ui",
195
+ " "
196
+ ] }),
197
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
198
+ "diff ",
728
199
  left.relativePath,
729
- " \u2194 ",
200
+ " \u2194 ",
730
201
  rightFile?.relativePath ?? "\u2014"
731
202
  ] })
732
203
  ] }),
733
- /* @__PURE__ */ jsxs4(
734
- Box4,
204
+ /* @__PURE__ */ jsxs(
205
+ Box,
735
206
  {
736
207
  flexDirection: "column",
737
208
  paddingX: 1,
@@ -741,16 +212,16 @@ function DiffView({ left, files, onClose }) {
741
212
  borderLeft: false,
742
213
  borderRight: false,
743
214
  children: [
744
- /* @__PURE__ */ jsx4(Text4, { bold: true, dimColor: true, children: "compare with" }),
745
- picker.above > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
746
- " \u2191 ",
215
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "compare with" }),
216
+ picker.above > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
217
+ " \u2191 ",
747
218
  picker.above,
748
219
  " more"
749
220
  ] }),
750
221
  others.slice(picker.start, picker.end).map((f, i) => {
751
222
  const selected = picker.start + i === pickerIndex;
752
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(
753
- Text4,
223
+ return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(
224
+ Text,
754
225
  {
755
226
  backgroundColor: selected ? "blue" : void 0,
756
227
  color: selected ? "white" : void 0,
@@ -761,59 +232,136 @@ function DiffView({ left, files, onClose }) {
761
232
  }
762
233
  ) }, f.path);
763
234
  }),
764
- picker.below > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
765
- " \u2193 ",
235
+ picker.below > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
236
+ " \u2193 ",
766
237
  picker.below,
767
238
  " more"
768
239
  ] }),
769
- others.length === 0 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " no other files" })
240
+ others.length === 0 && /* @__PURE__ */ jsx(Text, { dimColor: true, children: " no other files" })
770
241
  ]
771
242
  }
772
243
  ),
773
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
774
- /* @__PURE__ */ jsxs4(Box4, { borderStyle: "single", borderBottom: true, borderTop: false, borderLeft: false, borderRight: false, children: [
775
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "KEY".padEnd(22) }),
776
- /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
777
- " ",
778
- leftName.padEnd(COL_VAL + 2)
779
- ] }),
780
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: rightName })
781
- ] }),
782
- rowsAbove > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
244
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
245
+ /* @__PURE__ */ jsxs(
246
+ Box,
247
+ {
248
+ borderStyle: "single",
249
+ borderBottom: true,
250
+ borderTop: false,
251
+ borderLeft: false,
252
+ borderRight: false,
253
+ children: [
254
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "KEY".padEnd(keyColWidth) }),
255
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
256
+ " ",
257
+ leftName.padEnd(COL_VAL + 2)
258
+ ] }),
259
+ /* @__PURE__ */ jsx(Text, { bold: true, children: rightName })
260
+ ]
261
+ }
262
+ ),
263
+ rowsAbove > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
783
264
  "\u2191 ",
784
265
  rowsAbove,
785
266
  " more"
786
267
  ] }),
787
- visibleRows.map((row) => /* @__PURE__ */ jsx4(DiffRow, { row }, row.key)),
788
- rowsBelow > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
268
+ visibleRows.map((row) => /* @__PURE__ */ jsx(DiffRow, { row, keyColWidth }, row.key)),
269
+ rowsBelow > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
789
270
  "\u2193 ",
790
271
  rowsBelow,
791
272
  " more"
792
273
  ] }),
793
- rows.length === 0 && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Select a file to compare." }) })
274
+ rows.length === 0 && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Select a file to compare." }) })
794
275
  ] }),
795
- /* @__PURE__ */ jsxs4(Box4, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingX: 1, children: [
796
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 pick file j/k scroll esc close " }),
797
- /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u25CF same" })
798
- ] })
276
+ /* @__PURE__ */ jsxs(
277
+ Box,
278
+ {
279
+ borderStyle: "single",
280
+ borderTop: true,
281
+ borderBottom: false,
282
+ borderLeft: false,
283
+ borderRight: false,
284
+ paddingX: 1,
285
+ children: [
286
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 pick file j/k scroll esc close " }),
287
+ /* @__PURE__ */ jsx(Text, { color: "green", children: "\u25CF same" })
288
+ ]
289
+ }
290
+ )
799
291
  ] });
800
292
  }
801
- function DiffRow({ row }) {
293
+ function DiffRow({ row, keyColWidth }) {
802
294
  const { key, leftDisplay, rightDisplay, status } = row;
803
295
  const color = status === "same" ? "green" : void 0;
804
- return /* @__PURE__ */ jsxs4(Box4, { children: [
805
- /* @__PURE__ */ jsx4(Text4, { color, children: key.padEnd(22) }),
806
- /* @__PURE__ */ jsxs4(Text4, { color, children: [
296
+ return /* @__PURE__ */ jsxs(Box, { children: [
297
+ /* @__PURE__ */ jsx(Text, { color, children: key.padEnd(keyColWidth) }),
298
+ /* @__PURE__ */ jsxs(Text, { color, children: [
807
299
  " ",
808
300
  (leftDisplay || "\u2014").padEnd(COL_VAL + 2)
809
301
  ] }),
810
- /* @__PURE__ */ jsx4(Text4, { color, children: rightDisplay || "\u2014" })
302
+ /* @__PURE__ */ jsx(Text, { color, children: rightDisplay || "\u2014" })
811
303
  ] });
812
304
  }
813
305
 
306
+ // src/tui/FileList.tsx
307
+ import { Box as Box2, Text as Text2, useInput as useInput2, useStdin as useStdin2 } from "ink";
308
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
309
+ function FileList({
310
+ files,
311
+ selectedIndex,
312
+ focused,
313
+ interactive,
314
+ onSelect
315
+ }) {
316
+ const { isRawModeSupported } = useStdin2();
317
+ useInput2(
318
+ (_, key) => {
319
+ if (!focused) return;
320
+ if (key.upArrow) onSelect(Math.max(0, selectedIndex - 1));
321
+ if (key.downArrow)
322
+ onSelect(Math.min(files.length - 1, selectedIndex + 1));
323
+ },
324
+ { isActive: isRawModeSupported && interactive }
325
+ );
326
+ const byPkg = Map.groupBy(files, (f) => f.package);
327
+ return /* @__PURE__ */ jsx2(
328
+ Box2,
329
+ {
330
+ flexDirection: "column",
331
+ width: 24,
332
+ borderStyle: "single",
333
+ borderRight: true,
334
+ borderTop: false,
335
+ borderBottom: false,
336
+ borderLeft: false,
337
+ children: Array.from(byPkg.entries()).map(([pkg, pkgFiles]) => /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
338
+ /* @__PURE__ */ jsxs2(Text2, { bold: true, dimColor: true, children: [
339
+ " ",
340
+ pkg
341
+ ] }),
342
+ pkgFiles.map((f) => {
343
+ const idx = files.indexOf(f);
344
+ const selected = idx === selectedIndex;
345
+ return /* @__PURE__ */ jsx2(Box2, { paddingLeft: 2, children: /* @__PURE__ */ jsxs2(
346
+ Text2,
347
+ {
348
+ backgroundColor: selected && focused ? "blue" : void 0,
349
+ color: selected && focused ? "white" : selected ? "cyan" : void 0,
350
+ children: [
351
+ f.encrypted ? "\u{1F512} " : " ",
352
+ f.environment
353
+ ]
354
+ }
355
+ ) }, f.path);
356
+ })
357
+ ] }, pkg))
358
+ }
359
+ );
360
+ }
361
+
814
362
  // src/tui/HelpOverlay.tsx
815
- import { Box as Box5, Text as Text5, useInput as useInput4, useStdin as useStdin4 } from "ink";
816
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
363
+ import { Box as Box3, Text as Text3, useInput as useInput3, useStdin as useStdin3 } from "ink";
364
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
817
365
  var SECTIONS = [
818
366
  {
819
367
  title: "Navigation",
@@ -851,30 +399,36 @@ var SECTIONS = [
851
399
  ];
852
400
  var KEY_WIDTH = 10;
853
401
  function HelpOverlay({ onClose }) {
854
- const { isRawModeSupported } = useStdin4();
855
- useInput4((input, key) => {
856
- if (input === "?" || input === "q" || key.escape) onClose();
857
- }, { isActive: isRawModeSupported });
858
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
859
- /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, children: [
860
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "dotenvx-ui " }),
861
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "keyboard shortcuts" })
402
+ const { isRawModeSupported } = useStdin3();
403
+ useInput3(
404
+ (input, key) => {
405
+ if (input === "?" || input === "q" || key.escape) onClose();
406
+ },
407
+ { isActive: isRawModeSupported }
408
+ );
409
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
410
+ /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
411
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, color: "cyan", children: [
412
+ "dotenvx-ui",
413
+ " "
414
+ ] }),
415
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "keyboard shortcuts" })
862
416
  ] }),
863
- SECTIONS.map((section) => /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
864
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: section.title }),
865
- section.rows.map(([key, desc]) => /* @__PURE__ */ jsxs5(Box5, { children: [
866
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: key.padEnd(KEY_WIDTH) }),
867
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: desc })
417
+ SECTIONS.map((section) => /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
418
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: section.title }),
419
+ section.rows.map(([key, desc]) => /* @__PURE__ */ jsxs3(Box3, { children: [
420
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: key.padEnd(KEY_WIDTH) }),
421
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: desc })
868
422
  ] }, key))
869
423
  ] }, section.title)),
870
- /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "? / q / esc close help" }) })
424
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "? / q / esc close help" }) })
871
425
  ] });
872
426
  }
873
427
 
874
428
  // src/tui/InlineForm.tsx
875
- import { useState as useState3, useRef as useRef2 } from "react";
876
- import { Box as Box6, Text as Text6, useInput as useInput5, useStdin as useStdin5, useStdout as useStdout2 } from "ink";
877
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
429
+ import { Box as Box4, Text as Text4, useInput as useInput4, useStdin as useStdin4, useStdout as useStdout2 } from "ink";
430
+ import { useRef as useRef2, useState as useState3 } from "react";
431
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
878
432
  function makeInitialState(value) {
879
433
  const lines = value.split("\n");
880
434
  return {
@@ -884,153 +438,332 @@ function makeInitialState(value) {
884
438
  };
885
439
  }
886
440
  var CHROME = 5;
887
- function InlineForm({ label, initialValue = "", onSubmit, onCancel }) {
888
- const { isRawModeSupported } = useStdin5();
441
+ function InlineForm({
442
+ label,
443
+ initialValue = "",
444
+ onSubmit,
445
+ onCancel
446
+ }) {
447
+ const { isRawModeSupported } = useStdin4();
889
448
  const { stdout } = useStdout2();
890
- const [editor, setEditor] = useState3(() => makeInitialState(initialValue));
449
+ const [editor, setEditor] = useState3(
450
+ () => makeInitialState(initialValue)
451
+ );
891
452
  const editorRef = useRef2(editor);
892
453
  editorRef.current = editor;
893
- useInput5((input, key) => {
894
- if (key.escape) {
895
- onCancel();
896
- return;
897
- }
898
- const { lines: lines2, row: row2, col: col2 } = editorRef.current;
899
- const lineWidth = Math.max(1, (stdout?.columns ?? 80) - CHROME);
900
- if (key.return) {
901
- onSubmit(lines2.join("\n"));
902
- return;
903
- }
904
- if (key.upArrow) {
905
- const line = lines2[row2];
906
- const visualRow = Math.floor(col2 / lineWidth);
907
- if (visualRow > 0) {
908
- const targetVisualRow = visualRow - 1;
909
- const colInVisualRow = col2 % lineWidth;
910
- const newCol = Math.min(targetVisualRow * lineWidth + colInVisualRow, line.length);
911
- setEditor({ lines: lines2, row: row2, col: newCol });
912
- } else if (row2 > 0) {
913
- const prevLine = lines2[row2 - 1];
914
- const prevVisualRows = Math.floor(prevLine.length / lineWidth);
915
- const colInVisualRow = col2 % lineWidth;
916
- const newCol = Math.min(prevVisualRows * lineWidth + colInVisualRow, prevLine.length);
917
- setEditor({ lines: lines2, row: row2 - 1, col: newCol });
454
+ useInput4(
455
+ (input, key) => {
456
+ if (key.escape) {
457
+ onCancel();
458
+ return;
918
459
  }
919
- return;
920
- }
921
- if (key.downArrow) {
922
- const line = lines2[row2];
923
- const visualRow = Math.floor(col2 / lineWidth);
924
- const lastVisualRow = Math.floor(line.length / lineWidth);
925
- if (visualRow < lastVisualRow) {
926
- const colInVisualRow = col2 % lineWidth;
927
- const newCol = Math.min((visualRow + 1) * lineWidth + colInVisualRow, line.length);
928
- setEditor({ lines: lines2, row: row2, col: newCol });
929
- } else if (row2 < lines2.length - 1) {
930
- const colInVisualRow = col2 % lineWidth;
931
- const newCol = Math.min(colInVisualRow, lines2[row2 + 1].length);
932
- setEditor({ lines: lines2, row: row2 + 1, col: newCol });
460
+ const { lines: lines2, row: row2, col: col2 } = editorRef.current;
461
+ const lineWidth = Math.max(1, (stdout?.columns ?? 80) - CHROME);
462
+ if (key.return) {
463
+ onSubmit(lines2.join("\n"));
464
+ return;
933
465
  }
934
- return;
935
- }
936
- if (key.leftArrow) {
937
- if (col2 > 0) {
938
- setEditor({ lines: lines2, row: row2, col: col2 - 1 });
939
- } else if (row2 > 0) {
940
- const newRow = row2 - 1;
941
- setEditor({ lines: lines2, row: newRow, col: lines2[newRow].length });
466
+ if (key.upArrow) {
467
+ const line = lines2[row2];
468
+ const visualRow = Math.floor(col2 / lineWidth);
469
+ if (visualRow > 0) {
470
+ const targetVisualRow = visualRow - 1;
471
+ const colInVisualRow = col2 % lineWidth;
472
+ const newCol = Math.min(
473
+ targetVisualRow * lineWidth + colInVisualRow,
474
+ line.length
475
+ );
476
+ setEditor({ lines: lines2, row: row2, col: newCol });
477
+ } else if (row2 > 0) {
478
+ const prevLine = lines2[row2 - 1];
479
+ const prevVisualRows = Math.floor(prevLine.length / lineWidth);
480
+ const colInVisualRow = col2 % lineWidth;
481
+ const newCol = Math.min(
482
+ prevVisualRows * lineWidth + colInVisualRow,
483
+ prevLine.length
484
+ );
485
+ setEditor({ lines: lines2, row: row2 - 1, col: newCol });
486
+ }
487
+ return;
942
488
  }
943
- return;
944
- }
945
- if (key.rightArrow) {
946
- if (col2 < lines2[row2].length) {
947
- setEditor({ lines: lines2, row: row2, col: col2 + 1 });
948
- } else if (row2 < lines2.length - 1) {
949
- setEditor({ lines: lines2, row: row2 + 1, col: 0 });
489
+ if (key.downArrow) {
490
+ const line = lines2[row2];
491
+ const visualRow = Math.floor(col2 / lineWidth);
492
+ const lastVisualRow = Math.floor(line.length / lineWidth);
493
+ if (visualRow < lastVisualRow) {
494
+ const colInVisualRow = col2 % lineWidth;
495
+ const newCol = Math.min(
496
+ (visualRow + 1) * lineWidth + colInVisualRow,
497
+ line.length
498
+ );
499
+ setEditor({ lines: lines2, row: row2, col: newCol });
500
+ } else if (row2 < lines2.length - 1) {
501
+ const colInVisualRow = col2 % lineWidth;
502
+ const newCol = Math.min(colInVisualRow, lines2[row2 + 1].length);
503
+ setEditor({ lines: lines2, row: row2 + 1, col: newCol });
504
+ }
505
+ return;
950
506
  }
951
- return;
952
- }
953
- if (key.ctrl && input === "a" || key.home) {
954
- setEditor({ lines: lines2, row: row2, col: 0 });
955
- return;
956
- }
957
- if (key.ctrl && input === "e" || key.end) {
958
- setEditor({ lines: lines2, row: row2, col: lines2[row2].length });
959
- return;
960
- }
961
- if (key.ctrl && input === "k") {
962
- const newLines = lines2.map((l, i) => i === row2 ? l.slice(0, col2) : l);
963
- setEditor({ lines: newLines, row: row2, col: col2 });
964
- return;
965
- }
966
- if (key.ctrl && input === "u") {
967
- const newLines = lines2.map((l, i) => i === row2 ? l.slice(col2) : l);
968
- setEditor({ lines: newLines, row: row2, col: 0 });
969
- return;
970
- }
971
- if (key.backspace) {
972
- if (col2 > 0) {
973
- const cur = lines2[row2];
974
- const newLines = lines2.map((l, i) => i === row2 ? cur.slice(0, col2 - 1) + cur.slice(col2) : l);
975
- setEditor({ lines: newLines, row: row2, col: col2 - 1 });
976
- } else if (row2 > 0) {
977
- const prevLen = lines2[row2 - 1].length;
978
- const merged = lines2[row2 - 1] + lines2[row2];
979
- const newLines = [...lines2.slice(0, row2 - 1), merged, ...lines2.slice(row2 + 1)];
980
- setEditor({ lines: newLines, row: row2 - 1, col: prevLen });
507
+ if (key.leftArrow) {
508
+ if (col2 > 0) {
509
+ setEditor({ lines: lines2, row: row2, col: col2 - 1 });
510
+ } else if (row2 > 0) {
511
+ const newRow = row2 - 1;
512
+ setEditor({ lines: lines2, row: newRow, col: lines2[newRow].length });
513
+ }
514
+ return;
981
515
  }
982
- return;
983
- }
984
- if (key.delete) {
985
- const cur = lines2[row2];
986
- if (col2 < cur.length) {
987
- const newLines = lines2.map((l, i) => i === row2 ? cur.slice(0, col2) + cur.slice(col2 + 1) : l);
988
- setEditor({ lines: newLines, row: row2, col: col2 });
989
- } else if (row2 < lines2.length - 1) {
990
- const merged = cur + lines2[row2 + 1];
991
- const newLines = [...lines2.slice(0, row2), merged, ...lines2.slice(row2 + 2)];
516
+ if (key.rightArrow) {
517
+ if (col2 < lines2[row2].length) {
518
+ setEditor({ lines: lines2, row: row2, col: col2 + 1 });
519
+ } else if (row2 < lines2.length - 1) {
520
+ setEditor({ lines: lines2, row: row2 + 1, col: 0 });
521
+ }
522
+ return;
523
+ }
524
+ if (key.ctrl && input === "a" || key.home) {
525
+ setEditor({ lines: lines2, row: row2, col: 0 });
526
+ return;
527
+ }
528
+ if (key.ctrl && input === "e" || key.end) {
529
+ setEditor({ lines: lines2, row: row2, col: lines2[row2].length });
530
+ return;
531
+ }
532
+ if (key.ctrl && input === "k") {
533
+ const newLines = lines2.map((l, i) => i === row2 ? l.slice(0, col2) : l);
992
534
  setEditor({ lines: newLines, row: row2, col: col2 });
535
+ return;
993
536
  }
994
- return;
995
- }
996
- if (input && !key.ctrl && !key.meta) {
997
- const cur = lines2[row2];
998
- const newLines = lines2.map((l, i) => i === row2 ? cur.slice(0, col2) + input + cur.slice(col2) : l);
999
- setEditor({ lines: newLines, row: row2, col: col2 + input.length });
1000
- }
1001
- }, { isActive: isRawModeSupported });
537
+ if (key.ctrl && input === "u") {
538
+ const newLines = lines2.map((l, i) => i === row2 ? l.slice(col2) : l);
539
+ setEditor({ lines: newLines, row: row2, col: 0 });
540
+ return;
541
+ }
542
+ if (key.backspace) {
543
+ if (col2 > 0) {
544
+ const cur = lines2[row2];
545
+ const newLines = lines2.map(
546
+ (l, i) => i === row2 ? cur.slice(0, col2 - 1) + cur.slice(col2) : l
547
+ );
548
+ setEditor({ lines: newLines, row: row2, col: col2 - 1 });
549
+ } else if (row2 > 0) {
550
+ const prevLen = lines2[row2 - 1].length;
551
+ const merged = lines2[row2 - 1] + lines2[row2];
552
+ const newLines = [
553
+ ...lines2.slice(0, row2 - 1),
554
+ merged,
555
+ ...lines2.slice(row2 + 1)
556
+ ];
557
+ setEditor({ lines: newLines, row: row2 - 1, col: prevLen });
558
+ }
559
+ return;
560
+ }
561
+ if (key.delete) {
562
+ const cur = lines2[row2];
563
+ if (col2 < cur.length) {
564
+ const newLines = lines2.map(
565
+ (l, i) => i === row2 ? cur.slice(0, col2) + cur.slice(col2 + 1) : l
566
+ );
567
+ setEditor({ lines: newLines, row: row2, col: col2 });
568
+ } else if (row2 < lines2.length - 1) {
569
+ const merged = cur + lines2[row2 + 1];
570
+ const newLines = [
571
+ ...lines2.slice(0, row2),
572
+ merged,
573
+ ...lines2.slice(row2 + 2)
574
+ ];
575
+ setEditor({ lines: newLines, row: row2, col: col2 });
576
+ }
577
+ return;
578
+ }
579
+ if (input && !key.ctrl && !key.meta) {
580
+ const cur = lines2[row2];
581
+ const newLines = lines2.map(
582
+ (l, i) => i === row2 ? cur.slice(0, col2) + input + cur.slice(col2) : l
583
+ );
584
+ setEditor({ lines: newLines, row: row2, col: col2 + input.length });
585
+ }
586
+ },
587
+ { isActive: isRawModeSupported }
588
+ );
1002
589
  const { lines, row, col } = editor;
1003
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingX: 1, paddingTop: 0, children: [
1004
- /* @__PURE__ */ jsxs6(Box6, { children: [
1005
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "cyan", children: [
1006
- label,
1007
- " "
1008
- ] }),
1009
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u21B5 confirm esc cancel" })
590
+ return /* @__PURE__ */ jsxs4(
591
+ Box4,
592
+ {
593
+ flexDirection: "column",
594
+ borderStyle: "single",
595
+ borderTop: true,
596
+ borderBottom: false,
597
+ borderLeft: false,
598
+ borderRight: false,
599
+ paddingX: 1,
600
+ paddingTop: 0,
601
+ children: [
602
+ /* @__PURE__ */ jsxs4(Box4, { children: [
603
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, color: "cyan", children: [
604
+ label,
605
+ " "
606
+ ] }),
607
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u21B5 confirm esc cancel" })
608
+ ] }),
609
+ lines.map((line, r) => {
610
+ const isActive = r === row;
611
+ if (!isActive) {
612
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
613
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " " }),
614
+ /* @__PURE__ */ jsx4(Text4, { children: line || " " })
615
+ ] }, r);
616
+ }
617
+ const before = line.slice(0, col);
618
+ const cursor = line[col] ?? " ";
619
+ const after = line.slice(col + 1);
620
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
621
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " " }),
622
+ /* @__PURE__ */ jsxs4(Text4, { children: [
623
+ before,
624
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, children: cursor }),
625
+ after
626
+ ] })
627
+ ] }, r);
628
+ })
629
+ ]
630
+ }
631
+ );
632
+ }
633
+
634
+ // src/tui/KeyTable.tsx
635
+ import { Box as Box5, Text as Text5, useInput as useInput5, useStdin as useStdin5 } from "ink";
636
+ import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
637
+ var SECRET_PATTERN = /secret|password|token|key|private|api_?key/i;
638
+ var MAX_INLINE = 48;
639
+ function truncate(s) {
640
+ const first = s.split("\n")[0];
641
+ return first.length > MAX_INLINE ? `${first.slice(0, MAX_INLINE - 1)}\u2026` : first;
642
+ }
643
+ function maskValue(k, revealed) {
644
+ if (revealed.has(k.key)) return truncate(revealed.get(k.key));
645
+ if (k.encrypted) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
646
+ if (SECRET_PATTERN.test(k.key)) return "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
647
+ return truncate(k.value);
648
+ }
649
+ function KeyTable({
650
+ file,
651
+ keys,
652
+ selectedIndex,
653
+ focused,
654
+ interactive,
655
+ revealed,
656
+ onSelect,
657
+ maxRows
658
+ }) {
659
+ const { isRawModeSupported } = useStdin5();
660
+ useInput5(
661
+ (_, key) => {
662
+ if (!focused) return;
663
+ if (key.upArrow) onSelect(Math.max(0, selectedIndex - 1));
664
+ if (key.downArrow) onSelect(Math.min(keys.length - 1, selectedIndex + 1));
665
+ },
666
+ { isActive: isRawModeSupported && interactive }
667
+ );
668
+ const maxVisible = Math.max(3, maxRows - 3);
669
+ const { start, end, above, below } = scrollWindow(
670
+ keys.length,
671
+ selectedIndex,
672
+ maxVisible
673
+ );
674
+ const visibleKeys = keys.slice(start, end);
675
+ const keyColWidth = Math.min(48, Math.max(16, ...keys.map((k) => k.key.length))) + 2;
676
+ const encBadge = file.encrypted ? /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: " encrypted" }) : /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " plain" });
677
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", flexGrow: 1, children: [
678
+ /* @__PURE__ */ jsxs5(Box5, { paddingX: 1, children: [
679
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: file.relativePath }),
680
+ encBadge,
681
+ /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
682
+ " ",
683
+ keys.length,
684
+ " key",
685
+ keys.length === 1 ? "" : "s"
686
+ ] })
1010
687
  ] }),
1011
- lines.map((line, r) => {
1012
- const isActive = r === row;
1013
- if (!isActive) {
1014
- return /* @__PURE__ */ jsxs6(Box6, { children: [
1015
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
1016
- /* @__PURE__ */ jsx6(Text6, { children: line || " " })
1017
- ] }, r);
1018
- }
1019
- const before = line.slice(0, col);
1020
- const cursor = line[col] ?? " ";
1021
- const after = line.slice(col + 1);
1022
- return /* @__PURE__ */ jsxs6(Box6, { children: [
1023
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
1024
- /* @__PURE__ */ jsxs6(Text6, { children: [
1025
- before,
1026
- /* @__PURE__ */ jsx6(Text6, { inverse: true, children: cursor }),
1027
- after
1028
- ] })
1029
- ] }, r);
1030
- })
688
+ keys.length === 0 ? /* @__PURE__ */ jsx5(Box5, { paddingX: 2, marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
689
+ "No keys found. Press ",
690
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "a" }),
691
+ " to add one."
692
+ ] }) }) : /* @__PURE__ */ jsxs5(Fragment, { children: [
693
+ above > 0 && /* @__PURE__ */ jsx5(Box5, { paddingX: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
694
+ "\u2191 ",
695
+ above,
696
+ " more"
697
+ ] }) }),
698
+ visibleKeys.map((k, i) => {
699
+ const idx = start + i;
700
+ const selected = idx === selectedIndex;
701
+ const value = maskValue(k, revealed);
702
+ const lockIcon = k.encrypted && !revealed.has(k.key) ? " \u{1F512}" : "";
703
+ return /* @__PURE__ */ jsx5(Box5, { paddingX: 1, children: /* @__PURE__ */ jsxs5(
704
+ Text5,
705
+ {
706
+ backgroundColor: selected && focused ? "blue" : void 0,
707
+ color: selected && focused ? "white" : selected ? "cyan" : void 0,
708
+ children: [
709
+ k.key.padEnd(keyColWidth),
710
+ /* @__PURE__ */ jsx5(Text5, { dimColor: !selected, children: value }),
711
+ lockIcon
712
+ ]
713
+ }
714
+ ) }, k.key);
715
+ }),
716
+ below > 0 && /* @__PURE__ */ jsx5(Box5, { paddingX: 1, children: /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
717
+ "\u2193 ",
718
+ below,
719
+ " more"
720
+ ] }) })
721
+ ] })
1031
722
  ] });
1032
723
  }
1033
724
 
725
+ // src/tui/StatusBar.tsx
726
+ import { Box as Box6, Text as Text6 } from "ink";
727
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
728
+ var FILE_HINTS = [
729
+ { key: "\u2191\u2193", action: "navigate" },
730
+ { key: "tab", action: "switch panel" },
731
+ { key: "?", action: "help" },
732
+ { key: "q", action: "quit" }
733
+ ];
734
+ var KEY_HINTS = [
735
+ { key: "\u2191\u2193", action: "navigate" },
736
+ { key: "tab", action: "switch" },
737
+ { key: "enter", action: "edit" },
738
+ { key: "y", action: "copy" },
739
+ { key: "r", action: "reveal" },
740
+ { key: "a", action: "add" },
741
+ { key: "D", action: "delete" },
742
+ { key: "d", action: "diff" },
743
+ { key: "?", action: "help" },
744
+ { key: "q", action: "quit" }
745
+ ];
746
+ function Hints({ hints }) {
747
+ return /* @__PURE__ */ jsx6(Box6, { gap: 2, children: hints.map(({ key, action }) => /* @__PURE__ */ jsxs6(Box6, { gap: 1, children: [
748
+ /* @__PURE__ */ jsx6(Text6, { bold: true, color: "cyan", children: key }),
749
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: action })
750
+ ] }, key)) });
751
+ }
752
+ function StatusBar({ message, focus }) {
753
+ return /* @__PURE__ */ jsx6(
754
+ Box6,
755
+ {
756
+ borderStyle: "single",
757
+ borderTop: true,
758
+ borderBottom: false,
759
+ borderLeft: false,
760
+ borderRight: false,
761
+ paddingX: 1,
762
+ children: message ? /* @__PURE__ */ jsx6(Text6, { children: message }) : /* @__PURE__ */ jsx6(Hints, { hints: focus === "files" ? FILE_HINTS : KEY_HINTS })
763
+ }
764
+ );
765
+ }
766
+
1034
767
  // src/tui/App.tsx
1035
768
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1036
769
  function App({ files }) {
@@ -1064,108 +797,111 @@ function App({ files }) {
1064
797
  setStatusMsg(msg);
1065
798
  setTimeout(() => setStatusMsg(void 0), ms);
1066
799
  }
1067
- useInput6((input, key) => {
1068
- if (input === "q" || key.escape) {
1069
- exit();
1070
- return;
1071
- }
1072
- if (input === "?") {
1073
- setMode({ type: "help" });
1074
- return;
1075
- }
1076
- if (key.tab) {
1077
- setFocus((f) => f === "files" ? "keys" : "files");
1078
- return;
1079
- }
1080
- if (focus !== "keys") return;
1081
- const k = keys[keyIndex];
1082
- if (input === "r") {
1083
- if (!k) return;
1084
- if (revealed.has(k.key)) {
1085
- setRevealed((prev) => {
1086
- const next = new Map(prev);
1087
- next.delete(k.key);
1088
- return next;
1089
- });
800
+ useInput6(
801
+ (input, key) => {
802
+ if (input === "q" || key.escape) {
803
+ exit();
1090
804
  return;
1091
805
  }
1092
- if (k.encrypted) {
1093
- const plain = decryptValue(k.value, selectedFile.path);
1094
- if (plain === null) {
1095
- flash("\u{1F512} Private key not found in environment");
1096
- return;
1097
- }
1098
- setRevealed((prev) => new Map(prev).set(k.key, plain));
1099
- } else {
1100
- setRevealed((prev) => new Map(prev).set(k.key, k.value));
806
+ if (input === "?") {
807
+ setMode({ type: "help" });
808
+ return;
1101
809
  }
1102
- return;
1103
- }
1104
- if (input === "R") {
1105
- if (revealed.size > 0) {
1106
- setRevealed(/* @__PURE__ */ new Map());
810
+ if (key.tab) {
811
+ setFocus((f) => f === "files" ? "keys" : "files");
1107
812
  return;
1108
813
  }
1109
- const decrypted = keys.some((e) => e.encrypted) ? decryptAllValues(selectedFile.path) : {};
1110
- const next = /* @__PURE__ */ new Map();
1111
- for (const entry of keys) {
1112
- if (entry.encrypted) {
1113
- const plain = decrypted[entry.key];
1114
- if (plain === void 0 || isEncryptedValue(plain)) {
1115
- flash("\u{1F512} Private key not found \u2014 cannot reveal all");
814
+ if (focus !== "keys") return;
815
+ const k = keys[keyIndex];
816
+ if (input === "r") {
817
+ if (!k) return;
818
+ if (revealed.has(k.key)) {
819
+ setRevealed((prev) => {
820
+ const next = new Map(prev);
821
+ next.delete(k.key);
822
+ return next;
823
+ });
824
+ return;
825
+ }
826
+ if (k.encrypted) {
827
+ const plain = decryptValue(k.value, selectedFile.path);
828
+ if (plain === null) {
829
+ flash("\u{1F512} Private key not found in environment");
1116
830
  return;
1117
831
  }
1118
- next.set(entry.key, plain);
832
+ setRevealed((prev) => new Map(prev).set(k.key, plain));
1119
833
  } else {
1120
- next.set(entry.key, entry.value);
834
+ setRevealed((prev) => new Map(prev).set(k.key, k.value));
1121
835
  }
836
+ return;
1122
837
  }
1123
- setRevealed(next);
1124
- return;
1125
- }
1126
- if (input === "y") {
1127
- if (!k) return;
1128
- const value = k.encrypted ? decryptValue(k.value, selectedFile.path) : k.value;
1129
- if (value === null) {
1130
- flash("\u{1F512} Private key not found \u2014 cannot copy");
838
+ if (input === "R") {
839
+ if (revealed.size > 0) {
840
+ setRevealed(/* @__PURE__ */ new Map());
841
+ return;
842
+ }
843
+ const decrypted = keys.some((e) => e.encrypted) ? decryptAllValues(selectedFile.path) : {};
844
+ const next = /* @__PURE__ */ new Map();
845
+ for (const entry of keys) {
846
+ if (entry.encrypted) {
847
+ const plain = decrypted[entry.key];
848
+ if (plain === void 0 || isEncryptedValue(plain)) {
849
+ flash("\u{1F512} Private key not found \u2014 cannot reveal all");
850
+ return;
851
+ }
852
+ next.set(entry.key, plain);
853
+ } else {
854
+ next.set(entry.key, entry.value);
855
+ }
856
+ }
857
+ setRevealed(next);
1131
858
  return;
1132
859
  }
1133
- clipboard.writeSync(value);
1134
- flash(`Copied ${k.key}`);
1135
- return;
1136
- }
1137
- if (key.return) {
1138
- if (!k) return;
1139
- if (k.encrypted) {
1140
- const plain = decryptValue(k.value, selectedFile.path);
1141
- if (plain === null) {
1142
- flash("\u{1F512} Private key not found \u2014 cannot edit");
860
+ if (input === "y") {
861
+ if (!k) return;
862
+ const value = k.encrypted ? decryptValue(k.value, selectedFile.path) : k.value;
863
+ if (value === null) {
864
+ flash("\u{1F512} Private key not found \u2014 cannot copy");
1143
865
  return;
1144
866
  }
1145
- setMode({ type: "edit", key: { ...k, value: plain } });
1146
- } else {
1147
- setMode({ type: "edit", key: k });
867
+ clipboard.writeSync(value);
868
+ flash(`Copied ${k.key}`);
869
+ return;
1148
870
  }
1149
- return;
1150
- }
1151
- if (input === "a") {
1152
- setMode({ type: "add-key" });
1153
- return;
1154
- }
1155
- if (input === "D") {
1156
- if (!k) return;
1157
- setMode({ type: "confirm-delete", key: k });
1158
- return;
1159
- }
1160
- if (input === "d") {
1161
- setMode({ type: "diff" });
1162
- return;
1163
- }
1164
- if (input === "e") {
1165
- setMode({ type: "confirm-encrypt" });
1166
- return;
1167
- }
1168
- }, { isActive: isRawModeSupported && mode.type === "normal" });
871
+ if (key.return) {
872
+ if (!k) return;
873
+ if (k.encrypted) {
874
+ const plain = decryptValue(k.value, selectedFile.path);
875
+ if (plain === null) {
876
+ flash("\u{1F512} Private key not found \u2014 cannot edit");
877
+ return;
878
+ }
879
+ setMode({ type: "edit", key: { ...k, value: plain } });
880
+ } else {
881
+ setMode({ type: "edit", key: k });
882
+ }
883
+ return;
884
+ }
885
+ if (input === "a") {
886
+ setMode({ type: "add-key" });
887
+ return;
888
+ }
889
+ if (input === "D") {
890
+ if (!k) return;
891
+ setMode({ type: "confirm-delete", key: k });
892
+ return;
893
+ }
894
+ if (input === "d") {
895
+ setMode({ type: "diff" });
896
+ return;
897
+ }
898
+ if (input === "e") {
899
+ setMode({ type: "confirm-encrypt" });
900
+ return;
901
+ }
902
+ },
903
+ { isActive: isRawModeSupported && mode.type === "normal" }
904
+ );
1169
905
  if (mode.type === "edit") {
1170
906
  const editing = mode.key;
1171
907
  return /* @__PURE__ */ jsx7(
@@ -1303,7 +1039,15 @@ function App({ files }) {
1303
1039
  statusMsg,
1304
1040
  focus2: focus,
1305
1041
  interactive: false,
1306
- extra: /* @__PURE__ */ jsx7(ConfirmAddEncrypt, { keyName, onEncrypt: () => commit(true), onPlain: () => commit(false), onCancel: () => setMode({ type: "normal" }) })
1042
+ extra: /* @__PURE__ */ jsx7(
1043
+ ConfirmAddEncrypt,
1044
+ {
1045
+ keyName,
1046
+ onEncrypt: () => commit(true),
1047
+ onPlain: () => commit(false),
1048
+ onCancel: () => setMode({ type: "normal" })
1049
+ }
1050
+ )
1307
1051
  }
1308
1052
  );
1309
1053
  }
@@ -1371,7 +1115,9 @@ function App({ files }) {
1371
1115
  flash(`Encrypted ${selectedFile.relativePath}`);
1372
1116
  }
1373
1117
  } catch (err) {
1374
- flash(`Error: ${err instanceof Error ? err.message : String(err)}`);
1118
+ flash(
1119
+ `Error: ${err instanceof Error ? err.message : String(err)}`
1120
+ );
1375
1121
  }
1376
1122
  refreshKeys();
1377
1123
  setRevealed(/* @__PURE__ */ new Map());
@@ -1435,17 +1181,26 @@ function Layout({
1435
1181
  /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
1436
1182
  /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "dotenvx-ui" }),
1437
1183
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1438
- " ",
1184
+ " ",
1439
1185
  selectedFile.relativePath,
1440
- " \xB7 ",
1186
+ " \xB7 ",
1441
1187
  files.length,
1442
- " files \xB7 ",
1188
+ " files \xB7 ",
1443
1189
  encCount,
1444
1190
  " enc"
1445
1191
  ] })
1446
1192
  ] }),
1447
1193
  /* @__PURE__ */ jsxs7(Box7, { height: listRows, children: [
1448
- /* @__PURE__ */ jsx7(FileList, { files, selectedIndex: fileIndex, focused: focus === "files", interactive, onSelect: onSelectFile }),
1194
+ /* @__PURE__ */ jsx7(
1195
+ FileList,
1196
+ {
1197
+ files,
1198
+ selectedIndex: fileIndex,
1199
+ focused: focus === "files",
1200
+ interactive,
1201
+ onSelect: onSelectFile
1202
+ }
1203
+ ),
1449
1204
  /* @__PURE__ */ jsx7(
1450
1205
  KeyTable,
1451
1206
  {
@@ -1461,11 +1216,26 @@ function Layout({
1461
1216
  )
1462
1217
  ] }),
1463
1218
  extra,
1464
- !extra && /* @__PURE__ */ jsx7(ValuePreview, { keys, keyIndex, focus, revealed, width: termCols }),
1219
+ !extra && /* @__PURE__ */ jsx7(
1220
+ ValuePreview,
1221
+ {
1222
+ keys,
1223
+ keyIndex,
1224
+ focus,
1225
+ revealed,
1226
+ width: termCols
1227
+ }
1228
+ ),
1465
1229
  /* @__PURE__ */ jsx7(StatusBar, { focus, message: statusMsg })
1466
1230
  ] });
1467
1231
  }
1468
- function ValuePreview({ keys, keyIndex, focus, revealed, width }) {
1232
+ function ValuePreview({
1233
+ keys,
1234
+ keyIndex,
1235
+ focus,
1236
+ revealed,
1237
+ width
1238
+ }) {
1469
1239
  if (focus !== "keys") return null;
1470
1240
  const k = keys[keyIndex];
1471
1241
  if (!k) return null;
@@ -1489,35 +1259,47 @@ function ValuePreview({ keys, keyIndex, focus, revealed, width }) {
1489
1259
  children: [
1490
1260
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "cyan", children: [
1491
1261
  k.key,
1492
- " "
1262
+ " "
1493
1263
  ] }),
1494
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\xB7 " }),
1495
- /* @__PURE__ */ jsx7(Text7, { truncate: true, children: flat })
1264
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\xB7 " }),
1265
+ /* @__PURE__ */ jsx7(Box7, { overflow: "hidden", children: /* @__PURE__ */ jsx7(Text7, { children: flat }) })
1496
1266
  ]
1497
1267
  }
1498
1268
  );
1499
1269
  }
1500
1270
  function ConfirmDelete({ keyName, onConfirm, onCancel }) {
1501
1271
  const { isRawModeSupported } = useStdin6();
1502
- useInput6((input) => {
1503
- if (input === "y" || input === "Y") onConfirm();
1504
- else onCancel();
1505
- }, { isActive: isRawModeSupported });
1272
+ useInput6(
1273
+ (input) => {
1274
+ if (input === "y" || input === "Y") onConfirm();
1275
+ else onCancel();
1276
+ },
1277
+ { isActive: isRawModeSupported }
1278
+ );
1506
1279
  return /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
1507
1280
  /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
1508
1281
  "Delete ",
1509
1282
  /* @__PURE__ */ jsx7(Text7, { bold: true, children: keyName }),
1510
- "? "
1283
+ "?",
1284
+ " "
1511
1285
  ] }),
1512
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y confirm any other key cancel" })
1286
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y confirm any other key cancel" })
1513
1287
  ] });
1514
1288
  }
1515
- function ConfirmEncrypt({ decrypt, fileName, onConfirm, onCancel }) {
1289
+ function ConfirmEncrypt({
1290
+ decrypt,
1291
+ fileName,
1292
+ onConfirm,
1293
+ onCancel
1294
+ }) {
1516
1295
  const { isRawModeSupported } = useStdin6();
1517
- useInput6((input) => {
1518
- if (input === "y" || input === "Y") onConfirm();
1519
- else onCancel();
1520
- }, { isActive: isRawModeSupported });
1296
+ useInput6(
1297
+ (input) => {
1298
+ if (input === "y" || input === "Y") onConfirm();
1299
+ else onCancel();
1300
+ },
1301
+ { isActive: isRawModeSupported }
1302
+ );
1521
1303
  const action = decrypt ? "Decrypt" : "Encrypt";
1522
1304
  const color = decrypt ? "yellow" : "green";
1523
1305
  return /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
@@ -1525,42 +1307,52 @@ function ConfirmEncrypt({ decrypt, fileName, onConfirm, onCancel }) {
1525
1307
  action,
1526
1308
  " ",
1527
1309
  /* @__PURE__ */ jsx7(Text7, { bold: true, children: fileName }),
1528
- "? "
1310
+ "?",
1311
+ " "
1529
1312
  ] }),
1530
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y confirm any other key cancel" })
1313
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y confirm any other key cancel" })
1531
1314
  ] });
1532
1315
  }
1533
- function ConfirmAddEncrypt({ keyName, onEncrypt, onPlain, onCancel }) {
1316
+ function ConfirmAddEncrypt({
1317
+ keyName,
1318
+ onEncrypt,
1319
+ onPlain,
1320
+ onCancel
1321
+ }) {
1534
1322
  const { isRawModeSupported } = useStdin6();
1535
- useInput6((input, key) => {
1536
- if (key.escape) {
1537
- onCancel();
1538
- return;
1539
- }
1540
- if (input === "y" || input === "Y") {
1541
- onEncrypt();
1542
- return;
1543
- }
1544
- if (input === "n" || input === "N" || key.return) {
1545
- onPlain();
1546
- return;
1547
- }
1548
- }, { isActive: isRawModeSupported });
1323
+ useInput6(
1324
+ (input, key) => {
1325
+ if (key.escape) {
1326
+ onCancel();
1327
+ return;
1328
+ }
1329
+ if (input === "y" || input === "Y") {
1330
+ onEncrypt();
1331
+ return;
1332
+ }
1333
+ if (input === "n" || input === "N" || key.return) {
1334
+ onPlain();
1335
+ return;
1336
+ }
1337
+ },
1338
+ { isActive: isRawModeSupported }
1339
+ );
1549
1340
  return /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
1550
1341
  /* @__PURE__ */ jsxs7(Text7, { children: [
1551
1342
  "Encrypt ",
1552
1343
  /* @__PURE__ */ jsx7(Text7, { bold: true, children: keyName }),
1553
- "? "
1344
+ "?",
1345
+ " "
1554
1346
  ] }),
1555
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y encrypt n plain esc cancel" })
1347
+ /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "y encrypt n plain esc cancel" })
1556
1348
  ] });
1557
1349
  }
1558
1350
 
1559
1351
  // src/tui/ErrorBoundary.tsx
1560
- import React4 from "react";
1561
1352
  import { Box as Box8, Text as Text8 } from "ink";
1353
+ import React from "react";
1562
1354
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1563
- var ErrorBoundary = class extends React4.Component {
1355
+ var ErrorBoundary = class extends React.Component {
1564
1356
  state = { error: null };
1565
1357
  static getDerivedStateFromError(error) {
1566
1358
  return { error };
@@ -1578,9 +1370,8 @@ var ErrorBoundary = class extends React4.Component {
1578
1370
  };
1579
1371
 
1580
1372
  // src/cli.tsx
1581
- import { createRequire as createRequire2 } from "module";
1582
1373
  import { jsx as jsx9 } from "react/jsx-runtime";
1583
- var { version } = createRequire2(import.meta.url)("../package.json");
1374
+ var { version } = createRequire(import.meta.url)("../package.json");
1584
1375
  var HELP = `
1585
1376
  dotenvx-ui \u2014 terminal and web UI for dotenvx environment files
1586
1377
 
@@ -1609,12 +1400,14 @@ var commands = {
1609
1400
  console.log(HELP);
1610
1401
  process.exit(0);
1611
1402
  },
1612
- "ui": runWebUI
1403
+ ui: runWebUI
1613
1404
  };
1614
1405
  var [, , command] = process.argv;
1615
1406
  if (command !== void 0 && !(command in commands)) {
1616
- console.error(`Unknown command: ${command}
1617
- Run dotenvx-ui --help for usage.`);
1407
+ console.error(
1408
+ `Unknown command: ${command}
1409
+ Run dotenvx-ui --help for usage.`
1410
+ );
1618
1411
  process.exit(1);
1619
1412
  }
1620
1413
  commands[command ?? ""]?.() ?? runTUI();
@@ -1624,9 +1417,17 @@ function runTUI() {
1624
1417
  console.error("No .env files found in this directory.");
1625
1418
  process.exit(1);
1626
1419
  }
1627
- render(/* @__PURE__ */ jsx9(ErrorBoundary, { children: /* @__PURE__ */ jsx9(App, { files }) }), { alternateScreen: true });
1420
+ render(
1421
+ /* @__PURE__ */ jsx9(ErrorBoundary, { children: /* @__PURE__ */ jsx9(App, { files }) }),
1422
+ { alternateScreen: true }
1423
+ );
1628
1424
  }
1629
- function runWebUI() {
1630
- console.log("Web UI \u2014 coming soon");
1631
- process.exit(0);
1425
+ async function runWebUI() {
1426
+ const { startServer } = await import("./server-DDA3TXEA.js");
1427
+ const files = scan(process.cwd());
1428
+ if (files.length === 0) {
1429
+ console.error("No .env files found in this directory.");
1430
+ process.exit(1);
1431
+ }
1432
+ await startServer(process.cwd());
1632
1433
  }