tayo 0.1.11 → 0.1.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b925f8aa2d0555113bba7ca0f1ed1e2a4f7b923f4f8cd5f7ebc2ed0abb0a5df
4
- data.tar.gz: 42fe13a69f4c8b9bca65b2d46f8bfe501e34f2c350658d75e277dc1fb2005268
3
+ metadata.gz: d61ea1f144cfaf69b20fa118160100a9398ebcc04240a39d445a2f7b7729e828
4
+ data.tar.gz: c08f57b24d2fbb5397fea80d0773335925706225aae2b5fb055060b8bfedbaa7
5
5
  SHA512:
6
- metadata.gz: 13ce503cf8c8d0fd7c0ec6588cbf699d0713c01e6b1d9c26fa1b7c0339c4c1747b93b959e5e5071e9213ec28d6e2aa634032511f4ae921f5fbfeb2fd4ab5bac8
7
- data.tar.gz: d131a5928299a44bf63b2a0e1c65ebcf12113643120fc5d2f5a2084643b7e05de9388e34575ffddc3ad1e1804268051ce4fdd8df5213609b5e0361a6f447b304
6
+ metadata.gz: b51cc3c508a857449b7ec7a88d6f0e9fb822ee3fdd0f42ce40b088a0eb0bec9eaa31c69789ca65cf22e1214d72fee707da9c2078dcfce9054b666ac787ec39d0
7
+ data.tar.gz: 413c144d1c28b9f6ca125bb0d43b5580ab2eb5aa421e89afd09afa289b66f06a1c2dfd173b0c942cb1a09dcc1a0e244869a5700ffd79c46243a416cf6b4e6557
data/CHANGELOG.md ADDED
@@ -0,0 +1,75 @@
1
+ # 변경 기록 (Changelog)
2
+
3
+ ## [0.1.13] - 2025-06-30
4
+
5
+ ### 🚀 새로운 기능
6
+ - **SQLite 프로덕션 최적화 명령 추가** (`tayo sqlite`):
7
+ - Rails 8 SQLite 프로덕션 환경 최적화 설정 자동 적용
8
+ - WAL 모드와 IMMEDIATE 트랜잭션으로 동시성 문제 해결
9
+ - busy_timeout 5초 설정으로 SQLITE_BUSY 에러 방지
10
+ - 캐시 및 메모리 최적화로 성능 향상
11
+ - 모든 데이터베이스(primary, cache, queue, cable)에 적용
12
+ - **SQLite 설정 검증 도구**:
13
+ - `rails db:sqlite_check` - 모든 pragma 설정 검증
14
+ - `rails db:sqlite_stats` - 데이터베이스 성능 통계 확인
15
+ - **SQLite 가이드 문서 자동 생성**: 상세한 프로덕션 가이드 포함
16
+
17
+ ### 🛠️ 개선사항
18
+ - 공통 Base 클래스 추가로 코드 중복 제거
19
+ - 테스트 커버리지 확대
20
+
21
+ ## [0.1.12] - 2025-01-20
22
+
23
+ ### 🚀 새로운 기능
24
+ - **CLAUDE.md 문서 지원**: Claude AI 도우미를 위한 프로젝트별 지침 파일 지원
25
+ - **Cloudflare DNS 설정 대폭 개선**:
26
+ - Cloudflare Zone 목록에서 도메인을 직접 선택하는 방식으로 변경
27
+ - 루트 도메인(@)과 서브도메인 선택 UI 개선
28
+ - 기존 DNS 레코드 확인 및 덮어쓰기 시 사용자 확인 프롬프트 추가
29
+ - Cloudflare API 토큰을 `~/.tayo` 파일에 안전하게 저장 및 재사용
30
+ - **자동 Git 커밋**: `tayo gh`와 `tayo cf` 명령어 실행 후 자동으로 변경사항 커밋
31
+ - **버전 표시**: 명령어 실행 시 Tayo 버전 표시
32
+
33
+ ### 🛠️ 개선사항
34
+ - **Init 워크플로우 간소화**:
35
+ - Gemfile 수정 제거로 더 깔끔한 초기화 프로세스
36
+ - Docker 캐시 정리 기능 추가
37
+ - bootsnap 자동 처리 제거 (안정성 문제로 인해)
38
+ - **GitHub Container Registry 설정**:
39
+ - 조직(Organization) 계정 지원 추가
40
+ - ghcr.io URL 중복 제거 버그 수정
41
+ - Docker 로그인 자동화
42
+
43
+ ### 🐛 버그 수정
44
+ - Dockerfile bootsnap 프리컴파일 이슈 수정
45
+ - DNS 레코드 생성 시 프록시 설정 누락 문제 해결
46
+
47
+ ## [0.1.11] - 2025-01-19
48
+
49
+ ### 🛠️ 개선사항
50
+ - Bootsnap 처리를 DockerfileModifier 클래스로 리팩토링
51
+ - 더 안정적인 bootsnap 라인 제거 로직 구현
52
+ - 테스트 커버리지 강화
53
+
54
+ ## [0.1.10] - 2025-01-18
55
+
56
+ ### 🚀 새로운 기능
57
+ - Dockerfile에서 bootsnap 관련 설정 자동 제거/비활성화
58
+
59
+ ### 🐛 버그 수정
60
+ - Dockerfile bootsnap 프리컴파일 관련 다양한 엣지 케이스 처리
61
+
62
+ ## [0.1.9] - 2025-01-17
63
+
64
+ ### 🏗️ 기타 변경사항
65
+ - 홈페이지 URL을 TeamMilestone 조직으로 업데이트
66
+ - 저장소 이전에 따른 메타데이터 업데이트
67
+
68
+ ## [0.1.0] - 2025-01-15
69
+
70
+ ### 🎉 최초 릴리스
71
+ - `tayo init`: Rails 프로젝트 초기 설정 (Docker, Welcome 페이지)
72
+ - `tayo gh`: GitHub 저장소 및 Container Registry 설정
73
+ - `tayo cf`: Cloudflare DNS 설정
74
+ - OrbStack 자동 감지 및 실행
75
+ - 한국어 UI 지원
data/CLAUDE.md ADDED
@@ -0,0 +1,58 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+ Tayo is a Ruby gem that simplifies Rails app deployment to home servers using GitHub Container Registry and Cloudflare.
7
+
8
+ ## Common Development Commands
9
+
10
+ ### Testing
11
+ ```bash
12
+ # Run all tests
13
+ rake test
14
+
15
+ # Run specific test file
16
+ ruby -Ilib:test test/dockerfile_modifier_test.rb
17
+ ```
18
+
19
+ ### Building and Installing
20
+ ```bash
21
+ # Build gem
22
+ rake build
23
+
24
+ # Install locally
25
+ rake install
26
+
27
+ # Release to RubyGems.org
28
+ rake release
29
+ ```
30
+
31
+ ## Architecture
32
+
33
+ ### Core Structure
34
+ - `lib/tayo/cli.rb` - Thor-based CLI entry point
35
+ - `lib/tayo/commands/` - Command modules:
36
+ - `init.rb` - Rails project initialization with Docker setup
37
+ - `gh.rb` - GitHub repository and Container Registry configuration
38
+ - `cf.rb` - Cloudflare DNS configuration
39
+ - `lib/tayo/dockerfile_modifier.rb` - Handles bootsnap removal from Dockerfiles
40
+
41
+ ### Key Patterns
42
+ 1. **Command Structure**: Each command is a separate module under `Commands`
43
+ 2. **Error Handling**: Use colorized Korean messages for user-friendly output
44
+ 3. **Security**: Store sensitive tokens with 600 permissions, use macOS Keychain for Cloudflare tokens
45
+ 4. **Git Integration**: Auto-commit after each major step with descriptive messages
46
+ 5. **User Interaction**: Use TTY::Prompt for interactive configuration
47
+
48
+ ### Workflow
49
+ The typical usage flow:
50
+ 1. `tayo init` - Sets up Rails project with Docker
51
+ 2. `tayo gh` - Configures GitHub repository and Container Registry
52
+ 3. `tayo cf` - Sets up Cloudflare DNS
53
+ 4. `bin/kamal setup` - Deploys the application
54
+
55
+ ### Testing Approach
56
+ - Uses Minitest framework
57
+ - Tests focus on unit testing individual components
58
+ - DockerfileModifier has comprehensive test coverage
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Tayo
2
2
 
