Metadata-Version: 2.4
Name: hopper-cli
Version: 1.3.5
Summary: A git-like CLI for tutors on the hopper platform.
Author: hopper contributors
License-Expression: AGPL-3.0-or-later
Project-URL: Homepage, https://codeberg.org/hopper/cli
Project-URL: Repository, https://codeberg.org/hopper/cli
Project-URL: Issues, https://codeberg.org/hopper/cli/issues
Project-URL: Changelog, https://codeberg.org/hopper/cli/releases
Keywords: cli,grading,tutor,education,git
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Education
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Education
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# hopper

[![License: AGPL-3.0-or-later](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue.svg)](LICENSE)
[![PyPI](https://img.shields.io/pypi/v/hopper-cli)](https://pypi.org/project/hopper-cli/)
[![Python](https://img.shields.io/pypi/pyversions/hopper-cli)](https://pypi.org/project/hopper-cli/)
[![Forgejo](https://img.shields.io/badge/forge-codeberg.org-orange)](https://codeberg.org/hopper/cli)

A git-like CLI for tutors on the [hopper](https://codeberg.org/hopper/cli)
platform. It wraps `git` and the hopper orchestration services to safely
pull, grade, and push all of a tutor's assigned student repositories.

Admins can use the CLI for any course and operate on all its student
repositories, even when enrolled as a student or assigned as a tutor there.

## Installation

Install via **[uv](https://docs.astral.sh/uv/getting-started/installation/)**
(recommended) or **[pipx](https://pipx.pypa.io/stable/installation/)**, or grab
a prebuilt binary from the
[Forgejo releases page](https://codeberg.org/hopper/cli/releases). Do **not**
install via `go install` or build from source unless you are developing
hopper — see [Development](#development) for why.

**uv** (recommended, installs an isolated executable):

```
uv tool install hopper-cli
```

or with **pipx** (also installs an isolated executable):

```
pipx install hopper-cli
```

To upgrade later:

```
uv tool upgrade hopper-cli
pipx upgrade hopper-cli
```

Use the upgrade command matching how you installed hopper.

Avoid installing directly with `pip`: it installs into the active Python
environment and may conflict with other packages or be blocked by a
system-managed Python installation. Use uv or pipx for an isolated installation.
If you already installed hopper with pip, update it with
`pip install --upgrade hopper-cli` in that same environment.

### Prebuilt binaries

Prebuilt binaries for recent releases are available on the
[Forgejo releases page](https://codeberg.org/hopper/cli/releases).

## Requirements

- [git](https://git-scm.com) — hopper drives `git` under the hood
- Python 3.8+ (only for the pip/pipx installation)
- An account as tutor/staff/admin on a hopper instance

## Getting started

```
hopper auth            # set up an instance profile + API key
hopper init <course>   # create a workspace directory bound to a course
cd <course-uid>
hopper pull            # clone/update all your assigned student repos
```

## Concepts

### Profiles

A **profile** is a set of service URLs plus your API key — typically one per
university. They live in the global config at `os.UserConfigDir()/hopper/config.yaml`
(`~/.config/hopper/` on Linux, `~/Library/Application Support/hopper/` on
macOS, `%AppData%\hopper\` on Windows).

`hopper auth` interactively creates a profile, offering known presets (e.g.
`alu` for Albert-Ludwigs-Universität Freiburg) or custom URLs. You can hold
several profiles and pick which one a workspace uses at `init` time.

### Workspaces

`hopper init [profile/]<course>` creates a `./<course-uid>/` directory with a
`.hopper.yaml` inside. `<course>` may be a UID (`2025WS-EidP`) or numeric ID.
With multiple profiles, prefix the profile: `hopper init alu/2025WS-EidP`.
All commands run **inside** that directory and never write outside it.

## Commands

| Command | Description |
|---|---|
| `hopper auth` | Interactively set up a profile (preset or custom). |
| `hopper auth list` / `status` / `remove <id>` | Manage profiles. |
| `hopper init [profile/]<course>` | Bind a new directory to a course. |
| `hopper pull` | Clone or `pull --rebase --autostash` every assigned student repo. |
| `hopper logs` | Print the saved output of the most recent pull run. |
| `hopper push [msg]` | Commit README changes, validate point schema, push. |
| `hopper commit <msg>` | Commit without pushing (recovery path). |
| `hopper status` | Per-student branch, ahead/behind, dirty state. |
| `hopper students` | List students assigned to you. |
| `hopper info` | Course info, your role, exercises. |
| `hopper version` | Print version. |

Run `hopper <command> --help` for the full flag reference of each command.

### `auth`

```
hopper auth            # interactive setup (preset picker or custom URLs)
hopper auth --custom   # skip the preset picker and enter custom URLs
hopper auth list       # list configured profiles
hopper auth status     # show the current authentication state
hopper auth remove alu # remove a profile
```

Authorization happens in your browser: hopper opens the instance's authorize
page and receives the new API key via a local callback. If that fails, it
falls back to asking you to paste a key manually.

### `init`

```
hopper init 2025WS-EidP    # uses the only/default configured profile
hopper init alu/2025WS-EidP
hopper init alu/42         # numeric course ID also works
hopper init --force alu/2025WS-EidP   # overwrite an existing .hopper.yaml
```

### `pull`

```
hopper pull                 # sync all assigned student repos
hopper pull --student alice # pull a single student by username
hopper pull --only a,b,c    # pull only the listed usernames
hopper pull --prune         # remove local repos of students no longer assigned
hopper pull -j 8            # 8 concurrent git operations (default 4, 2 for ssh remotes)
```

- `--prune` removes local repos of students no longer assigned to you, after
  showing a preview and asking for confirmation; `--no-prune` skips pruning.
- Repos are synced concurrently, 4 at a time by default. When the instance's
  clone URL uses ssh, the default is 2: bursts of concurrent ssh handshakes
  can make the server reject some connections. Override with `-j`.
- Per-repo failures are reported and skipped, and one bad repo does not abort
  the whole run. Repos that fail are retried once, serially, after the
  concurrent pass, since transient ssh failures usually succeed on a second
  try.
- The final report lists every repo that still failed together with its
  error, and states that its local copy was NOT updated. For ssh
  authentication errors (`Permission denied (publickey)`, `Too many
  authentication failures`) it adds a hint to check `ssh-add -l` and the key
  registration in your hopper profile.
- Every run's full output is saved to a log file under the user cache dir, so
  a closed terminal does not lose a failure report. A failed run ends with
  `Full output: <path>` pointing at that file; inspect it later with
  `hopper logs`.
- If the configured origin branch is missing but `origin/main` exists, hopper
  repairs the upstream automatically. Fresh clones with an invalid remote HEAD
  check out `main`; existing checkouts keep their local branch and changes.

### `logs`

```
hopper logs        # print the saved output of the most recent pull run
hopper logs --path # print only the path to that file
```

Each `hopper pull` writes its full terminal output to
`pull-<timestamp>.log` in the hopper logs directory under the user cache dir
(`~/.cache/hopper/logs/` on Linux, `~/Library/Caches/hopper/logs/` on macOS).
The file starts with a header (command line, timestamp, profile, course, exit
status) followed by the output exactly as it appeared in the terminal. Only
the 5 most recent logs are kept; older ones are deleted automatically. The
command does not touch the server, so it also works offline.

### `push` / `commit`

```
hopper push                     # commit */README.md changes and push (default msg: "Grade exercises")
hopper push "Grade exercise 3"  # custom commit message
hopper push --all               # stage all changes, not just */README.md
hopper push --allow-invalid     # push even when the README point schema is invalid
hopper commit "WIP grading"     # commit without pushing (recovery path)
```

Before pushing, hopper validates that each modified README's first line
contains exactly one point schema like `(12/15)`. Invalid pushes are blocked
unless `--allow-invalid` is given. A conflicting rebase surfaces the conflict
and halts that repo instead of destroying local work — hopper never runs
`git reset --hard`.

### `status` / `students` / `info`

```
hopper status    # table: student, branch, ahead/behind, dirty/clean
hopper students  # table: username, name, matrikel
hopper info      # course name/uid/id, profile, your user and role, exercises
```

## Version gate

On every command that touches the server (everything except `auth`,
`version`, `logs`, and `help`), hopper asks the active profile's orchestration
server for the minimum supported CLI version:

- **running version < minimum** → the CLI refuses to run and tells you to upgrade;
- **running version >= minimum** → proceeds;
- **server unreachable** → proceeds silently (can't check offline).

## License

Licensed under [AGPL-3.0-or-later](LICENSE).

## Development

Requires [Go](https://go.dev) (see `go.mod` for the minimum version) and
[Task](https://taskfile.dev).

> ⚠️ **Note:** Building from source without injecting the version via
> ldflags produces a binary with a wrong/fallback version string
> (`v0.1.0` or `dev`). This breaks `hopper version`, the CLI's self-upgrade
> check, and the server-side minimum-version gate. End users should install
> via **uv**/**pipx** or the prebuilt release binaries instead — see
> [Installation](#installation). If you do build from source, always inject
> the version as shown below.

A plain `go install` works for quick local experiments only:

```
go install codeberg.org/hopper/cli/cmd/hopper@latest
```

Or clone the repository and build with Task (`task build` below produces
`bin/hopper`). Useful tasks:

```
task build       # build the CLI binary to bin/hopper
task test        # run unit tests
task lint        # run golangci-lint
task fmt         # format Go code and imports
task default     # fmt, lint, vet, test, build
task ci          # strict CI gate: fmt, lint, vet, test, build, clean tree
task hooks:install  # point git at the versioned githooks directory
```

The version is injected at build time via ldflags:

```
go build -ldflags "-X codeberg.org/hopper/cli/internal/cmd.Version=vX.Y.Z" -o hopper ./cmd/hopper/
```

### User-facing output

Use `internal/ui` for messages so commands share the same styling and spacing:

- `Success` marks completed actions; `Info` is for neutral progress and hints.
- `Warn` and `Error` write diagnostics to stderr. Keep recovery instructions in
  the same message so they stay with the error when output is redirected.
- Message helpers terminate output with one newline and indent multiline
  details beneath the message text.
- `Section` prints only a heading. Use `Break` once between output blocks,
  never before the first heading or after the last message.
- Keep raw output such as the version line unstyled.
- Disable ANSI styling for redirected output, `NO_COLOR`, and `TERM=dumb`.
