Skip to content

Commit fcb28bf

Browse files
committed
Add release packaging: .deb, Linux/macOS tarballs, man page
- Add man page (man/how.1) covering all commands, config, and features - Add CPack configuration for .deb with libcurl4 dependency - Add postinst/postrm scripts for symlink management - Add GitHub Actions release workflow triggered on v* tags - Produces .deb, Linux tarball, and macOS tarball per release - Supports both tag-push and GitHub UI release creation flows
1 parent 2de1f3c commit fcb28bf

7 files changed

Lines changed: 343 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*"]
6+
7+
env:
8+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
9+
10+
permissions:
11+
contents: write
12+
13+
jobs:
14+
build-deb:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install libcurl
20+
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev
21+
22+
- name: Configure
23+
run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF
24+
25+
- name: Build
26+
run: cmake --build build --parallel
27+
28+
- name: Package .deb
29+
run: cd build && cpack -G DEB
30+
31+
- name: Create tarball
32+
run: |
33+
VERSION=${GITHUB_REF_NAME#v}
34+
ARCH=$(uname -m)
35+
STAGING="how-${VERSION}-linux-${ARCH}"
36+
mkdir -p "${STAGING}/bin" "${STAGING}/share/man/man1"
37+
cp build/how "${STAGING}/bin/"
38+
ln -s how "${STAGING}/bin/what"
39+
ln -s how "${STAGING}/bin/when"
40+
ln -s how "${STAGING}/bin/why"
41+
cp man/how.1 "${STAGING}/share/man/man1/"
42+
cp packaging/INSTALL-linux.md "${STAGING}/INSTALL.md"
43+
tar czf "${STAGING}.tar.gz" "${STAGING}"
44+
45+
- name: Upload .deb
46+
uses: actions/upload-artifact@v4
47+
with:
48+
name: deb-package
49+
path: build/*.deb
50+
51+
- name: Upload Linux tarball
52+
uses: actions/upload-artifact@v4
53+
with:
54+
name: linux-tarball
55+
path: how-*.tar.gz
56+
57+
build-macos:
58+
runs-on: macos-latest
59+
steps:
60+
- uses: actions/checkout@v4
61+
62+
- name: Configure
63+
run: cmake -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF
64+
65+
- name: Build
66+
run: cmake --build build --parallel
67+
68+
- name: Determine version and arch
69+
id: meta
70+
run: |
71+
VERSION=${GITHUB_REF_NAME#v}
72+
ARCH=$(uname -m)
73+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
74+
echo "arch=$ARCH" >> "$GITHUB_OUTPUT"
75+
76+
- name: Create tarball
77+
run: |
78+
VERSION=${{ steps.meta.outputs.version }}
79+
ARCH=${{ steps.meta.outputs.arch }}
80+
STAGING="how-${VERSION}-macos-${ARCH}"
81+
mkdir -p "${STAGING}/bin" "${STAGING}/share/man/man1"
82+
cp build/how "${STAGING}/bin/"
83+
ln -s how "${STAGING}/bin/what"
84+
ln -s how "${STAGING}/bin/when"
85+
ln -s how "${STAGING}/bin/why"
86+
cp man/how.1 "${STAGING}/share/man/man1/"
87+
cp packaging/INSTALL-macos.md "${STAGING}/INSTALL.md"
88+
tar czf "${STAGING}.tar.gz" "${STAGING}"
89+
90+
- name: Upload tarball
91+
uses: actions/upload-artifact@v4
92+
with:
93+
name: macos-tarball
94+
path: how-*.tar.gz
95+
96+
release:
97+
needs: [ build-deb, build-macos ]
98+
runs-on: ubuntu-latest
99+
steps:
100+
- uses: actions/checkout@v4
101+
102+
- name: Download artifacts
103+
uses: actions/download-artifact@v4
104+
with:
105+
path: artifacts
106+
107+
- name: Create or update GitHub Release
108+
env:
109+
GH_TOKEN: ${{ github.token }}
110+
run: |
111+
if gh release view "$GITHUB_REF_NAME" > /dev/null 2>&1; then
112+
gh release upload "$GITHUB_REF_NAME" --clobber \
113+
artifacts/deb-package/*.deb \
114+
artifacts/linux-tarball/*.tar.gz \
115+
artifacts/macos-tarball/*.tar.gz
116+
else
117+
gh release create "$GITHUB_REF_NAME" \
118+
--title "$GITHUB_REF_NAME" \
119+
--generate-notes \
120+
artifacts/deb-package/*.deb \
121+
artifacts/linux-tarball/*.tar.gz \
122+
artifacts/macos-tarball/*.tar.gz
123+
fi

CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ endif()
118118
# Install
119119
include(GNUInstallDirs)
120120
install(TARGETS how RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
121+
install(FILES man/how.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
121122

122123
# Create symlinks for alternative command names
123124
foreach(alias what when why)
@@ -129,3 +130,19 @@ foreach(alias what when why)
129130
message(STATUS \"Symlink: ${alias} -> how\")
130131
")
131132
endforeach()
133+
134+
# --- CPack packaging ---
135+
set(CPACK_PACKAGE_NAME "how")
136+
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
137+
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Ask an LLM questions from the terminal")
138+
set(CPACK_PACKAGE_DESCRIPTION "A one-shot CLI utility that sends a question to a large language model and prints the answer. The command name forms the start of the query.")
139+
set(CPACK_PACKAGE_CONTACT "Mattias Lindblad")
140+
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/matlimatli/how")
141+
142+
# Debian
143+
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libcurl4")
144+
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
145+
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
146+
"${CMAKE_SOURCE_DIR}/packaging/postinst;${CMAKE_SOURCE_DIR}/packaging/postrm")
147+
148+
include(CPack)

man/how.1

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
.TH HOW 1 "2026-03-23" "how 1.0" "User Commands"
2+
.SH NAME
3+
how \- ask an LLM questions from the terminal
4+
.SH SYNOPSIS
5+
.B how
6+
.RI [ question... ]
7+
.br
8+
.B what
9+
.RI [ question... ]
10+
.br
11+
.B when
12+
.RI [ question... ]
13+
.br
14+
.B why
15+
.RI [ question... ]
16+
.SH DESCRIPTION
17+
.B how
18+
is a one-shot CLI utility that sends a question to a large language model and
19+
prints the answer.
20+
The command name forms the start of the query: running
21+
.B how to list files
22+
sends \(lqhow to list files\(rq to the configured LLM provider.
23+
.PP
24+
Symlinks
25+
.BR what ,
26+
.BR when ,
27+
and
28+
.B why
29+
allow different question styles.
30+
For example,
31+
.B what is my ip
32+
sends \(lqwhat is my ip\(rq.
33+
.PP
34+
The response is tailored to the user\(aqs operating system, shell, and working
35+
directory.
36+
Answers are concise and fit on a standard terminal screen.
37+
When a command is relevant, the LLM provides a brief explanation followed by the
38+
exact command(s).
39+
.SH CONFIGURATION
40+
.B how
41+
reads its configuration from
42+
.IR ~/.config/how/config .
43+
The file must have 0600 permissions.
44+
.PP
45+
The format is simple key=value pairs, one per line.
46+
Lines starting with
47+
.B #
48+
are comments.
49+
.SS Required keys
50+
.TP
51+
.B default_provider
52+
The LLM provider to use.
53+
Valid values:
54+
.BR openai ,
55+
.BR anthropic ,
56+
.BR mistral ,
57+
.BR google ,
58+
.BR custom .
59+
.TP
60+
.IB provider _api_key
61+
API key for the given provider (e.g.,
62+
.BR mistral_api_key ).
63+
.SS Optional keys
64+
.TP
65+
.IB provider _model
66+
Override the default model for a provider (e.g.,
67+
.BR openai_model ).
68+
.TP
69+
.B custom_endpoint
70+
URL for the custom provider (required when using
71+
.BR custom ).
72+
Compatible with OpenAI-format APIs such as Ollama and vLLM.
73+
.TP
74+
.B allow_insecure_ssl
75+
Set to
76+
.B true
77+
to disable SSL certificate verification.
78+
Intended for local LLM servers with self-signed certificates.
79+
.SS Example configuration
80+
.nf
81+
default_provider = mistral
82+
mistral_api_key = sk-...
83+
mistral_model = mistral-small-latest
84+
.fi
85+
.SH ENVIRONMENT
86+
.TP
87+
.B HOW_PROVIDER
88+
Override the configured default provider for this invocation.
89+
.TP
90+
.B HOW_MODEL
91+
Override the configured model for this invocation.
92+
.SH FOLLOW-UP DETECTION
93+
.B how
94+
caches the last exchange and automatically includes it as context when it
95+
detects a follow-up question:
96+
.IP \(bu 2
97+
Within 2 minutes: always treated as a follow-up.
98+
.IP \(bu 2
99+
2\(en10 minutes: treated as a follow-up if the query is short (3 words or
100+
fewer after the command name) or contains signal words like \(lqbut\(rq,
101+
\(lqalso\(rq, \(lqinstead\(rq, etc.
102+
.IP \(bu 2
103+
After 10 minutes: never treated as a follow-up.
104+
.SH FILES
105+
.TP
106+
.I ~/.config/how/config
107+
Configuration file (must be 0600).
108+
.TP
109+
.I ~/.cache/how/history
110+
Cached last exchange for follow-up detection.
111+
.SH EXIT STATUS
112+
.TP
113+
.B 0
114+
Success.
115+
.TP
116+
.B 1
117+
Error (missing config, network failure, invalid response, etc.).
118+
A diagnostic message is printed to standard error.
119+
.SH EXAMPLES
120+
.TP
121+
.B how to find large files
122+
Ask how to find large files on this system.
123+
.TP
124+
.B what is the default gateway
125+
Ask what the default gateway is.
126+
.TP
127+
.B how in rust
128+
Short follow-up: repeat the previous question but for Rust.
129+
.TP
130+
.B HOW_PROVIDER=anthropic how to list containers
131+
Use Anthropic for a single query.
132+
.SH NOTES
133+
The
134+
.B ?
135+
character is a shell glob.
136+
Quote it or omit it when asking questions:
137+
.PP
138+
.nf
139+
how "what is my ip?" # quoted
140+
how what is my ip # omit the question mark
141+
.fi
142+
.SH AUTHORS
143+
Mattias Lindblad

packaging/INSTALL-linux.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Installing how on Linux
2+
3+
Extract the archive and copy the files:
4+
5+
```sh
6+
sudo cp bin/how /usr/local/bin/
7+
sudo ln -sf /usr/local/bin/how /usr/local/bin/what
8+
sudo ln -sf /usr/local/bin/how /usr/local/bin/when
9+
sudo ln -sf /usr/local/bin/how /usr/local/bin/why
10+
sudo mkdir -p /usr/local/share/man/man1
11+
sudo cp share/man/man1/how.1 /usr/local/share/man/man1/
12+
```
13+
14+
Requires libcurl at runtime (`libcurl4` on Debian/Ubuntu, `libcurl` on Fedora/RHEL).
15+
16+
To uninstall:
17+
18+
```sh
19+
sudo rm -f /usr/local/bin/how /usr/local/bin/what /usr/local/bin/when /usr/local/bin/why
20+
sudo rm -f /usr/local/share/man/man1/how.1
21+
```

packaging/INSTALL-macos.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Installing how on macOS
2+
3+
Extract the archive and copy the files to `/usr/local`:
4+
5+
```sh
6+
sudo cp bin/how /usr/local/bin/
7+
sudo ln -sf /usr/local/bin/how /usr/local/bin/what
8+
sudo ln -sf /usr/local/bin/how /usr/local/bin/when
9+
sudo ln -sf /usr/local/bin/how /usr/local/bin/why
10+
sudo mkdir -p /usr/local/share/man/man1
11+
sudo cp share/man/man1/how.1 /usr/local/share/man/man1/
12+
```
13+
14+
To uninstall:
15+
16+
```sh
17+
sudo rm -f /usr/local/bin/how /usr/local/bin/what /usr/local/bin/when /usr/local/bin/why
18+
sudo rm -f /usr/local/share/man/man1/how.1
19+
```

packaging/postinst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/sh
2+
set -e
3+
4+
case "$1" in
5+
configure)
6+
for alias in what when why; do
7+
ln -sf how /usr/bin/$alias
8+
done
9+
;;
10+
esac

packaging/postrm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/sh
2+
set -e
3+
4+
case "$1" in
5+
remove|purge)
6+
for alias in what when why; do
7+
rm -f /usr/bin/$alias
8+
done
9+
;;
10+
esac

0 commit comments

Comments
 (0)