diff --git a/assets/templates/process/policies.yml b/assets/templates/process/policies.yml new file mode 100644 index 0000000..8245003 --- /dev/null +++ b/assets/templates/process/policies.yml @@ -0,0 +1,25 @@ + version: 1 + voting: + values: [READY, CHANGES, REJECT] + allow_agent_votes: true + quorum: + discussion: { ready: all, reject: all } + design: { ready: all, reject: all } + implementation: { ready: 1_human, reject: all } + testing: { ready: all, reject: all } + review: { ready: 1_human, reject: all } + eligibility: + agents_allowed: true + require_human_for: [implementation, review] + etiquette: + name_prefix_agents: "AI_" + vote_line_regex: "^VOTE:\\s*(READY|CHANGES|REJECT)\\s*$" + response_timeout_hours: 24 + timeouts: + discussion_stale_days: 3 + nudge_interval_hours: 24 + promotion_timeout_days: 14 + security: + scanners: + enabled: true + tool: gitleaks diff --git a/assets/templates/rules/features.ai-rules.yml b/assets/templates/rules/features.ai-rules.yml new file mode 100644 index 0000000..5b0e2f3 --- /dev/null +++ b/assets/templates/rules/features.ai-rules.yml @@ -0,0 +1,62 @@ +version: 1 + +file_associations: + "feature.discussion.md": "feature_discussion" + "feature.discussion.sum.md": "discussion_summary" + "design.discussion.md": "design_discussion" + "design.discussion.sum.md": "discussion_summary" + "implementation.discussion.md": "impl_discussion" + "implementation.discussion.sum.md":"discussion_summary" + "testing.discussion.md": "test_discussion" + "testing.discussion.sum.md": "discussion_summary" + "review.discussion.md": "review_discussion" + "review.discussion.sum.md": "discussion_summary" + +rules: + feature_discussion: + outputs: + summary_companion: + path: "{dir}/discussions/feature.discussion.sum.md" + output_type: "discussion_summary_writer" + instruction: | + Keep bounded sections only: DECISIONS, OPEN_QUESTIONS, AWAITING, ACTION_ITEMS, VOTES, TIMELINE, LINKS. + + design_discussion: + outputs: + summary_companion: + path: "{dir}/discussions/design.discussion.sum.md" + output_type: "discussion_summary_writer" + instruction: | + Same policy as feature; include link to ../design/design.md if present. + + impl_discussion: + outputs: + summary_companion: + path: "{dir}/discussions/implementation.discussion.sum.md" + output_type: "discussion_summary_writer" + instruction: | + Same policy; include any unchecked tasks from ../implementation/tasks.md. + + test_discussion: + outputs: + summary_companion: + path: "{dir}/discussions/testing.discussion.sum.md" + output_type: "discussion_summary_writer" + instruction: | + Same policy; surface FAILS either in OPEN_QUESTIONS or AWAITING. + + review_discussion: + outputs: + summary_companion: + path: "{dir}/discussions/review.discussion.sum.md" + output_type: "discussion_summary_writer" + instruction: | + Same policy; record READY_FOR_RELEASE decision date if present. + + discussion_summary: + outputs: + normalize: + path: "{path}" + output_type: "discussion_summary_normalizer" + instruction: | + If missing, create summary with standard markers. Never edit outside markers. diff --git a/assets/templates/rules/root.ai-rules.yml b/assets/templates/rules/root.ai-rules.yml new file mode 100644 index 0000000..efbcb61 --- /dev/null +++ b/assets/templates/rules/root.ai-rules.yml @@ -0,0 +1,23 @@ +version: 1 + +# Root defaults all folders inherit unless a closer .ai-rules.yml overrides them. +file_associations: + "README.md": "readme" + "process/policies.yml": "policies" + +rules: + readme: + outputs: + normalize: + path: "{repo}/README.md" + output_type: "readme_normalizer" + instruction: | + Ensure basic sections exist: Overview, Install, Usage, License. Be idempotent. + + policies: + outputs: + validate: + path: "{dir}/policies.yml" + output_type: "policy_validator" + instruction: | + Validate YAML keys according to DESIGN.md Appendix A. Do not auto-edit. diff --git a/docs/DESIGN.md b/docs/DESIGN.md index f061242..9dd06b7 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -74,7 +74,6 @@ Human → Git Commit → Pre-commit Hook → AI Generator → Markdown Artifact │ ├─ agents.yml # Role → stages mapping │ └─ config.yml # Configuration (future) ├─ process/ # Process documentation & templates -│ ├─ design.md # This document │ ├─ policies.md # Human-friendly policy documentation │ ├─ policies.yml # Machine-readable policy configuration │ └─ templates/ @@ -146,7 +145,16 @@ CascadingDev/ ├─ src/cascadingdev/ # core logic & optional dev CLI ├─ assets/ # single source of truth for shipped files │ ├─ hooks/pre-commit -│ ├─ templates/{feature_request.md,discussion.md,design_doc.md} +│ ├─ templates/ +│ │ ├─ USER_GUIDE.md +│ │ ├─ design_doc.md +│ │ ├─ discussion.md +│ │ ├─ feature_request.md +│ │ ├─ process/ +│ │ │ └─ policies.yml +│ │ └─ rules/ +│ │ ├─ root.ai-rules.yml # this becomes ./.ai-rules.yml +│ │ └─ features.ai-rules.yml # this becomes Docs/features/.ai-rules.yml │ └─ runtime/{ramble.py,create_feature.py} ├─ tools/build_installer.py # creates install/cascadingdev-/ ├─ install/ # build output (git-ignored) diff --git a/src/cascadingdev/__init__.py b/src/cascadingdev/__init__.py index e69de29..0fc2657 100644 --- a/src/cascadingdev/__init__.py +++ b/src/cascadingdev/__init__.py @@ -0,0 +1,4 @@ +# src/cascadingdev/__init__.py +from .utils import read_version +__all__ = ["cli"] +__version__ = read_version() diff --git a/src/cascadingdev/cli.py b/src/cascadingdev/cli.py index e69de29..9eeed6e 100644 --- a/src/cascadingdev/cli.py +++ b/src/cascadingdev/cli.py @@ -0,0 +1,67 @@ +# src/cascadingdev/cli.py +import argparse, sys, shutil +from pathlib import Path +from . import __version__ +from .utils import ROOT, read_version, bump_version, run + +def main(): + ap = argparse.ArgumentParser(prog="cascadingdev", description="CascadingDev CLI") + ap.add_argument("--version", action="store_true", help="Show version and exit") + sub = ap.add_subparsers(dest="cmd") + + sub.add_parser("doctor", help="Check environment and templates") + sub.add_parser("smoke", help="Run smoke test") + p_build = sub.add_parser("build", help="Build installer bundle (no version bump)") + p_rel = sub.add_parser("release", help="Bump version and rebuild") + p_rel.add_argument("--kind", choices=["major","minor","patch"], default="patch") + p_pack = sub.add_parser("pack", help="Zip the current installer bundle") + p_pack.add_argument("--out", help="Output zip path (default: ./install/cascadingdev-.zip)") + + args = ap.parse_args() + if args.version: + print(__version__) + return 0 + + if args.cmd == "doctor": + # minimal checks + required = [ + ROOT / "assets" / "templates" / "USER_GUIDE.md", + ROOT / "assets" / "templates" / "rules" / "features.ai-rules.yml", + ROOT / "assets" / "hooks" / "pre-commit", + ROOT / "src" / "cascadingdev" / "setup_project.py", + ] + missing = [str(p) for p in required if not p.exists()] + if missing: + print("Missing:\n " + "\n ".join(missing)); return 2 + print("Doctor OK."); return 0 + + if args.cmd == "smoke": + return run([sys.executable, str(ROOT / "tools" / "smoke_test.py")]) + + if args.cmd == "build": + return run([sys.executable, str(ROOT / "tools" / "build_installer.py")]) + + if args.cmd == "release": + newv = bump_version(args.kind, ROOT / "VERSION") + print(f"Bumped to {newv}") + rc = run([sys.executable, str(ROOT / "tools" / "build_installer.py")]) + if rc == 0: + print(f"Built installer for {newv}") + return rc + + if args.cmd == "pack": + ver = read_version(ROOT / "VERSION") + bundle = ROOT / "install" / f"cascadingdev-{ver}" + + if not bundle.exists(): + print(f"Bundle not found: {bundle}. Run `cascadingdev build` first.") + return 2 + out = Path(args.out) if args.out else (ROOT / "install" / f"cascadingdev-{ver}.zip") + if out.exists(): + out.unlink() + shutil.make_archive(out.with_suffix(""), "zip", root_dir=bundle) + print(f"Packed → {out}") + return 0 + + ap.print_help() + return 0 diff --git a/src/cascadingdev/setup_project.py b/src/cascadingdev/setup_project.py index 80746c2..20ccc3c 100644 --- a/src/cascadingdev/setup_project.py +++ b/src/cascadingdev/setup_project.py @@ -13,12 +13,12 @@ Examples: python setup_cascadingdev.py --target ~/dev/my-new-repo python setup_cascadingdev.py --target /abs/path --no-ramble """ -import sys import json -import shutil import argparse -import subprocess import datetime +import sys +import subprocess +import shutil from pathlib import Path # Bundle root (must contain assets/, ramble.py, VERSION) @@ -32,42 +32,42 @@ if not (INSTALL_ROOT / "assets").exists(): # ---------- Helper Functions ---------- -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 a message with immediate flush.""" +def say(msg: str) -> None: print(msg, flush=True) +def ensure_dir(p: Path) -> None: + p.mkdir(parents=True, exist_ok=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) +def write_if_missing(path: Path, content: str) -> None: + ensure_dir(path.parent) 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.""" +def copy_if_exists(src: Path, dst: Path) -> None: if src.exists(): - dst.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(str(src), str(dst)) + ensure_dir(dst.parent) + shutil.copy2(src, dst) +def copy_if_missing(src: Path, dst: Path) -> None: + ensure_dir(dst.parent) + if not dst.exists(): + shutil.copy2(src, dst) + +def run(cmd: list[str], cwd: Path | None = None) -> int: + proc = subprocess.Popen(cmd, cwd=cwd, stdout=sys.stdout, stderr=sys.stderr) + return proc.wait() 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)) + run(["git", "init", "-b", "main"], cwd=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" @@ -143,50 +143,31 @@ def run_ramble_and_collect(target: Path, provider: str = "mock", claude_cmd: str def seed_process_and_rules(target: Path): - """Create minimal, machine-readable rules that let the FIRST FEATURE define the project.""" + """Seed machine-readable policies and stage rules by copying installer templates.""" + # Seed process/policies.yml (machine-readable), per DESIGN.md Appendix A + process_dir = target / "process" + rules_dir = target / "Docs" / "features" + process_dir.mkdir(parents=True, exist_ok=True) + rules_dir.mkdir(parents=True, exist_ok=True) - # Create AI rules configuration for general markdown files - write_if_missing(target / ".ai-rules.yml", - """version: 1 -file_associations: - "*.md": "md-file" + # Locate templates in THIS installer bundle + t_root = INSTALL_ROOT / "assets" / "templates" + t_process = t_root / "process" / "policies.yml" + t_rules_root = t_root / "rules" / "root.ai-rules.yml" + t_rules_features = t_root / "rules" / "features.ai-rules.yml" -rules: - md-file: - description: "Normalize Markdown" - instruction: | - Keep markdown tidy (headings, lists, spacing). No content churn. -settings: - model: "local-mock" - temperature: 0.1 -""") + # Copy policies + if t_process.exists(): + copy_if_missing(t_process, process_dir / "policies.yml") - # Create AI rules specific to feature discussions - write_if_missing(target / "Docs" / "features" / ".ai-rules.yml", - """version: 1 -file_associations: - "feature.discussion.md": "feature_discussion" - "feature.discussion.sum.md": "discussion_summary" - -rules: - feature_discussion: - outputs: - summary_companion: - path: "{dir}/discussions/feature.discussion.sum.md" - output_type: "discussion_summary_writer" - instruction: | - Ensure the summary file exists and maintain only the bounded sections: - DECISIONS, OPEN_QUESTIONS, AWAITING, ACTION_ITEMS, VOTES, TIMELINE, LINKS. - - discussion_summary: - outputs: - normalize: - path: "{dir}/feature.discussion.sum.md" - output_type: "discussion_summary_normalizer" - instruction: | - If missing, create summary with standard markers. Do not edit text outside markers. -""") + # Copy rules files into expected locations + # Root rules (optional if you want a project-wide baseline) + if t_rules_root.exists(): + copy_if_missing(t_rules_root, target / ".ai-rules.yml") + # Discussion/feature rules (cascade/override within Docs/features) + if t_rules_features.exists(): + copy_if_missing(t_rules_features, rules_dir / ".ai-rules.yml") def seed_initial_feature(target: Path, req_fields: dict | None): """Create the initial feature request and associated discussion files.""" @@ -307,8 +288,8 @@ def copy_install_assets_to_target(target: Path): 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)) + run(["git", "add", "-A"], cwd=target) + run(["git", "commit", "-m", "chore: bootstrap Cascading Development scaffolding"], cwd=target) except Exception: # Silently continue if commit fails (e.g., no git config) pass diff --git a/src/cascadingdev/utils.py b/src/cascadingdev/utils.py index e69de29..99d3480 100644 --- a/src/cascadingdev/utils.py +++ b/src/cascadingdev/utils.py @@ -0,0 +1,62 @@ +from __future__ import annotations +from pathlib import Path +import shutil, subprocess, sys, re + +ROOT = Path(__file__).resolve().parents[2] # repo root + +def say(msg: str) -> None: + print(msg, flush=True) + +def ensure_dir(p: Path) -> None: + p.mkdir(parents=True, exist_ok=True) + +def write_if_missing(path: Path, content: str) -> None: + ensure_dir(path.parent) + if not path.exists(): + path.write_text(content, encoding="utf-8") + +def copy_if_exists(src: Path, dst: Path) -> None: + if src.exists(): + ensure_dir(dst.parent) + shutil.copy2(src, dst) +def copy_if_missing(src: Path, dst: Path) -> None: + dst.parent.mkdir(parents=True, exist_ok=True) + if not dst.exists(): + shutil.copy2(src, dst) +def run(cmd: list[str], cwd: Path | None = None) -> int: + proc = subprocess.Popen(cmd, cwd=cwd, stdout=sys.stdout, stderr=sys.stderr) + return proc.wait() + +def read_version(version_file: Path | None = None) -> str: + vf = version_file or (ROOT / "VERSION") + return (vf.read_text(encoding="utf-8").strip() if vf.exists() else "0.0.0") + +def write_version(new_version: str, version_file: Path | None = None) -> None: + vf = version_file or (ROOT / "VERSION") + vf.write_text(new_version.strip() + "\n", encoding="utf-8") + +_semver = re.compile(r"^(\d+)\.(\d+)\.(\d+)$") +def bump_version(kind: str = "patch", version_file: Path | None = None) -> str: + cur = read_version(version_file) + m = _semver.match(cur) or _semver.match("0.1.0") # default if missing + major, minor, patch = map(int, m.groups()) + if kind == "major": + major, minor, patch = major + 1, 0, 0 + elif kind == "minor": + minor, patch = minor + 1, 0 + else: + patch += 1 + new = f"{major}.{minor}.{patch}" + write_version(new, version_file) + return new + +def bundle_path(version_file: Path | None = None) -> Path: + """ + Return the install bundle path for the current VERSION (e.g., install/cascadingdev-0.1.2). + Raises FileNotFoundError if missing. + """ + ver = read_version(version_file) + bp = ROOT / "install" / f"cascadingdev-{ver}" + if not bp.exists(): + raise FileNotFoundError(f"Bundle not found: {bp}. Build it with `cascadingdev build`.") + return bp diff --git a/tools/build_installer.py b/tools/build_installer.py index c43d043..f4a8c58 100644 --- a/tools/build_installer.py +++ b/tools/build_installer.py @@ -8,149 +8,38 @@ VER = (ROOT / "VERSION").read_text().strip() if (ROOT / "VERSION").exists() els BUNDLE = OUT / f"cascadingdev-{VER}" def main(): + # Removes the old install bundle if it already exists if BUNDLE.exists(): shutil.rmtree(BUNDLE) - # copy essentials + # Create the directories (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") + # Copy the git hook and any other runtime utilities. shutil.copy2(ROOT / "assets" / "runtime" / "ramble.py", BUNDLE / "ramble.py") + shutil.copy2(ROOT / "assets" / "runtime" / "create_feature.py", BUNDLE / "create_feature.py") + shutil.copy2(ROOT / "assets" / "hooks" / "pre-commit", BUNDLE / "assets" / "hooks" / "pre-commit") - for t in ["feature_request.md","discussion.md","design_doc.md"]: + + # copy core templates + for t in ["feature_request.md","discussion.md","design_doc.md","USER_GUIDE.md"]: shutil.copy2(ROOT / "assets" / "templates" / t, BUNDLE / "assets" / "templates" / t) + # copy (recursively) the contents of process/ and rules/ templates folders + def copy_tree(src, dst): + if dst.exists(): + shutil.rmtree(dst) + shutil.copytree(src, dst) + copy_tree(ROOT / "assets" / "templates" / "process", BUNDLE / "assets" / "templates" / "process") + copy_tree(ROOT / "assets" / "templates" / "rules", BUNDLE / "assets" / "templates" / "rules") + # write installer entrypoint - (BUNDLE / "setup_cascadingdev.py").write_text(INSTALLER_PY, encoding="utf-8") + shutil.copy2(ROOT / "src" / "cascadingdev" / "setup_project.py", + BUNDLE / "setup_cascadingdev.py") + (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\n## Decisions (ADR-style)\n- (none yet)\n\n\n\n## Open Questions\n- (none yet)\n\n\n\n## Awaiting Replies\n- (none yet)\n\n\n\n## Action Items\n- (none yet)\n\n\n\n## Votes (latest per participant)\nREADY: 1 • CHANGES: 0 • REJECT: 0\n- Maintainer\n\n\n\n## Timeline (most recent first)\n- {today} Maintainer: Kickoff\n\n\n\n## Links\n- Design/Plan: ../design/design.md\n\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() diff --git a/tools/smoke_test.py b/tools/smoke_test.py index e69de29..cec35db 100644 --- a/tools/smoke_test.py +++ b/tools/smoke_test.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +from pathlib import Path + +def main(): + root = Path(__file__).resolve().parents[1] + required = [ + root / "assets" / "hooks" / "pre-commit", + root / "assets" / "templates" / "feature_request.md", + root / "assets" / "templates" / "discussion.md", + root / "assets" / "templates" / "design_doc.md", + root / "assets" / "templates" / "USER_GUIDE.md", # now required + root / "assets" / "runtime" / "ramble.py", + root / "tools" / "build_installer.py", + root / "src" / "cascadingdev" / "setup_project.py", + ] + missing = [str(p) for p in required if not p.exists()] + if missing: + print("Missing:", *missing, sep="\n ") + raise SystemExit(2) + print("Smoke OK.") +if __name__ == "__main__": + main()