mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-03 02:24:36 +00:00
feat(cli): add scope-aware universal grouping and improve output formatting
- list.ts: use isUniversalForScope for dynamic grouping based on scope - list.ts: group output by skill name to avoid duplicate path listings - uninstall.ts: use isUniversalForScope for dynamic grouping - uninstall.ts: add printUninstallResults for consistent formatted output - .npmignore: exclude development files from npm package - package.json: bump version to 1.1.1
This commit is contained in:
parent
e5da7397fa
commit
04bc62fed4
4 changed files with 151 additions and 44 deletions
|
|
@ -5,3 +5,10 @@ unbuild.config.ts
|
|||
tsconfig.json
|
||||
*.test.ts
|
||||
src/
|
||||
.claude/
|
||||
.omc/
|
||||
AGENTS.md
|
||||
README.md
|
||||
Makefile
|
||||
docs/
|
||||
scripts/
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "motovis-skillhub",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"description": "SkillHub CLI - 企业级 Agent Skill 管理工具,支持命名空间",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -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<string, Map<string, string[]>>();
|
||||
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<string, Map<string, string[]>>,
|
||||
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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, { agent: string; path: string }[]>();
|
||||
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<string, string[]>();
|
||||
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)`);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue