@markjaquith/agency 2.71.19 → 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.19",
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,9 +68,11 @@
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",
75
+ "benchmark:integration": "bun scripts/benchmark-integration.ts",
73
76
  "benchmark:workbase": "bun scripts/benchmark-workbase.ts",
74
77
  "benchmark:doctor": "bun scripts/benchmark-doctor.ts",
75
78
  "benchmark:context": "bun scripts/benchmark-context.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
+ }
@@ -18,7 +18,7 @@ export const init = (options: InitOptions = {}) =>
18
18
  const root = yield* workbase.initialize(
19
19
  options.path ? resolve(cwd, options.path) : cwd,
20
20
  )
21
- yield* integrations.sync(root)
21
+ yield* integrations.syncRoot(root)
22
22
  log(
23
23
  options.json
24
24
  ? JSON.stringify({ root }, null, 2)
@@ -258,6 +258,18 @@ const createHarness = (options: HarnessOptions = {}) => {
258
258
  ],
259
259
  })
260
260
  },
261
+ syncRoot: () => {
262
+ integrationSyncs += 1
263
+ return Effect.succeed({
264
+ root: "/workbase",
265
+ files: [
266
+ {
267
+ name: "opencode",
268
+ state: "managed",
269
+ },
270
+ ],
271
+ })
272
+ },
261
273
  }
