@markjaquith/agency 2.11.0 → 2.13.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.
@@ -16,6 +16,22 @@ class WorktreeError extends Data.TaggedError("WorktreeError")<{
16
16
  readonly message: string
17
17
  }> {}
18
18
 
19
+ interface WorkspaceOperation {
20
+ readonly action: "fetch" | "create-branch" | "create-worktree"
21
+ readonly repo: string
22
+ readonly command: readonly string[]
23
+ readonly status: "planned" | "completed"
24
+ }
25
+
26
+ interface WorkspaceCheckout {
27
+ readonly repo: string
28
+ readonly kind: "writable" | "reference"
29
+ readonly path: string
30
+ readonly requestedRef: string
31
+ readonly resolvedCommit: string | null
32
+ readonly action: "created" | "reused"
33
+ }
34
+
19
35
  interface ExecutionWorkspace {
20
36
  readonly root: string
21
37
  readonly taskPath: string
@@ -24,6 +40,9 @@ interface ExecutionWorkspace {
24
40
  readonly writablePath: string
25
41
  readonly repo: string
26
42
  readonly repos: readonly RepositoryReference[]
43
+ readonly dryRun: boolean
44
+ readonly checkouts: readonly WorkspaceCheckout[]
45
+ readonly operations: readonly WorkspaceOperation[]
27
46
  }
28
47
 
29
48
  interface GitWorktree {
@@ -121,7 +140,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
121
140
  codePath = join(dirname(task.path), "code")
122
141
  }
123
142
 
124
- yield* fs.createDirectory(codePath)
143
+ if (!options.dryRun) yield* fs.createDirectory(codePath)
144
+ const operations: WorkspaceOperation[] = []
145
+ const checkoutReports: WorkspaceCheckout[] = []
125
146
  const checkouts: readonly (
126
147
  | { readonly repo: string; readonly branch: string }
127
148
  | RepositoryReference
@@ -146,23 +167,38 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
146
167
  { captureOutput: true },
147
168
  )
148
169
  if (remote.exitCode !== 0) return false
170
+ const command = [
171
+ "git",
172
+ "-C",
173
+ repositoryPath,
174
+ "fetch",
175
+ "origin",
176
+ ...(ref ? [ref] : []),
177
+ ]
178
+ if (options.dryRun) {
179
+ operations.push({
180
+ action: "fetch",
181
+ repo: alias,
182
+ command,
183
+ status: "planned",
184
+ })
185
+ return false
186
+ }
149
187
 
150
- const fetch = yield* fs.runCommand(
151
- [
152
- "git",
153
- "-C",
154
- repositoryPath,
155
- "fetch",
156
- "origin",
157
- ...(ref ? [ref] : []),
158
- ],
159
- { captureOutput: true },
160
- )
188
+ const fetch = yield* fs.runCommand(command, {
189
+ captureOutput: true,
190
+ })
161
191
  if (fetch.exitCode !== 0) {
162
192
  return yield* new WorktreeError({
163
193
  message: `Failed to fetch '${alias}': ${fetch.stderr}`,
164
194
  })
165
195
  }
196
+ operations.push({
197
+ action: "fetch",
198
+ repo: alias,
199
+ command,
200
+ status: "completed",
201
+ })
166
202
  return true
167
203
  })
168
204
 
@@ -183,7 +219,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
183
219
  message: `Failed to inspect worktrees for '${alias}': ${listed.stderr}`,
184
220
  })
185
221
  }
186
- const canonicalCodePath = yield* fs.realPath(codePath)
222
+ const canonicalCodePath = (yield* fs.exists(codePath))
223
+ ? yield* fs.realPath(codePath)
224
+ : resolve(codePath)
187
225
  const canonicalCheckoutPath = join(canonicalCodePath, alias)
188
226
  const worktrees: GitWorktree[] = []
189
227
  for (const worktree of parseWorktreeList(listed.stdout)) {
@@ -212,7 +250,17 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
212
250
  })
