@markjaquith/agency 1.11.0 → 2.0.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 +302 -113
- package/cli.ts +123 -415
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +258 -0
- package/src/cli.test.ts +290 -0
- package/src/commands/description.test.ts +103 -0
- package/src/commands/epic.test.ts +103 -0
- package/src/commands/epic.ts +101 -0
- package/src/commands/init.test.ts +42 -290
- package/src/commands/init.ts +15 -123
- package/src/commands/phase.ts +120 -0
- package/src/commands/pr.test.ts +19 -170
- package/src/commands/pr.ts +23 -179
- package/src/commands/repo.test.ts +80 -0
- package/src/commands/repo.ts +83 -0
- package/src/commands/status.test.ts +35 -477
- package/src/commands/status.ts +24 -213
- package/src/commands/task-phase.test.ts +206 -0
- package/src/commands/task.ts +83 -1285
- package/src/commands/validate.test.ts +91 -0
- package/src/commands/validate.ts +50 -0
- package/src/commands/work.test.ts +172 -419
- package/src/commands/work.ts +47 -166
- package/src/services/EpicService.test.ts +71 -0
- package/src/services/EpicService.ts +135 -0
- package/src/services/FileSystemService.ts +61 -92
- package/src/services/PhaseService.ts +323 -0
- package/src/services/PullRequestService.test.ts +386 -0
- package/src/services/PullRequestService.ts +143 -0
- package/src/services/RepositoryService.test.ts +106 -0
- package/src/services/RepositoryService.ts +159 -0
- package/src/services/TaskPhaseService.test.ts +217 -0
- package/src/services/TaskService.ts +192 -0
- package/src/services/WorkbaseService.test.ts +446 -0
- package/src/services/WorkbaseService.ts +493 -0
- package/src/services/WorktreeService.test.ts +599 -0
- package/src/services/WorktreeService.ts +357 -0
- package/src/test-utils.ts +41 -438
- package/src/types.ts +1 -215
- package/src/utils/command.ts +1 -5
- package/src/utils/effect.test.ts +24 -0
- package/src/utils/effect.ts +3 -273
- package/src/utils/process.test.ts +0 -15
- package/src/utils/process.ts +1 -60
- package/src/workbase/frontmatter.test.ts +50 -0
- package/src/workbase/frontmatter.ts +77 -0
- package/src/workbase/repository-reference.test.ts +25 -0
- package/src/workbase/repository-reference.ts +18 -0
- package/src/workbase/schemas.test.ts +146 -0
- package/src/workbase/schemas.ts +78 -0
- package/src/workbase/worktree-command.test.ts +61 -0
- package/src/workbase/worktree-command.ts +52 -0
- package/src/commands/base.test.ts +0 -198
- package/src/commands/base.ts +0 -198
- package/src/commands/clean.test.ts +0 -299
- package/src/commands/clean.ts +0 -274
- package/src/commands/completions.test.ts +0 -51
- package/src/commands/completions.ts +0 -308
- package/src/commands/emit.integration.test.ts +0 -549
- package/src/commands/emit.test.ts +0 -576
- package/src/commands/emit.ts +0 -700
- package/src/commands/emitted.test.ts +0 -226
- package/src/commands/emitted.ts +0 -57
- package/src/commands/loop.test.ts +0 -621
- package/src/commands/loop.ts +0 -406
- package/src/commands/merge.integration.test.ts +0 -195
- package/src/commands/merge.test.ts +0 -246
- package/src/commands/merge.ts +0 -244
- package/src/commands/pull.test.ts +0 -395
- package/src/commands/pull.ts +0 -205
- package/src/commands/push.test.ts +0 -518
- package/src/commands/push.ts +0 -434
- package/src/commands/rebase.test.ts +0 -521
- package/src/commands/rebase.ts +0 -232
- package/src/commands/save.test.ts +0 -247
- package/src/commands/save.ts +0 -162
- package/src/commands/source.test.ts +0 -195
- package/src/commands/source.ts +0 -72
- package/src/commands/switch.test.ts +0 -194
- package/src/commands/switch.ts +0 -84
- package/src/commands/task-branching.test.ts +0 -633
- package/src/commands/task-continue.test.ts +0 -311
- package/src/commands/task-edit.test.ts +0 -235
- package/src/commands/task-main.test.ts +0 -1066
- package/src/commands/task-squash.test.ts +0 -266
- package/src/commands/tasks.test.ts +0 -391
- package/src/commands/tasks.ts +0 -161
- package/src/commands/template-delete.test.ts +0 -178
- package/src/commands/template-delete.ts +0 -98
- package/src/commands/template-list.test.ts +0 -135
- package/src/commands/template-list.ts +0 -87
- package/src/commands/template-view.test.ts +0 -158
- package/src/commands/template-view.ts +0 -86
- package/src/commands/template.test.ts +0 -32
- package/src/commands/template.ts +0 -96
- package/src/commands/use.test.ts +0 -87
- package/src/commands/use.ts +0 -97
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -17
- package/src/schemas.ts +0 -33
- package/src/services/AgencyMetadataService.ts +0 -311
- package/src/services/ClaudeService.test.ts +0 -184
- package/src/services/ClaudeService.ts +0 -91
- package/src/services/ConfigService.ts +0 -115
- package/src/services/FilterRepoService.ts +0 -164
- package/src/services/FormatterService.test.ts +0 -432
- package/src/services/FormatterService.ts +0 -219
- package/src/services/GitService.ts +0 -1183
- package/src/services/MockFilterRepoService.ts +0 -140
- package/src/services/OpencodeService.ts +0 -263
- package/src/services/PromptService.ts +0 -183
- package/src/services/TemplateService.ts +0 -81
- package/src/types/native-exec.d.ts +0 -8
- package/src/utils/colors.ts +0 -178
- package/src/utils/git-path.ts +0 -6
- package/src/utils/glob.test.ts +0 -161
- package/src/utils/glob.ts +0 -78
- package/src/utils/paths.ts +0 -51
- package/src/utils/pr-branch.test.ts +0 -376
- package/src/utils/pr-branch.ts +0 -508
- package/src/utils/spinner.test.ts +0 -45
- package/src/utils/spinner.ts +0 -96
- package/src/utils/task-parser.test.ts +0 -172
- package/src/utils/task-parser.ts +0 -93
- package/templates/AGENCY.md +0 -22
- package/templates/AGENTS.md +0 -11
- package/templates/CLAUDE.md +0 -3
- package/templates/TASK.md +0 -5
- package/templates/opencode.json +0 -4
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
5
|
+
import { epic } from "./epic"
|
|
6
|
+
import { phase } from "./phase"
|
|
7
|
+
import { task } from "./task"
|
|
8
|
+
|
|
9
|
+
describe("create command descriptions", () => {
|
|
10
|
+
let root: string
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
root = await createTempDir()
|
|
14
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
15
|
+
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(async () => cleanupTempDir(root))
|
|
19
|
+
|
|
20
|
+
test("persists and exposes descriptions for every body-of-work shape", async () => {
|
|
21
|
+
await runTestEffect(
|
|
22
|
+
epic({
|
|
23
|
+
subcommand: "create",
|
|
24
|
+
args: ["rollout"],
|
|
25
|
+
ticketUrl: "https://example.com/epic",
|
|
26
|
+
description: "Coordinate the rollout.",
|
|
27
|
+
repos: ["agency:main"],
|
|
28
|
+
cwd: root,
|
|
29
|
+
silent: true,
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
await runTestEffect(
|
|
33
|
+
task({
|
|
34
|
+
subcommand: "create",
|
|
35
|
+
args: ["delivery"],
|
|
36
|
+
ticketUrl: "https://example.com/task",
|
|
37
|
+
description: "Deliver the rollout.",
|
|
38
|
+
multiPhase: true,
|
|
39
|
+
cwd: root,
|
|
40
|
+
silent: true,
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
await runTestEffect(
|
|
44
|
+
phase({
|
|
45
|
+
subcommand: "create",
|
|
46
|
+
args: ["delivery", "contract"],
|
|
47
|
+
description: "Introduce the service contract.",
|
|
48
|
+
repo: "agency",
|
|
49
|
+
branch: "task/contract",
|
|
50
|
+
base: "main",
|
|
51
|
+
cwd: root,
|
|
52
|
+
silent: true,
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
await Bun.file(join(root, "epics/rollout/EPIC.md")).text(),
|
|
58
|
+
).toContain("description: Coordinate the rollout.")
|
|
59
|
+
expect(
|
|
60
|
+
await Bun.file(join(root, "tasks/delivery/TASK.md")).text(),
|
|
61
|
+
).toContain("description: Deliver the rollout.")
|
|
62
|
+
expect(
|
|
63
|
+
await Bun.file(
|
|
64
|
+
join(root, "tasks/delivery/phases/contract/PHASE.md"),
|
|
65
|
+
).text(),
|
|
66
|
+
).toContain("description: Introduce the service contract.")
|
|
67
|
+
|
|
68
|
+
const output: string[] = []
|
|
69
|
+
const originalLog = console.log
|
|
70
|
+
console.log = (message?: unknown) => output.push(String(message))
|
|
71
|
+
try {
|
|
72
|
+
await runTestEffect(
|
|
73
|
+
epic({ subcommand: "list", args: [], cwd: root, json: true }),
|
|
74
|
+
)
|
|
75
|
+
await runTestEffect(
|
|
76
|
+
task({
|
|
77
|
+
subcommand: "show",
|
|
78
|
+
args: ["delivery"],
|
|
79
|
+
cwd: root,
|
|
80
|
+
json: true,
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
await runTestEffect(
|
|
84
|
+
phase({
|
|
85
|
+
subcommand: "show",
|
|
86
|
+
args: ["delivery", "contract"],
|
|
87
|
+
cwd: root,
|
|
88
|
+
json: true,
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
} finally {
|
|
92
|
+
console.log = originalLog
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
expect(JSON.parse(output[0]!)[0].data.description).toBe(
|
|
96
|
+
"Coordinate the rollout.",
|
|
97
|
+
)
|
|
98
|
+
expect(JSON.parse(output[1]!).data.description).toBe("Deliver the rollout.")
|
|
99
|
+
expect(JSON.parse(output[2]!).data.description).toBe(
|
|
100
|
+
"Introduce the service contract.",
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdir } from "node:fs/promises"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import {
|
|
5
|
+
captureLogs,
|
|
6
|
+
cleanupTempDir,
|
|
7
|
+
createTempDir,
|
|
8
|
+
runTestEffect,
|
|
9
|
+
} from "../test-utils"
|
|
10
|
+
import { epic } from "./epic"
|
|
11
|
+
|
|
12
|
+
describe("epic command", () => {
|
|
13
|
+
let root: string
|
|
14
|
+
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
root = await createTempDir()
|
|
17
|
+
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
18
|
+
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(async () => cleanupTempDir(root))
|
|
22
|
+
|
|
23
|
+
test("requires create arguments", async () => {
|
|
24
|
+
await expect(
|
|
25
|
+
runTestEffect(epic({ subcommand: "create", args: [], silent: true })),
|
|
26
|
+
).rejects.toThrow("Usage: agency epic create")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test("outputs the created epic as JSON", async () => {
|
|
30
|
+
const logs = await captureLogs(() =>
|
|
31
|
+
runTestEffect(
|
|
32
|
+
epic({
|
|
33
|
+
subcommand: "create",
|
|
34
|
+
args: ["example"],
|
|
35
|
+
ticketUrl: "https://example.com/epic",
|
|
36
|
+
repos: ["agency:main"],
|
|
37
|
+
cwd: root,
|
|
38
|
+
json: true,
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
expect(JSON.parse(logs[0]!)).toEqual({
|
|
44
|
+
id: "example",
|
|
45
|
+
path: join(root, "epics/example/EPIC.md"),
|
|
46
|
+
data: {
|
|
47
|
+
ticketUrl: "https://example.com/epic",
|
|
48
|
+
repos: [{ repo: "agency", ref: "main" }],
|
|
49
|
+
tasks: [],
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("lists and shows stable epic JSON records", async () => {
|
|
55
|
+
await runTestEffect(
|
|
56
|
+
epic({
|
|
57
|
+
subcommand: "create",
|
|
58
|
+
args: ["example"],
|
|
59
|
+
ticketUrl: "https://example.com/epic",
|
|
60
|
+
repos: ["agency:main"],
|
|
61
|
+
cwd: root,
|
|
62
|
+
silent: true,
|
|
63
|
+
}),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const listLogs = await captureLogs(() =>
|
|
67
|
+
runTestEffect(
|
|
68
|
+
epic({ subcommand: "list", args: [], cwd: root, json: true }),
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
expect(JSON.parse(listLogs[0]!)).toEqual([
|
|
72
|
+
{
|
|
73
|
+
id: "example",
|
|
74
|
+
path: join(root, "epics/example/EPIC.md"),
|
|
75
|
+
data: {
|
|
76
|
+
ticketUrl: "https://example.com/epic",
|
|
77
|
+
repos: [{ repo: "agency", ref: "main" }],
|
|
78
|
+
tasks: [],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
const showLogs = await captureLogs(() =>
|
|
84
|
+
runTestEffect(
|
|
85
|
+
epic({
|
|
86
|
+
subcommand: "show",
|
|
87
|
+
args: ["example"],
|
|
88
|
+
cwd: root,
|
|
89
|
+
json: true,
|
|
90
|
+
}),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
expect(JSON.parse(showLogs[0]!)).toEqual({
|
|
94
|
+
id: "example",
|
|
95
|
+
path: join(root, "epics/example/EPIC.md"),
|
|
96
|
+
data: {
|
|
97
|
+
ticketUrl: "https://example.com/epic",
|
|
98
|
+
repos: [{ repo: "agency", ref: "main" }],
|
|
99
|
+
tasks: [],
|
|
100
|
+
},
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
3
|
+
import { EpicService } from "../services/EpicService"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
6
|
+
|
|
7
|
+
interface EpicOptions extends BaseCommandOptions {
|
|
8
|
+
readonly subcommand?: string
|
|
9
|
+
readonly args: readonly string[]
|
|
10
|
+
readonly ticketUrl?: string
|
|
11
|
+
readonly description?: string
|
|
12
|
+
readonly repos?: readonly string[]
|
|
13
|
+
readonly json?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const epic = (options: EpicOptions) =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const epics = yield* EpicService
|
|
19
|
+
const { log } = createLoggers(options)
|
|
20
|
+
const cwd = options.cwd ?? process.cwd()
|
|
21
|
+
|
|
22
|
+
switch (options.subcommand) {
|
|
23
|
+
case "create": {
|
|
24
|
+
const id = options.args[0]
|
|
25
|
+
if (!id || !options.ticketUrl || !options.repos?.length) {
|
|
26
|
+
return yield* Effect.fail(
|
|
27
|
+
new Error(
|
|
28
|
+
"Usage: agency epic create <id> --ticket-url <url> --repo <alias>:<ref>",
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
const record = yield* epics.create(
|
|
33
|
+
id,
|
|
34
|
+
options.ticketUrl,
|
|
35
|
+
parseRepositoryReferences(options.repos),
|
|
36
|
+
cwd,
|
|
37
|
+
options.description,
|
|
38
|
+
)
|
|
39
|
+
const { content: _, ...output } = record
|
|
40
|
+
log(
|
|
41
|
+
options.json
|
|
42
|
+
? JSON.stringify(output, null, 2)
|
|
43
|
+
: `Created epic '${record.id}'`,
|
|
44
|
+
)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "list": {
|
|
49
|
+
const records = yield* epics.list(cwd)
|
|
50
|
+
if (options.json) {
|
|
51
|
+
log(
|
|
52
|
+
JSON.stringify(
|
|
53
|
+
records.map(({ content: _, ...record }) => record),
|
|
54
|
+
null,
|
|
55
|
+
2,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
} else {
|
|
59
|
+
for (const record of records) log(record.id)
|
|
60
|
+
}
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "show": {
|
|
65
|
+
const id = options.args[0]
|
|
66
|
+
if (!id) return yield* Effect.fail(new Error("Epic ID is required"))
|
|
67
|
+
const record = yield* epics.show(id, cwd)
|
|
68
|
+
const { content: _, ...output } = record
|
|
69
|
+
log(
|
|
70
|
+
options.json
|
|
71
|
+
? JSON.stringify(output, null, 2)
|
|
72
|
+
: record.content.trimEnd(),
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
default:
|
|
78
|
+
return yield* Effect.fail(
|
|
79
|
+
new Error(
|
|
80
|
+
"Subcommand is required. Available subcommands: create, list, show",
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
export const help = `
|
|
87
|
+
Usage: agency epic <subcommand>
|
|
88
|
+
|
|
89
|
+
Subcommands:
|
|
90
|
+
create <id> Create an epic
|
|
91
|
+
list List epics
|
|
92
|
+
show <id> Show an epic
|
|
93
|
+
|
|
94
|
+
Create options:
|
|
95
|
+
--ticket-url <url> External ticket URL
|
|
96
|
+
--description <text> Short description of the epic
|
|
97
|
+
--repo <alias>:<ref> Read-only repository reference; repeatable
|
|
98
|
+
|
|
99
|
+
Options:
|
|
100
|
+
--json Output results as JSON
|
|
101
|
+
`
|
|
@@ -1,311 +1,63 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { stat } from "node:fs/promises"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import {
|
|
5
|
+
captureLogs,
|
|
6
|
+
cleanupTempDir,
|
|
7
|
+
createTempDir,
|
|
8
|
+
runTestEffect,
|
|
9
|
+
} from "../test-utils"
|
|
5
10
|
import { init } from "./init"
|
|
6
|
-
import { initGitRepo, getGitConfig, runTestEffect } from "../test-utils"
|
|
7
11
|
|
|
8
12
|
describe("init command", () => {
|
|
9
|
-
|
|
10
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
11
|
-
try {
|
|
12
|
-
await initGitRepo(tempDir)
|
|
13
|
-
await runTestEffect(
|
|
14
|
-
init({ template: "test-template", silent: true, cwd: tempDir }),
|
|
15
|
-
)
|
|
16
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe(
|
|
17
|
-
"test-template",
|
|
18
|
-
)
|
|
19
|
-
} finally {
|
|
20
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test("saves template name to git config", async () => {
|
|
25
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
26
|
-
try {
|
|
27
|
-
await initGitRepo(tempDir)
|
|
28
|
-
await runTestEffect(
|
|
29
|
-
init({ template: "my-template", silent: true, cwd: tempDir }),
|
|
30
|
-
)
|
|
31
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe("my-template")
|
|
32
|
-
} finally {
|
|
33
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
34
|
-
}
|
|
35
|
-
})
|
|
13
|
+
let parent: string
|
|
36
14
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
await initGitRepo(tempDir)
|
|
41
|
-
await runTestEffect(
|
|
42
|
-
init({ template: "my-work-template", silent: true, cwd: tempDir }),
|
|
43
|
-
)
|
|
44
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe(
|
|
45
|
-
"my-work-template",
|
|
46
|
-
)
|
|
47
|
-
} finally {
|
|
48
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
49
|
-
}
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
parent = await createTempDir()
|
|
50
17
|
})
|
|
51
18
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
await initGitRepo(tempDir)
|
|
56
|
-
await runTestEffect(
|
|
57
|
-
init({ template: "my_work_template", silent: true, cwd: tempDir }),
|
|
58
|
-
)
|
|
59
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe(
|
|
60
|
-
"my_work_template",
|
|
61
|
-
)
|
|
62
|
-
} finally {
|
|
63
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test("throws error when not in git repository", async () => {
|
|
68
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
69
|
-
try {
|
|
70
|
-
await expect(
|
|
71
|
-
runTestEffect(init({ template: "test", silent: true, cwd: tempDir })),
|
|
72
|
-
).rejects.toThrow("Not in a git repository")
|
|
73
|
-
} finally {
|
|
74
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
75
|
-
}
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await cleanupTempDir(parent)
|
|
76
21
|
})
|
|
77
22
|
|
|
78
|
-
test("
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
await initGitRepo(tempDir)
|
|
82
|
-
await runTestEffect(
|
|
83
|
-
init({ template: "first-template", silent: true, cwd: tempDir }),
|
|
84
|
-
)
|
|
85
|
-
await expect(
|
|
86
|
-
runTestEffect(init({ silent: true, cwd: tempDir })),
|
|
87
|
-
).rejects.toThrow("Already initialized")
|
|
88
|
-
} finally {
|
|
89
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
90
|
-
}
|
|
91
|
-
})
|
|
23
|
+
test("creates a workbase and required directories", async () => {
|
|
24
|
+
const root = join(parent, "workbase")
|
|
25
|
+
await runTestEffect(init({ path: root, silent: true }))
|
|
92
26
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
await
|
|
98
|
-
init({ template: "first-template", silent: true, cwd: tempDir }),
|
|
99
|
-
)
|
|
100
|
-
await runTestEffect(
|
|
101
|
-
init({ template: "second-template", silent: true, cwd: tempDir }),
|
|
102
|
-
)
|
|
103
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe(
|
|
104
|
-
"second-template",
|
|
105
|
-
)
|
|
106
|
-
} finally {
|
|
107
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
27
|
+
expect(await Bun.file(join(root, "agency.json")).json()).toEqual({
|
|
28
|
+
version: 2,
|
|
29
|
+
})
|
|
30
|
+
for (const directory of ["repos", "epics", "tasks"]) {
|
|
31
|
+
expect((await stat(join(root, directory))).isDirectory()).toBe(true)
|
|
108
32
|
}
|
|
33
|
+
expect(await Bun.file(join(root, ".gitignore")).text()).toBe(
|
|
34
|
+
"/repos/\n/tasks/*/code/\n/tasks/*/phases/*/code/\n",
|
|
35
|
+
)
|
|
109
36
|
})
|
|
110
37
|
|
|
111
|
-
test("
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
await initGitRepo(tempDir)
|
|
115
|
-
await expect(
|
|
116
|
-
runTestEffect(init({ silent: true, cwd: tempDir })),
|
|
117
|
-
).rejects.toThrow("Template name required")
|
|
118
|
-
} finally {
|
|
119
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
120
|
-
}
|
|
121
|
-
})
|
|
38
|
+
test("preserves existing gitignore entries", async () => {
|
|
39
|
+
await Bun.write(join(parent, ".gitignore"), "custom\n")
|
|
40
|
+
await runTestEffect(init({ path: parent, silent: true }))
|
|
122
41
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await initGitRepo(tempDir)
|
|
127
|
-
await runTestEffect(
|
|
128
|
-
init({ template: "nonexistent", silent: true, cwd: tempDir }),
|
|
129
|
-
)
|
|
130
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe("nonexistent")
|
|
131
|
-
} finally {
|
|
132
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
133
|
-
}
|
|
42
|
+
expect(await Bun.file(join(parent, ".gitignore")).text()).toStartWith(
|
|
43
|
+
"custom\n",
|
|
44
|
+
)
|
|
134
45
|
})
|
|
135
46
|
|
|
136
|
-
test("
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
init({ template: "silent-test", silent: true, cwd: tempDir }),
|
|
142
|
-
)
|
|
143
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe("silent-test")
|
|
144
|
-
} finally {
|
|
145
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
146
|
-
}
|
|
147
|
-
})
|
|
47
|
+
test("outputs the initialized workbase as JSON", async () => {
|
|
48
|
+
const root = join(parent, "json-workbase")
|
|
49
|
+
const logs = await captureLogs(() =>
|
|
50
|
+
runTestEffect(init({ path: root, json: true })),
|
|
51
|
+
)
|
|
148
52
|
|
|
149
|
-
|
|
150
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
151
|
-
try {
|
|
152
|
-
await initGitRepo(tempDir)
|
|
153
|
-
await expect(
|
|
154
|
-
runTestEffect(init({ silent: true, cwd: tempDir })),
|
|
155
|
-
).rejects.toThrow("Template name required")
|
|
156
|
-
} finally {
|
|
157
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
158
|
-
}
|
|
53
|
+
expect(JSON.parse(logs[0]!)).toEqual({ root })
|
|
159
54
|
})
|
|
160
55
|
|
|
161
|
-
test("
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
await initGitRepo(tempDir)
|
|
165
|
-
const originalLog = console.log
|
|
166
|
-
console.log = () => {}
|
|
167
|
-
try {
|
|
168
|
-
await runTestEffect(
|
|
169
|
-
init({
|
|
170
|
-
template: "verbose-test",
|
|
171
|
-
verbose: true,
|
|
172
|
-
silent: false,
|
|
173
|
-
cwd: tempDir,
|
|
174
|
-
}),
|
|
175
|
-
)
|
|
176
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe(
|
|
177
|
-
"verbose-test",
|
|
178
|
-
)
|
|
179
|
-
} finally {
|
|
180
|
-
console.log = originalLog
|
|
181
|
-
}
|
|
182
|
-
} finally {
|
|
183
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
184
|
-
}
|
|
185
|
-
})
|
|
56
|
+
test("rejects an existing Agency configuration", async () => {
|
|
57
|
+
await Bun.write(join(parent, "agency.json"), '{"version":2}\n')
|
|
186
58
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
await initGitRepo(tempDir)
|
|
191
|
-
await expect(
|
|
192
|
-
runTestEffect(init({ template: "", silent: true, cwd: tempDir })),
|
|
193
|
-
).rejects.toThrow("Template name required")
|
|
194
|
-
} finally {
|
|
195
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
test("handles template name with special characters", async () => {
|
|
200
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
201
|
-
try {
|
|
202
|
-
await initGitRepo(tempDir)
|
|
203
|
-
await runTestEffect(
|
|
204
|
-
init({ template: "test.template", silent: true, cwd: tempDir }),
|
|
205
|
-
)
|
|
206
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe(
|
|
207
|
-
"test.template",
|
|
208
|
-
)
|
|
209
|
-
} finally {
|
|
210
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
test("handles very long template names", async () => {
|
|
215
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
216
|
-
try {
|
|
217
|
-
await initGitRepo(tempDir)
|
|
218
|
-
const longName = "a".repeat(100)
|
|
219
|
-
await runTestEffect(
|
|
220
|
-
init({ template: longName, silent: true, cwd: tempDir }),
|
|
221
|
-
)
|
|
222
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe(longName)
|
|
223
|
-
} finally {
|
|
224
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
test("changing template updates git config", async () => {
|
|
229
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
230
|
-
try {
|
|
231
|
-
await initGitRepo(tempDir)
|
|
232
|
-
await runTestEffect(
|
|
233
|
-
init({ template: "template-a", silent: true, cwd: tempDir }),
|
|
234
|
-
)
|
|
235
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe("template-a")
|
|
236
|
-
await runTestEffect(
|
|
237
|
-
init({ template: "template-b", silent: true, cwd: tempDir }),
|
|
238
|
-
)
|
|
239
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe("template-b")
|
|
240
|
-
} finally {
|
|
241
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
test("preserves template name across command runs", async () => {
|
|
246
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
247
|
-
try {
|
|
248
|
-
await initGitRepo(tempDir)
|
|
249
|
-
await runTestEffect(
|
|
250
|
-
init({ template: "persistent", silent: true, cwd: tempDir }),
|
|
251
|
-
)
|
|
252
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe("persistent")
|
|
253
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe("persistent")
|
|
254
|
-
} finally {
|
|
255
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
256
|
-
}
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
test("does not create template directory during init", async () => {
|
|
260
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
261
|
-
const configDir = await mkdtemp(join(tmpdir(), "agency-config-"))
|
|
262
|
-
const originalConfigDir = process.env.AGENCY_CONFIG_DIR
|
|
263
|
-
try {
|
|
264
|
-
await initGitRepo(tempDir)
|
|
265
|
-
process.env.AGENCY_CONFIG_DIR = configDir
|
|
266
|
-
await runTestEffect(
|
|
267
|
-
init({ template: "test-template", silent: true, cwd: tempDir }),
|
|
268
|
-
)
|
|
269
|
-
expect(
|
|
270
|
-
await Bun.file(
|
|
271
|
-
`${configDir}/templates/test-template/AGENTS.md`,
|
|
272
|
-
).exists(),
|
|
273
|
-
).toBe(false)
|
|
274
|
-
} finally {
|
|
275
|
-
if (originalConfigDir !== undefined) {
|
|
276
|
-
process.env.AGENCY_CONFIG_DIR = originalConfigDir
|
|
277
|
-
} else {
|
|
278
|
-
delete process.env.AGENCY_CONFIG_DIR
|
|
279
|
-
}
|
|
280
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
281
|
-
await rm(configDir, { recursive: true, force: true })
|
|
282
|
-
}
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
test("works when templates directory does not exist", async () => {
|
|
286
|
-
const tempDir = await mkdtemp(join(tmpdir(), "agency-test-"))
|
|
287
|
-
const originalConfigDir = process.env.AGENCY_CONFIG_DIR
|
|
288
|
-
try {
|
|
289
|
-
await initGitRepo(tempDir)
|
|
290
|
-
// Point to a non-existent config directory
|
|
291
|
-
const configDir = join(tmpdir(), `agency-config-${Date.now()}`)
|
|
292
|
-
process.env.AGENCY_CONFIG_DIR = configDir
|
|
293
|
-
// Verify templates directory doesn't exist
|
|
294
|
-
expect(await Bun.file(join(configDir, "templates")).exists()).toBe(false)
|
|
295
|
-
// Init should work even without templates directory
|
|
296
|
-
await runTestEffect(
|
|
297
|
-
init({ template: "new-template", silent: true, cwd: tempDir }),
|
|
298
|
-
)
|
|
299
|
-
expect(await getGitConfig("agency.template", tempDir)).toBe(
|
|
300
|
-
"new-template",
|
|
301
|
-
)
|
|
302
|
-
} finally {
|
|
303
|
-
if (originalConfigDir !== undefined) {
|
|
304
|
-
process.env.AGENCY_CONFIG_DIR = originalConfigDir
|
|
305
|
-
} else {
|
|
306
|
-
delete process.env.AGENCY_CONFIG_DIR
|
|
307
|
-
}
|
|
308
|
-
await rm(tempDir, { recursive: true, force: true })
|
|
309
|
-
}
|
|
59
|
+
await expect(
|
|
60
|
+
runTestEffect(init({ path: parent, silent: true })),
|
|
61
|
+
).rejects.toThrow("already exists")
|
|
310
62
|
})
|
|
311
63
|
})
|