gemstack 0.3.5 → 0.3.6
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 +4 -0
- data/lib/gemstack/cli/app_generator.rb +5 -3
- data/lib/gemstack/cli/deploy_generator.rb +5 -1
- data/lib/gemstack/cli/destroy_generator/kinds.rb +68 -0
- data/lib/gemstack/cli/destroy_generator.rb +39 -28
- data/lib/gemstack/cli/generation_manifest.rb +3 -1
- data/lib/gemstack/cli/generator.rb +6 -3
- data/lib/gemstack/cli/job_generator.rb +14 -5
- data/lib/gemstack/cli/migration_generator.rb +4 -1
- data/lib/gemstack/cli/policy_generator.rb +4 -1
- data/lib/gemstack/cli/template_update.rb +299 -0
- data/lib/gemstack/cli/update_generator.rb +18 -2
- data/lib/gemstack/cli.rb +24 -6
- data/lib/gemstack/version.rb +1 -1
- data/templates/app/dot_gemstack/version.tt +1 -0
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3653be40ea53434b0176c572f0ecdba840c2af0b639a7d4ed64a9117037c72f4
|
|
4
|
+
data.tar.gz: 2a6fe58ab592b81ee4ecd2bbe794a9e6e94f1b693afa6963725f6ac1a0719f93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0059eae0a47937b5984e631adfd2430e0b261845f4d6165f3288d269b63cd3d9601b89b4b0efbde33933e678128350c4bdab767ed7cf8927d0e2272691beaee
|
|
7
|
+
data.tar.gz: 169b95e87f103a9d21a07e60b51da63da756381bdc541d9e57abf0224454e364d4d1d64c931398f3f702b545f4f5463be30eeefe7468ec899651ac1c63c04d8d
|
data/CHANGELOG.md
CHANGED
|
@@ -28,16 +28,18 @@ module GemStack
|
|
|
28
28
|
@module_name = Inflector.camelize(@name.tr("-", "_"))
|
|
29
29
|
@gemstack_path = options[:gemstack_path] ? File.expand_path(options[:gemstack_path]) : detect_checkout
|
|
30
30
|
@api_path = "/api"
|
|
31
|
+
self.templates = options[:templates] if options[:templates]
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
# (gemstack update renders older releases' templates with their own values.)
|
|
35
|
+
def version = @options[:version] || GemStack::VERSION
|
|
34
36
|
|
|
35
37
|
# Pins the app to the Ruby that generated it, in files every version
|
|
36
38
|
# manager reads (.ruby-version: rbenv, rvm, chruby, asdf, mise;
|
|
37
39
|
# .tool-versions: asdf, mise), and Node.js likewise (.node-version,
|
|
38
40
|
# .nvmrc, .tool-versions).
|
|
39
|
-
def ruby_version = RUBY_VERSION
|
|
40
|
-
def node_version = @node_version ||= Dev::Toolchain.pinned_node_version
|
|
41
|
+
def ruby_version = @options[:ruby_version] || RUBY_VERSION
|
|
42
|
+
def node_version = @node_version ||= @options[:node_version] || Dev::Toolchain.pinned_node_version
|
|
41
43
|
def frontend? = !@options[:skip_frontend]
|
|
42
44
|
def database? = !@options[:skip_database]
|
|
43
45
|
|
|
@@ -77,10 +77,12 @@ module GemStack
|
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def run
|
|
80
|
+
@manifest = GenerationManifest.new(@root)
|
|
80
81
|
template_files("deploy", override_root: @root).sort.each do |rel, source|
|
|
81
82
|
content = File.read(source)
|
|
82
83
|
content = render(content, source) if rel.end_with?(".tt")
|
|
83
|
-
|
|
84
|
+
write_tracked(File.join(@root, output_path(rel.delete_suffix(".tt"))), content,
|
|
85
|
+
owner: "deploy:app", mode: File.stat(source).mode)
|
|
84
86
|
end
|
|
85
87
|
keep = File.join(@root, "vendor/.keep")
|
|
86
88
|
write(keep, "") unless File.exist?(keep) # the Dockerfile copies vendor/ (e.g. vendor/cache)
|
|
@@ -88,6 +90,8 @@ module GemStack
|
|
|
88
90
|
enable_standalone_frontend if frontend?
|
|
89
91
|
warn_about_local_gems
|
|
90
92
|
self
|
|
93
|
+
ensure
|
|
94
|
+
@manifest&.save
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
private
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GemStack
|
|
4
|
+
class CLI < Thor
|
|
5
|
+
class DestroyGenerator < Generator
|
|
6
|
+
# What each kind owns, and what `destroy job|policy|deploy` checks beyond
|
|
7
|
+
# files and routes.
|
|
8
|
+
module Kinds
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def owners_for(name)
|
|
12
|
+
case @kind
|
|
13
|
+
when "controller" then return ["controller:#{ControllerGenerator.new(name, [], root: @root).file_name}"]
|
|
14
|
+
when "job"
|
|
15
|
+
@constant = JobGenerator.class_name_for(name)
|
|
16
|
+
return ["job:#{Inflector.underscore(@constant)}"]
|
|
17
|
+
when "policy"
|
|
18
|
+
policy = PolicyGenerator.new(name, root: @root)
|
|
19
|
+
@constant = policy.class_name
|
|
20
|
+
return ["policy:#{policy.file_name}"]
|
|
21
|
+
when "migration"
|
|
22
|
+
file_name = Inflector.underscore(name.to_s)
|
|
23
|
+
raise Thor::Error, "Invalid migration name #{name.inspect}" unless file_name.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
24
|
+
|
|
25
|
+
return ["migration:#{file_name}"]
|
|
26
|
+
when "deploy" then return ["deploy:app"]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@spec = ResourceSpec.new(name)
|
|
30
|
+
owners = ["model:#{@spec.file_name}"]
|
|
31
|
+
owners += ["controller:#{@spec.plural}", "resource:#{@spec.file_name}"] if @kind == "resource"
|
|
32
|
+
owners
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# `gemstack generate deploy` adds exactly DeployGenerator::KAMAL_GEM; that
|
|
36
|
+
# line is removed, an edited one is left for the user.
|
|
37
|
+
def plan_gemfile
|
|
38
|
+
path = @manifest.absolute("Gemfile")
|
|
39
|
+
@gemfile_content = File.file?(path) ? File.read(path, encoding: "UTF-8") : ""
|
|
40
|
+
lines = @gemfile_content.lines
|
|
41
|
+
return unless lines.include?(DeployGenerator::KAMAL_GEM)
|
|
42
|
+
|
|
43
|
+
@gemfile_after = "#{lines.reject { |line| line == DeployGenerator::KAMAL_GEM }.join.rstrip}\n"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A job or policy that other code still names would break it at runtime.
|
|
47
|
+
def check_constant_references
|
|
48
|
+
Dir.glob(File.join(@root, "{app,config,lib}/**/*.rb")).each do |absolute|
|
|
49
|
+
relative = Pathname.new(absolute).relative_path_from(Pathname.new(@root)).to_s
|
|
50
|
+
next if @files.key?(relative)
|
|
51
|
+
|
|
52
|
+
@manifest.absolute(relative)
|
|
53
|
+
tokens = Ripper.lex(File.read(absolute, encoding: "UTF-8"))
|
|
54
|
+
next unless tokens.any? { |_, type, text, _| type == :on_const && text == @constant.split("::").last }
|
|
55
|
+
|
|
56
|
+
@conflicts << "Remaining Ruby reference to #{@constant}: #{relative}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def verify_gemfile!
|
|
61
|
+
return if File.read(@manifest.absolute("Gemfile"), encoding: "UTF-8") == @gemfile_content
|
|
62
|
+
|
|
63
|
+
raise Thor::Error, "Gemfile changed during confirmation; run destroy again"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ripper"
|
|
4
|
+
require_relative "destroy_generator/kinds"
|
|
4
5
|
|
|
5
6
|
module GemStack
|
|
6
7
|
class CLI < Thor
|
|
7
8
|
# Plans the complete operation before changing anything. Only migrations
|
|
8
9
|
# confirmed pending in the selected environment are eligible for removal.
|
|
9
10
|
class DestroyGenerator < Generator
|
|
10
|
-
|
|
11
|
+
include Kinds
|
|
12
|
+
|
|
13
|
+
KINDS = %w[model controller resource job policy migration deploy].freeze
|
|
14
|
+
# Kinds that change the API, so the TypeScript contract is regenerated after them.
|
|
15
|
+
CONTRACT_KINDS = %w[model controller resource].freeze
|
|
16
|
+
# What `gemstack generate deploy` writes at the app's root.
|
|
17
|
+
DEPLOY_FILES = %w[Dockerfile .dockerignore config/deploy.yml .kamal/secrets bin/docker-entrypoint].freeze
|
|
11
18
|
SHARED = (Generator::BASE_CLASSES.values + %w[
|
|
12
19
|
app/controllers/application_controller.rb test/test_helper.rb
|
|
13
20
|
frontend/lib/format.ts frontend/lib/gemstack/client.ts
|
|
@@ -36,7 +43,7 @@ module GemStack
|
|
|
36
43
|
environment: options[:environment]
|
|
37
44
|
)
|
|
38
45
|
removal.run { options[:yes] || confirm?(cli) }
|
|
39
|
-
yield root if removal.
|
|
46
|
+
yield root if removal.contract_affected? && !options[:skip_contract]
|
|
40
47
|
end
|
|
41
48
|
|
|
42
49
|
def self.confirm?(cli)
|
|
@@ -48,8 +55,9 @@ module GemStack
|
|
|
48
55
|
def initialize(kind, name, root:, dry_run: false, force: false, remove_migrations: false,
|
|
49
56
|
output: $stdout, environment: nil)
|
|
50
57
|
super(output: output, force: force)
|
|
51
|
-
unless KINDS.include?(kind) && name && !name.empty?
|
|
52
|
-
raise Thor::Error, "Usage: gemstack destroy model|controller|resource NAME"
|
|
58
|
+
unless KINDS.include?(kind) && (kind == "deploy" || (name && !name.empty?))
|
|
59
|
+
raise Thor::Error, "Usage: gemstack destroy model|controller|resource|job|policy|migration NAME, " \
|
|
60
|
+
"or gemstack destroy deploy"
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
@root = File.expand_path(root)
|
|
@@ -65,12 +73,13 @@ module GemStack
|
|
|
65
73
|
end
|
|
66
74
|
|
|
67
75
|
def changed? = @changed
|
|
76
|
+
def contract_affected? = @changed && CONTRACT_KINDS.include?(@kind)
|
|
68
77
|
|
|
69
78
|
def run
|
|
70
79
|
plan!
|
|
71
80
|
preview
|
|
72
81
|
raise Thor::Error, "Nothing removed:\n #{@conflicts.join("\n ")}" unless @conflicts.empty?
|
|
73
|
-
return self if @dry_run || (@files.empty? && @routes.empty?)
|
|
82
|
+
return self if @dry_run || (@files.empty? && @routes.empty? && !@gemfile_after)
|
|
74
83
|
|
|
75
84
|
if block_given? && !yield
|
|
76
85
|
@output.puts("Cancelled; no files changed.")
|
|
@@ -84,18 +93,6 @@ module GemStack
|
|
|
84
93
|
|
|
85
94
|
private
|
|
86
95
|
|
|
87
|
-
def owners_for(name)
|
|
88
|
-
if @kind == "controller"
|
|
89
|
-
controller = ControllerGenerator.new(name, [], root: @root)
|
|
90
|
-
return ["controller:#{controller.file_name}"]
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
@spec = ResourceSpec.new(name)
|
|
94
|
-
owners = ["model:#{@spec.file_name}"]
|
|
95
|
-
owners += ["controller:#{@spec.plural}", "resource:#{@spec.file_name}"] if @kind == "resource"
|
|
96
|
-
owners
|
|
97
|
-
end
|
|
98
|
-
|
|
99
96
|
def plan!
|
|
100
97
|
@conflicts = []
|
|
101
98
|
selected = @manifest.files.select { |_, entry| @owners.include?(entry["owner"]) }
|
|
@@ -105,13 +102,15 @@ module GemStack
|
|
|
105
102
|
plan_migrations
|
|
106
103
|
@files.each { |path, entry| check_file(path, entry) }
|
|
107
104
|
plan_routes
|
|
105
|
+
plan_gemfile if @kind == "deploy"
|
|
108
106
|
check_dependencies if @spec && @files.key?("app/models/#{@spec.file_name}.rb")
|
|
107
|
+
check_constant_references if @constant && @files.any?
|
|
109
108
|
end
|
|
110
109
|
|
|
111
110
|
def check_file(path, entry)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
allowed = path.start_with?("app/", "test/", "frontend/", "db/migrations/") ||
|
|
112
|
+
(@kind == "deploy" && DEPLOY_FILES.include?(path))
|
|
113
|
+
raise Thor::Error, "Refusing to remove shared or unsupported path: #{path}" if SHARED.include?(path) || !allowed
|
|
115
114
|
|
|
116
115
|
absolute = @manifest.absolute(path)
|
|
117
116
|
raise Thor::Error, "Expected a generated file: #{path}" if File.exist?(absolute) && !File.file?(absolute)
|
|
@@ -226,8 +225,11 @@ module GemStack
|
|
|
226
225
|
status(@snapshots[path] ? "remove" : "missing", path)
|
|
227
226
|
end
|
|
228
227
|
@routes.each { |entry| status("unroute", "config/routes.rb", entry["line"].strip) }
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
status("ungem", "Gemfile", "kamal") if @gemfile_after
|
|
229
|
+
if @files.empty? && @routes.empty? && !@gemfile_after
|
|
230
|
+
@output.puts("No tracked files to remove. Code generated before ownership tracking (resources, models " \
|
|
231
|
+
"and controllers before 0.3.4; jobs, policies, migrations and deploy files before 0.3.6) " \
|
|
232
|
+
"needs manual removal.")
|
|
231
233
|
end
|
|
232
234
|
@output.puts("Dry run; no files or API contracts changed.") if @dry_run
|
|
233
235
|
end
|
|
@@ -235,15 +237,22 @@ module GemStack
|
|
|
235
237
|
# Recheck after interactive confirmation, before the first mutation.
|
|
236
238
|
def verify_snapshot!
|
|
237
239
|
@manifest.verify!
|
|
240
|
+
verify_migrations!
|
|
241
|
+
verify_files!
|
|
242
|
+
verify_routes!
|
|
243
|
+
verify_gemfile! if @gemfile_after
|
|
244
|
+
end
|
|
238
245
|
|
|
246
|
+
def verify_migrations!
|
|
239
247
|
if @pending && !@pending.empty? && @migration_status.applied != @applied
|
|
240
248
|
raise Thor::Error, "Migration state changed or became unavailable; run destroy again"
|
|
241
249
|
end
|
|
250
|
+
return unless @auto_removed_migrations&.any? { |path, _| @migration_git_status.committed(path) != false }
|
|
242
251
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
end
|
|
252
|
+
raise Thor::Error, "Migration Git state changed; run destroy again"
|
|
253
|
+
end
|
|
246
254
|
|
|
255
|
+
def verify_files!
|
|
247
256
|
@snapshots.each do |relative, original|
|
|
248
257
|
path = @manifest.absolute(relative)
|
|
249
258
|
current = File.file?(path) ? Digest::SHA256.file(path).hexdigest : nil
|
|
@@ -252,12 +261,12 @@ module GemStack
|
|
|
252
261
|
"File changed during confirmation: #{relative}; run destroy again"
|
|
253
262
|
end
|
|
254
263
|
end
|
|
264
|
+
end
|
|
255
265
|
|
|
266
|
+
def verify_routes!
|
|
256
267
|
path = @manifest.absolute("config/routes.rb")
|
|
257
268
|
current = File.file?(path) ? File.read(path, encoding: "UTF-8") : ""
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
raise Thor::Error, "Routes changed during confirmation; run destroy again"
|
|
269
|
+
raise Thor::Error, "Routes changed during confirmation; run destroy again" unless current == @route_content
|
|
261
270
|
end
|
|
262
271
|
|
|
263
272
|
def apply
|
|
@@ -266,10 +275,12 @@ module GemStack
|
|
|
266
275
|
File.delete(path) if File.file?(path)
|
|
267
276
|
end
|
|
268
277
|
File.write(@manifest.absolute("config/routes.rb"), @route_after) unless @routes.empty?
|
|
278
|
+
File.write(@manifest.absolute("Gemfile"), @gemfile_after) if @gemfile_after
|
|
269
279
|
@manifest.forget(@files.keys, @routes)
|
|
270
280
|
@manifest.save
|
|
271
281
|
@changed = true
|
|
272
282
|
@output.puts("Removed tracked code. Database data was not changed.")
|
|
283
|
+
@output.puts("Run bundle install to update Gemfile.lock.") if @gemfile_after
|
|
273
284
|
end
|
|
274
285
|
end
|
|
275
286
|
end
|
|
@@ -12,6 +12,8 @@ module GemStack
|
|
|
12
12
|
class GenerationManifest
|
|
13
13
|
PATH = ".gemstack/generators.json"
|
|
14
14
|
VERSION = 1
|
|
15
|
+
# "<generator>:<name>", e.g. "model:post", "job:send_digest", "deploy:app".
|
|
16
|
+
OWNER = %r{\A(?:model|controller|resource|job|policy|migration|deploy):[a-z][a-z0-9_]*(?:/[a-z][a-z0-9_]*)*\z}
|
|
15
17
|
|
|
16
18
|
attr_reader :files, :routes
|
|
17
19
|
|
|
@@ -115,7 +117,7 @@ module GemStack
|
|
|
115
117
|
end
|
|
116
118
|
|
|
117
119
|
def owner?(owner)
|
|
118
|
-
owner.is_a?(String) && owner.match?(
|
|
120
|
+
owner.is_a?(String) && owner.match?(OWNER)
|
|
119
121
|
end
|
|
120
122
|
end
|
|
121
123
|
end
|
|
@@ -23,7 +23,10 @@ module GemStack
|
|
|
23
23
|
@created = []
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# Another GemStack version's templates/ (gemstack update compares them).
|
|
27
|
+
attr_writer :templates
|
|
28
|
+
|
|
29
|
+
def template_root(name) = File.join(@templates || TEMPLATES, name)
|
|
27
30
|
|
|
28
31
|
# Ruby core/stdlib names an app class must not reuse (a top-level class
|
|
29
32
|
# named Digest or Set would collide with Ruby's own). Constants already
|
|
@@ -141,11 +144,11 @@ module GemStack
|
|
|
141
144
|
true
|
|
142
145
|
end
|
|
143
146
|
|
|
144
|
-
def write_tracked(path, content, owner:)
|
|
147
|
+
def write_tracked(path, content, owner:, mode: nil)
|
|
145
148
|
relative = Pathname.new(File.expand_path(path)).relative_path_from(Pathname.new(File.expand_path(@root))).to_s
|
|
146
149
|
@manifest.absolute(relative)
|
|
147
150
|
existed = File.exist?(path)
|
|
148
|
-
return unless write(path, content)
|
|
151
|
+
return unless write(path, content, mode: mode)
|
|
149
152
|
|
|
150
153
|
@manifest.record_file(path, owner: owner, existed: existed)
|
|
151
154
|
end
|
|
@@ -22,26 +22,35 @@ module GemStack
|
|
|
22
22
|
generator.write(File.join(root, "db/migrations/#{stamp}_create_gemstack_jobs.rb"), Jobs::Migration::SOURCE)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@root = root
|
|
25
|
+
# The job class for NAME: "send_digest" → "SendDigest", "DigestJob" stays.
|
|
26
|
+
def self.class_name_for(name)
|
|
28
27
|
base = Inflector.camelize(name.to_s).delete_suffix("Job")
|
|
29
28
|
raise Thor::Error, "Invalid job name #{name.inspect}" unless base.match?(/\A[A-Z][A-Za-z0-9]*\z/)
|
|
30
29
|
|
|
31
|
-
|
|
30
|
+
name.to_s.end_with?("Job") ? Inflector.camelize(name.to_s) : base
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(name, root:, queue: nil, output: $stdout, force: false)
|
|
34
|
+
super(output: output, force: force)
|
|
35
|
+
@root = root
|
|
36
|
+
@class_name = self.class.class_name_for(name)
|
|
32
37
|
@file_name = Inflector.underscore(@class_name)
|
|
33
38
|
@queue_name = queue
|
|
34
39
|
Generator.check_constant!(@class_name, suggestion: "#{@class_name}Job") unless name == "Placeholder"
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def run
|
|
43
|
+
@manifest = GenerationManifest.new(@root)
|
|
38
44
|
ensure_base_classes(@root, :job)
|
|
39
45
|
template_files("job", override_root: @root).each do |rel, source|
|
|
40
46
|
target = File.join(@root, rel.delete_suffix(".tt").gsub("%file_name%", file_name))
|
|
41
|
-
|
|
47
|
+
write_tracked(target, render(File.read(source), source), owner: "job:#{file_name}")
|
|
42
48
|
end
|
|
49
|
+
# The jobs table is shared by every job: not tracked, so destroy keeps it.
|
|
43
50
|
self.class.install_migration(@root, output: @output) if File.directory?(File.join(@root, "db"))
|
|
44
51
|
self
|
|
52
|
+
ensure
|
|
53
|
+
@manifest&.save
|
|
45
54
|
end
|
|
46
55
|
end
|
|
47
56
|
end
|
|
@@ -26,11 +26,14 @@ module GemStack
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def run
|
|
29
|
+
@manifest = GenerationManifest.new(@root)
|
|
29
30
|
template_files("migration", override_root: @root).each do |rel, source|
|
|
30
31
|
path = rel.delete_suffix(".tt").gsub("%timestamp%", timestamp).gsub("%file_name%", file_name)
|
|
31
|
-
|
|
32
|
+
write_tracked(File.join(@root, path), render(File.read(source), source), owner: "migration:#{file_name}")
|
|
32
33
|
end
|
|
33
34
|
self
|
|
35
|
+
ensure
|
|
36
|
+
@manifest&.save
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
private
|
|
@@ -24,11 +24,14 @@ module GemStack
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def run
|
|
27
|
+
@manifest = GenerationManifest.new(@root)
|
|
27
28
|
template_files("policy", override_root: @root).each do |rel, source|
|
|
28
29
|
target = File.join(@root, rel.delete_suffix(".tt").gsub("%file_name%", file_name))
|
|
29
|
-
|
|
30
|
+
write_tracked(target, render(File.read(source), source), owner: "policy:#{file_name}")
|
|
30
31
|
end
|
|
31
32
|
self
|
|
33
|
+
ensure
|
|
34
|
+
@manifest&.save
|
|
32
35
|
end
|
|
33
36
|
end
|
|
34
37
|
end
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "rubygems/package"
|
|
8
|
+
require "stringio"
|
|
9
|
+
require "tempfile"
|
|
10
|
+
require "tmpdir"
|
|
11
|
+
|
|
12
|
+
module GemStack
|
|
13
|
+
class CLI < Thor
|
|
14
|
+
# Finds the templates/ directory of a GemStack version: this one, an
|
|
15
|
+
# installed gem, or the gem downloaded from rubygems.org.
|
|
16
|
+
module TemplateSource
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Releases since templates took their current layout (0.3.0).
|
|
20
|
+
FIRST = Gem::Version.new("0.3.0")
|
|
21
|
+
|
|
22
|
+
def locate(version, cache: Dir.mktmpdir("gemstack-templates"))
|
|
23
|
+
return Generator::TEMPLATES if version == GemStack::VERSION
|
|
24
|
+
|
|
25
|
+
installed(version) || download(version, cache)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Every release before VERSION (rubygems.org's list, or the installed ones offline).
|
|
29
|
+
def releases_before(version)
|
|
30
|
+
limit = Gem::Version.new(version)
|
|
31
|
+
names = published_versions || installed_versions
|
|
32
|
+
names.map { |name| Gem::Version.new(name) }
|
|
33
|
+
.select { |v| !v.prerelease? && v >= FIRST && v < limit }.sort.map(&:to_s)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def published_versions
|
|
37
|
+
response = Net::HTTP.get_response(URI("https://rubygems.org/api/v1/versions/gemstack.json"))
|
|
38
|
+
JSON.parse(response.body).map { |release| release.fetch("number") } if response.is_a?(Net::HTTPSuccess)
|
|
39
|
+
rescue StandardError
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def installed_versions
|
|
44
|
+
gem_dirs.flat_map { |dir| Dir[File.join(dir, "gems", "gemstack-*", "templates")] }
|
|
45
|
+
.filter_map { |path| path[%r{gemstack-(\d+\.\d+\.\d+)/templates\z}, 1] }.uniq
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def gem_dirs = (Gem.path + [(Bundler.bundle_path.to_s if defined?(Bundler))]).compact.uniq
|
|
49
|
+
|
|
50
|
+
def installed(version)
|
|
51
|
+
gem_dirs.map { |dir| File.join(dir, "gems", "gemstack-#{version}", "templates") }
|
|
52
|
+
.find { |path| File.directory?(path) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def download(version, cache)
|
|
56
|
+
cache = File.join(cache, version)
|
|
57
|
+
FileUtils.mkdir_p(cache)
|
|
58
|
+
response = Net::HTTP.get_response(URI("https://rubygems.org/downloads/gemstack-#{version}.gem"))
|
|
59
|
+
return unless response.is_a?(Net::HTTPSuccess)
|
|
60
|
+
|
|
61
|
+
gem = File.join(cache, "gemstack-#{version}.gem")
|
|
62
|
+
File.binwrite(gem, response.body)
|
|
63
|
+
Gem::Package.new(gem).extract_files(cache, "templates/**/*")
|
|
64
|
+
templates = File.join(cache, "templates")
|
|
65
|
+
templates if File.directory?(templates)
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The template half of `gemstack update`: brings the files `gemstack new`
|
|
72
|
+
# wrote into line with this GemStack version. Each file is compared three
|
|
73
|
+
# ways — the app's copy, the old version's template and this version's,
|
|
74
|
+
# both rendered for this app — so only files a release changed are touched:
|
|
75
|
+
#
|
|
76
|
+
# template unchanged skipped (your edits are yours)
|
|
77
|
+
# new in this version created
|
|
78
|
+
# you never edited the file updated
|
|
79
|
+
# you edited it you choose: overwrite, skip, diff, or save as FILE.new
|
|
80
|
+
#
|
|
81
|
+
# Apps record the templates they're on in .gemstack/version. Older apps don't,
|
|
82
|
+
# so each file is compared with every earlier release's template: matching
|
|
83
|
+
# one means you never edited it (it's just out of date). Without any old
|
|
84
|
+
# templates (offline), files that differ are offered, never updated.
|
|
85
|
+
class TemplateUpdate
|
|
86
|
+
VERSION_FILE = ".gemstack/version"
|
|
87
|
+
# Managed elsewhere: the Gemfile by `gemstack update`/Bundler.
|
|
88
|
+
MANAGED = %w[Gemfile .gemstack/version].freeze
|
|
89
|
+
|
|
90
|
+
# created/updated/saved (as FILE.new) are done; conflicts were left for you;
|
|
91
|
+
# removed are files you deleted, which stay deleted.
|
|
92
|
+
Result = Struct.new(:created, :updated, :saved, :conflicts, :removed, keyword_init: true)
|
|
93
|
+
|
|
94
|
+
def self.recorded_version(root)
|
|
95
|
+
path = File.join(root, VERSION_FILE)
|
|
96
|
+
File.read(path).strip if File.file?(path)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def initialize(root:, from: nil, output: $stdout, dry_run: false, interactive: $stdin.tty?, prompt: nil,
|
|
100
|
+
old_templates: nil)
|
|
101
|
+
@root = root
|
|
102
|
+
@from = from || self.class.recorded_version(root)
|
|
103
|
+
@output = output
|
|
104
|
+
@dry_run = dry_run
|
|
105
|
+
@interactive = interactive && !dry_run
|
|
106
|
+
@prompt = prompt || lambda { |question|
|
|
107
|
+
@output.print(" #{question} ")
|
|
108
|
+
$stdin.gets.to_s.strip.downcase
|
|
109
|
+
}
|
|
110
|
+
@old_templates = old_templates
|
|
111
|
+
@result = Result.new(created: [], updated: [], saved: [], conflicts: [], removed: [])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def run
|
|
115
|
+
new_tree = render(Generator::TEMPLATES, version: GemStack::VERSION)
|
|
116
|
+
old_trees = old_trees_for_comparison
|
|
117
|
+
new_tree.each { |path, file| apply(path, file, old_trees.map { |tree| tree[path]&.content }) }
|
|
118
|
+
report
|
|
119
|
+
record_version unless @dry_run || @result.conflicts.any?
|
|
120
|
+
@result
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
AppFile = Struct.new(:content, :mode)
|
|
126
|
+
|
|
127
|
+
# The renders to compare with: the recorded/given version's, or every
|
|
128
|
+
# earlier release's when the app doesn't say which it came from.
|
|
129
|
+
def old_trees_for_comparison
|
|
130
|
+
trees = if @old_templates
|
|
131
|
+
Array(@old_templates).map { |templates| render(templates, version: GemStack::VERSION) }
|
|
132
|
+
else
|
|
133
|
+
release_trees(@from ? [@from] : TemplateSource.releases_before(GemStack::VERSION))
|
|
134
|
+
end
|
|
135
|
+
describe(trees)
|
|
136
|
+
trees
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def release_trees(versions)
|
|
140
|
+
@versions = versions
|
|
141
|
+
Dir.mktmpdir("gemstack-templates") do |cache|
|
|
142
|
+
versions.filter_map do |version|
|
|
143
|
+
source = TemplateSource.locate(version, cache: cache)
|
|
144
|
+
render(source, version: version) if source
|
|
145
|
+
rescue Thor::Error
|
|
146
|
+
nil # an old template this version can't render: compare without it
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def describe(trees)
|
|
152
|
+
if trees.empty?
|
|
153
|
+
@output.puts("Templates: earlier GemStack templates aren't available, so files that differ are " \
|
|
154
|
+
"offered, never updated automatically")
|
|
155
|
+
elsif @from
|
|
156
|
+
@output.puts("Templates: comparing GemStack #{@from}'s with #{GemStack::VERSION}'s")
|
|
157
|
+
else
|
|
158
|
+
range = @versions&.any? ? " (#{@versions.first} to #{@versions.last})" : ""
|
|
159
|
+
@output.puts("Templates: the app doesn't record its templates version, so files are compared with " \
|
|
160
|
+
"every earlier release#{range}")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Renders `gemstack new` for this app (its name, database, frontend, pinned
|
|
165
|
+
# Ruby and Node.js) from a release's templates, as that release wrote them.
|
|
166
|
+
def render(templates, version:)
|
|
167
|
+
Dir.mktmpdir("gemstack-render") do |dir|
|
|
168
|
+
name = File.basename(@root).downcase.gsub(/[^a-z0-9_-]/, "-").sub(/\A[^a-z]+/, "")
|
|
169
|
+
destination = File.join(dir, name.empty? ? "app" : name)
|
|
170
|
+
options = app_options.merge(templates: templates, version: version)
|
|
171
|
+
AppGenerator.new(destination, options, output: StringIO.new).run
|
|
172
|
+
Dir.glob("**/*", File::FNM_DOTMATCH, base: destination).sort.filter_map do |rel|
|
|
173
|
+
full = File.join(destination, rel)
|
|
174
|
+
next unless File.file?(full) && !MANAGED.include?(rel)
|
|
175
|
+
|
|
176
|
+
[rel, AppFile.new(File.binread(full), File.stat(full).mode)]
|
|
177
|
+
end.to_h
|
|
178
|
+
end
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
raise Thor::Error, "Couldn't render the app templates (#{e.class}: #{e.message.lines.first&.strip})"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def app_options
|
|
184
|
+
database_yml = File.join(@root, "config/database.yml")
|
|
185
|
+
adapter = File.file?(database_yml) && File.read(database_yml)[/^\s+adapter:\s*(\w+)/, 1]
|
|
186
|
+
{
|
|
187
|
+
skip_install: true, skip_git: true,
|
|
188
|
+
skip_frontend: !File.file?(File.join(@root, "frontend/package.json")),
|
|
189
|
+
skip_database: !adapter, database: adapter || nil,
|
|
190
|
+
ruby_version: pinned(".ruby-version")&.delete_prefix("ruby-"),
|
|
191
|
+
node_version: pinned(".nvmrc") || pinned(".node-version")
|
|
192
|
+
}.compact
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def pinned(file)
|
|
196
|
+
path = File.join(@root, file)
|
|
197
|
+
File.read(path).strip.delete_prefix("v").then { |value| value unless value.empty? } if File.file?(path)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# olds: this file's content in each compared release's render (nil where absent).
|
|
201
|
+
def apply(path, file, olds)
|
|
202
|
+
mine_path = File.join(@root, path)
|
|
203
|
+
mine = File.binread(mine_path) if File.file?(mine_path)
|
|
204
|
+
return if mine == file.content
|
|
205
|
+
return if olds.any? && olds.all?(file.content) # no release changed it: your edits are yours
|
|
206
|
+
# Missing: new to your app if some compared release didn't have it either;
|
|
207
|
+
# deleted by you if every one did (then it stays deleted).
|
|
208
|
+
return create(path, file) if mine.nil? && (olds.empty? || olds.include?(nil))
|
|
209
|
+
return @result.removed << path if mine.nil?
|
|
210
|
+
return update(path, file, :updated) if olds.include?(mine) # an unedited, older copy
|
|
211
|
+
|
|
212
|
+
conflict(path, file, mine)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def create(path, file)
|
|
216
|
+
write(path, file)
|
|
217
|
+
@result.created << path
|
|
218
|
+
status("create", path)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def update(path, file, kind)
|
|
222
|
+
write(path, file)
|
|
223
|
+
@result.public_send(kind) << path
|
|
224
|
+
status("update", path)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def conflict(path, file, mine)
|
|
228
|
+
return record_conflict(path) unless @interactive
|
|
229
|
+
|
|
230
|
+
loop do
|
|
231
|
+
case @prompt.call("#{path} changed in this version and you edited it — [o]verwrite, [s]kip, " \
|
|
232
|
+
"[d]iff, save as [n]ew file?")
|
|
233
|
+
when "o" then return update(path, file, :updated)
|
|
234
|
+
when "n" then return save_new(path, file)
|
|
235
|
+
when "d" then @output.puts(diff(mine, file.content, path))
|
|
236
|
+
else return record_conflict(path)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def save_new(path, file)
|
|
242
|
+
write("#{path}.new", file)
|
|
243
|
+
@result.saved << path
|
|
244
|
+
status("create", "#{path}.new")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def record_conflict(path)
|
|
248
|
+
@result.conflicts << path
|
|
249
|
+
status("conflict", path)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def write(path, file)
|
|
253
|
+
return if @dry_run
|
|
254
|
+
|
|
255
|
+
target = File.join(@root, path)
|
|
256
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
257
|
+
File.binwrite(target, file.content)
|
|
258
|
+
File.chmod(file.mode & 0o777, target)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def diff(mine, theirs, path)
|
|
262
|
+
Tempfile.create("mine") do |a|
|
|
263
|
+
Tempfile.create("new") do |b|
|
|
264
|
+
a.write(mine)
|
|
265
|
+
b.write(theirs)
|
|
266
|
+
[a, b].each(&:flush)
|
|
267
|
+
labels = ["--label", "#{path} (yours)", "--label", "#{path} (new)"]
|
|
268
|
+
out, = Open3.capture2("diff", "-u", *labels, a.path, b.path)
|
|
269
|
+
out
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
rescue SystemCallError
|
|
273
|
+
"(diff isn't available on this system)"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def report
|
|
277
|
+
done = { "created" => @result.created, "updated" => @result.updated, "saved as .new" => @result.saved,
|
|
278
|
+
"left for you" => @result.conflicts }.reject { |_, paths| paths.empty? }
|
|
279
|
+
if done.empty?
|
|
280
|
+
@output.puts(" nothing to update")
|
|
281
|
+
else
|
|
282
|
+
@output.puts(" #{done.map { |label, paths| "#{paths.size} #{label}" }.join(", ")}" \
|
|
283
|
+
"#{" — dry run, nothing written" if @dry_run}")
|
|
284
|
+
end
|
|
285
|
+
return if @result.conflicts.empty?
|
|
286
|
+
|
|
287
|
+
@output.puts(" Review those in a terminal: gemstack update --templates#{" --from #{@from}" if @from}")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def record_version
|
|
291
|
+
target = File.join(@root, VERSION_FILE)
|
|
292
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
293
|
+
File.write(target, "#{GemStack::VERSION}\n")
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def status(label, path) = @output.puts(" #{label.rjust(9)} #{path}")
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -17,12 +17,20 @@ module GemStack
|
|
|
17
17
|
VERSION_FORMAT = /\A\d+\.\d+\.\d+\z/
|
|
18
18
|
LATEST_URL = "https://rubygems.org/api/v1/versions/gemstack/latest.json"
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# The first version whose `gemstack update --templates` exists.
|
|
21
|
+
TEMPLATE_UPDATES_SINCE = Gem::Version.new("0.3.6")
|
|
22
|
+
|
|
23
|
+
def initialize(root:, version: nil, output: $stdout, latest: nil, bundle: nil, templates: nil)
|
|
21
24
|
@root = root
|
|
22
25
|
@version = version
|
|
23
26
|
@output = output
|
|
24
27
|
@latest = latest || -> { latest_release }
|
|
25
28
|
@bundle = bundle || ->(gems) { Bundler.with_unbundled_env { system("bundle", "update", *gems, chdir: @root) } }
|
|
29
|
+
# Runs in the app's updated bundle, so the new version renders its own templates.
|
|
30
|
+
# (The lockfile's version can be newer than the app's templates, so it isn't passed on.)
|
|
31
|
+
@templates = templates || lambda {
|
|
32
|
+
Bundler.with_unbundled_env { system("bundle", "exec", "gemstack", "update", "--templates", chdir: @root) }
|
|
33
|
+
}
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
def run
|
|
@@ -71,9 +79,11 @@ module GemStack
|
|
|
71
79
|
return failure("bundle update finished but Gemfile.lock has gemstack #{installed.inspect}, not #{target}")
|
|
72
80
|
end
|
|
73
81
|
|
|
82
|
+
@output.puts("\nGemStack #{target} is installed.\n\n")
|
|
83
|
+
update_templates(target)
|
|
74
84
|
@output.puts(<<~DONE)
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
Next:
|
|
77
87
|
- read what changed: https://github.com/gemstack-rb/gemstack/blob/main/CHANGELOG.md
|
|
78
88
|
- gemstack doctor
|
|
79
89
|
- gemstack test
|
|
@@ -89,6 +99,12 @@ module GemStack
|
|
|
89
99
|
end
|
|
90
100
|
end
|
|
91
101
|
|
|
102
|
+
def update_templates(target)
|
|
103
|
+
return if Gem::Version.new(target) < TEMPLATE_UPDATES_SINCE
|
|
104
|
+
|
|
105
|
+
@templates.call || @output.puts("Templates weren't updated; run: gemstack update --templates")
|
|
106
|
+
end
|
|
107
|
+
|
|
92
108
|
def gem_line(name) = /^\s*gem\s+["']#{Regexp.escape(name)}["']/
|
|
93
109
|
|
|
94
110
|
def checkout(gemfile)
|
data/lib/gemstack/cli.rb
CHANGED
|
@@ -25,6 +25,7 @@ module GemStack
|
|
|
25
25
|
require_relative "cli/deploy_generator"
|
|
26
26
|
require_relative "cli/add_generator"
|
|
27
27
|
require_relative "cli/update_generator"
|
|
28
|
+
require_relative "cli/template_update"
|
|
28
29
|
require_relative "cli/commands/db"
|
|
29
30
|
require_relative "cli/commands/jobs"
|
|
30
31
|
require_relative "cli/console_methods"
|
|
@@ -188,7 +189,8 @@ module GemStack
|
|
|
188
189
|
end
|
|
189
190
|
end
|
|
190
191
|
|
|
191
|
-
desc "destroy GENERATOR NAME",
|
|
192
|
+
desc "destroy GENERATOR [NAME]",
|
|
193
|
+
"Remove generated code (alias: d): model, controller, resource, job, policy, migration, deploy"
|
|
192
194
|
DestroyGenerator.configure(self)
|
|
193
195
|
def destroy(generator = nil, name = nil)
|
|
194
196
|
DestroyGenerator.invoke(generator, name, cli: self) { |root| refresh_contract(root) }
|
|
@@ -230,19 +232,35 @@ module GemStack
|
|
|
230
232
|
desc "version", "Print the GemStack version"
|
|
231
233
|
def version = say("GemStack #{GemStack::VERSION}")
|
|
232
234
|
|
|
233
|
-
desc "update [VERSION]", "Update this app to the latest GemStack release (or VERSION)"
|
|
235
|
+
desc "update [VERSION]", "Update this app to the latest GemStack release (or VERSION), templates included"
|
|
234
236
|
long_desc <<~DESC
|
|
235
237
|
Sets every GemStack gem in the Gemfile (gemstack, gemstack-auth, gemstack-realtime) to
|
|
236
238
|
"~> VERSION" and runs bundle update for them. Without VERSION, uses the latest release on
|
|
237
|
-
rubygems.org.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
rubygems.org. Then updates the files `gemstack new` wrote (config, bin/, frontend setup…):
|
|
240
|
+
files the release didn't change are left alone, new ones are created, ones you never edited
|
|
241
|
+
are updated, and for ones you edited you choose: overwrite, skip, see the diff, or FILE.new.
|
|
242
|
+
|
|
243
|
+
gemstack update # the latest release
|
|
244
|
+
gemstack update 0.3.6 # a specific version
|
|
245
|
+
gemstack update --templates --dry-run # only the template step, preview
|
|
246
|
+
gemstack update --templates --from 0.3.5
|
|
241
247
|
DESC
|
|
248
|
+
method_option :templates, type: :boolean, default: false, desc: "Only update the app's templates"
|
|
249
|
+
method_option :from, type: :string, desc: "--templates: the GemStack version the app's files came from"
|
|
250
|
+
method_option :dry_run, type: :boolean, default: false, desc: "--templates: show what would change"
|
|
242
251
|
def update(version = nil)
|
|
252
|
+
return update_templates if options[:templates]
|
|
253
|
+
|
|
243
254
|
exit(1) unless UpdateGenerator.new(root: Project.root!, version: version).run
|
|
244
255
|
end
|
|
245
256
|
|
|
257
|
+
no_commands do
|
|
258
|
+
def update_templates
|
|
259
|
+
root = Project.ensure_bundle!(self.class.argv) # the app's installed GemStack renders its templates
|
|
260
|
+
TemplateUpdate.new(root: root, from: options[:from], dry_run: options[:dry_run]).run
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
246
264
|
include Helpers
|
|
247
265
|
end
|
|
248
266
|
end
|
data/lib/gemstack/version.rb
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= version %>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemstack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adware Technologies
|
|
@@ -44,14 +44,14 @@ dependencies:
|
|
|
44
44
|
requirements:
|
|
45
45
|
- - '='
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.3.
|
|
47
|
+
version: 0.3.6
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - '='
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.3.
|
|
54
|
+
version: 0.3.6
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: json
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -180,6 +180,7 @@ files:
|
|
|
180
180
|
- lib/gemstack/cli/controller_generator.rb
|
|
181
181
|
- lib/gemstack/cli/deploy_generator.rb
|
|
182
182
|
- lib/gemstack/cli/destroy_generator.rb
|
|
183
|
+
- lib/gemstack/cli/destroy_generator/kinds.rb
|
|
183
184
|
- lib/gemstack/cli/doctor.rb
|
|
184
185
|
- lib/gemstack/cli/doctor/secret_checks.rb
|
|
185
186
|
- lib/gemstack/cli/doctor/upgrade_check.rb
|
|
@@ -195,6 +196,7 @@ files:
|
|
|
195
196
|
- lib/gemstack/cli/project.rb
|
|
196
197
|
- lib/gemstack/cli/resource_generator.rb
|
|
197
198
|
- lib/gemstack/cli/resource_spec.rb
|
|
199
|
+
- lib/gemstack/cli/template_update.rb
|
|
198
200
|
- lib/gemstack/cli/update_generator.rb
|
|
199
201
|
- lib/gemstack/contract.rb
|
|
200
202
|
- lib/gemstack/contract/builder.rb
|
|
@@ -295,6 +297,7 @@ files:
|
|
|
295
297
|
- templates/app/db/migrations/dot_keep
|
|
296
298
|
- templates/app/db/seeds.rb
|
|
297
299
|
- templates/app/dot_env.example.tt
|
|
300
|
+
- templates/app/dot_gemstack/version.tt
|
|
298
301
|
- templates/app/dot_gitignore
|
|
299
302
|
- templates/app/dot_node-version.tt
|
|
300
303
|
- templates/app/dot_nvmrc.tt
|