feat(skills): add optional shell command runner
This commit is contained in:
+11
-2
@@ -1451,6 +1451,15 @@
|
|||||||
"src/cli/skills.test.ts"
|
"src/cli/skills.test.ts"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing"
|
"test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing"
|
||||||
|
},
|
||||||
|
"installer_optional_shell_runner": {
|
||||||
|
"status": "completed",
|
||||||
|
"description": "Added an optional concrete shell-based installer command runner that emits structured succeeded/failed command results with machine-readable reasons, while default flow remains policy-gated and no-op",
|
||||||
|
"files_modified": [
|
||||||
|
"src/cli/skills.ts",
|
||||||
|
"src/cli/skills.test.ts"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm typecheck + pnpm test:run src/cli/skills.test.ts + pnpm test:run + pnpm lint (warnings only, 0 errors) + pnpm build passing"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1479,7 +1488,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
"total_test_count": 1543,
|
"total_test_count": 1545,
|
||||||
"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%)",
|
||||||
@@ -1499,7 +1508,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 3: add an optional concrete runner implementation (still opt-in) that emits command-level success/failure reasons into the existing execution envelope schema"
|
"next_up": "Skills infrastructure Phase 3: add explicit CLI opt-in wiring for execution runner selection while preserving execution-disabled defaults and existing confirmation policy gates"
|
||||||
},
|
},
|
||||||
"soul_md_and_cron_create": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
mergeInstallerExecutionResults,
|
mergeInstallerExecutionResults,
|
||||||
runInstallerCommandsWithPolicy,
|
runInstallerCommandsWithPolicy,
|
||||||
noOpSkillInstallerCommandRunner,
|
noOpSkillInstallerCommandRunner,
|
||||||
|
createShellSkillInstallerCommandRunner,
|
||||||
runSkillInstallAction,
|
runSkillInstallAction,
|
||||||
} from './skills.js';
|
} from './skills.js';
|
||||||
import type { Skill } from '../skills/index.js';
|
import type { Skill } from '../skills/index.js';
|
||||||
@@ -376,6 +377,33 @@ describe('skills CLI helpers', () => {
|
|||||||
expect(runner.run).toHaveBeenCalledWith(['brew install jq']);
|
expect(runner.run).toHaveBeenCalledWith(['brew install jq']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shell command runner reports succeeded command status', () => {
|
||||||
|
const runner = createShellSkillInstallerCommandRunner();
|
||||||
|
|
||||||
|
const results = runner.run(['node -e "process.exit(0)"']);
|
||||||
|
|
||||||
|
expect(results).toEqual([
|
||||||
|
{
|
||||||
|
command: 'node -e "process.exit(0)"',
|
||||||
|
status: 'succeeded',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shell command runner reports failed command with exit code reason', () => {
|
||||||
|
const runner = createShellSkillInstallerCommandRunner();
|
||||||
|
|
||||||
|
const results = runner.run(['node -e "process.exit(7)"']);
|
||||||
|
|
||||||
|
expect(results).toEqual([
|
||||||
|
{
|
||||||
|
command: 'node -e "process.exit(7)"',
|
||||||
|
status: 'failed',
|
||||||
|
reason: 'exit_code_7',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('maps runner command results into structured per-step statuses', () => {
|
it('maps runner command results into structured per-step statuses', () => {
|
||||||
const attempted = [
|
const attempted = [
|
||||||
{ installer_type: 'brew', command: 'brew install jq' },
|
{ installer_type: 'brew', command: 'brew install jq' },
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Command } from 'commander';
|
import type { Command } from 'commander';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { homedir } from 'os';
|
import { homedir } from 'os';
|
||||||
|
import { spawnSync } from 'child_process';
|
||||||
import type { Skill } from '../skills/index.js';
|
import type { Skill } from '../skills/index.js';
|
||||||
import { loadAllSkills, SkillInstaller, buildInstallerPlan, loadSkill } from '../skills/index.js';
|
import { loadAllSkills, SkillInstaller, buildInstallerPlan, loadSkill } from '../skills/index.js';
|
||||||
import { loadConfigSafe } from './shared.js';
|
import { loadConfigSafe } from './shared.js';
|
||||||
@@ -78,6 +79,57 @@ export const noOpSkillInstallerCommandRunner: SkillInstallerCommandRunner = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function createShellSkillInstallerCommandRunner(): SkillInstallerCommandRunner {
|
||||||
|
return {
|
||||||
|
run(commands: string[]): SkillInstallerCommandRunResult[] {
|
||||||
|
return commands.map((command) => {
|
||||||
|
const result = spawnSync(command, {
|
||||||
|
shell: true,
|
||||||
|
stdio: 'pipe',
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
status: 'failed',
|
||||||
|
reason: `spawn_error:${result.error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === 0) {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
status: 'succeeded',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status !== null) {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
status: 'failed',
|
||||||
|
reason: `exit_code_${result.status}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.signal) {
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
status: 'failed',
|
||||||
|
reason: `signal_${result.signal}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
status: 'failed',
|
||||||
|
reason: 'unknown_failure',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function toInstallerExecutionStepEnvelopes(
|
export function toInstallerExecutionStepEnvelopes(
|
||||||
steps: Array<{ installerType: string; command: string }>,
|
steps: Array<{ installerType: string; command: string }>,
|
||||||
policy: SkillInstallerExecutionPolicy,
|
policy: SkillInstallerExecutionPolicy,
|
||||||
|
|||||||
Reference in New Issue
Block a user