@cydm/happy-elves 0.1.0-beta.271 → 0.1.0-beta.273

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.
Files changed (42) hide show
  1. package/apps/cli/dist/commands/app.js +2 -0
  2. package/apps/cli/dist/commands/lib/args.js +13 -0
  3. package/apps/cli/dist/commands/lib/paths.d.ts +1 -0
  4. package/apps/cli/dist/commands/lib/paths.js +1 -0
  5. package/apps/cli/dist/commands/lib/session-view.d.ts +3 -0
  6. package/apps/cli/dist/commands/lib/session-view.js +3 -0
  7. package/apps/cli/dist/commands/lib/types.d.ts +2 -0
  8. package/apps/cli/dist/commands/lib/usage.js +43 -10
  9. package/apps/cli/dist/commands/release.d.ts +2 -0
  10. package/apps/cli/dist/commands/release.js +299 -0
  11. package/apps/cli/dist/commands/remote.js +6 -2
  12. package/apps/cli/dist/commands/session.js +59 -0
  13. package/apps/cli/dist/commands/turn.js +9 -2
  14. package/apps/cli/package.json +1 -1
  15. package/apps/daemon/dist/paths.d.ts +1 -0
  16. package/apps/daemon/dist/paths.js +1 -0
  17. package/apps/daemon/dist/relay/connection.js +5 -0
  18. package/apps/daemon/dist/relay/lifecycle.js +74 -5
  19. package/apps/daemon/dist/relay/register.js +1 -0
  20. package/apps/daemon/dist/relay/release.d.ts +7 -0
  21. package/apps/daemon/dist/relay/release.js +359 -0
  22. package/apps/daemon/dist/types.d.ts +2 -0
  23. package/apps/daemon/package.json +1 -1
  24. package/apps/relay/dist/controller-handlers.js +61 -0
  25. package/apps/relay/dist/machine-handlers.js +12 -0
  26. package/package.json +1 -1
  27. package/packages/client/dist/client.d.ts +6 -1
  28. package/packages/client/dist/client.js +53 -2
  29. package/packages/client/dist/index.d.ts +1 -1
  30. package/packages/client/dist/transport.d.ts +5 -0
  31. package/packages/client/dist/transport.js +7 -0
  32. package/packages/client/dist/types.d.ts +25 -1
  33. package/packages/shared/dist/args.js +16 -4
  34. package/packages/shared/dist/index.d.ts +1 -0
  35. package/packages/shared/dist/index.js +1 -0
  36. package/packages/shared/dist/protocol-schemas.d.ts +107 -0
  37. package/packages/shared/dist/protocol-schemas.js +57 -0
  38. package/packages/shared/dist/protocol-types.d.ts +58 -0
  39. package/packages/shared/dist/protocol.d.ts +33 -2
  40. package/packages/shared/dist/protocol.js +37 -1
  41. package/packages/shared/dist/session-tags.d.ts +12 -0
  42. package/packages/shared/dist/session-tags.js +50 -0
@@ -9,6 +9,7 @@ import { handleLoop } from "./loop.js";
9
9
  import { handleMachine } from "./machine.js";
10
10
  import { handleMemory } from "./memory.js";
11
11
  import { handleRelay } from "./relay.js";
12
+ import { handleRelease } from "./release.js";
12
13
  import { handleSchedule } from "./schedule.js";
13
14
  import { handleRemote } from "./remote.js";
14
15
  import { handleSession } from "./session.js";
