Skip to content

Index

๐Ÿค– AI-Generated Content

This documentation was generated with AI assistance and is still being audited. Some, or potentially a lot, of this information may be inaccurate. Learn more.

flavor.commands

Command modules for the flavor CLI.

Functions

clean_command

clean_command(
    all: bool, helpers: bool, dry_run: bool, yes: bool
) -> None

Clean work environment cache (default) or helpers.

Source code in flavor/commands/utils.py
@click.command("clean")
@click.option(
    "--all",
    is_flag=True,
    help="Clean both work environment and helpers",
)
@click.option(
    "--helpers",
    is_flag=True,
    help="Clean only helper binaries",
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Show what would be removed without removing",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    help="Skip confirmation prompt",
)
def clean_command(all: bool, helpers: bool, dry_run: bool, yes: bool) -> None:
    """Clean work environment cache (default) or helpers."""
    log.debug(
        "Clean command started",
        all=all,
        helpers=helpers,
        dry_run=dry_run,
        yes=yes,
    )

    # Determine what to clean
    clean_workenv = not helpers or all
    clean_helpers = helpers or all

    if dry_run:
        pout("๐Ÿ” DRY RUN - Nothing will be removed\n")

    total_freed = 0

    if clean_workenv:
        total_freed += _clean_workenv_cache(dry_run, yes)

    if clean_helpers:
        total_freed += _clean_helper_binaries(dry_run, yes)

    _show_total_freed(dry_run, total_freed)

doctor_command

doctor_command() -> None

Check FlavorPack installation health and report findings.

Source code in flavor/commands/doctor.py
@click.command("doctor")
def doctor_command() -> None:  # noqa: C901
    """Check FlavorPack installation health and report findings."""

    errors: list[str] = []
    warnings: list[str] = []

    pout("FlavorPack Doctor")
    pout("=================")
    pout("")

    # --- Python version ---
    py_version = sys.version.split()[0]
    major, minor = sys.version_info.major, sys.version_info.minor
    if major < 3 or (major == 3 and minor < 11):
        py_status = "WARN"
        warnings.append(f"Python {py_version} is below the required 3.11")
    else:
        py_status = "OK"
    py_marker = "OK" if py_status == "OK" else "WARN"
    pout(f"Python:        {py_version} [{py_marker}]")

    # --- FlavorPack version ---
    pout(f"FlavorPack:    {flavor_version}")

    # --- Platform ---
    arch = platform.machine()
    pout(f"Platform:      {sys.platform} {arch}")
    pout("")

    # --- Helper binaries ---
    pout("Helpers:")
    manager = HelperManager()
    helpers = manager.list_helpers()
    all_helpers = helpers.get("launchers", []) + helpers.get("builders", [])

    if not all_helpers:
        pout("  (none found)")
        warnings.append("No helper binaries found โ€” run: flavor helpers build")
    else:
        for helper in sorted(all_helpers, key=lambda h: h.name):
            exists = helper.path.exists()
            executable = exists and os.access(helper.path, os.X_OK)
            size_mb = helper.size / (1024 * 1024) if helper.size else 0
            version_str = helper.version or "unknown"

            if not exists:
                status = "MISSING"
                errors.append(f"Helper {helper.name} not found at {helper.path}")
            elif not executable:
                status = "NOT-EXEC"
                errors.append(f"Helper {helper.name} is not executable")
            else:
                status = "OK"

            pout(f"  {helper.name:<40} [{status}]  {version_str}  ({size_mb:.1f} MB)")

    pout("")

    # --- Directories ---
    pout("Directories:")

    # Cache dir
    cache_dir = get_cache_dir()
    cache_exists = cache_dir.exists()
    cache_writable = cache_exists and os.access(cache_dir, os.W_OK)
    if not cache_exists:
        cache_note = "(not created yet)"
    elif cache_writable:
        cache_note = "writable [OK]"
    else:
        cache_note = "NOT WRITABLE [ERR]"
        errors.append(f"Cache directory not writable: {cache_dir}")
    pout(f"  Cache:        {cache_dir}  {cache_note}")

    # Config dir
    config_dir = get_config_dir()
    config_note = "[OK]" if config_dir.exists() else "(not created yet)"
    pout(f"  Config:       {config_dir}  {config_note}")

    # Trusted keys dir
    keys_dir = get_trusted_keys_dir()
    if keys_dir.exists():
        key_count = len(list(keys_dir.glob("*.pub")))
        if key_count == 0:
            keys_note = "0 keys  [WARN]"
            warnings.append("No trusted public keys found โ€” packages cannot be signature-verified")
        else:
            keys_note = f"{key_count} key(s)  [OK]"
    else:
        keys_note = "0 keys  [WARN]  (not created yet)"
        warnings.append("Trusted keys directory does not exist โ€” packages cannot be signature-verified")
    pout(f"  Trusted keys: {keys_dir}  {keys_note}")

    pout("")

    # --- Overall status ---
    if errors:
        perr("Overall: [ERR] Not ready")
        for msg in errors:
            perr(f"  - {msg}")
        for msg in warnings:
            pout(f"  - {msg}")
        raise SystemExit(1)
    elif warnings:
        pout("Overall: [WARN] Warnings found")
        for msg in warnings:
            pout(f"  - {msg}")
    else:
        pout("Overall: [OK] Ready")

