@matthesketh/fleet 1.1.0 → 1.6.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.
Files changed (217) hide show
  1. package/README.md +183 -251
  2. package/dist/adapters/detector/index.d.ts +8 -0
  3. package/dist/adapters/detector/index.js +54 -0
  4. package/dist/adapters/notifier/index.d.ts +2 -0
  5. package/dist/adapters/notifier/index.js +2 -0
  6. package/dist/adapters/notifier/stdout.d.ts +2 -0
  7. package/dist/adapters/notifier/stdout.js +8 -0
  8. package/dist/adapters/notifier/webhook.d.ts +9 -0
  9. package/dist/adapters/notifier/webhook.js +38 -0
  10. package/dist/adapters/runner/claude-cli.d.ts +7 -0
  11. package/dist/adapters/runner/claude-cli.js +231 -0
  12. package/dist/adapters/runner/mcp-call.d.ts +8 -0
  13. package/dist/adapters/runner/mcp-call.js +82 -0
  14. package/dist/adapters/runner/shell.d.ts +2 -0
  15. package/dist/adapters/runner/shell.js +103 -0
  16. package/dist/adapters/scheduler/systemd-timer.d.ts +17 -0
  17. package/dist/adapters/scheduler/systemd-timer.js +149 -0
  18. package/dist/adapters/signals/ci-status.d.ts +2 -0
  19. package/dist/adapters/signals/ci-status.js +79 -0
  20. package/dist/adapters/signals/container-up.d.ts +5 -0
  21. package/dist/adapters/signals/container-up.js +54 -0
  22. package/dist/adapters/signals/git-clean.d.ts +2 -0
  23. package/dist/adapters/signals/git-clean.js +55 -0
  24. package/dist/adapters/signals/index.d.ts +6 -0
  25. package/dist/adapters/signals/index.js +7 -0
  26. package/dist/adapters/types.d.ts +52 -0
  27. package/dist/adapters/types.js +1 -0
  28. package/dist/cli.js +43 -2
  29. package/dist/commands/add.js +0 -6
  30. package/dist/commands/boot-start.d.ts +1 -0
  31. package/dist/commands/boot-start.js +51 -0
  32. package/dist/commands/deploy.js +13 -0
  33. package/dist/commands/deps.js +5 -0
  34. package/dist/commands/egress.d.ts +1 -0
  35. package/dist/commands/egress.js +106 -0
  36. package/dist/commands/freeze.d.ts +4 -0
  37. package/dist/commands/freeze.js +64 -0
  38. package/dist/commands/logs.d.ts +1 -1
  39. package/dist/commands/logs.js +237 -8
  40. package/dist/commands/patch-systemd.d.ts +1 -0
  41. package/dist/commands/patch-systemd.js +126 -0
  42. package/dist/commands/rollback.d.ts +1 -0
  43. package/dist/commands/rollback.js +58 -0
  44. package/dist/commands/routine-run.d.ts +1 -0
  45. package/dist/commands/routine-run.js +122 -0
  46. package/dist/commands/routines.d.ts +1 -0
  47. package/dist/commands/routines.js +25 -0
  48. package/dist/commands/secrets.js +449 -16
  49. package/dist/commands/status.js +7 -3
  50. package/dist/commands/watchdog.d.ts +1 -1
  51. package/dist/commands/watchdog.js +16 -40
  52. package/dist/core/boot-refresh.d.ts +57 -0
  53. package/dist/core/boot-refresh.js +116 -0
  54. package/dist/core/deps/actors/pr-creator.js +11 -9
  55. package/dist/core/deps/collectors/docker-running.js +2 -2
  56. package/dist/core/deps/collectors/github-pr.js +5 -2
  57. package/dist/core/deps/collectors/npm.js +10 -5
  58. package/dist/core/deps/collectors/vulnerability.js +10 -6
  59. package/dist/core/deps/reporters/motd.js +1 -1
  60. package/dist/core/deps/reporters/telegram.js +2 -29
  61. package/dist/core/docker.js +45 -15
  62. package/dist/core/egress.d.ts +41 -0
  63. package/dist/core/egress.js +161 -0
  64. package/dist/core/exec.d.ts +7 -1
  65. package/dist/core/exec.js +25 -17
  66. package/dist/core/git.d.ts +1 -0
  67. package/dist/core/git.js +36 -23
  68. package/dist/core/github.js +27 -8
  69. package/dist/core/health.d.ts +3 -0
  70. package/dist/core/health.js +15 -3
  71. package/dist/core/logs-multi.d.ts +73 -0
  72. package/dist/core/logs-multi.js +163 -0
  73. package/dist/core/logs-policy.d.ts +55 -0
  74. package/dist/core/logs-policy.js +148 -0
  75. package/dist/core/nginx.js +8 -4
  76. package/dist/core/notify.d.ts +15 -0
  77. package/dist/core/notify.js +55 -0
  78. package/dist/core/registry.d.ts +25 -0
  79. package/dist/core/registry.js +57 -10
  80. package/dist/core/routines/cost-queries.d.ts +24 -0
  81. package/dist/core/routines/cost-queries.js +65 -0
  82. package/dist/core/routines/db.d.ts +9 -0
  83. package/dist/core/routines/db.js +126 -0
  84. package/dist/core/routines/defaults.d.ts +2 -0
  85. package/dist/core/routines/defaults.js +72 -0
  86. package/dist/core/routines/engine.d.ts +59 -0
  87. package/dist/core/routines/engine.js +175 -0
  88. package/dist/core/routines/incidents.d.ts +13 -0
  89. package/dist/core/routines/incidents.js +35 -0
  90. package/dist/core/routines/schema.d.ts +418 -0
  91. package/dist/core/routines/schema.js +113 -0
  92. package/dist/core/routines/signals-collector.d.ts +35 -0
  93. package/dist/core/routines/signals-collector.js +114 -0
  94. package/dist/core/routines/store.d.ts +316 -0
  95. package/dist/core/routines/store.js +99 -0
  96. package/dist/core/routines/test-utils.d.ts +2 -0
  97. package/dist/core/routines/test-utils.js +13 -0
  98. package/dist/core/secrets-audit.d.ts +21 -0
  99. package/dist/core/secrets-audit.js +60 -0
  100. package/dist/core/secrets-metadata.d.ts +39 -0
  101. package/dist/core/secrets-metadata.js +82 -0
  102. package/dist/core/secrets-motd.d.ts +20 -0
  103. package/dist/core/secrets-motd.js +72 -0
  104. package/dist/core/secrets-ops.d.ts +3 -1
  105. package/dist/core/secrets-ops.js +78 -13
  106. package/dist/core/secrets-providers.d.ts +50 -0
  107. package/dist/core/secrets-providers.js +291 -0
  108. package/dist/core/secrets-rotation.d.ts +52 -0
  109. package/dist/core/secrets-rotation.js +165 -0
  110. package/dist/core/secrets-snapshots.d.ts +26 -0
  111. package/dist/core/secrets-snapshots.js +95 -0
  112. package/dist/core/secrets-validate.js +2 -1
  113. package/dist/core/secrets.d.ts +12 -1
  114. package/dist/core/secrets.js +35 -24
  115. package/dist/core/self-update.d.ts +41 -0
  116. package/dist/core/self-update.js +73 -0
  117. package/dist/core/systemd.js +29 -12
  118. package/dist/core/telegram.d.ts +6 -0
  119. package/dist/core/telegram.js +32 -0
  120. package/dist/core/validate.d.ts +7 -0
  121. package/dist/core/validate.js +42 -0
  122. package/dist/index.js +0 -4
  123. package/dist/mcp/deps-tools.js +9 -1
  124. package/dist/mcp/git-tools.js +4 -4
  125. package/dist/mcp/server.js +193 -8
  126. package/dist/templates/systemd.js +3 -3
  127. package/dist/templates/unseal.js +5 -1
  128. package/dist/tui/components/Confirm.js +3 -4
  129. package/dist/tui/components/Header.js +37 -8
  130. package/dist/tui/components/KeyHint.js +14 -5
  131. package/dist/tui/exec-bridge.js +26 -12
  132. package/dist/tui/hooks/use-fleet-data.js +5 -2
  133. package/dist/tui/hooks/use-health.js +5 -2
  134. package/dist/tui/hooks/use-terminal-size.d.ts +1 -0
  135. package/dist/tui/hooks/use-terminal-size.js +1 -0
  136. package/dist/tui/router.js +133 -8
  137. package/dist/tui/routines/RoutinesApp.d.ts +8 -0
  138. package/dist/tui/routines/RoutinesApp.js +277 -0
  139. package/dist/tui/routines/components/AlertsPanel.d.ts +7 -0
  140. package/dist/tui/routines/components/AlertsPanel.js +22 -0
  141. package/dist/tui/routines/components/AlertsPanel.test.d.ts +1 -0
  142. package/dist/tui/routines/components/AlertsPanel.test.js +52 -0
  143. package/dist/tui/routines/components/CommandPalette.d.ts +12 -0
  144. package/dist/tui/routines/components/CommandPalette.js +21 -0
  145. package/dist/tui/routines/components/LiveRunPanel.d.ts +12 -0
  146. package/dist/tui/routines/components/LiveRunPanel.js +107 -0
  147. package/dist/tui/routines/components/RoutineForm.d.ts +8 -0
  148. package/dist/tui/routines/components/RoutineForm.js +254 -0
  149. package/dist/tui/routines/components/SignalsGrid.d.ts +13 -0
  150. package/dist/tui/routines/components/SignalsGrid.js +34 -0
  151. package/dist/tui/routines/components/SignalsGrid.test.d.ts +1 -0
  152. package/dist/tui/routines/components/SignalsGrid.test.js +43 -0
  153. package/dist/tui/routines/format.d.ts +7 -0
  154. package/dist/tui/routines/format.js +51 -0
  155. package/dist/tui/routines/hooks/use-git-fleet.d.ts +33 -0
  156. package/dist/tui/routines/hooks/use-git-fleet.js +82 -0
  157. package/dist/tui/routines/hooks/use-logs-stream.d.ts +13 -0
  158. package/dist/tui/routines/hooks/use-logs-stream.js +64 -0
  159. package/dist/tui/routines/hooks/use-ops-fleet.d.ts +20 -0
  160. package/dist/tui/routines/hooks/use-ops-fleet.js +70 -0
  161. package/dist/tui/routines/hooks/use-repo-detail.d.ts +31 -0
  162. package/dist/tui/routines/hooks/use-repo-detail.js +104 -0
  163. package/dist/tui/routines/hooks/use-security.d.ts +33 -0
  164. package/dist/tui/routines/hooks/use-security.js +110 -0
  165. package/dist/tui/routines/hooks/use-signals.d.ts +9 -0
  166. package/dist/tui/routines/hooks/use-signals.js +60 -0
  167. package/dist/tui/routines/runtime.d.ts +20 -0
  168. package/dist/tui/routines/runtime.js +40 -0
  169. package/dist/tui/routines/tabs/CostTab.d.ts +7 -0
  170. package/dist/tui/routines/tabs/CostTab.js +24 -0
  171. package/dist/tui/routines/tabs/DashboardTab.d.ts +15 -0
  172. package/dist/tui/routines/tabs/DashboardTab.js +10 -0
  173. package/dist/tui/routines/tabs/GitTab.d.ts +6 -0
  174. package/dist/tui/routines/tabs/GitTab.js +39 -0
  175. package/dist/tui/routines/tabs/LogsTab.d.ts +6 -0
  176. package/dist/tui/routines/tabs/LogsTab.js +58 -0
  177. package/dist/tui/routines/tabs/OpsTab.d.ts +6 -0
  178. package/dist/tui/routines/tabs/OpsTab.js +34 -0
  179. package/dist/tui/routines/tabs/RepoDetailView.d.ts +6 -0
  180. package/dist/tui/routines/tabs/RepoDetailView.js +12 -0
  181. package/dist/tui/routines/tabs/RoutinesTab.d.ts +10 -0
  182. package/dist/tui/routines/tabs/RoutinesTab.js +58 -0
  183. package/dist/tui/routines/tabs/ScaffoldTab.d.ts +2 -0
  184. package/dist/tui/routines/tabs/ScaffoldTab.js +127 -0
  185. package/dist/tui/routines/tabs/SecurityTab.d.ts +6 -0
  186. package/dist/tui/routines/tabs/SecurityTab.js +31 -0
  187. package/dist/tui/routines/tabs/SettingsTab.d.ts +6 -0
  188. package/dist/tui/routines/tabs/SettingsTab.js +61 -0
  189. package/dist/tui/routines/tabs/TimelineTab.d.ts +7 -0
  190. package/dist/tui/routines/tabs/TimelineTab.js +26 -0
  191. package/dist/tui/state.js +16 -1
  192. package/dist/tui/tests/flicker.test.d.ts +1 -0
  193. package/dist/tui/tests/flicker.test.js +105 -0
  194. package/dist/tui/tests/keyboard-integration.test.d.ts +1 -0
  195. package/dist/tui/tests/keyboard-integration.test.js +120 -0
  196. package/dist/tui/tests/test-app.d.ts +4 -0
  197. package/dist/tui/tests/test-app.js +79 -0
  198. package/dist/tui/types.d.ts +14 -1
  199. package/dist/tui/views/AppDetail.js +40 -26
  200. package/dist/tui/views/Dashboard.js +34 -9
  201. package/dist/tui/views/HealthView.js +42 -12
  202. package/dist/tui/views/LogsView.js +38 -10
  203. package/dist/tui/views/MultiLogsView.d.ts +2 -0
  204. package/dist/tui/views/MultiLogsView.js +165 -0
  205. package/dist/tui/views/SecretEdit.js +18 -7
  206. package/dist/tui/views/SecretsView.js +55 -39
  207. package/dist/ui/prompt.d.ts +52 -0
  208. package/dist/ui/prompt.js +169 -0
  209. package/package.json +33 -5
  210. package/dist/commands/motd.d.ts +0 -1
  211. package/dist/commands/motd.js +0 -10
  212. package/dist/templates/motd.d.ts +0 -1
  213. package/dist/templates/motd.js +0 -7
  214. package/dist/tui/components/AppList.d.ts +0 -12
  215. package/dist/tui/components/AppList.js +0 -32
  216. package/dist/tui/hooks/use-keyboard.d.ts +0 -1
  217. package/dist/tui/hooks/use-keyboard.js +0 -44
