postinstall.js
1 #!/usr/bin/env node 2 3 /** 4 * postinstall script — install shell completion files and print setup instructions. 5 * 6 * Detects the user's default shell and writes the completion script to the 7 * standard completion directory. For zsh and bash, the script prints manual 8 * instructions instead of modifying rc files (~/.zshrc, ~/.bashrc) — this 9 * avoids breaking multi-line shell commands and other fragile rc structures. 10 * Fish completions work automatically without rc changes. 11 * 12 * Supported shells: bash, zsh, fish. 13 * 14 * This script is intentionally plain Node.js (no TypeScript, no imports from 15 * the main source tree) so that it can run without a build step. 16 */ 17 18 import { mkdirSync, writeFileSync, existsSync } from 'node:fs'; 19 import { join } from 'node:path'; 20 import { homedir } from 'node:os'; 21 22 23 // ── Completion script content ────────────────────────────────────────────── 24 25 const BASH_COMPLETION = `# Bash completion for opencli (auto-installed) 26 _opencli_completions() { 27 local cur words cword 28 _get_comp_words_by_ref -n : cur words cword 29 30 local completions 31 completions=$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null) 32 33 COMPREPLY=( $(compgen -W "$completions" -- "$cur") ) 34 __ltrim_colon_completions "$cur" 35 } 36 complete -F _opencli_completions opencli 37 `; 38 39 const ZSH_COMPLETION = `#compdef opencli 40 # Zsh completion for opencli (auto-installed) 41 _opencli() { 42 local -a completions 43 local cword=$((CURRENT - 1)) 44 completions=(\${(f)"$(opencli --get-completions --cursor "$cword" "\${words[@]:1}" 2>/dev/null)"}) 45 compadd -a completions 46 } 47 _opencli 48 `; 49 50 const FISH_COMPLETION = `# Fish completion for opencli (auto-installed) 51 complete -c opencli -f -a '( 52 set -l tokens (commandline -cop) 53 set -l cursor (count (commandline -cop)) 54 opencli --get-completions --cursor $cursor $tokens[2..] 2>/dev/null 55 )' 56 `; 57 58 // ── Helpers ──────────────────────────────────────────────────────────────── 59 60 function detectShell() { 61 const shell = process.env.SHELL || ''; 62 if (shell.includes('zsh')) return 'zsh'; 63 if (shell.includes('bash')) return 'bash'; 64 if (shell.includes('fish')) return 'fish'; 65 return null; 66 } 67 68 function ensureDir(dir) { 69 if (!existsSync(dir)) { 70 mkdirSync(dir, { recursive: true }); 71 } 72 } 73 74 // ── Main ─────────────────────────────────────────────────────────────────── 75 76 function main() { 77 // Skip in CI environments 78 if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) { 79 return; 80 } 81 82 // Only install completion for global installs and npm link 83 const isGlobal = process.env.npm_config_global === 'true'; 84 if (!isGlobal) { 85 return; 86 } 87 88 const shell = detectShell(); 89 if (!shell) { 90 // Cannot determine shell; silently skip 91 return; 92 } 93 94 const home = homedir(); 95 96 try { 97 switch (shell) { 98 case 'zsh': { 99 const completionsDir = join(home, '.zsh', 'completions'); 100 const completionFile = join(completionsDir, '_opencli'); 101 ensureDir(completionsDir); 102 writeFileSync(completionFile, ZSH_COMPLETION, 'utf8'); 103 104 console.log(`✓ Zsh completion installed to ${completionFile}`); 105 console.log(''); 106 console.log(' \x1b[1mTo enable, add these lines to your ~/.zshrc:\x1b[0m'); 107 console.log(` fpath=(${completionsDir} $fpath)`); 108 console.log(' autoload -Uz compinit && compinit'); 109 console.log(''); 110 console.log(' If you already have compinit (oh-my-zsh, zinit, etc.), just add the fpath line \x1b[1mbefore\x1b[0m it.'); 111 console.log(' Then restart your shell or run: \x1b[36mexec zsh\x1b[0m'); 112 break; 113 } 114 case 'bash': { 115 const userCompDir = join(home, '.bash_completion.d'); 116 const completionFile = join(userCompDir, 'opencli'); 117 ensureDir(userCompDir); 118 writeFileSync(completionFile, BASH_COMPLETION, 'utf8'); 119 120 console.log(`✓ Bash completion installed to ${completionFile}`); 121 console.log(''); 122 console.log(' \x1b[1mTo enable, add this line to your ~/.bashrc:\x1b[0m'); 123 console.log(` [ -f "${completionFile}" ] && source "${completionFile}"`); 124 console.log(''); 125 console.log(' Then restart your shell or run: \x1b[36msource ~/.bashrc\x1b[0m'); 126 break; 127 } 128 case 'fish': { 129 const completionsDir = join(home, '.config', 'fish', 'completions'); 130 const completionFile = join(completionsDir, 'opencli.fish'); 131 ensureDir(completionsDir); 132 writeFileSync(completionFile, FISH_COMPLETION, 'utf8'); 133 134 console.log(`✓ Fish completion installed to ${completionFile}`); 135 console.log(` Restart your shell to activate.`); 136 break; 137 } 138 } 139 } catch (err) { 140 // Completion install is best-effort; never fail the package install 141 if (process.env.OPENCLI_VERBOSE) { 142 console.error(`Warning: Could not install shell completion: ${err.message}`); 143 } 144 } 145 146 // ── Spotify credentials template ──────────────────────────────────── 147 const opencliDir = join(home, '.opencli'); 148 const spotifyEnvFile = join(opencliDir, 'spotify.env'); 149 ensureDir(opencliDir); 150 if (!existsSync(spotifyEnvFile)) { 151 writeFileSync(spotifyEnvFile, 152 `# Spotify credentials — get them at https://developer.spotify.com/dashboard\n` + 153 `# Add http://127.0.0.1:8888/callback as a Redirect URI in your Spotify app\n` + 154 `SPOTIFY_CLIENT_ID=your_spotify_client_id_here\n` + 155 `SPOTIFY_CLIENT_SECRET=your_spotify_client_secret_here\n`, 156 'utf8' 157 ); 158 console.log(`✓ Spotify credentials template created at ${spotifyEnvFile}`); 159 console.log(` Edit the file and add your Client ID and Secret, then run: opencli spotify auth`); 160 } 161 162 // ── Browser Bridge setup hint ─────────────────────────────────────── 163 console.log(''); 164 console.log(' \x1b[1mNext step — Browser Bridge setup\x1b[0m'); 165 console.log(' Browser commands (bilibili, zhihu, twitter...) require the extension:'); 166 console.log(' 1. Download: https://github.com/jackwener/opencli/releases'); 167 console.log(' 2. In Chrome or Chromium, open chrome://extensions → enable Developer Mode → Load unpacked'); 168 console.log(''); 169 console.log(' Then run \x1b[36mopencli doctor\x1b[0m to verify.'); 170 console.log(''); 171 172 } 173 174 main();