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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
use crate::error::ProtoError;
use crate::proto::ProtoEnvironment;
use crate::tool::Tool;
use crate::tools_config::{ToolsConfig, SCHEMA_PLUGIN_KEY};
use crate::user_config::UserConfig;
use extism::{manifest::Wasm, Manifest};
use miette::IntoDiagnostic;
use proto_pdk_api::{HostArch, HostEnvironment, HostOS, UserConfigSettings};
use starbase_utils::{json, toml};
use std::path::PathBuf;
use std::{env, path::Path};
use tracing::{debug, trace};
use warpgate::{create_http_client_with_options, to_virtual_path, Id, PluginLocator};

pub fn inject_default_manifest_config(
    id: &Id,
    proto: &ProtoEnvironment,
    user_config: &UserConfig,
    manifest: &mut Manifest,
) -> miette::Result<()> {
    trace!(id = id.as_str(), "Storing tool identifier");

    manifest
        .config
        .insert("proto_tool_id".to_string(), id.to_string());

    let value = json::to_string(&UserConfigSettings {
        auto_clean: user_config.auto_clean,
        auto_install: user_config.auto_install,
        node_intercept_globals: user_config.node_intercept_globals,
    })
    .into_diagnostic()?;

    trace!(config = %value, "Storing user configuration");

    manifest
        .config
        .insert("proto_user_config".to_string(), value);

    let paths_map = manifest.allowed_paths.as_ref().unwrap();

    let value = json::to_string(&HostEnvironment {
        arch: HostArch::from_env(),
        os: HostOS::from_env(),
        home_dir: to_virtual_path(paths_map, &proto.home),
        proto_dir: to_virtual_path(paths_map, &proto.root),
    })
    .into_diagnostic()?;

    trace!(env = %value, "Storing proto environment");

    manifest
        .config
        .insert("proto_environment".to_string(), value);

    Ok(())
}

pub fn locate_tool(
    id: &Id,
    proto: &ProtoEnvironment,
    user_config: &UserConfig,
    current_dir_only: bool,
) -> miette::Result<PluginLocator> {
    let mut locator = None;

    debug!(
        tool = id.as_str(),
        "Traversing upwards to find a configured plugin"
    );

    // 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(id) {
                locator = Some(maybe_locator.to_owned());
                break;
            }

            // Don't traverse passed the home directory,
            // or only want to check the current directory
            if dir == proto.home || current_dir_only {
                break;
            }

            current_dir = dir.parent();
        }
    }

    // Then check the user's config
    if locator.is_none() {
        if let Some(maybe_locator) = user_config.plugins.get(id) {
            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(id) {
            locator = Some(maybe_locator.to_owned());
        }
    }

    let Some(locator) = locator else {
        return Err(ProtoError::UnknownTool { id: id.to_owned() }.into());
    };

    Ok(locator)
}

pub async fn load_schema_plugin(
    proto: impl AsRef<ProtoEnvironment>,
    user_config: &UserConfig,
) -> miette::Result<PathBuf> {
    let proto = proto.as_ref();
    let http_client = create_http_client_with_options(user_config.http.clone())?;
    let plugin_loader = proto.get_plugin_loader();

    let schema_id = Id::raw(SCHEMA_PLUGIN_KEY);
    let schema_locator = locate_tool(&schema_id, proto, user_config, true)?;

    plugin_loader
        .load_plugin_with_client(schema_id, schema_locator, &http_client)
        .await
}

pub async fn load_tool_from_locator(
    id: impl AsRef<Id>,
    proto: impl AsRef<ProtoEnvironment>,
    locator: impl AsRef<PluginLocator>,
    user_config: &UserConfig,
) -> miette::Result<Tool> {
    let id = id.as_ref();
    let proto = proto.as_ref();
    let locator = locator.as_ref();

    let http_client = create_http_client_with_options(user_config.http.clone())?;
    let plugin_loader = proto.get_plugin_loader();
    let plugin_path = plugin_loader
        .load_plugin_with_client(&id, locator, &http_client)
        .await?;

    // If a TOML plugin, we need to load the WASM plugin for it,
    // wrap it, and modify the plugin manifest.
    let mut manifest = if plugin_path
        .extension()
        .map(|ext| ext == "toml")
        .unwrap_or(false)
    {
        debug!(source = ?plugin_path, "Loading TOML plugin");

        let mut manifest = Tool::create_plugin_manifest(
            proto,
            Wasm::file(load_schema_plugin(proto, user_config).await?),
        )?;

        // Convert TOML to JSON
        let schema: json::JsonValue = toml::read_file(plugin_path)?;
        let schema = json::to_string(&schema).into_diagnostic()?;

        trace!(schema = %schema, "Storing schema settings");

        manifest.config.insert("schema".to_string(), schema);
        manifest

        // Otherwise, just use the WASM plugin as is
    } else {
        debug!(source = ?plugin_path, "Loading WASM plugin");

        Tool::create_plugin_manifest(proto, Wasm::file(plugin_path))?
    };

    inject_default_manifest_config(id, proto, user_config, &mut manifest)?;

    let mut tool = Tool::load_from_manifest(id, proto, manifest)?;
    tool.locator = Some(locator.to_owned());

    Ok(tool)
}

pub async fn load_tool(id: &Id) -> miette::Result<Tool> {
    let proto = ProtoEnvironment::new()?;
    let user_config = proto.load_user_config()?;
    let locator = locate_tool(id, &proto, &user_config, false)?;

    load_tool_from_locator(id, proto, locator, &user_config).await
}