@markjaquith/agency 1.11.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +302 -113
- package/cli.ts +123 -415
- 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 -518
- package/src/commands/push.ts +0 -434
- 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 -1183
- 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/utils/pr-branch.ts
DELETED
|
@@ -1,508 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utilities for working with source and emit branch names and patterns
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Effect, pipe } from "effect"
|
|
6
|
-
import { FileSystemService } from "../services/FileSystemService"
|
|
7
|
-
import { GitService } from "../services/GitService"
|
|
8
|
-
import { AgencyMetadataService } from "../services/AgencyMetadataService"
|
|
9
|
-
import { AgencyMetadata } from "../schemas"
|
|
10
|
-
import { Schema } from "@effect/schema"
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Generate a source branch name by applying a pattern to a clean branch name.
|
|
14
|
-
* If pattern contains %branch%, it replaces it with the branch name.
|
|
15
|
-
* Otherwise, treats the pattern as a prefix.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* makeSourceBranchName("main", "agency--%branch%") // "agency--main"
|
|
19
|
-
* makeSourceBranchName("feature-foo", "wip/%branch%") // "wip/feature-foo"
|
|
20
|
-
* makeSourceBranchName("main", "agency--") // "agency--main"
|
|
21
|
-
*/
|
|
22
|
-
export function makeSourceBranchName(
|
|
23
|
-
cleanBranch: string,
|
|
24
|
-
pattern: string,
|
|
25
|
-
): string {
|
|
26
|
-
if (pattern.includes("%branch%")) {
|
|
27
|
-
return pattern.replace("%branch%", cleanBranch)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// If no %branch% placeholder, treat pattern as prefix
|
|
31
|
-
return pattern + cleanBranch
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Extract the clean branch name from a source branch name using a pattern.
|
|
36
|
-
* Returns null if the source branch name doesn't match the pattern.
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* extractCleanBranch("agency--main", "agency--%branch%") // "main"
|
|
40
|
-
* extractCleanBranch("wip/feature-foo", "wip/%branch%") // "feature-foo"
|
|
41
|
-
* extractCleanBranch("agency--main", "agency--") // "main"
|
|
42
|
-
* extractCleanBranch("main", "agency--%branch%") // null
|
|
43
|
-
*/
|
|
44
|
-
export function extractCleanBranch(
|
|
45
|
-
sourceBranchName: string,
|
|
46
|
-
pattern: string,
|
|
47
|
-
): string | null {
|
|
48
|
-
if (pattern.includes("%branch%")) {
|
|
49
|
-
// Split pattern into prefix and suffix around %branch%
|
|
50
|
-
const parts = pattern.split("%branch%")
|
|
51
|
-
if (parts.length !== 2) return null
|
|
52
|
-
|
|
53
|
-
const prefix = parts[0]!
|
|
54
|
-
const suffix = parts[1]!
|
|
55
|
-
|
|
56
|
-
// Check if source branch name matches the pattern
|
|
57
|
-
if (
|
|
58
|
-
!sourceBranchName.startsWith(prefix) ||
|
|
59
|
-
!sourceBranchName.endsWith(suffix)
|
|
60
|
-
) {
|
|
61
|
-
return null
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Extract the clean branch name by removing prefix and suffix
|
|
65
|
-
const cleanBranch = sourceBranchName.slice(
|
|
66
|
-
prefix.length,
|
|
67
|
-
sourceBranchName.length - suffix.length,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
// Ensure we extracted something (not empty string)
|
|
71
|
-
return cleanBranch.length > 0 ? cleanBranch : null
|
|
72
|
-
} else {
|
|
73
|
-
// Pattern is a prefix - check if branch starts with it
|
|
74
|
-
if (!sourceBranchName.startsWith(pattern)) {
|
|
75
|
-
return null
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const cleanBranch = sourceBranchName.slice(pattern.length)
|
|
79
|
-
return cleanBranch.length > 0 ? cleanBranch : null
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Generate an emit branch name from a clean branch name and emit pattern.
|
|
85
|
-
* If pattern is "%branch%", returns the clean branch name unchanged.
|
|
86
|
-
* Otherwise, applies the pattern the same way as makeSourceBranchName.
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* makeEmitBranchName("main", "%branch%") // "main"
|
|
90
|
-
* makeEmitBranchName("feature-foo", "%branch%--PR") // "feature-foo--PR"
|
|
91
|
-
* makeEmitBranchName("feature-foo", "PR/%branch%") // "PR/feature-foo"
|
|
92
|
-
*/
|
|
93
|
-
export function makeEmitBranchName(
|
|
94
|
-
cleanBranch: string,
|
|
95
|
-
emitPattern: string,
|
|
96
|
-
): string {
|
|
97
|
-
// Special case: "%branch%" means use clean branch name as-is
|
|
98
|
-
if (emitPattern === "%branch%") {
|
|
99
|
-
return cleanBranch
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (emitPattern.includes("%branch%")) {
|
|
103
|
-
return emitPattern.replace("%branch%", cleanBranch)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// If no %branch% placeholder, treat pattern as suffix
|
|
107
|
-
return cleanBranch + emitPattern
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Extract the clean branch name from an emit branch name using an emit pattern.
|
|
112
|
-
* Returns null if the emit branch name doesn't match the pattern.
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* extractCleanFromEmit("main", "%branch%") // "main"
|
|
116
|
-
* extractCleanFromEmit("feature-foo--PR", "%branch%--PR") // "feature-foo"
|
|
117
|
-
* extractCleanFromEmit("PR/feature-foo", "PR/%branch%") // "feature-foo"
|
|
118
|
-
* extractCleanFromEmit("main", "%branch%--PR") // null
|
|
119
|
-
*/
|
|
120
|
-
export function extractCleanFromEmit(
|
|
121
|
-
emitBranchName: string,
|
|
122
|
-
emitPattern: string,
|
|
123
|
-
): string | null {
|
|
124
|
-
// Special case: "%branch%" means emit branch is the clean branch name
|
|
125
|
-
if (emitPattern === "%branch%") {
|
|
126
|
-
return emitBranchName
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (emitPattern.includes("%branch%")) {
|
|
130
|
-
// Split pattern into prefix and suffix around %branch%
|
|
131
|
-
const parts = emitPattern.split("%branch%")
|
|
132
|
-
if (parts.length !== 2) return null
|
|
133
|
-
|
|
134
|
-
const prefix = parts[0]!
|
|
135
|
-
const suffix = parts[1]!
|
|
136
|
-
|
|
137
|
-
// Check if emit branch name matches the pattern
|
|
138
|
-
if (
|
|
139
|
-
!emitBranchName.startsWith(prefix) ||
|
|
140
|
-
!emitBranchName.endsWith(suffix)
|
|
141
|
-
) {
|
|
142
|
-
return null
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Extract the clean branch name by removing prefix and suffix
|
|
146
|
-
const cleanBranch = emitBranchName.slice(
|
|
147
|
-
prefix.length,
|
|
148
|
-
emitBranchName.length - suffix.length,
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
// Ensure we extracted something (not empty string)
|
|
152
|
-
return cleanBranch.length > 0 ? cleanBranch : null
|
|
153
|
-
} else {
|
|
154
|
-
// Pattern is a suffix - check if branch ends with it
|
|
155
|
-
if (!emitBranchName.endsWith(emitPattern)) {
|
|
156
|
-
return null
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const cleanBranch = emitBranchName.slice(0, -emitPattern.length)
|
|
160
|
-
return cleanBranch.length > 0 ? cleanBranch : null
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Result of resolving a branch pair (source and emit branches).
|
|
166
|
-
*/
|
|
167
|
-
export interface BranchPair {
|
|
168
|
-
/** The source branch name (with source pattern applied) */
|
|
169
|
-
sourceBranch: string
|
|
170
|
-
/** The emit branch name (clean or with emit pattern) */
|
|
171
|
-
emitBranch: string
|
|
172
|
-
/** Whether the current branch is the emit branch */
|
|
173
|
-
isOnEmitBranch: boolean
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Resolve the source and emit branch names from a current branch and patterns.
|
|
178
|
-
* This determines whether we're on an emit branch or source branch and provides
|
|
179
|
-
* both branch names.
|
|
180
|
-
*
|
|
181
|
-
* @example
|
|
182
|
-
* resolveBranchPair("agency--main", "agency--%branch%", "%branch%")
|
|
183
|
-
* // { sourceBranch: "agency--main", emitBranch: "main", isOnEmitBranch: false }
|
|
184
|
-
*
|
|
185
|
-
* resolveBranchPair("main", "agency--%branch%", "%branch%")
|
|
186
|
-
* // { sourceBranch: "agency--main", emitBranch: "main", isOnEmitBranch: true }
|
|
187
|
-
*/
|
|
188
|
-
function resolveBranchPair(
|
|
189
|
-
currentBranch: string,
|
|
190
|
-
sourcePattern: string,
|
|
191
|
-
emitPattern: string,
|
|
192
|
-
): BranchPair {
|
|
193
|
-
// First, try to extract clean branch from source pattern
|
|
194
|
-
const cleanFromSource = extractCleanBranch(currentBranch, sourcePattern)
|
|
195
|
-
|
|
196
|
-
if (cleanFromSource) {
|
|
197
|
-
// Current branch is a source branch (matches source pattern)
|
|
198
|
-
return {
|
|
199
|
-
sourceBranch: currentBranch,
|
|
200
|
-
emitBranch: makeEmitBranchName(cleanFromSource, emitPattern),
|
|
201
|
-
isOnEmitBranch: false,
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// If emit pattern is not just "%branch%" (which would match anything),
|
|
206
|
-
// check if current branch matches the emit pattern
|
|
207
|
-
if (emitPattern !== "%branch%") {
|
|
208
|
-
const cleanFromEmit = extractCleanFromEmit(currentBranch, emitPattern)
|
|
209
|
-
|
|
210
|
-
if (cleanFromEmit) {
|
|
211
|
-
// Current branch is an emit branch (matches emit pattern)
|
|
212
|
-
return {
|
|
213
|
-
sourceBranch: makeSourceBranchName(cleanFromEmit, sourcePattern),
|
|
214
|
-
emitBranch: currentBranch,
|
|
215
|
-
isOnEmitBranch: true,
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// If neither pattern matches (or emit pattern is "%branch%"), this is a
|
|
221
|
-
// "legacy" branch that doesn't follow the new naming convention.
|
|
222
|
-
// Treat it as a source branch where the branch name itself is the clean name.
|
|
223
|
-
return {
|
|
224
|
-
sourceBranch: currentBranch,
|
|
225
|
-
emitBranch: makeEmitBranchName(currentBranch, emitPattern),
|
|
226
|
-
isOnEmitBranch: false,
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Effect-based utilities for resolving branch pairs using agency.json
|
|
232
|
-
*/
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Try to read agency.json from the git root on a specific branch.
|
|
236
|
-
* Returns the metadata if found, null otherwise.
|
|
237
|
-
*/
|
|
238
|
-
const readAgencyJsonFromBranch = (
|
|
239
|
-
gitRoot: string,
|
|
240
|
-
branch: string,
|
|
241
|
-
): Effect.Effect<
|
|
242
|
-
AgencyMetadata | null,
|
|
243
|
-
never,
|
|
244
|
-
GitService | FileSystemService
|
|
245
|
-
> =>
|
|
246
|
-
Effect.gen(function* () {
|
|
247
|
-
const metadataService = yield* AgencyMetadataService
|
|
248
|
-
return yield* metadataService.readFromBranch(gitRoot, branch)
|
|
249
|
-
}).pipe(Effect.provide(AgencyMetadataService.Default))
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Get the agency.json metadata from the current branch (if it exists).
|
|
253
|
-
*/
|
|
254
|
-
const getCurrentBranchAgencyJson = (
|
|
255
|
-
gitRoot: string,
|
|
256
|
-
): Effect.Effect<
|
|
257
|
-
AgencyMetadata | null,
|
|
258
|
-
never,
|
|
259
|
-
FileSystemService | GitService
|
|
260
|
-
> =>
|
|
261
|
-
Effect.gen(function* () {
|
|
262
|
-
const metadataService = yield* AgencyMetadataService
|
|
263
|
-
return yield* metadataService.readFromDisk(gitRoot)
|
|
264
|
-
}).pipe(Effect.provide(AgencyMetadataService.Default))
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Find the source branch by searching all branches for an agency.json
|
|
268
|
-
* with a matching emitBranch value.
|
|
269
|
-
*/
|
|
270
|
-
const findSourceBranchByEmitBranch = (
|
|
271
|
-
gitRoot: string,
|
|
272
|
-
currentBranch: string,
|
|
273
|
-
): Effect.Effect<string | null, never, GitService | FileSystemService> =>
|
|
274
|
-
Effect.gen(function* () {
|
|
275
|
-
const git = yield* GitService
|
|
276
|
-
|
|
277
|
-
// Get all local branches
|
|
278
|
-
const branches = yield* pipe(
|
|
279
|
-
git.getAllLocalBranches(gitRoot),
|
|
280
|
-
Effect.map((allBranches) =>
|
|
281
|
-
allBranches.filter((b) => b !== currentBranch),
|
|
282
|
-
),
|
|
283
|
-
Effect.catchAll(() => Effect.succeed([] as readonly string[])),
|
|
284
|
-
)
|
|
285
|
-
|
|
286
|
-
if (branches.length === 0) {
|
|
287
|
-
return null
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Search each branch for agency.json with matching emitBranch
|
|
291
|
-
for (const branch of branches) {
|
|
292
|
-
const metadata = yield* readAgencyJsonFromBranch(gitRoot, branch)
|
|
293
|
-
|
|
294
|
-
if (metadata?.emitBranch === currentBranch) {
|
|
295
|
-
return branch
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return null
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Strategy 1: Try to resolve branch pair from current branch's agency.json.
|
|
304
|
-
* If current branch has agency.json with emitBranch that differs from current branch,
|
|
305
|
-
* we're on a source branch. If emitBranch equals current branch, we're on the emit branch.
|
|
306
|
-
*/
|
|
307
|
-
const tryResolveFromCurrentAgencyJson = (
|
|
308
|
-
gitRoot: string,
|
|
309
|
-
currentBranch: string,
|
|
310
|
-
): Effect.Effect<BranchPair | null, never, GitService | FileSystemService> =>
|
|
311
|
-
Effect.gen(function* () {
|
|
312
|
-
const currentMetadata = yield* getCurrentBranchAgencyJson(gitRoot)
|
|
313
|
-
|
|
314
|
-
if (currentMetadata?.emitBranch) {
|
|
315
|
-
// If emitBranch equals current branch, we're actually ON the emit branch,
|
|
316
|
-
// not the source branch. This can happen when skipFilter is used in tests
|
|
317
|
-
// and the agency.json is copied to the emit branch with emitBranch intact.
|
|
318
|
-
if (currentMetadata.emitBranch === currentBranch) {
|
|
319
|
-
// Return null to let Strategy 2 find the actual source branch
|
|
320
|
-
return null
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
sourceBranch: currentBranch,
|
|
324
|
-
emitBranch: currentMetadata.emitBranch,
|
|
325
|
-
isOnEmitBranch: false,
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return null
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Strategy 2: Search other branches for agency.json with matching emitBranch.
|
|
334
|
-
* If found, we're on an emit branch.
|
|
335
|
-
*/
|
|
336
|
-
const tryResolveFromOtherBranchAgencyJson = (
|
|
337
|
-
gitRoot: string,
|
|
338
|
-
currentBranch: string,
|
|
339
|
-
): Effect.Effect<BranchPair | null, never, GitService | FileSystemService> =>
|
|
340
|
-
Effect.gen(function* () {
|
|
341
|
-
const sourceBranch = yield* findSourceBranchByEmitBranch(
|
|
342
|
-
gitRoot,
|
|
343
|
-
currentBranch,
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
if (sourceBranch) {
|
|
347
|
-
return {
|
|
348
|
-
sourceBranch,
|
|
349
|
-
emitBranch: currentBranch,
|
|
350
|
-
isOnEmitBranch: true,
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
return null
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Strategy 3: For clean emit patterns ("%branch%"), check if a patterned source branch exists.
|
|
359
|
-
* If so, we're on an emit branch.
|
|
360
|
-
*/
|
|
361
|
-
const tryResolveFromPatternedSourceBranch = (
|
|
362
|
-
gitRoot: string,
|
|
363
|
-
currentBranch: string,
|
|
364
|
-
sourcePattern: string,
|
|
365
|
-
emitPattern: string,
|
|
366
|
-
): Effect.Effect<BranchPair | null, never, GitService | FileSystemService> =>
|
|
367
|
-
Effect.gen(function* () {
|
|
368
|
-
// Only applies when emit pattern is "%branch%" (clean emit branches)
|
|
369
|
-
if (emitPattern !== "%branch%") {
|
|
370
|
-
return null
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const git = yield* GitService
|
|
374
|
-
const possibleSourceBranch = makeSourceBranchName(
|
|
375
|
-
currentBranch,
|
|
376
|
-
sourcePattern,
|
|
377
|
-
)
|
|
378
|
-
|
|
379
|
-
const sourceExists = yield* pipe(
|
|
380
|
-
git.branchExists(gitRoot, possibleSourceBranch),
|
|
381
|
-
Effect.catchAll(() => Effect.succeed(false)),
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
if (sourceExists) {
|
|
385
|
-
return {
|
|
386
|
-
sourceBranch: possibleSourceBranch,
|
|
387
|
-
emitBranch: currentBranch,
|
|
388
|
-
isOnEmitBranch: true,
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return null
|
|
393
|
-
})
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Resolve branch pair using agency.json as the first source of truth,
|
|
397
|
-
* falling back to pattern-based resolution.
|
|
398
|
-
*
|
|
399
|
-
* Resolution strategies (in priority order):
|
|
400
|
-
* 1. Current branch has agency.json with emitBranch -> we're on source branch
|
|
401
|
-
* 2. Another branch has agency.json pointing to current branch -> we're on emit branch
|
|
402
|
-
* 3. Clean emit pattern and patterned source branch exists -> we're on emit branch
|
|
403
|
-
* 4. Fall back to pattern-based resolution
|
|
404
|
-
*/
|
|
405
|
-
export const resolveBranchPairWithAgencyJson = (
|
|
406
|
-
gitRoot: string,
|
|
407
|
-
currentBranch: string,
|
|
408
|
-
sourcePattern: string,
|
|
409
|
-
emitPattern: string,
|
|
410
|
-
): Effect.Effect<BranchPair, never, GitService | FileSystemService> =>
|
|
411
|
-
Effect.gen(function* () {
|
|
412
|
-
// Strategy 1: Check current branch's agency.json
|
|
413
|
-
const fromCurrentAgencyJson = yield* tryResolveFromCurrentAgencyJson(
|
|
414
|
-
gitRoot,
|
|
415
|
-
currentBranch,
|
|
416
|
-
)
|
|
417
|
-
if (fromCurrentAgencyJson) {
|
|
418
|
-
return fromCurrentAgencyJson
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Strategy 2: Search other branches for matching agency.json
|
|
422
|
-
const fromOtherBranchAgencyJson =
|
|
423
|
-
yield* tryResolveFromOtherBranchAgencyJson(gitRoot, currentBranch)
|
|
424
|
-
if (fromOtherBranchAgencyJson) {
|
|
425
|
-
return fromOtherBranchAgencyJson
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Strategy 3: Check for patterned source branch (clean emit patterns only)
|
|
429
|
-
const fromPatternedSource = yield* tryResolveFromPatternedSourceBranch(
|
|
430
|
-
gitRoot,
|
|
431
|
-
currentBranch,
|
|
432
|
-
sourcePattern,
|
|
433
|
-
emitPattern,
|
|
434
|
-
)
|
|
435
|
-
if (fromPatternedSource) {
|
|
436
|
-
return fromPatternedSource
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Strategy 4: Fall back to pattern-based resolution
|
|
440
|
-
return resolveBranchPair(currentBranch, sourcePattern, emitPattern)
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Resolve branch pair only when the current branch is in agency context.
|
|
445
|
-
* Unlike resolveBranchPairWithAgencyJson, this intentionally does not treat
|
|
446
|
-
* arbitrary legacy branches as source branches.
|
|
447
|
-
*/
|
|
448
|
-
export const resolveAgencyBranchPairWithAgencyJson = (
|
|
449
|
-
gitRoot: string,
|
|
450
|
-
currentBranch: string,
|
|
451
|
-
sourcePattern: string,
|
|
452
|
-
emitPattern: string,
|
|
453
|
-
): Effect.Effect<BranchPair | null, never, GitService | FileSystemService> =>
|
|
454
|
-
Effect.gen(function* () {
|
|
455
|
-
const fromCurrentAgencyJson = yield* tryResolveFromCurrentAgencyJson(
|
|
456
|
-
gitRoot,
|
|
457
|
-
currentBranch,
|
|
458
|
-
)
|
|
459
|
-
if (fromCurrentAgencyJson) {
|
|
460
|
-
return fromCurrentAgencyJson
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const fromOtherBranchAgencyJson =
|
|
464
|
-
yield* tryResolveFromOtherBranchAgencyJson(gitRoot, currentBranch)
|
|
465
|
-
if (fromOtherBranchAgencyJson) {
|
|
466
|
-
return fromOtherBranchAgencyJson
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const fromPatternedSource = yield* tryResolveFromPatternedSourceBranch(
|
|
470
|
-
gitRoot,
|
|
471
|
-
currentBranch,
|
|
472
|
-
sourcePattern,
|
|
473
|
-
emitPattern,
|
|
474
|
-
)
|
|
475
|
-
if (fromPatternedSource) {
|
|
476
|
-
return fromPatternedSource
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (extractCleanBranch(currentBranch, sourcePattern)) {
|
|
480
|
-
return resolveBranchPair(currentBranch, sourcePattern, emitPattern)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (emitPattern !== "%branch%") {
|
|
484
|
-
const cleanFromEmit = extractCleanFromEmit(currentBranch, emitPattern)
|
|
485
|
-
|
|
486
|
-
if (cleanFromEmit) {
|
|
487
|
-
const git = yield* GitService
|
|
488
|
-
const possibleSourceBranch = makeSourceBranchName(
|
|
489
|
-
cleanFromEmit,
|
|
490
|
-
sourcePattern,
|
|
491
|
-
)
|
|
492
|
-
const sourceExists = yield* pipe(
|
|
493
|
-
git.branchExists(gitRoot, possibleSourceBranch),
|
|
494
|
-
Effect.catchAll(() => Effect.succeed(false)),
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
if (sourceExists) {
|
|
498
|
-
return {
|
|
499
|
-
sourceBranch: possibleSourceBranch,
|
|
500
|
-
emitBranch: currentBranch,
|
|
501
|
-
isOnEmitBranch: true,
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return null
|
|
508
|
-
})
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
import { withSpinner } from "./spinner"
|
|
4
|
-
|
|
5
|
-
describe("withSpinner", () => {
|
|
6
|
-
test("clears and stops the spinner when an Effect failure has no fail text", async () => {
|
|
7
|
-
const originalNodeEnv = process.env.NODE_ENV
|
|
8
|
-
const originalBunEnv = process.env.BUN_ENV
|
|
9
|
-
delete process.env.NODE_ENV
|
|
10
|
-
delete process.env.BUN_ENV
|
|
11
|
-
|
|
12
|
-
const calls: string[] = []
|
|
13
|
-
const error = new Error("boom")
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
await expect(
|
|
17
|
-
Effect.runPromise(
|
|
18
|
-
withSpinner(Effect.fail(error), {
|
|
19
|
-
text: "Working",
|
|
20
|
-
createSpinner: () => ({
|
|
21
|
-
succeed: () => calls.push("succeed"),
|
|
22
|
-
fail: () => calls.push("fail"),
|
|
23
|
-
clear: () => calls.push("clear"),
|
|
24
|
-
stop: () => calls.push("stop"),
|
|
25
|
-
}),
|
|
26
|
-
}),
|
|
27
|
-
),
|
|
28
|
-
).rejects.toThrow("boom")
|
|
29
|
-
|
|
30
|
-
expect(calls).toEqual(["clear", "stop"])
|
|
31
|
-
} finally {
|
|
32
|
-
if (originalNodeEnv === undefined) {
|
|
33
|
-
delete process.env.NODE_ENV
|
|
34
|
-
} else {
|
|
35
|
-
process.env.NODE_ENV = originalNodeEnv
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (originalBunEnv === undefined) {
|
|
39
|
-
delete process.env.BUN_ENV
|
|
40
|
-
} else {
|
|
41
|
-
process.env.BUN_ENV = originalBunEnv
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
})
|
package/src/utils/spinner.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import ora from "ora"
|
|
2
|
-
import { Effect, Exit } from "effect"
|
|
3
|
-
|
|
4
|
-
interface Spinner {
|
|
5
|
-
succeed: (text?: string) => unknown
|
|
6
|
-
fail: (text?: string) => unknown
|
|
7
|
-
clear: () => unknown
|
|
8
|
-
stop: () => unknown
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Check if we're running in a test environment
|
|
13
|
-
*/
|
|
14
|
-
const isTestEnvironment = (): boolean => {
|
|
15
|
-
return process.env.NODE_ENV === "test" || process.env.BUN_ENV === "test"
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Configuration for a spinner operation
|
|
20
|
-
*/
|
|
21
|
-
interface SpinnerConfig {
|
|
22
|
-
/** The message to show while the spinner is running */
|
|
23
|
-
text: string
|
|
24
|
-
/** The message to show when the operation succeeds */
|
|
25
|
-
successText?: string
|
|
26
|
-
/** The message to show when the operation fails */
|
|
27
|
-
failText?: string
|
|
28
|
-
/** Whether the spinner is enabled (defaults to true) */
|
|
29
|
-
enabled?: boolean
|
|
30
|
-
/** Creates the spinner instance. Intended for tests. */
|
|
31
|
-
createSpinner?: (text: string) => Spinner
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Wraps an Effect operation with a spinner that shows progress
|
|
36
|
-
* and updates with success/failure messages.
|
|
37
|
-
*
|
|
38
|
-
* @param effect The Effect operation to run
|
|
39
|
-
* @param config Configuration for the spinner
|
|
40
|
-
* @returns The result of the Effect operation
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* ```ts
|
|
44
|
-
* const result = yield* withSpinner(
|
|
45
|
-
* someOperation(),
|
|
46
|
-
* {
|
|
47
|
-
* text: "Processing...",
|
|
48
|
-
* successText: "Processing complete",
|
|
49
|
-
* failText: "Processing failed"
|
|
50
|
-
* }
|
|
51
|
-
* )
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
export const withSpinner = <A, E, R>(
|
|
55
|
-
effect: Effect.Effect<A, E, R>,
|
|
56
|
-
config: SpinnerConfig,
|
|
57
|
-
): Effect.Effect<A, E, R> => {
|
|
58
|
-
const { text, successText, failText, enabled = true } = config
|
|
59
|
-
|
|
60
|
-
// Disable spinner in test environment or when explicitly disabled
|
|
61
|
-
if (!enabled || isTestEnvironment()) {
|
|
62
|
-
return effect
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const createSpinner =
|
|
66
|
-
config.createSpinner ??
|
|
67
|
-
((text: string) =>
|
|
68
|
-
ora({
|
|
69
|
-
text,
|
|
70
|
-
spinner: "dots",
|
|
71
|
-
color: "cyan",
|
|
72
|
-
}).start())
|
|
73
|
-
|
|
74
|
-
return Effect.acquireUseRelease(
|
|
75
|
-
Effect.sync(() => createSpinner(text)),
|
|
76
|
-
() => effect,
|
|
77
|
-
(spinner, exit) =>
|
|
78
|
-
Effect.sync(() => {
|
|
79
|
-
if (Exit.isSuccess(exit)) {
|
|
80
|
-
if (successText) {
|
|
81
|
-
spinner.succeed(successText)
|
|
82
|
-
} else {
|
|
83
|
-
spinner.stop()
|
|
84
|
-
}
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (failText) {
|
|
89
|
-
spinner.fail(failText)
|
|
90
|
-
} else {
|
|
91
|
-
spinner.clear()
|
|
92
|
-
spinner.stop()
|
|
93
|
-
}
|
|
94
|
-
}),
|
|
95
|
-
)
|
|
96
|
-
}
|