feat(skills): add install dispatch for local skill setup
This commit is contained in:
+11
-2
@@ -1228,6 +1228,15 @@
|
|||||||
"src/cli/skills.test.ts"
|
"src/cli/skills.test.ts"
|
||||||
],
|
],
|
||||||
"test_status": "typecheck + targeted skills CLI tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
"test_status": "typecheck + targeted skills CLI tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
||||||
|
},
|
||||||
|
"skills_install_command_local": {
|
||||||
|
"status": "completed",
|
||||||
|
"description": "Added `flynn skills install <path>` local-directory command dispatch using SkillInstaller with config-aware managed_dir resolution",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1268,7 +1277,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
"total_test_count": 1495,
|
"total_test_count": 1497,
|
||||||
"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%)",
|
||||||
@@ -1288,7 +1297,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 install <path>` command dispatch"
|
"next_up": "Skills infrastructure Phase 1: add `flynn skills uninstall <name>` command dispatch"
|
||||||
},
|
},
|
||||||
"soul_md_and_cron_create": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
+39
-1
@@ -1,5 +1,9 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { toSkillListRows, renderSkillsTable, renderSkillInfo } from './skills.js';
|
import { mkdtempSync, mkdirSync, writeFileSync, existsSync, rmSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
import { SkillInstaller } from '../skills/index.js';
|
||||||
|
import { toSkillListRows, renderSkillsTable, renderSkillInfo, installSkillFromDirectory } 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 {
|
||||||
@@ -91,4 +95,38 @@ describe('skills CLI helpers', () => {
|
|||||||
expect(output).toContain('Status: unavailable');
|
expect(output).toContain('Status: unavailable');
|
||||||
expect(output).toContain('Unavailable reasons: Required binary not found');
|
expect(output).toContain('Unavailable reasons: Required binary not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('installs a local skill directory', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||||
|
const sourceDir = join(root, 'source-skill');
|
||||||
|
const managedDir = join(root, 'managed');
|
||||||
|
mkdirSync(sourceDir, { recursive: true });
|
||||||
|
writeFileSync(join(sourceDir, 'SKILL.md'), '# My Skill\nInstructions');
|
||||||
|
writeFileSync(
|
||||||
|
join(sourceDir, 'manifest.json'),
|
||||||
|
JSON.stringify({ name: 'my-skill', description: 'My skill', version: '1.0.0' }),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
|
||||||
|
const installer = new SkillInstaller(managedDir);
|
||||||
|
const result = installSkillFromDirectory(installer, sourceDir);
|
||||||
|
|
||||||
|
expect(result.error).toBeUndefined();
|
||||||
|
expect(result.skill?.manifest.name).toBe('my-skill');
|
||||||
|
expect(existsSync(join(managedDir, 'my-skill', 'SKILL.md'))).toBe(true);
|
||||||
|
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when source directory is missing', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||||
|
const installer = new SkillInstaller(join(root, 'managed'));
|
||||||
|
|
||||||
|
const result = installSkillFromDirectory(installer, join(root, 'does-not-exist'));
|
||||||
|
|
||||||
|
expect(result.skill).toBeUndefined();
|
||||||
|
expect(result.error).toContain('does not exist');
|
||||||
|
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+39
-1
@@ -2,7 +2,7 @@ import type { Command } from 'commander';
|
|||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
import type { Skill } from '../skills/index.js';
|
import type { Skill } from '../skills/index.js';
|
||||||
import { loadAllSkills } from '../skills/index.js';
|
import { loadAllSkills, SkillInstaller } from '../skills/index.js';
|
||||||
import { loadConfigSafe } from './shared.js';
|
import { loadConfigSafe } from './shared.js';
|
||||||
|
|
||||||
export interface SkillListRow {
|
export interface SkillListRow {
|
||||||
@@ -87,6 +87,19 @@ function loadSkillsFromConfig(configPath?: string): { skills?: Skill[]; error?:
|
|||||||
return { skills };
|
return { skills };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function installSkillFromDirectory(installer: SkillInstaller, sourcePath: string): { skill?: Skill; error?: string } {
|
||||||
|
const sourceDir = resolve(sourcePath);
|
||||||
|
try {
|
||||||
|
const skill = installer.install(sourceDir);
|
||||||
|
if (!skill) {
|
||||||
|
return { error: `Failed to load skill from '${sourceDir}' after installation.` };
|
||||||
|
}
|
||||||
|
return { skill };
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error instanceof Error ? error.message : String(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function registerSkillsCommand(program: Command): void {
|
export function registerSkillsCommand(program: Command): void {
|
||||||
const skills = program
|
const skills = program
|
||||||
.command('skills')
|
.command('skills')
|
||||||
@@ -144,4 +157,29 @@ export function registerSkillsCommand(program: Command): void {
|
|||||||
|
|
||||||
console.log(renderSkillInfo(skill));
|
console.log(renderSkillInfo(skill));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
skills
|
||||||
|
.command('install <path>')
|
||||||
|
.description('Install a skill from a local directory')
|
||||||
|
.option('-c, --config <path>', 'Config file path')
|
||||||
|
.action((pathArg: string, opts: { 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 installer = new SkillInstaller(loaded.config.skills.managed_dir ?? defaultManagedDir);
|
||||||
|
const result = installSkillFromDirectory(installer, pathArg);
|
||||||
|
|
||||||
|
if (result.error || !result.skill) {
|
||||||
|
console.error(result.error ?? `Failed to install skill from '${pathArg}'.`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Installed skill '${result.skill.manifest.name}' (${result.skill.manifest.version}).`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user