@@ -26,6 +27,7 @@ const domainHandlers = [
26
27
  handleLoop,
27
28
  handleDaemon,
28
29
  handleRelay,
30
+ handleRelease,
29
31
  handleMachine,
30
32
  handleMemory,
31
33
  handleSchedule,
@@ -67,7 +67,18 @@ const valueFlags = new Set([
67
67
  "source",
68
68
  "stop-on",
69
69
  "tail",
70
+ "tag",
70
71
  "tags",
72
+ "release-tag",
73
+ "release-tags",
74
+ "reviewer-tag",
75
+ "reviewer-tags",
76
+ "dogfood-tag",
77
+ "dogfood-tags",
78
+ "target-machine",
79
+ "target-machines",
80
+ "expected-commit",
81
+ "profile",
71
82
  "text",
72
83
  "text-file",
73
84
  "timeout",
@@ -110,6 +121,8 @@ const booleanFlags = new Set([
110
121
  "wait",
111
122
  "hidden",
112
123
  "manual",
124
+ "copy-tags",
125
+ "no-daemon-update",
113
126
  ]);
114
127
  const knownFlags = new Set([...valueFlags, ...booleanFlags]);
115
128
  const dashPrefixedValueFlags = new Set(["secret"]);
@@ -8,3 +8,4 @@ export declare const daemonPidPath: string;
8
8
  export declare const loopStorePath: string;
9
9
  export declare const loopStoreLockPath: string;
10
10
  export declare const gatewayStorePath: string;
11
+ export declare const releaseProfilesPath: string;
@@ -12,4 +12,5 @@ export const daemonPidPath = path.join(configDir, "daemon.pid");
12
12
  export const loopStorePath = path.join(configDir, "loops.json");
13
13
  export const loopStoreLockPath = path.join(configDir, "loops.json.lock");
14
14
  export const gatewayStorePath = path.join(configDir, "gateway.json");
15
+ export const releaseProfilesPath = path.join(configDir, "release-profiles.json");
15
16
  //# sourceMappingURL=paths.js.map
@@ -25,6 +25,7 @@ export declare function structuredSessionProjection(client: ControllerClient, se
25
25
  sourceSessionId?: string;
26
26
  sourceCheckpointId?: string;
27
27
  capabilities: SessionSnapshot["capabilities"];
28
+ tags: string[];
28
29
  orchestration: OrchestrationSummary;
29
30
  }>;
30
31
  export declare function compactSessionListItem(client: ControllerClient, session: SessionSnapshot, options?: {
@@ -40,6 +41,7 @@ export declare function compactSessionListItem(client: ControllerClient, session
40
41
  machineId?: string;
41
42
  machineName?: string;
42
43
  cwdName?: string;
44
+ tags: string[];
43
45
  }>;
44
46
  export declare function conciseSessionSummary(client: ControllerClient, session: SessionSnapshot): Promise<{
45
47
  sessionId: string;
@@ -50,6 +52,7 @@ export declare function conciseSessionSummary(client: ControllerClient, session:
50
52
  status: SessionSnapshot["status"];
51
53
  state: OrchestrationState;
52
54
  updatedAt: number;
55
+ tags: string[];
53
56
  }>;
54
57
  export declare function requireProjectedSession(client: ControllerClient, sessionId: string, action: string): Promise<SessionSnapshot>;
55
58
  export declare function machineSessionOverview(client: ControllerClient, sessions: SessionSnapshot[], machineId: string): Promise<{
@@ -117,6 +117,7 @@ export async function structuredSessionProjection(client, session, events = [])
117
117
  sourceSessionId: session.sourceSessionId,
118
118
  sourceCheckpointId: session.sourceCheckpointId,
119
119
  capabilities: session.capabilities,
120
+ tags: Array.isArray(metadata.tags) ? metadata.tags.filter((tag) => typeof tag === "string") : [],
120
121
  orchestration,
121
122
  };
122
123
  }
@@ -135,6 +136,7 @@ export async function compactSessionListItem(client, session, options = {}) {
135
136
  state: orchestration.state,
136
137
  agent: metadata.agent ?? session.agent,
137
138
  updatedAt: session.updatedAt,
139
+ tags: Array.isArray(metadata.tags) ? metadata.tags.filter((tag) => typeof tag === "string") : [],
138
140
  ...(options.includeMachine ? { machineId: session.machineId, machineName: options.machineName } : {}),
139
141
  ...(options.includeCwd ? { cwdName: cwdDisplayName(metadata.cwd ?? session.cwd) } : {}),
140
142
  };
@@ -151,6 +153,7 @@ export async function conciseSessionSummary(client, session) {
151
153
  status: session.status,
152
154
  state: orchestration.state,
153
155
  updatedAt: session.updatedAt,
156
+ tags: Array.isArray(metadata.tags) ? metadata.tags.filter((tag) => typeof tag === "string") : [],
154
157
  };
155
158
  }
156
159
  export async function requireProjectedSession(client, sessionId, action) {
@@ -29,6 +29,8 @@ export type SessionMetadata = {
29
29
  cwd?: string;
30
30
  name?: string;
31
31
  requestedRuntimeName?: string;
32
+ tags?: string[];
33
+ tagKeys?: string[];
32
34
  runtime?: unknown;
33
35
  };
34
36
  export declare const idleSessionStatuses: SessionSnapshot["status"][];
@@ -50,8 +50,11 @@ Check local controller config, relay reachability, and daemon state.
50
50
  file preview is read-only and limited to files inside --root.
51
51
  `,
52
52
  session: `Usage:
53
- happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] [--hidden] [--source dogfood] --json
54
- happy-elves session list [--machine <machineId>] [--agent <agent>] [--cwd <cwd>] [--name <name>] [--limit n] [--all] [--verbose] [--json]
53
+ happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] [--hidden] [--source dogfood] [--tag <tag>] --json
54
+ happy-elves session list [--machine <machineId>] [--agent <agent>] [--cwd <cwd>] [--name <name>] [--tag <tag>] [--limit n] [--all] [--verbose] [--json]
55
+ happy-elves session tag add <sessionId> --tag <tag> --json
56
+ happy-elves session tag remove <sessionId> --tag <tag> --json
57
+ happy-elves session tag list <sessionId> --json
55
58
  happy-elves session history --machine <machineId> [--cwd <path>] [--agent <agent>] [--include-archived] [--limit 20] --json
56
59
  happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] --json
57
60
  happy-elves session status <sessionId> [--verbose] [--json]
@@ -73,6 +76,7 @@ Session management:
73
76
  repair-binding imports the matching runtime history row as a new loaded
74
77
  session and closes the empty/stale source session unless --keep-source is set.
75
78
  close archives a loaded session. There is no separate archive command.
79
+ tags are encrypted session metadata used for release/reviewer/dogfood routing.
76
80
  `,
77
81
  turn: `Usage:
78
82
  happy-elves turn list <sessionId> [--limit 10] [--verbose] [--json]
@@ -81,10 +85,10 @@ Session management:
81
85
  happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--inactivity-timeout 1000s] [--permission-mode approve-reads|approve-all|deny-all] [--memory off|readonly|readwrite] [--memory-project <key>] [--memory-top-n 3] [--json]
82
86
  happy-elves turn wait <sessionId> <turnId> [--timeout 1000s] --json
83
87
  happy-elves turn reconcile <sessionId> [turnId] --json
84
- happy-elves turn recover <sessionId> [turnId] --text <continuation> --confirm-risk [--detach] --json
88
+ happy-elves turn recover <sessionId> [turnId] --text <continuation> --confirm-risk [--copy-tags] [--detach] --json
85
89
  happy-elves turn cancel <sessionId> <turnId> [--reason <reason>] --json
86
90
  happy-elves turn rewind <sessionId> <turn-or-checkpoint-id> --json
87
- happy-elves turn fork <sessionId> <turn-or-checkpoint-id> --name <name> --json
91
+ happy-elves turn fork <sessionId> <turn-or-checkpoint-id> --name <name> [--copy-tags] --json
88
92
 
89
93
  Turn management:
90
94
  run starts one agent execution. It waits by default; use --detach for
@@ -113,6 +117,19 @@ Turn management:
113
117
 
114
118
  Workspace sync records account-level UI/library state shared across devices:
115
119
  session stars, recent sessions, pinned workspaces, and current project defaults.
120
+ `,
121
+ release: `Usage:
122
+ happy-elves release profile list --json
123
+ happy-elves release profile show [--profile <name>] --json
124
+ happy-elves release profile set <name> --release-tag <tag> [--release-tag <tag>] [--reviewer-tag <tag>] [--dogfood-tag <tag>] [--no-daemon-update] --json
125
+ happy-elves release resolve [--profile happy-elves] --json
126
+ happy-elves release start [--profile happy-elves] [--expected-commit <sha>] [--target-machine <machineId>] [--no-daemon-update] [--wait] --json
127
+ happy-elves release status [jobId] [--machine <machineId>] --json
128
+ happy-elves release logs [jobId] [--machine <machineId>] [--tail 200] --json
129
+
130
+ Release profiles resolve target sessions by encrypted session tags. The built-in
131
+ happy-elves profile uses Happy Elves + Release Session for the release target.
132
+ Release start runs a fixed daemon-local runner; it does not accept arbitrary shell.
116
133
  `,
117
134
  gateway: `Usage:
118
135
  happy-elves gateway enable [<channelId>] --channel lark --machine <machineId> (--cwd <cwd> | --session <sessionId>) [--agent codex] [--model <model>] --json
@@ -210,7 +227,9 @@ Core commands:
210
227
  machine directory <machineId> [--path <path>] [--json]
211
228
 
212
229
  session list [--machine <machineId>] [--cwd <path>] [--limit 20] [--json]
213
- session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--hidden] --json
230
+ session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--hidden] [--tag <tag>] --json
231
+ session tag add <sessionId> --tag <tag> --json
232
+ session tag remove <sessionId> --tag <tag> --json
214
233
  session history --machine <machineId> [--cwd <path>] [--limit 20] --json
