diff --git a/.codex/INSTALL.md b/.codex/INSTALL.md index 1250f5e07..d1ce46cc9 100644 --- a/.codex/INSTALL.md +++ b/.codex/INSTALL.md @@ -32,4 +32,14 @@ Test the installation: ~/.codex/superpowers/.codex/superpowers-codex bootstrap ``` -You should see skill listings and bootstrap instructions. The system is now ready for use. \ No newline at end of file +On Windows, use one of these instead: + +```bash +~/.codex/superpowers/.codex/superpowers-codex.cmd bootstrap +``` + +```bash +node ~/.codex/superpowers/.codex/superpowers-codex bootstrap +``` + +You should see skill listings and bootstrap instructions. The system is now ready for use. diff --git a/.codex/superpowers-bootstrap.md b/.codex/superpowers-bootstrap.md index 18fe657f0..4d4816a85 100644 --- a/.codex/superpowers-bootstrap.md +++ b/.codex/superpowers-bootstrap.md @@ -5,6 +5,8 @@ You have superpowers. **Tool for running skills:** - `~/.codex/superpowers/.codex/superpowers-codex use-skill ` + - On Windows: `~/.codex/superpowers/.codex/superpowers-codex.cmd use-skill ` (recommended) + - On any OS: `node ~/.codex/superpowers/.codex/superpowers-codex use-skill ` **Tool Mapping for Codex:** When skills reference tools you don't have, substitute your equivalent tools: diff --git a/.codex/superpowers-codex b/.codex/superpowers-codex index 1d9a0efb6..14a00ff5d 100755 --- a/.codex/superpowers-codex +++ b/.codex/superpowers-codex @@ -3,7 +3,9 @@ const fs = require('fs'); const path = require('path'); const os = require('os'); -const skillsCore = require('../lib/skills-core'); +const { pathToFileURL } = require('url'); + +let skillsCore; // Paths const homeDir = os.homedir(); @@ -238,30 +240,42 @@ function runUseSkill(skillName) { } -// Main CLI -const command = process.argv[2]; -const arg = process.argv[3]; - -switch (command) { - case 'bootstrap': - runBootstrap(); - break; - case 'use-skill': - runUseSkill(arg); - break; - case 'find-skills': - runFindSkills(); - break; - default: - console.log('Superpowers for Codex'); - console.log('Usage:'); - console.log(' superpowers-codex bootstrap # Run complete bootstrap with all skills'); - console.log(' superpowers-codex use-skill # Load a specific skill'); - console.log(' superpowers-codex find-skills # List all available skills'); - console.log(''); - console.log('Examples:'); - console.log(' superpowers-codex bootstrap'); - console.log(' superpowers-codex use-skill superpowers:brainstorming'); - console.log(' superpowers-codex use-skill my-custom-skill'); - break; +async function main() { + // `lib/skills-core.mjs` is an ES module. This CLI is intentionally + // extensionless (for Unix shebang execution), so we keep it CommonJS and + // dynamically import the shared core for compatibility. + const skillsCoreUrl = pathToFileURL(path.join(__dirname, '..', 'lib', 'skills-core.mjs')).href; + skillsCore = await import(skillsCoreUrl); + + const command = process.argv[2]; + const arg = process.argv[3]; + + switch (command) { + case 'bootstrap': + runBootstrap(); + break; + case 'use-skill': + runUseSkill(arg); + break; + case 'find-skills': + runFindSkills(); + break; + default: + console.log('Superpowers for Codex'); + console.log('Usage:'); + console.log(' superpowers-codex bootstrap # Run complete bootstrap with all skills'); + console.log(' superpowers-codex use-skill # Load a specific skill'); + console.log(' superpowers-codex find-skills # List all available skills'); + console.log(''); + console.log('Examples:'); + console.log(' superpowers-codex bootstrap'); + console.log(' superpowers-codex use-skill superpowers:brainstorming'); + console.log(' superpowers-codex use-skill my-custom-skill'); + break; + } } + +main().catch((error) => { + console.error(error?.stack || String(error)); + process.exitCode = 1; +}); diff --git a/.codex/superpowers-codex.cmd b/.codex/superpowers-codex.cmd new file mode 100644 index 000000000..9a250dd48 --- /dev/null +++ b/.codex/superpowers-codex.cmd @@ -0,0 +1,12 @@ +@echo off +setlocal + +REM Windows shim for the extensionless Unix-style Node launcher (superpowers-codex). +REM This enables running: +REM C:\Users\dan\.codex\superpowers\.codex\superpowers-codex.cmd bootstrap +REM or (in many shells) just: +REM superpowers-codex bootstrap +REM when this directory is on PATH. + +node "%~dp0superpowers-codex" %* + diff --git a/.opencode/plugin/superpowers.js b/.opencode/plugin/superpowers.js index c9a6e29ea..58a60da6e 100644 --- a/.opencode/plugin/superpowers.js +++ b/.opencode/plugin/superpowers.js @@ -10,7 +10,7 @@ import fs from 'fs'; import os from 'os'; import { fileURLToPath } from 'url'; import { tool } from '@opencode-ai/plugin/tool'; -import * as skillsCore from '../../lib/skills-core.js'; +import * as skillsCore from '../../lib/skills-core.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index bdb053758..d83f666e0 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -34,13 +34,13 @@ - Auto re-injection on session.compacted events - Three-tier skill priority: project > personal > superpowers - Project-local skills support (`.opencode/skills/`) - - Shared core module (`lib/skills-core.js`) for code reuse with Codex + - Shared core module (`lib/skills-core.mjs`) for code reuse with Codex - Automated test suite with proper isolation (`tests/opencode/`) - Platform-specific documentation (`docs/README.opencode.md`, `docs/README.codex.md`) ### Changed -- **Refactored Codex Implementation**: Now uses shared `lib/skills-core.js` ES module +- **Refactored Codex Implementation**: Now uses shared `lib/skills-core.mjs` ES module - Eliminates code duplication between Codex and OpenCode - Single source of truth for skill discovery and parsing - Codex successfully loads ES modules via Node.js interop diff --git a/docs/README.codex.md b/docs/README.codex.md index e43004f42..2eed5b918 100644 --- a/docs/README.codex.md +++ b/docs/README.codex.md @@ -91,6 +91,10 @@ Personal skills override superpowers skills with the same name. **Location:** `~/.codex/superpowers/.codex/superpowers-codex` +On Windows, prefer one of these: +- `~/.codex/superpowers/.codex/superpowers-codex.cmd ` (recommended) +- `node ~/.codex/superpowers/.codex/superpowers-codex ` + A Node.js CLI script that provides three commands: - `bootstrap` - Load complete bootstrap with all skills - `use-skill ` - Load a specific skill @@ -98,7 +102,7 @@ A Node.js CLI script that provides three commands: ### Shared Core Module -**Location:** `~/.codex/superpowers/lib/skills-core.js` +**Location:** `~/.codex/superpowers/lib/skills-core.mjs` The Codex implementation uses the shared `skills-core` module (ES module format) for skill discovery and parsing. This is the same module used by the OpenCode plugin, ensuring consistent behavior across platforms. @@ -132,6 +136,12 @@ git pull chmod +x ~/.codex/superpowers/.codex/superpowers-codex ``` +On Windows, use the shim instead: + +```bash +~/.codex/superpowers/.codex/superpowers-codex.cmd find-skills +``` + ### Node.js errors The CLI script requires Node.js. Verify: diff --git a/docs/README.opencode.md b/docs/README.opencode.md index 122fe55ea..6d135175d 100644 --- a/docs/README.opencode.md +++ b/docs/README.opencode.md @@ -157,11 +157,11 @@ Skills written for Claude Code are automatically adapted for OpenCode. The plugi - Two custom tools: `use_skill`, `find_skills` - chat.message hook for initial context injection - event handler for session.compacted re-injection -- Uses shared `lib/skills-core.js` module (also used by Codex) +- Uses shared `lib/skills-core.mjs` module (also used by Codex) ### Shared Core Module -**Location:** `~/.config/opencode/superpowers/lib/skills-core.js` +**Location:** `~/.config/opencode/superpowers/lib/skills-core.mjs` **Functions:** - `extractFrontmatter()` - Parse skill metadata diff --git a/docs/plans/2025-11-22-opencode-support-design.md b/docs/plans/2025-11-22-opencode-support-design.md index 144f1cea2..ab791179d 100644 --- a/docs/plans/2025-11-22-opencode-support-design.md +++ b/docs/plans/2025-11-22-opencode-support-design.md @@ -29,7 +29,7 @@ OpenCode.ai is a coding agent similar to Claude Code and Codex. Previous attempt ### High-Level Structure -1. **Shared Core Module** (`lib/skills-core.js`) +1. **Shared Core Module** (`lib/skills-core.mjs`) - Common skill discovery and parsing logic - Used by both Codex and OpenCode implementations @@ -46,7 +46,7 @@ OpenCode.ai is a coding agent similar to Claude Code and Codex. Previous attempt Extract common functionality from `.codex/superpowers-codex` into shared module: ```javascript -// lib/skills-core.js +// lib/skills-core.mjs module.exports = { extractFrontmatter(filePath), // Parse name + description from YAML findSkillsInDir(dir, maxDepth), // Recursive SKILL.md discovery @@ -200,7 +200,7 @@ export const SuperpowersPlugin = async ({ client, directory, $ }) => { ``` superpowers/ ├── lib/ -│ └── skills-core.js # NEW: Shared skill logic +│ └── skills-core.mjs # NEW: Shared skill logic ├── .codex/ │ ├── superpowers-codex # UPDATED: Use skills-core │ ├── superpowers-bootstrap.md @@ -216,14 +216,14 @@ superpowers/ ### Phase 1: Refactor Shared Core -1. Create `lib/skills-core.js` +1. Create `lib/skills-core.mjs` - Extract frontmatter parsing from `.codex/superpowers-codex` - Extract skill discovery logic - Extract path resolution (with shadowing) - Update to use only `name` and `description` (no `when_to_use`) 2. Update `.codex/superpowers-codex` to use shared core - - Import from `../lib/skills-core.js` + - Import from `../lib/skills-core.mjs` - Remove duplicated code - Keep CLI wrapper logic @@ -235,7 +235,7 @@ superpowers/ ### Phase 2: Build OpenCode Plugin 1. Create `.opencode/plugin/superpowers.js` - - Import shared core from `../../lib/skills-core.js` + - Import shared core from `../../lib/skills-core.mjs` - Implement plugin function - Define custom tools (use_skill, find_skills) - Implement session.started hook diff --git a/docs/plans/2025-11-22-opencode-support-implementation.md b/docs/plans/2025-11-22-opencode-support-implementation.md index 1a7c1fb99..cc4671163 100644 --- a/docs/plans/2025-11-22-opencode-support-implementation.md +++ b/docs/plans/2025-11-22-opencode-support-implementation.md @@ -4,7 +4,7 @@ **Goal:** Add full superpowers support for OpenCode.ai with a native JavaScript plugin that shares core functionality with the existing Codex implementation. -**Architecture:** Extract common skill discovery/parsing logic into `lib/skills-core.js`, refactor Codex to use it, then build OpenCode plugin using their native plugin API with custom tools and session hooks. +**Architecture:** Extract common skill discovery/parsing logic into `lib/skills-core.mjs`, refactor Codex to use it, then build OpenCode plugin using their native plugin API with custom tools and session hooks. **Tech Stack:** Node.js, JavaScript, OpenCode Plugin API, Git worktrees @@ -15,10 +15,10 @@ ### Task 1: Extract Frontmatter Parsing **Files:** -- Create: `lib/skills-core.js` +- Create: `lib/skills-core.mjs` - Reference: `.codex/superpowers-codex` (lines 40-74) -**Step 1: Create lib/skills-core.js with extractFrontmatter function** +**Step 1: Create lib/skills-core.mjs with extractFrontmatter function** ```javascript #!/usr/bin/env node @@ -82,13 +82,13 @@ module.exports = { **Step 2: Verify file was created** -Run: `ls -l lib/skills-core.js` +Run: `ls -l lib/skills-core.mjs` Expected: File exists **Step 3: Commit** ```bash -git add lib/skills-core.js +git add lib/skills-core.mjs git commit -m "feat: create shared skills core module with frontmatter parser" ``` @@ -97,10 +97,10 @@ git commit -m "feat: create shared skills core module with frontmatter parser" ### Task 2: Extract Skill Discovery Logic **Files:** -- Modify: `lib/skills-core.js` +- Modify: `lib/skills-core.mjs` - Reference: `.codex/superpowers-codex` (lines 97-136) -**Step 1: Add findSkillsInDir function to skills-core.js** +**Step 1: Add findSkillsInDir function to skills-core.mjs** Add before `module.exports`: @@ -164,13 +164,13 @@ module.exports = { **Step 3: Verify syntax** -Run: `node -c lib/skills-core.js` +Run: `node -c lib/skills-core.mjs` Expected: No output (success) **Step 4: Commit** ```bash -git add lib/skills-core.js +git add lib/skills-core.mjs git commit -m "feat: add skill discovery function to core module" ``` @@ -179,7 +179,7 @@ git commit -m "feat: add skill discovery function to core module" ### Task 3: Extract Skill Resolution Logic **Files:** -- Modify: `lib/skills-core.js` +- Modify: `lib/skills-core.mjs` - Reference: `.codex/superpowers-codex` (lines 212-280) **Step 1: Add resolveSkillPath function** @@ -243,13 +243,13 @@ module.exports = { **Step 3: Verify syntax** -Run: `node -c lib/skills-core.js` +Run: `node -c lib/skills-core.mjs` Expected: No output **Step 4: Commit** ```bash -git add lib/skills-core.js +git add lib/skills-core.mjs git commit -m "feat: add skill path resolution with shadowing support" ``` @@ -258,7 +258,7 @@ git commit -m "feat: add skill path resolution with shadowing support" ### Task 4: Extract Update Check Logic **Files:** -- Modify: `lib/skills-core.js` +- Modify: `lib/skills-core.mjs` - Reference: `.codex/superpowers-codex` (lines 16-38) **Step 1: Add checkForUpdates function** @@ -316,13 +316,13 @@ module.exports = { **Step 3: Verify syntax** -Run: `node -c lib/skills-core.js` +Run: `node -c lib/skills-core.mjs` Expected: No output **Step 4: Commit** ```bash -git add lib/skills-core.js +git add lib/skills-core.mjs git commit -m "feat: add git update checking to core module" ``` @@ -950,12 +950,12 @@ At the top of the file (after the header), add: - **OpenCode Support**: Native JavaScript plugin for OpenCode.ai - Custom tools: `use_skill` and `find_skills` - Automatic session bootstrap with tool mapping instructions - - Shared core module (`lib/skills-core.js`) for code reuse + - Shared core module (`lib/skills-core.mjs`) for code reuse - Installation guide in `.opencode/INSTALL.md` ### Changed -- **Refactored Codex Implementation**: Now uses shared `lib/skills-core.js` module +- **Refactored Codex Implementation**: Now uses shared `lib/skills-core.mjs` module - Eliminates code duplication between Codex and OpenCode - Single source of truth for skill discovery and parsing @@ -1014,7 +1014,7 @@ No commit needed - this is verification only. Run: ```bash -ls -l lib/skills-core.js +ls -l lib/skills-core.mjs ls -l .opencode/plugin/superpowers.js ls -l .opencode/INSTALL.md ``` @@ -1057,7 +1057,7 @@ Expected: Shows all commits from this implementation Create a completion summary showing: - Total commits made -- Files created: `lib/skills-core.js`, `.opencode/plugin/superpowers.js`, `.opencode/INSTALL.md` +- Files created: `lib/skills-core.mjs`, `.opencode/plugin/superpowers.js`, `.opencode/INSTALL.md` - Files modified: `.codex/superpowers-codex`, `README.md`, `RELEASE-NOTES.md` - Testing performed: Codex commands verified - Ready for: Testing with actual OpenCode installation @@ -1085,7 +1085,7 @@ These steps require OpenCode to be installed and are not part of the automated i ## Success Criteria -- [ ] `lib/skills-core.js` created with all core functions +- [ ] `lib/skills-core.mjs` created with all core functions - [ ] `.codex/superpowers-codex` refactored to use shared core - [ ] Codex commands still work (find-skills, use-skill, bootstrap) - [ ] `.opencode/plugin/superpowers.js` created with tools and hooks diff --git a/lib/skills-core.js b/lib/skills-core.js index 5e5bb7012..dc53539f3 100644 --- a/lib/skills-core.js +++ b/lib/skills-core.js @@ -1,6 +1,14 @@ -import fs from 'fs'; -import path from 'path'; -import { execSync } from 'child_process'; +/** + * NOTE: This file is intentionally NOT the source of truth anymore. + * + * The shared core module is `lib/skills-core.mjs` so it can be imported as an + * ES module without requiring a repo-level `package.json` with `"type":"module"`. + * + * If you are importing `lib/skills-core.js`, switch to `lib/skills-core.mjs`. + */ +'use strict'; + +throw new Error('superpowers: lib/skills-core.js has moved to lib/skills-core.mjs. Import that file instead.'); /** * Extract YAML frontmatter from a skill file. diff --git a/lib/skills-core.mjs b/lib/skills-core.mjs new file mode 100644 index 000000000..6cb8ccb4c --- /dev/null +++ b/lib/skills-core.mjs @@ -0,0 +1,209 @@ +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 +}; + diff --git a/tests/opencode/run-tests.sh b/tests/opencode/run-tests.sh index 28538bb2d..01267a038 100755 --- a/tests/opencode/run-tests.sh +++ b/tests/opencode/run-tests.sh @@ -44,7 +44,7 @@ while [[ $# -gt 0 ]]; do echo "" echo "Tests:" echo " test-plugin-loading.sh Verify plugin installation and structure" - echo " test-skills-core.sh Test skills-core.js library functions" + echo " test-skills-core.sh Test skills-core library functions" echo " test-tools.sh Test use_skill and find_skills tools (integration)" echo " test-priority.sh Test skill priority resolution (integration)" exit 0 diff --git a/tests/opencode/test-plugin-loading.sh b/tests/opencode/test-plugin-loading.sh index 11ae02b72..d997ef92e 100755 --- a/tests/opencode/test-plugin-loading.sh +++ b/tests/opencode/test-plugin-loading.sh @@ -30,12 +30,12 @@ else exit 1 fi -# Test 2: Verify lib/skills-core.js is in place -echo "Test 2: Checking skills-core.js..." -if [ -f "$HOME/.config/opencode/superpowers/lib/skills-core.js" ]; then - echo " [PASS] skills-core.js exists" +# Test 2: Verify shared skills core module is in place +echo "Test 2: Checking skills-core.mjs..." +if [ -f "$HOME/.config/opencode/superpowers/lib/skills-core.mjs" ]; then + echo " [PASS] skills-core.mjs exists" else - echo " [FAIL] skills-core.js not found" + echo " [FAIL] skills-core.mjs not found" exit 1 fi diff --git a/tests/opencode/test-skills-core.sh b/tests/opencode/test-skills-core.sh index b058d5fd5..f4b9ca09f 100755 --- a/tests/opencode/test-skills-core.sh +++ b/tests/opencode/test-skills-core.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Test: Skills Core Library -# Tests the skills-core.js library functions directly via Node.js +# Tests the skills-core library functions directly via Node.js # Does not require OpenCode - tests pure library functionality set -euo pipefail