Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,87 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}

homebrew:
runs-on: ubuntu-latest
needs: goreleaser
steps:
- name: Update Homebrew formula
env:
TAG: ${{ github.ref_name }}
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
run: |
VERSION="${TAG#v}"
BASE_URL="https://github.com/serpapi/serpapi-cli/releases/download/${TAG}"

# Fetch checksums
CHECKSUMS=$(curl -fsSL "${BASE_URL}/serpapi_${VERSION}_checksums.txt")

get_sha() {
echo "$CHECKSUMS" | grep -F "$1" | awk '{print $1}'
}
Comment on lines +42 to +44

SHA_DARWIN_AMD64=$(get_sha "serpapi_${VERSION}_darwin_amd64.tar.gz")
SHA_DARWIN_ARM64=$(get_sha "serpapi_${VERSION}_darwin_arm64.tar.gz")
SHA_LINUX_AMD64=$(get_sha "serpapi_${VERSION}_linux_amd64.tar.gz")
SHA_LINUX_ARM64=$(get_sha "serpapi_${VERSION}_linux_arm64.tar.gz")

cat > /tmp/serpapi-cli.rb <<FORMULA
class SerpapiCli < Formula
desc "HTTP client for structured web search data via SerpApi"
homepage "https://serpapi.com"
version "${VERSION}"
license "MIT"

on_macos do
if Hardware::CPU.arm?
url "${BASE_URL}/serpapi_${VERSION}_darwin_arm64.tar.gz"
sha256 "${SHA_DARWIN_ARM64}"
else
url "${BASE_URL}/serpapi_${VERSION}_darwin_amd64.tar.gz"
sha256 "${SHA_DARWIN_AMD64}"
end
end

on_linux do
if Hardware::CPU.arm?
url "${BASE_URL}/serpapi_${VERSION}_linux_arm64.tar.gz"
sha256 "${SHA_LINUX_ARM64}"
else
url "${BASE_URL}/serpapi_${VERSION}_linux_amd64.tar.gz"
sha256 "${SHA_LINUX_AMD64}"
end
end

def install
bin.install "serpapi"
end

test do
assert_match version.to_s, shell_output("#{bin}/serpapi --version")
assert_match "search", shell_output("#{bin}/serpapi --help")
assert_match "No API key", shell_output("#{bin}/serpapi account 2>&1", 2)
assert_match "No API key", shell_output("#{bin}/serpapi archive abc123 2>&1", 2)
assert_match "Invalid archive ID", shell_output("#{bin}/serpapi --api-key x archive ../bad 2>&1", 2)
end
end
FORMULA

# Set up SSH with deploy key
mkdir -p ~/.ssh
printf '%s\n' "$HOMEBREW_TAP_DEPLOY_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
printf '%s\n' "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" > ~/.ssh/known_hosts
export GIT_SSH_COMMAND="ssh -i ~/.ssh/deploy_key -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes"

# Clone, update formula, push
git clone git@github.com:serpapi/homebrew-tap.git
cp /tmp/serpapi-cli.rb homebrew-tap/Formula/serpapi-cli.rb
cd homebrew-tap
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git add Formula/serpapi-cli.rb
git diff --cached --quiet && echo "Formula already up to date" && exit 0
git commit -m "serpapi-cli ${VERSION}"
git push

20 changes: 2 additions & 18 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,5 @@ changelog:
- "^docs:"
- "^test:"

brews:
- name: serpapi-cli
repository:
owner: serpapi
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
folder: Formula
homepage: https://serpapi.com
description: "HTTP client for structured web search data via SerpApi"
license: MIT
install: |
bin.install "serpapi"
test: |
assert_match version.to_s, shell_output("#{bin}/serpapi --version")
assert_match "search", shell_output("#{bin}/serpapi --help")
assert_match "No API key", shell_output("#{bin}/serpapi account 2>&1", 2)
assert_match "No API key", shell_output("#{bin}/serpapi archive abc123 2>&1", 2)
assert_match "Invalid archive ID", shell_output("#{bin}/serpapi --api-key x archive ../bad 2>&1", 2)
announce:
skip: true
25 changes: 24 additions & 1 deletion pkg/cmd/account.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cmd

