gemstack-cli 0.2.5 → 0.3.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -25
  3. data/README.md +6 -17
  4. data/exe/gemstack +8 -1
  5. metadata +6 -159
  6. data/lib/gemstack/cli/add_generator.rb +0 -239
  7. data/lib/gemstack/cli/app_generator.rb +0 -142
  8. data/lib/gemstack/cli/commands/db.rb +0 -135
  9. data/lib/gemstack/cli/commands/jobs.rb +0 -96
  10. data/lib/gemstack/cli/controller_generator.rb +0 -73
  11. data/lib/gemstack/cli/deploy_generator.rb +0 -102
  12. data/lib/gemstack/cli/doctor.rb +0 -308
  13. data/lib/gemstack/cli/generator.rb +0 -144
  14. data/lib/gemstack/cli/job_generator.rb +0 -48
  15. data/lib/gemstack/cli/migration_generator.rb +0 -52
  16. data/lib/gemstack/cli/policy_generator.rb +0 -35
  17. data/lib/gemstack/cli/project.rb +0 -53
  18. data/lib/gemstack/cli/resource_generator.rb +0 -170
  19. data/lib/gemstack/cli/resource_spec.rb +0 -95
  20. data/lib/gemstack/cli.rb +0 -304
  21. data/templates/app/Gemfile.tt +0 -44
  22. data/templates/app/README.md.tt +0 -29
  23. data/templates/app/app/controllers/application_controller.rb +0 -6
  24. data/templates/app/app/jobs/application_job.rb +0 -9
  25. data/templates/app/app/mailers/application_mailer.rb +0 -8
  26. data/templates/app/app/mailers/templates/dot_keep +0 -0
  27. data/templates/app/app/models/application_model.rb +0 -19
  28. data/templates/app/app/serializers/application_serializer.rb +0 -6
  29. data/templates/app/bin/gemstack +0 -7
  30. data/templates/app/config/app.rb.tt +0 -54
  31. data/templates/app/config/database.yml.tt +0 -61
  32. data/templates/app/config/environments/development.rb.tt +0 -35
  33. data/templates/app/config/environments/production.rb.tt +0 -40
  34. data/templates/app/config/environments/test.rb.tt +0 -21
  35. data/templates/app/config/puma.rb +0 -21
  36. data/templates/app/config/routes.rb +0 -10
  37. data/templates/app/config.ru +0 -6
  38. data/templates/app/db/migrations/dot_keep +0 -0
  39. data/templates/app/db/seeds.rb +0 -5
  40. data/templates/app/dot_env.example.tt +0 -19
  41. data/templates/app/dot_gitignore +0 -16
  42. data/templates/app/dot_node-version.tt +0 -1
  43. data/templates/app/dot_nvmrc.tt +0 -1
  44. data/templates/app/dot_ruby-version.tt +0 -1
  45. data/templates/app/dot_tool-versions.tt +0 -4
  46. data/templates/app/test/health_test.rb +0 -18
  47. data/templates/app/test/test_helper.rb.tt +0 -25
  48. data/templates/auth/app/controllers/api_tokens_controller.rb +0 -35
  49. data/templates/auth/app/controllers/email_verifications_controller.rb +0 -31
  50. data/templates/auth/app/controllers/password_resets_controller.rb +0 -40
  51. data/templates/auth/app/controllers/registrations_controller.rb +0 -19
  52. data/templates/auth/app/controllers/sessions_controller.rb +0 -29
  53. data/templates/auth/app/mailers/auth_mailer.rb +0 -20
  54. data/templates/auth/app/mailers/templates/auth_mailer/email_verification.html.erb +0 -2
  55. data/templates/auth/app/mailers/templates/auth_mailer/email_verification.text.erb +0 -2
  56. data/templates/auth/app/mailers/templates/auth_mailer/password_reset.html.erb +0 -3
  57. data/templates/auth/app/mailers/templates/auth_mailer/password_reset.text.erb +0 -6
  58. data/templates/auth/app/models/auth_token.rb +0 -15
  59. data/templates/auth/app/models/user.rb +0 -12
  60. data/templates/auth/app/policies/application_policy.rb +0 -6
  61. data/templates/auth/app/serializers/api_token_serializer.rb +0 -6
  62. data/templates/auth/app/serializers/new_api_token_serializer.rb +0 -9
  63. data/templates/auth/app/serializers/user_serializer.rb +0 -6
  64. data/templates/auth/db/migrations/%timestamp%_create_auth_tables.rb +0 -41
  65. data/templates/auth/frontend/app/account/page.tsx +0 -116
  66. data/templates/auth/frontend/app/forgot-password/page.tsx +0 -48
  67. data/templates/auth/frontend/app/login/page.tsx +0 -26
  68. data/templates/auth/frontend/app/reset-password/page.tsx +0 -11
  69. data/templates/auth/frontend/app/signup/page.tsx +0 -27
  70. data/templates/auth/frontend/app/verify-email/page.tsx +0 -11
  71. data/templates/auth/frontend/components/auth/CredentialsForm.tsx +0 -58
  72. data/templates/auth/frontend/components/auth/ResetPasswordForm.tsx +0 -43
  73. data/templates/auth/frontend/components/auth/VerifyEmail.tsx +0 -28
  74. data/templates/auth/frontend/lib/auth.ts +0 -113
  75. data/templates/auth/test/controllers/auth_test.rb +0 -108
  76. data/templates/controller/app/controllers/%file_name%_controller.rb.tt +0 -10
  77. data/templates/controller/test/controllers/%file_name%_controller_test.rb.tt +0 -14
  78. data/templates/deploy/Caddyfile.tt +0 -19
  79. data/templates/deploy/Dockerfile.tt +0 -63
  80. data/templates/deploy/Procfile.tt +0 -6
  81. data/templates/deploy/compose.yaml.tt +0 -129
  82. data/templates/deploy/dot_dockerignore +0 -14
  83. data/templates/frontend/app/globals.css +0 -171
  84. data/templates/frontend/app/layout.tsx.tt +0 -19
  85. data/templates/frontend/app/page.module.css +0 -315
  86. data/templates/frontend/app/page.tsx.tt +0 -183
  87. data/templates/frontend/app/providers.tsx +0 -24
  88. data/templates/frontend/lib/gemstack/client.ts +0 -125
  89. data/templates/frontend/next-env.d.ts +0 -5
  90. data/templates/frontend/next.config.ts +0 -17
  91. data/templates/frontend/package.json.tt +0 -23
  92. data/templates/frontend/tsconfig.json +0 -21
  93. data/templates/job/app/jobs/%file_name%.rb.tt +0 -15
  94. data/templates/job/test/jobs/%file_name%_test.rb.tt +0 -15
  95. data/templates/migration/db/migrations/%timestamp%_%file_name%.rb.tt +0 -17
  96. data/templates/policy/app/policies/%file_name%_policy.rb.tt +0 -21
  97. data/templates/policy/test/policies/%file_name%_policy_test.rb.tt +0 -9
  98. data/templates/realtime/config/channels.rb +0 -14
  99. data/templates/realtime/frontend/lib/gemstack/realtime.ts +0 -120
  100. data/templates/resource/controller/app/controllers/%plural%_controller.rb.tt +0 -51
  101. data/templates/resource/controller/test/controllers/%plural%_controller_test.rb.tt +0 -74
  102. data/templates/resource/frontend/frontend/app/%url_segment%/[id]/edit/page.tsx.tt +0 -45
  103. data/templates/resource/frontend/frontend/app/%url_segment%/[id]/page.tsx.tt +0 -49
  104. data/templates/resource/frontend/frontend/app/%url_segment%/new/page.tsx.tt +0 -27
  105. data/templates/resource/frontend/frontend/app/%url_segment%/page.tsx.tt +0 -51
  106. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Card.tsx.tt +0 -15
  107. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Form.tsx.tt +0 -98
  108. data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Table.tsx.tt +0 -36
  109. data/templates/resource/frontend/frontend/lib/format.ts +0 -12
  110. data/templates/resource/frontend/frontend/lib/queries/%url_segment%.ts.tt +0 -64
  111. data/templates/resource/migration/db/migrations/%timestamp%_create_%table%.rb.tt +0 -18
  112. data/templates/resource/model/app/models/%file_name%.rb.tt +0 -15
  113. data/templates/resource/model/test/models/%file_name%_test.rb.tt +0 -29
  114. data/templates/resource/serializer/app/serializers/%file_name%_serializer.rb.tt +0 -7
  115. data/templates/storage/app/controllers/uploads_controller.rb.tt +0 -33
  116. data/templates/storage/app/serializers/upload_serializer.rb +0 -11
  117. data/templates/storage/frontend/lib/upload.ts +0 -52
  118. data/templates/storage/test/controllers/uploads_test.rb.tt +0 -30
