diff --git a/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx b/server/dashboard/src/modules/api-keys/components/CreateApiKeyDialog.tsx
index 4b56de6..8ad90fc 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);
}
}
@@ -105,11 +149,14 @@ export function CreateApiKeyDialog() {
+
+
+
+ {/* Read-only, wrapping command box: one paste registers this
+ server (URL + key as a single entry) and makes it the
+ default, so `cix search …` works right after. */}
+