Spaces:
Runtime error
Runtime error
Aryan Mishra
feat: initial commit - Multilingual ABSA project setup with 6 phases, 36 requirements
a6b96c2 | ; | |
| /** | |
| * Capability State Resolver β ADR-857 phase 4b | |
| * | |
| * Unified capability-state resolver that composes the three toggle systems | |
| * (install profile, runtime surface, config activation) into one per-capability | |
| * view. The loop resolver consumes this state so workflow dispatch and the | |
| * `gsd-tools capability state` diagnostic share the same enablement answer. | |
| * | |
| * Exports (three things, mirroring loop-resolver): | |
| * resolveCapabilityState({ registry, installedSkills, surfacedSkills, config, cwd }) | |
| * β { capabilities: CapabilityStateEntry[] } | |
| * cmdCapabilityState(cwd, runtimeConfigDir, raw, options) β I/O entry point | |
| * | |
| * resolveCapabilityState is DETERMINISTIC given (registry, installedSkills, | |
| * surfacedSkills, config) and β when `cwd` is provided β the project config | |
| * files at `cwd` (.planning/config.json etc). Pass `cwd: undefined` for a | |
| * pure, config-only resolution with no filesystem I/O. | |
| * cmdCapabilityState is the I/O handler. | |
| * | |
| * Dependencies (leaf modules only β no circular risk): | |
| * - node:path | |
| * - ./io.cjs (output, error) | |
| * - ./capability-activation.cjs (_resolveActivationValue) | |
| * - ./install-profiles.cjs (readActiveProfile, loadSkillsManifest, resolveProfile) | |
| * - ./surface.cjs (resolveSurface) | |
| * - ./config-loader.cjs (loadConfig) | |
| * - ./runtime-homes.cjs (getGlobalConfigDir β for runtimeConfigDir auto-detection) | |
| * - ./runtime-slash.cjs (resolveRuntime β GSD_RUNTIME > config.runtime > 'claude' precedence) | |
| * - capability-registry.cjs (loaded at call time) | |
| */ | |
| var __importDefault = (this && this.__importDefault) || function (mod) { | |
| return (mod && mod.__esModule) ? mod : { "default": mod }; | |
| }; | |
| const node_path_1 = __importDefault(require("node:path")); | |
| const node_fs_1 = __importDefault(require("node:fs")); | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const ioMod = require("./io.cjs"); | |
| const { output: coreOutput, error: coreError } = ioMod; | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const activationMod = require("./capability-activation.cjs"); | |
| const { _resolveActivationValue } = activationMod; | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const configLoaderMod = require("./config-loader.cjs"); | |
| const { loadConfig } = configLoaderMod; | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const installProfilesMod = require("./install-profiles.cjs"); | |
| const { readActiveProfile, loadSkillsManifest, resolveProfile, parseRequires } = installProfilesMod; | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const surfaceMod = require("./surface.cjs"); | |
| const { resolveSurface } = surfaceMod; | |
| // βββ Prototype-pollution guard (inline literal, CodeQL barrier) βββββββββββββββ | |
| function _isSafePropKey(key) { | |
| // Inline literal guards β CodeQL barrier pattern | |
| if (typeof key !== 'string') | |
| return false; | |
| if (key === '__proto__') | |
| return false; | |
| if (key === 'constructor') | |
| return false; | |
| if (key === 'prototype') | |
| return false; | |
| return true; | |
| } | |
| // βββ Pure resolver βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Deterministic resolver: for each capability in the registry, produce the | |
| * three-dimension state view: | |
| * 1. installed β does the install profile cover this capability? | |
| * 2. surfaced β does the runtime surface enable this capability? | |
| * 3. hooks β per-hook activation derived from config `when` keys. | |
| * | |
| * Determinism contract: given the same (registry, installedSkills, | |
| * surfacedSkills, config) and β when `cwd` is set β the same project config | |
| * files at `cwd`, the output is identical across calls. Pass `cwd: undefined` | |
| * for a pure, config-only resolution with no filesystem I/O. | |
| * | |
| * Never throws for malformed registry/hook entries β skips/defaults defensively. | |
| * An empty or missing capabilities object β { capabilities: [] }. | |
| * | |
| * @param input.registry The capability-registry.cjs module export. | |
| * @param input.installedSkills Set<string> | '*' β from resolveProfile().skills. | |
| * @param input.surfacedSkills Set<string> β from resolveSurface().skills. | |
| * @param input.config Record from loadConfig(cwd). | |
| * @param input.cwd Optional; when provided, enables raw .planning/config.json | |
| * fallback reads (levels 2+3 of _resolveActivationValue | |
| * precedence). Omit for a pure in-memory resolution. | |
| */ | |
| function resolveCapabilityState(input) { | |
| const { registry, installedSkills, surfacedSkills, config, cwd } = input; | |
| // Guard: registry missing capabilities | |
| if (!registry || typeof registry !== 'object' || Array.isArray(registry)) { | |
| return { capabilities: [] }; | |
| } | |
| const capabilitiesRaw = registry['capabilities']; | |
| if (!capabilitiesRaw || typeof capabilitiesRaw !== 'object' || Array.isArray(capabilitiesRaw)) { | |
| return { capabilities: [] }; | |
| } | |
| const capabilitiesMap = capabilitiesRaw; | |
| const results = []; | |
| for (const capId of Object.keys(capabilitiesMap)) { | |
| // Prototype-pollution guard on capability id | |
| if (!_isSafePropKey(capId)) | |
| continue; | |
| const cap = capabilitiesMap[capId]; | |
| if (!cap || typeof cap !== 'object' || Array.isArray(cap)) | |
| continue; | |
| const capObj = cap; | |
| // Extract tier | |
| const tier = typeof capObj['tier'] === 'string' ? capObj['tier'] : 'unknown'; | |
| // Extract skills array | |
| const skillsRaw = capObj['skills']; | |
| const skills = Array.isArray(skillsRaw) | |
| ? skillsRaw.filter((s) => typeof s === 'string') | |
| : []; | |
| // ββ installed ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Empty-skills cap β vacuously installed (no skills to be absent). | |
| // installedSkills === '*' β installed = true for every cap. | |
| let installed; | |
| if (installedSkills === '*') { | |
| installed = true; | |
| } | |
| else if (skills.length === 0) { | |
| installed = true; // vacuous: no skills required | |
| } | |
| else { | |
| installed = skills.every((s) => installedSkills.has(s)); | |
| } | |
| // ββ surfaced βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Empty-skills cap β vacuously surfaced. | |
| let surfaced; | |
| if (skills.length === 0) { | |
| surfaced = true; // vacuous | |
| } | |
| else { | |
| surfaced = skills.every((s) => surfacedSkills.has(s)); | |
| } | |
| const enabled = installed && surfaced; | |
| // ββ per-capability config activation ββββββββββββββββββββββββββββββββββββββ | |
| // Resolve the capability's own activationKey (if present). This is the | |
| // config-level toggle that gates the whole capability β separate from the | |
| // per-hook `when` keys that gate individual hooks. When activationKey is | |
| // absent, configActivation defaults to true (no config gate on the cap). | |
| // active = enabled && configActivation (enabled unchanged: installed && surfaced) | |
| const activationKey = typeof capObj['activationKey'] === 'string' && capObj['activationKey'].length > 0 | |
| ? capObj['activationKey'] | |
| : undefined; | |
| const configActivation = activationKey !== undefined | |
| ? _resolveActivationValue(activationKey, config, cwd, registry) | |
| : true; | |
| const active = enabled && configActivation; | |
| // ββ hooks ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // Collect from steps, gates, contributions. Each may have a `when` key. | |
| // Activation semantics (mirrors loop-resolver.isActive exactly): | |
| // - No `when` field present (undefined/null) β unconditional, active=true | |
| // - Non-empty string `when` β resolve via _resolveActivationValue | |
| // - Present-but-empty-string or non-string `when` β malformed, active=false | |
| // The original `when` value is carried through to the output for visibility. | |
| const hooks = []; | |
| function processHooks(arr, kind) { | |
| for (const hookRaw of arr) { | |
| if (!hookRaw || typeof hookRaw !== 'object' || Array.isArray(hookRaw)) | |
| continue; | |
| const h = hookRaw; | |
| const point = typeof h['point'] === 'string' ? h['point'] : ''; | |
| // Carry the raw `when` value through for visibility | |
| const whenRaw = h['when']; | |
| let configured; | |
| if (whenRaw === undefined || whenRaw === null) { | |
| // No `when` field β unconditional, always active | |
| configured = true; | |
| } | |
| else if (typeof whenRaw === 'string' && whenRaw.length > 0) { | |
| // Non-empty string `when` β resolve via _resolveActivationValue | |
| configured = _resolveActivationValue(whenRaw, config, cwd, registry); | |
| } | |
| else { | |
| // Present-but-empty-string or non-string `when` β malformed, inactive | |
| // (mirrors loop-resolver.isActive: `typeof when !== 'string' || when.length === 0` β false) | |
| configured = false; | |
| } | |
| // Hook active = capability-level active AND hook's own config gate. | |
| // The capability's `active` constant (= enabled && configActivation) is | |
| // used here so that a config-disabled capability (active=false) cannot | |
| // produce active hooks even when the hook's own `when` is unconditional | |
| // (configured=true). The capability gate cascades to all its hooks. | |
| hooks.push({ point, kind, when: whenRaw, configured, active: active && configured }); | |
| } | |
| } | |
| const stepsRaw = capObj['steps']; | |
| const gatesRaw = capObj['gates']; | |
| const contributionsRaw = capObj['contributions']; | |
| processHooks(Array.isArray(stepsRaw) ? stepsRaw : [], 'step'); | |
| processHooks(Array.isArray(gatesRaw) ? gatesRaw : [], 'gate'); | |
| processHooks(Array.isArray(contributionsRaw) ? contributionsRaw : [], 'contribution'); | |
| results.push({ id: capId, tier, skills, installed, surfaced, enabled, active, hooks }); | |
| } | |
| // Deterministic sort by id for stable output across calls | |
| results.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0); | |
| return { capabilities: results }; | |
| } | |
| // βββ I/O command handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Derive the commands/gsd path from __dirname (which resolves to | |
| * gsd-core/bin/lib/ at runtime). The source tree is: | |
| * <repo>/gsd-core/bin/lib/capability-state.cjs | |
| * <repo>/commands/gsd/*.md | |
| * So we walk up three levels: lib/ β bin/ β gsd-core/ β <repo>/, then | |
| * into commands/gsd/. | |
| */ | |
| function _resolveCommandsGsdDir() { | |
| // __dirname = gsd-core/bin/lib/ | |
| const repoRoot = node_path_1.default.resolve(__dirname, '..', '..', '..'); | |
| return node_path_1.default.join(repoRoot, 'commands', 'gsd'); | |
| } | |
| /** | |
| * Build a skill dependency manifest from an INSTALLED runtime's skills directory. | |
| * | |
| * In an installed runtime (e.g. Codex at ~/.codex), gsd skills live as | |
| * configDir/skills/gsd-STEM/SKILL.md. There is no commands/gsd source tree. | |
| * This function scans that installed layout and builds the same | |
| * Map shape that loadSkillsManifest produces from sources. | |
| * | |
| * Stem extraction: a directory named gsd-secure-phase maps to stem secure-phase. | |
| * Only directories whose names start with gsd- are included so user-created | |
| * skills (without the gsd- prefix) are not accidentally pulled in. | |
| * | |
| * The requires: field is parsed via the shared parseRequires helper (the same | |
| * parser loadSkillsManifest uses), so the two paths cannot drift. | |
| * | |
| * Returns an empty Map when the skills dir does not exist. | |
| */ | |
| function _loadInstalledSkillsManifest(configDir) { | |
| const manifest = new Map(); | |
| const skillsDir = node_path_1.default.join(configDir, 'skills'); | |
| if (!node_fs_1.default.existsSync(skillsDir)) | |
| return manifest; | |
| let entries; | |
| try { | |
| entries = node_fs_1.default.readdirSync(skillsDir, { withFileTypes: true }); | |
| } | |
| catch { | |
| return manifest; | |
| } | |
| for (const entry of entries) { | |
| if (!entry.isDirectory()) | |
| continue; | |
| if (!entry.name.startsWith('gsd-')) | |
| continue; | |
| // Strip the 'gsd-' prefix to get the skill stem | |
| const stem = entry.name.slice(4); // 'gsd-'.length === 4 | |
| if (!stem) | |
| continue; | |
| const skillMdPath = node_path_1.default.join(skillsDir, entry.name, 'SKILL.md'); | |
| // Parity with loadSkillsManifest: a stem exists only when its artifact | |
| // file is present. loadSkillsManifest registers a stem per .md FILE (and | |
| // tolerates an unreadable file as []), but never invents a stem for which | |
| // no file exists. Mirror that here: a stale gsd-<stem>/ directory with no | |
| // SKILL.md must NOT register the stem β otherwise the capability would be | |
| // wrongly reported surfaced/enabled and a verify:post hook would render | |
| // for a skill that cannot run. | |
| if (!node_fs_1.default.existsSync(skillMdPath)) | |
| continue; | |
| let content = ''; | |
| try { | |
| content = node_fs_1.default.readFileSync(skillMdPath, 'utf8'); | |
| } | |
| catch { | |
| // SKILL.md present but unreadable β register with no deps (parity with | |
| // loadSkillsManifest's readFileSync catch branch). | |
| } | |
| // Parse requires: via the SAME shared parser loadSkillsManifest uses, so | |
| // installed-runtime dependency resolution can never silently diverge from | |
| // the source-tree path (single source of truth β no duplicated regex). | |
| manifest.set(stem, content ? parseRequires(content) : []); | |
| // Mirror loadSkillsManifest's Map shape: it always sets a companion | |
| // `_calls_agents_<stem>` key. Installed SKILL.md bodies carry no | |
| // recoverable agent-call refs, so [] (the no-agents case) keeps the two | |
| // manifest shapes identical and prevents undefined-vs-[] drift for any | |
| // consumer that reads the agent-refs companion key. | |
| manifest.set(`_calls_agents_${stem}`, []); | |
| } | |
| return manifest; | |
| } | |
| /** | |
| * Resolve the skill dependency manifest for capability-state resolution. | |
| * | |
| * Resolution order (fixes #1160 β installed-runtime capability surface): | |
| * 1. If commandsGsdDir exists, load from source (repo-checkout behavior). | |
| * 2. Otherwise, fall back to installed skills at configDir/skills/gsd-[stem]/SKILL.md. | |
| * | |
| * In an installed runtime the commands/gsd source tree is absent; only the | |
| * skills/ layout exists. Returning an empty manifest caused resolveSurface to | |
| * materialise the full-sentinel to an empty Set, making every capability appear | |
| * unsurfaced even when the skill was physically installed. | |
| */ | |
| function _resolveManifest(commandsGsdDir, configDir) { | |
| if (node_fs_1.default.existsSync(commandsGsdDir)) { | |
| return loadSkillsManifest(commandsGsdDir); | |
| } | |
| return _loadInstalledSkillsManifest(configDir); | |
| } | |
| /** | |
| * Command entry point: resolve install profile, surface, and config; compute | |
| * capability state; emit the envelope via io.output. | |
| * | |
| * Envelope: { runtimeConfigDir, warnings?: string[], capabilities: CapabilityStateEntry[] } | |
| * | |
| * runtimeConfigDir resolution (when not provided or empty): | |
| * Detects the active runtime via the canonical precedence: | |
| * process.env.GSD_RUNTIME β config.runtime β 'claude' | |
| * (using resolveRuntime() from runtime-slash.cjs, the same precedence used | |
| * by profile-output.cjs and the rest of the runtime resolution chain). | |
| * Then calls getGlobalConfigDir(detectedRuntime) from runtime-homes.cjs β | |
| * the same resolver used by install.js. This correctly handles all supported | |
| * runtimes (claude, codex, cursor, gemini, opencode, grok, etc.) and their | |
| * env-var overrides (CLAUDE_CONFIG_DIR, CODEX_HOME, CURSOR_CONFIG_DIR, β¦). | |
| * Defaults to ~/.claude if either resolver throws. | |
| * | |
| * Failure surfacing: genuine resolution failures (manifest/profile/surface | |
| * errors) are reported in the `warnings` array in the envelope. The output | |
| * remains useful β degraded to the best available state β but the caller can | |
| * detect that the state is not fully resolved. | |
| * | |
| * Legitimate "no marker β default full profile" is NOT a warning. | |
| * A thrown error during profile/surface resolution IS a warning. | |
| * | |
| * @param cwd Project root directory | |
| * @param runtimeConfigDir Runtime config directory (e.g. ~/.claude). May be | |
| * empty/undefined β falls back to auto-detection. | |
| * Providing a value without a next token (e.g. the flag | |
| * is last in argv with no following value) should be | |
| * caught by the caller before invoking this function. | |
| * @param raw Whether to emit raw JSON (io.output raw mode) | |
| * @param _options Reserved for future use | |
| */ | |
| function resolveCapabilityRuntimeState(cwd, runtimeConfigDir, configOverride) { | |
| const warnings = []; | |
| // Resolve runtimeConfigDir using the canonical runtime-homes resolver. | |
| // When not provided, the active runtime is detected via the canonical | |
| // precedence: process.env.GSD_RUNTIME β config.runtime β 'claude' | |
| // (mirrors resolveRuntime() from runtime-slash.cjs and the precedence used | |
| // by profile-output.cjs and the rest of the runtime resolution chain). | |
| // getGlobalConfigDir(detectedRuntime) is then called, which honours the | |
| // runtime-specific env-var override (CLAUDE_CONFIG_DIR, CODEX_HOME, | |
| // CURSOR_CONFIG_DIR, GROK_AGENTS_HOME, etc.) correctly and without | |
| // fabricating env vars that don't exist upstream. | |
| let resolvedConfigDir = runtimeConfigDir || ''; | |
| if (!resolvedConfigDir) { | |
| try { | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const runtimeHomes = require('./runtime-homes.cjs'); | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const runtimeSlash = require('./runtime-slash.cjs'); | |
| // Detect the active runtime via GSD_RUNTIME β config.runtime β 'claude'. | |
| // resolveRuntime reads config.json directly (no side effects) and returns | |
| // a lowercased canonical runtime name. | |
| const detectedRuntime = runtimeSlash.resolveRuntime(cwd); | |
| resolvedConfigDir = runtimeHomes.getGlobalConfigDir(detectedRuntime); | |
| } | |
| catch { | |
| // Defensive fallback: use ~/.claude if the canonical resolver throws. | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const os = require('node:os'); | |
| resolvedConfigDir = node_path_1.default.join(os.homedir(), '.claude'); | |
| } | |
| } | |
| // ββ Load registry (ADR-857 phase 4c) ββββββββββββββββββββββββββββββββββββββββ | |
| // Load BEFORE resolveProfile and resolveSurface so both calls receive the | |
| // registry and capability-contributed skills are reflected in installed/surfaced. | |
| // No-op today (UI capability is tier:full β only adds to 'full', which returns | |
| // '*' regardless) but cutover-ready for future tier:core/standard capabilities. | |
| // eslint-disable-next-line @typescript-eslint/no-require-imports | |
| const registry = require('./capability-registry.cjs'); | |
| // ββ Resolve installed skills (from install profile) ββββββββββββββββββββββββββ | |
| // Distinguish "no profile marker β default full" (legitimate) from a thrown | |
| // error (surface as a warning and degrade gracefully β do NOT silently report | |
| // installedSkills='*' as if the install profile were truly unlimited). | |
| let installedSkills; | |
| try { | |
| const commandsGsdDir = _resolveCommandsGsdDir(); | |
| // Fix #1160: use _resolveManifest so installed-runtime layouts (where | |
| // commands/gsd is absent) fall back to <configDir>/skills/gsd-*/SKILL.md. | |
| const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir); | |
| const profileName = readActiveProfile(resolvedConfigDir) ?? 'full'; | |
| const resolvedInstall = resolveProfile({ | |
| modes: profileName.split(',').map((s) => s.trim()), | |
| manifest, | |
| registry, | |
| }); | |
| installedSkills = resolvedInstall.skills; | |
| } | |
| catch (err) { | |
| // Genuine resolution failure β surface it so the caller is not misled. | |
| const msg = err instanceof Error ? err.message : String(err); | |
| warnings.push(`profile-resolution failed: ${msg}`); | |
| // Degrade to empty set (not '*') so installed=false is reported accurately. | |
| installedSkills = new Set(); | |
| } | |
| // ββ Resolve surfaced skills (from runtime surface) ββββββββββββββββββββββββββββ | |
| let surfacedSkills; | |
| try { | |
| const commandsGsdDir = _resolveCommandsGsdDir(); | |
| // Fix #1160: use _resolveManifest so installed-runtime layouts (where | |
| // commands/gsd is absent) fall back to <configDir>/skills/gsd-*/SKILL.md. | |
| const manifest = _resolveManifest(commandsGsdDir, resolvedConfigDir); | |
| const surfaceResult = resolveSurface(resolvedConfigDir, manifest, undefined, registry); | |
| // resolveSurface returns { name, skills: Set<string>, agents: Set<string> } | |
| // (always a concrete Set β full profile is materialized) | |
| surfacedSkills = surfaceResult.skills instanceof Set | |
| ? surfaceResult.skills | |
| : new Set(); | |
| } | |
| catch (err) { | |
| // Genuine surface resolution failure β surface it so the caller is not misled. | |
| const msg = err instanceof Error ? err.message : String(err); | |
| warnings.push(`surface-resolution failed: ${msg}`); | |
| surfacedSkills = new Set(); | |
| } | |
| // ββ Load config βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // When the caller already holds a loadConfig snapshot (e.g. cmdLoopRenderHooks), | |
| // accept it via configOverride so capability `active` and hook resolution | |
| // share the SAME config object β single snapshot, no TOCTOU window. | |
| let config; | |
| if (configOverride !== undefined) { | |
| config = configOverride; | |
| } | |
| else { | |
| try { | |
| config = loadConfig(cwd); | |
| } | |
| catch { | |
| config = {}; | |
| } | |
| } | |
| // ββ Resolve state ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const result = resolveCapabilityState({ | |
| registry, | |
| installedSkills, | |
| surfacedSkills, | |
| config, | |
| cwd, | |
| }); | |
| return { | |
| runtimeConfigDir: resolvedConfigDir, | |
| warnings, | |
| capabilities: result.capabilities, | |
| }; | |
| } | |
| function cmdCapabilityState(cwd, runtimeConfigDir, raw, _options = {}) { | |
| const result = resolveCapabilityRuntimeState(cwd, runtimeConfigDir); | |
| for (const warning of result.warnings) { | |
| coreError(`capability state: ${warning}`); | |
| } | |
| // Build envelope β include warnings array only when non-empty so the nominal | |
| // path keeps the output clean and callers can check `warnings` for degraded state. | |
| const envelope = { | |
| runtimeConfigDir: result.runtimeConfigDir, | |
| capabilities: result.capabilities, | |
| }; | |
| if (result.warnings.length > 0) { | |
| envelope.warnings = result.warnings; | |
| } | |
| coreOutput(envelope, raw); | |
| } | |
| /** | |
| * Convenience predicate: returns true if the capability identified by `capId` | |
| * is active (installed && surfaced && config-enabled) in the current runtime | |
| * environment at `cwd`. | |
| * | |
| * Internally calls `resolveCapabilityRuntimeState(cwd, undefined)` and returns | |
| * the `active` field of the matching CapabilityStateEntry. | |
| * Returns `false` when the capability is not found in the registry. | |
| * | |
| * @param capId Capability identifier (e.g. 'graphify', 'intel') | |
| * @param cwd Project root directory for config resolution | |
| */ | |
| function isCapabilityActive(capId, cwd) { | |
| const result = resolveCapabilityRuntimeState(cwd, undefined); | |
| const entry = result.capabilities.find((c) => c.id === capId); | |
| return entry !== undefined ? entry.active : false; | |
| } | |
| module.exports = { | |
| resolveCapabilityState, | |
| resolveCapabilityRuntimeState, | |
| isCapabilityActive, | |
| cmdCapabilityState, | |
| // Exported for tests | |
| _resolveCommandsGsdDir, | |
| _loadInstalledSkillsManifest, | |
| _resolveManifest, | |
| _isSafePropKey, | |
| }; | |