Skip to content

Commit 14efb7f

Browse files
feat: add migrate command for agents.md standard layout
- Add 'ai-rules migrate' subcommand with --nested-depth, --dry-run, --force - Move ai-rules/skills and ai-rules/commands to .agents/, write root AGENTS.md - Clean generated files, purge ai-rules/, update .gitignore - One-way migration; prompts for confirmation unless --force or --dry-run - Add docs/migration.md and README section Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 00af603 commit 14efb7f

9 files changed

Lines changed: 495 additions & 2 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ curl -fsSL https://raw.githubusercontent.com/block/ai-rules/main/scripts/install
6161
| `ai-rules generate` | Generate rules for AI coding agents |
6262
| `ai-rules status` | Show sync status of AI rules |
6363
| `ai-rules clean` | Remove all generated files |
64+
| `ai-rules migrate` | Migrate from ai-rules/ layout to agents.md standard (one-way) |
6465
| `ai-rules list-agents` | List all supported agents |
6566

6667
### Common Options
@@ -89,6 +90,10 @@ AMP, Claude Code, Cline, Codex, Copilot, Cursor, Firebender, Gemini, Goose, Kilo
8990
9091
See [Supported Agents](docs/agents.md) for detailed compatibility information.
9192
93+
## Migration to agents.md standard
94+
95+
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.
96+
9297
## Documentation
9398

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

101107
## Development
102108

