@markjaquith/agency 0.5.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/cli.ts +569 -0
  4. package/index.ts +1 -0
  5. package/package.json +65 -0
  6. package/src/commands/base.test.ts +198 -0
  7. package/src/commands/base.ts +198 -0
  8. package/src/commands/clean.test.ts +299 -0
  9. package/src/commands/clean.ts +320 -0
  10. package/src/commands/emit.test.ts +412 -0
  11. package/src/commands/emit.ts +521 -0
  12. package/src/commands/emitted.test.ts +226 -0
  13. package/src/commands/emitted.ts +57 -0
  14. package/src/commands/init.test.ts +311 -0
  15. package/src/commands/init.ts +140 -0
  16. package/src/commands/merge.test.ts +365 -0
  17. package/src/commands/merge.ts +253 -0
  18. package/src/commands/pull.test.ts +385 -0
  19. package/src/commands/pull.ts +205 -0
  20. package/src/commands/push.test.ts +394 -0
  21. package/src/commands/push.ts +346 -0
  22. package/src/commands/save.test.ts +247 -0
  23. package/src/commands/save.ts +162 -0
  24. package/src/commands/source.test.ts +195 -0
  25. package/src/commands/source.ts +72 -0
  26. package/src/commands/status.test.ts +489 -0
  27. package/src/commands/status.ts +258 -0
  28. package/src/commands/switch.test.ts +194 -0
  29. package/src/commands/switch.ts +84 -0
  30. package/src/commands/task-branching.test.ts +334 -0
  31. package/src/commands/task-edit.test.ts +141 -0
  32. package/src/commands/task-main.test.ts +872 -0
  33. package/src/commands/task.ts +712 -0
  34. package/src/commands/tasks.test.ts +335 -0
  35. package/src/commands/tasks.ts +155 -0
  36. package/src/commands/template-delete.test.ts +178 -0
  37. package/src/commands/template-delete.ts +98 -0
  38. package/src/commands/template-list.test.ts +135 -0
  39. package/src/commands/template-list.ts +87 -0
  40. package/src/commands/template-view.test.ts +158 -0
  41. package/src/commands/template-view.ts +86 -0
  42. package/src/commands/template.test.ts +32 -0
  43. package/src/commands/template.ts +96 -0
  44. package/src/commands/use.test.ts +87 -0
  45. package/src/commands/use.ts +97 -0
  46. package/src/commands/work.test.ts +462 -0
  47. package/src/commands/work.ts +193 -0
  48. package/src/errors.ts +17 -0
  49. package/src/schemas.ts +33 -0
  50. package/src/services/AgencyMetadataService.ts +287 -0
  51. package/src/services/ClaudeService.test.ts +184 -0
  52. package/src/services/ClaudeService.ts +91 -0
  53. package/src/services/ConfigService.ts +115 -0
  54. package/src/services/FileSystemService.ts +222 -0
  55. package/src/services/GitService.ts +751 -0
  56. package/src/services/OpencodeService.ts +263 -0
  57. package/src/services/PromptService.ts +183 -0
  58. package/src/services/TemplateService.ts +75 -0
  59. package/src/test-utils.ts +362 -0
  60. package/src/types/native-exec.d.ts +8 -0
  61. package/src/types.ts +216 -0
  62. package/src/utils/colors.ts +178 -0
  63. package/src/utils/command.ts +17 -0
  64. package/src/utils/effect.ts +281 -0
  65. package/src/utils/exec.ts +48 -0
  66. package/src/utils/paths.ts +51 -0
  67. package/src/utils/pr-branch.test.ts +372 -0
  68. package/src/utils/pr-branch.ts +473 -0
  69. package/src/utils/process.ts +110 -0
  70. package/src/utils/spinner.ts +82 -0
  71. package/templates/AGENCY.md +20 -0
  72. package/templates/AGENTS.md +11 -0
  73. package/templates/CLAUDE.md +3 -0
  74. package/templates/TASK.md +5 -0
  75. package/templates/opencode.json +4 -0
