mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 06:04:43 +00:00
Fix Foundry UI bugs: org names, sessions, and repo selection (#250)
* Fix Foundry auth: migrate to Better Auth adapter, fix access token retrieval - Remove @ts-nocheck from better-auth.ts, auth-user/index.ts, app-shell.ts and fix all type errors - Fix getAccessTokenForSession: read GitHub token directly from account record instead of calling Better Auth's internal /get-access-token endpoint which returns 403 on server-side calls - Re-implement workspaceAuth helper functions (workspaceAuthColumn, normalizeAuthValue, workspaceAuthClause, workspaceAuthWhere) that were accidentally deleted - Remove all retry logic (withRetries, isRetryableAppActorError) - Implement CORS origin allowlist from configured environment - Document cachedAppWorkspace singleton pattern - Add inline org sync fallback in buildAppSnapshot for post-OAuth flow - Add no-retry rule to CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add Foundry dev panel from fix-git-data branch Port the dev panel component that was left out when PR #243 was replaced by PR #247. Adapted to remove runtime/mock-debug references that don't exist on the current branch. - Toggle with Shift+D, persists visibility to localStorage - Shows context, session, GitHub sync status sections - Dev-only (import.meta.env.DEV) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add full Docker image defaults, fix actor deadlocks, and improve dev experience - Add Dockerfile.full and --all flag to install-agent CLI for pre-built images - Centralize Docker image constant (FULL_IMAGE) pinned to 0.3.1-full - Remove examples/shared/Dockerfile{,.dev} and daytona snapshot example - Expand Docker docs with full runnable Dockerfile - Fix self-deadlock in createWorkbenchSession (fire-and-forget provisioning) - Audit and convert 12 task actions from wait:true to wait:false - Add bun --hot for dev backend hot reload - Remove --force from pnpm install in dev Dockerfile for faster startup - Add env_file support to compose.dev.yaml for automatic credential loading - Add mock frontend compose config and dev panel - Update CLAUDE.md with wait:true policy and dev environment setup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * WIP: async action fixes and interest manager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix Foundry UI bugs: org names, hanging sessions, and wrong repo creation - Fix org display name using GitHub description instead of name field - Fix createWorkbenchSession hanging when sandbox is provisioning - Fix auto-session creation retry storm on errors - Fix task creation using wrong repo due to React state race conditions - Remove Bun hot-reload from backend Dockerfile (causes port drift) - Add GitHub sync/install status to dev panel Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
58c54156f1
commit
d8b8b49f37
88 changed files with 9252 additions and 1933 deletions
1332
foundry/scripts/data/rivet-dev.json
Normal file
1332
foundry/scripts/data/rivet-dev.json
Normal file
File diff suppressed because it is too large
Load diff
290
foundry/scripts/pull-org-data.ts
Normal file
290
foundry/scripts/pull-org-data.ts
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Pull public GitHub organization data into a JSON fixture file.
|
||||
*
|
||||
* This script mirrors the sync logic in the backend workspace actor
|
||||
* (see: packages/backend/src/actors/workspace/app-shell.ts — syncGithubOrganizations
|
||||
* and syncGithubOrganizationRepos). Keep the two in sync: when the backend
|
||||
* sync workflow changes what data it fetches or how it structures organizations,
|
||||
* update this script to match.
|
||||
*
|
||||
* Key difference from the backend sync: this script only fetches **public** data
|
||||
* from the GitHub API (no auth token required, no private repos). It is used to
|
||||
* populate realistic mock/test data for the Foundry frontend without needing
|
||||
* GitHub OAuth credentials or a GitHub App installation.
|
||||
*
|
||||
* Usage:
|
||||
* bun foundry/scripts/pull-org-data.ts <org-login> [--out <path>]
|
||||
*
|
||||
* Examples:
|
||||
* bun foundry/scripts/pull-org-data.ts rivet-gg
|
||||
* bun foundry/scripts/pull-org-data.ts rivet-gg --out foundry/scripts/data/rivet-gg.json
|
||||
*/
|
||||
|
||||
import { parseArgs } from "node:util";
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
// ── Types matching the backend sync output ──
|
||||
// See: packages/shared/src/app-shell.ts
|
||||
|
||||
interface OrgFixtureRepo {
|
||||
fullName: string;
|
||||
cloneUrl: string;
|
||||
description: string | null;
|
||||
language: string | null;
|
||||
stars: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface OrgFixtureMember {
|
||||
id: string;
|
||||
login: string;
|
||||
avatarUrl: string;
|
||||
role: "admin" | "member";
|
||||
}
|
||||
|
||||
interface OrgFixturePullRequest {
|
||||
number: number;
|
||||
title: string;
|
||||
state: "open";
|
||||
draft: boolean;
|
||||
headRefName: string;
|
||||
author: string;
|
||||
repoFullName: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface OrgFixture {
|
||||
/** ISO timestamp of when this data was pulled */
|
||||
pulledAt: string;
|
||||
/** GitHub organization login (e.g. "rivet-gg") */
|
||||
login: string;
|
||||
/** GitHub numeric ID */
|
||||
id: number;
|
||||
/** Display name */
|
||||
name: string | null;
|
||||
/** Organization description */
|
||||
description: string | null;
|
||||
/** Public email */
|
||||
email: string | null;
|
||||
/** Blog/website URL */
|
||||
blog: string | null;
|
||||
/** Avatar URL */
|
||||
avatarUrl: string;
|
||||
/** Public repositories (excludes forks by default) */
|
||||
repos: OrgFixtureRepo[];
|
||||
/** Public members (only those with public membership) */
|
||||
members: OrgFixtureMember[];
|
||||
/** Open pull requests across all public repos */
|
||||
openPullRequests: OrgFixturePullRequest[];
|
||||
}
|
||||
|
||||
// ── GitHub API helpers ──
|
||||
// Mirrors the pagination approach in packages/backend/src/services/app-github.ts
|
||||
|
||||
const API_BASE = "https://api.github.com";
|
||||
const GITHUB_TOKEN = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? null;
|
||||
|
||||
function authHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"User-Agent": "foundry-pull-org-data/1.0",
|
||||
};
|
||||
if (GITHUB_TOKEN) {
|
||||
headers["Authorization"] = `Bearer ${GITHUB_TOKEN}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function githubGet<T>(url: string): Promise<T> {
|
||||
const response = await fetch(url, { headers: authHeaders() });
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => "");
|
||||
throw new Error(`GitHub API ${response.status}: ${url}\n${body.slice(0, 500)}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
function parseNextLink(linkHeader: string | null): string | null {
|
||||
if (!linkHeader) return null;
|
||||
for (const part of linkHeader.split(",")) {
|
||||
const [urlPart, relPart] = part.split(";").map((v) => v.trim());
|
||||
if (urlPart && relPart?.includes('rel="next"')) {
|
||||
return urlPart.replace(/^<|>$/g, "");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function githubPaginate<T>(path: string): Promise<T[]> {
|
||||
let url: string | null = `${API_BASE}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
const items: T[] = [];
|
||||
|
||||
while (url) {
|
||||
const response = await fetch(url, { headers: authHeaders() });
|
||||
if (!response.ok) {
|
||||
const body = await response.text().catch(() => "");
|
||||
throw new Error(`GitHub API ${response.status}: ${url}\n${body.slice(0, 500)}`);
|
||||
}
|
||||
const page = (await response.json()) as T[];
|
||||
items.push(...page);
|
||||
url = parseNextLink(response.headers.get("link"));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// ── Main ──
|
||||
|
||||
async function pullOrgData(orgLogin: string): Promise<OrgFixture> {
|
||||
console.log(`Fetching organization: ${orgLogin}`);
|
||||
|
||||
// 1. Fetch org profile
|
||||
// Backend equivalent: getViewer() + listOrganizations() derive org identity
|
||||
const org = await githubGet<{
|
||||
id: number;
|
||||
login: string;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
email: string | null;
|
||||
blog: string | null;
|
||||
avatar_url: string;
|
||||
public_repos: number;
|
||||
public_members_url: string;
|
||||
}>(`${API_BASE}/orgs/${orgLogin}`);
|
||||
|
||||
console.log(` ${org.name ?? org.login} — ${org.public_repos} public repos`);
|
||||
|
||||
// 2. Fetch public repos (non-fork, non-archived)
|
||||
// Backend equivalent: listInstallationRepositories() or listUserRepositories()
|
||||
// Key difference: we only fetch public repos here (type=public)
|
||||
const rawRepos = await githubPaginate<{
|
||||
full_name: string;
|
||||
clone_url: string;
|
||||
description: string | null;
|
||||
language: string | null;
|
||||
stargazers_count: number;
|
||||
updated_at: string;
|
||||
fork: boolean;
|
||||
archived: boolean;
|
||||
private: boolean;
|
||||
}>(`/orgs/${orgLogin}/repos?per_page=100&type=public&sort=updated`);
|
||||
|
||||
const repos: OrgFixtureRepo[] = rawRepos
|
||||
.filter((r) => !r.fork && !r.archived && !r.private)
|
||||
.map((r) => ({
|
||||
fullName: r.full_name,
|
||||
cloneUrl: r.clone_url,
|
||||
description: r.description,
|
||||
language: r.language,
|
||||
stars: r.stargazers_count,
|
||||
updatedAt: r.updated_at,
|
||||
}))
|
||||
.sort((a, b) => b.stars - a.stars);
|
||||
|
||||
console.log(` ${repos.length} public repos (excluding forks/archived)`);
|
||||
|
||||
// 3. Fetch public members
|
||||
// Backend equivalent: members are derived from the OAuth user + org membership
|
||||
// Here we can only see members with public membership visibility
|
||||
const rawMembers = await githubPaginate<{
|
||||
id: number;
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
}>(`/orgs/${orgLogin}/members?per_page=100`);
|
||||
|
||||
const members: OrgFixtureMember[] = rawMembers.map((m) => ({
|
||||
id: String(m.id),
|
||||
login: m.login,
|
||||
avatarUrl: m.avatar_url,
|
||||
role: "member" as const,
|
||||
}));
|
||||
|
||||
console.log(` ${members.length} public members`);
|
||||
|
||||
// 4. Fetch open PRs across all public repos
|
||||
// Backend equivalent: ProjectPrSyncActor polls GitHub for open PRs per repo
|
||||
// and stores them in the pr_cache table on the project actor
|
||||
const openPullRequests: OrgFixturePullRequest[] = [];
|
||||
for (const repo of repos) {
|
||||
const rawPrs = await githubPaginate<{
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
draft: boolean;
|
||||
head: { ref: string };
|
||||
user: { login: string } | null;
|
||||
updated_at: string;
|
||||
}>(`/repos/${repo.fullName}/pulls?state=open&per_page=100`);
|
||||
|
||||
for (const pr of rawPrs) {
|
||||
openPullRequests.push({
|
||||
number: pr.number,
|
||||
title: pr.title,
|
||||
state: "open",
|
||||
draft: pr.draft,
|
||||
headRefName: pr.head.ref,
|
||||
author: pr.user?.login ?? "unknown",
|
||||
repoFullName: repo.fullName,
|
||||
updatedAt: pr.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
if (rawPrs.length > 0) {
|
||||
console.log(` ${repo.fullName}: ${rawPrs.length} open PRs`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` ${openPullRequests.length} total open PRs`);
|
||||
|
||||
return {
|
||||
pulledAt: new Date().toISOString(),
|
||||
login: org.login,
|
||||
id: org.id,
|
||||
name: org.name,
|
||||
description: org.description,
|
||||
email: org.email,
|
||||
blog: org.blog,
|
||||
avatarUrl: org.avatar_url,
|
||||
repos,
|
||||
members,
|
||||
openPullRequests,
|
||||
};
|
||||
}
|
||||
|
||||
// ── CLI ──
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
out: { type: "string", short: "o" },
|
||||
help: { type: "boolean", short: "h" },
|
||||
},
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (values.help || positionals.length === 0) {
|
||||
console.log("Usage: bun foundry/scripts/pull-org-data.ts <org-login> [--out <path>]");
|
||||
console.log("");
|
||||
console.log("Pulls public GitHub organization data into a JSON fixture file.");
|
||||
console.log("Set GITHUB_TOKEN or GH_TOKEN to avoid rate limits.");
|
||||
process.exit(positionals.length === 0 && !values.help ? 1 : 0);
|
||||
}
|
||||
|
||||
const orgLogin = positionals[0]!;
|
||||
const defaultOutDir = resolve(import.meta.dirname ?? ".", "data");
|
||||
const outPath = values.out ?? resolve(defaultOutDir, `${orgLogin}.json`);
|
||||
|
||||
try {
|
||||
const data = await pullOrgData(orgLogin);
|
||||
|
||||
mkdirSync(dirname(outPath), { recursive: true });
|
||||
writeFileSync(outPath, JSON.stringify(data, null, 2) + "\n");
|
||||
|
||||
console.log(`\nWrote ${outPath}`);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue