superpowers-codex 13 KB

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