@matthesketh/fleet 1.8.1 → 1.11.1

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.
Files changed (230) hide show
  1. package/README.md +186 -16
  2. package/dist/bin/fleet-agent.d.ts +2 -0
  3. package/dist/bin/fleet-agent.js +7 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +73 -31
  6. package/dist/commands/add.d.ts +2 -1
  7. package/dist/commands/add.js +66 -59
  8. package/dist/commands/audit.d.ts +1 -0
  9. package/dist/commands/audit.js +144 -0
  10. package/dist/commands/backup.d.ts +1 -0
  11. package/dist/commands/backup.js +510 -0
  12. package/dist/commands/boot-start.d.ts +3 -1
  13. package/dist/commands/boot-start.js +39 -47
  14. package/dist/commands/completions.d.ts +6 -0
  15. package/dist/commands/completions.js +83 -0
  16. package/dist/commands/config.d.ts +16 -0
  17. package/dist/commands/config.js +96 -0
  18. package/dist/commands/deploy.js +3 -2
  19. package/dist/commands/deps.js +5 -1
  20. package/dist/commands/doctor.d.ts +32 -0
  21. package/dist/commands/doctor.js +186 -0
  22. package/dist/commands/egress.d.ts +1 -1
  23. package/dist/commands/egress.js +13 -10
  24. package/dist/commands/freeze.d.ts +8 -4
  25. package/dist/commands/freeze.js +77 -59
  26. package/dist/commands/git.js +2 -2
  27. package/dist/commands/health.d.ts +2 -1
  28. package/dist/commands/health.js +38 -56
  29. package/dist/commands/init.d.ts +2 -1
  30. package/dist/commands/init.js +83 -73
  31. package/dist/commands/install-mcp.d.ts +3 -1
  32. package/dist/commands/install-mcp.js +53 -34
  33. package/dist/commands/list.d.ts +2 -1
  34. package/dist/commands/list.js +22 -19
  35. package/dist/commands/logs.js +1 -1
  36. package/dist/commands/patch-systemd.d.ts +7 -1
  37. package/dist/commands/patch-systemd.js +71 -31
  38. package/dist/commands/remove.d.ts +3 -1
  39. package/dist/commands/remove.js +37 -26
  40. package/dist/commands/restart.d.ts +4 -1
  41. package/dist/commands/restart.js +17 -20
  42. package/dist/commands/rollback.d.ts +4 -1
  43. package/dist/commands/rollback.js +33 -42
  44. package/dist/commands/secrets.js +157 -9
  45. package/dist/commands/start.d.ts +4 -1
  46. package/dist/commands/start.js +17 -20
  47. package/dist/commands/status.d.ts +1 -1
  48. package/dist/commands/status.js +21 -26
  49. package/dist/commands/stop.d.ts +4 -1
  50. package/dist/commands/stop.js +17 -20
  51. package/dist/commands/testflight.d.ts +1 -0
  52. package/dist/commands/testflight.js +193 -0
  53. package/dist/commands/update.d.ts +16 -0
  54. package/dist/commands/update.js +95 -0
  55. package/dist/core/audit/cache.d.ts +4 -0
  56. package/dist/core/audit/cache.js +37 -0
  57. package/dist/core/audit/config.d.ts +5 -0
  58. package/dist/core/audit/config.js +35 -0
  59. package/dist/core/audit/greenlight.d.ts +11 -0
  60. package/dist/core/audit/greenlight.js +81 -0
  61. package/dist/core/audit/reporters/cli.d.ts +3 -0
  62. package/dist/core/audit/reporters/cli.js +68 -0
  63. package/dist/core/audit/suppress.d.ts +6 -0
  64. package/dist/core/audit/suppress.js +37 -0
  65. package/dist/core/audit/target.d.ts +5 -0
  66. package/dist/core/audit/target.js +26 -0
  67. package/dist/core/audit/types.d.ts +54 -0
  68. package/dist/core/audit/types.js +5 -0
  69. package/dist/core/backup/browser-api.d.ts +66 -0
  70. package/dist/core/backup/browser-api.js +197 -0
  71. package/dist/core/backup/browser-server.d.ts +11 -0
  72. package/dist/core/backup/browser-server.js +241 -0
  73. package/dist/core/backup/browser-ui.d.ts +5 -0
  74. package/dist/core/backup/browser-ui.js +268 -0
  75. package/dist/core/backup/cloudflare.d.ts +7 -0
  76. package/dist/core/backup/cloudflare.js +82 -0
  77. package/dist/core/backup/config.d.ts +9 -0
  78. package/dist/core/backup/config.js +80 -0
  79. package/dist/core/backup/detect.d.ts +11 -0
  80. package/dist/core/backup/detect.js +71 -0
  81. package/dist/core/backup/dump.d.ts +11 -0
  82. package/dist/core/backup/dump.js +82 -0
  83. package/dist/core/backup/index.d.ts +9 -0
  84. package/dist/core/backup/index.js +9 -0
  85. package/dist/core/backup/repo.d.ts +71 -0
  86. package/dist/core/backup/repo.js +256 -0
  87. package/dist/core/backup/schedule.d.ts +17 -0
  88. package/dist/core/backup/schedule.js +90 -0
  89. package/dist/core/backup/sensitive.d.ts +5 -0
  90. package/dist/core/backup/sensitive.js +37 -0
  91. package/dist/core/backup/status.d.ts +3 -0
  92. package/dist/core/backup/status.js +29 -0
  93. package/dist/core/backup/statuspage.d.ts +23 -0
  94. package/dist/core/backup/statuspage.js +145 -0
  95. package/dist/core/backup/system.d.ts +24 -0
  96. package/dist/core/backup/system.js +209 -0
  97. package/dist/core/backup/totp.d.ts +16 -0
  98. package/dist/core/backup/totp.js +116 -0
  99. package/dist/core/backup/types.d.ts +70 -0
  100. package/dist/core/backup/types.js +7 -0
  101. package/dist/core/backup/unlock.d.ts +19 -0
  102. package/dist/core/backup/unlock.js +69 -0
  103. package/dist/core/boot-refresh.d.ts +1 -1
  104. package/dist/core/boot-refresh.js +10 -9
  105. package/dist/core/deps/actors/pr-creator.d.ts +5 -3
  106. package/dist/core/deps/actors/pr-creator.js +71 -18
  107. package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
  108. package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
  109. package/dist/core/deps/collectors/npm.js +3 -1
  110. package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
  111. package/dist/core/deps/collectors/vulnerability.js +31 -2
  112. package/dist/core/deps/config.js +6 -0
  113. package/dist/core/deps/scanner.js +1 -1
  114. package/dist/core/deps/types.d.ts +8 -0
  115. package/dist/core/env.d.ts +3 -0
  116. package/dist/core/env.js +11 -0
  117. package/dist/core/exec.d.ts +1 -0
  118. package/dist/core/exec.js +4 -0
  119. package/dist/core/file-lock.d.ts +18 -0
  120. package/dist/core/file-lock.js +44 -0
  121. package/dist/core/git-onboard.js +10 -13
  122. package/dist/core/github.d.ts +3 -1
  123. package/dist/core/github.js +10 -7
  124. package/dist/core/logs-policy.d.ts +5 -0
  125. package/dist/core/logs-policy.js +20 -1
  126. package/dist/core/operator.d.ts +21 -0
  127. package/dist/core/operator.js +54 -0
  128. package/dist/core/registry.d.ts +18 -0
  129. package/dist/core/registry.js +26 -0
  130. package/dist/core/routines/schema.d.ts +11 -11
  131. package/dist/core/routines/schema.js +14 -3
  132. package/dist/core/routines/store.d.ts +8 -8
  133. package/dist/core/secrets-ops.d.ts +31 -6
  134. package/dist/core/secrets-ops.js +208 -102
  135. package/dist/core/secrets-providers.js +2 -2
  136. package/dist/core/secrets-rotation.d.ts +1 -1
  137. package/dist/core/secrets-rotation.js +58 -52
  138. package/dist/core/secrets-v2-cleanup.d.ts +19 -0
  139. package/dist/core/secrets-v2-cleanup.js +94 -0
  140. package/dist/core/secrets-v2-creds.d.ts +9 -0
  141. package/dist/core/secrets-v2-creds.js +44 -0
  142. package/dist/core/secrets-v2-install.d.ts +13 -0
  143. package/dist/core/secrets-v2-install.js +76 -0
  144. package/dist/core/secrets-v2-keypair.d.ts +10 -0
  145. package/dist/core/secrets-v2-keypair.js +31 -0
  146. package/dist/core/secrets-v2-migrate.d.ts +29 -0
  147. package/dist/core/secrets-v2-migrate.js +395 -0
  148. package/dist/core/secrets-v2-ops.d.ts +36 -0
  149. package/dist/core/secrets-v2-ops.js +184 -0
  150. package/dist/core/secrets-v2-protocol.d.ts +19 -0
  151. package/dist/core/secrets-v2-protocol.js +60 -0
  152. package/dist/core/secrets-v2-snapshot.d.ts +36 -0
  153. package/dist/core/secrets-v2-snapshot.js +115 -0
  154. package/dist/core/secrets-v2.d.ts +21 -0
  155. package/dist/core/secrets-v2.js +249 -0
  156. package/dist/core/secrets.d.ts +39 -4
  157. package/dist/core/secrets.js +91 -11
  158. package/dist/core/self-update.d.ts +32 -11
  159. package/dist/core/self-update.js +52 -14
  160. package/dist/core/testflight/asc.d.ts +12 -0
  161. package/dist/core/testflight/asc.js +101 -0
  162. package/dist/core/testflight/credentials.d.ts +3 -0
  163. package/dist/core/testflight/credentials.js +35 -0
  164. package/dist/core/testflight/resolve.d.ts +6 -0
  165. package/dist/core/testflight/resolve.js +44 -0
  166. package/dist/core/testflight/types.d.ts +13 -0
  167. package/dist/core/testflight/types.js +3 -0
  168. package/dist/core/testflight/workflow.d.ts +17 -0
  169. package/dist/core/testflight/workflow.js +65 -0
  170. package/dist/core/validate.d.ts +1 -0
  171. package/dist/core/validate.js +8 -0
  172. package/dist/index.js +0 -0
  173. package/dist/mcp/audit-tools.d.ts +2 -0
  174. package/dist/mcp/audit-tools.js +94 -0
  175. package/dist/mcp/git-tools.js +1 -1
  176. package/dist/mcp/registry-bridge.d.ts +10 -0
  177. package/dist/mcp/registry-bridge.js +65 -0
  178. package/dist/mcp/secrets-tools.js +2 -2
  179. package/dist/mcp/server.js +16 -82
  180. package/dist/mcp/testflight-tools.d.ts +2 -0
  181. package/dist/mcp/testflight-tools.js +52 -0
  182. package/dist/registry/context.d.ts +7 -0
  183. package/dist/registry/context.js +37 -0
  184. package/dist/registry/index.d.ts +5 -0
  185. package/dist/registry/index.js +44 -0
  186. package/dist/registry/parse-args.d.ts +13 -0
  187. package/dist/registry/parse-args.js +74 -0
  188. package/dist/registry/registry.d.ts +24 -0
  189. package/dist/registry/registry.js +26 -0
  190. package/dist/registry/render.d.ts +3 -0
  191. package/dist/registry/render.js +29 -0
  192. package/dist/registry/types.d.ts +50 -0
  193. package/dist/registry/types.js +1 -0
  194. package/dist/templates/agent-unit.d.ts +5 -0
  195. package/dist/templates/agent-unit.js +40 -0
  196. package/dist/templates/app-unit-edit.d.ts +2 -0
  197. package/dist/templates/app-unit-edit.js +46 -0
  198. package/dist/templates/compose-edit.d.ts +2 -0
  199. package/dist/templates/compose-edit.js +156 -0
  200. package/dist/templates/nginx.js +11 -0
  201. package/dist/templates/systemd.js +6 -0
  202. package/dist/tui/components/ArgForm.d.ts +7 -0
  203. package/dist/tui/components/ArgForm.js +64 -0
  204. package/dist/tui/components/ArgForm.test.d.ts +1 -0
  205. package/dist/tui/components/ArgForm.test.js +19 -0
  206. package/dist/tui/components/KeyHint.js +5 -0
  207. package/dist/tui/hooks/use-secrets.d.ts +8 -8
  208. package/dist/tui/hooks/use-secrets.js +7 -7
  209. package/dist/tui/router.d.ts +1 -0
  210. package/dist/tui/router.js +26 -9
  211. package/dist/tui/router.test.d.ts +1 -0
  212. package/dist/tui/router.test.js +13 -0
  213. package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
  214. package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
  215. package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
  216. package/dist/tui/tests/redaction-rerender.test.js +53 -0
  217. package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
  218. package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
  219. package/dist/tui/types.d.ts +1 -1
  220. package/dist/tui/views/CommandPalette.d.ts +5 -0
  221. package/dist/tui/views/CommandPalette.js +90 -0
  222. package/dist/tui/views/CommandPalette.test.d.ts +1 -0
  223. package/dist/tui/views/CommandPalette.test.js +117 -0
  224. package/dist/tui/views/Dashboard.js +9 -6
  225. package/dist/tui/views/HealthView.js +9 -4
  226. package/dist/tui/views/SecretEdit.js +15 -16
  227. package/dist/tui/views/SecretEdit.test.d.ts +1 -0
  228. package/dist/tui/views/SecretEdit.test.js +82 -0
  229. package/dist/tui/views/SecretsView.js +26 -16
  230. package/package.json +8 -5
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import { InputDispatcher } from '@matthesketh/ink-input-dispatcher';
5
+ // stable mock fleet status. the reference must stay constant across renders:
6
+ // dashboard memoises its list items, so a changing status object would rebuild
7
+ // them anyway and mask the redaction-staleness bug this test guards.
8
+ const { MOCK_STATUS } = vi.hoisted(() => ({
9
+ MOCK_STATUS: {
10
+ totalApps: 3,
11
+ healthy: 3,
12
+ unhealthy: 0,
13
+ apps: [
14
+ { name: 'alpha-service', systemd: 'active', containers: '1/1', health: 'healthy' },
15
+ { name: 'bravo-service', systemd: 'active', containers: '1/1', health: 'healthy' },
16
+ { name: 'charlie-service', systemd: 'active', containers: '1/1', health: 'healthy' },
17
+ ],
18
+ },
19
+ }));
20
+ vi.mock('../hooks/use-fleet-data', () => ({
21
+ useFleetData: () => ({ status: MOCK_STATUS, loading: false, error: null }),
22
+ }));
23
+ // fixed available height so the list renders every row under a test stdout.
24
+ vi.mock('@matthesketh/ink-viewport', () => ({
25
+ useAvailableHeight: () => 20,
26
+ Viewport: ({ children }) => children,
27
+ }));
28
+ import { Dashboard } from '../views/Dashboard.js';
29
+ import { AppStateContext, AppDispatchContext, initialState } from '../state.js';
30
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
31
+ function Harness({ redacted }) {
32
+ return (_jsx(AppStateContext.Provider, { value: { ...initialState, redacted }, children: _jsx(AppDispatchContext.Provider, { value: () => { }, children: _jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(Dashboard, {}) }) }) }));
33
+ }
34
+ describe('redaction re-renders every visible row', () => {
35
+ it('toggling redaction updates all app rows without scrolling', async () => {
36
+ const { lastFrame, rerender } = render(_jsx(Harness, { redacted: false }));
37
+ await delay(50);
38
+ const plain = lastFrame() ?? '';
39
+ expect(plain).toContain('alpha-service');
40
+ expect(plain).toContain('bravo-service');
41
+ expect(plain).toContain('charlie-service');
42
+ // flip redaction — equivalent to pressing 'x'. no j/k/arrow keys are sent,
43
+ // so nothing scrolls; every visible row must still pick up the new label.
44
+ rerender(_jsx(Harness, { redacted: true }));
45
+ await delay(50);
46
+ const out = lastFrame() ?? '';
47
+ expect(out).not.toContain('alpha-service');
48
+ expect(out).not.toContain('bravo-service');
49
+ expect(out).not.toContain('charlie-service');
50
+ // redacted labels follow the app-NN pattern from redactName()
51
+ expect(out).toMatch(/app-\d\d/);
52
+ });
53
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,145 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useReducer } from 'react';
3
+ import { EventEmitter } from 'node:events';
4
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
5
+ // Force Ink's real (non-CI) render path so the flicker branch is reachable.
6
+ // ink-testing-library renders in debug mode, where Ink's onRender returns
7
+ // before the clear-screen branch — so it cannot observe the flicker at all.
8
+ // This proof drives Ink's genuine production render() instead.
9
+ vi.mock('is-in-ci', () => ({ default: false }));
10
+ // eslint-disable-next-line import/first
11
+ import { render, Box, Text } from 'ink';
12
+ // eslint-disable-next-line import/first
13
+ import { Viewport, useAvailableHeight } from '@matthesketh/ink-viewport';
14
+ // eslint-disable-next-line import/first
15
+ import { ScrollableList } from '@matthesketh/ink-scrollable-list';
16
+ // eslint-disable-next-line import/first
17
+ import { InputDispatcher, useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
18
+ // ESC (0x1B) built without a control-char literal in source.
19
+ const ESC = String.fromCharCode(27);
20
+ // Ink writes this exact sequence (ansiEscapes.clearTerminal) before a frame
21
+ // whenever the frame height is >= stdout.rows — a full-screen wipe on every
22
+ // re-render. That wipe IS the flicker; counting it proves its presence.
23
+ const CLEAR_TERMINAL = `${ESC}[2J${ESC}[3J${ESC}[H`;
24
+ const DOWN_ARROW = `${ESC}[B`;
25
+ const ROWS = 24;
26
+ const COLUMNS = 120;
27
+ const SCROLL_STEPS = 20;
28
+ const APPS = Array.from({ length: 200 }, (_, i) => `app-${i}`);
29
+ /** A stdout that records every byte Ink writes and reports a fixed size. */
30
+ class RecordingStdout extends EventEmitter {
31
+ columns = COLUMNS;
32
+ rows = ROWS;
33
+ isTTY = true;
34
+ writes = [];
35
+ write = (data) => {
36
+ this.writes.push(data);
37
+ return true;
38
+ };
39
+ }
40
+ /** A TTY-like stdin mirroring ink-testing-library's: a keypress is delivered
41
+ * via both the 'readable'/read() and 'data' paths. */
42
+ class FakeStdin extends EventEmitter {
43
+ isTTY = true;
44
+ data = null;
45
+ press = (data) => {
46
+ this.data = data;
47
+ this.emit('readable');
48
+ this.emit('data', data);
49
+ };
50
+ setEncoding() { }
51
+ setRawMode() { }
52
+ resume() { }
53
+ pause() { }
54
+ ref() { }
55
+ unref() { }
56
+ read = () => {
57
+ const d = this.data;
58
+ this.data = null;
59
+ return d;
60
+ };
61
+ }
62
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
63
+ /** Mirrors Fleet's Dashboard: a windowed ScrollableList sized off
64
+ * useAvailableHeight(), driven by j / down-arrow via the input dispatcher. */
65
+ function MockDashboard() {
66
+ const [index, move] = useReducer((cur, delta) => Math.max(0, Math.min(APPS.length - 1, cur + delta)), 0);
67
+ const available = useAvailableHeight();
68
+ const listHeight = Math.max(5, available - 4); // same formula as Dashboard.tsx
69
+ const handler = (input, key) => {
70
+ if (input === 'j' || key.downArrow) {
71
+ move(1);
72
+ return true;
73
+ }
74
+ if (input === 'k' || key.upArrow) {
75
+ move(-1);
76
+ return true;
77
+ }
78
+ return false;
79
+ };
80
+ useRegisterHandler(handler);
81
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { bold: true, children: [APPS.length, " apps"] }), _jsx(ScrollableList, { items: APPS, selectedIndex: index, maxVisible: listHeight, renderItem: (item, selected) => (_jsxs(Text, { color: selected ? 'cyan' : undefined, children: [selected ? '> ' : ' ', item] })) })] }));
82
+ }
83
+ /** Fleet's real chrome: InputDispatcher outside, Viewport(chrome) inside. */
84
+ function FleetUI() {
85
+ return (_jsx(InputDispatcher, { globalHandler: () => false, children: _jsxs(Viewport, { chrome: 6, children: [_jsx(Text, { children: "fleet" }), _jsx(MockDashboard, {})] }) }));
86
+ }
87
+ /** The pre-fix viewport: a root box pinned to the FULL terminal height. */
88
+ function LegacyChrome() {
89
+ return (_jsx(InputDispatcher, { globalHandler: () => false, children: _jsxs(Box, { flexDirection: "column", height: ROWS, children: [_jsx(Text, { children: "fleet" }), _jsx(MockDashboard, {})] }) }));
90
+ }
91
+ /** Mount with Ink's production render path, scroll SCROLL_STEPS rows, and
92
+ * record every byte Ink writes to the terminal. */
93
+ async function scrollAndRecord(node) {
94
+ const stdout = new RecordingStdout();
95
+ const stdin = new FakeStdin();
96
+ const app = render(node, {
97
+ stdout: stdout,
98
+ stdin: stdin,
99
+ debug: false,
100
+ exitOnCtrlC: false,
101
+ patchConsole: false,
102
+ });
103
+ await sleep(40);
104
+ for (let i = 0; i < SCROLL_STEPS; i++) {
105
+ stdin.press(DOWN_ARROW);
106
+ await sleep(45); // > Ink's 32ms render throttle, so each scroll renders
107
+ }
108
+ await sleep(60);
109
+ app.unmount();
110
+ const output = stdout.writes.join('');
111
+ return { clears: output.split(CLEAR_TERMINAL).length - 1, output };
112
+ }
113
+ describe('Fleet TUI scroll flicker proof', () => {
114
+ beforeEach(() => {
115
+ process.setMaxListeners(0);
116
+ Object.defineProperty(process.stdout, 'rows', {
117
+ value: ROWS,
118
+ writable: true,
119
+ configurable: true,
120
+ });
121
+ Object.defineProperty(process.stdout, 'columns', {
122
+ value: COLUMNS,
123
+ writable: true,
124
+ configurable: true,
125
+ });
126
+ });
127
+ // skipped on ci: ink picks up github_actions / ci directly and gates the
128
+ // clearterminal branch on tty-ish heuristics that the in-process recorder
129
+ // can't fully spoof. the control passes locally where the production
130
+ // render path is reachable, so the proof below still has its anchor when
131
+ // run pre-commit; gh actions just doesn't reproduce the legacy bug.
132
+ it.skipIf(Boolean(process.env.CI))('CONTROL: a full-terminal-height frame flickers on every scroll render', async () => {
133
+ const { clears } = await scrollAndRecord(_jsx(LegacyChrome, {}));
134
+ expect(clears).toBeGreaterThan(0);
135
+ });
136
+ it('Fleet UI scrolls 20 rows with ZERO full-screen clears', async () => {
137
+ const { clears, output } = await scrollAndRecord(_jsx(FleetUI, {}));
138
+ // The scroll genuinely happened: ScrollableList only renders its
139
+ // "more above" indicator once the window has scrolled down.
140
+ expect(output).toContain('more above');
141
+ // ...and across every one of those re-renders, Ink never wiped the
142
+ // screen. Zero clearTerminal sequences => zero flicker.
143
+ expect(clears).toBe(0);
144
+ });
145
+ });
@@ -1,4 +1,4 @@
1
- export type View = 'dashboard' | 'app-detail' | 'health' | 'secrets' | 'secret-edit' | 'logs' | 'logs-multi';
1
+ export type View = 'dashboard' | 'app-detail' | 'health' | 'secrets' | 'secret-edit' | 'logs' | 'logs-multi' | 'command-palette';
2
2
  export type SecretsSubView = 'app-list' | 'secret-list';
