kyper 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/exe/kyper +6 -0
- data/lib/kyper_cli/cli.rb +71 -0
- data/lib/kyper_cli/client.rb +139 -0
- data/lib/kyper_cli/commands/env.rb +38 -0
- data/lib/kyper_cli/commands/helpers.rb +22 -0
- data/lib/kyper_cli/commands/init.rb +317 -0
- data/lib/kyper_cli/commands/login.rb +83 -0
- data/lib/kyper_cli/commands/logs.rb +91 -0
- data/lib/kyper_cli/commands/push.rb +120 -0
- data/lib/kyper_cli/commands/retry_build.rb +42 -0
- data/lib/kyper_cli/commands/status.rb +33 -0
- data/lib/kyper_cli/commands/validate.rb +174 -0
- data/lib/kyper_cli/commands/versions.rb +45 -0
- data/lib/kyper_cli/config.rb +32 -0
- data/lib/kyper_cli/version.rb +3 -0
- data/lib/kyper_cli.rb +19 -0
- metadata +139 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module KyperCli
|
|
2
|
+
module Commands
|
|
3
|
+
class Logs < Thor
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
POLL_INTERVAL = 2 # seconds between log-polling requests
|
|
7
|
+
|
|
8
|
+
no_commands do
|
|
9
|
+
# Streams build log to stdout via cursor-based polling.
|
|
10
|
+
# Returns the final status string (e.g. "in_review", "build_failed").
|
|
11
|
+
# Called by Push and RetryBuild commands to stream log in real time.
|
|
12
|
+
def tail_log(version_id, from: 0)
|
|
13
|
+
client = Client.new
|
|
14
|
+
cursor = from
|
|
15
|
+
printed = false
|
|
16
|
+
last_data = nil
|
|
17
|
+
|
|
18
|
+
deadline = Time.now + (30 * 60) # 30 minute max
|
|
19
|
+
loop do
|
|
20
|
+
if Time.now > deadline
|
|
21
|
+
say "\nTimed out waiting for build to complete (30 min). Check status with `kyper versions`.", :red
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
last_data = client.version_build_log(version_id, cursor: cursor)
|
|
26
|
+
chunk = last_data["log"].to_s
|
|
27
|
+
cursor = last_data["cursor"].to_i
|
|
28
|
+
|
|
29
|
+
if chunk.length.positive?
|
|
30
|
+
print chunk
|
|
31
|
+
$stdout.flush
|
|
32
|
+
printed = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
break if last_data["complete"]
|
|
36
|
+
sleep POLL_INTERVAL
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts "" if printed
|
|
40
|
+
say " " + ("─" * 56)
|
|
41
|
+
|
|
42
|
+
final_status = last_data["status"]
|
|
43
|
+
case final_status
|
|
44
|
+
when "in_review"
|
|
45
|
+
say " ✔ Build complete! Submitted for review.", :green
|
|
46
|
+
when "build_failed"
|
|
47
|
+
say " ✗ Build failed.", :red
|
|
48
|
+
say " Run `kyper retry` to try again.", :yellow
|
|
49
|
+
when "published"
|
|
50
|
+
say " ✔ Published.", :green
|
|
51
|
+
else
|
|
52
|
+
say " Status: #{final_status}"
|
|
53
|
+
end
|
|
54
|
+
say ""
|
|
55
|
+
|
|
56
|
+
final_status
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
desc "logs", "Show the build log for the latest version of your app"
|
|
61
|
+
def logs
|
|
62
|
+
require_auth!
|
|
63
|
+
yml = load_kyper_yml!
|
|
64
|
+
slug = to_slug(yml["name"])
|
|
65
|
+
|
|
66
|
+
status_data = Client.new.app_status(slug)
|
|
67
|
+
version_data = status_data["latest_version"]
|
|
68
|
+
|
|
69
|
+
unless version_data
|
|
70
|
+
say "No versions pushed yet.", :yellow
|
|
71
|
+
exit 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
version_id = version_data["id"]
|
|
75
|
+
unless version_id
|
|
76
|
+
say "Build log unavailable (update your Kyper CLI).", :yellow
|
|
77
|
+
exit 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
say ""
|
|
81
|
+
say " Build log — #{status_data["app"]} v#{version_data["version"]} (#{version_data["status"]})"
|
|
82
|
+
say " " + ("─" * 56)
|
|
83
|
+
|
|
84
|
+
tail_log(version_id, from: 0)
|
|
85
|
+
rescue KyperCli::Error => e
|
|
86
|
+
say "Error: #{e.message}", :red
|
|
87
|
+
exit 1
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
require "tty-spinner"
|
|
2
|
+
|
|
3
|
+
module KyperCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Push < Thor
|
|
6
|
+
include Helpers
|
|
7
|
+
|
|
8
|
+
desc "push", "Validate, package, and submit the current version to Kyper for review"
|
|
9
|
+
def push
|
|
10
|
+
require_auth!
|
|
11
|
+
|
|
12
|
+
unless File.exist?(Config::KYPER_FILE)
|
|
13
|
+
say "Error: No kyper.yml found. Run `kyper init` to create one.", :red
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
raw_yml = File.read(Config::KYPER_FILE)
|
|
18
|
+
yml = YAML.safe_load(raw_yml)
|
|
19
|
+
slug = to_slug(yml["name"])
|
|
20
|
+
|
|
21
|
+
say ""
|
|
22
|
+
say " Validating kyper.yml..."
|
|
23
|
+
validator = Commands::Validate.new
|
|
24
|
+
unless validator.validate_yml(yml)
|
|
25
|
+
say ""
|
|
26
|
+
say " Fix the errors above, then run `kyper push` again.", :red
|
|
27
|
+
say ""
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
say ""
|
|
31
|
+
|
|
32
|
+
zip_path = nil
|
|
33
|
+
version_id = nil
|
|
34
|
+
client = Client.new
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
zip_path = build_zip_with_spinner!
|
|
38
|
+
version_id = upload_with_spinner!(client, slug, yml, raw_yml, zip_path)
|
|
39
|
+
ensure
|
|
40
|
+
File.delete(zip_path) if zip_path && File.exist?(zip_path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
say ""
|
|
44
|
+
say " Following build log... (Ctrl+C to stop)"
|
|
45
|
+
say " " + ("─" * 56)
|
|
46
|
+
final_status = Commands::Logs.new.tail_log(version_id, from: 0)
|
|
47
|
+
|
|
48
|
+
offer_retry(version_id, client, final_status)
|
|
49
|
+
rescue KyperCli::Error => e
|
|
50
|
+
say ""
|
|
51
|
+
say " Error: #{e.message}", :red
|
|
52
|
+
say ""
|
|
53
|
+
exit 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def build_zip_with_spinner!
|
|
59
|
+
zip_path = File.join(Dir.tmpdir, "kyper_push_#{Process.pid}.zip")
|
|
60
|
+
spinner = new_spinner("Building archive")
|
|
61
|
+
ignore = load_kyperignore
|
|
62
|
+
exclude_args = (ignore + [ ".git/*", "*.log", "tmp/*" ]).flat_map { |p| [ "--exclude", p ] }
|
|
63
|
+
|
|
64
|
+
ok = system("zip", "-r", zip_path, ".", *exclude_args, "-q")
|
|
65
|
+
unless ok
|
|
66
|
+
spinner.error("Failed to build archive")
|
|
67
|
+
raise KyperCli::Error, "zip command failed — is `zip` installed?"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
size_mb = (File.size(zip_path).to_f / (1024 * 1024)).round(1)
|
|
71
|
+
spinner.success("#{size_mb} MB packed")
|
|
72
|
+
zip_path
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def upload_with_spinner!(client, slug, yml, raw_yml, zip_path)
|
|
76
|
+
spinner = new_spinner("Uploading")
|
|
77
|
+
begin
|
|
78
|
+
ensure_app_exists!(client, slug, yml)
|
|
79
|
+
result = client.push_version(slug, raw_yml, zip_path: zip_path)
|
|
80
|
+
version_id = result["id"]
|
|
81
|
+
spinner.success("Uploaded — build queued (version ##{version_id})")
|
|
82
|
+
version_id
|
|
83
|
+
rescue KyperCli::Error
|
|
84
|
+
spinner.error("Upload failed")
|
|
85
|
+
raise
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ensure_app_exists!(client, slug, yml)
|
|
90
|
+
client.app_status(slug)
|
|
91
|
+
rescue KyperCli::Error
|
|
92
|
+
client.create_app(name: yml["name"], slug: slug)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def offer_retry(version_id, client, final_status)
|
|
96
|
+
return unless final_status == "build_failed"
|
|
97
|
+
|
|
98
|
+
say ""
|
|
99
|
+
return unless TTY::Prompt.new.yes?(" Retry build?")
|
|
100
|
+
|
|
101
|
+
say ""
|
|
102
|
+
client.version_retry(version_id)
|
|
103
|
+
say " ✔ Build queued.", :green
|
|
104
|
+
say ""
|
|
105
|
+
say " Following build log..."
|
|
106
|
+
say " " + ("─" * 56)
|
|
107
|
+
Commands::Logs.new.tail_log(version_id, from: 0)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def new_spinner(label)
|
|
111
|
+
TTY::Spinner.new(" [:spinner] #{label}...", format: :dots, success_mark: "✔", error_mark: "✗").tap(&:auto_spin)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def load_kyperignore
|
|
115
|
+
return [] unless File.exist?(".kyperignore")
|
|
116
|
+
File.readlines(".kyperignore").map(&:strip).reject { |l| l.empty? || l.start_with?("#") }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module KyperCli
|
|
2
|
+
module Commands
|
|
3
|
+
class RetryBuild < Thor
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
desc "retry", "Retry a failed build for the current app"
|
|
7
|
+
def retry_build
|
|
8
|
+
require_auth!
|
|
9
|
+
yml = load_kyper_yml!
|
|
10
|
+
slug = to_slug(yml["name"])
|
|
11
|
+
|
|
12
|
+
status_data = Client.new.app_status(slug)
|
|
13
|
+
version_data = status_data["latest_version"]
|
|
14
|
+
|
|
15
|
+
unless version_data
|
|
16
|
+
say "No versions pushed yet.", :yellow
|
|
17
|
+
exit 0
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
unless version_data["status"] == "build_failed"
|
|
21
|
+
say "Latest version (#{version_data["version"]}) is not in a failed state — status: #{version_data["status"]}.", :yellow
|
|
22
|
+
exit 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
version_id = version_data["id"]
|
|
26
|
+
say ""
|
|
27
|
+
say " Retrying build for #{status_data["app"]} v#{version_data["version"]}..."
|
|
28
|
+
|
|
29
|
+
Client.new.version_retry(version_id)
|
|
30
|
+
say " ✔ Build queued.", :green
|
|
31
|
+
say ""
|
|
32
|
+
|
|
33
|
+
say " Following build log... (Ctrl+C to stop)"
|
|
34
|
+
say " " + ("─" * 56)
|
|
35
|
+
Commands::Logs.new.tail_log(version_id, from: 0)
|
|
36
|
+
rescue KyperCli::Error => e
|
|
37
|
+
say "Error: #{e.message}", :red
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module KyperCli
|
|
2
|
+
module Commands
|
|
3
|
+
class Status < Thor
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
desc "status", "Show the review status of your app"
|
|
7
|
+
def status
|
|
8
|
+
require_auth!
|
|
9
|
+
yml = load_kyper_yml!
|
|
10
|
+
|
|
11
|
+
slug = to_slug(yml["name"])
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
data = Client.new.app_status(slug)
|
|
15
|
+
say ""
|
|
16
|
+
say "#{data["app"]} — #{data["status"].upcase}"
|
|
17
|
+
|
|
18
|
+
if (v = data["latest_version"])
|
|
19
|
+
say " Latest version : #{v["version"]}"
|
|
20
|
+
say " Review status : #{v["status"]}"
|
|
21
|
+
say " Notes : #{v["review_notes"]}" if !v["review_notes"].to_s.empty?
|
|
22
|
+
else
|
|
23
|
+
say " No versions pushed yet."
|
|
24
|
+
end
|
|
25
|
+
say ""
|
|
26
|
+
rescue KyperCli::Error => e
|
|
27
|
+
say "Error: #{e.message}", :red
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
|
|
3
|
+
module KyperCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Validate < Thor
|
|
6
|
+
SEMVER_RE = /\A\d+\.\d+\.\d+\z/
|
|
7
|
+
|
|
8
|
+
CATEGORIES = %w[
|
|
9
|
+
developer_tools productivity finance health media
|
|
10
|
+
education business_operations data_analytics gaming
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
KNOWN_DEPS = %w[postgres mysql redis elasticsearch opensearch].freeze
|
|
14
|
+
DB_DEPS = %w[postgres mysql].freeze
|
|
15
|
+
|
|
16
|
+
ALLOWED_DEP_VERSIONS = {
|
|
17
|
+
"postgres" => %w[14 15 16],
|
|
18
|
+
"mysql" => %w[8],
|
|
19
|
+
"redis" => %w[6 7],
|
|
20
|
+
"elasticsearch" => %w[8],
|
|
21
|
+
"opensearch" => %w[2]
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
desc "validate", "Validate kyper.yml before pushing"
|
|
25
|
+
def validate
|
|
26
|
+
yml = load_kyper_yml!
|
|
27
|
+
errors, warnings = run_checks(yml)
|
|
28
|
+
|
|
29
|
+
if errors.empty?
|
|
30
|
+
if warnings.any?
|
|
31
|
+
say ""
|
|
32
|
+
warnings.each { |w| say " ⚠ #{w}", :yellow }
|
|
33
|
+
end
|
|
34
|
+
say ""
|
|
35
|
+
say " All checks passed — ready to push.", :green
|
|
36
|
+
say ""
|
|
37
|
+
true
|
|
38
|
+
else
|
|
39
|
+
say ""
|
|
40
|
+
errors.each { |e| say " #{e}", :red }
|
|
41
|
+
say ""
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
no_commands do
|
|
47
|
+
# Called by Push command — returns true/false without re-printing header.
|
|
48
|
+
def validate_yml(yml)
|
|
49
|
+
errors, _warnings = run_checks(yml)
|
|
50
|
+
errors.empty?
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def run_checks(yml)
|
|
57
|
+
errors = []
|
|
58
|
+
warnings = []
|
|
59
|
+
|
|
60
|
+
check("name present", yml["name"].to_s.strip.length.positive?) { errors << "name is required" }
|
|
61
|
+
check("name ≤ 100 chars", yml["name"].to_s.length <= 100) { errors << "name must be 100 characters or fewer" }
|
|
62
|
+
|
|
63
|
+
v = yml["version"].to_s
|
|
64
|
+
check("version present", v.length.positive?) { errors << "version is required" }
|
|
65
|
+
check("version is semver", !v.empty? && SEMVER_RE.match?(v)) { errors << "version must be semver (e.g. 1.0.0)" }
|
|
66
|
+
|
|
67
|
+
cat = yml["category"].to_s
|
|
68
|
+
check("category present", cat.length.positive?) { errors << "category is required" }
|
|
69
|
+
if cat.length.positive? && !CATEGORIES.include?(cat)
|
|
70
|
+
say_check(false, "category valid")
|
|
71
|
+
errors << "category must be one of: #{CATEGORIES.join(", ")}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
docker = yml["docker"] || {}
|
|
75
|
+
if docker["image"].to_s.length.positive?
|
|
76
|
+
say_check(false, "docker section")
|
|
77
|
+
errors << "docker.image is not supported — provide a dockerfile path (see ADR-001)"
|
|
78
|
+
else
|
|
79
|
+
df = docker["dockerfile"].to_s
|
|
80
|
+
check("docker.dockerfile set", df.length.positive?) { errors << "docker.dockerfile is required" }
|
|
81
|
+
if df.length.positive?
|
|
82
|
+
check("Dockerfile exists at #{df}", File.exist?(df)) { errors << "Dockerfile not found at #{df}" }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
procs = yml["processes"]
|
|
87
|
+
if procs
|
|
88
|
+
has_web = procs.is_a?(Hash) && (procs.key?("web") || procs.key?(:web))
|
|
89
|
+
check("processes.web defined", has_web) { errors << "processes must include a \"web\" key" }
|
|
90
|
+
else
|
|
91
|
+
say_check(false, "processes.web defined")
|
|
92
|
+
errors << "processes section is required (must include at least a \"web\" key)"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
deps = yml["deps"]
|
|
96
|
+
if deps.is_a?(Array) && deps.any?
|
|
97
|
+
# Support plain strings ("postgres"), version hashes ({"postgres" => "15"}),
|
|
98
|
+
# and version+storage hashes ({"postgres" => "15", "storage_gb" => 50}).
|
|
99
|
+
dep_names = deps.map do |d|
|
|
100
|
+
d.is_a?(Hash) ? (d.keys.map(&:to_s) - %w[storage_gb]).first.to_s : d.to_s
|
|
101
|
+
end
|
|
102
|
+
unknown = dep_names - KNOWN_DEPS
|
|
103
|
+
if unknown.any?
|
|
104
|
+
say_check(false, "deps valid")
|
|
105
|
+
errors << "unknown deps: #{unknown.join(", ")}. Allowed: #{KNOWN_DEPS.join(", ")}"
|
|
106
|
+
else
|
|
107
|
+
deps.each do |d|
|
|
108
|
+
next unless d.is_a?(Hash)
|
|
109
|
+
name = (d.keys.map(&:to_s) - %w[storage_gb]).first.to_s
|
|
110
|
+
version = (d[name] || d[name.to_sym]).to_s
|
|
111
|
+
next if version.empty?
|
|
112
|
+
allowed = ALLOWED_DEP_VERSIONS.fetch(name, [])
|
|
113
|
+
unless allowed.include?(version)
|
|
114
|
+
say_check(false, "#{name} version")
|
|
115
|
+
errors << "#{name}: version \"#{version}\" is not supported. Allowed: #{allowed.join(", ")}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
dep_labels = deps.map do |d|
|
|
120
|
+
if d.is_a?(Hash)
|
|
121
|
+
name = (d.keys.map(&:to_s) - %w[storage_gb]).first.to_s
|
|
122
|
+
ver = d[name] || d[name.to_sym]
|
|
123
|
+
gb = d["storage_gb"] || d[:storage_gb]
|
|
124
|
+
[ name, ver && "v#{ver}", gb && "#{gb}GB" ].compact.join(":")
|
|
125
|
+
else
|
|
126
|
+
d.to_s
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
say_check(true, "deps: #{dep_labels.join(", ")}")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Warn if a DB dep is present but no on_deploy hook is configured.
|
|
133
|
+
if (dep_names & DB_DEPS).any?
|
|
134
|
+
on_deploy = (yml["hooks"] || {})["on_deploy"].to_s.strip
|
|
135
|
+
if on_deploy.empty?
|
|
136
|
+
say_check(false, "hooks.on_deploy (warning)")
|
|
137
|
+
warnings << "hooks.on_deploy is not set — apps with a database dep typically need " \
|
|
138
|
+
"a migration command (e.g. 'bundle exec rails db:migrate')"
|
|
139
|
+
else
|
|
140
|
+
say_check(true, "hooks.on_deploy: #{on_deploy}")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
[ errors, warnings ]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Runs a boolean check, prints ✔/✗ with label, yields error message block on failure.
|
|
149
|
+
def check(label, condition, &block)
|
|
150
|
+
if condition
|
|
151
|
+
say_check(true, label)
|
|
152
|
+
else
|
|
153
|
+
say_check(false, label)
|
|
154
|
+
block.call
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def say_check(passed, label)
|
|
159
|
+
mark = passed ? "✔" : "✗"
|
|
160
|
+
color = passed ? :green : :red
|
|
161
|
+
say " #{mark} #{label}", color
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def load_kyper_yml!
|
|
165
|
+
yml = Config.kyper_yml
|
|
166
|
+
unless yml
|
|
167
|
+
say " ✗ kyper.yml not found — run `kyper init` to create one.", :red
|
|
168
|
+
exit 1
|
|
169
|
+
end
|
|
170
|
+
yml
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module KyperCli
|
|
2
|
+
module Commands
|
|
3
|
+
class Versions < Thor
|
|
4
|
+
include Helpers
|
|
5
|
+
|
|
6
|
+
desc "withdraw VERSION", "Withdraw the latest version from review (must not be published or building)"
|
|
7
|
+
def withdraw(version_string)
|
|
8
|
+
require_auth!
|
|
9
|
+
yml = load_kyper_yml!
|
|
10
|
+
slug = to_slug(yml["name"])
|
|
11
|
+
|
|
12
|
+
status_data = Client.new.app_status(slug)
|
|
13
|
+
version_data = status_data["latest_version"]
|
|
14
|
+
|
|
15
|
+
if version_data.nil? || version_data["version"] != version_string
|
|
16
|
+
say "Error: Version #{version_string} not found (only the latest version can be withdrawn).", :red
|
|
17
|
+
exit 1
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if %w[published building].include?(version_data["status"])
|
|
21
|
+
say "Cannot withdraw a #{version_data["status"]} version.", :red
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
say ""
|
|
26
|
+
say " Withdraw #{status_data["app"]} v#{version_string}?", :yellow
|
|
27
|
+
say " This will remove it from the review queue and cannot be undone."
|
|
28
|
+
say ""
|
|
29
|
+
|
|
30
|
+
unless TTY::Prompt.new.yes?(" Confirm withdraw?", default: false)
|
|
31
|
+
say " Aborted.", :yellow
|
|
32
|
+
exit 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Client.new.version_withdraw(version_data["id"])
|
|
36
|
+
say ""
|
|
37
|
+
say " ✔ Version #{version_string} withdrawn.", :green
|
|
38
|
+
say ""
|
|
39
|
+
rescue KyperCli::Error => e
|
|
40
|
+
say "Error: #{e.message}", :red
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module KyperCli
|
|
2
|
+
module Config
|
|
3
|
+
CONFIG_DIR = File.join(Dir.home, ".kyper")
|
|
4
|
+
CONFIG_FILE = File.join(CONFIG_DIR, "config.yml")
|
|
5
|
+
KYPER_FILE = "kyper.yml"
|
|
6
|
+
|
|
7
|
+
def self.api_host
|
|
8
|
+
ENV.fetch("KYPER_HOST", "https://kyper.shop")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.load
|
|
12
|
+
return {} unless File.exist?(CONFIG_FILE)
|
|
13
|
+
@config ||= YAML.safe_load(File.read(CONFIG_FILE)) || {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.api_token
|
|
17
|
+
load["api_token"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.save(data)
|
|
21
|
+
@config = nil
|
|
22
|
+
FileUtils.mkdir_p(CONFIG_DIR)
|
|
23
|
+
File.write(CONFIG_FILE, data.to_yaml)
|
|
24
|
+
FileUtils.chmod(0o600, CONFIG_FILE)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.kyper_yml
|
|
28
|
+
return nil unless File.exist?(KYPER_FILE)
|
|
29
|
+
YAML.safe_load(File.read(KYPER_FILE))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/kyper_cli.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "faraday"
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "kyper_cli/version"
|
|
7
|
+
require_relative "kyper_cli/config"
|
|
8
|
+
require_relative "kyper_cli/client"
|
|
9
|
+
require_relative "kyper_cli/commands/helpers"
|
|
10
|
+
require_relative "kyper_cli/commands/login"
|
|
11
|
+
require_relative "kyper_cli/commands/validate"
|
|
12
|
+
require_relative "kyper_cli/commands/init"
|
|
13
|
+
require_relative "kyper_cli/commands/push"
|
|
14
|
+
require_relative "kyper_cli/commands/status"
|
|
15
|
+
require_relative "kyper_cli/commands/logs"
|
|
16
|
+
require_relative "kyper_cli/commands/retry_build"
|
|
17
|
+
require_relative "kyper_cli/commands/env"
|
|
18
|
+
require_relative "kyper_cli/commands/versions"
|
|
19
|
+
require_relative "kyper_cli/cli"
|