use crate::config::get_cwd;
use crate::error::{Result, SkillcError};
use crate::logging::{LogEntry, get_run_id, init_log_db, log_access_with_fallback};
use crate::resolver::resolve_skill;
use crate::{OutputFormat, verbose};
use std::fs;
use std::time::Instant;
pub fn open(
skill: &str,
path: &str,
max_lines: Option<usize>,
format: OutputFormat,
) -> Result<String> {
let start = Instant::now();
let resolved = resolve_skill(skill)?;
let run_id = get_run_id();
verbose!("open: path=\"{}\" max_lines={:?}", path, max_lines);
verbose!("open: source_dir={}", resolved.source_dir.display());
let log_conn = init_log_db(&resolved.runtime_dir);
let args = serde_json::json!({ "path": path, "max_lines": max_lines });
let result = do_open(&resolved, path, max_lines, &format);
verbose!("open: completed in {:?}", start.elapsed());
log_access_with_fallback(
log_conn.as_ref(),
&LogEntry {
run_id,
command: "open".to_string(),
skill: resolved.name.clone(),
skill_path: resolved.source_dir.to_string_lossy().to_string(),
cwd: get_cwd(),
args: args.to_string(),
error: result.as_ref().err().map(|e| e.to_string()),
},
);
result
}
fn do_open(
resolved: &crate::resolver::ResolvedSkill,
path: &str,
max_lines: Option<usize>,
_format: &OutputFormat,
) -> Result<String> {
if path.contains("..") {
let full_path = resolved.source_dir.join(path);
if let Ok(canonical) = full_path.canonicalize() {
if !canonical.starts_with(&resolved.source_dir) {
return Err(SkillcError::PathEscapesRoot(path.to_string()));
}
} else {
return Err(SkillcError::PathEscapesRoot(path.to_string()));
}
}
let file_path = resolved.source_dir.join(path);
if file_path.exists() {
let canonical = file_path.canonicalize()?;
if !canonical.starts_with(&resolved.source_dir) {
return Err(SkillcError::PathEscapesRoot(path.to_string()));
}
}
if !file_path.exists() {
return Err(SkillcError::FileNotFound(path.to_string()));
}
if file_path.is_dir() {
return Err(SkillcError::InvalidPath(
"Path must be a file, not a directory".to_string(),
));
}
let content = fs::read_to_string(&file_path)?;
if let Some(limit) = max_lines {
let lines: Vec<&str> = content.lines().collect();
if lines.len() > limit {
let truncated: Vec<&str> = lines[..limit].to_vec();
let remaining = lines.len() - limit;
return Ok(format!(
"{}\n... ({} more lines)",
truncated.join("\n"),
remaining
));
}
}
Ok(content)
}