systemprompt-cli 0.2.2

Unified CLI for systemprompt.io AI governance: agent orchestration, MCP governance, analytics, profiles, cloud deploy, and self-hosted operations.
Documentation
use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
use systemprompt_cloud::ProjectContext;
use systemprompt_logging::CliService;
use systemprompt_models::CliPaths;

pub mod templates;

use super::dockerfile;
use crate::cli_settings::CliConfig;
use templates::{
    admin_agent_config, admin_mcp_config, agent_config, ai_config, blog_list_template,
    blog_post_template, content_config, cookie_policy, page_list_template, page_template,
    privacy_policy, root_config, scheduler_config, web_config, web_metadata, welcome_blog_post,
};

const ADMIN_MCP_REPO: &str = "https://github.com/systempromptio/systemprompt-mcp-admin.git";

const GITIGNORE_CONTENT: &str = "# Ignore sensitive files
credentials.json
tenants.json
**/secrets.json
docker/
storage/
";

const DOCKERIGNORE_CONTENT: &str = ".git
.gitignore
.gitmodules
target/debug
.cargo
.systemprompt/credentials.json
.systemprompt/tenants.json
.systemprompt/**/secrets.json
.systemprompt/docker
.systemprompt/storage
.env*
backup
docs
instructions
*.md
web/node_modules
.vscode
.idea
logs
*.log
";

fn entrypoint_content() -> String {
    format!(
        r#"#!/bin/sh
set -e

echo "Running database migrations..."
/app/bin/systemprompt {db_migrate_cmd}

echo "Starting services..."
exec /app/bin/systemprompt {services_serve_cmd} --foreground
"#,
        db_migrate_cmd = CliPaths::db_migrate_cmd(),
        services_serve_cmd = CliPaths::services_serve_cmd(),
    )
}

pub fn execute(force: bool, _config: &CliConfig) -> Result<()> {
    let project_root = std::env::current_dir().context("Failed to get current directory")?;
    let ctx = ProjectContext::new(project_root.clone());
    let systemprompt_dir = ctx.systemprompt_dir();
    let services_dir = project_root.join("services");

    let project_name = project_root
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("systemprompt")
        .to_string();

    CliService::section("Initialize Project");
    CliService::key_value("Project", &project_name);
    CliService::key_value("Root", &project_root.display().to_string());

    if systemprompt_dir.exists() {
        CliService::info(".systemprompt/ already exists");
    } else {
        create_systemprompt_dir(&systemprompt_dir, &project_root)?;
    }

    if !services_dir.exists() || force {
        if force && services_dir.exists() {
            CliService::warning("Removing existing services directory...");
            std::fs::remove_dir_all(&services_dir)
                .context("Failed to remove services directory")?;
        }
        generate_services_boilerplate(&project_root, &project_name)?;
    } else {
        CliService::info("services/ already exists (use --force to regenerate)");
    }

    CliService::section("Next Steps");
    CliService::info("1. systemprompt cloud auth login     # Authenticate");
    CliService::info("2. systemprompt cloud tenant create  # Create a tenant");
    CliService::info("3. systemprompt cloud profile create local  # Create a profile");

    Ok(())
}

fn create_systemprompt_dir(dir: &Path, project_root: &Path) -> Result<()> {
    std::fs::create_dir_all(dir).context("Failed to create .systemprompt directory")?;

    std::fs::write(dir.join(".gitignore"), GITIGNORE_CONTENT)
        .context("Failed to create .gitignore")?;
    CliService::info("  Created .systemprompt/.gitignore");

    std::fs::write(dir.join(".dockerignore"), DOCKERIGNORE_CONTENT)
        .context("Failed to create .dockerignore")?;
    CliService::info("  Created .systemprompt/.dockerignore");

    let dockerfile_content = dockerfile::generate_dockerfile_content(project_root);
    std::fs::write(dir.join("Dockerfile"), dockerfile_content)
        .context("Failed to create Dockerfile")?;
    CliService::info("  Created .systemprompt/Dockerfile");

    std::fs::write(dir.join("entrypoint.sh"), entrypoint_content())
        .context("Failed to create entrypoint.sh")?;
    CliService::info("  Created .systemprompt/entrypoint.sh");

    CliService::success("Created .systemprompt/");
    Ok(())
}

pub fn ensure_project_scaffolding(project_root: &Path) -> Result<()> {
    let services_dir = project_root.join("services");
    let web_dir = project_root.join("web");

    if services_dir.exists() && web_dir.exists() {
        return Ok(());
    }

    let project_name = project_root
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("systemprompt")
        .to_string();

    if !services_dir.exists() {
        CliService::info("Scaffolding services/ directory...");
        generate_services_boilerplate(project_root, &project_name)?;
    }

    if !web_dir.exists() {
        std::fs::create_dir_all(&web_dir)
            .with_context(|| format!("Failed to create directory: {}", web_dir.display()))?;
        CliService::info("Created web/ directory");
    }

    Ok(())
}

fn generate_services_boilerplate(project_root: &Path, project_name: &str) -> Result<()> {
    CliService::section("Creating Services Boilerplate");

    let services_dir = project_root.join("services");
    let logs_dir = project_root.join("logs");

    create_directories(&services_dir, &logs_dir)?;
    write_config_files(&services_dir, project_name)?;
    write_template_files(&services_dir)?;
    write_content_files(&services_dir, project_name)?;

    write_file(&services_dir.join("skills/.gitkeep"), "")?;

    clone_admin_mcp_server(&services_dir)?;

    CliService::success("Services boilerplate created");
    Ok(())
}

fn create_directories(services_dir: &Path, logs_dir: &Path) -> Result<()> {
    create_dir(services_dir)?;
    create_dir(&services_dir.join("config"))?;
    create_dir(&services_dir.join("agents"))?;
    create_dir(&services_dir.join("mcp"))?;
    create_dir(&services_dir.join("ai"))?;
    create_dir(&services_dir.join("content"))?;
    create_dir(&services_dir.join("content/blog"))?;
    create_dir(&services_dir.join("content/blog/welcome"))?;
    create_dir(&services_dir.join("content/legal"))?;
    create_dir(&services_dir.join("skills"))?;
    create_dir(&services_dir.join("web"))?;
    create_dir(&services_dir.join("web/templates"))?;
    create_dir(&services_dir.join("web/assets"))?;
    create_dir(&services_dir.join("scheduler"))?;

    create_dir(logs_dir)?;
    write_file(
        &logs_dir.join(".gitignore"),
        "# Ignore all log files\n*.log\n*.log.*\n",
    )?;

    Ok(())
}

fn write_config_files(services_dir: &Path, project_name: &str) -> Result<()> {
    write_file(&services_dir.join("config/config.yaml"), &root_config())?;
    write_file(
        &services_dir.join("agents/assistant.yaml"),
        &agent_config(project_name),
    )?;
    write_file(
        &services_dir.join("agents/admin.yaml"),
        &admin_agent_config(),
    )?;
    write_file(
        &services_dir.join("mcp/systemprompt-admin.yaml"),
        &admin_mcp_config(),
    )?;
    write_file(
        &services_dir.join("ai/config.yaml"),
        &ai_config("anthropic"),
    )?;
    write_file(&services_dir.join("content/config.yaml"), &content_config())?;
    write_file(
        &services_dir.join("web/config.yaml"),
        &web_config(project_name),
    )?;
    write_file(
        &services_dir.join("web/metadata.yaml"),
        &web_metadata(project_name),
    )?;
    write_file(
        &services_dir.join("scheduler/config.yaml"),
        &scheduler_config(),
    )?;

    Ok(())
}

fn write_template_files(services_dir: &Path) -> Result<()> {
    write_file(
        &services_dir.join("web/templates/page.html"),
        &page_template(),
    )?;
    write_file(
        &services_dir.join("web/templates/blog-post.html"),
        &blog_post_template(),
    )?;
    write_file(
        &services_dir.join("web/templates/blog-list.html"),
        &blog_list_template(),
    )?;
    write_file(
        &services_dir.join("web/templates/page-list.html"),
        &page_list_template(),
    )?;

    Ok(())
}

fn write_content_files(services_dir: &Path, project_name: &str) -> Result<()> {
    write_file(
        &services_dir.join("content/blog/welcome/index.md"),
        &welcome_blog_post(project_name),
    )?;
    write_file(
        &services_dir.join("content/legal/privacy-policy.md"),
        &privacy_policy(project_name),
    )?;
    write_file(
        &services_dir.join("content/legal/cookie-policy.md"),
        &cookie_policy(project_name),
    )?;

    Ok(())
}

fn clone_admin_mcp_server(services_dir: &Path) -> Result<()> {
    let mcp_dir = services_dir.join("mcp/systemprompt-admin");

    if mcp_dir.exists() {
        CliService::info("systemprompt-admin MCP server already exists");
        return Ok(());
    }

    let spinner = CliService::spinner("Cloning systemprompt-admin MCP server...");

    let output = Command::new("git")
        .args(["clone", "--depth", "1", ADMIN_MCP_REPO])
        .arg(&mcp_dir)
        .output()
        .context("Failed to execute git clone")?;

    spinner.finish_and_clear();

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        CliService::warning(&format!("Could not clone systemprompt-admin: {}", stderr));
        return Ok(());
    }

    let git_dir = mcp_dir.join(".git");
    if git_dir.exists() {
        std::fs::remove_dir_all(&git_dir).ok();
    }

    CliService::success("Cloned systemprompt-admin MCP server");
    Ok(())
}

fn create_dir(path: &Path) -> Result<()> {
    std::fs::create_dir_all(path)
        .with_context(|| format!("Failed to create directory: {}", path.display()))
}

fn write_file(path: &Path, content: &str) -> Result<()> {
    std::fs::write(path, content)
        .with_context(|| format!("Failed to write file: {}", path.display()))?;
    CliService::info(&format!("  Created {}", path.display()));
    Ok(())
}