Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6f55b85
feat(ios): inspect native macOS app targets
seanperez29 Aug 29, 2026
7ea93b4
feat(native): configure local macOS projects
seanperez29 Aug 29, 2026
1ac2c39
feat(native): support macOS remote reconciliation
seanperez29 Aug 29, 2026
b684dde
feat(doctor): support native macOS projects
seanperez29 Aug 29, 2026
f40e302
fix(native): align macOS setup integration
seanperez29 Aug 29, 2026
55220de
feat(ios): prepare macOS network capability
seanperez29 Aug 29, 2026
a677d64
feat(native): complete macOS setup support
seanperez29 Aug 29, 2026
168217f
fix(doctor): audit explicit native targets
seanperez29 Aug 29, 2026
e536785
fix(native): honor synchronized platform filters
seanperez29 Aug 29, 2026
3f382f4
fix(native): reject unknown synchronized filters
seanperez29 Aug 29, 2026
3358130
test(doctor): cover platform-aware SDK checks
seanperez29 Aug 29, 2026
8b3e694
fix(doctor): describe macOS target support
seanperez29 Aug 29, 2026
4be25fe
fix(native): inspect active Swift platform branches
seanperez29 Aug 29, 2026
9ed81fb
fix(native): preserve platform certainty
seanperez29 Aug 29, 2026
27114a8
fix(native): make final scaffold platform-aware
seanperez29 Aug 30, 2026
91491ed
fix(native): handle unresolved platform state
seanperez29 Aug 30, 2026
90a475f
fix(macos): ignore network settings when unsandboxed
seanperez29 Aug 30, 2026
fd08e27
Support macOS capabilities in multiplatform targets
seanperez29 Aug 30, 2026
39df6cd
fix(init): link Clerk SDK across Apple platforms
seanperez29 Aug 30, 2026
8bb2c4a
test(init): audit multiplatform setup flows
seanperez29 Aug 30, 2026
5d42d4a
fix(macos): validate multiplatform auth setup
seanperez29 Aug 30, 2026
f6340ef
fix(macos): validate multiplatform setup boundaries
seanperez29 Aug 30, 2026
64032c6
fix(macos): aggregate Apple entitlement intent
seanperez29 Aug 30, 2026
87daa0f
fix(doctor): fail malformed Xcode inspections
seanperez29 Aug 30, 2026
76ecc92
fix(ios): block unmodeled multiplatform targets
seanperez29 Aug 30, 2026
ad098fa
fix(macos): read application identifier entitlement
seanperez29 Aug 30, 2026
674f761
docs(doctor): keep native diagnostics static
seanperez29 Aug 31, 2026
3e9bc03
refactor(init): unify native setup lifecycle
seanperez29 Aug 31, 2026
55b58d3
fix(init): clarify visionOS setup boundary
seanperez29 Aug 31, 2026
335b741
docs(init): qualify visionOS refusal wording
seanperez29 Aug 31, 2026
2d4ffa2
fix(init): block unmodeled Mac Catalyst targets
seanperez29 Aug 31, 2026
abfd0d4
fix(init): detect SDKROOT-only Catalyst targets
seanperez29 Aug 31, 2026
e5847c1
docs(changeset): describe native macOS setup
seanperez29 Aug 31, 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
5 changes: 5 additions & 0 deletions .changeset/macos-native-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add native macOS project support to `clerk init` and `clerk doctor`.
14 changes: 14 additions & 0 deletions packages/cli-core/src/cli-program.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ test("does not register the removed clerk skill command", () => {
expect(skill).toBeUndefined();
});

test("doctor help describes static iOS and macOS target audits", () => {
const program = createProgram();
const doctor = program.commands.find((command) => command.name() === "doctor")!;
const help = doctor.helpInformation();

expect(help).toContain("Select an iOS or macOS application target");
expect(help).toContain("clerk doctor --target MyApp");
expect(help).toContain("Audit a specific iOS or macOS application");
expect(help).not.toContain("--build");
expect(help).not.toContain("--resolve-packages");
expect(help).not.toContain("--simulator");
expect(help).not.toContain("--device");
});

