@matthesketh/fleet 1.0.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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/data/registry.example.json +13 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +113 -0
  6. package/dist/commands/add.d.ts +1 -0
  7. package/dist/commands/add.js +95 -0
  8. package/dist/commands/deploy.d.ts +1 -0
  9. package/dist/commands/deploy.js +53 -0
  10. package/dist/commands/git.d.ts +1 -0
  11. package/dist/commands/git.js +278 -0
  12. package/dist/commands/health.d.ts +1 -0
  13. package/dist/commands/health.js +60 -0
  14. package/dist/commands/init.d.ts +1 -0
  15. package/dist/commands/init.js +157 -0
  16. package/dist/commands/install-mcp.d.ts +1 -0
  17. package/dist/commands/install-mcp.js +55 -0
  18. package/dist/commands/list.d.ts +1 -0
  19. package/dist/commands/list.js +20 -0
  20. package/dist/commands/logs.d.ts +1 -0
  21. package/dist/commands/logs.js +32 -0
  22. package/dist/commands/nginx.d.ts +1 -0
  23. package/dist/commands/nginx.js +94 -0
  24. package/dist/commands/remove.d.ts +1 -0
  25. package/dist/commands/remove.js +28 -0
  26. package/dist/commands/restart.d.ts +1 -0
  27. package/dist/commands/restart.js +22 -0
  28. package/dist/commands/secrets.d.ts +1 -0
  29. package/dist/commands/secrets.js +268 -0
  30. package/dist/commands/start.d.ts +1 -0
  31. package/dist/commands/start.js +22 -0
  32. package/dist/commands/status.d.ts +14 -0
  33. package/dist/commands/status.js +70 -0
  34. package/dist/commands/stop.d.ts +1 -0
  35. package/dist/commands/stop.js +22 -0
  36. package/dist/commands/watchdog.d.ts +1 -0
  37. package/dist/commands/watchdog.js +100 -0
  38. package/dist/core/docker.d.ts +15 -0
  39. package/dist/core/docker.js +72 -0
  40. package/dist/core/errors.d.ts +20 -0
  41. package/dist/core/errors.js +40 -0
  42. package/dist/core/exec.d.ts +14 -0
  43. package/dist/core/exec.js +30 -0
  44. package/dist/core/git-onboard.d.ts +11 -0
  45. package/dist/core/git-onboard.js +149 -0
  46. package/dist/core/git.d.ts +36 -0
  47. package/dist/core/git.js +155 -0
  48. package/dist/core/github.d.ts +22 -0
  49. package/dist/core/github.js +92 -0
  50. package/dist/core/health.d.ts +29 -0
  51. package/dist/core/health.js +56 -0
  52. package/dist/core/nginx.d.ts +17 -0
  53. package/dist/core/nginx.js +59 -0
  54. package/dist/core/registry.d.ts +38 -0
  55. package/dist/core/registry.js +47 -0
  56. package/dist/core/secrets-ops.d.ts +37 -0
  57. package/dist/core/secrets-ops.js +331 -0
  58. package/dist/core/secrets-validate.d.ts +8 -0
  59. package/dist/core/secrets-validate.js +81 -0
  60. package/dist/core/secrets.d.ts +36 -0
  61. package/dist/core/secrets.js +191 -0
  62. package/dist/core/systemd.d.ts +23 -0
  63. package/dist/core/systemd.js +106 -0
  64. package/dist/index.d.ts +2 -0
  65. package/dist/index.js +18 -0
  66. package/dist/mcp/git-tools.d.ts +2 -0
  67. package/dist/mcp/git-tools.js +148 -0
  68. package/dist/mcp/secrets-tools.d.ts +2 -0
  69. package/dist/mcp/secrets-tools.js +67 -0
  70. package/dist/mcp/server.d.ts +1 -0
  71. package/dist/mcp/server.js +179 -0
  72. package/dist/templates/gitignore.d.ts +3 -0
  73. package/dist/templates/gitignore.js +89 -0
  74. package/dist/templates/nginx.d.ts +8 -0
  75. package/dist/templates/nginx.js +111 -0
  76. package/dist/templates/systemd.d.ts +9 -0
  77. package/dist/templates/systemd.js +26 -0
  78. package/dist/templates/unseal.d.ts +1 -0
  79. package/dist/templates/unseal.js +22 -0
  80. package/dist/tui/app.d.ts +1 -0
  81. package/dist/tui/app.js +9 -0
  82. package/dist/tui/components/AppList.d.ts +12 -0
  83. package/dist/tui/components/AppList.js +32 -0
  84. package/dist/tui/components/Confirm.d.ts +2 -0
  85. package/dist/tui/components/Confirm.js +10 -0
  86. package/dist/tui/components/Header.d.ts +6 -0
  87. package/dist/tui/components/Header.js +16 -0
  88. package/dist/tui/components/KeyHint.d.ts +2 -0
  89. package/dist/tui/components/KeyHint.js +55 -0
  90. package/dist/tui/components/StatusBadge.d.ts +7 -0
  91. package/dist/tui/components/StatusBadge.js +8 -0
  92. package/dist/tui/exec-bridge.d.ts +11 -0
  93. package/dist/tui/exec-bridge.js +57 -0
  94. package/dist/tui/hooks/use-fleet-data.d.ts +9 -0
  95. package/dist/tui/hooks/use-fleet-data.js +30 -0
  96. package/dist/tui/hooks/use-health.d.ts +9 -0
  97. package/dist/tui/hooks/use-health.js +29 -0
  98. package/dist/tui/hooks/use-interval.d.ts +1 -0
  99. package/dist/tui/hooks/use-interval.js +13 -0
  100. package/dist/tui/hooks/use-keyboard.d.ts +1 -0
  101. package/dist/tui/hooks/use-keyboard.js +44 -0
  102. package/dist/tui/hooks/use-secrets.d.ts +47 -0
  103. package/dist/tui/hooks/use-secrets.js +152 -0
  104. package/dist/tui/router.d.ts +2 -0
  105. package/dist/tui/router.js +65 -0
  106. package/dist/tui/state.d.ts +12 -0
  107. package/dist/tui/state.js +83 -0
  108. package/dist/tui/theme.d.ts +11 -0
  109. package/dist/tui/theme.js +23 -0
  110. package/dist/tui/types.d.ts +41 -0
  111. package/dist/tui/types.js +1 -0
  112. package/dist/tui/views/AppDetail.d.ts +2 -0
  113. package/dist/tui/views/AppDetail.js +72 -0
  114. package/dist/tui/views/Dashboard.d.ts +2 -0
  115. package/dist/tui/views/Dashboard.js +29 -0
  116. package/dist/tui/views/HealthView.d.ts +2 -0
  117. package/dist/tui/views/HealthView.js +28 -0
  118. package/dist/tui/views/LogsView.d.ts +2 -0
  119. package/dist/tui/views/LogsView.js +71 -0
  120. package/dist/tui/views/SecretEdit.d.ts +2 -0
  121. package/dist/tui/views/SecretEdit.js +53 -0
  122. package/dist/tui/views/SecretsView.d.ts +2 -0
  123. package/dist/tui/views/SecretsView.js +108 -0
  124. package/dist/ui/confirm.d.ts +1 -0
  125. package/dist/ui/confirm.js +15 -0
  126. package/dist/ui/output.d.ts +27 -0
  127. package/dist/ui/output.js +61 -0
  128. package/package.json +64 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Hesketh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,318 @@
