belt 0.6.3 → 0.6.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a728d2756664cff1204c7926b8db4cbe7d327e1aacf6d544b9d39db2e2e258c
4
- data.tar.gz: 5b4eb1d119a8343abbdfe5fc329f167546bc963ecb7ef415d0c54b5c049cf084
3
+ metadata.gz: 2fcb629702530cd06ffb9af298dcd259c4fc0503107ef19ab58a58da476617b4
4
+ data.tar.gz: 167a12259478e5079620446f76dad474cde1d456f08b328663fe11f82860fa66
5
5
  SHA512:
6
- metadata.gz: bad0ef5562fe52c213e58b8ae0451e8fe48ae1f1b347f2cb6662e891e9b5893b59210002d5fedf0c3e3b03fcb72ed24ab05c5485c2156e034e1207a5c0f54316
7
- data.tar.gz: 2a92ccf0e821c055f0cbf661c5d5446ba98fd7bd45fb25b7fda6a774027f29aba07411fc962afb0b3f150c6685a768790c2981ba8548f9549b3828c969ed88ec
6
+ metadata.gz: bb37dd0fe9e0cd392a0824d14b0ab4c51c5d002b36240fc7795ad11ce2313dd0014bea535c020fab6e4ec8988c7cf4d5a90992e2f8cfcdbcea35bc780586592f
7
+ data.tar.gz: d8d7423e5b3cd0c3d1598ff7b9e82446c9de4705b68313ade0ee0479ec26a87b5d4e3acd23a52b664ec2f50fd37e66cb046e1383f18bbf696cd01a13d24b140e
data/CHANGELOG.md CHANGED
@@ -1,6 +1,31 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.6.4
4
+
5
+ ### Added
6
+
7
+ - **Plugins can register top-level commands.** A gem that ships
8
+ `lib/belt/commands/<name>_command.rb` defining `Belt::Commands::<Name>Command.run(args)`
9
+ gets `belt <name> ...` (underscores in the file name become hyphens), listed under
10
+ "Plugin Commands" in `belt --help`. Discovery mirrors `GeneratorRegistry`
11
+ (`Belt::CLI::CommandRegistry`). Optional `.description` for help text and `.standalone?`
12
+ to run outside a Belt project. Built-in commands and terraform actions win on a name
13
+ clash. `belt help` / `belt --help` / `belt -h` now print usage and exit 0.
14
+
15
+ ### Fixed
16
+
17
+ - **The data-migrations step of `belt deploy` no longer fails with "You have already
18
+ activated json X, but your Gemfile requires json Y".** The child `belt db:migrate` now
19
+ starts with the app's bundle loaded (`BUNDLE_GEMFILE` + `-rbundler/setup`), so the belt
20
+ CLI's own `require 'json'` can't activate a newer json than the app's lockfile pins.
21
+ - **A fresh environment's Lambdas missing their `config/lambda/*.yml` `env_vars`** (nested
22
+ envs especially, since they are always created fresh) is fixed in conveyor-belt 0.0.19:
23
+ the provider's create path read only the HCL `lambda_config` map and ignored
24
+ `lambda_config_dir`, so the YAML env vars, `iam_policy_arns` and triggers landed only on
25
+ a second deploy. Existing apps on `version = "~> 0.0.16"` pick it up with
26
+ `terraform init -upgrade` in the env's infrastructure directory.
27
+
28
+ ## 0.6.3
4
29
 
5
30
  ### Added
6
31
 
data/README.md CHANGED
@@ -830,6 +830,8 @@ No central registry file, no initializer hook. Belt discovers generators by scan
830
830
 
831
831
  That is the whole contract. After `bundle install`, `belt generate <name>` and `belt destroy <name>` just work.
832
832
 
833
+ Plugins can also add a top-level command: a file at `lib/belt/commands/<name>_command.rb` defining `Belt::Commands::<Name>Command.run(args)` becomes `belt <name> ...` and shows up under "Plugin Commands" in `belt --help`. Optional `.description` and `.standalone?` (run outside a project). Built-in commands win on a name clash. See `Belt::CLI::CommandRegistry` and `belt explain plugins`.
834
+
833
835
  See `Belt::CLI::GeneratorRegistry` and the CHANGELOG entry for **0.1.13 Generator Extension API**.
