@markjaquith/agency 1.11.1 → 2.1.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 +313 -117
- package/cli.ts +124 -420
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +266 -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 +326 -399
- package/src/commands/work.ts +153 -153
- 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/work-target.test.ts +77 -0
- package/src/workbase/work-target.ts +180 -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
package/src/commands/rebase.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect"
|
|
2
|
-
import type { BaseCommandOptions } from "../utils/command"
|
|
3
|
-
import { GitService } from "../services/GitService"
|
|
4
|
-
import { FileSystemService } from "../services/FileSystemService"
|
|
5
|
-
import { AgencyMetadataService } from "../services/AgencyMetadataService"
|
|
6
|
-
import highlight, { done, info } from "../utils/colors"
|
|
7
|
-
import {
|
|
8
|
-
createLoggers,
|
|
9
|
-
ensureGitRepo,
|
|
10
|
-
resolveBaseBranch,
|
|
11
|
-
withBranchProtection,
|
|
12
|
-
} from "../utils/effect"
|
|
13
|
-
import { withSpinner } from "../utils/spinner"
|
|
14
|
-
|
|
15
|
-
interface RebaseOptions extends BaseCommandOptions {
|
|
16
|
-
baseBranch?: string // Internal option, populated from positional arg
|
|
17
|
-
emit?: string // Custom emit branch name to set after rebase
|
|
18
|
-
branch?: string // Deprecated: use emit instead
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const rebase = (options: RebaseOptions = {}) =>
|
|
22
|
-
Effect.gen(function* () {
|
|
23
|
-
const gitRoot = yield* ensureGitRepo()
|
|
24
|
-
|
|
25
|
-
// Wrap the entire rebase operation with branch protection
|
|
26
|
-
yield* withBranchProtection(gitRoot, rebaseCore(gitRoot, options))
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const rebaseCore = (gitRoot: string, options: RebaseOptions) =>
|
|
30
|
-
Effect.gen(function* () {
|
|
31
|
-
const { verbose = false } = options
|
|
32
|
-
const { log, verboseLog } = createLoggers(options)
|
|
33
|
-
|
|
34
|
-
const git = yield* GitService
|
|
35
|
-
|
|
36
|
-
// Get current branch
|
|
37
|
-
const currentBranch = yield* git.getCurrentBranch(gitRoot)
|
|
38
|
-
verboseLog(`Current branch: ${highlight.branch(currentBranch)}`)
|
|
39
|
-
|
|
40
|
-
// Check if current branch has agency.json (is a source branch)
|
|
41
|
-
const metadataService = yield* Effect.gen(function* () {
|
|
42
|
-
return yield* AgencyMetadataService
|
|
43
|
-
}).pipe(Effect.provide(AgencyMetadataService.Default))
|
|
44
|
-
|
|
45
|
-
const metadata = yield* metadataService.readFromDisk(gitRoot)
|
|
46
|
-
|
|
47
|
-
if (!metadata) {
|
|
48
|
-
return yield* Effect.fail(
|
|
49
|
-
new Error(
|
|
50
|
-
`Current branch ${highlight.branch(currentBranch)} does not have agency.json.\n` +
|
|
51
|
-
`The rebase command can only be used on agency source branches.\n` +
|
|
52
|
-
`Run 'agency task' first to initialize this branch, or switch to an existing agency branch.`,
|
|
53
|
-
),
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
verboseLog(`Branch is an agency source branch`)
|
|
58
|
-
|
|
59
|
-
// Check for uncommitted changes
|
|
60
|
-
const statusOutput = yield* git.getStatus(gitRoot)
|
|
61
|
-
|
|
62
|
-
if (statusOutput && statusOutput.trim().length > 0) {
|
|
63
|
-
return yield* Effect.fail(
|
|
64
|
-
new Error(
|
|
65
|
-
`You have uncommitted changes. Please commit or stash them before rebasing.\n` +
|
|
66
|
-
`Run 'git status' to see the changes.`,
|
|
67
|
-
),
|
|
68
|
-
)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
verboseLog(`Working directory is clean`)
|
|
72
|
-
|
|
73
|
-
// Resolve base branch (what we're rebasing onto)
|
|
74
|
-
const baseBranch = yield* resolveBaseBranch(gitRoot, options.baseBranch)
|
|
75
|
-
verboseLog(`Base branch: ${highlight.branch(baseBranch)}`)
|
|
76
|
-
|
|
77
|
-
// Check if base branch is a remote branch
|
|
78
|
-
const hasRemotePrefix = yield* git.hasRemotePrefix(baseBranch, gitRoot)
|
|
79
|
-
|
|
80
|
-
if (hasRemotePrefix) {
|
|
81
|
-
// Extract remote name from branch (e.g., "origin/main" -> "origin")
|
|
82
|
-
const remote = yield* git.getRemoteFromBranch(baseBranch, gitRoot)
|
|
83
|
-
|
|
84
|
-
if (remote) {
|
|
85
|
-
// Fetch the remote to ensure we have the latest commits
|
|
86
|
-
const fetchOperation = Effect.gen(function* () {
|
|
87
|
-
verboseLog(`Fetching ${highlight.branch(remote)}`)
|
|
88
|
-
yield* git.fetch(gitRoot, remote)
|
|
89
|
-
verboseLog(`Fetched ${highlight.branch(remote)}`)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
yield* withSpinner(fetchOperation, {
|
|
93
|
-
text: `Fetching ${highlight.branch(remote)}`,
|
|
94
|
-
successText: `Fetched ${highlight.branch(remote)}`,
|
|
95
|
-
enabled: !options.silent && !verbose,
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Perform the rebase
|
|
101
|
-
log(
|
|
102
|
-
info(
|
|
103
|
-
`Rebasing ${highlight.branch(currentBranch)} onto ${highlight.branch(baseBranch)}`,
|
|
104
|
-
),
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
const rebaseOperation = Effect.gen(function* () {
|
|
108
|
-
verboseLog(`Running: git rebase ${baseBranch}`)
|
|
109
|
-
|
|
110
|
-
const result = yield* git.rebase(gitRoot, baseBranch)
|
|
111
|
-
|
|
112
|
-
if (result.exitCode !== 0) {
|
|
113
|
-
return yield* Effect.fail(
|
|
114
|
-
new Error(
|
|
115
|
-
`Rebase failed with conflicts.\n\n` +
|
|
116
|
-
`${result.stderr}\n\n` +
|
|
117
|
-
`To resolve:\n` +
|
|
118
|
-
` 1. Fix the conflicts in your files\n` +
|
|
119
|
-
` 2. Run: git add <resolved-files>\n` +
|
|
120
|
-
` 3. Run: git rebase --continue\n\n` +
|
|
121
|
-
`To abort the rebase:\n` +
|
|
122
|
-
` Run: git rebase --abort`,
|
|
123
|
-
),
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
verboseLog(`Rebase completed successfully`)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
yield* withSpinner(rebaseOperation, {
|
|
131
|
-
text: "Rebasing branch",
|
|
132
|
-
successText: "Rebased branch successfully",
|
|
133
|
-
enabled: !options.silent && !verbose,
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
log(
|
|
137
|
-
done(
|
|
138
|
-
`Rebased ${highlight.branch(currentBranch)} onto ${highlight.branch(baseBranch)}`,
|
|
139
|
-
),
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
// Update emit branch in agency.json if --emit/--branch flag was provided
|
|
143
|
-
const newEmitBranch = options.emit || options.branch
|
|
144
|
-
if (newEmitBranch && metadata) {
|
|
145
|
-
verboseLog(`Updating emit branch to: ${highlight.branch(newEmitBranch)}`)
|
|
146
|
-
|
|
147
|
-
// Update metadata with new emit branch
|
|
148
|
-
const updatedMetadata = {
|
|
149
|
-
...metadata,
|
|
150
|
-
emitBranch: newEmitBranch,
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
yield* Effect.tryPromise({
|
|
154
|
-
try: async () => {
|
|
155
|
-
const { writeAgencyMetadata } = await import("../types")
|
|
156
|
-
await writeAgencyMetadata(gitRoot, updatedMetadata)
|
|
157
|
-
},
|
|
158
|
-
catch: (error) => new Error(`Failed to update agency.json: ${error}`),
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
// Stage and commit the change
|
|
162
|
-
yield* git.gitAdd(["agency.json"], gitRoot)
|
|
163
|
-
// Format: chore: agency rebase (baseBranch) sourceBranch → emitBranch
|
|
164
|
-
const commitMessage = `chore: agency rebase (${baseBranch}) ${currentBranch} → ${newEmitBranch}`
|
|
165
|
-
yield* git.gitCommit(commitMessage, gitRoot, { noVerify: true })
|
|
166
|
-
|
|
167
|
-
log(info(`Updated emit branch to ${highlight.branch(newEmitBranch)}`))
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Inform user about next steps
|
|
171
|
-
log(
|
|
172
|
-
info(
|
|
173
|
-
`Your source branch has been rebased. You may want to:\n` +
|
|
174
|
-
` - Run 'agency emit' to regenerate your emit branch\n` +
|
|
175
|
-
` - Run 'agency push --force' to update the remote branch (use with caution)`,
|
|
176
|
-
),
|
|
177
|
-
)
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
export const help = `
|
|
181
|
-
Usage: agency rebase [options]
|
|
182
|
-
|
|
183
|
-
Rebase the current agency source branch onto the latest base branch (typically origin/main).
|
|
184
|
-
|
|
185
|
-
This command is useful when:
|
|
186
|
-
- Your work has been merged to the main branch via PR
|
|
187
|
-
- You want to continue working on the same branch with a clean history
|
|
188
|
-
- You want to incorporate the latest changes from main into your branch
|
|
189
|
-
|
|
190
|
-
Behavior:
|
|
191
|
-
- Ensures you're on an agency source branch (has agency.json)
|
|
192
|
-
- Checks for uncommitted changes (rebase requires a clean working directory)
|
|
193
|
-
- Fetches the latest from the base branch if it's a remote branch
|
|
194
|
-
- Rebases your current branch onto the base branch
|
|
195
|
-
- All agency files (TASK.md, AGENTS.md, opencode.json, etc.) are preserved
|
|
196
|
-
- Your emit branch may need to be regenerated with 'agency emit'
|
|
197
|
-
|
|
198
|
-
Base Branch Selection:
|
|
199
|
-
The command determines the base branch in this order:
|
|
200
|
-
1. Explicitly provided base-branch argument
|
|
201
|
-
2. Branch-specific base branch from agency.json (set by 'agency task')
|
|
202
|
-
3. Repository-level default base branch from .git/config
|
|
203
|
-
4. Auto-detected from origin/HEAD or common branches (origin/main, etc.)
|
|
204
|
-
|
|
205
|
-
Arguments:
|
|
206
|
-
[base-branch] Optional base branch to rebase onto
|
|
207
|
-
(defaults to saved base branch or origin/main)
|
|
208
|
-
|
|
209
|
-
Options:
|
|
210
|
-
--emit <name> Set a new emit branch name in agency.json after rebasing
|
|
211
|
-
--branch <name> (Deprecated: use --emit) Set a new emit branch name
|
|
212
|
-
|
|
213
|
-
Examples:
|
|
214
|
-
agency rebase # Rebase onto saved base branch
|
|
215
|
-
agency rebase origin/main # Rebase onto origin/main explicitly
|
|
216
|
-
agency rebase --emit new-branch # Rebase and set new emit branch name
|
|
217
|
-
|
|
218
|
-
Workflow:
|
|
219
|
-
1. User works on agency--feature-A branch
|
|
220
|
-
2. User runs 'agency emit' and 'agency push' to create a PR
|
|
221
|
-
3. PR gets merged into main
|
|
222
|
-
4. User runs 'agency rebase' to rebase agency--feature-A onto origin/main
|
|
223
|
-
5. User continues working on agency--feature-A with updated main branch
|
|
224
|
-
6. User runs 'agency emit' again to create a fresh emit branch
|
|
225
|
-
|
|
226
|
-
Notes:
|
|
227
|
-
- This command only works on agency source branches (with agency.json)
|
|
228
|
-
- If conflicts occur during rebase, you must resolve them manually
|
|
229
|
-
- After rebasing, your emit branch will be outdated - run 'agency emit' to regenerate it
|
|
230
|
-
- Be careful when force-pushing after a rebase - coordinate with your team
|
|
231
|
-
- This command is different from 'agency task --continue' which creates a new branch
|
|
232
|
-
`
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import { save } from "./save"
|
|
4
|
-
import { task } from "./task"
|
|
5
|
-
import {
|
|
6
|
-
createTempDir,
|
|
7
|
-
cleanupTempDir,
|
|
8
|
-
initGitRepo,
|
|
9
|
-
initAgency,
|
|
10
|
-
readFile,
|
|
11
|
-
runTestEffect,
|
|
12
|
-
} from "../test-utils"
|
|
13
|
-
|
|
14
|
-
describe("save command", () => {
|
|
15
|
-
let tempDir: string
|
|
16
|
-
let originalCwd: string
|
|
17
|
-
let originalConfigDir: string | undefined
|
|
18
|
-
|
|
19
|
-
beforeEach(async () => {
|
|
20
|
-
tempDir = await createTempDir()
|
|
21
|
-
originalCwd = process.cwd()
|
|
22
|
-
originalConfigDir = process.env.AGENCY_CONFIG_DIR
|
|
23
|
-
// Use a temp config dir to avoid interference from user's actual config
|
|
24
|
-
process.env.AGENCY_CONFIG_DIR = await createTempDir()
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
afterEach(async () => {
|
|
28
|
-
process.chdir(originalCwd)
|
|
29
|
-
if (originalConfigDir !== undefined) {
|
|
30
|
-
process.env.AGENCY_CONFIG_DIR = originalConfigDir
|
|
31
|
-
} else {
|
|
32
|
-
delete process.env.AGENCY_CONFIG_DIR
|
|
33
|
-
}
|
|
34
|
-
if (
|
|
35
|
-
process.env.AGENCY_CONFIG_DIR &&
|
|
36
|
-
process.env.AGENCY_CONFIG_DIR !== originalConfigDir
|
|
37
|
-
) {
|
|
38
|
-
await cleanupTempDir(process.env.AGENCY_CONFIG_DIR)
|
|
39
|
-
}
|
|
40
|
-
await cleanupTempDir(tempDir)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test("saves specified files to template directory", async () => {
|
|
44
|
-
await initGitRepo(tempDir)
|
|
45
|
-
process.chdir(tempDir)
|
|
46
|
-
|
|
47
|
-
// Initialize with template
|
|
48
|
-
await initAgency(tempDir, "test-save")
|
|
49
|
-
await task({ silent: true, emit: "test-feature" })
|
|
50
|
-
|
|
51
|
-
// Modify the files
|
|
52
|
-
await Bun.write(join(tempDir, "AGENTS.md"), "# Modified content")
|
|
53
|
-
|
|
54
|
-
// Save specific files to template
|
|
55
|
-
await runTestEffect(save({ files: ["AGENTS.md"], silent: true }))
|
|
56
|
-
|
|
57
|
-
// Check template files were updated
|
|
58
|
-
const configDir = process.env.AGENCY_CONFIG_DIR!
|
|
59
|
-
const templateAgents = await readFile(
|
|
60
|
-
join(configDir, "templates", "test-save", "AGENTS.md"),
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
expect(templateAgents).toBe("# Modified content")
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test("throws error when no files specified", async () => {
|
|
67
|
-
await initGitRepo(tempDir)
|
|
68
|
-
process.chdir(tempDir)
|
|
69
|
-
|
|
70
|
-
// Initialize with template
|
|
71
|
-
await initAgency(tempDir, "test-no-files")
|
|
72
|
-
await task({
|
|
73
|
-
silent: true,
|
|
74
|
-
emit: "test-feature",
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
await expect(
|
|
78
|
-
runTestEffect(save({ files: [], silent: true })),
|
|
79
|
-
).rejects.toThrow("No files specified")
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test("throws error when no template configured", async () => {
|
|
83
|
-
await initGitRepo(tempDir)
|
|
84
|
-
process.chdir(tempDir)
|
|
85
|
-
|
|
86
|
-
await expect(
|
|
87
|
-
runTestEffect(save({ files: ["AGENTS.md"], silent: true })),
|
|
88
|
-
).rejects.toThrow("Repository not initialized")
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test("throws error when not in git repo", async () => {
|
|
92
|
-
process.chdir(tempDir)
|
|
93
|
-
|
|
94
|
-
await expect(
|
|
95
|
-
runTestEffect(save({ files: ["AGENTS.md"], silent: true })),
|
|
96
|
-
).rejects.toThrow("Not in a git repository")
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test("skips files that don't exist", async () => {
|
|
100
|
-
await initGitRepo(tempDir)
|
|
101
|
-
process.chdir(tempDir)
|
|
102
|
-
|
|
103
|
-
// Initialize with template
|
|
104
|
-
await initAgency(tempDir, "test-partial")
|
|
105
|
-
await task({
|
|
106
|
-
silent: true,
|
|
107
|
-
emit: "test-feature",
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
// Remove AGENTS.md (just to test skipping behavior)
|
|
111
|
-
const rmProc = Bun.spawn(["rm", join(tempDir, "AGENTS.md")], {
|
|
112
|
-
cwd: tempDir,
|
|
113
|
-
stdout: "pipe",
|
|
114
|
-
stderr: "pipe",
|
|
115
|
-
})
|
|
116
|
-
await rmProc.exited
|
|
117
|
-
|
|
118
|
-
// Modify a different file that exists
|
|
119
|
-
await Bun.write(join(tempDir, "test.txt"), "# Test content")
|
|
120
|
-
|
|
121
|
-
// Save should succeed but skip the missing file
|
|
122
|
-
await runTestEffect(
|
|
123
|
-
save({ files: ["AGENTS.md", "test.txt"], silent: true }),
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
// Check test.txt was updated
|
|
127
|
-
const configDir = process.env.AGENCY_CONFIG_DIR!
|
|
128
|
-
const testContent = await readFile(
|
|
129
|
-
join(configDir, "templates", "test-partial", "test.txt"),
|
|
130
|
-
)
|
|
131
|
-
expect(testContent).toBe("# Test content")
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
test("saves directories recursively", async () => {
|
|
135
|
-
await initGitRepo(tempDir)
|
|
136
|
-
process.chdir(tempDir)
|
|
137
|
-
|
|
138
|
-
// Initialize with template
|
|
139
|
-
await initAgency(tempDir, "test-dir")
|
|
140
|
-
await task({ silent: true, emit: "test-feature" })
|
|
141
|
-
|
|
142
|
-
// Create a directory with files
|
|
143
|
-
const docsDir = join(tempDir, "docs")
|
|
144
|
-
await Bun.write(join(docsDir, "README.md"), "# Documentation")
|
|
145
|
-
await Bun.write(join(docsDir, "guide.md"), "# Guide")
|
|
146
|
-
|
|
147
|
-
// Save the directory
|
|
148
|
-
await runTestEffect(save({ files: ["docs"], silent: true }))
|
|
149
|
-
|
|
150
|
-
// Check files were saved to template
|
|
151
|
-
const configDir = process.env.AGENCY_CONFIG_DIR!
|
|
152
|
-
const readmeContent = await readFile(
|
|
153
|
-
join(configDir, "templates", "test-dir", "docs", "README.md"),
|
|
154
|
-
)
|
|
155
|
-
const guideContent = await readFile(
|
|
156
|
-
join(configDir, "templates", "test-dir", "docs", "guide.md"),
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
expect(readmeContent).toBe("# Documentation")
|
|
160
|
-
expect(guideContent).toBe("# Guide")
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
test("refuses to save TASK.md", async () => {
|
|
164
|
-
await initGitRepo(tempDir)
|
|
165
|
-
process.chdir(tempDir)
|
|
166
|
-
|
|
167
|
-
// Initialize with template
|
|
168
|
-
await initAgency(tempDir, "test-task")
|
|
169
|
-
await task({ silent: true, emit: "test-feature" })
|
|
170
|
-
|
|
171
|
-
// Create a TASK.md with the {task} placeholder
|
|
172
|
-
await Bun.write(join(tempDir, "TASK.md"), "{task}\n\n## Notes")
|
|
173
|
-
|
|
174
|
-
// Save should fail regardless of content
|
|
175
|
-
await expect(
|
|
176
|
-
runTestEffect(save({ files: ["TASK.md"], silent: true })),
|
|
177
|
-
).rejects.toThrow("TASK.md files cannot be saved to templates")
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
test("refuses to save TASK.md even without {task} placeholder", async () => {
|
|
181
|
-
await initGitRepo(tempDir)
|
|
182
|
-
process.chdir(tempDir)
|
|
183
|
-
|
|
184
|
-
// Initialize with template
|
|
185
|
-
await initAgency(tempDir, "test-task-invalid")
|
|
186
|
-
await task({
|
|
187
|
-
silent: true,
|
|
188
|
-
emit: "test-feature",
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
// Create a TASK.md without the {task} placeholder
|
|
192
|
-
await Bun.write(
|
|
193
|
-
join(tempDir, "TASK.md"),
|
|
194
|
-
"# Specific Task\n\nThis is a specific task, not a template",
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
// Save should fail
|
|
198
|
-
await expect(
|
|
199
|
-
runTestEffect(save({ files: ["TASK.md"], silent: true })),
|
|
200
|
-
).rejects.toThrow("TASK.md files cannot be saved to templates")
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
test("refuses to save TASK.md in subdirectories", async () => {
|
|
204
|
-
await initGitRepo(tempDir)
|
|
205
|
-
process.chdir(tempDir)
|
|
206
|
-
|
|
207
|
-
// Initialize with template
|
|
208
|
-
await initAgency(tempDir, "test-task-subdir")
|
|
209
|
-
await task({
|
|
210
|
-
silent: true,
|
|
211
|
-
emit: "test-feature",
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
// Create a TASK.md in a subdirectory with the {task} placeholder
|
|
215
|
-
const subdir = join(tempDir, "projects")
|
|
216
|
-
await Bun.write(join(subdir, "TASK.md"), "{task}\n\n## Details")
|
|
217
|
-
|
|
218
|
-
// Save should fail regardless of content
|
|
219
|
-
await expect(
|
|
220
|
-
runTestEffect(save({ files: ["projects/TASK.md"], silent: true })),
|
|
221
|
-
).rejects.toThrow("TASK.md files cannot be saved to templates")
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
test("refuses to save TASK.md in subdirectory even without {task} placeholder", async () => {
|
|
225
|
-
await initGitRepo(tempDir)
|
|
226
|
-
process.chdir(tempDir)
|
|
227
|
-
|
|
228
|
-
// Initialize with template
|
|
229
|
-
await initAgency(tempDir, "test-task-subdir-invalid")
|
|
230
|
-
await task({
|
|
231
|
-
silent: true,
|
|
232
|
-
emit: "test-feature",
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
// Create a TASK.md in a subdirectory without the {task} placeholder
|
|
236
|
-
const subdir = join(tempDir, "projects")
|
|
237
|
-
await Bun.write(
|
|
238
|
-
join(subdir, "TASK.md"),
|
|
239
|
-
"# Specific Project Task\n\nThis is specific",
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
// Save should fail
|
|
243
|
-
await expect(
|
|
244
|
-
runTestEffect(save({ files: ["projects/TASK.md"], silent: true })),
|
|
245
|
-
).rejects.toThrow("TASK.md files cannot be saved to templates")
|
|
246
|
-
})
|
|
247
|
-
})
|
package/src/commands/save.ts
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { resolve, join, dirname, basename } from "path"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
-
import { TemplateService } from "../services/TemplateService"
|
|
5
|
-
import { FileSystemService } from "../services/FileSystemService"
|
|
6
|
-
import { RepositoryNotInitializedError } from "../errors"
|
|
7
|
-
import highlight, { done } from "../utils/colors"
|
|
8
|
-
import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
|
|
9
|
-
|
|
10
|
-
interface SaveOptions extends BaseCommandOptions {
|
|
11
|
-
files?: string[]
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function isDirectory(filePath: string): Effect.Effect<boolean, Error> {
|
|
15
|
-
return Effect.tryPromise({
|
|
16
|
-
try: async () => {
|
|
17
|
-
try {
|
|
18
|
-
const file = Bun.file(filePath)
|
|
19
|
-
const stat = await file.stat()
|
|
20
|
-
return stat?.isDirectory?.() ?? false
|
|
21
|
-
} catch {
|
|
22
|
-
return false
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
catch: (error) =>
|
|
26
|
-
new Error(`Failed to check if path is directory: ${error}`),
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const save = (options: SaveOptions = {}) =>
|
|
31
|
-
Effect.gen(function* () {
|
|
32
|
-
const { files: filesToSave = [] } = options
|
|
33
|
-
const { log, verboseLog } = createLoggers(options)
|
|
34
|
-
|
|
35
|
-
const templateService = yield* TemplateService
|
|
36
|
-
const fs = yield* FileSystemService
|
|
37
|
-
|
|
38
|
-
const gitRoot = yield* ensureGitRepo()
|
|
39
|
-
|
|
40
|
-
// Get template name from git config
|
|
41
|
-
const templateName = yield* getTemplateName(gitRoot)
|
|
42
|
-
if (!templateName) {
|
|
43
|
-
return yield* Effect.fail(new RepositoryNotInitializedError())
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
verboseLog(`Saving to template: ${highlight.template(templateName)}`)
|
|
47
|
-
|
|
48
|
-
// Get template directory
|
|
49
|
-
const templateDir = yield* templateService.getTemplateDir(templateName)
|
|
50
|
-
|
|
51
|
-
// Create template directory if it doesn't exist
|
|
52
|
-
yield* templateService.createTemplateDir(templateName)
|
|
53
|
-
verboseLog(`Ensured template directory exists: ${templateDir}`)
|
|
54
|
-
|
|
55
|
-
// Determine which files to save
|
|
56
|
-
let filesToProcess: string[] = []
|
|
57
|
-
|
|
58
|
-
if (filesToSave.length > 0) {
|
|
59
|
-
// Process provided file/dir names
|
|
60
|
-
for (const fileOrDir of filesToSave) {
|
|
61
|
-
const fullPath = resolve(gitRoot, fileOrDir)
|
|
62
|
-
const isDir = yield* isDirectory(fullPath)
|
|
63
|
-
|
|
64
|
-
if (isDir) {
|
|
65
|
-
// Recursively collect files from directory
|
|
66
|
-
const collected = yield* fs.collectFiles(fullPath, {
|
|
67
|
-
relativeTo: gitRoot,
|
|
68
|
-
})
|
|
69
|
-
filesToProcess.push(...collected)
|
|
70
|
-
} else {
|
|
71
|
-
// Add file path relative to git root
|
|
72
|
-
const relativePath = fileOrDir.startsWith(gitRoot)
|
|
73
|
-
? fileOrDir.replace(gitRoot + "/", "")
|
|
74
|
-
: fileOrDir
|
|
75
|
-
filesToProcess.push(relativePath)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
return yield* Effect.fail(
|
|
80
|
-
new Error(
|
|
81
|
-
"No files specified. Usage: agency save <file|dir> [file|dir ...]",
|
|
82
|
-
),
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Save each file
|
|
87
|
-
for (const filePath of filesToProcess) {
|
|
88
|
-
const sourceFilePath = resolve(gitRoot, filePath)
|
|
89
|
-
|
|
90
|
-
// Check if file exists
|
|
91
|
-
const exists = yield* fs.exists(sourceFilePath)
|
|
92
|
-
if (!exists) {
|
|
93
|
-
verboseLog(`Skipping ${filePath} (does not exist)`)
|
|
94
|
-
continue
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Refuse to save TASK.md files - agency itself must control these
|
|
98
|
-
const fileName = basename(filePath)
|
|
99
|
-
if (fileName === "TASK.md") {
|
|
100
|
-
return yield* Effect.fail(
|
|
101
|
-
new Error(
|
|
102
|
-
`Cannot save ${filePath}: TASK.md files cannot be saved to templates. ` +
|
|
103
|
-
`Agency itself must control the creation of TASK.md files.`,
|
|
104
|
-
),
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Read content
|
|
109
|
-
const content = yield* fs.readFile(sourceFilePath)
|
|
110
|
-
|
|
111
|
-
const templateFilePath = join(templateDir, filePath)
|
|
112
|
-
|
|
113
|
-
// Ensure directory exists
|
|
114
|
-
const dir = dirname(templateFilePath)
|
|
115
|
-
yield* fs.createDirectory(dir)
|
|
116
|
-
|
|
117
|
-
// Write to template
|
|
118
|
-
yield* fs.writeFile(templateFilePath, content)
|
|
119
|
-
log(
|
|
120
|
-
done(
|
|
121
|
-
`Saved ${highlight.file(filePath)} to ${highlight.template(templateName)} template`,
|
|
122
|
-
),
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
// Help text for reference (not exported as it's handled by template command)
|
|
128
|
-
const help = `
|
|
129
|
-
Usage: agency save <file|dir> [file|dir ...] [options]
|
|
130
|
-
|
|
131
|
-
Save specified files or directories to the configured template.
|
|
132
|
-
|
|
133
|
-
This command copies files and directories from the current git repository to
|
|
134
|
-
the template directory configured in .git/config (agency.template). Directories
|
|
135
|
-
are saved recursively.
|
|
136
|
-
|
|
137
|
-
Arguments:
|
|
138
|
-
<file|dir> File or directory path to save (relative to git root)
|
|
139
|
-
[file|dir ...] Additional files or directories to save
|
|
140
|
-
|
|
141
|
-
Options:
|
|
142
|
-
-h, --help Show this help message
|
|
143
|
-
-s, --silent Suppress output messages
|
|
144
|
-
-v, --verbose Show verbose output
|
|
145
|
-
|
|
146
|
-
Examples:
|
|
147
|
-
agency save AGENTS.md # Save specific file
|
|
148
|
-
agency save .config # Save entire directory
|
|
149
|
-
agency save src/ # Save src directory to template
|
|
150
|
-
agency save AGENTS.md docs/ # Save file and directory
|
|
151
|
-
agency save --verbose # Save with verbose output
|
|
152
|
-
agency save --help # Show this help message
|
|
153
|
-
|
|
154
|
-
Notes:
|
|
155
|
-
- Requires agency.template to be set (run 'agency init' first)
|
|
156
|
-
- At least one file or directory must be specified
|
|
157
|
-
- Files are saved to ~/.config/agency/templates/{template-name}/
|
|
158
|
-
- Template directory is created automatically if it doesn't exist
|
|
159
|
-
- Existing template files will be overwritten
|
|
160
|
-
- Directory structure is preserved in the template
|
|
161
|
-
- TASK.md files cannot be saved - agency controls their creation
|
|
162
|
-
`
|