@markjaquith/agency 2.74.0 → 3.1.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 +29 -122
- package/cli-main.ts +10 -28
- package/cli.ts +1 -17
- package/package.json +1 -2
- package/schemas/agency-graph-v1.schema.json +1 -1
- package/src/cli-parser.test.ts +5 -19
- package/src/cli-parser.ts +12 -30
- package/src/cli.test.ts +6 -6
- package/src/commands/archive.test.ts +73 -1
- package/src/commands/archive.ts +18 -7
- package/src/commands/context.test.ts +0 -67
- package/src/commands/init.test.ts +1 -2
- package/src/commands/pr.test.ts +2 -87
- package/src/commands/pr.ts +2 -79
- package/src/commands/push.test.ts +1 -2
- package/src/graph-schema.ts +1 -1
- package/src/services/ArchiveBulkService.test.ts +1 -142
- package/src/services/ArchiveService.test.ts +1 -172
- package/src/services/ArchiveService.ts +59 -27
- package/src/services/ContextService.ts +3 -47
- package/src/services/DoctorService.ts +8 -24
- package/src/services/GraphService.test.ts +1 -0
- package/src/services/GraphService.ts +2 -5
- package/src/services/PhaseService.ts +70 -191
- package/src/services/PullRequestService.test.ts +1 -45
- package/src/services/PullRequestService.ts +1 -22
- package/src/services/PushService.test.ts +29 -295
- package/src/services/PushService.ts +10 -239
- package/src/services/RepositoryService.test.ts +1 -61
- package/src/services/RepositoryService.ts +2 -13
- package/src/services/ReviewService.test.ts +1 -84
- package/src/services/ReviewService.ts +9 -48
- package/src/services/SyncService.test.ts +0 -81
- package/src/services/SyncService.ts +12 -39
- package/src/services/TaskPhaseService.test.ts +0 -80
- package/src/services/TaskService.ts +1 -9
- package/src/services/VersionControlService.test.ts +4 -117
- package/src/services/VersionControlService.ts +3 -426
- package/src/services/WorkbaseService.test.ts +0 -20
- package/src/services/WorkbaseService.ts +1 -18
- package/src/services/WorktreeService.test.ts +1 -705
- package/src/services/WorktreeService.ts +395 -1604
- package/src/services/push-validation.ts +0 -7
- package/src/test-utils.ts +0 -4
- package/src/workbase/AGENTS.md +4 -5
- package/src/workbase/checkout-command.test.ts +1 -1
- package/src/workbase/checkout-command.ts +1 -1
- package/src/workbase/delivery-command.test.ts +4 -35
- package/src/workbase/delivery-command.ts +2 -15
- package/src/workbase/schemas.test.ts +0 -19
- package/src/workbase/schemas.ts +1 -2
- package/src/commands/vcs.test.ts +0 -75
- package/src/commands/vcs.ts +0 -72
- package/src/services/VcsMigrationService.test.ts +0 -348
- package/src/services/VcsMigrationService.ts +0 -857
- package/src/vcs-status-fast.ts +0 -310
- package/src/workbase/version-control.ts +0 -5
- package/src/workbase/workspace-command.test.ts +0 -70
- package/src/workbase/workspace-command.ts +0 -63
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
buildNonPrCompletion,
|
|
31
31
|
type NonPrCompletionInput,
|
|
32
32
|
} from "../workbase/completion"
|
|
33
|
-
import { VersionControlService } from "./VersionControlService"
|
|
34
33
|
|
|
35
34
|
class PhaseError extends Data.TaggedError("PhaseError")<{
|
|
36
35
|
readonly message: string
|
|
@@ -93,10 +92,8 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
93
92
|
Effect.gen(function* () {
|
|
94
93
|
const fs = yield* FileSystemService
|
|
95
94
|
const workbase = yield* WorkbaseService
|
|
96
|
-
const versionControl = yield* VersionControlService
|
|
97
95
|
const tasks = yield* TaskService
|
|
98
96
|
const root = yield* workbase.discover(startPath)
|
|
99
|
-
const backend = yield* versionControl.forWorkbase(root)
|
|
100
97
|
const taskId = yield* decodeId(input.taskId, "task")
|
|
101
98
|
const id = yield* decodeId(input.id, "phase")
|
|
102
99
|
const task = yield* tasks.show(taskId, root)
|
|
@@ -274,213 +271,95 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
274
271
|
firstData.repo,
|
|
275
272
|
...(firstData.repos ?? []).map((reference) => reference.repo),
|
|
276
273
|
]
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
alias
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
continue
|
|
308
|
-
}
|
|
309
|
-
const expected = await realpath(oldPath)
|
|
310
|
-
const registered = (
|
|
311
|
-
await runBackend(
|
|
312
|
-
backend.listWorkspaces(join(root, "repos", alias)),
|
|
313
|
-
)
|
|
314
|
-
).find((workspace) => workspace.path === expected)
|
|
315
|
-
if (!registered?.name)
|
|
316
|
-
throw new Error(
|
|
317
|
-
`Cannot convert task '${taskId}'; checkout '${alias}' is not registered as a jj workspace`,
|
|
318
|
-
)
|
|
319
|
-
if (await runBackend(backend.workspaceDirty(oldPath)))
|
|
320
|
-
throw new Error(
|
|
321
|
-
`Cannot convert task '${taskId}'; jj workspace '${alias}' has uncommitted changes`,
|
|
322
|
-
)
|
|
323
|
-
const head = await runBackend(
|
|
324
|
-
backend.workspaceHead(oldPath),
|
|
325
|
-
)
|
|
326
|
-
if (!head)
|
|
327
|
-
throw new Error(
|
|
328
|
-
`Cannot resolve jj workspace '${alias}' before conversion`,
|
|
329
|
-
)
|
|
330
|
-
workspaces.push({
|
|
331
|
-
alias,
|
|
332
|
-
name: registered.name,
|
|
333
|
-
head,
|
|
334
|
-
oldPath,
|
|
335
|
-
newPath: join(firstCodePath, alias),
|
|
336
|
-
})
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
apply: async () => {
|
|
340
|
-
await mkdir(firstCodePath, { recursive: true })
|
|
341
|
-
for (const workspace of workspaces) {
|
|
342
|
-
const repositoryPath = join(
|
|
343
|
-
root,
|
|
344
|
-
"repos",
|
|
345
|
-
workspace.alias,
|
|
346
|
-
)
|
|
347
|
-
await runBackend(
|
|
348
|
-
backend.removeWorkspace({
|
|
349
|
-
repositoryPath,
|
|
350
|
-
workspacePath: workspace.oldPath,
|
|
351
|
-
workspaceName: workspace.name,
|
|
352
|
-
}),
|
|
353
|
-
)
|
|
354
|
-
await runBackend(
|
|
355
|
-
backend.createWorkspace({
|
|
356
|
-
repositoryPath,
|
|
357
|
-
workspacePath: workspace.newPath,
|
|
358
|
-
workspaceName: workspace.name,
|
|
359
|
-
revision: workspace.head,
|
|
360
|
-
}),
|
|
361
|
-
)
|
|
362
|
-
}
|
|
363
|
-
await rm(oldCodePath, { recursive: true, force: true })
|
|
364
|
-
},
|
|
365
|
-
rollback: async () => {
|
|
366
|
-
await mkdir(oldCodePath, { recursive: true })
|
|
367
|
-
for (const workspace of [...workspaces].reverse()) {
|
|
368
|
-
const repositoryPath = join(
|
|
369
|
-
root,
|
|
370
|
-
"repos",
|
|
371
|
-
workspace.alias,
|
|
372
|
-
)
|
|
373
|
-
await runBackend(
|
|
374
|
-
backend.removeWorkspace({
|
|
375
|
-
repositoryPath,
|
|
376
|
-
workspacePath: workspace.newPath,
|
|
377
|
-
workspaceName: workspace.name,
|
|
378
|
-
}),
|
|
379
|
-
)
|
|
380
|
-
await runBackend(
|
|
381
|
-
backend.createWorkspace({
|
|
382
|
-
repositoryPath,
|
|
383
|
-
workspacePath: workspace.oldPath,
|
|
384
|
-
workspaceName: workspace.name,
|
|
385
|
-
revision: workspace.head,
|
|
386
|
-
}),
|
|
274
|
+
const repair = async (basePath: string) => {
|
|
275
|
+
for (const alias of checkoutAliases) {
|
|
276
|
+
const checkoutPath = join(basePath, alias)
|
|
277
|
+
try {
|
|
278
|
+
await lstat(checkoutPath)
|
|
279
|
+
} catch {
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
const result = Bun.spawnSync([
|
|
283
|
+
"git",
|
|
284
|
+
"-C",
|
|
285
|
+
join(root, "repos", alias),
|
|
286
|
+
"worktree",
|
|
287
|
+
"repair",
|
|
288
|
+
checkoutPath,
|
|
289
|
+
])
|
|
290
|
+
if (result.exitCode !== 0) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Failed to repair moved worktree for '${alias}': ${new TextDecoder().decode(result.stderr)}`,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
steps.push({
|
|
298
|
+
label: `move and repair code for ${taskId}/${firstPhaseId}`,
|
|
299
|
+
preflight: async () => {
|
|
300
|
+
for (const entry of await readdir(oldCodePath)) {
|
|
301
|
+
if (!checkoutAliases.includes(entry))
|
|
302
|
+
throw new Error(
|
|
303
|
+
`Cannot convert task '${taskId}'; code contains unmanaged entry '${entry}'`,
|
|
387
304
|
)
|
|
388
|
-
|
|
389
|
-
await rm(firstDirectory, { recursive: true, force: true })
|
|
390
|
-
},
|
|
391
|
-
manualRecovery: `Restore jj workspaces from ${firstCodePath} to ${oldCodePath}`,
|
|
392
|
-
})
|
|
393
|
-
} else {
|
|
394
|
-
const repair = async (basePath: string) => {
|
|
305
|
+
}
|
|
395
306
|
for (const alias of checkoutAliases) {
|
|
396
|
-
const checkoutPath = join(
|
|
307
|
+
const checkoutPath = join(oldCodePath, alias)
|
|
397
308
|
try {
|
|
398
309
|
await lstat(checkoutPath)
|
|
399
310
|
} catch {
|
|
400
311
|
continue
|
|
401
312
|
}
|
|
402
|
-
const
|
|
313
|
+
const listed = Bun.spawnSync([
|
|
403
314
|
"git",
|
|
404
315
|
"-C",
|
|
405
316
|
join(root, "repos", alias),
|
|
406
317
|
"worktree",
|
|
407
|
-
"
|
|
408
|
-
|
|
318
|
+
"list",
|
|
319
|
+
"--porcelain",
|
|
409
320
|
])
|
|
410
|
-
if (
|
|
321
|
+
if (listed.exitCode !== 0)
|
|
411
322
|
throw new Error(
|
|
412
|
-
`Failed to
|
|
323
|
+
`Failed to inspect worktrees for '${alias}'`,
|
|
413
324
|
)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
for (const entry of await readdir(oldCodePath)) {
|
|
421
|
-
if (!checkoutAliases.includes(entry))
|
|
422
|
-
throw new Error(
|
|
423
|
-
`Cannot convert task '${taskId}'; code contains unmanaged entry '${entry}'`,
|
|
424
|
-
)
|
|
425
|
-
}
|
|
426
|
-
for (const alias of checkoutAliases) {
|
|
427
|
-
const checkoutPath = join(oldCodePath, alias)
|
|
325
|
+
const expected = await realpath(checkoutPath)
|
|
326
|
+
let registered = false
|
|
327
|
+
for (const line of new TextDecoder()
|
|
328
|
+
.decode(listed.stdout)
|
|
329
|
+
.split("\n")) {
|
|
330
|
+
if (!line.startsWith("worktree ")) continue
|
|
428
331
|
try {
|
|
429
|
-
await
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
"git",
|
|
435
|
-
"-C",
|
|
436
|
-
join(root, "repos", alias),
|
|
437
|
-
"worktree",
|
|
438
|
-
"list",
|
|
439
|
-
"--porcelain",
|
|
440
|
-
])
|
|
441
|
-
if (listed.exitCode !== 0)
|
|
442
|
-
throw new Error(
|
|
443
|
-
`Failed to inspect worktrees for '${alias}'`,
|
|
444
|
-
)
|
|
445
|
-
const expected = await realpath(checkoutPath)
|
|
446
|
-
let registered = false
|
|
447
|
-
for (const line of new TextDecoder()
|
|
448
|
-
.decode(listed.stdout)
|
|
449
|
-
.split("\n")) {
|
|
450
|
-
if (!line.startsWith("worktree ")) continue
|
|
451
|
-
try {
|
|
452
|
-
if ((await realpath(line.slice(9))) === expected) {
|
|
453
|
-
registered = true
|
|
454
|
-
break
|
|
455
|
-
}
|
|
456
|
-
} catch {}
|
|
457
|
-
}
|
|
458
|
-
if (!registered)
|
|
459
|
-
throw new Error(
|
|
460
|
-
`Cannot convert task '${taskId}'; checkout '${alias}' is not registered as a Git worktree`,
|
|
461
|
-
)
|
|
462
|
-
}
|
|
463
|
-
},
|
|
464
|
-
apply: async () => {
|
|
465
|
-
await mkdir(firstDirectory, { recursive: true })
|
|
466
|
-
await rename(oldCodePath, firstCodePath)
|
|
467
|
-
try {
|
|
468
|
-
await repair(firstCodePath)
|
|
469
|
-
} catch (cause) {
|
|
470
|
-
await rename(firstCodePath, oldCodePath)
|
|
471
|
-
await repair(oldCodePath)
|
|
472
|
-
await rm(firstDirectory, { recursive: true, force: true })
|
|
473
|
-
throw cause
|
|
332
|
+
if ((await realpath(line.slice(9))) === expected) {
|
|
333
|
+
registered = true
|
|
334
|
+
break
|
|
335
|
+
}
|
|
336
|
+
} catch {}
|
|
474
337
|
}
|
|
475
|
-
|
|
476
|
-
|
|
338
|
+
if (!registered)
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Cannot convert task '${taskId}'; checkout '${alias}' is not registered as a Git worktree`,
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
apply: async () => {
|
|
345
|
+
await mkdir(firstDirectory, { recursive: true })
|
|
346
|
+
await rename(oldCodePath, firstCodePath)
|
|
347
|
+
try {
|
|
348
|
+
await repair(firstCodePath)
|
|
349
|
+
} catch (cause) {
|
|
477
350
|
await rename(firstCodePath, oldCodePath)
|
|
478
351
|
await repair(oldCodePath)
|
|
479
352
|
await rm(firstDirectory, { recursive: true, force: true })
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
|
|
353
|
+
throw cause
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
rollback: async () => {
|
|
357
|
+
await rename(firstCodePath, oldCodePath)
|
|
358
|
+
await repair(oldCodePath)
|
|
359
|
+
await rm(firstDirectory, { recursive: true, force: true })
|
|
360
|
+
},
|
|
361
|
+
manualRecovery: `Move ${firstCodePath} back to ${oldCodePath} and run git worktree repair`,
|
|
362
|
+
})
|
|
484
363
|
}
|
|
485
364
|
steps.push(
|
|
486
365
|
documentWriteStep(root, [
|
|
@@ -98,7 +98,7 @@ describe("PullRequestService", () => {
|
|
|
98
98
|
await Bun.write(
|
|
99
99
|
path,
|
|
100
100
|
`#!/usr/bin/env bun
|
|
101
|
-
await Bun.write(${JSON.stringify(ghCallPath)}, JSON.stringify({ args: Bun.argv.slice(2), cwd: process.cwd()
|
|
101
|
+
await Bun.write(${JSON.stringify(ghCallPath)}, JSON.stringify({ args: Bun.argv.slice(2), cwd: process.cwd() }))
|
|
102
102
|
process.stdout.write(${JSON.stringify(stdout)})
|
|
103
103
|
process.stderr.write(${JSON.stringify(stderr)})
|
|
104
104
|
process.exit(${exitCode})
|
|
@@ -111,7 +111,6 @@ process.exit(${exitCode})
|
|
|
111
111
|
(await Bun.file(ghCallPath).json()) as {
|
|
112
112
|
args: string[]
|
|
113
113
|
cwd: string
|
|
114
|
-
gitDir?: string
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
const expectRemoteBranch = async (branch: string, exists = true) => {
|
|
@@ -243,49 +242,6 @@ process.exit(${exitCode})
|
|
|
243
242
|
expect(updated.endsWith(`${body}\n`)).toBe(true)
|
|
244
243
|
})
|
|
245
244
|
|
|
246
|
-
test("creates a PR from a non-colocated jj workspace with backing Git context", async () => {
|
|
247
|
-
if (!Bun.which("jj")) return
|
|
248
|
-
await rm(join(root, "repos/agency"), { recursive: true, force: true })
|
|
249
|
-
await requireCommand([
|
|
250
|
-
"jj",
|
|
251
|
-
"git",
|
|
252
|
-
"clone",
|
|
253
|
-
"--no-colocate",
|
|
254
|
-
remotePath,
|
|
255
|
-
join(root, "repos/agency"),
|
|
256
|
-
])
|
|
257
|
-
await requireCommand(
|
|
258
|
-
["jj", "config", "set", "--repo", "user.name", "Agency Test"],
|
|
259
|
-
join(root, "repos/agency"),
|
|
260
|
-
)
|
|
261
|
-
await requireCommand(
|
|
262
|
-
["jj", "config", "set", "--repo", "user.email", "agency@example.com"],
|
|
263
|
-
join(root, "repos/agency"),
|
|
264
|
-
)
|
|
265
|
-
await Bun.write(
|
|
266
|
-
join(root, "agency.json"),
|
|
267
|
-
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
268
|
-
)
|
|
269
|
-
await createTask("jj-example", "task/jj-example")
|
|
270
|
-
const workspace = await materialize("jj-example")
|
|
271
|
-
await Bun.write(join(workspace.writablePath!, "CHANGE.md"), "jj change\n")
|
|
272
|
-
await requireCommand(
|
|
273
|
-
["jj", "describe", "-m", "feat: add jj example"],
|
|
274
|
-
workspace.writablePath!,
|
|
275
|
-
)
|
|
276
|
-
await requireCommand(["jj", "new"], workspace.writablePath!)
|
|
277
|
-
const url = "https://github.com/example/agency/pull/44"
|
|
278
|
-
await writeFakeGh({ stdout: `${url}\n` })
|
|
279
|
-
|
|
280
|
-
expect(await createPullRequest("jj-example")).toBe(url)
|
|
281
|
-
await expectRemoteBranch("task/jj-example")
|
|
282
|
-
const ghCall = await readGhCall()
|
|
283
|
-
expect(ghCall.args).toContain("--head")
|
|
284
|
-
expect(ghCall.args).toContain("--repo")
|
|
285
|
-
expect(ghCall.gitDir).toContain(".jj")
|
|
286
|
-
expect(await Bun.file(join(root, "repos/agency/.git")).exists()).toBe(false)
|
|
287
|
-
})
|
|
288
|
-
|
|
289
245
|
test("passes --draft to gh", async () => {
|
|
290
246
|
await createTask()
|
|
291
247
|
await writeFakeGh({
|
|
@@ -195,20 +195,6 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
yield* backend.push(workspace.writablePath, remote, execution.branch)
|
|
198
|
-
const defaults =
|
|
199
|
-
backend.kind === "jj"
|
|
200
|
-
? yield* backend.pullRequestDefaults(
|
|
201
|
-
workspace.writablePath,
|
|
202
|
-
execution.base,
|
|
203
|
-
)
|
|
204
|
-
: null
|
|
205
|
-
if (backend.kind === "jj" && !defaults) {
|
|
206
|
-
return yield* new PullRequestError({
|
|
207
|
-
message:
|
|
208
|
-
"Failed to derive pull request title and body from jj commits",
|
|
209
|
-
})
|
|
210
|
-
}
|
|
211
|
-
|
|
212
198
|
const remoteUrl = yield* backend.remoteUrl(
|
|
213
199
|
workspace.writablePath,
|
|
214
200
|
remote,
|
|
@@ -230,22 +216,15 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
230
216
|
})
|
|
231
217
|
: resolveGitHubCreateCommand({
|
|
232
218
|
base: execution.base,
|
|
233
|
-
branch: execution.branch,
|
|
234
|
-
repository,
|
|
235
219
|
draft,
|
|
236
|
-
vcs: config.vcs ?? "git",
|
|
237
220
|
title: options.title,
|
|
238
221
|
head: options.head,
|
|
239
222
|
labels: options.labels,
|
|
240
|
-
...(defaults ? { defaults } : {}),
|
|
241
223
|
})
|
|
242
|
-
const gitEnvironment = config.delivery
|
|
243
|
-
? {}
|
|
244
|
-
: yield* backend.gitEnvironment(workspace.writablePath)
|
|
245
224
|
const created = yield* fs.runCommand(resolved.argv, {
|
|
246
225
|
cwd: workspace.writablePath,
|
|
247
226
|
captureOutput: true,
|
|
248
|
-
env:
|
|
227
|
+
env: resolved.environment,
|
|
249
228
|
})
|
|
250
229
|
if (created.exitCode !== 0) {
|
|
251
230
|
return yield* new PullRequestError({
|