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
package/src/commands.ts DELETED
@@ -1,1134 +0,0 @@
1
- import * as path from "path";
2
- import * as fs from "fs/promises";
3
- import * as fsSync from "fs";
4
- import { Repo, AutomergeUrl } from "@automerge/automerge-repo";
5
- import * as diffLib from "diff";
6
- import { spawn } from "child_process";
7
- import {
8
- CloneOptions,
9
- SyncOptions,
10
- DiffOptions,
11
- LogOptions,
12
- CheckoutOptions,
13
- InitOptions,
14
- ConfigOptions,
15
- StatusOptions,
16
- WatchOptions,
17
- ReadOptions,
18
- DirectoryConfig,
19
- DirectoryDocument,
20
- FileDocument,
21
- CommandOptions,
22
- } from "./types/index.js";
23
- import { SyncEngine } from "./core/index.js";
24
- import { pathExists, ensureDirectoryExists, formatRelativePath } from "./utils/index.js";
25
- import { ConfigManager } from "./core/config.js";
26
- import { createRepo, createEphemeralRepo } from "./utils/repo-factory.js";
27
- import { out } from "./utils/output.js";
28
- import { waitForSync } from "./utils/network-sync.js";
29
- import { readDocContent } from "./utils/text-diff.js";
30
- import { DEFAULT_SYNC_SERVER } from "./types/config.js";
31
- import chalk from "chalk";
32
-
33
- /**
34
- * Shared context that commands can use
35
- */
36
- interface CommandContext {
37
- repo: Repo;
38
- syncEngine: SyncEngine;
39
- config: DirectoryConfig;
40
- workingDir: string;
41
- }
42
-
43
- /**
44
- * Initialize repository directory structure and configuration
45
- * Shared logic for init and clone commands
46
- */
47
- async function initializeRepository(
48
- resolvedPath: string,
49
- overrides: Partial<DirectoryConfig>
50
- ): Promise<{ config: DirectoryConfig; repo: Repo; syncEngine: SyncEngine }> {
51
- // Create .pushwork directory structure
52
- const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
53
- await ensureDirectoryExists(syncToolDir);
54
- await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
55
-
56
- // Create configuration with overrides
57
- const configManager = new ConfigManager(resolvedPath);
58
- const config = await configManager.initializeWithOverrides(overrides);
59
-
60
- // Create repository and sync engine
61
- const repo = await createRepo(resolvedPath, config);
62
- const syncEngine = new SyncEngine(repo, resolvedPath, config);
63
-
64
- return { config, repo, syncEngine };
65
- }
66
-
67
- /**
68
- * Shared pre-action that ensures repository and sync engine are properly initialized
69
- * This function always works, with or without network connectivity
70
- */
71
- async function setupCommandContext(
72
- workingDir: string = process.cwd(),
73
- options?: { syncEnabled?: boolean; forceDefaults?: boolean }
74
- ): Promise<CommandContext> {
75
- const resolvedPath = path.resolve(workingDir);
76
-
77
- // Check if initialized
78
- const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
79
- if (!(await pathExists(syncToolDir))) {
80
- throw new Error(
81
- 'Directory not initialized for sync. Run "pushwork init" first.'
82
- );
83
- }
84
-
85
- // Load configuration
86
- const configManager = new ConfigManager(resolvedPath);
87
- let config: DirectoryConfig;
88
-
89
- if (options?.forceDefaults) {
90
- // Force mode: use defaults, only preserving root_directory_url from local config
91
- const localConfig = await configManager.load();
92
- config = configManager.getDefaultDirectoryConfig();
93
- if (localConfig?.root_directory_url) {
94
- config.root_directory_url = localConfig.root_directory_url;
95
- }
96
- } else {
97
- config = await configManager.getMerged();
98
- }
99
-
100
- // Override sync_enabled if explicitly specified (e.g., for local-only operations)
101
- if (options?.syncEnabled !== undefined) {
102
- config = { ...config, sync_enabled: options.syncEnabled };
103
- }
104
-
105
- // Create repo with config
106
- const repo = await createRepo(resolvedPath, config);
107
-
108
- // Create sync engine
109
- const syncEngine = new SyncEngine(repo, resolvedPath, config);
110
-
111
- return {
112
- repo,
113
- syncEngine,
114
- config,
115
- workingDir: resolvedPath,
116
- };
117
- }
118
- /**
119
- * Safely shutdown a repository with proper error handling
120
- */
121
- async function safeRepoShutdown(repo: Repo): Promise<void> {
122
- // Handle uncaught WebSocket errors that occur during shutdown
123
- const uncaughtErrorHandler = (err: Error) => {
124
- if (err.message.includes("WebSocket")) {
125
- // Silently suppress WebSocket errors during shutdown
126
- return;
127
- }
128
- // Re-throw non-WebSocket errors
129
- throw err;
130
- };
131
-
132
- // Add the error handler before shutdown
133
- process.on("uncaughtException", uncaughtErrorHandler);
134
-
135
- try {
136
- await repo.shutdown();
137
- } catch (shutdownError) {
138
- // WebSocket errors during shutdown are common and non-critical
139
- // Silently ignore them - they don't affect data integrity
140
- const errorMessage =
141
- shutdownError instanceof Error
142
- ? shutdownError.message
143
- : String(shutdownError);
144
-
145
- // Ignore WebSocket-related errors entirely
146
- if (errorMessage.includes("WebSocket")) {
147
- // Silently ignore WebSocket shutdown errors
148
- return;
149
- }
150
- } finally {
151
- process.off("uncaughtException", uncaughtErrorHandler);
152
- }
153
- }
154
-
155
- /**
156
- * Initialize sync in a directory
157
- */
158
- export async function init(
159
- targetPath: string,
160
- options: InitOptions = {}
161
- ): Promise<void> {
162
- const resolvedPath = path.resolve(targetPath);
163
-
164
- out.task(`Initializing`);
165
-
166
- await ensureDirectoryExists(resolvedPath);
167
-
168
- // Check if already initialized
169
- const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
170
- if (await pathExists(syncToolDir)) {
171
- out.error("Directory already initialized for sync");
172
- out.exit(1);
173
- }
174
-
175
- // Initialize repository with optional CLI overrides
176
- out.update("Setting up repository");
177
- const { repo, syncEngine, config } = await initializeRepository(resolvedPath, {
178
- sync_server: options.syncServer,
179
- });
180
-
181
- // Create new root directory document
182
- out.update("Creating root directory");
183
- const dirName = path.basename(resolvedPath);
184
- const rootDoc: DirectoryDocument = {
185
- "@patchwork": { type: "folder" },
186
- name: dirName,
187
- title: dirName,
188
- docs: [],
189
- };
190
- const rootHandle = repo.create(rootDoc);
191
-
192
- // Set root directory URL in snapshot
193
- await syncEngine.setRootDirectoryUrl(rootHandle.url);
194
-
195
- // Wait for root document to sync to server if sync is enabled
196
- // This ensures the document is uploaded before we exit
197
- // waitForSync() verifies the server has the document by comparing local and remote heads
198
- if (config.sync_enabled && config.sync_server) {
199
- out.update("Syncing to server");
200
- const { failed } = await waitForSync([rootHandle]);
201
- if (failed.length > 0) {
202
- out.taskLine("Root document failed to sync to server", true);
203
- // Continue anyway - the document is created locally and will sync later
204
- }
205
- }
206
-
207
- // Run initial sync to capture existing files
208
- out.update("Running initial sync");
209
- const result = await syncEngine.sync();
210
-
211
- out.update("Writing to disk");
212
- await safeRepoShutdown(repo);
213
-
214
- out.done("Initialized");
215
- out.successBlock("INITIALIZED", rootHandle.url);
216
- if (result.filesChanged > 0) {
217
- out.info(`Synced ${result.filesChanged} ${plural("file", result.filesChanged)}`);
218
- }
219
-
220
- process.exit();
221
- }
222
-
223
- /**
224
- * Run bidirectional sync
225
- */
226
- export async function sync(
227
- targetPath = ".",
228
- options: SyncOptions
229
- ): Promise<void> {
230
- out.task(
231
- options.nuclear
232
- ? "Nuclear syncing"
233
- : options.gentle
234
- ? "Gentle syncing"
235
- : "Syncing"
236
- );
237
-
238
- const { repo, syncEngine } = await setupCommandContext(targetPath, {
239
- forceDefaults: !options.gentle,
240
- });
241
-
242
- if (options.nuclear) {
243
- await syncEngine.nuclearReset();
244
- }
245
-
246
- if (options.dryRun) {
247
- out.update("Analyzing changes");
248
- const preview = await syncEngine.previewChanges();
249
-
250
- if (preview.changes.length === 0 && preview.moves.length === 0) {
251
- out.done("Already synced");
252
- return;
253
- }
254
-
255
- out.done();
256
- out.infoBlock("CHANGES");
257
- out.obj({
258
- Changes: preview.changes.length.toString(),
259
- Moves:
260
- preview.moves.length > 0 ? preview.moves.length.toString() : undefined,
261
- });
262
-
263
- out.log("");
264
- out.log("Files:");
265
- for (const change of preview.changes.slice(0, 10)) {
266
- const prefix =
267
- change.changeType === "local_only"
268
- ? "[local] "
269
- : change.changeType === "remote_only"
270
- ? "[remote] "
271
- : "[conflict]";
272
- out.log(` ${prefix} ${change.path}`);
273
- }
274
- if (preview.changes.length > 10) {
275
- out.log(` ... and ${preview.changes.length - 10} more`);
276
- }
277
-
278
- if (preview.moves.length > 0) {
279
- out.log("");
280
- out.log("Moves:");
281
- for (const move of preview.moves.slice(0, 5)) {
282
- out.log(` ${move.fromPath} → ${move.toPath}`);
283
- }
284
- if (preview.moves.length > 5) {
285
- out.log(` ... and ${preview.moves.length - 5} more`);
286
- }
287
- }
288
-
289
- out.log("");
290
- out.log("Run without --dry-run to apply these changes");
291
- } else {
292
- const result = await syncEngine.sync();
293
-
294
- out.taskLine("Writing to disk");
295
- await safeRepoShutdown(repo);
296
-
297
- if (result.success) {
298
- out.done("Synced");
299
- if (result.filesChanged === 0 && result.directoriesChanged === 0) {
300
- } else {
301
- out.successBlock(
302
- "SYNCED",
303
- `${result.filesChanged} ${plural("file", result.filesChanged)}`
304
- );
305
- }
306
-
307
- if (result.warnings.length > 0) {
308
- out.log("");
309
- out.warnBlock("WARNINGS", `${result.warnings.length} warnings`);
310
- for (const warning of result.warnings.slice(0, 5)) {
311
- out.log(` ${warning}`);
312
- }
313
- if (result.warnings.length > 5) {
314
- out.log(` ... and ${result.warnings.length - 5} more`);
315
- }
316
- }
317
- } else {
318
- out.done("partial", false);
319
- out.warnBlock(
320
- "PARTIAL",
321
- `${result.filesChanged} updated, ${result.errors.length} errors`
322
- );
323
- out.obj({
324
- Files: result.filesChanged,
325
- Errors: result.errors.length,
326
- });
327
-
328
- result.errors
329
- .slice(0, 5)
330
- .forEach((error) => out.error(`${error.path}: ${error.error.message}`));
331
- if (result.errors.length > 5) {
332
- out.warn(`... and ${result.errors.length - 5} more errors`);
333
- }
334
- }
335
- }
336
-
337
- process.exit();
338
- }
339
-
340
- /**
341
- * Show differences between local and remote
342
- */
343
- export async function diff(
344
- targetPath = ".",
345
- options: DiffOptions
346
- ): Promise<void> {
347
- out.task("Analyzing changes");
348
-
349
- const { repo, syncEngine } = await setupCommandContext(targetPath, { syncEnabled: false });
350
- const preview = await syncEngine.previewChanges();
351
-
352
- out.done();
353
-
354
- if (options.nameOnly) {
355
- for (const change of preview.changes) {
356
- out.log(change.path);
357
- }
358
- return;
359
- }
360
-
361
- if (preview.changes.length === 0) {
362
- out.success("No changes detected");
363
- await safeRepoShutdown(repo);
364
- out.exit();
365
- return;
366
- }
367
-
368
- out.warn(`${preview.changes.length} changes detected`);
369
-
370
- for (const change of preview.changes) {
371
- const prefix =
372
- change.changeType === "local_only"
373
- ? "[local] "
374
- : change.changeType === "remote_only"
375
- ? "[remote] "
376
- : "[conflict]";
377
-
378
- try {
379
- // Get old content (from snapshot/remote)
380
- const oldContent = change.remoteContent || "";
381
- // Get new content (current local)
382
- const newContent = change.localContent || "";
383
-
384
- // Convert binary content to string representation if needed
385
- const oldText =
386
- typeof oldContent === "string"
387
- ? oldContent
388
- : `<binary content: ${oldContent.length} bytes>`;
389
- const newText =
390
- typeof newContent === "string"
391
- ? newContent
392
- : `<binary content: ${newContent.length} bytes>`;
393
-
394
- // Generate unified diff
395
- const diffResult = diffLib.createPatch(
396
- change.path,
397
- oldText,
398
- newText,
399
- "previous",
400
- "current"
401
- );
402
-
403
- // Skip the header lines and process the diff
404
- const lines = diffResult.split("\n").slice(4); // Skip index, ===, ---, +++ lines
405
-
406
- if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
407
- out.log(`${prefix}${change.path} (content identical)`, "cyan");
408
- continue;
409
- }
410
-
411
- // Extract first hunk header and show inline with path
412
- let firstHunk = "";
413
- let diffLines = lines;
414
- if (lines[0]?.startsWith("@@")) {
415
- firstHunk = ` ${lines[0]}`;
416
- diffLines = lines.slice(1);
417
- }
418
-
419
- out.log(`${prefix}${change.path}${firstHunk}`, "cyan");
420
-
421
- for (const line of diffLines) {
422
- if (line.startsWith("@@")) {
423
- // Additional hunk headers
424
- out.log(line, "dim");
425
- } else if (line.startsWith("+")) {
426
- // Added line
427
- out.log(line, "green");
428
- } else if (line.startsWith("-")) {
429
- // Removed line
430
- out.log(line, "red");
431
- } else if (line.startsWith(" ") || line === "") {
432
- // Context line or empty
433
- out.log(line, "dim");
434
- }
435
- }
436
- } catch (error) {
437
- out.log(`${prefix}${change.path} (diff error: ${error})`, "cyan");
438
- }
439
- }
440
-
441
- await safeRepoShutdown(repo);
442
- }
443
-
444
- /**
445
- * Show sync status
446
- */
447
- export async function status(
448
- targetPath: string = ".",
449
- options: StatusOptions = {}
450
- ): Promise<void> {
451
- const { repo, syncEngine, config } = await setupCommandContext(
452
- targetPath,
453
- { syncEnabled: false }
454
- );
455
- const syncStatus = await syncEngine.getStatus();
456
-
457
- out.infoBlock("STATUS");
458
-
459
- const statusInfo: Record<string, any> = {};
460
- const fileCount = syncStatus.snapshot?.files.size || 0;
461
-
462
- statusInfo["URL"] = syncStatus.snapshot?.rootDirectoryUrl;
463
- statusInfo["Files"] = syncStatus.snapshot
464
- ? `${fileCount} tracked`
465
- : undefined;
466
- statusInfo["Sync"] = config?.sync_server;
467
-
468
- // Add more detailed info in verbose mode
469
- if (options.verbose && syncStatus.snapshot?.rootDirectoryUrl) {
470
- try {
471
- const rootHandle = await repo.find<DirectoryDocument>(
472
- syncStatus.snapshot.rootDirectoryUrl
473
- );
474
- const rootDoc = await rootHandle.doc();
475
-
476
- if (rootDoc) {
477
- statusInfo["Entries"] = rootDoc.docs.length;
478
- statusInfo["Directories"] = syncStatus.snapshot.directories.size;
479
- if (rootDoc.lastSyncAt) {
480
- const lastSyncDate = new Date(rootDoc.lastSyncAt);
481
- statusInfo["Last sync"] = lastSyncDate.toISOString();
482
- }
483
- }
484
- } catch (error) {
485
- out.warn(`Warning: Could not load detailed info: ${error}`);
486
- }
487
- }
488
-
489
- statusInfo["Changes"] = syncStatus.hasChanges
490
- ? `${syncStatus.changeCount} pending`
491
- : undefined;
492
- statusInfo["Status"] = !syncStatus.hasChanges ? "up to date" : undefined;
493
-
494
- out.obj(statusInfo);
495
-
496
- // Show verbose details if requested
497
- if (options.verbose && syncStatus.snapshot?.rootDirectoryUrl) {
498
- const rootHandle = await repo.find<DirectoryDocument>(
499
- syncStatus.snapshot.rootDirectoryUrl
500
- );
501
- const rootDoc = await rootHandle.doc();
502
-
503
- if (rootDoc) {
504
- out.infoBlock("HEADS");
505
- out.arr(rootHandle.heads());
506
-
507
- if (syncStatus.snapshot && syncStatus.snapshot.files.size > 0) {
508
- out.infoBlock("TRACKED FILES");
509
- const filesObj: Record<string, string> = {};
510
- syncStatus.snapshot.files.forEach((entry, filePath) => {
511
- filesObj[filePath] = entry.url;
512
- });
513
- out.obj(filesObj);
514
- }
515
- }
516
- }
517
-
518
- if (syncStatus.hasChanges && !options.verbose) {
519
- out.info("Run 'pushwork diff' to see changes");
520
- }
521
-
522
- await safeRepoShutdown(repo);
523
- }
524
-
525
- /**
526
- * Show sync history
527
- */
528
- export async function log(
529
- targetPath = ".",
530
- _options: LogOptions
531
- ): Promise<void> {
532
- const { repo: logRepo, workingDir } = await setupCommandContext(
533
- targetPath,
534
- { syncEnabled: false }
535
- );
536
-
537
- // TODO: Implement history tracking
538
- const snapshotPath = path.join(
539
- workingDir,
540
- ConfigManager.CONFIG_DIR,
541
- "snapshot.json"
542
- );
543
- if (await pathExists(snapshotPath)) {
544
- const stats = await fs.stat(snapshotPath);
545
- out.infoBlock("HISTORY", "Sync history (stub)");
546
- out.obj({ "Last sync": stats.mtime.toISOString() });
547
- } else {
548
- out.info("No sync history found");
549
- }
550
-
551
- await safeRepoShutdown(logRepo);
552
- }
553
-
554
- /**
555
- * Checkout/restore from previous sync
556
- */
557
- export async function checkout(
558
- syncId: string,
559
- targetPath = ".",
560
- _options: CheckoutOptions
561
- ): Promise<void> {
562
- const { workingDir } = await setupCommandContext(targetPath);
563
-
564
- // TODO: Implement checkout functionality
565
- out.warnBlock("NOT IMPLEMENTED", "Checkout not yet implemented");
566
- out.obj({
567
- "Sync ID": syncId,
568
- Path: workingDir,
569
- });
570
- }
571
-
572
- /**
573
- * Clone an existing synced directory from an AutomergeUrl
574
- */
575
- export async function clone(
576
- rootUrl: string,
577
- targetPath: string,
578
- options: CloneOptions
579
- ): Promise<void> {
580
- // Validate that rootUrl is actually an Automerge URL
581
- if (!rootUrl.startsWith("automerge:")) {
582
- out.error(
583
- `Invalid Automerge URL: ${rootUrl}\n` +
584
- `Expected format: automerge:XXXXX\n` +
585
- `Usage: pushwork clone <automerge-url> <path>`
586
- );
587
- out.exit(1);
588
- }
589
-
590
-
591
- const resolvedPath = path.resolve(targetPath);
592
-
593
- out.task(`Cloning ${rootUrl}`);
594
-
595
- // Check if directory exists and handle --force
596
- if (await pathExists(resolvedPath)) {
597
- const files = await fs.readdir(resolvedPath);
598
- if (files.length > 0 && !options.force) {
599
- out.error("Target directory is not empty. Use --force to overwrite");
600
- out.exit(1);
601
- }
602
- } else {
603
- await ensureDirectoryExists(resolvedPath);
604
- }
605
-
606
- // Check if already initialized
607
- const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
608
- if (await pathExists(syncToolDir)) {
609
- if (!options.force) {
610
- out.error("Directory already initialized. Use --force to overwrite");
611
- out.exit(1);
612
- }
613
- await fs.rm(syncToolDir, { recursive: true, force: true });
614
- }
615
-
616
- // Initialize repository with optional CLI overrides
617
- out.update("Setting up repository");
618
- const { config, repo, syncEngine } = await initializeRepository(
619
- resolvedPath,
620
- {
621
- sync_server: options.syncServer,
622
- }
623
- );
624
-
625
- // Connect to existing root directory and download files
626
- out.update("Downloading files");
627
- await syncEngine.setRootDirectoryUrl(rootUrl as AutomergeUrl);
628
- const result = await syncEngine.sync();
629
-
630
- out.update("Writing to disk");
631
- await safeRepoShutdown(repo);
632
-
633
- out.done();
634
-
635
- out.obj({
636
- Path: resolvedPath,
637
- Files: `${result.filesChanged} downloaded`,
638
- Sync: config.sync_server,
639
- });
640
- out.successBlock("CLONED", rootUrl);
641
- process.exit();
642
- }
643
-
644
- /**
645
- * Get the root URL for the current pushwork repository
646
- */
647
- export async function url(targetPath: string = "."): Promise<void> {
648
- const resolvedPath = path.resolve(targetPath);
649
- const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
650
-
651
- if (!(await pathExists(syncToolDir))) {
652
- out.error("Directory not initialized for sync");
653
- out.exit(1);
654
- }
655
-
656
- const snapshotPath = path.join(syncToolDir, "snapshot.json");
657
- if (!(await pathExists(snapshotPath))) {
658
- out.error("No snapshot found");
659
- out.exit(1);
660
- }
661
-
662
- const snapshotData = await fs.readFile(snapshotPath, "utf-8");
663
- const snapshot = JSON.parse(snapshotData);
664
-
665
- if (snapshot.rootDirectoryUrl) {
666
- // Output just the URL for easy use in scripts
667
- out.log(snapshot.rootDirectoryUrl);
668
- } else {
669
- out.error("No root URL found in snapshot");
670
- out.exit(1);
671
- }
672
- }
673
-
674
- /**
675
- * Remove local pushwork data and log URL for recovery
676
- */
677
- export async function rm(targetPath: string = "."): Promise<void> {
678
- const resolvedPath = path.resolve(targetPath);
679
- const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
680
-
681
- if (!(await pathExists(syncToolDir))) {
682
- out.error("Directory not initialized for sync");
683
- out.exit(1);
684
- }
685
-
686
- // Read the URL before deletion for recovery
687
- let recoveryUrl = "";
688
- const snapshotPath = path.join(syncToolDir, "snapshot.json");
689
- if (await pathExists(snapshotPath)) {
690
- try {
691
- const snapshotData = await fs.readFile(snapshotPath, "utf-8");
692
- const snapshot = JSON.parse(snapshotData);
693
- recoveryUrl = snapshot.rootDirectoryUrl || null;
694
- } catch (error) {
695
- out.error(`Remove failed: ${error}`);
696
- out.exit(1);
697
- return;
698
- }
699
- }
700
-
701
- out.task("Removing local pushwork data");
702
- await fs.rm(syncToolDir, { recursive: true, force: true });
703
- out.done();
704
-
705
- out.warnBlock("REMOVED", recoveryUrl);
706
- process.exit();
707
- }
708
-
709
- export async function commit(
710
- targetPath: string,
711
- _options: CommandOptions = {}
712
- ): Promise<void> {
713
- out.task("Committing local changes");
714
-
715
- const { repo, syncEngine } = await setupCommandContext(targetPath, { syncEnabled: false });
716
-
717
- const result = await syncEngine.commitLocal();
718
- await safeRepoShutdown(repo);
719
-
720
- out.done();
721
-
722
- if (result.errors.length > 0) {
723
- out.errorBlock("ERROR", `${result.errors.length} errors`);
724
- result.errors.forEach((error) => out.error(error));
725
- out.exit(1);
726
- }
727
-
728
- out.successBlock("COMMITTED", `${result.filesChanged} files`);
729
- out.obj({
730
- Files: result.filesChanged,
731
- Directories: result.directoriesChanged,
732
- });
733
-
734
- if (result.warnings.length > 0) {
735
- result.warnings.forEach((warning) => out.warn(warning));
736
- }
737
- process.exit();
738
- }
739
-
740
- /**
741
- * List tracked files
742
- */
743
- export async function ls(
744
- targetPath: string = ".",
745
- options: CommandOptions = {}
746
- ): Promise<void> {
747
- const { repo, syncEngine } = await setupCommandContext(targetPath, { syncEnabled: false });
748
- const syncStatus = await syncEngine.getStatus();
749
-
750
- if (!syncStatus.snapshot) {
751
- out.error("No snapshot found");
752
- await safeRepoShutdown(repo);
753
- out.exit(1);
754
- return;
755
- }
756
-
757
- const files = Array.from(syncStatus.snapshot.files.entries()).sort(
758
- ([pathA], [pathB]) => pathA.localeCompare(pathB)
759
- );
760
-
761
- if (files.length === 0) {
762
- out.info("No tracked files");
763
- await safeRepoShutdown(repo);
764
- return;
765
- }
766
-
767
- if (options.verbose) {
768
- // Long format with URLs
769
- for (const [filePath, entry] of files) {
770
- const url = entry?.url || "unknown";
771
- out.log(`${filePath} -> ${url}`);
772
- }
773
- } else {
774
- // Simple list
775
- for (const [filePath] of files) {
776
- out.log(filePath);
777
- }
778
- }
779
-
780
- await safeRepoShutdown(repo);
781
- }
782
-
783
- /**
784
- * View or edit configuration
785
- */
786
- export async function config(
787
- targetPath: string = ".",
788
- options: ConfigOptions = {}
789
- ): Promise<void> {
790
- const resolvedPath = path.resolve(targetPath);
791
- const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
792
-
793
- if (!(await pathExists(syncToolDir))) {
794
- out.error("Directory not initialized for sync");
795
- out.exit(1);
796
- }
797
-
798
- const configManager = new ConfigManager(resolvedPath);
799
- const config = await configManager.getMerged();
800
-
801
- if (options.list) {
802
- // List all configuration
803
- out.infoBlock("CONFIGURATION", "Full configuration");
804
- out.log(JSON.stringify(config, null, 2));
805
- } else if (options.get) {
806
- // Get specific config value
807
- const keys = options.get.split(".");
808
- let value: any = config;
809
- for (const key of keys) {
810
- value = value?.[key];
811
- }
812
- if (value !== undefined) {
813
- out.log(
814
- typeof value === "object" ? JSON.stringify(value, null, 2) : value
815
- );
816
- } else {
817
- out.error(`Config key not found: ${options.get}`);
818
- out.exit(1);
819
- }
820
- } else {
821
- // Show basic config info
822
- out.infoBlock("CONFIGURATION");
823
- out.obj({
824
- "Sync server": config.sync_server || "default",
825
- "Sync enabled": config.sync_enabled ? "yes" : "no",
826
- Exclusions: config.exclude_patterns?.length,
827
- });
828
- out.log("");
829
- out.log("Use --list to see full configuration");
830
- }
831
- }
832
-
833
- /**
834
- * Watch a directory and sync after build script completes
835
- */
836
- export async function watch(
837
- targetPath: string = ".",
838
- options: WatchOptions = {}
839
- ): Promise<void> {
840
- const script = options.script || "pnpm build";
841
- const watchDir = options.watchDir || "src"; // Default to watching 'src' directory
842
- const verbose = options.verbose || false;
843
- const { repo, syncEngine, workingDir } = await setupCommandContext(
844
- targetPath
845
- );
846
-
847
- const absoluteWatchDir = path.resolve(workingDir, watchDir);
848
-
849
- // Check if watch directory exists
850
- if (!(await pathExists(absoluteWatchDir))) {
851
- out.error(`Watch directory does not exist: ${watchDir}`);
852
- await safeRepoShutdown(repo);
853
- out.exit(1);
854
- return;
855
- }
856
-
857
- out.spicyBlock(
858
- "WATCHING",
859
- `${chalk.underline(formatRelativePath(watchDir))} for changes...`
860
- );
861
- out.info(`Build script: ${script}`);
862
- out.info(`Working directory: ${workingDir}`);
863
-
864
- let isProcessing = false;
865
- let pendingChange = false;
866
-
867
- // Function to run build and sync
868
- const runBuildAndSync = async () => {
869
- if (isProcessing) {
870
- pendingChange = true;
871
- return;
872
- }
873
-
874
- isProcessing = true;
875
- pendingChange = false;
876
-
877
- try {
878
- out.spicy(`[${new Date().toLocaleTimeString()}] Changes detected...`);
879
- // Run build script
880
- const buildResult = await runScript(script, workingDir, verbose);
881
-
882
- if (!buildResult.success) {
883
- out.warn("Build script failed");
884
- if (buildResult.output) {
885
- out.log("");
886
- out.log(buildResult.output);
887
- }
888
- isProcessing = false;
889
- if (pendingChange) {
890
- setImmediate(() => runBuildAndSync());
891
- }
892
- return;
893
- }
894
-
895
- out.info("Build completed...");
896
-
897
- // Run sync
898
- out.task("Syncing");
899
- const result = await syncEngine.sync();
900
-
901
- if (result.success) {
902
- if (result.filesChanged === 0 && result.directoriesChanged === 0) {
903
- out.done("Already synced");
904
- } else {
905
- out.done(
906
- `Synced ${result.filesChanged} ${plural(
907
- "file",
908
- result.filesChanged
909
- )}`
910
- );
911
- }
912
- } else {
913
- out.warn(
914
- `⚠ Partial sync: ${result.filesChanged} updated, ${result.errors.length} errors`
915
- );
916
- result.errors
917
- .slice(0, 3)
918
- .forEach((error) =>
919
- out.error(` ${error.path}: ${error.error.message}`)
920
- );
921
- if (result.errors.length > 3) {
922
- out.warn(` ... and ${result.errors.length - 3} more errors`);
923
- }
924
- }
925
-
926
- if (result.warnings.length > 0) {
927
- result.warnings
928
- .slice(0, 3)
929
- .forEach((warning) => out.warn(` ${warning}`));
930
- if (result.warnings.length > 3) {
931
- out.warn(` ... and ${result.warnings.length - 3} more warnings`);
932
- }
933
- }
934
- } catch (error) {
935
- out.error(`Error during build/sync: ${error}`);
936
- } finally {
937
- isProcessing = false;
938
-
939
- // If changes occurred while we were processing, run again
940
- if (pendingChange) {
941
- setImmediate(() => runBuildAndSync());
942
- }
943
- }
944
- };
945
-
946
- // Set up file watcher - watches everything in the specified directory
947
- const watcher = fsSync.watch(
948
- absoluteWatchDir,
949
- { recursive: true },
950
- (_eventType, filename) => {
951
- if (filename) {
952
- runBuildAndSync();
953
- }
954
- }
955
- );
956
-
957
- // Handle graceful shutdown
958
- const shutdown = async () => {
959
- out.log("");
960
- out.info("Shutting down...");
961
- watcher.close();
962
- await safeRepoShutdown(repo);
963
- out.rainbow("Goodbye!");
964
- process.exit(0);
965
- };
966
-
967
- process.on("SIGINT", shutdown);
968
- process.on("SIGTERM", shutdown);
969
-
970
- // Run initial build and sync
971
- await runBuildAndSync();
972
-
973
- // Keep process alive
974
- await new Promise(() => {}); // Never resolves, keeps watching
975
- }
976
-
977
- /**
978
- * Run a shell script and wait for completion
979
- */
980
- async function runScript(
981
- script: string,
982
- cwd: string,
983
- verbose: boolean
984
- ): Promise<{ success: boolean; output?: string }> {
985
- return new Promise((resolve) => {
986
- const [command, ...args] = script.split(" ");
987
- const child = spawn(command, args, {
988
- cwd,
989
- stdio: verbose ? "inherit" : "pipe", // Show output directly if verbose, otherwise capture
990
- shell: true,
991
- });
992
-
993
- let output = "";
994
-
995
- // Capture output if not verbose (so we can show it on error)
996
- if (!verbose) {
997
- child.stdout?.on("data", (data) => {
998
- output += data.toString();
999
- });
1000
- child.stderr?.on("data", (data) => {
1001
- output += data.toString();
1002
- });
1003
- }
1004
-
1005
- child.on("close", (code) => {
1006
- resolve({
1007
- success: code === 0,
1008
- output: !verbose ? output : undefined,
1009
- });
1010
- });
1011
-
1012
- child.on("error", (error) => {
1013
- out.error(`Failed to run script: ${error.message}`);
1014
- resolve({
1015
- success: false,
1016
- output: !verbose ? output : undefined,
1017
- });
1018
- });
1019
- });
1020
- }
1021
-
1022
- /**
1023
- * Set root directory URL for an existing or new pushwork directory
1024
- */
1025
- export async function root(
1026
- rootUrl: string,
1027
- targetPath: string = ".",
1028
- options: { force?: boolean } = {}
1029
- ): Promise<void> {
1030
- if (!rootUrl.startsWith("automerge:")) {
1031
- out.error(
1032
- `Invalid Automerge URL: ${rootUrl}\n` +
1033
- `Expected format: automerge:XXXXX`
1034
- );
1035
- out.exit(1);
1036
- }
1037
-
1038
- const resolvedPath = path.resolve(targetPath);
1039
- const syncToolDir = path.join(resolvedPath, ConfigManager.CONFIG_DIR);
1040
-
1041
- if (await pathExists(syncToolDir)) {
1042
- if (!options.force) {
1043
- out.error("Directory already initialized for pushwork. Use --force to overwrite");
1044
- out.exit(1);
1045
- }
1046
- }
1047
-
1048
- await ensureDirectoryExists(syncToolDir);
1049
- await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
1050
-
1051
- // Create minimal snapshot with just the root URL
1052
- const snapshotPath = path.join(syncToolDir, "snapshot.json");
1053
- const snapshot = {
1054
- timestamp: Date.now(),
1055
- rootPath: resolvedPath,
1056
- rootDirectoryUrl: rootUrl,
1057
- files: [],
1058
- directories: [],
1059
- };
1060
- await fs.writeFile(snapshotPath, JSON.stringify(snapshot, null, 2), "utf-8");
1061
-
1062
- // Ensure config exists
1063
- const configManager = new ConfigManager(resolvedPath);
1064
- await configManager.initializeWithOverrides({});
1065
-
1066
- out.successBlock("ROOT SET", rootUrl);
1067
- process.exit();
1068
- }
1069
-
1070
- /**
1071
- * Read a file document by its Automerge URL and print its content
1072
- */
1073
- export async function read(
1074
- docUrl: string,
1075
- options: ReadOptions = {}
1076
- ): Promise<void> {
1077
- if (!docUrl.startsWith("automerge:")) {
1078
- out.error(
1079
- `Invalid Automerge URL: ${docUrl}\n` +
1080
- `Expected format: automerge:XXXXX`
1081
- );
1082
- out.exit(1);
1083
- }
1084
-
1085
- let repo: Repo;
1086
-
1087
- if (options.remote) {
1088
- // Create an ephemeral repo with Subduction to fetch from sync server
1089
- repo = await createEphemeralRepo(DEFAULT_SYNC_SERVER);
1090
- } else {
1091
- // Read from local pushwork storage
1092
- const ctx = await setupCommandContext(".", { syncEnabled: false });
1093
- repo = ctx.repo;
1094
- }
1095
-
1096
- try {
1097
- const handle = await repo.find<FileDocument>(docUrl as AutomergeUrl);
1098
- const doc = await handle.doc();
1099
-
1100
- if (!doc) {
1101
- out.error("Document not found or unavailable");
1102
- await safeRepoShutdown(repo);
1103
- out.exit(1);
1104
- return;
1105
- }
1106
-
1107
- const content = readDocContent(doc.content);
1108
-
1109
- if (content === null) {
1110
- out.error("Document has no content");
1111
- await safeRepoShutdown(repo);
1112
- out.exit(1);
1113
- return;
1114
- }
1115
-
1116
- if (content instanceof Uint8Array) {
1117
- process.stdout.write(content);
1118
- } else {
1119
- process.stdout.write(content);
1120
- }
1121
- } catch (error) {
1122
- out.error(`Failed to read document: ${error}`);
1123
- await safeRepoShutdown(repo);
1124
- out.exit(1);
1125
- return;
1126
- }
1127
-
1128
- await safeRepoShutdown(repo);
1129
- process.exit();
1130
- }
1131
-
1132
- function plural(word: string, count: number): string {
1133
- return count === 1 ? word : `${word}s`;
1134
- }