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 +4 -4
- data/CHANGELOG.md +75 -0
- data/CLAUDE.md +58 -0
- data/README.md +80 -21
- data/lib/tayo/cli.rb +6 -0
- data/lib/tayo/commands/base.rb +13 -0
- data/lib/tayo/commands/cf.rb +162 -116
- data/lib/tayo/commands/init.rb +2 -13
- data/lib/tayo/commands/sqlite.rb +413 -0
- data/lib/tayo/version.rb +1 -1
- data/scripts/setup_rubygems_key.sh +60 -0
- metadata +6 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d61ea1f144cfaf69b20fa118160100a9398ebcc04240a39d445a2f7b7729e828
|
4
|
+
data.tar.gz: c08f57b24d2fbb5397fea80d0773335925706225aae2b5fb055060b8bfedbaa7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
- 토큰을
|
80
|
-
-
|
81
|
-
|
82
|
-
- Zone
|
78
|
+
- 토큰을 `~/.tayo` 파일에 안전하게 저장하여 재사용
|
79
|
+
- 필요한 권한: Zone:Read, DNS:Edit
|
80
|
+
- **도메인 설정**:
|
81
|
+
- Cloudflare 계정의 Zone 목록에서 도메인 선택
|
82
|
+
- 루트 도메인(@) 또는 서브도메인 설정 지원
|
83
|
+
- 대화형 UI로 쉽게 설정 가능
|
83
84
|
- **DNS 레코드 생성/업데이트**:
|
84
|
-
- A 레코드 생성:
|
85
|
-
-
|
85
|
+
- A 레코드 생성: IP 주소로 연결
|
86
|
+
- CNAME 레코드 생성: 도메인으로 연결
|
87
|
+
- 기존 레코드가 있으면 사용자 확인 후 업데이트
|
86
88
|
- Proxied 설정 (Cloudflare CDN 사용)
|
87
|
-
-
|
88
|
-
-
|
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
|
-
|
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}"
|
data/lib/tayo/commands/cf.rb
CHANGED
@@ -17,30 +17,27 @@ module Tayo
|
|
17
17
|
return
|
18
18
|
end
|
19
19
|
|
20
|
-
#
|
21
|
-
domain_info = get_domain_input
|
20
|
+
# --- 로직 순서 변경 ---
|
22
21
|
|
23
|
-
# 2.
|
24
|
-
open_token_creation_page
|
25
|
-
|
26
|
-
# 3. 토큰 입력받기
|
22
|
+
# 2. 토큰 입력받기
|
27
23
|
token = get_cloudflare_token
|
28
24
|
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
#
|
32
|
+
# 5. DNS 레코드 추가/수정 (루트 도메인 덮어쓰기 로직 포함)
|
36
33
|
setup_dns_record(token, selected_zone, domain_info, existing_records)
|
37
34
|
|
38
|
-
#
|
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
|
-
|
54
|
-
|
50
|
+
# [신규] Cloudflare Zone 목록에서 도메인을 선택하고 구성하는 메소드
|
51
|
+
def configure_domain_from_zones(token)
|
52
|
+
puts "\n🌐 Cloudflare 계정의 도메인 목록을 조회합니다...".colorize(:yellow)
|
55
53
|
|
56
|
-
|
54
|
+
zones = get_cloudflare_zones(token)
|
57
55
|
|
58
|
-
|
59
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
178
|
-
|
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 "
|
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("
|
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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
puts "
|
256
|
-
|
257
|
-
|
258
|
-
|
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
|
-
|
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}
|
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
|
-
|
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*)[\
|
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
|
-
|
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(:
|
449
|
+
puts "ℹ️ 커밋할 변경사항이 없습니다.".colorize(:cyan)
|
400
450
|
return
|
401
451
|
end
|
402
452
|
|
403
|
-
|
404
|
-
system("git add -A")
|
453
|
+
system("git add config/deploy.yml")
|
405
454
|
|
406
|
-
#
|
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
|
data/lib/tayo/commands/init.rb
CHANGED
@@ -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
@@ -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.
|
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: []
|