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,489 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator_core.py — Shared infrastructure for Gator Command scripts.
|
|
4
|
+
|
|
5
|
+
Centralizes utilities that were previously duplicated across 6+ scripts:
|
|
6
|
+
version resolution, command-post discovery, registry parsing, path
|
|
7
|
+
normalization, git helpers, and stdout setup.
|
|
8
|
+
|
|
9
|
+
Usage (automatic — scripts/ is on sys.path when any sibling runs):
|
|
10
|
+
from gator_core import get_version, find_command_post, parse_registry, git
|
|
11
|
+
|
|
12
|
+
Not a standalone script — imported by other gator-* scripts.
|
|
13
|
+
|
|
14
|
+
@reads: filesystem, git tags, registry.md, .gator/command-post.md
|
|
15
|
+
@writes: nothing (pure library)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Version
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def _read_version_file(start=None):
|
|
31
|
+
"""Read VERSION file by walking up from start (or this script's dir)."""
|
|
32
|
+
d = start or Path(__file__).resolve().parent
|
|
33
|
+
for _ in range(5):
|
|
34
|
+
vf = d / "VERSION"
|
|
35
|
+
if vf.is_file():
|
|
36
|
+
text = vf.read_text(encoding="utf-8", errors="replace").strip()
|
|
37
|
+
if text:
|
|
38
|
+
return f"v{text}" if not text.startswith("v") else text
|
|
39
|
+
d = d.parent
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_version(cwd=None):
|
|
44
|
+
"""Get the current version from git tags, falling back to VERSION file.
|
|
45
|
+
|
|
46
|
+
Resolution order:
|
|
47
|
+
1. git describe --tags --always (exact tag or tag+offset)
|
|
48
|
+
2. VERSION file (for deployed repos without full git history)
|
|
49
|
+
3. git rev-parse --short HEAD (bare hash)
|
|
50
|
+
4. "dev"
|
|
51
|
+
"""
|
|
52
|
+
if cwd is None:
|
|
53
|
+
cwd = Path(__file__).resolve().parent
|
|
54
|
+
for _ in range(5):
|
|
55
|
+
if (cwd / ".git").is_dir():
|
|
56
|
+
break
|
|
57
|
+
cwd = cwd.parent
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
["git", "describe", "--tags", "--always"],
|
|
62
|
+
capture_output=True, text=True, cwd=cwd, timeout=5,
|
|
63
|
+
)
|
|
64
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
65
|
+
version = result.stdout.strip()
|
|
66
|
+
if version.startswith("v"):
|
|
67
|
+
return version
|
|
68
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# No tagged version from git — try VERSION file
|
|
72
|
+
from_file = _read_version_file(cwd)
|
|
73
|
+
if from_file:
|
|
74
|
+
return from_file
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
79
|
+
capture_output=True, text=True, cwd=cwd, timeout=5,
|
|
80
|
+
)
|
|
81
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
82
|
+
return f"dev-g{result.stdout.strip()}"
|
|
83
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
return "dev"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_version_short(cwd=None):
|
|
90
|
+
"""Get just the tag version, without commit count or hash.
|
|
91
|
+
|
|
92
|
+
Returns "v0.1.0" whether on the tag or after it.
|
|
93
|
+
Falls back to "dev" if no tags.
|
|
94
|
+
"""
|
|
95
|
+
version = get_version(cwd)
|
|
96
|
+
if version.startswith("v"):
|
|
97
|
+
return version.split("-")[0]
|
|
98
|
+
return version
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Path helpers
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
def normalize_path(raw_path):
|
|
106
|
+
"""Normalize MSYS2/Git Bash paths (/c/Users/...) to native Windows.
|
|
107
|
+
|
|
108
|
+
Converts /c/Users/... to C:/Users/... for cross-platform compatibility.
|
|
109
|
+
No-op on paths that don't match the MSYS2 pattern.
|
|
110
|
+
"""
|
|
111
|
+
if re.match(r'^/[a-zA-Z]/', raw_path):
|
|
112
|
+
raw_path = raw_path[1].upper() + ":" + raw_path[2:]
|
|
113
|
+
return raw_path
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def find_command_post(start_path=None):
|
|
117
|
+
"""Find the gator-command repo root.
|
|
118
|
+
|
|
119
|
+
Walks up from start_path looking for a directory containing
|
|
120
|
+
gator-command/mission.md. Used by command-post scripts (fleet-report,
|
|
121
|
+
drift, audit, init-command-post).
|
|
122
|
+
"""
|
|
123
|
+
path = Path(start_path) if start_path else Path.cwd()
|
|
124
|
+
path = path.resolve()
|
|
125
|
+
|
|
126
|
+
if (path / "gator-command" / "mission.md").exists():
|
|
127
|
+
return path
|
|
128
|
+
if (path / "mission.md").exists() and (path / "active-threads").is_dir():
|
|
129
|
+
return path.parent
|
|
130
|
+
for parent in path.parents:
|
|
131
|
+
if (parent / "gator-command" / "mission.md").exists():
|
|
132
|
+
return parent
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def find_gator_root(start_path=None):
|
|
137
|
+
"""Walk up from start_path looking for .gator/ directory.
|
|
138
|
+
|
|
139
|
+
Used by per-repo scripts (init, update) to find the governed repo root.
|
|
140
|
+
"""
|
|
141
|
+
path = Path(start_path) if start_path else Path.cwd()
|
|
142
|
+
path = path.resolve()
|
|
143
|
+
if (path / ".gator").is_dir():
|
|
144
|
+
return path
|
|
145
|
+
for parent in path.parents:
|
|
146
|
+
if (parent / ".gator").is_dir():
|
|
147
|
+
return parent
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def resolve_thin_link(gator_dir):
|
|
152
|
+
"""Read the command-post thin link and resolve the path.
|
|
153
|
+
|
|
154
|
+
Used by per-repo scripts (update) to find the command post from
|
|
155
|
+
a governed repo's .gator/command-post.md.
|
|
156
|
+
"""
|
|
157
|
+
cp_file = gator_dir / "command-post.md"
|
|
158
|
+
if not cp_file.exists():
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
text = cp_file.read_text(encoding="utf-8", errors="replace")
|
|
162
|
+
|
|
163
|
+
# Try command-post-absolute first (most reliable on same machine)
|
|
164
|
+
for line in text.splitlines():
|
|
165
|
+
if line.startswith("command-post-absolute:"):
|
|
166
|
+
raw = line.split(":", 1)[1].strip().split("#")[0].strip()
|
|
167
|
+
if raw:
|
|
168
|
+
resolved = Path(normalize_path(raw))
|
|
169
|
+
if resolved.is_dir():
|
|
170
|
+
return resolved
|
|
171
|
+
|
|
172
|
+
# Fall back to command-post (relative or absolute)
|
|
173
|
+
for line in text.splitlines():
|
|
174
|
+
if line.startswith("command-post:") and not line.startswith("command-post-absolute:"):
|
|
175
|
+
raw = line.split(":", 1)[1].strip()
|
|
176
|
+
if raw:
|
|
177
|
+
# Try as relative from repo root
|
|
178
|
+
resolved = (gator_dir.parent / raw).resolve()
|
|
179
|
+
if resolved.is_dir():
|
|
180
|
+
return resolved
|
|
181
|
+
# Try as absolute
|
|
182
|
+
resolved = Path(normalize_path(raw))
|
|
183
|
+
if resolved.is_dir():
|
|
184
|
+
return resolved
|
|
185
|
+
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------------------
|
|
190
|
+
# Product source and repo topology
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
def read_product_source(gator_dir):
|
|
194
|
+
"""Read .gator/product-source.json. Returns dict or None if missing/corrupt."""
|
|
195
|
+
ps_file = Path(gator_dir) / "product-source.json"
|
|
196
|
+
if not ps_file.exists():
|
|
197
|
+
return None
|
|
198
|
+
try:
|
|
199
|
+
data = json.loads(ps_file.read_text(encoding="utf-8"))
|
|
200
|
+
return data if isinstance(data, dict) else None
|
|
201
|
+
except (json.JSONDecodeError, OSError):
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def resolve_template_source(gator_dir, source_override=None):
|
|
206
|
+
"""Resolve the template source directory for product updates.
|
|
207
|
+
|
|
208
|
+
Resolution order:
|
|
209
|
+
1. Explicit --source override
|
|
210
|
+
2. .gator/product-source.json (gator_root + template_dir)
|
|
211
|
+
3. Thin link -> command post -> templates (legacy fallback)
|
|
212
|
+
4. None (caller should fail with clear message)
|
|
213
|
+
|
|
214
|
+
Returns (template_path, gator_root) or (None, None).
|
|
215
|
+
"""
|
|
216
|
+
# 1. Explicit override
|
|
217
|
+
if source_override:
|
|
218
|
+
root = Path(source_override).resolve()
|
|
219
|
+
for tpl_rel in ("gator-engine/templates/gator-starter",
|
|
220
|
+
"gator-command/templates/gator-starter",
|
|
221
|
+
"templates/gator-starter"):
|
|
222
|
+
tpl = root / tpl_rel
|
|
223
|
+
if tpl.is_dir():
|
|
224
|
+
return tpl, root
|
|
225
|
+
return None, None
|
|
226
|
+
|
|
227
|
+
gator_dir = Path(gator_dir)
|
|
228
|
+
|
|
229
|
+
# 2. product-source.json
|
|
230
|
+
ps = read_product_source(gator_dir)
|
|
231
|
+
if ps:
|
|
232
|
+
root = Path(ps.get("gator_root", ""))
|
|
233
|
+
tpl_rel = ps.get("template_dir", "")
|
|
234
|
+
if root.is_dir() and tpl_rel:
|
|
235
|
+
tpl = root / tpl_rel
|
|
236
|
+
if tpl.is_dir():
|
|
237
|
+
return tpl, root
|
|
238
|
+
|
|
239
|
+
# 3. Thin link fallback
|
|
240
|
+
cp = resolve_thin_link(gator_dir)
|
|
241
|
+
if cp:
|
|
242
|
+
for tpl_rel in ("gator-command/templates/gator-starter",
|
|
243
|
+
"gator-engine/templates/gator-starter",
|
|
244
|
+
"templates/gator-starter"):
|
|
245
|
+
tpl = Path(cp) / tpl_rel
|
|
246
|
+
if tpl.is_dir():
|
|
247
|
+
return tpl, Path(cp)
|
|
248
|
+
|
|
249
|
+
return None, None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_repo_topology(gator_dir):
|
|
253
|
+
"""Derive the governance topology of a repo.
|
|
254
|
+
|
|
255
|
+
Returns one of:
|
|
256
|
+
"policy-synced" — active thin link, repo receives org policy
|
|
257
|
+
"standalone" — no thin link, no policy artifacts, template updates only
|
|
258
|
+
"inconsistent" — partial artifacts remain, needs repair
|
|
259
|
+
"""
|
|
260
|
+
gator_dir = Path(gator_dir)
|
|
261
|
+
cp_file = gator_dir / "command-post.md"
|
|
262
|
+
gov_source = gator_dir / "governance-source.json"
|
|
263
|
+
policy_link = gator_dir / "policy-link.json"
|
|
264
|
+
policy_cache = gator_dir / "policy-cache"
|
|
265
|
+
|
|
266
|
+
has_thin_link = cp_file.exists()
|
|
267
|
+
has_policy_content = policy_link.exists() or (
|
|
268
|
+
policy_cache.is_dir() and any(policy_cache.iterdir())
|
|
269
|
+
)
|
|
270
|
+
has_gov_source = gov_source.exists()
|
|
271
|
+
|
|
272
|
+
# Thin link present and resolvable → policy-synced
|
|
273
|
+
if has_thin_link:
|
|
274
|
+
cp = resolve_thin_link(gator_dir)
|
|
275
|
+
if cp:
|
|
276
|
+
return "policy-synced"
|
|
277
|
+
# Thin link exists but points nowhere
|
|
278
|
+
return "inconsistent"
|
|
279
|
+
|
|
280
|
+
# No thin link — check for orphaned policy artifacts
|
|
281
|
+
if has_policy_content or has_gov_source:
|
|
282
|
+
return "inconsistent"
|
|
283
|
+
|
|
284
|
+
return "standalone"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def clear_policy_artifacts(gator_dir):
|
|
288
|
+
"""Remove all policy-source artifacts to make a repo standalone.
|
|
289
|
+
|
|
290
|
+
Clears: command-post.md, governance-source.json, policy-link.json, policy-cache/.
|
|
291
|
+
"""
|
|
292
|
+
import shutil
|
|
293
|
+
gator_dir = Path(gator_dir)
|
|
294
|
+
|
|
295
|
+
for name in ("command-post.md", "governance-source.json", "policy-link.json"):
|
|
296
|
+
f = gator_dir / name
|
|
297
|
+
if f.exists():
|
|
298
|
+
f.unlink()
|
|
299
|
+
|
|
300
|
+
cache = gator_dir / "policy-cache"
|
|
301
|
+
if cache.is_dir():
|
|
302
|
+
shutil.rmtree(cache)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# Registry
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def parse_registry(command_post):
|
|
310
|
+
"""Parse registry.md into a list of repo entries.
|
|
311
|
+
|
|
312
|
+
Each entry is a dict with: name, path, remote, registered, status.
|
|
313
|
+
"""
|
|
314
|
+
registry_file = command_post / "gator-command" / "registry.md"
|
|
315
|
+
if not registry_file.exists():
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
repos = []
|
|
319
|
+
text = registry_file.read_text(encoding="utf-8", errors="replace")
|
|
320
|
+
in_table = False
|
|
321
|
+
|
|
322
|
+
for line in text.splitlines():
|
|
323
|
+
stripped = line.strip()
|
|
324
|
+
if stripped.startswith("| Repo"):
|
|
325
|
+
in_table = True
|
|
326
|
+
continue
|
|
327
|
+
if stripped.startswith("|---"):
|
|
328
|
+
continue
|
|
329
|
+
if in_table and stripped.startswith("|"):
|
|
330
|
+
cells = [c.strip() for c in stripped.split("|")[1:-1]]
|
|
331
|
+
if len(cells) >= 5:
|
|
332
|
+
repos.append({
|
|
333
|
+
"name": cells[0],
|
|
334
|
+
"path": cells[1],
|
|
335
|
+
"remote": cells[2],
|
|
336
|
+
"registered": cells[3],
|
|
337
|
+
"status": cells[4],
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
return repos
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
# Git helpers
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
def git(*args, cwd=None):
|
|
348
|
+
"""Run a git command, return (stdout, success).
|
|
349
|
+
|
|
350
|
+
Returns a tuple so callers can distinguish 'git returned nothing'
|
|
351
|
+
from 'git failed'. Silent emptiness is the wrong failure mode for
|
|
352
|
+
a product whose claim is trustworthy governance telemetry.
|
|
353
|
+
"""
|
|
354
|
+
try:
|
|
355
|
+
result = subprocess.run(
|
|
356
|
+
["git"] + list(args),
|
|
357
|
+
capture_output=True, text=True, cwd=cwd, timeout=10,
|
|
358
|
+
)
|
|
359
|
+
return result.stdout.strip(), result.returncode == 0
|
|
360
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
361
|
+
return "", False
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
# Stdout setup
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
def ensure_utf8_stdout():
|
|
369
|
+
"""Ensure stdout uses UTF-8 encoding (needed on Windows)."""
|
|
370
|
+
if sys.stdout.encoding != "utf-8":
|
|
371
|
+
sys.stdout = io.TextIOWrapper(
|
|
372
|
+
sys.stdout.buffer, encoding="utf-8", errors="replace"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# ---------------------------------------------------------------------------
|
|
377
|
+
# Script import helper
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
def import_sibling(name):
|
|
381
|
+
"""Import a sibling script by filename (handles hyphens).
|
|
382
|
+
|
|
383
|
+
Example: import_sibling("gator-fleet-report") imports gator-fleet-report.py
|
|
384
|
+
from the same directory as the calling script.
|
|
385
|
+
|
|
386
|
+
Returns the module on success, None if the file doesn't exist.
|
|
387
|
+
Raises ImportError with diagnostic context if the file exists but
|
|
388
|
+
fails to load — callers should catch this if they want resilience.
|
|
389
|
+
"""
|
|
390
|
+
import importlib.util
|
|
391
|
+
|
|
392
|
+
scripts_dir = Path(__file__).resolve().parent
|
|
393
|
+
path = scripts_dir / f"{name}.py"
|
|
394
|
+
if not path.exists():
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
spec = importlib.util.spec_from_file_location(
|
|
398
|
+
name.replace("-", "_"), path
|
|
399
|
+
)
|
|
400
|
+
mod = importlib.util.module_from_spec(spec)
|
|
401
|
+
try:
|
|
402
|
+
spec.loader.exec_module(mod)
|
|
403
|
+
except Exception as e:
|
|
404
|
+
raise ImportError(
|
|
405
|
+
f"Failed to load {path.name}: {type(e).__name__}: {e}"
|
|
406
|
+
) from e
|
|
407
|
+
return mod
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# Charter surface resolution
|
|
412
|
+
# ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
def resolve_charter_surface(repo_root=None):
|
|
415
|
+
"""Return the authoritative charter surface for this repo.
|
|
416
|
+
|
|
417
|
+
Two modes:
|
|
418
|
+
- source-command-post: the gator-command development repo.
|
|
419
|
+
Charters at gator-command/charters/, cross-cutting is
|
|
420
|
+
scripts-cross-cutting.md.
|
|
421
|
+
- governed-repo: all other repos (fleet repos, deployed command posts).
|
|
422
|
+
Charters at .gator/charters/, cross-cutting is cross-cutting.md.
|
|
423
|
+
|
|
424
|
+
Returns dict with:
|
|
425
|
+
mode: "source-command-post" | "governed-repo"
|
|
426
|
+
charter_dir: Path to authoritative charter directory
|
|
427
|
+
cross_cutting: filename of the cross-cutting charter (or None)
|
|
428
|
+
index_file: Path to INDEX.md (or None if absent)
|
|
429
|
+
"""
|
|
430
|
+
if repo_root is None:
|
|
431
|
+
repo_root = Path.cwd()
|
|
432
|
+
else:
|
|
433
|
+
repo_root = Path(repo_root)
|
|
434
|
+
|
|
435
|
+
# Detect source-command-post mode: gator-command/charters/ exists AND
|
|
436
|
+
# scripts exist (at src/gator_command/scripts/ or legacy gator-command/scripts/).
|
|
437
|
+
cp_charters = repo_root / "gator-command" / "charters"
|
|
438
|
+
cp_scripts = repo_root / "src" / "gator_command" / "scripts"
|
|
439
|
+
if not cp_scripts.is_dir():
|
|
440
|
+
cp_scripts = repo_root / "gator-command" / "scripts"
|
|
441
|
+
if cp_charters.is_dir() and cp_scripts.is_dir():
|
|
442
|
+
cross_cutting = None
|
|
443
|
+
for f in cp_charters.iterdir():
|
|
444
|
+
if "cross-cutting" in f.name and f.suffix == ".md":
|
|
445
|
+
cross_cutting = f.name
|
|
446
|
+
break
|
|
447
|
+
index_file = cp_charters / "INDEX.md"
|
|
448
|
+
return {
|
|
449
|
+
"mode": "source-command-post",
|
|
450
|
+
"charter_dir": cp_charters,
|
|
451
|
+
"cross_cutting": cross_cutting,
|
|
452
|
+
"index_file": index_file if index_file.is_file() else None,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Default: governed-repo mode
|
|
456
|
+
charter_dir = repo_root / ".gator" / "charters"
|
|
457
|
+
cross_cutting = None
|
|
458
|
+
if charter_dir.is_dir():
|
|
459
|
+
for f in charter_dir.iterdir():
|
|
460
|
+
if "cross-cutting" in f.name and f.suffix == ".md":
|
|
461
|
+
cross_cutting = f.name
|
|
462
|
+
break
|
|
463
|
+
index_file = charter_dir / "INDEX.md"
|
|
464
|
+
return {
|
|
465
|
+
"mode": "governed-repo",
|
|
466
|
+
"charter_dir": charter_dir,
|
|
467
|
+
"cross_cutting": cross_cutting,
|
|
468
|
+
"index_file": index_file if index_file.is_file() else None,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ---------------------------------------------------------------------------
|
|
473
|
+
# Branding
|
|
474
|
+
# ---------------------------------------------------------------------------
|
|
475
|
+
|
|
476
|
+
# Template generation — single source of truth for Python scripts.
|
|
477
|
+
# gatorize.sh reads this value at runtime via grep.
|
|
478
|
+
# Gen 0: pre-gatorize.sh installs (no command-post.md, no .gator-version)
|
|
479
|
+
# Gen 1: first gatorize.sh installs (has command-post.md, no .gator-version)
|
|
480
|
+
# Gen 2: current (has .gator-version, overlay-not-replace upgrade logic)
|
|
481
|
+
CURRENT_GENERATION = 2
|
|
482
|
+
|
|
483
|
+
GATOR_MARK_LINES = [
|
|
484
|
+
r" .... . ..... ... .... ",
|
|
485
|
+
r" / ___| / \|_ _|/ _ \| _ \ .... ",
|
|
486
|
+
r"| | _ / _ \ | | | | | | |_) | ./( o )\_______",
|
|
487
|
+
r"| |_/ \/ /_\ \| | | |_| | _ < _/vVvVvVvV",
|
|
488
|
+
r" \____/m/ \m\_| \___/|m| \m\ \__.---------",
|
|
489
|
+
]
|