3
3
  export interface TuiState {
4
4
  currentView: View;
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ export declare function CommandPalette(props: {
3
+ onOpenView: (view: string) => void;
4
+ onClose: () => void;
5
+ }): React.JSX.Element;
@@ -0,0 +1,90 @@
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
5
+ import { ScrollableList } from '@matthesketh/ink-scrollable-list';
6
+ import { loadRegistry } from '../../registry/index.js';
7
+ import { allCommands } from '../../registry/registry.js';
8
+ import { ArgForm } from '../components/ArgForm.js';
9
+ import { runFleetCommand } from '../exec-bridge.js';
10
+ import { colors } from '../theme.js';
11
+ export function CommandPalette(props) {
12
+ // load the registry once and snapshot the visible commands. a lazy useState
13
+ // initialiser runs exactly once — unlike a bare call in the render body.
14
+ const [commands] = useState(() => {
15
+ loadRegistry();
16
+ return allCommands().filter(c => !c.cliOnly);
17
+ });
18
+ const [query, setQuery] = useState('');
19
+ const [index, setIndex] = useState(0);
20
+ const [chosen, setChosen] = useState(null);
21
+ const [output, setOutput] = useState(null);
22
+ const filtered = useMemo(() => commands.filter(c => (c.name + ' ' + c.summary).toLowerCase().includes(query.toLowerCase())), [commands, query]);
23
+ const listHandler = (input, key) => {
24
+ if (chosen || output !== null)
25
+ return false;
26
+ if (key.escape) {
27
+ props.onClose();
28
+ return true;
29
+ }
30
+ if (key.downArrow) {
31
+ setIndex(i => Math.min(i + 1, filtered.length - 1));
32
+ return true;
33
+ }
34
+ if (key.upArrow) {
35
+ setIndex(i => Math.max(i - 1, 0));
36
+ return true;
37
+ }
38
+ if (key.return) {
39
+ const cmd = filtered[index];
40
+ if (!cmd)
41
+ return true;
42
+ if (cmd.tui && typeof cmd.tui === 'object') {
43
+ props.onOpenView(cmd.tui.view);
44
+ return true;
45
+ }
46
+ setChosen(cmd);
47
+ return true;
48
+ }
49
+ if (key.backspace || key.delete) {
50
+ setQuery(q => q.slice(0, -1));
51
+ setIndex(0);
52
+ return true;
53
+ }
54
+ if (input && !key.ctrl && !key.meta) {
55
+ setQuery(q => q + input);
56
+ setIndex(0);
57
+ return true;
58
+ }
59
+ return false;
60
+ };
61
+ useRegisterHandler(listHandler);
62
+ if (output !== null) {
63
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { bold: true, color: colors.primary, children: [chosen?.name, " result"] }), _jsx(Text, { children: output }), _jsx(Text, { color: colors.muted, children: "esc to close" }), _jsx(CloseOnEscape, { onClose: () => { setOutput(null); setChosen(null); } })] }));
64
+ }
65
+ if (chosen) {
66
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: colors.primary, children: chosen.name }), _jsx(ArgForm, { schema: chosen.args, onCancel: () => setChosen(null), onSubmit: async (values) => {
67
+ const argv = [chosen.name];
68
+ for (const [k, v] of Object.entries(values)) {
69
+ if (v === true)
70
+ argv.push(`--${k}`);
71
+ else if (v !== false && v !== '' && v != null)
72
+ argv.push(`--${k}`, String(v));
73
+ }
74
+ const r = await runFleetCommand(argv);
75
+ setOutput(r.output);
76
+ } })] }));
77
+ }
78
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: colors.primary, children: "Command palette" }), _jsxs(Text, { color: colors.muted, children: ["filter: ", query || '(type to filter)'] }), _jsx(ScrollableList, { items: filtered, selectedIndex: Math.min(index, Math.max(0, filtered.length - 1)), maxVisible: 12, emptyText: " no matching commands", renderItem: (cmd, selected) => (_jsxs(Box, { children: [_jsx(Text, { color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Box, { width: 20, children: _jsx(Text, { bold: selected, children: cmd.name }) }), _jsx(Text, { color: colors.muted, children: cmd.summary })] })) })] }));
79
+ }
80
+ function CloseOnEscape(props) {
81
+ const handler = (_input, key) => {
82
+ if (key.escape) {
83
+ props.onClose();
84
+ return true;
85
+ }
86
+ return false;
87
+ };
88
+ useRegisterHandler(handler);
89
+ return _jsx(_Fragment, {});
90
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
4
+ import { z } from 'zod';
5
+ import { InputDispatcher } from '@matthesketh/ink-input-dispatcher';
6
+ vi.mock('../exec-bridge', () => ({
7
+ runFleetCommand: vi.fn(async () => ({ ok: true, output: 'done' })),
8
+ }));
9
+ import { runFleetCommand } from '../exec-bridge.js';
10
+ import { register, defineCommand } from '../../registry/registry.js';
11
+ import { loadRegistry, _resetLoader } from '../../registry/index.js';
12
+ import { CommandPalette } from './CommandPalette.js';
13
+ /** flush ink's render queue after a keystroke */
14
+ const flush = () => new Promise(r => setTimeout(r, 30));
15
+ beforeEach(() => _resetLoader());
16
+ afterEach(() => _resetLoader());
17
+ describe('CommandPalette', () => {
18
+ it('lists registry commands', async () => {
19
+ const { lastFrame } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: () => { }, onClose: () => { } }) }));
20
+ await flush();
21
+ // assert against the first alphabetical command — the palette caps
22
+ // visible items at 12 so commands later in the list (status, stop,
23
+ // whoami, ...) may scroll off when the registry grows.
24
+ expect(lastFrame() ?? '').toContain('add');
25
+ });
26
+ it('hides non-matching commands when a query is typed', async () => {
27
+ const { lastFrame, stdin } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: () => { }, onClose: () => { } }) }));
28
+ await flush();
29
+ // sanity: at least the first command is visible before filtering
30
+ expect(lastFrame() ?? '').toContain('add');
31
+ // type a query that matches nothing
32
+ stdin.write('z');
33
+ await flush();
34
+ stdin.write('z');
35
+ await flush();
36
+ stdin.write('z');
37
+ await flush();
38
+ stdin.write('z');
39
+ await flush();
40
+ const frame = lastFrame() ?? '';
41
+ expect(frame).not.toContain('status');
42
+ expect(frame).toContain('no matching commands');
43
+ });
44
+ it('calls onOpenView when enter is pressed on a command with a tui view', async () => {
45
+ // type 'stat' to filter to only status, then press enter.
46
+ const onOpenView = vi.fn();
47
+ const { stdin } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: onOpenView, onClose: () => { } }) }));
48
+ await flush();
49
+ stdin.write('s');
50
+ await flush();
51
+ stdin.write('t');
52
+ await flush();
53
+ stdin.write('a');
54
+ await flush();
55
+ stdin.write('t');
56
+ await flush();
57
+ stdin.write('\r');
58
+ await flush();
59
+ expect(onOpenView).toHaveBeenCalledWith('dashboard');
60
+ });
61
+ it('builds argv from an empty-args command and calls runFleetCommand', async () => {
62
+ // register an ad-hoc command with no tui and an empty args schema.
63
+ // loadRegistry() first so the loaded flag is set and the component's own
64
+ // call does not wipe the ad-hoc registration.
65
+ loadRegistry();
66
+ register(defineCommand({
67
+ name: 'demo-run',
68
+ summary: 'a demo command',
69
+ args: z.object({}),
70
+ async run() { return { ok: true, summary: 'ok', data: null }; },
71
+ }));
72
+ const { stdin } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: () => { }, onClose: () => { } }) }));
73
+ await flush();
74
+ // type 'demo-run' to filter to exactly this command so the test is
75
+ // stable regardless of how many other commands are in the registry.
76
+ for (const ch of 'demo-run') {
77
+ stdin.write(ch);
78
+ await flush();
79
+ }
80
+ // press enter on demo-run → ArgForm shown (no tui field).
81
+ stdin.write('\r');
82
+ await flush();
83
+ // ArgForm with no fields: press enter → onSubmit({}) → runFleetCommand.
84
+ stdin.write('\r');
85
+ await flush();
86
+ expect(vi.mocked(runFleetCommand)).toHaveBeenCalledWith(['demo-run']);
87
+ });
88
+ it('builds --flag argv when a boolean field is toggled before submit', async () => {
89
+ // register a command with a boolean arg.
90
+ loadRegistry();
91
+ register(defineCommand({
92
+ name: 'demo-flag',
93
+ summary: 'a flag demo',
94
+ args: z.object({ force: z.boolean().default(false) }),
95
+ async run() { return { ok: true, summary: 'ok', data: null }; },
96
+ }));
97
+ const { stdin } = render(_jsx(InputDispatcher, { globalHandler: () => false, children: _jsx(CommandPalette, { onOpenView: () => { }, onClose: () => { } }) }));
98
+ await flush();
99
+ // type 'demo-flag' to filter to exactly this command so the test is
100
+ // stable regardless of how many other commands are in the registry.
101
+ for (const ch of 'demo-flag') {
102
+ stdin.write(ch);
103
+ await flush();
104
+ }
105
+ // press enter on demo-flag.
106
+ stdin.write('\r');
107
+ await flush();
108
+ // ArgForm is now showing the 'force' boolean field (cursor on it).
109
+ // press space to toggle force → true.
110
+ stdin.write(' ');
111
+ await flush();
112
+ // press enter to submit { force: true }.
113
+ stdin.write('\r');
114
+ await flush();
115
+ expect(vi.mocked(runFleetCommand)).toHaveBeenCalledWith(['demo-flag', '--force']);
116
+ });
117
+ });
@@ -5,16 +5,20 @@ import Spinner from 'ink-spinner';
5
5
  import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
