The 1.11 compatibility gate previously hardcoded the target Obsidian
version in two places (an obsidian-1_11 alias devDependency and a static
tsconfig.compat.json), so bumping minAppVersion would silently leave the
check pinned to the old version.
Replace both with scripts/typecheck-compat.mjs, which reads the target from
versions.json (the entry for the current manifest version, falling back to
manifest.minAppVersion), installs the matching Obsidian typings into a
git-ignored cache on demand, and type-checks src against them. The gate now
follows minAppVersion automatically with no code changes.
Also fold `typecheck:compat` into `npm run build` so it runs wherever the
build runs (the husky pre-commit hook and any CI step that builds).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3
Release 1.2.0 raised minAppVersion to 1.13.0, pushing users still on
Obsidian 1.11/1.12 back to the older 1.1.2 build via versions.json. Only
three 1.13-era APIs were in use; two were already runtime-guarded, but
ButtonComponent.setDestructive() would throw on older Obsidian.
- Guard setDestructive() behind applyDestructiveStyle() (no-ops on < 1.13).
- Declare SettingDefinitionItem locally and read update() via a cast so the
forward-compatible settings code type-checks against 1.11 typings.
- Ship 1.2.1 with minAppVersion 1.11.0 (manifest.json + versions.json).
- Add tsconfig.compat.json and `npm run typecheck:compat` to type-check src
against the 1.11 API, plus regression tests for applyDestructiveStyle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3
Bump semantic-release, vitest, jsdom, and related tooling to latest
patch versions, and add npm overrides for transitive packages bundled
deep inside the npm CLI (sigstore, tar, ip-address) and the eslint
toolchain (js-yaml, undici via @actions/http-client) that npm install
alone could not reach.
brace-expansion needed no override: npm's default resolver already
picks the highest version satisfying each consumer's own semver range
once tar/sigstore/etc. are unpinned, so a nested override there only
forced an incompatible version onto minimatch@3.1.5 and broke lockfile
consistency (`npm ci` failed with EUSAGE).
All flagged packages were dev-only; none ship in the built plugin.
npm audit now reports 0 vulnerabilities, and `npm ci` + build/test/lint
pass from a clean install.
Obsidian APIs used (getSettingDefinitions, PluginSettingTab.update,
setDestructive) all require 1.13.0+, but manifest.json still declared
1.12.7. Bump minAppVersion to match and cut a new release version.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
detectRename only checked whether a tracked old path's file was missing
from the vault, with no content verification. Any orphaned syncMetadata
entry (e.g. left behind by a local delete, which never cleared it) would
cause the next unrelated push to be misclassified as a rename from that
stale path, using create-only semantics that 422'd if the target path
already existed remotely.
- detectRename now confirms identity by checking that the remote content
at the candidate old path still matches what's being pushed.
- handleRename looks up the existing remote file at the new path and
sends its sha, so pushing onto an existing remote path updates instead
of failing with "file already exists".
- Local deletes (single and batch) now clear syncMetadata for the
deleted path so it can't become an orphaned rename source later.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Token fields for GitLab, GitHub, and Gitea were plain text inputs, so
tokens were visible in plaintext during screen shares, recordings, or
on shared machines. They're now password-type inputs with a toggle
(eye icon) to reveal them when the user needs to verify what they
pasted.
- drop unnecessary type assertion in SyncStatusView tab rendering
- use window.setTimeout instead of global setTimeout
- use window instead of globalThis when resolving Electron's require
docs: list supported Git providers at the top of README
The push ribbon icon's tooltip and the "Push/Pull current file"
command names embedded the configured service name (e.g. "Push to
GitHub") but were only set once at plugin load. Switching service in
Settings afterward left them stale until Obsidian was reloaded.
- Ribbon tooltip is now re-applied via setTooltip() on every
saveSettings(), so it always reflects the current service.
- Command names have no rename API in Obsidian, so they're now
generic ("Push current file" / "Pull current file"), matching the
wording already used by "Push all files" / "Pull all files".
Single-file push/pull already detected when both local and remote
changed since the last sync and prompted via SyncConflictModal, but
"Push all"/"Pull all"/multi-select batch actions skipped that check
and force-overwrote. Batch operations now skip conflicting files and
report a conflicts count so nothing is silently clobbered.
Also correct delete-confirmation copy: it claimed local deletes are
recoverable ("moved to trash") unconditionally, but the actual
destination depends on the vault's "Deleted files" setting (which can
be permanent). Wording now defers to that setting instead of
asserting recoverability.
- listFilesDetailed rethrows 404 branch-resolution failures with a
message naming the branch, instead of a bare "Git Service Error (404)"
- testConnection now also checks the configured branch exists and
reports repoOk/branchOk separately so the settings UI can warn when
the repo is reachable but the branch is missing
- fixes settings.ts silently reporting "connection successful" even
when testConnection's result was never checked
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Refresh checked files one at a time (a sequential network request per
file) and re-rendered the whole view after every file, so a large vault
was slow. Run the per-file checks with a bounded concurrency pool (8) and
re-render on a ~150ms throttle plus a final render. Behavior and results
are unchanged; wall time drops roughly in line with the concurrency.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Adds end-to-end symlink syncing driven by the "Symbolic links" setting
(real / follow / skip; default real), building on the earlier detection.
- Pull: on desktop with "real", a remote symlink is recreated as a real OS
link via Node fs (utils/symlink.ts, guarded by Platform/FileSystemAdapter);
otherwise the target path is written as content.
- Push: GitHubService.pushSymlink commits a real symlink blob (mode 120000)
through the Git Data API (blob -> tree -> commit -> ref). getFile now
reports isSymlink/symlinkTarget.
- Config 防呆: only GitHub offers "real"; on GitLab/Gitea (no API to create
symlinks) "real" resolves to "skip" via getEffectiveSymlinkHandling.
- Safety: a "follow" push never overwrites a detected remote symlink with a
regular file; it is skipped with a notice.
- Docs: docs/symlink-handling.md plus a README settings note.
Lint is satisfied without disables (Electron global require, minimal Node
type shims). Adds tests for getFile detection, the pushSymlink Git Data
sequence, and the remote-symlink push guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Symlinks are Git blobs with mode 120000 whose content is the target path.
They were treated as ordinary files, which caused 404s on fetch and could
corrupt the file on pull. Add detection and let the user choose behavior.
- Settings: new "Symbolic links" option (symlinkHandling) with real
(default) / follow / skip.
- Services: listFilesDetailed() reports each entry's symlink flag from the
tree mode (120000) for GitHub and GitLab; listFiles() now delegates to it.
- Refresh/discovery: with "skip", remote symlinks are excluded from sync;
with "follow"/"real" they are included and synced as the target content.
Note: true OS-level symlink recreation on desktop and pushing files as
symlinks (Git Data API) are not yet implemented — on all platforms "real"
currently syncs the target content like "follow"; mobile has no symlink
API regardless. Tracked as a follow-up.
Add tests for symlink detection on both services and update settings fixtures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Loading .gitignore files probes paths that often don't exist remotely.
getFile already treats 404 as "empty file" (handleFileNotFound) and the
gitignore lookup ignores failures, but safeRequest logged every 404 as an
error — twice, because its own catch re-caught the error it had just
thrown. This produced scary "Git Service Request Failed (404): Not Found"
console output during a normal pull/refresh.
Restructure safeRequest so the catch only wraps the network call (no
double-logging of HTTP-status errors), and log an expected 404 at debug
level instead of error. Non-404 statuses are still logged as errors and
all statuses still throw so callers can handle them.
Add a debug level to the logger and regression tests for 404 vs 500.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
The ribbon button used 'list-checks' while the Sync Status view itself
uses 'git-compare' (getIcon). Use 'git-compare' for the ribbon too so the
"Open sync status" icon matches the sync status view icon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
The shared workflow obsidian-plugin-ci.yml@v1 no longer defines the
skip-sonar input or the SONAR_TOKEN secret, which made the caller
workflow invalid ("Invalid input/secret ... is not defined in the
referenced workflow"). Remove both so the workflow validates again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Reading a symlinked file via Obsidian's cached vault.read(TFile) can fail
(notably on mobile), which broke both refresh and push: the status check
swallowed the error and mislabeled the file as 'unsynced', so the user
saw a Push button that then failed re-reading the symlink.
Fall back to vault.adapter.read/readBinary(path) when vault.read throws,
in both the status view and the sync manager. Also stop silently
swallowing the status-check error so the real cause is logged.
Add a regression test covering the adapter fallback on push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
The view mixed hand-picked Unicode glyphs (✓ ⚠ ↑ ↓ ⟳ ↻ ✕ ≡ ⎇ 📁),
duplicated across files, which rendered at inconsistent sizes/weights
across platforms and could drift (e.g. the Refresh button used ↻ while
the "checking" status used ⟳).
Centralize every icon in src/ui/components/icons.ts as Lucide icon ids and
render them with Obsidian's setIcon, so status, action-bar, tab, and
info-strip icons all share one consistent icon set. Tabs now derive their
icon from statusMeta so they can no longer diverge from the file list.
Add CSS to size the SVGs uniformly.
Add setIcon to the obsidian test mock and update statusMeta expectations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Pushing a new file via the single-file push button failed with GitHub
HTTP 422. A 404 lookup returns sha === '' for new files, and the manual
push path (sync-manager pushFile/conflict resolution) forwarded that
empty string into the Contents API request body as "sha":"", which
GitHub rejects. Batch push avoided this via `remote.sha || undefined`.
Fix at the service layer so every caller is safe: only include sha in the
GitHub request body when it is non-empty. Apply the same guard to the
GitLab service's last_commit_id for symmetry.
Add regression tests for blank-sha pushes on both services.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Refresh failed with the cryptic "Unexpected token '<', "<!DOCTYPE ..."
is not valid JSON" whenever the Git server returned an HTML page (login,
SSO redirect, or proxy/error page) on a 2xx/3xx response. safeRequest
only threw on status >= 400, so such bodies slipped through and crashed
at response.json.
Add BaseGitService.parseJson() which detects non-JSON/HTML bodies and
throws an actionable message, and use it for all response.json reads in
the GitHub and GitLab services. Also harden parseErrorResponse so HTML
error pages produce a clear message instead of dumping the raw document.
Add tests covering HTML 2xx responses, HTML detection by leading '<',
malformed JSON, and HTML error pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Removes the eslint-disable comment and uses the standard setTimeout
which is properly typed in the DOM lib and satisfies the obsidianmd
lint rule requiring window-prefixed timer functions to use bare form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gitea's git/trees endpoint requires a tree or commit SHA, not a branch
name, on instances older than ~1.17. Resolve branch to commit SHA via
/branches/{branch} first, then fetch /git/trees/{commitSha}?recursive=1.
Also updates README with provider compatibility table and SVG icons for
GitHub, GitLab, and Gitea.
https://claude.ai/code/session_019Jbz6HpQvWU1wpm5M6MZpd
- Add GiteaService implementing BaseGitService/GitServiceInterface
- Uses Gitea API v1 endpoints (/api/v1/repos/{owner}/{repo}/...)
- POST for file creation, PUT for updates (differs from GitHub)
- Authorization: token {token} header
- Add giteaToken, giteaBaseUrl, giteaOwner, giteaRepo settings fields
- Add Gitea option to service type dropdown in settings UI
- Add displayGiteaSettings() panel in settings tab
- Update initializeGitService() to instantiate GiteaService
- Update getServiceName() to handle 'gitea' type
- Add comprehensive test suite (gitea-service.test.ts, 15 test cases)
- Update existing test fixtures to include new Gitea settings fields
Closes#26https://claude.ai/code/session_019Jbz6HpQvWU1wpm5M6MZpd