@aaronshaf/ger 2.0.10 → 3.0.1

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.
@@ -1,292 +0,0 @@
1
- import { Context, Data, Effect, Layer } from 'effect'
2
- import { exec } from 'node:child_process'
3
- import { promisify } from 'node:util'
4
-
5
- const execAsync = promisify(exec)
6
-
7
- // Shared response extraction logic for all AI tools
8
- const extractResponse = (stdout: string): string => {
9
- // Extract response from <response> tags or use full output
10
- const responseMatch = stdout.match(/<response>([\s\S]*?)<\/response>/i)
11
- return responseMatch ? responseMatch[1].trim() : stdout.trim()
12
- }
13
-
14
- // Simple strategy focused only on review needs
15
- export interface ReviewStrategyErrorFields {
16
- readonly message: string
17
- readonly cause?: unknown
18
- }
19
-
20
- const ReviewStrategyErrorBase = Data.TaggedError(
21
- 'ReviewStrategyError',
22
- )<ReviewStrategyErrorFields> as unknown
23
-
24
- export class ReviewStrategyError
25
- extends (ReviewStrategyErrorBase as new (
26
- args: ReviewStrategyErrorFields,
27
- ) => ReviewStrategyErrorFields & Error & { readonly _tag: 'ReviewStrategyError' })
28
- implements Error
29
- {
30
- readonly name = 'ReviewStrategyError'
31
- }
32
-
33
- // Review strategy interface - focused on specific review patterns
34
- export interface ReviewStrategy {
35
- readonly name: string
36
- readonly isAvailable: () => Effect.Effect<boolean, never>
37
- readonly executeReview: (
38
- prompt: string,
39
- options?: { cwd?: string; systemPrompt?: string },
40
- ) => Effect.Effect<string, ReviewStrategyError>
41
- }
42
-
43
- // Strategy implementations for different AI tools
44
- export const claudeCliStrategy: ReviewStrategy = {
45
- name: 'Claude CLI',
46
- isAvailable: () =>
47
- Effect.gen(function* () {
48
- const result = yield* Effect.tryPromise({
49
- try: () => execAsync('which claude'),
50
- catch: () => null,
51
- }).pipe(Effect.orElseSucceed(() => null))
52
-
53
- return Boolean(result && result.stdout.trim())
54
- }),
55
- executeReview: (prompt, options = {}) =>
56
- Effect.gen(function* () {
57
- const result = yield* Effect.tryPromise({
58
- try: async () => {
59
- const child = require('node:child_process').spawn('claude -p', {
60
- shell: true,
61
- stdio: ['pipe', 'pipe', 'pipe'],
62
- cwd: options.cwd || process.cwd(),
63
- })
64
-
65
- child.stdin.write(prompt)
66
- child.stdin.end()
67
-
68
- let stdout = ''
69
- let stderr = ''
70
-
71
- child.stdout.on('data', (data: Buffer) => {
72
- stdout += data.toString()
73
- })
74
-
75
- child.stderr.on('data', (data: Buffer) => {
76
- stderr += data.toString()
77
- })
78
-
79
- return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
80
- child.on('close', (code: number) => {
81
- if (code !== 0) {
82
- reject(new Error(`Claude CLI exited with code ${code}: ${stderr}`))
83
- } else {
84
- resolve({ stdout, stderr })
85
- }
86
- })
87
-
88
- child.on('error', reject)
89
- })
90
- },
91
- catch: (error) =>
92
- new ReviewStrategyError({
93
- message: `Claude CLI failed: ${error instanceof Error ? error.message : String(error)}`,
94
- cause: error,
95
- }),
96
- })
97
-
98
- return extractResponse(result.stdout)
99
- }),
100
- }
101
-
102
- export const geminiCliStrategy: ReviewStrategy = {
103
- name: 'Gemini CLI',
104
- isAvailable: () =>
105
- Effect.gen(function* () {
106
- const result = yield* Effect.tryPromise({
107
- try: () => execAsync('which gemini'),
108
- catch: () => null,
109
- }).pipe(Effect.orElseSucceed(() => null))
110
-
111
- return Boolean(result && result.stdout.trim())
112
- }),
113
- executeReview: (prompt, options = {}) =>
114
- Effect.gen(function* () {
115
- const result = yield* Effect.tryPromise({
116
- try: async () => {
117
- const child = require('node:child_process').spawn('gemini -p', {
118
- shell: true,
119
- stdio: ['pipe', 'pipe', 'pipe'],
120
- cwd: options.cwd || process.cwd(),
121
- })
122
-
123
- child.stdin.write(prompt)
124
- child.stdin.end()
125
-
126
- let stdout = ''
127
- let stderr = ''
128
-
129
- child.stdout.on('data', (data: Buffer) => {
130
- stdout += data.toString()
131
- })
132
-
133
- child.stderr.on('data', (data: Buffer) => {
134
- stderr += data.toString()
135
- })
136
-
137
- return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
138
- child.on('close', (code: number) => {
139
- if (code !== 0) {
140
- reject(new Error(`Gemini CLI exited with code ${code}: ${stderr}`))
141
- } else {
142
- resolve({ stdout, stderr })
143
- }
144
- })
145
-
146
- child.on('error', reject)
147
- })
148
- },
149
- catch: (error) =>
150
- new ReviewStrategyError({
151
- message: `Gemini CLI failed: ${error instanceof Error ? error.message : String(error)}`,
152
- cause: error,
153
- }),
154
- })
155
-
156
- return extractResponse(result.stdout)
157
- }),
158
- }
159
-
160
- export const openCodeCliStrategy: ReviewStrategy = {
161
- name: 'OpenCode CLI',
162
- isAvailable: () =>
163
- Effect.gen(function* () {
164
- const result = yield* Effect.tryPromise({
165
- try: () => execAsync('which opencode'),
166
- catch: () => null,
167
- }).pipe(Effect.orElseSucceed(() => null))
168
-
169
- return Boolean(result && result.stdout.trim())
170
- }),
171
- executeReview: (prompt, options = {}) =>
172
- Effect.gen(function* () {
173
- const result = yield* Effect.tryPromise({
174
- try: async () => {
175
- const child = require('node:child_process').spawn('opencode -p', {
176
- shell: true,
177
- stdio: ['pipe', 'pipe', 'pipe'],
178
- cwd: options.cwd || process.cwd(),
179
- })
180
-
181
- child.stdin.write(prompt)
182
- child.stdin.end()
183
-
184
- let stdout = ''
185
- let stderr = ''
186
-
187
- child.stdout.on('data', (data: Buffer) => {
188
- stdout += data.toString()
189
- })
190
-
191
- child.stderr.on('data', (data: Buffer) => {
192
- stderr += data.toString()
193
- })
194
-
195
- return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
196
- child.on('close', (code: number) => {
197
- if (code !== 0) {
198
- reject(new Error(`OpenCode CLI exited with code ${code}: ${stderr}`))
199
- } else {
200
- resolve({ stdout, stderr })
201
- }
202
- })
203
-
204
- child.on('error', reject)
205
- })
206
- },
207
- catch: (error) =>
208
- new ReviewStrategyError({
209
- message: `OpenCode CLI failed: ${error instanceof Error ? error.message : String(error)}`,
210
- cause: error,
211
- }),
212
- })
213
-
214
- return extractResponse(result.stdout)
215
- }),
216
- }
217
-
218
- // Review service interface using strategy pattern
219
- export interface ReviewStrategyServiceImpl {
220
- readonly getAvailableStrategies: () => Effect.Effect<ReviewStrategy[], never>
221
- readonly selectStrategy: (
222
- preferredName?: string,
223
- ) => Effect.Effect<ReviewStrategy, ReviewStrategyError>
224
- readonly executeWithStrategy: (
225
- strategy: ReviewStrategy,
226
- prompt: string,
227
- options?: { cwd?: string; systemPrompt?: string },
228
- ) => Effect.Effect<string, ReviewStrategyError>
229
- }
230
-
231
- // Export the service tag with explicit type
232
- export const ReviewStrategyService: Context.Tag<
233
- ReviewStrategyServiceImpl,
234
- ReviewStrategyServiceImpl
235
- > = Context.GenericTag<ReviewStrategyServiceImpl>('ReviewStrategyService')
236
-
237
- export type ReviewStrategyService = Context.Tag.Identifier<typeof ReviewStrategyService>
238
-
239
- export const ReviewStrategyServiceLive: Layer.Layer<ReviewStrategyServiceImpl> = Layer.succeed(
240
- ReviewStrategyService,
241
- {
242
- getAvailableStrategies: () =>
243
- Effect.gen(function* () {
244
- const strategies = [claudeCliStrategy, geminiCliStrategy, openCodeCliStrategy]
245
- const available: ReviewStrategy[] = []
246
-
247
- for (const strategy of strategies) {
248
- const isAvailable = yield* strategy.isAvailable()
249
- if (isAvailable) {
250
- available.push(strategy)
251
- }
252
- }
253
-
254
- return available
255
- }),
256
-
257
- selectStrategy: (preferredName?: string) =>
258
- Effect.gen(function* () {
259
- const strategies = [claudeCliStrategy, geminiCliStrategy, openCodeCliStrategy]
260
- const available: ReviewStrategy[] = []
261
-
262
- for (const strategy of strategies) {
263
- const isAvailable = yield* strategy.isAvailable()
264
- if (isAvailable) {
265
- available.push(strategy)
266
- }
267
- }
268
-
269
- if (available.length === 0) {
270
- return yield* Effect.fail(
271
- new ReviewStrategyError({
272
- message: 'No AI tools available. Please install claude, gemini, or opencode CLI.',
273
- }),
274
- )
275
- }
276
-
277
- if (preferredName) {
278
- const preferred = available.find((s: ReviewStrategy) =>
279
- s.name.toLowerCase().includes(preferredName.toLowerCase()),
280
- )
281
- if (preferred) {
282
- return preferred
283
- }
284
- }
285
-
286
- return available[0] // Return first available
287
- }),
288
-
289
- executeWithStrategy: (strategy, prompt, options = {}) =>
290
- strategy.executeReview(prompt, options),
291
- },
292
- )