feat: unify bootstrap export and apply cli
This commit is contained in:
@@ -1,117 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate and verify Agent OS bootstrap exports."""
|
||||
"""Compatibility wrapper for `bootstrap cli export|verify`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import fnmatch
|
||||
import json
|
||||
import shutil
|
||||
import stat
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
BOOTSTRAP_ROOT = Path(__file__).resolve().parent
|
||||
DEFAULT_OUTPUT = BOOTSTRAP_ROOT / "exports" / "generic"
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def parse_mode(mode: str) -> int:
|
||||
return int(mode, 8)
|
||||
|
||||
|
||||
def ensure_safe_output(output: Path) -> Path:
|
||||
resolved = output.resolve()
|
||||
if resolved.parts[-3:] != ("bootstrap", "exports", "generic"):
|
||||
raise SystemExit(
|
||||
"refusing to write outside a bootstrap/exports/generic directory: "
|
||||
f"{resolved}"
|
||||
)
|
||||
return resolved
|
||||
|
||||
|
||||
def copy_file(source: Path, target: Path, mode: str) -> None:
|
||||
if not source.is_file():
|
||||
raise FileNotFoundError(f"missing source file: {source}")
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, target)
|
||||
target.chmod(parse_mode(mode))
|
||||
|
||||
|
||||
def clean_output(output: Path) -> None:
|
||||
ensure_safe_output(output)
|
||||
if output.exists():
|
||||
shutil.rmtree(output)
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def create_directories(output: Path, manifest: dict) -> None:
|
||||
for directory in manifest["directories"]:
|
||||
target = output / directory["path"]
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
if directory.get("keep"):
|
||||
keep_file = target / ".gitkeep"
|
||||
keep_file.touch()
|
||||
keep_file.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
|
||||
|
||||
|
||||
def export_bundle(output: Path, manifest: dict, rules: dict) -> None:
|
||||
clean_output(output)
|
||||
create_directories(output, manifest)
|
||||
|
||||
for item in rules["metadata"]:
|
||||
copy_file(
|
||||
BOOTSTRAP_ROOT / item["source"],
|
||||
output / item["target"],
|
||||
item.get("mode", "0644"),
|
||||
)
|
||||
|
||||
template_root = BOOTSTRAP_ROOT / rules["template_root"]
|
||||
for item in manifest["templates"]:
|
||||
copy_file(
|
||||
template_root / item["source"],
|
||||
output / item["target"],
|
||||
item.get("mode", "0644"),
|
||||
)
|
||||
|
||||
|
||||
def verify_required(output: Path, rules: dict) -> list[str]:
|
||||
failures: list[str] = []
|
||||
for item in rules["verify"]["required"]:
|
||||
target = output / item["path"]
|
||||
expected_type = item["type"]
|
||||
if expected_type == "file" and not target.is_file():
|
||||
failures.append(f"missing file: {item['path']}")
|
||||
elif expected_type == "directory" and not target.is_dir():
|
||||
failures.append(f"missing directory: {item['path']}")
|
||||
return failures
|
||||
|
||||
|
||||
def verify_forbidden(output: Path, rules: dict) -> list[str]:
|
||||
failures: list[str] = []
|
||||
for path in output.rglob("*"):
|
||||
rel = path.relative_to(output).as_posix()
|
||||
for pattern in rules["verify"].get("forbidden", []):
|
||||
if fnmatch.fnmatch(rel, pattern):
|
||||
failures.append(f"forbidden path exported: {rel}")
|
||||
return failures
|
||||
|
||||
|
||||
def verify_export(output: Path, rules: dict) -> None:
|
||||
failures = verify_required(output, rules) + verify_forbidden(output, rules)
|
||||
if failures:
|
||||
for failure in failures:
|
||||
print(f"FAIL {failure}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def count_files(output: Path) -> int:
|
||||
return sum(1 for path in output.rglob("*") if path.is_file())
|
||||
from bootstrap_lib import DEFAULT_OUTPUT
|
||||
from cli import cmd_export, cmd_verify
|
||||
|
||||
|
||||
def main() -> int:
|
||||
@@ -128,19 +24,9 @@ def main() -> int:
|
||||
help="Verify an existing export without regenerating it.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
output = ensure_safe_output(args.output)
|
||||
manifest = load_json(BOOTSTRAP_ROOT / "bundle-manifest.json")
|
||||
rules = load_json(BOOTSTRAP_ROOT / "export-rules.json")
|
||||
|
||||
if not args.verify_only:
|
||||
export_bundle(output, manifest, rules)
|
||||
|
||||
verify_export(output, rules)
|
||||
print(f"OK {manifest['bundle']['id']} -> {output}")
|
||||
print(f"OK verified {len(rules['verify']['required'])} required paths")
|
||||
print(f"OK exported {count_files(output)} files")
|
||||
return 0
|
||||
if args.verify_only:
|
||||
return cmd_verify(args)
|
||||
return cmd_export(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user