diff --git a/package.json b/package.json index 93675fd..5f3373d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "git-env-manager", - "version": "1.2.12", + "version": "1.2.13", "description": "Multi-git-profile manager with SSH key integration", "type": "module", "bin": { diff --git a/src/commands/completion.ts b/src/commands/completion.ts index 3c77aaa..7c5b6f0 100644 --- a/src/commands/completion.ts +++ b/src/commands/completion.ts @@ -1,5 +1,5 @@ import type { Command } from 'commander'; -import { existsSync, readFileSync, appendFileSync } from 'node:fs'; +import { existsSync, readFileSync, appendFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; @@ -34,14 +34,14 @@ _ghem_completions() { return 0 ;; config) - COMPREPLY=($(compgen -W "set-lang set-prompt" -- "\${cur}")) + COMPREPLY=($(compgen -W "set-lang set-prompt set-completion" -- "\${cur}")) return 0 ;; set-lang) COMPREPLY=($(compgen -W "en ko" -- "\${cur}")) return 0 ;; - set-prompt) + set-prompt|set-completion) COMPREPLY=($(compgen -W "on off" -- "\${cur}")) return 0 ;; @@ -115,7 +115,7 @@ _ghem_completions() { _describe 'profile' profiles ;; config) - _describe 'subcommand' '(set-lang:Set\ display\ language set-prompt:Enable\ or\ disable\ prompt\ indicator)' + _describe 'subcommand' '(set-lang:Set\ display\ language set-prompt:Enable\ or\ disable\ prompt\ indicator set-completion:Install\ or\ remove\ shell\ completion)' ;; prompt) _arguments '--shell[Shell type]:shell:(bash zsh fish)' @@ -170,6 +170,50 @@ export function installCompletion(): InstallResult { } } +export type UninstallResult = + | { status: 'uninstalled'; shell: string; rcFile: string } + | { status: 'not_installed'; shell: string; rcFile: string } + | { status: 'failed'; shell: string; rcFile: string } + | { status: 'unsupported'; shell: string; rcFile: string }; + +export function uninstallCompletion(): UninstallResult { + const shell = detectShell(); + if (!shell) { + return { status: 'unsupported', shell: process.env.SHELL ?? 'unknown', rcFile: '' }; + } + const rcFile = shell === 'zsh' + ? join(homedir(), '.zshrc') + : join(homedir(), '.bashrc'); + + try { + if (!existsSync(rcFile)) { + return { status: 'not_installed', shell, rcFile }; + } + const content = readFileSync(rcFile, 'utf-8'); + if (!content.includes(COMPLETION_MARKER)) { + return { status: 'not_installed', shell, rcFile }; + } + const lines = content.split('\n'); + const filtered: string[] = []; + let skipNext = false; + for (const line of lines) { + if (skipNext) { + skipNext = false; + continue; + } + if (line.trim() === COMPLETION_MARKER) { + skipNext = true; + continue; + } + filtered.push(line); + } + writeFileSync(rcFile, filtered.join('\n'), 'utf-8'); + return { status: 'uninstalled', shell, rcFile }; + } catch { + return { status: 'failed', shell, rcFile }; + } +} + export function registerCompletionCommand(program: Command): void { program .command('completion') diff --git a/src/commands/config.ts b/src/commands/config.ts index 9ed2980..a617343 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,6 +1,8 @@ import type { Command } from 'commander'; import { readConfig, writeConfig, PersonaError } from '../core/config.js'; import { isValidLocale, setLocale, t } from '../i18n/index.js'; +import { installPromptIndicator } from './prompt.js'; +import { installCompletion, uninstallCompletion } from './completion.js'; import * as logger from '../utils/logger.js'; export function registerConfigCommand(program: Command): void { @@ -47,6 +49,76 @@ export function registerConfigCommand(program: Command): void { const updated = { ...config, promptIndicator: enabled }; writeConfig(updated); logger.success(t().promptUpdated(value)); + + if (enabled) { + const result = installPromptIndicator(); + switch (result.status) { + case 'installed': + logger.success(t().promptInstalled(result.rcFile)); + break; + case 'already_installed': + logger.info(t().promptAlreadyInstalled); + break; + case 'failed': + logger.error(t().promptFailed(result.rcFile)); + break; + case 'unsupported': + logger.error(t().promptUnsupported); + break; + } + } + } catch (err) { + if (err instanceof PersonaError) { + logger.error(err.message); + process.exit(1); + } + throw err; + } + }); + + configCmd + .command('set-completion ') + .description('Install or remove shell completion (on, off)') + .action((value: string) => { + try { + if (value !== 'on' && value !== 'off') { + logger.error(t().completionInvalid(value)); + process.exit(1); + } + + if (value === 'on') { + const result = installCompletion(); + switch (result.status) { + case 'installed': + logger.success(t().completionInstalled(result.rcFile)); + break; + case 'already_installed': + logger.info(t().completionAlreadyInstalled); + break; + case 'failed': + logger.error(t().completionFailed(result.rcFile)); + process.exit(1); + case 'unsupported': + logger.error(t().completionUnsupported); + process.exit(1); + } + } else { + const result = uninstallCompletion(); + switch (result.status) { + case 'uninstalled': + logger.success(t().completionUninstalled(result.rcFile)); + break; + case 'not_installed': + logger.info(t().completionNotInstalled); + break; + case 'failed': + logger.error(t().completionFailed(result.rcFile)); + process.exit(1); + case 'unsupported': + logger.error(t().completionUnsupported); + process.exit(1); + } + } } catch (err) { if (err instanceof PersonaError) { logger.error(err.message); diff --git a/src/commands/prompt.ts b/src/commands/prompt.ts index bc39907..16eef75 100644 --- a/src/commands/prompt.ts +++ b/src/commands/prompt.ts @@ -1,4 +1,7 @@ import type { Command } from 'commander'; +import { existsSync, readFileSync, appendFileSync, mkdirSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { homedir } from 'node:os'; import { PersonaError } from '../core/config.js'; import * as logger from '../utils/logger.js'; @@ -10,6 +13,18 @@ function detectShell(): string | null { return null; } +function rcFileFor(shell: string): string { + const home = homedir(); + switch (shell) { + case 'zsh': + return join(home, '.zshrc'); + case 'fish': + return join(home, '.config', 'fish', 'config.fish'); + default: + return join(home, '.bashrc'); + } +} + const AWK_SCRIPT = ` awk -v cwd="$__ghem_cwd" -v home="$HOME" ' /"promptIndicator"/ && /false/ { disabled = 1; exit } @@ -87,6 +102,62 @@ function generateScript(shell: string): string { } } +const PROMPT_MARKER = '# ghem prompt indicator'; + +function integrationLines(shell: string): string { + switch (shell) { + case 'zsh': + return `${PROMPT_MARKER} +eval "$(ghem prompt --shell zsh)" +setopt PROMPT_SUBST 2>/dev/null +if [[ "$RPROMPT" != *__ghem_prompt* ]]; then + RPROMPT='$(__ghem_prompt)'"$RPROMPT" +fi +`; + case 'fish': + return `${PROMPT_MARKER} +ghem prompt --shell fish | source +function fish_right_prompt + __ghem_prompt +end +`; + default: + return `${PROMPT_MARKER} +eval "$(ghem prompt --shell bash)" +if [[ "$PS1" != *__ghem_prompt* ]]; then + PS1='$(__ghem_prompt)'"$PS1" +fi +`; + } +} + +export type PromptInstallResult = + | { status: 'installed'; shell: string; rcFile: string } + | { status: 'already_installed'; shell: string; rcFile: string } + | { status: 'failed'; shell: string; rcFile: string } + | { status: 'unsupported'; shell: string; rcFile: string }; + +export function installPromptIndicator(): PromptInstallResult { + const shell = detectShell(); + if (!shell) { + return { status: 'unsupported', shell: process.env.SHELL ?? 'unknown', rcFile: '' }; + } + const rcFile = rcFileFor(shell); + try { + if (existsSync(rcFile)) { + const content = readFileSync(rcFile, 'utf-8'); + if (content.includes(PROMPT_MARKER)) { + return { status: 'already_installed', shell, rcFile }; + } + } + mkdirSync(dirname(rcFile), { recursive: true }); + appendFileSync(rcFile, `\n${integrationLines(shell)}`, 'utf-8'); + return { status: 'installed', shell, rcFile }; + } catch { + return { status: 'failed', shell, rcFile }; + } +} + export function registerPromptCommand(program: Command): void { program .command('prompt') diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 2b92986..e1571ad 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -59,12 +59,19 @@ export const en: Messages = { langInvalid: (locale) => `Invalid language '${locale}'. Supported: en, ko`, promptUpdated: (value) => `Prompt indicator set to '${value}'.`, promptInvalid: (value) => `Invalid value '${value}'. Use: on, off`, + promptInstalled: (rcFile) => `Prompt indicator installed in ${rcFile}. Restart your terminal to activate.`, + promptAlreadyInstalled: 'Prompt indicator is already installed in your shell config.', + promptFailed: (rcFile) => `Failed to install prompt indicator in ${rcFile}. Add it manually: eval "$(ghem prompt)"`, + promptUnsupported: 'Shell not recognized. Supported shells: bash, zsh, fish.', // Completion completionInstalled: (rcFile) => `Shell completion installed in ${rcFile}. Restart your terminal to activate.`, completionAlreadyInstalled: 'Shell completion is already installed.', completionFailed: (rcFile) => `Failed to install shell completion in ${rcFile}. You can add it manually: eval "$(ghem completion)"`, completionUnsupported: 'Shell not recognized. Run `ghem completion --shell bash` or `ghem completion --shell zsh` to get the completion script manually.', + completionInvalid: (value) => `Invalid value '${value}'. Use: on, off`, + completionUninstalled: (rcFile) => `Shell completion removed from ${rcFile}. Restart your terminal to apply.`, + completionNotInstalled: 'Shell completion is not installed.', // Status command statusDirectory: 'Directory:', diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 1aaaf3b..9d90312 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -59,12 +59,19 @@ export const ko: Messages = { langInvalid: (locale) => `유효하지 않은 언어입니다: '${locale}'. 지원: en, ko`, promptUpdated: (value) => `프롬프트 표시가 '${value}'(으)로 설정되었습니다.`, promptInvalid: (value) => `유효하지 않은 값입니다: '${value}'. 사용: on, off`, + promptInstalled: (rcFile) => `프롬프트 인디케이터가 ${rcFile}에 설치되었습니다. 터미널을 재시작하면 활성화됩니다.`, + promptAlreadyInstalled: '프롬프트 인디케이터가 이미 쉘 설정에 설치되어 있습니다.', + promptFailed: (rcFile) => `${rcFile}에 프롬프트 인디케이터 설치에 실패했습니다. 수동으로 추가하세요: eval "$(ghem prompt)"`, + promptUnsupported: '쉘을 인식할 수 없습니다. 지원 쉘: bash, zsh, fish.', // Completion completionInstalled: (rcFile) => `쉘 자동완성이 ${rcFile}에 설치되었습니다. 터미널을 재시작하면 활성화됩니다.`, completionAlreadyInstalled: '쉘 자동완성이 이미 설치되어 있습니다.', completionFailed: (rcFile) => `${rcFile}에 쉘 자동완성 설치에 실패했습니다. 수동으로 추가하세요: eval "$(ghem completion)"`, completionUnsupported: '쉘을 인식할 수 없습니다. `ghem completion --shell bash` 또는 `ghem completion --shell zsh`로 직접 스크립트를 확인하세요.', + completionInvalid: (value) => `유효하지 않은 값입니다: '${value}'. 사용: on, off`, + completionUninstalled: (rcFile) => `쉘 자동완성이 ${rcFile}에서 제거되었습니다. 터미널을 재시작하면 적용됩니다.`, + completionNotInstalled: '쉘 자동완성이 설치되어 있지 않습니다.', // Status command statusDirectory: '디렉토리:', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index 4af326c..57e26a0 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -61,12 +61,19 @@ export interface Messages { langInvalid: (locale: string) => string; promptUpdated: (value: string) => string; promptInvalid: (value: string) => string; + promptInstalled: (rcFile: string) => string; + promptAlreadyInstalled: string; + promptFailed: (rcFile: string) => string; + promptUnsupported: string; // Completion completionInstalled: (rcFile: string) => string; completionAlreadyInstalled: string; completionFailed: (rcFile: string) => string; completionUnsupported: string; + completionInvalid: (value: string) => string; + completionUninstalled: (rcFile: string) => string; + completionNotInstalled: string; // Status command statusDirectory: string; diff --git a/src/index.ts b/src/index.ts index 4f92a05..4dcb707 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,7 @@ try { // Keep default locale if config is unreadable } -const VERSION = '1.2.12'; +const VERSION = '1.2.13'; const program = new Command();