discharger 0.2.10 → 0.2.11

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.
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_command"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module Commands
8
+ class DockerCommand < BaseCommand
9
+ def execute
10
+ log "Ensure Docker is running"
11
+
12
+ unless system_quiet("docker info > /dev/null 2>&1")
13
+ log "Starting Docker..."
14
+ system_quiet("open -a Docker")
15
+ sleep 10
16
+ unless system_quiet("docker info > /dev/null 2>&1")
17
+ log "Docker is not running. Please start Docker manually."
18
+ return
19
+ end
20
+ end
21
+
22
+ # Setup database container if configured
23
+ if config.respond_to?(:database) && config.database
24
+ setup_container(
25
+ name: config.database.name || "db-app",
26
+ port: config.database.port || 5432,
27
+ image: "postgres:#{config.database.version || "14"}",
28
+ env: {"POSTGRES_PASSWORD" => config.database.password || "postgres"},
29
+ volume: "#{config.database.name || "db-app"}:/var/lib/postgresql/data",
30
+ internal_port: 5432
31
+ )
32
+ end
33
+
34
+ # Setup Redis container if configured
35
+ if config.respond_to?(:redis) && config.redis
36
+ setup_container(
37
+ name: config.redis.name || "redis-app",
38
+ port: config.redis.port || 6379,
39
+ image: "redis:#{config.redis.version || "latest"}",
40
+ internal_port: 6379
41
+ )
42
+ end
43
+ end
44
+
45
+ def can_execute?
46
+ # Only execute if Docker is available and containers are configured
47
+ system_quiet("which docker") && (
48
+ (config.respond_to?(:database) && config.database) ||
49
+ (config.respond_to?(:redis) && config.redis)
50
+ )
51
+ end
52
+
53
+ def description
54
+ "Setup Docker containers"
55
+ end
56
+
57
+ private
58
+
59
+ def setup_container(name:, port:, image:, internal_port:, env: {}, volume: nil)
60
+ log "Checking #{name} container"
61
+
62
+ if system_quiet("docker ps | grep #{name} > /dev/null 2>&1")
63
+ log "#{name} container is already running"
64
+ return
65
+ end
66
+
67
+ # Check if container exists but is stopped
68
+ if system_quiet("docker inspect #{name} > /dev/null 2>&1")
69
+ log "Starting existing #{name} container"
70
+ unless system_quiet("docker start #{name}")
71
+ log "Removing failed #{name} container"
72
+ system_quiet("docker rm -f #{name}")
73
+ create_container(name: name, port: port, image: image, env: env, volume: volume, internal_port: internal_port)
74
+ end
75
+ else
76
+ create_container(name: name, port: port, image: image, env: env, volume: volume, internal_port: internal_port)
77
+ end
78
+
79
+ # Verify container is running
80
+ sleep 2
81
+ unless system_quiet("docker ps | grep #{name} > /dev/null 2>&1")
82
+ log "#{name} container failed to start"
83
+ raise "#{name} container failed to start"
84
+ end
85
+ end
86
+
87
+ def create_container(name:, port:, image:, internal_port:, env: {}, volume: nil)
88
+ log "Creating new #{name} container"
89
+
90
+ cmd = ["docker", "run", "-d", "--name", name, "-p", "#{port}:#{internal_port}"]
91
+ env.each { |k, v| cmd.push("-e", "#{k}=#{v}") }
92
+ cmd.push("-v", volume) if volume
93
+ cmd.push(image)
94
+
95
+ system!(*cmd)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "base_command"
5
+
6
+ module Discharger
7
+ module SetupRunner
8
+ module Commands
9
+ class EnvCommand < BaseCommand
10
+ def execute
11
+ if File.exist?(".env")
12
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
13
+ require "rainbow"
14
+ puts Rainbow(" → .env file already exists. Skipping.").yellow
15
+ end
16
+ return
17
+ end
18
+
19
+ unless File.exist?(".env.example")
20
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
21
+ require "rainbow"
22
+ puts Rainbow(" → WARNING: .env.example not found. Skipping .env creation").yellow
23
+ end
24
+ return
25
+ end
26
+
27
+ simple_action("Creating .env from .env.example") do
28
+ FileUtils.cp(".env.example", ".env")
29
+ end
30
+ end
31
+
32
+ def can_execute?
33
+ File.exist?(".env.example")
34
+ end
35
+
36
+ def description
37
+ "Setup environment file"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_command"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module Commands
8
+ class GitCommand < BaseCommand
9
+ def execute
10
+ log "Setting up git configuration"
11
+
12
+ # Set up commit template if it exists
13
+ commit_template = File.join(app_root, ".commit-template")
14
+ if File.exist?(commit_template)
15
+ system! "git config --local commit.template .commit-template"
16
+ log "Git commit template configured"
17
+ end
18
+
19
+ # Set up git hooks if .githooks directory exists
20
+ githooks_dir = File.join(app_root, ".githooks")
21
+ if File.directory?(githooks_dir)
22
+ system! "git config --local core.hooksPath .githooks"
23
+ log "Git hooks path configured"
24
+ end
25
+
26
+ # Any other git config from the setup.yml
27
+ if config.respond_to?(:git_config) && config.git_config
28
+ config.git_config.each do |key, value|
29
+ system! "git config --local #{key} '#{value}'"
30
+ log "Set git config #{key}"
31
+ end
32
+ end
33
+ end
34
+
35
+ def can_execute?
36
+ File.directory?(File.join(app_root, ".git"))
37
+ end
38
+
39
+ def description
40
+ "Setup git configuration"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_command"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ module Commands
8
+ class YarnCommand < BaseCommand
9
+ def execute
10
+ log "Installing Node modules"
11
+
12
+ # Enable corepack if yarn.lock exists (Yarn 2+)
13
+ if File.exist?(File.join(app_root, "yarn.lock"))
14
+ if system_quiet("which corepack")
15
+ system! "corepack enable"
16
+ system! "corepack use yarn@stable"
17
+ end
18
+
19
+ # Install dependencies
20
+ system_quiet("yarn check --check-files > /dev/null 2>&1") || system!("yarn install")
21
+ elsif File.exist?(File.join(app_root, "package-lock.json"))
22
+ # NPM project
23
+ log "Found package-lock.json, using npm"
24
+ system! "npm ci"
25
+ elsif File.exist?(File.join(app_root, "package.json"))
26
+ # Generic package.json - try yarn first, fall back to npm
27
+ if system_quiet("which yarn")
28
+ system! "yarn install"
29
+ else
30
+ system! "npm install"
31
+ end
32
+ end
33
+ end
34
+
35
+ def can_execute?
36
+ File.exist?(File.join(app_root, "package.json"))
37
+ end
38
+
39
+ def description
40
+ "Install JavaScript dependencies"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ class ConditionEvaluator
8
+ class << self
9
+ def evaluate(condition, context = {})
10
+ return true if condition.nil? || condition.strip.empty?
11
+ ast = Prism.parse(condition).value
12
+ raise "Parse error" unless ast
13
+ evaluate_node(ast)
14
+ rescue => e
15
+ log_warning("Condition evaluation failed: #{e.message}")
16
+ false
17
+ end
18
+
19
+ private
20
+
21
+ def evaluate_node(node)
22
+ case node.type
23
+ when :program_node
24
+ # Evaluate the first statement in the program
25
+ stmts = node.statements
26
+ if stmts&.body&.any?
27
+ evaluate_node(stmts.body.first)
28
+ else
29
+ true
30
+ end
31
+ when :statements_node
32
+ # Evaluate the first statement
33
+ if node.body.any?
34
+ evaluate_node(node.body.first)
35
+ else
36
+ true
37
+ end
38
+ when :and_node
39
+ left = evaluate_node(node.left)
40
+ right = evaluate_node(node.right)
41
+ left && right
42
+ when :or_node
43
+ left = evaluate_node(node.left)
44
+ right = evaluate_node(node.right)
45
+ left || right
46
+ when :call_node
47
+ # Handle method calls
48
+ if node.receiver&.type == :constant_read_node
49
+ case node.receiver.name
50
+ when :ENV
51
+ if node.name == :[]
52
+ ENV[evaluate_node(node.arguments.arguments.first)]
53
+ else
54
+ raise "Unsafe ENV method: #{node.name}"
55
+ end
56
+ when :File
57
+ case node.name
58
+ when :exist?
59
+ File.exist?(evaluate_node(node.arguments.arguments.first))
60
+ when :directory?
61
+ File.directory?(evaluate_node(node.arguments.arguments.first))
62
+ when :file?
63
+ File.file?(evaluate_node(node.arguments.arguments.first))
64
+ else
65
+ raise "Unsafe File method: #{node.name}"
66
+ end
67
+ when :Dir
68
+ if node.name == :exist?
69
+ Dir.exist?(evaluate_node(node.arguments.arguments.first))
70
+ else
71
+ raise "Unsafe Dir method: #{node.name}"
72
+ end
73
+ else
74
+ raise "Unsafe method call: #{node.receiver.name}.#{node.name}"
75
+ end
76
+ elsif node.receiver&.type == :call_node
77
+ # Handle chained calls like ENV['FOO'] == 'bar'
78
+ if node.name == :==
79
+ left = evaluate_node(node.receiver)
80
+ right = evaluate_node(node.arguments.arguments.first)
81
+ left == right
82
+ elsif node.name == :!=
83
+ left = evaluate_node(node.receiver)
84
+ right = evaluate_node(node.arguments.arguments.first)
85
+ left != right
86
+ else
87
+ raise "Unsafe operator: #{node.name}"
88
+ end
89
+ elsif node.receiver.nil?
90
+ # Method call without receiver (like system)
91
+ raise "Unsafe method call: #{node.name}"
92
+ else
93
+ raise "Unsafe method call: #{node.receiver&.name}.#{node.name}"
94
+ end
95
+ when :constant_read_node
96
+ node.name
97
+ when :string_node
98
+ node.unescaped
99
+ when :true_node # standard:disable Lint/BooleanSymbol
100
+ true
101
+ when :false_node # standard:disable Lint/BooleanSymbol
102
+ false
103
+ when :array_node
104
+ node.elements.map { |el| evaluate_node(el) }
105
+ when :symbol_node
106
+ node.unescaped
107
+ when :integer_node
108
+ node.value
109
+ when :x_string_node
110
+ # Backtick commands - block for security
111
+ raise "Unsafe backtick command"
112
+ when :parentheses_node
113
+ # Evaluate the expression inside parentheses
114
+ evaluate_node(node.body)
115
+ else
116
+ raise "Unsafe node: #{node.type}"
117
+ end
118
+ end
119
+
120
+ def log_warning(message)
121
+ if defined?(Rails)
122
+ Rails.logger.warn(message)
123
+ else
124
+ warn("[SetupRunner] #{message}")
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Discharger
6
+ module SetupRunner
7
+ class Configuration
8
+ attr_accessor :app_name, :db_config, :redis_config, :services, :steps, :custom_steps
9
+
10
+ def initialize
11
+ @app_name = "Application"
12
+ @db_config = DatabaseConfig.new
13
+ @redis_config = RedisConfig.new
14
+ @services = []
15
+ @steps = []
16
+ @custom_steps = []
17
+ end
18
+
19
+ def self.from_file(path)
20
+ config = new
21
+ yaml = YAML.load_file(path)
22
+
23
+ # Handle empty YAML files
24
+ return config if yaml.nil? || yaml == false
25
+
26
+ config.app_name = yaml["app_name"] if yaml["app_name"]
27
+ config.db_config.from_hash(yaml["database"]) if yaml["database"]
28
+ config.redis_config.from_hash(yaml["redis"]) if yaml["redis"]
29
+ config.services = yaml["services"] || []
30
+ config.steps = yaml["steps"] || []
31
+ config.custom_steps = yaml["custom_steps"] || []
32
+
33
+ config
34
+ end
35
+ end
36
+
37
+ class DatabaseConfig
38
+ attr_accessor :port, :name, :version, :password
39
+
40
+ def initialize
41
+ @port = 5432
42
+ @name = "db-app"
43
+ @version = "14"
44
+ @password = "postgres"
45
+ end
46
+
47
+ def from_hash(hash)
48
+ @port = hash["port"] if hash["port"]
49
+ @name = hash["name"] if hash["name"]
50
+ @version = hash["version"] if hash["version"]
51
+ @password = hash["password"] if hash["password"]
52
+ end
53
+ end
54
+
55
+ class RedisConfig
56
+ attr_accessor :port, :name, :version
57
+
58
+ def initialize
59
+ @port = 6379
60
+ @name = "redis-app"
61
+ @version = "latest"
62
+ end
63
+
64
+ def from_hash(hash)
65
+ @port = hash["port"] if hash["port"]
66
+ @name = hash["name"] if hash["name"]
67
+ @version = hash["version"] if hash["version"]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "logger"
5
+ require_relative "command_factory"
6
+
7
+ module Discharger
8
+ module SetupRunner
9
+ class Error < StandardError; end
10
+
11
+ class Runner
12
+ attr_reader :config, :app_root, :logger, :command_factory
13
+
14
+ def initialize(config, app_root = nil, logger = nil)
15
+ @config = config
16
+ @app_root = app_root || Dir.pwd
17
+ @logger = logger || Logger.new($stdout)
18
+ @command_factory = CommandFactory.new(config, app_root, logger)
19
+ end
20
+
21
+ def run
22
+ require "rainbow"
23
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
24
+ puts Rainbow("\n🚀 Starting setup for #{config.app_name}").bright.blue
25
+ puts Rainbow("=" * 50).blue
26
+ end
27
+
28
+ FileUtils.chdir app_root do
29
+ execute_commands
30
+ end
31
+
32
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
33
+ puts Rainbow("\n✅ Setup completed successfully!").bright.green
34
+ end
35
+ rescue => e
36
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
37
+ puts Rainbow("\n❌ Setup failed: #{e.message}").bright.red
38
+ end
39
+ raise Error, e.message
40
+ end
41
+
42
+ def add_command(command)
43
+ commands << command
44
+ end
45
+
46
+ def remove_command(command_name)
47
+ commands.reject! { |cmd| cmd.class.name.demodulize.underscore == command_name.to_s }
48
+ end
49
+
50
+ def replace_command(command_name, new_command)
51
+ remove_command(command_name)
52
+ add_command(new_command)
53
+ end
54
+
55
+ def insert_command_before(target_command_name, new_command)
56
+ target_index = commands.find_index { |cmd| cmd.class.name.demodulize.underscore == target_command_name.to_s }
57
+ if target_index
58
+ commands.insert(target_index, new_command)
59
+ else
60
+ add_command(new_command)
61
+ end
62
+ end
63
+
64
+ def insert_command_after(target_command_name, new_command)
65
+ target_index = commands.find_index { |cmd| cmd.class.name.demodulize.underscore == target_command_name.to_s }
66
+ if target_index
67
+ commands.insert(target_index + 1, new_command)
68
+ else
69
+ add_command(new_command)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def commands
76
+ @commands ||= command_factory.create_all_commands
77
+ end
78
+
79
+ def execute_commands
80
+ commands.each do |command|
81
+ execute_command(command)
82
+ end
83
+ end
84
+
85
+ def execute_command(command)
86
+ unless command.can_execute?
87
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
88
+ require "rainbow"
89
+ puts Rainbow("⏭️ Skipping #{command.description} (prerequisites not met)").yellow
90
+ end
91
+ return
92
+ end
93
+
94
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
95
+ puts Rainbow("\n▶️ #{command.description}").bright
96
+ end
97
+ command.execute
98
+ rescue => e
99
+ unless ENV["QUIET_SETUP"] || ENV["DISABLE_OUTPUT"]
100
+ require "rainbow"
101
+ puts Rainbow("❌ Command #{command.description} failed: #{e.message}").red
102
+ end
103
+ raise e
104
+ end
105
+
106
+ def log(message)
107
+ logger.info message
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Discharger
4
+ module SetupRunner
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "setup_runner/version"
4
+ require_relative "setup_runner/configuration"
5
+ require_relative "setup_runner/command_registry"
6
+ require_relative "setup_runner/command_factory"
7
+ require_relative "setup_runner/runner"
8
+
9
+ module Discharger
10
+ module SetupRunner
11
+ class << self
12
+ def configure
13
+ yield configuration if block_given?
14
+ end
15
+
16
+ def configuration
17
+ @configuration ||= Configuration.new
18
+ end
19
+
20
+ def run(config_path = nil, logger = nil)
21
+ config = config_path ? Configuration.from_file(config_path) : configuration
22
+ runner = Runner.new(config, Dir.pwd, logger)
23
+ yield runner if block_given?
24
+ runner.run
25
+ end
26
+
27
+ # Extension points for adding custom commands
28
+ def register_command(name, command_class)
29
+ CommandRegistry.register(name, command_class)
30
+ end
31
+
32
+ def unregister_command(name)
33
+ # Re-register all commands except the one to remove
34
+ all_commands = {}
35
+ CommandRegistry.names.each do |cmd_name|
36
+ unless cmd_name == name.to_s
37
+ all_commands[cmd_name] = CommandRegistry.get(cmd_name)
38
+ end
39
+ end
40
+ CommandRegistry.clear
41
+ all_commands.each do |cmd_name, cmd_class|
42
+ CommandRegistry.register(cmd_name, cmd_class)
43
+ end
44
+ end
45
+
46
+ def list_commands
47
+ CommandRegistry.names
48
+ end
49
+
50
+ def get_command(name)
51
+ CommandRegistry.get(name)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,3 +1,3 @@
1
1
  module Discharger
