@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.
Files changed (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -415
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -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 +172 -419
  24. package/src/commands/work.ts +47 -166
  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/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -518
  73. package/src/commands/push.ts +0 -434
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1183
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. package/templates/opencode.json +0 -4
@@ -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
- })
@@ -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
- }