148 lines
4.5 KiB
TypeScript
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));
|
|
});
|
|
}
|