port_of_call 0.1.0.alpha1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c5f861789c53684db6036135fcb5eb0057c90ea56c95aedd7fabd4cc85c9c0b2
4
+ data.tar.gz: 0eab14311169be6984ed29ac0a531b0c0ac0f1da8a403dc7af2561a098969267
5
+ SHA512:
6
+ metadata.gz: 984c62a2c7dec18962070b1dd51b7a0149a21f4caa57c39a42832e5e460671f4ccc55a82445158102d26eb29a27a27f98a99ab7270ec1af9ac5fd900c71427cd
7
+ data.tar.gz: fcf8f557fa7763042ef1f0aae10f416d101d83413f9d9c5ec869266e217d3931b31dc7dc33afad5ef3cbfbc7f0bb107f9fb2943ae740945dd418a8f3fc59a878
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0.alpha1] - 2025-03-25
9
+
10
+ ### Added
11
+ - Initial pre-release of Port of Call gem
12
+ - Core port calculation functionality
13
+ - Project name extraction from git or directory
14
+ - Deterministic SHA256 hashing algorithm
15
+ - Configurable port range mapping
16
+ - Rails integration via Railtie
17
+ - Automatic port assignment for Rails server
18
+ - Generator for configuration initializer
19
+ - Command-line interface
20
+ - Server command for starting Rails
21
+ - Port command for displaying calculated port
22
+ - Set command for setting default port
23
+ - Version and help commands
24
+ - Rake tasks for common operations
25
+ - port_of_call:port to show calculated port
26
+ - port_of_call:start to start server
27
+ - port_of_call:install to generate initializer
28
+ - port_of_call:set_default to update development.rb
29
+ - port_of_call:check to verify port availability
30
+ - port_of_call:info to display configuration
31
+ - Configuration options
32
+ - Custom port range
33
+ - Base port setting
34
+ - Project name override
35
+ - Reserved ports list
data/LICENSE.txt ADDED
@@ -0,0 +1,31 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Jonathan P. Berger and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
23
+ ADDITIONAL DISCLAIMER:
24
+ This software is provided for educational and informational purposes only.
25
+ The author(s) of this software take no responsibility for any conflicts, errors,
26
+ or system issues that may arise from using this software. Users are advised
27
+ to thoroughly test this software in a controlled environment before deploying
28
+ it in production settings. Port conflicts, networking issues, or any other
29
+ problems that may occur when using this software are solely the responsibility
30
+ of the user. By using this software, you agree to hold the author(s) harmless
31
+ from any liability.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ ⛵️ Why use Port of Call?
2
+ =============================
3
+ Do you have multiple Rails apps running on your machine? Tired of conflicts on port 3000? I was, so I vibe-coded this gem.
4
+
5
+ Port of Call automatically assigns deterministic port numbers to each Rails application based on its name.
6
+
7
+ ⛵️⛵️ Who's Port of Call for?
8
+ =============================
9
+ Rails developers who tend to work on multiple applications simultaneously
10
+
11
+ ⛵️⛵️⛵️ What exactly does Port of Call do?
12
+ =============================
13
+ Port of Call deterministically assigns port numbers to Rails applications:
14
+
15
+ 1. It extracts your application's name from Git or directory name
16
+ 2. It creates a hash of the name using SHA256
17
+ 3. It maps this hash to a port number within your configured range (default: 3000-3999)
18
+ 4. It automatically sets this port when you start your Rails server
19
+
20
+ The same app always gets the same port on any machine, avoiding conflicts!
21
+
22
+ ⛵️⛵️⛵️⛵️ How do I use it?
23
+ =============================
24
+ 1. Install the gem:
25
+ ```ruby
26
+ # In your Gemfile
27
+ gem 'port_of_call'
28
+ ```
29
+
30
+ 2. Bundle install:
31
+ ```bash
32
+ $ bundle install
33
+ ```
34
+
35
+ 3. Generate the initializer (optional):
36
+ ```bash
37
+ $ rails generate port_of_call:install
38
+ ```
39
+
40
+ 4. Run your server as usual:
41
+ ```bash
42
+ $ rails server
43
+ ```
44
+
45
+ CLI Commands:
46
+ - `port_of_call server` - Start Rails server with calculated port
47
+ - `port_of_call port` - Show the calculated port
48
+ - `port_of_call set` - Set as default port in development.rb
49
+ - `port_of_call -v` - Show version information
50
+ - `port_of_call -h` - Show help
51
+
52
+ Rake Tasks:
53
+ - `rake port_of_call` - Show calculated port
54
+ - `rake port_of_call:start` - Start Rails server
55
+ - `rake poc` - Shorthand for starting the server
56
+
57
+ ⛵️⛵️⛵️⛵️⛵️ Extras
58
+ =============================
59
+ Configuration:
60
+ ```ruby
61
+ # In config/initializers/port_of_call.rb
62
+ PortOfCall.configure do |config|
63
+ # Change port range (default: 3000..3999)
64
+ config.port_range = 4000..4999
65
+
66
+ # Set custom project name
67
+ config.project_name = "my_unique_app_name"
68
+
69
+ # Avoid specific ports
70
+ config.reserved_ports = [4567, 5000]
71
+ end
72
+ ```
73
+
74
+ Troubleshooting:
75
+ - If your port is already in use, Port of Call will warn you
76
+ - To check port availability: `rake port_of_call:check`
77
+ - For detailed info: `rake port_of_call:info`
78
+
79
+ GitHub: [github.com/jonathanpberger/port-of-call](https://github.com/jonathanpberger/port-of-call)
80
+
81
+ License: MIT with additional [disclaimer](LICENSE.txt)
82
+ ```
83
+
84
+ ## Development
85
+
86
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt.
data/RELEASE_NOTES.md ADDED
@@ -0,0 +1,62 @@
1
+ # Port of Call 0.1.0 Release Notes
2
+
3
+ We're excited to announce the initial release of Port of Call!
4
+
5
+ Port of Call is a Ruby gem that assigns each Rails application a consistent, deterministic port number based on the application's name or repository. This solves the common conflict of multiple Rails apps all defaulting to port 3000.
6
+
7
+ ## Features
8
+
9
+ ### Automatic Port Assignment
10
+ - Simply install the gem, and your Rails server will use a unique port based on your project name
11
+ - No configuration required - it just works!
12
+
13
+ ### Deterministic Ports
14
+ - Same project always gets the same port on any machine
15
+ - Consistent development experience across your team
16
+
17
+ ### Multiple Interface Options
18
+ - Command-line interface with intuitive commands
19
+ - Rake tasks for easy integration
20
+ - Ruby API for programmatic use
21
+
22
+ ### Customization
23
+ - Configurable port range
24
+ - Custom project name option
25
+ - Reserved ports list
26
+
27
+ ## Installation
28
+
29
+ Add this line to your application's Gemfile:
30
+
31
+ ```ruby
32
+ gem 'port_of_call'
33
+ ```
34
+
35
+ And then execute:
36
+
37
+ ```bash
38
+ $ bundle install
39
+ $ rails generate port_of_call:install # Optional
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Start your Rails server as usual:
45
+
46
+ ```bash
47
+ $ rails server
48
+ ```
49
+
50
+ You should see output indicating the port that Port of Call has assigned.
51
+
52
+ ## Documentation
53
+
54
+ For detailed usage instructions, see the [README](README.md).
55
+
56
+ ## Feedback
57
+
58
+ We welcome feedback, bug reports, and feature requests! Please submit issues on GitHub.
59
+
60
+ ---
61
+
62
+ Thank you for using Port of Call!
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is an example of how to use Port of Call programmatically
4
+
5
+ require "port_of_call"
6
+
7
+ # Get the calculated port for the current project
8
+ puts "Project name: #{PortOfCall.project_name}"
9
+ puts "Calculated port: #{PortOfCall.calculate_port}"
10
+
11
+ # Check if the port is available
12
+ port = PortOfCall.calculate_port
13
+ if PortOfCall.port_in_use?(port)
14
+ puts "Port #{port} is already in use by another process"
15
+ else
16
+ puts "Port #{port} is available"
17
+ end
18
+
19
+ # Configure Port of Call
20
+ PortOfCall.configure do |config|
21
+ # Use a different port range
22
+ config.port_range = 4000..4999
23
+
24
+ # Set a different base port
25
+ config.base_port = 4000
26
+
27
+ # Set a custom project name
28
+ config.project_name = "my_custom_project"
29
+
30
+ # Reserve specific ports
31
+ config.reserved_ports = [4000, 4001, 4002]
32
+ end
33
+
34
+ # Get the new calculated port after configuration
35
+ puts "New project name: #{PortOfCall.project_name}"
36
+ puts "New calculated port: #{PortOfCall.calculate_port}"
37
+
38
+ # Get server options hash (useful for Rails server)
39
+ options = PortOfCall.server_options
40
+ puts "Server options: #{options.inspect}"
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file demonstrates how to use Port of Call in a Rails application
4
+
5
+ # In your Gemfile:
6
+ # -----------------
7
+ # gem 'port_of_call'
8
+
9
+
10
+ # In config/initializers/port_of_call.rb:
11
+ # ----------------------------------------
12
+ # PortOfCall.configure do |config|
13
+ # # The port range to use (default: 3000..3999)
14
+ # config.port_range = 3000..3999
15
+ #
16
+ # # The base port to start from (default: 3000)
17
+ # config.base_port = 3000
18
+ #
19
+ # # Optional: manually set a project name (default: nil, auto-detected)
20
+ # # config.project_name = "my_custom_app_name"
21
+ #
22
+ # # Optional: ports to avoid (default: [])
23
+ # # config.reserved_ports = [3333, 4567]
24
+ # end
25
+
26
+
27
+ # In your terminal:
28
+ # -----------------
29
+ # Show the calculated port:
30
+ # $ rake port_of_call
31
+ #
32
+ # Start the server with the calculated port:
33
+ # $ rails server
34
+ #
35
+ # Or use the CLI:
36
+ # $ port_of_call server # Start the server
37
+ # $ port_of_call port # Show the port
38
+ # $ port_of_call set # Set as default port
39
+ #
40
+ # Use the shortcut task:
41
+ # $ rake poc # Start the server
42
+
43
+
44
+ # In your Ruby code:
45
+ # ------------------
46
+ # # Get the calculated port:
47
+ # port = PortOfCall.calculate_port
48
+ #
49
+ # # Get the project name:
50
+ # name = PortOfCall.project_name
51
+ #
52
+ # # Check if the port is available:
53
+ # if PortOfCall.port_in_use?(port)
54
+ # puts "Port #{port} is in use"
55
+ # end
56
+ #
57
+ # # Get server options for Rails:
58
+ # options = PortOfCall.server_options # Returns { Port: calculated_port }
data/exe/port_of_call ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "port_of_call"
5
+
6
+ # This executable provides command-line functionality for Port of Call
7
+ PortOfCall::CLI.start(ARGV)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module PortOfCall
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Creates a Port of Call initializer for your application"
11
+
12
+ def copy_initializer
13
+ template "initializer.rb", "config/initializers/port_of_call.rb"
14
+ end
15
+
16
+ def show_readme
17
+ readme "README.md" if behavior == :invoke
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # Port of Call
2
+
3
+ ## Next Steps
4
+
5
+ Port of Call has been installed and configured. Your Rails server will now
6
+ automatically use a unique, deterministic port based on your project name.
7
+
8
+ ### Usage
9
+
10
+ To start your Rails server with the calculated port:
11
+
12
+ ```bash
13
+ $ rails server
14
+ ```
15
+
16
+ To see the calculated port:
17
+
18
+ ```bash
19
+ $ rake port_of_call
20
+ ```
21
+
22
+ ### Configuration
23
+
24
+ Your configuration file is at `config/initializers/port_of_call.rb`.
25
+ You can edit this file to customize the behavior of Port of Call.
26
+
27
+ For more information, see: https://github.com/jpb/port-of-claude
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Port of Call Configuration
4
+ #
5
+ # This initializer configures the Port of Call gem, which automatically
6
+ # assigns a unique, deterministic port to your Rails application.
7
+ #
8
+ # For more information, see: https://github.com/jpb/port-of-claude
9
+
10
+ PortOfCall.configure do |config|
11
+ # The port range to use (default: 3000..3999)
12
+ # config.port_range = 3000..3999
13
+
14
+ # The base port to start from (default: 3000)
15
+ # config.base_port = 3000
16
+
17
+ # Manually set a project name (default: nil, auto-detected)
18
+ # config.project_name = "my_custom_app_name"
19
+
20
+ # Reserved ports to avoid (default: [])
21
+ # config.reserved_ports = [3333, 4567]
22
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PortOfCall
4
+ # Command Line Interface for Port of Call
5
+ #
6
+ # Provides command-line functionality for interacting with Port of Call.
7
+ # Available commands:
8
+ # - server: Start Rails server with calculated port
9
+ # - port: Show the calculated port
10
+ # - set: Set port as default in development.rb
11
+ # - version: Show version information
12
+ # - help: Display usage information
13
+ class CLI
14
+ # Start the CLI with the given arguments
15
+ # @param args [Array<String>] command-line arguments
16
+ # @return [void]
17
+ def self.start(args)
18
+ new.start(args)
19
+ end
20
+
21
+ # Route to the appropriate command
22
+ # @param args [Array<String>] command-line arguments
23
+ # @return [void]
24
+ def start(args)
25
+ command = args.shift || "server"
26
+
27
+ case command
28
+ when "server", "s"
29
+ start_server(args)
30
+ when "port", "p"
31
+ show_port
32
+ when "set"
33
+ set_default_port
34
+ when "version", "--version", "-v"
35
+ show_version
36
+ when "help", "--help", "-h"
37
+ show_help
38
+ else
39
+ puts "Unknown command: #{command}"
40
+ show_help
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Start the Rails server with the calculated port
47
+ # @param args [Array<String>] additional arguments for the server
48
+ # @return [void]
49
+ def start_server(args)
50
+ # Get the calculated port
51
+ port = PortOfCall.calculate_port
52
+ project_name = PortOfCall.project_name
53
+
54
+ # Check if the port is already in use
55
+ if PortOfCall.port_in_use?(port)
56
+ puts "WARNING: Port #{port} is already in use by another process!"
57
+ exit 1
58
+ end
59
+
60
+ # Construct server command
61
+ if defined?(Rails) && defined?(Rails::Server)
62
+ puts "Port of Call - Starting Rails server for #{project_name} on port #{port}"
63
+
64
+ # Add -p option if not already specified
65
+ unless args.include?("-p") || args.include?("--port")
66
+ args.unshift("-p", port.to_s)
67
+ end
68
+
69
+ # Start server
70
+ Rails::Server.new(args).tap do |server|
71
+ # Set port in server options
72
+ server.options[:Port] = port if server.options[:Port].nil?
73
+
74
+ # Start server
75
+ server.start
76
+ end
77
+ else
78
+ # Fallback when not running in a Rails context
79
+ puts "Port of Call - Rails environment not detected"
80
+ puts "Calculated port for #{project_name}: #{port}"
81
+ end
82
+ end
83
+
84
+ # Show the calculated port
85
+ # @return [void]
86
+ def show_port
87
+ port = PortOfCall.calculate_port
88
+ project_name = PortOfCall.project_name
89
+
90
+ puts "Port of Call - Calculated port for #{project_name}: #{port}"
91
+
92
+ # Check if the port is already in use
93
+ if PortOfCall.port_in_use?(port)
94
+ puts "WARNING: Port #{port} is already in use by another process!"
95
+ end
96
+ end
97
+
98
+ # Set the calculated port as the default in development.rb
99
+ # @return [void]
100
+ def set_default_port
101
+ if defined?(Rails) && defined?(Rails.root)
102
+ # Use rake task for this
103
+ system("rake", "port_of_call:set_default")
104
+ else
105
+ puts "ERROR: Rails environment not detected"
106
+ exit 1
107
+ end
108
+ end
109
+
110
+ # Show version information
111
+ # @return [void]
112
+ def show_version
113
+ puts "Port of Call v#{PortOfCall::VERSION}"
114
+ end
115
+
116
+ # Show help information
117
+ # @return [void]
118
+ def show_help
119
+ puts <<~HELP
120
+ Port of Call v#{PortOfCall::VERSION} - Deterministic port assignment for Rails applications
121
+
122
+ Usage: port_of_call [COMMAND] [OPTIONS]
123
+
124
+ Commands:
125
+ server, s Start the Rails server with the calculated port (default)
126
+ port, p Show the calculated port for this application
127
+ set Set the calculated port as the default in development.rb
128
+ version, -v Display version information
129
+ help, -h Show this help message
130
+
131
+ Examples:
132
+ port_of_call # Start the Rails server with the calculated port
133
+ port_of_call port # Show the calculated port
134
+ port_of_call set # Set the calculated port as the default
135
+ HELP
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PortOfCall
4
+ # Configuration class for Port of Call settings
5
+ #
6
+ # @attr [Range] port_range The range of port numbers to use
7
+ # @attr [Integer] base_port The base port number
8
+ # @attr [String, nil] custom_project_name Optional custom project name to override auto-detection
9
+ # @attr [Array<Integer>] reserved_ports List of ports to avoid using
10
+ class Configuration
11
+ attr_accessor :port_range, :base_port, :custom_project_name, :reserved_ports
12
+
13
+ # Initialize a new configuration with default values
14
+ def initialize
15
+ @port_range = 3000..3999
16
+ @base_port = 3000
17
+ @custom_project_name = nil
18
+ @reserved_ports = []
19
+ end
20
+
21
+ # Get the custom project name
22
+ # @return [String, nil] the custom project name or nil if not set
23
+ def project_name
24
+ @custom_project_name
25
+ end
26
+
27
+ # Set a custom project name
28
+ # @param name [String] the project name to use
29
+ def project_name=(name)
30
+ @custom_project_name = name
31
+ end
32
+
33
+ # Calculate the size of the port range
34
+ # @return [Integer] the number of ports in the range
35
+ def port_range_size
36
+ @port_range.end - @port_range.begin + 1
37
+ end
38
+
39
+ # Check if a port is available within the configuration constraints
40
+ # @param port [Integer] the port to check
41
+ # @return [Boolean] true if the port is in range and not reserved
42
+ def available_port?(port)
43
+ port_range.include?(port) && !reserved_ports.include?(port)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module PortOfCall
6
+ # Calculates deterministic port numbers based on project name
7
+ #
8
+ # This class is responsible for:
9
+ # 1. Extracting the project name from various sources
10
+ # 2. Hashing the project name consistently
11
+ # 3. Mapping the hash to a port number within the configured range
12
+ class PortCalculator
13
+ # @return [Configuration] the configuration instance
14
+ attr_reader :config
15
+
16
+ # Initialize a new calculator with the given configuration
17
+ # @param config [Configuration] the configuration instance
18
+ def initialize(config)
19
+ @config = config
20
+ end
21
+
22
+ # Calculate a deterministic port number for the current project
23
+ # @return [Integer] the calculated port number
24
+ def calculate
25
+ project_name = extract_project_name
26
+ hash_value = hash_project_name(project_name)
27
+ port_from_hash(hash_value)
28
+ end
29
+
30
+ private
31
+
32
+ # Extract the project name using various methods
33
+ # @return [String] the project name
34
+ def extract_project_name
35
+ # Use custom project name if set
36
+ return config.project_name if config.project_name
37
+
38
+ # Try multiple methods to determine the project name
39
+ git_name || directory_name || fallback_name
40
+ end
41
+
42
+ # Try to get the project name from git
43
+ # @return [String, nil] the project name from git, or nil if not available
44
+ def git_name
45
+ return nil unless File.exist?(File.join(root_path, '.git'))
46
+
47
+ # Try to get name from git remote origin URL
48
+ remote_url = `cd #{root_path} && git config --get remote.origin.url 2>/dev/null`.strip
49
+ return extract_git_repo_name(remote_url) unless remote_url.empty?
50
+
51
+ # Fallback to git directory name
52
+ nil
53
+ end
54
+
55
+ # Extract repository name from git URL
56
+ # @param url [String] the git remote URL
57
+ # @return [String, nil] the repository name, or nil if not parseable
58
+ def extract_git_repo_name(url)
59
+ return nil if url.nil? || url.empty?
60
+
61
+ # Match various git URL formats:
62
+ # - https://github.com/user/repo.git
63
+ # - git@github.com:user/repo.git
64
+ # - git://github.com/user/repo
65
+ if url =~ %r{/([^/]+?)(\.git)?$} || url =~ %r{:([^/]+?)(\.git)?$}
66
+ return $1
67
+ end
68
+
69
+ nil
70
+ end
71
+
72
+ # Get the project name from the directory name
73
+ # @return [String] the directory name
74
+ def directory_name
75
+ # Use base name of the root directory
76
+ File.basename(root_path)
77
+ end
78
+
79
+ # Fallback project name to use if all else fails
80
+ # @return [String] the fallback name
81
+ def fallback_name
82
+ # Fallback name in case all else fails
83
+ "rails_app"
84
+ end
85
+
86
+ # Hash the project name to an integer consistently
87
+ # @param name [String] the project name
88
+ # @return [Integer] a hash value
89
+ def hash_project_name(name)
90
+ # Use SHA256 for consistent hashing across platforms
91
+ # Take the first 8 hex chars (32 bits) and convert to integer
92
+ Digest::SHA256.hexdigest(name)[0, 8].to_i(16)
93
+ end
94
+
95
+ # Map a hash value to a port number within the configured range
96
+ # @param hash_value [Integer] the hash value
97
+ # @return [Integer] a port number
98
+ def port_from_hash(hash_value)
99
+ # Get the size of the port range
100
+ range_size = config.port_range.size
101
+
102
+ # Calculate offset based on hash value
103
+ offset = hash_value % range_size
104
+
105
+ # Apply offset to base port
106
+ config.base_port + offset
107
+ end
108
+
109
+ # Get the root path of the application
110
+ # @return [String] the root path
111
+ def root_path
112
+ if defined?(Rails) && Rails.respond_to?(:root)
113
+ Rails.root.to_s
114
+ else
115
+ Dir.pwd
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PortOfCall
4
+ class Railtie < Rails::Railtie
5
+ # Load rake tasks
6
+ rake_tasks do
7
+ load File.expand_path("../tasks/port_of_call.rake", __dir__)
8
+ end
9
+
10
+ # Override the server default port
11
+ initializer "port_of_call.set_default_port" do |app|
12
+ # Only override if no port is explicitly specified
13
+ if !ENV["PORT"] && !ENV["RAILS_PORT"]
14
+ port = PortOfCall.calculate_port
15
+
16
+ # Set the port in the application configuration if available
17
+ if app.config.respond_to?(:server_timing) && app.config.server_timing.respond_to?(:set)
18
+ app.config.server_timing.set(Port: port)
19
+ end
20
+
21
+ # Hook into Rails command
22
+ if defined?(Rails::Command) && Rails::Command.respond_to?(:invoke)
23
+ Rails::Command.singleton_class.prepend(PortCommandExtension)
24
+ end
25
+
26
+ # Store the port for other components to use
27
+ ENV["PORT"] = port.to_s
28
+
29
+ Rails.logger.info "Port of Call: Assigned port #{port} to #{PortOfCall.project_name}" if Rails.logger
30
+ end
31
+ end
32
+
33
+ # Hook into Rails Server command
34
+ module PortCommandExtension
35
+ def invoke(command_name, *args)
36
+ if command_name == "server" && args.last.is_a?(Hash) && !args.last.key?(:Port)
37
+ args.last[:Port] = PortOfCall.calculate_port
38
+ end
39
+ super
40
+ end
41
+ end
42
+
43
+ # Generate initializer
44
+ generators do
45
+ require_relative "../generators/port_of_call/install_generator"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PortOfCall
4
+ # Version number for the gem
5
+ # Following Semantic Versioning (https://semver.org/)
6
+ VERSION = "0.1.0.alpha1"
7
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require_relative "port_of_call/version"
5
+ require_relative "port_of_call/configuration"
6
+ require_relative "port_of_call/port_calculator"
7
+ require_relative "port_of_call/railtie" if defined?(Rails)
8
+ require_relative "port_of_call/cli"
9
+
10
+ # Port of Call is a Ruby gem that assigns each Rails application a consistent,
11
+ # deterministic port number based on the application's name or repository.
12
+ # This solves the common conflict of multiple Rails apps all defaulting to port 3000.
13
+ #
14
+ # @example Basic usage with Rails
15
+ # # In Gemfile
16
+ # gem 'port_of_call'
17
+ #
18
+ # # Your Rails server will automatically use a unique port
19
+ # # based on your project name
20
+ #
21
+ # @example Configuration via initializer
22
+ # # In config/initializers/port_of_call.rb
23
+ # PortOfCall.configure do |config|
24
+ # config.port_range = 3000..3999
25
+ # config.base_port = 3000
26
+ # # config.project_name = "custom_name" # Optional
27
+ # end
28
+ #
29
+ # @example Ruby API usage
30
+ # require 'port_of_call'
31
+ #
32
+ # # Get the calculated port for the current project
33
+ # port = PortOfCall.calculate_port
34
+ #
35
+ # # Check if the port is available
36
+ # if PortOfCall.port_in_use?(port)
37
+ # puts "Port #{port} is already in use!"
38
+ # end
39
+ module PortOfCall
40
+ # Standard error class for Port of Call exceptions
41
+ class Error < StandardError; end
42
+
43
+ class << self
44
+ attr_writer :configuration
45
+
46
+ # Returns the current configuration
47
+ # @return [Configuration] the current configuration
48
+ def configuration
49
+ @configuration ||= Configuration.new
50
+ end
51
+
52
+ # Configures the gem with the given block
53
+ # @yield [config] The configuration object
54
+ # @example
55
+ # PortOfCall.configure do |config|
56
+ # config.port_range = 4000..4999
57
+ # config.base_port = 4000
58
+ # end
59
+ def configure
60
+ yield(configuration)
61
+ end
62
+
63
+ # Resets the configuration to defaults
64
+ # @return [Configuration] the new configuration object
65
+ def reset
66
+ @configuration = Configuration.new
67
+ end
68
+
69
+ # Calculates the port for the current project
70
+ # @return [Integer] the calculated port number
71
+ def calculate_port
72
+ calculator.calculate
73
+ end
74
+
75
+ # Returns the detected or configured project name
76
+ # @return [String] the project name
77
+ def project_name
78
+ calculator.send(:extract_project_name)
79
+ end
80
+
81
+ # Checks if the calculated port is available
82
+ # @return [Boolean] true if the port is available, false if in use
83
+ def available?
84
+ port = calculate_port
85
+ !port_in_use?(port)
86
+ end
87
+
88
+ # Checks if a specific port is in use by another process
89
+ # @param port [Integer] the port to check
90
+ # @return [Boolean] true if the port is in use, false if available
91
+ def port_in_use?(port)
92
+ # Check if the port is already in use by another process
93
+ begin
94
+ socket = TCPServer.new('127.0.0.1', port)
95
+ socket.close
96
+ false
97
+ rescue Errno::EADDRINUSE
98
+ true
99
+ end
100
+ end
101
+
102
+ # Returns options hash for Rails server
103
+ # @return [Hash] options with the Port key set
104
+ def server_options
105
+ { Port: calculate_port }
106
+ end
107
+
108
+ private
109
+
110
+ # Returns the port calculator instance
111
+ # @return [PortCalculator] the port calculator
112
+ def calculator
113
+ @calculator ||= PortCalculator.new(configuration)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :port_of_call do
4
+ desc "Print the calculated port for this Rails application"
5
+ task :port => :environment do
6
+ port = PortOfCall.calculate_port
7
+ project_name = PortOfCall.project_name
8
+ puts "Port of Call - Calculated port for #{project_name}: #{port}"
9
+
10
+ # Check if the port is already in use
11
+ if PortOfCall.port_in_use?(port)
12
+ puts "WARNING: Port #{port} is already in use by another process!"
13
+ end
14
+ end
15
+
16
+ desc "Start the Rails server with the calculated port"
17
+ task :start => :environment do
18
+ port = PortOfCall.calculate_port
19
+ project_name = PortOfCall.project_name
20
+
21
+ puts "Port of Call - Starting Rails server for #{project_name} on port #{port}"
22
+
23
+ # Check if the port is already in use
24
+ if PortOfCall.port_in_use?(port)
25
+ puts "WARNING: Port #{port} is already in use by another process!"
26
+ exit 1
27
+ end
28
+
29
+ # Start the Rails server with the calculated port
30
+ system("rails", "server", "-p", port.to_s)
31
+ end
32
+
33
+ desc "Generate an initializer for Port of Call"
34
+ task :install => :environment do
35
+ puts "Generating Port of Call initializer..."
36
+ system("rails", "generate", "port_of_call:install")
37
+ end
38
+
39
+ desc "Set the calculated port as the default for this Rails application"
40
+ task :set_default => :environment do
41
+ port = PortOfCall.calculate_port
42
+ project_name = PortOfCall.project_name
43
+
44
+ # Get Rails development.rb config file
45
+ config_file = Rails.root.join("config", "environments", "development.rb")
46
+
47
+ if File.exist?(config_file)
48
+ content = File.read(config_file)
49
+
50
+ if content.include?("config.port = ")
51
+ # Update existing port setting
52
+ updated_content = content.gsub(/config\.port\s*=\s*\d+/, "config.port = #{port}")
53
+ else
54
+ # Add new port setting at the end of the config block
55
+ updated_content = content.gsub(/(Rails\.application\.configure do.*?)(\nend)/m, "\\1\n # Port of Call - Assigned port\n config.port = #{port}\n\\2")
56
+ end
57
+
58
+ # Write updated content back to the file
59
+ File.write(config_file, updated_content)
60
+
61
+ puts "Port of Call - Set default port for #{project_name} to #{port} in #{config_file}"
62
+ else
63
+ puts "ERROR: Could not find development.rb config file"
64
+ exit 1
65
+ end
66
+ end
67
+
68
+ desc "Check if the calculated port is available"
69
+ task :check => :environment do
70
+ port = PortOfCall.calculate_port
71
+ project_name = PortOfCall.project_name
72
+
73
+ puts "Port of Call - Checking port #{port} for #{project_name}..."
74
+
75
+ if PortOfCall.port_in_use?(port)
76
+ puts "WARNING: Port #{port} is already in use by another process!"
77
+ exit 1
78
+ else
79
+ puts "Port #{port} is available."
80
+ end
81
+ end
82
+
83
+ desc "Show configuration information for Port of Call"
84
+ task :info => :environment do
85
+ port = PortOfCall.calculate_port
86
+ project_name = PortOfCall.project_name
87
+ config = PortOfCall.configuration
88
+
89
+ puts "Port of Call - Configuration Information"
90
+ puts "---------------------------------------"
91
+ puts "Project name: #{project_name}"
92
+ puts "Calculated port: #{port}"
93
+ puts "Port range: #{config.port_range}"
94
+ puts "Base port: #{config.base_port}"
95
+ puts "Custom name: #{config.custom_project_name || 'Not set'}"
96
+ puts "Reserved ports: #{config.reserved_ports.empty? ? 'None' : config.reserved_ports.join(', ')}"
97
+ puts "---------------------------------------"
98
+
99
+ if PortOfCall.port_in_use?(port)
100
+ puts "WARNING: Port #{port} is already in use by another process!"
101
+ else
102
+ puts "Port #{port} is available."
103
+ end
104
+ end
105
+ end
106
+
107
+ # Add shortcut tasks at the top level
108
+ desc "Print the calculated port for this Rails application"
109
+ task :port_of_call => "port_of_call:port"
110
+
111
+ desc "Start the Rails server with the calculated port"
112
+ task :poc => "port_of_call:start"
@@ -0,0 +1,4 @@
1
+ module PortOfCall
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: port_of_call
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha1
5
+ platform: ruby
6
+ authors:
7
+ - JPB
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-03-26 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: yard
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.9'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.9'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubocop
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.21'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.21'
82
+ description: Port of Call assigns each Rails application a consistent, deterministic
83
+ port number based on the application's name or repository. This solves the common
84
+ conflict of multiple Rails apps all defaulting to port 3000.
85
+ email:
86
+ - jonathanpberger@gmail.com
87
+ executables:
88
+ - port_of_call
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - ".rspec"
93
+ - CHANGELOG.md
94
+ - LICENSE.txt
95
+ - README.md
96
+ - RELEASE_NOTES.md
97
+ - Rakefile
98
+ - examples/basic_usage.rb
99
+ - examples/rails_example.rb
100
+ - exe/port_of_call
101
+ - lib/generators/port_of_call/install_generator.rb
102
+ - lib/generators/port_of_call/templates/README.md
103
+ - lib/generators/port_of_call/templates/initializer.rb
104
+ - lib/port_of_call.rb
105
+ - lib/port_of_call/cli.rb
106
+ - lib/port_of_call/configuration.rb
107
+ - lib/port_of_call/port_calculator.rb
108
+ - lib/port_of_call/railtie.rb
109
+ - lib/port_of_call/version.rb
110
+ - lib/tasks/port_of_call.rake
111
+ - sig/port_of_call.rbs
112
+ homepage: https://github.com/jonathanpberger/port-of-call
113
+ licenses:
114
+ - MIT
115
+ metadata:
116
+ homepage_uri: https://github.com/jonathanpberger/port-of-call
117
+ source_code_uri: https://github.com/jonathanpberger/port-of-call
118
+ changelog_uri: https://github.com/jonathanpberger/port-of-call/blob/main/CHANGELOG.md
119
+ rubygems_mfa_required: 'true'
120
+ documentation_uri: https://github.com/jonathanpberger/port-of-call/blob/main/README.md
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: 3.0.0
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubygems_version: 3.6.6
136
+ specification_version: 4
137
+ summary: Deterministic port assignment for Rails applications
138
+ test_files: []