215
234
  session continue <runtimeSessionId> --machine <machineId> --json
216
235
  session repair-binding <sessionId> [--runtime-session <runtimeSessionId>] --json
@@ -224,10 +243,10 @@ Core commands:
224
243
  turn wait <sessionId> <turnId> [--timeout 1000s] [--json]
225
244
  turn result <sessionId> [<turnId>] [--json]
226
245
  turn reconcile <sessionId> [turnId] --json
227
- turn recover <sessionId> [turnId] --text <continuation> --confirm-risk [--json]
246
+ turn recover <sessionId> [turnId] --text <continuation> --confirm-risk [--copy-tags] [--json]
228
247
  turn cancel <sessionId> <turnId> [--reason <reason>] --json
229
248
  turn rewind <sessionId> <turn-or-checkpoint-id> --json
230
- turn fork <sessionId> <turn-or-checkpoint-id> --name <name> --json
249
+ turn fork <sessionId> <turn-or-checkpoint-id> --name <name> [--copy-tags] --json
231
250
 
232
251
  remote status --json
233
252
  remote devices [--json]
@@ -238,8 +257,11 @@ Core commands:
238
257
  workspace star <sessionId> --json
239
258
  workspace pin-cwd --machine <machineId> --cwd <cwd> --json
240
259
 
