#!/usr/bin/env bash declare -A aliases declare -A helptext err() { printf '%s\n' "$@" >&2; return 1; } die() { (( $# > 0 )) && err "$@" exit 1 } select_from() { local cmd='command -v' for a; do case "$a" in -c) cmd="$2"; shift 2 ;; esac done for c; do if $cmd "${c%% *}" &> /dev/null; then echo "$c" return 0 fi done return 1 } has() { local v c if [[ $1 = '-v' ]]; then v=1 shift fi for c; do c="${c%% *}" if ! command -v "$c" &> /dev/null; then (( v > 0 )) && err "$c not found" return 1 fi done } export -f has aliases[s]=search helptext['search']="search and install packages" subcmd_search() { local init if [[ -n $1 ]]; then init=$(npm --json search "$*" | pretty_npm_search); fi # SHELL is needed to use exported functions if default shell is not bash SHELL=$(which bash) fzf \ --inline-info \ --query="$*" \ --phony \ --multi \ --preview-window=hidden \ --header='enter to install, C-d saves as devDependency, C-v to pick versions' \ --bind='?:toggle-preview' \ --bind='change:reload:npm --json search {q} | pretty_npm_search' \ --bind='ctrl-v:execute:subcmd_ls-versions {1} <> /dev/tty' \ --bind="enter:execute:subcmd_install {+1} <> /dev/tty" \ --bind="ctrl-d:execute:subcmd_install -D {+1} <> /dev/tty" \ --preview="npm view {1}" \ <<< "$init" } aliases[lsv]='ls-versions' helptext['ls-versions']='list and install versions' subcmd_ls-versions() { local package="$1" fzf --tac --preview="npm view ${package}@{1}" \ --header="choose version for $package | C-d saves as devDependency" \ --bind="enter:execute:subcmd_install '${package}@{1}' <> /dev/tty" \ --bind="ctrl-d:execute:subcmd_install -D '${package}@{1}' <> /dev/tty" \ < <(npm --json view "$package" | jq -r '.versions[]') # label with .dist-tags?) } export -f subcmd_ls-versions aliases[un]=uninstall helptext[uninstall]='uninstall packages' subcmd_uninstall() { local rm mapfile -t rm < <( jq -r '{dependencies, devDependencies}[] | keys[]' package.json | fzf -m --query="$*") (( ${#rm} > 0 )) || return $(select_from 'yarn remove' 'npm uninstall') "${rm[@]}" } aliases[i]=install helptext[install]='install packages' subcmd_install() { if [[ -e yarn.lock ]] && has yarn; then yarn add "$@" else npm i -S "$@" fi } export -f subcmd_install helptext[init]='guided project setup' subcmd_init() { # shellcheck disable=2091 $(select_from 'git-flow init -d' 'git init') > /dev/null npm init -y > /dev/null [[ -e .gitignore ]] || curl -sL https://raw.githubusercontent.com/toptal/gitignore/master/templates/Node.gitignore > .gitignore # install typescript? eslint? prettier? husky + lint-staged? # react? vue? bundlers etc } helptext[lint]='setup or use eslint' subcmd_lint() { # TODO # should check if typescript, react, babel is installed, use relevant plugins if [[ $(jq '.devDependencies.eslint' package.json) = null ]]; then npx eslint --init fi npx eslint --ignore-path=.gitignore "$@" } aliases[fmt]=format helptext[format]='setup or use prettier' subcmd_format() { # TODO if [[ $(jq '.devDependencies.prettier' package.json) = null ]]; then subcmd_install -D prettier # eslint-{config,plugin}-prettier fi npx prettier --ignore-path .gitignore --write "$@" } aliases[-h]=help helptext[help]='show this help ' subcmd_help() { LESS=-FEXR less <<-HELP js [subcmd] [options] $(for c in "${subcmds_avail[@]}"; do printf " %s\n %s\n" "$c" "${helptext[$c]}" done) HELP } pretty_npm_search() { jq -r '.[] | "\(.name) \(.version)\t\(.description[0:80])\t\(.author.name // .publisher.username)\t\((.keywords // []) | join(" "))"' | column -t -s $'\t'; } export -f pretty_npm_search has -v fzf jq npm || die mapfile -t subcmds_avail < <(compgen -A function | awk '/^subcmd_/ { sub(/^subcmd_/, "", $0); print }') if (( $# < 1 )); then err 'missing command' subcmd_help exit 1 elif has "subcmd_$1"; then subcmd="subcmd_$1" shift "$subcmd" "$@" elif [[ -v aliases[$1] ]]; then subcmd=subcmd_${aliases[$1]} shift "$subcmd" "$@" else err 'unknown command' subcmd_help exit 1 fi