@matthesketh/fleet 1.2.0 → 1.7.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 (218) 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 +46 -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/guard.d.ts +1 -0
  39. package/dist/commands/guard.js +144 -0
  40. package/dist/commands/logs.d.ts +1 -1
  41. package/dist/commands/logs.js +237 -8
  42. package/dist/commands/patch-systemd.d.ts +1 -0
  43. package/dist/commands/patch-systemd.js +126 -0
  44. package/dist/commands/rollback.d.ts +1 -0
  45. package/dist/commands/rollback.js +58 -0
  46. package/dist/commands/routine-run.d.ts +1 -0
  47. package/dist/commands/routine-run.js +122 -0
  48. package/dist/commands/routines.d.ts +1 -0
  49. package/dist/commands/routines.js +25 -0
  50. package/dist/commands/secrets.js +449 -16
  51. package/dist/commands/status.js +7 -3
  52. package/dist/commands/watchdog.d.ts +1 -1
  53. package/dist/commands/watchdog.js +16 -40
  54. package/dist/core/boot-refresh.d.ts +57 -0
  55. package/dist/core/boot-refresh.js +116 -0
  56. package/dist/core/deps/actors/pr-creator.js +11 -9
  57. package/dist/core/deps/collectors/docker-running.js +2 -2
  58. package/dist/core/deps/collectors/github-pr.js +5 -2
  59. package/dist/core/deps/collectors/npm.js +10 -5
  60. package/dist/core/deps/collectors/vulnerability.js +10 -6
  61. package/dist/core/deps/reporters/motd.js +1 -1
  62. package/dist/core/deps/reporters/telegram.js +2 -29
  63. package/dist/core/docker.js +45 -15
  64. package/dist/core/egress.d.ts +41 -0
  65. package/dist/core/egress.js +161 -0
  66. package/dist/core/exec.d.ts +7 -1
  67. package/dist/core/exec.js +25 -17
  68. package/dist/core/git.d.ts +1 -0
  69. package/dist/core/git.js +36 -23
  70. package/dist/core/github.js +27 -8
  71. package/dist/core/health.d.ts +3 -0
  72. package/dist/core/health.js +15 -3
  73. package/dist/core/logs-multi.d.ts +73 -0
  74. package/dist/core/logs-multi.js +163 -0
  75. package/dist/core/logs-policy.d.ts +55 -0
  76. package/dist/core/logs-policy.js +148 -0
  77. package/dist/core/nginx.js +8 -4
  78. package/dist/core/notify.d.ts +15 -0
  79. package/dist/core/notify.js +55 -0
  80. package/dist/core/registry.d.ts +25 -0
  81. package/dist/core/registry.js +57 -10
  82. package/dist/core/routines/cost-queries.d.ts +24 -0
  83. package/dist/core/routines/cost-queries.js +65 -0
  84. package/dist/core/routines/db.d.ts +9 -0
  85. package/dist/core/routines/db.js +126 -0
  86. package/dist/core/routines/defaults.d.ts +2 -0
  87. package/dist/core/routines/defaults.js +72 -0
  88. package/dist/core/routines/engine.d.ts +59 -0
  89. package/dist/core/routines/engine.js +175 -0
  90. package/dist/core/routines/incidents.d.ts +13 -0
  91. package/dist/core/routines/incidents.js +35 -0
  92. package/dist/core/routines/schema.d.ts +418 -0
  93. package/dist/core/routines/schema.js +113 -0
  94. package/dist/core/routines/signals-collector.d.ts +35 -0
  95. package/dist/core/routines/signals-collector.js +114 -0
  96. package/dist/core/routines/store.d.ts +316 -0
  97. package/dist/core/routines/store.js +99 -0
  98. package/dist/core/routines/test-utils.d.ts +2 -0
  99. package/dist/core/routines/test-utils.js +13 -0
  100. package/dist/core/secrets-audit.d.ts +21 -0
  101. package/dist/core/secrets-audit.js +60 -0
  102. package/dist/core/secrets-metadata.d.ts +39 -0
  103. package/dist/core/secrets-metadata.js +82 -0
  104. package/dist/core/secrets-motd.d.ts +20 -0
  105. package/dist/core/secrets-motd.js +72 -0
  106. package/dist/core/secrets-ops.d.ts +3 -1
  107. package/dist/core/secrets-ops.js +78 -13
  108. package/dist/core/secrets-providers.d.ts +50 -0
  109. package/dist/core/secrets-providers.js +291 -0
  110. package/dist/core/secrets-rotation.d.ts +52 -0
  111. package/dist/core/secrets-rotation.js +165 -0
  112. package/dist/core/secrets-snapshots.d.ts +26 -0
  113. package/dist/core/secrets-snapshots.js +95 -0
  114. package/dist/core/secrets-validate.js +2 -1
  115. package/dist/core/secrets.d.ts +12 -1
  116. package/dist/core/secrets.js +35 -24
  117. package/dist/core/self-update.d.ts +41 -0
  118. package/dist/core/self-update.js +73 -0
  119. package/dist/core/systemd.js +29 -12
  120. package/dist/core/telegram.d.ts +6 -0
  121. package/dist/core/telegram.js +32 -0
  122. package/dist/core/validate.d.ts +7 -0
  123. package/dist/core/validate.js +42 -0
  124. package/dist/index.js +0 -4
  125. package/dist/mcp/deps-tools.js +9 -1
  126. package/dist/mcp/git-tools.js +4 -4
  127. package/dist/mcp/server.js +193 -8
  128. package/dist/templates/systemd.js +3 -3
  129. package/dist/templates/unseal.js +5 -1
  130. package/dist/tui/components/KeyHint.js +10 -0
  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/router.js +60 -7
  135. package/dist/tui/routines/RoutinesApp.d.ts +8 -0
  136. package/dist/tui/routines/RoutinesApp.js +277 -0
  137. package/dist/tui/routines/components/AlertsPanel.d.ts +7 -0
  138. package/dist/tui/routines/components/AlertsPanel.js +22 -0
  139. package/dist/tui/routines/components/AlertsPanel.test.d.ts +1 -0
  140. package/dist/tui/routines/components/AlertsPanel.test.js +52 -0
  141. package/dist/tui/routines/components/CommandPalette.d.ts +12 -0
  142. package/dist/tui/routines/components/CommandPalette.js +21 -0
  143. package/dist/tui/routines/components/LiveRunPanel.d.ts +12 -0
  144. package/dist/tui/routines/components/LiveRunPanel.js +107 -0
  145. package/dist/tui/routines/components/RoutineForm.d.ts +8 -0
  146. package/dist/tui/routines/components/RoutineForm.js +254 -0
  147. package/dist/tui/routines/components/SignalsGrid.d.ts +13 -0
  148. package/dist/tui/routines/components/SignalsGrid.js +34 -0
  149. package/dist/tui/routines/components/SignalsGrid.test.d.ts +1 -0
  150. package/dist/tui/routines/components/SignalsGrid.test.js +43 -0
  151. package/dist/tui/routines/format.d.ts +7 -0
  152. package/dist/tui/routines/format.js +51 -0
  153. package/dist/tui/routines/hooks/use-git-fleet.d.ts +33 -0
  154. package/dist/tui/routines/hooks/use-git-fleet.js +82 -0
  155. package/dist/tui/routines/hooks/use-logs-stream.d.ts +13 -0
  156. package/dist/tui/routines/hooks/use-logs-stream.js +64 -0
  157. package/dist/tui/routines/hooks/use-ops-fleet.d.ts +20 -0
  158. package/dist/tui/routines/hooks/use-ops-fleet.js +70 -0
  159. package/dist/tui/routines/hooks/use-repo-detail.d.ts +31 -0
  160. package/dist/tui/routines/hooks/use-repo-detail.js +104 -0
  161. package/dist/tui/routines/hooks/use-security.d.ts +33 -0
  162. package/dist/tui/routines/hooks/use-security.js +110 -0
  163. package/dist/tui/routines/hooks/use-signals.d.ts +9 -0
  164. package/dist/tui/routines/hooks/use-signals.js +60 -0
  165. package/dist/tui/routines/runtime.d.ts +20 -0
  166. package/dist/tui/routines/runtime.js +40 -0
  167. package/dist/tui/routines/tabs/CostTab.d.ts +7 -0
  168. package/dist/tui/routines/tabs/CostTab.js +24 -0
  169. package/dist/tui/routines/tabs/DashboardTab.d.ts +15 -0
  170. package/dist/tui/routines/tabs/DashboardTab.js +10 -0
  171. package/dist/tui/routines/tabs/GitTab.d.ts +6 -0
  172. package/dist/tui/routines/tabs/GitTab.js +39 -0
  173. package/dist/tui/routines/tabs/LogsTab.d.ts +6 -0
  174. package/dist/tui/routines/tabs/LogsTab.js +58 -0
  175. package/dist/tui/routines/tabs/OpsTab.d.ts +6 -0
  176. package/dist/tui/routines/tabs/OpsTab.js +34 -0
  177. package/dist/tui/routines/tabs/RepoDetailView.d.ts +6 -0
  178. package/dist/tui/routines/tabs/RepoDetailView.js +12 -0
  179. package/dist/tui/routines/tabs/RoutinesTab.d.ts +10 -0
  180. package/dist/tui/routines/tabs/RoutinesTab.js +58 -0
  181. package/dist/tui/routines/tabs/ScaffoldTab.d.ts +2 -0
  182. package/dist/tui/routines/tabs/ScaffoldTab.js +127 -0
  183. package/dist/tui/routines/tabs/SecurityTab.d.ts +6 -0
  184. package/dist/tui/routines/tabs/SecurityTab.js +31 -0
  185. package/dist/tui/routines/tabs/SettingsTab.d.ts +6 -0
  186. package/dist/tui/routines/tabs/SettingsTab.js +61 -0
  187. package/dist/tui/routines/tabs/TimelineTab.d.ts +7 -0
  188. package/dist/tui/routines/tabs/TimelineTab.js +26 -0
  189. package/dist/tui/state.js +1 -1
  190. package/dist/tui/tests/keyboard-integration.test.js +3 -0
  191. package/dist/tui/tests/test-app.js +1 -1
  192. package/dist/tui/types.d.ts +2 -2
  193. package/dist/tui/views/AppDetail.js +3 -4
  194. package/dist/tui/views/HealthView.js +7 -1
  195. package/dist/tui/views/LogsView.js +24 -1
  196. package/dist/tui/views/MultiLogsView.d.ts +2 -0
  197. package/dist/tui/views/MultiLogsView.js +165 -0
  198. package/dist/tui/views/SecretEdit.js +10 -3
  199. package/dist/tui/views/SecretsView.js +6 -3
  200. package/dist/ui/prompt.d.ts +52 -0
  201. package/dist/ui/prompt.js +169 -0
  202. package/package.json +34 -21
  203. package/scripts/guard/cert-expiry-watch +109 -0
  204. package/scripts/guard/cf-audit-monitor +169 -0
  205. package/scripts/guard/cf-snapshot +124 -0
  206. package/scripts/guard/cron.d-cf-protect +11 -0
  207. package/scripts/guard/dns-drift-watch +138 -0
  208. package/scripts/guard/fleet-guard +282 -0
  209. package/scripts/guard/fleet-guard-execute +197 -0
  210. package/scripts/guard/notify +108 -0
  211. package/dist/commands/motd.d.ts +0 -1
  212. package/dist/commands/motd.js +0 -10
  213. package/dist/templates/motd.d.ts +0 -1
  214. package/dist/templates/motd.js +0 -7
  215. package/dist/tui/components/AppList.d.ts +0 -12
  216. package/dist/tui/components/AppList.js +0 -32
  217. package/dist/tui/hooks/use-keyboard.d.ts +0 -1
  218. package/dist/tui/hooks/use-keyboard.js +0 -44
