console_kit 0.1.3 → 0.1.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: 2171674e2c587a2bb53387614758fefb8c6542f7ddb88c1b9dc55f85bf5a481a
4
- data.tar.gz: 0fe05d8f5e3ee8d8814bba1b1eef1c3233b5c1996d805a4e2b06615326771480
3
+ metadata.gz: 634e983d59fba9a10d608945d11f0be809c4b3008612c804489a0ee18fd8bcf0
4
+ data.tar.gz: 646bf39efa3b81058524433d88852906313c20e0b584b1e205386bae45b8e638
5
5
  SHA512:
6
- metadata.gz: 763f319a96484ab677cad3eab21cc929e2775a98cf3d085db4fd317db8523f8220a3bcd808cedb3df857d193d1da9544de47a1263a8192a50888352e2fe1a575
7
- data.tar.gz: 7acf3b5134fd7155ca4ceaadd44d07e93bf2f48fe322d4133055e11a8436157dcdf7b3ed7b303c76149674beccbd26780d5883590659aa040a8f55f8a7792cf0
6
+ metadata.gz: 70f8f1282f3e6a63588187a86de442b663f17efd40773f7ad6c2916a21917549917d5fa9fba688774e18608f173e8126cee711cb468faf94efdda1ef7653151d
7
+ data.tar.gz: 436bfcb527f71341dd2f6fe1b865a884b4e4208a4dec75820e2bc46068de85db706f2e037e2b1aa94f07091600e204c35ef89cff612dd2bb103ae6d1baa414f7
data/.reek.yml ADDED
@@ -0,0 +1,4 @@
1
+ detectors:
2
+ UncommunicativeVariableName:
3
+ exclude:
4
+ - e
data/.rubocop.yml CHANGED
@@ -1,5 +1,10 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 3.1
3
+ NewCops: enable
4
+
5
+ plugins:
6
+ - rubocop-rspec
7
+ - rubocop-rake
3
8
 
4
9
  Metrics/BlockLength:
5
10
  Exclude:
data/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@ This project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.1.4] - 2025-09-30
10
+ ### Added
11
+ - Minor Fixes and Improvements
12
+
13
+ ---
14
+
9
15
  ## [0.1.3] - 2025-08-12
10
16
  ### Added
11
17
  - `ConsoleKit.current_tenant` method to retrieve the current tenant at runtime.
@@ -50,6 +56,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
50
56
 
51
57
  ## [Unreleased]
52
58
 
59
+ [0.1.4]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v0.1.4
53
60
  [0.1.3]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v0.1.3
54
61
  [0.1.2]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v0.1.2
55
62
  [0.1.1]: https://github.com/Soumyadeep-ai/console_kit/releases/tag/v0.1.1