834
836
 
835
837
  ### Plugin layout (canonical)
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module CLI
5
+ # Discovers top-level CLI commands provided by external gems.
6
+ #
7
+ # Gems register commands by placing a file at:
8
+ # lib/belt/commands/<name>_command.rb
9
+ #
10
+ # Each command class must:
11
+ # - Be named <Name>Command (e.g., ConnectCommand)
12
+ # - Live in the Belt::Commands module
13
+ # - Implement .run(args)
14
+ # - Implement .description for help text (optional)
15
+ # - Implement .standalone? returning true to run outside a Belt project (optional)
16
+ #
17
+ # Example gem structure:
18
+ # belt-connect/
19
+ # lib/
20
+ # belt/
21
+ # commands/
22
+ # connect_command.rb
23
+ #
24
+ # The command is then available as:
25
+ # belt connect ...
26
+ #
27
+ # Underscores in the file name become hyphens in the command name
28
+ # (lib/belt/commands/feature_flags_command.rb → `belt feature-flags`).
29
+ # Built-in commands win on a name clash; see Belt::CLI.plugin_command.
30
+ module CommandRegistry
31
+ COMMAND_PATH = 'lib/belt/commands'
32
+
33
+ class << self
34
+ # Returns a hash of { "name" => command_class } for all discovered commands.
35
+ def discovered_commands
36
+ @discovered_commands ||= discover_all
37
+ end
38
+
39
+ # Returns just the names of discovered commands.
40
+ def command_names
41
+ discovered_commands.keys
42
+ end
43
+
44
+ # Returns the command class for a given name, or nil if not found.
45
+ def find(name)
46
+ discovered_commands[name]
47
+ end
48
+
49
+ # Reset cache (useful in tests)
50
+ def reset!
51
+ @discovered_commands = nil
52
+ end
53
+
54
+ private
55
+
56
+ def discover_all
57
+ commands = {}
58
+
59
+ Gem.loaded_specs.each_value do |spec|
60
+ commands_dir = File.join(spec.gem_dir, COMMAND_PATH)
61
+ next unless File.directory?(commands_dir)
62
+
63
+ Dir.glob(File.join(commands_dir, '*_command.rb')).each do |file|
64
+ basename = File.basename(file, '.rb').sub(/_command\z/, '')
65
+ klass = load_command_class(file, basename)
66
+ commands[basename.tr('_', '-')] = klass if klass
67
+ end
68
+ end
69
+
70
+ commands
71
+ end
72
+
73
+ def load_command_class(file, basename)
74
+ require file
75
+
76
+ class_name = "#{classify(basename)}Command"
77
+ Belt::Commands.const_get(class_name, false)
78
+ rescue LoadError, NameError => e
79
+ warn "[Belt] Warning: Failed to load command '#{basename}' from #{file}: #{e.message}"
80
+ nil
81
+ end
82
+
83
+ def classify(name)
84
+ name.split('_').map(&:capitalize).join
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ # Namespace for top-level command classes provided by external gems
91
+ module Commands
92
+ end
93
+ end
@@ -923,13 +923,27 @@ module Belt
923
923
  return true if @skip_migrations || !DbMigrateCommand.migrations?(@project_root)
924
924
 
925
925
  puts "\n━━━ data migrations ━━━"
926
- return true if system(Gem.ruby, BELT_EXE, 'db:migrate', @env, '--yes', chdir: @project_root)
926
+ return true if system(migration_child_env, Gem.ruby, BELT_EXE, 'db:migrate', @env, '--yes',
927
+ chdir: @project_root)
927
928
 
928
929
  puts "\n ⚠ Data migrations failed for #{@env}. The deploy itself succeeded."
929
930
  puts " Fix the migration and run `belt db:migrate #{@env}` to retry."
930
931
  false
931
932
  end
932
933
 
