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:
- clap argument structs,
- GitHub API calls, and
- 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
- 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.
- Convert
label end to end as the template.
- Convert the remaining 23 domains one at a time — each independently reviewable, build never breaking.
- 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.
Summary
Split
gorinto a reusablegor-corelibrary crate and a thingorCLI 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 thegorbinary or reimplementing the GitHub API layer themselves.Problem
Today
goris a single crate with alib.rs+main.rssplit, but the "library" only exposesGor::run()(parse args, dispatch). Every command insrc/cmd/*entangles three concerns:Operations return
()and print directly; there are no typed models (rawserde_json::Valuethroughout). A consumer crate that wants "list labels" gets printed text, notVec<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 agor-coreop → render.Locked decisions
gor-core(lib) +gor(bin)#[serde(flatten)] extracapture-restDependency split
gor-corekeeps:reqwest,serde,serde_json,serde_yaml_ng,keyring(optional),thiserror,dirs,gix,tracing.gor-coredrops (they move to thegorbin):clap,clap_complete,indicatif,console,anyhow.Reusable pattern (per domain)
Consumer usage:
Rules: ops return
Result<T, GorError>(neveranyhow, 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(wrapsgor_core::config), interactiveauthprompting (token exchange itself moves to core), andapi(thin raw-request passthrough).Migration plan
crates/gor-core, movecli.rs/output.rs(→render.rs) /main.rs/cmd/intocrates/gor. Compile green, no behavior change.labelend to end as the template.Testing
assert_cmd,insta) are the safety net — CLI behavior/output must not change.wiremockasserting typed data and correctGorErrorvariant mapping.gor-coreitems (crate keeps#![deny(missing_docs)]).Non-goals
reqwest::blocking.Notes / open questions
#![deny(missing_docs)]+unwrap_used = "deny"carry intogor-core— every public model field/op needs docs. Real cost, sequenced per-domain.gor-coreassumed; open to alternatives.Full design spec committed on branch
refactor/split-core-libraryatdocs/superpowers/specs/2026-07-20-core-library-split-design.md.