1
+ <div align="center">
2
+
3
+ # fleet
4
+
5
+ **Docker production management CLI + MCP server**
6
+
7
+ [![CI](https://github.com/wrxck/fleet/actions/workflows/ci.yml/badge.svg)](https://github.com/wrxck/fleet/actions/workflows/ci.yml)
8
+ [![npm](https://img.shields.io/npm/v/@matthesketh/fleet)](https://www.npmjs.com/package/@matthesketh/fleet)
9
+ [![Node](https://img.shields.io/node/v/@matthesketh/fleet)](https://nodejs.org)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
11
+ [![License](https://img.shields.io/github/license/wrxck/fleet)](LICENSE)
12
+
13
+ Manages Docker Compose applications on a single server with systemd orchestration, nginx configuration, encrypted secrets, Git/GitHub workflows, health monitoring, and Telegram alerts.
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ fleet CLI (TypeScript/Node.js)
23
+ ├── Commands CLI interface (fleet <command>)
24
+ ├── MCP Server Claude Code integration (fleet mcp)
25
+ ├── Registry App inventory (data/registry.json)
26
+ ├── Secrets Vault age-encrypted secrets (vault/*.age)
27
+ └── Templates systemd, nginx, gitignore generators
28
+
29
+ fleet-bot (Go)
30
+ └── Telegram bot that runs Claude Code sessions for remote management
31
+ ```
32
+
33
+ ### How it works
34
+
35
+ Each Docker Compose app is registered in fleet's registry with its compose path, service name, domains, port, and container names. Fleet generates a systemd service unit for each app so they start on boot in the correct order (databases first, then dependents). Secrets are encrypted at rest using [age](https://github.com/FiloSottile/age) and decrypted to a tmpfs at `/run/fleet-secrets/` on boot via a systemd oneshot service.
36
+
37
+ ## Requirements
38
+
39
+ - Node.js 20+
40
+ - Docker + Docker Compose v2
41
+ - systemd
42
+ - nginx
43
+ - [age](https://github.com/FiloSottile/age) (for secrets)
44
+ - [gh](https://cli.github.com/) (for GitHub operations)
45
+
46
+ ## Install
47
+
48
+ ### From npm
49
+
50
+ ```bash
51
+ npm install -g @matthesketh/fleet
52
+ ```
53
+
54
+ ### From source
55
+
56
+ ```bash
57
+ git clone https://github.com/wrxck/fleet.git
58
+ cd fleet
59
+ npm install
60
+ npm run build
61
+ sudo npm link
62
+ ```
63
+
64
+ ### Install as Claude Code MCP server
65
+
66
+ ```bash
67
+ sudo fleet install-mcp
68
+ ```
69
+
70
+ This writes the MCP server config to `~/.claude.json` so all Claude Code sessions can use fleet tools. Alternatively, add manually:
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "fleet": {
76
+ "command": "fleet",
77
+ "args": ["mcp"]
78
+ }
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Usage
84
+
85
+ Fleet requires root for all commands except `mcp` and `install-mcp`.
86
+
87
+ ```bash
88
+ fleet <command> [options]
89
+ ```
90
+
91
+ ### App lifecycle
92
+
93
+ ```bash
94
+ fleet deploy <app-dir> # Register, build, and start (full pipeline)
95
+ fleet add <app-dir> # Register an existing app without deploying
96
+ fleet remove <app> # Stop, disable, and deregister
97
+ fleet init # Auto-discover all existing apps on the server
98
+ ```
99
+
100
+ `deploy` is the primary command -- it registers the app if needed, runs `docker compose build`, and starts/restarts the systemd service.
101
+
102
+ ### Service control
103
+
104
+ ```bash
105
+ fleet start <app> # Start via systemctl
106
+ fleet stop <app> # Stop via systemctl
107
+ fleet restart <app> # Restart via systemctl
108
+ fleet logs <app> [-f] # Container logs (follow mode with -f)
109
+ ```
110
+
111
+ ### Monitoring
112
+
113
+ ```bash
114
+ fleet status # Dashboard: all apps, systemd state, containers, health
115
+ fleet list [--json] # List registered apps
116
+ fleet health [app] # Health checks: systemd + container + HTTP
117
+ fleet watchdog # Check all services, send Telegram alert on failure
118
+ ```
119
+
120
+ `watchdog` is designed to run on a cron schedule. It checks systemd unit status, container state, and HTTP health endpoints, then sends a Telegram alert if anything is unhealthy. Configure Telegram credentials at `/etc/fleet/telegram.json`:
121
+
122
+ ```json
123
+ {
124
+ "botToken": "123456:ABC-DEF...",
125
+ "chatId": "-100..."
126
+ }
127
+ ```
128
+
129
+ ### Nginx management
130
+
131
+ ```bash
132
+ fleet nginx add <domain> --port <port> [--type proxy|spa|nextjs]
133
+ fleet nginx remove <domain>
134
+ fleet nginx list
135
+ ```
136
+
137
+ Generates an nginx server block, writes it to `/etc/nginx/sites-available/`, symlinks to `sites-enabled/`, tests the config, and reloads nginx. Supports three config types:
138
+
139
+ - **proxy** -- reverse proxy to a backend port (default)
140
+ - **spa** -- static SPA with `try_files` fallback to `index.html`
141
+ - **nextjs** -- Next.js-specific proxy with static asset handling
142
+
143
+ ### Secrets management
144
+
145
+ Fleet uses [age](https://github.com/FiloSottile/age) encryption for secrets at rest. Each app's secrets (`.env` files or secret directories) are encrypted as `.age` files in the `vault/` directory. On boot, a systemd oneshot service decrypts everything to `/run/fleet-secrets/` (tmpfs -- never touches disk).
146
+
147
+ ```bash
148
+ fleet secrets init # Create age keypair, install unseal service
149
+ fleet secrets import <app> [path] # Import .env or secrets dir into vault
150
+ fleet secrets export <app> # Print decrypted .env to stdout
151
+ fleet secrets list [app] # Show managed secrets (masked values)
152
+ fleet secrets set <app> <KEY> <VALUE> # Set a single secret
153
+ fleet secrets get <app> <KEY> # Print a single decrypted value
154
+ fleet secrets seal [app] # Re-encrypt from runtime back to vault
155
+ fleet secrets unseal # Decrypt vault to /run/fleet-secrets/
156
+ fleet secrets drift [app] # Detect vault vs runtime differences
157
+ fleet secrets restore <app> # Restore vault from backup
158
+ fleet secrets rotate # Generate new age key, re-encrypt everything
159
+ fleet secrets validate [app] # Check compose env vars vs vault keys
160
+ fleet secrets status # Vault state, key counts, seal status
161
+ ```
162
+
163
+ Two secret types are supported:
164
+ - **env** -- `.env` files (key=value pairs), encrypted as `<app>.env.age`
165
+ - **secrets-dir** -- directories of secret files (e.g. database passwords), encrypted as `<app>.secrets.age`
166
+
167
+ #### Vault safety features
168
+
169
+ All seal operations are protected with:
170
+ - **Automatic backups** -- vault files are backed up before any mutation
171
+ - **Pre-seal validation** -- rejects seal if >50% of keys would be removed (protects against accidental wipes)
172
+ - **Atomic rollback** -- backup is restored automatically if encryption fails
173
+ - **Drift detection** -- compare vault (survives reboot) vs runtime (lost on reboot) to catch unsaved changes
174
+
175
+ ### Git and GitHub
176
+
177
+ Fleet can onboard apps to GitHub and manage their full Git workflow. All GitHub operations use `gh` CLI over HTTPS.
178
+
179
+ ```bash
180
+ fleet git status [app] # Git state for one or all apps
181
+ fleet git onboard <app> # Create GitHub repo, push, protect branches
182
+ fleet git onboard-all # Onboard all registered apps
183
+ fleet git branch <app> <name> [--from dev] # Create and push a feature branch
184
+ fleet git commit <app> -m "msg" # Stage and commit changes
185
+ fleet git push <app> # Push current branch
186
+ fleet git pr create <app> --title "..." # Create a pull request
187
+ fleet git pr list <app> # List open PRs
188
+ fleet git release <app> # Create develop -> main release PR
189
+ ```
190
+
191
+ The `onboard` command handles everything: initialises git if needed, creates a private GitHub repo, pushes `main` and `develop` branches, and sets up branch protection rules.
192
+
193
+ ### Global flags
194
+
195
+ ```
196
+ --json Output as JSON (where supported)
197
+ --dry-run Show what would happen without making changes
198
+ -y, --yes Skip confirmation prompts
199
+ -v Show version
200
+ -h Show help
201
+ ```
202
+
203
+ ## MCP Server
204
+
205
+ Running `fleet mcp` starts a stdio-based [Model Context Protocol](https://modelcontextprotocol.io/) server. This exposes all fleet operations as tools that Claude Code (or any MCP client) can call.
206
+
207
+ ### Available tools (27)
208
+
209
+ | Tool | Description |
210
+ |------|-------------|
211
+ | `fleet_status` | Dashboard data for all apps |
212
+ | `fleet_list` | List registered apps with config |
213
+ | `fleet_start` | Start an app via systemctl |
214
+ | `fleet_stop` | Stop an app via systemctl |
215
+ | `fleet_restart` | Restart an app via systemctl |
216
+ | `fleet_logs` | Get container logs |
217
+ | `fleet_health` | Run health checks for one/all apps |
218
+ | `fleet_deploy` | Build and restart an app |
219
+ | `fleet_nginx_add` | Create nginx config for a domain |
220
+ | `fleet_nginx_list` | List nginx site configs |
221
+ | `fleet_register` | Register a new app in the fleet registry |
222
+ | `fleet_secrets_status` | Vault state and counts |
223
+ | `fleet_secrets_list` | List secrets (masked values) |
224
+ | `fleet_secrets_unseal` | Decrypt vault to runtime |
225
+ | `fleet_secrets_validate` | Check compose env vars vs vault |
226
+ | `fleet_secrets_set` | Set a single secret key/value |
227
+ | `fleet_secrets_get` | Get a single decrypted value |
228
+ | `fleet_secrets_seal` | Seal runtime changes back to vault |
229
+ | `fleet_secrets_drift` | Detect vault vs runtime drift |
230
+ | `fleet_secrets_restore` | Restore vault from backup |
231
+ | `fleet_git_status` | Git state for one/all apps |
232
+ | `fleet_git_onboard` | GitHub setup: repo, push, protect |
233
+ | `fleet_git_branch` | Create and push a feature branch |
234
+ | `fleet_git_commit` | Stage and commit changes |
235
+ | `fleet_git_push` | Push current branch |
236
+ | `fleet_git_pr_create` | Create a pull request |
237
+ | `fleet_git_pr_list` | List pull requests |
238
+ | `fleet_git_release` | Create develop -> main release PR |
239
+
240
+ ## fleet-bot
241
+
242
+ A Go Telegram bot (`bot/`) that provides remote server management through chat. It runs Claude Code sessions, giving Claude access to fleet's MCP tools for hands-free operations.
243
+
244
+ Built and deployed separately:
245
+
246
+ ```bash
247
+ cd bot
248
+ make build
249
+ sudo cp fleet-bot /usr/local/bin/
250
+ sudo systemctl enable --now fleet-bot
251
+ ```
252
+
253
+ ## Project structure
254
+
255
+ ```
256
+ src/
257
+ ├── index.ts Entry point (detects "mcp" arg)
258
+ ├── cli.ts CLI router and help text
259
+ ├── commands/ CLI command implementations
260
+ │ ├── add.ts Register an app
261
+ │ ├── deploy.ts Full deploy pipeline
262
+ │ ├── git.ts Git/GitHub operations
263
+ │ ├── health.ts Health checks
264
+ │ ├── init.ts Auto-discover apps
265
+ │ ├── install-mcp.ts Self-install as Claude Code MCP server
266
+ │ ├── list.ts List apps
267
+ │ ├── logs.ts Container logs
268
+ │ ├── nginx.ts Nginx management
269
+ │ ├── remove.ts Deregister app
270
+ │ ├── restart.ts Restart service
271
+ │ ├── secrets.ts Secrets vault management
272
+ │ ├── start.ts Start service
273
+ │ ├── status.ts Dashboard
274
+ │ ├── stop.ts Stop service
275
+ │ └── watchdog.ts Health monitor + Telegram alerts
276
+ ├── core/ Core logic
277
+ │ ├── docker.ts Docker Compose operations
278
+ │ ├── errors.ts Error types
279
+ │ ├── exec.ts Shell execution helpers
280
+ │ ├── git.ts Git operations
281
+ │ ├── git-onboard.ts GitHub onboarding logic
282
+ │ ├── github.ts GitHub API via gh CLI
283
+ │ ├── health.ts Health check logic
284
+ │ ├── nginx.ts Nginx file operations
285
+ │ ├── registry.ts App registry (data/registry.json)
286
+ │ ├── secrets.ts Vault primitives (age encrypt/decrypt, backup/restore)
287
+ │ ├── secrets-ops.ts High-level secrets operations (safe seal, drift, validation)
288
+ │ ├── secrets-validate.ts Compose vs vault validation
289
+ │ └── systemd.ts systemctl operations
290
+ ├── mcp/
291
+ │ ├── server.ts MCP server setup + tool registration
292
+ │ ├── git-tools.ts Git-related MCP tools
293
+ │ └── secrets-tools.ts Secrets MCP tools (set, get, seal, drift, restore)
294
+ ├── templates/
295
+ │ ├── gitignore.ts .gitignore generator
296
+ │ ├── nginx.ts Nginx config generator
297
+ │ ├── systemd.ts systemd unit generator
298
+ │ └── unseal.ts Unseal service generator
299
+ └── ui/
300
+ ├── confirm.ts Interactive confirmation
301
+ └── output.ts Coloured terminal output
302
+
303
+ bot/ fleet-bot (Go Telegram bot)
304
+ data/ Runtime data (registry.json)
305
+ vault/ Encrypted secrets (*.age files)
306
+ ```
307
+
308
+ ## Development
309
+
310
+ ```bash
311
+ npm run dev # Run with tsx (no build needed)
312
+ npm run build # Compile TypeScript to dist/
313
+ npm test # Run tests with vitest
314
+ ```
315
+
316
+ ## License
317
+
318
+ MIT
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": 1,
3
+ "apps": [],
4
+ "infrastructure": {
5
+ "databases": {
6
+ "serviceName": "docker-databases",
7
+ "composePath": "/home/user/docker-databases"
8
+ },
9
+ "nginx": {
10
+ "configPath": "/etc/nginx"
11
+ }
12
+ }
13
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function run(argv: string[]): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,113 @@
1
+ import { statusCommand } from './commands/status.js';
2
+ import { listCommand } from './commands/list.js';
3
+ import { startCommand } from './commands/start.js';
4
+ import { stopCommand } from './commands/stop.js';
5
+ import { restartCommand } from './commands/restart.js';
6
+ import { logsCommand } from './commands/logs.js';
7
+ import { healthCommand } from './commands/health.js';
8
+ import { addCommand } from './commands/add.js';
9
+ import { removeCommand } from './commands/remove.js';
10
+ import { deployCommand } from './commands/deploy.js';
11
+ import { nginxCommand } from './commands/nginx.js';
12
+ import { secretsCommand } from './commands/secrets.js';
13
+ import { gitCommand } from './commands/git.js';
14
+ import { initCommand } from './commands/init.js';
15
+ import { watchdogCommand } from './commands/watchdog.js';
16
+ import { installMcpCommand } from './commands/install-mcp.js';
17
+ import { startMcpServer } from './mcp/server.js';
18
+ import { error } from './ui/output.js';
19
+ const VERSION = '1.0.0';
20
+ const HELP = `fleet v${VERSION} - Docker production management CLI
21
+
22
+ Usage: fleet <command> [options]
23
+
24
+ Commands:
25
+ status Dashboard: all apps, services, health
26
+ list [--json] List registered apps
27
+ deploy <app-dir> Full pipeline: register, build, start
28
+ start <app> Start app via systemctl
29
+ stop <app> Stop app via systemctl
30
+ restart <app> Restart app via systemctl
31
+ logs <app> [-f] Container logs (follow mode with -f)
32
+ health [app] Health checks (systemd + container + HTTP)
33
+ add <app-dir> Register existing app
34
+ remove <app> Stop, disable, deregister
35
+ nginx add <domain> --port <port> [--type proxy|spa|nextjs]
36
+ nginx remove <domain>
37
+ nginx list
38
+ secrets init Initialise age vault and unseal service
39
+ secrets list [app] Show managed secrets (masked values)
40
+ secrets set <app> <KEY> <VAL> Set a secret
41
+ secrets get <app> <KEY> Print decrypted value
42
+ secrets import <app> [path] Import .env/secrets into vault
43
+ secrets export <app> Print full decrypted .env
44
+ secrets seal [app] Re-encrypt from runtime to vault
45
+ secrets unseal Decrypt vault to /run/fleet-secrets/
46
+ secrets rotate New age key, re-encrypt everything
47
+ secrets validate [app] Check compose secrets vs vault
48
+ secrets drift [app] Detect vault vs runtime differences
49
+ secrets restore <app> Restore vault from backup
50
+ secrets status Vault state and counts
51
+ git status [app] Git state for one/all apps
52
+ git onboard <app> Create GitHub repo, push, protect branches
53
+ git onboard-all Onboard all apps
54
+ git branch <app> <name> [--from develop] Create feature branch
55
+ git commit <app> -m "msg" Stage + commit
56
+ git push <app> Push current branch
57
+ git pr create <app> --title "..." Create PR
58
+ git pr list <app> List open PRs
59
+ git release <app> Create develop->main PR
60
+ tui, dashboard Interactive terminal dashboard
61
+ init Auto-discover all existing apps
62
+ watchdog Health check all services, alert on failure
63
+ install-mcp Install fleet as Claude Code MCP server
64
+ mcp Start as MCP server
65
+
66
+ Global flags:
67
+ --json Output as JSON
68
+ --dry-run Show what would happen without making changes
69
+ -y, --yes Skip confirmation prompts
70
+ -v, --version Show version
71
+ -h, --help Show this help
72
+ `;
73
+ export async function run(argv) {
74
+ const args = argv.slice(2);
75
+ const command = args[0];
76
+ const rest = args.slice(1);
77
+ if (args.includes('-v') || args.includes('--version')) {
78
+ process.stdout.write(VERSION + '\n');
79
+ return;
80
+ }
81
+ if (!command || args.includes('-h') || args.includes('--help')) {
82
+ process.stdout.write(HELP);
83
+ return;
84
+ }
85
+ switch (command) {
86
+ case 'status': return statusCommand(rest);
87
+ case 'list': return listCommand(rest);
88
+ case 'start': return startCommand(rest);
89
+ case 'stop': return stopCommand(rest);
90
+ case 'restart': return restartCommand(rest);
91
+ case 'logs': return logsCommand(rest);
92
+ case 'health': return healthCommand(rest);
93
+ case 'add': return addCommand(rest);
94
+ case 'remove': return removeCommand(rest);
95
+ case 'deploy': return deployCommand(rest);
96
+ case 'nginx': return nginxCommand(rest);
97
+ case 'secrets': return secretsCommand(rest);
98
+ case 'git': return gitCommand(rest);
99
+ case 'init': return initCommand(rest);
100
+ case 'watchdog': return watchdogCommand(rest);
101
+ case 'install-mcp': return installMcpCommand(rest);
102
+ case 'mcp': return startMcpServer();
103
+ case 'tui':
104
+ case 'dashboard': {
105
+ const { launchTui } = await import('./tui/app.js');
106
+ return launchTui();
107
+ }
108
+ default:
109
+ error(`Unknown command: ${command}`);
110
+ process.stdout.write(HELP);
111
+ process.exit(1);
112
+ }
113
+ }
@@ -0,0 +1 @@
1
+ export declare function addCommand(args: string[]): Promise<void>;
@@ -0,0 +1,95 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve, basename } from 'node:path';
3
+ import { load, save, addApp } from '../core/registry.js';
4
+ import { getContainersByCompose } from '../core/docker.js';
5
+ import { installServiceFile, readServiceFile, enableService } from '../core/systemd.js';
6
+ import { generateServiceFile } from '../templates/systemd.js';
7
+ import { FleetError } from '../core/errors.js';
8
+ import { success, info, error, warn } from '../ui/output.js';
9
+ import { confirm } from '../ui/confirm.js';
10
+ export async function addCommand(args) {
11
+ const dryRun = args.includes('--dry-run');
12
+ const yes = args.includes('-y') || args.includes('--yes');
13
+ const appDir = args.find(a => !a.startsWith('-'));
14
+ if (!appDir) {
15
+ error('Usage: fleet add <app-dir>');
16
+ process.exit(1);
17
+ }
18
+ const fullPath = resolve(appDir);
19
+ if (!existsSync(fullPath)) {
20
+ throw new FleetError(`Directory not found: ${fullPath}`);
21
+ }
22
+ const composePath = findComposePath(fullPath);
23
+ if (!composePath.path) {
24
+ throw new FleetError(`No docker-compose.yml found in ${fullPath} or ${fullPath}/server`);
25
+ }
26
+ const name = basename(fullPath).toLowerCase().replace(/[^a-z0-9-]/g, '-');
27
+ const existingService = readServiceFile(name);
28
+ const hasService = existingService !== null;
29
+ info(`Registering ${name} from ${fullPath}`);
30
+ info(`Compose path: ${composePath.path}`);
31
+ info(`Compose file: ${composePath.file ?? 'default'}`);
32
+ const containers = getContainersByCompose(composePath.path, composePath.file);
33
+ info(`Found containers: ${containers.join(', ') || 'none running'}`);
34
+ const app = {
35
+ name,
36
+ displayName: name,
37
+ composePath: composePath.path,
38
+ composeFile: composePath.file,
39
+ serviceName: name,
40
+ domains: [],
41
+ port: null,
42
+ usesSharedDb: false,
43
+ type: 'service',
44
+ containers: containers.length > 0 ? containers : [name],
45
+ dependsOnDatabases: false,
46
+ registeredAt: new Date().toISOString(),
47
+ };
48
+ if (!hasService) {
49
+ info('No systemd service file found');
50
+ if (!dryRun && (yes || await confirm('Create systemd service file?'))) {
51
+ const content = generateServiceFile({
52
+ serviceName: name,
53
+ description: `${name} Docker Service`,
54
+ workingDirectory: composePath.path,
55
+ composeFile: composePath.file,
56
+ dependsOnDatabases: false,
57
+ });
58
+ installServiceFile(name, content);
59
+ enableService(name);
60
+ success(`Created and enabled ${name}.service`);
61
+ }
62
+ }
63
+ else {
64
+ info('Existing systemd service file found');
65
+ }
66
+ if (dryRun) {
67
+ warn('Dry run - no changes saved');
68
+ process.stdout.write(JSON.stringify(app, null, 2) + '\n');
69
+ return;
70
+ }
71
+ const reg = load();
72
+ save(addApp(reg, app));
73
+ success(`Registered ${name}`);
74
+ }
75
+ function findComposePath(dir) {
76
+ if (existsSync(`${dir}/docker-compose.yml`)) {
77
+ return { path: dir, file: null };
78
+ }
79
+ if (existsSync(`${dir}/docker-compose.yaml`)) {
80
+ return { path: dir, file: null };
81
+ }
82
+ if (existsSync(`${dir}/server/docker-compose.yml`)) {
83
+ return { path: `${dir}/server`, file: null };
84
+ }
85
+ if (existsSync(`${dir}/server/docker-compose.yaml`)) {
86
+ return { path: `${dir}/server`, file: null };
87
+ }
88
+ const customFiles = ['docker-compose.imagemerger.yml'];
89
+ for (const f of customFiles) {
90
+ if (existsSync(`${dir}/${f}`)) {
91
+ return { path: dir, file: f };
92
+ }
93
+ }
94
+ return { path: '', file: null };
95
+ }
@@ -0,0 +1 @@
1
+ export declare function deployCommand(args: string[]): Promise<void>;
@@ -0,0 +1,53 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { load } from '../core/registry.js';
4
+ import { composeBuild } from '../core/docker.js';
5
+ import { startService, restartService, getServiceStatus } from '../core/systemd.js';
6
+ import { FleetError } from '../core/errors.js';
7
+ import { success, error, info, warn, heading } from '../ui/output.js';
8
+ import { addCommand } from './add.js';
9
+ export async function deployCommand(args) {
10
+ const dryRun = args.includes('--dry-run');
11
+ const yes = args.includes('-y') || args.includes('--yes');
12
+ const appDir = args.find(a => !a.startsWith('-'));
13
+ if (!appDir) {
14
+ error('Usage: fleet deploy <app-dir>');
15
+ process.exit(1);
16
+ }
17
+ const fullPath = resolve(appDir);
18
+ if (!existsSync(fullPath)) {
19
+ throw new FleetError(`Directory not found: ${fullPath}`);
20
+ }
21
+ heading('Deploy Pipeline');
22
+ let reg = load();
23
+ let app = reg.apps.find(a => a.composePath.startsWith(fullPath));
24
+ if (!app) {
25
+ info('App not registered, running add first...');
26
+ await addCommand([...args]);
27
+ reg = load();
28
+ app = reg.apps.find(a => a.composePath.startsWith(fullPath));
29
+ if (!app)
30
+ throw new FleetError('Failed to register app');
31
+ }
32
+ if (dryRun) {
33
+ info('Would build and deploy ' + app.name);
34
+ warn('Dry run - no changes made');
35
+ return;
36
+ }
37
+ info(`Building ${app.name}...`);
38
+ if (!composeBuild(app.composePath, app.composeFile, app.name)) {
39
+ error('Build failed');
40
+ process.exit(1);
41
+ }
42
+ success('Build complete');
43
+ info(`Starting ${app.name}...`);
44
+ const svc = getServiceStatus(app.serviceName);
45
+ const started = svc.active
46
+ ? restartService(app.serviceName)
47
+ : startService(app.serviceName);
48
+ if (!started) {
49
+ error('Service start failed - check logs with: fleet logs ' + app.name);
50
+ process.exit(1);
51
+ }
52
+ success(`Deployed ${app.name}`);
53
+ }
@@ -0,0 +1 @@
1
+ export declare function gitCommand(args: string[]): Promise<void>;