From 5d0c2284c36f27aa1e1b5020da7fd6cd638675a8 Mon Sep 17 00:00:00 2001 From: dcodesdev <101001810+dcodesdev@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:36:40 +0300 Subject: [PATCH 1/5] AI Rules feature --- ai-rules.md | 69 +++++++++++++++++++++++ cargo-shuttle/src/ai.rs | 116 ++++++++++++++++++++++++++++++++++++++ cargo-shuttle/src/args.rs | 24 ++++++++ cargo-shuttle/src/lib.rs | 7 +++ 4 files changed, 216 insertions(+) create mode 100644 ai-rules.md create mode 100644 cargo-shuttle/src/ai.rs diff --git a/ai-rules.md b/ai-rules.md new file mode 100644 index 000000000..a44a5ddf2 --- /dev/null +++ b/ai-rules.md @@ -0,0 +1,69 @@ +--- +alwaysApply: true +--- + +# 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..3d34009d9 --- /dev/null +++ b/cargo-shuttle/src/ai.rs @@ -0,0 +1,116 @@ +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, +} + +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", + } + } + + /// 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", + } + } + + /// Get all available platforms + fn all() -> Vec { + vec![AiPlatform::Cursor, AiPlatform::Claude, AiPlatform::Windsurf] + } +} + +/// Handle the `ai rules` command +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 { + // Interactive mode - prompt user to select platform + select_platform_interactive()? + }; + + // Write the rules file + write_rules_file(platform, working_directory)?; + + println!( + "✓ Successfully generated {} rules at: {}", + 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 +fn write_rules_file(platform: AiPlatform, working_directory: &Path) -> Result<()> { + let file_path = working_directory.join(platform.file_path()); + + // Check if file already exists + if file_path.exists() { + let overwrite = Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt(format!( + "File {} already exists. Overwrite?", + platform.file_path() + )) + .default(false) + .interact() + .context("Failed to get confirmation")?; + + if !overwrite { + println!("Aborted."); + return Ok(()); + } + } + + // 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 the content + fs::write(&file_path, AI_RULES_CONTENT) + .context(format!("Failed to write file: {}", file_path.display()))?; + + Ok(()) +} diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs index e12dd7c1f..350bdb7dd 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,27 @@ 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"])] + pub cursor: bool, + + /// Generate rules for Claude Code + #[arg(long, conflicts_with_all = ["cursor", "windsurf"])] + pub claude: bool, + + /// Generate rules for Windsurf AI + #[arg(long, conflicts_with_all = ["cursor", "claude"])] + pub windsurf: 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) + } + }, } } From e0e07541e75c53c9007397e1b23aa01e3534b8d0 Mon Sep 17 00:00:00 2001 From: dcodesdev <101001810+dcodesdev@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:45:17 +0300 Subject: [PATCH 2/5] Claude edge case fixed --- ai-rules.md | 4 --- cargo-shuttle/src/ai.rs | 70 ++++++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/ai-rules.md b/ai-rules.md index a44a5ddf2..b648dc900 100644 --- a/ai-rules.md +++ b/ai-rules.md @@ -1,7 +1,3 @@ ---- -alwaysApply: true ---- - # Shuttle Development Rules ## Core Setup diff --git a/cargo-shuttle/src/ai.rs b/cargo-shuttle/src/ai.rs index 3d34009d9..6418ce5ad 100644 --- a/cargo-shuttle/src/ai.rs +++ b/cargo-shuttle/src/ai.rs @@ -55,13 +55,28 @@ pub fn handle_ai_rules(args: &AiRulesArgs, working_directory: &Path) -> Result<( }; // Write the rules file - write_rules_file(platform, working_directory)?; - - println!( - "✓ Successfully generated {} rules at: {}", - platform.display_name(), - platform.file_path() - ); + let file_path = working_directory.join(platform.file_path()); + let file_existed = file_path.exists(); + let should_append = matches!(platform, AiPlatform::Claude) && 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(()) } @@ -81,24 +96,30 @@ fn select_platform_interactive() -> Result { Ok(platforms[selection]) } -/// Write the rules file to the appropriate location -fn write_rules_file(platform: AiPlatform, working_directory: &Path) -> Result<()> { +/// 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 Claude platform, append to existing CLAUDE.md instead of overwriting + let should_append = matches!(platform, AiPlatform::Claude) && file_path.exists(); + // Check if file already exists if file_path.exists() { - let overwrite = Confirm::with_theme(&ColorfulTheme::default()) + let action = if should_append { "append to" } else { "overwrite" }; + let confirm = Confirm::with_theme(&ColorfulTheme::default()) .with_prompt(format!( - "File {} already exists. Overwrite?", - platform.file_path() + "File {} already exists. {} it?", + platform.file_path(), + action.chars().next().unwrap().to_uppercase().to_string() + &action[1..] )) .default(false) .interact() .context("Failed to get confirmation")?; - if !overwrite { + if !confirm { println!("Aborted."); - return Ok(()); + return Ok(false); } } @@ -108,9 +129,22 @@ fn write_rules_file(platform: AiPlatform, working_directory: &Path) -> Result<() .context(format!("Failed to create directory: {}", parent.display()))?; } - // Write the content - fs::write(&file_path, AI_RULES_CONTENT) - .context(format!("Failed to write file: {}", file_path.display()))?; + // Write or append the content + if should_append { + // For Claude, append the AI rules to existing CLAUDE.md + let existing_content = fs::read_to_string(&file_path) + .context(format!("Failed to read existing file: {}", file_path.display()))?; - Ok(()) + // 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) } From 6e29b458d227c88b8786de2e55db4c10744caa41 Mon Sep 17 00:00:00 2001 From: dcodesdev <101001810+dcodesdev@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:51:47 +0300 Subject: [PATCH 3/5] Gemini and Codex CLI added --- cargo-shuttle/src/ai.rs | 26 +++++++++++++++++++++----- cargo-shuttle/src/args.rs | 14 +++++++++++--- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/cargo-shuttle/src/ai.rs b/cargo-shuttle/src/ai.rs index 6418ce5ad..7d51bf34a 100644 --- a/cargo-shuttle/src/ai.rs +++ b/cargo-shuttle/src/ai.rs @@ -13,6 +13,8 @@ pub enum AiPlatform { Cursor, Claude, Windsurf, + Gemini, + Codex, } impl AiPlatform { @@ -22,6 +24,8 @@ impl AiPlatform { AiPlatform::Cursor => ".cursor/rules/shuttle.mdc", AiPlatform::Claude => "CLAUDE.md", AiPlatform::Windsurf => ".windsurf/rules/shuttle.md", + AiPlatform::Gemini => "GEMINI.md", + AiPlatform::Codex => "AGENTS.md", } } @@ -31,12 +35,20 @@ impl AiPlatform { 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] + vec![ + AiPlatform::Cursor, + AiPlatform::Claude, + AiPlatform::Windsurf, + AiPlatform::Gemini, + AiPlatform::Codex, + ] } } @@ -49,6 +61,10 @@ pub fn handle_ai_rules(args: &AiRulesArgs, working_directory: &Path) -> Result<( 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()? @@ -57,7 +73,7 @@ pub fn handle_ai_rules(args: &AiRulesArgs, working_directory: &Path) -> Result<( // 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) && file_existed; + let should_append = matches!(platform, AiPlatform::Claude | AiPlatform::Gemini | AiPlatform::Codex) && file_existed; let was_written = write_rules_file(platform, working_directory)?; @@ -101,8 +117,8 @@ fn select_platform_interactive() -> Result { fn write_rules_file(platform: AiPlatform, working_directory: &Path) -> Result { let file_path = working_directory.join(platform.file_path()); - // For Claude platform, append to existing CLAUDE.md instead of overwriting - let should_append = matches!(platform, AiPlatform::Claude) && file_path.exists(); + // 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() { @@ -131,7 +147,7 @@ fn write_rules_file(platform: AiPlatform, working_directory: &Path) -> Result Date: Thu, 23 Oct 2025 12:20:49 +0300 Subject: [PATCH 4/5] cargo fmt --- cargo-shuttle/src/ai.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/cargo-shuttle/src/ai.rs b/cargo-shuttle/src/ai.rs index 7d51bf34a..f76c6882a 100644 --- a/cargo-shuttle/src/ai.rs +++ b/cargo-shuttle/src/ai.rs @@ -73,7 +73,10 @@ pub fn handle_ai_rules(args: &AiRulesArgs, working_directory: &Path) -> Result<( // 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 should_append = matches!( + platform, + AiPlatform::Claude | AiPlatform::Gemini | AiPlatform::Codex + ) && file_existed; let was_written = write_rules_file(platform, working_directory)?; @@ -118,11 +121,18 @@ fn write_rules_file(platform: AiPlatform, working_directory: &Path) -> Result Result Date: Thu, 23 Oct 2025 12:24:51 +0300 Subject: [PATCH 5/5] comments resolved --- cargo-shuttle/src/ai.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cargo-shuttle/src/ai.rs b/cargo-shuttle/src/ai.rs index f76c6882a..d2e4404f2 100644 --- a/cargo-shuttle/src/ai.rs +++ b/cargo-shuttle/src/ai.rs @@ -52,7 +52,8 @@ impl AiPlatform { } } -/// Handle the `ai rules` command +/// 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 { @@ -137,7 +138,12 @@ fn write_rules_file(platform: AiPlatform, working_directory: &Path) -> Result()) + .unwrap_or_default() + + &action[1..] )) .default(false) .interact()