pushwork 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +87 -335
  2. package/dist/.pushwork/automerge/3P/Dm3ekE2pmjGnWvDaG3vSR7ww98/snapshot/aa2349c94955ea561f698720142f9d884a6872d9f82dc332d578c216beb0df0e +0 -0
  3. package/dist/.pushwork/automerge/st/orage-adapter-id +1 -0
  4. package/dist/.pushwork/config.json +15 -0
  5. package/dist/.pushwork/snapshot.json +7 -0
  6. package/dist/cli.js +208 -213
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands.d.ts +51 -0
  9. package/dist/commands.d.ts.map +1 -0
  10. package/dist/commands.js +799 -0
  11. package/dist/commands.js.map +1 -0
  12. package/dist/core/change-detection.d.ts +2 -23
  13. package/dist/core/change-detection.d.ts.map +1 -1
  14. package/dist/core/change-detection.js +73 -115
  15. package/dist/core/change-detection.js.map +1 -1
  16. package/dist/{config/index.d.ts → core/config.d.ts} +13 -3
  17. package/dist/core/config.d.ts.map +1 -0
  18. package/dist/{config/index.js → core/config.js} +55 -73
  19. package/dist/core/config.js.map +1 -0
  20. package/dist/core/index.d.ts +1 -0
  21. package/dist/core/index.d.ts.map +1 -1
  22. package/dist/core/index.js +1 -1
  23. package/dist/core/index.js.map +1 -1
  24. package/dist/core/move-detection.d.ts +4 -3
  25. package/dist/core/move-detection.d.ts.map +1 -1
  26. package/dist/core/move-detection.js +8 -7
  27. package/dist/core/move-detection.js.map +1 -1
  28. package/dist/core/snapshot.d.ts +0 -4
  29. package/dist/core/snapshot.d.ts.map +1 -1
  30. package/dist/core/snapshot.js +2 -11
  31. package/dist/core/snapshot.js.map +1 -1
  32. package/dist/core/sync-engine.d.ts +5 -11
  33. package/dist/core/sync-engine.d.ts.map +1 -1
  34. package/dist/core/sync-engine.js +211 -308
  35. package/dist/core/sync-engine.js.map +1 -1
  36. package/dist/index.d.ts +0 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +0 -6
  39. package/dist/index.js.map +1 -1
  40. package/dist/types/config.d.ts +24 -88
  41. package/dist/types/config.d.ts.map +1 -1
  42. package/dist/types/config.js +6 -0
  43. package/dist/types/config.js.map +1 -1
  44. package/dist/types/documents.d.ts +15 -2
  45. package/dist/types/documents.d.ts.map +1 -1
  46. package/dist/types/documents.js.map +1 -1
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/index.js +0 -3
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/types/snapshot.d.ts +0 -21
  51. package/dist/types/snapshot.d.ts.map +1 -1
  52. package/dist/types/snapshot.js +0 -14
  53. package/dist/types/snapshot.js.map +1 -1
  54. package/dist/utils/content.d.ts.map +1 -1
  55. package/dist/utils/content.js +2 -6
  56. package/dist/utils/content.js.map +1 -1
  57. package/dist/utils/directory.d.ts +10 -0
  58. package/dist/utils/directory.d.ts.map +1 -0
  59. package/dist/utils/directory.js +37 -0
  60. package/dist/utils/directory.js.map +1 -0
  61. package/dist/utils/fs.d.ts +15 -2
  62. package/dist/utils/fs.d.ts.map +1 -1
  63. package/dist/utils/fs.js +54 -20
  64. package/dist/utils/fs.js.map +1 -1
  65. package/dist/utils/index.d.ts +1 -0
  66. package/dist/utils/index.d.ts.map +1 -1
  67. package/dist/utils/index.js +1 -3
  68. package/dist/utils/index.js.map +1 -1
  69. package/dist/utils/mime-types.d.ts.map +1 -1
  70. package/dist/utils/mime-types.js +11 -4
  71. package/dist/utils/mime-types.js.map +1 -1
  72. package/dist/utils/network-sync.d.ts +0 -6
  73. package/dist/utils/network-sync.d.ts.map +1 -1
  74. package/dist/utils/network-sync.js +55 -99
  75. package/dist/utils/network-sync.js.map +1 -1
  76. package/dist/utils/output.d.ts +129 -0
  77. package/dist/utils/output.d.ts.map +1 -0
  78. package/dist/utils/output.js +375 -0
  79. package/dist/utils/output.js.map +1 -0
  80. package/dist/utils/repo-factory.d.ts +2 -6
  81. package/dist/utils/repo-factory.d.ts.map +1 -1
  82. package/dist/utils/repo-factory.js +8 -31
  83. package/dist/utils/repo-factory.js.map +1 -1
  84. package/dist/utils/string-similarity.js +2 -2
  85. package/dist/utils/string-similarity.js.map +1 -1
  86. package/dist/utils/trace.d.ts +19 -0
  87. package/dist/utils/trace.d.ts.map +1 -0
  88. package/dist/utils/trace.js +68 -0
  89. package/dist/utils/trace.js.map +1 -0
  90. package/package.json +11 -11
  91. package/src/cli.ts +276 -308
  92. package/src/commands.ts +988 -0
  93. package/src/core/change-detection.ts +182 -240
  94. package/src/{config/index.ts → core/config.ts} +65 -82
  95. package/src/core/index.ts +1 -1
  96. package/src/core/move-detection.ts +10 -8
  97. package/src/core/snapshot.ts +2 -12
  98. package/src/core/sync-engine.ts +237 -427
  99. package/src/index.ts +0 -10
  100. package/src/types/config.ts +28 -93
  101. package/src/types/documents.ts +16 -2
  102. package/src/types/index.ts +0 -5
  103. package/src/types/snapshot.ts +0 -23
  104. package/src/utils/content.ts +2 -6
  105. package/src/utils/directory.ts +50 -0
  106. package/src/utils/fs.ts +58 -23
  107. package/src/utils/index.ts +1 -5
  108. package/src/utils/mime-types.ts +12 -4
  109. package/src/utils/network-sync.ts +79 -137
  110. package/src/utils/output.ts +450 -0
  111. package/src/utils/repo-factory.ts +13 -44
  112. package/src/utils/string-similarity.ts +2 -2
  113. package/src/utils/trace.ts +70 -0
  114. package/test/integration/exclude-patterns.test.ts +6 -15
  115. package/test/integration/fuzzer.test.ts +308 -391
  116. package/test/integration/init-sync.test.ts +89 -0
  117. package/test/integration/sync-deletion.test.ts +2 -61
  118. package/test/integration/sync-flow.test.ts +4 -24
  119. package/test/jest.setup.ts +34 -0
  120. package/test/unit/deletion-behavior.test.ts +3 -14
  121. package/test/unit/enhanced-mime-detection.test.ts +0 -22
  122. package/test/unit/snapshot.test.ts +2 -29
  123. package/test/unit/sync-convergence.test.ts +3 -198
  124. package/test/unit/sync-timing.test.ts +0 -44
  125. package/test/unit/utils.test.ts +0 -2
  126. package/tsconfig.json +3 -3
  127. package/bench/filesystem.bench.ts +0 -78
  128. package/bench/hashing.bench.ts +0 -60
  129. package/bench/move-detection.bench.ts +0 -130
  130. package/bench/runner.ts +0 -49
  131. package/dist/browser/browser-sync-engine.d.ts +0 -64
  132. package/dist/browser/browser-sync-engine.d.ts.map +0 -1
  133. package/dist/browser/browser-sync-engine.js +0 -303
  134. package/dist/browser/browser-sync-engine.js.map +0 -1
  135. package/dist/browser/filesystem-adapter.d.ts +0 -84
  136. package/dist/browser/filesystem-adapter.d.ts.map +0 -1
  137. package/dist/browser/filesystem-adapter.js +0 -413
  138. package/dist/browser/filesystem-adapter.js.map +0 -1
  139. package/dist/browser/index.d.ts +0 -36
  140. package/dist/browser/index.d.ts.map +0 -1
  141. package/dist/browser/index.js +0 -90
  142. package/dist/browser/index.js.map +0 -1
  143. package/dist/browser/types.d.ts +0 -70
  144. package/dist/browser/types.d.ts.map +0 -1
  145. package/dist/browser/types.js +0 -6
  146. package/dist/browser/types.js.map +0 -1
  147. package/dist/cli/commands.d.ts +0 -67
  148. package/dist/cli/commands.d.ts.map +0 -1
  149. package/dist/cli/commands.js +0 -794
  150. package/dist/cli/commands.js.map +0 -1
  151. package/dist/cli/index.d.ts +0 -2
  152. package/dist/cli/index.d.ts.map +0 -1
  153. package/dist/cli/index.js +0 -19
  154. package/dist/cli/index.js.map +0 -1
  155. package/dist/cli/output.d.ts +0 -75
  156. package/dist/cli/output.d.ts.map +0 -1
  157. package/dist/cli/output.js +0 -182
  158. package/dist/cli/output.js.map +0 -1
  159. package/dist/config/index.d.ts.map +0 -1
  160. package/dist/config/index.js.map +0 -1
  161. package/dist/config/remote-manager.d.ts +0 -65
  162. package/dist/config/remote-manager.d.ts.map +0 -1
  163. package/dist/config/remote-manager.js +0 -243
  164. package/dist/config/remote-manager.js.map +0 -1
  165. package/dist/core/isomorphic-snapshot.d.ts +0 -58
  166. package/dist/core/isomorphic-snapshot.d.ts.map +0 -1
  167. package/dist/core/isomorphic-snapshot.js +0 -204
  168. package/dist/core/isomorphic-snapshot.js.map +0 -1
  169. package/dist/platform/browser-filesystem.d.ts +0 -26
  170. package/dist/platform/browser-filesystem.d.ts.map +0 -1
  171. package/dist/platform/browser-filesystem.js +0 -91
  172. package/dist/platform/browser-filesystem.js.map +0 -1
  173. package/dist/platform/filesystem.d.ts +0 -29
  174. package/dist/platform/filesystem.d.ts.map +0 -1
  175. package/dist/platform/filesystem.js +0 -65
  176. package/dist/platform/filesystem.js.map +0 -1
  177. package/dist/platform/node-filesystem.d.ts +0 -21
  178. package/dist/platform/node-filesystem.d.ts.map +0 -1
  179. package/dist/platform/node-filesystem.js +0 -93
  180. package/dist/platform/node-filesystem.js.map +0 -1
  181. package/dist/utils/content-similarity.d.ts +0 -53
  182. package/dist/utils/content-similarity.d.ts.map +0 -1
  183. package/dist/utils/content-similarity.js +0 -155
  184. package/dist/utils/content-similarity.js.map +0 -1
  185. package/dist/utils/fs-browser.d.ts +0 -57
  186. package/dist/utils/fs-browser.d.ts.map +0 -1
  187. package/dist/utils/fs-browser.js +0 -311
  188. package/dist/utils/fs-browser.js.map +0 -1
  189. package/dist/utils/fs-node.d.ts +0 -53
  190. package/dist/utils/fs-node.d.ts.map +0 -1
  191. package/dist/utils/fs-node.js +0 -220
  192. package/dist/utils/fs-node.js.map +0 -1
  193. package/dist/utils/isomorphic.d.ts +0 -29
  194. package/dist/utils/isomorphic.d.ts.map +0 -1
  195. package/dist/utils/isomorphic.js +0 -139
  196. package/dist/utils/isomorphic.js.map +0 -1
  197. package/dist/utils/pure.d.ts +0 -25
  198. package/dist/utils/pure.d.ts.map +0 -1
  199. package/dist/utils/pure.js +0 -112
  200. package/dist/utils/pure.js.map +0 -1
  201. package/src/cli/commands.ts +0 -1030
  202. package/src/cli/index.ts +0 -2
  203. package/src/cli/output.ts +0 -244
  204. package/test/README-TESTING-GAPS.md +0 -174
