pushwork 2.0.0-a.sub.1 → 2.0.0-preview
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/branches.d.ts +19 -0
- package/dist/branches.d.ts.map +1 -0
- package/dist/branches.js +111 -0
- package/dist/branches.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +238 -272
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +84 -0
- package/dist/config.js.map +1 -0
- package/dist/fs-tree.d.ts +6 -0
- package/dist/fs-tree.d.ts.map +1 -0
- package/dist/fs-tree.js +99 -0
- package/dist/fs-tree.js.map +1 -0
- package/dist/ignore.d.ts +6 -0
- package/dist/ignore.d.ts.map +1 -0
- package/dist/ignore.js +74 -0
- package/dist/ignore.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -4
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +14 -0
- package/dist/log.js.map +1 -0
- package/dist/pushwork.d.ts +115 -0
- package/dist/pushwork.d.ts.map +1 -0
- package/dist/pushwork.js +918 -0
- package/dist/pushwork.js.map +1 -0
- package/dist/repo.d.ts +14 -0
- package/dist/repo.d.ts.map +1 -0
- package/dist/repo.js +60 -0
- package/dist/repo.js.map +1 -0
- package/dist/shapes/custom.d.ts +3 -0
- package/dist/shapes/custom.d.ts.map +1 -0
- package/dist/shapes/custom.js +57 -0
- package/dist/shapes/custom.js.map +1 -0
- package/dist/shapes/file.d.ts +20 -0
- package/dist/shapes/file.d.ts.map +1 -0
- package/dist/shapes/file.js +140 -0
- package/dist/shapes/file.js.map +1 -0
- package/dist/shapes/index.d.ts +10 -0
- package/dist/shapes/index.d.ts.map +1 -0
- package/dist/shapes/index.js +35 -0
- package/dist/shapes/index.js.map +1 -0
- package/dist/shapes/patchwork-folder.d.ts +3 -0
- package/dist/shapes/patchwork-folder.d.ts.map +1 -0
- package/dist/shapes/patchwork-folder.js +160 -0
- package/dist/shapes/patchwork-folder.js.map +1 -0
- package/dist/shapes/types.d.ts +37 -0
- package/dist/shapes/types.d.ts.map +1 -0
- package/dist/shapes/types.js +52 -0
- package/dist/shapes/types.js.map +1 -0
- package/dist/shapes/vfs.d.ts +3 -0
- package/dist/shapes/vfs.d.ts.map +1 -0
- package/dist/shapes/vfs.js +88 -0
- package/dist/shapes/vfs.js.map +1 -0
- package/dist/stash.d.ts +23 -0
- package/dist/stash.d.ts.map +1 -0
- package/dist/stash.js +118 -0
- package/dist/stash.js.map +1 -0
- package/flake.lock +128 -0
- package/flake.nix +66 -0
- package/package.json +15 -48
- package/patches/@automerge__automerge-repo@2.6.0-subduction.15.patch +26 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/branches.ts +93 -0
- package/src/cli.ts +258 -408
- package/src/config.ts +64 -0
- package/src/fs-tree.ts +70 -0
- package/src/ignore.ts +33 -0
- package/src/index.ts +38 -4
- package/src/log.ts +8 -0
- package/src/pushwork.ts +1055 -0
- package/src/repo.ts +76 -0
- package/src/shapes/custom.ts +29 -0
- package/src/shapes/file.ts +115 -0
- package/src/shapes/index.ts +19 -0
- package/src/shapes/patchwork-folder.ts +156 -0
- package/src/shapes/types.ts +79 -0
- package/src/shapes/vfs.ts +93 -0
- package/src/stash.ts +106 -0
- package/test/integration/branches.test.ts +389 -0
- package/test/integration/pushwork.test.ts +547 -0
- package/test/setup.ts +29 -0
- package/test/unit/doc-shape.test.ts +612 -0
- package/tsconfig.json +2 -3
- package/vitest.config.ts +14 -0
- package/ARCHITECTURE-ACCORDING-TO-CLAUDE.md +0 -248
- package/CLAUDE.md +0 -141
- package/README.md +0 -221
- package/babel.config.js +0 -5
- package/dist/cli/commands.d.ts +0 -71
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -794
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -19
- package/dist/cli/index.js.map +0 -1
- package/dist/commands.d.ts +0 -61
- package/dist/commands.d.ts.map +0 -1
- package/dist/commands.js +0 -861
- package/dist/commands.js.map +0 -1
- package/dist/config/index.d.ts +0 -71
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -314
- package/dist/config/index.js.map +0 -1
- package/dist/core/change-detection.d.ts +0 -80
- package/dist/core/change-detection.d.ts.map +0 -1
- package/dist/core/change-detection.js +0 -523
- package/dist/core/change-detection.js.map +0 -1
- package/dist/core/config.d.ts +0 -81
- package/dist/core/config.d.ts.map +0 -1
- package/dist/core/config.js +0 -258
- package/dist/core/config.js.map +0 -1
- package/dist/core/index.d.ts +0 -6
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -6
- package/dist/core/index.js.map +0 -1
- package/dist/core/move-detection.d.ts +0 -34
- package/dist/core/move-detection.d.ts.map +0 -1
- package/dist/core/move-detection.js +0 -121
- package/dist/core/move-detection.js.map +0 -1
- package/dist/core/snapshot.d.ts +0 -105
- package/dist/core/snapshot.d.ts.map +0 -1
- package/dist/core/snapshot.js +0 -217
- package/dist/core/snapshot.js.map +0 -1
- package/dist/core/sync-engine.d.ts +0 -157
- package/dist/core/sync-engine.d.ts.map +0 -1
- package/dist/core/sync-engine.js +0 -1379
- package/dist/core/sync-engine.js.map +0 -1
- package/dist/types/config.d.ts +0 -99
- package/dist/types/config.d.ts.map +0 -1
- package/dist/types/config.js +0 -5
- package/dist/types/config.js.map +0 -1
- package/dist/types/documents.d.ts +0 -88
- package/dist/types/documents.d.ts.map +0 -1
- package/dist/types/documents.js +0 -20
- package/dist/types/documents.js.map +0 -1
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -4
- package/dist/types/index.js.map +0 -1
- package/dist/types/snapshot.d.ts +0 -64
- package/dist/types/snapshot.d.ts.map +0 -1
- package/dist/types/snapshot.js +0 -2
- package/dist/types/snapshot.js.map +0 -1
- package/dist/utils/content-similarity.d.ts +0 -53
- package/dist/utils/content-similarity.d.ts.map +0 -1
- package/dist/utils/content-similarity.js +0 -155
- package/dist/utils/content-similarity.js.map +0 -1
- package/dist/utils/content.d.ts +0 -10
- package/dist/utils/content.d.ts.map +0 -1
- package/dist/utils/content.js +0 -31
- package/dist/utils/content.js.map +0 -1
- package/dist/utils/directory.d.ts +0 -24
- package/dist/utils/directory.d.ts.map +0 -1
- package/dist/utils/directory.js +0 -52
- package/dist/utils/directory.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -74
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js +0 -248
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/index.d.ts +0 -5
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -5
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/mime-types.d.ts +0 -13
- package/dist/utils/mime-types.d.ts.map +0 -1
- package/dist/utils/mime-types.js +0 -209
- package/dist/utils/mime-types.js.map +0 -1
- package/dist/utils/network-sync.d.ts +0 -36
- package/dist/utils/network-sync.d.ts.map +0 -1
- package/dist/utils/network-sync.js +0 -250
- package/dist/utils/network-sync.js.map +0 -1
- package/dist/utils/node-polyfills.d.ts +0 -9
- package/dist/utils/node-polyfills.d.ts.map +0 -1
- package/dist/utils/node-polyfills.js +0 -9
- package/dist/utils/node-polyfills.js.map +0 -1
- package/dist/utils/output.d.ts +0 -129
- package/dist/utils/output.d.ts.map +0 -1
- package/dist/utils/output.js +0 -368
- package/dist/utils/output.js.map +0 -1
- package/dist/utils/repo-factory.d.ts +0 -13
- package/dist/utils/repo-factory.d.ts.map +0 -1
- package/dist/utils/repo-factory.js +0 -46
- package/dist/utils/repo-factory.js.map +0 -1
- package/dist/utils/string-similarity.d.ts +0 -14
- package/dist/utils/string-similarity.d.ts.map +0 -1
- package/dist/utils/string-similarity.js +0 -39
- package/dist/utils/string-similarity.js.map +0 -1
- package/dist/utils/text-diff.d.ts +0 -37
- package/dist/utils/text-diff.d.ts.map +0 -1
- package/dist/utils/text-diff.js +0 -93
- package/dist/utils/text-diff.js.map +0 -1
- package/dist/utils/trace.d.ts +0 -19
- package/dist/utils/trace.d.ts.map +0 -1
- package/dist/utils/trace.js +0 -63
- package/dist/utils/trace.js.map +0 -1
- package/src/commands.ts +0 -1134
- package/src/core/change-detection.ts +0 -712
- package/src/core/config.ts +0 -313
- package/src/core/index.ts +0 -5
- package/src/core/move-detection.ts +0 -169
- package/src/core/snapshot.ts +0 -275
- package/src/core/sync-engine.ts +0 -1795
- package/src/types/config.ts +0 -111
- package/src/types/documents.ts +0 -91
- package/src/types/index.ts +0 -3
- package/src/types/snapshot.ts +0 -67
- package/src/utils/content.ts +0 -34
- package/src/utils/directory.ts +0 -73
- package/src/utils/fs.ts +0 -297
- package/src/utils/index.ts +0 -4
- package/src/utils/mime-types.ts +0 -244
- package/src/utils/network-sync.ts +0 -319
- package/src/utils/node-polyfills.ts +0 -8
- package/src/utils/output.ts +0 -450
- package/src/utils/repo-factory.ts +0 -73
- package/src/utils/string-similarity.ts +0 -54
- package/src/utils/text-diff.ts +0 -101
- package/src/utils/trace.ts +0 -70
- package/test/integration/README.md +0 -328
- package/test/integration/clone-test.sh +0 -310
- package/test/integration/conflict-resolution-test.sh +0 -309
- package/test/integration/debug-both-nested.sh +0 -74
- package/test/integration/debug-concurrent-nested.sh +0 -87
- package/test/integration/debug-nested.sh +0 -73
- package/test/integration/deletion-behavior-test.sh +0 -487
- package/test/integration/deletion-sync-test-simple.sh +0 -193
- package/test/integration/deletion-sync-test.sh +0 -297
- package/test/integration/exclude-patterns.test.ts +0 -144
- package/test/integration/full-integration-test.sh +0 -363
- package/test/integration/fuzzer.test.ts +0 -818
- package/test/integration/in-memory-sync.test.ts +0 -830
- package/test/integration/init-sync.test.ts +0 -89
- package/test/integration/manual-sync-test.sh +0 -84
- package/test/integration/sync-deletion.test.ts +0 -280
- package/test/integration/sync-flow.test.ts +0 -291
- package/test/jest.setup.ts +0 -34
- package/test/run-tests.sh +0 -225
- package/test/unit/deletion-behavior.test.ts +0 -249
- package/test/unit/enhanced-mime-detection.test.ts +0 -244
- package/test/unit/snapshot.test.ts +0 -404
- package/test/unit/sync-convergence.test.ts +0 -298
- package/test/unit/sync-timing.test.ts +0 -134
- package/test/unit/utils.test.ts +0 -366
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
# Pushwork Architecture
|
|
2
|
-
|
|
3
|
-
> This document was generated by Claude from reading the source code.
|
|
4
|
-
|
|
5
|
-
Pushwork is a CLI tool for bidirectional file synchronization using **Automerge CRDTs**. It maps a local filesystem directory tree to a mirror tree of Automerge documents and syncs them through a WebSocket relay server. Multiple peers can edit the same files and changes merge automatically.
|
|
6
|
-
|
|
7
|
-
## Module Diagram
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
11
|
-
│ CLI (cli.ts) │
|
|
12
|
-
│ Commander.js argument parsing │
|
|
13
|
-
└────────────────────────┬────────────────────────────────────┘
|
|
14
|
-
│
|
|
15
|
-
▼
|
|
16
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
17
|
-
│ Commands (commands.ts) │
|
|
18
|
-
│ setupCommandContext() → creates Repo + SyncEngine │
|
|
19
|
-
│ sync(), commit(), status(), diff(), clone(), init(), ... │
|
|
20
|
-
└────────────────────────┬────────────────────────────────────┘
|
|
21
|
-
│
|
|
22
|
-
┌──────────────┼──────────────┐
|
|
23
|
-
▼ ▼ ▼
|
|
24
|
-
┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐
|
|
25
|
-
│ ConfigMgr │ │ RepoFactory │ │ SyncEngine │
|
|
26
|
-
│ (config.ts) │ │(repo-factory)│ │ (sync-engine.ts) │
|
|
27
|
-
│ │ │ │ │ │
|
|
28
|
-
│ defaults < │ │ Automerge │ │ Orchestrates the │
|
|
29
|
-
│ global < │ │ Repo with │ │ entire sync cycle │
|
|
30
|
-
│ local │ │ storage + │ │ │
|
|
31
|
-
└──────────────┘ │ WebSocket │ │ ┌────────────────┐ │
|
|
32
|
-
└──────────────┘ │ │ChangeDetector │ │
|
|
33
|
-
│ │ FS vs snapshot │ │
|
|
34
|
-
│ │ vs remote docs │ │
|
|
35
|
-
│ └────────────────┘ │
|
|
36
|
-
│ ┌────────────────┐ │
|
|
37
|
-
│ │ MoveDetector │ │
|
|
38
|
-
│ │ content-sim │ │
|
|
39
|
-
│ │ rename detect │ │
|
|
40
|
-
│ └────────────────┘ │
|
|
41
|
-
│ ┌────────────────┐ │
|
|
42
|
-
│ │SnapshotManager │ │
|
|
43
|
-
│ │ .pushwork/ │ │
|
|
44
|
-
│ │ snapshot.json │ │
|
|
45
|
-
│ └────────────────┘ │
|
|
46
|
-
└──────────┬───────────┘
|
|
47
|
-
│
|
|
48
|
-
┌────────────────────────┼────────────────────┐
|
|
49
|
-
▼ ▼ ▼
|
|
50
|
-
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
51
|
-
│ text-diff.ts │ │ network-sync.ts │ │ directory.ts │
|
|
52
|
-
│ │ │ │ │ │
|
|
53
|
-
│ spliceText() │ │ waitForSync() │ │ getPlainUrl() │
|
|
54
|
-
│ updateText │ │ waitForBidi │ │ getVersioned │
|
|
55
|
-
│ Content() │ │ directionalSync()│ │ Url() │
|
|
56
|
-
│ readDocContent()│ │ batch size: 10 │ │ findFileIn │
|
|
57
|
-
└─────────────────┘ └────────┬─────────┘ │ Directory() │
|
|
58
|
-
│ └─────────────────┘
|
|
59
|
-
▼
|
|
60
|
-
┌──────────────────────┐
|
|
61
|
-
│ Automerge Relay │
|
|
62
|
-
│ (WebSocket server) │
|
|
63
|
-
│ sync3.automerge.org │
|
|
64
|
-
└──────────────────────┘
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Source Layout
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
src/
|
|
71
|
-
cli.ts — Commander.js entry point; registers all commands
|
|
72
|
-
commands.ts — Command implementations; shared setupCommandContext()
|
|
73
|
-
index.ts — Library export entry point
|
|
74
|
-
|
|
75
|
-
core/
|
|
76
|
-
sync-engine.ts — Two-phase sync coordinator (the heart of the system)
|
|
77
|
-
change-detection.ts — ChangeDetector: local + remote diff engine
|
|
78
|
-
move-detection.ts — MoveDetector: content-similarity-based rename detection
|
|
79
|
-
snapshot.ts — SnapshotManager: reads/writes .pushwork/snapshot.json
|
|
80
|
-
config.ts — ConfigManager: loads/merges defaults < global < local
|
|
81
|
-
|
|
82
|
-
utils/
|
|
83
|
-
repo-factory.ts — Creates the Automerge Repo with storage + network adapters
|
|
84
|
-
network-sync.ts — waitForSync(), waitForBidirectionalSync()
|
|
85
|
-
text-diff.ts — spliceText(), updateTextContent(), readDocContent()
|
|
86
|
-
content.ts — contentHash(), isContentEqual()
|
|
87
|
-
directory.ts — getPlainUrl(), findFileInDirectoryHierarchy()
|
|
88
|
-
fs.ts — readFileContent(), writeFileContent(), listDirectory()
|
|
89
|
-
mime-types.ts — isEnhancedTextFile(), getEnhancedMimeType()
|
|
90
|
-
string-similarity.ts — Sørensen-Dice coefficient for move detection
|
|
91
|
-
output.ts — Output singleton (ora spinner + chalk colors)
|
|
92
|
-
trace.ts — Debug tracing helpers
|
|
93
|
-
|
|
94
|
-
types/
|
|
95
|
-
documents.ts — FileDocument, DirectoryDocument, DirectoryEntry
|
|
96
|
-
snapshot.ts — SyncSnapshot, SnapshotFileEntry, SyncResult
|
|
97
|
-
config.ts — DirectoryConfig, GlobalConfig, CLI option interfaces
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## Data Model
|
|
101
|
-
|
|
102
|
-
Every node in the synced tree is an Automerge document:
|
|
103
|
-
|
|
104
|
-
- **FileDocument** — A file. Content is either collaborative text (mutable CRDT string supporting character-level `splice`) or raw bytes (for binary files). Also stores name, extension, mimeType, and permissions metadata.
|
|
105
|
-
- **DirectoryDocument** — A directory. Contains a `docs` array of `{name, type, url}` entries pointing to child documents. Forms a tree rooted at one directory document.
|
|
106
|
-
- **DirectoryEntry** — A single `{name, type, url}` reference inside a directory's `docs` array. For artifact files, the URL includes Automerge heads (a versioned snapshot); for regular files, it's a plain mutable URL.
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
DirectoryDocument (root)
|
|
110
|
-
├── docs: [
|
|
111
|
-
│ { name: "readme.md", type: "file", url: "automerge:abc123" }
|
|
112
|
-
│ { name: "src", type: "folder", url: "automerge:def456" }
|
|
113
|
-
│ { name: "config.json", type: "file", url: "automerge:ghi789" }
|
|
114
|
-
│ ]
|
|
115
|
-
│
|
|
116
|
-
└── DirectoryDocument (src/)
|
|
117
|
-
└── docs: [
|
|
118
|
-
{ name: "index.ts", type: "file", url: "automerge:jkl012" }
|
|
119
|
-
]
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
## How the Layers Connect
|
|
123
|
-
|
|
124
|
-
### CLI → Commands
|
|
125
|
-
|
|
126
|
-
`cli.ts` registers Commander.js commands and calls exported functions from `commands.ts`. The CLI layer is thin — it only handles argument parsing, option validation, and process exits. It doesn't touch Automerge directly.
|
|
127
|
-
|
|
128
|
-
### Commands → Core
|
|
129
|
-
|
|
130
|
-
`commands.ts` provides the shared setup function `setupCommandContext()`, which:
|
|
131
|
-
|
|
132
|
-
1. Verifies the `.pushwork/` directory exists
|
|
133
|
-
2. Loads and merges config (defaults < global `~/.pushwork/config.json` < local `.pushwork/config.json`)
|
|
134
|
-
3. Creates an Automerge `Repo` via `createRepo()` — sets up `NodeFSStorageAdapter` for local persistence and optionally a `BrowserWebSocketClientAdapter` for network sync
|
|
135
|
-
4. Instantiates a `SyncEngine` with the repo, working directory, and config
|
|
136
|
-
|
|
137
|
-
Every command (sync, commit, status, diff, ls, etc.) calls `setupCommandContext()`, uses the `SyncEngine`, then calls `safeRepoShutdown()`.
|
|
138
|
-
|
|
139
|
-
### SyncEngine — The Coordinator
|
|
140
|
-
|
|
141
|
-
`SyncEngine` owns the entire sync cycle and holds references to three subsystems:
|
|
142
|
-
|
|
143
|
-
- **ChangeDetector** — compares local filesystem state against the snapshot and remote Automerge documents to classify every file as local-only, remote-only, both-changed, or unchanged
|
|
144
|
-
- **MoveDetector** — pairs local-only deletions with local-only creations using content similarity (Sørensen-Dice coefficient) to detect renames
|
|
145
|
-
- **SnapshotManager** — reads and writes `.pushwork/snapshot.json`, which records the last-known state of every tracked file and directory
|
|
146
|
-
|
|
147
|
-
## The Sync Cycle
|
|
148
|
-
|
|
149
|
-
When you run `pushwork sync`, the following happens:
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
1. NETWORK WAIT
|
|
153
|
-
Wait for any incoming remote changes to arrive
|
|
154
|
-
(bidirectional sync poll until heads stabilize)
|
|
155
|
-
│
|
|
156
|
-
▼
|
|
157
|
-
2. DETECT CHANGES + MOVES
|
|
158
|
-
Compare local filesystem, snapshot heads, and
|
|
159
|
-
remote document heads to classify every file.
|
|
160
|
-
Pair deletions with creations to detect renames.
|
|
161
|
-
│
|
|
162
|
-
▼
|
|
163
|
-
3. PUSH (local → remote)
|
|
164
|
-
Process directories deepest-first:
|
|
165
|
-
new files → repo.create() a new Automerge doc
|
|
166
|
-
modified → handle.changeAt(heads) + spliceText()
|
|
167
|
-
deleted → remove entry from parent directory doc
|
|
168
|
-
moved → rename entry in parent directory doc
|
|
169
|
-
│
|
|
170
|
-
▼
|
|
171
|
-
4. NETWORK UPLOAD
|
|
172
|
-
waitForSync() — push docs to relay in batches of 10
|
|
173
|
-
waitForBidirectionalSync() — poll until heads stabilize
|
|
174
|
-
│
|
|
175
|
-
▼
|
|
176
|
-
5. RE-DETECT CHANGES
|
|
177
|
-
Fresh change detection pass; filter to remote-only
|
|
178
|
-
and both-changed files
|
|
179
|
-
│
|
|
180
|
-
▼
|
|
181
|
-
6. PULL (remote → local)
|
|
182
|
-
Write remote changes to the local filesystem
|
|
183
|
-
Process shallowest paths first (parents before children)
|
|
184
|
-
│
|
|
185
|
-
▼
|
|
186
|
-
7. UPDATE SNAPSHOT
|
|
187
|
-
Re-read all document heads, save snapshot.json
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
### The Snapshot as 3-Way Baseline
|
|
191
|
-
|
|
192
|
-
The snapshot's `head` (Automerge document heads) for each file acts as the common ancestor in a 3-way comparison:
|
|
193
|
-
|
|
194
|
-
- **Local filesystem content** vs **content at snapshot head** = local changes
|
|
195
|
-
- **Current Automerge document** vs **snapshot head** = remote changes
|
|
196
|
-
- Both changed → `BOTH_CHANGED` (the CRDT handles merge automatically via `changeAt`)
|
|
197
|
-
|
|
198
|
-
## Key Design Decisions
|
|
199
|
-
|
|
200
|
-
### Leaf-First Push Ordering
|
|
201
|
-
|
|
202
|
-
During the push phase, directories are sorted deepest-first. This guarantees that when a parent directory document is updated, all its children's Automerge documents are already finalized with their correct URLs. The parent then records the correct versioned URLs.
|
|
203
|
-
|
|
204
|
-
### `changeAt(heads)` for Merging
|
|
205
|
-
|
|
206
|
-
Edits branch from the snapshot's known heads using `handle.changeAt(heads, callback)`. This gives Automerge a proper common ancestor for conflict-free 3-way merge. Without this, concurrent edits from different peers would overwrite each other instead of merging.
|
|
207
|
-
|
|
208
|
-
### Orphaned Deletions
|
|
209
|
-
|
|
210
|
-
Deleting a file just removes it from its parent directory's `docs` array. The Automerge document itself is left as an orphan — clearing a large text CRDT would be O(n) in character operations, which is very slow for large files.
|
|
211
|
-
|
|
212
|
-
### Artifact Directories
|
|
213
|
-
|
|
214
|
-
Files in configured `artifact_directories` (default: `dist/`) are treated as immutable snapshots rather than collaboratively-edited text. They use `RawString` (not the text CRDT), are never diffed/spliced, and are always replaced with a new document on update. A SHA-256 `contentHash` in the snapshot detects local changes without reading the Automerge doc.
|
|
215
|
-
|
|
216
|
-
### Batched Network Sync
|
|
217
|
-
|
|
218
|
-
Documents sync to the relay server in batches of 10 (`SYNC_BATCH_SIZE`). The Automerge sync server is single-threaded with no backpressure, so sending 100+ documents simultaneously would overwhelm it.
|
|
219
|
-
|
|
220
|
-
### Immutable String Handling
|
|
221
|
-
|
|
222
|
-
Old Automerge documents may store text content as `RawString` (immutable) instead of the collaborative text CRDT. You can't `splice` into these. The codebase handles this with `updateTextContent()` which detects the type and either splices or assigns directly, and a nuclear path in `updateRemoteFile()` that recreates the entire document if needed.
|
|
223
|
-
|
|
224
|
-
## On-Disk Layout
|
|
225
|
-
|
|
226
|
-
```
|
|
227
|
-
your-project/
|
|
228
|
-
.pushwork/
|
|
229
|
-
config.json ← local config overrides
|
|
230
|
-
snapshot.json ← change-detection baseline (paths, URLs, heads)
|
|
231
|
-
automerge/ ← Automerge document binary storage
|
|
232
|
-
|
|
233
|
-
~/.pushwork/
|
|
234
|
-
config.json ← global user-level config
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
## Key Dependencies
|
|
238
|
-
|
|
239
|
-
| Package | Purpose |
|
|
240
|
-
|---|---|
|
|
241
|
-
| `@automerge/automerge` | Core CRDT engine: splice, changeAt, RawString |
|
|
242
|
-
| `@automerge/automerge-repo` | Repo, DocHandle, document lifecycle management |
|
|
243
|
-
| `@automerge/automerge-repo-network-websocket` | WebSocket transport to relay server |
|
|
244
|
-
| `@automerge/automerge-repo-storage-nodefs` | Local filesystem persistence for Automerge docs |
|
|
245
|
-
| `@commander-js/extra-typings` | CLI command framework |
|
|
246
|
-
| `diff` | Character-level diffing to feed `A.splice()` |
|
|
247
|
-
| `glob` | Recursive filesystem enumeration |
|
|
248
|
-
| `ignore` | Gitignore-style pattern matching for excludes |
|
package/CLAUDE.md
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# Pushwork - Claude's Notes
|
|
2
|
-
|
|
3
|
-
Always update this file as you learn new things about the codebase — patterns, pitfalls, performance considerations, architectural decisions. This is your persistent memory across sessions.
|
|
4
|
-
|
|
5
|
-
## What to do after changing code
|
|
6
|
-
|
|
7
|
-
Always run `npm run build` (which runs `tsc`) after finishing changes to verify compilation.
|
|
8
|
-
|
|
9
|
-
## Code style
|
|
10
|
-
|
|
11
|
-
- `src/core/sync-engine.ts` and `src/commands.ts` use tabs for indentation
|
|
12
|
-
- `src/utils/network-sync.ts` and `src/cli.ts` use 2-space indentation
|
|
13
|
-
- Adding a new CLI command requires changes in 4 places: types (`src/types/config.ts` for options interface), engine (`src/core/sync-engine.ts` for method), commands (`src/commands.ts` for command function), CLI (`src/cli.ts` for registration + import)
|
|
14
|
-
|
|
15
|
-
## What pushwork is
|
|
16
|
-
|
|
17
|
-
Pushwork is a CLI tool for bidirectional file synchronization using Automerge CRDTs. It maps a local filesystem directory to a tree of Automerge documents, syncing changes in both directions through a relay server. Multiple users can edit the same files and changes merge automatically without conflicts.
|
|
18
|
-
|
|
19
|
-
## Architecture overview
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
CLI (cli.ts) -> Commands (commands.ts) -> SyncEngine (core/sync-engine.ts)
|
|
23
|
-
|
|
|
24
|
-
+---------+---------+
|
|
25
|
-
| | |
|
|
26
|
-
ChangeDetector | MoveDetector
|
|
27
|
-
SnapshotManager
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### Key files
|
|
31
|
-
|
|
32
|
-
- `src/cli.ts` - Commander.js CLI entry point, defines all commands
|
|
33
|
-
- `src/commands.ts` - Command implementations, `setupCommandContext()` is the shared setup
|
|
34
|
-
- `src/core/sync-engine.ts` - The heart of the system. Two-phase sync: push local changes, then pull remote changes
|
|
35
|
-
- `src/core/change-detection.ts` - Compares local filesystem state against snapshot to find changes
|
|
36
|
-
- `src/core/move-detection.ts` - Detects file renames/moves by content similarity
|
|
37
|
-
- `src/core/snapshot.ts` - Manages `.pushwork/snapshot.json`, tracks what's been synced
|
|
38
|
-
- `src/core/config.ts` - Config loading/merging (defaults < global < local)
|
|
39
|
-
- `src/utils/text-diff.ts` - `spliceText()` for character-level CRDT edits, `updateTextContent()` for handling legacy immutable strings, `readDocContent()` for normalizing content reads
|
|
40
|
-
|
|
41
|
-
### Type definitions
|
|
42
|
-
|
|
43
|
-
- `src/types/documents.ts` - FileDocument, DirectoryDocument, DirectoryEntry
|
|
44
|
-
- `src/types/config.ts` - DirectoryConfig, GlobalConfig, all CLI option interfaces
|
|
45
|
-
- `src/types/snapshot.ts` - SyncSnapshot, SnapshotFileEntry, SyncResult
|
|
46
|
-
|
|
47
|
-
## How sync works
|
|
48
|
-
|
|
49
|
-
### Data model
|
|
50
|
-
|
|
51
|
-
Every file becomes an Automerge document (`FileDocument`) with content stored as either collaborative text (for text files, supporting character-level merge) or raw bytes (for binary files). Directories become `DirectoryDocument`s containing a `docs` array of `{name, type, url}` entries pointing to children. The whole thing forms a tree rooted at one directory document.
|
|
52
|
-
|
|
53
|
-
### Two-phase sync
|
|
54
|
-
|
|
55
|
-
1. **Push** (local -> remote): Detect local filesystem changes vs snapshot. New files get new Automerge docs. Modified files get spliced. Deleted files are removed from their parent directory document (the orphaned doc is left as-is).
|
|
56
|
-
2. **Network sync**: Wait for documents to reach the relay server, level-by-level deepest-first (children before parents).
|
|
57
|
-
3. **Pull** (remote -> local): Re-detect changes after network sync. Write remote-only changes to the local filesystem.
|
|
58
|
-
|
|
59
|
-
### Snapshot
|
|
60
|
-
|
|
61
|
-
The snapshot (`.pushwork/snapshot.json`) records:
|
|
62
|
-
- `rootDirectoryUrl` - the root Automerge document URL
|
|
63
|
-
- `files` - map of relative path -> `{url, head}` for every tracked file
|
|
64
|
-
- `directories` - map of relative path -> `{url, head}` for every tracked directory
|
|
65
|
-
|
|
66
|
-
The `head` (Automerge document heads) is how change detection works: if a document's current heads differ from the snapshot heads, it has changed.
|
|
67
|
-
|
|
68
|
-
### Versioned URLs
|
|
69
|
-
|
|
70
|
-
Automerge URLs can include heads (e.g. `automerge:docid#head1,head2`). Pushwork stores versioned URLs in directory entries so clients can fetch the exact version. `getPlainUrl()` strips heads when you need a mutable handle; `getVersionedUrl()` adds current heads.
|
|
71
|
-
|
|
72
|
-
## Immutable string handling
|
|
73
|
-
|
|
74
|
-
Old Automerge documents may store text content as `RawString` (aka `ImmutableString`) instead of the collaborative text CRDT. You can't `splice` into these. Two strategies:
|
|
75
|
-
|
|
76
|
-
1. **`updateTextContent()`** - Inside a change callback, detects if the field is a regular string (splice-able) or legacy immutable (assign directly to convert it).
|
|
77
|
-
2. **`updateRemoteFile()` nuclear path** - If `A.isImmutableString(content)` is true, throws away the old document entirely, creates a brand new one with proper mutable text, and replaces the entry in the parent directory via `replaceFileInDirectory()`.
|
|
78
|
-
|
|
79
|
-
`readDocContent()` normalizes `RawString` to plain strings when reading.
|
|
80
|
-
|
|
81
|
-
## CLI commands
|
|
82
|
-
|
|
83
|
-
- `pushwork init [path]` - Initialize, creates root directory document
|
|
84
|
-
- `pushwork clone <url> <path>` - Clone from an Automerge URL
|
|
85
|
-
- `pushwork sync [path]` - Full bidirectional sync (default: force mode — uses default config, preserves snapshot for incremental change detection)
|
|
86
|
-
- `--dry-run` - Preview only
|
|
87
|
-
- `--gentle` - Use merged config instead of defaults
|
|
88
|
-
- `--nuclear` - Recreate all Automerge documents from scratch (except root)
|
|
89
|
-
- `--force` - Silently accepted for backwards compatibility (does nothing, force is now the default)
|
|
90
|
-
- `pushwork track <url> [path]` - Set root directory URL without full init (creates minimal `.pushwork/snapshot.json`). `root` is a hidden alias.
|
|
91
|
-
- `pushwork commit [path]` - Save to Automerge docs without network sync
|
|
92
|
-
- `pushwork status [path]` - Show sync status
|
|
93
|
-
- `pushwork diff [path]` - Show changes
|
|
94
|
-
- `pushwork url [path]` - Print root Automerge URL
|
|
95
|
-
- `pushwork ls [path]` - List tracked files
|
|
96
|
-
- `pushwork config [path]` - View config
|
|
97
|
-
- `pushwork watch [path]` - Watch + build + sync loop
|
|
98
|
-
- `pushwork rm [path]` - Remove local `.pushwork` data
|
|
99
|
-
|
|
100
|
-
## Config
|
|
101
|
-
|
|
102
|
-
Stored in `.pushwork/config.json` (local) and `~/.pushwork/config.json` (global). Merged: defaults < global < local.
|
|
103
|
-
|
|
104
|
-
Key fields:
|
|
105
|
-
- `sync_enabled: boolean` - Whether to do network sync
|
|
106
|
-
- `sync_server: string` - WebSocket relay URL (default: `wss://subduction.sync.inkandswitch.com`)
|
|
107
|
-
- `exclude_patterns: string[]` - Gitignore-style patterns (default: `.git`, `node_modules`, `*.tmp`, `.pushwork`, `.DS_Store`)
|
|
108
|
-
- `sync.move_detection_threshold: number` - Similarity threshold for move detection (0-1, default 0.7)
|
|
109
|
-
|
|
110
|
-
## Network sync details (Subduction)
|
|
111
|
-
|
|
112
|
-
Pushwork uses Subduction for network sync. Subduction replaces the old `BrowserWebSocketClientAdapter` with a Subduction transport layer that handles sync automatically.
|
|
113
|
-
|
|
114
|
-
- **Repo setup** (`src/utils/repo-factory.ts`): Creates a `WebCryptoSigner`, wraps `NodeFSStorageAdapter` in `SubductionStorageBridge`, hydrates `Subduction`, and calls `connectDiscover()` to the sync server. The `Repo` receives `{ subduction, storage }` instead of `{ storage, network }`.
|
|
115
|
-
- **Sync verification**: Uses head-stability polling instead of `StorageId`-based `getSyncInfo()`. Both `waitForSync()` and `waitForBidirectionalSync()` poll document heads until they stabilize.
|
|
116
|
-
- Uses `waitForSync()` to verify documents reach the server by polling head stability
|
|
117
|
-
- Uses `waitForBidirectionalSync()` to poll until document heads stabilize (no more incoming changes)
|
|
118
|
-
- Accepts optional `handles` param to check only specific handles instead of full tree traversal (used post-push in `sync()`)
|
|
119
|
-
- Timeout scales dynamically: `max(timeoutMs, 5000 + docCount * 50)` so large trees don't prematurely time out
|
|
120
|
-
- Tree traversal (`collectHeadsRecursive`) fetches siblings concurrently via `Promise.all`
|
|
121
|
-
- Documents sync level-by-level, deepest first, so children are on the server before their parents
|
|
122
|
-
- `handlesByPath` map tracks which documents changed and need syncing
|
|
123
|
-
- Default sync server: `wss://subduction.sync.inkandswitch.com`
|
|
124
|
-
|
|
125
|
-
## Leaf-first ordering
|
|
126
|
-
|
|
127
|
-
`pushLocalChanges()` processes directories deepest-first via `batchUpdateDirectory()`, propagating subdirectory URL updates as it walks up toward the root. This ensures directory entries always point to the latest version of their children.
|
|
128
|
-
|
|
129
|
-
## The `changeWithOptionalHeads` helper
|
|
130
|
-
|
|
131
|
-
Used throughout sync-engine: if heads are available, calls `handle.changeAt(heads, cb)` to branch from a known version; otherwise falls back to `handle.change(cb)`. This is important for conflict-free merging when multiple peers are editing.
|
|
132
|
-
|
|
133
|
-
## Performance pitfalls
|
|
134
|
-
|
|
135
|
-
- **Avoid splicing large text deletions.** Automerge text CRDTs track every character as an individual op. `A.splice(doc, path, 0, largeString.length)` to clear a large file is O(n) in CRDT ops and very slow. This is why `deleteRemoteFile()` no longer clears content — it just lets the document become orphaned when removed from its parent directory.
|
|
136
|
-
- **Avoid diffing artifact files.** `diffChars()` is O(n*m) and pointless for artifact directories since they use RawString (immutable snapshots). Artifact files should always be replaced with a fresh document rather than diffed+spliced. This applies to `updateRemoteFile()`, `applyMoveToRemote()`, and change detection. `ChangeDetector` skips `getContentAtHead()` and `getCurrentRemoteContent()` for artifact paths — it uses a SHA-256 `contentHash` stored in the snapshot to detect local changes, and checks heads to detect remote changes. If neither changed, the artifact is skipped entirely. The `contentHash` field on `SnapshotFileEntry` is optional and only populated for artifact files.
|
|
137
|
-
- **Sync timeout recovery.** `waitForSync()` returns `{ failed: DocHandle[] }` instead of throwing. When documents fail to sync (timeout or unavailable), `recreateFailedDocuments()` creates new Automerge docs with the same content, updates snapshot entries and parent directory references, then retries once. If documents still fail after recreation, it's reported as an error (not a warning) so the sync shows as "PARTIAL" rather than "SYNCED".
|
|
138
|
-
- **Document availability during clone.** `repo.find()` rejects with "Document X is unavailable" if the sync server doesn't have the document yet. `DocHandle.doc()` is synchronous and throws if the handle isn't ready. For clone scenarios, `sync()` retries `repo.find()` for the root document with exponential backoff (up to 6 attempts). `ChangeDetector.findDocument()` wraps `repo.find()` + `doc()` with retry logic for all document fetches during change detection.
|
|
139
|
-
- **Server load.** `waitForSync` processes documents in batches of 10 (`SYNC_BATCH_SIZE`) to avoid flooding the server with concurrent sync messages. Without batching, syncing 100+ documents simultaneously can overwhelm the sync server.
|
|
140
|
-
- **`waitForBidirectionalSync` on large trees.** Full tree traversal (`getAllDocumentHeads`) is expensive because it `repo.find()`s every document. For post-push stabilization, pass the `handles` option to only check documents that actually changed. The initial pre-pull call still needs the full scan to discover remote changes. The dynamic timeout adds the first scan's duration on top of the base timeout, since the first scan is just establishing baseline — its duration shouldn't count against stability-wait time.
|
|
141
|
-
- **Versioned URLs and `repo.find()`.** `repo.find(versionedUrl)` returns a view handle whose `.heads()` returns the VERSION heads, not the current document heads. Always use `getPlainUrl()` when you need the current/mutable state. The snapshot head update loop at the end of `sync()` must use `getPlainUrl(snapshotEntry.url)` — without this, artifact directories (which store versioned URLs) get stale heads written to the snapshot, causing `changeAt()` to fork from the wrong point on the next sync. This was the root cause of the artifact deletion resurrection bug: `batchUpdateDirectory` would `changeAt` from an empty directory state where the file entry didn't exist yet, so the splice found nothing to delete.
|
package/README.md
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
# Pushwork
|
|
2
|
-
|
|
3
|
-
Bidirectional file synchronization using Automerge CRDTs for conflict-free collaborative editing.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Conflict-Free Sync**: Automatic conflict resolution using Automerge CRDTs
|
|
8
|
-
- **Real-time Collaboration**: Multiple users can edit the same files simultaneously
|
|
9
|
-
- **Intelligent Move Detection**: Detects file renames and moves based on content similarity
|
|
10
|
-
- **Offline Support**: Works offline and gracefully handles network interruptions
|
|
11
|
-
- **Cross-Platform**: Runs on Windows, macOS, and Linux
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
pnpm install
|
|
17
|
-
pnpm run build
|
|
18
|
-
pnpm link --global
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Requires: Node.js 18+, pnpm 8.15.0+
|
|
22
|
-
|
|
23
|
-
## Quick Start
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
# Initialize a directory
|
|
27
|
-
pushwork init ./my-project
|
|
28
|
-
|
|
29
|
-
# Clone an existing repository
|
|
30
|
-
pushwork clone <automerge-url> ./project
|
|
31
|
-
|
|
32
|
-
# Sync changes
|
|
33
|
-
pushwork sync
|
|
34
|
-
|
|
35
|
-
# Check status
|
|
36
|
-
pushwork status
|
|
37
|
-
|
|
38
|
-
# Get shareable URL
|
|
39
|
-
pushwork url
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Commands
|
|
43
|
-
|
|
44
|
-
### Core Commands
|
|
45
|
-
|
|
46
|
-
**`init [path]`** - Initialize sync in a directory
|
|
47
|
-
|
|
48
|
-
- `--sync-server <url>` - Custom sync server URL
|
|
49
|
-
- `--sync-server-storage-id <id>` - Custom storage ID
|
|
50
|
-
- `--debug` - Export performance flame graphs
|
|
51
|
-
|
|
52
|
-
**`clone <url> <path>`** - Clone an existing synced directory
|
|
53
|
-
|
|
54
|
-
- `--force` - Overwrite existing directory
|
|
55
|
-
- `--sync-server <url>` - Custom sync server URL
|
|
56
|
-
- `--sync-server-storage-id <id>` - Custom storage ID
|
|
57
|
-
|
|
58
|
-
**`sync [path]`** - Run bidirectional synchronization
|
|
59
|
-
|
|
60
|
-
- `--dry-run` - Preview changes without applying
|
|
61
|
-
- `--verbose` - Show detailed progress
|
|
62
|
-
- `--debug` - Export performance flame graphs
|
|
63
|
-
|
|
64
|
-
**`status [path]`** - Show sync status and repository info
|
|
65
|
-
|
|
66
|
-
- `--verbose` - Show detailed status including all tracked files
|
|
67
|
-
|
|
68
|
-
**`commit [path]`** - Commit local changes without network sync
|
|
69
|
-
|
|
70
|
-
- `--dry-run` - Preview what would be committed
|
|
71
|
-
- `--debug` - Export performance flame graphs
|
|
72
|
-
|
|
73
|
-
### Utility Commands
|
|
74
|
-
|
|
75
|
-
**`diff [path]`** - Show differences between local and remote
|
|
76
|
-
|
|
77
|
-
- `--name-only` - Show only changed file names
|
|
78
|
-
|
|
79
|
-
**`url [path]`** - Show the Automerge root URL for sharing
|
|
80
|
-
|
|
81
|
-
**`ls [path]`** - List tracked files
|
|
82
|
-
|
|
83
|
-
- `--long` - Show Automerge URLs
|
|
84
|
-
|
|
85
|
-
**`config [path]`** - View or edit configuration
|
|
86
|
-
|
|
87
|
-
- `--list` - Show full configuration
|
|
88
|
-
- `--get <key>` - Get specific config value (dot notation)
|
|
89
|
-
|
|
90
|
-
**`rm [path]`** - Remove local pushwork data
|
|
91
|
-
|
|
92
|
-
**`watch [path]`** - Watch directory, build, and sync automatically
|
|
93
|
-
|
|
94
|
-
- `--script <command>` - Build script (default: "pnpm build")
|
|
95
|
-
- `--dir <dir>` - Directory to watch (default: "src")
|
|
96
|
-
- `--verbose` - Show build output
|
|
97
|
-
|
|
98
|
-
**`log [path]`** - Show sync history _(experimental, limited functionality)_
|
|
99
|
-
|
|
100
|
-
**`checkout <sync-id> [path]`** - Restore to previous sync _(not yet implemented)_
|
|
101
|
-
|
|
102
|
-
## Configuration
|
|
103
|
-
|
|
104
|
-
Configuration is stored in `.pushwork/config.json`:
|
|
105
|
-
|
|
106
|
-
```json
|
|
107
|
-
{
|
|
108
|
-
"sync_server": "wss://sync3.automerge.org",
|
|
109
|
-
"sync_server_storage_id": "3760df37-a4c6-4f66-9ecd-732039a9385d",
|
|
110
|
-
"sync_enabled": true,
|
|
111
|
-
"defaults": {
|
|
112
|
-
"exclude_patterns": [".git", "node_modules", "*.tmp", ".pushwork"],
|
|
113
|
-
"large_file_threshold": "100MB"
|
|
114
|
-
},
|
|
115
|
-
"diff": {
|
|
116
|
-
"show_binary": false
|
|
117
|
-
},
|
|
118
|
-
"sync": {
|
|
119
|
-
"move_detection_threshold": 0.8,
|
|
120
|
-
"prompt_threshold": 0.5,
|
|
121
|
-
"auto_sync": false,
|
|
122
|
-
"parallel_operations": 4
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
## How It Works
|
|
128
|
-
|
|
129
|
-
Pushwork uses Automerge CRDTs for automatic conflict resolution:
|
|
130
|
-
|
|
131
|
-
- **Text files**: Character-level merging preserves all changes
|
|
132
|
-
- **Binary files**: Last-writer-wins with automatic convergence
|
|
133
|
-
- **Directories**: Additive merging supports simultaneous file creation
|
|
134
|
-
|
|
135
|
-
Sync process:
|
|
136
|
-
|
|
137
|
-
1. **Push**: Apply local changes to Automerge documents
|
|
138
|
-
2. **Pull**: Apply remote changes to local filesystem
|
|
139
|
-
3. **Convergence**: All repositories reach identical state
|
|
140
|
-
|
|
141
|
-
State tracking:
|
|
142
|
-
|
|
143
|
-
- `.pushwork/snapshot.json` - Tracks sync state and file mappings
|
|
144
|
-
- `.pushwork/config.json` - Configuration settings
|
|
145
|
-
- Content-based change detection using Automerge document heads
|
|
146
|
-
|
|
147
|
-
### Document Schema
|
|
148
|
-
|
|
149
|
-
**File Document:**
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
{
|
|
153
|
-
"@patchwork": { type: "file" };
|
|
154
|
-
name: string;
|
|
155
|
-
extension: string;
|
|
156
|
-
mimeType: string;
|
|
157
|
-
content: string | Uint8Array;
|
|
158
|
-
metadata: {
|
|
159
|
-
permissions: number;
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**Directory Document:**
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
{
|
|
168
|
-
"@patchwork": { type: "folder" };
|
|
169
|
-
docs: Array<{
|
|
170
|
-
name: string;
|
|
171
|
-
type: "file" | "folder";
|
|
172
|
-
url: AutomergeUrl;
|
|
173
|
-
}>;
|
|
174
|
-
lastSyncAt?: number;
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Development
|
|
179
|
-
|
|
180
|
-
### Setup
|
|
181
|
-
|
|
182
|
-
```bash
|
|
183
|
-
git clone <repository-url>
|
|
184
|
-
cd pushwork
|
|
185
|
-
pnpm install
|
|
186
|
-
pnpm run build
|
|
187
|
-
pnpm run dev # Watch mode
|
|
188
|
-
pnpm test # Run tests
|
|
189
|
-
pnpm run test:watch # Watch mode for tests
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Project Structure
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
src/
|
|
196
|
-
├── cli/ # Command-line interface
|
|
197
|
-
├── core/ # Core sync engine
|
|
198
|
-
├── config/ # Configuration management
|
|
199
|
-
├── tracing/ # Performance tracing
|
|
200
|
-
├── types/ # TypeScript type definitions
|
|
201
|
-
└── utils/ # Shared utilities
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
### Testing
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
pnpm test # Unit tests
|
|
208
|
-
./test/run-tests.sh # All integration tests
|
|
209
|
-
./test/integration/conflict-resolution-test.sh # Specific test
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
### Profiling
|
|
213
|
-
|
|
214
|
-
```bash
|
|
215
|
-
pushwork sync --debug # Export flame graphs
|
|
216
|
-
clinic flame --collect-only -- node --enable-source-maps --prof $(pnpm root -g)/pushwork/dist/cli.js sync
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
## License
|
|
220
|
-
|
|
221
|
-
MIT License
|