From ef1e6787620e159949643c3f48108ada8ae7a6b4 Mon Sep 17 00:00:00 2001 From: Railly Date: Mon, 27 Apr 2026 17:51:20 -0500 Subject: [PATCH 1/5] feat(auth): add agent skills install snippet to OAuth success page Adds a skills installer panel below the existing 'Authentication successful' confirmation in the local OAuth callback page. Shows the `npx skills add clerk/skills` command with copy button, a sparkle- prefixed header label, and a 'Learn more about building with AI' link pointing to clerk.com/docs/guides/ai/overview. Page now uses CSS custom properties (`--cli-*`) with light/dark mode overrides via `prefers-color-scheme`. Sets `color-scheme: light dark` on `:root` so native form controls follow the system theme. Charset is now declared in both the response header and a meta tag so the sparkle SVG and arrow render correctly across browsers. --- packages/cli-core/src/lib/auth-server.ts | 117 ++++++++++++++++++++--- 1 file changed, 106 insertions(+), 11 deletions(-) diff --git a/packages/cli-core/src/lib/auth-server.ts b/packages/cli-core/src/lib/auth-server.ts index 2ef06d80..d15d661c 100644 --- a/packages/cli-core/src/lib/auth-server.ts +++ b/packages/cli-core/src/lib/auth-server.ts @@ -25,10 +25,66 @@ const CLERK_LOGO = `${char}`; ); } -const SUCCESS_HTML = ` +const SPARKLE_ICON_SVG = ``; + +const EXTERNAL_ICON_SVG = ``; + +const COPY_ICON_SVG = ``; + +const CHECK_ICON_SVG = ``; + +const COPY_SCRIPT = ` + function copyClerkSkill(btn) { + const cmd = document.querySelector('.ai-cmd').textContent.replace(/\\s+/g, ' ').trim(); + const reset = function () { setTimeout(function () { btn.classList.remove('copied'); }, 1500); }; + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(cmd).then(function () { btn.classList.add('copied'); reset(); }); + } else { + const ta = document.createElement('textarea'); + ta.value = cmd; ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); ta.select(); + try { document.execCommand('copy'); btn.classList.add('copied'); reset(); } catch (e) {} + document.body.removeChild(ta); + } + } +`; + +export const SUCCESS_HTML = ` -Clerk CLI +Clerk CLI
${CLERK_LOGO}

${animatedText("Authentication successful")}

-

You can close this window and return to your terminal.

+

You can close this window and return to your terminal.

