diff --git a/skillhub-cli/.npmignore b/skillhub-cli/.npmignore index c59f4093..f8d3a0da 100644 --- a/skillhub-cli/.npmignore +++ b/skillhub-cli/.npmignore @@ -5,3 +5,10 @@ unbuild.config.ts tsconfig.json *.test.ts src/ +.claude/ +.omc/ +AGENTS.md +README.md +Makefile +docs/ +scripts/ \ No newline at end of file diff --git a/skillhub-cli/package.json b/skillhub-cli/package.json index 09ee421f..b9735d4a 100644 --- a/skillhub-cli/package.json +++ b/skillhub-cli/package.json @@ -1,6 +1,6 @@ { "name": "motovis-skillhub", - "version": "1.1.0", + "version": "1.1.1", "type": "module", "description": "SkillHub CLI - 企业级 Agent Skill 管理工具,支持命名空间", "bin": { diff --git a/skillhub-cli/src/commands/list.ts b/skillhub-cli/src/commands/list.ts index 7a4acaba..85a4f8be 100644 --- a/skillhub-cli/src/commands/list.ts +++ b/skillhub-cli/src/commands/list.ts @@ -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, getUniversalAgents, getNonUniversalAgents } from "../core/agent-detector.js"; +import { getAllAgents, isUniversalForScope } 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"; @@ -53,11 +53,20 @@ export function registerList(program: Command) { } } - const universalAgents = getUniversalAgents(); - const nonUniversalAgents = getNonUniversalAgents(); + // Determine scope for dynamic universal grouping + const isGlobal = scopeGlobal === true; + const allAgents = getAllAgents(); + const universalAgents = allAgents + .filter((a) => isUniversalForScope(a, isGlobal) && a.showInUniversalList !== false) + .sort((a, b) => a.name.localeCompare(b.name)); + const nonUniversalAgents = allAgents + .filter((a) => !isUniversalForScope(a, isGlobal)) + .sort((a, b) => a.name.localeCompare(b.name)); + + const canonicalLabel = isGlobal ? "Universal (~/.agents/skills)" : "Universal (.agents/skills)"; const universalSection = { - title: "Universal (.agents/skills)", + title: canonicalLabel, items: universalAgents.map((a) => ({ value: a.key, label: a.name, @@ -81,7 +90,7 @@ export function registerList(program: Command) { } const selectedAgents = agentSelection as string[]; - const agents = getAllAgents().filter((a) => selectedAgents.includes(a.key)); + const agents = allAgents.filter((a) => selectedAgents.includes(a.key)); if (agents.length === 0) { console.log("No agents selected."); @@ -90,44 +99,76 @@ export function registerList(program: Command) { console.log(""); - let found = false; + // Collect all skill entries grouped by (skillName, path) -> agentNames + const skillMap = new Map>(); + const home = homedir(); + const cwd = process.cwd(); + for (const agent of agents) { const showProject = scopeGlobal === null || scopeGlobal === false; const showGlobal = scopeGlobal === null || scopeGlobal === true; if (showProject) { - const projectDir = join(process.cwd(), agent.skillsDir); - const skills = getSkillsInDir(projectDir); - if (skills.length > 0) { - found = true; - info(`\n${agent.name} (project):`); - for (const s of skills) { - dim(` ${s}`); - } - } + const projectDir = join(cwd, agent.skillsDir); + collectSkills(skillMap, projectDir, agent.name, cwd, true); } if (showGlobal && agent.globalSkillsDir) { - const globalDir = join(homedir(), agent.globalSkillsDir); - const skills = getSkillsInDir(globalDir); - if (skills.length > 0) { - found = true; - info(`\n${agent.name} (global):`); - for (const s of skills) { - dim(` ${s}`); - } - } + const globalDir = join(home, agent.globalSkillsDir); + collectSkills(skillMap, globalDir, agent.name, home, false); } } - if (!found) { + if (skillMap.size === 0) { dim("No skills installed for selected agents and scope."); + } else { + // Output grouped by skill, then by path with agent names merged + const sortedSkills = [...skillMap.entries()].sort((a, b) => a[0].localeCompare(b[0])); + for (const [skillName, pathGroups] of sortedSkills) { + info(`${skillName}`); + const sortedPaths = [...pathGroups.entries()].sort((a, b) => a[0].localeCompare(b[0])); + for (const [displayPath, agentNames] of sortedPaths) { + const sorted = agentNames.sort((a, b) => a.localeCompare(b)); + const label = sorted.length <= 5 + ? sorted.join(", ") + : sorted.slice(0, 5).join(", ") + ` ${pc.dim(`+${sorted.length - 5}`)}`; + dim(` ${pc.dim("→")} ${label}: ${displayPath}`); + } + } } console.log(""); }); } +/** + * Collect skills from a directory into the skillMap. + * skillMap: skillName -> (displayPath -> agentNames[]) + */ +function collectSkills( + skillMap: Map>, + dir: string, + agentName: string, + baseForRelative: string, + isProject: boolean, +) { + if (!existsSync(dir)) return; + const skills = getSkillsInDir(dir); + for (const skillName of skills) { + const displayPath = isProject + ? dir.replace(baseForRelative, ".") + : dir.replace(baseForRelative, "~"); + let pathGroups = skillMap.get(skillName); + if (!pathGroups) { + pathGroups = new Map(); + skillMap.set(skillName, pathGroups); + } + const agents = pathGroups.get(displayPath) || []; + agents.push(agentName); + pathGroups.set(displayPath, agents); + } +} + function getSkillsInDir(dir: string): string[] { if (!existsSync(dir)) return []; return readdirSync(dir).filter((f) => { @@ -138,5 +179,5 @@ function getSkillsInDir(dir: string): string[] { } catch { return false; } - }); + }).sort((a, b) => a.localeCompare(b)); } diff --git a/skillhub-cli/src/commands/uninstall.ts b/skillhub-cli/src/commands/uninstall.ts index 0675c2c7..88541177 100644 --- a/skillhub-cli/src/commands/uninstall.ts +++ b/skillhub-cli/src/commands/uninstall.ts @@ -2,11 +2,12 @@ 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, isUniversalForScope, getUniversalAgents, getNonUniversalAgents, type AgentInfo } from "../core/agent-detector.js"; +import { getAllAgents, isUniversalForScope, 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"; import * as p from "@clack/prompts"; +import pc from "picocolors"; function removeDir(path: string) { try { @@ -100,7 +101,7 @@ function discoverInstalledSkills(scope: "local" | "global", agent?: AgentInfo): } catch {} } - return [...new Set(skills)]; + return [...new Set(skills)].sort((a, b) => a.localeCompare(b)); } function findAgentsWithSkill(skillName: string, scope: "global" | "local", agents: AgentInfo[]): AgentInfo[] { @@ -145,6 +146,7 @@ export function registerUninstall(program: Command) { } const allAgents = getAllAgents(); + const isGlobal = scope === "global"; if (opts.all) { const skills = discoverInstalledSkills(scope); @@ -166,18 +168,19 @@ export function registerUninstall(program: Command) { } const selectedSkills = selected as string[]; - let uninstalled = 0; + const results: { skill: string; agent: string; path: string; ok: boolean }[] = []; for (const skill of selectedSkills) { const agentsWithSkill = findAgentsWithSkill(skill, scope, allAgents); for (const agent of agentsWithSkill) { const ok = await uninstallSkill(skill, agent, scope, true); - if (ok) uninstalled++; + const skillPath = getSkillPath(skill, agent, scope); + results.push({ skill, agent: agent.name, path: skillPath || "", ok }); } await removeFromLock(skill); } - success(`Uninstalled ${uninstalled} skill(s).`); + printUninstallResults(results); return; } @@ -201,18 +204,19 @@ export function registerUninstall(program: Command) { } const selectedSkills = selected as string[]; - let uninstalled = 0; + const results: { skill: string; agent: string; path: string; ok: boolean }[] = []; for (const skill of selectedSkills) { const agentsWithSkill = findAgentsWithSkill(skill, scope, allAgents); for (const agent of agentsWithSkill) { const ok = await uninstallSkill(skill, agent, scope, !!opts.yes); - if (ok) uninstalled++; + const skillPath = getSkillPath(skill, agent, scope); + results.push({ skill, agent: agent.name, path: skillPath || "", ok }); } await removeFromLock(skill); } - success(`Uninstalled ${uninstalled} skill(s).`); + printUninstallResults(results); return; } @@ -240,11 +244,17 @@ export function registerUninstall(program: Command) { } } - const universalAgents = getUniversalAgents(); - const nonUniversalAgents = getNonUniversalAgents(); + // Dynamic universal grouping based on scope + const universalAgents = allAgents + .filter((a) => isUniversalForScope(a, isGlobal) && a.showInUniversalList !== false) + .sort((a, b) => a.name.localeCompare(b.name)); + const nonUniversalAgents = allAgents + .filter((a) => !isUniversalForScope(a, isGlobal)) + .sort((a, b) => a.name.localeCompare(b.name)); + const canonicalLabel = isGlobal ? "Universal (~/.agents/skills)" : "Universal (.agents/skills)"; const universalSection = { - title: "Universal (.agents/skills)", + title: canonicalLabel, items: universalAgents .filter((a) => agentsWithSkill.some((w) => w.key === a.key)) .map((a) => ({ @@ -317,12 +327,13 @@ export function registerUninstall(program: Command) { if (pathToAgents.size > 0) { const lines: string[] = []; - for (const [path, agents] of pathToAgents) { - if (agents.length > 1) { - lines.push(` ${agents.join(", ")} (${path})`); - } else { - lines.push(` ${agents[0]} (${path})`); - } + const sortedEntries = [...pathToAgents.entries()].sort((a, b) => a[0].localeCompare(b[0])); + for (const [path, agents] of sortedEntries) { + const sorted = agents.sort((a, b) => a.localeCompare(b)); + const label = sorted.length <= 5 + ? sorted.join(", ") + : sorted.slice(0, 5).join(", ") + ` ${pc.dim(`+${sorted.length - 5}`)}`; + lines.push(` ${pc.dim("→")} ${label}: ${pc.dim(path)}`); } success(`Uninstalled ${name} from ${selectedAgentKeys.length} agent(s):`); console.log(lines.join("\n")); @@ -332,3 +343,51 @@ export function registerUninstall(program: Command) { } }); } + +/** + * Print uninstall results grouped by skill and path (consistent with install output format). + */ +function printUninstallResults(results: { skill: string; agent: string; path: string; ok: boolean }[]) { + const successful = results.filter((r) => r.ok); + if (successful.length === 0) { + info("No skills were uninstalled."); + return; + } + + // Group by skill + const skillGroups = new Map(); + for (const r of successful) { + let group = skillGroups.get(r.skill); + if (!group) { + group = []; + skillGroups.set(r.skill, group); + } + group.push({ agent: r.agent, path: r.path }); + } + + const lines: string[] = []; + const sortedSkills = [...skillGroups.entries()].sort((a, b) => a[0].localeCompare(b[0])); + + for (const [skillName, entries] of sortedSkills) { + lines.push(`${pc.green("✓")} ${skillName}`); + + // Group by path + const pathGroups = new Map(); + for (const e of entries) { + const agents = pathGroups.get(e.path) || []; + agents.push(e.agent); + pathGroups.set(e.path, agents); + } + + const sortedPaths = [...pathGroups.entries()].sort((a, b) => a[0].localeCompare(b[0])); + for (const [path, agents] of sortedPaths) { + const sorted = agents.sort((a, b) => a.localeCompare(b)); + const label = sorted.length <= 5 + ? sorted.join(", ") + : sorted.slice(0, 5).join(", ") + ` ${pc.dim(`+${sorted.length - 5}`)}`; + lines.push(` ${pc.dim("→")} ${label}: ${pc.dim(path)}`); + } + } + + p.note(lines.join("\n"), `Uninstalled ${successful.length} skill(s)`); +}