@ours.network/fleet 1.2.0-nightly.66 → 1.2.0-nightly.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/atomic-file.d.ts +17 -0
- package/dist/atomic-file.js +93 -18
- package/dist/build-info.json +6 -4
- package/dist/capabilities.d.ts +4 -1
- package/dist/capabilities.js +5 -0
- package/dist/cli.js +30 -0
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +13 -1
- package/dist/doctor.d.ts +3 -0
- package/dist/doctor.js +20 -0
- package/dist/harness/agent-session.d.ts +2 -0
- package/dist/harness/claude-code-session.js +1 -1
- package/dist/harness/codex-session.js +2 -2
- package/dist/process-incarnation.d.ts +23 -0
- package/dist/process-incarnation.js +42 -0
- package/dist/rooms-tasks/cowork-adapter.d.ts +10 -1
- package/dist/rooms-tasks/cowork-adapter.js +8 -0
- package/dist/rooms-tasks/cowork-http.js +29 -7
- package/dist/runner.d.ts +7 -0
- package/dist/runner.js +76 -12
- package/dist/session/acp.d.ts +14 -0
- package/dist/session/acp.js +22 -6
- package/dist/session/codex-app-server.d.ts +6 -0
- package/dist/session/codex-app-server.js +17 -4
- package/dist/session/types.d.ts +6 -0
- package/dist/startup-readiness.d.ts +19 -0
- package/dist/startup-readiness.js +63 -0
- package/dist/supervisor/registration-refresh.d.ts +45 -0
- package/dist/supervisor/registration-refresh.js +266 -0
- package/dist/workspace-enrollment.d.ts +3 -2
- package/dist/workspace-enrollment.js +19 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -299,6 +299,7 @@ ours-fleet loops reload <Role>
|
|
|
299
299
|
ours-fleet loops run-now|disable|enable <Role> <Loop>
|
|
300
300
|
ours-fleet rm <Name>
|
|
301
301
|
ours-fleet doctor [--harness H]
|
|
302
|
+
ours-fleet service check|refresh [--json] service definitions vs this release (refresh never starts or restarts)
|
|
302
303
|
ours-fleet managed-cli setup [--enable <Agent>]... | status [--json] native harness policy for the task workflow
|
|
303
304
|
ours-fleet version [--json] build identity, capabilities, installs on PATH
|
|
304
305
|
ours-fleet init
|
|
@@ -326,6 +327,28 @@ is prepared the same way but not yet qualified. Scope, boundaries, unsupported
|
|
|
326
327
|
combinations and the evidence are in
|
|
327
328
|
[docs/validation/managed-cli-permissions.md](docs/validation/managed-cli-permissions.md).
|
|
328
329
|
|
|
330
|
+
### Service definitions after an update
|
|
331
|
+
|
|
332
|
+
`ours-fleet init`, `web install` and the watchdog installer write user service
|
|
333
|
+
definitions (systemd units on Linux, LaunchAgents on macOS) that start the exact
|
|
334
|
+
release tree they were written from. An update installs a new tree, but a running
|
|
335
|
+
service keeps its code, and an unrefreshed definition starts the previous release
|
|
336
|
+
again after the next reboot while agents and the CLI use the new one.
|
|
337
|
+
|
|
338
|
+
```sh
|
|
339
|
+
ours-fleet service check # read-only; exit 1 when a definition or running service uses another release
|
|
340
|
+
ours-fleet service refresh # re-point existing definitions at this release
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`refresh` changes only the release paths in definitions Fleet generated. It keeps
|
|
344
|
+
environment, profile, ports and drop-ins, runs `daemon-reload`, and never creates,
|
|
345
|
+
enables, starts, stops or restarts a service. Definitions that Fleet did not
|
|
346
|
+
generate, or whose `ExecStart` a drop-in overrides, are reported and left alone.
|
|
347
|
+
A running supervisor still uses its old code until you restart it
|
|
348
|
+
(`systemctl --user restart ours-fleet.service`); its agents resume through it.
|
|
349
|
+
`ours-install` runs `refresh` after updating Fleet, and `doctor` shows the same
|
|
350
|
+
rows as `check`.
|
|
351
|
+
|
|
329
352
|
### Which build am I running?
|
|
330
353
|
|
|
331
354
|
`--version` prints a semver, and a semver does not identify an artifact: version
|
package/dist/atomic-file.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type IncarnationProbe } from './process-incarnation.js';
|
|
1
2
|
/**
|
|
2
3
|
* Cross-process file protection: a lock, and a replace that cannot leave a
|
|
3
4
|
* partial file behind.
|
|
@@ -12,6 +13,22 @@ export interface LockDeps {
|
|
|
12
13
|
sleep?(ms: number): Promise<void>;
|
|
13
14
|
/** Test hook after a complete private claim is prepared, before atomic publication. */
|
|
14
15
|
beforePublish?(): void | Promise<void>;
|
|
16
|
+
/** Test seam for PID liveness; defaults to signal 0. */
|
|
17
|
+
alive?(pid: number): boolean;
|
|
18
|
+
/** Test seam for boot id and process start markers. */
|
|
19
|
+
incarnation?: IncarnationProbe;
|
|
20
|
+
/** Test hook inside the reclaim section, before the dead holder is re-read. */
|
|
21
|
+
beforeReclaim?(): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A live holder recorded without process-incarnation markers by a release that
|
|
25
|
+
* predates them. Its PID may be reused, so the lock can be neither trusted nor
|
|
26
|
+
* removed; the caller fails with the path instead of waiting forever.
|
|
27
|
+
*/
|
|
28
|
+
export declare class FileLockOwnerUnverifiableError extends Error {
|
|
29
|
+
readonly lockPath: string;
|
|
30
|
+
readonly pid: number;
|
|
31
|
+
constructor(lockPath: string, pid: number);
|
|
15
32
|
}
|
|
16
33
|
/** The syscalls a replace depends on, injectable so faults can be forced deterministically. */
|
|
17
34
|
export interface WriteDeps {
|
package/dist/atomic-file.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, writeSync, } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { incarnationEnded, procIncarnationProbe, processIncarnation, } from './process-incarnation.js';
|
|
5
|
+
/**
|
|
6
|
+
* A live holder recorded without process-incarnation markers by a release that
|
|
7
|
+
* predates them. Its PID may be reused, so the lock can be neither trusted nor
|
|
8
|
+
* removed; the caller fails with the path instead of waiting forever.
|
|
9
|
+
*/
|
|
10
|
+
export class FileLockOwnerUnverifiableError extends Error {
|
|
11
|
+
lockPath;
|
|
12
|
+
pid;
|
|
13
|
+
constructor(lockPath, pid) {
|
|
14
|
+
super(`lock ${lockPath} is held by PID ${pid}, recorded without a boot or start marker by an older `
|
|
15
|
+
+ 'release, so it can be neither trusted nor proved stale; it was left in place. Inspect PID '
|
|
16
|
+
+ `${pid}: the lock may be removed only once that process is proved not to be the Fleet process `
|
|
17
|
+
+ 'that wrote it.');
|
|
18
|
+
this.lockPath = lockPath;
|
|
19
|
+
this.pid = pid;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
4
22
|
function fsyncDirectory(dir) {
|
|
5
23
|
const dirFd = openSync(dir, 'r');
|
|
6
24
|
try {
|
|
@@ -11,6 +29,7 @@ function fsyncDirectory(dir) {
|
|
|
11
29
|
}
|
|
12
30
|
}
|
|
13
31
|
const DEFAULT_STALE_MS = 10_000;
|
|
32
|
+
const SYNCHRONOUS_LOCK_TIMEOUT = 'Timed out acquiring synchronous journal lock';
|
|
14
33
|
const POLL_MS = 25;
|
|
15
34
|
/**
|
|
16
35
|
* Run `fn` holding a cross-process lock. A fully populated private claim directory
|
|
@@ -23,16 +42,40 @@ const POLL_MS = 25;
|
|
|
23
42
|
export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_STALE_MS) {
|
|
24
43
|
const now = deps.now ?? (() => Date.now());
|
|
25
44
|
const sleep = deps.sleep ?? ((ms) => new Promise(r => { setTimeout(r, ms); }));
|
|
45
|
+
const probe = deps.incarnation ?? procIncarnationProbe;
|
|
46
|
+
const alive = deps.alive ?? ((pid) => { try {
|
|
47
|
+
process.kill(pid, 0);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
} });
|
|
26
53
|
const stampPath = join(lockPath, 'ts');
|
|
27
54
|
const ownerPath = join(lockPath, 'owner.json');
|
|
55
|
+
const reclaimPath = `${lockPath}.reclaim`;
|
|
28
56
|
const token = randomUUID();
|
|
29
57
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
58
|
+
/** The exact bytes that identify one holder, so a reclaim never removes its successor. */
|
|
59
|
+
const holder = () => {
|
|
60
|
+
let owner = '', stamp = '';
|
|
61
|
+
try {
|
|
62
|
+
owner = readFileSync(ownerPath, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
catch { /* legacy or incomplete lock */ }
|
|
65
|
+
try {
|
|
66
|
+
stamp = readFileSync(stampPath, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
catch { /* holder died between mkdir and stamp */ }
|
|
69
|
+
return owner || stamp ? `${stamp}\0${owner}` : null;
|
|
70
|
+
};
|
|
30
71
|
for (let waited = 0;;) {
|
|
31
72
|
const claimPath = `${lockPath}.claim.${process.pid}.${randomUUID()}`;
|
|
32
73
|
try {
|
|
33
74
|
mkdirSync(claimPath);
|
|
34
75
|
writeFileSync(join(claimPath, 'ts'), String(now()));
|
|
35
|
-
writeFileSync(join(claimPath, 'owner.json'), JSON.stringify({
|
|
76
|
+
writeFileSync(join(claimPath, 'owner.json'), JSON.stringify({
|
|
77
|
+
token, pid: process.pid, ...processIncarnation(process.pid, probe),
|
|
78
|
+
}));
|
|
36
79
|
await deps.beforePublish?.();
|
|
37
80
|
renameSync(claimPath, lockPath);
|
|
38
81
|
break;
|
|
@@ -41,6 +84,7 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
|
|
|
41
84
|
rmSync(claimPath, { recursive: true, force: true });
|
|
42
85
|
if (!['EEXIST', 'ENOTEMPTY'].includes(e.code ?? ''))
|
|
43
86
|
throw e;
|
|
87
|
+
const observed = holder();
|
|
44
88
|
let held = null;
|
|
45
89
|
try {
|
|
46
90
|
const n = parseInt(readFileSync(stampPath, 'utf8').trim(), 10);
|
|
@@ -48,21 +92,23 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
|
|
|
48
92
|
}
|
|
49
93
|
catch { /* holder died between mkdir and stamp */ }
|
|
50
94
|
let ownerAlive = false;
|
|
95
|
+
let unverifiable;
|
|
51
96
|
try {
|
|
52
97
|
const owner = JSON.parse(readFileSync(ownerPath, 'utf8'));
|
|
98
|
+
// A reused PID is not the holder: a lock written before a reboot, or by
|
|
99
|
+
// a process that has since exited, must not block every later caller.
|
|
53
100
|
if (typeof owner.pid === 'number') {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
catch { /* dead owner */ }
|
|
101
|
+
ownerAlive = alive(owner.pid) && !incarnationEnded(owner.pid, owner, probe);
|
|
102
|
+
if (ownerAlive && owner.boot === undefined && owner.start === undefined && probe.bootId())
|
|
103
|
+
unverifiable = owner.pid;
|
|
59
104
|
}
|
|
60
105
|
}
|
|
61
106
|
catch { /* legacy or incomplete lock */ }
|
|
62
|
-
|
|
63
|
-
|
|
107
|
+
const expired = (held !== null && now() - held > staleMs) || waited > staleMs * 2;
|
|
108
|
+
if (!ownerAlive && expired && reclaimDeadHolder(reclaimPath, lockPath, observed, holder, deps))
|
|
64
109
|
continue;
|
|
65
|
-
|
|
110
|
+
if (unverifiable !== undefined && expired && waited > staleMs * 2)
|
|
111
|
+
throw new FileLockOwnerUnverifiableError(lockPath, unverifiable);
|
|
66
112
|
await sleep(POLL_MS);
|
|
67
113
|
waited += POLL_MS;
|
|
68
114
|
}
|
|
@@ -79,6 +125,30 @@ export async function withFileLock(lockPath, fn, deps = {}, staleMs = DEFAULT_ST
|
|
|
79
125
|
catch { /* never remove a lock whose ownership cannot be proved */ }
|
|
80
126
|
}
|
|
81
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Remove a dead holder's lock only while it is still that holder's. Contenders
|
|
130
|
+
* that observed the same dead holder serialize through a bakery lock, whose
|
|
131
|
+
* claims are per-process files removed only when their process is provably
|
|
132
|
+
* gone, never by age. Inside it the holder is re-read, so a reclaimer that was
|
|
133
|
+
* descheduled after observing the dead holder cannot delete the lock a faster
|
|
134
|
+
* contender has since claimed.
|
|
135
|
+
*/
|
|
136
|
+
function reclaimDeadHolder(reclaimPath, lockPath, observed, holder, deps) {
|
|
137
|
+
try {
|
|
138
|
+
withSynchronousFileLock(reclaimPath, () => {
|
|
139
|
+
deps.beforeReclaim?.();
|
|
140
|
+
if (holder() === observed)
|
|
141
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
142
|
+
});
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// A live contender stalled inside the section: wait like any other poll.
|
|
147
|
+
if (!(error instanceof Error) || error.message !== SYNCHRONOUS_LOCK_TIMEOUT)
|
|
148
|
+
throw error;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
82
152
|
/**
|
|
83
153
|
* Replace a file's contents atomically: write a temp file in the SAME directory
|
|
84
154
|
* (so the rename cannot cross a filesystem boundary), fsync it, then rename over
|
|
@@ -152,6 +222,7 @@ export function withSynchronousFileLock(path, work) {
|
|
|
152
222
|
const token = `${process.pid}-${randomUUID()}.json`;
|
|
153
223
|
const own = join(path, token);
|
|
154
224
|
const deadline = Date.now() + 10_000;
|
|
225
|
+
const incarnation = processIncarnation(process.pid);
|
|
155
226
|
const claims = () => {
|
|
156
227
|
const result = [];
|
|
157
228
|
for (const name of readdirSync(path).filter(name => /^[0-9]+-[a-f0-9-]{36}\.json$/.test(name))) {
|
|
@@ -168,35 +239,39 @@ export function withSynchronousFileLock(path, work) {
|
|
|
168
239
|
if (!Number.isSafeInteger(value.pid) || value.pid <= 0 || !Number.isSafeInteger(value.ticket) || value.ticket < 0)
|
|
169
240
|
throw new Error('Invalid synchronous lock claim');
|
|
170
241
|
if (name !== token) {
|
|
242
|
+
let dead = false;
|
|
171
243
|
try {
|
|
172
244
|
process.kill(value.pid, 0);
|
|
173
245
|
}
|
|
174
246
|
catch (error) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
247
|
+
dead = error.code === 'ESRCH';
|
|
248
|
+
}
|
|
249
|
+
// A claim from before a reboot, or whose PID now names a later process,
|
|
250
|
+
// is as dead as an exited one.
|
|
251
|
+
if (dead || incarnationEnded(value.pid, value)) {
|
|
252
|
+
// This dead contender alone owned this UUID path. Never remove
|
|
253
|
+
// the namespace or a successor's differently named claim.
|
|
254
|
+
rmSync(file, { force: true });
|
|
255
|
+
continue;
|
|
181
256
|
}
|
|
182
257
|
}
|
|
183
258
|
result.push({ name, value });
|
|
184
259
|
}
|
|
185
260
|
return result;
|
|
186
261
|
};
|
|
187
|
-
replaceFileAtomically(own, JSON.stringify({ pid: process.pid, choosing: true, ticket: 0 }));
|
|
262
|
+
replaceFileAtomically(own, JSON.stringify({ pid: process.pid, choosing: true, ticket: 0, ...incarnation }));
|
|
188
263
|
try {
|
|
189
264
|
const ticket = Math.max(0, ...claims().map(claim => claim.value.ticket)) + 1;
|
|
190
265
|
if (!Number.isSafeInteger(ticket))
|
|
191
266
|
throw new Error('Synchronous lock ticket overflow');
|
|
192
|
-
replaceFileAtomically(own, JSON.stringify({ pid: process.pid, choosing: false, ticket }));
|
|
267
|
+
replaceFileAtomically(own, JSON.stringify({ pid: process.pid, choosing: false, ticket, ...incarnation }));
|
|
193
268
|
for (;;) {
|
|
194
269
|
const preceding = claims().some(claim => claim.name !== token && (claim.value.choosing
|
|
195
270
|
|| claim.value.ticket < ticket || (claim.value.ticket === ticket && claim.name < token)));
|
|
196
271
|
if (!preceding)
|
|
197
272
|
return work();
|
|
198
273
|
if (Date.now() >= deadline)
|
|
199
|
-
throw new Error(
|
|
274
|
+
throw new Error(SYNCHRONOUS_LOCK_TIMEOUT);
|
|
200
275
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
201
276
|
}
|
|
202
277
|
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.68",
|
|
3
|
+
"buildId": "ec9481a2cd81",
|
|
4
|
+
"commit": "7c3095b817127cff76cac375f21e8124558e8f38",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-16T12:16:06.490Z",
|
|
7
7
|
"capabilities": [
|
|
8
|
+
"service.refresh-v1",
|
|
8
9
|
"managed-cli.codex-reasoning-effort-v1",
|
|
9
10
|
"managed-cli.setup-v1",
|
|
10
11
|
"workspace.enroll.preserve-profile-v1",
|
|
12
|
+
"workspace.enroll.optional-profile-v1",
|
|
11
13
|
"cowork.http-management-v1",
|
|
12
14
|
"monitor.interrupt.after_tool"
|
|
13
15
|
]
|
package/dist/capabilities.d.ts
CHANGED
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
/** `monitor.interrupt: after_tool` — cancel at the next tool boundary (#67). */
|
|
16
16
|
export declare const CAP_MONITOR_INTERRUPT_AFTER_TOOL = "monitor.interrupt.after_tool";
|
|
17
17
|
export declare const CAP_HTTP_COWORK = "cowork.http-management-v1";
|
|
18
|
+
export declare const CAP_WORKSPACE_OPTIONAL_PROFILE = "workspace.enroll.optional-profile-v1";
|
|
18
19
|
export declare const CAP_WORKSPACE_PRESERVE_PROFILE = "workspace.enroll.preserve-profile-v1";
|
|
19
20
|
/** `ours-fleet managed-cli setup|status` and the `managed_cli` Agent key (#233). */
|
|
20
21
|
export declare const CAP_MANAGED_CLI_SETUP = "managed-cli.setup-v1";
|
|
21
22
|
/** Managed CLI accepts validated Codex Brain reasoning effort without dropping it (#235). */
|
|
22
23
|
export declare const CAP_MANAGED_CLI_CODEX_EFFORT = "managed-cli.codex-reasoning-effort-v1";
|
|
23
|
-
|
|
24
|
+
/** `ours-fleet service check|refresh`: re-point service definitions after an update. */
|
|
25
|
+
export declare const CAP_SERVICE_REFRESH = "service.refresh-v1";
|
|
26
|
+
export declare const CAPABILITIES: readonly ["service.refresh-v1", "managed-cli.codex-reasoning-effort-v1", "managed-cli.setup-v1", "workspace.enroll.preserve-profile-v1", "workspace.enroll.optional-profile-v1", "cowork.http-management-v1", "monitor.interrupt.after_tool"];
|
|
24
27
|
export type Capability = (typeof CAPABILITIES)[number];
|
|
25
28
|
/** Does this build declare `token`? */
|
|
26
29
|
export declare const hasCapability: (token: string) => boolean;
|
package/dist/capabilities.js
CHANGED
|
@@ -15,15 +15,20 @@
|
|
|
15
15
|
/** `monitor.interrupt: after_tool` — cancel at the next tool boundary (#67). */
|
|
16
16
|
export const CAP_MONITOR_INTERRUPT_AFTER_TOOL = 'monitor.interrupt.after_tool';
|
|
17
17
|
export const CAP_HTTP_COWORK = 'cowork.http-management-v1';
|
|
18
|
+
export const CAP_WORKSPACE_OPTIONAL_PROFILE = 'workspace.enroll.optional-profile-v1';
|
|
18
19
|
export const CAP_WORKSPACE_PRESERVE_PROFILE = 'workspace.enroll.preserve-profile-v1';
|
|
19
20
|
/** `ours-fleet managed-cli setup|status` and the `managed_cli` Agent key (#233). */
|
|
20
21
|
export const CAP_MANAGED_CLI_SETUP = 'managed-cli.setup-v1';
|
|
21
22
|
/** Managed CLI accepts validated Codex Brain reasoning effort without dropping it (#235). */
|
|
22
23
|
export const CAP_MANAGED_CLI_CODEX_EFFORT = 'managed-cli.codex-reasoning-effort-v1';
|
|
24
|
+
/** `ours-fleet service check|refresh`: re-point service definitions after an update. */
|
|
25
|
+
export const CAP_SERVICE_REFRESH = 'service.refresh-v1';
|
|
23
26
|
export const CAPABILITIES = [
|
|
27
|
+
CAP_SERVICE_REFRESH,
|
|
24
28
|
CAP_MANAGED_CLI_CODEX_EFFORT,
|
|
25
29
|
CAP_MANAGED_CLI_SETUP,
|
|
26
30
|
CAP_WORKSPACE_PRESERVE_PROFILE,
|
|
31
|
+
CAP_WORKSPACE_OPTIONAL_PROFILE,
|
|
27
32
|
CAP_HTTP_COWORK,
|
|
28
33
|
CAP_MONITOR_INTERRUPT_AFTER_TOOL,
|
|
29
34
|
];
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { spawn as spawnChild } from 'node:child_process';
|
|
|
15
15
|
import { randomUUID } from 'node:crypto';
|
|
16
16
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, rmSync } from 'node:fs';
|
|
17
17
|
import { realpathSync } from 'node:fs';
|
|
18
|
+
import { describeRegistration, refreshServiceRegistrations, registrationDrift, } from './supervisor/registration-refresh.js';
|
|
18
19
|
import { join as joinPath, resolve as resolvePath } from 'node:path';
|
|
19
20
|
import { createInterface } from 'node:readline';
|
|
20
21
|
import { Command } from 'commander';
|
|
@@ -1400,12 +1401,41 @@ cOpt(program.command('doctor').description('prerequisite report'))
|
|
|
1400
1401
|
harness: opts.harness,
|
|
1401
1402
|
configPath: opts.configuration,
|
|
1402
1403
|
yamlMode: opts.yamlMode,
|
|
1404
|
+
serviceRegistration: { binPath },
|
|
1403
1405
|
});
|
|
1404
1406
|
for (const c of rep.checks)
|
|
1405
1407
|
console.log(`${c.ok ? 'ok ' : 'MISS'} ${c.name.padEnd(22)} ${c.detail}`);
|
|
1406
1408
|
if (!rep.ok)
|
|
1407
1409
|
throw new FleetCliExit(1);
|
|
1408
1410
|
});
|
|
1411
|
+
const serviceCommand = program.command('service')
|
|
1412
|
+
.description('installed Fleet service definitions (supervisor, web, watchdogs)');
|
|
1413
|
+
const printRegistrations = (reports, json) => {
|
|
1414
|
+
if (json)
|
|
1415
|
+
console.log(JSON.stringify({ binPath, services: reports }, null, 2));
|
|
1416
|
+
else
|
|
1417
|
+
for (const report of reports)
|
|
1418
|
+
console.log(describeRegistration(report));
|
|
1419
|
+
};
|
|
1420
|
+
serviceCommand.command('check')
|
|
1421
|
+
.description('report whether installed definitions and running services use this release; changes nothing')
|
|
1422
|
+
.option('--json', 'machine-readable report')
|
|
1423
|
+
.action(async (opts) => {
|
|
1424
|
+
const reports = await refreshServiceRegistrations({ binPath, write: false });
|
|
1425
|
+
printRegistrations(reports, opts.json);
|
|
1426
|
+
if (registrationDrift(reports).length)
|
|
1427
|
+
throw new FleetCliExit(1);
|
|
1428
|
+
});
|
|
1429
|
+
serviceCommand.command('refresh')
|
|
1430
|
+
.description('point installed definitions at this release; never creates, enables, starts, stops or restarts a service')
|
|
1431
|
+
.option('--json', 'machine-readable report')
|
|
1432
|
+
.action(async (opts) => {
|
|
1433
|
+
const reports = await refreshServiceRegistrations({ binPath, write: true });
|
|
1434
|
+
printRegistrations(reports, opts.json);
|
|
1435
|
+
if (!opts.json && reports.some(report => report.running === 'stale'))
|
|
1436
|
+
console.log('A running service still uses another release. Restart it when convenient, '
|
|
1437
|
+
+ 'e.g. systemctl --user restart ours-fleet.service; agents resume through their supervisor.');
|
|
1438
|
+
});
|
|
1409
1439
|
cOpt(program.command('init').description('add missing Fleet defaults while preserving existing configuration')
|
|
1410
1440
|
.option('--settings <file>', 'validated InitAnswers JSON; runs without interactive questions'))
|
|
1411
1441
|
.action(async (opts) => {
|