github-sync-agent 0.1.1__py3-none-any.whl → 0.1.3__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/agent.py +26 -20
- github_sync/cli.py +27 -5
- github_sync/config.py +10 -6
- github_sync/setup.py +14 -2
- {github_sync_agent-0.1.1.dist-info → github_sync_agent-0.1.3.dist-info}/METADATA +1 -1
- github_sync_agent-0.1.3.dist-info/RECORD +10 -0
- github_sync_agent-0.1.1.dist-info/RECORD +0 -10
- {github_sync_agent-0.1.1.dist-info → github_sync_agent-0.1.3.dist-info}/WHEEL +0 -0
- {github_sync_agent-0.1.1.dist-info → github_sync_agent-0.1.3.dist-info}/entry_points.txt +0 -0
- {github_sync_agent-0.1.1.dist-info → github_sync_agent-0.1.3.dist-info}/top_level.txt +0 -0
github_sync/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.3"
|
github_sync/agent.py
CHANGED
|
@@ -153,19 +153,23 @@ def sync_project(path: Path, cfg: dict, dry_run: bool) -> bool:
|
|
|
153
153
|
# ---------------------------------------------------------------------------
|
|
154
154
|
|
|
155
155
|
def discover_projects(cfg: dict, project_name: str | None = None) -> list[Path]:
|
|
156
|
-
|
|
156
|
+
roots = [Path(r) for r in cfg["projects_root"]]
|
|
157
157
|
exclude = set(cfg.get("exclude", []))
|
|
158
158
|
|
|
159
159
|
if project_name:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
for root in roots:
|
|
161
|
+
path = root / project_name
|
|
162
|
+
if path.is_dir():
|
|
163
|
+
return [path]
|
|
164
|
+
searched = ", ".join(str(r) for r in roots)
|
|
165
|
+
raise ValueError(f"Project '{project_name}' not found in: {searched}")
|
|
164
166
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
projects = []
|
|
168
|
+
for root in roots:
|
|
169
|
+
for p in sorted(root.iterdir()):
|
|
170
|
+
if p.is_dir() and p.name not in exclude and not p.name.startswith("."):
|
|
171
|
+
projects.append(p)
|
|
172
|
+
return projects
|
|
169
173
|
|
|
170
174
|
|
|
171
175
|
# ---------------------------------------------------------------------------
|
|
@@ -173,20 +177,22 @@ def discover_projects(cfg: dict, project_name: str | None = None) -> list[Path]:
|
|
|
173
177
|
# ---------------------------------------------------------------------------
|
|
174
178
|
|
|
175
179
|
def get_status(cfg: dict) -> list[dict]:
|
|
176
|
-
|
|
180
|
+
roots = [Path(r) for r in cfg["projects_root"]]
|
|
177
181
|
exclude = set(cfg.get("exclude", []))
|
|
178
182
|
github_user = cfg["github_user"]
|
|
179
183
|
rows = []
|
|
180
184
|
|
|
181
|
-
for
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
185
|
+
for root in roots:
|
|
186
|
+
for path in sorted(root.iterdir()):
|
|
187
|
+
if not path.is_dir() or path.name in exclude or path.name.startswith("."):
|
|
188
|
+
continue
|
|
189
|
+
rows.append({
|
|
190
|
+
"project": path.name,
|
|
191
|
+
"root": str(root),
|
|
192
|
+
"git": is_git_repo(path),
|
|
193
|
+
"remote": is_git_repo(path) and has_remote(path),
|
|
194
|
+
"changes": has_uncommitted_changes(path) if is_git_repo(path) else False,
|
|
195
|
+
"on_github": github_repo_exists(github_user, path.name),
|
|
196
|
+
})
|
|
191
197
|
|
|
192
198
|
return rows
|
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
|
|
|
@@ -66,7 +73,9 @@ def sync(ctx: click.Context, project: str | None, dry_run: bool) -> None:
|
|
|
66
73
|
click.echo(f"Error: {exc}", err=True)
|
|
67
74
|
sys.exit(1)
|
|
68
75
|
|
|
69
|
-
|
|
76
|
+
roots = cfg['projects_root']
|
|
77
|
+
roots_str = roots[0] if len(roots) == 1 else f"{len(roots)} roots"
|
|
78
|
+
logging.info(f"Syncing {len(projects)} project(s) from {roots_str} → github.com/{cfg['github_user']}")
|
|
70
79
|
|
|
71
80
|
results = []
|
|
72
81
|
for path in projects:
|
|
@@ -95,16 +104,20 @@ def status(ctx: click.Context) -> None:
|
|
|
95
104
|
cfg = ctx.obj["cfg"]
|
|
96
105
|
rows = agent.get_status(cfg)
|
|
97
106
|
|
|
98
|
-
|
|
107
|
+
roots = cfg['projects_root']
|
|
108
|
+
click.echo(f"\nProjects root : {roots[0] if len(roots) == 1 else str(roots)}")
|
|
99
109
|
click.echo(f"GitHub user : {cfg['github_user']}\n")
|
|
100
110
|
|
|
101
|
-
header = f"{'Project':<25} {'Git':<6} {'Remote':<8} {'Changes':<10} {'On GitHub'}"
|
|
111
|
+
header = f"{'Project':<25} {'Root':<30} {'Git':<6} {'Remote':<8} {'Changes':<10} {'On GitHub'}"
|
|
102
112
|
click.echo(header)
|
|
103
113
|
click.echo("─" * len(header))
|
|
104
114
|
|
|
105
115
|
for row in rows:
|
|
116
|
+
# Show only the last part of root to keep table readable
|
|
117
|
+
root_short = Path(row['root']).name
|
|
106
118
|
click.echo(
|
|
107
119
|
f"{row['project']:<25} "
|
|
120
|
+
f"{root_short:<30} "
|
|
108
121
|
f"{'✓' if row['git'] else '✗':<6} "
|
|
109
122
|
f"{'✓' if row['remote'] else '✗':<8} "
|
|
110
123
|
f"{'dirty' if row['changes'] else 'clean':<10} "
|
|
@@ -117,6 +130,15 @@ def status(ctx: click.Context) -> None:
|
|
|
117
130
|
# init
|
|
118
131
|
# ---------------------------------------------------------------------------
|
|
119
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
|
+
|
|
120
142
|
@cli.command()
|
|
121
143
|
def init() -> None:
|
|
122
144
|
"""Interactive setup wizard — run this first on a new machine."""
|
|
@@ -130,7 +152,7 @@ def init() -> None:
|
|
|
130
152
|
click.echo(f" ✓ git found: {git_ver}")
|
|
131
153
|
else:
|
|
132
154
|
click.echo(click.style(" ✗ git is not installed.", fg="red"))
|
|
133
|
-
click.echo(" Install it with:
|
|
155
|
+
click.echo(f" Install it with: {_install_hint('git')}")
|
|
134
156
|
sys.exit(1)
|
|
135
157
|
|
|
136
158
|
# ── 2. Check gh ─────────────────────────────────────────────────────────
|
|
@@ -139,7 +161,7 @@ def init() -> None:
|
|
|
139
161
|
click.echo(f" ✓ gh found: {gh_ver}")
|
|
140
162
|
else:
|
|
141
163
|
click.echo(click.style(" ✗ gh (GitHub CLI) is not installed.", fg="red"))
|
|
142
|
-
click.echo(" Install it with:
|
|
164
|
+
click.echo(f" Install it with: {_install_hint('gh')}")
|
|
143
165
|
sys.exit(1)
|
|
144
166
|
|
|
145
167
|
# ── 3. GitHub authentication ─────────────────────────────────────────────
|
github_sync/config.py
CHANGED
|
@@ -26,10 +26,14 @@ def _apply_defaults(cfg: dict) -> None:
|
|
|
26
26
|
cfg.setdefault("exclude", [])
|
|
27
27
|
cfg.setdefault("log_file", str(Path.home() / ".local" / "share" / "github-sync" / "github-sync.log"))
|
|
28
28
|
cfg.setdefault("log_level", "INFO")
|
|
29
|
-
# Expand ~ in projects_root
|
|
30
|
-
if "projects_root" in cfg:
|
|
31
|
-
cfg["projects_root"] = str(Path(cfg["projects_root"]).expanduser())
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
if not cfg.get("github_user"):
|
|
31
|
+
raise ValueError("Missing required config key: 'github_user'")
|
|
32
|
+
|
|
33
|
+
# Normalize projects_root to a list of expanded absolute paths
|
|
34
|
+
raw = cfg.get("projects_root")
|
|
35
|
+
if not raw:
|
|
36
|
+
raise ValueError("Missing required config key: 'projects_root'")
|
|
37
|
+
if isinstance(raw, str):
|
|
38
|
+
raw = [raw]
|
|
39
|
+
cfg["projects_root"] = [str(Path(p).expanduser().resolve()) for p in raw]
|
github_sync/setup.py
CHANGED
|
@@ -112,8 +112,20 @@ CONFIG_TEMPLATE = """\
|
|
|
112
112
|
|
|
113
113
|
github_user: {github_user}
|
|
114
114
|
|
|
115
|
-
#
|
|
116
|
-
|
|
115
|
+
# Directories to scan for projects.
|
|
116
|
+
# Every subdirectory inside each root is treated as a project to sync.
|
|
117
|
+
# You can specify one path (string) or multiple paths (list):
|
|
118
|
+
#
|
|
119
|
+
# Single path:
|
|
120
|
+
# projects_root: ~/projects
|
|
121
|
+
#
|
|
122
|
+
# Multiple paths:
|
|
123
|
+
# projects_root:
|
|
124
|
+
# - ~/projects
|
|
125
|
+
# - ~/work
|
|
126
|
+
# - /opt/company-code
|
|
127
|
+
projects_root:
|
|
128
|
+
- {projects_root}
|
|
117
129
|
|
|
118
130
|
# Daily schedule time (used by the systemd timer / cron)
|
|
119
131
|
schedule_time: "02:00"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
github_sync/__init__.py,sha256=XEqb2aiIn8fzGE68Mph4ck1FtQqsR_am0wRWvrYPffQ,22
|
|
2
|
+
github_sync/agent.py,sha256=2WnJCwupDwT3C9bNZLoj2CAvZW-eoqZChqFTk4wNRKw,6692
|
|
3
|
+
github_sync/cli.py,sha256=xsbYJXhlSGSDgEOlTm43XcDSdn5M83u2uz6UuzfG5s0,9827
|
|
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.3.dist-info/METADATA,sha256=_FnEkvuZNeHDyHSojYdDEg6z-cxLbVWlwn6Upf3jIxo,2644
|
|
7
|
+
github_sync_agent-0.1.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
+
github_sync_agent-0.1.3.dist-info/entry_points.txt,sha256=-LmJUTvRzj5cvyS-dEWdipouBj3dgkW6hqrTz1GmyGw,53
|
|
9
|
+
github_sync_agent-0.1.3.dist-info/top_level.txt,sha256=fPYTA83USK1N4JwfouV8zcejgWfWmqRAKQVDCAT8a5I,12
|
|
10
|
+
github_sync_agent-0.1.3.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
github_sync/__init__.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
|
|
2
|
-
github_sync/agent.py,sha256=E21j6AoXXnmtEWKV6LjECMhyfQ78PY9ifyWBKUga3Xk,6391
|
|
3
|
-
github_sync/cli.py,sha256=GFakiYck02S7ZxsKfVqO_bi2Gc427B87kAgzvMUpKLs,9004
|
|
4
|
-
github_sync/config.py,sha256=zywBw0AkDzk1wHamyJ-sbss3DMAXWq7fZeoDKR1fE_s,1306
|
|
5
|
-
github_sync/setup.py,sha256=keV0bBTgADASGBOCmd6Jv8eXqIg_g0uxbBIYpCHaMxA,4461
|
|
6
|
-
github_sync_agent-0.1.1.dist-info/METADATA,sha256=ZRT3bRa67CNfojl4tY4yED3xqxviAbEmPVOCvktWQOU,2644
|
|
7
|
-
github_sync_agent-0.1.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
8
|
-
github_sync_agent-0.1.1.dist-info/entry_points.txt,sha256=-LmJUTvRzj5cvyS-dEWdipouBj3dgkW6hqrTz1GmyGw,53
|
|
9
|
-
github_sync_agent-0.1.1.dist-info/top_level.txt,sha256=fPYTA83USK1N4JwfouV8zcejgWfWmqRAKQVDCAT8a5I,12
|
|
10
|
-
github_sync_agent-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|