| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- import fs from 'fs';
- import path from 'path';
- import { execSync } from 'child_process';
- /**
- * Extract YAML frontmatter from a skill file.
- * Current format:
- * ---
- * name: skill-name
- * description: Use when [condition] - [what it does]
- * ---
- *
- * @param {string} filePath - Path to SKILL.md file
- * @returns {{name: string, description: string}}
- */
- function extractFrontmatter(filePath) {
- try {
- const content = fs.readFileSync(filePath, 'utf8');
- const lines = content.split('\n');
- let inFrontmatter = false;
- let name = '';
- let description = '';
- for (const line of lines) {
- if (line.trim() === '---') {
- if (inFrontmatter) break;
- inFrontmatter = true;
- continue;
- }
- if (inFrontmatter) {
- const match = line.match(/^(\w+):\s*(.*)$/);
- if (match) {
- const [, key, value] = match;
- switch (key) {
- case 'name':
- name = value.trim();
- break;
- case 'description':
- description = value.trim();
- break;
- }
- }
- }
- }
- return { name, description };
- } catch (error) {
- return { name: '', description: '' };
- }
- }
- /**
- * Find all SKILL.md files in a directory recursively.
- *
- * @param {string} dir - Directory to search
- * @param {string} sourceType - 'personal' or 'superpowers' for namespacing
- * @param {number} maxDepth - Maximum recursion depth (default: 3)
- * @returns {Array<{path: string, name: string, description: string, sourceType: string}>}
- */
- function findSkillsInDir(dir, sourceType, maxDepth = 3) {
- const skills = [];
- if (!fs.existsSync(dir)) return skills;
- function recurse(currentDir, depth) {
- if (depth > maxDepth) return;
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
- for (const entry of entries) {
- const fullPath = path.join(currentDir, entry.name);
- if (entry.isDirectory()) {
- // Check for SKILL.md in this directory
- const skillFile = path.join(fullPath, 'SKILL.md');
- if (fs.existsSync(skillFile)) {
- const { name, description } = extractFrontmatter(skillFile);
- skills.push({
- path: fullPath,
- skillFile: skillFile,
- name: name || entry.name,
- description: description || '',
- sourceType: sourceType
- });
- }
- // Recurse into subdirectories
- recurse(fullPath, depth + 1);
- }
- }
- }
- recurse(dir, 0);
- return skills;
- }
- /**
- * Resolve a skill name to its file path, handling shadowing
- * (personal skills override superpowers skills).
- *
- * @param {string} skillName - Name like "superpowers:brainstorming" or "my-skill"
- * @param {string} superpowersDir - Path to superpowers skills directory
- * @param {string} personalDir - Path to personal skills directory
- * @returns {{skillFile: string, sourceType: string, skillPath: string} | null}
- */
- function resolveSkillPath(skillName, superpowersDir, personalDir) {
- // Strip superpowers: prefix if present
- const forceSuperpowers = skillName.startsWith('superpowers:');
- const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
- // Try personal skills first (unless explicitly superpowers:)
- if (!forceSuperpowers && personalDir) {
- const personalPath = path.join(personalDir, actualSkillName);
- const personalSkillFile = path.join(personalPath, 'SKILL.md');
- if (fs.existsSync(personalSkillFile)) {
- return {
- skillFile: personalSkillFile,
- sourceType: 'personal',
- skillPath: actualSkillName
- };
- }
- }
- // Try superpowers skills
- if (superpowersDir) {
- const superpowersPath = path.join(superpowersDir, actualSkillName);
- const superpowersSkillFile = path.join(superpowersPath, 'SKILL.md');
- if (fs.existsSync(superpowersSkillFile)) {
- return {
- skillFile: superpowersSkillFile,
- sourceType: 'superpowers',
- skillPath: actualSkillName
- };
- }
- }
- return null;
- }
- /**
- * Check if a git repository has updates available.
- *
- * @param {string} repoDir - Path to git repository
- * @returns {boolean} - True if updates are available
- */
- function checkForUpdates(repoDir) {
- try {
- // Quick check with 3 second timeout to avoid delays if network is down
- const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
- cwd: repoDir,
- timeout: 3000,
- encoding: 'utf8',
- stdio: 'pipe'
- });
- // Parse git status output to see if we're behind
- const statusLines = output.split('\n');
- for (const line of statusLines) {
- if (line.startsWith('## ') && line.includes('[behind ')) {
- return true; // We're behind remote
- }
- }
- return false; // Up to date
- } catch (error) {
- // Network down, git error, timeout, etc. - don't block bootstrap
- return false;
- }
- }
- /**
- * Strip YAML frontmatter from skill content, returning just the content.
- *
- * @param {string} content - Full content including frontmatter
- * @returns {string} - Content without frontmatter
- */
- function stripFrontmatter(content) {
- const lines = content.split('\n');
- let inFrontmatter = false;
- let frontmatterEnded = false;
- const contentLines = [];
- for (const line of lines) {
- if (line.trim() === '---') {
- if (inFrontmatter) {
- frontmatterEnded = true;
- continue;
- }
- inFrontmatter = true;
- continue;
- }
- if (frontmatterEnded || !inFrontmatter) {
- contentLines.push(line);
- }
- }
- return contentLines.join('\n').trim();
- }
- export {
- extractFrontmatter,
- findSkillsInDir,
- resolveSkillPath,
- checkForUpdates,
- stripFrontmatter
- };
|