@matthesketh/fleet 1.2.0 → 1.6.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 +183 -251
- package/dist/adapters/detector/index.d.ts +8 -0
- package/dist/adapters/detector/index.js +54 -0
- package/dist/adapters/notifier/index.d.ts +2 -0
- package/dist/adapters/notifier/index.js +2 -0
- package/dist/adapters/notifier/stdout.d.ts +2 -0
- package/dist/adapters/notifier/stdout.js +8 -0
- package/dist/adapters/notifier/webhook.d.ts +9 -0
- package/dist/adapters/notifier/webhook.js +38 -0
- package/dist/adapters/runner/claude-cli.d.ts +7 -0
- package/dist/adapters/runner/claude-cli.js +231 -0
- package/dist/adapters/runner/mcp-call.d.ts +8 -0
- package/dist/adapters/runner/mcp-call.js +82 -0
- package/dist/adapters/runner/shell.d.ts +2 -0
- package/dist/adapters/runner/shell.js +103 -0
- package/dist/adapters/scheduler/systemd-timer.d.ts +17 -0
- package/dist/adapters/scheduler/systemd-timer.js +149 -0
- package/dist/adapters/signals/ci-status.d.ts +2 -0
- package/dist/adapters/signals/ci-status.js +79 -0
- package/dist/adapters/signals/container-up.d.ts +5 -0
- package/dist/adapters/signals/container-up.js +54 -0
- package/dist/adapters/signals/git-clean.d.ts +2 -0
- package/dist/adapters/signals/git-clean.js +55 -0
- package/dist/adapters/signals/index.d.ts +6 -0
- package/dist/adapters/signals/index.js +7 -0
- package/dist/adapters/types.d.ts +52 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli.js +43 -2
- package/dist/commands/add.js +0 -6
- package/dist/commands/boot-start.d.ts +1 -0
- package/dist/commands/boot-start.js +51 -0
- package/dist/commands/deploy.js +13 -0
- package/dist/commands/deps.js +5 -0
- package/dist/commands/egress.d.ts +1 -0
- package/dist/commands/egress.js +106 -0
- package/dist/commands/freeze.d.ts +4 -0
- package/dist/commands/freeze.js +64 -0
- package/dist/commands/logs.d.ts +1 -1
- package/dist/commands/logs.js +237 -8
- package/dist/commands/patch-systemd.d.ts +1 -0
- package/dist/commands/patch-systemd.js +126 -0
- package/dist/commands/rollback.d.ts +1 -0
- package/dist/commands/rollback.js +58 -0
- package/dist/commands/routine-run.d.ts +1 -0
- package/dist/commands/routine-run.js +122 -0
- package/dist/commands/routines.d.ts +1 -0
- package/dist/commands/routines.js +25 -0
- package/dist/commands/secrets.js +449 -16
- package/dist/commands/status.js +7 -3
- package/dist/commands/watchdog.d.ts +1 -1
- package/dist/commands/watchdog.js +16 -40
- package/dist/core/boot-refresh.d.ts +57 -0
- package/dist/core/boot-refresh.js +116 -0
- package/dist/core/deps/actors/pr-creator.js +11 -9
- package/dist/core/deps/collectors/docker-running.js +2 -2
- package/dist/core/deps/collectors/github-pr.js +5 -2
- package/dist/core/deps/collectors/npm.js +10 -5
- package/dist/core/deps/collectors/vulnerability.js +10 -6
- package/dist/core/deps/reporters/motd.js +1 -1
- package/dist/core/deps/reporters/telegram.js +2 -29
- package/dist/core/docker.js +45 -15
- package/dist/core/egress.d.ts +41 -0
- package/dist/core/egress.js +161 -0
- package/dist/core/exec.d.ts +7 -1
- package/dist/core/exec.js +25 -17
- package/dist/core/git.d.ts +1 -0
- package/dist/core/git.js +36 -23
- package/dist/core/github.js +27 -8
- package/dist/core/health.d.ts +3 -0
- package/dist/core/health.js +15 -3
- package/dist/core/logs-multi.d.ts +73 -0
- package/dist/core/logs-multi.js +163 -0
- package/dist/core/logs-policy.d.ts +55 -0
- package/dist/core/logs-policy.js +148 -0
- package/dist/core/nginx.js +8 -4
- package/dist/core/notify.d.ts +15 -0
- package/dist/core/notify.js +55 -0
- package/dist/core/registry.d.ts +25 -0
- package/dist/core/registry.js +57 -10
- package/dist/core/routines/cost-queries.d.ts +24 -0
- package/dist/core/routines/cost-queries.js +65 -0
- package/dist/core/routines/db.d.ts +9 -0
- package/dist/core/routines/db.js +126 -0
- package/dist/core/routines/defaults.d.ts +2 -0
- package/dist/core/routines/defaults.js +72 -0
- package/dist/core/routines/engine.d.ts +59 -0
- package/dist/core/routines/engine.js +175 -0
- package/dist/core/routines/incidents.d.ts +13 -0
- package/dist/core/routines/incidents.js +35 -0
- package/dist/core/routines/schema.d.ts +418 -0
- package/dist/core/routines/schema.js +113 -0
- package/dist/core/routines/signals-collector.d.ts +35 -0
- package/dist/core/routines/signals-collector.js +114 -0
- package/dist/core/routines/store.d.ts +316 -0
- package/dist/core/routines/store.js +99 -0
- package/dist/core/routines/test-utils.d.ts +2 -0
- package/dist/core/routines/test-utils.js +13 -0
- package/dist/core/secrets-audit.d.ts +21 -0
- package/dist/core/secrets-audit.js +60 -0
- package/dist/core/secrets-metadata.d.ts +39 -0
- package/dist/core/secrets-metadata.js +82 -0
- package/dist/core/secrets-motd.d.ts +20 -0
- package/dist/core/secrets-motd.js +72 -0
- package/dist/core/secrets-ops.d.ts +3 -1
- package/dist/core/secrets-ops.js +78 -13
- package/dist/core/secrets-providers.d.ts +50 -0
- package/dist/core/secrets-providers.js +291 -0
- package/dist/core/secrets-rotation.d.ts +52 -0
- package/dist/core/secrets-rotation.js +165 -0
- package/dist/core/secrets-snapshots.d.ts +26 -0
- package/dist/core/secrets-snapshots.js +95 -0
- package/dist/core/secrets-validate.js +2 -1
- package/dist/core/secrets.d.ts +12 -1
- package/dist/core/secrets.js +35 -24
- package/dist/core/self-update.d.ts +41 -0
- package/dist/core/self-update.js +73 -0
- package/dist/core/systemd.js +29 -12
- package/dist/core/telegram.d.ts +6 -0
- package/dist/core/telegram.js +32 -0
- package/dist/core/validate.d.ts +7 -0
- package/dist/core/validate.js +42 -0
- package/dist/index.js +0 -4
- package/dist/mcp/deps-tools.js +9 -1
- package/dist/mcp/git-tools.js +4 -4
- package/dist/mcp/server.js +193 -8
- package/dist/templates/systemd.js +3 -3
- package/dist/templates/unseal.js +5 -1
- package/dist/tui/components/KeyHint.js +10 -0
- package/dist/tui/exec-bridge.js +26 -12
- package/dist/tui/hooks/use-fleet-data.js +5 -2
- package/dist/tui/hooks/use-health.js +5 -2
- package/dist/tui/router.js +60 -7
- package/dist/tui/routines/RoutinesApp.d.ts +8 -0
- package/dist/tui/routines/RoutinesApp.js +277 -0
- package/dist/tui/routines/components/AlertsPanel.d.ts +7 -0
- package/dist/tui/routines/components/AlertsPanel.js +22 -0
- package/dist/tui/routines/components/AlertsPanel.test.d.ts +1 -0
- package/dist/tui/routines/components/AlertsPanel.test.js +52 -0
- package/dist/tui/routines/components/CommandPalette.d.ts +12 -0
- package/dist/tui/routines/components/CommandPalette.js +21 -0
- package/dist/tui/routines/components/LiveRunPanel.d.ts +12 -0
- package/dist/tui/routines/components/LiveRunPanel.js +107 -0
- package/dist/tui/routines/components/RoutineForm.d.ts +8 -0
- package/dist/tui/routines/components/RoutineForm.js +254 -0
- package/dist/tui/routines/components/SignalsGrid.d.ts +13 -0
- package/dist/tui/routines/components/SignalsGrid.js +34 -0
- package/dist/tui/routines/components/SignalsGrid.test.d.ts +1 -0
- package/dist/tui/routines/components/SignalsGrid.test.js +43 -0
- package/dist/tui/routines/format.d.ts +7 -0
- package/dist/tui/routines/format.js +51 -0
- package/dist/tui/routines/hooks/use-git-fleet.d.ts +33 -0
- package/dist/tui/routines/hooks/use-git-fleet.js +82 -0
- package/dist/tui/routines/hooks/use-logs-stream.d.ts +13 -0
- package/dist/tui/routines/hooks/use-logs-stream.js +64 -0
- package/dist/tui/routines/hooks/use-ops-fleet.d.ts +20 -0
- package/dist/tui/routines/hooks/use-ops-fleet.js +70 -0
- package/dist/tui/routines/hooks/use-repo-detail.d.ts +31 -0
- package/dist/tui/routines/hooks/use-repo-detail.js +104 -0
- package/dist/tui/routines/hooks/use-security.d.ts +33 -0
- package/dist/tui/routines/hooks/use-security.js +110 -0
- package/dist/tui/routines/hooks/use-signals.d.ts +9 -0
- package/dist/tui/routines/hooks/use-signals.js +60 -0
- package/dist/tui/routines/runtime.d.ts +20 -0
- package/dist/tui/routines/runtime.js +40 -0
- package/dist/tui/routines/tabs/CostTab.d.ts +7 -0
- package/dist/tui/routines/tabs/CostTab.js +24 -0
- package/dist/tui/routines/tabs/DashboardTab.d.ts +15 -0
- package/dist/tui/routines/tabs/DashboardTab.js +10 -0
- package/dist/tui/routines/tabs/GitTab.d.ts +6 -0
- package/dist/tui/routines/tabs/GitTab.js +39 -0
- package/dist/tui/routines/tabs/LogsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/LogsTab.js +58 -0
- package/dist/tui/routines/tabs/OpsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/OpsTab.js +34 -0
- package/dist/tui/routines/tabs/RepoDetailView.d.ts +6 -0
- package/dist/tui/routines/tabs/RepoDetailView.js +12 -0
- package/dist/tui/routines/tabs/RoutinesTab.d.ts +10 -0
- package/dist/tui/routines/tabs/RoutinesTab.js +58 -0
- package/dist/tui/routines/tabs/ScaffoldTab.d.ts +2 -0
- package/dist/tui/routines/tabs/ScaffoldTab.js +127 -0
- package/dist/tui/routines/tabs/SecurityTab.d.ts +6 -0
- package/dist/tui/routines/tabs/SecurityTab.js +31 -0
- package/dist/tui/routines/tabs/SettingsTab.d.ts +6 -0
- package/dist/tui/routines/tabs/SettingsTab.js +61 -0
- package/dist/tui/routines/tabs/TimelineTab.d.ts +7 -0
- package/dist/tui/routines/tabs/TimelineTab.js +26 -0
- package/dist/tui/state.js +1 -1
- package/dist/tui/tests/keyboard-integration.test.js +3 -0
- package/dist/tui/tests/test-app.js +1 -1
- package/dist/tui/types.d.ts +2 -2
- package/dist/tui/views/AppDetail.js +3 -4
- package/dist/tui/views/HealthView.js +7 -1
- package/dist/tui/views/LogsView.js +24 -1
- package/dist/tui/views/MultiLogsView.d.ts +2 -0
- package/dist/tui/views/MultiLogsView.js +165 -0
- package/dist/tui/views/SecretEdit.js +10 -3
- package/dist/tui/views/SecretsView.js +6 -3
- package/dist/ui/prompt.d.ts +52 -0
- package/dist/ui/prompt.js +169 -0
- package/package.json +33 -21
- package/dist/commands/motd.d.ts +0 -1
- package/dist/commands/motd.js +0 -10
- package/dist/templates/motd.d.ts +0 -1
- package/dist/templates/motd.js +0 -7
- package/dist/tui/components/AppList.d.ts +0 -12
- package/dist/tui/components/AppList.js +0 -32
- package/dist/tui/hooks/use-keyboard.d.ts +0 -1
- package/dist/tui/hooks/use-keyboard.js +0 -44
package/dist/commands/logs.js
CHANGED
|
@@ -2,31 +2,260 @@ import { load, findApp } from '../core/registry.js';
|
|
|
2
2
|
import { getContainerLogs } from '../core/docker.js';
|
|
3
3
|
import { execLive } from '../core/exec.js';
|
|
4
4
|
import { AppNotFoundError } from '../core/errors.js';
|
|
5
|
-
import { error } from '../ui/output.js';
|
|
5
|
+
import { c, error, heading, info, success, table, warn } from '../ui/output.js';
|
|
6
|
+
import { confirm } from '../ui/confirm.js';
|
|
7
|
+
import { prompt } from '../ui/prompt.js';
|
|
8
|
+
import { effectivePolicy, writeComposeOverride, getLogStatus, pruneLogs, readContainerLogs, } from '../core/logs-policy.js';
|
|
9
|
+
import { startMultiTail, resolveSources } from '../core/logs-multi.js';
|
|
6
10
|
export function logsCommand(args) {
|
|
11
|
+
const sub = args[0];
|
|
12
|
+
if (sub === 'setup')
|
|
13
|
+
return logsSetup(args.slice(1));
|
|
14
|
+
if (sub === 'status')
|
|
15
|
+
return logsStatus(args.slice(1));
|
|
16
|
+
if (sub === 'prune')
|
|
17
|
+
return logsPrune(args.slice(1));
|
|
18
|
+
// --all / --apps / --containers route to the multi-source tail.
|
|
19
|
+
if (args.includes('--all') || args.includes('--apps') || args.includes('--containers')) {
|
|
20
|
+
return logsMulti(args);
|
|
21
|
+
}
|
|
22
|
+
// Default: single-app tail / follow — synchronous so existing test
|
|
23
|
+
// expectations and process.exit semantics work unchanged.
|
|
24
|
+
return logsTail(args);
|
|
25
|
+
}
|
|
26
|
+
// ── Slice 1: multi-source CLI tail ─────────────────────────────────────────
|
|
27
|
+
const SOURCE_COLORS = [c.cyan, c.green, c.yellow, c.magenta, c.blue, c.red];
|
|
28
|
+
const sourceColorCache = new Map();
|
|
29
|
+
function colorForSource(name) {
|
|
30
|
+
let cached = sourceColorCache.get(name);
|
|
31
|
+
if (cached)
|
|
32
|
+
return cached;
|
|
33
|
+
// Stable hash → colour assignment so the same source always gets the same colour.
|
|
34
|
+
let h = 0;
|
|
35
|
+
for (let i = 0; i < name.length; i++)
|
|
36
|
+
h = (h * 31 + name.charCodeAt(i)) | 0;
|
|
37
|
+
cached = SOURCE_COLORS[Math.abs(h) % SOURCE_COLORS.length];
|
|
38
|
+
sourceColorCache.set(name, cached);
|
|
39
|
+
return cached;
|
|
40
|
+
}
|
|
41
|
+
function logsMulti(args) {
|
|
42
|
+
const all = args.includes('--all');
|
|
43
|
+
const follow = args.includes('-f') || args.includes('--follow');
|
|
44
|
+
const valOf = (flag) => {
|
|
45
|
+
const i = args.indexOf(flag);
|
|
46
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
47
|
+
};
|
|
48
|
+
const appsCsv = valOf('--apps');
|
|
49
|
+
const containersCsv = valOf('--containers');
|
|
50
|
+
const since = valOf('--since');
|
|
51
|
+
const grep = valOf('--grep');
|
|
52
|
+
const level = valOf('--level');
|
|
53
|
+
const tail = parseInt(valOf('--tail') ?? valOf('-n') ?? '50', 10) || 50;
|
|
54
|
+
if (!all && !appsCsv && !containersCsv) {
|
|
55
|
+
error('Usage: fleet logs --all [-f] [--since 15m] [--grep err] [--level warn]');
|
|
56
|
+
error(' fleet logs --apps macpool,shiftfaced [-f]');
|
|
57
|
+
error(' fleet logs --containers "*-postgres" [-f]');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
const reg = load();
|
|
61
|
+
const sources = resolveSources(reg.apps, {
|
|
62
|
+
apps: appsCsv ? appsCsv.split(',').map(s => s.trim()).filter(Boolean) : undefined,
|
|
63
|
+
containers: containersCsv ? containersCsv.split(',').map(s => s.trim()).filter(Boolean) : undefined,
|
|
64
|
+
});
|
|
65
|
+
if (sources.length === 0) {
|
|
66
|
+
error('No matching containers found.');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
// Width-align the prefix so lines stack readably.
|
|
70
|
+
const maxLabelLen = Math.max(...sources.map(s => `${s.app}/${s.container}`.length));
|
|
71
|
+
const onLine = (l) => {
|
|
72
|
+
const label = `${l.app}/${l.container}`.padEnd(maxLabelLen);
|
|
73
|
+
const colour = colorForSource(`${l.app}/${l.container}`);
|
|
74
|
+
process.stdout.write(`${colour}${label}${c.reset} ${l.text}\n`);
|
|
75
|
+
};
|
|
76
|
+
return new Promise(resolve => {
|
|
77
|
+
const handle = startMultiTail(sources, { tail, since, grep, level, follow }, onLine);
|
|
78
|
+
const shutdown = async (signal) => {
|
|
79
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
80
|
+
process.removeListener('SIGTERM', sigtermHandler);
|
|
81
|
+
await handle.stop();
|
|
82
|
+
if (signal !== 'exit')
|
|
83
|
+
process.stderr.write(`\nStopped (${signal})\n`);
|
|
84
|
+
resolve();
|
|
85
|
+
};
|
|
86
|
+
const sigintHandler = () => { void shutdown('SIGINT'); };
|
|
87
|
+
const sigtermHandler = () => { void shutdown('SIGTERM'); };
|
|
88
|
+
process.on('SIGINT', sigintHandler);
|
|
89
|
+
process.on('SIGTERM', sigtermHandler);
|
|
90
|
+
// Non-follow: poll until all tailers have closed, then resolve.
|
|
91
|
+
if (!follow) {
|
|
92
|
+
const tick = () => {
|
|
93
|
+
if (handle.active() === 0) {
|
|
94
|
+
void shutdown('exit');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setTimeout(tick, 100);
|
|
98
|
+
};
|
|
99
|
+
setTimeout(tick, 100);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function logsTail(args) {
|
|
7
104
|
const follow = args.includes('-f') || args.includes('--follow');
|
|
8
105
|
const nIdx = args.indexOf('-n');
|
|
9
106
|
const lines = nIdx >= 0 ? parseInt(args[nIdx + 1], 10) || 100 : 100;
|
|
10
|
-
const
|
|
107
|
+
const cIdx = args.indexOf('-c');
|
|
108
|
+
const containerArg = cIdx >= 0 ? args[cIdx + 1] : undefined;
|
|
109
|
+
const sinceIdx = args.indexOf('--since');
|
|
110
|
+
const since = sinceIdx >= 0 ? args[sinceIdx + 1] : undefined;
|
|
111
|
+
const grepIdx = args.indexOf('--grep');
|
|
112
|
+
const grep = grepIdx >= 0 ? args[grepIdx + 1] : undefined;
|
|
113
|
+
const levelIdx = args.indexOf('--level');
|
|
114
|
+
const level = levelIdx >= 0 ? args[levelIdx + 1] : undefined;
|
|
115
|
+
const skipIndices = new Set();
|
|
116
|
+
for (const i of [nIdx, cIdx, sinceIdx, grepIdx, levelIdx]) {
|
|
117
|
+
if (i >= 0) {
|
|
118
|
+
skipIndices.add(i);
|
|
119
|
+
skipIndices.add(i + 1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const appName = args.find((a, i) => !a.startsWith('-') && !skipIndices.has(i));
|
|
11
123
|
if (!appName) {
|
|
12
|
-
error('Usage: fleet logs <app> [-f] [-n <lines>]');
|
|
124
|
+
error('Usage: fleet logs <app> [-f] [-n <lines>] [-c <container>]');
|
|
125
|
+
error(' Subcommands: setup [--all] | status [<app>] | prune <app>');
|
|
126
|
+
error(' Tail filters: --since <Nm|Nh> | --grep <text> | --level info|warn|error');
|
|
13
127
|
process.exit(1);
|
|
14
128
|
}
|
|
15
129
|
const reg = load();
|
|
16
130
|
const app = findApp(reg, appName);
|
|
17
131
|
if (!app)
|
|
18
132
|
throw new AppNotFoundError(appName);
|
|
19
|
-
|
|
20
|
-
if (!container) {
|
|
133
|
+
if (app.containers.length === 0) {
|
|
21
134
|
error(`No containers registered for ${app.name}`);
|
|
22
135
|
process.exit(1);
|
|
23
136
|
}
|
|
137
|
+
let container = containerArg ?? app.containers[0];
|
|
138
|
+
if (containerArg && !app.containers.includes(containerArg)) {
|
|
139
|
+
error(`Container "${containerArg}" not found in ${app.name}. Available:`);
|
|
140
|
+
for (const ct of app.containers)
|
|
141
|
+
process.stderr.write(` - ${ct}\n`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
24
144
|
if (follow) {
|
|
25
|
-
|
|
145
|
+
// For follow mode we delegate to native docker — filtering would buffer.
|
|
146
|
+
const dockerArgs = ['logs', '-f', '--tail', lines.toString()];
|
|
147
|
+
if (since)
|
|
148
|
+
dockerArgs.push('--since', since);
|
|
149
|
+
dockerArgs.push(container);
|
|
150
|
+
const code = execLive('docker', dockerArgs);
|
|
26
151
|
process.exit(code);
|
|
27
152
|
}
|
|
153
|
+
// Non-follow: use the policy-aware reader so --level / --grep / size cap apply.
|
|
154
|
+
if (since || grep || level) {
|
|
155
|
+
const sinceMinutes = since ? parseSinceMinutes(since) : undefined;
|
|
156
|
+
const result = readContainerLogs(container, { lines, level, sinceMinutes, grep });
|
|
157
|
+
process.stdout.write(result.text + '\n');
|
|
158
|
+
if (result.truncated) {
|
|
159
|
+
warn('Output truncated at 200KB. Narrow with --since/--grep/--level/-n.');
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Plain tail: existing fast path.
|
|
164
|
+
const output = getContainerLogs(container, lines);
|
|
165
|
+
process.stdout.write(output + '\n');
|
|
166
|
+
}
|
|
167
|
+
function parseSinceMinutes(s) {
|
|
168
|
+
const m = s.match(/^(\d+)([mhd])?$/);
|
|
169
|
+
if (!m)
|
|
170
|
+
return 60;
|
|
171
|
+
const n = parseInt(m[1], 10);
|
|
172
|
+
const unit = m[2] ?? 'm';
|
|
173
|
+
return unit === 'h' ? n * 60 : unit === 'd' ? n * 1440 : n;
|
|
174
|
+
}
|
|
175
|
+
async function logsSetup(args) {
|
|
176
|
+
const all = args.includes('--all');
|
|
177
|
+
const yes = args.includes('-y') || args.includes('--yes');
|
|
178
|
+
const reg = load();
|
|
179
|
+
const apps = all ? reg.apps : (() => {
|
|
180
|
+
const name = args.find(a => !a.startsWith('-'));
|
|
181
|
+
if (!name) {
|
|
182
|
+
error('Usage: fleet logs setup <app> OR fleet logs setup --all');
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
const a = findApp(reg, name);
|
|
186
|
+
if (!a)
|
|
187
|
+
throw new AppNotFoundError(name);
|
|
188
|
+
return [a];
|
|
189
|
+
})();
|
|
190
|
+
let policy;
|
|
191
|
+
if (all || yes) {
|
|
192
|
+
policy = { retentionDays: 7, maxSizeMB: 100, level: 'info' };
|
|
193
|
+
info(`Applying default policy: ${policy.maxSizeMB}MB / ${policy.retentionDays}d / ${policy.level}`);
|
|
194
|
+
}
|
|
28
195
|
else {
|
|
29
|
-
const
|
|
30
|
-
|
|
196
|
+
const ret = await prompt('Retention days', '7');
|
|
197
|
+
const size = await prompt('Max size MB per container', '100');
|
|
198
|
+
const lvl = await prompt('Min level (debug|info|warn|error)', 'info');
|
|
199
|
+
policy = {
|
|
200
|
+
retentionDays: parseInt(ret, 10) || 7,
|
|
201
|
+
maxSizeMB: parseInt(size, 10) || 100,
|
|
202
|
+
level: lvl ?? 'info',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
for (const app of apps) {
|
|
206
|
+
const path = writeComposeOverride(app, policy);
|
|
207
|
+
success(`${app.name}: wrote ${path}`);
|
|
208
|
+
}
|
|
209
|
+
info('To activate: include the override in your compose start command,');
|
|
210
|
+
info(' e.g. `docker compose -f docker-compose.yml -f .fleet/logging.override.yml up -d`');
|
|
211
|
+
info('Or have fleet patch the systemd unit (see: fleet patch-systemd).');
|
|
212
|
+
}
|
|
213
|
+
function logsStatus(args) {
|
|
214
|
+
const json = args.includes('--json');
|
|
215
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
216
|
+
const reg = load();
|
|
217
|
+
const apps = appName ? [findApp(reg, appName)].filter(Boolean) : reg.apps;
|
|
218
|
+
const rows = [];
|
|
219
|
+
const data = [];
|
|
220
|
+
for (const app of apps) {
|
|
221
|
+
if (!app)
|
|
222
|
+
continue;
|
|
223
|
+
const policy = effectivePolicy(app);
|
|
224
|
+
const status = getLogStatus(app);
|
|
225
|
+
for (const s of status) {
|
|
226
|
+
data.push({ ...s, policy });
|
|
227
|
+
const sizeStr = s.totalBytes != null ? `${(s.totalBytes / 1024 / 1024).toFixed(1)}M` : '?';
|
|
228
|
+
const policyStr = `${policy.maxSizeMB}M/${policy.retentionDays}d/${policy.level}`;
|
|
229
|
+
const ind = s.policyApplied ? `${c.green}*${c.reset}` : `${c.yellow}!${c.reset}`;
|
|
230
|
+
rows.push([app.name, s.container, s.driver, sizeStr, policyStr, ind]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (json) {
|
|
234
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
heading(`Log status (${rows.length} containers)`);
|
|
238
|
+
table(['APP', 'CONTAINER', 'DRIVER', 'SIZE', 'POLICY', 'CONFIGURED'], rows);
|
|
239
|
+
process.stdout.write('\n');
|
|
240
|
+
info('* = override file present, ! = using docker defaults (unbounded by default)');
|
|
241
|
+
}
|
|
242
|
+
async function logsPrune(args) {
|
|
243
|
+
const yes = args.includes('-y') || args.includes('--yes');
|
|
244
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
245
|
+
if (!appName) {
|
|
246
|
+
error('Usage: fleet logs prune <app>');
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
const reg = load();
|
|
250
|
+
const app = findApp(reg, appName);
|
|
251
|
+
if (!app)
|
|
252
|
+
throw new AppNotFoundError(appName);
|
|
253
|
+
const policy = effectivePolicy(app);
|
|
254
|
+
warn(`Will vacuum journald to ${policy.retentionDays}d and truncate any json-file logs > 5x the policy max.`);
|
|
255
|
+
if (!yes && !await confirm('Proceed?', false)) {
|
|
256
|
+
info('Cancelled');
|
|
257
|
+
return;
|
|
31
258
|
}
|
|
259
|
+
const freed = pruneLogs(app, policy);
|
|
260
|
+
success(`Freed approximately ${(freed / 1024 / 1024).toFixed(1)}MB from json-file logs (journald vacuum applied separately).`);
|
|
32
261
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function patchSystemdCommand(args: string[]): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { load } from '../core/registry.js';
|
|
3
|
+
import { readServiceFile } from '../core/systemd.js';
|
|
4
|
+
import { execSafe } from '../core/exec.js';
|
|
5
|
+
import { success, warn, info, error } from '../ui/output.js';
|
|
6
|
+
const SERVICE_DIR = '/etc/systemd/system';
|
|
7
|
+
export function patchSystemdCommand(args) {
|
|
8
|
+
if (args.includes('--rollback'))
|
|
9
|
+
return rollback();
|
|
10
|
+
const reg = load();
|
|
11
|
+
const dbServiceName = reg.infrastructure.databases.serviceName;
|
|
12
|
+
const appServiceNames = reg.apps.map(a => a.serviceName);
|
|
13
|
+
// dedupe by service name with infra (rewriteExecStart=false) winning. a stale
|
|
14
|
+
// registry can list docker-databases under both reg.apps and infrastructure;
|
|
15
|
+
// without this guard the apps entry would rewrite ExecStart on the shared
|
|
16
|
+
// databases service, defeating the safety carve-out.
|
|
17
|
+
const targetMap = new Map();
|
|
18
|
+
for (const name of appServiceNames) {
|
|
19
|
+
targetMap.set(name, { name, rewriteExecStart: true });
|
|
20
|
+
}
|
|
21
|
+
targetMap.set(dbServiceName, { name: dbServiceName, rewriteExecStart: false });
|
|
22
|
+
const targets = Array.from(targetMap.values());
|
|
23
|
+
info(`Patching ${targets.length} service(s)...`);
|
|
24
|
+
let patched = 0;
|
|
25
|
+
let skipped = 0;
|
|
26
|
+
for (const { name, rewriteExecStart } of targets) {
|
|
27
|
+
const path = `${SERVICE_DIR}/${name}.service`;
|
|
28
|
+
const content = readServiceFile(name);
|
|
29
|
+
if (content === null) {
|
|
30
|
+
warn(`${name}: no service file found, skipping`);
|
|
31
|
+
skipped++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
let updated = content;
|
|
35
|
+
let changed = false;
|
|
36
|
+
// Existing behavior: add StartLimitBurst if missing (applies to ALL services including databases)
|
|
37
|
+
if (!updated.includes('StartLimitBurst=')) {
|
|
38
|
+
updated = updated.replace(/(\[Service\])/, '$1\nStartLimitBurst=5\nStartLimitIntervalSec=300');
|
|
39
|
+
changed = true;
|
|
40
|
+
}
|
|
41
|
+
// ExecStart + TimeoutStartSec rewrite ONLY for app services — databases has no git repo
|
|
42
|
+
if (rewriteExecStart) {
|
|
43
|
+
const expectedExecStart = `ExecStart=/usr/bin/env fleet boot-start ${name}`;
|
|
44
|
+
if (!updated.includes(expectedExecStart)) {
|
|
45
|
+
updated = updated.replace(/^ExecStart=.*$/m, expectedExecStart);
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
// Ensure TimeoutStartSec=900
|
|
49
|
+
if (!updated.includes('TimeoutStartSec=900')) {
|
|
50
|
+
if (/^TimeoutStartSec=\d+/m.test(updated)) {
|
|
51
|
+
updated = updated.replace(/^TimeoutStartSec=\d+.*$/m, 'TimeoutStartSec=900');
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
updated = updated.replace(/(\[Service\])/, '$1\nTimeoutStartSec=900');
|
|
55
|
+
}
|
|
56
|
+
changed = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!changed) {
|
|
60
|
+
info(`${name}: already patched, skipping`);
|
|
61
|
+
skipped++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Backup original before overwrite
|
|
65
|
+
try {
|
|
66
|
+
copyFileSync(path, `${path}.bak`);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
warn(`${name}: failed to create .bak (${err instanceof Error ? err.message : String(err)}); skipping for safety`);
|
|
70
|
+
skipped++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
writeFileSync(path, updated);
|
|
74
|
+
success(`${name}: patched`);
|
|
75
|
+
patched++;
|
|
76
|
+
}
|
|
77
|
+
if (patched === 0) {
|
|
78
|
+
info('No services needed patching');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
info('Running systemctl daemon-reload...');
|
|
82
|
+
const result = execSafe('systemctl', ['daemon-reload']);
|
|
83
|
+
if (result.ok) {
|
|
84
|
+
success(`Done — patched ${patched} service(s), skipped ${skipped}`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
warn(`daemon-reload failed: ${result.stderr}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function rollback() {
|
|
91
|
+
const reg = load();
|
|
92
|
+
const serviceNames = [
|
|
93
|
+
...reg.apps.map(a => a.serviceName),
|
|
94
|
+
reg.infrastructure.databases.serviceName,
|
|
95
|
+
];
|
|
96
|
+
let restored = 0;
|
|
97
|
+
let missing = 0;
|
|
98
|
+
for (const name of serviceNames) {
|
|
99
|
+
const path = `${SERVICE_DIR}/${name}.service`;
|
|
100
|
+
const bak = `${path}.bak`;
|
|
101
|
+
if (!existsSync(bak)) {
|
|
102
|
+
missing++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
renameSync(bak, path);
|
|
107
|
+
success(`${name}: restored from .bak`);
|
|
108
|
+
restored++;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
error(`${name}: failed to restore: ${err instanceof Error ? err.message : String(err)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (restored === 0) {
|
|
115
|
+
info('No .bak files found to restore');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
info('Running systemctl daemon-reload...');
|
|
119
|
+
const result = execSafe('systemctl', ['daemon-reload']);
|
|
120
|
+
if (result.ok) {
|
|
121
|
+
success(`Done — restored ${restored}, missing ${missing}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
warn(`daemon-reload failed: ${result.stderr}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function rollbackCommand(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { load, findApp } from '../core/registry.js';
|
|
2
|
+
import { execSafe } from '../core/exec.js';
|
|
3
|
+
import { restartService } from '../core/systemd.js';
|
|
4
|
+
function log(msg) {
|
|
5
|
+
process.stdout.write(`[rollback] ${msg}\n`);
|
|
6
|
+
}
|
|
7
|
+
function logErr(msg) {
|
|
8
|
+
process.stderr.write(`[rollback] ${msg}\n`);
|
|
9
|
+
}
|
|
10
|
+
function resolveImageName(composePath, composeFile) {
|
|
11
|
+
const args = ['compose', ...(composeFile ? ['-f', composeFile] : []), 'config', '--images'];
|
|
12
|
+
const r = execSafe('docker', args, { cwd: composePath, timeout: 15_000 });
|
|
13
|
+
if (!r.ok)
|
|
14
|
+
return null;
|
|
15
|
+
return r.stdout.split('\n').filter(Boolean)[0] ?? null;
|
|
16
|
+
}
|
|
17
|
+
function splitImageBase(image) {
|
|
18
|
+
const lastColon = image.lastIndexOf(':');
|
|
19
|
+
if (lastColon <= 0)
|
|
20
|
+
return image;
|
|
21
|
+
return image.slice(0, lastColon);
|
|
22
|
+
}
|
|
23
|
+
export async function rollbackCommand(args) {
|
|
24
|
+
const appName = args[0];
|
|
25
|
+
if (!appName) {
|
|
26
|
+
logErr('Usage: fleet rollback <app>');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const reg = load();
|
|
30
|
+
const app = findApp(reg, appName);
|
|
31
|
+
if (!app) {
|
|
32
|
+
logErr(`app not found: ${appName}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const image = resolveImageName(app.composePath, app.composeFile);
|
|
36
|
+
if (!image) {
|
|
37
|
+
logErr(`could not resolve image name for ${app.name}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const base = splitImageBase(image);
|
|
41
|
+
const previous = `${base}:fleet-previous`;
|
|
42
|
+
const latest = image;
|
|
43
|
+
if (!execSafe('docker', ['image', 'inspect', previous], { timeout: 10_000 }).ok) {
|
|
44
|
+
logErr(`no previous image found (${previous}) — nothing to roll back to`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const tag = execSafe('docker', ['tag', previous, latest], { timeout: 10_000 });
|
|
48
|
+
if (!tag.ok) {
|
|
49
|
+
logErr(`docker tag failed: ${tag.stderr || `exit ${tag.exitCode}`}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const ok = restartService(app.serviceName);
|
|
53
|
+
if (!ok) {
|
|
54
|
+
logErr(`tag restored but service restart failed for ${app.serviceName}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
log(`rolled back ${app.name} to ${previous}`);
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function routineRunCommand(argv: string[]): Promise<void>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { load } from '../core/registry.js';
|
|
2
|
+
import { createRuntime } from '../tui/routines/runtime.js';
|
|
3
|
+
import { error } from '../ui/output.js';
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
const out = {};
|
|
6
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7
|
+
const arg = argv[i];
|
|
8
|
+
switch (arg) {
|
|
9
|
+
case '--id':
|
|
10
|
+
out.id = argv[++i];
|
|
11
|
+
break;
|
|
12
|
+
case '--target':
|
|
13
|
+
out.target = argv[++i];
|
|
14
|
+
break;
|
|
15
|
+
case '--trigger': {
|
|
16
|
+
const v = argv[++i];
|
|
17
|
+
if (v === 'manual' || v === 'scheduled' || v === 'api')
|
|
18
|
+
out.trigger = v;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case '--json':
|
|
22
|
+
out.json = true;
|
|
23
|
+
break;
|
|
24
|
+
case '-h':
|
|
25
|
+
case '--help':
|
|
26
|
+
out.help = true;
|
|
27
|
+
break;
|
|
28
|
+
default:
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
const HELP = `fleet routine-run - execute a registered routine
|
|
35
|
+
|
|
36
|
+
Usage: fleet routine-run --id <routine-id> [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--id <id> Required. The routine id to run.
|
|
40
|
+
--target <repo> Optional. Run scoped to a single registered repo.
|
|
41
|
+
--trigger <source> manual (default) | scheduled | api
|
|
42
|
+
--json Emit events as JSON lines to stdout
|
|
43
|
+
-h, --help Show this help
|
|
44
|
+
|
|
45
|
+
Exit codes:
|
|
46
|
+
0 routine ended with status=ok
|
|
47
|
+
1 routine ended with status=failed or timeout or aborted
|
|
48
|
+
2 invocation error (unknown id, bad args)
|
|
49
|
+
|
|
50
|
+
Intended entrypoint for systemd-timer-generated units. Persists every
|
|
51
|
+
event to sqlite and exits with the run's status.
|
|
52
|
+
`;
|
|
53
|
+
export async function routineRunCommand(argv) {
|
|
54
|
+
const args = parseArgs(argv);
|
|
55
|
+
if (args.help || !args.id) {
|
|
56
|
+
process.stdout.write(HELP);
|
|
57
|
+
process.exit(args.help ? 0 : 2);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const runtime = createRuntime({ seedDefaults: false });
|
|
61
|
+
const registry = load();
|
|
62
|
+
const repoPath = args.target
|
|
63
|
+
? registry.apps.find(a => a.name === args.target)?.composePath ?? null
|
|
64
|
+
: null;
|
|
65
|
+
if (args.target && !repoPath) {
|
|
66
|
+
error(`unknown target repo: ${args.target}`);
|
|
67
|
+
runtime.close();
|
|
68
|
+
process.exit(2);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const routine = runtime.store.get(args.id);
|
|
72
|
+
if (!routine) {
|
|
73
|
+
error(`routine not found: ${args.id}`);
|
|
74
|
+
runtime.close();
|
|
75
|
+
process.exit(2);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const ac = new AbortController();
|
|
79
|
+
const onSignal = () => ac.abort();
|
|
80
|
+
process.on('SIGINT', onSignal);
|
|
81
|
+
process.on('SIGTERM', onSignal);
|
|
82
|
+
let finalStatus = null;
|
|
83
|
+
try {
|
|
84
|
+
for await (const ev of runtime.engine.runOnce(args.id, { repo: args.target ?? null, repoPath }, args.trigger ?? 'manual', ac.signal)) {
|
|
85
|
+
if (args.json) {
|
|
86
|
+
process.stdout.write(`${JSON.stringify(ev)}\n`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
switch (ev.kind) {
|
|
90
|
+
case 'start':
|
|
91
|
+
process.stdout.write(`▶ ${args.id}${ev.target ? ` · ${ev.target}` : ''}\n`);
|
|
92
|
+
break;
|
|
93
|
+
case 'stdout':
|
|
94
|
+
process.stdout.write(ev.chunk);
|
|
95
|
+
break;
|
|
96
|
+
case 'stderr':
|
|
97
|
+
process.stderr.write(ev.chunk);
|
|
98
|
+
break;
|
|
99
|
+
case 'tool-call':
|
|
100
|
+
process.stdout.write(` ↳ ${ev.name}${ev.argsPreview ? ` ${ev.argsPreview}` : ''}\n`);
|
|
101
|
+
break;
|
|
102
|
+
case 'cost':
|
|
103
|
+
process.stdout.write(` $${ev.usd.toFixed(4)} in=${ev.inputTokens} out=${ev.outputTokens}\n`);
|
|
104
|
+
break;
|
|
105
|
+
case 'end':
|
|
106
|
+
finalStatus = ev.status;
|
|
107
|
+
process.stdout.write(`◼ ${ev.status} exit=${ev.exitCode} (${ev.durationMs}ms)${ev.error ? ` — ${ev.error}` : ''}\n`);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
error(`run failed: ${err.message}`);
|
|
114
|
+
runtime.close();
|
|
115
|
+
process.exit(1);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
process.off('SIGINT', onSignal);
|
|
119
|
+
process.off('SIGTERM', onSignal);
|
|
120
|
+
runtime.close();
|
|
121
|
+
process.exit(finalStatus === 'ok' ? 0 : 1);
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function routinesCommand(_args: string[]): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { render, Box, Text } from 'ink';
|
|
3
|
+
import { InputDispatcher } from '@matthesketh/ink-input-dispatcher';
|
|
4
|
+
import { Viewport } from '@matthesketh/ink-viewport';
|
|
5
|
+
import { ToastProvider, ToastContainer } from '@matthesketh/ink-toast';
|
|
6
|
+
import { load } from '../core/registry.js';
|
|
7
|
+
import { RoutinesApp } from '../tui/routines/RoutinesApp.js';
|
|
8
|
+
import { createRuntime } from '../tui/routines/runtime.js';
|
|
9
|
+
function Shell({ runtime, registry }) {
|
|
10
|
+
const globalHandler = (input, _key) => {
|
|
11
|
+
if (input === 'q') {
|
|
12
|
+
runtime.close();
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
return (_jsx(ToastProvider, { children: _jsx(InputDispatcher, { globalHandler: globalHandler, children: _jsx(Viewport, { chrome: 2, children: _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsxs(Box, { paddingX: 1, paddingY: 0, children: [_jsx(Text, { bold: true, color: "cyan", children: "fleet routines" }), _jsx(Text, { color: "gray", children: " \u00B7 q to quit" })] }), _jsx(RoutinesApp, { runtime: runtime, registry: registry }), _jsx(ToastContainer, {})] }) }) }) }));
|
|
18
|
+
}
|
|
19
|
+
export async function routinesCommand(_args) {
|
|
20
|
+
const runtime = createRuntime();
|
|
21
|
+
const registry = load();
|
|
22
|
+
const { waitUntilExit } = render(_jsx(Shell, { runtime: runtime, registry: registry }));
|
|
23
|
+
await waitUntilExit();
|
|
24
|
+
runtime.close();
|
|
25
|
+
}
|