@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.
Files changed (130) hide show
  1. package/README.md +302 -113
  2. package/cli.ts +123 -415
  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 -518
  73. package/src/commands/push.ts +0 -434
  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 -1183
  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,161 +0,0 @@
1
- import { Effect, DateTime } from "effect"
2
- import { Schema } from "@effect/schema"
3
- import type { BaseCommandOptions } from "../utils/command"
4
- import { GitService } from "../services/GitService"
5
- import { ConfigService } from "../services/ConfigService"
6
- import { AgencyMetadata } from "../schemas"
7
- import highlight from "../utils/colors"
8
- import { createLoggers, ensureGitRepo } from "../utils/effect"
9
-
10
- interface TasksOptions extends BaseCommandOptions {
11
- json?: boolean
12
- }
13
-
14
- /**
15
- * Task branch info for output
16
- */
17
- interface TaskBranchInfo {
18
- branch: string
19
- template: string | null
20
- baseBranch: string | null
21
- createdAt: string | null
22
- }
23
-
24
- /**
25
- * Read agency.json metadata from a specific branch using git show.
26
- */
27
- const readAgencyMetadataFromBranch = (gitRoot: string, branch: string) =>
28
- Effect.gen(function* () {
29
- const git = yield* GitService
30
-
31
- // Try to read agency.json from the branch using git show
32
- const content = yield* git.getFileAtRef(gitRoot, branch, "agency.json")
33
-
34
- if (!content) {
35
- return null
36
- }
37
-
38
- return yield* parseAgencyMetadata(content)
39
- }).pipe(Effect.catchAll(() => Effect.succeed(null)))
40
-
41
- /**
42
- * Parse and validate agency.json content.
43
- */
44
- const parseAgencyMetadata = (content: string) =>
45
- Effect.gen(function* () {
46
- const data = yield* Effect.try({
47
- try: () => JSON.parse(content),
48
- catch: () => new Error("Failed to parse agency.json"),
49
- })
50
-
51
- // Validate version
52
- if (typeof data.version !== "number" || data.version !== 1) {
53
- return null
54
- }
55
-
56
- // Parse and validate using Effect schema
57
- const metadata = yield* Effect.try({
58
- try: () => Schema.decodeUnknownSync(AgencyMetadata)(data),
59
- catch: () => new Error("Invalid agency.json format"),
60
- })
61
-
62
- return metadata
63
- }).pipe(Effect.catchAll(() => Effect.succeed(null)))
64
-
65
- /**
66
- * Extract the prefix from a source branch pattern (everything before %branch%).
67
- */
68
- const getSourceBranchPrefix = (pattern: string): string => {
69
- if (pattern.includes("%branch%")) {
70
- return pattern.split("%branch%")[0]!
71
- }
72
- // If no %branch% placeholder, the whole pattern is the prefix
73
- return pattern
74
- }
75
-
76
- /**
77
- * Find all branches that match the source branch pattern prefix
78
- */
79
- const findAllTaskBranches = (gitRoot: string) =>
80
- Effect.gen(function* () {
81
- const git = yield* GitService
82
- const configService = yield* ConfigService
83
-
84
- // Get source branch pattern from config
85
- const config = yield* configService.loadConfig()
86
- const prefix = getSourceBranchPrefix(config.sourceBranchPattern)
87
-
88
- // Get only branches matching the prefix (fast - single git command)
89
- const branches = yield* git.getBranchesByPrefix(gitRoot, prefix)
90
-
91
- // For each matching branch, try to read agency.json for metadata
92
- const taskBranches: TaskBranchInfo[] = []
93
- for (const branch of branches) {
94
- const metadata = yield* readAgencyMetadataFromBranch(gitRoot, branch)
95
-
96
- if (metadata) {
97
- taskBranches.push({
98
- branch,
99
- template: metadata.template ?? null,
100
- baseBranch: metadata.baseBranch ?? null,
101
- createdAt: metadata.createdAt
102
- ? DateTime.toDateUtc(metadata.createdAt).toISOString()
103
- : null,
104
- })
105
- } else {
106
- // Branch matches prefix but has no valid agency.json - still list it
107
- taskBranches.push({
108
- branch,
109
- template: null,
110
- baseBranch: null,
111
- createdAt: null,
112
- })
113
- }
114
- }
115
-
116
- return taskBranches
117
- })
118
-
119
- export const tasks = (options: TasksOptions = {}) =>
120
- Effect.gen(function* () {
121
- const { json = false } = options
122
- const { log } = createLoggers(options)
123
-
124
- const gitRoot = yield* ensureGitRepo()
125
-
126
- // Find all branches with agency.json
127
- const taskBranches = yield* findAllTaskBranches(gitRoot)
128
-
129
- if (json) {
130
- // Output JSON format
131
- log(JSON.stringify(taskBranches, null, 2))
132
- } else {
133
- // Output human-readable format - just branch names
134
- if (taskBranches.length === 0) {
135
- log("")
136
- log("No task branches found.")
137
- log(
138
- `Run ${highlight.value("agency task")} to create a task on a feature branch.`,
139
- )
140
- log("")
141
- } else {
142
- for (const task of taskBranches) {
143
- log(task.branch)
144
- }
145
- }
146
- }
147
- })
148
-
149
- export const help = `
150
- Usage: agency tasks [options]
151
-
152
- List all source branches that have agency tasks (branches matching the source
153
- branch pattern, e.g. "agency--*").
154
-
155
- Options:
156
- --json Output as JSON (includes metadata: template, base branch, created date)
157
-
158
- Example:
159
- agency tasks # List all task branches (names only)
160
- agency tasks --json # Output as JSON with full metadata
161
- `
@@ -1,178 +0,0 @@
1
- import { describe, expect, test, beforeEach, afterEach } from "bun:test"
2
- import { templateDelete } from "./template-delete"
3
- import { mkdir, writeFile, rm } from "node:fs/promises"
4
- import { join } from "node:path"
5
- import { tmpdir } from "node:os"
6
- import { runTestEffect } from "../test-utils"
7
-
8
- describe("template delete command", () => {
9
- let testDir: string
10
- let gitRoot: string
11
- let templateDir: string
12
-
13
- beforeEach(async () => {
14
- // Create temporary test directory
15
- testDir = join(tmpdir(), `agency-test-delete-${Date.now()}`)
16
- await mkdir(testDir, { recursive: true })
17
- gitRoot = join(testDir, "repo")
18
- await mkdir(gitRoot, { recursive: true })
19
-
20
- // Initialize git repo
21
- await Bun.spawn(["git", "init"], {
22
- cwd: gitRoot,
23
- stdout: "ignore",
24
- stderr: "ignore",
25
- }).exited
26
-
27
- // Set up config dir
28
- const configDir = join(testDir, "config")
29
- templateDir = join(configDir, "templates", "test-template")
30
- await mkdir(templateDir, { recursive: true })
31
-
32
- // Set environment variable for config
33
- process.env.AGENCY_CONFIG_DIR = configDir
34
-
35
- // Set git config
36
- await Bun.spawn(["git", "config", "agency.template", "test-template"], {
37
- cwd: gitRoot,
38
- stdout: "ignore",
39
- stderr: "ignore",
40
- }).exited
41
- })
42
-
43
- afterEach(async () => {
44
- // Clean up
45
- await rm(testDir, { recursive: true, force: true })
46
- delete process.env.AGENCY_CONFIG_DIR
47
- })
48
-
49
- test("deletes file from template directory", async () => {
50
- // Create test file in template
51
- const testFile = join(templateDir, "test.md")
52
- await writeFile(testFile, "# Test")
53
-
54
- const originalCwd = process.cwd()
55
- process.chdir(gitRoot)
56
-
57
- try {
58
- await runTestEffect(templateDelete({ files: ["test.md"], silent: true }))
59
-
60
- // Verify file was deleted
61
- const file = Bun.file(testFile)
62
- expect(await file.exists()).toBe(false)
63
- } finally {
64
- process.chdir(originalCwd)
65
- }
66
- })
67
-
68
- test("deletes multiple files", async () => {
69
- // Create test files in template
70
- await writeFile(join(templateDir, "file1.md"), "# File 1")
71
- await writeFile(join(templateDir, "file2.md"), "# File 2")
72
- await writeFile(join(templateDir, "file3.md"), "# File 3")
73
-
74
- const originalCwd = process.cwd()
75
- process.chdir(gitRoot)
76
-
77
- try {
78
- await runTestEffect(
79
- templateDelete({
80
- files: ["file1.md", "file2.md"],
81
- silent: true,
82
- }),
83
- )
84
-
85
- // Verify files were deleted
86
- expect(await Bun.file(join(templateDir, "file1.md")).exists()).toBe(false)
87
- expect(await Bun.file(join(templateDir, "file2.md")).exists()).toBe(false)
88
- // file3.md should still exist
89
- expect(await Bun.file(join(templateDir, "file3.md")).exists()).toBe(true)
90
- } finally {
91
- process.chdir(originalCwd)
92
- }
93
- })
94
-
95
- test("deletes directory recursively", async () => {
96
- // Create test directory with files
97
- const docsDir = join(templateDir, "docs")
98
- await mkdir(docsDir, { recursive: true })
99
- await writeFile(join(docsDir, "README.md"), "# Docs")
100
- await writeFile(join(docsDir, "guide.md"), "# Guide")
101
-
102
- const originalCwd = process.cwd()
103
- process.chdir(gitRoot)
104
-
105
- try {
106
- await runTestEffect(templateDelete({ files: ["docs"], silent: true }))
107
-
108
- // Verify directory was deleted
109
- const dir = Bun.file(docsDir)
110
- expect(await dir.exists()).toBe(false)
111
- } finally {
112
- process.chdir(originalCwd)
113
- }
114
- })
115
-
116
- test("handles non-existent files gracefully", async () => {
117
- const originalCwd = process.cwd()
118
- process.chdir(gitRoot)
119
-
120
- try {
121
- // Should not throw error for non-existent file
122
- await runTestEffect(
123
- templateDelete({ files: ["nonexistent.md"], silent: true }),
124
- )
125
- } finally {
126
- process.chdir(originalCwd)
127
- }
128
- })
129
-
130
- test("throws error when no files specified", async () => {
131
- const originalCwd = process.cwd()
132
- process.chdir(gitRoot)
133
-
134
- try {
135
- await expect(
136
- runTestEffect(templateDelete({ files: [], silent: true })),
137
- ).rejects.toThrow("No files specified")
138
- } finally {
139
- process.chdir(originalCwd)
140
- }
141
- })
142
-
143
- test("throws error when not in git repository", async () => {
144
- const nonGitDir = join(testDir, "non-git")
145
- await mkdir(nonGitDir, { recursive: true })
146
-
147
- const originalCwd = process.cwd()
148
- process.chdir(nonGitDir)
149
-
150
- try {
151
- await expect(
152
- runTestEffect(templateDelete({ files: ["test.md"], silent: true })),
153
- ).rejects.toThrow("Not in a git repository")
154
- } finally {
155
- process.chdir(originalCwd)
156
- }
157
- })
158
-
159
- test("throws error when template not configured", async () => {
160
- // Remove git config
161
- await Bun.spawn(["git", "config", "--unset", "agency.template"], {
162
- cwd: gitRoot,
163
- stdout: "ignore",
164
- stderr: "ignore",
165
- }).exited
166
-
167
- const originalCwd = process.cwd()
168
- process.chdir(gitRoot)
169
-
170
- try {
171
- await expect(
172
- runTestEffect(templateDelete({ files: ["test.md"], silent: true })),
173
- ).rejects.toThrow("Repository not initialized")
174
- } finally {
175
- process.chdir(originalCwd)
176
- }
177
- })
178
- })
@@ -1,98 +0,0 @@
1
- import { resolve } from "path"
2
- import { rm } from "node:fs/promises"
3
- import { Effect } from "effect"
4
- import type { BaseCommandOptions } from "../utils/command"
5
- import { TemplateService } from "../services/TemplateService"
6
- import { FileSystemService } from "../services/FileSystemService"
7
- import { RepositoryNotInitializedError } from "../errors"
8
- import highlight, { done } from "../utils/colors"
9
- import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
10
-
11
- interface DeleteOptions extends BaseCommandOptions {
12
- files?: string[]
13
- }
14
-
15
- export const templateDelete = (options: DeleteOptions = {}) =>
16
- Effect.gen(function* () {
17
- const { files: filesToDelete = [] } = options
18
- const { log, verboseLog } = createLoggers(options)
19
-
20
- const templateService = yield* TemplateService
21
- const fs = yield* FileSystemService
22
-
23
- const gitRoot = yield* ensureGitRepo()
24
-
25
- // Get template name from git config
26
- const templateName = yield* getTemplateName(gitRoot)
27
- if (!templateName) {
28
- return yield* Effect.fail(new RepositoryNotInitializedError())
29
- }
30
-
31
- if (filesToDelete.length === 0) {
32
- return yield* Effect.fail(
33
- new Error(
34
- "No files specified. Usage: agency template delete <file> [file ...]",
35
- ),
36
- )
37
- }
38
-
39
- verboseLog(`Deleting from template: ${highlight.template(templateName)}`)
40
-
41
- // Get template directory
42
- const templateDir = yield* templateService.getTemplateDir(templateName)
43
-
44
- // Delete each file
45
- for (const filePath of filesToDelete) {
46
- const templateFilePath = resolve(templateDir, filePath)
47
-
48
- // Check if file exists
49
- const exists = yield* fs.exists(templateFilePath)
50
- if (!exists) {
51
- verboseLog(`Skipping ${filePath} (does not exist in template)`)
52
- continue
53
- }
54
-
55
- // Delete the file/directory
56
- yield* Effect.tryPromise({
57
- try: () => rm(templateFilePath, { recursive: true, force: true }),
58
- catch: (error) => new Error(`Failed to delete ${filePath}: ${error}`),
59
- })
60
-
61
- log(
62
- done(
63
- `Deleted ${highlight.file(filePath)} from ${highlight.template(templateName)} template`,
64
- ),
65
- )
66
- }
67
- })
68
-
69
- // Help text for reference (not exported as it's handled by template command)
70
- const help = `
71
- Usage: agency template delete <file> [file ...] [options]
72
-
73
- Delete specified files from the configured template directory.
74
-
75
- This command removes files from the template directory configured in
76
- .git/config (agency.template).
77
-
78
- Arguments:
79
- <file> File path to delete (relative to template root)
80
- [file ...] Additional files to delete
81
-
82
- Options:
83
- -h, --help Show this help message
84
- -s, --silent Suppress output messages
85
- -v, --verbose Show verbose output
86
-
87
- Examples:
88
- agency template delete AGENTS.md # Delete specific file
89
- agency template delete docs/ src/ # Delete directories
90
- agency template delete file1 file2 file3 # Delete multiple files
91
-
92
- Notes:
93
- - Requires agency.template to be set (run 'agency init' first)
94
- - At least one file must be specified
95
- - Files are deleted from ~/.config/agency/templates/{template-name}/
96
- - Non-existent files are skipped with a warning in verbose mode
97
- - Directories are deleted recursively
98
- `
@@ -1,135 +0,0 @@
1
- import { describe, expect, test, beforeEach, afterEach } from "bun:test"
2
- import { templateList } from "./template-list"
3
- import { mkdir, writeFile, rm } from "node:fs/promises"
4
- import { join } from "node:path"
5
- import { tmpdir } from "node:os"
6
- import { runTestEffect } from "../test-utils"
7
-
8
- describe("template list command", () => {
9
- let testDir: string
10
- let gitRoot: string
11
- let templateDir: string
12
-
13
- beforeEach(async () => {
14
- // Create temporary test directory
15
- testDir = join(tmpdir(), `agency-test-list-${Date.now()}`)
16
- await mkdir(testDir, { recursive: true })
17
- gitRoot = join(testDir, "repo")
18
- await mkdir(gitRoot, { recursive: true })
19
-
20
- // Initialize git repo
21
- await Bun.spawn(["git", "init"], {
22
- cwd: gitRoot,
23
- stdout: "ignore",
24
- stderr: "ignore",
25
- }).exited
26
-
27
- // Set up config dir
28
- const configDir = join(testDir, "config")
29
- templateDir = join(configDir, "templates", "test-template")
30
- await mkdir(templateDir, { recursive: true })
31
-
32
- // Set environment variable for config
33
- process.env.AGENCY_CONFIG_DIR = configDir
34
-
35
- // Set git config
36
- await Bun.spawn(["git", "config", "agency.template", "test-template"], {
37
- cwd: gitRoot,
38
- stdout: "ignore",
39
- stderr: "ignore",
40
- }).exited
41
- })
42
-
43
- afterEach(async () => {
44
- // Clean up
45
- await rm(testDir, { recursive: true, force: true })
46
- delete process.env.AGENCY_CONFIG_DIR
47
- })
48
-
49
- test("lists files in template directory", async () => {
50
- // Create test files in template
51
- await writeFile(join(templateDir, "AGENTS.md"), "# Agents")
52
- await writeFile(join(templateDir, "opencode.json"), "{}")
53
- await mkdir(join(templateDir, "docs"), { recursive: true })
54
- await writeFile(join(templateDir, "docs", "README.md"), "# Docs")
55
-
56
- // Capture output
57
- const output: string[] = []
58
- const originalLog = console.log
59
- console.log = (...args: any[]) => {
60
- output.push(args.join(" "))
61
- }
62
-
63
- const originalCwd = process.cwd()
64
- process.chdir(gitRoot)
65
-
66
- try {
67
- await runTestEffect(templateList({ silent: false }))
68
- } finally {
69
- process.chdir(originalCwd)
70
- console.log = originalLog
71
- }
72
-
73
- expect(output.length).toBeGreaterThan(0)
74
- expect(output.some((line) => line.includes("AGENTS.md"))).toBe(true)
75
- expect(output.some((line) => line.includes("opencode.json"))).toBe(true)
76
- expect(output.some((line) => line.includes("docs/README.md"))).toBe(true)
77
- })
78
-
79
- test("handles empty template directory", async () => {
80
- // Capture output
81
- const output: string[] = []
82
- const originalLog = console.log
83
- console.log = (...args: any[]) => {
84
- output.push(args.join(" "))
85
- }
86
-
87
- const originalCwd = process.cwd()
88
- process.chdir(gitRoot)
89
-
90
- try {
91
- await runTestEffect(templateList({ silent: false }))
92
- } finally {
93
- process.chdir(originalCwd)
94
- console.log = originalLog
95
- }
96
-
97
- expect(output.some((line) => line.includes("has no files"))).toBe(true)
98
- })
99
-
100
- test("throws error when not in git repository", async () => {
101
- const nonGitDir = join(testDir, "non-git")
102
- await mkdir(nonGitDir, { recursive: true })
103
-
104
- const originalCwd = process.cwd()
105
- process.chdir(nonGitDir)
106
-
107
- try {
108
- await expect(
109
- runTestEffect(templateList({ silent: true })),
110
- ).rejects.toThrow("Not in a git repository")
111
- } finally {
112
- process.chdir(originalCwd)
113
- }
114
- })
115
-
116
- test("throws error when template not configured", async () => {
117
- // Remove git config
118
- await Bun.spawn(["git", "config", "--unset", "agency.template"], {
119
- cwd: gitRoot,
120
- stdout: "ignore",
121
- stderr: "ignore",
122
- }).exited
123
-
124
- const originalCwd = process.cwd()
125
- process.chdir(gitRoot)
126
-
127
- try {
128
- await expect(
129
- runTestEffect(templateList({ silent: true })),
130
- ).rejects.toThrow("Repository not initialized")
131
- } finally {
132
- process.chdir(originalCwd)
133
- }
134
- })
135
- })
@@ -1,87 +0,0 @@
1
- import { Effect } from "effect"
2
- import type { BaseCommandOptions } from "../utils/command"
3
- import { TemplateService } from "../services/TemplateService"
4
- import { FileSystemService } from "../services/FileSystemService"
5
- import { RepositoryNotInitializedError } from "../errors"
6
- import highlight from "../utils/colors"
7
- import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
8
-
9
- interface ListOptions extends BaseCommandOptions {}
10
-
11
- export const templateList = (options: ListOptions = {}) =>
12
- Effect.gen(function* () {
13
- const { log, verboseLog } = createLoggers(options)
14
-
15
- const templateService = yield* TemplateService
16
- const fs = yield* FileSystemService
17
-
18
- const gitRoot = yield* ensureGitRepo()
19
-
20
- // Get template name from git config
21
- const templateName = yield* getTemplateName(gitRoot)
22
- if (!templateName) {
23
- return yield* Effect.fail(new RepositoryNotInitializedError())
24
- }
25
-
26
- verboseLog(`Listing files in template: ${highlight.template(templateName)}`)
27
-
28
- // Get template directory
29
- const templateDir = yield* templateService.getTemplateDir(templateName)
30
-
31
- // Check if template directory exists and is a directory
32
- const isDirectory = yield* Effect.tryPromise({
33
- try: async () => {
34
- const file = Bun.file(templateDir)
35
- const stat = await file.stat()
36
- return stat?.isDirectory?.() ?? false
37
- },
38
- catch: (error) =>
39
- new Error(`Failed to check template directory: ${error}`),
40
- })
41
- if (!isDirectory) {
42
- return yield* Effect.fail(
43
- new Error(
44
- `Template directory does not exist: ${highlight.template(templateName)}`,
45
- ),
46
- )
47
- }
48
-
49
- // Collect all files recursively
50
- const files = yield* fs.collectFiles(templateDir, {
51
- exclude: [".gitkeep"],
52
- sort: true,
53
- })
54
-
55
- if (files.length === 0) {
56
- log(`Template ${highlight.template(templateName)} has no files`)
57
- return
58
- }
59
-
60
- for (const file of files) {
61
- log(highlight.file(file))
62
- }
63
- })
64
-
65
- // Help text for reference (not exported as it's handled by template command)
66
- const help = `
67
- Usage: agency template list [options]
68
-
69
- List all files in the configured template directory.
70
-
71
- This command displays all files stored in the template configured for the
72
- current git repository.
73
-
74
- Options:
75
- -h, --help Show this help message
76
- -s, --silent Suppress output messages
77
- -v, --verbose Show verbose output
78
-
79
- Example:
80
- agency template list # List files in current template
81
-
82
- Notes:
83
- - Requires agency.template to be set (run 'agency init' first)
84
- - Shows files relative to template root directory
85
- - Files are listed in alphabetical order
86
- - Template directory must exist (created when you save files)
87
- `