@cydm/happy-elves 0.1.0-beta.270 → 0.1.0-beta.271
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/apps/cli/dist/commands/lib/args.js +4 -0
- package/apps/cli/dist/commands/lib/usage.js +4 -0
- package/apps/cli/dist/commands/remote.js +130 -1
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/paths.d.ts +2 -0
- package/apps/daemon/dist/paths.js +2 -0
- package/apps/daemon/dist/relay/connection.js +5 -0
- package/apps/daemon/dist/relay/lifecycle.d.ts +7 -0
- package/apps/daemon/dist/relay/lifecycle.js +476 -0
- package/apps/daemon/dist/relay/register.js +1 -0
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/controller-handlers.js +33 -0
- package/apps/relay/dist/machine-handlers.js +12 -0
- package/apps/relay/dist/projections.js +2 -0
- package/package.json +1 -1
- package/packages/client/dist/client.d.ts +2 -1
- package/packages/client/dist/client.js +16 -1
- package/packages/client/dist/index.d.ts +1 -1
- package/packages/client/dist/transport.d.ts +5 -0
- package/packages/client/dist/transport.js +7 -0
- package/packages/client/dist/types.d.ts +17 -1
- package/packages/shared/dist/protocol-schemas.d.ts +78 -2
- package/packages/shared/dist/protocol-schemas.js +30 -0
- package/packages/shared/dist/protocol-types.d.ts +33 -0
- package/packages/shared/dist/protocol.d.ts +30 -2
- package/packages/shared/dist/protocol.js +33 -1
|
@@ -32,6 +32,7 @@ const valueFlags = new Set([
|
|
|
32
32
|
"memory-project",
|
|
33
33
|
"memory-top-n",
|
|
34
34
|
"name",
|
|
35
|
+
"package-url",
|
|
35
36
|
"model",
|
|
36
37
|
"mode",
|
|
37
38
|
"output",
|
|
@@ -61,6 +62,7 @@ const valueFlags = new Set([
|
|
|
61
62
|
"sessions",
|
|
62
63
|
"session",
|
|
63
64
|
"secret",
|
|
65
|
+
"sha256",
|
|
64
66
|
"since",
|
|
65
67
|
"source",
|
|
66
68
|
"stop-on",
|
|
@@ -81,6 +83,8 @@ const valueFlags = new Set([
|
|
|
81
83
|
"url",
|
|
82
84
|
"turn",
|
|
83
85
|
"until",
|
|
86
|
+
"version",
|
|
87
|
+
"wait-timeout",
|
|
84
88
|
]);
|
|
85
89
|
const booleanFlags = new Set([
|
|
86
90
|
"all",
|
|
@@ -30,6 +30,10 @@ Check local controller config, relay reachability, and daemon state.
|
|
|
30
30
|
happy-elves remote add-controller [--device-id <id>] [--device-name <name>] --json
|
|
31
31
|
happy-elves remote rename <deviceId> --name <name> --json
|
|
32
32
|
happy-elves remote revoke <deviceId> --json
|
|
33
|
+
happy-elves remote daemon-status <machineId> [--json]
|
|
34
|
+
happy-elves remote daemon-restart <machineId> [--wait] [--timeout 2m] [--json]
|
|
35
|
+
happy-elves remote daemon-update <machineId> [--version latest|0.1.0-beta.N] [--package-url <url> --sha256 <hex>] [--wait] [--timeout 10m] [--json]
|
|
36
|
+
happy-elves remote daemon-logs <machineId> [--tail 100] [--json]
|
|
33
37
|
`,
|
|
34
38
|
machine: `Usage:
|
|
35
39
|
happy-elves machine list [--verbose] [--json]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
|
-
import { ControllerClient, configPath, ok, parseControllerJoinUrl, parseDurationMs, randomId, readConfig, relayHealth, requirePositional, requireRelayUrl, requireString, showMachine, wantsJson, wantsVerbose, writeConfig } from "./lib/index.js";
|
|
2
|
+
import { CliError, ControllerClient, configPath, ok, parseControllerJoinUrl, parseDurationMs, randomId, readConfig, relayHealth, requirePositional, requireRelayUrl, requireString, showMachine, wantsJson, wantsVerbose, writeConfig } from "./lib/index.js";
|
|
3
3
|
export async function handleRemote({ domain, action, positional, flags }) {
|
|
4
4
|
if (domain === "remote" && action === "devices") {
|
|
5
5
|
const config = await readConfig(flags);
|
|
@@ -81,6 +81,56 @@ export async function handleRemote({ domain, action, positional, flags }) {
|
|
|
81
81
|
ok("remote.status", data);
|
|
82
82
|
return true;
|
|
83
83
|
}
|
|
84
|
+
if (domain === "remote" && action === "daemon-status") {
|
|
85
|
+
const config = await readConfig(flags);
|
|
86
|
+
const machineId = requirePositional(positional[0], "machineId");
|
|
87
|
+
const result = await new ControllerClient(config).daemonLifecycle({
|
|
88
|
+
machineId,
|
|
89
|
+
action: "status",
|
|
90
|
+
waitTimeoutMs: parseDurationMs(flags["wait-timeout"], 45_000),
|
|
91
|
+
});
|
|
92
|
+
printDaemonLifecycle("remote.daemon-status", result, flags);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (domain === "remote" && action === "daemon-logs") {
|
|
96
|
+
const config = await readConfig(flags);
|
|
97
|
+
const machineId = requirePositional(positional[0], "machineId");
|
|
98
|
+
const tail = typeof flags.tail === "string" ? Number(flags.tail) : 100;
|
|
99
|
+
if (!Number.isInteger(tail) || tail <= 0)
|
|
100
|
+
throw new CliError("--tail must be a positive integer", "INVALID_ARGUMENT");
|
|
101
|
+
const result = await new ControllerClient(config).daemonLifecycle({
|
|
102
|
+
machineId,
|
|
103
|
+
action: "logs",
|
|
104
|
+
tail,
|
|
105
|
+
waitTimeoutMs: parseDurationMs(flags["wait-timeout"], 45_000),
|
|
106
|
+
});
|
|
107
|
+
printDaemonLifecycle("remote.daemon-logs", result, flags);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (domain === "remote" && (action === "daemon-restart" || action === "daemon-update")) {
|
|
111
|
+
const config = await readConfig(flags);
|
|
112
|
+
const client = new ControllerClient(config);
|
|
113
|
+
const machineId = requirePositional(positional[0], "machineId");
|
|
114
|
+
const isUpdate = action === "daemon-update";
|
|
115
|
+
const result = await client.daemonLifecycle({
|
|
116
|
+
machineId,
|
|
117
|
+
action: isUpdate ? "update" : "restart",
|
|
118
|
+
targetVersion: isUpdate ? (typeof flags.version === "string" ? flags.version : "latest") : undefined,
|
|
119
|
+
packageUrl: isUpdate && typeof flags["package-url"] === "string" ? flags["package-url"] : undefined,
|
|
120
|
+
sha256: isUpdate && typeof flags.sha256 === "string" ? flags.sha256 : undefined,
|
|
121
|
+
waitTimeoutMs: parseDurationMs(flags["wait-timeout"], 45_000),
|
|
122
|
+
});
|
|
123
|
+
let finalResult = result;
|
|
124
|
+
if (result.accepted === true && flags.wait) {
|
|
125
|
+
finalResult = await waitForDaemonLifecycle(client, machineId, result.requestId, isUpdate ? "update" : "restart", {
|
|
126
|
+
timeoutMs: parseDurationMs(flags.timeout, isUpdate ? 10 * 60_000 : 2 * 60_000),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
printDaemonLifecycle(isUpdate ? "remote.daemon-update" : "remote.daemon-restart", finalResult, flags);
|
|
130
|
+
if (finalResult.accepted === false || finalResult.updateState?.status === "failed")
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
84
134
|
if (domain === "remote" &&
|
|
85
135
|
((action === "pairing" && positional[0] === "new") || (action === "machine-pairing" && positional[0] === "new"))) {
|
|
86
136
|
const config = await readConfig(flags);
|
|
@@ -148,4 +198,83 @@ export async function handleRemote({ domain, action, positional, flags }) {
|
|
|
148
198
|
}
|
|
149
199
|
return false;
|
|
150
200
|
}
|
|
201
|
+
function printDaemonLifecycle(type, result, flags) {
|
|
202
|
+
const success = result.accepted !== false && result.updateState?.status !== "failed";
|
|
203
|
+
if (wantsJson(flags)) {
|
|
204
|
+
ok(type, result, { requestId: result.requestId, machineId: result.machineId }, success);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const state = result.updateState;
|
|
208
|
+
const summary = [
|
|
209
|
+
`Machine: ${result.machineId}`,
|
|
210
|
+
`Action: ${result.action}`,
|
|
211
|
+
`Version: ${result.version ?? "-"}`,
|
|
212
|
+
`Running: ${result.running === undefined ? "-" : result.running ? "yes" : "no"}`,
|
|
213
|
+
`Install: ${result.installMode ?? "-"}`,
|
|
214
|
+
];
|
|
215
|
+
if (result.updateSupported === false)
|
|
216
|
+
summary.push(`Update: unsupported (${result.updateUnsupportedReason ?? "unknown"})`);
|
|
217
|
+
if (result.accepted !== undefined)
|
|
218
|
+
summary.push(`Accepted: ${result.accepted ? "yes" : "no"}`);
|
|
219
|
+
if (state) {
|
|
220
|
+
summary.push(`Update state: ${state.status}`);
|
|
221
|
+
if (state.targetVersion)
|
|
222
|
+
summary.push(`Target: ${state.targetVersion}`);
|
|
223
|
+
if (state.toVersion)
|
|
224
|
+
summary.push(`Installed: ${state.toVersion}`);
|
|
225
|
+
if (state.logPath)
|
|
226
|
+
summary.push(`Log: ${state.logPath}`);
|
|
227
|
+
if (state.error)
|
|
228
|
+
summary.push(`Error: ${state.error}`);
|
|
229
|
+
}
|
|
230
|
+
for (const line of summary)
|
|
231
|
+
console.log(line);
|
|
232
|
+
if (result.logs?.length) {
|
|
233
|
+
console.log("Logs:");
|
|
234
|
+
for (const line of result.logs)
|
|
235
|
+
console.log(line);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function waitForDaemonLifecycle(client, machineId, requestId, action, options) {
|
|
239
|
+
const deadline = Date.now() + Math.max(1, options.timeoutMs);
|
|
240
|
+
let lastResult;
|
|
241
|
+
let lastError;
|
|
242
|
+
while (Date.now() < deadline) {
|
|
243
|
+
try {
|
|
244
|
+
const result = await client.daemonLifecycle({ machineId, action: "status", waitTimeoutMs: 15_000 });
|
|
245
|
+
lastResult = result;
|
|
246
|
+
const state = result.updateState;
|
|
247
|
+
if (state?.requestId === requestId) {
|
|
248
|
+
if (state.status === "succeeded" || state.status === "failed")
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
else if (action === "restart" && result.running === true) {
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
lastError = error;
|
|
257
|
+
const code = errorCode(error);
|
|
258
|
+
if (code !== "MACHINE_OFFLINE" && code !== "COMMAND_TIMEOUT" && code !== "RELAY_CONNECT_TIMEOUT")
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
await sleep(1000);
|
|
262
|
+
}
|
|
263
|
+
if (lastResult)
|
|
264
|
+
return {
|
|
265
|
+
...lastResult,
|
|
266
|
+
accepted: false,
|
|
267
|
+
updateUnsupportedReason: `WAIT_TIMEOUT_${requestId}`,
|
|
268
|
+
};
|
|
269
|
+
throw new CliError(`Timed out waiting for daemon ${action}: ${errorMessage(lastError)}`, "COMMAND_TIMEOUT");
|
|
270
|
+
}
|
|
271
|
+
function errorCode(error) {
|
|
272
|
+
return typeof error === "object" && error !== null && "code" in error ? String(error.code) : undefined;
|
|
273
|
+
}
|
|
274
|
+
function errorMessage(error) {
|
|
275
|
+
return error instanceof Error ? error.message : String(error ?? "unknown");
|
|
276
|
+
}
|
|
277
|
+
function sleep(ms) {
|
|
278
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
279
|
+
}
|
|
151
280
|
//# sourceMappingURL=remote.js.map
|
package/apps/cli/package.json
CHANGED
|
@@ -8,6 +8,8 @@ export declare const loopStoreLockPath: string;
|
|
|
8
8
|
export declare const gatewayStorePath: string;
|
|
9
9
|
export declare const scheduleStorePath: string;
|
|
10
10
|
export declare const memoryRootDir: string;
|
|
11
|
+
export declare const updatesDir: string;
|
|
12
|
+
export declare const updateStatePath: string;
|
|
11
13
|
export declare const pidPath: string;
|
|
12
14
|
export declare const daemonScriptPath: string;
|
|
13
15
|
export declare const daemonScriptRelativePath: string;
|
|
@@ -13,6 +13,8 @@ export const loopStoreLockPath = path.join(configDir, "loops.json.lock");
|
|
|
13
13
|
export const gatewayStorePath = path.join(configDir, "gateway.json");
|
|
14
14
|
export const scheduleStorePath = path.join(configDir, "schedules.json");
|
|
15
15
|
export const memoryRootDir = path.join(configDir, "memory");
|
|
16
|
+
export const updatesDir = path.join(configDir, "updates");
|
|
17
|
+
export const updateStatePath = path.join(configDir, "update-state.json");
|
|
16
18
|
export const pidPath = path.join(configDir, "daemon.pid");
|
|
17
19
|
export const daemonScriptPath = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./cli.ts" : "./cli.js", import.meta.url));
|
|
18
20
|
export const daemonScriptRelativePath = path.relative(process.cwd(), daemonScriptPath);
|
|
@@ -12,6 +12,7 @@ import { handleFork, handleRename, handleRewind } from "../session/primitives.js
|
|
|
12
12
|
import { handleReconcileTurn } from "../session/reconcile.js";
|
|
13
13
|
import { parseRelayMessage, relayWsUrl } from "../relay-http.js";
|
|
14
14
|
import { handleDevExec, handleDiagnose } from "./devtools.js";
|
|
15
|
+
import { handleDaemonLifecycleCommand } from "./lifecycle.js";
|
|
15
16
|
import { registerMachine } from "./register.js";
|
|
16
17
|
import { clearActiveRelaySocket, configureRelaySender, flushRelayOutbox, handleMachineMessageAck, send, sendCommandResponse, setActiveRelaySocket, } from "./send.js";
|
|
17
18
|
async function handleServerMessage(ws, config, message) {
|
|
@@ -63,6 +64,10 @@ async function handleServerMessage(ws, config, message) {
|
|
|
63
64
|
await handleDevExec(ws, config, message);
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
67
|
+
if (message.type === "machine:daemonLifecycle") {
|
|
68
|
+
await handleDaemonLifecycleCommand(ws, config, message);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
66
71
|
if (message.type === "machine:gateway") {
|
|
67
72
|
await handleGatewayCommand(ws, config, message);
|
|
68
73
|
return;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MachineCommand } from "../../../../packages/shared/dist/index.js";
|
|
2
|
+
import type { DaemonConfig } from "../types.js";
|
|
3
|
+
type DaemonLifecycleCommand = Extract<MachineCommand, {
|
|
4
|
+
type: "machine:daemonLifecycle";
|
|
5
|
+
}>;
|
|
6
|
+
export declare function handleDaemonLifecycleCommand(ws: WebSocket, config: DaemonConfig, command: DaemonLifecycleCommand): Promise<void>;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import daemonPackage from "../../package.json" with { type: "json" };
|
|
5
|
+
import { appendAudit } from "../audit.js";
|
|
6
|
+
import { auditLogPath, configDir, daemonScriptPath, pidPath, updateStatePath, updatesDir } from "../paths.js";
|
|
7
|
+
import { localStatus } from "../process.js";
|
|
8
|
+
import { claimCommandRequest, sendCommandResponse } from "./send.js";
|
|
9
|
+
const defaultTargetPackage = "@cydm/happy-elves";
|
|
10
|
+
const maxLogTail = 2000;
|
|
11
|
+
export async function handleDaemonLifecycleCommand(ws, config, command) {
|
|
12
|
+
if (!claimCommandRequest(ws, command.requestId))
|
|
13
|
+
return;
|
|
14
|
+
const result = await executeDaemonLifecycleCommand(command);
|
|
15
|
+
await appendAudit({
|
|
16
|
+
machineId: config.machineId,
|
|
17
|
+
actor: "user",
|
|
18
|
+
action: `daemon.lifecycle.${command.action}`,
|
|
19
|
+
summary: `Daemon lifecycle ${command.action}: ${result.accepted === false ? "rejected" : "accepted"}`,
|
|
20
|
+
evidence: {
|
|
21
|
+
requestId: command.requestId,
|
|
22
|
+
targetVersion: command.targetVersion,
|
|
23
|
+
packageUrl: command.packageUrl ? redactUrl(command.packageUrl) : undefined,
|
|
24
|
+
status: result.updateState?.status,
|
|
25
|
+
updateSupported: result.updateSupported,
|
|
26
|
+
updateUnsupportedReason: result.updateUnsupportedReason,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
await sendCommandResponse(ws, {
|
|
30
|
+
type: "machine:daemonLifecycleResult",
|
|
31
|
+
requestId: command.requestId,
|
|
32
|
+
machineId: config.machineId,
|
|
33
|
+
result,
|
|
34
|
+
});
|
|
35
|
+
if ((command.action === "restart" || command.action === "update") && result.accepted === true && result.updateState) {
|
|
36
|
+
spawnUpdaterRunner(result.updateState.requestId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function executeDaemonLifecycleCommand(command) {
|
|
40
|
+
if (command.action === "status") {
|
|
41
|
+
return await lifecycleStatus("status");
|
|
42
|
+
}
|
|
43
|
+
if (command.action === "logs") {
|
|
44
|
+
return {
|
|
45
|
+
...(await lifecycleStatus("logs")),
|
|
46
|
+
logs: await readLifecycleLogTail(command.tail ?? 100),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (command.action === "restart") {
|
|
50
|
+
const state = await createUpdateIntent(command, "restart");
|
|
51
|
+
await writeUpdateState(state);
|
|
52
|
+
await writeRunnerFile(state.requestId, runnerInput(state, "restart"));
|
|
53
|
+
return {
|
|
54
|
+
...(await lifecycleStatus("restart")),
|
|
55
|
+
accepted: true,
|
|
56
|
+
updateState: state,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (command.action === "update") {
|
|
60
|
+
const status = await lifecycleStatus("update");
|
|
61
|
+
if (!status.updateSupported) {
|
|
62
|
+
return {
|
|
63
|
+
...status,
|
|
64
|
+
accepted: false,
|
|
65
|
+
updateUnsupportedReason: status.updateUnsupportedReason ?? "UPDATE_UNSUPPORTED_INSTALL_MODE",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (command.packageUrl && !command.sha256) {
|
|
69
|
+
return {
|
|
70
|
+
...status,
|
|
71
|
+
accepted: false,
|
|
72
|
+
updateUnsupportedReason: "PACKAGE_SHA256_REQUIRED",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (command.sha256 && !command.packageUrl) {
|
|
76
|
+
return {
|
|
77
|
+
...status,
|
|
78
|
+
accepted: false,
|
|
79
|
+
updateUnsupportedReason: "PACKAGE_URL_REQUIRED_FOR_SHA256",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const state = await createUpdateIntent(command, "update");
|
|
83
|
+
await writeUpdateState(state);
|
|
84
|
+
await writeRunnerFile(state.requestId, runnerInput(state, "update"));
|
|
85
|
+
return {
|
|
86
|
+
...status,
|
|
87
|
+
accepted: true,
|
|
88
|
+
updateState: state,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
action: command.action,
|
|
93
|
+
accepted: false,
|
|
94
|
+
updateUnsupportedReason: `Unsupported daemon lifecycle action: ${command.action}`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function lifecycleStatus(action) {
|
|
98
|
+
const local = await localStatus();
|
|
99
|
+
const installMode = detectInstallMode();
|
|
100
|
+
const updateState = await readUpdateState();
|
|
101
|
+
return {
|
|
102
|
+
action,
|
|
103
|
+
version: daemonPackage.version,
|
|
104
|
+
pid: local.pid,
|
|
105
|
+
running: local.running,
|
|
106
|
+
platform: process.platform,
|
|
107
|
+
arch: process.arch,
|
|
108
|
+
installMode,
|
|
109
|
+
updateSupported: installMode === "npm-global",
|
|
110
|
+
...(installMode === "npm-global" ? {} : { updateUnsupportedReason: "UPDATE_UNSUPPORTED_DEV_INSTALL" }),
|
|
111
|
+
...(updateState ? { updateState } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function detectInstallMode() {
|
|
115
|
+
if (process.env.HAPPY_ELVES_TEST_INSTALL_MODE === "npm-global")
|
|
116
|
+
return "npm-global";
|
|
117
|
+
if (process.env.HAPPY_ELVES_TEST_INSTALL_MODE === "repo-dev")
|
|
118
|
+
return "repo-dev";
|
|
119
|
+
if (daemonPackage.name === "@cydm/happy-elves-daemon")
|
|
120
|
+
return "npm-global";
|
|
121
|
+
if (daemonPackage.private === true)
|
|
122
|
+
return "repo-dev";
|
|
123
|
+
return "unknown";
|
|
124
|
+
}
|
|
125
|
+
async function createUpdateIntent(command, action) {
|
|
126
|
+
const logPath = path.join(updatesDir, command.requestId, "updater.log");
|
|
127
|
+
const targetVersion = action === "update" ? (command.targetVersion ?? "latest") : undefined;
|
|
128
|
+
const packageSpec = action === "update" && !command.packageUrl ? `${defaultTargetPackage}@${targetVersion}` : undefined;
|
|
129
|
+
return {
|
|
130
|
+
requestId: command.requestId,
|
|
131
|
+
action,
|
|
132
|
+
...(targetVersion ? { targetVersion } : {}),
|
|
133
|
+
...(packageSpec ? { packageSpec } : {}),
|
|
134
|
+
...(command.packageUrl ? { packageUrl: command.packageUrl } : {}),
|
|
135
|
+
...(command.sha256 ? { sha256: command.sha256 } : {}),
|
|
136
|
+
status: "accepted",
|
|
137
|
+
fromVersion: daemonPackage.version,
|
|
138
|
+
startedAt: new Date().toISOString(),
|
|
139
|
+
logPath,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function readUpdateState() {
|
|
143
|
+
try {
|
|
144
|
+
return JSON.parse(await fs.readFile(updateStatePath, "utf8"));
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error.code === "ENOENT")
|
|
148
|
+
return undefined;
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async function writeUpdateState(state) {
|
|
153
|
+
await fs.mkdir(path.dirname(updateStatePath), { recursive: true });
|
|
154
|
+
const tempPath = `${updateStatePath}.${process.pid}.${Date.now()}.tmp`;
|
|
155
|
+
await fs.writeFile(tempPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
156
|
+
await fs.rename(tempPath, updateStatePath);
|
|
157
|
+
}
|
|
158
|
+
function runnerInput(state, action) {
|
|
159
|
+
return {
|
|
160
|
+
action,
|
|
161
|
+
configDir,
|
|
162
|
+
daemonScriptPath,
|
|
163
|
+
homeEnv: process.env.HAPPY_ELVES_HOME ?? configDir,
|
|
164
|
+
logPath: state.logPath,
|
|
165
|
+
packageSpec: state.packageSpec,
|
|
166
|
+
packageUrl: state.packageUrl,
|
|
167
|
+
pidPath,
|
|
168
|
+
requestId: state.requestId,
|
|
169
|
+
sha256: state.sha256,
|
|
170
|
+
startMode: action === "restart" ? "current-script" : "global-cli",
|
|
171
|
+
statePath: updateStatePath,
|
|
172
|
+
targetPackage: defaultTargetPackage,
|
|
173
|
+
targetVersion: state.targetVersion,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function writeRunnerFile(requestId, input) {
|
|
177
|
+
const runnerDir = path.join(updatesDir, requestId);
|
|
178
|
+
await fs.mkdir(runnerDir, { recursive: true });
|
|
179
|
+
await fs.writeFile(path.join(runnerDir, "runner.mjs"), renderRunnerScript(input), { mode: 0o700 });
|
|
180
|
+
}
|
|
181
|
+
function spawnUpdaterRunner(requestId) {
|
|
182
|
+
if (process.env.HAPPY_ELVES_TEST_NO_SPAWN_UPDATER === "1")
|
|
183
|
+
return;
|
|
184
|
+
const runnerPath = path.join(updatesDir, requestId, "runner.mjs");
|
|
185
|
+
const child = spawn(process.execPath, [runnerPath], {
|
|
186
|
+
cwd: process.cwd(),
|
|
187
|
+
detached: true,
|
|
188
|
+
env: { ...process.env, HAPPY_ELVES_HOME: process.env.HAPPY_ELVES_HOME ?? configDir },
|
|
189
|
+
stdio: "ignore",
|
|
190
|
+
windowsHide: true,
|
|
191
|
+
});
|
|
192
|
+
child.unref();
|
|
193
|
+
}
|
|
194
|
+
async function readLifecycleLogTail(tail) {
|
|
195
|
+
const boundedTail = Math.max(1, Math.min(maxLogTail, Math.floor(tail)));
|
|
196
|
+
const state = await readUpdateState();
|
|
197
|
+
const paths = [auditLogPath, state?.logPath].filter((item) => Boolean(item));
|
|
198
|
+
const lines = [];
|
|
199
|
+
for (const filePath of paths) {
|
|
200
|
+
try {
|
|
201
|
+
const text = await fs.readFile(filePath, "utf8");
|
|
202
|
+
const label = filePath === auditLogPath ? "daemon" : "updater";
|
|
203
|
+
for (const line of text.split(/\r?\n/u).filter(Boolean).slice(-boundedTail)) {
|
|
204
|
+
lines.push(`[${label}] ${line}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error.code !== "ENOENT") {
|
|
209
|
+
lines.push(`[log-error] ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return lines.slice(-boundedTail);
|
|
214
|
+
}
|
|
215
|
+
function redactUrl(value) {
|
|
216
|
+
try {
|
|
217
|
+
const url = new URL(value);
|
|
218
|
+
if (url.password)
|
|
219
|
+
url.password = "redacted";
|
|
220
|
+
if (url.username)
|
|
221
|
+
url.username = "redacted";
|
|
222
|
+
return url.toString();
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function renderRunnerScript(input) {
|
|
229
|
+
const runner = {
|
|
230
|
+
...input,
|
|
231
|
+
startDelayMs: 750,
|
|
232
|
+
};
|
|
233
|
+
const inputJson = JSON.stringify(runner);
|
|
234
|
+
return `#!/usr/bin/env node
|
|
235
|
+
import { spawn } from "node:child_process";
|
|
236
|
+
import { createHash } from "node:crypto";
|
|
237
|
+
import fs from "node:fs/promises";
|
|
238
|
+
import path from "node:path";
|
|
239
|
+
|
|
240
|
+
const input = ${inputJson};
|
|
241
|
+
|
|
242
|
+
main().catch(async (error) => {
|
|
243
|
+
await fail(error);
|
|
244
|
+
process.exitCode = 1;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
async function main() {
|
|
248
|
+
await fs.mkdir(path.dirname(input.logPath), { recursive: true });
|
|
249
|
+
await delay(input.startDelayMs);
|
|
250
|
+
await updateState({ status: "running" });
|
|
251
|
+
await log("runner started " + JSON.stringify({ action: input.action, targetVersion: input.targetVersion, packageSpec: input.packageSpec, packageUrl: input.packageUrl ? redactUrl(input.packageUrl) : undefined }));
|
|
252
|
+
await stopDaemon();
|
|
253
|
+
if (input.action === "update") {
|
|
254
|
+
await installPackage();
|
|
255
|
+
}
|
|
256
|
+
await startDaemon();
|
|
257
|
+
await waitForDaemonRunning();
|
|
258
|
+
const version = await currentHappyElvesVersion().catch(() => undefined);
|
|
259
|
+
await updateState({ status: "succeeded", finishedAt: new Date().toISOString(), ...(version ? { toVersion: version } : {}) });
|
|
260
|
+
await log("runner succeeded");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function fail(error) {
|
|
264
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
265
|
+
await log("runner failed: " + message).catch(() => {});
|
|
266
|
+
await updateState({ status: "failed", finishedAt: new Date().toISOString(), error: String(error instanceof Error ? error.message : error) }).catch(() => {});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function updateState(patch) {
|
|
270
|
+
const current = JSON.parse(await fs.readFile(input.statePath, "utf8"));
|
|
271
|
+
const next = { ...current, ...patch };
|
|
272
|
+
const tempPath = input.statePath + "." + process.pid + "." + Date.now() + ".tmp";
|
|
273
|
+
await fs.writeFile(tempPath, JSON.stringify(next, null, 2) + "\\n", { mode: 0o600 });
|
|
274
|
+
await fs.rename(tempPath, input.statePath);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function log(line) {
|
|
278
|
+
await fs.mkdir(path.dirname(input.logPath), { recursive: true });
|
|
279
|
+
await fs.appendFile(input.logPath, "[" + new Date().toISOString() + "] " + line + "\\n", { mode: 0o600 });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function stopDaemon() {
|
|
283
|
+
await updateState({ status: "stopping" });
|
|
284
|
+
const pid = await readPid();
|
|
285
|
+
if (!pid || pid === process.pid) {
|
|
286
|
+
await fs.rm(input.pidPath, { force: true }).catch(() => {});
|
|
287
|
+
await log("no managed daemon pid to stop");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
await log("stopping daemon pid " + pid);
|
|
291
|
+
try {
|
|
292
|
+
process.kill(pid, "SIGTERM");
|
|
293
|
+
} catch (error) {
|
|
294
|
+
await log("SIGTERM failed: " + String(error instanceof Error ? error.message : error));
|
|
295
|
+
}
|
|
296
|
+
const deadline = Date.now() + 10_000;
|
|
297
|
+
while (Date.now() < deadline) {
|
|
298
|
+
if (!isPidAlive(pid)) {
|
|
299
|
+
await fs.rm(input.pidPath, { force: true }).catch(() => {});
|
|
300
|
+
await log("daemon stopped");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await delay(200);
|
|
304
|
+
}
|
|
305
|
+
throw new Error("Timed out stopping daemon pid " + pid);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function installPackage() {
|
|
309
|
+
await updateState({ status: "installing" });
|
|
310
|
+
if (input.packageUrl) {
|
|
311
|
+
const tarballPath = path.join(path.dirname(input.logPath), "package.tgz");
|
|
312
|
+
await download(input.packageUrl, tarballPath);
|
|
313
|
+
if (!input.sha256) throw new Error("packageUrl update requires sha256");
|
|
314
|
+
const actual = await sha256File(tarballPath);
|
|
315
|
+
if (actual.toLowerCase() !== String(input.sha256).toLowerCase()) {
|
|
316
|
+
throw new Error("Package sha256 mismatch: expected " + input.sha256 + " got " + actual);
|
|
317
|
+
}
|
|
318
|
+
await run(commandName("npm"), ["install", "-g", "--force", tarballPath], { timeoutMs: 300_000 });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const spec = input.packageSpec || input.targetPackage + "@" + (input.targetVersion || "latest");
|
|
322
|
+
await run(commandName("npm"), ["install", "-g", "--force", spec], { timeoutMs: 300_000 });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function startDaemon() {
|
|
326
|
+
await updateState({ status: "starting" });
|
|
327
|
+
if (input.startMode === "current-script") {
|
|
328
|
+
await log("starting daemon from current script");
|
|
329
|
+
await spawnDetached(process.execPath, [input.daemonScriptPath, "start", "--json"]);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
await log("starting daemon from global happy-elves");
|
|
333
|
+
await spawnDetached(commandName("happy-elves"), ["daemon", "start", "--json"]);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function waitForDaemonRunning() {
|
|
337
|
+
const deadline = Date.now() + 45_000;
|
|
338
|
+
while (Date.now() < deadline) {
|
|
339
|
+
const result = await captureStatus().catch(() => undefined);
|
|
340
|
+
if (result?.ok === true && (result.data?.local?.running === true || result.data?.running === true)) {
|
|
341
|
+
await log("daemon status running");
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
await delay(1000);
|
|
345
|
+
}
|
|
346
|
+
throw new Error("Timed out waiting for restarted daemon");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function captureStatus() {
|
|
350
|
+
const status = statusCommand();
|
|
351
|
+
const result = await run(status.command, status.args, { timeoutMs: 10_000, allowFailure: true });
|
|
352
|
+
const text = result.stdout.trim();
|
|
353
|
+
return text ? JSON.parse(text) : undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function currentHappyElvesVersion() {
|
|
357
|
+
if (input.startMode === "current-script") return undefined;
|
|
358
|
+
const result = await run(commandName("happy-elves"), ["--version"], { timeoutMs: 10_000, allowFailure: true });
|
|
359
|
+
const text = (result.stdout + "\\n" + result.stderr).trim();
|
|
360
|
+
const match = text.match(/(\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?)/);
|
|
361
|
+
return match?.[1];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function statusCommand() {
|
|
365
|
+
if (input.startMode === "current-script") {
|
|
366
|
+
return { command: process.execPath, args: [input.daemonScriptPath, "status", "--json"] };
|
|
367
|
+
}
|
|
368
|
+
return { command: commandName("happy-elves"), args: ["daemon", "status", "--local", "--json"] };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function spawnDetached(command, args) {
|
|
372
|
+
const child = spawn(command, args, {
|
|
373
|
+
cwd: input.configDir,
|
|
374
|
+
detached: true,
|
|
375
|
+
env: { ...process.env, HAPPY_ELVES_HOME: input.homeEnv },
|
|
376
|
+
stdio: "ignore",
|
|
377
|
+
windowsHide: true,
|
|
378
|
+
});
|
|
379
|
+
child.unref();
|
|
380
|
+
await log("spawned " + command + " pid " + (child.pid || "unknown"));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function run(command, args, options = {}) {
|
|
384
|
+
await log("run " + command + " " + args.join(" "));
|
|
385
|
+
return await new Promise((resolve, reject) => {
|
|
386
|
+
const child = spawn(command, args, {
|
|
387
|
+
cwd: input.configDir,
|
|
388
|
+
env: { ...process.env, HAPPY_ELVES_HOME: input.homeEnv },
|
|
389
|
+
windowsHide: true,
|
|
390
|
+
});
|
|
391
|
+
let stdout = "";
|
|
392
|
+
let stderr = "";
|
|
393
|
+
const timer = setTimeout(() => {
|
|
394
|
+
child.kill();
|
|
395
|
+
reject(new Error("Command timed out: " + command));
|
|
396
|
+
}, options.timeoutMs || 60_000);
|
|
397
|
+
child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
398
|
+
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
399
|
+
child.on("error", (error) => {
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
reject(error);
|
|
402
|
+
});
|
|
403
|
+
child.on("close", (code, signal) => {
|
|
404
|
+
clearTimeout(timer);
|
|
405
|
+
const compactStdout = compact(stdout);
|
|
406
|
+
const compactStderr = compact(stderr);
|
|
407
|
+
if (compactStdout) void log("stdout: " + compactStdout).catch(() => {});
|
|
408
|
+
if (compactStderr) void log("stderr: " + compactStderr).catch(() => {});
|
|
409
|
+
if (!options.allowFailure && code !== 0) {
|
|
410
|
+
reject(new Error(command + " exited " + (code ?? signal ?? "unknown") + ": " + compactStderr));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
resolve({ code, signal, stdout, stderr });
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function download(url, targetPath) {
|
|
419
|
+
await log("download " + redactUrl(url));
|
|
420
|
+
const response = await fetch(url);
|
|
421
|
+
if (!response.ok) throw new Error("Download failed: HTTP " + response.status);
|
|
422
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
423
|
+
await fs.writeFile(targetPath, buffer, { mode: 0o600 });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function sha256File(filePath) {
|
|
427
|
+
const hash = createHash("sha256");
|
|
428
|
+
hash.update(await fs.readFile(filePath));
|
|
429
|
+
return hash.digest("hex");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function readPid() {
|
|
433
|
+
try {
|
|
434
|
+
const raw = await fs.readFile(input.pidPath, "utf8");
|
|
435
|
+
const pid = Number(raw.trim());
|
|
436
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (error && error.code === "ENOENT") return undefined;
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isPidAlive(pid) {
|
|
444
|
+
try {
|
|
445
|
+
process.kill(pid, 0);
|
|
446
|
+
return true;
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function commandName(name) {
|
|
453
|
+
return process.platform === "win32" ? name + ".cmd" : name;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function compact(text) {
|
|
457
|
+
return text.replace(/\\s+/g, " ").trim().slice(0, 2000);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function delay(ms) {
|
|
461
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function redactUrl(value) {
|
|
465
|
+
try {
|
|
466
|
+
const url = new URL(value);
|
|
467
|
+
if (url.username) url.username = "redacted";
|
|
468
|
+
if (url.password) url.password = "redacted";
|
|
469
|
+
return url.toString();
|
|
470
|
+
} catch {
|
|
471
|
+
return value;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
`;
|
|
475
|
+
}
|
|
476
|
+
//# sourceMappingURL=lifecycle.js.map
|
package/apps/daemon/package.json
CHANGED
|
@@ -483,6 +483,39 @@ export function handleControllerMessage(context, connection, message) {
|
|
|
483
483
|
});
|
|
484
484
|
return;
|
|
485
485
|
}
|
|
486
|
+
if (message.type === "controller:daemonLifecycle") {
|
|
487
|
+
if (!allowControllerAction(connection, "machine.lifecycle", message.requestId))
|
|
488
|
+
return;
|
|
489
|
+
if (!allowControllerMachine(connection, message.machineId, message.requestId))
|
|
490
|
+
return;
|
|
491
|
+
const machineConnection = connectedMachine(connection.accountId, message.machineId);
|
|
492
|
+
if (!machineConnection) {
|
|
493
|
+
sendMachineOffline(connection, message.requestId);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
if (machineCapabilities(connection.accountId, message.machineId)?.daemonLifecycle !== true) {
|
|
497
|
+
sendError(connection, {
|
|
498
|
+
type: "server:error",
|
|
499
|
+
requestId: message.requestId,
|
|
500
|
+
code: "DAEMON_LIFECYCLE_UNSUPPORTED",
|
|
501
|
+
capability: "machine.lifecycle",
|
|
502
|
+
message: "This daemon does not support remote lifecycle commands. Upgrade it manually once.",
|
|
503
|
+
});
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
markCommandPending(connection.accountId, message.machineId, message.requestId, { command: "daemonLifecycle" });
|
|
507
|
+
send(machineConnection.socket, {
|
|
508
|
+
type: "machine:daemonLifecycle",
|
|
509
|
+
requestId: message.requestId,
|
|
510
|
+
machineId: message.machineId,
|
|
511
|
+
action: message.action,
|
|
512
|
+
targetVersion: message.targetVersion,
|
|
513
|
+
packageUrl: message.packageUrl,
|
|
514
|
+
sha256: message.sha256,
|
|
515
|
+
tail: message.tail,
|
|
516
|
+
});
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
486
519
|
if (message.type === "controller:gateway") {
|
|
487
520
|
if (!allowControllerAction(connection, "gateway.manage", message.requestId))
|
|
488
521
|
return;
|
|
@@ -119,6 +119,18 @@ export function handleMachineMessage(context, connection, message) {
|
|
|
119
119
|
broadcastControllers(connection.accountId, response);
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
|
+
if (message.type === "machine:daemonLifecycleResult") {
|
|
123
|
+
const response = {
|
|
124
|
+
type: "server:daemonLifecycleResult",
|
|
125
|
+
requestId: message.requestId,
|
|
126
|
+
machineId: connection.machineId,
|
|
127
|
+
result: message.result,
|
|
128
|
+
};
|
|
129
|
+
rememberCommandResponse(connection.accountId, message.requestId, response);
|
|
130
|
+
clearCommandPending(connection.accountId, message.requestId);
|
|
131
|
+
broadcastControllers(connection.accountId, response);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
122
134
|
if (message.type === "machine:gatewayResult") {
|
|
123
135
|
const response = {
|
|
124
136
|
type: "server:gatewayResult",
|
|
@@ -114,6 +114,8 @@ export function normalizeMachineCapabilities(value) {
|
|
|
114
114
|
capabilities.filePreview = record.filePreview;
|
|
115
115
|
if (typeof record.devTools === "boolean")
|
|
116
116
|
capabilities.devTools = record.devTools;
|
|
117
|
+
if (typeof record.daemonLifecycle === "boolean")
|
|
118
|
+
capabilities.daemonLifecycle = record.daemonLifecycle;
|
|
117
119
|
if (isRecord(record.sessionPrimitives)) {
|
|
118
120
|
capabilities.sessionPrimitives = {
|
|
119
121
|
resume: primitiveSupport(record.sessionPrimitives.resume),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DirectoryListing, type EncryptedSessionEvent, type EncryptedEnvelope, type HistoricalSessionSnapshot, type MachineSnapshot, type PairingStartResponse, type SessionSnapshot } from "../../shared/dist/index.js";
|
|
2
2
|
import { sendCommand } from "./transport.js";
|
|
3
|
-
import type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, FilePreviewInput, ForkInput, GatewayCommandInput, GatewayCommandResponse, HistoricalSessionListPage, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineMetadata, MachineDevExecInput, MachineDevExecResponse, MachineDiagnosticsResponse, MemoryCommandInput, MemoryCommandResponse, DecodedMemoryRecord, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScheduleCommandInput, ScheduleCommandResponse, ScopedTokenInfo, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitInput, MemorySaveInput, MemorySearchInput, MemorySettings, TurnReconcileResponse, WorkspaceCwdPin, WorkspaceCwdPinInput, WorkspaceProjectPreference, WorkspaceProjectPreferenceInput, WorkspaceRecentSession, WorkspaceSessionStarResult, WorkspaceSyncState } from "./types.js";
|
|
3
|
+
import type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DaemonLifecycleInput, DaemonLifecycleResponse, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, FilePreviewInput, ForkInput, GatewayCommandInput, GatewayCommandResponse, HistoricalSessionListPage, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineMetadata, MachineDevExecInput, MachineDevExecResponse, MachineDiagnosticsResponse, MemoryCommandInput, MemoryCommandResponse, DecodedMemoryRecord, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScheduleCommandInput, ScheduleCommandResponse, ScopedTokenInfo, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitInput, MemorySaveInput, MemorySearchInput, MemorySettings, TurnReconcileResponse, WorkspaceCwdPin, WorkspaceCwdPinInput, WorkspaceProjectPreference, WorkspaceProjectPreferenceInput, WorkspaceRecentSession, WorkspaceSessionStarResult, WorkspaceSyncState } from "./types.js";
|
|
4
4
|
export declare class ControllerClient {
|
|
5
5
|
readonly config: ControllerClientConfig;
|
|
6
6
|
constructor(config: ControllerClientConfig);
|
|
@@ -73,6 +73,7 @@ export declare class ControllerClient {
|
|
|
73
73
|
previewFile(input: FilePreviewInput): Promise<import("../../shared/dist/index.js").FilePreviewResult>;
|
|
74
74
|
machineDiagnostics(machineId: string): Promise<MachineDiagnosticsResponse>;
|
|
75
75
|
machineDevExec(input: MachineDevExecInput): Promise<MachineDevExecResponse>;
|
|
76
|
+
daemonLifecycle(input: DaemonLifecycleInput): Promise<DaemonLifecycleResponse>;
|
|
76
77
|
gateway(input: GatewayCommandInput): Promise<GatewayCommandResponse>;
|
|
77
78
|
schedule(input: ScheduleCommandInput): Promise<ScheduleCommandResponse>;
|
|
78
79
|
memory(input: MemoryCommandInput): Promise<MemoryCommandResponse>;
|
|
@@ -3,7 +3,7 @@ import { addControllerDevice, claimControllerInvite, createControllerInvite, cre
|
|
|
3
3
|
import { ControllerClientError, ControllerCommandError } from "./errors.js";
|
|
4
4
|
import { authenticatedJson, normalizeRelayUrl, readRelayJson, relayHttpError } from "./http.js";
|
|
5
5
|
import { parseRepairSessionHeadResult, parseSessionEventsResponse, } from "./parsers.js";
|
|
6
|
-
import { sendAndWaitForDirectoryListing, sendAndWaitForFilePreview, sendAndWaitForGatewayResult, sendAndWaitForHistoricalSessions, sendAndWaitForMachineDevExecResult, sendAndWaitForMachineDiagnostics, sendAndWaitForMemoryResult, sendAndWaitForScheduleResult, sendAndWaitForTurnReconcileResult, sendCommand, subscribe, } from "./transport.js";
|
|
6
|
+
import { sendAndWaitForDirectoryListing, sendAndWaitForDaemonLifecycleResult, sendAndWaitForFilePreview, sendAndWaitForGatewayResult, sendAndWaitForHistoricalSessions, sendAndWaitForMachineDevExecResult, sendAndWaitForMachineDiagnostics, sendAndWaitForMemoryResult, sendAndWaitForScheduleResult, sendAndWaitForTurnReconcileResult, sendCommand, subscribe, } from "./transport.js";
|
|
7
7
|
import { DEFAULT_SESSION_CREATE_WAIT_MS, } from "./types.js";
|
|
8
8
|
import { isWaitCondition, matchesWaitCondition, parseEventCursor, parseEventLimit, parsePositiveDuration, requireNonEmpty, sleep, } from "./validation.js";
|
|
9
9
|
const DEFAULT_BLOCKING_RUN_WAIT_MS = 30 * 60 * 1000;
|
|
@@ -364,6 +364,21 @@ export class ControllerClient {
|
|
|
364
364
|
}, input.waitTimeoutMs);
|
|
365
365
|
return message.result;
|
|
366
366
|
}
|
|
367
|
+
async daemonLifecycle(input) {
|
|
368
|
+
const machineId = requireNonEmpty(input.machineId, "machineId");
|
|
369
|
+
const requestId = randomId("req");
|
|
370
|
+
const message = await sendAndWaitForDaemonLifecycleResult(this.config, {
|
|
371
|
+
type: "controller:daemonLifecycle",
|
|
372
|
+
requestId,
|
|
373
|
+
machineId,
|
|
374
|
+
action: input.action,
|
|
375
|
+
targetVersion: input.targetVersion,
|
|
376
|
+
packageUrl: input.packageUrl,
|
|
377
|
+
sha256: input.sha256,
|
|
378
|
+
tail: input.tail,
|
|
379
|
+
}, input.waitTimeoutMs);
|
|
380
|
+
return { ...message.result, requestId, machineId: message.machineId };
|
|
381
|
+
}
|
|
367
382
|
async gateway(input) {
|
|
368
383
|
const machineId = requireNonEmpty(input.machineId, "machineId");
|
|
369
384
|
const requestId = randomId("req");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ControllerClient } from "./client.js";
|
|
2
2
|
export { ControllerClientError, ControllerCommandError } from "./errors.js";
|
|
3
|
-
export type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerDevice, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, DirectoryListResponse, FilePreviewInput, FilePreviewResponse, EventCursor, ForkInput, HistoricalSessionListPage, HistoricalSessionListResponse, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineListResponse, MachineMetadata, DecodedMemoryRecord, EncryptedMemoryRecord, MemoryPayload, MemoryCommandInput, MemoryCommandResponse, MemorySaveInput, MemorySearchInput, MemorySettings, PermissionMode, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScopedTokenInfo, ScopedTokenScope, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitCondition, WaitInput, WorkspaceCwdPin, WorkspaceCwdPinInput, WorkspaceProjectPreference, WorkspaceProjectPreferenceInput, WorkspaceRecentSession, WorkspaceSessionStar, WorkspaceSessionStarResult, WorkspaceSyncState, } from "./types.js";
|
|
3
|
+
export type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerDevice, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DaemonLifecycleInput, DaemonLifecycleResponse, DeviceListResponse, DirectoryInput, DirectoryListResponse, FilePreviewInput, FilePreviewResponse, EventCursor, ForkInput, HistoricalSessionListPage, HistoricalSessionListResponse, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineListResponse, MachineMetadata, DecodedMemoryRecord, EncryptedMemoryRecord, MemoryPayload, MemoryCommandInput, MemoryCommandResponse, MemorySaveInput, MemorySearchInput, MemorySettings, PermissionMode, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScopedTokenInfo, ScopedTokenScope, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitCondition, WaitInput, WorkspaceCwdPin, WorkspaceCwdPinInput, WorkspaceProjectPreference, WorkspaceProjectPreferenceInput, WorkspaceRecentSession, WorkspaceSessionStar, WorkspaceSessionStarResult, WorkspaceSyncState, } from "./types.js";
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -37,6 +37,11 @@ export declare function sendAndWaitForMachineDevExecResult(config: ControllerCli
|
|
|
37
37
|
}>, waitTimeoutMs?: number): Promise<Extract<ServerMessage, {
|
|
38
38
|
type: "server:machineDevExecResult";
|
|
39
39
|
}>>;
|
|
40
|
+
export declare function sendAndWaitForDaemonLifecycleResult(config: ControllerClientConfig, command: Extract<ControllerClientMessage, {
|
|
41
|
+
type: "controller:daemonLifecycle";
|
|
42
|
+
}>, waitTimeoutMs?: number): Promise<Extract<ServerMessage, {
|
|
43
|
+
type: "server:daemonLifecycleResult";
|
|
44
|
+
}>>;
|
|
40
45
|
export declare function sendAndWaitForGatewayResult(config: ControllerClientConfig, command: Extract<ControllerClientMessage, {
|
|
41
46
|
type: "controller:gateway";
|
|
42
47
|
}>): Promise<Extract<ServerMessage, {
|
|
@@ -64,6 +64,10 @@ export function subscribe(config, handlers) {
|
|
|
64
64
|
void handlers.onMemoryResult?.(message);
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
|
+
if (message.type === "server:daemonLifecycleResult") {
|
|
68
|
+
void handlers.onDaemonLifecycleResult?.(message);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
67
71
|
if (message.type === "server:turnReconciled") {
|
|
68
72
|
void handlers.onTurnReconciled?.(message);
|
|
69
73
|
return;
|
|
@@ -248,6 +252,9 @@ export async function sendAndWaitForMachineDiagnostics(config, command) {
|
|
|
248
252
|
export async function sendAndWaitForMachineDevExecResult(config, command, waitTimeoutMs) {
|
|
249
253
|
return await sendAndWaitForServerMessage(config, command, "server:machineDevExecResult", "machine dev exec result; remote result is unknown", waitTimeoutMs ?? (command.timeoutMs ?? 60_000) + 15_000);
|
|
250
254
|
}
|
|
255
|
+
export async function sendAndWaitForDaemonLifecycleResult(config, command, waitTimeoutMs) {
|
|
256
|
+
return await sendAndWaitForServerMessage(config, command, "server:daemonLifecycleResult", "daemon lifecycle result", waitTimeoutMs ?? 45_000);
|
|
257
|
+
}
|
|
251
258
|
export async function sendAndWaitForGatewayResult(config, command) {
|
|
252
259
|
return await sendAndWaitForServerMessage(config, command, "server:gatewayResult", "gateway result", 30_000);
|
|
253
260
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DirectoryListing, EncryptedSessionEvent, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayCommandResult, HistoricalSessionSnapshot, MachineSnapshot, MachineDevExecResult, MachineDiagnostics, ScheduleCommandResult, ScheduleCreateInput, MemoryCommandResult, MemoryMode, MemoryPromoteInput, MemoryWriteInput, ServerMessage, SessionEventPayload, SessionSnapshot, TurnReconcileResult } from "../../shared/dist/index.js";
|
|
1
|
+
import type { DirectoryListing, DaemonLifecycleAction, DaemonLifecycleResult, EncryptedSessionEvent, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayCommandResult, HistoricalSessionSnapshot, MachineSnapshot, MachineDevExecResult, MachineDiagnostics, ScheduleCommandResult, ScheduleCreateInput, MemoryCommandResult, MemoryMode, MemoryPromoteInput, MemoryWriteInput, ServerMessage, SessionEventPayload, SessionSnapshot, TurnReconcileResult } from "../../shared/dist/index.js";
|
|
2
2
|
export type ControllerClientConfig = {
|
|
3
3
|
relayUrl: string;
|
|
4
4
|
controllerToken: string;
|
|
@@ -51,6 +51,9 @@ export type ControllerSubscriptionHandlers = {
|
|
|
51
51
|
onMemoryResult?: (message: Extract<ServerMessage, {
|
|
52
52
|
type: "server:memoryResult";
|
|
53
53
|
}>) => void;
|
|
54
|
+
onDaemonLifecycleResult?: (message: Extract<ServerMessage, {
|
|
55
|
+
type: "server:daemonLifecycleResult";
|
|
56
|
+
}>) => void;
|
|
54
57
|
onTurnReconciled?: (message: Extract<ServerMessage, {
|
|
55
58
|
type: "server:turnReconciled";
|
|
56
59
|
}>) => void;
|
|
@@ -290,6 +293,19 @@ export type MachineDevExecInput = {
|
|
|
290
293
|
timeoutMs?: number;
|
|
291
294
|
waitTimeoutMs?: number;
|
|
292
295
|
};
|
|
296
|
+
export type DaemonLifecycleInput = {
|
|
297
|
+
machineId: string;
|
|
298
|
+
action: DaemonLifecycleAction;
|
|
299
|
+
targetVersion?: string;
|
|
300
|
+
packageUrl?: string;
|
|
301
|
+
sha256?: string;
|
|
302
|
+
tail?: number;
|
|
303
|
+
waitTimeoutMs?: number;
|
|
304
|
+
};
|
|
305
|
+
export type DaemonLifecycleResponse = DaemonLifecycleResult & {
|
|
306
|
+
requestId: string;
|
|
307
|
+
machineId: string;
|
|
308
|
+
};
|
|
293
309
|
export type GatewayCommandInput = {
|
|
294
310
|
machineId: string;
|
|
295
311
|
action: "list" | "get" | "upsert" | "enable" | "disable" | "test";
|
|
@@ -94,6 +94,7 @@ export declare const capabilitiesSchema: z.ZodObject<{
|
|
|
94
94
|
directoryBrowser: z.ZodOptional<z.ZodBoolean>;
|
|
95
95
|
filePreview: z.ZodOptional<z.ZodBoolean>;
|
|
96
96
|
devTools: z.ZodOptional<z.ZodBoolean>;
|
|
97
|
+
daemonLifecycle: z.ZodOptional<z.ZodBoolean>;
|
|
97
98
|
}, z.core.$strip>;
|
|
98
99
|
export declare const machineSnapshotSchema: z.ZodObject<{
|
|
99
100
|
id: z.ZodString;
|
|
@@ -182,6 +183,7 @@ export declare const machineSnapshotSchema: z.ZodObject<{
|
|
|
182
183
|
directoryBrowser: z.ZodOptional<z.ZodBoolean>;
|
|
183
184
|
filePreview: z.ZodOptional<z.ZodBoolean>;
|
|
184
185
|
devTools: z.ZodOptional<z.ZodBoolean>;
|
|
186
|
+
daemonLifecycle: z.ZodOptional<z.ZodBoolean>;
|
|
185
187
|
}, z.core.$strip>;
|
|
186
188
|
encryptedMetadata: z.ZodOptional<z.ZodObject<{
|
|
187
189
|
v: z.ZodLiteral<1>;
|
|
@@ -413,14 +415,88 @@ export declare const machineDevExecResultSchema: z.ZodObject<{
|
|
|
413
415
|
durationMs: z.ZodNumber;
|
|
414
416
|
timedOut: z.ZodOptional<z.ZodBoolean>;
|
|
415
417
|
}, z.core.$strip>;
|
|
418
|
+
export declare const daemonUpdateStateSchema: z.ZodObject<{
|
|
419
|
+
requestId: z.ZodString;
|
|
420
|
+
action: z.ZodEnum<{
|
|
421
|
+
restart: "restart";
|
|
422
|
+
update: "update";
|
|
423
|
+
}>;
|
|
424
|
+
targetVersion: z.ZodOptional<z.ZodString>;
|
|
425
|
+
packageSpec: z.ZodOptional<z.ZodString>;
|
|
426
|
+
packageUrl: z.ZodOptional<z.ZodString>;
|
|
427
|
+
sha256: z.ZodOptional<z.ZodString>;
|
|
428
|
+
status: z.ZodEnum<{
|
|
429
|
+
running: "running";
|
|
430
|
+
failed: "failed";
|
|
431
|
+
accepted: "accepted";
|
|
432
|
+
stopping: "stopping";
|
|
433
|
+
installing: "installing";
|
|
434
|
+
starting: "starting";
|
|
435
|
+
succeeded: "succeeded";
|
|
436
|
+
}>;
|
|
437
|
+
fromVersion: z.ZodString;
|
|
438
|
+
toVersion: z.ZodOptional<z.ZodString>;
|
|
439
|
+
startedAt: z.ZodString;
|
|
440
|
+
finishedAt: z.ZodOptional<z.ZodString>;
|
|
441
|
+
error: z.ZodOptional<z.ZodString>;
|
|
442
|
+
logPath: z.ZodString;
|
|
443
|
+
}, z.core.$strip>;
|
|
444
|
+
export declare const daemonLifecycleResultSchema: z.ZodObject<{
|
|
445
|
+
action: z.ZodEnum<{
|
|
446
|
+
status: "status";
|
|
447
|
+
restart: "restart";
|
|
448
|
+
update: "update";
|
|
449
|
+
logs: "logs";
|
|
450
|
+
}>;
|
|
451
|
+
accepted: z.ZodOptional<z.ZodBoolean>;
|
|
452
|
+
version: z.ZodOptional<z.ZodString>;
|
|
453
|
+
pid: z.ZodOptional<z.ZodNumber>;
|
|
454
|
+
running: z.ZodOptional<z.ZodBoolean>;
|
|
455
|
+
platform: z.ZodOptional<z.ZodString>;
|
|
456
|
+
arch: z.ZodOptional<z.ZodString>;
|
|
457
|
+
installMode: z.ZodOptional<z.ZodEnum<{
|
|
458
|
+
"npm-global": "npm-global";
|
|
459
|
+
"repo-dev": "repo-dev";
|
|
460
|
+
unknown: "unknown";
|
|
461
|
+
}>>;
|
|
462
|
+
updateSupported: z.ZodOptional<z.ZodBoolean>;
|
|
463
|
+
updateUnsupportedReason: z.ZodOptional<z.ZodString>;
|
|
464
|
+
updateState: z.ZodOptional<z.ZodObject<{
|
|
465
|
+
requestId: z.ZodString;
|
|
466
|
+
action: z.ZodEnum<{
|
|
467
|
+
restart: "restart";
|
|
468
|
+
update: "update";
|
|
469
|
+
}>;
|
|
470
|
+
targetVersion: z.ZodOptional<z.ZodString>;
|
|
471
|
+
packageSpec: z.ZodOptional<z.ZodString>;
|
|
472
|
+
packageUrl: z.ZodOptional<z.ZodString>;
|
|
473
|
+
sha256: z.ZodOptional<z.ZodString>;
|
|
474
|
+
status: z.ZodEnum<{
|
|
475
|
+
running: "running";
|
|
476
|
+
failed: "failed";
|
|
477
|
+
accepted: "accepted";
|
|
478
|
+
stopping: "stopping";
|
|
479
|
+
installing: "installing";
|
|
480
|
+
starting: "starting";
|
|
481
|
+
succeeded: "succeeded";
|
|
482
|
+
}>;
|
|
483
|
+
fromVersion: z.ZodString;
|
|
484
|
+
toVersion: z.ZodOptional<z.ZodString>;
|
|
485
|
+
startedAt: z.ZodString;
|
|
486
|
+
finishedAt: z.ZodOptional<z.ZodString>;
|
|
487
|
+
error: z.ZodOptional<z.ZodString>;
|
|
488
|
+
logPath: z.ZodString;
|
|
489
|
+
}, z.core.$strip>>;
|
|
490
|
+
logs: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
491
|
+
}, z.core.$strip>;
|
|
416
492
|
export declare const turnReconcileResultSchema: z.ZodObject<{
|
|
417
493
|
sessionId: z.ZodString;
|
|
418
494
|
turnId: z.ZodOptional<z.ZodString>;
|
|
419
495
|
requestId: z.ZodOptional<z.ZodString>;
|
|
420
496
|
status: z.ZodEnum<{
|
|
497
|
+
unknown: "unknown";
|
|
421
498
|
active: "active";
|
|
422
499
|
terminal: "terminal";
|
|
423
|
-
unknown: "unknown";
|
|
424
500
|
not_found: "not_found";
|
|
425
501
|
uninspectable: "uninspectable";
|
|
426
502
|
}>;
|
|
@@ -1359,12 +1435,12 @@ export declare const memoryTurnOptionsSchema: z.ZodObject<{
|
|
|
1359
1435
|
export declare const memoryCommandResultSchema: z.ZodObject<{
|
|
1360
1436
|
action: z.ZodEnum<{
|
|
1361
1437
|
search: "search";
|
|
1438
|
+
status: "status";
|
|
1362
1439
|
overview: "overview";
|
|
1363
1440
|
read: "read";
|
|
1364
1441
|
reindex: "reindex";
|
|
1365
1442
|
write: "write";
|
|
1366
1443
|
promote: "promote";
|
|
1367
|
-
status: "status";
|
|
1368
1444
|
sync: "sync";
|
|
1369
1445
|
}>;
|
|
1370
1446
|
overview: z.ZodOptional<z.ZodObject<{
|
|
@@ -53,6 +53,7 @@ export const capabilitiesSchema = z.object({
|
|
|
53
53
|
directoryBrowser: z.boolean().optional(),
|
|
54
54
|
filePreview: z.boolean().optional(),
|
|
55
55
|
devTools: z.boolean().optional(),
|
|
56
|
+
daemonLifecycle: z.boolean().optional(),
|
|
56
57
|
});
|
|
57
58
|
const sessionCapabilitiesSchema = z.object({
|
|
58
59
|
resume: z.enum(["acpx", "native", "checkpoint", "unsupported"]),
|
|
@@ -198,6 +199,35 @@ export const machineDevExecResultSchema = z.object({
|
|
|
198
199
|
durationMs: z.number().nonnegative(),
|
|
199
200
|
timedOut: z.boolean().optional(),
|
|
200
201
|
});
|
|
202
|
+
export const daemonUpdateStateSchema = z.object({
|
|
203
|
+
requestId: z.string().min(1),
|
|
204
|
+
action: z.enum(["restart", "update"]),
|
|
205
|
+
targetVersion: z.string().min(1).optional(),
|
|
206
|
+
packageSpec: z.string().min(1).optional(),
|
|
207
|
+
packageUrl: z.string().min(1).optional(),
|
|
208
|
+
sha256: z.string().regex(/^[a-fA-F0-9]{64}$/).optional(),
|
|
209
|
+
status: z.enum(["accepted", "running", "stopping", "installing", "starting", "succeeded", "failed"]),
|
|
210
|
+
fromVersion: z.string().min(1),
|
|
211
|
+
toVersion: z.string().min(1).optional(),
|
|
212
|
+
startedAt: z.string().min(1),
|
|
213
|
+
finishedAt: z.string().min(1).optional(),
|
|
214
|
+
error: z.string().optional(),
|
|
215
|
+
logPath: z.string().min(1),
|
|
216
|
+
});
|
|
217
|
+
export const daemonLifecycleResultSchema = z.object({
|
|
218
|
+
action: z.enum(["status", "restart", "update", "logs"]),
|
|
219
|
+
accepted: z.boolean().optional(),
|
|
220
|
+
version: z.string().min(1).optional(),
|
|
221
|
+
pid: z.number().int().positive().optional(),
|
|
222
|
+
running: z.boolean().optional(),
|
|
223
|
+
platform: z.string().min(1).optional(),
|
|
224
|
+
arch: z.string().min(1).optional(),
|
|
225
|
+
installMode: z.enum(["npm-global", "repo-dev", "unknown"]).optional(),
|
|
226
|
+
updateSupported: z.boolean().optional(),
|
|
227
|
+
updateUnsupportedReason: z.string().min(1).optional(),
|
|
228
|
+
updateState: daemonUpdateStateSchema.optional(),
|
|
229
|
+
logs: z.array(z.string()).optional(),
|
|
230
|
+
});
|
|
201
231
|
export const turnReconcileResultSchema = z.object({
|
|
202
232
|
sessionId: z.string().min(1),
|
|
203
233
|
turnId: z.string().min(1).optional(),
|
|
@@ -46,6 +46,7 @@ export type MachineSnapshot = {
|
|
|
46
46
|
directoryBrowser?: boolean;
|
|
47
47
|
filePreview?: boolean;
|
|
48
48
|
devTools?: boolean;
|
|
49
|
+
daemonLifecycle?: boolean;
|
|
49
50
|
};
|
|
50
51
|
encryptedMetadata?: EncryptedEnvelope;
|
|
51
52
|
};
|
|
@@ -175,6 +176,38 @@ export type MachineDevExecResult = {
|
|
|
175
176
|
durationMs: number;
|
|
176
177
|
timedOut?: boolean;
|
|
177
178
|
};
|
|
179
|
+
export type DaemonLifecycleAction = "status" | "restart" | "update" | "logs";
|
|
180
|
+
export type DaemonInstallMode = "npm-global" | "repo-dev" | "unknown";
|
|
181
|
+
export type DaemonUpdateStatus = "accepted" | "running" | "stopping" | "installing" | "starting" | "succeeded" | "failed";
|
|
182
|
+
export type DaemonUpdateState = {
|
|
183
|
+
requestId: string;
|
|
184
|
+
action: "restart" | "update";
|
|
185
|
+
targetVersion?: string;
|
|
186
|
+
packageSpec?: string;
|
|
187
|
+
packageUrl?: string;
|
|
188
|
+
sha256?: string;
|
|
189
|
+
status: DaemonUpdateStatus;
|
|
190
|
+
fromVersion: string;
|
|
191
|
+
toVersion?: string;
|
|
192
|
+
startedAt: string;
|
|
193
|
+
finishedAt?: string;
|
|
194
|
+
error?: string;
|
|
195
|
+
logPath: string;
|
|
196
|
+
};
|
|
197
|
+
export type DaemonLifecycleResult = {
|
|
198
|
+
action: DaemonLifecycleAction;
|
|
199
|
+
accepted?: boolean;
|
|
200
|
+
version?: string;
|
|
201
|
+
pid?: number;
|
|
202
|
+
running?: boolean;
|
|
203
|
+
platform?: string;
|
|
204
|
+
arch?: string;
|
|
205
|
+
installMode?: DaemonInstallMode;
|
|
206
|
+
updateSupported?: boolean;
|
|
207
|
+
updateUnsupportedReason?: string;
|
|
208
|
+
updateState?: DaemonUpdateState;
|
|
209
|
+
logs?: string[];
|
|
210
|
+
};
|
|
178
211
|
export type TurnInspectionStatus = "active" | "terminal" | "unknown" | "not_found" | "uninspectable";
|
|
179
212
|
export type TurnReconcileResult = {
|
|
180
213
|
sessionId: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EncryptedEnvelope } from "./crypto.js";
|
|
2
|
-
import type { DirectoryListing, EncryptedSessionEvent, EncryptedTranscriptReplacementEvent, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayCommandResult, HistoricalSessionSnapshot, MachineDevExecResult, MachineDiagnostics, MachineSnapshot, MemoryCommandResult, MemoryMode, MemoryPromoteInput, MemoryWriteInput, PermissionMode, ScheduleCommandResult, ScheduleCreateInput, SessionHeadAdvanceBasis, SessionSnapshot, TurnReconcileResult } from "./protocol-types.js";
|
|
2
|
+
import type { DirectoryListing, DaemonLifecycleAction, DaemonLifecycleResult, EncryptedSessionEvent, EncryptedTranscriptReplacementEvent, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayCommandResult, HistoricalSessionSnapshot, MachineDevExecResult, MachineDiagnostics, MachineSnapshot, MemoryCommandResult, MemoryMode, MemoryPromoteInput, MemoryWriteInput, PermissionMode, ScheduleCommandResult, ScheduleCreateInput, SessionHeadAdvanceBasis, SessionSnapshot, TurnReconcileResult } from "./protocol-types.js";
|
|
3
3
|
export { encryptedEnvelopeSchema } from "./protocol-schemas.js";
|
|
4
|
-
export type { AgentProfile, DirectoryEntry, DirectoryListing, EncryptedSessionEvent, EncryptedTranscriptReplacementEvent, FilePreviewKind, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayChannelDomain, GatewayDestination, GatewayEventPayload, GatewayInboundMode, GatewayLastDeliveredMessage, GatewayLastMessageRef, GatewayChannelPublic, GatewayChannelType, GatewayCommandResult, GatewayDeliveryResult, GatewayProcessingReactionConfig, HistoricalSessionSnapshot, MachineDevExecResult, MachineDiagnostics, MachineSnapshot, MemoryCitation, MemoryCommandResult, MemoryFileSummary, MemoryLayer, MemoryMode, MemoryOverview, MemoryPromoteInput, MemorySearchResult, MemorySyncConflict, MemoryTurnOptions, MemoryWriteInput, PairingClaimRequest, PairingClaimResponse, PairingStartRequest, PairingStartResponse, PermissionMode, ScheduleCommandResult, ScheduleCreateInput, ScheduleExecution, ScheduleMemoryMode, ScheduleRecord, ScheduleRunRecord, ScheduleSpec, ScheduleStopReason, ScheduleTarget, SessionHeadAdvanceBasis, SessionEventPayload, SessionSnapshot, SessionUsageSnapshot, TurnInspectionStatus, TurnReconcileResult, } from "./protocol-types.js";
|
|
4
|
+
export type { AgentProfile, DaemonInstallMode, DaemonLifecycleAction, DaemonLifecycleResult, DaemonUpdateState, DaemonUpdateStatus, DirectoryEntry, DirectoryListing, EncryptedSessionEvent, EncryptedTranscriptReplacementEvent, FilePreviewKind, FilePreviewMode, FilePreviewResult, GatewayChannelInput, GatewayChannelDomain, GatewayDestination, GatewayEventPayload, GatewayInboundMode, GatewayLastDeliveredMessage, GatewayLastMessageRef, GatewayChannelPublic, GatewayChannelType, GatewayCommandResult, GatewayDeliveryResult, GatewayProcessingReactionConfig, HistoricalSessionSnapshot, MachineDevExecResult, MachineDiagnostics, MachineSnapshot, MemoryCitation, MemoryCommandResult, MemoryFileSummary, MemoryLayer, MemoryMode, MemoryOverview, MemoryPromoteInput, MemorySearchResult, MemorySyncConflict, MemoryTurnOptions, MemoryWriteInput, PairingClaimRequest, PairingClaimResponse, PairingStartRequest, PairingStartResponse, PermissionMode, ScheduleCommandResult, ScheduleCreateInput, ScheduleExecution, ScheduleMemoryMode, ScheduleRecord, ScheduleRunRecord, ScheduleSpec, ScheduleStopReason, ScheduleTarget, SessionHeadAdvanceBasis, SessionEventPayload, SessionSnapshot, SessionUsageSnapshot, TurnInspectionStatus, TurnReconcileResult, } from "./protocol-types.js";
|
|
5
5
|
export type ControllerClientMessage = {
|
|
6
6
|
type: "controller:createSession";
|
|
7
7
|
requestId: string;
|
|
@@ -80,6 +80,15 @@ export type ControllerClientMessage = {
|
|
|
80
80
|
cwd?: string;
|
|
81
81
|
shell?: string;
|
|
82
82
|
timeoutMs?: number;
|
|
83
|
+
} | {
|
|
84
|
+
type: "controller:daemonLifecycle";
|
|
85
|
+
requestId: string;
|
|
86
|
+
machineId: string;
|
|
87
|
+
action: DaemonLifecycleAction;
|
|
88
|
+
targetVersion?: string;
|
|
89
|
+
packageUrl?: string;
|
|
90
|
+
sha256?: string;
|
|
91
|
+
tail?: number;
|
|
83
92
|
} | {
|
|
84
93
|
type: "controller:gateway";
|
|
85
94
|
requestId: string;
|
|
@@ -233,6 +242,11 @@ export type MachineClientMessage = {
|
|
|
233
242
|
requestId: string;
|
|
234
243
|
machineId: string;
|
|
235
244
|
result: MachineDevExecResult;
|
|
245
|
+
} | {
|
|
246
|
+
type: "machine:daemonLifecycleResult";
|
|
247
|
+
requestId: string;
|
|
248
|
+
machineId: string;
|
|
249
|
+
result: DaemonLifecycleResult;
|
|
236
250
|
} | {
|
|
237
251
|
type: "machine:gatewayResult";
|
|
238
252
|
requestId: string;
|
|
@@ -388,6 +402,15 @@ export type MachineCommand = {
|
|
|
388
402
|
cwd?: string;
|
|
389
403
|
shell?: string;
|
|
390
404
|
timeoutMs?: number;
|
|
405
|
+
} | {
|
|
406
|
+
type: "machine:daemonLifecycle";
|
|
407
|
+
requestId: string;
|
|
408
|
+
machineId: string;
|
|
409
|
+
action: DaemonLifecycleAction;
|
|
410
|
+
targetVersion?: string;
|
|
411
|
+
packageUrl?: string;
|
|
412
|
+
sha256?: string;
|
|
413
|
+
tail?: number;
|
|
391
414
|
} | {
|
|
392
415
|
type: "machine:gateway";
|
|
393
416
|
requestId: string;
|
|
@@ -520,6 +543,11 @@ export type ServerMessage = {
|
|
|
520
543
|
requestId: string;
|
|
521
544
|
machineId: string;
|
|
522
545
|
result: MachineDevExecResult;
|
|
546
|
+
} | {
|
|
547
|
+
type: "server:daemonLifecycleResult";
|
|
548
|
+
requestId: string;
|
|
549
|
+
machineId: string;
|
|
550
|
+
result: DaemonLifecycleResult;
|
|
523
551
|
} | {
|
|
524
552
|
type: "server:gatewayResult";
|
|
525
553
|
requestId: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { capabilitiesSchema, directoryEntrySchema, encryptedEnvelopeSchema, encryptedSessionEventSchema, encryptedTranscriptReplacementEventSchema, filePreviewResultSchema, gatewayChannelInputSchema, gatewayCommandResultSchema, historicalSessionSnapshotSchema, machineDevExecResultSchema, machineDiagnosticsSchema, machineSnapshotSchema, memoryCommandResultSchema, memoryPromoteInputSchema, memoryWriteInputSchema, scheduleCommandResultSchema, scheduleCreateInputSchema, sessionSnapshotSchema, turnReconcileResultSchema, } from "./protocol-schemas.js";
|
|
2
|
+
import { capabilitiesSchema, daemonLifecycleResultSchema, directoryEntrySchema, encryptedEnvelopeSchema, encryptedSessionEventSchema, encryptedTranscriptReplacementEventSchema, filePreviewResultSchema, gatewayChannelInputSchema, gatewayCommandResultSchema, historicalSessionSnapshotSchema, machineDevExecResultSchema, machineDiagnosticsSchema, machineSnapshotSchema, memoryCommandResultSchema, memoryPromoteInputSchema, memoryWriteInputSchema, scheduleCommandResultSchema, scheduleCreateInputSchema, sessionSnapshotSchema, turnReconcileResultSchema, } from "./protocol-schemas.js";
|
|
3
3
|
import { isSessionNameValid } from "./session-name.js";
|
|
4
4
|
export { encryptedEnvelopeSchema } from "./protocol-schemas.js";
|
|
5
5
|
const controllerMessageSchema = z.discriminatedUnion("type", [
|
|
@@ -92,6 +92,16 @@ const controllerMessageSchema = z.discriminatedUnion("type", [
|
|
|
92
92
|
shell: z.string().optional(),
|
|
93
93
|
timeoutMs: z.number().int().positive().max(600_000).optional(),
|
|
94
94
|
}),
|
|
95
|
+
z.object({
|
|
96
|
+
type: z.literal("controller:daemonLifecycle"),
|
|
97
|
+
requestId: z.string().min(1),
|
|
98
|
+
machineId: z.string().min(1),
|
|
99
|
+
action: z.enum(["status", "restart", "update", "logs"]),
|
|
100
|
+
targetVersion: z.string().min(1).optional(),
|
|
101
|
+
packageUrl: z.string().min(1).optional(),
|
|
102
|
+
sha256: z.string().regex(/^[a-fA-F0-9]{64}$/).optional(),
|
|
103
|
+
tail: z.number().int().positive().max(2000).optional(),
|
|
104
|
+
}),
|
|
95
105
|
z.object({
|
|
96
106
|
type: z.literal("controller:gateway"),
|
|
97
107
|
requestId: z.string().min(1),
|
|
@@ -283,6 +293,12 @@ const machineMessageSchema = z.discriminatedUnion("type", [
|
|
|
283
293
|
machineId: z.string().min(1),
|
|
284
294
|
result: machineDevExecResultSchema,
|
|
285
295
|
}),
|
|
296
|
+
z.object({
|
|
297
|
+
type: z.literal("machine:daemonLifecycleResult"),
|
|
298
|
+
requestId: z.string().min(1),
|
|
299
|
+
machineId: z.string().min(1),
|
|
300
|
+
result: daemonLifecycleResultSchema,
|
|
301
|
+
}),
|
|
286
302
|
z.object({
|
|
287
303
|
type: z.literal("machine:gatewayResult"),
|
|
288
304
|
requestId: z.string().min(1),
|
|
@@ -463,6 +479,16 @@ const machineCommandSchema = z.discriminatedUnion("type", [
|
|
|
463
479
|
shell: z.string().optional(),
|
|
464
480
|
timeoutMs: z.number().int().positive().max(600_000).optional(),
|
|
465
481
|
}),
|
|
482
|
+
z.object({
|
|
483
|
+
type: z.literal("machine:daemonLifecycle"),
|
|
484
|
+
requestId: z.string().min(1),
|
|
485
|
+
machineId: z.string().min(1),
|
|
486
|
+
action: z.enum(["status", "restart", "update", "logs"]),
|
|
487
|
+
targetVersion: z.string().min(1).optional(),
|
|
488
|
+
packageUrl: z.string().min(1).optional(),
|
|
489
|
+
sha256: z.string().regex(/^[a-fA-F0-9]{64}$/).optional(),
|
|
490
|
+
tail: z.number().int().positive().max(2000).optional(),
|
|
491
|
+
}),
|
|
466
492
|
z.object({
|
|
467
493
|
type: z.literal("machine:gateway"),
|
|
468
494
|
requestId: z.string().min(1),
|
|
@@ -624,6 +650,12 @@ const serverMessageSchema = z.union([
|
|
|
624
650
|
machineId: z.string().min(1),
|
|
625
651
|
result: machineDevExecResultSchema,
|
|
626
652
|
}),
|
|
653
|
+
z.object({
|
|
654
|
+
type: z.literal("server:daemonLifecycleResult"),
|
|
655
|
+
requestId: z.string().min(1),
|
|
656
|
+
machineId: z.string().min(1),
|
|
657
|
+
result: daemonLifecycleResultSchema,
|
|
658
|
+
}),
|
|
627
659
|
z.object({
|
|
628
660
|
type: z.literal("server:gatewayResult"),
|
|
629
661
|
requestId: z.string().min(1),
|