@cydm/happy-elves 0.1.0-beta.500 → 0.1.0-beta.501

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,8 +6,8 @@ Public beta CLI for Happy Elves.
6
6
 
7
7
  Run once in the existing user account; it detects %USERPROFILE%\.happy-elves\hosted, keeps the current daemon and sessions running, installs the independent updater, and registers per-user startup.
8
8
 
9
- npm.cmd install --prefix $HOME\.happy-elves\management-entry --ignore-scripts --no-audit --no-fund --package-lock=false @cydm/happy-elves@0.1.0-beta.500
10
- npm.cmd exec --yes --package @cydm/happy-elves@0.1.0-beta.500 -- happy-elves-updater install --state-root $HOME\.happy-elves\hosted --autostart
9
+ npm.cmd install --prefix $HOME\.happy-elves\management-entry --ignore-scripts --no-audit --no-fund --package-lock=false @cydm/happy-elves@0.1.0-beta.501
10
+ npm.cmd exec --yes --package @cydm/happy-elves@0.1.0-beta.501 -- happy-elves-updater install --state-root $HOME\.happy-elves\hosted --autostart
11
11
 
12
12
  The same updater entrypoint works on macOS after selecting a permanent user prefix.
13
13
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.500",
3
+ "version": "0.1.0-beta.501",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -10,7 +10,7 @@ import { rehydrateRuntimeSession } from "../session/lifecycle.js";
10
10
  import { runtimeRead } from "../session/runtime-truth.js";
11
11
  import { encryptedSessionMetadata } from "../session/metadata.js";
12
12
  import { loadSessionProjections } from "../session/store.js";
13
- import { appendAttention, expireInflightLeases, leaseNextAttention, latestAttentionForTask, markAttentionDelivered, markAttentionDeliveryFailed, markSubscriptionLiveness, recordTurnRuntimeBinding, reopenUnresolvedArtifactFeedbackAttentions, readOrchestrationStore, } from "./store.js";
13
+ import { appendAttention, closeSubscriptionForTurn, expireInflightLeases, leaseNextAttention, latestAttentionForTask, markAttentionDelivered, markAttentionDeliveryFailed, markSubscriptionLiveness, recordTurnRuntimeBinding, reopenUnresolvedArtifactFeedbackAttentions, readOrchestrationStore, } from "./store.js";
14
14
  import { attentionFromReconcile, catchUpSubscriptionEvents } from "./watcher.js";
15
15
  import { scheduleRemoteOrchestrationWork } from "./remote-work-budget.js";
16
16
  import { resolveWakeIdentity } from "./wake-identity.js";
