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,434 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gator init — Branded boot sequence for Gator-governed repos.
4
+
5
+ Detects .gator/ in the current directory (or a specified path), reads
6
+ the knowledge layer, and prints a formatted status display. Designed
7
+ to run at session open in any AI CLI (Claude Code, Codex, Gemini).
8
+
9
+ Usage:
10
+ python .gator/scripts/gator-init.py
11
+ python .gator/scripts/gator-init.py --path /some/repo
12
+ python .gator/scripts/gator-init.py --json
13
+
14
+ @reads: .gator/ directory structure, constitution.md, charters/, threads/, command-post.md
15
+ @writes: .git/hooks/ (self-heal only when hooks are missing or stale)
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+
24
+ from gator_core import (
25
+ get_version, find_gator_root, ensure_utf8_stdout, GATOR_MARK_LINES,
26
+ import_sibling,
27
+ )
28
+
29
+ VERSION = get_version()
30
+
31
+ TAGLINE = "oriented. the terrain is mapped."
32
+
33
+
34
+ # --- Detection and counting ---
35
+
36
+ def count_constitution_rules(gator_dir):
37
+ """Count enforceable rules in the constitution.
38
+
39
+ Counts numbered list items, table rows with content, and bold-prefixed
40
+ rules. This is a heuristic — not a parser.
41
+
42
+ @reads: .gator/constitution.md
43
+ """
44
+ constitution = gator_dir / "constitution.md"
45
+ if not constitution.exists():
46
+ return 0
47
+
48
+ text = constitution.read_text(encoding="utf-8", errors="replace")
49
+ count = 0
50
+
51
+ for line in text.splitlines():
52
+ stripped = line.strip()
53
+ # Numbered steps (1. **Before changing code**: ...)
54
+ if re.match(r'^\d+\.?\s+\*\*', stripped):
55
+ count += 1
56
+ # Bold-prefixed rules (- **BRANCHING**: ...)
57
+ elif re.match(r'^-\s+\*\*[A-Z]', stripped):
58
+ count += 1
59
+ # Standalone bold imperatives (**The agent always asks before committing**)
60
+ elif re.match(r'^\*\*The (agent|Architect|PI|enforcer)', stripped):
61
+ count += 1
62
+
63
+ return count
64
+
65
+
66
+ def count_charters(gator_dir):
67
+ """Count charter files and estimate mapping coverage.
68
+
69
+ @reads: .gator/charters/
70
+ """
71
+ charters_dir = gator_dir / "charters"
72
+ if not charters_dir.is_dir():
73
+ return 0, 0, 0.0
74
+
75
+ skip = {"_template.md", "README.md", "INDEX.md"}
76
+ charter_files = [
77
+ f for f in charters_dir.iterdir()
78
+ if f.suffix == ".md" and f.name not in skip
79
+ ]
80
+
81
+ # Count function entries across all charters (lines starting with ### )
82
+ function_count = 0
83
+ for cf in charter_files:
84
+ text = cf.read_text(encoding="utf-8", errors="replace")
85
+ for line in text.splitlines():
86
+ if line.strip().startswith("### ") and "(" in line:
87
+ function_count += 1
88
+
89
+ # Estimate total functions in the repo (count def/func/function declarations)
90
+ repo_root = gator_dir.parent
91
+ total_functions = 0
92
+ for ext in ("*.py", "*.gd", "*.js", "*.ts", "*.go", "*.rs", "*.java"):
93
+ for code_file in repo_root.rglob(ext):
94
+ # Skip .gator/ itself
95
+ if ".gator" in str(code_file):
96
+ continue
97
+ try:
98
+ text = code_file.read_text(encoding="utf-8", errors="replace")
99
+ for line in text.splitlines():
100
+ stripped = line.strip()
101
+ if (stripped.startswith("def ") or
102
+ stripped.startswith("func ") or
103
+ stripped.startswith("function ") or
104
+ re.match(r'^(pub\s+)?(fn|func)\s+', stripped) or
105
+ re.match(r'^(export\s+)?(async\s+)?function\s+', stripped)):
106
+ total_functions += 1
107
+ except (OSError, UnicodeDecodeError):
108
+ continue
109
+
110
+ coverage = (function_count / total_functions * 100) if total_functions > 0 else 0.0
111
+
112
+ return len(charter_files), function_count, coverage
113
+
114
+
115
+ def count_working_set(gator_dir):
116
+ """Count threads and their total line count.
117
+
118
+ @reads: .gator/threads/, .gator/active-threads/ (if present)
119
+ """
120
+ total_threads = 0
121
+ total_lines = 0
122
+
123
+ for subdir_name in ("active-threads", "threads"):
124
+ subdir = gator_dir / subdir_name
125
+ if not subdir.is_dir():
126
+ continue
127
+ for f in subdir.iterdir():
128
+ if f.suffix == ".md" and f.name != ".gitkeep":
129
+ total_threads += 1
130
+ try:
131
+ total_lines += len(f.read_text(encoding="utf-8", errors="replace").splitlines())
132
+ except (OSError, UnicodeDecodeError):
133
+ pass
134
+
135
+ return total_threads, total_lines
136
+
137
+
138
+ def detect_enforcer(gator_dir):
139
+ """Detect enforcer configuration.
140
+
141
+ @reads: .gator/scripts/enforcer-prompt.md, .gator/scripts/enforcer-review.py
142
+ """
143
+ has_prompt = (gator_dir / "scripts" / "enforcer-prompt.md").exists()
144
+ has_script = (gator_dir / "scripts" / "enforcer-review.py").exists()
145
+
146
+ if has_prompt and has_script:
147
+ return "ready"
148
+ elif has_prompt or has_script:
149
+ return "partial"
150
+ else:
151
+ return "not configured"
152
+
153
+
154
+ def count_field_guides(gator_dir):
155
+ """Count field guide languages from pattern files.
156
+
157
+ Field guides use two files per language: {lang}-patterns.md and
158
+ {lang}-tutorial.md. Count languages from pattern files.
159
+
160
+ @reads: .gator/field-guides/
161
+ """
162
+ guides_dir = gator_dir / "field-guides"
163
+ if not guides_dir.is_dir():
164
+ return 0, []
165
+
166
+ guides = [
167
+ f.name.removesuffix("-patterns.md")
168
+ for f in guides_dir.iterdir()
169
+ if f.name.endswith("-patterns.md")
170
+ ]
171
+ return len(guides), sorted(guides)
172
+
173
+
174
+ def detect_command_post(gator_dir):
175
+ """Check for command-post thin link.
176
+
177
+ @reads: .gator/command-post.md
178
+ """
179
+ cp_file = gator_dir / "command-post.md"
180
+ if not cp_file.exists():
181
+ return None
182
+
183
+ text = cp_file.read_text(encoding="utf-8", errors="replace")
184
+ for line in text.splitlines():
185
+ if line.startswith("command-post:"):
186
+ return line.split(":", 1)[1].strip()
187
+ return "linked"
188
+
189
+
190
+ def read_version(gator_dir):
191
+ """Read generation from .gator-version.
192
+
193
+ @reads: .gator/.gator-version
194
+ """
195
+ version_file = gator_dir / ".gator-version"
196
+ if not version_file.exists():
197
+ return None
198
+
199
+ text = version_file.read_text(encoding="utf-8", errors="replace")
200
+ info = {}
201
+ for line in text.splitlines():
202
+ if ":" in line:
203
+ key, val = line.split(":", 1)
204
+ info[key.strip()] = val.strip()
205
+ return info
206
+
207
+
208
+ def ensure_git_hooks(repo_root, gator_dir):
209
+ """Self-heal git hooks at session start.
210
+
211
+ Uses gator-update's hook planning and installation logic so there is
212
+ exactly one definition of what "correctly installed hooks" means.
213
+ """
214
+ try:
215
+ gator_update = import_sibling("gator-update")
216
+ except ImportError as e:
217
+ return {
218
+ "status": "error",
219
+ "detail": f"hook repair unavailable: {e}",
220
+ "adds": 0,
221
+ "updates": 0,
222
+ }
223
+ if gator_update is None:
224
+ return {
225
+ "status": "error",
226
+ "detail": "gator-update.py not found",
227
+ "adds": 0,
228
+ "updates": 0,
229
+ }
230
+
231
+ if not (repo_root / ".git").exists():
232
+ return {
233
+ "status": "unavailable",
234
+ "detail": "no .git directory",
235
+ "adds": 0,
236
+ "updates": 0,
237
+ }
238
+
239
+ # Check that the hook target script exists — without it, hooks can't work
240
+ # even if they're installed, and plan_hook_updates returns [] (masking the problem).
241
+ gator_script = gator_dir / "scripts" / "gator-pre-commit.py"
242
+ if not gator_script.exists():
243
+ return {
244
+ "status": "degraded",
245
+ "detail": "gator-pre-commit.py missing",
246
+ "adds": 0,
247
+ "updates": 0,
248
+ }
249
+
250
+ plan = gator_update.plan_hook_updates(gator_dir, repo_root)
251
+ adds = sum(1 for _, action in plan if action == "add")
252
+ updates = sum(1 for _, action in plan if action == "update")
253
+
254
+ if adds or updates:
255
+ installed = gator_update.install_git_hooks(gator_dir, repo_root)
256
+ if updates:
257
+ detail = f"refreshed ({installed} hooks)"
258
+ status = "refreshed"
259
+ else:
260
+ detail = f"installed ({installed} hooks)"
261
+ status = "installed"
262
+ else:
263
+ detail = "ok"
264
+ status = "ok"
265
+
266
+ return {
267
+ "status": status,
268
+ "detail": detail,
269
+ "adds": adds,
270
+ "updates": updates,
271
+ }
272
+
273
+
274
+ # --- Output formatting ---
275
+
276
+ def format_check(label, value, width=16):
277
+ """Format a single status line."""
278
+ return f" \u2713 {label:<{width}}{value}"
279
+
280
+
281
+ def print_boot_sequence(repo_root, gator_dir, hook_status):
282
+ """Print the branded boot sequence.
283
+
284
+ @reads: all detection functions above
285
+ @writes: stdout
286
+ """
287
+ repo_name = repo_root.name
288
+
289
+ # Header
290
+ print()
291
+ for line in GATOR_MARK_LINES:
292
+ print(f" {line}")
293
+ print()
294
+ print(f" {VERSION} · navigation coding, governed")
295
+ print()
296
+ print(f" $ gator init")
297
+ print()
298
+
299
+ # Detection
300
+ print(f" found .gator/")
301
+ print()
302
+
303
+ # Constitution
304
+ rule_count = count_constitution_rules(gator_dir)
305
+ print(format_check("constitution", f"{rule_count} rules in force"))
306
+
307
+ # Charters
308
+ charter_count, func_count, coverage = count_charters(gator_dir)
309
+ if charter_count > 0:
310
+ detail = f"{charter_count} modules \u00b7 {func_count} functions documented"
311
+ print(format_check("charters", detail))
312
+ else:
313
+ print(format_check("charters", "empty \u00b7 ready for bootstrap"))
314
+
315
+ # Working set
316
+ thread_count, line_count = count_working_set(gator_dir)
317
+ if thread_count > 0:
318
+ print(format_check("working set", f"{thread_count} threads \u00b7 {line_count:,} lines"))
319
+ else:
320
+ print(format_check("working set", "empty \u00b7 threads emerge from work"))
321
+
322
+ # Field guides (only show when present)
323
+ guide_count, guide_langs = count_field_guides(gator_dir)
324
+ if guide_count > 0:
325
+ lang_list = ", ".join(guide_langs)
326
+ print(format_check("field guides", f"{guide_count} languages ({lang_list})"))
327
+
328
+ # Enforcer
329
+ enforcer_status = detect_enforcer(gator_dir)
330
+ print(format_check("enforcer", enforcer_status))
331
+
332
+ # Git hooks
333
+ print(format_check("hooks", hook_status["detail"]))
334
+
335
+ # Command post
336
+ cp = detect_command_post(gator_dir)
337
+ if cp:
338
+ print(format_check("command post", "linked"))
339
+
340
+ # Tagline
341
+ print()
342
+ print(f" {TAGLINE}")
343
+ print(f" \u25b8")
344
+ print()
345
+
346
+
347
+ def print_json(repo_root, gator_dir, hook_status):
348
+ """Print status as JSON for programmatic consumption."""
349
+ rule_count = count_constitution_rules(gator_dir)
350
+ charter_count, func_count, coverage = count_charters(gator_dir)
351
+ thread_count, line_count = count_working_set(gator_dir)
352
+ guide_count, guide_langs = count_field_guides(gator_dir)
353
+ enforcer_status = detect_enforcer(gator_dir)
354
+ cp = detect_command_post(gator_dir)
355
+ version_info = read_version(gator_dir)
356
+
357
+ data = {
358
+ "version": VERSION,
359
+ "repo": repo_root.name,
360
+ "repo_path": str(repo_root),
361
+ "constitution_rules": rule_count,
362
+ "charters": {
363
+ "modules": charter_count,
364
+ "functions_mapped": func_count,
365
+ "coverage_pct": round(coverage, 1),
366
+ },
367
+ "working_set": {
368
+ "threads": thread_count,
369
+ "lines": line_count,
370
+ },
371
+ "field_guides": {
372
+ "count": guide_count,
373
+ "languages": guide_langs,
374
+ },
375
+ "enforcer": enforcer_status,
376
+ "hooks": hook_status,
377
+ "command_post": cp,
378
+ "gator_version": version_info,
379
+ }
380
+
381
+ print(json.dumps(data, indent=2))
382
+
383
+
384
+ def print_not_found():
385
+ """Print message when .gator/ is not found."""
386
+ print()
387
+ for line in GATOR_MARK_LINES:
388
+ print(f" {line}")
389
+ print()
390
+ print(f" {VERSION} · navigation coding, governed")
391
+ print()
392
+ print(" no .gator/ found.")
393
+ print()
394
+ print(" to gatorize this repo:")
395
+ print(" bash gator-command/scripts/gatorize.sh .")
396
+ print()
397
+
398
+
399
+ # --- Entry point ---
400
+
401
+ def main():
402
+ ensure_utf8_stdout()
403
+
404
+ parser = argparse.ArgumentParser(
405
+ description="Gator init — branded boot sequence for governed repos."
406
+ )
407
+ parser.add_argument(
408
+ "--path", "-p",
409
+ help="Path to search for .gator/ (default: current directory)",
410
+ )
411
+ parser.add_argument(
412
+ "--json", "-j",
413
+ action="store_true",
414
+ help="Output as JSON instead of formatted display",
415
+ )
416
+ args = parser.parse_args()
417
+
418
+ repo_root = find_gator_root(args.path)
419
+
420
+ if not repo_root:
421
+ print_not_found()
422
+ sys.exit(1)
423
+
424
+ gator_dir = repo_root / ".gator"
425
+ hook_status = ensure_git_hooks(repo_root, gator_dir)
426
+
427
+ if args.json:
428
+ print_json(repo_root, gator_dir, hook_status)
429
+ else:
430
+ print_boot_sequence(repo_root, gator_dir, hook_status)
431
+
432
+
433
+ if __name__ == "__main__":
434
+ main()
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gator-machine-id.py — Stable machine identity for session audit trails.
4
+
5
+ Generates and stores a persistent machine identifier at ~/.gator/machine-id.
6
+ The ID is a UUID generated once, persists across sessions and repos,
7
+ and is used in session summaries as the "where" pointer for audit trails.
8
+
9
+ Why not hostname?
10
+ - Hostnames change (reimages, domain joins, laptop swaps)
11
+ - Hostnames can be meaningless in enterprise (DESKTOP-NKSU8RO)
12
+ - Hostnames can collide across machines
13
+
14
+ The machine-id file also stores a human-readable label that the PI
15
+ can set to make the audit trail legible:
16
+
17
+ ~/.gator/machine-id:
18
+ id: a1b2c3d4-5678-90ab-cdef-1234567890ab
19
+ hostname: DESKTOP-NKSU8RO
20
+ label: alan-home-desktop
21
+ created: 2026-05-30
22
+
23
+ Usage:
24
+ python gator-command/scripts/gator-machine-id.py # show or create
25
+ python gator-command/scripts/gator-machine-id.py --label "alan-home-desktop"
26
+ python gator-command/scripts/gator-machine-id.py --json
27
+
28
+ As a module:
29
+ from gator_machine_id import get_machine_id
30
+ mid = get_machine_id() # {"id": "...", "hostname": "...", "label": "..."}
31
+
32
+ @reads: ~/.gator/machine-id
33
+ @writes: ~/.gator/machine-id (on first run only, or --label update)
34
+ """
35
+
36
+ import argparse
37
+ import json
38
+ import platform
39
+ import sys
40
+ import uuid
41
+ from datetime import date
42
+ from pathlib import Path
43
+
44
+
45
+ GATOR_USER_DIR = Path.home() / ".gator"
46
+ MACHINE_ID_FILE = GATOR_USER_DIR / "machine-id"
47
+
48
+
49
+ def get_machine_id():
50
+ """Get or create the stable machine identity.
51
+
52
+ Returns dict with id, hostname, label, created.
53
+ Creates ~/.gator/machine-id on first call.
54
+ """
55
+ if MACHINE_ID_FILE.exists():
56
+ return _read_machine_id()
57
+
58
+ # First run — generate and store
59
+ return _create_machine_id()
60
+
61
+
62
+ def _read_machine_id():
63
+ """Read existing machine-id file."""
64
+ data = {}
65
+ try:
66
+ for line in MACHINE_ID_FILE.read_text(encoding="utf-8").splitlines():
67
+ line = line.strip()
68
+ if ":" in line and not line.startswith("#"):
69
+ key, _, value = line.partition(":")
70
+ data[key.strip()] = value.strip()
71
+ except OSError:
72
+ return _create_machine_id()
73
+
74
+ if "id" not in data:
75
+ return _create_machine_id()
76
+
77
+ return data
78
+
79
+
80
+ def _create_machine_id():
81
+ """Generate a new machine-id and write to file."""
82
+ GATOR_USER_DIR.mkdir(parents=True, exist_ok=True)
83
+
84
+ data = {
85
+ "id": str(uuid.uuid4()),
86
+ "hostname": platform.node(),
87
+ "label": platform.node(), # default label = hostname, PI can change
88
+ "created": str(date.today()),
89
+ }
90
+
91
+ lines = [
92
+ f"id: {data['id']}",
93
+ f"hostname: {data['hostname']}",
94
+ f"label: {data['label']}",
95
+ f"created: {data['created']}",
96
+ ]
97
+ MACHINE_ID_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
98
+
99
+ return data
100
+
101
+
102
+ def set_label(new_label):
103
+ """Update the human-readable label."""
104
+ data = get_machine_id()
105
+ data["label"] = new_label
106
+
107
+ lines = [
108
+ f"id: {data['id']}",
109
+ f"hostname: {data.get('hostname', platform.node())}",
110
+ f"label: {data['label']}",
111
+ f"created: {data.get('created', str(date.today()))}",
112
+ ]
113
+ MACHINE_ID_FILE.write_text("\n".join(lines) + "\n", encoding="utf-8")
114
+
115
+ return data
116
+
117
+
118
+ def main():
119
+ parser = argparse.ArgumentParser(
120
+ description="Gator machine identity for session audit trails."
121
+ )
122
+ parser.add_argument(
123
+ "--label", "-l",
124
+ help="Set a human-readable label for this machine",
125
+ )
126
+ parser.add_argument(
127
+ "--json", "-j",
128
+ action="store_true",
129
+ help="Output as JSON",
130
+ )
131
+ args = parser.parse_args()
132
+
133
+ if args.label:
134
+ data = set_label(args.label)
135
+ if args.json:
136
+ print(json.dumps(data, indent=2))
137
+ else:
138
+ print(f" Machine label set to: {data['label']}")
139
+ print(f" ID: {data['id']}")
140
+ else:
141
+ data = get_machine_id()
142
+ if args.json:
143
+ print(json.dumps(data, indent=2))
144
+ else:
145
+ print(f" Machine ID: {data['id']}")
146
+ print(f" Hostname: {data.get('hostname', '?')}")
147
+ print(f" Label: {data.get('label', '?')}")
148
+ print(f" Created: {data.get('created', '?')}")
149
+ print(f" Stored at: {MACHINE_ID_FILE}")
150
+
151
+
152
+ if __name__ == "__main__":
153
+ main()