@matthesketh/fleet 1.8.1 → 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 +9 -6
  228. package/dist/tui/views/HealthView.js +9 -4
  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 +8 -5
@@ -19,6 +19,10 @@ import { checkHealth } from '../core/health.js';
19
19
  import { listSnapshots, restoreSnapshot, snapshotApp } from '../core/secrets-snapshots.js';
20
20
  import { auditLog } from '../core/secrets-audit.js';
21
21
  import { summariseSecrets, formatSecretsMotd, generateSecretsMotdScript } from '../core/secrets-motd.js';
22
+ import { migrateAppToV2, revertAppFromV2 } from '../core/secrets-v2-migrate.js';
23
+ import { cleanupV2Backups } from '../core/secrets-v2-cleanup.js';
24
+ import { getV2Status } from '../core/secrets-v2-ops.js';
25
+ import { installV2 } from '../core/secrets-v2-install.js';
22
26
  function getDbSecretsDir() {
23
27
  const reg = load();
24
28
  return join(reg.infrastructure.databases.composePath, 'secrets');
@@ -46,8 +50,13 @@ export async function secretsCommand(args) {
46
50
  case 'snapshots': return secretsSnapshots(rest);
47
51
  case 'motd-init': return secretsMotdInit();
48
52
  case 'seal-runtime': return secretsSeal(rest);
53
+ case 'migrate-v2': return secretsMigrateV2(rest);
54
+ case 'revert-v2': return secretsRevertV2(rest);
55
+ case 'cleanup-v2': return secretsCleanupV2(rest);
56
+ case 'status-v2': return secretsStatusV2(rest);
57
+ case 'install-v2': return secretsInstallV2(rest);
49
58
  default:
50
- error('Usage: fleet secrets <init|list|set|get|import|export|seal|unseal|rotate|rotate-key|ages|rollback|snapshots|validate|status|drift|restore>');
59
+ error('Usage: fleet secrets <init|list|set|get|import|export|seal|unseal|rotate|rotate-key|ages|rollback|snapshots|validate|status|drift|restore|migrate-v2|revert-v2|cleanup-v2|status-v2|install-v2>');
51
60
  process.exit(1);
52
61
  }
53
62
  }
@@ -129,7 +138,7 @@ async function secretsSet(args) {
129
138
  error('Empty value — aborting');
130
139
  process.exit(1);
131
140
  }
132
- setSecret(app, key, value, { allowWeak });
141
+ await setSecret(app, key, value, { allowWeak });
133
142
  success(`Set ${key} for ${app}`);
134
143
  }
135
144
  function secretsGet(args) {
@@ -145,7 +154,7 @@ function secretsGet(args) {
145
154
  }
146
155
  process.stdout.write(val + '\n');
147
156
  }
