Skip to content
Merged
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
159 changes: 152 additions & 7 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,15 +767,48 @@ fn run_claude_md_mode(global: bool, verbose: u8) -> Result<()> {

if path.exists() {
let existing = fs::read_to_string(&path)?;
// upsert_rtk_block handles all 4 cases: add, update, unchanged, malformed
let (new_content, action) = upsert_rtk_block(&existing, RTK_INSTRUCTIONS);

if existing.contains("<!-- rtk-instructions") {
println!("✅ {} already contains rtk instructions", path.display());
return Ok(());
}
match action {
RtkBlockUpsert::Added => {
fs::write(&path, new_content)?;
println!("✅ Added rtk instructions to existing {}", path.display());
}
RtkBlockUpsert::Updated => {
fs::write(&path, new_content)?;
println!("✅ Updated rtk instructions in {}", path.display());
}
RtkBlockUpsert::Unchanged => {
println!(
"✅ {} already contains up-to-date rtk instructions",
path.display()
);
return Ok(());
}
RtkBlockUpsert::Malformed => {
eprintln!(
"⚠️ Warning: Found '<!-- rtk-instructions' without closing marker in {}",
path.display()
);

let new_content = format!("{}\n\n{}", existing.trim(), RTK_INSTRUCTIONS);
fs::write(&path, new_content)?;
println!("✅ Added rtk instructions to existing {}", path.display());
if let Some((line_num, _)) = existing
.lines()
.enumerate()
.find(|(_, line)| line.contains("<!-- rtk-instructions"))
{
eprintln!(" Location: line {}", line_num + 1);
}

eprintln!(" Action: Manually remove the incomplete block, then re-run:");
if global {
eprintln!(" rtk init -g --claude-md");
} else {
eprintln!(" rtk init --claude-md");
}
return Ok(());
}
}
} else {
fs::write(&path, RTK_INSTRUCTIONS)?;
println!("✅ Created {} with rtk instructions", path.display());
Expand All @@ -790,6 +823,69 @@ fn run_claude_md_mode(global: bool, verbose: u8) -> Result<()> {
Ok(())
}

// --- upsert_rtk_block: idempotent RTK block management ---

#[derive(Debug, Clone, Copy, PartialEq)]
enum RtkBlockUpsert {
/// No existing block found — appended new block
Added,
/// Existing block found with different content — replaced
Updated,
/// Existing block found with identical content — no-op
Unchanged,
/// Opening marker found without closing marker — not safe to rewrite
Malformed,
}

/// Insert or replace the RTK instructions block in `content`.
///
/// Returns `(new_content, action)` describing what happened.
/// The caller decides whether to write `new_content` based on `action`.
fn upsert_rtk_block(content: &str, block: &str) -> (String, RtkBlockUpsert) {
let start_marker = "<!-- rtk-instructions";
let end_marker = "<!-- /rtk-instructions -->";

if let Some(start) = content.find(start_marker) {
if let Some(relative_end) = content[start..].find(end_marker) {
let end = start + relative_end;
let end_pos = end + end_marker.len();
let current_block = content[start..end_pos].trim();
let desired_block = block.trim();

if current_block == desired_block {
return (content.to_string(), RtkBlockUpsert::Unchanged);
}

// Replace stale block with desired block
let before = content[..start].trim_end();
let after = content[end_pos..].trim_start();

let result = match (before.is_empty(), after.is_empty()) {
(true, true) => desired_block.to_string(),
(true, false) => format!("{desired_block}\n\n{after}"),
(false, true) => format!("{before}\n\n{desired_block}"),
(false, false) => format!("{before}\n\n{desired_block}\n\n{after}"),
};

return (result, RtkBlockUpsert::Updated);
}

// Opening marker without closing marker — malformed
return (content.to_string(), RtkBlockUpsert::Malformed);
}

// No existing block — append
let trimmed = content.trim();
if trimmed.is_empty() {
(block.to_string(), RtkBlockUpsert::Added)
} else {
(
format!("{trimmed}\n\n{}", block.trim()),
RtkBlockUpsert::Added,
)
}
}

/// Patch CLAUDE.md: add @RTK.md, migrate if old block exists
fn patch_claude_md(path: &Path, verbose: u8) -> Result<bool> {
let mut content = if path.exists() {
Expand Down Expand Up @@ -1103,6 +1199,55 @@ More content"#;
assert!(RTK_INSTRUCTIONS.len() > 4000);
}

// --- upsert_rtk_block tests ---

#[test]
fn test_upsert_rtk_block_appends_when_missing() {
let input = "# Team instructions";
let (content, action) = upsert_rtk_block(input, RTK_INSTRUCTIONS);
assert_eq!(action, RtkBlockUpsert::Added);
assert!(content.contains("# Team instructions"));
assert!(content.contains("<!-- rtk-instructions"));
}

#[test]
fn test_upsert_rtk_block_updates_stale_block() {
let input = r#"# Team instructions

<!-- rtk-instructions v1 -->
OLD RTK CONTENT
<!-- /rtk-instructions -->

More notes
"#;

let (content, action) = upsert_rtk_block(input, RTK_INSTRUCTIONS);
assert_eq!(action, RtkBlockUpsert::Updated);
assert!(!content.contains("OLD RTK CONTENT"));
assert!(content.contains("rtk cargo test")); // from current RTK_INSTRUCTIONS
assert!(content.contains("# Team instructions"));
assert!(content.contains("More notes"));
}

#[test]
fn test_upsert_rtk_block_noop_when_already_current() {
let input = format!(
"# Team instructions\n\n{}\n\nMore notes\n",
RTK_INSTRUCTIONS
);
let (content, action) = upsert_rtk_block(&input, RTK_INSTRUCTIONS);
assert_eq!(action, RtkBlockUpsert::Unchanged);
assert_eq!(content, input);
}

#[test]
fn test_upsert_rtk_block_detects_malformed_block() {
let input = "<!-- rtk-instructions v2 -->\npartial";
let (content, action) = upsert_rtk_block(input, RTK_INSTRUCTIONS);
assert_eq!(action, RtkBlockUpsert::Malformed);
assert_eq!(content, input);
}

#[test]
fn test_init_is_idempotent() {
let temp = TempDir::new().unwrap();
Expand Down
1 change: 0 additions & 1 deletion src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,5 +395,4 @@ mod tests {
let result = truncate(cjk, 6);
assert!(result.ends_with("..."));
}

}
Loading