@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.
- package/README.md +186 -16
- package/dist/bin/fleet-agent.d.ts +2 -0
- package/dist/bin/fleet-agent.js +7 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +73 -31
- package/dist/commands/add.d.ts +2 -1
- package/dist/commands/add.js +66 -59
- package/dist/commands/audit.d.ts +1 -0
- package/dist/commands/audit.js +144 -0
- package/dist/commands/backup.d.ts +1 -0
- package/dist/commands/backup.js +510 -0
- package/dist/commands/boot-start.d.ts +3 -1
- package/dist/commands/boot-start.js +39 -47
- package/dist/commands/completions.d.ts +6 -0
- package/dist/commands/completions.js +83 -0
- package/dist/commands/config.d.ts +16 -0
- package/dist/commands/config.js +96 -0
- package/dist/commands/deploy.js +3 -2
- package/dist/commands/deps.js +5 -1
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.js +186 -0
- package/dist/commands/egress.d.ts +1 -1
- package/dist/commands/egress.js +13 -10
- package/dist/commands/freeze.d.ts +8 -4
- package/dist/commands/freeze.js +77 -59
- package/dist/commands/git.js +2 -2
- package/dist/commands/health.d.ts +2 -1
- package/dist/commands/health.js +38 -56
- package/dist/commands/init.d.ts +2 -1
- package/dist/commands/init.js +83 -73
- package/dist/commands/install-mcp.d.ts +3 -1
- package/dist/commands/install-mcp.js +53 -34
- package/dist/commands/list.d.ts +2 -1
- package/dist/commands/list.js +22 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/notify.d.ts +1 -0
- package/dist/commands/notify.js +51 -0
- package/dist/commands/patch-systemd.d.ts +7 -1
- package/dist/commands/patch-systemd.js +71 -31
- package/dist/commands/remove.d.ts +3 -1
- package/dist/commands/remove.js +37 -26
- package/dist/commands/restart.d.ts +4 -1
- package/dist/commands/restart.js +17 -20
- package/dist/commands/rollback.d.ts +4 -1
- package/dist/commands/rollback.js +33 -42
- package/dist/commands/secrets.js +157 -9
- package/dist/commands/start.d.ts +4 -1
- package/dist/commands/start.js +17 -20
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.js +21 -26
- package/dist/commands/stop.d.ts +4 -1
- package/dist/commands/stop.js +17 -20
- package/dist/commands/testflight.d.ts +1 -0
- package/dist/commands/testflight.js +193 -0
- package/dist/commands/update.d.ts +16 -0
- package/dist/commands/update.js +95 -0
- package/dist/core/audit/cache.d.ts +4 -0
- package/dist/core/audit/cache.js +37 -0
- package/dist/core/audit/config.d.ts +5 -0
- package/dist/core/audit/config.js +35 -0
- package/dist/core/audit/greenlight.d.ts +11 -0
- package/dist/core/audit/greenlight.js +81 -0
- package/dist/core/audit/reporters/cli.d.ts +3 -0
- package/dist/core/audit/reporters/cli.js +68 -0
- package/dist/core/audit/suppress.d.ts +6 -0
- package/dist/core/audit/suppress.js +37 -0
- package/dist/core/audit/target.d.ts +5 -0
- package/dist/core/audit/target.js +26 -0
- package/dist/core/audit/types.d.ts +54 -0
- package/dist/core/audit/types.js +5 -0
- package/dist/core/backup/browser-api.d.ts +66 -0
- package/dist/core/backup/browser-api.js +197 -0
- package/dist/core/backup/browser-server.d.ts +11 -0
- package/dist/core/backup/browser-server.js +241 -0
- package/dist/core/backup/browser-ui.d.ts +5 -0
- package/dist/core/backup/browser-ui.js +268 -0
- package/dist/core/backup/cloudflare.d.ts +7 -0
- package/dist/core/backup/cloudflare.js +82 -0
- package/dist/core/backup/config.d.ts +9 -0
- package/dist/core/backup/config.js +80 -0
- package/dist/core/backup/detect.d.ts +11 -0
- package/dist/core/backup/detect.js +71 -0
- package/dist/core/backup/dump.d.ts +11 -0
- package/dist/core/backup/dump.js +82 -0
- package/dist/core/backup/index.d.ts +9 -0
- package/dist/core/backup/index.js +9 -0
- package/dist/core/backup/repo.d.ts +71 -0
- package/dist/core/backup/repo.js +256 -0
- package/dist/core/backup/schedule.d.ts +17 -0
- package/dist/core/backup/schedule.js +90 -0
- package/dist/core/backup/sensitive.d.ts +5 -0
- package/dist/core/backup/sensitive.js +37 -0
- package/dist/core/backup/status.d.ts +3 -0
- package/dist/core/backup/status.js +29 -0
- package/dist/core/backup/statuspage.d.ts +23 -0
- package/dist/core/backup/statuspage.js +145 -0
- package/dist/core/backup/system.d.ts +24 -0
- package/dist/core/backup/system.js +209 -0
- package/dist/core/backup/totp.d.ts +16 -0
- package/dist/core/backup/totp.js +116 -0
- package/dist/core/backup/types.d.ts +70 -0
- package/dist/core/backup/types.js +7 -0
- package/dist/core/backup/unlock.d.ts +19 -0
- package/dist/core/backup/unlock.js +69 -0
- package/dist/core/boot-refresh.d.ts +1 -1
- package/dist/core/boot-refresh.js +10 -9
- package/dist/core/deps/actors/pr-creator.d.ts +5 -3
- package/dist/core/deps/actors/pr-creator.js +71 -18
- package/dist/core/deps/collectors/fetch-with-timeout.d.ts +7 -0
- package/dist/core/deps/collectors/fetch-with-timeout.js +16 -0
- package/dist/core/deps/collectors/npm.js +3 -1
- package/dist/core/deps/collectors/vulnerability.d.ts +8 -0
- package/dist/core/deps/collectors/vulnerability.js +31 -2
- package/dist/core/deps/config.js +6 -0
- package/dist/core/deps/scanner.js +1 -1
- package/dist/core/deps/types.d.ts +8 -0
- package/dist/core/env.d.ts +3 -0
- package/dist/core/env.js +11 -0
- package/dist/core/exec.d.ts +1 -0
- package/dist/core/exec.js +4 -0
- package/dist/core/file-lock.d.ts +18 -0
- package/dist/core/file-lock.js +44 -0
- package/dist/core/git-onboard.js +10 -13
- package/dist/core/github.d.ts +3 -1
- package/dist/core/github.js +10 -7
- package/dist/core/logs-policy.d.ts +5 -0
- package/dist/core/logs-policy.js +20 -1
- package/dist/core/operator.d.ts +21 -0
- package/dist/core/operator.js +54 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.js +26 -0
- package/dist/core/routines/schema.d.ts +11 -11
- package/dist/core/routines/schema.js +14 -3
- package/dist/core/routines/store.d.ts +8 -8
- package/dist/core/secrets-ops.d.ts +31 -6
- package/dist/core/secrets-ops.js +208 -102
- package/dist/core/secrets-providers.js +2 -2
- package/dist/core/secrets-rotation.d.ts +1 -1
- package/dist/core/secrets-rotation.js +58 -52
- package/dist/core/secrets-v2-cleanup.d.ts +19 -0
- package/dist/core/secrets-v2-cleanup.js +94 -0
- package/dist/core/secrets-v2-creds.d.ts +9 -0
- package/dist/core/secrets-v2-creds.js +44 -0
- package/dist/core/secrets-v2-install.d.ts +13 -0
- package/dist/core/secrets-v2-install.js +76 -0
- package/dist/core/secrets-v2-keypair.d.ts +10 -0
- package/dist/core/secrets-v2-keypair.js +31 -0
- package/dist/core/secrets-v2-migrate.d.ts +29 -0
- package/dist/core/secrets-v2-migrate.js +395 -0
- package/dist/core/secrets-v2-ops.d.ts +36 -0
- package/dist/core/secrets-v2-ops.js +184 -0
- package/dist/core/secrets-v2-protocol.d.ts +19 -0
- package/dist/core/secrets-v2-protocol.js +60 -0
- package/dist/core/secrets-v2-snapshot.d.ts +36 -0
- package/dist/core/secrets-v2-snapshot.js +115 -0
- package/dist/core/secrets-v2.d.ts +21 -0
- package/dist/core/secrets-v2.js +249 -0
- package/dist/core/secrets.d.ts +39 -4
- package/dist/core/secrets.js +91 -11
- package/dist/core/self-update.d.ts +32 -11
- package/dist/core/self-update.js +52 -14
- package/dist/core/testflight/asc.d.ts +12 -0
- package/dist/core/testflight/asc.js +101 -0
- package/dist/core/testflight/credentials.d.ts +3 -0
- package/dist/core/testflight/credentials.js +35 -0
- package/dist/core/testflight/eas.d.ts +4 -0
- package/dist/core/testflight/eas.js +38 -0
- package/dist/core/testflight/resolve.d.ts +6 -0
- package/dist/core/testflight/resolve.js +44 -0
- package/dist/core/testflight/types.d.ts +13 -0
- package/dist/core/testflight/types.js +3 -0
- package/dist/core/testflight/workflow.d.ts +17 -0
- package/dist/core/testflight/workflow.js +65 -0
- package/dist/core/validate.d.ts +1 -0
- package/dist/core/validate.js +8 -0
- package/dist/mcp/audit-tools.d.ts +2 -0
- package/dist/mcp/audit-tools.js +94 -0
- package/dist/mcp/git-tools.js +1 -1
- package/dist/mcp/registry-bridge.d.ts +10 -0
- package/dist/mcp/registry-bridge.js +65 -0
- package/dist/mcp/secrets-tools.js +2 -2
- package/dist/mcp/server.js +16 -82
- package/dist/mcp/testflight-tools.d.ts +2 -0
- package/dist/mcp/testflight-tools.js +52 -0
- package/dist/registry/context.d.ts +7 -0
- package/dist/registry/context.js +37 -0
- package/dist/registry/index.d.ts +5 -0
- package/dist/registry/index.js +44 -0
- package/dist/registry/parse-args.d.ts +13 -0
- package/dist/registry/parse-args.js +74 -0
- package/dist/registry/registry.d.ts +24 -0
- package/dist/registry/registry.js +26 -0
- package/dist/registry/render.d.ts +3 -0
- package/dist/registry/render.js +29 -0
- package/dist/registry/types.d.ts +50 -0
- package/dist/registry/types.js +1 -0
- package/dist/templates/agent-unit.d.ts +5 -0
- package/dist/templates/agent-unit.js +40 -0
- package/dist/templates/app-unit-edit.d.ts +2 -0
- package/dist/templates/app-unit-edit.js +46 -0
- package/dist/templates/compose-edit.d.ts +2 -0
- package/dist/templates/compose-edit.js +156 -0
- package/dist/templates/nginx.js +11 -0
- package/dist/templates/systemd.js +6 -0
- package/dist/tui/components/ArgForm.d.ts +7 -0
- package/dist/tui/components/ArgForm.js +64 -0
- package/dist/tui/components/ArgForm.test.d.ts +1 -0
- package/dist/tui/components/ArgForm.test.js +19 -0
- package/dist/tui/components/KeyHint.js +5 -0
- package/dist/tui/hooks/use-secrets.d.ts +8 -8
- package/dist/tui/hooks/use-secrets.js +7 -7
- package/dist/tui/router.d.ts +1 -0
- package/dist/tui/router.js +26 -9
- package/dist/tui/router.test.d.ts +1 -0
- package/dist/tui/router.test.js +13 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +2 -2
- package/dist/tui/routines/tabs/ScaffoldTab.js +1 -1
- package/dist/tui/tests/redaction-rerender.test.d.ts +1 -0
- package/dist/tui/tests/redaction-rerender.test.js +53 -0
- package/dist/tui/tests/scroll-flicker-proof.test.d.ts +1 -0
- package/dist/tui/tests/scroll-flicker-proof.test.js +145 -0
- package/dist/tui/types.d.ts +1 -1
- package/dist/tui/views/CommandPalette.d.ts +5 -0
- package/dist/tui/views/CommandPalette.js +90 -0
- package/dist/tui/views/CommandPalette.test.d.ts +1 -0
- package/dist/tui/views/CommandPalette.test.js +117 -0
- package/dist/tui/views/Dashboard.js +10 -7
- package/dist/tui/views/HealthView.js +14 -5
- package/dist/tui/views/SecretEdit.js +15 -16
- package/dist/tui/views/SecretEdit.test.d.ts +1 -0
- package/dist/tui/views/SecretEdit.test.js +82 -0
- package/dist/tui/views/SecretsView.js +26 -16
- package/package.json +9 -6
package/README.md
CHANGED
|
@@ -28,32 +28,43 @@ graph TD
|
|
|
28
28
|
TUI["TUI Dashboard"]
|
|
29
29
|
MCP["MCP Server"]
|
|
30
30
|
BOT["fleet-bot (Go)"]
|
|
31
|
+
Explorer["Backup Explorer<br/>(browser)"]
|
|
31
32
|
|
|
32
33
|
CLI --> Core
|
|
33
34
|
TUI --> Core
|
|
34
35
|
MCP --> Core
|
|
35
|
-
BOT -->|"
|
|
36
|
+
BOT -->|"MCP + exec"| Core
|
|
37
|
+
Explorer --> BackupCore["Backup core"]
|
|
36
38
|
|
|
37
|
-
subgraph Core["Core
|
|
39
|
+
subgraph Core["Core modules"]
|
|
38
40
|
Registry["Registry"]
|
|
39
41
|
Docker["Docker Compose"]
|
|
40
42
|
Systemd["systemd"]
|
|
41
43
|
Nginx["nginx"]
|
|
42
|
-
Secrets["Secrets
|
|
43
|
-
|
|
44
|
+
Secrets["Secrets vault"]
|
|
45
|
+
Agent["Secrets agent v2<br/>(per-app socket)"]
|
|
46
|
+
Health["Health checks"]
|
|
44
47
|
Git["Git / GitHub"]
|
|
45
|
-
Deps["Dependency
|
|
48
|
+
Deps["Dependency monitor"]
|
|
49
|
+
Routines["Routines<br/>(scheduled tasks)"]
|
|
50
|
+
BackupCore
|
|
51
|
+
Guard["Cloudflare guard"]
|
|
52
|
+
Mobile["Mobile (TestFlight + audit)"]
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
Docker --> Containers["Containers"]
|
|
49
|
-
Systemd --> Services["systemd
|
|
50
|
-
Nginx --> Proxy["Reverse
|
|
56
|
+
Systemd --> Services["systemd services"]
|
|
57
|
+
Nginx --> Proxy["Reverse proxy"]
|
|
51
58
|
Secrets --> Vault["vault/*.age"]
|
|
52
59
|
Secrets --> Runtime["/run/fleet-secrets"]
|
|
60
|
+
Agent --> Containers
|
|
53
61
|
Health --> Alerts["Telegram / iMessage"]
|
|
62
|
+
BackupCore --> Restic["restic + age<br/>(off-host)"]
|
|
54
63
|
```
|
|
55
64
|
|
|
56
|
-
Each Docker Compose app is registered with its compose path, domains, port, and container names. Fleet generates systemd units so apps start on boot in the correct order. Secrets are encrypted at rest with [age](https://github.com/FiloSottile/age) and decrypted to a tmpfs on boot.
|
|
65
|
+
Each Docker Compose app is registered with its compose path, domains, port, and container names. Fleet generates systemd units so apps start on boot in the correct order. Secrets are encrypted at rest with [age](https://github.com/FiloSottile/age) and either decrypted to a tmpfs on boot (v1) or served per-app over a Unix socket by the secrets agent (v2).
|
|
66
|
+
|
|
67
|
+
Operator-specific identity (GitHub org, home dir, domain, username) lives in `data/operator.json` — gitignored, instance-local. Copy `data/operator.example.json` to seed it on a fresh install.
|
|
57
68
|
|
|
58
69
|
## Install
|
|
59
70
|
|
|
@@ -77,8 +88,24 @@ Requires Node.js 20+, Docker Compose v2, systemd, nginx, and [age](https://githu
|
|
|
77
88
|
|
|
78
89
|
**Git workflows** -- Onboard apps to GitHub, manage branches, PRs, and releases from the CLI.
|
|
79
90
|
|
|
91
|
+
**Off-host backups** -- `fleet backup` runs restic against an append-only REST backend with age-encrypted dumps for databases. Includes `schedule` for systemd-timer-driven recurring backups, `verify` / `integrity` for repository checks, and `serve` for a browser-based restore explorer.
|
|
92
|
+
|
|
93
|
+
**Routines** -- `fleet routines` is a TUI for signal-based scheduled tasks (each routine has a target repo, a trigger condition, and a runner — claude-cli, shell, or mcp). `fleet routine-run --id <id>` is the headless entrypoint for systemd-timer units.
|
|
94
|
+
|
|
95
|
+
**Mobile pipelines** -- `fleet testflight` dispatches the macOS build workflow and publishes to TestFlight; `fleet audit` runs an App Store Review Guidelines audit via greenlight.
|
|
96
|
+
|
|
97
|
+
**Cloudflare guard** -- `fleet guard` installs a watchdog layer (cf-snapshot, dns-drift-watch, cert-expiry-watch, cf-audit-monitor) that detects unauthorised dashboard changes and DNS drift on protected zones.
|
|
98
|
+
|
|
80
99
|
**Interactive dashboard** -- Run bare `fleet` to launch a full-screen TUI with real-time status.
|
|
81
100
|
|
|
101
|
+
**Host preflight** -- `fleet doctor` checks every external requirement the rest of the tool assumes (`age`, `docker compose v2`, `systemd ≥ 240`, `node ≥ 20`), plus the registry, vault, operator config and orphaned-app detection. Run on any fresh server.
|
|
102
|
+
|
|
103
|
+
**Self-update from the CLI** -- `fleet update [--check] [--channel prerelease]` is the non-TUI counterpart to the dashboard banner. Cron-able, ssh-friendly.
|
|
104
|
+
|
|
105
|
+
**Operator config** -- `fleet config show / get <field> / set <field> <value>` and the one-liner `fleet whoami` keep `data/operator.json` (username + home + domain + github org) editable from the CLI.
|
|
106
|
+
|
|
107
|
+
**Shell completions** -- `fleet completions bash | zsh | fish` emits a completion script driven from the command registry, so it stays accurate as the migration finishes.
|
|
108
|
+
|
|
82
109
|
See the [CLI reference](https://fleet.hesketh.pro/cli/) for the complete command list.
|
|
83
110
|
|
|
84
111
|
## Secrets Flow
|
|
@@ -102,6 +129,16 @@ graph LR
|
|
|
102
129
|
|
|
103
130
|
Secrets are imported or set individually, encrypted with age, and stored in the vault. On boot (or manually), they are decrypted to a tmpfs mount that Docker containers reference. Sealing writes runtime changes back to the vault. Drift detection compares vault vs runtime to catch unsaved changes.
|
|
104
131
|
|
|
132
|
+
### Per-app secrets agent (v2, opt-in)
|
|
133
|
+
|
|
134
|
+
The v1 model decrypts every app's secrets to `/run/fleet-secrets/<app>/`, which means any process on the host can read the tmpfs file. v2 replaces that with a per-app systemd-templated socket service:
|
|
135
|
+
|
|
136
|
+
- Each app gets its own age keypair; the vault is encrypted to (admin + per-app) recipients.
|
|
137
|
+
- `fleet-secrets-agent@<app>.service` runs under `DynamicUser=yes`, loads the per-app key via `LoadCredentialEncrypted`, and serves the decrypted env over a Unix socket at `/run/fleet-secrets/<app>.sock`.
|
|
138
|
+
- Consumers (the container) fetch secrets via HTTP/1.1 over the socket. The [`@matthesketh/fleet-secrets-client`](https://www.npmjs.com/package/@matthesketh/fleet-secrets-client) package wraps that protocol for Node apps.
|
|
139
|
+
- `fleet secrets migrate-v2 <app>` orchestrates the move: snapshot, generate per-app keypair, re-encrypt to the new recipient set, edit the compose file + app unit, swap, and auto-rollback on any failure.
|
|
140
|
+
- `fleet secrets revert-v2 <app>` rolls back from a snapshot if you need to drop back to v1.
|
|
141
|
+
|
|
105
142
|
### Per-secret rotation (v1.6)
|
|
106
143
|
|
|
107
144
|
Each secret carries metadata (`lastRotated`, `provider`, `strategy`) so fleet knows when it's stale and how to safely rotate it.
|
|
@@ -166,6 +203,107 @@ v1 is **observe-only** — it never blocks packets, so zero risk of breaking app
|
|
|
166
203
|
|
|
167
204
|
`enforce` mode (actual default-deny via nftables) is deferred to a future phase — by design, it requires the operator to explicitly promote a shadow-clean app, never auto-promotes.
|
|
168
205
|
|
|
206
|
+
## Backups (off-host)
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
fleet backup init # initialise the restic repository
|
|
210
|
+
fleet backup register <app> [--paths …] # tell fleet which paths and dbs to capture
|
|
211
|
+
fleet backup register-all # bulk-register every known app
|
|
212
|
+
fleet backup snapshot <app> [--tag …] # take an on-demand snapshot
|
|
213
|
+
fleet backup snapshot-all # one snapshot per registered app
|
|
214
|
+
fleet backup list [<app>] # list snapshots, latest first
|
|
215
|
+
fleet backup restore <app> --snap <id> --target <dir>
|
|
216
|
+
fleet backup prune # apply retention policy
|
|
217
|
+
fleet backup verify # restic check (data integrity)
|
|
218
|
+
fleet backup integrity # repository integrity report
|
|
219
|
+
fleet backup schedule <app> --cron "..." # install a fleet-backup@<app>.timer unit
|
|
220
|
+
fleet backup schedule-all # bulk-install timers from registered apps
|
|
221
|
+
fleet backup unschedule <app> # remove the timer
|
|
222
|
+
fleet backup status [<app>] # last-snapshot age, sizes, append-only state
|
|
223
|
+
fleet backup serve --port <n> # browser-based restore explorer (see below)
|
|
224
|
+
fleet backup test # end-to-end snapshot+restore smoke
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Snapshots go to a restic backend mounted with `append-only` mode so a compromised host can't delete history. Database dumps stream straight into the snapshot via `dumpFileSpawn`, never landing on disk in plaintext. Backups are encrypted twice — restic at rest, plus per-app age encryption inside the snapshot for anything classified as sensitive.
|
|
228
|
+
|
|
229
|
+
### Restore explorer
|
|
230
|
+
|
|
231
|
+
`fleet backup serve --port 7300` starts a localhost-bound HTTP service designed to live behind nginx. It serves:
|
|
232
|
+
|
|
233
|
+
- a read-only status dashboard at `/backups` showing the latest snapshot per app and any retention lag
|
|
234
|
+
- a tree explorer at `/backups/explore` for browsing snapshot contents
|
|
235
|
+
- a per-file streaming endpoint that pipes `restic dump` straight to the browser
|
|
236
|
+
- a one-click restore into a fresh timestamped staging dir under `/var/restore`
|
|
237
|
+
|
|
238
|
+
Authentication is **TOTP only** — paste the secret into your authenticator app on setup (`fleet backup setup-totp`). Sessions are signed cookies (`fleet_backup_session`), `Secure; HttpOnly; SameSite=Strict; Path=/backups`. Sensitive paths (`.env`, age keys, private SSH keys) are classified server-side and refuse view/download regardless of who's logged in.
|
|
239
|
+
|
|
240
|
+
CSRF posture: every `/api/*` request must carry `x-fleet-backup: 1` (a custom header browsers can't set cross-origin without preflight), and write methods (POST / DELETE) must additionally carry an `Origin` header whose host matches the deployment domain exactly. Read methods tolerate a missing `Origin` so curl probes still work.
|
|
241
|
+
|
|
242
|
+
## Routines
|
|
243
|
+
|
|
244
|
+
`fleet routines` is a TUI for signal-based scheduled tasks. Each routine has:
|
|
245
|
+
|
|
246
|
+
- a **target repo** the routine operates against
|
|
247
|
+
- a **trigger condition** (signals: open issues count, failing checks, branch ahead of remote, custom git-clean signal, scheduled, manual)
|
|
248
|
+
- a **runner**: `claude-cli` (drives a Claude Code session), `shell` (just runs a script), or `mcp-call` (invokes one MCP tool)
|
|
249
|
+
- a **schema** that validates inputs before the runner sees them
|
|
250
|
+
|
|
251
|
+
Routines run from a systemd-timer-driven service called `fleet-routine@<id>.timer` — fleet ships the templates and `fleet routine-run --id <id>` is the headless entrypoint that timer fires. The TUI shows a signals grid (which routines are gated on what), a routine list, and per-routine run history.
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
fleet routines # interactive TUI
|
|
255
|
+
fleet routine-run --id <id> [--target <repo>] [--trigger scheduled] [--json]
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The claude-cli runner serialises through a `proper-lockfile` mutex so two routines targeting the same repo can't race each other, and respects an abort signal so a `systemctl stop` cleans up mid-run rather than orphaning a child process.
|
|
259
|
+
|
|
260
|
+
## Mobile pipelines
|
|
261
|
+
|
|
262
|
+
For iOS/macOS apps, fleet drives both the build pipeline and the App Store compliance check.
|
|
263
|
+
|
|
264
|
+
### TestFlight publishing
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
fleet testflight publish <app> # dispatch the macOS build workflow to TestFlight
|
|
268
|
+
fleet testflight builds <app> # list TestFlight builds for the app
|
|
269
|
+
fleet testflight update <app> --build <id> --whats-new "..."
|
|
270
|
+
fleet testflight delete <app> --build <id> # expire a TestFlight build
|
|
271
|
+
fleet testflight doctor <app> # check gh + App Store Connect credentials
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The publish flow goes through a GitHub Actions macOS runner — fleet dispatches the workflow with the app's bundle ID and version, then polls until the build appears in App Store Connect. Auth uses an API key (issuer + key ID + p8) loaded via a fleet-managed secret.
|
|
275
|
+
|
|
276
|
+
### App Store compliance audit
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
fleet audit [target] # run greenlight against a mobile project
|
|
280
|
+
fleet audit guidelines # browse App Store Review Guidelines (list/show/search)
|
|
281
|
+
fleet audit doctor # check the greenlight binary is installed
|
|
282
|
+
fleet audit ignore "<title>" --reason "..." # suppress a greenlight false positive
|
|
283
|
+
fleet audit ignores # list audit ignore rules
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Greenlight runs a corpus of App Store Review Guideline checks against the project source. Findings classified as "confirmed false positive" are suppressed via the ignore list so the next run is noise-free; everything else fails the audit. Useful as a pre-flight before each TestFlight publish.
|
|
287
|
+
|
|
288
|
+
## Cloudflare guard
|
|
289
|
+
|
|
290
|
+
`fleet guard` installs a watchdog layer that detects unauthorised dashboard changes and DNS drift on protected Cloudflare zones.
|
|
291
|
+
|
|
292
|
+
```
|
|
293
|
+
sudo fleet guard install # install scripts + cron + log rotation
|
|
294
|
+
fleet guard status # show what's protected and the last check time
|
|
295
|
+
fleet guard approve <change-id> # acknowledge a flagged change as intentional
|
|
296
|
+
fleet guard reject <change-id> # treat a flagged change as compromise
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Components installed under `/usr/local/sbin`:
|
|
300
|
+
|
|
301
|
+
- **`cf-snapshot`** — periodic snapshot of the Cloudflare zone configuration (DNS, page rules, WAF settings) to `/var/lib/cf-snapshots/`
|
|
302
|
+
- **`cf-audit-monitor`** — diffs the latest snapshot against the previous and surfaces unauthorised changes for operator approval
|
|
303
|
+
- **`dns-drift-watch`** — detects when DNS records drift from the snapshot (cron, alerts via the same channels as `fleet watchdog`)
|
|
304
|
+
- **`cert-expiry-watch`** — flags certs approaching expiry across all protected hosts
|
|
305
|
+
- **`fleet-guard` / `fleet-guard-execute`** — the orchestrator + executor pair, run as the `fleet-guard` system user with no shell
|
|
306
|
+
|
|
169
307
|
## Deployment Flow
|
|
170
308
|
|
|
171
309
|
```mermaid
|
|
@@ -224,7 +362,7 @@ Any app with `lastBuiltCommit` unset will trigger a full rebuild the first time
|
|
|
224
362
|
|
|
225
363
|
## MCP Server
|
|
226
364
|
|
|
227
|
-
Fleet exposes
|
|
365
|
+
Fleet exposes 50+ tools via the [Model Context Protocol](https://modelcontextprotocol.io/) for AI-assisted server management — the static surface (server.ts + git / secrets / deps / audit / testflight tool families) plus every migrated registry command exposed through the registry bridge. Run `fleet mcp` to start the stdio server, or install it into Claude Code:
|
|
228
366
|
|
|
229
367
|
```bash
|
|
230
368
|
sudo fleet install-mcp
|
|
@@ -240,19 +378,48 @@ See the [bot documentation](https://fleet.hesketh.pro/bot/setup/) for setup inst
|
|
|
240
378
|
|
|
241
379
|
## Self-update
|
|
242
380
|
|
|
243
|
-
When `fleet`'s TUI launches it does a non-blocking `git fetch` against
|
|
381
|
+
When `fleet`'s TUI launches it does a non-blocking `git fetch` against the configured update channel and compares HEAD to that remote branch. If the local repo is behind, a banner appears under the header:
|
|
244
382
|
|
|
245
383
|
```
|
|
246
384
|
↑ Update available: 3 commits ahead — feat: ... Press U to install.
|
|
247
385
|
```
|
|
248
386
|
|
|
249
|
-
Pressing `U` runs `git pull --ff-only
|
|
387
|
+
Pressing `U` runs `git pull --ff-only origin <channel-branch>` then `npm run build` (refused if the working tree is dirty). The new binary is live for the next `fleet …` invocation. Recheck happens every 30 minutes for long-running TUI sessions.
|
|
388
|
+
|
|
389
|
+
**Channels**
|
|
390
|
+
|
|
391
|
+
| channel | tracks | who it's for |
|
|
392
|
+
|---|---|---|
|
|
393
|
+
| `stable` (default) | `origin/main` — tagged releases only | everyone |
|
|
394
|
+
| `prerelease` | `origin/develop` — work in flight, may break | operators willing to canary |
|
|
395
|
+
|
|
396
|
+
Opt into prerelease for the current process:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
FLEET_UPDATE_CHANNEL=prerelease fleet
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Or persist it (e.g. in `~/.bashrc` or the systemd unit's `Environment=`):
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
export FLEET_UPDATE_CHANNEL=prerelease
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
When the banner is on the prerelease channel it labels itself `↑ Update available (prerelease): …` so you can tell at a glance.
|
|
409
|
+
|
|
410
|
+
**Escape hatch — track an arbitrary branch**
|
|
411
|
+
|
|
412
|
+
For forks or release branches, `FLEET_UPDATE_BRANCH=<name>` overrides the channel entirely:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
FLEET_UPDATE_BRANCH=release/2026.q3 fleet
|
|
416
|
+
```
|
|
250
417
|
|
|
251
418
|
## Testing
|
|
252
419
|
|
|
253
420
|
```bash
|
|
254
|
-
npm test # unit + mocked tests (
|
|
255
|
-
FLEET_INTEGRATION=1 npm test # also runs boot-refresh integration tests
|
|
421
|
+
npm test # unit + mocked tests (~1720 passing)
|
|
422
|
+
FLEET_INTEGRATION=1 npm test # also runs boot-refresh + secrets-v2 integration tests
|
|
256
423
|
```
|
|
257
424
|
|
|
258
425
|
Set `FLEET_INTEGRATION=1` to opt into integration tests that hit real systemd / docker. Skipped by default in CI.
|
|
@@ -263,11 +430,14 @@ Set `FLEET_INTEGRATION=1` to opt into integration tests that hit real systemd /
|
|
|
263
430
|
git clone https://github.com/wrxck/fleet.git
|
|
264
431
|
cd fleet
|
|
265
432
|
npm install
|
|
266
|
-
npm test
|
|
267
|
-
npm run build
|
|
268
|
-
npm run dev
|
|
433
|
+
npm test # vitest
|
|
434
|
+
npm run build # compile TypeScript to dist/
|
|
435
|
+
npm run dev # run with tsx (no build needed)
|
|
436
|
+
npm run changelog # regenerate CHANGELOG.md from git tags
|
|
269
437
|
```
|
|
270
438
|
|
|
439
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the release history (auto-generated from tags; the GitHub releases page has extra context). The `npm run changelog` script regenerates it after a tag bump.
|
|
440
|
+
|
|
271
441
|
## License
|
|
272
442
|
|
|
273
443
|
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from '../core/secrets-v2.js';
|
|
3
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
4
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5
|
+
process.stderr.write(`[fleet-agent] fatal: ${message}\n`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
package/dist/cli.d.ts
CHANGED
|
@@ -1 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolves a command from the registry and runs it. returns true when handled,
|
|
3
|
+
* false when the name is unknown (so run() falls through to the legacy switch).
|
|
4
|
+
*/
|
|
5
|
+
export declare function dispatchRegistryCommand(command: string, rest: string[], write?: (s: string) => void): Promise<boolean>;
|
|
1
6
|
export declare function run(argv: string[]): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
4
|
+
import { loadRegistry } from './registry/index.js';
|
|
5
|
+
import { getCommand } from './registry/registry.js';
|
|
6
|
+
import { parseArgs } from './registry/parse-args.js';
|
|
7
|
+
import { renderToText } from './registry/render.js';
|
|
8
|
+
import { makeCliContext } from './registry/context.js';
|
|
9
9
|
import { logsCommand } from './commands/logs.js';
|
|
10
10
|
import { egressCommand } from './commands/egress.js';
|
|
11
|
-
import { healthCommand } from './commands/health.js';
|
|
12
|
-
import { addCommand } from './commands/add.js';
|
|
13
|
-
import { removeCommand } from './commands/remove.js';
|
|
14
11
|
import { deployCommand } from './commands/deploy.js';
|
|
15
12
|
import { nginxCommand } from './commands/nginx.js';
|
|
16
13
|
import { secretsCommand } from './commands/secrets.js';
|
|
17
14
|
import { gitCommand } from './commands/git.js';
|
|
18
|
-
import { initCommand } from './commands/init.js';
|
|
19
15
|
import { depsCommand } from './commands/deps.js';
|
|
16
|
+
import { auditCommand } from './commands/audit.js';
|
|
17
|
+
import { testflightCommand } from './commands/testflight.js';
|
|
20
18
|
import { watchdogCommand } from './commands/watchdog.js';
|
|
21
|
-
import { installMcpCommand } from './commands/install-mcp.js';
|
|
22
|
-
import { patchSystemdCommand } from './commands/patch-systemd.js';
|
|
23
|
-
import { freezeCommand, unfreezeCommand } from './commands/freeze.js';
|
|
24
19
|
import { guardCommand } from './commands/guard.js';
|
|
25
|
-
import {
|
|
26
|
-
import { rollbackCommand } from './commands/rollback.js';
|
|
20
|
+
import { backupCommand } from './commands/backup.js';
|
|
27
21
|
import { routineRunCommand } from './commands/routine-run.js';
|
|
28
22
|
import { routinesCommand } from './commands/routines.js';
|
|
29
23
|
import { startMcpServer } from './mcp/server.js';
|
|
@@ -50,6 +44,16 @@ Commands:
|
|
|
50
44
|
deps config Show/set configuration
|
|
51
45
|
deps ignore <pkg> Suppress a finding
|
|
52
46
|
deps init Install cron + MOTD for automated scanning
|
|
47
|
+
audit [target] App Store compliance audit of a mobile project (greenlight)
|
|
48
|
+
audit guidelines Browse Apple App Store Review Guidelines (list|show|search)
|
|
49
|
+
audit doctor Check the greenlight binary is installed
|
|
50
|
+
audit ignore "<title>" --reason "..." Suppress a greenlight false positive
|
|
51
|
+
audit ignores List audit ignore rules
|
|
52
|
+
testflight publish <app> Dispatch the macOS build workflow to TestFlight
|
|
53
|
+
testflight builds <app> List TestFlight builds
|
|
54
|
+
testflight update <app> --build <id> --whats-new "..." Set test notes
|
|
55
|
+
testflight delete <app> --build <id> Expire a TestFlight build
|
|
56
|
+
testflight doctor <app> Check gh + App Store Connect credentials
|
|
53
57
|
add <app-dir> Register existing app
|
|
54
58
|
remove <app> Stop, disable, deregister
|
|
55
59
|
nginx add <domain> --port <port> [--type proxy|spa|nextjs]
|
|
@@ -91,6 +95,14 @@ Commands:
|
|
|
91
95
|
rollback <app> Roll back app to previous image
|
|
92
96
|
unfreeze <app> Unfreeze and restart a frozen service
|
|
93
97
|
guard <subcommand> Cloudflare protection layer (install/status/approve/reject/...)
|
|
98
|
+
backup <subcommand> Encrypted off-host backups via restic + age (init/snapshot/list/restore/...)
|
|
99
|
+
update [--check] [--channel stable|prerelease] [--branch <name>]
|
|
100
|
+
Self-update fleet (check / apply, channel selectable)
|
|
101
|
+
doctor Preflight: host requirements, registry, vault, operator config, orphans
|
|
102
|
+
config [show|get|set] [<field>] [<value>]
|
|
103
|
+
Show or update the operator identity (data/operator.json)
|
|
104
|
+
whoami Print operator identity in one line
|
|
105
|
+
completions <shell> Emit shell completion script (bash | zsh | fish)
|
|
94
106
|
|
|
95
107
|
Global flags:
|
|
96
108
|
--json Output as JSON
|
|
@@ -99,6 +111,45 @@ Global flags:
|
|
|
99
111
|
-v, --version Show version
|
|
100
112
|
-h, --help Show this help
|
|
101
113
|
`;
|
|
114
|
+
/**
|
|
115
|
+
* resolves a command from the registry and runs it. returns true when handled,
|
|
116
|
+
* false when the name is unknown (so run() falls through to the legacy switch).
|
|
117
|
+
*/
|
|
118
|
+
export async function dispatchRegistryCommand(command, rest, write = s => process.stdout.write(s)) {
|
|
119
|
+
loadRegistry();
|
|
120
|
+
const def = getCommand(command);
|
|
121
|
+
if (!def)
|
|
122
|
+
return false;
|
|
123
|
+
// --json is an output flag for the registry dispatch path — handled here,
|
|
124
|
+
// not a per-command argument, so it is stripped before the schema parse
|
|
125
|
+
// would reject it as unknown. legacy (non-registry) commands that still
|
|
126
|
+
// live in the switch below parse --json themselves.
|
|
127
|
+
const jsonMode = rest.includes('--json');
|
|
128
|
+
const cmdArgs = rest.filter(arg => arg !== '--json');
|
|
129
|
+
const parsed = parseArgs(def.args, cmdArgs);
|
|
130
|
+
if (parsed.help) {
|
|
131
|
+
// minimal help for now — one-line summary; richer per-command help is future work.
|
|
132
|
+
write(`${def.name} — ${def.summary}\n`);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (!parsed.ok) {
|
|
136
|
+
process.stderr.write(`error: ${parsed.error}\n`);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
const result = await def.run(parsed.values, makeCliContext());
|
|
141
|
+
if (jsonMode) {
|
|
142
|
+
write(JSON.stringify(result.data, null, 2) + '\n');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (result.render)
|
|
146
|
+
write(renderToText(result.render) + '\n');
|
|
147
|
+
write(result.summary + '\n');
|
|
148
|
+
}
|
|
149
|
+
if (!result.ok)
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
102
153
|
export async function run(argv) {
|
|
103
154
|
const args = argv.slice(2);
|
|
104
155
|
const command = args[0];
|
|
@@ -115,40 +166,31 @@ export async function run(argv) {
|
|
|
115
166
|
const { launchTui } = await import('./tui/app.js');
|
|
116
167
|
return launchTui();
|
|
117
168
|
}
|
|
118
|
-
//
|
|
169
|
+
// commands that require root privileges
|
|
119
170
|
const ROOT_COMMANDS = new Set([
|
|
120
171
|
'start', 'stop', 'restart', 'deploy', 'freeze', 'unfreeze',
|
|
121
|
-
'nginx', 'secrets', 'patch-systemd', 'init', 'watchdog',
|
|
172
|
+
'nginx', 'secrets', 'patch-systemd', 'init', 'watchdog', 'backup',
|
|
173
|
+
'testflight',
|
|
122
174
|
]);
|
|
123
175
|
if (ROOT_COMMANDS.has(command) && process.getuid && process.getuid() !== 0) {
|
|
124
176
|
error(`'fleet ${command}' requires root privileges. Run with sudo.`);
|
|
125
177
|
process.exit(1);
|
|
126
178
|
}
|
|
179
|
+
if (await dispatchRegistryCommand(command, rest))
|
|
180
|
+
return;
|
|
127
181
|
switch (command) {
|
|
128
|
-
case 'status': return statusCommand(rest);
|
|
129
|
-
case 'list': return listCommand(rest);
|
|
130
|
-
case 'start': return startCommand(rest);
|
|
131
|
-
case 'stop': return stopCommand(rest);
|
|
132
|
-
case 'restart': return restartCommand(rest);
|
|
133
182
|
case 'logs': return logsCommand(rest);
|
|
134
183
|
case 'egress': return egressCommand(rest);
|
|
135
|
-
case 'health': return healthCommand(rest);
|
|
136
184
|
case 'deps': return depsCommand(rest);
|
|
137
|
-
case '
|
|
138
|
-
case '
|
|
185
|
+
case 'audit': return auditCommand(rest);
|
|
186
|
+
case 'testflight': return testflightCommand(rest);
|
|
139
187
|
case 'deploy': return deployCommand(rest);
|
|
140
188
|
case 'nginx': return nginxCommand(rest);
|
|
141
189
|
case 'secrets': return secretsCommand(rest);
|
|
142
190
|
case 'git': return gitCommand(rest);
|
|
143
|
-
case 'init': return initCommand(rest);
|
|
144
191
|
case 'watchdog': return watchdogCommand(rest);
|
|
145
|
-
case 'install-mcp': return installMcpCommand(rest);
|
|
146
|
-
case 'patch-systemd': return patchSystemdCommand(rest);
|
|
147
|
-
case 'boot-start': return bootStartCommand(rest);
|
|
148
|
-
case 'freeze': return freezeCommand(rest);
|
|
149
|
-
case 'rollback': return rollbackCommand(rest);
|
|
150
|
-
case 'unfreeze': return unfreezeCommand(rest);
|
|
151
192
|
case 'guard': return guardCommand(rest);
|
|
193
|
+
case 'backup': return backupCommand(rest);
|
|
152
194
|
case 'mcp': return startMcpServer();
|
|
153
195
|
case 'tui':
|
|
154
196
|
case 'dashboard': {
|
package/dist/commands/add.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { AppEntry } from '../core/registry.js';
|
|
2
|
+
export declare const addCommand: import("../registry/types.js").CommandDef<AppEntry | null>;
|
package/dist/commands/add.js
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { resolve, basename } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { addApp, withRegistry } from '../core/registry.js';
|
|
4
5
|
import { getContainersByCompose } from '../core/docker.js';
|
|
5
6
|
import { installServiceFile, readServiceFile, enableService } from '../core/systemd.js';
|
|
6
7
|
import { generateServiceFile } from '../templates/systemd.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!dryRun && (yes || await confirm('Create systemd service file?'))) {
|
|
8
|
+
import { assertComposeFile } from '../core/validate.js';
|
|
9
|
+
import { defineCommand } from '../registry/registry.js';
|
|
10
|
+
export const addCommand = defineCommand({
|
|
11
|
+
name: 'add',
|
|
12
|
+
summary: 'Register an existing app',
|
|
13
|
+
args: z.object({
|
|
14
|
+
dir: z.string(),
|
|
15
|
+
'dry-run': z.boolean().default(false),
|
|
16
|
+
yes: z.boolean().default(false),
|
|
17
|
+
}),
|
|
18
|
+
async run(args, ctx) {
|
|
19
|
+
const fullPath = resolve(args.dir);
|
|
20
|
+
if (!existsSync(fullPath)) {
|
|
21
|
+
return { ok: false, summary: `directory not found: ${fullPath}`, data: null };
|
|
22
|
+
}
|
|
23
|
+
const composePath = findComposePath(fullPath);
|
|
24
|
+
if (!composePath.path) {
|
|
25
|
+
return { ok: false, summary: `no docker-compose.yml found in ${fullPath} or ${fullPath}/server`, data: null };
|
|
26
|
+
}
|
|
27
|
+
const name = basename(fullPath).toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
28
|
+
const hasService = readServiceFile(name) !== null;
|
|
29
|
+
ctx.log({ level: 'info', message: `registering ${name} from ${fullPath}` });
|
|
30
|
+
const containers = getContainersByCompose(composePath.path, composePath.file);
|
|
31
|
+
const app = {
|
|
32
|
+
name,
|
|
33
|
+
displayName: name,
|
|
34
|
+
composePath: composePath.path,
|
|
35
|
+
composeFile: composePath.file,
|
|
36
|
+
serviceName: name,
|
|
37
|
+
domains: [],
|
|
38
|
+
port: null,
|
|
39
|
+
usesSharedDb: false,
|
|
40
|
+
type: 'service',
|
|
41
|
+
containers: containers.length > 0 ? containers : [name],
|
|
42
|
+
dependsOnDatabases: false,
|
|
43
|
+
registeredAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
const dryRun = args['dry-run'];
|
|
46
|
+
if (!hasService && !dryRun && (args.yes || (await ctx.confirm('Create systemd service file?')))) {
|
|
47
|
+
// defence-in-depth: validate the compose filename before interpolating
|
|
48
|
+
// it into the generated systemd unit.
|
|
49
|
+
if (composePath.file)
|
|
50
|
+
assertComposeFile(composePath.file);
|
|
51
51
|
const content = generateServiceFile({
|
|
52
52
|
serviceName: name,
|
|
53
53
|
description: `${name} Docker Service`,
|
|
@@ -57,21 +57,28 @@ export async function addCommand(args) {
|
|
|
57
57
|
});
|
|
58
58
|
installServiceFile(name, content);
|
|
59
59
|
enableService(name);
|
|
60
|
-
|
|
60
|
+
ctx.log({ level: 'info', message: `created and enabled ${name}.service` });
|
|
61
61
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
if (dryRun) {
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
summary: `dry run — ${name} not registered`,
|
|
66
|
+
data: app,
|
|
67
|
+
render: {
|
|
68
|
+
kind: 'keyValue',
|
|
69
|
+
pairs: [
|
|
70
|
+
['name', app.name],
|
|
71
|
+
['composePath', app.composePath],
|
|
72
|
+
['composeFile', app.composeFile ?? '(default)'],
|
|
73
|
+
['containers', app.containers.join(', ')],
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
await withRegistry(reg => addApp(reg, app));
|
|
79
|
+
return { ok: true, summary: `registered ${name}`, data: app };
|
|
80
|
+
},
|
|
81
|
+
});
|
|
75
82
|
function findComposePath(dir) {
|
|
76
83
|
if (existsSync(`${dir}/docker-compose.yml`)) {
|
|
77
84
|
return { path: dir, file: null };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function auditCommand(args: string[]): Promise<void>;
|