@markjaquith/agency 2.49.0 → 2.51.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 +56 -9
- package/cli.ts +56 -1
- package/package.json +1 -1
- package/schemas/agency-graph-v1.schema.json +142 -38
- package/src/cli-parser.test.ts +81 -0
- package/src/cli-parser.ts +78 -1
- package/src/cli.test.ts +4 -4
- package/src/commands/init.test.ts +2 -0
- package/src/commands/review.ts +38 -0
- package/src/commands/task.ts +27 -5
- package/src/commands/vcs.test.ts +75 -0
- package/src/commands/vcs.ts +72 -0
- package/src/commands/work.test.ts +2 -0
- package/src/commands/work.ts +2 -2
- package/src/commands/worktree.ts +1 -1
- package/src/graph-schema.test.ts +1 -1
- package/src/graph-schema.ts +36 -13
- package/src/protocol.ts +1 -0
- package/src/services/ArchiveService.test.ts +2 -2
- package/src/services/ArchiveService.ts +24 -0
- package/src/services/ClaimService.ts +3 -2
- package/src/services/ContextService.ts +118 -17
- package/src/services/DoctorService.ts +58 -7
- package/src/services/GraphMutationService.ts +5 -0
- package/src/services/GraphService.ts +119 -54
- package/src/services/PhaseService.ts +196 -70
- package/src/services/PullRequestService.test.ts +2 -2
- package/src/services/PullRequestService.ts +37 -33
- package/src/services/ReadinessService.ts +7 -3
- package/src/services/RepositoryService.test.ts +31 -1
- package/src/services/RepositoryService.ts +63 -31
- package/src/services/ReviewService.test.ts +472 -0
- package/src/services/ReviewService.ts +427 -0
- package/src/services/SyncService.test.ts +69 -2
- package/src/services/SyncService.ts +161 -90
- package/src/services/TaskPhaseService.test.ts +80 -0
- package/src/services/TaskService.ts +82 -9
- package/src/services/VcsMigrationService.test.ts +211 -0
- package/src/services/VcsMigrationService.ts +816 -0
- package/src/services/VersionControlService.test.ts +100 -0
- package/src/services/VersionControlService.ts +479 -0
- package/src/services/WorkbaseService.ts +7 -2
- package/src/services/WorktreeService.test.ts +88 -17
- package/src/services/WorktreeService.ts +865 -329
- package/src/test-utils.ts +12 -0
- package/src/work-view.ts +14 -6
- package/src/workbase/AGENTS.md +2 -2
- package/src/workbase/schemas.test.ts +57 -0
- package/src/workbase/schemas.ts +57 -0
- package/src/workbase/version-control.ts +5 -0
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
buildNonPrCompletion,
|
|
31
31
|
type NonPrCompletionInput,
|
|
32
32
|
} from "../workbase/completion"
|
|
33
|
+
import { VersionControlService } from "./VersionControlService"
|
|
33
34
|
|
|
34
35
|
class PhaseError extends Data.TaggedError("PhaseError")<{
|
|
35
36
|
readonly message: string
|
|
@@ -92,11 +93,18 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
92
93
|
Effect.gen(function* () {
|
|
93
94
|
const fs = yield* FileSystemService
|
|
94
95
|
const workbase = yield* WorkbaseService
|
|
96
|
+
const versionControl = yield* VersionControlService
|
|
95
97
|
const tasks = yield* TaskService
|
|
96
98
|
const root = yield* workbase.discover(startPath)
|
|
99
|
+
const backend = yield* versionControl.forWorkbase(root)
|
|
97
100
|
const taskId = yield* decodeId(input.taskId, "task")
|
|
98
101
|
const id = yield* decodeId(input.id, "phase")
|
|
99
102
|
const task = yield* tasks.show(taskId, root)
|
|
103
|
+
if ("review" in task.data) {
|
|
104
|
+
return yield* new PhaseError({
|
|
105
|
+
message: `Review task '${taskId}' cannot be converted to phases`,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
100
108
|
const isMultiPhase = "phases" in task.data
|
|
101
109
|
let firstPhaseId: string | undefined
|
|
102
110
|
if (!isMultiPhase) {
|
|
@@ -263,95 +271,213 @@ export class PhaseService extends Effect.Service<PhaseService>()(
|
|
|
263
271
|
firstData.repo,
|
|
264
272
|
...(firstData.repos ?? []).map((reference) => reference.repo),
|
|
265
273
|
]
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
274
|
+
if (backend.kind === "jj") {
|
|
275
|
+
const workspaces: {
|
|
276
|
+
alias: string
|
|
277
|
+
name: string
|
|
278
|
+
head: string
|
|
279
|
+
oldPath: string
|
|
280
|
+
newPath: string
|
|
281
|
+
}[] = []
|
|
282
|
+
const runBackend = <A>(
|
|
283
|
+
effect: Effect.Effect<A, unknown, any>,
|
|
284
|
+
) =>
|
|
285
|
+
Effect.runPromise(
|
|
286
|
+
effect.pipe(
|
|
287
|
+
Effect.provideService(FileSystemService, fs),
|
|
288
|
+
) as Effect.Effect<A, unknown, never>,
|
|
289
|
+
)
|
|
290
|
+
steps.push({
|
|
291
|
+
label: `move jj workspaces for ${taskId}/${firstPhaseId}`,
|
|
292
|
+
preflight: async () => {
|
|
293
|
+
for (const entry of await readdir(oldCodePath)) {
|
|
294
|
+
if (!checkoutAliases.includes(entry))
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Cannot convert task '${taskId}'; code contains unmanaged entry '${entry}'`,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
for (const alias of checkoutAliases) {
|
|
300
|
+
const oldPath = join(oldCodePath, alias)
|
|
301
|
+
try {
|
|
302
|
+
await lstat(oldPath)
|
|
303
|
+
} catch {
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
const expected = await realpath(oldPath)
|
|
307
|
+
const registered = (
|
|
308
|
+
await runBackend(
|
|
309
|
+
backend.listWorkspaces(join(root, "repos", alias)),
|
|
310
|
+
)
|
|
311
|
+
).find((workspace) => workspace.path === expected)
|
|
312
|
+
if (!registered?.name)
|
|
313
|
+
throw new Error(
|
|
314
|
+
`Cannot convert task '${taskId}'; checkout '${alias}' is not registered as a jj workspace`,
|
|
315
|
+
)
|
|
316
|
+
if (await runBackend(backend.workspaceDirty(oldPath)))
|
|
317
|
+
throw new Error(
|
|
318
|
+
`Cannot convert task '${taskId}'; jj workspace '${alias}' has uncommitted changes`,
|
|
319
|
+
)
|
|
320
|
+
const head = await runBackend(
|
|
321
|
+
backend.workspaceHead(oldPath),
|
|
296
322
|
)
|
|
297
|
-
|
|
323
|
+
if (!head)
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Cannot resolve jj workspace '${alias}' before conversion`,
|
|
326
|
+
)
|
|
327
|
+
workspaces.push({
|
|
328
|
+
alias,
|
|
329
|
+
name: registered.name,
|
|
330
|
+
head,
|
|
331
|
+
oldPath,
|
|
332
|
+
newPath: join(firstCodePath, alias),
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
apply: async () => {
|
|
337
|
+
await mkdir(firstCodePath, { recursive: true })
|
|
338
|
+
for (const workspace of workspaces) {
|
|
339
|
+
const repositoryPath = join(
|
|
340
|
+
root,
|
|
341
|
+
"repos",
|
|
342
|
+
workspace.alias,
|
|
343
|
+
)
|
|
344
|
+
await runBackend(
|
|
345
|
+
backend.removeWorkspace({
|
|
346
|
+
repositoryPath,
|
|
347
|
+
workspacePath: workspace.oldPath,
|
|
348
|
+
workspaceName: workspace.name,
|
|
349
|
+
}),
|
|
350
|
+
)
|
|
351
|
+
await runBackend(
|
|
352
|
+
backend.createWorkspace({
|
|
353
|
+
repositoryPath,
|
|
354
|
+
workspacePath: workspace.newPath,
|
|
355
|
+
workspaceName: workspace.name,
|
|
356
|
+
revision: workspace.head,
|
|
357
|
+
}),
|
|
358
|
+
)
|
|
359
|
+
}
|
|
360
|
+
await rm(oldCodePath, { recursive: true, force: true })
|
|
361
|
+
},
|
|
362
|
+
rollback: async () => {
|
|
363
|
+
await mkdir(oldCodePath, { recursive: true })
|
|
364
|
+
for (const workspace of [...workspaces].reverse()) {
|
|
365
|
+
const repositoryPath = join(
|
|
366
|
+
root,
|
|
367
|
+
"repos",
|
|
368
|
+
workspace.alias,
|
|
369
|
+
)
|
|
370
|
+
await runBackend(
|
|
371
|
+
backend.removeWorkspace({
|
|
372
|
+
repositoryPath,
|
|
373
|
+
workspacePath: workspace.newPath,
|
|
374
|
+
workspaceName: workspace.name,
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
await runBackend(
|
|
378
|
+
backend.createWorkspace({
|
|
379
|
+
repositoryPath,
|
|
380
|
+
workspacePath: workspace.oldPath,
|
|
381
|
+
workspaceName: workspace.name,
|
|
382
|
+
revision: workspace.head,
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
await rm(firstDirectory, { recursive: true, force: true })
|
|
387
|
+
},
|
|
388
|
+
manualRecovery: `Restore jj workspaces from ${firstCodePath} to ${oldCodePath}`,
|
|
389
|
+
})
|
|
390
|
+
} else {
|
|
391
|
+
const repair = async (basePath: string) => {
|
|
298
392
|
for (const alias of checkoutAliases) {
|
|
299
|
-
const checkoutPath = join(
|
|
393
|
+
const checkoutPath = join(basePath, alias)
|
|
300
394
|
try {
|
|
301
395
|
await lstat(checkoutPath)
|
|
302
396
|
} catch {
|
|
303
397
|
continue
|
|
304
398
|
}
|
|
305
|
-
const
|
|
399
|
+
const result = Bun.spawnSync([
|
|
306
400
|
"git",
|
|
307
401
|
"-C",
|
|
308
402
|
join(root, "repos", alias),
|
|
309
403
|
"worktree",
|
|
310
|
-
"
|
|
311
|
-
|
|
404
|
+
"repair",
|
|
405
|
+
checkoutPath,
|
|
312
406
|
])
|
|
313
|
-
if (
|
|
407
|
+
if (result.exitCode !== 0) {
|
|
314
408
|
throw new Error(
|
|
315
|
-
`Failed to
|
|
409
|
+
`Failed to repair moved worktree for '${alias}': ${new TextDecoder().decode(result.stderr)}`,
|
|
316
410
|
)
|
|
317
|
-
const expected = await realpath(checkoutPath)
|
|
318
|
-
let registered = false
|
|
319
|
-
for (const line of new TextDecoder()
|
|
320
|
-
.decode(listed.stdout)
|
|
321
|
-
.split("\n")) {
|
|
322
|
-
if (!line.startsWith("worktree ")) continue
|
|
323
|
-
try {
|
|
324
|
-
if ((await realpath(line.slice(9))) === expected) {
|
|
325
|
-
registered = true
|
|
326
|
-
break
|
|
327
|
-
}
|
|
328
|
-
} catch {}
|
|
329
411
|
}
|
|
330
|
-
if (!registered)
|
|
331
|
-
throw new Error(
|
|
332
|
-
`Cannot convert task '${taskId}'; checkout '${alias}' is not registered as a Git worktree`,
|
|
333
|
-
)
|
|
334
412
|
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
413
|
+
}
|
|
414
|
+
steps.push({
|
|
415
|
+
label: `move and repair code for ${taskId}/${firstPhaseId}`,
|
|
416
|
+
preflight: async () => {
|
|
417
|
+
for (const entry of await readdir(oldCodePath)) {
|
|
418
|
+
if (!checkoutAliases.includes(entry))
|
|
419
|
+
throw new Error(
|
|
420
|
+
`Cannot convert task '${taskId}'; code contains unmanaged entry '${entry}'`,
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
for (const alias of checkoutAliases) {
|
|
424
|
+
const checkoutPath = join(oldCodePath, alias)
|
|
425
|
+
try {
|
|
426
|
+
await lstat(checkoutPath)
|
|
427
|
+
} catch {
|
|
428
|
+
continue
|
|
429
|
+
}
|
|
430
|
+
const listed = Bun.spawnSync([
|
|
431
|
+
"git",
|
|
432
|
+
"-C",
|
|
433
|
+
join(root, "repos", alias),
|
|
434
|
+
"worktree",
|
|
435
|
+
"list",
|
|
436
|
+
"--porcelain",
|
|
437
|
+
])
|
|
438
|
+
if (listed.exitCode !== 0)
|
|
439
|
+
throw new Error(
|
|
440
|
+
`Failed to inspect worktrees for '${alias}'`,
|
|
441
|
+
)
|
|
442
|
+
const expected = await realpath(checkoutPath)
|
|
443
|
+
let registered = false
|
|
444
|
+
for (const line of new TextDecoder()
|
|
445
|
+
.decode(listed.stdout)
|
|
446
|
+
.split("\n")) {
|
|
447
|
+
if (!line.startsWith("worktree ")) continue
|
|
448
|
+
try {
|
|
449
|
+
if ((await realpath(line.slice(9))) === expected) {
|
|
450
|
+
registered = true
|
|
451
|
+
break
|
|
452
|
+
}
|
|
453
|
+
} catch {}
|
|
454
|
+
}
|
|
455
|
+
if (!registered)
|
|
456
|
+
throw new Error(
|
|
457
|
+
`Cannot convert task '${taskId}'; checkout '${alias}' is not registered as a Git worktree`,
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
apply: async () => {
|
|
462
|
+
await mkdir(firstDirectory, { recursive: true })
|
|
463
|
+
await rename(oldCodePath, firstCodePath)
|
|
464
|
+
try {
|
|
465
|
+
await repair(firstCodePath)
|
|
466
|
+
} catch (cause) {
|
|
467
|
+
await rename(firstCodePath, oldCodePath)
|
|
468
|
+
await repair(oldCodePath)
|
|
469
|
+
await rm(firstDirectory, { recursive: true, force: true })
|
|
470
|
+
throw cause
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
rollback: async () => {
|
|
342
474
|
await rename(firstCodePath, oldCodePath)
|
|
343
475
|
await repair(oldCodePath)
|
|
344
476
|
await rm(firstDirectory, { recursive: true, force: true })
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
await rename(firstCodePath, oldCodePath)
|
|
350
|
-
await repair(oldCodePath)
|
|
351
|
-
await rm(firstDirectory, { recursive: true, force: true })
|
|
352
|
-
},
|
|
353
|
-
manualRecovery: `Move ${firstCodePath} back to ${oldCodePath} and run git worktree repair`,
|
|
354
|
-
})
|
|
477
|
+
},
|
|
478
|
+
manualRecovery: `Move ${firstCodePath} back to ${oldCodePath} and run git worktree repair`,
|
|
479
|
+
})
|
|
480
|
+
}
|
|
355
481
|
}
|
|
356
482
|
steps.push(
|
|
357
483
|
documentWriteStep(root, [
|
|
@@ -353,7 +353,7 @@ process.exit(${exitCode})
|
|
|
353
353
|
test("blocks a dirty checkout before push or gh", async () => {
|
|
354
354
|
await createTask()
|
|
355
355
|
const workspace = await materialize()
|
|
356
|
-
await Bun.write(join(workspace.writablePath
|
|
356
|
+
await Bun.write(join(workspace.writablePath!, "dirty.txt"), "dirty\n")
|
|
357
357
|
await writeFakeGh({ stdout: "https://github.com/example/agency/pull/45" })
|
|
358
358
|
|
|
359
359
|
await expect(createPullRequest()).rejects.toThrow(
|
|
@@ -518,7 +518,7 @@ process.stdout.write(${JSON.stringify(JSON.stringify(providerRecord))})
|
|
|
518
518
|
test("reports git status failure before push or gh", async () => {
|
|
519
519
|
await createTask()
|
|
520
520
|
const workspace = await materialize()
|
|
521
|
-
await rm(join(workspace.writablePath
|
|
521
|
+
await rm(join(workspace.writablePath!, ".git"))
|
|
522
522
|
await writeFakeGh({ stdout: "https://github.com/example/agency/pull/48" })
|
|
523
523
|
|
|
524
524
|
await expect(createPullRequest()).rejects.toThrow(
|
|
@@ -6,6 +6,7 @@ import type { BaseCommandOptions } from "../utils/command"
|
|
|
6
6
|
import { TaskService } from "./TaskService"
|
|
7
7
|
import { PhaseService } from "./PhaseService"
|
|
8
8
|
import { ReadinessService } from "./ReadinessService"
|
|
9
|
+
import { VersionControlService } from "./VersionControlService"
|
|
9
10
|
import {
|
|
10
11
|
formatMarkdownDocument,
|
|
11
12
|
parseFrontmatter,
|
|
@@ -50,6 +51,11 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
50
51
|
const phases = yield* PhaseService
|
|
51
52
|
const root = yield* workbase.discover(startPath)
|
|
52
53
|
const task = yield* tasks.show(taskId, root)
|
|
54
|
+
if ("review" in task.data) {
|
|
55
|
+
return yield* new PullRequestError({
|
|
56
|
+
message: `Review task '${taskId}' cannot record a delivery pull request`,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
53
59
|
const target =
|
|
54
60
|
"phases" in task.data
|
|
55
61
|
? phaseId
|
|
@@ -105,15 +111,21 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
105
111
|
const worktrees = yield* WorktreeService
|
|
106
112
|
const readiness = yield* ReadinessService
|
|
107
113
|
const workbase = yield* WorkbaseService
|
|
108
|
-
const
|
|
114
|
+
const versionControl = yield* VersionControlService
|
|
115
|
+
const requestedTask = yield* tasks.show(taskId, startPath)
|
|
116
|
+
if ("review" in requestedTask.data) {
|
|
117
|
+
return yield* new PullRequestError({
|
|
118
|
+
message: `Review task '${taskId}' cannot create a delivery pull request`,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
109
121
|
const target =
|
|
110
|
-
"phases" in
|
|
122
|
+
"phases" in requestedTask.data
|
|
111
123
|
? phaseId
|
|
112
124
|
? yield* phases.show(taskId, phaseId, startPath)
|
|
113
125
|
: yield* new PullRequestError({
|
|
114
126
|
message: `Task '${taskId}' requires a phase ID`,
|
|
115
127
|
})
|
|
116
|
-
:
|
|
128
|
+
: requestedTask
|
|
117
129
|
if ("completion" in target.data && target.data.completion) {
|
|
118
130
|
return yield* new PullRequestError({
|
|
119
131
|
message:
|
|
@@ -134,56 +146,48 @@ export class PullRequestService extends Effect.Service<PullRequestService>()(
|
|
|
134
146
|
options,
|
|
135
147
|
)
|
|
136
148
|
const workspaceTask = yield* tasks.show(taskId, workspace.root)
|
|
149
|
+
if ("review" in workspaceTask.data) {
|
|
150
|
+
return yield* new PullRequestError({
|
|
151
|
+
message: `Review task '${taskId}' cannot create a delivery pull request`,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
137
154
|
const execution =
|
|
138
155
|
"phases" in workspaceTask.data
|
|
139
156
|
? (yield* phases.show(taskId, phaseId!, workspace.root)).data
|
|
140
157
|
: workspaceTask.data
|
|
141
158
|
const { config } = yield* workbase.loadConfig(workspace.root)
|
|
159
|
+
const backend = yield* versionControl.forWorkbase(workspace.root)
|
|
160
|
+
if (!workspace.writablePath) {
|
|
161
|
+
return yield* new PullRequestError({
|
|
162
|
+
message: `Task '${taskId}' has no writable checkout`,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
142
165
|
const remote = config.delivery?.remote ?? "origin"
|
|
143
166
|
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
{ captureOutput: true },
|
|
147
|
-
)
|
|
148
|
-
if (status.exitCode !== 0) {
|
|
167
|
+
const dirty = yield* backend.workspaceDirty(workspace.writablePath)
|
|
168
|
+
if (dirty === null) {
|
|
149
169
|
return yield* new PullRequestError({
|
|
150
|
-
message:
|
|
170
|
+
message: "Failed to inspect worktree status",
|
|
151
171
|
})
|
|
152
172
|
}
|
|
153
|
-
if (
|
|
173
|
+
if (dirty) {
|
|
154
174
|
return yield* new PullRequestError({
|
|
155
175
|
message: "Cannot create a PR with a dirty worktree",
|
|
156
176
|
})
|
|
157
177
|
}
|
|
158
178
|
|
|
159
|
-
|
|
160
|
-
[
|
|
161
|
-
"git",
|
|
162
|
-
"-C",
|
|
163
|
-
workspace.writablePath,
|
|
164
|
-
"push",
|
|
165
|
-
"--set-upstream",
|
|
166
|
-
remote,
|
|
167
|
-
execution.branch,
|
|
168
|
-
],
|
|
169
|
-
{ captureOutput: true },
|
|
170
|
-
)
|
|
171
|
-
if (push.exitCode !== 0) {
|
|
172
|
-
return yield* new PullRequestError({
|
|
173
|
-
message: `Failed to push branch: ${push.stderr}`,
|
|
174
|
-
})
|
|
175
|
-
}
|
|
179
|
+
yield* backend.push(workspace.writablePath, remote, execution.branch)
|
|
176
180
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
const remoteUrl = yield* backend.remoteUrl(
|
|
182
|
+
workspace.writablePath,
|
|
183
|
+
remote,
|
|
180
184
|
)
|
|
181
|
-
if (
|
|
185
|
+
if (!remoteUrl) {
|
|
182
186
|
return yield* new PullRequestError({
|
|
183
|
-
message: `Failed to inspect delivery remote '${remote}'
|
|
187
|
+
message: `Failed to inspect delivery remote '${remote}'`,
|
|
184
188
|
})
|
|
185
189
|
}
|
|
186
|
-
const repository = repositoryFromRemote(
|
|
190
|
+
const repository = repositoryFromRemote(remoteUrl)
|
|
187
191
|
const resolved = config.delivery
|
|
188
192
|
? resolveDeliveryCommand(config.delivery, "create", {
|
|
189
193
|
repository,
|
|
@@ -77,7 +77,7 @@ const itemFor = (
|
|
|
77
77
|
? task.data.epic
|
|
78
78
|
: undefined
|
|
79
79
|
const dependentIds = new Set(node.dependents)
|
|
80
|
-
if (node.data.phaseId) {
|
|
80
|
+
if ("phaseId" in node.data && node.data.phaseId) {
|
|
81
81
|
const siblings = graph.nodes.filter(
|
|
82
82
|
(candidate): candidate is ExecutionNode =>
|
|
83
83
|
candidate.kind === "execution-unit" &&
|
|
@@ -93,10 +93,14 @@ const itemFor = (
|
|
|
93
93
|
rank,
|
|
94
94
|
key: node.key,
|
|
95
95
|
taskId: node.data.taskId,
|
|
96
|
-
...(node.data
|
|
96
|
+
...("phaseId" in node.data && node.data.phaseId
|
|
97
|
+
? { phaseId: node.data.phaseId }
|
|
98
|
+
: {}),
|
|
97
99
|
...(node.data.description ? { description: node.data.description } : {}),
|
|
98
100
|
parent: {
|
|
99
|
-
...(node.data
|
|
101
|
+
...("phaseId" in node.data && node.data.phaseId
|
|
102
|
+
? { taskId: node.data.taskId }
|
|
103
|
+
: {}),
|
|
100
104
|
...(epicId ? { epicId } : {}),
|
|
101
105
|
},
|
|
102
106
|
status: node.status,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
2
|
import { Effect } from "effect"
|
|
3
|
-
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { mkdir, stat } from "node:fs/promises"
|
|
4
4
|
import { dirname, join } from "node:path"
|
|
5
5
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
6
|
import { RepositoryService } from "./RepositoryService"
|
|
@@ -98,6 +98,36 @@ describe("RepositoryService", () => {
|
|
|
98
98
|
])
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
+
test("initializes a managed clone with jj for a jj workbase", async () => {
|
|
102
|
+
if (!Bun.which("jj")) return
|
|
103
|
+
await Bun.write(
|
|
104
|
+
join(root, "agency.json"),
|
|
105
|
+
JSON.stringify({ version: 2, vcs: "jj" }),
|
|
106
|
+
)
|
|
107
|
+
const source = join(root, "source")
|
|
108
|
+
await runGit(["init", "--initial-branch=main", source])
|
|
109
|
+
await runGit(["-C", source, "config", "user.email", "test@example.com"])
|
|
110
|
+
await runGit(["-C", source, "config", "user.name", "Test"])
|
|
111
|
+
await Bun.write(join(source, "README.md"), "example\n")
|
|
112
|
+
await runGit(["-C", source, "add", "README.md"])
|
|
113
|
+
await runGit(["-C", source, "commit", "-m", "initial"])
|
|
114
|
+
await setPortableOrigin(source, "jj-agency")
|
|
115
|
+
|
|
116
|
+
const destination = await runTestEffect(
|
|
117
|
+
RepositoryService.pipe(
|
|
118
|
+
Effect.flatMap((service) => service.add("agency", source, root)),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
expect((await stat(join(destination, ".jj"))).isDirectory()).toBe(true)
|
|
122
|
+
expect(
|
|
123
|
+
await runTestEffect(
|
|
124
|
+
RepositoryService.pipe(
|
|
125
|
+
Effect.flatMap((service) => service.show("agency", root)),
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
).toMatchObject({ kind: "repository" })
|
|
129
|
+
})
|
|
130
|
+
|
|
101
131
|
test("links an existing repository", async () => {
|
|
102
132
|
const target = join(root, "linked-repository")
|
|
103
133
|
await mkdir(target, { recursive: true })
|