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.
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 -157
  133. package/dist/core/sync-engine.d.ts.map +0 -1
  134. package/dist/core/sync-engine.js +0 -1379
  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 -1795
  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,450 +0,0 @@
1
- import chalk from "chalk";
2
- import ora, { Ora } from "ora";
3
-
4
- /**
5
- * Clean terminal output manager (Singleton)
6
- * - Progress stays on one line (spinner updates in place)
7
- * - No emojis
8
- * - Background colors for section headers
9
- * - Minimal output
10
- * - Supports scrolling task lines (max-lines)
11
- */
12
- export class Output {
13
- private static instance: Output | null = null;
14
- private spinner: Ora | null = null;
15
- private taskStartTime: number | null = null;
16
- private taskOriginalMessage: string | null = null; // Original task message for done()
17
- private taskCurrentMessage: string | null = null; // Current display message (can be updated)
18
- private taskLines: string[] = []; // Lines written during active task
19
- private taskMaxLines: number = 0; // 0 = unlimited
20
-
21
- private constructor() {}
22
-
23
- /**
24
- * Get the singleton instance
25
- */
26
- static getInstance(): Output {
27
- if (!Output.instance) {
28
- Output.instance = new Output();
29
- }
30
- return Output.instance;
31
- }
32
-
33
- /**
34
- * Reset the singleton (useful for testing)
35
- */
36
- static reset(): void {
37
- if (Output.instance?.spinner) {
38
- Output.instance.spinner.stop();
39
- Output.instance.spinner.clear();
40
- }
41
- Output.instance = null;
42
- }
43
-
44
- /**
45
- * Start a task with spinner - updates in place
46
- * Completes any previous task before starting the new one
47
- * @param message - The task message
48
- * @param maxLines - Maximum number of task lines to show (0 = unlimited, lines scroll)
49
- */
50
- task(message: string, maxLines: number = 0): void {
51
- // Complete any existing task first
52
- if (this.spinner) {
53
- this.done();
54
- }
55
-
56
- this.taskStartTime = Date.now();
57
- this.taskOriginalMessage = message;
58
- this.taskCurrentMessage = message;
59
- this.taskMaxLines = maxLines;
60
- this.taskLines = [];
61
- this.spinner = ora(message).start();
62
- }
63
-
64
- /**
65
- * Update spinner text (stays on same line)
66
- */
67
- update(message: string): void {
68
- if (this.spinner) {
69
- this.taskCurrentMessage = message;
70
- this.#updateTaskDisplay();
71
- }
72
- }
73
-
74
- /**
75
- * Add a line to the active task (appears below spinner, scrolls if max-lines set)
76
- * Lines are dimmed and temporary - they disappear when task completes unless kept
77
- * If no task is active, displays as a regular log message
78
- */
79
- taskLine(message: string, keepOnComplete: boolean = false): void {
80
- if (!this.spinner) {
81
- // No active task, just log normally as regular output
82
- this.info(message);
83
- return;
84
- }
85
-
86
- // Add to task lines buffer with keep flag
87
- this.taskLines.push(keepOnComplete ? `[keep]${message}` : message);
88
-
89
- // If max lines set, trim from the start (scroll)
90
- if (this.taskMaxLines > 0 && this.taskLines.length > this.taskMaxLines) {
91
- this.taskLines = this.taskLines.slice(-this.taskMaxLines);
92
- }
93
-
94
- this.#updateTaskDisplay();
95
- }
96
-
97
- /**
98
- * Clear all task lines (useful when you want to reset the scrolling window)
99
- */
100
- clearTaskLines(): void {
101
- this.taskLines = [];
102
- this.#updateTaskDisplay();
103
- }
104
-
105
- /**
106
- * Update the task display (spinner + task lines)
107
- * Uses ora's multiline text support to keep spinner at top with lines below
108
- */
109
- #updateTaskDisplay(): void {
110
- if (!this.spinner) return;
111
-
112
- const currentText =
113
- this.taskCurrentMessage || this.spinner.text.split("\n")[0] || "";
114
-
115
- // If no task lines, show just the spinner message
116
- if (this.taskLines.length === 0) {
117
- this.spinner.text = currentText;
118
- return;
119
- }
120
-
121
- // Build multiline text: spinner message + task lines below
122
- const taskLinesText = this.taskLines
123
- .map((line) => {
124
- const cleanLine = line.startsWith("[keep]") ? line.slice(6) : line;
125
- return chalk.dim(` ${cleanLine}`);
126
- })
127
- .join("\n");
128
-
129
- // Set spinner text to include task lines (ora handles multiline rendering)
130
- this.spinner.text = `${currentText}\n${taskLinesText}`;
131
- }
132
-
133
- /**
134
- * Complete task with optional duration display
135
- * Defaults to showing the original task message with duration
136
- * Task lines marked with keepOnComplete will be preserved, others are cleared
137
- */
138
- done(message?: string, showTime: boolean = true): void {
139
- if (!this.spinner) return;
140
-
141
- let text = message || this.taskOriginalMessage || "done";
142
- if (showTime && this.taskStartTime) {
143
- const durationMs = Date.now() - this.taskStartTime;
144
- const durationText = (() => {
145
- switch (true) {
146
- case durationMs < 1000:
147
- return `${durationMs}ms`;
148
- case durationMs < 2000:
149
- return `${(durationMs / 1000).toFixed(2)}s`;
150
- default:
151
- return `${(durationMs / 1000).toFixed(1)}s`;
152
- }
153
- })();
154
- text += chalk.dim(` (${durationText})`);
155
- }
156
-
157
- // Clear multiline text and set to just completion message
158
- this.spinner.text = text;
159
- this.spinner.succeed();
160
- this.spinner = null;
161
-
162
- // Print kept task lines after completion
163
- const keptLines = this.taskLines.filter((line) =>
164
- line.startsWith("[keep]")
165
- );
166
- for (const line of keptLines) {
167
- console.log(chalk.dim(` ${line.slice(6)}`));
168
- }
169
-
170
- this.taskStartTime = null;
171
- this.taskOriginalMessage = null;
172
- this.taskCurrentMessage = null;
173
- this.taskLines = [];
174
- this.taskMaxLines = 0;
175
- }
176
-
177
- /**
178
- * Show an object as a table of key-value pairs
179
- * Filters out undefined values and applies optional transforms
180
- * Automatically calculates key padding from max key length
181
- */
182
- obj(
183
- obj: Record<string, any>,
184
- keyTransform?: (key: string) => string,
185
- valueTransform?: (value: any, key: string) => string
186
- ): void {
187
- this.#stopTask();
188
-
189
- // Filter out undefined values and apply key transform
190
- const entries: Array<[string, string, any]> = [];
191
- for (const [key, value] of Object.entries(obj)) {
192
- if (value === undefined) continue;
193
- const displayKey = keyTransform ? keyTransform(key) : key;
194
- entries.push([key, displayKey, value]);
195
- }
196
-
197
- // Calculate max key length for padding
198
- const maxKeyLength = Math.max(
199
- ...entries.map(([, displayKey]) => displayKey.length)
200
- );
201
-
202
- // Print each entry
203
- for (const [key, displayKey, value] of entries) {
204
- const displayValue = valueTransform
205
- ? valueTransform(value, key)
206
- : String(value);
207
- const keyFormatted = chalk.dim(displayKey.padEnd(maxKeyLength + 2));
208
- console.log(`${keyFormatted}${displayValue}`);
209
- }
210
- }
211
-
212
- /**
213
- * Display array as bulleted list
214
- * Each item shown with dim bullet and white text
215
- */
216
- arr(items: any[]): void {
217
- this.#stopTask();
218
-
219
- for (const item of items) {
220
- const bullet = chalk.dim("• ");
221
- console.log(`${bullet}${String(item)}`);
222
- }
223
- }
224
-
225
- /**
226
- * Show plain message with optional color
227
- */
228
- log(
229
- message: string,
230
- color?:
231
- | "red"
232
- | "green"
233
- | "yellow"
234
- | "blue"
235
- | "cyan"
236
- | "magenta"
237
- | "gray"
238
- | "dim"
239
- ): void {
240
- this.#stopTask();
241
-
242
- if (color) {
243
- const colorFn = color === "dim" ? chalk.dim : chalk[color];
244
- console.log(colorFn(message));
245
- } else {
246
- console.log(message);
247
- }
248
- }
249
-
250
- /**
251
- * Show success message (green text)
252
- */
253
- success(message: string): void {
254
- this.#stopTask();
255
- console.log(chalk.green(message));
256
- }
257
-
258
- /**
259
- * Show success block (green background label + optional message)
260
- */
261
- successBlock(label: string, message: string = ""): void {
262
- this.#stopTask();
263
- console.log(
264
- `\n${chalk.bgGreen.black(` ${label} `)}${message && ` ${message}`}`
265
- );
266
- }
267
-
268
- /**
269
- * Show success message (green text)
270
- */
271
- spicy(message: string): void {
272
- this.#stopTask();
273
- console.log(chalk.cyan(message));
274
- }
275
-
276
- /**
277
- * Show success block (green background label + optional message)
278
- */
279
- spicyBlock(label: string, message: string = ""): void {
280
- this.#stopTask();
281
- console.log(
282
- `\n${chalk.bgCyan.black(` ${label} `)}${message && ` ${message}`}`
283
- );
284
- }
285
-
286
- /**
287
- * Show message with rainbow gradient
288
- */
289
- rainbow(message: string): void {
290
- this.#stopTask();
291
-
292
- // Rainbow colors in order
293
- const colors = [
294
- chalk.red,
295
- chalk.rgb(255, 165, 0), // orange
296
- chalk.yellow,
297
- chalk.green,
298
- chalk.cyan,
299
- chalk.blue,
300
- chalk.magenta,
301
- ];
302
-
303
- const chars = message.split("");
304
- const colorCount = colors.length;
305
-
306
- // Spread colors across the string
307
- const rainbow = chars
308
- .map((char, i) => {
309
- // Calculate which color to use based on position
310
- const colorIndex = Math.floor((i / chars.length) * colorCount);
311
- const color = colors[Math.min(colorIndex, colorCount - 1)];
312
- return color(char);
313
- })
314
- .join("");
315
-
316
- console.log(rainbow);
317
- }
318
-
319
- /**
320
- * Show info message (dim text)
321
- */
322
- info(message: string): void {
323
- this.#stopTask();
324
- console.log(chalk.dim(message));
325
- }
326
-
327
- /**
328
- * Show info block (grey background label + optional message)
329
- */
330
- infoBlock(label: string, message: string = ""): void {
331
- this.#stopTask();
332
- console.log(
333
- `\n${chalk.bgGrey.white(` ${label} `)}${message && ` ${message}`}`
334
- );
335
- }
336
-
337
- /**
338
- * Show error message (red text) - fails spinner if running
339
- */
340
- error(message: string | Error | unknown): void {
341
- if (this.spinner) {
342
- this.spinner.fail("failed");
343
- this.spinner = null;
344
- this.taskStartTime = null;
345
- this.taskOriginalMessage = null;
346
- this.taskCurrentMessage = null;
347
- }
348
- console.log(
349
- chalk.red(
350
- message instanceof Error
351
- ? message.message
352
- : message instanceof Object
353
- ? JSON.stringify(message)
354
- : String(message)
355
- )
356
- );
357
- }
358
-
359
- /**
360
- * Show error block (red background label + optional message) - fails spinner if running
361
- */
362
- errorBlock(label: string, message: string = ""): void {
363
- if (this.spinner) {
364
- this.spinner.fail("failed");
365
- this.spinner = null;
366
- this.taskStartTime = null;
367
- this.taskOriginalMessage = null;
368
- this.taskCurrentMessage = null;
369
- }
370
- console.log(
371
- `\n${chalk.bgRed.white(` ${label} `)}${message && ` ${message}`}`
372
- );
373
- }
374
-
375
- /**
376
- * Show warning message (yellow text)
377
- */
378
- warn(message: string): void {
379
- this.#stopTask();
380
- console.log(chalk.yellow(message));
381
- }
382
-
383
- /**
384
- * Show warning block (yellow background label + optional message)
385
- */
386
- warnBlock(label: string, message: string = ""): void {
387
- this.#stopTask();
388
- console.log(
389
- `\n${chalk.bgYellow.black(` ${label} `)}${message && ` ${message}`}`
390
- );
391
- }
392
-
393
- /**
394
- * Show detailed error information and exit the program
395
- * Use this when an unexpected/unrecoverable error occurs
396
- * Shows error message and stack trace, then exits
397
- */
398
- crash(error: unknown, exitCode: number = 1): never {
399
- this.#stopTask();
400
-
401
- if (error instanceof Error) {
402
- // Error type and message
403
- console.log(chalk.red(`${error.name}: ${error.message}`));
404
-
405
- // Stack trace
406
- if (error.stack) {
407
- console.log("");
408
- console.log(chalk.dim("Stack trace:"));
409
- const stackLines = error.stack.split("\n").slice(1); // Skip first line (error message)
410
- stackLines.forEach((line) =>
411
- console.log(chalk.dim(` ${line.trim()}`))
412
- );
413
- }
414
- } else {
415
- console.log(chalk.red(String(error)));
416
- }
417
-
418
- process.exit(exitCode);
419
- }
420
-
421
- /**
422
- * Exit with code
423
- */
424
- exit(code?: number): never {
425
- this.#stopTask();
426
- process.exit(code || 0);
427
- }
428
-
429
- /**
430
- * Stop spinner without showing result
431
- */
432
- #stopTask(): void {
433
- if (this.spinner) {
434
- this.spinner.stop();
435
- this.spinner.clear();
436
- this.spinner = null;
437
- }
438
- this.taskStartTime = null;
439
- this.taskOriginalMessage = null;
440
- this.taskCurrentMessage = null;
441
- this.taskLines = [];
442
- this.taskMaxLines = 0;
443
- }
444
- }
445
-
446
- /**
447
- * Global singleton output instance
448
- * Import and use this anywhere in your code
449
- */
450
- export const out = Output.getInstance();
@@ -1,73 +0,0 @@
1
- import "./node-polyfills.js";
2
- import { Repo } from "@automerge/automerge-repo";
3
- import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs";
4
- import * as subductionModule from "@automerge/automerge-subduction";
5
- import {
6
- initSubductionModule,
7
- SubductionStorageBridge,
8
- } from "@automerge/automerge-repo-subduction-bridge";
9
- import * as path from "path";
10
- import * as os from "os";
11
- import { DirectoryConfig } from "../types/index.js";
12
-
13
- const { WebCryptoSigner, Subduction } = subductionModule;
14
-
15
- let subductionModuleInitialized = false;
16
-
17
- function ensureSubductionModuleInit() {
18
- if (!subductionModuleInitialized) {
19
- initSubductionModule(subductionModule);
20
- subductionModuleInitialized = true;
21
- }
22
- }
23
-
24
- /**
25
- * Create an Automerge repository with Subduction-based setup
26
- */
27
- export async function createRepo(
28
- workingDir: string,
29
- config: DirectoryConfig
30
- ): Promise<Repo> {
31
- ensureSubductionModuleInit();
32
-
33
- const syncToolDir = path.join(workingDir, ".pushwork");
34
- const nodeStorage = new NodeFSStorageAdapter(path.join(syncToolDir, "automerge"));
35
-
36
- const signer = await WebCryptoSigner.setup();
37
- const storageBridge = new SubductionStorageBridge(nodeStorage);
38
- const subduction = await Subduction.hydrate(signer, storageBridge);
39
-
40
- // Connect to sync server if sync is enabled
41
- if (config.sync_enabled && config.sync_server) {
42
- await subduction.connectDiscover(
43
- new URL(config.sync_server),
44
- signer
45
- );
46
- }
47
-
48
- return new Repo({ subduction } as any);
49
- }
50
-
51
- /**
52
- * Create an ephemeral Automerge repository for remote reads.
53
- * Uses a temporary directory for storage.
54
- */
55
- export async function createEphemeralRepo(
56
- syncServer: string
57
- ): Promise<Repo> {
58
- ensureSubductionModuleInit();
59
-
60
- const tmpDir = path.join(os.tmpdir(), `pushwork-ephemeral-${Date.now()}`);
61
- const nodeStorage = new NodeFSStorageAdapter(tmpDir);
62
-
63
- const signer = await WebCryptoSigner.setup();
64
- const storageBridge = new SubductionStorageBridge(nodeStorage);
65
- const subduction = await Subduction.hydrate(signer, storageBridge);
66
-
67
- await subduction.connectDiscover(
68
- new URL(syncServer),
69
- signer
70
- );
71
-
72
- return new Repo({ subduction } as any);
73
- }
@@ -1,54 +0,0 @@
1
- /* Based on the Sørensen–Dice coefficient, code from https://github.com/stephenjjbrown/string-similarity-js */
2
-
3
- /**
4
-
5
- * Calculate similarity between two strings
6
-
7
- * @param {string} str1 First string to match
8
- * @param {string} str2 Second string to match
9
- * @param {number} [substringLength=2] Optional. Length of substring to be used in calculating similarity. Default 2.
10
- * @param {boolean} [caseSensitive=false] Optional. Whether you want to consider case in string matching. Default false;
11
-
12
- * @returns Number between 0 and 1, with 0 being a low match score.
13
-
14
- */
15
-
16
- export const stringSimilarity = (
17
- str1: string,
18
- str2: string,
19
- substringLength: number = 2,
20
- caseSensitive: boolean = false
21
- ) => {
22
- if (str1 === str2) return 1;
23
- if (!caseSensitive) {
24
- str1 = str1.toLowerCase();
25
-
26
- str2 = str2.toLowerCase();
27
- }
28
-
29
- if (str1.length < substringLength || str2.length < substringLength) return 0;
30
-
31
- const map = new Map();
32
-
33
- for (let i = 0; i < str1.length - (substringLength - 1); i++) {
34
- const substr1 = str1.substring(i, i + substringLength);
35
-
36
- map.set(substr1, map.has(substr1) ? map.get(substr1) + 1 : 1);
37
- }
38
-
39
- let match = 0;
40
-
41
- for (let j = 0; j < str2.length - (substringLength - 1); j++) {
42
- const substr2 = str2.substring(j, j + substringLength);
43
-
44
- const count = map.has(substr2) ? map.get(substr2) : 0;
45
-
46
- if (count > 0) {
47
- map.set(substr2, count - 1);
48
-
49
- match++;
50
- }
51
- }
52
-
53
- return (match * 2) / (str1.length + str2.length - (substringLength - 1) * 2);
54
- };
@@ -1,101 +0,0 @@
1
- import * as A from "@automerge/automerge"
2
- import * as diffLib from "diff"
3
-
4
- /**
5
- * Read content from an Automerge document, normalizing legacy ImmutableString
6
- * values to plain strings for backwards compatibility.
7
- *
8
- * Old documents may store text as ImmutableString. This helper ensures callers
9
- * always get back `string | Uint8Array | null`.
10
- */
11
- export function readDocContent(content: unknown): string | Uint8Array | null {
12
- if (content == null) return null
13
- if (typeof content === "string") return content
14
- if (content instanceof Uint8Array) return content
15
- // Legacy ImmutableString — convert to plain string
16
- if (A.isImmutableString(content)) return content.toString()
17
- return null
18
- }
19
-
20
- /**
21
- * Update text content on an Automerge document property inside a change
22
- * callback.
23
- *
24
- * If the existing value is already a collaborative text string, we diff and
25
- * splice for minimal CRDT operations. If the existing value is a legacy
26
- * ImmutableString we can't splice into it, so we assign the whole string
27
- * which converts the field to a collaborative text CRDT going forward.
28
- *
29
- * @param doc - The mutable Automerge document (inside a change callback)
30
- * @param path - Property path to the text field, e.g. ["content"]
31
- * @param newContent - The desired new text value
32
- */
33
- export function updateTextContent(
34
- doc: any,
35
- path: A.Prop[],
36
- newContent: string
37
- ): void {
38
- const target = path.reduce((obj: any, key) => obj?.[key], doc)
39
-
40
- if (typeof target === "string") {
41
- // Already a collaborative text string — diff and splice
42
- spliceText(doc, path, target, newContent)
43
- } else {
44
- // Legacy ImmutableString, undefined, or other — assign directly.
45
- // This converts the field to a collaborative text CRDT.
46
- let obj: any = doc
47
- for (let i = 0; i < path.length - 1; i++) {
48
- obj = obj[path[i]]
49
- }
50
- obj[path[path.length - 1]] = newContent
51
- }
52
- }
53
-
54
- /**
55
- * Apply a text diff between oldContent and newContent as Automerge splice
56
- * operations on the given document property path.
57
- *
58
- * This preserves the collaborative text CRDT structure by making minimal
59
- * character-level edits rather than replacing the entire string.
60
- *
61
- * @param doc - The Automerge document (inside a change callback)
62
- * @param path - The property path to the text field, e.g. ["content"]
63
- * @param oldContent - The previous text content
64
- * @param newContent - The desired new text content
65
- */
66
- export function spliceText(
67
- doc: any,
68
- path: A.Prop[],
69
- oldContent: string,
70
- newContent: string
71
- ): void {
72
- if (oldContent === newContent) return
73
-
74
- // Fast path: if old is empty, just insert everything
75
- if (oldContent === "") {
76
- A.splice(doc, path, 0, 0, newContent)
77
- return
78
- }
79
-
80
- // Fast path: if new is empty, just delete everything
81
- if (newContent === "") {
82
- A.splice(doc, path, 0, oldContent.length)
83
- return
84
- }
85
-
86
- const changes = diffLib.diffChars(oldContent, newContent)
87
-
88
- let pos = 0
89
- for (const part of changes) {
90
- if (part.removed) {
91
- A.splice(doc, path, pos, part.value.length)
92
- // Don't advance pos — text shifted left after deletion
93
- } else if (part.added) {
94
- A.splice(doc, path, pos, 0, part.value)
95
- pos += part.value.length
96
- } else {
97
- // Unchanged text — just advance the cursor
98
- pos += part.value.length
99
- }
100
- }
101
- }