@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.
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/data/registry.example.json +13 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +113 -0
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.js +95 -0
- package/dist/commands/deploy.d.ts +1 -0
- package/dist/commands/deploy.js +53 -0
- package/dist/commands/git.d.ts +1 -0
- package/dist/commands/git.js +278 -0
- package/dist/commands/health.d.ts +1 -0
- package/dist/commands/health.js +60 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +157 -0
- package/dist/commands/install-mcp.d.ts +1 -0
- package/dist/commands/install-mcp.js +55 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +20 -0
- package/dist/commands/logs.d.ts +1 -0
- package/dist/commands/logs.js +32 -0
- package/dist/commands/nginx.d.ts +1 -0
- package/dist/commands/nginx.js +94 -0
- package/dist/commands/remove.d.ts +1 -0
- package/dist/commands/remove.js +28 -0
- package/dist/commands/restart.d.ts +1 -0
- package/dist/commands/restart.js +22 -0
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +268 -0
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +22 -0
- package/dist/commands/status.d.ts +14 -0
- package/dist/commands/status.js +70 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +22 -0
- package/dist/commands/watchdog.d.ts +1 -0
- package/dist/commands/watchdog.js +100 -0
- package/dist/core/docker.d.ts +15 -0
- package/dist/core/docker.js +72 -0
- package/dist/core/errors.d.ts +20 -0
- package/dist/core/errors.js +40 -0
- package/dist/core/exec.d.ts +14 -0
- package/dist/core/exec.js +30 -0
- package/dist/core/git-onboard.d.ts +11 -0
- package/dist/core/git-onboard.js +149 -0
- package/dist/core/git.d.ts +36 -0
- package/dist/core/git.js +155 -0
- package/dist/core/github.d.ts +22 -0
- package/dist/core/github.js +92 -0
- package/dist/core/health.d.ts +29 -0
- package/dist/core/health.js +56 -0
- package/dist/core/nginx.d.ts +17 -0
- package/dist/core/nginx.js +59 -0
- package/dist/core/registry.d.ts +38 -0
- package/dist/core/registry.js +47 -0
- package/dist/core/secrets-ops.d.ts +37 -0
- package/dist/core/secrets-ops.js +331 -0
- package/dist/core/secrets-validate.d.ts +8 -0
- package/dist/core/secrets-validate.js +81 -0
- package/dist/core/secrets.d.ts +36 -0
- package/dist/core/secrets.js +191 -0
- package/dist/core/systemd.d.ts +23 -0
- package/dist/core/systemd.js +106 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/mcp/git-tools.d.ts +2 -0
- package/dist/mcp/git-tools.js +148 -0
- package/dist/mcp/secrets-tools.d.ts +2 -0
- package/dist/mcp/secrets-tools.js +67 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +179 -0
- package/dist/templates/gitignore.d.ts +3 -0
- package/dist/templates/gitignore.js +89 -0
- package/dist/templates/nginx.d.ts +8 -0
- package/dist/templates/nginx.js +111 -0
- package/dist/templates/systemd.d.ts +9 -0
- package/dist/templates/systemd.js +26 -0
- package/dist/templates/unseal.d.ts +1 -0
- package/dist/templates/unseal.js +22 -0
- package/dist/tui/app.d.ts +1 -0
- package/dist/tui/app.js +9 -0
- package/dist/tui/components/AppList.d.ts +12 -0
- package/dist/tui/components/AppList.js +32 -0
- package/dist/tui/components/Confirm.d.ts +2 -0
- package/dist/tui/components/Confirm.js +10 -0
- package/dist/tui/components/Header.d.ts +6 -0
- package/dist/tui/components/Header.js +16 -0
- package/dist/tui/components/KeyHint.d.ts +2 -0
- package/dist/tui/components/KeyHint.js +55 -0
- package/dist/tui/components/StatusBadge.d.ts +7 -0
- package/dist/tui/components/StatusBadge.js +8 -0
- package/dist/tui/exec-bridge.d.ts +11 -0
- package/dist/tui/exec-bridge.js +57 -0
- package/dist/tui/hooks/use-fleet-data.d.ts +9 -0
- package/dist/tui/hooks/use-fleet-data.js +30 -0
- package/dist/tui/hooks/use-health.d.ts +9 -0
- package/dist/tui/hooks/use-health.js +29 -0
- package/dist/tui/hooks/use-interval.d.ts +1 -0
- package/dist/tui/hooks/use-interval.js +13 -0
- package/dist/tui/hooks/use-keyboard.d.ts +1 -0
- package/dist/tui/hooks/use-keyboard.js +44 -0
- package/dist/tui/hooks/use-secrets.d.ts +47 -0
- package/dist/tui/hooks/use-secrets.js +152 -0
- package/dist/tui/router.d.ts +2 -0
- package/dist/tui/router.js +65 -0
- package/dist/tui/state.d.ts +12 -0
- package/dist/tui/state.js +83 -0
- package/dist/tui/theme.d.ts +11 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/types.d.ts +41 -0
- package/dist/tui/types.js +1 -0
- package/dist/tui/views/AppDetail.d.ts +2 -0
- package/dist/tui/views/AppDetail.js +72 -0
- package/dist/tui/views/Dashboard.d.ts +2 -0
- package/dist/tui/views/Dashboard.js +29 -0
- package/dist/tui/views/HealthView.d.ts +2 -0
- package/dist/tui/views/HealthView.js +28 -0
- package/dist/tui/views/LogsView.d.ts +2 -0
- package/dist/tui/views/LogsView.js +71 -0
- package/dist/tui/views/SecretEdit.d.ts +2 -0
- package/dist/tui/views/SecretEdit.js +53 -0
- package/dist/tui/views/SecretsView.d.ts +2 -0
- package/dist/tui/views/SecretsView.js +108 -0
- package/dist/ui/confirm.d.ts +1 -0
- package/dist/ui/confirm.js +15 -0
- package/dist/ui/output.d.ts +27 -0
- package/dist/ui/output.js +61 -0
- 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
|
+
[](https://github.com/wrxck/fleet/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/@matthesketh/fleet)
|
|
9
|
+
[](https://nodejs.org)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](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
|
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>;
|