@@ -1,135 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemStack
4
- class CLI < Thor
5
- map "db:create" => :db_create, "db:drop" => :db_drop, "db:migrate" => :db_migrate,
6
- "db:rollback" => :db_rollback, "db:seed" => :db_seed, "db:setup" => :db_setup,
7
- "db:status" => :db_status, "db:reset" => :db_reset
8
-
9
- class_option :environment, aliases: "-e", type: :string, desc: "Environment (default: GEMSTACK_ENV or development)"
10
-
11
- desc "db:create", "Create the database (config/database.yml or DATABASE_URL)"
12
- def db_create
13
- with_database do
14
- result = DB::Tasks.create
15
- say("#{result == :created ? "Created" : "Already exists"}: #{DB::Tasks.database_name}")
16
- end
17
- end
18
-
19
- desc "db:drop", "Drop the database (refused in production unless GEMSTACK_ALLOW_DB_DROP=1)"
20
- def db_drop
21
- with_database do
22
- result = DB::Tasks.drop
23
- say("#{result == :dropped ? "Dropped" : "Does not exist"}: #{DB::Tasks.database_name}")
24
- end
25
- end
26
-
27
- desc "db:migrate", "Apply pending migrations (--target VERSION to migrate up or down to a version)"
28
- method_option :target, type: :numeric
29
- def db_migrate
30
- with_database do
31
- changed = DB::Migrator.new.migrate(target: options[:target])
32
- changed.empty? ? say("No pending migrations.") : changed.sort.each { |file| say(" migrated #{file}") }
33
- end
34
- end
35
-
36
- desc "db:rollback", "Revert the last migration (--steps N for more)"
37
- method_option :steps, type: :numeric, default: 1
38
- def db_rollback
39
- with_database do
40
- reverted = DB::Migrator.new.rollback(steps: options[:steps])
41
- if reverted.empty?
42
- say("Nothing to roll back.")
43
- else
44
- reverted.sort.reverse_each do |file|
45
- say(" reverted #{file}")
46
- end
47
- end
48
- end
49
- end
50
-
51
- desc "db:status", "List migrations and whether they are applied"
52
- def db_status
53
- with_database do
54
- rows = DB::Migrator.new.status.map { |m| [m.applied ? "up" : "down", m.version.to_s, m.name] }
55
- if rows.empty?
56
- say("No migrations in #{DB.config.migrations_path}.")
57
- else
58
- print_table([%w[Status Version Name],
59
- *rows])
60
- end
61
- end
62
- end
63
-
64
- desc "db:seed", "Load db/seeds.rb"
65
- def db_seed
66
- with_database(boot: true) do
67
- DB::Tasks.seed ? say("Seeded from #{DB.config.seeds_path}.") : say("No #{DB.config.seeds_path} found.")
68
- end
69
- end
70
-
71
- desc "db:setup", "Create the database, migrate and seed"
72
- def db_setup
73
- db_create
74
- db_migrate
75
- db_seed
76
- end
77
-
78
- desc "db:reset", "Drop, recreate, migrate and seed (development/test only)"
79
- def db_reset
80
- db_drop
81
- db_setup
82
- end
83
-
84
- desc "contract", "Generate TypeScript types, API clients and OpenAPI from the backend"
85
- method_option :quiet, type: :boolean, default: false
86
- def contract
87
- root = Project.ensure_bundle!(self.class.argv)
88
- ENV["GEMSTACK_ENV"] = options[:environment] || ENV["GEMSTACK_ENV"] || "development"
89
- Project.load_config!(root)
90
- require "gemstack/contract"
91
- GemStack.config.reload_code = false
92
- GemStack.config.logger.level = :warn
93
- GemStack.config.db.log_queries = false if GemStack.config.respond_to?(:db)
94
- contract = Contract.build(GemStack.boot!)
95
- report = Contract.write(contract, root: root, typescript: File.directory?(File.join(root, "frontend")))
96
- print_contract_report(report, contract, root)
97
- rescue Sequel::DatabaseConnectionError => e
98
- abort("gemstack contract: database unreachable (#{e.message.lines.first.strip}). Models need it to load.")
99
- end
100
-
101
- no_commands do
102
- def with_database(boot: false)
103
- root = Project.ensure_bundle!(self.class.argv)
104
- ENV["GEMSTACK_ENV"] = options[:environment] || ENV["GEMSTACK_ENV"] || "development"
105
- Project.load_config!(root)
106
- unless defined?(GemStack::DB)
107
- abort("This app doesn't use gemstack-db. Add `gem \"gemstack-db\"` to the Gemfile.")
108
- end
109
- GemStack.config.reload_code = false
110
- GemStack.config.db.log_queries = false
111
- GemStack.config.logger.level = :warn # these commands print their own summary
112
- GemStack.boot! if boot
113
- yield
114
- rescue Sequel::DatabaseConnectionError => e
115
- settings = DB.settings
116
- abort("Can't connect to #{DB::Configuration.describe(settings)} (from #{settings[:source]}): " \
117
- "#{e.message.lines.first.strip}\nCheck config/database.yml or DATABASE_URL — see docs/database.md.")
118
- rescue GemStack::Error => e
119
- abort(e.message)
120
- end
121
-
122
- def redact(url) = url.sub(%r{//([^:/@]+):[^@]+@}, '//\1:***@')
123
-
124
- def print_contract_report(report, contract, root)
125
- rel = ->(path) { path.delete_prefix("#{root}/") }
126
- report[:written].each { |path| say(" #{"write".rjust(9)} #{rel.call(path)}") }
127
- report[:removed].each { |path| say(" #{"remove".rjust(9)} #{rel.call(path)}") }
128
- contract[:warnings].each { |w| say(" #{"warning".rjust(9)} #{w}", :yellow) } unless options[:quiet]
129
- return if options[:quiet] || !report[:written].empty? || !report[:removed].empty?
130
-
131
- say("Contract up to date (#{contract[:resources].size} resources, #{contract[:types].size} types).")
132
- end
133
- end
134
- end
135
- end
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemStack
4
- class CLI < Thor
5
- map "jobs:status" => :jobs_status, "jobs:retry" => :jobs_retry, "jobs:discard" => :jobs_discard,
6
- "jobs:install" => :jobs_install, "jobs:failed" => :jobs_failed
7
-
8
- desc "jobs", "Run a background job worker (PostgreSQL queue)"
9
- method_option :queues, aliases: "-q", type: :string, desc: "Comma-separated queues (default: all)"
10
- method_option :concurrency, aliases: "-c", type: :numeric,
11
- desc: "Worker threads (default: GEMSTACK_JOB_CONCURRENCY or 5)"
12
- def jobs
13
- with_jobs do |config|
14
- config.jobs.queues = options[:queues].split(",").map(&:strip) if options[:queues]
15
- config.jobs.concurrency = options[:concurrency] if options[:concurrency]
16
- # Each worker thread holds a connection, plus LISTEN and the reaper.
17
- config.db.pool_size = [config.db.pool_size, config.jobs.concurrency + 2].max
18
- GemStack.boot!
19
- GemStack.application.eager_load!
20
- require "gemstack/jobs/worker"
21
- Jobs::Worker.new.run
22
- end
23
- end
24
-
25
- desc "jobs:status", "Show queued, scheduled, running and failed jobs per queue"
26
- def jobs_status
27
- with_jobs(quiet: true) do
28
- stats = Jobs::Adapters::Database.new.stats
29
- return say("No jobs.") if stats.empty?
30
-
31
- rows = stats.sort.map { |queue, s| [queue, s[:ready], s[:scheduled], s[:running], s[:failed]].map(&:to_s) }
32
- print_table([%w[Queue Ready Scheduled Running Failed], *rows])
33
- end
34
- end
35
-
36
- desc "jobs:failed", "List recently failed jobs"
37
- method_option :limit, type: :numeric, default: 20
38
- def jobs_failed
39
- with_jobs(quiet: true) do
40
- failed = Jobs::Adapters::Database.new.failed(limit: options[:limit])
41
- return say("No failed jobs.") if failed.empty?
42
-
43
- failed.each do |job|
44
- say("##{job[:id]} #{job[:job_class]} (#{job[:queue]}) " \
45
- "attempts=#{job[:attempts]} failed_at=#{job[:failed_at]}")
46
- say(" #{job[:last_error].to_s.lines.first&.strip}")
47
- end
48
- end
49
- end
50
-
51
- desc "jobs:retry [IDS...]", "Put failed jobs back on the queue (all failed jobs without IDS)"
52
- def jobs_retry(*ids)
53
- with_jobs(quiet: true) do
54
- count = Jobs::Adapters::Database.new.retry_failed(ids.empty? ? nil : ids.map(&:to_i))
55
- say("Re-queued #{count} job(s).")
56
- end
57
- end
58
-
59
- desc "jobs:discard [IDS...]", "Delete failed jobs (all failed jobs without IDS)"
60
- def jobs_discard(*ids)
61
- with_jobs(quiet: true) do
62
- count = Jobs::Adapters::Database.new.discard_failed(ids.empty? ? nil : ids.map(&:to_i))
63
- say("Deleted #{count} failed job(s).")
64
- end
65
- end
66
-
67
- desc "jobs:install", "Add the gemstack_jobs table migration (done automatically by `generate job`)"
68
- def jobs_install
69
- JobGenerator.install_migration(Project.root!, output: $stdout)
70
- end
71
-
72
- no_commands do
73
- def with_jobs(quiet: false)
74
- root = Project.ensure_bundle!(self.class.argv)
75
- ENV["GEMSTACK_ENV"] = options[:environment] || ENV["GEMSTACK_ENV"] || "development"
76
- Project.load_config!(root)
77
- unless defined?(GemStack::Jobs)
78
- abort('This app doesn\'t use gemstack-jobs. Add `gem "gemstack-jobs"` to the Gemfile.')
79
- end
80
- config = GemStack.config
81
- config.reload_code = false
82
- if quiet
83
- config.logger.level = :warn
84
- config.db.log_queries = false if config.respond_to?(:db)
85
- end
86
- yield config
87
- rescue Sequel::DatabaseError => e
88
- raise unless e.message.include?("gemstack_jobs")
89
-
90
- abort("The gemstack_jobs table doesn't exist yet. Run `gemstack jobs:install && gemstack db:migrate`.")
91
- rescue Sequel::DatabaseConnectionError => e
92
- abort("Can't connect to the database: #{e.message.lines.first.strip}")
93
- end
94
- end
95
- end
96
- end
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemStack
4
- class CLI < Thor
5
- # `gemstack generate controller Products index show create`
6
- #
7
- # Creates the controller, a test per action and the matching routes.
8
- # Conventional action names map to REST routes; any other action becomes
9
- # GET /<resource>/<action>.
10
- class ControllerGenerator < Generator
11
- REST = {
12
- "index" => ["get", ""], "show" => ["get", "/:id"], "create" => ["post", ""],
13
- "update" => ["patch", "/:id"], "destroy" => ["delete", "/:id"]
14
- }.freeze
15
- ROUTES_BLOCK = /^GemStack\.routes do[ \t]*\n/
16
-
17
- attr_reader :file_name, :class_name, :actions, :url_path
18
-
19
- def initialize(name, actions, root:, output: $stdout, force: false)
20
- super(output: output, force: force)
21
- @root = root
22
- @file_name = Inflector.underscore(name.to_s.delete_suffix("Controller")) # "admin/products"
23
- @class_name = "#{Inflector.camelize(@file_name)}Controller" # "Admin::ProductsController"
24
- @actions = (actions.empty? ? ["index"] : actions).map { |a| Inflector.underscore(a) }.uniq
25
- @url_path = "/#{@file_name.split("/").map { |part| Inflector.dasherize(part) }.join("/")}"
26
- validate!
27
- end
28
-
29
- def run
30
- template_files("controller", override_root: @root).each do |rel, source|
31
- target = File.join(@root, rel.delete_suffix(".tt").gsub("%file_name%", file_name))
32
- write(target, render(File.read(source), source))
33
- end
34
- add_routes
35
- self
36
- end
37
-
38
- def route_for(action)
39
- verb, suffix = REST.fetch(action) { ["get", "/#{Inflector.dasherize(action)}"] }
40
- [verb, url_path + suffix]
41
- end
42
-
43
- # Example path for tests: :id becomes 1.
44
- def request_path(action) = "/api#{route_for(action)[1].sub(":id", "1")}"
45
-
46
- def route_line(action)
47
- verb, path = route_for(action)
48
- %(#{verb} "#{path}", to: "#{file_name}##{action}")
49
- end
50
-
51
- private
52
-
53
- def validate!
54
- invalid = actions.grep_v(/\A[a-z_][a-z0-9_]*\z/)
55
- raise Thor::Error, "Invalid action name(s): #{invalid.join(", ")}" unless invalid.empty?
56
- raise Thor::Error, "Invalid controller name" unless file_name.match?(%r{\A[a-z][a-z0-9_/]*\z})
57
- end
58
-
59
- def add_routes
60
- path = File.join(@root, "config/routes.rb")
61
- return status("skip", path, "not found — add routes manually") unless File.file?(path)
62
-
63
- content = File.read(path)
64
- lines = actions.map { |action| route_line(action) }.reject { |line| content.include?(line) }
65
- return status("identical", path) if lines.empty?
66
- return status("skip", path, "no `GemStack.routes do` block found") unless content.match?(ROUTES_BLOCK)
67
-
68
- File.write(path, content.sub(ROUTES_BLOCK) { |open| open + lines.map { |l| " #{l}\n" }.join })
69
- status("route", path, lines.join("; "))
70
- end
71
- end
72
- end
73
- end
@@ -1,102 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module GemStack
4
- class CLI < Thor
5
- # `gemstack generate deploy` — Dockerfile (api + web targets), compose.yaml
6
- # (Postgres, migrations, API, jobs, Next.js, Caddy), Caddyfile, Procfile and
7
- # .dockerignore. Nothing is deployed anywhere.
8
- class DeployGenerator < Generator
9
- def initialize(root:, output: $stdout, force: false)
10
- super(output: output, force: force)
11
- @root = root
12
- end
13
-
14
- def app_name = File.basename(@root).downcase.gsub(/[^a-z0-9_-]/, "-")
15
- def frontend? = File.file?(File.join(@root, "frontend/package.json"))
16
-
17
- # A worker process is needed for background jobs (and auth's emails).
18
- def jobs?
19
- return true if gemfile.match?(/^\s*gem "gemstack-auth"/)
20
-
21
- gemfile.match?(/^\s*gem "gemstack-jobs"/) && Generator.background_work?(@root)
22
- end
23
-
24
- def mail? = gemfile.match?(/^\s*gem "gemstack-(mail|auth)"/)
25
- def database? = gemfile.match?(/^\s*gem "gemstack-db"/)
26
- def realtime? = gemfile.match?(/^\s*gem "gemstack-realtime"/)
27
-
28
- # From config/database.yml (production, else the first adapter listed);
29
- # apps without one use PostgreSQL, GemStack's original default.
30
- def database_adapter
31
- path = File.join(@root, "config/database.yml")
32
- return "postgresql" unless File.file?(path)
33
-
34
- text = File.read(path)
35
- production = text[/^production:.*?(?=^\S|\z)/m].to_s
36
- (production[/^\s+adapter:\s*(\w+)/, 1] || text[/^\s+adapter:\s*(\w+)/, 1] || "postgresql")
37
- .then { |name| { "postgres" => "postgresql", "sqlite" => "sqlite3", "mysql" => "mysql2" }.fetch(name, name) }
38
- end
39
-
40
- def sqlite? = database_adapter == "sqlite3"
41
- def mysql? = %w[mysql2 trilogy].include?(database_adapter)
42
- def postgresql? = database_adapter == "postgresql"
43
- # A Redis for realtime fan-out when the database can't do it (PostgreSQL uses NOTIFY).
44
- def redis? = realtime? && !postgresql?
45
-
46
- def build_packages
47
- (%w[build-essential libyaml-dev
48
- git] + { "postgresql" => ["libpq-dev"], "mysql2" => ["default-libmysqlclient-dev"] }.fetch(database_adapter,
49
- [])).join(" ")
50
- end
51
-
52
- def runtime_packages
53
- (%w[libyaml-0-2
54
- curl] + { "postgresql" => ["libpq5"], "mysql2" => ["libmariadb3"] }.fetch(database_adapter, [])).join(" ")
55
- end
56
-
57
- def database_url
58
- case database_adapter
59
- when "sqlite3" then "sqlite3:/data/production.sqlite3"
60
- when "postgresql" then "postgres://app:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/app"
61
- else "#{database_adapter}://app:${MYSQL_PASSWORD:?set MYSQL_PASSWORD}@db:3306/app"
62
- end
63
- end
64
-
65
- def ruby_version
66
- pinned = File.file?(File.join(@root, ".ruby-version")) && File.read(File.join(@root, ".ruby-version")).strip
67
- pinned && !pinned.empty? ? pinned.delete_prefix("ruby-") : RUBY_VERSION
68
- end
69
-
70
- def node_version
71
- tools = File.join(@root, ".tool-versions")
72
- pinned = File.file?(tools) && File.read(tools)[/^nodejs\s+(\d+)/, 1]
73
- pinned || "22"
74
- end
75
-
76
- def run
77
- template_files("deploy", override_root: @root).sort.each do |rel, source|
78
- content = File.read(source)
79
- content = render(content, source) if rel.end_with?(".tt")
80
- write(File.join(@root, output_path(rel.delete_suffix(".tt"))), content)
81
- end
82
- keep = File.join(@root, "vendor/.keep")
83
- write(keep, "") unless File.exist?(keep) # the Dockerfile copies vendor/ (e.g. vendor/cache)
84
- warn_about_local_gems
85
- self
86
- end
87
-
88
- private
89
-
90
- def gemfile = @gemfile ||= File.read(File.join(@root, "Gemfile"))
91
-
92
- # Apps generated from a GemStack checkout point at it with an absolute
93
- # path, which doesn't exist inside the image.
94
- def warn_about_local_gems
95
- return unless gemfile.match?(/^path "/)
96
-
97
- status("warning", "Gemfile", "GemStack comes from a local path the image can't see — " \
98
- "run `bundle cache --all` (vendor/cache) or use released gems")
99
- end
100
- end
101
- end
102
- end