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,916 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator-deploy.py - Deploy the public Gator 1.0 repo as a blank command post.
|
|
4
|
+
|
|
5
|
+
Produces a working command post with:
|
|
6
|
+
gator-engine/ - product code (scripts, templates, docs, tests)
|
|
7
|
+
gator-command/ - blank knowledge layer (stubs, empty dirs)
|
|
8
|
+
.gator/ - self-governance (hooks, constitution, stub charters)
|
|
9
|
+
README.md - short, links to docs/
|
|
10
|
+
|
|
11
|
+
The structural separation means users can update product code via
|
|
12
|
+
`git fetch upstream && git checkout upstream/main -- gator-engine/`
|
|
13
|
+
without touching their content in gator-command/.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python src/gator_command/scripts/gator-deploy.py ../gator
|
|
17
|
+
python src/gator_command/scripts/gator-deploy.py ../gator --dry-run
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import io
|
|
24
|
+
import re
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Preserved across deploys — never deleted by remove_target_contents().
|
|
32
|
+
# gator-command/ is user-owned knowledge; .gator/ has user charters and content.
|
|
33
|
+
PRESERVE = {".git", "LICENSE", "gator-command", ".gator"}
|
|
34
|
+
|
|
35
|
+
PUBLIC_PROCEDURES = [
|
|
36
|
+
"branching-standard.md",
|
|
37
|
+
"charter-lookup.md",
|
|
38
|
+
"coding-standard.md",
|
|
39
|
+
"knowledge-capture.md",
|
|
40
|
+
"gatorize-permutations.md",
|
|
41
|
+
"session-schema.md",
|
|
42
|
+
"thin-link-spec.md",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
DOC_STUBS = {} # Real docs now live in docs/ at repo root
|
|
46
|
+
|
|
47
|
+
COMMAND_POST_STUBS = {
|
|
48
|
+
"mission.md": (
|
|
49
|
+
"# Mission\n\n## What We're Building\n\n"
|
|
50
|
+
"[What is this project? One paragraph.]\n\n"
|
|
51
|
+
"## Why It Exists\n\n[What problem does it solve?]\n"
|
|
52
|
+
),
|
|
53
|
+
"roadmap.md": (
|
|
54
|
+
"# Roadmap\n\n[Priority-ordered. Updated as items complete or priorities shift.]\n\n"
|
|
55
|
+
"**Status key**: Done · Building · Designed · Considering · Deferred\n"
|
|
56
|
+
),
|
|
57
|
+
"inbox.md": (
|
|
58
|
+
"# Inbox\n\nDrop anything here. No formatting needed. "
|
|
59
|
+
"The chat agent triages at session open.\n\n---\n\n"
|
|
60
|
+
),
|
|
61
|
+
"identity.md": (
|
|
62
|
+
"---\noperating-mode: designer\n---\n\n# Identity\n\n## Basics\n\n"
|
|
63
|
+
"- **Name**: [Your name]\n"
|
|
64
|
+
"- **Location**: [Your timezone]\n"
|
|
65
|
+
"- **Role**: [Your role]\n\n"
|
|
66
|
+
"## Background\n\n[Brief background]\n"
|
|
67
|
+
),
|
|
68
|
+
"issues.md": (
|
|
69
|
+
"# Issues\n\nActive bugs, blockers, and known fragilities. "
|
|
70
|
+
"Checked at session open.\n\n**Status key**: Open · Working · Resolved\n\n---\n\n"
|
|
71
|
+
),
|
|
72
|
+
"registry.md": (
|
|
73
|
+
"# Registry\n\nRepos governed by this Gator Command post. "
|
|
74
|
+
"Updated by `gatorize.sh` and maintained by the Architect.\n\n"
|
|
75
|
+
"| Repo | Local path | Remote | Registered | Status |\n"
|
|
76
|
+
"|------|-----------|--------|------------|--------|\n"
|
|
77
|
+
),
|
|
78
|
+
"commit_draft.md": (
|
|
79
|
+
"---\nmessage: \"\"\nchange-type:\nsignificance:\ndecision-tags: []\n"
|
|
80
|
+
"agent:\narchitect:\n---\n\n# Session Change Log\n\n"
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
COMMAND_POST_DIRS = [
|
|
85
|
+
"active-threads",
|
|
86
|
+
"threads",
|
|
87
|
+
"artifacts",
|
|
88
|
+
"vault",
|
|
89
|
+
"patterns",
|
|
90
|
+
"policies",
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
SELF_GOVERNANCE_STUBS = {
|
|
94
|
+
"commit_draft.md": (
|
|
95
|
+
"---\nmessage: \"\"\nchange-type:\nsignificance:\ndecision-tags: []\n"
|
|
96
|
+
"agent:\narchitect:\n---\n\n# Session Change Log\n\n"
|
|
97
|
+
),
|
|
98
|
+
"whiteboard.md": "# Whiteboard\n\nNo findings.\n",
|
|
99
|
+
"issues.md": "# Issues\n\n---\n\n",
|
|
100
|
+
"mission.md": (
|
|
101
|
+
"# Mission\n\n"
|
|
102
|
+
"This Gator Command post governs AI-assisted development across "
|
|
103
|
+
"a fleet of project repos.\n\n"
|
|
104
|
+
"See `gator-command/mission.md` for your project-specific mission "
|
|
105
|
+
"once populated during the concierge bootstrap.\n"
|
|
106
|
+
),
|
|
107
|
+
"command-post.md": (
|
|
108
|
+
"command-post: ..\nversion: 2026-05-30\n\n# Gator Command Post\n\n"
|
|
109
|
+
"This repo is the command post.\n"
|
|
110
|
+
),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
SELF_GOVERNANCE_DIRS = ["threads", "artifacts", "sessions", "blueprints"]
|
|
114
|
+
|
|
115
|
+
ENTRY_POINTS = [
|
|
116
|
+
("CLAUDE.md", "# Claude Code Entry Point"),
|
|
117
|
+
("AGENTS.md", "# Codex Entry Point"),
|
|
118
|
+
("GEMINI.md", "# Gemini Entry Point"),
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
ENTRY_BODY = (
|
|
122
|
+
"\nThis is a **Gator Command post**.\n\n"
|
|
123
|
+
"Read [`constitution.md`](constitution.md) before your first response.\n\n"
|
|
124
|
+
"**Fresh instance detection**: If `gator-command/identity.md` still has bracket "
|
|
125
|
+
"placeholders (e.g., `[Your name]`), this is a first session. Greet the Architect and "
|
|
126
|
+
"ask what they're working on.\n\n"
|
|
127
|
+
"**Returning sessions**: Follow the session-opening procedure in the constitution.\n\n"
|
|
128
|
+
'**"gator init"**: run `python gator-engine/scripts/gator-init-command-post.py` '
|
|
129
|
+
"and display the output exactly as printed. This is the command post boot sequence.\n\n"
|
|
130
|
+
'**"gator dashboard"**: run `python gator-engine/scripts/gator-dashboard.py` '
|
|
131
|
+
"to open the governance dashboard in your browser.\n"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
ROOT_GITIGNORE = (
|
|
135
|
+
"# Vault — sensitive material and large files that must not be committed\n"
|
|
136
|
+
"gator-command/vault/\n"
|
|
137
|
+
".gator/vault/\n"
|
|
138
|
+
"\n"
|
|
139
|
+
"__pycache__/\n"
|
|
140
|
+
"*.pyc\n"
|
|
141
|
+
".pytest_tmp/\n"
|
|
142
|
+
".pytest_cache/\n"
|
|
143
|
+
".vscode/\n"
|
|
144
|
+
".claude/settings.local.json\n"
|
|
145
|
+
"\n"
|
|
146
|
+
"# Ephemeral commit artifacts — reset by hooks\n"
|
|
147
|
+
".gator/commit_draft.md\n"
|
|
148
|
+
".gator/commit_issues.md\n"
|
|
149
|
+
"gator-command/commit_draft.md\n"
|
|
150
|
+
"\n"
|
|
151
|
+
"# Override request/approval artifacts — ephemeral\n"
|
|
152
|
+
".gator/override-request.json\n"
|
|
153
|
+
".gator/override-approved.json\n"
|
|
154
|
+
"\n"
|
|
155
|
+
"# Rolling active session logs\n"
|
|
156
|
+
".gator/sessions/_active/\n"
|
|
157
|
+
"\n"
|
|
158
|
+
"# Machine-local state (not portable)\n"
|
|
159
|
+
".gator/product-source.json\n"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def find_command_post():
|
|
164
|
+
"""Find the source command post root."""
|
|
165
|
+
script_dir = Path(__file__).resolve().parent
|
|
166
|
+
# Scripts are now at src/gator_command/scripts/ — repo root is 3 levels up
|
|
167
|
+
candidate = script_dir.parent.parent.parent
|
|
168
|
+
if (candidate / "gator-command" / "mission.md").exists():
|
|
169
|
+
return candidate
|
|
170
|
+
# Fallback: try 2 levels up (old layout or deployed gator-engine/)
|
|
171
|
+
candidate = script_dir.parent.parent
|
|
172
|
+
if (candidate / "gator-command" / "mission.md").exists():
|
|
173
|
+
return candidate
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_source_version(source):
|
|
178
|
+
"""Get version string from source repo via git describe.
|
|
179
|
+
|
|
180
|
+
Returns a string like 'v1.0.0-5-gabcdef1' between tags, or 'v1.0.0'
|
|
181
|
+
on an exact tag. Falls back to VERSION file content, then 'dev'.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
result = subprocess.run(
|
|
185
|
+
["git", "describe", "--tags", "--always"],
|
|
186
|
+
capture_output=True, encoding="utf-8", errors="replace", cwd=source, timeout=5,
|
|
187
|
+
)
|
|
188
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
189
|
+
version = result.stdout.strip()
|
|
190
|
+
if version.startswith("v"):
|
|
191
|
+
return version
|
|
192
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
version_file = source / "VERSION"
|
|
196
|
+
if version_file.exists():
|
|
197
|
+
text = version_file.read_text(encoding="utf-8").strip()
|
|
198
|
+
if text:
|
|
199
|
+
return f"v{text}" if not text.startswith("v") else text
|
|
200
|
+
|
|
201
|
+
return "dev"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def get_source_short_hash(source):
|
|
205
|
+
"""Get the short commit hash of the source repo HEAD."""
|
|
206
|
+
try:
|
|
207
|
+
result = subprocess.run(
|
|
208
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
209
|
+
capture_output=True, encoding="utf-8", errors="replace", cwd=source, timeout=5,
|
|
210
|
+
)
|
|
211
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
212
|
+
return result.stdout.strip()
|
|
213
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
214
|
+
pass
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def stamp_readme_version(target, version, source_hash=None):
|
|
219
|
+
"""Replace the static version in README.md with the deploy version and source hash."""
|
|
220
|
+
readme = target / "README.md"
|
|
221
|
+
if not readme.exists():
|
|
222
|
+
return
|
|
223
|
+
text = readme.read_text(encoding="utf-8")
|
|
224
|
+
|
|
225
|
+
# Build the replacement: "v1.0.0 (b8b1542) · navigation..." or just version
|
|
226
|
+
version_stamp = version
|
|
227
|
+
if source_hash:
|
|
228
|
+
version_stamp = f"{version} ({source_hash})"
|
|
229
|
+
|
|
230
|
+
# Match the version line in the ASCII banner: " v1.0.0 · ..." or " v1.0.0 (hash) · ..."
|
|
231
|
+
text = re.sub(
|
|
232
|
+
r"^( )v[\d]+\.[\d]+\.[\d]+\S*(?:\s+\([a-f0-9]+\))?( · .*)$",
|
|
233
|
+
rf"\g<1>{version_stamp}\g<2>",
|
|
234
|
+
text,
|
|
235
|
+
count=1,
|
|
236
|
+
flags=re.MULTILINE,
|
|
237
|
+
)
|
|
238
|
+
readme.write_text(text, encoding="utf-8")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def remove_target_contents(target, dry_run=False):
|
|
242
|
+
"""Remove everything except files explicitly preserved."""
|
|
243
|
+
removed = []
|
|
244
|
+
for item in sorted(target.iterdir()):
|
|
245
|
+
if item.name in PRESERVE:
|
|
246
|
+
continue
|
|
247
|
+
if item.is_dir():
|
|
248
|
+
if not dry_run:
|
|
249
|
+
shutil.rmtree(item)
|
|
250
|
+
removed.append(f"{item.name}/")
|
|
251
|
+
else:
|
|
252
|
+
if not dry_run:
|
|
253
|
+
item.unlink()
|
|
254
|
+
removed.append(item.name)
|
|
255
|
+
return removed
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def write_file(path, content, dry_run=False):
|
|
259
|
+
"""Write a file, creating parent dirs."""
|
|
260
|
+
if dry_run:
|
|
261
|
+
return
|
|
262
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
path.write_text(content, encoding="utf-8")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def copy_file(src, dest, dry_run=False):
|
|
267
|
+
"""Copy a file, creating parent dirs."""
|
|
268
|
+
if dry_run:
|
|
269
|
+
return
|
|
270
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
shutil.copy2(src, dest)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def copy_tree_mirror(src, dest, dry_run=False):
|
|
275
|
+
"""Mirror a directory tree — exact copy, prune stale destination files.
|
|
276
|
+
|
|
277
|
+
Used for product-owned directories (gator-engine/, docs/, .github/)
|
|
278
|
+
where the deploy output should exactly match the source.
|
|
279
|
+
"""
|
|
280
|
+
if dry_run:
|
|
281
|
+
return
|
|
282
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
283
|
+
if dest.exists():
|
|
284
|
+
shutil.rmtree(dest)
|
|
285
|
+
shutil.copytree(
|
|
286
|
+
src, dest,
|
|
287
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def copy_tree_overlay(src, dest, dry_run=False):
|
|
292
|
+
"""Overlay a directory tree — update same-named files, preserve extras.
|
|
293
|
+
|
|
294
|
+
Used for user-owned directories (gator-command/procedures/,
|
|
295
|
+
reference-notes/) where user-authored content must survive deploys.
|
|
296
|
+
"""
|
|
297
|
+
if dry_run:
|
|
298
|
+
return
|
|
299
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
300
|
+
for item in src.iterdir():
|
|
301
|
+
if item.name in ("__pycache__",) or item.suffix == ".pyc":
|
|
302
|
+
continue
|
|
303
|
+
target = dest / item.name
|
|
304
|
+
if item.is_dir():
|
|
305
|
+
copy_tree_overlay(item, target, dry_run)
|
|
306
|
+
else:
|
|
307
|
+
shutil.copy2(item, target)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def write_stub_files(base_dir, files, dry_run=False):
|
|
311
|
+
"""Write a mapping of relative paths to file contents.
|
|
312
|
+
|
|
313
|
+
Only creates files that don't already exist — user-owned content
|
|
314
|
+
is never overwritten. This is the deploy equivalent of gatorize.sh's
|
|
315
|
+
overlay-not-replace principle.
|
|
316
|
+
"""
|
|
317
|
+
for relative_path, content in files.items():
|
|
318
|
+
dest = base_dir / relative_path
|
|
319
|
+
if dest.exists():
|
|
320
|
+
continue
|
|
321
|
+
write_file(dest, content, dry_run)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def ensure_gitkeep_dirs(base_dir, dir_names, dry_run=False):
|
|
325
|
+
"""Create empty directories tracked with .gitkeep."""
|
|
326
|
+
for dirname in dir_names:
|
|
327
|
+
path = base_dir / dirname
|
|
328
|
+
if dry_run:
|
|
329
|
+
continue
|
|
330
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
(path / ".gitkeep").touch()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def copy_matching_files(src_dir, dest_dir, suffixes, results, result_prefix, dry_run=False):
|
|
335
|
+
"""Copy files from a directory when their suffix matches."""
|
|
336
|
+
if not dry_run:
|
|
337
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
338
|
+
|
|
339
|
+
for src in sorted(src_dir.iterdir()):
|
|
340
|
+
if src.is_file() and src.suffix in suffixes:
|
|
341
|
+
copy_file(src, dest_dir / src.name, dry_run)
|
|
342
|
+
results["created"].append(f"{result_prefix}/{src.name}")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def copy_optional_tree(src_dir, dest_dir, results, result_label, dry_run=False,
|
|
346
|
+
overlay=False):
|
|
347
|
+
"""Copy a directory if it exists and record a concise result label.
|
|
348
|
+
|
|
349
|
+
overlay=False (default): mirror mode — exact copy, prune stale files.
|
|
350
|
+
Use for product-owned directories.
|
|
351
|
+
overlay=True: overlay mode — update same-named, preserve extras.
|
|
352
|
+
Use for user-owned directories.
|
|
353
|
+
"""
|
|
354
|
+
if not src_dir.is_dir():
|
|
355
|
+
return
|
|
356
|
+
if overlay:
|
|
357
|
+
copy_tree_overlay(src_dir, dest_dir, dry_run)
|
|
358
|
+
else:
|
|
359
|
+
copy_tree_mirror(src_dir, dest_dir, dry_run)
|
|
360
|
+
count = sum(1 for item in src_dir.rglob("*") if item.is_file())
|
|
361
|
+
results["created"].append(f"{result_label} ({count} files)")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def copy_optional_file(src, dest, results, result_label, dry_run=False):
|
|
365
|
+
"""Copy a file if it exists and record the destination label."""
|
|
366
|
+
if not src.exists():
|
|
367
|
+
return
|
|
368
|
+
copy_file(src, dest, dry_run)
|
|
369
|
+
results["created"].append(result_label)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _fixup_test_paths(tests_dir, dry_run=False):
|
|
373
|
+
"""Rewrite path references in test files for the deployed layout.
|
|
374
|
+
|
|
375
|
+
In the command-post layout, tests/ is a sibling of gator-command/scripts/.
|
|
376
|
+
In the deployed layout, tests live at gator-engine/tests/ and scripts at
|
|
377
|
+
gator-engine/scripts/. This function patches conftest.py and
|
|
378
|
+
test_pre_commit.py so imports resolve correctly after deploy.
|
|
379
|
+
"""
|
|
380
|
+
if dry_run:
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
# conftest.py: SCRIPTS_DIR should resolve to gator-engine/scripts/
|
|
384
|
+
# (parent.parent / "scripts" from gator-engine/tests/)
|
|
385
|
+
conftest = tests_dir / "conftest.py"
|
|
386
|
+
if conftest.is_file():
|
|
387
|
+
text = conftest.read_text(encoding="utf-8")
|
|
388
|
+
# Replace any gator-command/scripts path with the deployed relative path
|
|
389
|
+
text = text.replace(
|
|
390
|
+
'Path(__file__).parent.parent / "src" / "gator_command" / "scripts"',
|
|
391
|
+
'Path(__file__).resolve().parent.parent / "scripts"',
|
|
392
|
+
)
|
|
393
|
+
conftest.write_text(text, encoding="utf-8")
|
|
394
|
+
|
|
395
|
+
# test_pre_commit.py: pre-commit hook lives at repo-root/.gator/scripts/
|
|
396
|
+
# (parent.parent.parent from gator-engine/tests/)
|
|
397
|
+
test_pc = tests_dir / "test_pre_commit.py"
|
|
398
|
+
if test_pc.is_file():
|
|
399
|
+
text = test_pc.read_text(encoding="utf-8")
|
|
400
|
+
text = text.replace(
|
|
401
|
+
'Path(__file__).parent.parent / ".gator" / "scripts"',
|
|
402
|
+
'Path(__file__).resolve().parent.parent.parent / ".gator" / "scripts"',
|
|
403
|
+
)
|
|
404
|
+
test_pc.write_text(text, encoding="utf-8")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def build_engine(source, target, results, dry_run=False):
|
|
408
|
+
"""Build gator-engine/, the shipped product surface."""
|
|
409
|
+
pkg = source / "src" / "gator_command"
|
|
410
|
+
engine = target / "gator-engine"
|
|
411
|
+
|
|
412
|
+
copy_matching_files(
|
|
413
|
+
pkg / "scripts",
|
|
414
|
+
engine / "scripts",
|
|
415
|
+
{".py", ".sh"},
|
|
416
|
+
results,
|
|
417
|
+
"gator-engine/scripts",
|
|
418
|
+
dry_run,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
copy_optional_tree(
|
|
422
|
+
pkg / "scripts" / "dashboard",
|
|
423
|
+
engine / "scripts" / "dashboard",
|
|
424
|
+
results,
|
|
425
|
+
"gator-engine/scripts/dashboard/",
|
|
426
|
+
dry_run,
|
|
427
|
+
)
|
|
428
|
+
copy_optional_tree(
|
|
429
|
+
pkg / "templates" / "gator-starter",
|
|
430
|
+
engine / "templates" / "gator-starter",
|
|
431
|
+
results,
|
|
432
|
+
"gator-engine/templates/gator-starter/",
|
|
433
|
+
dry_run,
|
|
434
|
+
)
|
|
435
|
+
copy_optional_tree(
|
|
436
|
+
source / "tests",
|
|
437
|
+
engine / "tests",
|
|
438
|
+
results,
|
|
439
|
+
"gator-engine/tests/",
|
|
440
|
+
dry_run,
|
|
441
|
+
)
|
|
442
|
+
_fixup_test_paths(engine / "tests", dry_run)
|
|
443
|
+
|
|
444
|
+
docs_dest = engine / "docs"
|
|
445
|
+
gc = source / "gator-command" # knowledge layer (didn't move)
|
|
446
|
+
copy_optional_file(
|
|
447
|
+
gc / "charters" / "README.md",
|
|
448
|
+
docs_dest / "charter-philosophy.md",
|
|
449
|
+
results,
|
|
450
|
+
"gator-engine/docs/charter-philosophy.md",
|
|
451
|
+
dry_run,
|
|
452
|
+
)
|
|
453
|
+
copy_optional_file(
|
|
454
|
+
gc / "org-policy.md",
|
|
455
|
+
docs_dest / "org-policy-example.md",
|
|
456
|
+
results,
|
|
457
|
+
"gator-engine/docs/org-policy-example.md",
|
|
458
|
+
dry_run,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
for name in PUBLIC_PROCEDURES:
|
|
462
|
+
copy_optional_file(
|
|
463
|
+
gc / "procedures" / name,
|
|
464
|
+
docs_dest / "procedures" / name,
|
|
465
|
+
results,
|
|
466
|
+
f"gator-engine/docs/procedures/{name}",
|
|
467
|
+
dry_run,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
write_stub_files(docs_dest, DOC_STUBS, dry_run)
|
|
471
|
+
results["created"].extend(
|
|
472
|
+
f"gator-engine/docs/{name}" for name in DOC_STUBS
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
copy_optional_file(
|
|
476
|
+
source / "pytest.ini",
|
|
477
|
+
engine / "pytest.ini",
|
|
478
|
+
results,
|
|
479
|
+
"gator-engine/pytest.ini",
|
|
480
|
+
dry_run,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def build_command_post_layer(source, target, results, dry_run=False):
|
|
485
|
+
"""Build gator-command/, the blank user-owned knowledge layer."""
|
|
486
|
+
gc = source / "gator-command"
|
|
487
|
+
dest = target / "gator-command"
|
|
488
|
+
|
|
489
|
+
write_stub_files(dest, COMMAND_POST_STUBS, dry_run)
|
|
490
|
+
results["created"].append("gator-command/ (knowledge layer stubs)")
|
|
491
|
+
|
|
492
|
+
copy_optional_file(
|
|
493
|
+
gc / "org-policy.md",
|
|
494
|
+
dest / "org-policy.md",
|
|
495
|
+
results,
|
|
496
|
+
"gator-command/org-policy.md",
|
|
497
|
+
dry_run,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
ensure_gitkeep_dirs(dest, COMMAND_POST_DIRS, dry_run)
|
|
501
|
+
|
|
502
|
+
for subdir in ("procedures", "reference-notes"):
|
|
503
|
+
copy_optional_tree(
|
|
504
|
+
gc / subdir,
|
|
505
|
+
dest / subdir,
|
|
506
|
+
results,
|
|
507
|
+
f"gator-command/{subdir}/",
|
|
508
|
+
dry_run,
|
|
509
|
+
overlay=True, # user-owned: preserve extras
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
write_file(
|
|
513
|
+
dest / "scripts" / "README.md",
|
|
514
|
+
"# Scripts\n\nProduct scripts live in `gator-engine/scripts/`.\n\n"
|
|
515
|
+
"This directory is for command-post-specific scripts you create.\n",
|
|
516
|
+
dry_run,
|
|
517
|
+
)
|
|
518
|
+
write_file(
|
|
519
|
+
dest / "templates" / "README.md",
|
|
520
|
+
"# Templates\n\nThe gator-starter template lives in "
|
|
521
|
+
"`gator-engine/templates/gator-starter/`.\n",
|
|
522
|
+
dry_run,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def copy_template_scripts(starter_scripts, dest_scripts, dry_run=False):
|
|
527
|
+
"""Copy template scripts, including nested hook directories."""
|
|
528
|
+
if not starter_scripts.is_dir():
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
for src in sorted(starter_scripts.iterdir()):
|
|
532
|
+
if src.name == "__pycache__":
|
|
533
|
+
continue
|
|
534
|
+
if src.is_file():
|
|
535
|
+
if src.suffix == ".pyc":
|
|
536
|
+
continue
|
|
537
|
+
copy_file(src, dest_scripts / src.name, dry_run)
|
|
538
|
+
continue
|
|
539
|
+
for nested in sorted(src.iterdir()):
|
|
540
|
+
if nested.is_file() and nested.suffix != ".pyc":
|
|
541
|
+
copy_file(nested, dest_scripts / src.name / nested.name, dry_run)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def copy_flat_directory(src_dir, dest_dir, dry_run=False):
|
|
545
|
+
"""Copy only direct child files from one directory to another."""
|
|
546
|
+
if not src_dir.is_dir():
|
|
547
|
+
return
|
|
548
|
+
for src in sorted(src_dir.iterdir()):
|
|
549
|
+
if src.is_file():
|
|
550
|
+
copy_file(src, dest_dir / src.name, dry_run)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def build_self_governance(source, target, results, dry_run=False):
|
|
554
|
+
"""Build .gator/, the repo's own governance scaffolding."""
|
|
555
|
+
starter = source / "src" / "gator_command" / "templates" / "gator-starter"
|
|
556
|
+
dest = target / ".gator"
|
|
557
|
+
|
|
558
|
+
copy_optional_file(
|
|
559
|
+
starter / "constitution.md",
|
|
560
|
+
dest / "constitution.md",
|
|
561
|
+
results,
|
|
562
|
+
".gator/constitution.md",
|
|
563
|
+
dry_run,
|
|
564
|
+
)
|
|
565
|
+
copy_optional_file(
|
|
566
|
+
starter / "gator-start-up.md",
|
|
567
|
+
dest / "gator-start-up.md",
|
|
568
|
+
results,
|
|
569
|
+
".gator/gator-start-up.md",
|
|
570
|
+
dry_run,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
for name in ("README.md", "_template.md"):
|
|
574
|
+
copy_optional_file(
|
|
575
|
+
starter / "charters" / name,
|
|
576
|
+
dest / "charters" / name,
|
|
577
|
+
results,
|
|
578
|
+
f".gator/charters/{name}",
|
|
579
|
+
dry_run,
|
|
580
|
+
)
|
|
581
|
+
index_path = dest / "charters" / "INDEX.md"
|
|
582
|
+
if not index_path.exists():
|
|
583
|
+
write_file(
|
|
584
|
+
index_path,
|
|
585
|
+
"# Charter Index\n\n| If you're changing... | Read these charters |\n"
|
|
586
|
+
"|---|---|\n| | |\n",
|
|
587
|
+
dry_run,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
copy_template_scripts(starter / "scripts", dest / "scripts", dry_run)
|
|
591
|
+
|
|
592
|
+
for subdir in ("reference-notes", "procedures"):
|
|
593
|
+
copy_flat_directory(starter / subdir, dest / subdir, dry_run)
|
|
594
|
+
|
|
595
|
+
write_stub_files(dest, SELF_GOVERNANCE_STUBS, dry_run)
|
|
596
|
+
ensure_gitkeep_dirs(dest, SELF_GOVERNANCE_DIRS, dry_run)
|
|
597
|
+
results["created"].append(".gator/ (self-governance)")
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def build_root_files(source, target, results, dry_run=False):
|
|
601
|
+
"""Build repo-root files for the deployed public command post."""
|
|
602
|
+
starter = source / "src" / "gator_command" / "templates" / "gator-starter"
|
|
603
|
+
|
|
604
|
+
copy_optional_file(
|
|
605
|
+
starter / "constitution.md",
|
|
606
|
+
target / "constitution.md",
|
|
607
|
+
results,
|
|
608
|
+
"constitution.md",
|
|
609
|
+
dry_run,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
for filename, header in ENTRY_POINTS:
|
|
613
|
+
write_file(target / filename, f"{header}\n{ENTRY_BODY}", dry_run)
|
|
614
|
+
results["created"].append(filename)
|
|
615
|
+
|
|
616
|
+
copy_optional_file(
|
|
617
|
+
source / "gator-command" / "deploy" / "README.md",
|
|
618
|
+
target / "README.md",
|
|
619
|
+
results,
|
|
620
|
+
"README.md",
|
|
621
|
+
dry_run,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
# User-facing docs (persona pages, feature docs)
|
|
625
|
+
copy_optional_tree(
|
|
626
|
+
source / "docs",
|
|
627
|
+
target / "docs",
|
|
628
|
+
results,
|
|
629
|
+
"docs/",
|
|
630
|
+
dry_run,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
copy_optional_file(
|
|
634
|
+
source / "VERSION",
|
|
635
|
+
target / "VERSION",
|
|
636
|
+
results,
|
|
637
|
+
"VERSION",
|
|
638
|
+
dry_run,
|
|
639
|
+
)
|
|
640
|
+
copy_optional_file(
|
|
641
|
+
source / "mkdocs.yml",
|
|
642
|
+
target / "mkdocs.yml",
|
|
643
|
+
results,
|
|
644
|
+
"mkdocs.yml",
|
|
645
|
+
dry_run,
|
|
646
|
+
)
|
|
647
|
+
copy_optional_file(
|
|
648
|
+
source / "CHANGELOG.md",
|
|
649
|
+
target / "CHANGELOG.md",
|
|
650
|
+
results,
|
|
651
|
+
"CHANGELOG.md",
|
|
652
|
+
dry_run,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
copy_optional_file(
|
|
656
|
+
source / "CONTRIBUTING.md",
|
|
657
|
+
target / "CONTRIBUTING.md",
|
|
658
|
+
results,
|
|
659
|
+
"CONTRIBUTING.md",
|
|
660
|
+
dry_run,
|
|
661
|
+
)
|
|
662
|
+
copy_optional_file(
|
|
663
|
+
source / "SECURITY.md",
|
|
664
|
+
target / "SECURITY.md",
|
|
665
|
+
results,
|
|
666
|
+
"SECURITY.md",
|
|
667
|
+
dry_run,
|
|
668
|
+
)
|
|
669
|
+
copy_optional_tree(
|
|
670
|
+
source / ".github",
|
|
671
|
+
target / ".github",
|
|
672
|
+
results,
|
|
673
|
+
".github/",
|
|
674
|
+
dry_run,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
write_file(target / ".gitignore", ROOT_GITIGNORE, dry_run)
|
|
678
|
+
results["created"].append(".gitignore")
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def find_last_deploy_source_hash(target):
|
|
682
|
+
"""Find the source repo hash from the most recent deploy commit in the target.
|
|
683
|
+
|
|
684
|
+
Looks for "From gator-command <hash>" in recent commit messages.
|
|
685
|
+
Returns the hash string, or None if not found.
|
|
686
|
+
"""
|
|
687
|
+
try:
|
|
688
|
+
result = subprocess.run(
|
|
689
|
+
["git", "log", "--format=%B", "-20"],
|
|
690
|
+
capture_output=True, encoding="utf-8", errors="replace", cwd=target, timeout=5,
|
|
691
|
+
)
|
|
692
|
+
if result.returncode != 0:
|
|
693
|
+
return None
|
|
694
|
+
for line in result.stdout.splitlines():
|
|
695
|
+
line = line.strip().lstrip("- ")
|
|
696
|
+
# Match both "From gator-command <hash>" and "Deploy from gator-command <hash> (...)"
|
|
697
|
+
if "from gator-command" in line.lower():
|
|
698
|
+
parts = line.split()
|
|
699
|
+
# Find the token after "gator-command"
|
|
700
|
+
for i, token in enumerate(parts):
|
|
701
|
+
if token == "gator-command" and i + 1 < len(parts):
|
|
702
|
+
return parts[i + 1].rstrip(":").split("(")[0].strip()
|
|
703
|
+
break
|
|
704
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
705
|
+
pass
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def build_deploy_changelog(source, target):
|
|
710
|
+
"""Build a changelog from source repo commits since the last deploy.
|
|
711
|
+
|
|
712
|
+
Returns a list of one-line commit summaries, newest first.
|
|
713
|
+
"""
|
|
714
|
+
base_hash = find_last_deploy_source_hash(target)
|
|
715
|
+
if not base_hash:
|
|
716
|
+
# No previous deploy found — use last 10 commits as context
|
|
717
|
+
log_range = ["-10"]
|
|
718
|
+
else:
|
|
719
|
+
log_range = [f"{base_hash}..HEAD"]
|
|
720
|
+
|
|
721
|
+
try:
|
|
722
|
+
result = subprocess.run(
|
|
723
|
+
["git", "log", "--oneline", "--no-decorate"] + log_range,
|
|
724
|
+
capture_output=True, encoding="utf-8", errors="replace", cwd=source, timeout=10,
|
|
725
|
+
)
|
|
726
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
727
|
+
return result.stdout.strip().splitlines()
|
|
728
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
729
|
+
pass
|
|
730
|
+
return []
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def write_deploy_commit_draft(target, source, version, dry_run=False):
|
|
734
|
+
"""Write the target repo's commit_draft.md with the deploy changelog.
|
|
735
|
+
|
|
736
|
+
Populates the frontmatter and body so the deploy commit carries
|
|
737
|
+
the full source changelog, not just "deploy from hash."
|
|
738
|
+
"""
|
|
739
|
+
if dry_run:
|
|
740
|
+
return
|
|
741
|
+
|
|
742
|
+
changelog = build_deploy_changelog(source, target)
|
|
743
|
+
|
|
744
|
+
# Get the current source HEAD for the "From" reference
|
|
745
|
+
try:
|
|
746
|
+
result = subprocess.run(
|
|
747
|
+
["git", "rev-parse", "--short", "HEAD"],
|
|
748
|
+
capture_output=True, encoding="utf-8", errors="replace", cwd=source, timeout=5,
|
|
749
|
+
)
|
|
750
|
+
source_hash = result.stdout.strip() if result.returncode == 0 else "unknown"
|
|
751
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
752
|
+
source_hash = "unknown"
|
|
753
|
+
|
|
754
|
+
# Build the body
|
|
755
|
+
body_lines = [f"- Deploy from gator-command {source_hash} ({version})"]
|
|
756
|
+
for line in changelog:
|
|
757
|
+
# Each line is "hash summary" — prefix with bullet
|
|
758
|
+
parts = line.split(" ", 1)
|
|
759
|
+
if len(parts) == 2:
|
|
760
|
+
body_lines.append(f"- {parts[1]}")
|
|
761
|
+
else:
|
|
762
|
+
body_lines.append(f"- {line}")
|
|
763
|
+
|
|
764
|
+
body = "\n".join(body_lines)
|
|
765
|
+
|
|
766
|
+
# Summarize for the message field — use first changelog entry as description
|
|
767
|
+
if changelog:
|
|
768
|
+
# Each changelog line is "hash summary" — extract the summary part
|
|
769
|
+
first = changelog[0].split(" ", 1)
|
|
770
|
+
summary = first[1] if len(first) == 2 else changelog[0]
|
|
771
|
+
# Strip redundant "Deploy:" prefix to avoid "Deploy v1.0.0: Deploy: ..."
|
|
772
|
+
if summary.lower().startswith("deploy:"):
|
|
773
|
+
summary = summary[7:].strip()
|
|
774
|
+
elif summary.lower().startswith("deploy "):
|
|
775
|
+
summary = summary[7:].strip()
|
|
776
|
+
if len(changelog) == 1:
|
|
777
|
+
message = f"Deploy {version}: {summary}"
|
|
778
|
+
else:
|
|
779
|
+
message = f"Deploy {version}: {summary} (+{len(changelog) - 1} more)"
|
|
780
|
+
else:
|
|
781
|
+
message = f"Deploy {version} from gator-command {source_hash}"
|
|
782
|
+
|
|
783
|
+
draft = (
|
|
784
|
+
f"---\n"
|
|
785
|
+
f'message: "{message}"\n'
|
|
786
|
+
f"change-type: deploy\n"
|
|
787
|
+
f"significance: routine\n"
|
|
788
|
+
f"decision-tags: [deploy]\n"
|
|
789
|
+
f"agent:\n"
|
|
790
|
+
f"architect:\n"
|
|
791
|
+
f"---\n\n"
|
|
792
|
+
f"# Session Change Log\n\n"
|
|
793
|
+
f"{body}\n"
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
draft_path = target / ".gator" / "commit_draft.md"
|
|
797
|
+
draft_path.parent.mkdir(parents=True, exist_ok=True)
|
|
798
|
+
draft_path.write_text(draft, encoding="utf-8")
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def deploy(source, target, dry_run=False):
|
|
802
|
+
"""Build the complete public repo structure."""
|
|
803
|
+
results = {"nuked": remove_target_contents(target, dry_run), "created": []}
|
|
804
|
+
build_engine(source, target, results, dry_run)
|
|
805
|
+
build_command_post_layer(source, target, results, dry_run)
|
|
806
|
+
build_self_governance(source, target, results, dry_run)
|
|
807
|
+
build_root_files(source, target, results, dry_run)
|
|
808
|
+
|
|
809
|
+
# Stamp the README with the source repo's git describe version + commit hash
|
|
810
|
+
version = get_source_version(source)
|
|
811
|
+
source_hash = get_source_short_hash(source)
|
|
812
|
+
if not dry_run:
|
|
813
|
+
stamp_readme_version(target, version, source_hash)
|
|
814
|
+
|
|
815
|
+
# Write the deploy commit_draft in the target with source changelog
|
|
816
|
+
write_deploy_commit_draft(target, source, version, dry_run)
|
|
817
|
+
|
|
818
|
+
# Pre-approve charter-skip override — deploy targets typically have blank
|
|
819
|
+
# charters, so the pre-commit hook would always block on code changes.
|
|
820
|
+
if not dry_run:
|
|
821
|
+
override_file = target / ".gator" / ".override"
|
|
822
|
+
override_file.write_text("charter-skip", encoding="utf-8")
|
|
823
|
+
|
|
824
|
+
return results
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def target_looks_initialized(target):
|
|
828
|
+
"""Detect whether the target appears to contain real command-post content."""
|
|
829
|
+
identity = target / "gator-command" / "identity.md"
|
|
830
|
+
threads_dir = target / "gator-command" / "active-threads"
|
|
831
|
+
if not identity.exists():
|
|
832
|
+
return False
|
|
833
|
+
|
|
834
|
+
text = identity.read_text(encoding="utf-8", errors="replace")
|
|
835
|
+
if "[Your name]" in text:
|
|
836
|
+
return False
|
|
837
|
+
|
|
838
|
+
if not threads_dir.is_dir():
|
|
839
|
+
return True
|
|
840
|
+
|
|
841
|
+
for entry in threads_dir.iterdir():
|
|
842
|
+
if entry.name != ".gitkeep":
|
|
843
|
+
return True
|
|
844
|
+
return False
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def print_report(results, dry_run=False):
|
|
848
|
+
"""Render a concise deployment summary."""
|
|
849
|
+
if sys.stdout.encoding != "utf-8":
|
|
850
|
+
sys.stdout = io.TextIOWrapper(
|
|
851
|
+
sys.stdout.buffer,
|
|
852
|
+
encoding="utf-8",
|
|
853
|
+
errors="replace",
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
mode = " (dry run)" if dry_run else ""
|
|
857
|
+
print(f"\n gator deploy{mode}\n")
|
|
858
|
+
|
|
859
|
+
if results["nuked"]:
|
|
860
|
+
print(f" Cleaned ({len(results['nuked'])}):")
|
|
861
|
+
for path in results["nuked"]:
|
|
862
|
+
print(f" x {path}")
|
|
863
|
+
print()
|
|
864
|
+
|
|
865
|
+
print(f" Created ({len(results['created'])}):")
|
|
866
|
+
for path in results["created"]:
|
|
867
|
+
print(f" + {path}")
|
|
868
|
+
print()
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def main():
|
|
872
|
+
parser = argparse.ArgumentParser(
|
|
873
|
+
description="Deploy Gator 1.0 as a blank command post."
|
|
874
|
+
)
|
|
875
|
+
parser.add_argument("target", help="Path to the public gator repo")
|
|
876
|
+
parser.add_argument("--dry-run", "-n", action="store_true")
|
|
877
|
+
parser.add_argument(
|
|
878
|
+
"--force",
|
|
879
|
+
action="store_true",
|
|
880
|
+
help="Allow deploying into a target that appears to contain real command-post content.",
|
|
881
|
+
)
|
|
882
|
+
args = parser.parse_args()
|
|
883
|
+
|
|
884
|
+
source = find_command_post()
|
|
885
|
+
if not source:
|
|
886
|
+
print(" Error: not in a gator-command repo.", file=sys.stderr)
|
|
887
|
+
sys.exit(1)
|
|
888
|
+
|
|
889
|
+
target = Path(args.target).resolve()
|
|
890
|
+
if not target.is_dir():
|
|
891
|
+
print(f" Error: target does not exist: {target}", file=sys.stderr)
|
|
892
|
+
sys.exit(1)
|
|
893
|
+
|
|
894
|
+
if target_looks_initialized(target) and not args.force:
|
|
895
|
+
print(" Error: target appears to be an active command post with real content.", file=sys.stderr)
|
|
896
|
+
print(" Use --force to override.", file=sys.stderr)
|
|
897
|
+
sys.exit(1)
|
|
898
|
+
|
|
899
|
+
print(f" Source: {source}")
|
|
900
|
+
print(f" Target: {target}")
|
|
901
|
+
|
|
902
|
+
results = deploy(source, target, dry_run=args.dry_run)
|
|
903
|
+
print_report(results, dry_run=args.dry_run)
|
|
904
|
+
|
|
905
|
+
if not args.dry_run:
|
|
906
|
+
print(" Structure:")
|
|
907
|
+
print(" gator-engine/ <- product code (refreshed each deploy)")
|
|
908
|
+
print(" gator-command/ <- your knowledge layer (preserved, stubs only if missing)")
|
|
909
|
+
print(" .gator/ <- self-governance (scripts refreshed, content preserved)")
|
|
910
|
+
print()
|
|
911
|
+
print(" Next: cd target && git add -A && git commit")
|
|
912
|
+
print()
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
if __name__ == "__main__":
|
|
916
|
+
main()
|