@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.
Files changed (132) hide show
  1. package/README.md +313 -117
  2. package/cli.ts +124 -420
  3. package/index.ts +1 -1
  4. package/package.json +12 -17
  5. package/skills/agency/SKILL.md +266 -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 +326 -399
  24. package/src/commands/work.ts +153 -153
  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/work-target.test.ts +77 -0
  53. package/src/workbase/work-target.ts +180 -0
  54. package/src/workbase/worktree-command.test.ts +61 -0
  55. package/src/workbase/worktree-command.ts +52 -0
  56. package/src/commands/base.test.ts +0 -198
  57. package/src/commands/base.ts +0 -198
  58. package/src/commands/clean.test.ts +0 -299
  59. package/src/commands/clean.ts +0 -274
  60. package/src/commands/completions.test.ts +0 -51
  61. package/src/commands/completions.ts +0 -308
  62. package/src/commands/emit.integration.test.ts +0 -549
  63. package/src/commands/emit.test.ts +0 -576
  64. package/src/commands/emit.ts +0 -700
  65. package/src/commands/emitted.test.ts +0 -226
  66. package/src/commands/emitted.ts +0 -57
  67. package/src/commands/loop.test.ts +0 -621
  68. package/src/commands/loop.ts +0 -406
  69. package/src/commands/merge.integration.test.ts +0 -195
  70. package/src/commands/merge.test.ts +0 -246
  71. package/src/commands/merge.ts +0 -244
  72. package/src/commands/pull.test.ts +0 -395
  73. package/src/commands/pull.ts +0 -205
  74. package/src/commands/push.test.ts +0 -641
  75. package/src/commands/push.ts +0 -482
  76. package/src/commands/rebase.test.ts +0 -521
  77. package/src/commands/rebase.ts +0 -232
  78. package/src/commands/save.test.ts +0 -247
  79. package/src/commands/save.ts +0 -162
  80. package/src/commands/source.test.ts +0 -195
  81. package/src/commands/source.ts +0 -72
  82. package/src/commands/switch.test.ts +0 -194
  83. package/src/commands/switch.ts +0 -84
  84. package/src/commands/task-branching.test.ts +0 -633
  85. package/src/commands/task-continue.test.ts +0 -311
  86. package/src/commands/task-edit.test.ts +0 -235
  87. package/src/commands/task-main.test.ts +0 -1066
  88. package/src/commands/task-squash.test.ts +0 -266
  89. package/src/commands/tasks.test.ts +0 -391
  90. package/src/commands/tasks.ts +0 -161
  91. package/src/commands/template-delete.test.ts +0 -178
  92. package/src/commands/template-delete.ts +0 -98
  93. package/src/commands/template-list.test.ts +0 -135
  94. package/src/commands/template-list.ts +0 -87
  95. package/src/commands/template-view.test.ts +0 -158
  96. package/src/commands/template-view.ts +0 -86
  97. package/src/commands/template.test.ts +0 -32
  98. package/src/commands/template.ts +0 -96
  99. package/src/commands/use.test.ts +0 -87
  100. package/src/commands/use.ts +0 -97
  101. package/src/constants.ts +0 -10
  102. package/src/errors.ts +0 -17
  103. package/src/schemas.ts +0 -33
  104. package/src/services/AgencyMetadataService.ts +0 -311
  105. package/src/services/ClaudeService.test.ts +0 -184
  106. package/src/services/ClaudeService.ts +0 -91
  107. package/src/services/ConfigService.ts +0 -115
  108. package/src/services/FilterRepoService.ts +0 -164
  109. package/src/services/FormatterService.test.ts +0 -432
  110. package/src/services/FormatterService.ts +0 -219
  111. package/src/services/GitService.ts +0 -1187
  112. package/src/services/MockFilterRepoService.ts +0 -140
  113. package/src/services/OpencodeService.ts +0 -263
  114. package/src/services/PromptService.ts +0 -183
  115. package/src/services/TemplateService.ts +0 -81
  116. package/src/types/native-exec.d.ts +0 -8
  117. package/src/utils/colors.ts +0 -178
  118. package/src/utils/git-path.ts +0 -6
  119. package/src/utils/glob.test.ts +0 -161
  120. package/src/utils/glob.ts +0 -78
  121. package/src/utils/paths.ts +0 -51
  122. package/src/utils/pr-branch.test.ts +0 -376
  123. package/src/utils/pr-branch.ts +0 -508
  124. package/src/utils/spinner.test.ts +0 -45
  125. package/src/utils/spinner.ts +0 -96
  126. package/src/utils/task-parser.test.ts +0 -172
  127. package/src/utils/task-parser.ts +0 -93
  128. package/templates/AGENCY.md +0 -22
  129. package/templates/AGENTS.md +0 -11
  130. package/templates/CLAUDE.md +0 -3
  131. package/templates/TASK.md +0 -5
  132. 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
- `