test("registers users create and list as subcommands", () => {
const program = createProgram();
const users = program.commands.find((command) => command.name() === "users")!;
Expand Down
36 changes: 20 additions & 16 deletions packages/cli-core/src/commands/doctor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ clerk doctor --target MyApp

## Options

| Flag | Description |
| ------------- | ----------------------------------------------------- |
| `--verbose` | Show detailed diagnostic info for each check |
| `--json` | Output results as machine-readable JSON |
| `--spotlight` | Only show warnings and failures (hide passing checks) |
| `--fix` | Offer to auto-fix issues with known remedies |
| `--target` | Select an iOS application target by name or object ID |
| Flag | Description |
| ------------- | -------------------------------------------------------------- |
| `--verbose` | Show detailed diagnostic info for each check |
| `--json` | Output results as machine-readable JSON |
| `--spotlight` | Only show warnings and failures (hide passing checks) |
| `--fix` | Offer to auto-fix issues with known remedies |
| `--target` | Select an iOS or macOS application target by name or object ID |

## Checks

Expand All @@ -34,29 +34,33 @@ clerk doctor --target MyApp
| Project linkage | Project | Current directory is linked to a Clerk app |
| Linked application | Project | Linked application ID is accessible via the API |
| Instances | Project | Configured dev/prod instance IDs match the application's instances |
| Environment variables | Environment | Non-iOS projects have Clerk keys in `.env.local` or `.env` |
| Environment variables | Environment | Projects without a supported iOS or macOS app have Clerk keys in `.env.local` or `.env` |
| CLI configuration | Configuration | CLI config file exists and parses |
| Shell completion | Configuration | Shell autocompletion is installed for the detected shell |
| MCP server | Integration | If a Clerk MCP entry is installed, every distinct configured server answers the `initialize` handshake; warns on an unreadable client config (skipped when nothing is installed; warns, never fails) |

### iOS projects
### iOS and macOS projects

When the current directory contains an Xcode project or `--target` is provided,
doctor replaces the web `.env` check with the same semantic Xcode, Swift, and
entitlements inspection used by `clerk init`. It reports separate results for:
When the current directory contains a supported iOS or macOS application target,
or `--target` is provided, Doctor replaces the web `.env` check with the same
semantic Xcode, Swift, and entitlements inspection used by `clerk init`. It
reports separate results for:

- application-target selection;
- ClerkKit and ClerkKitUI product linkage;
- `Clerk.configure` and, for direct literal configuration, the selected target's effective development key;
- SwiftUI environment injection and authentication-flow evidence;
- AuthView's enabled methods and required local Apple capability;
- Associated Domains and the optional Sign in with Apple entitlement;
- iOS Associated Domains or the macOS outgoing-network capability;
- the optional Sign in with Apple entitlement;
- Native API state and the exact Bundle ID registration on the linked
development instance; and
- the Clerk Apple connection when the selected target already declares the
native Apple entitlement.

iOS diagnostics never require a secret key in the Xcode project or an env
Doctor can still inspect and diagnose an iOS or macOS target that also ships visionOS or explicitly enables Mac Catalyst. It reports that platform boundary as a failure instead of treating the integration as ready for automatic setup; `clerk init` then applies no new Clerk setup changes and performs no remote writes. Doctor itself remains read-only.

Native Apple diagnostics never require a secret key in the Xcode project or an env
file. A direct literal publishable key is compared with the linked development
application using only redacted Frontend API host metadata. For a single
startup `Clerk.configure` call that uses a custom publishable-key source,
Expand Down Expand Up @@ -157,8 +161,8 @@ Exit code 1 signals one or more checks failed.
| ------ | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `GET` | `/oauth/userinfo` | Validates the stored auth token |
| `GET` | `/v1/platform/applications/{appId}` | Verifies the linked app and its instances exist |
| `GET` | `/v1/platform/applications/{appId}/instances/{instanceId}/native_settings` | Verifies Native API state for iOS projects |
| `GET` | `/v1/platform/applications/{appId}/instances/{instanceId}/native_applications/ios` | Verifies the exact iOS Bundle ID registration |
| `GET` | `/v1/platform/applications/{appId}/instances/{instanceId}/native_settings` | Verifies Native API state for iOS and macOS projects |
| `GET` | `/v1/platform/applications/{appId}/instances/{instanceId}/native_applications/ios` | Verifies the exact native Apple Bundle ID registration |
| `GET` | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Audits the Apple connection when native Apple is relevant |
| `GET` | `/v1/platform/applications/{appId}/instances/{instanceId}/config/schema` | Determines whether an unhealthy Apple connection can be safely reconciled by init |
| `GET` | `https://{fapiHost}/v1/environment` | Verifies whether AuthView currently offers native Apple sign-in |
Expand Down
178 changes: 177 additions & 1 deletion packages/cli-core/src/commands/doctor/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,44 @@ const IOS_FRAMEWORK: FrameworkInfo = {
ecosystem: "swift",
};

const IOS_INSPECTION = {} as IOSProjectInspectionResult;
const IOS_INSPECTION = {
platform: "ios",
appTargets: [{ platform: "ios" }],
selection: { state: "selected", platform: "ios" },
} as IOSProjectInspectionResult;
const MACOS_INSPECTION = {
platform: "macos",
appTargets: [{ platform: "macos" }],
selection: { state: "selected", platform: "macos" },
} as IOSProjectInspectionResult;
const UNSUPPORTED_XCODE_INSPECTION = {
schemaVersion: 1,
platform: "apple-native",
root: "/fixture",
workspaces: [],
projects: [],
appTargets: [],
selection: { state: "none" },
localPublishableKey: { state: "missing" },
generatedProject: null,
diagnostics: [
{
code: "xcode.no-ios-app-target",
severity: "error",
message: "No supported iOS or macOS application target was found.",
evidence: [],
},
],
} as IOSProjectInspectionResult;
const MISSING_TARGET_INSPECTION = {
...UNSUPPORTED_XCODE_INSPECTION,
platform: "ios",
selection: {
state: "not-found",
requested: "MissingApp",
candidates: ["MyApp (APP_TARGET)"],
},
} as IOSProjectInspectionResult;
const DOCTOR_CONTEXT = {} as DoctorContext;

function passingResult(name: string): CheckResult {
Expand All @@ -25,6 +62,7 @@ function passingResult(name: string): CheckResult {
function runDependencies(overrides: Partial<DoctorRunDependencies> = {}): DoctorRunDependencies {
return {
detectFramework: async () => IOS_FRAMEWORK,
inspectIOSProject: async () => IOS_INSPECTION,
getDoctorChecks: () => [async () => passingResult("Common")],
runIOSDoctorChecks: async () => ({
inspection: IOS_INSPECTION,
Expand All @@ -41,6 +79,144 @@ describe("getDoctorChecks", () => {
});
});

describe("Apple-native framework routing", () => {
test("keeps a pure macOS application on native Clerk checks", async () => {
let nativeChecks = false;
const results = await runChecks(
DOCTOR_CONTEXT,
{},
{
dependencies: runDependencies({
inspectIOSProject: async () => MACOS_INSPECTION,
getDoctorChecks: (native) => {
nativeChecks = native;
return [async () => passingResult("Common")];
},
runIOSDoctorChecks: async (_ctx, options) => ({
inspection: options.preparedInspection ?? MACOS_INSPECTION,
results: [passingResult("macOS")],
}),
}),
},
);

expect(nativeChecks).toBeTrue();
expect(results.map((result) => result.name)).toEqual(["Common", "macOS"]);
});

test("uses ordinary checks for an unsupported Xcode-only project", async () => {
let nativeChecks = true;
let nativeAuditCalls = 0;
const results = await runChecks(
DOCTOR_CONTEXT,
{},
{
dependencies: runDependencies({
inspectIOSProject: async () => UNSUPPORTED_XCODE_INSPECTION,
getDoctorChecks: (native) => {
nativeChecks = native;
return [async () => passingResult(native ? "Native" : "Environment variables")];
},
runIOSDoctorChecks: async () => {
nativeAuditCalls++;
return { inspection: UNSUPPORTED_XCODE_INSPECTION, results: [] };
},
}),
},
);

expect(nativeChecks).toBeFalse();
expect(nativeAuditCalls).toBe(0);
expect(results.map((result) => result.name)).toEqual(["Environment variables"]);
});

test.each([
["xcode.malformed-project", "Could not parse App.xcodeproj/project.pbxproj."],
["xcode.missing-project-file", "App.xcodeproj does not contain project.pbxproj."],
] as const)("fails native inspection for %s", async (code, message) => {
const failedInspection = {
...UNSUPPORTED_XCODE_INSPECTION,
diagnostics: [
{
code,
severity: "error" as const,
message,
remedy: "Repair the Xcode project file.",
evidence: [],
},
],
} as IOSProjectInspectionResult;
let nativeChecks = false;
let nativeAuditCalls = 0;

const results = await runChecks(
DOCTOR_CONTEXT,
{},
{
dependencies: runDependencies({
inspectIOSProject: async () => failedInspection,
getDoctorChecks: (native) => {
nativeChecks = native;
return [async () => passingResult(native ? "Native" : "Environment variables")];
},
runIOSDoctorChecks: async () => {
nativeAuditCalls++;
return { inspection: failedInspection, results: [] };
},
}),
},
);

expect(nativeChecks).toBeTrue();
expect(nativeAuditCalls).toBe(0);
expect(results).toEqual([
passingResult("Native"),
{
name: "Apple-native inspection",
status: "fail",
message: "Apple-native project inspection failed",
detail: message,
remedy: "Repair the Xcode project file.",
},
]);
});

test("routes a missing explicit target through the native audit", async () => {
let nativeChecks = false;
let nativeAuditCalls = 0;
const results = await runChecks(
DOCTOR_CONTEXT,
{ target: "MissingApp" },
{
dependencies: runDependencies({
inspectIOSProject: async () => MISSING_TARGET_INSPECTION,
getDoctorChecks: (native) => {
nativeChecks = native;
return [async () => passingResult("Common")];
},
runIOSDoctorChecks: async (_ctx, options) => {
nativeAuditCalls++;
expect(options.preparedInspection).toBe(MISSING_TARGET_INSPECTION);
return {
inspection: MISSING_TARGET_INSPECTION,
results: [
{
name: "iOS: Select the iOS application target",
status: "fail",
message: 'The requested target "MissingApp" was not found.',
},
],
};
},
}),
},
);

expect(nativeChecks).toBeTrue();
expect(nativeAuditCalls).toBe(1);
expect(results.some((result) => result.status === "fail")).toBeTrue();
});
});
describe("doctor telemetry stages", () => {
test("reports the ordered native diagnostic boundaries", async () => {
const stage = spyOn(telemetryMod, "setTelemetryStage");
Expand Down
Loading
Loading