fpf-cli 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +49 -73
  2. package/fpf +209 -59
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,40 +1,10 @@
1
- # Fuzzy Package Finder (`fpf`)
1
+ # fpf-cli (`fpf`)
2
2
 
3
- `fpf` is a cross-platform fuzzy package finder powered by `fzf`.
3
+ Simple fuzzy package finder for people who live in the terminal.
4
4
 
5
- It auto-detects your OS and distro package manager, then lets you fuzzy-search packages, preview details, and install/remove/update with one interface.
5
+ Search packages with `fzf`, preview details, and install/remove/update from one place.
6
6
 
7
- ## Supported package managers
8
-
9
- - Linux distro managers: `apt`, `dnf`, `pacman`, `zypper`, `emerge`
10
- - Cross-platform app managers: `snap`, `flatpak`
11
- - Dev package managers: `npm`, `bun`
12
- - macOS: `brew`
13
-
14
- `winget` is intentionally not supported.
15
-
16
- ## Features
17
-
18
- - Auto-detects default manager from OS + distro
19
- - Supports manager override with one or two-letter flags
20
- - Unified fuzzy UI for search/install/list/remove
21
- - Multi-select installs/removals with safety confirmation
22
- - Shared keybinds and preview pane behavior across managers
23
-
24
- ## Requirements
25
-
26
- - `bash`
27
- - `fzf`
28
- - One or more supported package managers from the list above
29
-
30
- ## Usage
31
-
32
- ```bash
33
- fpf [manager option] [action option] [query]
34
- fpf -m|--manager <name> [action option] [query]
35
- ```
36
-
37
- Default action is `search + install` using auto-detected manager.
7
+ ![Screenshot](./fpf.png)
38
8
 
39
9
  ## Install
40
10
 
@@ -46,59 +16,65 @@ npm install -g fpf-cli
46
16
  bun add -g fpf-cli
47
17
  ```
48
18
 
49
- ### Action options
19
+ ## Quick Start
50
20
 
51
- - `-l`, `--list-installed` - fuzzy list installed packages and inspect details
52
- - `-R`, `--remove` - fuzzy select installed packages to remove
53
- - `-U`, `--update` - run manager update/upgrade flow
54
- - `-h`, `--help` - show help
21
+ ```bash
22
+ # Search + install (default action)
23
+ fpf ripgrep
55
24
 
56
- ### Manager options
25
+ # List installed packages
26
+ fpf -l
57
27
 
58
- - `-ap`, `--apt`
59
- - `-dn`, `--dnf`
60
- - `-pm`, `--pacman`
61
- - `-zy`, `--zypper`
62
- - `-em`, `--emerge`
63
- - `-br`, `--brew`
64
- - `-sn`, `--snap`
65
- - `-fp`, `--flatpak`
66
- - `-np`, `--npm`
67
- - `-bn`, `--bun`
68
- - `-ad`, `--auto` (force auto-detection)
69
- - `-m`, `--manager <name>` (explicit manager by name)
28
+ # Remove packages
29
+ fpf -R
70
30
 
71
- ### Examples
31
+ # Update packages
32
+ fpf -U
33
+ ```
72
34
 
73
- ```bash
74
- # Auto-detected manager search/install
75
- fpf ripgrep
35
+ By default, `fpf` auto-detects your package manager.
76
36
 
77
- # Force DNF
78
- fpf -dn nginx
37
+ On macOS, default auto mode uses both `brew` and `bun` together.
79
38
 
80
- # Use APT and list installed packages
81
- fpf -ap -l openssl
39
+ ## Supported Managers
82
40
 
83
- # Remove with Homebrew
84
- fpf -br -R wget
41
+ - Linux: `apt`, `dnf`, `pacman`, `zypper`, `emerge`
42
+ - Cross-platform: `snap`, `flatpak`
43
+ - Dev: `npm`, `bun`
44
+ - macOS: `brew`
85
45
 
86
- # Update with auto-detected manager
87
- fpf -U
46
+ (`winget` intentionally not included.)
88
47
 
