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
package/src/utils/fs.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as crypto from "crypto";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
import * as mimeTypes from "mime-types";
|
|
6
|
+
import { FileSystemEntry, FileType } from "../types";
|
|
7
|
+
import { isEnhancedTextFile } from "./mime-types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a path exists
|
|
11
|
+
*/
|
|
12
|
+
export async function pathExists(filePath: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(filePath);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get file system entry metadata
|
|
23
|
+
*/
|
|
24
|
+
export async function getFileSystemEntry(
|
|
25
|
+
filePath: string
|
|
26
|
+
): Promise<FileSystemEntry | null> {
|
|
27
|
+
try {
|
|
28
|
+
const stats = await fs.stat(filePath);
|
|
29
|
+
const type = stats.isDirectory()
|
|
30
|
+
? FileType.DIRECTORY
|
|
31
|
+
: (await isEnhancedTextFile(filePath))
|
|
32
|
+
? FileType.TEXT
|
|
33
|
+
: FileType.BINARY;
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
path: filePath,
|
|
37
|
+
type,
|
|
38
|
+
size: stats.size,
|
|
39
|
+
mtime: stats.mtime,
|
|
40
|
+
permissions: stats.mode & parseInt("777", 8),
|
|
41
|
+
};
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Determine if a file is text or binary
|
|
49
|
+
*/
|
|
50
|
+
export async function isTextFile(filePath: string): Promise<boolean> {
|
|
51
|
+
try {
|
|
52
|
+
const mimeType = mimeTypes.lookup(filePath);
|
|
53
|
+
if (mimeType) {
|
|
54
|
+
return (
|
|
55
|
+
mimeType.startsWith("text/") ||
|
|
56
|
+
mimeType === "application/json" ||
|
|
57
|
+
mimeType === "application/xml" ||
|
|
58
|
+
mimeType.includes("javascript") ||
|
|
59
|
+
mimeType.includes("typescript")
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Sample first 8KB to detect binary content
|
|
64
|
+
const handle = await fs.open(filePath, "r");
|
|
65
|
+
const buffer = Buffer.alloc(Math.min(8192, (await handle.stat()).size));
|
|
66
|
+
await handle.read(buffer, 0, buffer.length, 0);
|
|
67
|
+
await handle.close();
|
|
68
|
+
|
|
69
|
+
// Check for null bytes which indicate binary content
|
|
70
|
+
return !buffer.includes(0);
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read file content as string or buffer
|
|
78
|
+
*/
|
|
79
|
+
export async function readFileContent(
|
|
80
|
+
filePath: string
|
|
81
|
+
): Promise<string | Uint8Array> {
|
|
82
|
+
const isText = await isEnhancedTextFile(filePath);
|
|
83
|
+
|
|
84
|
+
if (isText) {
|
|
85
|
+
return await fs.readFile(filePath, "utf8");
|
|
86
|
+
} else {
|
|
87
|
+
const buffer = await fs.readFile(filePath);
|
|
88
|
+
return new Uint8Array(buffer);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Write file content from string or buffer
|
|
94
|
+
*/
|
|
95
|
+
export async function writeFileContent(
|
|
96
|
+
filePath: string,
|
|
97
|
+
content: string | Uint8Array
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
await ensureDirectoryExists(path.dirname(filePath));
|
|
100
|
+
|
|
101
|
+
if (typeof content === "string") {
|
|
102
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
103
|
+
} else {
|
|
104
|
+
await fs.writeFile(filePath, content);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Ensure directory exists, creating it if necessary
|
|
110
|
+
*/
|
|
111
|
+
export async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
|
112
|
+
try {
|
|
113
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
114
|
+
} catch (error: any) {
|
|
115
|
+
if (error.code !== "EEXIST") {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Remove file or directory
|
|
123
|
+
*/
|
|
124
|
+
export async function removePath(filePath: string): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
const stats = await fs.stat(filePath);
|
|
127
|
+
if (stats.isDirectory()) {
|
|
128
|
+
await fs.rmdir(filePath, { recursive: true });
|
|
129
|
+
} else {
|
|
130
|
+
await fs.unlink(filePath);
|
|
131
|
+
}
|
|
132
|
+
} catch (error: any) {
|
|
133
|
+
if (error.code !== "ENOENT") {
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a path matches any of the exclude patterns
|
|
141
|
+
*/
|
|
142
|
+
function isExcluded(
|
|
143
|
+
filePath: string,
|
|
144
|
+
basePath: string,
|
|
145
|
+
excludePatterns: string[]
|
|
146
|
+
): boolean {
|
|
147
|
+
const relativePath = path.relative(basePath, filePath);
|
|
148
|
+
|
|
149
|
+
for (const pattern of excludePatterns) {
|
|
150
|
+
// Handle different pattern types
|
|
151
|
+
if (pattern.startsWith(".") && !pattern.includes("*")) {
|
|
152
|
+
// Directory pattern like ".pushwork" or ".git"
|
|
153
|
+
if (
|
|
154
|
+
relativePath.startsWith(pattern) ||
|
|
155
|
+
relativePath.includes(`/${pattern}/`) ||
|
|
156
|
+
relativePath.includes(`\\${pattern}\\`)
|
|
157
|
+
) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
} else if (pattern.includes("*")) {
|
|
161
|
+
// Glob pattern like "*.tmp"
|
|
162
|
+
const regex = new RegExp(
|
|
163
|
+
pattern.replace(/\*/g, ".*").replace(/\?/g, ".")
|
|
164
|
+
);
|
|
165
|
+
if (regex.test(relativePath)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// Exact directory name like "node_modules"
|
|
170
|
+
const parts = relativePath.split(/[/\\]/);
|
|
171
|
+
if (parts.includes(pattern)) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* List directory contents with metadata
|
|
182
|
+
*/
|
|
183
|
+
export async function listDirectory(
|
|
184
|
+
dirPath: string,
|
|
185
|
+
recursive = false,
|
|
186
|
+
excludePatterns: string[] = []
|
|
187
|
+
): Promise<FileSystemEntry[]> {
|
|
188
|
+
const entries: FileSystemEntry[] = [];
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const pattern = recursive
|
|
192
|
+
? path.join(dirPath, "**/*")
|
|
193
|
+
: path.join(dirPath, "*");
|
|
194
|
+
|
|
195
|
+
// Convert exclude patterns to glob ignore patterns
|
|
196
|
+
const ignorePatterns = excludePatterns.map((pattern) => {
|
|
197
|
+
if (pattern.startsWith(".") && !pattern.includes("*")) {
|
|
198
|
+
// Directory patterns
|
|
199
|
+
return `${pattern}/**`;
|
|
200
|
+
}
|
|
201
|
+
return pattern;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const paths = await glob(pattern, {
|
|
205
|
+
dot: true,
|
|
206
|
+
ignore: ignorePatterns,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
for (const filePath of paths) {
|
|
210
|
+
// Additional filtering for safety
|
|
211
|
+
if (!isExcluded(filePath, dirPath, excludePatterns)) {
|
|
212
|
+
const entry = await getFileSystemEntry(filePath);
|
|
213
|
+
if (entry) {
|
|
214
|
+
entries.push(entry);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
// Return empty array if directory doesn't exist or can't be read
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return entries;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Copy file with metadata preservation
|
|
227
|
+
*/
|
|
228
|
+
export async function copyFile(
|
|
229
|
+
sourcePath: string,
|
|
230
|
+
destPath: string
|
|
231
|
+
): Promise<void> {
|
|
232
|
+
await ensureDirectoryExists(path.dirname(destPath));
|
|
233
|
+
await fs.copyFile(sourcePath, destPath);
|
|
234
|
+
|
|
235
|
+
// Preserve file permissions
|
|
236
|
+
const stats = await fs.stat(sourcePath);
|
|
237
|
+
await fs.chmod(destPath, stats.mode);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Move/rename file or directory
|
|
242
|
+
*/
|
|
243
|
+
export async function movePath(
|
|
244
|
+
sourcePath: string,
|
|
245
|
+
destPath: string
|
|
246
|
+
): Promise<void> {
|
|
247
|
+
await ensureDirectoryExists(path.dirname(destPath));
|
|
248
|
+
await fs.rename(sourcePath, destPath);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Calculate content hash for change detection
|
|
253
|
+
*/
|
|
254
|
+
export async function calculateContentHash(
|
|
255
|
+
content: string | Uint8Array
|
|
256
|
+
): Promise<string> {
|
|
257
|
+
const hash = crypto.createHash("sha256");
|
|
258
|
+
hash.update(content);
|
|
259
|
+
return hash.digest("hex");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Get MIME type for file
|
|
264
|
+
*/
|
|
265
|
+
export function getMimeType(filePath: string): string {
|
|
266
|
+
return mimeTypes.lookup(filePath) || "application/octet-stream";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get file extension
|
|
271
|
+
*/
|
|
272
|
+
export function getFileExtension(filePath: string): string {
|
|
273
|
+
const ext = path.extname(filePath);
|
|
274
|
+
return ext.startsWith(".") ? ext.slice(1) : ext;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Normalize path separators for cross-platform compatibility
|
|
279
|
+
*/
|
|
280
|
+
export function normalizePath(filePath: string): string {
|
|
281
|
+
return path.posix.normalize(filePath.replace(/\\/g, "/"));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get relative path from base directory
|
|
286
|
+
*/
|
|
287
|
+
export function getRelativePath(basePath: string, filePath: string): string {
|
|
288
|
+
return normalizePath(path.relative(basePath, filePath));
|
|
289
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import * as mimeTypes from "mime-types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom MIME type definitions for developer files
|
|
5
|
+
* Based on patchwork-cli's approach
|
|
6
|
+
*/
|
|
7
|
+
const CUSTOM_MIME_TYPES: Record<string, string> = {
|
|
8
|
+
// TypeScript files - override the incorrect video/mp2t detection
|
|
9
|
+
".ts": "text/typescript",
|
|
10
|
+
".tsx": "text/tsx",
|
|
11
|
+
|
|
12
|
+
// Config file formats
|
|
13
|
+
".json": "application/json",
|
|
14
|
+
".yaml": "text/yaml",
|
|
15
|
+
".yml": "text/yaml",
|
|
16
|
+
".toml": "application/toml",
|
|
17
|
+
".ini": "text/plain",
|
|
18
|
+
".conf": "text/plain",
|
|
19
|
+
".config": "text/plain",
|
|
20
|
+
|
|
21
|
+
// Vue.js single file components
|
|
22
|
+
".vue": "text/vue",
|
|
23
|
+
|
|
24
|
+
// Modern CSS preprocessors
|
|
25
|
+
".scss": "text/scss",
|
|
26
|
+
".sass": "text/sass",
|
|
27
|
+
".less": "text/less",
|
|
28
|
+
".styl": "text/stylus",
|
|
29
|
+
|
|
30
|
+
// Modern JavaScript variants
|
|
31
|
+
".mjs": "application/javascript",
|
|
32
|
+
".cjs": "application/javascript",
|
|
33
|
+
|
|
34
|
+
// React JSX
|
|
35
|
+
".jsx": "text/jsx",
|
|
36
|
+
|
|
37
|
+
// Svelte components
|
|
38
|
+
".svelte": "text/svelte",
|
|
39
|
+
|
|
40
|
+
// Web assembly
|
|
41
|
+
".wasm": "application/wasm",
|
|
42
|
+
|
|
43
|
+
// Other common dev files
|
|
44
|
+
".d.ts": "text/typescript",
|
|
45
|
+
".map": "application/json", // Source maps
|
|
46
|
+
".env": "text/plain",
|
|
47
|
+
".gitignore": "text/plain",
|
|
48
|
+
".gitattributes": "text/plain",
|
|
49
|
+
".editorconfig": "text/plain",
|
|
50
|
+
".prettierrc": "application/json",
|
|
51
|
+
".eslintrc": "application/json",
|
|
52
|
+
".babelrc": "application/json",
|
|
53
|
+
|
|
54
|
+
// Documentation formats
|
|
55
|
+
".mdx": "text/markdown",
|
|
56
|
+
".rst": "text/x-rst",
|
|
57
|
+
|
|
58
|
+
// Docker files
|
|
59
|
+
Dockerfile: "text/plain",
|
|
60
|
+
".dockerignore": "text/plain",
|
|
61
|
+
|
|
62
|
+
// Package manager files
|
|
63
|
+
"package.json": "application/json",
|
|
64
|
+
"package-lock.json": "application/json",
|
|
65
|
+
"yarn.lock": "text/plain",
|
|
66
|
+
"pnpm-lock.yaml": "text/yaml",
|
|
67
|
+
"composer.json": "application/json",
|
|
68
|
+
Pipfile: "text/plain",
|
|
69
|
+
"requirements.txt": "text/plain",
|
|
70
|
+
|
|
71
|
+
// Build tool configs
|
|
72
|
+
"webpack.config.js": "application/javascript",
|
|
73
|
+
"vite.config.js": "application/javascript",
|
|
74
|
+
"rollup.config.js": "application/javascript",
|
|
75
|
+
"tsconfig.json": "application/json",
|
|
76
|
+
"jsconfig.json": "application/json",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* File extensions that should always be treated as text
|
|
81
|
+
* regardless of MIME type detection
|
|
82
|
+
*/
|
|
83
|
+
const FORCE_TEXT_EXTENSIONS = new Set([
|
|
84
|
+
".ts",
|
|
85
|
+
".tsx",
|
|
86
|
+
".jsx",
|
|
87
|
+
".vue",
|
|
88
|
+
".svelte",
|
|
89
|
+
".scss",
|
|
90
|
+
".sass",
|
|
91
|
+
".less",
|
|
92
|
+
".styl",
|
|
93
|
+
".env",
|
|
94
|
+
".gitignore",
|
|
95
|
+
".gitattributes",
|
|
96
|
+
".editorconfig",
|
|
97
|
+
".d.ts",
|
|
98
|
+
".map",
|
|
99
|
+
".mdx",
|
|
100
|
+
".rst",
|
|
101
|
+
".toml",
|
|
102
|
+
".ini",
|
|
103
|
+
".conf",
|
|
104
|
+
".config",
|
|
105
|
+
".lock",
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get enhanced MIME type for file with custom dev file support
|
|
110
|
+
*/
|
|
111
|
+
export function getEnhancedMimeType(filePath: string): string {
|
|
112
|
+
const filename = filePath.split("/").pop() || "";
|
|
113
|
+
const extension = getFileExtension(filePath);
|
|
114
|
+
|
|
115
|
+
// Check custom definitions first (by extension)
|
|
116
|
+
if (extension && CUSTOM_MIME_TYPES[extension]) {
|
|
117
|
+
return CUSTOM_MIME_TYPES[extension];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check custom definitions by full filename
|
|
121
|
+
if (CUSTOM_MIME_TYPES[filename]) {
|
|
122
|
+
return CUSTOM_MIME_TYPES[filename];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fall back to standard mime-types library
|
|
126
|
+
const standardMime = mimeTypes.lookup(filePath);
|
|
127
|
+
if (standardMime) {
|
|
128
|
+
return standardMime;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Final fallback
|
|
132
|
+
return "application/octet-stream";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if file extension should be forced to text type
|
|
137
|
+
*/
|
|
138
|
+
export function shouldForceAsText(filePath: string): boolean {
|
|
139
|
+
const extension = getFileExtension(filePath);
|
|
140
|
+
return extension ? FORCE_TEXT_EXTENSIONS.has(extension) : false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get file extension including the dot (internal helper)
|
|
145
|
+
*/
|
|
146
|
+
function getFileExtension(filePath: string): string {
|
|
147
|
+
const match = filePath.match(/\.[^.]*$/);
|
|
148
|
+
return match ? match[0] : "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Enhanced text file detection with developer file support
|
|
153
|
+
*/
|
|
154
|
+
export async function isEnhancedTextFile(filePath: string): Promise<boolean> {
|
|
155
|
+
// Force certain extensions to be treated as text
|
|
156
|
+
if (shouldForceAsText(filePath)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check MIME type
|
|
161
|
+
const mimeType = getEnhancedMimeType(filePath);
|
|
162
|
+
if (isTextMimeType(mimeType)) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// If it's a known binary type (but not the generic fallback), don't fall back to content detection
|
|
167
|
+
if (isBinaryMimeType(mimeType) && mimeType !== "application/octet-stream") {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// For generic octet-stream or unknown types, use content-based detection
|
|
172
|
+
return isTextByContent(filePath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Check if MIME type indicates text content
|
|
177
|
+
*/
|
|
178
|
+
function isTextMimeType(mimeType: string): boolean {
|
|
179
|
+
return (
|
|
180
|
+
mimeType.startsWith("text/") ||
|
|
181
|
+
mimeType === "application/json" ||
|
|
182
|
+
mimeType === "application/xml" ||
|
|
183
|
+
mimeType === "application/javascript" ||
|
|
184
|
+
mimeType === "application/typescript" ||
|
|
185
|
+
mimeType === "application/toml" ||
|
|
186
|
+
mimeType.includes("javascript") ||
|
|
187
|
+
mimeType.includes("typescript") ||
|
|
188
|
+
mimeType.includes("json") ||
|
|
189
|
+
mimeType.includes("xml")
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if MIME type indicates binary content
|
|
195
|
+
*/
|
|
196
|
+
function isBinaryMimeType(mimeType: string): boolean {
|
|
197
|
+
return (
|
|
198
|
+
mimeType.startsWith("image/") ||
|
|
199
|
+
mimeType.startsWith("video/") ||
|
|
200
|
+
mimeType.startsWith("audio/") ||
|
|
201
|
+
mimeType.startsWith("font/") ||
|
|
202
|
+
mimeType === "application/zip" ||
|
|
203
|
+
mimeType === "application/pdf" ||
|
|
204
|
+
mimeType === "application/octet-stream" ||
|
|
205
|
+
mimeType === "application/wasm" ||
|
|
206
|
+
mimeType.includes("binary")
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Content-based text detection (fallback method)
|
|
212
|
+
*/
|
|
213
|
+
async function isTextByContent(filePath: string): Promise<boolean> {
|
|
214
|
+
try {
|
|
215
|
+
const fs = await import("fs/promises");
|
|
216
|
+
|
|
217
|
+
// Sample first 8KB to detect binary content
|
|
218
|
+
const handle = await fs.open(filePath, "r");
|
|
219
|
+
const stats = await handle.stat();
|
|
220
|
+
const sampleSize = Math.min(8192, stats.size);
|
|
221
|
+
|
|
222
|
+
if (sampleSize === 0) {
|
|
223
|
+
await handle.close();
|
|
224
|
+
return true; // Empty file is text
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const buffer = Buffer.alloc(sampleSize);
|
|
228
|
+
await handle.read(buffer, 0, sampleSize, 0);
|
|
229
|
+
await handle.close();
|
|
230
|
+
|
|
231
|
+
// Check for null bytes which indicate binary content
|
|
232
|
+
return !buffer.includes(0);
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { DocHandle, StorageId } from "@automerge/automerge-repo";
|
|
2
|
+
import * as A from "@automerge/automerge";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wait for documents to sync to the remote server
|
|
6
|
+
* Based on patchwork-cli implementation with timeout for debugging
|
|
7
|
+
*/
|
|
8
|
+
export async function waitForSync(
|
|
9
|
+
handlesToWaitOn: DocHandle<unknown>[],
|
|
10
|
+
syncServerStorageId?: StorageId,
|
|
11
|
+
timeoutMs: number = 60000 // 60 second timeout for debugging
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
if (!syncServerStorageId) {
|
|
14
|
+
console.warn(
|
|
15
|
+
"No sync server storage ID provided. Skipping network sync wait."
|
|
16
|
+
);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (handlesToWaitOn.length === 0) {
|
|
21
|
+
console.log("🔄 No documents to sync");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Debug logging only in verbose mode (can be controlled via env var later)
|
|
26
|
+
const verbose = false;
|
|
27
|
+
|
|
28
|
+
if (verbose) {
|
|
29
|
+
console.log(
|
|
30
|
+
`🔄 Waiting for ${handlesToWaitOn.length} documents to sync...`
|
|
31
|
+
);
|
|
32
|
+
console.log(`📡 Using sync server storage ID: ${syncServerStorageId}`);
|
|
33
|
+
|
|
34
|
+
handlesToWaitOn.forEach((handle, i) => {
|
|
35
|
+
const localHeads = handle.heads();
|
|
36
|
+
const syncInfo = handle.getSyncInfo(syncServerStorageId);
|
|
37
|
+
const remoteHeads = syncInfo?.lastHeads;
|
|
38
|
+
console.log(` 📄 Document ${i + 1}: ${handle.url}`);
|
|
39
|
+
console.log(` 🏠 Local heads: ${JSON.stringify(localHeads)}`);
|
|
40
|
+
console.log(` 🌐 Remote heads: ${JSON.stringify(remoteHeads)}`);
|
|
41
|
+
console.log(
|
|
42
|
+
` ✅ Already synced: ${A.equals(localHeads, remoteHeads)}`
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const promises = handlesToWaitOn.map(
|
|
48
|
+
(handle, index) =>
|
|
49
|
+
new Promise<void>((resolve, reject) => {
|
|
50
|
+
const timeout = setTimeout(() => {
|
|
51
|
+
const localHeads = handle.heads();
|
|
52
|
+
const syncInfo = handle.getSyncInfo(syncServerStorageId);
|
|
53
|
+
const remoteHeads = syncInfo?.lastHeads;
|
|
54
|
+
console.log(`⏰ TIMEOUT for document ${index + 1}: ${handle.url}`);
|
|
55
|
+
console.log(` Final local heads: ${JSON.stringify(localHeads)}`);
|
|
56
|
+
console.log(` Final remote heads: ${JSON.stringify(remoteHeads)}`);
|
|
57
|
+
reject(
|
|
58
|
+
new Error(
|
|
59
|
+
`Sync timeout after ${timeoutMs}ms for document ${handle.url}`
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
}, timeoutMs);
|
|
63
|
+
|
|
64
|
+
const checkSync = () => {
|
|
65
|
+
const newHeads = handle.heads();
|
|
66
|
+
const syncInfo = handle.getSyncInfo(syncServerStorageId);
|
|
67
|
+
const remoteHeads = syncInfo?.lastHeads;
|
|
68
|
+
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.log(`🔍 Checking sync for ${handle.url}:`);
|
|
71
|
+
console.log(` Local heads: ${JSON.stringify(newHeads)}`);
|
|
72
|
+
console.log(` Remote heads: ${JSON.stringify(remoteHeads)}`);
|
|
73
|
+
console.log(` Heads equal: ${A.equals(newHeads, remoteHeads)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// If the remote heads are already up to date, we can resolve immediately
|
|
77
|
+
if (A.equals(newHeads, remoteHeads)) {
|
|
78
|
+
if (verbose) {
|
|
79
|
+
console.log(`✅ Document ${index + 1} synced: ${handle.url}`);
|
|
80
|
+
}
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
resolve();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Check if already synced
|
|
89
|
+
if (checkSync()) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Otherwise, wait for remote-heads event
|
|
94
|
+
const onRemoteHeads = ({
|
|
95
|
+
storageId,
|
|
96
|
+
heads,
|
|
97
|
+
}: {
|
|
98
|
+
storageId: StorageId;
|
|
99
|
+
heads: any;
|
|
100
|
+
}) => {
|
|
101
|
+
if (verbose) {
|
|
102
|
+
console.log(`📡 Received remote heads event for ${handle.url}:`);
|
|
103
|
+
console.log(` Event storage ID: ${storageId}`);
|
|
104
|
+
console.log(` Expected storage ID: ${syncServerStorageId}`);
|
|
105
|
+
console.log(` Event heads: ${JSON.stringify(heads)}`);
|
|
106
|
+
console.log(
|
|
107
|
+
` Current local heads: ${JSON.stringify(handle.heads())}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
storageId === syncServerStorageId &&
|
|
113
|
+
A.equals(handle.heads(), heads)
|
|
114
|
+
) {
|
|
115
|
+
if (verbose) {
|
|
116
|
+
console.log(
|
|
117
|
+
`✅ Document ${index + 1} synced via event: ${handle.url}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
handle.off("remote-heads", onRemoteHeads);
|
|
122
|
+
resolve();
|
|
123
|
+
} else if (verbose) {
|
|
124
|
+
console.log(`❌ Heads/storage mismatch for ${handle.url}`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (verbose) {
|
|
129
|
+
console.log(`👂 Listening for remote-heads events on ${handle.url}`);
|
|
130
|
+
}
|
|
131
|
+
handle.on("remote-heads", onRemoteHeads);
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await Promise.all(promises);
|
|
137
|
+
if (verbose) {
|
|
138
|
+
console.log("✅ All documents synced to network");
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error(`❌ Sync wait failed: ${error}`);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the storage ID for the sync server
|
|
148
|
+
* Using the same ID as patchwork-cli for consistency
|
|
149
|
+
*/
|
|
150
|
+
export function getSyncServerStorageId(customStorageId?: string): StorageId {
|
|
151
|
+
return (customStorageId ||
|
|
152
|
+
"3760df37-a4c6-4f66-9ecd-732039a9385d") as StorageId;
|
|
153
|
+
}
|