934
+ # Load the app's bundle before the belt CLI does. Otherwise the CLI's own
935
+ # `require 'json'` activates the newest installed json, and db:migrate's
936
+ # `bundler/setup` then fails with "You have already activated json X,
937
+ # but your Gemfile requires json Y" (Fizzy #1734).
938
+ def migration_child_env
939
+ gemfile = File.join(@project_root, 'Gemfile')
940
+ return {} unless File.exist?(gemfile)
941
+
942
+ rubyopt = ENV.fetch('RUBYOPT', '')
943
+ rubyopt = "-rbundler/setup #{rubyopt}".strip unless rubyopt.include?('bundler/setup')
944
+ { 'BUNDLE_GEMFILE' => gemfile, 'RUBYOPT' => rubyopt }
945
+ end
946
+
933
947
  def deploy_frontend_if_exists
934
948
  require_relative 'frontend_registry'
935
949
  require_relative 'frontend_deploy_command'
data/lib/belt/cli.rb CHANGED
@@ -31,6 +31,7 @@ require_relative 'cli/logs_command'
31
31
  require_relative 'cli/doctor_command'
32
32
  require_relative 'cli/plugin_command'
33
33
  require_relative 'cli/explain_command'
34
+ require_relative 'cli/command_registry'
34
35
 
35
36
  module Belt
36
37
  module CLI
@@ -56,7 +57,8 @@ module Belt
56
57
  'dns' => Belt::CLI::DnsCommand,
57
58
  'frontend' => Belt::CLI::FrontendEnvCommand,
58
59
  %w[server s] => Belt::CLI::ServerCommand,
59
- %w[version --version -v] => ->(_args) { puts "Belt #{Belt::VERSION}" }
60
+ %w[version --version -v] => ->(_args) { puts "Belt #{Belt::VERSION}" },
61
+ %w[help --help -h] => ->(_args) { puts usage }
60
62
  }.freeze
61
63
 
62
64
  COMMANDS = COMMANDS_DEFINITION.each_with_object({}) do |(keys, handler), hash|
@@ -66,7 +68,7 @@ module Belt
66
68
  TERRAFORM_ACTIONS = Belt::CLI::TerraformCommand::ACTIONS
67
69
 
68
70
  # Commands that can run without being inside a Belt project
69
- STANDALONE_COMMANDS = %w[new version --version -v doctor explain].freeze
71
+ STANDALONE_COMMANDS = %w[new version --version -v help --help -h doctor explain].freeze
70
72
 
71
73
  def self.start(args)
72
74
  command = args.shift
@@ -76,8 +78,10 @@ module Belt
76
78
  exit 1
77
79
  end
78
80
 
81
+ plugin = plugin_command(command)
82
+
79
83
  # For project-level commands, find and chdir to the project root
80
- ensure_project_root!(command) unless STANDALONE_COMMANDS.include?(command)
84
+ ensure_project_root!(command) unless standalone?(command, plugin)
81
85
 
82
86
  # `belt destroy` is ambiguous: could be terraform destroy or belt destroy <generator>.
83
87
  return if command == 'destroy' && route_destroy_command(args)
@@ -85,7 +89,7 @@ module Belt
85
89
  # Terraform shorthand: belt init wups, belt plan wups, belt apply wups, belt destroy wups
86
90
  return Belt::CLI::TerraformCommand.run(command, args) if TERRAFORM_ACTIONS.include?(command)
87
91
 
88
- handler = COMMANDS[command]
92
+ handler = COMMANDS[command] || plugin
89
93
 
90
94
  # If no built-in command matched, try running it as a rake task
91
95
  if handler.nil?
@@ -102,7 +106,43 @@ module Belt
102
106
  end
103
107
  end
104
108
 
109
+ # Returns the gem-provided command class for `name`, or nil. Built-in
110
+ # commands and terraform actions win on a name clash.
111
+ def self.plugin_command(name)
112
+ return nil if name.nil? || builtin_command?(name)
113
+
114
+ CommandRegistry.find(name)
115
+ end
116
+
117
+ def self.builtin_command?(name)
118
+ COMMANDS.key?(name) || TERRAFORM_ACTIONS.include?(name)
119
+ end
120
+
121
+ def self.standalone?(command, plugin)
122
+ return STANDALONE_COMMANDS.include?(command) unless plugin
123
+
124
+ plugin.respond_to?(:standalone?) && plugin.standalone?
125
+ end
126
+
105
127
  def self.usage
