pushwork 1.0.0

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 (184) hide show
  1. package/README.md +460 -0
  2. package/dist/browser/browser-sync-engine.d.ts +64 -0
  3. package/dist/browser/browser-sync-engine.d.ts.map +1 -0
  4. package/dist/browser/browser-sync-engine.js +303 -0
  5. package/dist/browser/browser-sync-engine.js.map +1 -0
  6. package/dist/browser/filesystem-adapter.d.ts +84 -0
  7. package/dist/browser/filesystem-adapter.d.ts.map +1 -0
  8. package/dist/browser/filesystem-adapter.js +413 -0
  9. package/dist/browser/filesystem-adapter.js.map +1 -0
  10. package/dist/browser/index.d.ts +36 -0
  11. package/dist/browser/index.d.ts.map +1 -0
  12. package/dist/browser/index.js +90 -0
  13. package/dist/browser/index.js.map +1 -0
  14. package/dist/browser/types.d.ts +70 -0
  15. package/dist/browser/types.d.ts.map +1 -0
  16. package/dist/browser/types.js +6 -0
  17. package/dist/browser/types.js.map +1 -0
  18. package/dist/cli/commands.d.ts +71 -0
  19. package/dist/cli/commands.d.ts.map +1 -0
  20. package/dist/cli/commands.js +794 -0
  21. package/dist/cli/commands.js.map +1 -0
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.d.ts.map +1 -0
  24. package/dist/cli/index.js +19 -0
  25. package/dist/cli/index.js.map +1 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +199 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/config/index.d.ts +71 -0
  31. package/dist/config/index.d.ts.map +1 -0
  32. package/dist/config/index.js +314 -0
  33. package/dist/config/index.js.map +1 -0
  34. package/dist/core/change-detection.d.ts +78 -0
  35. package/dist/core/change-detection.d.ts.map +1 -0
  36. package/dist/core/change-detection.js +370 -0
  37. package/dist/core/change-detection.js.map +1 -0
  38. package/dist/core/index.d.ts +5 -0
  39. package/dist/core/index.d.ts.map +1 -0
  40. package/dist/core/index.js +22 -0
  41. package/dist/core/index.js.map +1 -0
  42. package/dist/core/isomorphic-snapshot.d.ts +58 -0
  43. package/dist/core/isomorphic-snapshot.d.ts.map +1 -0
  44. package/dist/core/isomorphic-snapshot.js +204 -0
  45. package/dist/core/isomorphic-snapshot.js.map +1 -0
  46. package/dist/core/move-detection.d.ts +72 -0
  47. package/dist/core/move-detection.d.ts.map +1 -0
  48. package/dist/core/move-detection.js +200 -0
  49. package/dist/core/move-detection.js.map +1 -0
  50. package/dist/core/snapshot.d.ts +109 -0
  51. package/dist/core/snapshot.d.ts.map +1 -0
  52. package/dist/core/snapshot.js +263 -0
  53. package/dist/core/snapshot.js.map +1 -0
  54. package/dist/core/sync-engine.d.ts +110 -0
  55. package/dist/core/sync-engine.d.ts.map +1 -0
  56. package/dist/core/sync-engine.js +817 -0
  57. package/dist/core/sync-engine.js.map +1 -0
  58. package/dist/index.d.ts +6 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +27 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/platform/browser-filesystem.d.ts +26 -0
  63. package/dist/platform/browser-filesystem.d.ts.map +1 -0
  64. package/dist/platform/browser-filesystem.js +91 -0
  65. package/dist/platform/browser-filesystem.js.map +1 -0
  66. package/dist/platform/filesystem.d.ts +29 -0
  67. package/dist/platform/filesystem.d.ts.map +1 -0
  68. package/dist/platform/filesystem.js +65 -0
  69. package/dist/platform/filesystem.js.map +1 -0
  70. package/dist/platform/node-filesystem.d.ts +21 -0
  71. package/dist/platform/node-filesystem.d.ts.map +1 -0
  72. package/dist/platform/node-filesystem.js +93 -0
  73. package/dist/platform/node-filesystem.js.map +1 -0
  74. package/dist/types/config.d.ts +119 -0
  75. package/dist/types/config.d.ts.map +1 -0
  76. package/dist/types/config.js +3 -0
  77. package/dist/types/config.js.map +1 -0
  78. package/dist/types/documents.d.ts +70 -0
  79. package/dist/types/documents.d.ts.map +1 -0
  80. package/dist/types/documents.js +23 -0
  81. package/dist/types/documents.js.map +1 -0
  82. package/dist/types/index.d.ts +4 -0
  83. package/dist/types/index.d.ts.map +1 -0
  84. package/dist/types/index.js +23 -0
  85. package/dist/types/index.js.map +1 -0
  86. package/dist/types/snapshot.d.ts +81 -0
  87. package/dist/types/snapshot.d.ts.map +1 -0
  88. package/dist/types/snapshot.js +17 -0
  89. package/dist/types/snapshot.js.map +1 -0
  90. package/dist/utils/content-similarity.d.ts +53 -0
  91. package/dist/utils/content-similarity.d.ts.map +1 -0
  92. package/dist/utils/content-similarity.js +155 -0
  93. package/dist/utils/content-similarity.js.map +1 -0
  94. package/dist/utils/content.d.ts +5 -0
  95. package/dist/utils/content.d.ts.map +1 -0
  96. package/dist/utils/content.js +30 -0
  97. package/dist/utils/content.js.map +1 -0
  98. package/dist/utils/fs-browser.d.ts +57 -0
  99. package/dist/utils/fs-browser.d.ts.map +1 -0
  100. package/dist/utils/fs-browser.js +311 -0
  101. package/dist/utils/fs-browser.js.map +1 -0
  102. package/dist/utils/fs-node.d.ts +53 -0
  103. package/dist/utils/fs-node.d.ts.map +1 -0
  104. package/dist/utils/fs-node.js +220 -0
  105. package/dist/utils/fs-node.js.map +1 -0
  106. package/dist/utils/fs.d.ts +62 -0
  107. package/dist/utils/fs.d.ts.map +1 -0
  108. package/dist/utils/fs.js +293 -0
  109. package/dist/utils/fs.js.map +1 -0
  110. package/dist/utils/index.d.ts +4 -0
  111. package/dist/utils/index.d.ts.map +1 -0
  112. package/dist/utils/index.js +23 -0
  113. package/dist/utils/index.js.map +1 -0
  114. package/dist/utils/isomorphic.d.ts +29 -0
  115. package/dist/utils/isomorphic.d.ts.map +1 -0
  116. package/dist/utils/isomorphic.js +139 -0
  117. package/dist/utils/isomorphic.js.map +1 -0
  118. package/dist/utils/mime-types.d.ts +13 -0
  119. package/dist/utils/mime-types.d.ts.map +1 -0
  120. package/dist/utils/mime-types.js +240 -0
  121. package/dist/utils/mime-types.js.map +1 -0
  122. package/dist/utils/network-sync.d.ts +12 -0
  123. package/dist/utils/network-sync.d.ts.map +1 -0
  124. package/dist/utils/network-sync.js +149 -0
  125. package/dist/utils/network-sync.js.map +1 -0
  126. package/dist/utils/pure.d.ts +25 -0
  127. package/dist/utils/pure.d.ts.map +1 -0
  128. package/dist/utils/pure.js +112 -0
  129. package/dist/utils/pure.js.map +1 -0
  130. package/dist/utils/repo-factory.d.ts +11 -0
  131. package/dist/utils/repo-factory.d.ts.map +1 -0
  132. package/dist/utils/repo-factory.js +77 -0
  133. package/dist/utils/repo-factory.js.map +1 -0
  134. package/package.json +83 -0
  135. package/src/cli/commands.ts +1053 -0
  136. package/src/cli/index.ts +2 -0
  137. package/src/cli.ts +287 -0
  138. package/src/config/index.ts +334 -0
  139. package/src/core/change-detection.ts +484 -0
  140. package/src/core/index.ts +5 -0
  141. package/src/core/move-detection.ts +269 -0
  142. package/src/core/snapshot.ts +285 -0
  143. package/src/core/sync-engine.ts +1167 -0
  144. package/src/index.ts +14 -0
  145. package/src/types/config.ts +130 -0
  146. package/src/types/documents.ts +72 -0
  147. package/src/types/index.ts +8 -0
  148. package/src/types/snapshot.ts +88 -0
  149. package/src/utils/content-similarity.ts +194 -0
  150. package/src/utils/content.ts +28 -0
  151. package/src/utils/fs.ts +289 -0
  152. package/src/utils/index.ts +8 -0
  153. package/src/utils/mime-types.ts +236 -0
  154. package/src/utils/network-sync.ts +153 -0
  155. package/src/utils/repo-factory.ts +58 -0
  156. package/test/README-TESTING-GAPS.md +174 -0
  157. package/test/integration/README.md +328 -0
  158. package/test/integration/clone-test.sh +310 -0
  159. package/test/integration/conflict-resolution-test.sh +309 -0
  160. package/test/integration/deletion-behavior-test.sh +487 -0
  161. package/test/integration/deletion-sync-test-simple.sh +193 -0
  162. package/test/integration/deletion-sync-test.sh +297 -0
  163. package/test/integration/exclude-patterns.test.ts +152 -0
  164. package/test/integration/full-integration-test.sh +363 -0
  165. package/test/integration/sync-deletion.test.ts +339 -0
  166. package/test/integration/sync-flow.test.ts +309 -0
  167. package/test/run-tests.sh +225 -0
  168. package/test/unit/content-similarity.test.ts +236 -0
  169. package/test/unit/deletion-behavior.test.ts +260 -0
  170. package/test/unit/enhanced-mime-detection.test.ts +266 -0
  171. package/test/unit/snapshot.test.ts +431 -0
  172. package/test/unit/sync-timing.test.ts +178 -0
  173. package/test/unit/utils.test.ts +368 -0
  174. package/tools/browser-sync/README.md +116 -0
  175. package/tools/browser-sync/package.json +44 -0
  176. package/tools/browser-sync/patchwork.json +1 -0
  177. package/tools/browser-sync/pnpm-lock.yaml +4202 -0
  178. package/tools/browser-sync/src/components/BrowserSyncTool.tsx +599 -0
  179. package/tools/browser-sync/src/index.ts +20 -0
  180. package/tools/browser-sync/src/polyfills.ts +31 -0
  181. package/tools/browser-sync/src/styles.css +290 -0
  182. package/tools/browser-sync/src/types.ts +27 -0
  183. package/tools/browser-sync/vite.config.ts +25 -0
  184. package/tsconfig.json +22 -0
