dotenvx-ui 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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,41 +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
+ );
721
188
  const keyColWidth = Math.min(48, Math.max(16, ...rows.map((r) => r.key.length))) + 2;
722
189
  const leftName = trunc(left.relativePath, COL_VAL);
723
190
  const rightName = rightFile ? trunc(rightFile.relativePath, COL_VAL) : "\u2014";
724
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
725
- /* @__PURE__ */ jsxs4(Box4, { paddingX: 1, children: [
726
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "dotenvx-ui " }),
727
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
728
- "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 ",
729
199
  left.relativePath,
730
- " \u2194 ",
200
+ " \u2194 ",
731
201
  rightFile?.relativePath ?? "\u2014"
732
202
  ] })
733
203
  ] }),
734
- /* @__PURE__ */ jsxs4(
735
- Box4,
204
+ /* @__PURE__ */ jsxs(
205
+ Box,
736
206
  {
737
207
  flexDirection: "column",
738
208
  paddingX: 1,
@@ -742,16 +212,16 @@ function DiffView({ left, files, onClose }) {
742
212
  borderLeft: false,
743
213
  borderRight: false,
744
214
  children: [
745
- /* @__PURE__ */ jsx4(Text4, { bold: true, dimColor: true, children: "compare with" }),
746
- picker.above > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
747
- " \u2191 ",
215
+ /* @__PURE__ */ jsx(Text, { bold: true, dimColor: true, children: "compare with" }),
216
+ picker.above > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
217
+ " \u2191 ",
748
218
  picker.above,
749
219
  " more"
750
220
  ] }),
751
221
  others.slice(picker.start, picker.end).map((f, i) => {
752
222
  const selected = picker.start + i === pickerIndex;
753
- return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(
754
- Text4,
223
+ return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(
224
+ Text,
755
225
  {
756
226
  backgroundColor: selected ? "blue" : void 0,
757
227
  color: selected ? "white" : void 0,
@@ -762,59 +232,136 @@ function DiffView({ left, files, onClose }) {
762
232
  }
763
233
  ) }, f.path);
764
234
  }),
765
- picker.below > 0 && /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
766
- " \u2193 ",
235
+ picker.below > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
236
+ " \u2193 ",
767
237
  picker.below,
768
238
  " more"
769
239
  ] }),
770
- 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" })
771
241
  ]
772
242
  }
773
243
  ),
774
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", flexGrow: 1, paddingX: 1, children: [
775
- /* @__PURE__ */ jsxs4(Box4, { borderStyle: "single", borderBottom: true, borderTop: false, borderLeft: false, borderRight: false, children: [
776
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "KEY".padEnd(keyColWidth) }),
777
- /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
778
- " ",
779
- leftName.padEnd(COL_VAL + 2)
780
- ] }),
781
- /* @__PURE__ */ jsx4(Text4, { bold: true, children: rightName })
782
- ] }),
783
- 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: [
784
264
  "\u2191 ",
785
265
  rowsAbove,
786
266
  " more"
787
267
  ] }),
788
- visibleRows.map((row) => /* @__PURE__ */ jsx4(DiffRow, { row, keyColWidth }, row.key)),
789
- 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: [
790
270
  "\u2193 ",
791
271
  rowsBelow,
792
272
  " more"
793
273
  ] }),
794
- 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." }) })
795
275
  ] }),
796
- /* @__PURE__ */ jsxs4(Box4, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingX: 1, children: [
797
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2191\u2193 pick file j/k scroll esc close " }),
798
- /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u25CF same" })
799
- ] })
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
+ )
800
291
  ] });
801
292
  }
802
293
  function DiffRow({ row, keyColWidth }) {
803
294
  const { key, leftDisplay, rightDisplay, status } = row;
804
295
  const color = status === "same" ? "green" : void 0;
805
- return /* @__PURE__ */ jsxs4(Box4, { children: [
806
- /* @__PURE__ */ jsx4(Text4, { color, children: key.padEnd(keyColWidth) }),
807
- /* @__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: [
808
299
  " ",
809
300
  (leftDisplay || "\u2014").padEnd(COL_VAL + 2)
810
301
  ] }),
811
- /* @__PURE__ */ jsx4(Text4, { color, children: rightDisplay || "\u2014" })
302
+ /* @__PURE__ */ jsx(Text, { color, children: rightDisplay || "\u2014" })
812
303
  ] });
813
304
  }
814
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
+
815
362
  // src/tui/HelpOverlay.tsx
