from __future__ import annotations
import argparse
import fnmatch
import json
import shutil
import subprocess
import sys
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[1]
MANIFEST_PATH = ROOT_DIR / "skill" / "slidea" / "manifest.json"
def _load_manifest(manifest_path: Path | None = None) -> dict:
path = manifest_path or MANIFEST_PATH
if not path.exists():
raise FileNotFoundError(f"missing skill manifest: {path}")
return json.loads(path.read_text(encoding="utf-8"))
def _matches_any_pattern(path: Path, patterns: list[str]) -> bool:
normalized_path = path.as_posix()
return any(
fnmatch.fnmatch(normalized_path, pattern) or fnmatch.fnmatch(path.name, pattern)
for pattern in patterns
)
def _ignore_for_directory(source_dir: Path, exclude_patterns: list[str]):
def _ignore(_current_dir: str, names: list[str]) -> set[str]:
ignored: set[str] = set()
current_path = Path(_current_dir)
for name in names:
candidate = current_path / name
relative_candidate = candidate.relative_to(ROOT_DIR)
if _matches_any_pattern(relative_candidate, exclude_patterns):
ignored.add(name)
return ignored
return _ignore
def _copy_entry(source: Path, destination: Path, exclude_patterns: list[str]) -> None:
if not source.exists():
raise FileNotFoundError(f"missing skill-package source path: {source}")
if source.is_dir():
shutil.copytree(
source,
destination,
dirs_exist_ok=True,
ignore=_ignore_for_directory(source, exclude_patterns) if exclude_patterns else None,
)
return
if _matches_any_pattern(source.relative_to(ROOT_DIR), exclude_patterns):
return
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination)
def export_skill(
target_dir: Path, *, force: bool = False, update: bool = False,
manifest_path: Path | None = None
) -> Path:
manifest = _load_manifest(manifest_path)
exclude_patterns = manifest.get("exclude", [])
include_entries = manifest.get("include", [])
if not include_entries:
raise ValueError(f"skill manifest has no include entries: {manifest_path or MANIFEST_PATH}")
if target_dir.exists() and not update:
if not force:
raise FileExistsError(
f"target directory already exists: {target_dir}. Pass --force to replace it or --update to overwrite."
)
shutil.rmtree(target_dir)
target_dir.mkdir(parents=True, exist_ok=True)
for entry in include_entries:
source = ROOT_DIR / entry["from"]
destination = target_dir / entry["to"]
_copy_entry(source, destination, exclude_patterns)
return target_dir
def bootstrap_skill(target_dir: Path) -> None:
install_script = target_dir / "scripts" / "install" / "install.py"
if not install_script.exists():
raise FileNotFoundError(f"missing installer in exported skill package: {install_script}")
subprocess.run(
[sys.executable, str(install_script)],
cwd=target_dir,
check=True,
)
def build_argument_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser()
parser.add_argument("--target", required=True, help="Final output directory for the exported skill package.")
parser.add_argument("--force", action="store_true", help="Replace the target directory if it already exists.")
parser.add_argument(
"--update",
action="store_true",
help="Overwrite files in the target directory without deleting it. Preserves .env, .venv, .install_state.json.",
)
parser.add_argument(
"--bootstrap",
action="store_true",
help="After export, run the exported scripts/install/install.py inside the target directory.",
)
parser.add_argument(
"--manifest",
default="skill/slidea/manifest.json",
help="Path to skill manifest JSON relative to project root (default: skill/slidea/manifest.json).",
)
return parser
def main() -> int:
parser = build_argument_parser()
args = parser.parse_args()
manifest_path = ROOT_DIR / args.manifest
if not manifest_path.exists():
print(f"Error: manifest file not found: {manifest_path}", file=sys.stderr)
return 1
target_dir = Path(args.target).expanduser().resolve()
exported_dir = export_skill(target_dir, force=args.force, update=args.update, manifest_path=manifest_path)
print(f"Exported skill package to: {exported_dir}")
if args.bootstrap:
print(f"Bootstrapping exported skill package in: {exported_dir}")
bootstrap_skill(exported_dir)
return 0
if __name__ == "__main__":
raise SystemExit(main())