@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,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
- }