816
- import { Box as Box5, Text as Text5, useInput as useInput4, useStdin as useStdin4 } from "ink";
817
- 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";
818
365
  var SECTIONS = [
819
366
  {
820
367
  title: "Navigation",
@@ -852,30 +399,36 @@ var SECTIONS = [
852
399
  ];
853
400
  var KEY_WIDTH = 10;
854
401
  function HelpOverlay({ onClose }) {
855
- const { isRawModeSupported } = useStdin4();
856
- useInput4((input, key) => {
857
- if (input === "?" || input === "q" || key.escape) onClose();
858
- }, { isActive: isRawModeSupported });
859
- return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
860
- /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, children: [
861
- /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "dotenvx-ui " }),
862
- /* @__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" })
863
416
  ] }),
864
- SECTIONS.map((section) => /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginBottom: 1, children: [
865
- /* @__PURE__ */ jsx5(Text5, { bold: true, children: section.title }),
866
- section.rows.map(([key, desc]) => /* @__PURE__ */ jsxs5(Box5, { children: [
867
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: key.padEnd(KEY_WIDTH) }),
868
- /* @__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 })
869
422
  ] }, key))
870
423
  ] }, section.title)),
871
- /* @__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" }) })
872
425
  ] });
873
426
  }
874
427
 
875
428
  // src/tui/InlineForm.tsx
876
- import { useState as useState3, useRef as useRef2 } from "react";
877
- import { Box as Box6, Text as Text6, useInput as useInput5, useStdin as useStdin5, useStdout as useStdout2 } from "ink";
878
- 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";
879
432
  function makeInitialState(value) {
880
433
  const lines = value.split("\n");
881
434
  return {
@@ -885,153 +438,332 @@ function makeInitialState(value) {
885
438
  };
886
439
  }
887
440
  var CHROME = 5;
888
- function InlineForm({ label, initialValue = "", onSubmit, onCancel }) {
889
- const { isRawModeSupported } = useStdin5();
441
+ function InlineForm({
442
+ label,
443
+ initialValue = "",
444
+ onSubmit,
445
+ onCancel
446
+ }) {
447
+ const { isRawModeSupported } = useStdin4();
890
448
  const { stdout } = useStdout2();
891
- const [editor, setEditor] = useState3(() => makeInitialState(initialValue));
449
+ const [editor, setEditor] = useState3(
450
+ () => makeInitialState(initialValue)
451
+ );
892
452
  const editorRef = useRef2(editor);
893
453
  editorRef.current = editor;
894
- useInput5((input, key) => {
895
- if (key.escape) {
896
- onCancel();
897
- return;
898
- }
899
- const { lines: lines2, row: row2, col: col2 } = editorRef.current;
900
- const lineWidth = Math.max(1, (stdout?.columns ?? 80) - CHROME);
901
- if (key.return) {
902
- onSubmit(lines2.join("\n"));
903
- return;
904
- }
905
- if (key.upArrow) {
906
- const line = lines2[row2];
907
- const visualRow = Math.floor(col2 / lineWidth);
908
- if (visualRow > 0) {
909
- const targetVisualRow = visualRow - 1;
910
- const colInVisualRow = col2 % lineWidth;
911
- const newCol = Math.min(targetVisualRow * lineWidth + colInVisualRow, line.length);
912
- setEditor({ lines: lines2, row: row2, col: newCol });
913
- } else if (row2 > 0) {
914
- const prevLine = lines2[row2 - 1];
915
- const prevVisualRows = Math.floor(prevLine.length / lineWidth);
916
- const colInVisualRow = col2 % lineWidth;
917
- const newCol = Math.min(prevVisualRows * lineWidth + colInVisualRow, prevLine.length);
918
- setEditor({ lines: lines2, row: row2 - 1, col: newCol });
454
+ useInput4(
455
+ (input, key) => {
456
+ if (key.escape) {
457
+ onCancel();
458
+ return;
919
459
  }
920
- return;
921
- }
922
- if (key.downArrow) {
923
- const line = lines2[row2];
924
- const visualRow = Math.floor(col2 / lineWidth);
925
- const lastVisualRow = Math.floor(line.length / lineWidth);
926
- if (visualRow < lastVisualRow) {
927
- const colInVisualRow = col2 % lineWidth;
928
- const newCol = Math.min((visualRow + 1) * lineWidth + colInVisualRow, line.length);
929
- setEditor({ lines: lines2, row: row2, col: newCol });
930
- } else if (row2 < lines2.length - 1) {
931
- const colInVisualRow = col2 % lineWidth;
932
- const newCol = Math.min(colInVisualRow, lines2[row2 + 1].length);
933
- 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;
934
465
  }
935
- return;
936
- }
937
- if (key.leftArrow) {
938
- if (col2 > 0) {
939
- setEditor({ lines: lines2, row: row2, col: col2 - 1 });
940
- } else if (row2 > 0) {
941
- const newRow = row2 - 1;
942
- 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;
943
488
  }
944
- return;
945
- }
946
- if (key.rightArrow) {
947
- if (col2 < lines2[row2].length) {
948
- setEditor({ lines: lines2, row: row2, col: col2 + 1 });
949
- } else if (row2 < lines2.length - 1) {
950
- 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;
951
506
  }
952
- return;
953
- }
954
- if (key.ctrl && input === "a" || key.home) {
955
- setEditor({ lines: lines2, row: row2, col: 0 });
956
- return;
957
- }
958
- if (key.ctrl && input === "e" || key.end) {
959
- setEditor({ lines: lines2, row: row2, col: lines2[row2].length });
960
- return;
961
- }
962
- if (key.ctrl && input === "k") {
963
- const newLines = lines2.map((l, i) => i === row2 ? l.slice(0, col2) : l);
964
- setEditor({ lines: newLines, row: row2, col: col2 });
965
- return;
966
- }
967
- if (key.ctrl && input === "u") {
968
- const newLines = lines2.map((l, i) => i === row2 ? l.slice(col2) : l);
969
- setEditor({ lines: newLines, row: row2, col: 0 });
970
- return;
971
- }
972
- if (key.backspace) {
973
- if (col2 > 0) {
974
- const cur = lines2[row2];
975
- const newLines = lines2.map((l, i) => i === row2 ? cur.slice(0, col2 - 1) + cur.slice(col2) : l);
976
- setEditor({ lines: newLines, row: row2, col: col2 - 1 });
977
- } else if (row2 > 0) {
978
- const prevLen = lines2[row2 - 1].length;
979
- const merged = lines2[row2 - 1] + lines2[row2];
980
- const newLines = [...lines2.slice(0, row2 - 1), merged, ...lines2.slice(row2 + 1)];
981
- 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;
982
515
  }
983
- return;
984
- }
985
- if (key.delete) {
986
- const cur = lines2[row2];
987
- if (col2 < cur.length) {
988
- const newLines = lines2.map((l, i) => i === row2 ? cur.slice(0, col2) + cur.slice(col2 + 1) : l);
989
- setEditor({ lines: newLines, row: row2, col: col2 });
990
- } else if (row2 < lines2.length - 1) {
991
- const merged = cur + lines2[row2 + 1];
992
- 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);
993
534
  setEditor({ lines: newLines, row: row2, col: col2 });
535
+ return;
994
536
  }
995
- return;
996
- }
997
- if (input && !key.ctrl && !key.meta) {
998
- const cur = lines2[row2];
999
- const newLines = lines2.map((l, i) => i === row2 ? cur.slice(0, col2) + input + cur.slice(col2) : l);
1000
- setEditor({ lines: newLines, row: row2, col: col2 + input.length });
1001
- }
1002
- }, { 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
+ );
1003
589
  const { lines, row, col } = editor;
1004
- return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, paddingX: 1, paddingTop: 0, children: [
1005
- /* @__PURE__ */ jsxs6(Box6, { children: [
1006
- /* @__PURE__ */ jsxs6(Text6, { bold: true, color: "cyan", children: [
1007
- label,
1008
- " "
1009
- ] }),
1010
- /* @__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
+ ] })
1011
687
  ] }),
1012
- lines.map((line, r) => {
1013
- const isActive = r === row;
1014
- if (!isActive) {
1015
- return /* @__PURE__ */ jsxs6(Box6, { children: [
1016
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
1017
- /* @__PURE__ */ jsx6(Text6, { children: line || " " })
1018
- ] }, r);
1019
- }
1020
- const before = line.slice(0, col);
1021
- const cursor = line[col] ?? " ";
1022
- const after = line.slice(col + 1);
1023
- return /* @__PURE__ */ jsxs6(Box6, { children: [
1024
- /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " " }),
1025
- /* @__PURE__ */ jsxs6(Text6, { children: [
1026
- before,
1027
- /* @__PURE__ */ jsx6(Text6, { inverse: true, children: cursor }),
1028
- after
1029
- ] })
1030
- ] }, r);
1031
- })
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
+ ] })
1032
722
  ] });
