/** * Shell integration for jack cd/new/clone to auto-change directories. * Shell function lives in ~/.config/jack/shell.sh, sourced from rc file. * jack update regenerates the managed file. */ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import pkg from "../../package.json"; type Shell = "bash" | "zsh" | "unknown"; // Markers for detection const LEGACY_MARKER = "# jack shell integration"; const SOURCE_LINE_MARKER = "# jack: shell integration (do not edit)"; export function getShellFilePath(): string { return join(homedir(), ".config", "jack", "shell.sh"); } export function getSourceLine(): string { const shellFile = getShellFilePath(); return `${SOURCE_LINE_MARKER}\n[ -f "${shellFile}" ] && source "${shellFile}"`; } function getShellFunction(): string { return `# jack shell integration v${pkg.version} # This file is managed by jack. Do not edit manually. # It will be regenerated by 'jack update'. jack() { case "$1" in cd) # Get project path and cd to it local dir dir="$(command jack cd "\${@:2}" 2>/dev/null)" if [[ -n "$dir" && -d "$dir" ]]; then cd "$dir" || return 1 else # Show error output if cd failed command jack cd "\${@:2}" fi ;; new|clone) # Run the command, capture stdout (project path) while showing stderr local dir dir="$(command jack "$@")" local exit_code=$? if [[ $exit_code -eq 0 && -n "$dir" && -d "$dir" ]]; then cd "$dir" || return $exit_code fi return $exit_code ;; "") # No args: interactive picker - cd if it outputs a directory local output output="$(command jack)" if [[ -n "$output" && -d "$output" ]]; then cd "$output" || return 1 elif [[ -n "$output" ]]; then # Not a directory (e.g., help text) - show it echo "$output" fi ;; *) # Pass through all other commands command jack "$@" ;; esac }`; } export function detectShell(): Shell { const shell = process.env.SHELL || ""; if (shell.endsWith("/bash") || shell.endsWith("/bash5")) return "bash"; if (shell.endsWith("/zsh")) return "zsh"; return "unknown"; } export function getRcFilePath(shell: Shell): string | null { const home = homedir(); switch (shell) { case "zsh": return join(home, ".zshrc"); case "bash": { // Prefer .bashrc, fall back to .bash_profile on macOS const bashrc = join(home, ".bashrc"); const profile = join(home, ".bash_profile"); if (existsSync(bashrc)) return bashrc; if (existsSync(profile)) return profile; // Default to .bashrc (will be created) return bashrc; } default: return null; } } export function isInstalled(rcFile: string): boolean { if (!existsSync(rcFile)) return false; try { const content = readFileSync(rcFile, "utf-8"); return content.includes(SOURCE_LINE_MARKER); } catch { return false; } } export function hasLegacyInstall(rcFile: string): boolean { if (!existsSync(rcFile)) return false; try { const content = readFileSync(rcFile, "utf-8"); // Has old marker but not new source line return content.includes(LEGACY_MARKER) && !content.includes(SOURCE_LINE_MARKER); } catch { return false; } } export function removeLegacyInstall(rcFile: string): void { if (!existsSync(rcFile)) return; const content = readFileSync(rcFile, "utf-8"); const legacyPattern = /\n?# jack shell integration\njack\(\) \{[\s\S]*?\n\}\n?/g; const newContent = content.replace(legacyPattern, "\n"); writeFileSync(rcFile, newContent, "utf-8"); } export function writeShellFile(): void { const shellFile = getShellFilePath(); const dir = dirname(shellFile); mkdirSync(dir, { recursive: true }); writeFileSync(shellFile, getShellFunction(), "utf-8"); } export function addSourceLine(rcFile: string): void { const sourceLine = getSourceLine(); appendFileSync(rcFile, `\n${sourceLine}\n`, "utf-8"); } export function install(rcFile: string): { migrated: boolean } { let migrated = false; if (hasLegacyInstall(rcFile)) { removeLegacyInstall(rcFile); migrated = true; } writeShellFile(); if (!isInstalled(rcFile)) { addSourceLine(rcFile); } return { migrated }; } export function update(): void { writeShellFile(); } export function getShellCode(): string { return getShellFunction(); } export function getShellName(shell: Shell): string { switch (shell) { case "bash": return "Bash"; case "zsh": return "Zsh"; default: return "your shell"; } } export function getRcFileName(rcFile: string): string { return rcFile.split("/").pop() || rcFile; } export function getShellFileDisplayPath(): string { return "~/.config/jack/shell.sh"; }