pushwork 1.0.15 → 1.0.16
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/CLAUDE.md +114 -0
- package/README.md +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +38 -11
- package/dist/commands.js.map +1 -1
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +4 -12
- package/dist/core/change-detection.js.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +3 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/sync-engine.d.ts +25 -40
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +277 -317
- package/dist/core/sync-engine.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/documents.d.ts +1 -2
- package/dist/types/documents.d.ts.map +1 -1
- package/dist/types/documents.js.map +1 -1
- package/dist/utils/fs.d.ts +2 -3
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +1 -11
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +12 -0
- package/src/commands.ts +41 -11
- package/src/core/change-detection.ts +557 -564
- package/src/core/config.ts +3 -3
- package/src/core/sync-engine.ts +1399 -1427
- package/src/types/config.ts +1 -0
- package/src/types/documents.ts +38 -39
- package/src/utils/fs.ts +170 -178
- package/src/utils/index.ts +4 -3
- package/src/utils/text-diff.ts +101 -0
- package/dist/cli/commands.d.ts +0 -61
- package/dist/cli/commands.d.ts.map +0 -1
- package/dist/cli/commands.js +0 -661
- package/dist/cli/commands.js.map +0 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -19
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/output.d.ts +0 -61
- package/dist/cli/output.d.ts.map +0 -1
- package/dist/cli/output.js +0 -176
- package/dist/cli/output.js.map +0 -1
- package/dist/config/index.d.ts +0 -71
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -314
- package/dist/config/index.js.map +0 -1
- package/dist/utils/content-similarity.d.ts +0 -53
- package/dist/utils/content-similarity.d.ts.map +0 -1
- package/dist/utils/content-similarity.js +0 -155
- package/dist/utils/content-similarity.js.map +0 -1
- package/dist/utils/keyhive.d.ts +0 -9
- package/dist/utils/keyhive.d.ts.map +0 -1
- package/dist/utils/keyhive.js +0 -26
- package/dist/utils/keyhive.js.map +0 -1
package/src/types/config.ts
CHANGED
package/src/types/documents.ts
CHANGED
|
@@ -1,87 +1,86 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as A from "@automerge/automerge";
|
|
1
|
+
import {AutomergeUrl, UrlHeads} from "@automerge/automerge-repo"
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Entry in a directory document
|
|
6
5
|
*/
|
|
7
6
|
export interface DirectoryEntry {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
name: string
|
|
8
|
+
type: "file" | "folder"
|
|
9
|
+
url: AutomergeUrl
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Directory document structure
|
|
15
14
|
*/
|
|
16
15
|
export interface DirectoryDocument {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
"@patchwork": {type: "folder"}
|
|
17
|
+
docs: DirectoryEntry[]
|
|
18
|
+
lastSyncAt?: number // Timestamp of last sync operation that made changes
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* File document structure
|
|
24
23
|
*/
|
|
25
24
|
export interface FileDocument {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
"@patchwork": {type: "file"}
|
|
26
|
+
name: string
|
|
27
|
+
extension: string
|
|
28
|
+
mimeType: string
|
|
29
|
+
content: string | Uint8Array
|
|
30
|
+
metadata: {
|
|
31
|
+
permissions: number
|
|
32
|
+
}
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
36
|
* File type classification
|
|
38
37
|
*/
|
|
39
38
|
export enum FileType {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
TEXT = "text",
|
|
40
|
+
BINARY = "binary",
|
|
41
|
+
DIRECTORY = "directory",
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
/**
|
|
46
45
|
* Change type classification for sync operations
|
|
47
46
|
*/
|
|
48
47
|
export enum ChangeType {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
NO_CHANGE = "no_change",
|
|
49
|
+
LOCAL_ONLY = "local_only",
|
|
50
|
+
REMOTE_ONLY = "remote_only",
|
|
51
|
+
BOTH_CHANGED = "both_changed",
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
/**
|
|
56
55
|
* File system entry metadata
|
|
57
56
|
*/
|
|
58
57
|
export interface FileSystemEntry {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
path: string
|
|
59
|
+
type: FileType
|
|
60
|
+
size: number
|
|
61
|
+
mtime: Date
|
|
62
|
+
permissions: number
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
/**
|
|
67
66
|
* Move detection result
|
|
68
67
|
*/
|
|
69
68
|
export interface MoveCandidate {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
fromPath: string
|
|
70
|
+
toPath: string
|
|
71
|
+
similarity: number
|
|
72
|
+
newContent?: string | Uint8Array // Content at destination (may differ from source if modified during move)
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
/**
|
|
77
76
|
* Represents a detected change
|
|
78
77
|
*/
|
|
79
78
|
export interface DetectedChange {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
path: string
|
|
80
|
+
changeType: ChangeType
|
|
81
|
+
fileType: FileType
|
|
82
|
+
localContent: string | Uint8Array | null
|
|
83
|
+
remoteContent: string | Uint8Array | null
|
|
84
|
+
localHead?: UrlHeads
|
|
85
|
+
remoteHead?: UrlHeads
|
|
87
86
|
}
|
package/src/utils/fs.ts
CHANGED
|
@@ -1,144 +1,140 @@
|
|
|
1
|
-
import * as fs from "fs/promises"
|
|
2
|
-
import * as path from "path"
|
|
3
|
-
import * as crypto from "crypto"
|
|
4
|
-
import {
|
|
5
|
-
import * as mimeTypes from "mime-types"
|
|
6
|
-
import * as ignore from "ignore"
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import { isEnhancedTextFile } from "./mime-types";
|
|
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 * as ignore from "ignore"
|
|
7
|
+
import {FileSystemEntry, FileType} from "../types"
|
|
8
|
+
import {isEnhancedTextFile} from "./mime-types"
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Check if a path exists
|
|
13
12
|
*/
|
|
14
13
|
export async function pathExists(filePath: string): Promise<boolean> {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(filePath)
|
|
16
|
+
return true
|
|
17
|
+
} catch {
|
|
18
|
+
return false
|
|
19
|
+
}
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
23
|
* Get file system entry metadata
|
|
25
24
|
*/
|
|
26
25
|
export async function getFileSystemEntry(
|
|
27
|
-
|
|
26
|
+
filePath: string
|
|
28
27
|
): Promise<FileSystemEntry | null> {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
28
|
+
try {
|
|
29
|
+
const stats = await fs.stat(filePath)
|
|
30
|
+
const type = stats.isDirectory()
|
|
31
|
+
? FileType.DIRECTORY
|
|
32
|
+
: (await isEnhancedTextFile(filePath))
|
|
33
|
+
? FileType.TEXT
|
|
34
|
+
: FileType.BINARY
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
path: filePath,
|
|
38
|
+
type,
|
|
39
|
+
size: stats.size,
|
|
40
|
+
mtime: stats.mtime,
|
|
41
|
+
permissions: stats.mode & parseInt("777", 8),
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
49
|
* Determine if a file is text or binary
|
|
51
50
|
*/
|
|
52
51
|
export async function isTextFile(filePath: string): Promise<boolean> {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
52
|
+
try {
|
|
53
|
+
const mimeType = mimeTypes.lookup(filePath)
|
|
54
|
+
if (mimeType) {
|
|
55
|
+
return (
|
|
56
|
+
mimeType.startsWith("text/") ||
|
|
57
|
+
mimeType === "application/json" ||
|
|
58
|
+
mimeType === "application/xml" ||
|
|
59
|
+
mimeType.includes("javascript") ||
|
|
60
|
+
mimeType.includes("typescript")
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Sample first 8KB to detect binary content
|
|
65
|
+
const handle = await fs.open(filePath, "r")
|
|
66
|
+
const buffer = Buffer.alloc(Math.min(8192, (await handle.stat()).size))
|
|
67
|
+
await handle.read(buffer, 0, buffer.length, 0)
|
|
68
|
+
await handle.close()
|
|
69
|
+
|
|
70
|
+
// Check for null bytes which indicate binary content
|
|
71
|
+
return !buffer.includes(0)
|
|
72
|
+
} catch {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
78
|
* Read file content as string or buffer
|
|
80
79
|
*/
|
|
81
80
|
export async function readFileContent(
|
|
82
|
-
|
|
81
|
+
filePath: string
|
|
83
82
|
): Promise<string | Uint8Array> {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
const isText = await isEnhancedTextFile(filePath)
|
|
84
|
+
|
|
85
|
+
if (isText) {
|
|
86
|
+
return await fs.readFile(filePath, "utf8")
|
|
87
|
+
} else {
|
|
88
|
+
const buffer = await fs.readFile(filePath)
|
|
89
|
+
return new Uint8Array(buffer)
|
|
90
|
+
}
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
/**
|
|
95
94
|
* Write file content from string or buffer
|
|
96
95
|
*/
|
|
97
96
|
export async function writeFileContent(
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
filePath: string,
|
|
98
|
+
content: string | Uint8Array
|
|
100
99
|
): Promise<void> {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} else {
|
|
109
|
-
await fs.writeFile(filePath, content);
|
|
110
|
-
}
|
|
100
|
+
await ensureDirectoryExists(path.dirname(filePath))
|
|
101
|
+
|
|
102
|
+
if (typeof content === "string") {
|
|
103
|
+
await fs.writeFile(filePath, content, "utf8")
|
|
104
|
+
} else {
|
|
105
|
+
await fs.writeFile(filePath, content)
|
|
106
|
+
}
|
|
111
107
|
}
|
|
112
108
|
|
|
113
109
|
/**
|
|
114
110
|
* Ensure directory exists, creating it if necessary
|
|
115
111
|
*/
|
|
116
112
|
export async function ensureDirectoryExists(dirPath: string): Promise<void> {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
try {
|
|
114
|
+
await fs.mkdir(dirPath, {recursive: true})
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
if (error.code !== "EEXIST") {
|
|
117
|
+
throw error
|
|
118
|
+
}
|
|
119
|
+
}
|
|
124
120
|
}
|
|
125
121
|
|
|
126
122
|
/**
|
|
127
123
|
* Remove file or directory
|
|
128
124
|
*/
|
|
129
125
|
export async function removePath(filePath: string): Promise<void> {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
126
|
+
try {
|
|
127
|
+
const stats = await fs.stat(filePath)
|
|
128
|
+
if (stats.isDirectory()) {
|
|
129
|
+
await fs.rm(filePath, {recursive: true})
|
|
130
|
+
} else {
|
|
131
|
+
await fs.unlink(filePath)
|
|
132
|
+
}
|
|
133
|
+
} catch (error: any) {
|
|
134
|
+
if (error.code !== "ENOENT") {
|
|
135
|
+
throw error
|
|
136
|
+
}
|
|
137
|
+
}
|
|
142
138
|
}
|
|
143
139
|
|
|
144
140
|
/**
|
|
@@ -146,120 +142,116 @@ export async function removePath(filePath: string): Promise<void> {
|
|
|
146
142
|
* Supports proper gitignore-style patterns (e.g., "node_modules", "*.tmp", ".git")
|
|
147
143
|
*/
|
|
148
144
|
function isExcluded(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
filePath: string,
|
|
146
|
+
basePath: string,
|
|
147
|
+
excludePatterns: string[]
|
|
152
148
|
): boolean {
|
|
153
|
-
|
|
149
|
+
if (excludePatterns.length === 0) return false
|
|
154
150
|
|
|
155
|
-
|
|
151
|
+
const relativePath = path.relative(basePath, filePath)
|
|
156
152
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
// Use the ignore library which implements proper .gitignore semantics
|
|
154
|
+
// This is the same library used by ESLint and other major tools
|
|
155
|
+
const ig = ignore.default().add(excludePatterns)
|
|
160
156
|
|
|
161
|
-
|
|
157
|
+
return ig.ignores(relativePath)
|
|
162
158
|
}
|
|
163
159
|
|
|
164
160
|
/**
|
|
165
161
|
* List directory contents with metadata
|
|
166
162
|
*/
|
|
167
163
|
export async function listDirectory(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
dirPath: string,
|
|
165
|
+
recursive = false,
|
|
166
|
+
excludePatterns: string[] = []
|
|
171
167
|
): Promise<FileSystemEntry[]> {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
168
|
+
const entries: FileSystemEntry[] = []
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
// Construct pattern using path.join for proper cross-platform handling
|
|
172
|
+
const pattern = recursive
|
|
173
|
+
? path.join(dirPath, "**/*")
|
|
174
|
+
: path.join(dirPath, "*")
|
|
175
|
+
|
|
176
|
+
// glob expects forward slashes, even on Windows
|
|
177
|
+
const normalizedPattern = pattern.replace(/\\/g, "/")
|
|
178
|
+
|
|
179
|
+
// Use glob to get all paths (with dot files)
|
|
180
|
+
// Note: We don't use glob's ignore option because it doesn't support gitignore semantics
|
|
181
|
+
const paths = await glob(normalizedPattern, {
|
|
182
|
+
dot: true,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Parallelize all stat calls for better performance
|
|
186
|
+
const allEntries = await Promise.all(
|
|
187
|
+
paths.map(async filePath => {
|
|
188
|
+
// Filter using proper gitignore semantics from the ignore library
|
|
189
|
+
if (isExcluded(filePath, dirPath, excludePatterns)) {
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
return await getFileSystemEntry(filePath)
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
// Filter out null entries (excluded files or files that couldn't be read)
|
|
197
|
+
entries.push(...allEntries.filter((e): e is FileSystemEntry => e !== null))
|
|
198
|
+
} catch {
|
|
199
|
+
// Return empty array if directory doesn't exist or can't be read
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return entries
|
|
207
203
|
}
|
|
208
204
|
|
|
209
205
|
/**
|
|
210
206
|
* Copy file with metadata preservation
|
|
211
207
|
*/
|
|
212
208
|
export async function copyFile(
|
|
213
|
-
|
|
214
|
-
|
|
209
|
+
sourcePath: string,
|
|
210
|
+
destPath: string
|
|
215
211
|
): Promise<void> {
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
await ensureDirectoryExists(path.dirname(destPath))
|
|
213
|
+
await fs.copyFile(sourcePath, destPath)
|
|
218
214
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
215
|
+
// Preserve file permissions
|
|
216
|
+
const stats = await fs.stat(sourcePath)
|
|
217
|
+
await fs.chmod(destPath, stats.mode)
|
|
222
218
|
}
|
|
223
219
|
|
|
224
220
|
/**
|
|
225
221
|
* Move/rename file or directory
|
|
226
222
|
*/
|
|
227
223
|
export async function movePath(
|
|
228
|
-
|
|
229
|
-
|
|
224
|
+
sourcePath: string,
|
|
225
|
+
destPath: string
|
|
230
226
|
): Promise<void> {
|
|
231
|
-
|
|
232
|
-
|
|
227
|
+
await ensureDirectoryExists(path.dirname(destPath))
|
|
228
|
+
await fs.rename(sourcePath, destPath)
|
|
233
229
|
}
|
|
234
230
|
|
|
235
231
|
/**
|
|
236
232
|
* Calculate content hash for change detection
|
|
237
233
|
*/
|
|
238
234
|
export async function calculateContentHash(
|
|
239
|
-
|
|
235
|
+
content: string | Uint8Array
|
|
240
236
|
): Promise<string> {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
} else {
|
|
245
|
-
hash.update(content);
|
|
246
|
-
}
|
|
247
|
-
return hash.digest("hex");
|
|
237
|
+
const hash = crypto.createHash("sha256")
|
|
238
|
+
hash.update(content)
|
|
239
|
+
return hash.digest("hex")
|
|
248
240
|
}
|
|
249
241
|
|
|
250
242
|
/**
|
|
251
243
|
* Get MIME type for file
|
|
252
244
|
*/
|
|
253
245
|
export function getMimeType(filePath: string): string {
|
|
254
|
-
|
|
246
|
+
return mimeTypes.lookup(filePath) || "application/octet-stream"
|
|
255
247
|
}
|
|
256
248
|
|
|
257
249
|
/**
|
|
258
250
|
* Get file extension
|
|
259
251
|
*/
|
|
260
252
|
export function getFileExtension(filePath: string): string {
|
|
261
|
-
|
|
262
|
-
|
|
253
|
+
const ext = path.extname(filePath)
|
|
254
|
+
return ext.startsWith(".") ? ext.slice(1) : ext
|
|
263
255
|
}
|
|
264
256
|
|
|
265
257
|
/**
|
|
@@ -267,7 +259,7 @@ export function getFileExtension(filePath: string): string {
|
|
|
267
259
|
* Converts all path separators to forward slashes for consistent storage
|
|
268
260
|
*/
|
|
269
261
|
export function normalizePath(filePath: string): string {
|
|
270
|
-
|
|
262
|
+
return path.posix.normalize(filePath.replace(/\\/g, "/"))
|
|
271
263
|
}
|
|
272
264
|
|
|
273
265
|
/**
|
|
@@ -275,17 +267,17 @@ export function normalizePath(filePath: string): string {
|
|
|
275
267
|
* Use this instead of string concatenation to ensure proper path handling on Windows
|
|
276
268
|
*/
|
|
277
269
|
export function joinAndNormalizePath(...paths: string[]): string {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
270
|
+
// Use path.join to properly handle path construction (handles Windows drive letters, etc.)
|
|
271
|
+
const joined = path.join(...paths)
|
|
272
|
+
// Then normalize to forward slashes for consistent storage/comparison
|
|
273
|
+
return normalizePath(joined)
|
|
282
274
|
}
|
|
283
275
|
|
|
284
276
|
/**
|
|
285
277
|
* Get relative path from base directory
|
|
286
278
|
*/
|
|
287
279
|
export function getRelativePath(basePath: string, filePath: string): string {
|
|
288
|
-
|
|
280
|
+
return normalizePath(path.relative(basePath, filePath))
|
|
289
281
|
}
|
|
290
282
|
|
|
291
283
|
/**
|
|
@@ -294,10 +286,10 @@ export function getRelativePath(basePath: string, filePath: string): string {
|
|
|
294
286
|
* Leaves absolute paths and paths already starting with . or .. unchanged
|
|
295
287
|
*/
|
|
296
288
|
export function formatRelativePath(filePath: string): string {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
289
|
+
// Already starts with . or / - leave as-is
|
|
290
|
+
if (filePath.startsWith(".") || filePath.startsWith("/")) {
|
|
291
|
+
return filePath
|
|
292
|
+
}
|
|
293
|
+
// Add ./ prefix for clarity
|
|
294
|
+
return `./${filePath}`
|
|
303
295
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export * from "./fs"
|
|
2
|
-
export * from "./mime-types"
|
|
3
|
-
export * from "./directory"
|
|
1
|
+
export * from "./fs"
|
|
2
|
+
export * from "./mime-types"
|
|
3
|
+
export * from "./directory"
|
|
4
|
+
export * from "./text-diff"
|