feat(skills): add execute command opt-in runner flow

This commit is contained in:
William Valentin
2026-02-12 19:28:44 -08:00
parent 3272387eaa
commit 8affe8bea9
3 changed files with 128 additions and 14 deletions
+57
View File
@@ -25,6 +25,7 @@ import {
noOpSkillInstallerCommandRunner,
createShellSkillInstallerCommandRunner,
resolveSkillInstallerCommandRunner,
runSkillExecuteAction,
runSkillInstallAction,
} from './skills.js';
import type { Skill } from '../skills/index.js';
@@ -277,6 +278,26 @@ describe('skills CLI helpers', () => {
expect(output).toContain('Would run:');
expect(output).toContain('- brew install jq');
expect(output).toContain('Skipped:');
expect(output).toContain('Results:');
});
it('renders execution report text when commands are executed', () => {
const output = renderSkillInstallerExecutionStub({
skill: { name: 'exec-stub', tier: 'bundled', version: '1.0.0' },
execution: 'stub',
mode: 'stub',
confirmed: true,
execution_enabled: true,
executed: ['brew install jq'],
reason: 'execution_enabled',
attempted: [{ installer_type: 'brew', command: 'brew install jq' }],
results: [{ installer_type: 'brew', command: 'brew install jq', status: 'succeeded', reason: 'runner_reported_success' }],
wouldRun: ['brew install jq'],
skipped: [],
});
expect(output).toContain('Installer commands were executed.');
expect(output).toContain('[brew] succeeded brew install jq (runner_reported_success)');
});
it('derives execution stub view from preflight data', () => {
@@ -756,6 +777,42 @@ describe('skills CLI helpers', () => {
rmSync(root, { recursive: true, force: true });
});
it('execute action honors opt-in execution and runner selection', () => {
const skill = buildSkill({
manifest: {
name: 'execute-skill',
description: 'Execute me',
version: '1.0.0',
tier: 'managed',
installers: [{ type: 'download', url: 'https://example.com/execute.tgz' }],
},
});
const runner = {
run: vi.fn((commands: string[]) =>
commands.map((command) => ({ command, status: 'succeeded' as const, reason: 'runner_reported_success' })),
),
};
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
const result = runSkillExecuteAction(skill, {
asJson: true,
confirmed: true,
executionRequested: true,
commandRunner: runner,
});
expect(result.ok).toBe(true);
expect(result.execution.execution_enabled).toBe(true);
expect(result.execution.reason).toBe('execution_enabled');
expect(runner.run).toHaveBeenCalledTimes(1);
const payload = JSON.parse(String(logSpy.mock.calls[0]?.[0]));
expect(payload.execution_enabled).toBe(true);
expect(payload.executed).toEqual(['download https://example.com/execute.tgz -> <default destination>']);
logSpy.mockRestore();
});
it('requires --yes confirmation for uninstall helper', () => {
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
const installer = new SkillInstaller(join(root, 'managed'));