213
251
  }
214
252
  if (yield* fs.isDirectory(checkoutPath)) {
215
- if (registeredAtPath?.branch === branchRef) continue
253
+ if (registeredAtPath?.branch === branchRef) {
254
+ checkoutReports.push({
255
+ repo: alias,
256
+ kind: "writable",
257
+ path: checkoutPath,
258
+ requestedRef: checkout.branch,
259
+ resolvedCommit: registeredAtPath.head ?? null,
260
+ action: "reused",
261
+ })
262
+ continue
263
+ }
216
264
  return yield* new WorktreeError({
217
265
  message: `Existing checkout ${checkoutPath} is not registered to branch '${checkout.branch}'`,
218
266
  })
@@ -260,21 +308,29 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
260
308
  { captureOutput: true },
261
309
  )
262
310
  if (branchExists.exitCode !== 0) {
263
- const createBranch = yield* fs.runCommand(
264
- [
265
- "git",
266
- "-C",
267
- repositoryPath,
268
- "branch",
269
- checkout.branch,
270
- execution.base,
271
- ],
272
- { captureOutput: true },
273
- )
274
- if (createBranch.exitCode !== 0) {
275
- return yield* new WorktreeError({
276
- message: `Failed to create branch '${checkout.branch}': ${createBranch.stderr}`,
311
+ const command = [
312
+ "git",
313
+ "-C",
314
+ repositoryPath,
315
+ "branch",
316
+ checkout.branch,
317
+ execution.base,
318
+ ]
319
+ operations.push({
320
+ action: "create-branch",
321
+ repo: alias,
322
+ command,
323
+ status: options.dryRun ? "planned" : "completed",
324
+ })
325
+ if (!options.dryRun) {
326
+ const createBranch = yield* fs.runCommand(command, {
327
+ captureOutput: true,
277
328
  })
329
+ if (createBranch.exitCode !== 0) {
330
+ return yield* new WorktreeError({
331
+ message: `Failed to create branch '${checkout.branch}': ${createBranch.stderr}`,
332
+ })
333
+ }
278
334
  }
279
335
  }
280
336
  args = [
@@ -287,6 +343,48 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
287
343
  checkout.branch,
288
344
  ]
289
345
  }
346
+ if (options.dryRun) {
347
+ operations.push({
348
+ action: "create-worktree",
349
+ repo: alias,
350
+ command: args,
351
+ status: "planned",
352
+ })
353
+ let resolved = yield* fs.runCommand(
354
+ [
355
+ "git",
356
+ "-C",
357
+ repositoryPath,
358
+ "rev-parse",
359
+ "--verify",
360
+ `${checkout.branch}^{commit}`,
361
+ ],
362
+ { captureOutput: true },
363
+ )
364
+ if (resolved.exitCode !== 0) {
365
+ resolved = yield* fs.runCommand(
366
+ [
367
+ "git",
368
+ "-C",
369
+ repositoryPath,
370
+ "rev-parse",
371
+ "--verify",
372
+ `${execution.base}^{commit}`,
373
+ ],
374
+ { captureOutput: true },
375
+ )
376
+ }
377
+ checkoutReports.push({
378
+ repo: alias,
379
+ kind: "writable",
380
+ path: checkoutPath,
381
+ requestedRef: checkout.branch,
382
+ resolvedCommit:
383
+ resolved.exitCode === 0 ? resolved.stdout.trim() : null,
384
+ action: "created",
385
+ })
386
+ continue
387
+ }
290
388
 
291
389
  if (config.worktreeCreateCommand) {
292
390
  verboseLog(`Running worktree command: ${formatCommand(args)}`)
@@ -308,6 +406,24 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
308
406
  message: `Worktree command did not create ${checkoutPath}`,
309
407
  })
310
408
  }
