mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-05 02:41:49 +00:00
feat(cli): improve list and check commands with better filtering
list.ts: - Remove locked universal section, all agents are now equal - Users manually select which agents to list from - No default selections, cleaner UX check.ts: - Add agent filtering via --agent flag or interactive selection - Add status filtering via --status flag or interactive selection - Default to showing only OK + Missing (not Orphaned) - Interactive prompts for scope, agents, and statuses - Show agent names and scope prefix in output - Smart defaults reduce noise from orphaned skills
This commit is contained in:
parent
3cf4d64cf5
commit
ee18161d56
2 changed files with 185 additions and 54 deletions
|
|
@ -2,9 +2,11 @@ import { Command } from "commander";
|
|||
import { existsSync, readdirSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import { getAllAgents } from "../core/agent-detector.js";
|
||||
import { getAllAgents, type AgentInfo } from "../core/agent-detector.js";
|
||||
import { getAllLockedSkills, getSkillLockPath } from "../core/skill-lock.js";
|
||||
import { success, error, info, warn, dim } from "../utils/logger.js";
|
||||
import * as p from "@clack/prompts";
|
||||
import { searchMultiselect, cancelSymbol } from "../utils/search-multiselect.js";
|
||||
|
||||
interface CheckResult {
|
||||
name: string;
|
||||
|
|
@ -13,11 +15,15 @@ interface CheckResult {
|
|||
location?: string;
|
||||
}
|
||||
|
||||
function findInstalledSkills(scope: "local" | "global"): Map<string, string[]> {
|
||||
function findInstalledSkills(
|
||||
scope: "local" | "global",
|
||||
agents?: AgentInfo[]
|
||||
): Map<string, string[]> {
|
||||
const skillsMap = new Map<string, string[]>();
|
||||
const agents = getAllAgents();
|
||||
const allAgents = getAllAgents();
|
||||
const targetAgents = agents || allAgents;
|
||||
|
||||
for (const agent of agents) {
|
||||
for (const agent of targetAgents) {
|
||||
const baseDir = scope === "global"
|
||||
? join(homedir(), agent.globalSkillsDir || agent.skillsDir)
|
||||
: join(process.cwd(), agent.skillsDir);
|
||||
|
|
@ -44,9 +50,118 @@ export function registerCheck(program: Command) {
|
|||
.command("check")
|
||||
.description("Check installed skills against lock file")
|
||||
.option("--global", "Check global scope skills")
|
||||
.option("--local", "Check local (project) scope skills")
|
||||
.option("--all", "Check both global and local scopes")
|
||||
.option("--agent <agents...>", "Filter by specific agents")
|
||||
.option("--status <status...>", "Filter by status (ok, missing, orphaned)")
|
||||
.option("--json", "Output results as JSON")
|
||||
.action(async (opts: { global?: boolean; json?: boolean }) => {
|
||||
const scope = opts.global ? "global" : "local";
|
||||
.action(async (opts: {
|
||||
global?: boolean;
|
||||
local?: boolean;
|
||||
all?: boolean;
|
||||
agent?: string[];
|
||||
status?: string[];
|
||||
json?: boolean;
|
||||
}) => {
|
||||
// Determine scopes
|
||||
let scopes: ("local" | "global")[] = [];
|
||||
|
||||
if (opts.all) {
|
||||
scopes = ["local", "global"];
|
||||
} else if (opts.global) {
|
||||
scopes = ["global"];
|
||||
} else if (opts.local) {
|
||||
scopes = ["local"];
|
||||
} else {
|
||||
// Interactive scope selection
|
||||
const scopeSelection = await p.select({
|
||||
message: "Which scope to check?",
|
||||
options: [
|
||||
{ value: "all", label: "All (global + project)" },
|
||||
{ value: "global", label: "Global only" },
|
||||
{ value: "local", label: "Project only" },
|
||||
],
|
||||
});
|
||||
|
||||
if (p.isCancel(scopeSelection)) {
|
||||
console.log("Cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (scopeSelection === "all") {
|
||||
scopes = ["local", "global"];
|
||||
} else if (scopeSelection === "global") {
|
||||
scopes = ["global"];
|
||||
} else {
|
||||
scopes = ["local"];
|
||||
}
|
||||
}
|
||||
|
||||
// Determine agents to check
|
||||
let targetAgents: AgentInfo[] | undefined;
|
||||
if (opts.agent && opts.agent.length > 0) {
|
||||
const allAgents = getAllAgents();
|
||||
targetAgents = allAgents.filter((a) => opts.agent!.includes(a.key));
|
||||
} else if (!opts.agent) {
|
||||
// Interactive agent selection
|
||||
const allAgents = getAllAgents();
|
||||
const agentItems = allAgents
|
||||
.map((a) => ({
|
||||
value: a.key,
|
||||
label: a.name,
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const selected = await searchMultiselect({
|
||||
message: "Which agents to check?",
|
||||
items: agentItems,
|
||||
required: false,
|
||||
});
|
||||
|
||||
if (selected === cancelSymbol) {
|
||||
console.log("Cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (selected && selected.length > 0) {
|
||||
targetAgents = allAgents.filter((a) => (selected as string[]).includes(a.key));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which statuses to show
|
||||
let showOk = false;
|
||||
let showMissing = false;
|
||||
let showOrphaned = false;
|
||||
|
||||
if (opts.status && opts.status.length > 0) {
|
||||
// Command line flags
|
||||
showOk = opts.status.includes("ok");
|
||||
showMissing = opts.status.includes("missing");
|
||||
showOrphaned = opts.status.includes("orphaned");
|
||||
} else {
|
||||
// Interactive status selection (default: ok + missing only)
|
||||
const statusSelection = await p.multiselect({
|
||||
message: "Which statuses to show?",
|
||||
options: [
|
||||
{ value: "ok", label: "OK (installed and in lock file)" },
|
||||
{ value: "missing", label: "Missing (in lock file but not installed)" },
|
||||
{ value: "orphaned", label: "Orphaned (installed but not in lock file)" },
|
||||
],
|
||||
required: false,
|
||||
initialValues: ["ok", "missing"],
|
||||
});
|
||||
|
||||
if (p.isCancel(statusSelection)) {
|
||||
console.log("Cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = statusSelection as string[];
|
||||
showOk = selected.includes("ok");
|
||||
showMissing = selected.includes("missing");
|
||||
showOrphaned = selected.includes("orphaned");
|
||||
}
|
||||
|
||||
const lockPath = getSkillLockPath();
|
||||
|
||||
if (!existsSync(lockPath)) {
|
||||
|
|
@ -59,21 +174,46 @@ export function registerCheck(program: Command) {
|
|||
}
|
||||
|
||||
const lockedSkills = await getAllLockedSkills();
|
||||
const installedSkills = findInstalledSkills(scope);
|
||||
const allResults: CheckResult[] = [];
|
||||
|
||||
const results: CheckResult[] = [];
|
||||
// Check each scope
|
||||
for (const scope of scopes) {
|
||||
const installedSkills = findInstalledSkills(scope, targetAgents);
|
||||
|
||||
for (const [name, entry] of Object.entries(lockedSkills)) {
|
||||
const installedLocations = installedSkills.get(name);
|
||||
if (installedLocations && installedLocations.length > 0) {
|
||||
allResults.push({
|
||||
name,
|
||||
status: "ok",
|
||||
source: entry.source,
|
||||
location: `${scope}: ${installedLocations.sort((a, b) => a.localeCompare(b)).join(", ")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, locations] of installedSkills.entries()) {
|
||||
if (!lockedSkills[name]) {
|
||||
allResults.push({
|
||||
name,
|
||||
status: "orphaned",
|
||||
location: `${scope}: ${locations.sort((a, b) => a.localeCompare(b)).join(", ")}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark missing skills (not found in any scope)
|
||||
const checkedNames = new Set<string>();
|
||||
for (const r of allResults) {
|
||||
if (r.status !== "orphaned") {
|
||||
checkedNames.add(r.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, entry] of Object.entries(lockedSkills)) {
|
||||
const installedLocations = installedSkills.get(name);
|
||||
if (installedLocations && installedLocations.length > 0) {
|
||||
results.push({
|
||||
name,
|
||||
status: "ok",
|
||||
source: entry.source,
|
||||
location: installedLocations.sort((a, b) => a.localeCompare(b)).join(", "),
|
||||
});
|
||||
} else {
|
||||
results.push({
|
||||
if (!checkedNames.has(name)) {
|
||||
allResults.push({
|
||||
name,
|
||||
status: "missing",
|
||||
source: entry.source,
|
||||
|
|
@ -81,41 +221,46 @@ export function registerCheck(program: Command) {
|
|||
}
|
||||
}
|
||||
|
||||
for (const [name, locations] of installedSkills.entries()) {
|
||||
if (!lockedSkills[name]) {
|
||||
results.push({
|
||||
name,
|
||||
status: "orphaned",
|
||||
location: locations.sort((a, b) => a.localeCompare(b)).join(", "),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort results: ok → missing → orphaned, then alphabetically by name
|
||||
results.sort((a, b) => {
|
||||
allResults.sort((a, b) => {
|
||||
const order = { ok: 0, missing: 1, orphaned: 2 };
|
||||
const diff = order[a.status] - order[b.status];
|
||||
return diff !== 0 ? diff : a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(results, null, 2));
|
||||
console.log(JSON.stringify(allResults, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter results by selected statuses
|
||||
const filteredResults = allResults.filter((r) => {
|
||||
if (r.status === "ok") return showOk;
|
||||
if (r.status === "missing") return showMissing;
|
||||
if (r.status === "orphaned") return showOrphaned;
|
||||
return false;
|
||||
});
|
||||
|
||||
const scopeLabel = scopes.length === 2 ? "all scopes" : `${scopes[0]} scope`;
|
||||
const agentLabel = targetAgents
|
||||
? ` (${targetAgents.map((a) => a.name).sort((a, b) => a.localeCompare(b)).join(", ")})`
|
||||
: "";
|
||||
|
||||
console.log("");
|
||||
info(`SkillHub Lock Check (${scope} scope):`);
|
||||
info(`SkillHub Lock Check (${scopeLabel})${agentLabel}:`);
|
||||
console.log("");
|
||||
|
||||
if (results.length === 0) {
|
||||
dim(" No skills found.");
|
||||
if (filteredResults.length === 0) {
|
||||
dim(" No matching skills found.");
|
||||
console.log("");
|
||||
return;
|
||||
}
|
||||
|
||||
let ok = 0, missing = 0, orphaned = 0;
|
||||
let ok = 0,
|
||||
missing = 0,
|
||||
orphaned = 0;
|
||||
|
||||
for (const r of results) {
|
||||
for (const r of filteredResults) {
|
||||
if (r.status === "ok") {
|
||||
ok++;
|
||||
success(` ✓ ${r.name}`);
|
||||
|
|
|
|||
|
|
@ -57,31 +57,17 @@ export function registerList(program: Command) {
|
|||
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: canonicalLabel,
|
||||
items: universalAgents.map((a) => ({
|
||||
// All agents are selectable - no locked section
|
||||
const selectableItems = allAgents
|
||||
.map((a) => ({
|
||||
value: a.key,
|
||||
label: a.name,
|
||||
})),
|
||||
};
|
||||
|
||||
const selectableItems = nonUniversalAgents.map((a) => ({
|
||||
value: a.key,
|
||||
label: a.name,
|
||||
}));
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
const agentSelection = await searchMultiselect({
|
||||
message: "Which agents to list from?",
|
||||
items: selectableItems,
|
||||
lockedSection: universalSection,
|
||||
});
|
||||
|
||||
if (agentSelection === cancelSymbol) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue