GitSyncMarks — Architecture Overview
High-Level Architecture
GitSyncMarks is a browser extension (Manifest V3, Chrome + Firefox) that bidirectionally synchronizes bookmarks with a Git repository (GitHub, GitLab, Codeberg, Gitea, Forgejo, or Gogs). It stores each bookmark as an individual JSON file and uses a three-way merge algorithm for conflict-free synchronization.
Component Descriptions
manifest.json / manifest.firefox.json
Extension metadata. Two manifests for browser-specific differences:
| Field | Chrome | Firefox |
|---|---|---|
description |
__MSG_extDescription__ |
__MSG_extDescriptionFirefox__ |
| Background | service_worker: "background.js" |
scripts: ["background.js"] |
| Browser-specific | — | browser_specific_settings.gecko |
Shared: Manifest V3, permissions (bookmarks, storage, alarms, notifications, contextMenus, activeTab, scripting, downloads), host permissions (api.github.com, gitlab.com, codeberg.org; self-hosted origins via optional permissions + runtime grant).
background.js — Background Script
The central coordinator:
- Bookmark event listeners —
onCreated,onRemoved,onChanged,onMovedtrigger debounced auto-sync - Context menu —
contextMenus.onClickedlistener (top-level for SW persistence);setupContextMenus()runs on service worker start (after migrations),chrome.runtime.onStartup, andonInstalled(and when sync storage for profiles, menu config, or Linkwarden visibility changes) so the menu matcheschrome.storage.syncafter every SW wake - Periodic sync alarm —
chrome.alarmsfor periodic three-way merge sync - Message handler — Receives
sync,push,pull,generateFilesNow,getStatus,switchProfile,settingsChanged,setSettingsSyncPassword,clearSettingsSyncPassword,listDeviceConfigs,importDeviceConfig,getDebugLogfrom popup/options - Badge & Error Visibility — Uses a centralized
updateSyncStatusBadgehelper to set an orange!badge and a descriptive tooltip (viachrome.action.setTitle) on sync failure, ensuring it clears reliably on success across all triggers. - Migration — Checks for and migrates legacy
bookmarks.jsonformat on startup
popup.html / popup.js — Popup UI
Toolbar popup with header (icon, title, profile dropdown when 2+ profiles), status area (status line, last change, commit link), conflict box, action buttons (Sync Now, Push, Pull), and compact footer (Settings, GitHub, Report Issue).
options.html / options.js — Settings Page
Full-page settings (opens in tab) with five tabs. options.js is the entry point that imports and orchestrates focused sub-modules in options/.
- Git (sub-tabs: Profile, Connection, Repos) — Profile selector (multiple profiles with separate repos); Git provider (GitHub or Gitea), token, repository, connection test, onboarding; optional Git repos folder (GitHubRepos / GiteaRepos)
- Sync — Sync profile, auto-sync, sync on start/focus, notifications; Debug Log
- Files (sub-tabs: Generated, Settings, Export/Import, Git Add, Bitwarden Backup, History) — Generated files (README.md, bookmarks.html, feed.xml, dashy-conf.yml) with Off/Manual/Auto mode; settings sync to Git (client name + Create in one row; Refresh, profile list, Import & Apply, Sync current to selected in one row; buttons disabled until client name set; password saved after Import/Sync/Create); compact export/import (bookmarks, Dashy, settings plain/encrypted via dropdown); automation guide for adding bookmarks via git or the GitHub Action template (the Action ensures
_index.jsonand_order.jsonso greenfield repos import on the next sync); Bitwarden backup (manual encrypted export upload, optional Git wrap, remote list/download/delete, CLI snippet) - Help — Quick links (Vote on backlog, Documentation, Discussions, Report Issue) as pill buttons; collapsible feature sections (Getting Started with Start setup wizard button, Profiles, GitHub Repos, Popup, Sync, Files, Notifications, Conflicts, Keyboard Shortcuts)
- About — Version, links, license, mobile app
Sub-modules (options/):
wizard.js— Onboarding wizard flow (token validation, repo setup, environment check, first sync)profiles.js— Profile switching, add/rename/delete with confirmation dialogslinkwarden.js— Linkwarden tab: connection test, tag picker, sync, debug log exporthistory.js— Sync history listing (four-column header + rows: date, SHA, client id, actions; checkmark + “current” forlastCommitSha), diff preview, bookmark restore, undocontext-menu-config.js— Context menu item ordering, toggling, category submenu configurationsettings.js— Settings sync to Git, file export/import, generated files toggles, automation clipboardbitwarden-backup.js— Bitwarden/Vaultwarden export upload, list, download, delete (manual Phase 1; no Bitwarden API)help-shortcuts.js— Help tab: keyboard shortcut labels and “Customize shortcuts” linkfactory-reset.js— Files tab: full extension data reset (sync + localstorageclear)
lib/sync-engine.js — Sync Engine (barrel)
Barrel module re-exporting from focused sub-modules:
lib/storage-keys.js— Single source forSTORAGE_KEYSandLOCAL_STORAGE_KEYSstring names; re-exported fromsync-settings.js/sync-engine.jsfor the rest of the applib/context-menu-defaults.js— Default context menu item list, submenu flags, andensureContextMenuItemDefaults()(shared by options andcontext-menu-setup.js)lib/sync-settings.js— Re-exportsSTORAGE_KEYSandLOCAL_STORAGE_KEYSfromstorage-keys.js;SYNC_PRESETS, settings accessors (getSettings,isConfigured,createApi,getDeviceId), local bookmark access (getLocalFileMap), file map filtering (filterForDiff,addGeneratedFiles), and encrypted settings sync (buildEncryptedSettings,applyEncryptedSettings, profile CRUD)lib/sync-core.js— Core sync operations (push,pull,sync,pushForProfile,previewRemoteOrphans,cleanRemoteOrphans), three-way merge (computeDiff,mergeDiffs,mergeOrderJson), sync state management (saveSyncState,getSyncStatus,isSyncInProgress), debounced auto-sync (debouncedSync,bootstrapFirstSync), Linkwarden mirroring, mirror fan-out hook (invokePushToMirrors), and a sync-activity listener (setSyncActivityListener) the background uses to keep the worker alive during long operationslib/sync-history.js— Commit history listing (listSyncHistory), bookmark restore (restoreFromCommit), undo support (getPreviousCommitSha), and diff preview (getCommitDiffPreview)lib/sync-commit-message.js— Parses standard GitSyncMarks commit subjects to extract the device/client id (extractClientIdFromCommitMessage) for Sync History displaylib/sync-migration.js— Legacy single-file format migration (migrateFromLegacyFormat)lib/bitwarden-backup.js— Validates Bitwarden encrypted exports, pushes/lists/downloads/deletes underbackups/bitwarden/(excluded from bookmark merge viaBITWARDEN_BACKUP_DEFAULT_PATTERN)
State is stored as LAST_SYNC_FILES (path → {sha, content}) and LAST_COMMIT_SHA.
lib/git-provider.js — Git Provider Factory
Selects GitHub, Gitea-family, or GitLab adapter via createGitProvider(). Provider capabilities (PROVIDER_CAPS) and shared URL helpers live in lib/git-provider-common.js. UI helpers in lib/provider-ui.js. See PROVIDERS.md.
lib/providers/github-api.js — GitHub API Wrapper
Wraps both the Contents API (legacy, used for migration/validation) and the Git Data API (for atomic multi-file commits on GitHub):
| Method | API | Description |
|---|---|---|
validateToken() |
REST | Check PAT validity |
checkRepo() |
REST | Verify repository access |
listContents(path) |
Contents | List directories at a given path (for folder browser) |
getFile() / createOrUpdateFile() |
Contents | Single-file operations (legacy) |
getLatestCommitSha() |
Git Data | Get current branch HEAD |
getCommit() / getTree() / getBlob() |
Git Data | Read commit, tree, file content. getTree() returns { tree, truncated }; callers must abort on truncated to avoid acting on a partial listing |
getCommitTreeSha() |
Git Data | Resolve a commit's tree SHA, reusing the tree built by the last atomicCommit to skip a redundant getCommit |
getAuthenticatedUser() / listUserRepos() |
REST | /user and paginated /user/repos (used by lib/github-repos.js) |
createBlob() / createTree() / createCommit() |
Git Data | Build new commit |
updateRef() / createRef() |
Git Data | Update or create branch |
atomicCommit(message, fileChanges) |
Git Data | Atomic multi-file commit via layered POST /git/trees with inline content (lib/github-tree-batch.js) |
listCommits({ path, perPage }) |
REST | List recent commits, optionally filtered by path |
lib/bookmark-serializer.js — Serializer
Converts between browser bookmark trees and the per-file format. All generators (Markdown, Netscape HTML, RSS, Dashy YAML) and the tree builder include orphan subfolders: folders present in the file map with their own _order.json but not listed in the parent's _order.json. This handles manually created folders, corrupted _order.json, or migration from older formats.
| Function | Description |
|---|---|
bookmarkTreeToFileMap(tree, basePath) |
Browser tree → file map (path → content) |
fileMapToBookmarkTree(files, basePath) |
File map → bookmark tree (role → children) |
fileMapToMarkdown(files, basePath) |
File map → human-readable Markdown |
fileMapToNetscapeHtml(files, basePath) |
File map → Netscape bookmarks HTML (browser import) |
fileMapToRssFeed(files, basePath) |
File map → RSS 2.0 XML feed |
fileMapToDashyYaml(files, basePath) |
File map → Dashy dashboard YAML config |
generateFilename(title, url) |
Deterministic filename: {slug}_{hash}.json |
detectRootFolderRole(node) |
Detect toolbar/other from browser IDs |
gitTreeToShaMap(entries, basePath) |
Git tree → SHA map for remote change detection |
serializeToJson() / deserializeFromJson() |
Legacy format (for import/export) |
lib/crypto.js — Encryption
AES-256-GCM encryption for the GitHub PAT at rest (non-extractable CryptoKey in IndexedDB, token in chrome.storage.local). Also provides password-based encryption (encryptWithPassword / decryptWithPassword using PBKDF2 + AES-256-GCM) for settings export (.enc files) and settings sync to Git.
lib/linkwarden-api.js — Linkwarden API
Minimal wrapper for the Linkwarden REST API:
saveLink(data): Create a new link in a collection with tags.uploadScreenshot(linkId, blob): Upload a PNG screenshot to an existing link.
lib/keep-alive.js — Background keep-alive
Keeps the non-persistent background (Firefox MV3 event page / Chrome MV3 service worker) alive while long operations run, so they are not terminated at the ~30s idle limit (issue #143). startKeepAlive() / stopKeepAlive() are reference-counted; while held, a timer touches runtime.getPlatformInfo() every 20s to reset the idle timer. The background wires this to sync-core's setSyncActivityListener so it covers every sync/push/pull/restore, and wraps generateFilesNow explicitly.
lib/i18n.js — Internationalization
Custom runtime i18n with manual language selection. SUPPORTED_LANGUAGES in lib/i18n.js lists each locale with code, name, and short (e.g. EN, DE); the options language <select> shows short in the label and name in each option’s title. Loads _locales/{lang}/messages.json, translates DOM via data-i18n attributes. Plain data-i18n sets textContent on each match except <select> elements (so <option data-i18n> still translates; <select data-i18n> is not used). data-i18n-html uses setTrustedHtml() from lib/dom-utils.js. English fallback.
lib/dom-utils.js — Safe DOM helpers
clearElement(el) removes children via replaceChildren(). setTrustedHtml(el, htmlString) inserts trusted HTML (bundled i18n) via DOMParser + replaceChildren, avoiding direct innerHTML assignment flagged by Firefox AMO addons-linter.
lib/theme.js — Theme
Light, dark, or auto (system) theme. initTheme() / applyTheme() read chrome.storage.sync (theme key), resolve dark vs light (including prefers-color-scheme when auto), and toggle html.dark on <html>. On the options page, #theme-selector is a three-segment control (Auto / Dark / Light) with SVG icons; options.js persists the choice and calls applyTheme(). The toolbar popup uses the same storage key via initTheme() (no segment UI there).
lib/ui-density.js — UI Density
Three density levels (compact / medium / large). Stores the choice in chrome.storage.sync (uiDensity key) and sets data-ui-density on <html>. CSS tokens in ui-density.css respond to the attribute; initUiDensity() is called early in every entry point (options, popup, search, linkwarden-save). A segmented S / M / L selector in the options header controls the setting.
ui-density.css — Density Tokens
Root-level CSS custom properties for typography, spacing, padding, and control sizing across three density levels. Loaded before all page-specific stylesheets. Page CSS references these tokens instead of hard-coded values.
shared.css — Shared Theme & Base Components
Loaded after ui-density.css and before each page stylesheet on options, popup, search, and Linkwarden save. Defines the extension-wide --color-* palette for light and html.dark, * box-sizing reset, default body font stack and font-size / line-height from density tokens, shared .btn variants, .spinner + @keyframes spin, and --focus-ring. Page CSS may override a small subset (e.g. popup --color-bg and --radius).
UI surface hierarchy (options and other pages):
| Class | Role |
|---|---|
.card |
Tab-level section (h2 + form or nested units) — options.css |
.card.card-nested |
Feature unit inside a card (h3, description, .card-nested-actions) — tokens --color-surface-nested, --shadow-nested |
.panel-compact |
Lightweight inset panel (wizard steps, popup alerts, Linkwarden metadata) — border + nested surface, no heavy card shadow |
lib/whats-new.js / lib/whats-new-ui.js — Post-update release notes
On chrome.runtime.onInstalled with reason === 'update', background.js writes showWhatsNewForVersion (manifest version string) to chrome.storage.local. popup.js and options.js call mountWhatsNewIfPending() from whats-new-ui.js, which shows a dismissible overlay (styled by whats-new.css) when the pending version matches the manifest and whats-new.js has copy for that version. If .popup is present, the overlay gets whats-new-overlay--popup for a compact, no-scroll layout; the options page uses the default larger panel. Closing the overlay removes the storage key. New installs do not set the flag, so onboarding stays first. Options defers the overlay until the onboarding wizard is hidden (MutationObserver on #onboarding-wizard-screen style). Copy is keyed by exact version; 3.0.3 reuses the 3.0 feature highlights so users updating from 2.8.x (the Chrome Web Store rejected 3.0.0–3.0.2) still discover multi-provider sync and other 3.0 features in-app.
lib/profile-manager.js — Profile Manager
Multiple bookmark profiles (Work/Personal) with separate GitHub repo config:
| Function | Description |
|---|---|
getProfiles() / getActiveProfileId() |
List profiles, get current active profile |
addProfile() / deleteProfile() / saveProfile() |
CRUD for profiles |
switchProfile(targetId) |
Diff-push current profile (skip when unchanged), HEAD-check target cache, delta-pull when remote advanced, replace local bookmarks |
migrateToProfiles() |
Migrate legacy single-config to profiles format |
State stored in chrome.storage.sync (profiles, activeProfileId, optional mirrors[] per profile) and chrome.storage.local (nested per-profile tokens { primary, mirrors: { id: enc } }, sync state including mirrors push metadata).
lib/profile-switch-logic.js — Fast Profile Switch
Testable helpers used by switchProfile() and sync path 8: buildSwitchPushChanges(), mergeLocalIntoSyncFiles() (preserves removed paths in base until push), buildStaleBasePushChanges(), loadTargetFileMapForSwitch() (HEAD check + delta pull).
lib/sync-diff.js — Diff Utilities
Shared computeDiff(), contentEquals(), filterForDiff() — no profile-manager dependency (avoids circular imports during profile switch).
lib/profile-transfer.js — Cross-Profile Transfer
One-shot copy of bookmark file maps between profiles without switching:
| Function | Description |
|---|---|
loadProfileFileMap(profileId) |
Active profile → browser tree; inactive → remote fetch or lastSyncFiles cache |
rewriteFileMapPaths() / filterGeneratedAndAuto() |
Path rewrite and strip generated/auto folders |
previewTransfer() / transferBookmarks() |
Replace or merge mode; optional push via pushForProfile() |
lib/sync-progress.js — Sync Progress UI
Formats onProgress payloads from sync-core (phase, current, total) and runs long sync actions over a syncProgress runtime port (popup sync/push/pull, wizard first sync and pull, connection-tab first push, Automation Generate now). Mirrors the profile-transfer port pattern so progress events are not lost to a connect/message race.
lib/mirror-push.js — Push Mirror Destinations
After each successful primary commit, optional mirror remotes receive a push-only copy of bookmark files (not involved in merge/fetch). Loop guard skips when lastPushedCommitSha === primaryCommitSha.
lib/wizard-sync-choice.js — Wizard First-Sync Choice
Pure helpers for setup-wizard first sync (#146): buildWizardSyncOptions() (allowed modes + default from remote/local bookmark counts), wizardSyncConfirmKey(), fetchRemoteBookmarkState(), and countLocalBookmarks(). Used by options/wizard.js so connection test and repository writes are separate steps.
lib/onboarding.js — Onboarding
First-time and new-profile setup when configuring Git:
| Function | Description |
|---|---|
checkPathSetup(api, basePath) |
Check if path exists and has bookmarks (unreachable / empty / hasBookmarks) |
createMinimalBookmarkStructure(basePath) |
Build _index.json and role folders with _order.json |
initializeRemoteFolder(api, basePath) |
Create minimal structure via atomicCommit |
lib/providers/gitea-api.js — Gitea-family Adapter
Shared adapter for Gitea, Forgejo, Codeberg, and Gogs (providerId preserved per profile). Extends the GitHub client for git data read/write; atomicCommit uses batched blobs + layered trees (one commit), with Contents API sequential fallback. Auth: Authorization: token {PAT}.
lib/providers/gitlab-api.js — GitLab Adapter
Standalone client for gitlab.com and self-managed GitLab. atomicCommit uses POST /repository/commits with actions[]. Subgroup paths encoded in project URL. Auth: Authorization: Bearer {PAT}.
lib/github-api.js — Compatibility Shim
Re-exports GitHubAPI / GitHubError from lib/providers/github-api.js and factory helpers from lib/git-provider.js.
lib/github-repos.js — Git Repos Folder
Fetches the authenticated user's repos via GitHub REST API and maintains a "GitHubRepos (username)" folder:
| Function | Description |
|---|---|
fetchCurrentUser(token) |
{ login } for folder name; delegates to GitHubAPI.getAuthenticatedUser() |
fetchUserRepos(token) |
Paginated { full_name, html_url, private }; delegates to GitHubAPI.listUserRepos() |
updateGitHubReposFolder(token, parentRole, username?, onUsername?) |
Find/create folder, diff existing bookmarks with API list, add/remove/update; optional callback to persist username on first run |
lib/remote-fetch.js — Remote File Map
| Function | Description |
|---|---|
fetchRemoteFileMap(api, basePath, baseFiles) |
Fetch bookmark files via git tree + batched blobs; Gitea-family falls back to Contents API through buildRemoteMaps() |
buildRemoteMaps(api, basePath, baseFiles, commitSha) |
Tree+blob first for all providers with getRecursiveTreeForCommit; Contents API fallback for Gitea-family |
fetchRemoteFileMapAtCommit(api, basePath, commitSha, options?) |
Fetch file map at a specific commit SHA (history restore/preview); batched getBlob (concurrency 5); optional short-lived in-memory cache per owner/repo/path/commit |
Gitea-family performance analysis and benchmark script: GITEA-PERFORMANCE.md (scripts/benchmark-gitea-sync.js).
lib/context-menu.js — Context Menu (barrel)
Barrel module re-exporting from focused sub-modules:
lib/context-menu-constants.js— Menu IDs (MENU_IDS), categories (CATEGORIES), ID prefixes, popup dimensions, and other shared constantslib/context-menu-setup.js— Static menu creation (setupContextMenus), called from the background service worker on startup andchrome.runtime.onInstalledlib/context-menu-dynamic.js— Dynamic menu management: profile submenus, quick folders, folder tree, open-all-from-folder items; concurrency protection (isRefreshing/refreshPending) and debounced rebuild (refreshContextMenuDynamicItemsDebounced, 500ms delay, 5s max-wait)lib/context-menu-handlers.js— Click event dispatch (handleContextMenuClick) and all action handlers (add bookmark, sync, Linkwarden save, search, favicon, profile switch)
| Menu Item | Context | Action |
|---|---|---|
| Add to Toolbar | page, link | Creates bookmark in toolbar root via chrome.bookmarks.create() |
| Add to Other Bookmarks | page, link | Creates bookmark in other root |
| Save to Linkwarden | page, link | Saves URL to Linkwarden instance; supports auto-screenshots via captureVisibleTab |
| Sync Now | page, link | Calls sync() from sync-engine.js directly |
| Search Bookmarks | page, link | Opens dedicated search popup window |
| Copy Favicon URL | page | Copies tab.favIconUrl to clipboard via chrome.scripting.executeScript() |
| Download Favicon | page | Downloads favicon via chrome.downloads.download() |
| Switch Profile | page, link | Submenu with radio items for each profile; active profile checked |
setupContextMenus() is called on SW load, onStartup, and onInstalled (and when relevant sync keys change); it builds the static items with the same contextMenuCreateAsync (await per contextMenus.create) as the dynamic module, then sets the static ready flag and calls refreshContextMenuDynamicItems(). The whole run is in one try/catch (storage read, defaults, and create chain) so early failures are handled and the ready flag does not get stuck. handleContextMenuClick() is wired to a top-level contextMenus.onClicked listener for MV3 service worker persistence.
lib/browser-polyfill.js — Browser Detection
Minimal shim: isFirefox, isChrome, getBrowserName().
File Structure
GitSyncMarks/
├── .cursor/mcp.json # Empty by default: put MCP in ~/.cursor/mcp.json; see docs/TESTING.md for JSON
├── manifest.json # Chrome manifest
├── manifest.firefox.json # Firefox manifest
├── background.js # Background script
├── popup.html / popup.js / popup.css
├── ui-density.css # Density tokens (compact / medium / large)
├── shared.css # Shared --color-* palette, reset, .btn, spinner
├── whats-new.css # Shared overlay styles (popup + options)
├── search.html / search.js / search.css
├── linkwarden-save.html / .js / .css
├── options.html / options.js / options.css
├── options/ # Options page sub-modules
│ ├── wizard.js # Onboarding wizard flow
│ ├── profiles.js # Profile management UI
│ ├── linkwarden.js # Linkwarden tab UI
│ ├── history.js # Sync history & restore
│ ├── context-menu-config.js # Context menu configuration
│ ├── settings.js # Settings sync, export/import, file generation
│ ├── help-shortcuts.js # Help tab keyboard shortcuts
│ └── factory-reset.js # Full data reset
├── lib/
│ ├── sync-engine.js # Barrel: re-exports sync sub-modules
│ ├── storage-keys.js # STORAGE_KEYS / LOCAL_STORAGE_KEYS (source of truth)
│ ├── sync-settings.js # Re-exports keys; settings, encrypted settings sync
│ ├── sync-core.js # Push/pull/sync, three-way merge, auto-sync
│ ├── sync-history.js # Commit history, restore, diff preview
│ ├── sync-commit-message.js # Parse commit subject → client id (history UI)
│ ├── sync-migration.js # Legacy format migration
│ ├── git-provider.js # Provider factory (GitHub / Gitea-family / GitLab)
│ ├── git-provider-common.js # PROVIDER_CAPS + URL helpers
│ ├── provider-ui.js # Shared provider dropdown / form UI
│ ├── dom-utils.js # clearElement / setTrustedHtml (AMO-safe DOM)
│ ├── providers/
│ │ ├── github-api.js # GitHub REST + Git Data API
│ │ ├── gitea-api.js # Gitea-family adapter
│ │ └── gitlab-api.js # GitLab adapter
│ ├── connection-settings.js # Form → createGitProvider helpers
│ ├── host-permissions.js # Runtime Gitea origin permission
│ ├── github-api.js # Re-export shim
│ ├── github-tree-batch.js # Chunk file changes for tree API (inline blob content)
│ ├── bookmark-serializer.js # Per-file bookmark conversion
│ ├── bookmark-replace.js # Replace local bookmarks
│ ├── github-repos.js # GitHub Repos folder
│ ├── profile-manager.js # Multiple profiles, switchProfile
│ ├── profile-switch-logic.js # Fast switch: diff push, HEAD check, delta pull
│ ├── sync-diff.js # computeDiff, filterForDiff, isBitwardenBackupDiffPath (no profile-manager dep)
│ ├── profile-transfer.js # Cross-profile bookmark copy
│ ├── sync-progress.js # Sync progress formatting + runtime port helper
│ ├── mirror-push.js # Push-only mirror destinations
│ ├── onboarding.js # checkPathSetup, initializeRemoteFolder
│ ├── wizard-sync-choice.js # Wizard sync mode matrix and push safety
│ ├── remote-fetch.js # fetchRemoteFileMap
│ ├── crypto.js # Token encryption (AES-256-GCM)
│ ├── context-menu.js # Barrel: re-exports context menu sub-modules
│ ├── context-menu-constants.js # Menu IDs, categories, prefixes
│ ├── context-menu-defaults.js # Default context menu list & merge helper
│ ├── context-menu-setup.js # Static menu creation (on SW start, onStartup, onInstalled)
│ ├── context-menu-dynamic.js # Dynamic profile/folder menus
│ ├── context-menu-handlers.js # Click event dispatch & actions
│ ├── linkwarden-api.js # Linkwarden REST API wrapper
│ ├── keep-alive.js # Keeps MV3 worker alive during long ops (#143)
│ ├── debug-log.js # Debug log for sync diagnostics
│ ├── i18n.js # Internationalization
│ ├── theme.js # Light/dark/auto theme
│ ├── ui-density.js # Compact/medium/large density
│ ├── whats-new.js # Per-version bullets, storage helpers
│ ├── whats-new-ui.js # Dismissible overlay DOM
│ └── browser-polyfill.js # Browser detection
├── _locales/ # 12 languages
│ ├── en/messages.json
│ ├── de/messages.json
│ └── .../messages.json
├── icons/
├── scripts/
│ ├── build.sh # Build Chrome + Firefox packages
│ ├── generate-screenshots.js # Auto-generate store screenshots
│ ├── fetch-app-content.sh # Fetch App README, assets
│ ├── build-docs.js # Markdown → HTML for docs/
│ ├── build-index.js # Build index.html
│ ├── add-bookmark-to-repo.py # Add bookmark JSON + ensure structure (used by add-bookmark.yml)
│ └── verify-test-repo.js # Verify bookmark files in GitHub test repo (API)
├── package.json # npm scripts for building
├── .github/workflows/
│ ├── test-e2e.yml # E2E tests (manual trigger only)
│ ├── release.yml # Build ZIPs, create release on tag
│ ├── screenshots.yml # Generate store screenshots
│ └── add-bookmark.yml # Automation: add bookmark via dispatch (runs scripts/add-bookmark-to-repo.py)
├── docs/ # Architecture documentation
├── website/ # GitHub Pages site
├── store-assets/ # Store listings & screenshots (12 languages)
├── LICENSE
├── PRIVACY.md
└── README.md
Developer Tooling (IDE / agents)
End-to-end regression is covered by Playwright (npm run test:e2e*, see ../e2e/README.md). Cursor can load Chrome DevTools MCP and Firefox DevTools MCP from your user ~/.cursor/mcp.json (or from a per-project file); the reference JSON and duplicate-avoidance notes are in TESTING.md and TESTING.md, not a replacement for the Playwright suite. The committed .cursor/mcp.json keeps an empty mcpServers so a global file can supply both servers without double registration in this clone.
Technology Stack
| Layer | Technology |
|---|---|
| Extension Framework | Manifest V3 (Chrome + Firefox) |
| Background | Service Worker (Chrome) / Background Script (Firefox) |
| Browser APIs | chrome.bookmarks, chrome.storage, chrome.alarms, chrome.contextMenus, chrome.scripting, chrome.downloads |
| Remote Storage | GitHub/GitLab/Gitea-family Git Data API (Gitea Contents API fallback) |
| Authentication | Bearer (GitHub, GitLab) or token (Gitea-family); PAT per profile |
| Sync Algorithm | Three-way merge (base vs local vs remote, per-file diff) |
| i18n | Custom runtime system + Chrome _locales/ |
| Build | Shell script (build.sh), separate Chrome/Firefox packages |
| CI/CD | GitHub Actions |
| JavaScript | Vanilla ES modules (no bundler) |