@markjaquith/agency 1.11.1 → 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.
- package/README.md +302 -113
- package/cli.ts +123 -420
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +258 -0
- package/src/cli.test.ts +290 -0
- package/src/commands/description.test.ts +103 -0
- package/src/commands/epic.test.ts +103 -0
- package/src/commands/epic.ts +101 -0
- package/src/commands/init.test.ts +42 -290
- package/src/commands/init.ts +15 -123
- package/src/commands/phase.ts +120 -0
- package/src/commands/pr.test.ts +19 -170
- package/src/commands/pr.ts +23 -179
- package/src/commands/repo.test.ts +80 -0
- package/src/commands/repo.ts +83 -0
- package/src/commands/status.test.ts +35 -477
- package/src/commands/status.ts +24 -213
- package/src/commands/task-phase.test.ts +206 -0
- package/src/commands/task.ts +83 -1285
- package/src/commands/validate.test.ts +91 -0
- package/src/commands/validate.ts +50 -0
- package/src/commands/work.test.ts +172 -419
- package/src/commands/work.ts +47 -166
- package/src/services/EpicService.test.ts +71 -0
- package/src/services/EpicService.ts +135 -0
- package/src/services/FileSystemService.ts +61 -92
- package/src/services/PhaseService.ts +323 -0
- package/src/services/PullRequestService.test.ts +386 -0
- package/src/services/PullRequestService.ts +143 -0
- package/src/services/RepositoryService.test.ts +106 -0
- package/src/services/RepositoryService.ts +159 -0
- package/src/services/TaskPhaseService.test.ts +217 -0
- package/src/services/TaskService.ts +192 -0
- package/src/services/WorkbaseService.test.ts +446 -0
- package/src/services/WorkbaseService.ts +493 -0
- package/src/services/WorktreeService.test.ts +599 -0
- package/src/services/WorktreeService.ts +357 -0
- package/src/test-utils.ts +41 -438
- package/src/types.ts +1 -215
- package/src/utils/command.ts +1 -5
- package/src/utils/effect.test.ts +24 -0
- package/src/utils/effect.ts +3 -273
- package/src/utils/process.test.ts +0 -15
- package/src/utils/process.ts +1 -60
- package/src/workbase/frontmatter.test.ts +50 -0
- package/src/workbase/frontmatter.ts +77 -0
- package/src/workbase/repository-reference.test.ts +25 -0
- package/src/workbase/repository-reference.ts +18 -0
- package/src/workbase/schemas.test.ts +146 -0
- package/src/workbase/schemas.ts +78 -0
- package/src/workbase/worktree-command.test.ts +61 -0
- package/src/workbase/worktree-command.ts +52 -0
- package/src/commands/base.test.ts +0 -198
- package/src/commands/base.ts +0 -198
- package/src/commands/clean.test.ts +0 -299
- package/src/commands/clean.ts +0 -274
- package/src/commands/completions.test.ts +0 -51
- package/src/commands/completions.ts +0 -308
- package/src/commands/emit.integration.test.ts +0 -549
- package/src/commands/emit.test.ts +0 -576
- package/src/commands/emit.ts +0 -700
- package/src/commands/emitted.test.ts +0 -226
- package/src/commands/emitted.ts +0 -57
- package/src/commands/loop.test.ts +0 -621
- package/src/commands/loop.ts +0 -406
- package/src/commands/merge.integration.test.ts +0 -195
- package/src/commands/merge.test.ts +0 -246
- package/src/commands/merge.ts +0 -244
- package/src/commands/pull.test.ts +0 -395
- package/src/commands/pull.ts +0 -205
- package/src/commands/push.test.ts +0 -641
- package/src/commands/push.ts +0 -482
- package/src/commands/rebase.test.ts +0 -521
- package/src/commands/rebase.ts +0 -232
- package/src/commands/save.test.ts +0 -247
- package/src/commands/save.ts +0 -162
- package/src/commands/source.test.ts +0 -195
- package/src/commands/source.ts +0 -72
- package/src/commands/switch.test.ts +0 -194
- package/src/commands/switch.ts +0 -84
- package/src/commands/task-branching.test.ts +0 -633
- package/src/commands/task-continue.test.ts +0 -311
- package/src/commands/task-edit.test.ts +0 -235
- package/src/commands/task-main.test.ts +0 -1066
- package/src/commands/task-squash.test.ts +0 -266
- package/src/commands/tasks.test.ts +0 -391
- package/src/commands/tasks.ts +0 -161
- package/src/commands/template-delete.test.ts +0 -178
- package/src/commands/template-delete.ts +0 -98
- package/src/commands/template-list.test.ts +0 -135
- package/src/commands/template-list.ts +0 -87
- package/src/commands/template-view.test.ts +0 -158
- package/src/commands/template-view.ts +0 -86
- package/src/commands/template.test.ts +0 -32
- package/src/commands/template.ts +0 -96
- package/src/commands/use.test.ts +0 -87
- package/src/commands/use.ts +0 -97
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -17
- package/src/schemas.ts +0 -33
- package/src/services/AgencyMetadataService.ts +0 -311
- package/src/services/ClaudeService.test.ts +0 -184
- package/src/services/ClaudeService.ts +0 -91
- package/src/services/ConfigService.ts +0 -115
- package/src/services/FilterRepoService.ts +0 -164
- package/src/services/FormatterService.test.ts +0 -432
- package/src/services/FormatterService.ts +0 -219
- package/src/services/GitService.ts +0 -1187
- package/src/services/MockFilterRepoService.ts +0 -140
- package/src/services/OpencodeService.ts +0 -263
- package/src/services/PromptService.ts +0 -183
- package/src/services/TemplateService.ts +0 -81
- package/src/types/native-exec.d.ts +0 -8
- package/src/utils/colors.ts +0 -178
- package/src/utils/git-path.ts +0 -6
- package/src/utils/glob.test.ts +0 -161
- package/src/utils/glob.ts +0 -78
- package/src/utils/paths.ts +0 -51
- package/src/utils/pr-branch.test.ts +0 -376
- package/src/utils/pr-branch.ts +0 -508
- package/src/utils/spinner.test.ts +0 -45
- package/src/utils/spinner.ts +0 -96
- package/src/utils/task-parser.test.ts +0 -172
- package/src/utils/task-parser.ts +0 -93
- package/templates/AGENCY.md +0 -22
- package/templates/AGENTS.md +0 -11
- package/templates/CLAUDE.md +0 -3
- package/templates/TASK.md +0 -5
- 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
|
-
})
|
package/src/commands/pull.ts
DELETED
|
@@ -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
|
-
`
|