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,2 @@
1
+ // CLI commands
2
+ export * from "./commands";
package/src/cli.ts ADDED
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import chalk from "chalk";
5
+ import {
6
+ init,
7
+ clone,
8
+ sync,
9
+ diff,
10
+ status,
11
+ log,
12
+ checkout,
13
+ commit,
14
+ url,
15
+ } from "./cli/commands";
16
+
17
+ /**
18
+ * Wrapper for command actions with consistent error handling
19
+ */
20
+ function withErrorHandling<T extends any[], R>(
21
+ fn: (...args: T) => Promise<R>
22
+ ): (...args: T) => Promise<void> {
23
+ return async (...args: T): Promise<void> => {
24
+ try {
25
+ await fn(...args);
26
+ } catch (error) {
27
+ console.error(chalk.red(`Error: ${error}`));
28
+ process.exit(1);
29
+ }
30
+ };
31
+ }
32
+
33
+ const program = new Command();
34
+
35
+ program
36
+ .name("pushwork")
37
+ .description("Bidirectional directory synchronization using Automerge CRDTs")
38
+ .version("1.0.0");
39
+
40
+ // Init command
41
+ program
42
+ .command("init")
43
+ .description("Initialize sync in directory")
44
+ .argument("<path>", "Directory path to initialize")
45
+ .option(
46
+ "--sync-server <url>",
47
+ "Custom sync server URL (must be used with --sync-server-storage-id)"
48
+ )
49
+ .option(
50
+ "--sync-server-storage-id <id>",
51
+ "Custom sync server storage ID (must be used with --sync-server)"
52
+ )
53
+ .addHelpText(
54
+ "after",
55
+ `
56
+ Examples:
57
+ pushwork init ./my-folder
58
+ pushwork init ./my-folder --sync-server ws://localhost:3030 --sync-server-storage-id 1d89eba7-f7a4-4e8e-80f2-5f4e2406f507
59
+
60
+ Note: Custom sync server options must always be used together.`
61
+ )
62
+ .action(
63
+ withErrorHandling(async (path: string, options) => {
64
+ // Validate that both sync server options are provided together
65
+ const hasSyncServer = !!options.syncServer;
66
+ const hasSyncServerStorageId = !!options.syncServerStorageId;
67
+
68
+ if (hasSyncServer && !hasSyncServerStorageId) {
69
+ console.error(
70
+ chalk.red("Error: --sync-server requires --sync-server-storage-id")
71
+ );
72
+ console.error(
73
+ chalk.yellow("Both arguments must be provided together.")
74
+ );
75
+ process.exit(1);
76
+ }
77
+
78
+ if (hasSyncServerStorageId && !hasSyncServer) {
79
+ console.error(
80
+ chalk.red("Error: --sync-server-storage-id requires --sync-server")
81
+ );
82
+ console.error(
83
+ chalk.yellow("Both arguments must be provided together.")
84
+ );
85
+ process.exit(1);
86
+ }
87
+
88
+ await init(path, options.syncServer, options.syncServerStorageId);
89
+ })
90
+ );
91
+
92
+ // Clone command
93
+ program
94
+ .command("clone")
95
+ .description("Clone an existing synced directory")
96
+ .argument("<url>", "AutomergeUrl of root directory to clone")
97
+ .argument("<path>", "Target directory path")
98
+ .option("--force", "Overwrite existing directory")
99
+ .option(
100
+ "--sync-server <url>",
101
+ "Custom sync server URL (must be used with --sync-server-storage-id)"
102
+ )
103
+ .option(
104
+ "--sync-server-storage-id <id>",
105
+ "Custom sync server storage ID (must be used with --sync-server)"
106
+ )
107
+ .addHelpText(
108
+ "after",
109
+ `
110
+ Examples:
111
+ pushwork clone automerge:abc123 ./my-clone
112
+ pushwork clone automerge:abc123 ./my-clone --force
113
+ pushwork clone automerge:abc123 ./my-clone --sync-server ws://localhost:3030 --sync-server-storage-id 1d89eba7-f7a4-4e8e-80f2-5f4e2406f507
114
+
115
+ Note: Custom sync server options must always be used together.`
116
+ )
117
+ .action(
118
+ withErrorHandling(async (url: string, path: string, options) => {
119
+ // Validate that both sync server options are provided together
120
+ const hasSyncServer = !!options.syncServer;
121
+ const hasSyncServerStorageId = !!options.syncServerStorageId;
122
+
123
+ if (hasSyncServer && !hasSyncServerStorageId) {
124
+ console.error(
125
+ chalk.red("Error: --sync-server requires --sync-server-storage-id")
126
+ );
127
+ console.error(
128
+ chalk.yellow("Both arguments must be provided together.")
129
+ );
130
+ process.exit(1);
131
+ }
132
+
133
+ if (hasSyncServerStorageId && !hasSyncServer) {
134
+ console.error(
135
+ chalk.red("Error: --sync-server-storage-id requires --sync-server")
136
+ );
137
+ console.error(
138
+ chalk.yellow("Both arguments must be provided together.")
139
+ );
140
+ process.exit(1);
141
+ }
142
+
143
+ await clone(url, path, {
144
+ force: options.force || false,
145
+ dryRun: false,
146
+ verbose: false,
147
+ syncServer: options.syncServer,
148
+ syncServerStorageId: options.syncServerStorageId,
149
+ });
150
+ })
151
+ );
152
+
153
+ // Commit command
154
+ program
155
+ .command("commit")
156
+ .description("Commit local changes (no network sync)")
157
+ .argument("[path]", "Directory path to commit", ".")
158
+ .option("--dry-run", "Show what would be committed without applying changes")
159
+ .action(
160
+ withErrorHandling(async (path: string, options) => {
161
+ await commit(path, options.dryRun || false);
162
+ })
163
+ );
164
+
165
+ // Sync command
166
+ program
167
+ .command("sync")
168
+ .description("Run full bidirectional synchronization")
169
+ .option("--dry-run", "Show what would be done without applying changes")
170
+ .option("-v, --verbose", "Verbose output")
171
+ .action(
172
+ withErrorHandling(async (options) => {
173
+ await sync({
174
+ dryRun: options.dryRun || false,
175
+ verbose: options.verbose || false,
176
+ });
177
+ })
178
+ );
179
+
180
+ // Diff command
181
+ program
182
+ .command("diff")
183
+ .description("Show changes in working directory since last sync")
184
+ .argument("[path]", "Limit diff to specific path", ".")
185
+ .option("--tool <tool>", "Use external diff tool (meld, vimdiff, etc.)")
186
+ .option("--name-only", "Show only changed file names")
187
+ .action(
188
+ withErrorHandling(async (path: string, options) => {
189
+ await diff(path, {
190
+ tool: options.tool,
191
+ nameOnly: options.nameOnly || false,
192
+ dryRun: false,
193
+ verbose: false,
194
+ });
195
+ })
196
+ );
197
+
198
+ // Status command
199
+ program
200
+ .command("status")
201
+ .description("Show sync status summary")
202
+ .action(
203
+ withErrorHandling(async (options) => {
204
+ await status();
205
+ })
206
+ );
207
+
208
+ // Log command
209
+ program
210
+ .command("log")
211
+ .description("Show sync history")
212
+ .argument("[path]", "Show history for specific file or directory", ".")
213
+ .option("--oneline", "Compact one-line per sync format")
214
+ .option("--since <date>", "Show syncs since date")
215
+ .option("--limit <n>", "Limit number of syncs shown", "10")
216
+ .action(
217
+ withErrorHandling(async (path: string, options) => {
218
+ await log(path, {
219
+ oneline: options.oneline || false,
220
+ since: options.since,
221
+ limit: parseInt(options.limit),
222
+ dryRun: false,
223
+ verbose: false,
224
+ });
225
+ })
226
+ );
227
+
228
+ // Checkout command
229
+ program
230
+ .command("checkout")
231
+ .description("Restore directory to state from previous sync")
232
+ .argument("<sync-id>", "Sync ID to restore to")
233
+ .argument("[path]", "Specific path to restore", ".")
234
+ .option("-f, --force", "Force checkout even if there are uncommitted changes")
235
+ .action(
236
+ withErrorHandling(async (syncId: string, path: string, options) => {
237
+ await checkout(syncId, path, {
238
+ force: options.force || false,
239
+ dryRun: false,
240
+ verbose: false,
241
+ });
242
+ })
243
+ );
244
+
245
+ // URL command
246
+ program
247
+ .command("url")
248
+ .description("Show the Automerge root URL for this repository")
249
+ .argument("[path]", "Directory path", ".")
250
+ .addHelpText(
251
+ "after",
252
+ `
253
+ Examples:
254
+ pushwork url # Show URL for current directory
255
+ pushwork url ./repo # Show URL for specific directory
256
+
257
+ Note: This command outputs only the URL, making it useful for scripts.`
258
+ )
259
+ .action(
260
+ withErrorHandling(async (path: string) => {
261
+ await url(path);
262
+ })
263
+ );
264
+
265
+ // Global error handler
266
+ process.on("unhandledRejection", (reason, promise) => {
267
+ console.error(
268
+ chalk.red("Unhandled Rejection at:"),
269
+ promise,
270
+ chalk.red("reason:"),
271
+ reason
272
+ );
273
+ process.exit(1);
274
+ });
275
+
276
+ process.on("uncaughtException", (error) => {
277
+ console.error(chalk.red("Uncaught Exception:"), error);
278
+ process.exit(1);
279
+ });
280
+
281
+ // Parse arguments
282
+ program.parse();
283
+
284
+ // Show help if no arguments provided
285
+ if (!process.argv.slice(2).length) {
286
+ program.outputHelp();
287
+ }
@@ -0,0 +1,334 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { GlobalConfig, DirectoryConfig } from "../types";
5
+ import { pathExists, ensureDirectoryExists } from "../utils";
6
+
7
+ /**
8
+ * Configuration manager for pushwork
9
+ */
10
+ export class ConfigManager {
11
+ private static readonly GLOBAL_CONFIG_DIR = ".pushwork";
12
+ private static readonly CONFIG_FILENAME = "config.json";
13
+ private static readonly LOCAL_CONFIG_DIR = ".pushwork";
14
+
15
+ constructor(private workingDir?: string) {}
16
+
17
+ /**
18
+ * Get global configuration path
19
+ */
20
+ private getGlobalConfigPath(): string {
21
+ return path.join(
22
+ os.homedir(),
23
+ ConfigManager.GLOBAL_CONFIG_DIR,
24
+ ConfigManager.CONFIG_FILENAME
25
+ );
26
+ }
27
+
28
+ /**
29
+ * Get local configuration path
30
+ */
31
+ private getLocalConfigPath(): string {
32
+ if (!this.workingDir) {
33
+ throw new Error("Working directory not set for local config");
34
+ }
35
+ return path.join(
36
+ this.workingDir,
37
+ ConfigManager.LOCAL_CONFIG_DIR,
38
+ ConfigManager.CONFIG_FILENAME
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Load global configuration
44
+ */
45
+ async loadGlobal(): Promise<GlobalConfig | null> {
46
+ try {
47
+ const configPath = this.getGlobalConfigPath();
48
+ if (!(await pathExists(configPath))) {
49
+ return null;
50
+ }
51
+
52
+ const content = await fs.readFile(configPath, "utf8");
53
+ return JSON.parse(content) as GlobalConfig;
54
+ } catch (error) {
55
+ console.warn(`Failed to load global config: ${error}`);
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Save global configuration
62
+ */
63
+ async saveGlobal(config: GlobalConfig): Promise<void> {
64
+ try {
65
+ const configPath = this.getGlobalConfigPath();
66
+ await ensureDirectoryExists(path.dirname(configPath));
67
+
68
+ const content = JSON.stringify(config, null, 2);
69
+ await fs.writeFile(configPath, content, "utf8");
70
+ } catch (error) {
71
+ throw new Error(`Failed to save global config: ${error}`);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Load local/directory configuration
77
+ */
78
+ async load(): Promise<DirectoryConfig | null> {
79
+ if (!this.workingDir) {
80
+ return null;
81
+ }
82
+
83
+ try {
84
+ const configPath = this.getLocalConfigPath();
85
+ if (!(await pathExists(configPath))) {
86
+ return null;
87
+ }
88
+
89
+ const content = await fs.readFile(configPath, "utf8");
90
+ return JSON.parse(content) as DirectoryConfig;
91
+ } catch (error) {
92
+ console.warn(`Failed to load local config: ${error}`);
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Save local/directory configuration
99
+ */
100
+ async save(config: DirectoryConfig): Promise<void> {
101
+ if (!this.workingDir) {
102
+ throw new Error("Working directory not set for local config");
103
+ }
104
+
105
+ try {
106
+ const configPath = this.getLocalConfigPath();
107
+ await ensureDirectoryExists(path.dirname(configPath));
108
+
109
+ const content = JSON.stringify(config, null, 2);
110
+ await fs.writeFile(configPath, content, "utf8");
111
+ } catch (error) {
112
+ throw new Error(`Failed to save local config: ${error}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get merged configuration (global + local)
118
+ */
119
+ async getMerged(): Promise<DirectoryConfig> {
120
+ const globalConfig = await this.loadGlobal();
121
+ const localConfig = await this.load();
122
+
123
+ // Create default configuration
124
+ const defaultConfig: DirectoryConfig = {
125
+ sync_enabled: true,
126
+ sync_server_storage_id: "3760df37-a4c6-4f66-9ecd-732039a9385d",
127
+ defaults: {
128
+ exclude_patterns: [".git", "node_modules", "*.tmp", ".pushwork"],
129
+ large_file_threshold: "100MB",
130
+ },
131
+ diff: {
132
+ show_binary: false,
133
+ },
134
+ sync: {
135
+ move_detection_threshold: 0.8,
136
+ prompt_threshold: 0.5,
137
+ auto_sync: false,
138
+ parallel_operations: 4,
139
+ },
140
+ };
141
+
142
+ // Merge configurations: default < global < local
143
+ let merged = { ...defaultConfig };
144
+
145
+ if (globalConfig) {
146
+ merged = this.mergeConfigs(merged, globalConfig);
147
+ }
148
+
149
+ if (localConfig) {
150
+ merged = this.mergeConfigs(merged, localConfig);
151
+ }
152
+
153
+ return merged;
154
+ }
155
+
156
+ /**
157
+ * Merge two configuration objects
158
+ */
159
+ private mergeConfigs(
160
+ base: DirectoryConfig,
161
+ override: Partial<DirectoryConfig> | GlobalConfig
162
+ ): DirectoryConfig {
163
+ const merged = { ...base };
164
+
165
+ if ("sync_server" in override && override.sync_server !== undefined) {
166
+ merged.sync_server = override.sync_server;
167
+ }
168
+
169
+ if (
170
+ "sync_server_storage_id" in override &&
171
+ override.sync_server_storage_id !== undefined
172
+ ) {
173
+ merged.sync_server_storage_id = override.sync_server_storage_id;
174
+ }
175
+
176
+ if ("sync_enabled" in override && override.sync_enabled !== undefined) {
177
+ merged.sync_enabled = override.sync_enabled;
178
+ }
179
+
180
+ // Handle GlobalConfig structure
181
+ if ("exclude_patterns" in override && override.exclude_patterns) {
182
+ merged.defaults.exclude_patterns = override.exclude_patterns;
183
+ }
184
+
185
+ if ("large_file_threshold" in override && override.large_file_threshold) {
186
+ merged.defaults.large_file_threshold = override.large_file_threshold;
187
+ }
188
+
189
+ // Handle DirectoryConfig structure
190
+ if ("defaults" in override && override.defaults) {
191
+ merged.defaults = { ...merged.defaults, ...override.defaults };
192
+ }
193
+
194
+ if ("diff" in override && override.diff) {
195
+ // Merge diff settings, ensuring show_binary has a default
196
+ merged.diff = {
197
+ ...merged.diff,
198
+ ...override.diff,
199
+ show_binary: override.diff.show_binary ?? merged.diff.show_binary,
200
+ };
201
+ }
202
+
203
+ if ("sync" in override && override.sync) {
204
+ merged.sync = { ...merged.sync, ...override.sync };
205
+ }
206
+
207
+ return merged;
208
+ }
209
+
210
+ /**
211
+ * Create default global configuration
212
+ */
213
+ async createDefaultGlobal(): Promise<void> {
214
+ const defaultGlobal: GlobalConfig = {
215
+ exclude_patterns: [
216
+ ".git",
217
+ "node_modules",
218
+ "*.tmp",
219
+ ".DS_Store",
220
+ ".pushwork",
221
+ ],
222
+ large_file_threshold: "100MB",
223
+ sync_server: "wss://sync3.automerge.org",
224
+ sync_server_storage_id: "3760df37-a4c6-4f66-9ecd-732039a9385d",
225
+ diff: {
226
+ show_binary: false,
227
+ },
228
+ sync: {
229
+ move_detection_threshold: 0.8,
230
+ prompt_threshold: 0.5,
231
+ auto_sync: false,
232
+ parallel_operations: 4,
233
+ },
234
+ };
235
+
236
+ await this.saveGlobal(defaultGlobal);
237
+ }
238
+
239
+ /**
240
+ * Check if global configuration exists
241
+ */
242
+ async globalConfigExists(): Promise<boolean> {
243
+ return await pathExists(this.getGlobalConfigPath());
244
+ }
245
+
246
+ /**
247
+ * Check if local configuration exists
248
+ */
249
+ async localConfigExists(): Promise<boolean> {
250
+ if (!this.workingDir) return false;
251
+ return await pathExists(this.getLocalConfigPath());
252
+ }
253
+
254
+ /**
255
+ * Get configuration value by path (e.g., 'sync.auto_sync')
256
+ */
257
+ async getValue(keyPath: string): Promise<any> {
258
+ const config = await this.getMerged();
259
+
260
+ const keys = keyPath.split(".");
261
+ let value: any = config;
262
+
263
+ for (const key of keys) {
264
+ if (value && typeof value === "object" && key in value) {
265
+ value = value[key];
266
+ } else {
267
+ return undefined;
268
+ }
269
+ }
270
+
271
+ return value;
272
+ }
273
+
274
+ /**
275
+ * Set configuration value by path
276
+ */
277
+ async setValue(keyPath: string, value: any): Promise<void> {
278
+ const config = (await this.load()) || ({} as DirectoryConfig);
279
+
280
+ const keys = keyPath.split(".");
281
+ let current: any = config;
282
+
283
+ // Navigate to the parent of the target key
284
+ for (let i = 0; i < keys.length - 1; i++) {
285
+ const key = keys[i];
286
+ if (!(key in current) || typeof current[key] !== "object") {
287
+ current[key] = {};
288
+ }
289
+ current = current[key];
290
+ }
291
+
292
+ // Set the value
293
+ const finalKey = keys[keys.length - 1];
294
+ current[finalKey] = value;
295
+
296
+ await this.save(config);
297
+ }
298
+
299
+ /**
300
+ * Validate configuration
301
+ */
302
+ validate(config: DirectoryConfig): { valid: boolean; errors: string[] } {
303
+ const errors: string[] = [];
304
+
305
+ if (config.sync?.move_detection_threshold !== undefined) {
306
+ if (
307
+ config.sync.move_detection_threshold < 0 ||
308
+ config.sync.move_detection_threshold > 1
309
+ ) {
310
+ errors.push("move_detection_threshold must be between 0 and 1");
311
+ }
312
+ }
313
+
314
+ if (config.sync?.prompt_threshold !== undefined) {
315
+ if (
316
+ config.sync.prompt_threshold < 0 ||
317
+ config.sync.prompt_threshold > 1
318
+ ) {
319
+ errors.push("prompt_threshold must be between 0 and 1");
320
+ }
321
+ }
322
+
323
+ if (config.sync?.parallel_operations !== undefined) {
324
+ if (config.sync.parallel_operations < 1) {
325
+ errors.push("parallel_operations must be at least 1");
326
+ }
327
+ }
328
+
329
+ return {
330
+ valid: errors.length === 0,
331
+ errors,
332
+ };
333
+ }
334
+ }