@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.
Files changed (105) hide show
  1. package/README.md +205 -0
  2. package/dist/analyze/formatting.d.ts +19 -0
  3. package/dist/analyze/goals.d.ts +18 -0
  4. package/dist/analyze/handler.d.ts +23 -0
  5. package/dist/analyze/index.d.ts +8 -0
  6. package/dist/analyze/milestone.d.ts +43 -0
  7. package/dist/analyze/prompt.d.ts +10 -0
  8. package/dist/analyze/spec.d.ts +54 -0
  9. package/dist/analyze/triage-prompt.d.ts +16 -0
  10. package/dist/cli/analyze.command.d.ts +24 -0
  11. package/dist/cli/analyze.command.mjs +1015 -0
  12. package/dist/cli/commands.json +1 -0
  13. package/dist/cli/configure.command.d.ts +21 -0
  14. package/dist/cli/configure.command.mjs +623 -0
  15. package/dist/cli/dispatch.command.d.ts +16 -0
  16. package/dist/cli/dispatch.command.mjs +777 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.mjs +40 -0
  19. package/dist/cli/init.command.d.ts +38 -0
  20. package/dist/cli/init.command.mjs +1287 -0
  21. package/dist/cli/merge.command.d.ts +36 -0
  22. package/dist/cli/merge.command.mjs +859 -0
  23. package/dist/cli/signal.command.d.ts +2 -0
  24. package/dist/cli/signal.command.mjs +288 -0
  25. package/dist/configure/handler.d.ts +19 -0
  26. package/dist/configure/index.d.ts +4 -0
  27. package/dist/configure/labels.d.ts +6 -0
  28. package/dist/configure/spec.d.ts +49 -0
  29. package/dist/dispatch/events.d.ts +12 -0
  30. package/dist/dispatch/handler.d.ts +21 -0
  31. package/dist/dispatch/index.d.ts +5 -0
  32. package/dist/dispatch/spec.d.ts +47 -0
  33. package/dist/dispatch/status.d.ts +24 -0
  34. package/dist/index.d.ts +7 -0
  35. package/dist/index.mjs +2105 -0
  36. package/dist/init/handler.d.ts +22 -0
  37. package/dist/init/index.d.ts +4 -0
  38. package/dist/init/ops/commit-files.d.ts +10 -0
  39. package/dist/init/ops/create-branch.d.ts +16 -0
  40. package/dist/init/ops/create-pr.d.ts +15 -0
  41. package/dist/init/ops/pr-body.d.ts +5 -0
  42. package/dist/init/ops/upload-secrets.d.ts +11 -0
  43. package/dist/init/spec.d.ts +50 -0
  44. package/dist/init/templates/analyze.d.ts +2 -0
  45. package/dist/init/templates/dispatch.d.ts +2 -0
  46. package/dist/init/templates/example-goal.d.ts +5 -0
  47. package/dist/init/templates/merge.d.ts +2 -0
  48. package/dist/init/templates/types.d.ts +6 -0
  49. package/dist/init/templates.d.ts +10 -0
  50. package/dist/init/types.d.ts +19 -0
  51. package/dist/init/wizard/headless.d.ts +8 -0
  52. package/dist/init/wizard/index.d.ts +3 -0
  53. package/dist/init/wizard/interactive.d.ts +9 -0
  54. package/dist/init/wizard/types.d.ts +22 -0
  55. package/dist/merge/handler.d.ts +21 -0
  56. package/dist/merge/index.d.ts +5 -0
  57. package/dist/merge/ops/index.d.ts +4 -0
  58. package/dist/merge/ops/redispatch.d.ts +8 -0
  59. package/dist/merge/ops/squash-merge.d.ts +8 -0
  60. package/dist/merge/ops/update-branch.d.ts +11 -0
  61. package/dist/merge/ops/wait-for-ci.d.ts +7 -0
  62. package/dist/merge/select/by-fleet-run.d.ts +8 -0
  63. package/dist/merge/select/by-label.d.ts +7 -0
  64. package/dist/merge/select/index.d.ts +2 -0
  65. package/dist/merge/spec.d.ts +99 -0
  66. package/dist/shared/auth/cache-plugin.d.ts +9 -0
  67. package/dist/shared/auth/git.d.ts +22 -0
  68. package/dist/shared/auth/index.d.ts +4 -0
  69. package/dist/shared/auth/octokit.d.ts +11 -0
  70. package/dist/shared/auth/resolve-key.d.ts +11 -0
  71. package/dist/shared/events/analyze.d.ts +37 -0
  72. package/dist/shared/events/configure.d.ts +21 -0
  73. package/dist/shared/events/dispatch.d.ts +26 -0
  74. package/dist/shared/events/error.d.ts +7 -0
  75. package/dist/shared/events/index.d.ts +16 -0
  76. package/dist/shared/events/init.d.ts +49 -0
  77. package/dist/shared/events/merge.d.ts +72 -0
  78. package/dist/shared/events.d.ts +1 -0
  79. package/dist/shared/index.d.ts +6 -0
  80. package/dist/shared/result/create-result-schemas.d.ts +72 -0
  81. package/dist/shared/result/fail.d.ts +10 -0
  82. package/dist/shared/result/index.d.ts +3 -0
  83. package/dist/shared/result/ok.d.ts +5 -0
  84. package/dist/shared/schemas/check-run.d.ts +16 -0
  85. package/dist/shared/schemas/index.d.ts +4 -0
  86. package/dist/shared/schemas/label.d.ts +16 -0
  87. package/dist/shared/schemas/pr.d.ts +19 -0
  88. package/dist/shared/schemas/repo-info.d.ts +16 -0
  89. package/dist/shared/session-dispatcher.d.ts +18 -0
  90. package/dist/shared/ui/assert-never.d.ts +13 -0
  91. package/dist/shared/ui/index.d.ts +18 -0
  92. package/dist/shared/ui/interactive.d.ts +19 -0
  93. package/dist/shared/ui/plain.d.ts +16 -0
  94. package/dist/shared/ui/render/analyze.d.ts +4 -0
  95. package/dist/shared/ui/render/configure.d.ts +4 -0
  96. package/dist/shared/ui/render/dispatch.d.ts +4 -0
  97. package/dist/shared/ui/render/error.d.ts +4 -0
  98. package/dist/shared/ui/render/init.d.ts +4 -0
  99. package/dist/shared/ui/render/merge.d.ts +4 -0
  100. package/dist/shared/ui/session-url.d.ts +13 -0
  101. package/dist/shared/ui/spec.d.ts +30 -0
  102. package/dist/signal/handler.d.ts +17 -0
  103. package/dist/signal/index.d.ts +3 -0
  104. package/dist/signal/spec.d.ts +60 -0
  105. 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
+ };