@matthesketh/fleet 1.8.1 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/README.md +186 -16
  2. package/dist/bin/fleet-agent.d.ts +2 -0
  3. package/dist/bin/fleet-agent.js +7 -0
  4. package/dist/cli.d.ts +5 -0
  5. package/dist/cli.js +73 -31
  6. package/dist/commands/add.d.ts +2 -1
  7. package/dist/commands/add.js +66 -59
  8. package/dist/commands/audit.d.ts +1 -0
  9. package/dist/commands/audit.js +144 -0
  10. package/dist/commands/backup.d.ts +1 -0
  11. package/dist/commands/backup.js +510 -0
  12. package/dist/commands/boot-start.d.ts +3 -1
  13. package/dist/commands/boot-start.js +39 -47
  14. package/dist/commands/completions.d.ts +6 -0
  15. package/dist/commands/completions.js +83 -0
  16. package/dist/commands/config.d.ts +16 -0
  17. package/dist/commands/config.js +96 -0
  18. package/dist/commands/deploy.js +3 -2
  19. package/dist/commands/deps.js +5 -1
  20. package/dist/commands/doctor.d.ts +32 -0
  21. package/dist/commands/doctor.js +186 -0
  22. package/dist/commands/egress.d.ts +1 -1
  23. package/dist/commands/egress.js +13 -10
  24. package/dist/commands/freeze.d.ts +8 -4
  25. package/dist/commands/freeze.js +77 -59
  26. package/dist/commands/git.js +2 -2
  27. package/dist/commands/health.d.ts +2 -1
  28. package/dist/commands/health.js +38 -56
  29. package/dist/commands/init.d.ts +2 -1
  30. package/dist/commands/init.js +83 -73
  31. package/dist/commands/install-mcp.d.ts +3 -1
  32. package/dist/commands/install-mcp.js +53 -34
  33. package/dist/commands/list.d.ts +2 -1
  34. package/dist/commands/list.js +22 -19
  35. package/dist/commands/logs.js +1 -1
  36. package/dist/commands/patch-systemd.d.ts +7 -1
  37. package/dist/commands/patch-systemd.js +71 -31
  38. package/dist/commands/remove.d.ts +3 -1
  39. package/dist/commands/remove.js +37 -26
  40. package/dist/commands/restart.d.ts +4 -1
  41. package/dist/commands/restart.js +17 -20
  42. package/dist/commands/rollback.d.ts +4 -1
  43. package/dist/commands/rollback.js +33 -42
  44. package/dist/commands/secrets.js +157 -9
  45. package/dist/commands/start.d.ts +4 -1
  46. package/dist/commands/start.js +17 -20
  47. package/dist/commands/status.d.ts +1 -1
  48. package/dist/commands/status.js +21 -26
  49. package/dist/commands/stop.d.ts +4 -1
  50. package/dist/commands/stop.js +17 -20
  51. package/dist/commands/testflight.d.ts +1 -0
  52. package/dist/commands/testflight.js +193 -0
  53. package/dist/commands/update.d.ts +16 -0
  54. package/dist/commands/update.js +95 -0
  55. package/dist/core/audit/cache.d.ts +4 -0
  56. package/dist/core/audit/cache.js +37 -0
  57. package/dist/core/audit/config.d.ts +5 -0
  58. package/dist/core/audit/config.js +35 -0
  59. package/dist/core/audit/greenlight.d.ts +11 -0
  60. package/dist/core/audit/greenlight.js +81 -0
  61. package/dist/core/audit/reporters/cli.d.ts +3 -0
  62. package/dist/core/audit/reporters/cli.js +68 -0
  63. package/dist/core/audit/suppress.d.ts +6 -0
  64. package/dist/core/audit/suppress.js +37 -0
  65. package/dist/core/audit/target.d.ts +5 -0
  66. package/dist/core/audit/target.js +26 -0
  67. package/dist/core/audit/types.d.ts +54 -0
  68. package/dist/core/audit/types.js +5 -0
  69. package/dist/core/backup/browser-api.d.ts +66 -0
  70. package/dist/core/backup/browser-api.js +197 -0
  71. package/dist/core/backup/browser-server.d.ts +11 -0
  72. package/dist/core/backup/browser-server.js +241 -0
  73. package/dist/core/backup/browser-ui.d.ts +5 -0
  74. package/dist/core/backup/browser-ui.js +268 -0
  75. package/dist/core/backup/cloudflare.d.ts +7 -0
  76. package/dist/core/backup/cloudflare.js +82 -0
  77. package/dist/core/backup/config.d.ts +9 -0
  78. package/dist/core/backup/config.js +80 -0
  79. package/dist/core/backup/detect.d.ts +11 -0
  80. package/dist/core/backup/detect.js +71 -0
  81. package/dist/core/backup/dump.d.ts +11 -0
  82. package/dist/core/backup/dump.js +82 -0
  83. package/dist/core/backup/index.d.ts +9 -0
  84. package/dist/core/backup/index.js +9 -0
  85. package/dist/core/backup/repo.d.ts +71 -0
  86. package/dist/core/backup/repo.js +256 -0
  87. package/dist/core/backup/schedule.d.ts +17 -0
  88. package/dist/core/backup/schedule.js +90 -0
  89. package/dist/core/backup/sensitive.d.ts +5 -0
  90. package/dist/core/backup/sensitive.js +37 -0
  91. package/dist/core/backup/status.d.ts +3 -0
  92. package/dist/core/backup/status.js +29 -0
  93. package/dist/core/backup/statuspage.d.ts +23 -0
  94. package/dist/core/backup/statuspage.js +145 -0
  95. package/dist/core/backup/system.d.ts +24 -0
  96. package/dist/core/backup/system.js +209 -0
  97. package/dist/core/backup/totp.d.ts +16 -0
  98. package/dist/core/backup/totp.js +116 -0
  99. package/dist/core/backup/types.d.ts +70 -0
  100. package/dist/core/backup/types.js +7 -0
  101. package/dist/core/backup/unlock.d.ts +19 -0
  102. package/dist/core/backup/unlock.js +69 -0
  103. package/dist/core/boot-refresh.d.ts +1 -1
  104. package/dist/core/boot-refresh.js +10 -9
  105. package/dist/core/deps/actors/pr-creator.d.ts +5 -3
  106. package/dist/core/deps/actors/pr-creator.js +71 -18
  107. package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
  108. package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
  109. package/dist/core/deps/collectors/npm.js +3 -1
  110. package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
  111. package/dist/core/deps/collectors/vulnerability.js +31 -2
  112. package/dist/core/deps/config.js +6 -0
  113. package/dist/core/deps/scanner.js +1 -1
  114. package/dist/core/deps/types.d.ts +8 -0
  115. package/dist/core/env.d.ts +3 -0
  116. package/dist/core/env.js +11 -0
  117. package/dist/core/exec.d.ts +1 -0
  118. package/dist/core/exec.js +4 -0
  119. package/dist/core/file-lock.d.ts +18 -0
  120. package/dist/core/file-lock.js +44 -0
  121. package/dist/core/git-onboard.js +10 -13
  122. package/dist/core/github.d.ts +3 -1
  123. package/dist/core/github.js +10 -7
  124. package/dist/core/logs-policy.d.ts +5 -0
  125. package/dist/core/logs-policy.js +20 -1
  126. package/dist/core/operator.d.ts +21 -0
  127. package/dist/core/operator.js +54 -0
  128. package/dist/core/registry.d.ts +18 -0
  129. package/dist/core/registry.js +26 -0
  130. package/dist/core/routines/schema.d.ts +11 -11
  131. package/dist/core/routines/schema.js +14 -3
  132. package/dist/core/routines/store.d.ts +8 -8
  133. package/dist/core/secrets-ops.d.ts +31 -6
  134. package/dist/core/secrets-ops.js +208 -102
  135. package/dist/core/secrets-providers.js +2 -2
  136. package/dist/core/secrets-rotation.d.ts +1 -1
  137. package/dist/core/secrets-rotation.js +58 -52
  138. package/dist/core/secrets-v2-cleanup.d.ts +19 -0
  139. package/dist/core/secrets-v2-cleanup.js +94 -0
  140. package/dist/core/secrets-v2-creds.d.ts +9 -0
  141. package/dist/core/secrets-v2-creds.js +44 -0
  142. package/dist/core/secrets-v2-install.d.ts +13 -0
  143. package/dist/core/secrets-v2-install.js +76 -0
  144. package/dist/core/secrets-v2-keypair.d.ts +10 -0
  145. package/dist/core/secrets-v2-keypair.js +31 -0
  146. package/dist/core/secrets-v2-migrate.d.ts +29 -0
  147. package/dist/core/secrets-v2-migrate.js +395 -0
  148. package/dist/core/secrets-v2-ops.d.ts +36 -0
  149. package/dist/core/secrets-v2-ops.js +184 -0
  150. package/dist/core/secrets-v2-protocol.d.ts +19 -0
  151. package/dist/core/secrets-v2-protocol.js +60 -0
  152. package/dist/core/secrets-v2-snapshot.d.ts +36 -0
  153. package/dist/core/secrets-v2-snapshot.js +115 -0
  154. package/dist/core/secrets-v2.d.ts +21 -0
  155. package/dist/core/secrets-v2.js +249 -0
  156. package/dist/core/secrets.d.ts +39 -4
  157. package/dist/core/secrets.js +91 -11
  158. package/dist/core/self-update.d.ts +32 -11
  159. package/dist/core/self-update.js +52 -14
  160. package/dist/core/testflight/asc.d.ts +12 -0
  161. package/dist/core/testflight/asc.js +101 -0
  162. package/dist/core/testflight/credentials.d.ts +3 -0
  163. package/dist/core/testflight/credentials.js +35 -0
  164. package/dist/core/testflight/resolve.d.ts +6 -0
  165. package/dist/core/testflight/resolve.js +44 -0
  166. package/dist/core/testflight/types.d.ts +13 -0
  167. package/dist/core/testflight/types.js +3 -0
  168. package/dist/core/testflight/workflow.d.ts +17 -0
  169. package/dist/core/testflight/workflow.js +65 -0
  170. package/dist/core/validate.d.ts +1 -0
  171. package/dist/core/validate.js +8 -0
  172. package/dist/index.js +0 -0
  173. package/dist/mcp/audit-tools.d.ts +2 -0
  174. package/dist/mcp/audit-tools.js +94 -0
  175. package/dist/mcp/git-tools.js +1 -1
  176. package/dist/mcp/registry-bridge.d.ts +10 -0
  177. package/dist/mcp/registry-bridge.js +65 -0
  178. package/dist/mcp/secrets-tools.js +2 -2
  179. package/dist/mcp/server.js +16 -82
  180. package/dist/mcp/testflight-tools.d.ts +2 -0
  181. package/dist/mcp/testflight-tools.js +52 -0
  182. package/dist/registry/context.d.ts +7 -0
  183. package/dist/registry/context.js +37 -0
  184. package/dist/registry/index.d.ts +5 -0
  185. package/dist/registry/index.js +44 -0
  186. package/dist/registry/parse-args.d.ts +13 -0
  187. package/dist/registry/parse-args.js +74 -0
  188. package/dist/registry/registry.d.ts +24 -0
  189. package/dist/registry/registry.js +26 -0
  190. package/dist/registry/render.d.ts +3 -0
  191. package/dist/registry/render.js +29 -0
  192. package/dist/registry/types.d.ts +50 -0
  193. package/dist/registry/types.js +1 -0
  194. package/dist/templates/agent-unit.d.ts +5 -0
  195. package/dist/templates/agent-unit.js +40 -0
  196. package/dist/templates/app-unit-edit.d.ts +2 -0
  197. package/dist/templates/app-unit-edit.js +46 -0
  198. package/dist/templates/compose-edit.d.ts +2 -0
  199. package/dist/templates/compose-edit.js +156 -0
  200. package/dist/templates/nginx.js +11 -0
  201. package/dist/templates/systemd.js +6 -0
  202. package/dist/tui/components/ArgForm.d.ts +7 -0
  203. package/dist/tui/components/ArgForm.js +64 -0
  204. package/dist/tui/components/ArgForm.test.d.ts +1 -0
  205. package/dist/tui/components/ArgForm.test.js +19 -0
  206. package/dist/tui/components/KeyHint.js +5 -0
  207. package/dist/tui/hooks/use-secrets.d.ts +8 -8
  208. package/dist/tui/hooks/use-secrets.js +7 -7
  209. package/dist/tui/router.d.ts +1 -0
  210. package/dist/tui/router.js +26 -9
  211. package/dist/tui/router.test.d.ts +1 -0
  212. package/dist/tui/router.test.js +13 -0
  213. package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
  214. package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
  215. package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
  216. package/dist/tui/tests/redaction-rerender.test.js +53 -0
  217. package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
  218. package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
  219. package/dist/tui/types.d.ts +1 -1
  220. package/dist/tui/views/CommandPalette.d.ts +5 -0
  221. package/dist/tui/views/CommandPalette.js +90 -0
  222. package/dist/tui/views/CommandPalette.test.d.ts +1 -0
  223. package/dist/tui/views/CommandPalette.test.js +117 -0
  224. package/dist/tui/views/Dashboard.js +9 -6
  225. package/dist/tui/views/HealthView.js +9 -4
  226. package/dist/tui/views/SecretEdit.js +15 -16
  227. package/dist/tui/views/SecretEdit.test.d.ts +1 -0
  228. package/dist/tui/views/SecretEdit.test.js +82 -0
  229. package/dist/tui/views/SecretsView.js +26 -16
  230. package/package.json +8 -5
