feat(skills): preview installer plan during install
This commit is contained in:
+80
-2
@@ -2,7 +2,7 @@ import type { Command } from 'commander';
|
||||
import { resolve } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import type { Skill } from '../skills/index.js';
|
||||
import { loadAllSkills, SkillInstaller, buildInstallerPlan } from '../skills/index.js';
|
||||
import { loadAllSkills, SkillInstaller, buildInstallerPlan, loadSkill } from '../skills/index.js';
|
||||
import { loadConfigSafe } from './shared.js';
|
||||
|
||||
export interface SkillListRow {
|
||||
@@ -30,6 +30,14 @@ export interface SkillInstallerPlanView {
|
||||
skipped: Array<{ installerType: string; reason: string }>;
|
||||
}
|
||||
|
||||
export interface SkillInstallPreflightView {
|
||||
sourcePath: string;
|
||||
skill: SkillInstallerPlanView['skill'];
|
||||
mode: 'dry-run';
|
||||
steps: SkillInstallerPlanView['steps'];
|
||||
skipped: SkillInstallerPlanView['skipped'];
|
||||
}
|
||||
|
||||
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
|
||||
return skills
|
||||
.map((skill) => ({
|
||||
@@ -147,6 +155,47 @@ export function renderSkillInstallerPlan(view: SkillInstallerPlanView): string {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function toSkillInstallPreflightView(sourcePath: string): SkillInstallPreflightView | null {
|
||||
const sourceSkill = loadSkill(resolve(sourcePath), 'managed');
|
||||
if (!sourceSkill) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const planView = toSkillInstallerPlanView(sourceSkill);
|
||||
return {
|
||||
sourcePath: resolve(sourcePath),
|
||||
skill: planView.skill,
|
||||
mode: planView.mode,
|
||||
steps: planView.steps,
|
||||
skipped: planView.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderSkillInstallPreflight(view: SkillInstallPreflightView): string {
|
||||
const lines: string[] = [
|
||||
`Install preflight for '${view.skill.name}' from ${view.sourcePath}`,
|
||||
`Mode: ${view.mode}`,
|
||||
];
|
||||
|
||||
if (view.steps.length === 0) {
|
||||
lines.push('Planned installer steps: none');
|
||||
} else {
|
||||
lines.push('Planned installer steps:');
|
||||
for (const step of view.steps) {
|
||||
lines.push(`- [${step.installerType}] ${step.command}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (view.skipped.length > 0) {
|
||||
lines.push('Skipped installer steps:');
|
||||
for (const skip of view.skipped) {
|
||||
lines.push(`- [${skip.installerType}] ${skip.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function summarizeSkillsRefresh(skills: Skill[]): SkillRefreshSummary {
|
||||
const summary: SkillRefreshSummary = {
|
||||
total: skills.length,
|
||||
@@ -290,8 +339,9 @@ export function registerSkillsCommand(program: Command): void {
|
||||
skills
|
||||
.command('install <path>')
|
||||
.description('Install a skill from a local directory')
|
||||
.option('--json', 'Output preflight and install result as JSON')
|
||||
.option('-c, --config <path>', 'Config file path')
|
||||
.action((pathArg: string, opts: { config?: string }) => {
|
||||
.action((pathArg: string, opts: { json?: boolean; config?: string }) => {
|
||||
const loaded = loadConfigSafe(opts.config);
|
||||
if (loaded.error || !loaded.config) {
|
||||
console.error(loaded.error ?? 'Failed to load config');
|
||||
@@ -301,6 +351,16 @@ export function registerSkillsCommand(program: Command): void {
|
||||
|
||||
const defaultManagedDir = resolve(homedir(), '.flynn/workspace/skills');
|
||||
const installer = new SkillInstaller(loaded.config.skills.managed_dir ?? defaultManagedDir);
|
||||
const preflight = toSkillInstallPreflightView(pathArg);
|
||||
|
||||
if (preflight) {
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify({ preflight }, null, 2));
|
||||
} else {
|
||||
console.log(renderSkillInstallPreflight(preflight));
|
||||
}
|
||||
}
|
||||
|
||||
const result = installSkillFromDirectory(installer, pathArg);
|
||||
|
||||
if (result.error || !result.skill) {
|
||||
@@ -309,6 +369,24 @@ export function registerSkillsCommand(program: Command): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
installed: {
|
||||
name: result.skill.manifest.name,
|
||||
version: result.skill.manifest.version,
|
||||
tier: result.skill.manifest.tier,
|
||||
directory: result.skill.directory,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Installed skill '${result.skill.manifest.name}' (${result.skill.manifest.version}).`);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user