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
package/src/cli.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { Command } from "commander";
3
+ import { StorageId } from "@automerge/automerge-repo";
4
+ import { Command } from "@commander-js/extra-typings";
4
5
  import chalk from "chalk";
5
6
  import {
6
7
  init,
@@ -12,319 +13,392 @@ import {
12
13
  checkout,
13
14
  commit,
14
15
  url,
15
- debug,
16
- } from "./cli/commands";
16
+ rm,
17
+ ls,
18
+ config,
19
+ watch,
20
+ } from "./commands";
17
21
 
18
- /**
19
- * Wrapper for command actions with consistent error handling
20
- */
21
- function withErrorHandling<T extends any[], R>(
22
- fn: (...args: T) => Promise<R>
23
- ): (...args: T) => Promise<void> {
24
- return async (...args: T): Promise<void> => {
25
- try {
26
- await fn(...args);
27
- } catch (error) {
28
- console.error(chalk.red(`Error: ${error}`));
29
- process.exit(1);
30
- }
31
- };
32
- }
33
-
34
- const program = new Command();
35
-
36
- // get the version from the package.json
37
22
  const version = require("../package.json").version;
38
-
39
- program
23
+ const program = new Command()
40
24
  .name("pushwork")
41
25
  .description("Bidirectional directory synchronization using Automerge CRDTs")
42
- .version(version);
26
+ .version(version, "-V, --version", "output the version number");
43
27
 
44
28
  // Init command
45
29
  program
46
30
  .command("init")
