diff --git a/src/identityManager.test.ts b/src/identityManager.test.ts index 22bac5b..0b06fa1 100644 --- a/src/identityManager.test.ts +++ b/src/identityManager.test.ts @@ -192,8 +192,8 @@ describe("identityManager", () => { expect(readActiveIdentity(paths)).toBeUndefined(); }); - it("throws InvalidIdentityNameError, not a raw ZodError, when the confirmed name fails IdentitySchema's own naming rule", async () => { - const prompts = scriptedIdentityPrompts(["create"]); + it("throws InvalidIdentityNameError before any prompt appears, not a raw ZodError after the user confirms, when the name fails IdentitySchema's own naming rule", async () => { + const prompts = scriptedIdentityPrompts([]); await expect(runIdentityWizard(prompts, paths, "joseph.mearman@exadev.io")).rejects.toThrow( InvalidIdentityNameError, ); diff --git a/src/identityManager.ts b/src/identityManager.ts index ae8e4ab..28d0322 100644 --- a/src/identityManager.ts +++ b/src/identityManager.ts @@ -82,11 +82,16 @@ export function useIdentity(paths: LayoutPaths, name: string): void { /** * The interactive setup wizard for a new identity, offered by the `@` shortcut and `identity use` when the identity doesn't exist yet and stdin is a real terminal. * + * Validates `name` against `IdentitySchema`'s own naming rule before any prompt appears — offering "Create it now?" for a name that could never validate (an email address, say, whose `@` the shortcut passes through verbatim) just to fail on confirm is a broken interaction, so an invalid name throws `InvalidIdentityNameError` immediately instead. + * * Confirms the user wants to create the identity, then optionally creates a default configuration profile (reusing `runProfileWizard`), links them, and sets the identity as active. A cancel at any step writes nothing beyond what was already committed — the identity is only created after the first confirm, and the profile wizard's own cancel handling means a profile-only cancellation still leaves the identity usable. Returns `true` when the identity was created and set active; `false` when the user declined at the initial confirm. * * Driven entirely by the injected `PromptsPort` so the whole flow is unit-testable with a scripted sequence of answers. */ export async function runIdentityWizard(prompts: PromptsPort, paths: LayoutPaths, name: string): Promise { + if (!IdentitySchema.safeParse({ name, allowAmbientCredential: false }).success) { + throw new InvalidIdentityNameError(name); + } const choice = await prompts.select({ message: `No identity named "${name}" exists yet. Create it now?`, options: [