3
- Rails 애플리케이션을 홈서버에 배포하기 위한 도구입니다.
3
+ Rails 애플리케이션을 홈서버에 배포하기 위한 도구입니다. GitHub Container Registry와 Cloudflare를 활용하여 간편한 배포 워크플로우를 제공합니다.
4
4
 
5
5
  ## 설치
6
6
 
@@ -23,15 +23,13 @@ tayo init
23
23
  이 명령어는 다음 작업들을 수행합니다:
24
24
 
25
25
  - **OrbStack 설치 확인**: Docker 컨테이너를 실행하기 위한 OrbStack이 설치되어 있는지 확인합니다
26
- - **Bundle 설치**: 의존성을 설치합니다
27
- - **Linux 플랫폼 추가**: `x86_64-linux`와 `aarch64-linux` 플랫폼을 Gemfile.lock에 추가합니다
28
26
  - **Dockerfile 생성**: Rails 7 기본 Dockerfile이 없으면 생성합니다
29
27
  - **Welcome 페이지 생성**:
30
28
  - `app/controllers/welcome_controller.rb` 컨트롤러 생성
31
29
  - `app/views/welcome/index.html.erb` 뷰 파일 생성 (애니메이션이 있는 예쁜 랜딩 페이지)
32
30
  - `config/routes.rb`에 `root 'welcome#index'` 설정 추가
33
- - **Git 커밋**: 변경사항을 자동으로 커밋합니다
34
31
  - **Docker 캐시 정리**: 디스크 공간 확보를 위해 Docker 캐시를 정리합니다
32
+ - **Git 커밋**: 변경사항을 자동으로 커밋합니다
35
33
 
36
34
  ### 2. `tayo gh` - GitHub 저장소 및 Container Registry 설정
37
35
 
@@ -52,6 +50,7 @@ tayo gh
52
50
  - **GitHub 원격 저장소 설정**:
53
51
  - 기존 원격 저장소가 있으면 사용
54
52
  - 없으면 새 저장소 생성 (public/private 선택 가능)
53
+ - 개인 계정 및 조직(Organization) 계정 모두 지원
55
54
  - 코드를 GitHub에 푸시
56
55
  - **GitHub Container Registry 설정**:
57
56
  - Registry URL 생성: `ghcr.io/username/repository-name`
@@ -62,6 +61,7 @@ tayo gh
62
61
  - **환경 변수 파일 준비**:
63
62
  - `.env.production` 파일 생성
64
63
  - `.gitignore`에 추가하여 보안 유지
64
+ - **Git 커밋**: 설정 변경사항을 자동으로 커밋합니다
65
65
 
66
66
  ### 3. `tayo cf` - Cloudflare DNS 설정
67
67
 
@@ -73,27 +73,86 @@ tayo cf
73
73
 
74
74
  이 명령어는 다음 작업들을 수행합니다:
75
75
 
76
- - **설정 파일 확인**: `config/deploy.yml` 파일에서 서버 IP와 도메인 정보를 읽습니다
77
76
  - **Cloudflare 인증**:
78
77
  - API 토큰 입력 요청 (처음 실행 시)
79
- - 토큰을 안전하게 저장 (macOS Keychain 사용)
80
- - **도메인 Zone 확인**:
81
- - Cloudflare 계정에서 도메인을 찾습니다
82
- - Zone ID를 자동으로 가져옵니다
78
+ - 토큰을 `~/.tayo` 파일에 안전하게 저장하여 재사용
79
+ - 필요한 권한: Zone:Read, DNS:Edit
80
+ - **도메인 설정**:
81
+ - Cloudflare 계정의 Zone 목록에서 도메인 선택
82
+ - 루트 도메인(@) 또는 서브도메인 설정 지원
83
+ - 대화형 UI로 쉽게 설정 가능
83
84
  - **DNS 레코드 생성/업데이트**:
84
- - A 레코드 생성: 도메인을 서버 IP에 연결
85
- - 기존 레코드가 있으면 업데이트
85
+ - A 레코드 생성: IP 주소로 연결
86
+ - CNAME 레코드 생성: 도메인으로 연결
87
+ - 기존 레코드가 있으면 사용자 확인 후 업데이트
86
88
  - Proxied 설정 (Cloudflare CDN 사용)
87
- - **설정 완료 확인**:
88
- - DNS 설정이 완료되면 성공 메시지 표시
89
- - 도메인으로 접속 가능함을 안내
89
+ - **배포 설정 업데이트**:
90
+ - `config/deploy.yml` 파일의 proxy.host 자동 업데이트
91
+ - 서버 정보 SSH 사용자 설정
92
+ - **Git 커밋**: DNS 설정 변경사항을 자동으로 커밋합니다
93
+
94
+ ### 4. `tayo sqlite` - SQLite 프로덕션 최적화 설정
95
+
96
+ Rails 8의 SQLite를 프로덕션 환경에서 안정적으로 사용하기 위한 최적화 설정을 적용합니다.
97
+
98
+ ```bash
99
+ tayo sqlite
100
+ ```
101
+
102
+ 이 명령어는 다음 작업들을 수행합니다:
103
+
104
+ - **database.yml 업데이트**:
105
+ - WAL (Write-Ahead Logging) 모드 활성화
106
+ - IMMEDIATE 트랜잭션 모드 설정
107
+ - busy_timeout 5초 설정으로 "database is locked" 에러 방지
108
+ - 캐시 및 메모리 최적화 설정
109
+ - 모든 데이터베이스(primary, cache, queue, cable)에 적용
110
+ - **SQLite pragma 초기화 파일 생성**:
111
+ - `config/initializers/sqlite3_pragmas.rb` 생성
112
+ - 런타임 시 추가 최적화 및 설정 검증
113
+ - 주기적인 PRAGMA optimize 실행
114
+ - **설정 검증 도구 생성**:
115
+ - `rails db:sqlite_check` - SQLite 설정 검증
116
+ - `rails db:sqlite_stats` - SQLite 성능 통계 확인
117
+ - **가이드 문서 생성**:
118
+ - `sqlite_guide.md` - 상세한 SQLite 프로덕션 가이드
119
+ - **Git 커밋**: 모든 변경사항을 자동으로 커밋합니다
120
+
121
+ ## 전체 워크플로우
122
+
123
+ ```bash
124
+ # 1. 새 Rails 프로젝트 생성
125
+ rails new myapp
126
+ cd myapp
127
+
128
+ # 2. Tayo로 배포 준비
129
+ tayo init # Rails 프로젝트 초기화
130
+ tayo gh # GitHub 저장소 및 Container Registry 설정
131
+ tayo cf # Cloudflare DNS 설정
132
+ tayo sqlite # SQLite 프로덕션 최적화 (선택사항)
133
+
134
+ # 3. Kamal로 배포
135
+ bin/kamal setup
136
+ ```
137
+
138
+ ## 주요 기능
139
+
140
+ - **🚀 원스톱 배포 설정**: 3개의 명령어로 배포 준비 완료
141
+ - **🐳 Docker 기반**: OrbStack과 GitHub Container Registry 활용
142
+ - **🌐 Cloudflare 통합**: 자동 DNS 설정 및 CDN 지원
143
+ - **🗄️ SQLite 최적화**: Rails 8 SQLite 프로덕션 환경 완벽 지원
144
+ - **🔒 보안**: 토큰과 환경 변수를 안전하게 관리
145
+ - **🎯 한국어 UI**: 모든 메시지가 한국어로 제공
146
+ - **🛡️ 오류 처리**: 각 단계별 검증과 친절한 오류 메시지
147
+
148
+ ## 요구사항
90
149
 
