feat(skills): expose list command for skill visibility
This commit is contained in:
+19
-4
@@ -1203,9 +1203,24 @@
|
||||
"phases": {
|
||||
"phase_1_command_dispatch": {
|
||||
"priority": "P0",
|
||||
"status": "not_started",
|
||||
"status": "in_progress",
|
||||
"description": "flynn skills CLI commands (list/info/install/uninstall/refresh) with doctor enhancement",
|
||||
"effort": "2-3 hours"
|
||||
"effort": "2-3 hours",
|
||||
"sub_slices": {
|
||||
"skills_list_command": {
|
||||
"status": "completed",
|
||||
"description": "Added `flynn skills list` command dispatch with table/JSON output and CLI registration",
|
||||
"files_created": [
|
||||
"src/cli/skills.ts",
|
||||
"src/cli/skills.test.ts"
|
||||
],
|
||||
"files_modified": [
|
||||
"src/cli/index.ts",
|
||||
"src/cli/index.test.ts"
|
||||
],
|
||||
"test_status": "typecheck + targeted skills/index CLI tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
||||
}
|
||||
}
|
||||
},
|
||||
"phase_2_skills_watcher": {
|
||||
"priority": "P1",
|
||||
@@ -1244,7 +1259,7 @@
|
||||
},
|
||||
|
||||
"overall_progress": {
|
||||
"total_test_count": 1490,
|
||||
"total_test_count": 1493,
|
||||
"all_tests_passing": true,
|
||||
"p0_completion": "3/3 (100%)",
|
||||
"p1_completion": "4/4 (100%)",
|
||||
@@ -1264,7 +1279,7 @@
|
||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
||||
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 2/2 (100%) — component registry, confidence routing. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||
"next_up": "Skills infrastructure implementation (Phase 1: command dispatch)"
|
||||
"next_up": "Skills infrastructure Phase 1: add `flynn skills info <name>` command dispatch"
|
||||
},
|
||||
"soul_md_and_cron_create": {
|
||||
"date": "2026-02-11",
|
||||
|
||||
@@ -12,6 +12,7 @@ describe('CLI program', () => {
|
||||
expect(commandNames).toContain('sessions');
|
||||
expect(commandNames).toContain('doctor');
|
||||
expect(commandNames).toContain('config');
|
||||
expect(commandNames).toContain('skills');
|
||||
});
|
||||
|
||||
it('has version info', () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { registerGcalAuthCommand } from './gcal-auth.js';
|
||||
import { registerGdocsAuthCommand } from './gdocs-auth.js';
|
||||
import { registerGdriveAuthCommand } from './gdrive-auth.js';
|
||||
import { registerGtasksAuthCommand } from './gtasks-auth.js';
|
||||
import { registerSkillsCommand } from './skills.js';
|
||||
|
||||
export function createProgram(): Command {
|
||||
const program = new Command();
|
||||
@@ -40,6 +41,7 @@ export function createProgram(): Command {
|
||||
registerGdocsAuthCommand(program);
|
||||
registerGdriveAuthCommand(program);
|
||||
registerGtasksAuthCommand(program);
|
||||
registerSkillsCommand(program);
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { toSkillListRows, renderSkillsTable } from './skills.js';
|
||||
import type { Skill } from '../skills/index.js';
|
||||
|
||||
function buildSkill(overrides: Partial<Skill>): Skill {
|
||||
return {
|
||||
manifest: {
|
||||
name: 'sample-skill',
|
||||
description: 'Sample skill',
|
||||
version: '1.0.0',
|
||||
tier: 'workspace',
|
||||
...overrides.manifest,
|
||||
},
|
||||
instructions: '# Sample skill',
|
||||
directory: '/tmp/sample-skill',
|
||||
available: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('skills CLI helpers', () => {
|
||||
it('maps and sorts skill rows', () => {
|
||||
const rows = toSkillListRows([
|
||||
buildSkill({
|
||||
manifest: {
|
||||
name: 'zeta',
|
||||
description: 'zeta',
|
||||
version: '1.0.0',
|
||||
tier: 'managed',
|
||||
},
|
||||
}),
|
||||
buildSkill({
|
||||
manifest: {
|
||||
name: 'alpha',
|
||||
description: 'alpha',
|
||||
version: '1.0.0',
|
||||
tier: 'bundled',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(rows.map((row) => row.name)).toEqual(['alpha', 'zeta']);
|
||||
expect(rows[0]?.status).toBe('available');
|
||||
});
|
||||
|
||||
it('includes unavailable reason text', () => {
|
||||
const rows = toSkillListRows([
|
||||
buildSkill({
|
||||
available: false,
|
||||
unavailableReasons: ['Required binary not found', 'Missing API key'],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(rows[0]?.status).toBe('unavailable');
|
||||
expect(rows[0]?.reason).toBe('Required binary not found; Missing API key');
|
||||
});
|
||||
|
||||
it('renders a no-skills message when empty', () => {
|
||||
expect(renderSkillsTable([])).toBe('No skills found.');
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user