Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
110 commits
Select commit Hold shift + click to select a range
5d7fa7c
feat(schema): add githubRepo to virtual MCP metadata
tlgimenes Apr 9, 2026
95c80b0
feat(tools): add GITHUB_LIST_INSTALLATIONS app-only tool
tlgimenes Apr 9, 2026
06396c7
feat(tools): add GITHUB_LIST_REPOS app-only tool
tlgimenes Apr 9, 2026
6a7d832
feat(tools): register GitHub tools in tool registry
tlgimenes Apr 9, 2026
3f6dc5f
feat(ui): add GitHubRepoDialog for repo selection flow
tlgimenes Apr 9, 2026
6913612
feat(ui): add GitHubRepoButton component
tlgimenes Apr 9, 2026
21b986d
feat(ui): add GitHub button to shell header
tlgimenes Apr 9, 2026
9e5e8e2
fix(ui): use KEYS constants for query keys, fix TS error in dialog
tlgimenes Apr 9, 2026
0e182d7
fix(github): address code review findings
tlgimenes Apr 9, 2026
d59d01b
refactor(github): switch to device flow, remove Better Auth dependency
tlgimenes Apr 9, 2026
db35d0a
fix(github): use correct Deco CMS GitHub App client ID, remove debug …
tlgimenes Apr 9, 2026
f95aecf
feat(ui): start device flow immediately on button click, skip interme…
tlgimenes Apr 9, 2026
cbca5f0
feat(ui): auto-copy device code to clipboard, add copy button
tlgimenes Apr 9, 2026
1086edf
feat(github): auto-sync instructions and detect runtime on repo connect
tlgimenes Apr 10, 2026
c2fcaf0
feat(vm): add Freestyle VM preview with terminal and dev server iframe
tlgimenes Apr 10, 2026
47019f8
fix(vm): remove web terminal integration, fix error handling and stat…
tlgimenes Apr 10, 2026
fe1a327
fix(vm): add w-full to preview component for proper centering
tlgimenes Apr 10, 2026
70e3b0a
fix(vm): deduplicate VMs by (virtualMcpId, userId) pair
tlgimenes Apr 10, 2026
50bcb64
feat(vm): add Deno support, configurable port, detect port from scripts
tlgimenes Apr 10, 2026
1844be7
fix(vm): add open-in-new-tab button, fix domain fallback, guard empty…
tlgimenes Apr 10, 2026
a8bbca2
fix(vm): use style.dev domains instead of ports config
tlgimenes Apr 10, 2026
0e88b2d
fix(vm): add socat port proxy for localhost-bound dev servers
tlgimenes Apr 10, 2026
70277d9
fix(vm): use deco.studio domain for VM preview URLs
tlgimenes Apr 10, 2026
602d338
fix(vm): fix systemd exec quoting — split bash -c into separate args
tlgimenes Apr 10, 2026
4862e87
fix(vm): use wrapper shell scripts instead of inline bash -c commands
tlgimenes Apr 10, 2026
a603c34
fix(vm): install deno/bun via curl instead of Freestyle integrations
tlgimenes Apr 10, 2026
415bcc9
fix(vm): install runtimes to /usr/local and remove socat proxy
tlgimenes Apr 10, 2026
b285cb2
fix(vm): add iframe proxy to strip X-Frame-Options headers
tlgimenes Apr 10, 2026
a7f0a05
feat(preview): add visual editor mode with element selection and AI p…
tlgimenes Apr 10, 2026
a0968a6
fix(preview): purple highlight color + open chat on visual editor send
tlgimenes Apr 10, 2026
1da98b5
feat(vm): add shared VmEntry/VmMetadata types and patchActiveVms helper
tlgimenes Apr 10, 2026
3649a08
refactor(vm): use named VirtualMCPUpdateData type in patchActiveVms cast
tlgimenes Apr 10, 2026
cffb906
feat(vm): read/write active VM entries from Virtual MCP metadata in s…
tlgimenes Apr 10, 2026
2bc2897
test(vm): add VM_START unit tests for cached-VM and new-VM paths
tlgimenes Apr 10, 2026
a0a3409
feat(vm): derive vmId from metadata in VM_STOP, fix deletion order
tlgimenes Apr 10, 2026
e3efd62
test(vm): add VM_STOP unit tests
tlgimenes Apr 10, 2026
37c4a81
chore(vm): delete in-memory registry (replaced by DB-backed metadata)
tlgimenes Apr 10, 2026
36f6b9d
feat(vm): update VM_STOP call to pass only virtualMcpId
tlgimenes Apr 10, 2026
15eb88c
fix(vm): cast mock.calls to fix Bun type definition limitation in tests
tlgimenes Apr 10, 2026
333c402
feat(vm): add web terminal to VM creation via VmWebTerminal
tlgimenes Apr 10, 2026
b233b13
feat(preview): split view with resizable terminal panel
tlgimenes Apr 10, 2026
24b94c5
fix(vm): use pre-installed ttyd instead of VmWebTerminal to avoid ins…
tlgimenes Apr 10, 2026
8669783
fix(vm): manually install ttyd with retries instead of relying on VmW…
tlgimenes Apr 10, 2026
b7c477c
fix(vm): use base image ttyd on port 7681 instead of installing manually
tlgimenes Apr 10, 2026
338a748
style(preview): vscode-style thin separator between preview and termi…
tlgimenes Apr 10, 2026
4abb09e
fix(vm): install ttyd to /opt/ instead of /usr/local/bin/ to avoid re…
tlgimenes Apr 10, 2026
7e30115
fix(vm): install ttyd to /tmp/ to avoid overlay fs write restrictions
tlgimenes Apr 10, 2026
ffbd896
fix(vm): make terminal read-only and show dev-server logs instead of …
tlgimenes Apr 10, 2026
7a1f1de
fix(preview): detect stale VM (503) and reset to idle state instead o…
tlgimenes Apr 10, 2026
7f26098
fix(vm): server-side liveness probe clears stale VMs returning 503
tlgimenes Apr 10, 2026
eaf807e
fix(preview): force-reload iframe when dev server becomes ready to cl…
tlgimenes Apr 10, 2026
509e223
docs: add VM preview UX redesign spec
tlgimenes Apr 10, 2026
e63816d
docs: add suspended VM handling to UX redesign spec
tlgimenes Apr 10, 2026
1dc8f37
docs: update VM preview UX spec with critique feedback — fix blockers…
tlgimenes Apr 10, 2026
11d25ec
docs: restore suspended state + smart preview detection with concrete…
tlgimenes Apr 10, 2026
81f3454
docs: add VM_PROBE tool — frontend owns state, backend is proxy only
tlgimenes Apr 10, 2026
7e6f654
docs: add VM preview UX redesign implementation plan
tlgimenes Apr 10, 2026
a3bbfdd
feat(vm): add shared requireVmEntry and resolveRuntimeConfig helpers
tlgimenes Apr 10, 2026
e6235ff
feat(vm): add VM_PROBE tool for backend-proxied HEAD requests
tlgimenes Apr 10, 2026
c8fa84d
feat(vm): add VM_EXEC tool for running install/dev commands in a Free…
tlgimenes Apr 10, 2026
25a97d2
refactor(vm): strip VM_START to infrastructure-only systemd, add isNe…
tlgimenes Apr 10, 2026
bc2cb0a
refactor(vm): use requireVmEntry helper in VM_STOP
tlgimenes Apr 10, 2026
599120e
feat(preview): new state machine with terminal-first flow, preview de…
tlgimenes Apr 10, 2026
e3857ec
feat(preview): add terminal dropdown with reinstall/restart actions
tlgimenes Apr 10, 2026
67db7ff
feat(preview): show vmId as tooltip on stop button for debugging
tlgimenes Apr 10, 2026
2e1877d
style(preview): remove Terminal label from dropdown, keep icon only
tlgimenes Apr 10, 2026
bc169fa
style(preview): use shadcn Tooltip for vmId on stop button
tlgimenes Apr 10, 2026
171b463
fix(preview): add shadcn Tooltip to running-state stop button too
tlgimenes Apr 10, 2026
fc88837
style(preview): tooltip text 'Stop VM {vmId}' for clarity
tlgimenes Apr 10, 2026
507e260
refactor(preview): single stable React tree — iframes stay mounted, C…
tlgimenes Apr 10, 2026
4129b20
style(preview): show vmId as badge in address bar, remove tooltip fro…
tlgimenes Apr 10, 2026
f64d780
style(preview): add 'Stop VM' tooltip back on stop button
tlgimenes Apr 10, 2026
1888e7a
fix(preview): context-aware status labels — Installing/Resuming/Resta…
tlgimenes Apr 10, 2026
4720046
fix(preview): use 'Connecting...' label for creating state — works fo…
tlgimenes Apr 10, 2026
f2fb230
fix(vm): use freestyle vm.start() to resume VMs, vm.stop() for gracef…
tlgimenes Apr 10, 2026
6db3cd3
fix(vm): improve reinstall UX — inline errors, remove installing stat…
tlgimenes Apr 10, 2026
2532e70
refactor(vm): async VM_EXEC, replace ttyd with Node.js log viewer
tlgimenes Apr 10, 2026
842d596
fix(vm): deploy log viewer on resumed VMs via ensureLogViewer
tlgimenes Apr 10, 2026
74ad05d
fix(vm): use base64 encoding for ensureLogViewer, deduplicate log vie…
tlgimenes Apr 10, 2026
9760396
fix(vm): fix \n literal in log separators, use echo for newlines
tlgimenes Apr 10, 2026
09433a5
refactor(vm): use Freestyle runtime integrations, fix systemd exec an…
tlgimenes Apr 10, 2026
c7ad9e1
refactor(vm): replace custom log viewer with @freestyle-sh/with-web-t…
tlgimenes Apr 10, 2026
951d59a
refactor(vm): flatten plugins, move constants to module scope, use Sy…
tlgimenes Apr 10, 2026
1bb9740
fix(vm): use md5(virtualMcpId:userId) for VM domain generation
tlgimenes Apr 10, 2026
a4937b3
fix(vm): use VmSpec.with() for runtime/terminal integrations
tlgimenes Apr 10, 2026
9867a36
fix(vm): always include VmNodeJs in spec; fix terminal iframe height
tlgimenes Apr 11, 2026
1da6478
fix(vm): prepend runtime bin path for deno/bun exec commands
tlgimenes Apr 11, 2026
1164bb1
fix(vm): clear storage entry before freestyle delete; skip await on v…
tlgimenes Apr 11, 2026
8a5965f
fix(vm): log vm.delete() errors instead of swallowing them
tlgimenes Apr 11, 2026
7492def
refactor(vm): use VmSpec fluent API for repo, files, and systemd config
tlgimenes Apr 11, 2026
54ea974
fix(vm): restart dev server on VM resume instead of assuming it's hea…
tlgimenes Apr 11, 2026
112adb1
fix(vm): fix iframe-proxy startup and exec timeouts
tlgimenes Apr 11, 2026
252e8e1
fix(vm): fix iframe-proxy systemd service and VM deletion
tlgimenes Apr 11, 2026
9684fa5
fix(vm): source NVM from /etc/profile.d/nvm.sh in iframe-proxy wrapper
tlgimenes Apr 11, 2026
5568fae
fix(preview): reuse current thread in visual editor prompt and gate v…
tlgimenes Apr 11, 2026
da3cdec
refactor(preview): consolidate duplicate preview iframes into single …
tlgimenes Apr 11, 2026
972eba8
fix(vm): update start.test.ts mock to match VmSpec fluent API refactor
tlgimenes Apr 11, 2026
9bd5744
feat(github): paginate installations and repos list tools; fix test f…
tlgimenes Apr 11, 2026
b93091a
fix(vm): clear activeVms on repo change; use sticky+recreate; comment…
tlgimenes Apr 11, 2026
1a8abdc
refactor(vm): rename iframe-proxy to daemon; add SSE endpoint, log ta…
tlgimenes Apr 11, 2026
e08f291
test(vm): update start tests for daemon rename; fix stale terminalUrl…
tlgimenes Apr 11, 2026
1702f15
feat(vm): add useVmEvents SSE hook for daemon event stream
tlgimenes Apr 11, 2026
f1f4516
feat(vm): add custom VmTerminal component for log rendering
tlgimenes Apr 11, 2026
01c422b
feat(vm): replace polling with daemon SSE; add custom terminal; SSE-b…
tlgimenes Apr 11, 2026
e527833
chore(vm): remove VM_PROBE tool; daemon SSE replaces frontend polling
tlgimenes Apr 11, 2026
4fc6a46
fix(vm): deactivate visual editor overlays when switching to interact…
tlgimenes Apr 11, 2026
84e6e14
chore(vm): add ansi-to-html type declarations
tlgimenes Apr 11, 2026
12134c1
feat(vm): add 30-minute idle timeout and fix stop test mock
tlgimenes Apr 11, 2026
7c639d9
feat(vm): use Freestyle Git + GitHub Sync for private repo cloning
tlgimenes Apr 11, 2026
5efc2e8
fix(vm): handle oneshot service exit code in install chain and improv…
tlgimenes Apr 13, 2026
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
9 changes: 7 additions & 2 deletions apps/mesh/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@freestyle-sh/with-bun": "^0.2.10",
"@freestyle-sh/with-deno": "^0.0.2",
"@freestyle-sh/with-nodejs": "^0.2.8",
"@inkjs/ui": "^2.0.0",
"@modelcontextprotocol/ext-apps": "^1.2.2",
"@openrouter/ai-sdk-provider": "^2.2.5",
Expand All @@ -57,6 +60,7 @@
"ai-sdk-provider-claude-code": "^3.4.4",
"ai-sdk-provider-codex-cli": "^1.1.0",
"embedded-postgres": "^18.3.0-beta.16",
"freestyle-sandboxes": "^0.1.43",
"ink": "^6.8.0",
"kysely": "^0.28.12",
"nats": "^2.29.3",
Expand All @@ -73,13 +77,13 @@
"@decocms/better-auth": "1.5.17",
"@decocms/bindings": "workspace:*",
"@decocms/mcp-utils": "workspace:*",
"@jitl/quickjs-wasmfile-release-sync": "0.31.0",
"@decocms/mesh-sdk": "workspace:*",
"@decocms/runtime": "workspace:*",
"@decocms/vite-plugin": "workspace:*",
"@electric-sql/pglite": "^0.3.15",
"@floating-ui/react": "^0.27.16",
"@hookform/resolvers": "^5.2.2",
"@jitl/quickjs-wasmfile-release-sync": "0.31.0",
"@modelcontextprotocol/sdk": "1.27.1",
"@monaco-editor/react": "^4.7.0",
"@opentelemetry/api": "^1.9.0",
Expand Down Expand Up @@ -123,6 +127,7 @@
"@vercel/nft": "^1.1.1",
"@vitejs/plugin-react": "^5.1.0",
"ai": "^6.0.116",
"ansi-to-html": "^0.7.2",
"babel-plugin-react-compiler": "^1.0.0",
"better-auth": "1.4.5",
"class-variance-authority": "^0.7.1",
Expand All @@ -136,14 +141,14 @@
"jose": "^6.0.11",
"kysely-pglite": "^0.6.1",
"lucide-react": "^0.468.0",
"react-resizable-panels": "^2.1.7",
"marked": "^15.0.6",
"mesh-plugin-workflows": "workspace:*",
"nanoid": "^5.1.6",
"pg": "^8.16.3",
"prettier": "^3.4.2",
"react-hook-form": "^7.66.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.7",
"react-syntax-highlighter": "^15.6.1",
"recharts": "^3.6.0",
"rehype-raw": "^7.0.0",
Expand Down
94 changes: 94 additions & 0 deletions apps/mesh/src/tools/github/device-flow-poll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* GITHUB_DEVICE_FLOW_POLL Tool
*
* Polls GitHub Device Flow for access token.
* App-only tool — not visible to AI models.
*/

