@matthesketh/fleet 1.8.0 → 1.11.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 (233) 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/notify.d.ts +1 -0
  37. package/dist/commands/notify.js +51 -0
  38. package/dist/commands/patch-systemd.d.ts +7 -1
  39. package/dist/commands/patch-systemd.js +71 -31
  40. package/dist/commands/remove.d.ts +3 -1
  41. package/dist/commands/remove.js +37 -26
  42. package/dist/commands/restart.d.ts +4 -1
  43. package/dist/commands/restart.js +17 -20
  44. package/dist/commands/rollback.d.ts +4 -1
  45. package/dist/commands/rollback.js +33 -42
  46. package/dist/commands/secrets.js +157 -9
  47. package/dist/commands/start.d.ts +4 -1
  48. package/dist/commands/start.js +17 -20
  49. package/dist/commands/status.d.ts +1 -1
  50. package/dist/commands/status.js +21 -26
  51. package/dist/commands/stop.d.ts +4 -1
  52. package/dist/commands/stop.js +17 -20
  53. package/dist/commands/testflight.d.ts +1 -0
  54. package/dist/commands/testflight.js +193 -0
  55. package/dist/commands/update.d.ts +16 -0
  56. package/dist/commands/update.js +95 -0
  57. package/dist/core/audit/cache.d.ts +4 -0
  58. package/dist/core/audit/cache.js +37 -0
  59. package/dist/core/audit/config.d.ts +5 -0
  60. package/dist/core/audit/config.js +35 -0
  61. package/dist/core/audit/greenlight.d.ts +11 -0
  62. package/dist/core/audit/greenlight.js +81 -0
  63. package/dist/core/audit/reporters/cli.d.ts +3 -0
  64. package/dist/core/audit/reporters/cli.js +68 -0
  65. package/dist/core/audit/suppress.d.ts +6 -0
  66. package/dist/core/audit/suppress.js +37 -0
  67. package/dist/core/audit/target.d.ts +5 -0
  68. package/dist/core/audit/target.js +26 -0
  69. package/dist/core/audit/types.d.ts +54 -0
  70. package/dist/core/audit/types.js +5 -0
  71. package/dist/core/backup/browser-api.d.ts +66 -0
  72. package/dist/core/backup/browser-api.js +197 -0
  73. package/dist/core/backup/browser-server.d.ts +11 -0
  74. package/dist/core/backup/browser-server.js +241 -0
  75. package/dist/core/backup/browser-ui.d.ts +5 -0
  76. package/dist/core/backup/browser-ui.js +268 -0
  77. package/dist/core/backup/cloudflare.d.ts +7 -0
  78. package/dist/core/backup/cloudflare.js +82 -0
  79. package/dist/core/backup/config.d.ts +9 -0
  80. package/dist/core/backup/config.js +80 -0
  81. package/dist/core/backup/detect.d.ts +11 -0
  82. package/dist/core/backup/detect.js +71 -0
  83. package/dist/core/backup/dump.d.ts +11 -0
  84. package/dist/core/backup/dump.js +82 -0
  85. package/dist/core/backup/index.d.ts +9 -0
  86. package/dist/core/backup/index.js +9 -0
  87. package/dist/core/backup/repo.d.ts +71 -0
  88. package/dist/core/backup/repo.js +256 -0
  89. package/dist/core/backup/schedule.d.ts +17 -0
  90. package/dist/core/backup/schedule.js +90 -0
  91. package/dist/core/backup/sensitive.d.ts +5 -0
  92. package/dist/core/backup/sensitive.js +37 -0
  93. package/dist/core/backup/status.d.ts +3 -0
  94. package/dist/core/backup/status.js +29 -0
  95. package/dist/core/backup/statuspage.d.ts +23 -0
  96. package/dist/core/backup/statuspage.js +145 -0
  97. package/dist/core/backup/system.d.ts +24 -0
  98. package/dist/core/backup/system.js +209 -0
  99. package/dist/core/backup/totp.d.ts +16 -0
  100. package/dist/core/backup/totp.js +116 -0
  101. package/dist/core/backup/types.d.ts +70 -0
  102. package/dist/core/backup/types.js +7 -0
  103. package/dist/core/backup/unlock.d.ts +19 -0
  104. package/dist/core/backup/unlock.js +69 -0
  105. package/dist/core/boot-refresh.d.ts +1 -1
  106. package/dist/core/boot-refresh.js +10 -9
  107. package/dist/core/deps/actors/pr-creator.d.ts +5 -3
  108. package/dist/core/deps/actors/pr-creator.js +71 -18
  109. package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
  110. package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
  111. package/dist/core/deps/collectors/npm.js +3 -1
  112. package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
  113. package/dist/core/deps/collectors/vulnerability.js +31 -2
  114. package/dist/core/deps/config.js +6 -0
  115. package/dist/core/deps/scanner.js +1 -1
  116. package/dist/core/deps/types.d.ts +8 -0
  117. package/dist/core/env.d.ts +3 -0
  118. package/dist/core/env.js +11 -0
  119. package/dist/core/exec.d.ts +1 -0
  120. package/dist/core/exec.js +4 -0
  121. package/dist/core/file-lock.d.ts +18 -0
  122. package/dist/core/file-lock.js +44 -0
  123. package/dist/core/git-onboard.js +10 -13
  124. package/dist/core/github.d.ts +3 -1
  125. package/dist/core/github.js +10 -7
  126. package/dist/core/logs-policy.d.ts +5 -0
  127. package/dist/core/logs-policy.js +20 -1
  128. package/dist/core/operator.d.ts +21 -0
  129. package/dist/core/operator.js +54 -0
  130. package/dist/core/registry.d.ts +18 -0
  131. package/dist/core/registry.js +26 -0
  132. package/dist/core/routines/schema.d.ts +11 -11
  133. package/dist/core/routines/schema.js +14 -3
  134. package/dist/core/routines/store.d.ts +8 -8
  135. package/dist/core/secrets-ops.d.ts +31 -6
  136. package/dist/core/secrets-ops.js +208 -102
  137. package/dist/core/secrets-providers.js +2 -2
  138. package/dist/core/secrets-rotation.d.ts +1 -1
  139. package/dist/core/secrets-rotation.js +58 -52
  140. package/dist/core/secrets-v2-cleanup.d.ts +19 -0
  141. package/dist/core/secrets-v2-cleanup.js +94 -0
  142. package/dist/core/secrets-v2-creds.d.ts +9 -0
  143. package/dist/core/secrets-v2-creds.js +44 -0
  144. package/dist/core/secrets-v2-install.d.ts +13 -0
  145. package/dist/core/secrets-v2-install.js +76 -0
  146. package/dist/core/secrets-v2-keypair.d.ts +10 -0
  147. package/dist/core/secrets-v2-keypair.js +31 -0
  148. package/dist/core/secrets-v2-migrate.d.ts +29 -0
  149. package/dist/core/secrets-v2-migrate.js +395 -0
  150. package/dist/core/secrets-v2-ops.d.ts +36 -0
  151. package/dist/core/secrets-v2-ops.js +184 -0
  152. package/dist/core/secrets-v2-protocol.d.ts +19 -0
  153. package/dist/core/secrets-v2-protocol.js +60 -0
  154. package/dist/core/secrets-v2-snapshot.d.ts +36 -0
  155. package/dist/core/secrets-v2-snapshot.js +115 -0
  156. package/dist/core/secrets-v2.d.ts +21 -0
  157. package/dist/core/secrets-v2.js +249 -0
  158. package/dist/core/secrets.d.ts +39 -4
  159. package/dist/core/secrets.js +91 -11
  160. package/dist/core/self-update.d.ts +32 -11
  161. package/dist/core/self-update.js +52 -14
  162. package/dist/core/testflight/asc.d.ts +12 -0
  163. package/dist/core/testflight/asc.js +101 -0
  164. package/dist/core/testflight/credentials.d.ts +3 -0
  165. package/dist/core/testflight/credentials.js +35 -0
  166. package/dist/core/testflight/eas.d.ts +4 -0
  167. package/dist/core/testflight/eas.js +38 -0
  168. package/dist/core/testflight/resolve.d.ts +6 -0
  169. package/dist/core/testflight/resolve.js +44 -0
  170. package/dist/core/testflight/types.d.ts +13 -0
  171. package/dist/core/testflight/types.js +3 -0
  172. package/dist/core/testflight/workflow.d.ts +17 -0
  173. package/dist/core/testflight/workflow.js +65 -0
  174. package/dist/core/validate.d.ts +1 -0
  175. package/dist/core/validate.js +8 -0
  176. package/dist/mcp/audit-tools.d.ts +2 -0
  177. package/dist/mcp/audit-tools.js +94 -0
  178. package/dist/mcp/git-tools.js +1 -1
  179. package/dist/mcp/registry-bridge.d.ts +10 -0
  180. package/dist/mcp/registry-bridge.js +65 -0
  181. package/dist/mcp/secrets-tools.js +2 -2
  182. package/dist/mcp/server.js +16 -82
  183. package/dist/mcp/testflight-tools.d.ts +2 -0
  184. package/dist/mcp/testflight-tools.js +52 -0
  185. package/dist/registry/context.d.ts +7 -0
  186. package/dist/registry/context.js +37 -0
  187. package/dist/registry/index.d.ts +5 -0
  188. package/dist/registry/index.js +44 -0
  189. package/dist/registry/parse-args.d.ts +13 -0
  190. package/dist/registry/parse-args.js +74 -0
  191. package/dist/registry/registry.d.ts +24 -0
  192. package/dist/registry/registry.js +26 -0
  193. package/dist/registry/render.d.ts +3 -0
  194. package/dist/registry/render.js +29 -0
  195. package/dist/registry/types.d.ts +50 -0
  196. package/dist/registry/types.js +1 -0
  197. package/dist/templates/agent-unit.d.ts +5 -0
  198. package/dist/templates/agent-unit.js +40 -0
  199. package/dist/templates/app-unit-edit.d.ts +2 -0
  200. package/dist/templates/app-unit-edit.js +46 -0
  201. package/dist/templates/compose-edit.d.ts +2 -0
  202. package/dist/templates/compose-edit.js +156 -0
  203. package/dist/templates/nginx.js +11 -0
  204. package/dist/templates/systemd.js +6 -0
  205. package/dist/tui/components/ArgForm.d.ts +7 -0
  206. package/dist/tui/components/ArgForm.js +64 -0
  207. package/dist/tui/components/ArgForm.test.d.ts +1 -0
  208. package/dist/tui/components/ArgForm.test.js +19 -0
  209. package/dist/tui/components/KeyHint.js +5 -0
  210. package/dist/tui/hooks/use-secrets.d.ts +8 -8
  211. package/dist/tui/hooks/use-secrets.js +7 -7
  212. package/dist/tui/router.d.ts +1 -0
  213. package/dist/tui/router.js +26 -9
  214. package/dist/tui/router.test.d.ts +1 -0
  215. package/dist/tui/router.test.js +13 -0
  216. package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
  217. package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
  218. package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
  219. package/dist/tui/tests/redaction-rerender.test.js +53 -0
  220. package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
  221. package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
  222. package/dist/tui/types.d.ts +1 -1
  223. package/dist/tui/views/CommandPalette.d.ts +5 -0
  224. package/dist/tui/views/CommandPalette.js +90 -0
  225. package/dist/tui/views/CommandPalette.test.d.ts +1 -0
  226. package/dist/tui/views/CommandPalette.test.js +117 -0
  227. package/dist/tui/views/Dashboard.js +10 -7
  228. package/dist/tui/views/HealthView.js +14 -5
  229. package/dist/tui/views/SecretEdit.js +15 -16
  230. package/dist/tui/views/SecretEdit.test.d.ts +1 -0
  231. package/dist/tui/views/SecretEdit.test.js +82 -0
  232. package/dist/tui/views/SecretsView.js +26 -16
  233. package/package.json +9 -6