package/src/index.ts CHANGED
@@ -1,14 +1,4 @@
1
- // Core sync functionality
2
1
  export * from "./core";
3
-
4
- // Utilities
5
2
  export * from "./utils";
6
-
7
- // Configuration
8
- export * from "./config";
9
-
10
- // Types
11
3
  export * from "./types";
12
-
13
- // CLI commands (for programmatic use)
14
4
  export * from "./cli";
@@ -1,85 +1,37 @@
1
- /**
2
- * Global configuration options
3
- */
4
- export interface GlobalConfig {
5
- sync_server?: string;
6
- sync_server_storage_id?: string;
7
- exclude_patterns?: string[];
8
- large_file_threshold?: string;
9
- diff?: {
10
- external_tool?: string;
11
- show_binary?: boolean;
12
- };
13
- sync?: {
14
- move_detection_threshold?: number;
15
- prompt_threshold?: number;
16
- auto_sync?: boolean;
17
- parallel_operations?: number;
18
- };
19
- }
1
+ import { StorageId } from "@automerge/automerge-repo";
20
2
 
21
3
  /**
22
- * Default settings
4
+ * Default sync server configuration
23
5
  */
24
- export interface DefaultSettings {
25
- remote_repo?: string;
26
- exclude_patterns: string[];
27
- large_file_threshold: string;
28
- }
6
+ export const DEFAULT_SYNC_SERVER = "wss://sync3.automerge.org";
7
+ export const DEFAULT_SYNC_SERVER_STORAGE_ID =
8
+ "3760df37-a4c6-4f66-9ecd-732039a9385d" as StorageId;
29
9
 
