yana-rt 1.3.0

Yana AI Runtime — safety CLI for AI agents: scan, graph, vault, hunt, ci, map, fix, doctor
use clap::Subcommand;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Subcommand)]
pub enum InitAction {
    /// Initialize Yana AI in a project directory
    Run {
        /// Target directory (default: current directory)
        #[arg(default_value = ".")]
        dir: String,

        /// Skip prompts, use defaults
        #[arg(long)]
        yes: bool,
    },
    /// Show what init would create without writing files
    Dry {
        #[arg(default_value = ".")]
        dir: String,
    },
}

pub fn dispatch(action: InitAction) {
    match action {
        InitAction::Run { dir, yes } => run(&dir, yes, false),
        InitAction::Dry { dir }     => run(&dir, true, true),
    }
}

// ── Templates ─────────────────────────────────────────────────────────────────

const SETTINGS_JSON: &str = r#"{
  "permissions": {
    "allow": [
      "Bash(git status:*)",
      "Bash(git log:*)",
      "Bash(git diff:*)",
      "Bash(cargo build:*)",
      "Bash(cargo test:*)",
      "Bash(npm test:*)",
      "Bash(yana-rt *)"
    ],
    "deny": []
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "bash .claude/hooks/pre-tool-use.sh" }]
      }
    ]
  }
}
"#;

const PRE_TOOL_USE_SH: &str = r#"#!/usr/bin/env bash
# Yana AI pre-tool-use gate — generated by yana-rt init
# Blocks: rm -rf, force push, pipe-to-shell, eval injection
set -uo pipefail

INPUT=$(cat)
TOOL=$(echo "$INPUT" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_name',''))" 2>/dev/null || echo "")
CMD=$(echo "$INPUT"  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('command',''))" 2>/dev/null || echo "")

BLOCKED=(
  "rm -rf"
  "rm -fr"
  "git push --force"
  "git push -f"
  "git reset --hard"
  "| bash"
  "| sh"
  "| python"
  "base64 -d.*|"
  "eval \$("
  "curl.*| bash"
  "wget.*| bash"
)

for pattern in "${BLOCKED[@]}"; do
  if echo "$CMD" | grep -qE "$pattern" 2>/dev/null; then
    echo '{"decision":"block","reason":"[yana-ai] Blocked pattern: '"$pattern"'"}'
    exit 0
  fi
done

echo '{"decision":"allow"}'
"#;

const YANA_CONFIG: &str = r#"# .yana-ai/config.toml — generated by yana-rt init
version = "1.0"

[sandbox]
mode = "auto"          # docker | nsjail | ulimit | auto

[gates]
level = 2              # 1=basic | 2=standard | 3=strict

[scan]
fail_on = "high"       # info | low | medium | high | critical

[budget]
daily_token_limit = 50000
session_token_cap  = 10000
"#;

const GITIGNORE_APPEND: &str = r#"
# Yana AI
.yana-ai/cache/
releases/logs/
"#;

// ── Runner ────────────────────────────────────────────────────────────────────

struct FileSpec {
    path:    &'static str,
    content: &'static str,
    exec:    bool,
}

fn file_specs() -> Vec<FileSpec> {
    vec![
        FileSpec { path: ".claude/settings.json",       content: SETTINGS_JSON,    exec: false },
        FileSpec { path: ".claude/hooks/pre-tool-use.sh", content: PRE_TOOL_USE_SH, exec: true  },
        FileSpec { path: ".yana-ai/config.toml",          content: YANA_CONFIG,    exec: false },
    ]
}

fn run(dir: &str, _yes: bool, dry: bool) {
    let root = PathBuf::from(dir);

    println!("  yana-rt init{}", if dry { " (dry run)" } else { "" });
    println!("  target: {}", root.display());
    println!("{}", "".repeat(52));

    let specs = file_specs();
    let mut created = 0usize;
    let mut skipped = 0usize;

    for spec in &specs {
        let dest = root.join(spec.path);
        let exists = dest.exists();

        if exists {
            println!("  skip   {}", spec.path);
            skipped += 1;
            continue;
        }

        println!("  create {}", spec.path);
        created += 1;

        if dry { continue; }

        if let Some(parent) = dest.parent() {
            fs::create_dir_all(parent)
                .unwrap_or_else(|e| eprintln!("[init] mkdir failed: {e}"));
        }

        fs::write(&dest, spec.content)
            .unwrap_or_else(|e| eprintln!("[init] write failed {}: {e}", spec.path));

        #[cfg(unix)]
        if spec.exec {
            use std::os::unix::fs::PermissionsExt;
            if let Ok(meta) = fs::metadata(&dest) {
                let mut perms = meta.permissions();
                perms.set_mode(0o755);
                let _ = fs::set_permissions(&dest, perms);
            }
        }
    }

    // Append to .gitignore if exists
    let gi = root.join(".gitignore");
    if gi.exists() {
        let existing = fs::read_to_string(&gi).unwrap_or_default();
        if !existing.contains("Yana AI") {
            println!("  append .gitignore");
            if !dry {
                let _ = fs::write(&gi, format!("{}{}", existing, GITIGNORE_APPEND));
            }
        }
    }

    println!("{}", "".repeat(52));
    println!("  {} created · {} skipped", created, skipped);

    if !dry {
        println!();
        println!("  Next steps:");
        println!("    1. npm install yana-ai   # wire hooks");
        println!("    2. npx yana-ai-install           # activate");
        println!("    3. yana-rt scan .             # first scan");
        println!("    4. yana-rt doctor run .       # health check");
    }
}