diff --git a/bin/install.js b/bin/install.js index 88799d7cd..07c42d706 100755 --- a/bin/install.js +++ b/bin/install.js @@ -21,22 +21,28 @@ const hasGlobal = args.includes('--global') || args.includes('-g'); const hasLocal = args.includes('--local') || args.includes('-l'); const hasOpencode = args.includes('--opencode'); const hasClaude = args.includes('--claude'); -const hasBoth = args.includes('--both'); +const hasGemini = args.includes('--gemini'); +const hasBoth = args.includes('--both'); // Legacy flag, keeps working +const hasAll = args.includes('--all'); const hasUninstall = args.includes('--uninstall') || args.includes('-u'); // Runtime selection - can be set by flags or interactive prompt let selectedRuntimes = []; -if (hasBoth) { +if (hasAll) { + selectedRuntimes = ['claude', 'opencode', 'gemini']; +} else if (hasBoth) { selectedRuntimes = ['claude', 'opencode']; -} else if (hasOpencode) { - selectedRuntimes = ['opencode']; -} else if (hasClaude) { - selectedRuntimes = ['claude']; +} else { + if (hasOpencode) selectedRuntimes.push('opencode'); + if (hasClaude) selectedRuntimes.push('claude'); + if (hasGemini) selectedRuntimes.push('gemini'); } // Helper to get directory name for a runtime (used for local/project installs) function getDirName(runtime) { - return runtime === 'opencode' ? '.opencode' : '.claude'; + if (runtime === 'opencode') return '.opencode'; + if (runtime === 'gemini') return '.gemini'; + return '.claude'; } /** @@ -66,7 +72,7 @@ function getOpencodeGlobalDir() { /** * Get the global config directory for a runtime - * @param {string} runtime - 'claude' or 'opencode' + * @param {string} runtime - 'claude', 'opencode', or 'gemini' * @param {string|null} explicitDir - Explicit directory from --config-dir flag */ function getGlobalDir(runtime, explicitDir = null) { @@ -78,6 +84,17 @@ function getGlobalDir(runtime, explicitDir = null) { return getOpencodeGlobalDir(); } + if (runtime === 'gemini') { + // Gemini: --config-dir > GEMINI_CONFIG_DIR > ~/.gemini + if (explicitDir) { + return expandTilde(explicitDir); + } + if (process.env.GEMINI_CONFIG_DIR) { + return expandTilde(process.env.GEMINI_CONFIG_DIR); + } + return path.join(os.homedir(), '.gemini'); + } + // Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude if (explicitDir) { return expandTilde(explicitDir); @@ -88,18 +105,17 @@ function getGlobalDir(runtime, explicitDir = null) { return path.join(os.homedir(), '.claude'); } -const banner = ` -${cyan} ██████╗ ███████╗██████╗ - ██╔════╝ ██╔════╝██╔══██╗ - ██║ ███╗███████╗██║ ██║ - ██║ ██║╚════██║██║ ██║ - ╚██████╔╝███████║██████╔╝ - ╚═════╝ ╚══════╝╚═════╝${reset} - - Get Shit Done ${dim}v${pkg.version}${reset} - A meta-prompting, context engineering and spec-driven - development system for Claude Code (and opencode) by TÂCHES. -`; +const banner = '\n' + + cyan + ' ██████╗ ███████╗██████╗\n' + + ' ██╔════╝ ██╔════╝██╔══██╗\n' + + ' ██║ ███╗███████╗██║ ██║\n' + + ' ██║ ██║╚════██║██║ ██║\n' + + ' ╚██████╔╝███████║██████╔╝\n' + + ' ╚═════╝ ╚══════╝╚═════╝' + reset + '\n' + + '\n' + + ' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' + + ' A meta-prompting, context engineering and spec-driven\n' + + ' development system for Claude Code, OpenCode, and Gemini by TÂCHES.\n'; // Parse --config-dir argument function parseConfigDirArg() { @@ -133,49 +149,7 @@ console.log(banner); // Show help if requested if (hasHelp) { - console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options] - - ${yellow}Options:${reset} - ${cyan}-g, --global${reset} Install globally (to config directory) - ${cyan}-l, --local${reset} Install locally (to current directory) - ${cyan}--claude${reset} Install for Claude Code only - ${cyan}--opencode${reset} Install for OpenCode only - ${cyan}--both${reset} Install for both Claude Code and OpenCode - ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files) - ${cyan}-c, --config-dir ${reset} Specify custom config directory - ${cyan}-h, --help${reset} Show this help message - ${cyan}--force-statusline${reset} Replace existing statusline config - - ${yellow}Examples:${reset} - ${dim}# Interactive install (prompts for runtime and location)${reset} - npx get-shit-done-cc - - ${dim}# Install for Claude Code globally${reset} - npx get-shit-done-cc --claude --global - - ${dim}# Install for OpenCode globally${reset} - npx get-shit-done-cc --opencode --global - - ${dim}# Install for both runtimes globally${reset} - npx get-shit-done-cc --both --global - - ${dim}# Install to custom config directory${reset} - npx get-shit-done-cc --claude --global --config-dir ~/.claude-bc - - ${dim}# Install to current project only${reset} - npx get-shit-done-cc --claude --local - - ${dim}# Uninstall GSD from Claude Code globally${reset} - npx get-shit-done-cc --claude --global --uninstall - - ${dim}# Uninstall GSD from current project${reset} - npx get-shit-done-cc --claude --local --uninstall - - ${yellow}Notes:${reset} - The --config-dir option is useful when you have multiple Claude Code - configurations (e.g., for different subscriptions). It takes priority - over the CLAUDE_CONFIG_DIR environment variable. -`); + 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}--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 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 --claude --global --config-dir ~/.claude-bc\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Claude Code globally${reset}\n npx get-shit-done-cc --claude --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 environment variables.\n`); process.exit(0); } @@ -193,9 +167,9 @@ function expandTilde(filePath) { * Build a hook command path using forward slashes for cross-platform compatibility. * On Windows, $HOME is not expanded by cmd.exe/PowerShell, so we use the actual path. */ -function buildHookCommand(claudeDir, hookName) { +function buildHookCommand(configDir, hookName) { // Use forward slashes for Node.js compatibility on all platforms - const hooksPath = claudeDir.replace(/\\/g, '/') + '/hooks/' + hookName; + const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName; return `node "${hooksPath}"`; } @@ -371,6 +345,47 @@ function convertClaudeToOpencodeFrontmatter(content) { return `---\n${newFrontmatter}\n---${body}`; } +/** + * Convert Claude Code markdown command to Gemini TOML format + * @param {string} content - Markdown file content with YAML frontmatter + * @returns {string} - TOML content + */ +function convertClaudeToGeminiToml(content) { + // Check if content has frontmatter + if (!content.startsWith('---')) { + return `prompt = ${JSON.stringify(content)}\n`; + } + + const endIndex = content.indexOf('---', 3); + if (endIndex === -1) { + return `prompt = ${JSON.stringify(content)}\n`; + } + + const frontmatter = content.substring(3, endIndex).trim(); + const body = content.substring(endIndex + 3).trim(); + + // Extract description from frontmatter + let description = ''; + const lines = frontmatter.split('\n'); + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith('description:')) { + description = trimmed.substring(12).trim(); + break; + } + } + + // Construct TOML + let toml = ''; + if (description) { + toml += `description = ${JSON.stringify(description)}\n`; + } + + toml += `prompt = ${JSON.stringify(body)}\n`; + + return toml; +} + /** * Copy commands to a flat structure for OpenCode * OpenCode expects: command/gsd-help.md (invoked as /gsd-help) @@ -410,20 +425,41 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) { } else if (entry.name.endsWith('.md')) { // Flatten: help.md -> gsd-help.md const baseName = entry.name.replace('.md', ''); - const destName = `${prefix}-${baseName}.md`; - const destPath = path.join(destDir, destName); - // Read, transform, and write - let content = fs.readFileSync(srcPath, 'utf8'); - // Replace path references - const claudeDirRegex = /~\/\.claude\//g; - const opencodeDirRegex = /~\/\.opencode\//g; - content = content.replace(claudeDirRegex, pathPrefix); - content = content.replace(opencodeDirRegex, pathPrefix); - // Convert frontmatter for opencode compatibility - content = convertClaudeToOpencodeFrontmatter(content); - - fs.writeFileSync(destPath, content); + if (runtime === 'gemini') { + // Flatten and convert to TOML for Gemini (if we used this function for Gemini, which we don't currently but for consistency) + // Actually Gemini uses nested structure via copyWithPathReplacement, so this block is only for OpenCode currently. + // Keeping as is for OpenCode. + const destName = `${prefix}-${baseName}.md`; + const destPath = path.join(destDir, destName); + + // Read, transform, and write + let content = fs.readFileSync(srcPath, 'utf8'); + // Replace path references + const claudeDirRegex = /~\/\.claude\//g; + const opencodeDirRegex = /~\/\.opencode\//g; + content = content.replace(claudeDirRegex, pathPrefix); + content = content.replace(opencodeDirRegex, pathPrefix); + // Convert frontmatter for opencode compatibility + content = convertClaudeToOpencodeFrontmatter(content); + + fs.writeFileSync(destPath, content); + } else { + const destName = `${prefix}-${baseName}.md`; + const destPath = path.join(destDir, destName); + + // Read, transform, and write + let content = fs.readFileSync(srcPath, 'utf8'); + // Replace path references + const claudeDirRegex = /~\/\.claude\//g; + const opencodeDirRegex = /~\/\.opencode\//g; + content = content.replace(claudeDirRegex, pathPrefix); + content = content.replace(opencodeDirRegex, pathPrefix); + // Convert frontmatter for opencode compatibility + content = convertClaudeToOpencodeFrontmatter(content); + + fs.writeFileSync(destPath, content); + } } } } @@ -434,7 +470,7 @@ function copyFlattenedCommands(srcDir, destDir, prefix, pathPrefix, runtime) { * @param {string} srcDir - Source directory * @param {string} destDir - Destination directory * @param {string} pathPrefix - Path prefix for file references - * @param {string} runtime - Target runtime ('claude' or 'opencode') + * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini') */ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) { const isOpencode = runtime === 'opencode'; @@ -455,15 +491,24 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) { if (entry.isDirectory()) { copyWithPathReplacement(srcPath, destPath, pathPrefix, runtime); } else if (entry.name.endsWith('.md')) { - // Replace ~/.claude/ with the appropriate prefix in Markdown files + // Always replace ~/.claude/ as it is the source of truth in the repo let content = fs.readFileSync(srcPath, 'utf8'); - const claudeDirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g'); + const claudeDirRegex = /~\/\.claude\//g; content = content.replace(claudeDirRegex, pathPrefix); + // Convert frontmatter for opencode compatibility if (isOpencode) { content = convertClaudeToOpencodeFrontmatter(content); + fs.writeFileSync(destPath, content); + } else if (runtime === 'gemini') { + // Convert to TOML for Gemini + const tomlContent = convertClaudeToGeminiToml(content); + // Replace extension with .toml + const tomlPath = destPath.replace(/\.md$/, '.toml'); + fs.writeFileSync(tomlPath, tomlContent); + } else { + fs.writeFileSync(destPath, content); } - fs.writeFileSync(destPath, content); } else { fs.copyFileSync(srcPath, destPath); } @@ -473,14 +518,14 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) { /** * Clean up orphaned files from previous GSD versions */ -function cleanupOrphanedFiles(claudeDir) { +function cleanupOrphanedFiles(configDir) { const orphanedFiles = [ 'hooks/gsd-notify.sh', // Removed in v1.6.x 'hooks/statusline.js', // Renamed to gsd-statusline.js in v1.9.0 ]; for (const relPath of orphanedFiles) { - const fullPath = path.join(claudeDir, relPath); + const fullPath = path.join(configDir, relPath); if (fs.existsSync(fullPath)) { fs.unlinkSync(fullPath); console.log(` ${green}✓${reset} Removed orphaned ${relPath}`); @@ -537,7 +582,7 @@ function cleanupOrphanedHooks(settings) { * Uninstall GSD from the specified directory for a specific runtime * Removes only GSD-specific files/directories, preserves user content * @param {boolean} isGlobal - Whether to uninstall from global or local - * @param {string} runtime - Target runtime ('claude' or 'opencode') + * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini') */ function uninstall(isGlobal, runtime = 'claude') { const isOpencode = runtime === 'opencode'; @@ -552,7 +597,10 @@ function uninstall(isGlobal, runtime = 'claude') { ? targetDir.replace(os.homedir(), '~') : targetDir.replace(process.cwd(), '.'); - const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code'; + let runtimeLabel = 'Claude Code'; + if (runtime === 'opencode') runtimeLabel = 'OpenCode'; + if (runtime === 'gemini') runtimeLabel = 'Gemini'; + console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`); // Check if target directory exists @@ -579,7 +627,7 @@ function uninstall(isGlobal, runtime = 'claude') { console.log(` ${green}✓${reset} Removed GSD commands from command/`); } } else { - // Claude Code: remove commands/gsd/ directory + // Claude Code & Gemini: remove commands/gsd/ directory const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd'); if (fs.existsSync(gsdCommandsDir)) { fs.rmSync(gsdCommandsDir, { recursive: true }); @@ -829,11 +877,12 @@ function verifyFileInstalled(filePath, description) { /** * Install to the specified directory for a specific runtime * @param {boolean} isGlobal - Whether to install globally or locally - * @param {string} runtime - Target runtime ('claude' or 'opencode') + * @param {string} runtime - Target runtime ('claude', 'opencode', 'gemini') */ function install(isGlobal, runtime = 'claude') { const isOpencode = runtime === 'opencode'; - const dirName = getDirName(runtime); // .opencode or .claude (for local installs) + const isGemini = runtime === 'gemini'; + const dirName = getDirName(runtime); const src = path.join(__dirname, '..'); // Get the target directory based on runtime and install type @@ -846,13 +895,16 @@ function install(isGlobal, runtime = 'claude') { : targetDir.replace(process.cwd(), '.'); // Path prefix for file references in markdown content - // For global installs: use full path (necessary when config dir is customized) - // For local installs: use relative ./.opencode/ or ./.claude/ + // For global installs: use full path + // For local installs: use relative const pathPrefix = isGlobal ? `${targetDir}/` : `./${dirName}/`; - const runtimeLabel = isOpencode ? 'OpenCode' : 'Claude Code'; + let runtimeLabel = 'Claude Code'; + if (isOpencode) runtimeLabel = 'OpenCode'; + if (isGemini) runtimeLabel = 'Gemini'; + console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`); // Track installation failures @@ -861,8 +913,8 @@ function install(isGlobal, runtime = 'claude') { // Clean up orphaned files from previous versions cleanupOrphanedFiles(targetDir); - // OpenCode uses 'command/' (singular) with flat structure: command/gsd-help.md - // Claude Code uses 'commands/' (plural) with nested structure: commands/gsd/help.md + // OpenCode uses 'command/' (singular) with flat structure + // Claude Code & Gemini use 'commands/' (plural) with nested structure if (isOpencode) { // OpenCode: flat structure in command/ directory const commandDir = path.join(targetDir, 'command'); @@ -878,7 +930,7 @@ function install(isGlobal, runtime = 'claude') { failures.push('command/gsd-*'); } } else { - // Claude Code: nested structure in commands/ directory + // Claude Code & Gemini: nested structure in commands/ directory const commandsDir = path.join(targetDir, 'commands'); fs.mkdirSync(commandsDir, { recursive: true }); @@ -902,8 +954,7 @@ function install(isGlobal, runtime = 'claude') { failures.push('get-shit-done'); } - // Copy agents to agents directory (subagents must be at root level) - // Only delete gsd-*.md files to preserve user's custom agents + // Copy agents to agents directory const agentsSrc = path.join(src, 'agents'); if (fs.existsSync(agentsSrc)) { const agentsDest = path.join(targetDir, 'agents'); @@ -918,12 +969,13 @@ function install(isGlobal, runtime = 'claude') { } } - // Copy new agents (don't use copyWithPathReplacement which would wipe the folder) + // Copy new agents const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true }); for (const entry of agentEntries) { if (entry.isFile() && entry.name.endsWith('.md')) { let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8'); - const dirRegex = new RegExp(`~/${dirName.replace('.', '\\.')}/`, 'g'); + // Always replace ~/.claude/ as it is the source of truth in the repo + const dirRegex = /~\/\.claude\//g; content = content.replace(dirRegex, pathPrefix); // Convert frontmatter for opencode compatibility if (isOpencode) { @@ -951,7 +1003,7 @@ function install(isGlobal, runtime = 'claude') { } } - // Write VERSION file for whats-new command + // Write VERSION file const versionDest = path.join(targetDir, 'get-shit-done', 'VERSION'); fs.writeFileSync(versionDest, pkg.version); if (verifyFileInstalled(versionDest, 'VERSION')) { @@ -968,7 +1020,6 @@ function install(isGlobal, runtime = 'claude') { const hookEntries = fs.readdirSync(hooksSrc); for (const entry of hookEntries) { const srcFile = path.join(hooksSrc, entry); - // Only copy files, not directories if (fs.statSync(srcFile).isFile()) { const destFile = path.join(hooksDest, entry); fs.copyFileSync(srcFile, destFile); @@ -981,14 +1032,13 @@ function install(isGlobal, runtime = 'claude') { } } - // If critical components failed, exit with error if (failures.length > 0) { console.error(`\n ${yellow}Installation incomplete!${reset} Failed: ${failures.join(', ')}`); - console.error(` Try running directly: node ~/.npm/_npx/*/node_modules/get-shit-done-cc/bin/install.js --global\n`); process.exit(1); } // Configure statusline and hooks in settings.json + // Gemini shares same hook system as Claude Code for now const settingsPath = path.join(targetDir, 'settings.json'); const settings = cleanupOrphanedHooks(readSettings(settingsPath)); const statuslineCommand = isGlobal @@ -998,7 +1048,7 @@ function install(isGlobal, runtime = 'claude') { ? buildHookCommand(targetDir, 'gsd-check-update.js') : 'node ' + dirName + '/hooks/gsd-check-update.js'; - // Configure SessionStart hook for update checking (skip for opencode - different hook system) + // Configure SessionStart hook for update checking (skip for opencode) if (!isOpencode) { if (!settings.hooks) { settings.hooks = {}; @@ -1007,7 +1057,6 @@ function install(isGlobal, runtime = 'claude') { settings.hooks.SessionStart = []; } - // Check if GSD update hook already exists const hasGsdUpdateHook = settings.hooks.SessionStart.some(entry => entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-check-update')) ); @@ -1030,11 +1079,6 @@ function install(isGlobal, runtime = 'claude') { /** * Apply statusline config, then print completion message - * @param {string} settingsPath - Path to settings.json - * @param {object} settings - Settings object - * @param {string} statuslineCommand - Statusline command - * @param {boolean} shouldInstallStatusline - Whether to install statusline - * @param {string} runtime - Target runtime ('claude' or 'opencode') */ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude') { const isOpencode = runtime === 'opencode'; @@ -1047,15 +1091,18 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS console.log(` ${green}✓${reset} Configured statusline`); } - // Always write settings (hooks were already configured in install()) + // Always write settings writeSettings(settingsPath, settings); - // Configure OpenCode permissions if needed + // Configure OpenCode permissions if (isOpencode) { configureOpencodePermissions(); } - const program = isOpencode ? 'OpenCode' : 'Claude Code'; + let program = 'Claude Code'; + if (runtime === 'opencode') program = 'OpenCode'; + if (runtime === 'gemini') program = 'Gemini'; + const command = isOpencode ? '/gsd-help' : '/gsd:help'; console.log(` ${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}. @@ -1070,19 +1117,16 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS function handleStatusline(settings, isInteractive, callback) { const hasExisting = settings.statusLine != null; - // No existing statusline - just install it if (!hasExisting) { callback(true); return; } - // Has existing and --force-statusline flag if (forceStatusline) { callback(true); return; } - // Has existing, non-interactive mode - skip if (!isInteractive) { console.log(` ${yellow}⚠${reset} Skipping statusline (already configured)`); console.log(` Use ${cyan}--force-statusline${reset} to replace\n`); @@ -1090,7 +1134,6 @@ function handleStatusline(settings, isInteractive, callback) { return; } - // Has existing, interactive mode - prompt user const existingCmd = settings.statusLine.command || settings.statusLine.url || '(custom)'; const rl = readline.createInterface({ @@ -1099,8 +1142,7 @@ function handleStatusline(settings, isInteractive, callback) { }); console.log(` - ${yellow}⚠${reset} Existing statusline detected - + ${yellow}⚠${reset} Existing statusline detected\n Your current statusline: ${dim}command: ${existingCmd}${reset} @@ -1121,8 +1163,7 @@ function handleStatusline(settings, isInteractive, callback) { } /** - * Prompt for runtime selection (Claude Code / OpenCode / Both) - * @param {function} callback - Called with array of selected runtimes + * Prompt for runtime selection */ function promptRuntime(callback) { const rl = readline.createInterface({ @@ -1140,19 +1181,20 @@ function promptRuntime(callback) { } }); - console.log(` ${yellow}Which runtime(s) would you like to install for?${reset} - - ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset} + console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset} ${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models - ${cyan}3${reset}) Both + ${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset} + ${cyan}4${reset}) All `); rl.question(` Choice ${dim}[1]${reset}: `, (answer) => { answered = true; rl.close(); const choice = answer.trim() || '1'; - if (choice === '3') { - callback(['claude', 'opencode']); + if (choice === '4') { + callback(['claude', 'opencode', 'gemini']); + } else if (choice === '3') { + callback(['gemini']); } else if (choice === '2') { callback(['opencode']); } else { @@ -1163,11 +1205,8 @@ function promptRuntime(callback) { /** * Prompt for install location - * @param {string[]} runtimes - Array of runtimes to install for */ function promptLocation(runtimes) { - // Check if stdin is a TTY - if not, fall back to global install - // This handles npx execution in environments like WSL2 where stdin may not be properly connected if (!process.stdin.isTTY) { console.log(` ${yellow}Non-interactive terminal detected, defaulting to global install${reset}\n`); installAllRuntimes(runtimes, true, false); @@ -1179,10 +1218,8 @@ function promptLocation(runtimes) { output: process.stdout }); - // Track whether we've processed the answer to prevent double-execution let answered = false; - // Handle readline close event (Ctrl+C, Escape, etc.) - cancel installation rl.on('close', () => { if (!answered) { answered = true; @@ -1191,18 +1228,14 @@ function promptLocation(runtimes) { } }); - // Show paths for selected runtimes const pathExamples = runtimes.map(r => { - // Use the proper global directory function for each runtime const globalPath = getGlobalDir(r, explicitConfigDir); return globalPath.replace(os.homedir(), '~'); }).join(', '); const localExamples = runtimes.map(r => `./${getDirName(r)}`).join(', '); - console.log(` ${yellow}Where would you like to install?${reset} - - ${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects + console.log(` ${yellow}Where would you like to install?${reset}\n\n ${cyan}1${reset}) Global ${dim}(${pathExamples})${reset} - available in all projects ${cyan}2${reset}) Local ${dim}(${localExamples})${reset} - this project only `); @@ -1217,9 +1250,6 @@ function promptLocation(runtimes) { /** * Install GSD for all selected runtimes - * @param {string[]} runtimes - Array of runtimes to install for - * @param {boolean} isGlobal - Whether to install globally - * @param {boolean} isInteractive - Whether running interactively */ function installAllRuntimes(runtimes, isGlobal, isInteractive) { const results = []; @@ -1229,14 +1259,25 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { results.push(result); } - // Handle statusline for Claude Code only (OpenCode uses themes) + // Handle statusline for Claude & Gemini (OpenCode uses themes) const claudeResult = results.find(r => r.runtime === 'claude'); + const geminiResult = results.find(r => r.runtime === 'gemini'); - if (claudeResult) { - handleStatusline(claudeResult.settings, isInteractive, (shouldInstallStatusline) => { - finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude'); - - // Finish OpenCode install if present + // Logic: if both are present, ask once if interactive? Or ask for each? + // Simpler: Ask once and apply to both if applicable. + + if (claudeResult || geminiResult) { + // Use whichever settings exist to check for existing statusline + const primaryResult = claudeResult || geminiResult; + + handleStatusline(primaryResult.settings, isInteractive, (shouldInstallStatusline) => { + if (claudeResult) { + finishInstall(claudeResult.settingsPath, claudeResult.settings, claudeResult.statuslineCommand, shouldInstallStatusline, 'claude'); + } + if (geminiResult) { + finishInstall(geminiResult.settingsPath, geminiResult.settings, geminiResult.statuslineCommand, shouldInstallStatusline, 'gemini'); + } + const opencodeResult = results.find(r => r.runtime === 'opencode'); if (opencodeResult) { finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode'); @@ -1249,7 +1290,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) { } } -// Main +// Main logic if (hasGlobal && hasLocal) { console.error(` ${yellow}Cannot specify both --global and --local${reset}`); process.exit(1); @@ -1257,10 +1298,8 @@ if (hasGlobal && hasLocal) { console.error(` ${yellow}Cannot use --config-dir with --local${reset}`); process.exit(1); } else if (hasUninstall) { - // Uninstall mode if (!hasGlobal && !hasLocal) { console.error(` ${yellow}--uninstall requires --global or --local${reset}`); - console.error(` Example: npx get-shit-done-cc --claude --global --uninstall`); process.exit(1); } const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude']; @@ -1268,19 +1307,16 @@ if (hasGlobal && hasLocal) { uninstall(hasGlobal, runtime); } } else if (selectedRuntimes.length > 0) { - // Non-interactive: runtime specified via flags if (!hasGlobal && !hasLocal) { - // Need location but runtime is specified - prompt for location only promptLocation(selectedRuntimes); } else { - // Both runtime and location specified via flags installAllRuntimes(selectedRuntimes, hasGlobal, false); } } else if (hasGlobal || hasLocal) { - // Location specified but no runtime - default to Claude Code + // Default to Claude if no runtime specified but location is installAllRuntimes(['claude'], hasGlobal, false); } else { - // Fully interactive: prompt for runtime, then location + // Interactive if (!process.stdin.isTTY) { console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`); installAllRuntimes(['claude'], true, false); diff --git a/package.json b/package.json index 5fe3909e1..6906f0859 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "get-shit-done-cc", "version": "1.9.13", - "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code by TÂCHES.", + "description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode and Gemini by TÂCHES.", "bin": { "get-shit-done-cc": "bin/install.js" }, @@ -19,7 +19,9 @@ "ai", "meta-prompting", "context-engineering", - "spec-driven-development" + "spec-driven-development", + "gemini", + "gemini-cli" ], "author": "TÂCHES", "license": "MIT",