30
10
  /**
31
- * Diff tool settings
32
- */
33
- export interface DiffSettings {
34
- external_tool?: string;
35
- show_binary: boolean;
36
- }
37
-
38
- /**
39
- * Sync behavior settings
11
+ * Global configuration options
40
12
  */
41
- export interface SyncSettings {
42
- move_detection_threshold: number;
43
- prompt_threshold: number;
44
- auto_sync: boolean;
45
- parallel_operations: number;
13
+ export interface GlobalConfig {
14
+ sync_server?: string;
15
+ sync_server_storage_id?: StorageId;
16
+ exclude_patterns: string[];
17
+ sync: {
18
+ move_detection_threshold: number;
19
+ };
46
20
  }
47
21
 
48
22
  /**
49
23
  * Per-directory configuration
50
24
  */
51
- export interface DirectoryConfig {
52
- sync_server?: string;
53
- sync_server_storage_id?: string;
25
+ export interface DirectoryConfig extends GlobalConfig {
26
+ root_directory_url?: string;
54
27
  sync_enabled: boolean;
55
- root_directory_url?: string; // AutomergeUrl of the root directory document
56
- defaults: {
57
- exclude_patterns: string[];
58
- large_file_threshold: string;
59
- };
60
- diff: {
61
- external_tool?: string;
62
- show_binary: boolean;
63
- };
64
- sync: {
65
- move_detection_threshold: number;
66
- prompt_threshold: number;
67
- auto_sync: boolean;
68
- parallel_operations: number;
69
- };
70
28
  }
71
29
 
72
30
  /**
73
31
  * CLI command options
74
32
  */
75
33
  export interface CommandOptions {
76
- dryRun?: boolean;
77
34
  verbose?: boolean;
78
- debug?: boolean;
79
- tool?: string;
80
- nameOnly?: boolean;
81
- oneline?: boolean;
82
- remote?: string;
83
35
  }
84
36
 
85
37
  /**
@@ -88,22 +40,21 @@ export interface CommandOptions {
88
40
  export interface CloneOptions extends CommandOptions {
89
41
  force?: boolean; // Overwrite existing directory
90
42
  syncServer?: string; // Custom sync server URL
91
- syncServerStorageId?: string; // Custom sync server storage ID
43
+ syncServerStorageId?: StorageId; // Custom sync server storage ID
92
44
  }
93
45
 
94
46
  /**
95
47
  * Sync command specific options
96
48
  */
97
49
  export interface SyncOptions extends CommandOptions {
98
- dryRun: boolean;
99
50
  force?: boolean;
51
+ dryRun?: boolean;
100
52
  }
101
53
 
102
54
  /**
103
55
  * Diff command specific options
104
56
  */
105
57
  export interface DiffOptions extends CommandOptions {
106
- tool?: string;
107
58
  nameOnly: boolean;
108
59
  }
109
60
 
@@ -128,31 +79,7 @@ export interface CheckoutOptions extends CommandOptions {
128
79
  */
129
80
  export interface InitOptions extends CommandOptions {
130
81
  syncServer?: string;
131
- syncServerStorageId?: string;
132
- }
133
-
134
- /**
135
- * Commit command specific options
136
- */
137
- export interface CommitOptions extends CommandOptions {
138
- dryRun?: boolean;
139
- }
140
-
141
- /**
142
- * Status command specific options
143
- */
144
- export interface StatusOptions extends CommandOptions {}
145
-
146
- /**
147
- * URL command specific options
148
- */
149
- export interface UrlOptions extends CommandOptions {}
150
-
151
- /**
152
- * List (ls) command specific options
153
- */
154
- export interface ListOptions extends CommandOptions {
155
- long?: boolean;
82
+ syncServerStorageId?: StorageId;
156
83
  }
157
84
 
158
85
  /**
@@ -166,8 +93,16 @@ export interface ConfigOptions extends CommandOptions {
166
93
  }
167
94
 
168
95
  /**
169
- * Debug command specific options
96
+ * Status command specific options
170
97
  */
171
- export interface DebugOptions extends CommandOptions {
98
+ export interface StatusOptions extends CommandOptions {
172
99
  verbose?: boolean;
173
100
  }
101
+
102
+ /**
103
+ * Watch command specific options
104
+ */
105
+ export interface WatchOptions extends CommandOptions {
106
+ script?: string; // Script to run before syncing
107
+ watchDir?: string; // Directory to watch (relative to working dir)
108
+ }
@@ -1,4 +1,5 @@
1
- import { AutomergeUrl } from "@automerge/automerge-repo";
1
+ import { AutomergeUrl, UrlHeads } from "@automerge/automerge-repo";
2
+ import * as A from "@automerge/automerge";
2
3
 
3
4
  /**
4
5
  * Entry in a directory document
@@ -26,7 +27,7 @@ export interface FileDocument {
26
27
  name: string;
27
28
  extension: string;
28
29
  mimeType: string;
29
- content: string | Uint8Array;
30
+ content: A.ImmutableString | Uint8Array;
30
31
  metadata: {
31
32
  permissions: number;
32
33
  };
@@ -71,3 +72,16 @@ export interface MoveCandidate {
71
72
  similarity: number;
72
73
  newContent?: string | Uint8Array; // Content at destination (may differ from source if modified during move)
73
74
  }
75
+
76
+ /**
77
+ * Represents a detected change
78
+ */
79
+ export interface DetectedChange {
80
+ path: string;
81
+ changeType: ChangeType;
82
+ fileType: FileType;
83
+ localContent: string | Uint8Array | null;
84
+ remoteContent: string | Uint8Array | null;
85
+ localHead?: UrlHeads;
86
+ remoteHead?: UrlHeads;
87
+ }
@@ -1,8 +1,3 @@
1
- // Document types
2
1
  export * from "./documents";
3
-
4
- // Snapshot and sync types
5
2
  export * from "./snapshot";
6
-
7
- // Configuration types
8
3
  export * from "./config";
@@ -64,26 +64,3 @@ export interface SyncError {
64
64
  error: Error;
65
65
  recoverable: boolean;
66
66
  }
67
-
68
- /**
69
- * Sync operation type
70
- */
71
- export enum SyncOperation {
72
- CREATE_FILE = "create_file",
73
- UPDATE_FILE = "update_file",
74
- DELETE_FILE = "delete_file",
75
- MOVE_FILE = "move_file",
76
- CREATE_DIRECTORY = "create_directory",
77
- DELETE_DIRECTORY = "delete_directory",
78
- MOVE_DIRECTORY = "move_directory",
79
- }
80
-
81
- /**
82
- * Pending sync operation
83
- */
84
- export interface PendingSyncOperation {
85
- operation: SyncOperation;
86
- path: string;
87
- newPath?: string;
88
- priority: number;
89
- }
@@ -13,16 +13,12 @@ export function isContentEqual(
13
13
  if (typeof content1 === "string") {
14
14
  return content1 === content2;
15
15
  } else {
16
- // Compare Uint8Array
16
+ // Compare Uint8Array using native Buffer.equals() for better performance
17
17
  const buf1 = content1 as Uint8Array;
18
18
  const buf2 = content2 as Uint8Array;
19
19
 
20
20
  if (buf1.length !== buf2.length) return false;
21
21
 
22
- for (let i = 0; i < buf1.length; i++) {
23
- if (buf1[i] !== buf2[i]) return false;
24
- }
25
-
26
- return true;
22
+ return Buffer.from(buf1).equals(Buffer.from(buf2));
27
23
  }
28
24
  }
@@ -0,0 +1,50 @@
1
+ import { AutomergeUrl, Repo } from "@automerge/automerge-repo";
2
+ import { DirectoryDocument } from "../types";
3
+
4
+ /**
5
+ * Find a file in the directory hierarchy by path
6
+ */
7
+ export async function findFileInDirectoryHierarchy(
8
+ repo: Repo,
9
+ directoryUrl: AutomergeUrl,
10
+ filePath: string
11
+ ): Promise<{ name: string; type: string; url: AutomergeUrl } | null> {
12
+ try {
13
+ const pathParts = filePath.split("/");
14
+ let currentDirUrl = directoryUrl;
15
+
16
+ // Navigate through directories to find the parent directory
17
+ for (let i = 0; i < pathParts.length - 1; i++) {
18
+ const dirName = pathParts[i];
19
+ const dirHandle = await repo.find<DirectoryDocument>(currentDirUrl);
20
+ const dirDoc = await dirHandle.doc();
21
+
22
+ if (!dirDoc) return null;
23
+
24
+ const subDirEntry = dirDoc.docs.find(
25
+ (entry: { name: string; type: string; url: AutomergeUrl }) =>
26
+ entry.name === dirName && entry.type === "folder"
27
+ );
28
+
29
+ if (!subDirEntry) return null;
30
+ currentDirUrl = subDirEntry.url;
31
+ }
32
+
33
+ // Now look for the file in the final directory
34
+ const fileName = pathParts[pathParts.length - 1];
35
+ const finalDirHandle = await repo.find<DirectoryDocument>(currentDirUrl);
36
+ const finalDirDoc = await finalDirHandle.doc();
37
+
38
+ if (!finalDirDoc) return null;
39
+
40
+ const fileEntry = finalDirDoc.docs.find(
41
+ (entry: { name: string; type: string; url: AutomergeUrl }) =>
42
+ entry.name === fileName && entry.type === "file"
43
+ );
44
+
45
+ return fileEntry || null;
46
+ } catch (error) {
47
+ // Failed to find file in hierarchy
48
+ return null;
49
+ }
50
+ }
package/src/utils/fs.ts CHANGED
@@ -4,6 +4,7 @@ import * as crypto from "crypto";
4
4
  import { glob } from "glob";
5
5
  import * as mimeTypes from "mime-types";
6
6
  import * as ignore from "ignore";
7
+ import * as A from "@automerge/automerge";
7
8
  import { FileSystemEntry, FileType } from "../types";
8
9
  import { isEnhancedTextFile } from "./mime-types";
9
10
 
@@ -95,12 +96,15 @@ export async function readFileContent(
95
96
  */
96
97
  export async function writeFileContent(
97
98
  filePath: string,
98
- content: string | Uint8Array
99
+ content: string | A.ImmutableString | Uint8Array
99
100
  ): Promise<void> {
100
101
  await ensureDirectoryExists(path.dirname(filePath));
101
102
 
102
103
  if (typeof content === "string") {
103
104
  await fs.writeFile(filePath, content, "utf8");
105
+ } else if (A.isImmutableString(content)) {
106
+ // Convert ImmutableString to regular string for filesystem operations
107
+ await fs.writeFile(filePath, content.toString(), "utf8");
104
108
  } else {
105
109
  await fs.writeFile(filePath, content);
106
110
  }
@@ -126,7 +130,7 @@ export async function removePath(filePath: string): Promise<void> {
126
130
  try {
127
131
  const stats = await fs.stat(filePath);
128
132
  if (stats.isDirectory()) {
129
- await fs.rmdir(filePath, { recursive: true });
133
+ await fs.rm(filePath, { recursive: true });
130
134
  } else {
131
135
  await fs.unlink(filePath);
132
136
  }
@@ -168,33 +172,34 @@ export async function listDirectory(
168
172
  const entries: FileSystemEntry[] = [];
169
173
 
170
174
  try {
175
+ // Construct pattern using path.join for proper cross-platform handling
171
176
  const pattern = recursive
172
177
  ? path.join(dirPath, "**/*")
173
178
  : path.join(dirPath, "*");
179
+
180
+ // CRITICAL: glob expects forward slashes, even on Windows
181
+ // Convert backslashes to forward slashes for glob pattern
182
+ const normalizedPattern = pattern.replace(/\\/g, "/");
174
183
 
175
- // Convert exclude patterns to glob ignore patterns
176
- const ignorePatterns = excludePatterns.map((pattern) => {
177
- if (pattern.startsWith(".") && !pattern.includes("*")) {
178
- // Directory patterns
179
- return `${pattern}/**`;
180
- }
181
- return pattern;
182
- });
183
-
184
- const paths = await glob(pattern, {
184
+ // Use glob to get all paths (with dot files)
185
+ // Note: We don't use glob's ignore option because it doesn't support gitignore semantics
186
+ const paths = await glob(normalizedPattern, {
185
187
  dot: true,
186
- ignore: ignorePatterns,
187
188
  });
188
189
 
189
- for (const filePath of paths) {
190
- // Additional filtering for safety
191
- if (!isExcluded(filePath, dirPath, excludePatterns)) {
192
- const entry = await getFileSystemEntry(filePath);
193
- if (entry) {
194
- entries.push(entry);
190
+ // Parallelize all stat calls for better performance
191
+ const allEntries = await Promise.all(
192
+ paths.map(async (filePath) => {
193
+ // Filter using proper gitignore semantics from the ignore library
194
+ if (isExcluded(filePath, dirPath, excludePatterns)) {
195
+ return null;
195
196
  }
196
- }
197
- }
197
+ return await getFileSystemEntry(filePath);
198
+ })
199
+ );
200
+
201
+ // Filter out null entries (excluded files or files that couldn't be read)
202
+ entries.push(...allEntries.filter((e): e is FileSystemEntry => e !== null));
198
203
  } catch {
199
204
  // Return empty array if directory doesn't exist or can't be read
200
205
  }
@@ -232,10 +237,14 @@ export async function movePath(
232
237
  * Calculate content hash for change detection
233
238
  */
234
239
  export async function calculateContentHash(
235
- content: string | Uint8Array
240
+ content: string | A.ImmutableString | Uint8Array
236
241
  ): Promise<string> {
237
242
  const hash = crypto.createHash("sha256");
238
- hash.update(content);
243
+ if (A.isImmutableString(content)) {
244
+ hash.update(content.toString());
245
+ } else {
246
+ hash.update(content);
247
+ }
239
248
  return hash.digest("hex");
240
249
  }
241
250
 
@@ -256,14 +265,40 @@ export function getFileExtension(filePath: string): string {
256
265
 
257
266
  /**
258
267
  * Normalize path separators for cross-platform compatibility
268
+ * Converts all path separators to forward slashes for consistent storage
259
269
  */
260
270
  export function normalizePath(filePath: string): string {
261
271
  return path.posix.normalize(filePath.replace(/\\/g, "/"));
262
272
  }
263
273
 
274
+ /**
275
+ * Join paths and normalize separators for cross-platform compatibility
276
+ * Use this instead of string concatenation to ensure proper path handling on Windows
277
+ */
278
+ export function joinAndNormalizePath(...paths: string[]): string {
279
+ // Use path.join to properly handle path construction (handles Windows drive letters, etc.)
280
+ const joined = path.join(...paths);
281
+ // Then normalize to forward slashes for consistent storage/comparison
282
+ return normalizePath(joined);
283
+ }
284
+
264
285
  /**
265
286
  * Get relative path from base directory
266
287
  */
267
288
  export function getRelativePath(basePath: string, filePath: string): string {
268
289
  return normalizePath(path.relative(basePath, filePath));
269
290
  }
291
+
292
+ /**
293
+ * Format a path as a relative path with proper prefix
294
+ * Ensures paths like "src" become "./src" for clarity
295
+ * Leaves absolute paths and paths already starting with . or .. unchanged
296
+ */
297
+ export function formatRelativePath(filePath: string): string {
298
+ // Already starts with . or / - leave as-is
299
+ if (filePath.startsWith(".") || filePath.startsWith("/")) {
300
+ return filePath;
301
+ }
302
+ // Add ./ prefix for clarity
303
+ return `./${filePath}`;
304
+ }
@@ -1,7 +1,3 @@
1
- // File system utilities
2
1
  export * from "./fs";
3
-
4
- // Content similarity utilities
5
-
6
- // Enhanced MIME type detection
7
2
  export * from "./mime-types";
3
+ export * from "./directory";
@@ -109,8 +109,9 @@ const FORCE_TEXT_EXTENSIONS = new Set([
109
109
  * Get enhanced MIME type for file with custom dev file support
110
110
  */
111
111
  export function getEnhancedMimeType(filePath: string): string {
112
- const filename = filePath.split("/").pop() || "";
113
- const extension = getFileExtension(filePath);
112
+ const normalized = normalizePathSeparators(filePath);
113
+ const filename = normalized.split("/").pop() || "";
114
+ const extension = getFileExtension(normalized);
114
115
 
115
116
  // Check custom definitions first (by extension)
116
117
  if (extension && CUSTOM_MIME_TYPES[extension]) {
@@ -123,7 +124,7 @@ export function getEnhancedMimeType(filePath: string): string {
123
124
  }
124
125
 
125
126
  // Fall back to standard mime-types library
126
- const standardMime = mimeTypes.lookup(filePath);
127
+ const standardMime = mimeTypes.lookup(normalized);
127
128
  if (standardMime) {
128
129
  return standardMime;
129
130
  }
@@ -145,7 +146,14 @@ export function shouldForceAsText(filePath: string): boolean {
145
146
  */
146
147
  function getFileExtension(filePath: string): string {
147
148
  const match = filePath.match(/\.[^.]*$/);
148
- return match ? match[0] : "";
149
+ return match ? match[0].toLowerCase() : "";
150
+ }
151
+
152
+ /**
153
+ * Normalize path separators to forward slashes for cross-platform consistency
154
+ */
155
+ function normalizePathSeparators(p: string): string {
156
+ return p.replace(/\\/g, "/");
149
157
  }
150
158
 
151
159
  /**