@@ -1,51 +1,43 @@
1
+ import { z } from 'zod';
1
2
  import { load, findApp } from '../core/registry.js';
2
3
  import { refresh } from '../core/boot-refresh.js';
3
4
  import { composeUp } from '../core/docker.js';
4
- function log(msg) {
5
- process.stdout.write(`[boot-start] ${msg}\n`);
6
- }
7
- function logErr(msg) {
8
- process.stderr.write(`[boot-start] ${msg}\n`);
9
- }
10
- export async function bootStartCommand(args) {
11
- const appName = args[0];
12
- if (!appName) {
13
- logErr('Usage: fleet boot-start <app>');
14
- process.exit(1);
15
- }
16
- const reg = load();
17
- const app = findApp(reg, appName);
18
- if (!app) {
19
- logErr(`app not found: ${appName}`);
20
- process.exit(1);
21
- }
22
- // Refresh is best-effort. Any error — sync or async — is caught here and logged,
23
- // then compose up ALWAYS runs. This is the fail-safe contract for boot.
24
- try {
25
- const result = await refresh(app);
26
- switch (result.kind) {
27
- case 'refreshed':
28
- log(`refreshed ${app.name} head=${result.head} built=${result.built}`);
29
- break;
30
- case 'no-change':
31
- log(`no-change ${app.name} head=${result.head}`);
32
- break;
33
- case 'skipped':
34
- log(`skipped ${app.name} reason=${result.reason}`);
35
- break;
36
- case 'failed-safe':
37
- log(`failed-safe ${app.name} step=${result.step} detail=${result.detail}`);
38
- break;
5
+ import { defineCommand } from '../registry/registry.js';
6
+ export const bootStartCommand = defineCommand({
7
+ name: 'boot-start',
8
+ summary: 'Start an app respecting boot-order dependencies',
9
+ args: z.object({ app: z.string() }),
10
+ cliOnly: 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 } };
39
15
  }
40
- }
41
- catch (err) {
42
- log(`failed-safe ${app.name} step=outer-catch detail=${err instanceof Error ? err.message : String(err)}`);
43
- }
44
- // compose up — the only step whose exit code matters
45
- const ok = composeUp(app.composePath, app.composeFile);
46
- if (!ok) {
47
- logErr(`compose up failed for ${app.name}`);
48
- process.exit(1);
49
- }
50
- log(`up ${app.name}`);
51
- }
16
+ // refresh is best-effort — any error (sync or async) is logged and compose
17
+ // up always runs. this is the fail-safe contract for boot.
18
+ try {
19
+ const result = await refresh(app);
20
+ switch (result.kind) {
21
+ case 'refreshed':
22
+ ctx.log({ level: 'info', message: `refreshed ${app.name} head=${result.head} built=${result.built}` });
23
+ break;
24
+ case 'no-change':
25
+ ctx.log({ level: 'info', message: `no-change ${app.name} head=${result.head}` });
26
+ break;
27
+ case 'skipped':
28
+ ctx.log({ level: 'info', message: `skipped ${app.name} reason=${result.reason}` });
29
+ break;
30
+ case 'failed-safe':
31
+ ctx.log({ level: 'warn', message: `failed-safe ${app.name} step=${result.step} detail=${result.detail}` });
32
+ break;
33
+ }
34
+ }
35
+ catch (err) {
36
+ ctx.log({ level: 'warn', message: `failed-safe ${app.name} step=outer-catch detail=${err instanceof Error ? err.message : String(err)}` });
37
+ }
38
+ if (!composeUp(app.composePath, app.composeFile)) {
39
+ return { ok: false, summary: `compose up failed for ${app.name}`, data: { app: app.name } };
40
+ }
41
+ return { ok: true, summary: `up ${app.name}`, data: { app: app.name } };
42
+ },
43
+ });
@@ -0,0 +1,6 @@
1
+ export type Shell = 'bash' | 'zsh' | 'fish';
2
+ export interface CompletionsData {
3
+ shell: Shell;
4
+ script: string;
5
+ }
6
+ export declare const completionsCommand: import("../registry/types.js").CommandDef<CompletionsData>;
@@ -0,0 +1,83 @@
1
+ import { z } from 'zod';
2
+ import { allCommands, defineCommand } from '../registry/registry.js';
3
+ // commands still living in the legacy switch in cli.ts. tracked separately
4
+ // from the registry so completions stay accurate during the migration;
5
+ // shrinks toward [] as each migration commit lands.
6
+ const LEGACY_COMMANDS = [
7
+ 'logs', 'egress', 'deps', 'audit', 'testflight', 'deploy', 'nginx', 'secrets',
8
+ 'git', 'watchdog', 'guard', 'backup', 'routines', 'routine-run',
9
+ 'tui', 'dashboard', 'mcp',
10
+ ];
11
+ /** lazy-import loadRegistry to dodge the circular: registry/index imports this
12
+ * module (to register the command), and this module needs the registry to be
13
+ * populated before allCommands() is called. importing index at call-time
14
+ * lets node finish evaluating both modules first. */
15
+ async function collectCommandNames() {
16
+ const { loadRegistry } = await import('../registry/index.js');
17
+ loadRegistry();
18
+ const registered = allCommands().map(c => c.name);
19
+ return Array.from(new Set([...registered, ...LEGACY_COMMANDS])).sort();
20
+ }
21
+ function bashScript(names) {
22
+ // tab completion entry-point — sourced from ~/.bashrc:
23
+ // eval "$(fleet completions bash)"
24
+ return `# fleet bash completions — install with: eval "$(fleet completions bash)"
25
+ _fleet_completions() {
26
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
27
+ if [ "\${COMP_CWORD}" -eq 1 ]; then
28
+ COMPREPLY=( $(compgen -W "${names.join(' ')}" -- "$cur") )
29
+ fi
30
+ return 0
31
+ }
32
+ complete -F _fleet_completions fleet
33
+ `;
34
+ }
35
+ function zshScript(names) {
36
+ return `# fleet zsh completions — install with: eval "$(fleet completions zsh)"
37
+ _fleet() {
38
+ local -a commands
39
+ commands=(${names.map(n => `'${n}'`).join(' ')})
40
+ if [ \${CURRENT} -eq 2 ]; then
41
+ _describe 'command' commands
42
+ fi
43
+ }
44
+ compdef _fleet fleet
45
+ `;
46
+ }
47
+ function fishScript(names) {
48
+ // fish reads completions from ~/.config/fish/completions/fleet.fish:
49
+ // fleet completions fish > ~/.config/fish/completions/fleet.fish
50
+ const lines = names.map(n => `complete -c fleet -f -n '__fish_use_subcommand' -a '${n}'`);
51
+ return `# fleet fish completions — install with:
52
+ # fleet completions fish > ~/.config/fish/completions/fleet.fish
53
+ ${lines.join('\n')}
54
+ `;
55
+ }
56
+ export const completionsCommand = defineCommand({
57
+ name: 'completions',
58
+ summary: 'Emit shell completion script for bash, zsh, or fish',
59
+ cliOnly: true,
60
+ args: z.object({
61
+ shell: z.enum(['bash', 'zsh', 'fish']),
62
+ }),
63
+ async run(args, _ctx) {
64
+ const names = await collectCommandNames();
65
+ let script;
66
+ switch (args.shell) {
67
+ case 'bash':
68
+ script = bashScript(names);
69
+ break;
70
+ case 'zsh':
71
+ script = zshScript(names);
72
+ break;
73
+ case 'fish':
74
+ script = fishScript(names);
75
+ break;
76
+ }
77
+ return {
78
+ ok: true,
79
+ summary: script,
80
+ data: { shell: args.shell, script },
81
+ };
82
+ },
83
+ });
@@ -0,0 +1,16 @@
1
+ import { type OperatorConfig, type OperatorField } from '../core/operator.js';
2
+ export interface ConfigData {
3
+ action: 'show' | 'get' | 'set';
4
+ path: string;
5
+ config?: OperatorConfig;
6
+ field?: OperatorField;
7
+ value?: string;
8
+ }
9
+ export declare const configCommand: import("../registry/types.js").CommandDef<ConfigData>;
10
+ export interface WhoamiData {
11
+ username: string;
12
+ domain: string;
13
+ githubOrg: string;
14
+ homeDir: string;
15
+ }
16
+ export declare const whoamiCommand: import("../registry/types.js").CommandDef<WhoamiData>;
@@ -0,0 +1,96 @@
1
+ import { z } from 'zod';
2
+ import { loadOperator, operatorPath, saveOperator, OPERATOR_FIELDS, } from '../core/operator.js';
3
+ import { defineCommand } from '../registry/registry.js';
4
+ function isOperatorField(name) {
5
+ return OPERATOR_FIELDS.includes(name);
6
+ }
7
+ export const configCommand = defineCommand({
8
+ name: 'config',
9
+ summary: 'Show or update the operator identity config (data/operator.json)',
10
+ args: z.object({
11
+ action: z.enum(['show', 'get', 'set']).default('show'),
12
+ field: z.string().optional(),
13
+ value: z.string().optional(),
14
+ }),
15
+ async run(args, _ctx) {
16
+ const path = operatorPath();
17
+ if (args.action === 'show') {
18
+ const cfg = loadOperator();
19
+ return {
20
+ ok: true,
21
+ summary: `operator: ${cfg.username} @ ${cfg.domain} (github ${cfg.githubOrg})`,
22
+ data: { action: 'show', path, config: cfg },
23
+ render: {
24
+ kind: 'keyValue',
25
+ pairs: [
26
+ ['path', path],
27
+ ['username', cfg.username],
28
+ ['homeDir', cfg.homeDir],
29
+ ['domain', cfg.domain],
30
+ ['githubOrg', cfg.githubOrg],
31
+ ],
32
+ },
33
+ };
34
+ }
35
+ if (args.action === 'get') {
36
+ if (!args.field) {
37
+ return { ok: false, summary: 'fleet config get <field>', data: { action: 'get', path } };
38
+ }
39
+ if (!isOperatorField(args.field)) {
40
+ return {
41
+ ok: false,
42
+ summary: `unknown field: ${args.field} (known: ${OPERATOR_FIELDS.join(', ')})`,
43
+ data: { action: 'get', path },
44
+ };
45
+ }
46
+ const cfg = loadOperator();
47
+ const value = cfg[args.field];
48
+ return {
49
+ ok: true,
50
+ summary: value,
51
+ data: { action: 'get', path, field: args.field, value },
52
+ };
53
+ }
54
+ // action === 'set'
55
+ if (!args.field || !args.value) {
56
+ return {
57
+ ok: false,
58
+ summary: 'fleet config set <field> <value>',
59
+ data: { action: 'set', path },
60
+ };
61
+ }
62
+ if (!isOperatorField(args.field)) {
63
+ return {
64
+ ok: false,
65
+ summary: `unknown field: ${args.field} (known: ${OPERATOR_FIELDS.join(', ')})`,
66
+ data: { action: 'set', path },
67
+ };
68
+ }
69
+ const cfg = loadOperator();
70
+ const next = { ...cfg, [args.field]: args.value };
71
+ saveOperator(next);
72
+ return {
73
+ ok: true,
74
+ summary: `set ${args.field}=${args.value}`,
75
+ data: { action: 'set', path, field: args.field, value: args.value, config: next },
76
+ };
77
+ },
78
+ });
79
+ export const whoamiCommand = defineCommand({
80
+ name: 'whoami',
81
+ summary: 'Print the operator identity in one line',
82
+ args: z.object({}),
83
+ async run(_args, _ctx) {
84
+ const cfg = loadOperator();
85
+ return {
86
+ ok: true,
87
+ summary: `${cfg.username} @ ${cfg.domain} (github ${cfg.githubOrg}, home ${cfg.homeDir})`,
88
+ data: {
89
+ username: cfg.username,
90
+ domain: cfg.domain,
91
+ githubOrg: cfg.githubOrg,
92
+ homeDir: cfg.homeDir,
93
+ },
94
+ };
95
+ },
96
+ });
@@ -6,6 +6,7 @@ import { startService, restartService, getServiceStatus } from '../core/systemd.
6
6
  import { FleetError } from '../core/errors.js';
