feat(skills): align discovery with Opencode API and improve skills editor UX (#441)

This commit is contained in:
Bohdan Triapitsyn
2026-02-18 12:26:08 +02:00
committed by GitHub
parent 7eba5141fa
commit 14737b6b28
15 changed files with 1231 additions and 286 deletions
@@ -105,8 +105,13 @@ async function cloneRepo({ cloneUrl, identity, tempDir }) {
};
}
function getTargetSkillDir({ scope, workingDirectory, userSkillDir, skillName }) {
function getTargetSkillDir({ scope, targetSource, workingDirectory, userSkillDir, skillName }) {
const source = targetSource === 'agents' ? 'agents' : 'opencode';
if (scope === 'user') {
if (source === 'agents') {
return path.join(os.homedir(), '.agents', 'skills', skillName);
}
return path.join(userSkillDir, skillName);
}
@@ -114,6 +119,10 @@ function getTargetSkillDir({ scope, workingDirectory, userSkillDir, skillName })
throw new Error('workingDirectory is required for project installs');
}
if (source === 'agents') {
return path.join(workingDirectory, '.agents', 'skills', skillName);
}
return path.join(workingDirectory, '.opencode', 'skills', skillName);
}
@@ -123,6 +132,7 @@ export async function installSkillsFromRepository({
defaultSubpath,
identity,
scope,
targetSource,
workingDirectory,
userSkillDir,
selections,
@@ -147,6 +157,10 @@ export async function installSkillsFromRepository({
return { ok: false, error: { kind: 'invalidSource', message: 'Invalid scope' } };
}
if (targetSource !== undefined && targetSource !== 'opencode' && targetSource !== 'agents') {
return { ok: false, error: { kind: 'invalidSource', message: 'Invalid target source' } };
}
if (scope === 'project' && !workingDirectory) {
return { ok: false, error: { kind: 'invalidSource', message: 'Project installs require a directory parameter' } };
}
@@ -178,12 +192,12 @@ export async function installSkillsFromRepository({
continue;
}
const targetDir = getTargetSkillDir({ scope, workingDirectory, userSkillDir, skillName: plan.skillName });
const targetDir = getTargetSkillDir({ scope, targetSource, workingDirectory, userSkillDir, skillName: plan.skillName });
if (fs.existsSync(targetDir)) {
const decision = conflictDecisions?.[plan.skillName];
const hasAutoPolicy = conflictPolicy === 'skipAll' || conflictPolicy === 'overwriteAll';
if (!decision && !hasAutoPolicy) {
conflicts.push({ skillName: plan.skillName, scope });
conflicts.push({ skillName: plan.skillName, scope, source: targetSource === 'agents' ? 'agents' : 'opencode' });
}
}
}
@@ -239,7 +253,7 @@ export async function installSkillsFromRepository({
continue;
}
const targetDir = getTargetSkillDir({ scope, workingDirectory, userSkillDir, skillName: plan.skillName });
const targetDir = getTargetSkillDir({ scope, targetSource, workingDirectory, userSkillDir, skillName: plan.skillName });
const exists = fs.existsSync(targetDir);
let decision = conflictDecisions?.[plan.skillName] || null;
@@ -263,7 +277,7 @@ export async function installSkillsFromRepository({
try {
await copyDirectoryNoSymlinks(srcDir, targetDir);
installed.push({ skillName: plan.skillName, scope });
installed.push({ skillName: plan.skillName, scope, source: targetSource === 'agents' ? 'agents' : 'opencode' });
} catch (error) {
await safeRm(targetDir);
skipped.push({