pushwork 2.0.0-a.sub.0 → 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.
Files changed (251) hide show
  1. package/dist/branches.d.ts +19 -0
  2. package/dist/branches.d.ts.map +1 -0
  3. package/dist/branches.js +111 -0
  4. package/dist/branches.js.map +1 -0
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +238 -272
  8. package/dist/cli.js.map +1 -1
  9. package/dist/config.d.ts +17 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +84 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/fs-tree.d.ts +6 -0
  14. package/dist/fs-tree.d.ts.map +1 -0
  15. package/dist/fs-tree.js +99 -0
  16. package/dist/fs-tree.js.map +1 -0
  17. package/dist/ignore.d.ts +6 -0
  18. package/dist/ignore.d.ts.map +1 -0
  19. package/dist/ignore.js +74 -0
  20. package/dist/ignore.js.map +1 -0
  21. package/dist/index.d.ts +8 -4
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +34 -4
  24. package/dist/index.js.map +1 -1
  25. package/dist/log.d.ts +3 -0
  26. package/dist/log.d.ts.map +1 -0
  27. package/dist/log.js +14 -0
  28. package/dist/log.js.map +1 -0
  29. package/dist/pushwork.d.ts +115 -0
  30. package/dist/pushwork.d.ts.map +1 -0
  31. package/dist/pushwork.js +918 -0
  32. package/dist/pushwork.js.map +1 -0
  33. package/dist/repo.d.ts +14 -0
  34. package/dist/repo.d.ts.map +1 -0
  35. package/dist/repo.js +60 -0
  36. package/dist/repo.js.map +1 -0
  37. package/dist/shapes/custom.d.ts +3 -0
  38. package/dist/shapes/custom.d.ts.map +1 -0
  39. package/dist/shapes/custom.js +57 -0
  40. package/dist/shapes/custom.js.map +1 -0
  41. package/dist/shapes/file.d.ts +20 -0
  42. package/dist/shapes/file.d.ts.map +1 -0
  43. package/dist/shapes/file.js +140 -0
  44. package/dist/shapes/file.js.map +1 -0
  45. package/dist/shapes/index.d.ts +10 -0
  46. package/dist/shapes/index.d.ts.map +1 -0
  47. package/dist/shapes/index.js +35 -0
  48. package/dist/shapes/index.js.map +1 -0
  49. package/dist/shapes/patchwork-folder.d.ts +3 -0
  50. package/dist/shapes/patchwork-folder.d.ts.map +1 -0
  51. package/dist/shapes/patchwork-folder.js +160 -0
  52. package/dist/shapes/patchwork-folder.js.map +1 -0
  53. package/dist/shapes/types.d.ts +37 -0
  54. package/dist/shapes/types.d.ts.map +1 -0
  55. package/dist/shapes/types.js +52 -0
  56. package/dist/shapes/types.js.map +1 -0
  57. package/dist/shapes/vfs.d.ts +3 -0
  58. package/dist/shapes/vfs.d.ts.map +1 -0
  59. package/dist/shapes/vfs.js +88 -0
  60. package/dist/shapes/vfs.js.map +1 -0
  61. package/dist/stash.d.ts +23 -0
  62. package/dist/stash.d.ts.map +1 -0
  63. package/dist/stash.js +118 -0
  64. package/dist/stash.js.map +1 -0
  65. package/flake.lock +128 -0
  66. package/flake.nix +66 -0
  67. package/package.json +15 -48
  68. package/patches/@automerge__automerge-repo@2.6.0-subduction.15.patch +26 -0
  69. package/pnpm-workspace.yaml +5 -0
  70. package/src/branches.ts +93 -0
  71. package/src/cli.ts +258 -408
  72. package/src/config.ts +64 -0
  73. package/src/fs-tree.ts +70 -0
  74. package/src/ignore.ts +33 -0
  75. package/src/index.ts +38 -4
  76. package/src/log.ts +8 -0
  77. package/src/pushwork.ts +1055 -0
  78. package/src/repo.ts +76 -0
  79. package/src/shapes/custom.ts +29 -0
  80. package/src/shapes/file.ts +115 -0
  81. package/src/shapes/index.ts +19 -0
  82. package/src/shapes/patchwork-folder.ts +156 -0
  83. package/src/shapes/types.ts +79 -0
  84. package/src/shapes/vfs.ts +93 -0
  85. package/src/stash.ts +106 -0
  86. package/test/integration/branches.test.ts +389 -0
  87. package/test/integration/pushwork.test.ts +547 -0
  88. package/test/setup.ts +29 -0
  89. package/test/unit/doc-shape.test.ts +612 -0
  90. package/tsconfig.json +2 -3
  91. package/vitest.config.ts +14 -0
  92. package/ARCHITECTURE-ACCORDING-TO-CLAUDE.md +0 -248
  93. package/CLAUDE.md +0 -141
  94. package/README.md +0 -221
  95. package/babel.config.js +0 -5
  96. package/dist/cli/commands.d.ts +0 -71
  97. package/dist/cli/commands.d.ts.map +0 -1
  98. package/dist/cli/commands.js +0 -794
  99. package/dist/cli/commands.js.map +0 -1
  100. package/dist/cli/index.d.ts +0 -2
  101. package/dist/cli/index.d.ts.map +0 -1
  102. package/dist/cli/index.js +0 -19
  103. package/dist/cli/index.js.map +0 -1
  104. package/dist/commands.d.ts +0 -61
  105. package/dist/commands.d.ts.map +0 -1
  106. package/dist/commands.js +0 -861
  107. package/dist/commands.js.map +0 -1
  108. package/dist/config/index.d.ts +0 -71
  109. package/dist/config/index.d.ts.map +0 -1
  110. package/dist/config/index.js +0 -314
  111. package/dist/config/index.js.map +0 -1
  112. package/dist/core/change-detection.d.ts +0 -80
  113. package/dist/core/change-detection.d.ts.map +0 -1
  114. package/dist/core/change-detection.js +0 -523
  115. package/dist/core/change-detection.js.map +0 -1
  116. package/dist/core/config.d.ts +0 -81
  117. package/dist/core/config.d.ts.map +0 -1
  118. package/dist/core/config.js +0 -258
  119. package/dist/core/config.js.map +0 -1
  120. package/dist/core/index.d.ts +0 -6
  121. package/dist/core/index.d.ts.map +0 -1
  122. package/dist/core/index.js +0 -6
  123. package/dist/core/index.js.map +0 -1
  124. package/dist/core/move-detection.d.ts +0 -34
  125. package/dist/core/move-detection.d.ts.map +0 -1
  126. package/dist/core/move-detection.js +0 -121
  127. package/dist/core/move-detection.js.map +0 -1
  128. package/dist/core/snapshot.d.ts +0 -105
  129. package/dist/core/snapshot.d.ts.map +0 -1
  130. package/dist/core/snapshot.js +0 -217
  131. package/dist/core/snapshot.js.map +0 -1
  132. package/dist/core/sync-engine.d.ts +0 -151
  133. package/dist/core/sync-engine.d.ts.map +0 -1
  134. package/dist/core/sync-engine.js +0 -1346
  135. package/dist/core/sync-engine.js.map +0 -1
  136. package/dist/types/config.d.ts +0 -99
  137. package/dist/types/config.d.ts.map +0 -1
  138. package/dist/types/config.js +0 -5
  139. package/dist/types/config.js.map +0 -1
  140. package/dist/types/documents.d.ts +0 -88
  141. package/dist/types/documents.d.ts.map +0 -1
  142. package/dist/types/documents.js +0 -20
  143. package/dist/types/documents.js.map +0 -1
  144. package/dist/types/index.d.ts +0 -4
  145. package/dist/types/index.d.ts.map +0 -1
  146. package/dist/types/index.js +0 -4
  147. package/dist/types/index.js.map +0 -1
  148. package/dist/types/snapshot.d.ts +0 -64
  149. package/dist/types/snapshot.d.ts.map +0 -1
  150. package/dist/types/snapshot.js +0 -2
  151. package/dist/types/snapshot.js.map +0 -1
  152. package/dist/utils/content-similarity.d.ts +0 -53
  153. package/dist/utils/content-similarity.d.ts.map +0 -1
  154. package/dist/utils/content-similarity.js +0 -155
  155. package/dist/utils/content-similarity.js.map +0 -1
  156. package/dist/utils/content.d.ts +0 -10
  157. package/dist/utils/content.d.ts.map +0 -1
  158. package/dist/utils/content.js +0 -31
  159. package/dist/utils/content.js.map +0 -1
  160. package/dist/utils/directory.d.ts +0 -24
  161. package/dist/utils/directory.d.ts.map +0 -1
  162. package/dist/utils/directory.js +0 -52
  163. package/dist/utils/directory.js.map +0 -1
  164. package/dist/utils/fs.d.ts +0 -74
  165. package/dist/utils/fs.d.ts.map +0 -1
  166. package/dist/utils/fs.js +0 -248
  167. package/dist/utils/fs.js.map +0 -1
  168. package/dist/utils/index.d.ts +0 -5
  169. package/dist/utils/index.d.ts.map +0 -1
  170. package/dist/utils/index.js +0 -5
  171. package/dist/utils/index.js.map +0 -1
  172. package/dist/utils/mime-types.d.ts +0 -13
  173. package/dist/utils/mime-types.d.ts.map +0 -1
  174. package/dist/utils/mime-types.js +0 -209
  175. package/dist/utils/mime-types.js.map +0 -1
  176. package/dist/utils/network-sync.d.ts +0 -36
  177. package/dist/utils/network-sync.d.ts.map +0 -1
  178. package/dist/utils/network-sync.js +0 -250
  179. package/dist/utils/network-sync.js.map +0 -1
  180. package/dist/utils/node-polyfills.d.ts +0 -9
  181. package/dist/utils/node-polyfills.d.ts.map +0 -1
  182. package/dist/utils/node-polyfills.js +0 -9
  183. package/dist/utils/node-polyfills.js.map +0 -1
  184. package/dist/utils/output.d.ts +0 -129
  185. package/dist/utils/output.d.ts.map +0 -1
  186. package/dist/utils/output.js +0 -368
  187. package/dist/utils/output.js.map +0 -1
  188. package/dist/utils/repo-factory.d.ts +0 -13
  189. package/dist/utils/repo-factory.d.ts.map +0 -1
  190. package/dist/utils/repo-factory.js +0 -46
  191. package/dist/utils/repo-factory.js.map +0 -1
  192. package/dist/utils/string-similarity.d.ts +0 -14
  193. package/dist/utils/string-similarity.d.ts.map +0 -1
  194. package/dist/utils/string-similarity.js +0 -39
  195. package/dist/utils/string-similarity.js.map +0 -1
  196. package/dist/utils/text-diff.d.ts +0 -37
  197. package/dist/utils/text-diff.d.ts.map +0 -1
  198. package/dist/utils/text-diff.js +0 -93
  199. package/dist/utils/text-diff.js.map +0 -1
  200. package/dist/utils/trace.d.ts +0 -19
  201. package/dist/utils/trace.d.ts.map +0 -1
  202. package/dist/utils/trace.js +0 -63
  203. package/dist/utils/trace.js.map +0 -1
  204. package/src/commands.ts +0 -1134
  205. package/src/core/change-detection.ts +0 -712
  206. package/src/core/config.ts +0 -313
  207. package/src/core/index.ts +0 -5
  208. package/src/core/move-detection.ts +0 -169
  209. package/src/core/snapshot.ts +0 -275
  210. package/src/core/sync-engine.ts +0 -1758
  211. package/src/types/config.ts +0 -111
  212. package/src/types/documents.ts +0 -91
  213. package/src/types/index.ts +0 -3
  214. package/src/types/snapshot.ts +0 -67
  215. package/src/utils/content.ts +0 -34
  216. package/src/utils/directory.ts +0 -73
  217. package/src/utils/fs.ts +0 -297
  218. package/src/utils/index.ts +0 -4
  219. package/src/utils/mime-types.ts +0 -244
  220. package/src/utils/network-sync.ts +0 -319
  221. package/src/utils/node-polyfills.ts +0 -8
  222. package/src/utils/output.ts +0 -450
  223. package/src/utils/repo-factory.ts +0 -73
  224. package/src/utils/string-similarity.ts +0 -54
  225. package/src/utils/text-diff.ts +0 -101
  226. package/src/utils/trace.ts +0 -70
  227. package/test/integration/README.md +0 -328
  228. package/test/integration/clone-test.sh +0 -310
  229. package/test/integration/conflict-resolution-test.sh +0 -309
  230. package/test/integration/debug-both-nested.sh +0 -74
  231. package/test/integration/debug-concurrent-nested.sh +0 -87
  232. package/test/integration/debug-nested.sh +0 -73
  233. package/test/integration/deletion-behavior-test.sh +0 -487
  234. package/test/integration/deletion-sync-test-simple.sh +0 -193
  235. package/test/integration/deletion-sync-test.sh +0 -297
  236. package/test/integration/exclude-patterns.test.ts +0 -144
  237. package/test/integration/full-integration-test.sh +0 -363
  238. package/test/integration/fuzzer.test.ts +0 -818
  239. package/test/integration/in-memory-sync.test.ts +0 -830
  240. package/test/integration/init-sync.test.ts +0 -89
  241. package/test/integration/manual-sync-test.sh +0 -84
  242. package/test/integration/sync-deletion.test.ts +0 -280
  243. package/test/integration/sync-flow.test.ts +0 -291
  244. package/test/jest.setup.ts +0 -34
  245. package/test/run-tests.sh +0 -225
  246. package/test/unit/deletion-behavior.test.ts +0 -249
  247. package/test/unit/enhanced-mime-detection.test.ts +0 -244
  248. package/test/unit/snapshot.test.ts +0 -404
  249. package/test/unit/sync-convergence.test.ts +0 -298
  250. package/test/unit/sync-timing.test.ts +0 -134
  251. 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
package/babel.config.js DELETED
@@ -1,5 +0,0 @@
1
- module.exports = {
2
- presets: [
3
- ['@babel/preset-env', { targets: { node: 'current' } }]
4
- ]
5
- };