@matthesketh/fleet 1.0.0 → 1.2.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 +27 -4
- package/dist/cli.js +8 -0
- package/dist/commands/deps.d.ts +1 -0
- package/dist/commands/deps.js +223 -0
- package/dist/commands/motd.d.ts +1 -0
- package/dist/commands/motd.js +10 -0
- package/dist/core/deps/actors/pr-creator.d.ts +14 -0
- package/dist/core/deps/actors/pr-creator.js +103 -0
- package/dist/core/deps/cache.d.ts +5 -0
- package/dist/core/deps/cache.js +28 -0
- package/dist/core/deps/collectors/composer.d.ts +12 -0
- package/dist/core/deps/collectors/composer.js +70 -0
- package/dist/core/deps/collectors/docker-image.d.ts +18 -0
- package/dist/core/deps/collectors/docker-image.js +132 -0
- package/dist/core/deps/collectors/docker-running.d.ts +17 -0
- package/dist/core/deps/collectors/docker-running.js +55 -0
- package/dist/core/deps/collectors/eol.d.ts +16 -0
- package/dist/core/deps/collectors/eol.js +139 -0
- package/dist/core/deps/collectors/github-pr.d.ts +8 -0
- package/dist/core/deps/collectors/github-pr.js +40 -0
- package/dist/core/deps/collectors/npm.d.ts +12 -0
- package/dist/core/deps/collectors/npm.js +63 -0
- package/dist/core/deps/collectors/pip.d.ts +15 -0
- package/dist/core/deps/collectors/pip.js +94 -0
- package/dist/core/deps/collectors/vulnerability.d.ts +9 -0
- package/dist/core/deps/collectors/vulnerability.js +102 -0
- package/dist/core/deps/config.d.ts +6 -0
- package/dist/core/deps/config.js +55 -0
- package/dist/core/deps/reporters/cli.d.ts +4 -0
- package/dist/core/deps/reporters/cli.js +123 -0
- package/dist/core/deps/reporters/motd.d.ts +3 -0
- package/dist/core/deps/reporters/motd.js +64 -0
- package/dist/core/deps/reporters/telegram.d.ts +6 -0
- package/dist/core/deps/reporters/telegram.js +106 -0
- package/dist/core/deps/scanner.d.ts +4 -0
- package/dist/core/deps/scanner.js +89 -0
- package/dist/core/deps/severity.d.ts +6 -0
- package/dist/core/deps/severity.js +45 -0
- package/dist/core/deps/types.d.ts +64 -0
- package/dist/core/deps/types.js +1 -0
- package/dist/mcp/deps-tools.d.ts +2 -0
- package/dist/mcp/deps-tools.js +81 -0
- package/dist/mcp/server.js +2 -0
- package/dist/templates/motd.d.ts +1 -0
- package/dist/templates/motd.js +7 -0
- package/dist/tui/components/AppList.js +1 -1
- package/dist/tui/components/Confirm.js +3 -4
- package/dist/tui/components/Header.js +37 -8
- package/dist/tui/components/KeyHint.js +4 -5
- package/dist/tui/hooks/use-terminal-size.d.ts +1 -0
- package/dist/tui/hooks/use-terminal-size.js +1 -0
- package/dist/tui/router.js +81 -9
- package/dist/tui/state.js +15 -0
- package/dist/tui/tests/flicker.test.d.ts +1 -0
- package/dist/tui/tests/flicker.test.js +105 -0
- package/dist/tui/tests/keyboard-integration.test.d.ts +1 -0
- package/dist/tui/tests/keyboard-integration.test.js +117 -0
- package/dist/tui/tests/test-app.d.ts +4 -0
- package/dist/tui/tests/test-app.js +79 -0
- package/dist/tui/types.d.ts +13 -0
- package/dist/tui/views/AppDetail.js +41 -26
- package/dist/tui/views/Dashboard.js +34 -9
- package/dist/tui/views/HealthView.js +36 -12
- package/dist/tui/views/LogsView.js +14 -9
- package/dist/tui/views/SecretEdit.js +8 -4
- package/dist/tui/views/SecretsView.js +49 -36
- package/package.json +17 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
11
|
[](LICENSE)
|
|
12
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.
|
|
13
|
+
Manages Docker Compose applications on a single server with systemd orchestration, nginx configuration, encrypted secrets, Git/GitHub workflows, health monitoring, dependency tracking, and Telegram alerts.
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -24,6 +24,7 @@ fleet CLI (TypeScript/Node.js)
|
|
|
24
24
|
├── MCP Server Claude Code integration (fleet mcp)
|
|
25
25
|
├── Registry App inventory (data/registry.json)
|
|
26
26
|
├── Secrets Vault age-encrypted secrets (vault/*.age)
|
|
27
|
+
├── Deps Monitor Dependency health scanning + alerting
|
|
27
28
|
└── Templates systemd, nginx, gitignore generators
|
|
28
29
|
|
|
29
30
|
fleet-bot (Go)
|
|
@@ -126,6 +127,19 @@ fleet watchdog # Check all services, send Telegram alert on fai
|
|
|
126
127
|
}
|
|
127
128
|
```
|
|
128
129
|
|
|
130
|
+
### Dependency health
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
fleet deps [app] # Summary dashboard or per-app detail
|
|
134
|
+
fleet deps scan # Run fresh dependency scan
|
|
135
|
+
fleet deps fix <app> [--dry-run] # Create PR for fixable dependency updates
|
|
136
|
+
fleet deps config # Show/set configuration
|
|
137
|
+
fleet deps ignore <pkg> --reason .. # Suppress a finding
|
|
138
|
+
fleet deps init # Install cron + MOTD for automated scanning
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Scans all registered apps for outdated packages (npm, Composer, pip), Docker image updates, runtime EOL warnings (via endoflife.date), security vulnerabilities (via OSV API), and open dependency PRs on GitHub. Results are cached and surfaced via CLI, MOTD on SSH login, and Telegram notifications. Runs automatically every 6 hours via cron (configurable).
|
|
142
|
+
|
|
129
143
|
### Nginx management
|
|
130
144
|
|
|
131
145
|
```bash
|
|
@@ -204,7 +218,7 @@ The `onboard` command handles everything: initialises git if needed, creates a p
|
|
|
204
218
|
|
|
205
219
|
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
220
|
|
|
207
|
-
### Available tools (
|
|
221
|
+
### Available tools (33)
|
|
208
222
|
|
|
209
223
|
| Tool | Description |
|
|
210
224
|
|------|-------------|
|
|
@@ -236,6 +250,12 @@ Running `fleet mcp` starts a stdio-based [Model Context Protocol](https://modelc
|
|
|
236
250
|
| `fleet_git_pr_create` | Create a pull request |
|
|
237
251
|
| `fleet_git_pr_list` | List pull requests |
|
|
238
252
|
| `fleet_git_release` | Create develop -> main release PR |
|
|
253
|
+
| `fleet_deps_status` | Dependency health summary from cache |
|
|
254
|
+
| `fleet_deps_scan` | Run a fresh dependency scan |
|
|
255
|
+
| `fleet_deps_app` | Dependency findings for a specific app |
|
|
256
|
+
| `fleet_deps_fix` | Create PR with dependency updates (dry-run default) |
|
|
257
|
+
| `fleet_deps_ignore` | Add an ignore rule for a finding |
|
|
258
|
+
| `fleet_deps_config` | Get or set dependency monitoring config |
|
|
239
259
|
|
|
240
260
|
## fleet-bot
|
|
241
261
|
|
|
@@ -259,6 +279,7 @@ src/
|
|
|
259
279
|
├── commands/ CLI command implementations
|
|
260
280
|
│ ├── add.ts Register an app
|
|
261
281
|
│ ├── deploy.ts Full deploy pipeline
|
|
282
|
+
│ ├── deps.ts Dependency health monitoring
|
|
262
283
|
│ ├── git.ts Git/GitHub operations
|
|
263
284
|
│ ├── health.ts Health checks
|
|
264
285
|
│ ├── init.ts Auto-discover apps
|
|
@@ -286,11 +307,13 @@ src/
|
|
|
286
307
|
│ ├── secrets.ts Vault primitives (age encrypt/decrypt, backup/restore)
|
|
287
308
|
│ ├── secrets-ops.ts High-level secrets operations (safe seal, drift, validation)
|
|
288
309
|
│ ├── secrets-validate.ts Compose vs vault validation
|
|
289
|
-
│
|
|
310
|
+
│ ├── systemd.ts systemctl operations
|
|
311
|
+
│ └── deps/ Dependency health (collectors, reporters, actors)
|
|
290
312
|
├── mcp/
|
|
291
313
|
│ ├── server.ts MCP server setup + tool registration
|
|
292
314
|
│ ├── git-tools.ts Git-related MCP tools
|
|
293
|
-
│
|
|
315
|
+
│ ├── secrets-tools.ts Secrets MCP tools (set, get, seal, drift, restore)
|
|
316
|
+
│ └── deps-tools.ts Dependency monitoring MCP tools
|
|
294
317
|
├── templates/
|
|
295
318
|
│ ├── gitignore.ts .gitignore generator
|
|
296
319
|
│ ├── nginx.ts Nginx config generator
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { nginxCommand } from './commands/nginx.js';
|
|
|
12
12
|
import { secretsCommand } from './commands/secrets.js';
|
|
13
13
|
import { gitCommand } from './commands/git.js';
|
|
14
14
|
import { initCommand } from './commands/init.js';
|
|
15
|
+
import { depsCommand } from './commands/deps.js';
|
|
15
16
|
import { watchdogCommand } from './commands/watchdog.js';
|
|
16
17
|
import { installMcpCommand } from './commands/install-mcp.js';
|
|
17
18
|
import { startMcpServer } from './mcp/server.js';
|
|
@@ -30,6 +31,12 @@ Commands:
|
|
|
30
31
|
restart <app> Restart app via systemctl
|
|
31
32
|
logs <app> [-f] Container logs (follow mode with -f)
|
|
32
33
|
health [app] Health checks (systemd + container + HTTP)
|
|
34
|
+
deps [app] Dependency health: outdated, CVEs, EOL, Docker
|
|
35
|
+
deps scan Run fresh dependency scan
|
|
36
|
+
deps fix <app> Create PR for fixable dependency updates
|
|
37
|
+
deps config Show/set configuration
|
|
38
|
+
deps ignore <pkg> Suppress a finding
|
|
39
|
+
deps init Install cron + MOTD for automated scanning
|
|
33
40
|
add <app-dir> Register existing app
|
|
34
41
|
remove <app> Stop, disable, deregister
|
|
35
42
|
nginx add <domain> --port <port> [--type proxy|spa|nextjs]
|
|
@@ -90,6 +97,7 @@ export async function run(argv) {
|
|
|
90
97
|
case 'restart': return restartCommand(rest);
|
|
91
98
|
case 'logs': return logsCommand(rest);
|
|
92
99
|
case 'health': return healthCommand(rest);
|
|
100
|
+
case 'deps': return depsCommand(rest);
|
|
93
101
|
case 'add': return addCommand(rest);
|
|
94
102
|
case 'remove': return removeCommand(rest);
|
|
95
103
|
case 'deploy': return deployCommand(rest);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function depsCommand(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { writeFileSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { load, findApp } from '../core/registry.js';
|
|
3
|
+
import { loadConfig, saveConfig, configPath } from '../core/deps/config.js';
|
|
4
|
+
import { loadCache, saveCache, isCacheStale, cachePath } from '../core/deps/cache.js';
|
|
5
|
+
import { runScan } from '../core/deps/scanner.js';
|
|
6
|
+
import { formatSummary, formatAppDetail } from '../core/deps/reporters/cli.js';
|
|
7
|
+
import { formatMotd, generateMotdScript } from '../core/deps/reporters/motd.js';
|
|
8
|
+
import { sendTelegramNotification, loadNotifiedFindings, saveNotifiedFindings, } from '../core/deps/reporters/telegram.js';
|
|
9
|
+
import { createDepsPr } from '../core/deps/actors/pr-creator.js';
|
|
10
|
+
import { AppNotFoundError } from '../core/errors.js';
|
|
11
|
+
import { heading, success, error, info, warn } from '../ui/output.js';
|
|
12
|
+
export async function depsCommand(args) {
|
|
13
|
+
const sub = args[0];
|
|
14
|
+
switch (sub) {
|
|
15
|
+
case 'scan': return depsScan(args.slice(1));
|
|
16
|
+
case 'fix': return depsFix(args.slice(1));
|
|
17
|
+
case 'config': return depsConfig(args.slice(1));
|
|
18
|
+
case 'ignore': return depsIgnore(args.slice(1));
|
|
19
|
+
case 'unignore': return depsUnignore(args.slice(1));
|
|
20
|
+
case 'init': return depsInit();
|
|
21
|
+
default: return depsShow(args);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function depsShow(args) {
|
|
25
|
+
const json = args.includes('--json');
|
|
26
|
+
const motd = args.includes('--motd');
|
|
27
|
+
const severityFilter = extractFlag(args, '--severity');
|
|
28
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
29
|
+
const config = loadConfig();
|
|
30
|
+
const cache = loadCache();
|
|
31
|
+
const reg = load();
|
|
32
|
+
if (!cache) {
|
|
33
|
+
warn('No scan data found. Run: fleet deps scan');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (isCacheStale(cache, config.scanIntervalHours)) {
|
|
37
|
+
warn(`Scan data is stale (last scan: ${cache.lastScan}). Run: fleet deps scan`);
|
|
38
|
+
}
|
|
39
|
+
if (json) {
|
|
40
|
+
if (appName) {
|
|
41
|
+
const findings = cache.findings.filter(f => f.appName === appName);
|
|
42
|
+
process.stdout.write(JSON.stringify(findings, null, 2) + '\n');
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
process.stdout.write(JSON.stringify(cache, null, 2) + '\n');
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (motd) {
|
|
50
|
+
process.stdout.write(formatMotd(cache, reg.apps.length) + '\n');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (appName) {
|
|
54
|
+
const app = findApp(reg, appName);
|
|
55
|
+
if (!app)
|
|
56
|
+
throw new AppNotFoundError(appName);
|
|
57
|
+
let findings = cache.findings.filter(f => f.appName === app.name);
|
|
58
|
+
if (severityFilter) {
|
|
59
|
+
const sevs = severityFilter.split(',');
|
|
60
|
+
findings = findings.filter(f => sevs.includes(f.severity));
|
|
61
|
+
}
|
|
62
|
+
heading(`Deps: ${app.name}`);
|
|
63
|
+
const lines = formatAppDetail(app.name, findings);
|
|
64
|
+
for (const line of lines)
|
|
65
|
+
process.stdout.write(line + '\n');
|
|
66
|
+
process.stdout.write('\n');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
heading('Dependency Health');
|
|
70
|
+
let findings = cache.findings;
|
|
71
|
+
if (severityFilter) {
|
|
72
|
+
const sevs = severityFilter.split(',');
|
|
73
|
+
findings = findings.filter(f => sevs.includes(f.severity));
|
|
74
|
+
}
|
|
75
|
+
const summaryCache = { ...cache, findings };
|
|
76
|
+
const lines = formatSummary(summaryCache, reg.apps.length);
|
|
77
|
+
for (const line of lines)
|
|
78
|
+
process.stdout.write(line + '\n');
|
|
79
|
+
process.stdout.write('\n');
|
|
80
|
+
}
|
|
81
|
+
async function depsScan(args) {
|
|
82
|
+
const quiet = args.includes('--quiet');
|
|
83
|
+
const reg = load();
|
|
84
|
+
const config = loadConfig();
|
|
85
|
+
if (!quiet)
|
|
86
|
+
info('Scanning dependencies across all apps...');
|
|
87
|
+
const cache = await runScan(reg.apps, config);
|
|
88
|
+
saveCache(cache);
|
|
89
|
+
if (config.notifications.telegram.enabled) {
|
|
90
|
+
const previousFindings = loadNotifiedFindings();
|
|
91
|
+
const sent = await sendTelegramNotification(cache.findings, reg.apps.length, previousFindings, config.notifications.telegram.minSeverity);
|
|
92
|
+
if (sent) {
|
|
93
|
+
saveNotifiedFindings(cache.findings);
|
|
94
|
+
if (!quiet)
|
|
95
|
+
info('Telegram notification sent');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!quiet) {
|
|
99
|
+
success(`Scan complete: ${cache.findings.length} findings across ${reg.apps.length} apps (${cache.scanDurationMs}ms)`);
|
|
100
|
+
if (cache.errors.length > 0) {
|
|
101
|
+
warn(`${cache.errors.length} collector errors`);
|
|
102
|
+
}
|
|
103
|
+
process.stdout.write('\n');
|
|
104
|
+
heading('Dependency Health');
|
|
105
|
+
const lines = formatSummary(cache, reg.apps.length);
|
|
106
|
+
for (const line of lines)
|
|
107
|
+
process.stdout.write(line + '\n');
|
|
108
|
+
process.stdout.write('\n');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function depsFix(args) {
|
|
112
|
+
const dryRun = args.includes('--dry-run');
|
|
113
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
114
|
+
if (!appName) {
|
|
115
|
+
error('Usage: fleet deps fix <app> [--dry-run] [--major]');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
const reg = load();
|
|
119
|
+
const app = findApp(reg, appName);
|
|
120
|
+
if (!app)
|
|
121
|
+
throw new AppNotFoundError(appName);
|
|
122
|
+
const cache = loadCache();
|
|
123
|
+
if (!cache) {
|
|
124
|
+
error('No scan data. Run: fleet deps scan');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const findings = cache.findings.filter(f => f.appName === app.name && f.fixable);
|
|
128
|
+
if (findings.length === 0) {
|
|
129
|
+
info('No fixable findings for this app');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const result = createDepsPr(app, findings, dryRun);
|
|
133
|
+
if (dryRun) {
|
|
134
|
+
heading(`Dry run: ${app.name}`);
|
|
135
|
+
info(`Would create branch: ${result.branch}`);
|
|
136
|
+
for (const bump of result.bumps) {
|
|
137
|
+
info(` ${bump.file}: ${bump.search} -> ${bump.replace}`);
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (result.prUrl) {
|
|
142
|
+
success(`PR created: ${result.prUrl}`);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
success(`Branch ${result.branch} pushed with ${result.bumps.length} updates`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function depsConfig(args) {
|
|
149
|
+
const config = loadConfig();
|
|
150
|
+
if (args.length === 0) {
|
|
151
|
+
process.stdout.write(JSON.stringify(config, null, 2) + '\n');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (args[0] === 'set' && args.length >= 3) {
|
|
155
|
+
const key = args[1];
|
|
156
|
+
const value = args[2];
|
|
157
|
+
const parsed = value === 'true' ? true : value === 'false' ? false : isNaN(Number(value)) ? value : Number(value);
|
|
158
|
+
config[key] = parsed;
|
|
159
|
+
saveConfig(config);
|
|
160
|
+
success(`Set ${key} = ${value}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
error('Usage: fleet deps config [set <key> <value>]');
|
|
164
|
+
}
|
|
165
|
+
async function depsIgnore(args) {
|
|
166
|
+
const pkg = args.find(a => !a.startsWith('-'));
|
|
167
|
+
const appName = extractFlag(args, '--app');
|
|
168
|
+
const reason = extractFlag(args, '--reason');
|
|
169
|
+
const until = extractFlag(args, '--until');
|
|
170
|
+
if (!pkg || !reason) {
|
|
171
|
+
error('Usage: fleet deps ignore <package> --reason "..." [--app <name>] [--until YYYY-MM-DD]');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
const config = loadConfig();
|
|
175
|
+
config.ignore.push({
|
|
176
|
+
package: pkg,
|
|
177
|
+
...(appName && { appName }),
|
|
178
|
+
reason,
|
|
179
|
+
...(until && { until }),
|
|
180
|
+
});
|
|
181
|
+
saveConfig(config);
|
|
182
|
+
success(`Ignoring ${pkg}${appName ? ` for ${appName}` : ''}: ${reason}`);
|
|
183
|
+
}
|
|
184
|
+
async function depsUnignore(args) {
|
|
185
|
+
const pkg = args.find(a => !a.startsWith('-'));
|
|
186
|
+
const appName = extractFlag(args, '--app');
|
|
187
|
+
if (!pkg) {
|
|
188
|
+
error('Usage: fleet deps unignore <package> [--app <name>]');
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const config = loadConfig();
|
|
192
|
+
config.ignore = config.ignore.filter(r => {
|
|
193
|
+
if (r.package !== pkg)
|
|
194
|
+
return true;
|
|
195
|
+
if (appName && r.appName !== appName)
|
|
196
|
+
return true;
|
|
197
|
+
return false;
|
|
198
|
+
});
|
|
199
|
+
saveConfig(config);
|
|
200
|
+
success(`Removed ignore rule for ${pkg}`);
|
|
201
|
+
}
|
|
202
|
+
async function depsInit() {
|
|
203
|
+
const config = loadConfig();
|
|
204
|
+
saveConfig(config);
|
|
205
|
+
success(`Config written to ${configPath()}`);
|
|
206
|
+
const motdPath = '/etc/update-motd.d/99-fleet-deps';
|
|
207
|
+
const script = generateMotdScript(cachePath());
|
|
208
|
+
writeFileSync(motdPath, script);
|
|
209
|
+
chmodSync(motdPath, 0o755);
|
|
210
|
+
success(`MOTD script installed at ${motdPath}`);
|
|
211
|
+
const cronLine = `0 */${config.scanIntervalHours} * * * root /usr/local/bin/fleet deps scan --quiet\n`;
|
|
212
|
+
writeFileSync('/etc/cron.d/fleet-deps', cronLine);
|
|
213
|
+
success(`Cron installed: every ${config.scanIntervalHours} hours`);
|
|
214
|
+
info('Running initial scan...');
|
|
215
|
+
await depsScan(['--quiet']);
|
|
216
|
+
success('Initial scan complete. Run: fleet deps');
|
|
217
|
+
}
|
|
218
|
+
function extractFlag(args, flag) {
|
|
219
|
+
const idx = args.indexOf(flag);
|
|
220
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
221
|
+
return undefined;
|
|
222
|
+
return args[idx + 1];
|
|
223
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function motdInstallCommand(): void;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { writeFileSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { generateMotdScript } from '../templates/motd.js';
|
|
3
|
+
import { success } from '../ui/output.js';
|
|
4
|
+
const MOTD_PATH = '/etc/update-motd.d/50-fleet-status';
|
|
5
|
+
export function motdInstallCommand() {
|
|
6
|
+
const script = generateMotdScript();
|
|
7
|
+
writeFileSync(MOTD_PATH, script);
|
|
8
|
+
chmodSync(MOTD_PATH, 0o755);
|
|
9
|
+
success(`Installed MOTD script at ${MOTD_PATH}`);
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AppEntry } from '../../registry.js';
|
|
2
|
+
import type { Finding } from '../types.js';
|
|
3
|
+
export interface VersionBump {
|
|
4
|
+
file: string;
|
|
5
|
+
search: string;
|
|
6
|
+
replace: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function generateVersionBump(finding: Finding): VersionBump | null;
|
|
9
|
+
export declare function buildPrBody(findings: Finding[]): string;
|
|
10
|
+
export declare function createDepsPr(app: AppEntry, findings: Finding[], dryRun: boolean): {
|
|
11
|
+
branch: string;
|
|
12
|
+
bumps: VersionBump[];
|
|
13
|
+
prUrl?: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { exec } from '../../exec.js';
|
|
4
|
+
export function generateVersionBump(finding) {
|
|
5
|
+
if (!finding.fixable || !finding.package || !finding.currentVersion || !finding.latestVersion) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
switch (finding.source) {
|
|
9
|
+
case 'npm':
|
|
10
|
+
return {
|
|
11
|
+
file: 'package.json',
|
|
12
|
+
search: `"${finding.package}": "${finding.currentVersion}"`,
|
|
13
|
+
replace: `"${finding.package}": "${finding.latestVersion}"`,
|
|
14
|
+
};
|
|
15
|
+
case 'composer':
|
|
16
|
+
return {
|
|
17
|
+
file: 'composer.json',
|
|
18
|
+
search: `"${finding.package}": "${finding.currentVersion}"`,
|
|
19
|
+
replace: `"${finding.package}": "${finding.latestVersion}"`,
|
|
20
|
+
};
|
|
21
|
+
case 'pip':
|
|
22
|
+
return {
|
|
23
|
+
file: 'requirements.txt',
|
|
24
|
+
search: `${finding.package}==${finding.currentVersion}`,
|
|
25
|
+
replace: `${finding.package}==${finding.latestVersion}`,
|
|
26
|
+
};
|
|
27
|
+
case 'docker-image':
|
|
28
|
+
return {
|
|
29
|
+
file: 'Dockerfile',
|
|
30
|
+
search: `${finding.package}:${finding.currentVersion}`,
|
|
31
|
+
replace: `${finding.package}:${finding.latestVersion}`,
|
|
32
|
+
};
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function buildPrBody(findings) {
|
|
38
|
+
const lines = [];
|
|
39
|
+
lines.push('## Dependency Updates\n');
|
|
40
|
+
lines.push('| Package | Current | Latest | Severity |');
|
|
41
|
+
lines.push('|---------|---------|--------|----------|');
|
|
42
|
+
for (const f of findings) {
|
|
43
|
+
lines.push(`| ${f.package ?? f.title} | ${f.currentVersion ?? '-'} | ${f.latestVersion ?? '-'} | ${f.severity} |`);
|
|
44
|
+
}
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push('## Post-merge steps');
|
|
47
|
+
lines.push('');
|
|
48
|
+
const hasNpm = findings.some(f => f.source === 'npm');
|
|
49
|
+
const hasComposer = findings.some(f => f.source === 'composer');
|
|
50
|
+
const hasPip = findings.some(f => f.source === 'pip');
|
|
51
|
+
const hasDocker = findings.some(f => f.source === 'docker-image');
|
|
52
|
+
if (hasNpm)
|
|
53
|
+
lines.push('- [ ] Run `npm install` to update lockfile');
|
|
54
|
+
if (hasComposer)
|
|
55
|
+
lines.push('- [ ] Run `composer update` to update lockfile');
|
|
56
|
+
if (hasPip)
|
|
57
|
+
lines.push('- [ ] Run `pip install -r requirements.txt` to verify');
|
|
58
|
+
if (hasDocker)
|
|
59
|
+
lines.push('- [ ] Rebuild Docker image and test');
|
|
60
|
+
lines.push('- [ ] Run tests');
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push('---');
|
|
63
|
+
lines.push('Generated by `fleet deps fix`');
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
export function createDepsPr(app, findings, dryRun) {
|
|
67
|
+
const fixable = findings.filter(f => f.fixable);
|
|
68
|
+
const bumps = fixable.map(generateVersionBump).filter((b) => b !== null);
|
|
69
|
+
if (bumps.length === 0) {
|
|
70
|
+
return { branch: '', bumps: [] };
|
|
71
|
+
}
|
|
72
|
+
const date = new Date().toISOString().split('T')[0];
|
|
73
|
+
const branch = `deps/${app.name}/${date}`;
|
|
74
|
+
if (dryRun) {
|
|
75
|
+
return { branch, bumps };
|
|
76
|
+
}
|
|
77
|
+
const sshEnv = { SSH_AUTH_SOCK: '/tmp/fleet-ssh-agent.sock' };
|
|
78
|
+
exec('git checkout develop', { cwd: app.composePath });
|
|
79
|
+
exec('git pull', { cwd: app.composePath, env: sshEnv });
|
|
80
|
+
exec(`git checkout -b ${branch}`, { cwd: app.composePath });
|
|
81
|
+
for (const bump of bumps) {
|
|
82
|
+
const filePath = join(app.composePath, bump.file);
|
|
83
|
+
if (!existsSync(filePath))
|
|
84
|
+
continue;
|
|
85
|
+
let content = readFileSync(filePath, 'utf-8');
|
|
86
|
+
content = content.replace(bump.search, bump.replace);
|
|
87
|
+
writeFileSync(filePath, content);
|
|
88
|
+
}
|
|
89
|
+
const files = [...new Set(bumps.map(b => b.file))];
|
|
90
|
+
exec(`git add ${files.join(' ')}`, { cwd: app.composePath });
|
|
91
|
+
const commitMsg = bumps.length === 1
|
|
92
|
+
? `chore(deps): update ${fixable[0].package} from ${fixable[0].currentVersion} to ${fixable[0].latestVersion}`
|
|
93
|
+
: `chore(deps): update ${bumps.length} dependencies`;
|
|
94
|
+
exec(`git commit -m "${commitMsg}"`, { cwd: app.composePath });
|
|
95
|
+
exec(`git push -u origin ${branch}`, { cwd: app.composePath, env: sshEnv });
|
|
96
|
+
if (!app.gitRepo)
|
|
97
|
+
return { branch, bumps };
|
|
98
|
+
const prBody = buildPrBody(fixable);
|
|
99
|
+
const prTitle = `chore(deps): update dependencies (${date})`;
|
|
100
|
+
const prResult = exec(`gh pr create --repo ${app.gitRepo} --title "${prTitle}" --body "${prBody.replace(/"/g, '\\"')}" --base develop`, { cwd: app.composePath, env: sshEnv });
|
|
101
|
+
const prUrl = prResult.ok ? prResult.stdout.trim() : undefined;
|
|
102
|
+
return { branch, bumps, prUrl };
|
|
103
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DepsCache } from './types.js';
|
|
2
|
+
export declare function loadCache(path?: string): DepsCache | null;
|
|
3
|
+
export declare function saveCache(cache: DepsCache, path?: string): void;
|
|
4
|
+
export declare function isCacheStale(cache: DepsCache | null, intervalHours: number): boolean;
|
|
5
|
+
export declare function cachePath(): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const DEFAULT_CACHE_PATH = join(__dirname, '..', '..', '..', 'data', 'deps-cache.json');
|
|
6
|
+
export function loadCache(path = DEFAULT_CACHE_PATH) {
|
|
7
|
+
if (!existsSync(path))
|
|
8
|
+
return null;
|
|
9
|
+
const raw = readFileSync(path, 'utf-8');
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
export function saveCache(cache, path = DEFAULT_CACHE_PATH) {
|
|
13
|
+
const dir = dirname(path);
|
|
14
|
+
if (!existsSync(dir))
|
|
15
|
+
mkdirSync(dir, { recursive: true });
|
|
16
|
+
const tmpPath = path + '.tmp';
|
|
17
|
+
writeFileSync(tmpPath, JSON.stringify(cache, null, 2) + '\n');
|
|
18
|
+
renameSync(tmpPath, path);
|
|
19
|
+
}
|
|
20
|
+
export function isCacheStale(cache, intervalHours) {
|
|
21
|
+
if (!cache)
|
|
22
|
+
return true;
|
|
23
|
+
const age = Date.now() - new Date(cache.lastScan).getTime();
|
|
24
|
+
return age > intervalHours * 60 * 60 * 1000;
|
|
25
|
+
}
|
|
26
|
+
export function cachePath() {
|
|
27
|
+
return DEFAULT_CACHE_PATH;
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AppEntry } from '../../registry.js';
|
|
2
|
+
import type { Collector, Finding, DepsConfig } from '../types.js';
|
|
3
|
+
type SeverityOverrides = DepsConfig['severityOverrides'];
|
|
4
|
+
export declare class ComposerCollector implements Collector {
|
|
5
|
+
private overrides;
|
|
6
|
+
type: "composer";
|
|
7
|
+
constructor(overrides: SeverityOverrides);
|
|
8
|
+
detect(appPath: string): boolean;
|
|
9
|
+
collect(app: AppEntry): Promise<Finding[]>;
|
|
10
|
+
private checkPackage;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { severityFromVersionDelta } from '../severity.js';
|
|
4
|
+
export class ComposerCollector {
|
|
5
|
+
overrides;
|
|
6
|
+
type = 'composer';
|
|
7
|
+
constructor(overrides) {
|
|
8
|
+
this.overrides = overrides;
|
|
9
|
+
}
|
|
10
|
+
detect(appPath) {
|
|
11
|
+
return existsSync(join(appPath, 'composer.json'));
|
|
12
|
+
}
|
|
13
|
+
async collect(app) {
|
|
14
|
+
const composerPath = join(app.composePath, 'composer.json');
|
|
15
|
+
if (!existsSync(composerPath))
|
|
16
|
+
return [];
|
|
17
|
+
const raw = readFileSync(composerPath, 'utf-8');
|
|
18
|
+
const composer = JSON.parse(raw);
|
|
19
|
+
const allDeps = {
|
|
20
|
+
...composer.require,
|
|
21
|
+
...composer['require-dev'],
|
|
22
|
+
};
|
|
23
|
+
const packages = Object.entries(allDeps).filter(([name]) => !name.startsWith('php') && !name.startsWith('ext-') && !name.startsWith('lib-'));
|
|
24
|
+
const findings = [];
|
|
25
|
+
const results = await Promise.allSettled(packages.map(([name, version]) => this.checkPackage(app.name, name, version)));
|
|
26
|
+
for (const result of results) {
|
|
27
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
28
|
+
findings.push(result.value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return findings;
|
|
32
|
+
}
|
|
33
|
+
async checkPackage(appName, name, currentRaw) {
|
|
34
|
+
const current = currentRaw.replace(/^[\^~>=<*]/, '').replace(/\.\*$/, '.0');
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`https://repo.packagist.org/p2/${name}.json`);
|
|
37
|
+
if (!res.ok)
|
|
38
|
+
return null;
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const versions = data.packages[name];
|
|
41
|
+
if (!versions?.length)
|
|
42
|
+
return null;
|
|
43
|
+
const stable = versions.find(v => /^\d+\.\d+\.\d+$/.test(v.version) || /^v\d+\.\d+\.\d+$/.test(v.version));
|
|
44
|
+
if (!stable)
|
|
45
|
+
return null;
|
|
46
|
+
const latest = stable.version.replace(/^v/, '');
|
|
47
|
+
if (current === latest)
|
|
48
|
+
return null;
|
|
49
|
+
const severity = severityFromVersionDelta(current, latest, this.overrides);
|
|
50
|
+
if (severity === 'info')
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
appName,
|
|
54
|
+
source: 'composer',
|
|
55
|
+
severity,
|
|
56
|
+
category: 'outdated-dep',
|
|
57
|
+
title: `${name} ${current} -> ${latest}`,
|
|
58
|
+
detail: `Composer package ${name} can be updated from ${current} to ${latest}`,
|
|
59
|
+
package: name,
|
|
60
|
+
currentVersion: current,
|
|
61
|
+
latestVersion: latest,
|
|
62
|
+
fixable: true,
|
|
63
|
+
updatedAt: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AppEntry } from '../../registry.js';
|
|
2
|
+
import type { Collector, Finding, DepsConfig } from '../types.js';
|
|
3
|
+
type SeverityOverrides = DepsConfig['severityOverrides'];
|
|
4
|
+
export interface ImageRef {
|
|
5
|
+
image: string;
|
|
6
|
+
tag: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class DockerImageCollector implements Collector {
|
|
9
|
+
private overrides;
|
|
10
|
+
type: "docker-image";
|
|
11
|
+
constructor(overrides: SeverityOverrides);
|
|
12
|
+
detect(appPath: string): boolean;
|
|
13
|
+
collect(app: AppEntry): Promise<Finding[]>;
|
|
14
|
+
parseDockerfile(content: string): ImageRef[];
|
|
15
|
+
parseComposeImages(content: string): ImageRef[];
|
|
16
|
+
private checkImage;
|
|
17
|
+
}
|
|
18
|
+
export {};
|