tayo 0.1.13 → 0.2.2
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 +4 -4
- data/CHANGELOG.md +67 -75
- data/README.md +77 -68
- data/lib/tayo/cli.rb +4 -4
- data/lib/tayo/commands/cf.rb +196 -221
- data/lib/tayo/commands/gh.rb +0 -9
- data/lib/tayo/commands/init.rb +35 -35
- data/lib/tayo/commands/proxy.rb +97 -0
- data/lib/tayo/proxy/cloudflare_client.rb +323 -0
- data/lib/tayo/proxy/docker_manager.rb +150 -0
- data/lib/tayo/proxy/network_config.rb +147 -0
- data/lib/tayo/proxy/traefik_config.rb +303 -0
- data/lib/tayo/proxy/welcome_service.rb +337 -0
- data/lib/tayo/version.rb +1 -1
- data/lib/templates/welcome/Dockerfile +14 -0
- data/lib/templates/welcome/index.html +173 -0
- metadata +24 -6
- data/CLAUDE.md +0 -58
- data/lib/tayo/commands/base.rb +0 -13
- data/lib/tayo/commands/sqlite.rb +0 -413
- data/scripts/setup_rubygems_key.sh +0 -60
data/lib/tayo/commands/init.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "colorize"
|
|
4
|
-
|
|
4
|
+
require_relative "../dockerfile_modifier"
|
|
5
5
|
|
|
6
6
|
module Tayo
|
|
7
7
|
module Commands
|
|
@@ -15,9 +15,9 @@ module Tayo
|
|
|
15
15
|
end
|
|
16
16
|
commit_initial_state
|
|
17
17
|
check_orbstack
|
|
18
|
-
create_welcome_page
|
|
18
|
+
create_welcome_page
|
|
19
19
|
clear_docker_cache
|
|
20
|
-
ensure_dockerfile_exists
|
|
20
|
+
ensure_dockerfile_exists
|
|
21
21
|
commit_changes
|
|
22
22
|
puts "✅ Tayo가 성공적으로 설정되었습니다!".colorize(:green)
|
|
23
23
|
end
|
|
@@ -30,19 +30,19 @@ module Tayo
|
|
|
30
30
|
|
|
31
31
|
def check_orbstack
|
|
32
32
|
puts "🐳 OrbStack 상태를 확인합니다...".colorize(:yellow)
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
# OrbStack 실행 상태 확인
|
|
35
35
|
orbstack_running = system("pgrep -x OrbStack > /dev/null 2>&1")
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
if orbstack_running
|
|
38
38
|
puts "✅ OrbStack이 실행 중입니다.".colorize(:green)
|
|
39
39
|
else
|
|
40
40
|
puts "🚀 OrbStack을 시작합니다...".colorize(:yellow)
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
# OrbStack 실행
|
|
43
43
|
if system("open -a OrbStack")
|
|
44
44
|
puts "✅ OrbStack이 시작되었습니다.".colorize(:green)
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
# OrbStack이 완전히 시작될 때까지 잠시 대기
|
|
47
47
|
print "Docker 서비스가 준비될 때까지 대기 중".colorize(:yellow)
|
|
48
48
|
5.times do
|
|
@@ -50,7 +50,7 @@ module Tayo
|
|
|
50
50
|
print ".".colorize(:yellow)
|
|
51
51
|
end
|
|
52
52
|
puts ""
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
# Docker가 준비되었는지 확인
|
|
55
55
|
if system("docker ps > /dev/null 2>&1")
|
|
56
56
|
puts "✅ Docker가 준비되었습니다.".colorize(:green)
|
|
@@ -64,11 +64,11 @@ module Tayo
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
def ensure_dockerfile_exists
|
|
69
69
|
unless File.exist?("Dockerfile")
|
|
70
70
|
puts "🐳 Dockerfile이 없습니다. 기본 Dockerfile을 생성합니다...".colorize(:yellow)
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
# Rails 7의 기본 Dockerfile 생성
|
|
73
73
|
if system("rails app:update:bin")
|
|
74
74
|
system("./bin/rails generate dockerfile")
|
|
@@ -80,7 +80,7 @@ module Tayo
|
|
|
80
80
|
end
|
|
81
81
|
else
|
|
82
82
|
puts "✅ Dockerfile이 이미 존재합니다.".colorize(:green)
|
|
83
|
-
end
|
|
83
|
+
end
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
def create_welcome_page
|
|
@@ -90,15 +90,15 @@ module Tayo
|
|
|
90
90
|
@welcome_page_created = false
|
|
91
91
|
return
|
|
92
92
|
end
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
puts "🎨 Welcome 페이지를 생성합니다...".colorize(:yellow)
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
# Welcome 컨트롤러 생성
|
|
97
97
|
system("rails generate controller Welcome index --skip-routes --no-helper --no-assets")
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
# 프로젝트 이름 가져오기
|
|
100
100
|
project_name = File.basename(Dir.pwd).gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
# Welcome 페이지 HTML 생성
|
|
103
103
|
welcome_html = <<~HTML
|
|
104
104
|
<!DOCTYPE html>
|
|
@@ -188,7 +188,7 @@ module Tayo
|
|
|
188
188
|
<div class="container">
|
|
189
189
|
<h1>🏠 #{project_name}</h1>
|
|
190
190
|
<p class="subtitle">Welcome to your Tayo-powered Rails application!</p>
|
|
191
|
-
|
|
191
|
+
|
|
192
192
|
<div class="info-grid">
|
|
193
193
|
<div class="info-card">
|
|
194
194
|
<h3>📦 Container Ready</h3>
|
|
@@ -203,7 +203,7 @@ module Tayo
|
|
|
203
203
|
<p>Domain management simplified</p>
|
|
204
204
|
</div>
|
|
205
205
|
</div>
|
|
206
|
-
|
|
206
|
+
|
|
207
207
|
<div class="deploy-badge">
|
|
208
208
|
Deployed with Tayo 🎉
|
|
209
209
|
</div>
|
|
@@ -211,15 +211,15 @@ module Tayo
|
|
|
211
211
|
</body>
|
|
212
212
|
</html>
|
|
213
213
|
HTML
|
|
214
|
-
|
|
214
|
+
|
|
215
215
|
# Welcome 뷰 파일에 저장
|
|
216
216
|
welcome_view_path = "app/views/welcome/index.html.erb"
|
|
217
217
|
File.write(welcome_view_path, welcome_html)
|
|
218
|
-
|
|
218
|
+
|
|
219
219
|
# routes.rb 업데이트
|
|
220
220
|
routes_file = "config/routes.rb"
|
|
221
221
|
routes_content = File.read(routes_file)
|
|
222
|
-
|
|
222
|
+
|
|
223
223
|
# root 경로 설정 - welcome#index가 이미 있는지 확인
|
|
224
224
|
unless routes_content.include?("welcome#index")
|
|
225
225
|
if routes_content.match?(/^\s*root\s+/)
|
|
@@ -229,18 +229,18 @@ module Tayo
|
|
|
229
229
|
# root 설정이 없으면 추가
|
|
230
230
|
routes_content.gsub!(/Rails\.application\.routes\.draw do\s*\n/, "Rails.application.routes.draw do\n root 'welcome#index'\n")
|
|
231
231
|
end
|
|
232
|
-
|
|
232
|
+
|
|
233
233
|
File.write(routes_file, routes_content)
|
|
234
234
|
puts " ✅ routes.rb에 root 경로를 설정했습니다.".colorize(:green)
|
|
235
235
|
else
|
|
236
236
|
puts " ℹ️ routes.rb에 welcome#index가 이미 설정되어 있습니다.".colorize(:yellow)
|
|
237
237
|
end
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
puts "✅ Welcome 페이지가 생성되었습니다!".colorize(:green)
|
|
240
240
|
puts " 경로: /".colorize(:gray)
|
|
241
241
|
puts " 컨트롤러: app/controllers/welcome_controller.rb".colorize(:gray)
|
|
242
242
|
puts " 뷰: app/views/welcome/index.html.erb".colorize(:gray)
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
@welcome_page_created = true
|
|
245
245
|
end
|
|
246
246
|
|
|
@@ -250,20 +250,20 @@ module Tayo
|
|
|
250
250
|
puts "⚠️ Git 저장소가 아닙니다. 커밋을 건너뜁니다.".colorize(:yellow)
|
|
251
251
|
return
|
|
252
252
|
end
|
|
253
|
-
|
|
253
|
+
|
|
254
254
|
puts "📝 초기 상태를 Git에 커밋합니다...".colorize(:yellow)
|
|
255
|
-
|
|
255
|
+
|
|
256
256
|
# Git 상태 확인
|
|
257
257
|
git_status = `git status --porcelain`
|
|
258
|
-
|
|
258
|
+
|
|
259
259
|
if git_status.strip.empty?
|
|
260
260
|
puts "ℹ️ 커밋할 변경사항이 없습니다.".colorize(:yellow)
|
|
261
261
|
return
|
|
262
262
|
end
|
|
263
|
-
|
|
263
|
+
|
|
264
264
|
# 변경사항 스테이징
|
|
265
265
|
system("git add .")
|
|
266
|
-
|
|
266
|
+
|
|
267
267
|
# 커밋
|
|
268
268
|
commit_message = "Save current state before Tayo initialization"
|
|
269
269
|
if system("git commit -m '#{commit_message}'")
|
|
@@ -280,20 +280,20 @@ module Tayo
|
|
|
280
280
|
puts "⚠️ Git 저장소가 아닙니다. 커밋을 건너뜁니다.".colorize(:yellow)
|
|
281
281
|
return
|
|
282
282
|
end
|
|
283
|
-
|
|
283
|
+
|
|
284
284
|
puts "📝 Tayo 설정 완료 상태를 Git에 커밋합니다...".colorize(:yellow)
|
|
285
|
-
|
|
285
|
+
|
|
286
286
|
# Git 상태 확인
|
|
287
287
|
git_status = `git status --porcelain`
|
|
288
|
-
|
|
288
|
+
|
|
289
289
|
if git_status.strip.empty?
|
|
290
290
|
puts "ℹ️ 커밋할 변경사항이 없습니다.".colorize(:yellow)
|
|
291
291
|
return
|
|
292
292
|
end
|
|
293
|
-
|
|
293
|
+
|
|
294
294
|
# 변경사항 스테이징
|
|
295
295
|
system("git add .")
|
|
296
|
-
|
|
296
|
+
|
|
297
297
|
# 커밋
|
|
298
298
|
commit_message = "Complete Tayo initialization with Welcome page and Docker setup"
|
|
299
299
|
if system("git commit -m '#{commit_message}'")
|
|
@@ -306,14 +306,14 @@ module Tayo
|
|
|
306
306
|
|
|
307
307
|
def clear_docker_cache
|
|
308
308
|
puts "🧹 Docker 캐시를 정리합니다...".colorize(:yellow)
|
|
309
|
-
|
|
309
|
+
|
|
310
310
|
# Docker system prune
|
|
311
311
|
if system("docker system prune -f > /dev/null 2>&1")
|
|
312
312
|
puts "✅ Docker 시스템 캐시가 정리되었습니다.".colorize(:green)
|
|
313
313
|
else
|
|
314
314
|
puts "⚠️ Docker 시스템 정리에 실패했습니다.".colorize(:yellow)
|
|
315
315
|
end
|
|
316
|
-
|
|
316
|
+
|
|
317
317
|
# Kamal build cache clear
|
|
318
318
|
if File.exist?("config/deploy.yml")
|
|
319
319
|
puts "🚢 Kamal 빌드 캐시를 정리합니다...".colorize(:yellow)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
require "tty-prompt"
|
|
5
|
+
require_relative "../proxy/cloudflare_client"
|
|
6
|
+
require_relative "../proxy/docker_manager"
|
|
7
|
+
require_relative "../proxy/network_config"
|
|
8
|
+
require_relative "../proxy/traefik_config"
|
|
9
|
+
require_relative "../proxy/welcome_service"
|
|
10
|
+
|
|
11
|
+
module Tayo
|
|
12
|
+
module Commands
|
|
13
|
+
class Proxy
|
|
14
|
+
def execute
|
|
15
|
+
puts "🚀 Kamal Proxy와 Caddy 설정을 시작합니다...".colorize(:green)
|
|
16
|
+
puts ""
|
|
17
|
+
|
|
18
|
+
# 1. Cloudflare 설정
|
|
19
|
+
cloudflare = Tayo::Proxy::CloudflareClient.new
|
|
20
|
+
cloudflare.ensure_token
|
|
21
|
+
|
|
22
|
+
# 2. 네트워크 설정
|
|
23
|
+
network = Tayo::Proxy::NetworkConfig.new
|
|
24
|
+
network.detect_ips
|
|
25
|
+
network.configure_ports
|
|
26
|
+
|
|
27
|
+
# 3. Docker 확인
|
|
28
|
+
docker = Tayo::Proxy::DockerManager.new
|
|
29
|
+
docker.check_containers
|
|
30
|
+
|
|
31
|
+
# 4. 도메인 선택
|
|
32
|
+
selected_domains = cloudflare.select_domains
|
|
33
|
+
|
|
34
|
+
if selected_domains.empty?
|
|
35
|
+
puts "❌ 도메인이 선택되지 않았습니다. 종료합니다.".colorize(:red)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 5. DNS 설정
|
|
40
|
+
puts "\n📝 DNS 레코드를 설정합니다...".colorize(:yellow)
|
|
41
|
+
cloudflare.setup_dns_records(selected_domains, network.public_ip)
|
|
42
|
+
|
|
43
|
+
# 6. Welcome 서비스 확인
|
|
44
|
+
welcome = Tayo::Proxy::WelcomeService.new
|
|
45
|
+
welcome.ensure_running
|
|
46
|
+
|
|
47
|
+
# 7. Traefik 설정
|
|
48
|
+
traefik = Tayo::Proxy::TraefikConfig.new
|
|
49
|
+
traefik.setup(selected_domains)
|
|
50
|
+
|
|
51
|
+
# 8. 최종 안내
|
|
52
|
+
show_summary(selected_domains, network)
|
|
53
|
+
|
|
54
|
+
rescue => e
|
|
55
|
+
puts "❌ 오류가 발생했습니다: #{e.message}".colorize(:red)
|
|
56
|
+
puts e.backtrace.join("\n") if ENV["DEBUG"]
|
|
57
|
+
exit 1
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def show_summary(domains, network)
|
|
63
|
+
puts "\n" + "="*60
|
|
64
|
+
puts "✅ Proxy 설정이 완료되었습니다!".colorize(:green)
|
|
65
|
+
puts "="*60
|
|
66
|
+
|
|
67
|
+
puts "\n📋 설정 요약:".colorize(:yellow)
|
|
68
|
+
puts "━"*40
|
|
69
|
+
puts "공인 IP: #{network.public_ip}".colorize(:white)
|
|
70
|
+
puts "내부 IP: #{network.internal_ip}".colorize(:white)
|
|
71
|
+
puts "Traefik: 80, 443 포트 사용 중".colorize(:white)
|
|
72
|
+
puts "대시보드: http://localhost:8080".colorize(:white)
|
|
73
|
+
puts "━"*40
|
|
74
|
+
|
|
75
|
+
puts "\n🌐 활성화된 도메인:".colorize(:yellow)
|
|
76
|
+
domains.each do |domain|
|
|
77
|
+
if network.use_custom_ports?
|
|
78
|
+
puts "• #{domain}".colorize(:cyan)
|
|
79
|
+
puts " HTTP: http://#{domain}:#{network.external_http}".colorize(:gray)
|
|
80
|
+
puts " HTTPS: https://#{domain}:#{network.external_https}".colorize(:gray)
|
|
81
|
+
else
|
|
82
|
+
puts "• #{domain}".colorize(:cyan)
|
|
83
|
+
puts " HTTP: http://#{domain}".colorize(:gray)
|
|
84
|
+
puts " HTTPS: https://#{domain}".colorize(:gray)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if network.use_custom_ports?
|
|
89
|
+
puts "\n💡 공유기 포트포워딩을 설정하세요!".colorize(:yellow)
|
|
90
|
+
network.show_port_forwarding_guide
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
puts "\n🎉 모든 설정이 완료되었습니다!".colorize(:green)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "colorize"
|
|
4
|
+
require "tty-prompt"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "json"
|
|
7
|
+
require "uri"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
|
|
10
|
+
module Tayo
|
|
11
|
+
module Proxy
|
|
12
|
+
class CloudflareClient
|
|
13
|
+
TOKEN_FILE = File.expand_path("~/.tayo/cloudflare_token")
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@prompt = TTY::Prompt.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ensure_token
|
|
20
|
+
@token = load_saved_token
|
|
21
|
+
|
|
22
|
+
if @token
|
|
23
|
+
puts "🔑 저장된 Cloudflare 토큰을 확인합니다...".colorize(:yellow)
|
|
24
|
+
|
|
25
|
+
if test_token(@token)
|
|
26
|
+
puts "✅ 저장된 Cloudflare 토큰이 유효합니다.".colorize(:green)
|
|
27
|
+
else
|
|
28
|
+
puts "⚠️ 저장된 토큰이 만료되었거나 유효하지 않습니다.".colorize(:yellow)
|
|
29
|
+
puts "새 토큰을 입력해주세요.".colorize(:cyan)
|
|
30
|
+
open_token_creation_page
|
|
31
|
+
@token = get_cloudflare_token
|
|
32
|
+
save_token(@token)
|
|
33
|
+
end
|
|
34
|
+
else
|
|
35
|
+
puts "\n🔑 Cloudflare API 토큰이 필요합니다.".colorize(:yellow)
|
|
36
|
+
open_token_creation_page
|
|
37
|
+
@token = get_cloudflare_token
|
|
38
|
+
save_token(@token)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def select_domains
|
|
43
|
+
puts "\n🌐 Cloudflare 도메인 목록을 조회합니다...".colorize(:yellow)
|
|
44
|
+
|
|
45
|
+
zones = get_cloudflare_zones
|
|
46
|
+
|
|
47
|
+
if zones.empty?
|
|
48
|
+
puts "❌ Cloudflare에 등록된 도메인이 없습니다.".colorize(:red)
|
|
49
|
+
puts "먼저 https://dash.cloudflare.com 에서 도메인을 추가해주세요.".colorize(:cyan)
|
|
50
|
+
return []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
zone_choices = zones.map { |zone| "#{zone['name']} (#{zone['status']})" }
|
|
54
|
+
|
|
55
|
+
selected_zones = @prompt.multi_select(
|
|
56
|
+
"Caddy로 라우팅할 도메인을 선택하세요 (Space로 선택, Enter로 완료):",
|
|
57
|
+
zone_choices,
|
|
58
|
+
per_page: 10,
|
|
59
|
+
min: 1
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 선택된 항목이 없으면 빈 배열 반환
|
|
63
|
+
return [] if selected_zones.nil? || selected_zones.empty?
|
|
64
|
+
|
|
65
|
+
puts "DEBUG: selected_zones = #{selected_zones.inspect}" if ENV["DEBUG"]
|
|
66
|
+
|
|
67
|
+
selected_domains = selected_zones.map do |selection|
|
|
68
|
+
zone_name = selection.split(' ').first
|
|
69
|
+
zone = zones.find { |z| z['name'] == zone_name }
|
|
70
|
+
|
|
71
|
+
# 서브도메인 추가 여부 확인
|
|
72
|
+
if @prompt.yes?("#{zone_name}에 서브도메인을 추가하시겠습니까?")
|
|
73
|
+
subdomain = @prompt.ask("서브도메인을 입력하세요 (예: app, api):")
|
|
74
|
+
if subdomain && !subdomain.empty?
|
|
75
|
+
{
|
|
76
|
+
domain: "#{subdomain}.#{zone_name}",
|
|
77
|
+
zone_id: zone['id'],
|
|
78
|
+
zone_name: zone_name
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
{
|
|
82
|
+
domain: zone_name,
|
|
83
|
+
zone_id: zone['id'],
|
|
84
|
+
zone_name: zone_name
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
{
|
|
89
|
+
domain: zone_name,
|
|
90
|
+
zone_id: zone['id'],
|
|
91
|
+
zone_name: zone_name
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "\n✅ 선택된 도메인:".colorize(:green)
|
|
97
|
+
selected_domains.each do |d|
|
|
98
|
+
puts " • #{d[:domain]}".colorize(:cyan)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
puts "DEBUG: returning selected_domains = #{selected_domains.inspect}" if ENV["DEBUG"]
|
|
102
|
+
|
|
103
|
+
selected_domains
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def setup_dns_records(domains, public_ip)
|
|
107
|
+
domains.each do |domain_info|
|
|
108
|
+
domain = domain_info[:domain]
|
|
109
|
+
zone_id = domain_info[:zone_id]
|
|
110
|
+
|
|
111
|
+
puts "\n⚙️ #{domain}의 DNS 레코드를 설정합니다...".colorize(:yellow)
|
|
112
|
+
|
|
113
|
+
# 기존 A 레코드 확인
|
|
114
|
+
existing_records = get_dns_records(zone_id, domain, ['A'])
|
|
115
|
+
|
|
116
|
+
if existing_records.any?
|
|
117
|
+
existing_record = existing_records.first
|
|
118
|
+
if existing_record['content'] == public_ip
|
|
119
|
+
puts "✅ #{domain} → #{public_ip} (이미 설정됨)".colorize(:green)
|
|
120
|
+
else
|
|
121
|
+
puts "⚠️ 기존 레코드를 업데이트합니다.".colorize(:yellow)
|
|
122
|
+
update_dns_record(zone_id, existing_record['id'], public_ip)
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
# 새 A 레코드 생성
|
|
126
|
+
create_dns_record(zone_id, domain, 'A', public_ip)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def load_saved_token
|
|
134
|
+
return nil unless File.exist?(TOKEN_FILE)
|
|
135
|
+
File.read(TOKEN_FILE).strip
|
|
136
|
+
rescue
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def save_token(token)
|
|
141
|
+
dir = File.dirname(TOKEN_FILE)
|
|
142
|
+
|
|
143
|
+
# 디렉토리가 파일로 존재하는 경우 삭제
|
|
144
|
+
if File.exist?(dir) && !File.directory?(dir)
|
|
145
|
+
FileUtils.rm(dir)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# 디렉토리가 없을 때만 생성
|
|
149
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
150
|
+
|
|
151
|
+
File.write(TOKEN_FILE, token)
|
|
152
|
+
File.chmod(0600, TOKEN_FILE)
|
|
153
|
+
puts "✅ API 토큰이 저장되었습니다.".colorize(:green)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def open_token_creation_page
|
|
157
|
+
puts "토큰 생성 페이지를 엽니다...".colorize(:cyan)
|
|
158
|
+
system("open 'https://dash.cloudflare.com/profile/api-tokens' 2>/dev/null || xdg-open 'https://dash.cloudflare.com/profile/api-tokens' 2>/dev/null")
|
|
159
|
+
|
|
160
|
+
puts "\n다음 권한으로 토큰을 생성해주세요:".colorize(:yellow)
|
|
161
|
+
puts ""
|
|
162
|
+
puts "한국어 화면:".colorize(:gray)
|
|
163
|
+
puts "• 영역 → 영역 → 읽기".colorize(:white)
|
|
164
|
+
puts "• 영역 → DNS → 편집".colorize(:white)
|
|
165
|
+
puts " (영역 리소스는 '모든 영역' 선택)".colorize(:gray)
|
|
166
|
+
puts ""
|
|
167
|
+
puts "English:".colorize(:gray)
|
|
168
|
+
puts "• Zone → Zone → Read".colorize(:white)
|
|
169
|
+
puts "• Zone → DNS → Edit".colorize(:white)
|
|
170
|
+
puts " (Zone Resources: Select 'All zones')".colorize(:gray)
|
|
171
|
+
puts ""
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def get_cloudflare_token
|
|
175
|
+
token = @prompt.mask("생성된 Cloudflare API 토큰을 붙여넣으세요:")
|
|
176
|
+
|
|
177
|
+
if token.nil? || token.strip.empty?
|
|
178
|
+
puts "❌ 토큰이 입력되지 않았습니다.".colorize(:red)
|
|
179
|
+
exit 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if test_token(token.strip)
|
|
183
|
+
puts "✅ 토큰이 확인되었습니다.".colorize(:green)
|
|
184
|
+
return token.strip
|
|
185
|
+
else
|
|
186
|
+
puts "❌ 토큰이 올바르지 않거나 권한이 부족합니다.".colorize(:red)
|
|
187
|
+
exit 1
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def test_token(token)
|
|
192
|
+
uri = URI('https://api.cloudflare.com/client/v4/user/tokens/verify')
|
|
193
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
194
|
+
http.use_ssl = true
|
|
195
|
+
|
|
196
|
+
request = Net::HTTP::Get.new(uri)
|
|
197
|
+
request['Authorization'] = "Bearer #{token}"
|
|
198
|
+
request['Content-Type'] = 'application/json'
|
|
199
|
+
|
|
200
|
+
response = http.request(request)
|
|
201
|
+
|
|
202
|
+
if response.code == '200'
|
|
203
|
+
data = JSON.parse(response.body)
|
|
204
|
+
return data['success'] == true
|
|
205
|
+
else
|
|
206
|
+
return false
|
|
207
|
+
end
|
|
208
|
+
rescue => e
|
|
209
|
+
puts "⚠️ 토큰 검증 중 오류: #{e.message}".colorize(:yellow) if ENV["DEBUG"]
|
|
210
|
+
false
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def get_cloudflare_zones
|
|
214
|
+
uri = URI('https://api.cloudflare.com/client/v4/zones')
|
|
215
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
216
|
+
http.use_ssl = true
|
|
217
|
+
|
|
218
|
+
request = Net::HTTP::Get.new(uri)
|
|
219
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
220
|
+
request['Content-Type'] = 'application/json'
|
|
221
|
+
|
|
222
|
+
response = http.request(request)
|
|
223
|
+
|
|
224
|
+
if response.code == '200'
|
|
225
|
+
data = JSON.parse(response.body)
|
|
226
|
+
return data['result'] || []
|
|
227
|
+
else
|
|
228
|
+
puts "❌ 도메인 목록 조회에 실패했습니다: #{response.code}".colorize(:red)
|
|
229
|
+
[]
|
|
230
|
+
end
|
|
231
|
+
rescue => e
|
|
232
|
+
puts "❌ API 요청 중 오류가 발생했습니다: #{e.message}".colorize(:red)
|
|
233
|
+
[]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def get_dns_records(zone_id, name, types)
|
|
237
|
+
records = []
|
|
238
|
+
|
|
239
|
+
types.each do |type|
|
|
240
|
+
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
|
|
241
|
+
uri.query = URI.encode_www_form({
|
|
242
|
+
type: type,
|
|
243
|
+
name: name
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
247
|
+
http.use_ssl = true
|
|
248
|
+
|
|
249
|
+
request = Net::HTTP::Get.new(uri)
|
|
250
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
251
|
+
request['Content-Type'] = 'application/json'
|
|
252
|
+
|
|
253
|
+
response = http.request(request)
|
|
254
|
+
|
|
255
|
+
if response.code == '200'
|
|
256
|
+
data = JSON.parse(response.body)
|
|
257
|
+
records.concat(data['result'] || [])
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
records
|
|
262
|
+
rescue => e
|
|
263
|
+
puts "⚠️ DNS 레코드 조회 중 오류: #{e.message}".colorize(:yellow)
|
|
264
|
+
[]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def create_dns_record(zone_id, name, type, content)
|
|
268
|
+
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
|
|
269
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
270
|
+
http.use_ssl = true
|
|
271
|
+
|
|
272
|
+
request = Net::HTTP::Post.new(uri)
|
|
273
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
274
|
+
request['Content-Type'] = 'application/json'
|
|
275
|
+
|
|
276
|
+
data = {
|
|
277
|
+
type: type,
|
|
278
|
+
name: name,
|
|
279
|
+
content: content,
|
|
280
|
+
ttl: 300,
|
|
281
|
+
proxied: false
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
request.body = data.to_json
|
|
285
|
+
response = http.request(request)
|
|
286
|
+
|
|
287
|
+
if response.code == '200'
|
|
288
|
+
puts "✅ #{name} → #{content} (A 레코드 생성됨)".colorize(:green)
|
|
289
|
+
else
|
|
290
|
+
puts "❌ DNS 레코드 생성 실패: #{response.code}".colorize(:red)
|
|
291
|
+
puts response.body if ENV["DEBUG"]
|
|
292
|
+
end
|
|
293
|
+
rescue => e
|
|
294
|
+
puts "❌ DNS 레코드 생성 중 오류: #{e.message}".colorize(:red)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def update_dns_record(zone_id, record_id, new_content)
|
|
298
|
+
uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records/#{record_id}")
|
|
299
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
300
|
+
http.use_ssl = true
|
|
301
|
+
|
|
302
|
+
request = Net::HTTP::Patch.new(uri)
|
|
303
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
304
|
+
request['Content-Type'] = 'application/json'
|
|
305
|
+
|
|
306
|
+
data = {
|
|
307
|
+
content: new_content
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
request.body = data.to_json
|
|
311
|
+
response = http.request(request)
|
|
312
|
+
|
|
313
|
+
if response.code == '200'
|
|
314
|
+
puts "✅ DNS 레코드가 업데이트되었습니다.".colorize(:green)
|
|
315
|
+
else
|
|
316
|
+
puts "❌ DNS 레코드 업데이트 실패: #{response.code}".colorize(:red)
|
|
317
|
+
end
|
|
318
|
+
rescue => e
|
|
319
|
+
puts "❌ DNS 레코드 업데이트 중 오류: #{e.message}".colorize(:red)
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|