From 71dee3de54de1b22a81bf36376a49e17338c1ae7 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sun, 22 Feb 2026 14:22:03 +0100 Subject: [PATCH] fix: detect frontmatter-only changes in sync to prevent stale note records Extend the PDS comparison in sync to cover title, description, tags, and note-specific fields (theme, fontSize, fontFamily) in addition to body text. Previously, changing only frontmatter and running sync would store the current file hash, causing publish to skip the update. The comparison logic is extracted into a matchesPDS helper for clarity. --- packages/cli/src/commands/sync.ts | 79 +++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/sync.ts b/packages/cli/src/commands/sync.ts index b433349..4459fbc 100644 --- a/packages/cli/src/commands/sync.ts +++ b/packages/cli/src/commands/sync.ts @@ -9,7 +9,10 @@ import { getCredentials, } from "../lib/credentials"; import { getOAuthHandle, getOAuthSession } from "../lib/oauth-store"; +import type { Agent } from "@atproto/api"; import { createAgent, listDocuments } from "../lib/atproto"; +import type { ListDocumentsResult } from "../lib/atproto"; +import type { BlogPost } from "../lib/types"; import { scanContentDirectory, getContentHash, @@ -19,6 +22,67 @@ import { } from "../lib/markdown"; import { exitOnCancel } from "../lib/prompts"; +async function matchesPDS( + localPost: BlogPost, + doc: ListDocumentsResult, + agent: Agent, + textContentField?: string, +): Promise { + // Compare body text content + const localTextContent = getTextContent(localPost, textContentField); + if (localTextContent.slice(0, 10000) !== doc.value.textContent) { + return false; + } + + // Compare document fields: title, description, tags + const trimmedContent = localPost.content.trim(); + const titleMatch = trimmedContent.match(/^# (.+)$/m); + const localTitle = titleMatch ? titleMatch[1] : localPost.frontmatter.title; + if (localTitle !== doc.value.title) return false; + + const localDescription = localPost.frontmatter.description || undefined; + if (localDescription !== doc.value.description) return false; + + const localTags = + localPost.frontmatter.tags && localPost.frontmatter.tags.length > 0 + ? localPost.frontmatter.tags + : undefined; + if (JSON.stringify(localTags) !== JSON.stringify(doc.value.tags)) { + return false; + } + + // Compare note-specific fields: theme, fontSize, fontFamily. + // Fetch the space.remanso.note record to check these fields. + const noteUriMatch = doc.uri.match(/^at:\/\/([^/]+)\/[^/]+\/(.+)$/); + if (noteUriMatch) { + const repo = noteUriMatch[1]!; + const rkey = noteUriMatch[2]!; + try { + const noteResponse = await agent.com.atproto.repo.getRecord({ + repo, + collection: "space.remanso.note", + rkey, + }); + const noteValue = noteResponse.data.value as Record; + if ( + (localPost.frontmatter.theme || undefined) !== + (noteValue.theme as string | undefined) || + (localPost.frontmatter.fontSize || undefined) !== + (noteValue.fontSize as number | undefined) || + (localPost.frontmatter.fontFamily || undefined) !== + (noteValue.fontFamily as string | undefined) + ) { + return false; + } + } catch { + // Note record doesn't exist — treat as matching to avoid + // forcing a re-publish of posts never published as notes. + } + } + + return true; +} + export const syncCommand = command({ name: "sync", description: "Sync state from ATProto to restore .sequoia-state.json", @@ -182,19 +246,14 @@ export const syncCommand = command({ log.message(` URI: ${doc.uri}`); log.message(` File: ${path.basename(localPost.filePath)}`); - // Compare local text content with PDS text content to detect changes. - // We must avoid storing the local rawContent hash blindly, because - // that would make publish think nothing changed even when content - // was modified since the last publish. - const localTextContent = getTextContent( + // If local content matches PDS, store the local hash (up to date). + // If it differs, store empty hash so publish detects the change. + const contentMatchesPDS = await matchesPDS( localPost, + doc, + agent, config.textContentField, ); - const contentMatchesPDS = - localTextContent.slice(0, 10000) === doc.value.textContent; - - // If local content matches PDS, store the local hash (up to date). - // If it differs, store empty hash so publish detects the change. const contentHash = contentMatchesPDS ? await getContentHash(localPost.rawContent) : ""; -- 2.51.2