deploio-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 +7 -0
- data/.ruby-version +1 -0
- data/README.md +173 -0
- data/Rakefile +12 -0
- data/bin/deploio +18 -0
- data/deploio-cli.gemspec +33 -0
- data/lib/deploio/app_ref.rb +67 -0
- data/lib/deploio/app_resolver.rb +112 -0
- data/lib/deploio/cli.rb +95 -0
- data/lib/deploio/commands/apps.rb +149 -0
- data/lib/deploio/commands/auth.rb +30 -0
- data/lib/deploio/commands/orgs.rb +54 -0
- data/lib/deploio/completion_generator.rb +152 -0
- data/lib/deploio/nctl_client.rb +214 -0
- data/lib/deploio/output.rb +59 -0
- data/lib/deploio/shared_options.rb +41 -0
- data/lib/deploio/templates/completion.zsh.erb +126 -0
- data/lib/deploio/utils.rb +12 -0
- data/lib/deploio/version.rb +5 -0
- data/lib/deploio.rb +18 -0
- data/setup +115 -0
- metadata +92 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Deploio
|
|
4
|
+
module Commands
|
|
5
|
+
class Orgs < Thor
|
|
6
|
+
include SharedOptions
|
|
7
|
+
|
|
8
|
+
namespace "orgs"
|
|
9
|
+
|
|
10
|
+
class_option :json, type: :boolean, default: false, desc: "Output as JSON"
|
|
11
|
+
|
|
12
|
+
default_task :list
|
|
13
|
+
|
|
14
|
+
desc "list", "List all organizations"
|
|
15
|
+
def list
|
|
16
|
+
setup_options
|
|
17
|
+
raw_orgs = @nctl.get_orgs
|
|
18
|
+
|
|
19
|
+
if options[:json]
|
|
20
|
+
puts JSON.pretty_generate(raw_orgs)
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if raw_orgs.empty?
|
|
25
|
+
Output.warning("No organizations found") unless merged_options[:dry_run]
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
rows = raw_orgs.map do |org|
|
|
30
|
+
current_marker = org["current"] ? "*" : ""
|
|
31
|
+
[
|
|
32
|
+
current_marker,
|
|
33
|
+
presence(org["name"])
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Output.table(rows, headers: ["", "ORGANIZATION"])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc "set ORG_NAME", "Set the current organization"
|
|
41
|
+
def set(org_name)
|
|
42
|
+
setup_options
|
|
43
|
+
@nctl.set_org(org_name)
|
|
44
|
+
Output.success("Switched to organization #{org_name}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def presence(value, default: "-")
|
|
50
|
+
(value.nil? || value.to_s.empty?) ? default : value
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module Deploio
|
|
6
|
+
# Generates zsh completion script from Thor command metadata.
|
|
7
|
+
# Can be reused with any Thor-based CLI by passing the CLI class.
|
|
8
|
+
#
|
|
9
|
+
# @example Basic usage
|
|
10
|
+
# CompletionGenerator.new(MyCLI).generate
|
|
11
|
+
#
|
|
12
|
+
# @example With custom option completers
|
|
13
|
+
# CompletionGenerator.new(MyCLI, option_completers: {
|
|
14
|
+
# 'env' => 'environment:(production staging development)'
|
|
15
|
+
# }).generate
|
|
16
|
+
#
|
|
17
|
+
class CompletionGenerator
|
|
18
|
+
TEMPLATE_PATH = File.expand_path("templates/completion.zsh.erb", __dir__)
|
|
19
|
+
|
|
20
|
+
# Commands that pass through to external programs (like exec)
|
|
21
|
+
# These get '*:command:_normal' as their final argument
|
|
22
|
+
PASSTHROUGH_COMMANDS = %w[exec run].freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :cli_class, :option_completers, :positional_completers, :program_name
|
|
25
|
+
|
|
26
|
+
# @param cli_class [Class] Thor CLI class to generate completions for
|
|
27
|
+
# @param option_completers [Hash] Custom completers for specific options
|
|
28
|
+
# e.g., { 'app' => 'app:_myapp_apps_list' }
|
|
29
|
+
# @param positional_completers [Hash] Custom completers for positional args
|
|
30
|
+
# e.g., { 'orgs:set' => "'1:organization:_myapp_orgs_list'" }
|
|
31
|
+
# @param program_name [String] Override the program name (default: derived from CLI class)
|
|
32
|
+
def initialize(cli_class = nil, option_completers: {}, positional_completers: {}, program_name: nil)
|
|
33
|
+
@cli_class = cli_class || default_cli_class
|
|
34
|
+
@program_name = program_name || derive_program_name
|
|
35
|
+
@option_completers = default_option_completers.merge(option_completers)
|
|
36
|
+
@positional_completers = default_positional_completers.merge(positional_completers)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate
|
|
40
|
+
template = File.read(TEMPLATE_PATH)
|
|
41
|
+
ERB.new(template, trim_mode: "-").result(binding)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def default_cli_class
|
|
47
|
+
require_relative "cli"
|
|
48
|
+
CLI
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def default_option_completers
|
|
52
|
+
{
|
|
53
|
+
"app" => "app:_#{program_name}_apps_list",
|
|
54
|
+
"size" => "size:(micro mini standard)"
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def default_positional_completers
|
|
59
|
+
{
|
|
60
|
+
"orgs:set" => "'1:organization:_#{program_name}_orgs_list'"
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def derive_program_name
|
|
65
|
+
# Convert "Deploio::CLI" -> "deploio", "MyApp::CLI" -> "myapp"
|
|
66
|
+
cli_class.name.split("::").first.downcase
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def subcommands
|
|
70
|
+
cli_class.subcommand_classes.map do |name, klass|
|
|
71
|
+
commands = klass.commands.except("help").map do |cmd_name, cmd|
|
|
72
|
+
[cmd_name, cmd.description, cmd.options]
|
|
73
|
+
end
|
|
74
|
+
[name, commands, klass.class_options]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def main_commands
|
|
79
|
+
cli_class.commands.except("help").map do |name, cmd|
|
|
80
|
+
[name, cmd.description]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def direct_commands
|
|
85
|
+
subcommand_names = cli_class.subcommands
|
|
86
|
+
cli_class.commands.reject do |name, _|
|
|
87
|
+
name == "help" || subcommand_names.include?(name) || passthrough_command?(name)
|
|
88
|
+
end.map do |name, cmd|
|
|
89
|
+
[name, cmd.options]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def passthrough_commands
|
|
94
|
+
PASSTHROUGH_COMMANDS.filter_map do |name|
|
|
95
|
+
cmd = cli_class.commands[name]
|
|
96
|
+
[name, cmd.options] if cmd
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def passthrough_command?(name)
|
|
101
|
+
PASSTHROUGH_COMMANDS.include?(name)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def cli_class_options
|
|
105
|
+
cli_class.class_options
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def positional_arg(subcommand, cmd_name)
|
|
109
|
+
positional_completers["#{subcommand}:#{cmd_name}"]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def format_options(method_options, class_options, extra_arg = nil)
|
|
113
|
+
all_options = cli_class.class_options.merge(class_options).merge(method_options)
|
|
114
|
+
lines = all_options.map { |name, opt| format_option(name, opt) }.compact
|
|
115
|
+
lines << extra_arg if extra_arg
|
|
116
|
+
|
|
117
|
+
return " # No options" if lines.empty?
|
|
118
|
+
|
|
119
|
+
lines.map.with_index do |line, i|
|
|
120
|
+
continuation = (i < lines.size - 1) ? " \\" : ""
|
|
121
|
+
" #{line}#{continuation}"
|
|
122
|
+
end.join("\n")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def format_option(name, opt)
|
|
126
|
+
flag = name.to_s.tr("_", "-")
|
|
127
|
+
short = opt.aliases&.first
|
|
128
|
+
desc = escape(opt.description || "")
|
|
129
|
+
completer = option_completer(name, flag)
|
|
130
|
+
|
|
131
|
+
if short
|
|
132
|
+
if opt.type == :boolean
|
|
133
|
+
"'(#{short} --#{flag})'{#{short},--#{flag}}'[#{desc}]'"
|
|
134
|
+
else
|
|
135
|
+
"'(#{short} --#{flag})'{#{short},--#{flag}}'[#{desc}]:#{completer}'"
|
|
136
|
+
end
|
|
137
|
+
elsif opt.type == :boolean
|
|
138
|
+
"'--#{flag}[#{desc}]'"
|
|
139
|
+
else
|
|
140
|
+
"'--#{flag}[#{desc}]:#{completer}'"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def option_completer(name, flag)
|
|
145
|
+
option_completers[name.to_s] || "#{flag}:"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def escape(text)
|
|
149
|
+
text.to_s.gsub("'", "'\\''").gsub("[", '\\[').gsub("]", '\\]')
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Deploio
|
|
7
|
+
class NctlClient
|
|
8
|
+
REQUIRED_VERSION = "1.10.0"
|
|
9
|
+
|
|
10
|
+
attr_reader :dry_run
|
|
11
|
+
|
|
12
|
+
def initialize(dry_run: false)
|
|
13
|
+
@dry_run = dry_run
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def check_requirements
|
|
17
|
+
check_nctl_installed
|
|
18
|
+
check_nctl_version
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def logs(app_ref, tail: false, lines: 100)
|
|
22
|
+
args = ["--lines=#{lines}"]
|
|
23
|
+
args << "-f" if tail
|
|
24
|
+
exec_passthrough("logs", "app", app_ref.app_name,
|
|
25
|
+
"--project", app_ref.project_name,
|
|
26
|
+
*args)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def exec_command(app_ref, command)
|
|
30
|
+
exec_passthrough("exec", "app", app_ref.app_name,
|
|
31
|
+
"--project", app_ref.project_name,
|
|
32
|
+
"--", *command)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get_all_apps
|
|
36
|
+
@all_apps ||= begin
|
|
37
|
+
output = capture("get", "apps", "-A", "-o", "json")
|
|
38
|
+
return [] if output.nil? || output.empty?
|
|
39
|
+
|
|
40
|
+
data = JSON.parse(output)
|
|
41
|
+
data.is_a?(Array) ? data : (data["items"] || [])
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
[]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def get_app(app_ref)
|
|
48
|
+
output = capture("get", "app", app_ref.app_name,
|
|
49
|
+
"--project", app_ref.project_name,
|
|
50
|
+
"-o", "json")
|
|
51
|
+
return {} if output.nil? || output.empty?
|
|
52
|
+
|
|
53
|
+
JSON.parse(output)
|
|
54
|
+
rescue JSON::ParserError
|
|
55
|
+
{}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def get_app_stats(app_ref)
|
|
59
|
+
capture("get", "app", app_ref.app_name,
|
|
60
|
+
"--project", app_ref.project_name,
|
|
61
|
+
"-o", "stats")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def edit_app(app_ref)
|
|
65
|
+
exec_passthrough("edit", "app", app_ref.app_name,
|
|
66
|
+
"--project", app_ref.project_name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def create_app(project, app_name, git_url:, git_revision:, size: "mini")
|
|
70
|
+
run("create", "app", app_name,
|
|
71
|
+
"--project", project,
|
|
72
|
+
"--git-url", git_url,
|
|
73
|
+
"--git-revision", git_revision,
|
|
74
|
+
"--size", size)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def delete_app(app_ref)
|
|
78
|
+
run("delete", "app", app_ref.app_name,
|
|
79
|
+
"--project", app_ref.project_name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def create_project(project_name)
|
|
83
|
+
run("create", "project", project_name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def get_projects
|
|
87
|
+
output = capture("get", "projects", "-o", "json")
|
|
88
|
+
return [] if output.nil? || output.empty?
|
|
89
|
+
|
|
90
|
+
data = JSON.parse(output)
|
|
91
|
+
data.is_a?(Array) ? data : (data["items"] || [])
|
|
92
|
+
rescue JSON::ParserError
|
|
93
|
+
[]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def get_orgs
|
|
97
|
+
output = capture("auth", "whoami")
|
|
98
|
+
return [] if output.nil? || output.empty?
|
|
99
|
+
|
|
100
|
+
parse_orgs_from_whoami(output)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def current_org
|
|
104
|
+
get_orgs.find { |o| o["current"] }&.fetch("name", nil)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_orgs_from_whoami(output)
|
|
108
|
+
orgs = []
|
|
109
|
+
in_orgs_section = false
|
|
110
|
+
|
|
111
|
+
output.each_line do |line|
|
|
112
|
+
if line.include?("Available Organizations:")
|
|
113
|
+
in_orgs_section = true
|
|
114
|
+
next
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
next unless in_orgs_section
|
|
118
|
+
break if line.strip.empty? || line.start_with?("To switch")
|
|
119
|
+
|
|
120
|
+
# Lines are either "*\torg_name" (current) or "\torg_name"
|
|
121
|
+
current = line.start_with?("*")
|
|
122
|
+
org_name = line.sub(/^\*?\t/, "").strip
|
|
123
|
+
next if org_name.empty?
|
|
124
|
+
|
|
125
|
+
orgs << {"name" => org_name, "current" => current}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
orgs
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def set_org(org_name)
|
|
132
|
+
run("auth", "set-org", org_name)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def auth_login
|
|
136
|
+
exec_passthrough("auth", "login")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def auth_logout
|
|
140
|
+
run("auth", "logout")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def auth_whoami
|
|
144
|
+
exec_passthrough("auth", "whoami")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
# Runs nctl command as subprocess with output to terminal. Used for
|
|
150
|
+
# commands that modify state (create, delete) where we show output but don't need to parse it.
|
|
151
|
+
def run(*args)
|
|
152
|
+
cmd = build_command(args)
|
|
153
|
+
Output.command(cmd.join(" "))
|
|
154
|
+
if dry_run
|
|
155
|
+
true
|
|
156
|
+
else
|
|
157
|
+
system(*cmd)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Replaces current process with nctl command. Used for interactive commands
|
|
162
|
+
# (logs -f, exec, edit, auth) that need direct terminal access. Never returns.
|
|
163
|
+
def exec_passthrough(*args)
|
|
164
|
+
cmd = build_command(args)
|
|
165
|
+
Output.command(cmd.join(" "))
|
|
166
|
+
if dry_run
|
|
167
|
+
true
|
|
168
|
+
else
|
|
169
|
+
exec(*cmd)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Runs nctl command and captures stdout. Used for commands that return data
|
|
174
|
+
# (get apps, get projects) that needs to be parsed. Raises on failure.
|
|
175
|
+
def capture(*args)
|
|
176
|
+
cmd = build_command(args)
|
|
177
|
+
if dry_run
|
|
178
|
+
Output.command(cmd.join(" "))
|
|
179
|
+
""
|
|
180
|
+
else
|
|
181
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
182
|
+
unless status.success?
|
|
183
|
+
raise Deploio::NctlError, "nctl command failed: #{stderr}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
stdout
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def build_command(args)
|
|
191
|
+
["nctl"] + args.map(&:to_s)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def check_nctl_installed
|
|
195
|
+
_stdout, _stderr, status = Open3.capture3("nctl", "--version")
|
|
196
|
+
return if status.success?
|
|
197
|
+
|
|
198
|
+
raise Deploio::NctlError,
|
|
199
|
+
"nctl not found. Please install it: https://github.com/ninech/nctl"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def check_nctl_version
|
|
203
|
+
stdout, _stderr, _status = Open3.capture3("nctl", "--version")
|
|
204
|
+
version_match = stdout.match(/(\d+\.\d+\.\d+)/)
|
|
205
|
+
return unless version_match
|
|
206
|
+
|
|
207
|
+
version = version_match[1]
|
|
208
|
+
return unless Gem::Version.new(version) < Gem::Version.new(REQUIRED_VERSION)
|
|
209
|
+
|
|
210
|
+
raise Deploio::NctlError,
|
|
211
|
+
"nctl version #{version} is too old. Need #{REQUIRED_VERSION}+. Run: brew upgrade nctl"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-table"
|
|
4
|
+
require "pastel"
|
|
5
|
+
|
|
6
|
+
# Utility module for formatted console output
|
|
7
|
+
module Deploio
|
|
8
|
+
module Output
|
|
9
|
+
class << self
|
|
10
|
+
def color_enabled
|
|
11
|
+
return @color_enabled if defined?(@color_enabled)
|
|
12
|
+
|
|
13
|
+
@color_enabled = $stdout.tty?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def color_enabled=(value)
|
|
17
|
+
@color_enabled = value
|
|
18
|
+
@pastel = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def success(message)
|
|
22
|
+
puts pastel.green("✓ #{message}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def error(message)
|
|
26
|
+
warn pastel.red("✗ #{message}")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def warning(message)
|
|
30
|
+
puts pastel.yellow("! #{message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def info(message)
|
|
34
|
+
puts pastel.cyan("→ #{message}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def command(cmd)
|
|
38
|
+
puts pastel.bold("> #{cmd}")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def header(text)
|
|
42
|
+
puts pastel.magenta.bold(text)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def table(rows, headers: nil)
|
|
46
|
+
return if rows.empty?
|
|
47
|
+
|
|
48
|
+
tty_table = headers ? TTY::Table.new(header: headers, rows: rows) : TTY::Table.new(rows: rows)
|
|
49
|
+
puts tty_table.render(:unicode, padding: [0, 2, 0, 1], width: 10_000)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def pastel
|
|
55
|
+
@pastel ||= Pastel.new(enabled: color_enabled)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Deploio
|
|
6
|
+
module SharedOptions
|
|
7
|
+
def self.included(base)
|
|
8
|
+
base.class_option :app, aliases: "-a", type: :string, desc: "App in <project>-<app> format"
|
|
9
|
+
base.class_option :org, aliases: "-o", type: :string, desc: "Organization"
|
|
10
|
+
base.class_option :dry_run, type: :boolean, default: false, desc: "Print commands without executing"
|
|
11
|
+
base.class_option :no_color, type: :boolean, default: false, desc: "Disable colored output"
|
|
12
|
+
|
|
13
|
+
base.define_singleton_method(:exit_on_failure?) { true }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# Merges parent CLI options with subcommand options.
|
|
19
|
+
# Parent options take precedence over subcommand defaults.
|
|
20
|
+
def merged_options
|
|
21
|
+
@merged_options ||= options
|
|
22
|
+
.to_h
|
|
23
|
+
.merge(parent_options.to_h) { |_key, sub, par| par.nil? ? sub : par }
|
|
24
|
+
.transform_keys(&:to_sym)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def setup_options
|
|
28
|
+
Output.color_enabled = !merged_options[:no_color] && $stdout.tty?
|
|
29
|
+
@nctl = NctlClient.new(dry_run: merged_options[:dry_run])
|
|
30
|
+
@nctl.check_requirements unless merged_options[:dry_run]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def resolve_app
|
|
34
|
+
resolver = AppResolver.new(nctl_client: @nctl)
|
|
35
|
+
resolver.resolve(app_name: merged_options[:app])
|
|
36
|
+
rescue Deploio::Error => e
|
|
37
|
+
Output.error(e.message)
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#compdef <%= program_name %> depl
|
|
2
|
+
|
|
3
|
+
# Zsh completion for <%= program_name %>-cli
|
|
4
|
+
# Auto-generated from Thor command definitions
|
|
5
|
+
# Enable with: eval "$(<%= program_name %> completion)"
|
|
6
|
+
|
|
7
|
+
# Dynamic completion for organizations
|
|
8
|
+
_<%= program_name %>_orgs_list() {
|
|
9
|
+
local -a orgs
|
|
10
|
+
orgs=(${(f)"$(<%= program_name %> orgs --json 2>/dev/null | ruby -rjson -e '
|
|
11
|
+
data = JSON.parse(STDIN.read) rescue []
|
|
12
|
+
data.each do |o|
|
|
13
|
+
name = o["name"] || ""
|
|
14
|
+
current = o["current"] ? " (current)" : ""
|
|
15
|
+
puts "#{name}:#{name}#{current}"
|
|
16
|
+
end
|
|
17
|
+
' 2>/dev/null)"})
|
|
18
|
+
|
|
19
|
+
if [[ ${#orgs[@]} -gt 0 ]]; then
|
|
20
|
+
_describe -t orgs 'available organizations' orgs
|
|
21
|
+
else
|
|
22
|
+
_message 'organization name'
|
|
23
|
+
fi
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Dynamic completion for apps
|
|
27
|
+
_<%= program_name %>_apps_list() {
|
|
28
|
+
local -a apps
|
|
29
|
+
apps=(${(f)"$(<%= program_name %> apps --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 |a|
|
|
36
|
+
ns = a.dig("metadata", "namespace") || ""
|
|
37
|
+
name = a.dig("metadata", "name") || ""
|
|
38
|
+
project = current_org && ns.start_with?("#{current_org}-") ? ns.delete_prefix("#{current_org}-") : ns
|
|
39
|
+
short_name = "#{project}-#{name}"
|
|
40
|
+
puts "#{short_name}:#{project}/#{name}"
|
|
41
|
+
end
|
|
42
|
+
' 2>/dev/null)"})
|
|
43
|
+
|
|
44
|
+
if [[ ${#apps[@]} -gt 0 ]]; then
|
|
45
|
+
_describe -t apps 'available apps' apps
|
|
46
|
+
else
|
|
47
|
+
_message 'app (format: project-appname)'
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
<% subcommands.each do |name, commands, class_options| %>
|
|
52
|
+
# <%= name %> subcommand
|
|
53
|
+
_<%= program_name %>_<%= name %>() {
|
|
54
|
+
local -a <%= name %>_commands
|
|
55
|
+
<%= name %>_commands=(
|
|
56
|
+
<% commands.each do |cmd_name, desc| -%>
|
|
57
|
+
'<%= cmd_name %>:<%= escape(desc) %>'
|
|
58
|
+
<% end -%>
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
_arguments -s \
|
|
62
|
+
'1:<%= name %> command:-><%= name %>_cmd' \
|
|
63
|
+
'*::<%= name %> args:-><%= name %>_args'
|
|
64
|
+
|
|
65
|
+
case "$state" in
|
|
66
|
+
<%= name %>_cmd)
|
|
67
|
+
_describe -t commands '<%= name %> commands' <%= name %>_commands
|
|
68
|
+
;;
|
|
69
|
+
<%= name %>_args)
|
|
70
|
+
case "$words[1]" in
|
|
71
|
+
<% commands.each do |cmd_name, _, options| -%>
|
|
72
|
+
<%= cmd_name %>)
|
|
73
|
+
_arguments -s \
|
|
74
|
+
<%= format_options(options, class_options, positional_arg(name, cmd_name)) %>
|
|
75
|
+
;;
|
|
76
|
+
<% end -%>
|
|
77
|
+
esac
|
|
78
|
+
;;
|
|
79
|
+
esac
|
|
80
|
+
}
|
|
81
|
+
<% end %>
|
|
82
|
+
|
|
83
|
+
# Main completion function
|
|
84
|
+
_<%= program_name %>() {
|
|
85
|
+
local -a main_commands
|
|
86
|
+
main_commands=(
|
|
87
|
+
<% main_commands.each do |name, desc| -%>
|
|
88
|
+
'<%= name %>:<%= escape(desc) %>'
|
|
89
|
+
<% end -%>
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_arguments -s \
|
|
93
|
+
'(-v --version)'{-v,--version}'[Show version]' \
|
|
94
|
+
'1:command:->cmd' \
|
|
95
|
+
'*::command args:->args'
|
|
96
|
+
|
|
97
|
+
case "$state" in
|
|
98
|
+
cmd)
|
|
99
|
+
_describe -t commands '<%= program_name %> commands' main_commands
|
|
100
|
+
;;
|
|
101
|
+
args)
|
|
102
|
+
case "$words[1]" in
|
|
103
|
+
<% subcommands.each do |name, _, _| -%>
|
|
104
|
+
<%= name %>)
|
|
105
|
+
_<%= program_name %>_<%= name %>
|
|
106
|
+
;;
|
|
107
|
+
<% end -%>
|
|
108
|
+
<% direct_commands.each do |cmd_name, options| -%>
|
|
109
|
+
<%= cmd_name %>)
|
|
110
|
+
_arguments -s \
|
|
111
|
+
<%= format_options(options, cli_class_options) %>
|
|
112
|
+
;;
|
|
113
|
+
<% end -%>
|
|
114
|
+
<% passthrough_commands.each do |cmd_name, options| -%>
|
|
115
|
+
<%= cmd_name %>)
|
|
116
|
+
_arguments -s \
|
|
117
|
+
<%= format_options(options, cli_class_options, "'*:command:_normal'") %>
|
|
118
|
+
;;
|
|
119
|
+
<% end -%>
|
|
120
|
+
esac
|
|
121
|
+
;;
|
|
122
|
+
esac
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Register the completion function
|
|
126
|
+
compdef _<%= program_name %> <%= program_name %> depl
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Deploio
|
|
2
|
+
module Utils
|
|
3
|
+
# fetches the git remote of origin in the current directory
|
|
4
|
+
# This is used in different places to auto-detect the app based on the current git remote ala heroku.
|
|
5
|
+
def self.detect_git_remote
|
|
6
|
+
stdout, _stderr, status = Open3.capture3("git", "remote", "get-url", "origin")
|
|
7
|
+
status.success? ? stdout.strip : nil
|
|
8
|
+
rescue Errno::ENOENT
|
|
9
|
+
nil
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
data/lib/deploio.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
require_relative "deploio/version"
|
|
6
|
+
require_relative "deploio/utils"
|
|
7
|
+
require_relative "deploio/output"
|
|
8
|
+
require_relative "deploio/app_ref"
|
|
9
|
+
require_relative "deploio/nctl_client"
|
|
10
|
+
require_relative "deploio/app_resolver"
|
|
11
|
+
require_relative "deploio/shared_options"
|
|
12
|
+
require_relative "deploio/cli"
|
|
13
|
+
|
|
14
|
+
module Deploio
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
class AppNotFoundError < Error; end
|
|
17
|
+
class NctlError < Error; end
|
|
18
|
+
end
|