Skip to main content

systemprompt_cli/commands/cloud/init/
scaffolding.rs

1//! Writes the default `services/` directory tree for a new project.
2//!
3//! Creates the config, agent, MCP, content, web, and scheduler files from the
4//! [`super::templates`] strings and clones the bundled systemprompt-admin MCP
5//! server.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use anyhow::{Context, Result};
11use std::path::Path;
12use std::process::Command;
13use systemprompt_logging::CliService;
14
15use super::templates::{
16    admin_agent_config, admin_mcp_config, agent_config, ai_config, blog_list_template,
17    blog_post_template, content_config, cookie_policy, page_list_template, page_template,
18    privacy_policy, root_config, scheduler_config, web_config, web_metadata, welcome_blog_post,
19};
20
21const ADMIN_MCP_REPO: &str = "https://github.com/systempromptio/systemprompt-mcp-admin.git";
22
23pub fn generate_services_boilerplate(project_root: &Path, project_name: &str) -> Result<()> {
24    CliService::section("Creating Services Boilerplate");
25
26    let services_dir = project_root.join("services");
27    let logs_dir = project_root.join("logs");
28
29    create_directories(&services_dir, &logs_dir)?;
30    write_config_files(&services_dir, project_name)?;
31    write_template_files(&services_dir)?;
32    write_content_files(&services_dir, project_name)?;
33
34    write_file(&services_dir.join("skills/.gitkeep"), "")?;
35
36    clone_admin_mcp_server(&services_dir)?;
37
38    CliService::success("Services boilerplate created");
39    Ok(())
40}
41
42fn create_directories(services_dir: &Path, logs_dir: &Path) -> Result<()> {
43    create_dir(services_dir)?;
44    create_dir(&services_dir.join("config"))?;
45    create_dir(&services_dir.join("agents"))?;
46    create_dir(&services_dir.join("mcp"))?;
47    create_dir(&services_dir.join("ai"))?;
48    create_dir(&services_dir.join("content"))?;
49    create_dir(&services_dir.join("content/blog"))?;
50    create_dir(&services_dir.join("content/blog/welcome"))?;
51    create_dir(&services_dir.join("content/legal"))?;
52    create_dir(&services_dir.join("skills"))?;
53    create_dir(&services_dir.join("web"))?;
54    create_dir(&services_dir.join("web/templates"))?;
55    create_dir(&services_dir.join("web/assets"))?;
56    create_dir(&services_dir.join("scheduler"))?;
57
58    create_dir(logs_dir)?;
59    write_file(
60        &logs_dir.join(".gitignore"),
61        "# Ignore all log files\n*.log\n*.log.*\n",
62    )?;
63
64    Ok(())
65}
66
67fn write_config_files(services_dir: &Path, project_name: &str) -> Result<()> {
68    write_file(&services_dir.join("config/config.yaml"), &root_config())?;
69    write_file(
70        &services_dir.join("agents/assistant.yaml"),
71        &agent_config(project_name),
72    )?;
73    write_file(
74        &services_dir.join("agents/admin.yaml"),
75        &admin_agent_config(),
76    )?;
77    write_file(
78        &services_dir.join("mcp/systemprompt-admin.yaml"),
79        &admin_mcp_config(),
80    )?;
81    write_file(
82        &services_dir.join("ai/config.yaml"),
83        &ai_config("anthropic"),
84    )?;
85    write_file(&services_dir.join("content/config.yaml"), &content_config())?;
86    write_file(
87        &services_dir.join("web/config.yaml"),
88        &web_config(project_name),
89    )?;
90    write_file(
91        &services_dir.join("web/metadata.yaml"),
92        &web_metadata(project_name),
93    )?;
94    write_file(
95        &services_dir.join("scheduler/config.yaml"),
96        &scheduler_config(),
97    )?;
98
99    Ok(())
100}
101
102fn write_template_files(services_dir: &Path) -> Result<()> {
103    write_file(
104        &services_dir.join("web/templates/page.html"),
105        &page_template(),
106    )?;
107    write_file(
108        &services_dir.join("web/templates/blog-post.html"),
109        &blog_post_template(),
110    )?;
111    write_file(
112        &services_dir.join("web/templates/blog-list.html"),
113        &blog_list_template(),
114    )?;
115    write_file(
116        &services_dir.join("web/templates/page-list.html"),
117        &page_list_template(),
118    )?;
119
120    Ok(())
121}
122
123fn write_content_files(services_dir: &Path, project_name: &str) -> Result<()> {
124    write_file(
125        &services_dir.join("content/blog/welcome/index.md"),
126        &welcome_blog_post(project_name),
127    )?;
128    write_file(
129        &services_dir.join("content/legal/privacy-policy.md"),
130        &privacy_policy(project_name),
131    )?;
132    write_file(
133        &services_dir.join("content/legal/cookie-policy.md"),
134        &cookie_policy(project_name),
135    )?;
136
137    Ok(())
138}
139
140fn clone_admin_mcp_server(services_dir: &Path) -> Result<()> {
141    let mcp_dir = services_dir.join("mcp/systemprompt-admin");
142
143    if mcp_dir.exists() {
144        CliService::info("systemprompt-admin MCP server already exists");
145        return Ok(());
146    }
147
148    let spinner = CliService::spinner("Cloning systemprompt-admin MCP server...");
149
150    let output = Command::new("git")
151        .args(["clone", "--depth", "1", ADMIN_MCP_REPO])
152        .arg(&mcp_dir)
153        .output()
154        .context("Failed to execute git clone")?;
155
156    spinner.finish_and_clear();
157
158    if !output.status.success() {
159        let stderr = String::from_utf8_lossy(&output.stderr);
160        CliService::warning(&format!("Could not clone systemprompt-admin: {}", stderr));
161        return Ok(());
162    }
163
164    let git_dir = mcp_dir.join(".git");
165    if git_dir.exists()
166        && let Err(e) = std::fs::remove_dir_all(&git_dir)
167    {
168        tracing::warn!(path = %git_dir.display(), error = %e, "failed to remove .git dir");
169    }
170
171    CliService::success("Cloned systemprompt-admin MCP server");
172    Ok(())
173}
174
175fn create_dir(path: &Path) -> Result<()> {
176    std::fs::create_dir_all(path)
177        .with_context(|| format!("Failed to create directory: {}", path.display()))
178}
179
180fn write_file(path: &Path, content: &str) -> Result<()> {
181    std::fs::write(path, content)
182        .with_context(|| format!("Failed to write file: {}", path.display()))?;
183    CliService::info(&format!("  Created {}", path.display()));
184    Ok(())
185}