@lifeaitools/clauth 1.30.23 → 1.30.24

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 (40) hide show
  1. package/.clauth-skill/SKILL.md +111 -111
  2. package/README.md +25 -0
  3. package/cli/api.classify.test.js +75 -75
  4. package/cli/assets/codevelop/launcher-active.cmd.template +20 -20
  5. package/cli/assets/codevelop/launcher-static.cmd.template +7 -7
  6. package/cli/assets/codevelop/windows-terminal.profiles.json +48 -48
  7. package/cli/assets/watchdog.ps1 +42 -42
  8. package/cli/commands/agent-cron.js +396 -396
  9. package/cli/commands/agent-pool.js +1962 -1962
  10. package/cli/commands/codevelop.js +1190 -1190
  11. package/cli/commands/doctor.js +302 -302
  12. package/cli/commands/install.js +10 -10
  13. package/cli/commands/invite.js +175 -175
  14. package/cli/commands/join.js +179 -179
  15. package/cli/commands/npm.js +182 -182
  16. package/cli/commands/scrub.js +327 -327
  17. package/cli/commands/scrub.test.js +115 -115
  18. package/cli/commands/serve.js +41 -95
  19. package/cli/commands/watchdog.js +209 -209
  20. package/cli/conf-path.js +21 -21
  21. package/cli/enrollment-script.js +82 -82
  22. package/cli/fingerprint.js +143 -143
  23. package/cli/index.js +1053 -1053
  24. package/cli/lib/fs-git.js +282 -282
  25. package/cli/recovery.js +101 -101
  26. package/cli/studio-debug.js +1095 -1095
  27. package/cli/supervisor-registry.js +594 -589
  28. package/cli/supervisor-registry.test.js +397 -397
  29. package/cli/supervisor-ui.test.js +5 -83
  30. package/cli/watchdog-registry.js +209 -209
  31. package/cli/watchdog-registry.test.js +89 -89
  32. package/install.ps1 +21 -21
  33. package/package.json +2 -2
  34. package/scripts/bin/bootstrap-linux +0 -0
  35. package/scripts/bin/bootstrap-macos +0 -0
  36. package/scripts/bin/bootstrap-win.exe +0 -0
  37. package/supabase/migrations/001_clauth_schema.sql +12 -12
  38. package/supabase/migrations/003_clauth_config.sql +13 -13
  39. package/supabase/migrations/003_machine_enrollments.sql +39 -39
  40. package/cli/served-script-syntax.test.mjs +0 -54
@@ -3,92 +3,14 @@ import assert from 'node:assert/strict';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { listSurfaces, listPlugins, runSurfaceAction, runPluginAction } from './supervisor-registry.js';
7
6
 
8
7
  const here = path.dirname(fileURLToPath(import.meta.url));
9
8
  const serveSource = fs.readFileSync(path.join(here, 'commands', 'serve.js'), 'utf8');
10
9
 
11
- // Regression coverage for the 2026-08-06 incident: every surface across every
12
- // plugin shares the bare id "primary", and the dashboard's onclick handlers
13
- // sent that bare id instead of the plugin-qualified "plugin_id:id" that
14
- // findSurface() already supported. The server-side .find() always resolved
15
- // to the FIRST surface in the array (codeflow-mcp), which unconditionally
16
- // rejects every action as self-owned -- so all 49 buttons (7 surfaces x 7
17
- // actions) silently hit the same surface and appeared to do nothing, on
18
- // every surface, for every action. The prior test in this file only
19
- // regex-matched that the action-name strings existed in source; it would
20
- // pass under this exact bug. These tests exercise the real resolution path
21
- // through the same exported functions the HTTP routes call (POST
22
- // /v1/surfaces/:id/actions -> runSurfaceAction; POST /v1/plugins/:id/:action
23
- // -> runPluginAction), which is the same simulate-a-button-click contract as
24
- // the dashboard onclick handlers use, without needing a browser.
25
-
26
- test('the dashboard encodes a plugin-qualified surface id, not the bare (colliding) surface.id', () => {
27
- assert.match(
28
- serveSource,
29
- /jsArg\(surface\.plugin_id \+ ":" \+ surface\.id\)/,
30
- 'renderSupervisorSurface must send plugin_id:id, not the bare surface.id shared by every surface'
31
- );
32
- });
33
-
34
- test('every real surface exists and every surface id collides with at least one other (documents why plugin_id is required)', () => {
35
- const surfaces = listSurfaces();
36
- assert.ok(surfaces.length > 1, 'expected more than one discovered surface to make this regression possible');
37
- const bareIds = new Set(surfaces.map((s) => s.id));
38
- assert.equal(bareIds.size, 1, 'expected every discovered surface to share the same bare id ("primary") -- if this ever stops being true, the plugin_id qualifier is still correct but this documentation test should be updated');
39
- });
40
-
41
- test('runSurfaceAction resolves the plugin-qualified id to that exact surface, not a different one', () => {
42
- const surfaces = listSurfaces();
43
- assert.ok(surfaces.length >= 1, 'no surfaces discovered -- cannot verify resolution');
44
- for (const surface of surfaces) {
45
- const compositeId = surface.plugin_id + ':' + surface.id;
46
- // action "test" is the designed no-op/safe action -- it never touches a
47
- // real process, it only records a receipt, which is exactly what a
48
- // verification harness should use.
49
- const receipt = runSurfaceAction(compositeId, 'test', 'supervisor-ui.test.js');
50
- assert.ok(!receipt.error, 'surface ' + compositeId + ' resolution failed: ' + receipt.error);
51
- assert.equal(
52
- receipt.prior_state?.plugin_id,
53
- surface.plugin_id,
54
- 'requested surface ' + compositeId + ' but runSurfaceAction resolved a different surface owned by "' + receipt.prior_state?.plugin_id + '" -- this is the exact 2026-08-06 regression'
55
- );
56
- assert.equal(receipt.target.surface_id, compositeId);
57
- }
58
- });
59
-
60
- test('bare (unqualified) surface id is ambiguous: it always resolves to the same single surface regardless of which surface was intended', () => {
61
- const surfaces = listSurfaces();
62
- assert.ok(surfaces.length > 1, 'need multiple surfaces to demonstrate ambiguity');
63
- const bareId = surfaces[0].id;
64
- const receiptsByBareId = surfaces.map(() => runSurfaceAction(bareId, 'test', 'supervisor-ui.test.js'));
65
- const resolvedOwners = new Set(receiptsByBareId.map((r) => r.prior_state?.plugin_id));
66
- assert.equal(
67
- resolvedOwners.size,
68
- 1,
69
- 'expected the bare id to be ambiguous (always resolve to one surface) -- if this now resolves multiple owners, the collision this test guards against has already been fixed at the id level and this test can be removed'
70
- );
71
- });
72
-
73
- test('every one of the 7 button actions is exercised end-to-end through runSurfaceAction for every discovered surface', () => {
74
- const actions = ['start', 'stop', 'restart', 'reconcile', 'test', 'promote', 'rollback'];
75
- const surfaces = listSurfaces();
76
- for (const surface of surfaces) {
77
- const compositeId = surface.plugin_id + ':' + surface.id;
78
- for (const action of actions) {
79
- const receipt = runSurfaceAction(compositeId, action, 'supervisor-ui.test.js');
80
- assert.notEqual(receipt.error, 'surface_not_found', compositeId + ' / ' + action + ' could not resolve the surface');
81
- assert.notEqual(receipt.error, 'invalid_action', compositeId + ' / ' + action + ' is not a recognized action');
82
- assert.equal(receipt.prior_state?.plugin_id, surface.plugin_id, compositeId + ' / ' + action + ' resolved the wrong surface');
83
- }
10
+ test('clauth supervisor UI exposes every lifecycle action and receipt feedback', () => {
11
+ for (const action of ['start', 'stop', 'restart', 'reconcile', 'test', 'promote', 'rollback']) {
12
+ assert.match(serveSource, new RegExp('"' + action + '"'));
84
13
  }
14
+ assert.match(serveSource, /supervisorFeedback\(/);
15
+ assert.match(serveSource, /supervisor-feedback/);
85
16
  });
86
-
87
- test('every plugin-level action is reachable through runPluginAction for every discovered plugin', () => {
88
- const plugins = listPlugins();
89
- assert.ok(plugins.length > 0, 'no plugins discovered');
90
- for (const plugin of plugins) {
91
- const receipt = runPluginAction(plugin.id, 'test', 'supervisor-ui.test.js');
92
- assert.notEqual(receipt.error, 'plugin_not_found', plugin.id + ' could not be resolved by its own id');
93
- }
94
- });
@@ -1,209 +1,209 @@
1
- import fs from "fs";
2
- import os from "os";
3
- import path from "path";
4
- import { spawnSync } from "child_process";
5
-
6
- const DEFAULT_TIMEOUT_MS = 3000;
7
- const VALID_KINDS = new Set(["http", "process", "pm2", "docker", "hook"]);
8
-
9
- export function getWatchdogDir() {
10
- if (process.env.CLAUTH_WATCHDOG_DIR) return process.env.CLAUTH_WATCHDOG_DIR;
11
- const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
12
- return path.join(appdata, "clauth");
13
- }
14
-
15
- export function getRegistryPath() {
16
- return path.join(getWatchdogDir(), "watchdog-services.json");
17
- }
18
-
19
- export function getEventsPath() {
20
- return path.join(getWatchdogDir(), "watchdog-events.jsonl");
21
- }
22
-
23
- function readJsonFile(filePath, fallback) {
24
- try {
25
- if (!fs.existsSync(filePath)) return fallback;
26
- return JSON.parse(fs.readFileSync(filePath, "utf8"));
27
- } catch {
28
- return fallback;
29
- }
30
- }
31
-
32
- function writeJsonFile(filePath, value) {
33
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
34
- fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
35
- }
36
-
37
- function appendEvent(event) {
38
- fs.mkdirSync(getWatchdogDir(), { recursive: true });
39
- fs.appendFileSync(getEventsPath(), `${JSON.stringify({ ts: new Date().toISOString(), ...event })}\n`, "utf8");
40
- }
41
-
42
- function normalizeCommand(command) {
43
- if (!command) return null;
44
- if (Array.isArray(command)) {
45
- const [cmd, ...args] = command;
46
- return { cmd, args: args.map(String) };
47
- }
48
- if (typeof command === "object" && typeof command.cmd === "string") {
49
- return { cmd: command.cmd, args: Array.isArray(command.args) ? command.args.map(String) : [] };
50
- }
51
- return null;
52
- }
53
-
54
- function validateCommand(command, field) {
55
- const normalized = normalizeCommand(command);
56
- if (!normalized) return null;
57
- const cmd = normalized.cmd.trim();
58
- if (!cmd) throw new Error(`${field}.cmd is required`);
59
- if (/[;&|<>]/.test(cmd)) throw new Error(`${field}.cmd must be an executable path/name, not shell syntax`);
60
- for (const arg of normalized.args) {
61
- if (/[<>]/.test(arg)) throw new Error(`${field}.args contains unsupported shell redirection`);
62
- }
63
- return normalized;
64
- }
65
-
66
- export function validateWatchdogService(service) {
67
- if (!service || typeof service !== "object") throw new Error("service must be an object");
68
- if (!service.id || typeof service.id !== "string") throw new Error("service.id is required");
69
- if (!/^[a-zA-Z0-9_.-]+$/.test(service.id)) throw new Error("service.id may contain only letters, numbers, dot, underscore, and dash");
70
- if (!service.label || typeof service.label !== "string") throw new Error("service.label is required");
71
- if (!VALID_KINDS.has(service.kind)) throw new Error(`service.kind must be one of ${[...VALID_KINDS].join(", ")}`);
72
-
73
- const restart = validateCommand(service.restart, "service.restart");
74
- const start = validateCommand(service.start, "service.start");
75
- const health = service.health && typeof service.health === "object" ? service.health : null;
76
- if (health?.url && typeof health.url !== "string") throw new Error("service.health.url must be a string");
77
- if (health?.url && !/^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(?::\d+)?\//.test(health.url)) {
78
- throw new Error("service.health.url must be localhost-only");
79
- }
80
-
81
- return {
82
- id: service.id,
83
- label: service.label,
84
- owner: service.owner || "local",
85
- kind: service.kind,
86
- health,
87
- start,
88
- restart,
89
- logs: Array.isArray(service.logs) ? service.logs.map(String) : [],
90
- tags: Array.isArray(service.tags) ? service.tags.map(String) : [],
91
- approvalRequired: service.approvalRequired !== false,
92
- restartPolicy: service.restartPolicy || "manual",
93
- };
94
- }
95
-
96
- export function validateWatchdogManifest(manifest) {
97
- if (!manifest || typeof manifest !== "object") throw new Error("manifest must be an object");
98
- const services = Array.isArray(manifest.services) ? manifest.services : null;
99
- if (!services || services.length === 0) throw new Error("manifest.services must be a non-empty array");
100
- return {
101
- schema: manifest.schema || "clauth.watchdog.v1",
102
- source: manifest.source || "manual",
103
- services: services.map(validateWatchdogService),
104
- };
105
- }
106
-
107
- export function loadRegistry() {
108
- const registry = readJsonFile(getRegistryPath(), { services: [] });
109
- return {
110
- services: Array.isArray(registry.services) ? registry.services.map(validateWatchdogService) : [],
111
- };
112
- }
113
-
114
- export function saveRegistry(registry) {
115
- writeJsonFile(getRegistryPath(), { services: registry.services.map(validateWatchdogService) });
116
- }
117
-
118
- export function registerWatchdogManifest(manifest) {
119
- const validated = validateWatchdogManifest(manifest);
120
- const registry = loadRegistry();
121
- const byId = new Map(registry.services.map((service) => [service.id, service]));
122
- for (const service of validated.services) byId.set(service.id, service);
123
- const next = { services: [...byId.values()].sort((a, b) => a.id.localeCompare(b.id)) };
124
- saveRegistry(next);
125
- appendEvent({ kind: "register", source: validated.source, service_count: validated.services.length });
126
- return { registered: validated.services.length, services: validated.services.map((service) => service.id) };
127
- }
128
-
129
- export async function evaluateWatchdogService(service) {
130
- const checkedAt = new Date().toISOString();
131
- if (service.health?.url) {
132
- const timeoutMs = Number(service.health.timeoutMs || DEFAULT_TIMEOUT_MS);
133
- try {
134
- const response = await fetch(service.health.url, { signal: AbortSignal.timeout(timeoutMs), cache: "no-store" });
135
- return {
136
- ...service,
137
- status: response.ok ? "healthy" : "degraded",
138
- checkedAt,
139
- httpStatus: response.status,
140
- };
141
- } catch (error) {
142
- return {
143
- ...service,
144
- status: "unreachable",
145
- checkedAt,
146
- error: error instanceof Error ? error.message : String(error),
147
- };
148
- }
149
- }
150
- return { ...service, status: "unknown", checkedAt };
151
- }
152
-
153
- export async function getWatchdogStatuses() {
154
- const registry = loadRegistry();
155
- const services = await Promise.all(registry.services.map(evaluateWatchdogService));
156
- return {
157
- checkedAt: new Date().toISOString(),
158
- total: services.length,
159
- healthy: services.filter((service) => service.status === "healthy").length,
160
- degraded: services.filter((service) => service.status === "degraded").length,
161
- unreachable: services.filter((service) => service.status === "unreachable").length,
162
- services,
163
- };
164
- }
165
-
166
- export function readWatchdogEvents(limit = 100) {
167
- try {
168
- if (!fs.existsSync(getEventsPath())) return [];
169
- return fs.readFileSync(getEventsPath(), "utf8")
170
- .split(/\r?\n/)
171
- .filter(Boolean)
172
- .slice(-limit)
173
- .map((line) => {
174
- try { return JSON.parse(line); } catch { return { raw: line }; }
175
- });
176
- } catch {
177
- return [];
178
- }
179
- }
180
-
181
- export function restartWatchdogService(id) {
182
- const service = loadRegistry().services.find((candidate) => candidate.id === id);
183
- if (!service) return { ok: false, error: "service_not_registered" };
184
- if (!service.restart) return { ok: false, error: "restart_not_configured" };
185
- if (service.approvalRequired && process.env.CLAUTH_WATCHDOG_APPROVED !== "1") {
186
- return { ok: false, error: "approval_required" };
187
- }
188
-
189
- const result = spawnSync(service.restart.cmd, service.restart.args, {
190
- cwd: service.restart.cwd || process.cwd(),
191
- windowsHide: true,
192
- encoding: "utf8",
193
- timeout: Number(service.restart.timeoutMs || 30000),
194
- });
195
- const event = {
196
- kind: "restart",
197
- service_id: id,
198
- status: result.status,
199
- error: result.error ? result.error.message : undefined,
200
- };
201
- appendEvent(event);
202
- return {
203
- ok: result.status === 0,
204
- status: result.status,
205
- stdout: result.stdout,
206
- stderr: result.stderr,
207
- error: result.error ? result.error.message : undefined,
208
- };
209
- }
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { spawnSync } from "child_process";
5
+
6
+ const DEFAULT_TIMEOUT_MS = 3000;
7
+ const VALID_KINDS = new Set(["http", "process", "pm2", "docker", "hook"]);
8
+
9
+ export function getWatchdogDir() {
10
+ if (process.env.CLAUTH_WATCHDOG_DIR) return process.env.CLAUTH_WATCHDOG_DIR;
11
+ const appdata = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
12
+ return path.join(appdata, "clauth");
13
+ }
14
+
15
+ export function getRegistryPath() {
16
+ return path.join(getWatchdogDir(), "watchdog-services.json");
17
+ }
18
+
19
+ export function getEventsPath() {
20
+ return path.join(getWatchdogDir(), "watchdog-events.jsonl");
21
+ }
22
+
23
+ function readJsonFile(filePath, fallback) {
24
+ try {
25
+ if (!fs.existsSync(filePath)) return fallback;
26
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
27
+ } catch {
28
+ return fallback;
29
+ }
30
+ }
31
+
32
+ function writeJsonFile(filePath, value) {
33
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
34
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
35
+ }
36
+
37
+ function appendEvent(event) {
38
+ fs.mkdirSync(getWatchdogDir(), { recursive: true });
39
+ fs.appendFileSync(getEventsPath(), `${JSON.stringify({ ts: new Date().toISOString(), ...event })}\n`, "utf8");
40
+ }
41
+
42
+ function normalizeCommand(command) {
43
+ if (!command) return null;
44
+ if (Array.isArray(command)) {
45
+ const [cmd, ...args] = command;
46
+ return { cmd, args: args.map(String) };
47
+ }
48
+ if (typeof command === "object" && typeof command.cmd === "string") {
49
+ return { cmd: command.cmd, args: Array.isArray(command.args) ? command.args.map(String) : [] };
50
+ }
51
+ return null;
52
+ }
53
+
54
+ function validateCommand(command, field) {
55
+ const normalized = normalizeCommand(command);
56
+ if (!normalized) return null;
57
+ const cmd = normalized.cmd.trim();
58
+ if (!cmd) throw new Error(`${field}.cmd is required`);
59
+ if (/[;&|<>]/.test(cmd)) throw new Error(`${field}.cmd must be an executable path/name, not shell syntax`);
60
+ for (const arg of normalized.args) {
61
+ if (/[<>]/.test(arg)) throw new Error(`${field}.args contains unsupported shell redirection`);
62
+ }
63
+ return normalized;
64
+ }
65
+
66
+ export function validateWatchdogService(service) {
67
+ if (!service || typeof service !== "object") throw new Error("service must be an object");
68
+ if (!service.id || typeof service.id !== "string") throw new Error("service.id is required");
69
+ if (!/^[a-zA-Z0-9_.-]+$/.test(service.id)) throw new Error("service.id may contain only letters, numbers, dot, underscore, and dash");
70
+ if (!service.label || typeof service.label !== "string") throw new Error("service.label is required");
71
+ if (!VALID_KINDS.has(service.kind)) throw new Error(`service.kind must be one of ${[...VALID_KINDS].join(", ")}`);
72
+
73
+ const restart = validateCommand(service.restart, "service.restart");
74
+ const start = validateCommand(service.start, "service.start");
75
+ const health = service.health && typeof service.health === "object" ? service.health : null;
76
+ if (health?.url && typeof health.url !== "string") throw new Error("service.health.url must be a string");
77
+ if (health?.url && !/^https?:\/\/(127\.0\.0\.1|localhost|\[::1\])(?::\d+)?\//.test(health.url)) {
78
+ throw new Error("service.health.url must be localhost-only");
79
+ }
80
+
81
+ return {
82
+ id: service.id,
83
+ label: service.label,
84
+ owner: service.owner || "local",
85
+ kind: service.kind,
86
+ health,
87
+ start,
88
+ restart,
89
+ logs: Array.isArray(service.logs) ? service.logs.map(String) : [],
90
+ tags: Array.isArray(service.tags) ? service.tags.map(String) : [],
91
+ approvalRequired: service.approvalRequired !== false,
92
+ restartPolicy: service.restartPolicy || "manual",
93
+ };
94
+ }
95
+
96
+ export function validateWatchdogManifest(manifest) {
97
+ if (!manifest || typeof manifest !== "object") throw new Error("manifest must be an object");
98
+ const services = Array.isArray(manifest.services) ? manifest.services : null;
99
+ if (!services || services.length === 0) throw new Error("manifest.services must be a non-empty array");
100
+ return {
101
+ schema: manifest.schema || "clauth.watchdog.v1",
102
+ source: manifest.source || "manual",
103
+ services: services.map(validateWatchdogService),
104
+ };
105
+ }
106
+
107
+ export function loadRegistry() {
108
+ const registry = readJsonFile(getRegistryPath(), { services: [] });
109
+ return {
110
+ services: Array.isArray(registry.services) ? registry.services.map(validateWatchdogService) : [],
111
+ };
112
+ }
113
+
114
+ export function saveRegistry(registry) {
115
+ writeJsonFile(getRegistryPath(), { services: registry.services.map(validateWatchdogService) });
116
+ }
117
+
118
+ export function registerWatchdogManifest(manifest) {
119
+ const validated = validateWatchdogManifest(manifest);
120
+ const registry = loadRegistry();
121
+ const byId = new Map(registry.services.map((service) => [service.id, service]));
122
+ for (const service of validated.services) byId.set(service.id, service);
123
+ const next = { services: [...byId.values()].sort((a, b) => a.id.localeCompare(b.id)) };
124
+ saveRegistry(next);
125
+ appendEvent({ kind: "register", source: validated.source, service_count: validated.services.length });
126
+ return { registered: validated.services.length, services: validated.services.map((service) => service.id) };
127
+ }
128
+
129
+ export async function evaluateWatchdogService(service) {
130
+ const checkedAt = new Date().toISOString();
131
+ if (service.health?.url) {
132
+ const timeoutMs = Number(service.health.timeoutMs || DEFAULT_TIMEOUT_MS);
133
+ try {
134
+ const response = await fetch(service.health.url, { signal: AbortSignal.timeout(timeoutMs), cache: "no-store" });
135
+ return {
136
+ ...service,
137
+ status: response.ok ? "healthy" : "degraded",
138
+ checkedAt,
139
+ httpStatus: response.status,
140
+ };
141
+ } catch (error) {
142
+ return {
143
+ ...service,
144
+ status: "unreachable",
145
+ checkedAt,
146
+ error: error instanceof Error ? error.message : String(error),
147
+ };
148
+ }
149
+ }
150
+ return { ...service, status: "unknown", checkedAt };
151
+ }
152
+
153
+ export async function getWatchdogStatuses() {
154
+ const registry = loadRegistry();
155
+ const services = await Promise.all(registry.services.map(evaluateWatchdogService));
156
+ return {
157
+ checkedAt: new Date().toISOString(),
158
+ total: services.length,
159
+ healthy: services.filter((service) => service.status === "healthy").length,
160
+ degraded: services.filter((service) => service.status === "degraded").length,
161
+ unreachable: services.filter((service) => service.status === "unreachable").length,
162
+ services,
163
+ };
164
+ }
165
+
166
+ export function readWatchdogEvents(limit = 100) {
167
+ try {
168
+ if (!fs.existsSync(getEventsPath())) return [];
169
+ return fs.readFileSync(getEventsPath(), "utf8")
170
+ .split(/\r?\n/)
171
+ .filter(Boolean)
172
+ .slice(-limit)
173
+ .map((line) => {
174
+ try { return JSON.parse(line); } catch { return { raw: line }; }
175
+ });
176
+ } catch {
177
+ return [];
178
+ }
179
+ }
180
+
181
+ export function restartWatchdogService(id) {
182
+ const service = loadRegistry().services.find((candidate) => candidate.id === id);
183
+ if (!service) return { ok: false, error: "service_not_registered" };
184
+ if (!service.restart) return { ok: false, error: "restart_not_configured" };
185
+ if (service.approvalRequired && process.env.CLAUTH_WATCHDOG_APPROVED !== "1") {
186
+ return { ok: false, error: "approval_required" };
187
+ }
188
+
189
+ const result = spawnSync(service.restart.cmd, service.restart.args, {
190
+ cwd: service.restart.cwd || process.cwd(),
191
+ windowsHide: true,
192
+ encoding: "utf8",
193
+ timeout: Number(service.restart.timeoutMs || 30000),
194
+ });
195
+ const event = {
196
+ kind: "restart",
197
+ service_id: id,
198
+ status: result.status,
199
+ error: result.error ? result.error.message : undefined,
200
+ };
201
+ appendEvent(event);
202
+ return {
203
+ ok: result.status === 0,
204
+ status: result.status,
205
+ stdout: result.stdout,
206
+ stderr: result.stderr,
207
+ error: result.error ? result.error.message : undefined,
208
+ };
209
+ }