@nowcrew/daemon 0.5.35 → 0.5.37

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.
@@ -0,0 +1,189 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { acquireDaemonInstallationLease } from "./daemon-installation-lease.js";
3
+ import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, uninstallService, } from "./computer-service.js";
4
+ import { ManagedServiceConflictError, ManagedServiceRegistryError, assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, managedServiceConflict, normalizeManagedServiceRecord, serviceDescriptorSha256, withManagedServiceRegistryLock, } from "./managed-service-registry.js";
5
+ function requirePortableManagedSpec(spec) {
6
+ if ((spec.platform !== "darwin" && spec.platform !== "linux")
7
+ || spec.descriptorPath === null
8
+ || spec.descriptor === null) {
9
+ throw new ManagedServiceRegistryError("Managed service registry supports launchd and systemd descriptors only");
10
+ }
11
+ }
12
+ export function buildManagedServiceRecord(input) {
13
+ requirePortableManagedSpec(input.spec);
14
+ return normalizeManagedServiceRecord({
15
+ version: 1,
16
+ generation: (input.generation ?? randomUUID)(),
17
+ platform: input.spec.platform,
18
+ serviceId: input.spec.id,
19
+ profile: input.spec.profile,
20
+ daemonHome: input.daemonHome,
21
+ agentsRoot: input.agentsRoot,
22
+ nodePath: input.nodePath,
23
+ entryPath: input.entryPath,
24
+ packageRoot: input.packageRoot,
25
+ npmPrefix: input.npmPrefix,
26
+ descriptorPath: input.spec.descriptorPath,
27
+ descriptorSha256: serviceDescriptorSha256(input.spec.descriptor),
28
+ createdAt: (input.now ?? (() => new Date()))().toISOString(),
29
+ });
30
+ }
31
+ function sameRecord(left, right) {
32
+ return JSON.stringify(left) === JSON.stringify(right);
33
+ }
34
+ function sameManagedIdentity(left, right) {
35
+ const { generation: _generation, createdAt: _createdAt, ...expected } = right;
36
+ return managedServiceIdentityMatches(left, expected);
37
+ }
38
+ function assertRecordDescribesSpec(input) {
39
+ requirePortableManagedSpec(input.spec);
40
+ const { spec, record } = input;
41
+ const normalizedDescriptorPath = normalizeManagedServiceRecord({
42
+ ...record,
43
+ descriptorPath: spec.descriptorPath,
44
+ }).descriptorPath;
45
+ if (record.platform !== spec.platform
46
+ || record.serviceId !== spec.id
47
+ || record.profile !== spec.profile
48
+ || record.descriptorPath !== normalizedDescriptorPath
49
+ || record.descriptorSha256 !== serviceDescriptorSha256(spec.descriptor)) {
50
+ throw new ManagedServiceRegistryError(`Managed service record does not describe '${spec.id}'`);
51
+ }
52
+ }
53
+ export async function installManagedService(input) {
54
+ assertRecordDescribesSpec(input);
55
+ requirePortableManagedSpec(input.spec);
56
+ const acquire = input.acquireInstallationLease ?? acquireDaemonInstallationLease;
57
+ await withManagedServiceRegistryLock(input.registry ?? {}, async ({ services, replace }) => {
58
+ const exact = services.find((record) => sameManagedIdentity(record, input.record));
59
+ if (exact !== undefined) {
60
+ const descriptor = await readServiceDescriptor(input.spec);
61
+ if (descriptor === input.spec.descriptor) {
62
+ await ensureServiceEnabled(input.spec, input.runner);
63
+ return;
64
+ }
65
+ if (descriptor !== null) {
66
+ throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match its registry record`);
67
+ }
68
+ const lease = await acquire(input.record.npmPrefix, {
69
+ ...(input.registry?.userHome === undefined ? {} : { userHome: input.registry.userHome }),
70
+ });
71
+ try {
72
+ await installService(input.spec, input.runner);
73
+ }
74
+ finally {
75
+ await lease.close();
76
+ }
77
+ return;
78
+ }
79
+ const conflict = managedServiceConflict(services, input.record);
80
+ if (conflict !== null) {
81
+ throw new ManagedServiceConflictError(conflict.resource, input.record, conflict.owner);
82
+ }
83
+ const existingDescriptor = await readServiceDescriptor(input.spec);
84
+ if (existingDescriptor === input.spec.descriptor) {
85
+ await ensureServiceEnabled(input.spec, input.runner);
86
+ await replace([...services, input.record].sort((left, right) => left.serviceId.localeCompare(right.serviceId)));
87
+ return;
88
+ }
89
+ if (existingDescriptor !== null) {
90
+ throw new ManagedServiceRegistryError(`Existing descriptor for '${input.spec.id}' conflicts with the requested identity`);
91
+ }
92
+ const lease = await acquire(input.record.npmPrefix, {
93
+ ...(input.registry?.userHome === undefined ? {} : { userHome: input.registry.userHome }),
94
+ });
95
+ try {
96
+ const descriptor = await readServiceDescriptor(input.spec);
97
+ if (descriptor === null) {
98
+ await installService(input.spec, input.runner);
99
+ await input.hooks?.afterDescriptorInstall?.();
100
+ }
101
+ else if (descriptor !== input.spec.descriptor) {
102
+ throw new ManagedServiceRegistryError(`Existing descriptor for '${input.spec.id}' conflicts with the requested identity`);
103
+ }
104
+ await replace([...services, input.record].sort((left, right) => left.serviceId.localeCompare(right.serviceId)));
105
+ }
106
+ finally {
107
+ await lease.close();
108
+ }
109
+ });
110
+ }
111
+ export async function uninstallUnregisteredManagedService(input) {
112
+ requirePortableManagedSpec(input.spec);
113
+ await withManagedServiceRegistryLock(input.registry ?? {}, async ({ services }) => {
114
+ const registered = services.find((record) => record.serviceId === input.spec.id
115
+ || (record.daemonHome === input.daemonHome && record.profile === input.spec.profile)
116
+ || record.descriptorPath === input.spec.descriptorPath);
117
+ if (registered !== undefined) {
118
+ throw new ManagedServiceRegistryError(`Managed service '${input.spec.id}' became registered before removal`);
119
+ }
120
+ const descriptor = await readServiceDescriptor(input.spec);
121
+ if (descriptor !== input.spec.descriptor) {
122
+ throw new ManagedServiceRegistryError(`Managed service '${input.spec.id}' is not registered and its descriptor does not match the requested identity`);
123
+ }
124
+ await serviceAction(input.spec, "stop", input.runner);
125
+ const acquire = input.acquireInstallationLease ?? acquireDaemonInstallationLease;
126
+ const lease = await acquire(input.npmPrefix, {
127
+ ...(input.registry?.userHome === undefined ? {} : { userHome: input.registry.userHome }),
128
+ });
129
+ try {
130
+ await uninstallService(input.spec, input.runner);
131
+ }
132
+ finally {
133
+ await lease.close();
134
+ }
135
+ });
136
+ }
137
+ export async function uninstallManagedService(input) {
138
+ assertRecordDescribesSpec(input);
139
+ requirePortableManagedSpec(input.spec);
140
+ await withManagedServiceRegistryLock(input.registry ?? {}, async ({ services, replace }) => {
141
+ const index = services.findIndex((record) => record.serviceId === input.record.serviceId
142
+ && record.daemonHome === input.record.daemonHome
143
+ && record.profile === input.record.profile);
144
+ if (index < 0 || !sameRecord(services[index], input.record)) {
145
+ throw new ManagedServiceRegistryError(`Managed service '${input.spec.id}' identity changed before removal`);
146
+ }
147
+ const descriptor = await readServiceDescriptor(input.spec);
148
+ if (descriptor !== null && descriptor !== input.spec.descriptor) {
149
+ throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match its registry record`);
150
+ }
151
+ const acquire = input.acquireInstallationLease ?? acquireDaemonInstallationLease;
152
+ if (descriptor !== null) {
153
+ await serviceAction(input.spec, "stop", input.runner);
154
+ }
155
+ const lease = await acquire(input.record.npmPrefix, {
156
+ ...(input.registry?.userHome === undefined ? {} : { userHome: input.registry.userHome }),
157
+ });
158
+ try {
159
+ if (descriptor !== null)
160
+ await uninstallService(input.spec, input.runner);
161
+ await replace(services.filter((_, candidateIndex) => candidateIndex !== index));
162
+ }
163
+ finally {
164
+ await lease.close();
165
+ }
166
+ });
167
+ }
168
+ export async function assertManagedServiceIdentity(input) {
169
+ assertRecordDescribesSpec({
170
+ ...input,
171
+ runner: async () => ({ exitCode: 0, stdout: "", stderr: "" }),
172
+ });
173
+ requirePortableManagedSpec(input.spec);
174
+ await withManagedServiceRegistryLock(input.registry ?? {}, async ({ services }) => {
175
+ assertRegistryCoversDescriptors(services, await listManagedServiceDescriptorPaths(input.spec.platform, input.registry?.userHome));
176
+ const registered = services.find((record) => record.serviceId === input.record.serviceId
177
+ || (record.daemonHome === input.record.daemonHome && record.profile === input.record.profile));
178
+ if (registered === undefined) {
179
+ throw new ManagedServiceRegistryError(`Managed service '${input.spec.id}' is not registered; run install first`);
180
+ }
181
+ if (!sameManagedIdentity(registered, input.record)) {
182
+ throw new ManagedServiceRegistryError(`Managed service '${input.spec.id}' identity does not match the registry`);
183
+ }
184
+ const descriptor = await readServiceDescriptor(input.spec);
185
+ if (descriptor !== input.spec.descriptor) {
186
+ throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match its registry record`);
187
+ }
188
+ });
189
+ }
@@ -0,0 +1,285 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { chmod, mkdir, open, readFile, readdir, rename, rm } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, isAbsolute, resolve } from "node:path";
6
+ import { z } from "zod";
7
+ import { acquireProfileSaveLock } from "./computer-profile-lock.js";
8
+ const AbsolutePathSchema = z.string().min(1).refine(isAbsolute, "must be an absolute path");
9
+ const ManagedServiceRecordSchema = z.object({
10
+ version: z.literal(1),
11
+ generation: z.string().uuid(),
12
+ platform: z.enum(["darwin", "linux"]),
13
+ serviceId: z.string().min(1).max(256),
14
+ profile: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,47}$/),
15
+ daemonHome: AbsolutePathSchema,
16
+ agentsRoot: AbsolutePathSchema,
17
+ nodePath: AbsolutePathSchema,
18
+ entryPath: AbsolutePathSchema,
19
+ packageRoot: AbsolutePathSchema,
20
+ npmPrefix: AbsolutePathSchema,
21
+ descriptorPath: AbsolutePathSchema,
22
+ descriptorSha256: z.string().regex(/^[0-9a-f]{64}$/),
23
+ createdAt: z.string().datetime({ offset: true }),
24
+ }).strict();
25
+ const ManagedServiceRegistrySchema = z.object({
26
+ version: z.literal(1),
27
+ services: z.array(ManagedServiceRecordSchema).max(256),
28
+ }).strict();
29
+ export class ManagedServiceRegistryError extends Error {
30
+ constructor(message, options) {
31
+ super(message, options);
32
+ this.name = "ManagedServiceRegistryError";
33
+ }
34
+ }
35
+ export class ManagedServiceConflictError extends ManagedServiceRegistryError {
36
+ resource;
37
+ owner;
38
+ constructor(resource, candidate, owner) {
39
+ super(`Managed service '${candidate.serviceId}' conflicts with '${owner.serviceId}' on ${resource}`);
40
+ this.name = "ManagedServiceConflictError";
41
+ this.resource = resource;
42
+ this.owner = owner;
43
+ }
44
+ }
45
+ function registryDirectory(userHome) {
46
+ return resolve(userHome, ".crew", "daemon", "managed-services");
47
+ }
48
+ export function managedServiceRegistryPath(userHome = homedir()) {
49
+ return resolve(registryDirectory(userHome), "registry.json");
50
+ }
51
+ export async function listManagedServiceDescriptorPaths(platform, userHome = homedir()) {
52
+ const directory = platform === "darwin"
53
+ ? resolve(userHome, "Library", "LaunchAgents")
54
+ : platform === "linux"
55
+ ? resolve(userHome, ".config", "systemd", "user")
56
+ : null;
57
+ if (directory === null)
58
+ return [];
59
+ const pattern = platform === "darwin"
60
+ ? /^com\.nowcrew\.daemon\.[a-z0-9][a-z0-9_-]{0,47}(?:\.[0-9a-f]{12})?\.plist$/
61
+ : /^nowcrew-daemon-[a-z0-9][a-z0-9_-]{0,47}(?:\.[0-9a-f]{12})?\.service$/;
62
+ try {
63
+ return (await readdir(directory, { withFileTypes: true }))
64
+ .filter((entry) => entry.isFile() && pattern.test(entry.name))
65
+ .map((entry) => resolve(directory, entry.name))
66
+ .sort();
67
+ }
68
+ catch (error) {
69
+ if (error.code === "ENOENT")
70
+ return [];
71
+ throw error;
72
+ }
73
+ }
74
+ export function assertRegistryCoversDescriptors(services, descriptorPaths) {
75
+ const registered = new Set(services.map((record) => canonicalizePath(record.descriptorPath)));
76
+ const unregistered = descriptorPaths.map(canonicalizePath).find((path) => !registered.has(path));
77
+ if (unregistered !== undefined) {
78
+ throw new ManagedServiceRegistryError(`Unregistered NowCrew service descriptor: ${unregistered}`);
79
+ }
80
+ }
81
+ export function normalizeManagedServiceRecord(input) {
82
+ const record = ManagedServiceRecordSchema.parse(input);
83
+ return ManagedServiceRecordSchema.parse({
84
+ ...record,
85
+ daemonHome: canonicalizePath(record.daemonHome),
86
+ agentsRoot: canonicalizePath(record.agentsRoot),
87
+ nodePath: canonicalizePath(record.nodePath),
88
+ entryPath: canonicalizePath(record.entryPath),
89
+ packageRoot: canonicalizePath(record.packageRoot),
90
+ npmPrefix: canonicalizePath(record.npmPrefix),
91
+ descriptorPath: canonicalizePath(record.descriptorPath),
92
+ });
93
+ }
94
+ function canonicalizePath(path) {
95
+ const absolute = resolve(path);
96
+ try {
97
+ return realpathSync.native(absolute);
98
+ }
99
+ catch (error) {
100
+ const code = error.code;
101
+ if (code === "ENOTDIR")
102
+ return absolute;
103
+ if (code !== "ENOENT")
104
+ throw error;
105
+ }
106
+ const missing = [];
107
+ let ancestor = absolute;
108
+ for (;;) {
109
+ const parent = dirname(ancestor);
110
+ if (parent === ancestor)
111
+ return absolute;
112
+ missing.unshift(ancestor.slice(parent.length + (parent.endsWith("/") ? 0 : 1)));
113
+ ancestor = parent;
114
+ try {
115
+ return resolve(realpathSync.native(ancestor), ...missing);
116
+ }
117
+ catch (error) {
118
+ const code = error.code;
119
+ if (code === "ENOTDIR")
120
+ return absolute;
121
+ if (code !== "ENOENT")
122
+ throw error;
123
+ }
124
+ }
125
+ }
126
+ function parseRegistry(path, raw) {
127
+ try {
128
+ const parsed = ManagedServiceRegistrySchema.parse(JSON.parse(raw));
129
+ const services = parsed.services.map(normalizeManagedServiceRecord);
130
+ for (let index = 0; index < services.length; index += 1) {
131
+ const candidate = services[index];
132
+ const conflict = managedServiceConflict(services.slice(0, index), candidate);
133
+ if (conflict !== null) {
134
+ throw new ManagedServiceConflictError(conflict.resource, candidate, conflict.owner);
135
+ }
136
+ }
137
+ return services;
138
+ }
139
+ catch (error) {
140
+ throw new ManagedServiceRegistryError(`Invalid managed service registry: ${path}`, { cause: error });
141
+ }
142
+ }
143
+ export function serviceDescriptorSha256(descriptor) {
144
+ return createHash("sha256").update(descriptor).digest("hex");
145
+ }
146
+ export function managedServiceIdentityMatches(record, expected) {
147
+ const normalized = normalizeManagedServiceRecord(record);
148
+ return normalized.version === expected.version
149
+ && normalized.platform === expected.platform
150
+ && normalized.serviceId === expected.serviceId
151
+ && normalized.profile === expected.profile
152
+ && normalized.daemonHome === canonicalizePath(expected.daemonHome)
153
+ && normalized.agentsRoot === canonicalizePath(expected.agentsRoot)
154
+ && normalized.nodePath === canonicalizePath(expected.nodePath)
155
+ && normalized.entryPath === canonicalizePath(expected.entryPath)
156
+ && normalized.packageRoot === canonicalizePath(expected.packageRoot)
157
+ && normalized.npmPrefix === canonicalizePath(expected.npmPrefix)
158
+ && normalized.descriptorPath === canonicalizePath(expected.descriptorPath)
159
+ && normalized.descriptorSha256 === expected.descriptorSha256;
160
+ }
161
+ async function readRegistryFile(path) {
162
+ try {
163
+ return parseRegistry(path, await readFile(path, "utf8"));
164
+ }
165
+ catch (error) {
166
+ if (error.code === "ENOENT")
167
+ return [];
168
+ throw error;
169
+ }
170
+ }
171
+ async function syncDirectory(path, platform) {
172
+ if (platform === "win32")
173
+ return;
174
+ const handle = await open(path, "r");
175
+ try {
176
+ await handle.sync();
177
+ }
178
+ finally {
179
+ await handle.close();
180
+ }
181
+ }
182
+ async function writeRegistryFile(path, services) {
183
+ const directory = dirname(path);
184
+ await mkdir(directory, { recursive: true, mode: 0o700 });
185
+ await chmod(directory, 0o700);
186
+ const temporary = `${path}.${randomUUID()}.tmp`;
187
+ let handle = null;
188
+ try {
189
+ handle = await open(temporary, "wx", 0o600);
190
+ await handle.writeFile(`${JSON.stringify({ version: 1, services }, null, 2)}\n`, "utf8");
191
+ await handle.sync();
192
+ await handle.close();
193
+ handle = null;
194
+ await rename(temporary, path);
195
+ await chmod(path, 0o600);
196
+ await syncDirectory(directory, process.platform);
197
+ }
198
+ finally {
199
+ await handle?.close().catch(() => undefined);
200
+ await rm(temporary, { force: true }).catch(() => undefined);
201
+ }
202
+ }
203
+ function sameRecord(left, right) {
204
+ return JSON.stringify(left) === JSON.stringify(right);
205
+ }
206
+ export function managedServiceConflict(services, candidate) {
207
+ for (const owner of services) {
208
+ if (owner.serviceId === candidate.serviceId)
209
+ return { resource: "serviceId", owner };
210
+ if (owner.daemonHome === candidate.daemonHome && owner.profile === candidate.profile) {
211
+ return { resource: "daemonProfile", owner };
212
+ }
213
+ if (owner.agentsRoot === candidate.agentsRoot)
214
+ return { resource: "agentsRoot", owner };
215
+ if (owner.npmPrefix === candidate.npmPrefix)
216
+ return { resource: "npmPrefix", owner };
217
+ }
218
+ return null;
219
+ }
220
+ export async function withManagedServiceRegistryLock(options, operation) {
221
+ const userHome = options.userHome ?? homedir();
222
+ const directory = registryDirectory(userHome);
223
+ const release = await acquireProfileSaveLock(directory, {
224
+ ...(options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs }),
225
+ ...(options.lockRetryMs === undefined ? {} : { retryMs: options.lockRetryMs }),
226
+ });
227
+ let operationError;
228
+ try {
229
+ const path = managedServiceRegistryPath(userHome);
230
+ const services = await readRegistryFile(path);
231
+ return await operation({
232
+ services,
233
+ replace: async (replacement) => {
234
+ const normalized = replacement.map(normalizeManagedServiceRecord);
235
+ for (let index = 0; index < normalized.length; index += 1) {
236
+ const candidate = normalized[index];
237
+ const conflict = managedServiceConflict(normalized.slice(0, index), candidate);
238
+ if (conflict !== null) {
239
+ throw new ManagedServiceConflictError(conflict.resource, candidate, conflict.owner);
240
+ }
241
+ }
242
+ await writeRegistryFile(path, normalized);
243
+ },
244
+ });
245
+ }
246
+ catch (error) {
247
+ operationError = error;
248
+ throw error;
249
+ }
250
+ finally {
251
+ try {
252
+ await release();
253
+ }
254
+ catch (releaseError) {
255
+ if (operationError === undefined)
256
+ throw releaseError;
257
+ }
258
+ }
259
+ }
260
+ export async function readManagedServiceRegistry(options = {}) {
261
+ return readRegistryFile(managedServiceRegistryPath(options.userHome ?? homedir()));
262
+ }
263
+ export async function registerManagedService(input, options = {}) {
264
+ const candidate = normalizeManagedServiceRecord(input);
265
+ await withManagedServiceRegistryLock(options, async ({ services, replace }) => {
266
+ const conflict = managedServiceConflict(services, candidate);
267
+ if (conflict !== null)
268
+ throw new ManagedServiceConflictError(conflict.resource, candidate, conflict.owner);
269
+ await replace([...services, candidate].sort((left, right) => left.serviceId.localeCompare(right.serviceId)));
270
+ });
271
+ }
272
+ export async function unregisterManagedService(input, options = {}) {
273
+ const candidate = normalizeManagedServiceRecord(input);
274
+ await withManagedServiceRegistryLock(options, async ({ services, replace }) => {
275
+ const index = services.findIndex((record) => record.serviceId === candidate.serviceId
276
+ && record.daemonHome === candidate.daemonHome
277
+ && record.profile === candidate.profile);
278
+ if (index < 0)
279
+ throw new ManagedServiceRegistryError(`Managed service '${candidate.serviceId}' is not registered`);
280
+ if (!sameRecord(services[index], candidate)) {
281
+ throw new ManagedServiceRegistryError(`Managed service '${candidate.serviceId}' identity changed before removal`);
282
+ }
283
+ await replace(services.filter((_, candidateIndex) => candidateIndex !== index));
284
+ });
285
+ }
@@ -0,0 +1,86 @@
1
+ import { homedir } from "node:os";
2
+ import { resolve } from "node:path";
3
+ import { buildServiceSpec, readServiceDescriptor } from "./computer-service.js";
4
+ import { daemonGlobalInstallation } from "./daemon-installation.js";
5
+ import { ManagedServiceRegistryError, assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
6
+ export async function assertManagedServiceStartup(input) {
7
+ if (input.platform !== "darwin" && input.platform !== "linux")
8
+ return;
9
+ const spec = input.spec ?? buildServiceSpec({
10
+ platform: input.platform,
11
+ profile: input.profile,
12
+ userHome: input.userHome,
13
+ uid: input.uid,
14
+ nodePath: input.nodePath,
15
+ entryPath: input.entryPath,
16
+ profileHome: input.daemonHome,
17
+ });
18
+ const readRegistry = input.readManagedServices
19
+ ?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
20
+ const listDescriptors = input.listManagedDescriptorPaths
21
+ ?? (() => listManagedServiceDescriptorPaths(input.platform, input.userHome));
22
+ const readDescriptor = input.readServiceDescriptor ?? readServiceDescriptor;
23
+ const [services, descriptorPaths, descriptor] = await Promise.all([
24
+ readRegistry(),
25
+ listDescriptors(),
26
+ readDescriptor(spec),
27
+ ]);
28
+ const registered = services.find((record) => record.serviceId === spec.id
29
+ || (record.daemonHome === input.daemonHome && record.profile === input.profile));
30
+ const defaultDaemonHome = resolve(input.userHome, ".crew", "daemon");
31
+ if (registered === undefined
32
+ && resolve(input.daemonHome) === defaultDaemonHome
33
+ && descriptor === spec.descriptor) {
34
+ return;
35
+ }
36
+ if (registered === undefined && descriptor === null) {
37
+ const legacySpec = buildServiceSpec({
38
+ platform: input.platform,
39
+ profile: input.profile,
40
+ userHome: input.userHome,
41
+ uid: input.uid,
42
+ nodePath: input.nodePath,
43
+ entryPath: input.entryPath,
44
+ profileHome: input.daemonHome,
45
+ legacyServiceId: true,
46
+ });
47
+ const legacyDescriptor = await readDescriptor(legacySpec);
48
+ if (legacyDescriptor === legacySpec.descriptor)
49
+ return;
50
+ if (descriptorPaths.length === 0)
51
+ return;
52
+ }
53
+ if (registered === undefined) {
54
+ throw new ManagedServiceRegistryError(`Managed service '${spec.id}' is installed but not registered`);
55
+ }
56
+ assertRegistryCoversDescriptors(services, descriptorPaths);
57
+ const installation = daemonGlobalInstallation(input.entryPath, input.platform);
58
+ if (installation === null || descriptor === null || spec.descriptorPath === null || spec.descriptor === null
59
+ || descriptor !== spec.descriptor
60
+ || !managedServiceIdentityMatches(registered, {
61
+ version: 1,
62
+ platform: input.platform,
63
+ serviceId: spec.id,
64
+ profile: input.profile,
65
+ daemonHome: input.daemonHome,
66
+ agentsRoot: input.agentsRoot,
67
+ nodePath: input.nodePath,
68
+ entryPath: input.entryPath,
69
+ packageRoot: installation.packageRoot,
70
+ npmPrefix: installation.npmPrefix,
71
+ descriptorPath: spec.descriptorPath,
72
+ descriptorSha256: serviceDescriptorSha256(descriptor),
73
+ })) {
74
+ throw new ManagedServiceRegistryError(`Managed service '${spec.id}' identity does not match its runtime`);
75
+ }
76
+ }
77
+ export function defaultManagedServiceStartupInput(input) {
78
+ return {
79
+ ...input,
80
+ platform: process.platform,
81
+ userHome: homedir(),
82
+ uid: process.getuid?.(),
83
+ nodePath: process.execPath,
84
+ entryPath: process.argv[1] ?? "",
85
+ };
86
+ }
@@ -0,0 +1,57 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { stat } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ const TRACE_PATTERN = /\[memory-prune trace_id=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]/i;
6
+ export const MEMORY_PRUNE_HASH_LIMIT_BYTES = 10 * 1024 * 1024;
7
+ export function parseMemoryPruneTraceId(wakePrompt) {
8
+ return TRACE_PATTERN.exec(wakePrompt)?.[1] ?? null;
9
+ }
10
+ async function inspectFile(path) {
11
+ let metadata;
12
+ try {
13
+ metadata = await stat(path);
14
+ }
15
+ catch (error) {
16
+ const code = error.code;
17
+ if (code === "ENOENT")
18
+ return { exists: false };
19
+ return { exists: false, error: code ?? (error instanceof Error ? error.name : "unknown") };
20
+ }
21
+ const fact = { exists: true, size: metadata.size, mtime_ms: metadata.mtimeMs };
22
+ if (!metadata.isFile())
23
+ return { ...fact, hash_skipped_reason: "not_regular_file" };
24
+ if (metadata.size > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
25
+ return { ...fact, hash_skipped_reason: "file_too_large" };
26
+ }
27
+ try {
28
+ const hash = createHash("sha256");
29
+ let bytesRead = 0;
30
+ const stream = createReadStream(path);
31
+ try {
32
+ for await (const chunk of stream) {
33
+ bytesRead += chunk.length;
34
+ if (bytesRead > MEMORY_PRUNE_HASH_LIMIT_BYTES) {
35
+ return { ...fact, hash_skipped_reason: "grew_too_large" };
36
+ }
37
+ hash.update(chunk);
38
+ }
39
+ }
40
+ finally {
41
+ stream.destroy();
42
+ }
43
+ return { ...fact, sha256: hash.digest("hex") };
44
+ }
45
+ catch (error) {
46
+ const code = error.code;
47
+ return { ...fact, error: code ?? (error instanceof Error ? error.name : "unknown") };
48
+ }
49
+ }
50
+ export async function inspectMemoryPruneFiles(homeDir, workLogPath) {
51
+ const [memory, lessons, workLog] = await Promise.all([
52
+ inspectFile(join(homeDir, "MEMORY.md")),
53
+ inspectFile(join(homeDir, "notes", "lessons.md")),
54
+ inspectFile(workLogPath),
55
+ ]);
56
+ return { memory, lessons, work_log: workLog };
57
+ }