github-sync-agent 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
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.
- github_sync/__init__.py +1 -1
- github_sync/cli.py +37 -13
- github_sync/setup.py +145 -11
- {github_sync_agent-0.1.2.dist-info → github_sync_agent-0.1.4.dist-info}/METADATA +4 -1
- github_sync_agent-0.1.4.dist-info/RECORD +10 -0
- github_sync_agent-0.1.2.dist-info/RECORD +0 -10
- {github_sync_agent-0.1.2.dist-info → github_sync_agent-0.1.4.dist-info}/WHEEL +0 -0
- {github_sync_agent-0.1.2.dist-info → github_sync_agent-0.1.4.dist-info}/entry_points.txt +0 -0
- {github_sync_agent-0.1.2.dist-info → github_sync_agent-0.1.4.dist-info}/top_level.txt +0 -0
github_sync/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.4"
|
github_sync/cli.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import platform
|
|
2
3
|
import sys
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
@@ -35,10 +36,16 @@ def _setup_logging(cfg: dict) -> None:
|
|
|
35
36
|
def cli(ctx: click.Context, config_path: Path | None) -> None:
|
|
36
37
|
"""GitHub Sync Agent — sync local projects to GitHub."""
|
|
37
38
|
ctx.ensure_object(dict)
|
|
39
|
+
|
|
40
|
+
# init creates the config — it must not require one to exist first
|
|
41
|
+
if ctx.invoked_subcommand == "init":
|
|
42
|
+
return
|
|
43
|
+
|
|
38
44
|
try:
|
|
39
45
|
ctx.obj["cfg"] = config.load(config_path)
|
|
40
46
|
except (FileNotFoundError, ValueError) as exc:
|
|
41
47
|
click.echo(f"Error: {exc}", err=True)
|
|
48
|
+
click.echo("Run 'github-sync init' to create a config file.", err=True)
|
|
42
49
|
sys.exit(1)
|
|
43
50
|
|
|
44
51
|
|
|
@@ -123,6 +130,15 @@ def status(ctx: click.Context) -> None:
|
|
|
123
130
|
# init
|
|
124
131
|
# ---------------------------------------------------------------------------
|
|
125
132
|
|
|
133
|
+
def _install_hint(tool: str) -> str:
|
|
134
|
+
system = platform.system()
|
|
135
|
+
if system == "Darwin":
|
|
136
|
+
return f"brew install {tool}"
|
|
137
|
+
if system == "Linux":
|
|
138
|
+
return f"sudo apt-get install -y {tool}"
|
|
139
|
+
return f"Install {tool} from your system package manager"
|
|
140
|
+
|
|
141
|
+
|
|
126
142
|
@cli.command()
|
|
127
143
|
def init() -> None:
|
|
128
144
|
"""Interactive setup wizard — run this first on a new machine."""
|
|
@@ -136,7 +152,7 @@ def init() -> None:
|
|
|
136
152
|
click.echo(f" ✓ git found: {git_ver}")
|
|
137
153
|
else:
|
|
138
154
|
click.echo(click.style(" ✗ git is not installed.", fg="red"))
|
|
139
|
-
click.echo(" Install it with:
|
|
155
|
+
click.echo(f" Install it with: {_install_hint('git')}")
|
|
140
156
|
sys.exit(1)
|
|
141
157
|
|
|
142
158
|
# ── 2. Check gh ─────────────────────────────────────────────────────────
|
|
@@ -145,7 +161,7 @@ def init() -> None:
|
|
|
145
161
|
click.echo(f" ✓ gh found: {gh_ver}")
|
|
146
162
|
else:
|
|
147
163
|
click.echo(click.style(" ✗ gh (GitHub CLI) is not installed.", fg="red"))
|
|
148
|
-
click.echo(" Install it with:
|
|
164
|
+
click.echo(f" Install it with: {_install_hint('gh')}")
|
|
149
165
|
sys.exit(1)
|
|
150
166
|
|
|
151
167
|
# ── 3. GitHub authentication ─────────────────────────────────────────────
|
|
@@ -191,8 +207,8 @@ def init() -> None:
|
|
|
191
207
|
|
|
192
208
|
click.echo()
|
|
193
209
|
|
|
194
|
-
# ── 6. SSH
|
|
195
|
-
click.echo(click.style(" Setting up SSH
|
|
210
|
+
# ── 6. SSH setup + verification ─────────────────────────────────────────
|
|
211
|
+
click.echo(click.style(" Setting up SSH for GitHub...", bold=True))
|
|
196
212
|
|
|
197
213
|
if not setup.ssh_key_exists():
|
|
198
214
|
click.echo(" No SSH key found — generating one...")
|
|
@@ -204,17 +220,25 @@ def init() -> None:
|
|
|
204
220
|
else:
|
|
205
221
|
click.echo(" ✓ SSH key already exists")
|
|
206
222
|
|
|
207
|
-
setup.
|
|
223
|
+
setup.add_github_host_keys()
|
|
208
224
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
225
|
+
click.echo(" Verifying SSH connection to GitHub...")
|
|
226
|
+
ok, msg, fixes = setup.ensure_github_ssh()
|
|
227
|
+
|
|
228
|
+
for fix in fixes:
|
|
229
|
+
click.echo(f" → applied fix: {fix}")
|
|
230
|
+
|
|
231
|
+
if ok:
|
|
232
|
+
user = setup.ssh_auth_username(msg) or github_user
|
|
233
|
+
click.echo(f" ✓ SSH verified as {user}")
|
|
216
234
|
else:
|
|
217
|
-
click.echo("
|
|
235
|
+
click.echo(click.style(" ✗ SSH verification failed.", fg="red"))
|
|
236
|
+
click.echo(f" {msg}")
|
|
237
|
+
click.echo()
|
|
238
|
+
click.echo(" Manual checks:")
|
|
239
|
+
click.echo(" ssh -T git@github.com")
|
|
240
|
+
click.echo(" gh ssh-key add ~/.ssh/id_ed25519.pub")
|
|
241
|
+
sys.exit(1)
|
|
218
242
|
|
|
219
243
|
click.echo()
|
|
220
244
|
|
github_sync/setup.py
CHANGED
|
@@ -3,6 +3,8 @@ Setup helpers used by `github-sync init`.
|
|
|
3
3
|
Checks prerequisites, detects credentials, sets up SSH.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import platform
|
|
7
|
+
import re
|
|
6
8
|
import shutil
|
|
7
9
|
import subprocess
|
|
8
10
|
from pathlib import Path
|
|
@@ -49,6 +51,16 @@ def get_github_username() -> str | None:
|
|
|
49
51
|
# ---------------------------------------------------------------------------
|
|
50
52
|
|
|
51
53
|
DEFAULT_KEY = Path.home() / ".ssh" / "id_ed25519"
|
|
54
|
+
SSH_CONFIG = Path.home() / ".ssh" / "config"
|
|
55
|
+
|
|
56
|
+
GITHUB_PORT443_BLOCK = """
|
|
57
|
+
# Added by github-sync — use port 443 when port 22 is blocked
|
|
58
|
+
Host github.com
|
|
59
|
+
HostName ssh.github.com
|
|
60
|
+
Port 443
|
|
61
|
+
User git
|
|
62
|
+
IdentityFile ~/.ssh/id_ed25519
|
|
63
|
+
"""
|
|
52
64
|
|
|
53
65
|
|
|
54
66
|
def ssh_key_exists() -> bool:
|
|
@@ -56,6 +68,7 @@ def ssh_key_exists() -> bool:
|
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
def generate_ssh_key(passphrase: str = "") -> bool:
|
|
71
|
+
Path.home().joinpath(".ssh").mkdir(mode=0o700, exist_ok=True)
|
|
59
72
|
result = subprocess.run(
|
|
60
73
|
["ssh-keygen", "-t", "ed25519", "-C", "github-sync", "-f", str(DEFAULT_KEY), "-N", passphrase],
|
|
61
74
|
capture_output=True, text=True,
|
|
@@ -63,29 +76,101 @@ def generate_ssh_key(passphrase: str = "") -> bool:
|
|
|
63
76
|
return result.returncode == 0
|
|
64
77
|
|
|
65
78
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)
|
|
71
|
-
|
|
79
|
+
def _append_known_hosts(hostname: str, port: int | None = None) -> bool:
|
|
80
|
+
cmd = ["ssh-keyscan", "-t", "ed25519"]
|
|
81
|
+
if port:
|
|
82
|
+
cmd.extend(["-p", str(port)])
|
|
83
|
+
cmd.append(hostname)
|
|
84
|
+
|
|
85
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
86
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
72
87
|
return False
|
|
88
|
+
|
|
73
89
|
known_hosts = Path.home() / ".ssh" / "known_hosts"
|
|
74
90
|
known_hosts.parent.mkdir(mode=0o700, exist_ok=True)
|
|
75
91
|
existing = known_hosts.read_text() if known_hosts.exists() else ""
|
|
76
|
-
if
|
|
92
|
+
if hostname not in existing:
|
|
77
93
|
with open(known_hosts, "a") as f:
|
|
78
94
|
f.write(result.stdout)
|
|
79
95
|
return True
|
|
80
96
|
|
|
81
97
|
|
|
82
|
-
def
|
|
83
|
-
"""
|
|
98
|
+
def add_github_host_keys() -> None:
|
|
99
|
+
"""Trust GitHub host keys for both port 22 and port 443."""
|
|
100
|
+
_append_known_hosts("github.com")
|
|
101
|
+
_append_known_hosts("ssh.github.com", port=443)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_github_ssh() -> tuple[bool, str]:
|
|
105
|
+
"""Test SSH auth to GitHub. Returns (success, combined output)."""
|
|
84
106
|
result = subprocess.run(
|
|
85
|
-
[
|
|
107
|
+
[
|
|
108
|
+
"ssh", "-T",
|
|
109
|
+
"-o", "BatchMode=yes",
|
|
110
|
+
"-o", "ConnectTimeout=15",
|
|
111
|
+
"-o", "StrictHostKeyChecking=accept-new",
|
|
112
|
+
"git@github.com",
|
|
113
|
+
],
|
|
86
114
|
capture_output=True, text=True,
|
|
87
115
|
)
|
|
88
|
-
|
|
116
|
+
output = (result.stdout + result.stderr).strip()
|
|
117
|
+
ok = "successfully authenticated" in output.lower()
|
|
118
|
+
return ok, output
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def ssh_auth_username(output: str) -> str | None:
|
|
122
|
+
match = re.search(r"Hi ([^!]+)!", output)
|
|
123
|
+
return match.group(1) if match else None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def is_ssh_connection_error(message: str) -> bool:
|
|
127
|
+
lower = message.lower()
|
|
128
|
+
return any(
|
|
129
|
+
phrase in lower
|
|
130
|
+
for phrase in ("connection closed", "connection reset", "timed out", "network is unreachable")
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def is_ssh_auth_error(message: str) -> bool:
|
|
135
|
+
lower = message.lower()
|
|
136
|
+
return "permission denied" in lower or "publickey" in lower
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def has_github_port443_config() -> bool:
|
|
140
|
+
if not SSH_CONFIG.exists():
|
|
141
|
+
return False
|
|
142
|
+
content = SSH_CONFIG.read_text()
|
|
143
|
+
return "ssh.github.com" in content and "443" in content
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def configure_github_ssh_port_443() -> bool:
|
|
147
|
+
"""Configure ~/.ssh/config to reach GitHub over port 443."""
|
|
148
|
+
if has_github_port443_config():
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
SSH_CONFIG.parent.mkdir(mode=0o700, exist_ok=True)
|
|
152
|
+
with open(SSH_CONFIG, "a") as f:
|
|
153
|
+
f.write(GITHUB_PORT443_BLOCK)
|
|
154
|
+
SSH_CONFIG.chmod(0o600)
|
|
155
|
+
_append_known_hosts("ssh.github.com", port=443)
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def add_ssh_key_to_agent() -> bool:
|
|
160
|
+
"""Load the default key into ssh-agent (required on some macOS setups)."""
|
|
161
|
+
if not DEFAULT_KEY.exists():
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
commands = []
|
|
165
|
+
if platform.system() == "Darwin":
|
|
166
|
+
commands.append(["ssh-add", "--apple-use-keychain", str(DEFAULT_KEY)])
|
|
167
|
+
commands.append(["ssh-add", str(DEFAULT_KEY)])
|
|
168
|
+
|
|
169
|
+
for cmd in commands:
|
|
170
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
171
|
+
if result.returncode == 0:
|
|
172
|
+
return True
|
|
173
|
+
return False
|
|
89
174
|
|
|
90
175
|
|
|
91
176
|
def upload_ssh_key_to_github(title: str = "github-sync") -> bool:
|
|
@@ -99,6 +184,55 @@ def upload_ssh_key_to_github(title: str = "github-sync") -> bool:
|
|
|
99
184
|
return result.returncode == 0
|
|
100
185
|
|
|
101
186
|
|
|
187
|
+
def ssh_key_on_github() -> bool:
|
|
188
|
+
"""Back-compat wrapper."""
|
|
189
|
+
ok, _ = test_github_ssh()
|
|
190
|
+
return ok
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def ensure_github_ssh() -> tuple[bool, str, list[str]]:
|
|
194
|
+
"""
|
|
195
|
+
Verify GitHub SSH works; apply fixes as needed.
|
|
196
|
+
Returns (success, last_message, fixes_applied).
|
|
197
|
+
"""
|
|
198
|
+
fixes: list[str] = []
|
|
199
|
+
|
|
200
|
+
ok, msg = test_github_ssh()
|
|
201
|
+
if ok:
|
|
202
|
+
return True, msg, fixes
|
|
203
|
+
|
|
204
|
+
if is_ssh_connection_error(msg):
|
|
205
|
+
configure_github_ssh_port_443()
|
|
206
|
+
fixes.append("configured port 443")
|
|
207
|
+
ok, msg = test_github_ssh()
|
|
208
|
+
if ok:
|
|
209
|
+
return True, msg, fixes
|
|
210
|
+
|
|
211
|
+
if is_ssh_auth_error(msg):
|
|
212
|
+
if upload_ssh_key_to_github():
|
|
213
|
+
fixes.append("uploaded SSH key")
|
|
214
|
+
ok, msg = test_github_ssh()
|
|
215
|
+
if ok:
|
|
216
|
+
return True, msg, fixes
|
|
217
|
+
|
|
218
|
+
if add_ssh_key_to_agent():
|
|
219
|
+
fixes.append("loaded key into ssh-agent")
|
|
220
|
+
ok, msg = test_github_ssh()
|
|
221
|
+
if ok:
|
|
222
|
+
return True, msg, fixes
|
|
223
|
+
|
|
224
|
+
# Last resort: port 443 + agent together
|
|
225
|
+
if is_ssh_connection_error(msg) and not has_github_port443_config():
|
|
226
|
+
configure_github_ssh_port_443()
|
|
227
|
+
add_ssh_key_to_agent()
|
|
228
|
+
fixes.append("port 443 + ssh-agent")
|
|
229
|
+
ok, msg = test_github_ssh()
|
|
230
|
+
if ok:
|
|
231
|
+
return True, msg, fixes
|
|
232
|
+
|
|
233
|
+
return False, msg, fixes
|
|
234
|
+
|
|
235
|
+
|
|
102
236
|
# ---------------------------------------------------------------------------
|
|
103
237
|
# Config writer
|
|
104
238
|
# ---------------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-sync-agent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Sync all your local projects to GitHub automatically
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/yuvalavni/Agents
|
|
@@ -49,8 +49,11 @@ The wizard will:
|
|
|
49
49
|
- Authenticate with GitHub (`gh auth login`)
|
|
50
50
|
- Ask which folder to scan for projects
|
|
51
51
|
- Set up your SSH key automatically
|
|
52
|
+
- **Verify SSH works** (auto-fixes port 443 and ssh-agent if needed)
|
|
52
53
|
- Write a config file to `~/.config/github-sync/config.yaml`
|
|
53
54
|
|
|
55
|
+
Setup will **not** complete until `ssh -T git@github.com` succeeds.
|
|
56
|
+
|
|
54
57
|
---
|
|
55
58
|
|
|
56
59
|
## Usage
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
github_sync/__init__.py,sha256=Wzf5T3NBDfhQoTnhnRNHSlAsE0XMqbclXG-M81Vas70,22
|
|
2
|
+
github_sync/agent.py,sha256=2WnJCwupDwT3C9bNZLoj2CAvZW-eoqZChqFTk4wNRKw,6692
|
|
3
|
+
github_sync/cli.py,sha256=72iJIhveIXoYJUdi375iazVf21Q1RAaCwqgaX-VRJJk,9976
|
|
4
|
+
github_sync/config.py,sha256=BQrSW7DyhcZB6gZnDKkc-W6u1vtD-l76Rn_gW8Egypk,1444
|
|
5
|
+
github_sync/setup.py,sha256=8QS0bBjp1dZfUlfxmnoNu87ULDsUxDzqTyVG7zuXtuE,8504
|
|
6
|
+
github_sync_agent-0.1.4.dist-info/METADATA,sha256=IU6Z_jprTGtNZq6kKO47jrXzCzRTaJkaTWqQlQ0tCfA,2782
|
|
7
|
+
github_sync_agent-0.1.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
+
github_sync_agent-0.1.4.dist-info/entry_points.txt,sha256=-LmJUTvRzj5cvyS-dEWdipouBj3dgkW6hqrTz1GmyGw,53
|
|
9
|
+
github_sync_agent-0.1.4.dist-info/top_level.txt,sha256=fPYTA83USK1N4JwfouV8zcejgWfWmqRAKQVDCAT8a5I,12
|
|
10
|
+
github_sync_agent-0.1.4.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
github_sync/__init__.py,sha256=YvuYzWnKtqBb-IqG8HAu-nhIYAsgj9Vmc_b9o7vO-js,22
|
|
2
|
-
github_sync/agent.py,sha256=2WnJCwupDwT3C9bNZLoj2CAvZW-eoqZChqFTk4wNRKw,6692
|
|
3
|
-
github_sync/cli.py,sha256=ndmfTQhK4KbtVEjXWuOg4RLs3UVFpgb0rZAf_YZFWgs,9336
|
|
4
|
-
github_sync/config.py,sha256=BQrSW7DyhcZB6gZnDKkc-W6u1vtD-l76Rn_gW8Egypk,1444
|
|
5
|
-
github_sync/setup.py,sha256=2ZO0ZCCxQ_LyVSdQbWrObwjjBQCDLqdu_IHplUqW0qU,4720
|
|
6
|
-
github_sync_agent-0.1.2.dist-info/METADATA,sha256=u4dfGfpiUf-TQgtldf043Wj1NSZFvTy8fJG2bYj4dFo,2644
|
|
7
|
-
github_sync_agent-0.1.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
-
github_sync_agent-0.1.2.dist-info/entry_points.txt,sha256=-LmJUTvRzj5cvyS-dEWdipouBj3dgkW6hqrTz1GmyGw,53
|
|
9
|
-
github_sync_agent-0.1.2.dist-info/top_level.txt,sha256=fPYTA83USK1N4JwfouV8zcejgWfWmqRAKQVDCAT8a5I,12
|
|
10
|
-
github_sync_agent-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|