mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-19 02:32:56 +02:00
Implement cli commands to configure ai clients
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
toPublicVirtualApiKey
|
||||
} from "@server/lib/virtualApiKey";
|
||||
import type { ListMyVirtualApiKeysResponse } from "@server/routers/virtualApiKey/types";
|
||||
import { formatPublicResourceAccess } from "@server/routers/launcher/formatLauncherAccess";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
@@ -125,11 +126,19 @@ export async function listMyVirtualApiKeys(
|
||||
|
||||
let resourceId: number | undefined;
|
||||
let resourceName: string | undefined;
|
||||
let resourceNiceId: string | undefined;
|
||||
let resourceAccessUrl: string | null = null;
|
||||
if (resourceGuid) {
|
||||
const [resource] = await db
|
||||
.select({
|
||||
resourceId: resources.resourceId,
|
||||
name: resources.name
|
||||
name: resources.name,
|
||||
niceId: resources.niceId,
|
||||
mode: resources.mode,
|
||||
fullDomain: resources.fullDomain,
|
||||
ssl: resources.ssl,
|
||||
proxyPort: resources.proxyPort,
|
||||
wildcard: resources.wildcard
|
||||
})
|
||||
.from(resources)
|
||||
.where(
|
||||
@@ -151,6 +160,14 @@ export async function listMyVirtualApiKeys(
|
||||
|
||||
resourceId = resource.resourceId;
|
||||
resourceName = resource.name;
|
||||
resourceNiceId = resource.niceId;
|
||||
resourceAccessUrl = formatPublicResourceAccess({
|
||||
mode: resource.mode ?? "",
|
||||
fullDomain: resource.fullDomain,
|
||||
ssl: resource.ssl,
|
||||
proxyPort: resource.proxyPort,
|
||||
wildcard: resource.wildcard
|
||||
}).accessUrl;
|
||||
}
|
||||
|
||||
const [user] = await db
|
||||
@@ -223,7 +240,9 @@ export async function listMyVirtualApiKeys(
|
||||
manualKeys: manualRows.map((row) =>
|
||||
toKeyWithResources(row, resourceIdsByKey)
|
||||
),
|
||||
...(resourceName !== undefined ? { resourceName } : {})
|
||||
...(resourceName !== undefined ? { resourceName } : {}),
|
||||
...(resourceNiceId !== undefined ? { resourceNiceId } : {}),
|
||||
...(resourceNiceId !== undefined ? { resourceAccessUrl } : {})
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -23,6 +23,8 @@ export type ListMyVirtualApiKeysResponse = {
|
||||
userKey: VirtualApiKeyWithResources;
|
||||
manualKeys: VirtualApiKeyWithResources[];
|
||||
resourceName?: string | null;
|
||||
resourceNiceId?: string | null;
|
||||
resourceAccessUrl?: string | null;
|
||||
};
|
||||
|
||||
export type GetMyVirtualApiKeyResponse = {
|
||||
|
||||
@@ -113,7 +113,12 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) {
|
||||
launcherMode
|
||||
showViewAsAdmin={isAdminOrOwner}
|
||||
>
|
||||
<UserVirtualApiKeys orgId={orgId} initialData={keysData} />
|
||||
<UserVirtualApiKeys
|
||||
orgId={orgId}
|
||||
initialData={keysData}
|
||||
resourceNiceId={keysData.resourceNiceId ?? undefined}
|
||||
endpoint={keysData.resourceAccessUrl ?? undefined}
|
||||
/>
|
||||
</Layout>
|
||||
</UserProvider>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,10 @@ import { formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat";
|
||||
type UserVirtualApiKeysProps = {
|
||||
orgId: string;
|
||||
initialData: ListMyVirtualApiKeysResponse;
|
||||
/** The resource's niceId, when this page is scoped to a single resource. */
|
||||
resourceNiceId?: string;
|
||||
/** The resource's real access URL, when known; falls back to a placeholder otherwise. */
|
||||
endpoint?: string;
|
||||
};
|
||||
|
||||
function OwnedKeySecret({
|
||||
@@ -163,7 +167,9 @@ function ManualKeyRow({
|
||||
|
||||
export default function UserVirtualApiKeys({
|
||||
orgId,
|
||||
initialData
|
||||
initialData,
|
||||
resourceNiceId,
|
||||
endpoint
|
||||
}: UserVirtualApiKeysProps) {
|
||||
const t = useTranslations();
|
||||
const resourceName = initialData.resourceName;
|
||||
@@ -217,11 +223,12 @@ export default function UserVirtualApiKeys({
|
||||
|
||||
<AiClientConfigSection
|
||||
layout="wide"
|
||||
endpoint={t("aiClientConfigEndpointPlaceholder")}
|
||||
endpoint={endpoint ?? t("aiClientConfigEndpointPlaceholder")}
|
||||
auth={{
|
||||
mode: "keyed",
|
||||
getKeyText: getKeyCopyText
|
||||
}}
|
||||
resourceNiceId={resourceNiceId}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</>
|
||||
|
||||
@@ -40,6 +40,7 @@ type AiClientConfigCardProps = {
|
||||
keyAuth: AiClientAuthInput;
|
||||
description: string;
|
||||
iconSrc: { light: string; dark: string };
|
||||
resourceNiceId?: string;
|
||||
};
|
||||
|
||||
export function AiClientConfigCard({
|
||||
@@ -48,7 +49,8 @@ export function AiClientConfigCard({
|
||||
endpoint,
|
||||
keyAuth,
|
||||
description,
|
||||
iconSrc
|
||||
iconSrc,
|
||||
resourceNiceId
|
||||
}: AiClientConfigCardProps) {
|
||||
const t = useTranslations();
|
||||
const { theme } = useTheme();
|
||||
@@ -94,16 +96,23 @@ export function AiClientConfigCard({
|
||||
|
||||
const guide = useMemo(() => {
|
||||
if (keyAuth.mode === "keyless") {
|
||||
return buildAiClientGuide(clientId, endpoint, { mode: "keyless" });
|
||||
return buildAiClientGuide(
|
||||
clientId,
|
||||
endpoint,
|
||||
{ mode: "keyless" },
|
||||
resourceNiceId
|
||||
);
|
||||
}
|
||||
if (revealedKey === null) {
|
||||
return null;
|
||||
}
|
||||
return buildAiClientGuide(clientId, endpoint, {
|
||||
mode: "keyed",
|
||||
key: revealedKey
|
||||
});
|
||||
}, [clientId, endpoint, keyAuth.mode, revealedKey]);
|
||||
return buildAiClientGuide(
|
||||
clientId,
|
||||
endpoint,
|
||||
{ mode: "keyed", key: revealedKey },
|
||||
resourceNiceId
|
||||
);
|
||||
}, [clientId, endpoint, keyAuth.mode, revealedKey, resourceNiceId]);
|
||||
|
||||
const preset =
|
||||
guide?.presets.find((p) => p.id === presetId) ?? guide?.presets[0];
|
||||
|
||||
@@ -27,6 +27,12 @@ type AiClientConfigSectionProps = {
|
||||
*/
|
||||
layout?: "wide" | "compact";
|
||||
className?: string;
|
||||
/**
|
||||
* The resource's niceId, when known, so the displayed CLI commands can
|
||||
* include `--resource <niceId>` and let `pangolin configure` skip its
|
||||
* own resource picker.
|
||||
*/
|
||||
resourceNiceId?: string;
|
||||
};
|
||||
|
||||
// Each logo file is named for its own color, not the theme it belongs on, so
|
||||
@@ -54,7 +60,8 @@ export function AiClientConfigSection({
|
||||
endpoint,
|
||||
auth,
|
||||
layout = "compact",
|
||||
className
|
||||
className,
|
||||
resourceNiceId
|
||||
}: AiClientConfigSectionProps) {
|
||||
const t = useTranslations();
|
||||
const isWide = layout === "wide";
|
||||
@@ -93,6 +100,7 @@ export function AiClientConfigSection({
|
||||
keyAuth={auth}
|
||||
description={descriptions[clientId]}
|
||||
iconSrc={CLIENT_LOGOS[clientId]}
|
||||
resourceNiceId={resourceNiceId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -351,6 +351,7 @@ function PublicResourceDetails({
|
||||
mode: "keyed",
|
||||
getKeyText: getAiKeyCopyText
|
||||
}}
|
||||
resourceNiceId={launcherResource.niceId}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
@@ -428,6 +429,7 @@ function PrivateResourceDetails({
|
||||
<AiClientConfigSection
|
||||
endpoint={launcherResource.accessUrl ?? ""}
|
||||
auth={{ mode: "keyless" }}
|
||||
resourceNiceId={launcherResource.niceId}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
+34
-12
@@ -59,12 +59,17 @@ function block(
|
||||
|
||||
function buildCli(
|
||||
clientArg: "claude" | "codex" | "opencode",
|
||||
auth: AiClientAuth
|
||||
auth: AiClientAuth,
|
||||
resourceNiceId?: string
|
||||
): AiCliCommands {
|
||||
const resourceFlag = resourceNiceId
|
||||
? ` --resource ${resourceNiceId}`
|
||||
: "";
|
||||
|
||||
const configure: AiConfigBlock = {
|
||||
id: `cli-configure-${clientArg}`,
|
||||
label: "Configure",
|
||||
displayText: `pangolin configure ${clientArg}`
|
||||
displayText: `pangolin configure ${clientArg}${resourceFlag}`
|
||||
};
|
||||
|
||||
if (auth.mode !== "keyed") {
|
||||
@@ -76,12 +81,16 @@ function buildCli(
|
||||
configureWithKey: {
|
||||
id: `cli-configure-key-${clientArg}`,
|
||||
label: "Configure with an API key",
|
||||
displayText: `pangolin configure ${clientArg} ${auth.key}`
|
||||
displayText: `pangolin configure ${clientArg} ${auth.key}${resourceFlag}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
|
||||
function buildClaudeGuide(
|
||||
endpoint: string,
|
||||
auth: AiClientAuth,
|
||||
resourceNiceId?: string
|
||||
): AiClientGuide {
|
||||
const defaultSettings = block(
|
||||
"claude-default-settings",
|
||||
"~/.claude/settings.json",
|
||||
@@ -168,7 +177,7 @@ function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
|
||||
return {
|
||||
id: "claude",
|
||||
name: AI_CLIENT_NAMES.claude,
|
||||
cli: buildCli("claude", auth),
|
||||
cli: buildCli("claude", auth, resourceNiceId),
|
||||
presets: [
|
||||
{
|
||||
id: "default",
|
||||
@@ -194,7 +203,11 @@ function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
|
||||
};
|
||||
}
|
||||
|
||||
function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
|
||||
function buildCodexGuide(
|
||||
endpoint: string,
|
||||
auth: AiClientAuth,
|
||||
resourceNiceId?: string
|
||||
): AiClientGuide {
|
||||
const settings = block(
|
||||
"codex-settings",
|
||||
"~/.codex/config.toml",
|
||||
@@ -224,7 +237,7 @@ function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
|
||||
return {
|
||||
id: "codex",
|
||||
name: AI_CLIENT_NAMES.codex,
|
||||
cli: buildCli("codex", auth),
|
||||
cli: buildCli("codex", auth, resourceNiceId),
|
||||
presets: [
|
||||
{
|
||||
id: "default",
|
||||
@@ -235,7 +248,11 @@ function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
|
||||
};
|
||||
}
|
||||
|
||||
function buildOpencodeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
|
||||
function buildOpencodeGuide(
|
||||
endpoint: string,
|
||||
auth: AiClientAuth,
|
||||
resourceNiceId?: string
|
||||
): AiClientGuide {
|
||||
const config = block(
|
||||
"opencode-config",
|
||||
"Step 1: opencode.json",
|
||||
@@ -273,7 +290,7 @@ function buildOpencodeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide
|
||||
return {
|
||||
id: "opencode",
|
||||
name: AI_CLIENT_NAMES.opencode,
|
||||
cli: buildCli("opencode", auth),
|
||||
cli: buildCli("opencode", auth, resourceNiceId),
|
||||
presets: [
|
||||
{
|
||||
id: "default",
|
||||
@@ -318,7 +335,11 @@ function buildCursorGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
|
||||
|
||||
const GUIDE_BUILDERS: Record<
|
||||
AiClientId,
|
||||
(endpoint: string, auth: AiClientAuth) => AiClientGuide
|
||||
(
|
||||
endpoint: string,
|
||||
auth: AiClientAuth,
|
||||
resourceNiceId?: string
|
||||
) => AiClientGuide
|
||||
> = {
|
||||
claude: buildClaudeGuide,
|
||||
codex: buildCodexGuide,
|
||||
@@ -329,7 +350,8 @@ const GUIDE_BUILDERS: Record<
|
||||
export function buildAiClientGuide(
|
||||
clientId: AiClientId,
|
||||
endpoint: string,
|
||||
auth: AiClientAuth
|
||||
auth: AiClientAuth,
|
||||
resourceNiceId?: string
|
||||
): AiClientGuide {
|
||||
return GUIDE_BUILDERS[clientId](endpoint, auth);
|
||||
return GUIDE_BUILDERS[clientId](endpoint, auth, resourceNiceId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user