develoz-rails 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 (172) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +31 -0
  3. data/.github/workflows/release.yml +459 -0
  4. data/.rubocop.yml +52 -0
  5. data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
  6. data/.tool-versions +2 -0
  7. data/CHANGELOG.md +28 -0
  8. data/CONTRIBUTING.md +120 -0
  9. data/README.md +76 -0
  10. data/Rakefile +18 -0
  11. data/config/generators.yml +105 -0
  12. data/docs/guide/installation.md +574 -0
  13. data/exe/develoz +6 -0
  14. data/install +98 -0
  15. data/lib/develoz/canonical_fetcher.rb +56 -0
  16. data/lib/develoz/cli.rb +84 -0
  17. data/lib/develoz/generators/base.rb +134 -0
  18. data/lib/develoz/manifest.rb +44 -0
  19. data/lib/develoz/options.rb +55 -0
  20. data/lib/develoz/version.rb +5 -0
  21. data/lib/develoz/version_resolver.rb +103 -0
  22. data/lib/develoz.rb +15 -0
  23. data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
  24. data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
  25. data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
  26. data/lib/generators/develoz/admin/admin_generator.rb +44 -0
  27. data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
  28. data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
  29. data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
  30. data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
  31. data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
  32. data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
  33. data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
  34. data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
  35. data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
  36. data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
  37. data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
  38. data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
  39. data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
  40. data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
  41. data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
  42. data/lib/generators/develoz/api/api_generator.rb +55 -0
  43. data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
  44. data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
  45. data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
  46. data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
  47. data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
  48. data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
  49. data/lib/generators/develoz/auth/auth_generator.rb +82 -0
  50. data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
  51. data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
  52. data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
  53. data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
  54. data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
  55. data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
  56. data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
  57. data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
  58. data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
  59. data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
  60. data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
  61. data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
  62. data/lib/generators/develoz/ci/ci_generator.rb +66 -0
  63. data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
  64. data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
  65. data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
  66. data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
  67. data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
  68. data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
  69. data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
  70. data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
  71. data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
  72. data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
  73. data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
  74. data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
  75. data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
  76. data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
  77. data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
  78. data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
  79. data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
  80. data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
  81. data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
  82. data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
  83. data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
  84. data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
  85. data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
  86. data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
  87. data/lib/generators/develoz/database/database_generator.rb +44 -0
  88. data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
  89. data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
  90. data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
  91. data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
  92. data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
  93. data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
  94. data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
  95. data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
  96. data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
  97. data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
  98. data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
  99. data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
  100. data/lib/generators/develoz/docker/docker_generator.rb +54 -0
  101. data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
  102. data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
  103. data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
  104. data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
  105. data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
  106. data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
  107. data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
  108. data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
  109. data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
  110. data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
  111. data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
  112. data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
  113. data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
  114. data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
  115. data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
  116. data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
  117. data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
  118. data/lib/generators/develoz/install/install_generator.rb +51 -0
  119. data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
  120. data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
  121. data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
  122. data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
  123. data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
  124. data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
  125. data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
  126. data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
  127. data/lib/generators/develoz/push/push_generator.rb +61 -0
  128. data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
  129. data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
  130. data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
  131. data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
  132. data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
  133. data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
  134. data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
  135. data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
  136. data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
  137. data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
  138. data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
  139. data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
  140. data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
  141. data/lib/generators/develoz/solid/solid_generator.rb +52 -0
  142. data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
  143. data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
  144. data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
  145. data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
  146. data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
  147. data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
  148. data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
  149. data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
  150. data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
  151. data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
  152. data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
  153. data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
  154. data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
  155. data/lib/generators/develoz/testing/testing_generator.rb +39 -0
  156. data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
  157. data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
  158. data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
  159. data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
  160. data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
  161. data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
  162. data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
  163. data/lib/generators/develoz/ui/ui_generator.rb +57 -0
  164. data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
  165. data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
  166. data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
  167. data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
  168. data/lib/tasks/canonical.rake +27 -0
  169. data/templates/.keep +0 -0
  170. data/templates/CANONICAL_SOURCES.md +93 -0
  171. data/templates/pull_request_template.md.tt +13 -0
  172. metadata +383 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "base64"
