diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c6527e..1371747 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to the "promptitude" extension will be documented in this fi ## [Unreleased] +### Improved + +- Sync failure messages now distinguish **invalid repository URLs** (e.g. sub-paths like `/tree/main/...`) from **incompatible repository structures** (repo is reachable but has no supported prompt folders). The summary toast shows a categorised count, and "Show Details" lists the specific reason per repository. + ## [1.5.4] - 2026-01-12 ### Fixed diff --git a/src/syncManager.ts b/src/syncManager.ts index 0231894..d1dc1f8 100644 --- a/src/syncManager.ts +++ b/src/syncManager.ts @@ -325,6 +325,27 @@ export class SyncManager { } } + /** + * Validates that a repository URL is a well-formed repo root (not a sub-path like /tree/main/...). + * Returns an error string if invalid, or undefined if valid. + */ + private validateRepositoryUrl(repoUrl: string): string | undefined { + try { + const parsed = new URL(repoUrl); + const normalizedPath = parsed.pathname.replace(/\.git$/, '').replace(/\/+$/, ''); + const segments = normalizedPath.split('/').filter(Boolean); + + // GitHub: expect exactly owner/repo (2 segments) + if (parsed.hostname === 'github.com' && segments.length !== 2) { + return `Invalid URL – expected https://github.com/owner/repo but got a sub-path (${parsed.pathname}). Remove extra path segments like /tree/…`; + } + + return undefined; + } catch { + return `Invalid URL format – could not parse "${repoUrl}" as a repository URL`; + } + } + private async syncMultipleRepositories(repositories: string[]): Promise { const results: RepositorySyncResult[] = []; let totalItemsUpdated = 0; @@ -338,6 +359,20 @@ export class SyncManager { try { this.logger.debug(`Syncing repository: ${repoUrl}`); + // Early validation: reject malformed repository URLs + const urlError = this.validateRepositoryUrl(repoUrl); + if (urlError) { + this.logger.warn(`Skipping repository ${repoUrl}: ${urlError}`); + results.push({ + repository: repoUrl, + success: false, + itemsUpdated: 0, + error: urlError + }); + errors.push(`${repoUrl}: ${urlError}`); + continue; + } + // Get or create Git API manager for this repository let gitApi = this.gitProviders.get(repoUrl); if (!gitApi) { @@ -379,14 +414,20 @@ export class SyncManager { if (relevantFiles.length === 0) { this.logger.warn(`No relevant files found to sync in ${repoUrl} based on current settings`); - const promptLocation = `${REPO_SYNC_CHAT_MODE_PATH}, ${REPO_SYNC_CHAT_MODE_LEGACY_PATH}, ${REPO_SYNC_CHAT_MODE_LEGACY_SINGULAR_PATH}, ${REPO_SYNC_INSTRUCTIONS_PATH}, ${REPO_SYNC_PROMPT_PATH}`; + const enabledTypes: string[] = []; + if (this.config.syncChatmode) { enabledTypes.push(REPO_SYNC_CHAT_MODE_PATH); } + if (this.config.syncInstructions) { enabledTypes.push(REPO_SYNC_INSTRUCTIONS_PATH); } + if (this.config.syncPrompt) { enabledTypes.push(REPO_SYNC_PROMPT_PATH); } + const structureError = enabledTypes.length > 0 + ? `Incompatible repository – no .md/.txt files found under ${enabledTypes.join(', ')} on branch "${branch}". Check that the repo uses a supported folder layout.` + : `No sync types enabled – enable at least one of syncChatmode, syncInstructions, or syncPrompt in settings.`; results.push({ repository: repoUrl, success: false, itemsUpdated: 0, - error: `No relevant files found, make sure prompts are in valid directories: ${promptLocation}` + error: structureError }); - errors.push(`${repoUrl}: No relevant files found`); + errors.push(`${repoUrl}: ${structureError}`); continue; } this.logger.debug(`Found ${relevantFiles.length} relevant files to sync for ${repoUrl}`); diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts index 2340c07..5af899d 100644 --- a/src/utils/notifications.ts +++ b/src/utils/notifications.ts @@ -47,7 +47,27 @@ export class NotificationManager { } async showPartialSyncSuccess(itemsCount: number, successCount: number, totalCount: number, errors: string[]): Promise { - const message = `⚠️ Partial sync completed! ${itemsCount} items updated from ${successCount}/${totalCount} repositories.`; + // Categorise failures so the summary toast is immediately actionable + let badUrlCount = 0; + let badStructureCount = 0; + let otherCount = 0; + for (const err of errors) { + if (err.includes('Invalid URL')) { + badUrlCount++; + } else if (err.includes('Incompatible repository') || err.includes('No sync types enabled')) { + badStructureCount++; + } else { + otherCount++; + } + } + + const failParts: string[] = []; + if (badUrlCount > 0) { failParts.push(`${badUrlCount} bad URL${badUrlCount > 1 ? 's' : ''}`); } + if (badStructureCount > 0) { failParts.push(`${badStructureCount} incompatible structure`); } + if (otherCount > 0) { failParts.push(`${otherCount} other error${otherCount > 1 ? 's' : ''}`); } + + const failSummary = failParts.length > 0 ? ` Failures: ${failParts.join(', ')}.` : ''; + const message = `⚠️ Partial sync: ${itemsCount} items updated from ${successCount}/${totalCount} repos.${failSummary}`; const result = await this.showWarning( message, 'Show Details',