Address ObsidianReviewBot feedback for community plugin submission

- settings: replace HTML heading with Setting().setHeading() pattern; sentence-case "GitHub personal access token"
- push: read configDir from Vault instead of hardcoding ".obsidian"
- pull/push/seed/main: drop "Vault Sync:" prefix from Notice strings (sentence case + Obsidian style)
- pull/seed: omit unused catch params

Bumps version to 1.0.3.
This commit is contained in:
Andrew Boldi 2026-04-26 19:30:01 -07:00
parent 29baa452eb
commit 5f5b8d4c74
7 changed files with 55 additions and 58 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
{
"id": "vault-sync-rest",
"name": "Vault Sync (REST)",
"version": "1.0.2",
"version": "1.0.3",
"minAppVersion": "1.4.0",
"description": "Two-way sync with a GitHub repo via REST API. Works on iOS for vaults of any size including images, where git-protocol plugins crash from WebView memory limits.",
"author": "Andrew Boldi",

View file

@ -21,7 +21,7 @@ export default class VaultSyncPlugin extends Plugin {
this.statusBar = this.addStatusBarItem();
this.updateStatusBar("idle");
this.addRibbonIcon("refresh-cw", "Vault Sync: Sync now", () => {
this.addRibbonIcon("refresh-cw", "Sync vault now", () => {
void this.runWithErrorNotice("sync", () => this.runSync(false));
});

View file

@ -18,7 +18,7 @@ async function ensureDir(plugin: VaultSyncPlugin, dirPath: string): Promise<void
if (!dirPath || dirPath === "/" || dirPath === ".") return;
try {
await plugin.app.vault.adapter.mkdir(dirPath);
} catch (_e) {
} catch {
/* exists */
}
}
@ -41,7 +41,7 @@ async function ensureParentDirs(
async function safeRemove(plugin: VaultSyncPlugin, path: string): Promise<void> {
try {
await plugin.app.vault.adapter.remove(path);
} catch (_e) {
} catch {
/* already gone */
}
}
@ -78,35 +78,35 @@ export async function pullFromGitHub(
if (!repoUrl) throw new Error("Set a repo URL in Vault Sync settings.");
if (!lastCommitSha) {
throw new Error(
"No baseline commit recorded. Run 'Vault Sync: Seed from GitHub' first."
"No baseline commit recorded. Run 'Seed from GitHub' first."
);
}
const ref = parseRepoUrl(repoUrl);
const notice = opts.silent
? null
: new Notice("Vault Sync: checking remote…", 0);
: new Notice("Checking remote…", 0);
const headSha = await getBranchSha(ref, branch || "main", pat);
if (headSha === lastCommitSha) {
if (notice) {
notice?.setMessage("Vault Sync: already up to date");
notice?.setMessage("Already up to date");
setTimeout(() => notice?.hide(), 4000);
}
return;
}
notice?.setMessage(`Vault Sync: comparing ${lastCommitSha.slice(0, 7)}${headSha.slice(0, 7)}`);
notice?.setMessage(`Comparing ${lastCommitSha.slice(0, 7)}${headSha.slice(0, 7)}`);
const cmp = await compareCommits(ref, lastCommitSha, headSha, pat);
if (cmp.status === "diverged") {
notice?.setMessage(
"Vault Sync: remote diverged from local baseline. Manual resolution needed (Phase 3 push will detect this)."
"Remote diverged from local baseline. Manual resolution needed (push will detect this)."
);
setTimeout(() => notice?.hide(), 10000);
throw new Error(
`Remote and local have diverged (ahead ${cmp.ahead_by}, behind ${cmp.behind_by}). Vault Sync can only fast-forward in Phase 2.`
`Remote and local have diverged (ahead ${cmp.ahead_by}, behind ${cmp.behind_by}). Pull only supports fast-forward updates.`
);
}
@ -114,14 +114,14 @@ export async function pullFromGitHub(
if (files.length === 0) {
plugin.settings.lastCommitSha = headSha;
await plugin.saveSettings();
notice?.setMessage("Vault Sync: pull complete (no file changes)");
notice?.setMessage("Pull complete (no file changes)");
setTimeout(() => notice?.hide(), 4000);
return;
}
if (files.length === 300) {
new Notice(
"Vault Sync: compare returned exactly 300 files (GitHub's per-call cap). Some changes may be missing — re-seed if pull seems incomplete.",
"Compare returned exactly 300 files (GitHub's per-call cap). Some changes may be missing — re-seed if pull seems incomplete.",
10000
);
}
@ -150,7 +150,7 @@ export async function pullFromGitHub(
}
notice?.setMessage(
`Vault Sync: ${toFetch.length} to fetch, ${toRemove.length} to remove…`
`${toFetch.length} to fetch, ${toRemove.length} to remove…`
);
// Apply removals first (cheap, sequential).
@ -171,7 +171,7 @@ export async function pullFromGitHub(
bytes += content.byteLength;
if (done % 5 === 0) {
notice?.setMessage(
`Vault Sync: ${done}/${toFetch.length} files, ${formatBytes(bytes)}`
`${done}/${toFetch.length} files, ${formatBytes(bytes)}`
);
}
});
@ -181,7 +181,7 @@ export async function pullFromGitHub(
await plugin.saveSettings();
notice?.setMessage(
`Vault Sync: pull complete — ${toFetch.length} updated, ${toRemove.length} removed @ ${headSha.slice(0, 7)}`
`Pull complete — ${toFetch.length} updated, ${toRemove.length} removed @ ${headSha.slice(0, 7)}`
);
setTimeout(() => notice?.hide(), 6000);
}

View file

@ -11,30 +11,28 @@ import {
type TreeUpdateEntry,
} from "./github";
const SKIP_PATHS = new Set<string>([
".obsidian/workspace.json",
".obsidian/workspace-mobile.json",
".obsidian/cache",
".obsidian/appearance.json",
".DS_Store",
"_GIT-DEBUG-ERROR.md",
]);
const SKIP_PREFIXES = [
".trash/",
function buildSkipFn(configDir: string): (path: string) => boolean {
const skipPaths = new Set<string>([
`${configDir}/workspace.json`,
`${configDir}/workspace-mobile.json`,
`${configDir}/cache`,
`${configDir}/appearance.json`,
".DS_Store",
"_GIT-DEBUG-ERROR.md",
]);
// Plugin installs are per-device. Each Obsidian install pulls plugin code
// from its own source (community store, BRAT, etc). Syncing plugin folders
// bloats the repo and risks leaking credentials stored in plugin data.json.
// The list of *which* plugins to enable still syncs via .obsidian/community-plugins.json.
".obsidian/plugins/",
];
function shouldSkip(path: string): boolean {
if (SKIP_PATHS.has(path)) return true;
if (path.endsWith(".tmp")) return true;
for (const p of SKIP_PREFIXES) if (path.startsWith(p)) return true;
if (path.startsWith(".obsidian/workspace")) return true;
return false;
// The list of *which* plugins to enable still syncs via community-plugins.json.
const skipPrefixes = [".trash/", `${configDir}/plugins/`];
const workspacePrefix = `${configDir}/workspace`;
return (path: string) => {
if (skipPaths.has(path)) return true;
if (path.endsWith(".tmp")) return true;
for (const p of skipPrefixes) if (path.startsWith(p)) return true;
if (path.startsWith(workspacePrefix)) return true;
return false;
};
}
async function gitBlobSha(content: ArrayBuffer): Promise<string> {
@ -86,23 +84,24 @@ export async function pushToGitHub(
const ref = parseRepoUrl(repoUrl);
const notice = opts.silent
? null
: new Notice("Vault Sync: scanning local changes…", 0);
: new Notice("Scanning local changes…", 0);
// 1. Detect divergence — refuse if remote moved.
const remoteHead = await getBranchSha(ref, branch || "main", pat);
if (remoteHead !== lastCommitSha) {
notice?.hide();
throw new Error(
`Remote advanced (${remoteHead.slice(0, 7)} vs local baseline ${lastCommitSha.slice(0, 7)}). Run 'Vault Sync: Pull from GitHub' first.`
`Remote advanced (${remoteHead.slice(0, 7)} vs local baseline ${lastCommitSha.slice(0, 7)}). Run 'Pull from GitHub' first.`
);
}
// 2. Walk vault, hash every non-skipped file, compare to fileShaMap.
const shouldSkip = buildSkipFn(plugin.app.vault.configDir);
const allPaths = await listAllVaultFiles(plugin);
const localFiles = allPaths.filter((p) => !shouldSkip(p));
const localSet = new Set(localFiles);
notice?.setMessage(`Vault Sync: hashing ${localFiles.length} files…`);
notice?.setMessage(`Hashing ${localFiles.length} files…`);
type Change = { path: string; kind: "add" | "modify"; content: ArrayBuffer };
const changes: Change[] = [];
@ -121,7 +120,7 @@ export async function pushToGitHub(
scanned++;
if (scanned % 50 === 0) {
notice?.setMessage(
`Vault Sync: hashed ${scanned}/${localFiles.length}`
`Hashed ${scanned}/${localFiles.length}`
);
}
}
@ -131,14 +130,14 @@ export async function pushToGitHub(
}
if (changes.length === 0 && deletions.length === 0) {
notice?.setMessage("Vault Sync: nothing to push");
notice?.setMessage("Nothing to push");
setTimeout(() => notice?.hide(), 4000);
return;
}
// 3. Upload blobs for changed files (sequential to keep memory bounded for large files).
notice?.setMessage(
`Vault Sync: uploading ${changes.length} blobs (${deletions.length} deletions)…`
`Uploading ${changes.length} blobs (${deletions.length} deletions)…`
);
const treeEntries: TreeUpdateEntry[] = [];
@ -156,7 +155,7 @@ export async function pushToGitHub(
bytes += c.content.byteLength;
if (uploaded % 5 === 0) {
notice?.setMessage(
`Vault Sync: uploaded ${uploaded}/${changes.length} (${formatBytes(bytes)})`
`Uploaded ${uploaded}/${changes.length} (${formatBytes(bytes)})`
);
}
}
@ -166,12 +165,12 @@ export async function pushToGitHub(
}
// 4. Build new tree on top of the last commit's tree.
notice?.setMessage("Vault Sync: creating tree…");
notice?.setMessage("Creating tree…");
const baseTreeSha = await getCommitTreeSha(ref, lastCommitSha, pat);
const newTreeSha = await createTree(ref, baseTreeSha, treeEntries, pat);
// 5. Create commit.
notice?.setMessage("Vault Sync: creating commit…");
notice?.setMessage("Creating commit…");
const message = `Vault Sync: ${changes.length} changed, ${deletions.length} deleted from mobile`;
const newCommitSha = await createCommit(
ref,
@ -182,7 +181,7 @@ export async function pushToGitHub(
);
// 6. Update branch ref.
notice?.setMessage("Vault Sync: updating branch…");
notice?.setMessage("Updating branch…");
const upd = await updateRef(ref, branch || "main", newCommitSha, pat);
if (!upd.ok) {
notice?.hide();
@ -204,7 +203,7 @@ export async function pushToGitHub(
await plugin.saveSettings();
notice?.setMessage(
`Vault Sync: pushed ${changes.length} changes (${formatBytes(bytes)}) → ${newCommitSha.slice(0, 7)}`
`Pushed ${changes.length} changes (${formatBytes(bytes)}) → ${newCommitSha.slice(0, 7)}`
);
setTimeout(() => notice?.hide(), 6000);
}

View file

@ -19,7 +19,7 @@ async function ensureDir(plugin: VaultSyncPlugin, dirPath: string): Promise<void
if (!dirPath || dirPath === "/" || dirPath === ".") return;
try {
await plugin.app.vault.adapter.mkdir(dirPath);
} catch (_e) {
} catch {
// already exists
}
}
@ -73,7 +73,7 @@ export async function seedFromGitHub(plugin: VaultSyncPlugin): Promise<void> {
if (!repoUrl) throw new Error("Set a repo URL in Vault Sync settings.");
const ref = parseRepoUrl(repoUrl);
const notice = new Notice("Vault Sync: resolving branch…", 0);
const notice = new Notice("Resolving branch…", 0);
let sha: string;
try {
@ -83,7 +83,7 @@ export async function seedFromGitHub(plugin: VaultSyncPlugin): Promise<void> {
throw e;
}
notice.setMessage(`Vault Sync: listing ${ref.owner}/${ref.repo}@${sha.slice(0, 7)}`);
notice.setMessage(`Listing ${ref.owner}/${ref.repo}@${sha.slice(0, 7)}`);
let tree: TreeEntry[];
try {
@ -97,7 +97,7 @@ export async function seedFromGitHub(plugin: VaultSyncPlugin): Promise<void> {
const blobs = tree.filter((e) => e.type === "blob");
notice.setMessage(
`Vault Sync: ${blobs.length} files, ${dirs.length} dirs — preparing…`
`${blobs.length} files, ${dirs.length} dirs — preparing…`
);
// Pre-create all directories sequentially (cheap, prevents race conditions).
@ -118,7 +118,7 @@ export async function seedFromGitHub(plugin: VaultSyncPlugin): Promise<void> {
byteCount += content.byteLength;
if (fileCount % PROGRESS_EVERY === 0) {
notice.setMessage(
`Vault Sync: ${fileCount}/${blobs.length} files, ${formatBytes(byteCount)}`
`${fileCount}/${blobs.length} files, ${formatBytes(byteCount)}`
);
}
});
@ -128,7 +128,7 @@ export async function seedFromGitHub(plugin: VaultSyncPlugin): Promise<void> {
await plugin.saveSettings();
notice.setMessage(
`Vault Sync: seed complete — ${fileCount} files, ${formatBytes(byteCount)} @ ${sha.slice(0, 7)}`
`Seed complete — ${fileCount} files, ${formatBytes(byteCount)} @ ${sha.slice(0, 7)}`
);
setTimeout(() => notice.hide(), 6000);
}

View file

@ -31,10 +31,8 @@ export class VaultSyncSettingTab extends PluginSettingTab {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Vault Sync (REST)" });
new Setting(containerEl)
.setName("GitHub Personal Access Token")
.setName("GitHub personal access token")
.setDesc(
"Fine-grained or classic PAT with repo (contents: read/write) scope on the target repo."
)
@ -50,7 +48,7 @@ export class VaultSyncSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Repository URL")
.setDesc("e.g. https://github.com/owner/repo")
.setDesc("For example, https://github.com/owner/repo")
.addText((text) =>
text
.setPlaceholder("https://github.com/owner/repo")