Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
52 changes: 48 additions & 4 deletions src/commands/completion.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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
;;
Expand Down Expand Up @@ -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)'
Expand Down Expand Up @@ -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')
Expand Down
72 changes: 72 additions & 0 deletions src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 <on|off>')
.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);
Expand Down
71 changes: 71 additions & 0 deletions src/commands/prompt.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 }
Expand Down Expand Up @@ -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')
Expand Down
7 changes: 7 additions & 0 deletions src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:',
Expand Down
7 changes: 7 additions & 0 deletions src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '디렉토리:',
Expand Down
7 changes: 7 additions & 0 deletions src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading