pushwork 1.0.4 → 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 (195) hide show
  1. package/README.md +87 -328
  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 +231 -170
  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 +6 -19
  13. package/dist/core/change-detection.d.ts.map +1 -1
  14. package/dist/core/change-detection.js +101 -80
  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 +12 -50
  25. package/dist/core/move-detection.d.ts.map +1 -1
  26. package/dist/core/move-detection.js +58 -139
  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 +220 -362
  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 +43 -67
  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 -3
  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 +3 -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 +63 -53
  64. package/dist/utils/fs.js.map +1 -1
  65. package/dist/utils/index.d.ts +1 -1
  66. package/dist/utils/index.d.ts.map +1 -1
  67. package/dist/utils/index.js +1 -4
  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 -22
  83. package/dist/utils/repo-factory.js.map +1 -1
  84. package/dist/utils/string-similarity.d.ts +14 -0
  85. package/dist/utils/string-similarity.d.ts.map +1 -0
  86. package/dist/utils/string-similarity.js +43 -0
  87. package/dist/utils/string-similarity.js.map +1 -0
  88. package/dist/utils/trace.d.ts +19 -0
  89. package/dist/utils/trace.d.ts.map +1 -0
  90. package/dist/utils/trace.js +68 -0
  91. package/dist/utils/trace.js.map +1 -0
  92. package/package.json +17 -12
  93. package/src/cli.ts +326 -252
  94. package/src/commands.ts +988 -0
  95. package/src/core/change-detection.ts +199 -162
  96. package/src/{config/index.ts → core/config.ts} +65 -82
  97. package/src/core/index.ts +1 -1
  98. package/src/core/move-detection.ts +74 -180
  99. package/src/core/snapshot.ts +2 -12
  100. package/src/core/sync-engine.ts +248 -499
  101. package/src/index.ts +0 -10
  102. package/src/types/config.ts +50 -72
  103. package/src/types/documents.ts +16 -3
  104. package/src/types/index.ts +0 -5
  105. package/src/types/snapshot.ts +1 -23
  106. package/src/utils/content.ts +2 -6
  107. package/src/utils/directory.ts +50 -0
  108. package/src/utils/fs.ts +67 -56
  109. package/src/utils/index.ts +1 -6
  110. package/src/utils/mime-types.ts +12 -4
  111. package/src/utils/network-sync.ts +79 -137
  112. package/src/utils/output.ts +450 -0
  113. package/src/utils/repo-factory.ts +13 -31
  114. package/src/utils/string-similarity.ts +54 -0
  115. package/src/utils/trace.ts +70 -0
  116. package/test/integration/exclude-patterns.test.ts +6 -15
  117. package/test/integration/fuzzer.test.ts +308 -391
  118. package/test/integration/init-sync.test.ts +89 -0
  119. package/test/integration/sync-deletion.test.ts +2 -61
  120. package/test/integration/sync-flow.test.ts +4 -24
  121. package/test/jest.setup.ts +34 -0
  122. package/test/unit/deletion-behavior.test.ts +3 -14
  123. package/test/unit/enhanced-mime-detection.test.ts +0 -22
  124. package/test/unit/snapshot.test.ts +2 -29
  125. package/test/unit/sync-convergence.test.ts +3 -198
  126. package/test/unit/sync-timing.test.ts +0 -44
  127. package/test/unit/utils.test.ts +0 -2
  128. package/tsconfig.json +3 -3
  129. package/dist/browser/browser-sync-engine.d.ts +0 -64
  130. package/dist/browser/browser-sync-engine.d.ts.map +0 -1
  131. package/dist/browser/browser-sync-engine.js +0 -303
  132. package/dist/browser/browser-sync-engine.js.map +0 -1
  133. package/dist/browser/filesystem-adapter.d.ts +0 -84
  134. package/dist/browser/filesystem-adapter.d.ts.map +0 -1
  135. package/dist/browser/filesystem-adapter.js +0 -413
  136. package/dist/browser/filesystem-adapter.js.map +0 -1
  137. package/dist/browser/index.d.ts +0 -36
  138. package/dist/browser/index.d.ts.map +0 -1
  139. package/dist/browser/index.js +0 -90
  140. package/dist/browser/index.js.map +0 -1
  141. package/dist/browser/types.d.ts +0 -70
  142. package/dist/browser/types.d.ts.map +0 -1
  143. package/dist/browser/types.js +0 -6
  144. package/dist/browser/types.js.map +0 -1
  145. package/dist/cli/commands.d.ts +0 -77
  146. package/dist/cli/commands.d.ts.map +0 -1
  147. package/dist/cli/commands.js +0 -904
  148. package/dist/cli/commands.js.map +0 -1
  149. package/dist/cli/index.d.ts +0 -2
  150. package/dist/cli/index.d.ts.map +0 -1
  151. package/dist/cli/index.js +0 -19
  152. package/dist/cli/index.js.map +0 -1
  153. package/dist/config/index.d.ts.map +0 -1
  154. package/dist/config/index.js.map +0 -1
  155. package/dist/core/isomorphic-snapshot.d.ts +0 -58
  156. package/dist/core/isomorphic-snapshot.d.ts.map +0 -1
  157. package/dist/core/isomorphic-snapshot.js +0 -204
  158. package/dist/core/isomorphic-snapshot.js.map +0 -1
  159. package/dist/platform/browser-filesystem.d.ts +0 -26
  160. package/dist/platform/browser-filesystem.d.ts.map +0 -1
  161. package/dist/platform/browser-filesystem.js +0 -91
  162. package/dist/platform/browser-filesystem.js.map +0 -1
  163. package/dist/platform/filesystem.d.ts +0 -29
  164. package/dist/platform/filesystem.d.ts.map +0 -1
  165. package/dist/platform/filesystem.js +0 -65
  166. package/dist/platform/filesystem.js.map +0 -1
  167. package/dist/platform/node-filesystem.d.ts +0 -21
  168. package/dist/platform/node-filesystem.d.ts.map +0 -1
  169. package/dist/platform/node-filesystem.js +0 -93
  170. package/dist/platform/node-filesystem.js.map +0 -1
  171. package/dist/utils/content-similarity.d.ts +0 -53
  172. package/dist/utils/content-similarity.d.ts.map +0 -1
  173. package/dist/utils/content-similarity.js +0 -155
  174. package/dist/utils/content-similarity.js.map +0 -1
  175. package/dist/utils/fs-browser.d.ts +0 -57
  176. package/dist/utils/fs-browser.d.ts.map +0 -1
  177. package/dist/utils/fs-browser.js +0 -311
  178. package/dist/utils/fs-browser.js.map +0 -1
  179. package/dist/utils/fs-node.d.ts +0 -53
  180. package/dist/utils/fs-node.d.ts.map +0 -1
  181. package/dist/utils/fs-node.js +0 -220
  182. package/dist/utils/fs-node.js.map +0 -1
  183. package/dist/utils/isomorphic.d.ts +0 -29
  184. package/dist/utils/isomorphic.d.ts.map +0 -1
  185. package/dist/utils/isomorphic.js +0 -139
  186. package/dist/utils/isomorphic.js.map +0 -1
  187. package/dist/utils/pure.d.ts +0 -25
  188. package/dist/utils/pure.d.ts.map +0 -1
  189. package/dist/utils/pure.js +0 -112
  190. package/dist/utils/pure.js.map +0 -1
  191. package/src/cli/commands.ts +0 -1207
  192. package/src/cli/index.ts +0 -2
  193. package/src/utils/content-similarity.ts +0 -194
  194. package/test/README-TESTING-GAPS.md +0 -174
  195. package/test/unit/content-similarity.test.ts +0 -236
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,91 +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
- }
29
-
30
- /**
31
- * Diff tool settings
32
- */
33
- export interface DiffSettings {
34
- external_tool?: string;
35
- show_binary: boolean;
36
- }
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;
37
9
 
38
10
  /**
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
- tool?: string;
79
- nameOnly?: boolean;
80
- oneline?: boolean;
81
- remote?: string;
82
- }
83
-
84
- /**
85
- * Init command specific options
86
- */
87
- export interface InitOptions extends CommandOptions {
88
- // No additional options needed - init creates a new sync directory
89
35
  }
90
36
 
91
37
  /**
@@ -94,22 +40,21 @@ export interface InitOptions extends CommandOptions {
94
40
  export interface CloneOptions extends CommandOptions {
95
41
  force?: boolean; // Overwrite existing directory
96
42
  syncServer?: string; // Custom sync server URL
97
- syncServerStorageId?: string; // Custom sync server storage ID
43
+ syncServerStorageId?: StorageId; // Custom sync server storage ID
98
44
  }
99
45
 
100
46
  /**
101
47
  * Sync command specific options
102
48
  */
103
49
  export interface SyncOptions extends CommandOptions {
104
- dryRun: boolean;
105
50
  force?: boolean;
51
+ dryRun?: boolean;
106
52
  }
107
53
 
108
54
  /**
109
55
  * Diff command specific options
110
56
  */
111
57
  export interface DiffOptions extends CommandOptions {
112
- tool?: string;
113
58
  nameOnly: boolean;
114
59
  }
