Skip to main content

outlayer_cli/commands/
deploy.rs

1use anyhow::{Context, Result};
2use serde_json::json;
3use sha2::{Digest, Sha256};
4
5use crate::config::{self, NetworkConfig};
6use crate::near::{ContractCaller, NearClient};
7
8/// `outlayer deploy <name>` — deploy agent to OutLayer
9pub async fn deploy(
10    network: &NetworkConfig,
11    project_name: &str,
12    wasm_url: Option<String>,
13    wasm_hash: Option<String>,
14    build_target: &str,
15    no_activate: bool,
16) -> Result<()> {
17    let creds = config::load_credentials(network)?;
18    let owner = &creds.account_id;
19
20    let (source, version_label) = if let Some(url) = &wasm_url {
21        // WASM URL source (FastFS or any URL)
22        let hash = match &wasm_hash {
23            Some(h) => h.clone(),
24            None => {
25                eprintln!("Downloading WASM to compute hash...");
26                compute_wasm_hash(url).await?
27            }
28        };
29        let source = json!({
30            "WasmUrl": {
31                "url": url,
32                "hash": hash,
33                "build_target": build_target
34            }
35        });
36        let short_hash = if hash.len() > 12 { &hash[..12] } else { &hash };
37        (source, format!("{short_hash}..."))
38    } else {
39        // GitHub source (default)
40        let commit = get_git_commit()?;
41        let repo_url = get_git_remote()?;
42        let source = json!({
43            "GitHub": {
44                "repo": repo_url,
45                "commit": commit,
46                "build_target": build_target
47            }
48        });
49        (source, commit)
50    };
51
52    eprintln!("Deploying {owner}/{project_name}...");
53    if wasm_url.is_some() {
54        eprintln!("  Source: WasmUrl ({})", wasm_url.as_ref().unwrap());
55    } else {
56        eprintln!("  Source: GitHub ({version_label})");
57    }
58
59    let near = NearClient::new(network);
60    let caller = ContractCaller::from_credentials(&creds, network)?;
61
62    let project_id = format!("{owner}/{project_name}");
63    let deposit = 100_000_000_000_000_000_000_000u128; // 0.1 NEAR
64    let gas = 100_000_000_000_000u64; // 100 TGas
65
66    // Check if project exists
67    let existing = near.get_project(&project_id).await?;
68
69    if existing.is_some() {
70        eprintln!("  Adding version {version_label}...");
71        caller
72            .call_contract(
73                "add_version",
74                json!({
75                    "project_name": project_name,
76                    "source": source,
77                    "set_active": !no_activate
78                }),
79                gas,
80                deposit,
81            )
82            .await
83            .context("Failed to add version")?;
84    } else {
85        eprintln!("  Creating new project...");
86        caller
87            .call_contract(
88                "create_project",
89                json!({
90                    "name": project_name,
91                    "source": source
92                }),
93                gas,
94                deposit,
95            )
96            .await
97            .context("Failed to create project")?;
98    }
99
100    if no_activate {
101        eprintln!("  Version {version_label} deployed (not activated)");
102    } else {
103        eprintln!("  Project deployed");
104    }
105
106    eprintln!("\nRun with: outlayer run {owner}/{project_name} '{{\"command\": \"hello\"}}'");
107    Ok(())
108}
109
110async fn compute_wasm_hash(url: &str) -> Result<String> {
111    let response = reqwest::get(url)
112        .await
113        .with_context(|| format!("Failed to download WASM from {url}"))?;
114
115    if !response.status().is_success() {
116        anyhow::bail!("Failed to download WASM: HTTP {}", response.status());
117    }
118
119    let bytes = response
120        .bytes()
121        .await
122        .context("Failed to read WASM response body")?;
123
124    Ok(hex::encode(Sha256::digest(&bytes)))
125}
126
127fn get_git_commit() -> Result<String> {
128    let output = std::process::Command::new("git")
129        .args(["rev-parse", "HEAD"])
130        .output()
131        .context("Failed to run git rev-parse HEAD")?;
132
133    if !output.status.success() {
134        anyhow::bail!("Not a git repository. Initialize git first: git init && git commit");
135    }
136
137    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
138}
139
140fn get_git_remote() -> Result<String> {
141    let output = std::process::Command::new("git")
142        .args(["remote", "get-url", "origin"])
143        .output()
144        .context("Failed to get git remote URL")?;
145
146    if !output.status.success() {
147        anyhow::bail!("No git remote found. Add one: git remote add origin <url>");
148    }
149
150    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
151}