@markjaquith/agency 1.8.6 → 1.9.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/cli.ts CHANGED
@@ -186,6 +186,7 @@ const commands: Record<string, Command> = {
186
186
  emit: options.emit || options.branch,
187
187
  silent: options.silent,
188
188
  force: options.force,
189
+ noVerify: options["no-verify"],
189
190
  verbose: options.verbose,
190
191
  pr: options.pr,
191
192
  }),
@@ -593,6 +594,9 @@ try {
593
594
  type: "boolean",
594
595
  short: "f",
595
596
  },
597
+ "no-verify": {
598
+ type: "boolean",
599
+ },
596
600
  verbose: {
597
601
  type: "boolean",
598
602
  short: "v",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "1.8.6",
3
+ "version": "1.9.0",
4
4
  "description": "Manages personal agents files",
5
5
  "keywords": [
6
6
  "agents",
@@ -314,6 +314,50 @@ describe("push command", () => {
314
314
  expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
315
315
  })
316
316
 
317
+ test("reports pre-push hook output without suggesting --force", async () => {
318
+ await Bun.spawn(
319
+ ["git", "config", "core.hooksPath", join(tempDir, ".git", "hooks")],
320
+ {
321
+ cwd: tempDir,
322
+ stdout: "pipe",
323
+ stderr: "pipe",
324
+ },
325
+ ).exited
326
+ const hookPath = join(tempDir, ".git", "hooks", "pre-push")
327
+ await Bun.write(
328
+ hookPath,
329
+ [
330
+ "#!/bin/sh",
331
+ "printf 'pre-push hook stdout failure\\n'",
332
+ "printf 'pre-push hook stderr failure\\n' >&2",
333
+ "exit 1",
334
+ "",
335
+ ].join("\n"),
336
+ )
337
+ await Bun.spawn(["chmod", "+x", hookPath], {
338
+ cwd: tempDir,
339
+ stdout: "pipe",
340
+ stderr: "pipe",
341
+ }).exited
342
+
343
+ let error: unknown
344
+ try {
345
+ await runTestEffect(
346
+ push({ baseBranch: "main", silent: true, skipFilter: true }),
347
+ )
348
+ } catch (caught) {
349
+ error = caught
350
+ }
351
+
352
+ expect(error).toBeInstanceOf(Error)
353
+ const message = error instanceof Error ? error.message : String(error)
354
+ expect(message).toContain("pre-push hook stdout failure")
355
+ expect(message).toContain("pre-push hook stderr failure")
356
+ expect(message).not.toContain("agency push --force")
357
+
358
+ expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
359
+ })
360
+
317
361
  test("does not report force push when --force is provided but not needed", async () => {
318
362
  // Capture output to check for force push message
319
363
  const originalLog = console.log
@@ -19,6 +19,7 @@ interface PushOptions extends BaseCommandOptions {
19
19
  emit?: string
20
20
  branch?: string // Deprecated: use emit instead
21
21
  force?: boolean
22
+ noVerify?: boolean
22
23
  pr?: boolean
23
24
  skipFilter?: boolean
24
25
  }
@@ -44,15 +45,21 @@ const getPushFailureStderr = (error: unknown): string => {
44
45
  return String(error).trim()
45
46
  }
46
47
 
48
+ const formatProcessOutput = (result: {
49
+ readonly stdout?: string
50
+ readonly stderr?: string
51
+ }): string => [result.stdout, result.stderr].filter(Boolean).join("\n").trim()
52
+
47
53
  const pushFailureNeedsForce = (stderr: string): boolean => {
48
54
  const normalized = stderr.toLowerCase()
49
55
 
50
- return [
51
- "non-fast-forward",
52
- "updates were rejected because the tip of your current branch is behind",
53
- "fetch first",
54
- "failed to push some refs",
55
- ].some((pattern) => normalized.includes(pattern))
56
+ return (
57
+ normalized.includes("non-fast-forward") ||
58
+ normalized.includes(
59
+ "updates were rejected because the tip of your current branch is behind",
60
+ ) ||
61
+ (normalized.includes("[rejected]") && normalized.includes("fetch first"))
62
+ )
56
63
  }
57
64
 
58
65
  const formatPushFailure = (error: unknown): Error => {
@@ -180,6 +187,7 @@ const pushCore = (gitRoot: string, options: PushOptions) =>
180
187
  withSpinner(
181
188
  pushBranchToRemoteEffect(gitRoot, emitBranchName, remote, {
182
189
  force: options.force,
190
+ noVerify: options.noVerify,
183
191
  verbose: options.verbose,
184
192
  }),
185
193
  {
@@ -237,16 +245,17 @@ const pushBranchToRemoteEffect = (
237
245
  remote: string,
238
246
  options: {
239
247
  readonly force?: boolean
248
+ readonly noVerify?: boolean
240
249
  readonly verbose?: boolean
241
250
  },
242
251
  ) =>
243
252
  Effect.gen(function* () {
244
253
  const git = yield* GitService
245
- const { force = false } = options
254
+ const { force = false, noVerify = false } = options
246
255
 
247
256
  // Try pushing without force first
248
257
  const pushResult = yield* git
249
- .push(gitRoot, remote, branchName, { setUpstream: true })
258
+ .push(gitRoot, remote, branchName, { setUpstream: true, noVerify })
250
259
  .pipe(
251
260
  Effect.catchAll((error: any) =>
252
261
  Effect.succeed({
@@ -261,7 +270,7 @@ const pushBranchToRemoteEffect = (
261
270
 
262
271
  // If push failed, check if we should retry with --force
263
272
  if (pushResult.exitCode !== 0) {
264
- const stderr = pushResult.stderr
273
+ const stderr = formatProcessOutput(pushResult)
265
274
 
266
275
  // Check if this is a force-push-needed error
267
276
  const needsForce = pushFailureNeedsForce(stderr)
@@ -269,7 +278,11 @@ const pushBranchToRemoteEffect = (
269
278
  if (needsForce && force) {
270
279
  // User provided --force flag, retry with force
271
280
  const forceResult = yield* git
272
- .push(gitRoot, remote, branchName, { setUpstream: true, force: true })
281
+ .push(gitRoot, remote, branchName, {
282
+ setUpstream: true,
283
+ force: true,
284
+ noVerify,
285
+ })
273
286
  .pipe(
274
287
  Effect.catchAll((error: any) =>
275
288
  Effect.succeed({
@@ -283,7 +296,7 @@ const pushBranchToRemoteEffect = (
283
296
  if (forceResult.exitCode !== 0) {
284
297
  return yield* Effect.fail(
285
298
  new Error(
286
- `Failed to force push branch to remote: ${forceResult.stderr}`,
299
+ `Failed to force push branch to remote: ${formatProcessOutput(forceResult)}`,
287
300
  ),
288
301
  )
289
302
  }
@@ -399,15 +412,17 @@ Arguments:
399
412
 
400
413
  Options:
401
414
  --emit Custom name for emit branch (defaults to pattern from config)
402
- --branch (Deprecated: use --emit) Custom name for emit branch
403
- -f, --force Force push to remote if branch has diverged
404
- --pr Open GitHub PR in browser after pushing (requires gh CLI)
415
+ --branch (Deprecated: use --emit) Custom name for emit branch
416
+ -f, --force Force push to remote if branch has diverged
417
+ --no-verify Bypass git pre-push hooks
418
+ --pr Open GitHub PR in browser after pushing (requires gh CLI)
405
419
 
406
420
  Examples:
407
421
  agency push # Create PR, push, return to source
408
- agency push origin/main # Explicitly use origin/main as base
409
- agency push --force # Force push if branch has diverged
410
- agency push --pr # Push and open GitHub PR in browser
422
+ agency push origin/main # Explicitly use origin/main as base
423
+ agency push --force # Force push if branch has diverged
424
+ agency push --no-verify # Push without running pre-push hooks
425
+ agency push --pr # Push and open GitHub PR in browser
411
426
 
412
427
  Notes:
413
428
  - Must be run from a source branch (not a emit branch)
@@ -1046,6 +1046,7 @@ export class GitService extends Effect.Service<GitService>()("GitService", {
1046
1046
  options?: {
1047
1047
  readonly setUpstream?: boolean
1048
1048
  readonly force?: boolean
1049
+ readonly noVerify?: boolean
1049
1050
  },
1050
1051
  ) => {
1051
1052
  const args = ["git", "push"]
@@ -1055,6 +1056,9 @@ export class GitService extends Effect.Service<GitService>()("GitService", {
1055
1056
  if (options?.force) {
1056
1057
  args.push("--force")
1057
1058
  }
1059
+ if (options?.noVerify) {
1060
+ args.push("--no-verify")
1061
+ }
1058
1062
  args.push(remote, branch)
1059
1063
 
1060
1064
  return pipe(