@nowcrew/daemon 0.6.77 → 0.6.78

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.
Files changed (52) hide show
  1. package/README.md +27 -0
  2. package/dist/automatic-daemon-installer.js +44 -24
  3. package/dist/automatic-install-target.js +34 -9
  4. package/dist/computer-cli.js +43 -14
  5. package/dist/computer-profile.js +15 -2
  6. package/dist/computer-service.js +26 -5
  7. package/dist/config.js +28 -8
  8. package/dist/control-plane-url.js +3 -9
  9. package/dist/daemon-installation-lease.js +7 -4
  10. package/dist/daemon-local-home.js +108 -0
  11. package/dist/daemon-migration-controller.js +15 -4
  12. package/dist/daemon-registry.js +20 -0
  13. package/dist/daemon-update-controller.js +1 -0
  14. package/dist/daemon-update-eligibility.js +17 -7
  15. package/dist/daemon-updater.js +26 -26
  16. package/dist/execution-runner.js +7 -7
  17. package/dist/host-execution-coordinator.js +4 -3
  18. package/dist/local-executor.js +23 -17
  19. package/dist/local-memory/runtime.js +5 -3
  20. package/dist/machine-info.js +3 -0
  21. package/dist/main.js +13 -2
  22. package/dist/managed-service-diagnostics.js +1 -0
  23. package/dist/managed-service-registry.js +12 -7
  24. package/dist/managed-service-startup.js +4 -1
  25. package/dist/npm-command.js +9 -0
  26. package/dist/project-skills/controller.js +6 -71
  27. package/dist/project-skills/execution-adapter.js +7 -0
  28. package/dist/project-skills/execution-projection.js +163 -0
  29. package/dist/project-skills/initialized-reconciler.js +10 -8
  30. package/dist/project-skills/legacy-cleanup.js +224 -0
  31. package/dist/project-skills/machine-hello-publisher.js +20 -0
  32. package/dist/project-skills/reconciler.js +114 -500
  33. package/dist/project-skills/resolution-records.js +63 -0
  34. package/dist/project-skills/runtime-launch.js +0 -42
  35. package/dist/project-skills/runtime-warning.js +1 -3
  36. package/dist/project-skills/scanner.js +1 -17
  37. package/dist/project-skills/startup-cleanup.js +24 -0
  38. package/dist/runner.js +2 -1
  39. package/dist/serve.js +49 -49
  40. package/dist/slog.js +14 -1
  41. package/dist/storage-control.js +12 -1
  42. package/dist/storage-identity.js +15 -1
  43. package/dist/storage-migration-controller.js +25 -4
  44. package/dist/storage-ownership.js +55 -0
  45. package/dist/storage-runtime-controller.js +29 -7
  46. package/dist/storage-runtime-readiness.js +26 -4
  47. package/dist/storage-runtime.js +18 -2
  48. package/dist/storage-status-delivery.js +4 -0
  49. package/dist/storage-status-replay.js +16 -1
  50. package/dist/storage-worker-executor.js +1 -0
  51. package/dist/storage-worker-operation.js +4 -0
  52. package/package.json +1 -1
package/README.md CHANGED
@@ -479,6 +479,33 @@ failure matrix, rollout limits, and the server-native follow-up design.
479
479
 
480
480
  ## Code Map
481
481
 
482
+ ## Local daemon installation root (experimental)
483
+
484
+ New installations use `~/.nowwork` for machine-local daemon configuration and npm runtimes. The
485
+ Workspace persistence root is still selected independently by `NOWWORK_HOME`; it may be a local
486
+ directory or an ECS-specific NAS directory. Runtime packages, locks, sockets, PIDs and temporary
487
+ files must remain on the machine running the daemon.
488
+
489
+ The same local root also owns shared daemon control state:
490
+ `daemon/managed-services/registry.json`, `daemon/managed-services/installation-leases/`,
491
+ `daemon/compute-execution/`, and `logs/sls-spool/`. This keeps service ownership, upgrade leases,
492
+ host-wide execution admission, and offline log delivery on the daemon host rather than on NAS.
493
+
494
+ The selector is controlled by `NOWWORK_LOCAL_HOME_MODE`:
495
+
496
+ - `auto` (default): use an existing `.nowwork` installation, otherwise preserve an existing `.crew`
497
+ installation; a brand-new profile is created under `.nowwork`.
498
+ - `nowwork`: require the `.nowwork` installation root.
499
+ - `legacy`: force the old `.crew` root for rollback.
500
+
501
+ `NOWWORK_STORAGE_MODE` is accepted as a compatibility alias. `NOWWORK_LOCAL_HOME` can override the
502
+ local root and should point to a local filesystem, never to the NAS Workspace root; the mount/device
503
+ check remains a doctor/migration gate. If the same profile
504
+ is found in both roots, startup/install is refused until the duplicate is resolved explicitly. The
505
+ old `.crew` tree is not deleted by this compatibility layer.
506
+ For shared machine control state, `auto` prefers an existing `~/.nowwork` root; during a mixed
507
+ migration window, set `NOWWORK_LOCAL_HOME_MODE=legacy` on services that still use the old registry.
508
+
482
509
  - `src/serve.ts`: connection, negotiation, routing, sync, and legacy boundary.