91
- 명령어는 단계별로 진행 상황을 표시하며, 오류가 발생하면 친절한 안내 메시지를 제공합니다.
150
+ - Ruby 3.1.0 이상
151
+ - Rails 7.0 이상
152
+ - macOS (OrbStack 사용)
153
+ - GitHub 계정
154
+ - Cloudflare 계정
92
155
 
93
- rails new 로 프로젝트 생성 후
94
- bundle exec tayo init
95
- bundle exec tayo gh
96
- bundle exec tayo cf
156
+ ## 라이선스
97
157
 
98
- 순으로 진행 후
99
- bin/kamal setup 으로 배포 진행
158
+ MIT License
data/lib/tayo/cli.rb CHANGED
@@ -5,6 +5,7 @@ require "colorize"
5
5
  require_relative "commands/init"
6
6
  require_relative "commands/gh"
7
7
  require_relative "commands/cf"
8
+ require_relative "commands/sqlite"
8
9
 
9
10
  module Tayo
10
11
  class CLI < Thor
@@ -23,6 +24,11 @@ module Tayo
23
24
  Commands::Cf.new.execute
24
25
  end
25
26
 
27
+ desc "sqlite", "SQLite 프로덕션 최적화 설정을 적용합니다"
28
+ def sqlite
29
+ Commands::Sqlite.new.execute
30
+ end
31
+
26
32
  desc "version", "Tayo 버전을 표시합니다"
27
33
  def version
28
34
  puts "Tayo #{VERSION}"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tayo
4
+ module Commands
5
+ class Base
6
+ private
7
+
8
+ def in_rails_project?
9
+ File.exist?('Gemfile') && File.exist?('config/application.rb')
10
+ end
11
+ end
12
+ end
13
+ end
@@ -17,30 +17,27 @@ module Tayo
17
17
  return
18
18
  end
19
19
 
20
- # 1. 도메인 입력받기
21
- domain_info = get_domain_input
20
+ # --- 로직 순서 변경 ---
22
21
 
23
- # 2. Cloudflare 토큰 생성 페이지 열기 및 권한 안내
24
- open_token_creation_page
25
-
26
- # 3. 토큰 입력받기
22
+ # 2. 토큰 입력받기
27
23
  token = get_cloudflare_token
28
24
 
29
- # 4. Cloudflare API로 도메인 목록 조회 및 선택
30
- selected_zone = select_cloudflare_zone(token)
31
-
32
- # 5. 루트 도메인 레코드 확인
25
+ # 3. Cloudflare 선택 및 도메인 구성 (새로운 방식)
26
+ domain_info = configure_domain_from_zones(token)
27
+ selected_zone = domain_info[:selected_zone_object]
28
+
29
+ # 4. 기존 DNS 레코드 확인 (참고용)
33
30
  existing_records = check_existing_records(token, selected_zone, domain_info)
34
31
 
35
- # 6. DNS 레코드 추가/수정
32
+ # 5. DNS 레코드 추가/수정 (루트 도메인 덮어쓰기 로직 포함)
36
33
  setup_dns_record(token, selected_zone, domain_info, existing_records)
37
34
 
38
- # 7. config/deploy.yml 업데이트
35
+ # 6. config/deploy.yml 업데이트
39
36
  update_deploy_config(domain_info)
40
37
 
41
38
  puts "\n🎉 Cloudflare DNS 설정이 완료되었습니다!".colorize(:green)
42
39
 
43
- # 변경사항 커밋
40
+ # 7. 변경사항 커밋
44
41
  commit_cloudflare_changes(domain_info)
45
42
  end
46
43
 
@@ -50,31 +47,59 @@ module Tayo
50
47
  File.exist?("Gemfile") && File.exist?("config/application.rb")
51
48
  end
52
49
 
53
- def get_domain_input
54
- prompt = TTY::Prompt.new
50
+ # [신규] Cloudflare Zone 목록에서 도메인을 선택하고 구성하는 메소드
51
+ def configure_domain_from_zones(token)
52
+ puts "\n🌐 Cloudflare 계정의 도메인 목록을 조회합니다...".colorize(:yellow)
55
53
 
56
- puts "\n📝 배포할 도메인을 설정합니다.".colorize(:yellow)
54
+ zones = get_cloudflare_zones(token)
57
55
 
