@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
package/src/commands/base.ts
DELETED
|
@@ -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
|
-
})
|