7
7
  import { success, error, info, warn, heading } from '../ui/output.js';
8
8
  import { addCommand } from './add.js';
9
+ import { makeCliContext } from '../registry/context.js';
9
10
  import { execGit } from '../core/exec.js';
10
11
  import { getProjectRoot } from '../core/git.js';
11
12
  import { recordBuiltCommit } from '../core/boot-refresh.js';
@@ -26,7 +27,7 @@ export async function deployCommand(args) {
26
27
  let app = reg.apps.find(a => a.composePath.startsWith(fullPath));
27
28
  if (!app) {
28
29
  info('App not registered, running add first...');
29
- await addCommand([...args]);
30
+ await addCommand.run({ dir: fullPath, 'dry-run': dryRun, yes }, makeCliContext());
30
31
  reg = load();
31
32
  app = reg.apps.find(a => a.composePath.startsWith(fullPath));
32
33
  if (!app)
@@ -47,7 +48,7 @@ export async function deployCommand(args) {
47
48
  const root = getProjectRoot(app.composePath);
48
49
  const head = execGit(['rev-parse', 'HEAD'], { cwd: root, timeout: 10_000 });
49
50
  if (head.ok && head.stdout.trim()) {
50
- recordBuiltCommit(app.name, head.stdout.trim());
51
+ await recordBuiltCommit(app.name, head.stdout.trim());
51
52
  }
52
53
  }
53
54
  catch {
@@ -130,11 +130,15 @@ async function depsFix(args) {
130
130
  return;
131
131
  }
132
132
  const result = createDepsPr(app, findings, dryRun);
133
+ if (result.error) {
134
+ error(result.error);
135
+ process.exit(1);
136
+ }
133
137
  if (dryRun) {
134
138
  heading(`Dry run: ${app.name}`);
135
139
  info(`Would create branch: ${result.branch}`);
136
140
  for (const bump of result.bumps) {
137
- info(` ${bump.file}: ${bump.search} -> ${bump.replace}`);
141
+ info(` ${bump.file}: ${bump.searchRegex.source} -> ${bump.replace}`);
138
142
  }
139
143
  return;
140
144
  }
@@ -0,0 +1,32 @@
1
+ import { load as loadRegistry } from '../core/registry.js';
2
+ import { loadOperator } from '../core/operator.js';
3
+ export type CheckStatus = 'ok' | 'warn' | 'fail';
4
+ export interface DoctorCheck {
5
+ name: string;
6
+ status: CheckStatus;
7
+ detail: string;
8
+ }
9
+ export interface DoctorData {
10
+ checks: DoctorCheck[];
11
+ summary: {
12
+ ok: number;
13
+ warn: number;
14
+ fail: number;
15
+ };
16
+ }
17
+ interface SystemRunner {
18
+ exec: (cmd: string, args: string[]) => {
19
+ ok: boolean;
20
+ stdout: string;
21
+ stderr: string;
22
+ };
23
+ exists: (path: string) => boolean;
24
+ loadRegistry: () => ReturnType<typeof loadRegistry>;
25
+ loadOperator: () => ReturnType<typeof loadOperator>;
26
+ vaultInitialised: () => boolean;
27
+ vaultSealed: () => boolean;
28
+ }
29
+ /** core check runner. exported so tests can drive the pure list. */
30
+ export declare function runChecks(runner: SystemRunner): DoctorData;
31
+ export declare const doctorCommand: import("../registry/types.js").CommandDef<DoctorData>;
32
+ export {};
@@ -0,0 +1,186 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { z } from 'zod';
3
+ import { execSafe } from '../core/exec.js';
4
+ import { isInitialized, isSealed } from '../core/secrets.js';
5
+ import { load as loadRegistry } from '../core/registry.js';
6
+ import { loadOperator } from '../core/operator.js';
7
+ import { defineCommand } from '../registry/registry.js';
8
+ /** real implementations of every external probe — split out so the test suite
9
+ * can swap them with deterministic stubs. */
10
+ function realRunner() {
11
+ return {
12
+ exec: (cmd, args) => {
13
+ const r = execSafe(cmd, args, { timeout: 5_000 });
14
+ return { ok: r.ok, stdout: r.stdout, stderr: r.stderr };
15
+ },
16
+ exists: existsSync,
17
+ loadRegistry,
18
+ loadOperator,
19
+ vaultInitialised: isInitialized,
20
+ vaultSealed: isSealed,
21
+ };
22
+ }
23
+ // minimum supported component versions. systemd 240 introduced
24
+ // LoadCredentialEncrypted (used by the v2 secrets agent). docker compose v2
25
+ // is the minimum the rest of fleet assumes. node 20 is the baseline.
26
+ const MIN_NODE_MAJOR = 20;
27
+ const MIN_DOCKER_COMPOSE_MAJOR = 2;
28
+ const MIN_SYSTEMD = 240;
29
+ function checkNode(runner) {
30
+ const major = parseInt(process.versions.node.split('.')[0], 10);
31
+ if (Number.isNaN(major) || major < MIN_NODE_MAJOR) {
32
+ return { name: 'node', status: 'fail', detail: `node ${process.versions.node} < ${MIN_NODE_MAJOR}` };
33
+ }
34
+ // suppress the warning about unused runner parameter in this branch
35
+ void runner;
36
+ return { name: 'node', status: 'ok', detail: process.versions.node };
37
+ }
38
+ function checkAge(runner) {
39
+ if (!runner.exec('which', ['age']).ok) {
40
+ return { name: 'age', status: 'fail', detail: 'age not on PATH (apt install age)' };
41
+ }
42
+ const v = runner.exec('age', ['--version']);
43
+ return { name: 'age', status: 'ok', detail: v.stdout || 'present' };
44
+ }
45
+ function checkDockerCompose(runner) {
46
+ const r = runner.exec('docker', ['compose', 'version']);
47
+ if (!r.ok) {
48
+ return { name: 'docker compose', status: 'fail', detail: 'docker compose v2 not available' };
49
+ }
50
+ // sample stdout: "docker compose version v2.27.0"
51
+ const m = r.stdout.match(/v?(\d+)\.(\d+)/);
52
+ if (!m) {
53
+ return { name: 'docker compose', status: 'warn', detail: `unable to parse version: ${r.stdout}` };
54
+ }
55
+ const major = parseInt(m[1], 10);
56
+ if (major < MIN_DOCKER_COMPOSE_MAJOR) {
57
+ return { name: 'docker compose', status: 'fail', detail: `${r.stdout} < v${MIN_DOCKER_COMPOSE_MAJOR}` };
58
+ }
59
+ return { name: 'docker compose', status: 'ok', detail: r.stdout };
60
+ }
61
+ function checkSystemd(runner) {
62
+ const r = runner.exec('systemctl', ['--version']);
63
+ if (!r.ok) {
64
+ return { name: 'systemd', status: 'fail', detail: 'systemctl not available' };
65
+ }
66
+ const m = r.stdout.match(/systemd (\d+)/);
67
+ if (!m) {
68
+ return { name: 'systemd', status: 'warn', detail: `unable to parse version: ${r.stdout.split('\n')[0]}` };
69
+ }
70
+ const v = parseInt(m[1], 10);
71
+ if (v < MIN_SYSTEMD) {
72
+ return {
73
+ name: 'systemd',
74
+ status: 'warn',
75
+ detail: `systemd ${v} < ${MIN_SYSTEMD} (LoadCredentialEncrypted needs 240+; v2 secrets won't work)`,
76
+ };
77
+ }
78
+ return { name: 'systemd', status: 'ok', detail: `systemd ${v}` };
79
+ }
80
+ function checkRegistry(runner) {
81
+ try {
82
+ const reg = runner.loadRegistry();
83
+ return {
84
+ name: 'registry',
85
+ status: 'ok',
86
+ detail: `${reg.apps.length} app(s) registered`,
87
+ };
88
+ }
89
+ catch (err) {
90
+ return {
91
+ name: 'registry',
92
+ status: 'fail',
93
+ detail: `parse failed: ${err.message}`,
94
+ };
95
+ }
96
+ }
97
+ function checkOperator(runner) {
98
+ try {
99
+ const op = runner.loadOperator();
100
+ return {
101
+ name: 'operator config',
102
+ status: 'ok',
103
+ detail: `${op.username} @ ${op.domain} (github: ${op.githubOrg})`,
104
+ };
105
+ }
106
+ catch (err) {
107
+ return {
108
+ name: 'operator config',
109
+ status: 'fail',
110
+ detail: err.message,
111
+ };
112
+ }
113
+ }
114
+ function checkVault(runner) {
115
+ if (!runner.vaultInitialised()) {
116
+ return { name: 'secrets vault', status: 'warn', detail: 'vault not initialised (run: fleet secrets init)' };
117
+ }
118
+ const sealed = runner.vaultSealed();
119
+ return {
120
+ name: 'secrets vault',
121
+ status: 'ok',
122
+ detail: sealed ? 'initialised, sealed' : 'initialised, unsealed',
123
+ };
124
+ }
125
+ function checkOrphans(runner) {
126
+ try {
127
+ const reg = runner.loadRegistry();
128
+ const orphans = reg.apps.filter(a => !runner.exists(a.composePath));
129
+ if (orphans.length === 0) {
130
+ return { name: 'registered apps on disk', status: 'ok', detail: 'all composePath entries exist' };
131
+ }
132
+ return {
133
+ name: 'registered apps on disk',
134
+ status: 'warn',
135
+ detail: `${orphans.length} orphan(s): ${orphans.map(a => a.name).join(', ')}`,
136
+ };
137
+ }
138
+ catch {
139
+ // registry parse already reported separately — don't double-fail.
140
+ return { name: 'registered apps on disk', status: 'warn', detail: 'skipped (registry unreadable)' };
141
+ }
142
+ }
143
+ /** core check runner. exported so tests can drive the pure list. */
144
+ export function runChecks(runner) {
145
+ const checks = [
146
+ checkNode(runner),
147
+ checkAge(runner),
148
+ checkDockerCompose(runner),
149
+ checkSystemd(runner),
150
+ checkRegistry(runner),
151
+ checkOperator(runner),
152
+ checkVault(runner),
153
+ checkOrphans(runner),
154
+ ];
155
+ const summary = {
156
+ ok: checks.filter(c => c.status === 'ok').length,
157
+ warn: checks.filter(c => c.status === 'warn').length,
158
+ fail: checks.filter(c => c.status === 'fail').length,
159
+ };
160
+ return { checks, summary };
161
+ }
162
+ const STATUS_LABEL = {
163
+ ok: 'OK',
164
+ warn: 'WARN',
165
+ fail: 'FAIL',
166
+ };
167
+ export const doctorCommand = defineCommand({
168
+ name: 'doctor',
169
+ summary: 'Preflight: host requirements, registry, vault, operator config, orphan apps',
170
+ args: z.object({}),
171
+ async run(_args, _ctx) {
172
+ const data = runChecks(realRunner());
173
+ return {
174
+ ok: data.summary.fail === 0,
175
+ summary: data.summary.fail === 0
176
+ ? `doctor: ${data.summary.ok} ok, ${data.summary.warn} warn, 0 fail`
177
+ : `doctor: ${data.summary.fail} fail, ${data.summary.warn} warn, ${data.summary.ok} ok`,
178
+ data,
179
+ render: {
180
+ kind: 'table',
181
+ columns: ['CHECK', 'STATUS', 'DETAIL'],
182
+ rows: data.checks.map(c => [c.name, STATUS_LABEL[c.status], c.detail]),
183
+ },
184
+ };
185
+ },
186
+ });
@@ -1 +1 @@
1
- export declare function egressCommand(args: string[]): void;
1
+ export declare function egressCommand(args: string[]): Promise<void>;
@@ -1,8 +1,8 @@
1
- import { load, save, findApp } from '../core/registry.js';
1
+ import { load, findApp, withRegistry } from '../core/registry.js';
2
2
  import { snapshotEgress, addEgressAllow } from '../core/egress.js';
3
3
  import { AppNotFoundError } from '../core/errors.js';
4
4
  import { c, error, heading, info, success, table, warn } from '../ui/output.js';
5
- export function egressCommand(args) {
5
+ export async function egressCommand(args) {
6
6
  const sub = args[0];
7
7
  switch (sub) {
8
8
  case 'observe': return egressObserve(args.slice(1));
@@ -89,18 +89,21 @@ function egressShow(args) {
89
89
  }
90
90
  info('Note: v1 is observe/shadow only — no packets are actually dropped.');
91
91
  }
92
- function egressAllow(args) {
92
+ async function egressAllow(args) {
93
93
  const positional = args.filter(a => !a.startsWith('-'));
94
94
  const [appName, host] = positional;
95
95
  if (!appName || !host) {
96
96
  error('Usage: fleet egress allow <app> <host[:port] | *.host | cidr>');
97
97
  process.exit(1);
98
98
  }
99
- const reg = load();
100
- const app = findApp(reg, appName);
101
- if (!app)
102
- throw new AppNotFoundError(appName);
103
- const updated = addEgressAllow(app, host);
104
- save(reg);
105
- success(`${appName} allow → ${host} (now ${updated.length} entries)`);
99
+ let entryCount = 0;
100
+ await withRegistry(reg => {
101
+ const app = findApp(reg, appName);
102
+ if (!app)
103
+ throw new AppNotFoundError(appName);
104
+ const updated = addEgressAllow(app, host);
105
+ entryCount = updated.length;
106
+ return reg;
107
+ });
108
+ success(`${appName} allow → ${host} (now ${entryCount} entries)`);
106
109
  }
@@ -1,4 +1,8 @@
1
- export declare function freezeApp(appName: string, reason?: string): void;
2
- export declare function unfreezeApp(appName: string): void;
3
- export declare function freezeCommand(args: string[]): void;
4
- export declare function unfreezeCommand(args: string[]): void;
1
+ export declare function freezeApp(appName: string, reason?: string): Promise<void>;
2
+ export declare function unfreezeApp(appName: string): Promise<void>;
3
+ export declare const freezeCommand: import("../registry/types.js").CommandDef<{
4
+ app: string;
5
+ }>;
6
+ export declare const unfreezeCommand: import("../registry/types.js").CommandDef<{
7
+ app: string;
8
+ }>;