# Compound Sync

Bidirectional sync between Compound and local markdown files. Edit files locally in your favorite editor and they sync to Compound in real time.

## Quick Start

```bash
npx @doubling/compound-sync
```

That's it. A browser window will open for sign-in (Google or email/password), then the setup wizard will walk you through selecting your organization and sync folder.

### Prerequisites

- [Node.js](https://nodejs.org/) (v18 or later)

### TLS certificates

On Node 22+ the daemon auto-detects the OS system certificate store (`/etc/ssl/cert.pem` on macOS, `/etc/ssl/certs/ca-certificates.crt` or `/etc/pki/tls/certs/ca-bundle.crt` on Linux) and re-execs itself with `NODE_EXTRA_CA_CERTS` set; otherwise `fetch()` to Google Identity / Firebase Auth endpoints fails with `auth/network-request-failed` because Node's bundled CA store doesn't match what those services use.

If you're behind a corporate proxy with custom roots, set `NODE_EXTRA_CA_CERTS` explicitly to your bundle and the daemon will use that instead:

```bash
NODE_EXTRA_CA_CERTS=/path/to/corporate-bundle.pem npx @doubling/compound-sync
```

## What it does

Compound Sync watches a local folder and your Compound workspace simultaneously. Changes in either direction are synced automatically:

- Edit a file locally → it updates in Compound
- Edit a file in Compound → it updates locally
- Create or delete files in either place → synced

## Local folder structure

```
{Sync Folder}/
  {TeamName} Teamspace/   -- team files (bidirectional)
  Private/                 -- your private files (bidirectional)
  Shared by Me/            -- symlinks to files you've shared
  Shared with Me/          -- files shared with you (read-only)
```

## Hidden sync metadata

The daemon maintains per-machine state under your sync folder (dotfiles and
directories, not uploaded to Compound):

| Path | Purpose |
| ---- | ------- |
| `.compound-sync/manifest.json` | Maps synced paths to Firestore file IDs (startup reconcile) |
| `.compound-sync-state.json` | Content-hash baseline for push/pull decisions |
| `.compound-yjs-binding/` | Offline Yjs merge state per file |

Do not edit these by hand. See [docs/features/sync/local-metadata.md](../docs/features/sync/local-metadata.md).

## Running

After setup, start syncing with:

```bash
npx @doubling/compound-sync
```

The sync daemon runs until you press Ctrl+C.

## Multiple workspaces

If you're a member of more than one workspace, you can sync them all from a single daemon. During `--setup` you'll be asked which workspaces to sync (comma-separated or `all`) and given a per-workspace local folder prompt.

You can also keep multiple separate configs (one per workspace, or one for "all my workspaces") with `--config` pointing at an absolute path:

```bash
# Setup
npx @doubling/compound-sync --config ~/.config/compound-sync/work.json --setup

# Run
npx @doubling/compound-sync --config ~/.config/compound-sync/work.json
```

`--config` accepts absolute paths (with `~` expanded), so you can keep configs outside the package install directory. Bare filenames still resolve relative to the package install for backward compatibility.

## Security

- **First sign-in is interactive** (browser popup, Google or email/password). After that the daemon runs silently: it persists the Firebase **refresh token** locally so relaunches don't re-prompt.
- Persisted credentials live at `~/.config/compound-sync/{project}__{account}.json`, written mode `0600` (owner read/write only) via an atomic tmp+rename. The desktop app overrides the path per-account with `COMPOUND_AUTH_FILE`. See [`auth-persistence.ts`](./auth-persistence.ts).
- The config file (`config.json`, `config-*.json`) holds no credentials — only `projectId` and org→localPath mappings.
- All data access uses the Firebase **client SDK** with **App Check** and the **same Firestore/Storage rules as the web app** — the daemon has no Admin-SDK elevation and cannot see beyond the signed-in user's org memberships. See [../docs/security/09-sync-daemon.md](../docs/security/09-sync-daemon.md).

---

## Internal Development

For Doubling team members testing against sandbox or dev environments:

```bash
# Setup
npx @doubling/compound-sync --env sandbox --config config-sandbox.json --setup
npx @doubling/compound-sync --env dev --config config-dev.json --setup

# Run
npx @doubling/compound-sync --env sandbox --config config-sandbox.json
npx @doubling/compound-sync --env dev --config config-dev.json
```

Config files (`config-*.json`) are gitignored and stored locally.

### Tests

```bash
# Pure unit tests (no emulator required)
npm run test:unit

# Integration tests (boots firestore + storage emulators)
npm run test:integration

# Both
npm test
```

### TypeScript conventions (DOU-181)

`sync/` is being migrated to TypeScript file-by-file. The conventions:

- **Source layout: in-place.** Each `.ts` file emits `.js`, `.d.ts`, and source maps as siblings via `tsc` with `outDir: "."`. The emitted `.js` is a gitignored build artifact, never edited by hand; the `.ts` is the source.
- **Imports always use the `.js` extension** (NodeNext convention). At runtime: in dev/test the `tsx` loader maps `./paths.js` to `./paths.ts` so source runs without a build step; in production the compiled `paths.js` exists alongside and resolves directly.
- **Dev (no build needed):** `npm run sync` runs through `node --import tsx`, so `.ts` files load on demand.
- **CI / publish:** `npm run build` compiles every `.ts` source to its `.js` sibling. `prepack` runs build automatically before `npm publish` so the npm tarball ships compiled JS. The desktop staging script also runs `npm run -w sync build` before copying.
- **Typecheck:** `npm run typecheck` (alias for `tsc --noEmit`) at the workspace level, or at repo root `npm run typecheck` which runs web + sync together.
- **Strictness:** `strict: true`, `noUncheckedIndexedAccess: true`, `noImplicitOverride: true`. No `any`. No `// @ts-ignore`. If a third-party module lacks types, declare its shape in a `.d.ts` or PR types upstream.

### Logging convention

Org-scoped output: `  [${orgId}] [tag] message`. Use `console.warn` for recoverable issues, `console.error` for failures. Tags like `[local scan]` and `[manifest reconcile]` identify subsystems.

Phase 1 migrated `paths.js` to `paths.ts` as proof of pattern. Phase 3 ([[DOU-183]]) migrates the remaining source files; everything in `sync/` will be `.ts` by the end.

### Building & running from source

`sync.js` (and the sibling `*.js` files listed in `package.json`'s `files`) are **build artifacts**, gitignored, never edited by hand. Run from the `.ts` source with the `tsx` loader:

```bash
# Dev (no build; tsx maps ./foo.js → ./foo.ts on demand)
npm run sync -- --setup --env sandbox      # first-run wizard against sandbox
npm run sync -- --env sandbox              # start the daemon

# Compile every .ts to its .js/.d.ts sibling (tsc, outDir ".")
npm run build

# Typecheck only
npm run typecheck
```

`--env` takes a **short** name (`sandbox` | `dev` | `prod`); it is expanded to `doubling-compound-<name>` internally ([sync.ts](./sync.ts)). Omitting `--env` defaults to prod.

### Publishing to npm

`@doubling/compound-sync` is published **automatically** by [`.github/workflows/publish-sync.yml`](../.github/workflows/publish-sync.yml) on every GitHub **Release** whose tag starts with `@doubling/compound-sync@` (Changesets creates the release + version bump). Highlights:

- **Trusted Publishing (OIDC):** no long-lived `NPM_TOKEN` — GitHub mints a short-lived OIDC token that the npm CLI (≥ 11.5.1) exchanges for a publish-scoped token.
- `prepack` runs `npm run build`, so the tarball ships compiled JS.
- Idempotent: the workflow checks `npm view` and skips cleanly if `sync/package.json`'s version is already on npm.
- Kept in lockstep with the desktop `.dmg` built from the same commit (DOU-159), so `npx @doubling/compound-sync` and the bundled daemon are identical.

Do not `npm publish` by hand; cut a Changesets release instead.

## Design docs

- Sync daemon design: [DESIGN.md](./DESIGN.md)
- System-wide context: [../DESIGN.md](../DESIGN.md) (§7 sync/conflict model)
- Feature docs: [../docs/features/sync/index.md](../docs/features/sync/index.md), [local-metadata.md](../docs/features/sync/local-metadata.md), [sync-manifest-design.md](../docs/features/sync/sync-manifest-design.md)
- Security: [../docs/security/09-sync-daemon.md](../docs/security/09-sync-daemon.md)