@@ -0,0 +1,105 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect } from 'vitest';
4
+ import { TestApp } from './test-app.js';
5
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
6
+ const apps = ['app-alpha', 'app-bravo', 'app-charlie'];
7
+ describe('no-flicker guarantees', () => {
8
+ it('tab switch produces no intermediate frames', async () => {
9
+ const { stdin, frames } = render(_jsx(TestApp, { items: apps }));
10
+ await delay(100);
11
+ const beforeCount = frames.length;
12
+ stdin.write('\t');
13
+ await delay(50);
14
+ // grab only the frames produced by this action
15
+ const newFrames = frames.slice(beforeCount);
16
+ // every new frame must show the destination view, never a blank or half-state
17
+ for (const frame of newFrames) {
18
+ expect(frame).toContain('view:');
19
+ // should not show a frame with view:dashboard after we switched to health
20
+ expect(frame).toContain('view:health');
21
+ }
22
+ });
23
+ it('enter on dashboard does not flash dashboard before showing detail', async () => {
24
+ const { stdin, frames } = render(_jsx(TestApp, { items: apps }));
25
+ await delay(100);
26
+ stdin.write('j');
27
+ await delay(50);
28
+ const beforeCount = frames.length;
29
+ stdin.write('\r');
30
+ await delay(50);
31
+ const newFrames = frames.slice(beforeCount);
32
+ // should have at least one frame with the detail view
33
+ const detailFrames = newFrames.filter(f => f.includes('view:app-detail'));
34
+ expect(detailFrames.length).toBeGreaterThan(0);
35
+ // no frame should show view:dashboard after the enter press
36
+ // (which would indicate SELECT_APP rendered before NAVIGATE)
37
+ for (const frame of newFrames) {
38
+ if (frame.includes('view:')) {
39
+ expect(frame).toContain('view:app-detail');
40
+ }
41
+ }
42
+ });
43
+ it('arrow key navigation produces exactly one visual change', async () => {
44
+ const { stdin, frames } = render(_jsx(TestApp, { items: apps }));
45
+ await delay(100);
46
+ const beforeCount = frames.length;
47
+ stdin.write('\x1B[B'); // down arrow
48
+ await delay(50);
49
+ const newFrames = frames.slice(beforeCount);
50
+ // all new frames should show the cursor on app-bravo
51
+ for (const frame of newFrames) {
52
+ expect(frame).toContain('> app-bravo');
53
+ }
54
+ });
55
+ it('escape from sub-view does not flash intermediate state', async () => {
56
+ const { stdin, frames } = render(_jsx(TestApp, { items: apps }));
57
+ await delay(100);
58
+ // go to detail
59
+ stdin.write('\r');
60
+ await delay(50);
61
+ expect(frames[frames.length - 1]).toContain('view:app-detail');
62
+ const beforeCount = frames.length;
63
+ stdin.write('\x1B');
64
+ await delay(50);
65
+ const newFrames = frames.slice(beforeCount);
66
+ for (const frame of newFrames) {
67
+ expect(frame).toContain('view:dashboard');
68
+ }
69
+ });
70
+ it('rapid key presses do not produce garbled frames', async () => {
71
+ const { stdin, frames } = render(_jsx(TestApp, { items: apps }));
72
+ await delay(100);
73
+ // rapid j-j-j
74
+ stdin.write('j');
75
+ stdin.write('j');
76
+ stdin.write('j');
77
+ await delay(100);
78
+ const lastFrame = frames[frames.length - 1];
79
+ // should be clamped at the last item
80
+ expect(lastFrame).toContain('> app-charlie');
81
+ // should still show the view label
82
+ expect(lastFrame).toContain('view:dashboard');
83
+ });
84
+ it('help overlay toggle produces no blank frames', async () => {
85
+ const { stdin, frames } = render(_jsx(TestApp, { items: apps }));
86
+ await delay(100);
87
+ const beforeCount = frames.length;
88
+ stdin.write('?');
89
+ await delay(50);
90
+ const helpFrames = frames.slice(beforeCount);
91
+ for (const frame of helpFrames) {
92
+ expect(frame).toContain('view:dashboard');
93
+ expect(frame).toContain('help-overlay');
94
+ }
95
+ const beforeDismiss = frames.length;
96
+ stdin.write('x');
97
+ await delay(50);
98
+ const dismissFrames = frames.slice(beforeDismiss);
99
+ for (const frame of dismissFrames) {
100
+ expect(frame).toContain('view:dashboard');
101
+ // help should be gone
102
+ expect(frame).not.toContain('help-overlay');
103
+ }
104
+ });
105
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect } from 'vitest';
4
+ import { TestApp } from './test-app.js';
5
+ const delay = (ms) => new Promise((r) => setTimeout(r, ms));
6
+ const apps = ['app-alpha', 'app-bravo', 'app-charlie'];
7
+ describe('keyboard integration', () => {
8
+ it('tab switches between top-level views', async () => {
9
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
10
+ await delay(100);
11
+ expect(lastFrame()).toContain('view:dashboard');
12
+ stdin.write('\t');
13
+ await delay(50);
14
+ expect(lastFrame()).toContain('view:health');
15
+ stdin.write('\t');
16
+ await delay(50);
17
+ expect(lastFrame()).toContain('view:secrets');
18
+ stdin.write('\t');
19
+ await delay(50);
20
+ expect(lastFrame()).toContain('view:logs-multi');
21
+ stdin.write('\t');
22
+ await delay(50);
23
+ expect(lastFrame()).toContain('view:dashboard');
24
+ });
25
+ it('arrow down moves selection in dashboard', async () => {
26
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
27
+ await delay(100);
28
+ expect(lastFrame()).toContain('> app-alpha');
29
+ stdin.write('\x1B[B');
30
+ await delay(50);
31
+ expect(lastFrame()).toContain('> app-bravo');
32
+ stdin.write('\x1B[B');
33
+ await delay(50);
34
+ expect(lastFrame()).toContain('> app-charlie');
35
+ });
36
+ it('arrow up moves selection up in dashboard', async () => {
37
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
38
+ await delay(100);
39
+ stdin.write('\x1B[B');
40
+ await delay(50);
41
+ stdin.write('\x1B[B');
42
+ await delay(50);
43
+ expect(lastFrame()).toContain('> app-charlie');
44
+ stdin.write('\x1B[A');
45
+ await delay(50);
46
+ expect(lastFrame()).toContain('> app-bravo');
47
+ });
48
+ it('j/k keys also navigate the list', async () => {
49
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
50
+ await delay(100);
51
+ stdin.write('j');
52
+ await delay(50);
53
+ expect(lastFrame()).toContain('> app-bravo');
54
+ stdin.write('k');
55
+ await delay(50);
56
+ expect(lastFrame()).toContain('> app-alpha');
57
+ });
58
+ it('enter selects an app and navigates to detail', async () => {
59
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
60
+ await delay(100);
61
+ stdin.write('j');
62
+ await delay(50);
63
+ stdin.write('\r');
64
+ await delay(50);
65
+ expect(lastFrame()).toContain('view:app-detail');
66
+ expect(lastFrame()).toContain('detail:app-bravo');
67
+ });
68
+ it('escape goes back from sub-view', async () => {
69
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
70
+ await delay(100);
71
+ stdin.write('\r');
72
+ await delay(50);
73
+ expect(lastFrame()).toContain('view:app-detail');
74
+ stdin.write('\x1B');
75
+ await delay(50);
76
+ expect(lastFrame()).toContain('view:dashboard');
77
+ });
78
+ it('arrow keys are clamped at list boundaries', async () => {
79
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
80
+ await delay(100);
81
+ stdin.write('\x1B[A');
82
+ await delay(50);
83
+ expect(lastFrame()).toContain('> app-alpha');
84
+ stdin.write('\x1B[B');
85
+ await delay(50);
86
+ stdin.write('\x1B[B');
87
+ await delay(50);
88
+ stdin.write('\x1B[B');
89
+ await delay(50);
90
+ expect(lastFrame()).toContain('> app-charlie');
91
+ });
92
+ it('? toggles help overlay and any key dismisses it', async () => {
93
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
94
+ await delay(100);
95
+ stdin.write('?');
96
+ await delay(50);
97
+ expect(lastFrame()).toContain('help-overlay');
98
+ stdin.write('x');
99
+ await delay(50);
100
+ expect(lastFrame()).not.toContain('help-overlay');
101
+ });
102
+ it('tab works from a sub-view, using previousView as base', async () => {
103
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
104
+ await delay(100);
105
+ stdin.write('\r');
106
+ await delay(50);
107
+ expect(lastFrame()).toContain('view:app-detail');
108
+ stdin.write('\t');
109
+ await delay(50);
110
+ expect(lastFrame()).toContain('view:health');
111
+ });
112
+ it('escape does nothing on top-level view with no previousView', async () => {
113
+ const { stdin, lastFrame } = render(_jsx(TestApp, { items: apps }));
114
+ await delay(100);
115
+ expect(lastFrame()).toContain('view:dashboard');
116
+ stdin.write('\x1B');
117
+ await delay(50);
118
+ expect(lastFrame()).toContain('view:dashboard');
119
+ });
120
+ });
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ export declare function TestApp({ items }: {
3
+ items: string[];
4
+ }): React.JSX.Element;
@@ -0,0 +1,79 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useReducer, useState, useCallback } from 'react';
3
+ import { Text, Box } from 'ink';
4
+ import { InputDispatcher, useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
5
+ import { reducer, initialState, nextTopView } from '../state.js';
6
+ function MockDashboard({ state, dispatch, items, }) {
7
+ const handler = (input, key) => {
8
+ if (items.length === 0)
9
+ return false;
10
+ if (input === 'j' || key.downArrow) {
11
+ dispatch({ type: 'SET_INDEX', view: 'dashboard', index: Math.min(state.dashboardIndex + 1, items.length - 1) });
12
+ return true;
13
+ }
14
+ if (input === 'k' || key.upArrow) {
15
+ dispatch({ type: 'SET_INDEX', view: 'dashboard', index: Math.max(state.dashboardIndex - 1, 0) });
16
+ return true;
17
+ }
18
+ if (key.return) {
19
+ dispatch({ type: 'SELECT_APP', app: items[state.dashboardIndex] });
20
+ dispatch({ type: 'NAVIGATE', view: 'app-detail' });
21
+ return true;
22
+ }
23
+ return false;
24
+ };
25
+ useRegisterHandler(handler);
26
+ return (_jsx(Box, { flexDirection: "column", children: items.map((item, i) => (_jsxs(Text, { children: [i === state.dashboardIndex ? '> ' : ' ', item] }, item))) }));
27
+ }
28
+ export function TestApp({ items }) {
29
+ const [state, dispatch] = useReducer(reducer, initialState);
30
+ const [showHelp, setShowHelp] = useState(false);
31
+ const globalHandler = useCallback((input, key) => {
32
+ if (showHelp) {
33
+ setShowHelp(false);
34
+ return true;
35
+ }
36
+ if (state.confirmAction) {
37
+ if (input === 'y' || input === 'Y') {
38
+ state.confirmAction.onConfirm();
39
+ dispatch({ type: 'CANCEL_CONFIRM' });
40
+ }
41
+ else if (input === 'n' || input === 'N' || key.escape) {
42
+ dispatch({ type: 'CANCEL_CONFIRM' });
43
+ }
44
+ return true;
45
+ }
46
+ if (input === '?') {
47
+ setShowHelp(true);
48
+ return true;
49
+ }
50
+ if (key.tab) {
51
+ const topViews = ['dashboard', 'health', 'secrets', 'logs-multi'];
52
+ const base = topViews.includes(state.currentView)
53
+ ? state.currentView
54
+ : state.previousView ?? 'dashboard';
55
+ dispatch({ type: 'NAVIGATE', view: nextTopView(base) });
56
+ return true;
57
+ }
58
+ if (key.escape && state.previousView) {
59
+ dispatch({ type: 'GO_BACK' });
60
+ return true;
61
+ }
62
+ return false;
63
+ }, [state.confirmAction, state.currentView, state.previousView, showHelp]);
64
+ const renderView = () => {
65
+ switch (state.currentView) {
66
+ case 'dashboard':
67
+ return _jsx(MockDashboard, { state: state, dispatch: dispatch, items: items });
68
+ case 'health':
69
+ return _jsx(Text, { children: "health-view" });
70
+ case 'secrets':
71
+ return _jsx(Text, { children: "secrets-view" });
72
+ case 'app-detail':
73
+ return _jsxs(Text, { children: ["detail:", state.selectedApp] });
74
+ default:
75
+ return _jsx(MockDashboard, { state: state, dispatch: dispatch, items: items });
76
+ }
77
+ };
78
+ return (_jsx(InputDispatcher, { globalHandler: globalHandler, children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["view:", state.currentView] }), showHelp ? _jsx(Text, { children: "help-overlay" }) : renderView()] }) }));
79
+ }
@@ -1,4 +1,5 @@
1
- export type View = 'dashboard' | 'app-detail' | 'health' | 'secrets' | 'secret-edit' | 'logs';
1
+ export type View = 'dashboard' | 'app-detail' | 'health' | 'secrets' | 'secret-edit' | 'logs' | 'logs-multi';
2
+ export type SecretsSubView = 'app-list' | 'secret-list';
2
3
  export interface TuiState {
3
4
  currentView: View;
4
5
  previousView: View | null;
@@ -8,6 +9,11 @@ export interface TuiState {
8
9
  loading: boolean;
9
10
  error: string | null;
10
11
  confirmAction: ConfirmAction | null;
12
+ dashboardIndex: number;
13
+ healthIndex: number;
14
+ secretsIndex: number;
15
+ secretsSubView: SecretsSubView;
16
+ appDetailIndex: number;
11
17
  }
12
18
  export interface ConfirmAction {
13
19
  label: string;
@@ -38,4 +44,11 @@ export type Action = {
38
44
  action: ConfirmAction;
39
45
  } | {
40
46
  type: 'CANCEL_CONFIRM';
47
+ } | {
48
+ type: 'SET_INDEX';
49
+ view: 'dashboard' | 'health' | 'secrets' | 'appDetail';
50
+ index: number;
51
+ } | {
52
+ type: 'SET_SECRETS_SUBVIEW';
53
+ subView: SecretsSubView;
41
54
  };
@@ -1,7 +1,8 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { Box, Text, useInput } from 'ink';
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text } from 'ink';
4
4
  import Spinner from 'ink-spinner';
5
+ import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
5
6
  import { useAppState, useAppDispatch, useRedact } from '../state.js';
6
7
  import { runFleetCommand } from '../exec-bridge.js';
7
8
  import { colors } from '../theme.js';
@@ -14,28 +15,48 @@ const ACTIONS = [
14
15
  { key: '5', label: 'Logs', command: ['logs'] },
15
16
  ];
16
17
  export function AppDetail() {
17
- const { selectedApp, redacted } = useAppState();
18
+ const { selectedApp, redacted, appDetailIndex } = useAppState();
18
19
  const dispatch = useAppDispatch();
19
20
  const redact = useRedact();
20
- const [actionIndex, setActionIndex] = useState(0);
21
21
  const [running, setRunning] = useState(false);
22
22
  const [result, setResult] = useState(null);
23
- const reg = load();
24
- const app = selectedApp ? findApp(reg, selectedApp) : undefined;
25
- useInput((input, key) => {
26
- if (running)
23
+ const [app, setApp] = useState(undefined);
24
+ useEffect(() => {
25
+ if (selectedApp) {
26
+ try {
27
+ const reg = load();
28
+ setApp(findApp(reg, selectedApp));
29
+ }
30
+ catch {
31
+ setApp(undefined);
32
+ }
33
+ }
34
+ }, [selectedApp]);
35
+ function executeAction(action) {
36
+ if (!selectedApp)
27
37
  return;
38
+ setRunning(true);
39
+ setResult(null);
40
+ runFleetCommand([...action.command, selectedApp])
41
+ .then(res => setResult(res))
42
+ .finally(() => setRunning(false));
43
+ }
44
+ const handler = (input, key) => {
45
+ if (running)
46
+ return false;
28
47
  if (input === 'j' || key.downArrow) {
29
- setActionIndex(prev => Math.min(prev + 1, ACTIONS.length - 1));
48
+ dispatch({ type: 'SET_INDEX', view: 'appDetail', index: Math.min(appDetailIndex + 1, ACTIONS.length - 1) });
49
+ return true;
30
50
  }
31
- else if (input === 'k' || key.upArrow) {
32
- setActionIndex(prev => Math.max(prev - 1, 0));
51
+ if (input === 'k' || key.upArrow) {
52
+ dispatch({ type: 'SET_INDEX', view: 'appDetail', index: Math.max(appDetailIndex - 1, 0) });
53
+ return true;
33
54
  }
34
- else if (key.return) {
35
- const action = ACTIONS[actionIndex];
55
+ if (key.return) {
56
+ const action = ACTIONS[appDetailIndex];
36
57
  if (action.command[0] === 'logs') {
37
58
  dispatch({ type: 'NAVIGATE', view: 'logs' });
38
- return;
59
+ return true;
39
60
  }
40
61
  if (action.destructive) {
41
62
  dispatch({
@@ -50,23 +71,16 @@ export function AppDetail() {
50
71
  else {
51
72
  executeAction(action);
52
73
  }
74
+ return true;
53
75
  }
54
- });
55
- function executeAction(action) {
56
- if (!selectedApp)
57
- return;
58
- setRunning(true);
59
- setResult(null);
60
- runFleetCommand([...action.command, selectedApp]).then(res => {
61
- setResult(res);
62
- setRunning(false);
63
- });
64
- }
76
+ return false;
77
+ };
78
+ useRegisterHandler(handler);
65
79
  if (!app) {
66
80
  return (_jsx(Box, { padding: 1, children: _jsxs(Text, { color: colors.error, children: ["App not found: ", selectedApp] }) }));
67
81
  }
68
82
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { bold: true, color: colors.primary, children: redact(app.displayName || app.name) }), _jsxs(Box, { marginY: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.muted, children: "Type: " }), app.type] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.muted, children: "Service: " }), redacted ? '***' : app.serviceName] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.muted, children: "Compose: " }), redacted ? '***' : app.composePath] }), app.domains.length > 0 && (_jsxs(Text, { children: [_jsx(Text, { color: colors.muted, children: "Domains: " }), redacted ? '***' : app.domains.join(', ')] })), app.port && (_jsxs(Text, { children: [_jsx(Text, { color: colors.muted, children: "Port: " }), app.port] })), _jsxs(Text, { children: [_jsx(Text, { color: colors.muted, children: "Containers:" }), " ", redacted ? '***' : app.containers.join(', ')] }), app.gitRepo && (_jsxs(Text, { children: [_jsx(Text, { color: colors.muted, children: "Git: " }), redacted ? '***' : app.gitRepo] }))] }), _jsx(Text, { bold: true, children: "Actions" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ACTIONS.map((action, i) => {
69
- const selected = i === actionIndex;
83
+ const selected = i === appDetailIndex;
70
84
  return (_jsxs(Text, { children: [_jsx(Text, { color: colors.primary, children: selected ? '> ' : ' ' }), _jsxs(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: ["[", action.key, "] ", action.label] }), action.destructive && _jsx(Text, { color: colors.warning, children: " !" })] }, action.key));
71
85
  }) }), running && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Running..."] }) })), result && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: result.ok ? colors.success : colors.error, children: result.ok ? 'Done' : 'Failed' }), result.output && (_jsx(Text, { color: colors.muted, children: result.output.trim().slice(0, 500) }))] }))] }));
