diff --git a/.gitignore b/.gitignore index e4ffe3a..d6d13b0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ node_modules/ *.tmp *.swp .codex/ - +__pycache__/ +*.pyc diff --git a/AGENTS.md b/AGENTS.md index 4f0729d..bbf59f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,12 +8,14 @@ Treat the root files as the canonical project guidance: - `Intent.md` is the behavior spec for the skill. - `README.md` explains the repo layout at a high level. - `cspell.json` defines the spellcheck vocabulary used by CI. +- `copy-script/README.md` documents the installer helper. ## Working Rules - Read `Intent.md` and `README.md` before making repo-wide changes. - Keep the three package folders aligned when a shared description changes. - Keep prose concise and consistent across `codex/`, `claude/`, and `openclaw/`. +- Keep the copy script aligned with the package folder names if those change. - When adding new markdown wording or project terms, update `cspell.json` if CI should accept them. - Preserve intentional misspellings only when they are part of a test or a deliberate example. - Prefer `apply_patch` for edits and keep changes minimal. @@ -22,4 +24,3 @@ Treat the root files as the canonical project guidance: - Markdown files are checked in CI with `cspell`. - Make sure new or edited `.md` files pass spellcheck before you consider the work done. - diff --git a/README.md b/README.md index 14a5700..51d60d2 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This repository packages the same `death-is-soon-wrap-up` concept for three diff - `codex/death-is-soon-wrap-up`: Codex-formatted package - `claude/death-is-soon-wrap-up`: Claude Code-formatted package - `openclaw/death-is-soon-wrap-up`: OpenClaw-formatted package +- `copy-script/`: Python installer and its README ## What It Does @@ -24,12 +25,12 @@ It is designed to: - `Intent.md` is the canonical brief for the skill - Each package folder contains a `SKILL.md` plus a short README for that ecosystem +- `copy-script/README.md` explains the installer and its options ## Installation -Copy the package folders into the matching skills directories under your home folder. +Run the Python installer in `copy-script/` to copy the package folders into the matching skills directories under your home folder. ## License This repository is licensed under the MIT License. See [LICENSE](LICENSE). - diff --git a/copy-script/README.md b/copy-script/README.md new file mode 100644 index 0000000..b34946b --- /dev/null +++ b/copy-script/README.md @@ -0,0 +1,44 @@ +# Copy Script + +This folder contains the Python installer for the `death-is-soon-wrap-up` skill packages. + +## What It Does + +The script copies each ecosystem package into the matching skills directory in your home folder: + +- `codex/death-is-soon-wrap-up` -> `~/.codex/skills/death-is-soon-wrap-up` +- `claude/death-is-soon-wrap-up` -> `~/.claude/skills/death-is-soon-wrap-up` +- `openclaw/death-is-soon-wrap-up` -> `~/.openclaw/skills/death-is-soon-wrap-up` + +## Interactive Mode + +Run the script with no flags in a terminal to pick ecosystems interactively. + +It will: + +- show the available ecosystems, +- let you choose one or more, +- and ask before overwriting existing installs. + +## CLI Mode + +Use flags when you want a non-interactive run. + +Examples: + +```bash +python copy-script/install-skills.py --all +python copy-script/install-skills.py --ecosystems codex openclaw +python copy-script/install-skills.py --all --dry-run +python copy-script/install-skills.py --all --force +``` + +## Options + +- `--all`: install into every supported ecosystem +- `--ecosystems`: install into selected ecosystems only +- `--repo-root`: point the script at a different repository root +- `--home`: point the script at a different home directory +- `--dry-run`: print actions without copying files +- `--force`: skip overwrite confirmation + diff --git a/copy-script/install-skills.py b/copy-script/install-skills.py new file mode 100644 index 0000000..b2b41d2 --- /dev/null +++ b/copy-script/install-skills.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import argparse +import shutil +import sys +from pathlib import Path + + +ECOSYSTEMS = { + "codex": ("codex", ".codex"), + "claude": ("claude", ".claude"), + "openclaw": ("openclaw", ".openclaw"), +} + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Copy the death-is-soon-wrap-up skill packages into local skills directories." + ) + parser.add_argument( + "--ecosystems", + nargs="+", + choices=sorted(ECOSYSTEMS), + help="One or more ecosystems to install into. Omit for interactive mode.", + ) + parser.add_argument( + "--all", + action="store_true", + help="Install into all supported ecosystems.", + ) + parser.add_argument( + "--repo-root", + type=Path, + default=Path(__file__).resolve().parent.parent, + help="Path to the repository root. Defaults to the parent of copy-script/.", + ) + parser.add_argument( + "--home", + type=Path, + default=Path.home(), + help="Home directory that contains the target skills folders.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be copied without writing anything.", + ) + parser.add_argument( + "--force", + action="store_true", + help="Overwrite existing installations without prompting.", + ) + return parser + + +def prompt_ecosystems() -> list[str]: + options = list(ECOSYSTEMS) + print("Select ecosystems to install:") + for index, name in enumerate(options, start=1): + print(f" {index}. {name}") + print(" a. all") + + choice = input("Enter numbers or names separated by commas [a]: ").strip().lower() + if not choice or choice == "a" or choice == "all": + return options + + selected: list[str] = [] + for raw_item in choice.split(","): + item = raw_item.strip() + if not item: + continue + if item.isdigit(): + index = int(item) - 1 + if index < 0 or index >= len(options): + raise SystemExit(f"Invalid ecosystem number: {item}") + selected.append(options[index]) + continue + if item not in ECOSYSTEMS: + raise SystemExit(f"Unknown ecosystem: {item}") + selected.append(item) + + if not selected: + raise SystemExit("No ecosystems selected.") + + return list(dict.fromkeys(selected)) + + +def prompt_confirmation(message: str) -> bool: + choice = input(f"{message} [y/N]: ").strip().lower() + return choice in {"y", "yes"} + + +def resolve_targets(args: argparse.Namespace) -> list[str]: + if args.all: + return list(ECOSYSTEMS) + + if args.ecosystems: + return list(dict.fromkeys(args.ecosystems)) + + if sys.stdin.isatty(): + return prompt_ecosystems() + + raise SystemExit("Specify --all or --ecosystems when running non-interactively.") + + +def install_skill(source: Path, destination_root: Path, dry_run: bool) -> Path: + destination = destination_root / source.name + if dry_run: + print(f"Would copy {source} -> {destination}") + return destination + + destination_root.mkdir(parents=True, exist_ok=True) + + if destination.exists(): + if destination.is_dir(): + shutil.rmtree(destination) + else: + destination.unlink() + + shutil.copytree(source, destination) + print(f"Copied {source} -> {destination}") + return destination + + +def main() -> None: + parser = build_parser() + args = parser.parse_args() + + repo_root = args.repo_root.resolve() + home = args.home.expanduser().resolve() + selected = resolve_targets(args) + + jobs = [] + for ecosystem in selected: + source_dirname, destination_dirname = ECOSYSTEMS[ecosystem] + source = repo_root / source_dirname / "death-is-soon-wrap-up" + destination_root = home / destination_dirname / "skills" + jobs.append((ecosystem, source, destination_root)) + + if not args.force and not args.dry_run: + destinations = [destination_root / "death-is-soon-wrap-up" for _, _, destination_root in jobs] + existing = [path for path in destinations if path.exists()] + if existing: + print("Existing installations found:") + for path in existing: + print(f" {path}") + if not sys.stdin.isatty() or not prompt_confirmation("Overwrite them"): + raise SystemExit("Cancelled.") + + for _, source, destination_root in jobs: + install_skill(source, destination_root, args.dry_run) + + +if __name__ == "__main__": + main() + diff --git a/cspell.json b/cspell.json index 2e2dab4..10f54d8 100644 --- a/cspell.json +++ b/cspell.json @@ -7,6 +7,7 @@ "Codex", "Gitea", "LICENSE", + "copy-script", "OpenClaw", "README", "apply_patch", @@ -16,6 +17,7 @@ "goodbye", "hyphen-case", "memory", + "pycache", "reset", "subagents", "wrap-up" @@ -24,4 +26,3 @@ "LICENSE" ] } -