47
- .description("Initialize sync in directory")
48
- .argument("<path>", "Directory path to initialize")
49
- .option(
50
- "--sync-server <url>",
51
- "Custom sync server URL (must be used with --sync-server-storage-id)"
31
+ .summary("Initialize sync in a directory")
32
+ .argument(
33
+ "[path]",
34
+ "Directory path to initialize (default: current directory)",
35
+ "."
52
36
  )
53
37
  .option(
54
- "--sync-server-storage-id <id>",
55
- "Custom sync server storage ID (must be used with --sync-server)"
38
+ "--sync-server <url> <storage-id...>",
39
+ "Custom sync server URL and storage ID"
56
40
  )
57
- .addHelpText(
58
- "after",
59
- `
60
- Examples:
61
- pushwork init ./my-folder
62
- pushwork init ./my-folder --sync-server ws://localhost:3030 --sync-server-storage-id 1d89eba7-f7a4-4e8e-80f2-5f4e2406f507
63
-
64
- Note: Custom sync server options must always be used together.`
65
- )
66
- .action(
67
- withErrorHandling(async (path: string, options) => {
68
- // Validate that both sync server options are provided together
69
- const hasSyncServer = !!options.syncServer;
70
- const hasSyncServerStorageId = !!options.syncServerStorageId;
71
-
72
- if (hasSyncServer && !hasSyncServerStorageId) {
73
- console.error(
74
- chalk.red("Error: --sync-server requires --sync-server-storage-id")
75
- );
76
- console.error(
77
- chalk.yellow("Both arguments must be provided together.")
78
- );
79
- process.exit(1);
80
- }
81
-
82
- if (hasSyncServerStorageId && !hasSyncServer) {
83
- console.error(
84
- chalk.red("Error: --sync-server-storage-id requires --sync-server")
85
- );
86
- console.error(
87
- chalk.yellow("Both arguments must be provided together.")
88
- );
89
- process.exit(1);
90
- }
91
-
92
- await init(path, options.syncServer, options.syncServerStorageId);
93
- })
94
- );
41
+ .action(async (path, opts) => {
42
+ const [syncServer, syncServerStorageId] = validateSyncServer(
43
+ opts.syncServer
44
+ );
45
+ await init(path, { syncServer, syncServerStorageId });
46
+ });
95
47
 
96
48
  // Clone command
97
49
  program
98
50
  .command("clone")
99
- .description("Clone an existing synced directory")
100
- .argument("<url>", "AutomergeUrl of root directory to clone")
101
- .argument("<path>", "Target directory path")
102
- .option("--force", "Overwrite existing directory")
103
- .option(
104
- "--sync-server <url>",
105
- "Custom sync server URL (must be used with --sync-server-storage-id)"
51
+ .summary("Clone an existing synced directory")
52
+ .argument(
53
+ "<url>",
54
+ "AutomergeUrl of root directory to clone (format: automerge:XXXXX)"
106
55
  )
56
+ .argument("<path>", "Target directory path")
57
+ .option("-f, --force", "Overwrite existing directory", false)
107
58
  .option(
108
- "--sync-server-storage-id <id>",
109
- "Custom sync server storage ID (must be used with --sync-server)"
59
+ "--sync-server <url> <storage-id...>",
60
+ "Custom sync server URL and storage ID"
110
61
  )
111
- .addHelpText(
112
- "after",
113
- `
114
- Examples:
115
- pushwork clone automerge:abc123 ./my-clone
116
- pushwork clone automerge:abc123 ./my-clone --force
117
- pushwork clone automerge:abc123 ./my-clone --sync-server ws://localhost:3030 --sync-server-storage-id 1d89eba7-f7a4-4e8e-80f2-5f4e2406f507
118
-
119
- Note: Custom sync server options must always be used together.`
120
- )
121
- .action(
122
- withErrorHandling(async (url: string, path: string, options) => {
123
- // Validate that both sync server options are provided together
124
- const hasSyncServer = !!options.syncServer;
125
- const hasSyncServerStorageId = !!options.syncServerStorageId;
126
-
127
- if (hasSyncServer && !hasSyncServerStorageId) {
128
- console.error(
129
- chalk.red("Error: --sync-server requires --sync-server-storage-id")
130
- );
131
- console.error(
132
- chalk.yellow("Both arguments must be provided together.")
133
- );
134
- process.exit(1);
135
- }
136
-
137
- if (hasSyncServerStorageId && !hasSyncServer) {
138
- console.error(
139
- chalk.red("Error: --sync-server-storage-id requires --sync-server")
140
- );
141
- console.error(
142
- chalk.yellow("Both arguments must be provided together.")
143
- );
144
- process.exit(1);
145
- }
146
-
147
- await clone(url, path, {
148
- force: options.force || false,
149
- dryRun: false,
150
- verbose: false,
151
- syncServer: options.syncServer,
152
- syncServerStorageId: options.syncServerStorageId,
153
- });
154
- })
155
- );
62
+ .option("-v, --verbose", "Verbose output", false)
63
+ .action(async (url, path, opts) => {
64
+ const [syncServer, syncServerStorageId] = validateSyncServer(
65
+ opts.syncServer
66
+ );
67
+ await clone(url, path, {
68
+ force: opts.force,
69
+ verbose: opts.verbose,
70
+ syncServer,
71
+ syncServerStorageId,
72
+ });
73
+ });
156
74
 
157
75
  // Commit command
158
76
  program
159
77
  .command("commit")
160
- .description("Commit local changes (no network sync)")
161
- .argument("[path]", "Directory path to commit", ".")
162
- .option("--dry-run", "Show what would be committed without applying changes")
163
- .action(
164
- withErrorHandling(async (path: string, options) => {
165
- await commit(path, options.dryRun || false);
166
- })
167
- );
78
+ .summary("Save local changes to Automerge documents")
79
+ .argument(
80
+ "[path]",
81
+ "Directory path to commit (default: current directory)",
82
+ "."
83
+ )
84
+ .action(async (path, _opts) => {
85
+ await commit(path);
86
+ });
168
87
 
169
88
  // Sync command
170
89
  program
171
90
  .command("sync")
172
- .description("Run full bidirectional synchronization")
173
- .option("--dry-run", "Show what would be done without applying changes")
174
- .option("-v, --verbose", "Verbose output")
175
- .action(
176
- withErrorHandling(async (options) => {
177
- await sync({
178
- dryRun: options.dryRun || false,
179
- verbose: options.verbose || false,
180
- });
181
- })
182
- );
91
+ .summary("Run full bidirectional synchronization")
92
+ .argument(
93
+ "[path]",
94
+ "Directory path to sync (default: current directory)",
95
+ "."
96
+ )
97
+ .option(
98
+ "--dry-run",
99
+ "Show what would be done without applying changes",
100
+ false
101
+ )
102
+ .option("-v, --verbose", "Verbose output", false)
103
+ .action(async (path, opts) => {
104
+ await sync(path, {
105
+ dryRun: opts.dryRun,
106
+ verbose: opts.verbose,
107
+ });
108
+ });
183
109
 
184
110
  // Diff command
185
111
  program
186
112
  .command("diff")
187
- .description("Show changes in working directory since last sync")
188
- .argument("[path]", "Limit diff to specific path", ".")
189
- .option("--tool <tool>", "Use external diff tool (meld, vimdiff, etc.)")
190
- .option("--name-only", "Show only changed file names")
191
- .action(
192
- withErrorHandling(async (path: string, options) => {
193
- await diff(path, {
194
- tool: options.tool,
195
- nameOnly: options.nameOnly || false,
196
- dryRun: false,
197
- verbose: false,
198
- });
199
- })
200
- );
113
+ .summary("Show changes in working directory")
114
+ .argument(
115
+ "[path]",
116
+ "Limit diff to specific path (default: current directory)",
117
+ "."
118
+ )
119
+ .option("--name-only", "Show only changed file names", false)
120
+ .action(async (path, opts) => {
121
+ await diff(path, {
122
+ nameOnly: opts.nameOnly,
123
+ });
124
+ });
201
125
 
202
126
  // Status command
203
127
  program
204
128
  .command("status")
205
- .description("Show sync status summary")
206
- .action(
207
- withErrorHandling(async (options) => {
208
- await status();
209
- })
210
- );
129
+ .summary("Show sync status summary")
130
+ .argument("[path]", "Directory path (default: current directory)", ".")
131
+ .option(
132
+ "-v, --verbose",
133
+ "Show detailed status including document info and all tracked files",
134
+ false
135
+ )
136
+ .action(async (path, opts) => {
137
+ await status(path, {
138
+ verbose: opts.verbose,
139
+ });
140
+ });
211
141
 
212
142
  // Log command
213
143
  program
214
144
  .command("log")
215
- .description("Show sync history")
216
- .argument("[path]", "Show history for specific file or directory", ".")
217
- .option("--oneline", "Compact one-line per sync format")
145
+ .summary("Show sync history (experimental)")
146
+ .argument(
147
+ "[path]",
148
+ "Show history for specific file or directory (default: current directory)",
149
+ "."
150
+ )
151
+ .option("--oneline", "Compact one-line per sync format", false)
218
152
  .option("--since <date>", "Show syncs since date")
219
153
  .option("--limit <n>", "Limit number of syncs shown", "10")
220
- .action(
221
- withErrorHandling(async (path: string, options) => {
222
- await log(path, {
223
- oneline: options.oneline || false,
224
- since: options.since,
225
- limit: parseInt(options.limit),
226
- dryRun: false,
227
- verbose: false,
228
- });
229
- })
230
- );
154
+ .action(async (path, opts) => {
155
+ await log(path, {
156
+ oneline: opts.oneline,
157
+ since: opts.since,
158
+ limit: parseInt(opts.limit),
159
+ });
160
+ });
231
161
 
232
162
  // Checkout command
233
163
  program
234
164
  .command("checkout")
235
- .description("Restore directory to state from previous sync")
165
+ .summary("Restore to previous sync (experimental)")
236
166
  .argument("<sync-id>", "Sync ID to restore to")
237
- .argument("[path]", "Specific path to restore", ".")
238
- .option("-f, --force", "Force checkout even if there are uncommitted changes")
239
- .action(
240
- withErrorHandling(async (syncId: string, path: string, options) => {
241
- await checkout(syncId, path, {
242
- force: options.force || false,
243
- dryRun: false,
244
- verbose: false,
245
- });
246
- })
247
- );
167
+ .argument(
168
+ "[path]",
169
+ "Specific path to restore (default: current directory)",
170
+ "."
171
+ )
172
+ .option(
173
+ "-f, --force",
174
+ "Force checkout even if there are uncommitted changes",
175
+ false
176
+ )
177
+ .action(async (syncId, path, opts) => {
178
+ await checkout(syncId, path, {
179
+ force: opts.force,
180
+ });
181
+ });
248
182
 
249
183
  // URL command
250
184
  program
251
185
  .command("url")
252
- .description("Show the Automerge root URL for this repository")
253
- .argument("[path]", "Directory path", ".")
254
- .addHelpText(
255
- "after",
256
- `
257
- Examples:
258
- pushwork url # Show URL for current directory
259
- pushwork url ./repo # Show URL for specific directory
260
-
261
- Note: This command outputs only the URL, making it useful for scripts.`
186
+ .summary("Show the Automerge root URL")
187
+ .argument("[path]", "Directory path (default: current directory)", ".")
188
+ .action(async (path) => {
189
+ await url(path);
190
+ });
191
+
192
+ // Remove command
193
+ program
194
+ .command("rm")
195
+ .summary("Remove local pushwork data")
196
+ .argument("[path]", "Directory path (default: current directory)", ".")
197
+ .action(async (path) => {
198
+ await rm(path);
199
+ });
200
+
201
+ // List command
202
+ program
203
+ .command("ls")
204
+ .summary("List tracked files")
205
+ .argument("[path]", "Directory path (default: current directory)", ".")
206
+ .option("-v, --verbose", "Show with Automerge URLs", false)
207
+ .action(async (path, opts) => {
208
+ await ls(path, {
209
+ verbose: opts.verbose,
210
+ });
211
+ });
212
+
213
+ // Config command
214
+ program
215
+ .command("config")
216
+ .summary("View or edit configuration")
217
+ .argument("[path]", "Directory path (default: current directory)", ".")
218
+ .option("--list", "Show full configuration", false)
219
+ .option(
220
+ "--get <key>",
221
+ "Get specific config value (dot notation, e.g., sync.move_detection_threshold)"
262
222
  )
263
- .action(
264
- withErrorHandling(async (path: string) => {
265
- await url(path);
266
- })
267
- );
223
+ .action(async (path, opts) => {
224
+ await config(path, {
225
+ list: opts.list,
226
+ get: opts.get,
227
+ });
228
+ });
268
229
 
269
- // Debug command
230
+ // Watch command
270
231
  program
271
- .command("debug")
272
- .description("Show internal debug information including lastSyncAt timestamp")
273
- .argument("[path]", "Directory path", ".")
232
+ .command("watch")
233
+ .summary("Watch directory for changes, build, and sync")
234
+ .argument(
235
+ "[path]",
236
+ "Directory path to sync (default: current directory)",
237
+ "."
238
+ )
274
239
  .option(
275
- "-v, --verbose",
276
- "Show verbose debug information including full document contents"
240
+ "--script <command>",
241
+ "Build script to run before syncing",
242
+ "pnpm build"
277
243
  )
278
- .addHelpText(
279
- "after",
280
- `
281
- Examples:
282
- pushwork debug # Show debug info for current directory
283
- pushwork debug --verbose # Show verbose debug info including full document contents
284
- pushwork debug ./repo # Show debug info for specific directory
285
-
286
- This command displays internal document state, including the lastSyncAt timestamp
287
- that gets updated when sync operations make changes.`
244
+ .option(
245
+ "--dir <dir>",
246
+ "Directory to watch for changes (relative to working directory)",
247
+ "src"
288
248
  )
289
- .action(
290
- withErrorHandling(async (path: string, options) => {
291
- await debug(path, {
292
- verbose: options.verbose || false,
293
- });
249
+ .option("-v, --verbose", "Show build script output", false)
250
+ .action(async (path, opts) => {
251
+ await watch(path, {
252
+ script: opts.script,
253
+ watchDir: opts.dir,
254
+ verbose: opts.verbose,
255
+ });
256
+ });
257
+
258
+ // Completion command (hidden from help)
259
+ program.command("completion", { hidden: true }).action(() => {
260
+ // Generate completion dynamically from registered commands
261
+ const commands = program.commands
262
+ .filter((cmd) => cmd.name() !== "completion") // Exclude self
263
+ .map((cmd) => {
264
+ const name = cmd.name();
265
+ const desc = (cmd.summary() || cmd.description() || "").replace(
266
+ /'/g,
267
+ "\\'"
268
+ );
269
+ return `'${name}:${desc}'`;
294
270
  })
295
- );
271
+ .join(" ");
296
272
 
297
- // Global error handler
298
- process.on("unhandledRejection", (reason, promise) => {
299
- console.error(
300
- chalk.red("Unhandled Rejection at:"),
301
- promise,
302
- chalk.red("reason:"),
303
- reason
304
- );
305
- process.exit(1);
273
+ // Generate option completions for each command
274
+ const commandCases = program.commands
275
+ .filter((cmd) => cmd.name() !== "completion")
276
+ .map((cmd) => {
277
+ const options = cmd.options
278
+ .filter((opt) => opt.flags !== "-h, --help") // Exclude help
279
+ .map((opt) => {
280
+ // Parse flags like "-v, --verbose" or "--dry-run"
281
+ const flags = opt.flags.split(",").map((f) => f.trim());
282
+ const desc = (opt.description || "")
283
+ .replace(/'/g, "\\'")
284
+ .replace(/\n/g, " ");
285
+
286
+ // For options with arguments like "--sync-server <url>"
287
+ // Extract just the flag part
288
+ const cleanFlags = flags.map((f) => f.split(/\s+/)[0]);
289
+
290
+ if (cleanFlags.length > 1) {
291
+ // Multiple flags (short and long): '(-v --verbose)'{-v,--verbose}'[description]'
292
+ const short = cleanFlags[0];
293
+ const long = cleanFlags[1];
294
+ return `'(${short} ${long})'{${short},${long}}'[${desc}]'`;
295
+ } else {
296
+ // Single flag: '--flag[description]'
297
+ return `'${cleanFlags[0]}[${desc}]'`;
298
+ }
299
+ })
300
+ .join(" \\\n ");
301
+
302
+ return options
303
+ ? ` ${cmd.name()})
304
+ _arguments \\
305
+ ${options}
306
+ ;;`
307
+ : "";
308
+ })
309
+ .filter(Boolean)
310
+ .join("\n");
311
+
312
+ const completionScript = `
313
+ # pushwork completion for zsh
314
+ _pushwork() {
315
+ local -a commands
316
+ commands=(${commands})
317
+
318
+ _arguments -C \\
319
+ '1: :->command' \\
320
+ '*::arg:->args'
321
+
322
+ case $state in
323
+ command)
324
+ _describe 'command' commands
325
+ ;;
326
+ args)
327
+ case $words[1] in
328
+ ${commandCases}
329
+ esac
330
+ ;;
331
+ esac
332
+ }
333
+
334
+ compdef _pushwork pushwork
335
+ `.trim();
336
+
337
+ console.log(completionScript);
306
338
  });
307
339
 
308
- process.on("uncaughtException", (error) => {
309
- // Ignore WebSocket errors during shutdown - they're non-critical
310
- const errorMessage = error instanceof Error ? error.message : String(error);
311
- if (
312
- errorMessage.includes("WebSocket") ||
313
- errorMessage.includes("connection was established") ||
314
- errorMessage.includes("was closed")
315
- ) {
316
- // Silently ignore WebSocket shutdown errors
317
- return;
340
+ // Helper to validate and extract sync server options
341
+ function validateSyncServer(
342
+ syncServerOpt: string[] | undefined
343
+ ): [string | undefined, StorageId | undefined] {
344
+ if (!syncServerOpt) {
345
+ return [undefined, undefined];
318
346
  }
319
347
 
320
- console.error(chalk.red("Uncaught Exception:"), error);
348
+ if (syncServerOpt.length < 2) {
349
+ console.error(
350
+ chalk.red("Error: --sync-server requires both URL and storage ID")
351
+ );
352
+ process.exit(1);
353
+ }
354
+
355
+ const [syncServer, syncServerStorageId] = syncServerOpt;
356
+ return [syncServer, syncServerStorageId as StorageId];
357
+ }
358
+
359
+ process.on("unhandledRejection", (error) => {
360
+ console.log(chalk.bgRed.white(" ERROR "));
361
+ if (error instanceof Error && error.stack) {
362
+ console.log(chalk.red(error.stack));
363
+ } else {
364
+ console.error(chalk.red(error));
365
+ }
321
366
  process.exit(1);
322
367
  });
323
368
 
324
- // Parse arguments
325
- program.parse();
369
+ // Configure help colors using Commander v13's built-in color support
370
+ program
371
+ .configureHelp({
372
+ styleTitle: (str) => chalk.bold(str),
373
+ styleCommandText: (str) => chalk.white(str),
374
+ styleCommandDescription: (str) => chalk.dim(str),
375
+ styleOptionText: (str) => chalk.green(str),
376
+ styleArgumentText: (str) => chalk.cyan(str),
377
+ subcommandTerm: (cmd) => {
378
+ const opts = cmd.options
379
+ .filter((opt) => opt.flags !== "-h, --help")
380
+ .map((opt) => opt.short || opt.long)
381
+ .join(", ");
326
382
 
327
- // Show help if no arguments provided
328
- if (!process.argv.slice(2).length) {
329
- program.outputHelp();
330
- }
383
+ const name = chalk.white(cmd.name());
384
+ const args = cmd.registeredArguments
385
+ .map((arg) =>
386
+ arg.required
387
+ ? chalk.cyan(`<${arg.name()}>`)
388
+ : chalk.dim(`[${arg.name()}]`)
389
+ )
390
+ .join(" ");
391
+
392
+ return [name, args, opts && chalk.dim(`[${opts}]`)]
393
+ .filter(Boolean)
394
+ .join(" ");
395
+ },
396
+ })
397
+ .addHelpText(
398
+ "after",
399
+ chalk.dim(
400
+ '\nEnable tab completion by adding this to your ~/.zshrc:\neval "$(pushwork completion)"'
401
+ )
402
+ );
403
+
404
+ program.parseAsync();