gator-command 1.0.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.
Files changed (110) hide show
  1. gator_command/__init__.py +2 -0
  2. gator_command/cli.py +137 -0
  3. gator_command/scripts/crawler.py +633 -0
  4. gator_command/scripts/dashboard/dashboard.css +982 -0
  5. gator_command/scripts/dashboard/dashboard.html +84 -0
  6. gator_command/scripts/dashboard/dashboard.js +419 -0
  7. gator_command/scripts/dashboard/views/audit.js +270 -0
  8. gator_command/scripts/dashboard/views/fleet.js +307 -0
  9. gator_command/scripts/dashboard/views/repo.js +599 -0
  10. gator_command/scripts/dashboard/views/settings.js +173 -0
  11. gator_command/scripts/dashboard/views/updates.js +308 -0
  12. gator_command/scripts/enforcer-prompt.md +22 -0
  13. gator_command/scripts/extract-claude-sessions.py +489 -0
  14. gator_command/scripts/extract-codex-sessions.py +477 -0
  15. gator_command/scripts/extract-gemini-sessions.py +410 -0
  16. gator_command/scripts/gator-audit.py +956 -0
  17. gator_command/scripts/gator-charter-draft.py +919 -0
  18. gator_command/scripts/gator-charter-lint.py +427 -0
  19. gator_command/scripts/gator-charter-verify.py +606 -0
  20. gator_command/scripts/gator-dashboard.py +1271 -0
  21. gator_command/scripts/gator-deploy.py +916 -0
  22. gator_command/scripts/gator-drift.py +569 -0
  23. gator_command/scripts/gator-enforce.py +82 -0
  24. gator_command/scripts/gator-fleet-intel.py +460 -0
  25. gator_command/scripts/gator-fleet-report.py +615 -0
  26. gator_command/scripts/gator-init-command-post.py +315 -0
  27. gator_command/scripts/gator-init.py +434 -0
  28. gator_command/scripts/gator-machine-id.py +153 -0
  29. gator_command/scripts/gator-policy-status.py +631 -0
  30. gator_command/scripts/gator-pulse.py +459 -0
  31. gator_command/scripts/gator-repo-status.py +649 -0
  32. gator_command/scripts/gator-session-common.py +372 -0
  33. gator_command/scripts/gator-session-sink.py +831 -0
  34. gator_command/scripts/gator-sessions.py +1244 -0
  35. gator_command/scripts/gator-update.py +615 -0
  36. gator_command/scripts/gator-version.py +38 -0
  37. gator_command/scripts/gator_core.py +489 -0
  38. gator_command/scripts/gator_remote.py +381 -0
  39. gator_command/scripts/gator_runtime.py +142 -0
  40. gator_command/scripts/gatorize-actions.sh +989 -0
  41. gator_command/scripts/gatorize-lib.sh +166 -0
  42. gator_command/scripts/gatorize-post.sh +394 -0
  43. gator_command/scripts/gatorize.py +1163 -0
  44. gator_command/scripts/gatorize.sh +185 -0
  45. gator_command/scripts/generate_markdown.py +212 -0
  46. gator_command/scripts/generate_wiki.py +424 -0
  47. gator_command/scripts/graph_health.py +780 -0
  48. gator_command/scripts/memex-lint.py +286 -0
  49. gator_command/scripts/memex-lint.sh +205 -0
  50. gator_command/scripts/memex.py +1472 -0
  51. gator_command/scripts/memex_formatters.py +191 -0
  52. gator_command/scripts/memex_state.py +236 -0
  53. gator_command/scripts/spawn.py +650 -0
  54. gator_command/templates/gator-starter/blueprints/README.md +32 -0
  55. gator_command/templates/gator-starter/charterignore +53 -0
  56. gator_command/templates/gator-starter/charters/README.md +178 -0
  57. gator_command/templates/gator-starter/charters/_template.md +31 -0
  58. gator_command/templates/gator-starter/commands/commit.md +33 -0
  59. gator_command/templates/gator-starter/commands/init.md +11 -0
  60. gator_command/templates/gator-starter/commands/update.md +5 -0
  61. gator_command/templates/gator-starter/constitution.md +165 -0
  62. gator_command/templates/gator-starter/field-guides/README.md +25 -0
  63. gator_command/templates/gator-starter/gator-start-up.md +119 -0
  64. gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
  65. gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
  66. gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
  67. gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
  68. gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
  69. gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
  70. gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
  71. gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
  72. gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
  73. gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
  74. gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
  75. gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
  76. gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
  77. gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
  78. gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
  79. gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
  80. gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
  81. gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
  82. gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
  83. gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
  84. gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
  85. gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
  86. gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
  87. gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
  88. gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
  89. gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
  90. gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
  91. gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
  92. gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
  93. gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
  94. gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
  95. gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
  96. gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
  97. gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
  98. gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
  99. gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
  100. gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
  101. gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
  102. gator_command/templates/gator-starter/sessions/.gitignore +7 -0
  103. gator_command/templates/gator-starter/vault/.gitkeep +0 -0
  104. gator_command/templates/gator-starter/whiteboard.md +5 -0
  105. gator_command-1.0.0.dist-info/METADATA +122 -0
  106. gator_command-1.0.0.dist-info/RECORD +110 -0
  107. gator_command-1.0.0.dist-info/WHEEL +5 -0
  108. gator_command-1.0.0.dist-info/entry_points.txt +2 -0
  109. gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
  110. gator_command-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gator_remote.py — Remote fleet scanning via bare clone cache.
