tayo 0.1.5 β†’ 0.1.6

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.
@@ -0,0 +1,1690 @@
1
+ This file is a merged representation of the entire codebase, combined into a single document by Repomix.
2
+
3
+ <file_summary>
4
+ This section contains a summary of this file.
5
+
6
+ <purpose>
7
+ This file contains a packed representation of the entire repository's contents.
8
+ It is designed to be easily consumable by AI systems for analysis, code review,
9
+ or other automated processes.
10
+ </purpose>
11
+
12
+ <file_format>
13
+ The content is organized as follows:
14
+ 1. This summary section
15
+ 2. Repository information
16
+ 3. Directory structure
17
+ 4. Repository files (if enabled)
18
+ 5. Multiple file entries, each consisting of:
19
+ - File path as an attribute
20
+ - Full contents of the file
21
+ </file_format>
22
+
23
+ <usage_guidelines>
24
+ - This file should be treated as read-only. Any changes should be made to the
25
+ original repository files, not this packed version.
26
+ - When processing this file, use the file path to distinguish
27
+ between different files in the repository.
28
+ - Be aware that this file may contain sensitive information. Handle it with
29
+ the same level of security as you would the original repository.
30
+ </usage_guidelines>
31
+
32
+ <notes>
33
+ - Some files may have been excluded based on .gitignore rules and Repomix's configuration
34
+ - Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
35
+ - Files matching patterns in .gitignore are excluded
36
+ - Files matching default ignore patterns are excluded
37
+ - Files are sorted by Git change count (files with more changes are at the bottom)
38
+ </notes>
39
+
40
+ </file_summary>
41
+
42
+ <directory_structure>
43
+ .claude/
44
+ settings.local.json
45
+ bin/
46
+ console
47
+ setup
48
+ exe/
49
+ tayo
50
+ lib/
51
+ tayo/
52
+ commands/
53
+ cf.rb
54
+ gh.rb
55
+ init.rb
56
+ cli.rb
57
+ version.rb
58
+ tayo.rb
59
+ sig/
60
+ tayo.rbs
61
+ .gitignore
62
+ Gemfile
63
+ Rakefile
64
+ README.md
65
+ tayo.gemspec
66
+ </directory_structure>
67
+
68
+ <files>
69
+ This section contains the contents of the repository's files.
70
+
71
+ <file path=".claude/settings.local.json">
72
+ {
73
+ "permissions": {
74
+ "allow": [
75
+ "Bash(rg:*)",
76
+ "Bash(mv:*)",
77
+ "Bash(bundle update:*)",
78
+ "Bash(chmod:*)",
79
+ "Bash(ls:*)",
80
+ "Bash(git init:*)",
81
+ "Bash(git add:*)",
82
+ "Bash(rake build)",
83
+ "Bash(git commit:*)",
84
+ "Bash(gh repo create:*)",
85
+ "Bash(gem push:*)",
86
+ "Bash(gem owner:*)",
87
+ "Bash(gem signout:*)",
88
+ "Bash(gem help:*)",
89
+ "Bash(gem:*)",
90
+ "Bash(mkdir:*)",
91
+ "Bash(git push:*)"
92
+ ],
93
+ "deny": []
94
+ }
95
+ }
96
+ </file>
97
+
98
+ <file path="bin/console">
99
+ #!/usr/bin/env ruby
100
+ # frozen_string_literal: true
101
+
102
+ require "bundler/setup"
103
+ require "tayo"
104
+
105
+ # You can add fixtures and/or initialization code here to make experimenting
106
+ # with your gem easier. You can also use a different console, if you like.
107
+
108
+ require "irb"
109
+ IRB.start(__FILE__)
110
+ </file>
111
+
112
+ <file path="bin/setup">
113
+ #!/usr/bin/env bash
114
+ set -euo pipefail
115
+ IFS=$'\n\t'
116
+ set -vx
117
+
118
+ bundle install
119
+
120
+ # Do any other automated setup that you need to do here
121
+ </file>
122
+
123
+ <file path="exe/tayo">
124
+ #!/usr/bin/env ruby
125
+
126
+ require "tayo"
127
+
128
+ Tayo::CLI.start(ARGV)
129
+ </file>
130
+
131
+ <file path="lib/tayo/cli.rb">
132
+ # frozen_string_literal: true
133
+
134
+ require "thor"
135
+ require "colorize"
136
+ require_relative "commands/init"
137
+ require_relative "commands/gh"
138
+ require_relative "commands/cf"
139
+
140
+ module Tayo
141
+ class CLI < Thor
142
+ desc "init", "Rails ν”„λ‘œμ νŠΈμ— Tayoλ₯Ό μ„€μ •ν•©λ‹ˆλ‹€"
143
+ def init
144
+ Commands::Init.new.execute
145
+ end
146
+
147
+ desc "gh", "GitHub μ €μž₯μ†Œμ™€ μ»¨ν…Œμ΄λ„ˆ λ ˆμ§€μŠ€νŠΈλ¦¬λ₯Ό μ„€μ •ν•©λ‹ˆλ‹€"
148
+ def gh
149
+ Commands::Gh.new.execute
150
+ end
151
+
152
+ desc "cf", "Cloudflare DNSλ₯Ό μ„€μ •ν•˜μ—¬ ν™ˆμ„œλ²„μ— 도메인을 μ—°κ²°ν•©λ‹ˆλ‹€"
153
+ def cf
154
+ Commands::Cf.new.execute
155
+ end
156
+
157
+ desc "version", "Tayo 버전을 ν‘œμ‹œν•©λ‹ˆλ‹€"
158
+ def version
159
+ puts "Tayo #{VERSION}"
160
+ end
161
+ end
162
+ end
163
+ </file>
164
+
165
+ <file path="lib/tayo.rb">
166
+ # frozen_string_literal: true
167
+
168
+ require_relative "tayo/version"
169
+ require_relative "tayo/cli"
170
+
171
+ module Tayo
172
+ class Error < StandardError; end
173
+ end
174
+ </file>
175
+
176
+ <file path="sig/tayo.rbs">
177
+ module Tayo
178
+ VERSION: String
179
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
180
+ end
181
+ </file>
182
+
183
+ <file path=".gitignore">
184
+ *.gem
185
+ *.rbc
186
+ /.config
187
+ /coverage/
188
+ /InstalledFiles
189
+ /pkg/
190
+ /spec/reports/
191
+ /spec/examples.txt
192
+ /test/tmp/
193
+ /test/version_tmp/
194
+ /tmp/
195
+ .DS_Store
196
+
197
+ # Used by dotenv library to load environment variables.
198
+ # .env
199
+
200
+ # Ignore Byebug command history file.
201
+ .byebug_history
202
+
203
+ ## Specific to RubyMotion:
204
+ .dat*
205
+ .repl_history
206
+ build/
207
+ *.bridgesupport
208
+ build-iPhoneOS/
209
+ build-iPhoneSimulator/
210
+
211
+ ## Documentation cache and generated files:
212
+ /.yardoc/
213
+ /_yardoc/
214
+ /doc/
215
+ /rdoc/
216
+
217
+ ## Environment normalization:
218
+ /.bundle/
219
+ /vendor/bundle
220
+ /lib/bundler/man/
221
+
222
+ # for a library or gem, you might want to ignore these files since the code is
223
+ # intended to run in multiple environments; otherwise, check them in:
224
+ # Gemfile.lock
225
+ # .ruby-version
226
+ # .ruby-gemset
227
+
228
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
229
+ .rvmrc
230
+
231
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
232
+ # .rubocop-https?--*
233
+ </file>
234
+
235
+ <file path="Gemfile">
236
+ # frozen_string_literal: true
237
+
238
+ source "https://rubygems.org"
239
+
240
+ # Specify your gem's dependencies in tayo.gemspec
241
+ gemspec
242
+
243
+ gem "irb"
244
+ gem "rake", "~> 13.0"
245
+ </file>
246
+
247
+ <file path="Rakefile">
248
+ # frozen_string_literal: true
249
+
250
+ require "bundler/gem_tasks"
251
+ task default: %i[]
252
+ </file>
253
+
254
+ <file path="README.md">
255
+ # Tayo
256
+
257
+ Rails μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ ν™ˆμ„œλ²„μ— λ°°ν¬ν•˜κΈ° μœ„ν•œ λ„κ΅¬μž…λ‹ˆλ‹€.
258
+
259
+ ## μ„€μΉ˜
260
+
261
+ μ‹œμŠ€ν…œ μ™€μ΄λ“œλ‘œ μ„€μΉ˜:
262
+
263
+ ```bash
264
+ gem install tayo
265
+ ```
266
+
267
+ ## μ‚¬μš©λ²•
268
+
269
+ ### 1. `tayo init` - Rails ν”„λ‘œμ νŠΈ μ΄ˆκΈ°ν™”
270
+
271
+ Rails ν”„λ‘œμ νŠΈλ₯Ό ν™ˆμ„œλ²„ 배포λ₯Ό μœ„ν•΄ μ€€λΉ„ν•©λ‹ˆλ‹€.
272
+
273
+ ```bash
274
+ tayo init
275
+ ```
276
+
277
+ 이 λͺ…λ Ήμ–΄λŠ” λ‹€μŒ μž‘μ—…λ“€μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€:
278
+
279
+ - **OrbStack μ„€μΉ˜ 확인**: Docker μ»¨ν…Œμ΄λ„ˆλ₯Ό μ‹€ν–‰ν•˜κΈ° μœ„ν•œ OrbStack이 μ„€μΉ˜λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€
280
+ - **Gemfile μˆ˜μ •**: development 그룹에 tayo gem을 μΆ”κ°€ν•©λ‹ˆλ‹€
281
+ - **Bundle μ„€μΉ˜**: μ˜μ‘΄μ„±μ„ μ„€μΉ˜ν•©λ‹ˆλ‹€
282
+ - **Linux ν”Œλž«νΌ μΆ”κ°€**: `x86_64-linux`와 `aarch64-linux` ν”Œλž«νΌμ„ Gemfile.lock에 μΆ”κ°€ν•©λ‹ˆλ‹€
283
+ - **Dockerfile 생성**: Rails 7 κΈ°λ³Έ Dockerfile이 μ—†μœΌλ©΄ μƒμ„±ν•©λ‹ˆλ‹€
284
+ - **Welcome νŽ˜μ΄μ§€ 생성**:
285
+ - `app/controllers/welcome_controller.rb` 컨트둀러 생성
286
+ - `app/views/welcome/index.html.erb` λ·° 파일 생성 (μ• λ‹ˆλ©”μ΄μ…˜μ΄ μžˆλŠ” 예쁜 λžœλ”© νŽ˜μ΄μ§€)
287
+ - `config/routes.rb`에 `root 'welcome#index'` μ„€μ • μΆ”κ°€
288
+ - **Git 컀밋**: 변경사항을 μžλ™μœΌλ‘œ μ»€λ°‹ν•©λ‹ˆλ‹€
289
+ - **Docker μΊμ‹œ 정리**: λ””μŠ€ν¬ 곡간 확보λ₯Ό μœ„ν•΄ Docker μΊμ‹œλ₯Ό μ •λ¦¬ν•©λ‹ˆλ‹€
290
+
291
+ ### 2. `tayo gh` - GitHub μ €μž₯μ†Œ 및 Container Registry μ„€μ •
292
+
293
+ GitHub μ €μž₯μ†Œλ₯Ό μƒμ„±ν•˜κ³  Container Registryλ₯Ό μ„€μ •ν•©λ‹ˆλ‹€.
294
+
295
+ ```bash
296
+ tayo gh
297
+ ```
298
+
299
+ 이 λͺ…λ Ήμ–΄λŠ” λ‹€μŒ μž‘μ—…λ“€μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€:
300
+
301
+ - **GitHub CLI μ„€μΉ˜ 확인**: `gh` λͺ…λ Ήμ–΄κ°€ μ„€μΉ˜λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€
302
+ - **GitHub 인증 확인**:
303
+ - GitHub에 λ‘œκ·ΈμΈλ˜μ–΄ μžˆλŠ”μ§€ 확인
304
+ - ν•„μš”ν•œ κΆŒν•œ(repo, read:org, write:packages) 확인
305
+ - κΆŒν•œμ΄ μ—†μœΌλ©΄ λΈŒλΌμš°μ €μ—μ„œ 토큰 생성 νŽ˜μ΄μ§€λ₯Ό μ—½λ‹ˆλ‹€
306
+ - **Git μ €μž₯μ†Œ μ΄ˆκΈ°ν™”**: 아직 git μ €μž₯μ†Œκ°€ μ•„λ‹ˆλ©΄ μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€
307
+ - **GitHub 원격 μ €μž₯μ†Œ μ„€μ •**:
308
+ - κΈ°μ‘΄ 원격 μ €μž₯μ†Œκ°€ 있으면 μ‚¬μš©
309
+ - μ—†μœΌλ©΄ μƒˆ μ €μž₯μ†Œ 생성 (public/private 선택 κ°€λŠ₯)
310
+ - μ½”λ“œλ₯Ό GitHub에 ν‘Έμ‹œ
311
+ - **GitHub Container Registry μ„€μ •**:
312
+ - Registry URL 생성: `ghcr.io/username/repository-name`
313
+ - Docker둜 μžλ™ 둜그인 μ‹€ν–‰
314
+ - **배포 μ„€μ • 파일 생성**:
315
+ - `config/deploy.yml` 파일 생성 λ˜λŠ” μ—…λ°μ΄νŠΈ
316
+ - μ„œλ²„ IP, 도메인, λ°μ΄ν„°λ² μ΄μŠ€ λ“± μ„€μ • 포함
317
+ - **ν™˜κ²½ λ³€μˆ˜ 파일 μ€€λΉ„**:
318
+ - `.env.production` 파일 생성
319
+ - `.gitignore`에 μΆ”κ°€ν•˜μ—¬ λ³΄μ•ˆ μœ μ§€
320
+
321
+ ### 3. `tayo cf` - Cloudflare DNS μ„€μ •
322
+
323
+ Cloudflareλ₯Ό 톡해 도메인을 ν™ˆμ„œλ²„ IP에 μ—°κ²°ν•©λ‹ˆλ‹€.
324
+
325
+ ```bash
326
+ tayo cf
327
+ ```
328
+
329
+ 이 λͺ…λ Ήμ–΄λŠ” λ‹€μŒ μž‘μ—…λ“€μ„ μˆ˜ν–‰ν•©λ‹ˆλ‹€:
330
+
331
+ - **μ„€μ • 파일 확인**: `config/deploy.yml` νŒŒμΌμ—μ„œ μ„œλ²„ IP와 도메인 정보λ₯Ό μ½μŠ΅λ‹ˆλ‹€
332
+ - **Cloudflare 인증**:
333
+ - API 토큰 μž…λ ₯ μš”μ²­ (처음 μ‹€ν–‰ μ‹œ)
334
+ - 토큰을 μ•ˆμ „ν•˜κ²Œ μ €μž₯ (macOS Keychain μ‚¬μš©)
335
+ - **도메인 Zone 확인**:
336
+ - Cloudflare κ³„μ •μ—μ„œ 도메인을 μ°ΎμŠ΅λ‹ˆλ‹€
337
+ - Zone IDλ₯Ό μžλ™μœΌλ‘œ κ°€μ Έμ˜΅λ‹ˆλ‹€
338
+ - **DNS λ ˆμ½”λ“œ 생성/μ—…λ°μ΄νŠΈ**:
339
+ - A λ ˆμ½”λ“œ 생성: 도메인을 μ„œλ²„ IP에 μ—°κ²°
340
+ - κΈ°μ‘΄ λ ˆμ½”λ“œκ°€ 있으면 μ—…λ°μ΄νŠΈ
341
+ - Proxied μ„€μ • (Cloudflare CDN μ‚¬μš©)
342
+ - **μ„€μ • μ™„λ£Œ 확인**:
343
+ - DNS 섀정이 μ™„λ£Œλ˜λ©΄ 성곡 λ©”μ‹œμ§€ ν‘œμ‹œ
344
+ - λ„λ©”μΈμœΌλ‘œ 접속 κ°€λŠ₯함을 μ•ˆλ‚΄
345
+
346
+ 각 λͺ…λ Ήμ–΄λŠ” λ‹¨κ³„λ³„λ‘œ μ§„ν–‰ 상황을 ν‘œμ‹œν•˜λ©°, 였λ₯˜κ°€ λ°œμƒν•˜λ©΄ μΉœμ ˆν•œ μ•ˆλ‚΄ λ©”μ‹œμ§€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
347
+
348
+ rails new 둜 ν”„λ‘œμ νŠΈ 생성 ν›„
349
+ bundle exec tayo init
350
+ bundle exec tayo gh
351
+ bundle exec tayo cf
352
+
353
+ 순으둜 μ§„ν–‰ ν›„
354
+ bin/kamal setup 으둜 배포 μ§„ν–‰
355
+ </file>
356
+
357
+ <file path="lib/tayo/commands/cf.rb">
358
+ # frozen_string_literal: true
359
+
360
+ require "colorize"
361
+ require "tty-prompt"
362
+ require "net/http"
363
+ require "json"
364
+ require "uri"
365
+
366
+ module Tayo
367
+ module Commands
368
+ class Cf
369
+ def execute
370
+ puts "☁️ Cloudflare DNS 섀정을 μ‹œμž‘ν•©λ‹ˆλ‹€...".colorize(:green)
371
+
372
+ unless rails_project?
373
+ puts "❌ Rails ν”„λ‘œμ νŠΈκ°€ μ•„λ‹™λ‹ˆλ‹€. Rails ν”„λ‘œμ νŠΈ λ£¨νŠΈμ—μ„œ μ‹€ν–‰ν•΄μ£Όμ„Έμš”.".colorize(:red)
374
+ return
375
+ end
376
+
377
+ # 1. 도메인 μž…λ ₯λ°›κΈ°
378
+ domain_info = get_domain_input
379
+
380
+ # 2. Cloudflare 토큰 생성 νŽ˜μ΄μ§€ μ—΄κΈ° 및 κΆŒν•œ μ•ˆλ‚΄
381
+ open_token_creation_page
382
+
383
+ # 3. 토큰 μž…λ ₯λ°›κΈ°
384
+ token = get_cloudflare_token
385
+
386
+ # 4. Cloudflare API둜 도메인 λͺ©λ‘ 쑰회 및 선택
387
+ selected_zone = select_cloudflare_zone(token)
388
+
389
+ # 5. 루트 도메인 λ ˆμ½”λ“œ 확인
390
+ existing_records = check_existing_records(token, selected_zone, domain_info)
391
+
392
+ # 6. DNS λ ˆμ½”λ“œ μΆ”κ°€/μˆ˜μ •
393
+ setup_dns_record(token, selected_zone, domain_info, existing_records)
394
+
395
+ # 7. config/deploy.yml μ—…λ°μ΄νŠΈ
396
+ update_deploy_config(domain_info)
397
+
398
+ puts "\nπŸŽ‰ Cloudflare DNS 섀정이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€!".colorize(:green)
399
+
400
+ # 변경사항 컀밋
401
+ commit_cloudflare_changes(domain_info)
402
+ end
403
+
404
+ private
405
+
406
+ def rails_project?
407
+ File.exist?("Gemfile") && File.exist?("config/application.rb")
408
+ end
409
+
410
+ def get_domain_input
411
+ prompt = TTY::Prompt.new
412
+
413
+ puts "\nπŸ“ 배포할 도메인을 μ„€μ •ν•©λ‹ˆλ‹€.".colorize(:yellow)
414
+
415
+ domain = prompt.ask("배포할 도메인을 μž…λ ₯ν•˜μ„Έμš” (예: myapp.com, api.example.com):") do |q|
416
+ q.validate(/\A[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/, "μ˜¬λ°”λ₯Έ 도메인 ν˜•μ‹μ„ μž…λ ₯ν•΄μ£Όμ„Έμš” (예: myapp.com)")
417
+ end
418
+
419
+ # 도메인이 λ£¨νŠΈμΈμ§€ μ„œλΈŒλ„λ©”μΈμΈμ§€ νŒλ‹¨
420
+ parts = domain.split('.')
421
+ if parts.length == 2
422
+ { type: :root, domain: domain, zone: domain }
423
+ else
424
+ zone = parts[-2..-1].join('.')
425
+ { type: :subdomain, domain: domain, zone: zone, subdomain: parts[0..-3].join('.') }
426
+ end
427
+ end
428
+
429
+ def open_token_creation_page
430
+ puts "\nπŸ”‘ Cloudflare API 토큰이 ν•„μš”ν•©λ‹ˆλ‹€.".colorize(:yellow)
431
+ puts "토큰 생성 νŽ˜μ΄μ§€λ₯Ό μ—½λ‹ˆλ‹€...".colorize(:cyan)
432
+
433
+ # Cloudflare API 토큰 생성 νŽ˜μ΄μ§€ μ—΄κΈ°
434
+ system("open 'https://dash.cloudflare.com/profile/api-tokens'")
435
+
436
+ puts "\nλ‹€μŒ κΆŒν•œμœΌλ‘œ 토큰을 μƒμ„±ν•΄μ£Όμ„Έμš”:".colorize(:yellow)
437
+ puts ""
438
+ puts "ν•œκ΅­μ–΄ ν™”λ©΄:".colorize(:gray)
439
+ puts "β€’ μ˜μ—­ β†’ DNS β†’ 읽기".colorize(:white)
440
+ puts "β€’ μ˜μ—­ β†’ DNS β†’ νŽΈμ§‘".colorize(:white)
441
+ puts " (μ˜μ—­ λ¦¬μ†ŒμŠ€λŠ” 'λͺ¨λ“  μ˜μ—­' 선택)".colorize(:gray)
442
+ puts ""
443
+ puts "English:".colorize(:gray)
444
+ puts "β€’ Zone β†’ DNS β†’ Read".colorize(:white)
445
+ puts "β€’ Zone β†’ DNS β†’ Edit".colorize(:white)
446
+ puts " (Zone Resources: Select 'All zones')".colorize(:gray)
447
+ puts ""
448
+ end
449
+
450
+ def get_cloudflare_token
451
+ prompt = TTY::Prompt.new
452
+
453
+ token = prompt.mask("μƒμ„±λœ Cloudflare API 토큰을 λΆ™μ—¬λ„£μœΌμ„Έμš”:")
454
+
455
+ if token.nil? || token.strip.empty?
456
+ puts "❌ 토큰이 μž…λ ₯λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.".colorize(:red)
457
+ exit 1
458
+ end
459
+
460
+ # 토큰 μœ νš¨μ„± 간단 확인
461
+ if test_cloudflare_token(token.strip)
462
+ puts "βœ… 토큰이 ν™•μΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
463
+ return token.strip
464
+ else
465
+ puts "❌ 토큰이 μ˜¬λ°”λ₯΄μ§€ μ•Šκ±°λ‚˜ κΆŒν•œμ΄ λΆ€μ‘±ν•©λ‹ˆλ‹€.".colorize(:red)
466
+ exit 1
467
+ end
468
+ end
469
+
470
+ def test_cloudflare_token(token)
471
+ uri = URI('https://api.cloudflare.com/client/v4/user/tokens/verify')
472
+ http = Net::HTTP.new(uri.host, uri.port)
473
+ http.use_ssl = true
474
+
475
+ request = Net::HTTP::Get.new(uri)
476
+ request['Authorization'] = "Bearer #{token}"
477
+ request['Content-Type'] = 'application/json'
478
+
479
+ response = http.request(request)
480
+ return response.code == '200'
481
+ rescue
482
+ return false
483
+ end
484
+
485
+ def select_cloudflare_zone(token)
486
+ puts "\n🌐 Cloudflare 도메인 λͺ©λ‘μ„ μ‘°νšŒν•©λ‹ˆλ‹€...".colorize(:yellow)
487
+
488
+ zones = get_cloudflare_zones(token)
489
+
490
+ if zones.empty?
491
+ puts "❌ Cloudflare에 λ“±λ‘λœ 도메인이 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:red)
492
+ puts "λ¨Όμ € https://dash.cloudflare.com μ—μ„œ 도메인을 μΆ”κ°€ν•΄μ£Όμ„Έμš”.".colorize(:cyan)
493
+ exit 1
494
+ end
495
+
496
+ prompt = TTY::Prompt.new
497
+ zone_choices = zones.map { |zone| "#{zone['name']} (#{zone['status']})" }
498
+
499
+ selected = prompt.select("도메인을 μ„ νƒν•˜μ„Έμš”:", zone_choices)
500
+ zone_name = selected.split(' ').first
501
+
502
+ selected_zone = zones.find { |zone| zone['name'] == zone_name }
503
+ puts "βœ… μ„ νƒλœ 도메인: #{zone_name}".colorize(:green)
504
+
505
+ return selected_zone
506
+ end
507
+
508
+ def get_cloudflare_zones(token)
509
+ uri = URI('https://api.cloudflare.com/client/v4/zones')
510
+ http = Net::HTTP.new(uri.host, uri.port)
511
+ http.use_ssl = true
512
+
513
+ request = Net::HTTP::Get.new(uri)
514
+ request['Authorization'] = "Bearer #{token}"
515
+ request['Content-Type'] = 'application/json'
516
+
517
+ response = http.request(request)
518
+
519
+ if response.code == '200'
520
+ data = JSON.parse(response.body)
521
+ return data['result'] || []
522
+ else
523
+ puts "❌ 도메인 λͺ©λ‘ μ‘°νšŒμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: #{response.code}".colorize(:red)
524
+ exit 1
525
+ end
526
+ rescue => e
527
+ puts "❌ API μš”μ²­ 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: #{e.message}".colorize(:red)
528
+ exit 1
529
+ end
530
+
531
+ def check_existing_records(token, zone, domain_info)
532
+ puts "\nπŸ” κΈ°μ‘΄ DNS λ ˆμ½”λ“œλ₯Ό ν™•μΈν•©λ‹ˆλ‹€...".colorize(:yellow)
533
+
534
+ zone_id = zone['id']
535
+ zone_name = zone['name']
536
+
537
+ # 루트 λ„λ©”μΈμ˜ A/CNAME λ ˆμ½”λ“œ 확인
538
+ records = get_dns_records(token, zone_id, zone_name, ['A', 'CNAME'])
539
+
540
+ puts "κΈ°μ‘΄ λ ˆμ½”λ“œ: #{records.length}개 발견".colorize(:gray)
541
+
542
+ return records
543
+ end
544
+
545
+ def get_dns_records(token, zone_id, name, types)
546
+ records = []
547
+
548
+ types.each do |type|
549
+ uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
550
+ uri.query = URI.encode_www_form({
551
+ type: type,
552
+ name: name
553
+ })
554
+
555
+ http = Net::HTTP.new(uri.host, uri.port)
556
+ http.use_ssl = true
557
+
558
+ request = Net::HTTP::Get.new(uri)
559
+ request['Authorization'] = "Bearer #{token}"
560
+ request['Content-Type'] = 'application/json'
561
+
562
+ response = http.request(request)
563
+
564
+ if response.code == '200'
565
+ data = JSON.parse(response.body)
566
+ records.concat(data['result'] || [])
567
+ end
568
+ end
569
+
570
+ return records
571
+ rescue => e
572
+ puts "❌ DNS λ ˆμ½”λ“œ 쑰회 쀑 였λ₯˜: #{e.message}".colorize(:red)
573
+ return []
574
+ end
575
+
576
+ def setup_dns_record(token, zone, domain_info, existing_records)
577
+ puts "\nβš™οΈ DNS λ ˆμ½”λ“œλ₯Ό μ„€μ •ν•©λ‹ˆλ‹€...".colorize(:yellow)
578
+
579
+ # ν™ˆμ„œλ²„ IP/URL μž…λ ₯λ°›κΈ°
580
+ prompt = TTY::Prompt.new
581
+
582
+ server_info = prompt.ask("ν™ˆμ„œλ²„ IP λ˜λŠ” 도메인을 μž…λ ₯ν•˜μ„Έμš”:") do |q|
583
+ q.validate(/\A.+\z/, "μ„œλ²„ 정보λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”")
584
+ end
585
+
586
+ # SSH μ‚¬μš©μž 계정 μž…λ ₯λ°›κΈ°
587
+ ssh_user = prompt.ask("SSH μ‚¬μš©μž 계정을 μž…λ ₯ν•˜μ„Έμš”:", default: "root")
588
+
589
+ # IP인지 도메인인지 νŒλ‹¨
590
+ is_ip = server_info.match?(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/)
591
+ record_type = is_ip ? 'A' : 'CNAME'
592
+
593
+ zone_id = zone['id']
594
+ zone_name = zone['name']
595
+
596
+ # 도메인 정보에 따라 λ ˆμ½”λ“œ μ„€μ •
597
+ final_domain = determine_final_domain(domain_info, zone_name, existing_records)
598
+
599
+ # λŒ€μƒ λ„λ©”μΈμ˜ λͺ¨λ“  A/CNAME λ ˆμ½”λ“œ 확인
600
+ all_records = get_dns_records(token, zone_id, final_domain[:name], ['A', 'CNAME'])
601
+
602
+ if all_records.any?
603
+ existing_record = all_records.first
604
+
605
+ # λ™μΌν•œ νƒ€μž…μ΄κ³  같은 값이면 κ±΄λ„ˆλ›°κΈ°
606
+ if existing_record['type'] == record_type && existing_record['content'] == server_info
607
+ puts "βœ… DNS λ ˆμ½”λ“œκ°€ 이미 μ˜¬λ°”λ₯΄κ²Œ μ„€μ •λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
608
+ puts " #{final_domain[:full_domain]} β†’ #{server_info} (#{record_type} λ ˆμ½”λ“œ)".colorize(:gray)
609
+ else
610
+ # νƒ€μž…μ΄ λ‹€λ₯΄κ±°λ‚˜ 값이 λ‹€λ₯Έ 경우 μ‚­μ œ ν›„ μž¬μƒμ„±
611
+ puts "⚠️ κΈ°μ‘΄ λ ˆμ½”λ“œλ₯Ό μ‚­μ œν•˜κ³  μƒˆλ‘œ μƒμ„±ν•©λ‹ˆλ‹€.".colorize(:yellow)
612
+ puts " κΈ°μ‘΄: #{existing_record['content']} (#{existing_record['type']}) β†’ μƒˆλ‘œμš΄: #{server_info} (#{record_type})".colorize(:gray)
613
+
614
+ # κΈ°μ‘΄ λ ˆμ½”λ“œ μ‚­μ œ
615
+ delete_dns_record(token, zone_id, existing_record['id'])
616
+
617
+ # μƒˆ λ ˆμ½”λ“œ 생성
618
+ create_dns_record(token, zone_id, final_domain[:name], record_type, server_info)
619
+ end
620
+ else
621
+ # DNS λ ˆμ½”λ“œ 생성
622
+ create_dns_record(token, zone_id, final_domain[:name], record_type, server_info)
623
+ end
624
+
625
+ # μ΅œμ’… 도메인 정보 μ €μž₯
626
+ @final_domain = final_domain[:full_domain]
627
+ @server_info = server_info
628
+ @ssh_user = ssh_user
629
+ end
630
+
631
+ def determine_final_domain(domain_info, zone_name, existing_records)
632
+ case domain_info[:type]
633
+ when :root
634
+ if existing_records.any?
635
+ puts "⚠️ 루트 도메인에 이미 λ ˆμ½”λ“œκ°€ μžˆμŠ΅λ‹ˆλ‹€. app.#{zone_name}을 μ‚¬μš©ν•©λ‹ˆλ‹€.".colorize(:yellow)
636
+ { name: "app.#{zone_name}", full_domain: "app.#{zone_name}" }
637
+ else
638
+ { name: zone_name, full_domain: zone_name }
639
+ end
640
+ when :subdomain
641
+ { name: domain_info[:domain], full_domain: domain_info[:domain] }
642
+ end
643
+ end
644
+
645
+ def create_dns_record(token, zone_id, name, type, content)
646
+ uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
647
+ http = Net::HTTP.new(uri.host, uri.port)
648
+ http.use_ssl = true
649
+
650
+ request = Net::HTTP::Post.new(uri)
651
+ request['Authorization'] = "Bearer #{token}"
652
+ request['Content-Type'] = 'application/json'
653
+
654
+ data = {
655
+ type: type,
656
+ name: name,
657
+ content: content,
658
+ ttl: 300
659
+ }
660
+
661
+ request.body = data.to_json
662
+ response = http.request(request)
663
+
664
+ if response.code == '200'
665
+ puts "βœ… DNS λ ˆμ½”λ“œκ°€ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
666
+ puts " #{name} β†’ #{content} (#{type} λ ˆμ½”λ“œ)".colorize(:gray)
667
+ else
668
+ puts "❌ DNS λ ˆμ½”λ“œ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: #{response.code}".colorize(:red)
669
+ puts response.body
670
+ exit 1
671
+ end
672
+ rescue => e
673
+ puts "❌ DNS λ ˆμ½”λ“œ 생성 쀑 였λ₯˜: #{e.message}".colorize(:red)
674
+ exit 1
675
+ end
676
+
677
+ def delete_dns_record(token, zone_id, record_id)
678
+ uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records/#{record_id}")
679
+ http = Net::HTTP.new(uri.host, uri.port)
680
+ http.use_ssl = true
681
+
682
+ request = Net::HTTP::Delete.new(uri)
683
+ request['Authorization'] = "Bearer #{token}"
684
+ request['Content-Type'] = 'application/json'
685
+
686
+ response = http.request(request)
687
+
688
+ if response.code == '200'
689
+ puts "βœ… κΈ°μ‘΄ DNS λ ˆμ½”λ“œκ°€ μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
690
+ else
691
+ puts "❌ DNS λ ˆμ½”λ“œ μ‚­μ œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: #{response.code}".colorize(:red)
692
+ puts response.body
693
+ exit 1
694
+ end
695
+ rescue => e
696
+ puts "❌ DNS λ ˆμ½”λ“œ μ‚­μ œ 쀑 였λ₯˜: #{e.message}".colorize(:red)
697
+ exit 1
698
+ end
699
+
700
+ def update_deploy_config(domain_info)
701
+ puts "\nπŸ“ 배포 섀정을 μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€...".colorize(:yellow)
702
+
703
+ config_file = "config/deploy.yml"
704
+
705
+ unless File.exist?(config_file)
706
+ puts "⚠️ config/deploy.yml 파일이 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:yellow)
707
+ return
708
+ end
709
+
710
+ content = File.read(config_file)
711
+
712
+ # proxy.host μ„€μ • μ—…λ°μ΄νŠΈ
713
+ if content.include?("proxy:")
714
+ content.gsub!(/(\s+host:\s+).*$/, "\\1#{@final_domain}")
715
+ else
716
+ # proxy μ„Ήμ…˜μ΄ μ—†μœΌλ©΄ μΆ”κ°€
717
+ proxy_config = "\n# Proxy configuration\nproxy:\n ssl: true\n host: #{@final_domain}\n"
718
+ content += proxy_config
719
+ end
720
+
721
+ # servers μ„€μ • μ—…λ°μ΄νŠΈ
722
+ if content.match?(/servers:\s*\n\s*web:\s*\n\s*-\s*/)
723
+ content.gsub!(/(\s*servers:\s*\n\s*web:\s*\n\s*-\s*)[\d.]+/, "\\1#{@server_info}")
724
+ end
725
+
726
+ # ssh user μ„€μ • μ—…λ°μ΄νŠΈ
727
+ if @ssh_user && @ssh_user != "root"
728
+ if content.match?(/^ssh:/)
729
+ # κΈ°μ‘΄ ssh μ„Ήμ…˜ μ—…λ°μ΄νŠΈ
730
+ content.gsub!(/^ssh:\s*\n\s*user:\s*\w+/, "ssh:\n user: #{@ssh_user}")
731
+ else
732
+ # ssh μ„Ήμ…˜ μΆ”κ°€ (accessories μ„Ήμ…˜ μ•žμ— μΆ”κ°€)
733
+ if content.match?(/^# Use accessory services/)
734
+ content.gsub!(/^# Use accessory services/, "# Use a different ssh user than root\nssh:\n user: #{@ssh_user}\n\n# Use accessory services")
735
+ else
736
+ # 파일 끝에 μΆ”κ°€
737
+ content += "\n# Use a different ssh user than root\nssh:\n user: #{@ssh_user}\n"
738
+ end
739
+ end
740
+ end
741
+
742
+ File.write(config_file, content)
743
+ puts "βœ… config/deploy.yml이 μ—…λ°μ΄νŠΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
744
+ puts " proxy.host: #{@final_domain}".colorize(:gray)
745
+ puts " servers.web: #{@server_info}".colorize(:gray)
746
+ puts " ssh.user: #{@ssh_user}".colorize(:gray) if @ssh_user && @ssh_user != "root"
747
+ end
748
+
749
+ def commit_cloudflare_changes(domain_info)
750
+ puts "\nπŸ“ 변경사항을 Git에 μ»€λ°‹ν•©λ‹ˆλ‹€...".colorize(:yellow)
751
+
752
+ # λ³€κ²½λœ 파일이 μžˆλŠ”μ§€ 확인
753
+ status_output = `git status --porcelain`.strip
754
+
755
+ if status_output.empty?
756
+ puts "ℹ️ 컀밋할 변경사항이 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:yellow)
757
+ return
758
+ end
759
+
760
+ # Git add
761
+ system("git add -A")
762
+
763
+ # Commit λ©”μ‹œμ§€ 생성
764
+ commit_message = "Configure Cloudflare DNS settings\n\n- Setup DNS for domain: #{domain_info[:domain]}\n- Configure server IP: #{domain_info[:server_ip]}\n- Update deployment configuration\n- Add proxy host settings\n\nπŸ€– Generated with Tayo"
765
+
766
+ # Commit μ‹€ν–‰
767
+ if system("git commit -m \"#{commit_message}\"")
768
+ puts "βœ… 변경사항이 μ„±κ³΅μ μœΌλ‘œ μ»€λ°‹λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
769
+
770
+ # GitHub에 ν‘Έμ‹œ
771
+ if system("git push", out: File::NULL, err: File::NULL)
772
+ puts "βœ… 변경사항이 GitHub에 ν‘Έμ‹œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
773
+ else
774
+ puts "⚠️ GitHub ν‘Έμ‹œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μˆ˜λ™μœΌλ‘œ 'git push'λ₯Ό μ‹€ν–‰ν•΄μ£Όμ„Έμš”.".colorize(:yellow)
775
+ end
776
+ else
777
+ puts "❌ Git 컀밋에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:red)
778
+ end
779
+ end
780
+ end
781
+ end
782
+ end
783
+ </file>
784
+
785
+ <file path="lib/tayo/commands/gh.rb">
786
+ # frozen_string_literal: true
787
+
788
+ require "colorize"
789
+ require "json"
790
+ require "git"
791
+ require "yaml"
792
+ require "tty-prompt"
793
+
794
+ module Tayo
795
+ module Commands
796
+ class Gh
797
+ def execute
798
+ puts "πŸš€ GitHub μ €μž₯μ†Œ 및 μ»¨ν…Œμ΄λ„ˆ λ ˆμ§€μŠ€νŠΈλ¦¬ 섀정을 μ‹œμž‘ν•©λ‹ˆλ‹€...".colorize(:green)
799
+
800
+ unless rails_project?
801
+ puts "❌ Rails ν”„λ‘œμ νŠΈκ°€ μ•„λ‹™λ‹ˆλ‹€. Rails ν”„λ‘œμ νŠΈ λ£¨νŠΈμ—μ„œ μ‹€ν–‰ν•΄μ£Όμ„Έμš”.".colorize(:red)
802
+ return
803
+ end
804
+
805
+ puts "\n[1/7] GitHub CLI μ„€μΉ˜ 확인".colorize(:blue)
806
+ check_github_cli
807
+
808
+ puts "\n[2/7] GitHub 둜그인 확인".colorize(:blue)
809
+ check_github_auth
810
+
811
+ puts "\n[3/7] μ»¨ν…Œμ΄λ„ˆ λ ˆμ§€μŠ€νŠΈλ¦¬ κΆŒν•œ 확인".colorize(:blue)
812
+ check_container_registry_permission
813
+
814
+ puts "\n[4/7] Git μ €μž₯μ†Œ μ΄ˆκΈ°ν™”".colorize(:blue)
815
+ init_git_repo
816
+
817
+ puts "\n[5/7] GitHub μ €μž₯μ†Œ 생성".colorize(:blue)
818
+ create_github_repository
819
+
820
+ puts "\n[6/7] μ»¨ν…Œμ΄λ„ˆ λ ˆμ§€μŠ€νŠΈλ¦¬ μ„€μ •".colorize(:blue)
821
+ create_container_registry
822
+
823
+ puts "\n[7/7] 배포 μ„€μ • 파일 생성".colorize(:blue)
824
+ create_deploy_config
825
+
826
+ puts "\nπŸŽ‰ λͺ¨λ“  섀정이 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€!".colorize(:green)
827
+ puts "\nλ‹€μŒ 정보가 μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€:".colorize(:yellow)
828
+ puts "β€’ GitHub μ €μž₯μ†Œ: https://github.com/#{@username}/#{@repo_name}".colorize(:cyan)
829
+ puts "β€’ Container Registry: #{@registry_url}".colorize(:cyan)
830
+ puts "β€’ 배포 μ„€μ •: config/deploy.yml".colorize(:cyan)
831
+
832
+ # 변경사항 컀밋
833
+ commit_github_changes
834
+ end
835
+
836
+ private
837
+
838
+ def rails_project?
839
+ File.exist?("Gemfile") && File.exist?("config/application.rb")
840
+ end
841
+
842
+ def check_github_cli
843
+ if system("gh --version", out: File::NULL, err: File::NULL)
844
+ puts "βœ… GitHub CLIκ°€ 이미 μ„€μΉ˜λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
845
+ else
846
+ puts "πŸ“¦ GitHub CLIλ₯Ό μ„€μΉ˜ν•©λ‹ˆλ‹€...".colorize(:yellow)
847
+ system("brew install gh")
848
+ puts "βœ… GitHub CLI μ„€μΉ˜ μ™„λ£Œ.".colorize(:green)
849
+ end
850
+ end
851
+
852
+ def check_github_auth
853
+ auth_status = `gh auth status 2>&1`
854
+
855
+ unless $?.success?
856
+ puts "πŸ”‘ GitHub 둜그인이 ν•„μš”ν•©λ‹ˆλ‹€.".colorize(:yellow)
857
+ puts "λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μ‹€ν–‰ν•˜μ—¬ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”:".colorize(:yellow)
858
+ puts "gh auth login".colorize(:cyan)
859
+ exit 1
860
+ end
861
+
862
+ # 토큰 만료 확인
863
+ if auth_status.include?("Token has expired") || auth_status.include?("authentication failed")
864
+ puts "⚠️ GitHub 토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
865
+ puts "λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”:".colorize(:yellow)
866
+ puts "gh auth login".colorize(:cyan)
867
+ exit 1
868
+ end
869
+
870
+ puts "βœ… GitHub에 λ‘œκ·ΈμΈλ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
871
+ end
872
+
873
+ def check_container_registry_permission
874
+ scopes = `gh auth status -t 2>&1`
875
+
876
+ # 토큰 만료 확인 (κΆŒν•œ 체크 μ‹œμ—λ„)
877
+ if scopes.include?("Token has expired") || scopes.include?("authentication failed")
878
+ puts "⚠️ GitHub 토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
879
+ puts "λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”:".colorize(:yellow)
880
+ puts "gh auth login".colorize(:cyan)
881
+ exit 1
882
+ end
883
+
884
+ unless scopes.include?("write:packages") || scopes.include?("admin:packages")
885
+ puts "⚠️ μ»¨ν…Œμ΄λ„ˆ λ ˆμ§€μŠ€νŠΈλ¦¬ κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.".colorize(:yellow)
886
+ puts "\nTayoκ°€ 정상 μž‘λ™ν•˜κΈ° μœ„ν•΄ λ‹€μŒ κΆŒν•œλ“€μ΄ ν•„μš”ν•©λ‹ˆλ‹€:".colorize(:yellow)
887
+ puts "β€’ repo - GitHub μ €μž₯μ†Œ 생성 및 관리".colorize(:yellow)
888
+ puts "β€’ read:org - 쑰직 정보 읽기".colorize(:yellow)
889
+ puts "β€’ write:packages - Docker 이미지λ₯Ό Container Registry에 ν‘Έμ‹œ".colorize(:yellow)
890
+ puts "\n토큰 생성 νŽ˜μ΄μ§€λ₯Ό μ—½λ‹ˆλ‹€...".colorize(:cyan)
891
+
892
+ project_name = File.basename(Dir.pwd)
893
+ token_description = "Tayo%20-%20#{project_name}"
894
+ token_url = "https://github.com/settings/tokens/new?scopes=repo,read:org,write:packages&description=#{token_description}"
895
+ system("open '#{token_url}'")
896
+
897
+ puts "\nβœ… λΈŒλΌμš°μ €μ—μ„œ GitHub 토큰 생성 νŽ˜μ΄μ§€κ°€ μ—΄λ ΈμŠ΅λ‹ˆλ‹€.".colorize(:green)
898
+ puts "πŸ“Œ ν•„μš”ν•œ κΆŒν•œλ“€μ΄ 이미 μ²΄ν¬λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€:".colorize(:green)
899
+ puts " β€’ repo - μ €μž₯μ†Œ 생성 및 관리".colorize(:gray)
900
+ puts " β€’ read:org - 쑰직 정보 읽기".colorize(:gray)
901
+ puts " β€’ write:packages - Container Registry μ ‘κ·Ό".colorize(:gray)
902
+
903
+ puts "\nλ‹€μŒ 단계λ₯Ό λ”°λΌμ£Όμ„Έμš”:".colorize(:yellow)
904
+ puts "1. νŽ˜μ΄μ§€ ν•˜λ‹¨μ˜ 'Generate token' λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”".colorize(:cyan)
905
+ puts "2. μƒμ„±λœ 토큰을 λ³΅μ‚¬ν•˜μ„Έμš”".colorize(:cyan)
906
+ puts "3. μ•„λž˜μ— 토큰을 λΆ™μ—¬λ„£μœΌμ„Έμš”:".colorize(:cyan)
907
+
908
+ print "\n토큰 μž…λ ₯: ".colorize(:yellow)
909
+ token = STDIN.gets.chomp
910
+
911
+ if token.empty?
912
+ puts "❌ 토큰이 μž…λ ₯λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.".colorize(:red)
913
+ exit 1
914
+ end
915
+
916
+ # 토큰을 μž„μ‹œ νŒŒμΌμ— μ €μž₯ν•˜κ³  gh auth login μ‹€ν–‰
917
+ require 'tempfile'
918
+ Tempfile.create('github_token') do |f|
919
+ f.write(token)
920
+ f.flush
921
+
922
+ puts "\nπŸ” GitHub에 둜그인 쀑...".colorize(:yellow)
923
+ if system("gh auth login --with-token < #{f.path}")
924
+ puts "βœ… GitHub λ‘œκ·ΈμΈμ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€!".colorize(:green)
925
+ puts "\nλ‹€μ‹œ 'tayo gh' λͺ…령을 μ‹€ν–‰ν•΄μ£Όμ„Έμš”.".colorize(:cyan)
926
+ else
927
+ puts "❌ GitHub λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:red)
928
+ end
929
+ end
930
+
931
+ exit 0
932
+ end
933
+
934
+ puts "βœ… μ»¨ν…Œμ΄λ„ˆ λ ˆμ§€μŠ€νŠΈλ¦¬ κΆŒν•œμ΄ ν™•μΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
935
+ end
936
+
937
+ def init_git_repo
938
+ unless Dir.exist?(".git")
939
+ Git.init(".")
940
+ puts "βœ… Git μ €μž₯μ†Œλ₯Ό μ΄ˆκΈ°ν™”ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
941
+ else
942
+ puts "ℹ️ Git μ €μž₯μ†Œκ°€ 이미 μ΄ˆκΈ°ν™”λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
943
+ end
944
+
945
+ git = Git.open(".")
946
+
947
+ # HEAD 컀밋이 μžˆλŠ”μ§€ 확인
948
+ has_commits = begin
949
+ git.log.count > 0
950
+ rescue Git::GitExecuteError
951
+ false
952
+ end
953
+
954
+ # git status둜 변경사항 확인 (HEADκ°€ μ—†μœΌλ©΄ λ‹€λ₯Έ 방법 μ‚¬μš©)
955
+ has_changes = if has_commits
956
+ git.status.untracked.any? || git.status.changed.any?
957
+ else
958
+ # HEADκ°€ 없을 λ•ŒλŠ” μ›Œν‚Ή 디렉토리에 파일이 μžˆλŠ”μ§€ 확인
959
+ Dir.glob("*", File::FNM_DOTMATCH).reject { |f| f == "." || f == ".." || f == ".git" }.any?
960
+ end
961
+
962
+ if has_changes
963
+ git.add(all: true)
964
+ git.commit("init")
965
+ puts "βœ… 초기 컀밋을 μƒμ„±ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
966
+ else
967
+ puts "ℹ️ 컀밋할 변경사항이 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:yellow)
968
+ end
969
+ end
970
+
971
+ def create_github_repository
972
+ repo_name = File.basename(Dir.pwd)
973
+ username = `gh api user -q .login`.strip
974
+
975
+ # 쑰직 λͺ©λ‘ κ°€μ Έμ˜€κΈ°
976
+ orgs_json = `gh api user/orgs -q '.[].login' 2>/dev/null`
977
+ orgs = orgs_json.strip.split("\n").reject(&:empty?)
978
+
979
+ owner = username
980
+
981
+ if orgs.any?
982
+ prompt = TTY::Prompt.new
983
+ choices = ["#{username} (개인 계정)"] + orgs.map { |org| "#{org} (쑰직)" }
984
+
985
+ selection = prompt.select("🏒 μ €μž₯μ†Œλ₯Ό 생성할 μœ„μΉ˜λ₯Ό μ„ νƒν•˜μ„Έμš”:", choices)
986
+
987
+ if selection != "#{username} (개인 계정)"
988
+ owner = selection.split(" ").first
989
+ end
990
+ end
991
+
992
+ # μ €μž₯μ†Œ 쑴재 μ—¬λΆ€ 확인
993
+ repo_exists = system("gh repo view #{owner}/#{repo_name}", out: File::NULL, err: File::NULL)
994
+
995
+ if repo_exists
996
+ puts "ℹ️ GitHub μ €μž₯μ†Œκ°€ 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€: https://github.com/#{owner}/#{repo_name}".colorize(:yellow)
997
+ @repo_name = repo_name
998
+ @username = owner
999
+ else
1000
+ create_cmd = if owner == username
1001
+ "gh repo create #{repo_name} --private --source=. --remote=origin --push"
1002
+ else
1003
+ "gh repo create #{owner}/#{repo_name} --private --source=. --remote=origin --push"
1004
+ end
1005
+
1006
+ result = system(create_cmd)
1007
+
1008
+ if result
1009
+ puts "βœ… GitHub μ €μž₯μ†Œλ₯Ό μƒμ„±ν–ˆμŠ΅λ‹ˆλ‹€: https://github.com/#{owner}/#{repo_name}".colorize(:green)
1010
+ @repo_name = repo_name
1011
+ @username = owner
1012
+ else
1013
+ puts "❌ GitHub μ €μž₯μ†Œ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:red)
1014
+ exit 1
1015
+ end
1016
+ end
1017
+ end
1018
+
1019
+ def create_container_registry
1020
+ # Docker 이미지 νƒœκ·ΈλŠ” μ†Œλ¬Έμžμ—¬μ•Ό 함
1021
+ registry_url = "ghcr.io/#{@username.downcase}/#{@repo_name.downcase}"
1022
+ @registry_url = registry_url
1023
+
1024
+ puts "βœ… μ»¨ν…Œμ΄λ„ˆ λ ˆμ§€μŠ€νŠΈλ¦¬κ°€ μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1025
+ puts " URL: #{registry_url}".colorize(:gray)
1026
+ puts " ℹ️ μ»¨ν…Œμ΄λ„ˆ λ ˆμ§€μŠ€νŠΈλ¦¬λŠ” 첫 이미지 ν‘Έμ‹œ μ‹œ μžλ™μœΌλ‘œ μƒμ„±λ©λ‹ˆλ‹€.".colorize(:gray)
1027
+
1028
+ # Docker둜 GitHub Container Registry에 둜그인
1029
+ puts "\n🐳 Docker둜 GitHub Container Registry에 λ‘œκ·ΈμΈν•©λ‹ˆλ‹€...".colorize(:yellow)
1030
+
1031
+ # ν˜„μž¬ GitHub 토큰 κ°€μ Έμ˜€κΈ°
1032
+ token = `gh auth token`.strip
1033
+
1034
+ if token.empty?
1035
+ puts "❌ GitHub 토큰을 κ°€μ Έμ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:red)
1036
+ return
1037
+ end
1038
+
1039
+ # Docker login μ‹€ν–‰
1040
+ login_cmd = "echo #{token} | docker login ghcr.io -u #{@username} --password-stdin"
1041
+
1042
+ if system(login_cmd)
1043
+ puts "βœ… Docker λ‘œκ·ΈμΈμ— μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€!".colorize(:green)
1044
+ puts " Registry: ghcr.io".colorize(:gray)
1045
+ puts " Username: #{@username}".colorize(:gray)
1046
+ else
1047
+ puts "❌ Docker λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:red)
1048
+ puts " μˆ˜λ™μœΌλ‘œ λ‹€μŒ λͺ…령을 μ‹€ν–‰ν•΄μ£Όμ„Έμš”:".colorize(:yellow)
1049
+ puts " docker login ghcr.io".colorize(:cyan)
1050
+ end
1051
+ end
1052
+
1053
+ def create_deploy_config
1054
+ config_dir = "config"
1055
+ Dir.mkdir(config_dir) unless Dir.exist?(config_dir)
1056
+
1057
+ if File.exist?("config/deploy.yml")
1058
+ puts "ℹ️ κΈ°μ‘΄ config/deploy.yml νŒŒμΌμ„ μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€.".colorize(:yellow)
1059
+ update_kamal_config
1060
+ else
1061
+ puts "βœ… config/deploy.yml νŒŒμΌμ„ μƒμ„±ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1062
+ create_tayo_config
1063
+ end
1064
+ end
1065
+
1066
+ private
1067
+
1068
+ def update_kamal_config
1069
+ content = File.read("config/deploy.yml")
1070
+
1071
+ # 이미지 μ„€μ • μ—…λ°μ΄νŠΈ (ghcr.io 쀑볡 제거)
1072
+ # @registry_url은 이미 ghcr.ioλ₯Ό ν¬ν•¨ν•˜κ³  μžˆμœΌλ―€λ‘œ, κ·ΈλŒ€λ‘œ μ‚¬μš©
1073
+ content.gsub!(/^image:\s+.*$/, "image: #{@registry_url}")
1074
+
1075
+ # registry μ„Ήμ…˜ μ—…λ°μ΄νŠΈ
1076
+ if content.include?("registry:")
1077
+ # κΈ°μ‘΄ registry μ„Ήμ…˜ μˆ˜μ •
1078
+ # server 라인이 μ£Όμ„μ²˜λ¦¬λ˜μ–΄ μžˆλŠ”μ§€ 확인
1079
+ if content.match?(/^\s*#\s*server:/)
1080
+ content.gsub!(/^\s*#\s*server:\s*.*$/, " server: ghcr.io")
1081
+ elsif content.match?(/^\s*server:/)
1082
+ content.gsub!(/^\s*server:\s*.*$/, " server: ghcr.io")
1083
+ else
1084
+ # server 라인이 μ—†μœΌλ©΄ username μœ„μ— μΆ”κ°€
1085
+ content.gsub!(/(\s*username:\s+)/, " server: ghcr.io\n\\1")
1086
+ end
1087
+ # username도 μ†Œλ¬Έμžλ‘œ λ³€ν™˜
1088
+ content.gsub!(/^\s*username:\s+.*$/, " username: #{@username.downcase}")
1089
+ else
1090
+ # registry μ„Ήμ…˜ μΆ”κ°€
1091
+ registry_config = "\n# Container registry configuration\nregistry:\n server: ghcr.io\n username: #{@username.downcase}\n password:\n - KAMAL_REGISTRY_PASSWORD\n"
1092
+ content.gsub!(/^# Credentials for your image host\.\nregistry:.*?^$/m, registry_config)
1093
+ end
1094
+
1095
+ File.write("config/deploy.yml", content)
1096
+
1097
+ # GitHub 토큰을 Kamal secrets νŒŒμΌμ— μ„€μ •
1098
+ setup_kamal_secrets
1099
+
1100
+ puts "βœ… Container Registry 섀정이 μ—…λ°μ΄νŠΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€:".colorize(:green)
1101
+ puts " β€’ 이미지: #{@registry_url}".colorize(:gray)
1102
+ puts " β€’ λ ˆμ§€μŠ€νŠΈλ¦¬ μ„œλ²„: ghcr.io".colorize(:gray)
1103
+ puts " β€’ μ‚¬μš©μžλͺ…: #{@username}".colorize(:gray)
1104
+ end
1105
+
1106
+ def setup_kamal_secrets
1107
+ # .kamal 디렉토리 생성
1108
+ Dir.mkdir(".kamal") unless Dir.exist?(".kamal")
1109
+
1110
+ # ν˜„μž¬ GitHub 토큰 κ°€μ Έμ˜€κΈ°
1111
+ token_output = `gh auth token 2>/dev/null`
1112
+
1113
+ if $?.success? && !token_output.strip.empty?
1114
+ token = token_output.strip
1115
+ secrets_file = ".kamal/secrets"
1116
+
1117
+ # κΈ°μ‘΄ secrets 파일 읽기 (μžˆλ‹€λ©΄)
1118
+ existing_content = File.exist?(secrets_file) ? File.read(secrets_file) : ""
1119
+
1120
+ # KAMAL_REGISTRY_PASSWORDκ°€ 이미 μžˆλŠ”μ§€ 확인
1121
+ if existing_content.include?("KAMAL_REGISTRY_PASSWORD")
1122
+ # κΈ°μ‘΄ κ°’ μ—…λ°μ΄νŠΈ
1123
+ updated_content = existing_content.gsub(/^KAMAL_REGISTRY_PASSWORD=.*$/, "KAMAL_REGISTRY_PASSWORD=#{token}")
1124
+ else
1125
+ # μƒˆλ‘œ μΆ”κ°€
1126
+ updated_content = existing_content.empty? ? "KAMAL_REGISTRY_PASSWORD=#{token}\n" : "#{existing_content.chomp}\nKAMAL_REGISTRY_PASSWORD=#{token}\n"
1127
+ end
1128
+
1129
+ File.write(secrets_file, updated_content)
1130
+ puts "βœ… GitHub 토큰이 .kamal/secrets에 μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1131
+
1132
+ # .gitignore에 secrets 파일 μΆ”κ°€
1133
+ add_to_gitignore(".kamal/secrets")
1134
+ else
1135
+ puts "⚠️ GitHub 토큰을 κ°€μ Έμ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€. μˆ˜λ™μœΌλ‘œ μ„€μ •ν•΄μ£Όμ„Έμš”:".colorize(:yellow)
1136
+ puts " echo 'KAMAL_REGISTRY_PASSWORD=your_github_token' >> .kamal/secrets".colorize(:cyan)
1137
+ end
1138
+ end
1139
+
1140
+ def add_to_gitignore(file_path)
1141
+ gitignore_file = ".gitignore"
1142
+
1143
+ if File.exist?(gitignore_file)
1144
+ content = File.read(gitignore_file)
1145
+ unless content.include?(file_path)
1146
+ File.write(gitignore_file, "#{content.chomp}\n#{file_path}\n")
1147
+ puts "βœ… .gitignore에 #{file_path}λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1148
+ end
1149
+ else
1150
+ File.write(gitignore_file, "#{file_path}\n")
1151
+ puts "βœ… .gitignore νŒŒμΌμ„ μƒμ„±ν•˜κ³  #{file_path}λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1152
+ end
1153
+ end
1154
+
1155
+ def create_tayo_config
1156
+ deploy_config = {
1157
+ "production" => {
1158
+ "registry" => @registry_url,
1159
+ "repository" => "https://github.com/#{@username}/#{@repo_name}",
1160
+ "server" => {
1161
+ "host" => "your-home-server.local",
1162
+ "user" => "deploy",
1163
+ "port" => 22
1164
+ },
1165
+ "environment" => {
1166
+ "RAILS_ENV" => "production",
1167
+ "RAILS_MASTER_KEY" => "your-master-key"
1168
+ }
1169
+ }
1170
+ }
1171
+
1172
+ File.write("config/deploy.yml", deploy_config.to_yaml)
1173
+ puts " ⚠️ μ„œλ²„ 정보와 ν™˜κ²½ λ³€μˆ˜λ₯Ό μ„€μ •ν•΄μ£Όμ„Έμš”.".colorize(:yellow)
1174
+ end
1175
+
1176
+ def commit_github_changes
1177
+ puts "\nπŸ“ 변경사항을 Git에 μ»€λ°‹ν•©λ‹ˆλ‹€...".colorize(:yellow)
1178
+
1179
+ # λ³€κ²½λœ 파일이 μžˆλŠ”μ§€ 확인
1180
+ status_output = `git status --porcelain`.strip
1181
+
1182
+ if status_output.empty?
1183
+ puts "ℹ️ 컀밋할 변경사항이 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:yellow)
1184
+ return
1185
+ end
1186
+
1187
+ # Git add
1188
+ system("git add -A")
1189
+
1190
+ # Commit λ©”μ‹œμ§€ 생성
1191
+ commit_message = "Add GitHub Container Registry configuration\n\n- Setup GitHub repository: #{@repo_name}\n- Configure container registry: #{@registry_url}\n- Add deployment configuration files\n- Setup environment variables\n\nπŸ€– Generated with Tayo"
1192
+
1193
+ # Commit μ‹€ν–‰
1194
+ if system("git commit -m \"#{commit_message}\"")
1195
+ puts "βœ… 변경사항이 μ„±κ³΅μ μœΌλ‘œ μ»€λ°‹λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1196
+
1197
+ # GitHub에 ν‘Έμ‹œ
1198
+ if system("git push", out: File::NULL, err: File::NULL)
1199
+ puts "βœ… 변경사항이 GitHub에 ν‘Έμ‹œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1200
+ else
1201
+ puts "⚠️ GitHub ν‘Έμ‹œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μˆ˜λ™μœΌλ‘œ 'git push'λ₯Ό μ‹€ν–‰ν•΄μ£Όμ„Έμš”.".colorize(:yellow)
1202
+ end
1203
+ else
1204
+ puts "❌ Git 컀밋에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:red)
1205
+ end
1206
+ end
1207
+ end
1208
+ end
1209
+ end
1210
+ </file>
1211
+
1212
+ <file path="tayo.gemspec">
1213
+ # frozen_string_literal: true
1214
+
1215
+ require_relative "lib/tayo/version"
1216
+
1217
+ Gem::Specification.new do |spec|
1218
+ spec.name = "tayo"
1219
+ spec.version = Tayo::VERSION
1220
+ spec.authors = ["이원섭wonsup Lee/Alfonso"]
1221
+ spec.email = ["onesup.lee@gmail.com"]
1222
+
1223
+ spec.summary = "Rails deployment tool for home servers"
1224
+ spec.description = "Tayo is a deployment tool for Rails applications to home servers using GitHub Container Registry and Cloudflare CLI."
1225
+ spec.homepage = "https://github.com/TeamMilestone/tayo"
1226
+ spec.required_ruby_version = ">= 3.1.0"
1227
+
1228
+ spec.metadata["homepage_uri"] = spec.homepage
1229
+ spec.metadata["source_code_uri"] = "https://github.com/TeamMilestone/tayo"
1230
+ spec.metadata["changelog_uri"] = "https://github.com/TeamMilestone/tayo/blob/main/CHANGELOG.md"
1231
+
1232
+ # Specify which files should be added to the gem when it is released.
1233
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
1234
+ gemspec = File.basename(__FILE__)
1235
+ spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
1236
+ ls.readlines("\x0", chomp: true).reject do |f|
1237
+ (f == gemspec) ||
1238
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git appveyor Gemfile])
1239
+ end
1240
+ end
1241
+ spec.bindir = "exe"
1242
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
1243
+ spec.require_paths = ["lib"]
1244
+
1245
+ spec.add_dependency "thor", "~> 1.3"
1246
+ spec.add_dependency "git", "~> 1.18"
1247
+ spec.add_dependency "colorize", "~> 1.1"
1248
+ spec.add_dependency "tty-prompt", "~> 0.23"
1249
+
1250
+ # For more information and examples about making a new gem, check out our
1251
+ # guide at: https://bundler.io/guides/creating_gem.html
1252
+ end
1253
+ </file>
1254
+
1255
+ <file path="lib/tayo/commands/init.rb">
1256
+ # frozen_string_literal: true
1257
+
1258
+ require "colorize"
1259
+
1260
+ module Tayo
1261
+ module Commands
1262
+ class Init
1263
+ def execute
1264
+ puts "🏠 Tayo μ΄ˆκΈ°ν™”λ₯Ό μ‹œμž‘ν•©λ‹ˆλ‹€...".colorize(:green)
1265
+
1266
+ unless rails_project?
1267
+ puts "❌ Rails ν”„λ‘œμ νŠΈκ°€ μ•„λ‹™λ‹ˆλ‹€. Rails ν”„λ‘œμ νŠΈ λ£¨νŠΈμ—μ„œ μ‹€ν–‰ν•΄μ£Όμ„Έμš”.".colorize(:red)
1268
+ return
1269
+ end
1270
+
1271
+ check_orbstack
1272
+ add_to_gemfile
1273
+ bundle_install
1274
+ add_linux_platform
1275
+ create_welcome_page
1276
+ commit_changes
1277
+ clear_docker_cache
1278
+
1279
+ puts "βœ… Tayoκ°€ μ„±κ³΅μ μœΌλ‘œ μ„€μ •λ˜μ—ˆμŠ΅λ‹ˆλ‹€!".colorize(:green)
1280
+ end
1281
+
1282
+ private
1283
+
1284
+ def rails_project?
1285
+ File.exist?("Gemfile") && File.exist?("config/application.rb")
1286
+ end
1287
+
1288
+ def check_orbstack
1289
+ puts "🐳 OrbStack μƒνƒœλ₯Ό ν™•μΈν•©λ‹ˆλ‹€...".colorize(:yellow)
1290
+
1291
+ # OrbStack μ‹€ν–‰ μƒνƒœ 확인
1292
+ orbstack_running = system("pgrep -x OrbStack > /dev/null 2>&1")
1293
+
1294
+ if orbstack_running
1295
+ puts "βœ… OrbStack이 μ‹€ν–‰ μ€‘μž…λ‹ˆλ‹€.".colorize(:green)
1296
+ else
1297
+ puts "πŸš€ OrbStack을 μ‹œμž‘ν•©λ‹ˆλ‹€...".colorize(:yellow)
1298
+
1299
+ # OrbStack μ‹€ν–‰
1300
+ if system("open -a OrbStack")
1301
+ puts "βœ… OrbStack이 μ‹œμž‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1302
+
1303
+ # OrbStack이 μ™„μ „νžˆ μ‹œμž‘λ  λ•ŒκΉŒμ§€ μž μ‹œ λŒ€κΈ°
1304
+ print "Docker μ„œλΉ„μŠ€κ°€ 쀀비될 λ•ŒκΉŒμ§€ λŒ€κΈ° 쀑".colorize(:yellow)
1305
+ 5.times do
1306
+ sleep 1
1307
+ print ".".colorize(:yellow)
1308
+ end
1309
+ puts ""
1310
+
1311
+ # Dockerκ°€ μ€€λΉ„λ˜μ—ˆλŠ”μ§€ 확인
1312
+ if system("docker ps > /dev/null 2>&1")
1313
+ puts "βœ… Dockerκ°€ μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1314
+ else
1315
+ puts "⚠️ Dockerκ°€ 아직 μ€€λΉ„λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.".colorize(:yellow)
1316
+ end
1317
+ else
1318
+ puts "❌ OrbStack을 μ‹œμž‘ν•  수 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:red)
1319
+ puts "OrbStack이 μ„€μΉ˜λ˜μ–΄ μžˆλŠ”μ§€ ν™•μΈν•΄μ£Όμ„Έμš”.".colorize(:yellow)
1320
+ puts "https://orbstack.dev μ—μ„œ λ‹€μš΄λ‘œλ“œν•  수 μžˆμŠ΅λ‹ˆλ‹€.".colorize(:cyan)
1321
+ end
1322
+ end
1323
+ end
1324
+
1325
+ def add_to_gemfile
1326
+ gemfile_content = File.read("Gemfile")
1327
+
1328
+ if gemfile_content.include?("tayo")
1329
+ puts "ℹ️ Tayoκ°€ 이미 Gemfile에 μžˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
1330
+ return
1331
+ end
1332
+
1333
+ development_group = gemfile_content.match(/group :development do\n(.*?)\nend/m)
1334
+
1335
+ if development_group
1336
+ updated_content = gemfile_content.sub(
1337
+ /group :development do\n/,
1338
+ "group :development do\n gem 'tayo'\n"
1339
+ )
1340
+ else
1341
+ updated_content = gemfile_content + "\n\ngroup :development do\n gem 'tayo'\nend\n"
1342
+ end
1343
+
1344
+ File.write("Gemfile", updated_content)
1345
+ puts "βœ… Gemfile에 Tayoλ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1346
+ end
1347
+
1348
+ def bundle_install
1349
+ puts "πŸ“¦ bundle install을 μ‹€ν–‰ν•©λ‹ˆλ‹€...".colorize(:yellow)
1350
+ system("bundle install")
1351
+ end
1352
+
1353
+ def add_linux_platform
1354
+ puts "🐧 Linux ν”Œλž«νΌμ„ ν™•μΈν•˜κ³  μΆ”κ°€ν•©λ‹ˆλ‹€...".colorize(:yellow)
1355
+
1356
+ # Gemfile.lock 파일 확인
1357
+ unless File.exist?("Gemfile.lock")
1358
+ puts "⚠️ Gemfile.lock 파일이 μ—†μŠ΅λ‹ˆλ‹€. bundle install을 λ¨Όμ € μ‹€ν–‰ν•΄μ£Όμ„Έμš”.".colorize(:yellow)
1359
+ return
1360
+ end
1361
+
1362
+ gemfile_lock_content = File.read("Gemfile.lock")
1363
+ platforms_needed = []
1364
+
1365
+ # ν•„μš”ν•œ ν”Œλž«νΌ 확인
1366
+ unless gemfile_lock_content.include?("x86_64-linux")
1367
+ platforms_needed << "x86_64-linux"
1368
+ end
1369
+
1370
+ unless gemfile_lock_content.include?("aarch64-linux")
1371
+ platforms_needed << "aarch64-linux"
1372
+ end
1373
+
1374
+ if platforms_needed.empty?
1375
+ puts "βœ… ν•„μš”ν•œ Linux ν”Œλž«νΌμ΄ 이미 μΆ”κ°€λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1376
+ return
1377
+ end
1378
+
1379
+ # ν”Œλž«νΌ μΆ”κ°€
1380
+ platforms_needed.each do |platform|
1381
+ puts "πŸ“¦ #{platform} ν”Œλž«νΌμ„ μΆ”κ°€ν•©λ‹ˆλ‹€...".colorize(:yellow)
1382
+ if system("bundle lock --add-platform #{platform}")
1383
+ puts "βœ… #{platform} ν”Œλž«νΌμ΄ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1384
+ else
1385
+ puts "❌ #{platform} ν”Œλž«νΌ 좔가에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:red)
1386
+ end
1387
+ end
1388
+
1389
+ # Dockerfile 확인 및 생성
1390
+ ensure_dockerfile_exists
1391
+ end
1392
+
1393
+ def ensure_dockerfile_exists
1394
+ dockerfile_created = false
1395
+
1396
+ unless File.exist?("Dockerfile")
1397
+ puts "🐳 Dockerfile이 μ—†μŠ΅λ‹ˆλ‹€. κΈ°λ³Έ Dockerfile을 μƒμ„±ν•©λ‹ˆλ‹€...".colorize(:yellow)
1398
+
1399
+ # Rails 7의 κΈ°λ³Έ Dockerfile 생성
1400
+ if system("rails app:update:bin")
1401
+ system("./bin/rails generate dockerfile")
1402
+ puts "βœ… Dockerfile이 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1403
+ dockerfile_created = true
1404
+ else
1405
+ puts "⚠️ Dockerfile 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μˆ˜λ™μœΌλ‘œ μƒμ„±ν•΄μ£Όμ„Έμš”.".colorize(:yellow)
1406
+ puts " λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μ‹€ν–‰ν•˜μ„Έμš”: ./bin/rails generate dockerfile".colorize(:cyan)
1407
+ return
1408
+ end
1409
+ else
1410
+ puts "βœ… Dockerfile이 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€.".colorize(:green)
1411
+ end
1412
+
1413
+ # Dockerfileμ—μ„œ bootsnap precompile λΆ€λΆ„ 제거 (λΉŒλ“œ 문제 ν•΄κ²°)
1414
+ fix_dockerfile_bootsnap_issue
1415
+ end
1416
+
1417
+ def fix_dockerfile_bootsnap_issue
1418
+ return unless File.exist?("Dockerfile")
1419
+
1420
+ dockerfile_content = File.read("Dockerfile")
1421
+ original_content = dockerfile_content.dup
1422
+
1423
+ # Dockerfile을 λΌμΈλ³„λ‘œ 처리
1424
+ lines = dockerfile_content.split("\n")
1425
+ filtered_lines = []
1426
+ skip_next = false
1427
+
1428
+ lines.each_with_index do |line, index|
1429
+ # bootsnap κ΄€λ ¨ 주석 μ°ΎκΈ°
1430
+ if line.match?(/^\s*#.*bootsnap.*faster boot times/i)
1431
+ skip_next = true # λ‹€μŒ 라인도 μ œκ±°ν•  μ€€λΉ„
1432
+ next # 이 라인은 제거
1433
+ end
1434
+
1435
+ # bootsnap precompile RUN λͺ…λ Ή μ°ΎκΈ°
1436
+ if line.match?(/^\s*RUN.*bootsnap\s+precompile/i)
1437
+ skip_next = false # 리셋
1438
+ next # 이 라인은 제거
1439
+ end
1440
+
1441
+ # skip_nextκ°€ true이고 ν˜„μž¬ 라인이 RUN bootsnap이면 제거
1442
+ if skip_next && line.match?(/^\s*RUN.*bootsnap/i)
1443
+ skip_next = false
1444
+ next
1445
+ end
1446
+
1447
+ skip_next = false
1448
+ filtered_lines << line
1449
+ end
1450
+
1451
+ new_content = filtered_lines.join("\n") + "\n"
1452
+
1453
+ if new_content != original_content
1454
+ File.write("Dockerfile", new_content)
1455
+ puts "βœ… Dockerfileμ—μ„œ bootsnap precompile 뢀뢄을 μ œκ±°ν–ˆμŠ΅λ‹ˆλ‹€. (λΉŒλ“œ 문제 ν•΄κ²°)".colorize(:green)
1456
+ puts " - μ΄λŠ” μ•Œλ €μ§„ λΉŒλ“œ 문제λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•¨μž…λ‹ˆλ‹€.".colorize(:gray)
1457
+ end
1458
+ end
1459
+
1460
+ def create_welcome_page
1461
+ # Welcome μ»¨νŠΈλ‘€λŸ¬κ°€ 이미 μžˆλŠ”μ§€ 확인
1462
+ if File.exist?("app/controllers/welcome_controller.rb")
1463
+ puts "ℹ️ Welcome νŽ˜μ΄μ§€κ°€ 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€.".colorize(:yellow)
1464
+ @welcome_page_created = false
1465
+ return
1466
+ end
1467
+
1468
+ puts "🎨 Welcome νŽ˜μ΄μ§€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€...".colorize(:yellow)
1469
+
1470
+ # Welcome 컨트둀러 생성
1471
+ system("rails generate controller Welcome index --skip-routes --no-helper --no-assets")
1472
+
1473
+ # ν”„λ‘œμ νŠΈ 이름 κ°€μ Έμ˜€κΈ°
1474
+ project_name = File.basename(Dir.pwd).gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
1475
+
1476
+ # Welcome νŽ˜μ΄μ§€ HTML 생성
1477
+ welcome_html = <<~HTML
1478
+ <!DOCTYPE html>
1479
+ <html>
1480
+ <head>
1481
+ <title>#{project_name} - Welcome</title>
1482
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1483
+ <style>
1484
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1485
+ body {
1486
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1487
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1488
+ min-height: 100vh;
1489
+ display: flex;
1490
+ align-items: center;
1491
+ justify-content: center;
1492
+ color: white;
1493
+ }
1494
+ .container {
1495
+ text-align: center;
1496
+ padding: 2rem;
1497
+ max-width: 800px;
1498
+ animation: fadeIn 1s ease-out;
1499
+ }
1500
+ h1 {
1501
+ font-size: 4rem;
1502
+ margin-bottom: 1rem;
1503
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
1504
+ animation: slideDown 0.8s ease-out;
1505
+ }
1506
+ .subtitle {
1507
+ font-size: 1.5rem;
1508
+ margin-bottom: 3rem;
1509
+ opacity: 0.9;
1510
+ animation: slideUp 0.8s ease-out 0.2s both;
1511
+ }
1512
+ .info-grid {
1513
+ display: grid;
1514
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1515
+ gap: 2rem;
1516
+ margin-top: 3rem;
1517
+ }
1518
+ .info-card {
1519
+ background: rgba(255,255,255,0.1);
1520
+ backdrop-filter: blur(10px);
1521
+ padding: 2rem;
1522
+ border-radius: 10px;
1523
+ border: 1px solid rgba(255,255,255,0.2);
1524
+ animation: fadeIn 0.8s ease-out 0.4s both;
1525
+ }
1526
+ .info-card h3 {
1527
+ font-size: 1.2rem;
1528
+ margin-bottom: 0.5rem;
1529
+ }
1530
+ .info-card p {
1531
+ opacity: 0.8;
1532
+ font-size: 0.9rem;
1533
+ }
1534
+ .deploy-badge {
1535
+ display: inline-block;
1536
+ background: rgba(255,255,255,0.2);
1537
+ padding: 0.5rem 1rem;
1538
+ border-radius: 20px;
1539
+ margin-top: 2rem;
1540
+ font-size: 0.9rem;
1541
+ animation: pulse 2s infinite;
1542
+ }
1543
+ @keyframes fadeIn {
1544
+ from { opacity: 0; transform: translateY(20px); }
1545
+ to { opacity: 1; transform: translateY(0); }
1546
+ }
1547
+ @keyframes slideDown {
1548
+ from { opacity: 0; transform: translateY(-30px); }
1549
+ to { opacity: 1; transform: translateY(0); }
1550
+ }
1551
+ @keyframes slideUp {
1552
+ from { opacity: 0; transform: translateY(30px); }
1553
+ to { opacity: 1; transform: translateY(0); }
1554
+ }
1555
+ @keyframes pulse {
1556
+ 0%, 100% { opacity: 1; }
1557
+ 50% { opacity: 0.8; }
1558
+ }
1559
+ </style>
1560
+ </head>
1561
+ <body>
1562
+ <div class="container">
1563
+ <h1>🏠 #{project_name}</h1>
1564
+ <p class="subtitle">Welcome to your Tayo-powered Rails application!</p>
1565
+
1566
+ <div class="info-grid">
1567
+ <div class="info-card">
1568
+ <h3>πŸ“¦ Container Ready</h3>
1569
+ <p>Your app is configured for container deployment</p>
1570
+ </div>
1571
+ <div class="info-card">
1572
+ <h3>πŸš€ GitHub Integration</h3>
1573
+ <p>Ready to push to GitHub Container Registry</p>
1574
+ </div>
1575
+ <div class="info-card">
1576
+ <h3>☁️ Cloudflare DNS</h3>
1577
+ <p>Domain management simplified</p>
1578
+ </div>
1579
+ </div>
1580
+
1581
+ <div class="deploy-badge">
1582
+ Deployed with Tayo πŸŽ‰
1583
+ </div>
1584
+ </div>
1585
+ </body>
1586
+ </html>
1587
+ HTML
1588
+
1589
+ # Welcome λ·° νŒŒμΌμ— μ €μž₯
1590
+ welcome_view_path = "app/views/welcome/index.html.erb"
1591
+ File.write(welcome_view_path, welcome_html)
1592
+
1593
+ # routes.rb μ—…λ°μ΄νŠΈ
1594
+ routes_file = "config/routes.rb"
1595
+ routes_content = File.read(routes_file)
1596
+
1597
+ # root 경둜 μ„€μ • - welcome#indexκ°€ 이미 μžˆλŠ”μ§€ 확인
1598
+ unless routes_content.include?("welcome#index")
1599
+ if routes_content.match?(/^\s*root\s+/)
1600
+ # κΈ°μ‘΄ root 섀정이 있으면 ꡐ체
1601
+ routes_content.gsub!(/^\s*root\s+.*$/, " root 'welcome#index'")
1602
+ else
1603
+ # root 섀정이 μ—†μœΌλ©΄ μΆ”κ°€
1604
+ routes_content.gsub!(/Rails\.application\.routes\.draw do\s*\n/, "Rails.application.routes.draw do\n root 'welcome#index'\n")
1605
+ end
1606
+
1607
+ File.write(routes_file, routes_content)
1608
+ puts " βœ… routes.rb에 root 경둜λ₯Ό μ„€μ •ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1609
+ else
1610
+ puts " ℹ️ routes.rb에 welcome#indexκ°€ 이미 μ„€μ •λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
1611
+ end
1612
+
1613
+ puts "βœ… Welcome νŽ˜μ΄μ§€κ°€ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€!".colorize(:green)
1614
+ puts " 경둜: /".colorize(:gray)
1615
+ puts " 컨트둀러: app/controllers/welcome_controller.rb".colorize(:gray)
1616
+ puts " λ·°: app/views/welcome/index.html.erb".colorize(:gray)
1617
+
1618
+ @welcome_page_created = true
1619
+ end
1620
+
1621
+ def commit_changes
1622
+ # Git μ €μž₯μ†ŒμΈμ§€ 확인
1623
+ unless Dir.exist?(".git")
1624
+ puts "⚠️ Git μ €μž₯μ†Œκ°€ μ•„λ‹™λ‹ˆλ‹€. 컀밋을 κ±΄λ„ˆλœλ‹ˆλ‹€.".colorize(:yellow)
1625
+ return
1626
+ end
1627
+
1628
+ # Welcome νŽ˜μ΄μ§€κ°€ μƒˆλ‘œ μƒμ„±λœ κ²½μš°μ—λ§Œ 컀밋
1629
+ unless @welcome_page_created
1630
+ puts "ℹ️ μƒˆλ‘œμš΄ 변경사항이 μ—†μ–΄ 컀밋을 κ±΄λ„ˆλœλ‹ˆλ‹€.".colorize(:yellow)
1631
+ return
1632
+ end
1633
+
1634
+ puts "πŸ“ 변경사항을 Git에 μ»€λ°‹ν•©λ‹ˆλ‹€...".colorize(:yellow)
1635
+
1636
+ # Git μƒνƒœ 확인
1637
+ git_status = `git status --porcelain`
1638
+
1639
+ if git_status.strip.empty?
1640
+ puts "ℹ️ 컀밋할 변경사항이 μ—†μŠ΅λ‹ˆλ‹€.".colorize(:yellow)
1641
+ return
1642
+ end
1643
+
1644
+ # 변경사항 μŠ€ν…Œμ΄μ§•
1645
+ system("git add .")
1646
+
1647
+ # 컀밋
1648
+ commit_message = "Add Tayo configuration and Welcome page"
1649
+ if system("git commit -m '#{commit_message}'")
1650
+ puts "βœ… 변경사항이 μ»€λ°‹λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1651
+ puts " 컀밋 λ©”μ‹œμ§€: #{commit_message}".colorize(:gray)
1652
+ else
1653
+ puts "⚠️ 컀밋에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
1654
+ end
1655
+ end
1656
+
1657
+ def clear_docker_cache
1658
+ puts "🧹 Docker μΊμ‹œλ₯Ό μ •λ¦¬ν•©λ‹ˆλ‹€...".colorize(:yellow)
1659
+
1660
+ # Docker system prune
1661
+ if system("docker system prune -f > /dev/null 2>&1")
1662
+ puts "βœ… Docker μ‹œμŠ€ν…œ μΊμ‹œκ°€ μ •λ¦¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1663
+ else
1664
+ puts "⚠️ Docker μ‹œμŠ€ν…œ 정리에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
1665
+ end
1666
+
1667
+ # Kamal build cache clear
1668
+ if File.exist?("config/deploy.yml")
1669
+ puts "🚒 Kamal λΉŒλ“œ μΊμ‹œλ₯Ό μ •λ¦¬ν•©λ‹ˆλ‹€...".colorize(:yellow)
1670
+ if system("kamal build --clear-cache > /dev/null 2>&1")
1671
+ puts "βœ… Kamal λΉŒλ“œ μΊμ‹œκ°€ μ •λ¦¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.".colorize(:green)
1672
+ else
1673
+ puts "⚠️ Kamal λΉŒλ“œ μΊμ‹œ 정리에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.".colorize(:yellow)
1674
+ end
1675
+ end
1676
+ end
1677
+ end
1678
+ end
1679
+ end
1680
+ </file>
1681
+
1682
+ <file path="lib/tayo/version.rb">
1683
+ # frozen_string_literal: true
1684
+
1685
+ module Tayo
1686
+ VERSION = "0.1.5"
1687
+ end
1688
+ </file>
1689
+
1690
+ </files>