+
+
+
+
${SPARKLE_ICON_SVG}Using an AI coding agent? Add Clerk Skills
+
+
+ npx skills add clerk/skills +
+ +
+
+ Learn more about building with AI${EXTERNAL_ICON_SVG} +
+
+ `; -const ERROR_HTML = (message: string) => ` +export const ERROR_HTML = (message: string) => ` -Clerk CLI +Clerk CLI
${CLERK_LOGO} @@ -112,7 +207,7 @@ export function startAuthServer(expectedState: string): AuthServerResult { clearTimeout(timeout); setTimeout(() => server?.stop(), 100); return new Response(ERROR_HTML(description), { - headers: { "Content-Type": "text/html" }, + headers: { "Content-Type": "text/html; charset=utf-8" }, }); } @@ -123,7 +218,7 @@ export function startAuthServer(expectedState: string): AuthServerResult { setTimeout(() => server?.stop(), 100); return new Response(ERROR_HTML("Invalid state parameter."), { status: 400, - headers: { "Content-Type": "text/html" }, + headers: { "Content-Type": "text/html; charset=utf-8" }, }); } @@ -134,7 +229,7 @@ export function startAuthServer(expectedState: string): AuthServerResult { setTimeout(() => server?.stop(), 100); return new Response(ERROR_HTML("No authorization code received."), { status: 400, - headers: { "Content-Type": "text/html" }, + headers: { "Content-Type": "text/html; charset=utf-8" }, }); } @@ -143,7 +238,7 @@ export function startAuthServer(expectedState: string): AuthServerResult { clearTimeout(timeout); setTimeout(() => server?.stop(), 100); return new Response(SUCCESS_HTML, { - headers: { "Content-Type": "text/html" }, + headers: { "Content-Type": "text/html; charset=utf-8" }, }); }, }, From 16c8f433c2a4a2d1bc6c152170f79e60fff632d5 Mon Sep 17 00:00:00 2001 From: Railly Date: Mon, 27 Apr 2026 17:53:13 -0500 Subject: [PATCH 2/5] chore(changeset): add minor bump for OAuth success skills snippet --- .changeset/oauth-success-skills-snippet.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/oauth-success-skills-snippet.md diff --git a/.changeset/oauth-success-skills-snippet.md b/.changeset/oauth-success-skills-snippet.md new file mode 100644 index 00000000..2315f3de --- /dev/null +++ b/.changeset/oauth-success-skills-snippet.md @@ -0,0 +1,5 @@ +--- +"clerk": minor +--- + +Promote Clerk Skills on the OAuth success page. After `clerk auth login` redirects back to the local callback, the success page now shows an installer panel with `npx skills add clerk/skills`, a copy button, and a link to `clerk.com/docs/guides/ai/overview`. Page styling moved to CSS custom properties with `prefers-color-scheme` overrides and `color-scheme: light dark` for native form theming. UTF-8 charset is now declared in the response header and meta tag. From 85ec448a1be100773bc9e1785a1749d5c50d3e6d Mon Sep 17 00:00:00 2001 From: Railly Date: Tue, 28 Apr 2026 12:57:42 -0500 Subject: [PATCH 3/5] fix(auth): apply review feedback on OAuth success page - Add CSS reset (`* { margin: 0; padding: 0; box-sizing: border-box; }`) to fix overflow - Wrap content in `.auth-page` with `padding-block: 4rem` and reapply title/subtext margins after the reset - Center-align the 'Learn more' link within the installer wrap (was left-aligned) - Move syntax highlight from `npx` to `clerk/skills` so the package name is the visual anchor; `skills add` keeps purple, `npx` becomes muted --- packages/cli-core/src/lib/auth-server.ts | 28 +++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/cli-core/src/lib/auth-server.ts b/packages/cli-core/src/lib/auth-server.ts index d15d661c..56983347 100644 --- a/packages/cli-core/src/lib/auth-server.ts +++ b/packages/cli-core/src/lib/auth-server.ts @@ -25,6 +25,7 @@ const CLERK_LOGO = ` p { margin-top: 0.75rem; } @keyframes roll-in { from { transform: rotateX(90deg); opacity: 0; } to { transform: rotateX(0deg); opacity: 1; } } @keyframes fade-in { from { opacity: 0; filter: blur(2px); } to { opacity: 1; filter: blur(0px); } } @keyframes sparkle-pulse { 0%, 100% { opacity: 0.7; transform: scale(1); } 50% { opacity: 1; transform: scale(1.12); } } .ai-section { margin-top: 2rem; display: flex; justify-content: center; opacity: 0; animation: fade-in 0.4s ease-out 0.85s forwards; } - .ai-installer-wrap { display: flex; flex-direction: column; width: 24rem; max-width: 100%; } - .ai-installer { display: flex; flex-direction: column; border: 1px solid var(--cli-border); border-radius: 12px; overflow: hidden; text-align: left; } + .ai-installer-wrap { display: flex; flex-direction: column; align-items: center; width: 24rem; max-width: 100%; } + .ai-installer { display: flex; flex-direction: column; width: 100%; border: 1px solid var(--cli-border); border-radius: 12px; overflow: hidden; text-align: left; } .ai-header { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 1rem; border-bottom: 1px solid var(--cli-border); background: var(--cli-tab-bg); font-size: 12.5px; color: var(--cli-fg-muted); } .ai-header span { color: var(--cli-fg-muted); } - .ai-learn { display: inline-flex; align-items: center; gap: 0.3rem; align-self: flex-start; margin-top: 0.6rem; padding: 0 0.05rem; font-size: 12px; color: var(--cli-fg-faint); text-decoration: none; transition: color 0.15s; } + .ai-learn { display: inline-flex; align-items: center; gap: 0.3rem; margin-top: 0.75rem; font-size: 12px; color: var(--cli-fg-faint); text-decoration: none; transition: color 0.15s; } .ai-learn:hover { color: var(--cli-accent); } .ai-learn:hover .ai-external { transform: translate(1px, -1px); } .ai-external { width: 11px; height: 11px; flex: none; color: currentColor; transition: transform 0.2s; } @@ -75,10 +77,10 @@ const PAGE_STYLE = ` .ai-code-row { display: flex; align-items: center; gap: 0.5rem; padding: 0.7rem 0.5rem 0.7rem 1rem; background: var(--cli-code-bg); } .ai-cmd-wrap { flex: 1; min-width: 0; overflow: hidden; -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 1.875rem), transparent 100%); mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 1.875rem), transparent 100%); } .ai-cmd { display: block; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; line-height: 1.1; letter-spacing: -0.02em; color: var(--cli-fg-muted); user-select: text; } - .ai-bin { color: var(--cli-syntax-bin); font-weight: 500; } - .ai-mid { color: var(--cli-syntax-mid); font-weight: 500; } + .ai-bin { color: var(--cli-fg-muted); } + .ai-mid { color: var(--cli-syntax-mid); } .ai-target { color: var(--cli-syntax-target); font-weight: 500; } - .ai-copy { background: transparent; border: 0; padding: 0; width: 1.75rem; height: 1.75rem; cursor: pointer; color: var(--cli-fg-dim); border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; flex: none; transition: color 0.3s cubic-bezier(0.4,0.36,0,1), background 0.15s, transform 0.15s; } + .ai-copy { background: transparent; border: 0; width: 1.75rem; height: 1.75rem; cursor: pointer; color: var(--cli-fg-dim); border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; flex: none; transition: color 0.3s cubic-bezier(0.4,0.36,0,1), background 0.15s, transform 0.15s; } .ai-copy:hover { color: var(--cli-fg); background: var(--cli-hover-bg); } .ai-copy:active { transform: scale(0.92); } .ai-copy .icon-check { display: none; color: #16a34a; } @@ -131,7 +133,7 @@ export const SUCCESS_HTML = ` Clerk CLI -
+
${CLERK_LOGO}

