@markjaquith/agency 1.11.1 → 2.1.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 +313 -117
- package/cli.ts +124 -420
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +266 -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 +326 -399
- package/src/commands/work.ts +153 -153
- 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/work-target.test.ts +77 -0
- package/src/workbase/work-target.ts +180 -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,641 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import { push } from "./push"
|
|
4
|
-
import {
|
|
5
|
-
createTempDir,
|
|
6
|
-
cleanupTempDir,
|
|
7
|
-
initGitRepo,
|
|
8
|
-
getCurrentBranch,
|
|
9
|
-
getGitOutput,
|
|
10
|
-
createCommit,
|
|
11
|
-
checkoutBranch,
|
|
12
|
-
createBranch,
|
|
13
|
-
addAndCommit,
|
|
14
|
-
renameBranch,
|
|
15
|
-
runTestEffect,
|
|
16
|
-
runTestEffectWithMockFilterRepo,
|
|
17
|
-
} from "../test-utils"
|
|
18
|
-
|
|
19
|
-
async function setupAgencyJson(gitRoot: string): Promise<void> {
|
|
20
|
-
const agencyJson = {
|
|
21
|
-
version: 1,
|
|
22
|
-
injectedFiles: ["AGENTS.MD", "TASK.md"],
|
|
23
|
-
template: "test",
|
|
24
|
-
createdAt: new Date().toISOString(),
|
|
25
|
-
}
|
|
26
|
-
await Bun.write(
|
|
27
|
-
join(gitRoot, "agency.json"),
|
|
28
|
-
JSON.stringify(agencyJson, null, 2) + "\n",
|
|
29
|
-
)
|
|
30
|
-
await addAndCommit(gitRoot, "agency.json", "Add agency.json")
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function setupBareRemote(): Promise<string> {
|
|
34
|
-
// Create a bare repository outside the working tree to avoid polluting git status
|
|
35
|
-
const remoteDir = await createTempDir()
|
|
36
|
-
await Bun.spawn(["git", "init", "--bare", remoteDir], {
|
|
37
|
-
stdout: "pipe",
|
|
38
|
-
stderr: "pipe",
|
|
39
|
-
}).exited
|
|
40
|
-
return remoteDir
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function checkoutNonAgencyBranch(gitRoot: string): Promise<void> {
|
|
44
|
-
const proc = Bun.spawn(
|
|
45
|
-
["git", "checkout", "-q", "-b", "plain-feature", "origin/main"],
|
|
46
|
-
{
|
|
47
|
-
cwd: gitRoot,
|
|
48
|
-
stdout: "pipe",
|
|
49
|
-
stderr: "pipe",
|
|
50
|
-
},
|
|
51
|
-
)
|
|
52
|
-
const exitCode = await proc.exited
|
|
53
|
-
if (exitCode !== 0) {
|
|
54
|
-
throw new Error(await new Response(proc.stderr).text())
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
describe("push command", () => {
|
|
59
|
-
let tempDir: string
|
|
60
|
-
let remoteDir: string
|
|
61
|
-
let originalCwd: string
|
|
62
|
-
|
|
63
|
-
beforeEach(async () => {
|
|
64
|
-
tempDir = await createTempDir()
|
|
65
|
-
originalCwd = process.cwd()
|
|
66
|
-
process.chdir(tempDir)
|
|
67
|
-
|
|
68
|
-
// Set config path to non-existent file to use defaults
|
|
69
|
-
process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
|
|
70
|
-
|
|
71
|
-
// Initialize git repo
|
|
72
|
-
await initGitRepo(tempDir)
|
|
73
|
-
await createCommit(tempDir, "Initial commit")
|
|
74
|
-
|
|
75
|
-
// Rename to main if needed
|
|
76
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
77
|
-
if (currentBranch === "master") {
|
|
78
|
-
await renameBranch(tempDir, "main")
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Setup bare remote and push main
|
|
82
|
-
remoteDir = await setupBareRemote()
|
|
83
|
-
await Bun.spawn(["git", "remote", "add", "origin", remoteDir], {
|
|
84
|
-
cwd: tempDir,
|
|
85
|
-
stdout: "pipe",
|
|
86
|
-
stderr: "pipe",
|
|
87
|
-
}).exited
|
|
88
|
-
await Bun.spawn(["git", "push", "-u", "origin", "main"], {
|
|
89
|
-
cwd: tempDir,
|
|
90
|
-
stdout: "pipe",
|
|
91
|
-
stderr: "pipe",
|
|
92
|
-
}).exited
|
|
93
|
-
|
|
94
|
-
// Setup agency.json
|
|
95
|
-
await setupAgencyJson(tempDir)
|
|
96
|
-
|
|
97
|
-
// Create a source branch (with agency-- prefix per new default config)
|
|
98
|
-
await createBranch(tempDir, "agency--feature")
|
|
99
|
-
await createCommit(tempDir, "Feature work")
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
afterEach(async () => {
|
|
103
|
-
process.chdir(originalCwd)
|
|
104
|
-
delete process.env.AGENCY_CONFIG_PATH
|
|
105
|
-
await cleanupTempDir(tempDir)
|
|
106
|
-
await cleanupTempDir(remoteDir)
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
describe("basic functionality", () => {
|
|
110
|
-
test("falls back to git push outside agency context", async () => {
|
|
111
|
-
await checkoutNonAgencyBranch(tempDir)
|
|
112
|
-
await createCommit(tempDir, "Plain feature work")
|
|
113
|
-
await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
|
|
114
|
-
|
|
115
|
-
await runTestEffect(
|
|
116
|
-
push({
|
|
117
|
-
gitArgs: ["origin", "plain-feature"],
|
|
118
|
-
silent: true,
|
|
119
|
-
}),
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
const localHead = await getGitOutput(tempDir, ["rev-parse", "HEAD"])
|
|
123
|
-
const remoteHead = await getGitOutput(tempDir, [
|
|
124
|
-
"rev-parse",
|
|
125
|
-
"refs/remotes/origin/plain-feature",
|
|
126
|
-
])
|
|
127
|
-
expect(remoteHead.trim()).toBe(localHead.trim())
|
|
128
|
-
expect(await Bun.file(join(tempDir, "uncommitted.txt")).exists()).toBe(
|
|
129
|
-
true,
|
|
130
|
-
)
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test("forwards --no-verify to the git push fallback", async () => {
|
|
134
|
-
await checkoutNonAgencyBranch(tempDir)
|
|
135
|
-
await createCommit(tempDir, "Plain feature work")
|
|
136
|
-
|
|
137
|
-
await Bun.spawn(
|
|
138
|
-
["git", "config", "core.hooksPath", join(tempDir, ".git", "hooks")],
|
|
139
|
-
{
|
|
140
|
-
cwd: tempDir,
|
|
141
|
-
stdout: "pipe",
|
|
142
|
-
stderr: "pipe",
|
|
143
|
-
},
|
|
144
|
-
).exited
|
|
145
|
-
const hookPath = join(tempDir, ".git", "hooks", "pre-push")
|
|
146
|
-
await Bun.write(hookPath, "#!/bin/sh\nexit 1\n")
|
|
147
|
-
await Bun.spawn(["chmod", "+x", hookPath], {
|
|
148
|
-
cwd: tempDir,
|
|
149
|
-
stdout: "pipe",
|
|
150
|
-
stderr: "pipe",
|
|
151
|
-
}).exited
|
|
152
|
-
|
|
153
|
-
await runTestEffect(
|
|
154
|
-
push({
|
|
155
|
-
gitArgs: ["origin", "plain-feature"],
|
|
156
|
-
noVerify: true,
|
|
157
|
-
silent: true,
|
|
158
|
-
}),
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
const remoteBranches = await getGitOutput(tempDir, [
|
|
162
|
-
"ls-remote",
|
|
163
|
-
"--heads",
|
|
164
|
-
"origin",
|
|
165
|
-
"plain-feature",
|
|
166
|
-
])
|
|
167
|
-
expect(remoteBranches).toContain("plain-feature")
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
test("forwards force modes to the git push fallback", async () => {
|
|
171
|
-
await checkoutNonAgencyBranch(tempDir)
|
|
172
|
-
await createCommit(tempDir, "Initial plain feature work")
|
|
173
|
-
await Bun.spawn(["git", "push", "-u", "origin", "plain-feature"], {
|
|
174
|
-
cwd: tempDir,
|
|
175
|
-
stdout: "pipe",
|
|
176
|
-
stderr: "pipe",
|
|
177
|
-
}).exited
|
|
178
|
-
|
|
179
|
-
await createCommit(tempDir, "Remote state for lease")
|
|
180
|
-
await Bun.spawn(["git", "push"], {
|
|
181
|
-
cwd: tempDir,
|
|
182
|
-
stdout: "pipe",
|
|
183
|
-
stderr: "pipe",
|
|
184
|
-
}).exited
|
|
185
|
-
await Bun.spawn(["git", "reset", "--hard", "HEAD~1"], {
|
|
186
|
-
cwd: tempDir,
|
|
187
|
-
stdout: "pipe",
|
|
188
|
-
stderr: "pipe",
|
|
189
|
-
}).exited
|
|
190
|
-
await createCommit(tempDir, "Lease-protected rewrite")
|
|
191
|
-
|
|
192
|
-
await runTestEffect(push({ forceWithLease: true, silent: true }))
|
|
193
|
-
|
|
194
|
-
await createCommit(tempDir, "Remote state for force")
|
|
195
|
-
await Bun.spawn(["git", "push"], {
|
|
196
|
-
cwd: tempDir,
|
|
197
|
-
stdout: "pipe",
|
|
198
|
-
stderr: "pipe",
|
|
199
|
-
}).exited
|
|
200
|
-
await Bun.spawn(["git", "reset", "--hard", "HEAD~1"], {
|
|
201
|
-
cwd: tempDir,
|
|
202
|
-
stdout: "pipe",
|
|
203
|
-
stderr: "pipe",
|
|
204
|
-
}).exited
|
|
205
|
-
await createCommit(tempDir, "Forced rewrite")
|
|
206
|
-
|
|
207
|
-
await runTestEffect(push({ force: true, silent: true }))
|
|
208
|
-
|
|
209
|
-
const localHead = await getGitOutput(tempDir, ["rev-parse", "HEAD"])
|
|
210
|
-
const remoteHead = await getGitOutput(tempDir, [
|
|
211
|
-
"ls-remote",
|
|
212
|
-
"origin",
|
|
213
|
-
"refs/heads/plain-feature",
|
|
214
|
-
])
|
|
215
|
-
expect(remoteHead.split("\t")[0]).toBe(localHead.trim())
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
test("fails when there are uncommitted changes", async () => {
|
|
219
|
-
// Create uncommitted changes
|
|
220
|
-
await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
|
|
221
|
-
|
|
222
|
-
expect(
|
|
223
|
-
runTestEffect(
|
|
224
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
225
|
-
),
|
|
226
|
-
).rejects.toThrow(/uncommitted changes/)
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
test("fails when there are staged but uncommitted changes", async () => {
|
|
230
|
-
// Create a staged but uncommitted change
|
|
231
|
-
await Bun.write(join(tempDir, "staged.txt"), "staged\n")
|
|
232
|
-
await Bun.spawn(["git", "add", "staged.txt"], {
|
|
233
|
-
cwd: tempDir,
|
|
234
|
-
stdout: "pipe",
|
|
235
|
-
stderr: "pipe",
|
|
236
|
-
}).exited
|
|
237
|
-
|
|
238
|
-
expect(
|
|
239
|
-
runTestEffect(
|
|
240
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
241
|
-
),
|
|
242
|
-
).rejects.toThrow(/uncommitted changes/)
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
test("creates emit branch, pushes it, and returns to source", async () => {
|
|
246
|
-
// We're on agency--feature branch (source)
|
|
247
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
248
|
-
|
|
249
|
-
// Run push command
|
|
250
|
-
await runTestEffect(
|
|
251
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
252
|
-
)
|
|
253
|
-
|
|
254
|
-
// Should be back on agency--feature branch (source)
|
|
255
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
256
|
-
|
|
257
|
-
// emit branch (feature) should exist locally and on remote
|
|
258
|
-
const branches = await getGitOutput(tempDir, ["branch"])
|
|
259
|
-
expect(branches).toContain("feature")
|
|
260
|
-
|
|
261
|
-
const remoteBranches = await getGitOutput(tempDir, [
|
|
262
|
-
"ls-remote",
|
|
263
|
-
"--heads",
|
|
264
|
-
"origin",
|
|
265
|
-
"feature",
|
|
266
|
-
])
|
|
267
|
-
expect(remoteBranches).toContain("feature")
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
test("forwards verbose output from emit step", async () => {
|
|
271
|
-
const originalLog = console.log
|
|
272
|
-
const logMessages: string[] = []
|
|
273
|
-
console.log = (msg: string) => {
|
|
274
|
-
logMessages.push(msg)
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
try {
|
|
278
|
-
await runTestEffect(
|
|
279
|
-
push({
|
|
280
|
-
baseBranch: "main",
|
|
281
|
-
verbose: true,
|
|
282
|
-
skipFilter: true,
|
|
283
|
-
}),
|
|
284
|
-
)
|
|
285
|
-
} finally {
|
|
286
|
-
console.log = originalLog
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
expect(logMessages).toContain("Step 1: Emitting...")
|
|
290
|
-
expect(
|
|
291
|
-
logMessages.some(
|
|
292
|
-
(msg) => msg.includes("Using base branch:") && msg.includes("main"),
|
|
293
|
-
),
|
|
294
|
-
).toBe(true)
|
|
295
|
-
expect(
|
|
296
|
-
logMessages.some((msg) =>
|
|
297
|
-
msg.includes("Skipping git-filter-repo (skipFilter=true)"),
|
|
298
|
-
),
|
|
299
|
-
).toBe(true)
|
|
300
|
-
expect(logMessages.some((msg) => msg.includes("Emitted"))).toBe(true)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
test("works with custom branch name", async () => {
|
|
304
|
-
await runTestEffect(
|
|
305
|
-
push({
|
|
306
|
-
baseBranch: "main",
|
|
307
|
-
emit: "custom-pr-branch",
|
|
308
|
-
silent: true,
|
|
309
|
-
skipFilter: true,
|
|
310
|
-
}),
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
// Should be back on agency--feature branch (source)
|
|
314
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
315
|
-
|
|
316
|
-
// Custom branch should exist
|
|
317
|
-
const branches = await getGitOutput(tempDir, ["branch"])
|
|
318
|
-
expect(branches).toContain("custom-pr-branch")
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
test("recreates emit branch if it already exists", async () => {
|
|
322
|
-
// First push
|
|
323
|
-
await runTestEffect(
|
|
324
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
// Make more changes on source branch
|
|
328
|
-
await checkoutBranch(tempDir, "agency--feature")
|
|
329
|
-
await createCommit(tempDir, "More feature work")
|
|
330
|
-
|
|
331
|
-
// Second push should recreate the emit branch
|
|
332
|
-
await runTestEffect(
|
|
333
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
// Should still be back on agency--feature branch (source)
|
|
337
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
338
|
-
})
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
describe("error handling", () => {
|
|
342
|
-
test("throws when emitted CLAUDE.md references AGENCY.md", async () => {
|
|
343
|
-
await Bun.write(
|
|
344
|
-
join(tempDir, "CLAUDE.md"),
|
|
345
|
-
"# Instructions\n\n@AGENCY.md\n",
|
|
346
|
-
)
|
|
347
|
-
await addAndCommit(tempDir, "CLAUDE.md", "Add Claude agency reference")
|
|
348
|
-
|
|
349
|
-
await expect(
|
|
350
|
-
runTestEffectWithMockFilterRepo(
|
|
351
|
-
push({ baseBranch: "main", silent: true }),
|
|
352
|
-
),
|
|
353
|
-
).rejects.toThrow(
|
|
354
|
-
"Failed to create emit branch: CLAUDE.md on emitted branch feature still references @AGENCY.md",
|
|
355
|
-
)
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
test("switches to source branch when run from emit branch", async () => {
|
|
359
|
-
// First create the emit branch from source branch
|
|
360
|
-
await runTestEffect(
|
|
361
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
// Now we're on agency--feature, switch to the emit branch (feature)
|
|
365
|
-
await checkoutBranch(tempDir, "feature")
|
|
366
|
-
|
|
367
|
-
// Verify we're on the emit branch
|
|
368
|
-
expect(await getCurrentBranch(tempDir)).toBe("feature")
|
|
369
|
-
|
|
370
|
-
// Make a change on source branch that we'll push
|
|
371
|
-
await checkoutBranch(tempDir, "agency--feature")
|
|
372
|
-
await createCommit(tempDir, "Another feature commit")
|
|
373
|
-
|
|
374
|
-
// Switch back to emit branch
|
|
375
|
-
await checkoutBranch(tempDir, "feature")
|
|
376
|
-
|
|
377
|
-
// Run push from emit branch - should detect we're on emit branch,
|
|
378
|
-
// switch to source (agency--feature), and continue
|
|
379
|
-
await runTestEffect(
|
|
380
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
// Should be back on agency--feature branch (the source branch)
|
|
384
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
385
|
-
})
|
|
386
|
-
|
|
387
|
-
test("throws error when not in a git repository", async () => {
|
|
388
|
-
const nonGitDir = await createTempDir()
|
|
389
|
-
process.chdir(nonGitDir)
|
|
390
|
-
|
|
391
|
-
await expect(
|
|
392
|
-
runTestEffect(
|
|
393
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
394
|
-
),
|
|
395
|
-
).rejects.toThrow("Not in a git repository")
|
|
396
|
-
|
|
397
|
-
await cleanupTempDir(nonGitDir)
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
test("handles push failure gracefully", async () => {
|
|
401
|
-
// Remove the remote to cause push to fail
|
|
402
|
-
await Bun.spawn(["git", "remote", "remove", "origin"], {
|
|
403
|
-
cwd: tempDir,
|
|
404
|
-
stdout: "pipe",
|
|
405
|
-
stderr: "pipe",
|
|
406
|
-
}).exited
|
|
407
|
-
|
|
408
|
-
// Push should fail because no remote exists
|
|
409
|
-
await expect(
|
|
410
|
-
runTestEffect(
|
|
411
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
412
|
-
),
|
|
413
|
-
).rejects.toThrow(/No git remotes found/)
|
|
414
|
-
})
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
describe("silent mode", () => {
|
|
418
|
-
test("silent flag suppresses output", async () => {
|
|
419
|
-
// Capture output
|
|
420
|
-
const originalLog = console.log
|
|
421
|
-
let logCalled = false
|
|
422
|
-
console.log = () => {
|
|
423
|
-
logCalled = true
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
await runTestEffect(
|
|
427
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
428
|
-
)
|
|
429
|
-
|
|
430
|
-
console.log = originalLog
|
|
431
|
-
expect(logCalled).toBe(false)
|
|
432
|
-
})
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
describe("force push", () => {
|
|
436
|
-
test("force pushes when branch has diverged and --force is provided", async () => {
|
|
437
|
-
// First push
|
|
438
|
-
await runTestEffect(
|
|
439
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
// Make changes on source branch
|
|
443
|
-
await checkoutBranch(tempDir, "agency--feature")
|
|
444
|
-
await createCommit(tempDir, "More feature work")
|
|
445
|
-
|
|
446
|
-
// Modify the emit branch to create divergence
|
|
447
|
-
await checkoutBranch(tempDir, "feature")
|
|
448
|
-
await createCommit(tempDir, "Direct emit branch commit")
|
|
449
|
-
await Bun.spawn(["git", "push"], {
|
|
450
|
-
cwd: tempDir,
|
|
451
|
-
stdout: "pipe",
|
|
452
|
-
stderr: "pipe",
|
|
453
|
-
}).exited
|
|
454
|
-
|
|
455
|
-
// Go back to source branch and try to push again with --force
|
|
456
|
-
await checkoutBranch(tempDir, "agency--feature")
|
|
457
|
-
|
|
458
|
-
// Capture output to check for force push message
|
|
459
|
-
const originalLog = console.log
|
|
460
|
-
let logMessages: string[] = []
|
|
461
|
-
console.log = (msg: string) => {
|
|
462
|
-
logMessages.push(msg)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
await runTestEffect(
|
|
466
|
-
push({
|
|
467
|
-
baseBranch: "main",
|
|
468
|
-
force: true,
|
|
469
|
-
silent: false,
|
|
470
|
-
skipFilter: true,
|
|
471
|
-
}),
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
console.log = originalLog
|
|
475
|
-
|
|
476
|
-
// Should be back on agency--feature branch (source)
|
|
477
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
478
|
-
|
|
479
|
-
// Should have reported force push
|
|
480
|
-
expect(logMessages.some((msg) => msg.includes("(forced)"))).toBe(true)
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
test("suggests using --force when push is rejected without it", async () => {
|
|
484
|
-
// First push
|
|
485
|
-
await runTestEffect(
|
|
486
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
487
|
-
)
|
|
488
|
-
|
|
489
|
-
// Make changes on source branch
|
|
490
|
-
await checkoutBranch(tempDir, "agency--feature")
|
|
491
|
-
await createCommit(tempDir, "More feature work")
|
|
492
|
-
|
|
493
|
-
// Modify the emit branch to create divergence
|
|
494
|
-
await checkoutBranch(tempDir, "feature")
|
|
495
|
-
await createCommit(tempDir, "Direct emit branch commit")
|
|
496
|
-
await Bun.spawn(["git", "push"], {
|
|
497
|
-
cwd: tempDir,
|
|
498
|
-
stdout: "pipe",
|
|
499
|
-
stderr: "pipe",
|
|
500
|
-
}).exited
|
|
501
|
-
|
|
502
|
-
// Go back to source branch and try to push without --force
|
|
503
|
-
await checkoutBranch(tempDir, "agency--feature")
|
|
504
|
-
|
|
505
|
-
// Should throw error suggesting --force
|
|
506
|
-
await expect(
|
|
507
|
-
runTestEffect(
|
|
508
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
509
|
-
),
|
|
510
|
-
).rejects.toThrow(
|
|
511
|
-
/non-fast-forward update.*agency push --force|agency push --force.*non-fast-forward update/s,
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
// Should still be on agency--feature branch (not left in intermediate state)
|
|
515
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
test("reports pre-push hook output without suggesting --force", async () => {
|
|
519
|
-
await Bun.spawn(
|
|
520
|
-
["git", "config", "core.hooksPath", join(tempDir, ".git", "hooks")],
|
|
521
|
-
{
|
|
522
|
-
cwd: tempDir,
|
|
523
|
-
stdout: "pipe",
|
|
524
|
-
stderr: "pipe",
|
|
525
|
-
},
|
|
526
|
-
).exited
|
|
527
|
-
const hookPath = join(tempDir, ".git", "hooks", "pre-push")
|
|
528
|
-
await Bun.write(
|
|
529
|
-
hookPath,
|
|
530
|
-
[
|
|
531
|
-
"#!/bin/sh",
|
|
532
|
-
"printf 'pre-push hook stdout failure\\n'",
|
|
533
|
-
"printf 'pre-push hook stderr failure\\n' >&2",
|
|
534
|
-
"exit 1",
|
|
535
|
-
"",
|
|
536
|
-
].join("\n"),
|
|
537
|
-
)
|
|
538
|
-
await Bun.spawn(["chmod", "+x", hookPath], {
|
|
539
|
-
cwd: tempDir,
|
|
540
|
-
stdout: "pipe",
|
|
541
|
-
stderr: "pipe",
|
|
542
|
-
}).exited
|
|
543
|
-
|
|
544
|
-
let error: unknown
|
|
545
|
-
try {
|
|
546
|
-
await runTestEffect(
|
|
547
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
548
|
-
)
|
|
549
|
-
} catch (caught) {
|
|
550
|
-
error = caught
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
expect(error).toBeInstanceOf(Error)
|
|
554
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
555
|
-
expect(message).toContain("pre-push hook stdout failure")
|
|
556
|
-
expect(message).toContain("pre-push hook stderr failure")
|
|
557
|
-
expect(message).not.toContain("agency push --force")
|
|
558
|
-
|
|
559
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
560
|
-
})
|
|
561
|
-
|
|
562
|
-
test("does not report force push when --force is provided but not needed", async () => {
|
|
563
|
-
// Capture output to check for force push message
|
|
564
|
-
const originalLog = console.log
|
|
565
|
-
let logMessages: string[] = []
|
|
566
|
-
console.log = (msg: string) => {
|
|
567
|
-
logMessages.push(msg)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// First push with --force (but it won't actually need force)
|
|
571
|
-
await runTestEffect(
|
|
572
|
-
push({
|
|
573
|
-
baseBranch: "main",
|
|
574
|
-
force: true,
|
|
575
|
-
silent: false,
|
|
576
|
-
skipFilter: true,
|
|
577
|
-
}),
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
console.log = originalLog
|
|
581
|
-
|
|
582
|
-
// Should be back on agency--feature branch (source)
|
|
583
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
584
|
-
|
|
585
|
-
// Should NOT have reported force push (since it wasn't actually used)
|
|
586
|
-
expect(logMessages.some((msg) => msg.includes("Force pushed"))).toBe(
|
|
587
|
-
false,
|
|
588
|
-
)
|
|
589
|
-
expect(logMessages.some((msg) => msg.includes("Pushed"))).toBe(true)
|
|
590
|
-
})
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
describe("--pr flag", () => {
|
|
594
|
-
test("handles gh CLI failure gracefully and continues", async () => {
|
|
595
|
-
// Capture error output
|
|
596
|
-
const originalError = console.error
|
|
597
|
-
let errorMessages: string[] = []
|
|
598
|
-
console.error = (msg: string) => {
|
|
599
|
-
errorMessages.push(msg)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Should not throw - command should complete despite gh failure
|
|
603
|
-
// (gh will fail in test environment because there's no GitHub remote)
|
|
604
|
-
await runTestEffect(
|
|
605
|
-
push({ baseBranch: "main", pr: true, silent: true, skipFilter: true }),
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
console.error = originalError
|
|
609
|
-
|
|
610
|
-
// Should be back on agency--feature branch (command completes despite gh failure)
|
|
611
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
612
|
-
|
|
613
|
-
// Should have warned about gh failure
|
|
614
|
-
expect(
|
|
615
|
-
errorMessages.some((msg) => msg.includes("Failed to open GitHub PR")),
|
|
616
|
-
).toBe(true)
|
|
617
|
-
})
|
|
618
|
-
|
|
619
|
-
test("does not call gh when --pr flag is not set", async () => {
|
|
620
|
-
// Capture error output
|
|
621
|
-
const originalError = console.error
|
|
622
|
-
let errorMessages: string[] = []
|
|
623
|
-
console.error = (msg: string) => {
|
|
624
|
-
errorMessages.push(msg)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Push without --pr flag
|
|
628
|
-
await runTestEffect(
|
|
629
|
-
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
630
|
-
)
|
|
631
|
-
|
|
632
|
-
console.error = originalError
|
|
633
|
-
|
|
634
|
-
// Should be back on agency--feature branch (source)
|
|
635
|
-
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
|
636
|
-
|
|
637
|
-
// gh should NOT have been called (no error about GitHub PR)
|
|
638
|
-
expect(errorMessages.some((msg) => msg.includes("GitHub PR"))).toBe(false)
|
|
639
|
-
})
|
|
640
|
-
})
|
|
641
|
-
})
|