Compare commits

...

6 Commits

Author SHA1 Message Date
Jesse Vincent
c5e6eaf411 refactor: replace MCP server with native Gemini CLI extension
Remove the custom MCP server (find_skills/use_skill tools) and
force-invoke GEMINI.md. Gemini CLI natively supports the Agent Skills
format — our skills/ directory works as-is.

GEMINI.md now uses @import to inline using-superpowers content at
session start. Needs testing to verify @import resolves relative
to the extension root.
2026-03-09 18:53:45 -07:00
Jesse Vincent
bdd45c70ab WIP: Gemini CLI extension infrastructure
Add experimental Gemini CLI extension with MCP server that exposes
skills as individual tools. Infrastructure works but auto-triggering
skills is blocked by Gemini CLI treating context files as advisory
rather than executable instructions.

See issue #128 for detailed findings.

- gemini-extension.json manifest
- MCP server with individual skill tools
- GEMINI.md bootstrap attempts (don't work)
- Installation documentation
2026-03-09 18:26:35 -07:00
Jesse Vincent
ec3f7f1027 fix(brainstorming): add user review gate between spec and writing-plans
After the spec review loop passes, the skill now asks the user to review
the written spec file before invoking writing-plans. This prevents the
agent from racing ahead to implementation planning without giving the
user a chance to read and adjust the written document.

Fixes #565
2026-03-09 18:16:22 -07:00
Jesse Vincent
edbb62e50f chore: remove dead lib/skills-core.js and its tests
Last consumer (Codex bootstrap CLI) was removed on 2026-02-05.
Removes the library, its dedicated test file, and references
in test-plugin-loading.sh and run-tests.sh.

h/t @RomarQ (PR #525) for flagging this.
2026-03-09 17:40:52 -07:00
mvanhorn
74f2b1c96e fix(hooks): emit session-start context only once per platform
Claude Code reads both additional_context and hookSpecificOutput without
deduplication, causing double injection. Detect platform via
CLAUDE_PLUGIN_ROOT and emit only the appropriate field.

Co-authored-by: mvanhorn <mvanhorn@users.noreply.github.com>
2026-03-09 17:20:31 -07:00
daniel-graham
991e9d4de9 fix: replace bare except with except Exception
Co-authored-by: daniel-graham <daniel-graham@users.noreply.github.com>
2026-03-09 17:10:07 -07:00
9 changed files with 38 additions and 669 deletions

1
.gemini/GEMINI.md Normal file
View File

@@ -0,0 +1 @@
@../skills/using-superpowers/SKILL.md

View 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"
}

View File

@@ -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
# 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 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

View File

@@ -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
};

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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"

View File

@@ -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 ==="