1033
723
  }
1034
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
+
1035
767
  // src/tui/App.tsx
1036
768
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1037
769
  function App({ files }) {
@@ -1065,108 +797,111 @@ function App({ files }) {
1065
797
  setStatusMsg(msg);
1066
798
  setTimeout(() => setStatusMsg(void 0), ms);
1067
799
  }
1068
- useInput6((input, key) => {
1069
- if (input === "q" || key.escape) {
1070
- exit();
1071
- return;
1072
- }
1073
- if (input === "?") {
1074
- setMode({ type: "help" });
1075
- return;
1076
- }
1077
- if (key.tab) {
1078
- setFocus((f) => f === "files" ? "keys" : "files");
1079
- return;
1080
- }
1081
- if (focus !== "keys") return;
1082
- const k = keys[keyIndex];
1083
- if (input === "r") {
1084
- if (!k) return;
1085
- if (revealed.has(k.key)) {
1086
- setRevealed((prev) => {
1087
- const next = new Map(prev);
1088
- next.delete(k.key);
1089
- return next;
1090
- });
800
+ useInput6(
801
+ (input, key) => {
802
+ if (input === "q" || key.escape) {
803
+ exit();
1091
804
  return;
1092
805
  }
1093
- if (k.encrypted) {
1094
- const plain = decryptValue(k.value, selectedFile.path);
1095
- if (plain === null) {
1096
- flash("\u{1F512} Private key not found in environment");
1097
- return;
1098
- }
1099
- setRevealed((prev) => new Map(prev).set(k.key, plain));
1100
- } else {
1101
- setRevealed((prev) => new Map(prev).set(k.key, k.value));
806
+ if (input === "?") {
807
+ setMode({ type: "help" });
808
+ return;
1102
809
  }
1103
- return;
1104
- }
1105
- if (input === "R") {
1106
- if (revealed.size > 0) {
1107
- setRevealed(/* @__PURE__ */ new Map());
810
+ if (key.tab) {
811
+ setFocus((f) => f === "files" ? "keys" : "files");
1108
812
  return;
1109
813
  }
1110
- const decrypted = keys.some((e) => e.encrypted) ? decryptAllValues(selectedFile.path) : {};
1111
- const next = /* @__PURE__ */ new Map();
1112
- for (const entry of keys) {
1113
- if (entry.encrypted) {
1114
- const plain = decrypted[entry.key];
1115
- if (plain === void 0 || isEncryptedValue(plain)) {
1116
- 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");
1117
830
  return;
1118
831
  }
1119
- next.set(entry.key, plain);
832
+ setRevealed((prev) => new Map(prev).set(k.key, plain));
1120
833
  } else {
1121
- next.set(entry.key, entry.value);
834
+ setRevealed((prev) => new Map(prev).set(k.key, k.value));
1122
835
  }
836
+ return;
1123
837
  }
1124
- setRevealed(next);
1125
- return;
1126
- }
1127
- if (input === "y") {
1128
- if (!k) return;
1129
- const value = k.encrypted ? decryptValue(k.value, selectedFile.path) : k.value;
1130
- if (value === null) {
1131
- 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);
1132
858
  return;
1133
859
  }
1134
- clipboard.writeSync(value);
1135
- flash(`Copied ${k.key}`);
1136
- return;
1137
- }
1138
- if (key.return) {
1139
- if (!k) return;
1140
- if (k.encrypted) {
1141
- const plain = decryptValue(k.value, selectedFile.path);
1142
- if (plain === null) {
1143
- 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");
1144
865
  return;
1145
866
  }
1146
- setMode({ type: "edit", key: { ...k, value: plain } });
1147
- } else {
1148
- setMode({ type: "edit", key: k });
867
+ clipboard.writeSync(value);
868
+ flash(`Copied ${k.key}`);
869
+ return;
1149
870
  }
1150
- return;
1151
- }
1152
- if (input === "a") {
1153
- setMode({ type: "add-key" });
1154
- return;
1155
- }
1156
- if (input === "D") {
1157
- if (!k) return;
1158
- setMode({ type: "confirm-delete", key: k });
1159
- return;
1160
- }
1161
- if (input === "d") {
1162
- setMode({ type: "diff" });
1163
- return;
1164
- }
1165
- if (input === "e") {
1166
- setMode({ type: "confirm-encrypt" });
1167
- return;
1168
- }
1169
- }, { 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
+ );
1170
905
  if (mode.type === "edit") {
1171
906
  const editing = mode.key;
1172
907
  return /* @__PURE__ */ jsx7(
@@ -1304,7 +1039,15 @@ function App({ files }) {
1304
1039
  statusMsg,
1305
1040
  focus2: focus,
1306
1041
  interactive: false,
1307
- 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
+ )
1308
1051
  }
1309
1052
  );
1310
1053
  }
@@ -1372,7 +1115,9 @@ function App({ files }) {
1372
1115
  flash(`Encrypted ${selectedFile.relativePath}`);
1373
1116
  }
1374
1117
  } catch (err) {
1375
- flash(`Error: ${err instanceof Error ? err.message : String(err)}`);
1118
+ flash(
1119
+ `Error: ${err instanceof Error ? err.message : String(err)}`
1120
+ );
1376
1121
  }
1377
1122
  refreshKeys();
1378
1123
  setRevealed(/* @__PURE__ */ new Map());
@@ -1436,17 +1181,26 @@ function Layout({
1436
1181
  /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
1437
1182
  /* @__PURE__ */ jsx7(Text7, { bold: true, color: "cyan", children: "dotenvx-ui" }),
1438
1183
  /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
1439
- " ",
1184
+ " ",
1440
1185
  selectedFile.relativePath,
1441
- " \xB7 ",
1186
+ " \xB7 ",
1442
1187
  files.length,
1443
- " files \xB7 ",
1188
+ " files \xB7 ",
1444
1189
  encCount,
1445
1190
  " enc"
1446
1191
  ] })
1447
1192
  ] }),
