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,615 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator update — Refresh templates from the command post.
|
|
4
|
+
|
|
5
|
+
Reads the command-post thin link to find the gator-command repo, then
|
|
6
|
+
overlays template-derived files (constitution, procedures, scripts,
|
|
7
|
+
reference-notes, charter templates) from the gator-starter template
|
|
8
|
+
into the current repo's .gator/. User content (charters, threads,
|
|
9
|
+
artifacts, mission, roadmap, inbox, etc.) is never touched.
|
|
10
|
+
|
|
11
|
+
Overlay-not-replace: template files overwrite same-named files; files
|
|
12
|
+
that exist only in the target are left untouched.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python .gator/scripts/gator-update.py # interactive
|
|
16
|
+
python .gator/scripts/gator-update.py --dry-run # show what would change
|
|
17
|
+
python .gator/scripts/gator-update.py --json # output plan as JSON
|
|
18
|
+
|
|
19
|
+
@reads: .gator/command-post.md, gator-command/templates/gator-starter/
|
|
20
|
+
@writes: .gator/ template-derived files (overlay, never deletes)
|
|
21
|
+
@does-not-own: user content (charters, threads, artifacts, mission, roadmap, etc.)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import filecmp
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from gator_core import (
|
|
33
|
+
get_version, find_gator_root, normalize_path, resolve_thin_link,
|
|
34
|
+
ensure_utf8_stdout, git, import_sibling,
|
|
35
|
+
resolve_template_source, read_product_source, get_repo_topology,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
VERSION = get_version()
|
|
39
|
+
|
|
40
|
+
# Policy sync integration — graceful degradation if module unavailable
|
|
41
|
+
try:
|
|
42
|
+
_ps = import_sibling("gator-policy-status")
|
|
43
|
+
if _ps:
|
|
44
|
+
_sync_policy = _ps.sync_policy
|
|
45
|
+
_get_governance_source = _ps.get_governance_source
|
|
46
|
+
_init_governance_source = _ps.init_governance_source
|
|
47
|
+
_HAS_POLICY_STATUS = True
|
|
48
|
+
else:
|
|
49
|
+
_HAS_POLICY_STATUS = False
|
|
50
|
+
except Exception:
|
|
51
|
+
_HAS_POLICY_STATUS = False
|
|
52
|
+
|
|
53
|
+
# Files that are template-derived (safe to overwrite)
|
|
54
|
+
TEMPLATE_FILES = [
|
|
55
|
+
"constitution.md",
|
|
56
|
+
"gator-start-up.md",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# Dotfiles that need renaming from template (stored without dot to avoid git issues)
|
|
60
|
+
TEMPLATE_DOTFILES = {
|
|
61
|
+
"charterignore": ".charterignore",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Directories where templates overlay (add/update, never delete)
|
|
65
|
+
TEMPLATE_DIRS = {
|
|
66
|
+
"procedures": "procedures",
|
|
67
|
+
"reference-notes": "reference-notes",
|
|
68
|
+
"scripts": "scripts",
|
|
69
|
+
"sessions": "sessions",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Charter scaffolding files (not user charters)
|
|
73
|
+
CHARTER_TEMPLATE_FILES = ["README.md", "_template.md"]
|
|
74
|
+
|
|
75
|
+
# Blueprint scaffolding files (not user blueprints)
|
|
76
|
+
BLUEPRINT_TEMPLATE_FILES = ["README.md"]
|
|
77
|
+
|
|
78
|
+
# Slash commands
|
|
79
|
+
CLAUDE_COMMANDS_DIR = "commands"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# --- Path resolution ---
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def find_templates(command_post):
|
|
86
|
+
"""Find the gator-starter template directory in the command post.
|
|
87
|
+
|
|
88
|
+
Checks both gator-command/ (dev layout) and gator-engine/ (public
|
|
89
|
+
distribution layout).
|
|
90
|
+
"""
|
|
91
|
+
for parent_dir in ["gator-command", "gator-engine"]:
|
|
92
|
+
templates = command_post / parent_dir / "templates" / "gator-starter"
|
|
93
|
+
if templates.is_dir():
|
|
94
|
+
return templates
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --- Diff and overlay ---
|
|
99
|
+
|
|
100
|
+
def plan_file_update(src, dest):
|
|
101
|
+
"""Compare a template file to the installed version.
|
|
102
|
+
|
|
103
|
+
Returns: (action, src, dest) where action is 'add', 'update', 'unchanged', or 'skip'
|
|
104
|
+
"""
|
|
105
|
+
if not dest.exists():
|
|
106
|
+
return "add", src, dest
|
|
107
|
+
if filecmp.cmp(src, dest, shallow=False):
|
|
108
|
+
return "unchanged", src, dest
|
|
109
|
+
return "update", src, dest
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def plan_updates(templates_dir, gator_dir, repo_root):
|
|
113
|
+
"""Build the full update plan.
|
|
114
|
+
|
|
115
|
+
@reads: templates_dir, gator_dir
|
|
116
|
+
@does-not-own: executing the updates
|
|
117
|
+
"""
|
|
118
|
+
plan = []
|
|
119
|
+
|
|
120
|
+
# Top-level template files
|
|
121
|
+
for filename in TEMPLATE_FILES:
|
|
122
|
+
src = templates_dir / filename
|
|
123
|
+
dest = gator_dir / filename
|
|
124
|
+
if src.exists():
|
|
125
|
+
plan.append(plan_file_update(src, dest))
|
|
126
|
+
|
|
127
|
+
# Dotfiles (stored without dot in template, installed with dot)
|
|
128
|
+
for src_name, dest_name in TEMPLATE_DOTFILES.items():
|
|
129
|
+
src = templates_dir / src_name
|
|
130
|
+
dest = gator_dir / dest_name
|
|
131
|
+
if src.exists():
|
|
132
|
+
plan.append(plan_file_update(src, dest))
|
|
133
|
+
|
|
134
|
+
# Charter scaffolding
|
|
135
|
+
for filename in CHARTER_TEMPLATE_FILES:
|
|
136
|
+
src = templates_dir / "charters" / filename
|
|
137
|
+
dest = gator_dir / "charters" / filename
|
|
138
|
+
if src.exists():
|
|
139
|
+
plan.append(plan_file_update(src, dest))
|
|
140
|
+
|
|
141
|
+
# Blueprint scaffolding
|
|
142
|
+
for filename in BLUEPRINT_TEMPLATE_FILES:
|
|
143
|
+
src = templates_dir / "blueprints" / filename
|
|
144
|
+
dest = gator_dir / "blueprints" / filename
|
|
145
|
+
if src.exists():
|
|
146
|
+
plan.append(plan_file_update(src, dest))
|
|
147
|
+
|
|
148
|
+
# Template directories (overlay)
|
|
149
|
+
# Note: no mkdir here — plan_updates is read-only. Directories are
|
|
150
|
+
# created by execute_updates() when files are actually written.
|
|
151
|
+
for src_subdir, dest_subdir in TEMPLATE_DIRS.items():
|
|
152
|
+
src_dir = templates_dir / src_subdir
|
|
153
|
+
dest_dir = gator_dir / dest_subdir
|
|
154
|
+
if not src_dir.is_dir():
|
|
155
|
+
continue
|
|
156
|
+
for src_file in sorted(src_dir.iterdir()):
|
|
157
|
+
if src_file.is_file():
|
|
158
|
+
dest_file = dest_dir / src_file.name
|
|
159
|
+
plan.append(plan_file_update(src_file, dest_file))
|
|
160
|
+
|
|
161
|
+
# Nested template subdirectories (e.g., scripts/hooks/)
|
|
162
|
+
for src_subdir, dest_subdir in TEMPLATE_DIRS.items():
|
|
163
|
+
src_dir = templates_dir / src_subdir
|
|
164
|
+
if not src_dir.is_dir():
|
|
165
|
+
continue
|
|
166
|
+
for sub in sorted(src_dir.iterdir()):
|
|
167
|
+
if sub.is_dir():
|
|
168
|
+
dest_sub = gator_dir / dest_subdir / sub.name
|
|
169
|
+
for src_file in sorted(sub.iterdir()):
|
|
170
|
+
if src_file.is_file():
|
|
171
|
+
dest_file = dest_sub / src_file.name
|
|
172
|
+
plan.append(plan_file_update(src_file, dest_file))
|
|
173
|
+
|
|
174
|
+
# Claude Code slash commands
|
|
175
|
+
src_commands = templates_dir / CLAUDE_COMMANDS_DIR
|
|
176
|
+
if src_commands.is_dir():
|
|
177
|
+
dest_commands = repo_root / ".claude" / "commands"
|
|
178
|
+
for src_file in sorted(src_commands.iterdir()):
|
|
179
|
+
if src_file.is_file():
|
|
180
|
+
dest_file = dest_commands / src_file.name
|
|
181
|
+
plan.append(plan_file_update(src_file, dest_file))
|
|
182
|
+
|
|
183
|
+
return plan
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_command_post_policy_date(command_post):
|
|
187
|
+
"""Return the policy date from the command post's org-policy.md git history.
|
|
188
|
+
|
|
189
|
+
Uses the same logic as gator-drift.py: last commit date of org-policy.md,
|
|
190
|
+
falling back to None if git fails.
|
|
191
|
+
"""
|
|
192
|
+
policy_date, ok = git(
|
|
193
|
+
"log", "-1", "--format=%ci", "--", "gator-command/org-policy.md",
|
|
194
|
+
cwd=command_post,
|
|
195
|
+
)
|
|
196
|
+
if ok and policy_date:
|
|
197
|
+
return policy_date[:10] # YYYY-MM-DD
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def bump_policy_version(gator_dir, command_post):
|
|
202
|
+
"""Update the version: field in .gator/command-post.md to match the
|
|
203
|
+
command post's current org-policy.md date.
|
|
204
|
+
|
|
205
|
+
Returns (new_version, changed) where changed is True if the file was
|
|
206
|
+
actually rewritten, False if it was already current or could not be read.
|
|
207
|
+
|
|
208
|
+
@reads: command post git history (org-policy.md last commit date)
|
|
209
|
+
@writes: .gator/command-post.md (version: line only)
|
|
210
|
+
"""
|
|
211
|
+
policy_date = get_command_post_policy_date(command_post)
|
|
212
|
+
if not policy_date:
|
|
213
|
+
return None, False
|
|
214
|
+
|
|
215
|
+
cp_file = gator_dir / "command-post.md"
|
|
216
|
+
if not cp_file.exists():
|
|
217
|
+
return policy_date, False
|
|
218
|
+
|
|
219
|
+
text = cp_file.read_text(encoding="utf-8", errors="replace")
|
|
220
|
+
lines = text.splitlines(keepends=True)
|
|
221
|
+
new_lines = []
|
|
222
|
+
changed = False
|
|
223
|
+
for line in lines:
|
|
224
|
+
if line.startswith("version:"):
|
|
225
|
+
current = line.split(":", 1)[1].strip().rstrip("\n")
|
|
226
|
+
if current != policy_date:
|
|
227
|
+
new_lines.append(f"version: {policy_date}\n")
|
|
228
|
+
changed = True
|
|
229
|
+
else:
|
|
230
|
+
new_lines.append(line)
|
|
231
|
+
else:
|
|
232
|
+
new_lines.append(line)
|
|
233
|
+
|
|
234
|
+
if changed:
|
|
235
|
+
cp_file.write_text("".join(new_lines), encoding="utf-8")
|
|
236
|
+
|
|
237
|
+
return policy_date, changed
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def execute_updates(plan):
|
|
241
|
+
"""Execute the update plan — copy files that need adding or updating.
|
|
242
|
+
|
|
243
|
+
@writes: .gator/ template-derived files
|
|
244
|
+
@does-not-own: user content
|
|
245
|
+
"""
|
|
246
|
+
added = 0
|
|
247
|
+
updated = 0
|
|
248
|
+
unchanged = 0
|
|
249
|
+
|
|
250
|
+
for action, src, dest in plan:
|
|
251
|
+
if action == "add":
|
|
252
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
253
|
+
shutil.copy2(src, dest)
|
|
254
|
+
added += 1
|
|
255
|
+
elif action == "update":
|
|
256
|
+
shutil.copy2(src, dest)
|
|
257
|
+
updated += 1
|
|
258
|
+
else:
|
|
259
|
+
unchanged += 1
|
|
260
|
+
|
|
261
|
+
return added, updated, unchanged
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def plan_hook_updates(gator_dir, repo_root):
|
|
265
|
+
"""Plan git hook installations. Returns list of (hook_name, action).
|
|
266
|
+
|
|
267
|
+
Compares the generated cross-platform Python wrappers to the current
|
|
268
|
+
.git/hooks/ contents. This matches what install_git_hooks() actually
|
|
269
|
+
writes, so the plan is accurate on both Unix and Windows.
|
|
270
|
+
"""
|
|
271
|
+
git_hooks = repo_root / ".git" / "hooks"
|
|
272
|
+
gator_script = gator_dir / "scripts" / "gator-pre-commit.py"
|
|
273
|
+
|
|
274
|
+
if not git_hooks.parent.is_dir() or not gator_script.exists():
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
expected_hooks = build_git_hook_wrappers()
|
|
278
|
+
results = []
|
|
279
|
+
for hook_name, expected_content in sorted(expected_hooks.items()):
|
|
280
|
+
dest = git_hooks / hook_name
|
|
281
|
+
if not dest.exists():
|
|
282
|
+
results.append((hook_name, "add"))
|
|
283
|
+
continue
|
|
284
|
+
actual = dest.read_text(encoding="utf-8", errors="replace")
|
|
285
|
+
if actual != expected_content:
|
|
286
|
+
results.append((hook_name, "update"))
|
|
287
|
+
else:
|
|
288
|
+
results.append((hook_name, "unchanged"))
|
|
289
|
+
|
|
290
|
+
return results
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _hook_shebang():
|
|
294
|
+
"""Return the platform-correct shebang for hook wrappers.
|
|
295
|
+
|
|
296
|
+
Windows: `python` — the standard executable name; `python3` often absent.
|
|
297
|
+
Unix: `python3` — the guaranteed Python 3 command; `python` may be absent
|
|
298
|
+
or point to Python 2 on older systems.
|
|
299
|
+
"""
|
|
300
|
+
return "#!/usr/bin/env python" if os.name == "nt" else "#!/usr/bin/env python3"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def build_git_hook_wrappers():
|
|
304
|
+
"""Return the exact hook wrapper contents gator installs.
|
|
305
|
+
|
|
306
|
+
Shebang uses env dispatch (platform-selected) so hook launch works even
|
|
307
|
+
when sys.executable contains spaces (e.g. C:/Program Files/Python313/).
|
|
308
|
+
The subprocess.call inside uses the exact interpreter path — spaces are
|
|
309
|
+
safe there because it's a Python string argument, not a shebang.
|
|
310
|
+
"""
|
|
311
|
+
python_path = sys.executable.replace("\\", "/")
|
|
312
|
+
gator_script = ".gator/scripts/gator-pre-commit.py"
|
|
313
|
+
shebang = _hook_shebang()
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
"pre-commit": (
|
|
317
|
+
f'{shebang}\n'
|
|
318
|
+
'import subprocess, sys\n'
|
|
319
|
+
f'sys.exit(subprocess.call([r"{python_path}", "{gator_script}", "--phase", "validate"]))\n'
|
|
320
|
+
),
|
|
321
|
+
"commit-msg": (
|
|
322
|
+
f'{shebang}\n'
|
|
323
|
+
'import subprocess, sys\n'
|
|
324
|
+
f'sys.exit(subprocess.call([r"{python_path}", "{gator_script}", "--phase", "trailers", sys.argv[1]]))\n'
|
|
325
|
+
),
|
|
326
|
+
"post-commit": (
|
|
327
|
+
f'{shebang}\n'
|
|
328
|
+
'import subprocess, sys\n'
|
|
329
|
+
f'sys.exit(subprocess.call([r"{python_path}", "{gator_script}", "--phase", "cleanup"]))\n'
|
|
330
|
+
),
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def install_git_hooks(gator_dir, repo_root):
|
|
335
|
+
"""Install/refresh git hooks as cross-platform Python wrappers.
|
|
336
|
+
|
|
337
|
+
Writes Python-shebang hooks that call gator-pre-commit.py directly.
|
|
338
|
+
Works on Windows CMD/PowerShell (no bash dependency) and Unix.
|
|
339
|
+
|
|
340
|
+
@writes: .git/hooks/pre-commit, .git/hooks/commit-msg, .git/hooks/post-commit
|
|
341
|
+
"""
|
|
342
|
+
git_hooks = repo_root / ".git" / "hooks"
|
|
343
|
+
if not git_hooks.parent.is_dir():
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
git_hooks.mkdir(parents=True, exist_ok=True)
|
|
347
|
+
installed = 0
|
|
348
|
+
|
|
349
|
+
for name, content in build_git_hook_wrappers().items():
|
|
350
|
+
dest = git_hooks / name
|
|
351
|
+
dest.write_text(content, encoding="utf-8")
|
|
352
|
+
try:
|
|
353
|
+
import stat
|
|
354
|
+
dest.chmod(dest.stat().st_mode | stat.S_IEXEC)
|
|
355
|
+
except (OSError, AttributeError):
|
|
356
|
+
pass
|
|
357
|
+
installed += 1
|
|
358
|
+
|
|
359
|
+
return installed
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# --- Output ---
|
|
363
|
+
|
|
364
|
+
def print_plan(plan, dry_run=False, hooks=None):
|
|
365
|
+
"""Print the update plan."""
|
|
366
|
+
adds = [(a, s, d) for a, s, d in plan if a == "add"]
|
|
367
|
+
updates = [(a, s, d) for a, s, d in plan if a == "update"]
|
|
368
|
+
unchanged = [(a, s, d) for a, s, d in plan if a == "unchanged"]
|
|
369
|
+
|
|
370
|
+
hook_adds = [h for h in (hooks or []) if h[1] == "add"]
|
|
371
|
+
hook_updates = [h for h in (hooks or []) if h[1] == "update"]
|
|
372
|
+
|
|
373
|
+
mode = " (dry run)" if dry_run else ""
|
|
374
|
+
print()
|
|
375
|
+
print(f" gator update{mode}")
|
|
376
|
+
print()
|
|
377
|
+
|
|
378
|
+
if adds:
|
|
379
|
+
print(f" New files ({len(adds)}):")
|
|
380
|
+
for _, _, dest in adds:
|
|
381
|
+
print(f" + {dest.name}")
|
|
382
|
+
print()
|
|
383
|
+
|
|
384
|
+
if updates:
|
|
385
|
+
print(f" Updated files ({len(updates)}):")
|
|
386
|
+
for _, _, dest in updates:
|
|
387
|
+
print(f" ~ {dest.name}")
|
|
388
|
+
print()
|
|
389
|
+
|
|
390
|
+
if hook_adds or hook_updates:
|
|
391
|
+
print(f" Git hooks ({len(hook_adds)} new, {len(hook_updates)} updated):")
|
|
392
|
+
for name, action in (hooks or []):
|
|
393
|
+
if action == "add":
|
|
394
|
+
print(f" + .git/hooks/{name}")
|
|
395
|
+
elif action == "update":
|
|
396
|
+
print(f" ~ .git/hooks/{name}")
|
|
397
|
+
print()
|
|
398
|
+
|
|
399
|
+
if not adds and not updates and not hook_adds and not hook_updates:
|
|
400
|
+
print(f" Everything is current. No updates needed.")
|
|
401
|
+
print()
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
print(f" Unchanged: {len(unchanged)} files")
|
|
405
|
+
print(f" Total: {len(adds)} new, {len(updates)} updated, {len(unchanged)} unchanged")
|
|
406
|
+
print()
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def print_result(added, updated, unchanged):
|
|
410
|
+
"""Print the result after executing updates."""
|
|
411
|
+
print(f" Done: {added} added, {updated} updated, {unchanged} unchanged")
|
|
412
|
+
print()
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def print_json_plan(plan, command_post, templates_dir, hooks=None):
|
|
416
|
+
"""Output the plan as JSON."""
|
|
417
|
+
items = []
|
|
418
|
+
for action, src, dest in plan:
|
|
419
|
+
items.append({
|
|
420
|
+
"action": action,
|
|
421
|
+
"source": str(src),
|
|
422
|
+
"destination": str(dest),
|
|
423
|
+
"filename": dest.name,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
hook_items = []
|
|
427
|
+
for name, action in (hooks or []):
|
|
428
|
+
hook_items.append({
|
|
429
|
+
"hook": name,
|
|
430
|
+
"action": action,
|
|
431
|
+
"destination": f".git/hooks/{name}",
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
output = {
|
|
435
|
+
"version": VERSION,
|
|
436
|
+
"command_post": str(command_post),
|
|
437
|
+
"templates": str(templates_dir),
|
|
438
|
+
"plan": items,
|
|
439
|
+
"hooks": hook_items,
|
|
440
|
+
"summary": {
|
|
441
|
+
"add": sum(1 for i in items if i["action"] == "add"),
|
|
442
|
+
"update": sum(1 for i in items if i["action"] == "update"),
|
|
443
|
+
"unchanged": sum(1 for i in items if i["action"] == "unchanged"),
|
|
444
|
+
"hooks_add": sum(1 for h in hook_items if h["action"] == "add"),
|
|
445
|
+
"hooks_update": sum(1 for h in hook_items if h["action"] == "update"),
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
print(json.dumps(output, indent=2))
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# --- Entry point ---
|
|
452
|
+
|
|
453
|
+
def main():
|
|
454
|
+
ensure_utf8_stdout()
|
|
455
|
+
|
|
456
|
+
parser = argparse.ArgumentParser(
|
|
457
|
+
description="Gator update — refresh templates and optionally sync org policy."
|
|
458
|
+
)
|
|
459
|
+
parser.add_argument(
|
|
460
|
+
"--dry-run", "-n",
|
|
461
|
+
action="store_true",
|
|
462
|
+
help="Show what would be updated without changing anything"
|
|
463
|
+
)
|
|
464
|
+
parser.add_argument(
|
|
465
|
+
"--json", "-j",
|
|
466
|
+
action="store_true",
|
|
467
|
+
help="Output update plan as JSON"
|
|
468
|
+
)
|
|
469
|
+
parser.add_argument(
|
|
470
|
+
"--path", "-p",
|
|
471
|
+
help="Path to search for .gator/ (default: current directory)"
|
|
472
|
+
)
|
|
473
|
+
parser.add_argument(
|
|
474
|
+
"--source", "-s",
|
|
475
|
+
help="Path to the Gator clone (rebinds product-source.json)"
|
|
476
|
+
)
|
|
477
|
+
parser.add_argument(
|
|
478
|
+
"--no-policy",
|
|
479
|
+
action="store_true",
|
|
480
|
+
help="Skip org-policy sync (channel 1 template updates only)"
|
|
481
|
+
)
|
|
482
|
+
args = parser.parse_args()
|
|
483
|
+
|
|
484
|
+
# Find the repo
|
|
485
|
+
repo_root = find_gator_root(args.path)
|
|
486
|
+
if not repo_root:
|
|
487
|
+
print(" Error: no .gator/ found. Run from a gatorized repo.", file=sys.stderr)
|
|
488
|
+
sys.exit(1)
|
|
489
|
+
|
|
490
|
+
gator_dir = repo_root / ".gator"
|
|
491
|
+
|
|
492
|
+
# Channel 1: resolve template source (product-source.json, --source, or thin link fallback)
|
|
493
|
+
templates_dir, gator_root = resolve_template_source(gator_dir, args.source)
|
|
494
|
+
if not templates_dir:
|
|
495
|
+
ps = read_product_source(gator_dir)
|
|
496
|
+
stale_path = ps.get("gator_root", "?") if ps else "not configured"
|
|
497
|
+
print(f" Error: product template source not found.", file=sys.stderr)
|
|
498
|
+
print(f" Last known source: {stale_path}", file=sys.stderr)
|
|
499
|
+
print(f" Run: gator update --source /path/to/gator to rebind", file=sys.stderr)
|
|
500
|
+
sys.exit(1)
|
|
501
|
+
|
|
502
|
+
# Update product-source.json if --source was given (rebind)
|
|
503
|
+
if args.source:
|
|
504
|
+
import json as _json
|
|
505
|
+
ps_file = gator_dir / "product-source.json"
|
|
506
|
+
tpl_rel = str(templates_dir.relative_to(gator_root))
|
|
507
|
+
ps_data = {
|
|
508
|
+
"gator_root": str(gator_root),
|
|
509
|
+
"template_dir": tpl_rel,
|
|
510
|
+
"installed": "",
|
|
511
|
+
"updated": "",
|
|
512
|
+
}
|
|
513
|
+
# Preserve installed date if existing
|
|
514
|
+
old_ps = read_product_source(gator_dir)
|
|
515
|
+
if old_ps:
|
|
516
|
+
ps_data["installed"] = old_ps.get("installed", "")
|
|
517
|
+
from datetime import date
|
|
518
|
+
ps_data["updated"] = str(date.today())
|
|
519
|
+
if not ps_data["installed"]:
|
|
520
|
+
ps_data["installed"] = ps_data["updated"]
|
|
521
|
+
ps_file.write_text(_json.dumps(ps_data, indent=2) + "\n", encoding="utf-8")
|
|
522
|
+
print(f" Product source rebound to: {gator_root}")
|
|
523
|
+
|
|
524
|
+
# Channel 2: resolve command post for policy sync (only if policy-synced)
|
|
525
|
+
command_post = resolve_thin_link(gator_dir)
|
|
526
|
+
topology = get_repo_topology(gator_dir)
|
|
527
|
+
|
|
528
|
+
# Build plan
|
|
529
|
+
plan = plan_updates(templates_dir, gator_dir, repo_root)
|
|
530
|
+
|
|
531
|
+
# Plan git hook installation
|
|
532
|
+
hooks_to_install = plan_hook_updates(gator_dir, repo_root)
|
|
533
|
+
|
|
534
|
+
# JSON mode
|
|
535
|
+
if args.json:
|
|
536
|
+
print_json_plan(plan, command_post, templates_dir, hooks_to_install)
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
# Dry run
|
|
540
|
+
if args.dry_run:
|
|
541
|
+
print_plan(plan, dry_run=True, hooks=hooks_to_install)
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
# Execute
|
|
545
|
+
print_plan(plan, hooks=hooks_to_install)
|
|
546
|
+
added, updated, unchanged = execute_updates(plan)
|
|
547
|
+
|
|
548
|
+
# Install/refresh git hooks
|
|
549
|
+
hooks_installed = install_git_hooks(gator_dir, repo_root)
|
|
550
|
+
if hooks_installed > 0:
|
|
551
|
+
print(f" Git hooks: {hooks_installed} installed/refreshed")
|
|
552
|
+
|
|
553
|
+
# Channel 2: policy sync (only for policy-synced repos, unless --no-policy)
|
|
554
|
+
if args.no_policy:
|
|
555
|
+
pass # Skip channel 2 entirely
|
|
556
|
+
elif topology == "policy-synced" and command_post:
|
|
557
|
+
# Bump policy version in command-post.md
|
|
558
|
+
policy_date, bumped = bump_policy_version(gator_dir, command_post)
|
|
559
|
+
if bumped:
|
|
560
|
+
print(f" Policy version: bumped to {policy_date}")
|
|
561
|
+
|
|
562
|
+
# Sync policy cache from source
|
|
563
|
+
if _HAS_POLICY_STATUS:
|
|
564
|
+
try:
|
|
565
|
+
source, is_derived = _get_governance_source(gator_dir)
|
|
566
|
+
if not source:
|
|
567
|
+
pass
|
|
568
|
+
else:
|
|
569
|
+
if is_derived:
|
|
570
|
+
init_result = _init_governance_source(gator_dir, source)
|
|
571
|
+
if init_result.get("status") == "ok":
|
|
572
|
+
print(f" Policy source: created governance-source.json")
|
|
573
|
+
source, _ = _get_governance_source(gator_dir)
|
|
574
|
+
|
|
575
|
+
result = _sync_policy(gator_dir, source, repo_root)
|
|
576
|
+
if result.get("status") == "ok":
|
|
577
|
+
print(f" Policy cache: synced ({result.get('source_type', '?')}, {result['cached_at'][:10]})")
|
|
578
|
+
elif result.get("error"):
|
|
579
|
+
print(f" Policy cache: {result['error']}")
|
|
580
|
+
except Exception as e:
|
|
581
|
+
print(f" Policy cache: skipped ({e})")
|
|
582
|
+
elif topology == "standalone":
|
|
583
|
+
print(f" Policy sync: skipped (standalone mode)")
|
|
584
|
+
elif topology == "inconsistent":
|
|
585
|
+
print(f" Policy sync: skipped (inconsistent topology — repair with dashboard or re-gatorize)")
|
|
586
|
+
|
|
587
|
+
# Update product-source.json timestamp
|
|
588
|
+
ps = read_product_source(gator_dir)
|
|
589
|
+
if ps:
|
|
590
|
+
import json as _json
|
|
591
|
+
from datetime import date
|
|
592
|
+
ps["updated"] = str(date.today())
|
|
593
|
+
ps_file = gator_dir / "product-source.json"
|
|
594
|
+
ps_file.write_text(_json.dumps(ps, indent=2) + "\n", encoding="utf-8")
|
|
595
|
+
|
|
596
|
+
# Stamp .gator-version updated timestamp
|
|
597
|
+
if added > 0 or updated > 0:
|
|
598
|
+
version_file = gator_dir / ".gator-version"
|
|
599
|
+
if version_file.exists():
|
|
600
|
+
from datetime import datetime
|
|
601
|
+
now = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
|
602
|
+
lines = version_file.read_text(encoding="utf-8").splitlines()
|
|
603
|
+
new_lines = []
|
|
604
|
+
for line in lines:
|
|
605
|
+
if line.startswith("updated:"):
|
|
606
|
+
new_lines.append(f"updated: {now}")
|
|
607
|
+
else:
|
|
608
|
+
new_lines.append(line)
|
|
609
|
+
version_file.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
|
|
610
|
+
|
|
611
|
+
print_result(added, updated, unchanged)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
if __name__ == "__main__":
|
|
615
|
+
main()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator-version.py — Git-native version resolution for Gator Command.
|
|
4
|
+
|
|
5
|
+
The version IS the git state. Tagged releases (v0.1.0, v1.0.0) are the
|
|
6
|
+
anchors. Between tags, git describe gives the full picture:
|
|
7
|
+
v0.1.0-14-g8ea4749 = 14 commits after v0.1.0, at commit 8ea4749.
|
|
8
|
+
|
|
9
|
+
Usage as a module:
|
|
10
|
+
from gator_core import get_version
|
|
11
|
+
version = get_version() # "v0.1.0-14-g8ea4749" or "v0.1.0" or "dev"
|
|
12
|
+
|
|
13
|
+
Usage as a script:
|
|
14
|
+
python gator-command/scripts/gator-version.py
|
|
15
|
+
python gator-command/scripts/gator-version.py --short
|
|
16
|
+
|
|
17
|
+
No hardcoded version strings. The repo is the source of truth.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from gator_core import get_version, get_version_short
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
parser = argparse.ArgumentParser(description="Gator version from git tags")
|
|
28
|
+
parser.add_argument("--short", action="store_true", help="Tag only, no commit count")
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
if args.short:
|
|
32
|
+
print(get_version_short())
|
|
33
|
+
else:
|
|
34
|
+
print(get_version())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|