@cydm/happy-elves 0.1.0-beta.85 → 0.1.0-beta.86

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.
@@ -1,6 +1,6 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
- import { CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, idleSessionStatuses, localDaemonStatus, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, showMachine, spawnDaemonStart, stopLocalDaemon, throwRelayHttpError, wantsJson, writeDaemonConfig } from "./lib/index.js";
3
+ import { CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, ensureDaemonRunning, idleSessionStatuses, localDaemonStatus, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, restartDaemonForNewConfig, showMachine, spawnDaemonStart, stopLocalDaemon, throwRelayHttpError, waitForMachineProjection, wantsJson, writeDaemonConfig } from "./lib/index.js";
4
4
  export async function handleDaemon({ domain, action, flags }) {
5
5
  if (domain === "daemon" && action === "pair") {
6
6
  const relayUrl = requireRelayUrl(flags);
@@ -25,11 +25,87 @@ export async function handleDaemon({ domain, action, flags }) {
25
25
  machineToken: claimed.machineToken,
26
26
  accountSecret,
27
27
  };
28
+ const localBefore = await localDaemonStatus();
28
29
  const daemonConfigPath = await writeDaemonConfig(daemonConfig);
29
- ok("daemon.pair", {
30
+ let daemonStart = { skipped: true, reason: "not requested" };
31
+ let projection = { checked: false, reason: "not checked" };
32
+ let startError;
33
+ if (flags["no-start"] === true) {
34
+ daemonStart = { skipped: true, reason: "--no-start" };
35
+ projection = { checked: false, reason: "--no-start" };
36
+ }
37
+ else {
38
+ try {
39
+ const startResult = localBefore.running
40
+ ? await restartDaemonForNewConfig(relayUrl)
41
+ : await ensureDaemonRunning(relayUrl);
42
+ daemonStart = {
43
+ skipped: false,
44
+ restarted: localBefore.running,
45
+ local: startResult.local,
46
+ started: startResult.started,
47
+ ...(startResult.startedPid ? { startedPid: startResult.startedPid } : {}),
48
+ ...("stopped" in startResult ? { stop: startResult.stopped } : {}),
49
+ };
50
+ try {
51
+ const controllerConfig = await readConfig({ relay: relayUrl });
52
+ const machine = await waitForMachineProjection(new ControllerClient(controllerConfig), claimed.machineId, 10_000);
53
+ projection = {
54
+ checked: true,
55
+ online: machine?.online === true,
56
+ machineId: claimed.machineId,
57
+ ...(machine?.lastSeen ? { lastSeen: machine.lastSeen } : {}),
58
+ };
59
+ }
60
+ catch (error) {
61
+ projection = {
62
+ checked: false,
63
+ reason: error instanceof Error ? error.message : String(error),
64
+ };
65
+ }
66
+ }
67
+ catch (error) {
68
+ const code = error instanceof CliError ? error.code : "DAEMON_START_FAILED";
69
+ startError = { code, message: error instanceof Error ? error.message : String(error) };
70
+ process.exitCode = 1;
71
+ daemonStart = { skipped: true, reason: "start failed" };
72
+ projection = { checked: false, reason: "daemon did not start" };
73
+ }
74
+ }
75
+ const data = {
30
76
  path: daemonConfigPath,
31
77
  config: redactedDaemonConfig(daemonConfig),
32
- });
78
+ daemon: daemonStart,
79
+ projection,
80
+ ...(startError ? { error: startError } : {}),
81
+ };
82
+ const commandOk = !startError && !(projection.checked && !projection.online);
83
+ if (!commandOk)
84
+ process.exitCode = 1;
85
+ if (!wantsJson(flags)) {
86
+ console.log(`Paired ${machineName} with ${relayUrl}`);
87
+ console.log(`Config: ${daemonConfigPath}`);
88
+ if (startError) {
89
+ console.error(`Daemon start failed: ${startError.message}`);
90
+ }
91
+ else if (daemonStart.skipped) {
92
+ console.log(`Daemon start: skipped (${daemonStart.reason})`);
93
+ }
94
+ else {
95
+ console.log(`${daemonStart.restarted ? "Restarted" : "Started"} daemon${daemonStart.startedPid ? ` pid ${daemonStart.startedPid}` : ""}`);
96
+ }
97
+ if (projection.checked) {
98
+ console.log(`Relay projection: ${projection.online ? "online" : "offline"} (${projection.machineId})`);
99
+ if (!projection.online) {
100
+ console.log("If this stays offline, run happy-elves daemon logs --tail 100 and happy-elves daemon status --local.");
101
+ }
102
+ }
103
+ else {
104
+ console.log(`Relay projection: not checked (${projection.reason})`);
105
+ }
106
+ return true;
107
+ }
108
+ ok("daemon.pair", data, { machineId: claimed.machineId }, commandOk);
33
109
  return true;
34
110
  }
35
111
  if (domain === "daemon" && action === "doctor") {
@@ -118,6 +194,14 @@ export async function handleDaemon({ domain, action, flags }) {
118
194
  if (domain === "daemon" && action === "start") {
119
195
  const status = await localDaemonStatus();
120
196
  if (status.running) {
197
+ const existingConfig = await readDaemonConfig();
198
+ if (typeof flags.relay === "string")
199
+ existingConfig.relayUrl = requireRelayUrl(flags);
200
+ const restarted = await ensureDaemonRunning(existingConfig.relayUrl);
201
+ if (restarted.started) {
202
+ ok("daemon.start", { ...restarted.local, started: true, startedPid: restarted.startedPid, restartedForChangedConfig: true });
203
+ return true;
204
+ }
121
205
  ok("daemon.start", { ...status, started: false });
122
206
  return true;
123
207
  }
@@ -97,6 +97,7 @@ const booleanFlags = new Set([
97
97
  "keep-source",
98
98
  "local",
99
99
  "no-open",
100
+ "no-start",
100
101
  "no-wait",
101
102
  "repair-head",
102
103
  "summary",
@@ -1,11 +1,11 @@
1
1
  import { CliError } from "../../errors.js";
2
2
  import { startDaemonTimeoutMs } from "./paths.js";
3
3
  import { readDaemonConfig } from "./config.js";
4
- import { daemonBinaryUpdatedAfterStart, localDaemonStatus, spawnDaemonStart, stopLocalDaemon } from "./local-daemon.js";
4
+ import { daemonBinaryUpdatedAfterStart, daemonConfigUpdatedAfterStart, localDaemonStatus, spawnDaemonStart, stopLocalDaemon } from "./local-daemon.js";
5
5
  export async function ensureDaemonRunning(relayUrl, cwd = process.cwd()) {
6
6
  const current = await localDaemonStatus();
7
7
  if (current.running) {
8
- if (await daemonBinaryUpdatedAfterStart(current)) {
8
+ if ((await daemonBinaryUpdatedAfterStart(current)) || (await daemonConfigUpdatedAfterStart(current))) {
9
9
  return await restartDaemonForNewConfig(relayUrl, cwd);
10
10
  }
11
11
  return { local: current, started: false };
@@ -4,6 +4,7 @@ export declare function localDaemonStatus(): Promise<{
4
4
  running: boolean;
5
5
  }>;
6
6
  export declare function daemonBinaryUpdatedAfterStart(status: Awaited<ReturnType<typeof localDaemonStatus>>): Promise<boolean>;
7
+ export declare function daemonConfigUpdatedAfterStart(status: Awaited<ReturnType<typeof localDaemonStatus>>): Promise<boolean>;
7
8
  export declare function spawnDaemonStart(args: string[], cwd?: string): number;
8
9
  export declare function stopLocalDaemon(timeoutMs?: number): Promise<{
9
10
  pidPath: string;
@@ -102,6 +102,20 @@ export async function daemonBinaryUpdatedAfterStart(status) {
102
102
  return false;
103
103
  }
104
104
  }
105
+ export async function daemonConfigUpdatedAfterStart(status) {
106
+ if (!status.running || !status.pid)
107
+ return false;
108
+ const startedAt = readProcessStartedAtMs(status.pid);
109
+ if (!startedAt)
110
+ return false;
111
+ try {
112
+ const config = await fs.stat(path.join(configDir, "daemon.json"));
113
+ return config.mtimeMs > startedAt + 1000;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
105
119
  export function spawnDaemonStart(args, cwd = process.cwd()) {
106
120
  const child = spawn(process.execPath, [daemonCliPath(), "start", ...args], {
107
121
  cwd,
@@ -157,7 +157,7 @@ missed and are never auto-caught-up.
157
157
  happy-elves loop delete <loopId> --json
158
158
  `,
159
159
  daemon: `Usage:
160
- happy-elves daemon pair --relay <url> --code <code> --secret <account-secret> [--name <machine-name>] --json
160
+ happy-elves daemon pair --relay <url> --code <code> --secret <account-secret> [--name <machine-name>] [--no-start] --json
161
161
  happy-elves daemon start [--relay <url>] [--json]
162
162
  happy-elves daemon stop [--json]
163
163
  happy-elves daemon restart [--relay <url>] [--json]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.85",
3
+ "version": "0.1.0-beta.86",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.85",
3
+ "version": "0.1.0-beta.86",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -18,6 +18,7 @@ const dbPath = process.env.HAPPY_ELVES_DB ?? path.join(process.cwd(), ".happy-el
18
18
  const pairingTtlMs = 10 * 60 * 1000;
19
19
  const controllerInviteTtlMs = 10 * 60 * 1000;
20
20
  const eventReplayLimit = intEnv("HAPPY_ELVES_EVENT_REPLAY_LIMIT", process.env.HAPPY_ELVES_EVENT_REPLAY_LIMIT, 100);
21
+ const lanPackDir = process.env.HAPPY_ELVES_LAN_PACK_DIR ? path.resolve(process.env.HAPPY_ELVES_LAN_PACK_DIR) : undefined;
21
22
  const retention = createRetentionConfig(process.env);
22
23
  const retentionPruneIntervalMs = retentionMs("HAPPY_ELVES_RETENTION_PRUNE_INTERVAL_MS", process.env.HAPPY_ELVES_RETENTION_PRUNE_INTERVAL_MS, 60_000);
23
24
  const originAllowed = createOriginPolicy(process.env);
@@ -77,6 +78,33 @@ app.setErrorHandler((error, _request, reply) => {
77
78
  }
78
79
  reply.code(500).send({ error: { code: "INTERNAL_ERROR", message } });
79
80
  });
81
+ if (lanPackDir) {
82
+ app.get("/__lan/*", async (request, reply) => {
83
+ const params = request.params;
84
+ const requested = decodeURIComponent(params["*"] ?? "");
85
+ const filePath = path.resolve(lanPackDir, requested);
86
+ if (!filePath.startsWith(`${lanPackDir}${path.sep}`)) {
87
+ reply.code(403).send("forbidden");
88
+ return;
89
+ }
90
+ let stat;
91
+ try {
92
+ stat = fs.statSync(filePath);
93
+ }
94
+ catch {
95
+ reply.code(404).send("not found");
96
+ return;
97
+ }
98
+ if (!stat.isFile()) {
99
+ reply.code(404).send("not found");
100
+ return;
101
+ }
102
+ const extension = path.extname(filePath);
103
+ reply.header("content-length", String(stat.size));
104
+ reply.type(extension === ".ps1" ? "text/plain; charset=utf-8" : extension === ".tgz" ? "application/gzip" : "application/octet-stream");
105
+ return reply.send(fs.createReadStream(filePath));
106
+ });
107
+ }
80
108
  registerHttpRoutes(app, context, controllerInviteTtlMs);
81
109
  registerWebsocketRoute(app, context, originAllowed);
82
110
  export async function startRelay(options = {}) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.85",
3
+ "version": "0.1.0-beta.86",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {