feat(codex): install GSD commands as prompts for '/' menu discoverability

Skills use '$gsd-*' syntax which isn't visible in the '/' command menu.
Adding parallel install to ~/.codex/prompts/gsd_*.md surfaces all GSD
commands as /prompts:gsd_* entries in the Codex UI slash command menu.

- Add installCodexPrompts() to install commands/gsd/*.md as prompts/gsd_*.md
- Add convertClaudeToCodexPrompt() to strip to description/argument-hint only
- Remove cleanupOrphanedFiles() code that was deleting prompts/gsd_*.md
- Both skills (30) and prompts (30) now install side by side

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson
2026-02-18 11:35:24 -06:00
parent 87c387348f
commit db1d003d86

View File

@@ -1038,6 +1038,102 @@ function uninstallCodexSkills(configDir) {
return count;
}
/**
* Install GSD commands as Codex prompts (legacy UI discoverability).
* Each commands/gsd/<name>.md becomes ~/.codex/prompts/gsd_<name>.md
* Invoked in Codex UI as /prompts:gsd_<name>
*
* Prompts are the deprecated predecessor to skills, but remain functional
* and appear in the '/' command menu — unlike skills which use '$' syntax.
*
* @param {string} srcDir - Source commands/gsd/ directory
* @param {string} configDir - Codex config dir (e.g. ~/.codex)
* @param {string} pathPrefix - Path prefix for file references
* @param {string} runtime - 'codex'
*/
function installCodexPrompts(srcDir, configDir, pathPrefix, runtime) {
if (!fs.existsSync(srcDir)) return 0;
const promptsDir = path.join(configDir, 'prompts');
// Remove existing gsd_*.md prompt files (clean install)
if (fs.existsSync(promptsDir)) {
for (const file of fs.readdirSync(promptsDir)) {
if (file.startsWith('gsd_') && file.endsWith('.md')) {
fs.unlinkSync(path.join(promptsDir, file));
}
}
}
fs.mkdirSync(promptsDir, { recursive: true });
let count = 0;
function processDir(dir, nameSegments) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
processDir(entryPath, nameSegments.concat(entry.name));
} else if (entry.name.endsWith('.md')) {
const baseName = entry.name.replace('.md', '');
const rawName = nameSegments.concat(baseName).join('_');
const promptName = `gsd_${rawName}`;
let content = fs.readFileSync(entryPath, 'utf8');
// Apply path replacements
content = content.replace(/~\/\.claude\//g, pathPrefix);
content = content.replace(/\.\/\.claude\//g, './.codex/');
content = processAttribution(content, getCommitAttribution(runtime));
content = convertClaudeToCodexPrompt(content);
fs.writeFileSync(path.join(promptsDir, `${promptName}.md`), content);
count++;
}
}
}
processDir(srcDir, []);
return count;
}
/**
* Convert a Claude Code GSD command to Codex prompts format.
* Codex prompts support: description, argument-hint in frontmatter.
* Everything else (name, allowed-tools, color) is stripped.
*/
function convertClaudeToCodexPrompt(content) {
if (!content.startsWith('---')) {
return content;
}
const endIndex = content.indexOf('---', 3);
if (endIndex === -1) {
return content;
}
const frontmatter = content.substring(3, endIndex).trim();
const body = content.substring(endIndex + 3);
let description = '';
let argumentHint = '';
for (const line of frontmatter.split('\n')) {
const trimmed = line.trim();
if (trimmed.startsWith('description:')) {
description = trimmed.substring(12).trim();
} else if (trimmed.startsWith('argument-hint:')) {
argumentHint = trimmed.substring(14).trim();
}
}
const fmLines = [];
if (description) fmLines.push(`description: ${description}`);
if (argumentHint) fmLines.push(`argument-hint: ${argumentHint}`);
const newFrontmatter = fmLines.length > 0 ? fmLines.join('\n') : '';
return newFrontmatter ? `---\n${newFrontmatter}\n---${body}` : body.trimStart();
}
/**
* Copy commands to a flat structure for OpenCode
* OpenCode expects: command/gsd-help.md (invoked as /gsd-help)
@@ -1176,21 +1272,6 @@ function cleanupOrphanedFiles(configDir) {
}
}
// Codex: remove old-format gsd_*.md prompts (pre-skills install format)
// These show up as /prompts:gsd_* in the Codex UI and conflict with the new skills system
const promptsDir = path.join(configDir, 'prompts');
if (fs.existsSync(promptsDir)) {
let removed = 0;
for (const file of fs.readdirSync(promptsDir)) {
if (file.startsWith('gsd_') && file.endsWith('.md')) {
fs.unlinkSync(path.join(promptsDir, file));
removed++;
}
}
if (removed > 0) {
console.log(` ${green}✓${reset} Removed ${removed} old GSD prompts (migrated to skills)`);
}
}
}
/**
@@ -1847,6 +1928,16 @@ function install(isGlobal, runtime = 'claude') {
} else {
failures.push('skills/gsd-*');
}
// Also install as prompts for '/' menu discoverability
// Skills use '$gsd-*' syntax; prompts appear in the '/' command menu as /prompts:gsd_*
const promptCount = installCodexPrompts(gsdSrc, targetDir, pathPrefix, runtime);
const promptsDir = path.join(targetDir, 'prompts');
if (verifyInstalled(promptsDir, 'prompts/gsd_*')) {
console.log(` ${green}✓${reset} Installed ${promptCount} prompts to prompts/`);
} else {
failures.push('prompts/gsd_*');
}
} else if (isOpencode) {
// OpenCode uses 'command/' (singular) with flat structure
const commandDir = path.join(targetDir, 'command');