mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-15 23:31:10 +00:00
refactor(cli): use dynamic isUniversalForScope instead of static isUniversalAgent
- Add isUniversalForScope(agent, isGlobal) and getAgentTargetDir(agent, isGlobal) to agent-detector.ts - An agent is 'universal' when its target install dir equals the canonical .agents/skills directory - This fixes incorrect symlink skipping for agents like Codex/Cursor whose globalSkillsDir differs from .agents/skills (e.g. .codex/skills, .cursor/skills) - Update installer.ts to use dynamic check via optional AgentInfo parameter - Update install.ts to use getAgentTargetDir and pass agent info to installSkill - Update uninstall.ts to use isUniversalForScope for correct path resolution - Clean up unused isUniversalAgent import in list.ts
This commit is contained in:
parent
ea4e206ad6
commit
32395ee5c2
5 changed files with 112 additions and 55 deletions
|
|
@ -8,7 +8,7 @@ import { loadConfig } from "../core/config.js";
|
|||
import { readToken } from "../core/auth-token.js";
|
||||
import { discoverSkills } from "../core/skill-discovery.js";
|
||||
import { installSkill } from "../core/installer.js";
|
||||
import { getAllAgents, detectInstalledAgents, getUniversalAgents, getNonUniversalAgents, isUniversalAgent } from "../core/agent-detector.js";
|
||||
import { getAllAgents, detectInstalledAgents, getUniversalAgents, getNonUniversalAgents, isUniversalForScope, getAgentTargetDir, type AgentInfo } from "../core/agent-detector.js";
|
||||
import { parseSource, getCloneUrl } from "../core/source-parser.js";
|
||||
import { addToLock } from "../core/skill-lock.js";
|
||||
import { success, error, info, dim } from "../utils/logger.js";
|
||||
|
|
@ -107,10 +107,10 @@ async function selectInstallMode(): Promise<"symlink" | "copy" | null> {
|
|||
return result as "symlink" | "copy";
|
||||
}
|
||||
|
||||
function buildAgentSummary(targetAgents: { key: string; name: string; skillsDir: string }[], mode: "symlink" | "copy"): string[] {
|
||||
function buildAgentSummary(targetAgents: AgentInfo[], mode: "symlink" | "copy", isGlobal: boolean): string[] {
|
||||
const lines: string[] = [];
|
||||
const universal = targetAgents.filter((a) => isUniversalAgent(a));
|
||||
const symlinked = targetAgents.filter((a) => !isUniversalAgent(a));
|
||||
const universal = targetAgents.filter((a) => isUniversalForScope(a, isGlobal));
|
||||
const symlinked = targetAgents.filter((a) => !isUniversalForScope(a, isGlobal));
|
||||
|
||||
if (mode === "symlink") {
|
||||
if (universal.length > 0) {
|
||||
|
|
@ -400,9 +400,7 @@ async function installFromRegistry(slug: string, opts: Record<string, string | s
|
|||
|
||||
// Only prompt for install mode when there are multiple unique target directories.
|
||||
// When all selected agents share the same skillsDir, symlink vs copy is meaningless.
|
||||
const uniqueDirs = new Set(targetAgents.map((a) =>
|
||||
isGlobal ? (a.globalSkillsDir || a.skillsDir) : a.skillsDir
|
||||
));
|
||||
const uniqueDirs = new Set(targetAgents.map((a) => getAgentTargetDir(a, isGlobal)));
|
||||
|
||||
if (uniqueDirs.size <= 1) {
|
||||
// Single target directory — default to copy (no symlink needed)
|
||||
|
|
@ -425,7 +423,7 @@ async function installFromRegistry(slug: string, opts: Record<string, string | s
|
|||
? `~/.agents/skills/${skill.name}`
|
||||
: `./.agents/skills/${skill.name}`;
|
||||
summaryLines.push(`${pc.cyan(canonicalPath)}`);
|
||||
for (const line of buildAgentSummary(targetAgents, mode)) {
|
||||
for (const line of buildAgentSummary(targetAgents, mode, isGlobal)) {
|
||||
summaryLines.push(` ${line}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -457,9 +455,10 @@ async function installFromRegistry(slug: string, opts: Record<string, string | s
|
|||
skill.dir,
|
||||
skill.name,
|
||||
agent.key,
|
||||
isGlobal ? agent.globalSkillsDir || agent.skillsDir : agent.skillsDir,
|
||||
getAgentTargetDir(agent, isGlobal),
|
||||
mode,
|
||||
isGlobal,
|
||||
agent,
|
||||
);
|
||||
results.push({
|
||||
skill: skill.name,
|
||||
|
|
@ -659,9 +658,7 @@ async function installFromGit(skillName: string, source: string, sourceType: Sou
|
|||
|
||||
// Only prompt for install mode when there are multiple unique target directories.
|
||||
// When all selected agents share the same skillsDir, symlink vs copy is meaningless.
|
||||
const uniqueDirs = new Set(targetAgents.map((a) =>
|
||||
isGlobal ? (a.globalSkillsDir || a.skillsDir) : a.skillsDir
|
||||
));
|
||||
const uniqueDirs = new Set(targetAgents.map((a) => getAgentTargetDir(a, isGlobal)));
|
||||
|
||||
if (uniqueDirs.size <= 1) {
|
||||
// Single target directory — default to copy (no symlink needed)
|
||||
|
|
@ -684,7 +681,7 @@ async function installFromGit(skillName: string, source: string, sourceType: Sou
|
|||
? `~/.agents/skills/${skill.name}`
|
||||
: `./.agents/skills/${skill.name}`;
|
||||
summaryLines.push(`${pc.cyan(canonicalPath)}`);
|
||||
for (const line of buildAgentSummary(targetAgents, mode)) {
|
||||
for (const line of buildAgentSummary(targetAgents, mode, isGlobal)) {
|
||||
summaryLines.push(` ${line}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -716,9 +713,10 @@ async function installFromGit(skillName: string, source: string, sourceType: Sou
|
|||
skill.dir,
|
||||
skill.name,
|
||||
agent.key,
|
||||
isGlobal ? agent.globalSkillsDir || agent.skillsDir : agent.skillsDir,
|
||||
getAgentTargetDir(agent, isGlobal),
|
||||
mode,
|
||||
isGlobal,
|
||||
agent,
|
||||
);
|
||||
results.push({
|
||||
skill: skill.name,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Command } from "commander";
|
|||
import { existsSync, readdirSync, lstatSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { getAllAgents, isUniversalAgent, getUniversalAgents, getNonUniversalAgents } from "../core/agent-detector.js";
|
||||
import { getAllAgents, getUniversalAgents, getNonUniversalAgents } from "../core/agent-detector.js";
|
||||
import { info, dim } from "../utils/logger.js";
|
||||
import { searchMultiselect, cancelSymbol } from "../utils/search-multiselect.js";
|
||||
import * as p from "@clack/prompts";
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Command } from "commander";
|
|||
import { existsSync, readdirSync, statSync, unlinkSync, rmdirSync, lstatSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { getAllAgents, isUniversalAgent, getUniversalAgents, getNonUniversalAgents, type AgentInfo } from "../core/agent-detector.js";
|
||||
import { getAllAgents, isUniversalForScope, getUniversalAgents, getNonUniversalAgents, type AgentInfo } from "../core/agent-detector.js";
|
||||
import { success, info, dim } from "../utils/logger.js";
|
||||
import { removeFromLock } from "../core/skill-lock.js";
|
||||
import { searchMultiselect, cancelSymbol } from "../utils/search-multiselect.js";
|
||||
|
|
@ -34,7 +34,7 @@ async function uninstallSkill(
|
|||
let baseDir: string;
|
||||
|
||||
if (scope === "global") {
|
||||
if (isUniversalAgent(agent)) {
|
||||
if (isUniversalForScope(agent, true)) {
|
||||
baseDir = join(home, ".agents/skills");
|
||||
} else {
|
||||
baseDir = join(home, agent.globalSkillsDir || agent.skillsDir);
|
||||
|
|
@ -65,7 +65,7 @@ function getSkillPath(skillName: string, agent: AgentInfo, scope: "global" | "lo
|
|||
let baseDir: string;
|
||||
|
||||
if (scope === "global") {
|
||||
if (isUniversalAgent(agent)) {
|
||||
if (isUniversalForScope(agent, true)) {
|
||||
baseDir = join(home, ".agents/skills");
|
||||
} else {
|
||||
baseDir = join(home, agent.globalSkillsDir || agent.skillsDir);
|
||||
|
|
|
|||
|
|
@ -7,15 +7,19 @@ export interface AgentInfo {
|
|||
name: string;
|
||||
skillsDir: string;
|
||||
globalSkillsDir?: string;
|
||||
/** Whether to show this agent in the Universal section of interactive prompts.
|
||||
* Agents with skillsDir === ".agents/skills" are universal by default,
|
||||
* but some (like "replit" for cloud environments) should be hidden. */
|
||||
showInUniversalList?: boolean;
|
||||
}
|
||||
|
||||
const home = homedir();
|
||||
|
||||
const AGENTS: AgentInfo[] = [
|
||||
// Universal agents (.agents/skills)
|
||||
// Universal agents (.agents/skills) — share the canonical .agents/skills directory
|
||||
{ key: "amp", name: "Amp", skillsDir: ".agents/skills", globalSkillsDir: ".config/agents/skills" },
|
||||
{ key: "antigravity", name: "Antigravity", skillsDir: ".agents/skills", globalSkillsDir: ".gemini/antigravity/skills" },
|
||||
{ key: "cline", name: "Cline", skillsDir: ".agents/skills" },
|
||||
{ key: "cline", name: "Cline", skillsDir: ".agents/skills", globalSkillsDir: ".agents/skills" },
|
||||
{ key: "codex", name: "Codex", skillsDir: ".agents/skills", globalSkillsDir: ".codex/skills" },
|
||||
{ key: "cursor", name: "Cursor", skillsDir: ".agents/skills", globalSkillsDir: ".cursor/skills" },
|
||||
{ key: "deepagents", name: "Deep Agents", skillsDir: ".agents/skills", globalSkillsDir: ".deepagents/agent/skills" },
|
||||
|
|
@ -23,40 +27,44 @@ const AGENTS: AgentInfo[] = [
|
|||
{ key: "gemini-cli", name: "Gemini CLI", skillsDir: ".agents/skills", globalSkillsDir: ".gemini/skills" },
|
||||
{ key: "github-copilot", name: "GitHub Copilot", skillsDir: ".agents/skills", globalSkillsDir: ".copilot/skills" },
|
||||
{ key: "kimi-cli", name: "Kimi Code CLI", skillsDir: ".agents/skills", globalSkillsDir: ".config/agents/skills" },
|
||||
{ key: "kilo", name: "Kilo Code", skillsDir: ".agents/skills", globalSkillsDir: ".kilocode/skills" },
|
||||
{ key: "mux", name: "Mux", skillsDir: ".agents/skills" },
|
||||
{ key: "opencode", name: "OpenCode", skillsDir: ".agents/skills", globalSkillsDir: ".config/opencode/skills" },
|
||||
{ key: "replit", name: "Replit", skillsDir: ".agents/skills" },
|
||||
{ key: "warp", name: "Warp", skillsDir: ".agents/skills" },
|
||||
{ key: "warp", name: "Warp", skillsDir: ".agents/skills", globalSkillsDir: ".agents/skills" },
|
||||
// Universal agents hidden from the interactive list
|
||||
{ key: "replit", name: "Replit", skillsDir: ".agents/skills", globalSkillsDir: ".config/agents/skills", showInUniversalList: false },
|
||||
{ key: "universal", name: "Universal", skillsDir: ".agents/skills", globalSkillsDir: ".config/agents/skills", showInUniversalList: false },
|
||||
|
||||
// Agent-specific path agents
|
||||
// Agent-specific path agents (non-universal)
|
||||
{ key: "claude-code", name: "Claude Code", skillsDir: ".claude/skills", globalSkillsDir: ".claude/skills" },
|
||||
{ key: "augment", name: "Augment", skillsDir: ".augment/skills" },
|
||||
{ key: "bob", name: "IBM Bob", skillsDir: ".bob/skills" },
|
||||
{ key: "augment", name: "Augment", skillsDir: ".augment/skills", globalSkillsDir: ".augment/skills" },
|
||||
{ key: "bob", name: "IBM Bob", skillsDir: ".bob/skills", globalSkillsDir: ".bob/skills" },
|
||||
{ key: "openclaw", name: "OpenClaw", skillsDir: "skills", globalSkillsDir: ".openclaw/skills" },
|
||||
{ key: "codebuddy", name: "CodeBuddy", skillsDir: ".codebuddy/skills" },
|
||||
{ key: "continue", name: "Continue", skillsDir: ".continue/skills" },
|
||||
{ key: "codebuddy", name: "CodeBuddy", skillsDir: ".codebuddy/skills", globalSkillsDir: ".codebuddy/skills" },
|
||||
{ key: "command-code", name: "Command Code", skillsDir: ".commandcode/skills", globalSkillsDir: ".commandcode/skills" },
|
||||
{ key: "continue", name: "Continue", skillsDir: ".continue/skills", globalSkillsDir: ".continue/skills" },
|
||||
{ key: "cortex", name: "Cortex Code", skillsDir: ".cortex/skills", globalSkillsDir: ".snowflake/cortex/skills" },
|
||||
{ key: "crush", name: "Crush", skillsDir: ".crush/skills", globalSkillsDir: ".config/crush/skills" },
|
||||
{ key: "droid", name: "Droid", skillsDir: ".factory/skills" },
|
||||
{ key: "droid", name: "Droid", skillsDir: ".factory/skills", globalSkillsDir: ".factory/skills" },
|
||||
{ key: "goose", name: "Goose", skillsDir: ".goose/skills", globalSkillsDir: ".config/goose/skills" },
|
||||
{ key: "junie", name: "Junie", skillsDir: ".junie/skills" },
|
||||
{ key: "iflow-cli", name: "iFlow CLI", skillsDir: ".iflow/skills" },
|
||||
{ key: "kode", name: "Kode", skillsDir: ".kode/skills" },
|
||||
{ key: "mcpjam", name: "MCPJam", skillsDir: ".mcpjam/skills" },
|
||||
{ key: "mistral-vibe", name: "Mistral Vibe", skillsDir: ".vibe/skills" },
|
||||
{ key: "openhands", name: "OpenHands", skillsDir: ".openhands/skills" },
|
||||
{ key: "junie", name: "Junie", skillsDir: ".junie/skills", globalSkillsDir: ".junie/skills" },
|
||||
{ key: "iflow-cli", name: "iFlow CLI", skillsDir: ".iflow/skills", globalSkillsDir: ".iflow/skills" },
|
||||
{ key: "kilo", name: "Kilo Code", skillsDir: ".kilocode/skills", globalSkillsDir: ".kilocode/skills" },
|
||||
{ key: "kiro-cli", name: "Kiro CLI", skillsDir: ".kiro/skills", globalSkillsDir: ".kiro/skills" },
|
||||
{ key: "kode", name: "Kode", skillsDir: ".kode/skills", globalSkillsDir: ".kode/skills" },
|
||||
{ key: "mcpjam", name: "MCPJam", skillsDir: ".mcpjam/skills", globalSkillsDir: ".mcpjam/skills" },
|
||||
{ key: "mistral-vibe", name: "Mistral Vibe", skillsDir: ".vibe/skills", globalSkillsDir: ".vibe/skills" },
|
||||
{ key: "mux", name: "Mux", skillsDir: ".mux/skills", globalSkillsDir: ".mux/skills" },
|
||||
{ key: "openhands", name: "OpenHands", skillsDir: ".openhands/skills", globalSkillsDir: ".openhands/skills" },
|
||||
{ key: "pi", name: "Pi", skillsDir: ".pi/skills", globalSkillsDir: ".pi/agent/skills" },
|
||||
{ key: "qoder", name: "Qoder", skillsDir: ".qoder/skills" },
|
||||
{ key: "qwen-code", name: "Qwen Code", skillsDir: ".qwen/skills" },
|
||||
{ key: "roo", name: "Roo Code", skillsDir: ".roo/skills" },
|
||||
{ key: "trae", name: "Trae", skillsDir: ".trae/skills" },
|
||||
{ key: "qoder", name: "Qoder", skillsDir: ".qoder/skills", globalSkillsDir: ".qoder/skills" },
|
||||
{ key: "qwen-code", name: "Qwen Code", skillsDir: ".qwen/skills", globalSkillsDir: ".qwen/skills" },
|
||||
{ key: "roo", name: "Roo Code", skillsDir: ".roo/skills", globalSkillsDir: ".roo/skills" },
|
||||
{ key: "trae", name: "Trae", skillsDir: ".trae/skills", globalSkillsDir: ".trae/skills" },
|
||||
{ key: "trae-cn", name: "Trae CN", skillsDir: ".trae/skills", globalSkillsDir: ".trae-cn/skills" },
|
||||
{ key: "windsurf", name: "Windsurf", skillsDir: ".windsurf/skills", globalSkillsDir: ".codeium/windsurf/skills" },
|
||||
{ key: "zencoder", name: "Zencoder", skillsDir: ".zencoder/skills" },
|
||||
{ key: "neovate", name: "Neovate", skillsDir: ".neovate/skills" },
|
||||
{ key: "pochi", name: "Pochi", skillsDir: ".pochi/skills" },
|
||||
{ key: "adal", name: "AdaL", skillsDir: ".adal/skills" },
|
||||
{ key: "zencoder", name: "Zencoder", skillsDir: ".zencoder/skills", globalSkillsDir: ".zencoder/skills" },
|
||||
{ key: "neovate", name: "Neovate", skillsDir: ".neovate/skills", globalSkillsDir: ".neovate/skills" },
|
||||
{ key: "pochi", name: "Pochi", skillsDir: ".pochi/skills", globalSkillsDir: ".pochi/skills" },
|
||||
{ key: "adal", name: "AdaL", skillsDir: ".adal/skills", globalSkillsDir: ".adal/skills" },
|
||||
];
|
||||
|
||||
export function getAllAgents(): AgentInfo[] {
|
||||
|
|
@ -74,16 +82,65 @@ export function getAgentByKey(key: string): AgentInfo | undefined {
|
|||
return AGENTS.find((a) => a.key === key);
|
||||
}
|
||||
|
||||
const UNIVERSAL_PATH = ".agents/skills";
|
||||
const CANONICAL_SKILLS_DIR = ".agents/skills";
|
||||
|
||||
/**
|
||||
* Check if an agent uses the canonical .agents/skills directory at the project level.
|
||||
* Used for UI grouping (Universal section in interactive prompts).
|
||||
*/
|
||||
export function isUniversalAgent(agent: AgentInfo): boolean {
|
||||
return agent.skillsDir === UNIVERSAL_PATH;
|
||||
return agent.skillsDir === CANONICAL_SKILLS_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target installation directory for an agent in the given scope.
|
||||
* In global scope, uses globalSkillsDir if defined, otherwise falls back to skillsDir.
|
||||
* In project scope, always uses skillsDir.
|
||||
*/
|
||||
export function getAgentTargetDir(agent: AgentInfo, isGlobal: boolean): string {
|
||||
return isGlobal
|
||||
? (agent.globalSkillsDir || agent.skillsDir)
|
||||
: agent.skillsDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically determine if an agent is "universal" for the given scope.
|
||||
* An agent is universal when its target installation directory equals the canonical
|
||||
* .agents/skills directory — meaning no symlink is needed because the canonical
|
||||
* location IS the agent's own directory.
|
||||
*
|
||||
* This differs from isUniversalAgent() which only checks project-level skillsDir.
|
||||
* For example, Codex has skillsDir=".agents/skills" (universal at project level)
|
||||
* but globalSkillsDir=".codex/skills" (NOT universal at global level — needs symlink).
|
||||
*/
|
||||
export function isUniversalForScope(agent: AgentInfo, isGlobal: boolean): boolean {
|
||||
return getAgentTargetDir(agent, isGlobal) === CANONICAL_SKILLS_DIR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns universal agents that should appear in the interactive selection list.
|
||||
* Excludes agents with showInUniversalList === false (e.g. replit, which is cloud-only).
|
||||
*/
|
||||
export function getUniversalAgents(): AgentInfo[] {
|
||||
return AGENTS.filter((a) => isUniversalAgent(a));
|
||||
return AGENTS.filter((a) => isUniversalAgent(a) && a.showInUniversalList !== false);
|
||||
}
|
||||
|
||||
export function getNonUniversalAgents(): AgentInfo[] {
|
||||
return AGENTS.filter((a) => !isUniversalAgent(a));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that all universal agents are included in the target agent list.
|
||||
* This guarantees that skills are always installed to ~/.agents/skills (the canonical location),
|
||||
* making them available to any agent that reads from that directory.
|
||||
*/
|
||||
export function ensureUniversalAgents(targetAgents: AgentInfo[]): AgentInfo[] {
|
||||
const universalAgents = getUniversalAgents();
|
||||
const result = [...targetAgents];
|
||||
for (const ua of universalAgents) {
|
||||
if (!result.some((a) => a.key === ua.key)) {
|
||||
result.push(ua);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { mkdirSync, symlinkSync, copyFileSync, readdirSync, lstatSync, unlinkSync, existsSync } from "node:fs";
|
||||
import { join, dirname, relative } from "node:path";
|
||||
import { homedir, platform } from "node:os";
|
||||
import { isUniversalForScope, type AgentInfo } from "./agent-detector.js";
|
||||
|
||||
export interface SkillInstallResult {
|
||||
skillName: string;
|
||||
|
|
@ -11,15 +12,11 @@ export interface SkillInstallResult {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
const UNIVERSAL_PATH = ".agents/skills";
|
||||
|
||||
function isUniversalAgent(skillsDir: string): boolean {
|
||||
return skillsDir === UNIVERSAL_PATH;
|
||||
}
|
||||
const CANONICAL_SKILLS_DIR = ".agents/skills";
|
||||
|
||||
function getCanonicalBase(isGlobal: boolean, cwd: string): string {
|
||||
const home = homedir();
|
||||
return isGlobal ? join(home, UNIVERSAL_PATH) : join(cwd, UNIVERSAL_PATH);
|
||||
return isGlobal ? join(home, CANONICAL_SKILLS_DIR) : join(cwd, CANONICAL_SKILLS_DIR);
|
||||
}
|
||||
|
||||
function getAgentBaseDir(skillsDir: string, isGlobal: boolean, cwd: string): string {
|
||||
|
|
@ -83,6 +80,7 @@ export function installSkill(
|
|||
targetDir: string,
|
||||
mode: "symlink" | "copy",
|
||||
isGlobal: boolean,
|
||||
agent?: AgentInfo,
|
||||
): SkillInstallResult {
|
||||
const cwd = process.cwd();
|
||||
const canonicalBase = getCanonicalBase(isGlobal, cwd);
|
||||
|
|
@ -90,7 +88,11 @@ export function installSkill(
|
|||
const agentBase = getAgentBaseDir(targetDir, isGlobal, cwd);
|
||||
const agentDir = join(agentBase, skillName);
|
||||
|
||||
const agentIsUniversal = isUniversalAgent(targetDir);
|
||||
// Use dynamic scope-aware universal check if agent info is available,
|
||||
// otherwise fall back to static targetDir check
|
||||
const agentIsUniversal = agent
|
||||
? isUniversalForScope(agent, isGlobal)
|
||||
: targetDir === CANONICAL_SKILLS_DIR;
|
||||
|
||||
try {
|
||||
if (mode === "copy") {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue