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.
- package/README.md +460 -0
- package/dist/browser/browser-sync-engine.d.ts +64 -0
- package/dist/browser/browser-sync-engine.d.ts.map +1 -0
- package/dist/browser/browser-sync-engine.js +303 -0
- package/dist/browser/browser-sync-engine.js.map +1 -0
- package/dist/browser/filesystem-adapter.d.ts +84 -0
- package/dist/browser/filesystem-adapter.d.ts.map +1 -0
- package/dist/browser/filesystem-adapter.js +413 -0
- package/dist/browser/filesystem-adapter.js.map +1 -0
- package/dist/browser/index.d.ts +36 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +90 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/types.d.ts +70 -0
- package/dist/browser/types.d.ts.map +1 -0
- package/dist/browser/types.js +6 -0
- package/dist/browser/types.js.map +1 -0
- package/dist/cli/commands.d.ts +71 -0
- package/dist/cli/commands.d.ts.map +1 -0
- package/dist/cli/commands.js +794 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +199 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +71 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +314 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/change-detection.d.ts +78 -0
- package/dist/core/change-detection.d.ts.map +1 -0
- package/dist/core/change-detection.js +370 -0
- package/dist/core/change-detection.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +22 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/isomorphic-snapshot.d.ts +58 -0
- package/dist/core/isomorphic-snapshot.d.ts.map +1 -0
- package/dist/core/isomorphic-snapshot.js +204 -0
- package/dist/core/isomorphic-snapshot.js.map +1 -0
- package/dist/core/move-detection.d.ts +72 -0
- package/dist/core/move-detection.d.ts.map +1 -0
- package/dist/core/move-detection.js +200 -0
- package/dist/core/move-detection.js.map +1 -0
- package/dist/core/snapshot.d.ts +109 -0
- package/dist/core/snapshot.d.ts.map +1 -0
- package/dist/core/snapshot.js +263 -0
- package/dist/core/snapshot.js.map +1 -0
- package/dist/core/sync-engine.d.ts +110 -0
- package/dist/core/sync-engine.d.ts.map +1 -0
- package/dist/core/sync-engine.js +817 -0
- package/dist/core/sync-engine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/browser-filesystem.d.ts +26 -0
- package/dist/platform/browser-filesystem.d.ts.map +1 -0
- package/dist/platform/browser-filesystem.js +91 -0
- package/dist/platform/browser-filesystem.js.map +1 -0
- package/dist/platform/filesystem.d.ts +29 -0
- package/dist/platform/filesystem.d.ts.map +1 -0
- package/dist/platform/filesystem.js +65 -0
- package/dist/platform/filesystem.js.map +1 -0
- package/dist/platform/node-filesystem.d.ts +21 -0
- package/dist/platform/node-filesystem.d.ts.map +1 -0
- package/dist/platform/node-filesystem.js +93 -0
- package/dist/platform/node-filesystem.js.map +1 -0
- package/dist/types/config.d.ts +119 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +3 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/documents.d.ts +70 -0
- package/dist/types/documents.d.ts.map +1 -0
- package/dist/types/documents.js +23 -0
- package/dist/types/documents.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +23 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/snapshot.d.ts +81 -0
- package/dist/types/snapshot.d.ts.map +1 -0
- package/dist/types/snapshot.js +17 -0
- package/dist/types/snapshot.js.map +1 -0
- package/dist/utils/content-similarity.d.ts +53 -0
- package/dist/utils/content-similarity.d.ts.map +1 -0
- package/dist/utils/content-similarity.js +155 -0
- package/dist/utils/content-similarity.js.map +1 -0
- package/dist/utils/content.d.ts +5 -0
- package/dist/utils/content.d.ts.map +1 -0
- package/dist/utils/content.js +30 -0
- package/dist/utils/content.js.map +1 -0
- package/dist/utils/fs-browser.d.ts +57 -0
- package/dist/utils/fs-browser.d.ts.map +1 -0
- package/dist/utils/fs-browser.js +311 -0
- package/dist/utils/fs-browser.js.map +1 -0
- package/dist/utils/fs-node.d.ts +53 -0
- package/dist/utils/fs-node.d.ts.map +1 -0
- package/dist/utils/fs-node.js +220 -0
- package/dist/utils/fs-node.js.map +1 -0
- package/dist/utils/fs.d.ts +62 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +293 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +23 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/isomorphic.d.ts +29 -0
- package/dist/utils/isomorphic.d.ts.map +1 -0
- package/dist/utils/isomorphic.js +139 -0
- package/dist/utils/isomorphic.js.map +1 -0
- package/dist/utils/mime-types.d.ts +13 -0
- package/dist/utils/mime-types.d.ts.map +1 -0
- package/dist/utils/mime-types.js +240 -0
- package/dist/utils/mime-types.js.map +1 -0
- package/dist/utils/network-sync.d.ts +12 -0
- package/dist/utils/network-sync.d.ts.map +1 -0
- package/dist/utils/network-sync.js +149 -0
- package/dist/utils/network-sync.js.map +1 -0
- package/dist/utils/pure.d.ts +25 -0
- package/dist/utils/pure.d.ts.map +1 -0
- package/dist/utils/pure.js +112 -0
- package/dist/utils/pure.js.map +1 -0
- package/dist/utils/repo-factory.d.ts +11 -0
- package/dist/utils/repo-factory.d.ts.map +1 -0
- package/dist/utils/repo-factory.js +77 -0
- package/dist/utils/repo-factory.js.map +1 -0
- package/package.json +83 -0
- package/src/cli/commands.ts +1053 -0
- package/src/cli/index.ts +2 -0
- package/src/cli.ts +287 -0
- package/src/config/index.ts +334 -0
- package/src/core/change-detection.ts +484 -0
- package/src/core/index.ts +5 -0
- package/src/core/move-detection.ts +269 -0
- package/src/core/snapshot.ts +285 -0
- package/src/core/sync-engine.ts +1167 -0
- package/src/index.ts +14 -0
- package/src/types/config.ts +130 -0
- package/src/types/documents.ts +72 -0
- package/src/types/index.ts +8 -0
- package/src/types/snapshot.ts +88 -0
- package/src/utils/content-similarity.ts +194 -0
- package/src/utils/content.ts +28 -0
- package/src/utils/fs.ts +289 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/mime-types.ts +236 -0
- package/src/utils/network-sync.ts +153 -0
- package/src/utils/repo-factory.ts +58 -0
- package/test/README-TESTING-GAPS.md +174 -0
- package/test/integration/README.md +328 -0
- package/test/integration/clone-test.sh +310 -0
- package/test/integration/conflict-resolution-test.sh +309 -0
- package/test/integration/deletion-behavior-test.sh +487 -0
- package/test/integration/deletion-sync-test-simple.sh +193 -0
- package/test/integration/deletion-sync-test.sh +297 -0
- package/test/integration/exclude-patterns.test.ts +152 -0
- package/test/integration/full-integration-test.sh +363 -0
- package/test/integration/sync-deletion.test.ts +339 -0
- package/test/integration/sync-flow.test.ts +309 -0
- package/test/run-tests.sh +225 -0
- package/test/unit/content-similarity.test.ts +236 -0
- package/test/unit/deletion-behavior.test.ts +260 -0
- package/test/unit/enhanced-mime-detection.test.ts +266 -0
- package/test/unit/snapshot.test.ts +431 -0
- package/test/unit/sync-timing.test.ts +178 -0
- package/test/unit/utils.test.ts +368 -0
- package/tools/browser-sync/README.md +116 -0
- package/tools/browser-sync/package.json +44 -0
- package/tools/browser-sync/patchwork.json +1 -0
- package/tools/browser-sync/pnpm-lock.yaml +4202 -0
- package/tools/browser-sync/src/components/BrowserSyncTool.tsx +599 -0
- package/tools/browser-sync/src/index.ts +20 -0
- package/tools/browser-sync/src/polyfills.ts +31 -0
- package/tools/browser-sync/src/styles.css +290 -0
- package/tools/browser-sync/src/types.ts +27 -0
- package/tools/browser-sync/vite.config.ts +25 -0
- 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 {};
|