Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ name: CI/CD Pipeline
on:
push:
branches:
- production-release
pull_request:
branches:
- production-release
- main

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Prompt Vault extension",
"description": "Sync GitHub Copilot prompts, chatmodes and instructions from git repositories",
"version": "1.2.0",
"publisher": "logient-nventive",
"publisher": "logientnventive",
"repository": {
"type": "git",
"url": "https://github.com/nventive/PromptVault.git",
Expand Down
51 changes: 22 additions & 29 deletions src/syncManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,40 +171,46 @@ export class SyncManager {
await this.fileSystem.ensureDirectoryExists(promptsDir);

let itemsUpdated = 0;
let statusBarRepoCount = 0;

for (const file of files) {
this.logger.debug(`Syncing file: ${file.path}`);
this.statusBar.setStatus(SyncStatus.Syncing, `Syncing ${statusBarRepoCount}/${files.length}`);
statusBarRepoCount++;

let content = null;
const fileName = this.fileSystem.getBasename(file.path);
const localPath = this.fileSystem.joinPath(promptsDir, fileName);

try {
const fileExists = await this.fileSystem.fileExists(localPath);
if(fileExists) {
const localFileContent = await this.fileSystem.readFileContent(localPath);
const localFileHash = await this.fileSystem.calculateFileHash(localFileContent);

const githubFileHash = await this.github.getFileHash(owner, repo, file.path, this.config.branch);
if(localFileHash === githubFileHash) {
this.logger.info(`File ${file.path} is up to date, skipping`);
continue;
}
}

content = await this.github.getFileContent(owner, repo, file.path, this.config.branch);
} catch (error) {
// An error occured will retrieving file content, Return here
this.logger.warn(`Failed to fetch content for ${file.path}: ${error}`);
this.notifications.showSyncError(`Failed to fetch content for ${file.path}: ${error}. Make sure that the correct is set. Current branch: ${this.config.branch}`);
this.notifications.showSyncError(`Failed to fetch content for ${file.path}: ${error}. Make sure that the branch is correct. Current branch: ${this.config.branch}`);
return itemsUpdated;
}

try {
// Flatten the structure - extract just the filename and place directly in prompts directory
const fileName = this.fileSystem.getBasename(file.path);
const localPath = this.fileSystem.joinPath(promptsDir, fileName);

// Check if file needs updating
if(!content) {
this.logger.warn(`No content retrieved for ${file.path}, skipping`);
continue;
}
const needsUpdate = await this.shouldUpdateFile(localPath, content);

if (needsUpdate) {
await this.fileSystem.writeFileContent(localPath, content);
itemsUpdated++;
this.logger.debug(`Updated file: ${localPath}`);
} else {
this.logger.debug(`File unchanged: ${localPath}`);
}


await this.fileSystem.writeFileContent(localPath, content);
itemsUpdated++;
} catch (error) {
this.logger.warn(`Failed to sync file ${file.path}: ${error}`);
// Continue with other files even if one fails
Expand Down Expand Up @@ -285,19 +291,6 @@ export class SyncManager {
};
}

private async shouldUpdateFile(localPath: string, newContent: string): Promise<boolean> {
try {
if (!(await this.fileSystem.fileExists(localPath))) {
return true; // File doesn't exist, needs to be created
}

const existingContent = await this.fileSystem.readFileContent(localPath);
return existingContent !== newContent;
} catch {
return true; // Error reading file, assume it needs updating
}
}

private scheduleNextSync(): void {
// Clear existing timer
if (this.timer) {
Expand Down
15 changes: 15 additions & 0 deletions src/utils/fileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ export class FileSystemManager {
async copyFile(source: string, destination: string): Promise<void> {
const content = await this.readFileContent(source);
await this.writeFileContent(destination, content);
}

async calculateFileHash(content: string): Promise<string> {
// GitHub uses the following format: `blob ${content.length}\0${content}`
const blobPrefix = `blob ${Buffer.from(content).length}\0`;
const fullContent = blobPrefix + content;

const encoder = new TextEncoder();
const data = encoder.encode(fullContent);
const hashBuffer = await crypto.subtle.digest('SHA-1', data);

const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');

return hashHex;
}

getRelativePath(from: string, to: string): string {
Expand Down
22 changes: 22 additions & 0 deletions src/utils/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,28 @@ export class GitHubApiManager {
throw new Error('No content found in file');
}

async getFileHash(owner: string, repo: string, path: string, branch: string = 'master'): Promise<string> {
const session = await vscode.authentication.getSession('github', ['repo'], { createIfNone: false });
if (!session) {
throw new Error('GitHub authentication required');
}

const response = await fetch(`${this.baseUrl}/repos/${owner}/${repo}/contents/${path}?ref=${branch}`, {
headers: {
'Authorization': `Bearer ${session.accessToken}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'VS Code Prompts Sync Extension'
}
});

if (!response.ok) {
throw new Error(`Failed to get file hash: ${response.status} ${response.statusText}`);
}

const data = await response.json();
return data.sha;
}

parseRepositoryUrl(url: string): { owner: string; repo: string } {
// Handle different GitHub URL formats
const patterns = [
Expand Down