CascadingDev_simplified/tools/build_installer.py

157 lines
7.3 KiB
Python

#!/usr/bin/env python3
import shutil, os
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "install"
VER = (ROOT / "VERSION").read_text().strip() if (ROOT / "VERSION").exists() else "0.1.0"
BUNDLE = OUT / f"cascadingdev-{VER}"
def main():
if BUNDLE.exists():
shutil.rmtree(BUNDLE)
# copy essentials
(BUNDLE / "assets" / "hooks").mkdir(parents=True, exist_ok=True)
(BUNDLE / "assets" / "templates").mkdir(parents=True, exist_ok=True)
shutil.copy2(ROOT / "DESIGN.md", BUNDLE / "DESIGN.md")
shutil.copy2(ROOT / "assets" / "runtime" / "ramble.py", BUNDLE / "ramble.py")
shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit")
for t in ["feature_request.md","discussion.md","design_doc.md"]:
shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t)
# write installer entrypoint
(BUNDLE / "setup_cascadingdev.py").write_text(INSTALLER_PY, encoding="utf-8")
(BUNDLE / "INSTALL.md").write_text("Unzip, then run:\n\n python3 setup_cascadingdev.py\n", encoding="utf-8")
(BUNDLE / "VERSION").write_text(VER, encoding="utf-8")
print(f"[✓] Built installer → {BUNDLE}")
INSTALLER_PY = r'''#!/usr/bin/env python3
import argparse, json, os, shutil, subprocess, sys, datetime
from pathlib import Path
HERE = Path(__file__).resolve().parent
def sh(cmd, cwd=None):
return subprocess.run(cmd, check=True, text=True, capture_output=True, cwd=cwd)
def say(x): print(x, flush=True)
def write_if_missing(p: Path, content: str):
p.parent.mkdir(parents=True, exist_ok=True)
if not p.exists():
p.write_text(content, encoding="utf-8")
def copytree(src: Path, dst: Path):
dst.parent.mkdir(parents=True, exist_ok=True)
if src.is_file():
shutil.copy2(src, dst)
else:
shutil.copytree(src, dst, dirs_exist_ok=True)
def ensure_git_repo(target: Path):
if not (target / ".git").exists():
sh(["git", "init", "-b", "main"], cwd=target)
write_if_missing(target / ".gitignore", ".env\n.env.*\nsecrets/\n__pycache__/\n*.pyc\n.pytest_cache/\n.DS_Store\n")
def install_hook(target: Path):
hooks = target / ".git" / "hooks"; hooks.mkdir(parents=True, exist_ok=True)
src = HERE / "assets" / "hooks" / "pre-commit"
dst = hooks / "pre-commit"
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
dst.chmod(0o755)
def run_ramble(target: Path, provider="mock"):
ramble = target / "ramble.py"
if not ramble.exists(): return None
args = [sys.executable, str(ramble),
"--provider", provider,
"--prompt", "Describe your initial feature request for this repository",
"--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview",
"--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}']
say("[•] Launching Ramble…")
p = subprocess.run(args, text=True, capture_output=True, cwd=target)
try:
return json.loads((p.stdout or "").strip())
except Exception:
say("[-] Could not parse Ramble output; using template defaults.")
return None
def seed_rules_and_templates(target: Path):
write_if_missing(target / ".ai-rules.yml",
"version: 1\nfile_associations:\n \"*.md\": \"md-file\"\n\nrules:\n md-file:\n description: \"Normalize Markdown\"\n instruction: |\n Keep markdown tidy (headings, lists, spacing). No content churn.\nsettings:\n model: \"local-mock\"\n temperature: 0.1\n")
# copy templates
copytree(HERE / "assets" / "templates", target / "process" / "templates")
def seed_first_feature(target: Path, req):
today = datetime.date.today().isoformat()
fr = target / "Docs" / "features" / f"FR_{today}_initial-feature-request"
disc = fr / "discussions"; disc.mkdir(parents=True, exist_ok=True)
if req:
fields = req.get("fields", {}) or {}
title = (fields.get("Title") or "initialProjectDesign").strip()
intent = (fields.get("Intent") or "").strip()
problem = (fields.get("ProblemItSolves") or "").strip()
brief = (fields.get("BriefOverview") or "").strip()
summary = (req.get("summary") or "").strip()
body = f"# Feature Request: {title}\n\n**Intent**: {intent}\n**Motivation / Problem**: {problem}\n**Brief Overview**: {brief}\n\n**Summary**: {summary}\n**Meta**: Created: {today}\n"
else:
body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8")
(fr / "request.md").write_text(body, encoding="utf-8")
(disc / "feature.discussion.md").write_text(
f"---\ntype: discussion\nstage: feature\nstatus: OPEN\nfeature_id: FR_{today}_initial-feature-request\ncreated: {today}\n---\n## Summary\nKickoff discussion. Append comments below.\n\n## Participation\n- Maintainer: Kickoff. VOTE: READY\n", encoding="utf-8")
(disc / "feature.discussion.sum.md").write_text(
"# Summary — Feature\n\n<!-- SUMMARY:DECISIONS START -->\n## Decisions (ADR-style)\n- (none yet)\n<!-- SUMMARY:DECISIONS END -->\n\n<!-- SUMMARY:OPEN_QUESTIONS START -->\n## Open Questions\n- (none yet)\n<!-- SUMMARY:OPEN_QUESTIONS END -->\n\n<!-- SUMMARY:AWAITING START -->\n## Awaiting Replies\n- (none yet)\n<!-- SUMMARY:AWAITING END -->\n\n<!-- SUMMARY:ACTION_ITEMS START -->\n## Action Items\n- (none yet)\n<!-- SUMMARY:ACTION_ITEMS END -->\n\n<!-- SUMMARY:VOTES START -->\n## Votes (latest per participant)\nREADY: 1 • CHANGES: 0 • REJECT: 0\n- Maintainer\n<!-- SUMMARY:VOTES END -->\n\n<!-- SUMMARY:TIMELINE START -->\n## Timeline (most recent first)\n- {today} Maintainer: Kickoff\n<!-- SUMMARY:TIMELINE END -->\n\n<!-- SUMMARY:LINKS START -->\n## Links\n- Design/Plan: ../design/design.md\n<!-- SUMMARY:LINKS END -->\n".replace("{today}", today), encoding="utf-8")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--target", help="Destination path for the user's project")
ap.add_argument("--no-ramble", action="store_true")
ap.add_argument("--provider", default="mock")
args = ap.parse_args()
target = Path(args.target or input("User's project folder: ").strip()).expanduser().resolve()
target.mkdir(parents=True, exist_ok=True)
say(f"[=] Installing into: {target}")
# copy top-level assets
shutil.copy2(HERE / "DESIGN.md", target / "DESIGN.md")
shutil.copy2(HERE / "ramble.py", target / "ramble.py")
# basic tree
for p in [target / "Docs" / "features",
target / "Docs" / "discussions" / "reviews",
target / "Docs" / "diagrams" / "file_diagrams",
target / "scripts" / "hooks",
target / "src", target / "tests", target / "process"]:
p.mkdir(parents=True, exist_ok=True)
# rules / templates
seed_rules_and_templates(target)
# git + hook
ensure_git_repo(target)
install_hook(target)
# ramble
req = None if args.no_ramble else run_ramble(target, provider=args.provider)
# seed FR
seed_first_feature(target, req)
try:
sh(["git", "add", "-A"], cwd=target)
sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target)
except Exception:
pass
say("[✓] Done. Next:\n cd " + str(target) + "\n git status")
if __name__ == "__main__":
main()
'''
if __name__ == "__main__":
main()