58
- domain = prompt.ask("배포할 도메인을 입력하세요 (예: myapp.com, api.example.com):") do |q|
59
- q.validate(/\A[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\z/, "올바른 도메인 형식을 입력해주세요 (예: myapp.com)")
56
+ if zones.empty?
57
+ puts " Cloudflare에 등록된 도메인(Zone)이 없습니다.".colorize(:red)
58
+ puts "먼저 https://dash.cloudflare.com 에서 도메인을 추가해주세요.".colorize(:cyan)
59
+ exit 1
60
60
  end
61
61
 
62
- # 도메인이 루트인지 서브도메인인지 판단
63
- parts = domain.split('.')
64
- if parts.length == 2
65
- { type: :root, domain: domain, zone: domain }
66
- else
67
- zone = parts[-2..-1].join('.')
68
- { type: :subdomain, domain: domain, zone: zone, subdomain: parts[0..-3].join('.') }
62
+ prompt = TTY::Prompt.new
63
+ # 사용자가 Zone을 이름으로 선택하고, 선택 시 전체 Zone 객체를 반환하도록 설정
64
+ zone_choices = zones.map { |zone| { name: "#{zone['name']} (#{zone['status']})", value: zone } }
65
+
66
+ selected_zone = prompt.select("설정할 도메인(Zone)을 선택하세요:", zone_choices, filter: true, per_page: 10)
67
+ zone_name = selected_zone['name']
68
+ puts "✅ 선택된 Zone: #{zone_name}".colorize(:green)
69
+
70
+ domain_type = prompt.select("\n어떤 종류의 도메인을 설정하시겠습니까?", [
71
+ { name: "루트 도메인 (@) - 예: #{zone_name}", value: :root },
72
+ { name: "서브도메인 - 예: www.#{zone_name}", value: :subdomain }
73
+ ])
74
+
75
+ if domain_type == :root
76
+ return {
77
+ type: :root,
78
+ domain: zone_name,
79
+ zone: zone_name,
80
+ selected_zone_object: selected_zone
81
+ }
82
+ else # :subdomain
83
+ subdomain_part = prompt.ask("사용할 서브도메인을 입력하세요 (예: www, api):") do |q|
84
+ q.required true
85
+ q.validate(/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, "유효한 서브도메인을 입력해주세요 (특수문자, . 사용 불가)")
86
+ end
87
+
88
+ full_domain = "#{subdomain_part.downcase}.#{zone_name}"
89
+ puts "✅ 설정할 전체 도메인: #{full_domain}".colorize(:green)
90
+
91
+ return {
92
+ type: :subdomain,
93
+ domain: full_domain,
94
+ zone: zone_name,
95
+ subdomain: subdomain_part.downcase,
96
+ selected_zone_object: selected_zone
97
+ }
69
98
  end
70
99
  end
71
100
 
72
101
  def open_token_creation_page
73
102
  puts "\n🔑 Cloudflare API 토큰이 필요합니다.".colorize(:yellow)
74
- puts "토큰 생성 페이지를 엽니다...".colorize(:cyan)
75
-
76
- # Cloudflare API 토큰 생성 페이지 열기
77
- system("open 'https://dash.cloudflare.com/profile/api-tokens'")
78
103
 
79
104
  puts "\n다음 권한으로 토큰을 생성해주세요:".colorize(:yellow)
80
105
  puts ""
@@ -88,9 +113,28 @@ module Tayo
88
113
  puts "• Zone → DNS → Edit".colorize(:white)
89
114
  puts " (Zone Resources: Select 'All zones')".colorize(:gray)
90
115
  puts ""
116
+
117
+ puts "토큰 생성 페이지를 엽니다...".colorize(:cyan)
118
+
119
+ system("open 'https://dash.cloudflare.com/profile/api-tokens'")
91
120
  end
92
121
 
93
122
  def get_cloudflare_token
123
+ existing_token = load_saved_token
124
+
125
+ if existing_token
126
+ puts "💾 저장된 토큰을 발견했습니다.".colorize(:cyan)
127
+ if test_cloudflare_token(existing_token)
128
+ puts "✅ 저장된 토큰이 유효합니다.".colorize(:green)
129
+ return existing_token
130
+ else
131
+ puts "❌ 저장된 토큰이 만료되거나 무효합니다. 새 토큰을 입력해주세요.".colorize(:yellow)
132
+ open_token_creation_page
133
+ end
134
+ else
135
+ open_token_creation_page
136
+ end
137
+
94
138
  prompt = TTY::Prompt.new
95
139
 
96
140
  token = prompt.mask("생성된 Cloudflare API 토큰을 붙여넣으세요:")
@@ -100,9 +144,9 @@ module Tayo
100
144
  exit 1
101
145
  end
102
146
 
103
- # 토큰 유효성 간단 확인
104
147
  if test_cloudflare_token(token.strip)
105
148
  puts "✅ 토큰이 확인되었습니다.".colorize(:green)
149
+ save_token(token.strip)
106
150
  return token.strip
107
151
  else
108
152
  puts "❌ 토큰이 올바르지 않거나 권한이 부족합니다.".colorize(:red)
@@ -125,27 +169,42 @@ module Tayo
125
169
  return false
126
170
  end
127
171
 
128
- def select_cloudflare_zone(token)
129
- puts "\n🌐 Cloudflare 도메인 목록을 조회합니다...".colorize(:yellow)
130
-
131
- zones = get_cloudflare_zones(token)
132
-
133
- if zones.empty?
134
- puts "❌ Cloudflare에 등록된 도메인이 없습니다.".colorize(:red)
135
- puts "먼저 https://dash.cloudflare.com 에서 도메인을 추가해주세요.".colorize(:cyan)
136
- exit 1
172
+ def load_saved_token
173
+ token_file = File.expand_path("~/.tayo")
174
+ return nil unless File.exist?(token_file)
175
+
176
+ begin
177
+ content = File.read(token_file)
178
+ token_line = content.lines.find { |line| line.start_with?("CLOUDFLARE_TOKEN=") }
179
+ return nil unless token_line
180
+
181
+ token = token_line.split("=", 2)[1]&.strip
182
+ return token unless token.nil? || token.empty?
183
+
184
+ nil
185
+ rescue => e
186
+ puts "⚠️ 토큰 파일 읽기 중 오류가 발생했습니다: #{e.message}".colorize(:yellow)
187
+ nil
188
+ end
189
+ end
190
+
191
+ def save_token(token)
192
+ token_file = File.expand_path("~/.tayo")
193
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
194
+
195
+ content = <<~CONTENT
196
+ # Tayo Configuration File
197
+ # Created: #{timestamp}
198
+ CLOUDFLARE_TOKEN=#{token}
199
+ CONTENT
200
+
201
+ begin
202
+ File.write(token_file, content)
203
+ File.chmod(0600, token_file)
204
+ puts "💾 토큰이 ~/.tayo 파일에 저장되었습니다.".colorize(:green)
205
+ rescue => e
206
+ puts "⚠️ 토큰 저장 중 오류가 발생했습니다: #{e.message}".colorize(:yellow)
137
207
  end
138
-
139
- prompt = TTY::Prompt.new
140
- zone_choices = zones.map { |zone| "#{zone['name']} (#{zone['status']})" }
141
-
142
- selected = prompt.select("도메인을 선택하세요:", zone_choices)
143
- zone_name = selected.split(' ').first
144
-
145
- selected_zone = zones.find { |zone| zone['name'] == zone_name }
146
- puts "✅ 선택된 도메인: #{zone_name}".colorize(:green)
147
-
148
- return selected_zone
149
208
  end
150
209
 
151
210
  def get_cloudflare_zones(token)
@@ -174,13 +233,10 @@ module Tayo
174
233
  def check_existing_records(token, zone, domain_info)
175
234
  puts "\n🔍 기존 DNS 레코드를 확인합니다...".colorize(:yellow)
176
235
 
177
- zone_id = zone['id']
178
- zone_name = zone['name']
179
-
180
- # 루트 도메인의 A/CNAME 레코드 확인
181
- records = get_dns_records(token, zone_id, zone_name, ['A', 'CNAME'])
236
+ target_name = (domain_info[:type] == :root) ? zone['name'] : domain_info[:domain]
237
+ records = get_dns_records(token, zone['id'], target_name, ['A', 'CNAME'])
182
238
 
183
- puts "기존 레코드: #{records.length}개 발견".colorize(:gray)
239
+ puts " (확인 대상: #{target_name}, 발견된 A/CNAME 레코드: #{records.length}개)".colorize(:gray)
184
240
 
185
241
  return records
186
242
  end
@@ -190,10 +246,7 @@ module Tayo
190
246
 
191
247
  types.each do |type|
192
248
  uri = URI("https://api.cloudflare.com/client/v4/zones/#{zone_id}/dns_records")
193
- uri.query = URI.encode_www_form({
194
- type: type,
195
- name: name
196
- })
249
+ uri.query = URI.encode_www_form({ type: type, name: name })
197
250
 
198
251
  http = Net::HTTP.new(uri.host, uri.port)
199
252
  http.use_ssl = true
@@ -215,71 +268,74 @@ module Tayo
215
268
  puts "❌ DNS 레코드 조회 중 오류: #{e.message}".colorize(:red)
216
269
  return []
217
270
  end
218
-
271
+
219
272
  def setup_dns_record(token, zone, domain_info, existing_records)
220
273
  puts "\n⚙️ DNS 레코드를 설정합니다...".colorize(:yellow)
221
274
 
222
- # 홈서버 IP/URL 입력받기
223
275
  prompt = TTY::Prompt.new
224
276
 
225
- server_info = prompt.ask("홈서버 IP 또는 도메인을 입력하세요:") do |q|
277
+ server_info = prompt.ask("연결할 서버 IP 또는 도메인을 입력하세요:") do |q|
226
278
  q.validate(/\A.+\z/, "서버 정보를 입력해주세요")
227
279
  end
228
280
 
229
- # SSH 사용자 계정 입력받기
230
281
  ssh_user = prompt.ask("SSH 사용자 계정을 입력하세요:", default: "root")
231
282
 
232
- # IP인지 도메인인지 판단
233
283
  is_ip = server_info.match?(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/)
234
284
  record_type = is_ip ? 'A' : 'CNAME'
235
285
 
236
286
  zone_id = zone['id']
237
287
  zone_name = zone['name']
238
288
 
239
- # 도메인 정보에 따라 레코드 설정
240
289
  final_domain = determine_final_domain(domain_info, zone_name, existing_records)
241
-
242
- # 대상 도메인의 모든 A/CNAME 레코드 확인
243
290
  all_records = get_dns_records(token, zone_id, final_domain[:name], ['A', 'CNAME'])
244
291
 
245
- if all_records.any?
246
- existing_record = all_records.first
247
-
248
- # 동일한 타입이고 같은 값이면 건너뛰기
249
- if existing_record['type'] == record_type && existing_record['content'] == server_info
250
- puts "✅ DNS 레코드가 이미 올바르게 설정되어 있습니다.".colorize(:green)
251
- puts " #{final_domain[:full_domain]} → #{server_info} (#{record_type} 레코드)".colorize(:gray)
252
- else
253
- # 타입이 다르거나 값이 다른 경우 삭제 후 재생성
254
- puts "⚠️ 기존 레코드를 삭제하고 새로 생성합니다.".colorize(:yellow)
255
- puts " 기존: #{existing_record['content']} (#{existing_record['type']}) 새로운: #{server_info} (#{record_type})".colorize(:gray)
256
-
257
- # 기존 레코드 삭제
258
- delete_dns_record(token, zone_id, existing_record['id'])
292
+ is_already_configured = all_records.length == 1 &&
293
+ all_records.first['type'] == record_type &&
294
+ all_records.first['content'] == server_info
295
+
296
+ if is_already_configured
297
+ puts "✅ DNS 레코드가 이미 올바르게 설정되어 있습니다.".colorize(:green)
298
+ puts " #{final_domain[:full_domain]} → #{server_info} (#{record_type} 레코드)".colorize(:gray)
299
+ else
300
+ # [수정됨] 기존 레코드가 있으면 사용자에게 확인을 받습니다.
301
+ if all_records.any?
302
+ puts "\n⚠️ '#{final_domain[:full_domain]}' 이미 설정된 DNS 레코드가 있습니다.".colorize(:yellow)
303
+ puts "--------------------------------------------------"
304
+ all_records.each do |record|
305
+ puts " - 타입: ".ljust(10) + "#{record['type']}".colorize(:cyan)
306
+ puts " 내용: ".ljust(10) + "#{record['content']}".colorize(:cyan)
307
+ puts " 프록시: ".ljust(10) + "#{record['proxied'] ? '활성' : '비활성'}".colorize(:cyan)
308
+ puts " "
309
+ end
310
+ puts "--------------------------------------------------"
311
+
312
+ message = "이 레코드를 삭제하고 새로 설정하시겠습니까? (이 작업은 되돌릴 수 없습니다)"
313
+ unless prompt.yes?(message)
314
+ puts "❌ DNS 설정이 사용자에 의해 취소되었습니다. 스크립트를 종료합니다.".colorize(:red)
315
+ exit 0
316
+ end
259
317
 
260
- #레코드 생성
318
+ puts "\n✅ 사용자가 승인하여 기존 레코드를 삭제하고 레코드를 생성합니다.".colorize(:green)
319
+ all_records.each do |record|
320
+ delete_dns_record(token, zone_id, record['id'])
321
+ end
322
+ create_dns_record(token, zone_id, final_domain[:name], record_type, server_info)
323
+
324
+ else
325
+ # 기존 레코드가 없으면 바로 생성합니다.
261
326
  create_dns_record(token, zone_id, final_domain[:name], record_type, server_info)
262
327
  end
263
- else
264
- # DNS 레코드 생성
265
- create_dns_record(token, zone_id, final_domain[:name], record_type, server_info)
266
328
  end
267
329
 
268
- # 최종 도메인 정보 저장
269
330
  @final_domain = final_domain[:full_domain]
270
331
  @server_info = server_info
271
332
  @ssh_user = ssh_user
272
333
  end
273
-
334
+
274
335
  def determine_final_domain(domain_info, zone_name, existing_records)
275
336
  case domain_info[:type]
276
337
  when :root
277
- if existing_records.any?
278
- puts "⚠️ 루트 도메인에 이미 레코드가 있습니다. app.#{zone_name}을 사용합니다.".colorize(:yellow)
279
- { name: "app.#{zone_name}", full_domain: "app.#{zone_name}" }
280
- else
281
- { name: zone_name, full_domain: zone_name }
282
- end
338
+ { name: zone_name, full_domain: zone_name }
283
339
  when :subdomain
284
340
  { name: domain_info[:domain], full_domain: domain_info[:domain] }
285
341
  end
@@ -298,7 +354,8 @@ module Tayo
298
354
  type: type,
299
355
  name: name,
300
356
  content: content,
301
- ttl: 300
357
+ ttl: 300,
358
+ proxied: true
302
359
  }
