arcature-cli 2026.2.0

Developer lifecycle CLI for Arcature applications.
Documentation
//! `arc package` execution (AP2.1-10).
//!
//! The entry point that orchestrates a production artifact bundle: build
//! (unless `--no-build`), resolve the inputs from the project config,
//! discover the host target and framework version, assemble the bundle,
//! and report the bundle path. The individual responsibilities (bundle
//! assembly, dependency reading, manifest, checksums, SBOM, licenses,
//! target triple, framework version, workspace discovery, timestamp) each
//! live in their own file; this module wires them together.

use super::bundle;
use super::deps;
use super::error::PackageError;
use super::target::host_target_triple;
use super::timestamp::rfc3339_now;
use super::version::read_framework_version;
use super::workspace::discover_workspace_root;
use crate::cli::PackageOptions;
use crate::error::CommandError;
use crate::project;

/// Execute `arc package`: build (unless `--no-build`), assemble the bundle,
/// and report the bundle path. `options` is the CLI-layer
/// [`PackageOptions`] (defined in `cli::command`, matching the `make` /
/// `new` option-struct pattern: one option type per command, owned by the
/// CLI layer, consumed by the command `execute`).
pub(crate) fn execute(options: PackageOptions) -> Result<(), CommandError> {
    let project = project::discover()?;
    // The build precondition: run `arc build` unless `--no-build`. The
    // build command itself runs `pnpm build` + `cargo build --release`.
    if !options.no_build {
        println!("package  running build precondition");
        super::super::build::execute()?;
    }

    // Resolve the inputs from the project config.
    let app_root = project.root();
    let release_binary = app_root
        .join("target")
        .join("release")
        .join(&project.backend_binary);
    if !release_binary.is_file() {
        return Err(CommandError::from(PackageError::Missing {
            what: "release binary",
            path: release_binary,
        }));
    }
    let public_build = app_root.join("public").join("build");
    if !public_build.is_dir() {
        return Err(CommandError::from(PackageError::Missing {
            what: "frontend build",
            path: public_build,
        }));
    }

    // The bundle target label: explicit override or `<app>-<host-target>`.
    let target_triple = host_target_triple();
    let target_label = options
        .target_label
        .unwrap_or_else(|| format!("{}-{}", project.backend_package, target_triple));

    // The Arcature framework version: read from the workspace `arcature`
    // crate's Cargo.toml. We avoid a direct dependency on the `arcature`
    // crate (the CLI depends on templates + db, not the facade) — parse
    // the workspace Cargo.toml's `arcature` member version. This is the
    // honest source: the version the app was compiled against is the
    // workspace's `arcature` version.
    let framework_version = read_framework_version(app_root)?;
    let created_at = rfc3339_now();

    // The Arcature workspace root: the directory containing the root
    // `Cargo.toml` (the workspace manifest). The project root may be a
    // subdirectory of a dogfood app; walk up to find the workspace root.
    let framework_root = discover_workspace_root(app_root);
    let lockfile = framework_root.join("Cargo.lock");
    let dependencies = deps::read_dependencies(&lockfile).unwrap_or_default();

    let dist_root = app_root.join("dist");
    let bundle = bundle::assemble(
        &dist_root,
        &target_label,
        &release_binary,
        &public_build,
        &project.backend_package,
        &framework_version,
        &target_triple,
        &created_at,
        &framework_root,
        app_root,
        dependencies,
    )
    .map_err(CommandError::from)?;

    println!(
        "package  bundle at {} ({} files)",
        bundle.root.display(),
        bundle.files.len()
    );
    println!(
        "package  verify with: sha256sum -c {}/checksums.sha256",
        bundle.root.display()
    );
    Ok(())
}