dev-cockpit 0.1.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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/dev-cockpit.mjs +20 -0
- package/dist/buildCli.d.ts +17 -0
- package/dist/buildCli.d.ts.map +1 -0
- package/dist/buildCli.js +107 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/cockpit/Cockpit.d.ts +33 -0
- package/dist/cockpit/Cockpit.d.ts.map +1 -0
- package/dist/cockpit/Cockpit.js +73 -0
- package/dist/cockpit/Footer.d.ts +22 -0
- package/dist/cockpit/Footer.d.ts.map +1 -0
- package/dist/cockpit/Footer.js +33 -0
- package/dist/cockpit/TabBar.d.ts +3 -0
- package/dist/cockpit/TabBar.d.ts.map +1 -0
- package/dist/cockpit/TabBar.js +12 -0
- package/dist/cockpit/help/content.d.ts +12 -0
- package/dist/cockpit/help/content.d.ts.map +1 -0
- package/dist/cockpit/help/content.js +22 -0
- package/dist/cockpit/help/loader.d.ts +65 -0
- package/dist/cockpit/help/loader.d.ts.map +1 -0
- package/dist/cockpit/help/loader.js +118 -0
- package/dist/cockpit/help/renderer.d.ts +16 -0
- package/dist/cockpit/help/renderer.d.ts.map +1 -0
- package/dist/cockpit/help/renderer.js +35 -0
- package/dist/cockpit/help/types.d.ts +12 -0
- package/dist/cockpit/help/types.d.ts.map +1 -0
- package/dist/cockpit/help/types.js +1 -0
- package/dist/cockpit/hooks/useCockpitStore.d.ts +3 -0
- package/dist/cockpit/hooks/useCockpitStore.d.ts.map +1 -0
- package/dist/cockpit/hooks/useCockpitStore.js +5 -0
- package/dist/cockpit/hooks/useGlobalKeys.d.ts +56 -0
- package/dist/cockpit/hooks/useGlobalKeys.d.ts.map +1 -0
- package/dist/cockpit/hooks/useGlobalKeys.js +173 -0
- package/dist/cockpit/panes/FilterModal.d.ts +3 -0
- package/dist/cockpit/panes/FilterModal.d.ts.map +1 -0
- package/dist/cockpit/panes/FilterModal.js +22 -0
- package/dist/cockpit/panes/Health.d.ts +13 -0
- package/dist/cockpit/panes/Health.d.ts.map +1 -0
- package/dist/cockpit/panes/Health.js +30 -0
- package/dist/cockpit/panes/Help.d.ts +14 -0
- package/dist/cockpit/panes/Help.d.ts.map +1 -0
- package/dist/cockpit/panes/Help.js +81 -0
- package/dist/cockpit/panes/Output.d.ts +14 -0
- package/dist/cockpit/panes/Output.d.ts.map +1 -0
- package/dist/cockpit/panes/Output.js +108 -0
- package/dist/cockpit/panes/Repos.d.ts +3 -0
- package/dist/cockpit/panes/Repos.d.ts.map +1 -0
- package/dist/cockpit/panes/Repos.js +48 -0
- package/dist/cockpit/panes/SearchModal.d.ts +3 -0
- package/dist/cockpit/panes/SearchModal.d.ts.map +1 -0
- package/dist/cockpit/panes/SearchModal.js +31 -0
- package/dist/cockpit/state/store.d.ts +93 -0
- package/dist/cockpit/state/store.d.ts.map +1 -0
- package/dist/cockpit/state/store.js +111 -0
- package/dist/cockpit/tab-state.d.ts +4 -0
- package/dist/cockpit/tab-state.d.ts.map +1 -0
- package/dist/cockpit/tab-state.js +7 -0
- package/dist/commands/dev.d.ts +20 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +158 -0
- package/dist/commands/doctor.d.ts +20 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +66 -0
- package/dist/commands/init-config-wizard.d.ts +84 -0
- package/dist/commands/init-config-wizard.d.ts.map +1 -0
- package/dist/commands/init-config-wizard.js +818 -0
- package/dist/commands/init-config.d.ts +35 -0
- package/dist/commands/init-config.d.ts.map +1 -0
- package/dist/commands/init-config.js +131 -0
- package/dist/commands/mount.d.ts +48 -0
- package/dist/commands/mount.d.ts.map +1 -0
- package/dist/commands/mount.js +150 -0
- package/dist/core/config.d.ts +391 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +152 -0
- package/dist/core/logger.d.ts +6 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +38 -0
- package/dist/core/notifier.d.ts +23 -0
- package/dist/core/notifier.d.ts.map +1 -0
- package/dist/core/notifier.js +100 -0
- package/dist/core/paths.d.ts +15 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +18 -0
- package/dist/core/subprocess.d.ts +20 -0
- package/dist/core/subprocess.d.ts.map +1 -0
- package/dist/core/subprocess.js +82 -0
- package/dist/core/types.d.ts +125 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/docker/highlights.d.ts +48 -0
- package/dist/docker/highlights.d.ts.map +1 -0
- package/dist/docker/highlights.js +79 -0
- package/dist/docker/logs.d.ts +84 -0
- package/dist/docker/logs.d.ts.map +1 -0
- package/dist/docker/logs.js +172 -0
- package/dist/docker/restart.d.ts +26 -0
- package/dist/docker/restart.d.ts.map +1 -0
- package/dist/docker/restart.js +45 -0
- package/dist/docker/stack-trace.d.ts +25 -0
- package/dist/docker/stack-trace.d.ts.map +1 -0
- package/dist/docker/stack-trace.js +44 -0
- package/dist/health/builtin.d.ts +8 -0
- package/dist/health/builtin.d.ts.map +1 -0
- package/dist/health/builtin.js +144 -0
- package/dist/health/context.d.ts +3 -0
- package/dist/health/context.d.ts.map +1 -0
- package/dist/health/context.js +31 -0
- package/dist/health/notify-resolver.d.ts +18 -0
- package/dist/health/notify-resolver.d.ts.map +1 -0
- package/dist/health/notify-resolver.js +28 -0
- package/dist/health/registry.d.ts +20 -0
- package/dist/health/registry.d.ts.map +1 -0
- package/dist/health/registry.js +64 -0
- package/dist/health/remediations.d.ts +6 -0
- package/dist/health/remediations.d.ts.map +1 -0
- package/dist/health/remediations.js +41 -0
- package/dist/health/runner.d.ts +4 -0
- package/dist/health/runner.d.ts.map +1 -0
- package/dist/health/runner.js +22 -0
- package/dist/health/scheduler.d.ts +41 -0
- package/dist/health/scheduler.d.ts.map +1 -0
- package/dist/health/scheduler.js +107 -0
- package/dist/health/types.d.ts +73 -0
- package/dist/health/types.d.ts.map +1 -0
- package/dist/health/types.js +1 -0
- package/dist/health/useHealth.d.ts +40 -0
- package/dist/health/useHealth.d.ts.map +1 -0
- package/dist/health/useHealth.js +122 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/ink.d.ts +3 -0
- package/dist/ink.d.ts.map +1 -0
- package/dist/ink.js +1 -0
- package/dist/lint/reactive.d.ts +38 -0
- package/dist/lint/reactive.d.ts.map +1 -0
- package/dist/lint/reactive.js +131 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +1 -0
- package/dist/runCockpit.d.ts +34 -0
- package/dist/runCockpit.d.ts.map +1 -0
- package/dist/runCockpit.js +75 -0
- package/dist/watchers/manager.d.ts +63 -0
- package/dist/watchers/manager.d.ts.map +1 -0
- package/dist/watchers/manager.js +239 -0
- package/dist/watchers/path-mapper.d.ts +23 -0
- package/dist/watchers/path-mapper.d.ts.map +1 -0
- package/dist/watchers/path-mapper.js +29 -0
- package/dist/watchers/types.d.ts +22 -0
- package/dist/watchers/types.d.ts.map +1 -0
- package/dist/watchers/types.js +9 -0
- package/docs/commands.md +71 -0
- package/docs/config-reference.md +20 -0
- package/docs/getting-started.md +39 -0
- package/docs/health.md +120 -0
- package/docs/index.md +13 -0
- package/docs/init-config.md +46 -0
- package/docs/mount.md +55 -0
- package/docs/notifications.md +39 -0
- package/docs/panes.md +45 -0
- package/docs/watchers.md +27 -0
- package/examples/cockpit.yaml +116 -0
- package/package.json +91 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DockerLogTailer — tails `docker compose logs -f` for configured services,
|
|
3
|
+
* routes lines into the Output pane, surfaces highlight matches into the
|
|
4
|
+
* Recent Errors ring buffer, and emits OS notifications via the shared
|
|
5
|
+
* notifier.
|
|
6
|
+
*
|
|
7
|
+
* Discipline:
|
|
8
|
+
* - Subprocess goes through `core/subprocess.spawnStream` (DRY).
|
|
9
|
+
* - Notifications go through `core/notifier.emitEvent` (DRY).
|
|
10
|
+
* - Notification debounce: identical errors (same file:line OR same text
|
|
11
|
+
* fingerprint) within `DEBOUNCE_MS` produce ONE notification, not N.
|
|
12
|
+
*
|
|
13
|
+
* Line format: `docker compose logs --no-color` emits `<service> | <body>`
|
|
14
|
+
* (variable spaces). The leading service token is parsed; the rest is the
|
|
15
|
+
* line body.
|
|
16
|
+
*/
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import { spawnStream } from '../core/subprocess.js';
|
|
20
|
+
import { emitEvent } from '../core/notifier.js';
|
|
21
|
+
import { compileHighlights } from './highlights.js';
|
|
22
|
+
import { extractFileLine } from './stack-trace.js';
|
|
23
|
+
/** Debounce window for identical errors (default 30s). */
|
|
24
|
+
export const DEBOUNCE_MS = 30_000;
|
|
25
|
+
/** Liveness threshold — a service silent for this long is treated as down. */
|
|
26
|
+
export const SILENT_THRESHOLD_MS = 90_000;
|
|
27
|
+
export class DockerLogTailer {
|
|
28
|
+
opts;
|
|
29
|
+
handle = null;
|
|
30
|
+
compiled;
|
|
31
|
+
spawn;
|
|
32
|
+
now;
|
|
33
|
+
/** fingerprint → timestamp of last notify. Used for debounce. */
|
|
34
|
+
lastNotified = new Map();
|
|
35
|
+
serviceState = new Map();
|
|
36
|
+
liveCheckInterval = null;
|
|
37
|
+
constructor(opts) {
|
|
38
|
+
this.opts = opts;
|
|
39
|
+
this.compiled = compileHighlights(opts.highlightPatterns);
|
|
40
|
+
this.spawn = opts.spawn ?? spawnStream;
|
|
41
|
+
this.now = opts.now ?? Date.now;
|
|
42
|
+
for (const svc of opts.services) {
|
|
43
|
+
this.serviceState.set(svc, { lastSeen: this.now(), notifiedDown: false });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the docker-compose file path. If `opts.composeFile` is set
|
|
48
|
+
* and exists, use it. Otherwise return null and let docker auto-discover.
|
|
49
|
+
*/
|
|
50
|
+
resolveComposeFile() {
|
|
51
|
+
if (this.opts.composeFile) {
|
|
52
|
+
const abs = path.isAbsolute(this.opts.composeFile)
|
|
53
|
+
? this.opts.composeFile
|
|
54
|
+
: path.join(this.opts.workspaceRoot, this.opts.composeFile);
|
|
55
|
+
return fs.existsSync(abs) ? abs : null;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
start() {
|
|
60
|
+
if (this.handle)
|
|
61
|
+
return;
|
|
62
|
+
const composeFile = this.resolveComposeFile();
|
|
63
|
+
const args = [
|
|
64
|
+
'compose',
|
|
65
|
+
...(composeFile ? ['-f', composeFile] : []),
|
|
66
|
+
'logs',
|
|
67
|
+
'-f',
|
|
68
|
+
'--no-color',
|
|
69
|
+
'--tail=0',
|
|
70
|
+
...this.opts.services,
|
|
71
|
+
];
|
|
72
|
+
this.handle = this.spawn('docker', args, {
|
|
73
|
+
cwd: this.opts.workspaceRoot,
|
|
74
|
+
onStdout: (line) => this.handleLine(line),
|
|
75
|
+
onStderr: (line) => this.handleLine(line),
|
|
76
|
+
});
|
|
77
|
+
// Periodic liveness watch — docker doesn't push container-stop events
|
|
78
|
+
// through `logs -f`; we infer from silence.
|
|
79
|
+
this.liveCheckInterval = setInterval(() => this.checkLiveness(), 30_000);
|
|
80
|
+
}
|
|
81
|
+
async stop() {
|
|
82
|
+
if (this.liveCheckInterval) {
|
|
83
|
+
clearInterval(this.liveCheckInterval);
|
|
84
|
+
this.liveCheckInterval = null;
|
|
85
|
+
}
|
|
86
|
+
if (this.handle) {
|
|
87
|
+
const handle = this.handle;
|
|
88
|
+
this.handle = null;
|
|
89
|
+
handle.kill();
|
|
90
|
+
// Await the child's exit so the ChildProcess + stdio sockets are reaped
|
|
91
|
+
// before we resolve. Without this, the Node event loop still tracks
|
|
92
|
+
// those handles, blocking process exit on the consumer side.
|
|
93
|
+
try {
|
|
94
|
+
await handle.exitCode;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// execa is reject:false but swallow defensively just in case.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parse a single docker-compose log line: `<service> | <body>`.
|
|
103
|
+
* Returns null if the line doesn't match (e.g. compose preamble).
|
|
104
|
+
*/
|
|
105
|
+
parseLine(raw) {
|
|
106
|
+
const m = /^([a-zA-Z0-9_.\-]+)\s*\|\s?(.*)$/.exec(raw);
|
|
107
|
+
if (!m || !m[1])
|
|
108
|
+
return null;
|
|
109
|
+
return { service: m[1], body: m[2] ?? '' };
|
|
110
|
+
}
|
|
111
|
+
/** Public for testing — exercise per-line logic without a real subprocess. */
|
|
112
|
+
handleLine(raw) {
|
|
113
|
+
const parsed = this.parseLine(raw);
|
|
114
|
+
if (!parsed)
|
|
115
|
+
return;
|
|
116
|
+
const { service, body } = parsed;
|
|
117
|
+
const tracked = this.serviceState.get(service);
|
|
118
|
+
if (tracked) {
|
|
119
|
+
tracked.lastSeen = this.now();
|
|
120
|
+
if (tracked.notifiedDown)
|
|
121
|
+
tracked.notifiedDown = false;
|
|
122
|
+
}
|
|
123
|
+
const match = this.compiled.match(body);
|
|
124
|
+
const severity = match.severity;
|
|
125
|
+
this.opts.appendOutput({
|
|
126
|
+
ts: this.now(),
|
|
127
|
+
source: `docker:${service}`,
|
|
128
|
+
severity,
|
|
129
|
+
text: body,
|
|
130
|
+
});
|
|
131
|
+
if (!match.matched)
|
|
132
|
+
return;
|
|
133
|
+
// Highlight match → recent-errors + (debounced) notify.
|
|
134
|
+
const fileLine = extractFileLine(body);
|
|
135
|
+
const recentErr = {
|
|
136
|
+
ts: this.now(),
|
|
137
|
+
service,
|
|
138
|
+
severity: severity === 'info' ? 'warn' : severity,
|
|
139
|
+
text: body,
|
|
140
|
+
...(fileLine ? { file: fileLine.file, line: fileLine.line } : {}),
|
|
141
|
+
};
|
|
142
|
+
this.opts.pushRecentError(recentErr);
|
|
143
|
+
// Initiators (new error event) notify; continuations (Stack trace) don't.
|
|
144
|
+
if (severity === 'error' && match.isInitiator) {
|
|
145
|
+
this.maybeNotifyError(service, body, fileLine);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
maybeNotifyError(service, body, fileLine) {
|
|
149
|
+
const fingerprint = fileLine
|
|
150
|
+
? `${service}:${fileLine.file}:${fileLine.line}`
|
|
151
|
+
: `${service}:${body}`;
|
|
152
|
+
const last = this.lastNotified.get(fingerprint);
|
|
153
|
+
const now = this.now();
|
|
154
|
+
if (last !== undefined && now - last < DEBOUNCE_MS)
|
|
155
|
+
return;
|
|
156
|
+
this.lastNotified.set(fingerprint, now);
|
|
157
|
+
const eventName = this.opts.errorEventName ?? 'docker-error';
|
|
158
|
+
emitEvent(eventName, `docker:${service}`, fileLine ? `${body}\n ↳ ${fileLine.file}:${fileLine.line}` : body, this.opts.notifyOpts());
|
|
159
|
+
}
|
|
160
|
+
/** If a tracked service has been silent past the threshold, notify once. */
|
|
161
|
+
checkLiveness() {
|
|
162
|
+
const now = this.now();
|
|
163
|
+
for (const [service, state] of this.serviceState) {
|
|
164
|
+
if (state.notifiedDown)
|
|
165
|
+
continue;
|
|
166
|
+
if (now - state.lastSeen > SILENT_THRESHOLD_MS) {
|
|
167
|
+
state.notifiedDown = true;
|
|
168
|
+
emitEvent('container-down', `docker:${service}`, `Service ${service} has been silent for ${Math.round((now - state.lastSeen) / 1000)}s`, this.opts.notifyOpts());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker service restart helper.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `docker compose restart <service>` and streams stdout/stderr to
|
|
5
|
+
* the Output pane via the supplied appendOutput callback.
|
|
6
|
+
*
|
|
7
|
+
* Used by the Repos pane when the user presses `r` on a docker entry.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnStream } from '../core/subprocess.js';
|
|
10
|
+
import type { OutputLine } from '../cockpit/state/store.js';
|
|
11
|
+
export interface RestartDockerDeps {
|
|
12
|
+
/** Workspace root — passed as `cwd` to `docker compose`. */
|
|
13
|
+
workspaceRoot: string;
|
|
14
|
+
/** Service name (e.g. "web", "db"). */
|
|
15
|
+
service: string;
|
|
16
|
+
/** Pushes a line to the Output pane. */
|
|
17
|
+
appendOutput: (line: OutputLine) => void;
|
|
18
|
+
/** Inject a custom spawner for tests. Defaults to spawnStream. */
|
|
19
|
+
spawn?: typeof spawnStream;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Restart one docker compose service. Resolves with the exit code.
|
|
23
|
+
* Errors do not throw — they're surfaced as warn-level Output lines.
|
|
24
|
+
*/
|
|
25
|
+
export declare function restartDockerService(deps: RestartDockerDeps): Promise<number>;
|
|
26
|
+
//# sourceMappingURL=restart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restart.d.ts","sourceRoot":"","sources":["../../src/docker/restart.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,aAAa,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,YAAY,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACzC,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,WAAW,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCnF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker service restart helper.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `docker compose restart <service>` and streams stdout/stderr to
|
|
5
|
+
* the Output pane via the supplied appendOutput callback.
|
|
6
|
+
*
|
|
7
|
+
* Used by the Repos pane when the user presses `r` on a docker entry.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnStream } from '../core/subprocess.js';
|
|
10
|
+
/**
|
|
11
|
+
* Restart one docker compose service. Resolves with the exit code.
|
|
12
|
+
* Errors do not throw — they're surfaced as warn-level Output lines.
|
|
13
|
+
*/
|
|
14
|
+
export async function restartDockerService(deps) {
|
|
15
|
+
const spawn = deps.spawn ?? spawnStream;
|
|
16
|
+
deps.appendOutput({
|
|
17
|
+
ts: Date.now(),
|
|
18
|
+
source: `docker:${deps.service}`,
|
|
19
|
+
severity: 'info',
|
|
20
|
+
text: `restart ${deps.service} requested`,
|
|
21
|
+
});
|
|
22
|
+
const handle = spawn('docker', ['compose', 'restart', deps.service], {
|
|
23
|
+
cwd: deps.workspaceRoot,
|
|
24
|
+
onStdout: (line) => deps.appendOutput({
|
|
25
|
+
ts: Date.now(),
|
|
26
|
+
source: `docker:${deps.service}`,
|
|
27
|
+
severity: 'info',
|
|
28
|
+
text: line,
|
|
29
|
+
}),
|
|
30
|
+
onStderr: (line) => deps.appendOutput({
|
|
31
|
+
ts: Date.now(),
|
|
32
|
+
source: `docker:${deps.service}`,
|
|
33
|
+
severity: 'warn',
|
|
34
|
+
text: line,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
const code = await handle.exitCode;
|
|
38
|
+
deps.appendOutput({
|
|
39
|
+
ts: Date.now(),
|
|
40
|
+
source: `docker:${deps.service}`,
|
|
41
|
+
severity: code === 0 ? 'info' : 'warn',
|
|
42
|
+
text: `restart ${deps.service} exited with code ${code}`,
|
|
43
|
+
});
|
|
44
|
+
return code;
|
|
45
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract `file:line` from stack-trace-style log lines.
|
|
3
|
+
*
|
|
4
|
+
* Pure. No I/O. Recognizes two common shapes:
|
|
5
|
+
*
|
|
6
|
+
* in /path/to/File.<ext>:42
|
|
7
|
+
* /path/to/File.<ext>(42)
|
|
8
|
+
*
|
|
9
|
+
* The path matcher captures any absolute path ending in a code-file
|
|
10
|
+
* extension (`.<letters/digits>`), so it works across languages. Container
|
|
11
|
+
* paths are returned as-is; container ↔ host mapping is the caller's job.
|
|
12
|
+
*
|
|
13
|
+
* When multiple references appear, the first wins — typically the most
|
|
14
|
+
* specific (the throw site rather than framework internals).
|
|
15
|
+
*/
|
|
16
|
+
export interface FileLine {
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Try to extract the first `file:line` reference from a single log line or
|
|
22
|
+
* a multi-line trace. Returns null if no plausible reference is found.
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractFileLine(text: string): FileLine | null;
|
|
25
|
+
//# sourceMappingURL=stack-trace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stack-trace.d.ts","sourceRoot":"","sources":["../../src/docker/stack-trace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAgBD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAc7D"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract `file:line` from stack-trace-style log lines.
|
|
3
|
+
*
|
|
4
|
+
* Pure. No I/O. Recognizes two common shapes:
|
|
5
|
+
*
|
|
6
|
+
* in /path/to/File.<ext>:42
|
|
7
|
+
* /path/to/File.<ext>(42)
|
|
8
|
+
*
|
|
9
|
+
* The path matcher captures any absolute path ending in a code-file
|
|
10
|
+
* extension (`.<letters/digits>`), so it works across languages. Container
|
|
11
|
+
* paths are returned as-is; container ↔ host mapping is the caller's job.
|
|
12
|
+
*
|
|
13
|
+
* When multiple references appear, the first wins — typically the most
|
|
14
|
+
* specific (the throw site rather than framework internals).
|
|
15
|
+
*/
|
|
16
|
+
// Anchored to capture absolute paths only — avoids false-matching log
|
|
17
|
+
// timestamps, IDs, or arbitrary numbers. Preceded by start-of-string,
|
|
18
|
+
// whitespace, or common opening punctuation (quote / paren / bracket).
|
|
19
|
+
//
|
|
20
|
+
// The extension allow-list is a curated set of common code-file extensions
|
|
21
|
+
// (broad enough to cover most real stacks; tight enough to skip log files,
|
|
22
|
+
// sockets, pid files, and other non-code artifacts).
|
|
23
|
+
const PRE = `(?:^|[\\s'"(\\[])`;
|
|
24
|
+
const EXT = '(?:js|jsx|ts|tsx|mjs|cjs|py|rb|go|rs|java|kt|swift|scala|php|c|h|cpp|hpp|hh|cc|cs|fs|sh|bash|zsh|lua|dart|ex|exs|erl|clj|cljs|sql|css|scss|sass|less|html|htm|vue|svelte|elm|hs|ml|nim|zig|jl|r|pl|pm|tcl|groovy|gradle|kts)';
|
|
25
|
+
const PATH = `(\\/(?:[^\\s:()'"\\[\\]]+\\/)*[^\\s:()'"\\[\\]]+\\.${EXT})`;
|
|
26
|
+
const RE_COLON = new RegExp(`${PRE}${PATH}:(\\d+)`, 'i');
|
|
27
|
+
const RE_PAREN = new RegExp(`${PRE}${PATH}\\((\\d+)\\)`, 'i');
|
|
28
|
+
/**
|
|
29
|
+
* Try to extract the first `file:line` reference from a single log line or
|
|
30
|
+
* a multi-line trace. Returns null if no plausible reference is found.
|
|
31
|
+
*/
|
|
32
|
+
export function extractFileLine(text) {
|
|
33
|
+
if (!text)
|
|
34
|
+
return null;
|
|
35
|
+
const colonMatch = RE_COLON.exec(text);
|
|
36
|
+
if (colonMatch?.[1] && colonMatch[2]) {
|
|
37
|
+
return { file: colonMatch[1], line: Number(colonMatch[2]) };
|
|
38
|
+
}
|
|
39
|
+
const parenMatch = RE_PAREN.exec(text);
|
|
40
|
+
if (parenMatch?.[1] && parenMatch[2]) {
|
|
41
|
+
return { file: parenMatch[1], line: Number(parenMatch[2]) };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HealthCheck, HealthCheckConfig, BuiltinCheckType } from './types.js';
|
|
2
|
+
import type { Severity } from '../core/types.js';
|
|
3
|
+
type BuiltinFactory = (entry: HealthCheckConfig) => HealthCheck['predicate'];
|
|
4
|
+
export declare const BUILTIN_CHECK_FACTORIES: Record<BuiltinCheckType, BuiltinFactory>;
|
|
5
|
+
export declare const BUILTIN_DEFAULT_TRIGGERS: Record<BuiltinCheckType, HealthCheck['triggers']>;
|
|
6
|
+
export declare const BUILTIN_DEFAULT_SEVERITY: Record<BuiltinCheckType, Severity>;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=builtin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builtin.d.ts","sourceRoot":"","sources":["../../src/health/builtin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAiB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAClG,OAAO,KAAK,EAAgB,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE/D,KAAK,cAAc,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,WAAW,CAAC,WAAW,CAAC,CAAC;AA+H7E,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,gBAAgB,EAAE,cAAc,CAM5E,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,gBAAgB,EAAE,WAAW,CAAC,UAAU,CAAC,CAMtF,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,gBAAgB,EAAE,QAAQ,CAMvE,CAAC"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const containerRunning = (entry) => {
|
|
4
|
+
const container = entry.container;
|
|
5
|
+
if (!container) {
|
|
6
|
+
throw new Error(`health[${entry.id}]: type 'container-running' requires 'container'`);
|
|
7
|
+
}
|
|
8
|
+
return async (ctx) => {
|
|
9
|
+
const { stdout, exitCode } = await ctx.exec('docker', [
|
|
10
|
+
'inspect',
|
|
11
|
+
'--format',
|
|
12
|
+
'{{.State.Running}}',
|
|
13
|
+
container,
|
|
14
|
+
]);
|
|
15
|
+
const running = exitCode === 0 && stdout.trim() === 'true';
|
|
16
|
+
return {
|
|
17
|
+
id: entry.id,
|
|
18
|
+
label: entry.label,
|
|
19
|
+
severity: running ? 'ok' : (entry.severity ?? 'error'),
|
|
20
|
+
detail: running
|
|
21
|
+
? `container '${container}' is running`
|
|
22
|
+
: `container '${container}' is not running`,
|
|
23
|
+
remediationKey: entry.remediation.key,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const portOpen = (entry) => {
|
|
28
|
+
const port = entry.port;
|
|
29
|
+
if (typeof port !== 'number') {
|
|
30
|
+
throw new Error(`health[${entry.id}]: type 'port-open' requires numeric 'port'`);
|
|
31
|
+
}
|
|
32
|
+
const host = entry.host ?? '127.0.0.1';
|
|
33
|
+
return async () => {
|
|
34
|
+
const open = await new Promise((resolve) => {
|
|
35
|
+
const sock = new net.Socket();
|
|
36
|
+
const done = (ok) => {
|
|
37
|
+
sock.destroy();
|
|
38
|
+
resolve(ok);
|
|
39
|
+
};
|
|
40
|
+
sock.setTimeout(2000);
|
|
41
|
+
sock.once('connect', () => done(true));
|
|
42
|
+
sock.once('error', () => done(false));
|
|
43
|
+
sock.once('timeout', () => done(false));
|
|
44
|
+
sock.connect(port, host);
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
id: entry.id,
|
|
48
|
+
label: entry.label,
|
|
49
|
+
severity: open ? 'ok' : (entry.severity ?? 'error'),
|
|
50
|
+
detail: open ? `${host}:${port} is reachable` : `${host}:${port} is not reachable`,
|
|
51
|
+
remediationKey: entry.remediation.key,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const httpOk = (entry) => {
|
|
56
|
+
const url = entry.url;
|
|
57
|
+
if (!url) {
|
|
58
|
+
throw new Error(`health[${entry.id}]: type 'http-ok' requires 'url'`);
|
|
59
|
+
}
|
|
60
|
+
const expect = entry.expectStatus ?? 200;
|
|
61
|
+
return async () => {
|
|
62
|
+
let status = 0;
|
|
63
|
+
let detail = '';
|
|
64
|
+
try {
|
|
65
|
+
const ac = new AbortController();
|
|
66
|
+
const t = setTimeout(() => ac.abort(), 5000);
|
|
67
|
+
const res = await fetch(url, { signal: ac.signal });
|
|
68
|
+
clearTimeout(t);
|
|
69
|
+
status = res.status;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
detail = `fetch failed: ${String(err.message ?? err)}`;
|
|
73
|
+
}
|
|
74
|
+
const ok = status === expect;
|
|
75
|
+
return {
|
|
76
|
+
id: entry.id,
|
|
77
|
+
label: entry.label,
|
|
78
|
+
severity: ok ? 'ok' : (entry.severity ?? 'error'),
|
|
79
|
+
detail: ok
|
|
80
|
+
? `${url} returned ${status}`
|
|
81
|
+
: detail || `${url} returned ${status} (expected ${expect})`,
|
|
82
|
+
remediationKey: entry.remediation.key,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
const fileExists = (entry) => {
|
|
87
|
+
const target = entry.path;
|
|
88
|
+
if (!target) {
|
|
89
|
+
throw new Error(`health[${entry.id}]: type 'file-exists' requires 'path'`);
|
|
90
|
+
}
|
|
91
|
+
return async (ctx) => {
|
|
92
|
+
const resolved = path.isAbsolute(target) ? target : path.join(ctx.workspaceRoot, target);
|
|
93
|
+
const exists = ctx.fs.existsSync(resolved);
|
|
94
|
+
return {
|
|
95
|
+
id: entry.id,
|
|
96
|
+
label: entry.label,
|
|
97
|
+
severity: exists ? 'ok' : (entry.severity ?? 'error'),
|
|
98
|
+
detail: exists ? `${resolved} exists` : `${resolved} is missing`,
|
|
99
|
+
remediationKey: entry.remediation.key,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const execZero = (entry) => {
|
|
104
|
+
const command = entry.command;
|
|
105
|
+
if (!command) {
|
|
106
|
+
throw new Error(`health[${entry.id}]: type 'exec-zero' requires 'command'`);
|
|
107
|
+
}
|
|
108
|
+
const args = entry.args ?? [];
|
|
109
|
+
return async (ctx) => {
|
|
110
|
+
const cwd = entry.cwd ?? ctx.workspaceRoot;
|
|
111
|
+
const { exitCode } = await ctx.exec(command, args, cwd);
|
|
112
|
+
const ok = exitCode === 0;
|
|
113
|
+
return {
|
|
114
|
+
id: entry.id,
|
|
115
|
+
label: entry.label,
|
|
116
|
+
severity: ok ? 'ok' : (entry.severity ?? 'error'),
|
|
117
|
+
detail: ok
|
|
118
|
+
? `${command} ${args.join(' ')} exited 0`
|
|
119
|
+
: `${command} ${args.join(' ')} exited ${exitCode}`,
|
|
120
|
+
remediationKey: entry.remediation.key,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
export const BUILTIN_CHECK_FACTORIES = {
|
|
125
|
+
'container-running': containerRunning,
|
|
126
|
+
'port-open': portOpen,
|
|
127
|
+
'http-ok': httpOk,
|
|
128
|
+
'file-exists': fileExists,
|
|
129
|
+
'exec-zero': execZero,
|
|
130
|
+
};
|
|
131
|
+
export const BUILTIN_DEFAULT_TRIGGERS = {
|
|
132
|
+
'container-running': ['startup', 'docker'],
|
|
133
|
+
'port-open': ['startup'],
|
|
134
|
+
'http-ok': ['startup'],
|
|
135
|
+
'file-exists': ['startup', 'fsevent'],
|
|
136
|
+
'exec-zero': ['startup'],
|
|
137
|
+
};
|
|
138
|
+
export const BUILTIN_DEFAULT_SEVERITY = {
|
|
139
|
+
'container-running': 'error',
|
|
140
|
+
'port-open': 'error',
|
|
141
|
+
'http-ok': 'error',
|
|
142
|
+
'file-exists': 'error',
|
|
143
|
+
'exec-zero': 'error',
|
|
144
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/health/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA4BhD,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,EACrB,YAAY,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,GAC3C,aAAa,CAOf"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawnStream } from '../core/subprocess.js';
|
|
3
|
+
const defaultExec = async (cmd, args, cwd) => {
|
|
4
|
+
let stdout = '';
|
|
5
|
+
const handle = spawnStream(cmd, args, {
|
|
6
|
+
cwd,
|
|
7
|
+
onStdout: (line) => {
|
|
8
|
+
stdout += line + '\n';
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
const exitCode = await handle.exitCode;
|
|
12
|
+
return { stdout, exitCode };
|
|
13
|
+
};
|
|
14
|
+
const defaultFs = {
|
|
15
|
+
existsSync: (p) => fs.existsSync(p),
|
|
16
|
+
statSync: (p) => fs.statSync(p),
|
|
17
|
+
readdirSync: (p, opts) => {
|
|
18
|
+
if (opts?.withFileTypes) {
|
|
19
|
+
return fs.readdirSync(p, { withFileTypes: true });
|
|
20
|
+
}
|
|
21
|
+
return fs.readdirSync(p);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export function buildHealthContext(workspaceRoot, appendOutput) {
|
|
25
|
+
return {
|
|
26
|
+
workspaceRoot,
|
|
27
|
+
fs: defaultFs,
|
|
28
|
+
exec: defaultExec,
|
|
29
|
+
appendOutput,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { NotificationsConfig } from '../core/notifier.js';
|
|
2
|
+
import type { Transition } from '../core/types.js';
|
|
3
|
+
import type { NotifyPolicy } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolves whether a single transition should fire a notification given the
|
|
6
|
+
* global notifications policy and an optional per-item override.
|
|
7
|
+
*
|
|
8
|
+
* Precedence (per-item beats global):
|
|
9
|
+
* - per-item === false → silent, no matter what global says
|
|
10
|
+
* - per-item === { onTransitionTo: [...] } → fire only if transition.event ∈ list
|
|
11
|
+
* - per-item === undefined → defer to global (config.enabled + config.exclude)
|
|
12
|
+
*
|
|
13
|
+
* The global config is still consulted for `enabled` and `exclude` even when
|
|
14
|
+
* per-item is set to `{ onTransitionTo }`. That keeps the global session toggle
|
|
15
|
+
* (`n` keystroke at the global level) authoritative.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveNotify(transition: Transition, globalConfig: NotificationsConfig, perItem?: NotifyPolicy): boolean;
|
|
18
|
+
//# sourceMappingURL=notify-resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notify-resolver.d.ts","sourceRoot":"","sources":["../../src/health/notify-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAmB,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,mBAAmB,EACjC,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAcT"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves whether a single transition should fire a notification given the
|
|
3
|
+
* global notifications policy and an optional per-item override.
|
|
4
|
+
*
|
|
5
|
+
* Precedence (per-item beats global):
|
|
6
|
+
* - per-item === false → silent, no matter what global says
|
|
7
|
+
* - per-item === { onTransitionTo: [...] } → fire only if transition.event ∈ list
|
|
8
|
+
* - per-item === undefined → defer to global (config.enabled + config.exclude)
|
|
9
|
+
*
|
|
10
|
+
* The global config is still consulted for `enabled` and `exclude` even when
|
|
11
|
+
* per-item is set to `{ onTransitionTo }`. That keeps the global session toggle
|
|
12
|
+
* (`n` keystroke at the global level) authoritative.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveNotify(transition, globalConfig, perItem) {
|
|
15
|
+
if (perItem === false)
|
|
16
|
+
return false;
|
|
17
|
+
if (!globalConfig.enabled)
|
|
18
|
+
return false;
|
|
19
|
+
if (globalConfig.exclude.includes(transition.event))
|
|
20
|
+
return false;
|
|
21
|
+
if (perItem && typeof perItem === 'object') {
|
|
22
|
+
const allowed = perItem.onTransitionTo;
|
|
23
|
+
if (allowed && allowed.length > 0) {
|
|
24
|
+
return allowed.includes(transition.event);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HealthCheck, HealthCheckConfig } from './types.js';
|
|
2
|
+
export interface BuildHealthRegistryOptions {
|
|
3
|
+
/** Pre-built checks contributed by the active profile. */
|
|
4
|
+
profileChecks?: HealthCheck[];
|
|
5
|
+
/** Config-driven entries from cockpit.yaml `health[]`. */
|
|
6
|
+
configEntries?: HealthCheckConfig[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the final ordered list of HealthCheck definitions:
|
|
10
|
+
* 1. Built-in factory entries from cockpit.yaml are materialised first.
|
|
11
|
+
* 2. Profile-contributed checks come next.
|
|
12
|
+
*
|
|
13
|
+
* Collisions are detected on either `id` or `type` (where applicable):
|
|
14
|
+
* - If a profile check declares a built-in `type`, it is rejected (built-ins win).
|
|
15
|
+
* - If two checks share an `id`, the second is rejected.
|
|
16
|
+
*
|
|
17
|
+
* Profiles MAY register checks with novel `type` ids (or no `type`) — those are allowed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildHealthRegistry(opts?: BuildHealthRegistryOptions): HealthCheck[];
|
|
20
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/health/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAoB,MAAM,YAAY,CAAC;AAUnF,MAAM,WAAW,0BAA0B;IACzC,0DAA0D;IAC1D,aAAa,CAAC,EAAE,WAAW,EAAE,CAAC;IAC9B,0DAA0D;IAC1D,aAAa,CAAC,EAAE,iBAAiB,EAAE,CAAC;CACrC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,0BAA+B,GAAG,WAAW,EAAE,CAkDxF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { BUILTIN_CHECK_FACTORIES, BUILTIN_DEFAULT_TRIGGERS, BUILTIN_DEFAULT_SEVERITY, } from './builtin.js';
|
|
2
|
+
import { getLogger } from '../core/logger.js';
|
|
3
|
+
const BUILTIN_TYPES = new Set(Object.keys(BUILTIN_CHECK_FACTORIES));
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the final ordered list of HealthCheck definitions:
|
|
6
|
+
* 1. Built-in factory entries from cockpit.yaml are materialised first.
|
|
7
|
+
* 2. Profile-contributed checks come next.
|
|
8
|
+
*
|
|
9
|
+
* Collisions are detected on either `id` or `type` (where applicable):
|
|
10
|
+
* - If a profile check declares a built-in `type`, it is rejected (built-ins win).
|
|
11
|
+
* - If two checks share an `id`, the second is rejected.
|
|
12
|
+
*
|
|
13
|
+
* Profiles MAY register checks with novel `type` ids (or no `type`) — those are allowed.
|
|
14
|
+
*/
|
|
15
|
+
export function buildHealthRegistry(opts = {}) {
|
|
16
|
+
const log = safeLogger();
|
|
17
|
+
const seenIds = new Set();
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const entry of opts.configEntries ?? []) {
|
|
20
|
+
if (seenIds.has(entry.id)) {
|
|
21
|
+
log?.warn?.({ id: entry.id }, 'health: duplicate id from config; skipping');
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!BUILTIN_TYPES.has(entry.type)) {
|
|
25
|
+
log?.warn?.({ id: entry.id, type: entry.type }, 'health: unknown built-in type in config; skipping');
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const type = entry.type;
|
|
29
|
+
const factory = BUILTIN_CHECK_FACTORIES[type];
|
|
30
|
+
const check = {
|
|
31
|
+
id: entry.id,
|
|
32
|
+
label: entry.label,
|
|
33
|
+
severity: entry.severity ?? BUILTIN_DEFAULT_SEVERITY[type],
|
|
34
|
+
triggers: entry.triggers ?? BUILTIN_DEFAULT_TRIGGERS[type],
|
|
35
|
+
predicate: factory(entry),
|
|
36
|
+
remediation: entry.remediation,
|
|
37
|
+
notify: entry.notify,
|
|
38
|
+
};
|
|
39
|
+
seenIds.add(entry.id);
|
|
40
|
+
out.push(check);
|
|
41
|
+
}
|
|
42
|
+
for (const check of opts.profileChecks ?? []) {
|
|
43
|
+
if (seenIds.has(check.id)) {
|
|
44
|
+
log?.warn?.({ id: check.id }, 'health: profile check id collides with config; skipping');
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const profileType = check.type;
|
|
48
|
+
if (profileType && BUILTIN_TYPES.has(profileType)) {
|
|
49
|
+
log?.warn?.({ id: check.id, type: profileType }, 'health: profile attempted to override built-in type; skipping');
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
seenIds.add(check.id);
|
|
53
|
+
out.push(check);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function safeLogger() {
|
|
58
|
+
try {
|
|
59
|
+
return getLogger();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HealthCheck, HealthContext, Remediation } from './types.js';
|
|
2
|
+
export type RemediationKey = string;
|
|
3
|
+
export declare function runRemediation(key: RemediationKey, checks: HealthCheck[], ctx: HealthContext, workspaceRoot: string): Promise<void>;
|
|
4
|
+
export declare function findRemediation(key: RemediationKey, checks: HealthCheck[]): HealthCheck | undefined;
|
|
5
|
+
export declare function dispatchRemediation(remediation: Remediation, ctx: HealthContext, workspaceRoot: string, sourceId: string): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=remediations.d.ts.map
|