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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -25
- data/README.md +6 -17
- data/exe/gemstack +8 -1
- metadata +6 -159
- data/lib/gemstack/cli/add_generator.rb +0 -239
- data/lib/gemstack/cli/app_generator.rb +0 -142
- data/lib/gemstack/cli/commands/db.rb +0 -135
- data/lib/gemstack/cli/commands/jobs.rb +0 -96
- data/lib/gemstack/cli/controller_generator.rb +0 -73
- data/lib/gemstack/cli/deploy_generator.rb +0 -102
- data/lib/gemstack/cli/doctor.rb +0 -308
- data/lib/gemstack/cli/generator.rb +0 -144
- data/lib/gemstack/cli/job_generator.rb +0 -48
- data/lib/gemstack/cli/migration_generator.rb +0 -52
- data/lib/gemstack/cli/policy_generator.rb +0 -35
- data/lib/gemstack/cli/project.rb +0 -53
- data/lib/gemstack/cli/resource_generator.rb +0 -170
- data/lib/gemstack/cli/resource_spec.rb +0 -95
- data/lib/gemstack/cli.rb +0 -304
- data/templates/app/Gemfile.tt +0 -44
- data/templates/app/README.md.tt +0 -29
- data/templates/app/app/controllers/application_controller.rb +0 -6
- data/templates/app/app/jobs/application_job.rb +0 -9
- data/templates/app/app/mailers/application_mailer.rb +0 -8
- data/templates/app/app/mailers/templates/dot_keep +0 -0
- data/templates/app/app/models/application_model.rb +0 -19
- data/templates/app/app/serializers/application_serializer.rb +0 -6
- data/templates/app/bin/gemstack +0 -7
- data/templates/app/config/app.rb.tt +0 -54
- data/templates/app/config/database.yml.tt +0 -61
- data/templates/app/config/environments/development.rb.tt +0 -35
- data/templates/app/config/environments/production.rb.tt +0 -40
- data/templates/app/config/environments/test.rb.tt +0 -21
- data/templates/app/config/puma.rb +0 -21
- data/templates/app/config/routes.rb +0 -10
- data/templates/app/config.ru +0 -6
- data/templates/app/db/migrations/dot_keep +0 -0
- data/templates/app/db/seeds.rb +0 -5
- data/templates/app/dot_env.example.tt +0 -19
- data/templates/app/dot_gitignore +0 -16
- data/templates/app/dot_node-version.tt +0 -1
- data/templates/app/dot_nvmrc.tt +0 -1
- data/templates/app/dot_ruby-version.tt +0 -1
- data/templates/app/dot_tool-versions.tt +0 -4
- data/templates/app/test/health_test.rb +0 -18
- data/templates/app/test/test_helper.rb.tt +0 -25
- data/templates/auth/app/controllers/api_tokens_controller.rb +0 -35
- data/templates/auth/app/controllers/email_verifications_controller.rb +0 -31
- data/templates/auth/app/controllers/password_resets_controller.rb +0 -40
- data/templates/auth/app/controllers/registrations_controller.rb +0 -19
- data/templates/auth/app/controllers/sessions_controller.rb +0 -29
- data/templates/auth/app/mailers/auth_mailer.rb +0 -20
- data/templates/auth/app/mailers/templates/auth_mailer/email_verification.html.erb +0 -2
- data/templates/auth/app/mailers/templates/auth_mailer/email_verification.text.erb +0 -2
- data/templates/auth/app/mailers/templates/auth_mailer/password_reset.html.erb +0 -3
- data/templates/auth/app/mailers/templates/auth_mailer/password_reset.text.erb +0 -6
- data/templates/auth/app/models/auth_token.rb +0 -15
- data/templates/auth/app/models/user.rb +0 -12
- data/templates/auth/app/policies/application_policy.rb +0 -6
- data/templates/auth/app/serializers/api_token_serializer.rb +0 -6
- data/templates/auth/app/serializers/new_api_token_serializer.rb +0 -9
- data/templates/auth/app/serializers/user_serializer.rb +0 -6
- data/templates/auth/db/migrations/%timestamp%_create_auth_tables.rb +0 -41
- data/templates/auth/frontend/app/account/page.tsx +0 -116
- data/templates/auth/frontend/app/forgot-password/page.tsx +0 -48
- data/templates/auth/frontend/app/login/page.tsx +0 -26
- data/templates/auth/frontend/app/reset-password/page.tsx +0 -11
- data/templates/auth/frontend/app/signup/page.tsx +0 -27
- data/templates/auth/frontend/app/verify-email/page.tsx +0 -11
- data/templates/auth/frontend/components/auth/CredentialsForm.tsx +0 -58
- data/templates/auth/frontend/components/auth/ResetPasswordForm.tsx +0 -43
- data/templates/auth/frontend/components/auth/VerifyEmail.tsx +0 -28
- data/templates/auth/frontend/lib/auth.ts +0 -113
- data/templates/auth/test/controllers/auth_test.rb +0 -108
- data/templates/controller/app/controllers/%file_name%_controller.rb.tt +0 -10
- data/templates/controller/test/controllers/%file_name%_controller_test.rb.tt +0 -14
- data/templates/deploy/Caddyfile.tt +0 -19
- data/templates/deploy/Dockerfile.tt +0 -63
- data/templates/deploy/Procfile.tt +0 -6
- data/templates/deploy/compose.yaml.tt +0 -129
- data/templates/deploy/dot_dockerignore +0 -14
- data/templates/frontend/app/globals.css +0 -171
- data/templates/frontend/app/layout.tsx.tt +0 -19
- data/templates/frontend/app/page.module.css +0 -315
- data/templates/frontend/app/page.tsx.tt +0 -183
- data/templates/frontend/app/providers.tsx +0 -24
- data/templates/frontend/lib/gemstack/client.ts +0 -125
- data/templates/frontend/next-env.d.ts +0 -5
- data/templates/frontend/next.config.ts +0 -17
- data/templates/frontend/package.json.tt +0 -23
- data/templates/frontend/tsconfig.json +0 -21
- data/templates/job/app/jobs/%file_name%.rb.tt +0 -15
- data/templates/job/test/jobs/%file_name%_test.rb.tt +0 -15
- data/templates/migration/db/migrations/%timestamp%_%file_name%.rb.tt +0 -17
- data/templates/policy/app/policies/%file_name%_policy.rb.tt +0 -21
- data/templates/policy/test/policies/%file_name%_policy_test.rb.tt +0 -9
- data/templates/realtime/config/channels.rb +0 -14
- data/templates/realtime/frontend/lib/gemstack/realtime.ts +0 -120
- data/templates/resource/controller/app/controllers/%plural%_controller.rb.tt +0 -51
- data/templates/resource/controller/test/controllers/%plural%_controller_test.rb.tt +0 -74
- data/templates/resource/frontend/frontend/app/%url_segment%/[id]/edit/page.tsx.tt +0 -45
- data/templates/resource/frontend/frontend/app/%url_segment%/[id]/page.tsx.tt +0 -49
- data/templates/resource/frontend/frontend/app/%url_segment%/new/page.tsx.tt +0 -27
- data/templates/resource/frontend/frontend/app/%url_segment%/page.tsx.tt +0 -51
- data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Card.tsx.tt +0 -15
- data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Form.tsx.tt +0 -98
- data/templates/resource/frontend/frontend/components/%url_segment%/%class_name%Table.tsx.tt +0 -36
- data/templates/resource/frontend/frontend/lib/format.ts +0 -12
- data/templates/resource/frontend/frontend/lib/queries/%url_segment%.ts.tt +0 -64
- data/templates/resource/migration/db/migrations/%timestamp%_create_%table%.rb.tt +0 -18
- data/templates/resource/model/app/models/%file_name%.rb.tt +0 -15
- data/templates/resource/model/test/models/%file_name%_test.rb.tt +0 -29
- data/templates/resource/serializer/app/serializers/%file_name%_serializer.rb.tt +0 -7
- data/templates/storage/app/controllers/uploads_controller.rb.tt +0 -33
- data/templates/storage/app/serializers/upload_serializer.rb +0 -11
- data/templates/storage/frontend/lib/upload.ts +0 -52
- 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
|