Skip to content

Commit 20dbcae

Browse files
Merge pull request #480 from community-scripts/fix/405
fix: delete local JSON files when removed from remote repo (fixes #405)
2 parents 201b33e + 8e8c724 commit 20dbcae

File tree

2 files changed

+132
-22
lines changed

2 files changed

+132
-22
lines changed

src/server/services/githubJsonService.js

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// JavaScript wrapper for githubJsonService (for use with node server.js)
2-
import { writeFile, mkdir, readdir, readFile } from 'fs/promises';
2+
import { writeFile, mkdir, readdir, readFile, unlink } from 'fs/promises';
33
import { join } from 'path';
44
import { repositoryService } from './repositoryService.js';
55
import { listDirectory, downloadRawFile } from '../lib/gitProvider/index.js';
@@ -163,33 +163,51 @@ class GitHubJsonService {
163163
const localFiles = await this.getLocalJsonFiles();
164164
console.log(`Found ${localFiles.length} local JSON files`);
165165

166+
// Delete local JSON files that belong to this repo but are no longer in the remote
167+
const remoteFilenames = new Set(githubFiles.map((f) => f.name));
168+
const deletedFiles = await this.deleteLocalFilesRemovedFromRepo(repoUrl, remoteFilenames);
169+
if (deletedFiles.length > 0) {
170+
console.log(`Removed ${deletedFiles.length} obsolete JSON file(s) no longer in ${repoUrl}`);
171+
}
172+
166173
const filesToSync = await this.findFilesToSyncForRepo(repoUrl, githubFiles, localFiles);
167174
console.log(`Found ${filesToSync.length} files that need syncing from ${repoUrl}`);
168175

169176
if (filesToSync.length === 0) {
177+
const msg =
178+
deletedFiles.length > 0
179+
? `All JSON files are up to date for repository: ${repoUrl}. Removed ${deletedFiles.length} obsolete file(s).`
180+
: `All JSON files are up to date for repository: ${repoUrl}`;
170181
return {
171182
success: true,
172-
message: `All JSON files are up to date for repository: ${repoUrl}`,
183+
message: msg,
173184
count: 0,
174-
syncedFiles: []
185+
syncedFiles: [],
186+
deletedFiles
175187
};
176188
}
177189

178190
const syncedFiles = await this.syncSpecificFiles(repoUrl, filesToSync);
179191

192+
const msg =
193+
deletedFiles.length > 0
194+
? `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}, removed ${deletedFiles.length} obsolete file(s).`
195+
: `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}`;
180196
return {
181197
success: true,
182-
message: `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}`,
198+
message: msg,
183199
count: syncedFiles.length,
184-
syncedFiles
200+
syncedFiles,
201+
deletedFiles
185202
};
186203
} catch (error) {
187204
console.error(`JSON sync failed for ${repoUrl}:`, error);
188205
return {
189206
success: false,
190207
message: `Failed to sync JSON files from ${repoUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`,
191208
count: 0,
192-
syncedFiles: []
209+
syncedFiles: [],
210+
deletedFiles: []
193211
};
194212
}
195213
}
@@ -205,13 +223,15 @@ class GitHubJsonService {
205223
success: false,
206224
message: 'No enabled repositories found',
207225
count: 0,
208-
syncedFiles: []
226+
syncedFiles: [],
227+
deletedFiles: []
209228
};
210229
}
211230

212231
console.log(`Found ${enabledRepos.length} enabled repositories`);
213232

214233
const allSyncedFiles = [];
234+
const allDeletedFiles = [];
215235
const processedSlugs = new Set();
216236
let totalSynced = 0;
217237

