@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,65 @@
|
|
|
1
|
+
function isoDaysAgo(n) {
|
|
2
|
+
return new Date(Date.now() - n * 86_400_000).toISOString();
|
|
3
|
+
}
|
|
4
|
+
export function costRollup(db) {
|
|
5
|
+
const windows = {
|
|
6
|
+
day: isoDaysAgo(1),
|
|
7
|
+
week: isoDaysAgo(7),
|
|
8
|
+
month: isoDaysAgo(30),
|
|
9
|
+
};
|
|
10
|
+
const stmt = db.prepare(`
|
|
11
|
+
SELECT COALESCE(SUM(c.usd), 0) AS usd, COUNT(DISTINCT r.run_id) AS runs
|
|
12
|
+
FROM routine_runs r
|
|
13
|
+
LEFT JOIN routine_cost c ON c.run_id = r.run_id
|
|
14
|
+
WHERE r.started_at >= ?
|
|
15
|
+
`);
|
|
16
|
+
const dayRow = stmt.get(windows.day);
|
|
17
|
+
const weekRow = stmt.get(windows.week);
|
|
18
|
+
const monthRow = stmt.get(windows.month);
|
|
19
|
+
return {
|
|
20
|
+
usdToday: dayRow.usd,
|
|
21
|
+
usdWeek: weekRow.usd,
|
|
22
|
+
usdMonth: monthRow.usd,
|
|
23
|
+
runsToday: dayRow.runs,
|
|
24
|
+
runsWeek: weekRow.runs,
|
|
25
|
+
runsMonth: monthRow.runs,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function costByRoutine(db, days = 30, limit = 20) {
|
|
29
|
+
const since = isoDaysAgo(days);
|
|
30
|
+
const rows = db.prepare(`
|
|
31
|
+
SELECT r.routine_id AS routineId,
|
|
32
|
+
COUNT(DISTINCT r.run_id) AS runs,
|
|
33
|
+
COALESCE(SUM(c.usd), 0) AS usd,
|
|
34
|
+
COALESCE(SUM(c.input_tokens), 0) AS inputTokens,
|
|
35
|
+
COALESCE(SUM(c.output_tokens), 0) AS outputTokens
|
|
36
|
+
FROM routine_runs r
|
|
37
|
+
LEFT JOIN routine_cost c ON c.run_id = r.run_id
|
|
38
|
+
WHERE r.started_at >= ?
|
|
39
|
+
GROUP BY r.routine_id
|
|
40
|
+
ORDER BY usd DESC
|
|
41
|
+
LIMIT ?
|
|
42
|
+
`).all(since, limit);
|
|
43
|
+
return rows;
|
|
44
|
+
}
|
|
45
|
+
export function dailyCostSeries(db, days = 14) {
|
|
46
|
+
const buckets = [];
|
|
47
|
+
const now = new Date();
|
|
48
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
49
|
+
const day = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i);
|
|
50
|
+
const start = day.toISOString();
|
|
51
|
+
const end = new Date(day.getTime() + 86_400_000).toISOString();
|
|
52
|
+
const row = db.prepare(`
|
|
53
|
+
SELECT COALESCE(SUM(c.usd), 0) AS usd, COUNT(DISTINCT r.run_id) AS runs
|
|
54
|
+
FROM routine_runs r
|
|
55
|
+
LEFT JOIN routine_cost c ON c.run_id = r.run_id
|
|
56
|
+
WHERE r.started_at >= ? AND r.started_at < ?
|
|
57
|
+
`).get(start, end);
|
|
58
|
+
buckets.push({
|
|
59
|
+
date: day.toISOString().slice(0, 10),
|
|
60
|
+
usd: row.usd,
|
|
61
|
+
runs: row.runs,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return buckets;
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
export interface OpenOptions {
|
|
3
|
+
path?: string;
|
|
4
|
+
readonly?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function openDb(opts?: OpenOptions): Database.Database;
|
|
7
|
+
export declare function closeDb(): void;
|
|
8
|
+
export declare function dbPath(): string;
|
|
9
|
+
export declare function currentSchemaVersion(): number;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import Database from 'better-sqlite3';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const DEFAULT_DB_PATH = join(__dirname, '..', '..', '..', 'data', 'fleet.db');
|
|
7
|
+
let _db = null;
|
|
8
|
+
let _currentPath = null;
|
|
9
|
+
const SCHEMA = Object.freeze([
|
|
10
|
+
`CREATE TABLE IF NOT EXISTS schema_version (
|
|
11
|
+
version INTEGER PRIMARY KEY
|
|
12
|
+
)`,
|
|
13
|
+
`CREATE TABLE IF NOT EXISTS routine_runs (
|
|
14
|
+
run_id TEXT PRIMARY KEY,
|
|
15
|
+
routine_id TEXT NOT NULL,
|
|
16
|
+
target TEXT,
|
|
17
|
+
started_at TEXT NOT NULL,
|
|
18
|
+
ended_at TEXT,
|
|
19
|
+
status TEXT NOT NULL,
|
|
20
|
+
exit_code INTEGER,
|
|
21
|
+
duration_ms INTEGER,
|
|
22
|
+
error TEXT,
|
|
23
|
+
runner_kind TEXT NOT NULL,
|
|
24
|
+
scheduler_kind TEXT NOT NULL,
|
|
25
|
+
triggered_by TEXT NOT NULL
|
|
26
|
+
)`,
|
|
27
|
+
`CREATE INDEX IF NOT EXISTS idx_runs_routine_started
|
|
28
|
+
ON routine_runs(routine_id, started_at DESC)`,
|
|
29
|
+
`CREATE INDEX IF NOT EXISTS idx_runs_status
|
|
30
|
+
ON routine_runs(status, started_at DESC)`,
|
|
31
|
+
`CREATE TABLE IF NOT EXISTS routine_run_events (
|
|
32
|
+
run_id TEXT NOT NULL,
|
|
33
|
+
seq INTEGER NOT NULL,
|
|
34
|
+
at TEXT NOT NULL,
|
|
35
|
+
kind TEXT NOT NULL,
|
|
36
|
+
payload TEXT NOT NULL,
|
|
37
|
+
PRIMARY KEY (run_id, seq),
|
|
38
|
+
FOREIGN KEY (run_id) REFERENCES routine_runs(run_id) ON DELETE CASCADE
|
|
39
|
+
)`,
|
|
40
|
+
`CREATE TABLE IF NOT EXISTS routine_cost (
|
|
41
|
+
run_id TEXT PRIMARY KEY,
|
|
42
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
cache_create_tokens INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
usd REAL NOT NULL DEFAULT 0,
|
|
47
|
+
FOREIGN KEY (run_id) REFERENCES routine_runs(run_id) ON DELETE CASCADE
|
|
48
|
+
)`,
|
|
49
|
+
`CREATE TABLE IF NOT EXISTS signal_cache (
|
|
50
|
+
repo TEXT NOT NULL,
|
|
51
|
+
kind TEXT NOT NULL,
|
|
52
|
+
state TEXT NOT NULL,
|
|
53
|
+
value TEXT,
|
|
54
|
+
detail TEXT NOT NULL DEFAULT '',
|
|
55
|
+
collected_at TEXT NOT NULL,
|
|
56
|
+
ttl_ms INTEGER NOT NULL,
|
|
57
|
+
PRIMARY KEY (repo, kind)
|
|
58
|
+
)`,
|
|
59
|
+
`CREATE TABLE IF NOT EXISTS signal_history (
|
|
60
|
+
repo TEXT NOT NULL,
|
|
61
|
+
kind TEXT NOT NULL,
|
|
62
|
+
state TEXT NOT NULL,
|
|
63
|
+
value TEXT,
|
|
64
|
+
detail TEXT NOT NULL DEFAULT '',
|
|
65
|
+
collected_at TEXT NOT NULL
|
|
66
|
+
)`,
|
|
67
|
+
`CREATE INDEX IF NOT EXISTS idx_signal_history
|
|
68
|
+
ON signal_history(repo, kind, collected_at DESC)`,
|
|
69
|
+
]);
|
|
70
|
+
const CURRENT_VERSION = 1;
|
|
71
|
+
function ensureDirFor(path) {
|
|
72
|
+
const dir = dirname(path);
|
|
73
|
+
if (!existsSync(dir))
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
function runMigrations(db) {
|
|
77
|
+
db.pragma('journal_mode = WAL');
|
|
78
|
+
db.pragma('foreign_keys = ON');
|
|
79
|
+
db.pragma('synchronous = NORMAL');
|
|
80
|
+
db.exec('BEGIN');
|
|
81
|
+
try {
|
|
82
|
+
for (const stmt of SCHEMA)
|
|
83
|
+
db.exec(stmt);
|
|
84
|
+
const row = db.prepare('SELECT version FROM schema_version LIMIT 1').get();
|
|
85
|
+
if (!row) {
|
|
86
|
+
db.prepare('INSERT INTO schema_version(version) VALUES (?)').run(CURRENT_VERSION);
|
|
87
|
+
}
|
|
88
|
+
else if (row.version !== CURRENT_VERSION) {
|
|
89
|
+
db.prepare('UPDATE schema_version SET version = ?').run(CURRENT_VERSION);
|
|
90
|
+
}
|
|
91
|
+
db.exec('COMMIT');
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
db.exec('ROLLBACK');
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function openDb(opts = {}) {
|
|
99
|
+
const path = opts.path ?? DEFAULT_DB_PATH;
|
|
100
|
+
if (_db && _currentPath === path)
|
|
101
|
+
return _db;
|
|
102
|
+
if (_db) {
|
|
103
|
+
_db.close();
|
|
104
|
+
_db = null;
|
|
105
|
+
}
|
|
106
|
+
ensureDirFor(path);
|
|
107
|
+
const db = new Database(path, { readonly: opts.readonly ?? false });
|
|
108
|
+
if (!opts.readonly)
|
|
109
|
+
runMigrations(db);
|
|
110
|
+
_db = db;
|
|
111
|
+
_currentPath = path;
|
|
112
|
+
return db;
|
|
113
|
+
}
|
|
114
|
+
export function closeDb() {
|
|
115
|
+
if (_db) {
|
|
116
|
+
_db.close();
|
|
117
|
+
_db = null;
|
|
118
|
+
_currentPath = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function dbPath() {
|
|
122
|
+
return _currentPath ?? DEFAULT_DB_PATH;
|
|
123
|
+
}
|
|
124
|
+
export function currentSchemaVersion() {
|
|
125
|
+
return CURRENT_VERSION;
|
|
126
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function builtInDefaultRoutines() {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
id: 'nightly-audit',
|
|
5
|
+
name: 'Nightly fleet audit',
|
|
6
|
+
description: 'Runs `/auto-audit:tick` against each registered repo, capturing findings and opening PRs for high-severity fixes.',
|
|
7
|
+
schedule: { kind: 'calendar', onCalendar: '*-*-* 02:00:00', randomizedDelaySec: 600, persistent: true },
|
|
8
|
+
enabled: false,
|
|
9
|
+
targets: [],
|
|
10
|
+
perTarget: true,
|
|
11
|
+
task: {
|
|
12
|
+
kind: 'claude-cli',
|
|
13
|
+
prompt: 'Run /auto-audit:tick against this repo. Report any HIGH-severity findings as bullet points; be terse.',
|
|
14
|
+
outputFormat: 'json',
|
|
15
|
+
tokenCap: 150_000,
|
|
16
|
+
wallClockMs: 20 * 60 * 1000,
|
|
17
|
+
maxUsd: 2,
|
|
18
|
+
},
|
|
19
|
+
notify: [{ kind: 'stdout', on: 'failure', config: {} }],
|
|
20
|
+
tags: ['security', 'audit'],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'weekly-dep-drift',
|
|
24
|
+
name: 'Weekly dep drift',
|
|
25
|
+
description: 'Detects outdated dependencies and packages >1 major behind. Produces a consolidated markdown report.',
|
|
26
|
+
schedule: { kind: 'calendar', onCalendar: 'Mon *-*-* 06:00:00', randomizedDelaySec: 300, persistent: true },
|
|
27
|
+
enabled: false,
|
|
28
|
+
targets: [],
|
|
29
|
+
perTarget: true,
|
|
30
|
+
task: {
|
|
31
|
+
kind: 'shell',
|
|
32
|
+
argv: ['npm', 'outdated', '--json'],
|
|
33
|
+
wallClockMs: 5 * 60 * 1000,
|
|
34
|
+
},
|
|
35
|
+
notify: [{ kind: 'stdout', on: 'always', config: {} }],
|
|
36
|
+
tags: ['deps'],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'stale-pr-nag',
|
|
40
|
+
name: 'Stale PR nag',
|
|
41
|
+
description: 'Lists open PRs older than 7 days across the fleet.',
|
|
42
|
+
schedule: { kind: 'calendar', onCalendar: 'Fri *-*-* 16:00:00', randomizedDelaySec: 0, persistent: true },
|
|
43
|
+
enabled: false,
|
|
44
|
+
targets: [],
|
|
45
|
+
perTarget: true,
|
|
46
|
+
task: {
|
|
47
|
+
kind: 'shell',
|
|
48
|
+
argv: ['gh', 'pr', 'list', '--state', 'open', '--json', 'number,title,updatedAt,author,url', '--limit', '50'],
|
|
49
|
+
wallClockMs: 60_000,
|
|
50
|
+
},
|
|
51
|
+
notify: [{ kind: 'stdout', on: 'always', config: {} }],
|
|
52
|
+
tags: ['git', 'nag'],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'deploy-readiness',
|
|
56
|
+
name: 'Deploy readiness',
|
|
57
|
+
description: 'Aggregates fleet signals into a ship/block verdict. Manual trigger only.',
|
|
58
|
+
schedule: { kind: 'manual' },
|
|
59
|
+
enabled: true,
|
|
60
|
+
targets: [],
|
|
61
|
+
perTarget: false,
|
|
62
|
+
task: {
|
|
63
|
+
kind: 'mcp-call',
|
|
64
|
+
tool: 'fleet_status',
|
|
65
|
+
args: { summary: true },
|
|
66
|
+
wallClockMs: 60_000,
|
|
67
|
+
},
|
|
68
|
+
notify: [],
|
|
69
|
+
tags: ['deploy', 'release'],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
import type { NotifierAdapter, RunnerAdapter, SchedulerAdapter } from '../../adapters/types.js';
|
|
3
|
+
import type { Routine, RunEvent, RunStatus } from './schema.js';
|
|
4
|
+
import { RoutineStore } from './store.js';
|
|
5
|
+
export type RunTrigger = 'manual' | 'scheduled' | 'api';
|
|
6
|
+
export interface RunPersistencePayload {
|
|
7
|
+
runId: string;
|
|
8
|
+
routineId: string;
|
|
9
|
+
target: string | null;
|
|
10
|
+
startedAt: string;
|
|
11
|
+
endedAt?: string;
|
|
12
|
+
status: RunStatus;
|
|
13
|
+
exitCode?: number;
|
|
14
|
+
durationMs?: number;
|
|
15
|
+
error?: string;
|
|
16
|
+
runnerKind: Routine['task']['kind'];
|
|
17
|
+
schedulerKind: SchedulerAdapter['id'] | 'none';
|
|
18
|
+
triggeredBy: RunTrigger;
|
|
19
|
+
}
|
|
20
|
+
export interface RecentRun {
|
|
21
|
+
runId: string;
|
|
22
|
+
routineId: string;
|
|
23
|
+
target: string | null;
|
|
24
|
+
startedAt: string;
|
|
25
|
+
endedAt: string | null;
|
|
26
|
+
status: RunStatus;
|
|
27
|
+
exitCode: number | null;
|
|
28
|
+
durationMs: number | null;
|
|
29
|
+
error: string | null;
|
|
30
|
+
usd: number | null;
|
|
31
|
+
inputTokens: number | null;
|
|
32
|
+
outputTokens: number | null;
|
|
33
|
+
}
|
|
34
|
+
export interface EngineOptions {
|
|
35
|
+
store: RoutineStore;
|
|
36
|
+
db: Database.Database;
|
|
37
|
+
runners: RunnerAdapter[];
|
|
38
|
+
scheduler?: SchedulerAdapter | null;
|
|
39
|
+
notifiers?: NotifierAdapter[];
|
|
40
|
+
logsDir?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare class RoutineEngine {
|
|
43
|
+
private readonly opts;
|
|
44
|
+
private readonly runners;
|
|
45
|
+
constructor(opts: EngineOptions);
|
|
46
|
+
get store(): RoutineStore;
|
|
47
|
+
get db(): Database.Database;
|
|
48
|
+
runOnce(routineId: string, target?: {
|
|
49
|
+
repo: string | null;
|
|
50
|
+
repoPath: string | null;
|
|
51
|
+
}, trigger?: RunTrigger, signal?: AbortSignal): AsyncIterable<RunEvent>;
|
|
52
|
+
recentRuns(routineId: string, limit?: number): RecentRun[];
|
|
53
|
+
costSinceDays(routineId: string, days?: number): {
|
|
54
|
+
usd: number;
|
|
55
|
+
runs: number;
|
|
56
|
+
};
|
|
57
|
+
register(routine: Routine): Promise<Routine>;
|
|
58
|
+
unregister(routineId: string): Promise<boolean>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
function initRunRow(db, p) {
|
|
3
|
+
db.prepare(`
|
|
4
|
+
INSERT INTO routine_runs (run_id, routine_id, target, started_at, status, runner_kind, scheduler_kind, triggered_by)
|
|
5
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
6
|
+
`).run(p.runId, p.routineId, p.target, p.startedAt, p.status, p.runnerKind, p.schedulerKind, p.triggeredBy);
|
|
7
|
+
}
|
|
8
|
+
function finishRunRow(db, runId, update) {
|
|
9
|
+
db.prepare(`
|
|
10
|
+
UPDATE routine_runs
|
|
11
|
+
SET ended_at = ?, status = ?, exit_code = ?, duration_ms = ?, error = ?
|
|
12
|
+
WHERE run_id = ?
|
|
13
|
+
`).run(update.endedAt, update.status, update.exitCode, update.durationMs, update.error ?? null, runId);
|
|
14
|
+
}
|
|
15
|
+
function appendEvent(db, runId, seq, event) {
|
|
16
|
+
const at = 'at' in event ? event.at : new Date().toISOString();
|
|
17
|
+
db.prepare(`
|
|
18
|
+
INSERT INTO routine_run_events (run_id, seq, at, kind, payload)
|
|
19
|
+
VALUES (?, ?, ?, ?, ?)
|
|
20
|
+
`).run(runId, seq, at, event.kind, JSON.stringify(event));
|
|
21
|
+
}
|
|
22
|
+
function upsertCost(db, runId, c) {
|
|
23
|
+
db.prepare(`
|
|
24
|
+
INSERT INTO routine_cost (run_id, input_tokens, output_tokens, cache_create_tokens, cache_read_tokens, usd)
|
|
25
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
26
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
27
|
+
input_tokens = excluded.input_tokens,
|
|
28
|
+
output_tokens = excluded.output_tokens,
|
|
29
|
+
cache_create_tokens = excluded.cache_create_tokens,
|
|
30
|
+
cache_read_tokens = excluded.cache_read_tokens,
|
|
31
|
+
usd = excluded.usd
|
|
32
|
+
`).run(runId, c.inputTokens, c.outputTokens, c.cacheCreateTokens, c.cacheReadTokens, c.usd);
|
|
33
|
+
}
|
|
34
|
+
export class RoutineEngine {
|
|
35
|
+
opts;
|
|
36
|
+
runners;
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
this.opts = opts;
|
|
39
|
+
this.runners = new Map(opts.runners.map(r => [r.id, r]));
|
|
40
|
+
}
|
|
41
|
+
get store() {
|
|
42
|
+
return this.opts.store;
|
|
43
|
+
}
|
|
44
|
+
get db() {
|
|
45
|
+
return this.opts.db;
|
|
46
|
+
}
|
|
47
|
+
async *runOnce(routineId, target = { repo: null, repoPath: null }, trigger = 'manual', signal = new AbortController().signal) {
|
|
48
|
+
const routine = this.opts.store.get(routineId);
|
|
49
|
+
if (!routine)
|
|
50
|
+
throw new Error(`routine not found: ${routineId}`);
|
|
51
|
+
const runner = this.runners.get(routine.task.kind);
|
|
52
|
+
if (!runner)
|
|
53
|
+
throw new Error(`no runner registered for task kind: ${routine.task.kind}`);
|
|
54
|
+
const runId = randomUUID();
|
|
55
|
+
const startedAt = new Date().toISOString();
|
|
56
|
+
initRunRow(this.opts.db, {
|
|
57
|
+
runId,
|
|
58
|
+
routineId,
|
|
59
|
+
target: target.repo,
|
|
60
|
+
startedAt,
|
|
61
|
+
status: 'running',
|
|
62
|
+
runnerKind: routine.task.kind,
|
|
63
|
+
schedulerKind: this.opts.scheduler?.id ?? 'none',
|
|
64
|
+
triggeredBy: trigger,
|
|
65
|
+
});
|
|
66
|
+
const ctx = {
|
|
67
|
+
repo: target.repo,
|
|
68
|
+
repoPath: target.repoPath,
|
|
69
|
+
runId,
|
|
70
|
+
routineId,
|
|
71
|
+
startedAt,
|
|
72
|
+
logsDir: this.opts.logsDir ?? '/var/log/fleet',
|
|
73
|
+
env: { FLEET_ROUTINE_ID: routineId, FLEET_RUN_ID: runId },
|
|
74
|
+
};
|
|
75
|
+
let seq = 0;
|
|
76
|
+
let lastExitCode = -1;
|
|
77
|
+
let lastDurationMs = 0;
|
|
78
|
+
let lastStatus = 'failed';
|
|
79
|
+
let lastError;
|
|
80
|
+
try {
|
|
81
|
+
for await (const event of runner.run(routine.task, ctx, signal)) {
|
|
82
|
+
appendEvent(this.opts.db, runId, seq++, event);
|
|
83
|
+
if (event.kind === 'cost')
|
|
84
|
+
upsertCost(this.opts.db, runId, event);
|
|
85
|
+
if (event.kind === 'end') {
|
|
86
|
+
lastExitCode = event.exitCode;
|
|
87
|
+
lastDurationMs = event.durationMs;
|
|
88
|
+
lastStatus = event.status;
|
|
89
|
+
lastError = event.error;
|
|
90
|
+
}
|
|
91
|
+
yield event;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
lastError = err.message;
|
|
96
|
+
appendEvent(this.opts.db, runId, seq++, {
|
|
97
|
+
kind: 'end',
|
|
98
|
+
status: 'failed',
|
|
99
|
+
exitCode: -1,
|
|
100
|
+
durationMs: 0,
|
|
101
|
+
at: new Date().toISOString(),
|
|
102
|
+
error: lastError,
|
|
103
|
+
});
|
|
104
|
+
lastStatus = 'failed';
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
finishRunRow(this.opts.db, runId, {
|
|
108
|
+
endedAt: new Date().toISOString(),
|
|
109
|
+
status: lastStatus,
|
|
110
|
+
exitCode: lastExitCode,
|
|
111
|
+
durationMs: lastDurationMs,
|
|
112
|
+
error: lastError,
|
|
113
|
+
});
|
|
114
|
+
if (this.opts.notifiers?.length) {
|
|
115
|
+
const shouldNotify = routine.notify.some(n => n.on === 'always' || (n.on === 'failure' && lastStatus !== 'ok') || (n.on === 'success' && lastStatus === 'ok'));
|
|
116
|
+
if (shouldNotify) {
|
|
117
|
+
const subject = `fleet: ${routine.id} ${lastStatus}`;
|
|
118
|
+
const body = `run ${runId} for ${routine.id} ended ${lastStatus} (exit=${lastExitCode}, ${lastDurationMs}ms)`;
|
|
119
|
+
await Promise.allSettled(this.opts.notifiers.map(n => n.notify(subject, body, { routineId, runId, status: lastStatus })));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
recentRuns(routineId, limit = 20) {
|
|
125
|
+
const rows = this.opts.db.prepare(`
|
|
126
|
+
SELECT r.run_id, r.routine_id, r.target, r.started_at, r.ended_at, r.status, r.exit_code, r.duration_ms, r.error,
|
|
127
|
+
c.usd, c.input_tokens, c.output_tokens
|
|
128
|
+
FROM routine_runs r
|
|
129
|
+
LEFT JOIN routine_cost c ON c.run_id = r.run_id
|
|
130
|
+
WHERE r.routine_id = ?
|
|
131
|
+
ORDER BY r.started_at DESC
|
|
132
|
+
LIMIT ?
|
|
133
|
+
`).all(routineId, limit);
|
|
134
|
+
return rows.map(row => ({
|
|
135
|
+
runId: row.run_id,
|
|
136
|
+
routineId: row.routine_id,
|
|
137
|
+
target: row.target,
|
|
138
|
+
startedAt: row.started_at,
|
|
139
|
+
endedAt: row.ended_at,
|
|
140
|
+
status: row.status,
|
|
141
|
+
exitCode: row.exit_code,
|
|
142
|
+
durationMs: row.duration_ms,
|
|
143
|
+
error: row.error,
|
|
144
|
+
usd: row.usd,
|
|
145
|
+
inputTokens: row.input_tokens,
|
|
146
|
+
outputTokens: row.output_tokens,
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
costSinceDays(routineId, days = 30) {
|
|
150
|
+
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
151
|
+
const row = this.opts.db.prepare(`
|
|
152
|
+
SELECT COALESCE(SUM(c.usd), 0) AS usd, COUNT(DISTINCT c.run_id) AS runs
|
|
153
|
+
FROM routine_cost c
|
|
154
|
+
JOIN routine_runs r ON r.run_id = c.run_id
|
|
155
|
+
WHERE r.routine_id = ? AND r.started_at >= ?
|
|
156
|
+
`).get(routineId, since);
|
|
157
|
+
return { usd: row.usd, runs: row.runs };
|
|
158
|
+
}
|
|
159
|
+
async register(routine) {
|
|
160
|
+
const stored = this.opts.store.upsert(routine);
|
|
161
|
+
if (this.opts.scheduler && stored.schedule.kind !== 'manual') {
|
|
162
|
+
await this.opts.scheduler.upsert(stored);
|
|
163
|
+
}
|
|
164
|
+
return stored;
|
|
165
|
+
}
|
|
166
|
+
async unregister(routineId) {
|
|
167
|
+
if (this.opts.scheduler) {
|
|
168
|
+
try {
|
|
169
|
+
await this.opts.scheduler.remove(routineId);
|
|
170
|
+
}
|
|
171
|
+
catch { /* ignore */ }
|
|
172
|
+
}
|
|
173
|
+
return this.opts.store.remove(routineId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
export type IncidentKind = 'routine-failed' | 'routine-timeout' | 'signal-error' | 'signal-warn';
|
|
3
|
+
export interface Incident {
|
|
4
|
+
at: string;
|
|
5
|
+
kind: IncidentKind;
|
|
6
|
+
subject: string;
|
|
7
|
+
detail: string;
|
|
8
|
+
}
|
|
9
|
+
export interface IncidentQueryOptions {
|
|
10
|
+
sinceDays?: number;
|
|
11
|
+
limit?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadIncidents(db: Database.Database, opts?: IncidentQueryOptions): Incident[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function loadIncidents(db, opts = {}) {
|
|
2
|
+
const sinceDays = opts.sinceDays ?? 7;
|
|
3
|
+
const limit = opts.limit ?? 100;
|
|
4
|
+
const since = new Date(Date.now() - sinceDays * 86_400_000).toISOString();
|
|
5
|
+
const routineRows = db.prepare(`
|
|
6
|
+
SELECT started_at AS at, routine_id AS subject, status, error, target
|
|
7
|
+
FROM routine_runs
|
|
8
|
+
WHERE started_at >= ? AND status IN ('failed', 'timeout', 'aborted')
|
|
9
|
+
ORDER BY started_at DESC
|
|
10
|
+
LIMIT ?
|
|
11
|
+
`).all(since, limit);
|
|
12
|
+
const incidents = routineRows.map(r => ({
|
|
13
|
+
at: r.at,
|
|
14
|
+
kind: r.status === 'timeout' ? 'routine-timeout' : 'routine-failed',
|
|
15
|
+
subject: `${r.subject}${r.target ? ` · ${r.target}` : ''}`,
|
|
16
|
+
detail: r.error ?? r.status,
|
|
17
|
+
}));
|
|
18
|
+
const signalRows = db.prepare(`
|
|
19
|
+
SELECT collected_at AS at, repo, kind AS signal_kind, state, detail
|
|
20
|
+
FROM signal_history
|
|
21
|
+
WHERE collected_at >= ? AND state IN ('error', 'warn')
|
|
22
|
+
ORDER BY collected_at DESC
|
|
23
|
+
LIMIT ?
|
|
24
|
+
`).all(since, limit);
|
|
25
|
+
for (const row of signalRows) {
|
|
26
|
+
incidents.push({
|
|
27
|
+
at: row.at,
|
|
28
|
+
kind: row.state === 'error' ? 'signal-error' : 'signal-warn',
|
|
29
|
+
subject: `${row.repo} · ${row.signal_kind}`,
|
|
30
|
+
detail: row.detail,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
incidents.sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime());
|
|
34
|
+
return incidents.slice(0, limit);
|
|
35
|
+
}
|