128
+ base_usage.sub("\nRake Tasks:\n", "#{plugin_usage}\nRake Tasks:\n")
129
+ end
130
+
131
+ # "Plugin Commands:" help section for gem-provided commands ("" when none).
132
+ def self.plugin_usage
133
+ names = CommandRegistry.command_names.reject { |name| builtin_command?(name) }.sort
134
+ return '' if names.empty?
135
+
136
+ lines = names.map do |name|
137
+ klass = CommandRegistry.find(name)
138
+ description = klass.respond_to?(:description) ? klass.description.to_s : ''
139
+ " #{name.ljust(42)} #{description}".rstrip
140
+ end
141
+
142
+ "\nPlugin Commands:\n#{lines.join("\n")}\n"
143
+ end
144
+
145
+ def self.base_usage
106
146
  <<~USAGE
107
147
  Usage: belt <command> [options]
108
148
 
@@ -151,6 +191,7 @@ module Belt
151
191
  destroy [environment] <env> terraform destroy for environment
152
192
  output [environment] <env> terraform output for environment
153
193
  --version Show Belt version
194
+ help, --help, -h Show this help
154
195
 
155
196
  Rake Tasks:
156
197
  Any rake task from your Gemfile dependencies can be run directly:
@@ -45,6 +45,38 @@ Belt discovers plugins automatically when:
45
45
 
46
46
  Optional: `.destroy(args)` and `.description` for destroy path and help text.
47
47
 
48
+ ## Top-level Commands
49
+
50
+ A plugin can also add its own `belt <name> ...` command, the way `belt-connect`
51
+ adds `belt connect`:
52
+
53
+ 1. Gem has a file at `lib/belt/commands/<name>_command.rb`
54
+ 2. Class is `Belt::Commands::<Name>Command`
55
+ 3. Class implements `.run(args)` (required); `args` is everything after `<name>`
56
+
57
+ Optional: `.description` (shown under "Plugin Commands" in `belt --help`) and
58
+ `.standalone?` returning `true` to run outside a Belt project. Otherwise the
59
+ command runs from the project root, like built-ins.
60
+
61
+ Underscores in the file name become hyphens (`feature_flags_command.rb` →
62
+ `belt feature-flags`). Built-in commands and terraform actions win on a name
63
+ clash, so a plugin can't shadow `belt deploy`.
64
+
65
+ ```ruby
66
+ # lib/belt/commands/connect_command.rb
67
+ module Belt
68
+ module Commands
69
+ class ConnectCommand
70
+ def self.description = 'Connect widgets to your app'
71
+
72
+ def self.run(args)
73
+ # belt connect widget orders --env dev → args == ['widget', 'orders', '--env', 'dev']
74
+ end
75
+ end
76
+ end
77
+ end
78
+ ```
79
+
48
80
  ## Generator Checklist
49
81
 
50
82
  A good plugin generator typically installs:
@@ -71,6 +103,8 @@ belt-messaging/
71
103
  │ │ ├── version.rb
72
104
  │ │ ├── controllers/ # Default controllers
73
105
  │ │ └── templates/ # ERB templates for generator
106
+ │ ├── commands/
107
+ │ │ └── messaging_command.rb # ← optional `belt messaging`
74
108
  │ └── generators/
75
109
  │ └── messaging_generator.rb # ← auto-discovered
76
110
  └── spec/
data/lib/belt/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.6.3'
4
+ VERSION = '0.6.4'
5
5
 
6
6
  # Ruby version new apps run on in Lambda. Rendered into the conveyor-belt
7
7
  # provider block (`ruby_version`) by `belt new` / `belt generate environment`.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.3
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -118,6 +118,7 @@ files:
118
118
  - lib/belt/cli/backup_runner.rb
119
119
  - lib/belt/cli/bucket_security.rb
120
120
  - lib/belt/cli/cognito_sharer.rb
121
+ - lib/belt/cli/command_registry.rb
121
122
  - lib/belt/cli/console_command.rb
122
123
  - lib/belt/cli/contracts_command.rb
123
124
  - lib/belt/cli/db_copy_command.rb