@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/push.ts
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
import { Effect, Either } from "effect"
|
|
2
|
-
import type { BaseCommandOptions } from "../utils/command"
|
|
3
|
-
import { GitCommandError, GitService } from "../services/GitService"
|
|
4
|
-
import { ConfigService } from "../services/ConfigService"
|
|
5
|
-
import { resolveAgencyBranchPairWithAgencyJson } from "../utils/pr-branch"
|
|
6
|
-
import { emit } from "./emit"
|
|
7
|
-
import highlight, { done } from "../utils/colors"
|
|
8
|
-
import {
|
|
9
|
-
createLoggers,
|
|
10
|
-
ensureGitRepo,
|
|
11
|
-
getRemoteName,
|
|
12
|
-
withBranchProtection,
|
|
13
|
-
} from "../utils/effect"
|
|
14
|
-
import { withSpinner } from "../utils/spinner"
|
|
15
|
-
import { spawnProcess } from "../utils/process"
|
|
16
|
-
|
|
17
|
-
interface PushOptions extends BaseCommandOptions {
|
|
18
|
-
baseBranch?: string
|
|
19
|
-
gitArgs?: string[]
|
|
20
|
-
emit?: string
|
|
21
|
-
branch?: string // Deprecated: use emit instead
|
|
22
|
-
force?: boolean
|
|
23
|
-
forceWithLease?: boolean
|
|
24
|
-
noVerify?: boolean
|
|
25
|
-
pr?: boolean
|
|
26
|
-
skipFilter?: boolean
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const getFallbackGitPushArgs = (options: PushOptions): string[] => {
|
|
30
|
-
const args: string[] = []
|
|
31
|
-
|
|
32
|
-
if (options.force) {
|
|
33
|
-
args.push("--force")
|
|
34
|
-
}
|
|
35
|
-
if (options.forceWithLease) {
|
|
36
|
-
args.push("--force-with-lease")
|
|
37
|
-
}
|
|
38
|
-
if (options.noVerify) {
|
|
39
|
-
args.push("--no-verify")
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
args.push(...(options.gitArgs ?? []))
|
|
43
|
-
return args
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const pushWithGit = (gitRoot: string, options: PushOptions) =>
|
|
47
|
-
Effect.gen(function* () {
|
|
48
|
-
const args = ["git", "push", ...getFallbackGitPushArgs(options)]
|
|
49
|
-
const result = yield* spawnProcess(args, {
|
|
50
|
-
cwd: gitRoot,
|
|
51
|
-
stdin: "inherit",
|
|
52
|
-
stdout: options.silent ? "pipe" : "inherit",
|
|
53
|
-
stderr: options.silent ? "pipe" : "inherit",
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
if (result.exitCode !== 0) {
|
|
57
|
-
return yield* Effect.fail(
|
|
58
|
-
new GitCommandError({
|
|
59
|
-
command: args.join(" "),
|
|
60
|
-
exitCode: result.exitCode,
|
|
61
|
-
stderr: result.stderr,
|
|
62
|
-
}),
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
const getPushFailureStderr = (error: unknown): string => {
|
|
68
|
-
if (error instanceof GitCommandError) {
|
|
69
|
-
return error.stderr.trim()
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
typeof error === "object" &&
|
|
74
|
-
error !== null &&
|
|
75
|
-
"cause" in error &&
|
|
76
|
-
(error as { cause?: unknown }).cause instanceof GitCommandError
|
|
77
|
-
) {
|
|
78
|
-
return (error as { cause: GitCommandError }).cause.stderr.trim()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (error instanceof Error) {
|
|
82
|
-
return error.message.trim()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return String(error).trim()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const formatProcessOutput = (result: {
|
|
89
|
-
readonly stdout?: string
|
|
90
|
-
readonly stderr?: string
|
|
91
|
-
}): string => [result.stdout, result.stderr].filter(Boolean).join("\n").trim()
|
|
92
|
-
|
|
93
|
-
const pushFailureNeedsForce = (stderr: string): boolean => {
|
|
94
|
-
const normalized = stderr.toLowerCase()
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
normalized.includes("non-fast-forward") ||
|
|
98
|
-
normalized.includes(
|
|
99
|
-
"updates were rejected because the tip of your current branch is behind",
|
|
100
|
-
) ||
|
|
101
|
-
(normalized.includes("[rejected]") && normalized.includes("fetch first"))
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const formatPushFailure = (error: unknown): Error => {
|
|
106
|
-
const stderr = getPushFailureStderr(error)
|
|
107
|
-
|
|
108
|
-
if (pushFailureNeedsForce(stderr)) {
|
|
109
|
-
return new Error(
|
|
110
|
-
"Failed to push branch to remote: the remote rejected a non-fast-forward update, which usually means the branch was rebased or rewritten.\n" +
|
|
111
|
-
"Run `agency push --force` to replace the remote branch.",
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return new Error(`Failed to push branch to remote: ${stderr}`)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export const push = (options: PushOptions = {}) =>
|
|
119
|
-
Effect.gen(function* () {
|
|
120
|
-
const gitRoot = yield* ensureGitRepo()
|
|
121
|
-
|
|
122
|
-
// Wrap the entire push operation with branch protection
|
|
123
|
-
// This ensures we return to the original branch on Ctrl-C interrupt
|
|
124
|
-
yield* withBranchProtection(gitRoot, pushCore(gitRoot, options))
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
const pushCore = (gitRoot: string, options: PushOptions) =>
|
|
128
|
-
Effect.gen(function* () {
|
|
129
|
-
const { log, verboseLog } = createLoggers(options)
|
|
130
|
-
|
|
131
|
-
const git = yield* GitService
|
|
132
|
-
const configService = yield* ConfigService
|
|
133
|
-
|
|
134
|
-
// Load config to check emit branch pattern
|
|
135
|
-
const config = yield* configService.loadConfig()
|
|
136
|
-
|
|
137
|
-
// Get current branch
|
|
138
|
-
let sourceBranch = yield* git.getCurrentBranch(gitRoot)
|
|
139
|
-
|
|
140
|
-
// Check if we're already on an emit branch using proper branch resolution
|
|
141
|
-
const branchInfo = yield* resolveAgencyBranchPairWithAgencyJson(
|
|
142
|
-
gitRoot,
|
|
143
|
-
sourceBranch,
|
|
144
|
-
config.sourceBranchPattern,
|
|
145
|
-
config.emitBranch,
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
if (!branchInfo) {
|
|
149
|
-
verboseLog("Outside agency branch context; falling back to git push")
|
|
150
|
-
return yield* pushWithGit(gitRoot, options)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Agency push performs internal checkouts, so require a clean worktree.
|
|
154
|
-
const statusOutput = yield* git.getStatus(gitRoot)
|
|
155
|
-
|
|
156
|
-
if (statusOutput && statusOutput.trim().length > 0) {
|
|
157
|
-
return yield* Effect.fail(
|
|
158
|
-
new Error(
|
|
159
|
-
`You have uncommitted changes. Please commit or stash them before pushing.\n` +
|
|
160
|
-
`The push command performs internal checkouts that would destroy uncommitted work.\n` +
|
|
161
|
-
`Run 'git status' to see the changes.`,
|
|
162
|
-
),
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
verboseLog(`Working directory is clean`)
|
|
167
|
-
|
|
168
|
-
// If we're on an emit branch, switch to the source branch first
|
|
169
|
-
if (branchInfo.isOnEmitBranch) {
|
|
170
|
-
const actualSourceBranch = branchInfo.sourceBranch
|
|
171
|
-
// Check if the source branch exists
|
|
172
|
-
const sourceExists = yield* git.branchExists(gitRoot, actualSourceBranch)
|
|
173
|
-
if (sourceExists) {
|
|
174
|
-
verboseLog(
|
|
175
|
-
`Currently on emit branch ${highlight.branch(sourceBranch)}, switching to source branch ${highlight.branch(actualSourceBranch)}`,
|
|
176
|
-
)
|
|
177
|
-
yield* git.checkoutBranch(gitRoot, actualSourceBranch)
|
|
178
|
-
sourceBranch = actualSourceBranch
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
verboseLog(`Starting push workflow from ${highlight.branch(sourceBranch)}`)
|
|
183
|
-
|
|
184
|
-
// Step 1: Create emit branch (agency emit)
|
|
185
|
-
verboseLog("Step 1: Emitting...")
|
|
186
|
-
// Use the emit command for the entire emit step so push and emit stay in sync.
|
|
187
|
-
const prEffectWithOptions = emit({
|
|
188
|
-
baseBranch: options.baseBranch,
|
|
189
|
-
emit: options.emit || options.branch,
|
|
190
|
-
silent: options.silent,
|
|
191
|
-
force: options.force,
|
|
192
|
-
verbose: options.verbose,
|
|
193
|
-
skipFilter: options.skipFilter,
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
const prResult = yield* Effect.either(prEffectWithOptions)
|
|
197
|
-
if (Either.isLeft(prResult)) {
|
|
198
|
-
const error =
|
|
199
|
-
prResult.left instanceof Error
|
|
200
|
-
? prResult.left
|
|
201
|
-
: new Error(String(prResult.left))
|
|
202
|
-
const message = error.message || ""
|
|
203
|
-
const ignorable =
|
|
204
|
-
message === "" ||
|
|
205
|
-
message.includes("already exists") ||
|
|
206
|
-
message.includes("check if branch exists")
|
|
207
|
-
|
|
208
|
-
if (!ignorable) {
|
|
209
|
-
return yield* Effect.fail(
|
|
210
|
-
new Error(`Failed to create emit branch: ${message}`),
|
|
211
|
-
)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
verboseLog("Emit reported existing branch; continuing")
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Compute the emit branch name (emit() command now stays on source branch)
|
|
218
|
-
// Use the branchInfo we already computed earlier
|
|
219
|
-
const emitBranchName =
|
|
220
|
-
options.emit || options.branch || branchInfo.emitBranch
|
|
221
|
-
|
|
222
|
-
// Step 2: Push to remote (git push)
|
|
223
|
-
const remote = yield* getRemoteName(gitRoot)
|
|
224
|
-
verboseLog(
|
|
225
|
-
`Step 2: Pushing ${highlight.branch(emitBranchName)} to ${highlight.remote(remote)}...`,
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
// Switch to emit branch before pushing so that pre-push hooks run
|
|
229
|
-
// in the context of the emit branch (without backpack files)
|
|
230
|
-
yield* git.checkoutBranch(gitRoot, emitBranchName)
|
|
231
|
-
|
|
232
|
-
const pushEither = yield* Effect.either(
|
|
233
|
-
withSpinner(
|
|
234
|
-
pushBranchToRemoteEffect(gitRoot, emitBranchName, remote, {
|
|
235
|
-
force: options.force,
|
|
236
|
-
forceWithLease: options.forceWithLease,
|
|
237
|
-
noVerify: options.noVerify,
|
|
238
|
-
verbose: options.verbose,
|
|
239
|
-
}),
|
|
240
|
-
{
|
|
241
|
-
text:
|
|
242
|
-
options.force || options.forceWithLease
|
|
243
|
-
? `Pushing to ${highlight.remote(remote)} (forced)`
|
|
244
|
-
: `Pushing to ${highlight.remote(remote)}`,
|
|
245
|
-
enabled: !options.silent && !options.verbose,
|
|
246
|
-
},
|
|
247
|
-
),
|
|
248
|
-
)
|
|
249
|
-
if (Either.isLeft(pushEither)) {
|
|
250
|
-
const error = formatPushFailure(pushEither.left)
|
|
251
|
-
// If push failed, switch back to source branch
|
|
252
|
-
yield* git.checkoutBranch(gitRoot, sourceBranch)
|
|
253
|
-
return yield* Effect.fail(error)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const usedForce = pushEither.right
|
|
257
|
-
|
|
258
|
-
if (usedForce) {
|
|
259
|
-
log(done(`Pushed to ${highlight.remote(remote)} (forced)`))
|
|
260
|
-
} else {
|
|
261
|
-
log(done(`Pushed to ${highlight.remote(remote)}`))
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Step 3 (optional): Open GitHub PR if --pr flag is set
|
|
265
|
-
if (options.pr) {
|
|
266
|
-
verboseLog("Step 3: Opening GitHub PR...")
|
|
267
|
-
|
|
268
|
-
const ghEither = yield* Effect.either(
|
|
269
|
-
openGitHubPR(gitRoot, emitBranchName, {
|
|
270
|
-
verbose: options.verbose,
|
|
271
|
-
}),
|
|
272
|
-
)
|
|
273
|
-
|
|
274
|
-
if (Either.isLeft(ghEither)) {
|
|
275
|
-
const error = ghEither.left
|
|
276
|
-
// Don't fail the entire command if gh fails, just warn
|
|
277
|
-
console.error(
|
|
278
|
-
`⚠ Failed to open GitHub PR: ${error instanceof Error ? error.message : String(error)}`,
|
|
279
|
-
)
|
|
280
|
-
} else {
|
|
281
|
-
log(done("Opened GitHub PR in browser"))
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Switch back to source branch
|
|
286
|
-
yield* git.checkoutBranch(gitRoot, sourceBranch)
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
// Helper: Push branch to remote with optional force and retry logic
|
|
290
|
-
const pushBranchToRemoteEffect = (
|
|
291
|
-
gitRoot: string,
|
|
292
|
-
branchName: string,
|
|
293
|
-
remote: string,
|
|
294
|
-
options: {
|
|
295
|
-
readonly force?: boolean
|
|
296
|
-
readonly forceWithLease?: boolean
|
|
297
|
-
readonly noVerify?: boolean
|
|
298
|
-
readonly verbose?: boolean
|
|
299
|
-
},
|
|
300
|
-
) =>
|
|
301
|
-
Effect.gen(function* () {
|
|
302
|
-
const git = yield* GitService
|
|
303
|
-
const { force = false, forceWithLease = false, noVerify = false } = options
|
|
304
|
-
|
|
305
|
-
// Try pushing without force first
|
|
306
|
-
const pushResult = yield* git
|
|
307
|
-
.push(gitRoot, remote, branchName, { setUpstream: true, noVerify })
|
|
308
|
-
.pipe(
|
|
309
|
-
Effect.catchAll((error: any) =>
|
|
310
|
-
Effect.succeed({
|
|
311
|
-
exitCode: error.exitCode ?? -1,
|
|
312
|
-
stdout: "",
|
|
313
|
-
stderr: error.stderr ?? String(error),
|
|
314
|
-
}),
|
|
315
|
-
),
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
let usedForce = false
|
|
319
|
-
|
|
320
|
-
// If push failed, check if we should retry with --force
|
|
321
|
-
if (pushResult.exitCode !== 0) {
|
|
322
|
-
const stderr = formatProcessOutput(pushResult)
|
|
323
|
-
|
|
324
|
-
// Check if this is a force-push-needed error
|
|
325
|
-
const needsForce = pushFailureNeedsForce(stderr)
|
|
326
|
-
|
|
327
|
-
if (needsForce && (force || forceWithLease)) {
|
|
328
|
-
// Retry using the force mode explicitly requested by the user.
|
|
329
|
-
const forceResult = yield* git
|
|
330
|
-
.push(gitRoot, remote, branchName, {
|
|
331
|
-
setUpstream: true,
|
|
332
|
-
force,
|
|
333
|
-
forceWithLease,
|
|
334
|
-
noVerify,
|
|
335
|
-
})
|
|
336
|
-
.pipe(
|
|
337
|
-
Effect.catchAll((error: any) =>
|
|
338
|
-
Effect.succeed({
|
|
339
|
-
exitCode: error.exitCode ?? -1,
|
|
340
|
-
stdout: "",
|
|
341
|
-
stderr: error.stderr ?? String(error),
|
|
342
|
-
}),
|
|
343
|
-
),
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
if (forceResult.exitCode !== 0) {
|
|
347
|
-
return yield* Effect.fail(
|
|
348
|
-
new Error(
|
|
349
|
-
`Failed to force push branch to remote: ${formatProcessOutput(forceResult)}`,
|
|
350
|
-
),
|
|
351
|
-
)
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
usedForce = true
|
|
355
|
-
} else if (needsForce) {
|
|
356
|
-
// User didn't provide --force but it's needed
|
|
357
|
-
return yield* Effect.fail(
|
|
358
|
-
formatPushFailure(
|
|
359
|
-
new GitCommandError({
|
|
360
|
-
command: `git push -u ${remote} ${branchName}`,
|
|
361
|
-
exitCode: pushResult.exitCode,
|
|
362
|
-
stderr,
|
|
363
|
-
}),
|
|
364
|
-
),
|
|
365
|
-
)
|
|
366
|
-
} else {
|
|
367
|
-
// Some other error
|
|
368
|
-
return yield* Effect.fail(
|
|
369
|
-
formatPushFailure(
|
|
370
|
-
new GitCommandError({
|
|
371
|
-
command: `git push -u ${remote} ${branchName}`,
|
|
372
|
-
exitCode: pushResult.exitCode,
|
|
373
|
-
stderr,
|
|
374
|
-
}),
|
|
375
|
-
),
|
|
376
|
-
)
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return usedForce
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
// Helper: Open GitHub PR using gh CLI
|
|
384
|
-
const openGitHubPR = (
|
|
385
|
-
gitRoot: string,
|
|
386
|
-
branchName: string,
|
|
387
|
-
options: {
|
|
388
|
-
readonly verbose?: boolean
|
|
389
|
-
},
|
|
390
|
-
) =>
|
|
391
|
-
Effect.gen(function* () {
|
|
392
|
-
const { verbose = false } = options
|
|
393
|
-
|
|
394
|
-
// Run gh pr create --web with --head to specify the emit branch
|
|
395
|
-
// Set environment variables to ensure non-interactive mode in CI
|
|
396
|
-
// Add a 3 second timeout to prevent hanging in CI environments
|
|
397
|
-
const ghEffect = spawnProcess(
|
|
398
|
-
["gh", "pr", "create", "--web", "--head", branchName],
|
|
399
|
-
{
|
|
400
|
-
cwd: gitRoot,
|
|
401
|
-
stdout: verbose ? "inherit" : "pipe",
|
|
402
|
-
stderr: "pipe",
|
|
403
|
-
env: {
|
|
404
|
-
GH_PROMPT_DISABLED: "1",
|
|
405
|
-
NO_COLOR: "1",
|
|
406
|
-
},
|
|
407
|
-
},
|
|
408
|
-
).pipe(
|
|
409
|
-
Effect.timeout("3 seconds"),
|
|
410
|
-
Effect.catchAll((error) => {
|
|
411
|
-
// Handle timeout or process errors
|
|
412
|
-
if (error._tag === "TimeoutException") {
|
|
413
|
-
return Effect.succeed({
|
|
414
|
-
exitCode: -1,
|
|
415
|
-
stdout: "",
|
|
416
|
-
stderr: "gh command timed out after 3 seconds",
|
|
417
|
-
})
|
|
418
|
-
}
|
|
419
|
-
// Handle ProcessError
|
|
420
|
-
return Effect.succeed({
|
|
421
|
-
exitCode: (error as any).exitCode ?? -1,
|
|
422
|
-
stdout: "",
|
|
423
|
-
stderr: (error as any).stderr ?? String(error),
|
|
424
|
-
})
|
|
425
|
-
}),
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
const ghResult = yield* ghEffect
|
|
429
|
-
|
|
430
|
-
if (ghResult.exitCode !== 0) {
|
|
431
|
-
return yield* Effect.fail(
|
|
432
|
-
new Error(`gh CLI command failed: ${ghResult.stderr.trim()}`),
|
|
433
|
-
)
|
|
434
|
-
}
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
export const help = `
|
|
438
|
-
Usage: agency push [arguments] [options]
|
|
439
|
-
|
|
440
|
-
Create an emit branch, push it to remote, and return to the source branch.
|
|
441
|
-
Outside agency branch context, fall through to plain 'git push'.
|
|
442
|
-
|
|
443
|
-
This command is a convenience wrapper that runs operations in sequence:
|
|
444
|
-
1. agency emit [base-branch] - Create emit branch with backpack files reverted
|
|
445
|
-
2. git push -u origin <pr-branch> - Push emit branch to remote
|
|
446
|
-
3. gh pr create --web (optional with --pr) - Open GitHub PR in browser
|
|
447
|
-
4. git checkout <source-branch> - Switch back to source branch
|
|
448
|
-
|
|
449
|
-
The command ensures you end up back on your source branch after pushing
|
|
450
|
-
the emit branch, making it easy to continue working locally while having
|
|
451
|
-
a clean emit branch ready on the remote.
|
|
452
|
-
|
|
453
|
-
Base Branch Selection:
|
|
454
|
-
Same as 'agency emit' - see 'agency emit --help' for details
|
|
455
|
-
|
|
456
|
-
Prerequisites:
|
|
457
|
-
- Agency branch pushes require git-filter-repo: brew install git-filter-repo
|
|
458
|
-
- A Git remote must be configured
|
|
459
|
-
|
|
460
|
-
Arguments:
|
|
461
|
-
arguments In agency context, the first argument is the base branch.
|
|
462
|
-
Otherwise, arguments are passed to git push as remote/refspecs.
|
|
463
|
-
|
|
464
|
-
Options:
|
|
465
|
-
--emit Custom name for emit branch (defaults to pattern from config)
|
|
466
|
-
--branch (Deprecated: use --emit) Custom name for emit branch
|
|
467
|
-
-f, --force Force push to remote if branch has diverged
|
|
468
|
-
--force-with-lease Force push only if the remote branch is unchanged
|
|
469
|
-
--no-verify Bypass git pre-push hooks
|
|
470
|
-
--pr Open GitHub PR in browser after pushing (requires gh CLI)
|
|
471
|
-
|
|
472
|
-
Examples:
|
|
473
|
-
agency push # Push the emitted branch or current branch
|
|
474
|
-
agency push origin/main # Use an explicit base in agency context
|
|
475
|
-
agency push origin feature # Pass remote/refspec outside agency context
|
|
476
|
-
|
|
477
|
-
Notes:
|
|
478
|
-
- Outside agency context, positional arguments and --force, --force-with-lease, and --no-verify are passed to git push
|
|
479
|
-
- In agency context, creates or recreates the emit branch and pushes it with -u
|
|
480
|
-
- In agency context, automatically returns to the source branch after pushing
|
|
481
|
-
- If any step fails, the command stops and reports the error
|
|
482
|
-
`
|