From e8b1ad0c666c37a166d847ac02004ea6078b2431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Wed, 17 Jun 2026 12:52:34 +0300 Subject: [PATCH 1/2] fix(cli): surface real GitHub status on skill download failures Map GitHub API failures (401/403/404) to actionable messages instead of a bare "GitHub API error", so an expired GITHUB_TOKEN/GH_TOKEN is distinguishable from a rate limit. --- .changeset/surface-github-api-error.md | 5 +++++ packages/cli/src/utils/github.ts | 28 +++++++++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 .changeset/surface-github-api-error.md diff --git a/.changeset/surface-github-api-error.md b/.changeset/surface-github-api-error.md new file mode 100644 index 000000000..ac4788abb --- /dev/null +++ b/.changeset/surface-github-api-error.md @@ -0,0 +1,5 @@ +--- +"ctx7": patch +--- + +Surface the real GitHub status when skill downloads fail. A bare "GitHub API error" now becomes an actionable message distinguishing 401 (invalid/expired token), 403 (rate limit), and 404 (repo/branch not found), so users can tell an expired GITHUB_TOKEN/GH_TOKEN apart from a rate limit. diff --git a/packages/cli/src/utils/github.ts b/packages/cli/src/utils/github.ts index 5ef14d7f5..1f11766fa 100644 --- a/packages/cli/src/utils/github.ts +++ b/packages/cli/src/utils/github.ts @@ -152,15 +152,32 @@ function getGitHubHeaders(): Record { }; } +// Turns a GitHub HTTP status into an actionable message. The common failures +// here are an expired/invalid token (401) shadowing a working `gh` login, or +// the anonymous rate limit (403); both previously surfaced as a bare +// "GitHub API error" with no way to tell them apart. +function describeGitHubError(status: number): string { + switch (status) { + case 401: + return "GitHub API error: 401 (invalid or expired token; check GITHUB_TOKEN/GH_TOKEN, or run `gh auth login`)"; + case 403: + return "GitHub API error: 403 (rate limited; set GITHUB_TOKEN/GH_TOKEN, or run `gh auth login` for higher limits)"; + case 404: + return "GitHub API error: 404 (repository or branch not found)"; + default: + return `GitHub API error: ${status}`; + } +} + async function fetchRepoTree( owner: string, repo: string, branch: string, headers: Record -): Promise { +): Promise { const treeUrl = `${GITHUB_API}/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`; const response = await fetch(treeUrl, { headers }); - if (!response.ok) return null; + if (!response.ok) return { status: response.status }; return (await response.json()) as GitHubTreeResponse; } @@ -193,7 +210,8 @@ export async function listSkillsFromGitHub(project: string): Promise item.type === "blob" && item.path.toLowerCase().endsWith("skill.md") @@ -250,8 +268,8 @@ export async function downloadSkillFromGitHub( const ghHeaders = getGitHubHeaders(); const treeData = await fetchRepoTree(owner, repo, branch, ghHeaders); - if (!treeData) { - return { files: [], error: `GitHub API error` }; + if ("status" in treeData) { + return { files: [], error: describeGitHubError(treeData.status) }; } const skillFiles = treeData.tree.filter( From c4b7dbdc1de7c9046b78723537aa8a26f2442609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahreddin=20=C3=96zcan?= Date: Wed, 17 Jun 2026 12:57:44 +0300 Subject: [PATCH 2/2] feat(cli): add actionable tip line for GitHub auth/rate-limit errors On 401/403 skill download failures, setup now prints a tip: line pointing users at GITHUB_TOKEN/GH_TOKEN or `gh auth login`, mirroring the existing EACCES tip. --- .changeset/surface-github-api-error.md | 2 +- packages/cli/src/commands/setup.ts | 7 +++++++ packages/cli/src/utils/github.ts | 29 ++++++++++++++++++++------ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/.changeset/surface-github-api-error.md b/.changeset/surface-github-api-error.md index ac4788abb..1db4181d5 100644 --- a/.changeset/surface-github-api-error.md +++ b/.changeset/surface-github-api-error.md @@ -2,4 +2,4 @@ "ctx7": patch --- -Surface the real GitHub status when skill downloads fail. A bare "GitHub API error" now becomes an actionable message distinguishing 401 (invalid/expired token), 403 (rate limit), and 404 (repo/branch not found), so users can tell an expired GITHUB_TOKEN/GH_TOKEN apart from a rate limit. +Surface the real GitHub status when skill downloads fail. A bare "GitHub API error" now becomes an actionable message distinguishing 401 (invalid/expired token), 403 (rate limit), and 404 (repo/branch not found), and setup prints a `tip:` line telling users to refresh GITHUB_TOKEN/GH_TOKEN or run `gh auth login`, so an expired token is no longer mistaken for a rate limit. diff --git a/packages/cli/src/commands/setup.ts b/packages/cli/src/commands/setup.ts index 4039a4600..66edad2eb 100644 --- a/packages/cli/src/commands/setup.ts +++ b/packages/cli/src/commands/setup.ts @@ -10,6 +10,7 @@ import { log } from "../utils/logger.js"; import { checkboxWithHover } from "../utils/prompts.js"; import { trackEvent } from "../utils/tracking.js"; import { getBaseUrl, downloadSkill } from "../utils/api.js"; +import { githubErrorTip } from "../utils/github.js"; import { installSkillFiles } from "../utils/installer.js"; import { performLogin } from "./auth.js"; import { saveTokens, getValidAccessToken } from "../utils/auth.js"; @@ -428,6 +429,8 @@ async function setupMcp(agents: SetupAgent[], options: SetupOptions, scope: Scop ` ${pc.yellow("tip:")} fix permissions with: ${pc.cyan(`sudo chown -R $(whoami) ${dirname(dirname(r.skillPath))}`)}` ); } + const skillTip = githubErrorTip(r.skillStatus); + if (skillTip) log.plain(` ${pc.yellow("tip:")} ${skillTip}`); } log.blank(); @@ -483,6 +486,8 @@ async function setupCli(options: SetupOptions): Promise { const downloadData = await downloadSkill("/upstash/context7", "find-docs"); if (downloadData.error || downloadData.files.length === 0) { spinner.fail(`Failed to download find-docs skill: ${downloadData.error || "no files"}`); + const tip = githubErrorTip(downloadData.error); + if (tip) log.plain(` ${pc.yellow("tip:")} ${tip}`); return; } @@ -516,6 +521,8 @@ async function setupCli(options: SetupOptions): Promise { ` ${pc.yellow("tip:")} fix permissions with: ${pc.cyan(`sudo chown -R $(whoami) ${dirname(dirname(r.skillPath))}`)}` ); } + const skillTip = githubErrorTip(r.skillStatus); + if (skillTip) log.plain(` ${pc.yellow("tip:")} ${skillTip}`); const ruleIcon = r.ruleStatus === "installed" || r.ruleStatus === "updated" ? pc.green("+") : pc.dim("~"); log.plain(` ${ruleIcon} Rule ${r.ruleStatus}`); diff --git a/packages/cli/src/utils/github.ts b/packages/cli/src/utils/github.ts index 1f11766fa..a717f80f1 100644 --- a/packages/cli/src/utils/github.ts +++ b/packages/cli/src/utils/github.ts @@ -152,16 +152,17 @@ function getGitHubHeaders(): Record { }; } -// Turns a GitHub HTTP status into an actionable message. The common failures -// here are an expired/invalid token (401) shadowing a working `gh` login, or -// the anonymous rate limit (403); both previously surfaced as a bare -// "GitHub API error" with no way to tell them apart. +// Turns a GitHub HTTP status into a short, recognizable message. The common +// failures are an expired/invalid token (401) shadowing a working `gh` login, +// or the anonymous rate limit (403); both previously surfaced as a bare +// "GitHub API error" with no way to tell them apart. The actionable fix is +// returned separately by `githubErrorTip` so callers can render it as a hint. function describeGitHubError(status: number): string { switch (status) { case 401: - return "GitHub API error: 401 (invalid or expired token; check GITHUB_TOKEN/GH_TOKEN, or run `gh auth login`)"; + return "GitHub API error: 401 (invalid or expired token)"; case 403: - return "GitHub API error: 403 (rate limited; set GITHUB_TOKEN/GH_TOKEN, or run `gh auth login` for higher limits)"; + return "GitHub API error: 403 (rate limited)"; case 404: return "GitHub API error: 404 (repository or branch not found)"; default: @@ -169,6 +170,22 @@ function describeGitHubError(status: number): string { } } +/** + * Returns an actionable hint for a GitHub error message produced by + * `describeGitHubError`, or `undefined` when there's nothing useful to add. + * Meant to be shown as a `tip:` line below the failure. + */ +export function githubErrorTip(error: string | undefined): string | undefined { + if (!error) return undefined; + if (error.includes("401")) { + return "your GitHub token is invalid or expired; refresh GITHUB_TOKEN/GH_TOKEN or run `gh auth login`"; + } + if (error.includes("403")) { + return "GitHub rate limit reached; set GITHUB_TOKEN/GH_TOKEN or run `gh auth login` for higher limits"; + } + return undefined; +} + async function fetchRepoTree( owner: string, repo: string,