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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +31 -0
- data/.github/workflows/release.yml +459 -0
- data/.rubocop.yml +52 -0
- data/.sisyphus/notepads/develoz-rails-template/learnings.md +855 -0
- data/.tool-versions +2 -0
- data/CHANGELOG.md +28 -0
- data/CONTRIBUTING.md +120 -0
- data/README.md +76 -0
- data/Rakefile +18 -0
- data/config/generators.yml +105 -0
- data/docs/guide/installation.md +574 -0
- data/exe/develoz +6 -0
- data/install +98 -0
- data/lib/develoz/canonical_fetcher.rb +56 -0
- data/lib/develoz/cli.rb +84 -0
- data/lib/develoz/generators/base.rb +134 -0
- data/lib/develoz/manifest.rb +44 -0
- data/lib/develoz/options.rb +55 -0
- data/lib/develoz/version.rb +5 -0
- data/lib/develoz/version_resolver.rb +103 -0
- data/lib/develoz.rb +15 -0
- data/lib/generators/develoz/active_resource/active_resource_generator.rb +25 -0
- data/lib/generators/develoz/active_resource/templates/application_resource.rb.tt +7 -0
- data/lib/generators/develoz/active_resource/templates/example_resource.rb.tt +5 -0
- data/lib/generators/develoz/admin/admin_generator.rb +44 -0
- data/lib/generators/develoz/admin/templates/admin_base_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/admin_layout.html.erb.tt +38 -0
- data/lib/generators/develoz/admin/templates/dashboard_controller.rb.tt +7 -0
- data/lib/generators/develoz/admin/templates/dashboard_index.html.erb.tt +2 -0
- data/lib/generators/develoz/admin/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/agents_docs/agents_docs_generator.rb +50 -0
- data/lib/generators/develoz/agents_docs/templates/.github/pull_request_template.md.tt +13 -0
- data/lib/generators/develoz/agents_docs/templates/AGENTS.md.tt +36 -0
- data/lib/generators/develoz/agents_docs/templates/docs/development.md.tt +121 -0
- data/lib/generators/develoz/agents_docs/templates/docs/performance.md.tt +46 -0
- data/lib/generators/develoz/agents_docs/templates/docs/testing.md.tt +92 -0
- data/lib/generators/develoz/agents_docs/templates/spec/cassettes/Example_API/fetches_data_from_an_external_API.yml.tt +23 -0
- data/lib/generators/develoz/agents_docs/templates/spec/requests/example_api_spec.rb.tt +14 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/faraday.rb.tt +19 -0
- data/lib/generators/develoz/agents_docs/templates/spec/support/vcr.rb.tt +11 -0
- data/lib/generators/develoz/api/api_generator.rb +55 -0
- data/lib/generators/develoz/api/templates/app/blueprints/example_blueprint.rb.tt +6 -0
- data/lib/generators/develoz/api/templates/app/controllers/api/v1/base_controller.rb.tt +96 -0
- data/lib/generators/develoz/api/templates/config/initializers/blueprinter.rb.tt +16 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_api.rb.tt +15 -0
- data/lib/generators/develoz/api/templates/config/initializers/rswag_ui.rb.tt +17 -0
- data/lib/generators/develoz/api/templates/spec/requests/api/v1/examples_spec.rb.tt +25 -0
- data/lib/generators/develoz/auth/auth_generator.rb +82 -0
- data/lib/generators/develoz/auth/templates/app/controllers/concerns/authentication.rb.tt +53 -0
- data/lib/generators/develoz/auth/templates/app/controllers/passwords_controller.rb.tt +35 -0
- data/lib/generators/develoz/auth/templates/app/controllers/sessions_controller.rb.tt +22 -0
- data/lib/generators/develoz/auth/templates/app/mailers/passwords_mailer.rb.tt +8 -0
- data/lib/generators/develoz/auth/templates/app/models/current.rb.tt +5 -0
- data/lib/generators/develoz/auth/templates/app/models/user.rb.tt +7 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords/edit.html.erb.tt +9 -0
- data/lib/generators/develoz/auth/templates/app/views/passwords_mailer/reset.html.erb.tt +6 -0
- data/lib/generators/develoz/auth/templates/app/views/sessions/new.html.erb.tt +11 -0
- data/lib/generators/develoz/auth/templates/db/migrate/create_users.rb.tt +13 -0
- data/lib/generators/develoz/auth/templates/spec/requests/passwords_spec.rb.tt +60 -0
- data/lib/generators/develoz/auth/templates/spec/requests/sessions_spec.rb.tt +37 -0
- data/lib/generators/develoz/ci/ci_generator.rb +66 -0
- data/lib/generators/develoz/ci/templates/.github/workflows/ci.yml.tt +18 -0
- data/lib/generators/develoz/ci/templates/.haml-lint.yml.tt +12 -0
- data/lib/generators/develoz/ci/templates/.markdownlint.json.tt +10 -0
- data/lib/generators/develoz/ci/templates/.reek.yml.tt +28 -0
- data/lib/generators/develoz/ci/templates/.rubocop.yml.tt +22 -0
- data/lib/generators/develoz/ci/templates/.stylelintrc.json.tt +4 -0
- data/lib/generators/develoz/ci/templates/.yamllint.tt +14 -0
- data/lib/generators/develoz/ci/templates/Rakefile.tt +73 -0
- data/lib/generators/develoz/ci/templates/bin/ci.tt +6 -0
- data/lib/generators/develoz/ci/templates/biome.json.tt +41 -0
- data/lib/generators/develoz/ci/templates/config/ci.rb.tt +23 -0
- data/lib/generators/develoz/concerns/concerns_generator.rb +48 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/configurable.rb.tt +120 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/optimized_finders.rb.tt +93 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/searchable_concern.rb.tt +31 -0
- data/lib/generators/develoz/concerns/templates/app/models/concerns/transitionable.rb.tt +144 -0
- data/lib/generators/develoz/concerns/templates/app/models/configuration.rb.tt +104 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/add_status_transitions.rb.tt +30 -0
- data/lib/generators/develoz/concerns/templates/db/migrate/create_configurations.rb.tt +25 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/configurable_spec.rb.tt +172 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/optimized_finders_spec.rb.tt +200 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/searchable_concern_spec.rb.tt +84 -0
- data/lib/generators/develoz/concerns/templates/spec/models/concerns/transitionable_spec.rb.tt +234 -0
- data/lib/generators/develoz/concerns/templates/spec/models/configuration_spec.rb.tt +223 -0
- data/lib/generators/develoz/database/database_generator.rb +44 -0
- data/lib/generators/develoz/database/templates/config/database.yml.tt +53 -0
- data/lib/generators/develoz/database/templates/config/initializers/pg_search.rb.tt +5 -0
- data/lib/generators/develoz/database/templates/db/migrate/create_pg_extensions.rb.tt +13 -0
- data/lib/generators/develoz/db_backup/db_backup_generator.rb +47 -0
- data/lib/generators/develoz/db_backup/templates/backup_rake.tt +48 -0
- data/lib/generators/develoz/db_backup/templates/bin_db_backup.tt +25 -0
- data/lib/generators/develoz/db_backup/templates/compose_service.tt +14 -0
- data/lib/generators/develoz/doc_specs/doc_specs_generator.rb +33 -0
- data/lib/generators/develoz/doc_specs/templates/bin/generate-docs.tt +392 -0
- data/lib/generators/develoz/doc_specs/templates/lib/tasks/docs_check.rake.tt +8 -0
- data/lib/generators/develoz/doc_specs/templates/spec/support/doc_screenshot_helper.rb.tt +17 -0
- data/lib/generators/develoz/doc_specs/templates/spec/system/example_doc_spec.rb.tt +38 -0
- data/lib/generators/develoz/docker/docker_generator.rb +54 -0
- data/lib/generators/develoz/docker/templates/bin_dev.tt +5 -0
- data/lib/generators/develoz/docker/templates/bin_docker_entrypoint.tt +8 -0
- data/lib/generators/develoz/docker/templates/bin_run.tt +152 -0
- data/lib/generators/develoz/docker/templates/bin_setup.tt +15 -0
- data/lib/generators/develoz/docker/templates/docker-compose.yml.tt +59 -0
- data/lib/generators/develoz/docker/templates/dockerfile_dev.tt +24 -0
- data/lib/generators/develoz/docs_render/docs_render_generator.rb +46 -0
- data/lib/generators/develoz/docs_render/templates/app/assets/stylesheets/documentation.scss.tt +145 -0
- data/lib/generators/develoz/docs_render/templates/app/controllers/docs_controller.rb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/app/javascript/docs.js.tt +15 -0
- data/lib/generators/develoz/docs_render/templates/app/models/document.rb.tt +189 -0
- data/lib/generators/develoz/docs_render/templates/app/views/docs/show.html.erb.tt +7 -0
- data/lib/generators/develoz/docs_render/templates/config/initializers/redcarpet_rouge.rb.tt +16 -0
- data/lib/generators/develoz/frontend_core/frontend_core_generator.rb +40 -0
- data/lib/generators/develoz/frontend_core/templates/.annotaterb.yml.tt +16 -0
- data/lib/generators/develoz/frontend_core/templates/config/importmap.rb.tt +12 -0
- data/lib/generators/develoz/frontend_core/templates/config/initializers/pagy.rb.tt +10 -0
- data/lib/generators/develoz/install/install_generator.rb +51 -0
- data/lib/generators/develoz/kamal/kamal_generator.rb +59 -0
- data/lib/generators/develoz/kamal/templates/accessories_postgres.yml.tt +14 -0
- data/lib/generators/develoz/kamal/templates/deploy.yml.tt +43 -0
- data/lib/generators/develoz/kamal/templates/dockerfile_prod.tt +54 -0
- data/lib/generators/develoz/kamal/templates/kamal_secrets.tt +22 -0
- data/lib/generators/develoz/maintenance/maintenance_generator.rb +29 -0
- data/lib/generators/develoz/maintenance/templates/app/tasks/maintenance/example_task.rb.tt +20 -0
- data/lib/generators/develoz/maintenance/templates/lib/tasks/maintenance_counters.rake.tt +27 -0
- data/lib/generators/develoz/push/push_generator.rb +61 -0
- data/lib/generators/develoz/push/templates/app/javascript/pwa/subscription.js.tt +57 -0
- data/lib/generators/develoz/push/templates/app/models/push_subscription.rb.tt +14 -0
- data/lib/generators/develoz/push/templates/app/services/push_notification_service.rb.tt +36 -0
- data/lib/generators/develoz/push/templates/app/views/pwa/sw_push_handlers.js.tt +49 -0
- data/lib/generators/develoz/push/templates/constants_env.tt +5 -0
- data/lib/generators/develoz/push/templates/db/migrate/create_push_subscriptions.rb.tt +15 -0
- data/lib/generators/develoz/pwa/pwa_generator.rb +46 -0
- data/lib/generators/develoz/pwa/templates/app/controllers/pwa_controller.rb.tt +17 -0
- data/lib/generators/develoz/pwa/templates/app/javascript/pwa/registration.js.tt +25 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/manifest.json.erb.tt +24 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/offline.html.erb.tt +51 -0
- data/lib/generators/develoz/pwa/templates/app/views/pwa/service-worker.js.tt +109 -0
- data/lib/generators/develoz/pwa/templates/routes.rb.tt +3 -0
- data/lib/generators/develoz/solid/solid_generator.rb +52 -0
- data/lib/generators/develoz/solid/templates/app/jobs/application_job.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/cable.yml.tt +10 -0
- data/lib/generators/develoz/solid/templates/config/cache.yml.tt +16 -0
- data/lib/generators/develoz/solid/templates/config/initializers/mission_control.rb.tt +8 -0
- data/lib/generators/develoz/solid/templates/config/initializers/solid.rb.tt +7 -0
- data/lib/generators/develoz/solid/templates/config/queue.yml.tt +18 -0
- data/lib/generators/develoz/solid/templates/config/recurring.yml.tt +20 -0
- data/lib/generators/develoz/strict_loading/strict_loading_generator.rb +17 -0
- data/lib/generators/develoz/strict_loading/templates/config/initializers/strict_loading.rb.tt +13 -0
- data/lib/generators/develoz/testing/templates/rspec.tt +2 -0
- data/lib/generators/develoz/testing/templates/rspec_parallel.tt +2 -0
- data/lib/generators/develoz/testing/templates/spec/rails_helper.rb.tt +32 -0
- data/lib/generators/develoz/testing/templates/spec/spec_helper.rb.tt +31 -0
- data/lib/generators/develoz/testing/testing_generator.rb +39 -0
- data/lib/generators/develoz/tooling/templates/constants.rb.tt +17 -0
- data/lib/generators/develoz/tooling/templates/env.example.tt +4 -0
- data/lib/generators/develoz/tooling/templates/vscode/extensions.json.tt +8 -0
- data/lib/generators/develoz/tooling/templates/vscode/settings.json.tt +26 -0
- data/lib/generators/develoz/tooling/templates/vscode/tasks.json.tt +26 -0
- data/lib/generators/develoz/tooling/tooling_generator.rb +33 -0
- data/lib/generators/develoz/ui/templates/bin/setup_develoz_ui.tt +20 -0
- data/lib/generators/develoz/ui/ui_generator.rb +57 -0
- data/lib/generators/develoz/versioning/templates/app/helpers/application_helper.rb.tt +7 -0
- data/lib/generators/develoz/versioning/templates/app/views/shared/_app_version.html.erb.tt +3 -0
- data/lib/generators/develoz/versioning/templates/spec/helpers/application_helper_spec.rb.tt +11 -0
- data/lib/generators/develoz/versioning/versioning_generator.rb +48 -0
- data/lib/tasks/canonical.rake +27 -0
- data/templates/.keep +0 -0
- data/templates/CANONICAL_SOURCES.md +93 -0
- data/templates/pull_request_template.md.tt +13 -0
- 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
|
data/lib/develoz/cli.rb
ADDED
|
@@ -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,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,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,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 · <%%= 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>
|