Implement cli commands to configure ai clients

This commit is contained in:
Owen
2026-08-18 16:58:27 -04:00
parent 3fd3d90c10
commit 667804043c
8 changed files with 99 additions and 25 deletions
@@ -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,
+2
View File
@@ -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>
); );
+9 -2
View File
@@ -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
View File
@@ -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);
} }