feat: unify bootstrap export and apply cli

This commit is contained in:
2026-07-02 16:02:54 +00:00
parent 5f9302abc5
commit 6c5498085d
6 changed files with 851 additions and 123 deletions
+6 -120
View File
@@ -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__":