@fission-ai/openspec
Version:
AI-native system for spec-driven development
140 lines • 4.46 kB
JavaScript
/**
* Tool Detection Utilities
*
* Shared utilities for detecting tool configurations and version status.
*/
import path from 'path';
import * as fs from 'fs';
import { AI_TOOLS } from '../config.js';
/**
* Names of skill directories created by openspec init.
*/
export const SKILL_NAMES = [
'openspec-explore',
'openspec-new-change',
'openspec-continue-change',
'openspec-apply-change',
'openspec-ff-change',
'openspec-sync-specs',
'openspec-archive-change',
'openspec-bulk-archive-change',
'openspec-verify-change',
];
/**
* Gets the list of tools with skillsDir configured.
*/
export function getToolsWithSkillsDir() {
return AI_TOOLS.filter((t) => t.skillsDir).map((t) => t.value);
}
/**
* Checks which skill files exist for a tool.
*/
export function getToolSkillStatus(projectRoot, toolId) {
const tool = AI_TOOLS.find((t) => t.value === toolId);
if (!tool?.skillsDir) {
return { configured: false, fullyConfigured: false, skillCount: 0 };
}
const skillsDir = path.join(projectRoot, tool.skillsDir, 'skills');
let skillCount = 0;
for (const skillName of SKILL_NAMES) {
const skillFile = path.join(skillsDir, skillName, 'SKILL.md');
if (fs.existsSync(skillFile)) {
skillCount++;
}
}
return {
configured: skillCount > 0,
fullyConfigured: skillCount === SKILL_NAMES.length,
skillCount,
};
}
/**
* Gets the skill status for all tools with skillsDir configured.
*/
export function getToolStates(projectRoot) {
const states = new Map();
const toolIds = AI_TOOLS.filter((t) => t.skillsDir).map((t) => t.value);
for (const toolId of toolIds) {
states.set(toolId, getToolSkillStatus(projectRoot, toolId));
}
return states;
}
/**
* Extracts the generatedBy version from a skill file's YAML frontmatter.
* Returns null if the field is not found or the file doesn't exist.
*/
export function extractGeneratedByVersion(skillFilePath) {
try {
if (!fs.existsSync(skillFilePath)) {
return null;
}
const content = fs.readFileSync(skillFilePath, 'utf-8');
// Look for generatedBy in the YAML frontmatter
// The file format is:
// ---
// ...
// metadata:
// author: openspec
// version: "1.0"
// generatedBy: "0.23.0"
// ---
const generatedByMatch = content.match(/^\s*generatedBy:\s*["']?([^"'\n]+)["']?\s*$/m);
if (generatedByMatch && generatedByMatch[1]) {
return generatedByMatch[1].trim();
}
return null;
}
catch {
return null;
}
}
/**
* Gets version status for a tool by reading the first available skill file.
*/
export function getToolVersionStatus(projectRoot, toolId, currentVersion) {
const tool = AI_TOOLS.find((t) => t.value === toolId);
if (!tool?.skillsDir) {
return {
toolId,
toolName: toolId,
configured: false,
generatedByVersion: null,
needsUpdate: false,
};
}
const skillsDir = path.join(projectRoot, tool.skillsDir, 'skills');
let generatedByVersion = null;
// Find the first skill file that exists and read its version
for (const skillName of SKILL_NAMES) {
const skillFile = path.join(skillsDir, skillName, 'SKILL.md');
if (fs.existsSync(skillFile)) {
generatedByVersion = extractGeneratedByVersion(skillFile);
break;
}
}
const configured = getToolSkillStatus(projectRoot, toolId).configured;
const needsUpdate = configured && (generatedByVersion === null || generatedByVersion !== currentVersion);
return {
toolId,
toolName: tool.name,
configured,
generatedByVersion,
needsUpdate,
};
}
/**
* Gets all configured tools in the project.
*/
export function getConfiguredTools(projectRoot) {
return AI_TOOLS
.filter((t) => t.skillsDir && getToolSkillStatus(projectRoot, t.value).configured)
.map((t) => t.value);
}
/**
* Gets version status for all configured tools.
*/
export function getAllToolVersionStatus(projectRoot, currentVersion) {
const configuredTools = getConfiguredTools(projectRoot);
return configuredTools.map((toolId) => getToolVersionStatus(projectRoot, toolId, currentVersion));
}
//# sourceMappingURL=tool-detection.js.map