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.
- gator_command/__init__.py +2 -0
- gator_command/cli.py +137 -0
- gator_command/scripts/crawler.py +633 -0
- gator_command/scripts/dashboard/dashboard.css +982 -0
- gator_command/scripts/dashboard/dashboard.html +84 -0
- gator_command/scripts/dashboard/dashboard.js +419 -0
- gator_command/scripts/dashboard/views/audit.js +270 -0
- gator_command/scripts/dashboard/views/fleet.js +307 -0
- gator_command/scripts/dashboard/views/repo.js +599 -0
- gator_command/scripts/dashboard/views/settings.js +173 -0
- gator_command/scripts/dashboard/views/updates.js +308 -0
- gator_command/scripts/enforcer-prompt.md +22 -0
- gator_command/scripts/extract-claude-sessions.py +489 -0
- gator_command/scripts/extract-codex-sessions.py +477 -0
- gator_command/scripts/extract-gemini-sessions.py +410 -0
- gator_command/scripts/gator-audit.py +956 -0
- gator_command/scripts/gator-charter-draft.py +919 -0
- gator_command/scripts/gator-charter-lint.py +427 -0
- gator_command/scripts/gator-charter-verify.py +606 -0
- gator_command/scripts/gator-dashboard.py +1271 -0
- gator_command/scripts/gator-deploy.py +916 -0
- gator_command/scripts/gator-drift.py +569 -0
- gator_command/scripts/gator-enforce.py +82 -0
- gator_command/scripts/gator-fleet-intel.py +460 -0
- gator_command/scripts/gator-fleet-report.py +615 -0
- gator_command/scripts/gator-init-command-post.py +315 -0
- gator_command/scripts/gator-init.py +434 -0
- gator_command/scripts/gator-machine-id.py +153 -0
- gator_command/scripts/gator-policy-status.py +631 -0
- gator_command/scripts/gator-pulse.py +459 -0
- gator_command/scripts/gator-repo-status.py +649 -0
- gator_command/scripts/gator-session-common.py +372 -0
- gator_command/scripts/gator-session-sink.py +831 -0
- gator_command/scripts/gator-sessions.py +1244 -0
- gator_command/scripts/gator-update.py +615 -0
- gator_command/scripts/gator-version.py +38 -0
- gator_command/scripts/gator_core.py +489 -0
- gator_command/scripts/gator_remote.py +381 -0
- gator_command/scripts/gator_runtime.py +142 -0
- gator_command/scripts/gatorize-actions.sh +989 -0
- gator_command/scripts/gatorize-lib.sh +166 -0
- gator_command/scripts/gatorize-post.sh +394 -0
- gator_command/scripts/gatorize.py +1163 -0
- gator_command/scripts/gatorize.sh +185 -0
- gator_command/scripts/generate_markdown.py +212 -0
- gator_command/scripts/generate_wiki.py +424 -0
- gator_command/scripts/graph_health.py +780 -0
- gator_command/scripts/memex-lint.py +286 -0
- gator_command/scripts/memex-lint.sh +205 -0
- gator_command/scripts/memex.py +1472 -0
- gator_command/scripts/memex_formatters.py +191 -0
- gator_command/scripts/memex_state.py +236 -0
- gator_command/scripts/spawn.py +650 -0
- gator_command/templates/gator-starter/blueprints/README.md +32 -0
- gator_command/templates/gator-starter/charterignore +53 -0
- gator_command/templates/gator-starter/charters/README.md +178 -0
- gator_command/templates/gator-starter/charters/_template.md +31 -0
- gator_command/templates/gator-starter/commands/commit.md +33 -0
- gator_command/templates/gator-starter/commands/init.md +11 -0
- gator_command/templates/gator-starter/commands/update.md +5 -0
- gator_command/templates/gator-starter/constitution.md +165 -0
- gator_command/templates/gator-starter/field-guides/README.md +25 -0
- gator_command/templates/gator-starter/gator-start-up.md +119 -0
- gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
- gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
- gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
- gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
- gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
- gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
- gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
- gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
- gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
- gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
- gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
- gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
- gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
- gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
- gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
- gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
- gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
- gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
- gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
- gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
- gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
- gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
- gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
- gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
- gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
- gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
- gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
- gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
- gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
- gator_command/templates/gator-starter/sessions/.gitignore +7 -0
- gator_command/templates/gator-starter/vault/.gitkeep +0 -0
- gator_command/templates/gator-starter/whiteboard.md +5 -0
- gator_command-1.0.0.dist-info/METADATA +122 -0
- gator_command-1.0.0.dist-info/RECORD +110 -0
- gator_command-1.0.0.dist-info/WHEEL +5 -0
- gator_command-1.0.0.dist-info/entry_points.txt +2 -0
- gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|