iparty 0.1.0

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: 2d819e6f38465278e0ef5afc147405a67bcf1e7868779dc3a719392301802840
4
+ data.tar.gz: a09d60d32a690d4ad066503ac81a6462a4750a613b772a2c56b14689a6d44663
5
+ SHA512:
6
+ metadata.gz: daae7a38a07d47641cfd4ff24e1fce4e07e2100e99a4cb66a9181ece7132a82fe9543f899e2b282bf699639fdd36e4f75e1724a12cab789c0e19be9c53f6a0dc
7
+ data.tar.gz: dc37d49c82e4eb2f7c4bc0c108654a1b4b8b5f4109d56fdf4531077485f2003efeee0165df152815c05314a4092de5ee1a9bd9670c53331df0653b281446e5c9
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-05-26
4
+
5
+ * Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Sven Pachnit
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.
data/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # IParty
2
+
3
+ ## 0.1.0 alpha: risk of party crashers
4
+
5
+
6
+ Makes (geo) IP fun again! Ain't no party like an IParty, because an IParty don't stop.
7
+
8
+ ```ruby
9
+ IParty.fetch_db_files! # api key required
10
+ ip = IParty(request.remote_ip)
11
+
12
+ # all these are true
13
+ ip.country.de?
14
+ ip.country.germany?
15
+ ip.country.in_european_union?
16
+ ip.country.is_a?(Hash)
17
+ ip.country == "Germany" # 🤨
18
+ ```
19
+
20
+ * IParty handles download\* and decoding of, and lookup in, mmdb-files (\* = shelling to curl and tar)
21
+ * IParty lets you annotate IPs/networks with arbitrary helpers, data and tags
22
+ * IParty lets you ignore the MAC address part of an ipv6 address more easily
23
+ * IParty has *no*\* dependencies (\* = stdlib dependencies: fileutils, forwardable, tmpdir, optparse)
24
+ * IParty is essentially a fork/refactor of the [maxminddb](https://github.com/yhirose/maxminddb) gem.
25
+ The reimaginated implementation details were however too party for a pull request in my opinion.
26
+ * IParty parties hard!
27
+
28
+
29
+ ## IParty CLI utility
30
+
31
+ IParty ships with a cli utility `iparty`, refer to [docs/cli/README.md](docs/cli/README.md) for more information.
32
+
33
+
34
+
35
+ ## Start partying
36
+
37
+ ### Requirements
38
+
39
+ ```ruby
40
+ spec.required_ruby_version = ">= 3.2.0"
41
+ ```
42
+
43
+
44
+ ### Installation
45
+
46
+ Install the gem and add to the application's Gemfile by executing:
47
+
48
+ ```bash
49
+ bundle add iparty
50
+ ```
51
+
52
+ If bundler is not being used to manage dependencies, install the gem by executing:
53
+
54
+ ```bash
55
+ gem install iparty
56
+ ```
57
+
58
+
59
+ ### Configuration
60
+
61
+ These default settings should or could be changed at "boot" (i.e. in an initializer):
62
+
63
+ ```ruby
64
+ defined?(IParty) && IParty.configure do |config|
65
+ config.account_id = config.env_value("MAXMIND_ACCOUNT_ID", nil)
66
+ config.license_key = config.env_value("MAXMIND_LICENSE_KEY", nil)
67
+ end
68
+ ```
69
+
70
+ There are more ways to configure and/or customize IParty.
71
+ You can also change most of these settings via ENV variables.
72
+
73
+ See [docs/configuration.md](docs/configuration.md) for more information.
74
+
75
+
76
+ ### Usage
77
+
78
+ #### Fetching mmdb-files
79
+
80
+ Note: This requires a unix-oid environment with curl, tar and gzip available.
81
+ You may change the download process.
82
+ For more information see [docs/configuration.md](docs/configuration.md) and [docs/mmdb_download.md](docs/mmdb_download.md)
83
+
84
+ Either in your application startup, and/or in a rake task:
85
+
86
+ ```ruby
87
+ IParty.fetch_db_files! # always download a fresh copy
88
+ IParty.fetch_db_files!(:missing) # only download missing files
89
+ IParty.fetch_db_files!(14 * 24 * 60 * 60) # only download missing or expired files (14.days also works with ActiveSupport)
90
+ ```
91
+
92
+ You may also use the shipped rake tasks for this purpose:
93
+
94
+ ```ruby
95
+ rake iparty:fetch
96
+ rake iparty:fetch[14.days] # or int-seconds without AS
97
+ rake iparty:update
98
+ ```
99
+
100
+ Outside of Rails you need to register them manually in your Rakefile:
101
+
102
+ ```ruby
103
+ require "iparty/rake_task"
104
+ IParty::RakeTask.new
105
+ ```
106
+
107
+
108
+ #### Basic usage
109
+
110
+ ```ruby
111
+ ip = IParty("1.2.3.4") # shorthand for IParty.normalize
112
+ ip.as_json
113
+
114
+ # all these are true
115
+ ip.country.de?
116
+ ip.country.germany?
117
+ ip.country.in_european_union?
118
+ ip.country.is_a?(Hash)
119
+ ip.country == "Germany" # 🤨
120
+ ip.country == 123_345 # you may want to read the docs at this point lol
121
+ ip.country.names.de == "Deutschland"
122
+ ip.country.name(:es, fallback_locale: :fr) == "Germany"
123
+ ip.country.dig(:names, :en) == "Germany"
124
+ ```
125
+
126
+ You should definitely take a quick look at the documentation, specifically about the [MaxMine::Result](docs/maxmind_result.md) object.
127
+ It should be intuitive magic but you may scratch your head if you "don't get it".
128
+
129
+
130
+
131
+ ## Further reading
132
+
133
+ * [docs/benchmark.md](docs/benchmark.md)
134
+ * [docs/configuration.md](docs/configuration.md)
135
+ * [docs/exceptions.md](docs/exceptions.md)
136
+ * [docs/maxmind_result.md](docs/maxmind_result.md)
137
+ * [docs/mmdb_download.md](docs/mmdb_download.md)
138
+
139
+
140
+
141
+ ## Compatibility to maxminddb gem
142
+
143
+ IParty is somewhat compatible with (read: replacing) maxminddb depending on your usage. Most notably the result data hash is symbolized.
144
+
145
+
146
+
147
+ ## Development
148
+
149
+ * Check out the repository
150
+ * Run `bin/setup` to install dependencies
151
+ * Run `bin/console` to experiment with an interactive irb prompt
152
+ * Run `rake` to run all the tests
153
+ * Run `rake ci` to fetch mmdb-files, then run all the tests (more coverage)
154
+
155
+ In order to run all the tests you must have API credentials for MaxMind (or a mirror / local copy of the mmdb files for Country, City and ASN).
156
+ The mmdb-files can no longer be distributed or downloaded without API credentials due to licensing. See Configuration.
157
+
158
+
159
+
160
+ ## Contributing
161
+
162
+ Bug reports, ideas, feedback and pull requests are welcome on GitHub at https://github.com/2called-chaos/iparty.
163
+
164
+ * [Open an issue](https://github.com/2called-chaos/iparty/issues/new)
165
+
166
+ or
167
+
168
+ 1. [Fork it](http://github.com/2called-chaos/iparty/fork)
169
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
170
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
171
+ 4. Make sure the tests pass and test your changes too (`rake` or `rake ci`)
172
+ 5. Push to the branch (`git push origin my-new-feature`)
173
+ 6. Create new Pull Request
174
+
175
+
176
+
177
+ ## License
178
+
179
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT), see [LICENSE.txt](https://github.com/2called-chaos/iparty/blob/master/LICENSE.txt).
180
+
181
+
182
+
183
+ ## Legal
184
+
185
+ * © 2014, yhirose [maxminddb](https://github.com/yhirose/maxminddb) and contributors
186
+ * © 2026, Sven Pachnit (www.bmonkeys.net) and contributors
187
+ * iparty is licensed under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "rspec/core/rake_task"
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+ RuboCop::RakeTask.new(:rubocop) do |task|
10
+ task.options = ["--fail-level", "W"]
11
+ end
12
+
13
+ task default: %i[spec rubocop]
14
+ task ci: %i[early_simplecov fetch_mmdb_files default]
15
+
16
+ desc "load simplecov early"
17
+ task :early_simplecov do
18
+ require "simplecov"
19
+ SimpleCov.command_name "early"
20
+ end
21
+
22
+ desc "download mmdb files for testing"
23
+ task :fetch_mmdb_files do
24
+ require "iparty"
25
+ IParty.config.directory = Pathname.new(__dir__).join("spec", "cache")
26
+ IParty.config.mirror = "https://statics.bmonkeys.net/maxmind/:edition.tar.gz"
27
+
28
+ require "iparty/rake_task"
29
+ IParty::RakeTask.new
30
+
31
+ # always fetch one for coverage
32
+ smallest = IParty.config.directory.join("GeoLite2-ASN.mmdb")
33
+ smallest.unlink if smallest.exist?
34
+
35
+ Rake::Task["iparty:fetch"].invoke
36
+
37
+ # invalid file
38
+ IParty.config.directory.join("GeoLite2-INVALID.mmdb").binwrite("INVALID")
39
+ end
40
+
41
+ # ---
42
+
43
+ desc "same as cop:html_open"
44
+ task cop: "cop:html_open"
45
+
46
+ namespace :cop do
47
+ desc "Show worst offenders / worst files"
48
+ task :worst do
49
+ sh("rubocop -f autogenconf -f worst --fail-level W") {} # ignore non-zero exit code
50
+ end
51
+
52
+ desc "Create rubocop HTML report"
53
+ task :html do
54
+ sh("rubocop -f autogenconf -f html -o tmp/rubocop.html") {} # ignore non-zero exit code
55
+ end
56
+
57
+ desc "Create rubocop HTML report and open it"
58
+ task :html_open do
59
+ Rake::Task["cop:html"].invoke
60
+ sh "open tmp/rubocop.html"
61
+ end
62
+ end
data/exe/iparty ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "irb"
6
+ require "iparty"
7
+ require "iparty/cli/application"
8
+
9
+ app = IParty::CLI::Application.new(argv: ARGV, argf: ARGF, env: ENV)
10
+ app.dispatch
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module IParty
6
+ class Address < IPAddr
7
+ extend Forwardable
8
+
9
+ attr_accessor :ipv6_significant
10
+
11
+ def initialize *args, **kw
12
+ self.ipv6_significant = kw.fetch(:significant, true)
13
+ super(*args)
14
+ self.ipv6_significant = true if force_significant?
15
+
16
+ raise IPAddr::AddressFamilyError, "unsupported address family" unless ipv4? || ipv6?
17
+ end
18
+
19
+ def force_significant?
20
+ @family == Socket::AF_INET6 && @addr == 1
21
+ end
22
+
23
+ def type
24
+ if @family == Socket::AF_INET
25
+ :ipv4
26
+ elsif @family == Socket::AF_INET6
27
+ :ipv6
28
+ end
29
+ end
30
+
31
+ def size significant: ipv6_significant
32
+ if ipv4?
33
+ 2**(32 - prefix)
34
+ elsif ipv6?
35
+ if force_significant? || significant || ipv4_mapped? || ipv4_compat?
36
+ 2**(128 - prefix)
37
+ else
38
+ 2**[0, 128 - prefix - 64].max
39
+ end
40
+ end
41
+ end
42
+
43
+ def range? **kw
44
+ size(**kw) > 1
45
+ end
46
+
47
+ # super but keeping significant option
48
+ def to_range
49
+ self.class.new(begin_addr, @family, significant: ipv6_significant)..self.class.new(end_addr, @family, significant: ipv6_significant)
50
+ end
51
+
52
+ def to_long_range **kw
53
+ range = to_range
54
+ [range.first.to_i(**kw), range.last.to_i(**kw)]
55
+ end
56
+
57
+ def to_significant
58
+ self.class.new(@addr, @family, significant: true)
59
+ end
60
+
61
+ def to_insignificant
62
+ self.class.new(@addr, @family, significant: false)
63
+ end
64
+
65
+ def to_cidr expand_v6: false, default_masks: false, netmask: false, significant: ipv6_significant
66
+ significant = true if force_significant?
67
+ cidr = expand_v6 ? to_string(significant:) : to_s(significant:)
68
+ return cidr if !default_masks && !range?(significant: true) && !(ipv6? && !significant)
69
+
70
+ mp = prefix
71
+ if @family == Socket::AF_INET
72
+ masklen = 32 - mp
73
+ mask_addr = ((IN4MASK >> masklen) << masklen)
74
+ else
75
+ mp = 64 if !significant && mp > 64
76
+ masklen = 128 - mp
77
+ mask_addr = ((IN6MASK >> masklen) << masklen)
78
+ end
79
+
80
+ "#{cidr}/#{netmask ? _to_string(mask_addr) : mp}"
81
+ end
82
+
83
+ def prefix significant: ipv6_significant
84
+ return super() if force_significant? || significant || !ipv6? || ipv4_mapped? || ipv4_compat? || super() <= 64
85
+
86
+ 64
87
+ end
88
+
89
+ def to_i significant: ipv6_significant
90
+ return super() if force_significant? || significant || !ipv6? || ipv4_mapped? || ipv4_compat? || prefix(significant: true) <= 64
91
+
92
+ # drop upper 64 bits / host-identifier of ipv6
93
+ (super() >> 64) & ((1 << 64) - 1)
94
+ end
95
+
96
+ def to_s significant: ipv6_significant
97
+ return super() if force_significant? || significant || !ipv6? || ipv4_mapped? || ipv4_compat? || prefix(significant: true) <= 64
98
+
99
+ mask(64).to_s(significant: true)
100
+ end
101
+
102
+ def to_string significant: ipv6_significant
103
+ return super() if force_significant? || significant || !ipv6? || ipv4_mapped? || ipv4_compat? || prefix(significant: true) <= 64
104
+
105
+ mask(64).to_string(significant: true)
106
+ end
107
+
108
+ def asn
109
+ defined?(@_asn) ? @_asn : (@_asn = MaxMind.lookup(:ASN, self, result_class: MaxMind::Result::Asn) || MaxMind::Result::Asn.new)
110
+ end
111
+
112
+ def geo_country
113
+ defined?(@_country) ? @_country : (@_country = MaxMind.lookup(:Country, self, result_class: MaxMind::Result::GeoCountry) || MaxMind::Result::GeoCountry.new)
114
+ end
115
+
116
+ def geo_city
117
+ defined?(@_city) ? @_city : (@_city = MaxMind.lookup(:City, self, result_class: MaxMind::Result::GeoCity) || MaxMind::Result::GeoCity.new)
118
+ end
119
+
120
+ def geo
121
+ defined?(@_geo) ? @_geo : (@_geo = geo_city.presence || geo_country.presence || MaxMind::Result::Geo.new)
122
+ end
123
+
124
+ def annotations
125
+ result = {}
126
+ IParty.config.annotations&.each do |ipp, adata|
127
+ next unless ipp.include?(self)
128
+
129
+ result.merge!(adata.merge(tags: result.fetch(:tags, []) | adata.fetch(:tags, [])))
130
+ end
131
+
132
+ result unless result.empty?
133
+ end
134
+
135
+ def as_json
136
+ {
137
+ type: type,
138
+ prefix: prefix,
139
+ address: to_s,
140
+ cidr: to_cidr,
141
+ network: nil,
142
+ annotations: annotations,
143
+ }.merge(asn.merge(network: nil), geo).compact
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IParty
4
+ module CLI
5
+ class Application
6
+ module Actions
7
+ # via --irb/-d irb
8
+ def dispatch_irb
9
+ IrbContext.new(self).start
10
+ end
11
+
12
+ # via -h/--help
13
+ def dispatch_help
14
+ help_text = colorized_help_text
15
+ help_text = help_text.map{ decolorize(_1) } unless @opts[:colorize]
16
+
17
+ puts help_text, nil, c("The current config directory is #{c @config_path, :magenta}")
18
+ end
19
+
20
+ # via -v/--version
21
+ def dispatch_appinfo pad: 20
22
+ parts = if @opts[:debug]
23
+ %i[runtime cli_opts cli_config formatters iparty_config mmdb_status]
24
+ else
25
+ %i[runtime cli_config mmdb_status]
26
+ end
27
+
28
+ parts.each_with_index do |imeth, i|
29
+ puts unless i.zero?
30
+ send(:"appinfo_#{imeth}", pad: pad)
31
+ end
32
+ end
33
+
34
+ def dispatch_info use_argf: read_from_stdin?
35
+ return dispatch_help if @argv.empty? && !use_argf
36
+
37
+ ensure_mmdb_files!
38
+
39
+ if use_argf
40
+ each_line_in_argf_as_addresses do |addresses, index|
41
+ out << formatter.format_all(addresses, base_index: index){|ip| ip_to_data(ip, colorize: formatter.colorize?) }
42
+ end
43
+ else
44
+ addresses = IParty.expand_hostnames(@argv)
45
+
46
+ out << if addresses.length > 1
47
+ formatter.format_all(addresses){|ip| ip_to_data(ip, colorize: formatter.colorize?) }
48
+ else
49
+ formatter.format(addresses[0]){|ip| ip_to_data(ip, colorize: formatter.colorize?) }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IParty
4
+ module CLI
5
+ class Application
6
+ module Appinfo
7
+ def appinfo_runtime pad: 20
8
+ puts c("#{"".rjust(pad + 2)}# Runtime", :magenta)
9
+ puts c("#{"IParty".rjust(pad)}: #{c IParty::VERSION, :blue}")
10
+ puts c("#{"Ruby".rjust(pad)}: #{c RUBY_DESCRIPTION, :blue}")
11
+ end
12
+
13
+ def appinfo_cli_opts pad: 20
14
+ puts c("#{"".rjust(pad + 2)}# CLI options", :magenta)
15
+ @opts.each_pair do |key, value|
16
+ puts c("#{key.to_s.rjust(pad)}: #{c value.inspect, default_options[key] == value ? :cyan : :blue}")
17
+ end
18
+ end
19
+
20
+ def appinfo_cli_config pad: 20
21
+ path_inaccessible = c(" (inaccessible)", :red) if !@config_path.directory? || !@config_path.readable?
22
+ file_inaccessible = c(" (inaccessible)", :red) if !@config_file.exist? || !@config_file.readable?
23
+ file_disabled = c(" (disabled)", :red) if @rc_disabled
24
+
25
+ puts c("#{"".rjust(pad + 2)}# CLI config", :magenta)
26
+ puts c("#{"config_path".rjust(pad)}: #{c @config_path.inspect, :blue}#{path_inaccessible}")
27
+ puts c("#{"config_file".rjust(pad)}: #{c @config_file.inspect, :blue}#{file_disabled || file_inaccessible}")
28
+ end
29
+
30
+ def appinfo_iparty_config pad: 20
31
+ puts c("#{"".rjust(pad + 2)}# IParty.config", :magenta)
32
+ IParty.config.each_pair do |key, value|
33
+ if key == :annotations && value
34
+ puts c("#{key.to_s.rjust(pad)}:")
35
+ value.each do |ipp, adata|
36
+ puts c(" #{"".rjust(pad)}#{ipp.to_cidr}: #{c adata.inspect, :blue}")
37
+ end
38
+ else
39
+ puts c("#{key.to_s.rjust(pad)}: #{c value.inspect, :blue}")
40
+ end
41
+ end
42
+ end
43
+
44
+ def appinfo_mmdb_status pad: 20
45
+ puts c("#{"".rjust(pad + 2)}# MMDB file status", :magenta)
46
+ IParty.config.editions.map do |edition|
47
+ file = IParty.config.directory.join("#{edition}.mmdb")
48
+ reason = IParty::MaxMind.fetch_db_file_reason(file, @opts[:mmdb_fetch_when])
49
+
50
+ status = if file.exist?
51
+ ctime = file.ctime
52
+ age = (Time.now - ctime)
53
+ days = (age / (60 * 60 * 24)).floor
54
+ hours = ((age / (60 * 60)) % 24).floor
55
+ minutes = ((age / 60) % 60).floor
56
+ age_string = [
57
+ ("#{days}d" if days > 0),
58
+ ("%02d:%02d" % [hours, minutes] if hours > 0 || minutes > 0),
59
+ ].compact.join(" ")
60
+ age_string = "#{age.floor}s" if age_string.empty?
61
+
62
+ if reason
63
+ "#{c(reason.upcase, :red)} #{c("[age: #{age_string}, ctime: #{ctime}]", :black)} #{file}"
64
+ else
65
+ "#{c("OK", :green)} #{c("[age: #{age_string}, ctime: #{ctime}]", :black)} #{file}"
66
+ end
67
+ else
68
+ "#{c("MISSING", :red)} #{file}"
69
+ end
70
+
71
+ puts c("#{edition.to_s.rjust(pad)}: #{status}")
72
+ !reason
73
+ end.all?
74
+ end
75
+
76
+ def appinfo_formatters pad: 20
77
+ pad = -2 if pad.zero?
78
+
79
+ puts c("#{"".rjust(pad + 2)}# Available formatters", :magenta)
80
+ CLI::Formatter.descendants.each do |fmt|
81
+ name = fmt.to_s
82
+ source_location = begin
83
+ if name.start_with?("#<Class:")
84
+ rest = name.split("::", 2)[1]
85
+ name = c("<IPARTYRC> ", :red) + c(rest, :blue)
86
+ singleton_class.const_source_location(rest) if singleton_class.const_defined?(rest)
87
+ else
88
+ Object.const_source_location(name)
89
+ end
90
+ rescue StandardError => ex
91
+ ["UNKNOWN(#{ex.class}: #{ex.message})", 0]
92
+ end || ["UNKNOWN", 0]
93
+
94
+ puts [
95
+ c("#{"".rjust(pad)}* "),
96
+ c(name, :blue),
97
+ c("(", :black),
98
+ c(fmt.id.inspect, :green),
99
+ c(")", :black),
100
+ c(" in #{source_location.join(":")}", :black),
101
+ ].join
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IParty
4
+ module CLI
5
+ class Application
6
+ class IrbContext
7
+ attr_reader :app, :formatter
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def to_s
14
+ "IParty::CLI"
15
+ end
16
+
17
+ def help
18
+ puts "app # application reference"
19
+ puts "exit # exit IRB"
20
+ puts "ip *ips # show summary for IPs"
21
+ end
22
+
23
+ def ip *ips
24
+ addresses = IParty.expand_hostnames(ips)
25
+ @app.out << if addresses.length > 1
26
+ app.formatter.format_all(addresses){|ip| app.ip_to_data(ip, colorize: app.formatter.colorize?) }
27
+ else
28
+ [app.formatter.format(addresses[0]){|ip| app.ip_to_data(ip, colorize: app.formatter.colorize?) }]
29
+ end
30
+
31
+ nil
32
+ end
33
+
34
+ def start
35
+ help
36
+ binding.irb(show_code: false) # rubocop:disable Lint/Debugger -- no comment
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end