@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.
Files changed (132) hide show
  1. package/README.md +313 -117
  2. package/cli.ts +124 -420
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +266 -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 +326 -399
  24. package/src/commands/work.ts +153 -153
  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/work-target.test.ts +77 -0
  53. package/src/workbase/work-target.ts +180 -0
  54. package/src/workbase/worktree-command.test.ts +61 -0
  55. package/src/workbase/worktree-command.ts +52 -0
  56. package/src/commands/base.test.ts +0 -198
  57. package/src/commands/base.ts +0 -198
  58. package/src/commands/clean.test.ts +0 -299
  59. package/src/commands/clean.ts +0 -274
  60. package/src/commands/completions.test.ts +0 -51
  61. package/src/commands/completions.ts +0 -308
  62. package/src/commands/emit.integration.test.ts +0 -549
  63. package/src/commands/emit.test.ts +0 -576
  64. package/src/commands/emit.ts +0 -700
  65. package/src/commands/emitted.test.ts +0 -226
  66. package/src/commands/emitted.ts +0 -57
  67. package/src/commands/loop.test.ts +0 -621
  68. package/src/commands/loop.ts +0 -406
  69. package/src/commands/merge.integration.test.ts +0 -195
  70. package/src/commands/merge.test.ts +0 -246
  71. package/src/commands/merge.ts +0 -244
  72. package/src/commands/pull.test.ts +0 -395
  73. package/src/commands/pull.ts +0 -205
  74. package/src/commands/push.test.ts +0 -641
  75. package/src/commands/push.ts +0 -482
  76. package/src/commands/rebase.test.ts +0 -521
  77. package/src/commands/rebase.ts +0 -232
  78. package/src/commands/save.test.ts +0 -247
  79. package/src/commands/save.ts +0 -162
  80. package/src/commands/source.test.ts +0 -195
  81. package/src/commands/source.ts +0 -72
  82. package/src/commands/switch.test.ts +0 -194
  83. package/src/commands/switch.ts +0 -84
  84. package/src/commands/task-branching.test.ts +0 -633
  85. package/src/commands/task-continue.test.ts +0 -311
  86. package/src/commands/task-edit.test.ts +0 -235
  87. package/src/commands/task-main.test.ts +0 -1066
  88. package/src/commands/task-squash.test.ts +0 -266
  89. package/src/commands/tasks.test.ts +0 -391
  90. package/src/commands/tasks.ts +0 -161
  91. package/src/commands/template-delete.test.ts +0 -178
  92. package/src/commands/template-delete.ts +0 -98
  93. package/src/commands/template-list.test.ts +0 -135
  94. package/src/commands/template-list.ts +0 -87
  95. package/src/commands/template-view.test.ts +0 -158
  96. package/src/commands/template-view.ts +0 -86
  97. package/src/commands/template.test.ts +0 -32
  98. package/src/commands/template.ts +0 -96
  99. package/src/commands/use.test.ts +0 -87
  100. package/src/commands/use.ts +0 -97
  101. package/src/constants.ts +0 -10
  102. package/src/errors.ts +0 -17
  103. package/src/schemas.ts +0 -33
  104. package/src/services/AgencyMetadataService.ts +0 -311
  105. package/src/services/ClaudeService.test.ts +0 -184
  106. package/src/services/ClaudeService.ts +0 -91
  107. package/src/services/ConfigService.ts +0 -115
  108. package/src/services/FilterRepoService.ts +0 -164
  109. package/src/services/FormatterService.test.ts +0 -432
  110. package/src/services/FormatterService.ts +0 -219
  111. package/src/services/GitService.ts +0 -1187
  112. package/src/services/MockFilterRepoService.ts +0 -140
  113. package/src/services/OpencodeService.ts +0 -263
  114. package/src/services/PromptService.ts +0 -183
  115. package/src/services/TemplateService.ts +0 -81
  116. package/src/types/native-exec.d.ts +0 -8
  117. package/src/utils/colors.ts +0 -178
  118. package/src/utils/git-path.ts +0 -6
  119. package/src/utils/glob.test.ts +0 -161
  120. package/src/utils/glob.ts +0 -78
  121. package/src/utils/paths.ts +0 -51
  122. package/src/utils/pr-branch.test.ts +0 -376
  123. package/src/utils/pr-branch.ts +0 -508
  124. package/src/utils/spinner.test.ts +0 -45
  125. package/src/utils/spinner.ts +0 -96
  126. package/src/utils/task-parser.test.ts +0 -172
  127. package/src/utils/task-parser.ts +0 -93
  128. package/templates/AGENCY.md +0 -22
  129. package/templates/AGENTS.md +0 -11
  130. package/templates/CLAUDE.md +0 -3
  131. package/templates/TASK.md +0 -5
  132. package/templates/opencode.json +0 -4
