@cydm/happy-elves 0.1.0-beta.270 → 0.1.0-beta.272

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.
@@ -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,86 @@ 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" &&
259
+ code !== "COMMAND_TIMEOUT" &&
260
+ code !== "RELAY_CONNECT_TIMEOUT" &&
261
+ code !== "RELAY_WEBSOCKET_ERROR")
262
+ throw error;
263
+ }
264
+ await sleep(1000);
265
+ }
266
+ if (lastResult)
267
+ return {
268
+ ...lastResult,
269
+ accepted: false,
270
+ updateUnsupportedReason: `WAIT_TIMEOUT_${requestId}`,
271
+ };
272
+ throw new CliError(`Timed out waiting for daemon ${action}: ${errorMessage(lastError)}`, "COMMAND_TIMEOUT");
273
+ }
274
+ function errorCode(error) {
275
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : undefined;
276
+ }
277
+ function errorMessage(error) {
278
+ return error instanceof Error ? error.message : String(error ?? "unknown");
279
+ }
280
+ function sleep(ms) {
281
+ return new Promise((resolve) => setTimeout(resolve, ms));
282
+ }
151
283
  //# sourceMappingURL=remote.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.270",
3
+ "version": "0.1.0-beta.272",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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 {};