2
- VERSION = "0.2.10"
2
+ VERSION = "0.2.11"
3
3
  end
data/lib/discharger.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "discharger/version"
2
2
  require "discharger/railtie"
3
+ require "discharger/setup_runner"
3
4
 
4
5
  module Discharger
5
6
  class << self
@@ -3,9 +3,23 @@ module Discharger
3
3
  class InstallGenerator < Rails::Generators::Base
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
+ class_option :setup_path,
7
+ type: :string,
8
+ default: "bin/setup",
9
+ desc: "Path where the setup script should be created"
10
+
6
11
  def copy_initializer
7
12
  template "discharger_initializer.rb", "config/initializers/discharger.rb"
8
13
  end
14
+
15
+ def create_setup_script
16
+ template "setup", options[:setup_path]
17
+ chmod options[:setup_path], 0o755
18
+ end
19
+
20
+ def create_sample_setup_yml
21
+ template "setup.yml", "config/setup.yml"
22
+ end
9
23
  end
10
24
  end
11
25
  end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ require "fileutils"
3
+ require "pathname"
4
+ require "bundler/setup"
5
+
6
+ # Path to the application root.
7
+ APP_ROOT = File.expand_path("..", __dir__)
8
+
9
+ FileUtils.chdir APP_ROOT do
10
+ # This script uses Discharger to set up your development environment automatically.
11
+ # All setup steps are configured in config/setup.yml
12
+ # This script is idempotent, so you can run it at any time and get an expectable outcome.
13
+
14
+ unless File.exist?("Gemfile")
15
+ puts "No Gemfile found. Please run this script from the root of your Rails application."
16
+ exit 1
17
+ end
18
+
19
+ unless File.exist?("config/setup.yml")
20
+ puts "No config/setup.yml found. Please run 'rails generate discharger:install' first."
21
+ exit 1
22
+ end
23
+
24
+ puts "== Running Discharger setup =="
25
+ puts "Configuration loaded from: config/setup.yml"
26
+
27
+ # Load Rails environment first, then discharger
28
+ require_relative "../config/application"
29
+ Rails.application.initialize!
30
+
31
+ # Load the discharger gem and run the setup
32
+ require "discharger"
33
+ Discharger::SetupRunner.run("config/setup.yml")
34
+
35
+ puts "\n== Setup completed successfully! =="
36
+ end