Documentation
¶
Overview ¶
Package github holds the host-side GitHub integration: the credential store, the device-flow runtime, the GitHub API client, and the orchestration that combines them for "create a PR from a worktree" requests.
Credentials live in ~/.local/share/clank/github.json — sibling to the Anthropic sink in internal/host/auth.go. The credential never travels through clank's infrastructure: the device flow runs between this process and github.com directly, and PR creation calls the GitHub API from this process too. The gateway is a pure proxy for the connect-flow status and PR creation requests.
Index ¶
- Constants
- Variables
- func ExistingURLFromError(err error) string
- func GitCredentialHelperValue(executable string, canUseGhCLIAuth bool) string
- func ParseGitHubRemote(remoteURL string) (owner, repo string, err error)
- func RemoteHost(remoteURL string) string
- func RunGitCredentialHelper(action string, in io.Reader, out io.Writer, store *Store, canUseGhCLIAuth bool) error
- type Branch
- type CheckRollup
- type CheckRollupState
- type CreatePRInput
- type CreateRepoInput
- type CreatedRepo
- type Credentials
- type DeviceFlowStart
- type DeviceFlowState
- type DeviceFlowStatus
- type Manager
- func (m *Manager) AccessToken() (string, error)
- func (m *Manager) CanUseGhCLIAuth() bool
- func (m *Manager) CancelConnect(_ context.Context, flowID string) error
- func (m *Manager) CheckRollupForRef(ctx context.Context, token, owner, repo, ref string) (*CheckRollup, error)
- func (m *Manager) ClientID() string
- func (m *Manager) ConnectStatus(_ context.Context, flowID string) (DeviceFlowStatus, error)
- func (m *Manager) CreatePullRequest(ctx context.Context, accessToken, owner, repo string, in CreatePRInput) (PullRequest, error)
- func (m *Manager) CreateRepository(ctx context.Context, token string, in CreateRepoInput) (CreatedRepo, error)
- func (m *Manager) Disconnect(_ context.Context) error
- func (m *Manager) EnableGhCLIFallback()
- func (m *Manager) FindOpenPRForBranch(ctx context.Context, accessToken, owner, repo, branch string) (*PullRequest, error)
- func (m *Manager) GetPullRequest(ctx context.Context, token, owner, repo string, number int) (PullRequestDetails, error)
- func (m *Manager) GetRepository(ctx context.Context, token, owner, repo string) (RepositoryDetails, error)
- func (m *Manager) HTTPClient() *http.Client
- func (m *Manager) IsAvailable() bool
- func (m *Manager) ListBranches(ctx context.Context, token, owner, repo string) ([]Branch, error)
- func (m *Manager) ListPullRequests(ctx context.Context, token, owner, repo string, state PRListState) ([]PullRequestSummary, error)
- func (m *Manager) ListRepositories(ctx context.Context, token string) ([]Repo, error)
- func (m *Manager) ListTemplateRepositories(ctx context.Context, token string) ([]TemplateRepo, error)
- func (m *Manager) MarkPRReadyForReview(ctx context.Context, accessToken, owner, repo string, number int) error
- func (m *Manager) PRMergeable(ctx context.Context, token, owner, repo string, number int) (MergeableState, error)
- func (m *Manager) SetAPIBaseURL(u string)
- func (m *Manager) SetAuthBaseURL(u string)
- func (m *Manager) SetHTTPClient(c *http.Client)
- func (m *Manager) SetPollSafetyMargin(d time.Duration)
- func (m *Manager) StartConnect(ctx context.Context) (DeviceFlowStart, error)
- func (m *Manager) Status(_ context.Context) (Status, error)
- func (m *Manager) Store() *Store
- func (m *Manager) StoredLogin() string
- type MergeableState
- type PRListState
- type PullRequest
- type PullRequestDetails
- type PullRequestSummary
- type Repo
- type RepositoryDetails
- type Status
- type Store
- type TemplateRepo
Constants ¶
const ( // CredentialSourceStore is a token from clank's own device-flow // store — connected/disconnected through clank. CredentialSourceStore = "store" // CredentialSourceGhCLI is a token borrowed live from the // machine's gh CLI login. clank stores nothing and cannot // disconnect it — that's `gh auth logout`'s job — so clients // should hide the disconnect affordance for this source. CredentialSourceGhCLI = "gh_cli" )
Status is the wire shape returned from GET /credentials/github/status. CredentialSource values for Status.Source.
const ClientIDEnv = "CLANK_GITHUB_OAUTH_CLIENT_ID"
ClientIDEnv is the environment variable carrying the Clank GitHub OAuth App's client_id. When unset, the Manager reports available:false and refuses to start the device flow. The env-var approach lets sprite provisioners set it without code changes; the laptop's clankd forwards it from its own environment.
const GitCredentialGhCLIAuthFlag = "--gh-cli-auth"
GitCredentialGhCLIAuthFlag lets the helper borrow the machine's gh login.
Variables ¶
var ( // ErrNotConfigured is returned by StartConnect when the host has // no CLANK_GITHUB_OAUTH_CLIENT_ID. The mux maps it to 503. ErrNotConfigured = errors.New("github: client_id not configured (set CLANK_GITHUB_OAUTH_CLIENT_ID)") // ErrUnknownFlow is returned when a status/cancel call targets a // flow the manager has no record of (typo, expired TTL, or a // fresh start has bumped the slot). ErrUnknownFlow = errors.New("github: unknown flow id") )
Errors returned by the device-flow surface. Map to specific HTTP statuses in the mux handlers — see github_connect.go.
var ( // ErrPRAlreadyExists is GitHub's 422 "A pull request already // exists for X:Y" error. ExistingURL is the URL of the open PR // we found via the follow-up search. ErrPRAlreadyExists = errors.New("github pr: already exists for this head") // ErrPRBaseNotFound covers 422 errors where the base branch // doesn't exist on the remote. Distinct from "already exists" so // the UI can show the right hint. ErrPRBaseNotFound = errors.New("github pr: base branch not found") // ErrPRTokenInvalid is GitHub's 401 — typically means the token // was revoked or expired. UI should prompt the user to reconnect. ErrPRTokenInvalid = errors.New("github pr: token invalid or revoked") // ErrPRForbidden is GitHub's 403 — token can't write to this // repo (e.g. lacks `repo` scope, or repo doesn't accept PRs from // this account). ErrPRForbidden = errors.New("github pr: forbidden") )
Errors returned by CreatePullRequest. ErrPRAlreadyExists carries the existing PR URL so the mux handler can surface it in the response body — the UI uses it to deep-link instead of showing a dead-end "conflict" error.
var ( ErrRepositoryNotFound = errors.New("github: repository not found") ErrRepositoryForbidden = errors.New("github: repository forbidden") ErrRepositoryTokenInvalid = errors.New("github: repository token invalid") )
var ErrNotConnected = errors.New("github: not connected")
ErrNotConnected reports that no GitHub credential is stored on this host — the user hasn't completed the device flow. Distinct from ErrNotConfigured (the host has no OAuth client_id at all).
var ErrNotGitHubRemote = errors.New("not a github.com remote")
ErrNotGitHubRemote is returned by ParseGitHubRemote when the URL doesn't point to github.com. v1 supports github.com only; GHE and non-GitHub forges are deferred.
var ErrPullRequestNotFound = errors.New("github: pull request not found")
ErrPullRequestNotFound reports that GitHub did not expose the requested PR.
var ErrRepoNameTaken = errors.New("github: a repository with that name already exists on this account")
ErrRepoNameTaken is GitHub's 422 when the authenticated user already owns a repository with the requested name.
Functions ¶
func ExistingURLFromError ¶
ExistingURLFromError extracts the existing-PR URL from an error returned by CreatePullRequest. Returns "" when the error isn't the "already exists" case or carries no URL.
func GitCredentialHelperValue ¶
GitCredentialHelperValue routes a repo's auth prompts to this binary. The executable is quoted so paths with spaces survive the helper shell.
func ParseGitHubRemote ¶
ParseGitHubRemote extracts owner and repo from a github.com remote URL. Accepts the three forms git uses in practice:
https://github.com/owner/repo(.git)? git@github.com:owner/repo(.git)? ssh://git@github.com/owner/repo(.git)?
Anything else returns ErrNotGitHubRemote, including non-github.com hosts and URLs that don't have both an owner and a repo segment.
func RemoteHost ¶
RemoteHost returns just the host portion of a git remote URL, regardless of whether the host is github.com. Used by the PR preview endpoint to surface the actual host in "non_github" error messages (e.g. "Origin points to gitlab.com").
Returns the empty string when the URL is unparseable. Accepts the same three URL shapes as ParseGitHubRemote.
func RunGitCredentialHelper ¶
func RunGitCredentialHelper(action string, in io.Reader, out io.Writer, store *Store, canUseGhCLIAuth bool) error
RunGitCredentialHelper implements the helper protocol over in/out for one invocation. Only `get` for protocol=https host=github.com is answered, and only when an allowed credential source has a token. Every other case prints nothing so git treats it as "no credential". Store read errors are returned rather than hidden as disconnection.
Types ¶
type Branch ¶
Branch is the trimmed branch shape returned to clients for the import branch picker. Protected mirrors GitHub's branch-protection flag so the UI can badge protected branches; the picker marks the repo's default using the default_branch already carried by the repos list.
type CheckRollup ¶
type CheckRollup struct {
State CheckRollupState `json:"state"`
Passed int `json:"passed"`
Failed int `json:"failed"`
Pending int `json:"pending"`
Total int `json:"total"`
}
CheckRollup aggregates a commit's check runs: the overall State plus the per-bucket counts (Passed + Failed + Pending == Total).
type CheckRollupState ¶
type CheckRollupState string
CheckRollupState is the one-glance CI verdict for a commit.
const ( CheckStatePassing CheckRollupState = "passing" CheckStateFailing CheckRollupState = "failing" CheckStatePending CheckRollupState = "pending" )
type CreatePRInput ¶
type CreatePRInput struct {
Title string `json:"title"`
Body string `json:"body"`
Head string `json:"head"`
Base string `json:"base"`
Draft bool `json:"draft,omitempty"`
}
CreatePRInput is the request body for CreatePullRequest. Head is just the branch name — same-owner PRs only in v1; cross-fork support would require "owner:branch" syntax.
type CreateRepoInput ¶
CreateRepoInput is the request for CreateRepository. Creating a PRIVATE repo needs the token's `repo` scope — the connect flow already requests it.
type CreatedRepo ¶
type CreatedRepo struct {
Owner string `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Private bool `json:"private"`
HTMLURL string `json:"html_url"`
CloneURL string `json:"clone_url"`
}
CreatedRepo is the trimmed shape returned after a successful create.
type Credentials ¶
type Credentials struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitzero"`
Scopes []string `json:"scopes,omitempty"`
GitHubLogin string `json:"github_login,omitempty"`
GitHubUserID int64 `json:"github_user_id,omitempty"`
InstalledAt time.Time `json:"installed_at,omitzero"`
}
Credentials is the on-disk shape at Store.Path(). Only AccessToken is required; the other fields are best-effort metadata captured when the device flow completes so the UI can show "@login" without a round-trip to GitHub.
type DeviceFlowStart ¶
type DeviceFlowStart struct {
FlowID string `json:"flow_id"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri"`
VerificationURIComplete string `json:"verification_uri_complete"`
ExpiresAt time.Time `json:"expires_at"`
Interval int `json:"interval"`
}
DeviceFlowStart is the response from StartConnect. UserCode and VerificationURIComplete are what the client (mobile/TUI) shows to the user — opening VerificationURIComplete in a browser brings up the GitHub page with UserCode pre-filled.
type DeviceFlowState ¶
type DeviceFlowState string
DeviceFlowState is the state a flow is in. Mirrors the agent.DeviceFlow alphabet but lives in this package so the github surface stays decoupled from the broader provider catalog.
const ( FlowPending DeviceFlowState = "pending" FlowSuccess DeviceFlowState = "success" FlowDenied DeviceFlowState = "denied" FlowExpired DeviceFlowState = "expired" FlowError DeviceFlowState = "error" FlowCanceled DeviceFlowState = "canceled" )
type DeviceFlowStatus ¶
type DeviceFlowStatus struct {
State DeviceFlowState `json:"state"`
Error string `json:"error,omitempty"`
GitHubLogin string `json:"github_login,omitempty"`
}
DeviceFlowStatus is the polled state of an in-progress flow. GitHubLogin is populated only when State is "success" — same atomic step that wrote the credential file fetched the login.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns this host's GitHub integration: the credential store, the in-progress device flow (if any), and the HTTP client used for outbound calls to github.com. Single Manager per host.Service.
Device-flow plumbing lives in device_flow.go; PR creation lives in pr.go (PR 3). Base URLs are overridable via SetAuthBaseURL / SetAPIBaseURL so tests can swap an httptest.Server.
func NewManager ¶
NewManager constructs a Manager rooted at homeDir with the given OAuth App client_id. Empty clientID is allowed — the manager will report available:false and refuse to start the device flow, which is the correct behavior for hosts that haven't been configured with a GitHub integration (e.g. self-hosters who didn't set the env var, or laptop dev runs without one).
func (*Manager) AccessToken ¶
AccessToken returns the GitHub access token every API and authed-git flow should use, or ErrNotConnected when none is available. Centralizes the "connected?" check so callers don't re-implement it. An explicit clank connection (the store) always wins; with the gh CLI fallback enabled, a laptop host borrows the machine's own gh login instead of demanding a second device flow.
func (*Manager) CanUseGhCLIAuth ¶
CanUseGhCLIAuth reports whether this host may borrow the machine's gh login.
func (*Manager) CancelConnect ¶
CancelConnect signals the polling goroutine to stop and transitions the flow to "canceled". Idempotent — calling cancel on a flow that's already terminal is a no-op.
func (*Manager) CheckRollupForRef ¶
func (m *Manager) CheckRollupForRef(ctx context.Context, token, owner, repo, ref string) (*CheckRollup, error)
CheckRollupForRef aggregates the latest check runs on ref (a SHA or branch). Returns nil when the commit has no check runs at all — "no CI configured" renders as nothing, not as green.
func (*Manager) ClientID ¶
ClientID returns the configured OAuth App client_id; empty when the manager is unavailable.
func (*Manager) ConnectStatus ¶
ConnectStatus returns the current state of flowID. Pure read. ErrUnknownFlow when the ID doesn't match the active slot (either it never existed, or a newer StartConnect replaced it).
func (*Manager) CreatePullRequest ¶
func (m *Manager) CreatePullRequest(ctx context.Context, accessToken, owner, repo string, in CreatePRInput) (PullRequest, error)
CreatePullRequest opens a PR on the named repo with the given token. On 422 "already exists", looks up the existing PR via a follow-up GET and returns ErrPRAlreadyExists with the URL embedded.
func (*Manager) CreateRepository ¶
func (m *Manager) CreateRepository(ctx context.Context, token string, in CreateRepoInput) (CreatedRepo, error)
CreateRepository creates a repository owned by the authenticated user (the empty org argument means "the token's user"). AutoInit is false on purpose: we push the worktree's existing history, so an initial commit on GitHub's side would make the first push a non-fast-forward.
func (*Manager) Disconnect ¶
Disconnect removes the stored credential. Idempotent: missing file is not an error.
func (*Manager) EnableGhCLIFallback ¶
func (m *Manager) EnableGhCLIFallback()
EnableGhCLIFallback lets AccessToken (and Status) fall back to the machine's own gh CLI login when no clank connection exists. A deployment decision, not a default: the local laptop provisioner enables it — the host IS the user's machine, where agent sessions could run `gh auth token` themselves, so borrowing it exposes nothing new — while sandboxes keep token access explicit. Call once at wiring time, before the manager serves requests.
func (*Manager) FindOpenPRForBranch ¶
func (m *Manager) FindOpenPRForBranch(ctx context.Context, accessToken, owner, repo, branch string) (*PullRequest, error)
FindOpenPRForBranch returns the open PR whose head is owner:branch, or nil when none exists. (nil, nil) means "no open PR"; a non-nil error is a real API/transport failure the caller can treat as best-effort.
func (*Manager) GetPullRequest ¶
func (m *Manager) GetPullRequest(ctx context.Context, token, owner, repo string, number int) (PullRequestDetails, error)
GetPullRequest returns one PR. An empty token deliberately makes an anonymous request, which is sufficient for public repositories.
func (*Manager) GetRepository ¶
func (m *Manager) GetRepository(ctx context.Context, token, owner, repo string) (RepositoryDetails, error)
GetRepository returns one repository. An empty token deliberately makes an anonymous request so public repositories do not require a GitHub connection.
func (*Manager) HTTPClient ¶
HTTPClient returns the configured client. PRs 2/3 use this for outbound GitHub calls so tests can swap it via SetHTTPClient.
func (*Manager) IsAvailable ¶
IsAvailable reports whether GitHub Connect is enabled on this host. Returns false when ClientIDEnv was unset at startup.
func (*Manager) ListBranches ¶
ListBranches lists the branches of owner/repo as returned by GitHub, capped at maxBranches. token authenticates the call (private repos need it). Mirrors ListRepositories' paginate-and-trim approach.
func (*Manager) ListPullRequests ¶
func (m *Manager) ListPullRequests(ctx context.Context, token, owner, repo string, state PRListState) ([]PullRequestSummary, error)
ListPullRequests lists owner/repo's pull requests in the given state, most recently updated first, capped at maxPulls. token authenticates the call (private repos need it). Mirrors ListBranches' paginate-and-trim approach.
func (*Manager) ListRepositories ¶
ListRepositories lists repositories the authenticated user can access (owner, collaborator, and org-member affiliations), most recently pushed first, capped at maxRepos. token authenticates the call.
func (*Manager) ListTemplateRepositories ¶
func (m *Manager) ListTemplateRepositories(ctx context.Context, token string) ([]TemplateRepo, error)
ListTemplateRepositories lists repositories OWNED by the authenticated user that are marked as templates, most recently pushed first, capped at maxRepos. Owner-only affiliation is the v1 product boundary: "your own templates" — org/community sources come later with their own trust story. Membership in this listing is also the create-time authorization: the gateway only creates from ids it can find here (or in the operator catalog).
func (*Manager) MarkPRReadyForReview ¶
func (m *Manager) MarkPRReadyForReview(ctx context.Context, accessToken, owner, repo string, number int) error
MarkPRReadyForReview flips PR `number` from draft to ready-for-review. Idempotent: an already-ready PR is a successful no-op (no mutation is sent).
func (*Manager) PRMergeable ¶
func (m *Manager) PRMergeable(ctx context.Context, token, owner, repo string, number int) (MergeableState, error)
PRMergeable reports whether PR number merges cleanly into its base.
func (*Manager) SetAPIBaseURL ¶
SetAPIBaseURL overrides the api.github.com base URL for user info and PR creation.
func (*Manager) SetAuthBaseURL ¶
SetAuthBaseURL overrides the github.com base URL used for the device-flow and token-exchange calls. Tests set this to httptest.Server.URL; production leaves it at the default.
func (*Manager) SetHTTPClient ¶
SetHTTPClient overrides the client used for outbound GitHub calls. Tests use this to point at an httptest.Server simulating github.com.
func (*Manager) SetPollSafetyMargin ¶
SetPollSafetyMargin overrides the slack added to each device-flow poll interval. Defaults to 3s (matches RFC 8628 §3.4 guidance); tests set 0 so the flow polls at GitHub's returned cadence.
func (*Manager) StartConnect ¶
func (m *Manager) StartConnect(ctx context.Context) (DeviceFlowStart, error)
StartConnect kicks off the device flow against GitHub and spawns a goroutine that polls until completion. Returns the user-facing fields the client renders. Any in-flight flow is canceled first — single-slot registry per host.
func (*Manager) Status ¶
Status returns the current connection state. Reads the credential file on every call — cheap and avoids stale-cache bugs across disconnect/reconnect. With the gh CLI fallback enabled, a host with no stored token but a logged-in gh still reports connected (source gh_cli, login unknown — identity isn't part of gh's token handoff).
func (*Manager) Store ¶
Store returns the credential store. Exposed for PRs 2/3 that need to write/read credentials from the device-flow goroutine and the PR creation path.
func (*Manager) StoredLogin ¶
StoredLogin returns the connected account's login when clank knows it, "" otherwise (no connection, or a gh CLI-borrowed token — identity isn't part of gh's token handoff). Best-effort: callers use it for annotations like is_mine, never for auth decisions.
type MergeableState ¶
type MergeableState string
MergeableState is whether a PR can merge cleanly into its base.
const ( MergeableStateMergeable MergeableState = "mergeable" MergeableStateConflicting MergeableState = "conflicting" // MergeableStateUnknown means GitHub hasn't computed the test merge // yet. The GET itself kicks the computation off, so a later refresh // resolves it — callers should treat unknown as "not yet", not retry // inline. MergeableStateUnknown MergeableState = "unknown" )
type PRListState ¶
type PRListState string
PRListState selects which pull requests ListPullRequests returns.
const ( PRListStateOpen PRListState = "open" PRListStateClosed PRListState = "closed" )
type PullRequest ¶
type PullRequest struct {
Number int `json:"number"`
HTMLURL string `json:"html_url"`
Draft bool `json:"draft"`
Head struct {
SHA string `json:"sha"`
} `json:"head"`
Base struct {
Ref string `json:"ref"`
} `json:"base"`
}
PullRequest mirrors the subset of GitHub's response we surface upstream — the broader go-github type leaks through the package boundary otherwise.
type PullRequestDetails ¶
type PullRequestDetails struct {
Number int `json:"number"`
Title string `json:"title"`
HTMLURL string `json:"html_url"`
HeadOwner string `json:"head_owner"`
HeadRepo string `json:"head_repo"`
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
BaseBranch string `json:"base_branch"`
Author string `json:"author"`
IsPrivate bool `json:"is_private"`
}
PullRequestDetails identifies the exact code revision behind a GitHub PR.
type PullRequestSummary ¶
type PullRequestSummary struct {
Number int `json:"number"`
Title string `json:"title"`
State string `json:"state"`
Draft bool `json:"draft"`
HTMLURL string `json:"html_url"`
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
BaseBranch string `json:"base_branch"`
Author string `json:"author"`
UpdatedAt time.Time `json:"updated_at,omitzero"`
MergedAt time.Time `json:"merged_at,omitzero"`
}
PullRequestSummary is the trimmed PR shape returned to clients. HeadBranch is what the UI cross-references against loaded worktrees (to link a PR to a branch you already have) or forks from (to check a PR out). MergedAt is non-zero only for merged PRs — GitHub reports merged PRs as state "closed", so this is the merged-vs-abandoned discriminator.
type Repo ¶
type Repo struct {
Owner string `json:"owner"`
Name string `json:"name"`
FullName string `json:"full_name"`
Private bool `json:"private"`
DefaultBranch string `json:"default_branch"`
Description string `json:"description,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitzero"`
}
Repo is the trimmed repository shape returned to clients. FullName is "owner/name"; Owner/Name are split out so the import endpoint can take them as discrete fields without re-parsing.
type RepositoryDetails ¶
type RepositoryDetails struct {
Owner string `json:"owner"`
Name string `json:"name"`
HTMLURL string `json:"html_url"`
Description string `json:"description"`
DefaultBranch string `json:"default_branch"`
IsPrivate bool `json:"is_private"`
}
RepositoryDetails is the repository metadata needed before Clank imports code.
type Status ¶
type Status struct {
Available bool `json:"available"`
Connected bool `json:"connected"`
Source string `json:"source,omitempty"` // CredentialSource* when connected
GitHubLogin string `json:"github_login,omitempty"`
Scopes []string `json:"scopes,omitempty"`
InstalledAt time.Time `json:"installed_at,omitzero"`
}
`available` is the host-level capability flag; `connected` is the per-user "do we have a token" flag. Both are needed by the UI so it can distinguish "this host doesn't support GitHub" from "this host supports it but you haven't connected yet."
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store reads and writes Credentials atomically. Safe for concurrent use; in practice only one writer ever exists per host (the device flow's polling goroutine).
func NewStore ¶
NewStore constructs a Store rooted at homeDir. The credential file lives at <homeDir>/.local/share/clank/github.json.
func (*Store) Delete ¶
Delete removes the credential file. Missing file is not an error — disconnect is idempotent so the UI doesn't have to think about "I already disconnected" races.
func (*Store) IsConnected ¶
IsConnected reports whether a usable access token is stored. Convenience wrapper around Read for callers that don't care about the rest of the credential.
func (*Store) Read ¶
func (s *Store) Read() (Credentials, error)
Read returns the persisted credentials. A missing file is treated as "not connected" — returns a zero Credentials with nil error so callers can use IsConnected to branch without an error check.
func (*Store) Write ¶
func (s *Store) Write(c Credentials) error
Write replaces the credential atomically: write a sibling .tmp, then rename. A reader observing the file at any instant sees either the previous credential or the new one — never partial JSON.
type TemplateRepo ¶
TemplateRepo is one entry in the template listing: the trimmed repo shape plus the clone URL the gateway needs for create-time resolution. The clone URL never travels past the gateway.