Skip to content

Commit 728c5b7

Browse files
committed
Feat: Add GitHub and GitLab Connectors to quanuxctl VCS
- Added 'server/cli/src/quanuxctl/lib/vcs_providers.py' to abstract provider APIs. - Implemented 'setup' command to securely store PATs in OS Keyring. - Implemented 'publish' command to create properties on GitHub/GitLab and push to them automatically. - Allows 'native' feel for creating and sharing strategies.
1 parent 6c7bb6d commit 728c5b7

2 files changed

Lines changed: 164 additions & 0 deletions

File tree

server/cli/src/quanuxctl/commands/vcs.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,70 @@ def connect(
8282
"""
8383
run_git_cmd(["remote", "add", name, url])
8484
console.print(f"[green]Connected remote '{name}' to {url}[/green]")
85+
86+
# --- Advanced Connectors ---
87+
88+
@app.command()
89+
def setup(
90+
provider: str = typer.Argument(..., help="Provider name (github, gitlab)"),
91+
token: str = typer.Option(None, prompt=True, hide_input=True, help="Personal Access Token")
92+
):
93+
"""
94+
Configure credentials for a VCS provider (GitHub, GitLab).
95+
"""
96+
from ...lib.vcs_providers import get_provider
97+
98+
prov = get_provider(provider)
99+
if not prov:
100+
console.print(f"[red]Unsupported provider: {provider}[/red]")
101+
return
102+
103+
if token:
104+
prov.set_token(token)
105+
106+
@app.command()
107+
def publish(
108+
provider: str = typer.Argument(..., help="Provider name (github, gitlab)"),
109+
name: str = typer.Option(None, help="Repository name (defaults to current folder)"),
110+
private: bool = typer.Option(True, help="Create as private repository")
111+
):
112+
"""
113+
Create a remote repository on the provider and push the current project to it.
114+
"""
115+
from ...lib.vcs_providers import get_provider
116+
import os
117+
118+
prov = get_provider(provider)
119+
if not prov:
120+
console.print(f"[red]Unsupported provider: {provider}[/red]")
121+
return
122+
123+
if not name:
124+
name = os.path.basename(os.getcwd())
125+
126+
# 1. Create Remote
127+
console.print(f"[bold blue]Creating {provider} repository '{name}'...[/bold blue]")
128+
repo_url = prov.create_repo(name, private)
129+
130+
if repo_url:
131+
# 2. Local Git Init (idempotent)
132+
if not os.path.exists(".git"):
133+
run_git_cmd(["init"])
134+
# Initial add just in case
135+
run_git_cmd(["add", "."])
136+
run_git_cmd(["commit", "-m", "Initial commit via QuanuX"])
137+
138+
# 3. Add Remote
139+
# Check if origin exists
140+
try:
141+
# This will fail if remote exists, handled below
142+
run_git_cmd(["remote", "add", "origin", repo_url])
143+
except Exception:
144+
console.print("[yellow]Remote 'origin' already exists. Setting URL...[/yellow]")
145+
run_git_cmd(["remote", "set-url", "origin", repo_url])
146+
147+
# 4. Push
148+
console.print("[bold blue]Pushing to new remote...[/bold blue]")
149+
# Provide upstream set
150+
run_git_cmd(["push", "-u", "origin", "main"]) # or master, depending on git config, but main is modern standard
151+
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import httpx
2+
import keyring
3+
from rich.console import Console
4+
from typing import Optional
5+
6+
console = Console()
7+
SERVICE_NAME = "QuanuX"
8+
9+
class VCSProvider:
10+
def __init__(self, name: str):
11+
self.name = name.lower()
12+
13+
def get_token(self) -> Optional[str]:
14+
return keyring.get_password(SERVICE_NAME, f"VCS_{self.name.upper()}_TOKEN")
15+
16+
def set_token(self, token: str):
17+
keyring.set_password(SERVICE_NAME, f"VCS_{self.name.upper()}_TOKEN", token)
18+
console.print(f"[green]Token for {self.name} stored securely.[/green]")
19+
20+
def create_repo(self, name: str, private: bool) -> Optional[str]:
21+
raise NotImplementedError
22+
23+
class GitHubProvider(VCSProvider):
24+
def __init__(self):
25+
super().__init__("github")
26+
self.api_url = "https://api.github.com"
27+
28+
def create_repo(self, name: str, private: bool) -> Optional[str]:
29+
token = self.get_token()
30+
if not token:
31+
console.print("[red]No token found. Run 'quanuxctl vcs setup github' first.[/red]")
32+
return None
33+
34+
headers = {
35+
"Authorization": f"token {token}",
36+
"Accept": "application/vnd.github.v3+json"
37+
}
38+
data = {
39+
"name": name,
40+
"private": private,
41+
"description": "Created via QuanuX CLI"
42+
}
43+
44+
try:
45+
with httpx.Client() as client:
46+
response = client.post(f"{self.api_url}/user/repos", json=data, headers=headers)
47+
48+
if response.status_code == 201:
49+
repo_url = response.json().get("clone_url")
50+
console.print(f"[green]Successfully created GitHub repository: {name}[/green]")
51+
return repo_url
52+
else:
53+
console.print(f"[red]Failed to create GitHub repo: {response.text}[/red]")
54+
return None
55+
except Exception as e:
56+
console.print(f"[red]Connection error: {e}[/red]")
57+
return None
58+
59+
class GitLabProvider(VCSProvider):
60+
def __init__(self):
61+
super().__init__("gitlab")
62+
self.api_url = "https://gitlab.com/api/v4"
63+
64+
def create_repo(self, name: str, private: bool) -> Optional[str]:
65+
token = self.get_token()
66+
if not token:
67+
console.print("[red]No token found. Run 'quanuxctl vcs setup gitlab' first.[/red]")
68+
return None
69+
70+
headers = {"Private-Token": token}
71+
data = {
72+
"name": name,
73+
"visibility": "private" if private else "public",
74+
"description": "Created via QuanuX CLI"
75+
}
76+
77+
try:
78+
with httpx.Client() as client:
79+
response = client.post(f"{self.api_url}/projects", json=data, headers=headers)
80+
81+
if response.status_code == 201:
82+
repo_url = response.json().get("http_url_to_repo")
83+
console.print(f"[green]Successfully created GitLab project: {name}[/green]")
84+
return repo_url
85+
else:
86+
console.print(f"[red]Failed to create GitLab project: {response.text}[/red]")
87+
return None
88+
except Exception as e:
89+
console.print(f"[red]Connection error: {e}[/red]")
90+
return None
91+
92+
def get_provider(name: str) -> Optional[VCSProvider]:
93+
if name.lower() == "github":
94+
return GitHubProvider()
95+
elif name.lower() == "gitlab":
96+
return GitLabProvider()
97+
return None

0 commit comments

Comments
 (0)