diff --git a/bun.lock b/bun.lock
index 798c00a88a..ea221f77a8 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,12 +1,11 @@
{
"lockfileVersion": 1,
- "configVersion": 0,
"workspaces": {
"": {
"name": "@appwrite/console",
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
- "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983",
+ "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@56957ff",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
"@appwrite.io/pink-legacy": "^1.0.3",
@@ -130,7 +129,7 @@
"@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="],
- "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983", { "dependencies": { "json-bigint": "1.0.0" } }],
+ "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@56957ff", { "dependencies": { "json-bigint": "1.0.0" } }],
"@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="],
diff --git a/package.json b/package.json
index fd0ebc590d..e54f0e19d7 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
- "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@ed09983",
+ "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@56957ff",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
"@appwrite.io/pink-legacy": "^1.0.3",
diff --git a/src/lib/components/git/IconOrigin.svelte b/src/lib/components/git/IconOrigin.svelte
new file mode 100644
index 0000000000..184073d564
--- /dev/null
+++ b/src/lib/components/git/IconOrigin.svelte
@@ -0,0 +1,13 @@
+
+
diff --git a/src/lib/components/git/connectGit.svelte b/src/lib/components/git/connectGit.svelte
index 6473f5f6d1..f9b0abe917 100644
--- a/src/lib/components/git/connectGit.svelte
+++ b/src/lib/components/git/connectGit.svelte
@@ -1,20 +1,14 @@
@@ -41,23 +35,16 @@
title="No installation was added to the project yet"
description="Add an installation to connect repositories">
-
-
- {#if isGiteaEnabled}
+
+ {#each providers as provider (provider.id)}
- {/if}
+ {/each}
diff --git a/src/lib/components/git/connectRepoModal.svelte b/src/lib/components/git/connectRepoModal.svelte
index faca63d917..7247151062 100644
--- a/src/lib/components/git/connectRepoModal.svelte
+++ b/src/lib/components/git/connectRepoModal.svelte
@@ -13,7 +13,9 @@
import { Click, trackEvent } from '$lib/actions/analytics';
import RepositoryBehaviour from '$lib/components/git/repositoryBehaviour.svelte';
import { page } from '$app/state';
- import { connectGitHub } from '$lib/stores/git';
+
+ import { connectVcsProvider, supportsRepositoryCreation } from '$lib/stores/git';
+ import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
let {
show = $bindable(false),
@@ -37,6 +39,11 @@
let selectedInstallationId = $state('');
let selectedRepository = $state('');
let installations = $state({ installations: [], total: 0 });
+ const canCreateRepository = $derived(
+ installations.installations.some((entry) =>
+ supportsRepositoryCreation(entry.provider, $regionalConsoleVariables)
+ )
+ );
let error = $state('');
onMount(async () => {
@@ -96,7 +103,7 @@
{#if !!installations?.total}
- {#if !onlyExisting}
+ {#if !onlyExisting && canCreateRepository}
{/if}
{#if repositoryBehaviour === 'new'}
@@ -143,12 +150,26 @@
{#if repositoryBehaviour === 'existing'}
-
-
- Missing a repository? check your permissions
-
-
+ {#if $installation?.provider === 'origin'}
+
+
+ Missing a repository? check your repository access
+
+
+ {:else}
+
+
+ Missing a repository? check your permissions
+
+
+ {/if}
{:else if repositoryBehaviour === 'new'}
diff --git a/src/lib/components/git/deploymentSource.svelte b/src/lib/components/git/deploymentSource.svelte
index 9b3220aaae..cc223e8b1d 100644
--- a/src/lib/components/git/deploymentSource.svelte
+++ b/src/lib/components/git/deploymentSource.svelte
@@ -20,6 +20,7 @@
Typography
} from '@appwrite.io/pink-svelte';
import Button from '$lib/elements/forms/button.svelte';
+ import IconOrigin from './IconOrigin.svelte';
let {
deployment,
@@ -35,6 +36,12 @@
let repository = $state(null);
+ // The deployment's server-supplied URLs carry the provider; the model has
+ // no provider field of its own.
+ const isOrigin = $derived(
+ (deployment?.providerRepositoryUrl ?? '').toLowerCase().includes('cursor.com')
+ );
+
async function loadRepository() {
if (!resource?.installationId || !resource?.providerRepositoryId || !region || !project) {
return;
@@ -66,7 +73,11 @@
toggle(e);
}}>
- GitHub
+ {#if isOrigin}
+ Origin
+ {:else}
+ GitHub
+ {/if}
{#if repository?.authorized === false}
@@ -82,8 +93,10 @@
To enable, add the repository to the installation settings on
- GitHub
+ href={isOrigin
+ ? 'https://cursor.com/codebase/settings/apps'
+ : `https://github.com/settings/installations/${repository.providerInstallationId}`}>
+ {isOrigin ? 'Origin' : 'GitHub'}
.
@@ -97,7 +110,7 @@
+ leadingIcon={isOrigin ? IconOrigin : IconGithub}>
{deployment.providerRepositoryOwner}/{deployment.providerRepositoryName}
import { InputCheckbox, InputSelect, InputText } from '$lib/elements/forms';
import { installation } from '$lib/stores/vcs';
+ import { supportsPublicRepositories, supportsRepositoryCreation } from '$lib/stores/git';
+ import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import type { Models } from '@appwrite.io/console';
import { Layout } from '@appwrite.io/pink-svelte';
- export let selectedInstallationId: string;
- export let installations: Models.InstallationList;
- export let repositoryName: string;
- export let repositoryPrivate = true;
- export let disableFields = false;
+ let {
+ selectedInstallationId = $bindable(),
+ installations,
+ repositoryName = $bindable(),
+ repositoryPrivate = $bindable(true),
+ disableFields = false
+ }: {
+ selectedInstallationId: string;
+ installations: Models.InstallationList;
+ repositoryName: string;
+ repositoryPrivate?: boolean;
+ disableFields?: boolean;
+ } = $props();
+
+ // Only offer organizations whose provider can create repositories.
+ const creatableInstallations = $derived(
+ installations.installations.filter((entry) =>
+ supportsRepositoryCreation(entry.provider, $regionalConsoleVariables)
+ )
+ );
+
+ const supportsPublic = $derived(
+ supportsPublicRepositories(
+ creatableInstallations.find((entry) => entry.$id === selectedInstallationId)?.provider,
+ $regionalConsoleVariables
+ )
+ );
+
+ $effect(() => {
+ if (
+ creatableInstallations.length &&
+ !creatableInstallations.some((entry) => entry.$id === selectedInstallationId)
+ ) {
+ selectedInstallationId = creatableInstallations[0].$id;
+ $installation = creatableInstallations[0];
+ }
+ });
+
+ $effect(() => {
+ // Without public repository support every repository is private.
+ if (!supportsPublic) {
+ repositoryPrivate = true;
+ }
+ });
@@ -18,14 +59,14 @@
id="installation"
label="Git organization"
disabled={disableFields}
- options={installations.installations.map((entry) => {
+ options={creatableInstallations.map((entry) => {
return {
label: entry.organization,
value: entry.$id
};
})}
on:change={() => {
- $installation = installations.installations.find(
+ $installation = creatableInstallations.find(
(entry) => entry.$id === selectedInstallationId
);
}}
@@ -38,9 +79,11 @@
disabled={disableFields}
bind:value={repositoryName} />
-
+ {#if supportsPublic}
+
+ {/if}
diff --git a/src/lib/components/git/repositories.svelte b/src/lib/components/git/repositories.svelte
index df2c800501..619e9ee953 100644
--- a/src/lib/components/git/repositories.svelte
+++ b/src/lib/components/git/repositories.svelte
@@ -19,7 +19,8 @@
import SvgIcon from '../svgIcon.svelte';
import { Query, VCSDetectionType, type Models } from '@appwrite.io/console';
import { getFrameworkIcon } from '$lib/stores/sites';
- import { connectGitHub } from '$lib/stores/git';
+ import { connectVcsProvider, enabledVcsProviders, getVcsProvider } from '$lib/stores/git';
+ import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
import { addNotification } from '$lib/stores/notifications';
import { page } from '$app/state';
import Card from '../card.svelte';
@@ -27,6 +28,8 @@
import { onMount, untrack, onDestroy } from 'svelte';
import { debounce } from '$lib/helpers/debounce';
+ const vcsProviders = enabledVcsProviders($regionalConsoleVariables?._APP_VCS_PROVIDERS);
+
let {
action = $bindable('select'),
selectedRepository = $bindable(undefined),
@@ -176,18 +179,27 @@
...installationsMap.map((entry) => {
return {
label: entry.organization,
+ leadingIcon: getVcsProvider(entry.provider).icon,
value: entry.$id
};
}),
- {
- label: 'Add installation',
- leadingIcon: IconPlus,
- value: 'new'
- }
+ ...vcsProviders.map((provider) => {
+ return {
+ label:
+ vcsProviders.length > 1
+ ? `Add ${provider.label} installation`
+ : 'Add installation',
+ leadingIcon: IconPlus,
+ value: `new-${provider.id}`
+ };
+ })
]}
on:change={() => {
- if (selectedInstallation === 'new') {
- window.location.href = connectGitHub(callbackState).toString();
+ if (selectedInstallation?.startsWith('new-')) {
+ window.location.href = connectVcsProvider(
+ selectedInstallation.slice('new-'.length),
+ callbackState
+ ).toString();
}
search = '';
installation.set(
diff --git a/src/lib/components/git/repositoryCard.svelte b/src/lib/components/git/repositoryCard.svelte
index 0c64432c55..c898dc539b 100644
--- a/src/lib/components/git/repositoryCard.svelte
+++ b/src/lib/components/git/repositoryCard.svelte
@@ -6,10 +6,18 @@
import { IconGithub } from '@appwrite.io/pink-icons-svelte';
import { Card, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
import { createEventDispatcher } from 'svelte';
+ import { installation } from '$lib/stores/vcs';
+ import { getProviderOrganizationUrl } from '$lib/stores/git';
+ import IconOrigin from './IconOrigin.svelte';
export let repository: Models.ProviderRepository;
const dispatch = createEventDispatcher();
+
+ // ProviderRepository carries no provider, so read it off the owning installation.
+ $: provider = $installation?.provider ?? 'github';
+ // '' when the provider's host is unknown client-side (e.g. Gitea).
+ $: organizationUrl = getProviderOrganizationUrl(provider, repository.organization);
@@ -24,7 +32,9 @@
alignItems="flex-start"
gap="s"
style="min-width: 0; flex: 1 1 16rem;">
-
+
{repository.name}
@@ -38,13 +48,15 @@
•
-
- {repository.organization}
-
+ {#if organizationUrl}
+
+ {repository.organization}
+
+ {:else}
+
+ {repository.organization}
+
+ {/if}
diff --git a/src/lib/stores/git.ts b/src/lib/stores/git.ts
index 8181f20cce..f55f3e7975 100644
--- a/src/lib/stores/git.ts
+++ b/src/lib/stores/git.ts
@@ -1,7 +1,104 @@
import { page } from '$app/state';
+import type { Models } from '@appwrite.io/console';
+import type { ComponentType } from 'svelte';
+import { IconGithub } from '@appwrite.io/pink-icons-svelte';
+import IconGitea from '$lib/components/git/IconGitea.svelte';
+import IconOrigin from '$lib/components/git/IconOrigin.svelte';
+import { isSelfHosted } from '$lib/system';
import { getApiEndpoint } from './sdk';
-function connectVcsProvider(provider: string, callbackState: Record = null) {
+export type VcsProviderId = 'github' | 'origin' | 'gitea';
+
+export interface VcsProviderMeta {
+ id: VcsProviderId;
+ label: string;
+ icon: ComponentType;
+ /** Web URL of an owner/organization on the provider, or '' when the host is not knowable client-side. */
+ organizationUrl: (organization: string) => string;
+ /** Where the user manages the installation's repository access, or null when the provider has no such page. */
+ installationSettingsUrl: (providerInstallationId: string) => string | null;
+ /** GitHub predates the _APP_VCS_PROVIDERS variable, so it stays on unconditionally. */
+ alwaysEnabled?: boolean;
+ /** Gitea is a self-hosted-only feature, not offered on Appwrite Cloud. */
+ selfHostedOnly?: boolean;
+}
+
+/**
+ * Single source of truth for the console's git providers - icons, labels,
+ * provider URLs and the authorize redirects. Key order is display order:
+ * GitHub first, Origin second, then the rest.
+ */
+export const VCS_PROVIDERS: Record = {
+ github: {
+ id: 'github',
+ label: 'GitHub',
+ icon: IconGithub,
+ organizationUrl: (organization) => `https://github.com/${organization}`,
+ installationSettingsUrl: (providerInstallationId) =>
+ `https://github.com/settings/installations/${providerInstallationId}`,
+ alwaysEnabled: true
+ },
+ origin: {
+ id: 'origin',
+ label: 'Origin',
+ icon: IconOrigin as unknown as ComponentType,
+ // Origin repositories are browsed in the Codebase section of cursor.com
+ organizationUrl: () => 'https://cursor.com/codebase',
+ installationSettingsUrl: () => 'https://cursor.com/codebase/settings/apps'
+ },
+ gitea: {
+ id: 'gitea',
+ label: 'Gitea',
+ icon: IconGitea as unknown as ComponentType,
+ // The Gitea host is per-deployment server configuration the client does not know
+ organizationUrl: () => '',
+ installationSettingsUrl: () => null,
+ selfHostedOnly: true
+ }
+};
+
+/** Provider metadata with a GitHub fallback for unknown ids (cosmetic use only). */
+export function getVcsProvider(provider?: string): VcsProviderMeta {
+ return VCS_PROVIDERS[provider as VcsProviderId] ?? VCS_PROVIDERS.github;
+}
+
+/**
+ * Providers the connect UI should offer, honoring the server's
+ * _APP_VCS_PROVIDERS list and each provider's own gates.
+ */
+export function enabledVcsProviders(vcsProviders: string[] | undefined): VcsProviderMeta[] {
+ return Object.values(VCS_PROVIDERS).filter(
+ (provider) =>
+ provider.alwaysEnabled ||
+ ((vcsProviders?.includes(provider.id) ?? false) &&
+ (!provider.selfHostedOnly || isSelfHosted))
+ );
+}
+
+/**
+ * Whether the console can create repositories on the provider. Servers that
+ * predate _APP_VCS_PROVIDERS_WITH_REPOSITORY_CREATION omit the list; keep the
+ * console's historical behavior (creation offered) for them.
+ */
+export function supportsRepositoryCreation(
+ provider: string,
+ variables: Models.ConsoleVariables
+): boolean {
+ return variables?._APP_VCS_PROVIDERS_WITH_REPOSITORY_CREATION?.includes(provider) ?? true;
+}
+
+/**
+ * Whether the provider can host public repositories. When it cannot, every
+ * repository is private and the public/private choice is meaningless.
+ */
+export function supportsPublicRepositories(
+ provider: string,
+ variables: Models.ConsoleVariables
+): boolean {
+ return variables?._APP_VCS_PROVIDERS_WITH_PUBLIC_REPOSITORIES?.includes(provider) ?? true;
+}
+
+export function connectVcsProvider(provider: string, callbackState: Record = null) {
const redirect = new URL(page.url);
if (callbackState) {
Object.keys(callbackState).forEach((key) => {
@@ -24,6 +121,28 @@ export function connectGitea(callbackState: Record = null) {
return connectVcsProvider('gitea', callbackState);
}
+export function connectOrigin(callbackState: Record = null) {
+ return connectVcsProvider('origin', callbackState);
+}
+
+/**
+ * Browser URL of an owner/organization on the provider. Unknown providers
+ * fall back to GitHub, matching the console's historical behavior.
+ */
+export function getProviderOrganizationUrl(provider: string, organization: string): string {
+ return getVcsProvider(provider).organizationUrl(organization);
+}
+
+/**
+ * Browser URL of a repository on the provider, or '' when the provider's host
+ * is not knowable client-side. Callers must not render a link for ''; an empty
+ * or relative href would resolve against the console's own origin.
+ */
+export function getProviderRepositoryUrl(provider: string, organization: string, name: string) {
+ const organizationUrl = getProviderOrganizationUrl(provider, organization);
+ return organizationUrl ? `${organizationUrl}/${name}` : '';
+}
+
export function deploymentStatusConverter(status: string) {
// Status component possible values - status: 'waiting' | 'ready' | 'processing' | 'pending' | 'failed' | 'complete';
switch (status) {
diff --git a/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/aside.svelte b/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/aside.svelte
index 4acf3d41a7..2b473c61a7 100644
--- a/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/aside.svelte
+++ b/src/routes/(console)/project-[region]-[project]/functions/create-function/(components)/aside.svelte
@@ -1,7 +1,9 @@