72
86
  }
@@ -1,15 +1,42 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
2
3
  import { Box, Text } from 'ink';
3
4
  import Spinner from 'ink-spinner';
4
- import { useAppDispatch, useRedact } from '../state.js';
5
+ import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
6
+ import { ScrollableList } from '@matthesketh/ink-scrollable-list';
7
+ import { useAvailableHeight } from '@matthesketh/ink-viewport';
8
+ import { useAppState, useAppDispatch, useRedact } from '../state.js';
5
9
  import { useFleetData } from '../hooks/use-fleet-data.js';
6
- import { AppList } from '../components/AppList.js';
7
- import { StatusBadge } from '../components/StatusBadge.js';
8
10
  import { colors } from '../theme.js';
9
11
  export function Dashboard() {
12
+ const state = useAppState();
10
13
  const dispatch = useAppDispatch();
11
14
  const { status, loading, error } = useFleetData();
12
15
  const redact = useRedact();
16
+ const availableHeight = useAvailableHeight();
17
+ const items = useMemo(() => status?.apps.map(app => ({ ...app, name: app.name })) ?? [], [status]);
18
+ const handler = (input, key) => {
19
+ if (items.length === 0)
20
+ return false;
21
+ if (input === 'j' || key.downArrow) {
22
+ dispatch({ type: 'SET_INDEX', view: 'dashboard', index: Math.min(state.dashboardIndex + 1, items.length - 1) });
23
+ return true;
24
+ }
25
+ if (input === 'k' || key.upArrow) {
26
+ dispatch({ type: 'SET_INDEX', view: 'dashboard', index: Math.max(state.dashboardIndex - 1, 0) });
27
+ return true;
28
+ }
29
+ if (key.return) {
30
+ const item = items[state.dashboardIndex];
31
+ if (item) {
32
+ dispatch({ type: 'SELECT_APP', app: item.name });
33
+ dispatch({ type: 'NAVIGATE', view: 'app-detail' });
34
+ }
35
+ return true;
36
+ }
37
+ return false;
38
+ };
39
+ useRegisterHandler(handler);
13
40
  if (loading && !status) {
14
41
  return (_jsx(Box, { padding: 1, children: _jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Loading fleet status..."] }) }));
15
42
  }
@@ -18,12 +45,10 @@ export function Dashboard() {
18
45
  }
19
46
  if (!status)
20
47
  return _jsx(Text, { color: colors.muted, children: "No data" });
21
- const items = status.apps.map(app => ({ ...app, name: app.name }));
22
- 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"] })), loading && _jsx(Text, { color: colors.muted, children: _jsx(Spinner, { type: "dots" }) })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: [" ", 'APP'.padEnd(24), 'SYSTEMD'.padEnd(14), 'CONTAINERS'.padEnd(14), "HEALTH"] }) }), _jsx(AppList, { items: items, onSelect: (item) => {
23
- dispatch({ type: 'SELECT_APP', app: item.name });
24
- dispatch({ type: 'NAVIGATE', view: 'app-detail' });
25
- }, renderItem: (item, selected) => {
48
+ const listHeight = Math.max(5, availableHeight - 4);
49
+ 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"] })), loading && _jsx(Text, { color: colors.muted, children: _jsx(Spinner, { type: "dots" }) })] }), _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) => {
26
50
  const app = status.apps.find(a => a.name === item.name);
27
- return (_jsxs(Box, { children: [_jsx(Text, { bold: selected, color: selected ? colors.primary : colors.text, children: redact(app.name).padEnd(22) }), _jsx(Text, { children: " " }), _jsx(Box, { width: 14, children: _jsx(StatusBadge, { value: app.systemd, type: "systemd" }) }), _jsx(Box, { width: 14, children: _jsx(Text, { children: app.containers }) }), _jsx(StatusBadge, { value: app.health, type: "health" })] }));
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) }) })] }));
28
53
  } })] }));