docs/migration.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Migration to agents.md standard
2+
3+
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.
4+
5+
## Prerequisites
6+
7+
- Your project has an `ai-rules/` directory (created by `ai-rules init` or manually).
8+
- **Back up** the `ai-rules/` directory if you want to keep the original layout; migration cannot be automatically reverted.
9+
10+
## Command options
11+
12+
| Option | Description |
13+
|--------|-------------|
14+
| `--nested-depth <N>` | Maximum nested directory depth to traverse (0 = current directory only). Same precedence as other commands: CLI overrides config file. |
15+
| `--dry-run` | Print what would be done without writing or deleting. **Recommended first step.** |
16+
| `--force` | Skip the confirmation prompt and run migration. |
17+
18+
## Confirmation behavior
19+
20+
- **With `--dry-run`**: No confirmation; the command only prints which directories would be migrated and what actions would be performed.
21+
- **With `--force`**: No confirmation; migration runs immediately.
22+
- **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.
23+
24+
**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.
25+
26+
## What is written where
27+
28+
| Outcome | Description |
29+
|--------|-------------|
30+
| **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). |
31+
| **`.agents/skills/`** | The directory `ai-rules/skills/` is **moved** here. The whole tree is relocated; nothing is left under `ai-rules/`. |
32+
| **`.agents/commands/`** | The directory `ai-rules/commands/` is **moved** here. Same as skills. |
33+
| **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. |
34+
| **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. |
35+
36+
## What is removed
37+
38+
- **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`.
39+
- **`ai-rules/` directory**: After moving content out and running the equivalent of clean, the entire `ai-rules/` directory is deleted.
40+
41+
## .gitignore behavior
42+
43+
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.
44+
45+
## What is not migrated
46+
47+
- **`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.
48+
- **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.
49+
50+
## Nested / monorepo usage
51+
52+
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`).
53+
54+
## After migration
55+
56+
- 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).
57+
- Tools that read `AGENTS.md` / `AGENTS(S).md` and `.agents/` (e.g. [agents.md](https://agents.md/)) can use the new layout directly.

src/cli/args.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub enum Commands {
3232
Status(StatusArgs),
3333
/// Clean up generated files
3434
Clean(CleanArgs),
35+
/// Migrate from ai-rules/ layout to agents.md standard (one-way)
36+
Migrate(MigrateArgs),
3537
/// List all supported coding agents
3638
ListAgents,
3739
}
@@ -113,6 +115,24 @@ pub struct CleanArgs {
113115
pub nested_depth_args: NestedDepthArgs,
114116
}
115117

118+
#[derive(Args)]
119+
#[command(after_help = "Examples:
120+
ai-rules migrate # Migrate current directory only (nested_depth 0)
121+
ai-rules migrate --nested-depth 2 # Migrate ai-rules/ in nested directories
122+
123+
Migrates from ai-rules/ layout to agents.md standard: writes AGENTS.md at project root,
124+
moves ai-rules/skills and ai-rules/commands (and other ai-rules subdirs) into .agents/,
125+
removes generated files and purges the ai-rules/ directory. One-way. Prompts for confirmation
126+
unless --force or --dry-run. Run only when ready to adopt the standard. Use --dry-run first.")]
127+
pub struct MigrateArgs {
128+
#[command(flatten)]
129+
pub nested_depth_args: NestedDepthArgs,
130+
#[arg(long, help = "Print what would be done without writing or deleting")]
131+
pub dry_run: bool,
132+
#[arg(long, help = "Skip confirmation prompt and run migration (default: prompt before migrating)")]
133+
pub force: bool,
134+
}
135+
116136
#[derive(Debug, Clone)]
117137
pub struct ResolvedGenerateArgs {
118138
pub agents: Option<Vec<String>>,

src/cli/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ mod tests;
66

77
pub use args::*;
88

9-
use crate::commands::{run_clean, run_generate, run_init, run_list_agents, run_status};
9+
use crate::commands::{run_clean, run_generate, run_init, run_list_agents, run_migrate, run_status};
1010
use crate::config;
1111
use clap::Parser;
1212

@@ -43,6 +43,10 @@ pub fn run_cli() -> anyhow::Result<()> {
4343
let nested_depth = args.nested_depth_args.with_config(config.as_ref());
4444
run_clean(&current_dir, nested_depth, use_claude_skills)
4545
}
46+
Some(Commands::Migrate(args)) => {
47+
let nested_depth = args.nested_depth_args.with_config(config.as_ref());
48+
run_migrate(&current_dir, nested_depth, args.dry_run, args.force)
49+
}
4650
Some(Commands::ListAgents) => run_list_agents(use_claude_skills),
4751
None => {
4852
// If no command is provided and --summary is not used, show help

src/commands/migrate.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use crate::operations;
2+
use crate::utils::file_utils;
3+
use crate::utils::prompt_utils::prompt_yes_no;
4+
use anyhow::Result;
5+
use std::path::Path;
6+
7+
pub fn run_migrate(
8+
current_dir: &Path,
9+
nested_depth: usize,
10+
dry_run: bool,
11+
force: bool,
12+
) -> Result<()> {
13+
// Discover all directories that would be migrated
14+
let mut to_migrate = Vec::new();
15+
file_utils::traverse_project_directories(current_dir, nested_depth, 0, &mut |dir| {
16+
if operations::migrate::should_migrate(dir) {
17+
to_migrate.push(dir.to_path_buf());
18+
}
19+
Ok(())
20+
})?;
21+
22+
if to_migrate.is_empty() {
23+
println!("No ai-rules/ directories found to migrate.");
24+
return Ok(());
25+
}
26+
27+
if dry_run {
28+
println!("Dry run: would migrate {} project(s) to the agents.md standard:", to_migrate.len());
29+
for path in &to_migrate {
30+
println!(" {}", path.display());
31+
}
32+
} else if !force {
33+
println!(
34+
"This will migrate {} project(s) to the agents.md standard and remove ai-rules/ directories. This cannot be undone.",
35+
to_migrate.len()
36+
);
37+
if !prompt_yes_no("Proceed with migration?")? {
38+
println!("Migration cancelled.");
39+
return Ok(());
40+
}
41+
}
42+
43+
let mut results = Vec::new();
44+
for dir in &to_migrate {
45+
let result = operations::migrate::run_migration_for_dir(dir, dry_run)?;
46+
results.push(result);
47+
}
48+
49+
// Summary
50+
let migrated: Vec<_> = results.iter().filter(|r| !r.skipped).collect();
51+
if migrated.is_empty() && !dry_run {
52+
println!("No directories were migrated.");
53+
} else if dry_run {
54+
for r in &results {
55+
if !r.skipped {
56+
println!(" {}: would {}", r.path.display(), r.actions.join(", "));
57+
}
58+
}
59+
} else {
60+
for r in &migrated {
61+
println!("Migrated {}: {}", r.path.display(), r.actions.join(", "));
62+
}
63+
}
64+
65+
Ok(())
66+
}

src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ mod clean;
22
mod generate;
33
mod init;
44
mod list_agents;
5+
mod migrate;
56
mod status;
67

78
pub use clean::run_clean;
89
pub use generate::run_generate;
910
pub use init::run_init;
1011
pub use list_agents::run_list_agents;
12+
pub use migrate::run_migrate;
1113
pub use status::run_status;
1214

1315
#[cfg(test)]

src/operations/gitignore_updater.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ fn collect_all_gitignore_patterns(
6464
}
6565
}
6666

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

108+
/// Removes the "# AI Rules - Generated Files" / "# End AI Rules" block from a .gitignore file at the given path.
109+
/// No-op if the file does not exist or the section is not present.
110+
pub fn remove_ai_rules_section_from_file(gitignore_path: &Path) -> Result<()> {
111+
if !gitignore_path.exists() {
112+
return Ok(());
113+
}
114+
let content = fs::read_to_string(gitignore_path)?;
115+
let new_content = remove_ai_rules_section(content.clone());
116+
if new_content != content {
117+
fs::write(gitignore_path, new_content)?;
118+
}
119+
Ok(())
120+
}
121+
108122
pub fn remove_gitignore_section(current_dir: &Path, registry: &AgentToolRegistry) -> Result<()> {
109123
let gitignore_path = current_dir.join(".gitignore");
110124

0 commit comments

Comments
 (0)