data/SECURITY.md ADDED
@@ -0,0 +1,28 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ We do **not maintain multiple supported versions**. Only the latest released version is eligible for security updates.
6
+
7
+ Once a new version is released, the previous version is branched and locked, and will no longer receive updates — including security patches.
8
+
9
+ | Version | Supported |
10
+ | ------- | ------------------ |
11
+ | 0.1.4 | :white_check_mark: |
12
+ | 0.1.3 | :x: |
13
+ | 0.1.2 | :x: |
14
+ | 0.1.1 | :x: |
15
+ | 0.1.0 | :x: |
16
+
17
+ ## Reporting a Vulnerability
18
+
19
+ If you discover a security vulnerability, please use the GitHub [Security Advisories](https://github.com/Soumyadeep-ai/console_kit/security/advisories/new) feature to report it privately.
20
+
21
+ We follow this process:
22
+ - All reported vulnerabilities are reviewed promptly.
23
+ - A new version with security fixes will be released.
24
+ - Once validated, we will publish an advisory and issue a patched release if necessary.
25
+
26
+ Please avoid disclosing vulnerabilities publicly until we’ve had a chance to review and respond.
27
+
28
+ For more details, visit [GitHub Docs: Reporting a Vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability).
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- # Stores configurations
4
+ # Stores ConsoleKit configurations such as tenant map and context behavior
5
5
  class Configuration
6
- attr_accessor :pretty_output, :tenants, :context_class
6
+ attr_reader :pretty_output, :tenants, :context_class
7
7
 
8
- def initialize(pretty_output: true, tenants: nil, context_class: nil)
9
- @pretty_output = pretty_output
8
+ def initialize(tenants: nil, context_class: nil)
9
+ @pretty_output = true
10
10
  @tenants = tenants
11
11
  @context_class = context_class
12
12
  end
13
+
14
+ %i[pretty_output tenants context_class].each do |attr|
15
+ define_method("#{attr}=") { |value| instance_variable_set("@#{attr}", value) }
16
+ end
13
17
  end
14
18
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConsoleKit
4
+ module Connections
5
+ # Parent class for connection handlers
6
+ class BaseConnectionHandler
7
+ attr_reader :context
8
+
9
+ def initialize(context) = @context = context
10
+
11
+ def connect
12
+ raise NotImplementedError, "#{self.class} must implement #connect"
13
+ end
14
+
15
+ def available? = false
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sql_connection_handler'
4
+ require_relative 'mongo_connection_handler'
5
+
6
+ module ConsoleKit
7
+ module Connections
8
+ # Manages available connection handlers
9
+ class ConnectionManager
10
+ class << self
11
+ def available_handlers(context)
12
+ handler_classes.filter_map do |klass|
13
+ handler = klass.new(context)
14
+ handler if handler.available?
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def handler_classes = BaseConnectionHandler.descendants
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'base_connection_handler'
5
+
6
+ module ConsoleKit
7
+ module Connections
8
+ # Handles MongoDB connections
9
+ class MongoConnectionHandler < BaseConnectionHandler
10
+ extend Forwardable
11
+
12
+ def_delegator :@context, :tenant_mongo_db
13
+
14
+ def connect
15
+ return if tenant_mongo_db.blank?
16
+
17
+ Output.print_info("Switching to MongoDB client: #{tenant_mongo_db}")
18
+ Mongoid.override_client(tenant_mongo_db)
19
+ rescue NoMethodError
20
+ Output.print_warning('Mongoid.override_client is not defined.')
21
+ end
22
+
23
+ def available? = defined?(Mongoid)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+ require_relative 'base_connection_handler'
5
+
6
+ module ConsoleKit
7
+ module Connections
8
+ # Handles SQL connections
9
+ class SqlConnectionHandler < BaseConnectionHandler
10
+ extend Forwardable
11
+
12
+ def_delegator :@context, :tenant_shard
13
+
14
+ def connect
15
+ return if tenant_shard.blank?
16
+
17
+ Output.print_info("Establishing SQL connection to shard: #{tenant_shard}")
18
+ ApplicationRecord.establish_connection(tenant_shard.to_sym)
19
+ end
20
+
21
+ def available? = defined?(ApplicationRecord)
22
+ end
23
+ end
24
+ end
@@ -16,30 +16,33 @@ module ConsoleKit
16
16
 
17
17
  class << self
18
18
  TYPES.each_key do |type|
19
- define_method("print_#{type}") do |text|
19
+ define_method("print_#{type}") do |text, timestamp: false|
20
20
  formatted = (type == :header ? "\n=== #{text} ===" : text)
21
- print_with(type, formatted)
21
+ print_with(type, formatted, timestamp)
22
22
  end
23
23
  end
24
24
 
25
+ # Backtrace prints always with timestamp, no param
25
26
  def print_backtrace(exception)
26
- exception&.backtrace&.each { |line| print_with(:trace, " #{line}") }
27
+ exception&.backtrace&.each { |line| print_with(:trace, " #{line}", true) }
27
28
  end
28
29
 
29
30
  private
30
31
 
31
- def print_with(type, text, timestamp: false)
32
- meta = TYPES[type]
32
+ def print_with(type, text, timestamp)
33
+ meta = TYPES.fetch(type)
33
34
  message = build_message(text, meta[:symbol], timestamp)
34
35
  output(message, meta[:color])
35
36
  end
36
37
 
37
38
  def build_message(text, symbol, timestamp)
38
- time = timestamp ? "[#{Time.current.strftime('%Y-%m-%d %H:%M:%S')}] " : ''
39
- sym = symbol ? "#{symbol} " : ''
40
- "#{PREFIX} #{time}#{sym}#{text}"
39
+ "#{PREFIX} #{timestamp_prefix(timestamp)}#{symbol_prefix(symbol)}#{text}"
41
40
  end
42
41
 
42
+ def prefix_for(value) = value ? yield(value) : ''
43
+ def timestamp_prefix(timestamp) = prefix_for(timestamp) { Time.current.strftime('[%Y-%m-%d %H:%M:%S] ') }
44
+ def symbol_prefix(symbol) = prefix_for(symbol) { |sym| "#{sym} " }
45
+
43
46
  def output(message, color)
44
47
  return puts message unless ConsoleKit.configuration.pretty_output && color
45
48
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- # Railtie
4
+ # Railtie for integrating ConsoleKit with Rails console.
5
5
  class Railtie < Rails::Railtie
6
6
  console { ConsoleKit::Setup.setup }
7
7
  end
@@ -11,34 +11,39 @@ module ConsoleKit
11
11
  class << self
12
12
  attr_reader :current_tenant
13
13
 
14
- def setup
15
- return Output.print_error('No tenants configured.') if no_tenants?
16
-
17
- key = select_tenant_key
18
- return Output.print_error('No tenant selected. Loading without tenant configuration.') unless key
19
-
20
- configure(key)
21
- rescue StandardError => e
22
- handle_error(e)
23
- end
24
-
14
+ def setup = run_setup
25
15
  def tenant_setup_successful? = !@current_tenant.to_s.empty?
26
16
 
27
17
  def reset_current_tenant
28
18
  return warn_no_tenants unless tenants?
29
19
 
30
20
  warn_reset if @current_tenant
31
- TenantConfigurator.clear(ConsoleKit.configuration.context_class) if @current_tenant
21
+ TenantConfigurator.clear if @current_tenant
32
22
 
33
23
  @current_tenant = nil
34
24
  setup
35
- tenant_setup_successful?
36
25
  end
37
26
 
38
27
  private
39
28
 
29
+ def run_setup
30
+ return Output.print_error('No tenants configured.') if no_tenants?
31
+
32
+ select_and_configure
33
+ rescue StandardError => e
34
+ handle_error(e)
35
+ end
36
+
37
+ def select_and_configure
38
+ key = select_tenant_key
39
+ return Output.print_error('No tenant selected. Loading without tenant configuration.') unless key
40
+
41
+ configure(key)
42
+ end
43
+
40
44
  def configure(key)
41
- return unless TenantConfigurator.configure_tenant(key, tenants, context_class)
45
+ TenantConfigurator.configure_tenant(key)
46
+ return unless TenantConfigurator.configuration_success
42
47
 
43
48
  @current_tenant = key
44
49
  Output.print_success("Tenant initialized: #{key}")
@@ -48,25 +53,12 @@ module ConsoleKit
48
53
  def context_class = ConsoleKit.configuration.context_class
49
54
  def tenants? = tenants&.any?
50
55
  def no_tenants? = !tenants?
51
-
52
- def select_tenant_key
53
- return tenants.keys.first if auto_select?
54
-
55
- TenantSelector.select(tenants, tenants.keys)
56
- end
57
-
56
+ def select_tenant_key = auto_select? ? tenants.keys.first : TenantSelector.select
58
57
  def auto_select? = single_tenant? || non_interactive?
59
58
  def single_tenant? = tenants.size == 1
60
59
  def non_interactive? = !$stdin.tty?
61
-
62
- def warn_no_tenants
63
- Output.print_warning('Cannot reset tenant: No tenants configured.')
64
- false
65
- end
66
-
67
- def warn_reset
68
- Output.print_warning("Resetting tenant: #{@current_tenant}")
69
- end
60
+ def warn_no_tenants = Output.print_warning('Cannot reset tenant: No tenants configured.')
61
+ def warn_reset = Output.print_warning("Resetting tenant: #{@current_tenant}")
70
62
 
71
63
  def handle_error(error)
72
64
  Output.print_error("Error setting up tenant: #{error.message}")
@@ -1,29 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'output'
4
+ require_relative 'connections/connection_manager'
4
5
 
5
6
  module ConsoleKit
6
7
  # For tenant configuration
7
8
  module TenantConfigurator
8
9
  class << self
9
- def configure_tenant(key, tenants, context_class)
10
- constants = tenants[key]&.[](:constants)
11
- return missing_config_error(key) unless constants
10
+ attr_reader :configuration_success
12
11
 
13
- validate_constants!(constants)
14
- apply_context(context_class, constants)
15
- setup_connections(context_class)
12
+ def configure_tenant(key)
13
+ constants = ConsoleKit.configuration.tenants[key]&.[](:constants)
14
+ return missing_config_error(key) unless constants
16
15
 
17
- Output.print_success("Tenant set to: #{key}")
18
- true
16
+ perform_configuration(key, constants)
19
17
  rescue StandardError => e
20
18
  handle_error(e, key)
21
- false
22
19
  end
23
20
 
24
- def clear(context_class)
21
+ def clear
22
+ @configuration_success = false
25
23
  %i[tenant_shard tenant_mongo_db partner_identifier].each do |attr|
26
- context_class.public_send("#{attr}=", nil)
24
+ ConsoleKit.configuration.context_class.public_send("#{attr}=", nil)
27
25
  end
28
26
  Output.print_info('Tenant context has been cleared.')
29
27
  end
@@ -36,34 +34,36 @@ module ConsoleKit
36
34
  end
37
35
 
38
36
  def missing_config_error(key)
37
+ @configuration_success = false
39
38
  Output.print_error("No configuration found for tenant: #{key}")
40
- false
41
39
  end
42
40
 
43
- def apply_context(ctx, constant)
41
+ def perform_configuration(key, constants)
42
+ validate_constants!(constants)
43
+ apply_context(constants)
44
+ configure_success(key)
45
+ end
46
+
47
+ def apply_context(constant)
48
+ ctx = ConsoleKit.configuration.context_class
44
49
  ctx.tenant_shard = constant[:shard]
45
50
  ctx.tenant_mongo_db = constant[:mongo_db]
46
51
  ctx.partner_identifier = constant[:partner_code]
47
- end
48
-
49
- def setup_connections(ctx)
50
- ApplicationRecord.establish_connection(ctx.tenant_shard.to_sym) if defined?(ApplicationRecord)
51
- return unless defined?(Mongoid) && Mongoid.respond_to?(:override_client)
52
- return if ctx.tenant_mongo_db.nil? || ctx.tenant_mongo_db.empty?
53
52
 
54
- client = ctx.tenant_mongo_db.to_s
55
- Mongoid.override_client(client)
53
+ setup_connections(ctx)
56
54
  end
57
55
 
58
- def setup_database_connections(context_class)
59
- ApplicationRecord.establish_connection(context_class.tenant_shard.to_sym) if defined?(ApplicationRecord)
60
- return unless defined?(Mongoid) && Mongoid.respond_to?(:override_client)
61
- return if context_class.tenant_mongo_db.nil? || context_class.tenant_mongo_db.empty?
56
+ def setup_connections(context)
57
+ ConsoleKit::Connections::ConnectionManager.available_handlers(context).each(&:connect)
58
+ end
62
59
 
63
- Mongoid.override_client(context_class.tenant_mongo_db.to_s)
60
+ def configure_success(key)
61
+ Output.print_success("Tenant set to: #{key}")
62
+ @configuration_success = true
64
63
  end
65
64
 
66
65
  def handle_error(error, key)
66
+ @configuration_success = false
67
67
  Output.print_error("Failed to configure tenant '#{key}': #{error.message}")
68
68
  Output.print_backtrace(error)
69
69
  end
@@ -5,51 +5,64 @@ require_relative 'output'
5
5
  module ConsoleKit
6
6
  # For tenant selection
7
7
  module TenantSelector
8
+ RETRY_LIMIT = 3
9
+ DEFAULT_SELECTION = '1'
10
+
8
11
  class << self
9
- def select(tenants, keys)
10
- print_tenant_selection_menu(tenants, keys)
12
+ def select
13
+ attempt_selection(RETRY_LIMIT)
14
+ end
11
15
 
12
- 3.times do |attempt|
13
- index = prompt_user_for_selection(keys.size)
14
- return nil if index.zero?
15
- return keys[index - 1] if index.positive?
16
+ private
16
17
 
17
- print_tenant_selection_menu(tenants, keys) if attempt < 2
18
- end
18
+ def attempt_selection(retries_left)
19
+ return nil if retries_left.zero?
19
20
 
20
- nil
21
+ print_tenant_selection_menu
22
+ selection = parse_user_selection
23
+ selection ? resolve_selection(selection) : attempt_selection(retries_left - 1)
21
24
  end
22
25
 
23
- private
24
-
25
- def print_tenant_selection_menu(tenants, keys)
26
+ def print_tenant_selection_menu
26
27
  Output.print_header('Multiple tenants detected. Please choose one:')
27
28
  Output.print_info(' 0. Load without tenant (no tenant configuration)')
28
29
 
29
- keys.each_with_index do |key, index|
30
- partner = tenants.dig(key, :constants, :partner_code) || 'N/A'
31
- Output.print_info(" #{index + 1}. #{key} (partner: #{partner})")
30
+ ConsoleKit.tenants.keys.each_with_index do |key, index|
31
+ Output.print_info(" #{index + 1}. #{key} (partner: #{tenant_partner(key)})")
32
32
  end
33
33
  end
34
34
 
35
- def prompt_user_for_selection(max_index)
36
- Output.print_prompt("\nEnter the number of the tenant you want (or press Enter for default '1'): ")
37
- input = $stdin.gets&.chomp&.strip
38
- input = '1' if input.to_s.empty?
39
- return invalid_input_response unless valid_integer?(input)
35
+ def tenant_partner(key) = ConsoleKit.tenants.dig(key, :constants, :partner_code) || 'N/A'
40
36
 
41
- parsed_index = input.to_i
42
- return invalid_range_response(max_index) unless parsed_index.between?(0, max_index)
37
+ def parse_user_selection
38
+ input = read_input_with_default
39
+ return handle_invalid_input('Invalid input. Please enter a number.') unless valid_integer?(input)
43
40
 
44
- parsed_index
41
+ index = input.to_i
42
+ unless valid_selection_index?(index)
43
+ return handle_invalid_input("Selection must be between 0 and #{max_index}.")
44
+ end
45
+
46
+ index
47
+ end
48
+
49
+ def read_input_with_default
50
+ prompt_message = "\nEnter the number of the tenant you want " \
51
+ "(or press Enter for default '#{DEFAULT_SELECTION}'): "
52
+ Output.print_prompt(prompt_message)
53
+ input = $stdin.gets&.chomp&.strip
54
+ input.to_s.empty? ? DEFAULT_SELECTION : input
45
55
  end
46
56
 
57
+ def handle_invalid_input(message) = Output.print_warning(message).then { nil }
47
58
  def valid_integer?(input) = input.match?(/\A\d+\z/)
48
- def invalid_input_response = Output.print_warning('Invalid input. Please enter a number.').then { - 1 }
59
+ def max_index = ConsoleKit.tenants.size
60
+ def valid_selection_index?(index) = index.between?(0, max_index)
61
+
62
+ def resolve_selection(index)
63
+ return nil if index.zero?
49
64
 
50
- def invalid_range_response(max_index)
51
- Output.print_warning("Selection must be between 0 and #{max_index}.")
52
- -1
65
+ ConsoleKit.tenants.keys[index - 1]
53
66
  end
54
67
  end
55
68
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsoleKit
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.4'
5
5
  end
data/lib/console_kit.rb CHANGED
@@ -7,6 +7,7 @@ require_relative 'console_kit/railtie' if defined?(Rails::Railtie)
7
7
 
8
8
  # Main module for console kit
9
9
  module ConsoleKit
10
+ # Base error class for ConsoleKit-related exceptions.
10
11
  class Error < StandardError; end
11
12
 
12
13
  class << self
@@ -12,12 +12,13 @@ module ConsoleKit
12
12
  class_option :force, type: :boolean, default: false, desc: 'Overwrite existing files'
13
13
 
14
14
  def copy_initializer
15
+ force = options[:force]
15
16
  initializer_path = Rails.root.join('config', 'initializers', 'console_kit.rb')
16
17
 
17
- if File.exist?(initializer_path) && !options[:force]
18
+ if File.exist?(initializer_path) && !force
18
19
  say_status :skipped, "Initializer already exists: #{initializer_path}", :yellow
19
20
  else
20
- template 'console_kit.rb', 'config/initializers/console_kit.rb', force: options[:force]
21
+ template 'console_kit.rb', 'config/initializers/console_kit.rb', force: force
21
22
  say_status :created, "Initializer generated at #{initializer_path}", :green
22
23
  end
23
24
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: console_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Soumyadeep Pal
@@ -44,6 +44,7 @@ executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
+ - ".reek.yml"
47
48
  - ".rspec"
48
49
  - ".rubocop.yml"
49
50
  - CHANGELOG.md
@@ -51,8 +52,13 @@ files:
51
52
  - LICENSE.txt
52
53
  - README.md
53
54
  - Rakefile
55
+ - SECURITY.md
54
56
  - lib/console_kit.rb
55
57
  - lib/console_kit/configuration.rb
58
+ - lib/console_kit/connections/base_connection_handler.rb
59
+ - lib/console_kit/connections/connection_manager.rb
60
+ - lib/console_kit/connections/mongo_connection_handler.rb
61
+ - lib/console_kit/connections/sql_connection_handler.rb
56
62
  - lib/console_kit/output.rb
57
63
  - lib/console_kit/railtie.rb
58
64
  - lib/console_kit/setup.rb
@@ -70,6 +76,7 @@ metadata:
70
76
  homepage_uri: https://github.com/Soumyadeep-ai/console_kit
71
77
  source_code_uri: https://github.com/Soumyadeep-ai/console_kit
72
78
  changelog_uri: https://github.com/Soumyadeep-ai/console_kit/blob/main/CHANGELOG.md
79
+ rubygems_mfa_required: 'true'
73
80
  rdoc_options: []
74
81
  require_paths:
75
82
  - lib