navig-github 0.2.0__tar.gz → 0.2.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.
- {navig_github-0.2.0 → navig_github-0.2.4}/PKG-INFO +15 -1
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/commands/github.py +15 -6
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/analytics.py +37 -15
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/cli.py +61 -45
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/config.py +63 -19
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/diff.py +12 -8
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/gists.py +2 -2
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/git_utils.py +17 -13
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/incremental.py +17 -9
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/mirror.py +32 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/notifications.py +18 -9
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/scheduler.py +90 -26
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/templates.py +54 -20
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/verify.py +12 -6
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github.egg-info/PKG-INFO +15 -1
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github.egg-info/SOURCES.txt +9 -1
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github.egg-info/requires.txt +1 -0
- navig_github-0.2.4/pyproject.toml +54 -0
- navig_github-0.2.4/tests/test_analytics_no_wipe.py +58 -0
- navig_github-0.2.4/tests/test_backup_history_wire.py +67 -0
- navig_github-0.2.4/tests/test_config.py +81 -0
- navig_github-0.2.4/tests/test_debrand_stores.py +55 -0
- navig_github-0.2.4/tests/test_incremental.py +45 -0
- navig_github-0.2.4/tests/test_scheduler_migration.py +42 -0
- navig_github-0.2.4/tests/test_scheduler_no_wipe.py +150 -0
- navig_github-0.2.4/tests/test_templates_no_wipe.py +73 -0
- navig_github-0.2.0/pyproject.toml +0 -33
- {navig_github-0.2.0 → navig_github-0.2.4}/LICENSE +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/README.md +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/__init__.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/commands/__init__.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/__init__.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/__main__.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/attachments.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/github_api.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/models.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/restore.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/rich_utils.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/transfer.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/engine/validation.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/plugin.py +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github/skills/github-ops/SKILL.md +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github.egg-info/dependency_links.txt +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github.egg-info/entry_points.txt +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/navig_github.egg-info/top_level.txt +0 -0
- {navig_github-0.2.0 → navig_github-0.2.4}/setup.cfg +0 -0
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: navig-github
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: NAVIG GitHub module — the full GitHub toolbox (backup, mirror, export, manage) wired into `navig github`. A first-party navig plugin (free, toggleable).
|
|
5
|
+
Author-email: NAVIG Development Team <opensource@navig.run>
|
|
5
6
|
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://navig.run
|
|
8
|
+
Project-URL: Documentation, https://navig.run/docs
|
|
9
|
+
Keywords: navig,cli,agent,automation,plugin,github,git,backup,mirror
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
6
19
|
Requires-Python: >=3.10
|
|
7
20
|
Description-Content-Type: text/markdown
|
|
8
21
|
License-File: LICENSE
|
|
22
|
+
Requires-Dist: navig>=3.24.0
|
|
9
23
|
Requires-Dist: requests>=2.31.0
|
|
10
24
|
Requires-Dist: python-dotenv>=1.0.0
|
|
11
25
|
Requires-Dist: schedule>=1.2.0
|
|
@@ -185,12 +185,12 @@ def _fallback_git_clone(repo_url: str, dest: str) -> None:
|
|
|
185
185
|
result = subprocess.run(
|
|
186
186
|
["git", "-C", str(clone_dest), "pull", "--ff-only"],
|
|
187
187
|
capture_output=True,
|
|
188
|
-
text=True,
|
|
188
|
+
text=True, encoding="utf-8", errors="replace",
|
|
189
189
|
)
|
|
190
190
|
else:
|
|
191
191
|
ch.info(f"Cloning {repo_url} → {clone_dest} …")
|
|
192
192
|
result = subprocess.run(
|
|
193
|
-
["git", "clone", repo_url, str(clone_dest)], capture_output=True, text=True
|
|
193
|
+
["git", "clone", repo_url, str(clone_dest)], capture_output=True, text=True, encoding="utf-8", errors="replace"
|
|
194
194
|
)
|
|
195
195
|
|
|
196
196
|
if result.returncode == 0:
|
|
@@ -339,10 +339,10 @@ def gh_clone(
|
|
|
339
339
|
token: Annotated[str | None, typer.Option("--token", "-t", help="GitHub token")] = None,
|
|
340
340
|
):
|
|
341
341
|
"""
|
|
342
|
-
⬇️ Clone a single repository (with the
|
|
342
|
+
⬇️ Clone a single repository (with the full toolbox if available, else plain git).
|
|
343
343
|
|
|
344
344
|
Always FLAT — lands directly as <dest>/<repo> (like `git clone`). Falls back
|
|
345
|
-
to a plain `git clone` when no token is configured and the
|
|
345
|
+
to a plain `git clone` when no token is configured and the full toolbox is unavailable.
|
|
346
346
|
|
|
347
347
|
Examples:
|
|
348
348
|
navig github clone owner/my-repo -d ./mirrors
|
|
@@ -390,7 +390,7 @@ def token_set(
|
|
|
390
390
|
] = True,
|
|
391
391
|
):
|
|
392
392
|
"""
|
|
393
|
-
🔑 Save a GitHub token for
|
|
393
|
+
🔑 Save a GitHub token for navig github to use automatically.
|
|
394
394
|
|
|
395
395
|
Stores in the navig vault by default (preferred). Falls back to
|
|
396
396
|
writing to ~/.navig/config.yaml when the vault is unavailable.
|
|
@@ -465,7 +465,7 @@ def token_show():
|
|
|
465
465
|
ch.warning(
|
|
466
466
|
"No GitHub token configured.\n"
|
|
467
467
|
"Set one with: navig github token set <your-token>\n"
|
|
468
|
-
"Without a token,
|
|
468
|
+
"Without a token, navig github only accesses public repos at low rate limits."
|
|
469
469
|
)
|
|
470
470
|
|
|
471
471
|
|
|
@@ -520,6 +520,7 @@ def gh_status():
|
|
|
520
520
|
navig github status
|
|
521
521
|
"""
|
|
522
522
|
# engine availability (bundled engine — a hard dependency of navig-github)
|
|
523
|
+
engine_missing = False
|
|
523
524
|
if _engine_available():
|
|
524
525
|
try:
|
|
525
526
|
import navig_github.engine as fm # type: ignore
|
|
@@ -531,6 +532,7 @@ def gh_status():
|
|
|
531
532
|
else:
|
|
532
533
|
ch.error("GitHub engine MISSING (it is a dependency of navig-github).")
|
|
533
534
|
ch.info("Reinstall the plugin: pip install navig-github")
|
|
535
|
+
engine_missing = True
|
|
534
536
|
|
|
535
537
|
# Token check
|
|
536
538
|
token = _resolve_github_token()
|
|
@@ -543,6 +545,13 @@ def gh_status():
|
|
|
543
545
|
"Set one with: navig github token set <your-token>"
|
|
544
546
|
)
|
|
545
547
|
|
|
548
|
+
if engine_missing:
|
|
549
|
+
# The engine is a hard dependency, so this is a BROKEN install, not a
|
|
550
|
+
# state the operator can be in on purpose. Raised at the end so the token
|
|
551
|
+
# check above still prints — a status command should show everything it
|
|
552
|
+
# knows before it exits.
|
|
553
|
+
raise typer.Exit(1)
|
|
554
|
+
|
|
546
555
|
|
|
547
556
|
# ---------------------------------------------------------------------------
|
|
548
557
|
# Full engine mount — every engine command becomes `navig github <command>`
|
|
@@ -12,6 +12,8 @@ from datetime import datetime
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
+
from navig.core.json_io import JsonReadError, atomic_write_json, load_json_for_update
|
|
16
|
+
|
|
15
17
|
|
|
16
18
|
@dataclass
|
|
17
19
|
class RepositoryStats:
|
|
@@ -151,22 +153,42 @@ class BackupAnalytics:
|
|
|
151
153
|
"""Initialize the analytics engine."""
|
|
152
154
|
self.backup_dir = backup_dir or Path("backups")
|
|
153
155
|
self._history: list[BackupHistory] = []
|
|
156
|
+
# True when history.json existed but was transiently UNREADABLE at load (a Windows
|
|
157
|
+
# AV/backup lock, or a truncated write). While set, _save_history REFUSES to write —
|
|
158
|
+
# _history is empty by accident, not because there are no backups, and overwriting it
|
|
159
|
+
# would wipe the recorded history. A per-run engine re-reads on the next construction.
|
|
160
|
+
self._history_load_failed = False
|
|
154
161
|
self._load_history()
|
|
155
162
|
|
|
156
163
|
def _load_history(self) -> None:
|
|
157
164
|
"""Load backup history from disk."""
|
|
158
165
|
history_path = self.backup_dir / self.HISTORY_FILE
|
|
159
|
-
if history_path.exists():
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
if not history_path.exists():
|
|
167
|
+
self._history_load_failed = False
|
|
168
|
+
return
|
|
169
|
+
try:
|
|
170
|
+
# load_json_for_update RAISES on a transiently-unreadable-but-populated file (a
|
|
171
|
+
# lock — the old narrow except missed PermissionError and crashed construction) and
|
|
172
|
+
# quarantines a corrupt/truncated one to *.corrupt instead of reading it as empty.
|
|
173
|
+
data = load_json_for_update(history_path, default={"history": []})
|
|
174
|
+
except JsonReadError:
|
|
175
|
+
self._history_load_failed = True
|
|
176
|
+
self._history = []
|
|
177
|
+
return
|
|
178
|
+
self._history_load_failed = False
|
|
179
|
+
try:
|
|
180
|
+
self._history = [
|
|
181
|
+
BackupHistory(**h) for h in data.get("history", [])
|
|
182
|
+
]
|
|
183
|
+
except (TypeError, KeyError):
|
|
184
|
+
self._history = []
|
|
185
|
+
|
|
168
186
|
def _save_history(self) -> None:
|
|
169
187
|
"""Save backup history to disk."""
|
|
188
|
+
if self._history_load_failed:
|
|
189
|
+
# History was unreadable at load, so _history is empty by accident — writing it
|
|
190
|
+
# would wipe the recorded backups. Refuse until a clean load.
|
|
191
|
+
return
|
|
170
192
|
history_path = self.backup_dir / self.HISTORY_FILE
|
|
171
193
|
history_path.parent.mkdir(parents=True, exist_ok=True)
|
|
172
194
|
data = {
|
|
@@ -174,7 +196,7 @@ class BackupAnalytics:
|
|
|
174
196
|
"updated_at": datetime.now().isoformat(),
|
|
175
197
|
"history": [h.to_dict() for h in self._history],
|
|
176
198
|
}
|
|
177
|
-
|
|
199
|
+
atomic_write_json(data, history_path)
|
|
178
200
|
|
|
179
201
|
def record_backup(
|
|
180
202
|
self,
|
|
@@ -247,7 +269,7 @@ class BackupAnalytics:
|
|
|
247
269
|
cwd=repo_path,
|
|
248
270
|
capture_output=True,
|
|
249
271
|
text=True,
|
|
250
|
-
timeout=30,
|
|
272
|
+
timeout=30, encoding="utf-8", errors="replace",
|
|
251
273
|
)
|
|
252
274
|
if result.returncode == 0:
|
|
253
275
|
stats.commit_count = int(result.stdout.strip())
|
|
@@ -261,7 +283,7 @@ class BackupAnalytics:
|
|
|
261
283
|
cwd=repo_path,
|
|
262
284
|
capture_output=True,
|
|
263
285
|
text=True,
|
|
264
|
-
timeout=30,
|
|
286
|
+
timeout=30, encoding="utf-8", errors="replace",
|
|
265
287
|
)
|
|
266
288
|
if result.returncode == 0:
|
|
267
289
|
stats.branch_count = len([
|
|
@@ -278,7 +300,7 @@ class BackupAnalytics:
|
|
|
278
300
|
cwd=repo_path,
|
|
279
301
|
capture_output=True,
|
|
280
302
|
text=True,
|
|
281
|
-
timeout=30,
|
|
303
|
+
timeout=30, encoding="utf-8", errors="replace",
|
|
282
304
|
)
|
|
283
305
|
if result.returncode == 0:
|
|
284
306
|
stats.tag_count = len([t for t in result.stdout.strip().split("\n") if t.strip()])
|
|
@@ -292,7 +314,7 @@ class BackupAnalytics:
|
|
|
292
314
|
cwd=repo_path,
|
|
293
315
|
capture_output=True,
|
|
294
316
|
text=True,
|
|
295
|
-
timeout=30,
|
|
317
|
+
timeout=30, encoding="utf-8", errors="replace",
|
|
296
318
|
)
|
|
297
319
|
if result.returncode == 0 and result.stdout.strip():
|
|
298
320
|
stats.last_commit_date = result.stdout.strip()
|
|
@@ -304,7 +326,7 @@ class BackupAnalytics:
|
|
|
304
326
|
if lfs_config.exists() or (repo_path / ".gitattributes").exists():
|
|
305
327
|
gitattributes = repo_path / ".gitattributes"
|
|
306
328
|
if gitattributes.exists():
|
|
307
|
-
content = gitattributes.read_text()
|
|
329
|
+
content = gitattributes.read_text(encoding="utf-8")
|
|
308
330
|
stats.has_lfs = "filter=lfs" in content
|
|
309
331
|
|
|
310
332
|
# Get language statistics (for non-bare repos)
|
|
@@ -251,7 +251,7 @@ def export_repository_data(
|
|
|
251
251
|
for issue in issues_list
|
|
252
252
|
],
|
|
253
253
|
}
|
|
254
|
-
with open(dest, "w") as f:
|
|
254
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
255
255
|
json.dump(issues_data, f, indent=2)
|
|
256
256
|
console.print(f" [green]✓ Issues exported: {owner}/{repo_name} ({len(issues_list)} issues)[/green]")
|
|
257
257
|
|
|
@@ -278,7 +278,7 @@ def export_repository_data(
|
|
|
278
278
|
for pr in prs_list
|
|
279
279
|
],
|
|
280
280
|
}
|
|
281
|
-
with open(dest, "w") as f:
|
|
281
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
282
282
|
json.dump(prs_data, f, indent=2)
|
|
283
283
|
console.print(f" [green]✓ Pull requests exported: {owner}/{repo_name} ({len(prs_list)} PRs)[/green]")
|
|
284
284
|
|
|
@@ -291,7 +291,7 @@ def export_repository_data(
|
|
|
291
291
|
if workflows_list:
|
|
292
292
|
for wf_file in workflow_files:
|
|
293
293
|
file_path = dest / Path(wf_file["path"]).name
|
|
294
|
-
with open(file_path, "w") as f:
|
|
294
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
295
295
|
f.write(wf_file["content"])
|
|
296
296
|
|
|
297
297
|
metadata = {
|
|
@@ -300,7 +300,7 @@ def export_repository_data(
|
|
|
300
300
|
"exported_at": datetime.now().isoformat(),
|
|
301
301
|
"workflows": [{"name": wf.name, "path": wf.path} for wf in workflows_list],
|
|
302
302
|
}
|
|
303
|
-
with open(dest / "metadata.json", "w") as f:
|
|
303
|
+
with open(dest / "metadata.json", "w", encoding="utf-8") as f:
|
|
304
304
|
json.dump(metadata, f, indent=2)
|
|
305
305
|
console.print(f" [green]✓ Workflows exported: {owner}/{repo_name} ({len(workflows_list)} workflows)[/green]")
|
|
306
306
|
|
|
@@ -325,7 +325,7 @@ def export_repository_data(
|
|
|
325
325
|
for release in releases_list
|
|
326
326
|
],
|
|
327
327
|
}
|
|
328
|
-
with open(dest / "metadata.json", "w") as f:
|
|
328
|
+
with open(dest / "metadata.json", "w", encoding="utf-8") as f:
|
|
329
329
|
json.dump(metadata, f, indent=2)
|
|
330
330
|
console.print(f" [green]✓ Releases exported: {owner}/{repo_name} ({len(releases_list)} releases)[/green]")
|
|
331
331
|
|
|
@@ -345,7 +345,7 @@ def export_repository_data(
|
|
|
345
345
|
capture_output=True,
|
|
346
346
|
text=True,
|
|
347
347
|
check=True,
|
|
348
|
-
timeout=300,
|
|
348
|
+
timeout=300, encoding="utf-8", errors="replace",
|
|
349
349
|
)
|
|
350
350
|
console.print(f" [green]✓ Wiki cloned: {owner}/{repo_name}[/green]")
|
|
351
351
|
except subprocess.CalledProcessError:
|
|
@@ -358,7 +358,7 @@ def export_repository_data(
|
|
|
358
358
|
capture_output=True,
|
|
359
359
|
text=True,
|
|
360
360
|
check=True,
|
|
361
|
-
timeout=120,
|
|
361
|
+
timeout=120, encoding="utf-8", errors="replace",
|
|
362
362
|
)
|
|
363
363
|
console.print(f" [green]✓ Wiki updated: {owner}/{repo_name}[/green]")
|
|
364
364
|
except subprocess.CalledProcessError:
|
|
@@ -1184,7 +1184,7 @@ def secrets(
|
|
|
1184
1184
|
⚠️ Note: GitHub API does NOT expose secret values, only names and metadata.
|
|
1185
1185
|
|
|
1186
1186
|
Example:
|
|
1187
|
-
navig github secrets miztizm/
|
|
1187
|
+
navig github secrets miztizm/navig-github
|
|
1188
1188
|
navig github secrets miztizm/hello-world --format yaml
|
|
1189
1189
|
navig github secrets miztizm/navig github --dest ./custom_secrets.json
|
|
1190
1190
|
"""
|
|
@@ -1546,7 +1546,7 @@ def clone(
|
|
|
1546
1546
|
drops the repo right where you are, defaults to HTTPS (no SSH keys needed
|
|
1547
1547
|
for public repos), and needs no token or GitHub API call for public repos.
|
|
1548
1548
|
|
|
1549
|
-
"Sometimes you just want the code, not the whole archive." —
|
|
1549
|
+
"Sometimes you just want the code, not the whole archive." — navig github
|
|
1550
1550
|
|
|
1551
1551
|
Example:
|
|
1552
1552
|
navig github clone microsoft/vscode
|
|
@@ -1707,10 +1707,10 @@ def issues(
|
|
|
1707
1707
|
|
|
1708
1708
|
# Export to file
|
|
1709
1709
|
if format == "yaml":
|
|
1710
|
-
with open(dest, "w") as f:
|
|
1710
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
1711
1711
|
yaml.dump(issues_data, f, default_flow_style=False, sort_keys=False)
|
|
1712
1712
|
else:
|
|
1713
|
-
with open(dest, "w") as f:
|
|
1713
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
1714
1714
|
json.dump(issues_data, f, indent=2)
|
|
1715
1715
|
|
|
1716
1716
|
# Create summary table
|
|
@@ -1840,10 +1840,10 @@ def pulls(
|
|
|
1840
1840
|
|
|
1841
1841
|
# Export to file
|
|
1842
1842
|
if format == "yaml":
|
|
1843
|
-
with open(dest, "w") as f:
|
|
1843
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
1844
1844
|
yaml.dump(prs_data, f, default_flow_style=False, sort_keys=False)
|
|
1845
1845
|
else:
|
|
1846
|
-
with open(dest, "w") as f:
|
|
1846
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
1847
1847
|
json.dump(prs_data, f, indent=2)
|
|
1848
1848
|
|
|
1849
1849
|
# Create summary table
|
|
@@ -1930,7 +1930,7 @@ def workflows(
|
|
|
1930
1930
|
# Save workflow files
|
|
1931
1931
|
for wf_file in workflow_files:
|
|
1932
1932
|
file_path = dest / Path(wf_file["path"]).name
|
|
1933
|
-
with open(file_path, "w") as f:
|
|
1933
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
1934
1934
|
f.write(wf_file["content"])
|
|
1935
1935
|
|
|
1936
1936
|
# Save metadata
|
|
@@ -1971,7 +1971,7 @@ def workflows(
|
|
|
1971
1971
|
|
|
1972
1972
|
# Save metadata
|
|
1973
1973
|
metadata_path = dest / "metadata.json"
|
|
1974
|
-
with open(metadata_path, "w") as f:
|
|
1974
|
+
with open(metadata_path, "w", encoding="utf-8") as f:
|
|
1975
1975
|
json.dump(metadata, f, indent=2)
|
|
1976
1976
|
|
|
1977
1977
|
console.print(f"\n[green]✅ Workflows backed up to: {dest}[/green]")
|
|
@@ -2070,7 +2070,7 @@ def releases(
|
|
|
2070
2070
|
}
|
|
2071
2071
|
|
|
2072
2072
|
metadata_path = dest / "metadata.json"
|
|
2073
|
-
with open(metadata_path, "w") as f:
|
|
2073
|
+
with open(metadata_path, "w", encoding="utf-8") as f:
|
|
2074
2074
|
json.dump(metadata, f, indent=2)
|
|
2075
2075
|
|
|
2076
2076
|
# Download assets if requested
|
|
@@ -2095,7 +2095,7 @@ def releases(
|
|
|
2095
2095
|
"author": release.author,
|
|
2096
2096
|
"html_url": release.html_url,
|
|
2097
2097
|
}
|
|
2098
|
-
with open(release_dir / "release.json", "w") as f:
|
|
2098
|
+
with open(release_dir / "release.json", "w", encoding="utf-8") as f:
|
|
2099
2099
|
json.dump(release_metadata, f, indent=2)
|
|
2100
2100
|
|
|
2101
2101
|
# Download each asset
|
|
@@ -2196,7 +2196,7 @@ def wiki(
|
|
|
2196
2196
|
capture_output=True,
|
|
2197
2197
|
text=True,
|
|
2198
2198
|
check=True,
|
|
2199
|
-
timeout=120,
|
|
2199
|
+
timeout=120, encoding="utf-8", errors="replace",
|
|
2200
2200
|
)
|
|
2201
2201
|
console.print(f"[green]✅ Wiki updated: {dest}[/green]")
|
|
2202
2202
|
except subprocess.CalledProcessError as e:
|
|
@@ -2210,7 +2210,7 @@ def wiki(
|
|
|
2210
2210
|
capture_output=True,
|
|
2211
2211
|
text=True,
|
|
2212
2212
|
check=True,
|
|
2213
|
-
timeout=300,
|
|
2213
|
+
timeout=300, encoding="utf-8", errors="replace",
|
|
2214
2214
|
)
|
|
2215
2215
|
console.print(f"[green]✅ Wiki cloned to: {dest}[/green]")
|
|
2216
2216
|
except subprocess.CalledProcessError as e:
|
|
@@ -2593,7 +2593,7 @@ def attachments(
|
|
|
2593
2593
|
issues and pull requests, including images, documents, and other attachments.
|
|
2594
2594
|
|
|
2595
2595
|
Example:
|
|
2596
|
-
navig github attachments miztizm/
|
|
2596
|
+
navig github attachments miztizm/navig-github
|
|
2597
2597
|
navig github attachments myorg/myrepo --source issues
|
|
2598
2598
|
navig github attachments myorg/myrepo --source pulls --dest ./my-attachments
|
|
2599
2599
|
"""
|
|
@@ -2739,7 +2739,7 @@ def labels(
|
|
|
2739
2739
|
|
|
2740
2740
|
|
|
2741
2741
|
Example:
|
|
2742
|
-
navig github labels miztizm/
|
|
2742
|
+
navig github labels miztizm/navig-github
|
|
2743
2743
|
navig github labels myorg/myrepo --format yaml
|
|
2744
2744
|
navig github labels myorg/myrepo --dest ./my-labels.json
|
|
2745
2745
|
"""
|
|
@@ -2791,10 +2791,10 @@ def labels(
|
|
|
2791
2791
|
|
|
2792
2792
|
# Export to file
|
|
2793
2793
|
if format.lower() == "yaml":
|
|
2794
|
-
with open(dest, "w") as f:
|
|
2794
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
2795
2795
|
yaml.dump(labels_data, f, default_flow_style=False, sort_keys=False)
|
|
2796
2796
|
else:
|
|
2797
|
-
with open(dest, "w") as f:
|
|
2797
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
2798
2798
|
json.dump(labels_data, f, indent=2)
|
|
2799
2799
|
|
|
2800
2800
|
# Create summary table
|
|
@@ -2857,7 +2857,7 @@ def milestones(
|
|
|
2857
2857
|
|
|
2858
2858
|
|
|
2859
2859
|
Example:
|
|
2860
|
-
navig github milestones miztizm/
|
|
2860
|
+
navig github milestones miztizm/navig-github
|
|
2861
2861
|
navig github milestones myorg/myrepo --state open
|
|
2862
2862
|
navig github milestones myorg/myrepo --format yaml
|
|
2863
2863
|
"""
|
|
@@ -2918,10 +2918,10 @@ def milestones(
|
|
|
2918
2918
|
|
|
2919
2919
|
# Export to file
|
|
2920
2920
|
if format.lower() == "yaml":
|
|
2921
|
-
with open(dest, "w") as f:
|
|
2921
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
2922
2922
|
yaml.dump(milestones_data, f, default_flow_style=False, sort_keys=False)
|
|
2923
2923
|
else:
|
|
2924
|
-
with open(dest, "w") as f:
|
|
2924
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
2925
2925
|
json.dump(milestones_data, f, indent=2)
|
|
2926
2926
|
|
|
2927
2927
|
# Create summary table
|
|
@@ -2987,7 +2987,7 @@ def webhooks(
|
|
|
2987
2987
|
⚠️ Note: Requires admin access to the repository. Secret values are redacted.
|
|
2988
2988
|
|
|
2989
2989
|
Example:
|
|
2990
|
-
navig github webhooks miztizm/
|
|
2990
|
+
navig github webhooks miztizm/navig-github
|
|
2991
2991
|
navig github webhooks myorg/myrepo --format yaml
|
|
2992
2992
|
"""
|
|
2993
2993
|
if format.lower() not in ["json", "yaml"]:
|
|
@@ -3042,10 +3042,10 @@ def webhooks(
|
|
|
3042
3042
|
|
|
3043
3043
|
# Export to file
|
|
3044
3044
|
if format.lower() == "yaml":
|
|
3045
|
-
with open(dest, "w") as f:
|
|
3045
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
3046
3046
|
yaml.dump(webhooks_data, f, default_flow_style=False, sort_keys=False)
|
|
3047
3047
|
else:
|
|
3048
|
-
with open(dest, "w") as f:
|
|
3048
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
3049
3049
|
json.dump(webhooks_data, f, indent=2)
|
|
3050
3050
|
|
|
3051
3051
|
# Create summary table
|
|
@@ -3188,10 +3188,10 @@ def followers(
|
|
|
3188
3188
|
|
|
3189
3189
|
# Export to file
|
|
3190
3190
|
if format.lower() == "yaml":
|
|
3191
|
-
with open(dest, "w") as f:
|
|
3191
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
3192
3192
|
yaml.dump(export_data, f, default_flow_style=False, sort_keys=False)
|
|
3193
3193
|
else:
|
|
3194
|
-
with open(dest, "w") as f:
|
|
3194
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
3195
3195
|
json.dump(export_data, f, indent=2)
|
|
3196
3196
|
|
|
3197
3197
|
# Create summary table
|
|
@@ -3244,7 +3244,7 @@ def discussions(
|
|
|
3244
3244
|
Uses the GitHub GraphQL API to fetch discussions data.
|
|
3245
3245
|
|
|
3246
3246
|
Example:
|
|
3247
|
-
navig github discussions miztizm/
|
|
3247
|
+
navig github discussions miztizm/navig-github
|
|
3248
3248
|
navig github discussions myorg/myrepo --format yaml
|
|
3249
3249
|
"""
|
|
3250
3250
|
if format.lower() not in ["json", "yaml"]:
|
|
@@ -3304,10 +3304,10 @@ def discussions(
|
|
|
3304
3304
|
|
|
3305
3305
|
# Export to file
|
|
3306
3306
|
if format.lower() == "yaml":
|
|
3307
|
-
with open(dest, "w") as f:
|
|
3307
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
3308
3308
|
yaml.dump(discussions_data, f, default_flow_style=False, sort_keys=False)
|
|
3309
3309
|
else:
|
|
3310
|
-
with open(dest, "w") as f:
|
|
3310
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
3311
3311
|
json.dump(discussions_data, f, indent=2)
|
|
3312
3312
|
|
|
3313
3313
|
# Create summary table
|
|
@@ -3672,7 +3672,7 @@ def schedule_add(
|
|
|
3672
3672
|
navig github schedule-add weekly-full --profile full-backup --interval weekly --on monday --at 03:00
|
|
3673
3673
|
navig github schedule-add frequent --profile quick --interval "every 6 hours"
|
|
3674
3674
|
"""
|
|
3675
|
-
from .scheduler import BackupScheduler, create_scheduled_backup
|
|
3675
|
+
from .scheduler import BackupScheduler, ScheduleStoreError, create_scheduled_backup
|
|
3676
3676
|
|
|
3677
3677
|
scheduler = BackupScheduler()
|
|
3678
3678
|
backup = create_scheduled_backup(
|
|
@@ -3683,7 +3683,12 @@ def schedule_add(
|
|
|
3683
3683
|
on_day=on_day,
|
|
3684
3684
|
)
|
|
3685
3685
|
|
|
3686
|
-
|
|
3686
|
+
try:
|
|
3687
|
+
scheduler.add_backup(backup)
|
|
3688
|
+
except ScheduleStoreError as e:
|
|
3689
|
+
print_error(f"Could not read existing schedules — not saving, to avoid losing them: {e}")
|
|
3690
|
+
console.print(" [dim]The schedule store is temporarily locked; try again in a moment.[/dim]")
|
|
3691
|
+
sys.exit(1)
|
|
3687
3692
|
print_success(f"Scheduled backup '{name}' added!")
|
|
3688
3693
|
console.print(f" [dim]Interval: {interval}[/dim]")
|
|
3689
3694
|
if at_time:
|
|
@@ -3748,10 +3753,16 @@ def schedule_remove(
|
|
|
3748
3753
|
Example:
|
|
3749
3754
|
navig github schedule-remove daily-backup
|
|
3750
3755
|
"""
|
|
3751
|
-
from .scheduler import BackupScheduler
|
|
3756
|
+
from .scheduler import BackupScheduler, ScheduleStoreError
|
|
3752
3757
|
|
|
3753
3758
|
scheduler = BackupScheduler()
|
|
3754
|
-
|
|
3759
|
+
try:
|
|
3760
|
+
removed = scheduler.remove_backup(name)
|
|
3761
|
+
except ScheduleStoreError as e:
|
|
3762
|
+
print_error(f"Could not read existing schedules — not saving, to avoid losing them: {e}")
|
|
3763
|
+
console.print(" [dim]The schedule store is temporarily locked; try again in a moment.[/dim]")
|
|
3764
|
+
sys.exit(1)
|
|
3765
|
+
if removed:
|
|
3755
3766
|
print_success(f"Schedule '{name}' removed")
|
|
3756
3767
|
else:
|
|
3757
3768
|
print_error(f"Schedule '{name}' not found")
|
|
@@ -4185,10 +4196,10 @@ def projects(
|
|
|
4185
4196
|
|
|
4186
4197
|
# Export to file
|
|
4187
4198
|
if format.lower() == "yaml":
|
|
4188
|
-
with open(dest, "w") as f:
|
|
4199
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
4189
4200
|
yaml.dump(projects_data, f, default_flow_style=False, sort_keys=False)
|
|
4190
4201
|
else:
|
|
4191
|
-
with open(dest, "w") as f:
|
|
4202
|
+
with open(dest, "w", encoding="utf-8") as f:
|
|
4192
4203
|
json.dump(projects_data, f, indent=2)
|
|
4193
4204
|
|
|
4194
4205
|
# Create summary table
|
|
@@ -4260,7 +4271,7 @@ def analytics_report(
|
|
|
4260
4271
|
report = analytics.generate_report(format=format.lower())
|
|
4261
4272
|
|
|
4262
4273
|
if output:
|
|
4263
|
-
output.write_text(report)
|
|
4274
|
+
output.write_text(report, encoding="utf-8")
|
|
4264
4275
|
print_success(f"Report saved to: {output}")
|
|
4265
4276
|
else:
|
|
4266
4277
|
console.print(report)
|
|
@@ -4385,7 +4396,7 @@ def diff_backups(
|
|
|
4385
4396
|
report = compare.generate_diff_report(diff, format=format.lower())
|
|
4386
4397
|
|
|
4387
4398
|
if output:
|
|
4388
|
-
output.write_text(report)
|
|
4399
|
+
output.write_text(report, encoding="utf-8")
|
|
4389
4400
|
print_success(f"Diff report saved to: {output}")
|
|
4390
4401
|
else:
|
|
4391
4402
|
console.print(report)
|
|
@@ -4535,9 +4546,14 @@ def show_template(
|
|
|
4535
4546
|
console.print(table)
|
|
4536
4547
|
|
|
4537
4548
|
if template.schedule_interval:
|
|
4538
|
-
|
|
4549
|
+
# Rich parses each print() independently, so a style opened in one call does not
|
|
4550
|
+
# carry into the next. Splitting `[cyan]…[/cyan]` across two prints left the
|
|
4551
|
+
# second holding a closing tag with no opener, and Rich raises MarkupError on
|
|
4552
|
+
# that rather than ignoring it — `template-show` crashed for any template with
|
|
4553
|
+
# both an interval and a time. Each line now opens and closes its own tag.
|
|
4554
|
+
console.print(f"\n [cyan]📅 Schedule: {template.schedule_interval}[/cyan]")
|
|
4539
4555
|
if template.schedule_time:
|
|
4540
|
-
console.print(f" At: {template.schedule_time}[/cyan]")
|
|
4556
|
+
console.print(f" [cyan]At: {template.schedule_time}[/cyan]")
|
|
4541
4557
|
|
|
4542
4558
|
|
|
4543
4559
|
@app.command("template-use")
|
|
@@ -4725,7 +4741,7 @@ def test_notifications() -> None:
|
|
|
4725
4741
|
|
|
4726
4742
|
if not manager.providers:
|
|
4727
4743
|
print_warning("No notification providers configured")
|
|
4728
|
-
console.print(" [dim]Configure notifications in ~/.config/
|
|
4744
|
+
console.print(" [dim]Configure notifications in ~/.config/navig-github/.navig-github_notifications.json[/dim]")
|
|
4729
4745
|
return
|
|
4730
4746
|
|
|
4731
4747
|
console.print("\n[cyan]🔔 Testing notification providers...[/cyan]")
|