260
+ release resolve --profile happy-elves --json
261
+ release start --profile happy-elves --json
262
+
241
263
  gateway status --json
242
- gateway enable --channel fake --machine <machineId> --cwd <cwd> --json # debug only
264
+ gateway enable --channel lark --machine <machineId> --cwd <cwd> --json
243
265
  gateway test --conversation <id> --message-id <id> --text <prompt> --json
244
266
 
245
267
  memory save --project <key> --text <fact> --json
@@ -261,12 +283,12 @@ Common flows:
261
283
  Discover: machine list --json; session list --machine <machineId> --json
262
284
  Continue: session history --machine <machineId> --json; session continue <runtimeSessionId> --machine <machineId> --json
263
285
  Execute: turn run <sessionId> --text <prompt> --json; turn result <sessionId> <turnId> --json
264
- Branch: turn rewind <sessionId> <turnId> --json; turn fork <sessionId> <turnId> --name <name> --json
286
+ Branch: turn rewind <sessionId> <turnId> --json; turn fork <sessionId> <turnId> --name <name> [--copy-tags] --json
265
287
 
266
288
  More:
267
289
  happy-elves --skill
268
290
  happy-elves <domain> --help
269
- start, status, doctor, collect, config, account, daemon, relay, token, workspace, gateway, memory, schedule, loop.
291
+ start, status, doctor, collect, config, account, daemon, relay, token, workspace, release, gateway, memory, schedule, loop.
270
292
  `;
271
293
  }
272
294
  export function skillText() {
@@ -292,6 +314,17 @@ Recommended agent workflow:
292
314
  6. turn wait <sessionId> <turnId> --json
293
315
  7. turn result <sessionId> <turnId> --json
294
316
 
317
+ Release / reviewer / dogfood workflow:
318
+ 1. Tag the release session once:
319
+ session tag add <sessionId> --tag "Happy Elves" --json
320
+ session tag add <sessionId> --tag "Release Session" --json
321
+ 2. Resolve before acting:
322
+ release resolve --profile happy-elves --json
323
+ 3. Start release only when the release target is exactly one session:
324
+ release start --profile happy-elves --json
325
+ 4. Use hidden sessions for temporary dogfood:
326
+ session create --hidden --source dogfood --tag "Happy Elves" --tag "Dogfood" ...
327
+
295
328
  Concepts:
296
329
  Loaded session: a Happy Elves control-plane session that can be operated by
297
330
  this CLI.
@@ -0,0 +1,2 @@
1
+ import type { CommandInput } from "./command.js";
2
+ export declare function handleRelease({ domain, action, positional, flags }: CommandInput): Promise<boolean>;
@@ -0,0 +1,299 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { normalizeSessionTags } from "../../../../packages/shared/dist/index.js";
4
+ import { CliError, ControllerClient, compactText, ok, parseDurationMs, readConfig, releaseProfilesPath, requirePositional, wantsJson, } from "./lib/index.js";
5
+ const defaultReleaseProfile = {
6
+ name: "happy-elves",
7
+ releaseSessionTags: ["Happy Elves", "Release Session"],
8
+ reviewerSessionTags: ["Happy Elves", "Reviewer"],
9
+ dogfoodSessionTags: ["Happy Elves", "Dogfood"],
10
+ targetDaemonPolicy: "all-online-lifecycle",
11
+ notifyOnStart: false,
12
+ notifyOnSuccess: true,
13
+ notifyOnFailure: true,
14
+ };
15
+ export async function handleRelease({ domain, action, positional, flags }) {
16
+ if (domain !== "release")
17
+ return false;
18
+ if (action === "profile") {
19
+ const subaction = requirePositional(positional[0], "profile action");
20
+ if (subaction === "list") {
21
+ const store = await readReleaseProfileStore();
22
+ printReleaseOutput("release.profile.list", { profiles: store.profiles, defaultProfile: defaultReleaseProfile.name }, flags);
23
+ return true;
24
+ }
25
+ if (subaction === "show") {
26
+ const name = positional[1] || profileNameFromFlags(flags);
27
+ const { profile } = await loadProfile(name);
28
+ printReleaseOutput("release.profile.show", { profile }, flags);
29
+ return true;
30
+ }
31
+ if (subaction === "set") {
32
+ const name = positional[1] || profileNameFromFlags(flags);
33
+ const profile = profileFromFlags(name, flags);
34
+ const store = await readReleaseProfileStore();
35
+ const profiles = store.profiles.filter((item) => item.name !== profile.name);
36
+ profiles.push(profile);
37
+ await writeReleaseProfileStore({ ...store, profiles: profiles.sort((left, right) => left.name.localeCompare(right.name)) });
38
+ printReleaseOutput("release.profile.set", { profile }, flags);
39
+ return true;
40
+ }
41
+ throw new CliError(`Unsupported release profile action: ${subaction}`, "USAGE");
42
+ }
43
+ if (action === "resolve") {
44
+ const config = await readConfig(flags);
45
+ const client = new ControllerClient(config);
46
+ const { profile } = await loadProfile(profileNameFromFlags(flags));
47
+ const resolved = await resolveReleaseTargets(client, profile, flags);
48
+ printReleaseOutput("release.resolve", resolved, flags);
49
+ return true;
50
+ }
51
+ if (action === "start") {
52
+ const config = await readConfig(flags);
53
+ const client = new ControllerClient(config);
54
+ const { profile } = await loadProfile(profileNameFromFlags(flags));
55
+ const resolved = await resolveReleaseTargets(client, profile, flags);
56
+ if (resolved.errors.length > 0) {
57
+ throw new CliError(resolved.errors[0]?.message ?? "Release target resolution failed", resolved.errors[0]?.code ?? "TAG_TARGET_NOT_FOUND");
58
+ }
59
+ const targetMachineIds = targetMachineIdsForRelease(resolved, flags);
60
+ const expectedCommit = typeof flags["expected-commit"] === "string" && flags["expected-commit"].trim() ? flags["expected-commit"].trim() : undefined;
61
+ const result = await client.releaseJob({
62
+ machineId: resolved.releaseTarget.machineId,
63
+ action: "start",
64
+ release: {
65
+ profile,
66
+ releaseTarget: resolved.releaseTarget,
67
+ ...(resolved.reviewerTarget ? { reviewerTarget: resolved.reviewerTarget } : {}),
68
+ ...(resolved.dogfoodTarget ? { dogfoodTarget: resolved.dogfoodTarget } : {}),
69
+ ...(targetMachineIds.length > 0 ? { targetMachineIds } : {}),
70
+ ...(expectedCommit ? { expectedCommit } : {}),
71
+ },
72
+ waitTimeoutMs: parseDurationMs(flags["wait-timeout"] ?? flags.timeout, 45_000),
73
+ });
74
+ await rememberReleaseJob(profile.name, result.jobId ?? result.requestId, result.machineId);
75
+ if (flags.wait === true && result.jobId) {
76
+ const waited = await waitForReleaseJob(client, result.machineId, result.jobId, parseDurationMs(flags.timeout, 30 * 60 * 1000));
77
+ printReleaseOutput("release.start", { ...result, waited }, flags);
78
+ }
79
+ else {
80
+ printReleaseOutput("release.start", result, flags);
81
+ }
82
+ return true;
83
+ }
84
+ if (action === "status" || action === "logs") {
85
+ const jobId = positional[0];
86
+ const config = await readConfig(flags);
87
+ const client = new ControllerClient(config);
88
+ const machineId = await releaseJobMachineId(jobId, flags);
89
+ const result = await client.releaseJob({
90
+ machineId,
91
+ action,
92
+ ...(jobId ? { jobId } : {}),
93
+ tail: parseTail(flags),
94
+ waitTimeoutMs: parseDurationMs(flags["wait-timeout"] ?? flags.timeout, 45_000),
95
+ });
96
+ printReleaseOutput(`release.${action}`, result, flags);
97
+ return true;
98
+ }
99
+ return false;
100
+ }
101
+ function profileNameFromFlags(flags) {
102
+ return typeof flags.profile === "string" && flags.profile.trim() ? flags.profile.trim() : defaultReleaseProfile.name;
103
+ }
104
+ function profileFromFlags(name, flags) {
105
+ const releaseSessionTags = tagsFromFlags(flags, "release-tag", "release-tags");
106
+ if (releaseSessionTags.length === 0)
107
+ throw new CliError("Missing --release-tag", "MISSING_ARGUMENT");
108
+ const reviewerSessionTags = tagsFromFlags(flags, "reviewer-tag", "reviewer-tags");
109
+ const dogfoodSessionTags = tagsFromFlags(flags, "dogfood-tag", "dogfood-tags");
110
+ return {
111
+ name,
112
+ releaseSessionTags,
113
+ ...(reviewerSessionTags.length > 0 ? { reviewerSessionTags } : {}),
114
+ ...(dogfoodSessionTags.length > 0 ? { dogfoodSessionTags } : {}),
115
+ targetDaemonPolicy: flags["no-daemon-update"] === true ? "none" : "all-online-lifecycle",
116
+ notifyOnStart: false,
117
+ notifyOnSuccess: true,
118
+ notifyOnFailure: true,
119
+ };
120
+ }
121
+ function tagsFromFlags(flags, single, plural) {
122
+ return normalizeSessionTags([
123
+ ...tagsFromFlagValue(flags[single]),
124
+ ...tagsFromFlagValue(flags[plural]),
125
+ ]);
126
+ }
127
+ function tagsFromFlagValue(value) {
128
+ if (typeof value !== "string")
129
+ return [];
130
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
131
+ }
132
+ async function loadProfile(name) {
133
+ const store = await readReleaseProfileStore();
134
+ return { profile: store.profiles.find((profile) => profile.name === name) ?? defaultReleaseProfile, store };
135
+ }
136
+ async function readReleaseProfileStore() {
137
+ try {
138
+ const parsed = JSON.parse(await fs.readFile(releaseProfilesPath, "utf8"));
139
+ const profiles = Array.isArray(parsed.profiles) ? parsed.profiles.filter(isReleaseProfile) : [];
140
+ const hasDefault = profiles.some((profile) => profile.name === defaultReleaseProfile.name);
141
+ return {
142
+ version: 1,
143
+ profiles: hasDefault ? profiles : [defaultReleaseProfile, ...profiles],
144
+ lastJobs: parsed.lastJobs && typeof parsed.lastJobs === "object" && !Array.isArray(parsed.lastJobs) ? parsed.lastJobs : {},
145
+ };
146
+ }
147
+ catch (error) {
148
+ if (error.code !== "ENOENT")
149
+ throw error;
150
+ return { version: 1, profiles: [defaultReleaseProfile], lastJobs: {} };
151
+ }
152
+ }
153
+ function isReleaseProfile(value) {
154
+ if (!value || typeof value !== "object")
155
+ return false;
156
+ const candidate = value;
157
+ return typeof candidate.name === "string" && Array.isArray(candidate.releaseSessionTags);
158
+ }
159
+ async function writeReleaseProfileStore(store) {
160
+ await fs.mkdir(path.dirname(releaseProfilesPath), { recursive: true });
161
+ const tempPath = `${releaseProfilesPath}.${process.pid}.${Date.now()}.tmp`;
162
+ await fs.writeFile(tempPath, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
163
+ await fs.rename(tempPath, releaseProfilesPath);
164
+ }
165
+ async function resolveReleaseTargets(client, profile, flags) {
166
+ const warnings = [];
167
+ const errors = [];
168
+ const release = await resolveTaggedSession(client, profile.releaseSessionTags);
169
+ if (release.kind === "missing")
170
+ errors.push({ code: "TAG_TARGET_NOT_FOUND", message: `No release session matches tags: ${profile.releaseSessionTags.join(", ")}` });
171
+ if (release.kind === "ambiguous")
172
+ errors.push({ code: "TAG_TARGET_AMBIGUOUS", message: `Multiple release sessions match tags: ${profile.releaseSessionTags.join(", ")}`, candidates: release.candidates });
173
+ const reviewer = profile.reviewerSessionTags?.length ? await resolveTaggedSession(client, profile.reviewerSessionTags) : { kind: "skipped" };
174
+ if (reviewer.kind === "ambiguous")
175
+ errors.push({ code: "TAG_TARGET_AMBIGUOUS", message: `Multiple reviewer sessions match tags: ${profile.reviewerSessionTags?.join(", ")}`, candidates: reviewer.candidates });
176
+ if (reviewer.kind === "missing")
177
+ warnings.push({ code: "TAG_TARGET_NOT_FOUND", message: `No reviewer session matches tags: ${profile.reviewerSessionTags?.join(", ")}` });
178
+ const dogfood = profile.dogfoodSessionTags?.length ? await resolveTaggedSession(client, profile.dogfoodSessionTags) : { kind: "skipped" };
179
+ if (dogfood.kind === "ambiguous")
180
+ errors.push({ code: "TAG_TARGET_AMBIGUOUS", message: `Multiple dogfood sessions match tags: ${profile.dogfoodSessionTags?.join(", ")}`, candidates: dogfood.candidates });
181
+ if (dogfood.kind === "missing")
182
+ warnings.push({ code: "TAG_TARGET_NOT_FOUND", message: `No dogfood session matches tags: ${profile.dogfoodSessionTags?.join(", ")}` });
183
+ const targetMachineIds = targetMachineIdsForPolicy(await client.listMachines(), profile, flags);
184
+ return {
185
+ profile,
186
+ releaseTarget: release.kind === "found" ? release.session : emptyReleaseTarget(),
187
+ ...(reviewer.kind === "found" ? { reviewerTarget: reviewer.session } : {}),
188
+ ...(dogfood.kind === "found" ? { dogfoodTarget: dogfood.session } : {}),
189
+ targetMachineIds,
190
+ warnings,
191
+ errors,
192
+ };
193
+ }
194
+ async function resolveTaggedSession(client, tags) {
195
+ const sessions = (await client.listSessions({ tags }))
196
+ .filter((session) => session.status !== "closed");
197
+ const summaries = await Promise.all(sessions.map(async (session) => {
198
+ const metadata = await client.decodeSessionMetadata(session);
199
+ const sessionTags = normalizeSessionTags(Array.isArray(metadata.tags) ? metadata.tags : []);
200
+ return {
201
+ sessionId: session.id,
202
+ machineId: session.machineId,
203
+ agent: metadata.agent ?? session.agent,
204
+ cwd: metadata.cwd ?? session.cwd,
205
+ name: metadata.name ?? session.name,
206
+ tags: sessionTags,
207
+ };
208
+ }));
209
+ if (summaries.length === 0)
210
+ return { kind: "missing" };
211
+ if (summaries.length > 1)
212
+ return { kind: "ambiguous", candidates: summaries };
213
+ return { kind: "found", session: summaries[0] };
214
+ }
215
+ function emptyReleaseTarget() {
216
+ return { sessionId: "", machineId: "", agent: "", cwd: "", name: "", tags: [] };
217
+ }
218
+ function targetMachineIdsForPolicy(machines, profile, flags) {
219
+ const explicit = normalizeSessionTags([
220
+ ...tagsFromFlagValue(flags["target-machine"]),
221
+ ...tagsFromFlagValue(flags["target-machines"]),
222
+ ]);
223
+ if (explicit.length > 0)
224
+ return explicit;
225
+ if (flags["no-daemon-update"] === true || profile.targetDaemonPolicy === "none")
226
+ return [];
227
+ return machines
228
+ .filter((machine) => machine.online && machine.capabilities.daemonLifecycle === true)
229
+ .map((machine) => machine.id);
230
+ }
231
+ function targetMachineIdsForRelease(resolved, flags) {
232
+ if (flags["no-daemon-update"] === true)
233
+ return [];
234
+ return resolved.targetMachineIds;
235
+ }
236
+ async function rememberReleaseJob(profileName, jobId, machineId) {
237
+ const store = await readReleaseProfileStore();
238
+ const entry = { jobId, machineId, profileName, startedAt: new Date().toISOString() };
239
+ await writeReleaseProfileStore({
240
+ ...store,
241
+ lastJobs: {
242
+ ...store.lastJobs,
243
+ [profileName]: entry,
244
+ [jobId]: entry,
245
+ },
246
+ });
247
+ }
248
+ async function releaseJobMachineId(jobId, flags) {
249
+ if (typeof flags.machine === "string" && flags.machine.trim())
250
+ return flags.machine.trim();
251
+ const profile = profileNameFromFlags(flags);
252
+ const store = await readReleaseProfileStore();
253
+ const entry = jobId ? store.lastJobs[jobId] : store.lastJobs[profile];
254
+ if (!entry?.machineId)
255
+ throw new CliError("Missing --machine and no remembered release job machine is available", "MISSING_ARGUMENT");
256
+ return entry.machineId;
257
+ }
258
+ async function waitForReleaseJob(client, machineId, jobId, timeoutMs) {
259
+ const deadline = Date.now() + (timeoutMs ?? 30 * 60 * 1000);
260
+ let last = await client.releaseJob({ machineId, action: "status", jobId });
261
+ while (Date.now() < deadline && !isTerminalReleaseStatus(last.status)) {
262
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
263
+ last = await client.releaseJob({ machineId, action: "status", jobId });
264
+ }
265
+ return last;
266
+ }
267
+ function isTerminalReleaseStatus(status) {
268
+ return status === "gate_failed" || status === "deploy_failed" || status === "verify_failed" || status === "update_failed" || status === "notify_failed" || status === "dogfood_failed" || status === "succeeded" || status === "failed";
269
+ }
270
+ function parseTail(flags) {
271
+ if (typeof flags.tail !== "string")
272
+ return undefined;
273
+ const value = Number(flags.tail);
274
+ if (!Number.isInteger(value) || value <= 0)
275
+ throw new CliError(`Invalid --tail: ${flags.tail}`, "INVALID_ARGUMENT");
276
+ return value;
277
+ }
278
+ function printReleaseOutput(type, data, flags) {
279
+ if (wantsJson(flags)) {
280
+ ok(type, data);
281
+ return;
282
+ }
283
+ if (type === "release.resolve") {
284
+ const resolved = data;
285
+ console.log(`Profile: ${resolved.profile.name}`);
286
+ console.log(`Release: ${resolved.releaseTarget.sessionId || "not found"} ${compactText(resolved.releaseTarget.name || "-", 64)}`);
287
+ if (resolved.reviewerTarget)
288
+ console.log(`Reviewer: ${resolved.reviewerTarget.sessionId} ${compactText(resolved.reviewerTarget.name, 64)}`);
289
+ if (resolved.dogfoodTarget)
290
+ console.log(`Dogfood: ${resolved.dogfoodTarget.sessionId} ${compactText(resolved.dogfoodTarget.name, 64)}`);
291
+ for (const warning of resolved.warnings)
292
+ console.log(`Warning: ${warning.code} ${warning.message}`);
293
+ for (const error of resolved.errors)
294
+ console.log(`Error: ${error.code} ${error.message}`);
295
+ return;
296
+ }
297
+ console.log(JSON.stringify(data, null, 2));
298
+ }
299
+ //# sourceMappingURL=release.js.map
@@ -199,7 +199,8 @@ export async function handleRemote({ domain, action, positional, flags }) {
199
199
  return false;
200
200
  }
201
201
  function printDaemonLifecycle(type, result, flags) {
202
- const success = result.accepted !== false && result.updateState?.status !== "failed";
202
+ const isReadOnlyAction = result.action === "status" || result.action === "logs";
203
+ const success = result.accepted !== false && (isReadOnlyAction || result.updateState?.status !== "failed");
203
204
  if (wantsJson(flags)) {
204
205
  ok(type, result, { requestId: result.requestId, machineId: result.machineId }, success);
205
206
  return;
@@ -255,7 +256,10 @@ async function waitForDaemonLifecycle(client, machineId, requestId, action, opti
255
256
  catch (error) {
256
257
  lastError = error;
257
258
  const code = errorCode(error);
258
- if (code !== "MACHINE_OFFLINE" && code !== "COMMAND_TIMEOUT" && code !== "RELAY_CONNECT_TIMEOUT")
259
+ if (code !== "MACHINE_OFFLINE" &&
260
+ code !== "COMMAND_TIMEOUT" &&
261
+ code !== "RELAY_CONNECT_TIMEOUT" &&
262
+ code !== "RELAY_WEBSOCKET_ERROR")
259
263
  throw error;
260
264
  }
261
265
  await sleep(1000);