@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.
- package/apps/cli/dist/commands/app.js +2 -0
- package/apps/cli/dist/commands/lib/args.js +13 -0
- package/apps/cli/dist/commands/lib/paths.d.ts +1 -0
- package/apps/cli/dist/commands/lib/paths.js +1 -0
- package/apps/cli/dist/commands/lib/session-view.d.ts +3 -0
- package/apps/cli/dist/commands/lib/session-view.js +3 -0
- package/apps/cli/dist/commands/lib/types.d.ts +2 -0
- package/apps/cli/dist/commands/lib/usage.js +43 -10
- package/apps/cli/dist/commands/release.d.ts +2 -0
- package/apps/cli/dist/commands/release.js +299 -0
- package/apps/cli/dist/commands/remote.js +6 -2
- package/apps/cli/dist/commands/session.js +59 -0
- package/apps/cli/dist/commands/turn.js +9 -2
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/paths.d.ts +1 -0
- package/apps/daemon/dist/paths.js +1 -0
- package/apps/daemon/dist/relay/connection.js +5 -0
- package/apps/daemon/dist/relay/lifecycle.js +74 -5
- package/apps/daemon/dist/relay/register.js +1 -0
- package/apps/daemon/dist/relay/release.d.ts +7 -0
- package/apps/daemon/dist/relay/release.js +359 -0
- package/apps/daemon/dist/types.d.ts +2 -0
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/controller-handlers.js +61 -0
- package/apps/relay/dist/machine-handlers.js +12 -0
- package/package.json +1 -1
- package/packages/client/dist/client.d.ts +6 -1
- package/packages/client/dist/client.js +53 -2
- 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 +25 -1
- package/packages/shared/dist/args.js +16 -4
- package/packages/shared/dist/index.d.ts +1 -0
- package/packages/shared/dist/index.js +1 -0
- package/packages/shared/dist/protocol-schemas.d.ts +107 -0
- package/packages/shared/dist/protocol-schemas.js +57 -0
- package/packages/shared/dist/protocol-types.d.ts +58 -0
- package/packages/shared/dist/protocol.d.ts +33 -2
- package/packages/shared/dist/protocol.js +37 -1
- package/packages/shared/dist/session-tags.d.ts +12 -0
- 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"]);
|
|
@@ -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) {
|
|
@@ -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
|
|
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,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
|
|
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" &&
|
|
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);
|