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,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,8 @@
1
+ // File system utilities
2
+ export * from "./fs";
3
+
4
+ // Content similarity utilities
5
+ export * from "./content-similarity";
6
+
7
+ // Enhanced MIME type detection
8
+ export * from "./mime-types";
@@ -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
+ }