6
6
  import { ScrollableList } from '@matthesketh/ink-scrollable-list';
7
7
  import { useAvailableHeight } from '@matthesketh/ink-viewport';
8
- import { useAppState, useAppDispatch, useRedact } from '../state.js';
8
+ import { useAppState, useAppDispatch, redactName } from '../state.js';
9
9
  import { useFleetData } from '../hooks/use-fleet-data.js';
10
10
  import { colors } from '../theme.js';
11
11
  export function Dashboard() {
12
12
  const state = useAppState();
13
13
  const dispatch = useAppDispatch();
14
14
  const { status, loading, error } = useFleetData();
15
- const redact = useRedact();
16
15
  const availableHeight = useAvailableHeight();
17
- const items = useMemo(() => status?.apps.map(app => ({ ...app, name: app.name })) ?? [], [status]);
16
+ const items = useMemo(() => (status?.apps ?? []).map(app => ({
17
+ ...app,
18
+ // bake the redacted label onto the item: ScrollableList memoises rows by
19
+ // item identity, so a redaction toggle must yield fresh item objects.
20
+ displayLabel: state.redacted ? redactName(app.name) : app.name,
21
+ })), [status, state.redacted]);
18
22
  const handler = (input, key) => {
19
23
  if (items.length === 0)
20
24
  return false;
@@ -47,8 +51,7 @@ export function Dashboard() {
47
51
  return _jsx(Text, { color: colors.muted, children: "No data" });
48
52
  const listHeight = Math.max(5, availableHeight - 4);
49
53
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsxs(Text, { bold: true, children: [status.totalApps, " apps"] }), _jsxs(Text, { color: colors.success, children: [status.healthy, " healthy"] }), status.unhealthy > 0 && (_jsxs(Text, { color: colors.error, children: [status.unhealthy, " unhealthy"] }))] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ['APP'.padEnd(24), 'SYSTEMD'.padEnd(14), 'CONTAINERS'.padEnd(14), 'HEALTH'.padEnd(12)] }) }), _jsx(ScrollableList, { items: items, selectedIndex: Math.min(state.dashboardIndex, items.length - 1), maxVisible: listHeight, renderItem: (item, selected) => {
50
- const app = status.apps.find(a => a.name === item.name);
51
- const displayName = redact(app.name);
52
- return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Box, { width: 24, children: _jsx(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: displayName.length > 22 ? displayName.slice(0, 19) + '...' : displayName }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: app.systemd.slice(0, 12) }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: app.containers }) }), _jsx(Box, { width: 12, children: _jsx(Text, { children: app.health.slice(0, 10) }) })] }));
54
+ const label = item.displayLabel;
55
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Box, { width: 24, children: _jsx(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: label.length > 22 ? label.slice(0, 19) + '...' : label }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: item.systemd.slice(0, 12) }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: item.containers }) }), _jsx(Box, { width: 12, children: _jsx(Text, { children: item.health.slice(0, 10) }) })] }));
53
56
  } })] }));