1448
1193
  /* @__PURE__ */ jsxs7(Box7, { height: listRows, children: [
1449
- /* @__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
+ ),
1450
1204
  /* @__PURE__ */ jsx7(
1451
1205
  KeyTable,
1452
1206
  {
@@ -1462,11 +1216,26 @@ function Layout({
1462
1216
  )
1463
1217
  ] }),
1464
1218
  extra,
1465
- !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
+ ),
1466
1229
  /* @__PURE__ */ jsx7(StatusBar, { focus, message: statusMsg })
1467
1230
  ] });
1468
1231
  }
1469
- function ValuePreview({ keys, keyIndex, focus, revealed, width }) {
1232
+ function ValuePreview({
1233
+ keys,
1234
+ keyIndex,
1235
+ focus,
1236
+ revealed,
1237
+ width
1238
+ }) {
1470
1239
  if (focus !== "keys") return null;
1471
1240
  const k = keys[keyIndex];
1472
1241
  if (!k) return null;
@@ -1490,35 +1259,47 @@ function ValuePreview({ keys, keyIndex, focus, revealed, width }) {
1490
1259
  children: [
1491
1260
  /* @__PURE__ */ jsxs7(Text7, { bold: true, color: "cyan", children: [
1492
1261
  k.key,
1493
- " "
1262
+ " "
1494
1263
  ] }),
1495
- /* @__PURE__ */ jsx7(Text7, { dimColor: true, children: "\xB7 " }),
1496
- /* @__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 }) })
1497
1266
  ]
1498
1267
  }
1499
1268
  );
1500
1269
  }
1501
1270
  function ConfirmDelete({ keyName, onConfirm, onCancel }) {
1502
1271
  const { isRawModeSupported } = useStdin6();
1503
- useInput6((input) => {
1504
- if (input === "y" || input === "Y") onConfirm();
1505
- else onCancel();
1506
- }, { isActive: isRawModeSupported });
1272
+ useInput6(
1273
+ (input) => {
1274
+ if (input === "y" || input === "Y") onConfirm();
1275
+ else onCancel();
1276
+ },
1277
+ { isActive: isRawModeSupported }
1278
+ );
1507
1279
  return /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
1508
1280
  /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
1509
1281
  "Delete ",
1510
1282
  /* @__PURE__ */ jsx7(Text7, { bold: true, children: keyName }),
1511
- "? "
1283
+ "?",
1284
+ " "
1512
1285
  ] }),
1513
- /* @__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" })
1514
1287
  ] });
1515
1288
  }
1516
- function ConfirmEncrypt({ decrypt, fileName, onConfirm, onCancel }) {
1289
+ function ConfirmEncrypt({
1290
+ decrypt,
1291
+ fileName,
1292
+ onConfirm,
1293
+ onCancel
1294
+ }) {
1517
1295
  const { isRawModeSupported } = useStdin6();
1518
- useInput6((input) => {
1519
- if (input === "y" || input === "Y") onConfirm();
1520
- else onCancel();
1521
- }, { isActive: isRawModeSupported });
1296
+ useInput6(
1297
+ (input) => {
1298
+ if (input === "y" || input === "Y") onConfirm();
1299
+ else onCancel();
1300
+ },
1301
+ { isActive: isRawModeSupported }
1302
+ );
1522
1303
  const action = decrypt ? "Decrypt" : "Encrypt";
1523
1304
  const color = decrypt ? "yellow" : "green";
1524
1305
  return /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
@@ -1526,42 +1307,52 @@ function ConfirmEncrypt({ decrypt, fileName, onConfirm, onCancel }) {
1526
1307
  action,
1527
1308
  " ",
1528
1309
  /* @__PURE__ */ jsx7(Text7, { bold: true, children: fileName }),
1529
- "? "
1310
+ "?",
1311
+ " "
1530
1312
  ] }),
