@markjaquith/agency 1.11.1 → 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 -420
- 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 -641
- package/src/commands/push.ts +0 -482
- 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 -1187
- 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,146 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { Schema } from "@effect/schema"
|
|
3
|
+
import {
|
|
4
|
+
EntityId,
|
|
5
|
+
EpicFrontmatter,
|
|
6
|
+
PhaseFrontmatter,
|
|
7
|
+
TaskFrontmatter,
|
|
8
|
+
WorkbaseConfig,
|
|
9
|
+
} from "./schemas"
|
|
10
|
+
|
|
11
|
+
describe("body-of-work descriptions", () => {
|
|
12
|
+
test("accepts descriptions on epics, tasks, and phases", () => {
|
|
13
|
+
const epic = Schema.decodeUnknownSync(EpicFrontmatter)({
|
|
14
|
+
ticketUrl: "https://example.com/epic",
|
|
15
|
+
description: "Coordinate the rollout.",
|
|
16
|
+
repos: [{ repo: "agency", ref: "main" }],
|
|
17
|
+
tasks: [],
|
|
18
|
+
})
|
|
19
|
+
const singlePhaseTask = Schema.decodeUnknownSync(TaskFrontmatter)({
|
|
20
|
+
ticketUrl: "https://example.com/task",
|
|
21
|
+
description: "Deliver the rollout.",
|
|
22
|
+
repo: "agency",
|
|
23
|
+
branch: "task/rollout",
|
|
24
|
+
base: "main",
|
|
25
|
+
pr: null,
|
|
26
|
+
})
|
|
27
|
+
const multiPhaseTask = Schema.decodeUnknownSync(TaskFrontmatter)({
|
|
28
|
+
ticketUrl: "https://example.com/task",
|
|
29
|
+
description: "Deliver the rollout in phases.",
|
|
30
|
+
phases: [],
|
|
31
|
+
})
|
|
32
|
+
const phase = Schema.decodeUnknownSync(PhaseFrontmatter)({
|
|
33
|
+
description: "Introduce the service contract.",
|
|
34
|
+
repo: "agency",
|
|
35
|
+
branch: "task/contract",
|
|
36
|
+
base: "main",
|
|
37
|
+
pr: null,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
expect(epic.description).toBe("Coordinate the rollout.")
|
|
41
|
+
expect(singlePhaseTask.description).toBe("Deliver the rollout.")
|
|
42
|
+
expect(multiPhaseTask.description).toBe("Deliver the rollout in phases.")
|
|
43
|
+
expect(phase.description).toBe("Introduce the service contract.")
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("allows omitted descriptions for existing documents", () => {
|
|
47
|
+
const epic = Schema.decodeUnknownSync(EpicFrontmatter)({
|
|
48
|
+
ticketUrl: "https://example.com/epic",
|
|
49
|
+
repos: [{ repo: "agency", ref: "main" }],
|
|
50
|
+
tasks: [],
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(epic.description).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test("rejects an empty description when present", () => {
|
|
57
|
+
expect(() =>
|
|
58
|
+
Schema.decodeUnknownSync(PhaseFrontmatter)({
|
|
59
|
+
description: "",
|
|
60
|
+
repo: "agency",
|
|
61
|
+
branch: "task/contract",
|
|
62
|
+
base: "main",
|
|
63
|
+
pr: null,
|
|
64
|
+
}),
|
|
65
|
+
).toThrow()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("schema boundaries", () => {
|
|
70
|
+
const rejects = <S extends Schema.Schema.AnyNoContext>(
|
|
71
|
+
schema: S,
|
|
72
|
+
input: unknown,
|
|
73
|
+
) => {
|
|
74
|
+
expect(() =>
|
|
75
|
+
Schema.decodeUnknownSync(schema, { onExcessProperty: "error" })(input),
|
|
76
|
+
).toThrow()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test("rejects invalid boundary values", () => {
|
|
80
|
+
const cases = [
|
|
81
|
+
{
|
|
82
|
+
name: "unsupported config version",
|
|
83
|
+
schema: WorkbaseConfig,
|
|
84
|
+
input: { version: 1 },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "excess config fields",
|
|
88
|
+
schema: WorkbaseConfig,
|
|
89
|
+
input: { version: 2, legacy: true },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "invalid entity ID",
|
|
93
|
+
schema: EntityId,
|
|
94
|
+
input: "invalid/id",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "empty epic repositories",
|
|
98
|
+
schema: EpicFrontmatter,
|
|
99
|
+
input: {
|
|
100
|
+
ticketUrl: "https://example.com/epic",
|
|
101
|
+
repos: [],
|
|
102
|
+
tasks: [],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "mixed task union",
|
|
107
|
+
schema: TaskFrontmatter,
|
|
108
|
+
input: {
|
|
109
|
+
ticketUrl: "https://example.com/task",
|
|
110
|
+
phases: [],
|
|
111
|
+
repo: "agency",
|
|
112
|
+
branch: "task/mixed",
|
|
113
|
+
base: "main",
|
|
114
|
+
pr: null,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "single-phase task missing pr",
|
|
119
|
+
schema: TaskFrontmatter,
|
|
120
|
+
input: {
|
|
121
|
+
ticketUrl: "https://example.com/task",
|
|
122
|
+
repo: "agency",
|
|
123
|
+
branch: "task/missing-pr",
|
|
124
|
+
base: "main",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "invalid phase PR URL",
|
|
129
|
+
schema: PhaseFrontmatter,
|
|
130
|
+
input: {
|
|
131
|
+
repo: "agency",
|
|
132
|
+
branch: "task/invalid-pr",
|
|
133
|
+
base: "main",
|
|
134
|
+
pr: "https://example.com/pull/1",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
] as const
|
|
138
|
+
|
|
139
|
+
for (const fixture of cases) {
|
|
140
|
+
expect(
|
|
141
|
+
() => rejects(fixture.schema, fixture.input),
|
|
142
|
+
fixture.name,
|
|
143
|
+
).not.toThrow()
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Schema } from "@effect/schema"
|
|
2
|
+
|
|
3
|
+
const NonEmptyString = Schema.String.pipe(Schema.minLength(1))
|
|
4
|
+
|
|
5
|
+
const Description = Schema.optional(NonEmptyString)
|
|
6
|
+
|
|
7
|
+
const IdPattern = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/
|
|
8
|
+
|
|
9
|
+
export const EntityId = NonEmptyString.pipe(Schema.pattern(IdPattern))
|
|
10
|
+
|
|
11
|
+
export const RepositoryAlias = NonEmptyString.pipe(Schema.pattern(IdPattern))
|
|
12
|
+
|
|
13
|
+
export const RepositoryReference = Schema.Struct({
|
|
14
|
+
repo: RepositoryAlias,
|
|
15
|
+
ref: NonEmptyString,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const Url = NonEmptyString.pipe(Schema.pattern(/^[a-zA-Z][a-zA-Z0-9+.-]*:/))
|
|
19
|
+
|
|
20
|
+
const GitHubPullRequestUrl = NonEmptyString.pipe(
|
|
21
|
+
Schema.pattern(/^https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+\/?$/),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
export const WorkbaseConfig = Schema.Struct({
|
|
25
|
+
version: Schema.Literal(2),
|
|
26
|
+
worktreeCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export const Dependency = Schema.Struct({
|
|
30
|
+
id: EntityId,
|
|
31
|
+
dependsOn: Schema.optional(Schema.Array(EntityId)),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const ExecutionUnit = {
|
|
35
|
+
repo: RepositoryAlias,
|
|
36
|
+
repos: Schema.optional(Schema.Array(RepositoryReference)),
|
|
37
|
+
branch: NonEmptyString,
|
|
38
|
+
base: NonEmptyString,
|
|
39
|
+
pr: Schema.NullOr(GitHubPullRequestUrl),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const EpicFrontmatter = Schema.Struct({
|
|
43
|
+
ticketUrl: Url,
|
|
44
|
+
description: Description,
|
|
45
|
+
repos: Schema.NonEmptyArray(RepositoryReference),
|
|
46
|
+
tasks: Schema.Array(Dependency),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const SinglePhaseTaskFrontmatter = Schema.Struct({
|
|
50
|
+
ticketUrl: Url,
|
|
51
|
+
description: Description,
|
|
52
|
+
epic: Schema.optional(EntityId),
|
|
53
|
+
...ExecutionUnit,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const MultiPhaseTaskFrontmatter = Schema.Struct({
|
|
57
|
+
ticketUrl: Url,
|
|
58
|
+
description: Description,
|
|
59
|
+
epic: Schema.optional(EntityId),
|
|
60
|
+
phases: Schema.Array(Dependency),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
export const TaskFrontmatter = Schema.Union(
|
|
64
|
+
SinglePhaseTaskFrontmatter,
|
|
65
|
+
MultiPhaseTaskFrontmatter,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
export const PhaseFrontmatter = Schema.Struct({
|
|
69
|
+
description: Description,
|
|
70
|
+
...ExecutionUnit,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
export type WorkbaseConfig = Schema.Schema.Type<typeof WorkbaseConfig>
|
|
74
|
+
export type Dependency = Schema.Schema.Type<typeof Dependency>
|
|
75
|
+
export type RepositoryReference = Schema.Schema.Type<typeof RepositoryReference>
|
|
76
|
+
export type EpicFrontmatter = Schema.Schema.Type<typeof EpicFrontmatter>
|
|
77
|
+
export type TaskFrontmatter = Schema.Schema.Type<typeof TaskFrontmatter>
|
|
78
|
+
export type PhaseFrontmatter = Schema.Schema.Type<typeof PhaseFrontmatter>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
expandWorktreeCreateCommand,
|
|
4
|
+
worktreeCommandEnvironment,
|
|
5
|
+
} from "./worktree-command"
|
|
6
|
+
|
|
7
|
+
const variables = {
|
|
8
|
+
repo: "/work/repos/app",
|
|
9
|
+
worktree: "/work/tasks/example/code/app",
|
|
10
|
+
branch: "task/example",
|
|
11
|
+
base: "main",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("worktree command templates", () => {
|
|
15
|
+
test("expands argv placeholders without shell interpolation", () => {
|
|
16
|
+
expect(
|
|
17
|
+
expandWorktreeCreateCommand(
|
|
18
|
+
[
|
|
19
|
+
"tool",
|
|
20
|
+
"--repo={repo}",
|
|
21
|
+
"--worktree",
|
|
22
|
+
"{worktree}",
|
|
23
|
+
"{branch}",
|
|
24
|
+
"{base}",
|
|
25
|
+
],
|
|
26
|
+
variables,
|
|
27
|
+
),
|
|
28
|
+
).toEqual([
|
|
29
|
+
"tool",
|
|
30
|
+
"--repo=/work/repos/app",
|
|
31
|
+
"--worktree",
|
|
32
|
+
"/work/tasks/example/code/app",
|
|
33
|
+
"task/example",
|
|
34
|
+
"main",
|
|
35
|
+
])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("requires repo and worktree placeholders", () => {
|
|
39
|
+
expect(() =>
|
|
40
|
+
expandWorktreeCreateCommand(["tool", "{repo}"], variables),
|
|
41
|
+
).toThrow("{worktree}")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test("rejects unknown placeholders", () => {
|
|
45
|
+
expect(() =>
|
|
46
|
+
expandWorktreeCreateCommand(
|
|
47
|
+
["tool", "{repo}", "{worktree}", "{unknown}"],
|
|
48
|
+
variables,
|
|
49
|
+
),
|
|
50
|
+
).toThrow("{unknown}")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test("provides equivalent environment variables", () => {
|
|
54
|
+
expect(worktreeCommandEnvironment(variables)).toEqual({
|
|
55
|
+
AGENCY_REPO: variables.repo,
|
|
56
|
+
AGENCY_WORKTREE: variables.worktree,
|
|
57
|
+
AGENCY_BRANCH: variables.branch,
|
|
58
|
+
AGENCY_BASE: variables.base,
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
interface WorktreeCommandVariables {
|
|
2
|
+
readonly repo: string
|
|
3
|
+
readonly worktree: string
|
|
4
|
+
readonly branch: string
|
|
5
|
+
readonly base: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const REQUIRED_PLACEHOLDERS = ["repo", "worktree"] as const
|
|
9
|
+
const PLACEHOLDERS = new Set(["repo", "worktree", "branch", "base"])
|
|
10
|
+
|
|
11
|
+
export const validateWorktreeCreateCommand = (command: readonly string[]) => {
|
|
12
|
+
const template = command.join("\u0000")
|
|
13
|
+
for (const placeholder of REQUIRED_PLACEHOLDERS) {
|
|
14
|
+
if (!template.includes(`{${placeholder}}`)) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`worktreeCreateCommand must include the {${placeholder}} placeholder`,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
for (const argument of command) {
|
|
21
|
+
for (const match of argument.matchAll(/\{([^{}]+)\}/g)) {
|
|
22
|
+
const placeholder = match[1]!
|
|
23
|
+
if (!PLACEHOLDERS.has(placeholder)) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Unknown worktreeCreateCommand placeholder: {${placeholder}}`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const expandWorktreeCreateCommand = (
|
|
33
|
+
command: readonly string[],
|
|
34
|
+
variables: WorktreeCommandVariables,
|
|
35
|
+
): string[] => {
|
|
36
|
+
validateWorktreeCreateCommand(command)
|
|
37
|
+
|
|
38
|
+
return command.map((argument) =>
|
|
39
|
+
argument.replaceAll(/\{([^{}]+)\}/g, (match, placeholder: string) => {
|
|
40
|
+
return variables[placeholder as keyof WorktreeCommandVariables] ?? match
|
|
41
|
+
}),
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const worktreeCommandEnvironment = (
|
|
46
|
+
variables: WorktreeCommandVariables,
|
|
47
|
+
): Record<string, string> => ({
|
|
48
|
+
AGENCY_REPO: variables.repo,
|
|
49
|
+
AGENCY_WORKTREE: variables.worktree,
|
|
50
|
+
AGENCY_BRANCH: variables.branch,
|
|
51
|
+
AGENCY_BASE: variables.base,
|
|
52
|
+
})
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { base } from "./base"
|
|
3
|
-
import {
|
|
4
|
-
createTempDir,
|
|
5
|
-
cleanupTempDir,
|
|
6
|
-
initGitRepo,
|
|
7
|
-
getGitConfig,
|
|
8
|
-
runTestEffect,
|
|
9
|
-
} from "../test-utils"
|
|
10
|
-
import { getBaseBranchFromMetadata, writeAgencyMetadata } from "../types"
|
|
11
|
-
|
|
12
|
-
describe("base", () => {
|
|
13
|
-
let testDir: string
|
|
14
|
-
let originalCwd: string
|
|
15
|
-
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
originalCwd = process.cwd()
|
|
18
|
-
testDir = await createTempDir()
|
|
19
|
-
await initGitRepo(testDir)
|
|
20
|
-
process.chdir(testDir)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
process.chdir(originalCwd)
|
|
25
|
-
await cleanupTempDir(testDir)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
describe("base set", () => {
|
|
29
|
-
test("sets base branch for current branch", async () => {
|
|
30
|
-
// Create a feature branch
|
|
31
|
-
await Bun.spawn(["git", "checkout", "-b", "feature"], {
|
|
32
|
-
cwd: testDir,
|
|
33
|
-
stdout: "pipe",
|
|
34
|
-
stderr: "pipe",
|
|
35
|
-
}).exited
|
|
36
|
-
|
|
37
|
-
// Create agency.json first
|
|
38
|
-
await writeAgencyMetadata(testDir, {
|
|
39
|
-
version: 1 as const,
|
|
40
|
-
template: "test",
|
|
41
|
-
injectedFiles: [],
|
|
42
|
-
createdAt: new Date().toISOString(),
|
|
43
|
-
} as any)
|
|
44
|
-
|
|
45
|
-
// Set base branch
|
|
46
|
-
await runTestEffect(
|
|
47
|
-
base({
|
|
48
|
-
subcommand: "set",
|
|
49
|
-
args: ["main"],
|
|
50
|
-
silent: true,
|
|
51
|
-
}),
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
// Verify it was saved
|
|
55
|
-
const savedBase = await getBaseBranchFromMetadata(testDir)
|
|
56
|
-
expect(savedBase).toBe("main")
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test("sets repository-level default base branch", async () => {
|
|
60
|
-
// Set repo-level base branch
|
|
61
|
-
await runTestEffect(
|
|
62
|
-
base({
|
|
63
|
-
subcommand: "set",
|
|
64
|
-
args: ["main"],
|
|
65
|
-
repo: true,
|
|
66
|
-
silent: true,
|
|
67
|
-
}),
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
// Verify it was saved to git config
|
|
71
|
-
const savedBase = await getGitConfig("agency.baseBranch", testDir)
|
|
72
|
-
expect(savedBase).toBe("main")
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test("throws error if base branch does not exist", async () => {
|
|
76
|
-
await expect(
|
|
77
|
-
runTestEffect(
|
|
78
|
-
base({
|
|
79
|
-
subcommand: "set",
|
|
80
|
-
args: ["nonexistent"],
|
|
81
|
-
silent: true,
|
|
82
|
-
}),
|
|
83
|
-
),
|
|
84
|
-
).rejects.toThrow("does not exist")
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe("base get", () => {
|
|
89
|
-
test("gets base branch for current branch", async () => {
|
|
90
|
-
// Create a feature branch
|
|
91
|
-
await Bun.spawn(["git", "checkout", "-b", "feature"], {
|
|
92
|
-
cwd: testDir,
|
|
93
|
-
stdout: "pipe",
|
|
94
|
-
stderr: "pipe",
|
|
95
|
-
}).exited
|
|
96
|
-
|
|
97
|
-
// Create agency.json with base branch
|
|
98
|
-
await writeAgencyMetadata(testDir, {
|
|
99
|
-
version: 1,
|
|
100
|
-
template: "test",
|
|
101
|
-
injectedFiles: [],
|
|
102
|
-
baseBranch: "main",
|
|
103
|
-
createdAt: new Date().toISOString(),
|
|
104
|
-
} as any)
|
|
105
|
-
|
|
106
|
-
// Mock console.log to capture output
|
|
107
|
-
const logs: string[] = []
|
|
108
|
-
const originalLog = console.log
|
|
109
|
-
console.log = (...args: any[]) => logs.push(args.join(" "))
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
await runTestEffect(
|
|
113
|
-
base({
|
|
114
|
-
subcommand: "get",
|
|
115
|
-
args: [],
|
|
116
|
-
silent: false,
|
|
117
|
-
}),
|
|
118
|
-
)
|
|
119
|
-
expect(logs[0]).toBe("main")
|
|
120
|
-
} finally {
|
|
121
|
-
console.log = originalLog
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test("throws error if no base branch configured", async () => {
|
|
126
|
-
// Create a feature branch
|
|
127
|
-
await Bun.spawn(["git", "checkout", "-b", "feature"], {
|
|
128
|
-
cwd: testDir,
|
|
129
|
-
stdout: "pipe",
|
|
130
|
-
stderr: "pipe",
|
|
131
|
-
}).exited
|
|
132
|
-
|
|
133
|
-
// Create agency.json without base branch
|
|
134
|
-
await writeAgencyMetadata(testDir, {
|
|
135
|
-
version: 1,
|
|
136
|
-
template: "test",
|
|
137
|
-
injectedFiles: [],
|
|
138
|
-
createdAt: new Date().toISOString(),
|
|
139
|
-
} as any)
|
|
140
|
-
|
|
141
|
-
await expect(
|
|
142
|
-
runTestEffect(
|
|
143
|
-
base({
|
|
144
|
-
subcommand: "get",
|
|
145
|
-
args: [],
|
|
146
|
-
silent: true,
|
|
147
|
-
}),
|
|
148
|
-
),
|
|
149
|
-
).rejects.toThrow("No base branch configured")
|
|
150
|
-
})
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
describe("base command", () => {
|
|
154
|
-
test("requires subcommand", async () => {
|
|
155
|
-
await expect(
|
|
156
|
-
runTestEffect(base({ args: [], silent: true })),
|
|
157
|
-
).rejects.toThrow("Subcommand is required")
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
test("handles 'set' subcommand", async () => {
|
|
161
|
-
await Bun.spawn(["git", "checkout", "-b", "feature"], {
|
|
162
|
-
cwd: testDir,
|
|
163
|
-
stdout: "pipe",
|
|
164
|
-
stderr: "pipe",
|
|
165
|
-
}).exited
|
|
166
|
-
|
|
167
|
-
await writeAgencyMetadata(testDir, {
|
|
168
|
-
version: 1,
|
|
169
|
-
template: "test",
|
|
170
|
-
injectedFiles: [],
|
|
171
|
-
createdAt: new Date().toISOString(),
|
|
172
|
-
} as any)
|
|
173
|
-
|
|
174
|
-
await runTestEffect(
|
|
175
|
-
base({
|
|
176
|
-
subcommand: "set",
|
|
177
|
-
args: ["main"],
|
|
178
|
-
silent: true,
|
|
179
|
-
}),
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
const savedBase = await getBaseBranchFromMetadata(testDir)
|
|
183
|
-
expect(savedBase).toBe("main")
|
|
184
|
-
})
|
|
185
|
-
|
|
186
|
-
test("throws error for unknown subcommand", async () => {
|
|
187
|
-
await expect(
|
|
188
|
-
runTestEffect(
|
|
189
|
-
base({
|
|
190
|
-
subcommand: "unknown",
|
|
191
|
-
args: [],
|
|
192
|
-
silent: true,
|
|
193
|
-
}),
|
|
194
|
-
),
|
|
195
|
-
).rejects.toThrow("Unknown subcommand")
|
|
196
|
-
})
|
|
197
|
-
})
|
|
198
|
-
})
|