@nowcrew/daemon 0.5.27 → 0.5.28
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/package.json +2 -2
- package/dist/attachments.js +0 -196
- package/dist/bound-im-decision.js +0 -22
- package/dist/completion-retransmitter.js +0 -77
- package/dist/computer-cli.js +0 -274
- package/dist/computer-profile-lock.js +0 -395
- package/dist/computer-profile.js +0 -364
- package/dist/computer-service.js +0 -358
- package/dist/config.js +0 -82
- package/dist/console-collapse.js +0 -13
- package/dist/console-formatter.js +0 -77
- package/dist/console-payload.js +0 -73
- package/dist/console.js +0 -329
- package/dist/daemon-startup-error.js +0 -30
- package/dist/execution-backend.js +0 -44
- package/dist/execution-event-limit.js +0 -64
- package/dist/execution-journal-lock.js +0 -421
- package/dist/execution-journal.js +0 -716
- package/dist/execution-protocol.js +0 -342
- package/dist/execution-recovery.js +0 -95
- package/dist/execution-runner.js +0 -659
- package/dist/execution-supervisor-child.js +0 -236
- package/dist/execution-supervisor.js +0 -316
- package/dist/execution-telemetry-journal.js +0 -71
- package/dist/external-output.js +0 -114
- package/dist/i18n.js +0 -64
- package/dist/json-result.js +0 -27
- package/dist/list-models.js +0 -92
- package/dist/local-executor.js +0 -439
- package/dist/log-format.js +0 -10
- package/dist/machine-info.js +0 -124
- package/dist/main.js +0 -118
- package/dist/normalize.js +0 -170
- package/dist/origin-decision.js +0 -44
- package/dist/platform.js +0 -8
- package/dist/prompt.js +0 -307
- package/dist/provider-env.js +0 -90
- package/dist/remote/claude-bridge.js +0 -402
- package/dist/remote/claude-channel.js +0 -164
- package/dist/remote/codex-client.js +0 -408
- package/dist/remote/codex-runtime.js +0 -77
- package/dist/remote/config.js +0 -83
- package/dist/remote/gateway.js +0 -572
- package/dist/remote/protocol.js +0 -178
- package/dist/remote/remote-cli.js +0 -233
- package/dist/remote/session-discovery.js +0 -249
- package/dist/remote/wrapper.js +0 -40
- package/dist/runner.js +0 -234
- package/dist/runtime-cancellation.js +0 -74
- package/dist/runtime-capabilities.js +0 -43
- package/dist/runtime-path.js +0 -60
- package/dist/runtimes/claude.js +0 -51
- package/dist/runtimes/codex-app-server-runner.js +0 -344
- package/dist/runtimes/codex-deepseek-catalog.js +0 -7
- package/dist/runtimes/codex-deepseek-config.js +0 -50
- package/dist/runtimes/codex.js +0 -53
- package/dist/runtimes/kimi-acp-runner.js +0 -364
- package/dist/runtimes/kimi.js +0 -45
- package/dist/runtimes/progress-watchdog.js +0 -26
- package/dist/scheduled-report.js +0 -51
- package/dist/scheduled-run-report.js +0 -57
- package/dist/serve-lifecycle.js +0 -82
- package/dist/serve.js +0 -868
- package/dist/session.js +0 -82
- package/dist/shared-execution-slots.js +0 -68
- package/dist/shutdown-deadline.js +0 -32
- package/dist/skill-preview.js +0 -21
- package/dist/skills.js +0 -56
- package/dist/slog.js +0 -228
- package/dist/supervised-runtime.js +0 -104
- package/dist/token.js +0 -24
- package/dist/unified-diff.js +0 -84
- package/dist/websocket-shutdown.js +0 -53
- package/dist/win32-job-object.js +0 -193
- package/dist/workspace-fs.js +0 -80
- package/dist/workspace-import.js +0 -127
- package/dist/workspace.js +0 -148
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nowcrew/daemon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "crew daemon — 运行在用户机器:拉起/管理 agent 进程,注入 crew CLI,归一化 runtime 事件",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"cross-spawn": "^7.0.6",
|
|
22
22
|
"ws": "^8",
|
|
23
23
|
"zod": "^3.23.0",
|
|
24
|
-
"@nowcrew/cli": "^0.4.
|
|
24
|
+
"@nowcrew/cli": "^0.4.13"
|
|
25
25
|
},
|
|
26
26
|
"optionalDependencies": {
|
|
27
27
|
"koffi": "^2.9.0"
|
package/dist/attachments.js
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { basename, extname, resolve, sep } from "node:path";
|
|
3
|
-
export const ATTACHMENT_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
4
|
-
export const ATTACHMENT_MAX_TOTAL_BYTES = 50 * 1024 * 1024;
|
|
5
|
-
const throwIfAborted = (signal) => {
|
|
6
|
-
if (signal?.aborted)
|
|
7
|
-
throw signal.reason ?? new Error("Attachment materialization aborted");
|
|
8
|
-
};
|
|
9
|
-
function truncateUtf8(value, maxBytes) {
|
|
10
|
-
let bytes = 0;
|
|
11
|
-
let output = "";
|
|
12
|
-
for (const character of value) {
|
|
13
|
-
const size = Buffer.byteLength(character, "utf8");
|
|
14
|
-
if (bytes + size > maxBytes)
|
|
15
|
-
break;
|
|
16
|
-
output += character;
|
|
17
|
-
bytes += size;
|
|
18
|
-
}
|
|
19
|
-
return output;
|
|
20
|
-
}
|
|
21
|
-
function safeFilename(value) {
|
|
22
|
-
const leaf = basename(value.replace(/\\/gu, "/"))
|
|
23
|
-
.replace(/[\u0000-\u001f\u007f]/gu, "_")
|
|
24
|
-
.replace(/^\.+/u, "")
|
|
25
|
-
.trim();
|
|
26
|
-
return truncateUtf8(leaf, 180) || "attachment";
|
|
27
|
-
}
|
|
28
|
-
function collisionName(filename, index) {
|
|
29
|
-
if (index === 1)
|
|
30
|
-
return filename;
|
|
31
|
-
const extension = extname(filename);
|
|
32
|
-
const stem = extension ? filename.slice(0, -extension.length) : filename;
|
|
33
|
-
return `${stem}-${index}${extension}`;
|
|
34
|
-
}
|
|
35
|
-
function containedPath(directory, filename) {
|
|
36
|
-
const target = resolve(directory, filename);
|
|
37
|
-
if (!target.startsWith(`${resolve(directory)}${sep}`)) {
|
|
38
|
-
throw new Error("Attachment path escapes the execution directory");
|
|
39
|
-
}
|
|
40
|
-
return target;
|
|
41
|
-
}
|
|
42
|
-
function checkedRedirect(value, base) {
|
|
43
|
-
const url = new URL(value, base);
|
|
44
|
-
if (url.protocol !== "https:" || url.username || url.password) {
|
|
45
|
-
throw new Error("Attachment redirects must use credential-free HTTPS URLs");
|
|
46
|
-
}
|
|
47
|
-
return url;
|
|
48
|
-
}
|
|
49
|
-
async function readBounded(response, maxBytes) {
|
|
50
|
-
const declared = Number(response.headers.get("content-length") ?? "");
|
|
51
|
-
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
52
|
-
throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
|
|
53
|
-
}
|
|
54
|
-
if (!response.body)
|
|
55
|
-
throw new Error("Attachment response body is empty");
|
|
56
|
-
const reader = response.body.getReader();
|
|
57
|
-
const chunks = [];
|
|
58
|
-
let size = 0;
|
|
59
|
-
while (true) {
|
|
60
|
-
const part = await reader.read();
|
|
61
|
-
if (part.done)
|
|
62
|
-
break;
|
|
63
|
-
const chunk = Buffer.from(part.value);
|
|
64
|
-
size += chunk.length;
|
|
65
|
-
if (size > maxBytes) {
|
|
66
|
-
await reader.cancel();
|
|
67
|
-
throw new Error(`Attachment exceeds the ${maxBytes} byte limit`);
|
|
68
|
-
}
|
|
69
|
-
chunks.push(chunk);
|
|
70
|
-
}
|
|
71
|
-
return Buffer.concat(chunks, size);
|
|
72
|
-
}
|
|
73
|
-
async function downloadAttachment(input) {
|
|
74
|
-
const initial = new URL(`/agent/attachments/${encodeURIComponent(input.attachment.id)}/download`, input.serverUrl);
|
|
75
|
-
if (!["http:", "https:"].includes(initial.protocol)
|
|
76
|
-
|| initial.username || initial.password) {
|
|
77
|
-
throw new Error("Invalid NowWork attachment download URL");
|
|
78
|
-
}
|
|
79
|
-
let current = initial;
|
|
80
|
-
const controller = new AbortController();
|
|
81
|
-
let timedOut = false;
|
|
82
|
-
const onAbort = () => controller.abort(input.signal?.reason ?? new Error("Attachment materialization aborted"));
|
|
83
|
-
input.signal?.addEventListener("abort", onAbort, { once: true });
|
|
84
|
-
if (input.signal?.aborted)
|
|
85
|
-
onAbort();
|
|
86
|
-
const timer = setTimeout(() => {
|
|
87
|
-
timedOut = true;
|
|
88
|
-
controller.abort(new Error(`Attachment download timed out after ${input.timeoutMs}ms`));
|
|
89
|
-
}, input.timeoutMs);
|
|
90
|
-
try {
|
|
91
|
-
let response = await input.fetchImpl(current, {
|
|
92
|
-
redirect: "manual",
|
|
93
|
-
headers: { authorization: `Bearer ${input.token}` },
|
|
94
|
-
signal: controller.signal,
|
|
95
|
-
});
|
|
96
|
-
for (let redirects = 0; [301, 302, 303, 307, 308].includes(response.status); redirects += 1) {
|
|
97
|
-
if (redirects >= input.maxRedirects)
|
|
98
|
-
throw new Error("Attachment redirect limit exceeded");
|
|
99
|
-
const location = response.headers.get("location");
|
|
100
|
-
if (!location)
|
|
101
|
-
throw new Error("Attachment redirect is missing a location");
|
|
102
|
-
current = checkedRedirect(location, current);
|
|
103
|
-
response = await input.fetchImpl(current, { redirect: "manual", signal: controller.signal });
|
|
104
|
-
}
|
|
105
|
-
if (!response.ok)
|
|
106
|
-
throw new Error(`Attachment download failed with status ${response.status}`);
|
|
107
|
-
const data = await readBounded(response, input.maxFileBytes);
|
|
108
|
-
throwIfAborted(input.signal);
|
|
109
|
-
if (data.length !== input.attachment.sizeBytes) {
|
|
110
|
-
throw new Error(`Attachment size mismatch: expected ${input.attachment.sizeBytes}, received ${data.length}`);
|
|
111
|
-
}
|
|
112
|
-
return data;
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
if (input.signal?.aborted) {
|
|
116
|
-
throw input.signal.reason ?? new Error("Attachment materialization aborted");
|
|
117
|
-
}
|
|
118
|
-
if (timedOut) {
|
|
119
|
-
throw new Error(`Attachment download timed out after ${input.timeoutMs}ms`);
|
|
120
|
-
}
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
finally {
|
|
124
|
-
clearTimeout(timer);
|
|
125
|
-
input.signal?.removeEventListener("abort", onAbort);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
export function executionAttachmentDirectory(runDir, executionId) {
|
|
129
|
-
const attachmentsRoot = resolve(runDir, "attachments");
|
|
130
|
-
const executionKey = executionId.replace(/[^A-Za-z0-9._-]/gu, "_");
|
|
131
|
-
const directory = resolve(attachmentsRoot, executionKey);
|
|
132
|
-
if (!directory.startsWith(`${attachmentsRoot}${sep}`)) {
|
|
133
|
-
throw new Error("Attachment directory escapes the run directory");
|
|
134
|
-
}
|
|
135
|
-
return directory;
|
|
136
|
-
}
|
|
137
|
-
export async function materializeAttachments(input) {
|
|
138
|
-
throwIfAborted(input.signal);
|
|
139
|
-
const maxFileBytes = input.maxFileBytes ?? ATTACHMENT_MAX_FILE_BYTES;
|
|
140
|
-
const maxTotalBytes = input.maxTotalBytes ?? ATTACHMENT_MAX_TOTAL_BYTES;
|
|
141
|
-
const total = input.attachments.reduce((sum, attachment) => sum + attachment.sizeBytes, 0);
|
|
142
|
-
if (input.attachments.some((attachment) => attachment.sizeBytes > maxFileBytes)
|
|
143
|
-
|| total > maxTotalBytes) {
|
|
144
|
-
throw new Error("Attachment metadata exceeds the configured byte limit");
|
|
145
|
-
}
|
|
146
|
-
const directory = executionAttachmentDirectory(input.runDir, input.executionId);
|
|
147
|
-
const used = new Set();
|
|
148
|
-
const materialized = [];
|
|
149
|
-
try {
|
|
150
|
-
await rm(directory, { recursive: true, force: true });
|
|
151
|
-
throwIfAborted(input.signal);
|
|
152
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
153
|
-
throwIfAborted(input.signal);
|
|
154
|
-
for (const attachment of input.attachments) {
|
|
155
|
-
const base = safeFilename(attachment.filename);
|
|
156
|
-
let collision = 1;
|
|
157
|
-
let filename = collisionName(base, collision);
|
|
158
|
-
while (used.has(filename.toLowerCase())) {
|
|
159
|
-
collision += 1;
|
|
160
|
-
filename = collisionName(base, collision);
|
|
161
|
-
}
|
|
162
|
-
used.add(filename.toLowerCase());
|
|
163
|
-
const path = containedPath(directory, filename);
|
|
164
|
-
const data = await downloadAttachment({
|
|
165
|
-
serverUrl: input.serverUrl,
|
|
166
|
-
token: input.token,
|
|
167
|
-
attachment,
|
|
168
|
-
fetchImpl: input.fetchImpl ?? fetch,
|
|
169
|
-
maxFileBytes,
|
|
170
|
-
maxRedirects: input.maxRedirects ?? 3,
|
|
171
|
-
timeoutMs: input.timeoutMs ?? 10_000,
|
|
172
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
173
|
-
});
|
|
174
|
-
throwIfAborted(input.signal);
|
|
175
|
-
await writeFile(path, data, {
|
|
176
|
-
flag: "wx",
|
|
177
|
-
mode: 0o600,
|
|
178
|
-
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
179
|
-
});
|
|
180
|
-
materialized.push({ ...attachment, filename, path });
|
|
181
|
-
}
|
|
182
|
-
return { directory, attachments: materialized };
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
try {
|
|
186
|
-
await rm(directory, { recursive: true, force: true });
|
|
187
|
-
}
|
|
188
|
-
catch (cleanupError) {
|
|
189
|
-
throw new AggregateError([error, cleanupError], "Attachment materialization failed and its execution directory could not be removed");
|
|
190
|
-
}
|
|
191
|
-
throw error;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
export async function cleanupMaterializedAttachments(directory) {
|
|
195
|
-
await rm(directory, { recursive: true, force: true });
|
|
196
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { readFile, unlink } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
export function boundImDecisionFilePath(runDir) {
|
|
4
|
-
return join(runDir, ".bound-im-decision.json");
|
|
5
|
-
}
|
|
6
|
-
export async function resetBoundImDecisionFile(path) {
|
|
7
|
-
await unlink(path).catch((error) => {
|
|
8
|
-
if (error.code !== "ENOENT")
|
|
9
|
-
throw error;
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
export async function readBoundImDecisionFile(path) {
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
15
|
-
if (!parsed || typeof parsed !== "object")
|
|
16
|
-
return null;
|
|
17
|
-
return parsed.decision === "notify" ? { decision: "notify" } : null;
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
const DEFAULT_DELAYS = Object.freeze([1_000, 2_000, 4_000, 8_000, 16_000, 30_000]);
|
|
2
|
-
export function createCompletionRetransmitter(options) {
|
|
3
|
-
const delays = options.retryDelaysMs?.length ? [...options.retryDelaysMs] : [...DEFAULT_DELAYS];
|
|
4
|
-
const pending = new Map();
|
|
5
|
-
let active = false;
|
|
6
|
-
let stopped = false;
|
|
7
|
-
const delayFor = (attempts) => delays[Math.min(Math.max(attempts - 1, 0), delays.length - 1)];
|
|
8
|
-
const clear = (entry) => {
|
|
9
|
-
if (entry.timer !== null)
|
|
10
|
-
clearTimeout(entry.timer);
|
|
11
|
-
entry.timer = null;
|
|
12
|
-
};
|
|
13
|
-
function schedule(entry) {
|
|
14
|
-
clear(entry);
|
|
15
|
-
if (!active || stopped)
|
|
16
|
-
return;
|
|
17
|
-
const delay = delayFor(entry.attempts);
|
|
18
|
-
entry.timer = setTimeout(() => {
|
|
19
|
-
entry.timer = null;
|
|
20
|
-
attempt(entry);
|
|
21
|
-
}, delay);
|
|
22
|
-
entry.timer.unref?.();
|
|
23
|
-
}
|
|
24
|
-
function attempt(entry) {
|
|
25
|
-
if (!active || stopped)
|
|
26
|
-
return;
|
|
27
|
-
const accepted = options.send(entry.frame);
|
|
28
|
-
if (accepted) {
|
|
29
|
-
entry.attempts += 1;
|
|
30
|
-
const nextDelayMs = delayFor(entry.attempts);
|
|
31
|
-
options.onAttempt?.({
|
|
32
|
-
kind: entry.attempts === 1 ? "sent" : "retried",
|
|
33
|
-
executionId: entry.frame.executionId,
|
|
34
|
-
attempt: entry.attempts,
|
|
35
|
-
nextDelayMs,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
schedule(entry);
|
|
39
|
-
}
|
|
40
|
-
return {
|
|
41
|
-
track(frame) {
|
|
42
|
-
if (stopped || pending.has(frame.executionId))
|
|
43
|
-
return;
|
|
44
|
-
const entry = { frame, attempts: 0, timer: null };
|
|
45
|
-
pending.set(frame.executionId, entry);
|
|
46
|
-
if (active)
|
|
47
|
-
attempt(entry);
|
|
48
|
-
},
|
|
49
|
-
acknowledge(executionId) {
|
|
50
|
-
const entry = pending.get(executionId);
|
|
51
|
-
if (!entry)
|
|
52
|
-
return null;
|
|
53
|
-
clear(entry);
|
|
54
|
-
pending.delete(executionId);
|
|
55
|
-
return entry.attempts;
|
|
56
|
-
},
|
|
57
|
-
pause() {
|
|
58
|
-
active = false;
|
|
59
|
-
for (const entry of pending.values())
|
|
60
|
-
clear(entry);
|
|
61
|
-
},
|
|
62
|
-
resume() {
|
|
63
|
-
if (stopped || active)
|
|
64
|
-
return;
|
|
65
|
-
active = true;
|
|
66
|
-
for (const entry of pending.values())
|
|
67
|
-
attempt(entry);
|
|
68
|
-
},
|
|
69
|
-
stop() {
|
|
70
|
-
stopped = true;
|
|
71
|
-
active = false;
|
|
72
|
-
for (const entry of pending.values())
|
|
73
|
-
clear(entry);
|
|
74
|
-
pending.clear();
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
}
|
package/dist/computer-cli.js
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import { parseArgs } from "node:util";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { assertProfileAgentsRootUnique, daemonHome, inspectProfileAgentsRoot, listProfiles, loadProfile, PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, ProfileAgentsRootConflictError, profileIsPrivate, profilePath, publicProfile, removeProfile, resolveAgentsRoot, saveProfile, windowsDpapiProtector, } from "./computer-profile.js";
|
|
6
|
-
import { buildServiceSpec, doctorService, hardenWindowsProfile, installService, serviceAction, serviceStatus, systemCommandRunner, uninstallService, upgradeDaemon, windowsProfileIsPrivate, } from "./computer-service.js";
|
|
7
|
-
import { defaultProcessController } from "./execution-journal.js";
|
|
8
|
-
import { inspectJournalLock } from "./execution-journal-lock.js";
|
|
9
|
-
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
10
|
-
const COMPUTER_COMMANDS = new Set(["profile", "doctor", "install", "uninstall", "start", "stop", "restart", "status", "upgrade"]);
|
|
11
|
-
export function builtDaemonEntry(moduleUrl = import.meta.url) {
|
|
12
|
-
return moduleUrl.endsWith(".js")
|
|
13
|
-
? fileURLToPath(new URL("./main.js", moduleUrl))
|
|
14
|
-
: "";
|
|
15
|
-
}
|
|
16
|
-
function defaults() {
|
|
17
|
-
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
18
|
-
return {
|
|
19
|
-
env: process.env,
|
|
20
|
-
platform: process.platform,
|
|
21
|
-
userHome: homedir(),
|
|
22
|
-
uid: process.getuid?.(),
|
|
23
|
-
nodePath: process.execPath,
|
|
24
|
-
entryPath: builtDaemonEntry(),
|
|
25
|
-
runner: systemCommandRunner,
|
|
26
|
-
...(process.platform === "win32" ? { profileProtector: windowsDpapiProtector } : {}),
|
|
27
|
-
resolveGlobalNodeModules: async () => {
|
|
28
|
-
const result = await systemCommandRunner(npmCommand, ["root", "--global"]);
|
|
29
|
-
if (result.exitCode !== 0 || !result.stdout.trim()) {
|
|
30
|
-
throw new Error(`Unable to locate global npm modules: ${result.stderr.trim() || `exit ${result.exitCode}`}`);
|
|
31
|
-
}
|
|
32
|
-
return result.stdout.trim();
|
|
33
|
-
},
|
|
34
|
-
readStdin: async () => {
|
|
35
|
-
if (process.stdin.isTTY)
|
|
36
|
-
throw new Error("--token-stdin requires a token on standard input");
|
|
37
|
-
const chunks = [];
|
|
38
|
-
for await (const chunk of process.stdin)
|
|
39
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
40
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
41
|
-
},
|
|
42
|
-
stdout: (text) => process.stdout.write(text),
|
|
43
|
-
stderr: (text) => process.stderr.write(text),
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export function isGlobalDaemonEntry(entryPath, globalNodeModules) {
|
|
47
|
-
return resolve(entryPath) === resolve(globalNodeModules, "@nowcrew", "daemon", "dist", "main.js");
|
|
48
|
-
}
|
|
49
|
-
function usage(td) {
|
|
50
|
-
return [
|
|
51
|
-
td("Computer lifecycle:"),
|
|
52
|
-
" crew-daemon profile save <name> --server-url <url> [--agents-root <path>] [--token-stdin]",
|
|
53
|
-
" crew-daemon profile list|show|remove [<name>]",
|
|
54
|
-
" crew-daemon doctor|install|uninstall|start|stop|restart|status --profile <name>",
|
|
55
|
-
" crew-daemon upgrade [--profile <name>]",
|
|
56
|
-
"",
|
|
57
|
-
td("profile save reads the machine token from CREW_MACHINE_TOKEN, or stdin with --token-stdin."),
|
|
58
|
-
].join("\n") + "\n";
|
|
59
|
-
}
|
|
60
|
-
function requireValue(value, name, td) {
|
|
61
|
-
if (!value)
|
|
62
|
-
throw new Error(td("Missing {{name}}", { name }));
|
|
63
|
-
return value;
|
|
64
|
-
}
|
|
65
|
-
function translatedError(error, td) {
|
|
66
|
-
if (error instanceof ProfileAgentsRootConflictError) {
|
|
67
|
-
return td(PROFILE_AGENTS_ROOT_CONFLICT_MESSAGE, {
|
|
68
|
-
profile: error.profile,
|
|
69
|
-
conflict: error.conflict,
|
|
70
|
-
agentsRoot: error.agentsRoot,
|
|
71
|
-
command: error.command,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
return td(error.message);
|
|
75
|
-
}
|
|
76
|
-
export async function runComputerCommand(argv, overrides = {}) {
|
|
77
|
-
const command = argv[0];
|
|
78
|
-
if (!command || !COMPUTER_COMMANDS.has(command))
|
|
79
|
-
return null;
|
|
80
|
-
const deps = { ...defaults(), ...overrides };
|
|
81
|
-
const lang = detectDaemonLang(deps.env);
|
|
82
|
-
const td = (message, values) => formatDaemonText(lang, message, values);
|
|
83
|
-
try {
|
|
84
|
-
const parsed = parseArgs({
|
|
85
|
-
args: [...argv],
|
|
86
|
-
allowPositionals: true,
|
|
87
|
-
strict: true,
|
|
88
|
-
options: {
|
|
89
|
-
profile: { type: "string" },
|
|
90
|
-
"server-url": { type: "string" },
|
|
91
|
-
"agents-root": { type: "string" },
|
|
92
|
-
"token-stdin": { type: "boolean", default: false },
|
|
93
|
-
help: { type: "boolean", short: "h", default: false },
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
if (parsed.values.help) {
|
|
97
|
-
deps.stdout(usage(td));
|
|
98
|
-
return 0;
|
|
99
|
-
}
|
|
100
|
-
const [, action, positionalName] = parsed.positionals;
|
|
101
|
-
const home = daemonHome(deps.env);
|
|
102
|
-
const profileStorage = {
|
|
103
|
-
platform: deps.platform,
|
|
104
|
-
userHome: deps.userHome,
|
|
105
|
-
...(deps.profileProtector ? { protector: deps.profileProtector } : {}),
|
|
106
|
-
...(deps.platform === "win32"
|
|
107
|
-
? { harden: (path) => hardenWindowsProfile(path, deps.runner) }
|
|
108
|
-
: {}),
|
|
109
|
-
};
|
|
110
|
-
if (command === "profile") {
|
|
111
|
-
if (action === "list") {
|
|
112
|
-
deps.stdout(`${JSON.stringify(await listProfiles(home), null, 2)}\n`);
|
|
113
|
-
return 0;
|
|
114
|
-
}
|
|
115
|
-
const name = requireValue(positionalName, "profile name", td);
|
|
116
|
-
if (action === "show") {
|
|
117
|
-
deps.stdout(`${JSON.stringify(publicProfile(await loadProfile(name, home, profileStorage)), null, 2)}\n`);
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
if (action === "remove") {
|
|
121
|
-
await loadProfile(name, home, profileStorage);
|
|
122
|
-
const spec = buildServiceSpec({
|
|
123
|
-
...deps, profile: name, profileHome: home,
|
|
124
|
-
});
|
|
125
|
-
const status = await serviceStatus(spec, deps.runner);
|
|
126
|
-
if (status.installed)
|
|
127
|
-
throw new Error(td("Service '{{id}}' is still installed; uninstall it before removing the profile", { id: spec.id }));
|
|
128
|
-
await removeProfile(name, home);
|
|
129
|
-
deps.stdout(`${td("Removed profile '{{name}}'.", { name })}\n`);
|
|
130
|
-
return 0;
|
|
131
|
-
}
|
|
132
|
-
if (action !== "save")
|
|
133
|
-
throw new Error(td("Expected profile save, list, show, or remove"));
|
|
134
|
-
const fromStdin = parsed.values["token-stdin"];
|
|
135
|
-
if (fromStdin && deps.env.CREW_MACHINE_TOKEN) {
|
|
136
|
-
throw new Error(td("Choose one token source: CREW_MACHINE_TOKEN or --token-stdin"));
|
|
137
|
-
}
|
|
138
|
-
const machineToken = (fromStdin ? await deps.readStdin() : deps.env.CREW_MACHINE_TOKEN)?.trim();
|
|
139
|
-
if (!machineToken)
|
|
140
|
-
throw new Error(td("Missing machine token; use CREW_MACHINE_TOKEN or --token-stdin"));
|
|
141
|
-
const profile = {
|
|
142
|
-
name,
|
|
143
|
-
serverUrl: requireValue(parsed.values["server-url"], "--server-url", td),
|
|
144
|
-
machineToken,
|
|
145
|
-
...(parsed.values["agents-root"]
|
|
146
|
-
? { agentsRoot: resolveAgentsRoot(parsed.values["agents-root"], deps.userHome, deps.platform) }
|
|
147
|
-
: {}),
|
|
148
|
-
...(deps.env.PATH ? { runtimePath: deps.env.PATH } : {}),
|
|
149
|
-
};
|
|
150
|
-
await saveProfile(profile, home, profileStorage);
|
|
151
|
-
deps.stdout(`${td("Saved profile '{{name}}' with private credentials.", { name })}\n`);
|
|
152
|
-
return 0;
|
|
153
|
-
}
|
|
154
|
-
const profileName = parsed.values.profile;
|
|
155
|
-
if (command === "upgrade") {
|
|
156
|
-
const restartProfile = profileName
|
|
157
|
-
? await loadProfile(profileName, home, profileStorage)
|
|
158
|
-
: null;
|
|
159
|
-
const spec = restartProfile
|
|
160
|
-
? buildServiceSpec({
|
|
161
|
-
...deps,
|
|
162
|
-
profile: restartProfile.name,
|
|
163
|
-
profileHome: home,
|
|
164
|
-
})
|
|
165
|
-
: null;
|
|
166
|
-
if (spec) {
|
|
167
|
-
if (!isGlobalDaemonEntry(deps.entryPath, await deps.resolveGlobalNodeModules())) {
|
|
168
|
-
throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest"));
|
|
169
|
-
}
|
|
170
|
-
const before = await serviceStatus(spec, deps.runner);
|
|
171
|
-
if (!before.installed)
|
|
172
|
-
throw new Error(td("Service '{{id}}' is not installed", { id: spec.id }));
|
|
173
|
-
}
|
|
174
|
-
await upgradeDaemon(deps.platform, deps.runner);
|
|
175
|
-
if (restartProfile) {
|
|
176
|
-
try {
|
|
177
|
-
await assertProfileAgentsRootUnique(restartProfile, home, deps.userHome, profileStorage);
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
if (!(error instanceof ProfileAgentsRootConflictError))
|
|
181
|
-
throw error;
|
|
182
|
-
deps.stdout(`${td("Upgraded daemon but skipped restart for '{{name}}': {{reason}}", { name: restartProfile.name, reason: translatedError(error, td) })}\n`);
|
|
183
|
-
return 0;
|
|
184
|
-
}
|
|
185
|
-
await serviceAction(spec, "restart", deps.runner);
|
|
186
|
-
deps.stdout(`${td("Upgraded daemon and restart request accepted for '{{name}}'. Verify with status.", { name: restartProfile.name })}\n`);
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
deps.stdout(`${td("Upgraded daemon. Installed services were not restarted; pass --profile to restart one.")}\n`);
|
|
190
|
-
}
|
|
191
|
-
return 0;
|
|
192
|
-
}
|
|
193
|
-
const name = requireValue(profileName, "--profile", td);
|
|
194
|
-
const profile = await loadProfile(name, home, profileStorage);
|
|
195
|
-
if (command === "install" || command === "start" || command === "restart") {
|
|
196
|
-
await assertProfileAgentsRootUnique(profile, home, deps.userHome, profileStorage);
|
|
197
|
-
}
|
|
198
|
-
if (!deps.entryPath.endsWith(".js")) {
|
|
199
|
-
throw new Error(td("Service lifecycle requires the built daemon entry (.js), not a TypeScript development entry"));
|
|
200
|
-
}
|
|
201
|
-
const spec = buildServiceSpec({
|
|
202
|
-
...deps, profile: name, profileHome: home,
|
|
203
|
-
});
|
|
204
|
-
if (command === "install") {
|
|
205
|
-
if (!isGlobalDaemonEntry(deps.entryPath, await deps.resolveGlobalNodeModules())) {
|
|
206
|
-
throw new Error(td("Service lifecycle requires a global @nowcrew/daemon install; run npm install --global @nowcrew/daemon@latest"));
|
|
207
|
-
}
|
|
208
|
-
await installService(spec, deps.runner);
|
|
209
|
-
deps.stdout(`${td("Installed '{{id}}'. Use status to confirm runtime state.", { id: spec.id })}\n`);
|
|
210
|
-
return 0;
|
|
211
|
-
}
|
|
212
|
-
if (command === "uninstall") {
|
|
213
|
-
await uninstallService(spec, deps.runner);
|
|
214
|
-
deps.stdout(`${td("Uninstalled '{{id}}'.", { id: spec.id })}\n`);
|
|
215
|
-
return 0;
|
|
216
|
-
}
|
|
217
|
-
if (command === "start" || command === "stop" || command === "restart") {
|
|
218
|
-
await serviceAction(spec, command, deps.runner);
|
|
219
|
-
deps.stdout(`${td("{{action}} request accepted for '{{id}}'. Verify with status.", {
|
|
220
|
-
action: command,
|
|
221
|
-
id: spec.id,
|
|
222
|
-
})}\n`);
|
|
223
|
-
return 0;
|
|
224
|
-
}
|
|
225
|
-
if (command === "status") {
|
|
226
|
-
const status = await serviceStatus(spec, deps.runner);
|
|
227
|
-
deps.stdout(`${JSON.stringify({ installed: status.installed, loaded: status.loaded, running: status.running, detail: (status.stdout || status.stderr).trim() }, null, 2)}\n`);
|
|
228
|
-
return status.installed && status.running ? 0 : 3;
|
|
229
|
-
}
|
|
230
|
-
if (command === "doctor") {
|
|
231
|
-
const privateFile = deps.platform === "win32"
|
|
232
|
-
? await windowsProfileIsPrivate(profilePath(name, home), deps.runner)
|
|
233
|
-
: await profileIsPrivate(name, home, deps.platform);
|
|
234
|
-
const serviceChecks = await doctorService(spec, privateFile, deps.runner);
|
|
235
|
-
const rootInspection = await inspectProfileAgentsRoot(profile, home, deps.userHome, profileStorage);
|
|
236
|
-
const journalPath = resolve(rootInspection.agentsRoot, ".crew", "executions");
|
|
237
|
-
const journalLock = await inspectJournalLock({
|
|
238
|
-
directory: journalPath,
|
|
239
|
-
inspectIdentity: defaultProcessController.inspectIdentity,
|
|
240
|
-
});
|
|
241
|
-
const checks = [
|
|
242
|
-
...serviceChecks,
|
|
243
|
-
{
|
|
244
|
-
name: "agents-root-unique",
|
|
245
|
-
ok: rootInspection.duplicateProfiles.length === 0,
|
|
246
|
-
detail: rootInspection.duplicateProfiles.length === 0
|
|
247
|
-
? "unique"
|
|
248
|
-
: `also used by: ${rootInspection.duplicateProfiles.join(", ")}`,
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
name: "journal-lock-health",
|
|
252
|
-
ok: journalLock.status === "unlocked" || journalLock.status === "owned",
|
|
253
|
-
detail: journalLock.detail,
|
|
254
|
-
},
|
|
255
|
-
];
|
|
256
|
-
const registered = (await serviceStatus(spec, deps.runner)).installed;
|
|
257
|
-
deps.stdout(`${JSON.stringify({
|
|
258
|
-
agentsRoot: rootInspection.agentsRoot,
|
|
259
|
-
journalPath,
|
|
260
|
-
duplicateProfiles: rootInspection.duplicateProfiles,
|
|
261
|
-
journalLock,
|
|
262
|
-
checks,
|
|
263
|
-
registered,
|
|
264
|
-
}, null, 2)}\n`);
|
|
265
|
-
return checks.every((check) => check.ok) ? 0 : 4;
|
|
266
|
-
}
|
|
267
|
-
deps.stderr(usage(td));
|
|
268
|
-
return 2;
|
|
269
|
-
}
|
|
270
|
-
catch (error) {
|
|
271
|
-
deps.stderr(`crew-daemon: ${translatedError(error, td)}\n`);
|
|
272
|
-
return 1;
|
|
273
|
-
}
|
|
274
|
-
}
|