4
+
5
+ Enables fleet reporting without local checkouts. Uses bare git clones
6
+ cached in ~/.gator/fleet-cache/ to read governance state from remote
7
+ refs using `git show` and `git log`.
8
+
9
+ Resolution order (in gator-fleet-report.py):
10
+ 1. Try local path → full scan (existing behavior)
11
+ 2. If local inaccessible AND remote exists → thin-fetch scan (this module)
12
+ 3. If neither accessible → report as unreachable
13
+
14
+ Not a standalone script — imported by gator-fleet-report.py and gator-audit.py.
15
+
16
+ @reads: ~/.gator/fleet-cache/ (bare clones), remote git refs
17
+ @writes: ~/.gator/fleet-cache/ (creates/updates bare clones)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import hashlib
23
+ import re
24
+ import subprocess
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ from gator_core import git
29
+
30
+
31
+ CACHE_DIR = Path.home() / ".gator" / "fleet-cache"
32
+
33
+
34
+ def _cache_key(repo_name: str, remote_url: str) -> str:
35
+ """Collision-proof cache directory name.
36
+
37
+ Incorporates a short hash of the remote URL so two different remotes
38
+ with the same registry name (e.g., 'api' in two orgs) get separate
39
+ bare clones instead of silently sharing/overwriting.
40
+ """
41
+ url_hash = hashlib.sha256(remote_url.encode()).hexdigest()[:8]
42
+ return f"{repo_name}-{url_hash}.git"
43
+
44
+
45
+ def _git_bare(*args, git_dir: Path, timeout: int = 30):
46
+ """Run a git command against a bare repo. Returns (stdout, success)."""
47
+ try:
48
+ result = subprocess.run(
49
+ ["git", f"--git-dir={git_dir}"] + list(args),
50
+ capture_output=True, text=True, timeout=timeout,
51
+ )
52
+ return result.stdout.strip(), result.returncode == 0
53
+ except (OSError, subprocess.TimeoutExpired):
54
+ return "", False
55
+
56
+
57
+ def ensure_cache(repo_name: str, remote_url: str) -> Path | None:
58
+ """Clone bare if not cached, fetch if cached. Returns cache path or None.
59
+
60
+ Cache key incorporates a hash of the remote URL to prevent collisions
61
+ when two different remotes share the same registry name.
62
+ """
63
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
64
+ cache_path = CACHE_DIR / _cache_key(repo_name, remote_url)
65
+
66
+ # Migration: if old-style name exists (no hash), rename it
67
+ legacy_path = CACHE_DIR / f"{repo_name}.git"
68
+ if legacy_path.is_dir() and not cache_path.is_dir():
69
+ legacy_path.rename(cache_path)
70
+
71
+ if cache_path.is_dir():
72
+ # Verify the cached remote matches what we expect
73
+ current_url, _ = _git_bare(
74
+ "remote", "get-url", "origin", git_dir=cache_path
75
+ )
76
+ if current_url and current_url != remote_url:
77
+ _git_bare(
78
+ "remote", "set-url", "origin", remote_url,
79
+ git_dir=cache_path,
80
+ )
81
+ # Update existing cache
82
+ _, ok = _git_bare("fetch", "origin", "--prune", git_dir=cache_path)
83
+ return cache_path if ok else None
84
+ else:
85
+ # Initial bare clone
86
+ try:
87
+ result = subprocess.run(
88
+ ["git", "clone", "--bare", remote_url, str(cache_path)],
89
+ capture_output=True, text=True, timeout=60,
90
+ )
91
+ if result.returncode == 0:
92
+ return cache_path
93
+ except (OSError, subprocess.TimeoutExpired):
94
+ pass
95
+ return None
96
+
97
+
98
+ def git_show(cache_path: Path, ref: str, filepath: str) -> str | None:
99
+ """Read a file from a ref without checkout. Returns content or None."""
100
+ content, ok = _git_bare("show", f"{ref}:{filepath}", git_dir=cache_path)
101
+ if ok:
102
+ return content
103
+ return None
104
+
105
+
106
+ def git_ls_tree(cache_path: Path, ref: str, dirpath: str) -> list[str]:
107
+ """List file names in a directory from a ref."""
108
+ output, ok = _git_bare(
109
+ "ls-tree", "--name-only", ref, dirpath + "/",
110
+ git_dir=cache_path,
111
+ )
112
+ if not ok or not output:
113
+ return []
114
+ # ls-tree returns full paths relative to repo root
115
+ return [line.split("/")[-1] for line in output.splitlines() if line.strip()]
116
+
117
+
118
+ def git_log_trailers(cache_path: Path, ref: str, count: int = 10) -> dict:
119
+ """Extract Gator-* trailers from the most recent commit on ref."""
120
+ raw, ok = _git_bare(
121
+ "log", f"-{count}", "--format=%(trailers)", ref,
122
+ git_dir=cache_path,
123
+ )
124
+ if not ok or not raw:
125
+ return {}
126
+
127
+ # Return trailers from the most recent commit that has them
128
+ trailers = {}
129
+ for line in raw.splitlines():
130
+ if line.startswith("Gator-"):
131
+ key, _, value = line.partition(":")
132
+ trailers[key.strip()] = value.strip()
133
+ elif trailers:
134
+ # We found trailers, stop at the first empty block boundary
135
+ break
136
+
137
+ return trailers
138
+
139
+
140
+ def git_log_last_commit(cache_path: Path, ref: str) -> dict | None:
141
+ """Get last commit info from a ref."""
142
+ log_line, ok = _git_bare(
143
+ "log", "-1", "--format=%h|%s|%cr|%ci", ref,
144
+ git_dir=cache_path,
145
+ )
146
+ if not ok or not log_line:
147
+ return None
148
+
149
+ parts = log_line.split("|", 3)
150
+ if len(parts) < 4:
151
+ return None
152
+
153
+ return {
154
+ "hash": parts[0],
155
+ "message": parts[1],
156
+ "age": parts[2],
157
+ "date": parts[3],
158
+ }
159
+
160
+
161
+ def git_commit_count(cache_path: Path, ref: str, days: int = 30) -> int:
162
+ """Count commits in the last N days on a ref."""
163
+ count_str, ok = _git_bare(
164
+ "rev-list", "--count", f"--since={days} days ago", ref,
165
+ git_dir=cache_path,
166
+ )
167
+ try:
168
+ return int(count_str)
169
+ except ValueError:
170
+ return 0
171
+
172
+
173
+ def _resolve_ref(cache_path: Path) -> str:
174
+ """Find the best ref to read from in a bare clone.
175
+
176
+ Bare clones from `git clone --bare` store refs as refs/heads/main
177
+ (not origin/main). After `git fetch origin`, they may also have
178
+ refs/remotes/origin/main. Try both patterns.
179
+ """
180
+ # Try origin/ refs first (populated after fetch)
181
+ for ref in ("origin/main", "origin/master", "origin/dev"):
182
+ _, ok = _git_bare("rev-parse", "--verify", ref, git_dir=cache_path)
183
+ if ok:
184
+ return ref
185
+ # Try bare-clone native refs (refs/heads/)
186
+ for ref in ("main", "master", "dev"):
187
+ _, ok = _git_bare("rev-parse", "--verify", ref, git_dir=cache_path)
188
+ if ok:
189
+ return ref
190
+ return "HEAD"
191
+
192
+
193
+ def read_gator_state_remote(cache_path: Path, ref: str | None = None) -> dict:
194
+ """Read governance state from a bare cache. Parallel to read_gator_state()."""
195
+ if ref is None:
196
+ ref = _resolve_ref(cache_path)
197
+
198
+ state = {
199
+ "gatorized": False,
200
+ "generation": 0,
201
+ "policy_version": None,
202
+ "charters": 0,
203
+ "functions": 0,
204
+ "threads": 0,
205
+ "issues": 0,
206
+ "mission_summary": None,
207
+ "has_hooks": False,
208
+ "hooks_sources": False,
209
+ "hooks_installed": False, # Cannot determine remotely
210
+ "scan_mode": "remote",
211
+ "ref": ref,
212
+ }
213
+
214
+ # Check if .gator/ exists
215
+ ls_gator = git_ls_tree(cache_path, ref, ".gator")
216
+ if not ls_gator:
217
+ return state
218
+ state["gatorized"] = True
219
+
220
+ # Generation
221
+ version_content = git_show(cache_path, ref, ".gator/.gator-version")
222
+ if version_content:
223
+ for line in version_content.splitlines():
224
+ if line.startswith("generation:"):
225
+ try:
226
+ state["generation"] = int(line.split(":", 1)[1].strip())
227
+ except ValueError:
228
+ pass
229
+
230
+ # Policy version
231
+ cp_content = git_show(cache_path, ref, ".gator/command-post.md")
232
+ if cp_content:
233
+ for line in cp_content.splitlines():
234
+ if line.startswith("version:"):
235
+ state["policy_version"] = line.split(":", 1)[1].strip()
236
+ break
237
+
238
+ # Charters
239
+ charter_files = git_ls_tree(cache_path, ref, ".gator/charters")
240
+ skip = {"_template.md", "README.md", "INDEX.md", ".gitkeep"}
241
+ real_charters = [f for f in charter_files if f.endswith(".md") and f not in skip]
242
+ state["charters"] = len(real_charters)
243
+
244
+ # Function count (read each charter)
245
+ func_count = 0
246
+ for charter_name in real_charters:
247
+ content = git_show(cache_path, ref, f".gator/charters/{charter_name}")
248
+ if content:
249
+ for line in content.splitlines():
250
+ if line.strip().startswith("### ") and "(" in line:
251
+ func_count += 1
252
+ state["functions"] = func_count
253
+
254
+ # Threads
255
+ for subdir in ("active-threads", "threads"):
256
+ entries = git_ls_tree(cache_path, ref, f".gator/{subdir}")
257
+ state["threads"] += len([
258
+ f for f in entries if f.endswith(".md") and f != ".gitkeep"
259
+ ])
260
+
261
+ # Issues
262
+ issues_content = git_show(cache_path, ref, ".gator/issues.md")
263
+ if issues_content:
264
+ for line in issues_content.splitlines():
265
+ if "**Status**: Open" in line or "**Status**: Working" in line:
266
+ state["issues"] += 1
267
+
268
+ # Mission summary
269
+ mission_content = git_show(cache_path, ref, ".gator/mission.md")
270
+ if mission_content:
271
+ for line in mission_content.splitlines():
272
+ stripped = line.strip()
273
+ if stripped and not stripped.startswith("#") and not stripped.startswith("["):
274
+ state["mission_summary"] = stripped[:80]
275
+ break
276
+
277
+ # Hook sources (can detect from committed files, not installed state)
278
+ hook_files = git_ls_tree(cache_path, ref, ".gator/scripts/hooks")
279
+ state["hooks_sources"] = len(hook_files) > 0
280
+ state["has_hooks"] = state["hooks_sources"]
281
+
282
+ return state
283
+
284
+
285
+ def scan_repo_remote(repo_entry: dict) -> dict:
286
+ """Scan a repo via thin-fetch. Parallel to scan_repo() in fleet-report."""
287
+ name = repo_entry["name"]
288
+ remote = repo_entry.get("remote", "")
289
+
290
+ report = {
291
+ "name": name,
292
+ "path": repo_entry.get("path", "—"),
293
+ "remote": remote,
294
+ "registered": repo_entry.get("registered", "—"),
295
+ "accessible": False,
296
+ "scan_mode": "remote",
297
+ }
298
+
299
+ if not remote or remote == "—":
300
+ report["error"] = "no remote URL in registry"
301
+ return report
302
+
303
+ # Ensure bare cache exists and is current
304
+ cache_path = ensure_cache(name, remote)
305
+ if not cache_path:
306
+ report["error"] = f"failed to fetch {remote}"
307
+ return report
308
+
309
+ report["accessible"] = True
310
+ ref = _resolve_ref(cache_path)
311
+
312
+ # Git state (from remote ref)
313
+ report["branch"] = ref.split("/")[-1] # Best guess from ref
314
+ report["last_commit"] = git_log_last_commit(cache_path, ref)
315
+ report["commits_30d"] = git_commit_count(cache_path, ref, 30)
316
+ report["working_tree"] = "remote (unknown)"
317
+ report["trailers"] = git_log_trailers(cache_path, ref)
318
+
319
+ # .gator/ state
320
+ gator = read_gator_state_remote(cache_path, ref)
321
+ report.update(gator)
322
+
323
+ return report
324
+
325
+
326
+ def list_committed_sessions_remote(
327
+ cache_path: Path, ref: str | None = None
328
+ ) -> list[str]:
329
+ """List committed session summary filenames from a bare cache."""
330
+ if ref is None:
331
+ ref = _resolve_ref(cache_path)
332
+ entries = git_ls_tree(cache_path, ref, ".gator/sessions")
333
+ return [f for f in entries if f.endswith(".md")]
334
+
335
+
336
+ def read_session_summary_remote(
337
+ cache_path: Path, filename: str, ref: str | None = None
338
+ ) -> str | None:
339
+ """Read a specific session summary from a bare cache."""
340
+ if ref is None:
341
+ ref = _resolve_ref(cache_path)
342
+ return git_show(cache_path, ref, f".gator/sessions/{filename}")
343
+
344
+
345
+ def get_cache_status() -> list[dict]:
346
+ """Report on all cached repos."""
347
+ if not CACHE_DIR.is_dir():
348
+ return []
349
+
350
+ status = []
351
+ for item in sorted(CACHE_DIR.iterdir()):
352
+ if item.is_dir() and item.name.endswith(".git"):
353
+ # Name format: "repo-name-abcdef12.git" (name + 8-char URL hash)
354
+ # Strip .git suffix, then strip the last -hash segment
355
+ bare_name = item.name[:-4] # remove .git
356
+ # Split on last dash to separate name from hash
357
+ parts = bare_name.rsplit("-", 1)
358
+ if len(parts) == 2 and len(parts[1]) == 8:
359
+ repo_name = parts[0]
360
+ else:
361
+ repo_name = bare_name # legacy format without hash
362
+ # Get remote URL
363
+ url, _ = _git_bare(
364
+ "remote", "get-url", "origin", git_dir=item
365
+ )
366
+ # Get last fetch time (use FETCH_HEAD mtime)
367
+ fetch_head = item / "FETCH_HEAD"
368
+ last_fetch = None
369
+ if fetch_head.exists():
370
+ import datetime as dt
371
+ mtime = fetch_head.stat().st_mtime
372
+ last_fetch = dt.datetime.fromtimestamp(mtime).isoformat()
373
+
374
+ status.append({
375
+ "name": repo_name,
376
+ "cache_path": str(item),
377
+ "remote": url or "unknown",
378
+ "last_fetch": last_fetch,
379
+ })
380
+
381
+ return status
@@ -0,0 +1,142 @@
1
+ """
2
+ gator_runtime.py — Runtime context resolver for Gator.
3
+
4
+ Answers the questions every Gator script needs:
5
+ - What mode am I running in? (source-checkout, public-clone, installed-package)
6
+ - Where are the scripts?
7
+ - Where are the templates?
8
+ - Where is the repo root I'm operating on?
9
+ - Where is the command post, if any?
10
+
11
+ This module is the seam that makes pipx-installed Gator possible.
12
+ Scripts import this instead of doing SCRIPTS_DIR.parent.parent arithmetic.
13
+ """
14
+
15
+ from pathlib import Path
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Runtime mode detection
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def get_runtime_mode():
23
+ """Detect how Gator is running.
24
+
25
+ Returns one of:
26
+ - 'source-checkout': running from the gator-command development repo
27
+ - 'public-clone': running from a deployed gator clone (gator-engine/ layout)
28
+ - 'installed-package': running from a pip/pipx installed package
29
+
30
+ Detection order:
31
+ 1. If __file__ is inside a site-packages path → installed-package
32
+ 2. If the scripts dir has a sibling gator-engine/ → public-clone
33
+ 3. Otherwise → source-checkout
34
+ """
35
+ scripts_dir = Path(__file__).resolve().parent
36
+ file_str = str(scripts_dir).lower()
37
+
38
+ # Installed package: scripts live inside site-packages
39
+ if "site-packages" in file_str or "dist-packages" in file_str:
40
+ return "installed-package"
41
+
42
+ # Public clone: gator-engine/ exists as a sibling to the scripts parent
43
+ repo_root = scripts_dir.parent.parent
44
+ if (repo_root / "gator-engine").is_dir():
45
+ return "public-clone"
46
+
47
+ return "source-checkout"
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Script and template resolution
52
+ # ---------------------------------------------------------------------------
53
+
54
+ def get_scripts_dir():
55
+ """Return the directory containing Gator runtime scripts."""
56
+ return Path(__file__).resolve().parent
57
+
58
+
59
+ def get_templates_dir():
60
+ """Return the gator-starter templates directory, or None if not found.
61
+
62
+ Resolution order:
63
+ 1. Source checkout: gator-command/templates/gator-starter/
64
+ 2. Public clone: gator-engine/templates/gator-starter/
65
+ 3. Installed package: package resources (future — returns None for now)
66
+ """
67
+ scripts_dir = Path(__file__).resolve().parent
68
+ mode = get_runtime_mode()
69
+
70
+ if mode == "source-checkout":
71
+ candidate = scripts_dir.parent / "templates" / "gator-starter"
72
+ if candidate.is_dir():
73
+ return candidate
74
+
75
+ elif mode == "public-clone":
76
+ repo_root = scripts_dir.parent.parent
77
+ candidate = repo_root / "gator-engine" / "templates" / "gator-starter"
78
+ if candidate.is_dir():
79
+ return candidate
80
+
81
+ elif mode == "installed-package":
82
+ # Future: use importlib.resources to resolve from package data
83
+ # For now, fall through to None
84
+ pass
85
+
86
+ return None
87
+
88
+
89
+ # ---------------------------------------------------------------------------
90
+ # Repo root resolution
91
+ # ---------------------------------------------------------------------------
92
+
93
+ def get_repo_root(cwd=None):
94
+ """Find the repo root by walking up from cwd to .git/.
95
+
96
+ Returns Path or None. This answers "what repo am I operating on?"
97
+ — not "where is Gator installed from."
98
+ """
99
+ candidate = Path(cwd).resolve() if cwd else Path.cwd().resolve()
100
+ while candidate != candidate.parent:
101
+ if (candidate / ".git").is_dir():
102
+ return candidate
103
+ candidate = candidate.parent
104
+ return None
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Command post resolution
109
+ # ---------------------------------------------------------------------------
110
+
111
+ def get_command_post_root(cwd=None):
112
+ """Find the command post root, if one exists.
113
+
114
+ Resolution order:
115
+ 1. Source checkout mode: the repo root containing this script IS the command post
116
+ 2. Public clone mode: the repo root containing this script IS the command post
117
+ 3. Installed package mode: look for a command post via thin link in the target repo
118
+ 4. Fallback: use gator_core.find_command_post() if available
119
+
120
+ Returns Path or None.
121
+ """
122
+ mode = get_runtime_mode()
123
+
124
+ if mode in ("source-checkout", "public-clone"):
125
+ # The repo this script lives in is the command post
126
+ scripts_dir = Path(__file__).resolve().parent
127
+ repo_root = scripts_dir.parent.parent
128
+ if (repo_root / ".git").is_dir():
129
+ return repo_root
130
+
131
+ # For installed-package mode or fallback: try gator_core
132
+ try:
133
+ scripts_dir = get_scripts_dir()
134
+ import sys
135
+ if str(scripts_dir) not in sys.path:
136
+ sys.path.insert(0, str(scripts_dir))
137
+ from gator_core import find_command_post
138
+ return find_command_post()
139
+ except (ImportError, Exception):
140
+ pass
141
+
142
+ return None