@@ -1,198 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test"
2
- import { base } from "./base"
3
- import {
4
- createTempDir,
5
- cleanupTempDir,
6
- initGitRepo,
7
- getGitConfig,
8
- runTestEffect,
9
- } from "../test-utils"
10
- import { getBaseBranchFromMetadata, writeAgencyMetadata } from "../types"
11
-
12
- describe("base", () => {
13
- let testDir: string
14
- let originalCwd: string
15
-
16
- beforeEach(async () => {
17
- originalCwd = process.cwd()
18
- testDir = await createTempDir()
19
- await initGitRepo(testDir)
20
- process.chdir(testDir)
21
- })
22
-
23
- afterEach(async () => {
24
- process.chdir(originalCwd)
25
- await cleanupTempDir(testDir)
26
- })
27
-
28
- describe("base set", () => {
29
- test("sets base branch for current branch", async () => {
30
- // Create a feature branch
31
- await Bun.spawn(["git", "checkout", "-b", "feature"], {
32
- cwd: testDir,
33
- stdout: "pipe",
34
- stderr: "pipe",
35
- }).exited
36
-
37
- // Create agency.json first
38
- await writeAgencyMetadata(testDir, {
39
- version: 1 as const,
40
- template: "test",
41
- injectedFiles: [],
42
- createdAt: new Date().toISOString(),
43
- } as any)
44
-
45
- // Set base branch
46
- await runTestEffect(
47
- base({
48
- subcommand: "set",
49
- args: ["main"],
50
- silent: true,
51
- }),
52
- )
53
-
54
- // Verify it was saved
55
- const savedBase = await getBaseBranchFromMetadata(testDir)
56
- expect(savedBase).toBe("main")
57
- })
58
-
59
- test("sets repository-level default base branch", async () => {
60
- // Set repo-level base branch
61
- await runTestEffect(
62
- base({
63
- subcommand: "set",
64
- args: ["main"],
65
- repo: true,
66
- silent: true,
67
- }),
68
- )
69
-
70
- // Verify it was saved to git config
71
- const savedBase = await getGitConfig("agency.baseBranch", testDir)
72
- expect(savedBase).toBe("main")
73
- })
74
-
75
- test("throws error if base branch does not exist", async () => {
76
- await expect(
77
- runTestEffect(
78
- base({
79
- subcommand: "set",
80
- args: ["nonexistent"],
81
- silent: true,
82
- }),
83
- ),
84
- ).rejects.toThrow("does not exist")
85
- })
86
- })
87
-
88
- describe("base get", () => {
89
- test("gets base branch for current branch", async () => {
90
- // Create a feature branch
91
- await Bun.spawn(["git", "checkout", "-b", "feature"], {
92
- cwd: testDir,
93
- stdout: "pipe",
94
- stderr: "pipe",
95
- }).exited
96
-
97
- // Create agency.json with base branch
98
- await writeAgencyMetadata(testDir, {
99
- version: 1,
100
- template: "test",
101
- injectedFiles: [],
102
- baseBranch: "main",
103
- createdAt: new Date().toISOString(),
104
- } as any)
105
-
106
- // Mock console.log to capture output
107
- const logs: string[] = []
108
- const originalLog = console.log
109
- console.log = (...args: any[]) => logs.push(args.join(" "))
110
-
111
- try {
112
- await runTestEffect(
113
- base({
114
- subcommand: "get",
115
- args: [],
116
- silent: false,
117
- }),
118
- )
119
- expect(logs[0]).toBe("main")
120
- } finally {
121
- console.log = originalLog
122
- }
123
- })
124
-
125
- test("throws error if no base branch configured", async () => {
126
- // Create a feature branch
127
- await Bun.spawn(["git", "checkout", "-b", "feature"], {
128
- cwd: testDir,
129
- stdout: "pipe",
130
- stderr: "pipe",
131
- }).exited
132
-
133
- // Create agency.json without base branch
134
- await writeAgencyMetadata(testDir, {
135
- version: 1,
136
- template: "test",
137
- injectedFiles: [],
138
- createdAt: new Date().toISOString(),
139
- } as any)
140
-
141
- await expect(
142
- runTestEffect(
143
- base({
144
- subcommand: "get",
145
- args: [],
146
- silent: true,
147
- }),
148
- ),
149
- ).rejects.toThrow("No base branch configured")
150
- })
151
- })
152
-
153
- describe("base command", () => {
154
- test("requires subcommand", async () => {
155
- await expect(
156
- runTestEffect(base({ args: [], silent: true })),
157
- ).rejects.toThrow("Subcommand is required")
158
- })
159
-
160
- test("handles 'set' subcommand", async () => {
161
- await Bun.spawn(["git", "checkout", "-b", "feature"], {
162
- cwd: testDir,
163
- stdout: "pipe",
164
- stderr: "pipe",
165
- }).exited
166
-
167
- await writeAgencyMetadata(testDir, {
168
- version: 1,
169
- template: "test",
170
- injectedFiles: [],
171
- createdAt: new Date().toISOString(),
172
- } as any)
173
-
174
- await runTestEffect(
175
- base({
176
- subcommand: "set",
177
- args: ["main"],
178
- silent: true,
179
- }),
180
- )
181
-
182
- const savedBase = await getBaseBranchFromMetadata(testDir)
183
- expect(savedBase).toBe("main")
184
- })
185
-
186
- test("throws error for unknown subcommand", async () => {
187
- await expect(
188
- runTestEffect(
189
- base({
190
- subcommand: "unknown",
191
- args: [],
192
- silent: true,
193
- }),
194
- ),
195
- ).rejects.toThrow("Unknown subcommand")
196
- })
197
- })
198
- })
@@ -1,198 +0,0 @@
1
- import { Effect } from "effect"
2
- import { GitService } from "../services/GitService"
3
- import { setBaseBranchInMetadata, getBaseBranchFromMetadata } from "../types"
4
- import highlight, { done } from "../utils/colors"
5
- import {
6
- createLoggers,
7
- ensureGitRepo,
8
- ensureBranchExists,
9
- } from "../utils/effect"
10
-
11
- interface BaseOptions {
12
- subcommand?: string
13
- args: string[]
14
- repo?: boolean
15
- silent?: boolean
16
- verbose?: boolean
17
- }
18
-
19
- interface BaseSetOptions {
20
- baseBranch: string
21
- repo?: boolean
22
- silent?: boolean
23
- verbose?: boolean
24
- }
25
-
26
- interface BaseGetOptions {
27
- repo?: boolean
28
- silent?: boolean
29
- verbose?: boolean
30
- }
31
-
32
- // Effect-based implementation
33
- const baseSetEffect = (options: BaseSetOptions) =>
34
- Effect.gen(function* () {
35
- const { baseBranch, repo = false } = options
36
- const { log, verboseLog } = createLoggers(options)
37
-
38
- const git = yield* GitService
39
- const gitRoot = yield* ensureGitRepo()
40
-
41
- // Validate that the base branch exists
42
- yield* ensureBranchExists(
43
- gitRoot,
44
- baseBranch,
45
- `Base branch ${highlight.branch(baseBranch)} does not exist. Please provide a valid branch name.`,
46
- )
47
-
48
- if (repo) {
49
- // Set repository-level default base branch in git config
50
- yield* git.setDefaultBaseBranchConfig(baseBranch, gitRoot)
51
- log(
52
- done(
53
- `Set repository-level default base branch to ${highlight.branch(baseBranch)}`,
54
- ),
55
- )
56
- } else {
57
- // Set branch-specific base branch in agency.json
58
- const currentBranch = yield* git.getCurrentBranch(gitRoot)
59
- verboseLog(`Current branch: ${highlight.branch(currentBranch)}`)
60
-
61
- // Use Effect.tryPromise for metadata functions
62
- yield* Effect.tryPromise({
63
- try: () => setBaseBranchInMetadata(gitRoot, baseBranch),
64
- catch: (error) =>
65
- new Error(`Failed to set base branch in metadata: ${error}`),
66
- })
67
- log(
68
- done(
69
- `Set base branch to ${highlight.branch(baseBranch)} for ${highlight.branch(currentBranch)}`,
70
- ),
71
- )
72
- }
73
- })
74
-
75
- // Effect-based implementation
76
- const baseGetEffect = (options: BaseGetOptions) =>
77
- Effect.gen(function* () {
78
- const { repo = false } = options
79
- const { log, verboseLog } = createLoggers(options)
80
-
81
- const git = yield* GitService
82
- const gitRoot = yield* ensureGitRepo()
83
-
84
- let currentBase: string | null
85
-
86
- if (repo) {
87
- // Get repository-level default base branch from git config
88
- verboseLog("Reading repository-level default base branch from git config")
89
- currentBase = yield* git.getDefaultBaseBranchConfig(gitRoot)
90
-
91
- if (!currentBase) {
92
- return yield* Effect.fail(
93
- new Error(
94
- "No repository-level base branch configured. Use 'agency base set --repo <branch>' to set one.",
95
- ),
96
- )
97
- }
98
- } else {
99
- // Get current branch
100
- const currentBranch = yield* git.getCurrentBranch(gitRoot)
101
- verboseLog(`Current branch: ${highlight.branch(currentBranch)}`)
102
-
103
- // Get branch-specific base branch from agency.json
104
- verboseLog("Reading branch-specific base branch from agency.json")
105
- currentBase = yield* Effect.tryPromise({
106
- try: () => getBaseBranchFromMetadata(gitRoot),
107
- catch: (error) =>
108
- new Error(`Failed to get base branch from metadata: ${error}`),
109
- })
110
-
111
- if (!currentBase) {
112
- return yield* Effect.fail(
113
- new Error(
114
- `No base branch configured for ${highlight.branch(currentBranch)}. Use 'agency base set <branch>' to set one.`,
115
- ),
116
- )
117
- }
118
- }
119
-
120
- log(currentBase)
121
- })
122
-
123
- export const base = (options: BaseOptions) =>
124
- Effect.gen(function* () {
125
- const {
126
- subcommand,
127
- args,
128
- repo = false,
129
- silent = false,
130
- verbose = false,
131
- } = options
132
-
133
- if (!subcommand) {
134
- return yield* Effect.fail(
135
- new Error("Subcommand is required. Usage: agency base <subcommand>"),
136
- )
137
- }
138
-
139
- switch (subcommand) {
140
- case "set": {
141
- if (!args[0]) {
142
- return yield* Effect.fail(
143
- new Error(
144
- "Base branch argument is required. Usage: agency base set <branch>",
145
- ),
146
- )
147
- }
148
- return yield* baseSetEffect({
149
- baseBranch: args[0],
150
- repo,
151
- silent,
152
- verbose,
153
- })
154
- }
155
- case "get": {
156
- return yield* baseGetEffect({
157
- repo,
158
- silent,
159
- verbose,
160
- })
161
- }
162
- default:
163
- return yield* Effect.fail(
164
- new Error(
165
- `Unknown subcommand '${subcommand}'. Available subcommands: set, get`,
166
- ),
167
- )
168
- }
169
- })
170
-
171
- export const help = `
172
- Usage: agency base <subcommand> [options]
173
-
174
- Get or set the base branch for the current feature branch.
175
-
176
- Subcommands:
177
- set <branch> Set the base branch for the current feature branch
178
- get Get the configured base branch
179
-
180
- Options:
181
- --repo Use repository-level default instead of branch-specific
182
- -h, --help Show this help message
183
- -s, --silent Suppress output messages
184
- -v, --verbose Show verbose output
185
-
186
- Examples:
187
- agency base set origin/main # Set base branch for current branch
188
- agency base set --repo origin/main # Set repository-level default for all branches
189
- agency base get # Get branch-specific base branch
190
- agency base get --repo # Get repository-level default base branch
191
-
192
- Notes:
193
- - The base branch must exist in the repository
194
- - Branch-specific base branch is saved in agency.json (committed with the branch)
195
- - Repository-level default is saved in .git/config (not committed)
196
- - Branch-specific settings take precedence over repository-level defaults
197
- - Base branch configuration is used by 'agency emit' when creating emit branches
198
- `
@@ -1,299 +0,0 @@
1
- import { test, expect, describe, beforeEach, afterEach } from "bun:test"
2
- import { join } from "path"
3
- import { clean } from "./clean"
4
- import {
5
- createTempDir,
6
- cleanupTempDir,
7
- initGitRepo,
8
- initAgency,
9
- getGitOutput,
10
- getCurrentBranch,
11
- createCommit,
12
- checkoutBranch,
13
- runTestEffect,
14
- } from "../test-utils"
15
-
16
- describe("clean command", () => {
17
- let tempDir: string
18
- let originalCwd: string
19
-
20
- beforeEach(async () => {
21
- tempDir = await createTempDir()
22
- originalCwd = process.cwd()
23
- process.chdir(tempDir)
24
-
25
- // Set config path to non-existent file to use defaults
26
- process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
27
- // Set config dir to temp dir to avoid picking up user's config files
28
- process.env.AGENCY_CONFIG_DIR = await createTempDir()
29
-
30
- // Initialize git repo with main branch
31
- await initGitRepo(tempDir)
32
-
33
- // Initialize agency in main branch
34
- await initAgency(tempDir, "test")
35
- })
36
-
37
- afterEach(async () => {
38
- process.chdir(originalCwd)
39
- delete process.env.AGENCY_CONFIG_PATH
40
- if (process.env.AGENCY_CONFIG_DIR) {
41
- await cleanupTempDir(process.env.AGENCY_CONFIG_DIR)
42
- delete process.env.AGENCY_CONFIG_DIR
43
- }
44
- await cleanupTempDir(tempDir)
45
- })
46
-
47
- describe("--merged-into flag requirement", () => {
48
- test("throws error when --merged-into flag is not provided", async () => {
49
- expect(runTestEffect(clean({ silent: true }))).rejects.toThrow(
50
- "--merged-into flag is required",
51
- )
52
- })
53
-
54
- test("throws error when specified branch does not exist", async () => {
55
- expect(
56
- runTestEffect(clean({ mergedInto: "nonexistent", silent: true })),
57
- ).rejects.toThrow("does not exist")
58
- })
59
- })
60
-
61
- describe("basic functionality", () => {
62
- test("finds and deletes branches merged into target", async () => {
63
- // Create feature branch, make commits, and merge to main
64
- await checkoutBranch(tempDir, "main")
65
- await Bun.spawn(["git", "checkout", "-b", "feature-1"], {
66
- cwd: tempDir,
67
- stdout: "pipe",
68
- stderr: "pipe",
69
- }).exited
70
- await createCommit(tempDir, "Feature 1 commit")
71
- await checkoutBranch(tempDir, "main")
72
- await Bun.spawn(["git", "merge", "--no-ff", "feature-1"], {
73
- cwd: tempDir,
74
- stdout: "pipe",
75
- stderr: "pipe",
76
- }).exited
77
-
78
- // Run clean command
79
- await runTestEffect(clean({ mergedInto: "main", silent: true }))
80
-
81
- // Verify feature-1 was deleted
82
- const branches = await getGitOutput(tempDir, ["branch", "--list"])
83
- expect(branches).not.toContain("feature-1")
84
- expect(branches).toContain("main")
85
- })
86
-
87
- test("finds source branches for merged emit branches", async () => {
88
- // Create source branch with agency pattern
89
- await checkoutBranch(tempDir, "main")
90
- await Bun.spawn(["git", "checkout", "-b", "agency--feature-2"], {
91
- cwd: tempDir,
92
- stdout: "pipe",
93
- stderr: "pipe",
94
- }).exited
95
-
96
- // Create agency.json with emitBranch
97
- await Bun.write(
98
- join(tempDir, "agency.json"),
99
- JSON.stringify({
100
- version: 1,
101
- injectedFiles: ["AGENTS.md"],
102
- template: "test",
103
- emitBranch: "feature-2",
104
- createdAt: new Date().toISOString(),
105
- }),
106
- )
107
- await Bun.spawn(["git", "add", "agency.json"], {
108
- cwd: tempDir,
109
- stdout: "pipe",
110
- stderr: "pipe",
111
- }).exited
112
- await createCommit(tempDir, "Feature 2 commit")
113
-
114
- // Create the emit branch manually
115
- await Bun.spawn(["git", "checkout", "-b", "feature-2"], {
116
- cwd: tempDir,
117
- stdout: "pipe",
118
- stderr: "pipe",
119
- }).exited
120
-
121
- // Merge emit branch to main
122
- await checkoutBranch(tempDir, "main")
123
- await Bun.spawn(["git", "merge", "--no-ff", "feature-2"], {
124
- cwd: tempDir,
125
- stdout: "pipe",
126
- stderr: "pipe",
127
- }).exited
128
-
129
- // Run clean command
130
- await runTestEffect(clean({ mergedInto: "main", silent: true }))
131
-
132
- // Verify both emit and source branches were deleted
133
- const branches = await getGitOutput(tempDir, ["branch", "--list"])
134
- expect(branches).not.toContain("feature-2")
135
- expect(branches).not.toContain("agency--feature-2")
136
- expect(branches).toContain("main")
137
- })
138
-
139
- test("does not delete unmerged branches", async () => {
140
- // Create an unmerged feature branch
141
- await checkoutBranch(tempDir, "main")
142
- await Bun.spawn(["git", "checkout", "-b", "feature-unmerged"], {
143
- cwd: tempDir,
144
- stdout: "pipe",
145
- stderr: "pipe",
146
- }).exited
147
- await createCommit(tempDir, "Unmerged commit")
148
-
149
- // Run clean command
150
- await checkoutBranch(tempDir, "main")
151
- await runTestEffect(clean({ mergedInto: "main", silent: true }))
152
-
153
- // Verify unmerged branch still exists
154
- const branches = await getGitOutput(tempDir, ["branch", "--list"])
155
- expect(branches).toContain("feature-unmerged")
156
- })
157
-
158
- test("does not delete the target branch itself", async () => {
159
- // Run clean on main (nothing should happen)
160
- await checkoutBranch(tempDir, "main")
161
- await runTestEffect(clean({ mergedInto: "main", silent: true }))
162
-
163
- // Verify main still exists
164
- const branches = await getGitOutput(tempDir, ["branch", "--list"])
165
- expect(branches).toContain("main")
166
- })
167
- })
168
-
169
- describe("dry-run mode", () => {
170
- test("shows branches without deleting in dry-run mode", async () => {
171
- // Create and merge feature branch
172
- await checkoutBranch(tempDir, "main")
173
- await Bun.spawn(["git", "checkout", "-b", "feature-dry"], {
174
- cwd: tempDir,
175
- stdout: "pipe",
176
- stderr: "pipe",
177
- }).exited
178
- await createCommit(tempDir, "Feature dry commit")
179
- await checkoutBranch(tempDir, "main")
180
- await Bun.spawn(["git", "merge", "--no-ff", "feature-dry"], {
181
- cwd: tempDir,
182
- stdout: "pipe",
183
- stderr: "pipe",
184
- }).exited
185
-
186
- // Run clean in dry-run mode
187
- await runTestEffect(
188
- clean({ mergedInto: "main", dryRun: true, silent: true }),
189
- )
190
-
191
- // Verify branch still exists
192
- const branches = await getGitOutput(tempDir, ["branch", "--list"])
193
- expect(branches).toContain("feature-dry")
194
- })
195
- })
196
-
197
- describe("branch switching", () => {
198
- test("switches away from branch being deleted if currently on it", async () => {
199
- // Create and merge feature branch
200
- await checkoutBranch(tempDir, "main")
201
- await Bun.spawn(["git", "checkout", "-b", "feature-switch"], {
202
- cwd: tempDir,
203
- stdout: "pipe",
204
- stderr: "pipe",
205
- }).exited
206
- await createCommit(tempDir, "Feature switch commit")
207
- await checkoutBranch(tempDir, "main")
208
- await Bun.spawn(["git", "merge", "--no-ff", "feature-switch"], {
209
- cwd: tempDir,
210
- stdout: "pipe",
211
- stderr: "pipe",
212
- }).exited
213
-
214
- // Checkout the branch that will be deleted
215
- await checkoutBranch(tempDir, "feature-switch")
216
-
217
- // Run clean command
218
- await runTestEffect(clean({ mergedInto: "main", silent: true }))
219
-
220
- // Verify we're now on main
221
- const currentBranch = await getCurrentBranch(tempDir)
222
- expect(currentBranch).toBe("main")
223
-
224
- // Verify branch was deleted
225
- const branches = await getGitOutput(tempDir, ["branch", "--list"])
226
- expect(branches).not.toContain("feature-switch")
227
- })
228
- })
229
-
230
- describe("multiple branches", () => {
231
- test("deletes multiple merged branches at once", async () => {
232
- await checkoutBranch(tempDir, "main")
233
-
234
- // Create and merge feature-1
235
- await Bun.spawn(["git", "checkout", "-b", "feature-1"], {
236
- cwd: tempDir,
237
- stdout: "pipe",
238
- stderr: "pipe",
239
- }).exited
240
- await createCommit(tempDir, "Feature 1 commit")
241
- await checkoutBranch(tempDir, "main")
242
- await Bun.spawn(["git", "merge", "--no-ff", "feature-1"], {
243
- cwd: tempDir,
244
- stdout: "pipe",
245
- stderr: "pipe",
246
- }).exited
247
-
248
- // Create and merge feature-2
249
- await Bun.spawn(["git", "checkout", "-b", "feature-2"], {
250
- cwd: tempDir,
251
- stdout: "pipe",
252
- stderr: "pipe",
253
- }).exited
254
- await createCommit(tempDir, "Feature 2 commit")
255
- await checkoutBranch(tempDir, "main")
256
- await Bun.spawn(["git", "merge", "--no-ff", "feature-2"], {
257
- cwd: tempDir,
258
- stdout: "pipe",
259
- stderr: "pipe",
260
- }).exited
261
-
262
- // Run clean command
263
- await runTestEffect(clean({ mergedInto: "main", silent: true }))
264
-
265
- // Verify both branches were deleted
266
- const branches = await getGitOutput(tempDir, ["branch", "--list"])
267
- expect(branches).not.toContain("feature-1")
268
- expect(branches).not.toContain("feature-2")
269
- expect(branches).toContain("main")
270
- })
271
- })
272
-
273
- describe("no merged branches", () => {
274
- test("handles case with no merged branches gracefully", async () => {
275
- // Don't create any feature branches, just run clean
276
- await checkoutBranch(tempDir, "main")
277
-
278
- // Should not throw
279
- await runTestEffect(clean({ mergedInto: "main", silent: true }))
280
-
281
- // Main should still exist
282
- const branches = await getGitOutput(tempDir, ["branch", "--list"])
283
- expect(branches).toContain("main")
284
- })
285
- })
286
-
287
- describe("error handling", () => {
288
- test("throws error when not in a git repository", async () => {
289
- const nonGitDir = await createTempDir()
290
- process.chdir(nonGitDir)
291
-
292
- expect(
293
- runTestEffect(clean({ mergedInto: "main", silent: true })),
294
- ).rejects.toThrow("Not in a git repository")
295
-
296
- await cleanupTempDir(nonGitDir)
297
- })
298
- })
299
- })