@nowcrew/daemon 0.6.80 → 0.6.82

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.
@@ -29,6 +29,14 @@ function profileStorage(platform, userHome, runner) {
29
29
  function sameHostname(left, right) {
30
30
  return new URL(left).hostname.toLowerCase() === new URL(right).hostname.toLowerCase();
31
31
  }
32
+ function localHomeForTarget(target, platform) {
33
+ const paths = platform === "win32" ? win32 : posix;
34
+ return localHomeForNpmPrefix(target.npmPrefix, platform);
35
+ }
36
+ function localHomeForNpmPrefix(npmPrefix, platform) {
37
+ const paths = platform === "win32" ? win32 : posix;
38
+ return paths.resolve(npmPrefix, "..", "..");
39
+ }
32
40
  function defaultDependencies() {
33
41
  const platform = process.platform;
34
42
  const userHome = homedir();
@@ -61,7 +69,7 @@ function defaultDependencies() {
61
69
  },
62
70
  captureService: async (spec, target) => {
63
71
  if (platform !== "win32") {
64
- managedRecords = await readManagedServiceRegistry({ userHome, localHome });
72
+ managedRecords = await readManagedServiceRegistry({ userHome, localHome: localHomeForTarget(target, platform) });
65
73
  const entryPath = spec.daemonCommand[1];
66
74
  const installation = daemonGlobalInstallation(entryPath, platform);
67
75
  if (installation === null) {
@@ -95,7 +103,10 @@ function defaultDependencies() {
95
103
  managedServices: managedRecords,
96
104
  }),
97
105
  stopService: (spec) => serviceAction(spec, "stop", runner),
98
- acquireInstallationLease: (npmPrefix) => acquireDaemonInstallationLease(npmPrefix, { userHome, localHome }),
106
+ acquireInstallationLease: (npmPrefix, options) => acquireDaemonInstallationLease(npmPrefix, {
107
+ userHome,
108
+ localHome: options?.localHome ?? localHome,
109
+ }),
99
110
  installPackage: installExactDaemonPackage,
100
111
  saveTargetProfile: async (profile, daemonHome) => {
101
112
  await saveProfile(profile, daemonHome, storage);
@@ -149,13 +160,16 @@ function defaultDependencies() {
149
160
  await restoreExactService(spec, previous, runner);
150
161
  return;
151
162
  }
152
- const current = (await readManagedServiceRegistry({ userHome, localHome })).find((record) => record.serviceId === spec.id);
163
+ const restoreLocalHome = previousManagedRecord === undefined
164
+ ? localHome
165
+ : localHomeForNpmPrefix(previousManagedRecord.npmPrefix, platform);
166
+ const current = (await readManagedServiceRegistry({ userHome, localHome: restoreLocalHome })).find((record) => record.serviceId === spec.id);
153
167
  if (current !== undefined && JSON.stringify(current) !== JSON.stringify(previousManagedRecord)) {
154
168
  await uninstallManagedService({
155
169
  spec,
156
170
  record: current,
157
171
  runner,
158
- registry: { userHome, localHome },
172
+ registry: { userHome, localHome: restoreLocalHome },
159
173
  });
160
174
  }
161
175
  if (previousManagedRecord !== undefined) {
@@ -163,7 +177,7 @@ function defaultDependencies() {
163
177
  spec,
164
178
  record: previousManagedRecord,
165
179
  runner,
166
- registry: { userHome, localHome },
180
+ registry: { userHome, localHome: restoreLocalHome },
167
181
  });
168
182
  if (previous.running)
169
183
  await serviceAction(spec, "start", runner);
@@ -283,7 +297,9 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
283
297
  stopped = true;
284
298
  await dependencies.stopService(spec);
285
299
  }
286
- const lease = await dependencies.acquireInstallationLease(target.npmPrefix);
300
+ const lease = await dependencies.acquireInstallationLease(target.npmPrefix, {
301
+ localHome: localHomeForTarget(target, dependencies.platform),
302
+ });
287
303
  let installFailure;
288
304
  try {
289
305
  await dependencies.installPackage(target.npmPrefix, dependencies.version, dependencies.platform, dependencies.runner, target.registryUrl);
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
3
3
  import { posix, resolve, win32 } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { assertProfileAgentsRootUnique, daemonHome, inspectProfileAgentsRoot, listProfiles, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, profileIsPrivate, profilePath, publicProfile, removeProfile, resolveAgentsRoot, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
6
- import { buildServiceSpec, doctorService, hardenWindowsProfile, installService, readServiceDescriptor, serviceAction, serviceStatus, systemCommandRunner, uninstallService, upgradeDaemon, windowsProfileIsPrivate, } from "./computer-service.js";
6
+ import { buildServiceSpec, doctorService, hardenWindowsProfile, installService, readServiceDescriptor, serviceAction, serviceStatus, systemCommandRunner, uninstallService, upgradeDaemon, windowsProfileIsPrivate, storageServiceEnvironment, } from "./computer-service.js";
7
7
  import { releaseTagForServerUrl, assertReleaseTagForServerUrl } from "./automatic-install-target.js";
8
8
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
9
9
  import { npmInvocation } from "./npm-command.js";
@@ -89,17 +89,6 @@ function translatedError(error, td) {
89
89
  }
90
90
  return td(error.message);
91
91
  }
92
- function storageServiceEnvironment(env) {
93
- return {
94
- ...(env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: env.NOWWORK_HOME }),
95
- ...(env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: env.NOWWORK_ENV_TAG }),
96
- ...(env.NOWWORK_STORAGE_POLICY_PATH === undefined
97
- ? {} : { NOWWORK_STORAGE_POLICY_PATH: env.NOWWORK_STORAGE_POLICY_PATH }),
98
- ...(env.NOWWORK_LOCAL_HOME === undefined ? {} : { NOWWORK_LOCAL_HOME: env.NOWWORK_LOCAL_HOME }),
99
- ...(env.NOWWORK_LOCAL_HOME_MODE === undefined && env.NOWWORK_STORAGE_MODE === undefined
100
- ? {} : { NOWWORK_LOCAL_HOME_MODE: env.NOWWORK_LOCAL_HOME_MODE ?? env.NOWWORK_STORAGE_MODE }),
101
- };
102
- }
103
92
  export async function runComputerCommand(argv, overrides = {}) {
104
93
  const command = argv[0];
105
94
  if (!command || !COMPUTER_COMMANDS.has(command))
@@ -195,6 +184,8 @@ export async function runComputerCommand(argv, overrides = {}) {
195
184
  platform: deps.platform,
196
185
  explicit: deps.env.NOWWORK_LOCAL_HOME,
197
186
  mode: configuredLocalHomeMode,
187
+ ...(resolvedProfile === undefined ? {} : { profile: resolvedProfile }),
188
+ ...(home === undefined ? {} : { daemonHome: home }),
198
189
  });
199
190
  const profileStorage = {
200
191
  platform: deps.platform,
@@ -32,6 +32,18 @@ export const systemCommandRunner = async (command, args) => {
32
32
  };
33
33
  }
34
34
  };
35
+ /** Build the non-secret storage environment persisted in managed services. */
36
+ export function storageServiceEnvironment(env) {
37
+ return {
38
+ ...(env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: env.NOWWORK_HOME }),
39
+ ...(env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: env.NOWWORK_ENV_TAG }),
40
+ ...(env.NOWWORK_STORAGE_POLICY_PATH === undefined
41
+ ? {} : { NOWWORK_STORAGE_POLICY_PATH: env.NOWWORK_STORAGE_POLICY_PATH }),
42
+ ...(env.NOWWORK_LOCAL_HOME === undefined ? {} : { NOWWORK_LOCAL_HOME: env.NOWWORK_LOCAL_HOME }),
43
+ ...(env.NOWWORK_LOCAL_HOME_MODE === undefined && env.NOWWORK_STORAGE_MODE === undefined
44
+ ? {} : { NOWWORK_LOCAL_HOME_MODE: env.NOWWORK_LOCAL_HOME_MODE ?? env.NOWWORK_STORAGE_MODE }),
45
+ };
46
+ }
35
47
  function serviceIdentitySuffix(profileHome, userHome) {
36
48
  if (profileHome === undefined || resolve(profileHome) === resolve(userHome, ".crew", "daemon"))
37
49
  return "";
@@ -50,6 +62,9 @@ function xml(value) {
50
62
  function systemdArg(value) {
51
63
  return `"${assertSingleLine(value, "service argument").replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
52
64
  }
65
+ function systemdEnvironmentEntry(key, value) {
66
+ return `Environment=${systemdArg(`${key}=${value}`)}`;
67
+ }
53
68
  export function windowsCommandArg(value) {
54
69
  const input = assertSingleLine(value, "service argument");
55
70
  return `"${input.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\*)$/g, "$1$1")}"`;
@@ -100,6 +115,14 @@ export function buildServiceSpec(input) {
100
115
  }
101
116
  if (platform === "linux") {
102
117
  const id = `nowcrew-daemon-${profile}${identitySuffix}.service`;
118
+ const environmentEntries = [
119
+ ["NOWWORK_HOME", environment.NOWWORK_HOME],
120
+ ["NOWWORK_ENV_TAG", environment.NOWWORK_ENV_TAG],
121
+ ["NOWWORK_STORAGE_POLICY_PATH", environment.NOWWORK_STORAGE_POLICY_PATH],
122
+ ["NOWWORK_LOCAL_HOME", environment.NOWWORK_LOCAL_HOME],
123
+ ["NOWWORK_LOCAL_HOME_MODE", environment.NOWWORK_LOCAL_HOME_MODE],
124
+ ].flatMap(([key, value]) => value === undefined ? [] : [systemdEnvironmentEntry(key, value)]);
125
+ const environmentBlock = environmentEntries.length === 0 ? "" : `${environmentEntries.join("\n")}\n`;
103
126
  return {
104
127
  platform,
105
128
  profile,
@@ -107,7 +130,7 @@ export function buildServiceSpec(input) {
107
130
  descriptorPath: resolve(userHome, ".config", "systemd", "user", id),
108
131
  daemonCommand,
109
132
  managerDomain: null,
110
- descriptor: `[Unit]\nDescription=NowCrew daemon (${profile})\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${daemonCommand.map(systemdArg).join(" ")}\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`,
133
+ descriptor: `[Unit]\nDescription=NowCrew daemon (${profile})\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\n${environmentBlock}ExecStart=${daemonCommand.map(systemdArg).join(" ")}\nRestart=on-failure\nRestartSec=5\n\n[Install]\nWantedBy=default.target\n`,
111
134
  };
112
135
  }
113
136
  const id = `NowCrew Daemon (${profile}${identitySuffix})`;
@@ -4,6 +4,13 @@ import { posix, win32 } from "node:path";
4
4
  const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
5
5
  const CONTROL = /[\0\r\n]/u;
6
6
  const pathsFor = (platform) => platform === "win32" ? win32 : posix;
7
+ function samePath(left, right, platform) {
8
+ const normalizedLeft = pathsFor(platform).resolve(left);
9
+ const normalizedRight = pathsFor(platform).resolve(right);
10
+ return platform === "win32"
11
+ ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
12
+ : normalizedLeft === normalizedRight;
13
+ }
7
14
  export function parseDaemonLocalHomeMode(value) {
8
15
  if (value === undefined || value.trim() === "")
9
16
  return "auto";
@@ -86,6 +93,22 @@ export function resolveDaemonLocalStateRoot(input) {
86
93
  return legacyRoot;
87
94
  if (input.explicit !== undefined || mode === "nowwork")
88
95
  return nowworkRoot;
96
+ // Managed service descriptors created before local-home was persisted carry
97
+ // only --daemon-home. Do not treat every --daemon-home as legacy: migrated
98
+ // services also use that argument, but point it at ~/.nowwork/config.
99
+ // Prefer the descriptor's exact home when it identifies either layout.
100
+ if (input.daemonHome !== undefined && input.profile !== undefined) {
101
+ const nowworkDaemonHome = resolution("nowwork", nowworkRoot, input.profile, input.platform, "auto_nowwork_existing").daemonHome;
102
+ const legacyDaemonHome = resolution("legacy", legacyRoot, input.profile, input.platform, "auto_legacy_existing").daemonHome;
103
+ if (samePath(input.daemonHome, nowworkDaemonHome, input.platform))
104
+ return nowworkRoot;
105
+ if (samePath(input.daemonHome, legacyDaemonHome, input.platform))
106
+ return legacyRoot;
107
+ // An arbitrary explicit daemon home has historically implied the legacy
108
+ // local state root. Preserve that behavior unless the descriptor declares
109
+ // a mode explicitly.
110
+ return legacyRoot;
111
+ }
89
112
  const exists = input.exists ?? existsSync;
90
113
  // Once the new root exists, shared state must follow it even while the
91
114
  // legacy root is retained as a rollback source.
@@ -133,9 +133,9 @@ export function createDaemonMigrationController(input) {
133
133
  requestedProfile: message.targetProfile,
134
134
  userHome,
135
135
  platform,
136
- // This controller migrates the legacy service layout; the new local-home
137
- // standard is handled by the storage migration controller.
138
- requestedLocalHomeMode: "legacy",
136
+ // Layout migration creates the canonical service in the new local-home
137
+ // root. Source registry and rollback material remain under legacy .crew.
138
+ requestedLocalHomeMode: "nowwork",
139
139
  });
140
140
  if (!isAbsolute(message.targetDaemonHome) || !isAbsolute(message.targetAgentsRoot)
141
141
  || resolve(message.targetDaemonHome) !== resolve(target.daemonHome)
@@ -178,6 +178,7 @@ export function createDaemonMigrationController(input) {
178
178
  serverUrl: input.serverUrl,
179
179
  machineToken: input.machineToken,
180
180
  requestedProfile: message.targetProfile,
181
+ requestedLocalHomeMode: "nowwork",
181
182
  ...(currentRegistryUrl === undefined ? {} : { requestedRegistryUrl: currentRegistryUrl }),
182
183
  });
183
184
  if (result.profile !== message.targetProfile || result.serviceId.length === 0) {
@@ -4,7 +4,7 @@ import { realpathSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { daemonHome, loadProfile, resolveAgentsRoot } from "./computer-profile.js";
6
6
  import { builtDaemonEntry } from "./computer-cli.js";
7
- import { buildServiceSpec, readServiceDescriptor, serviceStatus, } from "./computer-service.js";
7
+ import { buildServiceSpec, readServiceDescriptor, storageServiceEnvironment, serviceStatus, } from "./computer-service.js";
8
8
  import { daemonGlobalInstallation } from "./daemon-installation.js";
9
9
  import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
10
10
  import { inspectLegacyServiceInstallation } from "./legacy-service-installation.js";
@@ -21,9 +21,9 @@ export async function managedDaemonCapabilities(eligibility) {
21
21
  return [];
22
22
  }
23
23
  }
24
- function defaults(profileName) {
24
+ function defaults(profileName, selectedLocalHome) {
25
25
  const userHome = homedir();
26
- const localHome = resolveDaemonLocalStateRoot({
26
+ const localHome = selectedLocalHome ?? resolveDaemonLocalStateRoot({
27
27
  userHome,
28
28
  platform: process.platform,
29
29
  explicit: process.env.NOWWORK_LOCAL_HOME,
@@ -31,6 +31,7 @@ function defaults(profileName) {
31
31
  });
32
32
  return {
33
33
  platform: process.platform,
34
+ localHome,
34
35
  profileHome: daemonHome(process.env, profileName),
35
36
  userHome,
36
37
  uid: process.getuid?.(),
@@ -65,7 +66,7 @@ async function registryCoversUpdateScope(services, descriptorPaths, targetDescri
65
66
  return true;
66
67
  }
67
68
  export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
68
- const deps = { ...defaults(profileName), ...overrides };
69
+ const deps = { ...defaults(profileName, overrides.localHome), ...overrides };
69
70
  if (deps.platform !== "darwin" && deps.platform !== "linux" && deps.platform !== "win32") {
70
71
  return { eligible: false, reason: "unsupported_platform" };
71
72
  }
@@ -92,6 +93,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
92
93
  nodePath: deps.nodePath,
93
94
  entryPath: deps.entryPath,
94
95
  profileHome: deps.profileHome,
96
+ environment: storageServiceEnvironment(process.env),
95
97
  });
96
98
  if (deps.platform === "win32") {
97
99
  const scope = await deps.inspectWindowsServiceUpdateScope(spec, installation.npmPrefix)
@@ -169,6 +171,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
169
171
  entryPath: deps.entryPath,
170
172
  profileHome: deps.profileHome,
171
173
  legacyServiceId: true,
174
+ environment: storageServiceEnvironment(process.env),
172
175
  });
173
176
  const legacyDescriptor = await deps.readServiceDescriptor(legacySpec).catch(() => null);
174
177
  if (legacyDescriptor !== legacySpec.descriptor) {
@@ -9,7 +9,7 @@ import { DEEPSEEK_CODEX_MODEL, DEEPSEEK_CODEX_REASONING_LEVELS, materializeDeepS
9
9
  import { materializeDefaultCodexHome } from "./runtimes/codex-home.js";
10
10
  import { materializePrivateCodexHome } from "./codex-home-launch.js";
11
11
  import { spawnKimi, KIMI_EFFORT_LEVELS } from "./runtimes/kimi.js";
12
- import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, sanitizeEnvVars, } from "./provider-env.js";
12
+ import { applyDeepSeekHarnessMachineEnv, applyProviderEnv, providerFingerprint, } from "./provider-env.js";
13
13
  import { augmentedPath } from "./runtime-path.js";
14
14
  import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./normalize.js";
15
15
  import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
@@ -33,6 +33,8 @@ import { diffMemoryPruneNotes, evaluateMemoryPrunePostcondition, inspectMemoryPr
33
33
  import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
34
34
  import { CodexStartupStageParser } from "./codex-startup-stage.js";
35
35
  import { formatRuntimeStartupError } from "./runtime-startup-error.js";
36
+ import { machinePackageManagerEnv, packageManagerEnvFacts } from "./package-manager-env.js";
37
+ import { sanitizeEnvVars } from "./runtime-env.js";
36
38
  import { commitLocalMemoryResultAfterExit, initializeLocalMemoryV2ForWorkspace, isLocalMemoryV2Eligible, localMemoryResultPath as makeLocalMemoryResultPath, renderLocalMemoryResultInstructions, } from "./local-memory/runtime.js";
37
39
  export class RuntimeRunningCallbackTimeoutError extends Error {
38
40
  constructor(runtime, timeoutMs) {
@@ -50,15 +52,6 @@ const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
50
52
  const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
51
53
  const CLAUDE_INSTRUCTION_WARNING = "claude_additional_directory_instructions_unverified";
52
54
  const CODEX_COLLABORATION_EXEC_WARNING = "codex_collaboration_disabled_on_exec";
53
- const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
54
- const RESERVED_ENV = new Set([
55
- "PATH",
56
- "XDG_CONFIG_HOME",
57
- "XDG_DATA_HOME",
58
- "XDG_CACHE_HOME",
59
- "GH_CONFIG_DIR",
60
- "CLOUDSDK_CONFIG",
61
- ]);
62
55
  function withoutClaudeInstructionFlag(env) {
63
56
  if (!(CLAUDE_ADDITIONAL_DIRECTORY_INSTRUCTIONS_ENV in env))
64
57
  return env;
@@ -470,6 +463,9 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
470
463
  const baseEnv = {
471
464
  ...inheritedEnv,
472
465
  ...sanitizeEnvVars(providerConfig.envVars),
466
+ ...(input.trainingProjectContext === undefined
467
+ ? {}
468
+ : machinePackageManagerEnv(inheritedEnv)),
473
469
  ...input.launch.systemEnv,
474
470
  PATH: `${workspace.crewDir}${delimiter}${augmentedPath()}`,
475
471
  CREW_SERVER_URL: input.launch.serverUrl,
@@ -512,6 +508,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
512
508
  qa_runtime_root: childEnv.QA_RUNTIME_ROOT ?? null,
513
509
  qa_runtime_root_present: childEnv.QA_RUNTIME_ROOT !== undefined,
514
510
  inherited_qa_runtime_root_present: inheritedEnv.QA_RUNTIME_ROOT !== undefined,
511
+ ...packageManagerEnvFacts(childEnv),
515
512
  training_root: join(crewHome, "training"),
516
513
  project_context_present: input.projectContext !== undefined,
517
514
  project_ids: JSON.stringify(input.projectContext === undefined
package/dist/main.js CHANGED
@@ -31,6 +31,7 @@ import { runStorageWorkerCommand } from "./storage-worker-command.js";
31
31
  import { probeStorageMigrationRuntime } from "./storage-runtime-readiness.js";
32
32
  import { createStorageTargetConfirmationHandler } from "./storage-target-confirmation.js";
33
33
  import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
34
+ import { storageServiceEnvironment } from "./computer-service.js";
34
35
  async function main() {
35
36
  const workerResult = await runStorageWorkerCommand(process.argv.slice(2));
36
37
  if (workerResult !== null) {
@@ -78,9 +79,9 @@ async function main() {
78
79
  userHome: homedir(),
79
80
  platform: process.platform,
80
81
  explicit: process.env.NOWWORK_LOCAL_HOME,
81
- mode: process.env.NOWWORK_LOCAL_HOME_MODE
82
- ?? process.env.NOWWORK_STORAGE_MODE
83
- ?? (values["daemon-home"] === undefined ? undefined : "legacy"),
82
+ mode: process.env.NOWWORK_LOCAL_HOME_MODE ?? process.env.NOWWORK_STORAGE_MODE,
83
+ ...(values.profile === undefined ? {} : { profile: values.profile }),
84
+ ...(values["daemon-home"] === undefined ? {} : { daemonHome: values["daemon-home"] }),
84
85
  });
85
86
  if (values.profile) {
86
87
  const home = daemonHome(process.env, values.profile);
@@ -98,15 +99,7 @@ async function main() {
98
99
  agentsRoot: resolveAgentsRoot(profile.agentsRoot, homedir(), process.platform),
99
100
  nodePath: process.execPath,
100
101
  entryPath: builtDaemonEntry(),
101
- environment: {
102
- ...(process.env.NOWWORK_HOME === undefined ? {} : { NOWWORK_HOME: process.env.NOWWORK_HOME }),
103
- ...(process.env.NOWWORK_ENV_TAG === undefined ? {} : { NOWWORK_ENV_TAG: process.env.NOWWORK_ENV_TAG }),
104
- ...(process.env.NOWWORK_STORAGE_POLICY_PATH === undefined
105
- ? {} : { NOWWORK_STORAGE_POLICY_PATH: process.env.NOWWORK_STORAGE_POLICY_PATH }),
106
- ...(process.env.NOWWORK_LOCAL_HOME === undefined ? {} : { NOWWORK_LOCAL_HOME: process.env.NOWWORK_LOCAL_HOME }),
107
- ...(process.env.NOWWORK_LOCAL_HOME_MODE === undefined && process.env.NOWWORK_STORAGE_MODE === undefined
108
- ? {} : { NOWWORK_LOCAL_HOME_MODE: process.env.NOWWORK_LOCAL_HOME_MODE ?? process.env.NOWWORK_STORAGE_MODE }),
109
- },
102
+ environment: storageServiceEnvironment(process.env),
110
103
  });
111
104
  }
112
105
  applyProfileToEnv(profile, process.env);
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { acquireDaemonInstallationLease } from "./daemon-installation-lease.js";
2
+ import { acquireDaemonInstallationLease, } from "./daemon-installation-lease.js";
3
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) {
@@ -43,7 +43,7 @@ function sameManagedResource(left, right) {
43
43
  function descriptorDiffIsStorageEnvironmentOnly(previous, next) {
44
44
  if (!next.includes("\n <key>EnvironmentVariables</key>\n"))
45
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");
46
+ const strip = (descriptor) => descriptor.replace(/\n <key>EnvironmentVariables<\/key>\n <dict>\n(?: <key>(?:NOWWORK_HOME|NOWWORK_ENV_TAG|NOWWORK_STORAGE_POLICY_PATH|NOWWORK_LOCAL_HOME|NOWWORK_LOCAL_HOME_MODE)<\/key><string>[^<]*<\/string>\n)+ <\/dict>\n/u, "\n");
47
47
  return strip(previous) === strip(next);
48
48
  }
49
49
  function assertRecordDescribesSpec(input) {
@@ -90,6 +90,7 @@ export async function installManagedService(input) {
90
90
  }
91
91
  const lease = await acquire(input.record.npmPrefix, {
92
92
  ...(input.registry?.userHome === undefined ? {} : { userHome: input.registry.userHome }),
93
+ ...(input.registry?.localHome === undefined ? {} : { localHome: input.registry.localHome }),
93
94
  });
94
95
  try {
95
96
  await installService(input.spec, input.runner);
@@ -136,6 +137,7 @@ export async function installManagedService(input) {
136
137
  }
137
138
  const lease = await acquire(input.record.npmPrefix, {
138
139
  ...(input.registry?.userHome === undefined ? {} : { userHome: input.registry.userHome }),
140
+ ...(input.registry?.localHome === undefined ? {} : { localHome: input.registry.localHome }),
139
141
  });
140
142
  try {
141
143
  const descriptor = await readServiceDescriptor(input.spec);
@@ -170,6 +172,7 @@ export async function uninstallUnregisteredManagedService(input) {
170
172
  const acquire = input.acquireInstallationLease ?? acquireDaemonInstallationLease;
171
173
  const lease = await acquire(input.npmPrefix, {
172
174
  ...(input.registry?.userHome === undefined ? {} : { userHome: input.registry.userHome }),
175
+ ...(input.registry?.localHome === undefined ? {} : { localHome: input.registry.localHome }),
173
176
  });
174
177
  try {
175
178
  await uninstallService(input.spec, input.runner);
@@ -199,6 +202,7 @@ export async function uninstallManagedService(input) {
199
202
  }
200
203
  const lease = await acquire(input.record.npmPrefix, {
201
204
  ...(input.registry?.userHome === undefined ? {} : { userHome: input.registry.userHome }),
205
+ ...(input.registry?.localHome === undefined ? {} : { localHome: input.registry.localHome }),
202
206
  });
203
207
  try {
204
208
  if (descriptor !== null)
@@ -0,0 +1,75 @@
1
+ import { readFileSync } from "node:fs";
2
+ function nonEmpty(value) {
3
+ const normalized = value?.trim();
4
+ return normalized === undefined || normalized === "" ? undefined : normalized;
5
+ }
6
+ function registryUrl(value) {
7
+ const normalized = nonEmpty(value);
8
+ if (normalized === undefined)
9
+ return undefined;
10
+ try {
11
+ const protocol = new URL(normalized).protocol;
12
+ return protocol === "http:" || protocol === "https:" ? normalized : undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ function configuredPath(env) {
19
+ const explicit = env.CREW_PACKAGE_MANAGER_USERCONFIG?.trim();
20
+ if (explicit !== undefined && explicit !== "")
21
+ return explicit;
22
+ return undefined;
23
+ }
24
+ function registryFromNpmrc(path) {
25
+ if (path === undefined)
26
+ return undefined;
27
+ try {
28
+ const line = readFileSync(path, "utf8")
29
+ .split(/\r?\n/u)
30
+ .find((entry) => /^\s*registry\s*=/u.test(entry));
31
+ return registryUrl(line?.replace(/^\s*registry\s*=\s*/u, ""));
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ function npmrcHasAuth(path) {
38
+ if (path === undefined)
39
+ return false;
40
+ try {
41
+ return /^\s*(?:(?:\/\/[^=\s]+:)?_(?:auth|authToken|password))\s*=/imu.test(readFileSync(path, "utf8"));
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ /**
48
+ * Keep machine-owned npm credentials available for explicitly authorized
49
+ * training dependency installs. The daemon must opt in with
50
+ * CREW_PACKAGE_MANAGER_USERCONFIG; Agent env cannot reach this path.
51
+ */
52
+ export function machinePackageManagerEnv(env = process.env) {
53
+ const npmUserConfig = configuredPath(env);
54
+ const yarnRegistry = npmUserConfig === undefined
55
+ ? undefined
56
+ : registryUrl(env.YARN_REGISTRY) ?? registryFromNpmrc(npmUserConfig);
57
+ const entries = npmUserConfig === undefined
58
+ ? []
59
+ : [["NPM_CONFIG_USERCONFIG", npmUserConfig]];
60
+ const alwaysAuth = npmrcHasAuth(npmUserConfig) ? "true" : undefined;
61
+ return Object.fromEntries(yarnRegistry === undefined && alwaysAuth === undefined
62
+ ? entries
63
+ : [
64
+ ...entries,
65
+ ...(yarnRegistry === undefined ? [] : [["YARN_REGISTRY", yarnRegistry]]),
66
+ ...(alwaysAuth === undefined ? [] : [["NPM_CONFIG_ALWAYS_AUTH", alwaysAuth]]),
67
+ ]);
68
+ }
69
+ export function packageManagerEnvFacts(env) {
70
+ return {
71
+ npm_userconfig_present: Boolean(env.NPM_CONFIG_USERCONFIG?.trim()),
72
+ yarn_registry_present: Boolean(env.YARN_REGISTRY?.trim()),
73
+ npm_always_auth_present: Boolean(env.NPM_CONFIG_ALWAYS_AUTH?.trim()),
74
+ };
75
+ }
package/dist/runner.js CHANGED
@@ -12,7 +12,7 @@ import { readBoundImDecisionFile, resetBoundImDecisionFile } from "./bound-im-de
12
12
  import { launchSupervisedRuntime } from "./supervised-runtime.js";
13
13
  import { awaitWithCancellation, } from "./runtime-cancellation.js";
14
14
  export { awaitExit, exitActivity } from "./local-executor.js";
15
- export { sanitizeEnvVars } from "./provider-env.js";
15
+ export { sanitizeEnvVars } from "./runtime-env.js";
16
16
  const ICON = {
17
17
  init: "🟢", text: "💬", reading: "📖", sending: "📨", checking: "🔎",
18
18
  claiming: "📌", crew: "⚙️", tool: "🛠️", tool_result: "↩️", done: "✅", error: "❌",
@@ -0,0 +1,21 @@
1
+ const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2
+ export const RESERVED_ENV = new Set([
3
+ "PATH",
4
+ "XDG_CONFIG_HOME",
5
+ "XDG_DATA_HOME",
6
+ "XDG_CACHE_HOME",
7
+ "GH_CONFIG_DIR",
8
+ "CLOUDSDK_CONFIG",
9
+ "NPM_CONFIG_USERCONFIG",
10
+ "YARN_REGISTRY",
11
+ "NPM_CONFIG_ALWAYS_AUTH",
12
+ ]);
13
+ /** 用户自定义环境只能补充普通变量,不能覆盖 daemon 维护的运行时契约。 */
14
+ export function sanitizeEnvVars(raw) {
15
+ if (!raw)
16
+ return {};
17
+ return Object.fromEntries(Object.entries(raw).filter(([key, value]) => typeof value === "string"
18
+ && ENV_KEY_RE.test(key)
19
+ && !key.toUpperCase().startsWith("CREW_")
20
+ && !RESERVED_ENV.has(key.toUpperCase())));
21
+ }
package/dist/serve.js CHANGED
@@ -106,7 +106,7 @@ export function serve(config, opts = {}) {
106
106
  const cancellations = new Map();
107
107
  const legacyRuns = new Map();
108
108
  const updateEligibility = opts.update?.eligibility
109
- ?? (() => detectDaemonUpdateEligibility(opts.profileName));
109
+ ?? (() => detectDaemonUpdateEligibility(opts.profileName, { localHome }));
110
110
  const updateController = createDaemonUpdateController({
111
111
  eligibility: updateEligibility,
112
112
  install: opts.update?.install ?? ((input) => installExactDaemonUpdate({
@@ -3,6 +3,7 @@ import { posix, win32 } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { atomicPrivateWrite } from "./atomic-private-write.js";
5
5
  import { resolveEnvTag, resolveNowworkHome, storageLayout, } from "./storage-layout.js";
6
+ import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
6
7
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
7
8
  const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
8
9
  const PORTABLE_ABSOLUTE_PATH = z.string().min(1).max(32768).refine((value) => posix.isAbsolute(value) || win32.isAbsolute(value), "must be an absolute path");
@@ -46,9 +47,34 @@ function absoluteBase(value, fallback, platform, name) {
46
47
  }
47
48
  return paths.normalize(selected);
48
49
  }
50
+ function localHomeRoot(input) {
51
+ const environment = input.environment;
52
+ if (environment.NOWWORK_LOCAL_HOME === undefined
53
+ && environment.NOWWORK_LOCAL_HOME_MODE === undefined
54
+ && environment.NOWWORK_STORAGE_MODE === undefined) {
55
+ return null;
56
+ }
57
+ return resolveDaemonLocalStateRootFromEnv(input);
58
+ }
59
+ function rootsUnderLocalHome(root, platform) {
60
+ const paths = pathsFor(platform);
61
+ const cacheRoot = paths.join(root, "cache");
62
+ const stateRoot = paths.join(root, "state");
63
+ return Object.freeze({
64
+ configRoot: paths.join(root, "config"),
65
+ runtimeBase: paths.join(root, "runtime"),
66
+ cacheRoot,
67
+ stateRoot,
68
+ backupsRoot: paths.join(stateRoot, "backups"),
69
+ runtimePackagesRoot: paths.join(cacheRoot, "runtime-packages"),
70
+ });
71
+ }
49
72
  export function resolveLocalStorageRoots(input) {
50
73
  const paths = pathsFor(input.platform);
51
74
  const userHome = absoluteBase(input.userHome, input.userHome, input.platform, "user home");
75
+ const configuredLocalHome = localHomeRoot({ ...input, userHome });
76
+ if (configuredLocalHome !== null)
77
+ return rootsUnderLocalHome(configuredLocalHome, input.platform);
52
78
  if (input.platform === "win32") {
53
79
  const local = absoluteBase(input.environment.LOCALAPPDATA, paths.join(userHome, "AppData", "Local"), input.platform, "LOCALAPPDATA");
54
80
  const root = paths.join(local, "NowWork");
@@ -54,7 +54,6 @@ const REQUIRED_BACKUP_ARTIFACTS = Object.freeze([
54
54
  "profile.json",
55
55
  "service-descriptor",
56
56
  "managed-service-registry.json",
57
- "installations.json",
58
57
  ]);
59
58
  export class StorageMigrationFailure extends Error {
60
59
  code;
@@ -1,4 +1,5 @@
1
1
  import { realpathSync } from "node:fs";
2
+ import { lstat } from "node:fs/promises";
2
3
  import { isAbsolute, resolve } from "node:path";
3
4
  import { z } from "zod";
4
5
  import { buildServiceSpec } from "./computer-service.js";
@@ -10,7 +11,7 @@ import { readStorageMigrationPolicy } from "./storage-migration-policy.js";
10
11
  import { createStorageMigrationRuntimeController, } from "./storage-runtime-controller.js";
11
12
  import { resolveEnvTag, resolveNowworkHome } from "./storage-layout.js";
12
13
  import { readStorageOwnershipClaim } from "./storage-ownership.js";
13
- import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
14
+ import { parseDaemonLocalHomeMode, resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
14
15
  const notReady = (reason) => Object.freeze({ ready: false, reason });
15
16
  function canonicalPath(value) {
16
17
  const absolute = resolve(value);
@@ -75,15 +76,33 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
75
76
  serverUrl: z.string().url().parse(rawInput.serverUrl),
76
77
  platform: rawInput.platform,
77
78
  };
78
- const localHome = resolveDaemonLocalStateRoot({
79
- userHome: rawInput.userHome,
80
- platform: rawInput.platform,
81
- explicit: rawInput.environment.NOWWORK_LOCAL_HOME,
82
- mode: rawInput.environment.NOWWORK_LOCAL_HOME_MODE ?? rawInput.environment.NOWWORK_STORAGE_MODE,
83
- });
79
+ let sourceLocalHome;
80
+ let targetLocalHome;
81
+ let targetEnvironment;
84
82
  let nowworkHome;
85
83
  let environment;
84
+ let local;
86
85
  try {
86
+ sourceLocalHome = resolveDaemonLocalStateRoot({
87
+ userHome: rawInput.userHome,
88
+ platform: rawInput.platform,
89
+ profile: input.profile,
90
+ daemonHome: input.currentDaemonHome,
91
+ });
92
+ const requestedLocalHomeMode = parseDaemonLocalHomeMode(rawInput.environment.NOWWORK_LOCAL_HOME_MODE ?? rawInput.environment.NOWWORK_STORAGE_MODE);
93
+ if (requestedLocalHomeMode === "legacy")
94
+ throw new Error("storage migration target must use the canonical local .nowwork root");
95
+ targetLocalHome = resolveDaemonLocalStateRoot({
96
+ userHome: rawInput.userHome,
97
+ platform: rawInput.platform,
98
+ explicit: rawInput.environment.NOWWORK_LOCAL_HOME ?? resolve(rawInput.userHome, ".nowwork"),
99
+ mode: "nowwork",
100
+ });
101
+ targetEnvironment = {
102
+ ...input.environment,
103
+ NOWWORK_LOCAL_HOME: targetLocalHome,
104
+ NOWWORK_LOCAL_HOME_MODE: "nowwork",
105
+ };
87
106
  const home = resolveNowworkHome({
88
107
  environment: configuredNowworkHome,
89
108
  userHome: input.userHome,
@@ -97,6 +116,11 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
97
116
  ? {}
98
117
  : { environment: input.environment.NOWWORK_ENV_TAG }),
99
118
  }).environment;
119
+ local = resolveLocalStorageRoots({
120
+ platform: input.platform,
121
+ userHome: input.userHome,
122
+ environment: targetEnvironment,
123
+ });
100
124
  }
101
125
  catch {
102
126
  return notReady("storage_environment_invalid");
@@ -119,7 +143,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
119
143
  };
120
144
  let sourceRecord;
121
145
  try {
122
- const records = await dependencies.readManagedServices({ userHome: input.userHome, localHome });
146
+ const records = await dependencies.readManagedServices({ userHome: input.userHome, localHome: sourceLocalHome });
123
147
  const found = findSourceRecord(records, input);
124
148
  if (found === null)
125
149
  return notReady("source_service_record_missing");
@@ -141,7 +165,7 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
141
165
  const assertSourceIdentity = () => dependencies.assertManagedServiceIdentity({
142
166
  spec: sourceSpec,
143
167
  record: sourceRecord,
144
- registry: { userHome: input.userHome, localHome },
168
+ registry: { userHome: input.userHome, localHome: sourceLocalHome },
145
169
  });
146
170
  try {
147
171
  await assertSourceIdentity();
@@ -173,11 +197,18 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
173
197
  }
174
198
  if (ownership.environment !== environment)
175
199
  return notReady("storage_ownership_claim_invalid");
176
- const local = resolveLocalStorageRoots({
177
- platform: input.platform,
178
- userHome: input.userHome,
179
- environment: input.environment,
180
- });
200
+ const targetInstallationsPath = installationsPath(local.configRoot);
201
+ let targetInstallationsArtifact = null;
202
+ try {
203
+ const targetInstallations = await lstat(targetInstallationsPath);
204
+ if (!targetInstallations.isFile())
205
+ return notReady("target_installations_invalid");
206
+ targetInstallationsArtifact = { name: "installations.json", path: targetInstallationsPath };
207
+ }
208
+ catch (error) {
209
+ if (error.code !== "ENOENT")
210
+ return notReady("target_installations_invalid");
211
+ }
181
212
  const runtime = dependencies.createController({
182
213
  profile: input.profile,
183
214
  serverUrl: input.serverUrl,
@@ -194,12 +225,15 @@ export async function probeStorageMigrationRuntime(rawInput, injected = {}) {
194
225
  NOWWORK_HOME: nowworkHome,
195
226
  NOWWORK_ENV_TAG: environment,
196
227
  NOWWORK_STORAGE_POLICY_PATH: policyPath,
228
+ ...(targetEnvironment.NOWWORK_LOCAL_HOME === undefined ? {} : { NOWWORK_LOCAL_HOME: targetEnvironment.NOWWORK_LOCAL_HOME }),
229
+ ...(targetEnvironment.NOWWORK_LOCAL_HOME_MODE === undefined
230
+ ? {} : { NOWWORK_LOCAL_HOME_MODE: targetEnvironment.NOWWORK_LOCAL_HOME_MODE }),
197
231
  },
198
232
  backupArtifacts: [
199
233
  { name: "profile.json", path: profilePath(input.profile, input.currentDaemonHome) },
200
234
  { name: "service-descriptor", path: sourceRecord.descriptorPath },
201
- { name: "managed-service-registry.json", path: managedServiceRegistryPath(input.userHome, localHome) },
202
- { name: "installations.json", path: installationsPath(local.configRoot) },
235
+ { name: "managed-service-registry.json", path: managedServiceRegistryPath(input.userHome, sourceLocalHome) },
236
+ ...(targetInstallationsArtifact === null ? [] : [targetInstallationsArtifact]),
203
237
  ],
204
238
  }, { assertSourceIdentity });
205
239
  return Object.freeze({ ready: true, runtime });
@@ -12,6 +12,7 @@ import { readStorageWorkerHandoff } from "./storage-worker-handoff.js";
12
12
  import { readStorageWorkerOperation } from "./storage-worker-operation.js";
13
13
  import { storageIdFingerprint } from "./storage-identity.js";
14
14
  import { waitForStorageMigrationAcknowledgement } from "./storage-target-confirmation.js";
15
+ import { resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
15
16
  const defaultServiceDependencies = () => ({
16
17
  assertManagedServiceIdentity,
17
18
  serviceAction,
@@ -27,7 +28,7 @@ const defaultServiceDependencies = () => ({
27
28
  readInstallations,
28
29
  writeInstallations,
29
30
  });
30
- function serviceSpec(operation, record, profileHome) {
31
+ function serviceSpec(operation, record, profileHome, layout = "target") {
31
32
  return buildServiceSpec({
32
33
  platform: operation.service.platform,
33
34
  profile: operation.profile,
@@ -36,7 +37,7 @@ function serviceSpec(operation, record, profileHome) {
36
37
  nodePath: record.nodePath,
37
38
  entryPath: record.entryPath,
38
39
  profileHome,
39
- ...(operation.service.serviceEnvironment === undefined
40
+ ...(layout === "source" || operation.service.serviceEnvironment === undefined
40
41
  ? {}
41
42
  : { environment: operation.service.serviceEnvironment }),
42
43
  });
@@ -74,10 +75,27 @@ function canonicalServerUrl(value) {
74
75
  }
75
76
  export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
76
77
  const dependencies = { ...defaultServiceDependencies(), ...injected };
77
- const registry = { userHome: operation.service.userHome };
78
+ const sourceLocalHome = resolveDaemonLocalStateRoot({
79
+ userHome: operation.service.userHome,
80
+ platform: operation.service.platform,
81
+ profile: operation.service.sourceRecord.profile,
82
+ daemonHome: operation.service.sourceRecord.daemonHome,
83
+ });
84
+ const targetLocalHome = resolveDaemonLocalStateRoot({
85
+ userHome: operation.service.userHome,
86
+ platform: operation.service.platform,
87
+ profile: operation.service.targetRecord.profile,
88
+ daemonHome: operation.service.targetRecord.daemonHome,
89
+ ...(operation.service.serviceEnvironment?.NOWWORK_LOCAL_HOME === undefined
90
+ ? {} : { explicit: operation.service.serviceEnvironment.NOWWORK_LOCAL_HOME }),
91
+ ...(operation.service.serviceEnvironment?.NOWWORK_LOCAL_HOME_MODE === undefined
92
+ ? {} : { mode: operation.service.serviceEnvironment.NOWWORK_LOCAL_HOME_MODE }),
93
+ });
94
+ const sourceRegistry = { userHome: operation.service.userHome, localHome: sourceLocalHome };
95
+ const targetRegistry = { userHome: operation.service.userHome, localHome: targetLocalHome };
78
96
  const sourceRecord = operation.service.sourceRecord;
79
97
  const targetRecord = operation.service.targetRecord;
80
- const sourceSpec = serviceSpec(operation, sourceRecord, operation.migration.sourceRoots["daemon-home"]);
98
+ const sourceSpec = serviceSpec(operation, sourceRecord, operation.migration.sourceRoots["daemon-home"], "source");
81
99
  const targetSpec = serviceSpec(operation, targetRecord, operation.installation.configRoot);
82
100
  assertRecordMatchesSpec(sourceRecord, sourceSpec);
83
101
  assertRecordMatchesSpec(targetRecord, targetSpec);
@@ -89,7 +107,7 @@ export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
89
107
  const assertSourceIdentity = () => dependencies.assertManagedServiceIdentity({
90
108
  spec: sourceSpec,
91
109
  record: sourceRecord,
92
- registry,
110
+ registry: sourceRegistry,
93
111
  });
94
112
  const readTargetInstallations = () => dependencies.readInstallations({
95
113
  configRoot: operation.installation.configRoot,
@@ -140,7 +158,7 @@ export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
140
158
  spec: sourceSpec,
141
159
  record: sourceRecord,
142
160
  runner: systemCommandRunner,
143
- registry,
161
+ registry: sourceRegistry,
144
162
  });
145
163
  const { version: _version, ...profile } = sourceProfile;
146
164
  savedTargetProfile = await dependencies.saveProfile({
@@ -154,7 +172,7 @@ export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
154
172
  spec: targetSpec,
155
173
  record: targetRecord,
156
174
  runner: systemCommandRunner,
157
- registry,
175
+ registry: targetRegistry,
158
176
  });
159
177
  const target = await dependencies.captureExactService(targetSpec, systemCommandRunner);
160
178
  if (!target.installed || !target.exact || target.running) {
@@ -169,7 +187,7 @@ export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
169
187
  }
170
188
  };
171
189
  const removeTargetService = async () => {
172
- const records = await dependencies.readManagedServices(registry);
190
+ const records = await dependencies.readManagedServices(targetRegistry);
173
191
  const registered = records.find((record) => record.serviceId === targetRecord.serviceId
174
192
  || (record.profile === targetRecord.profile && record.daemonHome === targetRecord.daemonHome));
175
193
  if (registered !== undefined && !sameRecord(registered, targetRecord)) {
@@ -180,7 +198,7 @@ export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
180
198
  spec: targetSpec,
181
199
  record: targetRecord,
182
200
  runner: systemCommandRunner,
183
- registry,
201
+ registry: targetRegistry,
184
202
  });
185
203
  }
186
204
  else {
@@ -193,7 +211,7 @@ export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
193
211
  daemonHome: targetRecord.daemonHome,
194
212
  npmPrefix: targetRecord.npmPrefix,
195
213
  runner: systemCommandRunner,
196
- registry,
214
+ registry: targetRegistry,
197
215
  });
198
216
  }
199
217
  }
@@ -231,7 +249,7 @@ export function createStorageWorkerServiceLifecycle(operation, injected = {}) {
231
249
  spec: sourceSpec,
232
250
  record: sourceRecord,
233
251
  runner: systemCommandRunner,
234
- registry,
252
+ registry: sourceRegistry,
235
253
  });
236
254
  }
237
255
  const afterInstall = current.installed
@@ -7,6 +7,7 @@ import { atomicPrivateWrite } from "./atomic-private-write.js";
7
7
  import { assertStorageControlMaterialTokenFree } from "./storage-credential-guard.js";
8
8
  import { storageLayout } from "./storage-layout.js";
9
9
  import { StorageMigrationWarningCodeSchema } from "./storage-migration-policy.js";
10
+ import { parseDaemonLocalHomeMode } from "./daemon-local-home.js";
10
11
  const CONTROL = /[\0\r\n]/u;
11
12
  const PROFILE = /^[a-z0-9][a-z0-9_-]{0,47}$/u;
12
13
  const ENVIRONMENT = /^[a-z0-9][a-z0-9_-]{0,31}$/u;
@@ -95,6 +96,8 @@ const StorageServiceEnvironmentSchema = z.object({
95
96
  NOWWORK_HOME: AbsolutePathSchema.optional(),
96
97
  NOWWORK_ENV_TAG: z.string().regex(ENVIRONMENT).optional(),
97
98
  NOWWORK_STORAGE_POLICY_PATH: AbsolutePathSchema.optional(),
99
+ NOWWORK_LOCAL_HOME: AbsolutePathSchema.optional(),
100
+ NOWWORK_LOCAL_HOME_MODE: z.string().min(1).max(32).optional(),
98
101
  }).strict();
99
102
  const OperationSchema = z.object({
100
103
  version: z.literal(1),
@@ -182,6 +185,9 @@ function isInside(root, candidate, platform) {
182
185
  return child === "" || (!child.startsWith("..") && !paths.isAbsolute(child));
183
186
  }
184
187
  function assertSemanticIdentity(operation) {
188
+ const localHomeMode = operation.service.serviceEnvironment?.NOWWORK_LOCAL_HOME_MODE;
189
+ if (localHomeMode !== undefined)
190
+ parseDaemonLocalHomeMode(localHomeMode);
185
191
  const identity = [operation.doctorInput, operation.installation.target];
186
192
  if (identity.some((value) => value.workspaceId !== operation.workspaceId
187
193
  || value.environment !== operation.environment || value.profile !== operation.profile)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nowcrew/daemon",
3
- "version": "0.6.80",
3
+ "version": "0.6.82",
4
4
  "type": "module",
5
5
  "description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
6
6
  "license": "Apache-2.0",