From 67a44156004ddf7160159c00d6ceef22f2908964 Mon Sep 17 00:00:00 2001 From: rob Date: Mon, 27 Oct 2025 20:17:35 -0300 Subject: [PATCH] 1st commit --- .gitignore | 1 + assets/templates/USER_GUIDE.md | 23 +++++++++++ docs/DESIGN.md | 7 +++- docs/INSTALL.md | 28 +++++++++++++ src/cascadingdev/setup_project.py | 66 +++++++++++++++++++++++++------ 5 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 assets/templates/USER_GUIDE.md create mode 100644 docs/INSTALL.md diff --git a/.gitignore b/.gitignore index c94d1eb..72feffd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .idea/ __pycache__/ *.pyc +install/ diff --git a/assets/templates/USER_GUIDE.md b/assets/templates/USER_GUIDE.md new file mode 100644 index 0000000..30e3b72 --- /dev/null +++ b/assets/templates/USER_GUIDE.md @@ -0,0 +1,23 @@ +# Project Guide (CascadingDev) + +## Daily flow +1. Work under `Docs/features/FR_*/...` +2. Commit; the pre-commit hook ensures `*.discussion.sum.md` and (later) updates summaries/diagrams. +3. Use `Docs/features/.../discussions/*.discussion.md` for stage talks. + End each comment with `VOTE: READY|CHANGES|REJECT`. + +## Start a new feature +- Create `Docs/features/FR_YYYY-MM-DD_/request.md` from the template. +- Commit; the system seeds stage discussions and summaries automatically. + +## First run +- After installation, make an initial commit to activate the hook: + ```bash + git add . + git commit -m "chore: initial commit" + ``` + +## Notes +- Keep discussions append-only. +- Human READY is required at Implementation/Release. +- Ramble GUI (`ramble.py`) is optional; use it to seed the first feature. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 9d7b1a0..2d19be9 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -148,7 +148,12 @@ CascadingDev/ │ └─ runtime/ramble.py ├─ tools/build_installer.py # creates install/cascadingdev-/ ├─ install/ # build output (git-ignored) -│ └─ cascadingdev-/ # unzip + run bundle (setup_cascadingdev.py inside) +│ └─ cascadingdev-/ # self-contained installer bundle +│ ├─ setup_cascadingdev.py +│ ├─ assets/ # shipped templates + hooks +│ ├─ ramble.py +│ ├─ INSTALL.md # how to run the installer +│ └─ VERSION ├─ docs │ └─ DESIGN.md ├─ VERSION # semantic version of CascadingDev diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..b2b392d --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,28 @@ +# CascadingDev Installer + +## Requirements +- Python 3.10+ and git +- (Optional) PySide6 for GUI (`pip install PySide6`) + +## Quick start +```bash + +python setup_cascadingdev.py --target /path/to/new-project +``` + +### Skip GUI +```bash +python setup_cascadingdev.py --target /path/to/new-project --no-ramble +``` + +> After installation, open `USER_GUIDE.md` in your new project for daily usage. + +## Rebuild & Run (for maintainers) +Rebuild the bundle every time you change assets/ or the installer: +```bash +python tools/build_installer.py +``` +Then run only the bundled copy: +```bash + python install/cascadingdev-*/setup_cascadingdev.py --target /path/to/new-project +``` \ No newline at end of file diff --git a/src/cascadingdev/setup_project.py b/src/cascadingdev/setup_project.py index cf2770d..d95b4c4 100644 --- a/src/cascadingdev/setup_project.py +++ b/src/cascadingdev/setup_project.py @@ -22,50 +22,69 @@ import subprocess import datetime from pathlib import Path -INSTALL_ROOT = Path(__file__).resolve().parent.parent # installer root (contains this scripts/ dir) +# The root directory of the installer package (contains scripts/, assets/, etc.) +INSTALL_ROOT = Path(__file__).resolve().parent.parent + +# ---------- Helper Functions ---------- -# ---------- helpers ---------- def sh(cmd, check=True, cwd=None): + """Run a shell command and return the completed process.""" return subprocess.run(cmd, check=check, text=True, capture_output=True, cwd=cwd) -def say(msg): print(msg, flush=True) +def say(msg): + """Print a message with immediate flush.""" + print(msg, flush=True) def write_if_missing(path: Path, content: str): + """Write content to a file only if it doesn't already exist.""" path.parent.mkdir(parents=True, exist_ok=True) if not path.exists(): path.write_text(content, encoding="utf-8") def copy_if_exists(src: Path, dst: Path): + """Copy a file from source to destination if the source exists.""" if src.exists(): dst.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(str(src), str(dst)) def ensure_git_repo(target: Path): + """Initialize a git repository if one doesn't exist at the target path.""" if not (target / ".git").exists(): + # Initialize git repo with main branch sh(["git", "init", "-b", "main"], cwd=str(target)) + # Create basic .gitignore file write_if_missing(target / ".gitignore", "\n".join([ ".env", ".env.*", "secrets/", ".git/ai-rules-*", "__pycache__/", "*.pyc", ".pytest_cache/", ".DS_Store", ]) + "\n") def install_precommit_hook(target: Path): + """Install the pre-commit hook from installer assets to target git hooks.""" hook_src = INSTALL_ROOT / "assets" / "hooks" / "pre-commit" hooks_dir = target / ".git" / "hooks" hooks_dir.mkdir(parents=True, exist_ok=True) hook_dst = hooks_dir / "pre-commit" + if not hook_src.exists(): say("[-] pre-commit hook source missing at scripts/hooks/pre-commit in the installer.") return + + # Copy hook content and make it executable hook_dst.write_text(hook_src.read_text(encoding="utf-8"), encoding="utf-8") hook_dst.chmod(0o755) say(f"[+] Installed git hook → {hook_dst}") def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str = "claude"): + """ + Launch Ramble GUI to collect initial feature request details. + Falls back to terminal prompts if GUI fails or returns invalid JSON. + """ ramble = target / "ramble.py" if not ramble.exists(): say("[-] ramble.py not found in target; skipping interactive FR capture.") return None + # Build Ramble command arguments args = [ sys.executable, str(ramble), "--provider", provider, @@ -74,13 +93,16 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str "--fields", "Summary", "Title", "Intent", "ProblemItSolves", "BriefOverview", "--criteria", '{"Summary":"<= 2 sentences","Title":"camelCase, <= 24 chars"}' ] + say("[•] Launching Ramble (close the dialog with Submit to return JSON)…") proc = subprocess.run(args, text=True, capture_output=True, cwd=str(target)) + # Show any stderr output from Ramble if proc.stderr and proc.stderr.strip(): say("[Ramble stderr]") say(proc.stderr.strip()) + # Try to parse JSON output from Ramble out = (proc.stdout or "").strip() if out: try: @@ -88,7 +110,7 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str except Exception as e: say(f"[-] JSON parse failed: {e}") - # Terminal fallback so setup can proceed without GUI deps + # Terminal fallback - collect input manually if GUI fails say("[!] Falling back to terminal prompts.") def ask(label, default=""): try: @@ -97,6 +119,7 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str except EOFError: return default + # Collect required fields via terminal input fields = { "Title": ask("Title (camelCase, <=24 chars)", "initialProjectDesign"), "Intent": ask("Intent", "—"), @@ -107,10 +130,15 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str return {"fields": fields, "summary": fields.get("Summary", "")} def seed_process_and_rules(target: Path): + """Create the standard folder structure, templates, and configuration files.""" + + # Create basic process documentation files write_if_missing(target / "process" / "design.md", "# Process & Architecture (Local Notes)\n\n(See DESIGN.md for full spec.)\n") write_if_missing(target / "process" / "policies.md", "# Policies (Human-readable)\n\nSee machine-readable config in policies.yml.\n") + + # Create machine-readable policies configuration write_if_missing(target / "process" / "policies.yml", """version: 1 voting: @@ -129,6 +157,7 @@ etiquette: response_timeout_hours: 24 """) + # Create template files for different document types tmpl_dir = target / "process" / "templates" write_if_missing(tmpl_dir / "feature_request.md", "# Feature Request: \n\n**Intent**: …\n**Motivation / Problem**: …\n**Constraints / Non-Goals**: …\n**Rough Proposal**: …\n**Open Questions**: …\n") @@ -137,6 +166,7 @@ etiquette: write_if_missing(tmpl_dir / "design_doc.md", "# Design — <FR id / Title>\n\n## Context & Goals\n## Non-Goals & Constraints\n## Options Considered\n## Decision & Rationale\n## Architecture Diagram(s)\n## Risks & Mitigations\n## Acceptance Criteria\n") + # Create AI rules configuration for general markdown files write_if_missing(target / ".ai-rules.yml", """version: 1 file_associations: @@ -152,6 +182,7 @@ settings: temperature: 0.1 """) + # Create AI rules specific to feature discussions write_if_missing(target / "Docs" / "features" / ".ai-rules.yml", """version: 1 file_associations: @@ -178,11 +209,13 @@ rules: """) def seed_initial_feature(target: Path, req_fields: dict | None): + """Create the initial feature request and associated discussion files.""" today = datetime.date.today().isoformat() fr_dir = target / "Docs" / "features" / f"FR_{today}_initial-feature-request" disc_dir = fr_dir / "discussions" disc_dir.mkdir(parents=True, exist_ok=True) + # Create feature request content, using Ramble data if available if req_fields: title = (req_fields.get("fields", {}) or {}).get("Title", "").strip() or "initialProjectDesign" intent = (req_fields.get("fields", {}) or {}).get("Intent", "").strip() or "—" @@ -199,10 +232,12 @@ def seed_initial_feature(target: Path, req_fields: dict | None): **Meta**: Created: {today} """ else: + # Fallback to template content if no Ramble data body = (target / "process" / "templates" / "feature_request.md").read_text(encoding="utf-8") (fr_dir / "request.md").write_text(body, encoding="utf-8") + # Create initial discussion file (disc_dir / "feature.discussion.md").write_text( f"""--- type: discussion @@ -218,6 +253,7 @@ Initial discussion for the first feature request. Append your comments below. - Maintainer: Kickoff. VOTE: READY """, encoding="utf-8") + # Create companion summary file with structured sections (disc_dir / "feature.discussion.sum.md").write_text( """# Summary — Feature @@ -259,6 +295,7 @@ READY: 1 • CHANGES: 0 • REJECT: 0 """.replace("{ts}", today), encoding="utf-8") def copy_install_assets_to_target(target: Path): + """Copy essential files from the installer to the target repository.""" # Copy DESIGN.md and ramble.py from installer if present copy_if_exists(INSTALL_ROOT / "DESIGN.md", target / "DESIGN.md") copy_if_exists(INSTALL_ROOT / "ramble.py", target / "ramble.py") @@ -278,13 +315,17 @@ def copy_install_assets_to_target(target: Path): shutil.copytree(INSTALL_ROOT / "automation", target / "automation", dirs_exist_ok=True) def first_commit(target: Path): + """Perform the initial git commit of all scaffolded files.""" try: sh(["git", "add", "-A"], cwd=str(target)) sh(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=str(target)) except Exception: + # Silently continue if commit fails (e.g., no git config) pass def main(): + """Main entry point for the Cascading Development setup script.""" + # Parse command line arguments ap = argparse.ArgumentParser() ap.add_argument("--target", help="Destination path to create/use the repo") ap.add_argument("--provider", choices=["mock", "claude"], default="mock", help="Ramble provider (default: mock)") @@ -292,6 +333,7 @@ def main(): ap.add_argument("--claude-cmd", default="claude") args = ap.parse_args() + # Get target directory from args or prompt user target_str = args.target if not target_str: target_str = input("Destination repo path (will be created if missing): ").strip() @@ -299,15 +341,16 @@ def main(): say("No target specified. Aborting.") sys.exit(2) + # Resolve and create target directory target = Path(target_str).expanduser().resolve() target.mkdir(parents=True, exist_ok=True) say(f"[=] Installing Cascading Development into: {target}") - # Copy assets from installer into target + # Step 1: Copy assets from installer into target copy_install_assets_to_target(target) - # Ensure folder layout + # Step 2: Create standard folder structure for p in [ target / "Docs" / "features", target / "Docs" / "discussions" / "reviews", @@ -319,24 +362,25 @@ def main(): ]: p.mkdir(parents=True, exist_ok=True) - # Create rules/templates and basic process docs + # Step 3: Create rules/templates and basic process docs seed_process_and_rules(target) - # Initialize git & install pre-commit + # Step 4: Initialize git & install pre-commit ensure_git_repo(target) install_precommit_hook(target) - # Launch Ramble (if available) + # Step 5: Launch Ramble (if available and not disabled) req = None if not args.no_ramble: req = run_ramble_and_collect(target, provider=args.provider, claude_cmd=args.claude_cmd) - # Seed first feature based on Ramble output + # Step 6: Seed first feature based on Ramble output seed_initial_feature(target, req) - # First commit + # Step 7: Perform initial commit first_commit(target) + # Completion message say("[✓] Setup complete.") say(f"Next steps:\n cd {target}\n git status")