fix(skills): drive UI rename gating from server renamable flag

Expose authoritative renamable on skill list responses using the same
managed-root policy as renameSkill, drop the divergent UI path heuristic,
and remove an unused rejection-test fixture.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-03 09:25:25 +00:00
co-authored by Serhii Dziupin
parent bfea13ef1d
commit 20fc675af0
10 changed files with 35 additions and 40 deletions
@@ -352,6 +352,7 @@ an authoritative loopback callback URL even when OpenChamber binds port `0`.
- `registerSkillRoutes(app, dependencies)`: registers skills-related routes:
- Skills config CRUD and metadata under `/api/config/skills*`
- Skill rename via `PATCH /api/config/skills/:name` with `{ renameTo }` (directory rename preserves `SKILL.md` body and supporting files; restricted to managed skill roots under `.opencode/skills|skill`, `.claude/skills`, and `.agents/skills`)
- Skill list responses include authoritative `renamable` derived from the same managed-root policy used by rename
- Skills catalog listing/source pagination, scan, and install routes
- Supporting skill file read/write/delete routes
@@ -38,7 +38,7 @@ import {
decodePluginId,
} from './plugins.js';
import { SKILL_DIR, SKILL_SCOPE, readSkillSupportingFile, writeSkillSupportingFile, deleteSkillSupportingFile } from './shared.js';
import { getSkillSources, discoverSkills, mergeDiscoveredSkills, createSkill, updateSkill, deleteSkill, renameSkill } from './skills.js';
import { getSkillSources, discoverSkills, mergeDiscoveredSkills, createSkill, updateSkill, deleteSkill, renameSkill, isManagedSkillPath } from './skills.js';
import { getCuratedSkillsSources } from '../skills-catalog/curated-sources.js';
import { getCacheKey, getCachedScan, setCachedScan } from '../skills-catalog/cache.js';
import { isClawdHubSource, parseSkillRepoSource } from '../skills-catalog/source.js';
@@ -257,6 +257,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
updateSkill,
deleteSkill,
renameSkill,
isManagedSkillPath,
readSkillSupportingFile,
writeSkillSupportingFile,
deleteSkillSupportingFile,
@@ -22,6 +22,7 @@ export const registerSkillRoutes = (app, dependencies) => {
updateSkill,
deleteSkill,
renameSkill,
isManagedSkillPath,
readSkillSupportingFile,
writeSkillSupportingFile,
deleteSkillSupportingFile,
@@ -213,9 +214,15 @@ export const registerSkillRoutes = (app, dependencies) => {
const enrichedSkills = skills.map((skill) => {
const sources = getSkillSources(skill.name, directory, skill);
const skillPath = typeof skill.path === 'string' ? skill.path : null;
return {
...skill,
sources
sources,
renamable: Boolean(
skillPath
&& skillPath !== '<built-in>'
&& isManagedSkillPath(skillPath, directory)
),
};
});
@@ -734,4 +734,5 @@ export {
updateSkill,
deleteSkill,
renameSkill,
isManagedSkillPath,
};
@@ -224,7 +224,6 @@ describe('skills', () => {
const managedDir = path.join(projectRoot, '.opencode', 'skills', 'managed-skill');
const conflictDir = path.join(projectRoot, '.opencode', 'skills', 'taken-name');
const mismatchDir = path.join(projectRoot, '.opencode', 'skills', 'folder-name');
const unmanagedDir = path.join(projectRoot, 'custom-skills', 'unmanaged-skill');
const cacheStamp = `oc-rename-${Date.now()}`;
const cacheDir = path.join(os.homedir(), '.cache', 'opencode', 'skills', cacheStamp, 'cache-skill');
@@ -274,21 +273,6 @@ describe('skills', () => {
'utf8',
);
await fsPromises.mkdir(unmanagedDir, { recursive: true });
await fsPromises.writeFile(
path.join(unmanagedDir, 'SKILL.md'),
[
'---',
'name: unmanaged-skill',
'description: Unmanaged',
'---',
'',
'Unmanaged body',
'',
].join('\n'),
'utf8',
);
await fsPromises.mkdir(cacheDir, { recursive: true });
await fsPromises.writeFile(
path.join(cacheDir, 'SKILL.md'),