Skip to content

pjullrich/pushin-cli

pushin-cli

pun — the CLI for the pushin.eu API, mirroring the gh GitHub CLI's noun-verb shape.

Install

macOS

brew tap pjullrich/pushin-cli-tap https://pushin.eu/pjullrich/pushin-cli-tap.git \
&& brew trust pjullrich/pushin-cli-tap && brew install pun

The tap URL is needed once; after that plain brew install pun / brew upgrade pun work. The macOS binary is universal (Apple Silicon + Intel).

Linux

Homebrew

Homebrew runs on Linux and installs the matching static pun binary on x86_64 or ARM64. If Homebrew is not already installed, install it and follow its shell setup once:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"

Then install pun from the same tap used on macOS:

brew tap pjullrich/pushin-cli-tap https://pushin.eu/pjullrich/pushin-cli-tap.git \
&& brew trust pjullrich/pushin-cli-tap && brew install pun

Add the brew shellenv command printed by the installer to your shell profile so pun remains on PATH in new terminals.

Direct binary download

To install without a package manager, download the static binary for the machine's architecture from the tagged release archive:

PUN_VERSION=0.2.7
case "$(uname -m)" in
x86_64) PUN_PLATFORM=linux-x86_64 ;;
aarch64|arm64) PUN_PLATFORM=linux-arm64 ;;
*) echo "unsupported CPU architecture: $(uname -m)" >&2; exit 1 ;;
esac
PUN_TMP_DIR="$(mktemp -d)"
PUN_ARCHIVE="pushin-cli-v${PUN_VERSION}/dist/${PUN_PLATFORM}/pun"
curl -fsSL \
"https://pushin.eu/pjullrich/pushin-cli/archive/v${PUN_VERSION}.tar.gz" \
-o "$PUN_TMP_DIR/pushin-cli.tar.gz"
tar -xzf "$PUN_TMP_DIR/pushin-cli.tar.gz" \
-C "$PUN_TMP_DIR" --strip-components=3 "$PUN_ARCHIVE"
sudo install -m 0755 "$PUN_TMP_DIR/pun" /usr/local/bin/pun
rm -r "$PUN_TMP_DIR"
pun --version

This installs only pun; upgrades are manual. Repeat the commands with the new version when another release is available.

Build with Cargo

Developers with a Rust toolchain can compile and install the tagged source:

cargo install --locked \
--git https://pushin.eu/pjullrich/pushin-cli.git \
--tag v0.2.7

Distribution-native apt, dnf, and pacman packages are not published yet. Windows builds are prepared in the release matrix but not shipped yet.

Setup

pun login # paste a token from Settings → Security once; stored per host
pun status # check the wiring

Usage

Every command targets <owner>/<repo> — either via -R owner/repo or inferred from the origin git remote of the current directory.

# import a repository
pun import octocat/hello-world [name] # GitHub: git data + labels/issues/PRs/comments
pun import https://any-host/team/tool.git # any git URL: git data only
# import into an organization (created on pushin.eu if it isn't there yet)
pun import octocat/hello-world --into acme # a single repo, filed under acme
pun import --org acme --all # the whole GitHub org: org + every repo
pun import --org acme # same, but pick the repos interactively
pun import --org acme --list # preview only: writes nothing at all
# pull requests
pun pr create --title "My feature" --head my-branch [--base main] [--body ...]
pun pr list [--state open|closed|all]
pun pr view <number> [--web]
pun pr checkout <number> # fetch + switch to the PR's branch
pun pr comments <number> # inline review comments
pun pr comment <number> --path <file> --line <n> [--side LEFT|RIGHT] [--commit <sha>] <body>
pun pr reviews <number>
pun pr review <number> <review-id>
pun pr review-comments <number> <review-id>
pun pr conversation <number> # alias: conv
pun pr reply <number> <comment-id> <body>
pun pr resolve <comment-id> | unresolve <comment-id>
pun pr edit-comment <comment-id> <body>
pun pr edit-review <number> <review-id> <body>
# issues
pun issue create --title <t> [--body <b>] [--label <name>]...
pun issue comments <number>
pun issue comment <number> <body>
# labels
pun label list
pun label create <name> [--color <hex>] [--description <d>]
# repositories
pun repo view [--web]
pun repo create [name|owner/name] # interactive on a terminal
pun repo create <name> --no-interactive # public, no README/license
pun repo create <name> [--visibility public|private] [--description ...]
[--homepage ...] [--default-branch <b>] [--add-readme]
[--license <template>] [--no-interactive]
pun browse # open the repo page in the browser
# organizations
pun org list [--limit 30] # alias: org ls
pun org view <org>
pun org create <handle> [--name ...] [--description ...] [--location ...] [--homepage ...]
pun org edit <org> [--name ...] [--description ...] [--location ...] [--homepage ...]
pun org member list <org>
pun org member add <org> <username> [--role member|admin]
pun org member remove <org> <username> [--yes]
# account & escape hatch
pun status # who am I, masked token, version
pun user
pun api <METHOD> <path> [--input <json>] # any /api/v1 endpoint
pun api GET <path> --paginate # walk every page, print one list
pun api POST <path> -f key=value -f k2=v2 # build the body from fields
# quality of life
pun alias set prs "pr list --state all" # then: pun prs
pun alias list | pun alias delete <name>
pun completion zsh # shell completions (bash/fish/…)