262
274
  const fs = {
263
275
  isDirectory: (path: string) =>
@@ -130,7 +130,7 @@ export const work = (
130
130
  const inputAllowed = options.inputAllowed ?? true
131
131
  const root = yield* resolveWorkbase(startPath, pickBase, inputAllowed)
132
132
  if (!root) return
133
- yield* integrations.sync(root)
133
+ yield* integrations.syncRoot(root)
134
134
  const { config } = yield* workbase.loadConfig(root)
135
135
  const globalConfig = yield* workbase.loadGlobalConfig()
136
136
 
@@ -91,6 +91,38 @@ export class FileSystemService extends Effect.Service<FileSystemService>()(
91
91
  catch: () => new FileNotFoundError({ path }),
92
92
  }),
93
93
 
94
+ inspectFile: (path: string) =>
95
+ Effect.tryPromise({
96
+ try: async () => {
97
+ try {
98
+ const stats = await lstat(path)
99
+ if (stats.isSymbolicLink()) {
100
+ return { kind: "symlink" as const }
101
+ }
102
+ if (!stats.isFile()) return { kind: "other" as const }
103
+ return {
104
+ kind: "file" as const,
105
+ content: await Bun.file(path).text(),
106
+ }
107
+ } catch (error) {
108
+ if (
109
+ typeof error === "object" &&
110
+ error !== null &&
111
+ "code" in error &&
112
+ error.code === "ENOENT"
113
+ ) {
114
+ return { kind: "missing" as const }
115
+ }
116
+ throw error
117
+ }
118
+ },
119
+ catch: (error) =>
120
+ new FileSystemError({
121
+ message: `Failed to inspect file: ${path}`,
122
+ cause: error,
123
+ }),
124
+ }),
125
+
94
126
  writeFile: (path: string, content: string) =>
95
127
  Effect.tryPromise({
96
128
  try: () => Bun.write(path, content),
@@ -17,6 +17,7 @@ import {
17
17
  managedWorkbaseOpencodeTuiPlugin,
18
18
  } from "../workbase/opencode-tui-plugin-file"
19
19
  import { IntegrationService } from "./IntegrationService"
20
+ import { FileSystemService } from "./FileSystemService"
20
21
 
21
22
  const write = async (root: string, path: string, content: string) => {
22
23
  const fullPath = join(root, path)
@@ -90,6 +91,54 @@ describe("IntegrationService", () => {
90
91
  ])
91
92
  })
92
93
 
94
+ test("inspects each integration path once per status call", async () => {
95
+ const service = await Effect.runPromise(
96
+ Effect.provide(FileSystemService, FileSystemService.Default),
97
+ )
98
+ const inspected = new Map<string, number>()
99
+ const instrumented = {
100
+ ...service,
101
+ inspectFile: (path: string) => {
102
+ inspected.set(path, (inspected.get(path) ?? 0) + 1)
103
+ return service.inspectFile(path)
104
+ },
105
+ }
106
+
107
+ await runTestEffect(
108
+ IntegrationService.pipe(
109
+ Effect.flatMap((integration) => integration.statusRoot(root)),
110
+ Effect.provideService(FileSystemService, instrumented),
111
+ ),
112
+ )
113
+
114
+ expect(inspected.size).toBe(8)
115
+ expect([...inspected.values()]).toEqual(Array(8).fill(1))
116
+ })
117
+
118
+ test("inspects integration and legacy paths once per synchronized call", async () => {
119
+ const service = await Effect.runPromise(
120
+ Effect.provide(FileSystemService, FileSystemService.Default),
121
+ )
122
+ const inspected = new Map<string, number>()
123
+ const instrumented = {
124
+ ...service,
125
+ inspectFile: (path: string) => {
126
+ inspected.set(path, (inspected.get(path) ?? 0) + 1)
127
+ return service.inspectFile(path)
128
+ },
129
+ }
130
+
131
+ await runTestEffect(
132
+ IntegrationService.pipe(
133
+ Effect.flatMap((integration) => integration.syncRoot(root)),
134
+ Effect.provideService(FileSystemService, instrumented),
135
+ ),
136
+ )
137
+
138
+ expect(inspected.size).toBe(11)
139
+ expect([...inspected.values()]).toEqual(Array(11).fill(1))
140
+ })
141
+
93
142
  test("reports customized and checksum-safe drifted files", async () => {
94
143
  await write(root, ".agency/AGENTS.md", "# Custom instructions\n")
95
144
  await write(
@@ -202,35 +202,54 @@ const inspect = (root: string) =>
202
202
  "agency-repository-skills.ts",
203
203
  )
204
204
  const tuiPluginPath = join(opencodeDirectory, "tui", "agency-debug.ts")
205
+ const [
206
+ agents,
207
+ opencode,
208
+ opencodeJson,
209
+ plugin,
210
+ legacyPlugin,
211
+ tui,
212
+ tuiJson,
213
+ tuiPlugin,
214
+ ] = yield* Effect.all(
215
+ [
216
+ fs.inspectFile(agentsPath),
217
+ fs.inspectFile(opencodePath),
218
+ fs.inspectFile(opencodeJsonPath),
219
+ fs.inspectFile(pluginPath),
220
+ fs.inspectFile(legacyPluginPath),
221
+ fs.inspectFile(tuiPath),
222
+ fs.inspectFile(tuiJsonPath),
223
+ fs.inspectFile(tuiPluginPath),
224
+ ] as const,
225
+ { concurrency: 8 },
226
+ )
205
227
  const files: IntegrationFileStatus[] = []
206
228
 
207
229
  files.push(
208
- (yield* fs.readSymlinkTarget(agentsPath)) !== null
230
+ agents.kind === "symlink"
209
231
  ? fileStatus("agents", agentsPath, "customized")
210
- : (yield* fs.exists(agentsPath))
232
+ : agents.kind === "file"
211
233
  ? classify(
212
234
  "agents",
213
235
  agentsPath,
214
- yield* fs.readFile(agentsPath),
236
+ agents.content,
215
237
  managedWorkbaseAgents,
216
238
  canUpdateManagedWorkbaseAgents,
217
239
  )
218
240
  : fileStatus("agents", agentsPath, "missing"),
219
241
  )
220
242
 
221
- if ((yield* fs.readSymlinkTarget(opencodePath)) !== null) {
243
+ if (opencode.kind === "symlink") {
222
244
  files.push(fileStatus("opencode", opencodePath, "customized"))
223
- } else if (
224
- (yield* fs.readSymlinkTarget(opencodeJsonPath)) !== null ||
225
- (yield* fs.exists(opencodeJsonPath))
226
- ) {
245
+ } else if (opencodeJson.kind !== "missing") {
227
246
  files.push(fileStatus("opencode", opencodeJsonPath, "customized"))
228
- } else if (yield* fs.exists(opencodePath)) {
247
+ } else if (opencode.kind === "file") {
229
248
  files.push(
230
249
  classify(
231
250
  "opencode",
232
251
  opencodePath,
233
- yield* fs.readFile(opencodePath),
252
+ opencode.content,
234
253
  managedWorkbaseOpencode,
235
254
  canUpdateManagedWorkbaseOpencode,
236
255
  ),
@@ -239,26 +258,21 @@ const inspect = (root: string) =>
239
258
  files.push(fileStatus("opencode", opencodePath, "missing"))
240
259
  }
241
260
 
242
- if ((yield* fs.readSymlinkTarget(pluginPath)) !== null) {
261
+ if (plugin.kind === "symlink") {
243
262
  files.push(fileStatus("opencode-plugin", pluginPath, "customized"))
244
- } else if (yield* fs.exists(pluginPath)) {
263
+ } else if (plugin.kind === "file") {
245
264
  files.push(
246
265
  classify(
247
266
  "opencode-plugin",
248
267
  pluginPath,
249
- yield* fs.readFile(pluginPath),
268
+ plugin.content,
250
269
  managedWorkbaseOpencodePlugin,
251
270
  canUpdateManagedWorkbaseOpencodePlugin,
252
271
  ),
253
272
  )
254
- } else if (
255
- (yield* fs.readSymlinkTarget(legacyPluginPath)) !== null ||
256
- (yield* fs.exists(legacyPluginPath))
257
- ) {
273
+ } else if (legacyPlugin.kind !== "missing") {
258
274
  const legacyContent =
259
- (yield* fs.readSymlinkTarget(legacyPluginPath)) === null
260
- ? yield* fs.readFile(legacyPluginPath)
261
- : null
275
+ legacyPlugin.kind === "file" ? legacyPlugin.content : null
262
276
  files.push(
263
277
  legacyContent !== null &&
264
278
  canUpdateManagedWorkbaseOpencodePlugin(legacyContent)
@@ -269,19 +283,16 @@ const inspect = (root: string) =>
269
283
  files.push(fileStatus("opencode-plugin", pluginPath, "missing"))
270
284
  }
271
285
 
272
- if ((yield* fs.readSymlinkTarget(tuiPath)) !== null) {
286
+ if (tui.kind === "symlink") {
273
287
  files.push(fileStatus("opencode-tui", tuiPath, "customized"))
274
- } else if (
275
- (yield* fs.readSymlinkTarget(tuiJsonPath)) !== null ||
276
- (yield* fs.exists(tuiJsonPath))
277
- ) {
288
+ } else if (tuiJson.kind !== "missing") {
278
289
  files.push(fileStatus("opencode-tui", tuiJsonPath, "customized"))
279
- } else if (yield* fs.exists(tuiPath)) {
290
+ } else if (tui.kind === "file") {
280
291
  files.push(
281
292
  classify(
282
293
  "opencode-tui",
283
294
  tuiPath,
284
- yield* fs.readFile(tuiPath),
295
+ tui.content,
285
296
  managedWorkbaseOpencodeTui,
286
297
  canUpdateManagedWorkbaseOpencodeTui,
287
298
  ),
@@ -290,14 +301,14 @@ const inspect = (root: string) =>
290
301
  files.push(fileStatus("opencode-tui", tuiPath, "missing"))
291
302
  }
292
303
 
293
- if ((yield* fs.readSymlinkTarget(tuiPluginPath)) !== null) {
304
+ if (tuiPlugin.kind === "symlink") {
294
305
  files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "customized"))
295
- } else if (yield* fs.exists(tuiPluginPath)) {
306
+ } else if (tuiPlugin.kind === "file") {
296
307
  files.push(
297
308
  classify(
298
309
  "opencode-tui-plugin",
299
310
  tuiPluginPath,
300
- yield* fs.readFile(tuiPluginPath),
311
+ tuiPlugin.content,
301
312
  managedWorkbaseOpencodeTuiPlugin,
302
313
  canUpdateManagedWorkbaseOpencodeTuiPlugin,
303
314
  ),
@@ -306,32 +317,25 @@ const inspect = (root: string) =>
306
317
  files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "missing"))
307
318
  }
308
319
 
309
- return files
320
+ return { files, legacyPlugin }
310
321
  })
311
322
 
312
323
  const canRemoveLegacyAgents = (root: string) =>
313
324
  Effect.gen(function* () {
314
325
  const fs = yield* FileSystemService
315
326
  const path = join(root, "AGENTS.md")
316
- if (
317
- (yield* fs.readSymlinkTarget(path)) !== null ||
318
- !(yield* fs.exists(path))
319
- )
320
- return false
321
- return canUpdateManagedWorkbaseAgents(yield* fs.readFile(path))
327
+ const file = yield* fs.inspectFile(path)
328
+ return file.kind === "file" && canUpdateManagedWorkbaseAgents(file.content)
322
329
  })
323
330
 
324
331
  const canRemoveLegacyOpencodeCommand = (root: string) =>
325
332
  Effect.gen(function* () {
326
333
  const fs = yield* FileSystemService
327
334
  const path = join(root, ".opencode", "command", "agency.md")
328
- if (
329
- (yield* fs.readSymlinkTarget(path)) !== null ||
330
- !(yield* fs.exists(path))
331
- )
332
- return false
335
+ const file = yield* fs.inspectFile(path)
336
+ if (file.kind !== "file") return false
333
337
 
334
- const content = yield* fs.readFile(path)
338
+ const content = file.content
335
339
  const header = /^---\r?\n# agency-managed: sha256=([a-f0-9]{64})\r?\n/
336
340
  const match = content.match(header)
337
341
  if (!match?.[1]) return false
@@ -340,23 +344,6 @@ const canRemoveLegacyOpencodeCommand = (root: string) =>
340
344
  return createHash("sha256").update(canonical).digest("hex") === match[1]
341
345
  })
342
346
 
343
- const canRemoveLegacyOpencodePlugin = (root: string) =>
344
- Effect.gen(function* () {
345
- const fs = yield* FileSystemService
346
- const path = join(
347
- root,
348
- ".opencode",
349
- "plugin",
350
- "agency-repository-skills.ts",
351
- )
352
- if (
353
- (yield* fs.readSymlinkTarget(path)) !== null ||
354
- !(yield* fs.exists(path))
355
- )
356
- return false
357
- return canUpdateManagedWorkbaseOpencodePlugin(yield* fs.readFile(path))
358
- })
359
-
360
347
  export class IntegrationService extends Effect.Service<IntegrationService>()(
361
348
  "IntegrationService",
362
349
  {
@@ -365,34 +352,59 @@ export class IntegrationService extends Effect.Service<IntegrationService>()(
365
352
  Effect.gen(function* () {
366
353
  const workbase = yield* WorkbaseService
367
354
  const root = yield* workbase.discover(startPath)
368
- return { root, files: yield* inspect(root) }
355
+ return { root, files: (yield* inspect(root)).files }
369
356
  }),
370
357
 
358
+ statusRoot: (root: string) =>
359
+ inspect(root).pipe(Effect.map(({ files }) => ({ root, files }))),
360
+
371
361
  sync: (startPath: string = process.cwd()) =>
372
362
  Effect.gen(function* () {
373
- const fs = yield* FileSystemService
374
363
  const workbase = yield* WorkbaseService
375
364
  const root = yield* workbase.discover(startPath)
376
- const statuses = yield* inspect(root)
377
- const removeLegacyAgents =
378
- statuses.some(
379
- (status) =>
380
- status.name === "opencode" && status.state !== "customized",
381
- ) && (yield* canRemoveLegacyAgents(root))
382
- const removeLegacyOpencodeCommand =
383
- yield* canRemoveLegacyOpencodeCommand(root)
384
- const removeLegacyOpencodePlugin =
385
- yield* canRemoveLegacyOpencodePlugin(root)
365
+ const service = yield* IntegrationService
366
+ return yield* service.syncRoot(root)
367
+ }),
368
+
369
+ syncRoot: (root: string) =>
370
+ Effect.gen(function* () {
371
+ const fs = yield* FileSystemService
372
+ const { files: statuses, legacyPlugin } = yield* inspect(root)
386
373
  const legacyPiExtension = join(
387
374
  root,
388
375
  ".pi",
389
376
  "extensions",
390
377
  "agency-workbase.ts",
391
378
  )
392
- const removeLegacyPiExtension =
393
- (yield* fs.readSymlinkTarget(legacyPiExtension)) === null &&
394
- (yield* fs.exists(legacyPiExtension)) &&
395
- canRemoveLegacyPiExtension(yield* fs.readFile(legacyPiExtension))
379
+ const canRemoveAgents = statuses.some(
380
+ (status) =>
381
+ status.name === "opencode" && status.state !== "customized",
382
+ )
383
+ const [
384
+ removeLegacyAgents,
385
+ removeLegacyOpencodeCommand,
386
+ removeLegacyPiExtension,
387
+ ] = yield* Effect.all(
388
+ [
389
+ canRemoveAgents
390
+ ? canRemoveLegacyAgents(root)
391
+ : Effect.succeed(false),
392
+ canRemoveLegacyOpencodeCommand(root),
393
+ fs
394
+ .inspectFile(legacyPiExtension)
395
+ .pipe(
396
+ Effect.map(
397
+ (file) =>
398
+ file.kind === "file" &&
399
+ canRemoveLegacyPiExtension(file.content),
400
+ ),
401
+ ),
402
+ ] as const,
403
+ { concurrency: 3 },
404
+ )
405
+ const removeLegacyOpencodePlugin =
406
+ legacyPlugin.kind === "file" &&
407
+ canUpdateManagedWorkbaseOpencodePlugin(legacyPlugin.content)
396
408
  const files: IntegrationSyncFile[] = []
397
409
 
398
410
  for (const status of statuses) {
@@ -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({}),