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,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()