@envsec/tui 1.0.0-beta.13

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/views.js ADDED
@@ -0,0 +1,873 @@
1
+ /**
2
+ * TUI views — each view is a self-contained interactive screen.
3
+ * All views consume SecretStore via Effect dependency injection.
4
+ */
5
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { resolve } from "node:path";
7
+ import { expiresAtFromNow, formatTimeDistance, icons, parseDuration, SecretStore, } from "@envsec/core";
8
+ import { Effect } from "effect";
9
+ import { renderEmpty, renderFooter, renderHeader, renderMenu, renderMessage, renderTable, } from "./components.js";
10
+ import { c, cursor, readKey, readLine, screen, write, writeLine, } from "./terminal.js";
11
+ // ── Main Menu ───────────────────────────────────────────────────────
12
+ const mainMenuItems = [
13
+ {
14
+ key: "contexts",
15
+ label: "Contexts",
16
+ icon: icons.folder,
17
+ hint: "Browse & manage contexts",
18
+ },
19
+ {
20
+ key: "secrets",
21
+ label: "Secrets",
22
+ icon: icons.key,
23
+ hint: "View secrets in current context",
24
+ },
25
+ {
26
+ key: "add",
27
+ label: "Add Secret",
28
+ icon: icons.save,
29
+ hint: "Store a new secret",
30
+ },
31
+ {
32
+ key: "search",
33
+ label: "Search",
34
+ icon: icons.search,
35
+ hint: "Search secrets or contexts",
36
+ },
37
+ {
38
+ key: "commands",
39
+ label: "Saved Commands",
40
+ icon: icons.bolt,
41
+ hint: "Manage saved commands",
42
+ },
43
+ {
44
+ key: "audit",
45
+ label: "Audit",
46
+ icon: icons.chart,
47
+ hint: "Check expiring secrets",
48
+ },
49
+ {
50
+ key: "import",
51
+ label: "Import .env",
52
+ icon: icons.upload,
53
+ hint: "Load secrets from .env file",
54
+ },
55
+ {
56
+ key: "export",
57
+ label: "Export .env",
58
+ icon: icons.download,
59
+ hint: "Export secrets to .env file",
60
+ },
61
+ ];
62
+ export const mainMenuView = (initialContext) =>
63
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: interactive TUI loop with menu routing
64
+ Effect.gen(function* () {
65
+ let ctx = initialContext;
66
+ let selected = 0;
67
+ let message = null;
68
+ const render = () => {
69
+ write(screen.clear);
70
+ let row = renderHeader(ctx, "Main Menu");
71
+ row++;
72
+ row = renderMenu(mainMenuItems, selected, row);
73
+ row++;
74
+ if (message) {
75
+ renderMessage(row, message.text, message.type);
76
+ row++;
77
+ }
78
+ renderFooter([
79
+ "↑↓ navigate",
80
+ "Enter select",
81
+ "c change context",
82
+ "q quit",
83
+ ]);
84
+ };
85
+ let running = true;
86
+ while (running) {
87
+ render();
88
+ const key = yield* readKey;
89
+ message = null;
90
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
91
+ running = false;
92
+ continue;
93
+ }
94
+ if (key.name === "up") {
95
+ selected = (selected - 1 + mainMenuItems.length) % mainMenuItems.length;
96
+ }
97
+ else if (key.name === "down") {
98
+ selected = (selected + 1) % mainMenuItems.length;
99
+ }
100
+ else if (key.name === "c") {
101
+ const result = yield* contextsView();
102
+ if (result === "quit") {
103
+ running = false;
104
+ }
105
+ else if (result === "clearContext") {
106
+ ctx = null;
107
+ message = { text: "Context cleared", type: "info" };
108
+ }
109
+ else if (typeof result === "object" && "setContext" in result) {
110
+ ctx = result.setContext;
111
+ message = {
112
+ text: `Context set to "${result.setContext}"`,
113
+ type: "success",
114
+ };
115
+ }
116
+ }
117
+ else if (key.name === "return") {
118
+ const item = mainMenuItems[selected];
119
+ if (!item) {
120
+ continue;
121
+ }
122
+ switch (item.key) {
123
+ case "contexts": {
124
+ const result = yield* contextsView();
125
+ if (result === "quit") {
126
+ running = false;
127
+ }
128
+ else if (result === "clearContext") {
129
+ ctx = null;
130
+ message = { text: "Context cleared", type: "info" };
131
+ }
132
+ else if (typeof result === "object" && "setContext" in result) {
133
+ ctx = result.setContext;
134
+ message = {
135
+ text: `Context set to "${result.setContext}"`,
136
+ type: "success",
137
+ };
138
+ }
139
+ break;
140
+ }
141
+ case "secrets": {
142
+ let secretsCtx = ctx;
143
+ if (!secretsCtx) {
144
+ const picked = yield* selectContext("Secrets — Select Context");
145
+ if (!picked) {
146
+ break;
147
+ }
148
+ secretsCtx = picked;
149
+ }
150
+ const result = yield* secretsView(secretsCtx);
151
+ if (result === "quit") {
152
+ running = false;
153
+ }
154
+ break;
155
+ }
156
+ case "add": {
157
+ let addCtx = ctx;
158
+ if (!addCtx) {
159
+ const picked = yield* selectContext("Add Secret — Select Context");
160
+ if (!picked) {
161
+ break;
162
+ }
163
+ addCtx = picked;
164
+ }
165
+ const result = yield* addSecretView(addCtx);
166
+ if (result === "quit") {
167
+ running = false;
168
+ }
169
+ break;
170
+ }
171
+ case "search": {
172
+ const result = yield* searchView(ctx);
173
+ if (result === "quit") {
174
+ running = false;
175
+ }
176
+ break;
177
+ }
178
+ case "commands": {
179
+ const result = yield* commandsView();
180
+ if (result === "quit") {
181
+ running = false;
182
+ }
183
+ break;
184
+ }
185
+ case "audit": {
186
+ const result = yield* auditView(ctx);
187
+ if (result === "quit") {
188
+ running = false;
189
+ }
190
+ break;
191
+ }
192
+ case "import": {
193
+ let importCtx = ctx;
194
+ if (!importCtx) {
195
+ const picked = yield* selectContext("Import — Select Context");
196
+ if (!picked) {
197
+ break;
198
+ }
199
+ importCtx = picked;
200
+ }
201
+ const result = yield* importView(importCtx);
202
+ if (result === "quit") {
203
+ running = false;
204
+ }
205
+ break;
206
+ }
207
+ case "export": {
208
+ let exportCtx = ctx;
209
+ if (!exportCtx) {
210
+ const picked = yield* selectContext("Export — Select Context");
211
+ if (!picked) {
212
+ break;
213
+ }
214
+ exportCtx = picked;
215
+ }
216
+ const result = yield* exportView(exportCtx);
217
+ if (result === "quit") {
218
+ running = false;
219
+ }
220
+ break;
221
+ }
222
+ default:
223
+ break;
224
+ }
225
+ }
226
+ }
227
+ });
228
+ // ── Select Context (arrow navigation) ───────────────────────────────
229
+ const selectContext = (title) => Effect.gen(function* () {
230
+ const contexts = yield* SecretStore.listContexts().pipe(Effect.catchAll(() => Effect.succeed([])));
231
+ if (contexts.length === 0) {
232
+ write(screen.clear);
233
+ renderHeader(null, title);
234
+ renderEmpty(4, "No contexts found. Add secrets to create one.");
235
+ renderFooter(["any key to go back"]);
236
+ yield* readKey;
237
+ return null;
238
+ }
239
+ let selected = 0;
240
+ const loop = () =>
241
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: interactive TUI loop
242
+ Effect.gen(function* () {
243
+ write(screen.clear);
244
+ let row = renderHeader(null, title);
245
+ row++;
246
+ const items = contexts.map((ctx) => ({
247
+ key: ctx.context,
248
+ label: ctx.context,
249
+ icon: icons.folder,
250
+ hint: `${ctx.count} secrets`,
251
+ }));
252
+ selected = Math.min(selected, items.length - 1);
253
+ row = renderMenu(items, selected, row);
254
+ renderFooter(["↑↓ navigate", "Enter select", "Esc back"]);
255
+ const key = yield* readKey;
256
+ if (key.name === "escape" || (key.ctrl && key.name === "c")) {
257
+ return null;
258
+ }
259
+ if (key.name === "up") {
260
+ selected = (selected - 1 + items.length) % items.length;
261
+ return yield* loop();
262
+ }
263
+ if (key.name === "down") {
264
+ selected = (selected + 1) % items.length;
265
+ return yield* loop();
266
+ }
267
+ if (key.name === "return") {
268
+ const ctx = contexts[selected];
269
+ return ctx ? ctx.context : null;
270
+ }
271
+ return yield* loop();
272
+ });
273
+ return yield* loop();
274
+ });
275
+ // ── Contexts View ───────────────────────────────────────────────────
276
+ const contextsView = () => Effect.gen(function* () {
277
+ let selected = 0;
278
+ const loop = () =>
279
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: interactive TUI loop
280
+ Effect.gen(function* () {
281
+ const contexts = yield* SecretStore.listContexts().pipe(Effect.catchAll(() => Effect.succeed([])));
282
+ write(screen.clear);
283
+ let row = renderHeader(null, "Contexts");
284
+ row++;
285
+ if (contexts.length === 0) {
286
+ row = renderEmpty(row, "No contexts found. Add secrets to create one.");
287
+ renderFooter(["Esc back", "q quit"]);
288
+ const key = yield* readKey;
289
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
290
+ return "quit";
291
+ }
292
+ return "back";
293
+ }
294
+ const items = contexts.map((ctx) => ({
295
+ key: ctx.context,
296
+ label: ctx.context,
297
+ icon: icons.folder,
298
+ hint: `${ctx.count} secrets`,
299
+ }));
300
+ selected = Math.min(selected, items.length - 1);
301
+ row = renderMenu(items, selected, row);
302
+ row++;
303
+ renderFooter([
304
+ "↑↓ navigate",
305
+ "Enter view secrets",
306
+ "s set context",
307
+ "x clear context",
308
+ "d delete all",
309
+ "Esc back",
310
+ "q quit",
311
+ ]);
312
+ const key = yield* readKey;
313
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
314
+ return "quit";
315
+ }
316
+ if (key.name === "escape") {
317
+ return "back";
318
+ }
319
+ if (key.name === "up") {
320
+ selected = (selected - 1 + items.length) % items.length;
321
+ }
322
+ if (key.name === "down") {
323
+ selected = (selected + 1) % items.length;
324
+ }
325
+ if (key.name === "s") {
326
+ const ctx = contexts[selected];
327
+ if (ctx) {
328
+ return { setContext: ctx.context };
329
+ }
330
+ }
331
+ if (key.name === "x") {
332
+ return "clearContext";
333
+ }
334
+ if (key.name === "return") {
335
+ const ctx = contexts[selected];
336
+ if (ctx) {
337
+ const result = yield* secretsView(ctx.context);
338
+ if (result === "quit") {
339
+ return "quit";
340
+ }
341
+ }
342
+ }
343
+ if (key.name === "d") {
344
+ const ctx = contexts[selected];
345
+ if (ctx) {
346
+ yield* confirmDeleteContext(ctx.context);
347
+ }
348
+ }
349
+ return yield* loop();
350
+ });
351
+ return yield* loop();
352
+ });
353
+ // ── Confirm delete context ──────────────────────────────────────────
354
+ const confirmDeleteContext = (context) => Effect.gen(function* () {
355
+ write(screen.clear);
356
+ renderHeader(context, "Delete All Secrets");
357
+ writeLine(5, ` ${icons.warning} ${c.bold("Delete ALL secrets")} in context ${c.bold(c.cyan(`"${context}"`))}?`);
358
+ writeLine(7, ` ${c.dim("This cannot be undone.")}`);
359
+ writeLine(9, ` ${c.green("y")} confirm ${c.dim("/")} ${c.red("n")} cancel ${c.dim("/")} ${c.dim("Esc")} back`);
360
+ const key = yield* readKey;
361
+ if (key.name === "y") {
362
+ const secrets = yield* SecretStore.list(context).pipe(Effect.catchAll(() => Effect.succeed([])));
363
+ yield* SecretStore.beginBatch().pipe(Effect.catchAll(() => Effect.void));
364
+ for (const s of secrets) {
365
+ yield* SecretStore.remove(context, s.key).pipe(Effect.catchAll(() => Effect.void));
366
+ }
367
+ yield* SecretStore.endBatch().pipe(Effect.catchAll(() => Effect.void));
368
+ return true;
369
+ }
370
+ return false;
371
+ });
372
+ // ── Secrets View ────────────────────────────────────────────────────
373
+ const secretsView = (context) => Effect.gen(function* () {
374
+ let selected = 0;
375
+ let message = null;
376
+ const loop = () =>
377
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: interactive TUI loop
378
+ Effect.gen(function* () {
379
+ const secrets = yield* SecretStore.list(context).pipe(Effect.catchAll(() => Effect.succeed([])));
380
+ write(screen.clear);
381
+ let row = renderHeader(context, "Secrets");
382
+ row++;
383
+ if (secrets.length === 0) {
384
+ row = renderEmpty(row, "No secrets in this context.");
385
+ renderFooter(["a add secret", "Esc back", "q quit"]);
386
+ if (message) {
387
+ renderMessage(row, message.text, message.type);
388
+ }
389
+ const key = yield* readKey;
390
+ message = null;
391
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
392
+ return "quit";
393
+ }
394
+ if (key.name === "escape") {
395
+ return "back";
396
+ }
397
+ if (key.name === "a") {
398
+ yield* addSecretView(context);
399
+ return yield* loop();
400
+ }
401
+ return yield* loop();
402
+ }
403
+ selected = Math.min(selected, secrets.length - 1);
404
+ const columns = [
405
+ { header: "KEY", width: 30 },
406
+ { header: "UPDATED", width: 20 },
407
+ { header: "EXPIRES", width: 20 },
408
+ ];
409
+ const tableRows = secrets.map((s) => [
410
+ s.key,
411
+ s.updated_at.slice(0, 16).replace("T", " "),
412
+ s.expires_at
413
+ ? s.expires_at.slice(0, 16).replace("T", " ")
414
+ : c.dim("never"),
415
+ ]);
416
+ row = renderTable(columns, tableRows, selected, row);
417
+ row++;
418
+ if (message) {
419
+ renderMessage(row, message.text, message.type);
420
+ row++;
421
+ }
422
+ writeLine(row + 1, ` ${c.dim(`${secrets.length} secrets`)}`);
423
+ renderFooter([
424
+ "↑↓ navigate",
425
+ "Enter reveal",
426
+ "a add",
427
+ "d delete",
428
+ "Esc back",
429
+ "q quit",
430
+ ]);
431
+ const key = yield* readKey;
432
+ message = null;
433
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
434
+ return "quit";
435
+ }
436
+ if (key.name === "escape") {
437
+ return "back";
438
+ }
439
+ if (key.name === "up") {
440
+ selected = (selected - 1 + secrets.length) % secrets.length;
441
+ }
442
+ if (key.name === "down") {
443
+ selected = (selected + 1) % secrets.length;
444
+ }
445
+ if (key.name === "return") {
446
+ const secret = secrets[selected];
447
+ if (secret) {
448
+ yield* revealSecretView(context, secret.key);
449
+ }
450
+ }
451
+ if (key.name === "a") {
452
+ yield* addSecretView(context);
453
+ }
454
+ if (key.name === "d") {
455
+ const secret = secrets[selected];
456
+ if (secret) {
457
+ const confirmed = yield* confirmDelete(context, secret.key);
458
+ if (confirmed) {
459
+ message = { text: `Deleted "${secret.key}"`, type: "success" };
460
+ }
461
+ }
462
+ }
463
+ return yield* loop();
464
+ });
465
+ return yield* loop();
466
+ });
467
+ // ── Reveal Secret View ──────────────────────────────────────────────
468
+ const revealSecretView = (context, key) => Effect.gen(function* () {
469
+ write(screen.clear);
470
+ let row = renderHeader(context, "Secret Detail");
471
+ row++;
472
+ const meta = yield* SecretStore.getMetadata(context, key).pipe(Effect.catchAll(() => Effect.succeed(null)));
473
+ writeLine(row, ` ${c.bold("Key:")} ${c.cyan(key)}`);
474
+ row++;
475
+ if (meta) {
476
+ writeLine(row, ` ${c.bold("Created:")} ${c.dim(meta.created_at)}`);
477
+ row++;
478
+ writeLine(row, ` ${c.bold("Updated:")} ${c.dim(meta.updated_at)}`);
479
+ row++;
480
+ writeLine(row, ` ${c.bold("Expires:")} ${meta.expires_at ? c.yellow(meta.expires_at) : c.dim("never")}`);
481
+ row++;
482
+ }
483
+ row++;
484
+ writeLine(row, ` ${c.dim("Press 'r' to reveal value, Esc to go back")}`);
485
+ renderFooter(["r reveal value", "Esc back", "q quit"]);
486
+ const loop = () => Effect.gen(function* () {
487
+ const k = yield* readKey;
488
+ if (k.name === "q" || (k.ctrl && k.name === "c")) {
489
+ return "quit";
490
+ }
491
+ if (k.name === "escape") {
492
+ return "back";
493
+ }
494
+ if (k.name === "r") {
495
+ const value = yield* SecretStore.get(context, key).pipe(Effect.catchAll((e) => Effect.succeed(`[error: ${e._tag}]`)));
496
+ row++;
497
+ writeLine(row, ` ${c.bold("Value:")} ${c.green(String(value))}`);
498
+ row += 2;
499
+ writeLine(row, ` ${c.dim("Press any key to go back (value will be hidden)")}`);
500
+ renderFooter(["any key to go back"]);
501
+ yield* readKey;
502
+ return "back";
503
+ }
504
+ return yield* loop();
505
+ });
506
+ return yield* loop();
507
+ });
508
+ // ── Confirm Delete ──────────────────────────────────────────────────
509
+ const confirmDelete = (context, key) => Effect.gen(function* () {
510
+ write(screen.clear);
511
+ renderHeader(context, "Delete Secret");
512
+ writeLine(5, ` ${icons.warning} Delete secret ${c.bold(c.cyan(`"${key}"`))}?`);
513
+ writeLine(7, ` ${c.green("y")} confirm ${c.dim("/")} ${c.red("n")} cancel ${c.dim("/")} ${c.dim("Esc")} back`);
514
+ const k = yield* readKey;
515
+ if (k.name === "y") {
516
+ yield* SecretStore.remove(context, key).pipe(Effect.catchAll(() => Effect.void));
517
+ return true;
518
+ }
519
+ return false;
520
+ });
521
+ // ── Add Secret View ─────────────────────────────────────────────────
522
+ const addSecretView = (context) => Effect.gen(function* () {
523
+ write(screen.clear);
524
+ let row = renderHeader(context, "Add Secret");
525
+ row++;
526
+ write(cursor.show);
527
+ writeLine(row, "");
528
+ row++;
529
+ const key = yield* readLine(` ${c.cyan("Key:")} `);
530
+ if (key === null || key.trim() === "") {
531
+ write(cursor.hide);
532
+ return "back";
533
+ }
534
+ const value = yield* readLine(` ${c.cyan("Value:")} `, { mask: true });
535
+ if (value === null || value.trim() === "") {
536
+ write(cursor.hide);
537
+ return "back";
538
+ }
539
+ const expiresInput = yield* readLine(` ${c.cyan("Expires (e.g. 30d, 1y, empty for never):")} `);
540
+ write(cursor.hide);
541
+ let expiresAt = null;
542
+ if (expiresInput && expiresInput.trim() !== "") {
543
+ const duration = yield* parseDuration(expiresInput.trim()).pipe(Effect.catchAll(() => Effect.succeed(null)));
544
+ if (duration) {
545
+ expiresAt = expiresAtFromNow(duration);
546
+ }
547
+ }
548
+ yield* SecretStore.set(context, key.trim(), value, expiresAt).pipe(Effect.catchAll((e) => {
549
+ renderMessage(row + 2, `Error: ${e.message}`, "error");
550
+ return Effect.void;
551
+ }));
552
+ row += 2;
553
+ renderMessage(row, `Secret "${key.trim()}" stored`, "success");
554
+ row++;
555
+ writeLine(row, ` ${c.dim("Press any key to continue...")}`);
556
+ yield* readKey;
557
+ return "back";
558
+ });
559
+ // ── Search View ─────────────────────────────────────────────────────
560
+ const searchView = (context) => Effect.gen(function* () {
561
+ write(screen.clear);
562
+ let row = renderHeader(context, "Search");
563
+ row++;
564
+ write(cursor.show);
565
+ writeLine(row, ` ${c.cyan("Pattern (glob):")}`);
566
+ row++;
567
+ const pattern = yield* readLine(` ${c.dim("›")} `);
568
+ write(cursor.hide);
569
+ if (pattern === null || pattern.trim() === "") {
570
+ return "back";
571
+ }
572
+ row += 2;
573
+ if (context) {
574
+ const results = yield* SecretStore.search(context, pattern.trim()).pipe(Effect.catchAll(() => Effect.succeed([])));
575
+ if (results.length === 0) {
576
+ renderMessage(row, "No secrets found.", "info");
577
+ }
578
+ else {
579
+ writeLine(row, ` ${c.bold(`${results.length} results:`)}`);
580
+ row++;
581
+ for (const r of results.slice(0, 20)) {
582
+ writeLine(row, ` ${icons.key} ${r.key}`);
583
+ row++;
584
+ }
585
+ }
586
+ }
587
+ else {
588
+ const results = yield* SecretStore.searchContexts(pattern.trim()).pipe(Effect.catchAll(() => Effect.succeed([])));
589
+ if (results.length === 0) {
590
+ renderMessage(row, "No contexts found.", "info");
591
+ }
592
+ else {
593
+ writeLine(row, ` ${c.bold(`${results.length} contexts:`)}`);
594
+ row++;
595
+ for (const r of results.slice(0, 20)) {
596
+ writeLine(row, ` ${icons.folder} ${r.context} ${c.dim(`(${r.count} secrets)`)}`);
597
+ row++;
598
+ }
599
+ }
600
+ }
601
+ row += 2;
602
+ writeLine(row, ` ${c.dim("Press any key to continue...")}`);
603
+ yield* readKey;
604
+ return "back";
605
+ });
606
+ // ── Commands View ───────────────────────────────────────────────────
607
+ const commandsView = () => Effect.gen(function* () {
608
+ let selected = 0;
609
+ const loop = () =>
610
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: interactive TUI loop
611
+ Effect.gen(function* () {
612
+ const commands = yield* SecretStore.listCommands().pipe(Effect.catchAll(() => Effect.succeed([])));
613
+ write(screen.clear);
614
+ let row = renderHeader(null, "Saved Commands");
615
+ row++;
616
+ if (commands.length === 0) {
617
+ row = renderEmpty(row, "No saved commands.");
618
+ renderFooter(["Esc back", "q quit"]);
619
+ const key = yield* readKey;
620
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
621
+ return "quit";
622
+ }
623
+ return "back";
624
+ }
625
+ selected = Math.min(selected, commands.length - 1);
626
+ const columns = [
627
+ { header: "NAME", width: 20 },
628
+ { header: "COMMAND", width: 30 },
629
+ { header: "CONTEXT", width: 20 },
630
+ ];
631
+ const tableRows = commands.map((cmd) => [
632
+ cmd.name,
633
+ cmd.command.length > 30
634
+ ? `${cmd.command.slice(0, 27)}...`
635
+ : cmd.command,
636
+ cmd.context,
637
+ ]);
638
+ row = renderTable(columns, tableRows, selected, row);
639
+ row++;
640
+ writeLine(row, ` ${c.dim(`${commands.length} commands`)}`);
641
+ renderFooter(["↑↓ navigate", "d delete", "Esc back", "q quit"]);
642
+ const key = yield* readKey;
643
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
644
+ return "quit";
645
+ }
646
+ if (key.name === "escape") {
647
+ return "back";
648
+ }
649
+ if (key.name === "up") {
650
+ selected = (selected - 1 + commands.length) % commands.length;
651
+ }
652
+ if (key.name === "down") {
653
+ selected = (selected + 1) % commands.length;
654
+ }
655
+ if (key.name === "d") {
656
+ const cmd = commands[selected];
657
+ if (cmd) {
658
+ yield* SecretStore.removeCommand(cmd.name).pipe(Effect.catchAll(() => Effect.void));
659
+ }
660
+ }
661
+ return yield* loop();
662
+ });
663
+ return yield* loop();
664
+ });
665
+ // ── Audit View ──────────────────────────────────────────────────────
666
+ const auditView = (context) =>
667
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: interactive TUI view
668
+ Effect.gen(function* () {
669
+ write(screen.clear);
670
+ let row = renderHeader(context, "Audit — Expiring Secrets");
671
+ row++;
672
+ const windowMs = 30 * 24 * 60 * 60 * 1000; // 30 days
673
+ const now = Date.now();
674
+ if (context) {
675
+ const secrets = yield* SecretStore.listExpiring(context, windowMs).pipe(Effect.catchAll(() => Effect.succeed([])));
676
+ if (secrets.length === 0) {
677
+ renderMessage(row, "No secrets expiring within 30 days.", "success");
678
+ }
679
+ else {
680
+ writeLine(row, ` ${c.bold(`${secrets.length} secrets expiring within 30 days:`)}`);
681
+ row += 2;
682
+ for (const s of secrets.slice(0, 20)) {
683
+ const expired = s.expires_at
684
+ ? new Date(`${s.expires_at}Z`).getTime() <= now
685
+ : false;
686
+ const icon = expired ? icons.expired : icons.clock;
687
+ const status = expired ? c.red("EXPIRED") : c.yellow("expiring");
688
+ const distance = s.expires_at ? formatTimeDistance(s.expires_at) : "";
689
+ writeLine(row, ` ${icon} ${s.key} ${status} ${c.dim(distance)}`);
690
+ row++;
691
+ }
692
+ }
693
+ }
694
+ else {
695
+ const secrets = yield* SecretStore.listAllExpiring(windowMs).pipe(Effect.catchAll(() => Effect.succeed([])));
696
+ if (secrets.length === 0) {
697
+ renderMessage(row, "No secrets expiring within 30 days across all contexts.", "success");
698
+ }
699
+ else {
700
+ writeLine(row, ` ${c.bold(`${secrets.length} secrets expiring within 30 days:`)}`);
701
+ row += 2;
702
+ for (const s of secrets.slice(0, 20)) {
703
+ const expired = s.expires_at
704
+ ? new Date(`${s.expires_at}Z`).getTime() <= now
705
+ : false;
706
+ const icon = expired ? icons.expired : icons.clock;
707
+ const status = expired ? c.red("EXPIRED") : c.yellow("expiring");
708
+ const distance = s.expires_at ? formatTimeDistance(s.expires_at) : "";
709
+ writeLine(row, ` ${icon} ${c.dim(`[${s.env}]`)} ${s.key} ${status} ${c.dim(distance)}`);
710
+ row++;
711
+ }
712
+ }
713
+ }
714
+ // ── Env file exports ──────────────────────────────────────────────
715
+ row = yield* renderEnvFileExports(row, context);
716
+ row += 2;
717
+ writeLine(row, ` ${c.dim("Press any key to continue...")}`);
718
+ renderFooter(["any key to go back"]);
719
+ yield* readKey;
720
+ return "back";
721
+ });
722
+ // ── Env file exports (audit subsection) ─────────────────────────────
723
+ const renderEnvFileExports = (startRow, contextFilter) => Effect.gen(function* () {
724
+ let row = startRow;
725
+ const allExports = yield* SecretStore.listEnvFileExports().pipe(Effect.catchAll(() => Effect.succeed([])));
726
+ // Prune stale exports (files no longer on disk)
727
+ const alive = [];
728
+ const stale = [];
729
+ for (const e of allExports) {
730
+ if (existsSync(e.path)) {
731
+ alive.push(e);
732
+ }
733
+ else {
734
+ stale.push(e);
735
+ }
736
+ }
737
+ for (const e of stale) {
738
+ yield* SecretStore.removeEnvFileExport(e.path).pipe(Effect.catchAll(() => Effect.void));
739
+ }
740
+ if (stale.length > 0) {
741
+ row += 2;
742
+ writeLine(row, ` ${icons.broom} Removed ${c.bold(String(stale.length))} stale env file record${stale.length === 1 ? "" : "s"} ${c.dim("(files no longer on disk)")}`);
743
+ }
744
+ const filtered = contextFilter
745
+ ? alive.filter((e) => e.context === contextFilter)
746
+ : alive;
747
+ if (filtered.length === 0) {
748
+ return row;
749
+ }
750
+ row += 2;
751
+ writeLine(row, ` ${icons.file} ${c.bold("Generated .env files:")}`);
752
+ row++;
753
+ for (const e of filtered) {
754
+ const date = e.created_at.replace("T", " ").slice(0, 19);
755
+ row++;
756
+ writeLine(row, ` ${icons.file} ${e.path}`);
757
+ row++;
758
+ writeLine(row, ` ${c.dim(`context: ${e.context} generated: ${date}`)}`);
759
+ }
760
+ row += 2;
761
+ writeLine(row, ` ${icons.chart} ${c.bold(String(filtered.length))} env file${filtered.length === 1 ? "" : "s"} generated`);
762
+ return row;
763
+ });
764
+ // ── Import View ─────────────────────────────────────────────────────
765
+ const importView = (context) => Effect.gen(function* () {
766
+ write(screen.clear);
767
+ let row = renderHeader(context, "Import .env File");
768
+ row++;
769
+ write(cursor.show);
770
+ writeLine(row, ` ${c.cyan("File path (default: .env):")}`);
771
+ row++;
772
+ const filePath = yield* readLine(` ${c.dim("›")} `);
773
+ write(cursor.hide);
774
+ if (filePath === null) {
775
+ return "back";
776
+ }
777
+ const path = filePath.trim() === "" ? ".env" : filePath.trim();
778
+ row += 2;
779
+ writeLine(row, ` ${c.dim("Reading")} ${path}${c.dim("...")}`);
780
+ const content = yield* Effect.try({
781
+ try: () => readFileSync(path, "utf-8"),
782
+ catch: () => new Error(`Cannot read file: ${path}`),
783
+ }).pipe(Effect.catchAll((e) => {
784
+ renderMessage(row + 1, String(e), "error");
785
+ return Effect.succeed(null);
786
+ }));
787
+ if (content === null) {
788
+ row += 3;
789
+ writeLine(row, ` ${c.dim("Press any key to continue...")}`);
790
+ yield* readKey;
791
+ return "back";
792
+ }
793
+ const lines = content.split("\n");
794
+ let added = 0;
795
+ yield* SecretStore.beginBatch().pipe(Effect.catchAll(() => Effect.void));
796
+ for (const line of lines) {
797
+ const trimmed = line.trim();
798
+ if (trimmed === "" || trimmed.startsWith("#")) {
799
+ continue;
800
+ }
801
+ const eqIndex = trimmed.indexOf("=");
802
+ if (eqIndex === -1) {
803
+ continue;
804
+ }
805
+ const key = trimmed.slice(0, eqIndex).trim();
806
+ const value = trimmed
807
+ .slice(eqIndex + 1)
808
+ .trim()
809
+ .replace(/^["']|["']$/g, "");
810
+ const secretKey = key.toLowerCase().replaceAll("_", ".");
811
+ yield* SecretStore.set(context, secretKey, value).pipe(Effect.catchAll(() => Effect.void));
812
+ added++;
813
+ }
814
+ yield* SecretStore.endBatch().pipe(Effect.catchAll(() => Effect.void));
815
+ row++;
816
+ renderMessage(row, `Imported ${added} secrets from ${path}`, "success");
817
+ row += 2;
818
+ writeLine(row, ` ${c.dim("Press any key to continue...")}`);
819
+ yield* readKey;
820
+ return "back";
821
+ });
822
+ // ── Export View ─────────────────────────────────────────────────────
823
+ const exportView = (context) => Effect.gen(function* () {
824
+ write(screen.clear);
825
+ let row = renderHeader(context, "Export .env File");
826
+ row++;
827
+ write(cursor.show);
828
+ writeLine(row, ` ${c.cyan("Output path (default: .env):")}`);
829
+ row++;
830
+ const filePath = yield* readLine(` ${c.dim("›")} `);
831
+ write(cursor.hide);
832
+ if (filePath === null) {
833
+ return "back";
834
+ }
835
+ const path = filePath.trim() === "" ? ".env" : filePath.trim();
836
+ row += 2;
837
+ writeLine(row, ` ${c.dim("Exporting secrets...")}`);
838
+ const secrets = yield* SecretStore.list(context).pipe(Effect.catchAll(() => Effect.succeed([])));
839
+ if (secrets.length === 0) {
840
+ row++;
841
+ renderMessage(row, "No secrets to export.", "info");
842
+ row += 2;
843
+ writeLine(row, ` ${c.dim("Press any key to continue...")}`);
844
+ yield* readKey;
845
+ return "back";
846
+ }
847
+ const lines = [];
848
+ for (const item of secrets) {
849
+ const value = yield* SecretStore.get(context, item.key).pipe(Effect.catchAll(() => Effect.succeed("")));
850
+ const envKey = item.key.toUpperCase().replaceAll(".", "_");
851
+ const escaped = String(value)
852
+ .replaceAll("\\", "\\\\")
853
+ .replaceAll('"', '\\"')
854
+ .replaceAll("\n", "\\n");
855
+ lines.push(`${envKey}="${escaped}"`);
856
+ }
857
+ yield* Effect.try({
858
+ try: () => writeFileSync(path, `${lines.join("\n")}\n`, "utf-8"),
859
+ catch: () => new Error(`Failed to write: ${path}`),
860
+ }).pipe(Effect.catchAll((e) => {
861
+ renderMessage(row + 1, String(e), "error");
862
+ return Effect.void;
863
+ }));
864
+ const absolutePath = resolve(path);
865
+ yield* SecretStore.trackEnvFileExport(context, absolutePath).pipe(Effect.catchAll(() => Effect.void));
866
+ row++;
867
+ renderMessage(row, `Exported ${lines.length} secrets to ${path}`, "success");
868
+ row += 2;
869
+ writeLine(row, ` ${c.dim("Press any key to continue...")}`);
870
+ yield* readKey;
871
+ return "back";
872
+ });
873
+ //# sourceMappingURL=views.js.map