deploio-cli 0.1.0 → 0.2.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.
@@ -42,6 +42,15 @@ module Deploio
42
42
  puts pastel.magenta.bold(text)
43
43
  end
44
44
 
45
+ # Create a clickable hyperlink using OSC 8 escape sequences
46
+ # Supported by most modern terminal emulators (iTerm2, GNOME Terminal, Windows Terminal, etc.)
47
+ def link(text, url = nil)
48
+ return text unless color_enabled
49
+
50
+ url ||= text.start_with?("http") ? text : "https://#{text}"
51
+ "\e]8;;#{url}\e\\#{text}\e]8;;\e\\"
52
+ end
53
+
45
54
  def table(rows, headers: nil)
46
55
  return if rows.empty?
47
56
 
@@ -49,6 +58,37 @@ module Deploio
49
58
  puts tty_table.render(:unicode, padding: [0, 2, 0, 1], width: 10_000)
50
59
  end
51
60
 
61
+ def list(items)
62
+ items.each { |item| puts " • #{item}" }
63
+ end
64
+
65
+ def grouped_table(groups, headers: nil)
66
+ return if groups.empty?
67
+
68
+ all_rows = groups.values.flatten(1)
69
+ return if all_rows.empty?
70
+
71
+ # Track which row indices should have separators after them (0-indexed)
72
+ # The separator lambda receives the row index (0 = first data row)
73
+ separator_after = []
74
+ current_idx = 0
75
+ groups.each_with_index do |(_, group_rows), group_idx|
76
+ current_idx += group_rows.size
77
+ # Add separator after last row of each group (except the last group)
78
+ separator_after << (current_idx - 1) if group_idx < groups.size - 1
79
+ end
80
+
81
+ rows = groups.values.flatten(1)
82
+ tty_table = headers ? TTY::Table.new(header: headers, rows: rows) : TTY::Table.new(rows: rows)
83
+
84
+ output = tty_table.render(:unicode, padding: [0, 2, 0, 1], width: 10_000) do |renderer|
85
+ # row_idx 0 = separator after header, then data row indices
86
+ renderer.border.separator = ->(row_idx) { row_idx == 0 || separator_after.include?(row_idx - 1) }
87
+ end
88
+
89
+ puts output
90
+ end
91
+
52
92
  private
53
93
 
