@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,140 +0,0 @@
1
- import { Effect, Layer } from "effect"
2
- import { FilterRepoService } from "./FilterRepoService"
3
-
4
- /**
5
- * Captured filter-repo call for verification in tests.
6
- */
7
- export interface CapturedFilterRepoCall {
8
- gitRoot: string
9
- args: readonly string[]
10
- env?: Record<string, string>
11
- streamOutput?: boolean
12
- progressIntervalMs?: number
13
- }
14
-
15
- /**
16
- * Global state for captured filter-repo calls.
17
- * Tests can inspect this to verify correct commands were constructed.
18
- */
19
- let capturedCalls: CapturedFilterRepoCall[] = []
20
-
21
- /**
22
- * Clear all captured filter-repo calls.
23
- * Call this in beforeEach() to reset state between tests.
24
- */
25
- export function clearCapturedFilterRepoCalls(): void {
26
- capturedCalls = []
27
- }
28
-
29
- /**
30
- * Get all captured filter-repo calls.
31
- * @returns Array of captured calls
32
- */
33
- export function getCapturedFilterRepoCalls(): readonly CapturedFilterRepoCall[] {
34
- return capturedCalls
35
- }
36
-
37
- /**
38
- * Get the last captured filter-repo call.
39
- * @returns The last captured call, or undefined if none
40
- */
41
- export function getLastCapturedFilterRepoCall():
42
- | CapturedFilterRepoCall
43
- | undefined {
44
- return capturedCalls[capturedCalls.length - 1]
45
- }
46
-
47
- /**
48
- * Mock implementation of FilterRepoService.
49
- *
50
- * This mock:
51
- * - Always returns true for isInstalled()
52
- * - Captures the arguments passed to run() without executing
53
- * - Returns a successful result with exit code 0
54
- *
55
- * Use getCapturedFilterRepoCalls() to verify the correct commands were constructed.
56
- */
57
- export class MockFilterRepoService extends Effect.Service<MockFilterRepoService>()(
58
- "FilterRepoService", // Same tag as the real service to replace it
59
- {
60
- sync: () => ({
61
- isInstalled: () => Effect.succeed(true),
62
-
63
- run: (
64
- gitRoot: string,
65
- args: readonly string[],
66
- options?: {
67
- readonly env?: Record<string, string>
68
- readonly verboseLog?: (message: string) => void
69
- readonly streamOutput?: boolean
70
- readonly progressIntervalMs?: number
71
- },
72
- ) => {
73
- // Capture the call
74
- capturedCalls.push({
75
- gitRoot,
76
- args,
77
- env: options?.env,
78
- streamOutput: options?.streamOutput,
79
- progressIntervalMs: options?.progressIntervalMs,
80
- })
81
-
82
- // Return a successful result
83
- return Effect.succeed({
84
- exitCode: 0,
85
- stdout: "",
86
- stderr: "",
87
- })
88
- },
89
-
90
- filterFiles: (
91
- gitRoot: string,
92
- options: {
93
- readonly refs?: string
94
- readonly pathsToRemove?: readonly string[]
95
- readonly pathRenames?: readonly { from: string; to: string }[]
96
- readonly force?: boolean
97
- },
98
- ) => {
99
- // Build the args like the real implementation would
100
- const args: string[] = []
101
-
102
- if (options.refs) {
103
- args.push("--refs", options.refs)
104
- }
105
-
106
- if (options.pathsToRemove) {
107
- for (const path of options.pathsToRemove) {
108
- args.push("--invert-paths", "--path", path)
109
- }
110
- }
111
-
112
- if (options.pathRenames) {
113
- for (const rename of options.pathRenames) {
114
- args.push("--path-rename", `${rename.from}:${rename.to}`)
115
- }
116
- }
117
-
118
- if (options.force) {
119
- args.push("--force")
120
- }
121
-
122
- args.push("--prune-empty=always")
123
-
124
- // Capture the call
125
- capturedCalls.push({
126
- gitRoot,
127
- args,
128
- env: { GIT_CONFIG_GLOBAL: "" },
129
- })
130
-
131
- // Return a successful result
132
- return Effect.succeed({
133
- exitCode: 0,
134
- stdout: "",
135
- stderr: "",
136
- })
137
- },
138
- }),
139
- },
140
- ) {}
@@ -1,263 +0,0 @@
1
- import { Effect, Data } from "effect"
2
- import { parse as parseJsonc, type ParseError } from "jsonc-parser"
3
- import { FileSystemService } from "./FileSystemService"
4
-
5
- // Error types for Opencode operations
6
- class OpencodeError extends Data.TaggedError("OpencodeError")<{
7
- message: string
8
- cause?: unknown
9
- }> {}
10
-
11
- interface OpencodeConfig {
12
- instructions?: string[]
13
- [key: string]: unknown
14
- }
15
-
16
- interface OpencodeFileInfo {
17
- extension: "json" | "jsonc"
18
- relativePath: string
19
- }
20
-
21
- /**
22
- * Service for handling opencode.json/opencode.jsonc files
23
- * with merging capabilities to preserve existing configuration.
24
- */
25
- export class OpencodeService extends Effect.Service<OpencodeService>()(
26
- "OpencodeService",
27
- {
28
- sync: () => ({
29
- /**
30
- * Detect which opencode config file exists (json or jsonc).
31
- * Checks in order: .opencode/opencode.jsonc, .opencode/opencode.json,
32
- * opencode.jsonc, opencode.json
33
- * Returns the file info if found, null otherwise.
34
- */
35
- detectOpencodeFile: (gitRoot: string) =>
36
- Effect.gen(function* () {
37
- const fs = yield* FileSystemService
38
-
39
- // Check .opencode directory first
40
- const dotOpencodeJsoncPath = `${gitRoot}/.opencode/opencode.jsonc`
41
- const dotOpencodeJsoncExists = yield* fs.exists(dotOpencodeJsoncPath)
42
- if (dotOpencodeJsoncExists) {
43
- return {
44
- extension: "jsonc" as const,
45
- relativePath: ".opencode/opencode.jsonc",
46
- }
47
- }
48
-
49
- const dotOpencodeJsonPath = `${gitRoot}/.opencode/opencode.json`
50
- const dotOpencodeJsonExists = yield* fs.exists(dotOpencodeJsonPath)
51
- if (dotOpencodeJsonExists) {
52
- return {
53
- extension: "json" as const,
54
- relativePath: ".opencode/opencode.json",
55
- }
56
- }
57
-
58
- // Check for opencode.jsonc in root (takes precedence over opencode.json)
59
- const jsoncPath = `${gitRoot}/opencode.jsonc`
60
- const jsoncExists = yield* fs.exists(jsoncPath)
61
- if (jsoncExists) {
62
- return {
63
- extension: "jsonc" as const,
64
- relativePath: "opencode.jsonc",
65
- }
66
- }
67
-
68
- // Check for opencode.json in root
69
- const jsonPath = `${gitRoot}/opencode.json`
70
- const jsonExists = yield* fs.exists(jsonPath)
71
- if (jsonExists) {
72
- return { extension: "json" as const, relativePath: "opencode.json" }
73
- }
74
-
75
- return null
76
- }),
77
-
78
- /**
79
- * Parse JSON or JSONC content using jsonc-parser for robust comment handling.
80
- */
81
- parseConfig: (content: string, extension: "json" | "jsonc") =>
82
- Effect.try({
83
- try: () => {
84
- if (extension === "jsonc") {
85
- // Use jsonc-parser for robust JSONC parsing with comments and trailing commas
86
- const errors: ParseError[] = []
87
- const result = parseJsonc(content, errors, {
88
- allowTrailingComma: true,
89
- }) as OpencodeConfig
90
-
91
- // Check if there were any parse errors
92
- if (errors.length > 0) {
93
- throw new Error(
94
- `JSON Parse error: ${errors.map((e) => `Error code ${e.error} at offset ${e.offset}`).join(", ")}`,
95
- )
96
- }
97
-
98
- return result
99
- }
100
- return JSON.parse(content) as OpencodeConfig
101
- },
102
- catch: (error) =>
103
- new OpencodeError({
104
- message: `Failed to parse opencode.${extension}`,
105
- cause: error,
106
- }),
107
- }),
108
-
109
- /**
110
- * Merge our instructions into an existing opencode config.
111
- * Preserves all existing properties and adds our instructions to the array.
112
- */
113
- mergeConfig: (
114
- existingConfig: OpencodeConfig,
115
- instructionsToAdd: string[],
116
- ) =>
117
- Effect.sync(() => {
118
- const merged = { ...existingConfig }
119
-
120
- // Get existing instructions or initialize empty array
121
- const existingInstructions = Array.isArray(merged.instructions)
122
- ? merged.instructions
123
- : []
124
-
125
- // Add our instructions, avoiding duplicates
126
- const newInstructions = instructionsToAdd.filter(
127
- (instruction) => !existingInstructions.includes(instruction),
128
- )
129
-
130
- // Update instructions array
131
- merged.instructions = [...existingInstructions, ...newInstructions]
132
-
133
- return merged
134
- }),
135
-
136
- /**
137
- * Format the config back to string, preserving formatting.
138
- * For JSONC, we use JSON formatting (comments are not preserved).
139
- */
140
- formatConfig: (config: OpencodeConfig) =>
141
- Effect.sync(() => JSON.stringify(config, null, "\t") + "\n"),
142
-
143
- /**
144
- * Read, merge, and write the opencode config file.
145
- * Returns the relative path of the file that was written.
146
- */
147
- mergeOpencodeFile: (gitRoot: string, instructionsToAdd: string[]) =>
148
- Effect.gen(function* () {
149
- const fs = yield* FileSystemService
150
-
151
- // Detect which file exists
152
- const fileInfo = yield* Effect.gen(function* () {
153
- const detected = yield* Effect.gen(function* () {
154
- // Check .opencode directory first
155
- const dotOpencodeJsoncPath = `${gitRoot}/.opencode/opencode.jsonc`
156
- const dotOpencodeJsoncExists =
157
- yield* fs.exists(dotOpencodeJsoncPath)
158
- if (dotOpencodeJsoncExists) {
159
- return {
160
- extension: "jsonc" as const,
161
- relativePath: ".opencode/opencode.jsonc",
162
- }
163
- }
164
-
165
- const dotOpencodeJsonPath = `${gitRoot}/.opencode/opencode.json`
166
- const dotOpencodeJsonExists =
167
- yield* fs.exists(dotOpencodeJsonPath)
168
- if (dotOpencodeJsonExists) {
169
- return {
170
- extension: "json" as const,
171
- relativePath: ".opencode/opencode.json",
172
- }
173
- }
174
-
175
- // Check root directory
176
- const jsoncPath = `${gitRoot}/opencode.jsonc`
177
- const jsoncExists = yield* fs.exists(jsoncPath)
178
- if (jsoncExists) {
179
- return {
180
- extension: "jsonc" as const,
181
- relativePath: "opencode.jsonc",
182
- }
183
- }
184
-
185
- const jsonPath = `${gitRoot}/opencode.json`
186
- const jsonExists = yield* fs.exists(jsonPath)
187
- if (jsonExists) {
188
- return {
189
- extension: "json" as const,
190
- relativePath: "opencode.json",
191
- }
192
- }
193
-
194
- return null
195
- })
196
-
197
- if (!detected) {
198
- return yield* Effect.fail(
199
- new OpencodeError({
200
- message: "No opencode.json or opencode.jsonc file found",
201
- }),
202
- )
203
- }
204
-
205
- return detected
206
- })
207
-
208
- const filepath = `${gitRoot}/${fileInfo.relativePath}`
209
-
210
- // Read existing config
211
- const content = yield* fs.readFile(filepath)
212
-
213
- // Parse it
214
- const existingConfig = yield* Effect.gen(function* () {
215
- if (fileInfo.extension === "jsonc") {
216
- const errors: ParseError[] = []
217
- const result = parseJsonc(content, errors, {
218
- allowTrailingComma: true,
219
- }) as OpencodeConfig
220
-
221
- // Check if there were any parse errors
222
- if (errors.length > 0) {
223
- throw new Error(
224
- `JSON Parse error: ${errors.map((e) => `Error code ${e.error} at offset ${e.offset}`).join(", ")}`,
225
- )
226
- }
227
-
228
- return result
229
- }
230
- return JSON.parse(content) as OpencodeConfig
231
- }).pipe(
232
- Effect.catchAll((error) =>
233
- Effect.fail(
234
- new OpencodeError({
235
- message: `Failed to parse ${fileInfo.relativePath}`,
236
- cause: error,
237
- }),
238
- ),
239
- ),
240
- )
241
-
242
- // Merge with our instructions
243
- const merged = yield* Effect.sync(() => {
244
- const result = { ...existingConfig }
245
- const existingInstructions = Array.isArray(result.instructions)
246
- ? result.instructions
247
- : []
248
- const newInstructions = instructionsToAdd.filter(
249
- (instruction) => !existingInstructions.includes(instruction),
250
- )
251
- result.instructions = [...existingInstructions, ...newInstructions]
252
- return result
253
- })
254
-
255
- // Format and write back
256
- const formattedContent = JSON.stringify(merged, null, "\t") + "\n"
257
- yield* fs.writeFile(filepath, formattedContent)
258
-
259
- return fileInfo.relativePath
260
- }),
261
- }),
262
- },
263
- ) {}
@@ -1,183 +0,0 @@
1
- import { Effect, Data } from "effect"
2
- import { createInterface } from "node:readline"
3
- import highlight from "../utils/colors"
4
-
5
- // Error types for Prompt operations
6
- class PromptError extends Data.TaggedError("PromptError")<{
7
- message: string
8
- cause?: unknown
9
- }> {}
10
-
11
- // Prompt Service using Effect.Service pattern
12
- export class PromptService extends Effect.Service<PromptService>()(
13
- "PromptService",
14
- {
15
- sync: () => ({
16
- prompt: (question: string, defaultValue?: string) =>
17
- Effect.tryPromise({
18
- try: () =>
19
- new Promise<string>((resolve) => {
20
- const rl = createInterface({
21
- input: process.stdin,
22
- output: process.stdout,
23
- })
24
-
25
- rl.question(question, (answer) => {
26
- rl.close()
27
- resolve(answer.trim())
28
- })
29
-
30
- // If a default value is provided, pre-fill it in the prompt
31
- // This allows the user to backspace and delete it
32
- if (defaultValue) {
33
- rl.write(defaultValue)
34
- }
35
- }),
36
- catch: (error) =>
37
- new PromptError({
38
- message: "Failed to prompt user for input",
39
- cause: error,
40
- }),
41
- }),
42
-
43
- promptForSelection: (message: string, options: readonly string[]) =>
44
- Effect.tryPromise({
45
- try: () =>
46
- new Promise<string>((resolve, reject) => {
47
- console.log(message)
48
- options.forEach((option, index) => {
49
- console.log(` ${index + 1}. ${option}`)
50
- })
51
-
52
- const rl = createInterface({
53
- input: process.stdin,
54
- output: process.stdout,
55
- })
56
-
57
- rl.question(
58
- `\nSelect option (1-${options.length}) or enter custom value: `,
59
- (answer) => {
60
- rl.close()
61
-
62
- // Check if it's a number selection
63
- const selection = parseInt(answer, 10)
64
- if (
65
- !isNaN(selection) &&
66
- selection >= 1 &&
67
- selection <= options.length
68
- ) {
69
- const selected = options[selection - 1]
70
- if (selected === undefined) {
71
- reject(new Error("Invalid selection"))
72
- return
73
- }
74
- resolve(selected)
75
- return
76
- }
77
-
78
- // Otherwise treat as custom value
79
- resolve(answer.trim())
80
- },
81
- )
82
- }),
83
- catch: (error) =>
84
- new PromptError({
85
- message: "Failed to prompt user for selection",
86
- cause: error,
87
- }),
88
- }),
89
-
90
- sanitizeTemplateName: (name: string) =>
91
- Effect.sync(() => {
92
- // Replace problematic characters with hyphens
93
- return name
94
- .replace(/[^a-zA-Z0-9_-]/g, "-")
95
- .replace(/-+/g, "-")
96
- .replace(/^-|-$/g, "")
97
- .toLowerCase()
98
- }),
99
-
100
- /**
101
- * Prompt user to select a template from a list.
102
- * Returns the selected template name or a new name entered by user.
103
- */
104
- promptForTemplate: (
105
- templates: readonly string[],
106
- options?: {
107
- readonly currentTemplate?: string
108
- readonly defaultValue?: string
109
- readonly allowNew?: boolean
110
- },
111
- ) =>
112
- Effect.tryPromise({
113
- try: () =>
114
- new Promise<string>((resolve, reject) => {
115
- const {
116
- currentTemplate,
117
- defaultValue,
118
- allowNew = true,
119
- } = options ?? {}
120
-
121
- // Display template list
122
- if (templates.length > 0) {
123
- console.log("\nAvailable templates:")
124
- templates.forEach((t, i) => {
125
- const current = t === currentTemplate ? " (current)" : ""
126
- console.log(
127
- ` ${highlight.value(i + 1)}. ${highlight.template(t)}${current}`,
128
- )
129
- })
130
- console.log("")
131
- }
132
-
133
- const rl = createInterface({
134
- input: process.stdin,
135
- output: process.stdout,
136
- })
137
-
138
- const promptText =
139
- templates.length > 0
140
- ? allowNew
141
- ? `Template name (1-${templates.length}) or enter new name: `
142
- : `Template name (or number): `
143
- : "Template name: "
144
-
145
- rl.question(promptText, (answer) => {
146
- rl.close()
147
-
148
- const trimmed = answer.trim()
149
- if (!trimmed) {
150
- reject(new Error("Template name is required."))
151
- return
152
- }
153
-
154
- // Check if answer is a number (template selection)
155
- const num = parseInt(trimmed, 10)
156
- if (!isNaN(num) && num >= 1 && num <= templates.length) {
157
- const selected = templates[num - 1]
158
- if (!selected) {
159
- reject(new Error("Invalid selection"))
160
- return
161
- }
162
- resolve(selected)
163
- return
164
- }
165
-
166
- // Otherwise treat as template name
167
- resolve(trimmed)
168
- })
169
-
170
- // Pre-fill default value if provided
171
- if (defaultValue) {
172
- rl.write(defaultValue)
173
- }
174
- }),
175
- catch: (error) =>
176
- new PromptError({
177
- message: "Failed to prompt user for template selection",
178
- cause: error,
179
- }),
180
- }),
181
- }),
182
- },
183
- ) {}
@@ -1,81 +0,0 @@
1
- import { Effect, Data } from "effect"
2
- import { join } from "node:path"
3
- import {
4
- getAgencyConfigDir,
5
- getTemplateDir,
6
- getTemplatesDir,
7
- } from "../utils/paths"
8
- import { FileSystemService } from "./FileSystemService"
9
-
10
- // Error types for Template operations
11
- class TemplateError extends Data.TaggedError("TemplateError")<{
12
- message: string
13
- cause?: unknown
14
- }> {}
15
-
16
- // Template Service using Effect.Service pattern
17
- export class TemplateService extends Effect.Service<TemplateService>()(
18
- "TemplateService",
19
- {
20
- sync: () => ({
21
- getConfigDir: () => Effect.sync(() => getAgencyConfigDir()),
22
-
23
- getTemplateDir: (templateName: string) =>
24
- Effect.sync(() => getTemplateDir(templateName)),
25
-
26
- templateExists: (templateName: string) =>
27
- Effect.tryPromise({
28
- try: async () => {
29
- const templateDir = getTemplateDir(templateName)
30
- const file = Bun.file(join(templateDir, "AGENTS.md"))
31
- return await file.exists()
32
- },
33
- catch: () =>
34
- new TemplateError({
35
- message: `Failed to check if template exists: ${templateName}`,
36
- }),
37
- }),
38
-
39
- createTemplateDir: (templateName: string) =>
40
- Effect.gen(function* () {
41
- const fs = yield* FileSystemService
42
- const templateDir = getTemplateDir(templateName)
43
- const exists = yield* fs.exists(templateDir)
44
- if (!exists) {
45
- yield* fs.createDirectory(templateDir)
46
- }
47
- }),
48
-
49
- listTemplates: () =>
50
- Effect.tryPromise({
51
- try: async () => {
52
- const templatesDir = getTemplatesDir()
53
-
54
- // Check if templates directory exists first
55
- const templatesFile = Bun.file(templatesDir)
56
- const exists = await templatesFile.exists()
57
-
58
- if (!exists) {
59
- // Return empty array if templates directory doesn't exist
60
- return [] as readonly string[]
61
- }
62
-
63
- const entries = await Array.fromAsync(
64
- new Bun.Glob("*/AGENTS.md").scan({ cwd: templatesDir }),
65
- )
66
-
67
- // Extract template names from paths like "work/AGENTS.md"
68
- const templates = entries
69
- .map((entry) => entry.split("/")[0] || "")
70
- .filter(Boolean)
71
-
72
- return templates as readonly string[]
73
- },
74
- catch: () =>
75
- new TemplateError({
76
- message: "Failed to list templates",
77
- }),
78
- }),
79
- }),
80
- },
81
- ) {}
@@ -1,8 +0,0 @@
1
- declare module "@triggi/native-exec" {
2
- function exec(
3
- command: string,
4
- env?: NodeJS.ProcessEnv,
5
- ...args: string[]
6
- ): void
7
- export default exec
8
- }