agentcode 0.9.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +59 -0
  3. data/lib/agentcode/blueprint/blueprint_parser.rb +198 -0
  4. data/lib/agentcode/blueprint/blueprint_validator.rb +209 -0
  5. data/lib/agentcode/blueprint/generators/factory_generator.rb +74 -0
  6. data/lib/agentcode/blueprint/generators/policy_generator.rb +154 -0
  7. data/lib/agentcode/blueprint/generators/seeder_generator.rb +160 -0
  8. data/lib/agentcode/blueprint/generators/test_generator.rb +291 -0
  9. data/lib/agentcode/blueprint/manifest_manager.rb +81 -0
  10. data/lib/agentcode/commands/base_command.rb +57 -0
  11. data/lib/agentcode/commands/blueprint_command.rb +549 -0
  12. data/lib/agentcode/commands/export_postman_command.rb +328 -0
  13. data/lib/agentcode/commands/generate_command.rb +563 -0
  14. data/lib/agentcode/commands/install_command.rb +441 -0
  15. data/lib/agentcode/commands/invitation_link_command.rb +107 -0
  16. data/lib/agentcode/concerns/belongs_to_organization.rb +49 -0
  17. data/lib/agentcode/concerns/has_agentcode.rb +93 -0
  18. data/lib/agentcode/concerns/has_audit_trail.rb +125 -0
  19. data/lib/agentcode/concerns/has_auto_scope.rb +91 -0
  20. data/lib/agentcode/concerns/has_permissions.rb +117 -0
  21. data/lib/agentcode/concerns/has_uuid.rb +26 -0
  22. data/lib/agentcode/concerns/has_validation.rb +250 -0
  23. data/lib/agentcode/concerns/hidable_columns.rb +180 -0
  24. data/lib/agentcode/configuration.rb +98 -0
  25. data/lib/agentcode/controllers/auth_controller.rb +242 -0
  26. data/lib/agentcode/controllers/invitations_controller.rb +231 -0
  27. data/lib/agentcode/controllers/resources_controller.rb +813 -0
  28. data/lib/agentcode/engine.rb +65 -0
  29. data/lib/agentcode/mailers/invitation_mailer.rb +22 -0
  30. data/lib/agentcode/middleware/resolve_organization_from_route.rb +72 -0
  31. data/lib/agentcode/models/agentcode_model.rb +387 -0
  32. data/lib/agentcode/models/audit_log.rb +17 -0
  33. data/lib/agentcode/models/organization_invitation.rb +57 -0
  34. data/lib/agentcode/policies/invitation_policy.rb +54 -0
  35. data/lib/agentcode/policies/resource_policy.rb +197 -0
  36. data/lib/agentcode/query_builder.rb +278 -0
  37. data/lib/agentcode/railtie.rb +11 -0
  38. data/lib/agentcode/resource_scope.rb +59 -0
  39. data/lib/agentcode/routes.rb +124 -0
  40. data/lib/agentcode/tasks/agentcode.rake +39 -0
  41. data/lib/agentcode/templates/agentcode.rb +71 -0
  42. data/lib/agentcode/templates/agentcode_model.rb +104 -0
  43. data/lib/agentcode/templates/audit_trail/create_audit_logs.rb.erb +26 -0
  44. data/lib/agentcode/templates/generate/factory.rb.erb +43 -0
  45. data/lib/agentcode/templates/generate/migration.rb.erb +26 -0
  46. data/lib/agentcode/templates/generate/model.rb.erb +55 -0
  47. data/lib/agentcode/templates/generate/policy.rb.erb +52 -0
  48. data/lib/agentcode/templates/generate/scope.rb.erb +31 -0
  49. data/lib/agentcode/templates/multi_tenant/factories/organizations.rb.erb +9 -0
  50. data/lib/agentcode/templates/multi_tenant/factories/roles.rb.erb +9 -0
  51. data/lib/agentcode/templates/multi_tenant/factories/user_roles.rb.erb +10 -0
  52. data/lib/agentcode/templates/multi_tenant/factories/users.rb.erb +9 -0
  53. data/lib/agentcode/templates/multi_tenant/migrations/create_organizations.rb.erb +15 -0
  54. data/lib/agentcode/templates/multi_tenant/migrations/create_roles.rb.erb +15 -0
  55. data/lib/agentcode/templates/multi_tenant/migrations/create_user_roles.rb.erb +16 -0
  56. data/lib/agentcode/templates/multi_tenant/migrations/create_users.rb.erb +15 -0
  57. data/lib/agentcode/templates/multi_tenant/models/organization.rb.erb +18 -0
  58. data/lib/agentcode/templates/multi_tenant/models/role.rb.erb +11 -0
  59. data/lib/agentcode/templates/multi_tenant/models/user.rb.erb +14 -0
  60. data/lib/agentcode/templates/multi_tenant/models/user_role.rb.erb +9 -0
  61. data/lib/agentcode/templates/multi_tenant/policies/organization_policy.rb.erb +6 -0
  62. data/lib/agentcode/templates/multi_tenant/policies/role_policy.rb.erb +6 -0
  63. data/lib/agentcode/templates/multi_tenant/seeders/organization_seeder.rb.erb +9 -0
  64. data/lib/agentcode/templates/multi_tenant/seeders/role_seeder.rb.erb +19 -0
  65. data/lib/agentcode/templates/routes.rb +13 -0
  66. data/lib/agentcode/version.rb +5 -0
  67. data/lib/agentcode/views/lumina/invitation_mailer/invite.html.erb +29 -0
  68. data/lib/agentcode-rails.rb +3 -0
  69. data/lib/agentcode.rb +26 -0
  70. metadata +281 -0
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-prompt"
4
+
5
+ module AgentCode
6
+ module Commands
7
+ # Lightweight base for AgentCode CLI commands.
8
+ # Uses tty-prompt for interactive, navigable terminal UI.
9
+ class BaseCommand
10
+ def initialize
11
+ @prompt = TTY::Prompt.new(
12
+ active_color: :cyan,
13
+ help_color: :dim
14
+ )
15
+ end
16
+
17
+ private
18
+
19
+ def say(message = "", color = nil)
20
+ if color
21
+ message = @prompt.decorate(message, color)
22
+ end
23
+ puts message
24
+ end
25
+
26
+ def ask(message, **options)
27
+ @prompt.ask(message, **options)
28
+ end
29
+
30
+ def yes?(message)
31
+ @prompt.yes?(message)
32
+ end
33
+
34
+ def select(label, choices = nil, **options, &block)
35
+ if block
36
+ @prompt.select(label, **options, &block)
37
+ else
38
+ @prompt.select(label, choices, **options)
39
+ end
40
+ end
41
+
42
+ def multi_select(label, choices = nil, **options, &block)
43
+ if block
44
+ @prompt.multi_select(label, **options, &block)
45
+ else
46
+ @prompt.multi_select(label, choices, **options)
47
+ end
48
+ end
49
+
50
+ def task(description)
51
+ print " → #{@prompt.decorate(description + '...', :cyan)}"
52
+ yield
53
+ puts "\r ✓ #{@prompt.decorate(description, :green)} "
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,549 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "agentcode/commands/base_command"
4
+ require "fileutils"
5
+ require "agentcode/blueprint/blueprint_parser"
6
+ require "agentcode/blueprint/blueprint_validator"
7
+ require "agentcode/blueprint/manifest_manager"
8
+ require "agentcode/blueprint/generators/policy_generator"
9
+ require "agentcode/blueprint/generators/test_generator"
10
+ require "agentcode/blueprint/generators/seeder_generator"
11
+ require "agentcode/blueprint/generators/factory_generator"
12
+
13
+ module AgentCode
14
+ module Commands
15
+ # Zero-token deterministic code generation from YAML blueprint specs.
16
+ # Port of agentcode-server BlueprintCommand.php / agentcode-adonis-server blueprint.ts.
17
+ #
18
+ # Usage: rails agentcode:blueprint [OPTIONS]
19
+ class BlueprintCommand < BaseCommand
20
+ attr_accessor :options
21
+
22
+ def initialize
23
+ super
24
+ @options = {
25
+ dir: ".agentcode/blueprints",
26
+ model: nil,
27
+ force: false,
28
+ dry_run: false,
29
+ skip_tests: false,
30
+ skip_seeders: false
31
+ }
32
+ @migration_timestamp_offset = 0
33
+ end
34
+
35
+ def perform
36
+ print_banner
37
+
38
+ blueprints_dir = Rails.root.join(options[:dir]).to_s
39
+
40
+ unless Dir.exist?(blueprints_dir)
41
+ say "Blueprint directory not found: #{blueprints_dir}", :red
42
+ say "Run 'rails agentcode:install' first, or create the directory manually.", :yellow
43
+ return
44
+ end
45
+
46
+ parser = AgentCode::Blueprint::BlueprintParser.new
47
+ validator = AgentCode::Blueprint::BlueprintValidator.new
48
+ manifest = AgentCode::Blueprint::ManifestManager.new(blueprints_dir)
49
+
50
+ # 1. Parse roles
51
+ roles_file = File.join(blueprints_dir, "_roles.yaml")
52
+ roles = {}
53
+
54
+ if File.exist?(roles_file)
55
+ begin
56
+ roles = parser.parse_roles(roles_file)
57
+ role_result = validator.validate_roles(roles)
58
+ unless role_result[:valid]
59
+ say "Role validation errors:", :red
60
+ role_result[:errors].each { |e| say " • #{e}", :red }
61
+ return
62
+ end
63
+ say " ✓ Parsed #{roles.length} roles", :green
64
+ rescue => e
65
+ say " ✗ #{e.message}", :red
66
+ return
67
+ end
68
+ else
69
+ say " ⚠ No _roles.yaml found — role cross-reference disabled", :yellow
70
+ end
71
+
72
+ # 2. Discover YAML files
73
+ yaml_files = Dir.glob(File.join(blueprints_dir, "*.yaml"))
74
+ .reject { |f| File.basename(f).start_with?("_", ".") }
75
+ .sort
76
+
77
+ if options[:model]
78
+ yaml_files = yaml_files.select { |f| File.basename(f, ".yaml") == options[:model] }
79
+ end
80
+
81
+ if yaml_files.empty?
82
+ say "No blueprint YAML files found in #{blueprints_dir}", :yellow
83
+ return
84
+ end
85
+
86
+ say " Found #{yaml_files.length} blueprint(s)", :cyan
87
+
88
+ # 3. Process each blueprint
89
+ is_multi_tenant = multi_tenant_enabled?
90
+ org_identifier = detect_org_identifier
91
+ generated_count = 0
92
+ skipped_count = 0
93
+ all_blueprints = []
94
+ all_generated_files = {}
95
+
96
+ yaml_files.each do |yaml_file|
97
+ filename = File.basename(yaml_file)
98
+
99
+ begin
100
+ blueprint = parser.parse_model(yaml_file)
101
+ rescue => e
102
+ say " ✗ #{filename}: #{e.message}", :red
103
+ next
104
+ end
105
+
106
+ # Validate
107
+ result = validator.validate_model(blueprint, roles)
108
+
109
+ unless result[:valid]
110
+ say " ✗ #{filename}:", :red
111
+ result[:errors].each { |e| say " • #{e}", :red }
112
+ next
113
+ end
114
+
115
+ result[:warnings].each { |w| say " ⚠ #{w}", :yellow }
116
+
117
+ # Check manifest
118
+ current_hash = parser.compute_file_hash(yaml_file)
119
+
120
+ unless options[:force]
121
+ unless manifest.has_changed?(filename, current_hash)
122
+ say " ⊘ #{blueprint[:model]} — unchanged, skipping", :light_black
123
+ skipped_count += 1
124
+ all_blueprints << blueprint
125
+ next
126
+ end
127
+ end
128
+
129
+ all_blueprints << blueprint
130
+ generated_files = []
131
+
132
+ say " → #{blueprint[:model]}...", :cyan
133
+
134
+ unless options[:dry_run]
135
+ # Generate model
136
+ model_path = generate_model(blueprint, is_multi_tenant)
137
+ generated_files << model_path
138
+ say " ✓ Model: #{model_path}", :green
139
+
140
+ # Generate migration
141
+ migration_path = generate_migration(blueprint)
142
+ generated_files << migration_path
143
+ say " ✓ Migration: #{migration_path}", :green
144
+
145
+ # Generate factory
146
+ factory_path = generate_factory(blueprint)
147
+ generated_files << factory_path
148
+ say " ✓ Factory: #{factory_path}", :green
149
+
150
+ # Generate scope
151
+ scope_path = generate_scope(blueprint)
152
+ generated_files << scope_path
153
+ say " ✓ Scope: #{scope_path}", :green
154
+
155
+ # Generate policy
156
+ policy_path = generate_policy(blueprint)
157
+ generated_files << policy_path
158
+ say " ✓ Policy: #{policy_path}", :green
159
+
160
+ # Generate tests
161
+ unless options[:skip_tests]
162
+ test_path = generate_tests(blueprint, is_multi_tenant, org_identifier)
163
+ generated_files << test_path
164
+ say " ✓ Tests: #{test_path}", :green
165
+ end
166
+
167
+ # Register in config
168
+ register_model_in_config(blueprint[:model])
169
+
170
+ # Record in manifest
171
+ manifest.record_generation(filename, current_hash, generated_files)
172
+ all_generated_files[filename] = generated_files
173
+ end
174
+
175
+ generated_count += 1
176
+ end
177
+
178
+ # 4. Generate cross-model seeders
179
+ unless options[:skip_seeders] || options[:dry_run] || all_blueprints.empty?
180
+ generate_seeders(roles, all_blueprints, is_multi_tenant)
181
+ end
182
+
183
+ # 5. Save manifest
184
+ manifest.save unless options[:dry_run]
185
+
186
+ # 6. Summary
187
+ say ""
188
+ say "Blueprint generation complete!", :green
189
+ say " Generated: #{generated_count} model(s)", :cyan
190
+ say " Skipped: #{skipped_count} (unchanged)", :light_black
191
+ say ""
192
+ end
193
+
194
+ private
195
+
196
+ # ----------------------------------------------------------------
197
+ # Banner
198
+ # ----------------------------------------------------------------
199
+
200
+ def print_banner
201
+ say ""
202
+ lines = [
203
+ " █████╗ ██████╗ ███████╗███╗ ██╗████████╗ ██████╗ ██████╗ ██████╗ ███████╗",
204
+ " ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝██╔═══██╗██╔══██╗██╔════╝",
205
+ " ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
206
+ " ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
207
+ " ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ╚██████╗╚██████╔╝██████╔╝███████╗",
208
+ " ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝"
209
+ ]
210
+
211
+ gradient = [
212
+ [0, 255, 255], [0, 230, 200], [100, 220, 100],
213
+ [255, 220, 50], [255, 170, 30], [255, 120, 0]
214
+ ]
215
+
216
+ lines.each_with_index do |text, i|
217
+ r, g, b = gradient[i]
218
+ $stdout.puts "\033[38;2;#{r};#{g};#{b}m#{text}\033[0m"
219
+ end
220
+
221
+ say ""
222
+ say " + AgentCode :: Blueprint :: Zero-Token Code Generation +", :cyan
223
+ say ""
224
+ end
225
+
226
+ # ----------------------------------------------------------------
227
+ # Model generation
228
+ # ----------------------------------------------------------------
229
+
230
+ def generate_model(blueprint, is_multi_tenant)
231
+ name = blueprint[:model]
232
+ table_name = blueprint[:table]
233
+ columns = blueprint[:columns]
234
+ opts = blueprint[:options]
235
+
236
+ belongs_to_org = opts[:belongs_to_organization]
237
+ soft_deletes = opts[:soft_deletes]
238
+ audit_trail = opts[:audit_trail]
239
+
240
+ fillable = columns.map { |c| c[:name] }.reject { |n| n == "organization_id" }
241
+ filter_cols = columns.reject { |c| %w[text json].include?(c[:type]) }.map { |c| c[:name] }
242
+ sort_cols = (columns.reject { |c| %w[text json].include?(c[:type]) }.map { |c| c[:name] } + ["created_at"]).uniq
243
+ field_cols = (["id"] + columns.map { |c| c[:name] } + ["created_at"]).uniq
244
+ include_cols = columns.select { |c| c[:type] == "foreignId" && c[:foreign_model] }
245
+ .map { |c| c[:name].sub(/_id\z/, "") }
246
+
247
+ content = <<~RUBY
248
+ # frozen_string_literal: true
249
+
250
+ class #{name} < AgentCode::AgentCodeModel
251
+ RUBY
252
+
253
+ content += " include AgentCode::BelongsToOrganization\n" if belongs_to_org
254
+ content += " include Discard::Model\n" if soft_deletes
255
+ content += " include AgentCode::HasAuditTrail\n" if audit_trail
256
+
257
+ # Relationships
258
+ fk_columns = columns.select { |c| c[:type] == "foreignId" && c[:foreign_model] }
259
+ has_relationships = fk_columns.any? { |col| !(belongs_to_org && col[:foreign_model] == "Organization") }
260
+
261
+ if has_relationships
262
+ content += "\n"
263
+ fk_columns.each do |col|
264
+ relation_name = col[:name].sub(/_id\z/, "")
265
+ next if belongs_to_org && col[:foreign_model] == "Organization"
266
+
267
+ opts = "class_name: '#{col[:foreign_model]}'"
268
+ opts += ", optional: true" if col[:nullable]
269
+ content += " belongs_to :#{relation_name}, #{opts}\n"
270
+ end
271
+ end
272
+
273
+ # Query Builder configuration
274
+ content += "\n"
275
+ content += " agentcode_filters #{filter_cols.map { |c| ":#{c}" }.join(', ')}\n" unless filter_cols.empty?
276
+ content += " agentcode_sorts #{sort_cols.map { |c| ":#{c}" }.join(', ')}\n" unless sort_cols.empty?
277
+ content += " agentcode_fields #{field_cols.map { |c| ":#{c}" }.join(', ')}\n" unless field_cols.empty?
278
+ content += " agentcode_includes #{include_cols.map { |c| ":#{c}" }.join(', ')}\n" unless include_cols.empty?
279
+
280
+ # Validations
281
+
282
+ columns.each do |col|
283
+ validations = column_to_validations(col, table_name)
284
+ content += " validates :#{col[:name]}, #{validations}, allow_nil: true\n" unless validations.empty?
285
+ end
286
+
287
+ content += "end\n"
288
+
289
+ path = "app/models/#{name.underscore}.rb"
290
+ write_file(path, content)
291
+ path
292
+ end
293
+
294
+ # ----------------------------------------------------------------
295
+ # Migration generation
296
+ # ----------------------------------------------------------------
297
+
298
+ def generate_migration(blueprint)
299
+ table_name = blueprint[:table]
300
+ columns = blueprint[:columns]
301
+ soft_deletes = blueprint[:options][:soft_deletes]
302
+ belongs_to_org = blueprint[:options][:belongs_to_organization]
303
+
304
+ timestamp = (Time.current + @migration_timestamp_offset).strftime("%Y%m%d%H%M%S")
305
+ @migration_timestamp_offset += 1
306
+ class_name = "Create#{blueprint[:model].pluralize}"
307
+
308
+ lines = []
309
+
310
+ # Add organization reference if belongs_to_organization
311
+ if belongs_to_org && multi_tenant_enabled?
312
+ lines << "t.references :organization, null: false, foreign_key: true"
313
+ end
314
+
315
+ columns.each do |col|
316
+ lines << column_to_migration_line(col)
317
+ end
318
+
319
+ content = <<~RUBY
320
+ # frozen_string_literal: true
321
+
322
+ class #{class_name} < ActiveRecord::Migration[8.0]
323
+ def change
324
+ create_table :#{table_name} do |t|
325
+ #{lines.map { |l| " #{l}" }.join("\n")}
326
+ #{" t.datetime :discarded_at\n t.index :discarded_at" if soft_deletes}
327
+ t.timestamps
328
+ end
329
+ end
330
+ end
331
+ RUBY
332
+
333
+ path = "db/migrate/#{timestamp}_create_#{table_name}.rb"
334
+ write_file(path, content)
335
+ path
336
+ end
337
+
338
+ # ----------------------------------------------------------------
339
+ # Factory generation
340
+ # ----------------------------------------------------------------
341
+
342
+ def generate_factory(blueprint)
343
+ factory_gen = AgentCode::Blueprint::Generators::FactoryGenerator.new
344
+ content = factory_gen.generate(blueprint)
345
+
346
+ path = "spec/factories/#{blueprint[:model].underscore.pluralize}.rb"
347
+ write_file(path, content)
348
+ path
349
+ end
350
+
351
+ # ----------------------------------------------------------------
352
+ # Scope generation
353
+ # ----------------------------------------------------------------
354
+
355
+ def generate_scope(blueprint)
356
+ name = blueprint[:model]
357
+
358
+ content = <<~RUBY
359
+ # frozen_string_literal: true
360
+
361
+ module Scopes
362
+ class #{name}Scope < AgentCode::ResourceScope
363
+ # Custom query scope for #{name}.
364
+ # Applied automatically to all #{name} queries via HasAutoScope.
365
+ #
366
+ # Available methods: user, organization, role
367
+ #
368
+ # def apply(relation)
369
+ # relation.where(active: true)
370
+ # end
371
+ end
372
+ end
373
+ RUBY
374
+
375
+ path = "app/models/scopes/#{name.underscore}_scope.rb"
376
+ write_file(path, content)
377
+ path
378
+ end
379
+
380
+ # ----------------------------------------------------------------
381
+ # Policy generation
382
+ # ----------------------------------------------------------------
383
+
384
+ def generate_policy(blueprint)
385
+ policy_gen = AgentCode::Blueprint::Generators::PolicyGenerator.new
386
+ content = policy_gen.generate(blueprint)
387
+
388
+ path = "app/policies/#{blueprint[:model].underscore}_policy.rb"
389
+ write_file(path, content)
390
+ path
391
+ end
392
+
393
+ # ----------------------------------------------------------------
394
+ # Test generation
395
+ # ----------------------------------------------------------------
396
+
397
+ def generate_tests(blueprint, is_multi_tenant, org_identifier)
398
+ test_gen = AgentCode::Blueprint::Generators::TestGenerator.new
399
+ content = test_gen.generate(blueprint, is_multi_tenant, org_identifier)
400
+
401
+ path = "spec/models/#{blueprint[:model].underscore}_spec.rb"
402
+ write_file(path, content)
403
+ path
404
+ end
405
+
406
+ # ----------------------------------------------------------------
407
+ # Seeder generation
408
+ # ----------------------------------------------------------------
409
+
410
+ def generate_seeders(roles, blueprints, is_multi_tenant)
411
+ seeder_gen = AgentCode::Blueprint::Generators::SeederGenerator.new
412
+ aggregated = seeder_gen.aggregate_permissions(blueprints)
413
+
414
+ if is_multi_tenant
415
+ # Role seeder
416
+ role_content = seeder_gen.generate_role_seeder(roles)
417
+ write_file("db/seeds/role_seeder.rb", role_content)
418
+ say " ✓ Seeder: db/seeds/role_seeder.rb", :green
419
+
420
+ # UserRole seeder
421
+ user_role_content = seeder_gen.generate_user_role_seeder(roles, aggregated)
422
+ write_file("db/seeds/user_role_seeder.rb", user_role_content)
423
+ say " ✓ Seeder: db/seeds/user_role_seeder.rb", :green
424
+ else
425
+ # UserPermission seeder
426
+ user_perm_content = seeder_gen.generate_user_permission_seeder(roles, aggregated)
427
+ write_file("db/seeds/user_permission_seeder.rb", user_perm_content)
428
+ say " ✓ Seeder: db/seeds/user_permission_seeder.rb", :green
429
+ end
430
+ end
431
+
432
+ # ----------------------------------------------------------------
433
+ # Config registration
434
+ # ----------------------------------------------------------------
435
+
436
+ def register_model_in_config(name)
437
+ config_path = Rails.root.join("config/initializers/agentcode.rb")
438
+ return unless File.exist?(config_path)
439
+
440
+ content = File.read(config_path)
441
+ slug = name.underscore.pluralize
442
+
443
+ # Check if model is already registered (non-commented line)
444
+ return if content.match?(/^\s+\w+\.model\s+:#{slug}\b/)
445
+
446
+ # Detect the block variable name used in the config file (e.g., config, c, etc.)
447
+ block_var = content.match(/AgentCode\.configure\s+do\s+\|(\w+)\|/)&.captures&.first || "config"
448
+
449
+ new_entry = " #{block_var}.model :#{slug}, '#{name}'"
450
+
451
+ # Try to insert before the commented-out example line (matching any variable name)
452
+ if content.include?("# #{block_var}.model :posts, 'Post'")
453
+ content = content.gsub(
454
+ "# #{block_var}.model :posts, 'Post'",
455
+ "#{new_entry}\n # #{block_var}.model :posts, 'Post'"
456
+ )
457
+ elsif content.match?(/# \w+\.model :posts, 'Post'/)
458
+ content = content.sub(
459
+ /# (\w+)\.model :posts, 'Post'/,
460
+ "#{new_entry}\n # \\1.model :posts, 'Post'"
461
+ )
462
+ else
463
+ # Fallback: insert after the Models section comment
464
+ content = content.sub(
465
+ /(# Register your models here.*?\n)/,
466
+ "\\1#{new_entry}\n"
467
+ )
468
+ end
469
+
470
+ File.write(config_path, content)
471
+ end
472
+
473
+ # ----------------------------------------------------------------
474
+ # Helpers
475
+ # ----------------------------------------------------------------
476
+
477
+ def multi_tenant_enabled?
478
+ config_path = Rails.root.join("config/initializers/agentcode.rb")
479
+ return false unless File.exist?(config_path)
480
+
481
+ File.read(config_path).include?("route_group :tenant")
482
+ end
483
+
484
+ def detect_org_identifier
485
+ config_path = Rails.root.join("config/initializers/agentcode.rb")
486
+ return "id" unless File.exist?(config_path)
487
+
488
+ content = File.read(config_path)
489
+ if content.match(/organization_identifier_column.*?['"](\w+)['"]/)
490
+ $1
491
+ else
492
+ "slug"
493
+ end
494
+ end
495
+
496
+ def write_file(relative_path, content)
497
+ full_path = Rails.root.join(relative_path)
498
+ FileUtils.mkdir_p(File.dirname(full_path))
499
+ File.write(full_path, content)
500
+ end
501
+
502
+ def column_to_validations(column, _table_name)
503
+ parts = []
504
+
505
+ case column[:type]
506
+ when "string"
507
+ parts << "length: { maximum: 255 }"
508
+ when "integer", "bigInteger"
509
+ parts << "numericality: { only_integer: true }"
510
+ when "boolean"
511
+ parts << "inclusion: { in: [true, false] }"
512
+ when "decimal", "float"
513
+ parts << "numericality: true"
514
+ end
515
+
516
+ parts.join(", ")
517
+ end
518
+
519
+ def column_to_migration_line(col)
520
+ case col[:type]
521
+ when "foreignId", "references"
522
+ ref_name = col[:name].sub(/_id\z/, "")
523
+ foreign_table = col[:foreign_model]&.underscore&.pluralize
524
+
525
+ # If the reference name doesn't match the foreign model's table,
526
+ # we need to specify the target table explicitly
527
+ if foreign_table && foreign_table != ref_name.pluralize
528
+ line = "t.references :#{ref_name}, foreign_key: { to_table: :#{foreign_table} }"
529
+ else
530
+ line = "t.references :#{ref_name}, foreign_key: true"
531
+ end
532
+ line += ", null: true" if col[:nullable]
533
+ line
534
+ when "decimal"
535
+ precision = col[:precision] || 8
536
+ scale = col[:scale] || 2
537
+ line = "t.decimal :#{col[:name]}, precision: #{precision}, scale: #{scale}"
538
+ line += ", null: true" if col[:nullable]
539
+ line
540
+ else
541
+ line = "t.#{col[:type]} :#{col[:name]}"
542
+ line += ", null: true" if col[:nullable]
543
+ line += ", default: #{col[:default].inspect}" if col[:default]
544
+ line
545
+ end
546
+ end
547
+ end
548
+ end
549
+ end