helper_group

helper_group() -> None

Manage Flavor helper binaries (launchers and builders).

Source code in flavor/commands/helpers.py
@click.group("helpers")
def helper_group() -> None:
    """Manage Flavor helper binaries (launchers and builders)."""
    pass

init_command

init_command(global_: bool) -> None

Set up FlavorPack config directory structure on this host.

Creates the trusted-keys directory and a default policy.json. Safe to run multiple times โ€” existing files are never overwritten.

Source code in flavor/commands/init.py
@click.command("init")
@click.option(
    "--global",
    "global_",
    is_flag=True,
    default=False,
    help="Set up system-wide config under /etc/flavor (requires root/sudo).",
)
def init_command(global_: bool) -> None:
    """Set up FlavorPack config directory structure on this host.

    Creates the trusted-keys directory and a default policy.json.
    Safe to run multiple times โ€” existing files are never overwritten.
    """
    config_root: Path = get_system_config_dir() if global_ else get_config_dir()
    scope = "system" if global_ else "user"

    log.debug("Initializing FlavorPack config", scope=scope, root=str(config_root))

    # Create trusted-keys directory
    trusted_keys_dir = config_root / "trusted-keys"
    trusted_keys_dir.mkdir(parents=True, exist_ok=True)
    pout(f"โœ“ {trusted_keys_dir}")

    # Scaffold policy.json (never overwrite)
    policy_file = config_root / "policy.json"
    if not policy_file.exists():
        policy_file.write_text(json.dumps(_POLICY_JSON_SCAFFOLD, indent=2) + "\n", encoding="utf-8")
        pout(f"โœ“ {policy_file}  (scaffolded)")
    else:
        pout(f"  {policy_file}  (already exists, not modified)")

    pout(f"\nFlavorPack {scope} config initialised at {config_root}")
    if not global_:
        pout("  Add trusted keys with: flavor trust add <key.pub>")
        pout(f"  Edit policy:           {policy_file}")
    else:
        pout("  Add trusted keys with: sudo flavor trust add <key.pub> --global")
        pout(f"  Edit policy:           {policy_file}")

inspect_command

inspect_command(
    package_file: str,
    output_json: bool,
    show_sbom: bool,
    show_provenance: bool,
) -> None

Quick inspection of a flavor package.

Source code in flavor/commands/inspect.py
@click.command("inspect")
@click.argument(
    "package_file",
    type=click.Path(exists=True, dir_okay=False, resolve_path=True),
    required=True,
)
@click.option(
    "--json",
    "output_json",
    is_flag=True,
    help="Output as JSON",
)
@click.option("--sbom", "show_sbom", is_flag=True, default=False, help="Print the CycloneDX SBOM.")
@click.option("--provenance", "show_provenance", is_flag=True, default=False, help="Print build provenance.")
def inspect_command(package_file: str, output_json: bool, show_sbom: bool, show_provenance: bool) -> None:
    """Quick inspection of a flavor package."""
    package_path = Path(package_file)
    log.debug("Inspecting package", package=str(package_path), output_json=output_json)

    try:
        with PSPFReader(package_path) as reader:
            if show_sbom or show_provenance:
                _output_attestation(reader, show_sbom=show_sbom, show_provenance=show_provenance)
                return

            index = reader.read_index()
            metadata = reader.read_metadata()
            slot_descriptors = reader.read_slot_descriptors()
            slots_metadata = metadata.get("slots", [])

            log.debug(
                "Package inspection completed",
                format_version=f"0x{index.format_version:08x}",
                slot_count=len(slot_descriptors),
            )

            if output_json:
                _output_json_format(package_path, index, metadata, slot_descriptors, slots_metadata)
            else:
                _output_human_format(package_path, index, metadata, slot_descriptors, slots_metadata)

    except FileNotFoundError as e:
        log.error("Package not found", package=package_file)
        perr(f"โŒ Package not found: {package_file}")
        raise click.Abort() from e
    except Exception as e:
        log.error("Error inspecting package", package=package_file, error=str(e))
        perr(f"โŒ Error inspecting package: {e}")
        raise click.Abort() from e

keygen_command

keygen_command(out_dir: str) -> None

Generates an Ed25519 key pair for package integrity signing.

Source code in flavor/commands/keygen.py
@click.command("keygen")
@click.option(
    "--out-dir",
    default="keys",
    type=click.Path(file_okay=False, writable=True, resolve_path=True),
    help="Directory to save the Ed25519 key pair.",
)
def keygen_command(out_dir: str) -> None:
    """Generates an Ed25519 key pair for package integrity signing."""
    log.debug("Generating key pair", out_dir=out_dir)

    try:
        generate_key_pair(Path(out_dir))
        log.info("Key pair generated successfully", out_dir=out_dir)
        pout(f"โœ… Package integrity key pair generated in '{out_dir}'.")
    except BuildError as e:
        log.error("Keygen failed", error=str(e), out_dir=out_dir)
        perr(f"โŒ Keygen failed: {e}")
        raise click.Abort() from e

pack_command

pack_command(
    pyproject_toml_path: str,
    output_path: str | None,
    launcher_bin: str | None,
    builder_bin: str | None,
    verify: bool,
    strip: bool,
    progress: bool,
    quiet: bool,
    private_key: str | None,
    public_key: str | None,
    key_seed: str | None,
    workenv_base: str | None,
) -> None

Pack the application for one or more target platforms.

