@google/jules-fleet 0.0.1-experimental.0
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 +205 -0
- package/dist/analyze/formatting.d.ts +19 -0
- package/dist/analyze/goals.d.ts +18 -0
- package/dist/analyze/handler.d.ts +23 -0
- package/dist/analyze/index.d.ts +8 -0
- package/dist/analyze/milestone.d.ts +43 -0
- package/dist/analyze/prompt.d.ts +10 -0
- package/dist/analyze/spec.d.ts +54 -0
- package/dist/analyze/triage-prompt.d.ts +16 -0
- package/dist/cli/analyze.command.d.ts +24 -0
- package/dist/cli/analyze.command.mjs +1015 -0
- package/dist/cli/commands.json +1 -0
- package/dist/cli/configure.command.d.ts +21 -0
- package/dist/cli/configure.command.mjs +623 -0
- package/dist/cli/dispatch.command.d.ts +16 -0
- package/dist/cli/dispatch.command.mjs +777 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.mjs +40 -0
- package/dist/cli/init.command.d.ts +38 -0
- package/dist/cli/init.command.mjs +1287 -0
- package/dist/cli/merge.command.d.ts +36 -0
- package/dist/cli/merge.command.mjs +859 -0
- package/dist/cli/signal.command.d.ts +2 -0
- package/dist/cli/signal.command.mjs +288 -0
- package/dist/configure/handler.d.ts +19 -0
- package/dist/configure/index.d.ts +4 -0
- package/dist/configure/labels.d.ts +6 -0
- package/dist/configure/spec.d.ts +49 -0
- package/dist/dispatch/events.d.ts +12 -0
- package/dist/dispatch/handler.d.ts +21 -0
- package/dist/dispatch/index.d.ts +5 -0
- package/dist/dispatch/spec.d.ts +47 -0
- package/dist/dispatch/status.d.ts +24 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +2105 -0
- package/dist/init/handler.d.ts +22 -0
- package/dist/init/index.d.ts +4 -0
- package/dist/init/ops/commit-files.d.ts +10 -0
- package/dist/init/ops/create-branch.d.ts +16 -0
- package/dist/init/ops/create-pr.d.ts +15 -0
- package/dist/init/ops/pr-body.d.ts +5 -0
- package/dist/init/ops/upload-secrets.d.ts +11 -0
- package/dist/init/spec.d.ts +50 -0
- package/dist/init/templates/analyze.d.ts +2 -0
- package/dist/init/templates/dispatch.d.ts +2 -0
- package/dist/init/templates/example-goal.d.ts +5 -0
- package/dist/init/templates/merge.d.ts +2 -0
- package/dist/init/templates/types.d.ts +6 -0
- package/dist/init/templates.d.ts +10 -0
- package/dist/init/types.d.ts +19 -0
- package/dist/init/wizard/headless.d.ts +8 -0
- package/dist/init/wizard/index.d.ts +3 -0
- package/dist/init/wizard/interactive.d.ts +9 -0
- package/dist/init/wizard/types.d.ts +22 -0
- package/dist/merge/handler.d.ts +21 -0
- package/dist/merge/index.d.ts +5 -0
- package/dist/merge/ops/index.d.ts +4 -0
- package/dist/merge/ops/redispatch.d.ts +8 -0
- package/dist/merge/ops/squash-merge.d.ts +8 -0
- package/dist/merge/ops/update-branch.d.ts +11 -0
- package/dist/merge/ops/wait-for-ci.d.ts +7 -0
- package/dist/merge/select/by-fleet-run.d.ts +8 -0
- package/dist/merge/select/by-label.d.ts +7 -0
- package/dist/merge/select/index.d.ts +2 -0
- package/dist/merge/spec.d.ts +99 -0
- package/dist/shared/auth/cache-plugin.d.ts +9 -0
- package/dist/shared/auth/git.d.ts +22 -0
- package/dist/shared/auth/index.d.ts +4 -0
- package/dist/shared/auth/octokit.d.ts +11 -0
- package/dist/shared/auth/resolve-key.d.ts +11 -0
- package/dist/shared/events/analyze.d.ts +37 -0
- package/dist/shared/events/configure.d.ts +21 -0
- package/dist/shared/events/dispatch.d.ts +26 -0
- package/dist/shared/events/error.d.ts +7 -0
- package/dist/shared/events/index.d.ts +16 -0
- package/dist/shared/events/init.d.ts +49 -0
- package/dist/shared/events/merge.d.ts +72 -0
- package/dist/shared/events.d.ts +1 -0
- package/dist/shared/index.d.ts +6 -0
- package/dist/shared/result/create-result-schemas.d.ts +72 -0
- package/dist/shared/result/fail.d.ts +10 -0
- package/dist/shared/result/index.d.ts +3 -0
- package/dist/shared/result/ok.d.ts +5 -0
- package/dist/shared/schemas/check-run.d.ts +16 -0
- package/dist/shared/schemas/index.d.ts +4 -0
- package/dist/shared/schemas/label.d.ts +16 -0
- package/dist/shared/schemas/pr.d.ts +19 -0
- package/dist/shared/schemas/repo-info.d.ts +16 -0
- package/dist/shared/session-dispatcher.d.ts +18 -0
- package/dist/shared/ui/assert-never.d.ts +13 -0
- package/dist/shared/ui/index.d.ts +18 -0
- package/dist/shared/ui/interactive.d.ts +19 -0
- package/dist/shared/ui/plain.d.ts +16 -0
- package/dist/shared/ui/render/analyze.d.ts +4 -0
- package/dist/shared/ui/render/configure.d.ts +4 -0
- package/dist/shared/ui/render/dispatch.d.ts +4 -0
- package/dist/shared/ui/render/error.d.ts +4 -0
- package/dist/shared/ui/render/init.d.ts +4 -0
- package/dist/shared/ui/render/merge.d.ts +4 -0
- package/dist/shared/ui/session-url.d.ts +13 -0
- package/dist/shared/ui/spec.d.ts +30 -0
- package/dist/signal/handler.d.ts +17 -0
- package/dist/signal/index.d.ts +3 -0
- package/dist/signal/spec.d.ts +60 -0
- package/package.json +76 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/cli/merge.command.ts
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
|
|
7
|
+
// src/merge/spec.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var MergeMode = z.enum(["label", "fleet-run"]);
|
|
10
|
+
var MergeInputSchema = z.object({
|
|
11
|
+
mode: MergeMode.default("label"),
|
|
12
|
+
runId: z.string().optional(),
|
|
13
|
+
baseBranch: z.string().default("main"),
|
|
14
|
+
admin: z.boolean().default(false),
|
|
15
|
+
maxCIWaitSeconds: z.number().positive().default(600),
|
|
16
|
+
maxRetries: z.number().nonnegative().default(2),
|
|
17
|
+
reDispatch: z.boolean().default(false),
|
|
18
|
+
pollTimeoutSeconds: z.number().positive().default(900),
|
|
19
|
+
owner: z.string().min(1),
|
|
20
|
+
repo: z.string().min(1)
|
|
21
|
+
}).refine((d) => d.mode !== "fleet-run" || !!d.runId, {
|
|
22
|
+
message: "--run-id is required when mode is fleet-run",
|
|
23
|
+
path: ["runId"]
|
|
24
|
+
});
|
|
25
|
+
var MergeErrorCode = z.enum([
|
|
26
|
+
"NO_PRS_FOUND",
|
|
27
|
+
"CI_FAILED",
|
|
28
|
+
"CI_TIMEOUT",
|
|
29
|
+
"MERGE_FAILED",
|
|
30
|
+
"CONFLICT_RETRIES_EXHAUSTED",
|
|
31
|
+
"REDISPATCH_TIMEOUT",
|
|
32
|
+
"GITHUB_API_ERROR",
|
|
33
|
+
"UNKNOWN_ERROR"
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
// src/shared/result/create-result-schemas.ts
|
|
37
|
+
import { z as z2 } from "zod";
|
|
38
|
+
// src/shared/result/ok.ts
|
|
39
|
+
function ok(data) {
|
|
40
|
+
return { success: true, data };
|
|
41
|
+
}
|
|
42
|
+
// src/shared/result/fail.ts
|
|
43
|
+
function fail(code, message, recoverable = false, suggestion) {
|
|
44
|
+
return {
|
|
45
|
+
success: false,
|
|
46
|
+
error: { code, message, recoverable, suggestion }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// src/merge/select/by-label.ts
|
|
50
|
+
async function selectByLabel(octokit, owner, repo, baseBranch) {
|
|
51
|
+
const { data: pulls } = await octokit.rest.pulls.list({
|
|
52
|
+
owner,
|
|
53
|
+
repo,
|
|
54
|
+
state: "open",
|
|
55
|
+
base: baseBranch,
|
|
56
|
+
per_page: 100
|
|
57
|
+
});
|
|
58
|
+
const labeledPRs = pulls.filter((pr) => pr.labels.some((label) => label.name === "fleet-merge-ready")).sort((a, b) => a.number - b.number).map((pr) => ({
|
|
59
|
+
number: pr.number,
|
|
60
|
+
headRef: pr.head.ref,
|
|
61
|
+
headSha: pr.head.sha,
|
|
62
|
+
body: pr.body
|
|
63
|
+
}));
|
|
64
|
+
return labeledPRs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/merge/select/by-fleet-run.ts
|
|
68
|
+
var FLEET_RUN_MARKER_PREFIX = "<!-- fleet-run:";
|
|
69
|
+
async function selectByFleetRun(octokit, owner, repo, baseBranch, runId) {
|
|
70
|
+
const { data: pulls } = await octokit.rest.pulls.list({
|
|
71
|
+
owner,
|
|
72
|
+
repo,
|
|
73
|
+
state: "open",
|
|
74
|
+
base: baseBranch,
|
|
75
|
+
per_page: 100
|
|
76
|
+
});
|
|
77
|
+
const marker = `${FLEET_RUN_MARKER_PREFIX} ${runId} -->`;
|
|
78
|
+
const matchingPRs = pulls.filter((pr) => pr.body?.includes(marker)).sort((a, b) => a.number - b.number).map((pr) => ({
|
|
79
|
+
number: pr.number,
|
|
80
|
+
headRef: pr.head.ref,
|
|
81
|
+
headSha: pr.head.sha,
|
|
82
|
+
body: pr.body
|
|
83
|
+
}));
|
|
84
|
+
return matchingPRs;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/merge/ops/update-branch.ts
|
|
88
|
+
async function updateBranch(octokit, owner, repo, prNumber, emit) {
|
|
89
|
+
try {
|
|
90
|
+
emit({ type: "merge:branch:updating", prNumber });
|
|
91
|
+
await octokit.rest.pulls.updateBranch({
|
|
92
|
+
owner,
|
|
93
|
+
repo,
|
|
94
|
+
pull_number: prNumber
|
|
95
|
+
});
|
|
96
|
+
emit({ type: "merge:branch:updated", prNumber });
|
|
97
|
+
return { ok: true, conflict: false };
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
100
|
+
if (status === 422) {
|
|
101
|
+
emit({ type: "merge:conflict:detected", prNumber });
|
|
102
|
+
return { ok: false, conflict: true };
|
|
103
|
+
}
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
return { ok: false, conflict: false, error: message };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/merge/ops/wait-for-ci.ts
|
|
110
|
+
async function waitForCI(octokit, owner, repo, prNumber, maxWaitMs, emit, sleep) {
|
|
111
|
+
const { data: prData } = await octokit.rest.pulls.get({
|
|
112
|
+
owner,
|
|
113
|
+
repo,
|
|
114
|
+
pull_number: prNumber
|
|
115
|
+
});
|
|
116
|
+
const headSha = prData.head.sha;
|
|
117
|
+
emit({ type: "merge:ci:waiting", prNumber });
|
|
118
|
+
const start = Date.now();
|
|
119
|
+
while (Date.now() - start < maxWaitMs) {
|
|
120
|
+
const { data } = await octokit.rest.checks.listForRef({
|
|
121
|
+
owner,
|
|
122
|
+
repo,
|
|
123
|
+
ref: headSha
|
|
124
|
+
});
|
|
125
|
+
if (data.check_runs.length === 0) {
|
|
126
|
+
emit({ type: "merge:ci:none", prNumber });
|
|
127
|
+
return "none";
|
|
128
|
+
}
|
|
129
|
+
for (const run of data.check_runs) {
|
|
130
|
+
if (run.status === "completed") {
|
|
131
|
+
const passed = run.conclusion === "success" || run.conclusion === "skipped";
|
|
132
|
+
const durationMs = run.started_at && run.completed_at ? new Date(run.completed_at).getTime() - new Date(run.started_at).getTime() : undefined;
|
|
133
|
+
emit({
|
|
134
|
+
type: "merge:ci:check",
|
|
135
|
+
name: run.name,
|
|
136
|
+
status: passed ? "pass" : "fail",
|
|
137
|
+
duration: durationMs ? Math.round(durationMs / 1000) : undefined
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const allComplete = data.check_runs.every((run) => run.status === "completed");
|
|
142
|
+
const allPassed = data.check_runs.every((run) => run.conclusion === "success" || run.conclusion === "skipped");
|
|
143
|
+
if (allComplete && allPassed) {
|
|
144
|
+
emit({ type: "merge:ci:passed", prNumber });
|
|
145
|
+
return "pass";
|
|
146
|
+
}
|
|
147
|
+
if (allComplete && !allPassed) {
|
|
148
|
+
emit({ type: "merge:ci:failed", prNumber });
|
|
149
|
+
return "fail";
|
|
150
|
+
}
|
|
151
|
+
await sleep(30000);
|
|
152
|
+
}
|
|
153
|
+
emit({ type: "merge:ci:timeout", prNumber });
|
|
154
|
+
return "timeout";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/merge/ops/squash-merge.ts
|
|
158
|
+
async function squashMerge(octokit, owner, repo, prNumber) {
|
|
159
|
+
try {
|
|
160
|
+
await octokit.rest.pulls.merge({
|
|
161
|
+
owner,
|
|
162
|
+
repo,
|
|
163
|
+
pull_number: prNumber,
|
|
164
|
+
merge_method: "squash"
|
|
165
|
+
});
|
|
166
|
+
return { ok: true };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
return { ok: false, error: message };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/merge/ops/redispatch.ts
|
|
174
|
+
async function redispatch(octokit, owner, repo, oldPr, baseBranch, pollTimeoutSeconds, emit, sleep) {
|
|
175
|
+
emit({ type: "merge:redispatch:start", oldPr: oldPr.number });
|
|
176
|
+
try {
|
|
177
|
+
await octokit.rest.pulls.update({
|
|
178
|
+
owner,
|
|
179
|
+
repo,
|
|
180
|
+
pull_number: oldPr.number,
|
|
181
|
+
state: "closed",
|
|
182
|
+
body: `${oldPr.body ?? ""}
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
⚠️ Closed by fleet-merge: merge conflict detected. Task re-dispatched.`
|
|
186
|
+
});
|
|
187
|
+
} catch {}
|
|
188
|
+
try {
|
|
189
|
+
const { jules } = await import("@google/jules-sdk");
|
|
190
|
+
const session = await jules.session({
|
|
191
|
+
prompt: oldPr.body ?? "",
|
|
192
|
+
source: {
|
|
193
|
+
github: `${owner}/${repo}`,
|
|
194
|
+
baseBranch
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
emit({ type: "merge:redispatch:waiting", oldPr: oldPr.number });
|
|
198
|
+
const start = Date.now();
|
|
199
|
+
const timeoutMs = pollTimeoutSeconds * 1000;
|
|
200
|
+
const pollIntervalMs = 30000;
|
|
201
|
+
while (Date.now() - start < timeoutMs) {
|
|
202
|
+
await sleep(pollIntervalMs);
|
|
203
|
+
const { data: pulls } = await octokit.rest.pulls.list({
|
|
204
|
+
owner,
|
|
205
|
+
repo,
|
|
206
|
+
state: "open",
|
|
207
|
+
base: baseBranch,
|
|
208
|
+
per_page: 100
|
|
209
|
+
});
|
|
210
|
+
const newPr = pulls.find((pr) => pr.head.ref.includes(session.id) || pr.body?.includes(session.id));
|
|
211
|
+
if (newPr) {
|
|
212
|
+
const result = {
|
|
213
|
+
number: newPr.number,
|
|
214
|
+
headRef: newPr.head.ref,
|
|
215
|
+
headSha: newPr.head.sha,
|
|
216
|
+
body: newPr.body
|
|
217
|
+
};
|
|
218
|
+
emit({
|
|
219
|
+
type: "merge:redispatch:done",
|
|
220
|
+
oldPr: oldPr.number,
|
|
221
|
+
newPr: newPr.number
|
|
222
|
+
});
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
emit({
|
|
228
|
+
type: "error",
|
|
229
|
+
code: "REDISPATCH_FAILED",
|
|
230
|
+
message: `Re-dispatch failed: ${error instanceof Error ? error.message : error}`
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/merge/handler.ts
|
|
237
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
238
|
+
|
|
239
|
+
class MergeHandler {
|
|
240
|
+
octokit;
|
|
241
|
+
emit;
|
|
242
|
+
sleep;
|
|
243
|
+
constructor(deps) {
|
|
244
|
+
this.octokit = deps.octokit;
|
|
245
|
+
this.emit = deps.emit ?? (() => {});
|
|
246
|
+
this.sleep = deps.sleep ?? defaultSleep;
|
|
247
|
+
}
|
|
248
|
+
async execute(input) {
|
|
249
|
+
try {
|
|
250
|
+
const prs = await this.selectPRs(input);
|
|
251
|
+
if (prs.length === 0) {
|
|
252
|
+
this.emit({ type: "merge:no-prs" });
|
|
253
|
+
return ok({ merged: [], skipped: [], redispatched: [] });
|
|
254
|
+
}
|
|
255
|
+
this.emit({
|
|
256
|
+
type: "merge:start",
|
|
257
|
+
owner: input.owner,
|
|
258
|
+
repo: input.repo,
|
|
259
|
+
mode: input.mode,
|
|
260
|
+
prCount: prs.length
|
|
261
|
+
});
|
|
262
|
+
const merged = [];
|
|
263
|
+
const skipped = [];
|
|
264
|
+
const redispatched = [];
|
|
265
|
+
for (const pr of prs) {
|
|
266
|
+
let currentPr = pr;
|
|
267
|
+
let retryCount = 0;
|
|
268
|
+
let prMerged = false;
|
|
269
|
+
while (!prMerged) {
|
|
270
|
+
this.emit({
|
|
271
|
+
type: "merge:pr:processing",
|
|
272
|
+
number: currentPr.number,
|
|
273
|
+
title: currentPr.body?.split(`
|
|
274
|
+
`)[0] ?? `PR #${currentPr.number}`,
|
|
275
|
+
retry: retryCount > 0 ? retryCount : undefined
|
|
276
|
+
});
|
|
277
|
+
if (prs.indexOf(pr) > 0 || retryCount > 0) {
|
|
278
|
+
const updateResult = await updateBranch(this.octokit, input.owner, input.repo, currentPr.number, this.emit);
|
|
279
|
+
if (!updateResult.ok && updateResult.conflict) {
|
|
280
|
+
if (!input.reDispatch) {
|
|
281
|
+
return fail("CONFLICT_RETRIES_EXHAUSTED", `Merge conflict detected for PR #${currentPr.number}. Use --re-dispatch to automatically retry.`, false, `Review PR: https://github.com/${input.owner}/${input.repo}/pull/${currentPr.number}`);
|
|
282
|
+
}
|
|
283
|
+
if (retryCount >= input.maxRetries) {
|
|
284
|
+
return fail("CONFLICT_RETRIES_EXHAUSTED", `Conflict persists for PR #${currentPr.number} after ${input.maxRetries} retries. Human intervention required.`, false, `Review PR: https://github.com/${input.owner}/${input.repo}/pull/${currentPr.number}`);
|
|
285
|
+
}
|
|
286
|
+
const newPr = await redispatch(this.octokit, input.owner, input.repo, currentPr, input.baseBranch, input.pollTimeoutSeconds, this.emit, this.sleep);
|
|
287
|
+
if (!newPr) {
|
|
288
|
+
return fail("REDISPATCH_TIMEOUT", `Timed out waiting for re-dispatched PR for #${currentPr.number}.`, true);
|
|
289
|
+
}
|
|
290
|
+
redispatched.push({
|
|
291
|
+
oldPr: currentPr.number,
|
|
292
|
+
newPr: newPr.number
|
|
293
|
+
});
|
|
294
|
+
currentPr = newPr;
|
|
295
|
+
retryCount++;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!updateResult.ok && !updateResult.conflict) {
|
|
299
|
+
return fail("GITHUB_API_ERROR", `Failed to update branch for PR #${currentPr.number}: ${updateResult.error}`, true);
|
|
300
|
+
}
|
|
301
|
+
await this.sleep(5000);
|
|
302
|
+
}
|
|
303
|
+
const ciResult = await waitForCI(this.octokit, input.owner, input.repo, currentPr.number, input.maxCIWaitSeconds * 1000, this.emit, this.sleep);
|
|
304
|
+
if (ciResult === "none") {} else if (ciResult === "fail") {
|
|
305
|
+
this.emit({
|
|
306
|
+
type: "merge:pr:skipped",
|
|
307
|
+
prNumber: currentPr.number,
|
|
308
|
+
reason: "CI failure"
|
|
309
|
+
});
|
|
310
|
+
skipped.push(currentPr.number);
|
|
311
|
+
break;
|
|
312
|
+
} else if (ciResult === "timeout") {
|
|
313
|
+
this.emit({
|
|
314
|
+
type: "merge:pr:skipped",
|
|
315
|
+
prNumber: currentPr.number,
|
|
316
|
+
reason: "CI timeout"
|
|
317
|
+
});
|
|
318
|
+
skipped.push(currentPr.number);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
this.emit({ type: "merge:pr:merging", prNumber: currentPr.number });
|
|
322
|
+
const mergeResult = await squashMerge(this.octokit, input.owner, input.repo, currentPr.number);
|
|
323
|
+
if (!mergeResult.ok) {
|
|
324
|
+
return fail("MERGE_FAILED", `Failed to merge PR #${currentPr.number}: ${mergeResult.error}`, false);
|
|
325
|
+
}
|
|
326
|
+
this.emit({ type: "merge:pr:merged", prNumber: currentPr.number });
|
|
327
|
+
merged.push(currentPr.number);
|
|
328
|
+
prMerged = true;
|
|
329
|
+
}
|
|
330
|
+
await this.sleep(5000);
|
|
331
|
+
}
|
|
332
|
+
this.emit({ type: "merge:done", merged, skipped, redispatched });
|
|
333
|
+
return ok({ merged, skipped, redispatched });
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async selectPRs(input) {
|
|
339
|
+
if (input.mode === "fleet-run") {
|
|
340
|
+
return selectByFleetRun(this.octokit, input.owner, input.repo, input.baseBranch, input.runId);
|
|
341
|
+
}
|
|
342
|
+
return selectByLabel(this.octokit, input.owner, input.repo, input.baseBranch);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/shared/auth/octokit.ts
|
|
347
|
+
import { Octokit } from "octokit";
|
|
348
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
349
|
+
|
|
350
|
+
// src/shared/auth/cache-plugin.ts
|
|
351
|
+
function cachePlugin(octokit) {
|
|
352
|
+
const cache = new Map;
|
|
353
|
+
octokit.hook.wrap("request", async (request, options) => {
|
|
354
|
+
const key = `${options.method} ${options.url}`;
|
|
355
|
+
const cached = cache.get(key);
|
|
356
|
+
if (cached) {
|
|
357
|
+
options.headers = {
|
|
358
|
+
...options.headers,
|
|
359
|
+
"if-none-match": cached.etag
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const response = await request(options);
|
|
364
|
+
const etag = response.headers.etag;
|
|
365
|
+
if (etag) {
|
|
366
|
+
cache.set(key, { etag, data: response.data });
|
|
367
|
+
}
|
|
368
|
+
return response;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (error.status === 304 && cached) {
|
|
371
|
+
return { ...error.response, data: cached.data, status: 200 };
|
|
372
|
+
}
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/shared/auth/resolve-key.ts
|
|
379
|
+
function resolvePrivateKey(base64Value, rawValue) {
|
|
380
|
+
if (base64Value) {
|
|
381
|
+
return Buffer.from(base64Value, "base64").toString("utf-8");
|
|
382
|
+
}
|
|
383
|
+
if (rawValue) {
|
|
384
|
+
return rawValue.replace(/\\n/g, `
|
|
385
|
+
`);
|
|
386
|
+
}
|
|
387
|
+
throw new Error("No private key provided. Set GITHUB_APP_PRIVATE_KEY_BASE64 (recommended) or GITHUB_APP_PRIVATE_KEY.");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/shared/auth/octokit.ts
|
|
391
|
+
var CachedOctokit = Octokit.plugin(cachePlugin);
|
|
392
|
+
function getAuthOptions() {
|
|
393
|
+
const appId = process.env.GITHUB_APP_ID;
|
|
394
|
+
const privateKeyBase64 = process.env.GITHUB_APP_PRIVATE_KEY_BASE64;
|
|
395
|
+
const privateKeyRaw = process.env.GITHUB_APP_PRIVATE_KEY;
|
|
396
|
+
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
|
|
397
|
+
if (appId && (privateKeyBase64 || privateKeyRaw) && installationId) {
|
|
398
|
+
return {
|
|
399
|
+
authStrategy: createAppAuth,
|
|
400
|
+
auth: {
|
|
401
|
+
appId,
|
|
402
|
+
privateKey: resolvePrivateKey(privateKeyBase64, privateKeyRaw),
|
|
403
|
+
installationId: Number(installationId)
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const token = process.env.GITHUB_TOKEN;
|
|
408
|
+
if (token) {
|
|
409
|
+
return { auth: token };
|
|
410
|
+
}
|
|
411
|
+
throw new Error("GitHub auth not configured. Set GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY + GITHUB_APP_INSTALLATION_ID for App auth, or GITHUB_TOKEN for PAT auth.");
|
|
412
|
+
}
|
|
413
|
+
function createFleetOctokit() {
|
|
414
|
+
return new CachedOctokit(getAuthOptions());
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/shared/auth/git.ts
|
|
418
|
+
import { exec } from "child_process";
|
|
419
|
+
import { promisify } from "util";
|
|
420
|
+
var execAsync = promisify(exec);
|
|
421
|
+
async function getGitRepoInfo(remoteName = "origin") {
|
|
422
|
+
const ghRepo = process.env.GITHUB_REPOSITORY;
|
|
423
|
+
if (ghRepo) {
|
|
424
|
+
const [owner, repo] = ghRepo.split("/");
|
|
425
|
+
return { owner, repo, fullName: ghRepo };
|
|
426
|
+
}
|
|
427
|
+
const { stdout } = await execAsync(`git remote get-url ${remoteName}`);
|
|
428
|
+
return parseGitRemoteUrl(stdout.trim());
|
|
429
|
+
}
|
|
430
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
431
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(\.git)?$/);
|
|
432
|
+
if (sshMatch) {
|
|
433
|
+
const [, owner, repo] = sshMatch;
|
|
434
|
+
return {
|
|
435
|
+
owner,
|
|
436
|
+
repo: repo.replace(/\.git$/, ""),
|
|
437
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const httpsMatch = remoteUrl.match(/https?:\/\/github\.com\/([^/]+)\/(.+?)(\.git)?$/);
|
|
441
|
+
if (httpsMatch) {
|
|
442
|
+
const [, owner, repo] = httpsMatch;
|
|
443
|
+
return {
|
|
444
|
+
owner,
|
|
445
|
+
repo: repo.replace(/\.git$/, ""),
|
|
446
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
throw new Error(`Unable to parse git remote URL: ${remoteUrl}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/shared/ui/interactive.ts
|
|
453
|
+
import * as p from "@clack/prompts";
|
|
454
|
+
|
|
455
|
+
// src/shared/ui/render/init.ts
|
|
456
|
+
function renderInitEvent(event, ctx) {
|
|
457
|
+
switch (event.type) {
|
|
458
|
+
case "init:start":
|
|
459
|
+
ctx.info(`Initializing fleet for ${event.owner}/${event.repo}`);
|
|
460
|
+
break;
|
|
461
|
+
case "init:branch:creating":
|
|
462
|
+
ctx.startSpinner(`Creating branch ${event.name} from ${event.base}`);
|
|
463
|
+
break;
|
|
464
|
+
case "init:branch:created":
|
|
465
|
+
ctx.stopSpinner(`Branch ${event.name} created`);
|
|
466
|
+
break;
|
|
467
|
+
case "init:file:committed":
|
|
468
|
+
ctx.info(` ✓ ${event.path}`);
|
|
469
|
+
break;
|
|
470
|
+
case "init:file:skipped":
|
|
471
|
+
ctx.warn(` ⊘ ${event.path} — ${event.reason}`);
|
|
472
|
+
break;
|
|
473
|
+
case "init:pr:creating":
|
|
474
|
+
ctx.startSpinner("Creating pull request…");
|
|
475
|
+
break;
|
|
476
|
+
case "init:pr:created":
|
|
477
|
+
ctx.stopSpinner(`PR #${event.number} created`);
|
|
478
|
+
ctx.info(` ${event.url}`);
|
|
479
|
+
break;
|
|
480
|
+
case "init:done":
|
|
481
|
+
ctx.success(`Fleet initialized — PR: ${event.prUrl}`);
|
|
482
|
+
break;
|
|
483
|
+
case "init:auth:detected":
|
|
484
|
+
ctx.success(`Auth: ${event.method === "token" ? "GITHUB_TOKEN" : "GitHub App"}`);
|
|
485
|
+
break;
|
|
486
|
+
case "init:secret:uploading":
|
|
487
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
488
|
+
break;
|
|
489
|
+
case "init:secret:uploaded":
|
|
490
|
+
ctx.stopSpinner(`Secret ${event.name} saved`);
|
|
491
|
+
break;
|
|
492
|
+
case "init:secret:skipped":
|
|
493
|
+
ctx.warn(` ⊘ ${event.name} — ${event.reason}`);
|
|
494
|
+
break;
|
|
495
|
+
case "init:dry-run":
|
|
496
|
+
ctx.info("Would create:");
|
|
497
|
+
event.files.forEach((f) => ctx.message(` ${f}`));
|
|
498
|
+
break;
|
|
499
|
+
case "init:already-initialized":
|
|
500
|
+
ctx.warn("Repository is already initialized");
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/shared/ui/render/configure.ts
|
|
506
|
+
function renderConfigureEvent(event, ctx) {
|
|
507
|
+
switch (event.type) {
|
|
508
|
+
case "configure:start":
|
|
509
|
+
ctx.info(`Configuring ${event.resource} for ${event.owner}/${event.repo}`);
|
|
510
|
+
break;
|
|
511
|
+
case "configure:label:created":
|
|
512
|
+
ctx.info(` ✓ Label "${event.name}" created`);
|
|
513
|
+
break;
|
|
514
|
+
case "configure:label:exists":
|
|
515
|
+
ctx.warn(` ⊘ Label "${event.name}" already exists`);
|
|
516
|
+
break;
|
|
517
|
+
case "configure:secret:uploading":
|
|
518
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
519
|
+
break;
|
|
520
|
+
case "configure:secret:uploaded":
|
|
521
|
+
ctx.stopSpinner(`Secret ${event.name} uploaded`);
|
|
522
|
+
break;
|
|
523
|
+
case "configure:done":
|
|
524
|
+
ctx.success("Configuration complete");
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/shared/ui/session-url.ts
|
|
530
|
+
var JULES_BASE_URL = "https://jules.google.com";
|
|
531
|
+
function sessionUrl(sessionId) {
|
|
532
|
+
return `${JULES_BASE_URL}/sessions/${sessionId}`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/shared/ui/render/analyze.ts
|
|
536
|
+
function renderAnalyzeEvent(event, ctx) {
|
|
537
|
+
switch (event.type) {
|
|
538
|
+
case "analyze:start":
|
|
539
|
+
ctx.info(`Analyzing ${event.goalCount} goal(s) for ${event.owner}/${event.repo}`);
|
|
540
|
+
break;
|
|
541
|
+
case "analyze:goal:start":
|
|
542
|
+
if (event.total > 1) {
|
|
543
|
+
ctx.step(`[${event.index}/${event.total}] ${event.file}`);
|
|
544
|
+
} else {
|
|
545
|
+
ctx.step(event.file);
|
|
546
|
+
}
|
|
547
|
+
if (event.milestone)
|
|
548
|
+
ctx.info(` Milestone: ${event.milestone}`);
|
|
549
|
+
break;
|
|
550
|
+
case "analyze:milestone:resolved":
|
|
551
|
+
ctx.info(` Milestone "${event.title}" (#${event.id})`);
|
|
552
|
+
break;
|
|
553
|
+
case "analyze:context:fetched":
|
|
554
|
+
ctx.info(` Context: ${event.openIssues} open, ${event.closedIssues} closed, ${event.prs} PRs`);
|
|
555
|
+
break;
|
|
556
|
+
case "analyze:session:dispatching":
|
|
557
|
+
ctx.startSpinner(`Dispatching session for ${event.goal}…`);
|
|
558
|
+
break;
|
|
559
|
+
case "analyze:session:started":
|
|
560
|
+
ctx.stopSpinner(`Session started: ${event.id}`);
|
|
561
|
+
ctx.info(` ${sessionUrl(event.id)}`);
|
|
562
|
+
break;
|
|
563
|
+
case "analyze:session:failed":
|
|
564
|
+
ctx.stopSpinner();
|
|
565
|
+
ctx.error(` Failed: ${event.error}`);
|
|
566
|
+
break;
|
|
567
|
+
case "analyze:done":
|
|
568
|
+
ctx.success(`Analysis complete — ${event.sessionsStarted} session(s) from ${event.goalsProcessed} goal(s)`);
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/shared/ui/render/dispatch.ts
|
|
574
|
+
function renderDispatchEvent(event, ctx) {
|
|
575
|
+
switch (event.type) {
|
|
576
|
+
case "dispatch:start":
|
|
577
|
+
ctx.info(`Dispatching from milestone ${event.milestone}`);
|
|
578
|
+
break;
|
|
579
|
+
case "dispatch:scanning":
|
|
580
|
+
ctx.startSpinner("Scanning for fleet issues…");
|
|
581
|
+
break;
|
|
582
|
+
case "dispatch:found":
|
|
583
|
+
ctx.stopSpinner(`Found ${event.count} undispatched issue(s)`);
|
|
584
|
+
break;
|
|
585
|
+
case "dispatch:issue:dispatching":
|
|
586
|
+
ctx.startSpinner(`#${event.number}: ${event.title}`);
|
|
587
|
+
break;
|
|
588
|
+
case "dispatch:issue:dispatched":
|
|
589
|
+
ctx.stopSpinner(`#${event.number} → session ${event.sessionId}`);
|
|
590
|
+
ctx.info(` ${sessionUrl(event.sessionId)}`);
|
|
591
|
+
break;
|
|
592
|
+
case "dispatch:issue:skipped":
|
|
593
|
+
ctx.warn(` ⊘ #${event.number}: ${event.reason}`);
|
|
594
|
+
break;
|
|
595
|
+
case "dispatch:done":
|
|
596
|
+
ctx.success(`Dispatch complete — ${event.dispatched} dispatched, ${event.skipped} skipped`);
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/shared/ui/render/merge.ts
|
|
602
|
+
function renderMergeEvent(event, ctx) {
|
|
603
|
+
switch (event.type) {
|
|
604
|
+
case "merge:start":
|
|
605
|
+
ctx.info(`Merging ${event.prCount} PR(s) in ${event.owner}/${event.repo} [${event.mode}]`);
|
|
606
|
+
break;
|
|
607
|
+
case "merge:no-prs":
|
|
608
|
+
ctx.info("No PRs ready to merge.");
|
|
609
|
+
break;
|
|
610
|
+
case "merge:pr:processing":
|
|
611
|
+
ctx.startSpinner(`PR #${event.number}: ${event.title}${event.retry ? ` (retry ${event.retry})` : ""}`);
|
|
612
|
+
break;
|
|
613
|
+
case "merge:branch:updating":
|
|
614
|
+
ctx.startSpinner(`Updating branch for PR #${event.prNumber}…`);
|
|
615
|
+
break;
|
|
616
|
+
case "merge:branch:updated":
|
|
617
|
+
ctx.stopSpinner(`Branch updated for PR #${event.prNumber}`);
|
|
618
|
+
break;
|
|
619
|
+
case "merge:ci:waiting":
|
|
620
|
+
ctx.startSpinner(`Waiting for CI on PR #${event.prNumber}…`);
|
|
621
|
+
break;
|
|
622
|
+
case "merge:ci:check": {
|
|
623
|
+
const icon = event.status === "pass" ? "✓" : event.status === "fail" ? "✗" : "…";
|
|
624
|
+
const dur = event.duration ? ` (${event.duration}s)` : "";
|
|
625
|
+
ctx.info(` ${icon} ${event.name}${dur}`);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
case "merge:ci:passed":
|
|
629
|
+
ctx.stopSpinner(`CI passed for PR #${event.prNumber}`);
|
|
630
|
+
break;
|
|
631
|
+
case "merge:ci:failed":
|
|
632
|
+
ctx.stopSpinner(`CI failed for PR #${event.prNumber}`);
|
|
633
|
+
break;
|
|
634
|
+
case "merge:ci:timeout":
|
|
635
|
+
ctx.stopSpinner(`CI timed out for PR #${event.prNumber}`);
|
|
636
|
+
break;
|
|
637
|
+
case "merge:ci:none":
|
|
638
|
+
ctx.stopSpinner(`No CI checks for PR #${event.prNumber}`);
|
|
639
|
+
break;
|
|
640
|
+
case "merge:pr:merging":
|
|
641
|
+
ctx.startSpinner(`Merging PR #${event.prNumber}…`);
|
|
642
|
+
break;
|
|
643
|
+
case "merge:pr:merged":
|
|
644
|
+
ctx.stopSpinner(`PR #${event.prNumber} merged ✓`);
|
|
645
|
+
break;
|
|
646
|
+
case "merge:pr:skipped":
|
|
647
|
+
ctx.warn(` ⊘ PR #${event.prNumber}: ${event.reason}`);
|
|
648
|
+
break;
|
|
649
|
+
case "merge:conflict:detected":
|
|
650
|
+
ctx.stopSpinner(`Conflict detected on PR #${event.prNumber}`);
|
|
651
|
+
break;
|
|
652
|
+
case "merge:redispatch:start":
|
|
653
|
+
ctx.startSpinner(`Re-dispatching PR #${event.oldPr}…`);
|
|
654
|
+
break;
|
|
655
|
+
case "merge:redispatch:waiting":
|
|
656
|
+
ctx.startSpinner(`Waiting for re-dispatched PR (was #${event.oldPr})…`);
|
|
657
|
+
break;
|
|
658
|
+
case "merge:redispatch:done":
|
|
659
|
+
ctx.stopSpinner(`Re-dispatched: #${event.oldPr} → #${event.newPr}`);
|
|
660
|
+
break;
|
|
661
|
+
case "merge:done":
|
|
662
|
+
ctx.success(`Merge complete — ${event.merged.length} merged, ${event.skipped.length} skipped`);
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/shared/ui/render/error.ts
|
|
668
|
+
function renderErrorEvent(event, ctx) {
|
|
669
|
+
ctx.stopSpinner();
|
|
670
|
+
ctx.error(`[${event.code}] ${event.message}`);
|
|
671
|
+
if (event.suggestion)
|
|
672
|
+
ctx.info(` \uD83D\uDCA1 ${event.suggestion}`);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/shared/ui/interactive.ts
|
|
676
|
+
class InteractiveRenderer {
|
|
677
|
+
spinner = null;
|
|
678
|
+
ctx = {
|
|
679
|
+
info: (msg) => p.log.info(msg),
|
|
680
|
+
success: (msg) => p.log.success(msg),
|
|
681
|
+
warn: (msg) => p.log.warn(msg),
|
|
682
|
+
error: (msg) => p.log.error(msg),
|
|
683
|
+
message: (msg) => p.log.message(msg),
|
|
684
|
+
step: (msg) => p.log.step(msg),
|
|
685
|
+
startSpinner: (msg) => this.startSpinner(msg),
|
|
686
|
+
stopSpinner: (msg) => this.stopSpinner(msg)
|
|
687
|
+
};
|
|
688
|
+
start(title) {
|
|
689
|
+
p.intro(title);
|
|
690
|
+
}
|
|
691
|
+
end(message) {
|
|
692
|
+
this.stopSpinner();
|
|
693
|
+
p.outro(message);
|
|
694
|
+
}
|
|
695
|
+
error(message) {
|
|
696
|
+
this.stopSpinner();
|
|
697
|
+
p.log.error(message);
|
|
698
|
+
}
|
|
699
|
+
render(event) {
|
|
700
|
+
if (event.type.startsWith("init:"))
|
|
701
|
+
return renderInitEvent(event, this.ctx);
|
|
702
|
+
if (event.type.startsWith("configure:"))
|
|
703
|
+
return renderConfigureEvent(event, this.ctx);
|
|
704
|
+
if (event.type.startsWith("analyze:"))
|
|
705
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
706
|
+
if (event.type.startsWith("dispatch:"))
|
|
707
|
+
return renderDispatchEvent(event, this.ctx);
|
|
708
|
+
if (event.type.startsWith("merge:"))
|
|
709
|
+
return renderMergeEvent(event, this.ctx);
|
|
710
|
+
if (event.type === "error")
|
|
711
|
+
return renderErrorEvent(event, this.ctx);
|
|
712
|
+
}
|
|
713
|
+
startSpinner(message) {
|
|
714
|
+
this.stopSpinner();
|
|
715
|
+
this.spinner = p.spinner();
|
|
716
|
+
this.spinner.start(message);
|
|
717
|
+
}
|
|
718
|
+
stopSpinner(message) {
|
|
719
|
+
if (this.spinner) {
|
|
720
|
+
this.spinner.stop(message);
|
|
721
|
+
this.spinner = null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/shared/ui/plain.ts
|
|
727
|
+
class PlainRenderer {
|
|
728
|
+
ctx = {
|
|
729
|
+
info: (msg) => console.log(msg),
|
|
730
|
+
success: (msg) => console.log(msg),
|
|
731
|
+
warn: (msg) => console.log(msg),
|
|
732
|
+
error: (msg) => console.error(msg),
|
|
733
|
+
message: (msg) => console.log(msg),
|
|
734
|
+
step: (msg) => console.log(msg),
|
|
735
|
+
startSpinner: (msg) => console.log(msg),
|
|
736
|
+
stopSpinner: (msg) => {
|
|
737
|
+
if (msg)
|
|
738
|
+
console.log(` ✓ ${msg}`);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
start(title) {
|
|
742
|
+
console.log(`
|
|
743
|
+
═══ ${title} ═══
|
|
744
|
+
`);
|
|
745
|
+
}
|
|
746
|
+
end(message) {
|
|
747
|
+
console.log(`
|
|
748
|
+
═══ ${message} ═══
|
|
749
|
+
`);
|
|
750
|
+
}
|
|
751
|
+
error(message) {
|
|
752
|
+
console.error(`ERROR: ${message}`);
|
|
753
|
+
}
|
|
754
|
+
render(event) {
|
|
755
|
+
if (event.type.startsWith("init:"))
|
|
756
|
+
return renderInitEvent(event, this.ctx);
|
|
757
|
+
if (event.type.startsWith("configure:"))
|
|
758
|
+
return renderConfigureEvent(event, this.ctx);
|
|
759
|
+
if (event.type.startsWith("analyze:"))
|
|
760
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
761
|
+
if (event.type.startsWith("dispatch:"))
|
|
762
|
+
return renderDispatchEvent(event, this.ctx);
|
|
763
|
+
if (event.type.startsWith("merge:"))
|
|
764
|
+
return renderMergeEvent(event, this.ctx);
|
|
765
|
+
if (event.type === "error")
|
|
766
|
+
return renderErrorEvent(event, this.ctx);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/shared/ui/index.ts
|
|
771
|
+
function isInteractive() {
|
|
772
|
+
if (process.env.CI === "true")
|
|
773
|
+
return false;
|
|
774
|
+
if (!process.stdout.isTTY)
|
|
775
|
+
return false;
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
function createRenderer(interactive) {
|
|
779
|
+
const useInteractive = interactive ?? isInteractive();
|
|
780
|
+
return useInteractive ? new InteractiveRenderer : new PlainRenderer;
|
|
781
|
+
}
|
|
782
|
+
function createEmitter(renderer) {
|
|
783
|
+
return (event) => renderer.render(event);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/cli/merge.command.ts
|
|
787
|
+
var merge_command_default = defineCommand({
|
|
788
|
+
meta: {
|
|
789
|
+
name: "merge",
|
|
790
|
+
description: "Sequentially merge fleet PRs (label or fleet-run mode)"
|
|
791
|
+
},
|
|
792
|
+
args: {
|
|
793
|
+
mode: {
|
|
794
|
+
type: "string",
|
|
795
|
+
description: "PR selection mode: label or fleet-run",
|
|
796
|
+
default: "label"
|
|
797
|
+
},
|
|
798
|
+
"run-id": {
|
|
799
|
+
type: "string",
|
|
800
|
+
description: "Fleet run ID (required for fleet-run mode)",
|
|
801
|
+
default: ""
|
|
802
|
+
},
|
|
803
|
+
base: {
|
|
804
|
+
type: "string",
|
|
805
|
+
description: "Base branch to merge into",
|
|
806
|
+
default: process.env.FLEET_BASE_BRANCH || "main"
|
|
807
|
+
},
|
|
808
|
+
admin: {
|
|
809
|
+
type: "boolean",
|
|
810
|
+
description: "Use admin privileges to bypass branch protection",
|
|
811
|
+
default: false
|
|
812
|
+
},
|
|
813
|
+
"re-dispatch": {
|
|
814
|
+
type: "boolean",
|
|
815
|
+
description: "Automatically re-dispatch tasks on merge conflict (requires JULES_API_KEY)",
|
|
816
|
+
default: false
|
|
817
|
+
},
|
|
818
|
+
owner: {
|
|
819
|
+
type: "string",
|
|
820
|
+
description: "Repository owner (auto-detected from git remote if omitted)"
|
|
821
|
+
},
|
|
822
|
+
repo: {
|
|
823
|
+
type: "string",
|
|
824
|
+
description: "Repository name (auto-detected from git remote if omitted)"
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
async run({ args }) {
|
|
828
|
+
const renderer = createRenderer();
|
|
829
|
+
let owner = args.owner;
|
|
830
|
+
let repo = args.repo;
|
|
831
|
+
if (!owner || !repo) {
|
|
832
|
+
const repoInfo = await getGitRepoInfo();
|
|
833
|
+
owner = owner || repoInfo.owner;
|
|
834
|
+
repo = repo || repoInfo.repo;
|
|
835
|
+
}
|
|
836
|
+
renderer.start(`Fleet Merge — ${owner}/${repo} (${args.mode} mode)`);
|
|
837
|
+
const input = MergeInputSchema.parse({
|
|
838
|
+
mode: args.mode,
|
|
839
|
+
runId: args["run-id"] || undefined,
|
|
840
|
+
baseBranch: args.base,
|
|
841
|
+
admin: args.admin,
|
|
842
|
+
reDispatch: args["re-dispatch"],
|
|
843
|
+
owner,
|
|
844
|
+
repo
|
|
845
|
+
});
|
|
846
|
+
const octokit = createFleetOctokit();
|
|
847
|
+
const emit = createEmitter(renderer);
|
|
848
|
+
const handler = new MergeHandler({ octokit, emit });
|
|
849
|
+
const result = await handler.execute(input);
|
|
850
|
+
if (!result.success) {
|
|
851
|
+
renderer.error(result.error.message);
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
renderer.end("Sequential merge complete.");
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
export {
|
|
858
|
+
merge_command_default as default
|
|
859
|
+
};
|