dev-cockpit 0.1.0 → 0.2.1

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.
Files changed (140) hide show
  1. package/README.md +64 -29
  2. package/bin/dev-cockpit.mjs +26 -4
  3. package/dist/actions/builtin.d.ts +25 -0
  4. package/dist/actions/builtin.d.ts.map +1 -0
  5. package/dist/actions/dispatch.d.ts +21 -0
  6. package/dist/actions/dispatch.d.ts.map +1 -0
  7. package/dist/actions/registry.d.ts +11 -0
  8. package/dist/actions/registry.d.ts.map +1 -0
  9. package/dist/actions/types.d.ts +76 -0
  10. package/dist/actions/types.d.ts.map +1 -0
  11. package/dist/buildCli.d.ts.map +1 -1
  12. package/dist/chunk-6XGHLLYT.js +46 -0
  13. package/dist/chunk-6XGHLLYT.js.map +7 -0
  14. package/dist/chunk-Q6677JQF.js +32609 -0
  15. package/dist/chunk-Q6677JQF.js.map +7 -0
  16. package/dist/chunk-VN6UILQW.js +1460 -0
  17. package/dist/chunk-VN6UILQW.js.map +7 -0
  18. package/dist/cockpit/Cockpit.d.ts +6 -0
  19. package/dist/cockpit/Cockpit.d.ts.map +1 -1
  20. package/dist/cockpit/Footer.d.ts +6 -4
  21. package/dist/cockpit/Footer.d.ts.map +1 -1
  22. package/dist/cockpit/TabBar.d.ts.map +1 -1
  23. package/dist/cockpit/hooks/useGlobalKeys.d.ts +15 -15
  24. package/dist/cockpit/hooks/useGlobalKeys.d.ts.map +1 -1
  25. package/dist/cockpit/hooks/useTerminalWidth.d.ts +12 -0
  26. package/dist/cockpit/hooks/useTerminalWidth.d.ts.map +1 -0
  27. package/dist/cockpit/panes/CommandModal.d.ts +18 -0
  28. package/dist/cockpit/panes/CommandModal.d.ts.map +1 -0
  29. package/dist/cockpit/panes/Help.d.ts.map +1 -1
  30. package/dist/cockpit/panes/Output.d.ts +7 -0
  31. package/dist/cockpit/panes/Output.d.ts.map +1 -1
  32. package/dist/cockpit/panes/Repos.d.ts.map +1 -1
  33. package/dist/cockpit/state/store.d.ts +14 -11
  34. package/dist/cockpit/state/store.d.ts.map +1 -1
  35. package/dist/cockpit/tab-state.d.ts +12 -0
  36. package/dist/cockpit/tab-state.d.ts.map +1 -1
  37. package/dist/commands/dev.d.ts.map +1 -1
  38. package/dist/commands/init-config-wizard.d.ts +103 -2
  39. package/dist/commands/init-config-wizard.d.ts.map +1 -1
  40. package/dist/commands/init-config.d.ts.map +1 -1
  41. package/dist/commands/migrate-config.d.ts +18 -0
  42. package/dist/commands/migrate-config.d.ts.map +1 -0
  43. package/dist/commands/mount.d.ts +17 -32
  44. package/dist/commands/mount.d.ts.map +1 -1
  45. package/dist/core/config.d.ts +73 -5
  46. package/dist/core/config.d.ts.map +1 -1
  47. package/dist/core/migrations.d.ts +33 -0
  48. package/dist/core/migrations.d.ts.map +1 -0
  49. package/dist/core/subprocess.d.ts +20 -0
  50. package/dist/core/subprocess.d.ts.map +1 -1
  51. package/dist/core/types.d.ts +36 -12
  52. package/dist/core/types.d.ts.map +1 -1
  53. package/dist/devtools-YXMW6JJ6.js +3720 -0
  54. package/dist/devtools-YXMW6JJ6.js.map +7 -0
  55. package/dist/docker/highlights.d.ts +14 -4
  56. package/dist/docker/highlights.d.ts.map +1 -1
  57. package/dist/docker/logs.d.ts +3 -2
  58. package/dist/docker/logs.d.ts.map +1 -1
  59. package/dist/health/builtin.d.ts.map +1 -1
  60. package/dist/index.d.ts +14 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +92837 -53
  63. package/dist/index.js.map +7 -0
  64. package/dist/ink.js +38 -1
  65. package/dist/ink.js.map +7 -0
  66. package/dist/mount/compose.d.ts +21 -0
  67. package/dist/mount/compose.d.ts.map +1 -0
  68. package/dist/mount/discovery.d.ts +35 -0
  69. package/dist/mount/discovery.d.ts.map +1 -0
  70. package/dist/mount/git-status.d.ts +12 -0
  71. package/dist/mount/git-status.d.ts.map +1 -0
  72. package/dist/mount/manifest.d.ts +16 -0
  73. package/dist/mount/manifest.d.ts.map +1 -0
  74. package/dist/mount/symlinks.d.ts +30 -0
  75. package/dist/mount/symlinks.d.ts.map +1 -0
  76. package/dist/mount/types.d.ts +60 -0
  77. package/dist/mount/types.d.ts.map +1 -0
  78. package/dist/react.js +35 -1
  79. package/dist/react.js.map +7 -0
  80. package/dist/runCockpit.d.ts +3 -0
  81. package/dist/runCockpit.d.ts.map +1 -1
  82. package/docs/commands.md +29 -16
  83. package/docs/config-reference.md +115 -11
  84. package/docs/getting-started.md +9 -6
  85. package/docs/index.md +5 -1
  86. package/docs/init-config.md +34 -8
  87. package/docs/mount.md +198 -25
  88. package/docs/notifications.md +14 -13
  89. package/docs/panes.md +36 -15
  90. package/docs/processes.md +42 -0
  91. package/package.json +93 -90
  92. package/dist/buildCli.js +0 -107
  93. package/dist/cli.js +0 -2
  94. package/dist/cockpit/Cockpit.js +0 -73
  95. package/dist/cockpit/Footer.js +0 -33
  96. package/dist/cockpit/TabBar.js +0 -12
  97. package/dist/cockpit/help/content.js +0 -22
  98. package/dist/cockpit/help/loader.js +0 -118
  99. package/dist/cockpit/help/renderer.js +0 -35
  100. package/dist/cockpit/help/types.js +0 -1
  101. package/dist/cockpit/hooks/useCockpitStore.js +0 -5
  102. package/dist/cockpit/hooks/useGlobalKeys.js +0 -173
  103. package/dist/cockpit/panes/FilterModal.js +0 -22
  104. package/dist/cockpit/panes/Health.js +0 -30
  105. package/dist/cockpit/panes/Help.js +0 -81
  106. package/dist/cockpit/panes/Output.js +0 -108
  107. package/dist/cockpit/panes/Repos.js +0 -48
  108. package/dist/cockpit/panes/SearchModal.js +0 -31
  109. package/dist/cockpit/state/store.js +0 -111
  110. package/dist/cockpit/tab-state.js +0 -7
  111. package/dist/commands/dev.js +0 -158
  112. package/dist/commands/doctor.js +0 -66
  113. package/dist/commands/init-config-wizard.js +0 -818
  114. package/dist/commands/init-config.js +0 -131
  115. package/dist/commands/mount.js +0 -150
  116. package/dist/core/config.js +0 -152
  117. package/dist/core/logger.js +0 -38
  118. package/dist/core/notifier.js +0 -100
  119. package/dist/core/paths.js +0 -18
  120. package/dist/core/subprocess.js +0 -82
  121. package/dist/core/types.js +0 -1
  122. package/dist/docker/highlights.js +0 -79
  123. package/dist/docker/logs.js +0 -172
  124. package/dist/docker/restart.js +0 -45
  125. package/dist/docker/stack-trace.js +0 -44
  126. package/dist/health/builtin.js +0 -144
  127. package/dist/health/context.js +0 -31
  128. package/dist/health/notify-resolver.js +0 -28
  129. package/dist/health/registry.js +0 -64
  130. package/dist/health/remediations.js +0 -41
  131. package/dist/health/runner.js +0 -22
  132. package/dist/health/scheduler.js +0 -107
  133. package/dist/health/types.js +0 -1
  134. package/dist/health/useHealth.js +0 -122
  135. package/dist/lint/reactive.js +0 -131
  136. package/dist/runCockpit.js +0 -75
  137. package/dist/watchers/manager.js +0 -239
  138. package/dist/watchers/path-mapper.js +0 -29
  139. package/dist/watchers/types.js +0 -9
  140. package/docs/watchers.md +0 -27
