feat(skills): add installer plan command output
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
renderSkillsRefreshSummary,
|
||||
installSkillFromDirectory,
|
||||
uninstallSkillByName,
|
||||
toSkillInstallerPlanView,
|
||||
renderSkillInstallerPlan,
|
||||
} from './skills.js';
|
||||
import type { Skill } from '../skills/index.js';
|
||||
|
||||
@@ -124,6 +126,40 @@ describe('skills CLI helpers', () => {
|
||||
expect(output).toContain('[download] download https://example.com/tool.tgz -> /tmp/tool.tgz');
|
||||
});
|
||||
|
||||
it('builds installer plan view for automation output', () => {
|
||||
const view = toSkillInstallerPlanView(
|
||||
buildSkill({
|
||||
manifest: {
|
||||
name: 'plan-target',
|
||||
description: 'Plan me',
|
||||
version: '3.2.1',
|
||||
tier: 'managed',
|
||||
installers: [{ type: 'download', url: 'https://example.com/bin.tar.gz' }],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(view.skill.name).toBe('plan-target');
|
||||
expect(view.mode).toBe('dry-run');
|
||||
expect(view.steps.length).toBe(1);
|
||||
expect(view.steps[0]?.installerType).toBe('download');
|
||||
});
|
||||
|
||||
it('renders installer plan summary text', () => {
|
||||
const output = renderSkillInstallerPlan({
|
||||
skill: { name: 'plan-target', tier: 'bundled', version: '1.0.0' },
|
||||
mode: 'dry-run',
|
||||
steps: [{ installerType: 'download', command: 'download https://example.com/tool -> /tmp/tool' }],
|
||||
skipped: [{ installerType: 'brew', reason: 'brew not available in PATH' }],
|
||||
});
|
||||
|
||||
expect(output).toContain("Installer plan for 'plan-target'");
|
||||
expect(output).toContain('Planned steps:');
|
||||
expect(output).toContain('[download] download https://example.com/tool -> /tmp/tool');
|
||||
expect(output).toContain('Skipped steps:');
|
||||
expect(output).toContain('[brew] brew not available in PATH');
|
||||
});
|
||||
|
||||
it('summarizes refresh counts across status and tiers', () => {
|
||||
const summary = summarizeSkillsRefresh([
|
||||
buildSkill({ manifest: { name: 'a', description: 'a', version: '1.0.0', tier: 'bundled' } }),
|
||||
|
||||
@@ -19,6 +19,17 @@ export interface SkillRefreshSummary {
|
||||
tiers: Record<Skill['manifest']['tier'], number>;
|
||||
}
|
||||
|
||||
export interface SkillInstallerPlanView {
|
||||
skill: {
|
||||
name: string;
|
||||
tier: Skill['manifest']['tier'];
|
||||
version: string;
|
||||
};
|
||||
mode: 'dry-run';
|
||||
steps: Array<{ installerType: string; command: string }>;
|
||||
skipped: Array<{ installerType: string; reason: string }>;
|
||||
}
|
||||
|
||||
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
|
||||
return skills
|
||||
.map((skill) => ({
|
||||
@@ -95,6 +106,47 @@ export function renderSkillInfo(skill: Skill): string {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function toSkillInstallerPlanView(skill: Skill): SkillInstallerPlanView {
|
||||
const plan = buildInstallerPlan(skill.manifest.installers);
|
||||
return {
|
||||
skill: {
|
||||
name: skill.manifest.name,
|
||||
tier: skill.manifest.tier,
|
||||
version: skill.manifest.version,
|
||||
},
|
||||
mode: plan.mode,
|
||||
steps: plan.steps,
|
||||
skipped: plan.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderSkillInstallerPlan(view: SkillInstallerPlanView): string {
|
||||
const lines: string[] = [
|
||||
`Installer plan for '${view.skill.name}' (${view.skill.tier}, v${view.skill.version})`,
|
||||
`Mode: ${view.mode}`,
|
||||
];
|
||||
|
||||
if (view.steps.length === 0) {
|
||||
lines.push('Planned steps: none');
|
||||
} else {
|
||||
lines.push('Planned steps:');
|
||||
for (const step of view.steps) {
|
||||
lines.push(`- [${step.installerType}] ${step.command}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (view.skipped.length === 0) {
|
||||
lines.push('Skipped steps: none');
|
||||
} else {
|
||||
lines.push('Skipped 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,
|
||||
@@ -319,4 +371,33 @@ export function registerSkillsCommand(program: Command): void {
|
||||
|
||||
console.log(renderSkillsRefreshSummary(summary));
|
||||
});
|
||||
|
||||
skills
|
||||
.command('plan <name>')
|
||||
.description('Print dry-run installer plan 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;
|
||||
}
|
||||
|
||||
const view = toSkillInstallerPlanView(skill);
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(view, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(renderSkillInstallerPlan(view));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user