@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,1287 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/cli/init.command.ts
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
|
|
7
|
+
// src/init/spec.ts
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var InitInputSchema = z.object({
|
|
10
|
+
repo: z.string().regex(/^[^/]+\/[^/]+$/, "Must be in owner/repo format").optional(),
|
|
11
|
+
owner: z.string().min(1),
|
|
12
|
+
repoName: z.string().min(1),
|
|
13
|
+
baseBranch: z.string().default("main")
|
|
14
|
+
});
|
|
15
|
+
var InitErrorCode = z.enum([
|
|
16
|
+
"REPO_NOT_FOUND",
|
|
17
|
+
"BRANCH_CREATE_FAILED",
|
|
18
|
+
"FILE_COMMIT_FAILED",
|
|
19
|
+
"PR_CREATE_FAILED",
|
|
20
|
+
"LABEL_CREATE_FAILED",
|
|
21
|
+
"GITHUB_API_ERROR",
|
|
22
|
+
"UNKNOWN_ERROR"
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// src/shared/result/create-result-schemas.ts
|
|
26
|
+
import { z as z2 } from "zod";
|
|
27
|
+
// src/shared/result/ok.ts
|
|
28
|
+
function ok(data) {
|
|
29
|
+
return { success: true, data };
|
|
30
|
+
}
|
|
31
|
+
// src/shared/result/fail.ts
|
|
32
|
+
function fail(code, message, recoverable = false, suggestion) {
|
|
33
|
+
return {
|
|
34
|
+
success: false,
|
|
35
|
+
error: { code, message, recoverable, suggestion }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// src/init/templates/analyze.ts
|
|
39
|
+
var FLEET_ANALYZE_TEMPLATE = {
|
|
40
|
+
repoPath: ".github/workflows/fleet-analyze.yml",
|
|
41
|
+
content: `# Generated by @google/jules-fleet init
|
|
42
|
+
# https://github.com/google-labs-code/jules-sdk
|
|
43
|
+
|
|
44
|
+
name: Fleet Analyze
|
|
45
|
+
|
|
46
|
+
on:
|
|
47
|
+
schedule:
|
|
48
|
+
- cron: '0 5 * * *' # Daily at 5am UTC — edit to your preference
|
|
49
|
+
workflow_dispatch:
|
|
50
|
+
inputs:
|
|
51
|
+
goal:
|
|
52
|
+
description: 'Path to goal file (or blank for all)'
|
|
53
|
+
type: string
|
|
54
|
+
default: ''
|
|
55
|
+
milestone:
|
|
56
|
+
description: 'Milestone ID override'
|
|
57
|
+
type: string
|
|
58
|
+
default: ''
|
|
59
|
+
|
|
60
|
+
concurrency:
|
|
61
|
+
group: fleet-analyze
|
|
62
|
+
cancel-in-progress: false
|
|
63
|
+
|
|
64
|
+
jobs:
|
|
65
|
+
analyze:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
permissions:
|
|
68
|
+
contents: read
|
|
69
|
+
issues: write
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v4
|
|
72
|
+
- uses: actions/setup-node@v4
|
|
73
|
+
with:
|
|
74
|
+
node-version: '22'
|
|
75
|
+
- run: npx @google/jules-fleet analyze --goal "\${{ inputs.goal }}" --milestone "\${{ inputs.milestone }}"
|
|
76
|
+
env:
|
|
77
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
78
|
+
JULES_API_KEY: \${{ secrets.JULES_API_KEY }}
|
|
79
|
+
`
|
|
80
|
+
};
|
|
81
|
+
// src/init/templates/dispatch.ts
|
|
82
|
+
var FLEET_DISPATCH_TEMPLATE = {
|
|
83
|
+
repoPath: ".github/workflows/fleet-dispatch.yml",
|
|
84
|
+
content: `# Generated by @google/jules-fleet init
|
|
85
|
+
# https://github.com/google-labs-code/jules-sdk
|
|
86
|
+
|
|
87
|
+
name: Fleet Dispatch
|
|
88
|
+
|
|
89
|
+
on:
|
|
90
|
+
schedule:
|
|
91
|
+
- cron: '0 6 * * *' # Daily at 6am UTC — edit to your preference
|
|
92
|
+
workflow_dispatch:
|
|
93
|
+
inputs:
|
|
94
|
+
milestone:
|
|
95
|
+
description: 'Milestone ID to dispatch'
|
|
96
|
+
type: string
|
|
97
|
+
required: true
|
|
98
|
+
|
|
99
|
+
concurrency:
|
|
100
|
+
group: fleet-dispatch
|
|
101
|
+
cancel-in-progress: false
|
|
102
|
+
|
|
103
|
+
jobs:
|
|
104
|
+
dispatch:
|
|
105
|
+
runs-on: ubuntu-latest
|
|
106
|
+
permissions:
|
|
107
|
+
contents: read
|
|
108
|
+
issues: write
|
|
109
|
+
steps:
|
|
110
|
+
- uses: actions/checkout@v4
|
|
111
|
+
- uses: actions/setup-node@v4
|
|
112
|
+
with:
|
|
113
|
+
node-version: '22'
|
|
114
|
+
- run: npx @google/jules-fleet dispatch --milestone \${{ inputs.milestone }}
|
|
115
|
+
env:
|
|
116
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
117
|
+
JULES_API_KEY: \${{ secrets.JULES_API_KEY }}
|
|
118
|
+
`
|
|
119
|
+
};
|
|
120
|
+
// src/init/templates/merge.ts
|
|
121
|
+
var FLEET_MERGE_TEMPLATE = {
|
|
122
|
+
repoPath: ".github/workflows/fleet-merge.yml",
|
|
123
|
+
content: `# Generated by @google/jules-fleet init
|
|
124
|
+
# https://github.com/google-labs-code/jules-sdk
|
|
125
|
+
|
|
126
|
+
name: Fleet Merge
|
|
127
|
+
|
|
128
|
+
on:
|
|
129
|
+
schedule:
|
|
130
|
+
- cron: '0 */4 * * *' # Every 4 hours — edit to your preference
|
|
131
|
+
workflow_dispatch:
|
|
132
|
+
inputs:
|
|
133
|
+
mode:
|
|
134
|
+
description: 'PR selection mode'
|
|
135
|
+
type: choice
|
|
136
|
+
options:
|
|
137
|
+
- label
|
|
138
|
+
- fleet-run
|
|
139
|
+
default: 'label'
|
|
140
|
+
fleet_run_id:
|
|
141
|
+
description: 'Fleet run ID (required for fleet-run mode)'
|
|
142
|
+
type: string
|
|
143
|
+
default: ''
|
|
144
|
+
re_dispatch:
|
|
145
|
+
description: 'Auto re-dispatch on merge conflict'
|
|
146
|
+
type: boolean
|
|
147
|
+
default: true
|
|
148
|
+
|
|
149
|
+
concurrency:
|
|
150
|
+
group: fleet-merge
|
|
151
|
+
cancel-in-progress: false
|
|
152
|
+
|
|
153
|
+
jobs:
|
|
154
|
+
merge:
|
|
155
|
+
runs-on: ubuntu-latest
|
|
156
|
+
permissions:
|
|
157
|
+
contents: write
|
|
158
|
+
pull-requests: write
|
|
159
|
+
issues: write
|
|
160
|
+
steps:
|
|
161
|
+
- uses: actions/checkout@v4
|
|
162
|
+
- uses: actions/setup-node@v4
|
|
163
|
+
with:
|
|
164
|
+
node-version: '22'
|
|
165
|
+
- run: |
|
|
166
|
+
REDISPATCH_FLAG=""
|
|
167
|
+
if [ "\${{ inputs.re_dispatch }}" = "true" ]; then
|
|
168
|
+
REDISPATCH_FLAG="--re-dispatch"
|
|
169
|
+
fi
|
|
170
|
+
npx @google/jules-fleet merge \\
|
|
171
|
+
--mode \${{ inputs.mode || 'label' }} \\
|
|
172
|
+
--run-id "\${{ inputs.fleet_run_id }}" \\
|
|
173
|
+
$REDISPATCH_FLAG
|
|
174
|
+
env:
|
|
175
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
176
|
+
JULES_API_KEY: \${{ secrets.JULES_API_KEY }}
|
|
177
|
+
`
|
|
178
|
+
};
|
|
179
|
+
// src/init/templates.ts
|
|
180
|
+
var WORKFLOW_TEMPLATES = [
|
|
181
|
+
FLEET_ANALYZE_TEMPLATE,
|
|
182
|
+
FLEET_DISPATCH_TEMPLATE,
|
|
183
|
+
FLEET_MERGE_TEMPLATE
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
// src/init/templates/example-goal.ts
|
|
187
|
+
var EXAMPLE_GOAL = `---
|
|
188
|
+
milestone: "1"
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
# Example Fleet Goal
|
|
192
|
+
|
|
193
|
+
Analyze the codebase for potential improvements and create
|
|
194
|
+
issues for the engineering team.
|
|
195
|
+
|
|
196
|
+
## Tools
|
|
197
|
+
- Test Coverage: \`npx vitest --coverage --json\`
|
|
198
|
+
|
|
199
|
+
## Assessment Hints
|
|
200
|
+
- Focus on missing error handling in API routes
|
|
201
|
+
- Look for hardcoded configuration values
|
|
202
|
+
|
|
203
|
+
## Insight Hints
|
|
204
|
+
- Report on overall test coverage metrics
|
|
205
|
+
- Note any unusually complex functions (cyclomatic complexity)
|
|
206
|
+
|
|
207
|
+
## Constraints
|
|
208
|
+
- Do NOT propose changes already covered by open issues
|
|
209
|
+
- Do NOT propose changes rejected in recently closed issues
|
|
210
|
+
- Keep tasks small and isolated — one logical change per issue
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
// src/init/ops/create-branch.ts
|
|
214
|
+
async function createBranch(octokit, owner, repo, baseBranch, emit) {
|
|
215
|
+
const { data: refData } = await octokit.rest.git.getRef({
|
|
216
|
+
owner,
|
|
217
|
+
repo,
|
|
218
|
+
ref: `heads/${baseBranch}`
|
|
219
|
+
});
|
|
220
|
+
const baseSha = refData.object.sha;
|
|
221
|
+
const branchName = `fleet-init-${Date.now()}`;
|
|
222
|
+
emit({ type: "init:branch:creating", name: branchName, base: baseBranch });
|
|
223
|
+
try {
|
|
224
|
+
await octokit.rest.git.createRef({
|
|
225
|
+
owner,
|
|
226
|
+
repo,
|
|
227
|
+
ref: `refs/heads/${branchName}`,
|
|
228
|
+
sha: baseSha
|
|
229
|
+
});
|
|
230
|
+
emit({ type: "init:branch:created", name: branchName });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return fail("BRANCH_CREATE_FAILED", `Failed to create branch "${branchName}": ${error instanceof Error ? error.message : error}`, true);
|
|
233
|
+
}
|
|
234
|
+
return { branchName, baseSha };
|
|
235
|
+
}
|
|
236
|
+
function isBranchResult(result) {
|
|
237
|
+
return "success" in result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/init/ops/commit-files.ts
|
|
241
|
+
async function commitFiles(ctx, templates, exampleGoal) {
|
|
242
|
+
const filesCreated = [];
|
|
243
|
+
for (const tmpl of templates) {
|
|
244
|
+
try {
|
|
245
|
+
await ctx.octokit.rest.repos.createOrUpdateFileContents({
|
|
246
|
+
owner: ctx.owner,
|
|
247
|
+
repo: ctx.repo,
|
|
248
|
+
path: tmpl.repoPath,
|
|
249
|
+
message: `chore: add ${tmpl.repoPath}`,
|
|
250
|
+
content: Buffer.from(tmpl.content).toString("base64"),
|
|
251
|
+
branch: ctx.branchName
|
|
252
|
+
});
|
|
253
|
+
filesCreated.push(tmpl.repoPath);
|
|
254
|
+
ctx.emit({ type: "init:file:committed", path: tmpl.repoPath });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
257
|
+
if (status === 422) {
|
|
258
|
+
ctx.emit({ type: "init:file:skipped", path: tmpl.repoPath, reason: "already exists" });
|
|
259
|
+
} else {
|
|
260
|
+
return fail("FILE_COMMIT_FAILED", `Failed to commit "${tmpl.repoPath}": ${error instanceof Error ? error.message : error}`, true);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
await ctx.octokit.rest.repos.createOrUpdateFileContents({
|
|
266
|
+
owner: ctx.owner,
|
|
267
|
+
repo: ctx.repo,
|
|
268
|
+
path: ".fleet/goals/example.md",
|
|
269
|
+
message: "chore: add example fleet goal",
|
|
270
|
+
content: Buffer.from(exampleGoal).toString("base64"),
|
|
271
|
+
branch: ctx.branchName
|
|
272
|
+
});
|
|
273
|
+
filesCreated.push(".fleet/goals/example.md");
|
|
274
|
+
ctx.emit({ type: "init:file:committed", path: ".fleet/goals/example.md" });
|
|
275
|
+
} catch (error) {
|
|
276
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
277
|
+
if (status !== 422) {
|
|
278
|
+
ctx.emit({
|
|
279
|
+
type: "init:file:skipped",
|
|
280
|
+
path: ".fleet/goals/example.md",
|
|
281
|
+
reason: `${error instanceof Error ? error.message : error}`
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return filesCreated;
|
|
286
|
+
}
|
|
287
|
+
function isCommitResult(result) {
|
|
288
|
+
return !Array.isArray(result);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/init/ops/pr-body.ts
|
|
292
|
+
function buildInitPRBody(filesCreated) {
|
|
293
|
+
return [
|
|
294
|
+
"## Fleet Initialization",
|
|
295
|
+
"",
|
|
296
|
+
"This PR adds the fleet workflow files for automated issue dispatch, merge, and analysis.",
|
|
297
|
+
"",
|
|
298
|
+
"### Files added",
|
|
299
|
+
...filesCreated.map((f) => `- \`${f}\``),
|
|
300
|
+
"",
|
|
301
|
+
"### Next steps",
|
|
302
|
+
"1. Merge this PR",
|
|
303
|
+
"2. Add `JULES_API_KEY` to your repo secrets",
|
|
304
|
+
"3. Create milestones and issues with the `fleet` label",
|
|
305
|
+
"4. Run `jules-fleet configure labels` to set up labels (or they were already created)"
|
|
306
|
+
].join(`
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/init/ops/create-pr.ts
|
|
311
|
+
async function createInitPR(ctx, baseBranch, filesCreated) {
|
|
312
|
+
ctx.emit({ type: "init:pr:creating" });
|
|
313
|
+
try {
|
|
314
|
+
const { data: pr } = await ctx.octokit.rest.pulls.create({
|
|
315
|
+
owner: ctx.owner,
|
|
316
|
+
repo: ctx.repo,
|
|
317
|
+
title: "chore: initialize fleet workflows",
|
|
318
|
+
body: buildInitPRBody(filesCreated),
|
|
319
|
+
head: ctx.branchName,
|
|
320
|
+
base: baseBranch
|
|
321
|
+
});
|
|
322
|
+
ctx.emit({ type: "init:pr:created", url: pr.html_url, number: pr.number });
|
|
323
|
+
return { prUrl: pr.html_url, prNumber: pr.number };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return fail("PR_CREATE_FAILED", `Failed to create PR: ${error instanceof Error ? error.message : error}`, true);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function isPRResult(result) {
|
|
329
|
+
return "success" in result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/init/handler.ts
|
|
333
|
+
class InitHandler {
|
|
334
|
+
octokit;
|
|
335
|
+
emit;
|
|
336
|
+
labelConfigurator;
|
|
337
|
+
constructor(deps) {
|
|
338
|
+
this.octokit = deps.octokit;
|
|
339
|
+
this.emit = deps.emit ?? (() => {});
|
|
340
|
+
this.labelConfigurator = deps.labelConfigurator;
|
|
341
|
+
}
|
|
342
|
+
async execute(input) {
|
|
343
|
+
try {
|
|
344
|
+
const { owner, repoName: repo, baseBranch } = input;
|
|
345
|
+
this.emit({ type: "init:start", owner, repo });
|
|
346
|
+
const branchResult = await createBranch(this.octokit, owner, repo, baseBranch, this.emit);
|
|
347
|
+
if (isBranchResult(branchResult))
|
|
348
|
+
return branchResult;
|
|
349
|
+
const { branchName } = branchResult;
|
|
350
|
+
const ctx = {
|
|
351
|
+
octokit: this.octokit,
|
|
352
|
+
owner,
|
|
353
|
+
repo,
|
|
354
|
+
branchName,
|
|
355
|
+
emit: this.emit
|
|
356
|
+
};
|
|
357
|
+
const filesResult = await commitFiles(ctx, WORKFLOW_TEMPLATES, EXAMPLE_GOAL);
|
|
358
|
+
if (isCommitResult(filesResult))
|
|
359
|
+
return filesResult;
|
|
360
|
+
const filesCreated = filesResult;
|
|
361
|
+
if (filesCreated.length === 0) {
|
|
362
|
+
this.emit({
|
|
363
|
+
type: "error",
|
|
364
|
+
code: "ALREADY_INITIALIZED",
|
|
365
|
+
message: "All fleet files already exist — nothing to commit.",
|
|
366
|
+
suggestion: "This repo appears to be already initialized. Use jules-fleet configure to update settings."
|
|
367
|
+
});
|
|
368
|
+
return fail("FILE_COMMIT_FAILED", "All fleet files already exist — nothing to commit.", false, "This repo appears to be already initialized. Use jules-fleet configure to update settings.");
|
|
369
|
+
}
|
|
370
|
+
const prResult = await createInitPR(ctx, baseBranch, filesCreated);
|
|
371
|
+
if (isPRResult(prResult))
|
|
372
|
+
return prResult;
|
|
373
|
+
const { prUrl, prNumber } = prResult;
|
|
374
|
+
let labelsCreated = [];
|
|
375
|
+
if (this.labelConfigurator) {
|
|
376
|
+
const labelResult = await this.labelConfigurator.execute({
|
|
377
|
+
resource: "labels",
|
|
378
|
+
action: "create",
|
|
379
|
+
owner,
|
|
380
|
+
repo
|
|
381
|
+
});
|
|
382
|
+
labelsCreated = labelResult.success ? labelResult.data.created : [];
|
|
383
|
+
}
|
|
384
|
+
this.emit({
|
|
385
|
+
type: "init:done",
|
|
386
|
+
prUrl,
|
|
387
|
+
files: filesCreated,
|
|
388
|
+
labels: labelsCreated
|
|
389
|
+
});
|
|
390
|
+
return ok({ prUrl, prNumber, filesCreated, labelsCreated });
|
|
391
|
+
} catch (error) {
|
|
392
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/configure/labels.ts
|
|
398
|
+
var FLEET_LABELS = [
|
|
399
|
+
{
|
|
400
|
+
name: "fleet-merge-ready",
|
|
401
|
+
color: "0e8a16",
|
|
402
|
+
description: "Ready for fleet sequential merge"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: "fleet",
|
|
406
|
+
color: "1d76db",
|
|
407
|
+
description: "Fleet-managed issue"
|
|
408
|
+
}
|
|
409
|
+
];
|
|
410
|
+
|
|
411
|
+
// src/configure/handler.ts
|
|
412
|
+
class ConfigureHandler {
|
|
413
|
+
octokit;
|
|
414
|
+
emit;
|
|
415
|
+
constructor(deps) {
|
|
416
|
+
this.octokit = deps.octokit;
|
|
417
|
+
this.emit = deps.emit ?? (() => {});
|
|
418
|
+
}
|
|
419
|
+
async execute(input) {
|
|
420
|
+
try {
|
|
421
|
+
this.emit({
|
|
422
|
+
type: "configure:start",
|
|
423
|
+
resource: input.resource,
|
|
424
|
+
owner: input.owner,
|
|
425
|
+
repo: input.repo
|
|
426
|
+
});
|
|
427
|
+
if (input.resource === "labels") {
|
|
428
|
+
const result = input.action === "create" ? await this.createLabels(input.owner, input.repo) : await this.deleteLabels(input.owner, input.repo);
|
|
429
|
+
this.emit({ type: "configure:done" });
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
return fail("UNKNOWN_ERROR", `Unknown resource: ${input.resource}`, false);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return fail("UNKNOWN_ERROR", error instanceof Error ? error.message : String(error), false);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async createLabels(owner, repo) {
|
|
438
|
+
const created = [];
|
|
439
|
+
const skipped = [];
|
|
440
|
+
for (const label of FLEET_LABELS) {
|
|
441
|
+
try {
|
|
442
|
+
await this.octokit.rest.issues.createLabel({
|
|
443
|
+
owner,
|
|
444
|
+
repo,
|
|
445
|
+
name: label.name,
|
|
446
|
+
color: label.color,
|
|
447
|
+
description: label.description
|
|
448
|
+
});
|
|
449
|
+
created.push(label.name);
|
|
450
|
+
this.emit({ type: "configure:label:created", name: label.name });
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
453
|
+
if (status === 422) {
|
|
454
|
+
skipped.push(label.name);
|
|
455
|
+
this.emit({ type: "configure:label:exists", name: label.name });
|
|
456
|
+
} else {
|
|
457
|
+
return fail("GITHUB_API_ERROR", `Failed to create label "${label.name}": ${error instanceof Error ? error.message : error}`, true);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return ok({ created, deleted: [], skipped });
|
|
462
|
+
}
|
|
463
|
+
async deleteLabels(owner, repo) {
|
|
464
|
+
const deleted = [];
|
|
465
|
+
const skipped = [];
|
|
466
|
+
for (const label of FLEET_LABELS) {
|
|
467
|
+
try {
|
|
468
|
+
await this.octokit.rest.issues.deleteLabel({
|
|
469
|
+
owner,
|
|
470
|
+
repo,
|
|
471
|
+
name: label.name
|
|
472
|
+
});
|
|
473
|
+
deleted.push(label.name);
|
|
474
|
+
this.emit({ type: "configure:label:created", name: label.name });
|
|
475
|
+
} catch (error) {
|
|
476
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : 0;
|
|
477
|
+
if (status === 404) {
|
|
478
|
+
skipped.push(label.name);
|
|
479
|
+
this.emit({ type: "configure:label:exists", name: label.name });
|
|
480
|
+
} else {
|
|
481
|
+
return fail("GITHUB_API_ERROR", `Failed to delete label "${label.name}": ${error instanceof Error ? error.message : error}`, true);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return ok({ created: [], deleted, skipped });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/shared/auth/octokit.ts
|
|
490
|
+
import { Octokit } from "octokit";
|
|
491
|
+
import { createAppAuth } from "@octokit/auth-app";
|
|
492
|
+
|
|
493
|
+
// src/shared/auth/cache-plugin.ts
|
|
494
|
+
function cachePlugin(octokit) {
|
|
495
|
+
const cache = new Map;
|
|
496
|
+
octokit.hook.wrap("request", async (request, options) => {
|
|
497
|
+
const key = `${options.method} ${options.url}`;
|
|
498
|
+
const cached = cache.get(key);
|
|
499
|
+
if (cached) {
|
|
500
|
+
options.headers = {
|
|
501
|
+
...options.headers,
|
|
502
|
+
"if-none-match": cached.etag
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const response = await request(options);
|
|
507
|
+
const etag = response.headers.etag;
|
|
508
|
+
if (etag) {
|
|
509
|
+
cache.set(key, { etag, data: response.data });
|
|
510
|
+
}
|
|
511
|
+
return response;
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (error.status === 304 && cached) {
|
|
514
|
+
return { ...error.response, data: cached.data, status: 200 };
|
|
515
|
+
}
|
|
516
|
+
throw error;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/shared/auth/resolve-key.ts
|
|
522
|
+
function resolvePrivateKey(base64Value, rawValue) {
|
|
523
|
+
if (base64Value) {
|
|
524
|
+
return Buffer.from(base64Value, "base64").toString("utf-8");
|
|
525
|
+
}
|
|
526
|
+
if (rawValue) {
|
|
527
|
+
return rawValue.replace(/\\n/g, `
|
|
528
|
+
`);
|
|
529
|
+
}
|
|
530
|
+
throw new Error("No private key provided. Set GITHUB_APP_PRIVATE_KEY_BASE64 (recommended) or GITHUB_APP_PRIVATE_KEY.");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/shared/auth/octokit.ts
|
|
534
|
+
var CachedOctokit = Octokit.plugin(cachePlugin);
|
|
535
|
+
function getAuthOptions() {
|
|
536
|
+
const appId = process.env.GITHUB_APP_ID;
|
|
537
|
+
const privateKeyBase64 = process.env.GITHUB_APP_PRIVATE_KEY_BASE64;
|
|
538
|
+
const privateKeyRaw = process.env.GITHUB_APP_PRIVATE_KEY;
|
|
539
|
+
const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
|
|
540
|
+
if (appId && (privateKeyBase64 || privateKeyRaw) && installationId) {
|
|
541
|
+
return {
|
|
542
|
+
authStrategy: createAppAuth,
|
|
543
|
+
auth: {
|
|
544
|
+
appId,
|
|
545
|
+
privateKey: resolvePrivateKey(privateKeyBase64, privateKeyRaw),
|
|
546
|
+
installationId: Number(installationId)
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
const token = process.env.GITHUB_TOKEN;
|
|
551
|
+
if (token) {
|
|
552
|
+
return { auth: token };
|
|
553
|
+
}
|
|
554
|
+
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.");
|
|
555
|
+
}
|
|
556
|
+
function createFleetOctokit() {
|
|
557
|
+
return new CachedOctokit(getAuthOptions());
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/shared/ui/interactive.ts
|
|
561
|
+
import * as p from "@clack/prompts";
|
|
562
|
+
|
|
563
|
+
// src/shared/ui/render/init.ts
|
|
564
|
+
function renderInitEvent(event, ctx) {
|
|
565
|
+
switch (event.type) {
|
|
566
|
+
case "init:start":
|
|
567
|
+
ctx.info(`Initializing fleet for ${event.owner}/${event.repo}`);
|
|
568
|
+
break;
|
|
569
|
+
case "init:branch:creating":
|
|
570
|
+
ctx.startSpinner(`Creating branch ${event.name} from ${event.base}`);
|
|
571
|
+
break;
|
|
572
|
+
case "init:branch:created":
|
|
573
|
+
ctx.stopSpinner(`Branch ${event.name} created`);
|
|
574
|
+
break;
|
|
575
|
+
case "init:file:committed":
|
|
576
|
+
ctx.info(` ✓ ${event.path}`);
|
|
577
|
+
break;
|
|
578
|
+
case "init:file:skipped":
|
|
579
|
+
ctx.warn(` ⊘ ${event.path} — ${event.reason}`);
|
|
580
|
+
break;
|
|
581
|
+
case "init:pr:creating":
|
|
582
|
+
ctx.startSpinner("Creating pull request…");
|
|
583
|
+
break;
|
|
584
|
+
case "init:pr:created":
|
|
585
|
+
ctx.stopSpinner(`PR #${event.number} created`);
|
|
586
|
+
ctx.info(` ${event.url}`);
|
|
587
|
+
break;
|
|
588
|
+
case "init:done":
|
|
589
|
+
ctx.success(`Fleet initialized — PR: ${event.prUrl}`);
|
|
590
|
+
break;
|
|
591
|
+
case "init:auth:detected":
|
|
592
|
+
ctx.success(`Auth: ${event.method === "token" ? "GITHUB_TOKEN" : "GitHub App"}`);
|
|
593
|
+
break;
|
|
594
|
+
case "init:secret:uploading":
|
|
595
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
596
|
+
break;
|
|
597
|
+
case "init:secret:uploaded":
|
|
598
|
+
ctx.stopSpinner(`Secret ${event.name} saved`);
|
|
599
|
+
break;
|
|
600
|
+
case "init:secret:skipped":
|
|
601
|
+
ctx.warn(` ⊘ ${event.name} — ${event.reason}`);
|
|
602
|
+
break;
|
|
603
|
+
case "init:dry-run":
|
|
604
|
+
ctx.info("Would create:");
|
|
605
|
+
event.files.forEach((f) => ctx.message(` ${f}`));
|
|
606
|
+
break;
|
|
607
|
+
case "init:already-initialized":
|
|
608
|
+
ctx.warn("Repository is already initialized");
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// src/shared/ui/render/configure.ts
|
|
614
|
+
function renderConfigureEvent(event, ctx) {
|
|
615
|
+
switch (event.type) {
|
|
616
|
+
case "configure:start":
|
|
617
|
+
ctx.info(`Configuring ${event.resource} for ${event.owner}/${event.repo}`);
|
|
618
|
+
break;
|
|
619
|
+
case "configure:label:created":
|
|
620
|
+
ctx.info(` ✓ Label "${event.name}" created`);
|
|
621
|
+
break;
|
|
622
|
+
case "configure:label:exists":
|
|
623
|
+
ctx.warn(` ⊘ Label "${event.name}" already exists`);
|
|
624
|
+
break;
|
|
625
|
+
case "configure:secret:uploading":
|
|
626
|
+
ctx.startSpinner(`Uploading secret ${event.name}…`);
|
|
627
|
+
break;
|
|
628
|
+
case "configure:secret:uploaded":
|
|
629
|
+
ctx.stopSpinner(`Secret ${event.name} uploaded`);
|
|
630
|
+
break;
|
|
631
|
+
case "configure:done":
|
|
632
|
+
ctx.success("Configuration complete");
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/shared/ui/session-url.ts
|
|
638
|
+
var JULES_BASE_URL = "https://jules.google.com";
|
|
639
|
+
function sessionUrl(sessionId) {
|
|
640
|
+
return `${JULES_BASE_URL}/sessions/${sessionId}`;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/shared/ui/render/analyze.ts
|
|
644
|
+
function renderAnalyzeEvent(event, ctx) {
|
|
645
|
+
switch (event.type) {
|
|
646
|
+
case "analyze:start":
|
|
647
|
+
ctx.info(`Analyzing ${event.goalCount} goal(s) for ${event.owner}/${event.repo}`);
|
|
648
|
+
break;
|
|
649
|
+
case "analyze:goal:start":
|
|
650
|
+
if (event.total > 1) {
|
|
651
|
+
ctx.step(`[${event.index}/${event.total}] ${event.file}`);
|
|
652
|
+
} else {
|
|
653
|
+
ctx.step(event.file);
|
|
654
|
+
}
|
|
655
|
+
if (event.milestone)
|
|
656
|
+
ctx.info(` Milestone: ${event.milestone}`);
|
|
657
|
+
break;
|
|
658
|
+
case "analyze:milestone:resolved":
|
|
659
|
+
ctx.info(` Milestone "${event.title}" (#${event.id})`);
|
|
660
|
+
break;
|
|
661
|
+
case "analyze:context:fetched":
|
|
662
|
+
ctx.info(` Context: ${event.openIssues} open, ${event.closedIssues} closed, ${event.prs} PRs`);
|
|
663
|
+
break;
|
|
664
|
+
case "analyze:session:dispatching":
|
|
665
|
+
ctx.startSpinner(`Dispatching session for ${event.goal}…`);
|
|
666
|
+
break;
|
|
667
|
+
case "analyze:session:started":
|
|
668
|
+
ctx.stopSpinner(`Session started: ${event.id}`);
|
|
669
|
+
ctx.info(` ${sessionUrl(event.id)}`);
|
|
670
|
+
break;
|
|
671
|
+
case "analyze:session:failed":
|
|
672
|
+
ctx.stopSpinner();
|
|
673
|
+
ctx.error(` Failed: ${event.error}`);
|
|
674
|
+
break;
|
|
675
|
+
case "analyze:done":
|
|
676
|
+
ctx.success(`Analysis complete — ${event.sessionsStarted} session(s) from ${event.goalsProcessed} goal(s)`);
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/shared/ui/render/dispatch.ts
|
|
682
|
+
function renderDispatchEvent(event, ctx) {
|
|
683
|
+
switch (event.type) {
|
|
684
|
+
case "dispatch:start":
|
|
685
|
+
ctx.info(`Dispatching from milestone ${event.milestone}`);
|
|
686
|
+
break;
|
|
687
|
+
case "dispatch:scanning":
|
|
688
|
+
ctx.startSpinner("Scanning for fleet issues…");
|
|
689
|
+
break;
|
|
690
|
+
case "dispatch:found":
|
|
691
|
+
ctx.stopSpinner(`Found ${event.count} undispatched issue(s)`);
|
|
692
|
+
break;
|
|
693
|
+
case "dispatch:issue:dispatching":
|
|
694
|
+
ctx.startSpinner(`#${event.number}: ${event.title}`);
|
|
695
|
+
break;
|
|
696
|
+
case "dispatch:issue:dispatched":
|
|
697
|
+
ctx.stopSpinner(`#${event.number} → session ${event.sessionId}`);
|
|
698
|
+
ctx.info(` ${sessionUrl(event.sessionId)}`);
|
|
699
|
+
break;
|
|
700
|
+
case "dispatch:issue:skipped":
|
|
701
|
+
ctx.warn(` ⊘ #${event.number}: ${event.reason}`);
|
|
702
|
+
break;
|
|
703
|
+
case "dispatch:done":
|
|
704
|
+
ctx.success(`Dispatch complete — ${event.dispatched} dispatched, ${event.skipped} skipped`);
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/shared/ui/render/merge.ts
|
|
710
|
+
function renderMergeEvent(event, ctx) {
|
|
711
|
+
switch (event.type) {
|
|
712
|
+
case "merge:start":
|
|
713
|
+
ctx.info(`Merging ${event.prCount} PR(s) in ${event.owner}/${event.repo} [${event.mode}]`);
|
|
714
|
+
break;
|
|
715
|
+
case "merge:no-prs":
|
|
716
|
+
ctx.info("No PRs ready to merge.");
|
|
717
|
+
break;
|
|
718
|
+
case "merge:pr:processing":
|
|
719
|
+
ctx.startSpinner(`PR #${event.number}: ${event.title}${event.retry ? ` (retry ${event.retry})` : ""}`);
|
|
720
|
+
break;
|
|
721
|
+
case "merge:branch:updating":
|
|
722
|
+
ctx.startSpinner(`Updating branch for PR #${event.prNumber}…`);
|
|
723
|
+
break;
|
|
724
|
+
case "merge:branch:updated":
|
|
725
|
+
ctx.stopSpinner(`Branch updated for PR #${event.prNumber}`);
|
|
726
|
+
break;
|
|
727
|
+
case "merge:ci:waiting":
|
|
728
|
+
ctx.startSpinner(`Waiting for CI on PR #${event.prNumber}…`);
|
|
729
|
+
break;
|
|
730
|
+
case "merge:ci:check": {
|
|
731
|
+
const icon = event.status === "pass" ? "✓" : event.status === "fail" ? "✗" : "…";
|
|
732
|
+
const dur = event.duration ? ` (${event.duration}s)` : "";
|
|
733
|
+
ctx.info(` ${icon} ${event.name}${dur}`);
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
case "merge:ci:passed":
|
|
737
|
+
ctx.stopSpinner(`CI passed for PR #${event.prNumber}`);
|
|
738
|
+
break;
|
|
739
|
+
case "merge:ci:failed":
|
|
740
|
+
ctx.stopSpinner(`CI failed for PR #${event.prNumber}`);
|
|
741
|
+
break;
|
|
742
|
+
case "merge:ci:timeout":
|
|
743
|
+
ctx.stopSpinner(`CI timed out for PR #${event.prNumber}`);
|
|
744
|
+
break;
|
|
745
|
+
case "merge:ci:none":
|
|
746
|
+
ctx.stopSpinner(`No CI checks for PR #${event.prNumber}`);
|
|
747
|
+
break;
|
|
748
|
+
case "merge:pr:merging":
|
|
749
|
+
ctx.startSpinner(`Merging PR #${event.prNumber}…`);
|
|
750
|
+
break;
|
|
751
|
+
case "merge:pr:merged":
|
|
752
|
+
ctx.stopSpinner(`PR #${event.prNumber} merged ✓`);
|
|
753
|
+
break;
|
|
754
|
+
case "merge:pr:skipped":
|
|
755
|
+
ctx.warn(` ⊘ PR #${event.prNumber}: ${event.reason}`);
|
|
756
|
+
break;
|
|
757
|
+
case "merge:conflict:detected":
|
|
758
|
+
ctx.stopSpinner(`Conflict detected on PR #${event.prNumber}`);
|
|
759
|
+
break;
|
|
760
|
+
case "merge:redispatch:start":
|
|
761
|
+
ctx.startSpinner(`Re-dispatching PR #${event.oldPr}…`);
|
|
762
|
+
break;
|
|
763
|
+
case "merge:redispatch:waiting":
|
|
764
|
+
ctx.startSpinner(`Waiting for re-dispatched PR (was #${event.oldPr})…`);
|
|
765
|
+
break;
|
|
766
|
+
case "merge:redispatch:done":
|
|
767
|
+
ctx.stopSpinner(`Re-dispatched: #${event.oldPr} → #${event.newPr}`);
|
|
768
|
+
break;
|
|
769
|
+
case "merge:done":
|
|
770
|
+
ctx.success(`Merge complete — ${event.merged.length} merged, ${event.skipped.length} skipped`);
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/shared/ui/render/error.ts
|
|
776
|
+
function renderErrorEvent(event, ctx) {
|
|
777
|
+
ctx.stopSpinner();
|
|
778
|
+
ctx.error(`[${event.code}] ${event.message}`);
|
|
779
|
+
if (event.suggestion)
|
|
780
|
+
ctx.info(` \uD83D\uDCA1 ${event.suggestion}`);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/shared/ui/interactive.ts
|
|
784
|
+
class InteractiveRenderer {
|
|
785
|
+
spinner = null;
|
|
786
|
+
ctx = {
|
|
787
|
+
info: (msg) => p.log.info(msg),
|
|
788
|
+
success: (msg) => p.log.success(msg),
|
|
789
|
+
warn: (msg) => p.log.warn(msg),
|
|
790
|
+
error: (msg) => p.log.error(msg),
|
|
791
|
+
message: (msg) => p.log.message(msg),
|
|
792
|
+
step: (msg) => p.log.step(msg),
|
|
793
|
+
startSpinner: (msg) => this.startSpinner(msg),
|
|
794
|
+
stopSpinner: (msg) => this.stopSpinner(msg)
|
|
795
|
+
};
|
|
796
|
+
start(title) {
|
|
797
|
+
p.intro(title);
|
|
798
|
+
}
|
|
799
|
+
end(message) {
|
|
800
|
+
this.stopSpinner();
|
|
801
|
+
p.outro(message);
|
|
802
|
+
}
|
|
803
|
+
error(message) {
|
|
804
|
+
this.stopSpinner();
|
|
805
|
+
p.log.error(message);
|
|
806
|
+
}
|
|
807
|
+
render(event) {
|
|
808
|
+
if (event.type.startsWith("init:"))
|
|
809
|
+
return renderInitEvent(event, this.ctx);
|
|
810
|
+
if (event.type.startsWith("configure:"))
|
|
811
|
+
return renderConfigureEvent(event, this.ctx);
|
|
812
|
+
if (event.type.startsWith("analyze:"))
|
|
813
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
814
|
+
if (event.type.startsWith("dispatch:"))
|
|
815
|
+
return renderDispatchEvent(event, this.ctx);
|
|
816
|
+
if (event.type.startsWith("merge:"))
|
|
817
|
+
return renderMergeEvent(event, this.ctx);
|
|
818
|
+
if (event.type === "error")
|
|
819
|
+
return renderErrorEvent(event, this.ctx);
|
|
820
|
+
}
|
|
821
|
+
startSpinner(message) {
|
|
822
|
+
this.stopSpinner();
|
|
823
|
+
this.spinner = p.spinner();
|
|
824
|
+
this.spinner.start(message);
|
|
825
|
+
}
|
|
826
|
+
stopSpinner(message) {
|
|
827
|
+
if (this.spinner) {
|
|
828
|
+
this.spinner.stop(message);
|
|
829
|
+
this.spinner = null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/shared/ui/plain.ts
|
|
835
|
+
class PlainRenderer {
|
|
836
|
+
ctx = {
|
|
837
|
+
info: (msg) => console.log(msg),
|
|
838
|
+
success: (msg) => console.log(msg),
|
|
839
|
+
warn: (msg) => console.log(msg),
|
|
840
|
+
error: (msg) => console.error(msg),
|
|
841
|
+
message: (msg) => console.log(msg),
|
|
842
|
+
step: (msg) => console.log(msg),
|
|
843
|
+
startSpinner: (msg) => console.log(msg),
|
|
844
|
+
stopSpinner: (msg) => {
|
|
845
|
+
if (msg)
|
|
846
|
+
console.log(` ✓ ${msg}`);
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
start(title) {
|
|
850
|
+
console.log(`
|
|
851
|
+
═══ ${title} ═══
|
|
852
|
+
`);
|
|
853
|
+
}
|
|
854
|
+
end(message) {
|
|
855
|
+
console.log(`
|
|
856
|
+
═══ ${message} ═══
|
|
857
|
+
`);
|
|
858
|
+
}
|
|
859
|
+
error(message) {
|
|
860
|
+
console.error(`ERROR: ${message}`);
|
|
861
|
+
}
|
|
862
|
+
render(event) {
|
|
863
|
+
if (event.type.startsWith("init:"))
|
|
864
|
+
return renderInitEvent(event, this.ctx);
|
|
865
|
+
if (event.type.startsWith("configure:"))
|
|
866
|
+
return renderConfigureEvent(event, this.ctx);
|
|
867
|
+
if (event.type.startsWith("analyze:"))
|
|
868
|
+
return renderAnalyzeEvent(event, this.ctx);
|
|
869
|
+
if (event.type.startsWith("dispatch:"))
|
|
870
|
+
return renderDispatchEvent(event, this.ctx);
|
|
871
|
+
if (event.type.startsWith("merge:"))
|
|
872
|
+
return renderMergeEvent(event, this.ctx);
|
|
873
|
+
if (event.type === "error")
|
|
874
|
+
return renderErrorEvent(event, this.ctx);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/shared/ui/index.ts
|
|
879
|
+
function isInteractive() {
|
|
880
|
+
if (process.env.CI === "true")
|
|
881
|
+
return false;
|
|
882
|
+
if (!process.stdout.isTTY)
|
|
883
|
+
return false;
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
function createRenderer(interactive) {
|
|
887
|
+
const useInteractive = interactive ?? isInteractive();
|
|
888
|
+
return useInteractive ? new InteractiveRenderer : new PlainRenderer;
|
|
889
|
+
}
|
|
890
|
+
function createEmitter(renderer) {
|
|
891
|
+
return (event) => renderer.render(event);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/init/wizard/interactive.ts
|
|
895
|
+
import * as p2 from "@clack/prompts";
|
|
896
|
+
|
|
897
|
+
// src/shared/auth/git.ts
|
|
898
|
+
import { exec } from "child_process";
|
|
899
|
+
import { promisify } from "util";
|
|
900
|
+
var execAsync = promisify(exec);
|
|
901
|
+
async function getGitRepoInfo(remoteName = "origin") {
|
|
902
|
+
const ghRepo = process.env.GITHUB_REPOSITORY;
|
|
903
|
+
if (ghRepo) {
|
|
904
|
+
const [owner, repo] = ghRepo.split("/");
|
|
905
|
+
return { owner, repo, fullName: ghRepo };
|
|
906
|
+
}
|
|
907
|
+
const { stdout } = await execAsync(`git remote get-url ${remoteName}`);
|
|
908
|
+
return parseGitRemoteUrl(stdout.trim());
|
|
909
|
+
}
|
|
910
|
+
function parseGitRemoteUrl(remoteUrl) {
|
|
911
|
+
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/(.+?)(\.git)?$/);
|
|
912
|
+
if (sshMatch) {
|
|
913
|
+
const [, owner, repo] = sshMatch;
|
|
914
|
+
return {
|
|
915
|
+
owner,
|
|
916
|
+
repo: repo.replace(/\.git$/, ""),
|
|
917
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const httpsMatch = remoteUrl.match(/https?:\/\/github\.com\/([^/]+)\/(.+?)(\.git)?$/);
|
|
921
|
+
if (httpsMatch) {
|
|
922
|
+
const [, owner, repo] = httpsMatch;
|
|
923
|
+
return {
|
|
924
|
+
owner,
|
|
925
|
+
repo: repo.replace(/\.git$/, ""),
|
|
926
|
+
fullName: `${owner}/${repo.replace(/\.git$/, "")}`
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
throw new Error(`Unable to parse git remote URL: ${remoteUrl}`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/init/wizard/interactive.ts
|
|
933
|
+
async function runInitWizard(args, emit) {
|
|
934
|
+
let repoSlug = args.repo ?? process.env.GITHUB_REPOSITORY;
|
|
935
|
+
if (!repoSlug) {
|
|
936
|
+
try {
|
|
937
|
+
const info = await getGitRepoInfo();
|
|
938
|
+
repoSlug = info.fullName;
|
|
939
|
+
} catch {}
|
|
940
|
+
}
|
|
941
|
+
if (repoSlug) {
|
|
942
|
+
const confirmed = await p2.confirm({
|
|
943
|
+
message: `Detected repository: ${repoSlug}. Is this correct?`,
|
|
944
|
+
initialValue: true
|
|
945
|
+
});
|
|
946
|
+
if (p2.isCancel(confirmed))
|
|
947
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
948
|
+
if (!confirmed) {
|
|
949
|
+
const manual = await p2.text({
|
|
950
|
+
message: "Enter repository in owner/repo format:",
|
|
951
|
+
validate: (v) => !v || !/^[^/]+\/[^/]+$/.test(v) ? "Must be owner/repo format" : undefined
|
|
952
|
+
});
|
|
953
|
+
if (p2.isCancel(manual))
|
|
954
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
955
|
+
repoSlug = manual;
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
const manual = await p2.text({
|
|
959
|
+
message: "Enter repository in owner/repo format:",
|
|
960
|
+
validate: (v) => !v || !/^[^/]+\/[^/]+$/.test(v) ? "Must be owner/repo format" : undefined
|
|
961
|
+
});
|
|
962
|
+
if (p2.isCancel(manual))
|
|
963
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
964
|
+
repoSlug = manual;
|
|
965
|
+
}
|
|
966
|
+
const [owner, repo] = repoSlug.split("/");
|
|
967
|
+
const baseBranch = args.base ?? "main";
|
|
968
|
+
const hasToken = !!process.env.GITHUB_TOKEN;
|
|
969
|
+
const hasApp = !!(process.env.GITHUB_APP_ID && (process.env.GITHUB_APP_PRIVATE_KEY_BASE64 || process.env.GITHUB_APP_PRIVATE_KEY) && process.env.GITHUB_APP_INSTALLATION_ID);
|
|
970
|
+
let authMethod;
|
|
971
|
+
if (args.auth === "token" || args.auth === "app") {
|
|
972
|
+
authMethod = args.auth;
|
|
973
|
+
} else if (hasApp) {
|
|
974
|
+
authMethod = "app";
|
|
975
|
+
p2.log.success("GitHub App credentials detected");
|
|
976
|
+
} else if (hasToken) {
|
|
977
|
+
authMethod = "token";
|
|
978
|
+
p2.log.success("GITHUB_TOKEN detected");
|
|
979
|
+
} else {
|
|
980
|
+
const authChoice = await p2.select({
|
|
981
|
+
message: "How will Fleet authenticate with GitHub?",
|
|
982
|
+
options: [
|
|
983
|
+
{ value: "token", label: "Personal Access Token (GITHUB_TOKEN)" },
|
|
984
|
+
{ value: "app", label: "GitHub App (recommended for orgs)" }
|
|
985
|
+
]
|
|
986
|
+
});
|
|
987
|
+
if (p2.isCancel(authChoice))
|
|
988
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
989
|
+
authMethod = authChoice;
|
|
990
|
+
if (authMethod === "token") {
|
|
991
|
+
if (!hasToken) {
|
|
992
|
+
const token = await p2.password({
|
|
993
|
+
message: "Paste your GitHub token:"
|
|
994
|
+
});
|
|
995
|
+
if (p2.isCancel(token))
|
|
996
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
997
|
+
process.env.GITHUB_TOKEN = token;
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
if (!process.env.GITHUB_APP_ID) {
|
|
1001
|
+
const appId = await p2.text({ message: "Enter your GitHub App ID:" });
|
|
1002
|
+
if (p2.isCancel(appId))
|
|
1003
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
1004
|
+
process.env.GITHUB_APP_ID = appId;
|
|
1005
|
+
}
|
|
1006
|
+
if (!process.env.GITHUB_APP_INSTALLATION_ID) {
|
|
1007
|
+
const installId = await p2.text({ message: "Enter your Installation ID:" });
|
|
1008
|
+
if (p2.isCancel(installId))
|
|
1009
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
1010
|
+
process.env.GITHUB_APP_INSTALLATION_ID = installId;
|
|
1011
|
+
}
|
|
1012
|
+
if (!process.env.GITHUB_APP_PRIVATE_KEY_BASE64 && !process.env.GITHUB_APP_PRIVATE_KEY) {
|
|
1013
|
+
const key = await p2.password({ message: "Paste your private key (base64 encoded):" });
|
|
1014
|
+
if (p2.isCancel(key))
|
|
1015
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
1016
|
+
process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = key;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
emit({ type: "init:auth:detected", method: authMethod });
|
|
1021
|
+
const secretsToUpload = {};
|
|
1022
|
+
const julesKey = process.env.JULES_API_KEY;
|
|
1023
|
+
if (!julesKey) {
|
|
1024
|
+
const wantKey = await p2.confirm({
|
|
1025
|
+
message: "Fleet needs a JULES_API_KEY to dispatch sessions. Do you have one?",
|
|
1026
|
+
initialValue: true
|
|
1027
|
+
});
|
|
1028
|
+
if (!p2.isCancel(wantKey) && wantKey) {
|
|
1029
|
+
const key = await p2.password({ message: "Enter your Jules API key:" });
|
|
1030
|
+
if (!p2.isCancel(key)) {
|
|
1031
|
+
process.env.JULES_API_KEY = key;
|
|
1032
|
+
secretsToUpload["JULES_API_KEY"] = key;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
} else {
|
|
1036
|
+
p2.log.success("JULES_API_KEY detected");
|
|
1037
|
+
secretsToUpload["JULES_API_KEY"] = julesKey;
|
|
1038
|
+
}
|
|
1039
|
+
const shouldUpload = args["upload-secrets"] ?? true;
|
|
1040
|
+
if (shouldUpload && Object.keys(secretsToUpload).length > 0) {
|
|
1041
|
+
const confirmed = await p2.confirm({
|
|
1042
|
+
message: `Upload ${Object.keys(secretsToUpload).length} secret(s) to GitHub Actions secrets?`,
|
|
1043
|
+
initialValue: true
|
|
1044
|
+
});
|
|
1045
|
+
if (p2.isCancel(confirmed) || !confirmed) {
|
|
1046
|
+
Object.keys(secretsToUpload).forEach((k) => delete secretsToUpload[k]);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
if (shouldUpload && authMethod === "app") {
|
|
1050
|
+
const uploadApp = await p2.confirm({
|
|
1051
|
+
message: "Upload GitHub App credentials to repo secrets?",
|
|
1052
|
+
initialValue: true
|
|
1053
|
+
});
|
|
1054
|
+
if (!p2.isCancel(uploadApp) && uploadApp) {
|
|
1055
|
+
if (process.env.GITHUB_APP_ID)
|
|
1056
|
+
secretsToUpload["GITHUB_APP_ID"] = process.env.GITHUB_APP_ID;
|
|
1057
|
+
if (process.env.GITHUB_APP_PRIVATE_KEY_BASE64) {
|
|
1058
|
+
secretsToUpload["GITHUB_APP_PRIVATE_KEY_BASE64"] = process.env.GITHUB_APP_PRIVATE_KEY_BASE64;
|
|
1059
|
+
}
|
|
1060
|
+
if (process.env.GITHUB_APP_INSTALLATION_ID) {
|
|
1061
|
+
secretsToUpload["GITHUB_APP_INSTALLATION_ID"] = process.env.GITHUB_APP_INSTALLATION_ID;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
const dryRun = args["dry-run"] ?? false;
|
|
1066
|
+
if (!dryRun) {
|
|
1067
|
+
const files = WORKFLOW_TEMPLATES.map((t) => t.repoPath);
|
|
1068
|
+
files.push(".fleet/goals/example.md");
|
|
1069
|
+
p2.log.info([
|
|
1070
|
+
"Fleet will:",
|
|
1071
|
+
` • Create a branch from ${baseBranch}`,
|
|
1072
|
+
` • Commit ${files.length} files`,
|
|
1073
|
+
" • Open a pull request",
|
|
1074
|
+
" • Configure labels (fleet, fleet-merge-ready)"
|
|
1075
|
+
].join(`
|
|
1076
|
+
`));
|
|
1077
|
+
const proceed = await p2.confirm({
|
|
1078
|
+
message: "Create the PR now?",
|
|
1079
|
+
initialValue: true
|
|
1080
|
+
});
|
|
1081
|
+
if (p2.isCancel(proceed))
|
|
1082
|
+
return fail("UNKNOWN_ERROR", "Setup cancelled.", false);
|
|
1083
|
+
if (!proceed) {
|
|
1084
|
+
emit({ type: "init:dry-run", files });
|
|
1085
|
+
return fail("UNKNOWN_ERROR", `Dry run: would create ${files.length} files. Run again to proceed.`, false);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return { owner, repo, baseBranch, authMethod, secretsToUpload, dryRun };
|
|
1089
|
+
}
|
|
1090
|
+
// src/init/wizard/headless.ts
|
|
1091
|
+
async function validateHeadlessInputs(args, emit) {
|
|
1092
|
+
let repoSlug = args.repo ?? process.env.GITHUB_REPOSITORY;
|
|
1093
|
+
if (!repoSlug) {
|
|
1094
|
+
try {
|
|
1095
|
+
const info = await getGitRepoInfo();
|
|
1096
|
+
repoSlug = info.fullName;
|
|
1097
|
+
} catch {
|
|
1098
|
+
return fail("UNKNOWN_ERROR", "Missing repository. Set --repo, GITHUB_REPOSITORY env var, or run from a git repo.", true);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const [owner, repo] = repoSlug.split("/");
|
|
1102
|
+
if (!owner || !repo) {
|
|
1103
|
+
return fail("UNKNOWN_ERROR", `Invalid repo format: "${repoSlug}". Expected owner/repo.`, false);
|
|
1104
|
+
}
|
|
1105
|
+
const hasToken = !!process.env.GITHUB_TOKEN;
|
|
1106
|
+
const hasApp = !!((args["app-id"] || process.env.GITHUB_APP_ID) && (process.env.GITHUB_APP_PRIVATE_KEY_BASE64 || process.env.GITHUB_APP_PRIVATE_KEY) && (args["installation-id"] || process.env.GITHUB_APP_INSTALLATION_ID));
|
|
1107
|
+
let authMethod;
|
|
1108
|
+
if (args.auth === "app" || !args.auth && hasApp) {
|
|
1109
|
+
authMethod = "app";
|
|
1110
|
+
if (args["app-id"])
|
|
1111
|
+
process.env.GITHUB_APP_ID = args["app-id"];
|
|
1112
|
+
if (args["installation-id"])
|
|
1113
|
+
process.env.GITHUB_APP_INSTALLATION_ID = args["installation-id"];
|
|
1114
|
+
if (!hasApp) {
|
|
1115
|
+
const missing = [];
|
|
1116
|
+
if (!process.env.GITHUB_APP_ID && !args["app-id"])
|
|
1117
|
+
missing.push("GITHUB_APP_ID (env) or --app-id");
|
|
1118
|
+
if (!process.env.GITHUB_APP_PRIVATE_KEY_BASE64 && !process.env.GITHUB_APP_PRIVATE_KEY) {
|
|
1119
|
+
missing.push("GITHUB_APP_PRIVATE_KEY_BASE64 (env)");
|
|
1120
|
+
}
|
|
1121
|
+
if (!process.env.GITHUB_APP_INSTALLATION_ID && !args["installation-id"]) {
|
|
1122
|
+
missing.push("GITHUB_APP_INSTALLATION_ID (env) or --installation-id");
|
|
1123
|
+
}
|
|
1124
|
+
return fail("UNKNOWN_ERROR", `Missing GitHub App credentials: ${missing.join(", ")}.
|
|
1125
|
+
Or run without --non-interactive for guided setup.`, true);
|
|
1126
|
+
}
|
|
1127
|
+
} else if (args.auth === "token" || !args.auth && hasToken) {
|
|
1128
|
+
authMethod = "token";
|
|
1129
|
+
} else {
|
|
1130
|
+
return fail("UNKNOWN_ERROR", `Missing GitHub authentication.
|
|
1131
|
+
Set GITHUB_TOKEN or GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY_BASE64 + GITHUB_APP_INSTALLATION_ID.
|
|
1132
|
+
Or run without --non-interactive for guided setup.`, true);
|
|
1133
|
+
}
|
|
1134
|
+
emit({ type: "init:auth:detected", method: authMethod });
|
|
1135
|
+
if (!process.env.JULES_API_KEY) {
|
|
1136
|
+
emit({
|
|
1137
|
+
type: "init:secret:skipped",
|
|
1138
|
+
name: "JULES_API_KEY",
|
|
1139
|
+
reason: "Not set — Fleet workflows will not be able to dispatch sessions."
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
const baseBranch = args.base ?? "main";
|
|
1143
|
+
const dryRun = args["dry-run"] ?? false;
|
|
1144
|
+
const secretsToUpload = {};
|
|
1145
|
+
if (dryRun) {
|
|
1146
|
+
const files = WORKFLOW_TEMPLATES.map((t) => t.repoPath);
|
|
1147
|
+
files.push(".fleet/goals/example.md");
|
|
1148
|
+
emit({ type: "init:dry-run", files });
|
|
1149
|
+
}
|
|
1150
|
+
return { owner, repo, baseBranch, authMethod, secretsToUpload, dryRun };
|
|
1151
|
+
}
|
|
1152
|
+
// src/init/ops/upload-secrets.ts
|
|
1153
|
+
async function uploadSecret(octokit, owner, repo, secretName, secretValue, emit) {
|
|
1154
|
+
emit({ type: "init:secret:uploading", name: secretName });
|
|
1155
|
+
try {
|
|
1156
|
+
const { data: publicKey } = await octokit.rest.actions.getRepoPublicKey({
|
|
1157
|
+
owner,
|
|
1158
|
+
repo
|
|
1159
|
+
});
|
|
1160
|
+
const sodium = await import("libsodium-wrappers");
|
|
1161
|
+
await sodium.default.ready;
|
|
1162
|
+
const binKey = sodium.default.from_base64(publicKey.key, sodium.default.base64_variants.ORIGINAL);
|
|
1163
|
+
const binSecret = sodium.default.from_string(secretValue);
|
|
1164
|
+
const encrypted = sodium.default.crypto_box_seal(binSecret, binKey);
|
|
1165
|
+
const encryptedBase64 = sodium.default.to_base64(encrypted, sodium.default.base64_variants.ORIGINAL);
|
|
1166
|
+
await octokit.rest.actions.createOrUpdateRepoSecret({
|
|
1167
|
+
owner,
|
|
1168
|
+
repo,
|
|
1169
|
+
secret_name: secretName,
|
|
1170
|
+
encrypted_value: encryptedBase64,
|
|
1171
|
+
key_id: publicKey.key_id
|
|
1172
|
+
});
|
|
1173
|
+
emit({ type: "init:secret:uploaded", name: secretName });
|
|
1174
|
+
return { success: true };
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1177
|
+
emit({
|
|
1178
|
+
type: "init:secret:skipped",
|
|
1179
|
+
name: secretName,
|
|
1180
|
+
reason: message
|
|
1181
|
+
});
|
|
1182
|
+
return { success: false, error: message };
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/cli/init.command.ts
|
|
1187
|
+
var init_command_default = defineCommand({
|
|
1188
|
+
meta: {
|
|
1189
|
+
name: "init",
|
|
1190
|
+
description: "Scaffold fleet workflow files by creating a PR"
|
|
1191
|
+
},
|
|
1192
|
+
args: {
|
|
1193
|
+
repo: {
|
|
1194
|
+
type: "string",
|
|
1195
|
+
description: "Repository in owner/repo format (auto-detected from git remote if omitted)"
|
|
1196
|
+
},
|
|
1197
|
+
base: {
|
|
1198
|
+
type: "string",
|
|
1199
|
+
description: "Base branch for the PR",
|
|
1200
|
+
default: "main"
|
|
1201
|
+
},
|
|
1202
|
+
"non-interactive": {
|
|
1203
|
+
type: "boolean",
|
|
1204
|
+
description: "Disable wizard prompts — all inputs via flags/env vars",
|
|
1205
|
+
default: false
|
|
1206
|
+
},
|
|
1207
|
+
"dry-run": {
|
|
1208
|
+
type: "boolean",
|
|
1209
|
+
description: "Show what would be created without making changes",
|
|
1210
|
+
default: false
|
|
1211
|
+
},
|
|
1212
|
+
auth: {
|
|
1213
|
+
type: "string",
|
|
1214
|
+
description: "Auth mode: token | app (auto-detected from env vars)"
|
|
1215
|
+
},
|
|
1216
|
+
"app-id": {
|
|
1217
|
+
type: "string",
|
|
1218
|
+
description: "GitHub App ID (overrides GITHUB_APP_ID env var)"
|
|
1219
|
+
},
|
|
1220
|
+
"installation-id": {
|
|
1221
|
+
type: "string",
|
|
1222
|
+
description: "GitHub App Installation ID (overrides env var)"
|
|
1223
|
+
},
|
|
1224
|
+
"upload-secrets": {
|
|
1225
|
+
type: "boolean",
|
|
1226
|
+
description: "Upload secrets to GitHub Actions (default: true in interactive, false in non-interactive)"
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1229
|
+
async run({ args }) {
|
|
1230
|
+
const nonInteractive = args["non-interactive"] || !isInteractive();
|
|
1231
|
+
const renderer = createRenderer(!nonInteractive);
|
|
1232
|
+
const emit = createEmitter(renderer);
|
|
1233
|
+
const wizardArgs = args;
|
|
1234
|
+
const inputs = nonInteractive ? await validateHeadlessInputs(wizardArgs, emit) : await runInitWizard(wizardArgs, emit);
|
|
1235
|
+
if ("success" in inputs && !inputs.success) {
|
|
1236
|
+
renderer.error(inputs.error.message);
|
|
1237
|
+
if (inputs.error.suggestion) {
|
|
1238
|
+
renderer.render({
|
|
1239
|
+
type: "error",
|
|
1240
|
+
code: inputs.error.code,
|
|
1241
|
+
message: inputs.error.suggestion
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
const { owner, repo, baseBranch, secretsToUpload, dryRun } = inputs;
|
|
1247
|
+
renderer.start(`Fleet Init — ${owner}/${repo}`);
|
|
1248
|
+
if (dryRun) {
|
|
1249
|
+
const files = WORKFLOW_TEMPLATES.map((t) => t.repoPath);
|
|
1250
|
+
files.push(".fleet/goals/example.md");
|
|
1251
|
+
emit({ type: "init:dry-run", files });
|
|
1252
|
+
renderer.end(`Dry run complete. ${files.length} files would be created.`);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const input = InitInputSchema.parse({
|
|
1256
|
+
repo: `${owner}/${repo}`,
|
|
1257
|
+
owner,
|
|
1258
|
+
repoName: repo,
|
|
1259
|
+
baseBranch
|
|
1260
|
+
});
|
|
1261
|
+
const octokit = createFleetOctokit();
|
|
1262
|
+
const labelConfigurator = new ConfigureHandler({ octokit });
|
|
1263
|
+
const handler = new InitHandler({ octokit, emit, labelConfigurator });
|
|
1264
|
+
const result = await handler.execute(input);
|
|
1265
|
+
if (!result.success) {
|
|
1266
|
+
renderer.error(result.error.message);
|
|
1267
|
+
if (result.error.suggestion) {
|
|
1268
|
+
renderer.render({
|
|
1269
|
+
type: "error",
|
|
1270
|
+
code: result.error.code,
|
|
1271
|
+
message: result.error.suggestion
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
const secretNames = Object.keys(secretsToUpload);
|
|
1277
|
+
if (secretNames.length > 0) {
|
|
1278
|
+
for (const name of secretNames) {
|
|
1279
|
+
await uploadSecret(octokit, owner, repo, name, secretsToUpload[name], emit);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
renderer.end("Fleet initialized! Merge the PR to activate Fleet.");
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
export {
|
|
1286
|
+
init_command_default as default
|
|
1287
|
+
};
|