@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.
Files changed (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -420
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +258 -0
  6. package/src/cli.test.ts +290 -0
  7. package/src/commands/description.test.ts +103 -0
  8. package/src/commands/epic.test.ts +103 -0
  9. package/src/commands/epic.ts +101 -0
  10. package/src/commands/init.test.ts +42 -290
  11. package/src/commands/init.ts +15 -123
  12. package/src/commands/phase.ts +120 -0
  13. package/src/commands/pr.test.ts +19 -170
  14. package/src/commands/pr.ts +23 -179
  15. package/src/commands/repo.test.ts +80 -0
  16. package/src/commands/repo.ts +83 -0
  17. package/src/commands/status.test.ts +35 -477
  18. package/src/commands/status.ts +24 -213
  19. package/src/commands/task-phase.test.ts +206 -0
  20. package/src/commands/task.ts +83 -1285
  21. package/src/commands/validate.test.ts +91 -0
  22. package/src/commands/validate.ts +50 -0
  23. package/src/commands/work.test.ts +172 -419
  24. package/src/commands/work.ts +47 -166
  25. package/src/services/EpicService.test.ts +71 -0
  26. package/src/services/EpicService.ts +135 -0
  27. package/src/services/FileSystemService.ts +61 -92
  28. package/src/services/PhaseService.ts +323 -0
  29. package/src/services/PullRequestService.test.ts +386 -0
  30. package/src/services/PullRequestService.ts +143 -0
  31. package/src/services/RepositoryService.test.ts +106 -0
  32. package/src/services/RepositoryService.ts +159 -0
  33. package/src/services/TaskPhaseService.test.ts +217 -0
  34. package/src/services/TaskService.ts +192 -0
  35. package/src/services/WorkbaseService.test.ts +446 -0
  36. package/src/services/WorkbaseService.ts +493 -0
  37. package/src/services/WorktreeService.test.ts +599 -0
  38. package/src/services/WorktreeService.ts +357 -0
  39. package/src/test-utils.ts +41 -438
  40. package/src/types.ts +1 -215
  41. package/src/utils/command.ts +1 -5
  42. package/src/utils/effect.test.ts +24 -0
  43. package/src/utils/effect.ts +3 -273
  44. package/src/utils/process.test.ts +0 -15
  45. package/src/utils/process.ts +1 -60
  46. package/src/workbase/frontmatter.test.ts +50 -0
  47. package/src/workbase/frontmatter.ts +77 -0
  48. package/src/workbase/repository-reference.test.ts +25 -0
  49. package/src/workbase/repository-reference.ts +18 -0
  50. package/src/workbase/schemas.test.ts +146 -0
  51. package/src/workbase/schemas.ts +78 -0
  52. package/src/workbase/worktree-command.test.ts +61 -0
  53. package/src/workbase/worktree-command.ts +52 -0
  54. package/src/commands/base.test.ts +0 -198
  55. package/src/commands/base.ts +0 -198
  56. package/src/commands/clean.test.ts +0 -299
  57. package/src/commands/clean.ts +0 -274
  58. package/src/commands/completions.test.ts +0 -51
  59. package/src/commands/completions.ts +0 -308
  60. package/src/commands/emit.integration.test.ts +0 -549
  61. package/src/commands/emit.test.ts +0 -576
  62. package/src/commands/emit.ts +0 -700
  63. package/src/commands/emitted.test.ts +0 -226
  64. package/src/commands/emitted.ts +0 -57
  65. package/src/commands/loop.test.ts +0 -621
  66. package/src/commands/loop.ts +0 -406
  67. package/src/commands/merge.integration.test.ts +0 -195
  68. package/src/commands/merge.test.ts +0 -246
  69. package/src/commands/merge.ts +0 -244
  70. package/src/commands/pull.test.ts +0 -395
  71. package/src/commands/pull.ts +0 -205
  72. package/src/commands/push.test.ts +0 -641
  73. package/src/commands/push.ts +0 -482
  74. package/src/commands/rebase.test.ts +0 -521
  75. package/src/commands/rebase.ts +0 -232
  76. package/src/commands/save.test.ts +0 -247
  77. package/src/commands/save.ts +0 -162
  78. package/src/commands/source.test.ts +0 -195
  79. package/src/commands/source.ts +0 -72
  80. package/src/commands/switch.test.ts +0 -194
  81. package/src/commands/switch.ts +0 -84
  82. package/src/commands/task-branching.test.ts +0 -633
  83. package/src/commands/task-continue.test.ts +0 -311
  84. package/src/commands/task-edit.test.ts +0 -235
  85. package/src/commands/task-main.test.ts +0 -1066
  86. package/src/commands/task-squash.test.ts +0 -266
  87. package/src/commands/tasks.test.ts +0 -391
  88. package/src/commands/tasks.ts +0 -161
  89. package/src/commands/template-delete.test.ts +0 -178
  90. package/src/commands/template-delete.ts +0 -98
  91. package/src/commands/template-list.test.ts +0 -135
  92. package/src/commands/template-list.ts +0 -87
  93. package/src/commands/template-view.test.ts +0 -158
  94. package/src/commands/template-view.ts +0 -86
  95. package/src/commands/template.test.ts +0 -32
  96. package/src/commands/template.ts +0 -96
  97. package/src/commands/use.test.ts +0 -87
  98. package/src/commands/use.ts +0 -97
  99. package/src/constants.ts +0 -10
  100. package/src/errors.ts +0 -17
  101. package/src/schemas.ts +0 -33
  102. package/src/services/AgencyMetadataService.ts +0 -311
  103. package/src/services/ClaudeService.test.ts +0 -184
  104. package/src/services/ClaudeService.ts +0 -91
  105. package/src/services/ConfigService.ts +0 -115
  106. package/src/services/FilterRepoService.ts +0 -164
  107. package/src/services/FormatterService.test.ts +0 -432
  108. package/src/services/FormatterService.ts +0 -219
  109. package/src/services/GitService.ts +0 -1187
  110. package/src/services/MockFilterRepoService.ts +0 -140
  111. package/src/services/OpencodeService.ts +0 -263
  112. package/src/services/PromptService.ts +0 -183
  113. package/src/services/TemplateService.ts +0 -81
  114. package/src/types/native-exec.d.ts +0 -8
  115. package/src/utils/colors.ts +0 -178
  116. package/src/utils/git-path.ts +0 -6
  117. package/src/utils/glob.test.ts +0 -161
  118. package/src/utils/glob.ts +0 -78
  119. package/src/utils/paths.ts +0 -51
  120. package/src/utils/pr-branch.test.ts +0 -376
  121. package/src/utils/pr-branch.ts +0 -508
  122. package/src/utils/spinner.test.ts +0 -45
  123. package/src/utils/spinner.ts +0 -96
  124. package/src/utils/task-parser.test.ts +0 -172
  125. package/src/utils/task-parser.ts +0 -93
  126. package/templates/AGENCY.md +0 -22
  127. package/templates/AGENTS.md +0 -11
  128. package/templates/CLAUDE.md +0 -3
  129. package/templates/TASK.md +0 -5
  130. package/templates/opencode.json +0 -4
@@ -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
- })
@@ -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
- `