Skip to content

Commit 0bc3c74

Browse files
committed
feat: add support for JetBrains AI Assistant and Junie agents
1 parent 316fca3 commit 0bc3c74

6 files changed

Lines changed: 339 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ CLI tool to manage AI rules across different AI coding agents. Standardize and d
44

55
## Features
66

7-
- **Multi-Agent Support** - Generate rules for 11 AI coding agents from a single source
7+
- **Multi-Agent Support** - Generate rules for 13 AI coding agents from a single source
88
- **Sync Management** - Track and maintain consistency across all generated rule files
99
- **Selective Generation** - Generate rules for specific agents only
1010
- **MCP Support** - Generate Model Context Protocol configurations for compatible agents
@@ -85,7 +85,7 @@ See [Configuration Guide](docs/configuration.md) for all options.
8585
8686
## Supported Agents
8787
88-
AMP, Claude Code, Cline, Codex, Copilot, Cursor, Firebender, Gemini, Goose, Kilocode, Roo
88+
AMP, Claude Code, Cline, Codex, Copilot, Cursor, Firebender, Gemini, Goose, JetBrains AI Assistant, JetBrains Junie, Kilocode, Roo
8989
9090
See [Supported Agents](docs/agents.md) for detailed compatibility information.
9191

docs/agents.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
| **Firebender** | `firebender.json` | `firebender.json` (references `ai-rules/AGENTS.md`) | Embedded in `firebender.json` | Supports overlay files |
1414
| **Gemini** | `GEMINI.md` | `GEMINI.md` -> `ai-rules/AGENTS.md` | Embedded in `.gemini/settings.json` | |
1515
| **Goose** | `AGENTS.md` | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | |
16+
| **JetBrains AI Assistant** | `.aiassistant/rules/*.md` | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | Plain markdown, no frontmatter |
17+
| **JetBrains Junie** | `AGENTS.md` | `AGENTS.md` -> `ai-rules/AGENTS.md` | - | Native AGENTS.md support |
1618
| **Kilocode** | `.kilocode/rules/*.md` | `.kilocode/rules/AGENTS.md` -> `../../ai-rules/AGENTS.md` | - | |
1719
| **Roo** | `.roo/rules/*.md` | `.roo/rules/AGENTS.md` -> `../../ai-rules/AGENTS.md` | `.roo/mcp.json` | |
1820

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
use crate::agents::rule_generator::AgentRuleGenerator;
2+
use crate::constants::{AGENTS_MD_FILENAME, GENERATED_FILE_PREFIX, MD_EXTENSION};
3+
use crate::models::SourceFile;
4+
use crate::utils::file_utils::{
5+
check_agents_md_symlink, check_directory_exact_match, create_symlink_to_agents_md,
6+
ensure_trailing_newline,
7+
};
8+
use anyhow::Result;
9+
use std::collections::HashMap;
10+
use std::fs;
11+
use std::path::{Path, PathBuf};
12+
13+
pub struct JetbrainsAiAssistantGenerator;
14+
15+
fn get_rules_dir(current_dir: &Path) -> PathBuf {
16+
current_dir.join(".aiassistant").join("rules")
17+
}
18+
19+
impl AgentRuleGenerator for JetbrainsAiAssistantGenerator {
20+
fn name(&self) -> &str {
21+
"jetbrains-ai-assistant"
22+
}
23+
24+
fn clean(&self, current_dir: &Path) -> Result<()> {
25+
let rules_dir = get_rules_dir(current_dir);
26+
if rules_dir.exists() {
27+
fs::remove_dir_all(rules_dir)?;
28+
}
29+
let agent_md = current_dir.join(AGENTS_MD_FILENAME);
30+
if agent_md.exists() && agent_md.is_symlink() {
31+
fs::remove_file(agent_md)?;
32+
}
33+
Ok(())
34+
}
35+
36+
fn generate_agent_contents(
37+
&self,
38+
source_files: &[SourceFile],
39+
current_dir: &Path,
40+
) -> HashMap<PathBuf, String> {
41+
let mut agent_files = HashMap::new();
42+
43+
if source_files.is_empty() {
44+
return agent_files;
45+
}
46+
47+
let rules_dir = get_rules_dir(current_dir);
48+
49+
for source_file in source_files {
50+
let generated_file_name = format!(
51+
"{}{}.{}",
52+
GENERATED_FILE_PREFIX, source_file.base_file_name, MD_EXTENSION
53+
);
54+
55+
let file_path = rules_dir.join(generated_file_name);
56+
let content = ensure_trailing_newline(source_file.body.clone());
57+
agent_files.insert(file_path, content);
58+
}
59+
60+
agent_files
61+
}
62+
63+
fn check_agent_contents(
64+
&self,
65+
source_files: &[SourceFile],
66+
current_dir: &Path,
67+
) -> Result<bool> {
68+
let rules_dir = get_rules_dir(current_dir);
69+
70+
if source_files.is_empty() {
71+
return Ok(!rules_dir.exists());
72+
}
73+
74+
let expected_files = self.generate_agent_contents(source_files, current_dir);
75+
76+
check_directory_exact_match(&rules_dir, &expected_files)
77+
}
78+
79+
fn check_symlink(&self, current_dir: &Path) -> Result<bool> {
80+
let agents_md_path = current_dir.join(AGENTS_MD_FILENAME);
81+
check_agents_md_symlink(current_dir, &agents_md_path)
82+
}
83+
84+
fn gitignore_patterns(&self) -> Vec<String> {
85+
vec![".aiassistant/rules/".to_string()]
86+
}
87+
88+
fn generate_symlink(&self, current_dir: &Path) -> Result<Vec<PathBuf>> {
89+
let success = create_symlink_to_agents_md(current_dir, Path::new(AGENTS_MD_FILENAME))?;
90+
if success {
91+
Ok(vec![current_dir.join(AGENTS_MD_FILENAME)])
92+
} else {
93+
Ok(vec![])
94+
}
95+
}
96+
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use super::*;
101+
use crate::utils::test_utils::helpers::*;
102+
use tempfile::TempDir;
103+
104+
fn create_standard_test_source_file() -> SourceFile {
105+
create_test_source_file(
106+
"test",
107+
"Test rule",
108+
true,
109+
vec!["**/*.ts".to_string()],
110+
"test body",
111+
)
112+
}
113+
114+
#[test]
115+
fn test_name() {
116+
let generator = JetbrainsAiAssistantGenerator;
117+
assert_eq!(generator.name(), "jetbrains-ai-assistant");
118+
}
119+
120+
#[test]
121+
fn test_gitignore_patterns() {
122+
let generator = JetbrainsAiAssistantGenerator;
123+
let patterns = generator.gitignore_patterns();
124+
assert_eq!(patterns, vec![".aiassistant/rules/"]);
125+
}
126+
127+
#[test]
128+
fn test_generate_agent_contents_empty() {
129+
let generator = JetbrainsAiAssistantGenerator;
130+
let temp_dir = TempDir::new().unwrap();
131+
132+
let result = generator.generate_agent_contents(&[], temp_dir.path());
133+
134+
assert!(result.is_empty());
135+
}
136+
137+
#[test]
138+
fn test_generate_agent_contents() {
139+
let generator = JetbrainsAiAssistantGenerator;
140+
let temp_dir = TempDir::new().unwrap();
141+
let source_files = vec![
142+
create_test_source_file(
143+
"rule1",
144+
"First rule",
145+
true,
146+
vec!["**/*.ts".to_string()],
147+
"rule1 body",
148+
),
149+
create_test_source_file(
150+
"rule2",
151+
"Second rule",
152+
false,
153+
vec!["**/*.js".to_string()],
154+
"rule2 body",
155+
),
156+
];
157+
158+
let result = generator.generate_agent_contents(&source_files, temp_dir.path());
159+
160+
assert_eq!(result.len(), 2);
161+
162+
let expected_path1 = temp_dir
163+
.path()
164+
.join(".aiassistant/rules/ai-rules-generated-rule1.md");
165+
let expected_path2 = temp_dir
166+
.path()
167+
.join(".aiassistant/rules/ai-rules-generated-rule2.md");
168+
169+
// No frontmatter - just the body content
170+
let content1 = result.get(&expected_path1).unwrap();
171+
assert_eq!(content1, "rule1 body\n");
172+
173+
let content2 = result.get(&expected_path2).unwrap();
174+
assert_eq!(content2, "rule2 body\n");
175+
}
176+
177+
#[test]
178+
fn test_clean_non_existing_directory() {
179+
let generator = JetbrainsAiAssistantGenerator;
180+
let temp_dir = TempDir::new().unwrap();
181+
182+
let result = generator.clean(temp_dir.path());
183+
184+
assert!(result.is_ok());
185+
assert_file_not_exists(temp_dir.path(), ".aiassistant/rules");
186+
}
187+
188+
#[test]
189+
fn test_clean_existing_directory() {
190+
let generator = JetbrainsAiAssistantGenerator;
191+
let temp_dir = TempDir::new().unwrap();
192+
193+
create_file(
194+
temp_dir.path(),
195+
".aiassistant/rules/ai-rules-generated-test.md",
196+
"test content",
197+
);
198+
create_file(
199+
temp_dir.path(),
200+
".aiassistant/rules/ai-rules-generated-other.md",
201+
"other content",
202+
);
203+
assert_file_exists(
204+
temp_dir.path(),
205+
".aiassistant/rules/ai-rules-generated-test.md",
206+
);
207+
208+
let result = generator.clean(temp_dir.path());
209+
210+
assert!(result.is_ok());
211+
assert_file_not_exists(temp_dir.path(), ".aiassistant/rules");
212+
}
213+
214+
#[test]
215+
fn test_clean_removes_agents_md_symlink() {
216+
let generator = JetbrainsAiAssistantGenerator;
217+
let temp_dir = TempDir::new().unwrap();
218+
219+
create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Source content");
220+
221+
let result = generator.generate_symlink(temp_dir.path());
222+
assert!(result.is_ok());
223+
224+
let agents_md_path = temp_dir.path().join(AGENTS_MD_FILENAME);
225+
assert!(agents_md_path.is_symlink());
226+
227+
let result = generator.clean(temp_dir.path());
228+
assert!(result.is_ok());
229+
230+
assert!(!agents_md_path.exists());
231+
232+
assert_file_exists(temp_dir.path(), "ai-rules/AGENTS.md");
233+
}
234+
235+
#[test]
236+
fn test_check_empty_source_files_no_directory() {
237+
let generator = JetbrainsAiAssistantGenerator;
238+
let temp_dir = TempDir::new().unwrap();
239+
240+
let result = generator
241+
.check_agent_contents(&[], temp_dir.path())
242+
.unwrap();
243+
244+
assert!(result);
245+
}
246+
247+
#[test]
248+
fn test_check_empty_source_files_with_directory() {
249+
let generator = JetbrainsAiAssistantGenerator;
250+
let temp_dir = TempDir::new().unwrap();
251+
252+
create_file(
253+
temp_dir.path(),
254+
".aiassistant/rules/ai-rules-generated-stale.md",
255+
"stale content",
256+
);
257+
258+
let result = generator
259+
.check_agent_contents(&[], temp_dir.path())
260+
.unwrap();
261+
262+
assert!(!result);
263+
}
264+
265+
#[test]
266+
fn test_check_with_matching_files() {
267+
let generator = JetbrainsAiAssistantGenerator;
268+
let temp_dir = TempDir::new().unwrap();
269+
let source_file = create_standard_test_source_file();
270+
271+
create_file(
272+
temp_dir.path(),
273+
".aiassistant/rules/ai-rules-generated-test.md",
274+
"test body\n",
275+
);
276+
277+
let result = generator
278+
.check_agent_contents(&[source_file], temp_dir.path())
279+
.unwrap();
280+
281+
assert!(result);
282+
}
283+
284+
#[test]
285+
fn test_check_with_missing_files() {
286+
let generator = JetbrainsAiAssistantGenerator;
287+
let temp_dir = TempDir::new().unwrap();
288+
let source_file = create_standard_test_source_file();
289+
290+
let result = generator
291+
.check_agent_contents(&[source_file], temp_dir.path())
292+
.unwrap();
293+
294+
assert!(!result);
295+
}
296+
297+
#[test]
298+
fn test_check_with_incorrect_content() {
299+
let generator = JetbrainsAiAssistantGenerator;
300+
let temp_dir = TempDir::new().unwrap();
301+
let source_file = create_standard_test_source_file();
302+
303+
create_file(
304+
temp_dir.path(),
305+
".aiassistant/rules/ai-rules-generated-test.md",
306+
"wrong content",
307+
);
308+
309+
let result = generator
310+
.check_agent_contents(&[source_file], temp_dir.path())
311+
.unwrap();
312+
313+
assert!(!result);
314+
}
315+
316+
#[test]
317+
fn test_check_symlink_with_correct_symlink() {
318+
let generator = JetbrainsAiAssistantGenerator;
319+
let temp_dir = TempDir::new().unwrap();
320+
321+
create_file(temp_dir.path(), "ai-rules/AGENTS.md", "# Source content");
322+
323+
let result = generator.generate_symlink(temp_dir.path());
324+
assert!(result.is_ok());
325+
326+
let result = generator.check_symlink(temp_dir.path()).unwrap();
327+
assert!(result);
328+
}
329+
}

