github-batch-share-service.ts
1 import { Notice, Plugin } from 'obsidian'; 2 import { DreamNode } from '../types/dreamnode'; 3 import { githubService } from '../features/github-sharing/GitHubService'; 4 import { GitDreamNodeService } from './git-dreamnode-service'; 5 6 /** 7 * GitHub Batch Share Service 8 * 9 * Ensures multiple DreamNodes have GitHub URLs before sharing via email links. 10 * Handles batch GitHub share operations for Windows/GitHub fallback mode. 11 */ 12 export class GitHubBatchShareService { 13 private plugin: Plugin; 14 private dreamNodeService: GitDreamNodeService; 15 16 constructor(plugin: Plugin, dreamNodeService: GitDreamNodeService) { 17 this.plugin = plugin; 18 this.dreamNodeService = dreamNodeService; 19 } 20 21 /** 22 * Ensure all nodes have GitHub URLs, sharing those that don't 23 * Returns map of UUID â GitHub URL 24 */ 25 async ensureNodesHaveGitHubUrls(nodeUUIDs: string[]): Promise<Map<string, string>> { 26 console.log(`đŽ [GitHubBatchShare] Processing ${nodeUUIDs.length} nodes for GitHub URLs`); 27 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 console.log(`â [GitHubBatchShare] ${alreadyShared.length} nodes already have GitHub URLs`); 51 console.log(`đ [GitHubBatchShare] ${needsSharing.length} nodes need sharing`); 52 53 // Step 3: Add already-shared nodes to result 54 for (const node of alreadyShared) { 55 const githubUrl = await this.getGitHubUrlFromUdd(node); 56 if (githubUrl) { 57 result.set(node.id, githubUrl); 58 } 59 } 60 61 // Step 4: Batch share nodes that need it 62 if (needsSharing.length > 0) { 63 const notice = new Notice(`Sharing ${needsSharing.length} DreamNode${needsSharing.length > 1 ? 's' : ''} to GitHub...`, 0); 64 65 const shared = await this.batchShareNodes(needsSharing); 66 67 notice.hide(); 68 69 // Add newly shared nodes to result 70 for (const [uuid, githubUrl] of shared) { 71 result.set(uuid, githubUrl); 72 } 73 74 const successCount = shared.size; 75 const failCount = needsSharing.length - successCount; 76 77 if (successCount > 0) { 78 new Notice(`â Shared ${successCount} node${successCount > 1 ? 's' : ''} to GitHub`); 79 } 80 81 if (failCount > 0) { 82 console.warn(`â ī¸ [GitHubBatchShare] ${failCount} node(s) failed to share`); 83 } 84 } 85 86 console.log(`â [GitHubBatchShare] Complete: ${result.size}/${nodeUUIDs.length} nodes have GitHub URLs`); 87 return result; 88 89 } catch (error) { 90 console.error('â [GitHubBatchShare] Batch sharing failed:', error); 91 throw error; 92 } 93 } 94 95 /** 96 * Categorize nodes by GitHub sharing status 97 */ 98 private async categorizeNodes(nodes: DreamNode[]): Promise<{ 99 alreadyShared: DreamNode[]; 100 needsSharing: DreamNode[]; 101 }> { 102 const alreadyShared: DreamNode[] = []; 103 const needsSharing: DreamNode[] = []; 104 105 for (const node of nodes) { 106 const githubUrl = await this.getGitHubUrlFromUdd(node); 107 108 if (githubUrl) { 109 alreadyShared.push(node); 110 } else { 111 needsSharing.push(node); 112 } 113 } 114 115 return { alreadyShared, needsSharing }; 116 } 117 118 /** 119 * Read GitHub URL from .udd file 120 */ 121 private async getGitHubUrlFromUdd(node: DreamNode): Promise<string | null> { 122 try { 123 const path = require('path'); 124 const fs = require('fs').promises; 125 const adapter = this.plugin.app.vault.adapter as any; 126 const vaultPath = adapter.basePath || ''; 127 const uddPath = path.join(vaultPath, node.repoPath, '.udd'); 128 129 try { 130 const uddContent = await fs.readFile(uddPath, 'utf-8'); 131 const udd = JSON.parse(uddContent); 132 133 if (udd.githubRepoUrl) { 134 return udd.githubRepoUrl; 135 } 136 } catch (error) { 137 console.warn(`â ī¸ [GitHubBatchShare] Could not read .udd for ${node.name}:`, error); 138 } 139 140 return null; 141 } catch (error) { 142 console.warn(`â ī¸ [GitHubBatchShare] Could not get GitHub URL for ${node.name}:`, error); 143 return null; 144 } 145 } 146 147 /** 148 * Batch share nodes to GitHub, serializing to prevent race conditions 149 * Returns map of successful UUID â GitHub URL 150 */ 151 private async batchShareNodes(nodes: DreamNode[]): Promise<Map<string, string>> { 152 const result = new Map<string, string>(); 153 154 // Check if GitHub is available 155 const availabilityCheck = await githubService.isAvailable(); 156 if (!availabilityCheck.available) { 157 console.warn('â ī¸ [GitHubBatchShare] GitHub CLI not available, skipping sharing'); 158 throw new Error(availabilityCheck.error || 'GitHub CLI not available'); 159 } 160 161 // Get vault path 162 const adapter = this.plugin.app.vault.adapter as any; 163 const vaultPath = adapter.basePath || ''; 164 const path = require('path'); 165 166 // CRITICAL: Serialize sharing to prevent race conditions 167 // Process nodes one at a time to avoid git/GitHub conflicts 168 for (const node of nodes) { 169 try { 170 console.log(`đ [GitHubBatchShare] Sharing ${node.name}...`); 171 172 const fullRepoPath = path.join(vaultPath, node.repoPath); 173 174 // Share to GitHub (creates repo, pushes, builds Pages) 175 const shareResult = await githubService.shareDreamNode( 176 fullRepoPath, 177 node.id 178 ); 179 180 if (shareResult.repoUrl) { 181 result.set(node.id, shareResult.repoUrl); 182 console.log(`â [GitHubBatchShare] ${node.name} shared: ${shareResult.repoUrl}`); 183 184 // Update local node's .udd file to persist GitHub URL 185 // The shareDreamNode method should have already updated .udd, but verify 186 const githubUrl = await this.getGitHubUrlFromUdd(node); 187 if (!githubUrl) { 188 console.warn(`â ī¸ [GitHubBatchShare] ${node.name} shared but .udd not updated with GitHub URL`); 189 } 190 } else { 191 console.warn(`â ī¸ [GitHubBatchShare] ${node.name} shared but no GitHub URL returned`); 192 } 193 194 } catch (error) { 195 // Check if error is "already shared" or similar - this is NOT an error! 196 const errorMsg = error instanceof Error ? error.message : String(error); 197 198 if (errorMsg.includes('already exists') || errorMsg.includes('already shared')) { 199 console.log(`âšī¸ [GitHubBatchShare] ${node.name} already shared, retrieving URL...`); 200 201 try { 202 const githubUrl = await this.getGitHubUrlFromUdd(node); 203 204 if (githubUrl) { 205 result.set(node.id, githubUrl); 206 console.log(`â [GitHubBatchShare] ${node.name} already shared: ${githubUrl}`); 207 continue; // Success! Move to next node 208 } else { 209 console.warn(`â ī¸ [GitHubBatchShare] Could not retrieve GitHub URL for already-shared ${node.name}`); 210 } 211 } catch (getUrlError) { 212 console.error(`â [GitHubBatchShare] Could not retrieve GitHub URL for ${node.name}:`, getUrlError); 213 } 214 } else { 215 // Different error - log and continue 216 console.error(`â [GitHubBatchShare] Failed to share ${node.name}:`, error); 217 } 218 } 219 } 220 221 return result; 222 } 223 } 224 225 // Singleton instance 226 let _githubBatchShareService: GitHubBatchShareService | null = null; 227 228 export function initializeGitHubBatchShareService(plugin: Plugin, dreamNodeService: GitDreamNodeService): void { 229 _githubBatchShareService = new GitHubBatchShareService(plugin, dreamNodeService); 230 console.log(`đŽ [GitHubBatchShare] Service initialized`); 231 } 232 233 export function getGitHubBatchShareService(): GitHubBatchShareService { 234 if (!_githubBatchShareService) { 235 throw new Error('GitHubBatchShareService not initialized. Call initializeGitHubBatchShareService() first.'); 236 } 237 return _githubBatchShareService; 238 }