From 43451481bd8a4e7e982604f5a6e1293d2e3a90ab Mon Sep 17 00:00:00 2001 From: Alex Sexton Date: Mon, 13 Oct 2025 20:18:47 -0500 Subject: [PATCH] add skeleton files to shadcn registry --- .../github/installations/route.ts | 124 +-------------- apps/docs/app/api/code-storage/repo/route.ts | 64 +------- apps/docs/registry.json | 21 +++ bun.lock | 3 + packages/storage-elements-next/package.json | 6 +- .../src/github/installations.ts | 142 ++++++++++++++++++ packages/storage-elements-next/src/index.ts | 2 + .../storage-elements-next/src/repo/route.ts | 80 ++++++++++ .../api/github/callback/route.ts | 13 ++ .../api/github/installations/route.ts | 8 + .../git-platform-sync/api/repo/route.ts | 11 ++ .../git-platform-sync/pages/success/page.tsx | 94 ++++++++++++ packages/storage-elements/tsconfig.json | 4 + 13 files changed, 394 insertions(+), 178 deletions(-) create mode 100644 packages/storage-elements-next/src/github/installations.ts create mode 100644 packages/storage-elements-next/src/repo/route.ts create mode 100644 packages/storage-elements/blocks/git-platform-sync/api/github/callback/route.ts create mode 100644 packages/storage-elements/blocks/git-platform-sync/api/github/installations/route.ts create mode 100644 packages/storage-elements/blocks/git-platform-sync/api/repo/route.ts create mode 100644 packages/storage-elements/blocks/git-platform-sync/pages/success/page.tsx diff --git a/apps/docs/app/api/code-storage/github/installations/route.ts b/apps/docs/app/api/code-storage/github/installations/route.ts index eda381a3b..140ec2535 100644 --- a/apps/docs/app/api/code-storage/github/installations/route.ts +++ b/apps/docs/app/api/code-storage/github/installations/route.ts @@ -1,124 +1,8 @@ -import type { components } from '@octokit/openapi-types'; -import { type NextRequest, NextResponse } from 'next/server'; +import { CodeStorageGitHubInstallations } from '@pierre/storage-elements-next'; +import { type NextRequest } from 'next/server'; -type Installation = components['schemas']['installation']; - -type Owner = components['schemas']['simple-user'] & - components['schemas']['enterprise']; - -type InstallationResponse = { - installations: Installation[]; -}; - -type FilteredInstallation = { - id: string; - app_id: string; - app_slug: string; - permissions: Record; - owner_id: string | null; - events: string[]; - type: string | null; -}; - -type FilteredOwner = { - id: string; - login: string; - type: string | null; - avatar_url: string | null; -}; - -function filterInstallations(installations: Installation[]) { - return installations.map((installation) => { - return { - id: `${installation.id}`, - app_id: `${installation.app_id}`, - app_slug: installation.app_slug, - owner_id: - installation.account != null && 'id' in installation.account - ? `${installation.account.id}` - : null, - permissions: installation.permissions, - events: installation.events, - type: - installation.account != null && 'type' in installation.account - ? installation.account.type - : null, - } satisfies FilteredInstallation; - }); -} - -function filterOwners(owners: Owner[]) { - return owners.map((owner) => { - return { - id: `${owner.id}`, - login: owner.login, - type: owner.type, - avatar_url: owner.avatar_url, - } satisfies FilteredOwner; - }); -} - -function getOwnersFromInstallations(installations: Installation[]) { - return installations - .map((installation) => installation.account) - .filter((account): account is Owner => account !== null); -} +const installations = new CodeStorageGitHubInstallations({}); export async function GET(request: NextRequest) { - const token = request.cookies.get('github_token')?.value; - - if (token == null || token.trim() === '') { - return NextResponse.json({ data: { installations: [], owners: [] } }); - } - - try { - const response = await fetch(`https://api.github.com/user/installations`, { - headers: { - Authorization: `Bearer ${token}`, - Accept: 'application/vnd.github.v3+json', - }, - }); - - if (!response.ok) { - return NextResponse.json( - { error: 'Failed to fetch installations' }, - { status: 400 } - ); - } - - const data = (await response.json()) as InstallationResponse; - - if ((data?.installations?.length ?? 0) > 0) { - return NextResponse.json({ - data: { - installations: [], - owners: [], - }, - }); - } - - const filteredInstallations = filterInstallations(data.installations); - - const filteredOwners = filterOwners( - getOwnersFromInstallations(data.installations) - ); - - return NextResponse.json({ - data: { - installations: filteredInstallations, - owners: filteredOwners, - }, - // for debugging this can be nice - // _raw: data.installations, - }); - } catch (error) { - console.error('Error fetching installations:', error); - return NextResponse.json( - { - error: 'Failed to fetch installations', - errorMessage: error instanceof Error ? error.message : 'Unknown error', - }, - { status: 500 } - ); - } + return installations.handleRequest(request); } diff --git a/apps/docs/app/api/code-storage/repo/route.ts b/apps/docs/app/api/code-storage/repo/route.ts index 4abe14e4a..dc5ba1a0a 100644 --- a/apps/docs/app/api/code-storage/repo/route.ts +++ b/apps/docs/app/api/code-storage/repo/route.ts @@ -1,59 +1,11 @@ -import { GitStorage } from '@pierre/storage'; -import { type NextRequest, NextResponse } from 'next/server'; +import { CodeStorageRepo } from '@pierre/storage-elements-next'; +import { type NextRequest } from 'next/server'; -export async function POST(request: NextRequest) { - try { - const store = new GitStorage({ - name: 'pierre', - key: process.env.CODE_STORAGE_SYNC_PRIVATE_KEY ?? '', - }); - - const body = await request.json(); - const { owner, name, defaultBranch } = body; - - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!owner || !name) { - return NextResponse.json( - { success: false, error: 'Repository owner and name are required' }, - { status: 400 } - ); - } - - // TODO: Authenticate the user for this operation +const repo = new CodeStorageRepo({ + storageName: 'pierre', + privateKey: process.env.CODE_STORAGE_SYNC_PRIVATE_KEY!, +}); - const repo = await store.createRepo({ - baseRepo: { - owner, - name, - // NOTE(amadeus): Given these types are `any`, not sure the safest way - // to convert fix them... - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing - defaultBranch: defaultBranch || 'main', // Optional, defaults to 'main' - }, - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing - defaultBranch: defaultBranch || 'main', // Optional, defaults to 'main' for the Git Storage repo - }); - - const ciUrl = await repo.getRemoteURL({ - permissions: ['git:read', 'git:write'], - ttl: 86400, // 24 hours - }); - - return NextResponse.json({ - success: true, - url: ciUrl, - repository: { - owner, - name, - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing - defaultBranch: defaultBranch || 'main', - }, - }); - } catch (error) { - console.error('Error syncing storage:', error); - return NextResponse.json( - { error: 'Failed to sync storage' }, - { status: 500 } - ); - } +export async function POST(request: NextRequest) { + return repo.handlePostRequest(request); } diff --git a/apps/docs/registry.json b/apps/docs/registry.json index 58c0d99c9..f4c173a11 100644 --- a/apps/docs/registry.json +++ b/apps/docs/registry.json @@ -17,6 +17,7 @@ "select", "tooltip" ], + "dependencies": ["@pierre/storage-elements-next"], "files": [ { "path": "registry/new-york/blocks/git-platform-sync/git-platform-sync.tsx", @@ -37,6 +38,26 @@ { "path": "registry/new-york/blocks/git-platform-sync/github/owners.ts", "type": "registry:component" + }, + { + "path": "registry/new-york/blocks/git-platform-sync/pages/success/page.tsx", + "type": "registry:page", + "target": "code-storage/success/page.tsx" + }, + { + "path": "registry/new-york/blocks/git-platform-sync/api/github/callback/route.ts", + "type": "registry:page", + "target": "api/code-storage/github/callback/route.ts" + }, + { + "path": "registry/new-york/blocks/git-platform-sync/api/github/installations/route.ts", + "type": "registry:page", + "target": "api/code-storage/github/installations/route.ts" + }, + { + "path": "registry/new-york/blocks/git-platform-sync/api/repo/route.ts", + "type": "registry:page", + "target": "api/code-storage/repo/route.ts" } ] } diff --git a/bun.lock b/bun.lock index f24fdbf54..1178122ea 100644 --- a/bun.lock +++ b/bun.lock @@ -140,6 +140,9 @@ "packages/storage-elements-next": { "name": "@pierre/storage-elements-next", "version": "0.0.2", + "dependencies": { + "@pierre/storage": "catalog:", + }, "devDependencies": { "@octokit/openapi-types": "catalog:", "typescript": ">=5", diff --git a/packages/storage-elements-next/package.json b/packages/storage-elements-next/package.json index 3d8cafaa6..e1d77f905 100644 --- a/packages/storage-elements-next/package.json +++ b/packages/storage-elements-next/package.json @@ -1,6 +1,6 @@ { "name": "@pierre/storage-elements-next", - "version": "0.0.2", + "version": "0.0.3", "type": "module", "scripts": { "tsc": "tsc --build", @@ -17,7 +17,9 @@ "publishConfig": { "access": "public" }, - "dependencies": {}, + "dependencies": { + "@pierre/storage": "catalog:" + }, "peerDependencies": { "next": ">=15" }, diff --git a/packages/storage-elements-next/src/github/installations.ts b/packages/storage-elements-next/src/github/installations.ts new file mode 100644 index 000000000..887b48db3 --- /dev/null +++ b/packages/storage-elements-next/src/github/installations.ts @@ -0,0 +1,142 @@ +import type { components } from '@octokit/openapi-types'; +import { type NextRequest, NextResponse } from 'next/server'; + +type Installation = components['schemas']['installation']; + +type Owner = components['schemas']['simple-user'] & + components['schemas']['enterprise']; + +type InstallationResponse = { + installations: Installation[]; +}; + +type FilteredInstallation = { + id: string; + app_id: string; + app_slug: string; + permissions: Record; + owner_id: string | null; + events: string[]; + type: string | null; +}; + +type FilteredOwner = { + id: string; + login: string; + type: string | null; + avatar_url: string | null; +}; + +type CodeStorageGitHubInstallationsOptions = { + cookieName?: string; +}; + +export class CodeStorageGitHubInstallations { + private cookieName: string; + + constructor(options: CodeStorageGitHubInstallationsOptions) { + this.cookieName = options.cookieName ?? 'github_token'; + } + + private filterInstallations(installations: Installation[]) { + return installations.map((installation) => { + return { + id: `${installation.id}`, + app_id: `${installation.app_id}`, + app_slug: installation.app_slug, + owner_id: + installation.account != null && 'id' in installation.account + ? `${installation.account.id}` + : null, + permissions: installation.permissions, + events: installation.events, + type: + installation.account != null && 'type' in installation.account + ? installation.account.type + : null, + } satisfies FilteredInstallation; + }); + } + + private filterOwners(owners: Owner[]) { + return owners.map((owner) => { + return { + id: `${owner.id}`, + login: owner.login, + type: owner.type, + avatar_url: owner.avatar_url, + } satisfies FilteredOwner; + }); + } + + private getOwnersFromInstallations(installations: Installation[]) { + return installations + .map((installation) => installation.account) + .filter((account): account is Owner => account !== null); + } + + async handleRequest(request: NextRequest) { + const token = request.cookies.get(this.cookieName)?.value; + + if (token == null || token.trim() === '') { + return NextResponse.json({ data: { installations: [], owners: [] } }); + } + + try { + const response = await fetch( + `https://api.github.com/user/installations`, + { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + } + ); + + if (!response.ok) { + return NextResponse.json( + { error: 'Failed to fetch installations' }, + { status: 400 } + ); + } + + const data = (await response.json()) as InstallationResponse; + + if ((data?.installations?.length ?? 0) > 0) { + return NextResponse.json({ + data: { + installations: [], + owners: [], + }, + }); + } + + const filteredInstallations = this.filterInstallations( + data.installations + ); + + const filteredOwners = this.filterOwners( + this.getOwnersFromInstallations(data.installations) + ); + + return NextResponse.json({ + data: { + installations: filteredInstallations, + owners: filteredOwners, + }, + // for debugging this can be nice + // _raw: data.installations, + }); + } catch (error) { + console.error('Error fetching installations:', error); + return NextResponse.json( + { + error: 'Failed to fetch installations', + errorMessage: + error instanceof Error ? error.message : 'Unknown error', + }, + { status: 500 } + ); + } + } +} diff --git a/packages/storage-elements-next/src/index.ts b/packages/storage-elements-next/src/index.ts index e1b93e9b7..dc761135a 100644 --- a/packages/storage-elements-next/src/index.ts +++ b/packages/storage-elements-next/src/index.ts @@ -1 +1,3 @@ export * from './auth/success-callback'; +export * from './github/installations'; +export * from './repo/route'; diff --git a/packages/storage-elements-next/src/repo/route.ts b/packages/storage-elements-next/src/repo/route.ts new file mode 100644 index 000000000..f4d9ee3aa --- /dev/null +++ b/packages/storage-elements-next/src/repo/route.ts @@ -0,0 +1,80 @@ +import { GitStorage } from '@pierre/storage'; +import { type NextRequest, NextResponse } from 'next/server'; + +type CodeStorageRepoOptions = { + storageName: string; + privateKey: string; +}; + +type CreateRepoRequest = { + owner: string; + name: string; + defaultBranch?: string; +}; + +export class CodeStorageRepo { + private storageName: string; + private privateKey: string; + + constructor(options: CodeStorageRepoOptions) { + this.storageName = options.storageName; + this.privateKey = options.privateKey; + } + + async handlePostRequest(request: NextRequest) { + try { + const store = new GitStorage({ + name: this.storageName, + key: this.privateKey, + }); + + const body = (await request.json()) as CreateRepoRequest; + const { owner, name, defaultBranch } = body; + + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (!owner || !name) { + return NextResponse.json( + { success: false, error: 'Repository owner and name are required' }, + { status: 400 } + ); + } + + // TODO: Authenticate the user for this operation + + const repo = await store.createRepo({ + baseRepo: { + owner, + name, + // NOTE(amadeus): Given these types are `any`, not sure the safest way + // to convert fix them... + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing + defaultBranch: defaultBranch || 'main', // Optional, defaults to 'main' + }, + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing + defaultBranch: defaultBranch || 'main', // Optional, defaults to 'main' for the Git Storage repo + }); + + const ciUrl = await repo.getRemoteURL({ + permissions: ['git:read', 'git:write'], + ttl: 86400, // 24 hours + }); + + return NextResponse.json({ + success: true, + url: ciUrl, + repository: { + owner, + name, + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing + defaultBranch: defaultBranch || 'main', + }, + }); + } catch (error) { + console.error('Error syncing storage:', error); + return NextResponse.json( + { error: 'Failed to sync storage' }, + { status: 500 } + ); + } + } +} diff --git a/packages/storage-elements/blocks/git-platform-sync/api/github/callback/route.ts b/packages/storage-elements/blocks/git-platform-sync/api/github/callback/route.ts new file mode 100644 index 000000000..48b690c0b --- /dev/null +++ b/packages/storage-elements/blocks/git-platform-sync/api/github/callback/route.ts @@ -0,0 +1,13 @@ +import { CodeStorageSuccessCallback } from '@pierre/storage-elements-next'; +import { type NextRequest } from 'next/server'; + +const successCallback = new CodeStorageSuccessCallback({ + clientId: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + platform: 'github', + env: process.env.NODE_ENV, +}); + +export async function GET(request: NextRequest) { + return successCallback.handleRequest(request); +} diff --git a/packages/storage-elements/blocks/git-platform-sync/api/github/installations/route.ts b/packages/storage-elements/blocks/git-platform-sync/api/github/installations/route.ts new file mode 100644 index 000000000..140ec2535 --- /dev/null +++ b/packages/storage-elements/blocks/git-platform-sync/api/github/installations/route.ts @@ -0,0 +1,8 @@ +import { CodeStorageGitHubInstallations } from '@pierre/storage-elements-next'; +import { type NextRequest } from 'next/server'; + +const installations = new CodeStorageGitHubInstallations({}); + +export async function GET(request: NextRequest) { + return installations.handleRequest(request); +} diff --git a/packages/storage-elements/blocks/git-platform-sync/api/repo/route.ts b/packages/storage-elements/blocks/git-platform-sync/api/repo/route.ts new file mode 100644 index 000000000..dc5ba1a0a --- /dev/null +++ b/packages/storage-elements/blocks/git-platform-sync/api/repo/route.ts @@ -0,0 +1,11 @@ +import { CodeStorageRepo } from '@pierre/storage-elements-next'; +import { type NextRequest } from 'next/server'; + +const repo = new CodeStorageRepo({ + storageName: 'pierre', + privateKey: process.env.CODE_STORAGE_SYNC_PRIVATE_KEY!, +}); + +export async function POST(request: NextRequest) { + return repo.handlePostRequest(request); +} diff --git a/packages/storage-elements/blocks/git-platform-sync/pages/success/page.tsx b/packages/storage-elements/blocks/git-platform-sync/pages/success/page.tsx new file mode 100644 index 000000000..e5771deec --- /dev/null +++ b/packages/storage-elements/blocks/git-platform-sync/pages/success/page.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { CheckCircle } from 'lucide-react'; +import { useSearchParams } from 'next/navigation'; +import { Suspense, useEffect } from 'react'; + +const appInstallType = 'git-platform-sync-app-installed--github'; + +function SuccessPageContent() { + const searchParams = useSearchParams(); + const installationId = searchParams.get('installation_id'); + const setupAction = searchParams.get('setup_action'); + const state = searchParams.get('state'); + + useEffect(() => { + if (window.opener != null) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + window.opener.postMessage( + { + type: appInstallType, + installationId, + setupAction, + state, + }, + window.opener.origin + ); + + setTimeout(() => { + window.close(); + }, 2000); + } + }, [installationId, setupAction, state]); + + return ( +
+
+
+ +

+ Installation Successful! +

+

+ Your GitHub App has been successfully{' '} + {setupAction === 'install' ? 'installed' : 'updated'}. +

+ {installationId != null && ( +

+ Installation ID: {installationId} +

+ )} +
+

+ This window will close automatically… +

+ +
+
+
+
+ ); +} + +export default function SuccessPage() { + return ( + + + + ); +} diff --git a/packages/storage-elements/tsconfig.json b/packages/storage-elements/tsconfig.json index b225845cd..2048e213a 100644 --- a/packages/storage-elements/tsconfig.json +++ b/packages/storage-elements/tsconfig.json @@ -1,6 +1,10 @@ { "extends": "../../tsconfig.options.json", "include": ["blocks/**/*", "components/**/*", "lib/**/*"], + "exclude": [ + "blocks/git-platform-sync/api/**/*", + "blocks/git-platform-sync/pages/**/*" + ], "compilerOptions": { "noEmit": true, "lib": ["ES2020", "DOM", "DOM.Iterable"],