feat(skills): expose list command for skill visibility

This commit is contained in:
William Valentin
2026-02-12 16:42:00 -08:00
parent 90ce622080
commit b3e5aee333
5 changed files with 168 additions and 4 deletions
+85
View File
@@ -0,0 +1,85 @@
import type { Command } from 'commander';
import { resolve } from 'path';
import { homedir } from 'os';
import type { Skill } from '../skills/index.js';
import { loadAllSkills } from '../skills/index.js';
import { loadConfigSafe } from './shared.js';
export interface SkillListRow {
name: string;
tier: Skill['manifest']['tier'];
status: 'available' | 'unavailable';
reason?: string;
}
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
return skills
.map((skill) => ({
name: skill.manifest.name,
tier: skill.manifest.tier,
status: (skill.available ? 'available' : 'unavailable') as SkillListRow['status'],
reason: skill.unavailableReasons?.join('; '),
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
export function renderSkillsTable(rows: SkillListRow[]): string {
if (rows.length === 0) {
return 'No skills found.';
}
const nameWidth = Math.max('NAME'.length, ...rows.map((row) => row.name.length));
const tierWidth = Math.max('TIER'.length, ...rows.map((row) => row.tier.length));
const statusWidth = Math.max('STATUS'.length, ...rows.map((row) => row.status.length));
const lines = [
`${'NAME'.padEnd(nameWidth)} ${'TIER'.padEnd(tierWidth)} ${'STATUS'.padEnd(statusWidth)} REASON`,
`${'-'.repeat(nameWidth)} ${'-'.repeat(tierWidth)} ${'-'.repeat(statusWidth)} ------`,
];
for (const row of rows) {
lines.push(
`${row.name.padEnd(nameWidth)} ${row.tier.padEnd(tierWidth)} ${row.status.padEnd(statusWidth)} ${row.reason ?? ''}`,
);
}
return lines.join('\n');
}
export function registerSkillsCommand(program: Command): void {
const skills = program
.command('skills')
.description('Manage Flynn skills')
.action(() => {
skills.outputHelp();
});
skills
.command('list')
.description('List discovered skills')
.option('--json', 'Output as JSON')
.option('-c, --config <path>', 'Config file path')
.action((opts: { json?: boolean; config?: string }) => {
const loaded = loadConfigSafe(opts.config);
if (loaded.error || !loaded.config) {
console.error(loaded.error ?? 'Failed to load config');
process.exitCode = 1;
return;
}
const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills');
const skills = loadAllSkills({
bundledDir: loaded.config.skills.bundled_dir,
managedDir: loaded.config.skills.managed_dir ?? defaultManagedDir,
workspaceDir: loaded.config.skills.workspace_dir,
});
const rows = toSkillListRows(skills);
if (opts.json) {
console.log(JSON.stringify(rows, null, 2));
return;
}
console.log(renderSkillsTable(rows));
});
}