pushwork 1.0.4 → 1.0.7

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