@@ -11,6 +11,7 @@ import path from "node:path";
1111import { isCancel , select } from "@clack/prompts" ;
1212import {
1313 createProject ,
14+ getProject ,
1415 listOrganizations ,
1516 tryGetPrimaryDsn ,
1617} from "../api-client.js" ;
@@ -702,6 +703,56 @@ async function resolveOrgSlug(
702703 return selected ;
703704}
704705
706+ /**
707+ * Try to fetch an existing project by org + slug. Returns a successful
708+ * LocalOpResult if the project exists, or null if it doesn't (404).
709+ * Other errors are left to propagate.
710+ */
711+ async function tryGetExistingProject (
712+ orgSlug : string ,
713+ projectSlug : string
714+ ) : Promise < LocalOpResult | null > {
715+ try {
716+ const project = await getProject ( orgSlug , projectSlug ) ;
717+ const dsn = await tryGetPrimaryDsn ( orgSlug , project . slug ) ;
718+ const url = buildProjectUrl ( orgSlug , project . slug ) ;
719+ return {
720+ ok : true ,
721+ data : {
722+ orgSlug,
723+ projectSlug : project . slug ,
724+ projectId : project . id ,
725+ dsn : dsn ?? "" ,
726+ url,
727+ } ,
728+ } ;
729+ } catch ( error ) {
730+ // 404 means project doesn't exist — fall through to creation
731+ if ( error instanceof ApiError && error . status === 404 ) {
732+ return null ;
733+ }
734+ throw error ;
735+ }
736+ }
737+
738+ /**
739+ * Resolve the org slug from CLI args, env, config, or interactive prompt.
740+ * Returns the org slug string, or a LocalOpResult if resolution fails or is cancelled.
741+ */
742+ async function resolveOrgForInit (
743+ cwd : string ,
744+ options : WizardOptions
745+ ) : Promise < string | LocalOpResult > {
746+ if ( options . org ) {
747+ return options . org ;
748+ }
749+ const orgResult = await resolveOrgSlug ( cwd , options . yes ) ;
750+ if ( typeof orgResult !== "string" ) {
751+ return orgResult ;
752+ }
753+ return orgResult ;
754+ }
755+
705756async function createSentryProject (
706757 payload : CreateSentryProjectPayload ,
707758 options : WizardOptions
@@ -732,35 +783,40 @@ async function createSentryProject(
732783 }
733784
734785 try {
735- // 1. Resolve org — skip interactive resolution if explicitly provided via CLI arg
736- let orgSlug : string ;
737- if ( options . org ) {
738- orgSlug = options . org ;
739- } else {
740- const orgResult = await resolveOrgSlug ( payload . cwd , options . yes ) ;
741- if ( typeof orgResult !== "string" ) {
742- return orgResult ;
786+ // 1. Resolve org
787+ const orgResult = await resolveOrgForInit ( payload . cwd , options ) ;
788+ if ( typeof orgResult !== "string" ) {
789+ return orgResult ;
790+ }
791+ const orgSlug = orgResult ;
792+
793+ // 2. If both org and project were provided, check if the project already exists.
794+ // This avoids a 409 Conflict from the create API when re-running init on an
795+ // existing Sentry project (e.g., bare slug resolved via resolveProjectBySlug).
796+ if ( options . org && options . project ) {
797+ const existing = await tryGetExistingProject ( orgSlug , slug ) ;
798+ if ( existing ) {
799+ return existing ;
743800 }
744- orgSlug = orgResult ;
745801 }
746802
747- // 2 . Resolve or create team
803+ // 3 . Resolve or create team
748804 const team = await resolveOrCreateTeam ( orgSlug , {
749805 team : options . team ,
750806 autoCreateSlug : slug ,
751807 usageHint : "sentry init" ,
752808 } ) ;
753809
754- // 3 . Create project
810+ // 4 . Create project
755811 const project = await createProject ( orgSlug , team . slug , {
756812 name,
757813 platform,
758814 } ) ;
759815
760- // 4 . Get DSN (best-effort)
816+ // 5 . Get DSN (best-effort)
761817 const dsn = await tryGetPrimaryDsn ( orgSlug , project . slug ) ;
762818
763- // 5 . Build URL
819+ // 6 . Build URL
764820 const url = buildProjectUrl ( orgSlug , project . slug ) ;
765821
766822 return {
@@ -774,14 +830,17 @@ async function createSentryProject(
774830 } ,
775831 } ;
776832 } catch ( error ) {
777- let message : string ;
778- if ( error instanceof ApiError ) {
779- message = error . format ( ) ;
780- } else if ( error instanceof Error ) {
781- message = error . message ;
782- } else {
783- message = String ( error ) ;
784- }
785- return { ok : false , error : message } ;
833+ return { ok : false , error : formatLocalOpError ( error ) } ;
834+ }
835+ }
836+
837+ /** Format an error from a local-op into a user-facing message string. */
838+ function formatLocalOpError ( error : unknown ) : string {
839+ if ( error instanceof ApiError ) {
840+ return error . format ( ) ;
841+ }
842+ if ( error instanceof Error ) {
843+ return error . message ;
786844 }
845+ return String ( error ) ;
787846}
0 commit comments