github-sync-agent 0.1.3__tar.gz → 0.1.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-sync-agent
3
- Version: 0.1.3
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
@@ -30,8 +30,11 @@ The wizard will:
30
30
  - Authenticate with GitHub (`gh auth login`)
31
31
  - Ask which folder to scan for projects
32
32
  - Set up your SSH key automatically
33
+ - **Verify SSH works** (auto-fixes port 443 and ssh-agent if needed)
33
34
  - Write a config file to `~/.config/github-sync/config.yaml`
34
35
 
36
+ Setup will **not** complete until `ssh -T git@github.com` succeeds.
37
+
35
38
  ---
36
39
 
37
40
  ## Usage
@@ -0,0 +1 @@
1
+ __version__ = "0.1.4"
@@ -207,8 +207,8 @@ def init() -> None:
207
207
 
208
208
  click.echo()
209
209
 
210
- # ── 6. SSH key setup ────────────────────────────────────────────────────
211
- click.echo(click.style(" Setting up SSH key for GitHub...", bold=True))
210
+ # ── 6. SSH setup + verification ─────────────────────────────────────────
211
+ click.echo(click.style(" Setting up SSH for GitHub...", bold=True))
212
212
 
213
213
  if not setup.ssh_key_exists():
214
214
  click.echo(" No SSH key found — generating one...")
@@ -220,17 +220,25 @@ def init() -> None:
220
220
  else:
221
221
  click.echo(" ✓ SSH key already exists")
222
222
 
223
- setup.add_github_host_key()
223
+ setup.add_github_host_keys()
224
224
 
225
- if not setup.ssh_key_on_github():
226
- click.echo(" Uploading SSH key to GitHub...")
227
- if setup.upload_ssh_key_to_github():
228
- click.echo(" ✓ SSH key added to GitHub")
229
- else:
230
- click.echo(click.style(" ✗ Failed to upload SSH key.", fg="yellow"))
231
- click.echo(" Run manually: gh ssh-key add ~/.ssh/id_ed25519.pub")
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}")
232
234
  else:
233
- click.echo(" SSH key already on GitHub")
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)
234
242
 
235
243
  click.echo()
236
244
 
@@ -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 add_github_host_key() -> bool:
67
- result = subprocess.run(
68
- ["ssh-keyscan", "-t", "ed25519", "github.com"],
69
- capture_output=True, text=True,
70
- )
71
- if result.returncode != 0:
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 "github.com" not in existing:
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 ssh_key_on_github() -> bool:
83
- """Test if the current SSH key authenticates to GitHub."""
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
- ["ssh", "-T", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "git@github.com"],
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
- return "successfully authenticated" in (result.stdout + result.stderr)
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
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "github-sync-agent"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Sync all your local projects to GitHub automatically"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1 +0,0 @@
1
- __version__ = "0.1.3"