import { z } from "zod";
import { defineTool } from "../../core/define-tool";
import { requireAuth } from "../../core/mesh-context";

const GITHUB_CLIENT_ID = "Iv23liLNDj260RBdPV7p";

export const GITHUB_DEVICE_FLOW_POLL = defineTool({
name: "GITHUB_DEVICE_FLOW_POLL",
description: "Poll GitHub Device Flow for the access token.",
annotations: {
title: "Poll GitHub Device Flow",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
_meta: { ui: { visibility: "app" } },
inputSchema: z.object({
deviceCode: z.string().describe("Device code from the start step"),
}),
outputSchema: z.object({
status: z.enum(["pending", "success", "expired", "error"]),
token: z.string().nullable(),
error: z.string().nullable(),
}),

handler: async (input, ctx) => {
requireAuth(ctx);
await ctx.access.check();

const response = await fetch(
"https://github.com/login/oauth/access_token",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: GITHUB_CLIENT_ID,
device_code: input.deviceCode,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
},
);

if (!response.ok) {
return {
status: "error" as const,
token: null,
error: `HTTP ${response.status}`,
};
}

const data = (await response.json()) as {
access_token?: string;
error?: string;
error_description?: string;
};

if (data.access_token) {
return {
status: "success" as const,
token: data.access_token,
error: null,
};
}

if (data.error === "authorization_pending" || data.error === "slow_down") {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: slow_down is treated the same as authorization_pending, so callers cannot apply the required poll backoff and may keep hitting device-flow rate limits.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/tools/github/device-flow-poll.ts, line 76:

<comment>`slow_down` is treated the same as `authorization_pending`, so callers cannot apply the required poll backoff and may keep hitting device-flow rate limits.</comment>

<file context>
@@ -0,0 +1,94 @@
+      };
+    }
+
+    if (data.error === "authorization_pending" || data.error === "slow_down") {
+      return { status: "pending" as const, token: null, error: null };
+    }
</file context>
Fix with Cubic

return { status: "pending" as const, token: null, error: null };
}

if (data.error === "expired_token") {
return {
status: "expired" as const,
token: null,
error: "Device code expired",
};
}

return {
status: "error" as const,
token: null,
error: data.error_description ?? data.error ?? "Unknown error",
};
},
});
71 changes: 71 additions & 0 deletions apps/mesh/src/tools/github/device-flow-start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* GITHUB_DEVICE_FLOW_START Tool
*
* Starts GitHub Device Flow authentication.
* App-only tool — not visible to AI models.
*/

