@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.
- package/README.md +27 -0
- package/dist/automatic-daemon-installer.js +44 -24
- package/dist/automatic-install-target.js +34 -9
- package/dist/computer-cli.js +43 -14
- package/dist/computer-profile.js +15 -2
- package/dist/computer-service.js +26 -5
- package/dist/config.js +28 -8
- package/dist/control-plane-url.js +3 -9
- package/dist/daemon-installation-lease.js +7 -4
- package/dist/daemon-local-home.js +108 -0
- package/dist/daemon-migration-controller.js +15 -4
- package/dist/daemon-registry.js +20 -0
- package/dist/daemon-update-controller.js +1 -0
- package/dist/daemon-update-eligibility.js +17 -7
- package/dist/daemon-updater.js +26 -26
- package/dist/execution-runner.js +7 -7
- package/dist/host-execution-coordinator.js +4 -3
- package/dist/local-executor.js +23 -17
- package/dist/local-memory/runtime.js +5 -3
- package/dist/machine-info.js +3 -0
- package/dist/main.js +13 -2
- package/dist/managed-service-diagnostics.js +1 -0
- package/dist/managed-service-registry.js +12 -7
- package/dist/managed-service-startup.js +4 -1
- package/dist/npm-command.js +9 -0
- package/dist/project-skills/controller.js +6 -71
- package/dist/project-skills/execution-adapter.js +7 -0
- package/dist/project-skills/execution-projection.js +163 -0
- package/dist/project-skills/initialized-reconciler.js +10 -8
- package/dist/project-skills/legacy-cleanup.js +224 -0
- package/dist/project-skills/machine-hello-publisher.js +20 -0
- package/dist/project-skills/reconciler.js +114 -500
- package/dist/project-skills/resolution-records.js +63 -0
- package/dist/project-skills/runtime-launch.js +0 -42
- package/dist/project-skills/runtime-warning.js +1 -3
- package/dist/project-skills/scanner.js +1 -17
- package/dist/project-skills/startup-cleanup.js +24 -0
- package/dist/runner.js +2 -1
- package/dist/serve.js +49 -49
- package/dist/slog.js +14 -1
- package/dist/storage-control.js +12 -1
- package/dist/storage-identity.js +15 -1
- package/dist/storage-migration-controller.js +25 -4
- package/dist/storage-ownership.js +55 -0
- package/dist/storage-runtime-controller.js +29 -7
- package/dist/storage-runtime-readiness.js +26 -4
- package/dist/storage-runtime.js +18 -2
- package/dist/storage-status-delivery.js +4 -0
- package/dist/storage-status-replay.js +16 -1
- package/dist/storage-worker-executor.js +1 -0
- package/dist/storage-worker-operation.js +4 -0
- 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 {
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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:
|
|
80
|
-
agentsRoot: paths.join(
|
|
81
|
-
npmPrefix:
|
|
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:
|
|
109
|
-
agentsRoot: layout.
|
|
110
|
-
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:
|
|
135
|
-
agentsRoot: paths.join(
|
|
136
|
-
npmPrefix:
|
|
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
|
});
|
package/dist/computer-cli.js
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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,
|
package/dist/computer-profile.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/computer-service.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
562
|
-
|
|
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 =
|
|
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
|
-
|
|
165
|
+
reposRoot,
|
|
146
166
|
qaReportRoot: configuredQaReportRoot
|
|
147
167
|
? resolve(configuredQaReportRoot)
|
|
148
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|