fix: adopt plural config dirs for agents, commands, and skills (#198)
This commit is contained in:
committed by
GitHub
parent
7c36418e23
commit
95e41940f8
@@ -99,11 +99,19 @@ fn get_config_dir() -> PathBuf {
|
|||||||
|
|
||||||
/// Get agent directory path
|
/// Get agent directory path
|
||||||
fn get_agent_dir() -> PathBuf {
|
fn get_agent_dir() -> PathBuf {
|
||||||
|
get_config_dir().join("agents")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_legacy_agent_dir() -> PathBuf {
|
||||||
get_config_dir().join("agent")
|
get_config_dir().join("agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get user-level command directory path
|
/// Get user-level command directory path
|
||||||
fn get_command_dir() -> PathBuf {
|
fn get_command_dir() -> PathBuf {
|
||||||
|
get_config_dir().join("commands")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_legacy_command_dir() -> PathBuf {
|
||||||
get_config_dir().join("command")
|
get_config_dir().join("command")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,23 +475,38 @@ pub async fn remove_provider_config(
|
|||||||
|
|
||||||
/// Get project-level agent directory path
|
/// Get project-level agent directory path
|
||||||
fn get_project_agent_dir(working_directory: &Path) -> PathBuf {
|
fn get_project_agent_dir(working_directory: &Path) -> PathBuf {
|
||||||
|
working_directory.join(".opencode").join("agents")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_legacy_project_agent_dir(working_directory: &Path) -> PathBuf {
|
||||||
working_directory.join(".opencode").join("agent")
|
working_directory.join(".opencode").join("agent")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get project-level agent path
|
/// Get project-level agent path
|
||||||
fn get_project_agent_path(working_directory: &Path, agent_name: &str) -> PathBuf {
|
fn get_project_agent_path(working_directory: &Path, agent_name: &str) -> PathBuf {
|
||||||
get_project_agent_dir(working_directory).join(format!("{}.md", agent_name))
|
let plural_path = get_project_agent_dir(working_directory).join(format!("{}.md", agent_name));
|
||||||
|
let legacy_path = get_legacy_project_agent_dir(working_directory).join(format!("{}.md", agent_name));
|
||||||
|
if legacy_path.exists() && !plural_path.exists() {
|
||||||
|
return legacy_path;
|
||||||
|
}
|
||||||
|
plural_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get user-level agent path
|
/// Get user-level agent path
|
||||||
fn get_user_agent_path(agent_name: &str) -> PathBuf {
|
fn get_user_agent_path(agent_name: &str) -> PathBuf {
|
||||||
get_agent_dir().join(format!("{}.md", agent_name))
|
let plural_path = get_agent_dir().join(format!("{}.md", agent_name));
|
||||||
|
let legacy_path = get_legacy_agent_dir().join(format!("{}.md", agent_name));
|
||||||
|
if legacy_path.exists() && !plural_path.exists() {
|
||||||
|
return legacy_path;
|
||||||
|
}
|
||||||
|
plural_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure project agent directory exists
|
/// Ensure project agent directory exists
|
||||||
async fn ensure_project_agent_dir(working_directory: &Path) -> Result<PathBuf> {
|
async fn ensure_project_agent_dir(working_directory: &Path) -> Result<PathBuf> {
|
||||||
let project_agent_dir = get_project_agent_dir(working_directory);
|
let project_agent_dir = get_project_agent_dir(working_directory);
|
||||||
fs::create_dir_all(&project_agent_dir).await?;
|
fs::create_dir_all(&project_agent_dir).await?;
|
||||||
|
fs::create_dir_all(&get_legacy_project_agent_dir(working_directory)).await?;
|
||||||
Ok(project_agent_dir)
|
Ok(project_agent_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,23 +557,38 @@ fn get_agent_write_path(
|
|||||||
|
|
||||||
/// Get project-level command directory path
|
/// Get project-level command directory path
|
||||||
fn get_project_command_dir(working_directory: &Path) -> PathBuf {
|
fn get_project_command_dir(working_directory: &Path) -> PathBuf {
|
||||||
|
working_directory.join(".opencode").join("commands")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_legacy_project_command_dir(working_directory: &Path) -> PathBuf {
|
||||||
working_directory.join(".opencode").join("command")
|
working_directory.join(".opencode").join("command")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get project-level command path
|
/// Get project-level command path
|
||||||
fn get_project_command_path(working_directory: &Path, command_name: &str) -> PathBuf {
|
fn get_project_command_path(working_directory: &Path, command_name: &str) -> PathBuf {
|
||||||
get_project_command_dir(working_directory).join(format!("{}.md", command_name))
|
let plural_path = get_project_command_dir(working_directory).join(format!("{}.md", command_name));
|
||||||
|
let legacy_path = get_legacy_project_command_dir(working_directory).join(format!("{}.md", command_name));
|
||||||
|
if legacy_path.exists() && !plural_path.exists() {
|
||||||
|
return legacy_path;
|
||||||
|
}
|
||||||
|
plural_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get user-level command path
|
/// Get user-level command path
|
||||||
fn get_user_command_path(command_name: &str) -> PathBuf {
|
fn get_user_command_path(command_name: &str) -> PathBuf {
|
||||||
get_command_dir().join(format!("{}.md", command_name))
|
let plural_path = get_command_dir().join(format!("{}.md", command_name));
|
||||||
|
let legacy_path = get_legacy_command_dir().join(format!("{}.md", command_name));
|
||||||
|
if legacy_path.exists() && !plural_path.exists() {
|
||||||
|
return legacy_path;
|
||||||
|
}
|
||||||
|
plural_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure project command directory exists
|
/// Ensure project command directory exists
|
||||||
async fn ensure_project_command_dir(working_directory: &Path) -> Result<PathBuf> {
|
async fn ensure_project_command_dir(working_directory: &Path) -> Result<PathBuf> {
|
||||||
let project_command_dir = get_project_command_dir(working_directory);
|
let project_command_dir = get_project_command_dir(working_directory);
|
||||||
fs::create_dir_all(&project_command_dir).await?;
|
fs::create_dir_all(&project_command_dir).await?;
|
||||||
|
fs::create_dir_all(&get_legacy_project_command_dir(working_directory)).await?;
|
||||||
Ok(project_command_dir)
|
Ok(project_command_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,7 +646,9 @@ async fn ensure_dirs() -> Result<()> {
|
|||||||
|
|
||||||
fs::create_dir_all(&config_dir).await?;
|
fs::create_dir_all(&config_dir).await?;
|
||||||
fs::create_dir_all(&agent_dir).await?;
|
fs::create_dir_all(&agent_dir).await?;
|
||||||
|
fs::create_dir_all(&get_legacy_agent_dir()).await?;
|
||||||
fs::create_dir_all(&command_dir).await?;
|
fs::create_dir_all(&command_dir).await?;
|
||||||
|
fs::create_dir_all(&get_legacy_command_dir()).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1688,30 +1728,65 @@ pub struct DiscoveredSkill {
|
|||||||
|
|
||||||
/// Get user-level skill directory path
|
/// Get user-level skill directory path
|
||||||
fn get_skill_dir() -> PathBuf {
|
fn get_skill_dir() -> PathBuf {
|
||||||
|
get_config_dir().join("skills")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_legacy_skill_dir() -> PathBuf {
|
||||||
get_config_dir().join("skill")
|
get_config_dir().join("skill")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get user-level skill directory for a specific skill
|
/// Get user-level skill directory for a specific skill
|
||||||
fn get_user_skill_dir(skill_name: &str) -> PathBuf {
|
fn get_user_skill_dir(skill_name: &str) -> PathBuf {
|
||||||
get_skill_dir().join(skill_name)
|
let plural_path = get_skill_dir().join(skill_name);
|
||||||
|
let legacy_path = get_legacy_skill_dir().join(skill_name);
|
||||||
|
if legacy_path.exists() && !plural_path.exists() {
|
||||||
|
return legacy_path;
|
||||||
|
}
|
||||||
|
plural_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get user-level skill SKILL.md path
|
/// Get user-level skill SKILL.md path
|
||||||
fn get_user_skill_path(skill_name: &str) -> PathBuf {
|
fn get_user_skill_path(skill_name: &str) -> PathBuf {
|
||||||
get_user_skill_dir(skill_name).join("SKILL.md")
|
let plural_path = get_skill_dir().join(skill_name).join("SKILL.md");
|
||||||
|
let legacy_path = get_legacy_skill_dir().join(skill_name).join("SKILL.md");
|
||||||
|
if legacy_path.exists() && !plural_path.exists() {
|
||||||
|
return legacy_path;
|
||||||
|
}
|
||||||
|
plural_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get project-level skill directory (.opencode/skill/)
|
/// Get project-level skill directory (.opencode/skills/)
|
||||||
fn get_project_skill_dir(working_directory: &Path, skill_name: &str) -> PathBuf {
|
fn get_project_skill_dir(working_directory: &Path, skill_name: &str) -> PathBuf {
|
||||||
working_directory
|
let plural_path = working_directory
|
||||||
|
.join(".opencode")
|
||||||
|
.join("skills")
|
||||||
|
.join(skill_name);
|
||||||
|
let legacy_path = working_directory
|
||||||
.join(".opencode")
|
.join(".opencode")
|
||||||
.join("skill")
|
.join("skill")
|
||||||
.join(skill_name)
|
.join(skill_name);
|
||||||
|
if legacy_path.exists() && !plural_path.exists() {
|
||||||
|
return legacy_path;
|
||||||
|
}
|
||||||
|
plural_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get project-level skill SKILL.md path
|
/// Get project-level skill SKILL.md path
|
||||||
fn get_project_skill_path(working_directory: &Path, skill_name: &str) -> PathBuf {
|
fn get_project_skill_path(working_directory: &Path, skill_name: &str) -> PathBuf {
|
||||||
get_project_skill_dir(working_directory, skill_name).join("SKILL.md")
|
let plural_path = working_directory
|
||||||
|
.join(".opencode")
|
||||||
|
.join("skills")
|
||||||
|
.join(skill_name)
|
||||||
|
.join("SKILL.md");
|
||||||
|
let legacy_path = working_directory
|
||||||
|
.join(".opencode")
|
||||||
|
.join("skill")
|
||||||
|
.join(skill_name)
|
||||||
|
.join("SKILL.md");
|
||||||
|
if legacy_path.exists() && !plural_path.exists() {
|
||||||
|
return legacy_path;
|
||||||
|
}
|
||||||
|
plural_path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Claude-compatible skill directory (.claude/skills/)
|
/// Get Claude-compatible skill directory (.claude/skills/)
|
||||||
@@ -1731,6 +1806,7 @@ fn get_claude_skill_path(working_directory: &Path, skill_name: &str) -> PathBuf
|
|||||||
async fn ensure_skill_dirs() -> Result<()> {
|
async fn ensure_skill_dirs() -> Result<()> {
|
||||||
let skill_dir = get_skill_dir();
|
let skill_dir = get_skill_dir();
|
||||||
fs::create_dir_all(&skill_dir).await?;
|
fs::create_dir_all(&skill_dir).await?;
|
||||||
|
fs::create_dir_all(&get_legacy_skill_dir()).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1738,6 +1814,11 @@ async fn ensure_skill_dirs() -> Result<()> {
|
|||||||
async fn ensure_project_skill_dir(working_directory: &Path, skill_name: &str) -> Result<PathBuf> {
|
async fn ensure_project_skill_dir(working_directory: &Path, skill_name: &str) -> Result<PathBuf> {
|
||||||
let project_skill_dir = get_project_skill_dir(working_directory, skill_name);
|
let project_skill_dir = get_project_skill_dir(working_directory, skill_name);
|
||||||
fs::create_dir_all(&project_skill_dir).await?;
|
fs::create_dir_all(&project_skill_dir).await?;
|
||||||
|
let legacy_project_skill_dir = working_directory
|
||||||
|
.join(".opencode")
|
||||||
|
.join("skill")
|
||||||
|
.join(skill_name);
|
||||||
|
fs::create_dir_all(&legacy_project_skill_dir).await?;
|
||||||
Ok(project_skill_dir)
|
Ok(project_skill_dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1747,7 +1828,7 @@ pub fn get_skill_scope(
|
|||||||
working_directory: Option<&Path>,
|
working_directory: Option<&Path>,
|
||||||
) -> (Option<SkillScope>, Option<PathBuf>, Option<SkillSource>) {
|
) -> (Option<SkillScope>, Option<PathBuf>, Option<SkillSource>) {
|
||||||
if let Some(wd) = working_directory {
|
if let Some(wd) = working_directory {
|
||||||
// Check .opencode/skill first
|
// Check .opencode/skills first
|
||||||
let project_path = get_project_skill_path(wd, skill_name);
|
let project_path = get_project_skill_path(wd, skill_name);
|
||||||
if project_path.exists() {
|
if project_path.exists() {
|
||||||
return (
|
return (
|
||||||
@@ -1832,9 +1913,9 @@ pub fn discover_skills(working_directory: Option<&Path>) -> Vec<DiscoveredSkill>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Project level .opencode/skill/ (highest priority)
|
// 1. Project level .opencode/skills/ (highest priority)
|
||||||
if let Some(wd) = working_directory {
|
if let Some(wd) = working_directory {
|
||||||
let project_skill_dir = wd.join(".opencode").join("skill");
|
let project_skill_dir = wd.join(".opencode").join("skills");
|
||||||
if project_skill_dir.exists() {
|
if project_skill_dir.exists() {
|
||||||
if let Ok(entries) = std::fs::read_dir(&project_skill_dir) {
|
if let Ok(entries) = std::fs::read_dir(&project_skill_dir) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
@@ -1849,6 +1930,21 @@ pub fn discover_skills(working_directory: Option<&Path>) -> Vec<DiscoveredSkill>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let legacy_project_skill_dir = wd.join(".opencode").join("skill");
|
||||||
|
if legacy_project_skill_dir.exists() {
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&legacy_project_skill_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if entry.path().is_dir() {
|
||||||
|
let skill_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
let skill_md = entry.path().join("SKILL.md");
|
||||||
|
if skill_md.exists() {
|
||||||
|
add_skill(skill_name, skill_md, Scope::Project, SkillSource::Opencode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Claude-compatible .claude/skills/
|
// 2. Claude-compatible .claude/skills/
|
||||||
let claude_skill_dir = wd.join(".claude").join("skills");
|
let claude_skill_dir = wd.join(".claude").join("skills");
|
||||||
if claude_skill_dir.exists() {
|
if claude_skill_dir.exists() {
|
||||||
@@ -1866,7 +1962,7 @@ pub fn discover_skills(working_directory: Option<&Path>) -> Vec<DiscoveredSkill>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. User level ~/.config/opencode/skill/
|
// 3. User level ~/.config/opencode/skills/
|
||||||
let user_skill_dir = get_skill_dir();
|
let user_skill_dir = get_skill_dir();
|
||||||
if user_skill_dir.exists() {
|
if user_skill_dir.exists() {
|
||||||
if let Ok(entries) = std::fs::read_dir(&user_skill_dir) {
|
if let Ok(entries) = std::fs::read_dir(&user_skill_dir) {
|
||||||
@@ -1882,6 +1978,21 @@ pub fn discover_skills(working_directory: Option<&Path>) -> Vec<DiscoveredSkill>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let legacy_user_skill_dir = get_legacy_skill_dir();
|
||||||
|
if legacy_user_skill_dir.exists() {
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&legacy_user_skill_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if entry.path().is_dir() {
|
||||||
|
let skill_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
let skill_md = entry.path().join("SKILL.md");
|
||||||
|
if skill_md.exists() {
|
||||||
|
add_skill(skill_name, skill_md, Scope::User, SkillSource::Opencode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
skills.into_values().collect()
|
skills.into_values().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2240,6 +2351,16 @@ pub async fn delete_skill(skill_name: &str, working_directory: Option<&Path>) ->
|
|||||||
deleted = true;
|
deleted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let legacy_user_dir = get_legacy_skill_dir().join(skill_name);
|
||||||
|
if legacy_user_dir.exists() {
|
||||||
|
fs::remove_dir_all(&legacy_user_dir).await?;
|
||||||
|
info!(
|
||||||
|
"Deleted legacy user-level skill directory: {}",
|
||||||
|
legacy_user_dir.display()
|
||||||
|
);
|
||||||
|
deleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
if !deleted {
|
if !deleted {
|
||||||
return Err(anyhow!("Skill \"{}\" not found", skill_name));
|
return Err(anyhow!("Skill \"{}\" not found", skill_name));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1312,6 +1312,14 @@ pub struct SkillsInstallRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn user_skill_dir() -> Result<PathBuf> {
|
fn user_skill_dir() -> Result<PathBuf> {
|
||||||
|
Ok(dirs::home_dir()
|
||||||
|
.ok_or_else(|| anyhow!("Could not find home directory"))?
|
||||||
|
.join(".config")
|
||||||
|
.join("opencode")
|
||||||
|
.join("skills"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn legacy_user_skill_dir() -> Result<PathBuf> {
|
||||||
Ok(dirs::home_dir()
|
Ok(dirs::home_dir()
|
||||||
.ok_or_else(|| anyhow!("Could not find home directory"))?
|
.ok_or_else(|| anyhow!("Could not find home directory"))?
|
||||||
.join(".config")
|
.join(".config")
|
||||||
@@ -1321,14 +1329,27 @@ fn user_skill_dir() -> Result<PathBuf> {
|
|||||||
|
|
||||||
fn target_skill_dir(scope: &str, working_directory: &Path, skill_name: &str) -> Result<PathBuf> {
|
fn target_skill_dir(scope: &str, working_directory: &Path, skill_name: &str) -> Result<PathBuf> {
|
||||||
if scope == "user" {
|
if scope == "user" {
|
||||||
return Ok(user_skill_dir()?.join(skill_name));
|
let preferred = user_skill_dir()?.join(skill_name);
|
||||||
|
let legacy = legacy_user_skill_dir()?.join(skill_name);
|
||||||
|
if legacy.exists() && !preferred.exists() {
|
||||||
|
return Ok(legacy);
|
||||||
|
}
|
||||||
|
return Ok(preferred);
|
||||||
}
|
}
|
||||||
|
|
||||||
if scope == "project" {
|
if scope == "project" {
|
||||||
return Ok(working_directory
|
let preferred = working_directory
|
||||||
|
.join(".opencode")
|
||||||
|
.join("skills")
|
||||||
|
.join(skill_name);
|
||||||
|
let legacy = working_directory
|
||||||
.join(".opencode")
|
.join(".opencode")
|
||||||
.join("skill")
|
.join("skill")
|
||||||
.join(skill_name));
|
.join(skill_name);
|
||||||
|
if legacy.exists() && !preferred.exists() {
|
||||||
|
return Ok(legacy);
|
||||||
|
}
|
||||||
|
return Ok(preferred);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(anyhow!("Invalid scope"))
|
Err(anyhow!("Invalid scope"))
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import yaml from 'yaml';
|
|||||||
import { parse as parseJsonc } from 'jsonc-parser';
|
import { parse as parseJsonc } from 'jsonc-parser';
|
||||||
|
|
||||||
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
||||||
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agent');
|
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
|
||||||
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'command');
|
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
|
||||||
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
||||||
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
||||||
? path.resolve(process.env.OPENCODE_CONFIG)
|
? path.resolve(process.env.OPENCODE_CONFIG)
|
||||||
@@ -43,19 +43,29 @@ const ensureDirs = () => {
|
|||||||
// ============== AGENT SCOPE HELPERS ==============
|
// ============== AGENT SCOPE HELPERS ==============
|
||||||
|
|
||||||
const ensureProjectAgentDir = (workingDirectory: string): string => {
|
const ensureProjectAgentDir = (workingDirectory: string): string => {
|
||||||
const projectAgentDir = path.join(workingDirectory, '.opencode', 'agent');
|
const projectAgentDir = path.join(workingDirectory, '.opencode', 'agents');
|
||||||
if (!fs.existsSync(projectAgentDir)) {
|
if (!fs.existsSync(projectAgentDir)) {
|
||||||
fs.mkdirSync(projectAgentDir, { recursive: true });
|
fs.mkdirSync(projectAgentDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
const legacyProjectAgentDir = path.join(workingDirectory, '.opencode', 'agent');
|
||||||
|
if (!fs.existsSync(legacyProjectAgentDir)) {
|
||||||
|
fs.mkdirSync(legacyProjectAgentDir, { recursive: true });
|
||||||
|
}
|
||||||
return projectAgentDir;
|
return projectAgentDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectAgentPath = (workingDirectory: string, agentName: string): string => {
|
const getProjectAgentPath = (workingDirectory: string, agentName: string): string => {
|
||||||
return path.join(workingDirectory, '.opencode', 'agent', `${agentName}.md`);
|
const pluralPath = path.join(workingDirectory, '.opencode', 'agents', `${agentName}.md`);
|
||||||
|
const legacyPath = path.join(workingDirectory, '.opencode', 'agent', `${agentName}.md`);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserAgentPath = (agentName: string): string => {
|
const getUserAgentPath = (agentName: string): string => {
|
||||||
return path.join(AGENT_DIR, `${agentName}.md`);
|
const pluralPath = path.join(AGENT_DIR, `${agentName}.md`);
|
||||||
|
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'agent', `${agentName}.md`);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAgentScope = (agentName: string, workingDirectory?: string): { scope: AgentScope | null; path: string | null } => {
|
export const getAgentScope = (agentName: string, workingDirectory?: string): { scope: AgentScope | null; path: string | null } => {
|
||||||
@@ -97,19 +107,29 @@ const getAgentWritePath = (agentName: string, workingDirectory?: string, request
|
|||||||
// ============== COMMAND SCOPE HELPERS ==============
|
// ============== COMMAND SCOPE HELPERS ==============
|
||||||
|
|
||||||
const ensureProjectCommandDir = (workingDirectory: string): string => {
|
const ensureProjectCommandDir = (workingDirectory: string): string => {
|
||||||
const projectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
const projectCommandDir = path.join(workingDirectory, '.opencode', 'commands');
|
||||||
if (!fs.existsSync(projectCommandDir)) {
|
if (!fs.existsSync(projectCommandDir)) {
|
||||||
fs.mkdirSync(projectCommandDir, { recursive: true });
|
fs.mkdirSync(projectCommandDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
const legacyProjectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
||||||
|
if (!fs.existsSync(legacyProjectCommandDir)) {
|
||||||
|
fs.mkdirSync(legacyProjectCommandDir, { recursive: true });
|
||||||
|
}
|
||||||
return projectCommandDir;
|
return projectCommandDir;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectCommandPath = (workingDirectory: string, commandName: string): string => {
|
const getProjectCommandPath = (workingDirectory: string, commandName: string): string => {
|
||||||
return path.join(workingDirectory, '.opencode', 'command', `${commandName}.md`);
|
const pluralPath = path.join(workingDirectory, '.opencode', 'commands', `${commandName}.md`);
|
||||||
|
const legacyPath = path.join(workingDirectory, '.opencode', 'command', `${commandName}.md`);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserCommandPath = (commandName: string): string => {
|
const getUserCommandPath = (commandName: string): string => {
|
||||||
return path.join(COMMAND_DIR, `${commandName}.md`);
|
const pluralPath = path.join(COMMAND_DIR, `${commandName}.md`);
|
||||||
|
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'command', `${commandName}.md`);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCommandScope = (commandName: string, workingDirectory?: string): { scope: CommandScope | null; path: string | null } => {
|
export const getCommandScope = (commandName: string, workingDirectory?: string): { scope: CommandScope | null; path: string | null } => {
|
||||||
@@ -870,7 +890,7 @@ export const deleteCommand = (commandName: string, workingDirectory?: string) =>
|
|||||||
|
|
||||||
// ============== SKILL SCOPE HELPERS ==============
|
// ============== SKILL SCOPE HELPERS ==============
|
||||||
|
|
||||||
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skill');
|
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
|
||||||
|
|
||||||
export const SKILL_SCOPE = {
|
export const SKILL_SCOPE = {
|
||||||
USER: 'user',
|
USER: 'user',
|
||||||
@@ -915,19 +935,31 @@ const ensureSkillDirs = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getUserSkillDir = (skillName: string): string => {
|
const getUserSkillDir = (skillName: string): string => {
|
||||||
return path.join(SKILL_DIR, skillName);
|
const pluralPath = path.join(SKILL_DIR, skillName);
|
||||||
|
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'skill', skillName);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserSkillPath = (skillName: string): string => {
|
const getUserSkillPath = (skillName: string): string => {
|
||||||
return path.join(getUserSkillDir(skillName), 'SKILL.md');
|
const pluralPath = path.join(SKILL_DIR, skillName, 'SKILL.md');
|
||||||
|
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'skill', skillName, 'SKILL.md');
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectSkillDir = (workingDirectory: string, skillName: string): string => {
|
const getProjectSkillDir = (workingDirectory: string, skillName: string): string => {
|
||||||
return path.join(workingDirectory, '.opencode', 'skill', skillName);
|
const pluralPath = path.join(workingDirectory, '.opencode', 'skills', skillName);
|
||||||
|
const legacyPath = path.join(workingDirectory, '.opencode', 'skill', skillName);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectSkillPath = (workingDirectory: string, skillName: string): string => {
|
const getProjectSkillPath = (workingDirectory: string, skillName: string): string => {
|
||||||
return path.join(getProjectSkillDir(workingDirectory, skillName), 'SKILL.md');
|
const pluralPath = path.join(workingDirectory, '.opencode', 'skills', skillName, 'SKILL.md');
|
||||||
|
const legacyPath = path.join(workingDirectory, '.opencode', 'skill', skillName, 'SKILL.md');
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getClaudeSkillDir = (workingDirectory: string, skillName: string): string => {
|
const getClaudeSkillDir = (workingDirectory: string, skillName: string): string => {
|
||||||
@@ -1001,9 +1033,9 @@ export const discoverSkills = (workingDirectory?: string): DiscoveredSkill[] =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Project level .opencode/skill/ (highest priority)
|
// 1. Project level .opencode/skills/ (highest priority)
|
||||||
if (workingDirectory) {
|
if (workingDirectory) {
|
||||||
const projectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
const projectSkillDir = path.join(workingDirectory, '.opencode', 'skills');
|
||||||
if (fs.existsSync(projectSkillDir)) {
|
if (fs.existsSync(projectSkillDir)) {
|
||||||
const entries = fs.readdirSync(projectSkillDir, { withFileTypes: true });
|
const entries = fs.readdirSync(projectSkillDir, { withFileTypes: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -1015,6 +1047,19 @@ export const discoverSkills = (workingDirectory?: string): DiscoveredSkill[] =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyProjectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
||||||
|
if (fs.existsSync(legacyProjectSkillDir)) {
|
||||||
|
const entries = fs.readdirSync(legacyProjectSkillDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const skillMdPath = path.join(legacyProjectSkillDir, entry.name, 'SKILL.md');
|
||||||
|
if (fs.existsSync(skillMdPath)) {
|
||||||
|
addSkill(entry.name, skillMdPath, SKILL_SCOPE.PROJECT, 'opencode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Claude-compatible .claude/skills/
|
// 2. Claude-compatible .claude/skills/
|
||||||
const claudeSkillDir = path.join(workingDirectory, '.claude', 'skills');
|
const claudeSkillDir = path.join(workingDirectory, '.claude', 'skills');
|
||||||
@@ -1031,7 +1076,7 @@ export const discoverSkills = (workingDirectory?: string): DiscoveredSkill[] =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. User level ~/.config/opencode/skill/
|
// 3. User level ~/.config/opencode/skills/
|
||||||
if (fs.existsSync(SKILL_DIR)) {
|
if (fs.existsSync(SKILL_DIR)) {
|
||||||
const entries = fs.readdirSync(SKILL_DIR, { withFileTypes: true });
|
const entries = fs.readdirSync(SKILL_DIR, { withFileTypes: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -1043,6 +1088,19 @@ export const discoverSkills = (workingDirectory?: string): DiscoveredSkill[] =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyUserSkillDir = path.join(OPENCODE_CONFIG_DIR, 'skill');
|
||||||
|
if (fs.existsSync(legacyUserSkillDir)) {
|
||||||
|
const entries = fs.readdirSync(legacyUserSkillDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const skillMdPath = path.join(legacyUserSkillDir, entry.name, 'SKILL.md');
|
||||||
|
if (fs.existsSync(skillMdPath)) {
|
||||||
|
addSkill(entry.name, skillMdPath, SKILL_SCOPE.USER, 'opencode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Array.from(skills.values());
|
return Array.from(skills.values());
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ export async function installSkillsFromClawdHub(options: {
|
|||||||
|
|
||||||
const targetDir = options.scope === 'user'
|
const targetDir = options.scope === 'user'
|
||||||
? path.join(userSkillDir, slug)
|
? path.join(userSkillDir, slug)
|
||||||
: path.join(options.workingDirectory as string, '.opencode', 'skill', slug);
|
: path.join(options.workingDirectory as string, '.opencode', 'skills', slug);
|
||||||
|
|
||||||
if (fs.existsSync(targetDir)) {
|
if (fs.existsSync(targetDir)) {
|
||||||
const decision = options.conflictDecisions?.[slug];
|
const decision = options.conflictDecisions?.[slug];
|
||||||
@@ -286,7 +286,7 @@ export async function installSkillsFromClawdHub(options: {
|
|||||||
|
|
||||||
const targetDir = options.scope === 'user'
|
const targetDir = options.scope === 'user'
|
||||||
? path.join(userSkillDir, slug)
|
? path.join(userSkillDir, slug)
|
||||||
: path.join(options.workingDirectory as string, '.opencode', 'skill', slug);
|
: path.join(options.workingDirectory as string, '.opencode', 'skills', slug);
|
||||||
|
|
||||||
const exists = fs.existsSync(targetDir);
|
const exists = fs.existsSync(targetDir);
|
||||||
let decision = options.conflictDecisions?.[slug] || null;
|
let decision = options.conflictDecisions?.[slug] || null;
|
||||||
@@ -653,7 +653,10 @@ async function copyDirectoryNoSymlinks(srcDir: string, dstDir: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getUserSkillBaseDir() {
|
function getUserSkillBaseDir() {
|
||||||
return path.join(os.homedir(), '.config', 'opencode', 'skill');
|
const pluralPath = path.join(os.homedir(), '.config', 'opencode', 'skills');
|
||||||
|
const legacyPath = path.join(os.homedir(), '.config', 'opencode', 'skill');
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toFsPath(repoDir: string, repoRelPosixPath: string) {
|
function toFsPath(repoDir: string, repoRelPosixPath: string) {
|
||||||
@@ -701,7 +704,7 @@ export async function installSkillsFromRepository(options: {
|
|||||||
if (!plan.installable) continue;
|
if (!plan.installable) continue;
|
||||||
const targetDir = options.scope === 'user'
|
const targetDir = options.scope === 'user'
|
||||||
? path.join(userSkillDir, plan.skillName)
|
? path.join(userSkillDir, plan.skillName)
|
||||||
: path.join(options.workingDirectory as string, '.opencode', 'skill', plan.skillName);
|
: path.join(options.workingDirectory as string, '.opencode', 'skills', plan.skillName);
|
||||||
|
|
||||||
if (fs.existsSync(targetDir)) {
|
if (fs.existsSync(targetDir)) {
|
||||||
const decision = options.conflictDecisions?.[plan.skillName];
|
const decision = options.conflictDecisions?.[plan.skillName];
|
||||||
@@ -756,7 +759,7 @@ export async function installSkillsFromRepository(options: {
|
|||||||
|
|
||||||
const targetDir = options.scope === 'user'
|
const targetDir = options.scope === 'user'
|
||||||
? path.join(userSkillDir, plan.skillName)
|
? path.join(userSkillDir, plan.skillName)
|
||||||
: path.join(options.workingDirectory as string, '.opencode', 'skill', plan.skillName);
|
: path.join(options.workingDirectory as string, '.opencode', 'skills', plan.skillName);
|
||||||
|
|
||||||
const exists = fs.existsSync(targetDir);
|
const exists = fs.existsSync(targetDir);
|
||||||
let decision = options.conflictDecisions?.[plan.skillName] || null;
|
let decision = options.conflictDecisions?.[plan.skillName] || null;
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import yaml from 'yaml';
|
|||||||
import { parse as parseJsonc } from 'jsonc-parser';
|
import { parse as parseJsonc } from 'jsonc-parser';
|
||||||
|
|
||||||
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
|
||||||
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agent');
|
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
|
||||||
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'command');
|
const COMMAND_DIR = path.join(OPENCODE_CONFIG_DIR, 'commands');
|
||||||
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skill');
|
const SKILL_DIR = path.join(OPENCODE_CONFIG_DIR, 'skills');
|
||||||
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
const CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
|
||||||
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
const CUSTOM_CONFIG_FILE = process.env.OPENCODE_CONFIG
|
||||||
? path.resolve(process.env.OPENCODE_CONFIG)
|
? path.resolve(process.env.OPENCODE_CONFIG)
|
||||||
@@ -51,10 +51,14 @@ function ensureDirs() {
|
|||||||
* Ensure project-level agent directory exists
|
* Ensure project-level agent directory exists
|
||||||
*/
|
*/
|
||||||
function ensureProjectAgentDir(workingDirectory) {
|
function ensureProjectAgentDir(workingDirectory) {
|
||||||
const projectAgentDir = path.join(workingDirectory, '.opencode', 'agent');
|
const projectAgentDir = path.join(workingDirectory, '.opencode', 'agents');
|
||||||
if (!fs.existsSync(projectAgentDir)) {
|
if (!fs.existsSync(projectAgentDir)) {
|
||||||
fs.mkdirSync(projectAgentDir, { recursive: true });
|
fs.mkdirSync(projectAgentDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
const legacyProjectAgentDir = path.join(workingDirectory, '.opencode', 'agent');
|
||||||
|
if (!fs.existsSync(legacyProjectAgentDir)) {
|
||||||
|
fs.mkdirSync(legacyProjectAgentDir, { recursive: true });
|
||||||
|
}
|
||||||
return projectAgentDir;
|
return projectAgentDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,14 +66,20 @@ function ensureProjectAgentDir(workingDirectory) {
|
|||||||
* Get project-level agent path
|
* Get project-level agent path
|
||||||
*/
|
*/
|
||||||
function getProjectAgentPath(workingDirectory, agentName) {
|
function getProjectAgentPath(workingDirectory, agentName) {
|
||||||
return path.join(workingDirectory, '.opencode', 'agent', `${agentName}.md`);
|
const pluralPath = path.join(workingDirectory, '.opencode', 'agents', `${agentName}.md`);
|
||||||
|
const legacyPath = path.join(workingDirectory, '.opencode', 'agent', `${agentName}.md`);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user-level agent path
|
* Get user-level agent path
|
||||||
*/
|
*/
|
||||||
function getUserAgentPath(agentName) {
|
function getUserAgentPath(agentName) {
|
||||||
return path.join(AGENT_DIR, `${agentName}.md`);
|
const pluralPath = path.join(AGENT_DIR, `${agentName}.md`);
|
||||||
|
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'agent', `${agentName}.md`);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,10 +183,14 @@ function getAgentPermissionSource(agentName, workingDirectory) {
|
|||||||
* Ensure project-level command directory exists
|
* Ensure project-level command directory exists
|
||||||
*/
|
*/
|
||||||
function ensureProjectCommandDir(workingDirectory) {
|
function ensureProjectCommandDir(workingDirectory) {
|
||||||
const projectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
const projectCommandDir = path.join(workingDirectory, '.opencode', 'commands');
|
||||||
if (!fs.existsSync(projectCommandDir)) {
|
if (!fs.existsSync(projectCommandDir)) {
|
||||||
fs.mkdirSync(projectCommandDir, { recursive: true });
|
fs.mkdirSync(projectCommandDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
const legacyProjectCommandDir = path.join(workingDirectory, '.opencode', 'command');
|
||||||
|
if (!fs.existsSync(legacyProjectCommandDir)) {
|
||||||
|
fs.mkdirSync(legacyProjectCommandDir, { recursive: true });
|
||||||
|
}
|
||||||
return projectCommandDir;
|
return projectCommandDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,14 +198,20 @@ function ensureProjectCommandDir(workingDirectory) {
|
|||||||
* Get project-level command path
|
* Get project-level command path
|
||||||
*/
|
*/
|
||||||
function getProjectCommandPath(workingDirectory, commandName) {
|
function getProjectCommandPath(workingDirectory, commandName) {
|
||||||
return path.join(workingDirectory, '.opencode', 'command', `${commandName}.md`);
|
const pluralPath = path.join(workingDirectory, '.opencode', 'commands', `${commandName}.md`);
|
||||||
|
const legacyPath = path.join(workingDirectory, '.opencode', 'command', `${commandName}.md`);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user-level command path
|
* Get user-level command path
|
||||||
*/
|
*/
|
||||||
function getUserCommandPath(commandName) {
|
function getUserCommandPath(commandName) {
|
||||||
return path.join(COMMAND_DIR, `${commandName}.md`);
|
const pluralPath = path.join(COMMAND_DIR, `${commandName}.md`);
|
||||||
|
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'command', `${commandName}.md`);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -245,10 +265,14 @@ function getCommandWritePath(commandName, workingDirectory, requestedScope) {
|
|||||||
* Ensure project-level skill directory exists
|
* Ensure project-level skill directory exists
|
||||||
*/
|
*/
|
||||||
function ensureProjectSkillDir(workingDirectory) {
|
function ensureProjectSkillDir(workingDirectory) {
|
||||||
const projectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
const projectSkillDir = path.join(workingDirectory, '.opencode', 'skills');
|
||||||
if (!fs.existsSync(projectSkillDir)) {
|
if (!fs.existsSync(projectSkillDir)) {
|
||||||
fs.mkdirSync(projectSkillDir, { recursive: true });
|
fs.mkdirSync(projectSkillDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
const legacyProjectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
||||||
|
if (!fs.existsSync(legacyProjectSkillDir)) {
|
||||||
|
fs.mkdirSync(legacyProjectSkillDir, { recursive: true });
|
||||||
|
}
|
||||||
return projectSkillDir;
|
return projectSkillDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,28 +280,40 @@ function ensureProjectSkillDir(workingDirectory) {
|
|||||||
* Get project-level skill directory path (.opencode/skill/{name}/)
|
* Get project-level skill directory path (.opencode/skill/{name}/)
|
||||||
*/
|
*/
|
||||||
function getProjectSkillDir(workingDirectory, skillName) {
|
function getProjectSkillDir(workingDirectory, skillName) {
|
||||||
return path.join(workingDirectory, '.opencode', 'skill', skillName);
|
const pluralPath = path.join(workingDirectory, '.opencode', 'skills', skillName);
|
||||||
|
const legacyPath = path.join(workingDirectory, '.opencode', 'skill', skillName);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get project-level skill SKILL.md path
|
* Get project-level skill SKILL.md path
|
||||||
*/
|
*/
|
||||||
function getProjectSkillPath(workingDirectory, skillName) {
|
function getProjectSkillPath(workingDirectory, skillName) {
|
||||||
return path.join(getProjectSkillDir(workingDirectory, skillName), 'SKILL.md');
|
const pluralPath = path.join(workingDirectory, '.opencode', 'skills', skillName, 'SKILL.md');
|
||||||
|
const legacyPath = path.join(workingDirectory, '.opencode', 'skill', skillName, 'SKILL.md');
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user-level skill directory path
|
* Get user-level skill directory path
|
||||||
*/
|
*/
|
||||||
function getUserSkillDir(skillName) {
|
function getUserSkillDir(skillName) {
|
||||||
return path.join(SKILL_DIR, skillName);
|
const pluralPath = path.join(SKILL_DIR, skillName);
|
||||||
|
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'skill', skillName);
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user-level skill SKILL.md path
|
* Get user-level skill SKILL.md path
|
||||||
*/
|
*/
|
||||||
function getUserSkillPath(skillName) {
|
function getUserSkillPath(skillName) {
|
||||||
return path.join(getUserSkillDir(skillName), 'SKILL.md');
|
const pluralPath = path.join(SKILL_DIR, skillName, 'SKILL.md');
|
||||||
|
const legacyPath = path.join(OPENCODE_CONFIG_DIR, 'skill', skillName, 'SKILL.md');
|
||||||
|
if (fs.existsSync(legacyPath) && !fs.existsSync(pluralPath)) return legacyPath;
|
||||||
|
return pluralPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1463,9 +1499,9 @@ function discoverSkills(workingDirectory) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. Project level .opencode/skill/ (highest priority)
|
// 1. Project level .opencode/skills/ (highest priority)
|
||||||
if (workingDirectory) {
|
if (workingDirectory) {
|
||||||
const projectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
const projectSkillDir = path.join(workingDirectory, '.opencode', 'skills');
|
||||||
if (fs.existsSync(projectSkillDir)) {
|
if (fs.existsSync(projectSkillDir)) {
|
||||||
const entries = fs.readdirSync(projectSkillDir, { withFileTypes: true });
|
const entries = fs.readdirSync(projectSkillDir, { withFileTypes: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -1477,6 +1513,19 @@ function discoverSkills(workingDirectory) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyProjectSkillDir = path.join(workingDirectory, '.opencode', 'skill');
|
||||||
|
if (fs.existsSync(legacyProjectSkillDir)) {
|
||||||
|
const entries = fs.readdirSync(legacyProjectSkillDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const skillMdPath = path.join(legacyProjectSkillDir, entry.name, 'SKILL.md');
|
||||||
|
if (fs.existsSync(skillMdPath)) {
|
||||||
|
addSkill(entry.name, skillMdPath, SKILL_SCOPE.PROJECT, 'opencode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Claude-compatible .claude/skills/
|
// 2. Claude-compatible .claude/skills/
|
||||||
const claudeSkillDir = path.join(workingDirectory, '.claude', 'skills');
|
const claudeSkillDir = path.join(workingDirectory, '.claude', 'skills');
|
||||||
@@ -1493,7 +1542,7 @@ function discoverSkills(workingDirectory) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. User level ~/.config/opencode/skill/
|
// 3. User level ~/.config/opencode/skills/
|
||||||
if (fs.existsSync(SKILL_DIR)) {
|
if (fs.existsSync(SKILL_DIR)) {
|
||||||
const entries = fs.readdirSync(SKILL_DIR, { withFileTypes: true });
|
const entries = fs.readdirSync(SKILL_DIR, { withFileTypes: true });
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@@ -1505,6 +1554,19 @@ function discoverSkills(workingDirectory) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyUserSkillDir = path.join(OPENCODE_CONFIG_DIR, 'skill');
|
||||||
|
if (fs.existsSync(legacyUserSkillDir)) {
|
||||||
|
const entries = fs.readdirSync(legacyUserSkillDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const skillMdPath = path.join(legacyUserSkillDir, entry.name, 'SKILL.md');
|
||||||
|
if (fs.existsSync(skillMdPath)) {
|
||||||
|
addSkill(entry.name, skillMdPath, SKILL_SCOPE.USER, 'opencode');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Array.from(skills.values());
|
return Array.from(skills.values());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ import { downloadClawdHubSkill, fetchClawdHubSkillInfo } from './api.js';
|
|||||||
|
|
||||||
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
||||||
|
|
||||||
|
function normalizeUserSkillDir(userSkillDir) {
|
||||||
|
if (!userSkillDir) return null;
|
||||||
|
const legacySkillDir = path.join(os.homedir(), '.config', 'opencode', 'skill');
|
||||||
|
const pluralSkillDir = path.join(os.homedir(), '.config', 'opencode', 'skills');
|
||||||
|
if (userSkillDir === legacySkillDir) {
|
||||||
|
if (fs.existsSync(legacySkillDir) && !fs.existsSync(pluralSkillDir)) return legacySkillDir;
|
||||||
|
return pluralSkillDir;
|
||||||
|
}
|
||||||
|
return userSkillDir;
|
||||||
|
}
|
||||||
|
|
||||||
function validateSkillName(skillName) {
|
function validateSkillName(skillName) {
|
||||||
if (typeof skillName !== 'string') return false;
|
if (typeof skillName !== 'string') return false;
|
||||||
if (skillName.length < 1 || skillName.length > 64) return false;
|
if (skillName.length < 1 || skillName.length > 64) return false;
|
||||||
@@ -41,7 +52,7 @@ function getTargetSkillDir({ scope, workingDirectory, userSkillDir, skillName })
|
|||||||
throw new Error('workingDirectory is required for project installs');
|
throw new Error('workingDirectory is required for project installs');
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.join(workingDirectory, '.opencode', 'skill', skillName);
|
return path.join(workingDirectory, '.opencode', 'skills', skillName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +82,11 @@ export async function installSkillsFromClawdHub({
|
|||||||
return { ok: false, error: { kind: 'unknown', message: 'userSkillDir is required' } };
|
return { ok: false, error: { kind: 'unknown', message: 'userSkillDir is required' } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedUserSkillDir = normalizeUserSkillDir(userSkillDir);
|
||||||
|
if (normalizedUserSkillDir) {
|
||||||
|
userSkillDir = normalizedUserSkillDir;
|
||||||
|
}
|
||||||
|
|
||||||
if (scope === 'project' && !workingDirectory) {
|
if (scope === 'project' && !workingDirectory) {
|
||||||
return { ok: false, error: { kind: 'invalidSource', message: 'Project installs require a directory parameter' } };
|
return { ok: false, error: { kind: 'invalidSource', message: 'Project installs require a directory parameter' } };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ import { parseSkillRepoSource } from './source.js';
|
|||||||
|
|
||||||
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
|
||||||
|
|
||||||
|
function normalizeUserSkillDir(userSkillDir) {
|
||||||
|
if (!userSkillDir) return null;
|
||||||
|
const legacySkillDir = path.join(os.homedir(), '.config', 'opencode', 'skill');
|
||||||
|
const pluralSkillDir = path.join(os.homedir(), '.config', 'opencode', 'skills');
|
||||||
|
if (userSkillDir === legacySkillDir) {
|
||||||
|
if (fs.existsSync(legacySkillDir) && !fs.existsSync(pluralSkillDir)) return legacySkillDir;
|
||||||
|
return pluralSkillDir;
|
||||||
|
}
|
||||||
|
return userSkillDir;
|
||||||
|
}
|
||||||
|
|
||||||
function validateSkillName(skillName) {
|
function validateSkillName(skillName) {
|
||||||
if (typeof skillName !== 'string') return false;
|
if (typeof skillName !== 'string') return false;
|
||||||
if (skillName.length < 1 || skillName.length > 64) return false;
|
if (skillName.length < 1 || skillName.length > 64) return false;
|
||||||
@@ -103,7 +114,7 @@ function getTargetSkillDir({ scope, workingDirectory, userSkillDir, skillName })
|
|||||||
throw new Error('workingDirectory is required for project installs');
|
throw new Error('workingDirectory is required for project installs');
|
||||||
}
|
}
|
||||||
|
|
||||||
return path.join(workingDirectory, '.opencode', 'skill', skillName);
|
return path.join(workingDirectory, '.opencode', 'skills', skillName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installSkillsFromRepository({
|
export async function installSkillsFromRepository({
|
||||||
@@ -123,14 +134,19 @@ export async function installSkillsFromRepository({
|
|||||||
return { ok: false, error: gitCheck.error };
|
return { ok: false, error: gitCheck.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scope !== 'user' && scope !== 'project') {
|
const normalizedUserSkillDir = normalizeUserSkillDir(userSkillDir);
|
||||||
return { ok: false, error: { kind: 'invalidSource', message: 'Invalid scope' } };
|
if (normalizedUserSkillDir) {
|
||||||
|
userSkillDir = normalizedUserSkillDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userSkillDir) {
|
if (!userSkillDir) {
|
||||||
return { ok: false, error: { kind: 'unknown', message: 'userSkillDir is required' } };
|
return { ok: false, error: { kind: 'unknown', message: 'userSkillDir is required' } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scope !== 'user' && scope !== 'project') {
|
||||||
|
return { ok: false, error: { kind: 'invalidSource', message: 'Invalid scope' } };
|
||||||
|
}
|
||||||
|
|
||||||
if (scope === 'project' && !workingDirectory) {
|
if (scope === 'project' && !workingDirectory) {
|
||||||
return { ok: false, error: { kind: 'invalidSource', message: 'Project installs require a directory parameter' } };
|
return { ok: false, error: { kind: 'invalidSource', message: 'Project installs require a directory parameter' } };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user