148
- function secretsImport(args) {
157
+ async function secretsImport(args) {
149
158
  const app = args.find(a => !a.startsWith('-'));
150
159
  const pathArg = args[1] && !args[1].startsWith('-') ? args[1] : null;
151
160
  if (!app) {
@@ -154,7 +163,7 @@ function secretsImport(args) {
154
163
  }
155
164
  if (app === 'docker-databases') {
156
165
  const dir = pathArg || getDbSecretsDir();
157
- const count = importDbSecrets(app, dir);
166
+ const count = await importDbSecrets(app, dir);
158
167
  success(`Imported ${count} secret files from ${dir}`);
159
168
  return;
160
169
  }
@@ -170,7 +179,7 @@ function secretsImport(args) {
170
179
  else {
171
180
  throw new SecretsError(`App not in registry and no path given: ${app}`);
172
181
  }
173
- const count = importEnvFile(app, envPath);
182
+ const count = await importEnvFile(app, envPath);
174
183
  success(`Imported ${count} keys from ${envPath}`);
175
184
  }
176
185
  function secretsExport(args) {
@@ -187,9 +196,9 @@ function secretsUnseal() {
187
196
  const count = Object.keys(manifest.apps).length;
188
197
  success(`Unsealed ${count} apps to /run/fleet-secrets/`);
189
198
  }
190
- function secretsSeal(args) {
199
+ async function secretsSeal(args) {
191
200
  const app = args.find(a => !a.startsWith('-')) || undefined;
192
- const sealed = sealFromRuntime(app);
201
+ const sealed = await sealFromRuntime(app);
193
202
  for (const a of sealed) {
194
203
  success(`Sealed ${a}`);
195
204
  }
@@ -200,7 +209,7 @@ async function secretsRotateKey(args) {
200
209
  info('Cancelled');
201
210
  return;
202
211
  }
203
- const result = rotateKey();
212
+ const result = await rotateKey();
204
213
  success(`Key rotated`);
205
214
  info(`Old: ${result.oldPubkey}`);
206
215
  info(`New: ${result.newPubkey}`);
@@ -294,7 +303,7 @@ async function rotateOneInteractive(app, secret, opts) {
294
303
  info('Cancelled');
295
304
  return { acted: false, succeeded: false };
296
305
  }
297
- const result = performRotation(app, secret.name, newValue, {
306
+ const result = await performRotation(app, secret.name, newValue, {
298
307
  dryRun: opts.dryRun,
299
308
  dataMigrated: opts.dataMigrated,
300
309
  });
@@ -699,3 +708,142 @@ function secretsRestore(args) {
699
708
  success(`Restored vault backup for ${app}`);
700
709
  info('Run "fleet secrets unseal" to apply to runtime');
701
710
  }
711
+ async function secretsMigrateV2(args) {
712
+ const app = args[0];
713
+ if (!app || app.startsWith('-')) {
714
+ error('Usage: fleet secrets migrate-v2 <app> [--no-restart-app] [--dry-run]');
715
+ process.exit(1);
716
+ }
717
+ const noRestartApp = args.includes('--no-restart-app');
718
+ const dryRun = args.includes('--dry-run');
719
+ heading(`Migrating ${app} to v2 (mode=socket)`);
720
+ if (dryRun)
721
+ info('DRY RUN — no actual changes will be applied');
722
+ try {
723
+ const result = await migrateAppToV2({ app, noRestartApp, dryRun });
724
+ if (result.rolledBack) {
725
+ error(`Migration failed; rolled back from snapshot ${result.snapshotDir}`);
726
+ for (const step of result.steps) {
727
+ if (!step.ok)
728
+ info(` step ${step.step} (${step.name}): ${step.detail}`);
729
+ }
730
+ process.exit(1);
731
+ }
732
+ success(`Migrated ${app} to v2`);
733
+ if (result.snapshotDir)
734
+ info(`Snapshot: ${result.snapshotDir}`);
735
+ for (const step of result.steps) {
736
+ info(` step ${step.step} (${step.name}): ${step.ok ? 'OK' : 'FAILED'}`);
737
+ }
738
+ }
739
+ catch (err) {
740
+ error(`Migration failed: ${err.message}`);
741
+ process.exit(1);
742
+ }
743
+ }
744
+ async function secretsRevertV2(args) {
745
+ const app = args[0];
746
+ if (!app || app.startsWith('-')) {
747
+ error('Usage: fleet secrets revert-v2 <app> [--snapshot <timestamp>]');
748
+ process.exit(1);
749
+ }
750
+ const snapIdx = args.indexOf('--snapshot');
751
+ const snapshotTimestamp = snapIdx >= 0 ? args[snapIdx + 1] : undefined;
752
+ heading(`Reverting ${app} from v2`);
753
+ try {
754
+ const result = await revertAppFromV2({ app, snapshotTimestamp });
755
+ if (result.ok) {
756
+ success(`Reverted ${app} to v1; restored from ${result.snapshotUsed}`);
757
+ }
758
+ else {
759
+ error(`Revert reported issues — see steps below`);
760
+ }
761
+ for (const step of result.steps) {
762
+ const status = step.ok ? 'OK' : 'FAILED';
763
+ info(` step ${step.step} (${step.name}): ${status}${step.detail ? ' — ' + step.detail : ''}`);
764
+ }
765
+ }
766
+ catch (err) {
767
+ error(`Revert failed: ${err.message}`);
768
+ process.exit(1);
769
+ }
770
+ }
771
+ async function secretsCleanupV2(args) {
772
+ const app = args[0];
773
+ if (!app || app.startsWith('-')) {
774
+ error('Usage: fleet secrets cleanup-v2 <app> [--retention-days N] [--dry-run]');
775
+ process.exit(1);
776
+ }
777
+ const retIdx = args.indexOf('--retention-days');
778
+ const retentionDays = retIdx >= 0 ? parseInt(args[retIdx + 1], 10) : 30;
779
+ const dryRun = args.includes('--dry-run');
780
+ if (Number.isNaN(retentionDays) || retentionDays < 0) {
781
+ error(`--retention-days must be a non-negative integer`);
782
+ process.exit(1);
783
+ }
784
+ heading(`Cleaning up v2 backups for ${app} (retention=${retentionDays}d)`);
785
+ if (dryRun)
786
+ info('DRY RUN — no actual deletions');
787
+ try {
788
+ const result = await cleanupV2Backups({ app, retentionDays, dryRun });
789
+ if (result.removedBak)
790
+ info(`v1 backup blob ${dryRun ? 'would be removed' : 'removed'}`);
791
+ info(`Snapshots removed: ${result.removedSnapshots.length}`);
792
+ info(`Snapshots kept: ${result.keptSnapshots.length}`);
793
+ for (const ts of result.removedSnapshots)
794
+ info(` - ${ts}`);
795
+ success('Cleanup complete');
796
+ }
797
+ catch (err) {
798
+ error(`Cleanup failed: ${err.message}`);
799
+ process.exit(1);
800
+ }
801
+ }
802
+ async function secretsInstallV2(args) {
803
+ const dryRun = args.includes('--dry-run');
804
+ heading(`Installing fleet-secrets-agent v2 host components`);
805
+ if (dryRun)
806
+ info('DRY RUN');
807
+ try {
808
+ const result = await installV2({ dryRun });
809
+ if (result.agentBinaryInstalled)
810
+ success('Agent binary installed at /usr/local/bin/fleet-agent');
811
+ else
812
+ info('Agent binary already current');
813
+ if (result.unitFileInstalled)
814
+ success('Templated unit installed at /etc/systemd/system/fleet-secrets-agent@.service');
815
+ else
816
+ info('Unit file already current');
817
+ if (result.daemonReloaded)
818
+ success('systemctl daemon-reload completed');
819
+ if (!result.templateParseable && !dryRun)
820
+ warn('Templated unit did not parse cleanly — investigate');
821
+ else if (!dryRun)
822
+ success('Templated unit verified');
823
+ }
824
+ catch (err) {
825
+ error(`Install failed: ${err.message}`);
826
+ process.exit(1);
827
+ }
828
+ }
829
+ function secretsStatusV2(_args) {
830
+ const report = getV2Status();
831
+ heading('Fleet secrets v2 status');
832
+ info(`v1 (unseal): ${report.v1Count} apps`);
833
+ info(`v2 (socket): ${report.v2Count} apps`);
834
+ info('');
835
+ if (report.apps.length === 0) {
836
+ info('No apps in manifest');
837
+ return;
838
+ }
839
+ const headers = ['App', 'Mode', 'Agent', 'Socket', 'Keys', 'Last sealed'];
840
+ const rows = report.apps.map(a => [
841
+ a.name,
842
+ a.mode,
843
+ a.mode === 'socket' ? (a.agentActive ? 'active' : 'inactive') : '—',
844
+ a.mode === 'socket' ? (a.socketOk ? 'ok' : 'BAD') : '—',
845
+ String(a.keyCount),
846
+ a.lastSealedAt.slice(0, 10),
847
+ ]);
848
+ table(headers, rows);
849
+ }
@@ -1 +1,4 @@
1
- export declare function startCommand(args: string[]): void;
1
+ export declare const startCommand: 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 { startService } from '../core/systemd.js';
3
- import { AppNotFoundError } from '../core/errors.js';
4
- import { success, error } from '../ui/output.js';
5
- export function startCommand(args) {
6
- const appName = args[0];
7
- if (!appName) {
8
- error('Usage: fleet start <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 (startService(app.serviceName)) {
16
- success(`Started ${app.name}`);
17
- }
18
- else {
19
- error(`Failed to start ${app.name}`);
20
- process.exit(1);
21
- }
22
- }
4
+ import { defineCommand } from '../registry/registry.js';
5
+ export const startCommand = defineCommand({
6
+ name: 'start',
7
+ summary: 'Start 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 (!startService(app.serviceName)) {
15
+ return { ok: false, summary: `failed to start ${app.name}`, data: { app: app.name, service: app.serviceName } };
16
+ }
17
+ return { ok: true, summary: `started ${app.name}`, data: { app: app.name, service: app.serviceName } };
18
+ },
19
+ });
@@ -11,4 +11,4 @@ export interface StatusData {
11
11
  unhealthy: number;
12
12
  }
13
13
  export declare function getStatusData(): StatusData;
14
- export declare function statusCommand(args: string[]): void;
14
+ export declare const statusCommand: import("../registry/types.js").CommandDef<StatusData>;
@@ -1,7 +1,8 @@
1
+ import { z } from 'zod';
1
2
  import { load } from '../core/registry.js';
2
3
  import { getMultipleServiceStatuses, systemdAvailable } from '../core/systemd.js';
3
4
  import { listContainers } from '../core/docker.js';
4
- import { c, icon, heading, table, info } from '../ui/output.js';
5
+ import { defineCommand } from '../registry/registry.js';
5
6
  export function getStatusData() {
6
7
  const reg = load();
7
8
  const containers = listContainers();
@@ -47,28 +48,22 @@ export function getStatusData() {
47
48
  unhealthy: apps.filter(a => a.health !== 'healthy').length,
48
49
  };
49
50
  }
50
- export function statusCommand(args) {
51
- const json = args.includes('--json');
52
- const data = getStatusData();
53
- if (json) {
54
- process.stdout.write(JSON.stringify(data, null, 2) + '\n');
55
- return;
56
- }
57
- heading('Fleet Dashboard');
58
- info(`${data.totalApps} apps | ${c.green}${data.healthy} healthy${c.reset} | ${data.unhealthy > 0 ? c.red : c.dim}${data.unhealthy} unhealthy${c.reset}`);
59
- const rows = data.apps.map(app => {
60
- const healthIcon = app.health === 'healthy' ? icon.ok
61
- : app.health === 'frozen' ? icon.info
62
- : app.health === 'degraded' ? icon.warn
63
- : icon.err;
64
- const systemdColor = app.systemd === 'active' ? c.green : c.red;
65
- return [
66
- `${c.bold}${app.name}${c.reset}`,
67
- `${systemdColor}${app.systemd}${c.reset}`,
68
- app.containers,
69
- `${healthIcon} ${app.health}`,
70
- ];
71
- });
72
- table(['APP', 'SYSTEMD', 'CONTAINERS', 'HEALTH'], rows);
73
- process.stdout.write('\n');
74
- }
51
+ export const statusCommand = defineCommand({
52
+ name: 'status',
53
+ summary: 'Dashboard: all apps, systemd state, containers, health',
54
+ args: z.object({}),
55
+ tui: { view: 'dashboard' },
56
+ async run() {
57
+ const data = getStatusData();
58
+ return {
59
+ ok: true,
60
+ summary: `${data.totalApps} apps | ${data.healthy} healthy | ${data.unhealthy} unhealthy`,
61
+ data,
62
+ render: {
63
+ kind: 'table',
64
+ columns: ['APP', 'SYSTEMD', 'CONTAINERS', 'HEALTH'],
65
+ rows: data.apps.map(a => [a.name, a.systemd, a.containers, a.health]),
66
+ },
67
+ };
68
+ },
69
+ });
@@ -1 +1,4 @@
1
- export declare function stopCommand(args: string[]): void;
1
+ export declare const stopCommand: 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 { stopService } from '../core/systemd.js';
3
- import { AppNotFoundError } from '../core/errors.js';
4
- import { success, error } from '../ui/output.js';
5
- export function stopCommand(args) {
6
- const appName = args[0];
7
- if (!appName) {
8
- error('Usage: fleet stop <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 (stopService(app.serviceName)) {
16
- success(`Stopped ${app.name}`);
17
- }
18
- else {
19
- error(`Failed to stop ${app.name}`);
20
- process.exit(1);
21
- }
22
- }
4
+ import { defineCommand } from '../registry/registry.js';
5
+ export const stopCommand = defineCommand({
6
+ name: 'stop',
7
+ summary: 'Stop 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 (!stopService(app.serviceName)) {
15
+ return { ok: false, summary: `failed to stop ${app.name}`, data: { app: app.name, service: app.serviceName } };
16
+ }
17
+ return { ok: true, summary: `stopped ${app.name}`, data: { app: app.name, service: app.serviceName } };
18
+ },
19
+ });
@@ -0,0 +1 @@
1
+ export declare function testflightCommand(args: string[]): Promise<void>;
@@ -0,0 +1,193 @@
1
+ import { resolveAscCredentials, hasAscCredentials } from '../core/testflight/credentials.js';
2
+ import { ghVersion, resolveRepo, repoSecrets, dispatchWorkflow, latestRun, watchRun, } from '../core/testflight/workflow.js';
3
+ import { listBuilds, expireBuild, setWhatsNew, verifyApp } from '../core/testflight/asc.js';
4
+ import { resolveTestflightTarget, appSecretsEnv } from '../core/testflight/resolve.js';
5
+ import { heading, success, error, info, warn, table } from '../ui/output.js';
6
+ // the build workflow this command dispatches by default — a macos-runner
7
+ // workflow committed to the app's repo at .github/workflows/.
8
+ const DEFAULT_WORKFLOW = 'ios-testflight.yml';
9
+ // the actions secrets the build workflow needs to sign and upload an .ipa.
10
+ const REQUIRED_REPO_SECRETS = [
11
+ 'ASC_API_KEY_ID', 'ASC_API_KEY_ISSUER_ID', 'ASC_API_KEY_B64', 'APPLE_TEAM_ID',
12
+ ];
13
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
14
+ // `fleet testflight` — publish a mobile app to TestFlight by dispatching its
15
+ // repo's macOS build workflow, and manage its builds through the App Store
16
+ // Connect API. an iOS .ipa can only be built on macOS, so publish runs the
17
+ // build on a github-hosted runner rather than locally.
18
+ export async function testflightCommand(args) {
19
+ switch (args[0]) {
20
+ case 'doctor': return tfDoctor(args.slice(1));
21
+ case 'publish': return tfPublish(args.slice(1));
22
+ case 'builds': return tfBuilds(args.slice(1));
23
+ case 'update': return tfUpdate(args.slice(1));
24
+ case 'delete': return tfDelete(args.slice(1));
25
+ default:
26
+ error('Usage: fleet testflight <doctor|publish|builds|update|delete>');
27
+ process.exit(1);
28
+ }
29
+ }
30
+ function extractFlag(args, flag) {
31
+ const idx = args.indexOf(flag);
32
+ if (idx === -1 || idx + 1 >= args.length)
33
+ return undefined;
34
+ return args[idx + 1];
35
+ }
36
+ async function tfDoctor(args) {
37
+ heading('TestFlight — readiness');
38
+ const gh = ghVersion();
39
+ if (gh)
40
+ success(`GitHub CLI available: ${gh}`);
41
+ else
42
+ error('GitHub CLI (gh) not found — required to dispatch the build workflow');
43
+ const appName = args.find(a => !a.startsWith('-'));
44
+ if (!appName) {
45
+ info('Pass an app name to check its repo + credentials: fleet testflight doctor <app>');
46
+ return;
47
+ }
48
+ const { app, projectPath } = resolveTestflightTarget(appName);
49
+ const repo = resolveRepo(projectPath);
50
+ if (repo) {
51
+ success(`GitHub repo: ${repo}`);
52
+ const secrets = repoSecrets(repo);
53
+ if (secrets) {
54
+ const missing = REQUIRED_REPO_SECRETS.filter(s => !secrets.includes(s));
55
+ if (missing.length === 0)
56
+ success('Repo Actions secrets present — the build workflow can sign and upload');
57
+ else
58
+ warn(`Repo Actions secrets missing: ${missing.join(', ')} — set them with "gh secret set"`);
59
+ }
60
+ else {
61
+ warn('Could not list repo Actions secrets — check "gh auth status"');
62
+ }
63
+ }
64
+ else {
65
+ warn(`No GitHub repo resolved for ${app} — publish needs a gh checkout at ${projectPath}`);
66
+ }
67
+ const env = appSecretsEnv(app);
68
+ if (!hasAscCredentials(env)) {
69
+ error(`App Store Connect credentials missing for ${app}`);
70
+ info('Required vault secrets: ASC_API_KEY_ID, ASC_API_KEY_ISSUER_ID, ASC_API_KEY_B64');
71
+ process.exit(1);
72
+ }
73
+ success('App Store Connect credentials present (builds/update/delete)');
74
+ const ascAppId = env.ASC_APP_ID;
75
+ if (!ascAppId) {
76
+ warn('ASC_APP_ID not set — builds/update/delete need it');
77
+ return;
78
+ }
79
+ try {
80
+ const name = await verifyApp(resolveAscCredentials(env), ascAppId);
81
+ success(`App Store Connect reachable — app: ${name}`);
82
+ }
83
+ catch (err) {
84
+ error(`App Store Connect check failed: ${err.message}`);
85
+ process.exit(1);
86
+ }
87
+ }
88
+ async function tfPublish(args) {
89
+ const appName = args.find(a => !a.startsWith('-'));
90
+ if (!appName) {
91
+ error('Usage: fleet testflight publish <app> [--workflow <file>] [--ref <branch>] [--watch]');
92
+ process.exit(1);
93
+ }
94
+ const workflow = extractFlag(args, '--workflow') ?? DEFAULT_WORKFLOW;
95
+ const ref = extractFlag(args, '--ref');
96
+ const watch = args.includes('--watch');
97
+ const { app, projectPath } = resolveTestflightTarget(appName);
98
+ heading(`TestFlight publish: ${app}`);
99
+ if (!ghVersion()) {
100
+ error('GitHub CLI (gh) not found — required to dispatch the build workflow');
101
+ process.exit(1);
102
+ }
103
+ const repo = resolveRepo(projectPath);
104
+ if (!repo) {
105
+ error(`Could not resolve a GitHub repo for ${app} — is ${projectPath} a gh checkout?`);
106
+ process.exit(1);
107
+ }
108
+ // remember the newest run so the one this dispatch queues can be told
109
+ // apart from it — `gh workflow run` returns no run id of its own.
110
+ const before = latestRun(repo, workflow)?.databaseId ?? 0;
111
+ info(`Dispatching ${workflow} on ${repo}${ref ? ` (${ref})` : ''}...`);
112
+ const dispatch = dispatchWorkflow(repo, workflow, ref);
113
+ if (!dispatch.ok) {
114
+ error(`Workflow dispatch failed: ${dispatch.message}`);
115
+ process.exit(1);
116
+ }
117
+ success('Workflow dispatched — the iOS build runs on a macOS runner (~15-30 min)');
118
+ // the queued run is not addressable straight away; poll briefly for it.
119
+ let run = latestRun(repo, workflow);
120
+ for (let i = 0; i < 10 && (!run || run.databaseId === before); i++) {
121
+ await sleep(3000);
122
+ run = latestRun(repo, workflow);
123
+ }
124
+ if (!run || run.databaseId === before) {
125
+ info(`Track it at https://github.com/${repo}/actions/workflows/${workflow}`);
126
+ return;
127
+ }
128
+ info(`Run: ${run.url}`);
129
+ if (watch) {
130
+ const code = watchRun(repo, run.databaseId);
131
+ if (code !== 0) {
132
+ error('The build workflow failed — see the run log above');
133
+ process.exit(1);
134
+ }
135
+ success(`${app} built and uploaded to TestFlight`);
136
+ }
137
+ }
138
+ async function tfBuilds(args) {
139
+ const appName = args.find(a => !a.startsWith('-'));
140
+ const json = args.includes('--json');
141
+ if (!appName) {
142
+ error('Usage: fleet testflight builds <app> [--app-id <id>] [--json]');
143
+ process.exit(1);
144
+ }
145
+ const { app } = resolveTestflightTarget(appName);
146
+ const env = appSecretsEnv(app);
147
+ const ascAppId = extractFlag(args, '--app-id') ?? env.ASC_APP_ID;
148
+ if (!ascAppId) {
149
+ error('App Store Connect app id required — set ASC_APP_ID or pass --app-id');
150
+ process.exit(1);
151
+ }
152
+ const builds = await listBuilds(resolveAscCredentials(env), ascAppId);
153
+ if (json) {
154
+ process.stdout.write(JSON.stringify(builds, null, 2) + '\n');
155
+ return;
156
+ }
157
+ heading(`TestFlight builds: ${app}`);
158
+ if (builds.length === 0) {
159
+ info('No builds found.');
160
+ return;
161
+ }
162
+ table(['BUILD', 'VERSION', 'STATE', 'EXPIRED', 'UPLOADED'], builds.map(b => [
163
+ b.version,
164
+ b.shortVersion,
165
+ b.processingState,
166
+ b.expired ? 'yes' : 'no',
167
+ b.uploadedDate.slice(0, 10),
168
+ ]));
169
+ process.stdout.write('\n');
170
+ }
171
+ async function tfUpdate(args) {
172
+ const appName = args.find(a => !a.startsWith('-'));
173
+ const buildId = extractFlag(args, '--build');
174
+ const whatsNew = extractFlag(args, '--whats-new');
175
+ if (!appName || !buildId || !whatsNew) {
176
+ error('Usage: fleet testflight update <app> --build <build-id> --whats-new "..."');
177
+ process.exit(1);
178
+ }
179
+ const { app } = resolveTestflightTarget(appName);
180
+ await setWhatsNew(resolveAscCredentials(appSecretsEnv(app)), buildId, whatsNew);
181
+ success(`Updated the "What to Test" notes for build ${buildId}`);
182
+ }
183
+ async function tfDelete(args) {
184
+ const appName = args.find(a => !a.startsWith('-'));
185
+ const buildId = extractFlag(args, '--build');
186
+ if (!appName || !buildId) {
187
+ error('Usage: fleet testflight delete <app> --build <build-id>');
188
+ process.exit(1);
189
+ }
190
+ const { app } = resolveTestflightTarget(appName);
191
+ await expireBuild(resolveAscCredentials(appSecretsEnv(app)), buildId);
192
+ success(`Expired build ${buildId} — it is no longer installable from TestFlight`);
193
+ }
@@ -0,0 +1,16 @@
1
+ export interface UpdateData {
2
+ channel: 'stable' | 'prerelease';
3
+ remoteBranch: string;
4
+ localBranch: string;
5
+ available: boolean;
6
+ behind: number;
7
+ latestSubject: string;
8
+ /** populated only when an apply actually ran. */
9
+ pulled?: number;
10
+ buildOk?: boolean;
11
+ output?: string;
12
+ error?: string;
13
+ }
14
+ /** CLI surface for self-update. equivalent to the TUI banner + U key, but
15
+ * driveable from a dumb terminal, a cron job, or a Claude session. */
16
+ export declare const updateCommand: import("../registry/types.js").CommandDef<UpdateData>;