@@ -705,65 +705,101 @@ export class CherryPickPreviewModal extends Modal {
705705 const clonedRepos : string [ ] = [ ] ;
706706
707707 try {
708- const adapter = this . app . vault . adapter as any ;
708+ const adapter = this . app . vault . adapter as { basePath ?: string } ;
709709 const vaultPath = adapter . basePath || '' ;
710710 const path = require ( 'path' ) ;
711711 const fs = require ( 'fs' ) . promises ;
712712
713- // Read the .udd file to get submodule Radicle IDs
714- const uddPath = path . join ( vaultPath , repoName , '.udd' ) ;
715- let uddContent : string ;
713+ // rc.21: read submodules from .gitmodules (URL-based) rather than
714+ // the legacy .udd.submodules array (which held Radicle IDs). Each
715+ // entry's URL is `interbrain://<uuid>`; the submodule's path under
716+ // the parent doubles as its sovereign repo name at the vault root.
717+ const gitmodulesPath = path . join ( vaultPath , repoName , '.gitmodules' ) ;
718+ let gitmodulesContent : string ;
716719 try {
717- uddContent = await fs . readFile ( uddPath , 'utf-8' ) ;
720+ gitmodulesContent = await fs . readFile ( gitmodulesPath , 'utf-8' ) ;
718721 } catch {
719- console . log ( `[BeaconPreview] No .udd file found for ${ repoName } , skipping submodule scan` ) ;
722+ console . log ( `[BeaconPreview] No .gitmodules in ${ repoName } , skipping submodule scan` ) ;
720723 return clonedRepos ;
721724 }
722725
723- const udd = JSON . parse ( uddContent ) ;
724- const submoduleIds : string [ ] = udd . submodules || [ ] ;
726+ // Parse `[submodule "Name"]` blocks. We need both `path` and `url`.
727+ type ParsedSub = { name : string ; path : string ; uuid : string } ;
728+ const submodules : ParsedSub [ ] = [ ] ;
729+ const sectionRegex = / \[ s u b m o d u l e " ( [ ^ " ] + ) " \] ( [ \s \S ] * ?) (? = \n \[ | $ ) / g;
730+ let m : RegExpExecArray | null ;
731+ while ( ( m = sectionRegex . exec ( gitmodulesContent ) ) !== null ) {
732+ const name = m [ 1 ] ;
733+ const body = m [ 2 ] ;
734+ const pathMatch = body . match ( / ^ \s * p a t h \s * = \s * ( .+ ) $ / m) ;
735+ const urlMatch = body . match ( / ^ \s * u r l \s * = \s * i n t e r b r a i n : \/ \/ ( [ 0 - 9 a - f A - F - ] + ) / m) ;
736+ if ( pathMatch && urlMatch ) {
737+ submodules . push ( { name, path : pathMatch [ 1 ] . trim ( ) , uuid : urlMatch [ 1 ] . trim ( ) } ) ;
738+ }
739+ }
725740
726- if ( submoduleIds . length === 0 ) {
727- console . log ( `[BeaconPreview] No submodules in ${ repoName } ` ) ;
741+ if ( submodules . length === 0 ) {
742+ console . log ( `[BeaconPreview] No interbrain:// submodules in ${ repoName } /.gitmodules ` ) ;
728743 return clonedRepos ;
729744 }
730745
731- console . log ( `[BeaconPreview] Found ${ submoduleIds . length } submodule(s) in ${ repoName } : ${ submoduleIds . join ( ', ' ) } ` ) ;
746+ console . log ( `[BeaconPreview] Found ${ submodules . length } submodule(s) in ${ repoName } : ${ submodules . map ( s => s . name ) . join ( ', ' ) } ` ) ;
732747
733- // Get existing repos in vault to check what's already there
748+ // Existing top-level DreamNodes (by UUID — id === udd.uuid in the store).
734749 const store = useInterBrainStore . getState ( ) ;
735- const existingRadicleIds = new Set (
736- Array . from ( store . dreamNodes . values ( ) )
737- . map ( data => data . node . radicleId )
738- . filter ( id => id )
750+ const existingUuids = new Set (
751+ Array . from ( store . dreamNodes . values ( ) ) . map ( data => data . node . id )
739752 ) ;
740753
741754 const uriHandler = getURIHandlerService ( ) ;
742755
743- for ( const radicleId of submoduleIds ) {
744- // Check if already exists by Radicle ID
745- if ( existingRadicleIds . has ( radicleId ) ) {
746- console . log ( `[BeaconPreview] Submodule ${ radicleId } already exists, skipping` ) ;
756+ // Where to fetch this submodule from when the parent's transitivity
757+ // would have applied. We mirror the same logic the helper uses:
758+ // derive owner from one of the parent's GitHub remotes.
759+ const parentFullPath = path . join ( vaultPath , repoName ) ;
760+ const peerCandidates = await this . deriveOwnersFromGitRemotes ( parentFullPath ) ;
761+ if ( peerCandidates . length === 0 ) {
762+ console . warn ( `[BeaconPreview] No GitHub remotes on ${ repoName } — can't derive submodule clone sources` ) ;
763+ return clonedRepos ;
764+ }
765+
766+ for ( const sub of submodules ) {
767+ if ( existingUuids . has ( sub . uuid ) ) {
768+ console . log ( `[BeaconPreview] Submodule "${ sub . name } " (uuid=${ sub . uuid } ) already in vault, skipping sovereign clone` ) ;
747769 continue ;
748770 }
749771
750- // Clone the submodule
751- console . log ( `[BeaconPreview] Cloning missing submodule: ${ radicleId } ` ) ;
752- this . showProcessing ( `Cloning submodule...` ) ;
753-
754- const result = await uriHandler . cloneFromRadicle ( radicleId , true ) ; // silent mode
755-
756- if ( result . status === 'success' && result . repoName ) {
757- console . log ( `[BeaconPreview] Cloned submodule: ${ radicleId } → "${ result . repoName } "` ) ;
758- clonedRepos . push ( result . repoName ) ;
772+ // The submodule's repo name on each peer's outbox matches its
773+ // .gitmodules name by rc.21 convention (we ship Circle as
774+ // <owner>/Circle).
775+ let cloned : 'success' | 'skipped' | 'error' = 'error' ;
776+ let actualName = sub . name ;
777+ for ( const owner of peerCandidates ) {
778+ const repoPath = `${ owner } /${ sub . name } ` ;
779+ console . log ( `[BeaconPreview] Trying sovereign clone: ${ repoPath } ` ) ;
780+ this . showProcessing ( `Cloning ${ sub . name } …` ) ;
781+ try {
782+ const status = await uriHandler . cloneFromGitHub ( repoPath , true ) ;
783+ if ( status === 'success' || status === 'skipped' ) {
784+ cloned = status ;
785+ actualName = sub . name ;
786+ break ;
787+ }
788+ } catch ( cloneError ) {
789+ console . warn ( `[BeaconPreview] cloneFromGitHub(${ repoPath } ) threw:` , cloneError ) ;
790+ }
791+ }
759792
760- // Recursively clone nested submodules
761- const nestedClones = await this . cloneMissingSubmodules ( result . repoName ) ;
762- clonedRepos . push ( ...nestedClones ) ;
763- } else if ( result . status === 'skipped' ) {
764- console . log ( `[BeaconPreview] Submodule ${ radicleId } already existed as "${ result . repoName } "` ) ;
793+ if ( cloned === 'success' ) {
794+ console . log ( `[BeaconPreview] Cloned "${ actualName } " sovereign at vault root` ) ;
795+ clonedRepos . push ( actualName ) ;
796+ // Recurse — newly arrived node may have its own submodules.
797+ const nested = await this . cloneMissingSubmodules ( actualName ) ;
798+ clonedRepos . push ( ...nested ) ;
799+ } else if ( cloned === 'skipped' ) {
800+ console . log ( `[BeaconPreview] Submodule "${ actualName } " already cloned at vault root` ) ;
765801 } else {
766- console . warn ( `[BeaconPreview] Failed to clone submodule ${ radicleId } ` ) ;
802+ console . warn ( `[BeaconPreview] Failed to sovereign- clone " ${ sub . name } " from any peer ` ) ;
767803 }
768804 }
769805 } catch ( error ) {
@@ -773,6 +809,35 @@ export class CherryPickPreviewModal extends Modal {
773809 return clonedRepos ;
774810 }
775811
812+ /**
813+ * Read a git repo's remotes and extract the GitHub owner from each.
814+ * Used to figure out where to fetch a sovereign submodule from.
815+ */
816+ private async deriveOwnersFromGitRemotes ( repoPath : string ) : Promise < string [ ] > {
817+ try {
818+ const { exec } = require ( 'child_process' ) ;
819+ const { promisify } = require ( 'util' ) ;
820+ const execAsync = promisify ( exec ) ;
821+ const { stdout } = await execAsync ( 'git remote -v' , { cwd : repoPath } ) ;
822+ const owners : string [ ] = [ ] ;
823+ const seen = new Set < string > ( ) ;
824+ for ( const line of ( stdout as string ) . split ( '\n' ) ) {
825+ // line: "<name>\t<url> (fetch|push)"
826+ const parts = line . split ( / \s + / ) ;
827+ if ( parts . length < 2 ) continue ;
828+ const url = parts [ 1 ] ;
829+ const m = url . match ( / g i t h u b \. c o m [ / : ] ( [ ^ / ] + ) \/ / ) ;
830+ if ( m && ! seen . has ( m [ 1 ] ) ) {
831+ seen . add ( m [ 1 ] ) ;
832+ owners . push ( m [ 1 ] ) ;
833+ }
834+ }
835+ return owners ;
836+ } catch {
837+ return [ ] ;
838+ }
839+ }
840+
776841 /**
777842 * Preview a coherence beacon commit by opening the supermodule's DreamSong
778843 * (clones first if needed)
0 commit comments