install-codex.mjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/env node
  2. // Minimal installer for Superpowers native Codex skill discovery.
  3. // Creates a symlink from ~/.agents/skills/superpowers → repo skills/
  4. // and updates ~/.codex/AGENTS.md with a gatekeeper block.
  5. import { existsSync, readFileSync, writeFileSync, mkdirSync, symlinkSync, lstatSync, readlinkSync, unlinkSync } from 'fs';
  6. import { join, resolve, dirname } from 'path';
  7. import { fileURLToPath } from 'url';
  8. import { homedir, platform } from 'os';
  9. import { execSync } from 'child_process';
  10. const home = homedir();
  11. const isWindows = platform() === 'win32';
  12. // Paths
  13. const __dirname = dirname(fileURLToPath(import.meta.url));
  14. const repoSkillsDir = resolve(__dirname, '..', 'skills');
  15. const agentsSkillsDir = join(home, '.agents', 'skills');
  16. const symlinkPath = join(agentsSkillsDir, 'superpowers');
  17. const agentsMdPath = join(home, '.codex', 'AGENTS.md');
  18. const GATEKEEPER_BEGIN = '<!-- superpowers:begin -->';
  19. const GATEKEEPER_END = '<!-- superpowers:end -->';
  20. const GATEKEEPER_BLOCK = `${GATEKEEPER_BEGIN}
  21. ## Superpowers
  22. **MANDATORY: Invoke $using-superpowers before proceeding with ANY task.**
  23. IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.
  24. ### Tool Mappings for Codex
  25. - \`TodoWrite\` → \`update_plan\`
  26. - \`Task\`/\`Subagent\` → \`spawn_agent\` + \`wait\` (or sequential if collab disabled)
  27. - \`Skill\` tool → native \`$skill-name\` mention
  28. - \`Read\`, \`Write\`, \`Edit\`, \`Bash\` → use your native equivalents
  29. ${GATEKEEPER_END}`;
  30. // --- Symlink creation ---
  31. function createSymlink() {
  32. // Ensure parent dir exists
  33. mkdirSync(agentsSkillsDir, { recursive: true });
  34. // Check if target already exists
  35. if (existsSync(symlinkPath)) {
  36. try {
  37. const stat = lstatSync(symlinkPath);
  38. if (stat.isSymbolicLink()) {
  39. const target = readlinkSync(symlinkPath);
  40. if (resolve(target) === resolve(repoSkillsDir)) {
  41. console.log(`✓ Symlink already exists: ${symlinkPath} → ${repoSkillsDir}`);
  42. return;
  43. }
  44. console.log(`! Existing symlink points to ${target}, updating to ${repoSkillsDir}`);
  45. unlinkSync(symlinkPath);
  46. } else {
  47. console.error(`✗ ${symlinkPath} already exists and is not a symlink.`);
  48. console.error(` Remove it manually and re-run the installer.`);
  49. process.exit(1);
  50. }
  51. } catch (err) {
  52. console.error(`✗ Cannot inspect ${symlinkPath}: ${err.message}`);
  53. process.exit(1);
  54. }
  55. }
  56. // Create the link (symlink on macOS/Linux, junction on Windows)
  57. const linkType = isWindows ? 'junction' : 'dir';
  58. const linkLabel = isWindows ? 'junction' : 'symlink';
  59. try {
  60. symlinkSync(repoSkillsDir, symlinkPath, linkType);
  61. console.log(`✓ Created ${linkLabel}: ${symlinkPath} → ${repoSkillsDir}`);
  62. } catch (err) {
  63. if (isWindows) {
  64. // Node API failed on Windows — try junction via cmd.exe
  65. try {
  66. execSync(`cmd /c mklink /J "${symlinkPath}" "${repoSkillsDir}"`, { stdio: 'pipe' });
  67. console.log(`✓ Created junction: ${symlinkPath} → ${repoSkillsDir}`);
  68. } catch (junctionErr) {
  69. console.error(`✗ Failed to create junction: ${junctionErr.message}`);
  70. console.error(` Try running PowerShell as administrator.`);
  71. process.exit(1);
  72. }
  73. } else {
  74. console.error(`✗ Failed to create symlink: ${err.message}`);
  75. process.exit(1);
  76. }
  77. }
  78. }
  79. // --- AGENTS.md update ---
  80. // Patterns that identify the old bootstrap block
  81. const OLD_BLOCK_PATTERNS = [
  82. 'superpowers-codex bootstrap',
  83. 'superpowers-codex use-skill',
  84. 'superpowers-codex find-skills',
  85. ];
  86. function removeOldBootstrapBlock(content) {
  87. const lines = content.split('\n');
  88. const result = [];
  89. let inOldBlock = false;
  90. for (let i = 0; i < lines.length; i++) {
  91. const line = lines[i];
  92. // Detect start of old block by its section header
  93. if (!inOldBlock && line.startsWith('## ') && line.includes('Superpowers')) {
  94. // Check if this section contains old bootstrap patterns (look ahead)
  95. const sectionEnd = lines.findIndex((l, j) => j > i && l.startsWith('## ') && !l.includes('Superpowers'));
  96. const sectionLines = lines.slice(i, sectionEnd === -1 ? lines.length : sectionEnd);
  97. const hasOldPatterns = sectionLines.some(l => OLD_BLOCK_PATTERNS.some(p => l.includes(p)));
  98. if (hasOldPatterns) {
  99. inOldBlock = true;
  100. // Also remove leading blank lines before the header
  101. while (result.length > 0 && result[result.length - 1].trim() === '') {
  102. result.pop();
  103. }
  104. continue;
  105. }
  106. }
  107. if (inOldBlock) {
  108. // End of old block: next non-Superpowers section header
  109. if (line.startsWith('## ') && !line.includes('Superpowers')) {
  110. inOldBlock = false;
  111. result.push(line);
  112. continue;
  113. }
  114. // Skip this line (part of old block)
  115. continue;
  116. }
  117. result.push(line);
  118. }
  119. return result.join('\n');
  120. }
  121. function updateAgentsMd() {
  122. let content = '';
  123. if (existsSync(agentsMdPath)) {
  124. content = readFileSync(agentsMdPath, 'utf8');
  125. // Check if gatekeeper already present
  126. if (content.includes(GATEKEEPER_BEGIN)) {
  127. // Replace existing gatekeeper block
  128. const beginIdx = content.indexOf(GATEKEEPER_BEGIN);
  129. const endIdx = content.indexOf(GATEKEEPER_END);
  130. if (endIdx > beginIdx) {
  131. content = content.slice(0, beginIdx) + GATEKEEPER_BLOCK + content.slice(endIdx + GATEKEEPER_END.length);
  132. writeFileSync(agentsMdPath, content, 'utf8');
  133. console.log(`✓ Updated existing gatekeeper block in ${agentsMdPath}`);
  134. return;
  135. }
  136. }
  137. // Remove old bootstrap block if present
  138. const hasOldBlock = OLD_BLOCK_PATTERNS.some(p => content.includes(p));
  139. if (hasOldBlock) {
  140. content = removeOldBootstrapBlock(content);
  141. console.log(` Removed old bootstrap block from AGENTS.md`);
  142. }
  143. } else {
  144. // Ensure directory exists
  145. mkdirSync(join(home, '.codex'), { recursive: true });
  146. }
  147. // Append gatekeeper block
  148. const separator = content.length > 0 && !content.endsWith('\n\n') ? (content.endsWith('\n') ? '\n' : '\n\n') : '';
  149. content = content + separator + GATEKEEPER_BLOCK + '\n';
  150. writeFileSync(agentsMdPath, content, 'utf8');
  151. console.log(`✓ Added gatekeeper block to ${agentsMdPath}`);
  152. }
  153. // --- Main ---
  154. console.log('Superpowers — Codex Native Skills Installer');
  155. console.log('');
  156. if (!existsSync(repoSkillsDir)) {
  157. console.error(`✗ Skills directory not found: ${repoSkillsDir}`);
  158. console.error(` Are you running this from the superpowers repo?`);
  159. process.exit(1);
  160. }
  161. createSymlink();
  162. updateAgentsMd();
  163. console.log('');
  164. console.log('Done! Restart Codex to discover superpowers skills natively.');
  165. console.log('');
  166. console.log('To update: cd ~/.codex/superpowers && git pull');
  167. console.log('To remove: rm ~/.agents/skills/superpowers');
  168. console.log(' Then remove the superpowers block from ~/.codex/AGENTS.md');