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.
- checksums.yaml +7 -0
- data/README.md +59 -0
- data/lib/agentcode/blueprint/blueprint_parser.rb +198 -0
- data/lib/agentcode/blueprint/blueprint_validator.rb +209 -0
- data/lib/agentcode/blueprint/generators/factory_generator.rb +74 -0
- data/lib/agentcode/blueprint/generators/policy_generator.rb +154 -0
- data/lib/agentcode/blueprint/generators/seeder_generator.rb +160 -0
- data/lib/agentcode/blueprint/generators/test_generator.rb +291 -0
- data/lib/agentcode/blueprint/manifest_manager.rb +81 -0
- data/lib/agentcode/commands/base_command.rb +57 -0
- data/lib/agentcode/commands/blueprint_command.rb +549 -0
- data/lib/agentcode/commands/export_postman_command.rb +328 -0
- data/lib/agentcode/commands/generate_command.rb +563 -0
- data/lib/agentcode/commands/install_command.rb +441 -0
- data/lib/agentcode/commands/invitation_link_command.rb +107 -0
- data/lib/agentcode/concerns/belongs_to_organization.rb +49 -0
- data/lib/agentcode/concerns/has_agentcode.rb +93 -0
- data/lib/agentcode/concerns/has_audit_trail.rb +125 -0
- data/lib/agentcode/concerns/has_auto_scope.rb +91 -0
- data/lib/agentcode/concerns/has_permissions.rb +117 -0
- data/lib/agentcode/concerns/has_uuid.rb +26 -0
- data/lib/agentcode/concerns/has_validation.rb +250 -0
- data/lib/agentcode/concerns/hidable_columns.rb +180 -0
- data/lib/agentcode/configuration.rb +98 -0
- data/lib/agentcode/controllers/auth_controller.rb +242 -0
- data/lib/agentcode/controllers/invitations_controller.rb +231 -0
- data/lib/agentcode/controllers/resources_controller.rb +813 -0
- data/lib/agentcode/engine.rb +65 -0
- data/lib/agentcode/mailers/invitation_mailer.rb +22 -0
- data/lib/agentcode/middleware/resolve_organization_from_route.rb +72 -0
- data/lib/agentcode/models/agentcode_model.rb +387 -0
- data/lib/agentcode/models/audit_log.rb +17 -0
- data/lib/agentcode/models/organization_invitation.rb +57 -0
- data/lib/agentcode/policies/invitation_policy.rb +54 -0
- data/lib/agentcode/policies/resource_policy.rb +197 -0
- data/lib/agentcode/query_builder.rb +278 -0
- data/lib/agentcode/railtie.rb +11 -0
- data/lib/agentcode/resource_scope.rb +59 -0
- data/lib/agentcode/routes.rb +124 -0
- data/lib/agentcode/tasks/agentcode.rake +39 -0
- data/lib/agentcode/templates/agentcode.rb +71 -0
- data/lib/agentcode/templates/agentcode_model.rb +104 -0
- data/lib/agentcode/templates/audit_trail/create_audit_logs.rb.erb +26 -0
- data/lib/agentcode/templates/generate/factory.rb.erb +43 -0
- data/lib/agentcode/templates/generate/migration.rb.erb +26 -0
- data/lib/agentcode/templates/generate/model.rb.erb +55 -0
- data/lib/agentcode/templates/generate/policy.rb.erb +52 -0
- data/lib/agentcode/templates/generate/scope.rb.erb +31 -0
- data/lib/agentcode/templates/multi_tenant/factories/organizations.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/factories/roles.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/factories/user_roles.rb.erb +10 -0
- data/lib/agentcode/templates/multi_tenant/factories/users.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_organizations.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_roles.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_user_roles.rb.erb +16 -0
- data/lib/agentcode/templates/multi_tenant/migrations/create_users.rb.erb +15 -0
- data/lib/agentcode/templates/multi_tenant/models/organization.rb.erb +18 -0
- data/lib/agentcode/templates/multi_tenant/models/role.rb.erb +11 -0
- data/lib/agentcode/templates/multi_tenant/models/user.rb.erb +14 -0
- data/lib/agentcode/templates/multi_tenant/models/user_role.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/policies/organization_policy.rb.erb +6 -0
- data/lib/agentcode/templates/multi_tenant/policies/role_policy.rb.erb +6 -0
- data/lib/agentcode/templates/multi_tenant/seeders/organization_seeder.rb.erb +9 -0
- data/lib/agentcode/templates/multi_tenant/seeders/role_seeder.rb.erb +19 -0
- data/lib/agentcode/templates/routes.rb +13 -0
- data/lib/agentcode/version.rb +5 -0
- data/lib/agentcode/views/lumina/invitation_mailer/invite.html.erb +29 -0
- data/lib/agentcode-rails.rb +3 -0
- data/lib/agentcode.rb +26 -0
- 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
|