54
57
  }
@@ -7,7 +7,7 @@ import { ScrollableList } from '@matthesketh/ink-scrollable-list';
7
7
  import { useAvailableHeight } from '@matthesketh/ink-viewport';
8
8
  import { useHealth } from '../hooks/use-health.js';
9
9
  import { StatusBadge } from '../components/StatusBadge.js';
10
- import { useAppState, useAppDispatch, useRedact } from '../state.js';
10
+ import { useAppState, useAppDispatch, redactName } from '../state.js';
11
11
  import { colors } from '../theme.js';
12
12
  export function HealthView() {
13
13
  const state = useAppState();
@@ -17,13 +17,18 @@ export function HealthView() {
17
17
  // 15s) flip `loading` true/false too, but the data is already on screen —
18
18
  // ticking a spinner there causes the whole table to redraw at frame rate.
19
19
  const initialLoad = loading && results.length === 0;
20
- const redact = useRedact();
21
20
  const availableHeight = useAvailableHeight();
22
21
  const counts = useMemo(() => ({
23
22
  healthy: results.filter(r => r.overall === 'healthy').length,
24
23
  degraded: results.filter(r => r.overall === 'degraded').length,
25
24
  down: results.filter(r => r.overall === 'down').length,
26
25
  }), [results]);
26
+ const items = useMemo(() => results.map(r => ({
27
+ ...r,
28
+ // same reasoning as the dashboard view — ScrollableList memoises rows by
29
+ // item identity, so the redacted label must live on the item itself.
30
+ displayApp: state.redacted ? redactName(r.app) : r.app,
31
+ })), [results, state.redacted]);
27
32
  const handler = (input, key) => {
28
33
  if (results.length === 0)
29
34
  return false;
@@ -45,7 +50,7 @@ export function HealthView() {
45
50
  return (_jsx(Box, { padding: 1, children: _jsxs(Text, { color: colors.error, children: ["Error: ", error] }) }));
46
51
  }
47
52
  const listHeight = Math.max(5, availableHeight - 4);
48
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, children: "Health Monitor" }), _jsxs(Text, { color: colors.success, children: [counts.healthy, " healthy"] }), counts.degraded > 0 && _jsxs(Text, { color: colors.warning, children: [counts.degraded, " degraded"] }), counts.down > 0 && _jsxs(Text, { color: colors.error, children: [counts.down, " down"] })] }), _jsxs(Text, { bold: true, children: [' APP'.padEnd(26), 'SYSTEMD'.padEnd(12), 'CONTAINERS'.padEnd(20), 'HTTP'.padEnd(10), "OVERALL"] }), _jsx(ScrollableList, { items: results, selectedIndex: Math.min(state.healthIndex, results.length - 1), maxVisible: listHeight, renderItem: (result, selected) => {
53
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, gap: 2, children: [_jsx(Text, { bold: true, children: "Health Monitor" }), _jsxs(Text, { color: colors.success, children: [counts.healthy, " healthy"] }), counts.degraded > 0 && _jsxs(Text, { color: colors.warning, children: [counts.degraded, " degraded"] }), counts.down > 0 && _jsxs(Text, { color: colors.error, children: [counts.down, " down"] })] }), _jsxs(Text, { bold: true, children: [' APP'.padEnd(26), 'SYSTEMD'.padEnd(12), 'CONTAINERS'.padEnd(20), 'HTTP'.padEnd(10), "OVERALL"] }), _jsx(ScrollableList, { items: items, selectedIndex: Math.min(state.healthIndex, items.length - 1), maxVisible: listHeight, renderItem: (result, selected) => {
49
54
  const runningCount = result.containers.filter(c => c.running).length;
50
55
  const containerStr = `${runningCount}/${result.containers.length}`;
51
56
  // 404 → "no /health" (app never implemented one — distinct from a real failure).
@@ -57,6 +62,6 @@ export function HealthView() {
57
62
  ? 'no /health'
58
63
  : `${result.http.status ?? 'err'}`
59
64
  : 'n/a';
60
- return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Text, { children: redact(result.app).padEnd(24) }), _jsx(Box, { width: 12, children: _jsx(StatusBadge, { value: result.systemd.state, type: "systemd" }) }), _jsx(Text, { children: containerStr.padEnd(20) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: result.http?.ok ? colors.success : result.http ? colors.error : colors.muted, children: httpStr }) }), _jsx(StatusBadge, { value: result.overall, type: "health" })] }));
65
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: selected ? colors.primary : colors.muted, children: selected ? '> ' : ' ' }), _jsx(Text, { children: result.displayApp.padEnd(24) }), _jsx(Box, { width: 12, children: _jsx(StatusBadge, { value: result.systemd.state, type: "systemd" }) }), _jsx(Text, { children: containerStr.padEnd(20) }), _jsx(Box, { width: 10, children: _jsx(Text, { color: result.http?.ok ? colors.success : result.http ? colors.error : colors.muted, children: httpStr }) }), _jsx(StatusBadge, { value: result.overall, type: "health" })] }));
61
66
  } })] }));