409
+ operations.push({
410
+ action: "create-worktree",
411
+ repo: alias,
412
+ command: args,
413
+ status: "completed",
414
+ })
415
+ const head = yield* fs.runCommand(
416
+ ["git", "-C", checkoutPath, "rev-parse", "HEAD"],
417
+ { captureOutput: true },
418
+ )
419
+ checkoutReports.push({
420
+ repo: alias,
421
+ kind: "writable",
422
+ path: checkoutPath,
423
+ requestedRef: checkout.branch,
424
+ resolvedCommit: head.exitCode === 0 ? head.stdout.trim() : null,
425
+ action: "created",
426
+ })
311
427
  } else {
312
428
  const fetched = isCommitId(checkout.ref)
313
429
  ? false
@@ -349,6 +465,14 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
349
465
  currentHead.exitCode === 0 &&
350
466
  currentHead.stdout.trim() === commit
351
467
  ) {
468
+ checkoutReports.push({
469
+ repo: alias,
470
+ kind: "reference",
471
+ path: checkoutPath,
472
+ requestedRef: checkout.ref,
473
+ resolvedCommit: commit,
474
+ action: "reused",
475
+ })
352
476
  continue
353
477
  }
354
478
  return yield* new WorktreeError({
@@ -360,24 +484,55 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
360
484
  message: `Worktree registry contains a missing checkout at ${checkoutPath}`,
361
485
  })
362
486
  }
363
- const result = yield* fs.runCommand(
364
- [
365
- "git",
366
- "-C",
367
- repositoryPath,
368
- "worktree",
369
- "add",
370
- "--detach",
371
- checkoutPath,
372
- commit,
373
- ],
374
- { captureOutput: true },
375
- )
487
+ const command = [
488
+ "git",
489
+ "-C",
490
+ repositoryPath,
491
+ "worktree",
492
+ "add",
493
+ "--detach",
494
+ checkoutPath,
495
+ commit,
496
+ ]
497
+ if (options.dryRun) {
498
+ operations.push({
499
+ action: "create-worktree",
500
+ repo: alias,
501
+ command,
502
+ status: "planned",
503
+ })
504
+ checkoutReports.push({
505
+ repo: alias,
506
+ kind: "reference",
507
+ path: checkoutPath,
508
+ requestedRef: checkout.ref,
509
+ resolvedCommit: commit,
510
+ action: "created",
511
+ })
512
+ continue
513
+ }
514
+ const result = yield* fs.runCommand(command, {
515
+ captureOutput: true,
516
+ })
376
517
  if (result.exitCode !== 0) {
377
518
  return yield* new WorktreeError({
378
519
  message: `Failed to create worktree for '${alias}': ${result.stderr}`,
379
520
  })
380
521
  }
522
+ operations.push({
523
+ action: "create-worktree",
524
+ repo: alias,
525
+ command,
526
+ status: "completed",
527
+ })
528
+ checkoutReports.push({
529
+ repo: alias,
530
+ kind: "reference",
531
+ path: checkoutPath,
532
+ requestedRef: checkout.ref,
533
+ resolvedCommit: commit,
534
+ action: "created",
535
+ })
381
536
  }
382
537
  }
383
538
 
@@ -389,6 +544,9 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
389
544
  writablePath: join(codePath, execution.repo),
390
545
  repo: execution.repo,
391
546
  repos: execution.repos ?? [],
547
+ dryRun: options.dryRun === true,
548
+ checkouts: checkoutReports,
549
+ operations,
392
550
  } satisfies ExecutionWorkspace
393
551
  }),
394
552
 
@@ -5,6 +5,7 @@ export interface BaseCommandOptions {
5
5
  readonly silent?: boolean
6
6
  readonly verbose?: boolean
7
7
  readonly json?: boolean
8
+ readonly dryRun?: boolean
8
9
  /** Whether this invocation may open prompts or selectors. */
9
10
  readonly inputAllowed?: boolean
10
11
  /**