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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +200 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +589 -0
  6. data/Rakefile +17 -0
  7. data/lib/generators/tsykvas_rails_template/companions/companions_generator.rb +273 -0
  8. data/lib/generators/tsykvas_rails_template/concept/concept_generator.rb +145 -0
  9. data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.html.slim.tt +5 -0
  10. data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.rb.tt +11 -0
  11. data/lib/generators/tsykvas_rails_template/concept/templates/component/index.html.slim.tt +5 -0
  12. data/lib/generators/tsykvas_rails_template/concept/templates/component/index.rb.tt +11 -0
  13. data/lib/generators/tsykvas_rails_template/concept/templates/component/new.html.slim.tt +5 -0
  14. data/lib/generators/tsykvas_rails_template/concept/templates/component/new.rb.tt +11 -0
  15. data/lib/generators/tsykvas_rails_template/concept/templates/component/show.html.slim.tt +4 -0
  16. data/lib/generators/tsykvas_rails_template/concept/templates/component/show.rb.tt +11 -0
  17. data/lib/generators/tsykvas_rails_template/concept/templates/controller.rb.tt +45 -0
  18. data/lib/generators/tsykvas_rails_template/concept/templates/operation/create.rb.tt +31 -0
  19. data/lib/generators/tsykvas_rails_template/concept/templates/operation/destroy.rb.tt +13 -0
  20. data/lib/generators/tsykvas_rails_template/concept/templates/operation/edit.rb.tt +10 -0
  21. data/lib/generators/tsykvas_rails_template/concept/templates/operation/index.rb.tt +9 -0
  22. data/lib/generators/tsykvas_rails_template/concept/templates/operation/new.rb.tt +10 -0
  23. data/lib/generators/tsykvas_rails_template/concept/templates/operation/show.rb.tt +10 -0
  24. data/lib/generators/tsykvas_rails_template/concept/templates/operation/update.rb.tt +31 -0
  25. data/lib/generators/tsykvas_rails_template/install/bootstrap_installer.rb +225 -0
  26. data/lib/generators/tsykvas_rails_template/install/install_generator.rb +298 -0
  27. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/buddy.md +157 -0
  28. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/code-reviewer.md +117 -0
  29. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/security-reviewer.md +113 -0
  30. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/tech-lead.md +150 -0
  31. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/check.md +51 -0
  32. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/code-review.md +60 -0
  33. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/docs-create.md +102 -0
  34. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pr-review.md +81 -0
  35. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pushit.md +160 -0
  36. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/refactor.md +132 -0
  37. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/task-sum.md +47 -0
  38. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tests.md +67 -0
  39. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tsykvas-claude.md +262 -0
  40. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-docs.md +78 -0
  41. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-rules.md +102 -0
  42. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-tests.md +135 -0
  43. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/architecture.md +315 -0
  44. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/authentication.md +96 -0
  45. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/background-jobs.md +135 -0
  46. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/code-style.md +101 -0
  47. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/commands.md +34 -0
  48. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/companions.md +128 -0
  49. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/concepts-refactoring.md +194 -0
  50. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/database.md +135 -0
  51. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/deployment.md +138 -0
  52. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/design-system.md +322 -0
  53. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/documentation.md +89 -0
  54. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/forms.md +174 -0
  55. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/i18n.md +165 -0
  56. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/routing-and-namespaces.md +114 -0
  57. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/security.md +122 -0
  58. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/stimulus-controllers.md +166 -0
  59. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing-examples.md +180 -0
  60. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing.md +117 -0
  61. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md +280 -0
  62. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/ui-components.md +196 -0
  63. data/lib/generators/tsykvas_rails_template/install/templates/CLAUDE.md.tt +81 -0
  64. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/component/base.rb +6 -0
  65. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb +124 -0
  66. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb +56 -0
  67. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.html.slim +49 -0
  68. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.rb +11 -0
  69. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb +17 -0
  70. data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/concerns/operations_methods.rb +148 -0
  71. data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/home_controller.rb +10 -0
  72. data/lib/generators/tsykvas_rails_template/install/templates/app/policies/application_policy.rb +33 -0
  73. data/lib/generators/tsykvas_rails_template/install/templates/app/policies/home_policy.rb +8 -0
  74. data/lib/tasks/tsykvas.rake +11 -0
  75. data/lib/tsykvas_rails_template/probe.rb +236 -0
  76. data/lib/tsykvas_rails_template/railtie.rb +13 -0
  77. data/lib/tsykvas_rails_template/version.rb +5 -0
  78. data/lib/tsykvas_rails_template.rb +18 -0
  79. 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.