115
60
 
@@ -128,3 +73,36 @@ export interface LogOptions extends CommandOptions {
128
73
  export interface CheckoutOptions extends CommandOptions {
129
74
  force?: boolean;
130
75
  }
76
+
77
+ /**
78
+ * Init command specific options
79
+ */
80
+ export interface InitOptions extends CommandOptions {
81
+ syncServer?: string;
82
+ syncServerStorageId?: StorageId;
83
+ }
84
+
85
+ /**
86
+ * Config command specific options
87
+ */
88
+ export interface ConfigOptions extends CommandOptions {
89
+ list?: boolean;
90
+ get?: string;
91
+ set?: string;
92
+ value?: string;
93
+ }
94
+
95
+ /**
96
+ * Status command specific options
97
+ */
98
+ export interface StatusOptions extends CommandOptions {
99
+ verbose?: boolean;
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
  };
@@ -69,6 +70,18 @@ export interface MoveCandidate {
69
70
  fromPath: string;
70
71
  toPath: string;
71
72
  similarity: number;
72
- confidence: "auto" | "prompt" | "low";
73
73
  newContent?: string | Uint8Array; // Content at destination (may differ from source if modified during move)
74
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";
@@ -52,6 +52,7 @@ export interface SyncResult {
52
52
  directoriesChanged: number;
53
53
  errors: SyncError[];
54
54
  warnings: string[];
55
+ timings?: { [key: string]: number };
55
56
  }
56
57
 
57
58
  /**
@@ -63,26 +64,3 @@ export interface SyncError {
63
64
  error: Error;
64
65
  recoverable: boolean;
65
66
  }
66
-
67
- /**
68
- * Sync operation type
69
- */
70
- export enum SyncOperation {
71
- CREATE_FILE = "create_file",
72
- UPDATE_FILE = "update_file",
73
- DELETE_FILE = "delete_file",
74
- MOVE_FILE = "move_file",
75
- CREATE_DIRECTORY = "create_directory",
76
- DELETE_DIRECTORY = "delete_directory",
77
- MOVE_DIRECTORY = "move_directory",
78
- }
79
-
80
- /**
81
- * Pending sync operation
82
- */
83
- export interface PendingSyncOperation {
84
- operation: SyncOperation;
85
- path: string;
86
- newPath?: string;
87
- priority: number;
88
- }
@@ -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
@@ -3,6 +3,8 @@ import * as path from "path";
3
3
  import * as crypto from "crypto";
4
4
  import { glob } from "glob";
5
5
  import * as mimeTypes from "mime-types";
6
+ import * as ignore from "ignore";
7
+ import * as A from "@automerge/automerge";
6
8
  import { FileSystemEntry, FileType } from "../types";
7
9
  import { isEnhancedTextFile } from "./mime-types";
8
10
 
@@ -94,12 +96,15 @@ export async function readFileContent(
94
96
  */
95
97
  export async function writeFileContent(
96
98
  filePath: string,
97
- content: string | Uint8Array
99
+ content: string | A.ImmutableString | Uint8Array
98
100
  ): Promise<void> {
99
101
  await ensureDirectoryExists(path.dirname(filePath));
100
102
 
101
103
  if (typeof content === "string") {
102
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");
103
108
  } else {
104
109
  await fs.writeFile(filePath, content);
105
110
  }
@@ -125,7 +130,7 @@ export async function removePath(filePath: string): Promise<void> {
125
130
  try {
126
131
  const stats = await fs.stat(filePath);
127
132
  if (stats.isDirectory()) {
128
- await fs.rmdir(filePath, { recursive: true });
133
+ await fs.rm(filePath, { recursive: true });
129
134
  } else {
130
135
  await fs.unlink(filePath);
131
136
  }
@@ -137,48 +142,23 @@ export async function removePath(filePath: string): Promise<void> {
137
142
  }
138
143
 
139
144
  /**
140
- * Check if a path matches any of the exclude patterns
145
+ * Check if a path matches any of the exclude patterns using the ignore library
146
+ * Supports proper gitignore-style patterns (e.g., "node_modules", "*.tmp", ".git")
141
147
  */
142
148
  function isExcluded(
143
149
  filePath: string,
144
150
  basePath: string,
145
151
  excludePatterns: string[]
146
152
  ): boolean {
153
+ if (excludePatterns.length === 0) return false;
154
+
147
155
  const relativePath = path.relative(basePath, filePath);
148
156
 
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
- // CRITICAL FIX: Properly escape dots and anchor the pattern
163
- // Convert glob to regex: *.tmp -> ^.*\.tmp$ (not /.*.tmp/ which matches "fuftmp.ts"!)
164
- const regexPattern = pattern
165
- .replace(/\./g, "\\.") // Escape dots first
166
- .replace(/\*/g, ".*") // Then convert * to .*
167
- .replace(/\?/g, "."); // And ? to single char
168
- const regex = new RegExp(`^${regexPattern}$`); // Anchor to match full path
169
- if (regex.test(relativePath)) {
170
- return true;
171
- }
172
- } else {
173
- // Exact directory name like "node_modules"
174
- const parts = relativePath.split(/[/\\]/);
175
- if (parts.includes(pattern)) {
176
- return true;
177
- }
178
- }
179
- }
157
+ // Use the ignore library which implements proper .gitignore semantics
158
+ // This is the same library used by ESLint and other major tools
159
+ const ig = ignore.default().add(excludePatterns);
180
160
 
181
- return false;
161
+ return ig.ignores(relativePath);
182
162
  }
183
163
 
184
164
  /**
@@ -192,33 +172,34 @@ export async function listDirectory(
192
172
  const entries: FileSystemEntry[] = [];
193
173
 
194
174
  try {
175
+ // Construct pattern using path.join for proper cross-platform handling
195
176
  const pattern = recursive
196
177
  ? path.join(dirPath, "**/*")
197
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, "/");
198
183
 
199
- // Convert exclude patterns to glob ignore patterns
200
- const ignorePatterns = excludePatterns.map((pattern) => {
201
- if (pattern.startsWith(".") && !pattern.includes("*")) {
202
- // Directory patterns
203
- return `${pattern}/**`;
204
- }
205
- return pattern;
206
- });
207
-
208
- 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, {
209
187
  dot: true,
210
- ignore: ignorePatterns,
211
188
  });
212
189
 
213
- for (const filePath of paths) {
214
- // Additional filtering for safety
215
- if (!isExcluded(filePath, dirPath, excludePatterns)) {
216
- const entry = await getFileSystemEntry(filePath);
217
- if (entry) {
218
- 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;
219
196
  }
220
- }
221
- }
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));
222
203
  } catch {
223
204
  // Return empty array if directory doesn't exist or can't be read
224
205
  }
