Skip to content

Commit 1fde043

Browse files
committed
Add shell completions (bash, zsh, fish)
- Add \`completions\` subcommand: prints shell completion script to stdout - Auto-detects shell from $SHELL when --shell flag is omitted - Add \`getCompletionFilePath()\` pure helper (respects XDG_CONFIG_HOME, XDG_DATA_HOME, ZDOTDIR) - Refresh completions automatically after a successful \`upgrade\` (opt-in: only if the file already exists) - Install completions from \`install.sh\` using the installed binary - Add bats tests for \`install_completions()\` (fish, zsh, bash, unknown shell, XDG overrides) - Add \`test:bats\` script to package.json - Add \`test-bats\` job to CI (installs bats via npm, runs install.test.bats) - Merge CI \`test\` + \`quality\` jobs to avoid duplicate checkout/setup steps
1 parent 900ea04 commit 1fde043

10 files changed

Lines changed: 966 additions & 77 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88

99
jobs:
1010
test:
11-
name: Unit tests
11+
name: Test · Format · Lint · Dead code
1212
runs-on: ubuntu-latest
1313
permissions:
1414
pull-requests: write
@@ -36,22 +36,6 @@ jobs:
3636
lcov-file: coverage/lcov.info
3737
continue-on-error: true
3838

39-
quality:
40-
name: Format · Lint · Dead code
41-
runs-on: ubuntu-latest
42-
43-
steps:
44-
- name: Checkout
45-
uses: actions/checkout@v6
46-
47-
- name: Setup Bun
48-
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
49-
with:
50-
bun-version: latest
51-
52-
- name: Install dependencies
53-
run: bun install --frozen-lockfile
54-
5539
- name: Check formatting (oxfmt)
5640
run: bun run format:check
5741

@@ -60,3 +44,18 @@ jobs:
6044

6145
- name: Dead code detection (knip)
6246
run: bun run knip
47+
48+
test-bats:
49+
name: Shell tests (bats)
50+
runs-on: ubuntu-latest
51+
52+
steps:
53+
- name: Checkout
54+
uses: actions/checkout@v6
55+
56+
- name: Install bats-core
57+
# bats is not pre-installed on GitHub-hosted Ubuntu runners.
58+
run: npm install -g bats
59+
60+
- name: Run install.sh shell tests
61+
run: bats install.test.bats

github-code-search.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { buildOutput } from "./src/output.ts";
2222
import { groupByTeamPrefix, flattenTeamSections } from "./src/group.ts";
2323
import { checkForUpdate } from "./src/upgrade.ts";
2424
import { runInteractive } from "./src/tui.ts";
25+
import { generateCompletion, detectShell } from "./src/completions.ts";
2526
import type { OutputFormat, OutputType } from "./src/types.ts";
2627

2728
// Version + build metadata injected at compile time via --define (see build.ts).
@@ -349,6 +350,42 @@ program
349350
process.stdout.write(`error: ${e instanceof Error ? e.message : String(e)}\n`);
350351
process.exit(1);
351352
}
353+
const { refreshCompletions } = await import("./src/upgrade.ts");
354+
const refreshedPath = await refreshCompletions(detectShell(), undefined, opts.debug);
355+
if (refreshedPath) {
356+
process.stdout.write(`✓ Shell completions refreshed at ${refreshedPath}\n`);
357+
}
358+
process.exit(0);
359+
});
360+
361+
// `completions` subcommand — print a shell completion script to stdout
362+
program
363+
.command("completions")
364+
.description("Print a shell completion script for bash, zsh or fish")
365+
.configureHelp(helpFormatConfig)
366+
.addHelpText(
367+
"after",
368+
helpSection("Documentation:", "https://fulll.github.io/github-code-search/usage/upgrade"),
369+
)
370+
.option(
371+
"--shell <shell>",
372+
["Target shell: bash, zsh or fish.", "Auto-detected from $SHELL when omitted."].join("\n"),
373+
)
374+
.action((opts: { shell?: string }) => {
375+
const shell = opts.shell ?? detectShell();
376+
if (!shell) {
377+
writeFileSync(
378+
2,
379+
`error: could not detect shell. Use --shell bash|zsh|fish to specify it explicitly.\n`,
380+
);
381+
process.exit(1);
382+
}
383+
try {
384+
writeFileSync(1, generateCompletion(shell) + "\n");
385+
} catch (e: unknown) {
386+
writeFileSync(2, `error: ${e instanceof Error ? e.message : String(e)}\n`);
387+
process.exit(1);
388+
}
352389
process.exit(0);
353390
});
354391

install.sh

Lines changed: 114 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,69 +10,124 @@
1010

1111
set -euo pipefail
1212

13-
REPO="fulll/github-code-search"
1413
BINARY_NAME="github-code-search"
15-
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
16-
GITHUB_API="https://api.github.com"
17-
18-
# ─── Detect OS ────────────────────────────────────────────────────────────────
19-
20-
case "$(uname -s)" in
21-
Linux*) OS="linux" ;;
22-
Darwin*) OS="macos" ;;
23-
MINGW*|MSYS*|CYGWIN*) OS="windows" ;;
24-
*)
25-
echo "error: unsupported OS: $(uname -s)" >&2
26-
exit 1
27-
;;
28-
esac
29-
30-
# ─── Detect architecture ──────────────────────────────────────────────────────
31-
32-
MACHINE="$(uname -m)"
33-
case "$MACHINE" in
34-
x86_64|amd64) ARCH="x64" ;;
35-
arm64|aarch64) ARCH="arm64" ;;
36-
*)
37-
echo "error: unsupported architecture: $MACHINE" >&2
38-
exit 1
39-
;;
40-
esac
41-
42-
ARTIFACT="${BINARY_NAME}-${OS}-${ARCH}"
43-
[ "$OS" = "windows" ] && ARTIFACT="${ARTIFACT}.exe"
44-
45-
# ─── Resolve version ──────────────────────────────────────────────────────────
46-
47-
if [ -n "${VERSION:-}" ]; then
48-
TAG="$VERSION"
49-
else
50-
echo "Detecting latest release…"
51-
TAG=$(curl -fsSL "${GITHUB_API}/repos/${REPO}/releases/latest" \
52-
| grep '"tag_name"' \
53-
| sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
54-
fi
5514

56-
echo "Installing ${BINARY_NAME} ${TAG} (${OS}/${ARCH})…"
15+
# ─── Shell completions ────────────────────────────────────────────────────────
16+
# Defined early so it can be sourced and tested independently (INSTALL_SH_TEST=1).
5717

58-
# ─── Download ─────────────────────────────────────────────────────────────────
18+
install_completions() {
19+
local bin="$1"
20+
local shell_name
21+
shell_name="$(basename "${SHELL:-}")"
5922

60-
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${TAG}/${ARTIFACT}"
61-
TMP="$(mktemp)"
62-
curl -fsSL --progress-bar -o "$TMP" "$DOWNLOAD_URL"
63-
chmod +x "$TMP"
23+
case "$shell_name" in
24+
fish)
25+
local comp_dir="${XDG_CONFIG_HOME:-$HOME/.config}/fish/completions"
26+
local comp_file="${comp_dir}/github-code-search.fish"
27+
mkdir -p "$comp_dir"
28+
"$bin" completions --shell fish > "$comp_file"
29+
echo ""
30+
echo "✓ Fish completions installed at ${comp_file}"
31+
;;
32+
zsh)
33+
local comp_dir="${ZDOTDIR:-$HOME}/.zfunc"
34+
local comp_file="${comp_dir}/_github-code-search"
35+
mkdir -p "$comp_dir"
36+
"$bin" completions --shell zsh > "$comp_file"
37+
echo ""
38+
echo "✓ Zsh completions installed at ${comp_file}"
39+
echo " Make sure ${comp_dir} is on your fpath. Add to ~/.zshrc if needed:"
40+
echo " fpath=(${comp_dir} \$fpath)"
41+
echo " autoload -Uz compinit && compinit"
42+
;;
43+
bash)
44+
local comp_dir="${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions"
45+
local comp_file="${comp_dir}/github-code-search"
46+
mkdir -p "$comp_dir"
47+
"$bin" completions --shell bash > "$comp_file"
48+
echo ""
49+
echo "✓ Bash completions installed at ${comp_file}"
50+
;;
51+
*)
52+
echo ""
53+
echo " Shell completions are available for bash, zsh and fish."
54+
echo " Run the following to generate your completion script:"
55+
echo " ${BINARY_NAME} completions --shell <bash|zsh|fish>"
56+
;;
57+
esac
58+
}
6459

65-
# ─── Install ──────────────────────────────────────────────────────────────────
60+
# ─── Main installation ────────────────────────────────────────────────────────
61+
# Skipped when sourced for testing (export INSTALL_SH_TEST=1 before sourcing).
6662

67-
DEST="${INSTALL_DIR}/${BINARY_NAME}"
68-
if [ -w "$INSTALL_DIR" ]; then
69-
mv "$TMP" "$DEST"
70-
else
71-
echo " (sudo required for ${INSTALL_DIR})"
72-
sudo mv "$TMP" "$DEST"
73-
fi
63+
if [ -z "${INSTALL_SH_TEST:-}" ]; then
64+
65+
REPO="fulll/github-code-search"
66+
INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}"
67+
GITHUB_API="https://api.github.com"
68+
69+
# ─── Detect OS ──────────────────────────────────────────────────────────────
70+
71+
case "$(uname -s)" in
72+
Linux*) OS="linux" ;;
73+
Darwin*) OS="macos" ;;
74+
MINGW*|MSYS*|CYGWIN*) OS="windows" ;;
75+
*)
76+
echo "error: unsupported OS: $(uname -s)" >&2
77+
exit 1
78+
;;
79+
esac
80+
81+
# ─── Detect architecture ────────────────────────────────────────────────────
82+
83+
MACHINE="$(uname -m)"
84+
case "$MACHINE" in
85+
x86_64|amd64) ARCH="x64" ;;
86+
arm64|aarch64) ARCH="arm64" ;;
87+
*)
88+
echo "error: unsupported architecture: $MACHINE" >&2
89+
exit 1
90+
;;
91+
esac
92+
93+
ARTIFACT="${BINARY_NAME}-${OS}-${ARCH}"
94+
[ "$OS" = "windows" ] && ARTIFACT="${ARTIFACT}.exe"
95+
96+
# ─── Resolve version ────────────────────────────────────────────────────────
7497

75-
echo "${BINARY_NAME} ${TAG} installed at ${DEST}"
76-
echo ""
77-
echo " Remember to export your GitHub token:"
78-
echo " export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx"
98+
if [ -n "${VERSION:-}" ]; then
99+
TAG="$VERSION"
100+
else
101+
echo "Detecting latest release…"
102+
TAG=$(curl -fsSL "${GITHUB_API}/repos/${REPO}/releases/latest" \
103+
| grep '"tag_name"' \
104+
| sed -E 's/.*"tag_name": *"([^"]+)".*/\1/')
105+
fi
106+
107+
echo "Installing ${BINARY_NAME} ${TAG} (${OS}/${ARCH})…"
108+
109+
# ─── Download ───────────────────────────────────────────────────────────────
110+
111+
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${TAG}/${ARTIFACT}"
112+
TMP="$(mktemp)"
113+
curl -fsSL --progress-bar -o "$TMP" "$DOWNLOAD_URL"
114+
chmod +x "$TMP"
115+
116+
# ─── Install ────────────────────────────────────────────────────────────────
117+
118+
DEST="${INSTALL_DIR}/${BINARY_NAME}"
119+
if [ -w "$INSTALL_DIR" ]; then
120+
mv "$TMP" "$DEST"
121+
else
122+
echo " (sudo required for ${INSTALL_DIR})"
123+
sudo mv "$TMP" "$DEST"
124+
fi
125+
126+
echo "${BINARY_NAME} ${TAG} installed at ${DEST}"
127+
echo ""
128+
echo " Remember to export your GitHub token:"
129+
echo " export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx"
130+
131+
install_completions "$DEST"
132+
133+
fi

0 commit comments

Comments
 (0)