Source code in flavor/commands/package.py
@click.command("pack")
@click.option(
    "--manifest",
    "pyproject_toml_path",
    default="pyproject.toml",
    type=click.Path(exists=True, dir_okay=False, resolve_path=True),
    help="Path to the pyproject.toml manifest file.",
)
@click.option(
    "--output",
    "output_path",
    type=click.Path(dir_okay=False, resolve_path=True),
    help="Custom output path for the package (defaults to dist/<name>.psp).",
)
@click.option(
    "--launcher-bin",
    type=click.Path(exists=True, dir_okay=False, resolve_path=True),
    help="Path to launcher binary to embed in the package.",
)
@click.option(
    "--builder-bin",
    type=click.Path(exists=True, dir_okay=False, resolve_path=True),
    help="Path to builder binary (overrides default builder selection).",
)
@click.option(
    "--verify/--no-verify",
    default=True,
    help="Verify the package after building (default: verify).",
)
@click.option(
    "--strip",
    is_flag=True,
    help="Strip debug symbols from launcher binary for size reduction.",
)
@click.option(
    "--progress",
    is_flag=True,
    help="Show progress bars during packaging.",
)
@click.option(
    "--quiet",
    is_flag=True,
    help="Suppress progress output.",
)
@click.option(
    "--private-key",
    type=click.Path(exists=True, dir_okay=False, resolve_path=True),
    help="Path to private key (PEM format) for signing.",
)
@click.option(
    "--public-key",
    type=click.Path(exists=True, dir_okay=False, resolve_path=True),
    help="Path to public key (PEM format, optional if private key provided).",
)
@click.option(
    "--key-seed",
    type=str,
    help="Seed for deterministic key generation.",
)
@click.option(
    "--workenv-base",
    type=click.Path(exists=True, file_okay=False, resolve_path=True),
    help="Base directory for {workenv} resolution (defaults to CWD).",
)
def pack_command(
    pyproject_toml_path: str,
    output_path: str | None,
    launcher_bin: str | None,
    builder_bin: str | None,
    verify: bool,
    strip: bool,
    progress: bool,
    quiet: bool,
    private_key: str | None,
    public_key: str | None,
    key_seed: str | None,
    workenv_base: str | None,
) -> None:
    """Pack the application for one or more target platforms."""
    log.debug(
        "Starting package command",
        manifest=pyproject_toml_path,
        output_path=output_path,
        quiet=quiet,
    )

    if not quiet:
        pout("๐Ÿš€ Packaging application...")

    _setup_workenv_base(workenv_base)

    try:
        built_artifacts = _build_package_artifacts(
            pyproject_toml_path,
            output_path,
            launcher_bin,
            builder_bin,
            strip,
            progress,
            quiet,
            private_key,
            public_key,
            key_seed,
        )

        if not quiet:
            pout("๐Ÿ” Processing and verifying artifacts...")

        _process_built_artifacts(built_artifacts, verify, strip, quiet)
        _show_final_results(built_artifacts, quiet)

        log.info("Packaging completed successfully", artifact_count=len(built_artifacts))

    except (BuildError, PackagingError, click.UsageError) as e:
        log.error("Packaging failed", error=str(e), manifest=pyproject_toml_path)
        perr(f"โŒ Packaging Failed:\n{e}")
        raise click.Abort() from e

trust_group

trust_group() -> None

Manage the trusted signing key store.

Source code in flavor/commands/trust.py
@click.group("trust")
def trust_group() -> None:
    """Manage the trusted signing key store."""

verify_command

verify_command(package_file: str) -> None

Verifies a flavor package.

Source code in flavor/commands/verify.py
@click.command("verify")
@click.argument(
    "package_file",
    type=click.Path(exists=True, dir_okay=False, resolve_path=True),
    required=True,
)
def verify_command(package_file: str) -> None:
    """Verifies a flavor package."""
    final_package_file = Path(package_file)
    log.debug("Starting package verification", package=str(final_package_file))
    pout(f"๐Ÿ” Verifying package '{final_package_file}'...")

    try:
        result = verify_package(final_package_file)
        log.debug(
            "Package verification completed",
            format=result.get("format"),
            signature_valid=result.get("signature_valid"),
        )

        _display_basic_info(result)
        if result["format"] == "PSPF/2025":
            _display_pspf_info(result)
        _display_signature_status(result)

    except Exception as e:
        log.error("Verification failed", error=str(e), package=str(final_package_file))
        perr(f"โŒ Verification failed: {e}")
        raise click.Abort() from e

workenv_group

workenv_group() -> None

Manage the Flavor work environment cache.

Source code in flavor/commands/workenv.py
@click.group("workenv")
def workenv_group() -> None:
    """Manage the Flavor work environment cache."""
    pass