62
67
  }
@@ -5,7 +5,6 @@ import TextInput from 'ink-text-input';
5
5
  import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
6
6
  import { useAppState, useAppDispatch } from '../state.js';
7
7
  import { useSecrets } from '../hooks/use-secrets.js';
8
- import { getSecret as getCoreSecret } from '../../core/secrets-ops.js';
9
8
  import { colors } from '../theme.js';
10
9
  export function SecretEdit() {
11
10
  const { selectedApp, selectedSecret } = useAppState();
@@ -23,22 +22,20 @@ export function SecretEdit() {
23
22
  clearTimeout(timerRef.current);
24
23
  };
25
24
  }, []);
26
- useEffect(() => {
27
- if (!isNew && selectedApp && selectedSecret) {
28
- try {
29
- const existing = getCoreSecret(selectedApp, selectedSecret);
30
- if (existing)
31
- setValue(existing);
32
- }
33
- catch {
34
- // ignore
35
- }
36
- }
37
- }, [isNew, selectedApp, selectedSecret]);
38
- const save = () => {
25
+ // SECURITY: existing secret values are NEVER preloaded into editor state.
26
+ // The TextInput's `mask="*"` only changes the rendered glyph — the
27
+ // underlying React state would still hold plaintext, exposing it to
28
+ // DevTools dumps, error boundary captures, and process memory dumps.
29
+ // Editing requires re-typing the value, matching the CLI posture in
30
+ // `src/commands/secrets.ts` which rejects argv values for the same reason.
31
+ const save = async () => {
39
32
  if (!selectedApp || !keyName)
40
33
  return;
41
- const result = secrets.saveSecret(selectedApp, keyName, value);
34
+ const result = await secrets.saveSecret(selectedApp, keyName, value);
35
+ // Clear the local plaintext from React state immediately, regardless of
36
+ // success/failure. The state holds plaintext only for the duration of
37
+ // the save call.
38
+ setValue('');
42
39
  if (result.ok) {
43
40
  setStatus('Saved and re-sealed');
44
41
  timerRef.current = setTimeout(() => {
@@ -60,5 +57,7 @@ export function SecretEdit() {
60
57
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Text, { bold: true, color: colors.primary, children: [isNew ? 'Add Secret' : 'Edit Secret', " - ", selectedApp] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Key: " }), isNew && phase === 'key' ? (_jsx(TextInput, { value: keyName, onChange: setKeyName, onSubmit: () => {
61
58
  if (keyName)
62
59
  setPhase('value');
63
- } })) : (_jsx(Text, { bold: true, children: keyName }))] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Value: " }), phase === 'value' ? (_jsx(TextInput, { value: value, onChange: setValue, onSubmit: save, mask: "*" })) : (_jsx(Text, { color: colors.muted, children: "(press Enter on key first)" }))] })] }), status && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: status.startsWith('Error') ? colors.error : colors.success, children: status }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Enter to save | Esc to cancel" }) })] }));
60
+ } })) : (_jsx(Text, { bold: true, children: keyName }))] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.muted, children: "Value: " }), phase === 'value' ? (_jsx(TextInput, { value: value, onChange: setValue, onSubmit: save, mask: "*" })) : (_jsx(Text, { color: colors.muted, children: "(press Enter on key first)" }))] })] }), status && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: status.startsWith('Error') ? colors.error : colors.success, children: status }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: isNew
61
+ ? 'Adding new secret. Type the key name, then the value.'
62
+ : `Editing ${keyName} - paste new value to replace. (Current value not displayed.)` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: colors.muted, children: "Enter to save | Esc to cancel" }) })] }));
64
63
  }
@@ -0,0 +1 @@
1
+ export {};