@@ -256,10 +237,14 @@ export async function movePath(
256
237
  * Calculate content hash for change detection
257
238
  */
258
239
  export async function calculateContentHash(
259
- content: string | Uint8Array
240
+ content: string | A.ImmutableString | Uint8Array
260
241
  ): Promise<string> {
261
242
  const hash = crypto.createHash("sha256");
262
- hash.update(content);
243
+ if (A.isImmutableString(content)) {
244
+ hash.update(content.toString());
245
+ } else {
246
+ hash.update(content);
247
+ }
263
248
  return hash.digest("hex");
264
249
  }
265
250
 
@@ -280,14 +265,40 @@ export function getFileExtension(filePath: string): string {
280
265
 
281
266
  /**
282
267
  * Normalize path separators for cross-platform compatibility
268
+ * Converts all path separators to forward slashes for consistent storage
283
269
  */
284
270
  export function normalizePath(filePath: string): string {
285
271
  return path.posix.normalize(filePath.replace(/\\/g, "/"));
286
272
  }
287
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
+
288
285
  /**
289
286
  * Get relative path from base directory
290
287
  */
291
288
  export function getRelativePath(basePath: string, filePath: string): string {
292
289
  return normalizePath(path.relative(basePath, filePath));
293
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,8 +1,3 @@
1
- // File system utilities
2
1
  export * from "./fs";
3
-
4
- // Content similarity utilities
5
- export * from "./content-similarity";
6
-
7
- // Enhanced MIME type detection
8
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
  /**