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