navig-github 0.2.0__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.
@@ -0,0 +1,3 @@
1
+ """navig-github — GitHub tools plugin for NAVIG (backup · mirror · export · manage)."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,612 @@
1
+ """
2
+ navig github — the full GitHub toolbox, wired natively into navig.
3
+
4
+ The GitHub engine (`navig_github.engine`) is bundled and runs
5
+ **in-process**: every engine command is mounted as a `navig github`
6
+ subcommand (backup, exports, analytics, scheduling …), plus navig's own
7
+ curated wrappers (`backup`, `search`, `clone`) and vault-backed `token`
8
+ management. A group callback injects the resolved token into GITHUB_TOKEN,
9
+ which every engine command reads via its `envvar` — so the whole surface is
10
+ vault-wired with zero per-command glue.
11
+
12
+ Token resolution order:
13
+ 1. navig vault (secret name: github_token)
14
+ 2. GITHUB_TOKEN env var
15
+ 3. ~/.navig/config.yaml → github.token
16
+ 4. No token → public repos only (curated `clone` falls back to plain git)
17
+ """
18
+
19
+ import os
20
+ import subprocess
21
+ from pathlib import Path
22
+ from typing import Annotated
23
+
24
+ import typer
25
+
26
+ from navig.core.yaml_io import load_yaml_for_update, safe_load_yaml
27
+ from navig.lazy_loader import lazy_import
28
+ from navig.platform.paths import config_dir
29
+
30
+ ch = lazy_import("navig.console_helper")
31
+
32
+ github_app = typer.Typer(
33
+ name="github",
34
+ help="🐙 GitHub: backup · mirror · export · manage (full GitHub engine, vault-wired)",
35
+ no_args_is_help=True,
36
+ )
37
+
38
+
39
+ @github_app.callback()
40
+ def _github_root(ctx: typer.Context) -> None:
41
+ # Vault → GITHUB_TOKEN before any subcommand runs: every mounted engine
42
+ # command declares envvar="GITHUB_TOKEN", so this one hook authenticates
43
+ # the entire surface. An explicitly exported GITHUB_TOKEN always wins.
44
+ if not os.environ.get("GITHUB_TOKEN"):
45
+ tok = _resolve_github_token()
46
+ if tok:
47
+ os.environ["GITHUB_TOKEN"] = tok
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Token helpers
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ def _resolve_github_token() -> str | None:
55
+ """
56
+ Resolve a GitHub token with the full navig credential chain.
57
+
58
+ Priority:
59
+ 1. navig vault (secret name: github_token)
60
+ 2. GITHUB_TOKEN environment variable
61
+ 3. ~/.navig/config.yaml → github.token
62
+
63
+ Returns None if not found anywhere.
64
+ """
65
+ # 1 — navig vault
66
+ try:
67
+ from navig.vault import get_vault # type: ignore
68
+
69
+ vault = get_vault()
70
+ secret = vault.get("github_token", caller="navig-github")
71
+ if secret is not None:
72
+ # Credential exposes the decrypted payload via `.data` / `.get_secret()`
73
+ # — there is NO `.value` attribute. The token is stored under "value"
74
+ # (also accept "token"/"api_key" for older entries).
75
+ tok = None
76
+ data = getattr(secret, "data", None)
77
+ if isinstance(data, dict):
78
+ tok = data.get("value") or data.get("token") or data.get("api_key")
79
+ if not tok and hasattr(secret, "get_secret"):
80
+ tok = secret.get_secret("value") or secret.get_secret("token")
81
+ if not tok:
82
+ tok = getattr(secret, "value", None)
83
+ if tok:
84
+ return str(tok)
85
+ except Exception: # noqa: BLE001
86
+ pass # best-effort; failure is non-critical
87
+
88
+ # 2 — env var
89
+ token = os.environ.get("GITHUB_TOKEN", "").strip()
90
+ if token:
91
+ return token
92
+
93
+ # 3 — ~/.navig/config.yaml
94
+ try:
95
+ cfg_path = config_dir() / "config.yaml"
96
+ cfg = safe_load_yaml(cfg_path) or {}
97
+ token = cfg.get("github", {}).get("token", "").strip()
98
+ if token:
99
+ return token
100
+ except Exception: # noqa: BLE001
101
+ pass # best-effort; failure is non-critical
102
+
103
+ return None
104
+
105
+
106
+ def _engine_available() -> bool:
107
+ """Return True if the bundled GitHub engine is importable."""
108
+ try:
109
+ import navig_github.engine # noqa: F401
110
+
111
+ return True
112
+ except ImportError:
113
+ return False
114
+
115
+
116
+ def _require_engine() -> bool:
117
+ """
118
+ Ensure the GitHub engine is importable; print help if missing.
119
+ Returns True if available.
120
+ """
121
+ if _engine_available():
122
+ return True
123
+ ch.error(
124
+ "the navig-github engine is missing — reinstall the plugin:\n"
125
+ " pip install navig-github"
126
+ )
127
+ return False
128
+
129
+
130
+ def _run_engine(args: list[str], token: str | None) -> None:
131
+ """Dispatch into the bundled GitHub engine's CLI **in-process** (no subprocess).
132
+
133
+ An explicit *token* (from a --token flag) wins over whatever the group
134
+ callback already injected into GITHUB_TOKEN.
135
+ """
136
+ if not _require_engine():
137
+ raise typer.Exit(1)
138
+ if token:
139
+ os.environ["GITHUB_TOKEN"] = token
140
+
141
+ import click
142
+ from navig_github.engine.cli import app as engine_cli
143
+ from typer.main import get_command
144
+
145
+ cmd = get_command(engine_cli)
146
+ try:
147
+ cmd.main(list(args), prog_name="navig github", standalone_mode=False)
148
+ except click.exceptions.Exit as exc: # typer.Exit inside the engine lands here
149
+ code = exc.exit_code or 0
150
+ if code:
151
+ raise typer.Exit(code) from None
152
+ except click.exceptions.Abort:
153
+ ch.warning("Aborted.")
154
+ raise typer.Exit(1) from None
155
+ except click.ClickException as exc:
156
+ exc.show()
157
+ raise typer.Exit(exc.exit_code) from None
158
+ except KeyboardInterrupt:
159
+ pass # user interrupted; clean exit
160
+ except SystemExit as exc: # defensive: bare sys.exit() inside the engine
161
+ code = exc.code if isinstance(exc.code, int) else 1
162
+ if code:
163
+ raise typer.Exit(code) from None
164
+
165
+
166
+ # ---------------------------------------------------------------------------
167
+ # Fallback: navig-native git clone (no token required)
168
+ # ---------------------------------------------------------------------------
169
+
170
+
171
+ def _fallback_git_clone(repo_url: str, dest: str) -> None:
172
+ """
173
+ Minimal git-based clone used when no GitHub token is available.
174
+ Leverages the git binary already required by navig.
175
+ """
176
+ ch.warning("No GitHub token found — using plain `git clone` (public repos only).")
177
+ dest_path = Path(dest)
178
+ dest_path.mkdir(parents=True, exist_ok=True)
179
+
180
+ repo_name = repo_url.rstrip("/").split("/")[-1].removesuffix(".git")
181
+ clone_dest = dest_path / repo_name
182
+
183
+ if clone_dest.exists():
184
+ ch.info(f"Updating existing repo at {clone_dest} …")
185
+ result = subprocess.run(
186
+ ["git", "-C", str(clone_dest), "pull", "--ff-only"],
187
+ capture_output=True,
188
+ text=True,
189
+ )
190
+ else:
191
+ ch.info(f"Cloning {repo_url} → {clone_dest} …")
192
+ result = subprocess.run(
193
+ ["git", "clone", repo_url, str(clone_dest)], capture_output=True, text=True
194
+ )
195
+
196
+ if result.returncode == 0:
197
+ ch.success(f"Done: {clone_dest}")
198
+ else:
199
+ ch.error(result.stderr.strip() or "git clone failed")
200
+ raise typer.Exit(1)
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # sub-commands
205
+ # ---------------------------------------------------------------------------
206
+
207
+
208
+ @github_app.command("search", rich_help_panel="Backup & mirror")
209
+ def gh_search(
210
+ query: Annotated[str, typer.Argument(help="Search keyword (e.g. 'agent soul')")],
211
+ output_dir: Annotated[
212
+ Path | None, typer.Option("--output-dir", "-o", help="Destination directory")
213
+ ] = None,
214
+ limit: Annotated[
215
+ int, typer.Option("--limit", "-l", min=1, max=100, help="Max repos to clone")
216
+ ] = 20,
217
+ language: Annotated[str | None, typer.Option("--language", help="Filter by language")] = None,
218
+ min_stars: Annotated[int | None, typer.Option("--min-stars", help="Minimum star count")] = None,
219
+ sort: Annotated[
220
+ str, typer.Option("--sort", help="Sort: stars|forks|updated|best-match")
221
+ ] = "stars",
222
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Auto-confirm, no prompt")] = False,
223
+ workers: Annotated[int, typer.Option("--workers", "-w", help="Parallel clone workers")] = 4,
224
+ token: Annotated[
225
+ str | None,
226
+ typer.Option("--token", "-t", help="GitHub token (overrides auto-resolve)"),
227
+ ] = None,
228
+ flat: Annotated[
229
+ bool,
230
+ typer.Option(
231
+ "--flat/--no-flat",
232
+ help="Clone FLAT (<dir>/<repo>, collisions suffixed -<owner>) vs nested by owner",
233
+ ),
234
+ ] = True,
235
+ ):
236
+ """
237
+ 🔍 Search GitHub and clone matching repositories.
238
+
239
+ Token resolution: vault → GITHUB_TOKEN env → ~/.navig/config.yaml.
240
+ Without a token only public repos at low rate limits are accessible.
241
+
242
+ Downloads are FLAT by default (repos land directly as <dir>/<repo>, so a
243
+ second run updates them in place); use --no-flat for the owner-nested layout.
244
+
245
+ Examples:
246
+ navig github search "agent soul" -o ./mirrors --limit 50 -y
247
+ navig github search "machine learning" --language python --min-stars 500
248
+ navig github search "cli tools" -o ./tools --no-flat # nested by owner
249
+ """
250
+ resolved_token = token or _resolve_github_token()
251
+
252
+ if not resolved_token:
253
+ ch.warning(
254
+ "No GitHub token found. Using unauthenticated access (60 req/hour).\n"
255
+ "Set one with: navig github token set <your-token>"
256
+ )
257
+
258
+ if not _require_engine():
259
+ raise typer.Exit(1)
260
+
261
+ args = [
262
+ "search",
263
+ query,
264
+ "--limit",
265
+ str(limit),
266
+ "--sort",
267
+ sort,
268
+ "--workers",
269
+ str(workers),
270
+ ]
271
+ if output_dir:
272
+ args += ["--output-dir", str(output_dir)]
273
+ if language:
274
+ args += ["--language", language]
275
+ if min_stars is not None:
276
+ args += ["--min-stars", str(min_stars)]
277
+ if yes:
278
+ args += ["--yes"]
279
+ if flat:
280
+ args += ["--flat-structure"]
281
+
282
+ _run_engine(args, resolved_token)
283
+
284
+
285
+ @github_app.command("backup", rich_help_panel="Backup & mirror")
286
+ def gh_backup(
287
+ target: Annotated[str, typer.Argument(help="GitHub username (or org with --org) to back up")],
288
+ dest: Annotated[Path | None, typer.Option("--dest", "-d", help="Destination directory")] = None,
289
+ visibility: Annotated[str, typer.Option("--visibility", help="all|public|private")] = "all",
290
+ org: Annotated[bool, typer.Option("--org", help="Treat target as an organisation, not a user")] = False,
291
+ workers: Annotated[int, typer.Option("--workers", "-w", help="Parallel clone workers")] = 4,
292
+ yes: Annotated[bool, typer.Option("--yes", "-y", help="Auto-confirm")] = False,
293
+ flat: Annotated[
294
+ bool,
295
+ typer.Option("--flat/--no-flat", help="Clone FLAT (<dir>/<repo>) vs nested by visibility/owner"),
296
+ ] = True,
297
+ token: Annotated[str | None, typer.Option("--token", "-t", help="GitHub token")] = None,
298
+ ):
299
+ """
300
+ 📦 Clone / mirror every repo for a user (or --org organisation).
301
+
302
+ Downloads are FLAT by default (<dir>/<repo>, updates in place); --no-flat
303
+ keeps the repos/<visibility>/<owner> layout.
304
+
305
+ Examples:
306
+ navig github backup myuser -d ./mirrors -y
307
+ navig github backup myorg --org --visibility public
308
+ navig github backup myuser --no-flat # nested by visibility/owner
309
+ """
310
+ resolved_token = token or _resolve_github_token()
311
+
312
+ if not resolved_token:
313
+ ch.warning(
314
+ "No GitHub token found — only public repos accessible.\n"
315
+ "Set a token with: navig github token set <your-token>"
316
+ )
317
+
318
+ if not _require_engine():
319
+ raise typer.Exit(1)
320
+
321
+ # the engine exposes per-target commands `user` / `org` (there is no `backup`).
322
+ # 0.11 flags: concurrency is --max-workers; user/org don't prompt (no --yes).
323
+ # Full engine surface (incremental, --include-issues, --bare, --lfs …) is
324
+ # available directly: navig github user|org <name> --help
325
+ subcommand = "org" if org else "user"
326
+ args = [subcommand, target, "--visibility", visibility, "--max-workers", str(workers)]
327
+ if dest:
328
+ args += ["--dest", str(dest)]
329
+ if flat:
330
+ args += ["--flat"]
331
+
332
+ _run_engine(args, resolved_token)
333
+
334
+
335
+ @github_app.command("clone", rich_help_panel="Backup & mirror")
336
+ def gh_clone(
337
+ repo: Annotated[str, typer.Argument(help="owner/repo or full GitHub URL")],
338
+ dest: Annotated[Path | None, typer.Option("--dest", "-d", help="Destination directory")] = None,
339
+ token: Annotated[str | None, typer.Option("--token", "-t", help="GitHub token")] = None,
340
+ ):
341
+ """
342
+ ⬇️ Clone a single repository (with the engine if available, else plain git).
343
+
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 engine is unavailable.
346
+
347
+ Examples:
348
+ navig github clone owner/my-repo -d ./mirrors
349
+ navig github clone https://github.com/owner/repo
350
+ """
351
+ resolved_token = token or _resolve_github_token()
352
+
353
+ # Normalise URL
354
+ if not repo.startswith("http") and "/" in repo:
355
+ repo_url = f"https://github.com/{repo}"
356
+ else:
357
+ repo_url = repo
358
+
359
+ if not _engine_available():
360
+ # Graceful fallback: plain git clone (always flat into dest/<repo>).
361
+ _fallback_git_clone(repo_url, str(dest or Path.cwd()))
362
+ return
363
+
364
+ # `engine clone <repo> [dest]` — dest is a POSITIONAL arg, and the clone is
365
+ # already flat (lands as <dest>/<repo>), so no extra flags needed.
366
+ args = ["clone", repo_url]
367
+ if dest:
368
+ args += [str(dest)]
369
+
370
+ _run_engine(args, resolved_token)
371
+
372
+
373
+ # ---------------------------------------------------------------------------
374
+ # Token management sub-commands
375
+ # ---------------------------------------------------------------------------
376
+
377
+ token_app = typer.Typer(
378
+ name="token",
379
+ help="Manage the GitHub token used by the GitHub engine",
380
+ no_args_is_help=True,
381
+ )
382
+ github_app.add_typer(token_app, name="token", rich_help_panel="Setup")
383
+
384
+
385
+ @token_app.command("set")
386
+ def token_set(
387
+ value: Annotated[str, typer.Argument(help="GitHub Personal Access Token")],
388
+ use_vault: Annotated[
389
+ bool, typer.Option("--vault/--no-vault", help="Store in navig vault (default)")
390
+ ] = True,
391
+ ):
392
+ """
393
+ 🔑 Save a GitHub token for the engine to use automatically.
394
+
395
+ Stores in the navig vault by default (preferred). Falls back to
396
+ writing to ~/.navig/config.yaml when the vault is unavailable.
397
+
398
+ Examples:
399
+ navig github token set ghp_xxxxxxxxxxxxxxxx
400
+ """
401
+ if use_vault:
402
+ try:
403
+ from navig.vault import get_vault # type: ignore
404
+
405
+ vault = get_vault()
406
+ existing = vault.get("github_token", caller="navig-github.token_set")
407
+ if existing:
408
+ vault.update(existing.id, data={"value": value})
409
+ ch.success("GitHub token updated in navig vault.")
410
+ else:
411
+ vault.add(
412
+ provider="github_token",
413
+ credential_type="token",
414
+ data={"value": value},
415
+ metadata={"description": "GitHub PAT used by the GitHub engine"},
416
+ )
417
+ ch.success("GitHub token saved to navig vault.")
418
+ return
419
+ except Exception as exc:
420
+ ch.warning(f"Vault unavailable ({exc}), falling back to config file.")
421
+
422
+ # Fallback: write to ~/.navig/config.yaml
423
+ try:
424
+ cfg_path = config_dir() / "config.yaml"
425
+ cfg_path.parent.mkdir(parents=True, exist_ok=True)
426
+ cfg = load_yaml_for_update(cfg_path)
427
+ cfg.setdefault("github", {})["token"] = value
428
+ from navig.core.yaml_io import atomic_write_yaml
429
+
430
+ atomic_write_yaml(cfg, cfg_path)
431
+ ch.success(f"GitHub token saved to {cfg_path}")
432
+ except Exception as exc:
433
+ ch.error(f"Failed to save token: {exc}")
434
+ raise typer.Exit(1) from exc
435
+
436
+
437
+ @token_app.command("show")
438
+ def token_show():
439
+ """
440
+ 🔍 Display where the current GitHub token comes from (masked).
441
+
442
+ Examples:
443
+ navig github token show
444
+ """
445
+ token = _resolve_github_token()
446
+ if token:
447
+ masked = token[:6] + "..." + token[-4:]
448
+ ch.success(f"GitHub token found: {masked}")
449
+ # Identify source
450
+ try:
451
+ from navig.vault import get_vault # type: ignore
452
+
453
+ v = get_vault()
454
+ s = v.get("github_token", caller="navig-github.token_show")
455
+ if s and getattr(s, "value", None) == token:
456
+ ch.info("Source: navig vault")
457
+ return
458
+ except Exception: # noqa: BLE001
459
+ pass # best-effort; failure is non-critical
460
+ if os.environ.get("GITHUB_TOKEN", "").strip() == token:
461
+ ch.info("Source: GITHUB_TOKEN env var")
462
+ else:
463
+ ch.info("Source: ~/.navig/config.yaml")
464
+ else:
465
+ ch.warning(
466
+ "No GitHub token configured.\n"
467
+ "Set one with: navig github token set <your-token>\n"
468
+ "Without a token, the engine only accesses public repos at low rate limits."
469
+ )
470
+
471
+
472
+ @token_app.command("remove")
473
+ def token_remove():
474
+ """
475
+ 🗑 Remove the stored GitHub token.
476
+
477
+ Examples:
478
+ navig github token remove
479
+ """
480
+ removed = False
481
+
482
+ # Vault
483
+ try:
484
+ from navig.vault import get_vault # type: ignore
485
+
486
+ vault = get_vault()
487
+ secret = vault.get("github_token", caller="navig-github.token_remove")
488
+ if secret:
489
+ vault.delete(secret.id)
490
+ ch.success("GitHub token removed from navig vault.")
491
+ removed = True
492
+ except Exception: # noqa: BLE001
493
+ pass # best-effort; failure is non-critical
494
+
495
+ # Config file
496
+ try:
497
+ cfg_path = config_dir() / "config.yaml"
498
+ if cfg_path.exists():
499
+ cfg = load_yaml_for_update(cfg_path)
500
+ if cfg.get("github", {}).get("token"):
501
+ cfg["github"].pop("token")
502
+ from navig.core.yaml_io import atomic_write_yaml
503
+
504
+ atomic_write_yaml(cfg, cfg_path)
505
+ ch.success(f"GitHub token removed from {cfg_path}")
506
+ removed = True
507
+ except Exception: # noqa: BLE001
508
+ pass # best-effort; failure is non-critical
509
+
510
+ if not removed:
511
+ ch.info("No stored GitHub token found.")
512
+
513
+
514
+ @github_app.command("status", rich_help_panel="Setup")
515
+ def gh_status():
516
+ """
517
+ ℹ️ Show engine installation status and token configuration.
518
+
519
+ Examples:
520
+ navig github status
521
+ """
522
+ # engine availability (bundled engine — a hard dependency of navig-github)
523
+ if _engine_available():
524
+ try:
525
+ import navig_github.engine as fm # type: ignore
526
+
527
+ version = getattr(fm, "__version__", "unknown")
528
+ ch.success(f"GitHub engine bundled — version {version} (in-process)")
529
+ except Exception:
530
+ ch.success("GitHub engine bundled (in-process)")
531
+ else:
532
+ ch.error("GitHub engine MISSING (it is a dependency of navig-github).")
533
+ ch.info("Reinstall the plugin: pip install navig-github")
534
+
535
+ # Token check
536
+ token = _resolve_github_token()
537
+ if token:
538
+ masked = token[:6] + "..." + token[-4:]
539
+ ch.success(f"GitHub token configured: {masked}")
540
+ else:
541
+ ch.warning(
542
+ "No GitHub token — only public repos accessible at low rate limits.\n"
543
+ "Set one with: navig github token set <your-token>"
544
+ )
545
+
546
+
547
+ # ---------------------------------------------------------------------------
548
+ # Full engine mount — every engine command becomes `navig github <command>`
549
+ # ---------------------------------------------------------------------------
550
+
551
+ # Curated navig-side names that must not be shadowed by the engine's versions
552
+ # (`search`/`clone` keep navig's friendlier flags + git fallback; `backup`
553
+ # wraps user/org; `token`/`status` are navig-only).
554
+ _CURATED_CMDS = {"search", "backup", "clone", "token", "status"}
555
+
556
+ # --help organization for the mounted engine commands.
557
+ _ENGINE_PANELS: dict[str, str] = {
558
+ # backup & mirror
559
+ "user": "Backup & mirror", "org": "Backup & mirror", "repo": "Backup & mirror",
560
+ "starred": "Backup & mirror", "watched": "Backup & mirror", "gists": "Backup & mirror",
561
+ # exports
562
+ "issues": "Export", "pulls": "Export", "releases": "Export", "wiki": "Export",
563
+ "labels": "Export", "milestones": "Export", "workflows": "Export",
564
+ "attachments": "Export", "discussions": "Export", "followers": "Export",
565
+ "webhooks": "Export", "secrets": "Export", "profile": "Export",
566
+ # integrity & analytics
567
+ "verify": "Integrity & analytics", "diff": "Integrity & analytics",
568
+ "snapshot": "Integrity & analytics", "analytics": "Integrity & analytics",
569
+ "analytics-history": "Integrity & analytics",
570
+ # automation & config
571
+ "schedule-add": "Automation", "schedule-list": "Automation",
572
+ "schedule-remove": "Automation", "schedule-run": "Automation",
573
+ "notify-test": "Automation", "notify-status": "Automation",
574
+ "config-save": "Automation", "config-load": "Automation",
575
+ "config-list": "Automation", "config-delete": "Automation",
576
+ "config-export": "Automation", "config-import": "Automation",
577
+ "templates": "Automation", "template-show": "Automation",
578
+ "template-use": "Automation", "template-create": "Automation",
579
+ "template-delete": "Automation",
580
+ # destructive remote operations
581
+ "delete": "Danger zone (remote)", "transfer": "Danger zone (remote)",
582
+ }
583
+
584
+
585
+ def _mount_engine_commands() -> None:
586
+ """Mount every engine command (minus curated overrides) into `navig github`.
587
+
588
+ In-process: the engine's Typer CommandInfo objects are attached directly, so
589
+ `navig github issues owner/repo` runs the engine's own implementation. The
590
+ group callback above has already placed the vault token in GITHUB_TOKEN,
591
+ which each engine command reads via envvar. Import failure degrades to the
592
+ curated commands only (never blocks the CLI).
593
+ """
594
+ try:
595
+ from navig_github.engine.cli import app as engine_cli
596
+ except Exception: # noqa: BLE001 — degraded, never blocks boot
597
+ return
598
+ existing = {
599
+ (c.name or (c.callback.__name__.replace("_", "-") if c.callback else ""))
600
+ for c in github_app.registered_commands
601
+ }
602
+ for info in engine_cli.registered_commands:
603
+ name = info.name or (info.callback.__name__.replace("_", "-") if info.callback else "")
604
+ if not name or name in _CURATED_CMDS or name in existing:
605
+ continue
606
+ # unset = typer's DefaultPlaceholder, not None — only respect real strings
607
+ if not isinstance(getattr(info, "rich_help_panel", None), str):
608
+ info.rich_help_panel = _ENGINE_PANELS.get(name, "More (engine)")
609
+ github_app.registered_commands.append(info)
610
+
611
+
612
+ _mount_engine_commands()
@@ -0,0 +1,18 @@
1
+ """
2
+ The navig-github engine — GitHub backup, mirror, export & manage.
3
+
4
+ """
5
+
6
+ __version__ = "0.11.0"
7
+ __author__ = "miztizm"
8
+ __license__ = "MIT"
9
+
10
+ from .models import Config, Repository, TargetType, Visibility
11
+
12
+ __all__ = [
13
+ "Config",
14
+ "Repository",
15
+ "TargetType",
16
+ "Visibility",
17
+ "__version__",
18
+ ]
@@ -0,0 +1,11 @@
1
+ """
2
+ Entry point for running The GitHub engine as a module.
3
+
4
+ Usage: navig github
5
+
6
+ """
7
+
8
+ from .cli import app
9
+
10
+ if __name__ == "__main__":
11
+ app()