@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
package/src/commands/init.ts
CHANGED
|
@@ -1,140 +1,32 @@
|
|
|
1
|
-
import { basename } from "path"
|
|
2
1
|
import { Effect } from "effect"
|
|
3
2
|
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { TemplateService } from "../services/TemplateService"
|
|
7
|
-
import highlight, { done } from "../utils/colors"
|
|
8
|
-
import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
|
|
3
|
+
import { WorkbaseService } from "../services/WorkbaseService"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
9
5
|
|
|
10
6
|
interface InitOptions extends BaseCommandOptions {
|
|
11
|
-
|
|
7
|
+
readonly path?: string
|
|
12
8
|
}
|
|
13
9
|
|
|
14
10
|
export const init = (options: InitOptions = {}) =>
|
|
15
11
|
Effect.gen(function* () {
|
|
16
|
-
const
|
|
17
|
-
const { log
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const promptService = yield* PromptService
|
|
21
|
-
const templateService = yield* TemplateService
|
|
22
|
-
|
|
23
|
-
const gitRoot = yield* ensureGitRepo(cwd)
|
|
24
|
-
|
|
25
|
-
// Check if already initialized
|
|
26
|
-
const existingTemplate = yield* getTemplateName(gitRoot)
|
|
27
|
-
if (existingTemplate && !options.template) {
|
|
28
|
-
return yield* Effect.fail(
|
|
29
|
-
new Error(
|
|
30
|
-
`Already initialized with template ${highlight.template(existingTemplate)}.\n` +
|
|
31
|
-
`To change template, run: agency init --template <name>`,
|
|
32
|
-
),
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let templateName = options.template
|
|
37
|
-
|
|
38
|
-
// If template name not provided, show interactive selection
|
|
39
|
-
if (!templateName) {
|
|
40
|
-
if (silent) {
|
|
41
|
-
return yield* Effect.fail(
|
|
42
|
-
new Error(
|
|
43
|
-
"Template name required. Use --template flag in silent mode.",
|
|
44
|
-
),
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const existingTemplates = yield* templateService.listTemplates()
|
|
49
|
-
verboseLog(`Found ${existingTemplates.length} existing templates`)
|
|
50
|
-
|
|
51
|
-
// Get current directory name as default suggestion
|
|
52
|
-
let defaultTemplateName: string | undefined
|
|
53
|
-
const dirName = basename(gitRoot)
|
|
54
|
-
const sanitizedDirName =
|
|
55
|
-
yield* promptService.sanitizeTemplateName(dirName)
|
|
56
|
-
|
|
57
|
-
if (sanitizedDirName && !existingTemplates.includes(sanitizedDirName)) {
|
|
58
|
-
defaultTemplateName = sanitizedDirName
|
|
59
|
-
verboseLog(`Suggesting default template name: ${defaultTemplateName}`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const selectedName = yield* promptService.promptForTemplate(
|
|
63
|
-
existingTemplates,
|
|
64
|
-
{
|
|
65
|
-
defaultValue: defaultTemplateName,
|
|
66
|
-
allowNew: true,
|
|
67
|
-
},
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
// If selected from list, use as-is; otherwise sanitize new name
|
|
71
|
-
if (existingTemplates.includes(selectedName)) {
|
|
72
|
-
templateName = selectedName
|
|
73
|
-
} else {
|
|
74
|
-
templateName = yield* promptService.sanitizeTemplateName(selectedName)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
verboseLog(`Selected template: ${templateName}`)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!templateName) {
|
|
81
|
-
return yield* Effect.fail(new Error("Template name is required."))
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Resolve and save default remote (with smart precedence)
|
|
85
|
-
const remote = yield* git
|
|
86
|
-
.resolveRemote(gitRoot)
|
|
87
|
-
.pipe(Effect.catchAll(() => Effect.succeed(null)))
|
|
88
|
-
|
|
89
|
-
if (remote) {
|
|
90
|
-
yield* git.setRemoteConfig(remote, gitRoot)
|
|
91
|
-
verboseLog(`Detected and saved remote: ${remote}`)
|
|
92
|
-
} else {
|
|
93
|
-
verboseLog("No remote detected - skip remote configuration")
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Save template name to git config
|
|
97
|
-
yield* git.setGitConfig("agency.template", templateName, gitRoot)
|
|
98
|
-
log(
|
|
99
|
-
done(
|
|
100
|
-
`Initialized with template ${highlight.template(templateName)}${existingTemplate && existingTemplate !== templateName ? ` (was ${highlight.template(existingTemplate)})` : ""}`,
|
|
101
|
-
),
|
|
12
|
+
const workbase = yield* WorkbaseService
|
|
13
|
+
const { log } = createLoggers(options)
|
|
14
|
+
const root = yield* workbase.initialize(
|
|
15
|
+
options.path ?? options.cwd ?? process.cwd(),
|
|
102
16
|
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`Template directory will be created when you save files with 'agency template save'`,
|
|
17
|
+
log(
|
|
18
|
+
options.json
|
|
19
|
+
? JSON.stringify({ root }, null, 2)
|
|
20
|
+
: `Initialized Agency workbase at ${root}`,
|
|
108
21
|
)
|
|
109
22
|
})
|
|
110
23
|
|
|
111
24
|
export const help = `
|
|
112
|
-
Usage: agency init [
|
|
25
|
+
Usage: agency init [path]
|
|
113
26
|
|
|
114
|
-
Initialize
|
|
115
|
-
|
|
116
|
-
This command must be run before using other agency commands like 'agency task'.
|
|
117
|
-
It saves your template selection to .git/config (not committed to the repository).
|
|
118
|
-
|
|
119
|
-
On first run, you'll be prompted to either:
|
|
120
|
-
- Select an existing template from your ~/.config/agency/templates/
|
|
121
|
-
- Create a new template by entering a new name
|
|
122
|
-
|
|
123
|
-
If no templates exist, the default suggestion is the current directory name.
|
|
124
|
-
|
|
125
|
-
The template directory itself is only created when you actually save files to it
|
|
126
|
-
using 'agency template save'.
|
|
27
|
+
Initialize an Agency workbase. The current directory is used when path is
|
|
28
|
+
omitted.
|
|
127
29
|
|
|
128
30
|
Options:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
Examples:
|
|
132
|
-
agency init # Interactive template selection
|
|
133
|
-
agency init --template work # Set template to 'work'
|
|
134
|
-
|
|
135
|
-
Notes:
|
|
136
|
-
- Template name is saved to .git/config (agency.template)
|
|
137
|
-
- Template directory is NOT created until 'agency template save' is used
|
|
138
|
-
- To change template later, run 'agency init --template <name>'
|
|
139
|
-
- Run 'agency task' after initialization to create template files
|
|
31
|
+
--json Output the initialized workbase as JSON
|
|
140
32
|
`
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
3
|
+
import { PhaseService } from "../services/PhaseService"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
5
|
+
import { parseRepositoryReferences } from "../workbase/repository-reference"
|
|
6
|
+
|
|
7
|
+
interface PhaseOptions extends BaseCommandOptions {
|
|
8
|
+
readonly subcommand?: string
|
|
9
|
+
readonly args: readonly string[]
|
|
10
|
+
readonly description?: string
|
|
11
|
+
readonly repo?: string
|
|
12
|
+
readonly references?: readonly string[]
|
|
13
|
+
readonly branch?: string
|
|
14
|
+
readonly base?: string
|
|
15
|
+
readonly dependsOn?: readonly string[]
|
|
16
|
+
readonly firstPhase?: string
|
|
17
|
+
readonly json?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const phase = (options: PhaseOptions) =>
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const phases = yield* PhaseService
|
|
23
|
+
const { log } = createLoggers(options)
|
|
24
|
+
const cwd = options.cwd ?? process.cwd()
|
|
25
|
+
const [taskId, phaseId] = options.args
|
|
26
|
+
|
|
27
|
+
switch (options.subcommand) {
|
|
28
|
+
case "create": {
|
|
29
|
+
if (
|
|
30
|
+
!taskId ||
|
|
31
|
+
!phaseId ||
|
|
32
|
+
!options.repo ||
|
|
33
|
+
!options.branch ||
|
|
34
|
+
!options.base
|
|
35
|
+
) {
|
|
36
|
+
return yield* Effect.fail(
|
|
37
|
+
new Error(
|
|
38
|
+
"Usage: agency phase create <task-id> <phase-id> --repo <alias> --branch <name> --base <name>",
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
const record = yield* phases.create(
|
|
43
|
+
{
|
|
44
|
+
taskId,
|
|
45
|
+
id: phaseId,
|
|
46
|
+
description: options.description,
|
|
47
|
+
repo: options.repo,
|
|
48
|
+
repos: parseRepositoryReferences(options.references),
|
|
49
|
+
branch: options.branch,
|
|
50
|
+
base: options.base,
|
|
51
|
+
dependsOn: options.dependsOn,
|
|
52
|
+
firstPhase: options.firstPhase,
|
|
53
|
+
},
|
|
54
|
+
cwd,
|
|
55
|
+
)
|
|
56
|
+
const { content: _, ...output } = record
|
|
57
|
+
log(
|
|
58
|
+
options.json
|
|
59
|
+
? JSON.stringify(output, null, 2)
|
|
60
|
+
: `Created phase '${record.id}' on task '${record.taskId}'`,
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
case "list": {
|
|
65
|
+
if (!taskId) return yield* Effect.fail(new Error("Task ID is required"))
|
|
66
|
+
const records = yield* phases.list(taskId, cwd)
|
|
67
|
+
if (options.json) {
|
|
68
|
+
log(
|
|
69
|
+
JSON.stringify(
|
|
70
|
+
records.map(({ content: _, ...record }) => record),
|
|
71
|
+
null,
|
|
72
|
+
2,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
} else for (const record of records) log(record.id)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
case "show": {
|
|
79
|
+
if (!taskId || !phaseId) {
|
|
80
|
+
return yield* Effect.fail(
|
|
81
|
+
new Error("Task ID and phase ID are required"),
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
const record = yield* phases.show(taskId, phaseId, cwd)
|
|
85
|
+
const { content: _, ...output } = record
|
|
86
|
+
log(
|
|
87
|
+
options.json
|
|
88
|
+
? JSON.stringify(output, null, 2)
|
|
89
|
+
: record.content.trimEnd(),
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
return yield* Effect.fail(
|
|
95
|
+
new Error("Subcommand is required. Available: create, list, show"),
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
export const help = `
|
|
101
|
+
Usage: agency phase <subcommand> <task-id> [phase-id]
|
|
102
|
+
|
|
103
|
+
Subcommands:
|
|
104
|
+
create <task> <phase> Create a phase
|
|
105
|
+
list <task> List task phases
|
|
106
|
+
show <task> <phase> Show a phase
|
|
107
|
+
|
|
108
|
+
Create options:
|
|
109
|
+
--description <text> Short description of the phase
|
|
110
|
+
--repo <alias> Writable repository
|
|
111
|
+
--reference <alias>:<ref>
|
|
112
|
+
Read-only repository reference; repeatable
|
|
113
|
+
--branch <name> Working branch
|
|
114
|
+
--base <name> Base branch
|
|
115
|
+
--depends-on <id> Phase dependency; repeatable
|
|
116
|
+
--first-phase <id> Existing execution phase ID when converting a task
|
|
117
|
+
|
|
118
|
+
Options:
|
|
119
|
+
--json Output results as JSON
|
|
120
|
+
`
|
package/src/commands/pr.test.ts
CHANGED
|
@@ -1,177 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { captureLogs } from "../test-utils"
|
|
4
|
+
import { PullRequestService } from "../services/PullRequestService"
|
|
4
5
|
import { pr } from "./pr"
|
|
5
|
-
import {
|
|
6
|
-
cleanupTempDir,
|
|
7
|
-
createBranch,
|
|
8
|
-
createTempDir,
|
|
9
|
-
initGitRepo,
|
|
10
|
-
runTestEffect,
|
|
11
|
-
} from "../test-utils"
|
|
12
|
-
|
|
13
|
-
const restoreEnv = (key: string, value: string | undefined) => {
|
|
14
|
-
if (value === undefined) {
|
|
15
|
-
delete process.env[key]
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
process.env[key] = value
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const readGhArgs = async (recordPath: string): Promise<string[]> => {
|
|
23
|
-
const file = Bun.file(recordPath)
|
|
24
|
-
|
|
25
|
-
if (!(await file.exists())) {
|
|
26
|
-
return []
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const content = await file.text()
|
|
30
|
-
return content.trim().split("\n").filter(Boolean)
|
|
31
|
-
}
|
|
32
6
|
|
|
33
7
|
describe("pr command", () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
process.chdir(tempDir)
|
|
50
|
-
process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
|
|
51
|
-
process.env.AGENCY_TEST_GH_ARGS_FILE = recordPath
|
|
52
|
-
|
|
53
|
-
const binDir = join(tempDir, "bin")
|
|
54
|
-
const ghPath = join(binDir, "gh")
|
|
55
|
-
await mkdir(binDir)
|
|
56
|
-
await Bun.write(
|
|
57
|
-
ghPath,
|
|
58
|
-
`#!/bin/sh
|
|
59
|
-
: > "$AGENCY_TEST_GH_ARGS_FILE"
|
|
60
|
-
for arg in "$@"; do
|
|
61
|
-
printf '%s\n' "$arg" >> "$AGENCY_TEST_GH_ARGS_FILE"
|
|
62
|
-
done
|
|
63
|
-
`,
|
|
64
|
-
)
|
|
65
|
-
await chmod(ghPath, 0o755)
|
|
66
|
-
process.env.PATH = `${binDir}:${originalPath ?? ""}`
|
|
67
|
-
|
|
68
|
-
await initGitRepo(tempDir)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
afterEach(async () => {
|
|
72
|
-
process.chdir(originalCwd)
|
|
73
|
-
restoreEnv("PATH", originalPath)
|
|
74
|
-
restoreEnv("AGENCY_CONFIG_PATH", originalAgencyConfigPath)
|
|
75
|
-
restoreEnv("AGENCY_TEST_GH_ARGS_FILE", originalGhArgsFile)
|
|
76
|
-
await cleanupTempDir(tempDir)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test("passes through to gh pr unchanged outside agency context", async () => {
|
|
80
|
-
await createBranch(tempDir, "feature")
|
|
81
|
-
|
|
82
|
-
await runTestEffect(pr({ args: ["status"], silent: false }))
|
|
83
|
-
|
|
84
|
-
expect(await readGhArgs(recordPath)).toEqual(["pr", "status"])
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test("passes through outside agency context with custom emit pattern", async () => {
|
|
88
|
-
const configPath = process.env.AGENCY_CONFIG_PATH
|
|
89
|
-
|
|
90
|
-
if (!configPath) {
|
|
91
|
-
throw new Error("AGENCY_CONFIG_PATH is not set")
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
await Bun.write(
|
|
95
|
-
configPath,
|
|
96
|
-
JSON.stringify({
|
|
97
|
-
sourceBranchPattern: "agency--%branch%",
|
|
98
|
-
emitBranch: "%branch%--PR",
|
|
99
|
-
}),
|
|
8
|
+
test("outputs the created pull request URL as JSON", async () => {
|
|
9
|
+
const url = "https://github.com/markjaquith/agency/pull/123"
|
|
10
|
+
const logs = await captureLogs(() =>
|
|
11
|
+
Effect.runPromise(
|
|
12
|
+
pr({
|
|
13
|
+
subcommand: "create",
|
|
14
|
+
taskId: "example",
|
|
15
|
+
json: true,
|
|
16
|
+
}).pipe(
|
|
17
|
+
Effect.provideService(PullRequestService, {
|
|
18
|
+
create: () => Effect.succeed(url),
|
|
19
|
+
} as never),
|
|
20
|
+
) as Effect.Effect<void, unknown, never>,
|
|
21
|
+
),
|
|
100
22
|
)
|
|
101
|
-
await createBranch(tempDir, "feature")
|
|
102
|
-
|
|
103
|
-
await runTestEffect(pr({ args: ["status"], silent: false }))
|
|
104
|
-
|
|
105
|
-
expect(await readGhArgs(recordPath)).toEqual(["pr", "status"])
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test("appends emitted branch in agency context", async () => {
|
|
109
|
-
await createBranch(tempDir, "agency--feature")
|
|
110
|
-
|
|
111
|
-
await runTestEffect(pr({ args: ["view", "--web"], silent: false }))
|
|
112
|
-
|
|
113
|
-
expect(await readGhArgs(recordPath)).toEqual([
|
|
114
|
-
"pr",
|
|
115
|
-
"view",
|
|
116
|
-
"--web",
|
|
117
|
-
"feature",
|
|
118
|
-
])
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
test("appends emitted branch with gh pr value flags when no selector is provided", async () => {
|
|
122
|
-
await createBranch(tempDir, "agency--feature")
|
|
123
|
-
|
|
124
|
-
await runTestEffect(
|
|
125
|
-
pr({
|
|
126
|
-
args: ["view", "--json", "number", "--jq", ".number"],
|
|
127
|
-
silent: false,
|
|
128
|
-
}),
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
expect(await readGhArgs(recordPath)).toEqual([
|
|
132
|
-
"pr",
|
|
133
|
-
"view",
|
|
134
|
-
"--json",
|
|
135
|
-
"number",
|
|
136
|
-
"--jq",
|
|
137
|
-
".number",
|
|
138
|
-
"feature",
|
|
139
|
-
])
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
test("does not append emitted branch when an explicit selector is provided", async () => {
|
|
143
|
-
await createBranch(tempDir, "agency--feature")
|
|
144
|
-
|
|
145
|
-
await runTestEffect(
|
|
146
|
-
pr({
|
|
147
|
-
args: [
|
|
148
|
-
"view",
|
|
149
|
-
"123",
|
|
150
|
-
"--json",
|
|
151
|
-
"statusCheckRollup",
|
|
152
|
-
"--jq",
|
|
153
|
-
".statusCheckRollup[]",
|
|
154
|
-
],
|
|
155
|
-
silent: false,
|
|
156
|
-
}),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
expect(await readGhArgs(recordPath)).toEqual([
|
|
160
|
-
"pr",
|
|
161
|
-
"view",
|
|
162
|
-
"123",
|
|
163
|
-
"--json",
|
|
164
|
-
"statusCheckRollup",
|
|
165
|
-
"--jq",
|
|
166
|
-
".statusCheckRollup[]",
|
|
167
|
-
])
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
test("does not append emitted branch for subcommands without a selector", async () => {
|
|
171
|
-
await createBranch(tempDir, "agency--feature")
|
|
172
|
-
|
|
173
|
-
await runTestEffect(pr({ args: ["status"], silent: false }))
|
|
174
23
|
|
|
175
|
-
expect(
|
|
24
|
+
expect(JSON.parse(logs[0]!)).toEqual({ url })
|
|
176
25
|
})
|
|
177
26
|
})
|
package/src/commands/pr.ts
CHANGED
|
@@ -1,196 +1,40 @@
|
|
|
1
1
|
import { Effect } from "effect"
|
|
2
2
|
import type { BaseCommandOptions } from "../utils/command"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { resolveAgencyBranchPairWithAgencyJson } from "../utils/pr-branch"
|
|
6
|
-
import { ensureGitRepo } from "../utils/effect"
|
|
3
|
+
import { PullRequestService } from "../services/PullRequestService"
|
|
4
|
+
import { createLoggers } from "../utils/effect"
|
|
7
5
|
|
|
8
6
|
interface PrOptions extends BaseCommandOptions {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const PR_SUBCOMMANDS_WITH_OPTIONAL_SELECTOR = new Set([
|
|
14
|
-
"checkout",
|
|
15
|
-
"checks",
|
|
16
|
-
"close",
|
|
17
|
-
"comment",
|
|
18
|
-
"diff",
|
|
19
|
-
"edit",
|
|
20
|
-
"lock",
|
|
21
|
-
"merge",
|
|
22
|
-
"ready",
|
|
23
|
-
"reopen",
|
|
24
|
-
"review",
|
|
25
|
-
"unlock",
|
|
26
|
-
"update-branch",
|
|
27
|
-
"view",
|
|
28
|
-
])
|
|
29
|
-
|
|
30
|
-
const GH_PR_FLAGS_WITH_VALUE = new Set([
|
|
31
|
-
"--add-assignee",
|
|
32
|
-
"--add-label",
|
|
33
|
-
"--add-project",
|
|
34
|
-
"--add-reviewer",
|
|
35
|
-
"--app",
|
|
36
|
-
"--assignee",
|
|
37
|
-
"--author",
|
|
38
|
-
"--base",
|
|
39
|
-
"--body",
|
|
40
|
-
"--body-file",
|
|
41
|
-
"--head",
|
|
42
|
-
"--hostname",
|
|
43
|
-
"--json",
|
|
44
|
-
"--jq",
|
|
45
|
-
"--label",
|
|
46
|
-
"--limit",
|
|
47
|
-
"--match-head-commit",
|
|
48
|
-
"--milestone",
|
|
49
|
-
"--project",
|
|
50
|
-
"--remove-assignee",
|
|
51
|
-
"--remove-label",
|
|
52
|
-
"--remove-project",
|
|
53
|
-
"--remove-reviewer",
|
|
54
|
-
"--repo",
|
|
55
|
-
"--reviewer",
|
|
56
|
-
"--search",
|
|
57
|
-
"--state",
|
|
58
|
-
"--template",
|
|
59
|
-
"--title",
|
|
60
|
-
])
|
|
61
|
-
|
|
62
|
-
const GH_PR_SHORT_FLAGS_WITH_VALUE = new Set([
|
|
63
|
-
"-A",
|
|
64
|
-
"-B",
|
|
65
|
-
"-H",
|
|
66
|
-
"-L",
|
|
67
|
-
"-R",
|
|
68
|
-
"-a",
|
|
69
|
-
"-b",
|
|
70
|
-
"-l",
|
|
71
|
-
"-m",
|
|
72
|
-
"-p",
|
|
73
|
-
"-q",
|
|
74
|
-
"-r",
|
|
75
|
-
"-s",
|
|
76
|
-
"-t",
|
|
77
|
-
])
|
|
78
|
-
|
|
79
|
-
const hasExplicitPrSelector = (args: readonly string[]): boolean => {
|
|
80
|
-
const subcommandArgs = args.slice(1)
|
|
81
|
-
|
|
82
|
-
for (let i = 0; i < subcommandArgs.length; i++) {
|
|
83
|
-
const arg = subcommandArgs[i]
|
|
84
|
-
|
|
85
|
-
if (!arg) {
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (arg === "--") {
|
|
90
|
-
return subcommandArgs.slice(i + 1).some(Boolean)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (arg.startsWith("--")) {
|
|
94
|
-
const flagName = arg.includes("=") ? arg.slice(0, arg.indexOf("=")) : arg
|
|
95
|
-
|
|
96
|
-
if (!arg.includes("=") && GH_PR_FLAGS_WITH_VALUE.has(flagName)) {
|
|
97
|
-
i++
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (arg.startsWith("-") && arg.length > 1) {
|
|
104
|
-
if (GH_PR_SHORT_FLAGS_WITH_VALUE.has(arg)) {
|
|
105
|
-
i++
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
continue
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return true
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return false
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const shouldAppendEmitBranch = (args: readonly string[]): boolean => {
|
|
118
|
-
const subcommand = args[0]
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
typeof subcommand === "string" &&
|
|
122
|
-
PR_SUBCOMMANDS_WITH_OPTIONAL_SELECTOR.has(subcommand) &&
|
|
123
|
-
!hasExplicitPrSelector(args)
|
|
124
|
-
)
|
|
7
|
+
readonly subcommand?: string
|
|
8
|
+
readonly taskId?: string
|
|
9
|
+
readonly phaseId?: string
|
|
10
|
+
readonly draft?: boolean
|
|
125
11
|
}
|
|
126
12
|
|
|
127
13
|
export const pr = (options: PrOptions) =>
|
|
128
14
|
Effect.gen(function* () {
|
|
129
|
-
|
|
130
|
-
const configService = yield* ConfigService
|
|
131
|
-
|
|
132
|
-
const gitRoot = yield* ensureGitRepo()
|
|
133
|
-
|
|
134
|
-
// Load config
|
|
135
|
-
const config = yield* configService.loadConfig()
|
|
136
|
-
|
|
137
|
-
// Get current branch and resolve the branch pair when agency context exists
|
|
138
|
-
const currentBranch = yield* git.getCurrentBranch(gitRoot)
|
|
139
|
-
const branches = yield* resolveAgencyBranchPairWithAgencyJson(
|
|
140
|
-
gitRoot,
|
|
141
|
-
currentBranch,
|
|
142
|
-
config.sourceBranchPattern,
|
|
143
|
-
config.emitBranch,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
// Build the gh pr command with the emit branch only when gh accepts a selector.
|
|
147
|
-
const ghArgs =
|
|
148
|
-
branches && shouldAppendEmitBranch(options.args)
|
|
149
|
-
? ["gh", "pr", ...options.args, branches.emitBranch]
|
|
150
|
-
: ["gh", "pr", ...options.args]
|
|
151
|
-
|
|
152
|
-
// Run gh pr with stdio inherited so output goes directly to terminal
|
|
153
|
-
const exitCode = yield* Effect.tryPromise({
|
|
154
|
-
try: async () => {
|
|
155
|
-
const proc = Bun.spawn(ghArgs, {
|
|
156
|
-
cwd: gitRoot,
|
|
157
|
-
env: process.env,
|
|
158
|
-
stdin: "inherit",
|
|
159
|
-
stdout: "inherit",
|
|
160
|
-
stderr: "inherit",
|
|
161
|
-
})
|
|
162
|
-
return proc.exited
|
|
163
|
-
},
|
|
164
|
-
catch: (error) =>
|
|
165
|
-
new Error(
|
|
166
|
-
`Failed to run gh pr: ${error instanceof Error ? error.message : String(error)}`,
|
|
167
|
-
),
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
if (exitCode !== 0) {
|
|
15
|
+
if (options.subcommand !== "create" || !options.taskId) {
|
|
171
16
|
return yield* Effect.fail(
|
|
172
|
-
new Error(
|
|
17
|
+
new Error("Usage: agency pr create <task-id> [phase-id]"),
|
|
173
18
|
)
|
|
174
19
|
}
|
|
20
|
+
const pullRequests = yield* PullRequestService
|
|
21
|
+
const { log } = createLoggers(options)
|
|
22
|
+
const url = yield* pullRequests.create(
|
|
23
|
+
options.taskId,
|
|
24
|
+
options.phaseId,
|
|
25
|
+
options.draft,
|
|
26
|
+
options.cwd ?? process.cwd(),
|
|
27
|
+
)
|
|
28
|
+
log(options.json ? JSON.stringify({ url }, null, 2) : url)
|
|
175
29
|
})
|
|
176
30
|
|
|
177
31
|
export const help = `
|
|
178
|
-
Usage: agency pr <
|
|
179
|
-
|
|
180
|
-
Wrapper for 'gh pr' that automatically uses the emitted branch in agency context.
|
|
181
|
-
|
|
182
|
-
For gh pr subcommands that accept a PR selector, this command appends the
|
|
183
|
-
emitted branch name when you do not provide a selector. Outside agency context,
|
|
184
|
-
or when you provide a selector, it passes arguments through to 'gh pr' unchanged.
|
|
32
|
+
Usage: agency pr create <task-id> [phase-id]
|
|
185
33
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
agency pr checks # gh pr checks <emit-branch>
|
|
189
|
-
agency pr diff # gh pr diff <emit-branch>
|
|
34
|
+
Push the execution branch, create a GitHub pull request, and update its
|
|
35
|
+
task or phase document.
|
|
190
36
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
- Respects emitBranch field in agency.json when present
|
|
195
|
-
- Falls through to gh pr unchanged outside agency context
|
|
37
|
+
Options:
|
|
38
|
+
--draft Create a draft pull request
|
|
39
|
+
--json Output the pull request URL as JSON
|
|
196
40
|
`
|