@matthesketh/fleet 1.1.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/Confirm.js +3 -4
- package/dist/tui/components/Header.js +37 -8
- package/dist/tui/components/KeyHint.js +14 -5
- 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/hooks/use-terminal-size.d.ts +1 -0
- package/dist/tui/hooks/use-terminal-size.js +1 -0
- package/dist/tui/router.js +133 -8
- 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 +16 -1
- 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 +120 -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 +14 -1
- package/dist/tui/views/AppDetail.js +40 -26
- package/dist/tui/views/Dashboard.js +34 -9
- package/dist/tui/views/HealthView.js +42 -12
- package/dist/tui/views/LogsView.js +38 -10
- package/dist/tui/views/MultiLogsView.d.ts +2 -0
- package/dist/tui/views/MultiLogsView.js +165 -0
- package/dist/tui/views/SecretEdit.js +18 -7
- package/dist/tui/views/SecretsView.js +55 -39
- package/dist/ui/prompt.d.ts +52 -0
- package/dist/ui/prompt.js +169 -0
- package/package.json +33 -5
- 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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execSafe } from '../../core/exec.js';
|
|
2
|
+
function ghRunToState(run) {
|
|
3
|
+
if (run.status === 'in_progress' || run.status === 'queued' || run.status === 'waiting')
|
|
4
|
+
return 'warn';
|
|
5
|
+
switch (run.conclusion) {
|
|
6
|
+
case 'success': return 'ok';
|
|
7
|
+
case 'failure':
|
|
8
|
+
case 'timed_out':
|
|
9
|
+
case 'startup_failure':
|
|
10
|
+
return 'error';
|
|
11
|
+
case 'cancelled':
|
|
12
|
+
case 'skipped':
|
|
13
|
+
case 'stale':
|
|
14
|
+
case 'neutral':
|
|
15
|
+
return 'warn';
|
|
16
|
+
default: return 'unknown';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const ciStatusProvider = {
|
|
20
|
+
kind: 'ci-status',
|
|
21
|
+
ttlMs: 60_000,
|
|
22
|
+
strategy: 'event',
|
|
23
|
+
async collect(repoPath, repoName) {
|
|
24
|
+
const collectedAt = new Date().toISOString();
|
|
25
|
+
const result = execSafe('gh', [
|
|
26
|
+
'run', 'list',
|
|
27
|
+
'--limit', '1',
|
|
28
|
+
'--json', 'status,conclusion,name,headBranch,event,createdAt,url',
|
|
29
|
+
], { cwd: repoPath, timeout: 8_000 });
|
|
30
|
+
if (!result.ok) {
|
|
31
|
+
return {
|
|
32
|
+
repo: repoName,
|
|
33
|
+
kind: 'ci-status',
|
|
34
|
+
state: 'unknown',
|
|
35
|
+
value: null,
|
|
36
|
+
detail: result.stderr || 'gh run list failed',
|
|
37
|
+
collectedAt,
|
|
38
|
+
ttlMs: this.ttlMs,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
let runs = [];
|
|
42
|
+
try {
|
|
43
|
+
runs = JSON.parse(result.stdout);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return {
|
|
47
|
+
repo: repoName,
|
|
48
|
+
kind: 'ci-status',
|
|
49
|
+
state: 'unknown',
|
|
50
|
+
value: null,
|
|
51
|
+
detail: 'gh output not JSON',
|
|
52
|
+
collectedAt,
|
|
53
|
+
ttlMs: this.ttlMs,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (runs.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
repo: repoName,
|
|
59
|
+
kind: 'ci-status',
|
|
60
|
+
state: 'unknown',
|
|
61
|
+
value: null,
|
|
62
|
+
detail: 'no runs yet',
|
|
63
|
+
collectedAt,
|
|
64
|
+
ttlMs: this.ttlMs,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const latest = runs[0];
|
|
68
|
+
const state = ghRunToState(latest);
|
|
69
|
+
return {
|
|
70
|
+
repo: repoName,
|
|
71
|
+
kind: 'ci-status',
|
|
72
|
+
state,
|
|
73
|
+
value: latest.conclusion ?? latest.status,
|
|
74
|
+
detail: `${latest.name} · ${latest.headBranch} · ${latest.conclusion ?? latest.status}`,
|
|
75
|
+
collectedAt,
|
|
76
|
+
ttlMs: this.ttlMs,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { execSafe } from '../../core/exec.js';
|
|
2
|
+
const COMPOSE_LABEL = 'com.docker.compose.project';
|
|
3
|
+
export function createContainerUpProvider(opts = {}) {
|
|
4
|
+
const projectForRepo = opts.projectForRepo ?? ((name) => name);
|
|
5
|
+
return {
|
|
6
|
+
kind: 'container-up',
|
|
7
|
+
ttlMs: 15_000,
|
|
8
|
+
strategy: 'pull',
|
|
9
|
+
async collect(_repoPath, repoName) {
|
|
10
|
+
const project = projectForRepo(repoName);
|
|
11
|
+
const collectedAt = new Date().toISOString();
|
|
12
|
+
const result = execSafe('docker', [
|
|
13
|
+
'ps', '--all',
|
|
14
|
+
'--filter', `label=${COMPOSE_LABEL}=${project}`,
|
|
15
|
+
'--format', '{{.State}}',
|
|
16
|
+
], { timeout: 5_000 });
|
|
17
|
+
if (!result.ok) {
|
|
18
|
+
return {
|
|
19
|
+
repo: repoName,
|
|
20
|
+
kind: 'container-up',
|
|
21
|
+
state: 'unknown',
|
|
22
|
+
value: null,
|
|
23
|
+
detail: result.stderr || 'docker ps failed',
|
|
24
|
+
collectedAt,
|
|
25
|
+
ttlMs: this.ttlMs,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const states = result.stdout.split('\n').map(s => s.trim()).filter(Boolean);
|
|
29
|
+
if (states.length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
repo: repoName,
|
|
32
|
+
kind: 'container-up',
|
|
33
|
+
state: 'warn',
|
|
34
|
+
value: 0,
|
|
35
|
+
detail: 'no containers for project',
|
|
36
|
+
collectedAt,
|
|
37
|
+
ttlMs: this.ttlMs,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const running = states.filter(s => s === 'running').length;
|
|
41
|
+
const total = states.length;
|
|
42
|
+
const allRunning = running === total;
|
|
43
|
+
return {
|
|
44
|
+
repo: repoName,
|
|
45
|
+
kind: 'container-up',
|
|
46
|
+
state: allRunning ? 'ok' : running > 0 ? 'warn' : 'error',
|
|
47
|
+
value: running,
|
|
48
|
+
detail: `${running}/${total} running`,
|
|
49
|
+
collectedAt,
|
|
50
|
+
ttlMs: this.ttlMs,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { execSafe } from '../../core/exec.js';
|
|
3
|
+
function resolveSafe(path) {
|
|
4
|
+
try {
|
|
5
|
+
return realpathSync(path);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export const gitCleanProvider = {
|
|
12
|
+
kind: 'git-clean',
|
|
13
|
+
ttlMs: 5_000,
|
|
14
|
+
strategy: 'pull',
|
|
15
|
+
async collect(repoPath, repoName) {
|
|
16
|
+
const collectedAt = new Date().toISOString();
|
|
17
|
+
const toplevel = execSafe('git', ['-C', repoPath, 'rev-parse', '--show-toplevel'], { timeout: 5_000 });
|
|
18
|
+
const expected = resolveSafe(repoPath);
|
|
19
|
+
const actual = toplevel.ok ? resolveSafe(toplevel.stdout.trim()) : null;
|
|
20
|
+
if (!toplevel.ok || !expected || !actual || expected !== actual) {
|
|
21
|
+
return {
|
|
22
|
+
repo: repoName,
|
|
23
|
+
kind: 'git-clean',
|
|
24
|
+
state: 'unknown',
|
|
25
|
+
value: null,
|
|
26
|
+
detail: !toplevel.ok ? (toplevel.stderr || 'not a git repo') : 'path is not a git repo root',
|
|
27
|
+
collectedAt,
|
|
28
|
+
ttlMs: this.ttlMs,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const status = execSafe('git', ['-C', repoPath, 'status', '--porcelain=1'], { timeout: 5_000 });
|
|
32
|
+
if (!status.ok) {
|
|
33
|
+
return {
|
|
34
|
+
repo: repoName,
|
|
35
|
+
kind: 'git-clean',
|
|
36
|
+
state: 'unknown',
|
|
37
|
+
value: null,
|
|
38
|
+
detail: status.stderr || 'git status failed',
|
|
39
|
+
collectedAt,
|
|
40
|
+
ttlMs: this.ttlMs,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const dirty = status.stdout.trim().length > 0;
|
|
44
|
+
const changeCount = dirty ? status.stdout.split('\n').filter(Boolean).length : 0;
|
|
45
|
+
return {
|
|
46
|
+
repo: repoName,
|
|
47
|
+
kind: 'git-clean',
|
|
48
|
+
state: dirty ? 'warn' : 'ok',
|
|
49
|
+
value: !dirty,
|
|
50
|
+
detail: dirty ? `${changeCount} uncommitted change${changeCount === 1 ? '' : 's'}` : '',
|
|
51
|
+
collectedAt,
|
|
52
|
+
ttlMs: this.ttlMs,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SignalProvider } from '../types.js';
|
|
2
|
+
import { ciStatusProvider } from './ci-status.js';
|
|
3
|
+
import { createContainerUpProvider } from './container-up.js';
|
|
4
|
+
import { gitCleanProvider } from './git-clean.js';
|
|
5
|
+
export { ciStatusProvider, createContainerUpProvider, gitCleanProvider };
|
|
6
|
+
export declare function builtInSignalProviders(): SignalProvider[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ciStatusProvider } from './ci-status.js';
|
|
2
|
+
import { createContainerUpProvider } from './container-up.js';
|
|
3
|
+
import { gitCleanProvider } from './git-clean.js';
|
|
4
|
+
export { ciStatusProvider, createContainerUpProvider, gitCleanProvider };
|
|
5
|
+
export function builtInSignalProviders() {
|
|
6
|
+
return [gitCleanProvider, createContainerUpProvider(), ciStatusProvider];
|
|
7
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Routine, RoutineTask, RunEvent, Signal, SignalKind } from '../core/routines/schema.js';
|
|
2
|
+
export interface ScheduledEntry {
|
|
3
|
+
routineId: string;
|
|
4
|
+
unitName: string;
|
|
5
|
+
nextRunAt: string | null;
|
|
6
|
+
lastRunAt: string | null;
|
|
7
|
+
lastStatus: 'ok' | 'failed' | 'unknown';
|
|
8
|
+
active: boolean;
|
|
9
|
+
persistent: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SchedulerAdapter {
|
|
12
|
+
readonly id: 'systemd-timer';
|
|
13
|
+
readonly available: () => boolean;
|
|
14
|
+
upsert(routine: Routine): Promise<void>;
|
|
15
|
+
remove(routineId: string): Promise<void>;
|
|
16
|
+
list(): Promise<ScheduledEntry[]>;
|
|
17
|
+
get(routineId: string): Promise<ScheduledEntry | null>;
|
|
18
|
+
}
|
|
19
|
+
export interface RunContext {
|
|
20
|
+
repo: string | null;
|
|
21
|
+
repoPath: string | null;
|
|
22
|
+
runId: string;
|
|
23
|
+
routineId: string;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
logsDir: string;
|
|
26
|
+
env: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
export interface RunnerAdapter {
|
|
29
|
+
readonly id: RoutineTask['kind'];
|
|
30
|
+
supports(task: RoutineTask): boolean;
|
|
31
|
+
run(task: RoutineTask, ctx: RunContext, signal: AbortSignal): AsyncIterable<RunEvent>;
|
|
32
|
+
}
|
|
33
|
+
export interface SignalProvider {
|
|
34
|
+
readonly kind: SignalKind;
|
|
35
|
+
readonly ttlMs: number;
|
|
36
|
+
readonly strategy: 'push' | 'pull' | 'event';
|
|
37
|
+
collect(repoPath: string, repoName: string): Promise<Signal>;
|
|
38
|
+
watch?(repoPath: string, repoName: string, emit: (s: Signal) => void): () => void;
|
|
39
|
+
}
|
|
40
|
+
export interface NotifierAdapter {
|
|
41
|
+
readonly id: 'stdout' | 'webhook' | 'slack' | 'email';
|
|
42
|
+
notify(subject: string, body: string, meta: {
|
|
43
|
+
routineId: string;
|
|
44
|
+
runId: string;
|
|
45
|
+
status: string;
|
|
46
|
+
}): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export interface StackDetector {
|
|
49
|
+
readonly id: 'node' | 'python' | 'rust' | 'docker' | 'generic';
|
|
50
|
+
detect(repoPath: string): boolean;
|
|
51
|
+
priority: number;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
1
4
|
import { statusCommand } from './commands/status.js';
|
|
2
5
|
import { listCommand } from './commands/list.js';
|
|
3
6
|
import { startCommand } from './commands/start.js';
|
|
4
7
|
import { stopCommand } from './commands/stop.js';
|
|
5
8
|
import { restartCommand } from './commands/restart.js';
|
|
6
9
|
import { logsCommand } from './commands/logs.js';
|
|
10
|
+
import { egressCommand } from './commands/egress.js';
|
|
7
11
|
import { healthCommand } from './commands/health.js';
|
|
8
12
|
import { addCommand } from './commands/add.js';
|
|
9
13
|
import { removeCommand } from './commands/remove.js';
|
|
@@ -15,9 +19,17 @@ import { initCommand } from './commands/init.js';
|
|
|
15
19
|
import { depsCommand } from './commands/deps.js';
|
|
16
20
|
import { watchdogCommand } from './commands/watchdog.js';
|
|
17
21
|
import { installMcpCommand } from './commands/install-mcp.js';
|
|
22
|
+
import { patchSystemdCommand } from './commands/patch-systemd.js';
|
|
23
|
+
import { freezeCommand, unfreezeCommand } from './commands/freeze.js';
|
|
24
|
+
import { bootStartCommand } from './commands/boot-start.js';
|
|
25
|
+
import { rollbackCommand } from './commands/rollback.js';
|
|
26
|
+
import { routineRunCommand } from './commands/routine-run.js';
|
|
27
|
+
import { routinesCommand } from './commands/routines.js';
|
|
18
28
|
import { startMcpServer } from './mcp/server.js';
|
|
19
29
|
import { error } from './ui/output.js';
|
|
20
|
-
const
|
|
30
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
32
|
+
const VERSION = pkg.version;
|
|
21
33
|
const HELP = `fleet v${VERSION} - Docker production management CLI
|
|
22
34
|
|
|
23
35
|
Usage: fleet <command> [options]
|
|
@@ -65,10 +77,18 @@ Commands:
|
|
|
65
77
|
git pr list <app> List open PRs
|
|
66
78
|
git release <app> Create develop->main PR
|
|
67
79
|
tui, dashboard Interactive terminal dashboard
|
|
80
|
+
routines Fleet-wide routines TUI (signals grid + routine history)
|
|
81
|
+
routine-run --id <id> [--target <repo>] [--trigger scheduled]
|
|
82
|
+
Headless entrypoint for systemd-timer units. JSON mode: --json.
|
|
68
83
|
init Auto-discover all existing apps
|
|
69
84
|
watchdog Health check all services, alert on failure
|
|
70
85
|
install-mcp Install fleet as Claude Code MCP server
|
|
71
86
|
mcp Start as MCP server
|
|
87
|
+
patch-systemd Add StartLimitBurst/StartLimitIntervalSec to all service files
|
|
88
|
+
boot-start <app> Start app respecting boot-order dependencies
|
|
89
|
+
freeze <app> Freeze a crash-looping service (stop + disable)
|
|
90
|
+
rollback <app> Roll back app to previous image
|
|
91
|
+
unfreeze <app> Unfreeze and restart a frozen service
|
|
72
92
|
|
|
73
93
|
Global flags:
|
|
74
94
|
--json Output as JSON
|
|
@@ -85,10 +105,23 @@ export async function run(argv) {
|
|
|
85
105
|
process.stdout.write(VERSION + '\n');
|
|
86
106
|
return;
|
|
87
107
|
}
|
|
88
|
-
if (
|
|
108
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
89
109
|
process.stdout.write(HELP);
|
|
90
110
|
return;
|
|
91
111
|
}
|
|
112
|
+
if (!command) {
|
|
113
|
+
const { launchTui } = await import('./tui/app.js');
|
|
114
|
+
return launchTui();
|
|
115
|
+
}
|
|
116
|
+
// Commands that require root privileges
|
|
117
|
+
const ROOT_COMMANDS = new Set([
|
|
118
|
+
'start', 'stop', 'restart', 'deploy', 'freeze', 'unfreeze',
|
|
119
|
+
'nginx', 'secrets', 'patch-systemd', 'init', 'watchdog',
|
|
120
|
+
]);
|
|
121
|
+
if (ROOT_COMMANDS.has(command) && process.getuid && process.getuid() !== 0) {
|
|
122
|
+
error(`'fleet ${command}' requires root privileges. Run with sudo.`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
92
125
|
switch (command) {
|
|
93
126
|
case 'status': return statusCommand(rest);
|
|
94
127
|
case 'list': return listCommand(rest);
|
|
@@ -96,6 +129,7 @@ export async function run(argv) {
|
|
|
96
129
|
case 'stop': return stopCommand(rest);
|
|
97
130
|
case 'restart': return restartCommand(rest);
|
|
98
131
|
case 'logs': return logsCommand(rest);
|
|
132
|
+
case 'egress': return egressCommand(rest);
|
|
99
133
|
case 'health': return healthCommand(rest);
|
|
100
134
|
case 'deps': return depsCommand(rest);
|
|
101
135
|
case 'add': return addCommand(rest);
|
|
@@ -107,12 +141,19 @@ export async function run(argv) {
|
|
|
107
141
|
case 'init': return initCommand(rest);
|
|
108
142
|
case 'watchdog': return watchdogCommand(rest);
|
|
109
143
|
case 'install-mcp': return installMcpCommand(rest);
|
|
144
|
+
case 'patch-systemd': return patchSystemdCommand(rest);
|
|
145
|
+
case 'boot-start': return bootStartCommand(rest);
|
|
146
|
+
case 'freeze': return freezeCommand(rest);
|
|
147
|
+
case 'rollback': return rollbackCommand(rest);
|
|
148
|
+
case 'unfreeze': return unfreezeCommand(rest);
|
|
110
149
|
case 'mcp': return startMcpServer();
|
|
111
150
|
case 'tui':
|
|
112
151
|
case 'dashboard': {
|
|
113
152
|
const { launchTui } = await import('./tui/app.js');
|
|
114
153
|
return launchTui();
|
|
115
154
|
}
|
|
155
|
+
case 'routines': return routinesCommand(rest);
|
|
156
|
+
case 'routine-run': return routineRunCommand(rest);
|
|
116
157
|
default:
|
|
117
158
|
error(`Unknown command: ${command}`);
|
|
118
159
|
process.stdout.write(HELP);
|
package/dist/commands/add.js
CHANGED
|
@@ -85,11 +85,5 @@ function findComposePath(dir) {
|
|
|
85
85
|
if (existsSync(`${dir}/server/docker-compose.yaml`)) {
|
|
86
86
|
return { path: `${dir}/server`, file: null };
|
|
87
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
88
|
return { path: '', file: null };
|
|
95
89
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function bootStartCommand(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { load, findApp } from '../core/registry.js';
|
|
2
|
+
import { refresh } from '../core/boot-refresh.js';
|
|
3
|
+
import { composeUp } from '../core/docker.js';
|
|
4
|
+
function log(msg) {
|
|
5
|
+
process.stdout.write(`[boot-start] ${msg}\n`);
|
|
6
|
+
}
|
|
7
|
+
function logErr(msg) {
|
|
8
|
+
process.stderr.write(`[boot-start] ${msg}\n`);
|
|
9
|
+
}
|
|
10
|
+
export async function bootStartCommand(args) {
|
|
11
|
+
const appName = args[0];
|
|
12
|
+
if (!appName) {
|
|
13
|
+
logErr('Usage: fleet boot-start <app>');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const reg = load();
|
|
17
|
+
const app = findApp(reg, appName);
|
|
18
|
+
if (!app) {
|
|
19
|
+
logErr(`app not found: ${appName}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
// Refresh is best-effort. Any error — sync or async — is caught here and logged,
|
|
23
|
+
// then compose up ALWAYS runs. This is the fail-safe contract for boot.
|
|
24
|
+
try {
|
|
25
|
+
const result = await refresh(app);
|
|
26
|
+
switch (result.kind) {
|
|
27
|
+
case 'refreshed':
|
|
28
|
+
log(`refreshed ${app.name} head=${result.head} built=${result.built}`);
|
|
29
|
+
break;
|
|
30
|
+
case 'no-change':
|
|
31
|
+
log(`no-change ${app.name} head=${result.head}`);
|
|
32
|
+
break;
|
|
33
|
+
case 'skipped':
|
|
34
|
+
log(`skipped ${app.name} reason=${result.reason}`);
|
|
35
|
+
break;
|
|
36
|
+
case 'failed-safe':
|
|
37
|
+
log(`failed-safe ${app.name} step=${result.step} detail=${result.detail}`);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
log(`failed-safe ${app.name} step=outer-catch detail=${err instanceof Error ? err.message : String(err)}`);
|
|
43
|
+
}
|
|
44
|
+
// compose up — the only step whose exit code matters
|
|
45
|
+
const ok = composeUp(app.composePath, app.composeFile);
|
|
46
|
+
if (!ok) {
|
|
47
|
+
logErr(`compose up failed for ${app.name}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
log(`up ${app.name}`);
|
|
51
|
+
}
|
package/dist/commands/deploy.js
CHANGED
|
@@ -6,6 +6,9 @@ import { startService, restartService, getServiceStatus } from '../core/systemd.
|
|
|
6
6
|
import { FleetError } from '../core/errors.js';
|
|
7
7
|
import { success, error, info, warn, heading } from '../ui/output.js';
|
|
8
8
|
import { addCommand } from './add.js';
|
|
9
|
+
import { execGit } from '../core/exec.js';
|
|
10
|
+
import { getProjectRoot } from '../core/git.js';
|
|
11
|
+
import { recordBuiltCommit } from '../core/boot-refresh.js';
|
|
9
12
|
export async function deployCommand(args) {
|
|
10
13
|
const dryRun = args.includes('--dry-run');
|
|
11
14
|
const yes = args.includes('-y') || args.includes('--yes');
|
|
@@ -40,6 +43,16 @@ export async function deployCommand(args) {
|
|
|
40
43
|
process.exit(1);
|
|
41
44
|
}
|
|
42
45
|
success('Build complete');
|
|
46
|
+
try {
|
|
47
|
+
const root = getProjectRoot(app.composePath);
|
|
48
|
+
const head = execGit(['rev-parse', 'HEAD'], { cwd: root, timeout: 10_000 });
|
|
49
|
+
if (head.ok && head.stdout.trim()) {
|
|
50
|
+
recordBuiltCommit(app.name, head.stdout.trim());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Non-fatal: deploy already succeeded
|
|
55
|
+
}
|
|
43
56
|
info(`Starting ${app.name}...`);
|
|
44
57
|
const svc = getServiceStatus(app.serviceName);
|
|
45
58
|
const started = svc.active
|
package/dist/commands/deps.js
CHANGED
|
@@ -151,9 +151,14 @@ async function depsConfig(args) {
|
|
|
151
151
|
process.stdout.write(JSON.stringify(config, null, 2) + '\n');
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
|
+
const ALLOWED_KEYS = new Set(['scanIntervalHours', 'concurrency']);
|
|
154
155
|
if (args[0] === 'set' && args.length >= 3) {
|
|
155
156
|
const key = args[1];
|
|
156
157
|
const value = args[2];
|
|
158
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
159
|
+
error(`Cannot set key: ${key}. Allowed: ${[...ALLOWED_KEYS].join(', ')}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
157
162
|
const parsed = value === 'true' ? true : value === 'false' ? false : isNaN(Number(value)) ? value : Number(value);
|
|
158
163
|
config[key] = parsed;
|
|
159
164
|
saveConfig(config);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function egressCommand(args: string[]): void;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { load, save, findApp } from '../core/registry.js';
|
|
2
|
+
import { snapshotEgress, addEgressAllow } from '../core/egress.js';
|
|
3
|
+
import { AppNotFoundError } from '../core/errors.js';
|
|
4
|
+
import { c, error, heading, info, success, table, warn } from '../ui/output.js';
|
|
5
|
+
export function egressCommand(args) {
|
|
6
|
+
const sub = args[0];
|
|
7
|
+
switch (sub) {
|
|
8
|
+
case 'observe': return egressObserve(args.slice(1));
|
|
9
|
+
case 'show': return egressShow(args.slice(1));
|
|
10
|
+
case 'allow': return egressAllow(args.slice(1));
|
|
11
|
+
default:
|
|
12
|
+
error('Usage: fleet egress <observe|show|allow> ...');
|
|
13
|
+
error(' observe <app> take a snapshot of current outbound flows');
|
|
14
|
+
error(' show <app> show configured allowlist + observed flows');
|
|
15
|
+
error(' allow <app> <host> add a host to the allowlist');
|
|
16
|
+
error('Note: enforce mode (actual drop) is deferred to Phase E. v1 is observe-only.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function egressObserve(args) {
|
|
21
|
+
const json = args.includes('--json');
|
|
22
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
23
|
+
if (!appName) {
|
|
24
|
+
error('Usage: fleet egress observe <app>');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const reg = load();
|
|
28
|
+
const app = findApp(reg, appName);
|
|
29
|
+
if (!app)
|
|
30
|
+
throw new AppNotFoundError(appName);
|
|
31
|
+
const snap = snapshotEgress(app);
|
|
32
|
+
if (json) {
|
|
33
|
+
process.stdout.write(JSON.stringify(snap, null, 2) + '\n');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
heading(`Egress snapshot: ${app.name}`);
|
|
37
|
+
info(`Taken: ${snap.takenAt}`);
|
|
38
|
+
info(`Distinct remote endpoints: ${snap.uniqueRemotes.length}`);
|
|
39
|
+
if (snap.uniqueRemotes.length === 0) {
|
|
40
|
+
info('No outbound flows visible right now (containers may be idle).');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Dedupe per (container, remote)
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const f of snap.flows) {
|
|
47
|
+
const key = `${f.container}|${f.remote}`;
|
|
48
|
+
if (seen.has(key))
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(key);
|
|
51
|
+
const status = f.allowed
|
|
52
|
+
? `${c.green}allowed${c.reset}`
|
|
53
|
+
: `${c.yellow}not in allowlist${c.reset}`;
|
|
54
|
+
rows.push([f.container, f.remote, status]);
|
|
55
|
+
}
|
|
56
|
+
table(['CONTAINER', 'REMOTE', 'STATUS'], rows);
|
|
57
|
+
process.stdout.write('\n');
|
|
58
|
+
if (snap.violations.length > 0) {
|
|
59
|
+
warn(`${snap.violations.length} non-private destination(s) NOT in allowlist:`);
|
|
60
|
+
for (const v of snap.violations)
|
|
61
|
+
process.stdout.write(` - ${v}\n`);
|
|
62
|
+
info(`Add to allowlist: fleet egress allow ${app.name} <host>`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
success('All non-private destinations are allowed (or allowlist not yet seeded).');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function egressShow(args) {
|
|
69
|
+
const appName = args.find(a => !a.startsWith('-'));
|
|
70
|
+
if (!appName) {
|
|
71
|
+
error('Usage: fleet egress show <app>');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
const reg = load();
|
|
75
|
+
const app = findApp(reg, appName);
|
|
76
|
+
if (!app)
|
|
77
|
+
throw new AppNotFoundError(appName);
|
|
78
|
+
heading(`Egress config: ${app.name}`);
|
|
79
|
+
info(`Mode: ${app.egress?.mode ?? 'observe (default)'}`);
|
|
80
|
+
const allow = app.egress?.allow ?? [];
|
|
81
|
+
if (allow.length === 0) {
|
|
82
|
+
info('Allowlist: (empty — every external destination would be flagged)');
|
|
83
|
+
info(`Seed it: fleet egress observe ${app.name}, then fleet egress allow ${app.name} <host>`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
info(`Allowlist (${allow.length}):`);
|
|
87
|
+
for (const a of allow)
|
|
88
|
+
process.stdout.write(` - ${a}\n`);
|
|
89
|
+
}
|
|
90
|
+
info('Note: v1 is observe/shadow only — no packets are actually dropped.');
|
|
91
|
+
}
|
|
92
|
+
function egressAllow(args) {
|
|
93
|
+
const positional = args.filter(a => !a.startsWith('-'));
|
|
94
|
+
const [appName, host] = positional;
|
|
95
|
+
if (!appName || !host) {
|
|
96
|
+
error('Usage: fleet egress allow <app> <host[:port] | *.host | cidr>');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const reg = load();
|
|
100
|
+
const app = findApp(reg, appName);
|
|
101
|
+
if (!app)
|
|
102
|
+
throw new AppNotFoundError(appName);
|
|
103
|
+
const updated = addEgressAllow(app, host);
|
|
104
|
+
save(reg);
|
|
105
|
+
success(`${appName} allow → ${host} (now ${updated.length} entries)`);
|
|
106
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { load, save, findApp } from '../core/registry.js';
|
|
2
|
+
import { stopService, disableService, enableService, startService } from '../core/systemd.js';
|
|
3
|
+
import { AppNotFoundError } from '../core/errors.js';
|
|
4
|
+
import { success, error } from '../ui/output.js';
|
|
5
|
+
export function freezeApp(appName, reason) {
|
|
6
|
+
const reg = load();
|
|
7
|
+
const app = findApp(reg, appName);
|
|
8
|
+
if (!app)
|
|
9
|
+
throw new AppNotFoundError(appName);
|
|
10
|
+
if (app.frozenAt) {
|
|
11
|
+
throw new Error(`App "${appName}" is already frozen (since ${app.frozenAt})`);
|
|
12
|
+
}
|
|
13
|
+
stopService(app.serviceName);
|
|
14
|
+
disableService(app.serviceName);
|
|
15
|
+
app.frozenAt = new Date().toISOString();
|
|
16
|
+
if (reason)
|
|
17
|
+
app.frozenReason = reason;
|
|
18
|
+
save(reg);
|
|
19
|
+
}
|
|
20
|
+
export function unfreezeApp(appName) {
|
|
21
|
+
const reg = load();
|
|
22
|
+
const app = findApp(reg, appName);
|
|
23
|
+
if (!app)
|
|
24
|
+
throw new AppNotFoundError(appName);
|
|
25
|
+
if (!app.frozenAt) {
|
|
26
|
+
throw new Error(`App "${appName}" is not frozen`);
|
|
27
|
+
}
|
|
28
|
+
delete app.frozenAt;
|
|
29
|
+
delete app.frozenReason;
|
|
30
|
+
save(reg);
|
|
31
|
+
enableService(app.serviceName);
|
|
32
|
+
startService(app.serviceName);
|
|
33
|
+
}
|
|
34
|
+
export function freezeCommand(args) {
|
|
35
|
+
const appName = args[0];
|
|
36
|
+
if (!appName) {
|
|
37
|
+
error('Usage: fleet freeze <app> [reason]');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const reason = args.slice(1).join(' ') || undefined;
|
|
41
|
+
try {
|
|
42
|
+
freezeApp(appName, reason);
|
|
43
|
+
success(`Frozen ${appName}${reason ? `: ${reason}` : ''}`);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
error(err.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function unfreezeCommand(args) {
|
|
51
|
+
const appName = args[0];
|
|
52
|
+
if (!appName) {
|
|
53
|
+
error('Usage: fleet unfreeze <app>');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
unfreezeApp(appName);
|
|
58
|
+
success(`Unfrozen ${appName} — service enabled and started`);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
error(err.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/commands/logs.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function logsCommand(args: string[]): void
|
|
1
|
+
export declare function logsCommand(args: string[]): void | Promise<void>;
|