Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`ai-rules migrate`** – One-way migration from the ai-rules–managed layout to the [agents.md](https://agents.md/) standard: writes root `AGENTS.md`, moves `ai-rules/skills` and `ai-rules/commands` into `.agents/`, removes generated files, and purges the `ai-rules/` directory. Supports `--nested-depth`, `--dry-run`, and `--force`; prompts for confirmation unless `--force` or `--dry-run`. See [Migration guide](docs/migration.md).

## [1.5.0] - (see GitHub releases)
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ curl -fsSL https://raw.githubusercontent.com/block/ai-rules/main/scripts/install
| `ai-rules generate` | Generate rules for AI coding agents |
| `ai-rules status` | Show sync status of AI rules |
| `ai-rules clean` | Remove all generated files |
| `ai-rules migrate` | Migrate from ai-rules/ layout to agents.md standard (one-way) |
| `ai-rules list-agents` | List all supported agents |

### Common Options
Expand Down Expand Up @@ -89,6 +90,10 @@ AMP, Claude Code, Cline, Codex, Copilot, Cursor, Firebender, Gemini, Goose, Kilo

See [Supported Agents](docs/agents.md) for detailed compatibility information.

## Migration to agents.md standard

You can migrate from the ai-rules–managed layout to the [agents.md](https://agents.md/) standard: a single `AGENTS.md` at project root, skills in `.agents/skills/`, and commands in `.agents/commands/`. Run `ai-rules migrate` to write root `AGENTS.md`, move `ai-rules/skills` and `ai-rules/commands` into `.agents/`, remove generated files, and purge the `ai-rules/` directory. **This is one-way**; after migrating, the project no longer uses ai-rules generate/clean/status for that content. Use `--dry-run` first to see what would be done, then run without it (and confirm) or with `--force` to migrate. See [Migration guide](docs/migration.md) for details.

## Documentation

- [Configuration](docs/configuration.md) - Config file options and precedence
Expand All @@ -97,6 +102,7 @@ See [Supported Agents](docs/agents.md) for detailed compatibility information.
- [MCP Configuration](docs/mcp.md) - Model Context Protocol setup
- [Commands and Skills](docs/commands-and-skills.md) - Custom commands and skills
- [Project Structure](docs/project-structure.md) - Example project layouts
- [Migration to agents.md](docs/migration.md) - One-way migration from ai-rules/ layout

## Development

Expand Down
57 changes: 57 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Migration to agents.md standard

This guide describes how to migrate from the ai-rules–managed layout to the [agents.md](https://agents.md/) standard layout using `ai-rules migrate`. The migration is **one-way**: after migrating, the project no longer uses `ai-rules generate`, `ai-rules clean`, or `ai-rules status` for that content.

## Prerequisites

- Your project has an `ai-rules/` directory (created by `ai-rules init` or manually).
- **Back up** the `ai-rules/` directory if you want to keep the original layout; migration cannot be automatically reverted.

## Command options

| Option | Description |
|--------|-------------|
| `--nested-depth <N>` | Maximum nested directory depth to traverse (0 = current directory only). Same precedence as other commands: CLI overrides config file. |
| `--dry-run` | Print what would be done without writing or deleting. **Recommended first step.** |
| `--force` | Skip the confirmation prompt and run migration. |

## Confirmation behavior

- **With `--dry-run`**: No confirmation; the command only prints which directories would be migrated and what actions would be performed.
- **With `--force`**: No confirmation; migration runs immediately.
- **Without `--dry-run` or `--force`**: The command lists how many project(s) would be migrated, shows a warning that the change cannot be undone, and prompts: *"Proceed with migration? (y/N)"*. If you answer no, nothing is changed.

**Recommendation:** Run `ai-rules migrate --dry-run` first to see the list of directories and actions, then run `ai-rules migrate` (and confirm) or `ai-rules migrate --force` to perform the migration.

## What is written where

| Outcome | Description |
|--------|-------------|
| **Root `AGENTS.md`** | A single self-contained markdown file at the project root. In **symlink mode** (single `ai-rules/AGENTS.md` with no YAML frontmatter), its content is a copy of that file. In **standard mode** (multiple `.md` rules with frontmatter), content is the inlined combination of all rules (no `@` file references). |
| **`.agents/skills/`** | The directory `ai-rules/skills/` is **moved** here. The whole tree is relocated; nothing is left under `ai-rules/`. |
| **`.agents/commands/`** | The directory `ai-rules/commands/` is **moved** here. Same as skills. |
| **Other `ai-rules/` subdirs** | Any other non-generated directories under `ai-rules/` (e.g. custom dirs) are moved into `.agents/` with the same name so `ai-rules/` can be fully removed. |
| **Root `.mcp.json`** | If `ai-rules/mcp.json` existed, it is moved to the project root as `.mcp.json` so tools that read MCP from root (e.g. Claude Code) can use it. |

## What is removed

- **Generated files and symlinks**: All outputs previously created by `ai-rules generate` are removed (e.g. `CLAUDE.md`, `GEMINI.md`, `.cursor/rules/*.mdc`, `firebender.json`, command/skill symlinks in `.claude`, `.cursor`, `.agents`, etc.), using the same logic as `ai-rules clean`.
- **`ai-rules/` directory**: After moving content out and running the equivalent of clean, the entire `ai-rules/` directory is deleted.

## .gitignore behavior

The block between `# AI Rules - Generated Files` and `# End AI Rules` is removed from the project’s `.gitignore`. No new entries are added for `.agents/`; the new layout is intended to be committed.

## What is not migrated

- **`ai-rules-config.yaml`** and **`ai-rules/firebender-overlay.json`** are not copied; they become obsolete. If you use Firebender or other tools that relied on generated paths, reconfigure them to point at root `AGENTS.md` if desired.
- **Cursor `.cursor/rules/*.mdc`** and **Firebender `firebender.json`** are not regenerated from the new layout; those tools would need to be pointed at root `AGENTS.md` manually if you want to use them with the new layout.

## Nested / monorepo usage

Migration runs per directory. Each directory that has its own `ai-rules/` gets its own root `AGENTS.md` and `.agents/` at that directory (e.g. `frontend/AGENTS.md`, `frontend/.agents/`). Use `--nested-depth` to control how many levels are traversed (same as `generate` and `clean`).

## After migration

- The project no longer uses ai-rules for that content. To use ai-rules again you would need to recreate `ai-rules/` and run `generate` (not automated).
- Tools that read `AGENTS.md` / `AGENTS(S).md` and `.agents/` (e.g. [agents.md](https://agents.md/)) can use the new layout directly.
20 changes: 20 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub enum Commands {
Status(StatusArgs),
/// Clean up generated files
Clean(CleanArgs),
/// Migrate from ai-rules/ layout to agents.md standard (one-way)
Migrate(MigrateArgs),
/// List all supported coding agents
ListAgents,
}
Expand Down Expand Up @@ -113,6 +115,24 @@ pub struct CleanArgs {
pub nested_depth_args: NestedDepthArgs,
}

#[derive(Args)]
#[command(after_help = "Examples:
ai-rules migrate # Migrate current directory only (nested_depth 0)
ai-rules migrate --nested-depth 2 # Migrate ai-rules/ in nested directories

Migrates from ai-rules/ layout to agents.md standard: writes AGENTS.md at project root,
moves ai-rules/skills and ai-rules/commands (and other ai-rules subdirs) into .agents/,
removes generated files and purges the ai-rules/ directory. One-way. Prompts for confirmation
unless --force or --dry-run. Run only when ready to adopt the standard. Use --dry-run first.")]
pub struct MigrateArgs {
#[command(flatten)]
pub nested_depth_args: NestedDepthArgs,
#[arg(long, help = "Print what would be done without writing or deleting")]
pub dry_run: bool,
#[arg(long, help = "Skip confirmation prompt and run migration (default: prompt before migrating)")]
pub force: bool,
}

#[derive(Debug, Clone)]
pub struct ResolvedGenerateArgs {
pub agents: Option<Vec<String>>,
Expand Down
6 changes: 5 additions & 1 deletion src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ mod tests;

pub use args::*;

use crate::commands::{run_clean, run_generate, run_init, run_list_agents, run_status};
use crate::commands::{run_clean, run_generate, run_init, run_list_agents, run_migrate, run_status};
use crate::config;
use clap::Parser;

Expand Down Expand Up @@ -43,6 +43,10 @@ pub fn run_cli() -> anyhow::Result<()> {
let nested_depth = args.nested_depth_args.with_config(config.as_ref());
run_clean(&current_dir, nested_depth, use_claude_skills)
}
Some(Commands::Migrate(args)) => {
let nested_depth = args.nested_depth_args.with_config(config.as_ref());
run_migrate(&current_dir, nested_depth, args.dry_run, args.force)
}
Some(Commands::ListAgents) => run_list_agents(use_claude_skills),
None => {
// If no command is provided and --summary is not used, show help
Expand Down
66 changes: 66 additions & 0 deletions src/commands/migrate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use crate::operations;
use crate::utils::file_utils;
use crate::utils::prompt_utils::prompt_yes_no;
use anyhow::Result;
use std::path::Path;

pub fn run_migrate(
current_dir: &Path,
nested_depth: usize,
dry_run: bool,
force: bool,
) -> Result<()> {
// Discover all directories that would be migrated
let mut to_migrate = Vec::new();
file_utils::traverse_project_directories(current_dir, nested_depth, 0, &mut |dir| {
if operations::migrate::should_migrate(dir) {
to_migrate.push(dir.to_path_buf());
}
Ok(())
})?;

if to_migrate.is_empty() {
println!("No ai-rules/ directories found to migrate.");
return Ok(());
}

if dry_run {
println!("Dry run: would migrate {} project(s) to the agents.md standard:", to_migrate.len());
for path in &to_migrate {
println!(" {}", path.display());
}
} else if !force {
println!(
"This will migrate {} project(s) to the agents.md standard and remove ai-rules/ directories. This cannot be undone.",
to_migrate.len()
);
if !prompt_yes_no("Proceed with migration?")? {
println!("Migration cancelled.");
return Ok(());
}
}

let mut results = Vec::new();
for dir in &to_migrate {
let result = operations::migrate::run_migration_for_dir(dir, dry_run)?;
results.push(result);
}

// Summary
let migrated: Vec<_> = results.iter().filter(|r| !r.skipped).collect();
if migrated.is_empty() && !dry_run {
println!("No directories were migrated.");
} else if dry_run {
for r in &results {
if !r.skipped {
println!(" {}: would {}", r.path.display(), r.actions.join(", "));
}
}
} else {
for r in &migrated {
println!("Migrated {}: {}", r.path.display(), r.actions.join(", "));
}
}

Ok(())
}
2 changes: 2 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ mod clean;
mod generate;
mod init;
mod list_agents;
mod migrate;
mod status;

pub use clean::run_clean;
pub use generate::run_generate;
pub use init::run_init;
pub use list_agents::run_list_agents;
pub use migrate::run_migrate;
pub use status::run_status;

#[cfg(test)]
Expand Down
16 changes: 15 additions & 1 deletion src/operations/gitignore_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ fn collect_all_gitignore_patterns(
}
}

fn remove_ai_rules_section(content: String) -> String {
pub(crate) fn remove_ai_rules_section(content: String) -> String {
if let Some(start) = content.find("# AI Rules - Generated Files") {
if let Some(end) = content.find("# End AI Rules") {
let mut result = content;
Expand Down Expand Up @@ -105,6 +105,20 @@ fn update_gitignore(current_dir: &Path, patterns: Vec<String>) -> Result<()> {
Ok(())
}

/// Removes the "# AI Rules - Generated Files" / "# End AI Rules" block from a .gitignore file at the given path.
/// No-op if the file does not exist or the section is not present.
pub fn remove_ai_rules_section_from_file(gitignore_path: &Path) -> Result<()> {
if !gitignore_path.exists() {
return Ok(());
}
let content = fs::read_to_string(gitignore_path)?;
let new_content = remove_ai_rules_section(content.clone());
if new_content != content {
fs::write(gitignore_path, new_content)?;
}
Ok(())
}

pub fn remove_gitignore_section(current_dir: &Path, registry: &AgentToolRegistry) -> Result<()> {
let gitignore_path = current_dir.join(".gitignore");

Expand Down
Loading
Loading