29
54
  }
@@ -1,28 +1,58 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
2
3
  import { Box, Text } from 'ink';
3
4
  import Spinner from 'ink-spinner';
5
+ import { useRegisterHandler } from '@matthesketh/ink-input-dispatcher';
6
+ import { ScrollableList } from '@matthesketh/ink-scrollable-list';
7
+ import { useAvailableHeight } from '@matthesketh/ink-viewport';
4
8
  import { useHealth } from '../hooks/use-health.js';
5
9
  import { StatusBadge } from '../components/StatusBadge.js';
6
- import { useRedact } from '../state.js';
10
+ import { useAppState, useAppDispatch, useRedact } from '../state.js';
7
11
  import { colors } from '../theme.js';
8
12
  export function HealthView() {
13
+ const state = useAppState();
14
+ const dispatch = useAppDispatch();
9
15
  const { results, loading, error } = useHealth();
10
16
  const redact = useRedact();
17
+ const availableHeight = useAvailableHeight();
18
+ const counts = useMemo(() => ({
19
+ healthy: results.filter(r => r.overall === 'healthy').length,
20
+ degraded: results.filter(r => r.overall === 'degraded').length,
21
+ down: results.filter(r => r.overall === 'down').length,
22
+ }), [results]);
23
+ const handler = (input, key) => {
24
+ if (results.length === 0)
25
+ return false;
26
+ if (input === 'j' || key.downArrow) {
27
+ dispatch({ type: 'SET_INDEX', view: 'health', index: Math.min(state.healthIndex + 1, results.length - 1) });
28
+ return true;
29
+ }
30
+ if (input === 'k' || key.upArrow) {
31
+ dispatch({ type: 'SET_INDEX', view: 'health', index: Math.max(state.healthIndex - 1, 0) });
32
+ return true;
33
+ }
34
+ return false;
35
+ };
36
+ useRegisterHandler(handler);
11
37
  if (loading && results.length === 0) {
12
38
  return (_jsx(Box, { padding: 1, children: _jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Running health checks..."] }) }));
13
39
  }
14
40
  if (error && results.length === 0) {
15
41
  return (_jsx(Box, { padding: 1, children: _jsxs(Text, { color: colors.error, children: ["Error: ", error] }) }));
16
42
  }
17
- const healthy = results.filter(r => r.overall === 'healthy').length;
18
- const degraded = results.filter(r => r.overall === 'degraded').length;
19
- const down = results.filter(r => r.overall === 'down').length;
20
- 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: [healthy, " healthy"] }), degraded > 0 && _jsxs(Text, { color: colors.warning, children: [degraded, " degraded"] }), down > 0 && _jsxs(Text, { color: colors.error, children: [down, " down"] }), loading && _jsx(Text, { color: colors.muted, children: _jsx(Spinner, { type: "dots" }) })] }), _jsxs(Text, { bold: true, children: [' APP'.padEnd(26), 'SYSTEMD'.padEnd(12), 'CONTAINERS'.padEnd(20), 'HTTP'.padEnd(10), "OVERALL"] }), results.map(result => {
21
- const runningCount = result.containers.filter(c => c.running).length;
22
- const containerStr = `${runningCount}/${result.containers.length}`;
23
- const httpStr = result.http
24
- ? result.http.ok ? `${result.http.status}` : `err`
25
- : 'n/a';
26
- return (_jsxs(Box, { children: [_jsxs(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" })] }, result.app));
27
- })] }));
43
+ const listHeight = Math.max(5, availableHeight - 4);
44
+ 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"] }), loading && _jsx(Text, { color: colors.muted, children: _jsx(Spinner, { type: "dots" }) })] }), _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) => {
45
+ const runningCount = result.containers.filter(c => c.running).length;
46
+ const containerStr = `${runningCount}/${result.containers.length}`;
47
+ // 404 "no /health" (app never implemented one — distinct from a real failure).
48
+ // Keeps the TUI honest after the post-incident health-check fix.
49
+ const httpStr = result.http
50
+ ? result.http.ok
51
+ ? `${result.http.status}`
52
+ : result.http.endpointMissing
53
+ ? 'no /health'
54
+ : `${result.http.status ?? 'err'}`
55
+ : 'n/a';
56
+ 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" })] }));
57
+ } })] }));
28
58
  }