@markjaquith/agency 1.11.0 → 2.0.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 (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -415
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -0
  6. package/src/cli.test.ts +290 -0
  7. package/src/commands/description.test.ts +103 -0
  8. package/src/commands/epic.test.ts +103 -0
  9. package/src/commands/epic.ts +101 -0
  10. package/src/commands/init.test.ts +42 -290
  11. package/src/commands/init.ts +15 -123
  12. package/src/commands/phase.ts +120 -0
  13. package/src/commands/pr.test.ts +19 -170
  14. package/src/commands/pr.ts +23 -179
  15. package/src/commands/repo.test.ts +80 -0
  16. package/src/commands/repo.ts +83 -0
  17. package/src/commands/status.test.ts +35 -477
  18. package/src/commands/status.ts +24 -213
  19. package/src/commands/task-phase.test.ts +206 -0
  20. package/src/commands/task.ts +83 -1285
  21. package/src/commands/validate.test.ts +91 -0
  22. package/src/commands/validate.ts +50 -0
  23. package/src/commands/work.test.ts +172 -419
  24. package/src/commands/work.ts +47 -166
  25. package/src/services/EpicService.test.ts +71 -0
  26. package/src/services/EpicService.ts +135 -0
  27. package/src/services/FileSystemService.ts +61 -92
  28. package/src/services/PhaseService.ts +323 -0
  29. package/src/services/PullRequestService.test.ts +386 -0
  30. package/src/services/PullRequestService.ts +143 -0
  31. package/src/services/RepositoryService.test.ts +106 -0
  32. package/src/services/RepositoryService.ts +159 -0
  33. package/src/services/TaskPhaseService.test.ts +217 -0
  34. package/src/services/TaskService.ts +192 -0
  35. package/src/services/WorkbaseService.test.ts +446 -0
  36. package/src/services/WorkbaseService.ts +493 -0
  37. package/src/services/WorktreeService.test.ts +599 -0
  38. package/src/services/WorktreeService.ts +357 -0
  39. package/src/test-utils.ts +41 -438
  40. package/src/types.ts +1 -215
  41. package/src/utils/command.ts +1 -5
  42. package/src/utils/effect.test.ts +24 -0
  43. package/src/utils/effect.ts +3 -273
  44. package/src/utils/process.test.ts +0 -15
  45. package/src/utils/process.ts +1 -60
  46. package/src/workbase/frontmatter.test.ts +50 -0
  47. package/src/workbase/frontmatter.ts +77 -0
  48. package/src/workbase/repository-reference.test.ts +25 -0
  49. package/src/workbase/repository-reference.ts +18 -0
  50. package/src/workbase/schemas.test.ts +146 -0
  51. package/src/workbase/schemas.ts +78 -0
  52. package/src/workbase/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -518
  73. package/src/commands/push.ts +0 -434
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1183
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. package/templates/opencode.json +0 -4
@@ -1,395 +0,0 @@
1
- import {
2
- test,
3
- expect,
4
- describe,
5
- beforeEach,
6
- afterEach,
7
- setDefaultTimeout,
8
- } from "bun:test"
9
-
10
- // Pull tests require more time due to git operations
11
- setDefaultTimeout(30000)
12
- import { join } from "path"
13
- import { pull } from "./pull"
14
- import {
15
- createTempDir,
16
- cleanupTempDir,
17
- initGitRepo,
18
- getCurrentBranch,
19
- createCommit,
20
- checkoutBranch,
21
- runTestEffect,
22
- } from "../test-utils"
23
-
24
- async function createBranch(cwd: string, branchName: string): Promise<void> {
25
- await Bun.spawn(["git", "checkout", "-b", branchName], {
26
- cwd,
27
- stdout: "pipe",
28
- stderr: "pipe",
29
- }).exited
30
- }
31
-
32
- async function setupAgencyJson(
33
- gitRoot: string,
34
- emitBranch?: string,
35
- ): Promise<void> {
36
- const agencyJson = {
37
- version: 1,
38
- injectedFiles: ["AGENTS.MD", "TASK.md"],
39
- template: "test",
40
- createdAt: new Date().toISOString(),
41
- ...(emitBranch ? { emitBranch } : {}),
42
- }
43
- await Bun.write(
44
- join(gitRoot, "agency.json"),
45
- JSON.stringify(agencyJson, null, 2) + "\n",
46
- )
47
- await Bun.spawn(["git", "add", "agency.json"], {
48
- cwd: gitRoot,
49
- stdout: "pipe",
50
- stderr: "pipe",
51
- }).exited
52
- await createCommit(gitRoot, "Add agency.json")
53
- }
54
-
55
- async function setupBareRemote(tempDir: string): Promise<string> {
56
- // Create a bare repository to use as remote
57
- const remoteDir = join(tempDir, "remote.git")
58
- await Bun.spawn(["git", "init", "--bare", remoteDir], {
59
- stdout: "pipe",
60
- stderr: "pipe",
61
- }).exited
62
-
63
- return remoteDir
64
- }
65
-
66
- async function addRemote(cwd: string, remoteUrl: string): Promise<void> {
67
- await Bun.spawn(["git", "remote", "add", "origin", remoteUrl], {
68
- cwd,
69
- stdout: "pipe",
70
- stderr: "pipe",
71
- }).exited
72
- }
73
-
74
- async function getCommitCount(cwd: string, branch: string): Promise<number> {
75
- const proc = Bun.spawn(["git", "rev-list", "--count", branch], {
76
- cwd,
77
- stdout: "pipe",
78
- stderr: "pipe",
79
- })
80
- await proc.exited
81
- const output = await new Response(proc.stdout).text()
82
- return parseInt(output.trim(), 10)
83
- }
84
-
85
- async function getLatestCommitMessage(cwd: string): Promise<string> {
86
- const proc = Bun.spawn(["git", "log", "-1", "--pretty=%B"], {
87
- cwd,
88
- stdout: "pipe",
89
- stderr: "pipe",
90
- })
91
- await proc.exited
92
- const output = await new Response(proc.stdout).text()
93
- return output.trim()
94
- }
95
-
96
- describe("pull command", () => {
97
- let tempDir: string
98
- let remoteDir: string
99
- let originalCwd: string
100
-
101
- beforeEach(async () => {
102
- tempDir = await createTempDir()
103
- originalCwd = process.cwd()
104
- process.chdir(tempDir)
105
-
106
- // Set config path to non-existent file to use defaults
107
- process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
108
-
109
- // Initialize git repo
110
- await initGitRepo(tempDir)
111
- await createCommit(tempDir, "Initial commit")
112
-
113
- // Rename to main if needed
114
- const currentBranch = await getCurrentBranch(tempDir)
115
- if (currentBranch === "master") {
116
- await Bun.spawn(["git", "branch", "-m", "main"], {
117
- cwd: tempDir,
118
- stdout: "pipe",
119
- stderr: "pipe",
120
- }).exited
121
- }
122
-
123
- // Setup bare remote
124
- remoteDir = await setupBareRemote(tempDir)
125
- await addRemote(tempDir, remoteDir)
126
-
127
- // Push main to remote
128
- await Bun.spawn(["git", "push", "-u", "origin", "main"], {
129
- cwd: tempDir,
130
- stdout: "pipe",
131
- stderr: "pipe",
132
- }).exited
133
- })
134
-
135
- afterEach(async () => {
136
- process.chdir(originalCwd)
137
- delete process.env.AGENCY_CONFIG_PATH
138
- await cleanupTempDir(tempDir)
139
- })
140
-
141
- describe("basic functionality", () => {
142
- test("pulls commits from remote emit branch to source branch", async () => {
143
- // Setup agency.json
144
- await setupAgencyJson(tempDir)
145
-
146
- // Create source branch: A1 A2
147
- await createBranch(tempDir, "agency--feature")
148
- await createCommit(tempDir, "A1: Feature work")
149
-
150
- // Simulate agency emit: create local emit branch (B1) with rewritten history
151
- await createBranch(tempDir, "feature")
152
- // For test simplicity, we're not actually rewriting history, just creating emit branch
153
-
154
- // Push emit branch to remote
155
- await Bun.spawn(["git", "push", "-u", "origin", "feature"], {
156
- cwd: tempDir,
157
- stdout: "pipe",
158
- stderr: "pipe",
159
- }).exited
160
-
161
- // Now local emit and remote emit are in sync (both at B1)
162
- // Simulate someone adding commits to remote emit branch (B2, B3)
163
- await Bun.write(join(tempDir, "file1.txt"), "content1")
164
- await Bun.spawn(["git", "add", "file1.txt"], {
165
- cwd: tempDir,
166
- stdout: "pipe",
167
- stderr: "pipe",
168
- }).exited
169
- await createCommit(tempDir, "B2: Remote commit 1")
170
-
171
- await Bun.write(join(tempDir, "file2.txt"), "content2")
172
- await Bun.spawn(["git", "add", "file2.txt"], {
173
- cwd: tempDir,
174
- stdout: "pipe",
175
- stderr: "pipe",
176
- }).exited
177
- await createCommit(tempDir, "B3: Remote commit 2")
178
-
179
- await Bun.spawn(["git", "push"], {
180
- cwd: tempDir,
181
- stdout: "pipe",
182
- stderr: "pipe",
183
- }).exited
184
-
185
- // Reset local emit branch to B1 (before the remote commits B2, B3)
186
- // This simulates the state where remote has moved ahead
187
- await Bun.spawn(["git", "reset", "--hard", "HEAD~2"], {
188
- cwd: tempDir,
189
- stdout: "pipe",
190
- stderr: "pipe",
191
- }).exited
192
-
193
- // Go back to source branch (still at A1)
194
- await checkoutBranch(tempDir, "agency--feature")
195
- const beforeCommitCount = await getCommitCount(tempDir, "agency--feature")
196
-
197
- // Run pull command - should find B2 and B3 on remote that aren't on local emit
198
- // and cherry-pick them onto source as A2 and A3
199
- await runTestEffect(pull({ silent: true }))
200
-
201
- // Should still be on source branch
202
- expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
203
-
204
- // Should have the new commits
205
- const afterCommitCount = await getCommitCount(tempDir, "agency--feature")
206
- expect(afterCommitCount).toBe(beforeCommitCount + 2)
207
-
208
- // Last commit should be the second remote commit
209
- const lastCommit = await getLatestCommitMessage(tempDir)
210
- expect(lastCommit).toBe("B3: Remote commit 2")
211
- })
212
-
213
- test("handles no new commits gracefully", async () => {
214
- // Setup agency.json
215
- await setupAgencyJson(tempDir)
216
-
217
- // Create a source branch
218
- await createBranch(tempDir, "agency--feature")
219
- await createCommit(tempDir, "Feature work")
220
-
221
- // Create emit branch with same commits
222
- await createBranch(tempDir, "feature")
223
-
224
- // Push emit branch to remote
225
- await Bun.spawn(["git", "push", "-u", "origin", "feature"], {
226
- cwd: tempDir,
227
- stdout: "pipe",
228
- stderr: "pipe",
229
- }).exited
230
-
231
- // Go back to source branch
232
- await checkoutBranch(tempDir, "agency--feature")
233
-
234
- // Capture output
235
- const originalLog = console.log
236
- let logMessages: string[] = []
237
- console.log = (msg: string) => {
238
- logMessages.push(msg)
239
- }
240
-
241
- // Run pull command - should report no new commits
242
- await runTestEffect(pull({ silent: false }))
243
-
244
- console.log = originalLog
245
-
246
- // Should report no new commits
247
- expect(logMessages.some((msg) => msg.includes("No new commits"))).toBe(
248
- true,
249
- )
250
-
251
- // Should still be on source branch
252
- expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
253
- })
254
-
255
- test("works with custom remote", async () => {
256
- // Setup agency.json
257
- await setupAgencyJson(tempDir)
258
-
259
- // Create a source branch
260
- await createBranch(tempDir, "agency--feature")
261
- await createCommit(tempDir, "Feature work")
262
-
263
- // Create local emit branch
264
- await createBranch(tempDir, "feature")
265
-
266
- // Add a second remote
267
- const upstreamDir = await setupBareRemote(tempDir)
268
- await Bun.spawn(["git", "remote", "add", "upstream", upstreamDir], {
269
- cwd: tempDir,
270
- stdout: "pipe",
271
- stderr: "pipe",
272
- }).exited
273
-
274
- // Push emit branch to upstream
275
- await Bun.spawn(["git", "push", "-u", "upstream", "feature"], {
276
- cwd: tempDir,
277
- stdout: "pipe",
278
- stderr: "pipe",
279
- }).exited
280
-
281
- // Add a commit to upstream remote
282
- await Bun.write(join(tempDir, "upstream-file.txt"), "upstream content")
283
- await Bun.spawn(["git", "add", "upstream-file.txt"], {
284
- cwd: tempDir,
285
- stdout: "pipe",
286
- stderr: "pipe",
287
- }).exited
288
- await createCommit(tempDir, "Emit commit")
289
- await Bun.spawn(["git", "push", "upstream", "feature"], {
290
- cwd: tempDir,
291
- stdout: "pipe",
292
- stderr: "pipe",
293
- }).exited
294
-
295
- // Reset local emit branch to before the upstream commit
296
- await Bun.spawn(["git", "reset", "--hard", "HEAD~1"], {
297
- cwd: tempDir,
298
- stdout: "pipe",
299
- stderr: "pipe",
300
- }).exited
301
-
302
- // Go back to source branch
303
- await checkoutBranch(tempDir, "agency--feature")
304
-
305
- // Run pull with custom remote
306
- await runTestEffect(pull({ remote: "upstream", silent: true }))
307
-
308
- // Should still be on source branch
309
- expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
310
-
311
- // Should have the new commit
312
- const lastCommit = await getLatestCommitMessage(tempDir)
313
- expect(lastCommit).toBe("Emit commit")
314
- })
315
- })
316
-
317
- describe("error handling", () => {
318
- test("throws error when not in a git repository", async () => {
319
- const nonGitDir = await createTempDir()
320
- process.chdir(nonGitDir)
321
-
322
- await expect(runTestEffect(pull({ silent: true }))).rejects.toThrow(
323
- "Not in a git repository",
324
- )
325
-
326
- await cleanupTempDir(nonGitDir)
327
- })
328
-
329
- test("handles case when only source branch exists (no emit on remote)", async () => {
330
- // Setup agency.json
331
- await setupAgencyJson(tempDir)
332
-
333
- // Create a source branch
334
- await createBranch(tempDir, "agency--feature")
335
- await setupAgencyJson(tempDir, "feature")
336
- await createCommit(tempDir, "Feature work")
337
-
338
- // Don't create or push emit branch - just test that pull handles this gracefully
339
- // Run pull - should fail because remote emit branch doesn't exist
340
- await expect(runTestEffect(pull({ silent: true }))).rejects.toThrow()
341
- })
342
-
343
- test("throws error when remote emit branch does not exist", async () => {
344
- // Setup agency.json
345
- await setupAgencyJson(tempDir)
346
-
347
- // Create a source branch
348
- await createBranch(tempDir, "agency--feature")
349
- await setupAgencyJson(tempDir, "feature")
350
- await createCommit(tempDir, "Feature work")
351
-
352
- // Run pull - should fail because remote emit branch doesn't exist
353
- await expect(runTestEffect(pull({ silent: true }))).rejects.toThrow(
354
- "Failed to fetch",
355
- )
356
- })
357
- })
358
-
359
- describe("silent mode", () => {
360
- test("silent flag suppresses output", async () => {
361
- // Setup agency.json
362
- await setupAgencyJson(tempDir)
363
-
364
- // Create a source branch
365
- await createBranch(tempDir, "agency--feature")
366
- await createCommit(tempDir, "Feature work")
367
-
368
- // Create emit branch
369
- await createBranch(tempDir, "feature")
370
- await createCommit(tempDir, "Emit commit")
371
-
372
- // Push emit branch to remote
373
- await Bun.spawn(["git", "push", "-u", "origin", "feature"], {
374
- cwd: tempDir,
375
- stdout: "pipe",
376
- stderr: "pipe",
377
- }).exited
378
-
379
- // Go back to source branch
380
- await checkoutBranch(tempDir, "agency--feature")
381
-
382
- // Capture output
383
- const originalLog = console.log
384
- let logCalled = false
385
- console.log = () => {
386
- logCalled = true
387
- }
388
-
389
- await runTestEffect(pull({ silent: true }))
390
-
391
- console.log = originalLog
392
- expect(logCalled).toBe(false)
393
- })
394
- })
395
- })
@@ -1,205 +0,0 @@
1
- import { Effect } from "effect"
2
- import type { BaseCommandOptions } from "../utils/command"
3
- import { GitService } from "../services/GitService"
4
- import { ConfigService } from "../services/ConfigService"
5
- import { resolveBranchPairWithAgencyJson } from "../utils/pr-branch"
6
- import highlight, { done } from "../utils/colors"
7
- import {
8
- createLoggers,
9
- ensureGitRepo,
10
- withBranchProtection,
11
- } from "../utils/effect"
12
- import { withSpinner } from "../utils/spinner"
13
-
14
- interface PullOptions extends BaseCommandOptions {
15
- remote?: string
16
- }
17
-
18
- export const pull = (options: PullOptions = {}) =>
19
- Effect.gen(function* () {
20
- const gitRoot = yield* ensureGitRepo()
21
-
22
- // Wrap the entire pull operation with branch protection
23
- // This ensures we return to the original branch on Ctrl-C interrupt
24
- yield* withBranchProtection(gitRoot, pullCore(gitRoot, options))
25
- })
26
-
27
- const pullCore = (gitRoot: string, options: PullOptions) =>
28
- Effect.gen(function* () {
29
- const { verbose = false } = options
30
- const { log, verboseLog } = createLoggers(options)
31
-
32
- const git = yield* GitService
33
- const configService = yield* ConfigService
34
-
35
- // Resolve remote name (use provided option, config, or auto-detect)
36
- const remote = options.remote
37
- ? yield* git.resolveRemote(gitRoot, options.remote)
38
- : yield* git.resolveRemote(gitRoot)
39
-
40
- // Load config
41
- const config = yield* configService.loadConfig()
42
-
43
- // Get current branch
44
- let currentBranch = yield* git.getCurrentBranch(gitRoot)
45
-
46
- // Resolve branch pair to find source and emit branches
47
- const branches = yield* resolveBranchPairWithAgencyJson(
48
- gitRoot,
49
- currentBranch,
50
- config.sourceBranchPattern,
51
- config.emitBranch,
52
- )
53
- const { sourceBranch, emitBranch, isOnEmitBranch } = branches
54
-
55
- // If we're on emit branch, switch to source branch
56
- if (isOnEmitBranch) {
57
- verboseLog(
58
- `Currently on emit branch ${highlight.branch(currentBranch)}, switching to source branch ${highlight.branch(sourceBranch)}`,
59
- )
60
- const sourceExists = yield* git.branchExists(gitRoot, sourceBranch)
61
- if (!sourceExists) {
62
- return yield* Effect.fail(
63
- new Error(
64
- `Source branch ${highlight.branch(sourceBranch)} does not exist`,
65
- ),
66
- )
67
- }
68
- yield* git.checkoutBranch(gitRoot, sourceBranch)
69
- currentBranch = sourceBranch
70
- }
71
-
72
- verboseLog(`Source branch: ${highlight.branch(sourceBranch)}`)
73
- verboseLog(`Emit branch: ${highlight.branch(emitBranch)}`)
74
-
75
- // Fetch the remote emit branch
76
- const remoteEmitBranch = `${remote}/${emitBranch}`
77
- const fetchOperation = Effect.gen(function* () {
78
- const result = yield* git.fetch(gitRoot, remote, emitBranch).pipe(
79
- Effect.map(() => true),
80
- Effect.catchAll(() => Effect.succeed(false)),
81
- )
82
-
83
- if (!result) {
84
- return yield* Effect.fail(
85
- new Error(
86
- `Failed to fetch ${highlight.branch(remoteEmitBranch)}. Does the remote branch exist?`,
87
- ),
88
- )
89
- }
90
-
91
- verboseLog(`Fetched ${highlight.branch(remoteEmitBranch)}`)
92
-
93
- // Check if remote emit branch exists after fetch
94
- const remoteExists = yield* git.branchExists(gitRoot, remoteEmitBranch)
95
- if (!remoteExists) {
96
- return yield* Effect.fail(
97
- new Error(
98
- `Remote emit branch ${highlight.branch(remoteEmitBranch)} does not exist`,
99
- ),
100
- )
101
- }
102
- })
103
-
104
- yield* withSpinner(fetchOperation, {
105
- text: `Fetching ${highlight.branch(remoteEmitBranch)}`,
106
- successText: `Fetched ${highlight.branch(remoteEmitBranch)}`,
107
- enabled: !options.silent && !verbose,
108
- })
109
-
110
- // Check if local emit branch exists
111
- const localEmitExists = yield* git.branchExists(gitRoot, emitBranch)
112
- if (!localEmitExists) {
113
- verboseLog(
114
- `Local emit branch ${highlight.branch(emitBranch)} does not exist, comparing against source branch`,
115
- )
116
- }
117
-
118
- // Get the list of commits on remote emit branch that aren't on local emit branch
119
- // If local emit branch doesn't exist, compare against source branch
120
- const compareBase = localEmitExists ? emitBranch : sourceBranch
121
- verboseLog(
122
- `Comparing ${highlight.branch(remoteEmitBranch)} against ${highlight.branch(compareBase)}`,
123
- )
124
-
125
- const commitsResult = yield* git
126
- .getCommitsBetween(gitRoot, compareBase, remoteEmitBranch)
127
- .pipe(Effect.catchAll(() => Effect.succeed("")))
128
-
129
- if (!commitsResult || commitsResult.trim() === "") {
130
- log(done("No new commits to pull"))
131
- return
132
- }
133
-
134
- const commits = commitsResult.split("\n").filter((c) => c.trim().length > 0)
135
- verboseLog(`Found ${commits.length} commits to cherry-pick`)
136
-
137
- // Cherry-pick each commit
138
- let successCount = 0
139
- let failedCommit: string | null = null
140
-
141
- for (const commit of commits) {
142
- verboseLog(`Cherry-picking ${highlight.commit(commit.substring(0, 8))}`)
143
-
144
- const result = yield* git.cherryPick(gitRoot, commit).pipe(
145
- Effect.map(() => true),
146
- Effect.catchAll(() => {
147
- failedCommit = commit
148
- return Effect.succeed(false)
149
- }),
150
- )
151
-
152
- if (!result) {
153
- break
154
- }
155
-
156
- successCount++
157
- }
158
-
159
- if (failedCommit) {
160
- log(
161
- `Cherry-picked ${successCount} of ${commits.length} commits before conflict`,
162
- )
163
- return yield* Effect.fail(
164
- new Error(
165
- `Cherry-pick conflict at commit ${highlight.commit(failedCommit.substring(0, 8))}. Resolve conflicts and continue with: git cherry-pick --continue`,
166
- ),
167
- )
168
- }
169
-
170
- log(
171
- done(
172
- `Pulled ${commits.length} commit${commits.length === 1 ? "" : "s"} from ${highlight.branch(remoteEmitBranch)}`,
173
- ),
174
- )
175
- })
176
-
177
- export const help = `
178
- Usage: agency pull [options]
179
-
180
- Pull commits from the remote emit branch and cherry-pick them onto the source branch.
181
-
182
- This command is useful when someone else has pushed commits to the emit branch
183
- (e.g., after a PR review) and you want to bring those changes back into your
184
- source branch.
185
-
186
- Workflow:
187
- 1. Determines the source and emit branch names
188
- 2. If on emit branch, switches to source branch
189
- 3. Fetches the remote emit branch
190
- 4. Finds commits on remote emit branch that aren't on source branch
191
- 5. Cherry-picks each commit onto the source branch
192
-
193
- Options:
194
- -r, --remote Remote name to fetch from (defaults to 'origin')
195
-
196
- Examples:
197
- agency pull # Pull from origin/<emit-branch>
198
- agency pull --remote upstream # Pull from upstream/<emit-branch>
199
-
200
- Notes:
201
- - If a cherry-pick conflict occurs, you'll need to resolve it manually
202
- - Use 'git cherry-pick --continue' after resolving conflicts
203
- - The command will stop at the first conflict
204
- - Only commits that don't exist on the source branch are cherry-picked
205
- `