@@ -1,79 +0,0 @@
1
- /**
2
- * Highlight pattern matcher for log lines.
3
- *
4
- * Pure: no I/O. Patterns are compiled once via `compileHighlights` and the
5
- * resulting matcher is cheap to call per line.
6
- *
7
- * Severity inference: lines containing "fatal", "error", "exception", or
8
- * "stack trace" → 'error'. Lines containing "warning" → 'warn'. Otherwise
9
- * 'info'.
10
- *
11
- * Default patterns ship a domain-neutral minimum (ERROR / FATAL / WARN /
12
- * Stack trace). Consumers add language-specific patterns via config or a
13
- * profile-specific default.
14
- */
15
- /**
16
- * Domain-neutral default highlight patterns. Case-insensitive substring
17
- * matches. Consumers who want language-specific shapes (PHP fatals,
18
- * stack frames, framework-specific markers) add them via config.
19
- */
20
- export const DEFAULT_HIGHLIGHT_PATTERNS = [
21
- 'ERROR',
22
- 'FATAL',
23
- 'WARN',
24
- 'Stack trace',
25
- ];
26
- function inferSeverity(source) {
27
- const lower = source.toLowerCase();
28
- if (lower.includes('fatal') ||
29
- lower.includes('error') ||
30
- lower.includes('exception') ||
31
- lower.includes('stack trace')) {
32
- return 'error';
33
- }
34
- if (lower.includes('warning') || lower.includes('warn'))
35
- return 'warn';
36
- return 'info';
37
- }
38
- /**
39
- * Initiator = a NEW error event (worth notifying). Continuation = a
40
- * follow-on line (stack frame, trace header) that accompanies an
41
- * initiator already notified — should still highlight in Output but
42
- * shouldn't trigger its own notification.
43
- */
44
- function isInitiatorPattern(source) {
45
- const lower = source.toLowerCase();
46
- if (lower.includes('stack trace'))
47
- return false;
48
- return true;
49
- }
50
- /**
51
- * Compile a list of pattern sources into a matcher. Each source is treated
52
- * as a case-insensitive substring (regex metacharacters escaped).
53
- */
54
- export function compileHighlights(sources) {
55
- const patterns = sources.map((source) => ({
56
- source,
57
- regex: new RegExp(source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'),
58
- }));
59
- return {
60
- patterns,
61
- match(line) {
62
- for (const p of patterns) {
63
- if (p.regex.test(line)) {
64
- return {
65
- matched: true,
66
- severity: inferSeverity(p.source),
67
- pattern: p.source,
68
- isInitiator: isInitiatorPattern(p.source),
69
- };
70
- }
71
- }
72
- return { matched: false, severity: 'info', pattern: null, isInitiator: false };
73
- },
74
- };
75
- }
76
- /** One-shot match without an explicit compile step. Slower per call. */
77
- export function matchHighlights(line, patterns) {
78
- return compileHighlights(patterns).match(line);
79
- }
@@ -1,172 +0,0 @@
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
- }
@@ -1,45 +0,0 @@
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
- }
@@ -1,44 +0,0 @@
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
- }
@@ -1,144 +0,0 @@
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
- };
@@ -1,31 +0,0 @@
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
- }
@@ -1,28 +0,0 @@
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
- }
@@ -1,64 +0,0 @@
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
- }
@@ -1,41 +0,0 @@
1
- import { spawnStream } from '../core/subprocess.js';
2
- export async function runRemediation(key, checks, ctx, workspaceRoot) {
3
- const check = checks.find((c) => c.remediation.key === key);
4
- if (!check)
5
- return;
6
- await dispatchRemediation(check.remediation, ctx, workspaceRoot, check.id);
7
- }
8
- export function findRemediation(key, checks) {
9
- return checks.find((c) => c.remediation.key === key);
10
- }
11
- export async function dispatchRemediation(remediation, ctx, workspaceRoot, sourceId) {
12
- if ('run' in remediation) {
13
- await remediation.run(ctx, workspaceRoot);
14
- return;
15
- }
16
- const cmdline = remediation.command.trim();
17
- if (!cmdline)
18
- return;
19
- const parts = cmdline.split(/\s+/);
20
- const program = parts[0];
21
- if (!program)
22
- return;
23
- const args = parts.slice(1);
24
- const cwd = remediation.cwd ?? workspaceRoot;
25
- const handle = spawnStream(program, args, {
26
- cwd,
27
- onStdout: (line) => ctx.appendOutput?.({
28
- ts: Date.now(),
29
- source: `health:${sourceId}`,
30
- severity: 'info',
31
- text: line,
32
- }),
33
- onStderr: (line) => ctx.appendOutput?.({
34
- ts: Date.now(),
35
- source: `health:${sourceId}`,
36
- severity: 'warn',
37
- text: line,
38
- }),
39
- });
40
- await handle.exitCode;
41
- }
@@ -1,22 +0,0 @@
1
- export async function runChecks(checks, triggerSet, ctx) {
2
- const triggerSetAsSet = new Set(triggerSet);
3
- const relevantChecks = checks.filter((check) => check.triggers.some((t) => triggerSetAsSet.has(t)));
4
- if (relevantChecks.length === 0) {
5
- return [];
6
- }
7
- const results = await Promise.all(relevantChecks.map(async (check) => {
8
- try {
9
- return await check.predicate(ctx);
10
- }
11
- catch (err) {
12
- return {
13
- id: check.id,
14
- label: check.label,
15
- severity: 'warn',
16
- detail: `check failed with error: ${String(err)}`,
17
- remediationKey: check.remediation.key,
18
- };
19
- }
20
- }));
21
- return results;
22
- }