diff --git a/ai-rules.md b/ai-rules.md new file mode 100644 index 000000000..b648dc900 --- /dev/null +++ b/ai-rules.md @@ -0,0 +1,65 @@ +# Shuttle Development Rules + +## Core Setup + +Always use `#[shuttle_runtime::main]` as your entry point. Can be other frameworks too, you can use the Shuttle MCP - search docs tool to find the correct code for your framework. + +```rust +#[shuttle_runtime::main] +async fn main() -> ShuttleAxum { + let router = Router::new().route("/", get(hello)); + Ok(router.into()) +} +``` + +## Databases + +- **Shared DB** (free): `#[shuttle_shared_db::Postgres] pool: PgPool` +- **AWS RDS** (paid): `#[shuttle_aws_rds::Postgres] pool: PgPool` + +## Secrets + +Create `Secrets.toml` in project root, add to `.gitignore`: + +```toml +MY_API_KEY = 'your-api-key-here' +``` + +Use in code: + +```rust +#[shuttle_runtime::main] +async fn main(#[shuttle_runtime::Secrets] secrets: SecretStore) -> ShuttleAxum { + let api_key = secrets.get("MY_API_KEY").unwrap(); + Ok(router.into()) +} +``` + +## Static Assets + +Configure in `Shuttle.toml`: + +```toml +[build] +assets = [ + "assets/*", + "frontend/dist/*", + "static/*" +] + +[deploy] +include = ["ignored-files/*"] # Include files that are normally ignored by git +deny_dirty = true +``` + +## Development Workflow + +1. `shuttle run` - local development +2. Use MCP server for AI-assisted development +3. Use MCP server for Searching the Docs + +## Key Points + +- Always use `#[shuttle_runtime::main]` as your entry point +- Configure static assets in `Shuttle.toml` +- Use secrets for sensitive configuration diff --git a/cargo-shuttle/src/ai.rs b/cargo-shuttle/src/ai.rs new file mode 100644 index 000000000..d2e4404f2 --- /dev/null +++ b/cargo-shuttle/src/ai.rs @@ -0,0 +1,184 @@ +use anyhow::{Context, Result}; +use dialoguer::{theme::ColorfulTheme, Confirm, Select}; +use std::fs; +use std::path::Path; + +use crate::args::AiRulesArgs; + +/// Embedded content from ai-rules.md +const AI_RULES_CONTENT: &str = include_str!("../../ai-rules.md"); + +#[derive(Debug, Clone, Copy)] +pub enum AiPlatform { + Cursor, + Claude, + Windsurf, + Gemini, + Codex, +} + +impl AiPlatform { + /// Get the relative file path for this platform + fn file_path(&self) -> &'static str { + match self { + AiPlatform::Cursor => ".cursor/rules/shuttle.mdc", + AiPlatform::Claude => "CLAUDE.md", + AiPlatform::Windsurf => ".windsurf/rules/shuttle.md", + AiPlatform::Gemini => "GEMINI.md", + AiPlatform::Codex => "AGENTS.md", + } + } + + /// Get the display name for this platform + fn display_name(&self) -> &'static str { + match self { + AiPlatform::Cursor => "Cursor", + AiPlatform::Claude => "Claude Code", + AiPlatform::Windsurf => "Windsurf", + AiPlatform::Gemini => "Gemini CLI", + AiPlatform::Codex => "Codex CLI", + } + } + + /// Get all available platforms + fn all() -> Vec { + vec![ + AiPlatform::Cursor, + AiPlatform::Claude, + AiPlatform::Windsurf, + AiPlatform::Gemini, + AiPlatform::Codex, + ] + } +} + +/// Handle the `ai rules` command. +/// Generates AI coding assistant rules files for the selected platform in the working directory. +pub fn handle_ai_rules(args: &AiRulesArgs, working_directory: &Path) -> Result<()> { + // Determine platform from args or prompt user + let platform = if args.cursor { + AiPlatform::Cursor + } else if args.claude { + AiPlatform::Claude + } else if args.windsurf { + AiPlatform::Windsurf + } else if args.gemini { + AiPlatform::Gemini + } else if args.codex { + AiPlatform::Codex + } else { + // Interactive mode - prompt user to select platform + select_platform_interactive()? + }; + + // Write the rules file + let file_path = working_directory.join(platform.file_path()); + let file_existed = file_path.exists(); + let should_append = matches!( + platform, + AiPlatform::Claude | AiPlatform::Gemini | AiPlatform::Codex + ) && file_existed; + + let was_written = write_rules_file(platform, working_directory)?; + + if was_written { + let action = if should_append { + "appended to" + } else if file_existed { + "updated" + } else { + "generated" + }; + + println!( + "✓ Successfully {} {} rules at: {}", + action, + platform.display_name(), + platform.file_path() + ); + } + + Ok(()) +} + +/// Prompt user to select a platform interactively +fn select_platform_interactive() -> Result { + let platforms = AiPlatform::all(); + let platform_names: Vec<&str> = platforms.iter().map(|p| p.display_name()).collect(); + + let selection = Select::with_theme(&ColorfulTheme::default()) + .with_prompt("Select AI coding assistant") + .items(&platform_names) + .default(0) + .interact() + .context("Failed to get platform selection")?; + + Ok(platforms[selection]) +} + +/// Write the rules file to the appropriate location. +/// Returns Ok(true) if the file was written, Ok(false) if the user aborted. +fn write_rules_file(platform: AiPlatform, working_directory: &Path) -> Result { + let file_path = working_directory.join(platform.file_path()); + + // For top-level markdown platforms (Claude, Gemini, Codex), append to existing file instead of overwriting + let should_append = matches!( + platform, + AiPlatform::Claude | AiPlatform::Gemini | AiPlatform::Codex + ) && file_path.exists(); + + // Check if file already exists + if file_path.exists() { + let action = if should_append { + "append to" + } else { + "overwrite" + }; + let confirm = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "File {} already exists. {} it?", + platform.file_path(), + action + .chars() + .next() + .map(|c| c.to_uppercase().collect::()) + .unwrap_or_default() + + &action[1..] + )) + .default(false) + .interact() + .context("Failed to get confirmation")?; + + if !confirm { + println!("Aborted."); + return Ok(false); + } + } + + // Create parent directories if they don't exist + if let Some(parent) = file_path.parent() { + fs::create_dir_all(parent) + .context(format!("Failed to create directory: {}", parent.display()))?; + } + + // Write or append the content + if should_append { + // For top-level markdown files (Claude, Gemini, Codex), append the AI rules to existing file + let existing_content = fs::read_to_string(&file_path).context(format!( + "Failed to read existing file: {}", + file_path.display() + ))?; + + // Add separator and append new content + let combined_content = format!("{}\n\n{}", existing_content.trim_end(), AI_RULES_CONTENT); + + fs::write(&file_path, combined_content) + .context(format!("Failed to append to file: {}", file_path.display()))?; + } else { + // For other platforms or new files, write/overwrite the content + fs::write(&file_path, AI_RULES_CONTENT) + .context(format!("Failed to write file: {}", file_path.display()))?; + } + + Ok(true) +} diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs index e12dd7c1f..14d9e41a6 100644 --- a/cargo-shuttle/src/args.rs +++ b/cargo-shuttle/src/args.rs @@ -173,6 +173,9 @@ pub enum Command { /// Commands for the Shuttle MCP server #[command(subcommand)] Mcp(McpCommand), + /// AI assistant commands + #[command(subcommand)] + Ai(AiCommand), } #[derive(Subcommand)] @@ -181,6 +184,35 @@ pub enum McpCommand { Start, } +#[derive(Subcommand, Clone, Debug)] +pub enum AiCommand { + /// Generate AI coding assistant rules for your project + Rules(AiRulesArgs), +} + +#[derive(Parser, Clone, Debug)] +pub struct AiRulesArgs { + /// Generate rules for Cursor AI + #[arg(long, conflicts_with_all = ["claude", "windsurf", "gemini", "codex"])] + pub cursor: bool, + + /// Generate rules for Claude Code + #[arg(long, conflicts_with_all = ["cursor", "windsurf", "gemini", "codex"])] + pub claude: bool, + + /// Generate rules for Windsurf AI + #[arg(long, conflicts_with_all = ["cursor", "claude", "gemini", "codex"])] + pub windsurf: bool, + + /// Generate rules for Gemini CLI + #[arg(long, conflicts_with_all = ["cursor", "claude", "windsurf", "codex"])] + pub gemini: bool, + + /// Generate rules for Codex CLI + #[arg(long, conflicts_with_all = ["cursor", "claude", "windsurf", "gemini"])] + pub codex: bool, +} + #[derive(Subcommand)] pub enum GenerateCommand { /// Generate shell completions diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index af973380c..d7f3b239c 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -1,3 +1,4 @@ +mod ai; pub mod args; pub mod builder; pub mod config; @@ -373,6 +374,12 @@ impl Shuttle { .await .map(|_| CommandOutput::None), }, + Command::Ai(ai_cmd) => match ai_cmd { + args::AiCommand::Rules(ai_args) => { + ai::handle_ai_rules(&ai_args, &args.project_args.working_directory)?; + Ok(CommandOutput::None) + } + }, } }