@@ -0,0 +1,316 @@
1
+ import { z } from 'zod';
2
+ import { type Routine } from './schema.js';
3
+ declare const FileSchema: z.ZodObject<{
4
+ version: z.ZodLiteral<1>;
5
+ routines: z.ZodArray<z.ZodObject<{
6
+ id: z.ZodString;
7
+ name: z.ZodString;
8
+ description: z.ZodDefault<z.ZodString>;
9
+ schedule: z.ZodUnion<[z.ZodObject<{
10
+ kind: z.ZodLiteral<"manual">;
11
+ }, "strip", z.ZodTypeAny, {
12
+ kind: "manual";
13
+ }, {
14
+ kind: "manual";
15
+ }>, z.ZodObject<{
16
+ kind: z.ZodLiteral<"calendar">;
17
+ onCalendar: z.ZodString;
18
+ randomizedDelaySec: z.ZodDefault<z.ZodNumber>;
19
+ persistent: z.ZodDefault<z.ZodBoolean>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ kind: "calendar";
22
+ onCalendar: string;
23
+ randomizedDelaySec: number;
24
+ persistent: boolean;
25
+ }, {
26
+ kind: "calendar";
27
+ onCalendar: string;
28
+ randomizedDelaySec?: number | undefined;
29
+ persistent?: boolean | undefined;
30
+ }>]>;
31
+ enabled: z.ZodDefault<z.ZodBoolean>;
32
+ targets: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
33
+ perTarget: z.ZodDefault<z.ZodBoolean>;
34
+ task: z.ZodDiscriminatedUnion<"kind", [z.ZodObject<{
35
+ kind: z.ZodLiteral<"claude-cli">;
36
+ prompt: z.ZodString;
37
+ outputFormat: z.ZodDefault<z.ZodLiteral<"json">>;
38
+ tokenCap: z.ZodDefault<z.ZodNumber>;
39
+ wallClockMs: z.ZodDefault<z.ZodNumber>;
40
+ maxUsd: z.ZodDefault<z.ZodNumber>;
41
+ model: z.ZodOptional<z.ZodString>;
42
+ appendSystem: z.ZodOptional<z.ZodString>;
43
+ allowedTools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
44
+ }, "strip", z.ZodTypeAny, {
45
+ kind: "claude-cli";
46
+ prompt: string;
47
+ outputFormat: "json";
48
+ tokenCap: number;
49
+ wallClockMs: number;
50
+ maxUsd: number;
51
+ model?: string | undefined;
52
+ appendSystem?: string | undefined;
53
+ allowedTools?: string[] | undefined;
54
+ }, {
55
+ kind: "claude-cli";
56
+ prompt: string;
57
+ outputFormat?: "json" | undefined;
58
+ tokenCap?: number | undefined;
59
+ wallClockMs?: number | undefined;
60
+ maxUsd?: number | undefined;
61
+ model?: string | undefined;
62
+ appendSystem?: string | undefined;
63
+ allowedTools?: string[] | undefined;
64
+ }>, z.ZodObject<{
65
+ kind: z.ZodLiteral<"shell">;
66
+ argv: z.ZodArray<z.ZodString, "many">;
67
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
68
+ wallClockMs: z.ZodDefault<z.ZodNumber>;
69
+ }, "strip", z.ZodTypeAny, {
70
+ kind: "shell";
71
+ wallClockMs: number;
72
+ argv: string[];
73
+ env?: Record<string, string> | undefined;
74
+ }, {
75
+ kind: "shell";
76
+ argv: string[];
77
+ env?: Record<string, string> | undefined;
78
+ wallClockMs?: number | undefined;
79
+ }>, z.ZodObject<{
80
+ kind: z.ZodLiteral<"mcp-call">;
81
+ tool: z.ZodString;
82
+ args: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
83
+ wallClockMs: z.ZodDefault<z.ZodNumber>;
84
+ }, "strip", z.ZodTypeAny, {
85
+ kind: "mcp-call";
86
+ wallClockMs: number;
87
+ tool: string;
88
+ args: Record<string, unknown>;
89
+ }, {
90
+ kind: "mcp-call";
91
+ tool: string;
92
+ wallClockMs?: number | undefined;
93
+ args?: Record<string, unknown> | undefined;
94
+ }>]>;
95
+ notify: z.ZodDefault<z.ZodArray<z.ZodObject<{
96
+ kind: z.ZodEnum<["stdout", "webhook", "slack", "email"]>;
97
+ on: z.ZodDefault<z.ZodEnum<["always", "failure", "success"]>>;
98
+ config: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
99
+ }, "strip", z.ZodTypeAny, {
100
+ config: Record<string, unknown>;
101
+ kind: "email" | "stdout" | "webhook" | "slack";
102
+ on: "always" | "failure" | "success";
103
+ }, {
104
+ kind: "email" | "stdout" | "webhook" | "slack";
105
+ config?: Record<string, unknown> | undefined;
106
+ on?: "always" | "failure" | "success" | undefined;
107
+ }>, "many">>;
108
+ tags: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
109
+ createdAt: z.ZodOptional<z.ZodString>;
110
+ updatedAt: z.ZodOptional<z.ZodString>;
111
+ }, "strip", z.ZodTypeAny, {
112
+ enabled: boolean;
113
+ name: string;
114
+ id: string;
115
+ notify: {
116
+ config: Record<string, unknown>;
117
+ kind: "email" | "stdout" | "webhook" | "slack";
118
+ on: "always" | "failure" | "success";
119
+ }[];
120
+ description: string;
121
+ schedule: {
122
+ kind: "manual";
123
+ } | {
124
+ kind: "calendar";
125
+ onCalendar: string;
126
+ randomizedDelaySec: number;
127
+ persistent: boolean;
128
+ };
129
+ targets: string[];
130
+ perTarget: boolean;
131
+ task: {
132
+ kind: "claude-cli";
133
+ prompt: string;
134
+ outputFormat: "json";
135
+ tokenCap: number;
136
+ wallClockMs: number;
137
+ maxUsd: number;
138
+ model?: string | undefined;
139
+ appendSystem?: string | undefined;
140
+ allowedTools?: string[] | undefined;
141
+ } | {
142
+ kind: "shell";
143
+ wallClockMs: number;
144
+ argv: string[];
145
+ env?: Record<string, string> | undefined;
146
+ } | {
147
+ kind: "mcp-call";
148
+ wallClockMs: number;
149
+ tool: string;
150
+ args: Record<string, unknown>;
151
+ };
152
+ tags: string[];
153
+ updatedAt?: string | undefined;
154
+ createdAt?: string | undefined;
155
+ }, {
156
+ name: string;
157
+ id: string;
158
+ schedule: {
159
+ kind: "manual";
160
+ } | {
161
+ kind: "calendar";
162
+ onCalendar: string;
163
+ randomizedDelaySec?: number | undefined;
164
+ persistent?: boolean | undefined;
165
+ };
166
+ task: {
167
+ kind: "claude-cli";
168
+ prompt: string;
169
+ outputFormat?: "json" | undefined;
170
+ tokenCap?: number | undefined;
171
+ wallClockMs?: number | undefined;
172
+ maxUsd?: number | undefined;
173
+ model?: string | undefined;
174
+ appendSystem?: string | undefined;
175
+ allowedTools?: string[] | undefined;
176
+ } | {
177
+ kind: "shell";
178
+ argv: string[];
179
+ env?: Record<string, string> | undefined;
180
+ wallClockMs?: number | undefined;
181
+ } | {
182
+ kind: "mcp-call";
183
+ tool: string;
184
+ wallClockMs?: number | undefined;
185
+ args?: Record<string, unknown> | undefined;
186
+ };
187
+ enabled?: boolean | undefined;
188
+ updatedAt?: string | undefined;
189
+ notify?: {
190
+ kind: "email" | "stdout" | "webhook" | "slack";
191
+ config?: Record<string, unknown> | undefined;
192
+ on?: "always" | "failure" | "success" | undefined;
193
+ }[] | undefined;
194
+ description?: string | undefined;
195
+ targets?: string[] | undefined;
196
+ perTarget?: boolean | undefined;
197
+ tags?: string[] | undefined;
198
+ createdAt?: string | undefined;
199
+ }>, "many">;
200
+ defaultsSeededAt: z.ZodOptional<z.ZodString>;
201
+ }, "strip", z.ZodTypeAny, {
202
+ version: 1;
203
+ routines: {
204
+ enabled: boolean;
205
+ name: string;
206
+ id: string;
207
+ notify: {
208
+ config: Record<string, unknown>;
209
+ kind: "email" | "stdout" | "webhook" | "slack";
210
+ on: "always" | "failure" | "success";
211
+ }[];
212
+ description: string;
213
+ schedule: {
214
+ kind: "manual";
215
+ } | {
216
+ kind: "calendar";
217
+ onCalendar: string;
218
+ randomizedDelaySec: number;
219
+ persistent: boolean;
220
+ };
221
+ targets: string[];
222
+ perTarget: boolean;
223
+ task: {
224
+ kind: "claude-cli";
225
+ prompt: string;
226
+ outputFormat: "json";
227
+ tokenCap: number;
228
+ wallClockMs: number;
229
+ maxUsd: number;
230
+ model?: string | undefined;
231
+ appendSystem?: string | undefined;
232
+ allowedTools?: string[] | undefined;
233
+ } | {
234
+ kind: "shell";
235
+ wallClockMs: number;
236
+ argv: string[];
237
+ env?: Record<string, string> | undefined;
238
+ } | {
239
+ kind: "mcp-call";
240
+ wallClockMs: number;
241
+ tool: string;
242
+ args: Record<string, unknown>;
243
+ };
244
+ tags: string[];
245
+ updatedAt?: string | undefined;
246
+ createdAt?: string | undefined;
247
+ }[];
248
+ defaultsSeededAt?: string | undefined;
249
+ }, {
250
+ version: 1;
251
+ routines: {
252
+ name: string;
253
+ id: string;
254
+ schedule: {
255
+ kind: "manual";
256
+ } | {
257
+ kind: "calendar";
258
+ onCalendar: string;
259
+ randomizedDelaySec?: number | undefined;
260
+ persistent?: boolean | undefined;
261
+ };
262
+ task: {
263
+ kind: "claude-cli";
264
+ prompt: string;
265
+ outputFormat?: "json" | undefined;
266
+ tokenCap?: number | undefined;
267
+ wallClockMs?: number | undefined;
268
+ maxUsd?: number | undefined;
269
+ model?: string | undefined;
270
+ appendSystem?: string | undefined;
271
+ allowedTools?: string[] | undefined;
272
+ } | {
273
+ kind: "shell";
274
+ argv: string[];
275
+ env?: Record<string, string> | undefined;
276
+ wallClockMs?: number | undefined;
277
+ } | {
278
+ kind: "mcp-call";
279
+ tool: string;
280
+ wallClockMs?: number | undefined;
281
+ args?: Record<string, unknown> | undefined;
282
+ };
283
+ enabled?: boolean | undefined;
284
+ updatedAt?: string | undefined;
285
+ notify?: {
286
+ kind: "email" | "stdout" | "webhook" | "slack";
287
+ config?: Record<string, unknown> | undefined;
288
+ on?: "always" | "failure" | "success" | undefined;
289
+ }[] | undefined;
290
+ description?: string | undefined;
291
+ targets?: string[] | undefined;
292
+ perTarget?: boolean | undefined;
293
+ tags?: string[] | undefined;
294
+ createdAt?: string | undefined;
295
+ }[];
296
+ defaultsSeededAt?: string | undefined;
297
+ }>;
298
+ export type RoutineStoreFile = z.infer<typeof FileSchema>;
299
+ export declare class RoutineStore {
300
+ private readonly path;
301
+ private file;
302
+ constructor(path?: string);
303
+ private readFromDisk;
304
+ private writeAtomic;
305
+ list(): Routine[];
306
+ get(id: string): Routine | null;
307
+ upsert(routine: Routine): Routine;
308
+ remove(id: string): boolean;
309
+ seedDefaults(routines: Routine[]): {
310
+ seeded: number;
311
+ skipped: number;
312
+ };
313
+ reload(): void;
314
+ storePath(): string;
315
+ }
316
+ export {};
@@ -0,0 +1,99 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { z } from 'zod';
5
+ import { RoutineSchema } from './schema.js';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const DEFAULT_STORE_PATH = join(__dirname, '..', '..', '..', 'data', 'routines.json');
8
+ const FileSchema = z.object({
9
+ version: z.literal(1),
10
+ routines: z.array(RoutineSchema),
11
+ defaultsSeededAt: z.string().datetime().optional(),
12
+ });
13
+ function defaultFile() {
14
+ return { version: 1, routines: [] };
15
+ }
16
+ function ensureDirFor(path) {
17
+ const dir = dirname(path);
18
+ if (!existsSync(dir))
19
+ mkdirSync(dir, { recursive: true });
20
+ }
21
+ export class RoutineStore {
22
+ path;
23
+ file;
24
+ constructor(path = DEFAULT_STORE_PATH) {
25
+ this.path = path;
26
+ this.file = this.readFromDisk();
27
+ }
28
+ readFromDisk() {
29
+ if (!existsSync(this.path))
30
+ return defaultFile();
31
+ const raw = readFileSync(this.path, 'utf-8');
32
+ try {
33
+ return FileSchema.parse(JSON.parse(raw));
34
+ }
35
+ catch (err) {
36
+ process.stderr.write(`[routines] Warning: failed to parse ${this.path}: ${String(err)}\n`);
37
+ return defaultFile();
38
+ }
39
+ }
40
+ writeAtomic() {
41
+ ensureDirFor(this.path);
42
+ const tmp = `${this.path}.tmp.${process.pid}`;
43
+ writeFileSync(tmp, JSON.stringify(this.file, null, 2) + '\n', { mode: 0o600 });
44
+ renameSync(tmp, this.path);
45
+ }
46
+ list() {
47
+ return [...this.file.routines];
48
+ }
49
+ get(id) {
50
+ return this.file.routines.find(r => r.id === id) ?? null;
51
+ }
52
+ upsert(routine) {
53
+ const validated = RoutineSchema.parse(routine);
54
+ const now = new Date().toISOString();
55
+ const idx = this.file.routines.findIndex(r => r.id === validated.id);
56
+ const existing = idx >= 0 ? this.file.routines[idx] : null;
57
+ const prepared = {
58
+ ...validated,
59
+ createdAt: existing?.createdAt ?? validated.createdAt ?? now,
60
+ updatedAt: now,
61
+ };
62
+ if (idx >= 0) {
63
+ this.file.routines[idx] = prepared;
64
+ }
65
+ else {
66
+ this.file.routines.push(prepared);
67
+ }
68
+ this.writeAtomic();
69
+ return prepared;
70
+ }
71
+ remove(id) {
72
+ const before = this.file.routines.length;
73
+ this.file.routines = this.file.routines.filter(r => r.id !== id);
74
+ const removed = this.file.routines.length !== before;
75
+ if (removed)
76
+ this.writeAtomic();
77
+ return removed;
78
+ }
79
+ seedDefaults(routines) {
80
+ if (this.file.defaultsSeededAt)
81
+ return { seeded: 0, skipped: routines.length };
82
+ let seeded = 0;
83
+ for (const r of routines) {
84
+ if (!this.file.routines.some(existing => existing.id === r.id)) {
85
+ this.file.routines.push(RoutineSchema.parse(r));
86
+ seeded++;
87
+ }
88
+ }
89
+ this.file.defaultsSeededAt = new Date().toISOString();
90
+ this.writeAtomic();
91
+ return { seeded, skipped: routines.length - seeded };
92
+ }
93
+ reload() {
94
+ this.file = this.readFromDisk();
95
+ }
96
+ storePath() {
97
+ return this.path;
98
+ }
99
+ }
@@ -0,0 +1,2 @@
1
+ export declare function mkExecTmpDir(prefix: string): string;
2
+ export declare function rmExecTmpDir(dir: string): void;
@@ -0,0 +1,13 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const EXEC_TMP_ROOT = join(__dirname, '..', '..', '..', '.test-tmp');
6
+ export function mkExecTmpDir(prefix) {
7
+ if (!existsSync(EXEC_TMP_ROOT))
8
+ mkdirSync(EXEC_TMP_ROOT, { recursive: true });
9
+ return mkdtempSync(join(EXEC_TMP_ROOT, prefix));
10
+ }
11
+ export function rmExecTmpDir(dir) {
12
+ rmSync(dir, { recursive: true, force: true });
13
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Append-only audit log of every sensitive secret operation.
3
+ *
4
+ * Stored at ~/.local/share/fleet/audit.jsonl with mode 0600. Each line is a
5
+ * JSON object — never the secret VALUE, only its name. Flushed synchronously
6
+ * so a crash mid-rotation still leaves a trail.
7
+ */
8
+ export type AuditOp = 'init' | 'seal' | 'unseal' | 'set' | 'get' | 'rotate' | 'rotate-attempted' | 'rotate-failed' | 'rollback' | 'snapshot' | 'harden' | 'export' | 'import';
9
+ export interface AuditEntry {
10
+ ts: string;
11
+ op: AuditOp;
12
+ actor: string;
13
+ app?: string;
14
+ secret?: string;
15
+ ok: boolean;
16
+ details?: string;
17
+ }
18
+ export declare function auditLog(entry: Omit<AuditEntry, 'ts' | 'actor'> & {
19
+ actor?: string;
20
+ }): void;
21
+ export declare function getAuditPath(): string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Append-only audit log of every sensitive secret operation.
3
+ *
4
+ * Stored at ~/.local/share/fleet/audit.jsonl with mode 0600. Each line is a
5
+ * JSON object — never the secret VALUE, only its name. Flushed synchronously
6
+ * so a crash mid-rotation still leaves a trail.
7
+ */
8
+ import { existsSync, mkdirSync, appendFileSync, chmodSync, statSync, openSync, closeSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { homedir } from 'node:os';
11
+ const AUDIT_DIR = join(homedir(), '.local', 'share', 'fleet');
12
+ const AUDIT_PATH = join(AUDIT_DIR, 'audit.jsonl');
13
+ function getActor() {
14
+ return process.env.SUDO_USER || process.env.USER || process.env.LOGNAME || 'unknown';
15
+ }
16
+ function ensureLog() {
17
+ if (!existsSync(AUDIT_DIR)) {
18
+ mkdirSync(AUDIT_DIR, { recursive: true, mode: 0o700 });
19
+ }
20
+ if (!existsSync(AUDIT_PATH)) {
21
+ // Atomic create with the desired mode in a single syscall — closes the
22
+ // TOCTOU window where the file briefly existed at the umask default
23
+ // (typically 0o644) before the chmod.
24
+ const fd = openSync(AUDIT_PATH, 'a', 0o600);
25
+ closeSync(fd);
26
+ return;
27
+ }
28
+ try {
29
+ const mode = statSync(AUDIT_PATH).mode & 0o777;
30
+ if (mode !== 0o600)
31
+ chmodSync(AUDIT_PATH, 0o600);
32
+ }
33
+ catch {
34
+ /* ignore */
35
+ }
36
+ }
37
+ export function auditLog(entry) {
38
+ // Auditing must never block the actual operation. If the FS isn't writable
39
+ // (read-only mount, missing dir, mocked-out tests, etc.), surface a single
40
+ // stderr warning and continue. The op succeeds; we just lose this audit line.
41
+ try {
42
+ ensureLog();
43
+ const line = {
44
+ ts: new Date().toISOString(),
45
+ actor: entry.actor ?? getActor(),
46
+ op: entry.op,
47
+ app: entry.app,
48
+ secret: entry.secret,
49
+ ok: entry.ok,
50
+ details: entry.details,
51
+ };
52
+ appendFileSync(AUDIT_PATH, JSON.stringify(line) + '\n');
53
+ }
54
+ catch (err) {
55
+ process.stderr.write(`[fleet audit] WARNING: failed to write audit entry (${err instanceof Error ? err.message : err})\n`);
56
+ }
57
+ }
58
+ export function getAuditPath() {
59
+ return AUDIT_PATH;
60
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Per-secret metadata: read/write helpers around the optional `secrets` map
3
+ * on each ManifestEntry. Backwards-compatible: missing entries default to
4
+ * `lastRotated = entry.lastSealedAt`, provider derived via classifySecret.
5
+ */
6
+ import { type SecretMetadata } from './secrets.js';
7
+ import { type ProviderDef } from './secrets-providers.js';
8
+ export interface EnrichedSecret {
9
+ /** The env var (or filename for secrets-dir apps). */
10
+ name: string;
11
+ /** Masked value preview, e.g. "sk_***" — never the real value. */
12
+ maskedValue: string;
13
+ /** ISO timestamp of last rotation, or last seal if never rotated individually. */
14
+ lastRotated: string;
15
+ /** Days since lastRotated. Null if timestamps invalid. */
16
+ ageDays: number | null;
17
+ /** Resolved provider definition. May be null if unrecognised. */
18
+ provider: ProviderDef | null;
19
+ /** True if older than provider's rotationFrequencyDays. */
20
+ stale: boolean;
21
+ }
22
+ /** Get raw metadata for a single secret. Returns null if no per-secret entry exists. */
23
+ export declare function getSecretMetadata(app: string, secretName: string): SecretMetadata | null;
24
+ /** Persist metadata for a single secret. Creates the secrets map if missing. */
25
+ export declare function setSecretMetadata(app: string, secretName: string, meta: SecretMetadata): void;
26
+ /** Mark a secret as freshly rotated. Auto-classifies provider if not specified. */
27
+ export declare function markRotated(app: string, secretName: string, opts?: {
28
+ strategy?: SecretMetadata['strategy'];
29
+ notes?: string;
30
+ }): SecretMetadata;
31
+ /**
32
+ * List all secrets in an app, enriched with provider metadata + age + staleness.
33
+ * Backwards-compatible: secrets without per-secret metadata fall back to lastSealedAt.
34
+ */
35
+ export declare function enumerateSecrets(app: string): EnrichedSecret[];
36
+ /** All apps × all secrets, enriched. Used by `ages` (no app), MOTD, and bulk reports. */
37
+ export declare function enumerateAllSecrets(): Array<EnrichedSecret & {
38
+ app: string;
39
+ }>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Per-secret metadata: read/write helpers around the optional `secrets` map
3
+ * on each ManifestEntry. Backwards-compatible: missing entries default to
4
+ * `lastRotated = entry.lastSealedAt`, provider derived via classifySecret.
5
+ */
6
+ import { loadManifest, saveManifest, listSecrets } from './secrets.js';
7
+ import { classifySecret, getProviderById, ageInDays, isStale, } from './secrets-providers.js';
8
+ import { SecretsError } from './errors.js';
9
+ /** Get raw metadata for a single secret. Returns null if no per-secret entry exists. */
10
+ export function getSecretMetadata(app, secretName) {
11
+ const manifest = loadManifest();
12
+ const entry = manifest.apps[app];
13
+ if (!entry || !entry.secrets)
14
+ return null;
15
+ return entry.secrets[secretName] ?? null;
16
+ }
17
+ /** Persist metadata for a single secret. Creates the secrets map if missing. */
18
+ export function setSecretMetadata(app, secretName, meta) {
19
+ const manifest = loadManifest();
20
+ const entry = manifest.apps[app];
21
+ if (!entry)
22
+ throw new SecretsError(`No app in manifest: ${app}`);
23
+ entry.secrets = entry.secrets ?? {};
24
+ entry.secrets[secretName] = meta;
25
+ saveManifest(manifest);
26
+ }
27
+ /** Mark a secret as freshly rotated. Auto-classifies provider if not specified. */
28
+ export function markRotated(app, secretName, opts = {}) {
29
+ const provider = classifySecret(secretName);
30
+ const meta = {
31
+ lastRotated: new Date().toISOString(),
32
+ provider: provider?.id,
33
+ strategy: opts.strategy ?? provider?.strategy,
34
+ notes: opts.notes,
35
+ };
36
+ setSecretMetadata(app, secretName, meta);
37
+ return meta;
38
+ }
39
+ /**
40
+ * List all secrets in an app, enriched with provider metadata + age + staleness.
41
+ * Backwards-compatible: secrets without per-secret metadata fall back to lastSealedAt.
42
+ */
43
+ export function enumerateSecrets(app) {
44
+ const manifest = loadManifest();
45
+ const entry = manifest.apps[app];
46
+ if (!entry)
47
+ throw new SecretsError(`No app in manifest: ${app}`);
48
+ const items = listSecrets(app); // already returns { key, maskedValue }
49
+ const fallbackTs = entry.lastSealedAt;
50
+ return items.map(({ key, maskedValue }) => {
51
+ const stored = entry.secrets?.[key];
52
+ const provider = (stored?.provider ? getProviderById(stored.provider) : null) ?? classifySecret(key);
53
+ const lastRotated = stored?.lastRotated ?? fallbackTs;
54
+ const ageDays = ageInDays(lastRotated);
55
+ return {
56
+ name: key,
57
+ maskedValue,
58
+ lastRotated,
59
+ ageDays,
60
+ provider,
61
+ stale: isStale(ageDays, provider),
62
+ };
63
+ });
64
+ }
65
+ /** All apps × all secrets, enriched. Used by `ages` (no app), MOTD, and bulk reports. */
66
+ export function enumerateAllSecrets() {
67
+ const manifest = loadManifest();
68
+ const out = [];
69
+ for (const app of Object.keys(manifest.apps)) {
70
+ try {
71
+ for (const s of enumerateSecrets(app))
72
+ out.push({ app, ...s });
73
+ }
74
+ catch (err) {
75
+ // App may be sealed-but-unreadable (e.g. key missing). Continue but
76
+ // log so a corrupt manifest entry doesn't masquerade as "no secrets".
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ process.stderr.write(`[fleet secrets] skipped ${app}: ${msg}\n`);
79
+ }
80
+ }
81
+ return out;
82
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * MOTD reporter: short summary of secret rotation health, intended for
3
+ * /etc/update-motd.d/99-fleet-secrets to print on shell login.
4
+ */
5
+ import type { Sensitivity } from './secrets-providers.js';
6
+ export interface SecretsMotdSummary {
7
+ totalSecrets: number;
8
+ staleCount: number;
9
+ bySensitivity: Record<Sensitivity, number>;
10
+ appsWithStale: string[];
11
+ topStale: Array<{
12
+ app: string;
13
+ name: string;
14
+ ageDays: number;
15
+ sensitivity: Sensitivity;
16
+ }>;
17
+ }
18
+ export declare function summariseSecrets(): SecretsMotdSummary;
19
+ export declare function formatSecretsMotd(summary: SecretsMotdSummary): string;
20
+ export declare function generateSecretsMotdScript(): string;