@ctrl-spc/cs 0.7.14 → 0.7.16
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 +50 -3
- package/dist/agents.js +175 -1
- package/dist/autostart.js +103 -122
- package/dist/codex-home.js +14 -11
- package/dist/companion-ui.js +54 -6
- package/dist/companion.js +86 -169
- package/dist/config.js +199 -17
- package/dist/daemon-lifecycle.js +575 -0
- package/dist/daemon-lock.js +149 -42
- package/dist/daemon-processes.js +860 -0
- package/dist/daemon.js +14 -46
- package/dist/darwin-coalition.js +340 -0
- package/dist/failure-reason.js +76 -16
- package/dist/index.js +75 -76
- package/dist/login.js +9 -9
- package/dist/mcp.js +55 -56
- package/dist/native/darwin-coalition +0 -0
- package/dist/native/darwin-coalition.build.json +1 -0
- package/dist/native/darwin-coalition.c +145 -0
- package/dist/orchestrator.js +892 -575
- package/dist/panel3/coordinator.js +3 -1
- package/dist/panel3/presence.js +1 -1
- package/dist/panel3/run.js +996 -558
- package/dist/panel3/spawn.js +85 -23
- package/dist/panel3/tools.js +5 -0
- package/dist/presence-heartbeat.js +3 -0
- package/dist/presence.js +271 -135
- package/dist/supabase.js +173 -37
- package/dist/win-shell.js +464 -1
- package/dist/windows-job.js +312 -0
- package/package.json +4 -3
package/dist/daemon-lock.js
CHANGED
|
@@ -1,48 +1,155 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, linkSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { configDir, ensureLifecycleDir, lifecycleDir } from './config.js';
|
|
6
|
+
import { inspectProcess, processIdentityMatches, findHandoverProcesses } from './win-shell.js';
|
|
7
|
+
const identity = z.object({
|
|
8
|
+
pid: z.number().int().positive(), ppid: z.number().int().nonnegative(),
|
|
9
|
+
pgid: z.number().int().positive().optional(), owner: z.string().min(1),
|
|
10
|
+
birth: z.string().min(1), commandHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
11
|
+
macProcessVersion: z.number().int().positive().max(0xffffffff).optional(),
|
|
12
|
+
});
|
|
13
|
+
const runtimeSchema = z.object({
|
|
14
|
+
schema: z.literal(1), nonce: z.string().uuid(), config: z.string(), machineId: z.string(),
|
|
15
|
+
process: identity, entry: z.string(), version: z.string(), port: z.number().int().min(1).max(65535),
|
|
16
|
+
state: z.enum(['starting', 'running', 'stopping', 'degraded']),
|
|
17
|
+
});
|
|
18
|
+
const operationSchema = z.object({
|
|
19
|
+
schema: z.literal(1), id: z.string().uuid(), owner: identity,
|
|
20
|
+
action: z.enum(['start', 'stop', 'restart']),
|
|
21
|
+
successor: z.string().uuid().nullable(), successorPid: z.number().int().positive().nullable(),
|
|
22
|
+
entry: z.string().nullable(),
|
|
23
|
+
});
|
|
24
|
+
const migrationSchema = z.object({
|
|
25
|
+
schema: z.literal(1), boot: z.string().min(1), installation: z.string(), prepared: z.boolean(),
|
|
26
|
+
completed: z.boolean(), machineId: z.string(), accountId: z.string().nullable(),
|
|
27
|
+
});
|
|
28
|
+
function path(name) { return join(lifecycleDir(), name); }
|
|
29
|
+
function read(name, schema) {
|
|
30
|
+
let text;
|
|
31
|
+
try {
|
|
32
|
+
text = readFileSync(path(name), 'utf8');
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code === 'ENOENT')
|
|
36
|
+
return null;
|
|
37
|
+
throw new Error('Local service control records could not be read.', { cause: error });
|
|
38
|
+
}
|
|
39
|
+
let parsed;
|
|
40
40
|
try {
|
|
41
|
-
|
|
42
|
-
if (Number(readFileSync(path, 'utf8').trim()) === process.pid)
|
|
43
|
-
rmSync(path);
|
|
41
|
+
parsed = JSON.parse(text);
|
|
44
42
|
}
|
|
45
43
|
catch {
|
|
46
|
-
|
|
44
|
+
throw new Error('Local service control records are incomplete. No unverified process will be controlled.');
|
|
45
|
+
}
|
|
46
|
+
const result = schema.safeParse(parsed);
|
|
47
|
+
if (!result.success)
|
|
48
|
+
throw new Error('Local service control records are incomplete. No unverified process will be controlled.');
|
|
49
|
+
return result.data;
|
|
50
|
+
}
|
|
51
|
+
/** Publish complete records only. A temporary file never grants ownership. */
|
|
52
|
+
function publish(name, value, exclusive) {
|
|
53
|
+
const temp = join(ensureLifecycleDir(), name + '.' + randomUUID() + '.tmp');
|
|
54
|
+
const fd = openSync(temp, 'wx', 0o600);
|
|
55
|
+
try {
|
|
56
|
+
writeFileSync(fd, JSON.stringify(value));
|
|
57
|
+
fsyncSync(fd);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
closeSync(fd);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (exclusive)
|
|
64
|
+
linkSync(temp, path(name));
|
|
65
|
+
else
|
|
66
|
+
renameSync(temp, path(name));
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
rmSync(temp, { force: true });
|
|
47
70
|
}
|
|
48
71
|
}
|
|
72
|
+
export function readRuntime() {
|
|
73
|
+
const record = read('runtime.json', runtimeSchema);
|
|
74
|
+
if (record && record.config !== resolve(configDir()))
|
|
75
|
+
throw new Error('The service record belongs to another local instance.');
|
|
76
|
+
if (record && process.env.CTRL_SPC_V2_MACHINE_ID && record.machineId !== process.env.CTRL_SPC_V2_MACHINE_ID)
|
|
77
|
+
throw new Error('The service record belongs to another machine identity. Use that instance’s config and machine settings.');
|
|
78
|
+
return record;
|
|
79
|
+
}
|
|
80
|
+
export function readOperation() { return read('operation.json', operationSchema); }
|
|
81
|
+
export function readMigration() { return read('upgrade.json', migrationSchema); }
|
|
82
|
+
export function writeMigration(record) { publish('upgrade.json', migrationSchema.parse(record), false); }
|
|
83
|
+
export function publishRuntime(record) {
|
|
84
|
+
publish('runtime.json', runtimeSchema.parse(record), true);
|
|
85
|
+
writeFileSync(join(configDir(), 'daemon.pid'), String(record.process.pid), { mode: 0o600 });
|
|
86
|
+
}
|
|
87
|
+
export function updateRuntime(record) {
|
|
88
|
+
if (readRuntime()?.nonce !== record.nonce)
|
|
89
|
+
throw new Error('This process no longer owns the service record.');
|
|
90
|
+
publish('runtime.json', runtimeSchema.parse(record), false);
|
|
91
|
+
}
|
|
92
|
+
export function removeRuntime(nonce) {
|
|
93
|
+
const record = readRuntime();
|
|
94
|
+
if (!record || record.nonce !== nonce)
|
|
95
|
+
return;
|
|
96
|
+
rmSync(path('runtime.json'));
|
|
97
|
+
if (legacyPid() === record.process.pid)
|
|
98
|
+
rmSync(join(configDir(), 'daemon.pid'), { force: true });
|
|
99
|
+
}
|
|
100
|
+
export function legacyPid() {
|
|
101
|
+
const file = join(configDir(), 'daemon.pid');
|
|
102
|
+
if (!existsSync(file))
|
|
103
|
+
return null;
|
|
104
|
+
const value = Number(readFileSync(file, 'utf8').trim());
|
|
105
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
106
|
+
throw new Error('The older service record is incomplete; prepare the one-time upgrade with cs restart.');
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
export async function claimLifecycleOperation(action, deadline) {
|
|
110
|
+
const owner = await inspectProcess(process.pid, deadline);
|
|
111
|
+
if (!owner)
|
|
112
|
+
throw new Error('Cannot verify this command process.');
|
|
113
|
+
const record = { schema: 1, id: randomUUID(), owner, action, successor: null, successorPid: null, entry: null };
|
|
114
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
115
|
+
try {
|
|
116
|
+
publish('operation.json', record, true);
|
|
117
|
+
return record;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (error.code !== 'EEXIST')
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const prior = readOperation();
|
|
124
|
+
if (!prior)
|
|
125
|
+
continue;
|
|
126
|
+
const actual = await inspectProcess(prior.owner.pid, deadline);
|
|
127
|
+
if (actual && processIdentityMatches(prior.owner, actual))
|
|
128
|
+
throw new Error('Another service command is in progress. Run cs status to check its result.');
|
|
129
|
+
if (prior.successor) {
|
|
130
|
+
const runtime = readRuntime();
|
|
131
|
+
if (runtime?.nonce === prior.successor) {
|
|
132
|
+
const child = await inspectProcess(runtime.process.pid, deadline);
|
|
133
|
+
if (child && processIdentityMatches(runtime.process, child)) {
|
|
134
|
+
removeOperation(prior.id);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const candidates = (await findHandoverProcesses(prior.successor, deadline)).filter((candidate) => candidate.owner === prior.owner.owner);
|
|
139
|
+
if (candidates.length) {
|
|
140
|
+
throw new Error('A prior restart is still being reconciled. Run cs status before retrying; no second service was started.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
removeOperation(prior.id);
|
|
144
|
+
}
|
|
145
|
+
throw new Error('Service ownership changed while the command was starting. Run cs status and retry.');
|
|
146
|
+
}
|
|
147
|
+
export function updateOperation(record) {
|
|
148
|
+
if (readOperation()?.id !== record.id)
|
|
149
|
+
throw new Error('The lifecycle operation is no longer owned by this command.');
|
|
150
|
+
publish('operation.json', operationSchema.parse(record), false);
|
|
151
|
+
}
|
|
152
|
+
export function removeOperation(id) {
|
|
153
|
+
if (readOperation()?.id === id)
|
|
154
|
+
rmSync(path('operation.json'));
|
|
155
|
+
}
|