Skip to content

Split gor into reusable gor-core library + gor CLI (typed operations) #2

Description

@kerryhatcher

Summary

Split gor into a reusable gor-core library crate and a thin gor CLI crate so other Rust applications can depend on gor's GitHub functionality directly — calling typed operations that return structured data — instead of shelling out to the gor binary or reimplementing the GitHub API layer themselves.

Problem

Today gor is a single crate with a lib.rs + main.rs split, but the "library" only exposes Gor::run() (parse args, dispatch). Every command in src/cmd/* entangles three concerns:

  1. clap argument structs,
  2. GitHub API calls, and
  3. printing tables/JSON to stdout.

Operations return () and print directly; there are no typed models (raw serde_json::Value throughout). A consumer crate that wants "list labels" gets printed text, not Vec<Label>. The only reuse paths available are shelling out to the CLI or reimplementing the API layer.

Proposed solution

A Cargo workspace with two crates:

  • gor-core — publishable library: Client, auth, host, config, repository detection, keyring, error types, and one module per GitHub domain holding both its lean typed model and its typed operations. No CLI dependencies.
  • gor — binary: clap definitions, rendering (tables + JSON), and thin handlers that resolve args → call a gor-core op → render.

Locked decisions

Axis Decision
Reuse target Typed operations returning models, not just a raw client
Packaging Cargo workspace: gor-core (lib) + gor (bin)
Scope Everything — all 24 command domains converted
Model fidelity Lean structs: hand-written fields gor uses + #[serde(flatten)] extra capture-rest

Dependency split

gor-core keeps: reqwest, serde, serde_json, serde_yaml_ng, keyring (optional), thiserror, dirs, gix, tracing.

gor-core drops (they move to the gor bin): clap, clap_complete, indicatif, console, anyhow.

Reusable pattern (per domain)

#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Label {
    pub name: String,
    pub color: String,
    pub description: Option<String>,
    #[serde(flatten)]
    pub extra: serde_json::Map<String, serde_json::Value>, // capture-rest
}

pub fn list(client: &Client, spec: &RepoSplit, opts: &ListOptions)
    -> Result<Vec<Label>, GorError>;

Consumer usage:

let client = gor_core::client::Client::new("github.com")?;
let spec = gor_core::repository::parse_repo_spec("owner/repo")?;
let labels = gor_core::label::list(&client, &spec, &Default::default())?;

Rules: ops return Result<T, GorError> (never anyhow, never print); status→error mapping (404 → NotFound, etc.) lives in the op; models are #[non_exhaustive] + capture-rest so API additions don't break consumers.

Stays bin-only (no reusable API surface)

completion, alias, browse, extension, config (wraps gor_core::config), interactive auth prompting (token exchange itself moves to core), and api (thin raw-request passthrough).

Migration plan

  1. Create the workspace; move shared core files into crates/gor-core, move cli.rs / output.rs (→ render.rs) / main.rs / cmd/ into crates/gor. Compile green, no behavior change.
  2. Convert label end to end as the template.
  3. Convert the remaining 23 domains one at a time — each independently reviewable, build never breaking.
  4. Delete dead helpers left in the bin.

Testing

  • Existing CLI integration tests (assert_cmd, insta) are the safety net — CLI behavior/output must not change.
  • Add per-op unit tests against wiremock asserting typed data and correct GorError variant mapping.
  • Doctests on public gor-core items (crate keeps #![deny(missing_docs)]).

Non-goals

  • No new commands or features.
  • No change to CLI flags, output format, or config format.
  • No async conversion — stays reqwest::blocking.
  • No full faithful GitHub models (every documented field) — lean structs + capture-rest instead.

Notes / open questions

  • #![deny(missing_docs)] + unwrap_used = "deny" carry into gor-core — every public model field/op needs docs. Real cost, sequenced per-domain.
  • Library crate name gor-core assumed; open to alternatives.

Full design spec committed on branch refactor/split-core-library at docs/superpowers/specs/2026-07-20-core-library-split-design.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions