Skip to main content

ferro_cli/deploy/
mod.rs

1//! Deploy scaffold primitives. Phase 122.2 reduced this surface dramatically;
2//! after plan 122.2-04 only the helpers consumed by surviving doctor checks
3//! remain. Plans 06..12 will replace these with the new design in SCOPE.
4
5#![allow(dead_code)]
6
7pub mod app_yaml_existing;
8pub mod bin_detect;
9pub mod env_production;
10pub mod secret_keys;
11
12use toml::Value;
13
14// ---------------------------------------------------------------------------
15// Env parsing (.env / .env.example / .env.production)
16// ---------------------------------------------------------------------------
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct EnvEntry {
20    pub key: String,
21    pub value: String,
22}
23
24/// Parse a `.env`-style file body into ordered (key, value) entries.
25/// Skips blank and comment lines. Splits on the first `=`. Strips a trailing
26/// ` # comment` from unquoted values.
27pub fn parse_env_entries(content: &str) -> Vec<EnvEntry> {
28    let mut out = Vec::new();
29    for raw in content.lines() {
30        let trimmed = raw.trim_start();
31        if trimmed.is_empty() || trimmed.starts_with('#') {
32            continue;
33        }
34        let Some((key, value)) = raw.split_once('=') else {
35            continue;
36        };
37        let key = key.trim().to_string();
38        if key.is_empty() {
39            continue;
40        }
41        let stripped = strip_inline_comment(value.trim());
42        out.push(EnvEntry {
43            key,
44            value: stripped.to_string(),
45        });
46    }
47    out
48}
49
50fn strip_inline_comment(value: &str) -> &str {
51    let bytes = value.as_bytes();
52    if bytes.first() == Some(&b'"') || bytes.first() == Some(&b'\'') {
53        return value;
54    }
55    let mut prev_ws = false;
56    for (i, b) in bytes.iter().enumerate() {
57        if *b == b'#' && prev_ws {
58            return value[..i].trim_end();
59        }
60        prev_ws = *b == b' ' || *b == b'\t';
61    }
62    value
63}
64
65// ---------------------------------------------------------------------------
66// ferro path dep discovery (used by doctor path check)
67// ---------------------------------------------------------------------------
68
69const DEP_TABLES: &[&str] = &["dependencies", "dev-dependencies", "build-dependencies"];
70
71/// Walk dependency tables of a parsed Cargo.toml string and collect every key
72/// starting with `ferro` whose value is a table containing a `path` field.
73pub fn find_ferro_path_deps(content: &str) -> Vec<String> {
74    let parsed: Value = match content.parse() {
75        Ok(v) => v,
76        Err(_) => return Vec::new(),
77    };
78    let mut out: Vec<String> = Vec::new();
79    for table_name in DEP_TABLES {
80        let Some(table) = parsed.get(*table_name).and_then(|v| v.as_table()) else {
81            continue;
82        };
83        for (key, value) in table {
84            if !key.starts_with("ferro") {
85                continue;
86            }
87            let is_path_dep = value
88                .as_table()
89                .map(|t| t.contains_key("path"))
90                .unwrap_or(false);
91            if is_path_dep && !out.iter().any(|k| k == key) {
92                out.push(key.clone());
93            }
94        }
95    }
96    out
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn parse_env_entries_basic() {
105        let got = parse_env_entries("# c\nFOO=bar\n\nBAZ=qux # tail");
106        assert_eq!(got.len(), 2);
107        assert_eq!(got[0].key, "FOO");
108        assert_eq!(got[0].value, "bar");
109        assert_eq!(got[1].value, "qux");
110    }
111
112    #[test]
113    fn finds_ferro_path_deps() {
114        let deps = find_ferro_path_deps("[dependencies]\nferro = { path = \"..\" }\n");
115        assert_eq!(deps, vec!["ferro".to_string()]);
116    }
117}