import (
"encoding/json"
"regexp"

"github.com/spf13/cobra"

"github.com/serpapi/serpapi-cli/pkg/api"
Expand Down Expand Up @@ -32,5 +35,25 @@ func runAccount(cmd *cobra.Command, args []string) error {
return err
}

return handleOutput(result)
return handleOutput(maskAPIKey(result))
}

var reAPIKey = regexp.MustCompile(`("api_key"\s*:\s*")([^"]{9,})(")`)

// maskAPIKey replaces the api_key value in raw JSON with a masked version (first 4 + last 4).
// Operates on raw bytes to preserve original field order.
func maskAPIKey(raw json.RawMessage) json.RawMessage {
return reAPIKey.ReplaceAllFunc(raw, func(match []byte) []byte {
sub := reAPIKey.FindSubmatch(match)
if len(sub) < 4 {
return match
}
key := string(sub[2])
masked := key[:4] + "…" + key[len(key)-4:]
var buf []byte
buf = append(buf, sub[1]...)
buf = append(buf, masked...)
buf = append(buf, sub[3]...)
return buf
})
}
Comment on lines +41 to 59
27 changes: 27 additions & 0 deletions pkg/cmd/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,33 @@ func init() {
}

func runLogin(cmd *cobra.Command, args []string) error {
// Check if already authenticated
if existingKey, ok := config.LoadAPIKey(); ok {
client := api.New(existingKey)
raw, err := client.Account(cmd.Context())
if err == nil {
var account struct {
AccountEmail string `json:"account_email"`
AccountStatus string `json:"account_status"`
}
if json.Unmarshal(raw, &account) == nil && account.AccountEmail != "" {
fmt.Fprintf(os.Stderr, "Already logged in as %s (%s).\n", account.AccountEmail, account.AccountStatus)
if term.IsTerminal(int(os.Stdin.Fd())) {
fmt.Fprint(os.Stderr, "Re-authenticate with a different key? [y/N] ")
reader := bufio.NewReader(os.Stdin)
line, _ := reader.ReadString('\n')
if strings.TrimSpace(strings.ToLower(line)) != "y" {
return nil
}
} else {
// Non-interactive: exit non-zero so CI detects the no-op
return fmt.Errorf("already logged in as %s. Delete %s to re-authenticate", account.AccountEmail, config.Path())
}
}
}
// If API call failed (e.g. expired key), fall through to re-login
}

var apiKey string

if term.IsTerminal(int(os.Stdin.Fd())) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ var (
var rootCmd = &cobra.Command{
Use: "serpapi",
Short: "HTTP client for structured web search data via SerpApi",
Example: ` serpapi search engine=google q=coffee
serpapi search engine=google_light q="best pizza NYC" --jq ".organic_results[:3]"
serpapi account`,
Version: version.Version,
SilenceUsage: true,
SilenceErrors: true,
Expand Down
9 changes: 7 additions & 2 deletions pkg/cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,13 @@ const defaultMaxPages = 100
var searchCmd = &cobra.Command{
Use: "search [PARAMS...]",
Short: "Perform a search with any supported SerpApi engine",
Args: cobra.ArbitraryArgs,
RunE: runSearch,
Example: ` serpapi search engine=google q=coffee
serpapi search engine=google_light q="weather in Tokyo"
serpapi search engine=google_maps q="pizza" ll="@40.7455096,-74.0083012,14z"
serpapi search engine=google q=coffee --jq ".organic_results[:3]"
serpapi search engine=google q=coffee --all-pages --max-pages 3`,
Args: cobra.ArbitraryArgs,
RunE: runSearch,
}

func init() {
Expand Down
Loading