1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use convert_case::{Case, Casing};
use proto_core::*;
use proto_node as node;
use proto_rust as rust;
use proto_schema_plugin as schema_plugin;
use proto_wasm_plugin as wasm_plugin;
use starbase_utils::toml;
use std::{env, path::Path, str::FromStr};
use strum::EnumIter;
use tracing::debug;
use warpgate::{PluginLoader, PluginLocator};

#[derive(Clone, Debug, Eq, EnumIter, Hash, PartialEq)]
pub enum ToolType {
    // Node.js
    Node,
    Npm,
    Pnpm,
    Yarn,
    // Rust
    Rust,
    // Plugins
    Plugin(String),
}

impl FromStr for ToolType {
    type Err = ProtoError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value.to_lowercase().as_ref() {
            // Node.js
            "node" => Ok(Self::Node),
            "npm" => Ok(Self::Npm),
            "pnpm" => Ok(Self::Pnpm),
            "yarn" | "yarnpkg" => Ok(Self::Yarn),
            // Rust
            "rust" => Ok(Self::Rust),
            // Plugins
            name => Ok(Self::Plugin(name.to_case(Case::Kebab))),
        }
    }
}

pub async fn create_plugin_from_locator(
    plugin: &str,
    proto: impl AsRef<Proto>,
    locator: impl AsRef<PluginLocator>,
) -> Result<Box<dyn Tool<'static>>, ProtoError> {
    let proto = proto.as_ref();
    let locator = locator.as_ref();

    let plugin_path = PluginLoader::new(&proto.plugins_dir, &proto.temp_dir)
        .load_plugin(plugin, locator)
        .await
        .map_err(|e| ProtoError::Message(e.to_string()))?;
    let is_toml = plugin_path
        .extension()
        .map(|e| e == "toml")
        .unwrap_or(false);

    if is_toml {
        debug!(source = ?plugin_path, "Loading TOML plugin");

        return Ok(Box::new(schema_plugin::SchemaPlugin::new(
            proto,
            plugin.to_owned(),
            toml::read_file(plugin_path)?,
        )));
    }

    debug!(source = ?plugin_path, "Loading WASM plugin");

    Ok(Box::new(wasm_plugin::WasmPlugin::new(
        proto,
        plugin.to_owned(),
        plugin_path,
    )?))
}

pub async fn create_plugin_tool(
    plugin: &str,
    proto: Proto,
) -> Result<Box<dyn Tool<'static>>, ProtoError> {
    let mut locator = None;

    // Traverse upwards checking each `.prototools` for a plugin
    if let Ok(working_dir) = env::current_dir() {
        let mut current_dir: Option<&Path> = Some(&working_dir);

        while let Some(dir) = &current_dir {
            let tools_config = ToolsConfig::load_from(dir)?;

            if let Some(maybe_locator) = tools_config.plugins.get(plugin) {
                locator = Some(maybe_locator.to_owned());
                break;
            }

            current_dir = dir.parent();
        }
    }

    // Then check the user's config
    if locator.is_none() {
        let user_config = UserConfig::load()?;

        if let Some(maybe_locator) = user_config.plugins.get(plugin) {
            locator = Some(maybe_locator.to_owned());
        }
    }

    // And finally the builtin plugins
    if locator.is_none() {
        let builtin_plugins = ToolsConfig::builtin_plugins();

        if let Some(maybe_locator) = builtin_plugins.get(plugin) {
            locator = Some(maybe_locator.to_owned());
        }
    }

    let Some(locator) = locator else {
        return Err(ProtoError::MissingPlugin(plugin.to_owned()));
    };

    create_plugin_from_locator(plugin, proto, locator).await
}

pub async fn create_tool(tool: &ToolType) -> Result<Box<dyn Tool<'static>>, ProtoError> {
    let proto = Proto::new()?;

    Ok(match tool {
        // Node.js
        ToolType::Node => Box::new(node::NodeLanguage::new(proto)),
        ToolType::Npm => Box::new(node::NodeDependencyManager::new(
            proto,
            node::NodeDependencyManagerType::Npm,
        )),
        ToolType::Pnpm => Box::new(node::NodeDependencyManager::new(
            proto,
            node::NodeDependencyManagerType::Pnpm,
        )),
        ToolType::Yarn => Box::new(node::NodeDependencyManager::new(
            proto,
            node::NodeDependencyManagerType::Yarn,
        )),
        // Rust
        ToolType::Rust => Box::new(rust::RustLanguage::new(proto)),
        // Plugins
        ToolType::Plugin(plugin) => create_plugin_tool(plugin, proto).await?,
    })
}