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,599 @@
1
+ import "../polyfills";
2
+ import React, { useState, useRef, useEffect } from "react";
3
+ import { useDocHandle } from "@automerge/automerge-repo-react-hooks";
4
+ import { EditorProps } from "@patchwork/sdk";
5
+ // Simple browser-only sync - no pushwork imports needed
6
+ import {
7
+ FolderOpen,
8
+ RefreshCw,
9
+ Settings,
10
+ AlertCircle,
11
+ CheckCircle,
12
+ Clock,
13
+ File,
14
+ Folder,
15
+ Upload,
16
+ } from "lucide-react";
17
+ import { FolderDoc, SyncStatus, SyncSettings } from "../types";
18
+
19
+ interface BrowserSyncState {
20
+ directoryHandle: any;
21
+ status: SyncStatus;
22
+ settings: SyncSettings;
23
+ files: Array<{
24
+ name: string;
25
+ type: "file" | "directory";
26
+ size?: number;
27
+ lastModified?: Date;
28
+ }>;
29
+ showSettings: boolean;
30
+ }
31
+
32
+ const BrowserSyncTool: React.FC<EditorProps<FolderDoc, unknown>> = ({
33
+ docUrl,
34
+ }) => {
35
+ const handle = useDocHandle<FolderDoc>(docUrl);
36
+
37
+ // Debug logging
38
+ console.log("🔍 SimpleBrowserSyncTool initialized with:", {
39
+ docUrl,
40
+ handle,
41
+ doc: handle?.doc(),
42
+ isReady: handle?.isReady(),
43
+ });
44
+ const [state, setState] = useState<BrowserSyncState>({
45
+ directoryHandle: null,
46
+ status: {
47
+ isConnected: false,
48
+ hasDirectoryAccess: false,
49
+ lastSync: null,
50
+ filesCount: 0,
51
+ syncInProgress: false,
52
+ error: null,
53
+ },
54
+ settings: {
55
+ autoSync: false,
56
+ syncInterval: 30,
57
+ excludePatterns: [".git", "node_modules", "*.tmp", ".pushwork"],
58
+ syncServerUrl: "wss://sync3.automerge.org",
59
+ syncServerStorageId: "3760df37-a4c6-4f66-9ecd-732039a9385d",
60
+ },
61
+ files: [],
62
+ showSettings: false,
63
+ });
64
+
65
+ // Monitor document readiness
66
+ useEffect(() => {
67
+ console.log("📡 Handle changed:", {
68
+ handle,
69
+ doc: handle?.doc(),
70
+ });
71
+ }, [handle]);
72
+
73
+ const isFileSystemAccessSupported = (): boolean => {
74
+ try {
75
+ const hasAPI =
76
+ typeof window !== "undefined" &&
77
+ typeof (window as any).showDirectoryPicker === "function";
78
+ // Note: We only need showDirectoryPicker for folder sync, not showFilePicker
79
+
80
+ // File System Access API requires HTTPS or localhost
81
+ const isSecureContext =
82
+ window.isSecureContext ||
83
+ window.location.protocol === "https:" ||
84
+ window.location.hostname === "localhost" ||
85
+ window.location.hostname === "127.0.0.1";
86
+
87
+ return hasAPI && isSecureContext;
88
+ } catch (error) {
89
+ console.error("Error checking File System Access API support:", error);
90
+ return false;
91
+ }
92
+ };
93
+
94
+ const handleSelectFolder = async () => {
95
+ if (!isFileSystemAccessSupported()) {
96
+ setState((prev) => ({
97
+ ...prev,
98
+ status: {
99
+ ...prev.status,
100
+ error: "File System Access API not supported",
101
+ },
102
+ }));
103
+ return;
104
+ }
105
+
106
+ try {
107
+ setState((prev) => ({
108
+ ...prev,
109
+ status: { ...prev.status, error: null },
110
+ }));
111
+
112
+ console.log("📁 Opening directory picker...");
113
+ const directoryHandle = await (window as any).showDirectoryPicker({
114
+ mode: "readwrite",
115
+ id: "pushwork-sync-folder",
116
+ });
117
+
118
+ console.log("✅ Directory selected:", directoryHandle.name);
119
+ console.log("🔍 Directory handle details:", directoryHandle);
120
+
121
+ setState((prev) => ({
122
+ ...prev,
123
+ directoryHandle,
124
+ status: {
125
+ ...prev.status,
126
+ hasDirectoryAccess: true,
127
+ isConnected: true,
128
+ },
129
+ }));
130
+
131
+ // Load file list
132
+ await updateFileList(directoryHandle);
133
+ } catch (error) {
134
+ setState((prev) => ({
135
+ ...prev,
136
+ status: {
137
+ ...prev.status,
138
+ error: `Failed to select folder: ${error}`,
139
+ hasDirectoryAccess: false,
140
+ },
141
+ }));
142
+ }
143
+ };
144
+
145
+ const updateFileList = async (directoryHandle: any) => {
146
+ if (!directoryHandle) return;
147
+
148
+ try {
149
+ const files: any[] = [];
150
+
151
+ // Simple directory listing without recursion for demo
152
+ for await (const [name, handle] of directoryHandle.entries()) {
153
+ if (handle.kind === "file") {
154
+ const file = await handle.getFile();
155
+ files.push({
156
+ name,
157
+ type: "file",
158
+ size: file.size,
159
+ lastModified: new Date(file.lastModified),
160
+ });
161
+ } else if (handle.kind === "directory") {
162
+ files.push({
163
+ name,
164
+ type: "directory",
165
+ size: 0,
166
+ lastModified: new Date(),
167
+ });
168
+ }
169
+ }
170
+
171
+ setState((prev) => ({
172
+ ...prev,
173
+ files,
174
+ status: { ...prev.status, filesCount: files.length },
175
+ }));
176
+ } catch (error) {
177
+ console.error("Failed to update file list:", error);
178
+ }
179
+ };
180
+
181
+ const handleSync = async () => {
182
+ if (!state.directoryHandle) {
183
+ setState((prev) => ({
184
+ ...prev,
185
+ status: { ...prev.status, error: "No folder selected" },
186
+ }));
187
+ return;
188
+ }
189
+
190
+ setState((prev) => ({
191
+ ...prev,
192
+ status: { ...prev.status, syncInProgress: true, error: null },
193
+ }));
194
+
195
+ try {
196
+ console.log("🔄 Starting simple browser sync...");
197
+ console.log("📁 Directory handle:", state.directoryHandle);
198
+ console.log("📄 Patchwork doc URL:", docUrl);
199
+
200
+ if (!handle?.doc()) {
201
+ throw new Error("No Patchwork document available");
202
+ }
203
+
204
+ // Read files from the directory using File System Access API
205
+ const entries: Array<{
206
+ name: string;
207
+ type: "file" | "folder";
208
+ url?: string;
209
+ }> = [];
210
+
211
+ for await (const [name, entryHandle] of state.directoryHandle.entries()) {
212
+ console.log(`📂 Found: ${name} (${entryHandle.kind})`);
213
+
214
+ if (entryHandle.kind === "file") {
215
+ entries.push({
216
+ name,
217
+ type: "file",
218
+ url: `file://${name}`, // Placeholder URL - in real implementation would create actual Automerge file docs
219
+ });
220
+ } else {
221
+ entries.push({
222
+ name,
223
+ type: "folder",
224
+ url: `folder://${name}`, // Placeholder URL - in real implementation would create actual Automerge folder docs
225
+ });
226
+ }
227
+ }
228
+
229
+ console.log(`📊 Found ${entries.length} items in directory`);
230
+
231
+ // Update the Patchwork document with proper folder structure
232
+ handle.change((doc: FolderDoc) => {
233
+ // Ensure proper patchwork folder structure
234
+ if (!doc["@patchwork"]) {
235
+ doc["@patchwork"] = { type: "folder" };
236
+ }
237
+ if (!doc.docs) {
238
+ doc.docs = [];
239
+ }
240
+
241
+ // Clear and rebuild the docs array
242
+ doc.docs = entries.map((entry) => ({
243
+ name: entry.name,
244
+ type: entry.type,
245
+ url: entry.url as any, // Type assertion for the placeholder URLs
246
+ }));
247
+
248
+ console.log(
249
+ `✅ Updated Patchwork document with ${entries.length} entries`
250
+ );
251
+ console.log("📄 Document structure:", doc);
252
+ });
253
+
254
+ setState((prev) => ({
255
+ ...prev,
256
+ status: {
257
+ ...prev.status,
258
+ syncInProgress: false,
259
+ lastSync: new Date(),
260
+ error: null,
261
+ filesCount: entries.length,
262
+ },
263
+ }));
264
+
265
+ await updateFileList(state.directoryHandle);
266
+ } catch (error) {
267
+ console.error("❌ Simple sync failed:", error);
268
+ setState((prev) => ({
269
+ ...prev,
270
+ status: {
271
+ ...prev.status,
272
+ syncInProgress: false,
273
+ error: `Sync failed: ${error}`,
274
+ },
275
+ }));
276
+ }
277
+ };
278
+
279
+ const handleSettingsChange = (key: keyof SyncSettings, value: any) => {
280
+ setState((prev) => ({
281
+ ...prev,
282
+ settings: { ...prev.settings, [key]: value },
283
+ }));
284
+ };
285
+
286
+ const formatFileSize = (bytes?: number): string => {
287
+ if (!bytes) return "";
288
+ const units = ["B", "KB", "MB", "GB"];
289
+ let size = bytes;
290
+ let unitIndex = 0;
291
+
292
+ while (size >= 1024 && unitIndex < units.length - 1) {
293
+ size /= 1024;
294
+ unitIndex++;
295
+ }
296
+
297
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
298
+ };
299
+
300
+ if (!isFileSystemAccessSupported()) {
301
+ const hasAPI = typeof (window as any).showDirectoryPicker === "function";
302
+ const isSecureContext =
303
+ window.isSecureContext ||
304
+ window.location.protocol === "https:" ||
305
+ window.location.hostname === "localhost" ||
306
+ window.location.hostname === "127.0.0.1";
307
+
308
+ let errorMessage = "";
309
+ let suggestion = "";
310
+
311
+ if (!hasAPI) {
312
+ errorMessage = "File System Access API Not Available";
313
+ suggestion =
314
+ "Please use Chrome 86+, Edge 86+, or Safari 15.2+. Firefox doesn't support this API yet.";
315
+ } else if (!isSecureContext) {
316
+ errorMessage = "Secure Context Required";
317
+ suggestion =
318
+ "The File System Access API requires HTTPS or localhost. Please use https:// or run on localhost.";
319
+ } else {
320
+ errorMessage = "File System Access Not Supported";
321
+ suggestion =
322
+ "Your browser configuration doesn't allow File System Access.";
323
+ }
324
+
325
+ return (
326
+ <div className="browser-sync-tool">
327
+ <div className="empty-state">
328
+ <AlertCircle className="empty-state-icon" />
329
+ <h3 className="empty-state-title">{errorMessage}</h3>
330
+ <p className="empty-state-description">{suggestion}</p>
331
+ <div
332
+ style={{ marginTop: "1rem", fontSize: "0.75rem", color: "#9ca3af" }}
333
+ >
334
+ <strong>Debug Info:</strong>
335
+ <br />
336
+ Browser: {navigator.userAgent.split(" ").slice(-2).join(" ")}
337
+ <br />
338
+ Protocol: {window.location.protocol}
339
+ <br />
340
+ Host: {window.location.hostname}
341
+ <br />
342
+ Secure Context: {isSecureContext ? "Yes" : "No"}
343
+ <br />
344
+ API Available: {hasAPI ? "Yes" : "No"}
345
+ </div>
346
+ </div>
347
+ </div>
348
+ );
349
+ }
350
+
351
+ return (
352
+ <div className="browser-sync-tool">
353
+ <div className="browser-sync-header">
354
+ <h1 className="browser-sync-title">Browser Folder Sync</h1>
355
+ <p className="browser-sync-subtitle">
356
+ Synchronize a local folder with this Patchwork document
357
+ </p>
358
+ </div>
359
+
360
+ <div className="browser-sync-content">
361
+ <div className="browser-sync-main">
362
+ {/* Status Card */}
363
+ <div className="status-card">
364
+ <div className="status-header">
365
+ <div
366
+ className={`status-indicator ${
367
+ state.status.hasDirectoryAccess && state.status.isConnected
368
+ ? "connected"
369
+ : state.status.syncInProgress
370
+ ? "pending"
371
+ : "disconnected"
372
+ }`}
373
+ />
374
+ <h3 className="status-title">Sync Status</h3>
375
+ </div>
376
+
377
+ <div style={{ marginBottom: "0.5rem" }}>
378
+ <strong>Connection:</strong>{" "}
379
+ {state.status.isConnected ? "Connected" : "Disconnected"}
380
+ </div>
381
+ <div style={{ marginBottom: "0.5rem" }}>
382
+ <strong>Folder Access:</strong>{" "}
383
+ {state.status.hasDirectoryAccess ? "Granted" : "Not selected"}
384
+ </div>
385
+ <div style={{ marginBottom: "0.5rem" }}>
386
+ <strong>Files:</strong> {state.status.filesCount}
387
+ </div>
388
+ {state.status.lastSync && (
389
+ <div style={{ marginBottom: "0.5rem" }}>
390
+ <strong>Last Sync:</strong>{" "}
391
+ {state.status.lastSync.toLocaleString()}
392
+ </div>
393
+ )}
394
+ </div>
395
+
396
+ {/* Error Message */}
397
+ {state.status.error && (
398
+ <div className="error-message">
399
+ <AlertCircle
400
+ style={{
401
+ width: "16px",
402
+ height: "16px",
403
+ display: "inline",
404
+ marginRight: "0.5rem",
405
+ }}
406
+ />
407
+ {state.status.error}
408
+ </div>
409
+ )}
410
+
411
+ {/* Sync Progress */}
412
+ {state.status.syncInProgress && (
413
+ <div className="sync-progress">
414
+ <RefreshCw
415
+ className="sync-spinner"
416
+ style={{ width: "16px", height: "16px" }}
417
+ />
418
+ Synchronizing files...
419
+ </div>
420
+ )}
421
+
422
+ {/* Main Actions */}
423
+ <div style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}>
424
+ <button
425
+ className="button primary"
426
+ onClick={handleSelectFolder}
427
+ disabled={state.status.syncInProgress}
428
+ >
429
+ <FolderOpen style={{ width: "16px", height: "16px" }} />
430
+ {state.status.hasDirectoryAccess
431
+ ? "Change Folder"
432
+ : "Select Folder"}
433
+ </button>
434
+
435
+ <button
436
+ className="button"
437
+ onClick={handleSync}
438
+ disabled={
439
+ !state.status.hasDirectoryAccess || state.status.syncInProgress
440
+ }
441
+ >
442
+ <RefreshCw style={{ width: "16px", height: "16px" }} />
443
+ Sync Now
444
+ </button>
445
+
446
+ <button
447
+ className="button"
448
+ onClick={() =>
449
+ setState((prev) => ({
450
+ ...prev,
451
+ showSettings: !prev.showSettings,
452
+ }))
453
+ }
454
+ >
455
+ <Settings style={{ width: "16px", height: "16px" }} />
456
+ Settings
457
+ </button>
458
+ </div>
459
+
460
+ {/* File List */}
461
+ {state.files.length > 0 ? (
462
+ <div>
463
+ <h3
464
+ style={{
465
+ marginBottom: "0.5rem",
466
+ fontSize: "1rem",
467
+ fontWeight: "500",
468
+ }}
469
+ >
470
+ Files ({state.files.length})
471
+ </h3>
472
+ <div className="file-list">
473
+ {state.files.slice(0, 50).map((file, index) => (
474
+ <div key={index} className="file-item">
475
+ {file.type === "directory" ? (
476
+ <Folder className="file-icon" />
477
+ ) : (
478
+ <File className="file-icon" />
479
+ )}
480
+ <span className="file-name">{file.name}</span>
481
+ {file.size && (
482
+ <span className="file-size">
483
+ {formatFileSize(file.size)}
484
+ </span>
485
+ )}
486
+ </div>
487
+ ))}
488
+ {state.files.length > 50 && (
489
+ <div
490
+ className="file-item"
491
+ style={{ fontStyle: "italic", color: "#6b7280" }}
492
+ >
493
+ ... and {state.files.length - 50} more files
494
+ </div>
495
+ )}
496
+ </div>
497
+ </div>
498
+ ) : state.status.hasDirectoryAccess ? (
499
+ <div className="empty-state">
500
+ <Upload className="empty-state-icon" />
501
+ <h3 className="empty-state-title">No Files Found</h3>
502
+ <p className="empty-state-description">
503
+ The selected folder appears to be empty or all files are
504
+ excluded.
505
+ </p>
506
+ </div>
507
+ ) : (
508
+ <div className="empty-state">
509
+ <FolderOpen className="empty-state-icon" />
510
+ <h3 className="empty-state-title">No Folder Selected</h3>
511
+ <p className="empty-state-description">
512
+ Choose a folder from your computer to start syncing files.
513
+ </p>
514
+ </div>
515
+ )}
516
+ </div>
517
+
518
+ {/* Settings Panel */}
519
+ {state.showSettings && (
520
+ <div className="settings-panel">
521
+ <h3
522
+ style={{
523
+ marginBottom: "1rem",
524
+ fontSize: "1rem",
525
+ fontWeight: "500",
526
+ }}
527
+ >
528
+ Settings
529
+ </h3>
530
+
531
+ <div className="settings-row">
532
+ <label className="settings-label">Auto Sync</label>
533
+ <label className="toggle">
534
+ <input
535
+ type="checkbox"
536
+ checked={state.settings.autoSync}
537
+ onChange={(e) =>
538
+ handleSettingsChange("autoSync", e.target.checked)
539
+ }
540
+ />
541
+ <span className="toggle-slider"></span>
542
+ </label>
543
+ </div>
544
+
545
+ {state.settings.autoSync && (
546
+ <div className="settings-row">
547
+ <label className="settings-label">
548
+ Sync Interval (seconds)
549
+ </label>
550
+ <input
551
+ type="number"
552
+ value={state.settings.syncInterval}
553
+ onChange={(e) =>
554
+ handleSettingsChange(
555
+ "syncInterval",
556
+ parseInt(e.target.value)
557
+ )
558
+ }
559
+ min="5"
560
+ max="300"
561
+ style={{
562
+ width: "80px",
563
+ padding: "0.25rem",
564
+ border: "1px solid #d1d5db",
565
+ borderRadius: "0.25rem",
566
+ }}
567
+ />
568
+ </div>
569
+ )}
570
+
571
+ <div className="settings-row">
572
+ <label className="settings-label">Exclude Patterns</label>
573
+ <input
574
+ type="text"
575
+ value={state.settings.excludePatterns.join(", ")}
576
+ onChange={(e) =>
577
+ handleSettingsChange(
578
+ "excludePatterns",
579
+ e.target.value.split(",").map((s) => s.trim())
580
+ )
581
+ }
582
+ placeholder=".git, node_modules, *.tmp"
583
+ style={{
584
+ flex: 1,
585
+ marginLeft: "1rem",
586
+ padding: "0.25rem",
587
+ border: "1px solid #d1d5db",
588
+ borderRadius: "0.25rem",
589
+ }}
590
+ />
591
+ </div>
592
+ </div>
593
+ )}
594
+ </div>
595
+ </div>
596
+ );
597
+ };
598
+
599
+ export default BrowserSyncTool;
@@ -0,0 +1,20 @@
1
+ import "./polyfills";
2
+ import { type Plugin } from "@patchwork/sdk";
3
+ import type { FolderDoc } from "./types";
4
+
5
+ import "./styles.css";
6
+
7
+ export const plugins: Plugin<any>[] = [
8
+ {
9
+ type: "patchwork:tool",
10
+ id: "browser-folder-sync",
11
+ name: "Browser Sync",
12
+ icon: "FolderSync",
13
+ supportedDataTypes: ["folder"],
14
+ async load() {
15
+ const BrowserSyncTool = (await import("./components/BrowserSyncTool"))
16
+ .default;
17
+ return { EditorComponent: BrowserSyncTool };
18
+ },
19
+ },
20
+ ];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Browser polyfills for Node.js globals
3
+ */
4
+
5
+ // Define process global
6
+ (globalThis as any).process = {
7
+ env: {},
8
+ version: "v18.0.0",
9
+ platform: "browser",
10
+ nextTick: (fn: Function) => setTimeout(fn, 0),
11
+ cwd: () => "/",
12
+ argv: [],
13
+ exit: () => {},
14
+ stderr: { write: console.error },
15
+ stdout: { write: console.log },
16
+ };
17
+
18
+ // Define Buffer global (minimal implementation)
19
+ (globalThis as any).Buffer = {
20
+ from: (data: any) => new Uint8Array(data),
21
+ alloc: (size: number) => new Uint8Array(size),
22
+ isBuffer: () => false,
23
+ };
24
+
25
+ // Define global if not present
26
+ if (typeof (globalThis as any).global === "undefined") {
27
+ (globalThis as any).global = globalThis;
28
+ }
29
+
30
+ // Export for TypeScript
31
+ export {};