@nowcrew/daemon 0.6.58 → 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),
@@ -127,6 +127,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
127
127
  nodePath: sourceRecord.nodePath,
128
128
  entryPath: sourceRecord.entryPath,
129
129
  profileHome: input.currentDaemonHome,
130
+ environment: input.environment,
130
131
  });
131
132
  const assertSourceIdentity = () => dependencies.assertManagedServiceIdentity({
132
133
  spec: sourceSpec,
@@ -168,6 +169,11 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
168
169
  sourceRecord,
169
170
  local,
170
171
  policy,
172
+ serviceEnvironment: {
173
+ NOWWORK_HOME: nowworkHome,
174
+ NOWWORK_ENV_TAG: environment,
175
+ NOWWORK_STORAGE_POLICY_PATH: policyPath,
176
+ },
171
177
  backupArtifacts: [
172
178
  { name: "profile.json", path: profilePath(input.profile, input.currentDaemonHome) },
173
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) {
@@ -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.58",
3
+ "version": "0.6.60",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",