fledge 1.1.1

Dev lifecycle CLI. One tool for the dev loop, any language.
use anyhow::{bail, Context, Result};
use console::style;
use std::fs;
use std::path::Path;

use super::PLUGINS_CREATE_SCHEMA;

pub(crate) fn create_plugin(
    name: &str,
    output: &Path,
    description: Option<&str>,
    yes: bool,
    wasm: bool,
    json: bool,
) -> Result<()> {
    let yes = yes || crate::utils::is_non_interactive() || json;
    let target = output.join(name);

    if target.exists() {
        bail!("Directory '{}' already exists", target.display());
    }

    let desc = if yes || !crate::utils::is_interactive() {
        description.unwrap_or("A fledge plugin").to_string()
    } else {
        let theme = dialoguer::theme::ColorfulTheme::default();
        dialoguer::Input::with_theme(&theme)
            .with_prompt("Description")
            .default(description.unwrap_or("A fledge plugin").to_string())
            .interact_text()?
    };

    if wasm {
        return create_wasm_plugin(&target, name, &desc, json);
    }

    std::fs::create_dir_all(target.join("bin"))
        .with_context(|| format!("creating {}/bin", target.display()))?;

    let plugin_toml = format!(
        r#"[plugin]
name = {name:?}
version = "0.1.0"
description = {desc:?}
# author = "your-name"

[[commands]]
name = {name:?}
description = {desc:?}
binary = "bin/{name}"

[hooks]
# build = "cargo build --release"
# post_install = "hooks/post-install.sh"

[capabilities]
exec = false
store = false
metadata = false
"#,
    );
    fs::write(target.join("plugin.toml"), plugin_toml).context("writing plugin.toml")?;

    let script = format!(
        r#"#!/usr/bin/env bash
# fledge plugin entry point.
#
# fledge sets FLEDGE_PLUGIN_DIR to this plugin's source directory before
# invoking your binary. Use it to reach sibling files in `bin/`, hooks,
# fixtures, etc. Don't use `dirname "$0"` — the binary fledge invokes is
# a symlink in a shared bin/, so $0 won't point to your repo.
set -euo pipefail
PLUGIN_DIR="${{FLEDGE_PLUGIN_DIR:?FLEDGE_PLUGIN_DIR not set — fledge >= 0.15.3 sets it automatically}}"

echo "{name} plugin running with args: $@"
echo "(plugin dir: $PLUGIN_DIR)"

# To dispatch to sibling helpers in the same `bin/` (a common multi-subcommand
# pattern), use:
#
#   exec "$PLUGIN_DIR/bin/{name}-${{1?missing subcommand}}" "${{@:2}}"
"#
    );
    let script_path = target.join("bin").join(name);
    fs::write(&script_path, script).context("writing bin script")?;

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))
            .context("setting executable permission")?;
    }

    fs::write(
        target.join("README.md"),
        format!(
            r#"# {name} — fledge plugin

{desc}

## Install

```bash
fledge plugins install ./{name}
```

Or after publishing:

```bash
fledge plugins install owner/{name}
```

## Commands

| Command | Description |
|---------|-------------|
| `fledge {name}` | {desc} |

## Development

Edit `plugin.toml` to configure commands, hooks, and capabilities.
See [fledge plugin docs](https://github.com/CorvidLabs/fledge) for the full plugin format.
"#
        ),
    )
    .context("writing README.md")?;

    fs::write(
        target.join(".gitignore"),
        "# Build artifacts\n/target/\n/dist/\n\n# OS\n.DS_Store\nThumbs.db\n",
    )
    .context("writing .gitignore")?;

    let files_created = vec![
        "plugin.toml".to_string(),
        format!("bin/{name}"),
        "README.md".to_string(),
        ".gitignore".to_string(),
    ];

    if json {
        let result = serde_json::json!({
            "schema_version": PLUGINS_CREATE_SCHEMA,
            "action": "create",
            "path": target.display().to_string(),
            "name": name,
            "description": desc,
            "files_created": files_created,
        });
        println!("{}", serde_json::to_string_pretty(&result)?);
    } else {
        println!(
            "\n{} Created plugin at {}",
            style("").green().bold(),
            style(target.display()).cyan()
        );
        println!(
            "\n  {} Edit manifest in {}",
            style("1.").dim(),
            style("plugin.toml").green()
        );
        println!(
            "  {} Validate with: {}",
            style("2.").dim(),
            style(format!("fledge plugins validate ./{name}")).cyan()
        );
        println!(
            "  {} Publish with: {}",
            style("3.").dim(),
            style(format!("fledge plugins publish ./{name}")).cyan()
        );
    }

    Ok(())
}

