/ src / services / github-batch-share-service.ts
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  }