import { z } from "zod";
import { defineTool } from "../../core/define-tool";
import { requireAuth } from "../../core/mesh-context";

// Deco CMS GitHub App client ID (public, safe to hardcode)
const GITHUB_CLIENT_ID = "Iv23liLNDj260RBdPV7p";

export const GITHUB_DEVICE_FLOW_START = defineTool({
name: "GITHUB_DEVICE_FLOW_START",
description: "Start GitHub Device Flow authentication to get a user code.",
annotations: {
title: "Start GitHub Device Flow",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
_meta: { ui: { visibility: "app" } },
inputSchema: z.object({}),
outputSchema: z.object({
userCode: z.string(),
verificationUri: z.string(),
deviceCode: z.string(),
expiresIn: z.number(),
interval: z.number(),
}),

handler: async (_input, ctx) => {
requireAuth(ctx);
await ctx.access.check();

const response = await fetch("https://github.com/login/device/code", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
client_id: GITHUB_CLIENT_ID,
scope: "",
}),
});

if (!response.ok) {
throw new Error(`GitHub Device Flow error: ${response.status}`);
}

const data = (await response.json()) as {
device_code: string;
user_code: string;
verification_uri: string;
expires_in: number;
interval: number;
};

return {
userCode: data.user_code,
verificationUri: data.verification_uri,
deviceCode: data.device_code,
expiresIn: data.expires_in,
interval: data.interval,
};
},
});
56 changes: 56 additions & 0 deletions apps/mesh/src/tools/github/get-file-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* GITHUB_GET_FILE_CONTENT Tool
*
* Fetches raw file content from a GitHub repository.
* App-only tool — not visible to AI models.
*/

