fidius-cli 0.2.0

CLI for the Fidius plugin framework
// Copyright 2026 Colliery, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::path::PathBuf;
use std::process;

use clap::{Parser, Subcommand};

mod commands;
mod python_stub;

#[derive(Parser)]
#[command(name = "fidius", about = "Fidius plugin framework CLI", version)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Scaffold a new plugin interface crate
    InitInterface {
        /// Crate name for the interface
        name: String,
        /// Trait name to generate
        #[arg(long = "trait")]
        trait_name: String,
        /// Output directory (default: current dir)
        #[arg(long)]
        path: Option<PathBuf>,
        /// Pin fidius dependency version (checks crates.io if not a local path)
        #[arg(long)]
        version: Option<String>,
        /// Custom file extension for package archives (default: "fid")
        #[arg(long)]
        extension: Option<String>,
    },
    /// Scaffold a new plugin implementation crate
    InitPlugin {
        /// Crate name for the plugin
        name: String,
        /// Interface crate (local path, crates.io name, or crate name)
        #[arg(long)]
        interface: String,
        /// Trait name from the interface crate
        #[arg(long = "trait")]
        trait_name: String,
        /// Output directory (default: current dir)
        #[arg(long)]
        path: Option<PathBuf>,
        /// Pin interface dependency version (overrides auto-detection)
        #[arg(long)]
        version: Option<String>,
    },
    /// Scaffold a new host application crate that uses the typed Client
    InitHost {
        /// Crate name for the host binary
        name: String,
        /// Interface crate (local path, crates.io name, or crate name)
        #[arg(long)]
        interface: String,
        /// Trait name from the interface crate (used to reference the generated Client)
        #[arg(long = "trait")]
        trait_name: String,
        /// Output directory (default: current dir)
        #[arg(long)]
        path: Option<PathBuf>,
        /// Pin dependency versions (overrides auto-detection)
        #[arg(long)]
        version: Option<String>,
    },
    /// Generate an Ed25519 signing keypair
    Keygen {
        /// Output file base name (writes `<name>.secret` and `<name>.public`)
        #[arg(long)]
        out: String,
    },
    /// Sign a plugin dylib
    Sign {
        /// Path to the secret key file
        #[arg(long)]
        key: PathBuf,
        /// Path to the dylib to sign
        dylib: PathBuf,
    },
    /// Verify a plugin dylib signature
    Verify {
        /// Path to the public key file
        #[arg(long)]
        key: PathBuf,
        /// Path to the dylib to verify
        dylib: PathBuf,
    },
    /// Inspect a plugin dylib's registry
    Inspect {
        /// Path to the dylib to inspect
        dylib: PathBuf,
    },
    /// Smoke-test a plugin: build, load, and invoke each method with a zero-arg input
    Test {
        /// Path to the plugin package directory
        dir: PathBuf,
        /// Build in debug mode instead of release
        #[arg(long)]
        debug: bool,
    },
    /// Package management commands
    Package {
        #[command(subcommand)]
        command: PackageCommands,
    },
    /// Generate a Python stub for an interface trait so a Python plugin
    /// author has type-hinted method signatures plus the matching
    /// `__interface_hash__` constant.
    PythonStub {
        /// Path to the interface crate's source file (typically `src/lib.rs`).
        #[arg(long)]
        interface: PathBuf,
        /// Output `.py` file path. Use `-` for stdout.
        #[arg(long)]
        out: PathBuf,
        /// Trait name to generate the stub for. Required when the file
        /// declares more than one `#[plugin_interface]` trait.
        #[arg(long)]
        trait_name: Option<String>,
    },
}

#[derive(Subcommand)]
enum PackageCommands {
    /// Validate a package manifest
    Validate {
        /// Path to the package directory
        dir: PathBuf,
    },
    /// Build a package (compile the cdylib)
    Build {
        /// Path to the package directory
        dir: PathBuf,
        /// Build in debug mode instead of release
        #[arg(long)]
        debug: bool,
    },
    /// Inspect a package manifest
    Inspect {
        /// Path to the package directory
        dir: PathBuf,
    },
    /// Sign a package manifest
    Sign {
        /// Path to the secret key file
        #[arg(long)]
        key: PathBuf,
        /// Path to the package directory
        dir: PathBuf,
    },
    /// Verify a package manifest signature
    Verify {
        /// Path to the public key file
        #[arg(long)]
        key: PathBuf,
        /// Path to the package directory
        dir: PathBuf,
    },
    /// Pack a package directory into a .fid archive
    Pack {
        /// Path to the package directory
        dir: PathBuf,
        /// Output file path (default: {name}-{version}.fid in current dir)
        #[arg(long)]
        output: Option<PathBuf>,
    },
    /// Unpack a .fid archive
    Unpack {
        /// Path to the .fid archive
        archive: PathBuf,
        /// Destination directory (default: current dir)
        #[arg(long)]
        dest: Option<PathBuf>,
    },
}

fn main() {
    let cli = Cli::parse();

    let result = match cli.command {
        Commands::InitInterface {
            name,
            trait_name,
            path,
            version,
            extension,
        } => commands::init_interface(
            &name,
            &trait_name,
            path.as_deref(),
            version.as_deref(),
            extension.as_deref(),
        ),
        Commands::InitPlugin {
            name,
            interface,
            trait_name,
            path,
            version,
        } => commands::init_plugin(
            &name,
            &interface,
            &trait_name,
            path.as_deref(),
            version.as_deref(),
        ),
        Commands::InitHost {
            name,
            interface,
            trait_name,
            path,
            version,
        } => commands::init_host(
            &name,
            &interface,
            &trait_name,
            path.as_deref(),
            version.as_deref(),
        ),
        Commands::Keygen { out } => commands::keygen(&out),
        Commands::Sign { key, dylib } => commands::sign(&key, &dylib),
        Commands::Verify { key, dylib } => commands::verify(&key, &dylib),
        Commands::Inspect { dylib } => commands::inspect(&dylib),
        Commands::Test { dir, debug } => commands::test(&dir, !debug),
        Commands::Package { command } => match command {
            PackageCommands::Validate { dir } => commands::package_validate(&dir),
            PackageCommands::Build { dir, debug } => commands::package_build(&dir, !debug),
            PackageCommands::Inspect { dir } => commands::package_inspect(&dir),
            PackageCommands::Sign { key, dir } => commands::package_sign(&key, &dir),
            PackageCommands::Verify { key, dir } => commands::package_verify(&key, &dir),
            PackageCommands::Pack { dir, output } => {
                commands::package_pack(&dir, output.as_deref())
            }
            PackageCommands::Unpack { archive, dest } => {
                commands::package_unpack(&archive, dest.as_deref())
            }
        },
        Commands::PythonStub {
            interface,
            out,
            trait_name,
        } => commands::python_stub(&interface, &out, trait_name.as_deref()),
    };

    if let Err(e) = result {
        eprintln!("error: {e}");
        process::exit(1);
    }
}