@@ -222,6 +242,7 @@ class GitHubJsonService {
222242
const result = await this.syncJsonFilesForRepo(repo.url);
223243

224244
if (result.success) {
245+
allDeletedFiles.push(...(result.deletedFiles ?? []));
225246
const newFiles = result.syncedFiles.filter(file => {
226247
const slug = file.replace('.json', '');
227248
if (processedSlugs.has(slug)) {
@@ -243,19 +264,25 @@ class GitHubJsonService {
243264

244265
await this.updateExistingFilesWithRepositoryUrl();
245266

267+
const msg =
268+
allDeletedFiles.length > 0
269+
? `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories, removed ${allDeletedFiles.length} obsolete file(s).`
270+
: `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories`;
246271
return {
247272
success: true,
248-
message: `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories`,
273+
message: msg,
249274
count: totalSynced,
250-
syncedFiles: allSyncedFiles
275+
syncedFiles: allSyncedFiles,
276+
deletedFiles: allDeletedFiles
251277
};
252278
} catch (error) {
253279
console.error('Multi-repository JSON sync failed:', error);
254280
return {
255281
success: false,
256282
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
257283
count: 0,
258-
syncedFiles: []
284+
syncedFiles: [],
285+
deletedFiles: []
259286
};
260287
}
261288
}
@@ -297,6 +324,32 @@ class GitHubJsonService {
297324
}
298325
}
299326

327+
async deleteLocalFilesRemovedFromRepo(repoUrl, remoteFilenames) {
328+
this.initializeConfig();
329+
const localFiles = await this.getLocalJsonFiles();
330+
const deletedFiles = [];
331+
332+
for (const file of localFiles) {
333+
try {
334+
const filePath = join(this.localJsonDirectory, file);
335+
const content = await readFile(filePath, 'utf-8');
336+
const script = JSON.parse(content);
337+
338+
if (script.repository_url === repoUrl && !remoteFilenames.has(file)) {
339+
await unlink(filePath);
340+
const slug = file.replace(/\.json$/, '');
341+
this.scriptCache.delete(slug);
342+
deletedFiles.push(file);
343+
console.log(`Removed obsolete script JSON: ${file} (no longer in ${repoUrl})`);
344+
}
345+
} catch {
346+
// If we can't read or parse the file, skip (do not delete)
347+
}
348+
}
349+
350+
return deletedFiles;
351+
}
352+
300353
async findFilesToSyncForRepo(repoUrl, githubFiles, localFiles) {
301354
const filesToSync = [];
302355

src/server/services/githubJsonService.ts

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { writeFile, mkdir, readdir, readFile } from 'fs/promises';
1+
import { writeFile, mkdir, readdir, readFile, unlink } from 'fs/promises';
22
import { join } from 'path';
33
import { env } from '../../env.js';
44
import type { Script, ScriptCard, GitHubFile } from '../../types/script';
@@ -158,7 +158,7 @@ export class GitHubJsonService {
158158
/**
159159
* Sync JSON files from a specific repository
160160
*/
161-
async syncJsonFilesForRepo(repoUrl: string): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[] }> {
161+
async syncJsonFilesForRepo(repoUrl: string): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[]; deletedFiles: string[] }> {
162162
try {
163163
console.log(`Starting JSON sync from repository: ${repoUrl}`);
164164

@@ -170,44 +170,62 @@ export class GitHubJsonService {
170170
const localFiles = await this.getLocalJsonFiles();
171171
console.log(`Found ${localFiles.length} local JSON files`);
172172

173+
// Delete local JSON files that belong to this repo but are no longer in the remote
174+
const remoteFilenames = new Set(githubFiles.map((f) => f.name));
175+
const deletedFiles = await this.deleteLocalFilesRemovedFromRepo(repoUrl, remoteFilenames);
176+
if (deletedFiles.length > 0) {
177+
console.log(`Removed ${deletedFiles.length} obsolete JSON file(s) no longer in ${repoUrl}`);
178+
}
179+
173180
// Compare and find files that need syncing
174181
// For multi-repo support, we need to check if file exists AND if it's from this repo
175182
const filesToSync = await this.findFilesToSyncForRepo(repoUrl, githubFiles, localFiles);
176183
console.log(`Found ${filesToSync.length} files that need syncing from ${repoUrl}`);
177184

178185
if (filesToSync.length === 0) {
186+
const msg =
187+
deletedFiles.length > 0
188+
? `All JSON files are up to date for repository: ${repoUrl}. Removed ${deletedFiles.length} obsolete file(s).`
189+
: `All JSON files are up to date for repository: ${repoUrl}`;
179190
return {
180191
success: true,
181-
message: `All JSON files are up to date for repository: ${repoUrl}`,
192+
message: msg,
182193
count: 0,
183-
syncedFiles: []
194+
syncedFiles: [],
195+
deletedFiles
184196
};
185197
}
186198

187199
// Download and save only the files that need syncing
188200
const syncedFiles = await this.syncSpecificFiles(repoUrl, filesToSync);
189201

202+
const msg =
203+
deletedFiles.length > 0
204+
? `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}, removed ${deletedFiles.length} obsolete file(s).`
205+
: `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}`;
190206
return {
191207
success: true,
192-
message: `Successfully synced ${syncedFiles.length} JSON files from ${repoUrl}`,
208+
message: msg,
193209
count: syncedFiles.length,
194-
syncedFiles
210+
syncedFiles,
211+
deletedFiles
195212
};
196213
} catch (error) {
197214
console.error(`JSON sync failed for ${repoUrl}:`, error);
198215
return {
199216
success: false,
200217
message: `Failed to sync JSON files from ${repoUrl}: ${error instanceof Error ? error.message : 'Unknown error'}`,
201218
count: 0,
202-
syncedFiles: []
219+
syncedFiles: [],
220+
deletedFiles: []
203221
};
204222
}
205223
}
206224

207225
/**
208226
* Sync JSON files from all enabled repositories (main repo has priority)
209227
*/
210-
async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[] }> {
228+
async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[]; deletedFiles: string[] }> {
211229
try {
212230
console.log('Starting multi-repository JSON sync...');
213231

@@ -218,13 +236,15 @@ export class GitHubJsonService {
218236
success: false,
219237
message: 'No enabled repositories found',
220238
count: 0,
221-
syncedFiles: []
239+
syncedFiles: [],
240+
deletedFiles: []
222241
};
223242
}
224243

225244
console.log(`Found ${enabledRepos.length} enabled repositories`);
226245

227246
const allSyncedFiles: string[] = [];
247+
const allDeletedFiles: string[] = [];
228248
const processedSlugs = new Set<string>(); // Track slugs we've already processed
229249
let totalSynced = 0;
230250

@@ -236,6 +256,7 @@ export class GitHubJsonService {
236256
const result = await this.syncJsonFilesForRepo(repo.url);
237257

238258
if (result.success) {
259+
allDeletedFiles.push(...(result.deletedFiles ?? []));
239260
// Only count files that weren't already processed from a higher priority repo
240261
const newFiles = result.syncedFiles.filter(file => {
241262
const slug = file.replace('.json', '');
@@ -259,19 +280,25 @@ export class GitHubJsonService {
259280
// Also update existing files that don't have repository_url set (backward compatibility)
260281
await this.updateExistingFilesWithRepositoryUrl();
261282

283+
const msg =
284+
allDeletedFiles.length > 0
285+
? `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories, removed ${allDeletedFiles.length} obsolete file(s).`
286+
: `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories`;
262287
return {
263288
success: true,
264-
message: `Successfully synced ${totalSynced} JSON files from ${enabledRepos.length} repositories`,
289+
message: msg,
265290
count: totalSynced,
266-
syncedFiles: allSyncedFiles
291+
syncedFiles: allSyncedFiles,
292+
deletedFiles: allDeletedFiles
267293
};
268294
} catch (error) {
269295
console.error('Multi-repository JSON sync failed:', error);
270296
return {
271297
success: false,
272298
message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`,
273299
count: 0,
274-
syncedFiles: []
300+
syncedFiles: [],
301+
deletedFiles: []
275302
};
276303
}
277304
}
@@ -316,6 +343,36 @@ export class GitHubJsonService {
316343
}
317344
}
318345

346+
/**
347+
* Delete local JSON files that belong to this repo but are no longer in the remote list.
348+
* Returns the list of deleted filenames.
349+
*/
350+
private async deleteLocalFilesRemovedFromRepo(repoUrl: string, remoteFilenames: Set<string>): Promise<string[]> {
351+
this.initializeConfig();
352+
const localFiles = await this.getLocalJsonFiles();
353+
const deletedFiles: string[] = [];
354+
355+
for (const file of localFiles) {
356+
try {
357+
const filePath = join(this.localJsonDirectory!, file);
358+
const content = await readFile(filePath, 'utf-8');
359+
const script = JSON.parse(content) as Script;
360+
361+
if (script.repository_url === repoUrl && !remoteFilenames.has(file)) {
362+
await unlink(filePath);
363+
const slug = file.replace(/\.json$/, '');
364+
this.scriptCache.delete(slug);
365+
deletedFiles.push(file);
366+
console.log(`Removed obsolete script JSON: ${file} (no longer in ${repoUrl})`);
367+
}
368+
} catch {
369+
// If we can't read or parse the file, skip (do not delete)
370+
}
371+
}
372+
373+
return deletedFiles;
374+
}
375+
319376
/**
320377
* Find files that need syncing for a specific repository
321378
* This checks if file exists locally AND if it's from the same repository

0 commit comments

Comments
 (0)