From d4b1ff660ec541686a307a89e74725c1f38f7dbd Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Fri, 11 Sep 2026 08:33:32 +0100 Subject: [PATCH] fix: reject an invalid identity name before offering the creation wizard The @ shortcut passes its name portion through verbatim, so an email-shaped argument like @joseph.mearman@exadev.io reached runIdentityWizard unchanged. The wizard prompted "Create it now?" and only threw InvalidIdentityNameError after the user confirmed, an interaction that offers an action it can never perform. Validate the name against IdentitySchema at the top of the wizard, the single choke point both the @ shortcut and `identity use` route through, so the clean one-line error fires before any prompt appears. --- src/identityManager.test.ts | 4 ++-- src/identityManager.ts | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) 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: [