tsykvas_rails_template 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +200 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +589 -0
- data/Rakefile +17 -0
- data/lib/generators/tsykvas_rails_template/companions/companions_generator.rb +273 -0
- data/lib/generators/tsykvas_rails_template/concept/concept_generator.rb +145 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/index.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/index.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/new.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/new.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/show.html.slim.tt +4 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/show.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/controller.rb.tt +45 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/create.rb.tt +31 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/destroy.rb.tt +13 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/edit.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/index.rb.tt +9 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/new.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/show.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/update.rb.tt +31 -0
- data/lib/generators/tsykvas_rails_template/install/bootstrap_installer.rb +225 -0
- data/lib/generators/tsykvas_rails_template/install/install_generator.rb +298 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/buddy.md +157 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/code-reviewer.md +117 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/security-reviewer.md +113 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/tech-lead.md +150 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/check.md +51 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/code-review.md +60 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/docs-create.md +102 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pr-review.md +81 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pushit.md +160 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/refactor.md +132 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/task-sum.md +47 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tests.md +67 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tsykvas-claude.md +262 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-docs.md +78 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-rules.md +102 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-tests.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/architecture.md +315 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/authentication.md +96 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/background-jobs.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/code-style.md +101 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/commands.md +34 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/companions.md +128 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/concepts-refactoring.md +194 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/database.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/deployment.md +138 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/design-system.md +322 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/documentation.md +89 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/forms.md +174 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/i18n.md +165 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/routing-and-namespaces.md +114 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/security.md +122 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/stimulus-controllers.md +166 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing-examples.md +180 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing.md +117 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md +280 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/ui-components.md +196 -0
- data/lib/generators/tsykvas_rails_template/install/templates/CLAUDE.md.tt +81 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/component/base.rb +6 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb +124 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb +56 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.html.slim +49 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.rb +11 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb +17 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/concerns/operations_methods.rb +148 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/home_controller.rb +10 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/policies/application_policy.rb +33 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/policies/home_policy.rb +8 -0
- data/lib/tasks/tsykvas.rake +11 -0
- data/lib/tsykvas_rails_template/probe.rb +236 -0
- data/lib/tsykvas_rails_template/railtie.rb +13 -0
- data/lib/tsykvas_rails_template/version.rb +5 -0
- data/lib/tsykvas_rails_template.rb +18 -0
- metadata +183 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TsykvasRailsTemplate
|
|
4
|
+
module Generators
|
|
5
|
+
# Bootstrap install steps for InstallGenerator. Lives in its own module so
|
|
6
|
+
# the generator stays under the class-length limit and the Bootstrap-only
|
|
7
|
+
# logic is grouped together for opt-out (`--skip-bootstrap`).
|
|
8
|
+
#
|
|
9
|
+
# Expects the host to provide: `say_status`, `gem`, `create_file`,
|
|
10
|
+
# `empty_directory`, `append_to_file`, `in_root`, `destination_root`, `run`.
|
|
11
|
+
# All of those are Thor::Group / Rails::Generators::Base helpers.
|
|
12
|
+
module BootstrapInstaller
|
|
13
|
+
BOOTSTRAP_SCSS_ENTRY_PATH = "app/assets/stylesheets/application.bootstrap.scss"
|
|
14
|
+
BOOTSTRAP_SCSS_ENTRY_BODY = <<~SCSS
|
|
15
|
+
// tsykvas_rails_template — Bootstrap entrypoint compiled by dartsass-rails
|
|
16
|
+
// into app/assets/builds/application.css. Override $variables above the
|
|
17
|
+
// import to theme; add component partials below.
|
|
18
|
+
@import "bootstrap";
|
|
19
|
+
SCSS
|
|
20
|
+
|
|
21
|
+
DARTSASS_INITIALIZER_PATH = "config/initializers/dartsass.rb"
|
|
22
|
+
# `build_options` is consumed by dartsass-rails as Array#flat_map(&:split),
|
|
23
|
+
# so it MUST be an Array (a String would NoMethodError at build time).
|
|
24
|
+
# We extend dartsass-rails' own defaults (compressed + no-source-map):
|
|
25
|
+
# --quiet-deps silences warnings from gem load paths
|
|
26
|
+
# (Bootstrap 5.3.x's internal noise:
|
|
27
|
+
# @import / red() / mix() — 311 warns)
|
|
28
|
+
# --silence-deprecation=import silences the lone warning on the
|
|
29
|
+
# `@import "bootstrap"` in our own
|
|
30
|
+
# application.bootstrap.scss
|
|
31
|
+
# (Bootstrap 5.3 has no @use replacement)
|
|
32
|
+
DARTSASS_MANAGED_HEADER = "# Managed by tsykvas_rails_template:install — re-running install rewrites this file."
|
|
33
|
+
DARTSASS_INITIALIZER_BODY = <<~RUBY.freeze
|
|
34
|
+
# frozen_string_literal: true
|
|
35
|
+
#{DARTSASS_MANAGED_HEADER}
|
|
36
|
+
|
|
37
|
+
# tsykvas_rails_template — dartsass-rails build map.
|
|
38
|
+
# Keys are sources under app/assets/stylesheets, values are outputs
|
|
39
|
+
# under app/assets/builds (which Propshaft serves).
|
|
40
|
+
Rails.application.config.dartsass.builds = {
|
|
41
|
+
"application.bootstrap.scss" => "application.css"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Rails.application.config.dartsass.build_options = [
|
|
45
|
+
"--style=compressed",
|
|
46
|
+
"--no-source-map",
|
|
47
|
+
"--quiet-deps",
|
|
48
|
+
"--silence-deprecation=import"
|
|
49
|
+
]
|
|
50
|
+
RUBY
|
|
51
|
+
|
|
52
|
+
IMPORTMAP_PINS = <<~RUBY
|
|
53
|
+
pin "bootstrap", to: "https://ga.jspm.io/npm:bootstrap@5.3.3/dist/js/bootstrap.esm.js", preload: true
|
|
54
|
+
pin "@popperjs/core", to: "https://ga.jspm.io/npm:@popperjs/core@2.11.8/lib/index.js", preload: true
|
|
55
|
+
RUBY
|
|
56
|
+
|
|
57
|
+
APPLICATION_JS_PATH = "app/javascript/application.js"
|
|
58
|
+
APPLICATION_JS_BOOTSTRAP_BLOCK = <<~JS
|
|
59
|
+
|
|
60
|
+
// Bootstrap (added by tsykvas_rails_template:install) — exposed globally
|
|
61
|
+
// so OperationsMethods' format.js modal-dismiss path can call it.
|
|
62
|
+
import * as bootstrap from "bootstrap"
|
|
63
|
+
window.bootstrap = bootstrap
|
|
64
|
+
JS
|
|
65
|
+
|
|
66
|
+
PROCFILE_DEV_PATH = "Procfile.dev"
|
|
67
|
+
PROCFILE_DEV_BODY = <<~PROC
|
|
68
|
+
web: bin/rails server
|
|
69
|
+
css: bin/rails dartsass:watch
|
|
70
|
+
PROC
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def install_bootstrap_steps
|
|
75
|
+
add_bootstrap_gems
|
|
76
|
+
run_bundle_install_for_bootstrap
|
|
77
|
+
bundle_update_bootstrap_gems
|
|
78
|
+
write_bootstrap_scss_entry
|
|
79
|
+
ensure_assets_builds_directory
|
|
80
|
+
write_dartsass_initializer
|
|
81
|
+
pin_bootstrap_via_importmap
|
|
82
|
+
wire_application_js_bootstrap_import
|
|
83
|
+
write_procfile_dev
|
|
84
|
+
ensure_foreman_installed
|
|
85
|
+
precompile_bootstrap_css
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_bootstrap_gems
|
|
89
|
+
add_gem_if_missing("bootstrap", "~> 5.3")
|
|
90
|
+
add_gem_if_missing("dartsass-rails")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def gemfile_includes?(name)
|
|
94
|
+
gemfile = destination_path("Gemfile")
|
|
95
|
+
return false unless File.exist?(gemfile)
|
|
96
|
+
|
|
97
|
+
File.read(gemfile).match?(/^\s*gem\s+['"]#{Regexp.escape(name)}['"]/)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def add_gem_if_missing(name, *args)
|
|
101
|
+
if gemfile_includes?(name)
|
|
102
|
+
say_status :exist, "gem '#{name}' already in Gemfile", :blue
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
gem(name, *args)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def run_bundle_install_for_bootstrap
|
|
110
|
+
in_root do
|
|
111
|
+
Bundler.with_unbundled_env { run "bundle install" }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Re-runs of install on existing hosts may have older bootstrap /
|
|
116
|
+
# dartsass-rails versions locked in Gemfile.lock. `bundle install` is a
|
|
117
|
+
# no-op there. `bundle update` bumps to the latest within the Gemfile
|
|
118
|
+
# constraints (e.g. latest 5.3.x for `~> 5.3`).
|
|
119
|
+
def bundle_update_bootstrap_gems
|
|
120
|
+
in_root do
|
|
121
|
+
Bundler.with_unbundled_env { run "bundle update bootstrap dartsass-rails" }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def write_bootstrap_scss_entry
|
|
126
|
+
path = destination_path(BOOTSTRAP_SCSS_ENTRY_PATH)
|
|
127
|
+
if File.exist?(path)
|
|
128
|
+
say_status :exist, BOOTSTRAP_SCSS_ENTRY_PATH, :blue
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
empty_directory File.dirname(BOOTSTRAP_SCSS_ENTRY_PATH) unless File.directory?(File.dirname(path))
|
|
133
|
+
create_file BOOTSTRAP_SCSS_ENTRY_PATH, BOOTSTRAP_SCSS_ENTRY_BODY
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ensure_assets_builds_directory
|
|
137
|
+
keep = "app/assets/builds/.keep"
|
|
138
|
+
return if File.exist?(destination_path(keep))
|
|
139
|
+
|
|
140
|
+
empty_directory "app/assets/builds" unless File.directory?(destination_path("app/assets/builds"))
|
|
141
|
+
create_file keep, ""
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def write_dartsass_initializer
|
|
145
|
+
path = destination_path(DARTSASS_INITIALIZER_PATH)
|
|
146
|
+
unless File.exist?(path)
|
|
147
|
+
create_file DARTSASS_INITIALIZER_PATH, DARTSASS_INITIALIZER_BODY
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
contents = File.read(path)
|
|
152
|
+
if contents == DARTSASS_INITIALIZER_BODY
|
|
153
|
+
say_status :exist, DARTSASS_INITIALIZER_PATH, :blue
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Re-running install rewrites the initializer to its canonical form.
|
|
158
|
+
# The "managed" header marks this file as gem-owned so users know edits
|
|
159
|
+
# don't survive — customise dartsass options elsewhere if you need to.
|
|
160
|
+
File.write(path, DARTSASS_INITIALIZER_BODY)
|
|
161
|
+
if contents.include?(DARTSASS_MANAGED_HEADER)
|
|
162
|
+
say_status :update, "#{DARTSASS_INITIALIZER_PATH} (managed)", :green
|
|
163
|
+
else
|
|
164
|
+
say_status :overwrite,
|
|
165
|
+
"#{DARTSASS_INITIALIZER_PATH} (was hand-edited; superseded by canonical form)",
|
|
166
|
+
:yellow
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def pin_bootstrap_via_importmap
|
|
171
|
+
importmap = destination_path("config/importmap.rb")
|
|
172
|
+
return unless File.exist?(importmap)
|
|
173
|
+
return if File.read(importmap).include?(%(pin "bootstrap"))
|
|
174
|
+
|
|
175
|
+
append_to_file "config/importmap.rb",
|
|
176
|
+
"\n# Bootstrap (added by tsykvas_rails_template:install)\n#{IMPORTMAP_PINS}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def wire_application_js_bootstrap_import
|
|
180
|
+
path = destination_path(APPLICATION_JS_PATH)
|
|
181
|
+
return unless File.exist?(path)
|
|
182
|
+
return if File.read(path).include?('import * as bootstrap from "bootstrap"')
|
|
183
|
+
|
|
184
|
+
append_to_file APPLICATION_JS_PATH, APPLICATION_JS_BOOTSTRAP_BLOCK
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def write_procfile_dev
|
|
188
|
+
path = destination_path(PROCFILE_DEV_PATH)
|
|
189
|
+
if File.exist?(path)
|
|
190
|
+
return if File.read(path).include?("dartsass:watch")
|
|
191
|
+
|
|
192
|
+
append_to_file PROCFILE_DEV_PATH, "css: bin/rails dartsass:watch\n"
|
|
193
|
+
else
|
|
194
|
+
create_file PROCFILE_DEV_PATH, PROCFILE_DEV_BODY
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def precompile_bootstrap_css
|
|
199
|
+
in_root do
|
|
200
|
+
Bundler.with_unbundled_env { run "bin/rails dartsass:build" }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Foreman is required for `bin/dev` (which reads Procfile.dev) but its
|
|
205
|
+
# README explicitly says don't add it to Gemfile — install it system-wide.
|
|
206
|
+
# Idempotent: skip when already on PATH.
|
|
207
|
+
def ensure_foreman_installed
|
|
208
|
+
if foreman_already_installed?
|
|
209
|
+
say_status :exist, "foreman already installed system-wide", :blue
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
in_root do
|
|
214
|
+
Bundler.with_unbundled_env { run "gem install foreman --no-document" }
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def foreman_already_installed?
|
|
219
|
+
Bundler.with_unbundled_env do
|
|
220
|
+
system("gem list -i foreman > /dev/null 2>&1")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require_relative "bootstrap_installer"
|
|
5
|
+
|
|
6
|
+
module TsykvasRailsTemplate
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
+
include BootstrapInstaller
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc <<~DESC
|
|
14
|
+
Install the tsykvas_rails_template skeleton:
|
|
15
|
+
- copies app/concepts/base/{operation,component}/* into the host
|
|
16
|
+
- copies the OperationsMethods controller concern (the `endpoint` DSL)
|
|
17
|
+
- registers app/concepts in config.autoload_paths
|
|
18
|
+
- wires Pundit::Authorization + OperationsMethods into ApplicationController
|
|
19
|
+
- generates ApplicationPolicy if missing
|
|
20
|
+
- scaffolds a Home example concept (controller + operation + component
|
|
21
|
+
+ HomePolicy + root route) showing the canonical one-liner pattern
|
|
22
|
+
- drops .claude/{agents,commands,docs}/* and a CLAUDE.md scaffold
|
|
23
|
+
DESC
|
|
24
|
+
|
|
25
|
+
class_option :skip_application_policy,
|
|
26
|
+
type: :boolean,
|
|
27
|
+
default: false,
|
|
28
|
+
desc: "Don't generate ApplicationPolicy if it doesn't exist"
|
|
29
|
+
|
|
30
|
+
class_option :skip_autoload_paths,
|
|
31
|
+
type: :boolean,
|
|
32
|
+
default: false,
|
|
33
|
+
desc: "Don't patch config/application.rb"
|
|
34
|
+
|
|
35
|
+
class_option :skip_claude,
|
|
36
|
+
type: :boolean,
|
|
37
|
+
default: false,
|
|
38
|
+
desc: "Don't drop .claude/ payload or CLAUDE.md"
|
|
39
|
+
|
|
40
|
+
class_option :skip_home_example,
|
|
41
|
+
type: :boolean,
|
|
42
|
+
default: false,
|
|
43
|
+
desc: "Don't scaffold the Home example concept + root route"
|
|
44
|
+
|
|
45
|
+
class_option :keep_sqlite,
|
|
46
|
+
type: :boolean,
|
|
47
|
+
default: false,
|
|
48
|
+
desc: "Don't swap sqlite3 for pg in the Gemfile (default: always swap to PostgreSQL)"
|
|
49
|
+
|
|
50
|
+
class_option :skip_bootstrap,
|
|
51
|
+
type: :boolean,
|
|
52
|
+
default: false,
|
|
53
|
+
desc: "Don't install bootstrap + dartsass-rails or wire the SCSS / importmap pins"
|
|
54
|
+
|
|
55
|
+
def swap_database_to_postgresql
|
|
56
|
+
return if options[:keep_sqlite]
|
|
57
|
+
|
|
58
|
+
swap_gemfile_to_pg
|
|
59
|
+
swap_database_yml_to_pg
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def copy_concepts_base
|
|
63
|
+
directory "app/concepts/base", "app/concepts/base"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def copy_operations_methods_concern
|
|
67
|
+
copy_file "app/controllers/concerns/operations_methods.rb",
|
|
68
|
+
"app/controllers/concerns/operations_methods.rb"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_concepts_to_autoload_paths
|
|
72
|
+
return if options[:skip_autoload_paths]
|
|
73
|
+
|
|
74
|
+
target = destination_path("config/application.rb")
|
|
75
|
+
return unless File.exist?(target)
|
|
76
|
+
|
|
77
|
+
marker = "app/concepts"
|
|
78
|
+
contents = File.read(target)
|
|
79
|
+
if contents.include?("#{marker}]") || contents.include?("#{marker}\"]")
|
|
80
|
+
say_status :exist, "config.autoload_paths already includes #{marker}", :blue
|
|
81
|
+
return
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
application "config.autoload_paths += %W[\#{config.root}/app/concepts]\n"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def wire_application_controller
|
|
88
|
+
target_rel = "app/controllers/application_controller.rb"
|
|
89
|
+
target_abs = destination_path(target_rel)
|
|
90
|
+
return unless File.exist?(target_abs)
|
|
91
|
+
|
|
92
|
+
contents = File.read(target_abs)
|
|
93
|
+
|
|
94
|
+
unless contents.include?("Pundit::Authorization") || contents.include?("include Pundit\n")
|
|
95
|
+
inject_into_class target_rel, "ApplicationController", " include Pundit::Authorization\n"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
return if contents.include?("OperationsMethods")
|
|
99
|
+
|
|
100
|
+
inject_into_class target_rel, "ApplicationController", " include OperationsMethods\n"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def create_application_policy
|
|
104
|
+
return if options[:skip_application_policy]
|
|
105
|
+
return if File.exist?(destination_path("app/policies/application_policy.rb"))
|
|
106
|
+
|
|
107
|
+
empty_directory "app/policies" unless File.directory?(destination_path("app/policies"))
|
|
108
|
+
copy_file "app/policies/application_policy.rb",
|
|
109
|
+
"app/policies/application_policy.rb"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def generate_home_example
|
|
113
|
+
return if options[:skip_home_example]
|
|
114
|
+
return if File.exist?(destination_path("app/controllers/home_controller.rb"))
|
|
115
|
+
return if File.directory?(destination_path("app/concepts/home"))
|
|
116
|
+
|
|
117
|
+
copy_file "app/controllers/home_controller.rb",
|
|
118
|
+
"app/controllers/home_controller.rb"
|
|
119
|
+
directory "app/concepts/home", "app/concepts/home"
|
|
120
|
+
|
|
121
|
+
empty_directory "app/policies" unless File.directory?(destination_path("app/policies"))
|
|
122
|
+
return if File.exist?(destination_path("app/policies/home_policy.rb"))
|
|
123
|
+
|
|
124
|
+
copy_file "app/policies/home_policy.rb", "app/policies/home_policy.rb"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def add_root_route
|
|
128
|
+
return if options[:skip_home_example]
|
|
129
|
+
|
|
130
|
+
routes_path = destination_path("config/routes.rb")
|
|
131
|
+
return unless File.exist?(routes_path)
|
|
132
|
+
return if File.read(routes_path).match?(/^\s*root\s/)
|
|
133
|
+
|
|
134
|
+
route 'root "home#index"'
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def install_bootstrap
|
|
138
|
+
return if options[:skip_bootstrap]
|
|
139
|
+
|
|
140
|
+
install_bootstrap_steps
|
|
141
|
+
|
|
142
|
+
say_status :bootstrap,
|
|
143
|
+
"Bootstrap 5.3 + dartsass-rails wired. Run `bin/dev` for the live SCSS watcher, " \
|
|
144
|
+
"or `bin/rails dartsass:build` once before `bin/rails server`. " \
|
|
145
|
+
"Pass --skip-bootstrap to opt out.",
|
|
146
|
+
:green
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Propshaft serves from `public/assets/` whenever a `.manifest.json` is
|
|
150
|
+
# there, completely bypassing live `app/assets/builds/` and
|
|
151
|
+
# `app/javascript/`. A stale dir from a prior `rails assets:precompile`
|
|
152
|
+
# silently freezes the dev environment. Clean it here — but only if
|
|
153
|
+
# `.gitignore` lists it (so we know it's a transient cache, not
|
|
154
|
+
# checked-in content). `rails new` adds `/public/assets` by default.
|
|
155
|
+
def clean_propshaft_cache
|
|
156
|
+
require "fileutils"
|
|
157
|
+
|
|
158
|
+
cache_dir = destination_path("public/assets")
|
|
159
|
+
return unless File.directory?(cache_dir)
|
|
160
|
+
|
|
161
|
+
gitignore = destination_path(".gitignore")
|
|
162
|
+
listed = File.exist?(gitignore) && File.read(gitignore).match?(%r{^/?public/assets\b})
|
|
163
|
+
unless listed
|
|
164
|
+
say_status :skip,
|
|
165
|
+
"public/assets/ exists but is not in .gitignore — leaving alone " \
|
|
166
|
+
"(it might be checked-in content rather than a Propshaft cache).",
|
|
167
|
+
:yellow
|
|
168
|
+
return
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
FileUtils.rm_rf(cache_dir)
|
|
172
|
+
say_status :clean,
|
|
173
|
+
"Removed stale public/assets/ (Propshaft would otherwise shadow live " \
|
|
174
|
+
"app/assets/builds/ and app/javascript/ in dev).",
|
|
175
|
+
:green
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
SHIPPED_DOCS = %w[
|
|
179
|
+
architecture
|
|
180
|
+
authentication
|
|
181
|
+
background-jobs
|
|
182
|
+
code-style
|
|
183
|
+
commands
|
|
184
|
+
companions
|
|
185
|
+
concepts-refactoring
|
|
186
|
+
database
|
|
187
|
+
deployment
|
|
188
|
+
design-system
|
|
189
|
+
documentation
|
|
190
|
+
forms
|
|
191
|
+
i18n
|
|
192
|
+
routing-and-namespaces
|
|
193
|
+
security
|
|
194
|
+
stimulus-controllers
|
|
195
|
+
testing
|
|
196
|
+
testing-examples
|
|
197
|
+
tsykvas_rails_template
|
|
198
|
+
ui-components
|
|
199
|
+
].freeze
|
|
200
|
+
|
|
201
|
+
def copy_claude_payload
|
|
202
|
+
return if options[:skip_claude]
|
|
203
|
+
|
|
204
|
+
directory ".claude/agents", ".claude/agents"
|
|
205
|
+
directory ".claude/commands", ".claude/commands"
|
|
206
|
+
empty_directory ".claude/docs" unless File.directory?(destination_path(".claude/docs"))
|
|
207
|
+
SHIPPED_DOCS.each do |name|
|
|
208
|
+
copy_file ".claude/docs/#{name}.md", ".claude/docs/#{name}.md"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def write_claude_md
|
|
213
|
+
return if options[:skip_claude]
|
|
214
|
+
|
|
215
|
+
if File.exist?(destination_path("CLAUDE.md"))
|
|
216
|
+
say_status :skip,
|
|
217
|
+
"CLAUDE.md already exists; not overwriting. " \
|
|
218
|
+
"Run `/tsykvas-claude` in Claude Code to integrate the gem's " \
|
|
219
|
+
"must-know-rules and routing table inside fence markers without " \
|
|
220
|
+
"touching your existing content.",
|
|
221
|
+
:yellow
|
|
222
|
+
return
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
template "CLAUDE.md.tt", "CLAUDE.md"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def announce_completion
|
|
229
|
+
say ""
|
|
230
|
+
say " tsykvas_rails_template installed.", :green
|
|
231
|
+
say " A working Home example landed at /app/concepts/home/ with"
|
|
232
|
+
say " `root \"home#index\"` already in routes — start `bin/rails server`"
|
|
233
|
+
say " and visit http://localhost:3000 to see it (unless you passed"
|
|
234
|
+
say " --skip-home-example)."
|
|
235
|
+
say ""
|
|
236
|
+
say " Next steps:"
|
|
237
|
+
say " 1. rails g tsykvas_rails_template:companions"
|
|
238
|
+
say " (adds devise / simple_form / rspec stack / mini_magick / etc."
|
|
239
|
+
say " and runs their :install sub-generators)"
|
|
240
|
+
say " 2. rails g tsykvas_rails_template:concept <Name> [--controller]"
|
|
241
|
+
say " scaffolds your domain concepts."
|
|
242
|
+
say " 3. Open Claude Code and run /tsykvas-claude to refresh"
|
|
243
|
+
say " probe-driven sections in CLAUDE.md + .claude/docs/"
|
|
244
|
+
say " (concept folders, gem versions, branch names) when"
|
|
245
|
+
say " your stack changes."
|
|
246
|
+
say ""
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def destination_path(rel)
|
|
252
|
+
File.join(destination_root, rel)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def swap_gemfile_to_pg
|
|
256
|
+
gemfile = destination_path("Gemfile")
|
|
257
|
+
return unless File.exist?(gemfile)
|
|
258
|
+
|
|
259
|
+
contents = File.read(gemfile)
|
|
260
|
+
return if contents.match?(/^\s*gem\s+['"]pg['"]/)
|
|
261
|
+
|
|
262
|
+
if contents.match?(/^\s*gem\s+['"]sqlite3['"]/)
|
|
263
|
+
gsub_file "Gemfile",
|
|
264
|
+
/^(\s*)gem\s+['"]sqlite3['"][^\n]*$/,
|
|
265
|
+
"\\1# gem \"sqlite3\" — replaced by tsykvas_rails_template:install\n\\1gem \"pg\""
|
|
266
|
+
else
|
|
267
|
+
append_to_file "Gemfile", %(\ngem "pg"\n)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def swap_database_yml_to_pg
|
|
272
|
+
yml_path = destination_path("config/database.yml")
|
|
273
|
+
return unless File.exist?(yml_path)
|
|
274
|
+
|
|
275
|
+
contents = File.read(yml_path)
|
|
276
|
+
return unless contents.match?(/adapter:\s*sqlite3/)
|
|
277
|
+
|
|
278
|
+
# adapter: sqlite3 → adapter: postgresql (+ encoding: unicode)
|
|
279
|
+
gsub_file "config/database.yml",
|
|
280
|
+
/^(\s*)adapter:\s*sqlite3\s*$/,
|
|
281
|
+
"\\1adapter: postgresql\n\\1encoding: unicode"
|
|
282
|
+
# Drop sqlite-only `timeout:` lines.
|
|
283
|
+
gsub_file "config/database.yml", /^\s*timeout:\s*\d+\s*\n/, ""
|
|
284
|
+
# storage/<env>.sqlite3 → <app_name>_<env>
|
|
285
|
+
app = File.basename(destination_root).tr("-", "_")
|
|
286
|
+
gsub_file "config/database.yml",
|
|
287
|
+
%r{database:\s*storage/(\w+)\.sqlite3},
|
|
288
|
+
"database: #{app}_\\1"
|
|
289
|
+
|
|
290
|
+
say_status :db,
|
|
291
|
+
"Swapped sqlite3 → pg in Gemfile + config/database.yml. " \
|
|
292
|
+
"Run `bundle install && bin/rails db:create` after install completes. " \
|
|
293
|
+
"Pass --keep-sqlite to skip the swap.",
|
|
294
|
+
:yellow
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: buddy
|
|
3
|
+
description: Feature planning partner. Use when brainstorming, designing, or planning a new feature. Asks probing questions, researches solutions, and produces a feature_plan.md ready for implementation.
|
|
4
|
+
tools: Read, Grep, Glob, Bash, WebSearch, WebFetch
|
|
5
|
+
model: opus
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a senior Rails developer and product thinker. Your role is to plan features collaboratively with the user through conversation. You NEVER implement code — you only produce a plan.
|
|
9
|
+
|
|
10
|
+
## What you know
|
|
11
|
+
|
|
12
|
+
You are deeply familiar with this project's architecture. Before starting, read these docs to understand how everything is built:
|
|
13
|
+
|
|
14
|
+
- `.claude/docs/architecture.md` — Concepts Pattern, Operation/Component, `endpoint`, `api_endpoint`, Pundit
|
|
15
|
+
- `.claude/docs/concepts-refactoring.md` — Refactoring guide and end-to-end Operation/Component examples
|
|
16
|
+
- `.claude/docs/api-endpoints.md` — Building Api::V1::* endpoints with params adapters
|
|
17
|
+
- `.claude/docs/ui-components.md` — `Base::Component::Base`, `Base::Component::Table`, modals, helpers
|
|
18
|
+
- `.claude/docs/stimulus-controllers.md` — Stimulus conventions, listener cleanup
|
|
19
|
+
- `.claude/docs/testing.md` + `.claude/docs/testing-examples.md` — RSpec patterns, the operation shared context, what NOT to test
|
|
20
|
+
- `.claude/docs/code-style.md` — Ruby/Rails style, I18n keys, locales
|
|
21
|
+
- `CLAUDE.md` — top-level project conventions
|
|
22
|
+
|
|
23
|
+
The detailed legacy guides also live in `.cursor/rules/*.mdc` — read them when relevant.
|
|
24
|
+
|
|
25
|
+
## How you work
|
|
26
|
+
|
|
27
|
+
Guide the conversation through 4 phases. Don't rush — each phase matters.
|
|
28
|
+
|
|
29
|
+
### Phase 1: Understand the goal
|
|
30
|
+
|
|
31
|
+
Ask questions to understand WHAT and WHY:
|
|
32
|
+
- What does this feature do from the user's perspective?
|
|
33
|
+
- Who can access it? (account members, admins, API consumers, scan-by-token users?)
|
|
34
|
+
- What's the trigger? (button click, page visit, background job, API call, barcode scan?)
|
|
35
|
+
- Does it touch hardware or external services? (third-party APIs, image rendering, hardware drivers, scheduled jobs?)
|
|
36
|
+
- Does the API need an endpoint too, or HTML only?
|
|
37
|
+
- Are there edge cases or constraints the user already knows about?
|
|
38
|
+
|
|
39
|
+
Keep asking until you have a clear picture. Don't assume — ask.
|
|
40
|
+
|
|
41
|
+
### Phase 2: Technical design
|
|
42
|
+
|
|
43
|
+
Now figure out HOW. Research if needed:
|
|
44
|
+
- Look at existing similar features in the codebase (`app/concepts/`, `app/models/`) for reference patterns
|
|
45
|
+
- For external integrations (third-party APIs, etc.), search the web for their docs and find the best integration approach
|
|
46
|
+
- Read existing operations in the same domain to stay consistent
|
|
47
|
+
|
|
48
|
+
Discuss with the user:
|
|
49
|
+
- Database changes needed (new tables, columns, indexes, constraints, foreign keys)
|
|
50
|
+
- New models or concerns; existing models to extend
|
|
51
|
+
- Routes (RESTful resources? member/collection actions? API namespace?)
|
|
52
|
+
- How many operations and components — name them
|
|
53
|
+
- Authorization rules (who can do what — Pundit policy methods + Scope)
|
|
54
|
+
- Frontend interactions (Turbo Frames? Stimulus? Modal via `remote: true`? Static page?)
|
|
55
|
+
- Locale keys needed for all `config/locales/*.yml` files
|
|
56
|
+
- Background jobs (SolidQueue) for slow operations: image generation, mass updates, email
|
|
57
|
+
- Whether `Select2Helper` needs to be added on a model for autocomplete
|
|
58
|
+
|
|
59
|
+
### Phase 3: Detail every file
|
|
60
|
+
|
|
61
|
+
Go file-by-file through everything that needs to be created or modified. For each file:
|
|
62
|
+
- Full path
|
|
63
|
+
- What it does
|
|
64
|
+
- Key logic, fields, kwargs
|
|
65
|
+
|
|
66
|
+
### Phase 4: Pre-generation checklist
|
|
67
|
+
|
|
68
|
+
Before writing the plan, go through this checklist WITH the user:
|
|
69
|
+
|
|
70
|
+
- [ ] DB migrations — tables, columns, indexes, constraints, foreign keys
|
|
71
|
+
- [ ] Models — validations, associations, enums, scopes, concerns (incl. `Select2Helper` if needed)
|
|
72
|
+
- [ ] Routes — resources, nested routes, member/collection actions, API namespace
|
|
73
|
+
- [ ] Operations — one per action under `app/concepts/<feature>/operation/`. For each: `authorize!` / `policy_scope` / `skip_authorize` and what it does
|
|
74
|
+
- [ ] Components — kwargs, what data they display, which `Base::Component::Base` helpers they use (`header`, `modal`, `Base::Component::Table`)
|
|
75
|
+
- [ ] Slim templates — layout, forms, tables, modals
|
|
76
|
+
- [ ] Pundit policy — actions and `Scope#resolve`
|
|
77
|
+
- [ ] Locales — keys for all `config/locales/*.yml` files, and `activerecord.attributes.<ns>/<model>.<attr>` style for model attrs
|
|
78
|
+
- [ ] Stimulus controllers — if dynamic behavior needed; document-level listener cleanup planned?
|
|
79
|
+
- [ ] API: controller, base + per-action params adapters, swagger entry, request specs
|
|
80
|
+
- [ ] Background jobs — anything slow that should not block a request
|
|
81
|
+
- [ ] Tests — operations and request specs; skip association/validation/policy/component tests
|
|
82
|
+
- [ ] Implementation order — step-by-step sequence
|
|
83
|
+
|
|
84
|
+
Ask: "Planning done? Should I generate feature_plan.md?"
|
|
85
|
+
|
|
86
|
+
## Output: feature_plan.md
|
|
87
|
+
|
|
88
|
+
Only when the user confirms, write `feature_plan.md` to the project root with this structure:
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
# Feature: <name>
|
|
92
|
+
|
|
93
|
+
## Overview
|
|
94
|
+
What this feature does and why.
|
|
95
|
+
|
|
96
|
+
## Database changes
|
|
97
|
+
Migration details: tables, columns, types, indexes, constraints.
|
|
98
|
+
|
|
99
|
+
## Models
|
|
100
|
+
New or modified models, associations, validations, enums, scopes, concerns. Note any `Select2Helper` additions and the `select2_search_result` shape.
|
|
101
|
+
|
|
102
|
+
## Routes
|
|
103
|
+
```ruby
|
|
104
|
+
# New routes to add
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Operations
|
|
108
|
+
For each operation:
|
|
109
|
+
- **Class**: `Feature::Operation::Action < Base::Operation::Base`
|
|
110
|
+
- **Authorization**: authorize! / policy_scope / skip_authorize
|
|
111
|
+
- **Logic**: step by step, including notice / redirect / sub-operations
|
|
112
|
+
|
|
113
|
+
## Components
|
|
114
|
+
For each component:
|
|
115
|
+
- **Class**: `Feature::Component::Name < Base::Component::Base`
|
|
116
|
+
- **kwargs**: what data it receives (specific names, not `model:`)
|
|
117
|
+
- **Template**: key UI elements (header, table, modal, form)
|
|
118
|
+
|
|
119
|
+
## Policies
|
|
120
|
+
- **Class**: `FeaturePolicy < ApplicationPolicy`
|
|
121
|
+
- **Rules**: per-action (`index?`, `show?`, `edit?`, `update?`, `destroy?`)
|
|
122
|
+
- **Scope**: how `resolve` filters by account
|
|
123
|
+
|
|
124
|
+
## API (only if applicable)
|
|
125
|
+
- Controller: `Api::V1::FeatureController`
|
|
126
|
+
- ParamsAdapter::Base shape (basic_output, full_output)
|
|
127
|
+
- Per-action adapters: input_param declarations, input_params/output_params
|
|
128
|
+
- Swagger entries to add
|
|
129
|
+
|
|
130
|
+
## Locales
|
|
131
|
+
```yaml
|
|
132
|
+
# keys for every locale file in config/locales/
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Stimulus controllers
|
|
136
|
+
If needed — file name, identifier, targets, actions, document-level listeners + disconnect cleanup.
|
|
137
|
+
|
|
138
|
+
## Background jobs
|
|
139
|
+
SolidQueue jobs for slow work — what gets enqueued and when.
|
|
140
|
+
|
|
141
|
+
## Tests
|
|
142
|
+
Operation specs (happy + unauthorized + edge cases); request specs (auth, validation, success). Skip policy/component/association/validation specs.
|
|
143
|
+
|
|
144
|
+
## Implementation order
|
|
145
|
+
Numbered step-by-step sequence to follow when implementing.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Rules
|
|
149
|
+
|
|
150
|
+
- NEVER write code or implement anything. Only plan.
|
|
151
|
+
- Always read existing code before suggesting patterns — stay consistent with the codebase.
|
|
152
|
+
- When researching external services, share what you found and discuss options before deciding.
|
|
153
|
+
- Speak Ukrainian if the user writes in Ukrainian, English if in English.
|
|
154
|
+
- Keep responses focused — don't dump walls of text. Ask one thing at a time.
|
|
155
|
+
- Use compact class notation: `class Feature::Operation::Action < Base::Operation::Base`
|
|
156
|
+
- Remember: controllers are thin wrappers (`endpoint Op, Component` or `api_endpoint Op, ParamsAdapter`), all logic lives in operations.
|
|
157
|
+
- Locale keys live in All `config/locales/*.yml` files.
|