303
360
 
304
361
  request.body = data.to_json
@@ -306,7 +363,7 @@ module Tayo
306
363
 
307
364
  if response.code == '200'
308
365
  puts "✅ DNS 레코드가 생성되었습니다.".colorize(:green)
309
- puts " #{name} → #{content} (#{type} 레코드)".colorize(:gray)
366
+ puts " #{name} → #{content} (#{type} 레코드, 프록시됨)".colorize(:gray)
310
367
  else
311
368
  puts "❌ DNS 레코드 생성에 실패했습니다: #{response.code}".colorize(:red)
312
369
  puts response.body
@@ -328,16 +385,12 @@ module Tayo
328
385
 
329
386
  response = http.request(request)
330
387
 
331
- if response.code == '200'
332
- puts "✅ 기존 DNS 레코드가 삭제되었습니다.".colorize(:green)
333
- else
388
+ unless response.code == '200'
334
389
  puts "❌ DNS 레코드 삭제에 실패했습니다: #{response.code}".colorize(:red)
335
390
  puts response.body
336
- exit 1
337
391
  end
338
392
  rescue => e
339
393
  puts "❌ DNS 레코드 삭제 중 오류: #{e.message}".colorize(:red)
340
- exit 1
341
394
  end
342
395
 
343
396
  def update_deploy_config(domain_info)
@@ -356,27 +409,23 @@ module Tayo
356
409
  if content.include?("proxy:")
357
410
  content.gsub!(/(\s+host:\s+).*$/, "\\1#{@final_domain}")
358
411
  else
359
- # proxy 섹션이 없으면 추가
360
412
  proxy_config = "\n# Proxy configuration\nproxy:\n ssl: true\n host: #{@final_domain}\n"
361
413
  content += proxy_config
362
414
  end
363
415
 
364
416
  # servers 설정 업데이트
365
417
  if content.match?(/servers:\s*\n\s*web:\s*\n\s*-\s*/)
366
- content.gsub!(/(\s*servers:\s*\n\s*web:\s*\n\s*-\s*)[\d.]+/, "\\1#{@server_info}")
418
+ content.gsub!(/(\s*servers:\s*\n\s*web:\s*\n\s*-\s*)[\w.-]+/, "\\1#{@server_info}")
367
419
  end
368
420
 
369
421
  # ssh user 설정 업데이트
370
422
  if @ssh_user && @ssh_user != "root"
371
423
  if content.match?(/^ssh:/)
372
- # 기존 ssh 섹션 업데이트
373
424
  content.gsub!(/^ssh:\s*\n\s*user:\s*\w+/, "ssh:\n user: #{@ssh_user}")
374
425
  else
