Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copy to .env.local and fill values. Do NOT commit .env.local

TASK_SYNC_PROVIDER_A=google
TASK_SYNC_PROVIDER_B=microsoft

TASK_SYNC_STATE_DIR=.task-sync
TASK_SYNC_LOG_LEVEL=info

# Google Tasks
TASK_SYNC_GOOGLE_CLIENT_ID=
TASK_SYNC_GOOGLE_CLIENT_SECRET=
TASK_SYNC_GOOGLE_REFRESH_TOKEN=
TASK_SYNC_GOOGLE_TASKLIST_ID=@default

# Microsoft To Do
TASK_SYNC_MS_CLIENT_ID=
TASK_SYNC_MS_TENANT_ID=common
TASK_SYNC_MS_REFRESH_TOKEN=
# TASK_SYNC_MS_LIST_ID=
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: CI
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run build
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ yarn-error.log*
# local state
.task-sync/

# local env
.env.local
.env

# OS
.DS_Store
159 changes: 95 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,11 @@
# task-sync

Sync tasks between **Microsoft To Do (Microsoft Graph)** and **Google Tasks**.
Sync tasks between **Google Tasks** and **Microsoft To Do**.

This repo currently contains a solid **MVP scaffolding**:
Providers:

- A working CLI (`task-sync`) with:
- `task-sync doctor` → checks config/env
- `task-sync sync --dry-run` → runs the sync engine using **mock providers** (no API keys required)
- `task-sync sync` → intended for real providers (currently scaffolded; will error with clear instructions)
- A minimal sync engine:
- Canonical `Task` model
- JSON state store under `.task-sync/state.json`
- Mapping between provider IDs
- Conflict policy: **last-write-wins** (by `updatedAt`)
- “Zombie prevention”: completed/deleted tasks produce **tombstones** to avoid resurrecting them later
- Unit tests (Vitest)

## MVP scope (what works today)

✅ Works:

- Project builds (`npm run build`)
- Tests pass (`npm test`)
- Dry-run sync with mock providers (`task-sync sync --dry-run`)
- State store + mapping + tombstones logic

🚧 Not yet implemented (by design for this MVP):

- Real Google Tasks API calls
- Real Microsoft Graph API calls
- OAuth flows / token refresh

Those are intentionally left as **scaffolds** so you can add keys/tokens when ready.
- Google Tasks (OAuth refresh-token)
- Microsoft To Do via Microsoft Graph (OAuth refresh-token)

## Quickstart

Expand All @@ -45,55 +19,122 @@ Those are intentionally left as **scaffolds** so you can add keys/tokens when re
npm install
```

### Run health check
### Build + run doctor

```bash
npm run build
node dist/cli.js doctor
# or after global install: task-sync doctor
```

### Run dry-run sync (no API keys)
### Run sync once

```bash
npm run build
node dist/cli.js sync --dry-run
node dist/cli.js sync
```

### Polling mode

```bash
# every 5 minutes
node dist/cli.js sync --poll 5