@@ -0,0 +1,1053 @@
1
+ import * as path from "path";
2
+ import * as fs from "fs/promises";
3
+ import { Repo, StorageId, AutomergeUrl } from "@automerge/automerge-repo";
4
+ import chalk from "chalk";
5
+ import ora from "ora";
6
+ import * as diffLib from "diff";
7
+ import {
8
+ InitOptions,
9
+ CloneOptions,
10
+ SyncOptions,
11
+ DiffOptions,
12
+ LogOptions,
13
+ CheckoutOptions,
14
+ DirectoryConfig,
15
+ DirectoryDocument,
16
+ } from "../types";
17
+ import { SyncEngine } from "../core";
18
+ import { DetectedChange } from "../core/change-detection";
19
+ import { pathExists, ensureDirectoryExists } from "../utils";
20
+ import { ConfigManager } from "../config";
21
+ import { createRepo } from "../utils/repo-factory";
22
+
23
+ /**
24
+ * Shared context that commands can use
25
+ */
26
+ export interface CommandContext {
27
+ repo: Repo;
28
+ syncEngine: SyncEngine;
29
+ config: DirectoryConfig;
30
+ workingDir: string;
31
+ }
32
+
33
+ /**
34
+ * Shared pre-action that ensures repository and sync engine are properly initialized
35
+ * This function always works, with or without network connectivity
36
+ */
37
+ export async function setupCommandContext(
38
+ workingDir: string = process.cwd(),
39
+ customSyncServer?: string,
40
+ customStorageId?: string,
41
+ enableNetwork: boolean = true
42
+ ): Promise<CommandContext> {
43
+ const resolvedPath = path.resolve(workingDir);
44
+
45
+ // Check if initialized
46
+ const syncToolDir = path.join(resolvedPath, ".pushwork");
47
+ if (!(await pathExists(syncToolDir))) {
48
+ throw new Error(
49
+ 'Directory not initialized for sync. Run "pushwork init" first.'
50
+ );
51
+ }
52
+
53
+ // Load configuration
54
+ const configManager = new ConfigManager(resolvedPath);
55
+ const config = await configManager.getMerged();
56
+
57
+ // Create repo with configurable network setting
58
+ const repo = await createRepo(resolvedPath, {
59
+ enableNetwork,
60
+ syncServer: customSyncServer,
61
+ syncServerStorageId: customStorageId,
62
+ });
63
+
64
+ // Create sync engine with configurable network sync
65
+ const syncEngine = new SyncEngine(
66
+ repo,
67
+ resolvedPath,
68
+ config.defaults.exclude_patterns,
69
+ enableNetwork,
70
+ config.sync_server_storage_id
71
+ );
72
+
73
+ return {
74
+ repo,
75
+ syncEngine,
76
+ config,
77
+ workingDir: resolvedPath,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Safely shutdown a repository with proper error handling
83
+ */
84
+ export async function safeRepoShutdown(
85
+ repo: Repo,
86
+ context?: string
87
+ ): Promise<void> {
88
+ try {
89
+ await repo.shutdown();
90
+ } catch (shutdownError) {
91
+ // WebSocket errors during shutdown are common and non-critical
92
+ // Only warn about unexpected shutdown errors
93
+ const errorMessage =
94
+ shutdownError instanceof Error
95
+ ? shutdownError.message
96
+ : String(shutdownError);
97
+ if (
98
+ !errorMessage.includes("WebSocket") &&
99
+ !errorMessage.includes("connection was established")
100
+ ) {
101
+ console.warn(
102
+ `Warning: Repository shutdown failed${
103
+ context ? ` (${context})` : ""
104
+ }: ${shutdownError}`
105
+ );
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Common progress message helpers
112
+ */
113
+ export const ProgressMessages = {
114
+ // Setup messages
115
+ directoryFound: () => console.log(chalk.gray(" āœ“ Sync directory found")),
116
+ configLoaded: () => console.log(chalk.gray(" āœ“ Configuration loaded")),
117
+ repoConnected: () => console.log(chalk.gray(" āœ“ Connected to repository")),
118
+
119
+ // Configuration display
120
+ syncServer: (server: string) =>
121
+ console.log(chalk.gray(` āœ“ Sync server: ${server}`)),
122
+ storageId: (id: string) => console.log(chalk.gray(` āœ“ Storage ID: ${id}`)),
123
+ rootUrl: (url: string) => console.log(chalk.gray(` āœ“ Root URL: ${url}`)),
124
+
125
+ // Operation completion
126
+ changesWritten: () =>
127
+ console.log(chalk.gray(" āœ“ All changes written to disk")),
128
+ syncCompleted: (duration: number) =>
129
+ console.log(chalk.gray(` āœ“ Initial sync completed in ${duration}ms`)),
130
+ directoryStructureCreated: () =>
131
+ console.log(chalk.gray(" āœ“ Created sync directory structure")),
132
+ configSaved: () => console.log(chalk.gray(" āœ“ Saved configuration")),
133
+ repoCreated: () =>
134
+ console.log(chalk.gray(" āœ“ Created Automerge repository")),
135
+ };
136
+
137
+ /**
138
+ * Show actual content diff for a changed file
139
+ */
140
+ async function showContentDiff(change: DetectedChange): Promise<void> {
141
+ try {
142
+ // Get old content (from snapshot/remote)
143
+ const oldContent = change.remoteContent || "";
144
+
145
+ // Get new content (current local)
146
+ const newContent = change.localContent || "";
147
+
148
+ // Convert binary content to string representation if needed
149
+ const oldText =
150
+ typeof oldContent === "string"
151
+ ? oldContent
152
+ : `<binary content: ${oldContent.length} bytes>`;
153
+ const newText =
154
+ typeof newContent === "string"
155
+ ? newContent
156
+ : `<binary content: ${newContent.length} bytes>`;
157
+
158
+ // Generate unified diff
159
+ const diffResult = diffLib.createPatch(
160
+ change.path,
161
+ oldText,
162
+ newText,
163
+ "previous",
164
+ "current"
165
+ );
166
+
167
+ // Skip the header lines and process the diff
168
+ const lines = diffResult.split("\n").slice(4); // Skip index, ===, ---, +++ lines
169
+
170
+ if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) {
171
+ console.log(chalk.gray(" (content identical)"));
172
+ return;
173
+ }
174
+
175
+ for (const line of lines) {
176
+ if (line.startsWith("@@")) {
177
+ // Hunk header
178
+ console.log(chalk.cyan(line));
179
+ } else if (line.startsWith("+")) {
180
+ // Added line
181
+ console.log(chalk.green(line));
182
+ } else if (line.startsWith("-")) {
183
+ // Removed line
184
+ console.log(chalk.red(line));
185
+ } else if (line.startsWith(" ")) {
186
+ // Context line
187
+ console.log(chalk.gray(line));
188
+ } else if (line === "") {
189
+ // Empty line
190
+ console.log("");
191
+ }
192
+ }
193
+ } catch (error) {
194
+ console.log(chalk.gray(` (diff error: ${error})`));
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Initialize sync in a directory
200
+ */
201
+ export async function init(
202
+ targetPath: string,
203
+ syncServer?: string,
204
+ syncServerStorageId?: string
205
+ ): Promise<void> {
206
+ const spinner = ora("Starting initialization...").start();
207
+
208
+ try {
209
+ const resolvedPath = path.resolve(targetPath);
210
+
211
+ // Step 1: Directory setup
212
+ spinner.text = "Setting up directory structure...";
213
+ await ensureDirectoryExists(resolvedPath);
214
+
215
+ // Check if already initialized
216
+ const syncToolDir = path.join(resolvedPath, ".pushwork");
217
+ if (await pathExists(syncToolDir)) {
218
+ spinner.fail("Directory already initialized for sync");
219
+ return;
220
+ }
221
+
222
+ // Step 2: Create sync directories
223
+ spinner.text = "Creating .pushwork directory...";
224
+ await ensureDirectoryExists(syncToolDir);
225
+ await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
226
+
227
+ ProgressMessages.directoryStructureCreated();
228
+
229
+ // Step 3: Configuration setup
230
+ spinner.text = "Setting up configuration...";
231
+ const configManager = new ConfigManager(resolvedPath);
232
+ const defaultSyncServer = syncServer || "wss://sync3.automerge.org";
233
+ const defaultStorageId =
234
+ syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
235
+ const config: DirectoryConfig = {
236
+ sync_server: defaultSyncServer,
237
+ sync_server_storage_id: defaultStorageId,
238
+ sync_enabled: true,
239
+ defaults: {
240
+ exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
241
+ large_file_threshold: "100MB",
242
+ },
243
+ diff: {
244
+ show_binary: false,
245
+ },
246
+ sync: {
247
+ move_detection_threshold: 0.8,
248
+ prompt_threshold: 0.5,
249
+ auto_sync: false,
250
+ parallel_operations: 4,
251
+ },
252
+ };
253
+ await configManager.save(config);
254
+
255
+ ProgressMessages.configSaved();
256
+ ProgressMessages.syncServer(defaultSyncServer);
257
+ ProgressMessages.storageId(defaultStorageId);
258
+
259
+ // Step 4: Initialize Automerge repo and create root directory document
260
+ spinner.text = "Creating root directory document...";
261
+ const repo = await createRepo(resolvedPath, {
262
+ enableNetwork: true,
263
+ syncServer: syncServer,
264
+ syncServerStorageId: syncServerStorageId,
265
+ });
266
+
267
+ // Create the root directory document
268
+ const rootDoc: DirectoryDocument = {
269
+ "@patchwork": { type: "folder" },
270
+ docs: [],
271
+ };
272
+ const rootHandle = repo.create(rootDoc);
273
+
274
+ ProgressMessages.repoCreated();
275
+ ProgressMessages.rootUrl(rootHandle.url);
276
+
277
+ // Step 5: Scan existing files
278
+ spinner.text = "Scanning existing files...";
279
+ const syncEngine = new SyncEngine(
280
+ repo,
281
+ resolvedPath,
282
+ config.defaults.exclude_patterns,
283
+ true, // Network sync enabled for init
284
+ config.sync_server_storage_id
285
+ );
286
+
287
+ // Get file count for progress
288
+ const dirEntries = await fs.readdir(resolvedPath, { withFileTypes: true });
289
+ const fileCount = dirEntries.filter((dirent: any) =>
290
+ dirent.isFile()
291
+ ).length;
292
+
293
+ if (fileCount > 0) {
294
+ console.log(chalk.gray(` āœ“ Found ${fileCount} existing files`));
295
+ spinner.text = `Creating initial snapshot with ${fileCount} files...`;
296
+ } else {
297
+ spinner.text = "Creating initial empty snapshot...";
298
+ }
299
+
300
+ // Step 6: Set the root directory URL before creating initial snapshot
301
+ await syncEngine.setRootDirectoryUrl(rootHandle.url);
302
+
303
+ // Step 7: Create initial snapshot
304
+ spinner.text = "Creating initial snapshot...";
305
+ const startTime = Date.now();
306
+ await syncEngine.sync(false);
307
+ const duration = Date.now() - startTime;
308
+
309
+ ProgressMessages.syncCompleted(duration);
310
+
311
+ // Step 8: Ensure all Automerge operations are flushed to disk
312
+ spinner.text = "Flushing changes to disk...";
313
+ await safeRepoShutdown(repo, "init");
314
+ ProgressMessages.changesWritten();
315
+
316
+ spinner.succeed(`Initialized sync in ${chalk.green(resolvedPath)}`);
317
+
318
+ console.log(`\n${chalk.bold("šŸŽ‰ Sync Directory Created!")}`);
319
+ console.log(` šŸ“ Directory: ${chalk.blue(resolvedPath)}`);
320
+ console.log(` šŸ”— Sync server: ${chalk.blue(defaultSyncServer)}`);
321
+ console.log(
322
+ `\n${chalk.green("Initialization complete!")} Run ${chalk.cyan(
323
+ "pushwork sync"
324
+ )} to start syncing.`
325
+ );
326
+ } catch (error) {
327
+ spinner.fail(`Failed to initialize: ${error}`);
328
+ throw error;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Run bidirectional sync
334
+ */
335
+ export async function sync(options: SyncOptions): Promise<void> {
336
+ const spinner = ora("Starting sync operation...").start();
337
+
338
+ try {
339
+ // Step 1: Setup shared context
340
+ spinner.text = "Setting up sync context...";
341
+ const { repo, syncEngine, config, workingDir } =
342
+ await setupCommandContext();
343
+
344
+ ProgressMessages.directoryFound();
345
+ ProgressMessages.configLoaded();
346
+ ProgressMessages.syncServer(
347
+ config?.sync_server || "wss://sync3.automerge.org"
348
+ );
349
+ ProgressMessages.repoConnected();
350
+
351
+ // Show root directory URL for context
352
+ const syncStatus = await syncEngine.getStatus();
353
+ if (syncStatus.snapshot?.rootDirectoryUrl) {
354
+ ProgressMessages.rootUrl(syncStatus.snapshot.rootDirectoryUrl);
355
+ }
356
+
357
+ if (options.dryRun) {
358
+ // Dry run mode - detailed preview
359
+ spinner.text = "Analyzing changes (dry run)...";
360
+ const startTime = Date.now();
361
+ const preview = await syncEngine.previewChanges();
362
+ const analysisTime = Date.now() - startTime;
363
+
364
+ spinner.succeed("Change analysis completed");
365
+
366
+ console.log(`\n${chalk.bold("šŸ“Š Change Analysis")} (${analysisTime}ms):`);
367
+ console.log(chalk.gray(` Directory: ${workingDir}`));
368
+ console.log(chalk.gray(` Analysis time: ${analysisTime}ms`));
369
+
370
+ if (preview.changes.length === 0 && preview.moves.length === 0) {
371
+ console.log(
372
+ `\n${chalk.green("✨ No changes detected")} - everything is in sync!`
373
+ );
374
+ return;
375
+ }
376
+
377
+ console.log(`\n${chalk.bold("šŸ“‹ Summary:")}`);
378
+ console.log(` ${preview.summary}`);
379
+
380
+ if (preview.changes.length > 0) {
381
+ const localChanges = preview.changes.filter(
382
+ (c) =>
383
+ c.changeType === "local_only" || c.changeType === "both_changed"
384
+ ).length;
385
+ const remoteChanges = preview.changes.filter(
386
+ (c) =>
387
+ c.changeType === "remote_only" || c.changeType === "both_changed"
388
+ ).length;
389
+ const conflicts = preview.changes.filter(
390
+ (c) => c.changeType === "both_changed"
391
+ ).length;
392
+
393
+ console.log(
394
+ `\n${chalk.bold("šŸ“ File Changes:")} (${
395
+ preview.changes.length
396
+ } total)`
397
+ );
398
+ if (localChanges > 0) {
399
+ console.log(` ${chalk.green("šŸ“¤")} Local changes: ${localChanges}`);
400
+ }
401
+ if (remoteChanges > 0) {
402
+ console.log(` ${chalk.blue("šŸ“„")} Remote changes: ${remoteChanges}`);
403
+ }
404
+ if (conflicts > 0) {
405
+ console.log(` ${chalk.yellow("āš ļø")} Conflicts: ${conflicts}`);
406
+ }
407
+
408
+ console.log(`\n${chalk.bold("šŸ“„ Changed Files:")}`);
409
+ for (const change of preview.changes.slice(0, 10)) {
410
+ // Show first 10
411
+ const typeIcon =
412
+ change.changeType === "local_only"
413
+ ? chalk.green("šŸ“¤")
414
+ : change.changeType === "remote_only"
415
+ ? chalk.blue("šŸ“„")
416
+ : change.changeType === "both_changed"
417
+ ? chalk.yellow("āš ļø")
418
+ : chalk.gray("āž–");
419
+ console.log(` ${typeIcon} ${change.path}`);
420
+ }
421
+ if (preview.changes.length > 10) {
422
+ console.log(
423
+ ` ${chalk.gray(
424
+ `... and ${preview.changes.length - 10} more files`
425
+ )}`
426
+ );
427
+ }
428
+ }
429
+
430
+ if (preview.moves.length > 0) {
431
+ console.log(
432
+ `\n${chalk.bold("šŸ”„ Potential Moves:")} (${preview.moves.length})`
433
+ );
434
+ for (const move of preview.moves.slice(0, 5)) {
435
+ // Show first 5
436
+ const confidence =
437
+ move.confidence === "auto"
438
+ ? chalk.green("Auto")
439
+ : move.confidence === "prompt"
440
+ ? chalk.yellow("Prompt")
441
+ : chalk.red("Low");
442
+ console.log(` šŸ”„ ${move.fromPath} → ${move.toPath} (${confidence})`);
443
+ }
444
+ if (preview.moves.length > 5) {
445
+ console.log(
446
+ ` ${chalk.gray(`... and ${preview.moves.length - 5} more moves`)}`
447
+ );
448
+ }
449
+ }
450
+
451
+ console.log(
452
+ `\n${chalk.cyan("ā„¹ļø Run without --dry-run to apply these changes")}`
453
+ );
454
+ } else {
455
+ // Actual sync operation
456
+ spinner.text = "Detecting changes...";
457
+ const startTime = Date.now();
458
+
459
+ const result = await syncEngine.sync(false);
460
+ const totalTime = Date.now() - startTime;
461
+
462
+ if (result.success) {
463
+ spinner.succeed(`Sync completed in ${totalTime}ms`);
464
+
465
+ console.log(`\n${chalk.bold("āœ… Sync Results:")}`);
466
+ console.log(` šŸ“„ Files changed: ${chalk.yellow(result.filesChanged)}`);
467
+ console.log(
468
+ ` šŸ“ Directories changed: ${chalk.yellow(result.directoriesChanged)}`
469
+ );
470
+ console.log(` ā±ļø Total time: ${chalk.gray(totalTime + "ms")}`);
471
+
472
+ if (result.warnings.length > 0) {
473
+ console.log(
474
+ `\n${chalk.yellow("āš ļø Warnings:")} (${result.warnings.length})`
475
+ );
476
+ for (const warning of result.warnings.slice(0, 5)) {
477
+ console.log(` ${chalk.yellow("āš ļø")} ${warning}`);
478
+ }
479
+ if (result.warnings.length > 5) {
480
+ console.log(
481
+ ` ${chalk.gray(
482
+ `... and ${result.warnings.length - 5} more warnings`
483
+ )}`
484
+ );
485
+ }
486
+ }
487
+
488
+ if (result.filesChanged === 0 && result.directoriesChanged === 0) {
489
+ console.log(`\n${chalk.green("✨ Everything already in sync!")}`);
490
+ }
491
+
492
+ // Ensure all changes are flushed to disk
493
+ spinner.text = "Flushing changes to disk...";
494
+ await safeRepoShutdown(repo, "sync");
495
+ ProgressMessages.changesWritten();
496
+ } else {
497
+ spinner.fail("Sync completed with errors");
498
+
499
+ console.log(
500
+ `\n${chalk.red("āŒ Sync Errors:")} (${result.errors.length})`
501
+ );
502
+ for (const error of result.errors.slice(0, 5)) {
503
+ console.log(
504
+ ` ${chalk.red("āŒ")} ${error.path}: ${error.error.message}`
505
+ );
506
+ }
507
+ if (result.errors.length > 5) {
508
+ console.log(
509
+ ` ${chalk.gray(`... and ${result.errors.length - 5} more errors`)}`
510
+ );
511
+ }
512
+
513
+ if (result.filesChanged > 0 || result.directoriesChanged > 0) {
514
+ console.log(`\n${chalk.yellow("āš ļø Partial sync completed:")}`);
515
+ console.log(` šŸ“„ Files changed: ${result.filesChanged}`);
516
+ console.log(` šŸ“ Directories changed: ${result.directoriesChanged}`);
517
+ }
518
+
519
+ // Still try to flush any partial changes
520
+ await safeRepoShutdown(repo, "sync-error");
521
+ }
522
+ }
523
+ } catch (error) {
524
+ spinner.fail(`Sync failed: ${error}`);
525
+ throw error;
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Show differences between local and remote
531
+ */
532
+ export async function diff(
533
+ targetPath = ".",
534
+ options: DiffOptions
535
+ ): Promise<void> {
536
+ try {
537
+ // Setup shared context with network disabled for diff check
538
+ const { repo, syncEngine } = await setupCommandContext(
539
+ targetPath,
540
+ undefined,
541
+ undefined,
542
+ false
543
+ );
544
+ const preview = await syncEngine.previewChanges();
545
+
546
+ if (options.nameOnly) {
547
+ // Show only file names
548
+ for (const change of preview.changes) {
549
+ console.log(change.path);
550
+ }
551
+ return;
552
+ }
553
+
554
+ // Show root directory URL for context
555
+ const diffStatus = await syncEngine.getStatus();
556
+ if (diffStatus.snapshot?.rootDirectoryUrl) {
557
+ console.log(
558
+ chalk.gray(`Root URL: ${diffStatus.snapshot.rootDirectoryUrl}`)
559
+ );
560
+ console.log("");
561
+ }
562
+
563
+ if (preview.changes.length === 0) {
564
+ console.log(chalk.green("No changes detected"));
565
+ return;
566
+ }
567
+
568
+ console.log(chalk.bold("Differences:"));
569
+
570
+ for (const change of preview.changes) {
571
+ const typeLabel =
572
+ change.changeType === "local_only"
573
+ ? chalk.green("[LOCAL]")
574
+ : change.changeType === "remote_only"
575
+ ? chalk.blue("[REMOTE]")
576
+ : change.changeType === "both_changed"
577
+ ? chalk.yellow("[CONFLICT]")
578
+ : chalk.gray("[NO CHANGE]");
579
+
580
+ console.log(`\n${typeLabel} ${change.path}`);
581
+
582
+ if (options.tool) {
583
+ console.log(` Use "${options.tool}" to view detailed diff`);
584
+ } else {
585
+ // Show actual diff content
586
+ await showContentDiff(change);
587
+ }
588
+ }
589
+
590
+ // Cleanup repo resources
591
+ await safeRepoShutdown(repo, "diff");
592
+ } catch (error) {
593
+ console.error(chalk.red(`Diff failed: ${error}`));
594
+ throw error;
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Show sync status
600
+ */
601
+ export async function status(): Promise<void> {
602
+ try {
603
+ const spinner = ora("Loading sync status...").start();
604
+
605
+ // Setup shared context with network disabled for status check
606
+ const { repo, syncEngine, workingDir } = await setupCommandContext(
607
+ process.cwd(),
608
+ undefined,
609
+ undefined,
610
+ false
611
+ );
612
+ const syncStatus = await syncEngine.getStatus();
613
+
614
+ spinner.stop();
615
+
616
+ console.log(chalk.bold("šŸ“Š Sync Status Report"));
617
+ console.log(`${"=".repeat(50)}`);
618
+
619
+ // Directory information
620
+ console.log(`\n${chalk.bold("šŸ“ Directory Information:")}`);
621
+ console.log(` šŸ“‚ Path: ${chalk.blue(workingDir)}`);
622
+ console.log(` šŸ”§ Config: ${path.join(workingDir, ".pushwork")}`);
623
+
624
+ // Show root directory URL if available
625
+ if (syncStatus.snapshot?.rootDirectoryUrl) {
626
+ console.log(
627
+ ` šŸ”— Root URL: ${chalk.cyan(syncStatus.snapshot.rootDirectoryUrl)}`
628
+ );
629
+ } else {
630
+ console.log(` šŸ”— Root URL: ${chalk.yellow("Not set")}`);
631
+ }
632
+
633
+ // Sync timing
634
+ if (syncStatus.lastSync) {
635
+ const timeSince = Date.now() - syncStatus.lastSync.getTime();
636
+ const timeAgo =
637
+ timeSince < 60000
638
+ ? `${Math.floor(timeSince / 1000)}s ago`
639
+ : timeSince < 3600000
640
+ ? `${Math.floor(timeSince / 60000)}m ago`
641
+ : `${Math.floor(timeSince / 3600000)}h ago`;
642
+
643
+ console.log(`\n${chalk.bold("ā±ļø Sync Timing:")}`);
644
+ console.log(
645
+ ` šŸ• Last sync: ${chalk.green(syncStatus.lastSync.toLocaleString())}`
646
+ );
647
+ console.log(` ā³ Time since: ${chalk.gray(timeAgo)}`);
648
+ } else {
649
+ console.log(`\n${chalk.bold("ā±ļø Sync Timing:")}`);
650
+ console.log(` šŸ• Last sync: ${chalk.yellow("Never synced")}`);
651
+ console.log(
652
+ ` šŸ’” Run ${chalk.cyan("pushwork sync")} to perform initial sync`
653
+ );
654
+ }
655
+
656
+ // Change status
657
+ console.log(`\n${chalk.bold("šŸ“ Change Status:")}`);
658
+ if (syncStatus.hasChanges) {
659
+ console.log(
660
+ ` šŸ“„ Pending changes: ${chalk.yellow(syncStatus.changeCount)}`
661
+ );
662
+ console.log(` šŸ”„ Status: ${chalk.yellow("Sync needed")}`);
663
+ console.log(` šŸ’” Run ${chalk.cyan("pushwork diff")} to see details`);
664
+ } else {
665
+ console.log(` šŸ“„ Pending changes: ${chalk.green("None")}`);
666
+ console.log(` āœ… Status: ${chalk.green("Up to date")}`);
667
+ }
668
+
669
+ // Configuration
670
+ console.log(`\n${chalk.bold("āš™ļø Configuration:")}`);
671
+
672
+ const statusConfigManager2 = new ConfigManager(workingDir);
673
+ const statusConfig2 = await statusConfigManager2.load();
674
+
675
+ if (statusConfig2?.sync_server) {
676
+ console.log(` šŸ”— Sync server: ${chalk.blue(statusConfig2.sync_server)}`);
677
+ } else {
678
+ console.log(
679
+ ` šŸ”— Sync server: ${chalk.blue("wss://sync3.automerge.org")} (default)`
680
+ );
681
+ }
682
+
683
+ console.log(
684
+ ` ⚔ Auto sync: ${
685
+ statusConfig2?.sync?.auto_sync
686
+ ? chalk.green("Enabled")
687
+ : chalk.gray("Disabled")
688
+ }`
689
+ );
690
+
691
+ // Snapshot information
692
+ if (syncStatus.snapshot) {
693
+ const fileCount = syncStatus.snapshot.files.size;
694
+ const dirCount = syncStatus.snapshot.directories.size;
695
+
696
+ console.log(`\n${chalk.bold("šŸ“Š Repository Statistics:")}`);
697
+ console.log(` šŸ“„ Tracked files: ${chalk.yellow(fileCount)}`);
698
+ console.log(` šŸ“ Tracked directories: ${chalk.yellow(dirCount)}`);
699
+ console.log(
700
+ ` šŸ·ļø Snapshot timestamp: ${chalk.gray(
701
+ new Date(syncStatus.snapshot.timestamp).toLocaleString()
702
+ )}`
703
+ );
704
+ }
705
+
706
+ // Quick actions
707
+ console.log(`\n${chalk.bold("šŸš€ Quick Actions:")}`);
708
+ if (syncStatus.hasChanges) {
709
+ console.log(
710
+ ` ${chalk.cyan("pushwork diff")} - View pending changes`
711
+ );
712
+ console.log(` ${chalk.cyan("pushwork sync")} - Apply changes`);
713
+ } else {
714
+ console.log(
715
+ ` ${chalk.cyan("pushwork sync")} - Check for remote changes`
716
+ );
717
+ }
718
+ console.log(` ${chalk.cyan("pushwork log")} - View sync history`);
719
+
720
+ // Cleanup repo resources
721
+ await safeRepoShutdown(repo, "status");
722
+ } catch (error) {
723
+ console.error(chalk.red(`āŒ Status check failed: ${error}`));
724
+ throw error;
725
+ }
726
+ }
727
+
728
+ /**
729
+ * Show sync history
730
+ */
731
+ export async function log(
732
+ targetPath = ".",
733
+ options: LogOptions
734
+ ): Promise<void> {
735
+ try {
736
+ // Setup shared context with network disabled for log check
737
+ const {
738
+ repo: logRepo,
739
+ syncEngine: logSyncEngine,
740
+ workingDir,
741
+ } = await setupCommandContext(targetPath, undefined, undefined, false);
742
+ const logStatus = await logSyncEngine.getStatus();
743
+
744
+ if (logStatus.snapshot?.rootDirectoryUrl) {
745
+ console.log(
746
+ chalk.gray(`Root URL: ${logStatus.snapshot.rootDirectoryUrl}`)
747
+ );
748
+ console.log("");
749
+ }
750
+
751
+ // TODO: Implement history tracking and display
752
+ // For now, show basic information
753
+
754
+ console.log(chalk.bold("Sync History:"));
755
+
756
+ // Check for snapshot files
757
+ const snapshotPath = path.join(workingDir, ".pushwork", "snapshot.json");
758
+ if (await pathExists(snapshotPath)) {
759
+ const stats = await fs.stat(snapshotPath);
760
+
761
+ if (options.oneline) {
762
+ console.log(`${stats.mtime.toISOString()} - Last sync`);
763
+ } else {
764
+ console.log(`Last sync: ${chalk.green(stats.mtime.toISOString())}`);
765
+ console.log(`Snapshot size: ${stats.size} bytes`);
766
+ }
767
+ } else {
768
+ console.log(chalk.yellow("No sync history found"));
769
+ }
770
+
771
+ // Cleanup repo resources
772
+ await safeRepoShutdown(logRepo, "log");
773
+ } catch (error) {
774
+ console.error(chalk.red(`Log failed: ${error}`));
775
+ throw error;
776
+ }
777
+ }
778
+
779
+ /**
780
+ * Checkout/restore from previous sync
781
+ */
782
+ export async function checkout(
783
+ syncId: string,
784
+ targetPath = ".",
785
+ options: CheckoutOptions
786
+ ): Promise<void> {
787
+ try {
788
+ // Setup shared context
789
+ const { workingDir } = await setupCommandContext(targetPath);
790
+
791
+ // TODO: Implement checkout functionality
792
+ // This would involve:
793
+ // 1. Finding the sync with the given ID
794
+ // 2. Restoring file states from that sync
795
+ // 3. Updating the snapshot
796
+
797
+ console.log(chalk.yellow(`Checkout functionality not yet implemented`));
798
+ console.log(`Would restore to sync: ${syncId}`);
799
+ console.log(`Target path: ${workingDir}`);
800
+ } catch (error) {
801
+ console.error(chalk.red(`Checkout failed: ${error}`));
802
+ throw error;
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Clone an existing synced directory from an AutomergeUrl
808
+ */
809
+ export async function clone(
810
+ rootUrl: string,
811
+ targetPath: string,
812
+ options: CloneOptions
813
+ ): Promise<void> {
814
+ const spinner = ora("Starting clone operation...").start();
815
+
816
+ try {
817
+ const resolvedPath = path.resolve(targetPath);
818
+
819
+ // Step 1: Directory setup
820
+ spinner.text = "Setting up target directory...";
821
+
822
+ // Check if directory exists and handle --force
823
+ if (await pathExists(resolvedPath)) {
824
+ const files = await fs.readdir(resolvedPath);
825
+ if (files.length > 0 && !options.force) {
826
+ spinner.fail(
827
+ "Target directory is not empty. Use --force to overwrite."
828
+ );
829
+ return;
830
+ }
831
+ } else {
832
+ await ensureDirectoryExists(resolvedPath);
833
+ }
834
+
835
+ // Check if already initialized
836
+ const syncToolDir = path.join(resolvedPath, ".pushwork");
837
+ if (await pathExists(syncToolDir)) {
838
+ if (!options.force) {
839
+ spinner.fail(
840
+ "Directory already initialized for sync. Use --force to overwrite."
841
+ );
842
+ return;
843
+ }
844
+ // Clean up existing sync directory
845
+ await fs.rm(syncToolDir, { recursive: true, force: true });
846
+ }
847
+
848
+ console.log(chalk.gray(" āœ“ Target directory prepared"));
849
+
850
+ // Step 2: Create sync directories
851
+ spinner.text = "Creating .pushwork directory...";
852
+ await ensureDirectoryExists(syncToolDir);
853
+ await ensureDirectoryExists(path.join(syncToolDir, "automerge"));
854
+
855
+ ProgressMessages.directoryStructureCreated();
856
+
857
+ // Step 3: Configuration setup
858
+ spinner.text = "Setting up configuration...";
859
+ const configManager = new ConfigManager(resolvedPath);
860
+ const defaultSyncServer = options.syncServer || "wss://sync3.automerge.org";
861
+ const defaultStorageId =
862
+ options.syncServerStorageId || "3760df37-a4c6-4f66-9ecd-732039a9385d";
863
+ const config: DirectoryConfig = {
864
+ sync_server: defaultSyncServer,
865
+ sync_server_storage_id: defaultStorageId,
866
+ sync_enabled: true,
867
+ defaults: {
868
+ exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
869
+ large_file_threshold: "100MB",
870
+ },
871
+ diff: {
872
+ show_binary: false,
873
+ },
874
+ sync: {
875
+ move_detection_threshold: 0.8,
876
+ prompt_threshold: 0.5,
877
+ auto_sync: false,
878
+ parallel_operations: 4,
879
+ },
880
+ };
881
+ await configManager.save(config);
882
+
883
+ ProgressMessages.configSaved();
884
+ ProgressMessages.syncServer(defaultSyncServer);
885
+ ProgressMessages.storageId(defaultStorageId);
886
+
887
+ // Step 4: Initialize Automerge repo and connect to root directory
888
+ spinner.text = "Connecting to root directory document...";
889
+ const repo = await createRepo(resolvedPath, {
890
+ enableNetwork: true,
891
+ syncServer: options.syncServer,
892
+ syncServerStorageId: options.syncServerStorageId,
893
+ });
894
+
895
+ ProgressMessages.repoCreated();
896
+ ProgressMessages.rootUrl(rootUrl);
897
+
898
+ // Step 5: Initialize sync engine and pull existing structure
899
+ spinner.text = "Downloading directory structure...";
900
+ const syncEngine = new SyncEngine(
901
+ repo,
902
+ resolvedPath,
903
+ config.defaults.exclude_patterns,
904
+ true, // Network sync enabled for clone
905
+ defaultStorageId
906
+ );
907
+
908
+ // Set the root directory URL to connect to the cloned repository
909
+ await syncEngine.setRootDirectoryUrl(rootUrl as AutomergeUrl);
910
+
911
+ // Sync to pull the existing directory structure and files
912
+ const startTime = Date.now();
913
+ await syncEngine.sync(false);
914
+ const duration = Date.now() - startTime;
915
+
916
+ console.log(chalk.gray(` āœ“ Directory sync completed in ${duration}ms`));
917
+
918
+ // Ensure all changes are flushed to disk
919
+ spinner.text = "Flushing changes to disk...";
920
+ await safeRepoShutdown(repo, "clone");
921
+ ProgressMessages.changesWritten();
922
+
923
+ spinner.succeed(`Cloned sync directory to ${chalk.green(resolvedPath)}`);
924
+
925
+ console.log(`\n${chalk.bold("šŸ“‚ Directory Cloned!")}`);
926
+ console.log(` šŸ“ Directory: ${chalk.blue(resolvedPath)}`);
927
+ console.log(` šŸ”— Root URL: ${chalk.cyan(rootUrl)}`);
928
+ console.log(` šŸ”— Sync server: ${chalk.blue(defaultSyncServer)}`);
929
+ console.log(
930
+ `\n${chalk.green("Clone complete!")} Run ${chalk.cyan(
931
+ "pushwork sync"
932
+ )} to stay in sync.`
933
+ );
934
+ } catch (error) {
935
+ spinner.fail(`Failed to clone: ${error}`);
936
+ throw error;
937
+ }
938
+ }
939
+
940
+ /**
941
+ * Get the root URL for the current pushwork repository
942
+ */
943
+ export async function url(targetPath = "."): Promise<void> {
944
+ try {
945
+ const resolvedPath = path.resolve(targetPath);
946
+
947
+ // Check if initialized
948
+ const syncToolDir = path.join(resolvedPath, ".pushwork");
949
+ if (!(await pathExists(syncToolDir))) {
950
+ console.error(chalk.red("Directory not initialized for sync"));
951
+ console.error(`Run ${chalk.cyan("pushwork init .")} to get started`);
952
+ process.exit(1);
953
+ }
954
+
955
+ // Load the snapshot directly to get the URL without all the verbose output
956
+ const snapshotPath = path.join(syncToolDir, "snapshot.json");
957
+ if (!(await pathExists(snapshotPath))) {
958
+ console.error(chalk.red("No snapshot found"));
959
+ console.error(
960
+ chalk.gray("The repository may not be properly initialized")
961
+ );
962
+ process.exit(1);
963
+ }
964
+
965
+ const snapshotData = await fs.readFile(snapshotPath, "utf-8");
966
+ const snapshot = JSON.parse(snapshotData);
967
+
968
+ if (snapshot.rootDirectoryUrl) {
969
+ // Output just the URL for easy use in scripts
970
+ console.log(snapshot.rootDirectoryUrl);
971
+ } else {
972
+ console.error(chalk.red("No root URL found in snapshot"));
973
+ console.error(
974
+ chalk.gray("The repository may not be properly initialized")
975
+ );
976
+ process.exit(1);
977
+ }
978
+ } catch (error) {
979
+ console.error(chalk.red(`Failed to get URL: ${error}`));
980
+ process.exit(1);
981
+ }
982
+ }
983
+
984
+ export async function commit(
985
+ targetPath: string,
986
+ dryRun: boolean = false
987
+ ): Promise<void> {
988
+ const spinner = ora("Starting commit operation...").start();
989
+ let repo: Repo | undefined;
990
+
991
+ try {
992
+ // Setup shared context with network disabled for local-only commit
993
+ spinner.text = "Setting up commit context...";
994
+ const context = await setupCommandContext(
995
+ targetPath,
996
+ undefined,
997
+ undefined,
998
+ false
999
+ );
1000
+ repo = context.repo;
1001
+ const syncEngine = context.syncEngine;
1002
+ spinner.succeed("Connected to repository");
1003
+
1004
+ // Run local commit only
1005
+ spinner.text = "Committing local changes...";
1006
+ const startTime = Date.now();
1007
+ const result = await syncEngine.commitLocal(dryRun);
1008
+ const duration = Date.now() - startTime;
1009
+
1010
+ if (repo) {
1011
+ await safeRepoShutdown(repo, "commit");
1012
+ }
1013
+ spinner.succeed(`Commit completed in ${duration}ms`);
1014
+
1015
+ // Display results
1016
+ console.log(chalk.green("\nāœ… Commit Results:"));
1017
+ console.log(` šŸ“„ Files committed: ${result.filesChanged}`);
1018
+ console.log(` šŸ“ Directories committed: ${result.directoriesChanged}`);
1019
+ console.log(` ā±ļø Total time: ${duration}ms`);
1020
+
1021
+ if (result.warnings.length > 0) {
1022
+ console.log(chalk.yellow("\nāš ļø Warnings:"));
1023
+ result.warnings.forEach((warning: string) =>
1024
+ console.log(chalk.yellow(` • ${warning}`))
1025
+ );
1026
+ }
1027
+
1028
+ if (result.errors.length > 0) {
1029
+ console.log(chalk.red("\nāŒ Errors:"));
1030
+ result.errors.forEach((error) =>
1031
+ console.log(
1032
+ chalk.red(
1033
+ ` • ${error.operation} at ${error.path}: ${error.error.message}`
1034
+ )
1035
+ )
1036
+ );
1037
+ process.exit(1);
1038
+ }
1039
+
1040
+ console.log(
1041
+ chalk.gray("\nšŸ’” Run 'pushwork push' to upload to sync server")
1042
+ );
1043
+ } catch (error) {
1044
+ if (repo) {
1045
+ await safeRepoShutdown(repo, "commit-error");
1046
+ }
1047
+ spinner.fail(`Commit failed: ${error}`);
1048
+ console.error(chalk.red(`Error: ${error}`));
1049
+ process.exit(1);
1050
+ }
1051
+ }
1052
+
1053
+ // TODO: Add push and pull commands later