aider-ruby 0.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.
@@ -0,0 +1,571 @@
1
+ require 'open3'
2
+ require 'json'
3
+
4
+ module AiderRuby
5
+ module Client
6
+ # Module for model configuration methods
7
+ module ModelConfiguration
8
+ def model(model_name)
9
+ @config.model = model_name
10
+ self
11
+ end
12
+
13
+ def openai_api_key(key)
14
+ @config.openai_api_key = key
15
+ self
16
+ end
17
+
18
+ def anthropic_api_key(key)
19
+ @config.anthropic_api_key = key
20
+ self
21
+ end
22
+
23
+ def reasoning_effort(effort)
24
+ @config.reasoning_effort = effort
25
+ self
26
+ end
27
+
28
+ def thinking_tokens(tokens)
29
+ @config.thinking_tokens = tokens
30
+ self
31
+ end
32
+
33
+ def use_temperature(enabled = true)
34
+ @config.use_temperature = enabled
35
+ self
36
+ end
37
+
38
+ def use_system_prompt(enabled = true)
39
+ @config.use_system_prompt = enabled
40
+ self
41
+ end
42
+
43
+ def use_repo_map(enabled = true)
44
+ @config.use_repo_map = enabled
45
+ self
46
+ end
47
+
48
+ def extra_params(params)
49
+ @config.extra_params = params
50
+ self
51
+ end
52
+
53
+ def model_settings_file(file_path)
54
+ @config.model_settings_file = file_path
55
+ self
56
+ end
57
+
58
+ def model_metadata_file(file_path)
59
+ @config.model_metadata_file = file_path
60
+ self
61
+ end
62
+
63
+ def add_alias(alias_name, model_name)
64
+ @config.alias_settings ||= []
65
+ @config.alias_settings << { alias: alias_name, model: model_name }
66
+ self
67
+ end
68
+
69
+ def reasoning_tag(tag)
70
+ @config.reasoning_tag = tag
71
+ self
72
+ end
73
+
74
+ def weak_model_name(model_name)
75
+ @config.weak_model_name = model_name
76
+ self
77
+ end
78
+
79
+ def editor_model_name(model_name)
80
+ @config.editor_model_name = model_name
81
+ self
82
+ end
83
+ end
84
+
85
+ # Module for output configuration methods
86
+ module OutputConfiguration
87
+ def dark_mode(enabled = true)
88
+ @config.dark_mode = enabled
89
+ self
90
+ end
91
+
92
+ def light_mode(enabled = true)
93
+ @config.light_mode = enabled
94
+ self
95
+ end
96
+
97
+ def pretty(enabled = true)
98
+ @config.pretty = enabled
99
+ self
100
+ end
101
+
102
+ def stream(enabled = true)
103
+ @config.stream = enabled
104
+ self
105
+ end
106
+ end
107
+
108
+ # Module for Git configuration methods
109
+ module GitConfiguration
110
+ def git(enabled = true)
111
+ @config.git = enabled
112
+ self
113
+ end
114
+
115
+ def auto_commits(enabled = true)
116
+ @config.auto_commits = enabled
117
+ self
118
+ end
119
+ end
120
+
121
+ # Module for linting and testing configuration methods
122
+ module LintTestConfiguration
123
+ def lint(enabled = true)
124
+ @config.lint = enabled
125
+ self
126
+ end
127
+
128
+ def auto_lint(enabled = true)
129
+ @config.auto_lint = enabled
130
+ self
131
+ end
132
+
133
+ def test(enabled = true)
134
+ @config.test = enabled
135
+ self
136
+ end
137
+
138
+ def auto_test(enabled = true)
139
+ @config.auto_test = enabled
140
+ self
141
+ end
142
+ end
143
+
144
+ # Module for general configuration methods
145
+ module GeneralConfiguration
146
+ def disable_playwright(enabled = true)
147
+ @config.disable_playwright = enabled
148
+ self
149
+ end
150
+
151
+ def vim(enabled = true)
152
+ @config.vim = enabled
153
+ self
154
+ end
155
+
156
+ def chat_language(language)
157
+ @config.chat_language = language
158
+ self
159
+ end
160
+
161
+ def commit_language(language)
162
+ @config.commit_language = language
163
+ self
164
+ end
165
+
166
+ def yes_always(enabled = true)
167
+ @config.yes_always = enabled
168
+ self
169
+ end
170
+
171
+ def verbose(enabled = true)
172
+ @config.verbose = enabled
173
+ self
174
+ end
175
+
176
+ def encoding(encoding)
177
+ @config.encoding = encoding
178
+ self
179
+ end
180
+
181
+ def line_endings(endings)
182
+ @config.line_endings = endings
183
+ self
184
+ end
185
+
186
+ def suggest_shell_commands(enabled = true)
187
+ @config.suggest_shell_commands = enabled
188
+ self
189
+ end
190
+
191
+ def fancy_input(enabled = true)
192
+ @config.fancy_input = enabled
193
+ self
194
+ end
195
+
196
+ def multiline(enabled = true)
197
+ @config.multiline = enabled
198
+ self
199
+ end
200
+
201
+ def notifications(enabled = true)
202
+ @config.notifications = enabled
203
+ self
204
+ end
205
+
206
+ def notifications_command(command)
207
+ @config.notifications_command = command
208
+ self
209
+ end
210
+
211
+ def detect_urls(enabled = true)
212
+ @config.detect_urls = enabled
213
+ self
214
+ end
215
+
216
+ def editor(editor)
217
+ @config.editor = editor
218
+ self
219
+ end
220
+
221
+ def shell_completions(enabled = true)
222
+ @config.shell_completions = enabled
223
+ self
224
+ end
225
+ end
226
+
227
+ # Module for convention and edit format methods
228
+ module ConventionConfiguration
229
+ # Add multiple conventions files
230
+ def conventions_files(file_paths, validate: true)
231
+ file_paths = Array(file_paths)
232
+
233
+ if validate
234
+ file_paths.each do |path|
235
+ raise AiderRuby::ErrorHandling::FileError, "Conventions file not found: #{path}" unless File.exist?(path)
236
+ end
237
+ end
238
+
239
+ # Store multiple conventions files
240
+ @config.conventions_files ||= []
241
+ @config.conventions_files.concat(file_paths)
242
+ self
243
+ end
244
+
245
+ # Enhanced read files method with validation and filtering
246
+ def add_read_files(files, validate: true, extensions: nil, exclude_patterns: [])
247
+ files = Array(files)
248
+
249
+ # Filter files by extensions if specified
250
+ if extensions
251
+ files = files.select do |file|
252
+ file_ext = File.extname(file)
253
+ extensions.include?(file_ext)
254
+ end
255
+ end
256
+
257
+ # Exclude files matching patterns
258
+ files = files.reject do |file|
259
+ exclude_patterns.any? do |pattern|
260
+ case pattern
261
+ when String
262
+ file.include?(pattern)
263
+ when Regexp
264
+ file.match?(pattern)
265
+ end
266
+ end
267
+ end
268
+
269
+ # Validate files exist if requested
270
+ if validate
271
+ files.each do |file|
272
+ raise AiderRuby::ErrorHandling::FileError, "Read file not found: #{file}" unless File.exist?(file)
273
+ end
274
+ end
275
+
276
+ @config.read_files ||= []
277
+ @config.read_files.concat(files)
278
+ self
279
+ end
280
+
281
+ # Add read files from folder with filtering
282
+ def add_read_files_from_folder(folder_path, extensions: nil, exclude_patterns: [], validate: true)
283
+ require 'find'
284
+
285
+ files_in_folder = []
286
+
287
+ Find.find(folder_path) do |path|
288
+ next if File.directory?(path)
289
+
290
+ # Skip if file doesn't match extension filter
291
+ if extensions
292
+ file_ext = File.extname(path)
293
+ next unless extensions.include?(file_ext)
294
+ end
295
+
296
+ # Skip if file matches exclude patterns
297
+ skip_file = exclude_patterns.any? do |pattern|
298
+ case pattern
299
+ when String
300
+ path.include?(pattern)
301
+ when Regexp
302
+ path.match?(pattern)
303
+ end
304
+ end
305
+ next if skip_file
306
+
307
+ files_in_folder << path
308
+ end
309
+
310
+ add_read_files(files_in_folder, validate: validate)
311
+ end
312
+
313
+ # Clear all read files
314
+ def clear_read_files
315
+ @config.read_files = []
316
+ self
317
+ end
318
+
319
+ # Get list of read files
320
+ def read_files_list
321
+ @config.read_files || []
322
+ end
323
+
324
+ def edit_format_whole(enabled = true)
325
+ @config.edit_format_whole = enabled
326
+ self
327
+ end
328
+
329
+ def edit_format_diff(enabled = true)
330
+ @config.edit_format_diff = enabled
331
+ self
332
+ end
333
+
334
+ def edit_format_diff_fenced(enabled = true)
335
+ @config.edit_format_diff_fenced = enabled
336
+ self
337
+ end
338
+
339
+ def editor_edit_format_whole(enabled = true)
340
+ @config.editor_edit_format_whole = enabled
341
+ self
342
+ end
343
+
344
+ def editor_edit_format_diff(enabled = true)
345
+ @config.editor_edit_format_diff = enabled
346
+ self
347
+ end
348
+
349
+ def editor_edit_format_diff_fenced(enabled = true)
350
+ @config.editor_edit_format_diff_fenced = enabled
351
+ self
352
+ end
353
+ end
354
+
355
+ # Module for execution methods
356
+ module ExecutionMethods
357
+ def execute(message, options = {})
358
+ args = build_command_args(options)
359
+ args << '--message' << message
360
+
361
+ execute_command(args)
362
+ end
363
+
364
+ def interactive(options = {})
365
+ args = build_command_args(options)
366
+
367
+ execute_command(args, interactive: true)
368
+ end
369
+
370
+ def execute_from_file(message_file, options = {})
371
+ args = build_command_args(options)
372
+ args << '--message-file' << message_file
373
+
374
+ execute_command(args)
375
+ end
376
+
377
+ def apply_changes(file_path, options = {})
378
+ args = build_command_args(options)
379
+ args << '--apply' << file_path
380
+
381
+ execute_command(args)
382
+ end
383
+
384
+ def show_repo_map(options = {})
385
+ args = build_command_args(options)
386
+ args << '--show-repo-map'
387
+
388
+ execute_command(args)
389
+ end
390
+
391
+ def show_prompts(options = {})
392
+ args = build_command_args(options)
393
+ args << '--show-prompts'
394
+
395
+ execute_command(args)
396
+ end
397
+
398
+ def list_models(provider = nil)
399
+ args = ['aider']
400
+ args << '--list-models' << provider if provider
401
+
402
+ execute_command(args)
403
+ end
404
+
405
+ def check_update
406
+ args = ['aider', '--check-update']
407
+ execute_command(args)
408
+ end
409
+
410
+ def upgrade
411
+ args = ['aider', '--upgrade']
412
+ execute_command(args)
413
+ end
414
+ end
415
+
416
+ # Main client class
417
+ class Client
418
+ include ModelConfiguration
419
+ include OutputConfiguration
420
+ include GitConfiguration
421
+ include LintTestConfiguration
422
+ include GeneralConfiguration
423
+ include ConventionConfiguration
424
+ include ExecutionMethods
425
+
426
+ attr_reader :config, :files, :read_only_files
427
+
428
+ def initialize(options = {}, &block)
429
+ @config = Config::Configuration.new(options)
430
+ @files = []
431
+ @read_only_files = []
432
+
433
+ # Apply block configuration if provided
434
+ if block_given?
435
+ block.call(@config)
436
+ end
437
+ end
438
+
439
+ # Add files to edit (can take single file or array of files)
440
+ def add_files(file_path)
441
+ files_to_add = Array(file_path)
442
+ @files.concat(files_to_add)
443
+ self
444
+ end
445
+
446
+ # Add read-only files (can take single file or array of files)
447
+ def add_read_only_file(file_path)
448
+ files_to_add = Array(file_path)
449
+ @read_only_files.concat(files_to_add)
450
+ self
451
+ end
452
+
453
+ # Add entire folder (recursively finds all files)
454
+ def add_folder(folder_path, extensions: nil, exclude_patterns: [])
455
+ require 'find'
456
+
457
+ files_in_folder = []
458
+
459
+ Find.find(folder_path) do |path|
460
+ next if File.directory?(path)
461
+
462
+ # Skip if file doesn't match extension filter
463
+ if extensions
464
+ file_ext = File.extname(path)
465
+ next unless extensions.include?(file_ext)
466
+ end
467
+
468
+ # Skip if file matches exclude patterns
469
+ skip_file = exclude_patterns.any? do |pattern|
470
+ case pattern
471
+ when String
472
+ path.include?(pattern)
473
+ when Regexp
474
+ path.match?(pattern)
475
+ end
476
+ end
477
+ next if skip_file
478
+
479
+ files_in_folder << path
480
+ end
481
+
482
+ @files.concat(files_in_folder)
483
+ self
484
+ end
485
+
486
+ # Add folder as read-only context
487
+ def add_read_only_folder(folder_path, extensions: nil, exclude_patterns: [])
488
+ require 'find'
489
+
490
+ files_in_folder = []
491
+
492
+ Find.find(folder_path) do |path|
493
+ next if File.directory?(path)
494
+
495
+ # Skip if file doesn't match extension filter
496
+ if extensions
497
+ file_ext = File.extname(path)
498
+ next unless extensions.include?(file_ext)
499
+ end
500
+
501
+ # Skip if file matches exclude patterns
502
+ skip_file = exclude_patterns.any? do |pattern|
503
+ case pattern
504
+ when String
505
+ path.include?(pattern)
506
+ when Regexp
507
+ path.match?(pattern)
508
+ end
509
+ end
510
+ next if skip_file
511
+
512
+ files_in_folder << path
513
+ end
514
+
515
+ @read_only_files.concat(files_in_folder)
516
+ self
517
+ end
518
+
519
+ private
520
+
521
+ def build_command_args(options = {})
522
+ args = ['aider']
523
+
524
+ # Add config arguments
525
+ args.concat(@config.to_aider_args)
526
+
527
+ # Add files
528
+ @files.each { |file| args << '--file' << file }
529
+ @read_only_files.each { |file| args << '--read' << file }
530
+
531
+ # Add additional options
532
+ add_cli_options(args, options)
533
+
534
+ args
535
+ end
536
+
537
+ def add_cli_options(args, options)
538
+ options.each do |key, value|
539
+ case key
540
+ when :config_file
541
+ args << '--config' << value
542
+ when :env_file
543
+ args << '--env-file' << value
544
+ when :dry_run
545
+ args << '--dry-run' if value
546
+ when :verbose
547
+ args << '--verbose' if value
548
+ when :yes_always
549
+ args << '--yes-always' if value
550
+ end
551
+ end
552
+ end
553
+
554
+ def execute_command(args, interactive: false)
555
+ puts "Executing: #{args.join(' ')}" if @config.verbose
556
+
557
+ if interactive
558
+ # For interactive mode, we need to spawn a process that keeps running
559
+ spawn(*args)
560
+ else
561
+ # For non-interactive mode, capture output
562
+ stdout, stderr, status = Open3.capture3(*args)
563
+
564
+ raise Error, "Aider command failed: #{stderr}" unless status.success?
565
+
566
+ stdout
567
+ end
568
+ end
569
+ end
570
+ end
571
+ end