@markjaquith/agency 1.9.6 → 1.11.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 +25 -0
- package/cli.ts +17 -0
- package/package.json +1 -1
- package/src/commands/completions.test.ts +51 -0
- package/src/commands/completions.ts +308 -0
- package/src/commands/emit.test.ts +23 -0
- package/src/commands/emit.ts +15 -0
- package/src/commands/push.test.ts +32 -4
- package/src/commands/push.ts +15 -0
- package/templates/AGENCY.md +2 -0
package/README.md
CHANGED
|
@@ -104,6 +104,31 @@ Toggle between source branch and emit branch. If on an emit branch (e.g., `foo--
|
|
|
104
104
|
|
|
105
105
|
Switch to the source branch for the current emit branch.
|
|
106
106
|
|
|
107
|
+
### `agency completions <bash|zsh>`
|
|
108
|
+
|
|
109
|
+
Generate shell completion scripts. The generated scripts are static, so shell startup and tab completion do not call `agency`.
|
|
110
|
+
|
|
111
|
+
**zsh:**
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
mkdir -p ~/.zsh/completions
|
|
115
|
+
agency completions zsh > ~/.zsh/completions/_agency
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Then add this to `~/.zshrc` before `compinit`:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
fpath=(~/.zsh/completions $fpath)
|
|
122
|
+
autoload -Uz compinit && compinit
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**bash:**
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
mkdir -p ~/.local/share/bash-completion/completions
|
|
129
|
+
agency completions bash > ~/.local/share/bash-completion/completions/agency
|
|
130
|
+
```
|
|
131
|
+
|
|
107
132
|
## Requirements
|
|
108
133
|
|
|
109
134
|
- [Bun](https://bun.sh) >= 1.0.0 (recommended)
|
package/cli.ts
CHANGED
|
@@ -20,6 +20,10 @@ import { template, help as templateHelp } from "./src/commands/template"
|
|
|
20
20
|
import { work, help as workHelp } from "./src/commands/work"
|
|
21
21
|
import { loop, help as loopHelp } from "./src/commands/loop"
|
|
22
22
|
import { status, help as statusHelp } from "./src/commands/status"
|
|
23
|
+
import {
|
|
24
|
+
completions,
|
|
25
|
+
help as completionsHelp,
|
|
26
|
+
} from "./src/commands/completions"
|
|
23
27
|
import type { Command } from "./src/types"
|
|
24
28
|
import { setColorsEnabled } from "./src/utils/colors"
|
|
25
29
|
import { GitService } from "./src/services/GitService"
|
|
@@ -469,6 +473,18 @@ const commands: Record<string, Command> = {
|
|
|
469
473
|
},
|
|
470
474
|
help: loopHelp,
|
|
471
475
|
},
|
|
476
|
+
completions: {
|
|
477
|
+
name: "completions",
|
|
478
|
+
description: "Generate shell completion scripts",
|
|
479
|
+
run: async (args: string[], options: Record<string, any>) => {
|
|
480
|
+
if (options.help) {
|
|
481
|
+
console.log(completionsHelp)
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
await runCommand(completions(args[0]))
|
|
485
|
+
},
|
|
486
|
+
help: completionsHelp,
|
|
487
|
+
},
|
|
472
488
|
}
|
|
473
489
|
|
|
474
490
|
function showMainHelp() {
|
|
@@ -502,6 +518,7 @@ Commands:
|
|
|
502
518
|
source Switch to source branch from emitted branch
|
|
503
519
|
merge Merge emitted branch into base branch
|
|
504
520
|
status Show agency status for this repository
|
|
521
|
+
completions <shell> Generate bash or zsh completion scripts
|
|
505
522
|
|
|
506
523
|
Global Options:
|
|
507
524
|
-h, --help Show help for a command
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { completions } from "./completions"
|
|
4
|
+
|
|
5
|
+
const captureCompletions = async (shell: string) => {
|
|
6
|
+
const originalLog = console.log
|
|
7
|
+
let output = ""
|
|
8
|
+
console.log = (message: string) => {
|
|
9
|
+
output += message
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await Effect.runPromise(completions(shell))
|
|
14
|
+
} finally {
|
|
15
|
+
console.log = originalLog
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return output
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("completions command", () => {
|
|
22
|
+
test("generates bash completions", async () => {
|
|
23
|
+
const output = await captureCompletions("bash")
|
|
24
|
+
|
|
25
|
+
expect(output).toContain("_agency_completions()")
|
|
26
|
+
expect(output).toContain("complete -F _agency_completions agency")
|
|
27
|
+
expect(output).toContain("completions")
|
|
28
|
+
expect(output).toContain("bash zsh")
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("generates zsh completions", async () => {
|
|
32
|
+
const output = await captureCompletions("zsh")
|
|
33
|
+
|
|
34
|
+
expect(output).toContain("#compdef agency")
|
|
35
|
+
expect(output).toContain("_agency()")
|
|
36
|
+
expect(output).toContain("agency_commands=(")
|
|
37
|
+
expect(output).toContain("bash\\:Generate\\ bash\\ completions")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("prints the requested completion script", async () => {
|
|
41
|
+
const output = await captureCompletions("zsh")
|
|
42
|
+
|
|
43
|
+
expect(output).toContain("#compdef agency")
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("rejects unsupported shells", async () => {
|
|
47
|
+
await expect(Effect.runPromise(completions("fish"))).rejects.toThrow(
|
|
48
|
+
"Usage: agency completions <bash|zsh>",
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
|
|
3
|
+
const shells = ["bash", "zsh"] as const
|
|
4
|
+
type Shell = (typeof shells)[number]
|
|
5
|
+
|
|
6
|
+
const commands = [
|
|
7
|
+
"init",
|
|
8
|
+
"task",
|
|
9
|
+
"tasks",
|
|
10
|
+
"edit",
|
|
11
|
+
"work",
|
|
12
|
+
"template",
|
|
13
|
+
"emit",
|
|
14
|
+
"emitted",
|
|
15
|
+
"pr",
|
|
16
|
+
"push",
|
|
17
|
+
"pull",
|
|
18
|
+
"rebase",
|
|
19
|
+
"base",
|
|
20
|
+
"switch",
|
|
21
|
+
"source",
|
|
22
|
+
"merge",
|
|
23
|
+
"status",
|
|
24
|
+
"clean",
|
|
25
|
+
"loop",
|
|
26
|
+
"completions",
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
const templateSubcommands = ["use", "save", "list", "view", "delete"] as const
|
|
30
|
+
const baseSubcommands = ["get", "set"] as const
|
|
31
|
+
const completionSubcommands = shells
|
|
32
|
+
|
|
33
|
+
const globalOptions = [
|
|
34
|
+
"--help",
|
|
35
|
+
"-h",
|
|
36
|
+
"--version",
|
|
37
|
+
"-v",
|
|
38
|
+
"--no-color",
|
|
39
|
+
"--silent",
|
|
40
|
+
"-s",
|
|
41
|
+
"--verbose",
|
|
42
|
+
] as const
|
|
43
|
+
|
|
44
|
+
const commandOptions: Record<string, readonly string[]> = {
|
|
45
|
+
init: [
|
|
46
|
+
"--help",
|
|
47
|
+
"-h",
|
|
48
|
+
"--template",
|
|
49
|
+
"-t",
|
|
50
|
+
"--silent",
|
|
51
|
+
"-s",
|
|
52
|
+
"--verbose",
|
|
53
|
+
"-v",
|
|
54
|
+
],
|
|
55
|
+
emit: [
|
|
56
|
+
"--help",
|
|
57
|
+
"-h",
|
|
58
|
+
"--emit",
|
|
59
|
+
"--branch",
|
|
60
|
+
"--silent",
|
|
61
|
+
"-s",
|
|
62
|
+
"--verbose",
|
|
63
|
+
"-v",
|
|
64
|
+
],
|
|
65
|
+
push: [
|
|
66
|
+
"--help",
|
|
67
|
+
"-h",
|
|
68
|
+
"--emit",
|
|
69
|
+
"--branch",
|
|
70
|
+
"--force",
|
|
71
|
+
"-f",
|
|
72
|
+
"--no-verify",
|
|
73
|
+
"--pr",
|
|
74
|
+
"--silent",
|
|
75
|
+
"-s",
|
|
76
|
+
"--verbose",
|
|
77
|
+
"-v",
|
|
78
|
+
],
|
|
79
|
+
pull: ["--help", "-h", "--remote", "-r", "--silent", "-s", "--verbose", "-v"],
|
|
80
|
+
rebase: [
|
|
81
|
+
"--help",
|
|
82
|
+
"-h",
|
|
83
|
+
"--emit",
|
|
84
|
+
"--branch",
|
|
85
|
+
"--silent",
|
|
86
|
+
"-s",
|
|
87
|
+
"--verbose",
|
|
88
|
+
"-v",
|
|
89
|
+
],
|
|
90
|
+
task: [
|
|
91
|
+
"--help",
|
|
92
|
+
"-h",
|
|
93
|
+
"--emit",
|
|
94
|
+
"--branch",
|
|
95
|
+
"--task",
|
|
96
|
+
"--from",
|
|
97
|
+
"--from-current",
|
|
98
|
+
"--continue",
|
|
99
|
+
"--squash",
|
|
100
|
+
"--silent",
|
|
101
|
+
"-s",
|
|
102
|
+
"--verbose",
|
|
103
|
+
"-v",
|
|
104
|
+
],
|
|
105
|
+
tasks: ["--help", "-h", "--json", "--silent", "-s", "--verbose", "-v"],
|
|
106
|
+
base: ["--help", "-h", "--repo", "--silent", "-s", "--verbose", "-v"],
|
|
107
|
+
template: [
|
|
108
|
+
"--help",
|
|
109
|
+
"-h",
|
|
110
|
+
"--template",
|
|
111
|
+
"-t",
|
|
112
|
+
"--silent",
|
|
113
|
+
"-s",
|
|
114
|
+
"--verbose",
|
|
115
|
+
"-v",
|
|
116
|
+
],
|
|
117
|
+
work: [
|
|
118
|
+
"--help",
|
|
119
|
+
"-h",
|
|
120
|
+
"--opencode",
|
|
121
|
+
"--claude",
|
|
122
|
+
"--silent",
|
|
123
|
+
"-s",
|
|
124
|
+
"--verbose",
|
|
125
|
+
"-v",
|
|
126
|
+
],
|
|
127
|
+
loop: [
|
|
128
|
+
"--help",
|
|
129
|
+
"-h",
|
|
130
|
+
"--min-loops",
|
|
131
|
+
"--max-loops",
|
|
132
|
+
"--opencode",
|
|
133
|
+
"--claude",
|
|
134
|
+
"--silent",
|
|
135
|
+
"-s",
|
|
136
|
+
"--verbose",
|
|
137
|
+
"-v",
|
|
138
|
+
],
|
|
139
|
+
status: ["--help", "-h", "--json", "--silent", "-s", "--verbose", "-v"],
|
|
140
|
+
clean: [
|
|
141
|
+
"--help",
|
|
142
|
+
"-h",
|
|
143
|
+
"--dry-run",
|
|
144
|
+
"--merged-into",
|
|
145
|
+
"--silent",
|
|
146
|
+
"-s",
|
|
147
|
+
"--verbose",
|
|
148
|
+
"-v",
|
|
149
|
+
],
|
|
150
|
+
completions: ["--help", "-h"],
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const words = (values: readonly string[]) => values.join(" ")
|
|
154
|
+
|
|
155
|
+
const bashCase = (command: string, subcommands?: readonly string[]) => {
|
|
156
|
+
const options = commandOptions[command] ?? [
|
|
157
|
+
"--help",
|
|
158
|
+
"-h",
|
|
159
|
+
"--silent",
|
|
160
|
+
"-s",
|
|
161
|
+
"--verbose",
|
|
162
|
+
"-v",
|
|
163
|
+
]
|
|
164
|
+
const completions = subcommands ? [...subcommands, ...options] : options
|
|
165
|
+
return `\t\t${command})\n\t\t\tCOMPREPLY=( $(compgen -W "${words(completions)}" -- "$cur") )\n\t\t\treturn 0\n\t\t\t;;`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const zshCommandSpec = (command: string, description: string) =>
|
|
169
|
+
` '${command}:${description}'`
|
|
170
|
+
|
|
171
|
+
const generateBashCompletions = () => `# bash completion for agency
|
|
172
|
+
_agency_completions() {
|
|
173
|
+
local cur prev command
|
|
174
|
+
COMPREPLY=()
|
|
175
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
176
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
177
|
+
|
|
178
|
+
if [[ $COMP_CWORD -eq 1 ]]; then
|
|
179
|
+
COMPREPLY=( $(compgen -W "${words([...commands, ...globalOptions])}" -- "$cur") )
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
command="\${COMP_WORDS[1]}"
|
|
184
|
+
case "$command" in
|
|
185
|
+
${bashCase("template", templateSubcommands)}
|
|
186
|
+
${bashCase("base", baseSubcommands)}
|
|
187
|
+
${bashCase("completions", completionSubcommands)}
|
|
188
|
+
${commands
|
|
189
|
+
.filter((command) => !["template", "base", "completions"].includes(command))
|
|
190
|
+
.map((command) => bashCase(command))
|
|
191
|
+
.join("\n")}
|
|
192
|
+
esac
|
|
193
|
+
|
|
194
|
+
case "$prev" in
|
|
195
|
+
--template|-t|--emit|--branch|--task|--from|--remote|-r|--merged-into)
|
|
196
|
+
return 0
|
|
197
|
+
;;
|
|
198
|
+
esac
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
complete -F _agency_completions agency
|
|
202
|
+
`
|
|
203
|
+
|
|
204
|
+
const generateZshCompletions = () => `#compdef agency
|
|
205
|
+
# zsh completion for agency
|
|
206
|
+
|
|
207
|
+
_agency() {
|
|
208
|
+
local context state line
|
|
209
|
+
local -a agency_commands
|
|
210
|
+
typeset -A opt_args
|
|
211
|
+
|
|
212
|
+
agency_commands=(
|
|
213
|
+
${zshCommandSpec("init", "Initialize agency with template selection")}
|
|
214
|
+
${zshCommandSpec("task", "Initialize template files on a feature branch")}
|
|
215
|
+
${zshCommandSpec("tasks", "List all task branches")}
|
|
216
|
+
${zshCommandSpec("edit", "Open TASK.md in system editor")}
|
|
217
|
+
${zshCommandSpec("work", "Start working on TASK.md with OpenCode")}
|
|
218
|
+
${zshCommandSpec("template", "Template management commands")}
|
|
219
|
+
${zshCommandSpec("emit", "Emit a branch with backpack files reverted")}
|
|
220
|
+
${zshCommandSpec("emitted", "Get the name of the emitted branch")}
|
|
221
|
+
${zshCommandSpec("pr", "Run gh pr with the emitted branch name")}
|
|
222
|
+
${zshCommandSpec("push", "Emit, push to remote, return to source")}
|
|
223
|
+
${zshCommandSpec("pull", "Pull commits from remote emit branch to source")}
|
|
224
|
+
${zshCommandSpec("rebase", "Rebase source branch onto base branch")}
|
|
225
|
+
${zshCommandSpec("base", "Get or set the base branch")}
|
|
226
|
+
${zshCommandSpec("switch", "Toggle between source and emitted branch")}
|
|
227
|
+
${zshCommandSpec("source", "Switch to source branch from emitted branch")}
|
|
228
|
+
${zshCommandSpec("merge", "Merge emitted branch into base branch")}
|
|
229
|
+
${zshCommandSpec("status", "Show agency status for this repository")}
|
|
230
|
+
${zshCommandSpec("clean", "Delete branches merged into a specified branch")}
|
|
231
|
+
${zshCommandSpec("loop", "Run a Ralph Wiggum loop to complete all tasks")}
|
|
232
|
+
${zshCommandSpec("completions", "Generate shell completion scripts")}
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
_arguments -C \\
|
|
236
|
+
'(-h --help)'{-h,--help}'[show help]' \\
|
|
237
|
+
'(-v --version)'{-v,--version}'[show version]' \\
|
|
238
|
+
'--no-color[disable color output]' \\
|
|
239
|
+
'(-s --silent)'{-s,--silent}'[suppress output messages]' \\
|
|
240
|
+
'--verbose[show verbose output]' \\
|
|
241
|
+
'1:command:->command' \\
|
|
242
|
+
'*::arg:->args'
|
|
243
|
+
|
|
244
|
+
case $state in
|
|
245
|
+
command)
|
|
246
|
+
_describe 'agency command' agency_commands
|
|
247
|
+
;;
|
|
248
|
+
args)
|
|
249
|
+
case $line[1] in
|
|
250
|
+
template)
|
|
251
|
+
_arguments \\
|
|
252
|
+
'(-h --help)'{-h,--help}'[show help]' \\
|
|
253
|
+
'(-t --template)'{-t,--template}'[template name]:template:' \\
|
|
254
|
+
'(-s --silent)'{-s,--silent}'[suppress output messages]' \\
|
|
255
|
+
'--verbose[show verbose output]' \\
|
|
256
|
+
'1:subcommand:((use\\:Set\\ template save\\:Save\\ files list\\:List\\ files view\\:View\\ file delete\\:Delete\\ files))' \\
|
|
257
|
+
'*:file:_files'
|
|
258
|
+
;;
|
|
259
|
+
base)
|
|
260
|
+
_arguments \\
|
|
261
|
+
'(-h --help)'{-h,--help}'[show help]' \\
|
|
262
|
+
'--repo[use repository-local configuration]' \\
|
|
263
|
+
'(-s --silent)'{-s,--silent}'[suppress output messages]' \\
|
|
264
|
+
'--verbose[show verbose output]' \\
|
|
265
|
+
'1:subcommand:((get\\:Get\\ base\\ branch set\\:Set\\ base\\ branch))' \\
|
|
266
|
+
'*:branch:'
|
|
267
|
+
;;
|
|
268
|
+
completions)
|
|
269
|
+
_arguments \\
|
|
270
|
+
'(-h --help)'{-h,--help}'[show help]' \\
|
|
271
|
+
'1:shell:((bash\\:Generate\\ bash\\ completions zsh\\:Generate\\ zsh\\ completions))'
|
|
272
|
+
;;
|
|
273
|
+
*)
|
|
274
|
+
_arguments \\
|
|
275
|
+
'(-h --help)'{-h,--help}'[show help]' \\
|
|
276
|
+
'(-s --silent)'{-s,--silent}'[suppress output messages]' \\
|
|
277
|
+
'--verbose[show verbose output]'
|
|
278
|
+
;;
|
|
279
|
+
esac
|
|
280
|
+
;;
|
|
281
|
+
esac
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_agency "$@"
|
|
285
|
+
`
|
|
286
|
+
|
|
287
|
+
export const completions = (shell: string | undefined) =>
|
|
288
|
+
Effect.gen(function* () {
|
|
289
|
+
if (!shell || !shells.includes(shell as Shell)) {
|
|
290
|
+
return yield* Effect.fail(
|
|
291
|
+
new Error("Usage: agency completions <bash|zsh>"),
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log(
|
|
296
|
+
shell === "bash" ? generateBashCompletions() : generateZshCompletions(),
|
|
297
|
+
)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
export const help = `
|
|
301
|
+
Usage: agency completions <bash|zsh>
|
|
302
|
+
|
|
303
|
+
Generate shell completion scripts.
|
|
304
|
+
|
|
305
|
+
Examples:
|
|
306
|
+
agency completions zsh > ~/.zsh/completions/_agency
|
|
307
|
+
agency completions bash > ~/.local/share/bash-completion/completions/agency
|
|
308
|
+
`
|
|
@@ -78,6 +78,29 @@ describe("emit command", () => {
|
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
describe("basic functionality", () => {
|
|
81
|
+
test("fails when there are uncommitted changes", async () => {
|
|
82
|
+
// Create uncommitted changes
|
|
83
|
+
await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
runTestEffect(emit({ silent: true, skipFilter: true })),
|
|
87
|
+
).rejects.toThrow(/uncommitted changes/)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test("fails when there are staged but uncommitted changes", async () => {
|
|
91
|
+
// Create a staged but uncommitted change
|
|
92
|
+
await Bun.write(join(tempDir, "staged.txt"), "staged\n")
|
|
93
|
+
await Bun.spawn(["git", "add", "staged.txt"], {
|
|
94
|
+
cwd: tempDir,
|
|
95
|
+
stdout: "pipe",
|
|
96
|
+
stderr: "pipe",
|
|
97
|
+
}).exited
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
runTestEffect(emit({ silent: true, skipFilter: true })),
|
|
101
|
+
).rejects.toThrow(/uncommitted changes/)
|
|
102
|
+
})
|
|
103
|
+
|
|
81
104
|
test("throws error when git-filter-repo is not installed", async () => {
|
|
82
105
|
const hasGitFilterRepo = await checkGitFilterRepo()
|
|
83
106
|
if (hasGitFilterRepo) {
|
package/src/commands/emit.ts
CHANGED
|
@@ -54,6 +54,21 @@ export const emitCore = (gitRoot: string, options: EmitOptions) =>
|
|
|
54
54
|
const fs = yield* FileSystemService
|
|
55
55
|
const filterRepo = yield* FilterRepoService
|
|
56
56
|
|
|
57
|
+
// Check for uncommitted changes
|
|
58
|
+
const statusOutput = yield* git.getStatus(gitRoot)
|
|
59
|
+
|
|
60
|
+
if (statusOutput && statusOutput.trim().length > 0) {
|
|
61
|
+
return yield* Effect.fail(
|
|
62
|
+
new Error(
|
|
63
|
+
`You have uncommitted changes. Please commit or stash them before emitting.\n` +
|
|
64
|
+
`The emit command performs internal checkouts that would destroy uncommitted work.\n` +
|
|
65
|
+
`Run 'git status' to see the changes.`,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
verboseLog(`Working directory is clean`)
|
|
71
|
+
|
|
57
72
|
// Check if git-filter-repo is installed
|
|
58
73
|
const hasFilterRepo = yield* filterRepo.isInstalled()
|
|
59
74
|
if (!hasFilterRepo) {
|
|
@@ -30,9 +30,9 @@ async function setupAgencyJson(gitRoot: string): Promise<void> {
|
|
|
30
30
|
await addAndCommit(gitRoot, "agency.json", "Add agency.json")
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
async function setupBareRemote(
|
|
34
|
-
// Create a bare repository to
|
|
35
|
-
const remoteDir =
|
|
33
|
+
async function setupBareRemote(): Promise<string> {
|
|
34
|
+
// Create a bare repository outside the working tree to avoid polluting git status
|
|
35
|
+
const remoteDir = await createTempDir()
|
|
36
36
|
await Bun.spawn(["git", "init", "--bare", remoteDir], {
|
|
37
37
|
stdout: "pipe",
|
|
38
38
|
stderr: "pipe",
|
|
@@ -64,7 +64,7 @@ describe("push command", () => {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Setup bare remote and push main
|
|
67
|
-
remoteDir = await setupBareRemote(
|
|
67
|
+
remoteDir = await setupBareRemote()
|
|
68
68
|
await Bun.spawn(["git", "remote", "add", "origin", remoteDir], {
|
|
69
69
|
cwd: tempDir,
|
|
70
70
|
stdout: "pipe",
|
|
@@ -88,9 +88,37 @@ describe("push command", () => {
|
|
|
88
88
|
process.chdir(originalCwd)
|
|
89
89
|
delete process.env.AGENCY_CONFIG_PATH
|
|
90
90
|
await cleanupTempDir(tempDir)
|
|
91
|
+
await cleanupTempDir(remoteDir)
|
|
91
92
|
})
|
|
92
93
|
|
|
93
94
|
describe("basic functionality", () => {
|
|
95
|
+
test("fails when there are uncommitted changes", async () => {
|
|
96
|
+
// Create uncommitted changes
|
|
97
|
+
await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
|
|
98
|
+
|
|
99
|
+
expect(
|
|
100
|
+
runTestEffect(
|
|
101
|
+
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
102
|
+
),
|
|
103
|
+
).rejects.toThrow(/uncommitted changes/)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("fails when there are staged but uncommitted changes", async () => {
|
|
107
|
+
// Create a staged but uncommitted change
|
|
108
|
+
await Bun.write(join(tempDir, "staged.txt"), "staged\n")
|
|
109
|
+
await Bun.spawn(["git", "add", "staged.txt"], {
|
|
110
|
+
cwd: tempDir,
|
|
111
|
+
stdout: "pipe",
|
|
112
|
+
stderr: "pipe",
|
|
113
|
+
}).exited
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
runTestEffect(
|
|
117
|
+
push({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
118
|
+
),
|
|
119
|
+
).rejects.toThrow(/uncommitted changes/)
|
|
120
|
+
})
|
|
121
|
+
|
|
94
122
|
test("creates emit branch, pushes it, and returns to source", async () => {
|
|
95
123
|
// We're on agency--feature branch (source)
|
|
96
124
|
expect(await getCurrentBranch(tempDir)).toBe("agency--feature")
|
package/src/commands/push.ts
CHANGED
|
@@ -91,6 +91,21 @@ const pushCore = (gitRoot: string, options: PushOptions) =>
|
|
|
91
91
|
const git = yield* GitService
|
|
92
92
|
const configService = yield* ConfigService
|
|
93
93
|
|
|
94
|
+
// Check for uncommitted changes
|
|
95
|
+
const statusOutput = yield* git.getStatus(gitRoot)
|
|
96
|
+
|
|
97
|
+
if (statusOutput && statusOutput.trim().length > 0) {
|
|
98
|
+
return yield* Effect.fail(
|
|
99
|
+
new Error(
|
|
100
|
+
`You have uncommitted changes. Please commit or stash them before pushing.\n` +
|
|
101
|
+
`The push command performs internal checkouts that would destroy uncommitted work.\n` +
|
|
102
|
+
`Run 'git status' to see the changes.`,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
verboseLog(`Working directory is clean`)
|
|
108
|
+
|
|
94
109
|
// Load config to check emit branch pattern
|
|
95
110
|
const config = yield* configService.loadConfig()
|
|
96
111
|
|
package/templates/AGENCY.md
CHANGED
|
@@ -18,3 +18,5 @@ All work on this repository should begin by reading and understanding `TASK.md`.
|
|
|
18
18
|
When creating commit messages, do not reference changes to `TASK.md`, `AGENTS.md`, or any files tracked in `agency.json` (such as `opencode.json`). These are project management and configuration files that should not be mentioned in commit messages. Focus commit messages on actual code changes, features, fixes, and refactoring.
|
|
19
19
|
|
|
20
20
|
**Important:** Even when the only changes in a commit are to tracked files like `TASK.md`, you should still commit those changes. These updates should be co-located with the code changes they describe. Simply omit mentioning the tracked files in the commit message and focus the message on the actual code changes being made.
|
|
21
|
+
|
|
22
|
+
If a commit only updates `TASK.md`, use `--no-verify` to skip precommit hooks. Do not use `--no-verify` if the commit includes changes to any other file.
|