@nowcrew/daemon 0.6.57 → 0.6.60

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.
@@ -82,6 +82,14 @@ function translatedError(error, td) {
82
82
  }
83
83
  return td(error.message);
84
84
  }
85
+ function storageServiceEnvironment(env) {
86
+ return {
87
+ ...(env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: env.NOWWORK_HOME }),
88
+ ...(env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: env.NOWWORK_ENV_TAG }),
89
+ ...(env.NOWWORK_STORAGE_POLICY_PATH === undefined
90
+ ? {} : { NOWWORK_STORAGE_POLICY_PATH: env.NOWWORK_STORAGE_POLICY_PATH }),
91
+ };
92
+ }
85
93
  export async function runComputerCommand(argv, overrides = {}) {
86
94
  const command = argv[0];
87
95
  if (!command || !COMPUTER_COMMANDS.has(command))
@@ -202,6 +210,7 @@ export async function runComputerCommand(argv, overrides = {}) {
202
210
  ...deps,
203
211
  profile: restartProfile.name,
204
212
  profileHome: home,
213
+ environment: storageServiceEnvironment(deps.env),
205
214
  })
206
215
  : null;
207
216
  if (spec && restartProfile) {
@@ -272,7 +281,7 @@ export async function runComputerCommand(argv, overrides = {}) {
272
281
  throw new Error(td("Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry"));
273
282
  }
274
283
  const spec = buildServiceSpec({
275
- ...deps, profile: name, profileHome: home,
284
+ ...deps, profile: name, profileHome: home, environment: storageServiceEnvironment(deps.env),
276
285
  });
277
286
  const installation = daemonGlobalInstallation(deps.entryPath, deps.platform);
278
287
  const managedRecord = installation !== null && spec.platform !== "win32"
@@ -53,6 +53,7 @@ export function windowsCommandArg(value) {
53
53
  }
54
54
  export function buildServiceSpec(input) {
55
55
  const { platform, profile, userHome } = input;
56
+ const environment = input.environment ?? process.env;
56
57
  if (platform !== "darwin" && platform !== "linux" && platform !== "win32") {
57
58
  throw new Error(`Computer services are not supported on platform '${platform}'`);
58
59
  }
@@ -72,6 +73,16 @@ export function buildServiceSpec(input) {
72
73
  throw new Error("Cannot install launchd service without a numeric uid");
73
74
  const id = `com.nowcrew.daemon.${profile}${identitySuffix}`;
74
75
  const args = daemonCommand.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
76
+ const environmentEntries = [
77
+ ["NOWWORK_HOME", environment.NOWWORK_HOME],
78
+ ["NOWWORK_ENV_TAG", environment.NOWWORK_ENV_TAG],
79
+ ["NOWWORK_STORAGE_POLICY_PATH", environment.NOWWORK_STORAGE_POLICY_PATH],
80
+ ].flatMap(([key, value]) => value === undefined
81
+ ? []
82
+ : [` <key>${key}</key><string>${xml(assertSingleLine(value, key))}</string>`]);
83
+ const environmentBlock = environmentEntries.length === 0
84
+ ? ""
85
+ : ` <key>EnvironmentVariables</key>\n <dict>\n${environmentEntries.join("\n")}\n </dict>\n`;
75
86
  return {
76
87
  platform,
77
88
  profile,
@@ -79,7 +90,7 @@ export function buildServiceSpec(input) {
79
90
  descriptorPath: resolve(userHome, "Library", "LaunchAgents", `${id}.plist`),
80
91
  daemonCommand,
81
92
  managerDomain: `gui/${input.uid}`,
82
- descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
93
+ descriptor: `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(id)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n${environmentBlock} <key>KeepAlive</key><true/>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`,
83
94
  };
84
95
  }
85
96
  if (platform === "linux") {
@@ -266,6 +277,35 @@ export async function installService(spec, runner = systemCommandRunner) {
266
277
  throw error;
267
278
  }
268
279
  }
280
+ export async function replaceInstalledServiceDescriptor(spec, previousDescriptor, runner = systemCommandRunner) {
281
+ if (spec.platform === "win32" || spec.descriptorPath === null || spec.descriptor === null) {
282
+ throw new Error("Installed service descriptor replacement requires launchd or systemd");
283
+ }
284
+ const current = await readServiceDescriptor(spec);
285
+ if (current !== previousDescriptor) {
286
+ throw new Error(`Installed descriptor for '${spec.id}' changed before replacement`);
287
+ }
288
+ const status = await serviceStatus(spec, runner);
289
+ if (!status.installed)
290
+ throw new Error(`Service '${spec.id}' is not installed`);
291
+ if (status.running || status.loaded)
292
+ await serviceAction(spec, "stop", runner);
293
+ try {
294
+ await writePrivateAtomic(spec.descriptorPath, spec.descriptor);
295
+ if (spec.platform === "linux") {
296
+ await checked(runner, "systemctl", ["--user", "daemon-reload"], "systemd reload");
297
+ await checked(runner, "systemctl", ["--user", "enable", spec.id], "systemd enable");
298
+ }
299
+ if (status.running)
300
+ await serviceAction(spec, "start", runner);
301
+ }
302
+ catch (error) {
303
+ await writePrivateAtomic(spec.descriptorPath, previousDescriptor).catch(() => undefined);
304
+ if (status.running)
305
+ await serviceAction(spec, "start", runner).catch(() => undefined);
306
+ throw error;
307
+ }
308
+ }
269
309
  export async function ensureServiceEnabled(spec, runner = systemCommandRunner) {
270
310
  if (spec.platform === "linux") {
271
311
  if (spec.descriptorPath === null || !await pathExists(spec.descriptorPath)) {
package/dist/main.js CHANGED
@@ -85,6 +85,12 @@ async function main() {
85
85
  agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
86
86
  nodePath: process.execPath,
87
87
  entryPath: builtDaemonEntry(),
88
+ environment: {
89
+ ...(process.env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: process.env.NOWWORK_HOME }),
90
+ ...(process.env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: process.env.NOWWORK_ENV_TAG }),
91
+ ...(process.env.NOWWORK_STORAGE_POLICY_PATH === undefined
92
+ ? {} : { NOWWORK_STORAGE_POLICY_PATH: process.env.NOWWORK_STORAGE_POLICY_PATH }),
93
+ },
88
94
  });
89
95
  }
90
96
  applyProfileToEnv(profile, process.env);
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { acquireDaemonInstallationLease } from "./daemon-installation-lease.js";
3
- import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, uninstallService, } from "./computer-service.js";
3
+ import { installService, ensureServiceEnabled, readServiceDescriptor, serviceAction, replaceInstalledServiceDescriptor, uninstallService, } from "./computer-service.js";
4
4
  import { ManagedServiceConflictError, ManagedServiceRegistryError, assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, managedServiceConflict, normalizeManagedServiceRecord, serviceDescriptorSha256, withManagedServiceRegistryLock, } from "./managed-service-registry.js";
5
5
  function requirePortableManagedSpec(spec) {
6
6
  if ((spec.platform !== "darwin" && spec.platform !== "linux")
@@ -35,6 +35,17 @@ function sameManagedIdentity(left, right) {
35
35
  const { generation: _generation, createdAt: _createdAt, ...expected } = right;
36
36
  return managedServiceIdentityMatches(left, expected);
37
37
  }
38
+ function sameManagedResource(left, right) {
39
+ const { generation: _leftGeneration, createdAt: _leftCreatedAt, descriptorSha256: _leftHash, ...leftResource } = left;
40
+ const { generation: _rightGeneration, createdAt: _rightCreatedAt, descriptorSha256: _rightHash, ...rightResource } = right;
41
+ return JSON.stringify(leftResource) === JSON.stringify(rightResource);
42
+ }
43
+ function descriptorDiffIsStorageEnvironmentOnly(previous, next) {
44
+ if (!next.includes("\n <key>EnvironmentVariables</key>\n"))
45
+ return false;
46
+ const strip = (descriptor) => descriptor.replace(/\n <key>EnvironmentVariables<\/key>\n <dict>\n(?: <key>(?:NOWWORK_HOME|NOWWORK_ENV_TAG|NOWWORK_STORAGE_POLICY_PATH)<\/key><string>[^<]*<\/string>\n)+ <\/dict>\n/u, "\n");
47
+ return strip(previous) === strip(next);
48
+ }
38
49
  function assertRecordDescribesSpec(input) {
39
50
  requirePortableManagedSpec(input.spec);
40
51
  const { spec, record } = input;
@@ -51,8 +62,20 @@ function assertRecordDescribesSpec(input) {
51
62
  }
52
63
  }
53
64
  export async function installManagedService(input) {
54
- assertRecordDescribesSpec(input);
55
65
  requirePortableManagedSpec(input.spec);
66
+ const requestedDescriptor = input.spec.descriptor;
67
+ const expectedDescriptorHash = serviceDescriptorSha256(input.spec.descriptor);
68
+ const expectedPath = normalizeManagedServiceRecord({
69
+ ...input.record,
70
+ descriptorPath: input.spec.descriptorPath,
71
+ descriptorSha256: expectedDescriptorHash,
72
+ }).descriptorPath;
73
+ if (input.record.platform !== input.spec.platform
74
+ || input.record.serviceId !== input.spec.id
75
+ || input.record.profile !== input.spec.profile
76
+ || input.record.descriptorPath !== expectedPath) {
77
+ throw new ManagedServiceRegistryError(`Managed service record does not describe '${input.spec.id}'`);
78
+ }
56
79
  const acquire = input.acquireInstallationLease ?? acquireDaemonInstallationLease;
57
80
  await withManagedServiceRegistryLock(input.registry ?? {}, async ({ services, replace }) => {
58
81
  const exact = services.find((record) => sameManagedIdentity(record, input.record));
@@ -76,6 +99,28 @@ export async function installManagedService(input) {
76
99
  }
77
100
  return;
78
101
  }
102
+ const registered = services.find((record) => record.serviceId === input.record.serviceId
103
+ && record.profile === input.record.profile
104
+ && record.daemonHome === input.record.daemonHome);
105
+ if (registered !== undefined && sameManagedResource(registered, input.record)) {
106
+ const current = await readServiceDescriptor(input.spec);
107
+ if (current === null || serviceDescriptorSha256(current) !== registered.descriptorSha256) {
108
+ throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' does not match its registry record`);
109
+ }
110
+ if (!descriptorDiffIsStorageEnvironmentOnly(current, requestedDescriptor)) {
111
+ throw new ManagedServiceRegistryError(`Installed descriptor for '${input.spec.id}' conflicts with the requested identity`);
112
+ }
113
+ await replaceInstalledServiceDescriptor(input.spec, current, input.runner);
114
+ try {
115
+ await replace(services.map((record) => record === registered ? input.record : record));
116
+ }
117
+ catch (error) {
118
+ const rollbackSpec = { ...input.spec, descriptor: current };
119
+ await replaceInstalledServiceDescriptor(rollbackSpec, requestedDescriptor, input.runner).catch(() => undefined);
120
+ throw error;
121
+ }
122
+ return;
123
+ }
79
124
  const conflict = managedServiceConflict(services, input.record);
80
125
  if (conflict !== null) {
81
126
  throw new ManagedServiceConflictError(conflict.resource, input.record, conflict.owner);
@@ -14,6 +14,7 @@ export async function assertManagedServiceStartup(input) {
14
14
  nodePath: input.nodePath,
15
15
  entryPath: input.entryPath,
16
16
  profileHome: input.daemonHome,
17
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
17
18
  });
18
19
  const readRegistry = input.readManagedServices
19
20
  ?? (() => readManagedServiceRegistry({ userHome: input.userHome }));
@@ -42,6 +43,7 @@ export async function assertManagedServiceStartup(input) {
42
43
  nodePath: input.nodePath,
43
44
  entryPath: input.entryPath,
44
45
  profileHome: input.daemonHome,
46
+ ...(input.environment === undefined ? {} : { environment: input.environment }),
45
47
  legacyServiceId: true,
46
48
  });
47
49
  const legacyDescriptor = await readDescriptor(legacySpec);
@@ -67,6 +67,7 @@ function targetRecord(input, workspaceId, createdAt) {
67
67
  nodePath: input.sourceRecord.nodePath,
68
68
  entryPath: input.sourceRecord.entryPath,
69
69
  profileHome: input.local.configRoot,
70
+ ...(input.serviceEnvironment === undefined ? {} : { environment: input.serviceEnvironment }),
70
71
  });
71
72
  return buildManagedServiceRecord({
72
73
  spec,
@@ -183,6 +184,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
183
184
  platform: input.platform,
184
185
  userHome: input.userHome,
185
186
  ...(input.uid === undefined ? {} : { uid: input.uid }),
187
+ ...(input.serviceEnvironment === undefined ? {} : { serviceEnvironment: input.serviceEnvironment }),
186
188
  sourceWasRunning: true,
187
189
  sourceRecord: input.sourceRecord,
188
190
  targetRecord: targetRecord(input, workspaceId, createdAt),
@@ -236,6 +238,7 @@ export function createStorageMigrationRuntimeController(rawInput, injected = {})
236
238
  migrationId: operation.migrationId,
237
239
  platform: input.platform,
238
240
  userHome: input.userHome,
241
+ ...(input.uid === undefined ? {} : { uid: input.uid }),
239
242
  });
240
243
  workerLaunched = true;
241
244
  const outcome = await dependencies.monitorWorker({
@@ -58,9 +58,12 @@ function findSourceRecord(records, input) {
58
58
  return matches.length === 1 ? matches[0] : null;
59
59
  }
60
60
  export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
61
- if (rawInput.platform !== "linux") {
61
+ if (rawInput.platform !== "darwin" && rawInput.platform !== "linux") {
62
62
  return notReady("platform_not_supported");
63
63
  }
64
+ if (rawInput.platform === "darwin" && rawInput.uid === undefined) {
65
+ return notReady("platform_identity_missing");
66
+ }
64
67
  const configuredNowworkHome = rawInput.environment.NOWWORK_HOME;
65
68
  if (configuredNowworkHome === undefined)
66
69
  return notReady("nowwork_home_missing");
@@ -124,6 +127,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
124
127
  nodePath: sourceRecord.nodePath,
125
128
  entryPath: sourceRecord.entryPath,
126
129
  profileHome: input.currentDaemonHome,
130
+ environment: input.environment,
127
131
  });
128
132
  const assertSourceIdentity = () => dependencies.assertManagedServiceIdentity({
129
133
  spec: sourceSpec,
@@ -165,6 +169,11 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
165
169
  sourceRecord,
166
170
  local,
167
171
  policy,
172
+ serviceEnvironment: {
173
+ NOWWORK_HOME: nowworkHome,
174
+ NOWWORK_ENV_TAG: environment,
175
+ NOWWORK_STORAGE_POLICY_PATH: policyPath,
176
+ },
168
177
  backupArtifacts: [
169
178
  { name: "profile.json", path: profilePath(input.profile, input.currentDaemonHome) },
170
179
  { name: "service-descriptor", path: sourceRecord.descriptorPath },
@@ -35,6 +35,9 @@ function serviceSpec(operation, record, profileHome) {
35
35
  nodePath: record.nodePath,
36
36
  entryPath: record.entryPath,
37
37
  profileHome,
38
+ ...(operation.service.serviceEnvironment === undefined
39
+ ? {}
40
+ : { environment: operation.service.serviceEnvironment }),
38
41
  });
39
42
  }
40
43
  function assertRecordMatchesSpec(record, spec) {
@@ -62,6 +62,16 @@ function workerUnitName(migrationId) {
62
62
  function workerUnitPath(userHome, migrationId) {
63
63
  return resolve(userHome, ".config", "systemd", "user", workerUnitName(migrationId));
64
64
  }
65
+ function workerLaunchdLabel(migrationId) {
66
+ return `com.nowcrew.storage-migration.${migrationId}`;
67
+ }
68
+ function workerLaunchdPath(userHome, migrationId) {
69
+ return resolve(userHome, "Library", "LaunchAgents", `${workerLaunchdLabel(migrationId)}.plist`);
70
+ }
71
+ function xml(value) {
72
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
73
+ .replaceAll('"', "&quot;").replaceAll("'", "&apos;");
74
+ }
65
75
  function systemdArg(value) {
66
76
  if (/[\0\r\n]/u.test(value))
67
77
  throw new Error("storage worker service argument must be a single line");
@@ -77,9 +87,26 @@ export async function launchStorageMigrationWorker(input, runSupervisor = system
77
87
  AbsolutePathSchema.parse(input.handoffPath);
78
88
  AbsolutePathSchema.parse(input.userHome);
79
89
  const migrationId = z.string().uuid().parse(input.migrationId).toLowerCase();
80
- if (input.platform !== "linux")
81
- throw new Error("Persistent storage migration workers require Linux systemd");
82
90
  const worker = [process.execPath, input.entryPath, "storage-migration-worker", "--job", input.handoffPath];
91
+ if (input.platform === "darwin") {
92
+ if (input.uid === undefined || !Number.isSafeInteger(input.uid) || input.uid < 0) {
93
+ throw new Error("Persistent storage migration launchd worker requires a numeric uid");
94
+ }
95
+ const label = workerLaunchdLabel(migrationId);
96
+ const path = workerLaunchdPath(input.userHome, migrationId);
97
+ const args = worker.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
98
+ const descriptor = `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key><string>${xml(label)}</string>\n <key>ProgramArguments</key>\n <array>\n${args}\n </array>\n <key>RunAtLoad</key><true/>\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key><false/>\n </dict>\n <key>ProcessType</key><string>Background</string>\n</dict>\n</plist>\n`;
99
+ await atomicPrivateWrite(path, descriptor);
100
+ try {
101
+ await checkedSupervisor(runSupervisor, "launchctl", ["bootstrap", `gui/${input.uid}`, path]);
102
+ }
103
+ catch (error) {
104
+ await runSupervisor("launchctl", ["bootout", `gui/${input.uid}/${label}`]).catch(() => undefined);
105
+ await rm(path, { force: true });
106
+ throw new Error("Storage migration persistent service launch failed", { cause: error });
107
+ }
108
+ return;
109
+ }
83
110
  const unit = workerUnitName(migrationId);
84
111
  const path = workerUnitPath(input.userHome, migrationId);
85
112
  const descriptor = `[Unit]\nDescription=NowWork storage migration ${migrationId}\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=exec\nExecStart=${worker.map(systemdArg).join(" ")}\nRestart=on-abnormal\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`;
@@ -97,9 +124,16 @@ export async function launchStorageMigrationWorker(input, runSupervisor = system
97
124
  }
98
125
  export async function completeStorageMigrationWorkerService(input, runSupervisor = systemSupervisorRun) {
99
126
  const migrationId = z.string().uuid().parse(input.migrationId).toLowerCase();
100
- if (input.platform !== "linux")
101
- return;
102
127
  AbsolutePathSchema.parse(input.userHome);
128
+ if (input.platform === "darwin") {
129
+ if (input.uid === undefined || !Number.isSafeInteger(input.uid) || input.uid < 0) {
130
+ throw new Error("Persistent storage migration launchd worker requires a numeric uid");
131
+ }
132
+ const label = workerLaunchdLabel(migrationId);
133
+ await rm(workerLaunchdPath(input.userHome, migrationId), { force: true });
134
+ await runSupervisor("launchctl", ["bootout", `gui/${input.uid}/${label}`]);
135
+ return;
136
+ }
103
137
  const unit = workerUnitName(migrationId);
104
138
  await checkedSupervisor(runSupervisor, "systemctl", ["--user", "disable", unit]);
105
139
  await rm(workerUnitPath(input.userHome, migrationId), { force: true });
@@ -112,5 +146,6 @@ export async function completeStorageMigrationWorkerServiceForHandoff(path) {
112
146
  migrationId: operation.migrationId,
113
147
  platform: operation.service.platform,
114
148
  userHome: operation.service.userHome,
149
+ ...(operation.service.uid === undefined ? {} : { uid: operation.service.uid }),
115
150
  });
116
151
  }
@@ -90,6 +90,11 @@ const InstallationRecordSchema = z.object({
90
90
  createdAt: z.string().datetime({ offset: true }),
91
91
  updatedAt: z.string().datetime({ offset: true }),
92
92
  }).strict();
93
+ const StorageServiceEnvironmentSchema = z.object({
94
+ NOWWORK_HOME: AbsolutePathSchema.optional(),
95
+ NOWWORK_ENV_TAG: z.string().regex(ENVIRONMENT).optional(),
96
+ NOWWORK_STORAGE_POLICY_PATH: AbsolutePathSchema.optional(),
97
+ }).strict();
93
98
  const OperationSchema = z.object({
94
99
  version: z.literal(1),
95
100
  migrationId: z.string().uuid(),
@@ -119,6 +124,7 @@ const OperationSchema = z.object({
119
124
  platform: z.enum(["darwin", "linux"]),
120
125
  userHome: AbsolutePathSchema,
121
126
  uid: z.number().int().nonnegative().optional(),
127
+ serviceEnvironment: StorageServiceEnvironmentSchema.optional(),
122
128
  sourceWasRunning: z.boolean(),
123
129
  sourceRecord: ManagedServiceRecordSchema,
124
130
  targetRecord: ManagedServiceRecordSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.57",
3
+ "version": "0.6.60",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",