pun repo create asks for the missing name, owner, visibility, optional description, README initialization and license when run on a terminal without create flags. Supplying any create option (or --json) switches to non-interactive mode; --no-interactive makes that choice explicit. The authenticated user is the default owner. Organization memberships are shown in the owner picker, but only organizations where the user is an owner/admin can be selected for repository creation.

pun import from GitHub uses GITHUB_TOKEN (falling back to gh auth token, then anonymous access at 60 requests/hour) for both the metadata API and the git clone — private repositories work without any prompts as long as the token has repo access. Prefer ssh keys? --ssh clones via git@github.com instead. The GitHub token never leaves your machine. Re-running an import updates it instead of duplicating. Open PRs, reviews and reactions are not imported.

--org acme lists a GitHub organization's repositories and makes acme the destination: the organization is created on pushin.eu from its GitHub profile (handle, name, description) with you as its owner, and the repos land under acme/<repo>. Nothing is created until the import is committed — a cancelled picker, an unreadable source repo or an invalid handle all leave the namespace untouched. To keep the pre---org-destination behaviour of filing an org's repos under your own account, pass --into <your own login>.

An organization that already exists is never edited: a matching handle is no evidence it is the same organization, so its name and description stay as its owners wrote them. If you aren't one of those owners, the import stops before writing anything.

In an organization, a repository whose name is already taken is left alone: its description, homepage, default branch, issues and comments are all untouched, since it may be a colleague's and being an org owner is not a reason to write into it. Git data is still pushed (never forced), so the import stays a safe refresh.

Update check & telemetry headers

Every API request carries X-Pun-Version and a pun/<version> User-Agent, so the server can tell CLI traffic apart. After a command, pun checks (at most once per 24 h, cached in ~/.cache/pun, 2-second cap, never fatal) whether a newer release is tagged and prints an upgrade notice to stderr. Set PUN_NO_UPDATE_CHECK=1 to disable the check.

Development

The Rust and Zig versions are pinned in .tool-versions. After installing asdf, bootstrap the complete development and release toolchain with one command:

scripts/setup

The setup script adds the Rust and Zig asdf plugins when needed, installs the pinned tools and cross-compilation targets, installs cargo-zigbuild, and fetches the Rust dependencies. Afterwards, use the usual Cargo commands:

cargo test # unit + HTTP-contract + end-to-end CLI tests (mock servers)
cargo build

Releasing (macOS)

# once
scripts/setup
PUSHIN_TOKEN=<pat> scripts/release # or --dry-run first

The script tests, builds the universal macOS binary and static x86_64/ARM64 Linux binaries, commits them to dist/, tags v<version> (from Cargo.toml), pushes, hashes the server-produced archive, and updates Formula/pun.rb in the pushin-cli-tap tap. Additional targets build via cargo-zigbuild after their matrix rows are enabled.