@@ -0,0 +1,51 @@
1
+ import { loadNotifyConfig, sendNotification } from '../core/notify.js';
2
+ import { success, error, warn } from '../ui/output.js';
3
+ const HELP = `fleet notify - send a message via configured notify adapters
4
+
5
+ Usage:
6
+ fleet notify <message>
7
+ fleet notify - # read message from stdin
8
+ echo "msg" | fleet notify -
9
+
10
+ Reads adapter config from /etc/fleet/notify.json (telegram / bluebubbles).
11
+ Exits 0 if at least one adapter delivered the message, 1 otherwise.
12
+ `;
13
+ async function readStdin() {
14
+ const chunks = [];
15
+ for await (const chunk of process.stdin) {
16
+ chunks.push(chunk);
17
+ }
18
+ return Buffer.concat(chunks).toString('utf-8').trim();
19
+ }
20
+ export async function notifyCommand(args) {
21
+ if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
22
+ process.stdout.write(HELP);
23
+ if (args.length === 0)
24
+ process.exit(1);
25
+ return;
26
+ }
27
+ let message;
28
+ if (args[0] === '-') {
29
+ message = await readStdin();
30
+ }
31
+ else {
32
+ message = args.join(' ');
33
+ }
34
+ if (!message) {
35
+ error('Empty message');
36
+ process.exit(1);
37
+ }
38
+ const config = loadNotifyConfig();
39
+ if (!config) {
40
+ warn('No notify config at /etc/fleet/notify.json — message not sent');
41
+ process.exit(1);
42
+ }
43
+ const sent = await sendNotification(config, message);
44
+ if (sent) {
45
+ success('Notification sent');
46
+ }
47
+ else {
48
+ error('Failed to send notification (no adapter succeeded)');
49
+ process.exit(1);
50
+ }
51
+ }
@@ -1 +1,7 @@
1
- export declare function patchSystemdCommand(args: string[]): void;
1
+ interface PatchSystemdData {
2
+ action: 'patch' | 'rollback';
3
+ changed: number;
4
+ skipped: number;
5
+ }
6
+ export declare const patchSystemdCommand: import("../registry/types.js").CommandDef<PatchSystemdData>;
7
+ export {};
@@ -1,12 +1,11 @@
1
1
  import { copyFileSync, existsSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { z } from 'zod';
2
3
  import { load } from '../core/registry.js';
3
4
  import { readServiceFile } from '../core/systemd.js';
4
5
  import { execSafe } from '../core/exec.js';
5
- import { success, warn, info, error } from '../ui/output.js';
6
+ import { defineCommand } from '../registry/registry.js';
6
7
  const SERVICE_DIR = '/etc/systemd/system';
7
- export function patchSystemdCommand(args) {
8
- if (args.includes('--rollback'))
9
- return rollback();
8
+ function runPatch(ctx) {
10
9
  const reg = load();
11
10
  const dbServiceName = reg.infrastructure.databases.serviceName;
12
11
  const appServiceNames = reg.apps.map(a => a.serviceName);
@@ -20,20 +19,20 @@ export function patchSystemdCommand(args) {
20
19
  }
21
20
  targetMap.set(dbServiceName, { name: dbServiceName, rewriteExecStart: false });
22
21
  const targets = Array.from(targetMap.values());
23
- info(`Patching ${targets.length} service(s)...`);
22
+ ctx.log({ level: 'info', message: `patching ${targets.length} service(s)...` });
24
23
  let patched = 0;
25
24
  let skipped = 0;
26
25
  for (const { name, rewriteExecStart } of targets) {
27
26
  const path = `${SERVICE_DIR}/${name}.service`;
28
27
  const content = readServiceFile(name);
29
28
  if (content === null) {
30
- warn(`${name}: no service file found, skipping`);
29
+ ctx.log({ level: 'warn', message: `${name}: no service file found, skipping` });
31
30
  skipped++;
32
31
  continue;
33
32
  }
34
33
  let updated = content;
35
34
  let changed = false;
36
- // Existing behavior: add StartLimitBurst if missing (applies to ALL services including databases)
35
+ // existing behaviour: add StartLimitBurst if missing (applies to ALL services including databases)
37
36
  if (!updated.includes('StartLimitBurst=')) {
38
37
  updated = updated.replace(/(\[Service\])/, '$1\nStartLimitBurst=5\nStartLimitIntervalSec=300');
39
38
  changed = true;
@@ -45,7 +44,7 @@ export function patchSystemdCommand(args) {
45
44
  updated = updated.replace(/^ExecStart=.*$/m, expectedExecStart);
46
45
  changed = true;
47
46
  }
48
- // Ensure TimeoutStartSec=900
47
+ // ensure TimeoutStartSec=900
49
48
  if (!updated.includes('TimeoutStartSec=900')) {
50
49
  if (/^TimeoutStartSec=\d+/m.test(updated)) {
51
50
  updated = updated.replace(/^TimeoutStartSec=\d+.*$/m, 'TimeoutStartSec=900');
@@ -57,37 +56,49 @@ export function patchSystemdCommand(args) {
57
56
  }
58
57
  }
59
58
  if (!changed) {
60
- info(`${name}: already patched, skipping`);
59
+ ctx.log({ level: 'info', message: `${name}: already patched, skipping` });
61
60
  skipped++;
62
61
  continue;
63
62
  }
64
- // Backup original before overwrite
63
+ // backup original before overwrite
65
64
  try {
66
65
  copyFileSync(path, `${path}.bak`);
67
66
  }
68
67
  catch (err) {
69
- warn(`${name}: failed to create .bak (${err instanceof Error ? err.message : String(err)}); skipping for safety`);
68
+ ctx.log({
69
+ level: 'warn',
70
+ message: `${name}: failed to create .bak (${err instanceof Error ? err.message : String(err)}); skipping for safety`,
71
+ });
70
72
  skipped++;
71
73
  continue;
72
74
  }
73
75
  writeFileSync(path, updated);
74
- success(`${name}: patched`);
76
+ ctx.log({ level: 'info', message: `${name}: patched` });
75
77
  patched++;
76
78
  }
77
79
  if (patched === 0) {
78
- info('No services needed patching');
79
- return;
80
+ return {
81
+ ok: true,
82
+ summary: 'no services needed patching',
83
+ data: { action: 'patch', changed: 0, skipped },
84
+ };
80
85
  }
81
- info('Running systemctl daemon-reload...');
86
+ ctx.log({ level: 'info', message: 'running systemctl daemon-reload...' });
82
87
  const result = execSafe('systemctl', ['daemon-reload']);
83
- if (result.ok) {
84
- success(`Done — patched ${patched} service(s), skipped ${skipped}`);
85
- }
86
- else {
87
- warn(`daemon-reload failed: ${result.stderr}`);
88
+ if (!result.ok) {
89
+ return {
90
+ ok: false,
91
+ summary: `patched ${patched} service(s) but daemon-reload failed: ${result.stderr}`,
92
+ data: { action: 'patch', changed: patched, skipped },
93
+ };
88
94
  }
95
+ return {
96
+ ok: true,
97
+ summary: `patched ${patched} service(s), skipped ${skipped}`,
98
+ data: { action: 'patch', changed: patched, skipped },
99
+ };
89
100
  }
90
- function rollback() {
101
+ function runRollback(ctx) {
91
102
  const reg = load();
92
103
  const serviceNames = [
93
104
  ...reg.apps.map(a => a.serviceName),
@@ -104,23 +115,52 @@ function rollback() {
104
115
  }
105
116
  try {
106
117
  renameSync(bak, path);
107
- success(`${name}: restored from .bak`);
118
+ ctx.log({ level: 'info', message: `${name}: restored from .bak` });
108
119
  restored++;
109
120
  }
110
121
  catch (err) {
111
- error(`${name}: failed to restore: ${err instanceof Error ? err.message : String(err)}`);
122
+ ctx.log({
123
+ level: 'error',
124
+ message: `${name}: failed to restore: ${err instanceof Error ? err.message : String(err)}`,
125
+ });
112
126
  }
113
127
  }
114
128
  if (restored === 0) {
115
- info('No .bak files found to restore');
116
- return;
129
+ return {
130
+ ok: true,
131
+ summary: 'no .bak files found to restore',
132
+ data: { action: 'rollback', changed: 0, skipped: missing },
133
+ };
117
134
  }
118
- info('Running systemctl daemon-reload...');
135
+ ctx.log({ level: 'info', message: 'running systemctl daemon-reload...' });
119
136
  const result = execSafe('systemctl', ['daemon-reload']);
120
- if (result.ok) {
121
- success(`Done — restored ${restored}, missing ${missing}`);
122
- }
123
- else {
124
- warn(`daemon-reload failed: ${result.stderr}`);
137
+ if (!result.ok) {
138
+ return {
139
+ ok: false,
140
+ summary: `restored ${restored} but daemon-reload failed: ${result.stderr}`,
141
+ data: { action: 'rollback', changed: restored, skipped: missing },
142
+ };
125
143
  }
144
+ return {
145
+ ok: true,
146
+ summary: `restored ${restored}, missing ${missing}`,
147
+ data: { action: 'rollback', changed: restored, skipped: missing },
148
+ };
126
149
  }
150
+ export const patchSystemdCommand = defineCommand({
151
+ name: 'patch-systemd',
152
+ summary: 'Add StartLimit settings to all service files',
153
+ args: z.object({ rollback: z.boolean().default(false), yes: z.boolean().default(false) }),
154
+ destructive: true,
155
+ async run(args, ctx) {
156
+ const verb = args.rollback ? 'roll back' : 'patch';
157
+ if (!args.yes && !(await ctx.confirm(`${verb} all fleet systemd unit files?`))) {
158
+ return {
159
+ ok: false,
160
+ summary: 'cancelled',
161
+ data: { action: args.rollback ? 'rollback' : 'patch', changed: 0, skipped: 0 },
162
+ };
163
+ }
164
+ return args.rollback ? runRollback(ctx) : runPatch(ctx);
165
+ },
166
+ });
@@ -1 +1,3 @@
1
- export declare function removeCommand(args: string[]): Promise<void>;
1
+ export declare const removeCommand: import("../registry/types.js").CommandDef<{
2
+ app: string;
3
+ }>;
@@ -1,28 +1,39 @@
1
- import { load, save, findApp, removeApp } from '../core/registry.js';
1
+ import { z } from 'zod';
2
+ import { load, findApp, removeApp, withRegistry } from '../core/registry.js';
2
3
  import { stopService, disableService } from '../core/systemd.js';
3
4
  import { AppNotFoundError } from '../core/errors.js';
4
- import { success, error, info, warn } from '../ui/output.js';
5
- import { confirm } from '../ui/confirm.js';
6
- export async function removeCommand(args) {
7
- const yes = args.includes('-y') || args.includes('--yes');
8
- const appName = args.find(a => !a.startsWith('-'));
9
- if (!appName) {
10
- error('Usage: fleet remove <app>');
11
- process.exit(1);
12
- }
13
- const reg = load();
14
- const app = findApp(reg, appName);
15
- if (!app)
16
- throw new AppNotFoundError(appName);
17
- if (!yes && !await confirm(`Remove ${app.name}? This will stop and disable the service.`)) {
18
- info('Cancelled');
19
- return;
20
- }
21
- info(`Stopping ${app.serviceName}...`);
22
- stopService(app.serviceName);
23
- info(`Disabling ${app.serviceName}...`);
24
- disableService(app.serviceName);
25
- save(removeApp(reg, app.name));
26
- success(`Removed ${app.name} from registry`);
27
- warn('Service file not deleted - remove manually if needed');
28
- }
5
+ import { defineCommand } from '../registry/registry.js';
6
+ export const removeCommand = defineCommand({
7
+ name: 'remove',
8
+ summary: 'Stop, disable and deregister an app',
9
+ args: z.object({ app: z.string(), yes: z.boolean().default(false) }),
10
+ destructive: true,
11
+ async run(args, ctx) {
12
+ const app = findApp(load(), args.app);
13
+ if (!app) {
14
+ return { ok: false, summary: `app not found: ${args.app}`, data: { app: args.app } };
15
+ }
16
+ if (!args.yes && !(await ctx.confirm(`Remove ${app.name}? This will stop and disable the service.`))) {
17
+ return { ok: false, summary: 'cancelled', data: { app: app.name } };
18
+ }
19
+ // systemctl runs outside the registry lock so we don't hold it while
20
+ // services stop/disable.
21
+ stopService(app.serviceName);
22
+ disableService(app.serviceName);
23
+ try {
24
+ await withRegistry(reg => {
25
+ const fresh = findApp(reg, app.name);
26
+ if (!fresh)
27
+ throw new AppNotFoundError(app.name);
28
+ return removeApp(reg, fresh.name);
29
+ });
30
+ }
31
+ catch (err) {
32
+ // a concurrent process removed the app between the unlocked preview and
33
+ // the locked mutation — surface it as a graceful expected failure.
34
+ return { ok: false, summary: err instanceof Error ? err.message : String(err), data: { app: app.name } };
35
+ }
36
+ ctx.log({ level: 'warn', message: 'service file not deleted — remove manually if needed' });
37
+ return { ok: true, summary: `removed ${app.name} from registry`, data: { app: app.name } };
38
+ },
39
+ });
@@ -1 +1,4 @@
1
- export declare function restartCommand(args: string[]): void;
1
+ export declare const restartCommand: import("../registry/types.js").CommandDef<{
2
+ app: string;
3
+ service: string;
4
+ }>;
@@ -1,22 +1,19 @@
1
+ import { z } from 'zod';
1
2
  import { load, findApp } from '../core/registry.js';
2
3
  import { restartService } from '../core/systemd.js';
3
- import { AppNotFoundError } from '../core/errors.js';
4
- import { success, error } from '../ui/output.js';
5
- export function restartCommand(args) {
6
- const appName = args[0];
7
- if (!appName) {
8
- error('Usage: fleet restart <app>');
9
- process.exit(1);
10
- }
11
- const reg = load();
12
- const app = findApp(reg, appName);
13
- if (!app)
14
- throw new AppNotFoundError(appName);
15
- if (restartService(app.serviceName)) {
16
- success(`Restarted ${app.name}`);
17
- }
18
- else {
19
- error(`Failed to restart ${app.name}`);
20
- process.exit(1);
21
- }
22
- }
4
+ import { defineCommand } from '../registry/registry.js';
5
+ export const restartCommand = defineCommand({
6
+ name: 'restart',
7
+ summary: 'Restart an app via systemctl',
8
+ args: z.object({ app: z.string() }),
9
+ async run(args) {
10
+ const app = findApp(load(), args.app);
11
+ if (!app) {
12
+ return { ok: false, summary: `app not found: ${args.app}`, data: { app: args.app, service: '' } };
13
+ }
14
+ if (!restartService(app.serviceName)) {
15
+ return { ok: false, summary: `failed to restart ${app.name}`, data: { app: app.name, service: app.serviceName } };
16
+ }
17
+ return { ok: true, summary: `restarted ${app.name}`, data: { app: app.name, service: app.serviceName } };
18
+ },
19
+ });
@@ -1 +1,4 @@
1
- export declare function rollbackCommand(args: string[]): Promise<void>;
1
+ export declare const rollbackCommand: import("../registry/types.js").CommandDef<{
2
+ app: string;
3
+ image: string;
4
+ }>;
@@ -1,12 +1,8 @@
1
+ import { z } from 'zod';
1
2
  import { load, findApp } from '../core/registry.js';
2
3
  import { execSafe } from '../core/exec.js';
3
4
  import { restartService } from '../core/systemd.js';
4
- function log(msg) {
5
- process.stdout.write(`[rollback] ${msg}\n`);
6
- }
7
- function logErr(msg) {
8
- process.stderr.write(`[rollback] ${msg}\n`);
9
- }
5
+ import { defineCommand } from '../registry/registry.js';
10
6
  function resolveImageName(composePath, composeFile) {
11
7
  const args = ['compose', ...(composeFile ? ['-f', composeFile] : []), 'config', '--images'];
12
8
  const r = execSafe('docker', args, { cwd: composePath, timeout: 15_000 });
@@ -20,39 +16,34 @@ function splitImageBase(image) {
20
16
  return image;
21
17
  return image.slice(0, lastColon);
22
18
  }
23
- export async function rollbackCommand(args) {
24
- const appName = args[0];
25
- if (!appName) {
26
- logErr('Usage: fleet rollback <app>');
27
- process.exit(1);
28
- }
29
- const reg = load();
30
- const app = findApp(reg, appName);
31
- if (!app) {
32
- logErr(`app not found: ${appName}`);
33
- process.exit(1);
34
- }
35
- const image = resolveImageName(app.composePath, app.composeFile);
36
- if (!image) {
37
- logErr(`could not resolve image name for ${app.name}`);
38
- process.exit(1);
39
- }
40
- const base = splitImageBase(image);
41
- const previous = `${base}:fleet-previous`;
42
- const latest = image;
43
- if (!execSafe('docker', ['image', 'inspect', previous], { timeout: 10_000 }).ok) {
44
- logErr(`no previous image found (${previous}) nothing to roll back to`);
45
- process.exit(1);
46
- }
47
- const tag = execSafe('docker', ['tag', previous, latest], { timeout: 10_000 });
48
- if (!tag.ok) {
49
- logErr(`docker tag failed: ${tag.stderr || `exit ${tag.exitCode}`}`);
50
- process.exit(1);
51
- }
52
- const ok = restartService(app.serviceName);
53
- if (!ok) {
54
- logErr(`tag restored but service restart failed for ${app.serviceName}`);
55
- process.exit(1);
56
- }
57
- log(`rolled back ${app.name} to ${previous}`);
58
- }
19
+ export const rollbackCommand = defineCommand({
20
+ name: 'rollback',
21
+ summary: 'Roll back app to previous image',
22
+ args: z.object({ app: z.string(), yes: z.boolean().default(false) }),
23
+ destructive: true,
24
+ async run(args, ctx) {
25
+ const app = findApp(load(), args.app);
26
+ if (!app) {
27
+ return { ok: false, summary: `app not found: ${args.app}`, data: { app: args.app, image: '' } };
28
+ }
29
+ const image = resolveImageName(app.composePath, app.composeFile);
30
+ if (!image) {
31
+ return { ok: false, summary: `could not resolve image name for ${app.name}`, data: { app: app.name, image: '' } };
32
+ }
33
+ const previous = `${splitImageBase(image)}:fleet-previous`;
34
+ if (!execSafe('docker', ['image', 'inspect', previous], { timeout: 10_000 }).ok) {
35
+ return { ok: false, summary: `no previous image found (${previous}) — nothing to roll back to`, data: { app: app.name, image: '' } };
36
+ }
37
+ if (!(args.yes || (await ctx.confirm(`Roll back ${app.name} to ${previous} and restart?`)))) {
38
+ return { ok: false, summary: 'cancelled', data: { app: app.name, image: previous } };
39
+ }
40
+ const tag = execSafe('docker', ['tag', previous, image], { timeout: 10_000 });
41
+ if (!tag.ok) {
42
+ return { ok: false, summary: `docker tag failed: ${tag.stderr || `exit ${tag.exitCode}`}`, data: { app: app.name, image: previous } };
43
+ }
44
+ if (!restartService(app.serviceName)) {
45
+ return { ok: false, summary: `tag restored but service restart failed for ${app.serviceName}`, data: { app: app.name, image: previous } };
46
+ }
47
+ return { ok: true, summary: `rolled back ${app.name} to ${previous}`, data: { app: app.name, image: previous } };
48
+ },
49
+ });