Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ jobs:
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
RELEASE_GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
Comment thread
jclusso marked this conversation as resolved.
59 changes: 49 additions & 10 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
version: 2

# Default optional secrets so templates referencing them resolve even when the
# var is unset (e.g. `make release-snapshot`, where no tap token is exported).
env:
- RELEASE_GITHUB_TOKEN={{ envOrDefault "RELEASE_GITHUB_TOKEN" "" }}

before:
hooks:
- go mod tidy
# Generate man pages from a fresh local build. Goreleaser runs this once
# before any cross-compiled artifacts are produced, so the pages end up
# in every archive (see archives.files below) regardless of GOOS.
# Built from a local host binary, not a cross-compile — same man pages
# end up in every archive regardless of GOOS.
- sh -c 'go run . man -o ./man'

builds:
Expand Down Expand Up @@ -39,14 +43,33 @@ archives:
- goos: windows
formats:
- zip
# Bundle the generated man pages alongside the binary in every archive.
# The Homebrew cask (below) picks them up automatically; users on
# bare-tarball installs can copy `man/*.1` into their MANPATH.
# Bare-tarball users can copy man/*.1 onto their MANPATH.
files:
- LICENSE
- README.md
- man/*.1

nfpms:
- id: emailable
ids:
- emailable
package_name: emailable
vendor: Emailable
homepage: https://emailable.com
maintainer: Emailable <support@emailable.com>
description: Official command-line interface for the Emailable API.
license: MIT
formats:
- deb
- rpm
- apk
bindir: /usr/bin
contents:
# nfpm expands globs, so this picks up every generated page without a
# per-command list to keep in sync (unlike the Homebrew cask below).
- src: ./man/*.1
dst: /usr/share/man/man1/

checksum:
name_template: "checksums.txt"

Expand Down Expand Up @@ -74,13 +97,12 @@ homebrew_casks:
repository:
owner: emailable
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
token: "{{ .Env.RELEASE_GITHUB_TOKEN }}"
homepage: "https://emailable.com"
description: "Official command-line interface for the Emailable API"
skip_upload: auto
# Install the bundled man(1) pages so `man emailable` works out of the
# box for cask users. Globs aren't supported — list each page explicitly.
# Regenerate this list whenever a top-level command is added or removed.
# Globs aren't supported — list each page explicitly. Regenerate this
# list when a top-level command is added or removed.
manpages:
- man/emailable.1
- man/emailable-account.1
Expand All @@ -97,3 +119,20 @@ homebrew_casks:
- man/emailable-status.1
- man/emailable-verify.1
- man/emailable-version.1

scoops:
- name: emailable
ids:
- emailable
repository:
owner: emailable
name: scoop-bucket
# Same PAT as the Homebrew cask above; needs `contents:write` on both
# the homebrew-tap and scoop-bucket repos.
token: "{{ .Env.RELEASE_GITHUB_TOKEN }}"
homepage: https://emailable.com
description: Official command-line interface for the Emailable API.
license: MIT
# Skip the upload when the token is missing so `make release-snapshot`
# and the first release (before the bucket repo exists) keep working.
skip_upload: '{{ if eq .Env.RELEASE_GITHUB_TOKEN "" }}true{{ else }}auto{{ end }}'
77 changes: 70 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,72 @@ See the [CLI docs](https://emailable.com/docs/api/?code_language=cli).

## Installation

### Homebrew
### Quick install

**macOS, Linux, WSL2:**

```bash
curl -fsSL https://emailable.com/install-cli | bash
```

**Windows (PowerShell):**

```powershell
irm https://emailable.com/install-cli.ps1 | iex
```

Both scripts pick the right archive for your OS/arch, verify it against the
published `checksums.txt`, and install the binary (plus bundled man pages on
Unix). Override the version with `EMAILABLE_VERSION=v0.2.0` or the install
prefix with `EMAILABLE_PREFIX=$HOME/.local`.

### Package managers

**Homebrew (macOS):**

```bash
brew install emailable/tap/emailable
```

### Prebuilt binaries
**Scoop (Windows):**

Download the archive for your OS and architecture from the
[releases page](https://github.com/emailable/emailable-cli/releases), extract
it, and put the binary somewhere on your `PATH`.
```powershell
scoop bucket add emailable https://github.com/emailable/scoop-bucket
scoop install emailable
```

In each snippet below, set `ver`/`arch` to the release you want (use
`arch=arm64` on ARM). The `checksums.txt` step verifies the download before
installing — these are GitHub-hosted artifacts, not served from a signed repo.

**Debian / Ubuntu:**

```bash
tar -xzf emailable_<version>_<os>_<arch>.tar.gz
mv emailable /usr/local/bin/
ver=<version> arch=amd64
base="https://github.com/emailable/emailable-cli/releases/download/v$ver"
curl -fsSLO "$base/emailable_${ver}_linux_$arch.deb"
curl -fsSL "$base/checksums.txt" | sha256sum -c --ignore-missing
sudo apt install "./emailable_${ver}_linux_$arch.deb"
```

**Fedora / RHEL:**

```bash
ver=<version> arch=amd64
base="https://github.com/emailable/emailable-cli/releases/download/v$ver"
curl -fsSLO "$base/emailable_${ver}_linux_$arch.rpm"
curl -fsSL "$base/checksums.txt" | sha256sum -c --ignore-missing
sudo dnf install "./emailable_${ver}_linux_$arch.rpm"
```

**Alpine:**

```bash
ver=<version> arch=amd64
base="https://github.com/emailable/emailable-cli/releases/download/v$ver"
curl -fsSLO "$base/emailable_${ver}_linux_$arch.apk"
curl -fsSL "$base/checksums.txt" | sha256sum -c --ignore-missing
sudo apk add --allow-untrusted "./emailable_${ver}_linux_$arch.apk"
```

### From source
Expand All @@ -34,6 +85,18 @@ mv emailable /usr/local/bin/
go install github.com/emailable/emailable-cli@latest
```

### Prebuilt binaries

Download the archive for your OS and architecture from the
[releases page](https://github.com/emailable/emailable-cli/releases), verify it
against `checksums.txt`, extract, and drop the binary on your `PATH`:

```bash
tar -xzf emailable_<version>_<os>_<arch>.tar.gz
sha256sum -c checksums.txt --ignore-missing
mv emailable /usr/local/bin/
```

## Usage

### Authentication
Expand Down
22 changes: 19 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,14 @@ const (
// versionDisplay returns the multi-line version blurb used by both `--version`
// and the `version` subcommand.
func versionDisplay() string {
// Use the resolved version (collectVersionInfo falls back to the Go
// toolchain's module version for `go install` builds where ldflags
// weren't injected).
v := collectVersionInfo().Version

var b strings.Builder
b.WriteString("emailable version ")
b.WriteString(version)
b.WriteString(v)

if extras := versionExtras(); extras != "" {
b.WriteString(" (")
Expand All @@ -75,8 +80,8 @@ func versionDisplay() string {
b.WriteString("]")
}

if version != "" && version != "dev" {
tag := version
if v != "" && v != "dev" {
tag := v
if !strings.HasPrefix(tag, "v") {
tag = "v" + tag
}
Expand Down Expand Up @@ -106,9 +111,11 @@ func collectVersionInfo() versionInfo {
if !ok {
return vi
}
fromVCS := false
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
fromVCS = true
if len(s.Value) > 7 {
vi.Commit = s.Value[:7]
} else {
Expand All @@ -124,6 +131,15 @@ func collectVersionInfo() versionInfo {
vi.Dirty = s.Value == "true"
}
}
// `go install module@vX.Y.Z` injects no ldflags and builds from the module
// cache (no vcs.* settings), so the package-level `version` is still "dev".
// Fall back to the toolchain-recorded module version there. A local checkout
// always carries VCS info — its Main.Version is an untagged pseudo-version,
// not a real release, so leave it as "dev" rather than print a 404 tag URL.
if !fromVCS && (vi.Version == "" || vi.Version == "dev") &&
info.Main.Version != "" && info.Main.Version != "(devel)" {
vi.Version = strings.TrimPrefix(info.Main.Version, "v")
}
return vi
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ go 1.26.3

require (
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
github.com/spf13/cobra v1.10.2
Expand All @@ -15,6 +14,7 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
Expand Down
104 changes: 104 additions & 0 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Install the Emailable CLI on Windows.
#
# Usage (PowerShell):
# irm https://emailable.com/install-cli.ps1 | iex
#
# Environment overrides:
# $env:EMAILABLE_VERSION Specific version to install (e.g. v0.2.0).
# Defaults to the latest GitHub release.
# $env:EMAILABLE_PREFIX Install prefix. Defaults to
# "$env:LOCALAPPDATA\Programs\emailable". The
# binary lands directly under this directory.

$ErrorActionPreference = 'Stop'

$Repo = 'emailable/emailable-cli'
$Binary = 'emailable'

function Abort([string]$msg) {
Write-Host "Error: $msg" -ForegroundColor Red
exit 1
}

# --- detect architecture --------------------------------------------------

# A 32-bit PowerShell on 64-bit Windows reports x86 in PROCESSOR_ARCHITECTURE;
# PROCESSOR_ARCHITEW6432 holds the true machine arch in that case.
$procArch = $env:PROCESSOR_ARCHITEW6432
if (-not $procArch) { $procArch = $env:PROCESSOR_ARCHITECTURE }

$arch = switch ($procArch) {
'AMD64' { 'amd64' }
'ARM64' { 'arm64' }
default { Abort "unsupported architecture: $procArch" }
}

# --- resolve version ------------------------------------------------------

$version = $env:EMAILABLE_VERSION
if (-not $version) {
try {
$resp = Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 `
-Uri "https://github.com/$Repo/releases/latest" -ErrorAction SilentlyContinue
} catch {
$resp = $_.Exception.Response
}
$location = $null
if ($resp -and $resp.Headers) { $location = $resp.Headers['Location'] }
if (-not $location) { Abort "could not determine latest version" }
$version = ($location -split '/tag/')[-1]
}
$version = $version.TrimStart('v')
$tag = "v$version"

# --- pick prefix ----------------------------------------------------------

$prefix = $env:EMAILABLE_PREFIX
if (-not $prefix) {
$prefix = Join-Path $env:LOCALAPPDATA 'Programs\emailable'
}
New-Item -ItemType Directory -Force -Path $prefix | Out-Null

# --- download & verify ----------------------------------------------------

$archive = "${Binary}_${version}_windows_${arch}.zip"
$baseUrl = "https://github.com/$Repo/releases/download/$tag"

$tmp = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
try {
Write-Host "Downloading $archive from $tag..."
Invoke-WebRequest -UseBasicParsing -Uri "$baseUrl/$archive" -OutFile (Join-Path $tmp $archive)
Invoke-WebRequest -UseBasicParsing -Uri "$baseUrl/checksums.txt" -OutFile (Join-Path $tmp 'checksums.txt')

Write-Host "Verifying checksum..."
$expected = (Get-Content (Join-Path $tmp 'checksums.txt') `
| Where-Object { $_ -match " $([regex]::Escape($archive))$" } `
| ForEach-Object { ($_ -split '\s+')[0] } `
| Select-Object -First 1)
if (-not $expected) { Abort "no checksum entry for $archive" }

$actual = (Get-FileHash -Algorithm SHA256 (Join-Path $tmp $archive)).Hash.ToLower()
if ($expected.ToLower() -ne $actual) {
Abort "checksum mismatch (expected $expected, got $actual)"
}

Write-Host "Installing to $prefix\$Binary.exe..."
Expand-Archive -Force -LiteralPath (Join-Path $tmp $archive) -DestinationPath $tmp
Copy-Item -Force -Path (Join-Path $tmp "$Binary.exe") -Destination (Join-Path $prefix "$Binary.exe")
} finally {
Remove-Item -Recurse -Force -Path $tmp -ErrorAction SilentlyContinue
}

# --- ensure prefix is on the user PATH ------------------------------------

$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if (-not $userPath) { $userPath = '' }
$pathParts = $userPath -split ';' | Where-Object { $_ -ne '' }
if (-not ($pathParts -contains $prefix)) {
$newPath = if ($userPath) { "$userPath;$prefix" } else { $prefix }
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
Write-Host "Added $prefix to your user PATH. Open a new terminal to pick up the change." -ForegroundColor Yellow
}

Write-Host "Installed $Binary $version to $prefix\$Binary.exe" -ForegroundColor Green
Loading