use crate::util::{home_rel, parse_harness, resolve, usage};
use anyhow::{bail, Context, Result};
use cv_core::ir::{Harness, Session, SessionRef};
use cv_core::{Adapter, EmitOptions, ParseOptions};
use std::fs;
use std::path::{Path, PathBuf};
fn parse_for_emit(adapter: &dyn Adapter, r: &SessionRef, to_h: Harness) -> Result<Session> {
if r.harness == to_h {
let mut s = cv_core::stream::collect_with(adapter, r, &ParseOptions::complete())?;
s.materialize();
Ok(s)
} else {
adapter.parse(r)
}
}
pub(crate) fn cmd_port(
id: &str,
harness: Option<String>,
cwd: Option<PathBuf>,
out: Option<PathBuf>,
no_context: bool,
strict: bool,
thinking: cv_core::emit::ThinkingMode,
) -> Result<()> {
let (r, adapter) = resolve(id, None)?;
let to_h = match &harness {
Some(s) => Harness::parse(s).with_context(|| format!("unknown target harness: {s}"))?,
None => r.harness, };
let session = parse_for_emit(adapter.as_ref(), &r, to_h)?;
let new_cwd = cwd.clone();
emit_session(
&session,
to_h,
out,
EmitOptions {
new_cwd: cwd,
new_id: None,
strict,
thinking,
},
)?;
if !no_context {
if let (Some(src), Some(dst)) = (session.cwd.as_deref(), new_cwd.as_deref()) {
carry_context(src, dst);
}
}
Ok(())
}
const CONTEXT_FILES: &[&str] = &[
"CLAUDE.md",
"CLAUDE.local.md",
"AGENTS.md",
"GEMINI.md",
"MEMORY.md",
".cursorrules",
".windsurfrules",
];
fn carry_context(src: &Path, dst: &Path) {
if src == dst {
return;
}
let mut copied = Vec::new();
for name in CONTEXT_FILES {
let from = src.join(name);
if !from.is_file() {
continue;
}
let to = dst.join(name);
if to.exists() {
eprintln!(" ↳ context: {name} already exists at target — left as-is");
continue;
}
match fs::create_dir_all(dst).and_then(|_| fs::copy(&from, &to)) {
Ok(_) => copied.push(*name),
Err(e) => eprintln!(" ↳ context: couldn't copy {name}: {e}"),
}
}
if !copied.is_empty() {
println!(" ↳ carried context: {}", copied.join(", "));
}
}
pub(crate) fn emit_session(session: &Session, to_h: Harness, out: Option<PathBuf>, opts: EmitOptions) -> Result<()> {
if !cv_core::emit::supported_targets().contains(&to_h) {
return usage(format!(
"{to_h} is not a supported target (the source parses fine, {} messages); supported: {}",
session.messages.len(),
cv_core::emit::supported_targets()
.iter()
.map(|h| h.as_str())
.collect::<Vec<_>>()
.join(", ")
));
}
let out_dir = match out {
Some(d) => d,
None => cv_core::harness::for_harness(to_h)
.and_then(|a| a.storage_root())
.with_context(|| format!("{to_h} doesn't appear installed; pass --out <dir> to write somewhere"))?,
};
let (res, warnings) = cv_core::emit::emit_verified(session, to_h, &out_dir, &opts)?;
println!("✦ wrote {} ({})", res.path.display(), res.new_id);
if let Some(hint) = res.resume_hint {
println!(" ↳ {hint}");
}
for w in &warnings {
eprintln!(" ⚠ lossy: {w}");
}
Ok(())
}
pub(crate) fn cmd_resume(id: &str, harness: Option<String>, launch: bool) -> Result<()> {
let want = parse_harness(&harness)?;
let (r, _adapter) = resolve(id, want)?;
let cwd = r.cwd.clone();
let (program, args) = resume_command(r.harness, &r.id);
if launch {
let dir = cwd
.clone()
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
eprintln!("✦ launching: (cd {}) {} {}", home_rel(&dir), program, args.join(" "));
let status = std::process::Command::new(&program)
.args(&args)
.current_dir(&dir)
.status()
.with_context(|| format!("failed to launch {program:?}"))?;
if !status.success() {
bail!("{program} exited with status {status}");
}
return Ok(());
}
for line in resume_lines(r.harness, &r.id, cwd.as_deref()) {
println!("{line}");
}
Ok(())
}
pub(crate) fn resume_lines(h: Harness, id: &str, cwd: Option<&Path>) -> Vec<String> {
let (program, args) = resume_command(h, id);
let mut out = Vec::new();
if let Some(dir) = cwd {
out.push(format!("cd {}", shell_quote(&dir.display().to_string())));
}
out.push(format!("{} {}", program, args.join(" ")));
out
}
fn resume_command(h: Harness, id: &str) -> (String, Vec<String>) {
match h {
Harness::Claude => ("claude".into(), vec!["--resume".into(), id.into()]),
Harness::Codex => ("codex".into(), vec!["resume".into(), id.into()]),
Harness::Grok => ("grok".into(), vec!["--resume".into(), id.into()]),
Harness::OpenCode => ("opencode".into(), vec!["--session".into(), id.into()]),
Harness::Gemini => ("gemini".into(), vec!["--resume".into(), id.into()]),
Harness::Hermes => ("hermes".into(), vec!["resume".into(), id.into()]),
Harness::OpenClaw => ("openclaw".into(), vec!["--resume".into(), id.into()]),
Harness::Kimi => ("kimi".into(), vec!["--resume".into(), id.into()]),
Harness::KimiCode => ("kimi".into(), vec!["--session".into(), format!("session_{id}")]),
Harness::Qwen => ("qwen".into(), vec!["--resume".into(), id.into()]),
Harness::Devin => ("devin".into(), vec!["--resume".into(), id.into()]),
_ => (
format!("# no CLI resume for {h}; open the app and find the session"),
vec![id.into()],
),
}
}
fn shell_quote(s: &str) -> String {
if s.is_empty() {
return "''".into();
}
if s.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'/' | b'.' | b'_' | b'-' | b'~' | b'+' | b':' | b'@'))
{
s.to_string()
} else {
format!("'{}'", s.replace('\'', r"'\''"))
}
}