89
- # Explicit manager name
90
- fpf --manager flatpak gimp
91
- ```
48
+ ## Manager Override Flags
49
+
50
+ - `-ap` apt
51
+ - `-dn` dnf
52
+ - `-pm` pacman
53
+ - `-zy` zypper
54
+ - `-em` emerge
55
+ - `-br` brew
56
+ - `-sn` snap
57
+ - `-fp` flatpak
58
+ - `-np` npm
59
+ - `-bn` bun
60
+ - `-m, --manager <name>` full manager name
61
+
62
+ ## Common Options
63
+
64
+ - `-l, --list-installed` list installed packages
65
+ - `-R, --remove` remove selected packages
66
+ - `-U, --update` run update/upgrade flow
67
+ - `-h, --help` show help
92
68
 
93
69
  ## Keybinds
94
70
 
95
- - `ctrl-h` show help in preview
96
- - `ctrl-k` show keybinds in preview
71
+ - `ctrl-h` help in preview
72
+ - `ctrl-k` keybinds in preview
97
73
  - `ctrl-/` toggle preview
98
- - `ctrl-n` jump to next selected item
99
- - `ctrl-b` jump to previous selected item
74
+ - `ctrl-n` next selected item
75
+ - `ctrl-b` previous selected item
100
76
 
101
77
  ## Notes
102
78
 
103
- - Root-required managers (`apt`, `dnf`, `pacman`, `zypper`, `emerge`, `snap`) run privileged operations via `sudo` when needed.
104
- - User-space managers (`brew`, `npm`, `bun`, and default user-mode `flatpak`) run without `sudo`.
79
+ - Requires: `bash` + `fzf`
80
+ - Root managers (`apt`, `dnf`, `pacman`, `zypper`, `emerge`, `snap`) use `sudo` when needed.
package/fpf CHANGED
@@ -202,8 +202,60 @@ detect_default_manager() {
202
202
  die "Unable to auto-detect a supported package manager. Use --manager."
203
203
  }
204
204
 
205
+ detect_default_managers() {
206
+ local os
207
+ os="$(uname -s)"
208
+
209
+ if [[ "${os}" == "Darwin" ]]; then
210
+ local found=0
211
+ if command_exists brew; then
212
+ printf "brew\n"
213
+ found=1
214
+ fi
215
+ if command_exists bun; then
216
+ printf "bun\n"
217
+ found=1
218
+ fi
219
+ if [[ "${found}" -eq 1 ]]; then
220
+ return
221
+ fi
222
+ fi
223
+
224
+ printf "%s\n" "$(detect_default_manager)"
225
+ }
226
+
227
+ join_manager_labels() {
228
+ local out=""
229
+ local mgr
230
+
231
+ for mgr in "$@"; do
232
+ [[ -n "${mgr}" ]] || continue
233
+ if [[ -n "${out}" ]]; then
234
+ out+=", "
235
+ fi
236
+ out+="$(manager_label "${mgr}")"
237
+ done
238
+
239
+ printf "%s" "${out}"
240
+ }
241
+
242
+ join_manager_labels_with_ids() {
243
+ local out=""
244
+ local mgr
245
+
246
+ for mgr in "$@"; do
247
+ [[ -n "${mgr}" ]] || continue
248
+ if [[ -n "${out}" ]]; then
249
+ out+=", "
250
+ fi
251
+ out+="$(manager_label "${mgr}") (${mgr})"
252
+ done
253
+
254
+ printf "%s" "${out}"
255
+ }
256
+
205
257
  build_help_file() {
206
- local default_manager="$1"
258
+ local default_managers="$1"
207
259
 
208
260
  cat >"${HELP_FILE}" <<EOF
209
261
  ${SCRIPT_NAME} - ultimate fuzzy package finder
@@ -215,8 +267,8 @@ Syntax:
215
267
  Default behavior:
216
268
  Fuzzy-search available packages and install selected items.
217
269
 
218
- Detected default manager:
219
- $(manager_label "${default_manager}") (${default_manager})
270
+ Detected default manager(s):
271
+ ${default_managers}
220
272
 
221
273
  Action options:
222
274
  -l, --list-installed Fuzzy-search installed packages and show details
@@ -349,7 +401,7 @@ join_query() {
349
401
  return
350
402
  fi
351
403
 
352
- for part in "${QUERY_PARTS[@]}"; do
404
+ for part in "${QUERY_PARTS[@]-}"; do
353
405
  if [[ -z "${query}" ]]; then
354
406
  query="${part}"
355
407
  else
@@ -475,8 +527,7 @@ manager_search_entries() {
475
527
  if bun search "${effective_query}" >/dev/null 2>&1; then
476
528
  bun search "${effective_query}" 2>/dev/null |
477
529
  awk 'NR > 1 && NF > 0 { name=$1; $1=""; sub(/^[[:space:]]+/, "", $0); if ($0 == "") $0="-"; print name "\t" $0 }'
478
- fi
479
- if command_exists npm; then
530
+ elif command_exists npm; then
480
531
  npm search "${effective_query}" --searchlimit=500 --parseable 2>/dev/null |
481
532
  awk -F'\t' 'NF >= 2 { print $1 "\t" $2 }'
482
533
  fi
@@ -548,8 +599,7 @@ manager_installed_entries() {
548
599
  elif bun pm ls >/dev/null 2>&1; then
549
600
  bun pm ls 2>/dev/null |
550
601
  awk 'NR > 1 && NF > 0 { print $1 "\tglobal" }'
551
- fi
552
- if command_exists npm; then
602
+ elif command_exists npm; then
553
603
  npm ls -g --depth=0 --parseable 2>/dev/null |
554
604
  awk -F'/' 'NR > 1 { print $NF "\tglobal" }'
555
605
  fi
@@ -807,17 +857,16 @@ confirm_action() {
807
857
  }
808
858
 
809
859
  run_fuzzy_selector() {
810
- local manager="$1"
811
- local query="$2"
812
- local input_file="$3"
813
- local header_line="$4"
860
+ local query="$1"
861
+ local input_file="$2"
862
+ local header_line="$3"
814
863
  local preview_cmd
815
864
 
816
- preview_cmd="$(manager_preview_command "${manager}")"
865
+ preview_cmd='bash -c '\''mgr="$1"; pkg="$2"; case "$mgr" in apt) apt-cache show "$pkg" 2>/dev/null; printf "\n"; dpkg -L "$pkg" 2>/dev/null ;; dnf) dnf info "$pkg" 2>/dev/null; printf "\n"; rpm -ql "$pkg" 2>/dev/null ;; pacman) pacman -Si "$pkg" 2>/dev/null; printf "\n"; pacman -Fl "$pkg" 2>/dev/null | awk "{print \$2}" ;; zypper) zypper --non-interactive info "$pkg" 2>/dev/null ;; emerge) emerge --search --color=n "$pkg" 2>/dev/null ;; brew) brew info "$pkg" 2>/dev/null ;; snap) snap info "$pkg" 2>/dev/null ;; flatpak) flatpak info "$pkg" 2>/dev/null || flatpak remote-info flathub "$pkg" 2>/dev/null ;; npm) npm view "$pkg" 2>/dev/null ;; bun) bun info "$pkg" 2>/dev/null || npm view "$pkg" 2>/dev/null ;; esac'\'' _ {1} {2}'
817
866
 
818
867
  fzf -q "${query}" -e -m \
819
868
  --delimiter=$'\t' \
820
- --with-nth=1,2 \
869
+ --with-nth=1,2,3 \
821
870
  --preview="${preview_cmd}" \
822
871
  --preview-window=55%:wrap:border-sharp \
823
872
  --layout=reverse \
@@ -830,24 +879,27 @@ run_fuzzy_selector() {
830
879
  --bind=ctrl-h:preview:"cat ${HELP_FILE}" \
831
880
  --bind='ctrl-/:change-preview-window(hidden|)' \
832
881
  --bind=ctrl-n:next-selected,ctrl-b:prev-selected \
833
- --bind='focus:transform-preview-label:echo package: {1}' \
882
+ --bind='focus:transform-preview-label:echo {1}: {2}' \
834
883
  <"${input_file}"
835
884
  }
836
885
 
837
- collect_selected_packages() {
838
- local selected_lines="$1"
839
- printf "%s\n" "${selected_lines}" | awk -F'\t' 'NF > 0 { print $1 }'
840
- }
841
-
842
886
  main() {
843
887
  ensure_tmp_root
844
888
 
845
889
  parse_args "$@"
846
890
 
847
891
  local detected_manager
848
- detected_manager="$(detect_default_manager)"
892
+ local detected_managers=()
893
+ while IFS= read -r detected_manager; do
894
+ [[ -n "${detected_manager}" ]] || continue
895
+ detected_managers+=("${detected_manager}")
896
+ done < <(detect_default_managers)
897
+
898
+ local default_managers_display
899
+ default_managers_display="$(join_manager_labels_with_ids "${detected_managers[@]-}")"
900
+ [[ -n "${default_managers_display}" ]] || default_managers_display="None"
849
901
 
850
- build_help_file "${detected_manager}"
902
+ build_help_file "${default_managers_display}"
851
903
  build_keybind_file
852
904
 
853
905
  if [[ "${ACTION}" == "help" ]]; then
@@ -855,21 +907,39 @@ main() {
855
907
  exit 0
856
908
  fi
857
909
 
858
- local manager="${detected_manager}"
910
+ local managers=()
859
911
  if [[ -n "${MANAGER_OVERRIDE}" ]]; then
860
- manager="$(normalize_manager "${MANAGER_OVERRIDE}")"
912
+ managers+=("$(normalize_manager "${MANAGER_OVERRIDE}")")
913
+ else
914
+ for detected_manager in "${detected_managers[@]-}"; do
915
+ [[ -n "${detected_manager}" ]] || continue
916
+ managers+=("${detected_manager}")
917
+ done
918
+ fi
919
+
920
+ if [[ "${#managers[@]}" -eq 0 ]]; then
921
+ die "Unable to auto-detect supported package managers. Use --manager."
861
922
  fi
862
923
 
863
- manager_supported "${manager}" || die "Unsupported manager: ${manager}"
864
- manager_command_ready "${manager}" || die "Manager command(s) for '${manager}' not found on this system"
924
+ local manager
925
+ for manager in "${managers[@]-}"; do
926
+ manager_supported "${manager}" || die "Unsupported manager: ${manager}"
927
+ manager_command_ready "${manager}" || die "Manager command(s) for '${manager}' not found on this system"
928
+ done
929
+
930
+ local manager_display
931
+ manager_display="$(join_manager_labels "${managers[@]-}")"
865
932
 
866
933
  local query
867
934
  query="$(join_query)"
868
935
 
869
936
  if [[ "${ACTION}" == "update" ]]; then
870
- log "Using manager: $(manager_label "${manager}")"
871
- if confirm_action "Run update/upgrade for ${manager}?"; then
872
- manager_update "${manager}"
937
+ log "Using manager(s): ${manager_display}"
938
+ if confirm_action "Run update/upgrade for ${manager_display}?"; then
939
+ for manager in "${managers[@]-}"; do
940
+ log "Updating with $(manager_label "${manager}")"
941
+ manager_update "${manager}"
942
+ done
873
943
  else
874
944
  log "Update canceled"
875
945
  fi
@@ -878,34 +948,64 @@ main() {
878
948
 
879
949
  command_exists fzf || die "fzf is required"
880
950
 
881
- local source_file
882
951
  local display_file
883
- source_file="$(mktemp "${TMP_ROOT}/source.XXXXXX")"
884
952
  display_file="$(mktemp "${TMP_ROOT}/display.XXXXXX")"
953
+ : >"${display_file}"
885
954
 
886
- if [[ "${ACTION}" == "list" || "${ACTION}" == "remove" ]]; then
887
- manager_installed_entries "${manager}" >"${source_file}"
888
- cp "${source_file}" "${display_file}"
889
- else
890
- manager_search_entries "${manager}" "${query}" >"${source_file}"
891
- mark_installed_packages "${manager}" "${source_file}" "${display_file}"
955
+ local source_file
956
+ local marked_file
957
+ for manager in "${managers[@]-}"; do
958
+ source_file="$(mktemp "${TMP_ROOT}/source.XXXXXX")"
959
+
960
+ if [[ "${ACTION}" == "list" || "${ACTION}" == "remove" ]]; then
961
+ manager_installed_entries "${manager}" >"${source_file}" || true
962
+ if [[ -s "${source_file}" ]]; then
963
+ awk -F'\t' -v mgr="${manager}" '
964
+ NF >= 1 {
965
+ desc = $2
966
+ if (desc == "") desc = "-"
967
+ print mgr "\t" $1 "\t" desc
968
+ }
969
+ ' "${source_file}" >>"${display_file}"
970
+ fi
971
+ rm -f "${source_file}"
972
+ continue
973
+ fi
974
+
975
+ marked_file="$(mktemp "${TMP_ROOT}/marked.XXXXXX")"
976
+ manager_search_entries "${manager}" "${query}" >"${source_file}" || true
977
+ if [[ -s "${source_file}" ]]; then
978
+ mark_installed_packages "${manager}" "${source_file}" "${marked_file}"
979
+ awk -F'\t' -v mgr="${manager}" '
980
+ NF >= 1 {
981
+ desc = $2
982
+ if (desc == "") desc = "-"
983
+ print mgr "\t" $1 "\t" desc
984
+ }
985
+ ' "${marked_file}" >>"${display_file}"
986
+ fi
987
+ rm -f "${source_file}" "${marked_file}"
988
+ done
989
+
990
+ if [[ -s "${display_file}" ]]; then
991
+ sort -u "${display_file}" -o "${display_file}"
892
992
  fi
893
993
 
894
994
  if [[ ! -s "${display_file}" ]]; then
895
- rm -f "${source_file}" "${display_file}"
896
- die "No packages found for manager '${manager}' and query '${query}'"
995
+ rm -f "${display_file}"
996
+ die "No packages found for manager(s) '${manager_display}' and query '${query}'"
897
997
  fi
898
998
 
899
999
  local header
900
1000
  case "${ACTION}" in
901
1001
  search)
902
- header="Select package(s) to install with ${manager} (TAB to multi-select)"
1002
+ header="Select package(s) to install with ${manager_display} (TAB to multi-select)"
903
1003
  ;;
904
1004
  list)
905
- header="Select installed package(s) to inspect"
1005
+ header="Select installed package(s) to inspect from ${manager_display}"
906
1006
  ;;
907
1007
  remove)
908
- header="Select installed package(s) to remove"
1008
+ header="Select installed package(s) to remove from ${manager_display}"
909
1009
  ;;
910
1010
  *)
911
1011
  header="Select package(s)"
@@ -913,47 +1013,97 @@ main() {
913
1013
  esac
914
1014
 
915
1015
  local selected
916
- selected="$(run_fuzzy_selector "${manager}" "${query}" "${display_file}" "${header}" || true)"
1016
+ selected="$(run_fuzzy_selector "${query}" "${display_file}" "${header}" || true)"
917
1017
 
918
- rm -f "${source_file}" "${display_file}"
1018
+ rm -f "${display_file}"
919
1019
 
920
1020
  if [[ -z "${selected}" ]]; then
921
1021
  log "No package selected"
922
1022
  exit 0
923
1023
  fi
924
1024
 
925
- local packages=()
1025
+ local selected_managers=()
1026
+ local selected_packages=()
1027
+ local selected_line
1028
+ local selected_manager
926
1029
  local selected_pkg
927
- while IFS= read -r selected_pkg; do
928
- [[ -n "${selected_pkg}" ]] || continue
929
- packages+=("${selected_pkg}")
930
- done < <(collect_selected_packages "${selected}")
931
-
932
- if [[ "${#packages[@]}" -eq 0 ]]; then
1030
+ while IFS= read -r selected_line; do
1031
+ [[ -n "${selected_line}" ]] || continue
1032
+ IFS=$'\t' read -r selected_manager selected_pkg _ <<<"${selected_line}"
1033
+ [[ -n "${selected_manager}" && -n "${selected_pkg}" ]] || continue
1034
+ selected_managers+=("${selected_manager}")
1035
+ selected_packages+=("${selected_pkg}")
1036
+ done <<<"${selected}"
1037
+
1038
+ if [[ "${#selected_packages[@]}" -eq 0 ]]; then
933
1039
  log "No package selected"
934
1040
  exit 0
935
1041
  fi
936
1042
 
1043
+ local unique_managers=()
1044
+ local candidate_manager
1045
+ local already_seen
1046
+ local existing_manager
1047
+ for candidate_manager in "${selected_managers[@]-}"; do
1048
+ already_seen=0
1049
+ for existing_manager in "${unique_managers[@]-}"; do
1050
+ if [[ "${existing_manager}" == "${candidate_manager}" ]]; then
1051
+ already_seen=1
1052
+ break
1053
+ fi
1054
+ done
1055
+ if [[ "${already_seen}" -eq 0 ]]; then
1056
+ unique_managers+=("${candidate_manager}")
1057
+ fi
1058
+ done
1059
+
1060
+ local selected_manager_display
1061
+ selected_manager_display="$(join_manager_labels "${unique_managers[@]-}")"
1062
+
1063
+ local idx
1064
+ local mgr_packages=()
1065
+
937
1066
  case "${ACTION}" in
938
1067
  search)
939
- if confirm_action "Install ${#packages[@]} package(s) with ${manager}?"; then
940
- manager_install "${manager}" "${packages[@]}"
1068
+ if confirm_action "Install ${#selected_packages[@]} package(s) with ${selected_manager_display}?"; then
1069
+ for manager in "${unique_managers[@]-}"; do
1070
+ mgr_packages=()
1071
+ for idx in "${!selected_packages[@]-}"; do
1072
+ if [[ "${selected_managers[$idx]}" == "${manager}" ]]; then
1073
+ mgr_packages+=("${selected_packages[$idx]}")
1074
+ fi
1075
+ done
1076
+ if [[ "${#mgr_packages[@]}" -gt 0 ]]; then
1077
+ log "Installing ${#mgr_packages[@]} package(s) with $(manager_label "${manager}")"
1078
+ manager_install "${manager}" "${mgr_packages[@]-}"
1079
+ fi
1080
+ done
941
1081
  else
942
1082
  log "Install canceled"
943
1083
  fi
944
1084
  ;;
945
1085
  remove)
946
- if confirm_action "Remove ${#packages[@]} package(s) with ${manager}?"; then
947
- manager_remove "${manager}" "${packages[@]}"
1086
+ if confirm_action "Remove ${#selected_packages[@]} package(s) with ${selected_manager_display}?"; then
1087
+ for manager in "${unique_managers[@]-}"; do
1088
+ mgr_packages=()
1089
+ for idx in "${!selected_packages[@]-}"; do
1090
+ if [[ "${selected_managers[$idx]}" == "${manager}" ]]; then
1091
+ mgr_packages+=("${selected_packages[$idx]}")
1092
+ fi
1093
+ done
1094
+ if [[ "${#mgr_packages[@]}" -gt 0 ]]; then
1095
+ log "Removing ${#mgr_packages[@]} package(s) with $(manager_label "${manager}")"
1096
+ manager_remove "${manager}" "${mgr_packages[@]-}"
1097
+ fi
1098
+ done
948
1099
  else
949
1100
  log "Remove canceled"
950
1101
  fi
951
1102
  ;;
952
1103
  list)
953
- local pkg
954
- for pkg in "${packages[@]}"; do
955
- printf "\n=== %s (%s) ===\n" "${pkg}" "$(manager_label "${manager}")"
956
- manager_show_info "${manager}" "${pkg}" || true
1104
+ for idx in "${!selected_packages[@]-}"; do
1105
+ printf "\n=== %s (%s) ===\n" "${selected_packages[$idx]}" "$(manager_label "${selected_managers[$idx]}")"
1106
+ manager_show_info "${selected_managers[$idx]}" "${selected_packages[$idx]}" || true
957
1107
  done
958
1108
  ;;
959
1109
  esac
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fpf-cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Cross-platform fuzzy package finder powered by fzf",
5
5
  "bin": {
6
6
  "fpf": "fpf"