From 270779b2cd3cb6e0d96e4af5eb5dd8703b25c21d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 14:40:59 +0000 Subject: [PATCH] refactor(shared): derive requirements from metadata --- src/agents/skills-status.ts | 27 ++++---------------- src/hooks/hooks-status.ts | 27 ++++---------------- src/shared/requirements.test.ts | 21 +++++++++++++++ src/shared/requirements.ts | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 44 deletions(-) diff --git a/src/agents/skills-status.ts b/src/agents/skills-status.ts index ddca02f02b2..34ab7a2a7a4 100644 --- a/src/agents/skills-status.ts +++ b/src/agents/skills-status.ts @@ -1,6 +1,6 @@ import path from "node:path"; import type { OpenClawConfig } from "../config/config.js"; -import { evaluateRequirements } from "../shared/requirements.js"; +import { evaluateRequirementsFromMetadata } from "../shared/requirements.js"; import { CONFIG_DIR } from "../utils.js"; import { hasBinary, @@ -197,25 +197,14 @@ function buildSkillStatus( ? bundledNames.has(entry.skill.name) : entry.skill.source === "openclaw-bundled"; - const requiredBins = entry.metadata?.requires?.bins ?? []; - const requiredAnyBins = entry.metadata?.requires?.anyBins ?? []; - const requiredEnv = entry.metadata?.requires?.env ?? []; - const requiredConfig = entry.metadata?.requires?.config ?? []; - const requiredOs = entry.metadata?.os ?? []; - const { + required, missing, eligible: requirementsSatisfied, configChecks, - } = evaluateRequirements({ + } = evaluateRequirementsFromMetadata({ always, - required: { - bins: requiredBins, - anyBins: requiredAnyBins, - env: requiredEnv, - config: requiredConfig, - os: requiredOs, - }, + metadata: entry.metadata, hasLocalBin: hasBinary, hasRemoteBin: eligibility?.remote?.hasBin, hasRemoteAnyBin: eligibility?.remote?.hasAnyBin, @@ -247,13 +236,7 @@ function buildSkillStatus( disabled, blockedByAllowlist, eligible, - requirements: { - bins: requiredBins, - anyBins: requiredAnyBins, - env: requiredEnv, - config: requiredConfig, - os: requiredOs, - }, + requirements: required, missing, configChecks, install: normalizeInstallOptions(entry, prefs ?? resolveSkillsInstallPreferences(config)), diff --git a/src/hooks/hooks-status.ts b/src/hooks/hooks-status.ts index b680ca668d5..dd3e75294a0 100644 --- a/src/hooks/hooks-status.ts +++ b/src/hooks/hooks-status.ts @@ -1,7 +1,7 @@ import path from "node:path"; import type { OpenClawConfig } from "../config/config.js"; import type { HookEligibilityContext, HookEntry, HookInstallSpec } from "./types.js"; -import { evaluateRequirements } from "../shared/requirements.js"; +import { evaluateRequirementsFromMetadata } from "../shared/requirements.js"; import { CONFIG_DIR } from "../utils.js"; import { hasBinary, isConfigPathTruthy, resolveConfigPath, resolveHookConfig } from "./config.js"; import { loadWorkspaceHookEntries } from "./workspace.js"; @@ -110,25 +110,14 @@ function buildHookStatus( const homepage = homepageRaw?.trim() ? homepageRaw.trim() : undefined; const events = entry.metadata?.events ?? []; - const requiredBins = entry.metadata?.requires?.bins ?? []; - const requiredAnyBins = entry.metadata?.requires?.anyBins ?? []; - const requiredEnv = entry.metadata?.requires?.env ?? []; - const requiredConfig = entry.metadata?.requires?.config ?? []; - const requiredOs = entry.metadata?.os ?? []; - const { + required, missing, eligible: requirementsSatisfied, configChecks, - } = evaluateRequirements({ + } = evaluateRequirementsFromMetadata({ always, - required: { - bins: requiredBins, - anyBins: requiredAnyBins, - env: requiredEnv, - config: requiredConfig, - os: requiredOs, - }, + metadata: entry.metadata, hasLocalBin: hasBinary, hasRemoteBin: eligibility?.remote?.hasBin, hasRemoteAnyBin: eligibility?.remote?.hasAnyBin, @@ -157,13 +146,7 @@ function buildHookStatus( disabled, eligible, managedByPlugin, - requirements: { - bins: requiredBins, - anyBins: requiredAnyBins, - env: requiredEnv, - config: requiredConfig, - os: requiredOs, - }, + requirements: required, missing, configChecks, install: normalizeInstallOptions(entry), diff --git a/src/shared/requirements.test.ts b/src/shared/requirements.test.ts index a2e4f837e59..e8f6abbc56f 100644 --- a/src/shared/requirements.test.ts +++ b/src/shared/requirements.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { buildConfigChecks, + evaluateRequirementsFromMetadata, resolveMissingAnyBins, resolveMissingBins, resolveMissingEnv, @@ -60,4 +61,24 @@ describe("requirements helpers", () => { }), ).toEqual([{ path: "a.b", value: 1, satisfied: true }]); }); + + it("evaluateRequirementsFromMetadata derives required+missing", () => { + const res = evaluateRequirementsFromMetadata({ + always: false, + metadata: { + requires: { bins: ["a"], anyBins: ["b"], env: ["E"], config: ["cfg.value"] }, + os: ["darwin"], + }, + hasLocalBin: (bin) => bin === "a", + localPlatform: "linux", + isEnvSatisfied: (name) => name === "E", + resolveConfigValue: () => "x", + isConfigSatisfied: () => false, + }); + + expect(res.required.bins).toEqual(["a"]); + expect(res.missing.config).toEqual(["cfg.value"]); + expect(res.missing.os).toEqual(["darwin"]); + expect(res.eligible).toBe(false); + }); }); diff --git a/src/shared/requirements.ts b/src/shared/requirements.ts index 6ecb511b170..2aa4146146a 100644 --- a/src/shared/requirements.ts +++ b/src/shared/requirements.ts @@ -12,6 +12,11 @@ export type RequirementConfigCheck = { satisfied: boolean; }; +export type RequirementsMetadata = { + requires?: Partial>; + os?: string[]; +}; + export function resolveMissingBins(params: { required: string[]; hasLocalBin: (bin: string) => boolean; @@ -147,3 +152,43 @@ export function evaluateRequirements(params: { return { missing, eligible, configChecks }; } + +export function evaluateRequirementsFromMetadata(params: { + always: boolean; + metadata?: RequirementsMetadata; + hasLocalBin: (bin: string) => boolean; + hasRemoteBin?: (bin: string) => boolean; + hasRemoteAnyBin?: (bins: string[]) => boolean; + localPlatform: string; + remotePlatforms?: string[]; + isEnvSatisfied: (envName: string) => boolean; + resolveConfigValue: (pathStr: string) => unknown; + isConfigSatisfied: (pathStr: string) => boolean; +}): { + required: Requirements; + missing: Requirements; + eligible: boolean; + configChecks: RequirementConfigCheck[]; +} { + const required: Requirements = { + bins: params.metadata?.requires?.bins ?? [], + anyBins: params.metadata?.requires?.anyBins ?? [], + env: params.metadata?.requires?.env ?? [], + config: params.metadata?.requires?.config ?? [], + os: params.metadata?.os ?? [], + }; + + const result = evaluateRequirements({ + always: params.always, + required, + hasLocalBin: params.hasLocalBin, + hasRemoteBin: params.hasRemoteBin, + hasRemoteAnyBin: params.hasRemoteAnyBin, + localPlatform: params.localPlatform, + remotePlatforms: params.remotePlatforms, + isEnvSatisfied: params.isEnvSatisfied, + resolveConfigValue: params.resolveConfigValue, + isConfigSatisfied: params.isConfigSatisfied, + }); + return { required, ...result }; +}