Files
flynn/src/cli/skills.ts
T
2026-02-12 16:44:46 -08:00

148 lines
4.5 KiB
TypeScript

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 renderSkillInfo(skill: Skill): string {
const lines: string[] = [
`Name: ${skill.manifest.name}`,
`Description: ${skill.manifest.description}`,
`Version: ${skill.manifest.version}`,
`Tier: ${skill.manifest.tier}`,
`Status: ${skill.available ? 'available' : 'unavailable'}`,
`Directory: ${skill.directory}`,
];
if (skill.manifest.author) {
lines.push(`Author: ${skill.manifest.author}`);
}
if (skill.manifest.tools && skill.manifest.tools.length > 0) {
lines.push(`Tools: ${skill.manifest.tools.join(', ')}`);
}
if (skill.unavailableReasons && skill.unavailableReasons.length > 0) {
lines.push(`Unavailable reasons: ${skill.unavailableReasons.join('; ')}`);
}
return lines.join('\n');
}
function loadSkillsFromConfig(configPath?: string): { skills?: Skill[]; error?: string } {
const loaded = loadConfigSafe(configPath);
if (loaded.error || !loaded.config) {
return { error: loaded.error ?? 'Failed to load config' };
}
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,
});
return { skills };
}
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 = loadSkillsFromConfig(opts.config);
if (loaded.error || !loaded.skills) {
console.error(loaded.error ?? 'Failed to load skills');
process.exitCode = 1;
return;
}
const rows = toSkillListRows(loaded.skills);
if (opts.json) {
console.log(JSON.stringify(rows, null, 2));
return;
}
console.log(renderSkillsTable(rows));
});
skills
.command('info <name>')
.description('Show details for a skill')
.option('--json', 'Output as JSON')
.option('-c, --config <path>', 'Config file path')
.action((name: string, opts: { json?: boolean; config?: string }) => {
const loaded = loadSkillsFromConfig(opts.config);
if (loaded.error || !loaded.skills) {
console.error(loaded.error ?? 'Failed to load skills');
process.exitCode = 1;
return;
}
const skill = loaded.skills.find((item) => item.manifest.name === name);
if (!skill) {
console.error(`Skill '${name}' not found.`);
process.exitCode = 1;
return;
}
if (opts.json) {
console.log(JSON.stringify(skill, null, 2));
return;
}
console.log(renderSkillInfo(skill));
});
}