54
94
  def pastel
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "did_you_mean"
4
+
5
+ module Deploio
6
+ class PgDatabaseRef
7
+ attr_reader :project_name, :database_name
8
+
9
+ # The input is given in the format "<project>-<database>"
10
+ def initialize(input, available_databases: {})
11
+ @input = input.to_s
12
+ parse_from_available_databases(available_databases)
13
+ end
14
+
15
+ def full_name
16
+ "#{project_name}-#{database_name}"
17
+ end
18
+
19
+ def to_s
20
+ full_name
21
+ end
22
+
23
+ def ==(other)
24
+ return false unless other.is_a?(PgDatabaseRef)
25
+
26
+ project_name == other.project_name && database_name == other.database_name
27
+ end
28
+
29
+ private
30
+
31
+ def parse_from_available_databases(available_databases)
32
+ if available_databases.key?(@input)
33
+ match = available_databases[@input]
34
+ @project_name = match[:project_name]
35
+ @database_name = match[:database_name]
36
+ return
37
+ end
38
+
39
+ # If available_databases provided but no match, raise error with suggestions
40
+ raise_not_found_error(@input, available_databases.keys) unless available_databases.empty?
41
+
42
+ raise_not_found_error(@input, [])
43
+ end
44
+
45
+ def raise_not_found_error(input, available_database_names)
46
+ message = "Database not found: '#{input}'"
47
+
48
+ suggestions = suggest_similar(input, available_database_names)
49
+ unless suggestions.empty?
50
+ message += "\n\nDid you mean?"
51
+ suggestions.each { |s| message += "\n #{s}" }
52
+ end
53
+
54
+ message += "\n\nRun 'deploio pg' to see available Postgres databases."
55
+
56
+ raise Deploio::PgDatabaseNotFoundError, message
57
+ end
58
+
59
+ def suggest_similar(input, dictionary)
60
+ return [] if dictionary.empty?
61
+
62
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: dictionary)
63
+ spell_checker.correct(input)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Deploio
4
+ class PgDatabaseResolver
5
+ attr_reader :nctl, :current_org
6
+
7
+ def initialize(nctl_client:)
8
+ @nctl = nctl_client
9
+ @current_org = @nctl.current_org
10
+ end
11
+
12
+ def resolve(database_name: nil)
13
+ if database_name
14
+ return PgDatabaseRef.new(database_name, available_databases: available_databases_hash)
15
+ end
16
+
17
+ raise Deploio::Error, "No database specified"
18
+ end
19
+
20
+ # Returns hash mapping database names -> {project_name:, app_name:}
21
+ def available_databases_hash
22
+ @available_apps_hash ||= begin
23
+ hash = {}
24
+ current_org = @nctl.current_org
25
+ @nctl.get_all_pg_databases.each do |database|
26
+ metadata = database["metadata"] || {}
27
+ project_name = metadata["namespace"] || ""
28
+ database_name = metadata["name"]
29
+ full_name = "#{project_name}-#{database_name}"
30
+ hash[full_name] = {project_name: project_name, database_name: database_name}
31
+
32
+ # Also index by short name (without org prefix) for convenience
33
+ if current_org && project_name.start_with?("#{current_org}-")
34
+ project = project_name.delete_prefix("#{current_org}-")
35
+ short_name = "#{project}-#{database_name}"
36
+ hash[short_name] ||= {project_name: project_name, database_name: database_name}
37
+ end
38
+ end
39
+ hash
40
+ end
41
+ rescue
42
+ {}
43
+ end
44
+
45
+ def short_name_for(namespace, database_name)
46
+ org = current_org
47
+ if org && namespace.start_with?("#{org}-")
48
+ project = namespace.delete_prefix("#{org}-")
49
+ "#{project}-#{database_name}"
50
+ else
51
+ "#{namespace}-#{database_name}"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+ require "fileutils"
7
+
8
+ module Deploio
9
+ class PriceFetcher
10
+ CACHE_DIR = File.expand_path("~/.deploio")
11
+ CACHE_FILE = File.join(CACHE_DIR, "prices.json")
12
+ CACHE_TTL = 24 * 60 * 60 # 24 hours in seconds
13
+ API_URL = "https://calculator-api-production.2deb129.deploio.app/product"
14
+
15
+ def initialize
16
+ @prices = nil
17
+ end
18
+
19
+ def fetch
20
+ @prices ||= load_cached_prices || fetch_and_cache_prices
21
+ end
22
+
23
+ def price_for_service(type, spec)
24
+ fetch
25
+ return nil unless @prices
26
+
27
+ case type
28
+ when "postgres", "mysql"
29
+ price_for_database(type, spec)
30
+ when "postgresdatabases", "mysqldatabases"
31
+ price_for_single_database
32
+ when "keyvaluestore"
33
+ price_for_keyvaluestore(spec)
34
+ when "opensearch"
35
+ price_for_opensearch
36
+ when "bucket"
37
+ price_for_bucket
38
+ end
39
+ end
40
+
41
+ def price_for_app(app_data)
42
+ fetch
43
+ return nil unless @prices
44
+
45
+ spec = app_data["spec"] || app_data
46
+ status = app_data["status"] || {}
47
+ for_provider = spec["forProvider"] || {}
48
+ at_provider = status["atProvider"] || {}
49
+ config = for_provider["config"] || {}
50
+
51
+ size = (config["size"] || "micro").downcase
52
+ # Use status replicas (actual running) if available, otherwise spec replicas, default to 1
53
+ replicas = at_provider["replicas"] || for_provider["replicas"] || 1
54
+ replicas = replicas.to_i
55
+
56
+ size_price = @prices.dig("app", size)
57
+ return nil if size_price.nil?
58
+
59
+ size_price * replicas
60
+ end
61
+
62
+ def format_price(price)
63
+ return "-" if price.nil?
64
+
65
+ "CHF #{price}/mo"
66
+ end
67
+
68
+ private
69
+
70
+ def load_cached_prices
71
+ return nil unless File.exist?(CACHE_FILE)
72
+
73
+ cache_data = JSON.parse(File.read(CACHE_FILE))
74
+ cached_at = cache_data["cached_at"]
75
+ return nil if cached_at.nil? || Time.now.to_i - cached_at > CACHE_TTL
76
+
77
+ cache_data["prices"]
78
+ rescue JSON::ParserError, Errno::ENOENT
79
+ nil
80
+ end
81
+
82
+ def fetch_and_cache_prices
83
+ prices = fetch_prices_from_api
84
+ return nil if prices.nil?
85
+
86
+ cache_prices(prices)
87
+ prices
88
+ rescue
89
+ nil
90
+ end
91
+
92
+ def fetch_prices_from_api
93
+ uri = URI.parse(API_URL)
94
+ response = Net::HTTP.get_response(uri)
95
+ return nil unless response.is_a?(Net::HTTPSuccess)
96
+
97
+ products = JSON.parse(response.body)
98
+ build_price_map(products)
99
+ rescue JSON::ParserError, Net::OpenTimeout, Net::ReadTimeout, SocketError
100
+ nil
101
+ end
102
+
103
+ def build_price_map(products)
104
+ prices = {
105
+ "postgres" => {},
106
+ "mysql" => {},
107
+ "keyvaluestore" => {"base" => 15},
108
+ "opensearch" => {"base" => 60},
109
+ "single_database" => {"base" => 5},
110
+ "bucket" => {"base" => 0},
111
+ "app" => {"micro" => 8, "mini" => 16, "standard-1" => 32, "standard-2" => 58},
112
+ "ram_per_gib" => 5
113
+ }
114
+
115
+ products.each do |product|
116
+ name = product["name"]
117
+ list_price = product["list_price"]
118
+ categ_path = (product["categ_path"] || []).join("/")
119
+
120
+ case name
121
+ when /^PostgreSQL - (nine-(?:db|single-db)-\S+)/
122
+ machine_type = normalize_machine_type($1)
123
+ prices["postgres"][machine_type] = list_price
124
+ when /^MySQL - (nine-(?:db|single-db)-\S+)/
125
+ machine_type = normalize_machine_type($1)
126
+ prices["mysql"][machine_type] = list_price
127
+ when "Managed Service: Key-Value Store (Redis compatible)"
128
+ prices["keyvaluestore"]["base"] = list_price
129
+ when "Managed Service: OpenSearch (Elasticsearch compatible)"
130
+ prices["opensearch"]["base"] = list_price
131
+ when "Micro", "Mini", "Standard-1", "Standard-2"
132
+ # Only use deplo.io app sizes
133
+ prices["app"][name.downcase] = list_price if categ_path.include?("deplo")
134
+ end
135
+ end
136
+
137
+ prices
138
+ end
139
+
140
+ def normalize_machine_type(raw)
141
+ # "nine-single-db-l - 10GB" -> "nine-single-db-l"
142
+ raw.split(" ").first
143
+ end
144
+
145
+ def cache_prices(prices)
146
+ FileUtils.mkdir_p(CACHE_DIR)
147
+ cache_data = {
148
+ "cached_at" => Time.now.to_i,
149
+ "prices" => prices
150
+ }
151
+ File.write(CACHE_FILE, JSON.pretty_generate(cache_data))
152
+ end
153
+
154
+ def price_for_database(type, spec)
155
+ for_provider = spec.dig("forProvider") || {}
156
+ machine_type = for_provider["machineType"] || for_provider["singleDBMachineType"]
157
+ return nil if machine_type.nil?
158
+
159
+ @prices.dig(type, machine_type)
160
+ end
161
+
162
+ def price_for_keyvaluestore(spec)
163
+ base_price = @prices.dig("keyvaluestore", "base") || 15
164
+ ram_price = @prices["ram_per_gib"] || 5
165
+
166
+ memory_size = spec.dig("forProvider", "memorySize")
167
+ return base_price if memory_size.nil?
168
+
169
+ # Parse memory size (e.g., "256Mi", "1Gi", "512Mi")
170
+ gib = parse_memory_to_gib(memory_size)
171
+ (base_price + (gib * ram_price)).round
172
+ end
173
+
174
+ def price_for_opensearch
175
+ @prices.dig("opensearch", "base") || 60
176
+ end
177
+
178
+ def price_for_single_database
179
+ # Single databases on shared instances - base price for smallest tier
180
+ @prices.dig("single_database", "base") || 5
181
+ end
182
+
183
+ def price_for_bucket
184
+ # Buckets are usage-based, show base/minimum price
185
+ @prices.dig("bucket", "base") || 0
186
+ end
187
+
188
+ def parse_memory_to_gib(memory_str)
189
+ case memory_str
190
+ when /^(\d+(?:\.\d+)?)Gi$/
191
+ $1.to_f
192
+ when /^(\d+(?:\.\d+)?)Mi$/
193
+ $1.to_f / 1024
194
+ when /^(\d+(?:\.\d+)?)Ki$/
195
+ $1.to_f / (1024 * 1024)
196
+ else
197
+ 0
198
+ end
199
+ end
200
+ end
201
+ end
@@ -30,6 +30,7 @@ module Deploio
30
30
  @nctl.check_requirements unless merged_options[:dry_run]
31
31
  end
32
32
 
33
+ # @return [Deploio::AppRef]
33
34
  def resolve_app
34
35
  resolver = AppResolver.new(nctl_client: @nctl)
35
36
  resolver.resolve(app_name: merged_options[:app])
@@ -37,5 +38,20 @@ module Deploio
37
38
  Output.error(e.message)
38
39
  exit 1
39
40
  end
41
+
42
+ # Resolves a project name to its fully qualified form (org-project).
43
+ # Users can type short names like "myproject" and this will prepend the org.
44
+ # @param project [String] Project name (short or fully qualified)
45
+ # @return [String] Fully qualified project name
46
+ def resolve_project(project)
47
+ current_org = @nctl.current_org
48
+ return project unless current_org
49
+
50
+ # Special case: project equals org name (default project)
51
+ return project if project == current_org
52
+
53
+ # Always prepend org to get fully qualified name
54
+ "#{current_org}-#{project}"
55
+ end
40
56
  end
41
57
  end
@@ -23,6 +23,58 @@ _<%= program_name %>_orgs_list() {
23
23
  fi
24
24
  }
25
25
 
26
+ # Dynamic completion for services
27
+ _<%= program_name %>_services_list() {
28
+ local -a services
29
+ services=(${(f)"$(<%= program_name %> services --json 2>/dev/null | ruby -rjson -e '
30
+ data = JSON.parse(STDIN.read) rescue []
31
+ orgs_json = `<%= program_name %> orgs --json 2>/dev/null` rescue "[]"
32
+ orgs = JSON.parse(orgs_json) rescue []
33
+ current_org = orgs.find { |o| o["current"] }&.fetch("name", nil)
34
+
35
+ data.each do |s|
36
+ ns = s.dig("metadata", "namespace") || ""
37
+ name = s.dig("metadata", "name") || ""
38
+ type = s["_type"] || "unknown"
39
+ project = current_org && ns.start_with?("#{current_org}-") ? ns.delete_prefix("#{current_org}-") : ns
40
+ short_name = "#{project}-#{name}"
41
+ puts "#{short_name}:#{name} (#{type})"
42
+ end
43
+ ' 2>/dev/null)"})
44
+
45
+ if [[ ${#services[@]} -gt 0 ]]; then
46
+ _describe -t services 'available services' services
47
+ else
48
+ _message 'service (format: project-servicename)'
49
+ fi
50
+ }
51
+
52
+ # Dynamic completion for projects
53
+ _<%= program_name %>_projects_list() {
54
+ local -a projects
55
+ projects=(${(f)"$(<%= program_name %> projects --json 2>/dev/null | ruby -rjson -e '
56
+ data = JSON.parse(STDIN.read) rescue []
57
+ orgs_json = `<%= program_name %> orgs --json 2>/dev/null` rescue "[]"
58
+ orgs = JSON.parse(orgs_json) rescue []
59
+ current_org = orgs.find { |o| o["current"] }&.fetch("name", nil)
60
+
61
+ data.each do |p|
62
+ name = p.dig("metadata", "name") || ""
63
+ display_name = p.dig("spec", "displayName").to_s
64
+ # Strip org prefix from project name for short name
65
+ short_name = current_org && name.start_with?("#{current_org}-") ? name.delete_prefix("#{current_org}-") : name
66
+ label = display_name.length > 0 ? display_name : short_name
67
+ puts "#{short_name}:#{label}"
68
+ end
69
+ ' 2>/dev/null)"})
70
+
71
+ if [[ ${#projects[@]} -gt 0 ]]; then
72
+ _describe -t projects 'available projects' projects
73
+ else
74
+ _message 'project name'
75
+ fi
76
+ }
77
+
26
78
  # Dynamic completion for apps
27
79
  _<%= program_name %>_apps_list() {
28
80
  local -a apps
@@ -48,7 +100,37 @@ _<%= program_name %>_apps_list() {
48
100
  fi
49
101
  }
50
102
 
51
- <% subcommands.each do |name, commands, class_options| %>
103
+ # Dynamic completion for PostgreSQL databases
104
+ _<%= program_name %>_pg_databases_list() {
105
+ local -a databases
106
+ databases=(${(f)"$(<%= program_name %> pg list --json 2>/dev/null | ruby -rjson -e '
107
+ data = JSON.parse(STDIN.read) rescue []
108
+ orgs_json = `<%= program_name %> orgs --json 2>/dev/null` rescue "[]"
109
+ orgs = JSON.parse(orgs_json) rescue []
110
+ current_org = orgs.find { |o| o["current"] }&.fetch("name", nil)
111
+
112
+ data.each do |db|
113
+ metadata = db["metadata"] || {}
114
+ spec = db["spec"] || {}
115
+ for_provider = spec["forProvider"] || {}
116
+ ns = metadata["namespace"] || ""
117
+ name = metadata["name"] || ""
118
+ version = for_provider["version"] || "?"
119
+ kind = db["kind"] || ""
120
+ project = current_org && ns.start_with?("#{current_org}-") ? ns.delete_prefix("#{current_org}-") : ns
121
+ short_name = "#{project}-#{name}"
122
+ puts "#{short_name}:#{name} (#{kind}, v#{version})"
123
+ end
124
+ ' 2>/dev/null)"})
125
+
126
+ if [[ ${#databases[@]} -gt 0 ]]; then
127
+ _describe -t databases 'available PostgreSQL databases' databases
128
+ else
129
+ _message 'database (format: project-dbname)'
130
+ fi
131
+ }
132
+
133
+ <% subcommands.each do |name, commands, class_options, default_task, klass| %>
52
134
  # <%= name %> subcommand
53
135
  _<%= program_name %>_<%= name %>() {
54
136
  local -a <%= name %>_commands
@@ -58,6 +140,20 @@ _<%= program_name %>_<%= name %>() {
58
140
  <% end -%>
59
141
  )
60
142
 
143
+ <% default_cmd = commands.find { |cmd_name, _, _| cmd_name == default_task } -%>
144
+ <% if default_cmd -%>
145
+ # Handle options directly (for default task) or subcommand
146
+ # Check if current word or next word after subcommand starts with -
147
+ local first_arg="${words[2]}"
148
+ local current_word="${words[CURRENT]}"
149
+ if [[ "$first_arg" == -* ]] || [[ "$current_word" == -* ]] || [[ -z "$first_arg" && "$current_word" == -* ]]; then
150
+ # Options provided directly - use default task (<%= default_task %>)
151
+ _arguments -s \
152
+ <%= format_options(default_cmd[2], class_options, positional_arg(name, default_task)) %>
153
+ return
154
+ fi
155
+
156
+ <% end -%>
61
157
  _arguments -s \
62
158
  '1:<%= name %> command:-><%= name %>_cmd' \
63
159
  '*::<%= name %> args:-><%= name %>_args'
@@ -70,8 +166,45 @@ _<%= program_name %>_<%= name %>() {
70
166
  case "$words[1]" in
71
167
  <% commands.each do |cmd_name, _, options| -%>
72
168
  <%= cmd_name %>)
169
+ <% if klass.subcommand_classes.key?(cmd_name) -%>
170
+ _<%= program_name %>_<%= name %>_<%= cmd_name %>
171
+ <% else -%>
73
172
  _arguments -s \
74
173
  <%= format_options(options, class_options, positional_arg(name, cmd_name)) %>
174
+ <% end -%>
175
+ ;;
176
+ <% end -%>
177
+ esac
178
+ ;;
179
+ esac
180
+ }
181
+ <% end %>
182
+
183
+ <% nested_subcommands.each do |full_name, commands, class_options| %>
184
+ <% parent_name, nested_name = full_name.split(':') %>
185
+ # <%= full_name %> nested subcommand
186
+ _<%= program_name %>_<%= parent_name %>_<%= nested_name %>() {
187
+ local -a <%= nested_name %>_commands
188
+ <%= nested_name %>_commands=(
189
+ <% commands.each do |cmd_name, desc| -%>
190
+ '<%= cmd_name %>:<%= escape(desc) %>'
191
+ <% end -%>
192
+ )
193
+
194
+ _arguments -s \
195
+ '1:<%= nested_name %> command:-><%= nested_name %>_cmd' \
196
+ '*::<%= nested_name %> args:-><%= nested_name %>_args'
197
+
198
+ case "$state" in
199
+ <%= nested_name %>_cmd)
200
+ _describe -t commands '<%= nested_name %> commands' <%= nested_name %>_commands
201
+ ;;
202
+ <%= nested_name %>_args)
203
+ case "$words[1]" in
204
+ <% commands.each do |cmd_name, _, options| -%>
205
+ <%= cmd_name %>)
206
+ _arguments -s \
207
+ <%= format_options(options, class_options, positional_arg(full_name, cmd_name)) %>
75
208
  ;;
76
209
  <% end -%>
77
210
  esac
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deploio
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/deploio.rb CHANGED
@@ -6,13 +6,17 @@ require_relative "deploio/version"
6
6
  require_relative "deploio/utils"
7
7
  require_relative "deploio/output"
8
8
  require_relative "deploio/app_ref"
9
+ require_relative "deploio/pg_database_ref"
9
10
  require_relative "deploio/nctl_client"
10
11
  require_relative "deploio/app_resolver"
12
+ require_relative "deploio/pg_database_resolver"
13
+ require_relative "deploio/price_fetcher"
11
14
  require_relative "deploio/shared_options"
12
15
  require_relative "deploio/cli"
13
16
 
14
17
  module Deploio
15
18
  class Error < StandardError; end
16
19
  class AppNotFoundError < Error; end
20
+ class PgDatabaseNotFoundError < Error; end
17
21
  class NctlError < Error; end
18
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deploio-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Renuo AG
@@ -56,10 +56,18 @@ files:
56
56
  - lib/deploio/cli.rb
57
57
  - lib/deploio/commands/apps.rb
58
58
  - lib/deploio/commands/auth.rb
59
+ - lib/deploio/commands/builds.rb
59
60
  - lib/deploio/commands/orgs.rb
61
+ - lib/deploio/commands/postgresql.rb
62
+ - lib/deploio/commands/postgresql_backups.rb
63
+ - lib/deploio/commands/projects.rb
64
+ - lib/deploio/commands/services.rb
60
65
  - lib/deploio/completion_generator.rb
61
66
  - lib/deploio/nctl_client.rb
62
67
  - lib/deploio/output.rb
68
+ - lib/deploio/pg_database_ref.rb
69
+ - lib/deploio/pg_database_resolver.rb
70
+ - lib/deploio/price_fetcher.rb
63
71
  - lib/deploio/shared_options.rb
64
72
  - lib/deploio/templates/completion.zsh.erb
65
73
  - lib/deploio/utils.rb
@@ -72,6 +80,11 @@ metadata:
72
80
  homepage_uri: https://github.com/renuo/deploio-cli
73
81
  source_code_uri: https://github.com/renuo/deploio-cli
74
82
  changelog_uri: https://github.com/renuo/deploio-cli/blob/main/CHANGELOG.md
83
+ post_install_message: |+
84
+ To enable shell autocompletion for deploio, add this to your ~/.zshrc:
85
+
86
+ eval "$(deploio completion)"
87
+
75
88
  rdoc_options: []
76
89
  require_paths:
77
90
  - lib
@@ -86,7 +99,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
99
  - !ruby/object:Gem::Version
87
100
  version: '0'
88
101
  requirements: []
89
- rubygems_version: 4.0.3
102
+ rubygems_version: 3.6.9
90
103
  specification_version: 4
91
104
  summary: CLI for Deploio
92
105
  test_files: []
106
+ ...