import { z } from "zod";
import { defineTool } from "../../core/define-tool";
import { requireAuth } from "../../core/mesh-context";

export const GITHUB_GET_FILE_CONTENT = defineTool({
name: "GITHUB_GET_FILE_CONTENT",
description: "Fetch raw file content from a GitHub repository by path.",
annotations: {
title: "Get GitHub File Content",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
_meta: { ui: { visibility: "app" } },
inputSchema: z.object({
token: z.string().describe("GitHub access token"),
owner: z.string().describe("Repository owner"),
repo: z.string().describe("Repository name"),
path: z.string().describe("File path within the repository"),
}),
outputSchema: z.object({
content: z.string().nullable(),
found: z.boolean(),
}),

handler: async (input, ctx) => {
requireAuth(ctx);
await ctx.access.check();

const url = `https://raw.githubusercontent.com/${input.owner}/${input.repo}/HEAD/${input.path}`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${input.token}`,
},
});

if (response.status === 404) {
return { content: null, found: false };
}

if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}

const content = await response.text();
return { content, found: true };
},
});
11 changes: 11 additions & 0 deletions apps/mesh/src/tools/github/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* GitHub Tools
*
* Tools for GitHub integration (app-only, not visible to AI models).
*/

export { GITHUB_LIST_INSTALLATIONS } from "./list-installations";
export { GITHUB_LIST_REPOS } from "./list-repos";
export { GITHUB_DEVICE_FLOW_START } from "./device-flow-start";
export { GITHUB_DEVICE_FLOW_POLL } from "./device-flow-poll";
export { GITHUB_GET_FILE_CONTENT } from "./get-file-content";
93 changes: 93 additions & 0 deletions apps/mesh/src/tools/github/list-installations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* GITHUB_LIST_INSTALLATIONS Tool
*
* Lists GitHub App installations of the Deco CMS app for the current user.
* App-only tool — not visible to AI models.
*/

import { z } from "zod";
import { defineTool } from "../../core/define-tool";
import { requireAuth } from "../../core/mesh-context";

const InstallationSchema = z.object({
installationId: z.number(),
orgName: z.string(),
avatarUrl: z.string().nullable(),
});

export const GITHUB_LIST_INSTALLATIONS = defineTool({
name: "GITHUB_LIST_INSTALLATIONS",
description:
"List GitHub App installations of the Deco CMS app for the current user.",
annotations: {
title: "List GitHub Installations",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
_meta: { ui: { visibility: "app" } },
inputSchema: z.object({
token: z.string().describe("GitHub access token"),
}),
outputSchema: z.object({
installations: z.array(InstallationSchema),
}),

handler: async (input, ctx) => {
requireAuth(ctx);
await ctx.access.check();

const headers = {
Authorization: `Bearer ${input.token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};

type GitHubInstallation = {
id: number;
account: {
login: string;
avatar_url: string;
};
};

const allInstallations: GitHubInstallation[] = [];
let nextUrl: string | undefined =
"https://api.github.com/user/installations?per_page=100";

while (nextUrl) {
const response: Response = await fetch(nextUrl, { headers });

if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}

const data = (await response.json()) as {
installations: GitHubInstallation[];
};
allInstallations.push(...data.installations);

// Parse Link header for next page
nextUrl = undefined;
const link = response.headers.get("link");
if (link) {
const next = link
.split(",")
.find((part: string) => part.includes('rel="next"'));
if (next) {
const match = next.match(/<([^>]+)>/);
if (match?.[1]) nextUrl = match[1];
}
}
}

const installations = allInstallations.map((inst) => ({
installationId: inst.id,
orgName: inst.account.login,
avatarUrl: inst.account.avatar_url,
}));

return { installations };
},
});
Loading
Loading