From df72119259e18cfefc55d9a97cba00a0133eaf21 Mon Sep 17 00:00:00 2001 From: Fares Osman <43153226+fiosman@users.noreply.github.com> Date: Thu, 21 May 2026 01:14:26 -0400 Subject: [PATCH] feat: adds logic to sync git FS/DB for project ruleset --- .../node-src/services/project-lint-ruleset.ts | 6 +- .../src/sync/git/repo-file-watcher.ts | 80 +++++++++++++++++-- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/packages/insomnia/src/insomnia-data/node-src/services/project-lint-ruleset.ts b/packages/insomnia/src/insomnia-data/node-src/services/project-lint-ruleset.ts index d3294c7372..7e187c0890 100644 --- a/packages/insomnia/src/insomnia-data/node-src/services/project-lint-ruleset.ts +++ b/packages/insomnia/src/insomnia-data/node-src/services/project-lint-ruleset.ts @@ -8,15 +8,15 @@ export function getByParentId(projectId: string) { } export async function upsert(projectId: string, patch: Partial = {}) { - const spec = await db.findOne(type, { + const existing = await db.findOne(type, { parentId: projectId, }); - if (!spec) { + if (!existing) { return db.docCreate(type, { ...patch, parentId: projectId }); } - return spec; + return db.docUpdate(existing, patch); } export function remove(projectId: string) { diff --git a/packages/insomnia/src/sync/git/repo-file-watcher.ts b/packages/insomnia/src/sync/git/repo-file-watcher.ts index 14a098ef06..9ee28df616 100644 --- a/packages/insomnia/src/sync/git/repo-file-watcher.ts +++ b/packages/insomnia/src/sync/git/repo-file-watcher.ts @@ -51,6 +51,7 @@ import { database as db } from '../../common/database'; import { InsomniaFileTypeValues } from '../../common/import-v5-parser'; import { getInsomniaV5DataExport, tryImportV5Data } from '../../common/insomnia-v5'; import { SyncQueue } from './sync-queue'; +import YAML from 'yaml'; const POLL_INTERVAL_MS = 10_000; const DEBOUNCE_MS = 300; @@ -223,6 +224,7 @@ class RepoFileWatcher { } this.queue.enqueue(() => this.flushProjectWorkspacesToDisk()); + this.queue.enqueue(() => this.flushProjectLintRulesetToDisk()); await this.queue.waitUntilDone(); } @@ -359,6 +361,7 @@ class RepoFileWatcher { this.flushDebounce = setTimeout(() => { this.flushDebounce = null; this.queue.enqueue(() => this.flushProjectWorkspacesToDisk()); + this.queue.enqueue(() => this.flushProjectLintRulesetToDisk()); }, DEBOUNCE_MS); }); } @@ -431,6 +434,39 @@ class RepoFileWatcher { } } + private async flushProjectLintRulesetToDisk(): Promise { + if (this.stopped) { + return; + } + + const absPath = path.normalize(path.join(this.repoDir, '.spectral.yaml')); + const ruleset = await services.projectLintRuleset.getByParentId(this.projectId); + + try { + if (!ruleset) { + // Ruleset removed from the DB — remove the file if we were tracking it. + if (this.lastWrittenHash.has(absPath) || this.lastSyncMtime.has(absPath)) { + await fs.promises.rm(absPath, { force: true }); + this.lastWrittenHash.delete(absPath); + this.lastSyncMtime.delete(absPath); + } + return; + } + + const hash = contentHash(ruleset.rulesetContent); + if (this.lastWrittenHash.get(absPath) === hash) { + return; + } + + await fs.promises.writeFile(absPath, ruleset.rulesetContent, 'utf8'); + this.lastWrittenHash.set(absPath, hash); + const stat = await fs.promises.stat(absPath); + this.lastSyncMtime.set(absPath, stat.mtimeMs); + } catch (err) { + console.warn('[repo-file-watcher] Could not flush project lint ruleset to disk:', err); + } + } + // --------------------------------------------------------------------------- // FS → DB direction (inbound) // --------------------------------------------------------------------------- @@ -505,6 +541,27 @@ class RepoFileWatcher { this.debounceTimers.set(absPath, timer); } + private isSpectralRulesetPath(normalisedPath: string): boolean { + return ( + path.basename(normalisedPath) === '.spectral.yaml' && + path.normalize(path.dirname(normalisedPath)) === path.normalize(this.repoDir) + ); + } + + private isSpectralRulesetFile(normalisedPath: string, content: string): boolean { + if (!this.isSpectralRulesetPath(normalisedPath)) { + return false; + } + try { + const parsedContent = YAML.parse(content); + return ( + !!parsedContent && typeof parsedContent === 'object' && ('extends' in parsedContent || 'rules' in parsedContent) + ); + } catch { + return false; + } + } + /** * Read a YAML file from disk and import its documents into the DB. * @@ -528,6 +585,12 @@ class RepoFileWatcher { this.lastWrittenHash.set(normalised, result.hash); this.lastSyncMtime.set(normalised, result.mtimeMs); + if (this.isSpectralRulesetFile(normalised, result.content)) { + await services.projectLintRuleset.upsert(this.projectId, { rulesetContent: result.content }); + this.notifyRenderer(); + return; + } + const docs = this.parseAndValidate(absPath, normalised, result.content); if (!docs) { return; @@ -685,6 +748,16 @@ class RepoFileWatcher { return; } + // The lint ruleset file was deleted — remove the ProjectLintRuleset record. + if (this.isSpectralRulesetPath(normalised)) { + await services.projectLintRuleset.remove(this.projectId); + this.lastSyncMtime.delete(normalised); + this.lastWrittenHash.delete(normalised); + this.clearProblem(normalised); + this.notifyRenderer(); + return; + } + const relPath = this.toPosixRelPath(normalised); // Find the workspace whose gitFilePath matches this deleted file @@ -726,12 +799,7 @@ class RepoFileWatcher { return; } - console.warn( - '[repo-file-watcher] Failed to remove workspace file from disk:', - workspaceId, - normalisedPath, - err, - ); + console.warn('[repo-file-watcher] Failed to remove workspace file from disk:', workspaceId, normalisedPath, err); } }