375
- # ssh 섹션 추가 (accessories 섹션 앞에 추가)
376
426
  if content.match?(/^# Use accessory services/)
377
427
  content.gsub!(/^# Use accessory services/, "# Use a different ssh user than root\nssh:\n user: #{@ssh_user}\n\n# Use accessory services")
378
428
  else
379
- # 파일 끝에 추가
380
429
  content += "\n# Use a different ssh user than root\nssh:\n user: #{@ssh_user}\n"
381
430
  end
382
431
  end
@@ -386,31 +435,28 @@ module Tayo
386
435
  puts "✅ config/deploy.yml이 업데이트되었습니다.".colorize(:green)
387
436
  puts " proxy.host: #{@final_domain}".colorize(:gray)
388
437
  puts " servers.web: #{@server_info}".colorize(:gray)
389
- puts " ssh.user: #{@ssh_user}".colorize(:gray) if @ssh_user && @ssh_user != "root"
438
+ if @ssh_user && @ssh_user != "root"
439
+ puts " ssh.user: #{@ssh_user}".colorize(:gray)
440
+ end
390
441
  end
391
442
 
392
443
  def commit_cloudflare_changes(domain_info)
393
444
  puts "\n📝 변경사항을 Git에 커밋합니다...".colorize(:yellow)
394
445
 
395
- # 변경된 파일이 있는지 확인
396
- status_output = `git status --porcelain`.strip
446
+ status_output = `git status --porcelain config/deploy.yml`.strip
397
447
 
398
448
  if status_output.empty?
399
- puts "ℹ️ 커밋할 변경사항이 없습니다.".colorize(:yellow)
449
+ puts "ℹ️ 커밋할 변경사항이 없습니다.".colorize(:cyan)
400
450
  return
401
451
  end
402
452
 
403
- # Git add
404
- system("git add -A")
453
+ system("git add config/deploy.yml")
405
454
 
406
- # Commit 메시지 생성
407
- 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"
455
+ commit_message = "feat: Configure Cloudflare DNS for #{@final_domain}\n\n- Set DNS record for #{@final_domain} to point to #{@server_info}\n- Update deployment configuration in config/deploy.yml\n\n🤖 Generated by Tayo"
408
456
 
409
- # Commit 실행
410
457
  if system("git commit -m \"#{commit_message}\"")
411
458
  puts "✅ 변경사항이 성공적으로 커밋되었습니다.".colorize(:green)
412
459
 
413
- # GitHub에 푸시
414
460
  if system("git push", out: File::NULL, err: File::NULL)
415
461
  puts "✅ 변경사항이 GitHub에 푸시되었습니다.".colorize(:green)
416
462
  else
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "colorize"
4
- require_relative "../dockerfile_modifier"
4
+ # require_relative "../dockerfile_modifier"
5
5
 
6
6
  module Tayo
7
7
  module Commands
@@ -17,8 +17,7 @@ module Tayo
17
17
  check_orbstack
18
18
  create_welcome_page
19
19
  clear_docker_cache
20
- ensure_dockerfile_exists
21
- disable_bootsnap_in_dockerfile
20
+ ensure_dockerfile_exists
22
21
  commit_changes
23
22
  puts "✅ Tayo가 성공적으로 설정되었습니다!".colorize(:green)
24
23
  end
@@ -83,16 +82,6 @@ module Tayo
83
82
  puts "✅ Dockerfile이 이미 존재합니다.".colorize(:green)
84
83
  end
85
84
  end
86
-
87
- def disable_bootsnap_in_dockerfile
88
- puts "🔧 Dockerfile에서 bootsnap을 비활성화합니다...".colorize(:yellow)
89
- begin
90
- modifier = DockerfileModifier.new
91
- modifier.init
92
- rescue => e
93
- puts "⚠️ Dockerfile 수정 중 오류가 발생했습니다: #{e.message}".colorize(:yellow)
94
- end
95
- end
96
85
 
97
86
  def create_welcome_page
98
87
  # Welcome 컨트롤러가 이미 있는지 확인
@@ -0,0 +1,413 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tayo/commands/base'
4
+ require 'git'
5
+ require 'colorize'
6
+ require 'tty-prompt'
7
+
8
+ module Tayo
9
+ module Commands
10
+ class Sqlite < Base
11
+ def execute
12
+ puts "\n🗄️ SQLite 프로덕션 최적화 설정을 시작합니다...".colorize(:yellow)
13
+ puts "Tayo #{Tayo::VERSION}".colorize(:light_black)
14
+
15
+ unless in_rails_project?
16
+ puts "❌ Rails 프로젝트가 아닙니다. Rails 프로젝트 루트에서 실행해주세요.".colorize(:red)
17
+ exit 1
18
+ end
19
+
20
+ puts "\n이 명령은 다음 작업을 수행합니다:".colorize(:light_black)
21
+ puts " • database.yml에 SQLite 프로덕션 최적화 설정 추가"
22
+ puts " • SQLite pragma 초기화 파일 생성"
23
+ puts " • SQLite 설정 검증 rake task 생성"
24
+ puts " • SQLite 프로덕션 가이드 문서 생성"
25
+
26
+ prompt = TTY::Prompt.new
27
+ continue = prompt.yes?("\n계속하시겠습니까?")
28
+
29
+ unless continue
30
+ puts "취소되었습니다.".colorize(:yellow)
31
+ exit 0
32
+ end
33
+
34
+ # Git 상태 체크
35
+ git = Git.open('.')
36
+ unless git.status.changed.empty? && git.status.added.empty? && git.status.deleted.empty?
37
+ puts "\n⚠️ 커밋되지 않은 변경사항이 있습니다.".colorize(:yellow)
38
+ puts "SQLite 설정을 적용하기 전에 현재 상태를 커밋하는 것을 권장합니다.".colorize(:light_black)
39
+
40
+ commit_now = prompt.yes?("\n지금 커밋하시겠습니까?")
41
+ if commit_now
42
+ git.add(all: true)
43
+ git.commit("현재 상태 저장 (tayo sqlite 실행 전)")
44
+ puts "✓ 현재 상태를 커밋했습니다.".colorize(:green)
45
+ end
46
+ end
47
+
48
+ # database.yml 업데이트
49
+ update_database_yml
50
+
51
+ # SQLite pragma 초기화 파일 생성
52
+ create_sqlite_pragma_initializer
53
+
54
+ # SQLite 검증 rake task 생성
55
+ create_sqlite_check_rake_task
56
+
57
+ # SQLite 가이드 문서 생성
58
+ create_sqlite_guide
59
+
60
+ # Git 커밋
61
+ begin
62
+ git.add('config/database.yml')
63
+ git.add('config/initializers/sqlite3_pragmas.rb')
64
+ git.add('lib/tasks/sqlite_check.rake')
65
+ git.add('sqlite_guide.md')
66
+
67
+ commit_message = <<~MSG
68
+ SQLite 프로덕션 최적화 설정 추가
69
+
70
+ - WAL 모드와 IMMEDIATE 트랜잭션으로 동시성 문제 해결
71
+ - busy_timeout 5초 설정으로 SQLITE_BUSY 에러 방지
72
+ - 캐시 및 메모리 최적화로 성능 향상
73
+ - 설정 검증을 위한 rake task 추가
74
+ - 상세 가이드 문서 포함
75
+ MSG
76
+
77
+ git.commit(commit_message)
78
+ puts "\n✓ 변경사항을 Git에 커밋했습니다.".colorize(:green)
79
+ rescue => e
80
+ puts "\n⚠️ Git 커밋 실패: #{e.message}".colorize(:yellow)
81
+ puts "수동으로 커밋해주세요.".colorize(:light_black)
82
+ end
83
+
84
+ puts "\n✅ SQLite 프로덕션 최적화 설정이 완료되었습니다!".colorize(:green)
85
+ puts "\n다음 명령으로 설정을 검증할 수 있습니다:".colorize(:light_black)
86
+ puts " rails db:sqlite_check".colorize(:cyan)
87
+ puts " rails db:sqlite_stats".colorize(:cyan)
88
+ puts "\n자세한 내용은 sqlite_guide.md 파일을 참고하세요.".colorize(:light_black)
89
+ end
90
+
91
+ private
92
+
93
+ def update_database_yml
94
+ puts "\n📝 database.yml 업데이트 중...".colorize(:blue)
95
+
96
+ database_yml_path = 'config/database.yml'
97
+ unless File.exist?(database_yml_path)
98
+ puts "❌ config/database.yml 파일을 찾을 수 없습니다.".colorize(:red)
99
+ exit 1
100
+ end
101
+
102
+ # 백업 생성
103
+ backup_path = "#{database_yml_path}.backup"
104
+ FileUtils.cp(database_yml_path, backup_path)
105
+ puts " 백업 생성: #{backup_path}".colorize(:light_black)
106
+
107
+ content = File.read(database_yml_path)
108
+
109
+ # SQLite 최적화 설정 추가
110
+ optimized_content = add_sqlite_optimizations(content)
111
+
112
+ File.write(database_yml_path, optimized_content)
113
+ puts "✓ database.yml이 업데이트되었습니다.".colorize(:green)
114
+ end
115
+
116
+ def add_sqlite_optimizations(content)
117
+ # 이미 최적화 설정이 있는지 확인
118
+ if content.include?('transaction_mode: IMMEDIATE')
119
+ puts " 이미 SQLite 최적화 설정이 적용되어 있습니다.".colorize(:yellow)
120
+ return content
121
+ end
122
+
123
+ # production 섹션 찾기
124
+ if content =~ /^production:\s*$/
125
+ # 기존 production 설정이 단순한 경우
126
+ if content =~ /^production:\s*\n\s+<<:\s*\*default/
127
+ # 새로운 production 설정으로 교체
128
+ new_production = <<~YAML
129
+ production:
130
+ primary:
131
+ <<: *default
132
+ database: storage/production.sqlite3
133
+ transaction_mode: IMMEDIATE
134
+ pragmas:
135
+ journal_mode: WAL
136
+ synchronous: NORMAL
137
+ cache_size: 2000
138
+ journal_size_limit: 27103364
139
+ foreign_keys: ON
140
+ mmap_size: 134217728
141
+ busy_timeout: 5000
142
+ cache:
143
+ <<: *default
144
+ database: storage/production_cache.sqlite3
145
+ migrations_paths: db/cache_migrate
146
+ transaction_mode: IMMEDIATE
147
+ pragmas:
148
+ journal_mode: WAL
149
+ synchronous: NORMAL
150
+ cache_size: 2000
151
+ journal_size_limit: 27103364
152
+ foreign_keys: ON
153
+ mmap_size: 134217728
154
+ busy_timeout: 5000
155
+ queue:
156
+ <<: *default
157
+ database: storage/production_queue.sqlite3
158
+ migrations_paths: db/queue_migrate
159
+ transaction_mode: IMMEDIATE
160
+ pragmas:
161
+ journal_mode: WAL
162
+ synchronous: NORMAL
163
+ cache_size: 2000
164
+ journal_size_limit: 27103364
165
+ foreign_keys: ON
166
+ mmap_size: 134217728
167
+ busy_timeout: 5000
168
+ cable:
169
+ <<: *default
170
+ database: storage/production_cable.sqlite3
171
+ migrations_paths: db/cable_migrate
172
+ transaction_mode: IMMEDIATE
173
+ pragmas:
174
+ journal_mode: WAL
175
+ synchronous: NORMAL
176
+ cache_size: 2000
177
+ journal_size_limit: 27103364
178
+ foreign_keys: ON
179
+ mmap_size: 134217728
180
+ busy_timeout: 5000
181
+ YAML
182
+
183
+ content.sub!(/^production:\s*\n\s+<<:\s*\*default\s*\n\s+database:\s+storage\/production\.sqlite3/, new_production.chomp)
184
+ end
185
+ end
186
+
187
+ # default 섹션에 busy_timeout 추가
188
+ unless content.include?('pragmas:')
189
+ content.sub!(/^(\s+timeout:\s+5000)/, "\\1\n pragmas:\n busy_timeout: 5000")
190
+ end
191
+
192
+ content
193
+ end
194
+
195
+ def create_sqlite_pragma_initializer
196
+ puts "\n📝 SQLite pragma 초기화 파일 생성 중...".colorize(:blue)
197
+
198
+ initializer_path = 'config/initializers/sqlite3_pragmas.rb'
199
+
200
+ # consulteam의 sqlite3_pragmas.rb 내용
201
+ initializer_content = <<~RUBY
202
+ # SQLite3 프로덕션 최적화 설정
203
+ # database.yml의 pragma 설정을 보완하는 추가 설정
204
+
205
+ # ActiveRecord가 연결을 설정할 때마다 실행되도록 설정
206
+ ActiveSupport.on_load(:active_record) do
207
+ if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter)
208
+ # SQLite3Adapter의 configure_connection 메서드 오버라이드
209
+ module SQLite3BusyTimeoutPatch
210
+ def configure_connection
211
+ super
212
+ # busy_timeout을 확실하게 설정 (밀리초 단위)
213
+ if @raw_connection.respond_to?(:busy_timeout)
214
+ @raw_connection.busy_timeout(5000)
215
+ end
216
+ raw_execute("PRAGMA busy_timeout = 5000", "SCHEMA")
217
+ raw_execute("PRAGMA journal_size_limit = 27103364", "SCHEMA")
218
+ end
219
+ end
220
+
221
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.prepend(SQLite3BusyTimeoutPatch)
222
+ end
223
+ end
224
+
225
+ Rails.application.config.after_initialize do
226
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
227
+ if connection.adapter_name == 'SQLite'
228
+ # 개발 환경에서도 중요한 설정은 적용
229
+ # SQLite3 gem의 busy_timeout 메서드를 직접 호출
230
+ if connection.raw_connection.respond_to?(:busy_timeout)
231
+ connection.raw_connection.busy_timeout(5000)
232
+ else
233
+ connection.execute("PRAGMA busy_timeout = 5000")
234
+ end
235
+ connection.execute("PRAGMA journal_size_limit = 27103364")
236
+
237
+ if Rails.env.production?
238
+ # WAL 모드 확인 (database.yml에서 이미 설정되어 있어야 함)
239
+ result = connection.execute("PRAGMA journal_mode")
240
+ journal_mode = result.first["journal_mode"] rescue result.first.values.first
241
+
242
+ unless journal_mode.upcase == "WAL"
243
+ Rails.logger.warn "WARNING: SQLite is not in WAL mode. Production performance may be severely impacted."
244
+ end
245
+
246
+ # busy_timeout 확인
247
+ result = connection.execute("PRAGMA busy_timeout")
248
+ busy_timeout = result.first["busy_timeout"] rescue result.first.values.first
249
+
250
+ if busy_timeout.to_i < 5000
251
+ Rails.logger.warn "WARNING: SQLite busy_timeout is less than 5000ms. This may cause SQLITE_BUSY errors under load."
252
+ end
253
+
254
+ # 추가 최적화 설정
255
+ connection.execute("PRAGMA temp_store = MEMORY")
256
+ connection.execute("PRAGMA optimize")
257
+
258
+ Rails.logger.info "SQLite production optimizations applied successfully"
259
+ Rails.logger.info "Journal mode: \#{journal_mode}, Busy timeout: \#{busy_timeout}ms"
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ # 주기적인 최적화 실행 (선택사항)
266
+ if Rails.env.production? && defined?(ActiveRecord::Base)
267
+ Thread.new do
268
+ loop do
269
+ sleep(4.hours)
270
+ begin
271
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
272
+ if connection.adapter_name == 'SQLite'
273
+ connection.execute("PRAGMA optimize")
274
+ Rails.logger.info "SQLite PRAGMA optimize executed"
275
+ end
276
+ end
277
+ rescue => e
278
+ Rails.logger.error "Failed to run SQLite optimization: \#{e.message}"
279
+ end
280
+ end
281
+ end
282
+ end
283
+ RUBY
284
+
285
+ File.write(initializer_path, initializer_content)
286
+ puts "✓ #{initializer_path} 파일이 생성되었습니다.".colorize(:green)
287
+ end
288
+
289
+ def create_sqlite_check_rake_task
290
+ puts "\n📝 SQLite 검증 rake task 생성 중...".colorize(:blue)
291
+
292
+ # lib/tasks 디렉토리 생성
293
+ FileUtils.mkdir_p('lib/tasks')
294
+
295
+ rake_task_path = 'lib/tasks/sqlite_check.rake'
296
+
297
+ # consulteam의 sqlite_check.rake 내용
298
+ rake_task_content = <<~RUBY
299
+ namespace :db do
300
+ desc "SQLite 프로덕션 설정 검증"
301
+ task sqlite_check: :environment do
302
+ if ActiveRecord::Base.connection.adapter_name == 'SQLite'
303
+ puts "\\n=== SQLite 프로덕션 설정 검증 ==="
304
+
305
+ connection = ActiveRecord::Base.connection
306
+
307
+ # 각 pragma 설정 확인
308
+ pragmas = {
309
+ 'journal_mode' => 'WAL',
310
+ 'synchronous' => 'NORMAL',
311
+ 'cache_size' => '2000',
312
+ 'journal_size_limit' => '27103364',
313
+ 'foreign_keys' => '1',
314
+ 'mmap_size' => '134217728',
315
+ 'busy_timeout' => '5000'
316
+ }
317
+
318
+ all_good = true
319
+
320
+ pragmas.each do |pragma, expected|
321
+ result = connection.execute("PRAGMA \#{pragma}")
322
+ actual = result.first[pragma] rescue result.first.values.first
323
+
324
+ if pragma == 'foreign_keys'
325
+ status = actual.to_s == expected ? "✓" : "✗"
326
+ elsif pragma == 'journal_mode'
327
+ status = actual.to_s.upcase == expected ? "✓" : "✗"
328
+ elsif pragma == 'synchronous'
329
+ # NORMAL = 1 in SQLite
330
+ status = (actual.to_s == '1' || actual.to_s.upcase == expected) ? "✓" : "✗"
331
+ else
332
+ status = actual.to_s == expected ? "✓" : "✗"
333
+ end
334
+
335
+ puts "\#{status} \#{pragma.ljust(20)}: \#{actual} \#{status == '✗' ? "(예상값: \#{expected})" : ""}"
336
+ all_good = false if status == "✗"
337
+ end
338
+
339
+ # transaction_mode 확인 (Rails 로그에서만 확인 가능)
340
+ puts "\\n참고: transaction_mode=IMMEDIATE 설정은 런타임에서만 확인 가능합니다."
341
+
342
+ if all_good
343
+ puts "\\n✅ 모든 SQLite 프로덕션 설정이 올바르게 구성되었습니다!"
344
+ else
345
+ puts "\\n⚠️ 일부 설정이 권장값과 다릅니다. database.yml을 확인하세요."
346
+ end
347
+ else
348
+ puts "이 태스크는 SQLite 데이터베이스에서만 사용 가능합니다."
349
+ end
350
+ end
351
+
352
+ desc "SQLite 성능 통계 확인"
353
+ task sqlite_stats: :environment do
354
+ if ActiveRecord::Base.connection.adapter_name == 'SQLite'
355
+ puts "\\n=== SQLite 성능 통계 ==="
356
+
357
+ connection = ActiveRecord::Base.connection
358
+
359
+ # 데이터베이스 파일 크기
360
+ db_path = connection.instance_variable_get(:@config)[:database]
361
+ if File.exist?(db_path)
362
+ size_mb = File.size(db_path) / 1024.0 / 1024.0
363
+ puts "데이터베이스 크기: \#{'%.2f' % size_mb} MB"
364
+
365
+ # WAL 파일 크기 확인
366
+ wal_path = "\#{db_path}-wal"
367
+ if File.exist?(wal_path)
368
+ wal_size_mb = File.size(wal_path) / 1024.0 / 1024.0
369
+ puts "WAL 파일 크기: \#{'%.2f' % wal_size_mb} MB"
370
+ end
371
+ end
372
+
373
+ # 페이지 정보
374
+ result = connection.execute("PRAGMA page_count")
375
+ page_count = result.first.values.first
376
+
377
+ result = connection.execute("PRAGMA page_size")
378
+ page_size = result.first.values.first
379
+
380
+ puts "페이지 수: \#{page_count}"
381
+ puts "페이지 크기: \#{page_size} bytes"
382
+
383
+ # 캐시 히트율 (대략적인 추정)
384
+ result = connection.execute("PRAGMA cache_size")
385
+ cache_size = result.first.values.first
386
+ puts "캐시 크기: \#{cache_size} pages"
387
+
388
+ puts "\\n실시간 성능 모니터링을 위해서는 애플리케이션 로그를 확인하세요."
389
+ else
390
+ puts "이 태스크는 SQLite 데이터베이스에서만 사용 가능합니다."
391
+ end
392
+ end
393
+ end
394
+ RUBY
395
+
396
+ File.write(rake_task_path, rake_task_content)
397
+ puts "✓ #{rake_task_path} 파일이 생성되었습니다.".colorize(:green)
398
+ end
399
+
400
+ def create_sqlite_guide
401
+ puts "\n📝 SQLite 가이드 문서 생성 중...".colorize(:blue)
402
+
403
+ guide_path = 'sqlite_guide.md'
404
+
405
+ # consulteam의 sqlite_guide.md 내용
406
+ guide_content = File.read('/Users/alfonso/projects/consulteam/sqlite_guide.md')
407
+
408
+ File.write(guide_path, guide_content)
409
+ puts "✓ #{guide_path} 파일이 생성되었습니다.".colorize(:green)
410
+ end
411
+ end
412
+ end
413
+ end
data/lib/tayo/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tayo
4
- VERSION = "0.1.11"
4
+ VERSION = "0.1.13"
5
5
  end
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+
3
+ # RubyGems API 키 설정 스크립트
4
+
5
+ echo "🔑 RubyGems API 키 설정 도우미"
6
+ echo "================================"
7
+ echo ""
8
+ echo "이 스크립트는 RubyGems API 키를 안전하게 저장합니다."
9
+ echo ""
10
+
11
+ # 옵션 선택
12
+ echo "저장 방법을 선택하세요:"
13
+ echo "1) ~/.gem/credentials (표준 방법)"
14
+ echo "2) macOS Keychain (가장 안전)"
15
+ echo "3) 둘 다"
16
+ echo ""
17
+ read -p "선택 (1-3): " choice
18
+
19
+ # API 키 입력
20
+ echo ""
21
+ read -s -p "RubyGems API 키를 입력하세요: " api_key
22
+ echo ""
23
+
24
+ case $choice in
25
+ 1)
26
+ # credentials 파일에 저장
27
+ mkdir -p ~/.gem
28
+ echo "---" > ~/.gem/credentials
29
+ echo ":rubygems_api_key: $api_key" >> ~/.gem/credentials
30
+ chmod 0600 ~/.gem/credentials
31
+ echo "✅ ~/.gem/credentials에 저장되었습니다."
32
+ ;;
33
+ 2)
34
+ # Keychain에 저장
35
+ security add-generic-password -a "$USER" -s "rubygems-api-key" -w "$api_key" -U
36
+ echo "✅ macOS Keychain에 저장되었습니다."
37
+ ;;
38
+ 3)
39
+ # 둘 다
40
+ mkdir -p ~/.gem
41
+ echo "---" > ~/.gem/credentials
42
+ echo ":rubygems_api_key: $api_key" >> ~/.gem/credentials
43
+ chmod 0600 ~/.gem/credentials
44
+ security add-generic-password -a "$USER" -s "rubygems-api-key" -w "$api_key" -U
45
+ echo "✅ 두 곳 모두에 저장되었습니다."
46
+ ;;
47
+ *)
48
+ echo "❌ 잘못된 선택입니다."
49
+ exit 1
50
+ ;;
51
+ esac
52
+
53
+ echo ""
54
+ echo "🎉 설정이 완료되었습니다!"
55
+ echo ""
56
+ echo "이제 다음 명령으로 gem을 배포할 수 있습니다:"
57
+ echo " rake release"
58
+ echo ""
59
+ echo "또는 Keychain에서 키를 사용하려면:"
60
+ echo " GEM_HOST_API_KEY=\$(security find-generic-password -a \"$USER\" -s \"rubygems-api-key\" -w) rake release"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tayo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - 이원섭wonsup Lee/Alfonso
@@ -75,18 +75,23 @@ extensions: []
75
75
  extra_rdoc_files: []
76
76
  files:
77
77
  - ".DS_Store"
78
+ - CHANGELOG.md
79
+ - CLAUDE.md
78
80
  - README.md
79
81
  - Rakefile
80
82
  - exe/tayo
81
83
  - lib/tayo.rb
82
84
  - lib/tayo/cli.rb
85
+ - lib/tayo/commands/base.rb
83
86
  - lib/tayo/commands/cf.rb
84
87
  - lib/tayo/commands/gh.rb
85
88
  - lib/tayo/commands/init.rb
89
+ - lib/tayo/commands/sqlite.rb
86
90
  - lib/tayo/dockerfile_modifier.rb
87
91
  - lib/tayo/version.rb
88
92
  - pkg/homebody-0.1.0.gem
89
93
  - repomix-output.xml
94
+ - scripts/setup_rubygems_key.sh
90
95
  - sig/tayo.rbs
91
96
  homepage: https://github.com/TeamMilestone/tayo
92
97
  licenses: []