@markjaquith/agency 2.71.18 → 2.71.20
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 +3 -1
- package/src/commands/init.ts +1 -1
- package/src/commands/work.test.ts +12 -0
- package/src/commands/work.ts +1 -1
- package/src/services/FileSystemService.ts +32 -0
- package/src/services/GraphService.test.ts +60 -1
- package/src/services/GraphService.ts +39 -12
- package/src/services/IntegrationService.test.ts +49 -0
- package/src/services/IntegrationService.ts +88 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markjaquith/agency",
|
|
3
|
-
"version": "2.71.
|
|
3
|
+
"version": "2.71.20",
|
|
4
4
|
"description": "Manage agentic work across repositories with durable workbases",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"benchmark:status": "bun scripts/benchmark-status.ts",
|
|
71
71
|
"benchmark:next": "bun scripts/benchmark-next.ts",
|
|
72
72
|
"benchmark:init": "bun scripts/benchmark-init.ts",
|
|
73
|
+
"benchmark:integration": "bun scripts/benchmark-integration.ts",
|
|
73
74
|
"benchmark:workbase": "bun scripts/benchmark-workbase.ts",
|
|
74
75
|
"benchmark:doctor": "bun scripts/benchmark-doctor.ts",
|
|
75
76
|
"benchmark:context": "bun scripts/benchmark-context.ts",
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"benchmark:finish": "bun scripts/benchmark-finish.ts",
|
|
78
79
|
"benchmark:epic": "bun scripts/benchmark-epic.ts",
|
|
79
80
|
"benchmark:phase": "bun scripts/benchmark-phase.ts",
|
|
81
|
+
"benchmark:graph": "bun scripts/benchmark-graph.ts",
|
|
80
82
|
"benchmark:push": "bun scripts/benchmark-push.ts",
|
|
81
83
|
"benchmark:archive": "bun scripts/benchmark-archive.ts",
|
|
82
84
|
"benchmark:sync": "bun scripts/benchmark-sync.ts",
|
package/src/commands/init.ts
CHANGED
|
@@ -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.
|
|
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) =>
|
package/src/commands/work.ts
CHANGED
|
@@ -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.
|
|
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),
|
|
@@ -6,7 +6,10 @@ import { dirname, join } from "node:path"
|
|
|
6
6
|
import { AgencyGraph } from "../graph-schema"
|
|
7
7
|
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
8
8
|
import { GraphService } from "./GraphService"
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
VersionControlService,
|
|
11
|
+
type VersionControlBackend,
|
|
12
|
+
} from "./VersionControlService"
|
|
10
13
|
|
|
11
14
|
const write = async (root: string, path: string, content: string) => {
|
|
12
15
|
const fullPath = join(root, path)
|
|
@@ -364,6 +367,15 @@ status: open
|
|
|
364
367
|
|
|
365
368
|
const detailed = await getGraph(root, {
|
|
366
369
|
include: ["bodies", "workspace", "git", "pr"],
|
|
370
|
+
backend: {
|
|
371
|
+
kind: "git",
|
|
372
|
+
inspectRepository: () => Effect.succeed(null),
|
|
373
|
+
listWorkspaces: () => Effect.succeed([]),
|
|
374
|
+
resolveRevision: () => Effect.succeed(null),
|
|
375
|
+
workspaceHead: () => Effect.succeed(null),
|
|
376
|
+
workspaceDirty: () => Effect.succeed(null),
|
|
377
|
+
remoteUrl: () => Effect.succeed(null),
|
|
378
|
+
} as unknown as VersionControlBackend,
|
|
367
379
|
})
|
|
368
380
|
expect(detailed.includes).toEqual(["bodies", "git", "pr", "workspace"])
|
|
369
381
|
expect(detailed.workbase.root).toBe(root)
|
|
@@ -386,4 +398,51 @@ status: open
|
|
|
386
398
|
expect(execution?.pr).toEqual({ url: null, state: "none" })
|
|
387
399
|
expect(Schema.decodeUnknownSync(AgencyGraph)(detailed)).toEqual(detailed)
|
|
388
400
|
})
|
|
401
|
+
|
|
402
|
+
test("reuses repository metadata and revision lookups for git details", async () => {
|
|
403
|
+
const root = await createWorkbase()
|
|
404
|
+
roots.push(root)
|
|
405
|
+
const calls = {
|
|
406
|
+
listWorkspaces: 0,
|
|
407
|
+
remoteUrl: 0,
|
|
408
|
+
resolveRevision: new Map<string, number>(),
|
|
409
|
+
}
|
|
410
|
+
const backend = {
|
|
411
|
+
kind: "git",
|
|
412
|
+
inspectRepository: () => Effect.succeed(null),
|
|
413
|
+
listWorkspaces: () =>
|
|
414
|
+
Effect.sync(() => {
|
|
415
|
+
calls.listWorkspaces += 1
|
|
416
|
+
return []
|
|
417
|
+
}),
|
|
418
|
+
remoteUrl: () =>
|
|
419
|
+
Effect.sync(() => {
|
|
420
|
+
calls.remoteUrl += 1
|
|
421
|
+
return null
|
|
422
|
+
}),
|
|
423
|
+
resolveRevision: (_path: string, revision: string) =>
|
|
424
|
+
Effect.sync(() => {
|
|
425
|
+
calls.resolveRevision.set(
|
|
426
|
+
revision,
|
|
427
|
+
(calls.resolveRevision.get(revision) ?? 0) + 1,
|
|
428
|
+
)
|
|
429
|
+
return revision
|
|
430
|
+
}),
|
|
431
|
+
workspaceHead: () => Effect.succeed(null),
|
|
432
|
+
workspaceDirty: () => Effect.succeed(null),
|
|
433
|
+
} as unknown as VersionControlBackend
|
|
434
|
+
|
|
435
|
+
await getGraph(root, { include: ["git"], backend })
|
|
436
|
+
|
|
437
|
+
expect(calls.listWorkspaces).toBe(0)
|
|
438
|
+
expect(calls.remoteUrl).toBe(1)
|
|
439
|
+
expect(calls.resolveRevision).toEqual(
|
|
440
|
+
new Map([
|
|
441
|
+
["feat/prepare", 1],
|
|
442
|
+
["feat/implement", 1],
|
|
443
|
+
["feat/verify", 1],
|
|
444
|
+
["main", 1],
|
|
445
|
+
]),
|
|
446
|
+
)
|
|
447
|
+
})
|
|
389
448
|
})
|
|
@@ -674,6 +674,40 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
674
674
|
}
|
|
675
675
|
const dependents = (id: string) =>
|
|
676
676
|
[...(reverseDependencies.get(id) ?? [])].sort()
|
|
677
|
+
const repositoryWorkspaces = new Map<
|
|
678
|
+
string,
|
|
679
|
+
ReturnType<VersionControlBackend["listWorkspaces"]>
|
|
680
|
+
>()
|
|
681
|
+
const listWorkspaces = (path: string) => {
|
|
682
|
+
if (!backend) return Effect.succeed([])
|
|
683
|
+
const cached = repositoryWorkspaces.get(path)
|
|
684
|
+
if (cached) return cached
|
|
685
|
+
const workspaces = backend
|
|
686
|
+
.listWorkspaces(path)
|
|
687
|
+
.pipe(Effect.catchAll(() => Effect.succeed([])))
|
|
688
|
+
repositoryWorkspaces.set(path, workspaces)
|
|
689
|
+
return workspaces
|
|
690
|
+
}
|
|
691
|
+
const repositoryRemotes = new Map<string, string | null>()
|
|
692
|
+
const remoteUrl = (path: string, remote: string) =>
|
|
693
|
+
Effect.gen(function* () {
|
|
694
|
+
if (!backend) return null
|
|
695
|
+
const key = `${path}\u0000${remote}`
|
|
696
|
+
if (repositoryRemotes.has(key)) return repositoryRemotes.get(key)!
|
|
697
|
+
const url = yield* backend.remoteUrl(path, remote)
|
|
698
|
+
repositoryRemotes.set(key, url)
|
|
699
|
+
return url
|
|
700
|
+
})
|
|
701
|
+
const resolvedRevisions = new Map<string, string | null>()
|
|
702
|
+
const resolveRevision = (path: string, revision: string) =>
|
|
703
|
+
Effect.gen(function* () {
|
|
704
|
+
if (!backend) return null
|
|
705
|
+
const key = `${path}\u0000${revision}`
|
|
706
|
+
if (resolvedRevisions.has(key)) return resolvedRevisions.get(key)!
|
|
707
|
+
const resolved = yield* backend.resolveRevision(path, revision)
|
|
708
|
+
resolvedRevisions.set(key, resolved)
|
|
709
|
+
return resolved
|
|
710
|
+
})
|
|
677
711
|
|
|
678
712
|
const documentDetails = <T extends Record<string, unknown>>(
|
|
679
713
|
document: Document<T>,
|
|
@@ -697,11 +731,7 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
697
731
|
Effect.gen(function* () {
|
|
698
732
|
if (!backend) return undefined
|
|
699
733
|
const inspection = yield* backend.inspectRepository(path)
|
|
700
|
-
const workspaces = inspection
|
|
701
|
-
? yield* backend
|
|
702
|
-
.listWorkspaces(path)
|
|
703
|
-
.pipe(Effect.catchAll(() => Effect.succeed([])))
|
|
704
|
-
: []
|
|
734
|
+
const workspaces = inspection ? yield* listWorkspaces(path) : []
|
|
705
735
|
const primary = workspaces.find(
|
|
706
736
|
(workspace) => workspace.path === path,
|
|
707
737
|
)
|
|
@@ -734,15 +764,12 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
734
764
|
}
|
|
735
765
|
if (include.has("git")) {
|
|
736
766
|
if (!backend) return result
|
|
737
|
-
const remote = yield*
|
|
738
|
-
repositoryPath,
|
|
739
|
-
"origin",
|
|
740
|
-
)
|
|
767
|
+
const remote = yield* remoteUrl(repositoryPath, "origin")
|
|
741
768
|
const canonicalCheckoutPath = materialized
|
|
742
769
|
? yield* fs.realPath(checkoutPath)
|
|
743
770
|
: checkoutPath
|
|
744
771
|
const actualBranch = materialized
|
|
745
|
-
? yield*
|
|
772
|
+
? yield* listWorkspaces(repositoryPath).pipe(
|
|
746
773
|
Effect.map(
|
|
747
774
|
(workspaces) =>
|
|
748
775
|
workspaces
|
|
@@ -783,11 +810,11 @@ export class GraphService extends Effect.Service<GraphService>()(
|
|
|
783
810
|
: {
|
|
784
811
|
branch: data.branch,
|
|
785
812
|
base: data.base,
|
|
786
|
-
branchCommit: yield*
|
|
813
|
+
branchCommit: yield* resolveRevision(
|
|
787
814
|
repositoryPath,
|
|
788
815
|
data.branch,
|
|
789
816
|
),
|
|
790
|
-
baseCommit: yield*
|
|
817
|
+
baseCommit: yield* resolveRevision(
|
|
791
818
|
repositoryPath,
|
|
792
819
|
data.base,
|
|
793
820
|
),
|
|
@@ -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
|
-
|
|
230
|
+
agents.kind === "symlink"
|
|
209
231
|
? fileStatus("agents", agentsPath, "customized")
|
|
210
|
-
:
|
|
232
|
+
: agents.kind === "file"
|
|
211
233
|
? classify(
|
|
212
234
|
"agents",
|
|
213
235
|
agentsPath,
|
|
214
|
-
|
|
236
|
+
agents.content,
|
|
215
237
|
managedWorkbaseAgents,
|
|
216
238
|
canUpdateManagedWorkbaseAgents,
|
|
217
239
|
)
|
|
218
240
|
: fileStatus("agents", agentsPath, "missing"),
|
|
219
241
|
)
|
|
220
242
|
|
|
221
|
-
if (
|
|
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 (
|
|
247
|
+
} else if (opencode.kind === "file") {
|
|
229
248
|
files.push(
|
|
230
249
|
classify(
|
|
231
250
|
"opencode",
|
|
232
251
|
opencodePath,
|
|
233
|
-
|
|
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 (
|
|
261
|
+
if (plugin.kind === "symlink") {
|
|
243
262
|
files.push(fileStatus("opencode-plugin", pluginPath, "customized"))
|
|
244
|
-
} else if (
|
|
263
|
+
} else if (plugin.kind === "file") {
|
|
245
264
|
files.push(
|
|
246
265
|
classify(
|
|
247
266
|
"opencode-plugin",
|
|
248
267
|
pluginPath,
|
|
249
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
290
|
+
} else if (tui.kind === "file") {
|
|
280
291
|
files.push(
|
|
281
292
|
classify(
|
|
282
293
|
"opencode-tui",
|
|
283
294
|
tuiPath,
|
|
284
|
-
|
|
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 (
|
|
304
|
+
if (tuiPlugin.kind === "symlink") {
|
|
294
305
|
files.push(fileStatus("opencode-tui-plugin", tuiPluginPath, "customized"))
|
|
295
|
-
} else if (
|
|
306
|
+
} else if (tuiPlugin.kind === "file") {
|
|
296
307
|
files.push(
|
|
297
308
|
classify(
|
|
298
309
|
"opencode-tui-plugin",
|
|
299
310
|
tuiPluginPath,
|
|
300
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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 =
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
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
|
|
393
|
-
(
|
|
394
|
-
|
|
395
|
-
|
|
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) {
|