@markjaquith/agency 2.71.20 → 2.71.21

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markjaquith/agency",
3
- "version": "2.71.20",
3
+ "version": "2.71.21",
4
4
  "description": "Manage agentic work across repositories with durable workbases",
5
5
  "keywords": [
6
6
  "agents",
@@ -21,8 +21,9 @@
21
21
  "index.ts",
22
22
  "cli.ts",
23
23
  "cli-main.ts",
24
- "pi-extensions",
25
24
  "src",
25
+ "pi-extensions",
26
+ "scripts/install-pi-extension.ts",
26
27
  "schemas",
27
28
  "fixtures/protocol",
28
29
  "README.md",
@@ -67,6 +68,7 @@
67
68
  "benchmark:act": "bun scripts/benchmark-act.ts",
68
69
  "benchmark:pr": "bun scripts/benchmark-pr.ts",
69
70
  "benchmark:release": "bun scripts/benchmark-release.ts",
71
+ "benchmark:repositories": "bun scripts/benchmark-repositories.ts",
70
72
  "benchmark:status": "bun scripts/benchmark-status.ts",
71
73
  "benchmark:next": "bun scripts/benchmark-next.ts",
72
74
  "benchmark:init": "bun scripts/benchmark-init.ts",
@@ -0,0 +1,25 @@
1
+ import { cp, mkdir, rm } from "node:fs/promises"
2
+ import { homedir } from "node:os"
3
+ import { dirname, join } from "node:path"
4
+
5
+ export const piExtensionPath = (home = homedir()) =>
6
+ join(home, ".pi", "agent", "extensions", "agency.ts")
7
+
8
+ export const installPiExtension = async (
9
+ source = join(import.meta.dir, "..", "pi-extensions", "agency.ts"),
10
+ destination = piExtensionPath(),
11
+ ) => {
12
+ await mkdir(dirname(destination), { recursive: true })
13
+ await cp(source, destination)
14
+ }
15
+
16
+ export const uninstallPiExtension = async (destination = piExtensionPath()) => {
17
+ await rm(destination, { force: true })
18
+ }
19
+
20
+ if (import.meta.main) {
21
+ const command = process.argv[2] ?? "install"
22
+ if (command === "install") await installPiExtension()
23
+ else if (command === "uninstall") await uninstallPiExtension()
24
+ else throw new Error(`Unknown Pi extension lifecycle command: ${command}`)
25
+ }
@@ -520,8 +520,9 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
520
520
  ...Object.keys(config.repositories ?? {}),
521
521
  ...local.keys(),
522
522
  ])
523
- const repositories = yield* Effect.all(
524
- [...aliases].sort().map((alias) =>
523
+ return yield* Effect.forEach(
524
+ [...aliases].sort(),
525
+ (alias) =>
525
526
  Effect.gen(function* () {
526
527
  const path = join(reposPath, alias)
527
528
  const entry = local.get(alias)
@@ -575,10 +576,8 @@ export class RepositoryService extends Effect.Service<RepositoryService>()(
575
576
  states,
576
577
  } satisfies RepositoryInfo
577
578
  }),
578
- ),
579
- { concurrency: 4 },
579
+ { concurrency: 8 },
580
580
  )
581
- return repositories
582
581
  }),
583
582
 
584
583
  show: (alias: string, startPath: string = process.cwd()) =>
@@ -1,9 +1,13 @@
1
1
  import { afterEach, describe, expect, test } from "bun:test"
2
- import { Effect } from "effect"
2
+ import { Effect, Layer } from "effect"
3
3
  import { mkdir, realpath, stat } from "node:fs/promises"
4
4
  import { join } from "node:path"
5
5
  import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
6
- import { VersionControlService } from "./VersionControlService"
6
+ import {
7
+ GitVersionControlService,
8
+ VersionControlService,
9
+ } from "./VersionControlService"
10
+ import { FileSystemService } from "./FileSystemService"
7
11
  import { preferredVersionControl } from "../workbase/version-control"
8
12
 