@@ -0,0 +1,98 @@
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
+ `
@@ -0,0 +1,135 @@
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
+ })
@@ -0,0 +1,87 @@
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
+ `
@@ -0,0 +1,158 @@
1
+ import { describe, expect, test, beforeEach, afterEach } from "bun:test"
2
+ import { templateView } from "./template-view"
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 view 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-view-${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("views file from template directory", async () => {
50
+ // Create test file in template
51
+ const content = "# Test File\n\nThis is a test."
52
+ const testFile = join(templateDir, "test.md")
53
+ await writeFile(testFile, content)
54
+
55
+ const originalCwd = process.cwd()
56
+ process.chdir(gitRoot)
57
+
58
+ try {
59
+ // Just verify the command doesn't throw - output goes to stdout
60
+ await runTestEffect(templateView({ file: "test.md", silent: true }))
61
+
62
+ // Verify the file exists and has expected content
63
+ const fileContent = await Bun.file(testFile).text()
64
+ expect(fileContent).toContain("# Test File")
65
+ expect(fileContent).toContain("This is a test.")
66
+ } finally {
67
+ process.chdir(originalCwd)
68
+ }
69
+ })
70
+
71
+ test("views file in subdirectory", async () => {
72
+ // Create test file in subdirectory
73
+ const docsDir = join(templateDir, "docs")
74
+ await mkdir(docsDir, { recursive: true })
75
+ const content = "# Documentation\n\nDocs content."
76
+ const testFile = join(docsDir, "README.md")
77
+ await writeFile(testFile, content)
78
+
79
+ const originalCwd = process.cwd()
80
+ process.chdir(gitRoot)
81
+
82
+ try {
83
+ // Just verify the command doesn't throw - output goes to stdout
84
+ await runTestEffect(
85
+ templateView({ file: "docs/README.md", silent: true }),
86
+ )
87
+
88
+ // Verify the file exists and has expected content
89
+ const fileContent = await Bun.file(testFile).text()
90
+ expect(fileContent).toContain("# Documentation")
91
+ expect(fileContent).toContain("Docs content.")
92
+ } finally {
93
+ process.chdir(originalCwd)
94
+ }
95
+ })
96
+
97
+ test("throws error when file does not exist", async () => {
98
+ const originalCwd = process.cwd()
99
+ process.chdir(gitRoot)
100
+
101
+ try {
102
+ await expect(
103
+ runTestEffect(templateView({ file: "nonexistent.md", silent: true })),
104
+ ).rejects.toThrow("does not exist in template")
105
+ } finally {
106
+ process.chdir(originalCwd)
107
+ }
108
+ })
109
+
110
+ test("throws error when no file specified", async () => {
111
+ const originalCwd = process.cwd()
112
+ process.chdir(gitRoot)
113
+
114
+ try {
115
+ await expect(
116
+ runTestEffect(templateView({ silent: true })),
117
+ ).rejects.toThrow("File path is required")
118
+ } finally {
119
+ process.chdir(originalCwd)
120
+ }
121
+ })
122
+
123
+ test("throws error when not in git repository", async () => {
124
+ const nonGitDir = join(testDir, "non-git")
125
+ await mkdir(nonGitDir, { recursive: true })
126
+
127
+ const originalCwd = process.cwd()
128
+ process.chdir(nonGitDir)
129
+
130
+ try {
131
+ await expect(
132
+ runTestEffect(templateView({ file: "test.md", silent: true })),
133
+ ).rejects.toThrow("Not in a git repository")
134
+ } finally {
135
+ process.chdir(originalCwd)
136
+ }
137
+ })
138
+
139
+ test("throws error when template not configured", async () => {
140
+ // Remove git config
141
+ await Bun.spawn(["git", "config", "--unset", "agency.template"], {
142
+ cwd: gitRoot,
143
+ stdout: "ignore",
144
+ stderr: "ignore",
145
+ }).exited
146
+
147
+ const originalCwd = process.cwd()
148
+ process.chdir(gitRoot)
149
+
150
+ try {
151
+ await expect(
152
+ runTestEffect(templateView({ file: "test.md", silent: true })),
153
+ ).rejects.toThrow("Repository not initialized")
154
+ } finally {
155
+ process.chdir(originalCwd)
156
+ }
157
+ })
158
+ })
@@ -0,0 +1,86 @@
1
+ import { resolve } 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 from "../utils/colors"
8
+ import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
9
+
10
+ interface ViewOptions extends BaseCommandOptions {
11
+ file?: string
12
+ }
13
+
14
+ export const templateView = (options: ViewOptions = {}) =>
15
+ Effect.gen(function* () {
16
+ const { file: fileToView, silent = false } = options
17
+ const { verboseLog } = createLoggers(options)
18
+
19
+ const templateService = yield* TemplateService
20
+ const fs = yield* FileSystemService
21
+
22
+ const gitRoot = yield* ensureGitRepo()
23
+
24
+ // Get template name from git config
25
+ const templateName = yield* getTemplateName(gitRoot)
26
+ if (!templateName) {
27
+ return yield* Effect.fail(new RepositoryNotInitializedError())
28
+ }
29
+
30
+ if (!fileToView) {
31
+ return yield* Effect.fail(
32
+ new Error("File path is required. Usage: agency template view <file>"),
33
+ )
34
+ }
35
+
36
+ verboseLog(
37
+ `Viewing ${highlight.file(fileToView)} from ${highlight.template(templateName)} template`,
38
+ )
39
+
40
+ // Get template directory
41
+ const templateDir = yield* templateService.getTemplateDir(templateName)
42
+ const templateFilePath = resolve(templateDir, fileToView)
43
+
44
+ // Check if file exists
45
+ const exists = yield* fs.exists(templateFilePath)
46
+ if (!exists) {
47
+ return yield* Effect.fail(
48
+ new Error(
49
+ `File ${highlight.file(fileToView)} does not exist in template ${highlight.template(templateName)}`,
50
+ ),
51
+ )
52
+ }
53
+
54
+ // Read and display the file
55
+ const content = yield* fs.readFile(templateFilePath)
56
+ if (!silent) {
57
+ console.log(content)
58
+ }
59
+ })
60
+
61
+ // Help text for reference (not exported as it's handled by template command)
62
+ const help = `
63
+ Usage: agency template view <file> [options]
64
+
65
+ View the contents of a file in the configured template directory.
66
+
67
+ This command displays the contents of a file stored in the template
68
+ configured for the current git repository.
69
+
70
+ Arguments:
71
+ <file> File path to view (relative to template root)
72
+
73
+ Options:
74
+ -h, --help Show this help message
75
+ -s, --silent Suppress verbose messages (file content still shown)
76
+ -v, --verbose Show verbose output
77
+
78
+ Examples:
79
+ agency template view AGENTS.md # View AGENTS.md from template
80
+ agency template view docs/README.md # View file in subdirectory
81
+
82
+ Notes:
83
+ - Requires agency.template to be set (run 'agency init' first)
84
+ - File path is relative to template root directory
85
+ - File content is displayed directly to stdout
86
+ `
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { template } from "./template"
3
+ import { runTestEffect } from "../test-utils"
4
+
5
+ describe("template command", () => {
6
+ test("throws error when no subcommand provided", async () => {
7
+ await expect(
8
+ runTestEffect(
9
+ template({
10
+ args: [],
11
+ silent: true,
12
+ }),
13
+ ),
14
+ ).rejects.toThrow(
15
+ "Subcommand is required. Available subcommands: use, save, list, view, delete",
16
+ )
17
+ })
18
+
19
+ test("throws error for unknown subcommand", async () => {
20
+ await expect(
21
+ runTestEffect(
22
+ template({
23
+ subcommand: "invalid",
24
+ args: [],
25
+ silent: true,
26
+ }),
27
+ ),
28
+ ).rejects.toThrow(
29
+ "Unknown template subcommand 'invalid'. Available: use, save, list, view, delete",
30
+ )
31
+ })
32
+ })
@@ -0,0 +1,96 @@
1
+ import { Effect } from "effect"
2
+ import { use } from "./use"
3
+ import { save } from "./save"
4
+ import { templateList } from "./template-list"
5
+ import { templateDelete } from "./template-delete"
6
+ import { templateView } from "./template-view"
7
+
8
+ export const help = `
9
+ Usage: agency template <subcommand> [options]
10
+
11
+ Subcommands:
12
+ use [template] Set template for this repository
13
+ save <file|dir> ... Save files/dirs to configured template
14
+ list List all files in configured template
15
+ view <file> View contents of a file in template
16
+ delete <file> ... Delete files from configured template
17
+
18
+ Examples:
19
+ agency template use # Interactively select template
20
+ agency template use work # Set template to 'work'
21
+ agency template save AGENTS.md # Save specific file to template
22
+ agency template list # List files in current template
23
+ agency template view AGENTS.md # View file from template
24
+ agency template delete AGENTS.md # Delete file from template
25
+
26
+ For more information about a subcommand, run:
27
+ agency template <subcommand> --help
28
+ `
29
+
30
+ export const template = (options: {
31
+ subcommand?: string
32
+ args: string[]
33
+ silent?: boolean
34
+ verbose?: boolean
35
+ template?: string
36
+ }) =>
37
+ Effect.gen(function* () {
38
+ const {
39
+ subcommand,
40
+ args,
41
+ silent,
42
+ verbose,
43
+ template: templateName,
44
+ } = options
45
+
46
+ if (!subcommand) {
47
+ return yield* Effect.fail(
48
+ new Error(
49
+ "Subcommand is required. Available subcommands: use, save, list, view, delete\n\nRun 'agency template --help' for usage information.",
50
+ ),
51
+ )
52
+ }
53
+
54
+ switch (subcommand) {
55
+ case "use":
56
+ return yield* use({
57
+ template: args[0] || templateName,
58
+ silent,
59
+ verbose,
60
+ })
61
+
62
+ case "save":
63
+ return yield* save({
64
+ files: args,
65
+ silent,
66
+ verbose,
67
+ })
68
+
69
+ case "list":
70
+ return yield* templateList({
71
+ silent,
72
+ verbose,
73
+ })
74
+
75
+ case "view":
76
+ return yield* templateView({
77
+ file: args[0],
78
+ silent,
79
+ verbose,
80
+ })
81
+
82
+ case "delete":
83
+ return yield* templateDelete({
84
+ files: args,
85
+ silent,
86
+ verbose,
87
+ })
88
+
89
+ default:
90
+ return yield* Effect.fail(
91
+ new Error(
92
+ `Unknown template subcommand '${subcommand}'. Available: use, save, list, view, delete`,
93
+ ),
94
+ )
95
+ }
96
+ })