mirror of
https://github.com/obra/superpowers.git
synced 2026-04-22 01:19:04 +08:00
Compare commits
6 Commits
v5.0.0
...
wip-gemini
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5e6eaf411 | ||
|
|
bdd45c70ab | ||
|
|
ec3f7f1027 | ||
|
|
edbb62e50f | ||
|
|
74f2b1c96e | ||
|
|
991e9d4de9 |
1
.gemini/GEMINI.md
Normal file
1
.gemini/GEMINI.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@../skills/using-superpowers/SKILL.md
|
||||||
6
.gemini/gemini-extension.json
Normal file
6
.gemini/gemini-extension.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "superpowers",
|
||||||
|
"description": "Core skills library: TDD, debugging, collaboration patterns, and proven techniques",
|
||||||
|
"version": "5.0.0",
|
||||||
|
"contextFileName": "GEMINI.md"
|
||||||
|
}
|
||||||
@@ -35,17 +35,27 @@ warning_escaped=$(escape_for_json "$warning_message")
|
|||||||
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
|
session_context="<EXTREMELY_IMPORTANT>\nYou have superpowers.\n\n**Below is the full content of your 'superpowers:using-superpowers' skill - your introduction to using skills. For all other skills, use the 'Skill' tool:**\n\n${using_superpowers_escaped}\n\n${warning_escaped}\n</EXTREMELY_IMPORTANT>"
|
||||||
|
|
||||||
# Output context injection as JSON.
|
# Output context injection as JSON.
|
||||||
# Keep both shapes for compatibility:
|
# Cursor hooks expect additional_context.
|
||||||
# - Cursor hooks expect additional_context.
|
# Claude Code hooks expect hookSpecificOutput.additionalContext.
|
||||||
# - Claude hooks expect hookSpecificOutput.additionalContext.
|
# Claude Code reads BOTH fields without deduplication, so we must only
|
||||||
cat <<EOF
|
# emit the field consumed by the current platform to avoid double injection.
|
||||||
|
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
|
||||||
|
# Claude Code sets CLAUDE_PLUGIN_ROOT — emit only hookSpecificOutput
|
||||||
|
cat <<EOF
|
||||||
{
|
{
|
||||||
"additional_context": "${session_context}",
|
|
||||||
"hookSpecificOutput": {
|
"hookSpecificOutput": {
|
||||||
"hookEventName": "SessionStart",
|
"hookEventName": "SessionStart",
|
||||||
"additionalContext": "${session_context}"
|
"additionalContext": "${session_context}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
else
|
||||||
|
# Other platforms (Cursor, etc.) — emit only additional_context
|
||||||
|
cat <<EOF
|
||||||
|
{
|
||||||
|
"additional_context": "${session_context}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
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
|
|
||||||
};
|
|
||||||
@@ -27,7 +27,8 @@ You MUST create a task for each of these items and complete them in order:
|
|||||||
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
|
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
|
||||||
5. **Present design** — in sections scaled to their complexity, get user approval after each section
|
5. **Present design** — in sections scaled to their complexity, get user approval after each section
|
||||||
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
|
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
|
||||||
7. **Transition to implementation** — invoke writing-plans skill to create implementation plan
|
7. **User reviews written spec** — ask user to review the spec file before proceeding
|
||||||
|
8. **Transition to implementation** — invoke writing-plans skill to create implementation plan
|
||||||
|
|
||||||
## Process Flow
|
## Process Flow
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ digraph brainstorming {
|
|||||||
"Present design sections" [shape=box];
|
"Present design sections" [shape=box];
|
||||||
"User approves design?" [shape=diamond];
|
"User approves design?" [shape=diamond];
|
||||||
"Write design doc" [shape=box];
|
"Write design doc" [shape=box];
|
||||||
|
"User reviews spec?" [shape=diamond];
|
||||||
"Invoke writing-plans skill" [shape=doublecircle];
|
"Invoke writing-plans skill" [shape=doublecircle];
|
||||||
|
|
||||||
"Explore project context" -> "Visual questions ahead?";
|
"Explore project context" -> "Visual questions ahead?";
|
||||||
@@ -52,7 +54,9 @@ digraph brainstorming {
|
|||||||
"Present design sections" -> "User approves design?";
|
"Present design sections" -> "User approves design?";
|
||||||
"User approves design?" -> "Present design sections" [label="no, revise"];
|
"User approves design?" -> "Present design sections" [label="no, revise"];
|
||||||
"User approves design?" -> "Write design doc" [label="yes"];
|
"User approves design?" -> "Write design doc" [label="yes"];
|
||||||
"Write design doc" -> "Invoke writing-plans skill";
|
"Write design doc" -> "User reviews spec?";
|
||||||
|
"User reviews spec?" -> "Write design doc" [label="changes requested"];
|
||||||
|
"User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -113,6 +117,13 @@ After writing the spec document:
|
|||||||
2. If Issues Found: fix, re-dispatch, repeat until Approved
|
2. If Issues Found: fix, re-dispatch, repeat until Approved
|
||||||
3. If loop exceeds 5 iterations, surface to human for guidance
|
3. If loop exceeds 5 iterations, surface to human for guidance
|
||||||
|
|
||||||
|
**User Review Gate:**
|
||||||
|
After the spec review loop passes, ask the user to review the written spec before proceeding:
|
||||||
|
|
||||||
|
> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
|
||||||
|
|
||||||
|
Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
|
||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
|
|
||||||
- Invoke the writing-plans skill to create a detailed implementation plan
|
- Invoke the writing-plans skill to create a detailed implementation plan
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def analyze_main_session(filepath):
|
|||||||
subagent_usage[agent_id]['output_tokens'] += usage.get('output_tokens', 0)
|
subagent_usage[agent_id]['output_tokens'] += usage.get('output_tokens', 0)
|
||||||
subagent_usage[agent_id]['cache_creation'] += usage.get('cache_creation_input_tokens', 0)
|
subagent_usage[agent_id]['cache_creation'] += usage.get('cache_creation_input_tokens', 0)
|
||||||
subagent_usage[agent_id]['cache_read'] += usage.get('cache_read_input_tokens', 0)
|
subagent_usage[agent_id]['cache_read'] += usage.get('cache_read_input_tokens', 0)
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return main_usage, dict(subagent_usage)
|
return main_usage, dict(subagent_usage)
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ while [[ $# -gt 0 ]]; do
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Tests:"
|
echo "Tests:"
|
||||||
echo " test-plugin-loading.sh Verify plugin installation and structure"
|
echo " test-plugin-loading.sh Verify plugin installation and structure"
|
||||||
echo " test-skills-core.sh Test skills-core.js library functions"
|
|
||||||
echo " test-tools.sh Test use_skill and find_skills tools (integration)"
|
echo " test-tools.sh Test use_skill and find_skills tools (integration)"
|
||||||
echo " test-priority.sh Test skill priority resolution (integration)"
|
echo " test-priority.sh Test skill priority resolution (integration)"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -60,7 +59,6 @@ done
|
|||||||
# List of tests to run (no external dependencies)
|
# List of tests to run (no external dependencies)
|
||||||
tests=(
|
tests=(
|
||||||
"test-plugin-loading.sh"
|
"test-plugin-loading.sh"
|
||||||
"test-skills-core.sh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Integration tests (require OpenCode)
|
# Integration tests (require OpenCode)
|
||||||
|
|||||||
@@ -30,17 +30,8 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 2: Verify lib/skills-core.js is in place
|
# Test 2: Verify skills directory is populated
|
||||||
echo "Test 2: Checking skills-core.js..."
|
echo "Test 2: Checking skills directory..."
|
||||||
if [ -f "$HOME/.config/opencode/superpowers/lib/skills-core.js" ]; then
|
|
||||||
echo " [PASS] skills-core.js exists"
|
|
||||||
else
|
|
||||||
echo " [FAIL] skills-core.js not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 3: Verify skills directory is populated
|
|
||||||
echo "Test 3: Checking skills directory..."
|
|
||||||
skill_count=$(find "$HOME/.config/opencode/superpowers/skills" -name "SKILL.md" | wc -l)
|
skill_count=$(find "$HOME/.config/opencode/superpowers/skills" -name "SKILL.md" | wc -l)
|
||||||
if [ "$skill_count" -gt 0 ]; then
|
if [ "$skill_count" -gt 0 ]; then
|
||||||
echo " [PASS] Found $skill_count skills installed"
|
echo " [PASS] Found $skill_count skills installed"
|
||||||
|
|||||||
@@ -1,440 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Test: Skills Core Library
|
|
||||||
# Tests the skills-core.js library functions directly via Node.js
|
|
||||||
# Does not require OpenCode - tests pure library functionality
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
|
|
||||||
echo "=== Test: Skills Core Library ==="
|
|
||||||
|
|
||||||
# Source setup to create isolated environment
|
|
||||||
source "$SCRIPT_DIR/setup.sh"
|
|
||||||
|
|
||||||
# Trap to cleanup on exit
|
|
||||||
trap cleanup_test_env EXIT
|
|
||||||
|
|
||||||
# Test 1: Test extractFrontmatter function
|
|
||||||
echo "Test 1: Testing extractFrontmatter..."
|
|
||||||
|
|
||||||
# Create test file with frontmatter
|
|
||||||
test_skill_dir="$TEST_HOME/test-skill"
|
|
||||||
mkdir -p "$test_skill_dir"
|
|
||||||
cat > "$test_skill_dir/SKILL.md" <<'EOF'
|
|
||||||
---
|
|
||||||
name: test-skill
|
|
||||||
description: A test skill for unit testing
|
|
||||||
---
|
|
||||||
# Test Skill Content
|
|
||||||
|
|
||||||
This is the content.
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Run Node.js test using inline function (avoids ESM path resolution issues in test env)
|
|
||||||
result=$(node -e "
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
// Inline the extractFrontmatter function for testing
|
|
||||||
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;
|
|
||||||
if (key === 'name') name = value.trim();
|
|
||||||
if (key === 'description') description = value.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { name, description };
|
|
||||||
} catch (error) {
|
|
||||||
return { name: '', description: '' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = extractFrontmatter('$TEST_HOME/test-skill/SKILL.md');
|
|
||||||
console.log(JSON.stringify(result));
|
|
||||||
" 2>&1)
|
|
||||||
|
|
||||||
if echo "$result" | grep -q '"name":"test-skill"'; then
|
|
||||||
echo " [PASS] extractFrontmatter parses name correctly"
|
|
||||||
else
|
|
||||||
echo " [FAIL] extractFrontmatter did not parse name"
|
|
||||||
echo " Result: $result"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$result" | grep -q '"description":"A test skill for unit testing"'; then
|
|
||||||
echo " [PASS] extractFrontmatter parses description correctly"
|
|
||||||
else
|
|
||||||
echo " [FAIL] extractFrontmatter did not parse description"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 2: Test stripFrontmatter function
|
|
||||||
echo ""
|
|
||||||
echo "Test 2: Testing stripFrontmatter..."
|
|
||||||
|
|
||||||
result=$(node -e "
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = fs.readFileSync('$TEST_HOME/test-skill/SKILL.md', 'utf8');
|
|
||||||
const stripped = stripFrontmatter(content);
|
|
||||||
console.log(stripped);
|
|
||||||
" 2>&1)
|
|
||||||
|
|
||||||
if echo "$result" | grep -q "# Test Skill Content"; then
|
|
||||||
echo " [PASS] stripFrontmatter preserves content"
|
|
||||||
else
|
|
||||||
echo " [FAIL] stripFrontmatter did not preserve content"
|
|
||||||
echo " Result: $result"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! echo "$result" | grep -q "name: test-skill"; then
|
|
||||||
echo " [PASS] stripFrontmatter removes frontmatter"
|
|
||||||
else
|
|
||||||
echo " [FAIL] stripFrontmatter did not remove frontmatter"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 3: Test findSkillsInDir function
|
|
||||||
echo ""
|
|
||||||
echo "Test 3: Testing findSkillsInDir..."
|
|
||||||
|
|
||||||
# Create multiple test skills
|
|
||||||
mkdir -p "$TEST_HOME/skills-dir/skill-a"
|
|
||||||
mkdir -p "$TEST_HOME/skills-dir/skill-b"
|
|
||||||
mkdir -p "$TEST_HOME/skills-dir/nested/skill-c"
|
|
||||||
|
|
||||||
cat > "$TEST_HOME/skills-dir/skill-a/SKILL.md" <<'EOF'
|
|
||||||
---
|
|
||||||
name: skill-a
|
|
||||||
description: First skill
|
|
||||||
---
|
|
||||||
# Skill A
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TEST_HOME/skills-dir/skill-b/SKILL.md" <<'EOF'
|
|
||||||
---
|
|
||||||
name: skill-b
|
|
||||||
description: Second skill
|
|
||||||
---
|
|
||||||
# Skill B
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TEST_HOME/skills-dir/nested/skill-c/SKILL.md" <<'EOF'
|
|
||||||
---
|
|
||||||
name: skill-c
|
|
||||||
description: Nested skill
|
|
||||||
---
|
|
||||||
# Skill C
|
|
||||||
EOF
|
|
||||||
|
|
||||||
result=$(node -e "
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
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;
|
|
||||||
if (key === 'name') name = value.trim();
|
|
||||||
if (key === 'description') description = value.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { name, description };
|
|
||||||
} catch (error) {
|
|
||||||
return { name: '', description: '' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()) {
|
|
||||||
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(fullPath, depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recurse(dir, 0);
|
|
||||||
return skills;
|
|
||||||
}
|
|
||||||
|
|
||||||
const skills = findSkillsInDir('$TEST_HOME/skills-dir', 'test', 3);
|
|
||||||
console.log(JSON.stringify(skills, null, 2));
|
|
||||||
" 2>&1)
|
|
||||||
|
|
||||||
skill_count=$(echo "$result" | grep -c '"name":' || echo "0")
|
|
||||||
|
|
||||||
if [ "$skill_count" -ge 3 ]; then
|
|
||||||
echo " [PASS] findSkillsInDir found all skills (found $skill_count)"
|
|
||||||
else
|
|
||||||
echo " [FAIL] findSkillsInDir did not find all skills (expected 3, found $skill_count)"
|
|
||||||
echo " Result: $result"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$result" | grep -q '"name": "skill-c"'; then
|
|
||||||
echo " [PASS] findSkillsInDir found nested skills"
|
|
||||||
else
|
|
||||||
echo " [FAIL] findSkillsInDir did not find nested skill"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 4: Test resolveSkillPath function
|
|
||||||
echo ""
|
|
||||||
echo "Test 4: Testing resolveSkillPath..."
|
|
||||||
|
|
||||||
# Create skills in personal and superpowers locations for testing
|
|
||||||
mkdir -p "$TEST_HOME/personal-skills/shared-skill"
|
|
||||||
mkdir -p "$TEST_HOME/superpowers-skills/shared-skill"
|
|
||||||
mkdir -p "$TEST_HOME/superpowers-skills/unique-skill"
|
|
||||||
|
|
||||||
cat > "$TEST_HOME/personal-skills/shared-skill/SKILL.md" <<'EOF'
|
|
||||||
---
|
|
||||||
name: shared-skill
|
|
||||||
description: Personal version
|
|
||||||
---
|
|
||||||
# Personal Shared
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TEST_HOME/superpowers-skills/shared-skill/SKILL.md" <<'EOF'
|
|
||||||
---
|
|
||||||
name: shared-skill
|
|
||||||
description: Superpowers version
|
|
||||||
---
|
|
||||||
# Superpowers Shared
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "$TEST_HOME/superpowers-skills/unique-skill/SKILL.md" <<'EOF'
|
|
||||||
---
|
|
||||||
name: unique-skill
|
|
||||||
description: Only in superpowers
|
|
||||||
---
|
|
||||||
# Unique
|
|
||||||
EOF
|
|
||||||
|
|
||||||
result=$(node -e "
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
function resolveSkillPath(skillName, superpowersDir, personalDir) {
|
|
||||||
const forceSuperpowers = skillName.startsWith('superpowers:');
|
|
||||||
const actualSkillName = forceSuperpowers ? skillName.replace(/^superpowers:/, '') : skillName;
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const superpowersDir = '$TEST_HOME/superpowers-skills';
|
|
||||||
const personalDir = '$TEST_HOME/personal-skills';
|
|
||||||
|
|
||||||
// Test 1: Shared skill should resolve to personal
|
|
||||||
const shared = resolveSkillPath('shared-skill', superpowersDir, personalDir);
|
|
||||||
console.log('SHARED:', JSON.stringify(shared));
|
|
||||||
|
|
||||||
// Test 2: superpowers: prefix should force superpowers
|
|
||||||
const forced = resolveSkillPath('superpowers:shared-skill', superpowersDir, personalDir);
|
|
||||||
console.log('FORCED:', JSON.stringify(forced));
|
|
||||||
|
|
||||||
// Test 3: Unique skill should resolve to superpowers
|
|
||||||
const unique = resolveSkillPath('unique-skill', superpowersDir, personalDir);
|
|
||||||
console.log('UNIQUE:', JSON.stringify(unique));
|
|
||||||
|
|
||||||
// Test 4: Non-existent skill
|
|
||||||
const notfound = resolveSkillPath('not-a-skill', superpowersDir, personalDir);
|
|
||||||
console.log('NOTFOUND:', JSON.stringify(notfound));
|
|
||||||
" 2>&1)
|
|
||||||
|
|
||||||
if echo "$result" | grep -q 'SHARED:.*"sourceType":"personal"'; then
|
|
||||||
echo " [PASS] Personal skills shadow superpowers skills"
|
|
||||||
else
|
|
||||||
echo " [FAIL] Personal skills not shadowing correctly"
|
|
||||||
echo " Result: $result"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$result" | grep -q 'FORCED:.*"sourceType":"superpowers"'; then
|
|
||||||
echo " [PASS] superpowers: prefix forces superpowers resolution"
|
|
||||||
else
|
|
||||||
echo " [FAIL] superpowers: prefix not working"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$result" | grep -q 'UNIQUE:.*"sourceType":"superpowers"'; then
|
|
||||||
echo " [PASS] Unique superpowers skills are found"
|
|
||||||
else
|
|
||||||
echo " [FAIL] Unique superpowers skills not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$result" | grep -q 'NOTFOUND: null'; then
|
|
||||||
echo " [PASS] Non-existent skills return null"
|
|
||||||
else
|
|
||||||
echo " [FAIL] Non-existent skills should return null"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Test 5: Test checkForUpdates function
|
|
||||||
echo ""
|
|
||||||
echo "Test 5: Testing checkForUpdates..."
|
|
||||||
|
|
||||||
# Create a test git repo
|
|
||||||
mkdir -p "$TEST_HOME/test-repo"
|
|
||||||
cd "$TEST_HOME/test-repo"
|
|
||||||
git init --quiet
|
|
||||||
git config user.email "test@test.com"
|
|
||||||
git config user.name "Test"
|
|
||||||
echo "test" > file.txt
|
|
||||||
git add file.txt
|
|
||||||
git commit -m "initial" --quiet
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
# Test checkForUpdates on repo without remote (should return false, not error)
|
|
||||||
result=$(node -e "
|
|
||||||
const { execSync } = require('child_process');
|
|
||||||
|
|
||||||
function checkForUpdates(repoDir) {
|
|
||||||
try {
|
|
||||||
const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
|
|
||||||
cwd: repoDir,
|
|
||||||
timeout: 3000,
|
|
||||||
encoding: 'utf8',
|
|
||||||
stdio: 'pipe'
|
|
||||||
});
|
|
||||||
const statusLines = output.split('\n');
|
|
||||||
for (const line of statusLines) {
|
|
||||||
if (line.startsWith('## ') && line.includes('[behind ')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test 1: Repo without remote should return false (graceful error handling)
|
|
||||||
const result1 = checkForUpdates('$TEST_HOME/test-repo');
|
|
||||||
console.log('NO_REMOTE:', result1);
|
|
||||||
|
|
||||||
// Test 2: Non-existent directory should return false
|
|
||||||
const result2 = checkForUpdates('$TEST_HOME/nonexistent');
|
|
||||||
console.log('NONEXISTENT:', result2);
|
|
||||||
|
|
||||||
// Test 3: Non-git directory should return false
|
|
||||||
const result3 = checkForUpdates('$TEST_HOME');
|
|
||||||
console.log('NOT_GIT:', result3);
|
|
||||||
" 2>&1)
|
|
||||||
|
|
||||||
if echo "$result" | grep -q 'NO_REMOTE: false'; then
|
|
||||||
echo " [PASS] checkForUpdates handles repo without remote gracefully"
|
|
||||||
else
|
|
||||||
echo " [FAIL] checkForUpdates should return false for repo without remote"
|
|
||||||
echo " Result: $result"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$result" | grep -q 'NONEXISTENT: false'; then
|
|
||||||
echo " [PASS] checkForUpdates handles non-existent directory"
|
|
||||||
else
|
|
||||||
echo " [FAIL] checkForUpdates should return false for non-existent directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$result" | grep -q 'NOT_GIT: false'; then
|
|
||||||
echo " [PASS] checkForUpdates handles non-git directory"
|
|
||||||
else
|
|
||||||
echo " [FAIL] checkForUpdates should return false for non-git directory"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== All skills-core library tests passed ==="
|
|
||||||
Reference in New Issue
Block a user