From a0ae31fe2a1c62d0f7607f944d2aaf17608d5a00 Mon Sep 17 00:00:00 2001 From: dvcdsys Date: Wed, 3 Jun 2026 11:18:18 +0100 Subject: [PATCH 1/2] feat(dashboard): add copyable "connect CLI" command to API-key-created popup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After creating an API key, the success popup now shows a second copyable field: a ready-to-paste command that registers THIS server in the cix CLI and makes it the default, so the flow is create → copy → paste → use. The command writes a single `server.` entry (URL + the new key as one unit — the key belongs to the server), then sets it as default: cix config set server..url && \ cix config set server..key && \ cix config set default_server - comes from window.location.origin (dashboard is served same-origin from cix-server, so it's always the correct base URL). - is derived from window.location.host, sanitized to [a-z0-9-] so it's dot-/whitespace-free as the CLI's parseServerKey requires. Copy logic refactored into a reusable copyText() helper driving two independent button states (key + command), preserving the existing secure-context / execCommand fallback. Frontend-only; no CLI or server change — the command uses existing `cix config set` commands. Co-Authored-By: Claude Opus 4.8 --- .../components/CreateApiKeyDialog.tsx | 114 +++++++++++++++--- 1 file changed, 99 insertions(+), 15 deletions(-) diff --git a/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx b/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx index 4b56de6..633bd85 100644 --- a/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx +++ b/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { Check, Copy, Loader2, KeyRound, AlertTriangle } from 'lucide-react'; +import { Check, Copy, Loader2, KeyRound, AlertTriangle, Terminal } from 'lucide-react'; import { toast } from 'sonner'; import { ApiError } from '@/api/client'; import { Alert, AlertDescription, AlertTitle } from '@/ui/alert'; @@ -41,6 +41,21 @@ function legacyCopy(text: string): boolean { return ok; } +// Derive a cix CLI server alias from the browser host. The CLI stores each +// server as one entry under `server.` and parses that config key by +// splitting on dots, so the alias must be dot-free (and whitespace-free, and +// non-empty) — see validateServerName / parseServerKey in the CLI. We fold the +// host (including any non-default port, so distinct ports get distinct aliases) +// to [a-z0-9-]: "cix.example.com" -> "cix-example-com", +// "localhost:21847" -> "localhost-21847". +function cixServerAlias(host: string): string { + const alias = host + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + return alias || 'default'; +} + // Two-stage dialog: collect a name, then reveal the freshly minted key once. // Once revealed, the dialog refuses outside-click and Escape — accidental // dismissal would lose the unrecoverable secret. Only the explicit "I've @@ -49,7 +64,8 @@ export function CreateApiKeyDialog() { const [open, setOpen] = useState(false); const [name, setName] = useState(''); const [revealed, setRevealed] = useState(null); - const [copied, setCopied] = useState(false); + const [copiedKey, setCopiedKey] = useState(false); + const [copiedCmd, setCopiedCmd] = useState(false); const inputRef = useRef(null); const create = useCreateApiKey(); @@ -65,7 +81,8 @@ export function CreateApiKeyDialog() { function reset() { setName(''); setRevealed(null); - setCopied(false); + setCopiedKey(false); + setCopiedCmd(false); create.reset(); } @@ -81,23 +98,50 @@ export function CreateApiKeyDialog() { } } - async function copyToClipboard() { - if (!revealed) return; - // navigator.clipboard requires a secure context (HTTPS or localhost). On - // bare-IP / HTTP deploys it throws — fall back to document.execCommand - // through a transient textarea so users still get one-click copy. + // copyText copies one string to the clipboard, surfacing a toast on failure. + // navigator.clipboard requires a secure context (HTTPS or localhost). On + // bare-IP / HTTP deploys it throws — fall back to document.execCommand + // through a transient textarea so users still get one-click copy. + async function copyText(text: string): Promise { try { if (window.isSecureContext && navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(revealed); + await navigator.clipboard.writeText(text); } else { - if (!legacyCopy(revealed)) throw new Error('legacy copy failed'); + if (!legacyCopy(text)) throw new Error('legacy copy failed'); } - setCopied(true); - window.setTimeout(() => setCopied(false), 2000); + return true; } catch { toast.error('Could not copy automatically.', { - description: 'Click the field, ⌘A / Ctrl-A to select, then copy.', + description: 'Select the text, ⌘A / Ctrl-A, then copy.', }); + return false; + } + } + + // One-paste command that registers THIS server (URL + the freshly minted key, + // as a single `server.` entry) in the cix CLI config and makes it the + // default. The dashboard is served same-origin from cix-server, so + // window.location.origin is exactly the base URL the CLI must talk to. Values + // are shell-safe (a URL, a `cix_` key, an [a-z0-9-] alias), so no quoting + // is needed — matching the CLI README's unquoted examples. + const alias = cixServerAlias(window.location.host); + const connectCmd = + `cix config set server.${alias}.url ${window.location.origin} && ` + + `cix config set server.${alias}.key ${revealed ?? ''} && ` + + `cix config set default_server ${alias}`; + + async function copyKey() { + if (!revealed) return; + if (await copyText(revealed)) { + setCopiedKey(true); + window.setTimeout(() => setCopiedKey(false), 2000); + } + } + + async function copyConnectCmd() { + if (await copyText(connectCmd)) { + setCopiedCmd(true); + window.setTimeout(() => setCopiedCmd(false), 2000); } } @@ -165,8 +209,8 @@ export function CreateApiKeyDialog() { onFocus={(e) => e.currentTarget.select()} onClick={(e) => e.currentTarget.select()} /> - + +

+ Paste this in a terminal with the cix CLI installed. It saves the + server to ~/.cix/config.yaml as the default, then{' '} + cix search "…" works. +

+