# or env
export TASK_SYNC_POLL_INTERVAL_MINUTES=5
node dist/cli.js sync
```

You should see a JSON report describing the actions the engine would take.
### Dry-run

Dry-run still uses your configured providers, but **does not write** any changes.

## Configuration (for when real providers are implemented)
```bash
node dist/cli.js sync --dry-run
```

Set these env vars (placeholders for next steps):
## Configuration (.env)

Create a `.env.local` (recommended) or `.env`:

### Provider selection

- `TASK_SYNC_PROVIDER_A` = `google` | `microsoft`
- `TASK_SYNC_PROVIDER_B` = `google` | `microsoft`
```bash
TASK_SYNC_PROVIDER_A=google
TASK_SYNC_PROVIDER_B=microsoft
```

### State

```bash
TASK_SYNC_STATE_DIR=.task-sync
TASK_SYNC_LOG_LEVEL=info
```

### Google Tasks (scaffold)
### Google Tasks

- `TASK_SYNC_GOOGLE_CLIENT_ID`
- `TASK_SYNC_GOOGLE_CLIENT_SECRET`
- `TASK_SYNC_GOOGLE_REFRESH_TOKEN`
- `TASK_SYNC_GOOGLE_TASKLIST_ID` (optional; defaults to `@default`)
```bash
TASK_SYNC_GOOGLE_CLIENT_ID=...
TASK_SYNC_GOOGLE_CLIENT_SECRET=...
TASK_SYNC_GOOGLE_REFRESH_TOKEN=...
TASK_SYNC_GOOGLE_TASKLIST_ID=@default # optional
```

### Microsoft Graph / To Do (scaffold)
### Microsoft To Do (Graph)

- `TASK_SYNC_MS_CLIENT_ID`
- `TASK_SYNC_MS_TENANT_ID`
- `TASK_SYNC_MS_REFRESH_TOKEN`
- `TASK_SYNC_MS_LIST_ID` (optional)
```bash
TASK_SYNC_MS_CLIENT_ID=...
TASK_SYNC_MS_TENANT_ID=common # or your tenant id
TASK_SYNC_MS_REFRESH_TOKEN=...
TASK_SYNC_MS_LIST_ID=... # optional (defaults to first list)
```

Run:

```bash
task-sync doctor
```

to see what’s missing.
to see what's missing.

## OAuth helper scripts (refresh tokens)

These scripts spin up a local HTTP callback server, print an auth URL, and on success print the refresh token.

### Google refresh token

1) Create OAuth credentials in Google Cloud Console:
- APIs & Services → Credentials
- Create Credentials → OAuth client ID
- Application type: **Desktop app** (recommended)
- Enable the **Google Tasks API** on the project

2) Set env vars and run:

```bash
export TASK_SYNC_GOOGLE_CLIENT_ID=...
export TASK_SYNC_GOOGLE_CLIENT_SECRET=...
npm run oauth:google
```

### Microsoft refresh token

1) Create an app registration in Azure:
- Azure Portal → App registrations → New registration
- Add a **redirect URI** (platform: *Mobile and desktop applications*):
- `http://localhost:53683/callback`
- API permissions (Delegated):
- `offline_access`
- `User.Read`
- `Tasks.ReadWrite`

## How state works (.task-sync/)
2) Run:

```bash
export TASK_SYNC_MS_CLIENT_ID=...
export TASK_SYNC_MS_TENANT_ID=common
npm run oauth:microsoft
```

## How state works

`task-sync` writes local state under:

Expand All @@ -103,9 +144,9 @@ This includes:

- `lastSyncAt` watermark (ISO timestamp)
- `mappings`: links a canonical ID to provider IDs
- `tombstones`: prevents resurrecting completed/deleted tasks
- `tombstones`: prevents resurrecting deleted tasks

You can delete `.task-sync/` to reset state.
Delete `.task-sync/` to reset sync state.

## Development

Expand All @@ -117,16 +158,6 @@ npm run lint
npm run typecheck
```

## Next steps (planned)

- Implement GoogleTasksProvider using Google Tasks API
- Implement MicrosoftTodoProvider using Microsoft Graph
- Add real delta queries (list only changed tasks since watermark)
- Improve conflict handling:
- per-field merge strategies
- better deletion semantics
- Add a persistent DB store option (SQLite)

## License

MIT (see LICENSE)
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
{
"name": "task-sync",
"version": "0.1.0",
"description": "Sync tasks between Microsoft To Do and Google Tasks",
"description": "Sync tasks between Google Tasks and Microsoft To Do",
"main": "index.js",
"scripts": {
"test": "vitest run",
"build": "tsup",
"dev": "tsx src/cli.ts",
"test:watch": "vitest",
"lint": "eslint .",
"typecheck": "tsc -p tsconfig.json --noEmit"
"typecheck": "tsc -p tsconfig.json --noEmit",
"oauth:google": "tsx scripts/google_oauth.ts",
"oauth:microsoft": "tsx scripts/microsoft_oauth.ts"
},
"repository": {
"type": "git",
Expand Down
Loading