Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
wyattjoh May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy [options] Deploy a Clerk application to production
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand DownExpand Up@@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All@@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand DownExpand Up@@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
wyattjoh May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy [options] Deploy a Clerk application to production
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand DownExpand Up@@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All@@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand DownExpand Up@@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
wyattjoh May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy [options] Deploy a Clerk application to production
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand DownExpand Up@@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All@@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand DownExpand Up@@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
wyattjoh May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy [options] Deploy a Clerk application to production
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand DownExpand Up@@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All@@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand DownExpand Up@@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
wyattjoh May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy [options] Deploy a Clerk application to production
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand DownExpand Up@@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All@@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand DownExpand Up@@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
wyattjoh May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy [options] Deploy a Clerk application to production
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand DownExpand Up@@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All@@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand DownExpand Up@@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
wyattjoh May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy [options] Deploy a Clerk application to production
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand DownExpand Up@@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All@@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand DownExpand Up@@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eb5b8e7
refactor(deploy): extract poll core into status.ts
wyattjoh May 28, 2026
7314f67
refactor(deploy): move state resolution into status.ts, rename dnsCom…
wyattjoh May 28, 2026
76e9f3c
feat(deploy): add resolveDeployState discriminator
wyattjoh May 28, 2026
52050cf
feat(deploy): add buildDeployStatusReport payload builder
wyattjoh May 28, 2026
3d406df
feat(deploy): add clerk deploy check command
wyattjoh May 28, 2026
a619953
feat(deploy): tailor agent-mode deploy into a read-only handoff
wyattjoh May 28, 2026
10990d3
feat(deploy): register clerk deploy check subcommand
wyattjoh May 28, 2026
20871b5
docs(deploy): document deploy check and agent handoff
wyattjoh May 28, 2026
6a9758c
fix(deploy): surface agent status read failures
wyattjoh May 28, 2026
1cc3d64
docs(clerk-cli): document deploy agent workflow
wyattjoh May 28, 2026
e423a63
fix(deploy): avoid backoff in agent check
wyattjoh May 28, 2026
cc0d804
fix(deploy): check domain status as one DNS verification
wyattjoh May 28, 2026
a201308
fix(deploy): include domains URL in agent next action
wyattjoh May 28, 2026
94ba6e4
fix(deploy): prompt agents to open domains URL
wyattjoh May 28, 2026
a7e24fb
docs(clerk-cli): warn deploy wizard needs a terminal
wyattjoh May 28, 2026
3f355d9
fix(cli): address deploy review follow-ups
wyattjoh May 29, 2026
b282011
fix(deploy): persist live production instance metadata
wyattjoh May 29, 2026
ae2e086
chore: added link for codex
wyattjoh May 29, 2026
dbfeca5
docs(clerk-cli): clarify deploy check agent workflow
wyattjoh May 29, 2026
14ff7c0
refactor(deploy): remove unused status check helper
wyattjoh May 29, 2026
1274f60
test(deploy): avoid leaking deploy check mocks
wyattjoh May 29, 2026
94ea5e2
docs(testing): document bun test isolation
wyattjoh May 29, 2026
53e0a5a
docs: remove hidden bird command from readme
wyattjoh May 29, 2026
a419b77
feat(deploy): rename status check command
wyattjoh May 29, 2026
ba8fcdd
fix(deploy): humanize status dashboard guidance
wyattjoh May 29, 2026
19caa25
refactor(deploy): tighten status resolution
wyattjoh May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/audit-clerk-skill
1 change: 1 addition & 0 deletions .agents/skills/changesets
5 changes: 5 additions & 0 deletions .changeset/deploy-status.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk deploy status`, a read-only command that verifies a production deploy, including DNS, SSL, email DNS, and OAuth credential completeness. Agent-mode `clerk deploy` now emits a tailored read-only handoff instead of a hard usage error.
2 changes: 2 additions & 0 deletions .claude/rules/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,8 @@ bun run test

This runs each unit and integration test file as a separate `bun test` subprocess via `scripts/run-tests.ts`, isolating module state between files. E2E fixtures are excluded and require separate setup (see `rules/e2e.md`).

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures.

Prefer `spyOn()` for mocking, and always restore spies in `afterAll` with `mockRestore()`.

Never use `for` or `forEach` loops inside a single test to verify multiple inputs or cases — use `test.each` (or `it.each` / `describe.each`) so each case is its own reported test case with its own name, setup/teardown, and pinpointed failure output.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,6 +52,8 @@ Locally, prefer `bun run test:e2e:op` so secrets are injected from 1Password in-

CI runs `bun run format:check` (fails if unformatted), `bun run lint`, `bun test`, and `bun run test:e2e` on every PR to `main`. E2E tests only run for PRs from the same repository (not external forks) and target the production Clerk API with a dedicated test application.

When running multiple test files directly with `bun test`, always pass `--isolate` or `--parallel`. `--parallel` implies `--isolate`. Without isolation, Bun can share module mocks across files and produce order-dependent failures. Prefer `bun run test` for the full suite because it already isolates test files through `scripts/run-tests.ts`.

## Versioning

The `CLI_VERSION` global is injected at compile time via `bun build --compile --define "CLI_VERSION=..."`. Local `build:compile` omits it, so the binary reports `0.0.0-dev`. The CI release workflow injects the real version.
2 changes: 1 addition & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,7 @@ Commands:
completion [shell] Generate shell autocompletion script
skill Manage the bundled Clerk CLI agent skill
update [options] Update the Clerk CLI to the latest version
deploy [options] Deploy a Clerk application to production
deploy Deploy a Clerk application to production
help [command] Display help for command

Give AI agents better Clerk context: install the Clerk skills
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -53,6 +53,15 @@ test("deploy relies on global options", () => {
expect(optionNames).toEqual([]);
});

test("deploy status exposes wait option", () => {
const program = createProgram();
const deploy = program.commands.find((command) => command.name() === "deploy")!;
const status = deploy.commands.find((command) => command.name() === "status")!;
const optionNames = status.options.map((option) => option.long);

expect(optionNames).toContain("--wait");
});

describe("parseIntegerOption (via users list --limit / --offset)", () => {
function parseUsersList(args: readonly string[]) {
return createProgram().parseAsync(["users", "list", ...args], { from: "user" });
Expand Down
11 changes: 10 additions & 1 deletion packages/cli-core/src/cli-program.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,6 +46,7 @@ import { log } from "./lib/log.ts";
import { maybeNotifyUpdate, getCurrentVersion } from "./lib/update-check.ts";
import { update } from "./commands/update/index.ts";
import { deploy } from "./commands/deploy/index.ts";
import { deployStatus } from "./commands/deploy/status-command.ts";
import { isClerkSkillInstalled } from "./lib/skill-detection.ts";
import { orgsEnable, orgsDisable } from "./commands/orgs/index.ts";
import { billingEnable, billingDisable } from "./commands/billing/index.ts";
Expand DownExpand Up@@ -926,7 +927,15 @@ Tutorial — enable completions for your shell:
])
.action(update);

program.command("deploy").description("Deploy a Clerk application to production").action(deploy);
const deployCmd = program
.command("deploy")
.description("Deploy a Clerk application to production");
deployCmd.command("run", { isDefault: true, hidden: true }).action(deploy);
deployCmd
.command("status")
.description("Show production deploy status (read-only)")
.option("--wait", "Wait for DNS, SSL, and email DNS verification with retries")
.action(deployStatus);

registerExtras(program);

Expand Down
41 changes: 41 additions & 0 deletions packages/cli-core/src/commands/auth/login.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -89,6 +89,7 @@ mock.module("../../lib/autoclaim.ts", () => ({
attemptAutoclaim: async () => ({ status: "not_keyless" }),
}));

const { setLogLevel } = await import("../../lib/log.ts");
const { login } = await import("./login.ts");

describe("login", () => {
Expand All@@ -114,6 +115,7 @@ describe("login", () => {
mockEnsureFirstApplication.mockResolvedValue(undefined);
mockIsHuman.mockReturnValue(false);
mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "test" });
setLogLevel("info");
consoleSpy?.mockRestore();
consoleErrorSpy?.mockRestore();
try {
Expand DownExpand Up@@ -592,6 +594,45 @@ describe("login", () => {
expect(parsed.searchParams.get("clerk_client")).toBe("cli");
});

test("does not emit the OAuth authorize URL through debug logging", async () => {
setLogLevel("debug");
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();

const mockServer = {
port: 54321,
waitForCallback: mock().mockResolvedValue({ code: "fresh-auth-code" }),
stop: mock(),
};
mockStartAuthServer.mockReturnValue(mockServer);

mockExchangeCodeForToken.mockResolvedValue({
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
});
mockCreateOAuthSession.mockReturnValue({
accessToken: "new-access-token",
refreshToken: "new-refresh-token",
expiresAt: 123,
tokenType: "Bearer",
});
mockStoreToken.mockResolvedValue(undefined);
mockFetchUserInfo.mockResolvedValue({
userId: "user_new",
email: "new@example.com",
});
mockSetAuth.mockResolvedValue(undefined);

consoleSpy = spyOn(console, "log").mockImplementation(() => {});
await runLogin({ showNextSteps: false });

expect(captured.err).not.toContain("https://test.example.com/oauth/authorize");
expect(captured.err).not.toContain("test-state-value");
expect(captured.err).not.toContain("test-code-challenge");
});

test("calls ensureFirstApplication after a successful OAuth flow", async () => {
mockGetValidToken.mockResolvedValue(null);
mockBunSpawn();
Expand Down
1 change: 0 additions & 1 deletion packages/cli-core/src/commands/auth/login.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,7 +57,6 @@ async function performOAuthFlow(): Promise<UserInfo> {
// Critical fallback: the OAuth callback can't complete unless the user
// reaches the authorize URL somehow.
const urlString = authorizeUrl.toString();
log.debug(`Opening browser to URL: ${urlString}`);
const result = await openBrowser(urlString);
if (!result.ok) {
log.warn(
Expand Down
Loading