autodeploy-ai 1.5.0__tar.gz
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.
- autodeploy_ai-1.5.0/.env.example +17 -0
- autodeploy_ai-1.5.0/MANIFEST.in +4 -0
- autodeploy_ai-1.5.0/PKG-INFO +134 -0
- autodeploy_ai-1.5.0/README.md +110 -0
- autodeploy_ai-1.5.0/Takhir.png +0 -0
- autodeploy_ai-1.5.0/Tawal.png +0 -0
- autodeploy_ai-1.5.0/autodeploy_ai.egg-info/PKG-INFO +134 -0
- autodeploy_ai-1.5.0/autodeploy_ai.egg-info/SOURCES.txt +13 -0
- autodeploy_ai-1.5.0/autodeploy_ai.egg-info/dependency_links.txt +1 -0
- autodeploy_ai-1.5.0/autodeploy_ai.egg-info/entry_points.txt +2 -0
- autodeploy_ai-1.5.0/autodeploy_ai.egg-info/requires.txt +3 -0
- autodeploy_ai-1.5.0/autodeploy_ai.egg-info/top_level.txt +1 -0
- autodeploy_ai-1.5.0/deploy.py +701 -0
- autodeploy_ai-1.5.0/pyproject.toml +44 -0
- autodeploy_ai-1.5.0/setup.cfg +4 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# ── Pilih provider AI: openai | groq | ollama ──────────────────────────────
|
|
2
|
+
AI_PROVIDER=groq
|
|
3
|
+
|
|
4
|
+
# ── OpenAI ──────────────────────────────────────────────────────────────────
|
|
5
|
+
OPENAI_API_KEY=sk-...
|
|
6
|
+
OPENAI_MODEL=gpt-4o-mini
|
|
7
|
+
|
|
8
|
+
# ── Groq (gratis, cepat) ────────────────────────────────────────────────────
|
|
9
|
+
GROQ_API_KEY=gsk_...
|
|
10
|
+
GROQ_MODEL=llama-3.1-8b-instant
|
|
11
|
+
|
|
12
|
+
# ── Ollama (lokal, tidak perlu API key) ─────────────────────────────────────
|
|
13
|
+
OLLAMA_URL=http://localhost:11434
|
|
14
|
+
OLLAMA_MODEL=llama3.2:latest
|
|
15
|
+
|
|
16
|
+
# ── Git ──────────────────────────────────────────────────────────────────────
|
|
17
|
+
DEFAULT_BRANCH=main
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autodeploy-ai
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Git Init · AI Commit · Auto Push — one command from any terminal
|
|
5
|
+
Author: dhiksn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dhiksn/auto-deploy
|
|
8
|
+
Project-URL: Repository, https://github.com/dhiksn/auto-deploy
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/dhiksn/auto-deploy/issues
|
|
10
|
+
Keywords: git,deploy,ai,commit,cli,automation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: prompt_toolkit>=3.0.0
|
|
23
|
+
Requires-Dist: pyfiglet>=1.0.0
|
|
24
|
+
|
|
25
|
+
# ✦ AutoDeploy AI
|
|
26
|
+
|
|
27
|
+
> Git Init · AI Commit · Auto Push — dari terminal mana pun, satu command.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Fitur
|
|
36
|
+
|
|
37
|
+
- **Auto `git init`** kalau project belum punya `.git`
|
|
38
|
+
- **Set remote otomatis** dari GitHub URL yang lo kasih
|
|
39
|
+
- **Validasi repo** — cek apakah repo GitHub benar-benar ada sebelum deploy
|
|
40
|
+
- **AI generate commit message** pakai Groq, OpenAI, atau Ollama
|
|
41
|
+
- **Spinner animasi** di tiap step — staging, generating, commit, push
|
|
42
|
+
- **Global CLI** — bisa dipanggil dari folder project mana pun tanpa copy file
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install autodeploy-ai
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Setelah install, command `deploy` langsung tersedia dari terminal mana pun.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Setup
|
|
57
|
+
|
|
58
|
+
Buat file `.env` di folder manapun lo mau deploy, atau di home directory:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Windows
|
|
62
|
+
copy .env.example .env
|
|
63
|
+
|
|
64
|
+
# Linux / Mac
|
|
65
|
+
cp .env.example .env
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Edit `.env` sesuai provider AI yang lo pakai:
|
|
69
|
+
|
|
70
|
+
```env
|
|
71
|
+
# Pilih provider: groq | openai | ollama
|
|
72
|
+
AI_PROVIDER=groq
|
|
73
|
+
|
|
74
|
+
# Groq (gratis, cepat) — https://console.groq.com/keys
|
|
75
|
+
GROQ_API_KEY=gsk_...
|
|
76
|
+
GROQ_MODEL=llama-3.1-8b-instant
|
|
77
|
+
|
|
78
|
+
# Ollama (lokal, tidak perlu API key)
|
|
79
|
+
# OLLAMA_URL=http://localhost:11434
|
|
80
|
+
# OLLAMA_MODEL=llama3.2:latest
|
|
81
|
+
|
|
82
|
+
# OpenAI
|
|
83
|
+
# OPENAI_API_KEY=sk-...
|
|
84
|
+
# OPENAI_MODEL=gpt-4o-mini
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> **Penting:** jangan pernah commit file `.env` karena berisi API key.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Cara Pakai
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Project baru — belum ada .git
|
|
95
|
+
deploy https://github.com/username/repo-name
|
|
96
|
+
|
|
97
|
+
# Project yang sudah punya remote
|
|
98
|
+
deploy
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Flow yang terjadi
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
✓ Staging changes → git add .
|
|
105
|
+
✓ Generating commit message → AI generate via Groq / OpenAI / Ollama
|
|
106
|
+
└─ konfirmasi atau ketik manual
|
|
107
|
+
✓ Creating commit → git commit -m "<pesan>"
|
|
108
|
+
✓ Pushing to GitHub → git push -u origin <branch>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Kalau project belum ada `.git`, sebelum flow di atas akan otomatis:
|
|
112
|
+
1. Validasi repo GitHub — pastiin sudah dibuat di [github.com/new](https://github.com/new)
|
|
113
|
+
2. `git init`
|
|
114
|
+
3. `git remote add origin <url>`
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Provider AI
|
|
119
|
+
|
|
120
|
+
| Provider | Model | Keterangan | API Key |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| `groq` | `llama-3.1-8b-instant` | Online, gratis, cepat | [console.groq.com](https://console.groq.com/keys) |
|
|
123
|
+
| `ollama` | `llama3.2:latest` | Lokal, gratis, butuh Ollama running | Tidak perlu |
|
|
124
|
+
| `openai` | `gpt-4o-mini` | Online, berbayar | [platform.openai.com](https://platform.openai.com/api-keys) |
|
|
125
|
+
|
|
126
|
+
Untuk ganti provider, ubah `AI_PROVIDER` di file `.env`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Requirements
|
|
131
|
+
|
|
132
|
+
- Python 3.10+
|
|
133
|
+
- Git
|
|
134
|
+
- Salah satu AI provider di atas
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# ✦ AutoDeploy AI
|
|
2
|
+
|
|
3
|
+
> Git Init · AI Commit · Auto Push — dari terminal mana pun, satu command.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Fitur
|
|
12
|
+
|
|
13
|
+
- **Auto `git init`** kalau project belum punya `.git`
|
|
14
|
+
- **Set remote otomatis** dari GitHub URL yang lo kasih
|
|
15
|
+
- **Validasi repo** — cek apakah repo GitHub benar-benar ada sebelum deploy
|
|
16
|
+
- **AI generate commit message** pakai Groq, OpenAI, atau Ollama
|
|
17
|
+
- **Spinner animasi** di tiap step — staging, generating, commit, push
|
|
18
|
+
- **Global CLI** — bisa dipanggil dari folder project mana pun tanpa copy file
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install autodeploy-ai
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Setelah install, command `deploy` langsung tersedia dari terminal mana pun.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
Buat file `.env` di folder manapun lo mau deploy, atau di home directory:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Windows
|
|
38
|
+
copy .env.example .env
|
|
39
|
+
|
|
40
|
+
# Linux / Mac
|
|
41
|
+
cp .env.example .env
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Edit `.env` sesuai provider AI yang lo pakai:
|
|
45
|
+
|
|
46
|
+
```env
|
|
47
|
+
# Pilih provider: groq | openai | ollama
|
|
48
|
+
AI_PROVIDER=groq
|
|
49
|
+
|
|
50
|
+
# Groq (gratis, cepat) — https://console.groq.com/keys
|
|
51
|
+
GROQ_API_KEY=gsk_...
|
|
52
|
+
GROQ_MODEL=llama-3.1-8b-instant
|
|
53
|
+
|
|
54
|
+
# Ollama (lokal, tidak perlu API key)
|
|
55
|
+
# OLLAMA_URL=http://localhost:11434
|
|
56
|
+
# OLLAMA_MODEL=llama3.2:latest
|
|
57
|
+
|
|
58
|
+
# OpenAI
|
|
59
|
+
# OPENAI_API_KEY=sk-...
|
|
60
|
+
# OPENAI_MODEL=gpt-4o-mini
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> **Penting:** jangan pernah commit file `.env` karena berisi API key.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Cara Pakai
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Project baru — belum ada .git
|
|
71
|
+
deploy https://github.com/username/repo-name
|
|
72
|
+
|
|
73
|
+
# Project yang sudah punya remote
|
|
74
|
+
deploy
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Flow yang terjadi
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
✓ Staging changes → git add .
|
|
81
|
+
✓ Generating commit message → AI generate via Groq / OpenAI / Ollama
|
|
82
|
+
└─ konfirmasi atau ketik manual
|
|
83
|
+
✓ Creating commit → git commit -m "<pesan>"
|
|
84
|
+
✓ Pushing to GitHub → git push -u origin <branch>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Kalau project belum ada `.git`, sebelum flow di atas akan otomatis:
|
|
88
|
+
1. Validasi repo GitHub — pastiin sudah dibuat di [github.com/new](https://github.com/new)
|
|
89
|
+
2. `git init`
|
|
90
|
+
3. `git remote add origin <url>`
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Provider AI
|
|
95
|
+
|
|
96
|
+
| Provider | Model | Keterangan | API Key |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| `groq` | `llama-3.1-8b-instant` | Online, gratis, cepat | [console.groq.com](https://console.groq.com/keys) |
|
|
99
|
+
| `ollama` | `llama3.2:latest` | Lokal, gratis, butuh Ollama running | Tidak perlu |
|
|
100
|
+
| `openai` | `gpt-4o-mini` | Online, berbayar | [platform.openai.com](https://platform.openai.com/api-keys) |
|
|
101
|
+
|
|
102
|
+
Untuk ganti provider, ubah `AI_PROVIDER` di file `.env`.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Requirements
|
|
107
|
+
|
|
108
|
+
- Python 3.10+
|
|
109
|
+
- Git
|
|
110
|
+
- Salah satu AI provider di atas
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autodeploy-ai
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Git Init · AI Commit · Auto Push — one command from any terminal
|
|
5
|
+
Author: dhiksn
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dhiksn/auto-deploy
|
|
8
|
+
Project-URL: Repository, https://github.com/dhiksn/auto-deploy
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/dhiksn/auto-deploy/issues
|
|
10
|
+
Keywords: git,deploy,ai,commit,cli,automation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: rich>=13.0.0
|
|
22
|
+
Requires-Dist: prompt_toolkit>=3.0.0
|
|
23
|
+
Requires-Dist: pyfiglet>=1.0.0
|
|
24
|
+
|
|
25
|
+
# ✦ AutoDeploy AI
|
|
26
|
+
|
|
27
|
+
> Git Init · AI Commit · Auto Push — dari terminal mana pun, satu command.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Fitur
|
|
36
|
+
|
|
37
|
+
- **Auto `git init`** kalau project belum punya `.git`
|
|
38
|
+
- **Set remote otomatis** dari GitHub URL yang lo kasih
|
|
39
|
+
- **Validasi repo** — cek apakah repo GitHub benar-benar ada sebelum deploy
|
|
40
|
+
- **AI generate commit message** pakai Groq, OpenAI, atau Ollama
|
|
41
|
+
- **Spinner animasi** di tiap step — staging, generating, commit, push
|
|
42
|
+
- **Global CLI** — bisa dipanggil dari folder project mana pun tanpa copy file
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install autodeploy-ai
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Setelah install, command `deploy` langsung tersedia dari terminal mana pun.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Setup
|
|
57
|
+
|
|
58
|
+
Buat file `.env` di folder manapun lo mau deploy, atau di home directory:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Windows
|
|
62
|
+
copy .env.example .env
|
|
63
|
+
|
|
64
|
+
# Linux / Mac
|
|
65
|
+
cp .env.example .env
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Edit `.env` sesuai provider AI yang lo pakai:
|
|
69
|
+
|
|
70
|
+
```env
|
|
71
|
+
# Pilih provider: groq | openai | ollama
|
|
72
|
+
AI_PROVIDER=groq
|
|
73
|
+
|
|
74
|
+
# Groq (gratis, cepat) — https://console.groq.com/keys
|
|
75
|
+
GROQ_API_KEY=gsk_...
|
|
76
|
+
GROQ_MODEL=llama-3.1-8b-instant
|
|
77
|
+
|
|
78
|
+
# Ollama (lokal, tidak perlu API key)
|
|
79
|
+
# OLLAMA_URL=http://localhost:11434
|
|
80
|
+
# OLLAMA_MODEL=llama3.2:latest
|
|
81
|
+
|
|
82
|
+
# OpenAI
|
|
83
|
+
# OPENAI_API_KEY=sk-...
|
|
84
|
+
# OPENAI_MODEL=gpt-4o-mini
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
> **Penting:** jangan pernah commit file `.env` karena berisi API key.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Cara Pakai
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Project baru — belum ada .git
|
|
95
|
+
deploy https://github.com/username/repo-name
|
|
96
|
+
|
|
97
|
+
# Project yang sudah punya remote
|
|
98
|
+
deploy
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Flow yang terjadi
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
✓ Staging changes → git add .
|
|
105
|
+
✓ Generating commit message → AI generate via Groq / OpenAI / Ollama
|
|
106
|
+
└─ konfirmasi atau ketik manual
|
|
107
|
+
✓ Creating commit → git commit -m "<pesan>"
|
|
108
|
+
✓ Pushing to GitHub → git push -u origin <branch>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Kalau project belum ada `.git`, sebelum flow di atas akan otomatis:
|
|
112
|
+
1. Validasi repo GitHub — pastiin sudah dibuat di [github.com/new](https://github.com/new)
|
|
113
|
+
2. `git init`
|
|
114
|
+
3. `git remote add origin <url>`
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Provider AI
|
|
119
|
+
|
|
120
|
+
| Provider | Model | Keterangan | API Key |
|
|
121
|
+
|---|---|---|---|
|
|
122
|
+
| `groq` | `llama-3.1-8b-instant` | Online, gratis, cepat | [console.groq.com](https://console.groq.com/keys) |
|
|
123
|
+
| `ollama` | `llama3.2:latest` | Lokal, gratis, butuh Ollama running | Tidak perlu |
|
|
124
|
+
| `openai` | `gpt-4o-mini` | Online, berbayar | [platform.openai.com](https://platform.openai.com/api-keys) |
|
|
125
|
+
|
|
126
|
+
Untuk ganti provider, ubah `AI_PROVIDER` di file `.env`.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Requirements
|
|
131
|
+
|
|
132
|
+
- Python 3.10+
|
|
133
|
+
- Git
|
|
134
|
+
- Salah satu AI provider di atas
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
.env.example
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
Takhir.png
|
|
5
|
+
Tawal.png
|
|
6
|
+
deploy.py
|
|
7
|
+
pyproject.toml
|
|
8
|
+
autodeploy_ai.egg-info/PKG-INFO
|
|
9
|
+
autodeploy_ai.egg-info/SOURCES.txt
|
|
10
|
+
autodeploy_ai.egg-info/dependency_links.txt
|
|
11
|
+
autodeploy_ai.egg-info/entry_points.txt
|
|
12
|
+
autodeploy_ai.egg-info/requires.txt
|
|
13
|
+
autodeploy_ai.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
deploy
|
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
┌────────────────────────────────────────────────┐
|
|
4
|
+
│ AutoDeploy CLI │
|
|
5
|
+
│ Git Init · AI Commit · Auto Push │
|
|
6
|
+
│ │
|
|
7
|
+
│ Requires: │
|
|
8
|
+
│ pip install rich prompt_toolkit │
|
|
9
|
+
└────────────────────────────────────────────────┘
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# ── Standard library ──────────────────────────────────────────────────────────
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import json
|
|
17
|
+
import subprocess
|
|
18
|
+
import time
|
|
19
|
+
import threading
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
# ── Auto-install dependencies ─────────────────────────────────────────────────
|
|
23
|
+
def _ensure(pkg: str, import_as: str | None = None):
|
|
24
|
+
import importlib
|
|
25
|
+
name = import_as or pkg
|
|
26
|
+
try:
|
|
27
|
+
return importlib.import_module(name)
|
|
28
|
+
except ImportError:
|
|
29
|
+
subprocess.check_call(
|
|
30
|
+
[sys.executable, "-m", "pip", "install", pkg, "-q"],
|
|
31
|
+
stdout=subprocess.DEVNULL,
|
|
32
|
+
)
|
|
33
|
+
return importlib.import_module(name)
|
|
34
|
+
|
|
35
|
+
_ensure("rich")
|
|
36
|
+
_ensure("prompt_toolkit", "prompt_toolkit")
|
|
37
|
+
|
|
38
|
+
from rich.console import Console
|
|
39
|
+
from rich.panel import Panel
|
|
40
|
+
from rich.table import Table
|
|
41
|
+
from rich.text import Text
|
|
42
|
+
from rich.align import Align
|
|
43
|
+
from rich.box import DOUBLE_EDGE, SQUARE, MINIMAL
|
|
44
|
+
from rich.progress import Progress, BarColumn, TextColumn
|
|
45
|
+
from rich.markup import escape
|
|
46
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
47
|
+
from prompt_toolkit.styles import Style as PTStyle
|
|
48
|
+
from prompt_toolkit.formatted_text import HTML
|
|
49
|
+
|
|
50
|
+
# ── Console ───────────────────────────────────────────────────────────────────
|
|
51
|
+
console = Console(highlight=False, soft_wrap=True)
|
|
52
|
+
|
|
53
|
+
APP_VERSION = "1.5.0"
|
|
54
|
+
|
|
55
|
+
# ── Theme: teal + amber (no purple, no blue) ──────────────────────────────────
|
|
56
|
+
C_HEAD = "bold turquoise2"
|
|
57
|
+
C_LINE = "grey42"
|
|
58
|
+
C_LABEL = "grey62"
|
|
59
|
+
C_VAL = "wheat1"
|
|
60
|
+
C_OK = "sea_green2"
|
|
61
|
+
C_WARN = "dark_orange"
|
|
62
|
+
C_ERR = "bright_red"
|
|
63
|
+
C_DIM = "grey35"
|
|
64
|
+
C_TEXT = "white"
|
|
65
|
+
C_ACCENT = "turquoise2"
|
|
66
|
+
C_DIM_ANSI = "2" # ANSI dim untuk sys.stdout.write
|
|
67
|
+
|
|
68
|
+
PT_STYLE = PTStyle.from_dict({
|
|
69
|
+
"prompt": "bold ansigreen",
|
|
70
|
+
"placeholder": "ansibrightblack",
|
|
71
|
+
"": "ansiwhite",
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
LOGO = "Slant" # pyfiglet font
|
|
75
|
+
|
|
76
|
+
# ── Load .env ─────────────────────────────────────────────────────────────────
|
|
77
|
+
def load_env():
|
|
78
|
+
env_file = Path(__file__).parent / ".env"
|
|
79
|
+
if env_file.exists():
|
|
80
|
+
with open(env_file) as f:
|
|
81
|
+
for line in f:
|
|
82
|
+
line = line.strip()
|
|
83
|
+
if line and not line.startswith("#") and "=" in line:
|
|
84
|
+
key, _, value = line.partition("=")
|
|
85
|
+
# Selalu override dari .env, jangan pakai setdefault
|
|
86
|
+
os.environ[key.strip()] = value.strip().strip('"').strip("'")
|
|
87
|
+
|
|
88
|
+
load_env()
|
|
89
|
+
|
|
90
|
+
# ── Config ────────────────────────────────────────────────────────────────────
|
|
91
|
+
AI_PROVIDER = os.environ.get("AI_PROVIDER", "openai")
|
|
92
|
+
OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "")
|
|
93
|
+
GROQ_KEY = os.environ.get("GROQ_API_KEY", "")
|
|
94
|
+
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
|
|
95
|
+
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "llama3")
|
|
96
|
+
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
|
97
|
+
GROQ_MODEL = os.environ.get("GROQ_MODEL", "llama3-8b-8192")
|
|
98
|
+
DEFAULT_BRANCH = os.environ.get("DEFAULT_BRANCH", "main")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
102
|
+
# UI HELPERS
|
|
103
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
104
|
+
|
|
105
|
+
def ui_clear():
|
|
106
|
+
os.system("cls" if os.name == "nt" else "clear")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def ui_banner():
|
|
110
|
+
_ensure("pyfiglet")
|
|
111
|
+
import pyfiglet
|
|
112
|
+
fig = pyfiglet.figlet_format("AutoDeploy", font=LOGO, justify="center",
|
|
113
|
+
width=console.width or 100)
|
|
114
|
+
console.print()
|
|
115
|
+
console.print(f"[{C_HEAD}]{fig.rstrip()}[/]")
|
|
116
|
+
console.print()
|
|
117
|
+
console.print(Align.center(Text(f"v{APP_VERSION} · Git Init · AI Commit · Auto Push", style=C_DIM)))
|
|
118
|
+
console.print()
|
|
119
|
+
console.print(Align.center(Text("─" * 56, style=C_LINE)))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def ui_splash():
|
|
123
|
+
ui_clear()
|
|
124
|
+
ui_banner()
|
|
125
|
+
labels = ["Initialising", "Reading workspace", "Loading config", "Ready"]
|
|
126
|
+
term_width = os.get_terminal_size().columns if hasattr(os, "get_terminal_size") else 80
|
|
127
|
+
for label in labels:
|
|
128
|
+
text = f"[ {label} ]"
|
|
129
|
+
padded = text.center(term_width)
|
|
130
|
+
sys.stdout.write(f"\033[2K\r{padded}")
|
|
131
|
+
sys.stdout.flush()
|
|
132
|
+
time.sleep(0.15)
|
|
133
|
+
sys.stdout.write("\033[2K\r")
|
|
134
|
+
sys.stdout.flush()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def ui_header(repo_dir: str, branch: str, remote: str, ai_provider: str):
|
|
138
|
+
body = Table.grid(padding=(0, 2))
|
|
139
|
+
body.add_column(justify="right", style=C_LABEL, min_width=10)
|
|
140
|
+
body.add_column(style=C_VAL)
|
|
141
|
+
|
|
142
|
+
body.add_row("REMOTE", escape(remote or "not set"))
|
|
143
|
+
body.add_row("BRANCH", f"[{C_ACCENT}]{escape(branch or DEFAULT_BRANCH)}[/]")
|
|
144
|
+
body.add_row("AI", f"[{C_OK}]{ai_provider.upper()}[/] [{C_DIM}]{_ai_model_label()}[/]")
|
|
145
|
+
|
|
146
|
+
console.print(Panel(
|
|
147
|
+
body,
|
|
148
|
+
title="[bold white] SESSION [/]",
|
|
149
|
+
title_align="left",
|
|
150
|
+
border_style=C_LINE,
|
|
151
|
+
box=SQUARE,
|
|
152
|
+
padding=(1, 2),
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _ai_model_label() -> str:
|
|
157
|
+
if AI_PROVIDER == "openai":
|
|
158
|
+
return OPENAI_MODEL
|
|
159
|
+
if AI_PROVIDER == "groq":
|
|
160
|
+
return GROQ_MODEL
|
|
161
|
+
if AI_PROVIDER == "ollama":
|
|
162
|
+
return OLLAMA_MODEL
|
|
163
|
+
return "unknown"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def ui_success(commit_msg: str, branch: str, remote: str):
|
|
167
|
+
body = Table.grid(padding=(0, 2))
|
|
168
|
+
body.add_column(justify="right", style=C_LABEL, min_width=10)
|
|
169
|
+
body.add_column(style=C_TEXT)
|
|
170
|
+
body.add_row("COMMIT", escape(commit_msg))
|
|
171
|
+
body.add_row("BRANCH", f"[{C_ACCENT}]{escape(branch)}[/]")
|
|
172
|
+
body.add_row("REMOTE", escape(remote))
|
|
173
|
+
|
|
174
|
+
console.print()
|
|
175
|
+
console.print(Panel(
|
|
176
|
+
body,
|
|
177
|
+
title=f"[bold {C_OK}] DEPLOY COMPLETE [/]",
|
|
178
|
+
title_align="left",
|
|
179
|
+
border_style=C_OK,
|
|
180
|
+
box=DOUBLE_EDGE,
|
|
181
|
+
padding=(1, 2),
|
|
182
|
+
))
|
|
183
|
+
console.print()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def ui_error(message: str):
|
|
187
|
+
console.print()
|
|
188
|
+
console.print(Panel(
|
|
189
|
+
f"[{C_TEXT}]{escape(str(message))}[/]",
|
|
190
|
+
title=f"[bold {C_ERR}] ERROR [/]",
|
|
191
|
+
title_align="left",
|
|
192
|
+
border_style=C_ERR,
|
|
193
|
+
box=DOUBLE_EDGE,
|
|
194
|
+
padding=(1, 2),
|
|
195
|
+
))
|
|
196
|
+
console.print()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def ui_warn(message: str):
|
|
200
|
+
console.print(f"\n [bold {C_WARN}]![/] [{C_WARN}]{escape(message)}[/]\n")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def ui_step(label: str, value: str = ""):
|
|
204
|
+
val_str = f" [{C_DIM}]{escape(value)}[/]" if value else ""
|
|
205
|
+
console.print(f" [{C_ACCENT}]>[/] [{C_TEXT}]{escape(label)}[/]{val_str}")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def ui_tips():
|
|
209
|
+
console.print(Panel(
|
|
210
|
+
f"[{C_LABEL}]Enter[/] [{C_DIM}]terima commit message AI · [/]"
|
|
211
|
+
f"[{C_LABEL}]ketik pesan[/] [{C_DIM}]untuk override · [/]"
|
|
212
|
+
f"[{C_LABEL}]Ctrl+C[/] [{C_DIM}]batal kapan saja[/]",
|
|
213
|
+
border_style=C_DIM,
|
|
214
|
+
box=MINIMAL,
|
|
215
|
+
padding=(0, 1),
|
|
216
|
+
))
|
|
217
|
+
console.print()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
221
|
+
# GIT HELPERS
|
|
222
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
223
|
+
|
|
224
|
+
def run(cmd: list[str], capture: bool = False) -> subprocess.CompletedProcess:
|
|
225
|
+
return subprocess.run(
|
|
226
|
+
cmd,
|
|
227
|
+
capture_output=capture,
|
|
228
|
+
text=True,
|
|
229
|
+
encoding="utf-8",
|
|
230
|
+
errors="replace",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def spin_run(cmd: list[str], label: str) -> subprocess.CompletedProcess:
|
|
235
|
+
"""Jalankan command sambil tampilkan spinner animasi, lalu print status akhir."""
|
|
236
|
+
import threading
|
|
237
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
238
|
+
stop = threading.Event()
|
|
239
|
+
result_box: list[subprocess.CompletedProcess] = []
|
|
240
|
+
|
|
241
|
+
def _spin():
|
|
242
|
+
i = 0
|
|
243
|
+
while not stop.is_set():
|
|
244
|
+
sys.stdout.write(f"\r \033[{C_DIM_ANSI}m{frames[i % len(frames)]}\033[0m {label}...")
|
|
245
|
+
sys.stdout.flush()
|
|
246
|
+
time.sleep(0.08)
|
|
247
|
+
i += 1
|
|
248
|
+
|
|
249
|
+
t = threading.Thread(target=_spin, daemon=True)
|
|
250
|
+
t.start()
|
|
251
|
+
try:
|
|
252
|
+
result_box.append(subprocess.run(
|
|
253
|
+
cmd, capture_output=True, text=True,
|
|
254
|
+
encoding="utf-8", errors="replace",
|
|
255
|
+
))
|
|
256
|
+
finally:
|
|
257
|
+
stop.set()
|
|
258
|
+
t.join()
|
|
259
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
260
|
+
sys.stdout.flush()
|
|
261
|
+
|
|
262
|
+
result = result_box[0]
|
|
263
|
+
if result.returncode == 0:
|
|
264
|
+
console.print(f" [{C_OK}]✓[/] {label}")
|
|
265
|
+
else:
|
|
266
|
+
console.print(f" [{C_ERR}]✗[/] {label}")
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def git_has_changes() -> bool:
|
|
271
|
+
return bool((run(["git", "status", "--porcelain"], capture=True).stdout or "").strip())
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def git_current_branch() -> str:
|
|
275
|
+
result = run(["git", "rev-parse", "--abbrev-ref", "HEAD"], capture=True)
|
|
276
|
+
return (result.stdout or "").strip() or DEFAULT_BRANCH
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def git_remote_url() -> str:
|
|
280
|
+
result = run(["git", "remote", "get-url", "origin"], capture=True)
|
|
281
|
+
return (result.stdout or "").strip()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def git_diff_staged() -> str:
|
|
285
|
+
stat = (run(["git", "diff", "--staged", "--stat"], capture=True).stdout or "").strip()
|
|
286
|
+
diff = (run(["git", "diff", "--staged", "--unified=3"], capture=True).stdout or "").strip()
|
|
287
|
+
MAX = 3500
|
|
288
|
+
if len(diff) > MAX:
|
|
289
|
+
diff = diff[:MAX] + "\n...(truncated)"
|
|
290
|
+
return f"{stat}\n\n{diff}".strip()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
294
|
+
# AI COMMIT MESSAGE
|
|
295
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
296
|
+
|
|
297
|
+
def _build_prompt(diff: str) -> str:
|
|
298
|
+
return (
|
|
299
|
+
"You are a Git commit message generator. "
|
|
300
|
+
"Based on the following staged diff, write ONE concise commit message in English.\n"
|
|
301
|
+
"Format: <type>(<scope>): <short description>\n"
|
|
302
|
+
"Types: feat, fix, docs, style, refactor, chore, test\n"
|
|
303
|
+
"Rules: single line only, no extra explanation, max 72 characters.\n\n"
|
|
304
|
+
f"DIFF:\n{diff}"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _call_openai(diff: str) -> str:
|
|
309
|
+
import urllib.request
|
|
310
|
+
payload = json.dumps({
|
|
311
|
+
"model": OPENAI_MODEL,
|
|
312
|
+
"messages": [{"role": "user", "content": _build_prompt(diff)}],
|
|
313
|
+
"max_tokens": 100,
|
|
314
|
+
"temperature": 0.3,
|
|
315
|
+
}).encode()
|
|
316
|
+
req = urllib.request.Request(
|
|
317
|
+
"https://api.openai.com/v1/chat/completions",
|
|
318
|
+
data=payload,
|
|
319
|
+
headers={
|
|
320
|
+
"Content-Type": "application/json",
|
|
321
|
+
"Authorization": f"Bearer {OPENAI_KEY}",
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
325
|
+
data = json.loads(resp.read())
|
|
326
|
+
return data["choices"][0]["message"]["content"].strip().strip('"')
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _call_groq(diff: str) -> str:
|
|
330
|
+
import urllib.request
|
|
331
|
+
payload = json.dumps({
|
|
332
|
+
"model": GROQ_MODEL,
|
|
333
|
+
"messages": [{"role": "user", "content": _build_prompt(diff)}],
|
|
334
|
+
"max_tokens": 100,
|
|
335
|
+
"temperature": 0.3,
|
|
336
|
+
}).encode()
|
|
337
|
+
req = urllib.request.Request(
|
|
338
|
+
"https://api.groq.com/openai/v1/chat/completions",
|
|
339
|
+
data=payload,
|
|
340
|
+
headers={
|
|
341
|
+
"Content-Type": "application/json",
|
|
342
|
+
"Authorization": f"Bearer {GROQ_KEY}",
|
|
343
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
347
|
+
data = json.loads(resp.read())
|
|
348
|
+
return data["choices"][0]["message"]["content"].strip().strip('"')
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _call_ollama(diff: str) -> str:
|
|
352
|
+
import urllib.request
|
|
353
|
+
payload = json.dumps({
|
|
354
|
+
"model": OLLAMA_MODEL,
|
|
355
|
+
"prompt": _build_prompt(diff),
|
|
356
|
+
"stream": False,
|
|
357
|
+
}).encode()
|
|
358
|
+
req = urllib.request.Request(
|
|
359
|
+
f"{OLLAMA_URL}/api/generate",
|
|
360
|
+
data=payload,
|
|
361
|
+
headers={"Content-Type": "application/json"},
|
|
362
|
+
)
|
|
363
|
+
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
364
|
+
data = json.loads(resp.read())
|
|
365
|
+
return data.get("response", "").strip().strip('"')
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def generate_commit_message(diff: str) -> str:
|
|
369
|
+
"""Generate AI commit message pakai spinner, lalu user confirm atau override."""
|
|
370
|
+
ai_msg = ""
|
|
371
|
+
stop = threading.Event()
|
|
372
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
373
|
+
|
|
374
|
+
def _spin():
|
|
375
|
+
i = 0
|
|
376
|
+
while not stop.is_set():
|
|
377
|
+
sys.stdout.write(f"\r \033[2m{frames[i % len(frames)]}\033[0m Generating commit message...")
|
|
378
|
+
sys.stdout.flush()
|
|
379
|
+
time.sleep(0.08)
|
|
380
|
+
i += 1
|
|
381
|
+
|
|
382
|
+
t = threading.Thread(target=_spin, daemon=True)
|
|
383
|
+
t.start()
|
|
384
|
+
try:
|
|
385
|
+
if AI_PROVIDER == "openai":
|
|
386
|
+
if not OPENAI_KEY:
|
|
387
|
+
raise ValueError("OPENAI_API_KEY tidak ada di .env")
|
|
388
|
+
ai_msg = _call_openai(diff)
|
|
389
|
+
elif AI_PROVIDER == "groq":
|
|
390
|
+
if not GROQ_KEY:
|
|
391
|
+
raise ValueError("GROQ_API_KEY tidak ada di .env")
|
|
392
|
+
ai_msg = _call_groq(diff)
|
|
393
|
+
elif AI_PROVIDER == "ollama":
|
|
394
|
+
ai_msg = _call_ollama(diff)
|
|
395
|
+
else:
|
|
396
|
+
raise ValueError(f"AI provider tidak dikenal: {AI_PROVIDER}")
|
|
397
|
+
except Exception as exc:
|
|
398
|
+
ai_msg = ""
|
|
399
|
+
stop.set()
|
|
400
|
+
t.join()
|
|
401
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
402
|
+
sys.stdout.flush()
|
|
403
|
+
console.print(f" [{C_ERR}]✗[/] Generating commit message")
|
|
404
|
+
ui_warn(f"AI gagal: {str(exc)}")
|
|
405
|
+
else:
|
|
406
|
+
stop.set()
|
|
407
|
+
t.join()
|
|
408
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
409
|
+
sys.stdout.flush()
|
|
410
|
+
if ai_msg:
|
|
411
|
+
console.print(f" [{C_OK}]✓[/] Generating commit message")
|
|
412
|
+
|
|
413
|
+
if ai_msg:
|
|
414
|
+
console.print()
|
|
415
|
+
console.print(Panel(
|
|
416
|
+
f"[bold {C_TEXT}]{escape(ai_msg)}[/]",
|
|
417
|
+
title="[bold white] SUGGESTED COMMIT [/]",
|
|
418
|
+
title_align="left",
|
|
419
|
+
border_style=C_LINE,
|
|
420
|
+
box=SQUARE,
|
|
421
|
+
padding=(0, 2),
|
|
422
|
+
))
|
|
423
|
+
console.print()
|
|
424
|
+
try:
|
|
425
|
+
override = pt_prompt(
|
|
426
|
+
HTML('<ansibrightblack> > </ansibrightblack><ansigreen>commit </ansigreen>'),
|
|
427
|
+
style=PT_STYLE,
|
|
428
|
+
placeholder=" press Enter to accept, or type your own message",
|
|
429
|
+
).strip()
|
|
430
|
+
except EOFError:
|
|
431
|
+
return ai_msg
|
|
432
|
+
except KeyboardInterrupt:
|
|
433
|
+
raise
|
|
434
|
+
return override if override else ai_msg
|
|
435
|
+
|
|
436
|
+
# AI failed — manual fallback
|
|
437
|
+
ui_warn("AI tidak bisa generate commit message. Masukkan manual:")
|
|
438
|
+
try:
|
|
439
|
+
msg = pt_prompt(
|
|
440
|
+
HTML('<ansibrightblack> > </ansibrightblack><ansigreen>commit </ansigreen>'),
|
|
441
|
+
style=PT_STYLE,
|
|
442
|
+
placeholder=" e.g. feat(auth): add login endpoint",
|
|
443
|
+
).strip()
|
|
444
|
+
except EOFError:
|
|
445
|
+
msg = ""
|
|
446
|
+
except KeyboardInterrupt:
|
|
447
|
+
raise
|
|
448
|
+
return msg
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
452
|
+
# VALIDATION
|
|
453
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
454
|
+
|
|
455
|
+
def validate_github_url(url: str) -> tuple[bool, str]:
|
|
456
|
+
"""Cek apakah repo GitHub benar-benar ada. Return (valid, pesan_error)."""
|
|
457
|
+
import urllib.request
|
|
458
|
+
import urllib.error
|
|
459
|
+
|
|
460
|
+
# Normalisasi URL — ambil bagian username/repo saja
|
|
461
|
+
url = url.strip().rstrip("/")
|
|
462
|
+
if url.endswith(".git"):
|
|
463
|
+
url = url[:-4]
|
|
464
|
+
|
|
465
|
+
# Ekstrak path dari URL
|
|
466
|
+
# Support: https://github.com/user/repo atau git@github.com:user/repo
|
|
467
|
+
if url.startswith("git@github.com:"):
|
|
468
|
+
path = url.replace("git@github.com:", "")
|
|
469
|
+
elif "github.com/" in url:
|
|
470
|
+
path = url.split("github.com/")[-1]
|
|
471
|
+
else:
|
|
472
|
+
return False, "URL bukan GitHub. Gunakan format https://github.com/username/repo"
|
|
473
|
+
|
|
474
|
+
parts = path.strip("/").split("/")
|
|
475
|
+
if len(parts) < 2 or not parts[0] or not parts[1]:
|
|
476
|
+
return False, "Format URL tidak valid. Contoh: https://github.com/username/repo"
|
|
477
|
+
|
|
478
|
+
api_url = f"https://api.github.com/repos/{parts[0]}/{parts[1]}"
|
|
479
|
+
try:
|
|
480
|
+
req = urllib.request.Request(
|
|
481
|
+
api_url,
|
|
482
|
+
headers={"User-Agent": "AutoDeploy-CLI/1.5"}
|
|
483
|
+
)
|
|
484
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
485
|
+
if resp.status == 200:
|
|
486
|
+
return True, ""
|
|
487
|
+
except urllib.error.HTTPError as e:
|
|
488
|
+
if e.code == 404:
|
|
489
|
+
return False, f"Repo tidak ditemukan: [bold]{parts[0]}/{parts[1]}[/bold]\nBuat repo baru di [cyan]https://github.com/new[/cyan] terlebih dahulu."
|
|
490
|
+
elif e.code == 403:
|
|
491
|
+
# Rate limited tapi repo kemungkinan ada
|
|
492
|
+
return True, ""
|
|
493
|
+
return False, f"GitHub mengembalikan error {e.code}."
|
|
494
|
+
except Exception:
|
|
495
|
+
# Tidak bisa cek (offline, timeout) — lanjut saja
|
|
496
|
+
return True, ""
|
|
497
|
+
|
|
498
|
+
return False, "Tidak bisa memvalidasi URL."
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
502
|
+
# MAIN DEPLOY FLOW
|
|
503
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
504
|
+
|
|
505
|
+
def deploy(github_url: str = ""):
|
|
506
|
+
ui_splash()
|
|
507
|
+
|
|
508
|
+
cwd = os.getcwd()
|
|
509
|
+
is_git_repo = Path(".git").is_dir()
|
|
510
|
+
remote = ""
|
|
511
|
+
branch = DEFAULT_BRANCH
|
|
512
|
+
|
|
513
|
+
# ── Inisialisasi repo baru ────────────────────────────────────────────────
|
|
514
|
+
if not is_git_repo:
|
|
515
|
+
while True:
|
|
516
|
+
if not github_url:
|
|
517
|
+
ui_clear()
|
|
518
|
+
ui_banner()
|
|
519
|
+
console.print()
|
|
520
|
+
console.print(Panel(
|
|
521
|
+
f"[{C_DIM}]Project ini belum punya [/][{C_ACCENT}].git[/][{C_DIM}].[/]\n\n"
|
|
522
|
+
f"[{C_WARN}]![/] [{C_VAL}]Pastikan kamu sudah membuat repo baru di GitHub terlebih dahulu.[/]\n"
|
|
523
|
+
f" [{C_DIM}]Buka [/][{C_ACCENT}]https://github.com/new[/][{C_DIM}] → buat repo → copy URL-nya.[/]\n\n"
|
|
524
|
+
f"[{C_DIM}]Lalu masukkan URL repo tersebut di bawah.[/]",
|
|
525
|
+
title="[bold white] NEW REPOSITORY [/]",
|
|
526
|
+
title_align="left",
|
|
527
|
+
border_style=C_LINE,
|
|
528
|
+
box=SQUARE,
|
|
529
|
+
padding=(1, 2),
|
|
530
|
+
))
|
|
531
|
+
console.print()
|
|
532
|
+
try:
|
|
533
|
+
github_url = pt_prompt(
|
|
534
|
+
HTML('<ansibrightblack> > </ansibrightblack><ansigreen>github url </ansigreen>'),
|
|
535
|
+
style=PT_STYLE,
|
|
536
|
+
placeholder=" https://github.com/username/repo",
|
|
537
|
+
).strip()
|
|
538
|
+
except (KeyboardInterrupt, EOFError):
|
|
539
|
+
raise KeyboardInterrupt
|
|
540
|
+
|
|
541
|
+
if not github_url:
|
|
542
|
+
ui_warn("URL tidak boleh kosong.")
|
|
543
|
+
time.sleep(1)
|
|
544
|
+
continue
|
|
545
|
+
|
|
546
|
+
# Validasi repo
|
|
547
|
+
sys.stdout.write(f"\r \033[2m⠸\033[0m Checking repository...")
|
|
548
|
+
sys.stdout.flush()
|
|
549
|
+
valid, err_msg = validate_github_url(github_url)
|
|
550
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
551
|
+
sys.stdout.flush()
|
|
552
|
+
|
|
553
|
+
if valid:
|
|
554
|
+
console.print(f" [{C_OK}]✓[/] Repository found")
|
|
555
|
+
break
|
|
556
|
+
else:
|
|
557
|
+
# Tampil error, tunggu Enter, lalu ulang dari awal
|
|
558
|
+
console.print()
|
|
559
|
+
console.print(Panel(
|
|
560
|
+
f"[{C_ERR}]✗ Repo tidak ditemukan[/]\n\n"
|
|
561
|
+
f"[{C_TEXT}]{err_msg}[/]",
|
|
562
|
+
border_style=C_ERR,
|
|
563
|
+
box=SQUARE,
|
|
564
|
+
padding=(1, 2),
|
|
565
|
+
))
|
|
566
|
+
console.print()
|
|
567
|
+
try:
|
|
568
|
+
console.print(f" [{C_DIM}]Tekan Enter untuk coba lagi...[/]", end="")
|
|
569
|
+
input()
|
|
570
|
+
except (KeyboardInterrupt, EOFError):
|
|
571
|
+
raise KeyboardInterrupt
|
|
572
|
+
github_url = "" # reset, ulang dari awal
|
|
573
|
+
|
|
574
|
+
if not github_url:
|
|
575
|
+
ui_error("GitHub URL diperlukan untuk repo baru.")
|
|
576
|
+
sys.exit(1)
|
|
577
|
+
|
|
578
|
+
ui_step("git init")
|
|
579
|
+
run(["git", "init"])
|
|
580
|
+
run(["git", "checkout", "-b", DEFAULT_BRANCH])
|
|
581
|
+
|
|
582
|
+
ui_step("git remote add origin", github_url)
|
|
583
|
+
run(["git", "remote", "add", "origin", github_url])
|
|
584
|
+
remote = github_url
|
|
585
|
+
|
|
586
|
+
else:
|
|
587
|
+
remote = git_remote_url()
|
|
588
|
+
branch = git_current_branch()
|
|
589
|
+
|
|
590
|
+
# Repo ada tapi remote belum di-set
|
|
591
|
+
if not remote:
|
|
592
|
+
if not github_url:
|
|
593
|
+
console.print()
|
|
594
|
+
console.print(Panel(
|
|
595
|
+
f"[{C_DIM}]Repo ditemukan tapi belum punya remote origin. "
|
|
596
|
+
f"Masukkan GitHub URL.[/]",
|
|
597
|
+
title="[bold white] SET REMOTE [/]",
|
|
598
|
+
title_align="left",
|
|
599
|
+
border_style=C_LINE,
|
|
600
|
+
box=SQUARE,
|
|
601
|
+
padding=(1, 2),
|
|
602
|
+
))
|
|
603
|
+
console.print()
|
|
604
|
+
try:
|
|
605
|
+
github_url = pt_prompt(
|
|
606
|
+
HTML('<ansibrightblack> > </ansibrightblack><ansigreen>github url </ansigreen>'),
|
|
607
|
+
style=PT_STYLE,
|
|
608
|
+
placeholder=" https://github.com/username/repo",
|
|
609
|
+
).strip()
|
|
610
|
+
except (KeyboardInterrupt, EOFError):
|
|
611
|
+
raise KeyboardInterrupt
|
|
612
|
+
|
|
613
|
+
if not github_url:
|
|
614
|
+
ui_error("GitHub URL diperlukan.")
|
|
615
|
+
sys.exit(1)
|
|
616
|
+
|
|
617
|
+
ui_step("git remote add origin", github_url)
|
|
618
|
+
run(["git", "remote", "add", "origin", github_url])
|
|
619
|
+
remote = github_url
|
|
620
|
+
|
|
621
|
+
branch = git_current_branch()
|
|
622
|
+
|
|
623
|
+
# ── Tampilkan header ──────────────────────────────────────────────────────
|
|
624
|
+
ui_clear()
|
|
625
|
+
ui_banner()
|
|
626
|
+
ui_header(cwd, branch, remote, AI_PROVIDER)
|
|
627
|
+
ui_tips()
|
|
628
|
+
|
|
629
|
+
# ── Cek perubahan ─────────────────────────────────────────────────────────
|
|
630
|
+
if not git_has_changes():
|
|
631
|
+
console.print(Panel(
|
|
632
|
+
f"[{C_DIM}]Semua file sudah up to date. Tidak ada yang perlu di-commit.[/]",
|
|
633
|
+
title=f"[bold {C_WARN}] NO CHANGES [/]",
|
|
634
|
+
title_align="left",
|
|
635
|
+
border_style=C_WARN,
|
|
636
|
+
box=SQUARE,
|
|
637
|
+
padding=(1, 2),
|
|
638
|
+
))
|
|
639
|
+
console.print()
|
|
640
|
+
try:
|
|
641
|
+
console.print(f" [{C_DIM}]Press Enter to exit...[/]", end="")
|
|
642
|
+
input()
|
|
643
|
+
except (KeyboardInterrupt, EOFError):
|
|
644
|
+
pass
|
|
645
|
+
sys.exit(0)
|
|
646
|
+
|
|
647
|
+
# ── git add . ─────────────────────────────────────────────────────────────
|
|
648
|
+
result = spin_run(["git", "add", "."], "Staging changes")
|
|
649
|
+
|
|
650
|
+
# ── Ambil diff ────────────────────────────────────────────────────────────
|
|
651
|
+
diff = git_diff_staged()
|
|
652
|
+
|
|
653
|
+
# ── AI commit message ─────────────────────────────────────────────────────
|
|
654
|
+
commit_msg = generate_commit_message(diff)
|
|
655
|
+
if not commit_msg:
|
|
656
|
+
ui_error("Commit message kosong. Deploy dibatalkan.")
|
|
657
|
+
sys.exit(1)
|
|
658
|
+
console.print()
|
|
659
|
+
|
|
660
|
+
# ── git commit ────────────────────────────────────────────────────────────
|
|
661
|
+
result = spin_run(["git", "commit", "-m", commit_msg], "Creating commit")
|
|
662
|
+
if result.returncode != 0:
|
|
663
|
+
ui_error(f"git commit gagal.\n{(result.stderr or '').strip()}")
|
|
664
|
+
sys.exit(1)
|
|
665
|
+
|
|
666
|
+
# ── git push ──────────────────────────────────────────────────────────────
|
|
667
|
+
result = spin_run(["git", "push", "-u", "origin", branch], "Pushing to GitHub")
|
|
668
|
+
if result.returncode != 0:
|
|
669
|
+
ui_error(
|
|
670
|
+
f"git push gagal.\n\n"
|
|
671
|
+
f"[{C_DIM}]Kemungkinan penyebab:[/]\n"
|
|
672
|
+
f"- Remote URL salah\n"
|
|
673
|
+
f"- Belum ada akses ke repo\n"
|
|
674
|
+
f"- Branch belum ada di remote\n\n"
|
|
675
|
+
f"[{C_DIM}]{escape((result.stderr or '').strip())}[/]"
|
|
676
|
+
)
|
|
677
|
+
sys.exit(1)
|
|
678
|
+
|
|
679
|
+
ui_success(commit_msg, branch, remote)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
683
|
+
# ENTRYPOINT
|
|
684
|
+
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
685
|
+
|
|
686
|
+
def main_cli():
|
|
687
|
+
"""Entrypoint untuk global CLI command `deploy`."""
|
|
688
|
+
github_url = sys.argv[1] if len(sys.argv) > 1 else ""
|
|
689
|
+
try:
|
|
690
|
+
deploy(github_url)
|
|
691
|
+
except KeyboardInterrupt:
|
|
692
|
+
console.print()
|
|
693
|
+
console.print(Align.center(Text("─" * 56, style=C_DIM)))
|
|
694
|
+
console.print(Align.center(Text("Deploy dibatalkan", style=C_DIM)))
|
|
695
|
+
console.print(Align.center(Text("─" * 56, style=C_DIM)))
|
|
696
|
+
console.print()
|
|
697
|
+
sys.exit(0)
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
if __name__ == "__main__":
|
|
701
|
+
main_cli()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "autodeploy-ai"
|
|
7
|
+
version = "1.5.0"
|
|
8
|
+
description = "Git Init · AI Commit · Auto Push — one command from any terminal"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["git", "deploy", "ai", "commit", "cli", "automation"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
21
|
+
"Topic :: Utilities",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"rich>=13.0.0",
|
|
25
|
+
"prompt_toolkit>=3.0.0",
|
|
26
|
+
"pyfiglet>=1.0.0",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[[project.authors]]
|
|
30
|
+
name = "dhiksn"
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/dhiksn/auto-deploy"
|
|
34
|
+
Repository = "https://github.com/dhiksn/auto-deploy"
|
|
35
|
+
"Bug Tracker" = "https://github.com/dhiksn/auto-deploy/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
deploy = "deploy:main_cli"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools]
|
|
41
|
+
py-modules = ["deploy"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
"*" = ["*.md", "*.png"]
|