github-sync-agent 0.1.1__tar.gz → 0.1.3__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.1
3
+ Version: 0.1.3
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
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -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
- root = Path(cfg["projects_root"])
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
- path = root / project_name
161
- if not path.is_dir():
162
- raise ValueError(f"Project '{project_name}' not found in {root}")
163
- return [path]
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
- return sorted(
166
- p for p in root.iterdir()
167
- if p.is_dir() and p.name not in exclude and not p.name.startswith(".")
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
- root = Path(cfg["projects_root"])
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 path in sorted(root.iterdir()):
182
- if not path.is_dir() or path.name in exclude or path.name.startswith("."):
183
- continue
184
- rows.append({
185
- "project": path.name,
186
- "git": is_git_repo(path),
187
- "remote": is_git_repo(path) and has_remote(path),
188
- "changes": has_uncommitted_changes(path) if is_git_repo(path) else False,
189
- "on_github": github_repo_exists(github_user, path.name),
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
@@ -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
- logging.info(f"Syncing {len(projects)} project(s) → github.com/{cfg['github_user']}")
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
- click.echo(f"\nProjects root : {cfg['projects_root']}")
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: sudo apt-get install -y git")
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: sudo apt-get install -y gh")
164
+ click.echo(f" Install it with: {_install_hint('gh')}")
143
165
  sys.exit(1)
144
166
 
145
167
  # ── 3. GitHub authentication ─────────────────────────────────────────────
@@ -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
- for key in ("github_user", "projects_root"):
34
- if not cfg.get(key):
35
- raise ValueError(f"Missing required config key: '{key}'")
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]
@@ -112,8 +112,20 @@ CONFIG_TEMPLATE = """\
112
112
 
113
113
  github_user: {github_user}
114
114
 
115
- # Root folder every subdirectory here is treated as a project to sync
116
- projects_root: {projects_root}
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-sync-agent
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "github-sync-agent"
7
- version = "0.1.1"
7
+ version = "0.1.3"
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.1"