feat(skills): add info command for skill inspection
This commit is contained in:
+11
-2
@@ -1219,6 +1219,15 @@
|
|||||||
"src/cli/index.test.ts"
|
"src/cli/index.test.ts"
|
||||||
],
|
],
|
||||||
"test_status": "typecheck + targeted skills/index CLI tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
"test_status": "typecheck + targeted skills/index CLI tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
||||||
|
},
|
||||||
|
"skills_info_command": {
|
||||||
|
"status": "completed",
|
||||||
|
"description": "Added `flynn skills info <name>` command dispatch with human-readable and JSON output for discovered skills",
|
||||||
|
"files_modified": [
|
||||||
|
"src/cli/skills.ts",
|
||||||
|
"src/cli/skills.test.ts"
|
||||||
|
],
|
||||||
|
"test_status": "typecheck + targeted skills CLI tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1259,7 +1268,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
"total_test_count": 1493,
|
"total_test_count": 1495,
|
||||||
"all_tests_passing": true,
|
"all_tests_passing": true,
|
||||||
"p0_completion": "3/3 (100%)",
|
"p0_completion": "3/3 (100%)",
|
||||||
"p1_completion": "4/4 (100%)",
|
"p1_completion": "4/4 (100%)",
|
||||||
@@ -1279,7 +1288,7 @@
|
|||||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
"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",
|
"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",
|
"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 Phase 1: add `flynn skills info <name>` command dispatch"
|
"next_up": "Skills infrastructure Phase 1: add `flynn skills install <path>` command dispatch"
|
||||||
},
|
},
|
||||||
"soul_md_and_cron_create": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
+34
-1
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { toSkillListRows, renderSkillsTable } from './skills.js';
|
import { toSkillListRows, renderSkillsTable, renderSkillInfo } from './skills.js';
|
||||||
import type { Skill } from '../skills/index.js';
|
import type { Skill } from '../skills/index.js';
|
||||||
|
|
||||||
function buildSkill(overrides: Partial<Skill>): Skill {
|
function buildSkill(overrides: Partial<Skill>): Skill {
|
||||||
@@ -58,4 +58,37 @@ describe('skills CLI helpers', () => {
|
|||||||
it('renders a no-skills message when empty', () => {
|
it('renders a no-skills message when empty', () => {
|
||||||
expect(renderSkillsTable([])).toBe('No skills found.');
|
expect(renderSkillsTable([])).toBe('No skills found.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders detailed skill info for available skill', () => {
|
||||||
|
const output = renderSkillInfo(
|
||||||
|
buildSkill({
|
||||||
|
manifest: {
|
||||||
|
name: 'deploy',
|
||||||
|
description: 'Deployment helper',
|
||||||
|
version: '2.0.0',
|
||||||
|
tier: 'bundled',
|
||||||
|
author: 'Flynn',
|
||||||
|
tools: ['shell.exec', 'git.status'],
|
||||||
|
},
|
||||||
|
directory: '/opt/flynn/skills/deploy',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toContain('Name: deploy');
|
||||||
|
expect(output).toContain('Status: available');
|
||||||
|
expect(output).toContain('Tools: shell.exec, git.status');
|
||||||
|
expect(output).toContain('Directory: /opt/flynn/skills/deploy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders unavailable reasons when skill is unavailable', () => {
|
||||||
|
const output = renderSkillInfo(
|
||||||
|
buildSkill({
|
||||||
|
available: false,
|
||||||
|
unavailableReasons: ['Required binary not found'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(output).toContain('Status: unavailable');
|
||||||
|
expect(output).toContain('Unavailable reasons: Required binary not found');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+73
-11
@@ -46,6 +46,47 @@ export function renderSkillsTable(rows: SkillListRow[]): string {
|
|||||||
return lines.join('\n');
|
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 {
|
export function registerSkillsCommand(program: Command): void {
|
||||||
const skills = program
|
const skills = program
|
||||||
.command('skills')
|
.command('skills')
|
||||||
@@ -60,21 +101,14 @@ export function registerSkillsCommand(program: Command): void {
|
|||||||
.option('--json', 'Output as JSON')
|
.option('--json', 'Output as JSON')
|
||||||
.option('-c, --config <path>', 'Config file path')
|
.option('-c, --config <path>', 'Config file path')
|
||||||
.action((opts: { json?: boolean; config?: string }) => {
|
.action((opts: { json?: boolean; config?: string }) => {
|
||||||
const loaded = loadConfigSafe(opts.config);
|
const loaded = loadSkillsFromConfig(opts.config);
|
||||||
if (loaded.error || !loaded.config) {
|
if (loaded.error || !loaded.skills) {
|
||||||
console.error(loaded.error ?? 'Failed to load config');
|
console.error(loaded.error ?? 'Failed to load skills');
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills');
|
const rows = toSkillListRows(loaded.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) {
|
if (opts.json) {
|
||||||
console.log(JSON.stringify(rows, null, 2));
|
console.log(JSON.stringify(rows, null, 2));
|
||||||
return;
|
return;
|
||||||
@@ -82,4 +116,32 @@ export function registerSkillsCommand(program: Command): void {
|
|||||||
|
|
||||||
console.log(renderSkillsTable(rows));
|
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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user