From 4065a0193f649dab04ff7ca722cc35d00b55e64c Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 8 Mar 2026 15:00:28 -0400 Subject: [PATCH 1/2] feat: Add OpenHands CLI support - Add --openhands flag for installing GSD as OpenHands skills - Add installOpenHandsSkills function to generate skills in ~/.agents/ - Support OPENHANDS_CONFIG_DIR env var and --config-dir flag - Skills are installed as gsd-/SKILL.md format Co-authored-by: openhands --- bin/install.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/bin/install.js b/bin/install.js index b7f11e4cf6..30e6318a74 100755 --- a/bin/install.js +++ b/bin/install.js @@ -41,6 +41,7 @@ const hasOpencode = args.includes('--opencode'); const hasClaude = args.includes('--claude'); const hasGemini = args.includes('--gemini'); const hasCodex = args.includes('--codex'); +const hasOpenHands = args.includes('--openhands'); const hasBoth = args.includes('--both'); // Legacy flag, keeps working const hasAll = args.includes('--all'); const hasUninstall = args.includes('--uninstall') || args.includes('-u'); @@ -48,7 +49,7 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u'); // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; if (hasAll) { - selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex']; + selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'openhands']; } else if (hasBoth) { selectedRuntimes = ['claude', 'opencode']; } else { @@ -56,6 +57,7 @@ if (hasAll) { if (hasClaude) selectedRuntimes.push('claude'); if (hasGemini) selectedRuntimes.push('gemini'); if (hasCodex) selectedRuntimes.push('codex'); + if (hasOpenHands) selectedRuntimes.push('openhands'); } /** @@ -78,6 +80,7 @@ function getDirName(runtime) { if (runtime === 'opencode') return '.opencode'; if (runtime === 'gemini') return '.gemini'; if (runtime === 'codex') return '.codex'; + if (runtime === 'openhands') return '.agents'; return '.claude'; } @@ -163,6 +166,17 @@ function getGlobalDir(runtime, explicitDir = null) { } return path.join(os.homedir(), '.codex'); } + + if (runtime === 'openhands') { + // OpenHands: --config-dir > OPENHANDS_CONFIG_DIR > ~/.agents + if (explicitDir) { + return expandTilde(explicitDir); + } + if (process.env.OPENHANDS_CONFIG_DIR) { + return expandTilde(process.env.OPENHANDS_CONFIG_DIR); + } + return path.join(os.homedir(), '.agents'); + } // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude if (explicitDir) { @@ -218,7 +232,7 @@ console.log(banner); // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Codex globally${reset}\n npx get-shit-done-cc --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME environment variables.\n`); + console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--openhands${reset} Install for OpenHands CLI only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir ${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for OpenHands CLI globally${reset}\n npx get-shit-done-cc --openhands --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Codex globally${reset}\n npx get-shit-done-cc --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME environment variables.\n`); process.exit(0); } @@ -739,6 +753,59 @@ function installCodexConfig(targetDir, agentsSrc) { return agents.length; } +/** + * Generate OpenHands skills from GSD agent definitions. + * OpenHands skills are directories with SKILL.md files containing YAML frontmatter. + * Structure: targetDir/gsd-/SKILL.md + */ +function installOpenHandsSkills(targetDir, agentsSrc) { + const skillsDir = targetDir; + fs.mkdirSync(skillsDir, { recursive: true }); + + const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md')); + const openHandsPathPrefix = `${targetDir.replace(/\\/g, '/')}/`; + let count = 0; + + for (const file of agentEntries) { + const content = fs.readFileSync(path.join(agentsSrc, file), 'utf8'); + const { frontmatter, body } = extractFrontmatterAndBody(content); + const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', ''); + const description = extractFrontmatterField(frontmatter, 'description') || ''; + + // Create skill directory + const skillDir = path.join(skillsDir, `gsd-${name}`); + fs.mkdirSync(skillDir, { recursive: true }); + + // Generate SKILL.md content for OpenHands + const skillContent = generateOpenHandsSkillMd(name, description, body, openHandsPathPrefix); + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + + count++; + } + + return count; +} + +/** + * Generate SKILL.md content for OpenHands + */ +function generateOpenHandsSkillMd(name, description, body, pathPrefix) { + // Replace .claude paths with OpenHands config path + const adjustedBody = body + .replace(/~\/\.claude\//g, pathPrefix) + .replace(/\$HOME\/\.claude\//g, pathPrefix.replace('$HOME', os.homedir())); + + return `--- +name: gsd-${name} +description: ${description || 'GSD agent for ' + name} +--- + +# ${name} + +${adjustedBody} +`; +} + /** * Strip HTML tags for Gemini CLI output * Terminals don't support subscript — Gemini renders these as raw HTML. @@ -1876,6 +1943,7 @@ function install(isGlobal, runtime = 'claude') { const isOpencode = runtime === 'opencode'; const isGemini = runtime === 'gemini'; const isCodex = runtime === 'codex'; + const isOpenHands = runtime === 'openhands'; const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); @@ -1899,6 +1967,7 @@ function install(isGlobal, runtime = 'claude') { if (isOpencode) runtimeLabel = 'OpenCode'; if (isGemini) runtimeLabel = 'Gemini'; if (isCodex) runtimeLabel = 'Codex'; + if (isOpenHands) runtimeLabel = 'OpenHands'; console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`); @@ -2117,6 +2186,13 @@ function install(isGlobal, runtime = 'claude') { return { settingsPath: null, settings: null, statuslineCommand: null, runtime }; } + if (isOpenHands) { + // Generate OpenHands skills + const skillCount = installOpenHandsSkills(targetDir, agentsSrc); + console.log(` ${green}✓${reset} Installed ${skillCount} GSD skills for OpenHands`); + return { settingsPath: null, settings: null, statuslineCommand: null, runtime }; + } + // Configure statusline and hooks in settings.json // Gemini uses AfterTool instead of PostToolUse for post-tool hooks const postToolEvent = runtime === 'gemini' ? 'AfterTool' : 'PostToolUse'; From cf850f76be4e0b4d945a0d83f23b2432b21e9400 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 8 Mar 2026 15:05:20 -0400 Subject: [PATCH 2/2] fix: Resolve skill name duplication and add OpenHands support - Fix gsd- prefix duplication in skill names (gsd-gsd-planner -> gsd-planner) - Add isOpenHands check in finishInstall for statusline and settings - Skip finalize for OpenHands-only installs - Add OpenHands program label for completion message --- bin/install.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/bin/install.js b/bin/install.js index 30e6318a74..8401454492 100755 --- a/bin/install.js +++ b/bin/install.js @@ -769,11 +769,11 @@ function installOpenHandsSkills(targetDir, agentsSrc) { for (const file of agentEntries) { const content = fs.readFileSync(path.join(agentsSrc, file), 'utf8'); const { frontmatter, body } = extractFrontmatterAndBody(content); - const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', ''); + const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', '').replace(/^gsd-/, ''); const description = extractFrontmatterField(frontmatter, 'description') || ''; - // Create skill directory - const skillDir = path.join(skillsDir, `gsd-${name}`); + // Create skill directory (use name directly, already has gsd- prefix) + const skillDir = path.join(skillsDir, name); fs.mkdirSync(skillDir, { recursive: true }); // Generate SKILL.md content for OpenHands @@ -796,7 +796,7 @@ function generateOpenHandsSkillMd(name, description, body, pathPrefix) { .replace(/\$HOME\/\.claude\//g, pathPrefix.replace('$HOME', os.homedir())); return `--- -name: gsd-${name} +name: ${name} description: ${description || 'GSD agent for ' + name} --- @@ -2275,8 +2275,9 @@ function install(isGlobal, runtime = 'claude') { function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true) { const isOpencode = runtime === 'opencode'; const isCodex = runtime === 'codex'; + const isOpenHands = runtime === 'openhands'; - if (shouldInstallStatusline && !isOpencode && !isCodex) { + if (shouldInstallStatusline && !isOpencode && !isCodex && !isOpenHands) { settings.statusLine = { type: 'command', command: statuslineCommand @@ -2285,7 +2286,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS } // Write settings when runtime supports settings.json - if (!isCodex) { + if (!isCodex && !isOpenHands) { writeSettings(settingsPath, settings); } @@ -2298,6 +2299,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS if (runtime === 'opencode') program = 'OpenCode'; if (runtime === 'gemini') program = 'Gemini'; if (runtime === 'codex') program = 'Codex'; + if (runtime === 'openhands') program = 'OpenHands'; let command = '/gsd:new-project'; if (runtime === 'opencode') command = '/gsd-new-project'; @@ -2465,6 +2467,9 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { const finalize = (shouldInstallStatusline) => { for (const result of results) { + // Skip finalize for OpenHands (no settings.json needed) + if (result.runtime === 'openhands') continue; + const useStatusline = statuslineRuntimes.includes(result.runtime) && shouldInstallStatusline; finishInstall( result.settingsPath, @@ -2479,8 +2484,16 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { if (primaryStatuslineResult) { handleStatusline(primaryStatuslineResult.settings, isInteractive, finalize); - } else { + } else if (results.length > 0 && results.some(r => r.runtime !== 'openhands')) { + // Has other runtimes besides OpenHands finalize(false); + } else { + // Only OpenHands - no statusline needed, just print done message + console.log(` + ${green}Done!${reset} GSD skills installed for OpenHands. + + ${cyan}Join the community:${reset} https://discord.gg/gsd +`); } }