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,441 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "agentcode/commands/base_command"
4
+
5
+ module AgentCode
6
+ module Commands
7
+ # Interactive setup wizard — mirrors Laravel `php artisan agentcode:install` exactly.
8
+ #
9
+ # Usage: rails agentcode:install
10
+ class InstallCommand < BaseCommand
11
+ def perform
12
+ print_banner
13
+
14
+ say ""
15
+ say "+ AgentCode :: Install :: Let's build something great +", :cyan
16
+ say ""
17
+
18
+ features = multi_select("Which features would you like to configure?") do |menu|
19
+ menu.default 1
20
+ menu.choice "Publish config & routes", "publish"
21
+ menu.choice "Multi-tenant support (Organizations, Roles)", "multi_tenant"
22
+ menu.choice "Audit trail (change logging)", "audit_trail"
23
+ end
24
+
25
+ test_framework = select("Which test framework do you use?") do |menu|
26
+ menu.default 1
27
+ menu.choice "RSpec", "rspec"
28
+ menu.choice "Minitest", "minitest"
29
+ end
30
+
31
+ identifier_column = "id"
32
+ roles = ["admin"]
33
+
34
+ if features.include?("multi_tenant")
35
+ identifier_column = ask("What column should be used to identify organizations?", default: "id")
36
+
37
+ roles_input = ask("What roles should your app have?", default: "admin, editor, viewer")
38
+ roles = (["admin"] + roles_input.split(",").map(&:strip)).uniq
39
+ end
40
+
41
+ say ""
42
+
43
+ if features.include?("publish")
44
+ task("Publishing config") { publish_config(test_framework) }
45
+ task("Publishing routes") { publish_routes }
46
+ task("Creating blueprint directory") { create_blueprint_directory }
47
+ end
48
+
49
+ if features.include?("multi_tenant")
50
+ task("Creating migrations") { create_multi_tenant_migrations }
51
+ task("Creating models") { create_multi_tenant_models(roles) }
52
+ task("Creating factories") { create_factories }
53
+ task("Creating policies") { create_policies }
54
+ task("Updating config") { update_config(identifier_column) }
55
+ task("Creating seeders") { create_seeders(roles) }
56
+ end
57
+
58
+ if features.include?("audit_trail")
59
+ task("Creating audit trail migration") { create_audit_trail_migration }
60
+ end
61
+
62
+ say ""
63
+ run_post_install_steps(features)
64
+
65
+ install_ai_skill
66
+
67
+ say ""
68
+ say "AgentCode installed successfully!", :green
69
+ say ""
70
+
71
+ print_next_steps(features)
72
+ end
73
+
74
+ private
75
+
76
+ # ----------------------------------------------------------------
77
+ # Banner
78
+ # ----------------------------------------------------------------
79
+
80
+ def print_banner
81
+ say ""
82
+
83
+ lines = [
84
+ " █████╗ ██████╗ ███████╗███╗ ██╗████████╗ ██████╗ ██████╗ ██████╗ ███████╗",
85
+ " ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝██╔════╝██╔═══██╗██╔══██╗██╔════╝",
86
+ " ███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║ ██║ ██║ ██║██║ ██║█████╗ ",
87
+ " ██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
88
+ " ██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║ ╚██████╗╚██████╔╝██████╔╝███████╗",
89
+ " ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝"
90
+ ]
91
+
92
+ gradient = [
93
+ [0, 255, 255],
94
+ [0, 230, 200],
95
+ [100, 220, 100],
96
+ [255, 220, 50],
97
+ [255, 170, 30],
98
+ [255, 120, 0]
99
+ ]
100
+
101
+ lines.each_with_index do |text, i|
102
+ r, g, b = gradient[i]
103
+ $stdout.puts "\033[38;2;#{r};#{g};#{b}m#{text}\033[0m"
104
+ end
105
+ end
106
+
107
+ # ----------------------------------------------------------------
108
+ # Publish
109
+ # ----------------------------------------------------------------
110
+
111
+ def publish_config(test_framework)
112
+ template_path = File.expand_path("../../templates/agentcode.rb", __FILE__)
113
+ dest_path = Rails.root.join("config/initializers/agentcode.rb")
114
+
115
+ FileUtils.mkdir_p(File.dirname(dest_path))
116
+
117
+ content = File.read(template_path)
118
+ content = content.gsub('test_framework: "rspec"', "test_framework: \"#{test_framework}\"")
119
+ File.write(dest_path, content)
120
+ end
121
+
122
+ def publish_routes
123
+ template_path = File.expand_path("../../templates/routes.rb", __FILE__)
124
+ dest_path = Rails.root.join("config/routes/agentcode.rb")
125
+
126
+ FileUtils.mkdir_p(File.dirname(dest_path))
127
+ FileUtils.cp(template_path, dest_path)
128
+ end
129
+
130
+ # ----------------------------------------------------------------
131
+ # Multi-tenant
132
+ # ----------------------------------------------------------------
133
+
134
+ def create_multi_tenant_migrations
135
+ timestamp = Time.current.strftime("%Y%m%d%H%M%S")
136
+ migrations_path = Rails.root.join("db/migrate")
137
+ FileUtils.mkdir_p(migrations_path)
138
+
139
+ templates_dir = File.expand_path("../../templates/multi_tenant/migrations", __FILE__)
140
+
141
+ {
142
+ "create_users" => "#{timestamp}00",
143
+ "create_organizations" => "#{timestamp}01",
144
+ "create_roles" => "#{timestamp}02",
145
+ "create_user_roles" => "#{timestamp}03"
146
+ }.each do |name, ts|
147
+ template = File.join(templates_dir, "#{name}.rb.erb")
148
+ next unless File.exist?(template)
149
+
150
+ content = ERB.new(File.read(template), trim_mode: "-").result(binding)
151
+ File.write(migrations_path.join("#{ts}_#{name}.rb"), content)
152
+ end
153
+ end
154
+
155
+ def create_multi_tenant_models(roles)
156
+ models_path = Rails.root.join("app/models")
157
+ FileUtils.mkdir_p(models_path)
158
+
159
+ templates_dir = File.expand_path("../../templates/multi_tenant/models", __FILE__)
160
+
161
+ %w[user organization role user_role].each do |model|
162
+ template = File.join(templates_dir, "#{model}.rb.erb")
163
+ next unless File.exist?(template)
164
+
165
+ content = ERB.new(File.read(template), trim_mode: "-").result_with_hash(roles: roles)
166
+ File.write(models_path.join("#{model}.rb"), content)
167
+ end
168
+ end
169
+
170
+ def create_factories
171
+ factories_path = Rails.root.join("spec/factories")
172
+ FileUtils.mkdir_p(factories_path)
173
+
174
+ templates_dir = File.expand_path("../../templates/multi_tenant/factories", __FILE__)
175
+
176
+ %w[users organizations roles user_roles].each do |factory|
177
+ template = File.join(templates_dir, "#{factory}.rb.erb")
178
+ next unless File.exist?(template)
179
+
180
+ content = ERB.new(File.read(template), trim_mode: "-").result(binding)
181
+ File.write(factories_path.join("#{factory}.rb"), content)
182
+ end
183
+ end
184
+
185
+ def create_policies
186
+ policies_path = Rails.root.join("app/policies")
187
+ FileUtils.mkdir_p(policies_path)
188
+
189
+ templates_dir = File.expand_path("../../templates/multi_tenant/policies", __FILE__)
190
+
191
+ %w[organization_policy role_policy].each do |policy|
192
+ template = File.join(templates_dir, "#{policy}.rb.erb")
193
+ next unless File.exist?(template)
194
+
195
+ content = ERB.new(File.read(template), trim_mode: "-").result(binding)
196
+ File.write(policies_path.join("#{policy}.rb"), content)
197
+ end
198
+ end
199
+
200
+ def update_config(identifier_column)
201
+ config_path = Rails.root.join("config/initializers/agentcode.rb")
202
+ return unless File.exist?(config_path)
203
+
204
+ content = File.read(config_path)
205
+
206
+ # Update organization_identifier_column
207
+ content = content.gsub(
208
+ 'organization_identifier_column: "id"',
209
+ "organization_identifier_column: \"#{identifier_column}\""
210
+ )
211
+
212
+ # Add organization and role models
213
+ unless content.include?("config.model :organizations")
214
+ content = content.gsub(
215
+ "# config.model :posts, 'Post'",
216
+ "config.model :organizations, 'Organization'\n config.model :roles, 'Role'\n # config.model :posts, 'Post'"
217
+ )
218
+ end
219
+
220
+ # Add tenant route group
221
+ unless content.include?("config.route_group :tenant")
222
+ content = content.gsub(
223
+ "# config.route_group :default",
224
+ "config.route_group :tenant, prefix: \":organization\", middleware: [AgentCode::Middleware::ResolveOrganizationFromRoute], models: :all\n # config.route_group :default"
225
+ )
226
+ end
227
+
228
+ File.write(config_path, content)
229
+ end
230
+
231
+ def create_seeders(roles)
232
+ seeders_path = Rails.root.join("db/seeds")
233
+ FileUtils.mkdir_p(seeders_path)
234
+
235
+ templates_dir = File.expand_path("../../templates/multi_tenant/seeders", __FILE__)
236
+
237
+ template = File.join(templates_dir, "role_seeder.rb.erb")
238
+ if File.exist?(template)
239
+ content = ERB.new(File.read(template), trim_mode: "-").result_with_hash(roles: roles)
240
+ File.write(seeders_path.join("role_seeder.rb"), content)
241
+ end
242
+
243
+ template = File.join(templates_dir, "organization_seeder.rb.erb")
244
+ if File.exist?(template)
245
+ content = ERB.new(File.read(template), trim_mode: "-").result(binding)
246
+ File.write(seeders_path.join("organization_seeder.rb"), content)
247
+ end
248
+ end
249
+
250
+ # ----------------------------------------------------------------
251
+ # Audit trail
252
+ # ----------------------------------------------------------------
253
+
254
+ def create_audit_trail_migration
255
+ timestamp = Time.current.strftime("%Y%m%d%H%M%S")
256
+ migrations_path = Rails.root.join("db/migrate")
257
+ FileUtils.mkdir_p(migrations_path)
258
+
259
+ # Check for existing
260
+ existing = Dir.glob(migrations_path.join("*_create_audit_logs.rb"))
261
+ return unless existing.empty?
262
+
263
+ template = File.expand_path("../../templates/audit_trail/create_audit_logs.rb.erb", __FILE__)
264
+ if File.exist?(template)
265
+ content = ERB.new(File.read(template), trim_mode: "-").result(binding)
266
+ File.write(migrations_path.join("#{timestamp}_create_audit_logs.rb"), content)
267
+ end
268
+ end
269
+
270
+ # ----------------------------------------------------------------
271
+ # Post-install
272
+ # ----------------------------------------------------------------
273
+
274
+ def run_post_install_steps(features)
275
+ has_migrations = features.include?("multi_tenant") || features.include?("audit_trail")
276
+
277
+ if has_migrations
278
+ if yes?("Would you like to run migrations now?")
279
+ task("Running migrations") { system("rails db:migrate") }
280
+ end
281
+ end
282
+
283
+ if features.include?("multi_tenant")
284
+ if yes?("Would you like to seed the database?")
285
+ task("Seeding database") { system("rails db:seed") }
286
+ end
287
+ end
288
+ end
289
+
290
+ def print_next_steps(features)
291
+ say "Remaining steps:", :yellow
292
+ say ""
293
+
294
+ step = 1
295
+
296
+ if features.include?("audit_trail")
297
+ say " #{step}. Add AgentCode::HasAuditTrail concern to your models:"
298
+ say " include AgentCode::HasAuditTrail"
299
+ step += 1
300
+ end
301
+
302
+ if features.include?("multi_tenant")
303
+ say " #{step}. Add AgentCode::HasPermissions concern to your User model:"
304
+ say " include AgentCode::HasPermissions"
305
+ step += 1
306
+ end
307
+
308
+ say ""
309
+ end
310
+
311
+ # ----------------------------------------------------------------
312
+ # Blueprint directory
313
+ # ----------------------------------------------------------------
314
+
315
+ def create_blueprint_directory
316
+ bp_dir = Rails.root.join(".agentcode", "blueprints")
317
+ FileUtils.mkdir_p(bp_dir)
318
+
319
+ guide_path = bp_dir.join("..", "BLUEPRINT.md")
320
+ unless File.exist?(guide_path)
321
+ File.write(guide_path, blueprint_guide_content)
322
+ end
323
+ end
324
+
325
+ def blueprint_guide_content
326
+ <<~MD
327
+ # AgentCode Blueprint — AI Guide
328
+
329
+ Use this file to teach AI assistants how to generate valid YAML blueprint files.
330
+
331
+ ## Quick Start
332
+
333
+ 1. Create `_roles.yaml` in `.agentcode/blueprints/` with your role definitions
334
+ 2. Create `{model_slug}.yaml` for each model
335
+ 3. Run `rails agentcode:blueprint` to generate all files
336
+
337
+ ## Roles Format
338
+
339
+ ```yaml
340
+ roles:
341
+ owner:
342
+ name: Owner
343
+ description: "Full access"
344
+ viewer:
345
+ name: Viewer
346
+ description: "Read-only"
347
+ ```
348
+
349
+ ## Model Format
350
+
351
+ ```yaml
352
+ model: Contract
353
+ slug: contracts
354
+
355
+ options:
356
+ belongs_to_organization: true
357
+ soft_deletes: true
358
+
359
+ columns:
360
+ title:
361
+ type: string
362
+ filterable: true
363
+
364
+ permissions:
365
+ owner:
366
+ actions: [index, show, store, update, destroy]
367
+ show_fields: "*"
368
+ create_fields: "*"
369
+ update_fields: "*"
370
+ ```
371
+
372
+ ## Valid Column Types
373
+ string, text, integer, bigInteger, boolean, date, datetime, timestamp, decimal, float, json, uuid, foreignId
374
+
375
+ ## Valid Actions
376
+ index, show, store, update, destroy, trashed, restore, forceDelete
377
+ MD
378
+ end
379
+
380
+ # ----------------------------------------------------------------
381
+ # AI Skill
382
+ # ----------------------------------------------------------------
383
+
384
+ def install_ai_skill
385
+ say ""
386
+
387
+ ai_tools = multi_select("Install AgentCode AI Skill for which tools? (select none to skip)") do |menu|
388
+ menu.default 1
389
+ menu.choice "Claude Code (.claude/skills/agentcode/)", "claude"
390
+ menu.choice "Cursor (.cursor/rules/agentcode/)", "cursor"
391
+ menu.choice "AI Directory (.ai/skills/agentcode/)", "ai"
392
+ end
393
+
394
+ return if ai_tools.empty?
395
+
396
+ url = "https://startsoft-dev.github.io/agentcode-docs/skills/rails/SKILL.md"
397
+
398
+ destinations = {
399
+ "claude" => ".claude/skills/agentcode/SKILL.md",
400
+ "cursor" => ".cursor/rules/agentcode/SKILL.md",
401
+ "ai" => ".ai/skills/agentcode/SKILL.md"
402
+ }
403
+
404
+ # Download once
405
+ require "net/http"
406
+ require "uri"
407
+
408
+ uri = URI.parse(url)
409
+ response = Net::HTTP.get_response(uri)
410
+
411
+ # Follow redirect if needed
412
+ if response.is_a?(Net::HTTPRedirection)
413
+ uri = URI.parse(response["location"])
414
+ response = Net::HTTP.get_response(uri)
415
+ end
416
+
417
+ unless response.is_a?(Net::HTTPSuccess)
418
+ say " Could not download skill file. You can manually download it from:", :yellow
419
+ say " #{url}"
420
+ return
421
+ end
422
+
423
+ content = response.body
424
+
425
+ ai_tools.each do |tool|
426
+ dest_file = Rails.root.join(destinations[tool])
427
+
428
+ task("Installing skill for #{tool}") do
429
+ FileUtils.mkdir_p(File.dirname(dest_file))
430
+ File.write(dest_file, content)
431
+ end
432
+ end
433
+
434
+ say " AI Skill installed successfully.", :green
435
+ rescue StandardError => e
436
+ say " Could not download skill file (#{e.message}). You can manually download it from:", :yellow
437
+ say " #{url}"
438
+ end
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "agentcode/commands/base_command"
4
+
5
+ module AgentCode
6
+ module Commands
7
+ # Generate an invitation link for testing — mirrors Laravel `php artisan invitation:link` exactly.
8
+ #
9
+ # Usage: rails invitation:link EMAIL ORG [--role=ROLE] [--create]
10
+ class InvitationLinkCommand < BaseCommand
11
+ attr_accessor :email, :organization_identifier, :options
12
+
13
+ def initialize
14
+ super
15
+ @options = { role: nil, create: false }
16
+ end
17
+
18
+ def perform(email = @email, organization_identifier = @organization_identifier)
19
+ role_identifier = options[:role]
20
+ should_create = options[:create]
21
+
22
+ # Find organization
23
+ identifier_column = AgentCode.config.multi_tenant[:organization_identifier_column] || "slug"
24
+
25
+ org_class = "Organization".safe_constantize
26
+ unless org_class
27
+ say "Organization model not found.", :red
28
+ return
29
+ end
30
+
31
+ organization = org_class.find_by(identifier_column => organization_identifier)
32
+
33
+ unless organization
34
+ say "Organization '#{organization_identifier}' not found.", :red
35
+ return
36
+ end
37
+
38
+ # Find or create invitation
39
+ invitation = OrganizationInvitation
40
+ .where(email: email, organization_id: organization.id, status: "pending")
41
+ .first
42
+
43
+ if !invitation && !should_create
44
+ say "No pending invitation found for '#{email}' in organization '#{organization.name}'.", :red
45
+ say "Use --create flag to create a new invitation."
46
+ return
47
+ end
48
+
49
+ if !invitation && should_create
50
+ unless role_identifier
51
+ say "Role is required when creating a new invitation. Use --role option.", :red
52
+ return
53
+ end
54
+
55
+ role_class = "Role".safe_constantize
56
+ unless role_class
57
+ say "Role model not found.", :red
58
+ return
59
+ end
60
+
61
+ role = if role_identifier.match?(/\A\d+\z/)
62
+ role_class.find_by(id: role_identifier)
63
+ else
64
+ role_class.find_by(slug: role_identifier)
65
+ end
66
+
67
+ unless role
68
+ say "Role '#{role_identifier}' not found.", :red
69
+ return
70
+ end
71
+
72
+ user_class = "User".safe_constantize
73
+ invited_by = user_class&.first
74
+
75
+ unless invited_by
76
+ say "No user found to assign as 'invited_by'. Please create a user first.", :red
77
+ return
78
+ end
79
+
80
+ invitation = OrganizationInvitation.create!(
81
+ organization_id: organization.id,
82
+ email: email,
83
+ role_id: role.id,
84
+ invited_by: invited_by.id
85
+ )
86
+
87
+ say "Created new invitation for #{email}.", :green
88
+ end
89
+
90
+ # Build the invitation URL
91
+ frontend_url = ENV.fetch("FRONTEND_URL", "http://localhost:5173")
92
+ url = "#{frontend_url}/accept-invitation?token=#{invitation.token}"
93
+
94
+ say ""
95
+ say "Invitation link for #{email}:", :green
96
+ say url
97
+ say ""
98
+ say "Token: #{invitation.token}"
99
+ say "Organization: #{organization.name} (#{organization.try(:slug) || organization.id})"
100
+ say "Role: #{invitation.role&.name}" if invitation.respond_to?(:role) && invitation.role
101
+ say "Status: #{invitation.status}"
102
+ say "Expires: #{invitation.expires_at&.strftime('%Y-%m-%d %H:%M:%S')}" if invitation.expires_at
103
+ say ""
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ # Multi-tenant scoping concern.
5
+ # Mirrors the Laravel BelongsToOrganization trait.
6
+ #
7
+ # Usage:
8
+ # class Post < ApplicationRecord
9
+ # include AgentCode::BelongsToOrganization
10
+ # end
11
+ #
12
+ # For nested ownership (model doesn't have organization_id directly),
13
+ # the path is auto-detected from belongs_to associations.
14
+ module BelongsToOrganization
15
+ extend ActiveSupport::Concern
16
+
17
+ included do
18
+ belongs_to :organization
19
+
20
+ # Auto-set organization_id on creation from request context
21
+ before_create :set_organization_from_context
22
+
23
+ # Default scope to filter by current organization
24
+ default_scope lambda {
25
+ if defined?(RequestStore) && RequestStore.store[:agentcode_organization]
26
+ where(organization_id: RequestStore.store[:agentcode_organization].id)
27
+ else
28
+ all
29
+ end
30
+ }
31
+ end
32
+
33
+ class_methods do
34
+ def for_organization(organization)
35
+ where(organization_id: organization.id)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def set_organization_from_context
42
+ return if organization_id.present?
43
+ return unless defined?(RequestStore)
44
+
45
+ org = RequestStore.store[:agentcode_organization]
46
+ self.organization_id = org.id if org
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentCode
4
+ # Main model concern that provides the DSL for configuring query builder options.
5
+ #
6
+ # Usage:
7
+ # class Post < ApplicationRecord
8
+ # include AgentCode::HasAgentCode
9
+ #
10
+ # agentcode_filters :status, :user_id
11
+ # agentcode_sorts :title, :created_at
12
+ # agentcode_default_sort '-created_at'
13
+ # agentcode_includes :user, :comments
14
+ # agentcode_fields :id, :title, :status, :created_at
15
+ # agentcode_search :title, :content, 'user.name'
16
+ # agentcode_per_page 25
17
+ # agentcode_pagination_enabled true
18
+ # agentcode_middleware 'throttle:60,1'
19
+ # agentcode_middleware_actions store: ['verified'], update: ['verified']
20
+ # agentcode_except_actions :destroy
21
+ # end
22
+ module HasAgentCode
23
+ extend ActiveSupport::Concern
24
+
25
+ included do
26
+ class_attribute :allowed_filters, default: []
27
+ class_attribute :allowed_sorts, default: []
28
+ class_attribute :default_sort_field, default: nil
29
+ class_attribute :allowed_includes, default: []
30
+ class_attribute :allowed_fields, default: []
31
+ class_attribute :allowed_search, default: []
32
+ class_attribute :agentcode_per_page_count, default: 25
33
+ class_attribute :pagination_enabled, default: false
34
+ class_attribute :agentcode_model_middleware, default: []
35
+ class_attribute :agentcode_middleware_actions_map, default: {}
36
+ class_attribute :agentcode_except_actions_list, default: []
37
+ class_attribute :agentcode_owner_path, default: nil
38
+ end
39
+
40
+ class_methods do
41
+ def agentcode_filters(*fields)
42
+ self.allowed_filters = fields.map(&:to_s)
43
+ end
44
+
45
+ def agentcode_sorts(*fields)
46
+ self.allowed_sorts = fields.map(&:to_s)
47
+ end
48
+
49
+ def agentcode_default_sort(field)
50
+ self.default_sort_field = field.to_s
51
+ end
52
+
53
+ def agentcode_includes(*relations)
54
+ self.allowed_includes = relations.map(&:to_s)
55
+ end
56
+
57
+ def agentcode_fields(*fields)
58
+ self.allowed_fields = fields.map(&:to_s)
59
+ end
60
+
61
+ def agentcode_search(*fields)
62
+ self.allowed_search = fields.map(&:to_s)
63
+ end
64
+
65
+ def agentcode_per_page(count)
66
+ self.agentcode_per_page_count = count
67
+ end
68
+
69
+ def agentcode_pagination_enabled(enabled = true)
70
+ self.pagination_enabled = enabled
71
+ end
72
+
73
+ def agentcode_middleware(*middleware)
74
+ self.agentcode_model_middleware = middleware.map(&:to_s)
75
+ end
76
+
77
+ def agentcode_middleware_actions(actions_hash)
78
+ self.agentcode_middleware_actions_map = actions_hash.transform_keys(&:to_s)
79
+ end
80
+
81
+ def agentcode_except_actions(*actions)
82
+ self.agentcode_except_actions_list = actions.map(&:to_s)
83
+ end
84
+
85
+ # Check if model uses soft deletes (Discard gem)
86
+ def uses_soft_deletes?
87
+ column_names.include?("discarded_at") || column_names.include?("deleted_at")
88
+ rescue ActiveRecord::StatementInvalid
89
+ false
90
+ end
91
+ end
92
+ end
93
+ end