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
87 changes: 87 additions & 0 deletions .github/workflows/crates-io.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: crates-io

on:
push:
tags:
- v*.*.*
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
steps:
- name: checkout
uses: actions/checkout@v5

- name: set up rust
uses: dtolnay/rust-toolchain@stable

- name: verify tag matches crate version
shell: bash
run: |
set -euo pipefail
expected="${GITHUB_REF_NAME#v}"
actual="$(cargo pkgid -p ferriskey | sed 's/.*#.*://')"
if [[ "$expected" != "$actual" ]]; then
echo "tag version '$expected' does not match Cargo version '$actual'" >&2
exit 1
fi

- name: run checks
run: |
cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings

- name: publish crates
shell: bash
run: |
set -euo pipefail

publish_if_needed() {
local crate="$1"
local version="$2"

if curl -fsS "https://crates.io/api/v1/crates/${crate}/${version}" >/dev/null 2>&1; then
echo "${crate}@${version} already published"
return 0
fi

cargo publish -p "${crate}" --locked
}

wait_for_crate() {
local crate="$1"
local version="$2"

for _ in $(seq 1 30); do
if curl -fsS "https://crates.io/api/v1/crates/${crate}/${version}" >/dev/null 2>&1; then
return 0
fi
sleep 10
done

echo "crate ${crate}@${version} was not visible on crates.io in time" >&2
exit 1
}

version="${GITHUB_REF_NAME#v}"

publish_if_needed ferriskey-commands "${version}"
wait_for_crate ferriskey-commands "${version}"

publish_if_needed ferriskey-client "${version}"
wait_for_crate ferriskey-client "${version}"

publish_if_needed ferriskey-cli-core "${version}"
wait_for_crate ferriskey-cli-core "${version}"

publish_if_needed ferriskey "${version}"
3 changes: 3 additions & 0 deletions .github/workflows/docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
tags:
- v*.*.*
- v*.*.*-rc*
workflow_dispatch:
inputs:
push:
Expand Down
46 changes: 46 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
cargo build --release # Build all workspace crates
cargo test --workspace # Run all tests
cargo clippy --workspace --all-targets --all-features -- -D warnings # Lint (warnings are errors)
cargo test -p ferriskey-cli-core context::tests # Run a specific test module
```

## Architecture

This is a Cargo workspace with 4 crates:

- **`ferriskey`** (root) — Binary entry point. Parses CLI with `Cli::parse()`, passes to `ferriskey_cli_core::run()`.
- **`libs/ferriskey-commands`** — Clap derive structs only. Defines `Cli`, `Commands` enum, and per-command `*Command`/`*Args` structs. No logic.
- **`libs/ferriskey-cli-core`** — Command dispatch and execution. `run()` matches on `Commands` variants and delegates to module handlers (`context.rs`, `client.rs`). Owns config management.
- **`libs/ferriskey-client`** — `reqwest`-based HTTP client. `FerriskeyClient::new(base_url, prefix, token)` handles auth (Bearer token), request serialization, and response parsing.

### Data flow

```
CLI args → Cli::parse() → ferriskey_cli_core::run()
→ match Commands → handler (context/client)
→ FerriskeyClient → REST API
→ FileContextRepository → TOML config file
```

### Config storage

Contexts (URL, client_id, client_secret, optional realm) are stored as TOML at `$XDG_CONFIG_HOME/ferriskey/config.toml`. `FileContextRepository` does atomic writes (temp file + rename). `ContextStore` holds the map and tracks the active context.

### Output formatting

Root-level `--output` / `-o` flag accepts `table` (default), `json`, or `yaml`. Each handler has format-specific render functions.

### Unimplemented stubs

Realm and User commands are defined in `ferriskey-commands` but return `UnimplementedCommand` errors in `ferriskey-cli-core`. Client `get`/`delete` subcommands are also stubs.

### OAuth2 token exchange

`FerriskeyClient::exchange_client_credentials()` performs the client credentials flow before API calls that require authentication.
11 changes: 9 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ name = "ferriskey"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
description = "Official FerrisKey CLI"

[workspace]
members = ["libs/ferriskey-client", "libs/ferriskey-commands", "libs/ferriskey-cli-core"]
Expand All @@ -12,8 +16,11 @@ resolver = "2"
version = "0.1.0"
authors = ["nathaelb <[email protected]>"]
edition = "2024"
license = "Apache-2.0"
repository = "https://github.com/ferriskey/ferriskey/tree/main/cli"
homepage = "https://ferriskey.rs"

[dependencies]
clap = { version = "4.5", features = ["derive"] }
ferriskey-commands = { path = "libs/ferriskey-commands" }
ferriskey-cli-core = { path = "libs/ferriskey-cli-core" }
ferriskey-commands = { path = "libs/ferriskey-commands", version = "0.1.0" }
ferriskey-cli-core = { path = "libs/ferriskey-cli-core", version = "0.1.0" }
41 changes: 13 additions & 28 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,37 +1,24 @@
FROM rust:1.91.1-bookworm AS rust-build
FROM rust:1.95.0-bookworm AS chef

WORKDIR /usr/local/src/ferriskey

COPY Cargo.toml Cargo.lock ./
COPY libs/ferriskey-cli-core/Cargo.toml ./libs/ferriskey-cli-core/
COPY libs/ferriskey-client/Cargo.toml ./libs/ferriskey-client/
COPY libs/ferriskey-commands/Cargo.toml ./libs/ferriskey-commands/
RUN cargo install cargo-chef --version 0.1.77 --locked

FROM chef AS planner

COPY . .
RUN cargo chef prepare --recipe-path recipe.json

RUN \
mkdir -p src libs/ferriskey-cli-core/src libs/ferriskey-client/src libs/ferriskey-commands/src && \
touch libs/ferriskey-cli-core/src/lib.rs && \
touch libs/ferriskey-client/src/lib.rs && \
touch libs/ferriskey-commands/src/lib.rs && \
echo "fn main() {}" > src/main.rs && \
cargo build --release

COPY libs/ferriskey-cli-core libs/ferriskey-cli-core
COPY libs/ferriskey-client libs/ferriskey-client
COPY libs/ferriskey-commands libs/ferriskey-commands
FROM chef AS builder

COPY src src
COPY --from=planner /usr/local/src/ferriskey/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json

RUN \
COPY . .
RUN cargo build --release

touch libs/ferriskey-cli-core/src/lib.rs && \
touch libs/ferriskey-client/src/lib.rs && \
touch libs/ferriskey-commands/src/lib.rs && \
touch src/main.rs && \
cargo build --release

FROM debian:bookworm-slim AS cli
FROM debian:bookworm-slim AS runtime

RUN \
apt-get update && \
Expand All @@ -53,10 +40,8 @@ RUN \

USER ferriskey

FROM runtime AS api

COPY --from=rust-build /usr/local/src/ferriskey/target/release/ferriskey /usr/local/bin/
FROM runtime AS cli

EXPOSE 80
COPY --from=builder /usr/local/src/ferriskey/target/release/ferriskey /usr/local/bin/

ENTRYPOINT ["ferriskey"]
8 changes: 6 additions & 2 deletions libs/ferriskey-cli-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ name = "ferriskey-cli-core"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
description = "Application runtime for the FerrisKey CLI"

[lib]
doctest = false

[dependencies]
ferriskey-client = { path = "../ferriskey-client" }
ferriskey-commands = { path = "../ferriskey-commands" }
ferriskey-client = { path = "../ferriskey-client", version = "0.1.0" }
ferriskey-commands = { path = "../ferriskey-commands", version = "0.1.0" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"
Expand Down
Loading