batch-share-service.ts
1 import { Notice, Plugin } from 'obsidian'; 2 import { DreamNode } from '../../dreamnode'; 3 import { githubService } from './github-service'; 4 import { GitDreamNodeService } from '../../dreamnode/services/git-dreamnode-service'; 5 import { UDDService } from '../../dreamnode/services/udd-service'; 6 import { serviceManager } from '../../../core/services/service-manager'; 7 8 /** 9 * GitHub Batch Share Service 10 * 11 * Ensures multiple DreamNodes have GitHub URLs before sharing via email links. 12 * Handles batch GitHub share operations for Windows/GitHub fallback mode. 13 */ 14 export class GitHubBatchShareService { 15 private plugin: Plugin; 16 private dreamNodeService: GitDreamNodeService; 17 18 constructor(plugin: Plugin, dreamNodeService: GitDreamNodeService) { 19 this.plugin = plugin; 20 this.dreamNodeService = dreamNodeService; 21 } 22 23 /** 24 * Ensure all nodes have GitHub URLs, sharing those that don't 25 * Returns map of UUID → GitHub URL 26 */ 27 async ensureNodesHaveGitHubUrls(nodeUUIDs: string[]): Promise<Map<string, string>> { 28 const result = new Map<string, string>(); 29 30 if (nodeUUIDs.length === 0) { 31 return result; 32 } 33 34 try { 35 // Step 1: Load all nodes and check their GitHub status 36 const nodes: DreamNode[] = []; 37 38 for (const uuid of nodeUUIDs) { 39 const node = await this.dreamNodeService.get(uuid); 40 if (node) { 41 nodes.push(node); 42 } else { 43 console.warn(`[GitHubBatchShare] Node ${uuid} not found`); 44 } 45 } 46 47 // Step 2: Separate into already-shared vs needs-sharing 48 const { alreadyShared, needsSharing } = await this.categorizeNodes(nodes); 49 50 // Step 3: Add already-shared nodes to result 51 for (const node of alreadyShared) { 52 const githubUrl = await this.getGitHubUrlFromUdd(node); 53 if (githubUrl) { 54 result.set(node.id, githubUrl); 55 } 56 } 57 58 // Step 4: Batch share nodes that need it 59 if (needsSharing.length > 0) { 60 const notice = new Notice(`Sharing ${needsSharing.length} DreamNode${needsSharing.length > 1 ? 's' : ''} to GitHub...`, 0); 61 62 const shared = await this.batchShareNodes(needsSharing); 63 64 notice.hide(); 65 66 // Add newly shared nodes to result 67 for (const [uuid, githubUrl] of shared) { 68 result.set(uuid, githubUrl); 69 } 70 71 const successCount = shared.size; 72 const failCount = needsSharing.length - successCount; 73 74 if (successCount > 0) { 75 new Notice(`Shared ${successCount} node${successCount > 1 ? 's' : ''} to GitHub`); 76 } 77 78 if (failCount > 0) { 79 console.warn(`[GitHubBatchShare] ${failCount} node(s) failed to share`); 80 } 81 } 82 83 return result; 84 85 } catch (error) { 86 console.error('[GitHubBatchShare] Batch sharing failed:', error); 87 throw error; 88 } 89 } 90 91 /** 92 * Categorize nodes by GitHub sharing status 93 */ 94 private async categorizeNodes(nodes: DreamNode[]): Promise<{ 95 alreadyShared: DreamNode[]; 96 needsSharing: DreamNode[]; 97 }> { 98 const alreadyShared: DreamNode[] = []; 99 const needsSharing: DreamNode[] = []; 100 101 for (const node of nodes) { 102 const githubUrl = await this.getGitHubUrlFromUdd(node); 103 104 if (githubUrl) { 105 alreadyShared.push(node); 106 } else { 107 needsSharing.push(node); 108 } 109 } 110 111 return { alreadyShared, needsSharing }; 112 } 113 114 /** 115 * Read GitHub URL from .udd file using UDDService 116 */ 117 private async getGitHubUrlFromUdd(node: DreamNode): Promise<string | null> { 118 try { 119 const vaultService = serviceManager.getVaultService(); 120 const fullRepoPath = vaultService?.getFullPath(node.repoPath) || node.repoPath; 121 122 const udd = await UDDService.readUDD(fullRepoPath); 123 return (udd as any).githubRepoUrl || null; 124 } catch { 125 // Could not read .udd - this is expected for new nodes 126 return null; 127 } 128 } 129 130 /** 131 * Batch share nodes to GitHub, serializing to prevent race conditions 132 * Returns map of successful UUID → GitHub URL 133 */ 134 private async batchShareNodes(nodes: DreamNode[]): Promise<Map<string, string>> { 135 const result = new Map<string, string>(); 136 137 // Check if GitHub is available 138 const availabilityCheck = await githubService.isAvailable(); 139 if (!availabilityCheck.available) { 140 throw new Error(availabilityCheck.error || 'GitHub CLI not available'); 141 } 142 143 // Get VaultService for path resolution 144 const vaultService = serviceManager.getVaultService(); 145 146 // CRITICAL: Serialize sharing to prevent race conditions 147 // Process nodes one at a time to avoid git/GitHub conflicts 148 for (const node of nodes) { 149 try { 150 const fullRepoPath = vaultService?.getFullPath(node.repoPath) || node.repoPath; 151 152 // Share to GitHub (creates repo, pushes, builds Pages) 153 const shareResult = await githubService.shareDreamNode( 154 fullRepoPath, 155 node.id 156 ); 157 158 if (shareResult.repoUrl) { 159 result.set(node.id, shareResult.repoUrl); 160 } else { 161 console.warn(`[GitHubBatchShare] ${node.name} shared but no GitHub URL returned`); 162 } 163 164 } catch (error) { 165 // Check if error is "already shared" or similar - this is NOT an error! 166 const errorMsg = error instanceof Error ? error.message : String(error); 167 168 if (errorMsg.includes('already exists') || errorMsg.includes('already shared')) { 169 try { 170 const githubUrl = await this.getGitHubUrlFromUdd(node); 171 172 if (githubUrl) { 173 result.set(node.id, githubUrl); 174 continue; // Success! Move to next node 175 } 176 } catch (getUrlError) { 177 console.error(`[GitHubBatchShare] Could not retrieve GitHub URL for ${node.name}:`, getUrlError); 178 } 179 } else { 180 // Different error - log and continue 181 console.error(`[GitHubBatchShare] Failed to share ${node.name}:`, error); 182 } 183 } 184 } 185 186 return result; 187 } 188 } 189 190 // Singleton instance 191 let _githubBatchShareService: GitHubBatchShareService | null = null; 192 193 export function initializeGitHubBatchShareService(plugin: Plugin, dreamNodeService: GitDreamNodeService): void { 194 _githubBatchShareService = new GitHubBatchShareService(plugin, dreamNodeService); 195 } 196 197 export function getGitHubBatchShareService(): GitHubBatchShareService { 198 if (!_githubBatchShareService) { 199 throw new Error('GitHubBatchShareService not initialized. Call initializeGitHubBatchShareService() first.'); 200 } 201 return _githubBatchShareService; 202 }