1531
- /* @__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" })
1532
1314
  ] });
1533
1315
  }
1534
- function ConfirmAddEncrypt({ keyName, onEncrypt, onPlain, onCancel }) {
1316
+ function ConfirmAddEncrypt({
1317
+ keyName,
1318
+ onEncrypt,
1319
+ onPlain,
1320
+ onCancel
1321
+ }) {
1535
1322
  const { isRawModeSupported } = useStdin6();
1536
- useInput6((input, key) => {
1537
- if (key.escape) {
1538
- onCancel();
1539
- return;
1540
- }
1541
- if (input === "y" || input === "Y") {
1542
- onEncrypt();
1543
- return;
1544
- }
1545
- if (input === "n" || input === "N" || key.return) {
1546
- onPlain();
1547
- return;
1548
- }
1549
- }, { 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
+ );
1550
1340
  return /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, children: [
1551
1341
  /* @__PURE__ */ jsxs7(Text7, { children: [
1552
1342
  "Encrypt ",
1553
1343
  /* @__PURE__ */ jsx7(Text7, { bold: true, children: keyName }),
1554
- "? "
1344
+ "?",
1345
+ " "
1555
1346
  ] }),
1556
- /* @__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" })
1557
1348
  ] });
1558
1349
  }
1559
1350
 
1560
1351
  // src/tui/ErrorBoundary.tsx
1561
- import React4 from "react";
1562
1352
  import { Box as Box8, Text as Text8 } from "ink";
1353
+ import React from "react";
1563
1354
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1564
- var ErrorBoundary = class extends React4.Component {
1355
+ var ErrorBoundary = class extends React.Component {
1565
1356
  state = { error: null };
1566
1357
  static getDerivedStateFromError(error) {
1567
1358
  return { error };
@@ -1579,9 +1370,8 @@ var ErrorBoundary = class extends React4.Component {
1579
1370
  };
1580
1371
 
1581
1372
  // src/cli.tsx
1582
- import { createRequire as createRequire2 } from "module";
1583
1373
  import { jsx as jsx9 } from "react/jsx-runtime";
1584
- var { version } = createRequire2(import.meta.url)("../package.json");
1374
+ var { version } = createRequire(import.meta.url)("../package.json");
1585
1375
  var HELP = `
1586
1376
  dotenvx-ui \u2014 terminal and web UI for dotenvx environment files
1587
1377
 
@@ -1610,12 +1400,14 @@ var commands = {
1610
1400
  console.log(HELP);
1611
1401
  process.exit(0);
1612
1402
  },
1613
- "ui": runWebUI
1403
+ ui: runWebUI
1614
1404
  };
1615
1405
  var [, , command] = process.argv;
1616
1406
  if (command !== void 0 && !(command in commands)) {
1617
- console.error(`Unknown command: ${command}
1618
- Run dotenvx-ui --help for usage.`);
1407
+ console.error(
1408
+ `Unknown command: ${command}
1409
+ Run dotenvx-ui --help for usage.`
1410
+ );
1619
1411
  process.exit(1);
1620
1412
  }
1621
1413
  commands[command ?? ""]?.() ?? runTUI();
@@ -1625,9 +1417,17 @@ function runTUI() {
1625
1417
  console.error("No .env files found in this directory.");
1626
1418
  process.exit(1);
1627
1419
  }
1628
- 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
+ );
1629
1424
  }
1630
- function runWebUI() {
1631
- console.log("Web UI \u2014 coming soon");
1632
- 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());
1633
1433
  }