@nowcrew/daemon 0.5.44 → 0.5.45
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 +5 -3
- package/dist/computer-service.js +38 -5
- package/dist/daemon-installation.js +35 -18
- package/dist/daemon-update-eligibility.js +53 -12
- package/dist/daemon-updater.js +1 -1
- package/dist/main.js +0 -0
- package/dist/windows-scheduled-task.js +64 -0
- package/package.json +8 -9
package/README.md
CHANGED
|
@@ -105,8 +105,10 @@ CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon doctor --profile dev
|
|
|
105
105
|
CREW_DAEMON_HOME="$HOME/.crew/daemon-dev" crew-daemon status --profile dev
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
If the descriptor has drifted
|
|
109
|
-
|
|
108
|
+
If the target descriptor has drifted or the profile is stopped, the daemon fails closed and remains
|
|
109
|
+
manually upgradeable. On macOS, another unregistered legacy LaunchAgent may coexist only when its plist
|
|
110
|
+
exactly matches a generated NowCrew service and resolves to a different npm prefix. An unverifiable or
|
|
111
|
+
same-prefix descriptor still fails closed.
|
|
110
112
|
|
|
111
113
|
The daemon advertises `daemon_update_v1` only when all of these conditions hold:
|
|
112
114
|
|
|
@@ -115,7 +117,7 @@ The daemon advertises `daemon_update_v1` only when all of these conditions hold:
|
|
|
115
117
|
| Platform | macOS LaunchAgent or Linux systemd user service |
|
|
116
118
|
| Entrypoint | global `@nowcrew/daemon/dist/main.js`, not npx or source/tsx |
|
|
117
119
|
| Startup | `crew-daemon serve --profile <name>` through the installed service |
|
|
118
|
-
| Registry |
|
|
120
|
+
| Registry | registered descriptors remain conflict-free; the exact target legacy descriptor may be adopted once; an exact macOS legacy descriptor on another npm prefix may coexist |
|
|
119
121
|
| Identity | service ID, descriptor, daemon home, Agent root, entrypoint, package root, and npm prefix match |
|
|
120
122
|
| Service | the selected profile is running and holds the installation lease |
|
|
121
123
|
| Install root | a standard writable Unix global npm prefix can be derived from the running entrypoint |
|
package/dist/computer-service.js
CHANGED
|
@@ -315,8 +315,30 @@ setTimeout(() => {
|
|
|
315
315
|
let restarted;
|
|
316
316
|
if (spec.platform === "darwin") {
|
|
317
317
|
restarted = spawnSync("launchctl", ["kickstart", "-k", spec.target], { stdio: "ignore", shell: false });
|
|
318
|
-
} else {
|
|
318
|
+
} else if (spec.platform === "linux") {
|
|
319
319
|
restarted = spawnSync("systemctl", ["--user", "--no-block", "restart", spec.id], { stdio: "ignore", shell: false });
|
|
320
|
+
} else {
|
|
321
|
+
const encodedName = Buffer.from(spec.id, "utf16le").toString("base64");
|
|
322
|
+
const encodedExecutable = Buffer.from(spec.executable, "utf16le").toString("base64");
|
|
323
|
+
const encodedArguments = Buffer.from(spec.arguments, "utf16le").toString("base64");
|
|
324
|
+
const script = [
|
|
325
|
+
"$ErrorActionPreference='Stop';",
|
|
326
|
+
"$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
|
|
327
|
+
"$exe=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[2]));",
|
|
328
|
+
"$argv=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[3]));",
|
|
329
|
+
"$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};",
|
|
330
|
+
"$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
|
|
331
|
+
"$p=Get-Process -Id ([int]$args[0]) -ErrorAction Stop;",
|
|
332
|
+
"if($p.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() -cne $args[4]){throw 'daemon process identity changed'};",
|
|
333
|
+
"$p.Kill();",
|
|
334
|
+
"if(-not $p.WaitForExit(10000)){throw 'daemon exit timeout'};",
|
|
335
|
+
"$deadline=[DateTime]::UtcNow.AddSeconds(10);",
|
|
336
|
+
"do{$task=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});if($task.Count -ne 1){throw 'scheduled task identity changed'};if([int]$task[0].State -ne 4){break};Start-Sleep -Milliseconds 100}while([DateTime]::UtcNow -lt $deadline);",
|
|
337
|
+
"if([int]$task[0].State -eq 4){throw 'scheduled task stop timeout'};",
|
|
338
|
+
"$actions=@($task[0].Actions);if($actions.Count -ne 1 -or -not [String]::Equals($actions[0].Execute,$exe,[StringComparison]::OrdinalIgnoreCase) -or $actions[0].Arguments -cne $argv){throw 'scheduled task action changed'};",
|
|
339
|
+
"& schtasks.exe /Run /TN $n | Out-Null;if($LASTEXITCODE -ne 0){throw 'scheduled task start failed'};",
|
|
340
|
+
].join("");
|
|
341
|
+
restarted = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script, String(spec.daemonPid), encodedName, encodedExecutable, encodedArguments, spec.processIdentity], { stdio: "ignore", shell: false });
|
|
320
342
|
}
|
|
321
343
|
if (restarted.status !== 0) process.send?.({ type: "restart-failed", status: restarted.status });
|
|
322
344
|
setTimeout(() => {
|
|
@@ -326,16 +348,27 @@ setTimeout(() => {
|
|
|
326
348
|
}, 250);
|
|
327
349
|
`;
|
|
328
350
|
export function scheduleServiceRestart(spec, spawnDetached = systemDetachedSpawn) {
|
|
329
|
-
if (spec.platform
|
|
330
|
-
throw new Error("Windows detached restart is not supported");
|
|
331
|
-
if (spec.descriptorPath === null)
|
|
351
|
+
if (spec.platform !== "win32" && spec.descriptorPath === null) {
|
|
332
352
|
throw new Error("service descriptor is required for restart");
|
|
353
|
+
}
|
|
333
354
|
const payload = spec.platform === "darwin"
|
|
334
355
|
? {
|
|
335
356
|
platform: spec.platform,
|
|
336
357
|
target: `${spec.managerDomain}/${spec.id}`,
|
|
337
358
|
}
|
|
338
|
-
:
|
|
359
|
+
: spec.platform === "linux"
|
|
360
|
+
? { platform: spec.platform, id: spec.id }
|
|
361
|
+
: {
|
|
362
|
+
platform: spec.platform,
|
|
363
|
+
id: spec.id,
|
|
364
|
+
daemonPid: process.pid,
|
|
365
|
+
executable: spec.daemonCommand[0],
|
|
366
|
+
arguments: spec.daemonCommand.slice(1).map(windowsCommandArg).join(" "),
|
|
367
|
+
processIdentity: spec.runtimeProcessIdentity,
|
|
368
|
+
};
|
|
369
|
+
if (spec.platform === "win32" && !payload.processIdentity) {
|
|
370
|
+
throw new Error("Windows daemon process identity is required for restart");
|
|
371
|
+
}
|
|
339
372
|
const child = spawnDetached(process.execPath, ["-e", RESTART_HELPER_SOURCE, JSON.stringify(payload)], { detached: true, stdio: ["ignore", "ignore", "ignore", "ipc"], shell: false });
|
|
340
373
|
child.unref();
|
|
341
374
|
let settleFailure;
|
|
@@ -1,38 +1,55 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
|
-
import {
|
|
3
|
-
function canonicalPath(path, realpath) {
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
|
+
function canonicalPath(path, platform, realpath) {
|
|
4
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
4
5
|
try {
|
|
5
6
|
return realpath(path);
|
|
6
7
|
}
|
|
7
8
|
catch (error) {
|
|
8
9
|
const code = error.code;
|
|
9
|
-
return code === "ENOENT" || code === "ENOTDIR" ? resolve(path) : null;
|
|
10
|
+
return code === "ENOENT" || code === "ENOTDIR" ? paths.resolve(path) : null;
|
|
10
11
|
}
|
|
11
12
|
}
|
|
13
|
+
function samePath(left, right, platform) {
|
|
14
|
+
return platform === "win32"
|
|
15
|
+
? left.toLowerCase() === right.toLowerCase()
|
|
16
|
+
: left === right;
|
|
17
|
+
}
|
|
12
18
|
export function daemonGlobalInstallation(entryPath, platform, realpath = realpathSync.native) {
|
|
13
|
-
if (platform !== "darwin" && platform !== "linux")
|
|
19
|
+
if (platform !== "darwin" && platform !== "linux" && platform !== "win32")
|
|
20
|
+
return null;
|
|
21
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
22
|
+
const packageRoot = paths.resolve(paths.dirname(entryPath), "..");
|
|
23
|
+
const globalNodeModules = paths.resolve(packageRoot, "..", "..");
|
|
24
|
+
const modulesParent = paths.dirname(globalNodeModules);
|
|
25
|
+
if (paths.basename(globalNodeModules).toLowerCase() !== "node_modules")
|
|
14
26
|
return null;
|
|
15
|
-
|
|
16
|
-
const globalNodeModules = resolve(packageRoot, "..", "..");
|
|
17
|
-
const libDirectory = dirname(globalNodeModules);
|
|
18
|
-
if (basename(globalNodeModules) !== "node_modules" || basename(libDirectory) !== "lib")
|
|
27
|
+
if (platform !== "win32" && paths.basename(modulesParent) !== "lib")
|
|
19
28
|
return null;
|
|
20
|
-
if (resolve(entryPath)
|
|
29
|
+
if (!samePath(paths.resolve(entryPath), paths.resolve(packageRoot, "dist", "main.js"), platform))
|
|
21
30
|
return null;
|
|
22
|
-
if (resolve(packageRoot)
|
|
31
|
+
if (!samePath(paths.resolve(packageRoot), paths.resolve(globalNodeModules, "@nowcrew", "daemon"), platform))
|
|
23
32
|
return null;
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
33
|
+
const npmPrefix = platform === "win32" ? modulesParent : paths.dirname(modulesParent);
|
|
34
|
+
const canonicalPackageRoot = canonicalPath(packageRoot, platform, realpath);
|
|
35
|
+
const canonicalGlobalNodeModules = canonicalPath(globalNodeModules, platform, realpath);
|
|
36
|
+
const canonicalNpmPrefix = canonicalPath(npmPrefix, platform, realpath);
|
|
27
37
|
if (canonicalPackageRoot === null || canonicalGlobalNodeModules === null || canonicalNpmPrefix === null)
|
|
28
38
|
return null;
|
|
29
|
-
|
|
39
|
+
const normalizeCanonical = (path) => paths.resolve(path);
|
|
40
|
+
const normalizedPackageRoot = normalizeCanonical(canonicalPackageRoot);
|
|
41
|
+
const normalizedGlobalNodeModules = normalizeCanonical(canonicalGlobalNodeModules);
|
|
42
|
+
const normalizedNpmPrefix = normalizeCanonical(canonicalNpmPrefix);
|
|
43
|
+
if (!samePath(normalizedPackageRoot, paths.resolve(normalizedGlobalNodeModules, "@nowcrew", "daemon"), platform))
|
|
30
44
|
return null;
|
|
31
|
-
|
|
45
|
+
const expectedModules = platform === "win32"
|
|
46
|
+
? paths.resolve(normalizedNpmPrefix, "node_modules")
|
|
47
|
+
: paths.resolve(normalizedNpmPrefix, "lib", "node_modules");
|
|
48
|
+
if (!samePath(normalizedGlobalNodeModules, expectedModules, platform))
|
|
32
49
|
return null;
|
|
33
50
|
return {
|
|
34
|
-
packageRoot:
|
|
35
|
-
globalNodeModules:
|
|
36
|
-
npmPrefix:
|
|
51
|
+
packageRoot: normalizedPackageRoot,
|
|
52
|
+
globalNodeModules: normalizedGlobalNodeModules,
|
|
53
|
+
npmPrefix: normalizedNpmPrefix,
|
|
37
54
|
};
|
|
38
55
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { daemonHome, loadProfile, resolveAgentsRoot } from "./computer-profile.js";
|
|
5
6
|
import { builtDaemonEntry } from "./computer-cli.js";
|
|
6
7
|
import { buildServiceSpec, readServiceDescriptor, serviceStatus, } from "./computer-service.js";
|
|
7
8
|
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
8
9
|
import { daemonInstallationLeaseHeld } from "./daemon-installation-lease.js";
|
|
10
|
+
import { inspectLegacyServiceInstallation } from "./legacy-service-installation.js";
|
|
11
|
+
import { inspectWindowsServiceUpdateScope } from "./windows-scheduled-task.js";
|
|
9
12
|
import { assertRegistryCoversDescriptors, listManagedServiceDescriptorPaths, managedServiceIdentityMatches, readManagedServiceRegistry, serviceDescriptorSha256, } from "./managed-service-registry.js";
|
|
10
13
|
function defaults() {
|
|
11
14
|
return {
|
|
@@ -22,16 +25,35 @@ function defaults() {
|
|
|
22
25
|
serviceStatus,
|
|
23
26
|
assertWritable: (path) => access(path, constants.W_OK),
|
|
24
27
|
installationLeaseHeld: daemonInstallationLeaseHeld,
|
|
28
|
+
installationRealpath: realpathSync.native,
|
|
29
|
+
inspectWindowsServiceUpdateScope,
|
|
30
|
+
inspectLegacyDescriptorInstallation: (descriptorPath) => inspectLegacyServiceInstallation(descriptorPath, process.platform, homedir(), process.getuid?.()),
|
|
25
31
|
};
|
|
26
32
|
}
|
|
33
|
+
async function registryCoversUpdateScope(services, descriptorPaths, targetDescriptorPath, npmPrefix, inspectLegacyDescriptorInstallation) {
|
|
34
|
+
for (const descriptorPath of descriptorPaths) {
|
|
35
|
+
if (descriptorPath === targetDescriptorPath)
|
|
36
|
+
continue;
|
|
37
|
+
try {
|
|
38
|
+
assertRegistryCoversDescriptors(services, [descriptorPath]);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
const legacy = await inspectLegacyDescriptorInstallation(descriptorPath).catch(() => null);
|
|
43
|
+
if (legacy === null || legacy.npmPrefix === npmPrefix)
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
27
49
|
export async function detectDaemonUpdateEligibility(profileName, overrides = {}) {
|
|
28
50
|
const deps = { ...defaults(), ...overrides };
|
|
29
|
-
if (deps.platform !== "darwin" && deps.platform !== "linux") {
|
|
51
|
+
if (deps.platform !== "darwin" && deps.platform !== "linux" && deps.platform !== "win32") {
|
|
30
52
|
return { eligible: false, reason: "unsupported_platform" };
|
|
31
53
|
}
|
|
32
54
|
if (!profileName)
|
|
33
55
|
return { eligible: false, reason: "profile_required" };
|
|
34
|
-
const installation = daemonGlobalInstallation(deps.entryPath, deps.platform);
|
|
56
|
+
const installation = daemonGlobalInstallation(deps.entryPath, deps.platform, deps.installationRealpath);
|
|
35
57
|
if (installation === null)
|
|
36
58
|
return { eligible: false, reason: "global_install_required" };
|
|
37
59
|
let profile;
|
|
@@ -53,6 +75,33 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
53
75
|
entryPath: deps.entryPath,
|
|
54
76
|
profileHome: deps.profileHome,
|
|
55
77
|
});
|
|
78
|
+
if (deps.platform === "win32") {
|
|
79
|
+
const scope = await deps.inspectWindowsServiceUpdateScope(spec, installation.npmPrefix)
|
|
80
|
+
.catch(() => ({ valid: false }));
|
|
81
|
+
if (!scope.valid) {
|
|
82
|
+
return { eligible: false, reason: "managed_service_identity_mismatch" };
|
|
83
|
+
}
|
|
84
|
+
if (!scope.running)
|
|
85
|
+
return { eligible: false, reason: "service_not_running" };
|
|
86
|
+
if (!scope.processIdentity) {
|
|
87
|
+
return { eligible: false, reason: "managed_service_identity_mismatch" };
|
|
88
|
+
}
|
|
89
|
+
if (!deps.installationLeaseHeld(installation.npmPrefix)) {
|
|
90
|
+
return { eligible: false, reason: "installation_lease_missing" };
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await deps.assertWritable(installation.globalNodeModules);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return { eligible: false, reason: "global_root_not_writable" };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
eligible: true,
|
|
100
|
+
profileName,
|
|
101
|
+
...installation,
|
|
102
|
+
serviceSpec: { ...spec, runtimeProcessIdentity: scope.processIdentity },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
56
105
|
let selectedSpec = spec;
|
|
57
106
|
let status = await deps.serviceStatus(spec);
|
|
58
107
|
let services;
|
|
@@ -65,10 +114,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
65
114
|
const registered = services.find((record) => record.serviceId === spec.id
|
|
66
115
|
|| (record.daemonHome === deps.profileHome && record.profile === profileName));
|
|
67
116
|
if (registered !== undefined) {
|
|
68
|
-
|
|
69
|
-
assertRegistryCoversDescriptors(services, await deps.listManagedDescriptorPaths());
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
117
|
+
if (!await registryCoversUpdateScope(services, await deps.listManagedDescriptorPaths(), spec.descriptorPath, installation.npmPrefix, deps.inspectLegacyDescriptorInstallation)) {
|
|
72
118
|
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
73
119
|
}
|
|
74
120
|
const descriptor = await deps.readServiceDescriptor(spec).catch(() => null);
|
|
@@ -109,12 +155,7 @@ export async function detectDaemonUpdateEligibility(profileName, overrides = {})
|
|
|
109
155
|
if (legacyDescriptor !== legacySpec.descriptor) {
|
|
110
156
|
return { eligible: false, reason: "managed_service_not_registered" };
|
|
111
157
|
}
|
|
112
|
-
|
|
113
|
-
const descriptorPaths = await deps.listManagedDescriptorPaths();
|
|
114
|
-
const targetPath = legacySpec.descriptorPath;
|
|
115
|
-
assertRegistryCoversDescriptors(services, descriptorPaths.filter((descriptorPath) => descriptorPath !== targetPath));
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
158
|
+
if (!await registryCoversUpdateScope(services, await deps.listManagedDescriptorPaths(), legacySpec.descriptorPath, installation.npmPrefix, deps.inspectLegacyDescriptorInstallation)) {
|
|
118
159
|
return { eligible: false, reason: "managed_service_registry_incomplete" };
|
|
119
160
|
}
|
|
120
161
|
status = await deps.serviceStatus(legacySpec);
|
package/dist/daemon-updater.js
CHANGED
|
@@ -48,7 +48,7 @@ export async function installExactDaemonUpdate(input) {
|
|
|
48
48
|
await release();
|
|
49
49
|
return { ok: false, errorCode: "install_failed" };
|
|
50
50
|
}
|
|
51
|
-
const result = await runner(process.platform === "win32" ? "npm.cmd" : "npm", [
|
|
51
|
+
const result = await runner((input.platform ?? process.platform) === "win32" ? "npm.cmd" : "npm", [
|
|
52
52
|
"install",
|
|
53
53
|
"--global",
|
|
54
54
|
"--prefix",
|
package/dist/main.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { systemCommandRunner, windowsCommandArg, } from "./computer-service.js";
|
|
2
|
+
import { win32 } from "node:path";
|
|
3
|
+
function encoded(value) {
|
|
4
|
+
return Buffer.from(value, "utf16le").toString("base64");
|
|
5
|
+
}
|
|
6
|
+
function sameWindowsPath(left, right) {
|
|
7
|
+
if (left === undefined)
|
|
8
|
+
return false;
|
|
9
|
+
return left.replaceAll("/", "\\").toLowerCase()
|
|
10
|
+
=== right.replaceAll("/", "\\").toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
export async function inspectWindowsServiceUpdateScope(spec, npmPrefix, daemonPid = process.pid, runner = systemCommandRunner) {
|
|
13
|
+
if (spec.platform !== "win32")
|
|
14
|
+
return { valid: false };
|
|
15
|
+
const expectedArguments = spec.daemonCommand.slice(1).map(windowsCommandArg).join(" ");
|
|
16
|
+
const entryMarker = windowsCommandArg(win32.resolve(npmPrefix, "node_modules", "@nowcrew", "daemon", "dist", "main.js"));
|
|
17
|
+
const script = [
|
|
18
|
+
"$ErrorActionPreference='Stop';",
|
|
19
|
+
"$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));",
|
|
20
|
+
"$entry=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[1]));",
|
|
21
|
+
"$pidValue=[int]$args[2];",
|
|
22
|
+
"$tasks=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});",
|
|
23
|
+
"if($tasks.Count -ne 1){[pscustomobject]@{count=$tasks.Count}|ConvertTo-Json -Compress;exit 0};",
|
|
24
|
+
"$task=$tasks[0];",
|
|
25
|
+
"$scheduler=New-Object -ComObject 'Schedule.Service';$scheduler.Connect();",
|
|
26
|
+
"$registered=$scheduler.GetFolder('\\').GetTask($n);$instances=@($registered.GetInstances(0));",
|
|
27
|
+
"$current=Get-CimInstance Win32_Process -Filter ('ProcessId='+$pidValue) -ErrorAction Stop;",
|
|
28
|
+
"$ancestors=@();$cursor=$current;while($cursor.ParentProcessId -gt 0){$cursor=Get-CimInstance Win32_Process -Filter ('ProcessId='+$cursor.ParentProcessId) -ErrorAction SilentlyContinue;if($null -eq $cursor){break};$ancestors+=[int64]$cursor.ProcessId};",
|
|
29
|
+
"$processIdentity=(Get-Process -Id $pidValue -ErrorAction Stop).StartTime.ToUniversalTime().ToFileTimeUtc().ToString();",
|
|
30
|
+
"$actions=@($task.Actions);",
|
|
31
|
+
"$others=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -like 'NowCrew Daemon (*)' -and $_.TaskName -cne $n});",
|
|
32
|
+
"$conflicts=@($others|Where-Object{@($_.Actions|Where-Object{$_.Arguments.IndexOf($entry,[StringComparison]::OrdinalIgnoreCase) -ge 0}).Count -gt 0});",
|
|
33
|
+
"$enginePid=if($instances.Count -eq 1){[int64]$instances[0].EnginePID}else{0};",
|
|
34
|
+
"[pscustomobject]@{count=1;state=[int]$task.State;execute=if($actions.Count -eq 1){$actions[0].Execute}else{$null};arguments=if($actions.Count -eq 1){$actions[0].Arguments}else{$null};instanceCount=$instances.Count;enginePid=$enginePid;engineOwned=($enginePid -eq $pidValue -or $ancestors -contains $enginePid);processIdentity=$processIdentity;conflictingPrefixes=$conflicts.Count}|ConvertTo-Json -Compress;",
|
|
35
|
+
].join("");
|
|
36
|
+
const result = await runner("powershell.exe", [
|
|
37
|
+
"-NoProfile", "-NonInteractive", "-Command", script,
|
|
38
|
+
encoded(spec.id), encoded(entryMarker), String(daemonPid),
|
|
39
|
+
]);
|
|
40
|
+
if (result.exitCode !== 0)
|
|
41
|
+
return { valid: false };
|
|
42
|
+
let inspection;
|
|
43
|
+
try {
|
|
44
|
+
inspection = JSON.parse(result.stdout);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { valid: false };
|
|
48
|
+
}
|
|
49
|
+
const running = inspection.state === 4;
|
|
50
|
+
if (inspection.count !== 1
|
|
51
|
+
|| (running && (inspection.instanceCount !== 1 || inspection.engineOwned !== true
|
|
52
|
+
|| typeof inspection.processIdentity !== "string" || inspection.processIdentity.length === 0))
|
|
53
|
+
|| (!running && inspection.instanceCount !== 0)
|
|
54
|
+
|| inspection.conflictingPrefixes !== 0
|
|
55
|
+
|| !sameWindowsPath(inspection.execute, spec.daemonCommand[0])
|
|
56
|
+
|| inspection.arguments !== expectedArguments) {
|
|
57
|
+
return { valid: false };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
valid: true,
|
|
61
|
+
running,
|
|
62
|
+
...(running ? { processIdentity: inspection.processIdentity } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.45",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,13 +16,6 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
-
"scripts": {
|
|
20
|
-
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
21
|
-
"build": "tsc -p tsconfig.json",
|
|
22
|
-
"prepublishOnly": "pnpm build && node ../scripts/daemon-release-artifact.mjs --strict-registry",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"typecheck": "tsc --noEmit"
|
|
25
|
-
},
|
|
26
19
|
"dependencies": {
|
|
27
20
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
28
21
|
"@nowcrew/cli": "^0.4.13",
|
|
@@ -40,5 +33,11 @@
|
|
|
40
33
|
"tsx": "^4.19.0",
|
|
41
34
|
"typescript": "^5.6.0",
|
|
42
35
|
"vitest": "^2.1.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"daemon": "pnpm --filter @nowcrew/cli build && tsx src/main.ts",
|
|
39
|
+
"build": "tsc -p tsconfig.json",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
43
42
|
}
|
|
44
|
-
}
|
|
43
|
+
}
|