@@ -146,9 +146,6 @@ async function reconcileSubscriptionsOnce(config) {
146
146
  if (reconciledSubscriptions.has(key))
147
147
  continue;
148
148
  reconciledSubscriptions.add(key);
149
- const latest = await latestAttentionForTask(subscription.taskId);
150
- if (latest?.eventTypes.some((type) => type === "terminal" || type === "failure"))
151
- continue;
152
149
  if (crewMachineId !== config.machineId) {
153
150
  // Remote reconcile shares the same bounded/fair scheduler as transcript
154
151
  // catch-up. Startup recovery must not create an unbounded second fleet
@@ -252,8 +249,20 @@ async function reconcileRemoteSubscription(config, subscription) {
252
249
  return;
253
250
  }
254
251
  const attention = attentionFromReconcile(result);
255
- if (!attention || !subscription.eventTypes.includes(attention.eventType))
252
+ if (!attention)
256
253
  return;
254
+ const closesTurn = attention.eventType === "terminal" || attention.eventType === "failure";
255
+ if (!subscription.eventTypes.includes(attention.eventType)) {
256
+ if (closesTurn) {
257
+ await closeSubscriptionForTurn({
258
+ taskId: subscription.taskId,
259
+ consumerSessionId: subscription.consumerSessionId,
260
+ crewSessionId: subscription.crewSessionId,
261
+ turnId: subscription.turnId,
262
+ });
263
+ }
264
+ return;
265
+ }
257
266
  await appendAttention({
258
267
  taskId: subscription.taskId,
259
268
  consumerMachineId: subscription.consumerMachineId ?? config.machineId,
@@ -270,6 +279,7 @@ async function reconcileRemoteSubscription(config, subscription) {
270
279
  turnId: subscription.turnId,
271
280
  ref: result.status,
272
281
  },
282
+ closeSubscription: closesTurn,
273
283
  });
274
284
  }
275
285
  catch (error) {
@@ -307,14 +317,16 @@ async function markStaleSubscriptions(config) {
307
317
  if (Number.isFinite(lastSeenAt) && now - lastSeenAt < staleMs)
308
318
  continue;
309
319
  const latest = await latestAttentionForTask(subscription.taskId);
310
- if (latest?.eventTypes.some((type) => type === "terminal" || type === "failure"))
311
- continue;
312
- if (latest?.eventTypes.includes("stale") && latest.status !== "acked")
320
+ if (latest?.turnId === subscription.turnId && latest.eventTypes.includes("stale") && latest.status !== "acked")
313
321
  continue;
314
322
  if ((subscription.crewMachineId ?? config.machineId) === config.machineId) {
315
323
  await reconcileLocalSubscription(config, subscription, "stale-check");
316
- const reconciledLatest = await latestAttentionForTask(subscription.taskId);
317
- if (reconciledLatest?.eventTypes.some((type) => type === "terminal" || type === "failure" || type === "unknown"))
324
+ const reconciledStore = await readOrchestrationStore();
325
+ const currentSubscription = reconciledStore.subscriptions.find((record) => record.taskId === subscription.taskId &&
326
+ record.consumerSessionId === subscription.consumerSessionId &&
327
+ record.crewSessionId === subscription.crewSessionId &&
328
+ record.turnId === subscription.turnId);
329
+ if (!currentSubscription || currentSubscription.status !== "active")
318
330
  continue;
319
331
  if (await localSubscriptionTurnIsActive(subscription))
320
332
  continue;
@@ -123,6 +123,8 @@ export type AttentionInput = {
123
123
  eventType: TaskAttentionEventType;
124
124
  reason: string;
125
125
  payloadRef?: TaskAttentionPayloadRef;
126
+ /** Terminal/failure attention closes the exact turn subscription atomically. */
127
+ closeSubscription?: boolean;
126
128
  now?: string;
127
129
  };
128
130
  export declare function readOrchestrationStore(): Promise<OrchestrationStore>;
@@ -249,6 +251,9 @@ export declare function markSubscriptionRemoteScan(taskId: string, input: {
249
251
  at?: string;
250
252
  }): Promise<void>;
251
253
  export declare function appendAttention(input: AttentionInput): Promise<TaskAttentionRecord[]>;
254
+ export declare function closeSubscriptionForTurn(input: Pick<AttentionInput, "taskId" | "consumerSessionId" | "crewSessionId" | "turnId"> & {
255
+ at?: string;
256
+ }): Promise<void>;
252
257
  export declare function listAttentions(input?: {
253
258
  consumerSessionId?: string;
254
259
  status?: TaskAttentionStatus[];
@@ -794,6 +794,9 @@ export async function appendAttention(input) {
794
794
  let updated;
795
795
  const store = await updateOrchestrationStore((current) => {
796
796
  updated = appendAttentionRecord(current, { ...input, now: input.now ?? new Date().toISOString() });
797
+ if (input.closeSubscription && (input.eventType === "terminal" || input.eventType === "failure")) {
798
+ closeSubscriptionForTurnRecord(current, input);
799
+ }
797
800
  return current;
798
801
  });
799
802
  if (!updated)
@@ -802,6 +805,30 @@ export async function appendAttention(input) {
802
805
  record.consumerSessionId === updated?.consumerSessionId &&
803
806
  record.eventCursor === updated?.eventCursor);
804
807
  }
808
+ export async function closeSubscriptionForTurn(input) {
809
+ if (!input.turnId)
810
+ return;
811
+ const at = input.at ?? new Date().toISOString();
812
+ await updateOrchestrationStore((current) => {
813
+ closeSubscriptionForTurnRecord(current, { ...input, now: at });
814
+ return current;
815
+ });
816
+ }
817
+ function closeSubscriptionForTurnRecord(store, input) {
818
+ if (!input.turnId)
819
+ return;
820
+ const now = input.now ?? new Date().toISOString();
821
+ for (const subscription of store.subscriptions) {
822
+ if (subscription.status !== "active")
823
+ continue;
824
+ if (subscription.taskId !== input.taskId || subscription.consumerSessionId !== input.consumerSessionId)
825
+ continue;
826
+ if (subscription.crewSessionId !== input.crewSessionId || subscription.turnId !== input.turnId)
827
+ continue;
828
+ subscription.status = "deleted";
829
+ subscription.updatedAt = now;
830
+ }
831
+ }
805
832
  function appendAttentionRecord(store, input) {
806
833
  const index = store.attentions.findIndex((record) => record.taskId === input.taskId && record.consumerSessionId === input.consumerSessionId);
807
834
  const payloadRefs = input.payloadRef ? [input.payloadRef] : [];
@@ -2,7 +2,7 @@ import { randomId, } from "../../../../packages/shared/dist/index.js";
2
2
  import { appendAudit } from "../audit.js";
3
3
  import { RemoteCommandError, requestRemoteMachineResult } from "../relay/remote-command.js";
4
4
  import { runtimeRead } from "../session/runtime-truth.js";
5
- import { activeSubscriptionsForTurn, appendAttention, markSubscriptionLiveness, markSubscriptionRemoteScan, recordTurnRuntimeBinding, } from "./store.js";
5
+ import { activeSubscriptionsForTurn, appendAttention, closeSubscriptionForTurn, markSubscriptionLiveness, markSubscriptionRemoteScan, recordTurnRuntimeBinding, } from "./store.js";
6
6
  import { scheduleRemoteOrchestrationWork } from "./remote-work-budget.js";
7
7
  import { relayMachineRegistry } from "../state.js";
8
8
  const remoteCatchupsInFlight = new Map();
@@ -21,8 +21,18 @@ export async function observeSessionEvent(input) {
21
21
  }
22
22
  const subscriptions = await activeSubscriptionsForTurn(input.sessionId, input.turnId);
23
23
  for (const subscription of subscriptions) {
24
- if (!subscription.eventTypes.includes(attention.eventType))
24
+ const closesTurn = attention.eventType === "terminal" || attention.eventType === "failure";
25
+ if (!subscription.eventTypes.includes(attention.eventType)) {
26
+ if (closesTurn) {
27
+ await closeSubscriptionForTurn({
28
+ taskId: subscription.taskId,
29
+ consumerSessionId: subscription.consumerSessionId,
30
+ crewSessionId: subscription.crewSessionId,
31
+ turnId: subscription.turnId,
32
+ });
33
+ }
25
34
  continue;
35
+ }
26
36
  await appendAttention({
27
37
  taskId: subscription.taskId,
28
38
  consumerMachineId: subscription.consumerMachineId ?? input.config.machineId,
@@ -39,6 +49,7 @@ export async function observeSessionEvent(input) {
39
49
  turnId: input.turnId,
40
50
  eventSeq: input.seq,
41
51
  },
52
+ closeSubscription: closesTurn,
42
53
  });
43
54
  await appendOrchestrationAudit(input.config, "orchestration.attention.append", `Recorded ${attention.eventType} attention for ${subscription.taskId}`, {
44
55
  taskId: subscription.taskId,
@@ -89,6 +100,7 @@ export async function observeTurnReconcile(input) {
89
100
  turnId: input.result.turnId,
90
101
  ref: input.result.status,
91
102
  },
103
+ closeSubscription: attention.eventType === "terminal" || attention.eventType === "failure",
92
104
  });
93
105
  await appendOrchestrationAudit(input.config, "orchestration.attention.reconcile", `Recorded ${attention.eventType} reconcile attention for ${subscription.taskId}`, {
94
106
  taskId: subscription.taskId,
@@ -360,8 +372,18 @@ async function observeSubscriptionPayload(input) {
360
372
  }
361
373
  return;
362
374
  }
363
- if (!input.subscription.eventTypes.includes(attention.eventType))
375
+ const closesTurn = attention.eventType === "terminal" || attention.eventType === "failure";
376
+ if (!input.subscription.eventTypes.includes(attention.eventType)) {
377
+ if (closesTurn) {
378
+ await closeSubscriptionForTurn({
379
+ taskId: input.subscription.taskId,
380
+ consumerSessionId: input.subscription.consumerSessionId,
381
+ crewSessionId: input.subscription.crewSessionId,
382
+ turnId: input.subscription.turnId,
383
+ });
384
+ }
364
385
  return;
386
+ }
365
387
  await appendAttention({
366
388
  taskId: input.subscription.taskId,
367
389
  consumerMachineId: input.subscription.consumerMachineId ?? input.config.machineId,
@@ -378,6 +400,7 @@ async function observeSubscriptionPayload(input) {
378
400
  turnId: input.subscription.turnId,
379
401
  eventSeq: input.seq,
380
402
  },
403
+ closeSubscription: closesTurn,
381
404
  });
382
405
  await appendOrchestrationAudit(input.config, "orchestration.attention.append", `Recorded ${input.crewMachineId === input.config.machineId ? "" : "remote "}${attention.eventType} attention for ${input.subscription.taskId}`, {
383
406
  taskId: input.subscription.taskId,
@@ -703,10 +703,37 @@ export async function connectOnce(config) {
703
703
  let loopTimer;
704
704
  let scheduleTimer;
705
705
  let orchestrationTimer;
706
+ let authenticated = false;
707
+ const startAuthenticatedWork = () => {
708
+ if (!authenticated || ws.readyState !== WebSocket.OPEN)
709
+ return;
710
+ if (loopTimer || scheduleTimer || orchestrationTimer)
711
+ return;
712
+ const onlineSinceMs = Date.now();
713
+ loopTimer = setInterval(() => {
714
+ void tickLoops(ws, config).catch((error) => {
715
+ console.error(`Loop tick failed: ${error instanceof Error ? error.message : String(error)}`);
716
+ });
717
+ }, 1000);
718
+ scheduleTimer = setInterval(() => {
719
+ void tickSchedules(config, onlineSinceMs).catch((error) => {
720
+ console.error(`Schedule tick failed: ${error instanceof Error ? error.message : String(error)}`);
721
+ });
722
+ }, 1000);
723
+ orchestrationTimer = setInterval(() => {
724
+ void tickOrchestration(config).catch((error) => {
725
+ console.error(`Orchestration tick failed: ${error instanceof Error ? error.message : String(error)}`);
726
+ });
727
+ }, 1000);
728
+ void recoverCrewQueues(config).catch((error) => {
729
+ console.error(`Crew queue relay-open recovery failed: ${error instanceof Error ? error.message : String(error)}`);
730
+ });
731
+ void drainPendingArtifactFeedbackAttentions(config).catch((error) => {
732
+ console.error(`Artifact feedback attention relay-open recovery failed: ${error instanceof Error ? error.message : String(error)}`);
733
+ });
734
+ };
706
735
  await new Promise((resolve) => {
707
736
  ws.addEventListener("open", () => {
708
- const onlineSinceMs = Date.now();
709
- setActiveRelaySocket(ws);
710
737
  const hello = {
711
738
  type: "hello",
712
739
  token: config.machineToken,
@@ -714,27 +741,6 @@ export async function connectOnce(config) {
714
741
  nodeId: config.machineId,
715
742
  };
716
743
  ws.send(JSON.stringify(hello));
717
- loopTimer = setInterval(() => {
718
- void tickLoops(ws, config).catch((error) => {
719
- console.error(`Loop tick failed: ${error instanceof Error ? error.message : String(error)}`);
720
- });
721
- }, 1000);
722
- scheduleTimer = setInterval(() => {
723
- void tickSchedules(config, onlineSinceMs).catch((error) => {
724
- console.error(`Schedule tick failed: ${error instanceof Error ? error.message : String(error)}`);
725
- });
726
- }, 1000);
727
- orchestrationTimer = setInterval(() => {
728
- void tickOrchestration(config).catch((error) => {
729
- console.error(`Orchestration tick failed: ${error instanceof Error ? error.message : String(error)}`);
730
- });
731
- }, 1000);
732
- void recoverCrewQueues(config).catch((error) => {
733
- console.error(`Crew queue relay-open recovery failed: ${error instanceof Error ? error.message : String(error)}`);
734
- });
735
- void drainPendingArtifactFeedbackAttentions(config).catch((error) => {
736
- console.error(`Artifact feedback attention relay-open recovery failed: ${error instanceof Error ? error.message : String(error)}`);
737
- });
738
744
  });
739
745
  ws.addEventListener("message", (event) => {
740
746
  let message;
@@ -745,11 +751,24 @@ export async function connectOnce(config) {
745
751
  console.error(`Invalid relay message: ${error instanceof Error ? error.message : String(error)}`);
746
752
  return;
747
753
  }
748
- void handleRelayWireMessage(ws, config, message).catch(async (error) => {
754
+ void (async () => {
755
+ if (message.type === "server:hello" && !authenticated) {
756
+ // Relay authentication is complete only after the server hello.
757
+ // Do not activate the socket earlier: a reconnect with a durable
758
+ // outbox would otherwise flush publishPacket before the relay has
759
+ // installed the machine's authenticated routing context.
760
+ authenticated = true;
761
+ setActiveRelaySocket(ws);
762
+ }
763
+ await handleRelayWireMessage(ws, config, message);
764
+ if (message.type === "server:hello")
765
+ startAuthenticatedWork();
766
+ })().catch(async (error) => {
749
767
  await appendUnscopedRelayError(config, "DAEMON_COMMAND_FAILED", error);
750
768
  });
751
769
  });
752
770
  ws.addEventListener("close", () => {
771
+ authenticated = false;
753
772
  clearActiveRelaySocket(ws);
754
773
  if (loopTimer)
755
774
  clearInterval(loopTimer);
@@ -761,6 +780,7 @@ export async function connectOnce(config) {
761
780
  resolve();
762
781
  });
763
782
  ws.addEventListener("error", () => {
783
+ authenticated = false;
764
784
  clearActiveRelaySocket(ws);
765
785
  if (loopTimer)
766
786
  clearInterval(loopTimer);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.500",
3
+ "version": "0.1.0-beta.501",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -6,6 +6,7 @@ import { promisify } from "node:util";
6
6
  import { updaterPaths } from "./paths.js";
7
7
  import { daemonControlRequest } from "./control.js";
8
8
  import { readUpdaterMarker, readUpdaterOwner } from "./ownership.js";
9
+ import { managedProcessEnvironment } from "./managed-environment.js";
9
10
  const execFileAsync = promisify(execFile);
10
11
  export async function localDaemonStatus(stateRoot) {
11
12
  const file = updaterPaths(stateRoot).daemonPidPath;
@@ -91,7 +92,7 @@ export async function startDaemon(input) {
91
92
  const stderr = fsSync.openSync(paths.daemonStderrPath, "a", 0o600);
92
93
  const child = spawn(input.nodeExecutable, [input.daemonEntrypoint, "start"], {
93
94
  cwd: input.stateRoot,
94
- env: { ...process.env, HAPPY_ELVES_HOME: input.stateRoot, HAPPY_ELVES_UPDATER_GENERATION: input.generation },
95
+ env: { ...managedProcessEnvironment(), HAPPY_ELVES_HOME: input.stateRoot, HAPPY_ELVES_UPDATER_GENERATION: input.generation },
95
96
  detached: true,
96
97
  stdio: ["ignore", stdout, stderr],
97
98
  windowsHide: true,
@@ -119,7 +120,7 @@ export function spawnUpdaterFromStableLauncher(input) {
119
120
  const child = spawn(input.nodeExecutable, [runtimeScript, "updater", "run", "--state-root", input.stateRoot], {
120
121
  cwd: input.stateRoot,
121
122
  env: {
122
- ...process.env,
123
+ ...managedProcessEnvironment(),
123
124
  HAPPY_ELVES_HOME: input.stateRoot,
124
125
  ...(input.handoffFromPid ? { HAPPY_ELVES_UPDATER_HANDOFF_FROM: String(input.handoffFromPid) } : {}),
125
126
  ...(input.handoffToken ? { HAPPY_ELVES_UPDATER_HANDOFF_TOKEN: input.handoffToken } : {}),
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Read the current user's Windows environment without making it part of any
3
+ * host-job request. A manager can itself be launched by a deliberately
4
+ * sanitized host job; in that case process.env no longer contains provider
5
+ * credentials that the normal user-launched daemon would inherit.
6
+ *
7
+ * The result is used only for manager-owned updater/daemon children. It is
8
+ * never logged, persisted, sent over Relay, or passed to arbitrary host jobs.
9
+ */
10
+ export declare function managedProcessEnvironment(base?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, userEnvironment?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
11
+ export declare function parseWindowsUserEnvironment(output: string, expansionEnvironment?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
@@ -0,0 +1,125 @@
1
+ import { execFileSync } from "node:child_process";
2
+ const environmentLookupTimeoutMs = 2_000;
3
+ /**
4
+ * Read the current user's Windows environment without making it part of any
5
+ * host-job request. A manager can itself be launched by a deliberately
6
+ * sanitized host job; in that case process.env no longer contains provider
7
+ * credentials that the normal user-launched daemon would inherit.
8
+ *
9
+ * The result is used only for manager-owned updater/daemon children. It is
10
+ * never logged, persisted, sent over Relay, or passed to arbitrary host jobs.
11
+ */
12
+ export function managedProcessEnvironment(base = process.env, platform = process.platform, userEnvironment = platform === "win32" ? readWindowsUserEnvironment(base) : {}) {
13
+ if (platform !== "win32")
14
+ return { ...base };
15
+ const result = { ...base };
16
+ for (const [key, value] of Object.entries(userEnvironment)) {
17
+ if (!key || value === undefined || hasEnvironmentKey(result, key))
18
+ continue;
19
+ result[key] = value;
20
+ }
21
+ return result;
22
+ }
23
+ export function parseWindowsUserEnvironment(output, expansionEnvironment = {}) {
24
+ const entries = new Map();
25
+ const trimmed = output.trim();
26
+ if (trimmed.startsWith("{")) {
27
+ try {
28
+ const parsed = JSON.parse(trimmed);
29
+ for (const [key, value] of Object.entries(parsed)) {
30
+ if (!key)
31
+ continue;
32
+ if (typeof value === "string")
33
+ entries.set(key, { value, expand: false });
34
+ else if (value && typeof value === "object" && "value" in value && typeof value.value === "string") {
35
+ const typedValue = value;
36
+ entries.set(key, { value: typedValue.value, expand: isExpandableRegistryType(typedValue.type) });
37
+ }
38
+ }
39
+ return materializeEnvironment(entries, expansionEnvironment);
40
+ }
41
+ catch {
42
+ // Fall through to the line-oriented reg.exe parser.
43
+ }
44
+ }
45
+ for (const line of output.split(/\r?\n/u)) {
46
+ // `reg query HKCU\\Environment` prints:
47
+ // NAME REG_SZ value
48
+ const match = /^\s{4,}([^\s]+)\s+(REG_[A-Z0-9_]+)\s*(.*)$/u.exec(line);
49
+ if (!match)
50
+ continue;
51
+ const [, key, type, rawValue] = match;
52
+ if (!key || !rawValue)
53
+ continue;
54
+ entries.set(key, { value: rawValue, expand: isExpandableRegistryType(type) });
55
+ }
56
+ return materializeEnvironment(entries, expansionEnvironment);
57
+ }
58
+ function isExpandableRegistryType(type) {
59
+ if (typeof type !== "string")
60
+ return false;
61
+ const normalized = type.trim().toUpperCase().replace(/[-_\s]/gu, "");
62
+ // PowerShell's RegistryValueKind.ToString() returns ExpandString, while
63
+ // reg.exe uses REG_EXPAND_SZ. Both describe the same registry semantics.
64
+ return normalized === "EXPANDSTRING" || normalized === "REGEXPANDSZ";
65
+ }
66
+ function readWindowsUserEnvironment(base) {
67
+ try {
68
+ const output = execFileSync("powershell.exe", [
69
+ "-NoLogo",
70
+ "-NoProfile",
71
+ "-NonInteractive",
72
+ "-Command",
73
+ "$ErrorActionPreference='Stop'; $OutputEncoding=New-Object System.Text.UTF8Encoding($false); [Console]::OutputEncoding=New-Object System.Text.UTF8Encoding($false); $k=[Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment'); if ($null -eq $k) { '{}' } else { $o=[ordered]@{}; foreach ($n in $k.GetValueNames()) { $o[$n]=[ordered]@{ value=[string]$k.GetValue($n,$null,[Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames); type=$k.GetValueKind($n).ToString() } }; $o | ConvertTo-Json -Compress; $k.Dispose() }",
74
+ ], {
75
+ encoding: "utf8",
76
+ stdio: ["ignore", "pipe", "ignore"],
77
+ windowsHide: true,
78
+ timeout: environmentLookupTimeoutMs,
79
+ });
80
+ return parseWindowsUserEnvironment(output, base);
81
+ }
82
+ catch {
83
+ try {
84
+ const output = execFileSync("reg.exe", ["query", "HKCU\\Environment"], {
85
+ encoding: "utf8",
86
+ stdio: ["ignore", "pipe", "ignore"],
87
+ windowsHide: true,
88
+ timeout: environmentLookupTimeoutMs,
89
+ });
90
+ return parseWindowsUserEnvironment(output, base);
91
+ }
92
+ catch {
93
+ // A missing registry helper or a locked-down profile must not prevent
94
+ // the updater from starting. The daemon will surface a provider error
95
+ // if it genuinely needs a credential that is unavailable in its env.
96
+ return {};
97
+ }
98
+ }
99
+ }
100
+ function materializeEnvironment(entries, expansionEnvironment) {
101
+ const result = {};
102
+ const combined = { ...expansionEnvironment };
103
+ for (const [key, entry] of entries)
104
+ combined[key] = entry.value;
105
+ for (const [key, entry] of entries) {
106
+ result[key] = entry.expand ? expandWindowsVariables(entry.value, combined) : entry.value;
107
+ }
108
+ return result;
109
+ }
110
+ function hasEnvironmentKey(environment, key) {
111
+ if (Object.hasOwn(environment, key))
112
+ return true;
113
+ const normalized = key.toUpperCase();
114
+ return Object.keys(environment).some((candidate) => candidate.toUpperCase() === normalized);
115
+ }
116
+ function expandWindowsVariables(value, environment) {
117
+ return value.replace(/%([^%]+)%/gu, (match, name) => {
118
+ const direct = environment[name];
119
+ if (direct !== undefined)
120
+ return direct;
121
+ const existing = Object.entries(environment).find(([key]) => key.toUpperCase() === name.toUpperCase());
122
+ return existing?.[1] ?? match;
123
+ });
124
+ }
125
+ //# sourceMappingURL=managed-environment.js.map
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.500",
3
+ "version": "0.1.0-beta.501",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
- "buildId": "48f1f04bc7d6747ed2bf59b3fb1e2a58",
5
+ "buildId": "4edbb3ee3e6735ee0abb55e9b0612b17",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "happy-elves": "./apps/cli/dist/index.js",