5
+ require "json"
6
+ require "fileutils"
7
+
8
+ module Develoz
9
+ class CanonicalFetcher
10
+ GITHUB_API_BASE = "https://api.github.com"
11
+
12
+ def self.fetch(repo:, path:, dest:)
13
+ new.fetch(repo: repo, path: path, dest: dest)
14
+ end
15
+
16
+ def fetch(repo:, path:, dest:)
17
+ url = "#{GITHUB_API_BASE}/repos/#{repo}/contents/#{path}"
18
+ token = fetch_token
19
+
20
+ response = make_request(url, token)
21
+ raise "Failed to fetch #{repo}/#{path}: #{response.code}" unless response.code == "200"
22
+
23
+ content = JSON.parse(response.body)["content"]
24
+ decoded = Base64.decode64(content)
25
+
26
+ FileUtils.mkdir_p(File.dirname(dest))
27
+ File.write(dest, decoded)
28
+ end
29
+
30
+ private
31
+
32
+ def fetch_token
33
+ ENV["GH_TOKEN"] || shell_token
34
+ end
35
+
36
+ def shell_token
37
+ `gh auth token 2>/dev/null`.strip
38
+ rescue StandardError => e
39
+ warn "Unable to retrieve GitHub token: #{e.message}"
40
+ nil
41
+ end
42
+
43
+ def make_request(url, token)
44
+ uri = URI(url)
45
+ http = Net::HTTP.new(uri.host, uri.port)
46
+ http.use_ssl = true
47
+ http.open_timeout = 5
48
+ http.read_timeout = 5
49
+
50
+ request = Net::HTTP::Get.new(uri.request_uri)
51
+ request["Authorization"] = "token #{token}" if token
52
+
53
+ http.request(request)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "tty-prompt"
5
+ require_relative "version"
6
+
7
+ module Develoz
8
+ class CLI < Thor
9
+ OPT_IN_FLAGS = %i[api auth pwa push active_resource admin ui kamal docker db_backup].freeze
10
+ FLAG_LABELS = {
11
+ api: "API layer", auth: "authentication", pwa: "PWA support",
12
+ push: "push notifications", active_resource: "ActiveResource",
13
+ admin: "admin dashboard", ui: "develoz-ui", kamal: "Kamal deployment",
14
+ docker: "Docker setup", db_backup: "database backups"
15
+ }.freeze
16
+ RAILS_NEW_SKIPS = %w[--skip-test --skip-ci --skip-bundle].freeze
17
+
18
+ desc "version", "Show version"
19
+ def version
20
+ puts "develoz #{Develoz::VERSION}"
21
+ end
22
+
23
+ desc "new APP_NAME", "Generate a new Develoz Rails app"
24
+ option :api, type: :boolean
25
+ option :auth, type: :boolean
26
+ option :pwa, type: :boolean
27
+ option :push, type: :boolean
28
+ option :active_resource, type: :boolean
29
+ option :admin, type: :boolean
30
+ option :ui, type: :boolean
31
+ option :kamal, type: :boolean
32
+ option :docker, type: :boolean
33
+ option :db_backup, type: :boolean
34
+ option :skip_pagy, type: :boolean
35
+ option :ruby, type: :string
36
+ option :rails, type: :string
37
+ option :yes, type: :boolean
38
+ def new(app_name)
39
+ resolved = resolve_flags
40
+ resolver = Develoz::VersionResolver.new
41
+ versions = resolver.resolve(ruby: options[:ruby], rails: options[:rails])
42
+ run_rails_new(app_name, versions[:rails])
43
+ write_version_files(app_name, versions, resolver)
44
+ invoke_install(app_name, resolved)
45
+ puts "Created #{app_name} with Develoz Rails template."
46
+ end
47
+
48
+ private
49
+
50
+ def resolve_flags
51
+ OPT_IN_FLAGS.index_with { |flag| resolve_flag(flag) }
52
+ end
53
+
54
+ def resolve_flag(flag)
55
+ return options[flag] unless options[flag].nil?
56
+ return false if options[:yes]
57
+
58
+ prompt.yes?("Include #{FLAG_LABELS[flag]}?")
59
+ end
60
+
61
+ def prompt
62
+ @prompt ||= TTY::Prompt.new
63
+ end
64
+
65
+ def run_rails_new(app_name, rails_version)
66
+ return if system("rails", "new", app_name, "--rails-version=#{rails_version}", *RAILS_NEW_SKIPS)
67
+
68
+ raise "Failed to generate Rails app: #{app_name}"
69
+ end
70
+
71
+ def write_version_files(app_name, versions, resolver)
72
+ dir = File.expand_path(app_name)
73
+ resolver.write_tool_versions(dir, ruby: versions[:ruby])
74
+ resolver.write_ruby_version(dir, ruby: versions[:ruby])
75
+ end
76
+
77
+ def invoke_install(app_name, resolved)
78
+ opts = resolved.merge(skip_pagy: options[:skip_pagy])
79
+ Dir.chdir(app_name) do
80
+ Develoz::Generators::InstallGenerator.new([], opts, destination_root: Dir.pwd).install
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Develoz
6
+ module Generators
7
+ class Base < Rails::Generators::Base
8
+ cattr_accessor :migration_counter, default: 0
9
+
10
+ def self.next_migration_timestamp
11
+ self.migration_counter += 1
12
+ (Time.now.utc + migration_counter).strftime("%Y%m%d%H%M%S")
13
+ end
14
+
15
+ delegate :next_migration_timestamp, to: :class
16
+
17
+ def self.source_root
18
+ File.expand_path("../../../templates", __dir__)
19
+ end
20
+
21
+ def app_name
22
+ File.basename(destination_root)
23
+ end
24
+
25
+ def app_class
26
+ app_name.camelize
27
+ end
28
+
29
+ def inject_once(into:, content:, after: nil, before: nil, marker: nil)
30
+ file_path = File.join(destination_root, into)
31
+ return unless File.exist?(file_path)
32
+
33
+ file_content = File.read(file_path)
34
+
35
+ if marker && file_content.include?(marker)
36
+ say "#{into}: marker '#{marker}' already present, skipping", :green
37
+ return
38
+ end
39
+
40
+ if file_content.include?(content)
41
+ say "#{into}: content already present, skipping", :green
42
+ return
43
+ end
44
+
45
+ if after
46
+ inject_into_file(into, "#{content}\n", after: after)
47
+ elsif before
48
+ inject_into_file(into, "#{content}\n", before: before)
49
+ else
50
+ inject_into_file(into, "#{content}\n")
51
+ end
52
+ end
53
+
54
+ def add_gem(name, version = nil, group: nil, **)
55
+ file_path = File.join(destination_root, "Gemfile")
56
+ return unless File.exist?(file_path)
57
+
58
+ gemfile_content = File.read(file_path)
59
+
60
+ if gemfile_content.match?(/^\s*gem\s+["']#{Regexp.escape(name)}["']/)
61
+ say "Gemfile: gem '#{name}' already present, skipping", :green
62
+ return
63
+ end
64
+
65
+ gem(name, version, group: group, **)
66
+ gemfile = File.read(file_path)
67
+ formatted_gemfile = gemfile.gsub(/group: \[([^\]\n]+)\]/, 'group: [ \1 ]')
68
+ File.write(file_path, formatted_gemfile) if formatted_gemfile != gemfile
69
+ end
70
+
71
+ def insert_route(route_line)
72
+ file_path = File.join(destination_root, "config/routes.rb")
73
+ return unless File.exist?(file_path)
74
+
75
+ routes_content = File.read(file_path)
76
+
77
+ if routes_content.include?(route_line)
78
+ say "config/routes.rb: route already present, skipping", :green
79
+ return
80
+ end
81
+
82
+ inject_into_file("config/routes.rb", " #{route_line}\n", before: /^end\s*$/)
83
+ end
84
+
85
+ def append_env(key, value, example: true)
86
+ env_file = File.join(destination_root, ".env")
87
+ env_example_file = File.join(destination_root, ".env.example")
88
+
89
+ if File.exist?(env_file)
90
+ env_content = File.read(env_file)
91
+ if env_content.include?("#{key}=")
92
+ say ".env: key '#{key}' already present, skipping", :green
93
+ else
94
+ append_to_file(".env", "#{key}=#{value}\n")
95
+ end
96
+ end
97
+
98
+ return unless example && File.exist?(env_example_file)
99
+
100
+ example_content = File.read(env_example_file)
101
+ if example_content.include?("#{key}=")
102
+ say ".env.example: key '#{key}' already present, skipping", :green
103
+ else
104
+ append_to_file(".env.example", "#{key}=\n")
105
+ end
106
+ end
107
+
108
+ def ensure_gitignore(pattern)
109
+ file_path = File.join(destination_root, ".gitignore")
110
+ return unless File.exist?(file_path)
111
+
112
+ gitignore_content = File.read(file_path)
113
+
114
+ if gitignore_content.include?(pattern)
115
+ say ".gitignore: pattern '#{pattern}' already present, skipping", :green
116
+ return
117
+ end
118
+
119
+ append_to_file(".gitignore", "#{pattern}\n")
120
+ end
121
+
122
+ def migration_exists?(name)
123
+ Dir.glob(File.join(destination_root, "db/migrate/*_#{name}.rb")).any?
124
+ end
125
+
126
+ def apply_template(name, destination)
127
+ template_file = File.join(self.class.source_root, name)
128
+ return unless File.exist?(template_file)
129
+
130
+ copy_file(name, destination)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Develoz
6
+ class Manifest
7
+ Entry = Struct.new(:name, :description)
8
+
9
+ class << self
10
+ def load
11
+ manifest_path = File.expand_path("../../config/generators.yml", __dir__)
12
+ yaml_content = File.read(manifest_path)
13
+ YAML.safe_load(yaml_content) || {}
14
+ end
15
+
16
+ def for(options)
17
+ manifest = load
18
+ auto_include_pwa(options)
19
+ entries = []
20
+
21
+ manifest.each do |name, config|
22
+ if config["always"]
23
+ entries << Entry.new(name, config["description"])
24
+ else
25
+ requires = config["requires"] || []
26
+ entries << Entry.new(name, config["description"]) if requires_met?(options, requires)
27
+ end
28
+ end
29
+
30
+ entries
31
+ end
32
+
33
+ private
34
+
35
+ def auto_include_pwa(options)
36
+ options.instance_variable_set(:@pwa, true) if options.push? && !options.pwa?
37
+ end
38
+
39
+ def requires_met?(options, requires)
40
+ requires.all? { |flag| options.public_send("#{flag}?") }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Develoz
4
+ class Options
5
+ # Opt-in feature flags (default false)
6
+ OPT_IN_FLAGS = %i[api auth pwa push active_resource admin ui kamal docker db_backup].freeze
7
+
8
+ # Opt-out flags (default false → feature is included unless skipped)
9
+ OPT_OUT_FLAGS = %i[skip_pagy].freeze
10
+
11
+ # All valid flag keys
12
+ VALID_KEYS = (OPT_IN_FLAGS + OPT_OUT_FLAGS + %i[app_name ruby_version rails_version]).freeze
13
+
14
+ attr_reader :app_name, :ruby_version, :rails_version
15
+
16
+ def initialize(options = {})
17
+ unknown_keys = options.keys - VALID_KEYS
18
+ raise ArgumentError, "Unknown option(s): #{unknown_keys.join(', ')}" if unknown_keys.any?
19
+
20
+ @app_name = options[:app_name]
21
+ @ruby_version = options[:ruby_version]
22
+ @rails_version = options[:rails_version]
23
+
24
+ # Initialize opt-in flags (default false)
25
+ OPT_IN_FLAGS.each do |flag|
26
+ instance_variable_set("@#{flag}", options[flag] || false)
27
+ end
28
+
29
+ # Initialize opt-out flags (default false)
30
+ OPT_OUT_FLAGS.each do |flag|
31
+ instance_variable_set("@#{flag}", options[flag] || false)
32
+ end
33
+ end
34
+
35
+ # Predicate readers for opt-in flags
36
+ OPT_IN_FLAGS.each do |flag|
37
+ define_method("#{flag}?") { instance_variable_get("@#{flag}") }
38
+ end
39
+
40
+ # Predicate readers for opt-out flags
41
+ OPT_OUT_FLAGS.each do |flag|
42
+ define_method("#{flag}?") { instance_variable_get("@#{flag}") }
43
+ end
44
+
45
+ def to_h
46
+ {
47
+ app_name: @app_name,
48
+ ruby_version: @ruby_version,
49
+ rails_version: @rails_version,
50
+ **OPT_IN_FLAGS.index_with { |flag| instance_variable_get("@#{flag}") },
51
+ **OPT_OUT_FLAGS.index_with { |flag| instance_variable_get("@#{flag}") }
52
+ }
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Develoz
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Develoz
7
+ class VersionResolver
8
+ RUBY_FALLBACK = "4.0.5"
9
+ RAILS_FALLBACK = "8.1"
10
+
11
+ RUBY_VERSION_URL = "https://cache.ruby-lang.org/pub/misc/latest_ruby"
12
+ RAILS_VERSION_URL = "https://rubygems.org/api/v1/versions/rails/latest.json"
13
+
14
+ TIMEOUT_SECONDS = 5
15
+
16
+ def self.resolve(ruby: nil, rails: nil)
17
+ new.resolve(ruby:, rails:)
18
+ end
19
+
20
+ def resolve(ruby: nil, rails: nil)
21
+ {
22
+ ruby: ruby || fetch_ruby_version,
23
+ rails: rails || fetch_rails_version
24
+ }
25
+ end
26
+
27
+ def self.write_tool_versions(dir, ruby:, node: "24.15.0")
28
+ new.write_tool_versions(dir, ruby:, node:)
29
+ end
30
+
31
+ def write_tool_versions(dir, ruby:, node: "24.15.0")
32
+ path = File.join(dir, ".tool-versions")
33
+ content = "ruby #{ruby}\nnodejs #{node}\n"
34
+ File.write(path, content)
35
+ end
36
+
37
+ def self.write_ruby_version(dir, ruby:)
38
+ new.write_ruby_version(dir, ruby:)
39
+ end
40
+
41
+ def write_ruby_version(dir, ruby:)
42
+ path = File.join(dir, ".ruby-version")
43
+ content = "#{ruby}\n"
44
+ File.write(path, content)
45
+ end
46
+
47
+ private
48
+
49
+ def fetch_ruby_version
50
+ response = fetch_url(RUBY_VERSION_URL)
51
+ unless response
52
+ warn "Unable to fetch Ruby version; using fallback #{RUBY_FALLBACK}"
53
+ return RUBY_FALLBACK
54
+ end
55
+
56
+ version = response.strip
57
+ return version unless version.empty?
58
+
59
+ warn "Ruby version response was empty; using fallback #{RUBY_FALLBACK}"
60
+ RUBY_FALLBACK
61
+ end
62
+
63
+ def fetch_rails_version
64
+ response = fetch_url(RAILS_VERSION_URL)
65
+ unless response
66
+ warn "Unable to fetch Rails version; using fallback #{RAILS_FALLBACK}"
67
+ return RAILS_FALLBACK
68
+ end
69
+
70
+ data = JSON.parse(response)
71
+ version = data["version"]
72
+ return version if version.present?
73
+
74
+ warn "Rails version response was empty; using fallback #{RAILS_FALLBACK}"
75
+ RAILS_FALLBACK
76
+ rescue JSON::ParserError => e
77
+ warn "Unable to parse Rails version response (#{e.message}); using fallback #{RAILS_FALLBACK}"
78
+ RAILS_FALLBACK
79
+ end
80
+
81
+ def fetch_url(url)
82
+ uri = URI(url)
83
+ http = Net::HTTP.new(uri.host, uri.port)
84
+ http.use_ssl = uri.scheme == "https"
85
+ http.open_timeout = TIMEOUT_SECONDS
86
+ http.read_timeout = TIMEOUT_SECONDS
87
+
88
+ request = Net::HTTP::Get.new(uri.request_uri)
89
+ response = http.request(request)
90
+
91
+ return response.body if response.is_a?(Net::HTTPSuccess)
92
+
93
+ nil
94
+ rescue StandardError => e
95
+ warn_fetch_failure(url, e)
96
+ nil
97
+ end
98
+
99
+ def warn_fetch_failure(url, error)
100
+ warn "Unable to fetch version from #{url} (#{error.message})"
101
+ end
102
+ end
103
+ end
data/lib/develoz.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Develoz
4
+ class Error < StandardError; end
5
+ end
6
+
7
+ require_relative "develoz/version"
8
+
9
+ Dir[File.expand_path("develoz/**/*.rb", __dir__)].each do |file|
10
+ require file unless file.end_with?("version.rb")
11
+ end
12
+
13
+ Dir[File.expand_path("generators/develoz/**/*.rb", __dir__)].each do |file|
14
+ require file
15
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "develoz"
4
+
5
+ module Develoz
6
+ module Generators
7
+ class ActiveResourceGenerator < Develoz::Generators::Base
8
+ def self.source_root
9
+ File.expand_path("templates", __dir__)
10
+ end
11
+
12
+ def add_activeresource_gem
13
+ add_gem "activeresource"
14
+ end
15
+
16
+ def create_application_resource
17
+ template "application_resource.rb.tt", "app/models/application_resource.rb"
18
+ end
19
+
20
+ def create_example_resource
21
+ template "example_resource.rb.tt", "app/models/example_resource.rb"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationResource < ActiveResource::Base
4
+ self.site = ENV.fetch("ACTIVE_RESOURCE_SITE", "")
5
+ self.headers["Authorization"] = ENV.fetch("ACTIVE_RESOURCE_AUTHORIZATION", "")
6
+ self.headers["Accept"] = "application/json"
7
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ExampleResource < ApplicationResource
4
+ # self.element_name = "example"
5
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "develoz"
4
+
5
+ module Develoz
6
+ module Generators
7
+ class AdminGenerator < Develoz::Generators::Base
8
+ def self.source_root
9
+ File.expand_path("templates", __dir__)
10
+ end
11
+
12
+ class_option :ui, type: :boolean, default: false
13
+
14
+ def create_admin_base_controller
15
+ template "admin_base_controller.rb.tt", "app/controllers/admin/base_controller.rb"
16
+ end
17
+
18
+ def create_admin_layout
19
+ template "admin_layout.html.erb.tt", "app/views/layouts/admin.html.erb"
20
+ end
21
+
22
+ def create_dashboard_controller
23
+ template "dashboard_controller.rb.tt", "app/controllers/admin/dashboard_controller.rb"
24
+ end
25
+
26
+ def create_dashboard_view
27
+ template "dashboard_index.html.erb.tt",
28
+ "app/views/admin/dashboard/index.html.erb"
29
+ end
30
+
31
+ def insert_admin_routes
32
+ inject_once(
33
+ into: "config/routes.rb",
34
+ content: " namespace :admin do\n root \"dashboard#index\"\n end",
35
+ before: /^end\s*$/
36
+ )
37
+ end
38
+
39
+ def ui_enabled?
40
+ options[:ui]
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class BaseController < ApplicationController
5
+ layout "admin"
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Admin &middot; <%%= app_name %></title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; }
9
+ body { margin: 0; font-family: system-ui, -apple-system, sans-serif; color: #1a1a1a; background: #f5f5f5; }
10
+ .admin-shell { display: flex; min-height: 100vh; }
11
+ .admin-nav { width: 220px; background: #1f2937; color: #e5e7eb; padding: 1.5rem 1rem; }
12
+ .admin-nav h1 { font-size: 1rem; margin: 0 0 1.5rem; color: #fff; text-transform: uppercase; letter-spacing: 0.05em; }
13
+ .admin-nav a { display: block; padding: 0.5rem 0.75rem; color: #e5e7eb; text-decoration: none; border-radius: 0.25rem; }
14
+ .admin-nav a:hover { background: #374151; color: #fff; }
15
+ .admin-main { flex: 1; padding: 2rem; overflow-y: auto; }
16
+ .admin-flash { margin-bottom: 1rem; padding: 0.75rem 1rem; border-radius: 0.25rem; }
17
+ .admin-flash.alert { background: #fee2e2; color: #991b1b; }
18
+ .admin-flash.notice { background: #dcfce7; color: #166534; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div class="admin-shell">
23
+ <nav class="admin-nav">
24
+ <h1>Admin</h1>
25
+ <%%= link_to "Dashboard", admin_root_path %>
26
+ </nav>
27
+ <main class="admin-main">
28
+ <%% if flash[:alert] %>
29
+ <div class="admin-flash alert"><%%= flash[:alert] %></div>
30
+ <%% end %>
31
+ <%% if flash[:notice] %>
32
+ <div class="admin-flash notice"><%%= flash[:notice] %></div>
33
+ <%% end %>
34
+ <%%= yield %>
35
+ </main>
36
+ </div>
37
+ </body>
38
+ </html>
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Admin
4
+ class DashboardController < Admin::BaseController
5
+ def index; end
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ <h1>Dashboard</h1>
2
+ <p>Welcome to the admin dashboard.</p>
@@ -0,0 +1,3 @@
1
+ namespace :admin do
2
+ root "dashboard#index"
3
+ end