skipper-cli 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: 197b36f07814e98e9490c32bab5c531f3dd0f226ced2b44a5780b17ceb5485da
4
+ data.tar.gz: c2b578ad4e728a6c5ac829832311600144e5b3c69545340bb02f0446bd6be063
5
+ SHA512:
6
+ metadata.gz: 7238cbce4df4ecd9a1477fd7289cb6a1e1a737ac601fa2ac9cee6fca6456397c6cd8843355e042c9c11412d309a47b5d279bb67ef1286bde2df970ec1daf7556
7
+ data.tar.gz: 1eaf7a2f794d44571b2c0a6ad14130c0ba8d39dac533b72f6a5603b9cc732ba8460b74e985978f5a549667419cf72b8e0b1d8d3a993e60c1a920185c546063aa
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # SkipperClient
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/skipper_client`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
14
+
15
+ If bundler is not being used to manage dependencies, install the gem by executing:
16
+
17
+ $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/skipper_client.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ task default: %i[]
data/bin/clippy ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'skipper_client'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ c = SkipperClient::Clippy.new
10
+ c.run
data/config.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "uri": "http://localhost:8080",
3
+ "token": "a0b027fe-5b3f-4f0a-ac1f-4e84078ae65e"
4
+ }
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ class Config
6
+ @@configs_path = './config.json'
7
+
8
+ attr_reader :uri, :token
9
+
10
+ def initialize
11
+ data = read_configs
12
+ @uri = data['uri']
13
+ @token = data['token']
14
+ end
15
+
16
+ private
17
+
18
+ def read_configs
19
+ file = File.read @@configs_path
20
+ JSON.parse file
21
+ end
22
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'json'
5
+ require 'http'
6
+ require 'skipper_client/models/models'
7
+ require 'skipper_client/models/requests'
8
+
9
+ # cosumer
10
+ class Consumer
11
+ attr_reader :cfg
12
+
13
+ def initialize(cfg)
14
+ @cfg = cfg
15
+ end
16
+
17
+ def content_type
18
+ { 'Content-Type' => 'application/json' }
19
+ end
20
+
21
+ def x_api_key
22
+ { "x-api-key": token }
23
+ end
24
+
25
+ def merge_headers(*headers)
26
+ HTTP.headers(headers.reduce { |output, header| output.merge(header) })
27
+ end
28
+
29
+ def token_valid?
30
+ valid_uri = URI("#{uri}/auth/validate/#{token}".strip)
31
+ begin
32
+ (Models::SkipperResponse.from_response HTTP.get(valid_uri), Models::ValidApiKeyResponse).success
33
+ true
34
+ rescue StandardError
35
+ false
36
+ end
37
+ end
38
+
39
+ def create_api_key
40
+ new_token_api = URI "#{uri}/internal/management/api_key"
41
+ req = HTTP::Post.new new_token_api.path, content_type
42
+ req.body = { origin: '*', name: 'new_api_key', purpose: 'general' }
43
+ SkipperResponse.from_response HTTP.request(req), Models::ApiKeyResponse
44
+ end
45
+
46
+ def products
47
+ products_uri = URI "#{uri}/api/v1/products"
48
+ Models::SkipperResponse.from_response HTTP.headers("x-api-key": token).get(products_uri), Models::Price, :list
49
+ end
50
+
51
+ def coupon(coupon)
52
+ validation_uri = URI "#{uri}/api/v1/products/coupons/#{coupon}".strip
53
+ Models::SkipperResponse.from_response HTTP.headers("x-api-key": token).get(validation_uri), Models::Coupon,
54
+ :unit
55
+ end
56
+
57
+ def customer_by_company_and_project(company, project)
58
+ customers_uri = URI "#{uri}/api/v1/customers?project_name=#{project}&company_name=#{company}".strip
59
+ Models::SkipperResponse.from_response HTTP.headers("x-api-key": token).get(customers_uri), Models::Customer,
60
+ :unit
61
+ rescue Models::SkipperApiError
62
+ nil
63
+ end
64
+
65
+ def valid_coupon?(input)
66
+ (coupon input).success
67
+ rescue Models::SkipperApiError
68
+ false
69
+ end
70
+
71
+ def checkout(checkout_req)
72
+ raise 'Error checkout_req is not a JsonRequest' unless checkout_req.is_a? Models::JsonRequest
73
+
74
+ checkout_uri = URI "#{uri}/api/v1/checkout"
75
+ p checkout_req.as_json
76
+
77
+ res = merge_headers(x_api_key, content_type).post(checkout_uri, json: checkout_req.as_json)
78
+ Models::SkipperResponse.from_response res, Models::Checkout, :unit
79
+ end
80
+
81
+ def product(id)
82
+ products.data.find { |d| d.external_id == id }
83
+ end
84
+
85
+ def uri
86
+ @cfg.uri
87
+ end
88
+
89
+ def token
90
+ @cfg.token
91
+ end
92
+
93
+ private
94
+
95
+ def body_to_json(body)
96
+ JSON.parse! body
97
+ end
98
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Models
6
+ # Skipper api Exception
7
+ class SkipperApiError < StandardError; end
8
+
9
+ class MissingFromJSON < StandardError; end
10
+
11
+ # json model
12
+ class JsonModel
13
+ def self.from_json(_json_dict)
14
+ raise 'not implemented'
15
+ end
16
+ end
17
+
18
+ # Basic skipper response
19
+ class SkipperResponse
20
+ attr_reader :success, :message, :data
21
+
22
+ def initialize(json_dict, data)
23
+ @success = json_dict['success']
24
+ @message = json_dict['message']
25
+ @data = data
26
+ end
27
+
28
+ def self.from_response(response, data_class, type)
29
+ raise SkipperApiError, response.to_s unless response.status.success?
30
+ raise MissingFromJSON, 'missing from_json method' unless data_class < JsonModel
31
+
32
+ _parse_from_response parse_json(response.body), data_class, type
33
+ end
34
+
35
+ def self.parse_json(body)
36
+ JSON.parse! body
37
+ end
38
+
39
+ def self._parse_from_response(json_dict, data_class, type)
40
+ case type
41
+ when :unit
42
+ _from_unit_response json_dict, data_class
43
+ when :list
44
+ _from_list_response json_dict, data_class
45
+ else
46
+ raise SkipperApiError, 'invalid type passed'
47
+ end
48
+ end
49
+
50
+ def self._from_unit_response(json_dict, data_class)
51
+ json_data = json_dict['data']
52
+ raise SkipperApiError, 'data was expected to not be a list' if json_data.is_a? Array
53
+
54
+ new json_dict, data_class.from_json(json_dict['data'])
55
+ end
56
+
57
+ def self._from_list_response(json_dict, data_class)
58
+ json_data = json_dict['data']
59
+ raise SkipperApiError, 'data was expected to be a list' unless json_data.is_a? Array
60
+
61
+ new(json_dict, json_data.map { |item| data_class.from_json(item) })
62
+ end
63
+ end
64
+
65
+ # api create api key response
66
+ class ApiKeyResponse < JsonModel
67
+ attr_accessor :key
68
+
69
+ def initialize(key)
70
+ @key = key
71
+ super()
72
+ end
73
+
74
+ def self.from_json(json_dict)
75
+ new json_dict['key']
76
+ end
77
+ end
78
+
79
+ # api key models
80
+ class ValidApiKeyResponse < JsonModel
81
+ attr_reader :value, :name, :origin, :purpose
82
+
83
+ def initialize(value, name, origin, purpose)
84
+ @value = value
85
+ @name = name
86
+ @origin = origin
87
+ @purpose = purpose
88
+ super()
89
+ end
90
+
91
+ def self.from_json(json_dict)
92
+ new json_dict['value'], json_dict['name'], json_dict['origin'], json_dict['purpose']
93
+ end
94
+ end
95
+
96
+ # ExternalID string `json:"external_id"`
97
+ # Product Product `json:"product"`
98
+ # Currency string `json:"currency"`
99
+ # Amount int64 `json:"amount"`
100
+ # StripeData *stripe.Price `json:"stripe_data"`
101
+ class Price < JsonModel
102
+ attr_reader :external_id, :product, :currency, :amount, :stripe_data, :billing_schema
103
+
104
+ def initialize(external_id, product, currency, amount, stripe_data)
105
+ @external_id = external_id
106
+ @product = product
107
+ @currency = currency
108
+ @amount = amount
109
+ @stripe_data = stripe_data
110
+ @billing_schema = amount > 300_000 ? 'monthly' : 'weekly'
111
+ super()
112
+ end
113
+
114
+ def self.from_json(json_dict)
115
+ new json_dict['external_id'], json_dict['product'], json_dict['currency'], json_dict['amount'],
116
+ json_dict['stripe_data']
117
+ end
118
+
119
+ def to_dollar
120
+ (@amount / 100).to_s
121
+ end
122
+ end
123
+
124
+ # checkout session created res
125
+ class Checkout < JsonModel
126
+ attr_reader :payment_url
127
+
128
+ def initialize(payment_url)
129
+ @payment_url = payment_url
130
+ super()
131
+ end
132
+
133
+ def self.from_json(json_dict)
134
+ new json_dict['payment_url']
135
+ end
136
+ end
137
+
138
+ # ExternalID string `json:"id"`
139
+ # PercentOff float64 `json:"percent_off"`
140
+ # Duration string `json:"duration"`
141
+ # DurationInMonths int64 `json:"duration_in_months"`
142
+ class Coupon < JsonModel
143
+ attr_reader :id, :name, :percent_off, :duration, :duration_in_months
144
+
145
+ def initialize(id, name, _percent_off, duration, duration_in_months)
146
+ @id = id
147
+ @name = name
148
+ @duration = duration
149
+ @duration_in_months = duration_in_months
150
+ @percent_off = percent_off
151
+ super()
152
+ end
153
+
154
+ def self.from_json(json_dict)
155
+ new json_dict['id'], json_dict['name'], json_dict['percent_off'], json_dict['duration'],
156
+ json_dict['duration_in_months']
157
+ end
158
+ end
159
+
160
+ # ID int `db:"id"`
161
+ # CompanyName string `db:"company_name" json:"company_name"`
162
+ # ProjectName string `db:"project_name" json:"project_name"`
163
+ class Customer < JsonModel
164
+ attr_reader :id, :company_name, :project_name
165
+
166
+ def initialize(id, company_name, project_name)
167
+ @id = id
168
+ @company_name = company_name
169
+ @project_name = project_name
170
+ end
171
+
172
+ def self.from_json(json_dict)
173
+ new json_dict['id'], json_dict['company_name'], json_dict['project_name']
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Models
4
+ # base request
5
+ class JsonRequest
6
+ def as_json
7
+ raise "not implemented"
8
+ end
9
+ end
10
+
11
+ # PriceID string `json:"price_id"`
12
+ # Qty int `json:"quantity"`
13
+ # CompanyName string `json:"company_name"`
14
+ # ProjectName string `json:"project_name"`
15
+ # CouponCode *string `json:"coupon_code"`
16
+ # ReferredCode *string `json:"referred_code"`
17
+ class CheckouReq < JsonRequest
18
+ attr_reader :price_id, :quantity, :company_name, :project_name, :coupon_code
19
+
20
+ def initialize(price, quantity, company_name, project_name, coupon_code)
21
+ @price = price
22
+ @quantity = quantity
23
+ @company_name = company_name
24
+ @project_name = project_name
25
+ @coupon_code = coupon_code
26
+ super()
27
+ end
28
+
29
+ def as_json
30
+ {
31
+ price_id: @price.external_id,
32
+ quantity: @quantity,
33
+ company_name: @company_name,
34
+ project_name: @project_name,
35
+ coupon_code: @coupon_code
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SkipperClient
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'skipper_client/version'
4
+ require_relative 'skipper_client/configs'
5
+ require_relative 'skipper_client/consumer'
6
+ require 'tty-prompt'
7
+ require 'cli/ui'
8
+
9
+ module SkipperClient
10
+ class Error < StandardError; end
11
+
12
+ class Clippy
13
+ def initialize
14
+ @cfg = Config.new
15
+ @consumer = Consumer.new @cfg
16
+ @cached_products = []
17
+ end
18
+
19
+ def run
20
+ CLI::UI::StdoutRouter.enable
21
+ CLI::UI::Frame.open('SKIPPER DIGITAL STUDIO') do
22
+ puts CLI::UI.fmt title
23
+ fetch_products
24
+ product, amount, company_name, project_name, discount = subscription_form
25
+ subscription_details product, amount, company_name, project_name, discount
26
+ unless CLI::UI::Prompt.confirm 'Continue to checkout?', default: true
27
+ puts CLI::UI.fmt '{{error:Checkout canceled }}'
28
+ puts CLI::UI.fmt 'Thank you for using Clippy the Skipper Command Line Application'
29
+ return
30
+ end
31
+
32
+ session = checkout product, amount, company_name, project_name, discount
33
+ success_output session
34
+ end
35
+ end
36
+
37
+ def title
38
+ "
39
+ __|__ |___| |\\
40
+ |o__| |___| | \\
41
+ |___| |___| |o \\
42
+ _|___| |___| |__o\\
43
+ /...\\_____|___|____\\_/
44
+ \\ o * o * * o o /
45
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~
46
+
47
+ "
48
+ end
49
+
50
+ def products
51
+ return @cached_products unless @cached_products.empty?
52
+
53
+ @cached_products = @consumer.products.data
54
+ @cached_products
55
+ end
56
+
57
+ def product_from_prompt(str_product)
58
+ str_product.include?('WEEKLY') ? weekly_product : monthly_product
59
+ end
60
+
61
+ def success_output(session)
62
+ puts CLI::UI.fmt " {{success: checkout session created: \n #{session.data.payment_url} \n Open the ssession on any broweser you want}}"
63
+ puts CLI::UI.fmt "\n {{success: Thank you for Using Clippy - The Command line tool to work with Skipper Digital Studio }}"
64
+ end
65
+
66
+ def subscription_form
67
+ product = nil
68
+ CLI::UI::Frame.open('SUBSCRIPTION FORM') do
69
+ CLI::UI::Prompt.ask('Choose the subscription model you want') do |handler|
70
+ prompt_products.each do |prompt|
71
+ handler.option(prompt[:prompt]) do |_|
72
+ product = prompt[:value]
73
+ end
74
+ end
75
+ end
76
+ [product, cli_amount, cli_company_name, cli_project_name, cli_discount]
77
+ end
78
+ end
79
+
80
+ def subscription_details(product, amount, company_name, project_name, discount)
81
+ CLI::UI::Frame.open('Subscription details') do
82
+ puts CLI::UI.fmt "{{info: Company Name -> #{company_name} }}"
83
+ puts CLI::UI.fmt "{{info: Project Name -> #{project_name} }}"
84
+ puts CLI::UI.fmt "{{info: Number of workflows -> #{amount} }}"
85
+ unless discount.nil?
86
+ puts CLI::UI.fmt "\n{{info: Discount coupon applied '#{discount}' = 5% for the first 3 months}}"
87
+ end
88
+ net_amount = product.amount * amount
89
+ customer_res = (@consumer.customer_by_company_and_project company_name, project_name)
90
+ puts CLI::UI.fmt "\n{{info: New customer discount applied 45% for the first 3 months}}" if customer_res.nil?
91
+ new_customer_disc = customer_res.nil? ? 0.45 : 0.0
92
+ coupon_discount = discount.nil? ? 0.0 : 0.05
93
+ total = net_amount * (1 - coupon_discount - new_customer_disc)
94
+ puts CLI::UI.fmt "\n{{success: Total = #{product.currency} #{(total / 100).round} (First 3 months) - Then #{product.currency} #{net_amount / 100} }}"
95
+ end
96
+ end
97
+
98
+ def checkout(product, amount, company_name, project_name, discount)
99
+ session = nil
100
+ CLI::UI::Frame.open('CHECKOUT') do
101
+ CLI::UI::SpinGroup.new do |spin_group|
102
+ spin_group.add('Generating checkout session') do |_|
103
+ session = @consumer.checkout Models::CheckouReq.new(product, amount, company_name, project_name,
104
+ discount)
105
+ end
106
+ end
107
+ end
108
+ session
109
+ end
110
+
111
+ def fetch_products
112
+ CLI::UI::SpinGroup.new do |spin_group|
113
+ spin_group.add('Fetching all the products') do |_|
114
+ products
115
+ end
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def validate_string(str)
122
+ special = "?<>',?[]}{=-)(*&^%$#`,.~{}"
123
+ regex = /[#{special.gsub(/./) { |char| "\\#{char}" }}]/
124
+ !(str =~ regex)
125
+ end
126
+
127
+ def string_prompt(prompt_text, is_retry)
128
+ puts CLI::UI.fmt 'Invalid input. The text cannot contain any special character or . (dot) | , (comma)' if is_retry
129
+ CLI::UI::Prompt.ask prompt_text, allow_empty: false
130
+ end
131
+
132
+ def cli_amount
133
+ CLI::UI::Prompt.ask('How many workstreams you want?', default: '1').to_i
134
+ end
135
+
136
+ def cli_company_name
137
+ output = string_prompt "What's your company name?", false
138
+ loop do
139
+ break if validate_string output
140
+
141
+ output = string_prompt "What's your company name?", true
142
+ end
143
+ output
144
+ end
145
+
146
+ def cli_project_name
147
+ output = string_prompt "What's your project name?", false
148
+ loop do
149
+ break if validate_string output
150
+
151
+ output = string_prompt "What's your project name?", true
152
+ end
153
+ output
154
+ end
155
+
156
+ def cli_discount
157
+ output = nil
158
+ CLI::UI::Frame.open('DISCOUNT FORM') do
159
+ discount_coupon = cli_discount_loop
160
+ output = discount_coupon != '' ? discount_coupon : nil
161
+ end
162
+ output
163
+ end
164
+
165
+ def cli_discount_loop
166
+ loop do
167
+ discount_coupon = CLI::UI::Prompt.ask('Input a discount coupon if you have one / (Not required)')
168
+ return '' unless discount_coupon != ''
169
+
170
+ valid = valid_coupon? discount_coupon
171
+ return discount_coupon if valid
172
+
173
+ next if cli_should_try_again_discount
174
+
175
+ puts CLI::UI.fmt '{{info: Continuing without a discount coupon}}'
176
+ break
177
+ end
178
+ end
179
+
180
+ def cli_should_try_again_discount
181
+ CLI::UI::Prompt.ask 'The coupon was not a valid one. What you want to do?' do |handler|
182
+ handler.option('Use a different coupon?') { |_| true }
183
+ handler.option('Continue without a coupon?') { |_| false }
184
+ end
185
+ end
186
+
187
+ def valid_coupon?(discount_coupon)
188
+ final_glyph = ->(success) { success ? CLI::UI::Glyph::CHEVRON.to_s : CLI::UI::Glyph::X.to_s }
189
+ valid = false
190
+ CLI::UI::SpinGroup.new do |spin_group|
191
+ spin_group.add 'Validating coupon', final_glyph: final_glyph do |spinner|
192
+ valid = @consumer.valid_coupon?(discount_coupon)
193
+ spinner.update_title CLI::UI.fmt "{{error: Discount coupon #{discount_coupon} is Invalid }}" unless valid
194
+ end
195
+ end
196
+ valid
197
+ end
198
+
199
+ def prompt_products
200
+ products.map do |product|
201
+ { prompt: "#{product.billing_schema.upcase} -> #{product.currency} #{product.amount / 100}", value: product }
202
+ end
203
+ end
204
+
205
+ def weekly_product
206
+ products.find { |p| p.billing_schema == 'weekly' }
207
+ end
208
+
209
+ def monthly_product
210
+ products.find { |p| p.billing_schema == 'monthly' }
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,4 @@
1
+ module SkipperClient
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skipper-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - wmb1207
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-06-10 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: a command line client to interact with skipper
14
+ email:
15
+ - lao@skipper.studio
16
+ executables:
17
+ - clippy
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - README.md
22
+ - Rakefile
23
+ - bin/clippy
24
+ - config.json
25
+ - lib/skipper_client.rb
26
+ - lib/skipper_client/configs.rb
27
+ - lib/skipper_client/consumer.rb
28
+ - lib/skipper_client/models/models.rb
29
+ - lib/skipper_client/models/requests.rb
30
+ - lib/skipper_client/version.rb
31
+ - sig/skipper_client.rbs
32
+ homepage: https://skipper.studio
33
+ licenses: []
34
+ metadata:
35
+ allowed_push_host: https://rubygems.org
36
+ homepage_uri: https://skipper.studio
37
+ source_code_uri: https://gitlab.com/skipperstudio/clippy
38
+ changelog_uri: https://gitlab.com/skipperstudio/clippy/changelog.md
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.3.21
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: A command line client to interact with skipper
58
+ test_files: []