${animatedText("Authentication successful")}

You can close this window and return to your terminal.

@@ -158,7 +160,7 @@ export const ERROR_HTML = (message: string) => ` Clerk CLI -
+
${CLERK_LOGO}

Authentication failed

${escapeHtml(message)}

From aab267fa5fcdc289b5926753a642279269bb54ae Mon Sep 17 00:00:00 2001 From: Railly Date: Tue, 28 Apr 2026 12:57:43 -0500 Subject: [PATCH 4/5] chore(changeset): switch to patch bump with user-facing copy --- .changeset/oauth-success-skills-snippet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/oauth-success-skills-snippet.md b/.changeset/oauth-success-skills-snippet.md index 2315f3de..8f12be0f 100644 --- a/.changeset/oauth-success-skills-snippet.md +++ b/.changeset/oauth-success-skills-snippet.md @@ -1,5 +1,5 @@ --- -"clerk": minor +"clerk": patch --- -Promote Clerk Skills on the OAuth success page. After `clerk auth login` redirects back to the local callback, the success page now shows an installer panel with `npx skills add clerk/skills`, a copy button, and a link to `clerk.com/docs/guides/ai/overview`. Page styling moved to CSS custom properties with `prefers-color-scheme` overrides and `color-scheme: light dark` for native form theming. UTF-8 charset is now declared in the response header and meta tag. +After `clerk auth login`, the OAuth success page now points users to Clerk Skills with a copyable install command and a link to the AI building guide. From 5aa8c2444e104f6d66850b81635320fecb6f1d0f Mon Sep 17 00:00:00 2001 From: Railly Date: Tue, 28 Apr 2026 12:59:55 -0500 Subject: [PATCH 5/5] fix(auth): drop SUCCESS_HTML/ERROR_HTML exports to satisfy harness type The integration test harness types itself against `typeof import("../auth-server")`. Adding new exports broke that satisfies check. These constants were exported earlier for a local preview script that has since been removed, so they go back to module-private. --- packages/cli-core/src/lib/auth-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli-core/src/lib/auth-server.ts b/packages/cli-core/src/lib/auth-server.ts index 56983347..ec6d8f02 100644 --- a/packages/cli-core/src/lib/auth-server.ts +++ b/packages/cli-core/src/lib/auth-server.ts @@ -129,7 +129,7 @@ const COPY_SCRIPT = ` } `; -export const SUCCESS_HTML = ` +const SUCCESS_HTML = ` Clerk CLI @@ -156,7 +156,7 @@ export const SUCCESS_HTML = ` `; -export const ERROR_HTML = (message: string) => ` +const ERROR_HTML = (message: string) => ` Clerk CLI