pushwork 1.0.17 → 1.0.20
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/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +3 -0
- package/dist/commands.js.map +1 -1
- package/dist/core/change-detection.d.ts.map +1 -1
- package/dist/core/change-detection.js +23 -10
- package/dist/core/change-detection.js.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +5 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/sync-engine.d.ts +12 -0
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +78 -39
- 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 +3 -0
- package/dist/types/documents.d.ts.map +1 -1
- package/dist/types/documents.js.map +1 -1
- package/dist/utils/network-sync.js +1 -1
- package/package.json +1 -1
- package/src/commands.ts +3 -0
- package/src/core/change-detection.ts +20 -10
- package/src/core/config.ts +6 -0
- package/src/core/sync-engine.ts +89 -42
- package/src/types/config.ts +1 -0
- package/src/types/documents.ts +3 -0
- package/src/utils/network-sync.ts +1 -1
- package/test/integration/exclude-patterns.test.ts +1 -0
- package/test/integration/sync-flow.test.ts +2 -0
package/src/core/sync-engine.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {SnapshotManager} from "./snapshot"
|
|
|
36
36
|
import {ChangeDetector} from "./change-detection"
|
|
37
37
|
import {MoveDetector} from "./move-detection"
|
|
38
38
|
import {out} from "../utils/output"
|
|
39
|
+
import * as path from "path"
|
|
39
40
|
|
|
40
41
|
const isDebug = !!process.env.DEBUG
|
|
41
42
|
function debug(...args: any[]) {
|
|
@@ -110,6 +111,30 @@ export class SyncEngine {
|
|
|
110
111
|
return stringifyAutomergeUrl({documentId, heads})
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Determine if a file path is inside an artifact directory.
|
|
116
|
+
* Artifact files are stored as immutable strings (RawString) and
|
|
117
|
+
* referenced with versioned URLs in directory entries.
|
|
118
|
+
*/
|
|
119
|
+
private isArtifactPath(filePath: string): boolean {
|
|
120
|
+
const artifactDirs = this.config.artifact_directories || []
|
|
121
|
+
return artifactDirs.some(
|
|
122
|
+
dir => filePath === dir || filePath.startsWith(dir + "/")
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the appropriate URL for a directory entry.
|
|
128
|
+
* Artifact paths get versioned URLs (with heads) for exact version fetching.
|
|
129
|
+
* Non-artifact paths get plain URLs for collaborative editing.
|
|
130
|
+
*/
|
|
131
|
+
private getEntryUrl(handle: DocHandle<unknown>, filePath: string): AutomergeUrl {
|
|
132
|
+
if (this.isArtifactPath(filePath)) {
|
|
133
|
+
return this.getVersionedUrl(handle)
|
|
134
|
+
}
|
|
135
|
+
return getPlainUrl(handle.url)
|
|
136
|
+
}
|
|
137
|
+
|
|
113
138
|
/**
|
|
114
139
|
* Set the root directory URL in the snapshot
|
|
115
140
|
*/
|
|
@@ -527,8 +552,8 @@ export class SyncEngine {
|
|
|
527
552
|
// New file
|
|
528
553
|
const handle = await this.createRemoteFile(change)
|
|
529
554
|
if (handle) {
|
|
530
|
-
const
|
|
531
|
-
newEntries.push({name: fileName, url:
|
|
555
|
+
const entryUrl = this.getEntryUrl(handle, change.path)
|
|
556
|
+
newEntries.push({name: fileName, url: entryUrl})
|
|
532
557
|
this.snapshotManager.updateFileEntry(
|
|
533
558
|
snapshot,
|
|
534
559
|
change.path,
|
|
@@ -537,7 +562,7 @@ export class SyncEngine {
|
|
|
537
562
|
this.rootPath,
|
|
538
563
|
change.path
|
|
539
564
|
),
|
|
540
|
-
url:
|
|
565
|
+
url: entryUrl,
|
|
541
566
|
head: handle.heads(),
|
|
542
567
|
extension: getFileExtension(change.path),
|
|
543
568
|
mimeType: getEnhancedMimeType(change.path),
|
|
@@ -553,7 +578,7 @@ export class SyncEngine {
|
|
|
553
578
|
snapshot,
|
|
554
579
|
change.path
|
|
555
580
|
)
|
|
556
|
-
// Get current
|
|
581
|
+
// Get current entry URL (updateRemoteFile updates snapshot)
|
|
557
582
|
const updatedFileEntry = snapshot.files.get(change.path)
|
|
558
583
|
if (updatedFileEntry) {
|
|
559
584
|
const fileHandle =
|
|
@@ -562,7 +587,7 @@ export class SyncEngine {
|
|
|
562
587
|
)
|
|
563
588
|
updatedEntries.push({
|
|
564
589
|
name: fileName,
|
|
565
|
-
url: this.
|
|
590
|
+
url: this.getEntryUrl(fileHandle, change.path),
|
|
566
591
|
})
|
|
567
592
|
}
|
|
568
593
|
result.filesChanged++
|
|
@@ -593,7 +618,7 @@ export class SyncEngine {
|
|
|
593
618
|
)
|
|
594
619
|
subdirUpdates.push({
|
|
595
620
|
name: childName,
|
|
596
|
-
url: this.
|
|
621
|
+
url: this.getEntryUrl(childHandle, modifiedDir),
|
|
597
622
|
})
|
|
598
623
|
}
|
|
599
624
|
}
|
|
@@ -707,12 +732,11 @@ export class SyncEngine {
|
|
|
707
732
|
)
|
|
708
733
|
|
|
709
734
|
if (fileEntry) {
|
|
710
|
-
// Get versioned URL from handle (includes heads)
|
|
711
735
|
const fileHandle = await this.repo.find<FileDocument>(fileEntry.url)
|
|
712
|
-
const
|
|
736
|
+
const entryUrl = this.getEntryUrl(fileHandle, change.path)
|
|
713
737
|
this.snapshotManager.updateFileEntry(snapshot, change.path, {
|
|
714
738
|
path: localPath,
|
|
715
|
-
url:
|
|
739
|
+
url: entryUrl,
|
|
716
740
|
head: change.remoteHead,
|
|
717
741
|
extension: getFileExtension(change.path),
|
|
718
742
|
mimeType: getEnhancedMimeType(change.path),
|
|
@@ -774,11 +798,11 @@ export class SyncEngine {
|
|
|
774
798
|
}
|
|
775
799
|
})
|
|
776
800
|
|
|
777
|
-
// Get
|
|
778
|
-
const
|
|
801
|
+
// Get appropriate URL for directory entry
|
|
802
|
+
const entryUrl = this.getEntryUrl(handle, move.toPath)
|
|
779
803
|
|
|
780
|
-
// 4) Add file entry to destination directory
|
|
781
|
-
await this.addFileToDirectory(snapshot, move.toPath,
|
|
804
|
+
// 4) Add file entry to destination directory
|
|
805
|
+
await this.addFileToDirectory(snapshot, move.toPath, entryUrl)
|
|
782
806
|
|
|
783
807
|
// Track file handle for network sync
|
|
784
808
|
this.handlesByPath.set(move.toPath, handle)
|
|
@@ -788,7 +812,7 @@ export class SyncEngine {
|
|
|
788
812
|
this.snapshotManager.updateFileEntry(snapshot, move.toPath, {
|
|
789
813
|
...fromEntry,
|
|
790
814
|
path: joinAndNormalizePath(this.rootPath, move.toPath),
|
|
791
|
-
url:
|
|
815
|
+
url: entryUrl,
|
|
792
816
|
head: handle.heads(),
|
|
793
817
|
})
|
|
794
818
|
} catch (e) {
|
|
@@ -809,15 +833,21 @@ export class SyncEngine {
|
|
|
809
833
|
if (change.localContent === null) return null
|
|
810
834
|
|
|
811
835
|
const isText = this.isTextContent(change.localContent)
|
|
836
|
+
const isArtifact = this.isArtifactPath(change.path)
|
|
812
837
|
|
|
813
|
-
//
|
|
814
|
-
//
|
|
838
|
+
// For artifact files, store text as RawString (immutable snapshot).
|
|
839
|
+
// For regular files, store as collaborative text (empty string + splice).
|
|
815
840
|
const fileDoc: FileDocument = {
|
|
816
841
|
"@patchwork": {type: "file"},
|
|
817
842
|
name: change.path.split("/").pop() || "",
|
|
818
843
|
extension: getFileExtension(change.path),
|
|
819
844
|
mimeType: getEnhancedMimeType(change.path),
|
|
820
|
-
content:
|
|
845
|
+
content:
|
|
846
|
+
isText && isArtifact
|
|
847
|
+
? new A.RawString(change.localContent as string) as unknown as string
|
|
848
|
+
: isText
|
|
849
|
+
? ""
|
|
850
|
+
: change.localContent,
|
|
821
851
|
metadata: {
|
|
822
852
|
permissions: 0o644,
|
|
823
853
|
},
|
|
@@ -825,8 +855,8 @@ export class SyncEngine {
|
|
|
825
855
|
|
|
826
856
|
const handle = this.repo.create(fileDoc)
|
|
827
857
|
|
|
828
|
-
// For text files, splice in the content so it's stored as collaborative text
|
|
829
|
-
if (isText && typeof change.localContent === "string") {
|
|
858
|
+
// For non-artifact text files, splice in the content so it's stored as collaborative text
|
|
859
|
+
if (isText && !isArtifact && typeof change.localContent === "string") {
|
|
830
860
|
handle.change((doc: FileDocument) => {
|
|
831
861
|
updateTextContent(doc, ["content"], change.localContent as string)
|
|
832
862
|
})
|
|
@@ -855,14 +885,21 @@ export class SyncEngine {
|
|
|
855
885
|
const doc = await handle.doc()
|
|
856
886
|
const rawContent = doc?.content
|
|
857
887
|
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
)
|
|
888
|
+
// For artifact paths, always replace with a new document containing RawString.
|
|
889
|
+
// For non-artifact paths with immutable strings, replace with mutable text.
|
|
890
|
+
// In both cases we create a new document and update the snapshot URL.
|
|
891
|
+
const isArtifact = this.isArtifactPath(filePath)
|
|
892
|
+
if (
|
|
893
|
+
isArtifact ||
|
|
894
|
+
!doc ||
|
|
895
|
+
(rawContent != null && A.isImmutableString(rawContent))
|
|
896
|
+
) {
|
|
897
|
+
if (!isArtifact) {
|
|
898
|
+
out.taskLine(
|
|
899
|
+
`Replacing ${!doc ? 'unavailable' : 'immutable string'} document for ${filePath}`,
|
|
900
|
+
true
|
|
901
|
+
)
|
|
902
|
+
}
|
|
866
903
|
const fakeChange: DetectedChange = {
|
|
867
904
|
path: filePath,
|
|
868
905
|
changeType: ChangeType.LOCAL_ONLY,
|
|
@@ -874,10 +911,10 @@ export class SyncEngine {
|
|
|
874
911
|
}
|
|
875
912
|
const newHandle = await this.createRemoteFile(fakeChange)
|
|
876
913
|
if (newHandle) {
|
|
877
|
-
const
|
|
914
|
+
const entryUrl = this.getEntryUrl(newHandle, filePath)
|
|
878
915
|
this.snapshotManager.updateFileEntry(snapshot, filePath, {
|
|
879
916
|
path: joinAndNormalizePath(this.rootPath, filePath),
|
|
880
|
-
url:
|
|
917
|
+
url: entryUrl,
|
|
881
918
|
head: newHandle.heads(),
|
|
882
919
|
extension: getFileExtension(filePath),
|
|
883
920
|
mimeType: getEnhancedMimeType(filePath),
|
|
@@ -1059,19 +1096,18 @@ export class SyncEngine {
|
|
|
1059
1096
|
// Track discovered directory for sync
|
|
1060
1097
|
this.handlesByPath.set(directoryPath, childDirHandle)
|
|
1061
1098
|
|
|
1062
|
-
// Get
|
|
1063
|
-
const
|
|
1099
|
+
// Get appropriate URL for directory entry
|
|
1100
|
+
const entryUrl = this.getEntryUrl(childDirHandle, directoryPath)
|
|
1064
1101
|
|
|
1065
|
-
// Update snapshot with discovered directory
|
|
1102
|
+
// Update snapshot with discovered directory
|
|
1066
1103
|
this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
|
|
1067
1104
|
path: joinAndNormalizePath(this.rootPath, directoryPath),
|
|
1068
|
-
url:
|
|
1105
|
+
url: entryUrl,
|
|
1069
1106
|
head: childDirHandle.heads(),
|
|
1070
1107
|
entries: [],
|
|
1071
1108
|
})
|
|
1072
1109
|
|
|
1073
|
-
|
|
1074
|
-
return versionedUrl
|
|
1110
|
+
return entryUrl
|
|
1075
1111
|
} catch (resolveErr) {
|
|
1076
1112
|
// Failed to resolve directory - fall through to create a fresh directory document
|
|
1077
1113
|
}
|
|
@@ -1084,13 +1120,15 @@ export class SyncEngine {
|
|
|
1084
1120
|
// CREATE: Directory doesn't exist, create new one
|
|
1085
1121
|
const dirDoc: DirectoryDocument = {
|
|
1086
1122
|
"@patchwork": {type: "folder"},
|
|
1123
|
+
name: currentDirName,
|
|
1124
|
+
title: currentDirName,
|
|
1087
1125
|
docs: [],
|
|
1088
1126
|
}
|
|
1089
1127
|
|
|
1090
1128
|
const dirHandle = this.repo.create(dirDoc)
|
|
1091
1129
|
|
|
1092
|
-
// Get
|
|
1093
|
-
const
|
|
1130
|
+
// Get appropriate URL for directory entry
|
|
1131
|
+
const dirEntryUrl = this.getEntryUrl(dirHandle, directoryPath)
|
|
1094
1132
|
|
|
1095
1133
|
// Add this directory to its parent
|
|
1096
1134
|
// Use plain URL for mutable handle
|
|
@@ -1109,7 +1147,7 @@ export class SyncEngine {
|
|
|
1109
1147
|
doc.docs.push({
|
|
1110
1148
|
name: currentDirName,
|
|
1111
1149
|
type: "folder",
|
|
1112
|
-
url:
|
|
1150
|
+
url: dirEntryUrl,
|
|
1113
1151
|
})
|
|
1114
1152
|
didChange = true
|
|
1115
1153
|
}
|
|
@@ -1126,16 +1164,15 @@ export class SyncEngine {
|
|
|
1126
1164
|
}
|
|
1127
1165
|
}
|
|
1128
1166
|
|
|
1129
|
-
// Update snapshot with new directory
|
|
1167
|
+
// Update snapshot with new directory
|
|
1130
1168
|
this.snapshotManager.updateDirectoryEntry(snapshot, directoryPath, {
|
|
1131
1169
|
path: joinAndNormalizePath(this.rootPath, directoryPath),
|
|
1132
|
-
url:
|
|
1170
|
+
url: dirEntryUrl,
|
|
1133
1171
|
head: dirHandle.heads(),
|
|
1134
1172
|
entries: [],
|
|
1135
1173
|
})
|
|
1136
1174
|
|
|
1137
|
-
|
|
1138
|
-
return versionedDirUrl
|
|
1175
|
+
return dirEntryUrl
|
|
1139
1176
|
}
|
|
1140
1177
|
|
|
1141
1178
|
/**
|
|
@@ -1229,7 +1266,14 @@ export class SyncEngine {
|
|
|
1229
1266
|
const snapshotEntry = snapshot.directories.get(dirPath)
|
|
1230
1267
|
const heads = snapshotEntry?.head
|
|
1231
1268
|
|
|
1269
|
+
// Determine directory name
|
|
1270
|
+
const dirName = dirPath ? dirPath.split("/").pop() || "" : path.basename(this.rootPath)
|
|
1271
|
+
|
|
1232
1272
|
changeWithOptionalHeads(dirHandle, heads, (doc: DirectoryDocument) => {
|
|
1273
|
+
// Ensure name and title fields are set
|
|
1274
|
+
if (!doc.name) doc.name = dirName
|
|
1275
|
+
if (!doc.title) doc.title = dirName
|
|
1276
|
+
|
|
1233
1277
|
// Remove deleted file entries
|
|
1234
1278
|
for (const name of deletedNames) {
|
|
1235
1279
|
const idx = doc.docs.findIndex(
|
|
@@ -1421,8 +1465,11 @@ export class SyncEngine {
|
|
|
1421
1465
|
|
|
1422
1466
|
const timestamp = Date.now()
|
|
1423
1467
|
|
|
1468
|
+
const version = require("../../package.json").version
|
|
1469
|
+
|
|
1424
1470
|
changeWithOptionalHeads(rootHandle, heads, (doc: DirectoryDocument) => {
|
|
1425
1471
|
doc.lastSyncAt = timestamp
|
|
1472
|
+
doc.with = `pushwork@${version}`
|
|
1426
1473
|
})
|
|
1427
1474
|
|
|
1428
1475
|
// Track root directory for network sync
|
package/src/types/config.ts
CHANGED
package/src/types/documents.ts
CHANGED
|
@@ -14,8 +14,11 @@ export interface DirectoryEntry {
|
|
|
14
14
|
*/
|
|
15
15
|
export interface DirectoryDocument {
|
|
16
16
|
"@patchwork": {type: "folder"}
|
|
17
|
+
name: string
|
|
18
|
+
title: string
|
|
17
19
|
docs: DirectoryEntry[]
|
|
18
20
|
lastSyncAt?: number // Timestamp of last sync operation that made changes
|
|
21
|
+
with?: string // Tool identifier that last synced, e.g. "pushwork@1.0.19"
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -162,7 +162,7 @@ function headsMapEqual(
|
|
|
162
162
|
export async function waitForSync(
|
|
163
163
|
handlesToWaitOn: DocHandle<unknown>[],
|
|
164
164
|
syncServerStorageId?: StorageId,
|
|
165
|
-
timeoutMs: number =
|
|
165
|
+
timeoutMs: number = 60000,
|
|
166
166
|
): Promise<void> {
|
|
167
167
|
const startTime = Date.now();
|
|
168
168
|
|
|
@@ -106,6 +106,7 @@ describe("Exclude Patterns", () => {
|
|
|
106
106
|
sync_server: "wss://test.server.com",
|
|
107
107
|
sync_enabled: true,
|
|
108
108
|
exclude_patterns: [".git", "*.tmp", ".pushwork", "*.env"],
|
|
109
|
+
artifact_directories: ["dist"],
|
|
109
110
|
sync: {
|
|
110
111
|
move_detection_threshold: 0.8,
|
|
111
112
|
},
|
|
@@ -27,6 +27,7 @@ describe("Sync Flow Integration", () => {
|
|
|
27
27
|
sync_server: "wss://test.server.com",
|
|
28
28
|
sync_enabled: true,
|
|
29
29
|
exclude_patterns: [".git", "*.tmp"],
|
|
30
|
+
artifact_directories: ["dist"],
|
|
30
31
|
sync: {
|
|
31
32
|
move_detection_threshold: 0.8,
|
|
32
33
|
},
|
|
@@ -49,6 +50,7 @@ describe("Sync Flow Integration", () => {
|
|
|
49
50
|
sync_server: "wss://local.server.com",
|
|
50
51
|
sync_enabled: true,
|
|
51
52
|
exclude_patterns: [".git", "*.tmp"],
|
|
53
|
+
artifact_directories: ["dist"],
|
|
52
54
|
sync: {
|
|
53
55
|
move_detection_threshold: 0.9,
|
|
54
56
|
},
|