@markjaquith/agency 2.73.0 → 2.73.2
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/package.json
CHANGED
package/src/cli-parser.test.ts
CHANGED
|
@@ -229,7 +229,9 @@ describe("strict CLI parsing", () => {
|
|
|
229
229
|
test("parses addressable resource maintenance commands", () => {
|
|
230
230
|
for (const args of [
|
|
231
231
|
["doctor", "--json"],
|
|
232
|
+
["repo", "materialize", "--help"],
|
|
232
233
|
["repo", "show", "agency", "--json"],
|
|
234
|
+
["repo", "materialize", "agency", "--json"],
|
|
233
235
|
["repo", "fetch", "agency"],
|
|
234
236
|
["repo", "remove", "agency"],
|
|
235
237
|
["repo", "unlink", "agency"],
|
|
@@ -270,6 +272,7 @@ describe("strict CLI parsing", () => {
|
|
|
270
272
|
[["integration", "sync", "extra"], "agency integration sync"],
|
|
271
273
|
[["repo", "add", "a", "b", "extra"], "agency repo add"],
|
|
272
274
|
[["repo", "link", "a", "b", "extra"], "agency repo link"],
|
|
275
|
+
[["repo", "materialize", "a", "extra"], "agency repo materialize"],
|
|
273
276
|
[["repo", "list", "extra"], "agency repo list"],
|
|
274
277
|
[
|
|
275
278
|
[
|
package/src/cli-parser.ts
CHANGED
|
@@ -259,7 +259,7 @@ const commands = {
|
|
|
259
259
|
},
|
|
260
260
|
repo: {
|
|
261
261
|
usage:
|
|
262
|
-
"agency repo <setup|add|link|list|show|fetch|remove|unlink|rename|remote|verify>",
|
|
262
|
+
"agency repo <setup|add|link|materialize|list|show|fetch|remove|unlink|rename|remote|verify>",
|
|
263
263
|
options: {
|
|
264
264
|
...outputOptions,
|
|
265
265
|
"dry-run": { type: "boolean" },
|
|
@@ -285,6 +285,12 @@ const commands = {
|
|
|
285
285
|
maxArgs: 2,
|
|
286
286
|
options: ["json"],
|
|
287
287
|
},
|
|
288
|
+
materialize: {
|
|
289
|
+
usage: "agency repo materialize <alias> [--json]",
|
|
290
|
+
minArgs: 1,
|
|
291
|
+
maxArgs: 1,
|
|
292
|
+
options: ["json"],
|
|
293
|
+
},
|
|
288
294
|
list: {
|
|
289
295
|
usage: "agency repo list [--json]",
|
|
290
296
|
minArgs: 0,
|
package/src/cli.test.ts
CHANGED
|
@@ -934,6 +934,48 @@ status: open
|
|
|
934
934
|
})
|
|
935
935
|
})
|
|
936
936
|
|
|
937
|
+
test("materializes a linked repository through the CLI", async () => {
|
|
938
|
+
const parent = await createTempDir()
|
|
939
|
+
tempDirs.push(parent)
|
|
940
|
+
const root = join(parent, "workbase")
|
|
941
|
+
const source = join(parent, "source")
|
|
942
|
+
await runGit(["init", "--initial-branch=main", source])
|
|
943
|
+
await Bun.write(join(source, "README.md"), "materialize\n")
|
|
944
|
+
for (const args of [
|
|
945
|
+
["config", "user.email", "test@example.com"],
|
|
946
|
+
["config", "user.name", "Test"],
|
|
947
|
+
["add", "README.md"],
|
|
948
|
+
["-c", "commit.gpgsign=false", "commit", "-m", "initial"],
|
|
949
|
+
[
|
|
950
|
+
"remote",
|
|
951
|
+
"add",
|
|
952
|
+
"origin",
|
|
953
|
+
"https://example.com/agency-tests/source.git",
|
|
954
|
+
],
|
|
955
|
+
]) {
|
|
956
|
+
await runGit(["-C", source, ...args])
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
parseJson(await runCli(["init", root, "--json"], parent))
|
|
960
|
+
const configPath = join(root, "agency.json")
|
|
961
|
+
await Bun.write(
|
|
962
|
+
configPath,
|
|
963
|
+
JSON.stringify({ ...(await Bun.file(configPath).json()), vcs: "git" }),
|
|
964
|
+
)
|
|
965
|
+
parseJson(await runCli(["repo", "link", "agency", source, "--json"], root))
|
|
966
|
+
const materialized = parseJson(
|
|
967
|
+
await runCli(["repo", "materialize", "agency", "--json"], root),
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
expect(materialized).toMatchObject({
|
|
971
|
+
alias: "agency",
|
|
972
|
+
kind: "bare",
|
|
973
|
+
target: null,
|
|
974
|
+
states: ["declared", "materialized"],
|
|
975
|
+
})
|
|
976
|
+
expect(await Bun.file(join(root, "repos/agency/HEAD")).exists()).toBe(true)
|
|
977
|
+
})
|
|
978
|
+
|
|
937
979
|
test("prepares a workspace and reports a non-mutating dry-run", async () => {
|
|
938
980
|
const parent = await createTempDir()
|
|
939
981
|
tempDirs.push(parent)
|
|
@@ -212,6 +212,13 @@ status: working
|
|
|
212
212
|
"main",
|
|
213
213
|
])
|
|
214
214
|
await Bun.write(join(checkout, "dirty.txt"), "preserved\n")
|
|
215
|
+
await runGit([
|
|
216
|
+
"-C",
|
|
217
|
+
target,
|
|
218
|
+
"update-ref",
|
|
219
|
+
"refs/agency/reviews/active",
|
|
220
|
+
"main",
|
|
221
|
+
])
|
|
215
222
|
|
|
216
223
|
const result = await runTestEffect(
|
|
217
224
|
RepositoryService.pipe(
|
|
@@ -238,6 +245,96 @@ status: working
|
|
|
238
245
|
expect(await Bun.file(join(root, "tasks/active/TASK.md")).text()).toContain(
|
|
239
246
|
"repo: linked",
|
|
240
247
|
)
|
|
248
|
+
expect(
|
|
249
|
+
await gitOutput([
|
|
250
|
+
"--git-dir",
|
|
251
|
+
join(root, "repos/linked"),
|
|
252
|
+
"rev-parse",
|
|
253
|
+
"refs/agency/reviews/active",
|
|
254
|
+
]),
|
|
255
|
+
).toBe(await gitOutput(["-C", target, "rev-parse", "main"]))
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
test("preserves detached registered worktree commits from a shallow linked repository", async () => {
|
|
259
|
+
const upstream = join(root, "upstream")
|
|
260
|
+
const target = join(root, "shallow-linked-repository")
|
|
261
|
+
const checkout = join(root, "tasks/detached/code/linked")
|
|
262
|
+
await runGit(["init", "--bare", "--initial-branch=main", upstream])
|
|
263
|
+
const seed = join(root, "seed")
|
|
264
|
+
await runGit(["clone", upstream, seed])
|
|
265
|
+
await runGit(["-C", seed, "config", "user.email", "test@example.com"])
|
|
266
|
+
await runGit(["-C", seed, "config", "user.name", "Test"])
|
|
267
|
+
await Bun.write(join(seed, "README.md"), "shallow\n")
|
|
268
|
+
await runGit(["-C", seed, "add", "README.md"])
|
|
269
|
+
await runGit(["-C", seed, "commit", "-m", "initial"])
|
|
270
|
+
await runGit(["-C", seed, "push", "origin", "main"])
|
|
271
|
+
await runGit(["clone", "--depth=1", `file://${upstream}`, target])
|
|
272
|
+
await runGit(["-C", target, "config", "user.email", "test@example.com"])
|
|
273
|
+
await runGit(["-C", target, "config", "user.name", "Test"])
|
|
274
|
+
await runGit([
|
|
275
|
+
"-C",
|
|
276
|
+
target,
|
|
277
|
+
"remote",
|
|
278
|
+
"set-url",
|
|
279
|
+
"origin",
|
|
280
|
+
portableRemote("shallow-materialized"),
|
|
281
|
+
])
|
|
282
|
+
await mkdir(dirname(checkout), { recursive: true })
|
|
283
|
+
await runGit([
|
|
284
|
+
"-C",
|
|
285
|
+
target,
|
|
286
|
+
"worktree",
|
|
287
|
+
"add",
|
|
288
|
+
"--detach",
|
|
289
|
+
checkout,
|
|
290
|
+
"main",
|
|
291
|
+
])
|
|
292
|
+
await Bun.write(join(checkout, "detached.txt"), "preserved\n")
|
|
293
|
+
await runGit(["-C", checkout, "add", "detached.txt"])
|
|
294
|
+
await runGit(["-C", checkout, "commit", "-m", "detached work"])
|
|
295
|
+
const detachedCommit = await gitOutput([
|
|
296
|
+
"-C",
|
|
297
|
+
checkout,
|
|
298
|
+
"rev-parse",
|
|
299
|
+
"HEAD",
|
|
300
|
+
])
|
|
301
|
+
await runTestEffect(
|
|
302
|
+
RepositoryService.pipe(
|
|
303
|
+
Effect.flatMap((service) => service.link("linked", target, root)),
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
await runTestEffect(
|
|
308
|
+
RepositoryService.pipe(
|
|
309
|
+
Effect.flatMap((service) => service.materialize("linked", root)),
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
expect(await gitOutput(["-C", checkout, "rev-parse", "HEAD"])).toBe(
|
|
314
|
+
detachedCommit,
|
|
315
|
+
)
|
|
316
|
+
expect(await Bun.file(join(checkout, "detached.txt")).text()).toBe(
|
|
317
|
+
"preserved\n",
|
|
318
|
+
)
|
|
319
|
+
expect(
|
|
320
|
+
await gitOutput(["-C", checkout, "rev-parse", "--git-common-dir"]),
|
|
321
|
+
).toBe(await realpath(join(root, "repos/linked")))
|
|
322
|
+
await runGit([
|
|
323
|
+
"--git-dir",
|
|
324
|
+
join(root, "repos/linked"),
|
|
325
|
+
"cat-file",
|
|
326
|
+
"-e",
|
|
327
|
+
`${detachedCommit}^{commit}`,
|
|
328
|
+
])
|
|
329
|
+
expect(
|
|
330
|
+
await gitOutput([
|
|
331
|
+
"--git-dir",
|
|
332
|
+
join(root, "repos/linked"),
|
|
333
|
+
"for-each-ref",
|
|
334
|
+
"--format=%(refname)",
|
|
335
|
+
"refs/agency/materialize/",
|
|
336
|
+
]),
|
|
337
|
+
).toBe("")
|
|
241
338
|
})
|
|
242
339
|
|
|
243
340
|
test("materializes an alias linked through an existing worktree", async () => {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { documentRevision } from "../workbase/document-revision"
|
|
20
20
|
import {
|
|
21
21
|
VersionControlService,
|
|
22
|
+
type RegisteredWorkspace,
|
|
22
23
|
type VersionControlBackend,
|
|
23
24
|
} from "./VersionControlService"
|
|
24
25
|
|
|
@@ -558,9 +559,9 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
558
559
|
|
|
559
560
|
const source = yield* fs.realPath(repository.path)
|
|
560
561
|
const registered = yield* backend.listWorkspaces(repository.path)
|
|
561
|
-
const
|
|
562
|
-
.map((
|
|
563
|
-
.sort()
|
|
562
|
+
const registeredState = registered
|
|
563
|
+
.map(({ path, commit, branch }) => ({ path, commit, branch }))
|
|
564
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
564
565
|
const worktrees: (typeof registered)[number][] = []
|
|
565
566
|
for (const workspace of registered) {
|
|
566
567
|
if (!(yield* fs.isDirectory(workspace.path))) {
|
|
@@ -634,6 +635,77 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
634
635
|
),
|
|
635
636
|
),
|
|
636
637
|
)
|
|
638
|
+
const reviewRefs = yield* fs.runCommand(
|
|
639
|
+
[
|
|
640
|
+
"git",
|
|
641
|
+
"-C",
|
|
642
|
+
source,
|
|
643
|
+
"for-each-ref",
|
|
644
|
+
"--format=%(refname)",
|
|
645
|
+
"refs/agency/reviews/",
|
|
646
|
+
],
|
|
647
|
+
{ captureOutput: true },
|
|
648
|
+
)
|
|
649
|
+
if (reviewRefs.exitCode !== 0) {
|
|
650
|
+
yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
|
|
651
|
+
return yield* new RepositoryError({
|
|
652
|
+
message: `Failed to enumerate Agency review refs while materializing repository '${alias}'`,
|
|
653
|
+
})
|
|
654
|
+
}
|
|
655
|
+
const preservedReviewRefs = reviewRefs.stdout
|
|
656
|
+
.split("\n")
|
|
657
|
+
.map((ref) => ref.trim())
|
|
658
|
+
.filter(Boolean)
|
|
659
|
+
const worktreeCommits = [
|
|
660
|
+
...new Set(
|
|
661
|
+
worktrees
|
|
662
|
+
.map((workspace) => workspace.commit)
|
|
663
|
+
.filter((commit): commit is string => commit !== null),
|
|
664
|
+
),
|
|
665
|
+
]
|
|
666
|
+
const temporaryRefs = worktreeCommits.map(
|
|
667
|
+
(_, index) =>
|
|
668
|
+
`refs/agency/materialize/${suffix}/${String(index).padStart(4, "0")}`,
|
|
669
|
+
)
|
|
670
|
+
const preservationRefspecs = [
|
|
671
|
+
...preservedReviewRefs.map((ref) => `+${ref}:${ref}`),
|
|
672
|
+
...worktreeCommits.map(
|
|
673
|
+
(commit, index) => `+${commit}:${temporaryRefs[index]}`,
|
|
674
|
+
),
|
|
675
|
+
]
|
|
676
|
+
if (preservationRefspecs.length > 0) {
|
|
677
|
+
const preserved = yield* fs.runCommand(
|
|
678
|
+
[
|
|
679
|
+
"git",
|
|
680
|
+
"--git-dir",
|
|
681
|
+
staging,
|
|
682
|
+
"fetch",
|
|
683
|
+
"--no-tags",
|
|
684
|
+
"--no-write-fetch-head",
|
|
685
|
+
source,
|
|
686
|
+
...preservationRefspecs,
|
|
687
|
+
],
|
|
688
|
+
{ captureOutput: true },
|
|
689
|
+
)
|
|
690
|
+
if (preserved.exitCode !== 0) {
|
|
691
|
+
yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
|
|
692
|
+
return yield* new RepositoryError({
|
|
693
|
+
message: `Failed to preserve Agency refs and registered worktree commits while materializing repository '${alias}': ${preserved.stderr.trim()}`,
|
|
694
|
+
})
|
|
695
|
+
}
|
|
696
|
+
for (const ref of temporaryRefs) {
|
|
697
|
+
const removed = yield* fs.runCommand(
|
|
698
|
+
["git", "--git-dir", staging, "update-ref", "-d", ref],
|
|
699
|
+
{ captureOutput: true },
|
|
700
|
+
)
|
|
701
|
+
if (removed.exitCode !== 0) {
|
|
702
|
+
yield* fs.deleteDirectory(staging).pipe(Effect.ignore)
|
|
703
|
+
return yield* new RepositoryError({
|
|
704
|
+
message: `Failed to remove temporary materialization ref '${ref}' for repository '${alias}': ${removed.stderr.trim()}`,
|
|
705
|
+
})
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
637
709
|
yield* backend
|
|
638
710
|
.setRemoteUrl(staging, "origin", repository.declaredRemote)
|
|
639
711
|
.pipe(
|
|
@@ -728,16 +800,16 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
|
|
|
728
800
|
.pipe(
|
|
729
801
|
Effect.provideService(FileSystemService, fs),
|
|
730
802
|
) as unknown as Effect.Effect<
|
|
731
|
-
readonly
|
|
803
|
+
readonly RegisteredWorkspace[],
|
|
732
804
|
unknown,
|
|
733
805
|
never
|
|
734
806
|
>,
|
|
735
807
|
)
|
|
736
|
-
const
|
|
737
|
-
.map((
|
|
738
|
-
.sort()
|
|
808
|
+
const currentState = current
|
|
809
|
+
.map(({ path, commit, branch }) => ({ path, commit, branch }))
|
|
810
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
739
811
|
if (
|
|
740
|
-
JSON.stringify(
|
|
812
|
+
JSON.stringify(currentState) !== JSON.stringify(registeredState)
|
|
741
813
|
)
|
|
742
814
|
throw new Error(
|
|
743
815
|
`Git worktree registrations changed during materialization`,
|