use std::io::{self, IsTerminal, Write as IoWrite};
use std::path::PathBuf;
enum SyncResult {
Added(PathBuf),
Updated(PathBuf),
Unchanged(PathBuf),
Removed(PathBuf),
Skipped(PathBuf), Configured(String), Deconfigured(String), DryRun(String), }
impl SyncResult {
fn symbol(&self) -> &'static str {
match self {
Self::Added(_) | Self::Configured(_) => "✓",
Self::Updated(_) => "~",
Self::Unchanged(_) | Self::Skipped(_) => "·",
Self::Removed(_) | Self::Deconfigured(_) => "✗",
Self::DryRun(_) => "?",
}
}
fn tag(&self) -> &'static str {
match self {
Self::Added(_) => "added ",
Self::Updated(_) => "updated ",
Self::Unchanged(_) => "unchanged ",
Self::Removed(_) => "removed ",
Self::Skipped(_) => "skipped ",
Self::Configured(_) => "configured",
Self::Deconfigured(_) => "removed ",
Self::DryRun(_) => "dry-run ",
}
}
fn detail(&self) -> String {
match self {
Self::Added(p) | Self::Updated(p) | Self::Unchanged(p)
| Self::Removed(p) | Self::Skipped(p) => shorten_path(p),
Self::Configured(s) | Self::Deconfigured(s) | Self::DryRun(s) => s.clone(),
}
}
}
fn print_results(id: &str, results: &[SyncResult]) {
for r in results {
eprintln!(" {id:<10} {} {} {}", r.symbol(), r.tag(), r.detail());
}
}
struct Integration {
id: &'static str,
label: &'static str,
detect: fn() -> bool,
artifact: fn() -> PathBuf,
install: fn(dry_run: bool) -> Vec<SyncResult>,
uninstall: fn(dry_run: bool) -> Vec<SyncResult>,
}
static INTEGRATIONS: &[Integration] = &[
Integration {
id: "claude", label: "Claude Code",
detect: detect_claude, artifact: claude_artifact,
install: install_claude, uninstall: uninstall_claude,
},
Integration {
id: "gemini", label: "Gemini CLI",
detect: detect_gemini, artifact: gemini_artifact,
install: install_gemini, uninstall: uninstall_gemini,
},
Integration {
id: "codex", label: "Codex CLI",
detect: detect_codex, artifact: codex_artifact,
install: install_codex, uninstall: uninstall_codex,
},
Integration {
id: "opencode", label: "OpenCode",
detect: detect_opencode, artifact: opencode_artifact,
install: install_opencode, uninstall: uninstall_opencode,
},
Integration {
id: "cursor", label: "Cursor",
detect: detect_cursor, artifact: cursor_artifact,
install: install_cursor, uninstall: uninstall_cursor,
},
];
fn integration_names() -> String {
INTEGRATIONS.iter().map(|i| i.id).collect::<Vec<_>>().join(", ")
}
fn find_integration(id: &str) -> Option<&'static Integration> {
INTEGRATIONS.iter().find(|i| i.id == id)
}
fn detect_claude() -> bool { cmd_exists("claude") || dir_exists("~/.claude") }
fn detect_gemini() -> bool { cmd_exists("gemini") }
fn detect_codex() -> bool { cmd_exists("codex") }
fn detect_opencode() -> bool { cmd_exists("opencode") }
fn detect_cursor() -> bool { cmd_exists("cursor") || dir_exists("~/.cursor") }
fn claude_artifact() -> PathBuf { PathBuf::from(home()).join(".claude").join("transpile-hook.sh") }
fn gemini_artifact() -> PathBuf { skill_path("gemini") }
fn codex_artifact() -> PathBuf { skill_path("codex") }
fn opencode_artifact() -> PathBuf { skill_path("opencode") }
fn cursor_artifact() -> PathBuf { skill_path("cursor") }
fn install_claude(dry_run: bool) -> Vec<SyncResult> {
let mut out = Vec::new();
let hook = claude_artifact();
out.push(sync_file(&hook, CLAUDE_HOOK_SCRIPT, true, dry_run));
let settings = PathBuf::from(home()).join(".claude").join("settings.json");
if dry_run {
out.push(SyncResult::DryRun(format!(
"merge PostToolUse hook → {}",
shorten_path(&settings)
)));
} else {
out.push(merge_claude_hook(&hook, &settings));
}
out
}
fn uninstall_claude(dry_run: bool) -> Vec<SyncResult> {
let mut out = Vec::new();
let hook = claude_artifact();
out.push(remove_file(&hook, dry_run));
let settings = PathBuf::from(home()).join(".claude").join("settings.json");
if dry_run {
out.push(SyncResult::DryRun(format!(
"remove PostToolUse hook from {}",
shorten_path(&settings)
)));
} else {
out.push(strip_claude_hook(&settings));
}
if !dry_run {
let legacy = PathBuf::from(home()).join(".claude").join("commands").join("transpile.md");
if legacy.exists() { std::fs::remove_file(legacy).ok(); }
}
out
}
fn install_gemini(dry_run: bool) -> Vec<SyncResult> { vec![sync_file(&gemini_artifact(), &transpile_skill("gemini"), false, dry_run)] }
fn install_codex(dry_run: bool) -> Vec<SyncResult> { vec![sync_file(&codex_artifact(), &transpile_skill("codex"), false, dry_run)] }
fn install_opencode(dry_run: bool) -> Vec<SyncResult> { vec![sync_file(&opencode_artifact(), &transpile_skill("opencode"), false, dry_run)] }
fn install_cursor(dry_run: bool) -> Vec<SyncResult> { vec![sync_file(&cursor_artifact(), &transpile_skill("cursor"), false, dry_run)] }
fn uninstall_gemini(dry_run: bool) -> Vec<SyncResult> { vec![remove_file(&gemini_artifact(), dry_run)] }
fn uninstall_codex(dry_run: bool) -> Vec<SyncResult> { vec![remove_file(&codex_artifact(), dry_run)] }
fn uninstall_opencode(dry_run: bool) -> Vec<SyncResult> { vec![remove_file(&opencode_artifact(), dry_run)] }
fn uninstall_cursor(dry_run: bool) -> Vec<SyncResult> { vec![remove_file(&cursor_artifact(), dry_run)] }
pub fn run_install(tools: Vec<String>, all: bool, dry_run: bool, list: bool) -> i32 {
if list { return run_list(); }
let selected: Vec<&str> = if all {
INTEGRATIONS.iter().map(|i| i.id).collect()
} else if !tools.is_empty() {
let mut out = Vec::new();
for name in &tools {
match find_integration(name) {
Some(ig) => out.push(ig.id),
None => {
eprintln!("error: unknown integration '{name}'. Available: {}", integration_names());
return 1;
}
}
}
out
} else {
wizard_select()
};
if selected.is_empty() {
eprintln!("No integrations selected.");
return 0;
}
if dry_run { eprintln!("Dry run — no files will be written.\n"); }
let profile = detect_profile();
if !dry_run {
ensure_shell_block(&profile);
eprintln!();
}
for id in &selected {
let ig = find_integration(id).unwrap();
let results = (ig.install)(dry_run);
print_results(id, &results);
}
if !dry_run {
eprintln!();
eprintln!("Done. Restart your shell or: source {profile}");
}
0
}
pub fn run_uninstall(tools: Vec<String>, all: bool, dry_run: bool) -> i32 {
let selected: Vec<&str> = if all {
INTEGRATIONS.iter().map(|i| i.id).collect()
} else if !tools.is_empty() {
let mut out = Vec::new();
for name in &tools {
match find_integration(name) {
Some(ig) => out.push(ig.id),
None => {
eprintln!("error: unknown integration '{name}'. Available: {}", integration_names());
return 1;
}
}
}
out
} else {
wizard_uninstall_select()
};
if selected.is_empty() {
eprintln!("No integrations selected.");
return 0;
}
if dry_run { eprintln!("Dry run — no files will be removed.\n"); }
for id in &selected {
let ig = find_integration(id).unwrap();
let results = (ig.uninstall)(dry_run);
print_results(id, &results);
}
if !dry_run {
let profile = detect_profile();
let any_remain = INTEGRATIONS.iter().any(|ig| (ig.artifact)().exists());
if !any_remain {
remove_shell_block(&profile);
eprintln!(" shell block removed from {profile}");
}
eprintln!("\nDone.");
}
0
}
pub fn run_list() -> i32 {
eprintln!("Available integrations:\n");
eprintln!(" {:<10} {:<14} {:<12} {:<14} ARTIFACT", "ID", "LABEL", "DETECTED", "STATUS");
eprintln!(" {}", "─".repeat(72));
for ig in INTEGRATIONS {
let artifact = (ig.artifact)();
let detected = if (ig.detect)() { "✓ detected " } else { " " };
let status = if artifact.exists() { "✓ installed " } else { " not installed" };
eprintln!(" {:<10} {:<14} {:<12} {:<15} {}", ig.id, ig.label, detected, status, shorten_path(&artifact));
}
eprintln!();
0
}
fn sync_file(path: &PathBuf, content: &str, executable: bool, dry_run: bool) -> SyncResult {
if dry_run {
let action = if path.exists() { "update" } else { "create" };
return SyncResult::DryRun(format!("{action} {}", shorten_path(path)));
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
let exists = path.exists();
let same = exists
&& std::fs::read_to_string(path).map(|s| s == content).unwrap_or(false);
if same { return SyncResult::Unchanged(path.clone()); }
std::fs::write(path, content).ok();
#[cfg(unix)]
if executable {
use std::os::unix::fs::PermissionsExt;
if let Ok(m) = std::fs::metadata(path) {
let mut p = m.permissions();
p.set_mode(0o755);
std::fs::set_permissions(path, p).ok();
}
}
if exists { SyncResult::Updated(path.clone()) } else { SyncResult::Added(path.clone()) }
}
fn remove_file(path: &PathBuf, dry_run: bool) -> SyncResult {
if !path.exists() { return SyncResult::Skipped(path.clone()); }
if dry_run {
return SyncResult::DryRun(format!("remove {}", shorten_path(path)));
}
std::fs::remove_file(path).ok();
if let Some(parent) = path.parent() {
let _ = std::fs::remove_dir(parent); }
SyncResult::Removed(path.clone())
}
const CLAUDE_HOOK_SCRIPT: &str = r#"#!/usr/bin/env bash
# Auto-generated by `transpile install`. Re-run to update.
# PostToolUse hook: auto-compress large files read by Claude Code.
# Outputs {"additionalContext": "..."} so Claude prefers the token-efficient version.
set -euo pipefail
THRESHOLD=${TRANSPILE_THRESHOLD:-8192} # bytes; override via env var
# Parse file_path from hook JSON on stdin
INPUT=$(cat)
FILE=$(printf '%s' "$INPUT" | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(d.get('tool_input', {}).get('file_path', ''))
" 2>/dev/null) || exit 0
[ -z "$FILE" ] && exit 0
[ -f "$FILE" ] || exit 0
BYTES=$(wc -c < "$FILE" 2>/dev/null || echo 0)
[ "$BYTES" -lt "$THRESHOLD" ] && exit 0
# Run transpile; silently skip if binary not on PATH or transpile fails
COMPRESSED=$(transpile --input "$FILE" --fidelity semantic --quiet 2>/dev/null) || exit 0
[ -z "$COMPRESSED" ] && exit 0
FNAME=$(basename "$FILE")
python3 -c "
import json, sys
compressed = sys.argv[1]
fname = sys.argv[2]
bytes_val = sys.argv[3]
msg = (
f'[llm-transpile] {fname} is {bytes_val}B — token-compressed version below '
f'(prefer this over the raw content above):\n\n{compressed}'
)
print(json.dumps({'additionalContext': msg}))
" "$COMPRESSED" "$FNAME" "$BYTES"
"#;
fn merge_claude_hook(hook_script: &std::path::Path, settings_path: &std::path::Path) -> SyncResult {
if !settings_path.exists() {
std::fs::write(settings_path, "{}").ok();
}
let raw = std::fs::read_to_string(settings_path).unwrap_or_else(|_| "{}".into());
let mut cfg: serde_json::Value =
serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
let hook = serde_json::json!({
"_id": "llm-transpile",
"matcher": "Read",
"hooks": [{ "type": "command", "command": format!("bash \"{}\"", hook_script.display()) }]
});
if let Some(arr) = cfg["hooks"]["PostToolUse"].as_array_mut() {
arr.retain(|h| h.get("_id").and_then(|v| v.as_str()) != Some("llm-transpile"));
arr.push(hook);
} else {
cfg["hooks"]["PostToolUse"] = serde_json::json!([hook]);
}
std::fs::write(settings_path, serde_json::to_string_pretty(&cfg).unwrap()).ok();
let label = format!("PostToolUse hook → {}", shorten_path(settings_path));
SyncResult::Configured(label)
}
fn strip_claude_hook(settings_path: &std::path::Path) -> SyncResult {
if !settings_path.exists() {
return SyncResult::Skipped(settings_path.to_path_buf());
}
let raw = std::fs::read_to_string(settings_path).unwrap_or_else(|_| "{}".into());
let mut cfg: serde_json::Value =
serde_json::from_str(&raw).unwrap_or(serde_json::json!({}));
if let Some(arr) = cfg["hooks"]["PostToolUse"].as_array_mut() {
arr.retain(|h| h.get("_id").and_then(|v| v.as_str()) != Some("llm-transpile"));
}
if cfg["hooks"]["PostToolUse"].as_array().map(|a| a.is_empty()).unwrap_or(false) {
cfg["hooks"].as_object_mut().map(|o| o.remove("PostToolUse"));
}
if cfg["hooks"].as_object().map(|o| o.is_empty()).unwrap_or(false) {
cfg.as_object_mut().map(|o| o.remove("hooks"));
}
std::fs::write(settings_path, serde_json::to_string_pretty(&cfg).unwrap()).ok();
SyncResult::Deconfigured(format!("PostToolUse hook removed from {}", shorten_path(settings_path)))
}
fn transpile_skill(agent: &str) -> String {
format!("\
---
name: transpile
description: >
ALWAYS run this instead of directly reading .md, .html, or .txt files.
Trigger on file extension alone — no size check needed.
alwaysApply: true
---
# transpile — Read document files in token-compressed form
## Activation rule
**Whenever you are about to read a `.md`, `.html`, or `.txt` file, run this
instead. Use transpile's output as the file content — do not read the raw file.**
The file extension alone triggers this. No size check needed.
## Usage
```bash
TRANSPILE_AGENT={agent} transpile --input <file> --fidelity semantic --quiet
```
## Fidelity options
- `semantic` (default) — ~30% reduction, preserves all meaning
- `compressed` — ~40% reduction, use when near context limit
- `lossless` — no compression, for files where every word matters
")
}
fn skill_path(tool: &str) -> PathBuf {
let h = home();
match tool {
"gemini" => PathBuf::from(&h).join(".gemini").join("skills").join("transpile").join("SKILL.md"),
"codex" => PathBuf::from(&h).join(".agents").join("skills").join("transpile").join("SKILL.md"),
"opencode" => {
let cfg = std::env::var("XDG_CONFIG_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(&h).join(".config"));
cfg.join("opencode").join("skills").join("transpile").join("SKILL.md")
}
"cursor" => std::path::Path::new(".cursor").join("rules").join("transpile.mdc"),
_ => unreachable!(),
}
}
const MARKER_BEGIN: &str = "# >>> llm-transpile";
const MARKER_END: &str = "# <<< llm-transpile";
const SHELL_BLOCK: &str =
r#"tctx() { transpile --input "$1" --fidelity "${2:-semantic}" --quiet; }"#;
fn detect_profile() -> String {
let home = home();
for name in &[".zshrc", ".bashrc", ".profile"] {
let p = format!("{home}/{name}");
if std::path::Path::new(&p).exists() { return p; }
}
format!("{home}/.profile")
}
fn ensure_shell_block(profile: &str) {
let existing = std::fs::read_to_string(profile).unwrap_or_default();
if existing.contains(MARKER_BEGIN) { return; }
let block = format!("\n{MARKER_BEGIN}\n{SHELL_BLOCK}\n{MARKER_END}\n");
let new = format!("{existing}{block}");
std::fs::write(profile, new).ok();
eprintln!(" shell ✓ added tctx() helper → {profile}");
}
fn remove_shell_block(profile: &str) {
let existing = std::fs::read_to_string(profile).unwrap_or_default();
if !existing.contains(MARKER_BEGIN) { return; }
let cleaned = splice_block(&existing, MARKER_BEGIN, MARKER_END, "");
let cleaned = cleaned.trim_end().to_string() + "\n";
std::fs::write(profile, cleaned).ok();
}
fn splice_block(text: &str, begin: &str, end: &str, replacement: &str) -> String {
let start = match text.find(begin) {
Some(i) => i,
None => return format!("{text}\n{replacement}\n"),
};
let after_end = match text[start..].find(end) {
Some(i) => start + i + end.len(),
None => text.len(),
};
let prefix_end = if start > 0 && text.as_bytes()[start - 1] == b'\n' { start - 1 } else { start };
if replacement.is_empty() {
format!("{}{}", &text[..prefix_end], &text[after_end..])
} else {
format!("{}\n{}\n{}", &text[..prefix_end], replacement, &text[after_end..])
}
}
fn wizard_select() -> Vec<&'static str> {
if io::stdin().is_terminal() && io::stderr().is_terminal() {
wizard_tty()
} else {
wizard_pipe()
}
}
fn wizard_tty() -> Vec<&'static str> {
let detected: Vec<bool> = INTEGRATIONS.iter().map(|i| (i.detect)()).collect();
let mut checked: Vec<bool> = detected.clone();
let mut cursor = 0usize;
let _ = std::process::Command::new("stty").args(["-echo", "raw"]).status();
loop {
eprint!("\x1b[2J\x1b[H");
eprintln!("transpile install — select integrations\r");
eprintln!(" Space: toggle · A: all · N: none · Enter: confirm · Q: quit\r");
eprintln!("\r");
for (i, ig) in INTEGRATIONS.iter().enumerate() {
let artifact = (ig.artifact)();
let status = if artifact.exists() {
" [installed]"
} else if detected[i] {
" [detected] "
} else {
" "
};
let check = if checked[i] { "◉" } else { "○" };
let arrow = if i == cursor { "▶" } else { " " };
eprintln!(" {} {} {:<10} {}{}\r", arrow, check, ig.id, ig.label, status);
}
let _ = io::stderr().flush();
match read_key() {
b' ' => { checked[cursor] = !checked[cursor]; }
b'A'|b'a' => { checked.iter_mut().for_each(|c| *c = true); }
b'N'|b'n' => { checked.iter_mut().for_each(|c| *c = false); }
b'Q'|b'q' => {
let _ = std::process::Command::new("stty").args(["echo", "-raw"]).status();
return vec![];
}
b'\r'|b'\n' => break,
27 => {
if read_key() == b'[' {
match read_key() {
b'A' => { cursor = cursor.saturating_sub(1); }
b'B' => { if cursor < INTEGRATIONS.len() - 1 { cursor += 1; } }
_ => {}
}
}
}
_ => {}
}
}
let _ = std::process::Command::new("stty").args(["echo", "-raw"]).status();
eprint!("\x1b[2J\x1b[H");
INTEGRATIONS.iter().enumerate()
.filter_map(|(i, t)| if checked[i] { Some(t.id) } else { None })
.collect()
}
fn wizard_pipe() -> Vec<&'static str> {
eprintln!("transpile install — select integrations");
eprintln!("────────────────────────────────────────");
for (i, ig) in INTEGRATIONS.iter().enumerate() {
let flag = if (ig.detect)() { " *" } else { "" };
eprintln!(" [{}] {:<10} {}{}", i + 1, ig.id, ig.label, flag);
}
eprintln!(" [a] All of the above");
eprintln!();
eprint!("Selection (e.g. 1,3 or a): ");
let _ = io::stderr().flush();
read_selection()
}
fn wizard_uninstall_select() -> Vec<&'static str> {
if io::stdin().is_terminal() && io::stderr().is_terminal() {
wizard_uninstall_tty()
} else {
wizard_uninstall_pipe()
}
}
fn wizard_uninstall_tty() -> Vec<&'static str> {
let installed: Vec<bool> = INTEGRATIONS.iter().map(|ig| (ig.artifact)().exists()).collect();
let mut checked: Vec<bool> = installed.clone();
let mut cursor = 0usize;
let _ = std::process::Command::new("stty").args(["-echo", "raw"]).status();
loop {
eprint!("\x1b[2J\x1b[H");
eprintln!("transpile uninstall — select integrations to remove\r");
eprintln!(" Space: toggle · A: all · N: none · Enter: confirm · Q: quit\r");
eprintln!("\r");
for (i, ig) in INTEGRATIONS.iter().enumerate() {
let status = if installed[i] { " [installed] " } else { " [not installed]" };
let check = if checked[i] { "◉" } else { "○" };
let arrow = if i == cursor { "▶" } else { " " };
eprintln!(" {} {} {:<10} {}{}\r", arrow, check, ig.id, ig.label, status);
}
let _ = io::stderr().flush();
match read_key() {
b' ' => { checked[cursor] = !checked[cursor]; }
b'A' | b'a' => { checked.iter_mut().for_each(|c| *c = true); }
b'N' | b'n' => { checked.iter_mut().for_each(|c| *c = false); }
b'Q' | b'q' => {
let _ = std::process::Command::new("stty").args(["echo", "-raw"]).status();
return vec![];
}
b'\r' | b'\n' => break,
27 => {
if read_key() == b'[' {
match read_key() {
b'A' => { cursor = cursor.saturating_sub(1); }
b'B' => { if cursor < INTEGRATIONS.len() - 1 { cursor += 1; } }
_ => {}
}
}
}
_ => {}
}
}
let _ = std::process::Command::new("stty").args(["echo", "-raw"]).status();
eprint!("\x1b[2J\x1b[H");
INTEGRATIONS.iter().enumerate()
.filter_map(|(i, t)| if checked[i] { Some(t.id) } else { None })
.collect()
}
fn wizard_uninstall_pipe() -> Vec<&'static str> {
eprintln!("transpile uninstall — select integrations to remove");
eprintln!("─────────────────────────────────────────────────────");
for (i, ig) in INTEGRATIONS.iter().enumerate() {
let status = if (ig.artifact)().exists() { " [installed]" } else { "" };
eprintln!(" [{}] {:<10} {}{}", i + 1, ig.id, ig.label, status);
}
eprintln!(" [a] All of the above");
eprintln!();
eprint!("Selection (e.g. 1,3 or a): ");
let _ = io::stderr().flush();
read_selection()
}
fn read_key() -> u8 {
use std::io::Read;
let mut buf = [0u8; 1];
io::stdin().read_exact(&mut buf).ok();
buf[0]
}
fn read_selection() -> Vec<&'static str> {
let mut line = String::new();
let _ = io::stdin().read_line(&mut line);
let line = line.trim().to_lowercase();
if line == "a" || line == "all" {
return INTEGRATIONS.iter().map(|i| i.id).collect();
}
let mut out = Vec::new();
for token in line.split(',') {
if let Ok(n) = token.trim().parse::<usize>()
&& n >= 1 && n <= INTEGRATIONS.len() {
out.push(INTEGRATIONS[n - 1].id);
}
}
out
}
fn home() -> String {
std::env::var("HOME").unwrap_or_else(|_| "/tmp".into())
}
fn cmd_exists(name: &str) -> bool {
std::process::Command::new("which")
.arg(name)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn dir_exists(path: &str) -> bool {
let expanded = path.replacen("~", &home(), 1);
std::path::Path::new(&expanded).exists()
}
fn shorten_path(p: &std::path::Path) -> String {
let h = home();
let s = p.to_string_lossy();
if s.starts_with(&h) {
format!("~{}", &s[h.len()..])
} else {
s.into_owned()
}
}
#[cfg(test)]
mod tests {
use super::transpile_skill;
#[test]
fn skill_gemini_contains_agent_env() {
let content = transpile_skill("gemini");
assert!(
content.contains("TRANSPILE_AGENT=gemini transpile"),
"gemini skill must include TRANSPILE_AGENT=gemini in usage command"
);
}
#[test]
fn skill_codex_contains_agent_env() {
let content = transpile_skill("codex");
assert!(
content.contains("TRANSPILE_AGENT=codex transpile"),
"codex skill must include TRANSPILE_AGENT=codex in usage command"
);
}
#[test]
fn skill_opencode_contains_agent_env() {
let content = transpile_skill("opencode");
assert!(
content.contains("TRANSPILE_AGENT=opencode transpile"),
"opencode skill must include TRANSPILE_AGENT=opencode in usage command"
);
}
#[test]
fn skill_cursor_contains_agent_env() {
let content = transpile_skill("cursor");
assert!(
content.contains("TRANSPILE_AGENT=cursor transpile"),
"cursor skill must include TRANSPILE_AGENT=cursor in usage command"
);
}
#[test]
fn skill_preserves_rest_of_content() {
for agent in &["gemini", "codex", "opencode", "cursor"] {
let content = transpile_skill(agent);
assert!(content.contains("name: transpile"), "agent={agent}: missing frontmatter");
assert!(content.contains("--fidelity semantic --quiet"), "agent={agent}: missing fidelity flag");
assert!(content.contains("alwaysApply: true"), "agent={agent}: missing alwaysApply");
}
}
}