483
510
  - `src/daemon-update-eligibility.ts`, `src/daemon-updater.ts`, and `src/daemon-update-controller.ts`:
484
511
  managed-update eligibility, exact npm replacement, host-wide admission barrier, and restart handoff.
@@ -10,8 +10,10 @@ import { acquireDaemonInstallationLease, } from "./daemon-installation-lease.js"
10
10
  import { daemonVersion } from "./machine-info.js";
11
11
  import { daemonReleaseProvenance } from "./release-provenance.js";
12
12
  import { daemonReleaseChannel, isDaemonReleaseVersion } from "./daemon-release-version.js";
13
- import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
13
+ import { daemonRegistryCandidates } from "./daemon-registry.js";
14
+ import { npmInvocation } from "./npm-command.js";
14
15
  import { discoverExactLegacyService, } from "./automatic-service-migration.js";
16
+ import { resolveDaemonLocalStateRootFromEnv } from "./daemon-local-home.js";
15
17
  function profileStorage(platform, userHome, runner) {
16
18
  return {
17
19
  platform,
@@ -30,6 +32,7 @@ function sameHostname(left, right) {
30
32
  function defaultDependencies() {
31
33
  const platform = process.platform;
32
34
  const userHome = homedir();
35
+ const localHome = resolveDaemonLocalStateRootFromEnv({ userHome, platform, environment: process.env });
33
36
  const runner = systemCommandRunner;
34
37
  const storage = profileStorage(platform, userHome, runner);
35
38
  let managedRecords = [];
@@ -38,6 +41,7 @@ function defaultDependencies() {
38
41
  platform,
39
42
  userHome,
40
43
  uid: process.getuid?.(),
44
+ environment: process.env,
41
45
  nodePath: process.execPath,
42
46
  version: daemonVersion(),
43
47
  ...(process.env.PATH ? { runtimePath: process.env.PATH } : {}),
@@ -57,7 +61,7 @@ function defaultDependencies() {
57
61
  },
58
62
  captureService: async (spec, target) => {
59
63
  if (platform !== "win32") {
60
- managedRecords = await readManagedServiceRegistry({ userHome });
64
+ managedRecords = await readManagedServiceRegistry({ userHome, localHome });
61
65
  const entryPath = spec.daemonCommand[1];
62
66
  const installation = daemonGlobalInstallation(entryPath, platform);
63
67
  if (installation === null) {
@@ -91,7 +95,7 @@ function defaultDependencies() {
91
95
  managedServices: managedRecords,
92
96
  }),
93
97
  stopService: (spec) => serviceAction(spec, "stop", runner),
94
- acquireInstallationLease: (npmPrefix) => acquireDaemonInstallationLease(npmPrefix, { userHome }),
98
+ acquireInstallationLease: (npmPrefix) => acquireDaemonInstallationLease(npmPrefix, { userHome, localHome }),
95
99
  installPackage: installExactDaemonPackage,
96
100
  saveTargetProfile: async (profile, daemonHome) => {
97
101
  await saveProfile(profile, daemonHome, storage);
@@ -118,7 +122,7 @@ function defaultDependencies() {
118
122
  spec,
119
123
  record,
120
124
  runner,
121
- registry: { userHome },
125
+ registry: { userHome, localHome },
122
126
  });
123
127
  },
124
128
  removeLegacyService: async (legacy) => {
@@ -145,13 +149,13 @@ function defaultDependencies() {
145
149
  await restoreExactService(spec, previous, runner);
146
150
  return;
147
151
  }
148
- const current = (await readManagedServiceRegistry({ userHome })).find((record) => record.serviceId === spec.id);
152
+ const current = (await readManagedServiceRegistry({ userHome, localHome })).find((record) => record.serviceId === spec.id);
149
153
  if (current !== undefined && JSON.stringify(current) !== JSON.stringify(previousManagedRecord)) {
150
154
  await uninstallManagedService({
151
155
  spec,
152
156
  record: current,
153
157
  runner,
154
- registry: { userHome },
158
+ registry: { userHome, localHome },
155
159
  });
156
160
  }
157
161
  if (previousManagedRecord !== undefined) {
@@ -159,7 +163,7 @@ function defaultDependencies() {
159
163
  spec,
160
164
  record: previousManagedRecord,
161
165
  runner,
162
- registry: { userHome },
166
+ registry: { userHome, localHome },
163
167
  });
164
168
  if (previous.running)
165
169
  await serviceAction(spec, "start", runner);
@@ -178,24 +182,34 @@ export function stableDaemonEntry(target, platform) {
178
182
  ? paths.join(target.npmPrefix, "node_modules", "@nowcrew", "daemon", "dist", "main.js")
179
183
  : paths.join(target.npmPrefix, "lib", "node_modules", "@nowcrew", "daemon", "dist", "main.js");
180
184
  }
181
- export async function installExactDaemonPackage(npmPrefix, version, platform, runner, registryUrl) {
182
- const command = platform === "win32" ? "npm.cmd" : "npm";
183
- const result = await runner(command, [
184
- "install",
185
- "--global",
186
- "--prefix",
187
- npmPrefix,
188
- "--ignore-scripts",
189
- "--no-audit",
190
- "--no-fund",
191
- ...(registryUrl === undefined ? [] : ["--registry", normalizeDaemonNpmRegistry(registryUrl, {
192
- allowLoopbackHttp: daemonReleaseChannel(version) === "beta",
193
- })]),
194
- `@nowcrew/daemon@${version}`,
195
- ]);
196
- if (result.exitCode !== 0) {
197
- throw new Error(`Daemon package installation failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`}`);
185
+ export async function installExactDaemonPackage(npmPrefix, version, platform, runner, registryUrl, nodePath = process.execPath) {
186
+ const registries = daemonRegistryCandidates({
187
+ betaUpdate: daemonReleaseChannel(version) === "beta",
188
+ ...(registryUrl === undefined ? {} : { registryUrl }),
189
+ });
190
+ if (registries === null)
191
+ throw new Error("Daemon package installation failed: invalid registry");
192
+ const npm = npmInvocation(platform, nodePath);
193
+ let lastFailure = "";
194
+ for (const registry of registries) {
195
+ const result = await runner(npm.command, [
196
+ ...npm.argsPrefix,
197
+ "install",
198
+ "--global",
199
+ "--prefix",
200
+ npmPrefix,
201
+ "--ignore-scripts",
202
+ "--no-audit",
203
+ "--no-fund",
204
+ "--registry",
205
+ registry,
206
+ `@nowcrew/daemon@${version}`,
207
+ ]);
208
+ if (result.exitCode === 0)
209
+ return;
210
+ lastFailure = result.stderr.trim() || result.stdout.trim() || `exit ${result.exitCode}`;
198
211
  }
212
+ throw new Error(`Daemon package installation failed: ${lastFailure}`);
199
213
  }
200
214
  export async function waitForServiceRunning(spec, status, options = {}) {
201
215
  const timeoutMs = options.timeoutMs ?? 10_000;
@@ -220,6 +234,10 @@ function recoveryError(primary, failures) {
220
234
  }
221
235
  export async function installDaemonAutomatically(input, dependencies = defaultDependencies()) {
222
236
  const machineToken = MachineTokenSchema.parse(input.machineToken);
237
+ const requestedLocalHome = input.requestedLocalHome ?? dependencies.environment.NOWWORK_LOCAL_HOME;
238
+ const requestedLocalHomeMode = input.requestedLocalHomeMode
239
+ ?? dependencies.environment.NOWWORK_LOCAL_HOME_MODE
240
+ ?? dependencies.environment.NOWWORK_STORAGE_MODE;
223
241
  const target = resolveAutomaticInstallTarget({
224
242
  serverUrl: input.serverUrl,
225
243
  ...(input.requestedProfile === undefined ? {} : { requestedProfile: input.requestedProfile }),
@@ -227,6 +245,8 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
227
245
  ...(input.requestedRegistryUrl === undefined ? {} : { requestedRegistryUrl: input.requestedRegistryUrl }),
228
246
  userHome: dependencies.userHome,
229
247
  platform: dependencies.platform,
248
+ ...(requestedLocalHome === undefined ? {} : { requestedLocalHome }),
249
+ ...(requestedLocalHomeMode === undefined ? {} : { requestedLocalHomeMode }),
230
250
  });
231
251
  if (!isDaemonReleaseVersion(dependencies.version)) {
232
252
  throw new Error(`Invoking daemon package has an invalid release version '${dependencies.version}'`);
@@ -1,6 +1,7 @@
1
1
  import { posix, win32 } from "node:path";
2
2
  import { profileLayout, validateLocalInstanceName } from "./profile-layout.js";
3
3
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
4
+ import { resolveDaemonLocalHome } from "./daemon-local-home.js";
4
5
  const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
5
6
  const KNOWN_TARGETS = {
6
7
  "nowwork-in.nowcoder.com": {
@@ -72,13 +73,21 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
72
73
  const legacyProfile = environment === "production" ? "in" : environment;
73
74
  assertReleaseTagForServerUrl(input.serverUrl, input.requestedReleaseTag);
74
75
  if (requestedProfile === undefined || requestedProfile === legacyProfile) {
76
+ const local = resolveDaemonLocalHome({
77
+ userHome: input.userHome,
78
+ platform: input.platform,
79
+ profile: legacyProfile,
80
+ ...(input.requestedLocalHome === undefined ? {} : { explicit: input.requestedLocalHome }),
81
+ ...(input.requestedLocalHomeMode === undefined ? {} : { mode: input.requestedLocalHomeMode }),
82
+ ...(input.pathExists === undefined ? {} : { exists: input.pathExists }),
83
+ });
75
84
  return Object.freeze({
76
85
  kind: "known",
77
86
  profile: legacyProfile,
78
87
  serverUrl: normalizedServerUrl(parsed),
79
- daemonHome: paths.join(input.userHome, ".crew", environment === "production" ? "daemon" : "daemon-dev"),
80
- agentsRoot: paths.join(input.userHome, ".crew", environment === "production" ? "agents" : "agents-dev"),
81
- npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", legacyProfile),
88
+ daemonHome: local.daemonHome,
89
+ agentsRoot: paths.join(local.root, environment === "production" ? "agents" : "agents-dev"),
90
+ npmPrefix: local.runtimePrefix,
82
91
  releaseTag: environment === "dev" ? "beta" : "latest",
83
92
  });
84
93
  }
@@ -99,15 +108,23 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
99
108
  userHome: input.userHome,
100
109
  platform: input.platform,
101
110
  });
111
+ const local = resolveDaemonLocalHome({
112
+ userHome: input.userHome,
113
+ platform: input.platform,
114
+ profile: layout.profile,
115
+ ...(input.requestedLocalHome === undefined ? {} : { explicit: input.requestedLocalHome }),
116
+ ...(input.requestedLocalHomeMode === undefined ? {} : { mode: input.requestedLocalHomeMode }),
117
+ ...(input.pathExists === undefined ? {} : { exists: input.pathExists }),
118
+ });
102
119
  return Object.freeze({
103
120
  kind: "known",
104
121
  environment,
105
122
  localInstanceName,
106
123
  profile: layout.profile,
107
124
  serverUrl: normalizedServerUrl(parsed),
108
- daemonHome: layout.daemonHome,
109
- agentsRoot: layout.agentsRoot,
110
- npmPrefix: layout.npmPrefix,
125
+ daemonHome: local.daemonHome,
126
+ agentsRoot: paths.join(local.root, "agents-" + layout.environment + "-" + layout.localInstanceName),
127
+ npmPrefix: local.runtimePrefix,
111
128
  releaseTag: environment === "dev" ? "beta" : "latest",
112
129
  });
113
130
  }
@@ -127,13 +144,21 @@ export function resolveAutomaticInstallTarget(input) {
127
144
  return registryUrl === undefined ? target : Object.freeze({ ...target, registryUrl });
128
145
  }
129
146
  const profile = requireCustomProfile(hostname, input.requestedProfile);
147
+ const local = resolveDaemonLocalHome({
148
+ userHome: input.userHome,
149
+ platform: input.platform,
150
+ profile,
151
+ ...(input.requestedLocalHome === undefined ? {} : { explicit: input.requestedLocalHome }),
152
+ ...(input.requestedLocalHomeMode === undefined ? {} : { mode: input.requestedLocalHomeMode }),
153
+ ...(input.pathExists === undefined ? {} : { exists: input.pathExists }),
154
+ });
130
155
  return Object.freeze({
131
156
  kind: "custom",
132
157
  profile,
133
158
  serverUrl: normalizedServerUrl(parsed),
134
- daemonHome: paths.join(input.userHome, ".crew", `daemon-${profile}`),
135
- agentsRoot: paths.join(input.userHome, ".crew", `agents-${profile}`),
136
- npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", profile),
159
+ daemonHome: local.daemonHome,
160
+ agentsRoot: paths.join(local.root, `agents-${profile}`),
161
+ npmPrefix: local.runtimePrefix,
137
162
  releaseTag: input.requestedReleaseTag ?? "latest",
138
163
  ...(registryUrl === undefined ? {} : { registryUrl }),
139
164
  });
@@ -1,11 +1,12 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { homedir } from "node:os";
3
- import { resolve } from "node:path";
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
6
  import { buildServiceSpec, doctorService, hardenWindowsProfile, installService, readServiceDescriptor, serviceAction, serviceStatus, systemCommandRunner, uninstallService, upgradeDaemon, windowsProfileIsPrivate, } from "./computer-service.js";
7
7
  import { releaseTagForServerUrl, assertReleaseTagForServerUrl } from "./automatic-install-target.js";
8
8
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
9
+ import { npmInvocation } from "./npm-command.js";
9
10
  import { defaultProcessController } from "./execution-journal.js";
10
11
  import { inspectJournalLock } from "./execution-journal-lock.js";
11
12
  import { detectDaemonLang, formatDaemonText } from "./i18n.js";
@@ -17,6 +18,7 @@ import { assertManagedServiceIdentity, buildManagedServiceRecord, installManaged
17
18
  import { inspectManagedServiceDiagnostics } from "./managed-service-diagnostics.js";
18
19
  import { readManagedServiceRegistry } from "./managed-service-registry.js";
19
20
  import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
21
+ import { resolveDaemonLocalHome, resolveDaemonLocalStateRoot } from "./daemon-local-home.js";
20
22
  const COMPUTER_COMMANDS = new Set(["profile", "doctor", "install", "uninstall", "start", "stop", "restart", "status", "upgrade"]);
21
23
  export function builtDaemonEntry(moduleUrl = import.meta.url) {
22
24
  return moduleUrl.endsWith(".js")
@@ -24,7 +26,7 @@ export function builtDaemonEntry(moduleUrl = import.meta.url) {
24
26
  : "";
25
27
  }
26
28
  function defaults() {
27
- const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
29
+ const npm = npmInvocation(process.platform, process.execPath);
28
30
  return {
29
31
  env: process.env,
30
32
  platform: process.platform,
@@ -35,7 +37,7 @@ function defaults() {
35
37
  runner: systemCommandRunner,
36
38
  ...(process.platform === "win32" ? { profileProtector: windowsDpapiProtector } : {}),
37
39
  resolveGlobalNodeModules: async () => {
38
- const result = await systemCommandRunner(npmCommand, ["root", "--global"]);
40
+ const result = await systemCommandRunner(npm.command, [...npm.argsPrefix, "root", "--global"]);
39
41
  if (result.exitCode !== 0 || !result.stdout.trim()) {
40
42
  throw new Error(`Unable to locate global npm modules: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
41
43
  }
@@ -66,6 +68,7 @@ function usage(td) {
66
68
  " crew-daemon install --server-url <url> --api-key <sk_machine_*> [--profile <custom-name>] [--release-tag <latest|beta>] [--registry <https-url>]",
67
69
  " crew-daemon doctor|install|uninstall|start|stop|restart|status --profile <name>",
68
70
  " crew-daemon upgrade [--profile <name>] [--release-tag <latest|beta>] [--registry <https-url>]",
71
+ " NOWWORK_LOCAL_HOME_MODE=auto|nowwork|legacy NOWWORK_LOCAL_HOME=~/.nowwork",
69
72
  "",
70
73
  td("profile save reads the machine token from CREW_MACHINE_TOKEN, or stdin with --token-stdin."),
71
74
  ].join("\n") + "\n";
@@ -153,6 +156,7 @@ export async function runComputerCommand(argv, overrides = {}) {
153
156
  ?? deps.env.CREW_MACHINE_TOKEN)?.trim();
154
157
  if (!machineToken)
155
158
  throw new Error(td("Missing machine token for automatic installation"));
159
+ const requestedLocalHomeMode = deps.env.NOWWORK_LOCAL_HOME_MODE ?? deps.env.NOWWORK_STORAGE_MODE;
156
160
  const result = await deps.installAutomatically({
157
161
  serverUrl: parsed.values["server-url"],
158
162
  machineToken,
@@ -161,11 +165,34 @@ export async function runComputerCommand(argv, overrides = {}) {
161
165
  ...(configuredRegistryUrl === undefined
162
166
  ? {}
163
167
  : { requestedRegistryUrl: configuredRegistryUrl }),
168
+ ...(deps.env.NOWWORK_LOCAL_HOME === undefined
169
+ ? {}
170
+ : { requestedLocalHome: deps.env.NOWWORK_LOCAL_HOME }),
171
+ ...(requestedLocalHomeMode === undefined ? {} : { requestedLocalHomeMode }),
164
172
  });
165
173
  deps.stdout(`${td("Installed daemon {{version}} for profile '{{profile}}'; service '{{service}}' is running.", { version: result.version, profile: result.profile, service: result.serviceId })}\n`);
166
174
  return 0;
167
175
  }
168
- const home = daemonHome(deps.env);
176
+ const resolvedProfile = profileName ?? positionalName;
177
+ const configuredLocalHomeMode = deps.env.NOWWORK_LOCAL_HOME_MODE ?? deps.env.NOWWORK_STORAGE_MODE;
178
+ const home = resolvedProfile !== undefined
179
+ && (deps.env.NOWWORK_LOCAL_HOME !== undefined
180
+ || deps.env.NOWWORK_LOCAL_HOME_MODE !== undefined
181
+ || deps.env.NOWWORK_STORAGE_MODE !== undefined)
182
+ ? resolveDaemonLocalHome({
183
+ userHome: deps.userHome,
184
+ platform: deps.platform,
185
+ profile: resolvedProfile,
186
+ ...(deps.env.NOWWORK_LOCAL_HOME === undefined ? {} : { explicit: deps.env.NOWWORK_LOCAL_HOME }),
187
+ ...(configuredLocalHomeMode === undefined ? {} : { mode: configuredLocalHomeMode }),
188
+ }).daemonHome
189
+ : daemonHome(deps.env);
190
+ const localHome = resolveDaemonLocalStateRoot({
191
+ userHome: deps.userHome,
192
+ platform: deps.platform,
193
+ explicit: deps.env.NOWWORK_LOCAL_HOME,
194
+ mode: configuredLocalHomeMode,
195
+ });
169
196
  const profileStorage = {
170
197
  platform: deps.platform,
171
198
  userHome: deps.userHome,
@@ -220,7 +247,9 @@ export async function runComputerCommand(argv, overrides = {}) {
220
247
  }),
221
248
  ...(parsed.values["agents-root"]
222
249
  ? { agentsRoot: resolveAgentsRoot(parsed.values["agents-root"], deps.userHome, deps.platform) }
223
- : {}),
250
+ : (deps.env.NOWWORK_LOCAL_HOME !== undefined || configuredLocalHomeMode !== undefined)
251
+ ? { agentsRoot: resolveAgentsRoot((deps.platform === "win32" ? win32 : posix).join((deps.platform === "win32" ? win32 : posix).dirname(home), `agents-${name}`), deps.userHome, deps.platform) }
252
+ : {}),
224
253
  ...(deps.env.PATH ? { runtimePath: deps.env.PATH } : {}),
225
254
  };
226
255
  await saveProfile(profile, home, profileStorage);
@@ -275,11 +304,11 @@ export async function runComputerCommand(argv, overrides = {}) {
275
304
  try {
276
305
  installationLease = installation === null
277
306
  ? null
278
- : await deps.acquireInstallationLease(installation.npmPrefix, { userHome: deps.userHome });
307
+ : await deps.acquireInstallationLease(installation.npmPrefix, { userHome: deps.userHome, localHome });
279
308
  await upgradeDaemon(deps.platform, deps.runner, installation?.npmPrefix, releaseTag, configuredRegistryUrl === undefined
280
309
  ? restartProfile?.registryUrl
281
310
  ?? configuredRegistryUrl
282
- : normalizeDaemonNpmRegistry(configuredRegistryUrl, { allowLoopbackHttp: releaseTag === "beta" }));
311
+ : normalizeDaemonNpmRegistry(configuredRegistryUrl, { allowLoopbackHttp: releaseTag === "beta" }), deps.nodePath);
283
312
  }
284
313
  catch (error) {
285
314
  upgradeError = error;
@@ -357,7 +386,7 @@ export async function runComputerCommand(argv, overrides = {}) {
357
386
  spec,
358
387
  record: managedRecord,
359
388
  runner: deps.runner,
360
- registry: { userHome: deps.userHome },
389
+ registry: { userHome: deps.userHome, localHome },
361
390
  });
362
391
  deps.stdout(`${td("Installed '{{id}}'. Use status to confirm runtime state.", { id: spec.id })}\n`);
363
392
  return 0;
@@ -366,7 +395,7 @@ export async function runComputerCommand(argv, overrides = {}) {
366
395
  if (managedRecord === null)
367
396
  await uninstallService(spec, deps.runner);
368
397
  else {
369
- const current = (await readManagedServiceRegistry({ userHome: deps.userHome })).find((record) => record.serviceId === spec.id
398
+ const current = (await readManagedServiceRegistry({ userHome: deps.userHome, localHome })).find((record) => record.serviceId === spec.id
370
399
  && record.daemonHome === managedRecord.daemonHome
371
400
  && record.profile === name);
372
401
  if (current === undefined) {
@@ -377,7 +406,7 @@ export async function runComputerCommand(argv, overrides = {}) {
377
406
  daemonHome: home,
378
407
  npmPrefix: installation.npmPrefix,
379
408
  runner: deps.runner,
380
- registry: { userHome: deps.userHome },
409
+ registry: { userHome: deps.userHome, localHome },
381
410
  acquireInstallationLease: deps.acquireInstallationLease,
382
411
  });
383
412
  }
@@ -397,7 +426,7 @@ export async function runComputerCommand(argv, overrides = {}) {
397
426
  daemonHome: home,
398
427
  npmPrefix: installation.npmPrefix,
399
428
  runner: deps.runner,
400
- registry: { userHome: deps.userHome },
429
+ registry: { userHome: deps.userHome, localHome },
401
430
  acquireInstallationLease: deps.acquireInstallationLease,
402
431
  });
403
432
  }
@@ -407,7 +436,7 @@ export async function runComputerCommand(argv, overrides = {}) {
407
436
  spec,
408
437
  record: current,
409
438
  runner: deps.runner,
410
- registry: { userHome: deps.userHome },
439
+ registry: { userHome: deps.userHome, localHome },
411
440
  });
412
441
  }
413
442
  }
@@ -419,7 +448,7 @@ export async function runComputerCommand(argv, overrides = {}) {
419
448
  await assertManagedServiceIdentity({
420
449
  spec,
421
450
  record: managedRecord,
422
- registry: { userHome: deps.userHome },
451
+ registry: { userHome: deps.userHome, localHome },
423
452
  });
424
453
  }
425
454
  await serviceAction(spec, command, deps.runner);
@@ -450,7 +479,7 @@ export async function runComputerCommand(argv, overrides = {}) {
450
479
  : await inspectManagedServiceDiagnostics({
451
480
  spec,
452
481
  expectedRecord: managedRecord,
453
- registry: { userHome: deps.userHome },
482
+ registry: { userHome: deps.userHome, localHome },
454
483
  });
455
484
  const checks = [
456
485
  ...serviceChecks,
@@ -9,6 +9,7 @@ import { acquireProfileSaveLock, } from "./computer-profile-lock.js";
9
9
  import { formatDaemonText } from "./i18n.js";
10
10
  import { atomicPrivateWrite } from "./atomic-private-write.js";
11
11
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
12
+ import { resolveDaemonLocalHome } from "./daemon-local-home.js";
12
13
  const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
13
14
  export const MachineTokenSchema = z.string().startsWith("sk_machine_").max(4096);
14
15
  const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
@@ -94,8 +95,20 @@ export class ProfileAgentsRootConflictError extends Error {
94
95
  }
95
96
  const quotePosixArgument = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
96
97
  const quotePowerShellArgument = (value) => `'${value.replaceAll("'", "''")}'`;
97
- export function daemonHome(env = process.env) {
98
- return resolve(env.CREW_DAEMON_HOME ?? resolve(homedir(), ".crew/daemon"));
98
+ export function daemonHome(env = process.env, profile) {
99
+ if (env.CREW_DAEMON_HOME !== undefined)
100
+ return resolve(env.CREW_DAEMON_HOME);
101
+ const mode = env.NOWWORK_LOCAL_HOME_MODE ?? env.NOWWORK_STORAGE_MODE;
102
+ if (profile !== undefined && (env.NOWWORK_LOCAL_HOME !== undefined || mode !== undefined)) {
103
+ return resolveDaemonLocalHome({
104
+ userHome: homedir(),
105
+ platform: process.platform,
106
+ profile,
107
+ ...(env.NOWWORK_LOCAL_HOME === undefined ? {} : { explicit: env.NOWWORK_LOCAL_HOME }),
108
+ ...(mode === undefined ? {} : { mode }),
109
+ }).daemonHome;
110
+ }
111
+ return resolve(homedir(), ".crew/daemon");
99
112
  }
100
113
  export function resolveAgentsRoot(configured, userHome = homedir(), platform = process.platform) {
101
114
  const pathApi = platform === "win32" ? win32 : posix;
@@ -5,6 +5,8 @@ import { createHash, randomUUID } from "node:crypto";
5
5
  import { promisify } from "node:util";
6
6
  import { dirname, resolve } from "node:path";
7
7
  import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
8
+ import { daemonRegistryCandidates } from "./daemon-registry.js";
9
+ import { npmInvocation } from "./npm-command.js";
8
10
  const execFileAsync = promisify(execFile);
9
11
  const WINDOWS_STOP_TIMEOUT_MS = 5_000;
10
12
  const WINDOWS_STOP_POLL_MS = 100;
@@ -542,10 +544,12 @@ export async function doctorService(spec, profilePrivate, runner = systemCommand
542
544
  { name: "service-running", ok: status.running === true, detail: status.running ? "running" : "not running" },
543
545
  ];
544
546
  }
545
- export function buildUpgradeCommand(platform, npmPrefix, releaseTag = "latest", registryUrl) {
547
+ export function buildUpgradeCommand(platform, npmPrefix, releaseTag = "latest", registryUrl, nodePath = process.execPath) {
548
+ const npm = npmInvocation(platform, nodePath);
546
549
  return [
547
- platform === "win32" ? "npm.cmd" : "npm",
550
+ npm.command,
548
551
  [
552
+ ...npm.argsPrefix,
549
553
  "install",
550
554
  "--global",
551
555
  "--ignore-scripts",
@@ -557,9 +561,26 @@ export function buildUpgradeCommand(platform, npmPrefix, releaseTag = "latest",
557
561
  ],
558
562
  ];
559
563
  }
560
- export async function upgradeDaemon(platform, runner = systemCommandRunner, npmPrefix, releaseTag = "latest", registryUrl) {
561
- const [command, args] = buildUpgradeCommand(platform, npmPrefix, releaseTag, registryUrl);
562
- await checked(runner, command, args, "Daemon upgrade");
564
+ export async function upgradeDaemon(platform, runner = systemCommandRunner, npmPrefix, releaseTag = "latest", registryUrl, nodePath = process.execPath) {
565
+ const registries = daemonRegistryCandidates({
566
+ betaUpdate: releaseTag === "beta",
567
+ ...(registryUrl === undefined ? {} : { registryUrl }),
568
+ env: process.env,
569
+ });
570
+ if (registries === null)
571
+ throw new Error("Daemon upgrade failed: invalid registry");
572
+ let lastFailure = "";
573
+ for (const registry of registries) {
574
+ const [command, args] = buildUpgradeCommand(platform, npmPrefix, releaseTag, registry, nodePath);
575
+ try {
576
+ await checked(runner, command, args, "Daemon upgrade");
577
+ return;
578
+ }
579
+ catch (error) {
580
+ lastFailure = error instanceof Error ? error.message : String(error);
581
+ }
582
+ }
583
+ throw new Error(lastFailure || "Daemon upgrade failed");
563
584
  }
564
585
  const WINDOWS_SID_SCRIPT = "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value";
565
586
  export async function hardenWindowsProfile(path, runner = systemCommandRunner) {
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** daemon 配置:从 env 读取,解析 crew CLI 路径 (注入给 agent 的 wrapper 用)。 */
2
2
  import { fileURLToPath } from "node:url";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
  import { createRequire } from "node:module";
6
6
  import { detectDaemonLang, translateDaemon } from "./i18n.js";
@@ -28,6 +28,23 @@ export const MIN_EXECUTION_EVENT_BYTES = 512;
28
28
  export const MAX_EXECUTION_TIMEOUT_MS = 2_147_483_647;
29
29
  export class ConfigError extends Error {
30
30
  }
31
+ /**
32
+ * Resolve the shared workspace repository root from the daemon's agent root.
33
+ *
34
+ * Standard NowWork storage keeps `agent-items`, `daemon`, `repos`, and the QA
35
+ * report hub as siblings. Legacy/standalone profiles may still point at an
36
+ * arbitrary agents directory, for which the historical `<agentsRoot>/repos`
37
+ * layout remains the compatible fallback.
38
+ */
39
+ export function resolveReposRoot(agentsRoot, configured) {
40
+ const explicit = configured?.trim();
41
+ if (explicit)
42
+ return resolve(explicit);
43
+ const resolvedAgentsRoot = resolve(agentsRoot);
44
+ return basename(resolvedAgentsRoot) === "agent-items"
45
+ ? join(dirname(resolvedAgentsRoot), "repos")
46
+ : join(resolvedAgentsRoot, "repos");
47
+ }
31
48
  function positiveIntegerEnv(env, field, fallback) {
32
49
  const raw = env[field];
33
50
  const value = raw === undefined ? fallback : Number(raw);
@@ -59,11 +76,11 @@ function defaultCliPath() {
59
76
  return resolve(here, "../../cli/dist/main.js");
60
77
  }
61
78
  }
62
- // 临时 dev 验收名单。测试完成后删除此常量,恢复未配置时默认关闭的行为。
79
+ // 临时 dev 验收名单。这里必须填写 Server 生成的 Agent handle,而不是显示名称或频道 ID。
80
+ // 测试完成后删除此常量,恢复未配置时默认关闭的行为。
63
81
  const TEMP_LOCAL_MEMORY_V2_TEST_HANDLES = [
64
- "memory-v2-test-a",
65
- "memory-v2-test-b",
66
- "memory-v2-test-c",
82
+ "94533bf3-b5b7-4a1e-9c41-6a1ab0a0e1ea", // memory-v2-test-a
83
+ "967ab9ce-724c-4f22-a664-2f855cd798af", // memory-v2-test-b
67
84
  ];
68
85
  function isTemporaryDevServer(serverUrl) {
69
86
  try {
@@ -136,16 +153,19 @@ export function loadConfig(env = process.env) {
136
153
  });
137
154
  const agentsRoot = resolveAgentsRoot(env.CREW_AGENTS_ROOT, homedir());
138
155
  const configuredReposRoot = env.NOWWORK_REPOS_ROOT?.trim();
139
- const reposRoot = configuredReposRoot ? resolve(configuredReposRoot) : undefined;
156
+ const reposRoot = resolveReposRoot(agentsRoot, configuredReposRoot);
140
157
  const configuredQaReportRoot = env.QA_REPORT_HUB?.trim();
158
+ const defaultQaReportRoot = configuredReposRoot
159
+ ? join(dirname(reposRoot), "qa-report-server-v2")
160
+ : join(dirname(agentsRoot), "qa-report-server-v2");
141
161
  return {
142
162
  serverUrl,
143
163
  machineToken,
144
164
  agentsRoot,
145
- ...(reposRoot === undefined ? {} : { reposRoot }),
165
+ reposRoot,
146
166
  qaReportRoot: configuredQaReportRoot
147
167
  ? resolve(configuredQaReportRoot)
148
- : join(dirname(reposRoot ?? agentsRoot), "qa-report-server-v2"),
168
+ : defaultQaReportRoot,
149
169
  cliPath: env.CREW_CLI_PATH ?? defaultCliPath(),
150
170
  runtimeBin: env.CREW_RUNTIME ?? "claude",
151
171
  codexHomeMode: codexHomeModeEnv(env),
@@ -1,18 +1,12 @@
1
1
  import { executionBackendCapability } from "./execution-backend.js";
2
2
  import { daemonCapabilities, EXECUTION_PROTOCOL } from "./machine-info.js";
3
- import { PROJECT_SKILLS_CAPABILITY, PROJECT_SKILL_PROJECTION_V2_CAPABILITY, } from "./project-skills/types.js";
4
- export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe, projectSkillsAvailable = true, capabilities = daemonCapabilities(runtimePlatform)) {
3
+ export function buildControlPlaneUrl(serverUrl, machineToken, runtimePlatform = process.platform, jobObjectProbe, capabilities = daemonCapabilities(runtimePlatform)) {
5
4
  const query = new URLSearchParams({ key: machineToken });
6
5
  if (executionBackendCapability(runtimePlatform, jobObjectProbe).supported) {
7
6
  query.set("execution_min", String(EXECUTION_PROTOCOL.min));
8
7
  query.set("execution_max", String(EXECUTION_PROTOCOL.max));
9
8
  }
10
- for (const capability of capabilities) {
11
- if (projectSkillsAvailable
12
- || (capability !== PROJECT_SKILLS_CAPABILITY
13
- && capability !== PROJECT_SKILL_PROJECTION_V2_CAPABILITY)) {
14
- query.append("capability", capability);
15
- }
16
- }
9
+ for (const capability of capabilities)
10
+ query.append("capability", capability);
17
11
  return `${serverUrl.replace(/^http/, "ws").replace(/\/+$/, "")}/daemon/connect?${query.toString()}`;
18
12
  }