9
13
  const run = async (args: string[], cwd?: string) => {
@@ -24,6 +28,39 @@ describe("VersionControlService", () => {
24
28
  expect(preferredVersionControl(() => null)).toBe("git")
25
29
  })
26
30
 
31
+ test("inspects a Git repository with one subprocess", async () => {
32
+ const commands: readonly string[][] = []
33
+ const fileSystem = {
34
+ ...FileSystemService.Default,
35
+ runCommand: (command: readonly string[]) => {
36
+ ;(commands as string[][]).push([...command])
37
+ return Effect.succeed({
38
+ exitCode: 0,
39
+ stdout:
40
+ "local\tcore.bare true\nlocal\tremote.origin.url agency:agency.git\nglobal\turl.https://example.com/.insteadof agency:\n",
41
+ stderr: "",
42
+ })
43
+ },
44
+ } as unknown as Effect.Effect.Success<typeof FileSystemService>
45
+ const backend = await Effect.runPromise(
46
+ GitVersionControlService.pipe(
47
+ Effect.provide(GitVersionControlService.Default),
48
+ ),
49
+ )
50
+ const inspection = await Effect.runPromise(
51
+ backend
52
+ .inspectRepository("/repository")
53
+ .pipe(Effect.provide(Layer.succeed(FileSystemService, fileSystem))),
54
+ )
55
+
56
+ expect(commands).toHaveLength(1)
57
+ expect(commands[0]).toContain("--get-regexp")
58
+ expect(inspection).toEqual({
59
+ kind: "bare",
60
+ remote: "https://example.com/agency.git",
61
+ })
62
+ })
63
+
27
64
  test("selects the backend persisted by the workbase", async () => {
28
65
  for (const kind of ["git", "jj"] as const) {
29
66
  const root = await createTempDir()
@@ -118,6 +118,52 @@ const requireSuccess = (
118
118
  ),
119
119
  )
120
120
 
121
+ interface GitConfigEntry {
122
+ readonly scope: string
123
+ readonly key: string
124
+ readonly value: string
125
+ }
126
+
127
+ const parseGitConfig = (output: string): readonly GitConfigEntry[] =>
128
+ output
129
+ .trim()
130
+ .split("\n")
131
+ .filter(Boolean)
132
+ .flatMap((line) => {
133
+ const scopeSeparator = line.indexOf("\t")
134
+ const valueSeparator = line.indexOf(" ", scopeSeparator + 1)
135
+ return scopeSeparator >= 0 && valueSeparator >= 0
136
+ ? [
137
+ {
138
+ scope: line.slice(0, scopeSeparator),
139
+ key: line.slice(scopeSeparator + 1, valueSeparator),
140
+ value: line.slice(valueSeparator + 1),
141
+ },
142
+ ]
143
+ : []
144
+ })
145
+
146
+ const expandGitUrl = (
147
+ url: string,
148
+ entries: readonly GitConfigEntry[],
149
+ ): string => {
150
+ let replacement: { readonly prefix: string; readonly base: string } | null =
151
+ null
152
+ for (const entry of entries) {
153
+ const match = /^url\.(.*)\.insteadof$/i.exec(entry.key)
154
+ if (
155
+ match?.[1] !== undefined &&
156
+ url.startsWith(entry.value) &&
157
+ (replacement === null || entry.value.length > replacement.prefix.length)
158
+ ) {
159
+ replacement = { prefix: entry.value, base: match[1] }
160
+ }
161
+ }
162
+ return replacement === null
163
+ ? url
164
+ : replacement.base + url.slice(replacement.prefix.length)
165
+ }
166
+
121
167
  const parseGitWorktrees = (output: string): readonly RegisteredWorkspace[] => {
122
168
  const workspaces: RegisteredWorkspace[] = []
123
169
  let current: RegisteredWorkspace | null = null
@@ -168,22 +214,33 @@ export class GitVersionControlService extends Effect.Service<GitVersionControlSe
168
214
  inspectRepository: (path) =>
169
215
  Effect.gen(function* () {
170
216
  const fs = yield* FileSystemService
171
- const valid = yield* fs.runCommand(
172
- ["git", "-C", path, "rev-parse", "--git-dir"],
173
- { captureOutput: true },
174
- )
175
- if (valid.exitCode !== 0) return null
176
- const bare = yield* fs.runCommand(
177
- ["git", "-C", path, "rev-parse", "--is-bare-repository"],
178
- { captureOutput: true },
179
- )
180
- const remote = yield* fs.runCommand(
181
- ["git", "-C", path, "remote", "get-url", "origin"],
217
+ const inspection = yield* fs.runCommand(
218
+ [
219
+ "git",
220
+ "-C",
221
+ path,
222
+ "config",
223
+ "--show-scope",
224
+ "--get-regexp",
225
+ "^(core\\.bare|remote\\.origin\\.url|url\\..*\\.insteadof)$",
226
+ ],
182
227
  { captureOutput: true },
183
228
  )
229
+ if (inspection.exitCode !== 0) return null
230
+ const entries = parseGitConfig(inspection.stdout)
231
+ const bare = entries.find(
232
+ (entry) =>
233
+ (entry.scope === "local" || entry.scope === "worktree") &&
234
+ entry.key === "core.bare",
235
+ )
236
+ if (!bare) return null
237
+ const remote = entries.find(
238
+ (entry) => entry.key === "remote.origin.url",
239
+ )?.value
184
240
  return {
185
- kind: bare.stdout.trim() === "true" ? "bare" : "repository",
186
- remote: remote.exitCode === 0 ? remote.stdout.trim() : null,
241
+ kind: bare.value === "true" ? "bare" : "repository",
242
+ remote:
243
+ remote === undefined ? null : expandGitUrl(remote, entries),
187
244
  } as const
188
245
  }),
189
246
  gitEnvironment: () => Effect.succeed({}),