ask-rails 0.2.5 → 0.11.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 +4 -4
- data/CHANGELOG.md +157 -0
- data/README.md +94 -10
- data/app/controllers/ask/rails/chat_controller.rb +308 -0
- data/app/models/ask/rails/session.rb +23 -0
- data/app/views/ask/rails/chat/index.html.erb +6 -0
- data/app/views/layouts/ask/rails/application.html.erb +521 -0
- data/config/routes.rb +17 -0
- data/lib/ask/rails/audit_log.rb +181 -0
- data/lib/ask/rails/auth.rb +22 -0
- data/lib/ask/rails/configuration.rb +53 -1
- data/lib/ask/rails/engine.rb +1 -1
- data/lib/ask/rails/environment_permissions.rb +39 -0
- data/lib/ask/rails/persistence.rb +18 -5
- data/lib/ask/rails/railtie.rb +5 -1
- data/lib/ask/rails/tool.rb +29 -0
- data/lib/ask/rails/tools/read_file.rb +1 -4
- data/lib/ask/rails/tools/read_routes.rb +1 -4
- data/lib/ask/rails/tools/route_inspector.rb +69 -0
- data/lib/ask/rails/tools/run_command.rb +39 -1
- data/lib/ask/rails/tools/schema_graph.rb +174 -0
- data/lib/ask/rails/tools/search_codebase.rb +6 -7
- data/lib/ask/rails/version.rb +1 -1
- data/lib/ask/rails.rb +111 -4
- data/lib/generators/ask/rails/install/install_generator.rb +5 -0
- data/lib/generators/ask/rails/install/templates/audit_log_migration.rb +22 -0
- data/lib/generators/ask/rails/install/templates/initializer.rb +1 -1
- metadata +12 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module Rails
|
|
5
|
+
module Tools
|
|
6
|
+
class SchemaGraph < Ask::Rails::Tool
|
|
7
|
+
description "Return the full application schema graph — all models, tables, columns with types, " \
|
|
8
|
+
"associations (belongs_to, has_many, has_one, HABTM, through), validations, indexes, " \
|
|
9
|
+
"and polymorphic relationships. One call gives the agent a complete mental model " \
|
|
10
|
+
"of the application's data layer."
|
|
11
|
+
|
|
12
|
+
param :detail, type: :string, desc: "Detail level: 'all' (default), 'models', 'associations', 'tables'", required: false
|
|
13
|
+
|
|
14
|
+
TABLE_EXCLUSIONS = %w[schema_migrations ar_internal_metadata].freeze
|
|
15
|
+
|
|
16
|
+
def execute(detail: "all")
|
|
17
|
+
models = discover_ar_models
|
|
18
|
+
|
|
19
|
+
result = {
|
|
20
|
+
summary: {
|
|
21
|
+
model_count: models.size,
|
|
22
|
+
table_count: models.map { |m| m.table_name }.uniq.size,
|
|
23
|
+
association_count: models.sum { |m| m.reflect_on_all_associations.size }
|
|
24
|
+
},
|
|
25
|
+
models: %w[all models].include?(detail) ? build_model_details(models, detail) : nil,
|
|
26
|
+
associations: %w[all associations].include?(detail) ? build_association_graph(models) : nil,
|
|
27
|
+
tables: %w[all tables].include?(detail) ? build_table_details(detail) : nil
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def discover_ar_models
|
|
36
|
+
if defined?(::Rails::Application) && ::Rails.application
|
|
37
|
+
::Rails.application.eager_load! rescue nil
|
|
38
|
+
end
|
|
39
|
+
ActiveRecord::Base.descendants.reject do |klass|
|
|
40
|
+
klass.abstract_class? ||
|
|
41
|
+
klass.name.nil? ||
|
|
42
|
+
TABLE_EXCLUSIONS.include?(klass.table_name) ||
|
|
43
|
+
klass.name.start_with?("ActiveRecord::", "ActiveStorage::", "ActionText::")
|
|
44
|
+
end.sort_by(&:name)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
[]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_model_details(models, detail)
|
|
50
|
+
models.filter_map do |klass|
|
|
51
|
+
name = klass.name rescue nil
|
|
52
|
+
next nil unless name
|
|
53
|
+
|
|
54
|
+
entry = {
|
|
55
|
+
name: name,
|
|
56
|
+
table_name: klass.table_name,
|
|
57
|
+
primary_key: safe_primary_key(klass)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if klass.respond_to?(:columns)
|
|
61
|
+
entry[:columns] = safe_columns(klass)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if klass.respond_to?(:reflect_on_all_associations)
|
|
65
|
+
entry[:associations] = klass.reflect_on_all_associations.group_by(&:macro).transform_values { |refs|
|
|
66
|
+
refs.map { |a|
|
|
67
|
+
assoc = { name: a.name, class_name: a.class_name }
|
|
68
|
+
assoc[:through] = a.options[:through] if a.options[:through]
|
|
69
|
+
assoc[:source] = a.options[:source] if a.options[:source]
|
|
70
|
+
assoc[:foreign_key] = a.foreign_key
|
|
71
|
+
assoc[:polymorphic] = true if a.polymorphic?
|
|
72
|
+
assoc[:as] = a.options[:as] if a.options[:as]
|
|
73
|
+
assoc[:dependent] = a.options[:dependent] if a.options[:dependent]
|
|
74
|
+
assoc
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if klass.respond_to?(:validators)
|
|
80
|
+
entry[:validators] = klass.validators.map { |v|
|
|
81
|
+
{
|
|
82
|
+
attribute: v.attributes.first&.to_s,
|
|
83
|
+
kind: v.kind,
|
|
84
|
+
options: v.options.reject { |k, _| k == :if }
|
|
85
|
+
}
|
|
86
|
+
}.reject { |v| v[:attribute].nil? }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
entry
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_association_graph(models)
|
|
94
|
+
edges = []
|
|
95
|
+
|
|
96
|
+
models.each do |klass|
|
|
97
|
+
klass.reflect_on_all_associations.each do |a|
|
|
98
|
+
next if a.macro == :has_many && a.options[:through] # Skip through associations (derived)
|
|
99
|
+
|
|
100
|
+
target_model = find_model_for_association(models, a)
|
|
101
|
+
next unless target_model
|
|
102
|
+
|
|
103
|
+
edges << {
|
|
104
|
+
from: klass.name,
|
|
105
|
+
to: target_model.name,
|
|
106
|
+
type: a.macro,
|
|
107
|
+
via: a.name,
|
|
108
|
+
foreign_key: a.foreign_key,
|
|
109
|
+
polymorphic: a.polymorphic? || false
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
edges
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def find_model_for_association(models, association)
|
|
118
|
+
class_name = association.class_name
|
|
119
|
+
# Try exact match first, then match by class name suffix (handles namespace issues)
|
|
120
|
+
models.find { |m| m.name == class_name } ||
|
|
121
|
+
models.find { |m| m.name.end_with?("::#{class_name}") } ||
|
|
122
|
+
models.find { |m| class_name.end_with?("::#{m.name}") }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_table_details(detail)
|
|
126
|
+
models = discover_ar_models
|
|
127
|
+
tables = {}
|
|
128
|
+
|
|
129
|
+
models.each do |klass|
|
|
130
|
+
table = klass.table_name
|
|
131
|
+
next if TABLE_EXCLUSIONS.include?(table)
|
|
132
|
+
|
|
133
|
+
tables[table] = {
|
|
134
|
+
model: klass.name,
|
|
135
|
+
columns: safe_columns(klass),
|
|
136
|
+
indexes: fetch_indexes_for(table)
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
tables
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def safe_primary_key(klass)
|
|
144
|
+
klass.primary_key
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def safe_columns(klass)
|
|
150
|
+
klass.columns.map { |c|
|
|
151
|
+
col = { name: c.name, type: c.type, null: c.null }
|
|
152
|
+
col[:default] = c.default unless c.default.nil?
|
|
153
|
+
col[:primary_key] = true if c.name == klass.primary_key
|
|
154
|
+
col
|
|
155
|
+
}
|
|
156
|
+
rescue StandardError
|
|
157
|
+
[]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def fetch_indexes_for(table_name)
|
|
161
|
+
ActiveRecord::Base.connection.indexes(table_name).map do |idx|
|
|
162
|
+
{
|
|
163
|
+
name: idx.name,
|
|
164
|
+
columns: idx.columns,
|
|
165
|
+
unique: idx.unique
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
rescue StandardError
|
|
169
|
+
[]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -6,15 +6,14 @@ module Ask
|
|
|
6
6
|
class SearchCodebase < Ask::Rails::Tool
|
|
7
7
|
description "Search the Rails codebase with grep."
|
|
8
8
|
param :pattern, type: :string, desc: "Search pattern", required: true
|
|
9
|
-
param :path, type: :string, desc: "Subdirectory to search (optional)"
|
|
9
|
+
param :path, type: :string, desc: "Subdirectory to search (optional)", required: false
|
|
10
10
|
|
|
11
11
|
def execute(pattern:, path: nil)
|
|
12
|
-
search_path = path ? rails_root.join(path) : rails_root
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
12
|
+
search_path = (path ? rails_root.join(path) : rails_root).to_s
|
|
13
|
+
escaped_pattern = pattern.gsub("'", "'\\\\''")
|
|
14
|
+
escaped_path = search_path.gsub("'", "'\\\\''")
|
|
15
|
+
results = `cd #{rails_root} && grep -rn '#{escaped_pattern}' #{escaped_path} 2>&1 | head -50`
|
|
16
|
+
{ results: results.lines.map(&:chomp), count: results.lines.count }
|
|
18
17
|
end
|
|
19
18
|
end
|
|
20
19
|
end
|
data/lib/ask/rails/version.rb
CHANGED
data/lib/ask/rails.rb
CHANGED
|
@@ -16,21 +16,41 @@ module Ask
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def agent_session(**extra)
|
|
19
|
+
# Auto-prune if configured
|
|
20
|
+
cleanup! if configuration.max_session_age || configuration.max_sessions
|
|
21
|
+
|
|
19
22
|
tools = configuration.tools.map { |t| t.is_a?(Class) ? t.new : t }
|
|
20
23
|
prompt = extra.delete(:system_prompt) || configuration.system_prompt || default_system_prompt
|
|
21
24
|
|
|
25
|
+
# Resolve environment-specific permissions and wire into agent hooks
|
|
26
|
+
hooks = build_environment_hooks
|
|
27
|
+
|
|
22
28
|
Ask::Agent::Session.new(
|
|
23
29
|
model: configuration.default_model,
|
|
24
30
|
max_turns: configuration.max_turns,
|
|
25
31
|
system_prompt: prompt,
|
|
26
32
|
tools: tools,
|
|
27
|
-
|
|
33
|
+
state: configuration.persistence_adapter,
|
|
34
|
+
hooks: hooks,
|
|
28
35
|
**extra
|
|
29
36
|
)
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
def discover_tools!
|
|
33
|
-
self.configuration.tools = Ask::Tools::Shell::TOOLS.map(&:new) +
|
|
40
|
+
self.configuration.tools = Ask::Tools::Shell::TOOLS.map(&:new) + core_rails_tools + discovered_user_tools
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Prune old sessions and audit logs based on configuration limits.
|
|
44
|
+
#
|
|
45
|
+
# Removes sessions older than +max_session_age+ seconds, and limits the
|
|
46
|
+
# total number of sessions to +max_sessions+ (deleting the oldest first).
|
|
47
|
+
# Audit log entries older than the oldest kept session are also removed.
|
|
48
|
+
#
|
|
49
|
+
# Call manually via rake task or cron, or configure limits to auto-prune
|
|
50
|
+
# on agent_session creation.
|
|
51
|
+
def cleanup!
|
|
52
|
+
prune_old_sessions
|
|
53
|
+
limit_session_count
|
|
34
54
|
end
|
|
35
55
|
|
|
36
56
|
def root
|
|
@@ -39,7 +59,76 @@ module Ask
|
|
|
39
59
|
|
|
40
60
|
private
|
|
41
61
|
|
|
42
|
-
def
|
|
62
|
+
def build_environment_hooks
|
|
63
|
+
env_mode = configuration.effective_mode
|
|
64
|
+
return {} unless env_mode
|
|
65
|
+
|
|
66
|
+
perms = Ask::Agent::Extensions::Permissions.new(mode: env_mode)
|
|
67
|
+
{ before_tool: [perms.method(:before_tool_call)] }
|
|
68
|
+
rescue ArgumentError => e
|
|
69
|
+
warn "[ask-rails] Invalid environment mode: #{e.message}"
|
|
70
|
+
{}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def prune_old_sessions
|
|
74
|
+
age = configuration.max_session_age
|
|
75
|
+
return unless age&.> 0
|
|
76
|
+
return unless persistence_available?
|
|
77
|
+
|
|
78
|
+
cutoff = age.seconds.ago
|
|
79
|
+
count = 0
|
|
80
|
+
|
|
81
|
+
configuration.persistence_adapter.list.each do |id|
|
|
82
|
+
data = configuration.persistence_adapter.load(id)
|
|
83
|
+
created = data&.dig(:metadata, :created_at)
|
|
84
|
+
if created && Time.parse(created) < cutoff
|
|
85
|
+
configuration.persistence_adapter.delete(id)
|
|
86
|
+
count += 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
count
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def limit_session_count
|
|
96
|
+
max = configuration.max_sessions
|
|
97
|
+
return unless max&.> 0
|
|
98
|
+
return unless persistence_available?
|
|
99
|
+
|
|
100
|
+
sessions = configuration.persistence_adapter.list
|
|
101
|
+
excess = sessions.size - max
|
|
102
|
+
return unless excess > 0
|
|
103
|
+
|
|
104
|
+
# Delete oldest sessions first
|
|
105
|
+
with_timestamps = sessions.map { |id|
|
|
106
|
+
data = configuration.persistence_adapter.load(id)
|
|
107
|
+
created = data&.dig(:metadata, :created_at)
|
|
108
|
+
[id, created ? Time.parse(created) : Time.at(0)]
|
|
109
|
+
}.sort_by(&:last)
|
|
110
|
+
|
|
111
|
+
with_timestamps.first(excess).each do |id, _|
|
|
112
|
+
configuration.persistence_adapter.delete(id)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
excess
|
|
116
|
+
rescue StandardError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def persistence_available?
|
|
121
|
+
defined?(ActiveRecord::Base) &&
|
|
122
|
+
ActiveRecord::Base.connection.data_source_exists?("ask_sessions")
|
|
123
|
+
rescue StandardError
|
|
124
|
+
false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def core_rails_tools
|
|
128
|
+
CORE_RAILS_TOOLS.map(&:new)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def discovered_user_tools
|
|
43
132
|
tools = []
|
|
44
133
|
files = Dir[::Rails.root.join("app", "tools", "*.rb")]
|
|
45
134
|
files.each do |f|
|
|
@@ -67,7 +156,9 @@ end
|
|
|
67
156
|
require_relative "rails/version"
|
|
68
157
|
require_relative "rails/engine"
|
|
69
158
|
require_relative "rails/configuration"
|
|
70
|
-
require_relative "rails/
|
|
159
|
+
require_relative "rails/audit_log"
|
|
160
|
+
require_relative "rails/environment_permissions"
|
|
161
|
+
require_relative "rails/auth"
|
|
71
162
|
require_relative "rails/persistence"
|
|
72
163
|
require_relative "rails/service_discovery"
|
|
73
164
|
require_relative "rails/tool"
|
|
@@ -78,3 +169,19 @@ require_relative "rails/tools/read_routes"
|
|
|
78
169
|
require_relative "rails/tools/query_database"
|
|
79
170
|
require_relative "rails/tools/read_model"
|
|
80
171
|
require_relative "rails/tools/read_log"
|
|
172
|
+
require_relative "rails/tools/schema_graph"
|
|
173
|
+
require_relative "rails/tools/route_inspector"
|
|
174
|
+
|
|
175
|
+
# Railtie is loaded only when Rails is fully available
|
|
176
|
+
if defined?(::Rails::Railtie)
|
|
177
|
+
require_relative "rails/railtie"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Define after all tool files are loaded so the constants resolve
|
|
181
|
+
Ask::Rails::CORE_RAILS_TOOLS = [
|
|
182
|
+
Ask::Rails::Tools::ReadFile, Ask::Rails::Tools::RunCommand,
|
|
183
|
+
Ask::Rails::Tools::SearchCodebase, Ask::Rails::Tools::ReadRoutes,
|
|
184
|
+
Ask::Rails::Tools::QueryDatabase, Ask::Rails::Tools::ReadModel,
|
|
185
|
+
Ask::Rails::Tools::ReadLog, Ask::Rails::Tools::SchemaGraph,
|
|
186
|
+
Ask::Rails::Tools::RouteInspector
|
|
187
|
+
].freeze
|
|
@@ -19,6 +19,11 @@ module Ask
|
|
|
19
19
|
template "migration.rb", "db/migrate/#{timestamp}_create_ask_sessions.rb"
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
def create_audit_log_migration
|
|
23
|
+
timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
|
|
24
|
+
template "audit_log_migration.rb", "db/migrate/#{timestamp}_create_ask_audit_logs.rb"
|
|
25
|
+
end
|
|
26
|
+
|
|
22
27
|
def create_tools_directory
|
|
23
28
|
empty_directory "app/tools"
|
|
24
29
|
create_file "app/tools/.keep", ""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateAskAuditLogs < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :ask_audit_logs do |t|
|
|
4
|
+
t.string :session_id, null: false
|
|
5
|
+
t.string :tool_name, null: false
|
|
6
|
+
t.jsonb :params
|
|
7
|
+
t.jsonb :result_summary
|
|
8
|
+
t.string :status, null: false, default: "success"
|
|
9
|
+
t.text :error_message
|
|
10
|
+
t.integer :duration_ms
|
|
11
|
+
t.jsonb :user_context
|
|
12
|
+
t.string :environment
|
|
13
|
+
t.datetime :recorded_at, null: false
|
|
14
|
+
|
|
15
|
+
t.timestamps
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
add_index :ask_audit_logs, :session_id
|
|
19
|
+
add_index :ask_audit_logs, [:recorded_at, :tool_name]
|
|
20
|
+
add_index :ask_audit_logs, :status
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Ask::Rails.configure do |config|
|
|
2
2
|
config.default_model = ENV.fetch("ASK_DEFAULT_MODEL", "gpt-4o")
|
|
3
3
|
config.max_turns = ENV.fetch("ASK_MAX_TURNS", 25).to_i
|
|
4
|
-
|
|
4
|
+
config.persistence_adapter = Ask::Rails::Persistence.new
|
|
5
5
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ask-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -146,10 +146,18 @@ files:
|
|
|
146
146
|
- CHANGELOG.md
|
|
147
147
|
- LICENSE
|
|
148
148
|
- README.md
|
|
149
|
+
- app/controllers/ask/rails/chat_controller.rb
|
|
150
|
+
- app/models/ask/rails/session.rb
|
|
151
|
+
- app/views/ask/rails/chat/index.html.erb
|
|
152
|
+
- app/views/layouts/ask/rails/application.html.erb
|
|
153
|
+
- config/routes.rb
|
|
149
154
|
- lib/ask-rails.rb
|
|
150
155
|
- lib/ask/rails.rb
|
|
156
|
+
- lib/ask/rails/audit_log.rb
|
|
157
|
+
- lib/ask/rails/auth.rb
|
|
151
158
|
- lib/ask/rails/configuration.rb
|
|
152
159
|
- lib/ask/rails/engine.rb
|
|
160
|
+
- lib/ask/rails/environment_permissions.rb
|
|
153
161
|
- lib/ask/rails/persistence.rb
|
|
154
162
|
- lib/ask/rails/railtie.rb
|
|
155
163
|
- lib/ask/rails/service_discovery.rb
|
|
@@ -159,13 +167,16 @@ files:
|
|
|
159
167
|
- lib/ask/rails/tools/read_log.rb
|
|
160
168
|
- lib/ask/rails/tools/read_model.rb
|
|
161
169
|
- lib/ask/rails/tools/read_routes.rb
|
|
170
|
+
- lib/ask/rails/tools/route_inspector.rb
|
|
162
171
|
- lib/ask/rails/tools/run_command.rb
|
|
172
|
+
- lib/ask/rails/tools/schema_graph.rb
|
|
163
173
|
- lib/ask/rails/tools/search_codebase.rb
|
|
164
174
|
- lib/ask/rails/version.rb
|
|
165
175
|
- lib/ask/skills/rails.db_debug/SKILL.md
|
|
166
176
|
- lib/ask/skills/rails.deploy_pipeline/SKILL.md
|
|
167
177
|
- lib/ask/skills/rails.route_trouble/SKILL.md
|
|
168
178
|
- lib/generators/ask/rails/install/install_generator.rb
|
|
179
|
+
- lib/generators/ask/rails/install/templates/audit_log_migration.rb
|
|
169
180
|
- lib/generators/ask/rails/install/templates/initializer.rb
|
|
170
181
|
- lib/generators/ask/rails/install/templates/migration.rb
|
|
171
182
|
homepage: https://github.com/ask-rb/ask-rails
|