fn create_wasm_plugin(target: &Path, name: &str, desc: &str, json: bool) -> Result<()> {
    std::fs::create_dir_all(target.join("src"))
        .with_context(|| format!("creating {}/src", target.display()))?;

    let plugin_toml = format!(
        r#"[plugin]
name = {name:?}
version = "0.1.0"
description = {desc:?}
protocol = "fledge-v1"
runtime = "wasm"

[[commands]]
name = {name:?}
description = {desc:?}
binary = "target/wasm32-wasip1/release/{name}.wasm"

[hooks]
build = "cargo build --target wasm32-wasip1 --release"

[capabilities]
exec = false
store = false
metadata = false
filesystem = "none"
network = false
"#,
    );
    fs::write(target.join("plugin.toml"), plugin_toml).context("writing plugin.toml")?;

    let cargo_toml = format!(
        r#"[package]
name = {name:?}
version = "0.1.0"
edition = "2021"

[dependencies]
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
"#,
    );
    fs::write(target.join("Cargo.toml"), cargo_toml).context("writing Cargo.toml")?;

    // .cargo/config.toml so `cargo build --release` targets wasm32-wasip1 by default
    fs::create_dir_all(target.join(".cargo")).context("creating .cargo")?;
    fs::write(
        target.join(".cargo/config.toml"),
        "[build]\ntarget = \"wasm32-wasip1\"\n",
    )
    .context("writing .cargo/config.toml")?;

    let main_rs = format!(
        r#"use serde_json::json;

fn main() {{
    let output = json!({{
        "type": "output",
        "text": "{name} WASM plugin running\n"
    }});
    println!("{{}}", output);
}}
"#,
    );
    fs::write(target.join("src/main.rs"), main_rs).context("writing src/main.rs")?;

    fs::write(
        target.join(".gitignore"),
        "/target/\n.DS_Store\nThumbs.db\n",
    )
    .context("writing .gitignore")?;

    let readme = format!(
        r#"# {name} — fledge WASM plugin

{desc}

## Prerequisites

```bash
rustup target add wasm32-wasip1
```

## Build

```bash
cargo build --release
```

The `.cargo/config.toml` sets `wasm32-wasip1` as the default target, so `--target` is not needed.

## Install

```bash
fledge plugins install ./{name}
```

## Commands

| Command | Description |
|---------|-------------|
| `fledge {name}` | {desc} |
"#,
    );
    fs::write(target.join("README.md"), readme).context("writing README.md")?;

    let files_created = vec![
        "plugin.toml".to_string(),
        "Cargo.toml".to_string(),
        ".cargo/config.toml".to_string(),
        "src/main.rs".to_string(),
        "README.md".to_string(),
        ".gitignore".to_string(),
    ];

    if json {
        let result = serde_json::json!({
            "schema_version": super::PLUGINS_CREATE_SCHEMA,
            "action": "create",
            "path": target.display().to_string(),
            "name": name,
            "description": desc,
            "runtime": "wasm",
            "files_created": files_created,
        });
        println!("{}", serde_json::to_string_pretty(&result)?);
    } else {
        println!(
            "\n{} Created WASM plugin at {}",
            style("").green().bold(),
            style(target.display()).cyan()
        );
        println!(
            "\n  {} Build with: {}",
            style("1.").dim(),
            style("cargo build --target wasm32-wasip1 --release").cyan()
        );
        println!(
            "  {} Install with: {}",
            style("2.").dim(),
            style(format!("fledge plugins install ./{name}")).cyan()
        );
    }

    Ok(())
}