src/agents/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod external_commands_generator;
77
pub mod external_skills_generator;
88
pub mod firebender;
99
pub mod gemini;
10+
pub mod jetbrains_ai_assistant;
1011
pub mod mcp_generator;
1112
pub mod registry;
1213
pub mod roo;

src/agents/registry.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
use crate::agents::rule_generator::AgentRuleGenerator;
22
use crate::agents::{
33
amp::AmpGenerator, claude::ClaudeGenerator, codex::CodexGenerator, cursor::CursorGenerator,
4-
firebender::FirebenderGenerator, gemini::GeminiGenerator, roo::RooGenerator,
4+
firebender::FirebenderGenerator, gemini::GeminiGenerator,
5+
jetbrains_ai_assistant::JetbrainsAiAssistantGenerator, roo::RooGenerator,
56
single_file_based::SingleFileBasedGenerator,
67
};
78
use crate::constants::AGENTS_MD_FILENAME;
@@ -37,6 +38,8 @@ impl AgentToolRegistry {
3738
AGENTS_MD_FILENAME,
3839
)),
3940
Box::new(RooGenerator::new()),
41+
Box::new(JetbrainsAiAssistantGenerator),
42+
Box::new(SingleFileBasedGenerator::new("jetbrains-junie", AGENTS_MD_FILENAME)),
4043
];
4144

4245
for generator in generators {

0 commit comments

Comments
 (0)