@nowcrew/daemon 0.6.73 → 0.6.75
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 +22 -11
- package/dist/automatic-daemon-installer.js +19 -2
- package/dist/automatic-install-target.js +31 -1
- package/dist/computer-cli.js +59 -7
- package/dist/computer-profile.js +40 -4
- package/dist/computer-service.js +9 -4
- package/dist/daemon-migration-controller.js +3 -0
- package/dist/daemon-release-version.js +22 -0
- package/dist/daemon-update-controller.js +3 -2
- package/dist/daemon-updater.js +62 -2
- package/dist/external-output.js +34 -4
- package/dist/i18n.js +3 -1
- package/dist/list-models.js +1 -17
- package/dist/local-executor.js +14 -34
- package/dist/machine-info.js +10 -0
- package/dist/main.js +12 -3
- package/dist/memory-prune-diagnostics.js +33 -0
- package/dist/npm-registry.js +32 -0
- package/dist/release-channel-guard.js +60 -0
- package/dist/release-provenance.js +87 -0
- package/dist/runtimes/claude-models.js +123 -0
- package/dist/runtimes/codex-app-server-runner.js +33 -6
- package/dist/serve.js +7 -7
- package/dist/slog.js +4 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -11,6 +11,13 @@ For a temporary foreground connection, run:
|
|
|
11
11
|
npx @nowcrew/daemon@latest --server-url https://nowwork.example --api-key sk_machine_...
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
The package has two release channels. The `in` environment consumes stable `x.y.z` releases through
|
|
15
|
+
the `latest` tag; the `dev` environment consumes `x.y.z-beta.n` prereleases through the `beta` tag.
|
|
16
|
+
The parser also accepts the historical `x.y.z-beta-n` spelling and normalizes it for ordering; new
|
|
17
|
+
published versions should use the dotted SemVer form.
|
|
18
|
+
Never use `latest` for a dev installation or `beta` for an in installation. See the
|
|
19
|
+
[Daemon Release Runbook](../docs/releases/daemon-release-runbook.md) for release and rollback procedures.
|
|
20
|
+
|
|
14
21
|
This compatibility form stays resident in the current terminal and reconnects with backoff. New
|
|
15
22
|
**Add Computer** commands use the managed automatic installation described below. For local development, run
|
|
16
23
|
`pnpm --filter @nowcrew/daemon daemon`. `crew-daemon run --agent <handle> --channel <id>` is the legacy
|
|
@@ -22,7 +29,7 @@ Persist a named connection without putting its machine token in shell history or
|
|
|
22
29
|
|
|
23
30
|
```bash
|
|
24
31
|
printf '%s' "$CREW_MACHINE_TOKEN" | crew-daemon profile save work \
|
|
25
|
-
--server-url https://nowwork.example --token-stdin
|
|
32
|
+
--server-url https://nowwork.example --release-tag latest --token-stdin
|
|
26
33
|
crew-daemon profile list
|
|
27
34
|
crew-daemon profile show work # reports configured=true; never prints the token
|
|
28
35
|
crew-daemon serve --profile work
|
|
@@ -36,8 +43,9 @@ The connection command generated by **Add Computer** installs the package into a
|
|
|
36
43
|
saves the private profile, installs the native user service, starts it, and verifies that it is running:
|
|
37
44
|
|
|
38
45
|
```bash
|
|
39
|
-
npx -y --registry https://registry.npmjs.org @nowcrew/daemon@
|
|
40
|
-
--server-url https://nowwork-dev.nowcoder.com --api-key sk_machine_...
|
|
46
|
+
npx -y --registry https://registry.npmjs.org @nowcrew/daemon@beta install \
|
|
47
|
+
--server-url https://nowwork-dev.nowcoder.com --api-key sk_machine_... \
|
|
48
|
+
--release-tag beta --registry https://registry.npmjs.org
|
|
41
49
|
```
|
|
42
50
|
|
|
43
51
|
The installer selects the fixed `in` layout for `nowwork-in.nowcoder.com` and the fixed `dev` layout for
|
|
@@ -45,8 +53,8 @@ The installer selects the fixed `in` layout for `nowwork-in.nowcoder.com` and th
|
|
|
45
53
|
An unknown hostname is rejected before any write and reports how to retry with an explicit custom profile:
|
|
46
54
|
|
|
47
55
|
```bash
|
|
48
|
-
npx -y --registry https://registry.npmjs.org @nowcrew/daemon@
|
|
49
|
-
--server-url https://lab.example.com --api-key sk_machine_... --profile lab
|
|
56
|
+
npx -y --registry https://registry.npmjs.org @nowcrew/daemon@beta install \
|
|
57
|
+
--server-url https://lab.example.com --api-key sk_machine_... --profile lab --release-tag beta
|
|
50
58
|
```
|
|
51
59
|
|
|
52
60
|
Custom profiles use `~/.crew/daemon-<profile>`, `~/.crew/agents-<profile>`, and
|
|
@@ -95,13 +103,16 @@ Upgrade the global daemon package, optionally restarting one installed profile a
|
|
|
95
103
|
```bash
|
|
96
104
|
crew-daemon upgrade
|
|
97
105
|
crew-daemon upgrade --profile work
|
|
106
|
+
crew-daemon upgrade --profile work --release-tag beta
|
|
98
107
|
```
|
|
99
108
|
|
|
100
109
|
With `--profile`, upgrade validates isolation, stops the native service, acquires the installation lease,
|
|
101
110
|
replaces only the entrypoint-derived npm prefix, releases the lease, and starts the service. If package
|
|
102
111
|
replacement fails, it still attempts to start the previous installation. Without `--profile`, upgrade
|
|
103
|
-
refuses to replace a prefix currently leased by a running daemon;
|
|
104
|
-
|
|
112
|
+
refuses to replace a prefix currently leased by a running daemon; it also permits only the currently
|
|
113
|
+
installed daemon's release channel. Cross-channel upgrades (for example installing `beta` from a stable
|
|
114
|
+
global package) require `--profile`, so the target server environment and profile isolation are verified.
|
|
115
|
+
Use the profile form for any managed service.
|
|
105
116
|
|
|
106
117
|
## Managed Self-Update
|
|
107
118
|
|
|
@@ -171,7 +182,7 @@ daemon home also contributes a stable hash to the native service ID, so equal pr
|
|
|
171
182
|
|
|
172
183
|
Before replacing files, the daemon refuses new work, requires zero local active or queued work, and
|
|
173
184
|
non-blockingly acquires every host execution slot. It then installs only the server-selected exact
|
|
174
|
-
`x.y.z` version with npm using the prefix derived from the running global entrypoint, verifies the installed
|
|
185
|
+
exact `x.y.z` or `x.y.z-beta.n` version with npm using the prefix derived from the running global entrypoint, verifies the installed
|
|
175
186
|
package version, and schedules the existing native service restart from a detached helper. macOS uses
|
|
176
187
|
`launchctl kickstart -k` to avoid the `bootout`/`bootstrap` race; Linux uses `systemctl --user --no-block
|
|
177
188
|
restart`. The helper reports manager failure over IPC. A failed handoff releases both execution barriers and
|
|
@@ -195,11 +206,11 @@ Failures are recoverable and keep a bounded code in the machine record:
|
|
|
195
206
|
| `update_in_progress` | wait for the active request to reach a terminal state |
|
|
196
207
|
| `update_timeout` | inspect `crew-daemon status --profile work`, restart if needed, then retry |
|
|
197
208
|
|
|
198
|
-
|
|
199
|
-
the service manually:
|
|
209
|
+
The Web UI supports owner/admin rollback to an exact release in the configured channel. When the managed
|
|
210
|
+
control plane is unavailable, install a known exact release and restart the service manually:
|
|
200
211
|
|
|
201
212
|
```bash
|
|
202
|
-
npm install --global --prefix <environment-prefix> --ignore-scripts --no-audit --no-fund @nowcrew/daemon@<previous-
|
|
213
|
+
npm install --global --prefix <environment-prefix> --ignore-scripts --no-audit --no-fund @nowcrew/daemon@<previous-release>
|
|
203
214
|
crew-daemon restart --profile work
|
|
204
215
|
crew-daemon status --profile work
|
|
205
216
|
```
|
|
@@ -8,6 +8,9 @@ import { ManagedServiceConflictError, managedServiceConflict, managedServiceIden
|
|
|
8
8
|
import { resolveAutomaticInstallTarget, } from "./automatic-install-target.js";
|
|
9
9
|
import { acquireDaemonInstallationLease, } from "./daemon-installation-lease.js";
|
|
10
10
|
import { daemonVersion } from "./machine-info.js";
|
|
11
|
+
import { daemonReleaseProvenance } from "./release-provenance.js";
|
|
12
|
+
import { daemonReleaseChannel, isDaemonReleaseVersion } from "./daemon-release-version.js";
|
|
13
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
11
14
|
import { discoverExactLegacyService, } from "./automatic-service-migration.js";
|
|
12
15
|
function profileStorage(platform, userHome, runner) {
|
|
13
16
|
return {
|
|
@@ -175,7 +178,7 @@ export function stableDaemonEntry(target, platform) {
|
|
|
175
178
|
? paths.join(target.npmPrefix, "node_modules", "@nowcrew", "daemon", "dist", "main.js")
|
|
176
179
|
: paths.join(target.npmPrefix, "lib", "node_modules", "@nowcrew", "daemon", "dist", "main.js");
|
|
177
180
|
}
|
|
178
|
-
export async function installExactDaemonPackage(npmPrefix, version, platform, runner) {
|
|
181
|
+
export async function installExactDaemonPackage(npmPrefix, version, platform, runner, registryUrl) {
|
|
179
182
|
const command = platform === "win32" ? "npm.cmd" : "npm";
|
|
180
183
|
const result = await runner(command, [
|
|
181
184
|
"install",
|
|
@@ -185,6 +188,9 @@ export async function installExactDaemonPackage(npmPrefix, version, platform, ru
|
|
|
185
188
|
"--ignore-scripts",
|
|
186
189
|
"--no-audit",
|
|
187
190
|
"--no-fund",
|
|
191
|
+
...(registryUrl === undefined ? [] : ["--registry", normalizeDaemonNpmRegistry(registryUrl, {
|
|
192
|
+
allowLoopbackHttp: daemonReleaseChannel(version) === "beta",
|
|
193
|
+
})]),
|
|
188
194
|
`@nowcrew/daemon@${version}`,
|
|
189
195
|
]);
|
|
190
196
|
if (result.exitCode !== 0) {
|
|
@@ -217,9 +223,18 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
217
223
|
const target = resolveAutomaticInstallTarget({
|
|
218
224
|
serverUrl: input.serverUrl,
|
|
219
225
|
...(input.requestedProfile === undefined ? {} : { requestedProfile: input.requestedProfile }),
|
|
226
|
+
...(input.requestedReleaseTag === undefined ? {} : { requestedReleaseTag: input.requestedReleaseTag }),
|
|
227
|
+
...(input.requestedRegistryUrl === undefined ? {} : { requestedRegistryUrl: input.requestedRegistryUrl }),
|
|
220
228
|
userHome: dependencies.userHome,
|
|
221
229
|
platform: dependencies.platform,
|
|
222
230
|
});
|
|
231
|
+
if (!isDaemonReleaseVersion(dependencies.version)) {
|
|
232
|
+
throw new Error(`Invoking daemon package has an invalid release version '${dependencies.version}'`);
|
|
233
|
+
}
|
|
234
|
+
const invokingTag = daemonReleaseProvenance(dependencies.version).releaseTag;
|
|
235
|
+
if (invokingTag !== target.releaseTag) {
|
|
236
|
+
throw new Error(`Invoking daemon release tag '${invokingTag}' does not match target '${target.releaseTag}'`);
|
|
237
|
+
}
|
|
223
238
|
const spec = buildServiceSpec({
|
|
224
239
|
platform: dependencies.platform,
|
|
225
240
|
profile: target.profile,
|
|
@@ -251,7 +266,7 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
251
266
|
const lease = await dependencies.acquireInstallationLease(target.npmPrefix);
|
|
252
267
|
let installFailure;
|
|
253
268
|
try {
|
|
254
|
-
await dependencies.installPackage(target.npmPrefix, dependencies.version, dependencies.platform, dependencies.runner);
|
|
269
|
+
await dependencies.installPackage(target.npmPrefix, dependencies.version, dependencies.platform, dependencies.runner, target.registryUrl);
|
|
255
270
|
}
|
|
256
271
|
catch (error) {
|
|
257
272
|
installFailure = error;
|
|
@@ -280,6 +295,8 @@ export async function installDaemonAutomatically(input, dependencies = defaultDe
|
|
|
280
295
|
serverUrl: target.serverUrl,
|
|
281
296
|
machineToken,
|
|
282
297
|
agentsRoot: target.agentsRoot,
|
|
298
|
+
releaseTag: target.releaseTag,
|
|
299
|
+
...(target.registryUrl === undefined ? {} : { registryUrl: target.registryUrl }),
|
|
283
300
|
...(dependencies.runtimePath === undefined ? {} : { runtimePath: dependencies.runtimePath }),
|
|
284
301
|
}, target.daemonHome);
|
|
285
302
|
serviceReplacementStarted = true;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { posix, win32 } from "node:path";
|
|
2
2
|
import { profileLayout, validateLocalInstanceName } from "./profile-layout.js";
|
|
3
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
3
4
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
4
5
|
const KNOWN_TARGETS = {
|
|
5
6
|
"nowwork-in.nowcoder.com": {
|
|
@@ -13,6 +14,23 @@ const KNOWN_TARGETS = {
|
|
|
13
14
|
agentsDirectory: "agents-dev",
|
|
14
15
|
},
|
|
15
16
|
};
|
|
17
|
+
export function releaseTagForServerUrl(serverUrl) {
|
|
18
|
+
const parsed = parseServerUrl(serverUrl);
|
|
19
|
+
const known = KNOWN_TARGETS[parsed.hostname.toLowerCase()];
|
|
20
|
+
return known?.environment === "dev" ? "beta" : "latest";
|
|
21
|
+
}
|
|
22
|
+
export function assertReleaseTagForServerUrl(serverUrl, requested) {
|
|
23
|
+
if (requested === undefined)
|
|
24
|
+
return;
|
|
25
|
+
const parsed = parseServerUrl(serverUrl);
|
|
26
|
+
const known = KNOWN_TARGETS[parsed.hostname.toLowerCase()];
|
|
27
|
+
if (known === undefined)
|
|
28
|
+
return;
|
|
29
|
+
const expected = known.environment === "dev" ? "beta" : "latest";
|
|
30
|
+
if (requested !== expected) {
|
|
31
|
+
throw new Error(`Server hostname '${parsed.hostname}' requires release tag '${expected}'`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
16
34
|
function parseServerUrl(value) {
|
|
17
35
|
let parsed;
|
|
18
36
|
try {
|
|
@@ -52,6 +70,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
52
70
|
// Keep the no-flag target readable by old daemons; any workspace-scoped profile below
|
|
53
71
|
// uses the canonical environment token (production, never in).
|
|
54
72
|
const legacyProfile = environment === "production" ? "in" : environment;
|
|
73
|
+
assertReleaseTagForServerUrl(input.serverUrl, input.requestedReleaseTag);
|
|
55
74
|
if (requestedProfile === undefined || requestedProfile === legacyProfile) {
|
|
56
75
|
return Object.freeze({
|
|
57
76
|
kind: "known",
|
|
@@ -60,6 +79,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
60
79
|
daemonHome: paths.join(input.userHome, ".crew", environment === "production" ? "daemon" : "daemon-dev"),
|
|
61
80
|
agentsRoot: paths.join(input.userHome, ".crew", environment === "production" ? "agents" : "agents-dev"),
|
|
62
81
|
npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", legacyProfile),
|
|
82
|
+
releaseTag: environment === "dev" ? "beta" : "latest",
|
|
63
83
|
});
|
|
64
84
|
}
|
|
65
85
|
if (requestedProfile === "in" || requestedProfile === "dev") {
|
|
@@ -88,6 +108,7 @@ function knownTargetForProfile(environment, requestedProfile, input, parsed) {
|
|
|
88
108
|
daemonHome: layout.daemonHome,
|
|
89
109
|
agentsRoot: layout.agentsRoot,
|
|
90
110
|
npmPrefix: layout.npmPrefix,
|
|
111
|
+
releaseTag: environment === "dev" ? "beta" : "latest",
|
|
91
112
|
});
|
|
92
113
|
}
|
|
93
114
|
export function resolveAutomaticInstallTarget(input) {
|
|
@@ -95,8 +116,15 @@ export function resolveAutomaticInstallTarget(input) {
|
|
|
95
116
|
const hostname = parsed.hostname.toLowerCase();
|
|
96
117
|
const paths = input.platform === "win32" ? win32 : posix;
|
|
97
118
|
const known = KNOWN_TARGETS[hostname];
|
|
119
|
+
const targetReleaseTag = known?.environment === "dev"
|
|
120
|
+
? "beta"
|
|
121
|
+
: input.requestedReleaseTag ?? "latest";
|
|
122
|
+
const registryUrl = input.requestedRegistryUrl === undefined
|
|
123
|
+
? undefined
|
|
124
|
+
: normalizeDaemonNpmRegistry(input.requestedRegistryUrl, { allowLoopbackHttp: targetReleaseTag === "beta" });
|
|
98
125
|
if (known !== undefined) {
|
|
99
|
-
|
|
126
|
+
const target = knownTargetForProfile(known.environment, input.requestedProfile, input, parsed);
|
|
127
|
+
return registryUrl === undefined ? target : Object.freeze({ ...target, registryUrl });
|
|
100
128
|
}
|
|
101
129
|
const profile = requireCustomProfile(hostname, input.requestedProfile);
|
|
102
130
|
return Object.freeze({
|
|
@@ -106,5 +134,7 @@ export function resolveAutomaticInstallTarget(input) {
|
|
|
106
134
|
daemonHome: paths.join(input.userHome, ".crew", `daemon-${profile}`),
|
|
107
135
|
agentsRoot: paths.join(input.userHome, ".crew", `agents-${profile}`),
|
|
108
136
|
npmPrefix: paths.join(input.userHome, ".crew", "daemon-runtimes", profile),
|
|
137
|
+
releaseTag: input.requestedReleaseTag ?? "latest",
|
|
138
|
+
...(registryUrl === undefined ? {} : { registryUrl }),
|
|
109
139
|
});
|
|
110
140
|
}
|
package/dist/computer-cli.js
CHANGED
|
@@ -4,10 +4,14 @@ import { resolve } 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
|
+
import { releaseTagForServerUrl, assertReleaseTagForServerUrl } from "./automatic-install-target.js";
|
|
8
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
7
9
|
import { defaultProcessController } from "./execution-journal.js";
|
|
8
10
|
import { inspectJournalLock } from "./execution-journal-lock.js";
|
|
9
11
|
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
10
12
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
13
|
+
import { daemonVersion } from "./machine-info.js";
|
|
14
|
+
import { daemonReleaseProvenance } from "./release-provenance.js";
|
|
11
15
|
import { acquireDaemonInstallationLease, } from "./daemon-installation-lease.js";
|
|
12
16
|
import { assertManagedServiceIdentity, buildManagedServiceRecord, installManagedService, uninstallUnregisteredManagedService, uninstallManagedService, } from "./managed-service-lifecycle.js";
|
|
13
17
|
import { inspectManagedServiceDiagnostics } from "./managed-service-diagnostics.js";
|
|
@@ -57,11 +61,11 @@ export function isGlobalDaemonEntry(entryPath, globalNodeModules) {
|
|
|
57
61
|
function usage(td) {
|
|
58
62
|
return [
|
|
59
63
|
td("Computer lifecycle:"),
|
|
60
|
-
" crew-daemon profile save <name> --server-url <url> [--agents-root <path>] [--token-stdin]",
|
|
64
|
+
" crew-daemon profile save <name> --server-url <url> [--release-tag <latest|beta>] [--registry <https-url>] [--agents-root <path>] [--token-stdin]",
|
|
61
65
|
" crew-daemon profile list|show|remove [<name>]",
|
|
62
|
-
" crew-daemon install --server-url <url> --api-key <sk_machine_*> [--profile <custom-name>]",
|
|
66
|
+
" crew-daemon install --server-url <url> --api-key <sk_machine_*> [--profile <custom-name>] [--release-tag <latest|beta>] [--registry <https-url>]",
|
|
63
67
|
" crew-daemon doctor|install|uninstall|start|stop|restart|status --profile <name>",
|
|
64
|
-
" crew-daemon upgrade [--profile <name>]",
|
|
68
|
+
" crew-daemon upgrade [--profile <name>] [--release-tag <latest|beta>] [--registry <https-url>]",
|
|
65
69
|
"",
|
|
66
70
|
td("profile save reads the machine token from CREW_MACHINE_TOKEN, or stdin with --token-stdin."),
|
|
67
71
|
].join("\n") + "\n";
|
|
@@ -105,6 +109,8 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
105
109
|
options: {
|
|
106
110
|
profile: { type: "string" },
|
|
107
111
|
"server-url": { type: "string" },
|
|
112
|
+
"release-tag": { type: "string" },
|
|
113
|
+
registry: { type: "string" },
|
|
108
114
|
"agents-root": { type: "string" },
|
|
109
115
|
"token-stdin": { type: "boolean", default: false },
|
|
110
116
|
"api-key": { type: "string" },
|
|
@@ -118,6 +124,14 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
118
124
|
}
|
|
119
125
|
const [, action, positionalName] = parsed.positionals;
|
|
120
126
|
const profileName = parsed.values.profile;
|
|
127
|
+
const requestedReleaseTag = parsed.values["release-tag"];
|
|
128
|
+
const requestedRegistryUrl = parsed.values.registry;
|
|
129
|
+
const configuredRegistryUrl = requestedRegistryUrl === undefined
|
|
130
|
+
? deps.env.CREW_NPM_REGISTRY?.trim() || undefined
|
|
131
|
+
: requestedRegistryUrl;
|
|
132
|
+
if (requestedReleaseTag !== undefined && requestedReleaseTag !== "latest" && requestedReleaseTag !== "beta") {
|
|
133
|
+
throw new Error(td("Invalid release tag; expected latest or beta"));
|
|
134
|
+
}
|
|
121
135
|
const automaticInstall = command === "install" && parsed.values["server-url"] !== undefined;
|
|
122
136
|
const tokenOption = parsed.values["api-key"] !== undefined
|
|
123
137
|
? "--api-key"
|
|
@@ -143,6 +157,10 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
143
157
|
serverUrl: parsed.values["server-url"],
|
|
144
158
|
machineToken,
|
|
145
159
|
...(profileName === undefined ? {} : { requestedProfile: profileName }),
|
|
160
|
+
...(requestedReleaseTag === undefined ? {} : { requestedReleaseTag }),
|
|
161
|
+
...(configuredRegistryUrl === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { requestedRegistryUrl: configuredRegistryUrl }),
|
|
146
164
|
});
|
|
147
165
|
deps.stdout(`${td("Installed daemon {{version}} for profile '{{profile}}'; service '{{service}}' is running.", { version: result.version, profile: result.profile, service: result.serviceId })}\n`);
|
|
148
166
|
return 0;
|
|
@@ -187,10 +205,19 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
187
205
|
const machineToken = (fromStdin ? await deps.readStdin() : deps.env.CREW_MACHINE_TOKEN)?.trim();
|
|
188
206
|
if (!machineToken)
|
|
189
207
|
throw new Error(td("Missing machine token; use CREW_MACHINE_TOKEN or --token-stdin"));
|
|
208
|
+
const serverUrl = requireValue(parsed.values["server-url"], "--server-url", td);
|
|
209
|
+
assertReleaseTagForServerUrl(serverUrl, requestedReleaseTag);
|
|
210
|
+
const profileReleaseTag = requestedReleaseTag ?? releaseTagForServerUrl(serverUrl);
|
|
190
211
|
const profile = {
|
|
191
212
|
name,
|
|
192
|
-
serverUrl
|
|
213
|
+
serverUrl,
|
|
193
214
|
machineToken,
|
|
215
|
+
releaseTag: profileReleaseTag,
|
|
216
|
+
...(configuredRegistryUrl === undefined ? {} : {
|
|
217
|
+
registryUrl: normalizeDaemonNpmRegistry(configuredRegistryUrl, {
|
|
218
|
+
allowLoopbackHttp: profileReleaseTag === "beta",
|
|
219
|
+
}),
|
|
220
|
+
}),
|
|
194
221
|
...(parsed.values["agents-root"]
|
|
195
222
|
? { agentsRoot: resolveAgentsRoot(parsed.values["agents-root"], deps.userHome, deps.platform) }
|
|
196
223
|
: {}),
|
|
@@ -205,6 +232,19 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
205
232
|
const restartProfile = profileName
|
|
206
233
|
? await loadProfile(profileName, home, profileStorage)
|
|
207
234
|
: null;
|
|
235
|
+
if (!restartProfile && requestedReleaseTag !== undefined) {
|
|
236
|
+
const installedReleaseTag = daemonReleaseProvenance(daemonVersion(), deps.env).releaseTag;
|
|
237
|
+
if (requestedReleaseTag !== installedReleaseTag) {
|
|
238
|
+
throw new Error(td("Cross-channel daemon upgrade requires --profile so the server environment can be verified"));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const releaseTag = requestedReleaseTag
|
|
242
|
+
?? restartProfile?.releaseTag
|
|
243
|
+
?? (restartProfile === null
|
|
244
|
+
? daemonReleaseProvenance(daemonVersion(), deps.env).releaseTag
|
|
245
|
+
: releaseTagForServerUrl(restartProfile.serverUrl));
|
|
246
|
+
if (restartProfile)
|
|
247
|
+
assertReleaseTagForServerUrl(restartProfile.serverUrl, releaseTag);
|
|
208
248
|
const spec = restartProfile
|
|
209
249
|
? buildServiceSpec({
|
|
210
250
|
...deps,
|
|
@@ -215,7 +255,7 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
215
255
|
: null;
|
|
216
256
|
if (spec && restartProfile) {
|
|
217
257
|
if (installation === null && !isGlobalDaemonEntry(deps.entryPath, await deps.resolveGlobalNodeModules())) {
|
|
218
|
-
throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@
|
|
258
|
+
throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@{{tag}}", { tag: releaseTag }));
|
|
219
259
|
}
|
|
220
260
|
const before = await serviceStatus(spec, deps.runner);
|
|
221
261
|
if (!before.installed)
|
|
@@ -236,7 +276,10 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
236
276
|
installationLease = installation === null
|
|
237
277
|
? null
|
|
238
278
|
: await deps.acquireInstallationLease(installation.npmPrefix, { userHome: deps.userHome });
|
|
239
|
-
await upgradeDaemon(deps.platform, deps.runner, installation?.npmPrefix
|
|
279
|
+
await upgradeDaemon(deps.platform, deps.runner, installation?.npmPrefix, releaseTag, configuredRegistryUrl === undefined
|
|
280
|
+
? restartProfile?.registryUrl
|
|
281
|
+
?? configuredRegistryUrl
|
|
282
|
+
: normalizeDaemonNpmRegistry(configuredRegistryUrl, { allowLoopbackHttp: releaseTag === "beta" }));
|
|
240
283
|
}
|
|
241
284
|
catch (error) {
|
|
242
285
|
upgradeError = error;
|
|
@@ -274,6 +317,14 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
274
317
|
}
|
|
275
318
|
const name = requireValue(profileName, "--profile", td);
|
|
276
319
|
const profile = await loadProfile(name, home, profileStorage);
|
|
320
|
+
const profileReleaseTag = requestedReleaseTag ?? profile.releaseTag ?? releaseTagForServerUrl(profile.serverUrl);
|
|
321
|
+
if (command === "install") {
|
|
322
|
+
assertReleaseTagForServerUrl(profile.serverUrl, profileReleaseTag);
|
|
323
|
+
const installedReleaseTag = daemonReleaseProvenance(daemonVersion(), deps.env).releaseTag;
|
|
324
|
+
if (installedReleaseTag !== profileReleaseTag) {
|
|
325
|
+
throw new Error(td("Installed daemon release tag '{{installed}}' does not match profile '{{profile}}' release tag '{{target}}'", { installed: installedReleaseTag, profile: name, target: profileReleaseTag }));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
277
328
|
if (command === "install" || command === "start" || command === "restart") {
|
|
278
329
|
await assertProfileAgentsRootUnique(profile, home, deps.userHome, profileStorage);
|
|
279
330
|
}
|
|
@@ -296,7 +347,8 @@ export async function runComputerCommand(argv, overrides = {}) {
|
|
|
296
347
|
: null;
|
|
297
348
|
if (command === "install") {
|
|
298
349
|
if (installation === null && !isGlobalDaemonEntry(deps.entryPath, await deps.resolveGlobalNodeModules())) {
|
|
299
|
-
|
|
350
|
+
const releaseTag = requestedReleaseTag ?? profile.releaseTag ?? releaseTagForServerUrl(profile.serverUrl);
|
|
351
|
+
throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@{{tag}}", { tag: releaseTag }));
|
|
300
352
|
}
|
|
301
353
|
if (managedRecord === null)
|
|
302
354
|
await installService(spec, deps.runner);
|
package/dist/computer-profile.js
CHANGED
|
@@ -8,33 +8,57 @@ import { z } from "zod";
|
|
|
8
8
|
import { acquireProfileSaveLock, } from "./computer-profile-lock.js";
|
|
9
9
|
import { formatDaemonText } from "./i18n.js";
|
|
10
10
|
import { atomicPrivateWrite } from "./atomic-private-write.js";
|
|
11
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
11
12
|
const PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,47}$/;
|
|
12
13
|
export const MachineTokenSchema = z.string().startsWith("sk_machine_").max(4096);
|
|
13
14
|
const ServerUrlSchema = z.string().url().max(2048).refine((value) => {
|
|
14
15
|
const url = new URL(value);
|
|
15
16
|
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
|
|
16
17
|
}, "serverUrl must be an HTTP(S) URL without embedded credentials");
|
|
17
|
-
const
|
|
18
|
+
const ProfileFields = z.object({
|
|
18
19
|
version: z.literal(1),
|
|
19
20
|
name: z.string().regex(PROFILE_NAME),
|
|
20
21
|
serverUrl: ServerUrlSchema,
|
|
21
22
|
machineToken: MachineTokenSchema,
|
|
22
23
|
agentsRoot: z.string().min(1).optional(),
|
|
23
24
|
runtimePath: z.string().min(1).max(32768).refine((value) => !value.includes("\0") && !value.includes("\n") && !value.includes("\r"), "runtimePath must be a single line").optional(),
|
|
25
|
+
releaseTag: z.enum(["latest", "beta"]).optional(),
|
|
26
|
+
registryUrl: z.string().url().optional(),
|
|
24
27
|
codexHomeMode: z.enum(["shared", "private"]).optional(),
|
|
25
28
|
}).strict();
|
|
26
|
-
|
|
29
|
+
function validateProfileRegistry(value, context) {
|
|
30
|
+
const fields = value;
|
|
31
|
+
if (fields.registryUrl === undefined)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
const normalized = normalizeDaemonNpmRegistry(fields.registryUrl, {
|
|
35
|
+
allowLoopbackHttp: fields.releaseTag === "beta",
|
|
36
|
+
});
|
|
37
|
+
if (normalized !== fields.registryUrl) {
|
|
38
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["registryUrl"], message: "registryUrl must be normalized" });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
context.addIssue({
|
|
43
|
+
code: z.ZodIssueCode.custom,
|
|
44
|
+
path: ["registryUrl"],
|
|
45
|
+
message: error instanceof Error ? error.message : "registryUrl must be HTTPS",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const ProfileSchema = ProfileFields.superRefine(validateProfileRegistry);
|
|
50
|
+
const ProfileAgentsRootCandidateSchema = ProfileFields.pick({
|
|
27
51
|
name: true,
|
|
28
52
|
serverUrl: true,
|
|
29
53
|
agentsRoot: true,
|
|
30
54
|
}).strict();
|
|
31
55
|
const StoredPlainProfileSchema = ProfileSchema;
|
|
32
|
-
const StoredProtectedProfileSchema =
|
|
56
|
+
const StoredProtectedProfileSchema = ProfileFields.omit({ machineToken: true }).extend({
|
|
33
57
|
machineTokenProtected: z.object({
|
|
34
58
|
scheme: z.literal("dpapi-current-user"),
|
|
35
59
|
ciphertext: z.string().min(1).max(16384),
|
|
36
60
|
}).strict(),
|
|
37
|
-
}).strict();
|
|
61
|
+
}).strict().superRefine(validateProfileRegistry);
|
|
38
62
|
const StoredProfileSchema = z.union([StoredPlainProfileSchema, StoredProtectedProfileSchema]);
|
|
39
63
|
export const PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE = "Profile '{{profile}}' conflicts with profile '{{conflict}}': both resolve to agents root '{{agentsRoot}}'. Save it with a unique root, for example: {{command}}";
|
|
40
64
|
export class ProfileAgentsRootConflictError extends Error {
|
|
@@ -218,6 +242,8 @@ export async function saveProfile(input, home = daemonHome(), options = {}) {
|
|
|
218
242
|
serverUrl: profile.serverUrl,
|
|
219
243
|
...(profile.agentsRoot ? { agentsRoot: profile.agentsRoot } : {}),
|
|
220
244
|
...(profile.runtimePath ? { runtimePath: profile.runtimePath } : {}),
|
|
245
|
+
...(profile.releaseTag === undefined ? {} : { releaseTag: profile.releaseTag }),
|
|
246
|
+
...(profile.registryUrl === undefined ? {} : { registryUrl: profile.registryUrl }),
|
|
221
247
|
...(profile.codexHomeMode === undefined ? {} : { codexHomeMode: profile.codexHomeMode }),
|
|
222
248
|
machineTokenProtected: { scheme: "dpapi-current-user", ciphertext },
|
|
223
249
|
});
|
|
@@ -261,6 +287,8 @@ export async function loadProfile(name, home = daemonHome(), options = {}) {
|
|
|
261
287
|
serverUrl: stored.serverUrl,
|
|
262
288
|
...(stored.agentsRoot ? { agentsRoot: stored.agentsRoot } : {}),
|
|
263
289
|
...(stored.runtimePath ? { runtimePath: stored.runtimePath } : {}),
|
|
290
|
+
...(stored.releaseTag === undefined ? {} : { releaseTag: stored.releaseTag }),
|
|
291
|
+
...(stored.registryUrl === undefined ? {} : { registryUrl: stored.registryUrl }),
|
|
264
292
|
machineToken: await configured.protector.unprotect(stored.machineTokenProtected.ciphertext),
|
|
265
293
|
});
|
|
266
294
|
}
|
|
@@ -355,5 +383,13 @@ export function applyProfileToEnv(profile, env) {
|
|
|
355
383
|
delete env.CREW_AGENTS_ROOT;
|
|
356
384
|
if (profile.runtimePath)
|
|
357
385
|
env.PATH = profile.runtimePath;
|
|
386
|
+
if (profile.releaseTag)
|
|
387
|
+
env.CREW_DAEMON_RELEASE_TAG = profile.releaseTag;
|
|
388
|
+
else
|
|
389
|
+
delete env.CREW_DAEMON_RELEASE_TAG;
|
|
390
|
+
if (profile.registryUrl)
|
|
391
|
+
env.CREW_NPM_REGISTRY = profile.registryUrl;
|
|
392
|
+
else
|
|
393
|
+
delete env.CREW_NPM_REGISTRY;
|
|
358
394
|
env.CREW_CODEX_HOME_MODE = profile.codexHomeMode ?? "shared";
|
|
359
395
|
}
|
package/dist/computer-service.js
CHANGED
|
@@ -4,6 +4,7 @@ import { execFile, spawn } from "node:child_process";
|
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { dirname, resolve } from "node:path";
|
|
7
|
+
import { normalizeDaemonNpmRegistry } from "./npm-registry.js";
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
const WINDOWS_STOP_TIMEOUT_MS = 5_000;
|
|
9
10
|
const WINDOWS_STOP_POLL_MS = 100;
|
|
@@ -541,19 +542,23 @@ export async function doctorService(spec, profilePrivate, runner = systemCommand
|
|
|
541
542
|
{ name: "service-running", ok: status.running === true, detail: status.running ? "running" : "not running" },
|
|
542
543
|
];
|
|
543
544
|
}
|
|
544
|
-
export function buildUpgradeCommand(platform, npmPrefix) {
|
|
545
|
+
export function buildUpgradeCommand(platform, npmPrefix, releaseTag = "latest", registryUrl) {
|
|
545
546
|
return [
|
|
546
547
|
platform === "win32" ? "npm.cmd" : "npm",
|
|
547
548
|
[
|
|
548
549
|
"install",
|
|
549
550
|
"--global",
|
|
551
|
+
"--ignore-scripts",
|
|
552
|
+
"--no-audit",
|
|
553
|
+
"--no-fund",
|
|
550
554
|
...(npmPrefix === undefined ? [] : ["--prefix", npmPrefix]),
|
|
551
|
-
"
|
|
555
|
+
...(registryUrl === undefined ? [] : ["--registry", normalizeDaemonNpmRegistry(registryUrl, { allowLoopbackHttp: releaseTag === "beta" })]),
|
|
556
|
+
`@nowcrew/daemon@${releaseTag}`,
|
|
552
557
|
],
|
|
553
558
|
];
|
|
554
559
|
}
|
|
555
|
-
export async function upgradeDaemon(platform, runner = systemCommandRunner, npmPrefix) {
|
|
556
|
-
const [command, args] = buildUpgradeCommand(platform, npmPrefix);
|
|
560
|
+
export async function upgradeDaemon(platform, runner = systemCommandRunner, npmPrefix, releaseTag = "latest", registryUrl) {
|
|
561
|
+
const [command, args] = buildUpgradeCommand(platform, npmPrefix, releaseTag, registryUrl);
|
|
557
562
|
await checked(runner, command, args, "Daemon upgrade");
|
|
558
563
|
}
|
|
559
564
|
const WINDOWS_SID_SCRIPT = "[System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value";
|
|
@@ -119,10 +119,12 @@ export function createDaemonMigrationController(input) {
|
|
|
119
119
|
let currentSpec;
|
|
120
120
|
let currentService;
|
|
121
121
|
let managedRecord = null;
|
|
122
|
+
let currentRegistryUrl;
|
|
122
123
|
try {
|
|
123
124
|
const currentProfile = await (input.loadCurrentProfile ?? loadProfile)(input.profileName, input.currentDaemonHome);
|
|
124
125
|
if (currentProfile.machineToken !== input.machineToken)
|
|
125
126
|
throw new Error("active profile token does not match this daemon");
|
|
127
|
+
currentRegistryUrl = currentProfile.registryUrl;
|
|
126
128
|
const target = resolveAutomaticInstallTarget({ serverUrl: input.serverUrl, requestedProfile: message.targetProfile, userHome, platform });
|
|
127
129
|
if (!isAbsolute(message.targetDaemonHome) || !isAbsolute(message.targetAgentsRoot)
|
|
128
130
|
|| resolve(message.targetDaemonHome) !== resolve(target.daemonHome)
|
|
@@ -165,6 +167,7 @@ export function createDaemonMigrationController(input) {
|
|
|
165
167
|
serverUrl: input.serverUrl,
|
|
166
168
|
machineToken: input.machineToken,
|
|
167
169
|
requestedProfile: message.targetProfile,
|
|
170
|
+
...(currentRegistryUrl === undefined ? {} : { requestedRegistryUrl: currentRegistryUrl }),
|
|
168
171
|
});
|
|
169
172
|
if (result.profile !== message.targetProfile || result.serviceId.length === 0) {
|
|
170
173
|
throw new Error("target service identity did not match the requested profile");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { compare, prerelease, valid } from "semver";
|
|
2
|
+
const CANONICAL_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-beta(?:\.|-)(0|[1-9]\d*))?$/u;
|
|
3
|
+
function normalizedVersion(input) {
|
|
4
|
+
return input.replace(/-beta-(0|[1-9]\d*)$/u, "-beta.$1");
|
|
5
|
+
}
|
|
6
|
+
export function isDaemonReleaseVersion(input) {
|
|
7
|
+
const raw = input.trim();
|
|
8
|
+
if (raw !== input || !CANONICAL_VERSION.test(raw) || valid(raw) !== raw)
|
|
9
|
+
return false;
|
|
10
|
+
const parts = prerelease(raw);
|
|
11
|
+
return parts === null || normalizedVersion(raw).includes("-beta.");
|
|
12
|
+
}
|
|
13
|
+
export function daemonReleaseChannel(version) {
|
|
14
|
+
if (!isDaemonReleaseVersion(version))
|
|
15
|
+
return null;
|
|
16
|
+
return prerelease(version) === null ? "stable" : "beta";
|
|
17
|
+
}
|
|
18
|
+
export function compareDaemonReleaseVersions(left, right) {
|
|
19
|
+
return isDaemonReleaseVersion(left) && isDaemonReleaseVersion(right)
|
|
20
|
+
? compare(normalizedVersion(left), normalizedVersion(right))
|
|
21
|
+
: 0;
|
|
22
|
+
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { isDaemonReleaseVersion } from "./daemon-release-version.js";
|
|
2
3
|
const DaemonUpdateMessageSchema = z.object({
|
|
3
4
|
type: z.literal("daemon:update"),
|
|
4
5
|
updateId: z.string().uuid(),
|
|
5
|
-
targetVersion: z.string().
|
|
6
|
+
targetVersion: z.string().refine(isDaemonReleaseVersion, "invalid daemon release version"),
|
|
6
7
|
}).strict();
|
|
7
8
|
const DaemonRestartMessageSchema = z.object({
|
|
8
9
|
type: z.literal("daemon:restart"),
|
|
9
10
|
updateId: z.string().uuid(),
|
|
10
|
-
targetVersion: z.string().
|
|
11
|
+
targetVersion: z.string().refine(isDaemonReleaseVersion, "invalid daemon release version"),
|
|
11
12
|
}).strict();
|
|
12
13
|
const DaemonControlMessageSchema = z.union([DaemonUpdateMessageSchema, DaemonRestartMessageSchema]);
|
|
13
14
|
export function createDaemonUpdateController(deps) {
|