superpowers-codex 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const path = require('path');
  4. const os = require('os');
  5. const { execSync } = require('child_process');
  6. const skillsCore = require('../lib/skills-core');
  7. // Paths
  8. const homeDir = os.homedir();
  9. const superpowersSkillsDir = path.join(homeDir, '.codex', 'superpowers', 'skills');
  10. const personalSkillsDir = path.join(homeDir, '.codex', 'skills');
  11. const bootstrapFile = path.join(homeDir, '.codex', 'superpowers', '.codex', 'superpowers-bootstrap.md');
  12. const superpowersRepoDir = path.join(homeDir, '.codex', 'superpowers');
  13. // Utility functions
  14. function checkForUpdates() {
  15. try {
  16. // Quick check with 3 second timeout to avoid delays if network is down
  17. const output = execSync('git fetch origin && git status --porcelain=v1 --branch', {
  18. cwd: superpowersRepoDir,
  19. timeout: 3000,
  20. encoding: 'utf8',
  21. stdio: 'pipe'
  22. });
  23. // Parse git status output to see if we're behind
  24. const statusLines = output.split('\n');
  25. for (const line of statusLines) {
  26. if (line.startsWith('## ') && line.includes('[behind ')) {
  27. return true; // We're behind remote
  28. }
  29. }
  30. return false; // Up to date
  31. } catch (error) {
  32. // Network down, git error, timeout, etc. - don't block bootstrap
  33. return false;
  34. }
  35. }
  36. function extractFrontmatter(filePath) {
  37. try {
  38. const content = fs.readFileSync(filePath, 'utf8');
  39. const lines = content.split('\n');
  40. let inFrontmatter = false;
  41. let name = '';
  42. let description = '';
  43. let whenToUse = '';
  44. for (const line of lines) {
  45. if (line.trim() === '---') {
  46. if (inFrontmatter) break;
  47. inFrontmatter = true;
  48. continue;
  49. }
  50. if (inFrontmatter) {
  51. const match = line.match(/^(\w+):\s*(.*)$/);
  52. if (match) {
  53. const [, key, value] = match;
  54. switch (key) {
  55. case 'name': name = value.trim(); break;
  56. case 'description': description = value.trim(); break;
  57. case 'when_to_use': whenToUse = value.trim(); break;
  58. }
  59. }
  60. }
  61. }
  62. return { name, description, whenToUse };
  63. } catch (error) {
  64. return { name: '', description: '', whenToUse: '' };
  65. }
  66. }
  67. function printSkill(skillPath, sourceType) {
  68. const skillFile = path.join(skillPath, 'SKILL.md');
  69. const relPath = sourceType === 'personal'
  70. ? path.relative(personalSkillsDir, skillPath)
  71. : path.relative(superpowersSkillsDir, skillPath);
  72. // Print skill name with namespace
  73. if (sourceType === 'personal') {
  74. console.log(relPath.replace(/\\/g, '/')); // Personal skills are not namespaced
  75. } else {
  76. console.log(`superpowers:${relPath.replace(/\\/g, '/')}`); // Superpowers skills get superpowers namespace
  77. }
  78. // Extract and print metadata
  79. const { name, description, whenToUse } = extractFrontmatter(skillFile);
  80. if (description) console.log(` ${description}`);
  81. if (whenToUse) console.log(` When to use: ${whenToUse}`);
  82. console.log('');
  83. }
  84. function findSkillsInDir(dir, sourceType, maxDepth = 1) {
  85. const skills = [];
  86. if (!fs.existsSync(dir)) return skills;
  87. function searchDir(currentDir, currentDepth) {
  88. if (currentDepth > maxDepth) return;
  89. try {
  90. const entries = fs.readdirSync(currentDir, { withFileTypes: true });
  91. for (const entry of entries) {
  92. if (entry.isDirectory()) {
  93. const skillDir = path.join(currentDir, entry.name);
  94. const skillFile = path.join(skillDir, 'SKILL.md');
  95. if (fs.existsSync(skillFile)) {
  96. skills.push(skillDir);
  97. }
  98. // For personal skills, search deeper (category/skill structure)
  99. if (sourceType === 'personal' && currentDepth < maxDepth) {
  100. searchDir(skillDir, currentDepth + 1);
  101. }
  102. }
  103. }
  104. } catch (error) {
  105. // Ignore permission errors or other issues
  106. }
  107. }
  108. searchDir(dir, 0);
  109. return skills;
  110. }
  111. // Commands
  112. function runFindSkills() {
  113. console.log('Available skills:');
  114. console.log('==================');
  115. console.log('');
  116. const foundSkills = new Set();
  117. // Find personal skills first (these take precedence)
  118. const personalSkills = findSkillsInDir(personalSkillsDir, 'personal', 2);
  119. for (const skillPath of personalSkills) {
  120. const relPath = path.relative(personalSkillsDir, skillPath);
  121. foundSkills.add(relPath);
  122. printSkill(skillPath, 'personal');
  123. }
  124. // Find superpowers skills (only if not already found in personal)
  125. const superpowersSkills = findSkillsInDir(superpowersSkillsDir, 'superpowers', 1);
  126. for (const skillPath of superpowersSkills) {
  127. const relPath = path.relative(superpowersSkillsDir, skillPath);
  128. if (!foundSkills.has(relPath)) {
  129. printSkill(skillPath, 'superpowers');
  130. }
  131. }
  132. console.log('Usage:');
  133. console.log(' superpowers-codex use-skill <skill-name> # Load a specific skill');
  134. console.log('');
  135. console.log('Skill naming:');
  136. console.log(' Superpowers skills: superpowers:skill-name (from ~/.codex/superpowers/skills/)');
  137. console.log(' Personal skills: skill-name (from ~/.codex/skills/)');
  138. console.log(' Personal skills override superpowers skills when names match.');
  139. console.log('');
  140. console.log('Note: All skills are disclosed at session start via bootstrap.');
  141. }
  142. function runBootstrap() {
  143. console.log('# Superpowers Bootstrap for Codex');
  144. console.log('# ================================');
  145. console.log('');
  146. // Check for updates (with timeout protection)
  147. if (checkForUpdates()) {
  148. console.log('## Update Available');
  149. console.log('');
  150. console.log('⚠️ Your superpowers installation is behind the latest version.');
  151. console.log('To update, run: `cd ~/.codex/superpowers && git pull`');
  152. console.log('');
  153. console.log('---');
  154. console.log('');
  155. }
  156. // Show the bootstrap instructions
  157. if (fs.existsSync(bootstrapFile)) {
  158. console.log('## Bootstrap Instructions:');
  159. console.log('');
  160. try {
  161. const content = fs.readFileSync(bootstrapFile, 'utf8');
  162. console.log(content);
  163. } catch (error) {
  164. console.log(`Error reading bootstrap file: ${error.message}`);
  165. }
  166. console.log('');
  167. console.log('---');
  168. console.log('');
  169. }
  170. // Run find-skills to show available skills
  171. console.log('## Available Skills:');
  172. console.log('');
  173. runFindSkills();
  174. console.log('');
  175. console.log('---');
  176. console.log('');
  177. // Load the using-superpowers skill automatically
  178. console.log('## Auto-loading superpowers:using-superpowers skill:');
  179. console.log('');
  180. runUseSkill('superpowers:using-superpowers');
  181. console.log('');
  182. console.log('---');
  183. console.log('');
  184. console.log('# Bootstrap Complete!');
  185. console.log('# You now have access to all superpowers skills.');
  186. console.log('# Use "superpowers-codex use-skill <skill>" to load and apply skills.');
  187. console.log('# Remember: If a skill applies to your task, you MUST use it!');
  188. }
  189. function runUseSkill(skillName) {
  190. if (!skillName) {
  191. console.log('Usage: superpowers-codex use-skill <skill-name>');
  192. console.log('Examples:');
  193. console.log(' superpowers-codex use-skill superpowers:brainstorming # Load superpowers skill');
  194. console.log(' superpowers-codex use-skill brainstorming # Load personal skill (or superpowers if not found)');
  195. console.log(' superpowers-codex use-skill my-custom-skill # Load personal skill');
  196. return;
  197. }
  198. // Handle namespaced skill names
  199. let actualSkillPath;
  200. let forceSuperpowers = false;
  201. if (skillName.startsWith('superpowers:')) {
  202. // Remove the superpowers: namespace prefix
  203. actualSkillPath = skillName.substring('superpowers:'.length);
  204. forceSuperpowers = true;
  205. } else {
  206. actualSkillPath = skillName;
  207. }
  208. // Remove "skills/" prefix if present
  209. if (actualSkillPath.startsWith('skills/')) {
  210. actualSkillPath = actualSkillPath.substring('skills/'.length);
  211. }
  212. // Function to find skill file
  213. function findSkillFile(searchPath) {
  214. // Check for exact match with SKILL.md
  215. const skillMdPath = path.join(searchPath, 'SKILL.md');
  216. if (fs.existsSync(skillMdPath)) {
  217. return skillMdPath;
  218. }
  219. // Check for direct SKILL.md file
  220. if (searchPath.endsWith('SKILL.md') && fs.existsSync(searchPath)) {
  221. return searchPath;
  222. }
  223. return null;
  224. }
  225. let skillFile = null;
  226. // If superpowers: namespace was used, only check superpowers skills
  227. if (forceSuperpowers) {
  228. if (fs.existsSync(superpowersSkillsDir)) {
  229. const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
  230. skillFile = findSkillFile(superpowersPath);
  231. }
  232. } else {
  233. // First check personal skills directory (takes precedence)
  234. if (fs.existsSync(personalSkillsDir)) {
  235. const personalPath = path.join(personalSkillsDir, actualSkillPath);
  236. skillFile = findSkillFile(personalPath);
  237. if (skillFile) {
  238. console.log(`# Loading personal skill: ${actualSkillPath}`);
  239. console.log(`# Source: ${skillFile}`);
  240. console.log('');
  241. }
  242. }
  243. // If not found in personal, check superpowers skills
  244. if (!skillFile && fs.existsSync(superpowersSkillsDir)) {
  245. const superpowersPath = path.join(superpowersSkillsDir, actualSkillPath);
  246. skillFile = findSkillFile(superpowersPath);
  247. if (skillFile) {
  248. console.log(`# Loading superpowers skill: superpowers:${actualSkillPath}`);
  249. console.log(`# Source: ${skillFile}`);
  250. console.log('');
  251. }
  252. }
  253. }
  254. // If still not found, error
  255. if (!skillFile) {
  256. console.log(`Error: Skill not found: ${actualSkillPath}`);
  257. console.log('');
  258. console.log('Available skills:');
  259. runFindSkills();
  260. return;
  261. }
  262. // Extract frontmatter and content
  263. let content, frontmatter;
  264. try {
  265. const fullContent = fs.readFileSync(skillFile, 'utf8');
  266. const { name, description, whenToUse } = extractFrontmatter(skillFile);
  267. // Extract just the content after frontmatter
  268. const lines = fullContent.split('\n');
  269. let inFrontmatter = false;
  270. let frontmatterEnded = false;
  271. const contentLines = [];
  272. for (const line of lines) {
  273. if (line.trim() === '---') {
  274. if (inFrontmatter) {
  275. frontmatterEnded = true;
  276. continue;
  277. }
  278. inFrontmatter = true;
  279. continue;
  280. }
  281. if (frontmatterEnded || !inFrontmatter) {
  282. contentLines.push(line);
  283. }
  284. }
  285. content = contentLines.join('\n').trim();
  286. frontmatter = { name, description, whenToUse };
  287. } catch (error) {
  288. console.log(`Error reading skill file: ${error.message}`);
  289. return;
  290. }
  291. // Display skill header with clean info
  292. const displayName = forceSuperpowers ? `superpowers:${actualSkillPath}` :
  293. (skillFile.includes(personalSkillsDir) ? actualSkillPath : `superpowers:${actualSkillPath}`);
  294. const skillDirectory = path.dirname(skillFile);
  295. console.log(`# ${frontmatter.name || displayName}`);
  296. if (frontmatter.description) {
  297. console.log(`# ${frontmatter.description}`);
  298. }
  299. if (frontmatter.whenToUse) {
  300. console.log(`# When to use: ${frontmatter.whenToUse}`);
  301. }
  302. console.log(`# Supporting tools and docs are in ${skillDirectory}`);
  303. console.log('# ============================================');
  304. console.log('');
  305. // Display the skill content (without frontmatter)
  306. console.log(content);
  307. }
  308. // Main CLI
  309. const command = process.argv[2];
  310. const arg = process.argv[3];
  311. switch (command) {
  312. case 'bootstrap':
  313. runBootstrap();
  314. break;
  315. case 'use-skill':
  316. runUseSkill(arg);
  317. break;
  318. case 'find-skills':
  319. runFindSkills();
  320. break;
  321. default:
  322. console.log('Superpowers for Codex');
  323. console.log('Usage:');
  324. console.log(' superpowers-codex bootstrap # Run complete bootstrap with all skills');
  325. console.log(' superpowers-codex use-skill <skill-name> # Load a specific skill');
  326. console.log(' superpowers-codex find-skills # List all available skills');
  327. console.log('');
  328. console.log('Examples:');
  329. console.log(' superpowers-codex bootstrap');
  330. console.log(' superpowers-codex use-skill superpowers:brainstorming');
  331. console.log(' superpowers-codex use-skill my-custom-skill');
  332. break;
  333. }