feat(sync): backend interface + CLI refactor (#42)

* refactor(sync): extract backend interface, adapt gcal module

Problem: :Pending sync hardcodes Google Calendar — M.sync() does
pcall(require, 'pending.sync.gcal') and calls gcal.sync() directly.
The config has a flat gcal field. This prevents adding new sync backends
without modifying init.lua.

Solution: Define a backend interface contract (name, auth, sync, health
fields), refactor :Pending sync to dispatch via require('pending.sync.'
.. backend_name), add sync table to config with legacy gcal migration,
rename gcal.authorize to gcal.auth, add gcal.health for checkhealth,
and add tab completion for backend names and actions.

* docs(sync): update vimdoc for backend interface

Problem: Vimdoc documents :Pending sync as a bare command that pushes
to Google Calendar, with no mention of backends or the sync table config.

Solution: Update :Pending sync section to show {backend} [{action}]
syntax with examples, add SYNC BACKENDS section documenting the interface
contract, update config example to use sync.gcal, document legacy gcal
migration, and update health check description.

* test(sync): add backend dispatch tests

Problem: No test coverage for sync dispatch logic, config migration,
or gcal module interface conformance.

Solution: Add spec/sync_spec.lua with tests for: bare sync errors,
empty backend errors, unknown backend errors, unknown action errors,
default-to-sync routing, explicit sync/auth routing, legacy gcal config
migration, explicit sync.gcal precedence, and gcal module interface
fields (name, auth, sync, health).
This commit is contained in:
Barrett Ruth 2026-02-26 17:59:04 -05:00
parent 85cf0d42ed
commit 306e11aee6
7 changed files with 327 additions and 36 deletions

View file

@ -47,7 +47,7 @@ REQUIREMENTS *pending-requirements*
- Neovim 0.10+
- No external dependencies for local use
- `curl` and `openssl` are required for Google Calendar sync
- `curl` and `openssl` are required for the `gcal` sync backend
==============================================================================
INSTALL *pending-install*
@ -250,10 +250,23 @@ COMMANDS *pending-commands*
Open the list with |:copen| to navigate to each task's category.
*:Pending-sync*
:Pending sync
Push pending tasks that have a due date to Google Calendar as all-day
events. Requires |pending-gcal| to be configured. See |pending-gcal| for
full details on what gets created, updated, and deleted.
:Pending sync {backend} [{action}]
Run a sync action against a named backend. {backend} is required — bare
`:Pending sync` prints a usage message. {action} defaults to `sync`
when omitted. Each backend lives at `lua/pending/sync/<name>.lua`.
Examples: >vim
:Pending sync gcal " runs gcal.sync()
:Pending sync gcal auth " runs gcal.auth()
:Pending sync gcal sync " explicit sync (same as bare)
<
Tab completion after `:Pending sync ` lists discovered backends.
Tab completion after `:Pending sync gcal ` lists available actions.
Built-in backends: ~
`gcal` Google Calendar one-way push. See |pending-gcal|.
*:Pending-undo*
:Pending undo
@ -440,9 +453,11 @@ loads: >lua
next_task = ']t',
prev_task = '[t',
},
gcal = {
calendar = 'Tasks',
credentials_path = '/path/to/client_secret.json',
sync = {
gcal = {
calendar = 'Tasks',
credentials_path = '/path/to/client_secret.json',
},
},
}
<
@ -508,10 +523,16 @@ Fields: ~
vim.g.pending = { debug = true }
<
{sync} (table, default: {}) *pending.SyncConfig*
Sync backend configuration. Each key is a backend
name and the value is the backend-specific config
table. Currently only `gcal` is built-in.
{gcal} (table, default: nil)
Google Calendar sync configuration. See
|pending.GcalConfig|. Omit this field entirely to
disable Google Calendar sync.
Legacy shorthand for `sync.gcal`. If `gcal` is set
but `sync.gcal` is not, the value is migrated
automatically. New configs should use `sync.gcal`
instead. See |pending.GcalConfig|.
==============================================================================
LUA API *pending-api*
@ -632,13 +653,18 @@ not pulled back into pending.nvim.
Configuration: >lua
vim.g.pending = {
gcal = {
calendar = 'Tasks',
credentials_path = '/path/to/client_secret.json',
sync = {
gcal = {
calendar = 'Tasks',
credentials_path = '/path/to/client_secret.json',
},
},
}
<
The legacy `gcal` top-level key is still accepted and migrated automatically.
New configurations should use `sync.gcal`.
*pending.GcalConfig*
Fields: ~
{calendar} (string, default: 'Pendings')
@ -654,7 +680,7 @@ Fields: ~
that Google provides or as a bare credentials object.
OAuth flow: ~
On the first `:Pending sync` call the plugin detects that no refresh token
On the first `:Pending sync gcal` call the plugin detects that no refresh token
exists and opens the Google authorization URL in the browser using
|vim.ui.open()|. A temporary local HTTP server listens on port 18392 for the
OAuth redirect. The PKCE (Proof Key for Code Exchange) flow is used —
@ -664,7 +690,7 @@ authorization code is exchanged for tokens and the refresh token is stored at
use the stored refresh token and refresh the access token automatically when
it is about to expire.
`:Pending sync` behavior: ~
`:Pending sync gcal` behavior: ~
For each task in the store:
- A pending task with a due date and no existing event: a new all-day event is
created and the event ID is stored in the task's `_extra` table.
@ -677,6 +703,30 @@ For each task in the store:
A summary notification is shown after sync: `created: N, updated: N,
deleted: N`.
==============================================================================
SYNC BACKENDS *pending-sync-backend*
Sync backends are Lua modules under `lua/pending/sync/<name>.lua`. Each
module returns a table conforming to the backend interface: >lua
---@class pending.SyncBackend
---@field name string
---@field auth fun(): nil
---@field sync fun(): nil
---@field health? fun(): nil
<
Required fields: ~
{name} Backend identifier (matches the filename).
{sync} Main sync action. Called by `:Pending sync <name>`.
{auth} Authorization flow. Called by `:Pending sync <name> auth`.
Optional fields: ~
{health} Called by `:checkhealth pending` to report backend-specific
diagnostics (e.g. checking for external tools).
Backend-specific configuration goes under `sync.<name>` in |pending-config|.
==============================================================================
HIGHLIGHT GROUPS *pending-highlights*
@ -728,8 +778,8 @@ Checks performed: ~
- Whether the data directory exists (warning if not yet created)
- Whether the data file exists and can be parsed; reports total task count
- Validates recurrence specs on stored tasks
- Whether `curl` is available (required for Google Calendar sync)
- Whether `openssl` is available (required for OAuth PKCE)
- Discovers sync backends under `lua/pending/sync/` and runs each backend's
`health()` function if it exists (e.g. gcal checks for `curl` and `openssl`)
==============================================================================
DATA FORMAT *pending-data*