Skip to content
Open
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ cache the same queries run ~4.7× faster (e.g. `length` 966 ms →
drivers so the zero-file rule lives once.
- The emscripten (playground) build keeps the sequential engine —
no threads there.
## `toolpath-github`: the caller supplies the API token — 2026-08-14

- **`toolpath-github`** (0.7.0): breaking. Removed: `resolve_token`.
`DeriveConfig.token` is the only token input, so the caller owns
token resolution. The crate reads no environment variable and runs no
subprocess.
- **`path-cli`** (unreleased): `Config` reads `$GITHUB_TOKEN`.
`providers::github_token` returns that value when it is set and not
empty, and falls back to `gh auth token`. `p import github` and
`p list github` take the token from there, so both keep their
behavior for CLI users.

## `toolpath-pi`: the caller supplies the home directory — 2026-08-14

- **`toolpath-pi`** (0.7.0): breaking. `PathResolver::new(home)` takes
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ toolpath-codex = { version = "0.7.0", path = "crates/toolpath-codex" }
toolpath-copilot = { version = "0.2.0", path = "crates/toolpath-copilot" }
toolpath-opencode = { version = "0.6.0", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.3.0", path = "crates/toolpath-cursor" }
toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" }
toolpath-github = { version = "0.7.0", path = "crates/toolpath-github" }
toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" }
toolpath-md = { version = "0.7.0", path = "crates/toolpath-md" }
toolpath-pi = { version = "0.7.0", path = "crates/toolpath-pi" }
Expand Down
13 changes: 7 additions & 6 deletions crates/path-cli/src/cmd_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ fn derive(source: ImportSource, config: &Config) -> Result<Vec<DerivedDoc>> {
pr,
no_ci,
no_comments,
} => derive_github(url, repo, pr, no_ci, no_comments),
} => derive_github(url, repo, pr, no_ci, no_comments, config),
ImportSource::Claude {
project,
session,
Expand Down Expand Up @@ -386,10 +386,11 @@ fn derive_github(
pr: Option<u64>,
no_ci: bool,
no_comments: bool,
config: &Config,
) -> Result<Vec<DerivedDoc>> {
#[cfg(target_os = "emscripten")]
{
let _ = (url, repo, pr, no_ci, no_comments);
let _ = (url, repo, pr, no_ci, no_comments, config);
anyhow::bail!("'path import github' requires a native environment with network access");
}

Expand All @@ -413,15 +414,15 @@ fn derive_github(
);
};

let token = toolpath_github::resolve_token()?;
let config = toolpath_github::DeriveConfig {
token,
let derive_config = toolpath_github::DeriveConfig {
token: providers::github_token(config)?,
include_ci: !no_ci,
include_comments: !no_comments,
..Default::default()
};

let path = toolpath_github::derive_pull_request(&owner, &repo_name, pr_number, &config)?;
let path =
toolpath_github::derive_pull_request(&owner, &repo_name, pr_number, &derive_config)?;
let doc = Graph::from_path(path);
let cache_id = make_id("github", &format!("{owner}_{repo_name}-{pr_number}"));
Ok(vec![DerivedDoc {
Expand Down
13 changes: 6 additions & 7 deletions crates/path-cli/src/cmd_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ pub fn run(
let fmt = resolve_format(format, json_flag);
match source {
ListSource::Git { repo, remote } => run_git(repo, remote, fmt),
ListSource::Github { repo } => run_github(repo, fmt),
ListSource::Github { repo } => run_github(repo, fmt, config),
ListSource::Claude { project } => run_claude(project, fmt, config),
ListSource::Gemini { project } => run_gemini(project, fmt, config),
ListSource::Codex {} => run_codex(fmt, config),
Expand Down Expand Up @@ -197,10 +197,10 @@ fn run_git(repo_path: PathBuf, remote: String, fmt: ListFormat) -> Result<()> {

// ── GitHub ──────────────────────────────────────────────────────────────────

fn run_github(repo: String, fmt: ListFormat) -> Result<()> {
fn run_github(repo: String, fmt: ListFormat, config: &Config) -> Result<()> {
#[cfg(target_os = "emscripten")]
{
let _ = (repo, fmt);
let _ = (repo, fmt, config);
anyhow::bail!("'path list github' requires a native environment with network access");
}

Expand All @@ -210,13 +210,12 @@ fn run_github(repo: String, fmt: ListFormat) -> Result<()> {
.split_once('/')
.ok_or_else(|| anyhow::anyhow!("Repository must be in owner/repo format"))?;

let token = toolpath_github::resolve_token()?;
let config = toolpath_github::DeriveConfig {
token,
let derive_config = toolpath_github::DeriveConfig {
token: providers::github_token(config)?,
..Default::default()
};

let prs = toolpath_github::list_pull_requests(owner, repo_name, &config)?;
let prs = toolpath_github::list_pull_requests(owner, repo_name, &derive_config)?;

match fmt {
ListFormat::Json => {
Expand Down
5 changes: 5 additions & 0 deletions crates/path-cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ pub struct Config {
pub(crate) copilot_events_strict: Option<String>,
/// `$COPILOT_HOME`: Copilot CLI session root override.
pub(crate) copilot_home: Option<PathBuf>,
/// `$GITHUB_TOKEN`: GitHub API token (see `providers`).
pub(crate) github_token: Option<String>,
/// `$HOME`: config-root fallback and the harness resolvers' root.
pub(crate) home: Option<PathBuf>,
/// `$PATHBASE_URL`: Pathbase server override (see `cmd_pathbase`).
Expand Down Expand Up @@ -116,6 +118,7 @@ impl Config {
("CODEX_ROLLOUT_STRICT", "codex_rollout_strict"),
("COPILOT_EVENTS_STRICT", "copilot_events_strict"),
("COPILOT_HOME", "copilot_home"),
("GITHUB_TOKEN", "github_token"),
("HOME", "home"),
(PATHBASE_URL_ENV, "pathbase_url"),
(CONFIG_DIR_ENV, "toolpath_config_dir"),
Expand Down Expand Up @@ -212,6 +215,7 @@ mod tests {
jail.set_env("XDG_DATA_HOME", "/home/jailed/.local/share");
jail.set_env("COPILOT_EVENTS_STRICT", "1");
jail.set_env("COPILOT_HOME", "/home/jailed/.copilot");
jail.set_env("GITHUB_TOKEN", "gh-jailed-token");
jail.set_env("APPDATA", "/home/jailed/appdata");
jail.set_env(PATHBASE_URL_ENV, "https://pathbase.test");
jail.set_env("TOOLPATH_QUERY_EXPLAIN", "1");
Expand All @@ -225,6 +229,7 @@ mod tests {
codex_rollout_strict: Some("1".to_string()),
copilot_events_strict: Some("1".to_string()),
copilot_home: Some(PathBuf::from("/home/jailed/.copilot")),
github_token: Some("gh-jailed-token".to_string()),
home: Some(PathBuf::from("/home/jailed")),
pathbase_url: Some("https://pathbase.test".to_string()),
toolpath_config_dir: Some(PathBuf::from("/tmp/cfg-root")),
Expand Down
71 changes: 70 additions & 1 deletion crates/path-cli/src/providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@
//! cursor takes `$APPDATA` as an argument next to the home directory.
//! Only its Windows default user-data directory consults the value, so
//! the injection is gated to Windows.
//!
//! GitHub is a remote source: it takes an API token instead of a
//! resolver, and [`github_token`] builds it here.

use crate::config::Config;
#[cfg(not(target_os = "emscripten"))]
use crate::harness::HarnessBundle;

#[cfg(not(target_os = "emscripten"))]
use anyhow::{Context, bail};
use anyhow::{Result, anyhow};

fn missing_home(harness: &str) -> anyhow::Error {
Expand Down Expand Up @@ -140,6 +144,47 @@ pub(crate) fn require_pi_resolver(config: &Config) -> Result<toolpath_pi::PathRe
pi_resolver(config).ok_or_else(|| missing_home("Pi"))
}

/// The GitHub API token: `$GITHUB_TOKEN` when it is set and not empty,
/// otherwise the token the GitHub CLI holds.
#[cfg(not(target_os = "emscripten"))]
pub(crate) fn github_token(config: &Config) -> Result<String> {
github_token_or_else(config, gh_auth_token)
}

#[cfg(not(target_os = "emscripten"))]
fn github_token_or_else(
config: &Config,
fallback: impl FnOnce() -> Result<String>,
) -> Result<String> {
match config.github_token.as_deref() {
Some(token) if !token.is_empty() => Ok(token.to_string()),
_ => fallback(),
}
}

/// Read the token out of the GitHub CLI.
#[cfg(not(target_os = "emscripten"))]
fn gh_auth_token() -> Result<String> {
let output = std::process::Command::new("gh")
.args(["auth", "token"])
.output()
.context(
"Failed to run 'gh auth token'. Set GITHUB_TOKEN or install the GitHub CLI (gh).",
)?;

if output.status.success() {
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !token.is_empty() {
return Ok(token);
}
}

bail!(
"No GitHub token found. Set GITHUB_TOKEN environment variable \
or authenticate with 'gh auth login'."
)
}

/// The production [`HarnessBundle`], every provider built from
/// `config`. A provider whose resolver needs a home directory is
/// present only when `config` carries one; consumers skip the ones
Expand Down Expand Up @@ -369,6 +414,30 @@ mod tests {
);
}

#[test]
fn github_token_prefers_the_configured_value() {
let config = Config {
github_token: Some("configured".to_string()),
..Config::default()
};
let token = github_token_or_else(&config, || Ok("fallback".to_string())).unwrap();
assert_eq!(token, "configured");
}

#[test]
fn github_token_falls_back_when_unset_or_empty() {
let fallback = || Ok("fallback".to_string());
assert_eq!(
github_token_or_else(&Config::default(), fallback).unwrap(),
"fallback"
);
let config = Config {
github_token: Some(String::new()),
..Config::default()
};
assert_eq!(github_token_or_else(&config, fallback).unwrap(), "fallback");
}

#[test]
fn pi_resolver_roots_at_config_home() {
let resolver = pi_resolver(&config_with_home()).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion crates/toolpath-github/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "toolpath-github"
version = "0.6.0"
version = "0.7.0"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
7 changes: 4 additions & 3 deletions crates/toolpath-github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ are changes to `review://` artifacts; CI checks are changes to `ci://` artifacts

## Usage

The caller supplies the API token.

```rust,no_run
use toolpath_github::{derive_pull_request, resolve_token, DeriveConfig};
use toolpath_github::{derive_pull_request, DeriveConfig};

let token = resolve_token()?;
let token = std::env::var("GITHUB_TOKEN")?;
let config = DeriveConfig {
token,
include_ci: true,
Expand Down Expand Up @@ -58,7 +60,6 @@ to GitLab MRs, Gerrit, Phabricator, etc.

| Function | Description |
|---|---|
| `resolve_token()` | Resolve GitHub token from `GITHUB_TOKEN` or `gh auth token` |
| `derive_pull_request(owner, repo, pr, config)` | Derive a Path from a PR |
| `list_pull_requests(owner, repo, config)` | List PRs with summary info |
| `extract_issue_refs(body)` | Parse "Fixes #N" / "Closes #N" from text |
Expand Down
37 changes: 1 addition & 36 deletions crates/toolpath-github/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,41 +154,6 @@ mod native {

use super::{DeriveConfig, PullRequestInfo, extract_issue_refs};

// ====================================================================
// Auth
// ====================================================================

/// Resolve a GitHub API token.
///
/// Checks `GITHUB_TOKEN` environment variable first, then falls back to
/// `gh auth token` subprocess. Returns an error if neither works.
pub fn resolve_token() -> Result<String> {
if let Ok(token) = std::env::var("GITHUB_TOKEN")
&& !token.is_empty()
{
return Ok(token);
}

let output = std::process::Command::new("gh")
.args(["auth", "token"])
.output()
.context(
"Failed to run 'gh auth token'. Set GITHUB_TOKEN or install the GitHub CLI (gh).",
)?;

if output.status.success() {
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !token.is_empty() {
return Ok(token);
}
}

bail!(
"No GitHub token found. Set GITHUB_TOKEN environment variable \
or authenticate with 'gh auth login'."
)
}

// ====================================================================
// API Client
// ====================================================================
Expand Down Expand Up @@ -1685,7 +1650,7 @@ mod native {

// Re-export native-only functions at crate root for API compatibility
#[cfg(not(target_os = "emscripten"))]
pub use native::{derive_pull_request, list_pull_requests, resolve_token};
pub use native::{derive_pull_request, list_pull_requests};

#[cfg(test)]
mod tests {
Expand Down
2 changes: 1 addition & 1 deletion site/_data/crates.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
{
"name": "toolpath-github",
"version": "0.6.0",
"version": "0.7.0",
"description": "Derive from GitHub pull requests",
"docs": "https://docs.rs/toolpath-github",
"crate": "https://crates.io/crates/toolpath-github",
Expand Down
Loading