@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,777 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/cli/dispatch.command.ts
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
|
|
7
|
+
// src/dispatch/spec.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var DispatchInputSchema = z.object({
|
|
10
|
+
milestone: z.string().min(1),
|
|
11
|
+
owner: z.string().min(1),
|
|
12
|
+
repo: z.string().min(1),
|
|
13
|
+
baseBranch: z.string().default("main")
|
|
14
|
+
});
|
|
15
|
+
var DispatchErrorCode = z.enum([
|
|
16
|
+
"NO_FLEET_ISSUES",
|
|
17
|
+
"MILESTONE_FETCH_FAILED",
|
|
18
|
+
"SESSION_DISPATCH_FAILED",
|
|
19
|
+
"UNKNOWN_ERROR"
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// src/shared/result/create-result-schemas.ts
|
|
23
|
+
import { z as z2 } from "zod";
|
|
24
|
+
// src/shared/result/ok.ts
|
|
25
|
+
function ok(data) {
|
|
26
|
+
return { success: true, data };
|
|
27
|
+
}
|
|
28
|
+
// src/shared/result/fail.ts
|
|
29
|
+
function fail(code, message, recoverable = false, suggestion) {
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: { code, message, recoverable, suggestion }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// src/analyze/milestone.ts
|
|
36
|
+
var IGNORE_LABEL = "status: ignore";
|
|
37
|
+
function isTargetIssue(issue) {
|
|
38
|
+
if (issue.pull_request)
|
|
39
|
+
return false;
|
|
40
|
+
const hasIgnoreLabel = issue.labels?.some((label) => {
|
|
41
|
+
const labelName = typeof label === "string" ? label : label.name;
|
|
42
|
+
return labelName === IGNORE_LABEL;
|
|
43
|
+
});
|
|
44
|
+
return !hasIgnoreLabel;
|
|
45
|
+
}
|
|
46
|
+
function toMilestoneIssue(raw) {
|
|
47
|
+
return {
|
|
48
|
+
number: raw.number,
|
|
49
|
+
title: raw.title,
|
|
50
|
+
state: raw.state,
|
|
51
|
+
labels: (raw.labels ?? []).map((l) => typeof l === "string" ? l : l.name),
|
|
52
|
+
body: raw.body ?? "",
|
|
53
|
+
createdAt: raw.created_at,
|
|
54
|
+
closedAt: raw.closed_at ?? undefined
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function toPullRequest(raw) {
|
|
58
|
+
return {
|
|
59
|
+
number: raw.number,
|
|
60
|
+
title: raw.title,
|
|
61
|
+
head: raw.head.ref,
|
|
62
|
+
base: raw.base.ref,
|
|
63
|
+
body: raw.body ?? ""
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function getMilestoneContext(octokit, options) {
|
|
67
|
+
const { owner, repo, milestone, closedLookbackDays = 14 } = options;
|
|
68
|
+
let milestoneInfo;
|
|
69
|
+
if (milestone) {
|
|
70
|
+
const { data } = await octokit.rest.issues.getMilestone({
|
|
71
|
+
owner,
|
|
72
|
+
repo,
|
|
73
|
+
milestone_number: parseInt(milestone, 10)
|
|
74
|
+
});
|
|
75
|
+
milestoneInfo = { number: data.number, title: data.title };
|
|
76
|
+
}
|
|
77
|
+
const apiMilestoneFilter = milestone || "none";
|
|
78
|
+
const { data: openRaw } = await octokit.rest.issues.listForRepo({
|
|
79
|
+
owner,
|
|
80
|
+
repo,
|
|
81
|
+
state: "open",
|
|
82
|
+
milestone: apiMilestoneFilter,
|
|
83
|
+
per_page: 100
|
|
84
|
+
});
|
|
85
|
+
const since = new Date;
|
|
86
|
+
since.setDate(since.getDate() - closedLookbackDays);
|
|
87
|
+
const { data: closedRaw } = await octokit.rest.issues.listForRepo({
|
|
88
|
+
owner,
|
|
89
|
+
repo,
|
|
90
|
+
state: "closed",
|
|
91
|
+
milestone: apiMilestoneFilter,
|
|
92
|
+
since: since.toISOString(),
|
|
93
|
+
per_page: 100
|
|
94
|
+
});
|
|
95
|
+
const { data: openPRs } = await octokit.rest.pulls.list({
|
|
96
|
+
owner,
|
|
97
|
+
repo,
|
|
98
|
+
state: "open",
|
|
99
|
+
per_page: 100
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
milestone: milestoneInfo,
|
|
103
|
+
issues: {
|
|
104
|
+
open: openRaw.filter(isTargetIssue).map(toMilestoneIssue),
|
|
105
|
+
closed: closedRaw.filter(isTargetIssue).map(toMilestoneIssue)
|
|
106
|
+
},
|
|
107
|
+
pullRequests: openPRs.map(toPullRequest)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/dispatch/status.ts
|
|
112
|
+
var DISPATCH_MARKER = "\uD83E\uDD16 **Fleet Dispatch Event**";
|
|
113
|
+
async function getDispatchStatus(octokit, owner, repo, issueNumbers) {
|
|
114
|
+
const { data: openPRs } = await octokit.rest.pulls.list({
|
|
115
|
+
owner,
|
|
116
|
+
repo,
|
|
117
|
+
state: "open",
|
|
118
|
+
per_page: 100
|
|
119
|
+
});
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const issueNumber of issueNumbers) {
|
|
122
|
+
const { data: issue } = await octokit.rest.issues.get({
|
|
123
|
+
owner,
|
|
124
|
+
repo,
|
|
125
|
+
issue_number: issueNumber
|
|
126
|
+
});
|
|
127
|
+
const { data: comments } = await octokit.rest.issues.listComments({
|
|
128
|
+
owner,
|
|
129
|
+
repo,
|
|
130
|
+
issue_number: issueNumber,
|
|
131
|
+
per_page: 100
|
|
132
|
+
});
|
|
133
|
+
const dispatchComment = comments.find((c) => c.body?.includes(DISPATCH_MARKER));
|
|
134
|
+
let dispatchEvent = null;
|
|
135
|
+
if (dispatchComment?.body) {
|
|
136
|
+
const sessionMatch = dispatchComment.body.match(/Session:\s*`([^`]+)`/);
|
|
137
|
+
dispatchEvent = {
|
|
138
|
+
sessionId: sessionMatch?.[1] ?? "unknown",
|
|
139
|
+
timestamp: dispatchComment.created_at,
|
|
140
|
+
commentId: dispatchComment.id
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const linkedPRs = openPRs.filter((pr) => pr.body?.includes(`#${issueNumber}`) || pr.body?.includes(`Fixes #${issueNumber}`) || pr.body?.includes(`Closes #${issueNumber}`)).map((pr) => pr.number);
|
|
144
|
+
results.push({
|
|
145
|
+
number: issueNumber,
|
|
146
|
+
open: issue.state === "open",
|
|
147
|
+
labels: (issue.labels ?? []).map((l) => typeof l === "string" ? l : l.name),
|
|
148
|
+
dispatchEvent,
|
|
149
|
+
linkedPRs
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/dispatch/events.ts
|
|
156
|
+
async function recordDispatch(octokit, owner, repo, issueNumber, sessionId) {
|
|
157
|
+
const timestamp = new Date().toISOString();
|
|
158
|
+
const body = [
|
|
159
|
+
`\uD83E\uDD16 **Fleet Dispatch Event**`,
|
|
160
|
+
`Session: \`${sessionId}\``,
|
|
161
|
+
`Timestamp: ${timestamp}`
|
|
162
|
+
].join(`
|
|
163
|
+
`);
|
|
164
|
+
const { data: comment } = await octokit.rest.issues.createComment({
|
|
165
|
+
owner,
|
|
166
|
+
repo,
|
|
167
|
+
issue_number: issueNumber,
|
|
168
|
+
body
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
commentId: comment.id,
|
|
172
|
+
timestamp
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/dispatch/handler.ts
|
|
177
|
+
class DispatchHandler {
|
|
178
|
+
octokit;
|
|
179
|
+
dispatcher;
|
|
180
|
+
emit;
|
|
181
|
+
constructor(deps) {
|
|
182
|
+
this.octokit = deps.octokit;
|
|
183
|
+
this.dispatcher = deps.dispatcher;
|
|
184
|
+
this.emit = deps.emit ?? (() => {});
|
|
185
|
+
}
|
|
186
|
+
async execute(input) {
|
|
187
|
+
try {
|
|
188
|
+
this.emit({ type: "dispatch:start", milestone: input.milestone });
|
|
189
|
+
this.emit({ type: "dispatch:scanning" });
|
|
190
|
+
const ctx = await getMilestoneContext(this.octokit, {
|
|
191
|
+
owner: input.owner,
|
|
192
|
+
repo: input.repo,
|
|
193
|
+
milestone: input.milestone
|
|
194
|
+
});
|
|
195
|
+
const fleetIssues = ctx.issues.open.filter((issue) => issue.labels.includes("fleet"));
|
|
196
|
+
if (fleetIssues.length === 0) {
|
|
197
|
+
this.emit({ type: "dispatch:done", dispatched: 0, skipped: 0 });
|
|
198
|
+
return ok({ dispatched: [], skipped: 0 });
|
|
199
|
+
}
|
|
200
|
+
const statuses = await getDispatchStatus(this.octokit, input.owner, input.repo, fleetIssues.map((i) => i.number));
|
|
201
|
+
const undispatched = statuses.filter((s) => s.open && !s.dispatchEvent && s.linkedPRs.length === 0);
|
|
202
|
+
const skipped = statuses.length - undispatched.length;
|
|
203
|
+
if (undispatched.length === 0) {
|
|
204
|
+
this.emit({ type: "dispatch:done", dispatched: 0, skipped });
|
|
205
|
+
return ok({ dispatched: [], skipped });
|
|
206
|
+
}
|
|
207
|
+
this.emit({ type: "dispatch:found", count: undispatched.length });
|
|
208
|
+
const dispatched = [];
|
|
209
|
+
for (const status of undispatched) {
|
|
210
|
+
const issue = fleetIssues.find((i) => i.number === status.number);
|
|
211
|
+
this.emit({
|
|
212
|
+
type: "dispatch:issue:dispatching",
|
|
213
|
+
number: issue.number,
|
|
214
|
+
title: issue.title
|
|
215
|
+
});
|
|
216
|
+
const workerPrompt = `Fix Issue #${issue.number}: ${issue.title}
|
|
217
|
+
|
|
218
|
+
IMPORTANT: Your PR title MUST start with "Fixes #${issue.number}" and your PR description MUST include "Fixes #${issue.number}" on its own line so the issue is auto-closed on merge.
|
|
219
|
+
|
|
220
|
+
You are an autonomous execution agent. Implement the fix described below exactly as specified.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
${issue.body}
|
|
225
|
+
`;
|
|
226
|
+
try {
|
|
227
|
+
const session = await this.dispatcher.dispatch({
|
|
228
|
+
prompt: workerPrompt,
|
|
229
|
+
source: {
|
|
230
|
+
github: `${input.owner}/${input.repo}`,
|
|
231
|
+
baseBranch: input.baseBranch
|
|
232
|
+
},
|
|
233
|
+
requireApproval: false,
|
|
234
|
+
autoPr: true
|
|
235
|
+
});
|
|
236
|
+
await recordDispatch(this.octokit, input.owner, input.repo, issue.number, session.id);
|
|
237
|
+
dispatched.push({
|
|
238
|
+
issueNumber: issue.number,
|
|
239
|
+
sessionId: session.id
|
|
240
|
+
});
|
|
241
|
+
this.emit({
|
|
242
|
+
type: "dispatch:issue:dispatched",
|
|
243
|
+
number: issue.number,
|
|
244
|
+
sessionId: session.id
|
|
245
|
+
});
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.emit({
|
|
248
|
+
type: "error",
|
|
249
|
+
code: "DISPATCH_FAILED",
|
|
250
|
+
message: `Failed to dispatch #${issue.number}: ${error instanceof Error ? error.message : error}`
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const status of statuses) {
|
|
255
|
+
if (!undispatched.includes(status)) {
|
|
256
|
+
this.emit({
|
|
257
|
+
type: "dispatch:issue:skipped",
|
|
258
|
+
number: status.number,
|
|
259
|
+
reason: status.dispatchEvent ? "already dispatched" : "has linked PRs"
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
this.emit({
|
|
264
|
+
type: "dispatch:done",
|
|
265
|
+
dispatched: dispatched.length,
|
|
266
|
+
skipped
|
|
267
|
+
});
|
|
268
|
+
return ok({ dispatched, skipped });
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/shared/auth/octokit.ts
|
|
276
|
+
import { Octokit } from "octokit";
|
|
277
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
278
|
+
|
|
279
|
+
// src/shared/auth/cache-plugin.ts
|
|
280
|
+
function cachePlugin(octokit) {
|
|
281
|
+
const cache = new Map;
|
|
282
|
+
octokit.hook.wrap("request", async (request, options) => {
|
|
283
|
+
const key = `${options.method} ${options.url}`;
|
|
284
|
+
const cached = cache.get(key);
|
|
285
|
+
if (cached) {
|
|
286
|
+
options.headers = {
|
|
287
|
+
...options.headers,
|
|
288
|
+
"if-none-match": cached.etag
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
const response = await request(options);
|
|
293
|
+
const etag = response.headers.etag;
|
|
294
|
+
if (etag) {
|
|
295
|
+
cache.set(key, { etag, data: response.data });
|
|
296
|
+
}
|
|
297
|
+
return response;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (error.status === 304 && cached) {
|
|
300
|
+
return { ...error.response, data: cached.data, status: 200 };
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/shared/auth/resolve-key.ts
|
|
308
|
+
function resolvePrivateKey(base64Value, rawValue) {
|
|
309
|
+
if (base64Value) {
|
|
310
|
+
return Buffer.from(base64Value, "base64").toString("utf-8");
|
|
311
|
+
}
|
|
312
|
+
if (rawValue) {
|
|
313
|
+
return rawValue.replace(/\\n/g, `
|
|
314
|
+
`);
|
|
315
|
+
}
|
|
316
|
+
throw new Error("No private key provided. Set GITHUB_APP_PRIVATE_KEY_BASE64 (recommended) or GITHUB_APP_PRIVATE_KEY.");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/shared/auth/octokit.ts
|
|
320
|
+
var CachedOctokit = Octokit.plugin(cachePlugin);
|
|
321
|
+
function getAuthOptions() {
|
|
322
|
+
const appId = process.env.GITHUB_APP_ID;
|
|
323
|
+
const privateKeyBase64 = process.env.GITHUB_APP_PRIVATE_KEY_BASE64;
|
|
324
|
+
const privateKeyRaw = process.env.GITHUB_APP_PRIVATE_KEY;
|
|
325
|
+
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
|
|
326
|
+
if (appId && (privateKeyBase64 || privateKeyRaw) && installationId) {
|
|
327
|
+
return {
|
|
328
|
+
authStrategy: createAppAuth,
|
|
329
|
+
auth: {
|
|
330
|
+
appId,
|
|
331
|
+
privateKey: resolvePrivateKey(privateKeyBase64, privateKeyRaw),
|
|
332
|
+
installationId: Number(installationId)
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const token = process.env.GITHUB_TOKEN;
|
|
337
|
+
if (token) {
|
|
338
|
+
return { auth: token };
|
|
339
|
+
}
|
|
340
|
+
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.");
|
|
341
|
+
}
|
|
342
|
+
function createFleetOctokit() {
|
|
343
|
+
return new CachedOctokit(getAuthOptions());
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/shared/auth/git.ts
|
|
347
|
+
import { exec } from "child_process";
|
|
348
|
+
import { promisify } from "util";
|
|
349
|
+
var execAsync = promisify(exec);
|
|
350
|
+
async function getGitRepoInfo(remoteName = "origin") {
|
|
351
|
+
const ghRepo = process.env.GITHUB_REPOSITORY;
|
|
352
|
+
if (ghRepo) {
|
|
353
|
+
const [owner, repo] = ghRepo.split("/");
|
|
354
|
+
return { owner, repo, fullName: ghRepo };
|
|
355
|
+
}
|
|
356
|
+
const { stdout } = await execAsync(`git remote get-url ${remoteName}`);
|
|
357
|
+
return parseGitRemoteUrl(stdout.trim());
|
|
358
|
+
}
|
|
359
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
360
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(\.git)?$/);
|
|
361
|
+
if (sshMatch) {
|
|
362
|
+
const [, owner, repo] = sshMatch;
|
|
363
|
+
return {
|
|
364
|
+
owner,
|
|
365
|
+
repo: repo.replace(/\.git$/, ""),
|
|
366
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const httpsMatch = remoteUrl.match(/https?:\/\/github\.com\/([^/]+)\/(.+?)(\.git)?$/);
|
|
370
|
+
if (httpsMatch) {
|
|
371
|
+
const [, owner, repo] = httpsMatch;
|
|
372
|
+
return {
|
|
373
|
+
owner,
|
|
374
|
+
repo: repo.replace(/\.git$/, ""),
|
|
375
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
throw new Error(`Unable to parse git remote URL: ${remoteUrl}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/shared/ui/interactive.ts
|
|
382
|
+
import * as p from "@clack/prompts";
|
|
383
|
+
|
|
384
|
+
// src/shared/ui/render/init.ts
|
|
385
|
+
function renderInitEvent(event, ctx) {
|
|
386
|
+
switch (event.type) {
|
|
387
|
+
case "init:start":
|
|
388
|
+
ctx.info(`Initializing fleet for ${event.owner}/${event.repo}`);
|
|
389
|
+
break;
|
|
390
|
+
case "init:branch:creating":
|
|
391
|
+
ctx.startSpinner(`Creating branch ${event.name} from ${event.base}`);
|
|
392
|
+
break;
|
|
393
|
+
case "init:branch:created":
|
|
394
|
+
ctx.stopSpinner(`Branch ${event.name} created`);
|
|
395
|
+
break;
|
|
396
|
+
case "init:file:committed":
|
|
397
|
+
ctx.info(` ✓ ${event.path}`);
|
|
398
|
+
break;
|
|
399
|
+
case "init:file:skipped":
|
|
400
|
+
ctx.warn(` ⊘ ${event.path} — ${event.reason}`);
|
|
401
|
+
break;
|
|
402
|
+
case "init:pr:creating":
|
|
403
|
+
ctx.startSpinner("Creating pull request…");
|
|
404
|
+
break;
|
|
405
|
+
case "init:pr:created":
|
|
406
|
+
ctx.stopSpinner(`PR #${event.number} created`);
|
|
407
|
+
ctx.info(` ${event.url}`);
|
|
408
|
+
break;
|
|
409
|
+
case "init:done":
|
|
410
|
+
ctx.success(`Fleet initialized — PR: ${event.prUrl}`);
|
|
411
|
+
break;
|
|
412
|
+
case "init:auth:detected":
|
|
413
|
+
ctx.success(`Auth: ${event.method === "token" ? "GITHUB_TOKEN" : "GitHub App"}`);
|
|
414
|
+
break;
|
|
415
|
+
case "init:secret:uploading":
|
|
416
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
417
|
+
break;
|
|
418
|
+
case "init:secret:uploaded":
|
|
419
|
+
ctx.stopSpinner(`Secret ${event.name} saved`);
|
|
420
|
+
break;
|
|
421
|
+
case "init:secret:skipped":
|
|
422
|
+
ctx.warn(` ⊘ ${event.name} — ${event.reason}`);
|
|
423
|
+
break;
|
|
424
|
+
case "init:dry-run":
|
|
425
|
+
ctx.info("Would create:");
|
|
426
|
+
event.files.forEach((f) => ctx.message(` ${f}`));
|
|
427
|
+
break;
|
|
428
|
+
case "init:already-initialized":
|
|
429
|
+
ctx.warn("Repository is already initialized");
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/shared/ui/render/configure.ts
|
|
435
|
+
function renderConfigureEvent(event, ctx) {
|
|
436
|
+
switch (event.type) {
|
|
437
|
+
case "configure:start":
|
|
438
|
+
ctx.info(`Configuring ${event.resource} for ${event.owner}/${event.repo}`);
|
|
439
|
+
break;
|
|
440
|
+
case "configure:label:created":
|
|
441
|
+
ctx.info(` ✓ Label "${event.name}" created`);
|
|
442
|
+
break;
|
|
443
|
+
case "configure:label:exists":
|
|
444
|
+
ctx.warn(` ⊘ Label "${event.name}" already exists`);
|
|
445
|
+
break;
|
|
446
|
+
case "configure:secret:uploading":
|
|
447
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
448
|
+
break;
|
|
449
|
+
case "configure:secret:uploaded":
|
|
450
|
+
ctx.stopSpinner(`Secret ${event.name} uploaded`);
|
|
451
|
+
break;
|
|
452
|
+
case "configure:done":
|
|
453
|
+
ctx.success("Configuration complete");
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/shared/ui/session-url.ts
|
|
459
|
+
var JULES_BASE_URL = "https://jules.google.com";
|
|
460
|
+
function sessionUrl(sessionId) {
|
|
461
|
+
return `${JULES_BASE_URL}/sessions/${sessionId}`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/shared/ui/render/analyze.ts
|
|
465
|
+
function renderAnalyzeEvent(event, ctx) {
|
|
466
|
+
switch (event.type) {
|
|
467
|
+
case "analyze:start":
|
|
468
|
+
ctx.info(`Analyzing ${event.goalCount} goal(s) for ${event.owner}/${event.repo}`);
|
|
469
|
+
break;
|
|
470
|
+
case "analyze:goal:start":
|
|
471
|
+
if (event.total > 1) {
|
|
472
|
+
ctx.step(`[${event.index}/${event.total}] ${event.file}`);
|
|
473
|
+
} else {
|
|
474
|
+
ctx.step(event.file);
|
|
475
|
+
}
|
|
476
|
+
if (event.milestone)
|
|
477
|
+
ctx.info(` Milestone: ${event.milestone}`);
|
|
478
|
+
break;
|
|
479
|
+
case "analyze:milestone:resolved":
|
|
480
|
+
ctx.info(` Milestone "${event.title}" (#${event.id})`);
|
|
481
|
+
break;
|
|
482
|
+
case "analyze:context:fetched":
|
|
483
|
+
ctx.info(` Context: ${event.openIssues} open, ${event.closedIssues} closed, ${event.prs} PRs`);
|
|
484
|
+
break;
|
|
485
|
+
case "analyze:session:dispatching":
|
|
486
|
+
ctx.startSpinner(`Dispatching session for ${event.goal}…`);
|
|
487
|
+
break;
|
|
488
|
+
case "analyze:session:started":
|
|
489
|
+
ctx.stopSpinner(`Session started: ${event.id}`);
|
|
490
|
+
ctx.info(` ${sessionUrl(event.id)}`);
|
|
491
|
+
break;
|
|
492
|
+
case "analyze:session:failed":
|
|
493
|
+
ctx.stopSpinner();
|
|
494
|
+
ctx.error(` Failed: ${event.error}`);
|
|
495
|
+
break;
|
|
496
|
+
case "analyze:done":
|
|
497
|
+
ctx.success(`Analysis complete — ${event.sessionsStarted} session(s) from ${event.goalsProcessed} goal(s)`);
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/shared/ui/render/dispatch.ts
|
|
503
|
+
function renderDispatchEvent(event, ctx) {
|
|
504
|
+
switch (event.type) {
|
|
505
|
+
case "dispatch:start":
|
|
506
|
+
ctx.info(`Dispatching from milestone ${event.milestone}`);
|
|
507
|
+
break;
|
|
508
|
+
case "dispatch:scanning":
|
|
509
|
+
ctx.startSpinner("Scanning for fleet issues…");
|
|
510
|
+
break;
|
|
511
|
+
case "dispatch:found":
|
|
512
|
+
ctx.stopSpinner(`Found ${event.count} undispatched issue(s)`);
|
|
513
|
+
break;
|
|
514
|
+
case "dispatch:issue:dispatching":
|
|
515
|
+
ctx.startSpinner(`#${event.number}: ${event.title}`);
|
|
516
|
+
break;
|
|
517
|
+
case "dispatch:issue:dispatched":
|
|
518
|
+
ctx.stopSpinner(`#${event.number} → session ${event.sessionId}`);
|
|
519
|
+
ctx.info(` ${sessionUrl(event.sessionId)}`);
|
|
520
|
+
break;
|
|
521
|
+
case "dispatch:issue:skipped":
|
|
522
|
+
ctx.warn(` ⊘ #${event.number}: ${event.reason}`);
|
|
523
|
+
break;
|
|
524
|
+
case "dispatch:done":
|
|
525
|
+
ctx.success(`Dispatch complete — ${event.dispatched} dispatched, ${event.skipped} skipped`);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/shared/ui/render/merge.ts
|
|
531
|
+
function renderMergeEvent(event, ctx) {
|
|
532
|
+
switch (event.type) {
|
|
533
|
+
case "merge:start":
|
|
534
|
+
ctx.info(`Merging ${event.prCount} PR(s) in ${event.owner}/${event.repo} [${event.mode}]`);
|
|
535
|
+
break;
|
|
536
|
+
case "merge:no-prs":
|
|
537
|
+
ctx.info("No PRs ready to merge.");
|
|
538
|
+
break;
|
|
539
|
+
case "merge:pr:processing":
|
|
540
|
+
ctx.startSpinner(`PR #${event.number}: ${event.title}${event.retry ? ` (retry ${event.retry})` : ""}`);
|
|
541
|
+
break;
|
|
542
|
+
case "merge:branch:updating":
|
|
543
|
+
ctx.startSpinner(`Updating branch for PR #${event.prNumber}…`);
|
|
544
|
+
break;
|
|
545
|
+
case "merge:branch:updated":
|
|
546
|
+
ctx.stopSpinner(`Branch updated for PR #${event.prNumber}`);
|
|
547
|
+
break;
|
|
548
|
+
case "merge:ci:waiting":
|
|
549
|
+
ctx.startSpinner(`Waiting for CI on PR #${event.prNumber}…`);
|
|
550
|
+
break;
|
|
551
|
+
case "merge:ci:check": {
|
|
552
|
+
const icon = event.status === "pass" ? "✓" : event.status === "fail" ? "✗" : "…";
|
|
553
|
+
const dur = event.duration ? ` (${event.duration}s)` : "";
|
|
554
|
+
ctx.info(` ${icon} ${event.name}${dur}`);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case "merge:ci:passed":
|
|
558
|
+
ctx.stopSpinner(`CI passed for PR #${event.prNumber}`);
|
|
559
|
+
break;
|
|
560
|
+
case "merge:ci:failed":
|
|
561
|
+
ctx.stopSpinner(`CI failed for PR #${event.prNumber}`);
|
|
562
|
+
break;
|
|
563
|
+
case "merge:ci:timeout":
|
|
564
|
+
ctx.stopSpinner(`CI timed out for PR #${event.prNumber}`);
|
|
565
|
+
break;
|
|
566
|
+
case "merge:ci:none":
|
|
567
|
+
ctx.stopSpinner(`No CI checks for PR #${event.prNumber}`);
|
|
568
|
+
break;
|
|
569
|
+
case "merge:pr:merging":
|
|
570
|
+
ctx.startSpinner(`Merging PR #${event.prNumber}…`);
|
|
571
|
+
break;
|
|
572
|
+
case "merge:pr:merged":
|
|
573
|
+
ctx.stopSpinner(`PR #${event.prNumber} merged ✓`);
|
|
574
|
+
break;
|
|
575
|
+
case "merge:pr:skipped":
|
|
576
|
+
ctx.warn(` ⊘ PR #${event.prNumber}: ${event.reason}`);
|
|
577
|
+
break;
|
|
578
|
+
case "merge:conflict:detected":
|
|
579
|
+
ctx.stopSpinner(`Conflict detected on PR #${event.prNumber}`);
|
|
580
|
+
break;
|
|
581
|
+
case "merge:redispatch:start":
|
|
582
|
+
ctx.startSpinner(`Re-dispatching PR #${event.oldPr}…`);
|
|
583
|
+
break;
|
|
584
|
+
case "merge:redispatch:waiting":
|
|
585
|
+
ctx.startSpinner(`Waiting for re-dispatched PR (was #${event.oldPr})…`);
|
|
586
|
+
break;
|
|
587
|
+
case "merge:redispatch:done":
|
|
588
|
+
ctx.stopSpinner(`Re-dispatched: #${event.oldPr} → #${event.newPr}`);
|
|
589
|
+
break;
|
|
590
|
+
case "merge:done":
|
|
591
|
+
ctx.success(`Merge complete — ${event.merged.length} merged, ${event.skipped.length} skipped`);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/shared/ui/render/error.ts
|
|
597
|
+
function renderErrorEvent(event, ctx) {
|
|
598
|
+
ctx.stopSpinner();
|
|
599
|
+
ctx.error(`[${event.code}] ${event.message}`);
|
|
600
|
+
if (event.suggestion)
|
|
601
|
+
ctx.info(` \uD83D\uDCA1 ${event.suggestion}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/shared/ui/interactive.ts
|
|
605
|
+
class InteractiveRenderer {
|
|
606
|
+
spinner = null;
|
|
607
|
+
ctx = {
|
|
608
|
+
info: (msg) => p.log.info(msg),
|
|
609
|
+
success: (msg) => p.log.success(msg),
|
|
610
|
+
warn: (msg) => p.log.warn(msg),
|
|
611
|
+
error: (msg) => p.log.error(msg),
|
|
612
|
+
message: (msg) => p.log.message(msg),
|
|
613
|
+
step: (msg) => p.log.step(msg),
|
|
614
|
+
startSpinner: (msg) => this.startSpinner(msg),
|
|
615
|
+
stopSpinner: (msg) => this.stopSpinner(msg)
|
|
616
|
+
};
|
|
617
|
+
start(title) {
|
|
618
|
+
p.intro(title);
|
|
619
|
+
}
|
|
620
|
+
end(message) {
|
|
621
|
+
this.stopSpinner();
|
|
622
|
+
p.outro(message);
|
|
623
|
+
}
|
|
624
|
+
error(message) {
|
|
625
|
+
this.stopSpinner();
|
|
626
|
+
p.log.error(message);
|
|
627
|
+
}
|
|
628
|
+
render(event) {
|
|
629
|
+
if (event.type.startsWith("init:"))
|
|
630
|
+
return renderInitEvent(event, this.ctx);
|
|
631
|
+
if (event.type.startsWith("configure:"))
|
|
632
|
+
return renderConfigureEvent(event, this.ctx);
|
|
633
|
+
if (event.type.startsWith("analyze:"))
|
|
634
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
635
|
+
if (event.type.startsWith("dispatch:"))
|
|
636
|
+
return renderDispatchEvent(event, this.ctx);
|
|
637
|
+
if (event.type.startsWith("merge:"))
|
|
638
|
+
return renderMergeEvent(event, this.ctx);
|
|
639
|
+
if (event.type === "error")
|
|
640
|
+
return renderErrorEvent(event, this.ctx);
|
|
641
|
+
}
|
|
642
|
+
startSpinner(message) {
|
|
643
|
+
this.stopSpinner();
|
|
644
|
+
this.spinner = p.spinner();
|
|
645
|
+
this.spinner.start(message);
|
|
646
|
+
}
|
|
647
|
+
stopSpinner(message) {
|
|
648
|
+
if (this.spinner) {
|
|
649
|
+
this.spinner.stop(message);
|
|
650
|
+
this.spinner = null;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/shared/ui/plain.ts
|
|
656
|
+
class PlainRenderer {
|
|
657
|
+
ctx = {
|
|
658
|
+
info: (msg) => console.log(msg),
|
|
659
|
+
success: (msg) => console.log(msg),
|
|
660
|
+
warn: (msg) => console.log(msg),
|
|
661
|
+
error: (msg) => console.error(msg),
|
|
662
|
+
message: (msg) => console.log(msg),
|
|
663
|
+
step: (msg) => console.log(msg),
|
|
664
|
+
startSpinner: (msg) => console.log(msg),
|
|
665
|
+
stopSpinner: (msg) => {
|
|
666
|
+
if (msg)
|
|
667
|
+
console.log(` ✓ ${msg}`);
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
start(title) {
|
|
671
|
+
console.log(`
|
|
672
|
+
═══ ${title} ═══
|
|
673
|
+
`);
|
|
674
|
+
}
|
|
675
|
+
end(message) {
|
|
676
|
+
console.log(`
|
|
677
|
+
═══ ${message} ═══
|
|
678
|
+
`);
|
|
679
|
+
}
|
|
680
|
+
error(message) {
|
|
681
|
+
console.error(`ERROR: ${message}`);
|
|
682
|
+
}
|
|
683
|
+
render(event) {
|
|
684
|
+
if (event.type.startsWith("init:"))
|
|
685
|
+
return renderInitEvent(event, this.ctx);
|
|
686
|
+
if (event.type.startsWith("configure:"))
|
|
687
|
+
return renderConfigureEvent(event, this.ctx);
|
|
688
|
+
if (event.type.startsWith("analyze:"))
|
|
689
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
690
|
+
if (event.type.startsWith("dispatch:"))
|
|
691
|
+
return renderDispatchEvent(event, this.ctx);
|
|
692
|
+
if (event.type.startsWith("merge:"))
|
|
693
|
+
return renderMergeEvent(event, this.ctx);
|
|
694
|
+
if (event.type === "error")
|
|
695
|
+
return renderErrorEvent(event, this.ctx);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/shared/ui/index.ts
|
|
700
|
+
function isInteractive() {
|
|
701
|
+
if (process.env.CI === "true")
|
|
702
|
+
return false;
|
|
703
|
+
if (!process.stdout.isTTY)
|
|
704
|
+
return false;
|
|
705
|
+
return true;
|
|
706
|
+
}
|
|
707
|
+
function createRenderer(interactive) {
|
|
708
|
+
const useInteractive = interactive ?? isInteractive();
|
|
709
|
+
return useInteractive ? new InteractiveRenderer : new PlainRenderer;
|
|
710
|
+
}
|
|
711
|
+
function createEmitter(renderer) {
|
|
712
|
+
return (event) => renderer.render(event);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/cli/dispatch.command.ts
|
|
716
|
+
var dispatch_command_default = defineCommand({
|
|
717
|
+
meta: {
|
|
718
|
+
name: "dispatch",
|
|
719
|
+
description: "Poll for undispatched fleet issues and fire Jules worker sessions"
|
|
720
|
+
},
|
|
721
|
+
args: {
|
|
722
|
+
milestone: {
|
|
723
|
+
type: "string",
|
|
724
|
+
description: "Milestone ID to scope dispatch",
|
|
725
|
+
required: true
|
|
726
|
+
},
|
|
727
|
+
owner: {
|
|
728
|
+
type: "string",
|
|
729
|
+
description: "Repository owner (auto-detected from git remote if omitted)"
|
|
730
|
+
},
|
|
731
|
+
repo: {
|
|
732
|
+
type: "string",
|
|
733
|
+
description: "Repository name (auto-detected from git remote if omitted)"
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
async run({ args }) {
|
|
737
|
+
const renderer = createRenderer();
|
|
738
|
+
let owner = args.owner;
|
|
739
|
+
let repo = args.repo;
|
|
740
|
+
if (!owner || !repo) {
|
|
741
|
+
const repoInfo = await getGitRepoInfo();
|
|
742
|
+
owner = owner || repoInfo.owner;
|
|
743
|
+
repo = repo || repoInfo.repo;
|
|
744
|
+
}
|
|
745
|
+
renderer.start(`Fleet Dispatch — Milestone ${args.milestone}`);
|
|
746
|
+
const input = DispatchInputSchema.parse({
|
|
747
|
+
milestone: args.milestone,
|
|
748
|
+
owner,
|
|
749
|
+
repo,
|
|
750
|
+
baseBranch: process.env.FLEET_BASE_BRANCH || "main"
|
|
751
|
+
});
|
|
752
|
+
const { jules } = await import("@google/jules-sdk");
|
|
753
|
+
const dispatcher = {
|
|
754
|
+
async dispatch(options) {
|
|
755
|
+
return jules.session({
|
|
756
|
+
prompt: options.prompt,
|
|
757
|
+
source: options.source,
|
|
758
|
+
requireApproval: options.requireApproval,
|
|
759
|
+
autoPr: options.autoPr
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
const octokit = createFleetOctokit();
|
|
764
|
+
const emit = createEmitter(renderer);
|
|
765
|
+
const handler = new DispatchHandler({ octokit, dispatcher, emit });
|
|
766
|
+
const result = await handler.execute(input);
|
|
767
|
+
if (!result.success) {
|
|
768
|
+
renderer.error(result.error.message);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
const { dispatched, skipped } = result.data;
|
|
772
|
+
renderer.end(`${dispatched.length} dispatched, ${skipped} skipped.`);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
export {
|
|
776
|
+
dispatch_command_default as default
|
|
777
|
+
};
|