goldsync 0.1.13 → 0.1.15
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/README.md +4 -0
- package/assets/GoldSync.rbxmx +226 -49
- package/package.json +1 -1
- package/src/config.mjs +22 -2
- package/src/server.mjs +1 -0
- package/src/store.mjs +6 -1
package/README.md
CHANGED
|
@@ -73,6 +73,10 @@ Place `goldsync.project.json` at the game repository root:
|
|
|
73
73
|
|
|
74
74
|
A `file` root becomes one RBXM. A `directory` root creates separate files for descendants at `splitDepth`. Split at complete models, effects, or UI components—not individual parts or attachments. Never map the same hierarchy in Rojo, and do not include scripts inside a GoldSync root.
|
|
75
75
|
|
|
76
|
+
Directory roots can contain more specific roots. Discovery skips branches owned by those roots, including existing files on disk. For example, split `ReplicatedStorage.Assets` at depth 1 and add another depth-1 directory root for `ReplicatedStorage.Assets.Effects`. New asset categories and individual effects are discovered automatically, without creating an overlapping `Effects.rbxm`. Existing explicit file roots can stay in place to preserve their sync history. File roots cannot contain other roots because their snapshots include the entire subtree.
|
|
77
|
+
|
|
78
|
+
When upgrading an existing project, put the new parent directory roots in the optional top-level `discoveryRoots` array, using the same fields as directory roots. Older GoldSync versions ignore that array, so they cannot create overlapping snapshots before the service is restarted with nested-root support. Existing roots and files remain unchanged.
|
|
79
|
+
|
|
76
80
|
GoldSync reads the Rojo project name and fallback `servePort` from `default.project.json`. It automatically detects `rojo serve --port` overrides and verifies the listener's project name before connecting. That active Rojo session selects the GoldSync workspace, so Studio place IDs and names do not need configuration. Set `rojo.projectFile` only when the repository uses another project file.
|
|
77
81
|
|
|
78
82
|
Map GoldSync's generated live-session marker in the Rojo project tree:
|
package/assets/GoldSync.rbxmx
CHANGED
|
@@ -22,7 +22,7 @@ local CLIENT_HEADERS = {
|
|
|
22
22
|
}
|
|
23
23
|
local SETTINGS_PREFIX = "GoldSync:v2:"
|
|
24
24
|
local UI_SETTINGS_PREFIX = "GoldSync:ui:v1:"
|
|
25
|
-
local VERSION = "0.1.
|
|
25
|
+
local VERSION = "0.1.15"
|
|
26
26
|
local REQUEST_INTERVAL_SECONDS = 0.15
|
|
27
27
|
local REQUEST_RETRY_DELAYS = { 1, 2, 4 }
|
|
28
28
|
|
|
@@ -340,6 +340,8 @@ local lastRequestAt = 0
|
|
|
340
340
|
local serverConfig
|
|
341
341
|
local states = {}
|
|
342
342
|
local groups = {}
|
|
343
|
+
local batchingStatusUpdates = false
|
|
344
|
+
local splitRootConnections = {}
|
|
343
345
|
local refreshConfig
|
|
344
346
|
local updateGroupSummaries
|
|
345
347
|
local refreshStatusSummary
|
|
@@ -369,6 +371,31 @@ local function collectSplitPaths(root, depth)
|
|
|
369
371
|
return paths
|
|
370
372
|
end
|
|
371
373
|
|
|
374
|
+
local function collectSplitInstances(root, depth)
|
|
375
|
+
local current = { root }
|
|
376
|
+
for _ = 1, depth do
|
|
377
|
+
local nextLevel = {}
|
|
378
|
+
for _, parent in current do
|
|
379
|
+
for _, child in parent:GetChildren() do
|
|
380
|
+
table.insert(nextLevel, child)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
current = nextLevel
|
|
384
|
+
end
|
|
385
|
+
return current
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
local function isAtSplitDepth(root, instance, depth)
|
|
389
|
+
local current = instance
|
|
390
|
+
for _ = 1, depth do
|
|
391
|
+
if not current or current == root then
|
|
392
|
+
return false
|
|
393
|
+
end
|
|
394
|
+
current = current.Parent
|
|
395
|
+
end
|
|
396
|
+
return current == root
|
|
397
|
+
end
|
|
398
|
+
|
|
372
399
|
local function acquireRequestLock()
|
|
373
400
|
if requestLocked then
|
|
374
401
|
local waiter = Instance.new("BindableEvent")
|
|
@@ -464,6 +491,131 @@ local function resolveParent(studioPath)
|
|
|
464
491
|
return current
|
|
465
492
|
end
|
|
466
493
|
|
|
494
|
+
local function disconnectSplitRoot(id)
|
|
495
|
+
local watched = splitRootConnections[id]
|
|
496
|
+
if not watched then
|
|
497
|
+
return
|
|
498
|
+
end
|
|
499
|
+
for _, connection in watched.connections do
|
|
500
|
+
connection:Disconnect()
|
|
501
|
+
end
|
|
502
|
+
for _, connection in watched.nameConnections do
|
|
503
|
+
connection:Disconnect()
|
|
504
|
+
end
|
|
505
|
+
splitRootConnections[id] = nil
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
local function disconnectSplitRoots()
|
|
509
|
+
local ids = {}
|
|
510
|
+
for id in splitRootConnections do
|
|
511
|
+
table.insert(ids, id)
|
|
512
|
+
end
|
|
513
|
+
for _, id in ids do
|
|
514
|
+
disconnectSplitRoot(id)
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
local function discoverSplitRoot(splitRoot, root)
|
|
519
|
+
if not serverConfig or splitRootConnections[splitRoot.id] == nil then
|
|
520
|
+
return
|
|
521
|
+
end
|
|
522
|
+
local existingPaths = {}
|
|
523
|
+
for _, manifest in serverConfig.roots do
|
|
524
|
+
if manifest.splitRootId == splitRoot.id then
|
|
525
|
+
existingPaths[table.concat(manifest.studioPath, "\0")] = true
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
local missingPaths = {}
|
|
529
|
+
for _, relativePath in collectSplitPaths(root, splitRoot.splitDepth) do
|
|
530
|
+
local excluded = false
|
|
531
|
+
for _, excludedPath in splitRoot.excludedPaths or {} do
|
|
532
|
+
local matches = true
|
|
533
|
+
for index, segment in excludedPath do
|
|
534
|
+
if relativePath[index] ~= segment then
|
|
535
|
+
matches = false
|
|
536
|
+
break
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
if matches then
|
|
540
|
+
excluded = true
|
|
541
|
+
break
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
if excluded then
|
|
545
|
+
continue
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
local fullPath = table.clone(splitRoot.studioPath)
|
|
549
|
+
for _, segment in relativePath do
|
|
550
|
+
table.insert(fullPath, segment)
|
|
551
|
+
end
|
|
552
|
+
if not existingPaths[table.concat(fullPath, "\0")] then
|
|
553
|
+
table.insert(missingPaths, relativePath)
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
if #missingPaths > 0 then
|
|
557
|
+
request("POST", "/v1/splits/" .. splitRoot.id .. "/discover", HttpService:JSONEncode({ paths = missingPaths }), {
|
|
558
|
+
["Content-Type"] = "application/json",
|
|
559
|
+
})
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
local function watchSplitRoot(splitRoot)
|
|
564
|
+
local root = resolveRoot(splitRoot.studioPath)
|
|
565
|
+
local watched = splitRootConnections[splitRoot.id]
|
|
566
|
+
if watched and watched.root == root then
|
|
567
|
+
return
|
|
568
|
+
end
|
|
569
|
+
disconnectSplitRoot(splitRoot.id)
|
|
570
|
+
if not root then
|
|
571
|
+
return
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
local entry = {
|
|
575
|
+
root = root,
|
|
576
|
+
generation = 0,
|
|
577
|
+
connections = {},
|
|
578
|
+
nameConnections = {},
|
|
579
|
+
}
|
|
580
|
+
splitRootConnections[splitRoot.id] = entry
|
|
581
|
+
local function scheduleDiscovery()
|
|
582
|
+
entry.generation += 1
|
|
583
|
+
local generation = entry.generation
|
|
584
|
+
task.delay(0.1, function()
|
|
585
|
+
if running and splitRootConnections[splitRoot.id] == entry and entry.generation == generation then
|
|
586
|
+
discoverSplitRoot(splitRoot, root)
|
|
587
|
+
end
|
|
588
|
+
end)
|
|
589
|
+
end
|
|
590
|
+
local function watchName(instance)
|
|
591
|
+
if entry.nameConnections[instance] or not isAtSplitDepth(root, instance, splitRoot.splitDepth) then
|
|
592
|
+
return
|
|
593
|
+
end
|
|
594
|
+
entry.nameConnections[instance] = instance:GetPropertyChangedSignal("Name"):Connect(scheduleDiscovery)
|
|
595
|
+
end
|
|
596
|
+
for _, instance in collectSplitInstances(root, splitRoot.splitDepth) do
|
|
597
|
+
watchName(instance)
|
|
598
|
+
end
|
|
599
|
+
table.insert(entry.connections, root.DescendantAdded:Connect(function(descendant)
|
|
600
|
+
watchName(descendant)
|
|
601
|
+
scheduleDiscovery()
|
|
602
|
+
end))
|
|
603
|
+
table.insert(entry.connections, root.DescendantRemoving:Connect(function(descendant)
|
|
604
|
+
local connection = entry.nameConnections[descendant]
|
|
605
|
+
if connection then
|
|
606
|
+
connection:Disconnect()
|
|
607
|
+
entry.nameConnections[descendant] = nil
|
|
608
|
+
end
|
|
609
|
+
scheduleDiscovery()
|
|
610
|
+
end))
|
|
611
|
+
table.insert(entry.connections, root.AncestryChanged:Connect(function()
|
|
612
|
+
if resolveRoot(splitRoot.studioPath) ~= root then
|
|
613
|
+
disconnectSplitRoot(splitRoot.id)
|
|
614
|
+
end
|
|
615
|
+
end))
|
|
616
|
+
discoverSplitRoot(splitRoot, root)
|
|
617
|
+
end
|
|
618
|
+
|
|
467
619
|
local function applyClassIcon(image, className)
|
|
468
620
|
local success, icon = pcall(function()
|
|
469
621
|
return StudioService:GetClassIcon(className)
|
|
@@ -499,12 +651,8 @@ local function findCode(root)
|
|
|
499
651
|
end
|
|
500
652
|
|
|
501
653
|
local function setStatus(state, text, color)
|
|
502
|
-
local
|
|
503
|
-
|
|
504
|
-
state.status.TextColor3 = color or Color3.fromRGB(180, 180, 190)
|
|
505
|
-
state.dot.TextColor3 = color or Color3.fromRGB(180, 180, 190)
|
|
506
|
-
state.searchQuery = nil
|
|
507
|
-
state.statusKind = if string.sub(text, 1, 6) == "Synced"
|
|
654
|
+
local statusColor = color or Color3.fromRGB(180, 180, 190)
|
|
655
|
+
local statusKind = if string.sub(text, 1, 6) == "Synced"
|
|
508
656
|
then "synced"
|
|
509
657
|
elseif string.find(text, "syncing")
|
|
510
658
|
or string.find(text, "Serializing")
|
|
@@ -514,7 +662,16 @@ local function setStatus(state, text, color)
|
|
|
514
662
|
then "working"
|
|
515
663
|
elseif string.find(text, "Rojo") and string.find(text, "offline") then "paused"
|
|
516
664
|
else "issue"
|
|
517
|
-
if
|
|
665
|
+
if state.status.Text == text and state.status.TextColor3 == statusColor and state.statusKind == statusKind then
|
|
666
|
+
return
|
|
667
|
+
end
|
|
668
|
+
local previousStatusKind = state.statusKind
|
|
669
|
+
state.status.Text = text
|
|
670
|
+
state.status.TextColor3 = statusColor
|
|
671
|
+
state.dot.TextColor3 = statusColor
|
|
672
|
+
state.searchQuery = nil
|
|
673
|
+
state.statusKind = statusKind
|
|
674
|
+
if updateGroupSummaries and not batchingStatusUpdates then
|
|
518
675
|
if previousStatusKind ~= state.statusKind then
|
|
519
676
|
updateGroupSummaries()
|
|
520
677
|
elseif refreshStatusSummary then
|
|
@@ -1414,14 +1571,6 @@ local function createGroup(path, order)
|
|
|
1414
1571
|
section.LayoutOrder = order
|
|
1415
1572
|
section.Size = UDim2.new(1, 0, 0, 0)
|
|
1416
1573
|
section.Parent = if parentGroup then parentGroup.list else rootsContainer
|
|
1417
|
-
if parentGroup then
|
|
1418
|
-
local connector = Instance.new("Frame")
|
|
1419
|
-
connector.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
|
|
1420
|
-
connector.BorderSizePixel = 0
|
|
1421
|
-
connector.Position = UDim2.new(0, -8, 0, 11)
|
|
1422
|
-
connector.Size = UDim2.fromOffset(35, 1)
|
|
1423
|
-
connector.Parent = section
|
|
1424
|
-
end
|
|
1425
1574
|
|
|
1426
1575
|
local sectionLayout = Instance.new("UIListLayout")
|
|
1427
1576
|
sectionLayout.Padding = UDim.new(0, 2)
|
|
@@ -1434,6 +1583,14 @@ local function createGroup(path, order)
|
|
|
1434
1583
|
header.LayoutOrder = 1
|
|
1435
1584
|
header.Size = UDim2.new(1, 0, 0, 24)
|
|
1436
1585
|
header.Parent = section
|
|
1586
|
+
if parentGroup then
|
|
1587
|
+
local connector = Instance.new("Frame")
|
|
1588
|
+
connector.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
|
|
1589
|
+
connector.BorderSizePixel = 0
|
|
1590
|
+
connector.Position = UDim2.new(0, -8, 0, 11)
|
|
1591
|
+
connector.Size = UDim2.fromOffset(35, 1)
|
|
1592
|
+
connector.Parent = header
|
|
1593
|
+
end
|
|
1437
1594
|
|
|
1438
1595
|
local disclosure = Instance.new("TextButton")
|
|
1439
1596
|
disclosure.BackgroundTransparency = 1
|
|
@@ -1726,7 +1883,6 @@ local function createInstanceNode(instance, parent, depth, order)
|
|
|
1726
1883
|
connector.BorderSizePixel = 0
|
|
1727
1884
|
connector.Position = UDim2.new(0, -8, 0, 11)
|
|
1728
1885
|
connector.Size = UDim2.fromOffset(35, 1)
|
|
1729
|
-
connector.Parent = section
|
|
1730
1886
|
|
|
1731
1887
|
local header = Instance.new("Frame")
|
|
1732
1888
|
header.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
|
|
@@ -1734,6 +1890,7 @@ local function createInstanceNode(instance, parent, depth, order)
|
|
|
1734
1890
|
header.LayoutOrder = 1
|
|
1735
1891
|
header.Size = UDim2.new(1, 0, 0, 22)
|
|
1736
1892
|
header.Parent = section
|
|
1893
|
+
connector.Parent = header
|
|
1737
1894
|
header.MouseEnter:Connect(function()
|
|
1738
1895
|
header.BackgroundTransparency = 0
|
|
1739
1896
|
end)
|
|
@@ -1894,7 +2051,6 @@ local function createRootRow(manifest, order)
|
|
|
1894
2051
|
connector.BorderSizePixel = 0
|
|
1895
2052
|
connector.Position = UDim2.new(0, -8, 0, 11)
|
|
1896
2053
|
connector.Size = UDim2.fromOffset(35, 1)
|
|
1897
|
-
connector.Parent = row
|
|
1898
2054
|
|
|
1899
2055
|
local corner = Instance.new("UICorner")
|
|
1900
2056
|
corner.CornerRadius = UDim.new(0, 6)
|
|
@@ -1902,8 +2058,6 @@ local function createRootRow(manifest, order)
|
|
|
1902
2058
|
|
|
1903
2059
|
local rowPadding = Instance.new("UIPadding")
|
|
1904
2060
|
rowPadding.PaddingBottom = UDim.new(0, 5)
|
|
1905
|
-
rowPadding.PaddingLeft = UDim.new(0, 6)
|
|
1906
|
-
rowPadding.PaddingRight = UDim.new(0, 6)
|
|
1907
2061
|
rowPadding.PaddingTop = UDim.new(0, 5)
|
|
1908
2062
|
rowPadding.Parent = row
|
|
1909
2063
|
|
|
@@ -1918,6 +2072,7 @@ local function createRootRow(manifest, order)
|
|
|
1918
2072
|
titleLine.LayoutOrder = 1
|
|
1919
2073
|
titleLine.Size = UDim2.new(1, 0, 0, 22)
|
|
1920
2074
|
titleLine.Parent = row
|
|
2075
|
+
connector.Parent = titleLine
|
|
1921
2076
|
|
|
1922
2077
|
local contentDisclosure = Instance.new("TextButton")
|
|
1923
2078
|
contentDisclosure.BackgroundTransparency = 1
|
|
@@ -2393,6 +2548,19 @@ searchBox:GetPropertyChangedSignal("Text"):Connect(function()
|
|
|
2393
2548
|
end)
|
|
2394
2549
|
end)
|
|
2395
2550
|
|
|
2551
|
+
local function manifestChanged(previous, current)
|
|
2552
|
+
return not previous
|
|
2553
|
+
or previous.hash ~= current.hash
|
|
2554
|
+
or previous.exists ~= current.exists
|
|
2555
|
+
or previous.deleted ~= current.deleted
|
|
2556
|
+
or previous.revision ~= current.revision
|
|
2557
|
+
or previous.gitConflict ~= current.gitConflict
|
|
2558
|
+
or previous.conflictToken ~= current.conflictToken
|
|
2559
|
+
or previous.file ~= current.file
|
|
2560
|
+
or previous.splitRootId ~= current.splitRootId
|
|
2561
|
+
or table.concat(previous.studioPath, "\0") ~= table.concat(current.studioPath, "\0")
|
|
2562
|
+
end
|
|
2563
|
+
|
|
2396
2564
|
refreshConfig = function()
|
|
2397
2565
|
local response, requestError = request("GET", "/v1/config")
|
|
2398
2566
|
if not response or not response.Success then
|
|
@@ -2407,13 +2575,15 @@ refreshConfig = function()
|
|
|
2407
2575
|
end
|
|
2408
2576
|
|
|
2409
2577
|
local decoded = HttpService:JSONDecode(response.Body)
|
|
2410
|
-
|
|
2578
|
+
local projectChanged = serverConfig
|
|
2411
2579
|
and (serverConfig.projectId ~= decoded.projectId or serverConfig.workspaceId ~= decoded.workspaceId)
|
|
2412
|
-
|
|
2580
|
+
local connectionChanged = serverConfig and serverConfig.rojoConnected ~= decoded.rojoConnected
|
|
2581
|
+
if projectChanged then
|
|
2413
2582
|
for _, state in states do
|
|
2414
2583
|
disconnectRoot(state)
|
|
2415
2584
|
destroyConflictWorkspace(state)
|
|
2416
2585
|
end
|
|
2586
|
+
disconnectSplitRoots()
|
|
2417
2587
|
table.clear(states)
|
|
2418
2588
|
for _, child in rootsContainer:GetChildren() do
|
|
2419
2589
|
if child ~= rootsLayout then
|
|
@@ -2432,36 +2602,38 @@ refreshConfig = function()
|
|
|
2432
2602
|
end
|
|
2433
2603
|
connectionDot.TextColor3 = connectionStatus.TextColor3
|
|
2434
2604
|
|
|
2435
|
-
local
|
|
2436
|
-
for _, manifest in decoded.roots do
|
|
2437
|
-
if manifest.splitRootId then
|
|
2438
|
-
existingSplitPaths[manifest.splitRootId .. "\0" .. table.concat(manifest.studioPath, "\0")] = true
|
|
2439
|
-
end
|
|
2440
|
-
end
|
|
2605
|
+
local currentSplitRootIds = {}
|
|
2441
2606
|
for _, splitRoot in decoded.splitRoots do
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
end
|
|
2450
|
-
if not existingSplitPaths[splitRoot.id .. "\0" .. table.concat(fullPath, "\0")] then
|
|
2451
|
-
table.insert(missingPaths, relativePath)
|
|
2452
|
-
end
|
|
2453
|
-
end
|
|
2454
|
-
if #missingPaths > 0 then
|
|
2455
|
-
request("POST", "/v1/splits/" .. splitRoot.id .. "/discover", HttpService:JSONEncode({ paths = missingPaths }), {
|
|
2456
|
-
["Content-Type"] = "application/json",
|
|
2457
|
-
})
|
|
2458
|
-
end
|
|
2607
|
+
currentSplitRootIds[splitRoot.id] = true
|
|
2608
|
+
watchSplitRoot(splitRoot)
|
|
2609
|
+
end
|
|
2610
|
+
local staleSplitRootIds = {}
|
|
2611
|
+
for id in splitRootConnections do
|
|
2612
|
+
if not currentSplitRootIds[id] then
|
|
2613
|
+
table.insert(staleSplitRootIds, id)
|
|
2459
2614
|
end
|
|
2460
2615
|
end
|
|
2616
|
+
for _, id in staleSplitRootIds do
|
|
2617
|
+
disconnectSplitRoot(id)
|
|
2618
|
+
end
|
|
2461
2619
|
|
|
2620
|
+
local treeChanged = projectChanged == true or connectionChanged == true
|
|
2621
|
+
batchingStatusUpdates = true
|
|
2462
2622
|
for order, manifest in decoded.roots do
|
|
2463
|
-
local state = states[manifest.id]
|
|
2464
|
-
|
|
2623
|
+
local state = states[manifest.id]
|
|
2624
|
+
local created = state == nil
|
|
2625
|
+
if created then
|
|
2626
|
+
state = createRootRow(manifest, order)
|
|
2627
|
+
end
|
|
2628
|
+
local resolvedRoot = resolveRoot(manifest.studioPath)
|
|
2629
|
+
local rootChanged = state.observedRoot ~= resolvedRoot
|
|
2630
|
+
state.observedRoot = resolvedRoot
|
|
2631
|
+
if created or connectionChanged or rootChanged or manifestChanged(state.manifest, manifest) then
|
|
2632
|
+
reconcile(state, manifest)
|
|
2633
|
+
treeChanged = true
|
|
2634
|
+
else
|
|
2635
|
+
state.manifest = manifest
|
|
2636
|
+
end
|
|
2465
2637
|
end
|
|
2466
2638
|
local currentIds = {}
|
|
2467
2639
|
for _, manifest in decoded.roots do
|
|
@@ -2479,9 +2651,13 @@ refreshConfig = function()
|
|
|
2479
2651
|
destroyConflictWorkspace(state)
|
|
2480
2652
|
state.row:Destroy()
|
|
2481
2653
|
states[id] = nil
|
|
2654
|
+
treeChanged = true
|
|
2655
|
+
end
|
|
2656
|
+
batchingStatusUpdates = false
|
|
2657
|
+
if treeChanged then
|
|
2658
|
+
updateGroupSummaries()
|
|
2659
|
+
applyTreeFilter()
|
|
2482
2660
|
end
|
|
2483
|
-
updateGroupSummaries()
|
|
2484
|
-
applyTreeFilter()
|
|
2485
2661
|
return true
|
|
2486
2662
|
end
|
|
2487
2663
|
|
|
@@ -2497,6 +2673,7 @@ end)
|
|
|
2497
2673
|
|
|
2498
2674
|
plugin.Unloading:Connect(function()
|
|
2499
2675
|
running = false
|
|
2676
|
+
disconnectSplitRoots()
|
|
2500
2677
|
for _, state in states do
|
|
2501
2678
|
disconnectRoot(state)
|
|
2502
2679
|
destroyConflictWorkspace(state)
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -86,6 +86,10 @@ export async function loadConfig(configPath) {
|
|
|
86
86
|
if (!Array.isArray(raw.roots) || raw.roots.length === 0) {
|
|
87
87
|
throw new Error("roots must contain at least one sync root");
|
|
88
88
|
}
|
|
89
|
+
if (raw.discoveryRoots !== undefined && (!Array.isArray(raw.discoveryRoots)
|
|
90
|
+
|| raw.discoveryRoots.some((root) => !root || root.splitDepth === undefined))) {
|
|
91
|
+
throw new Error("discoveryRoots must contain directory roots with splitDepth");
|
|
92
|
+
}
|
|
89
93
|
const maxPathDepth = raw.maxPathDepth ?? 3;
|
|
90
94
|
if (!Number.isInteger(maxPathDepth) || maxPathDepth < 2 || maxPathDepth > 10) {
|
|
91
95
|
throw new Error("maxPathDepth must be an integer between 2 and 10");
|
|
@@ -95,8 +99,8 @@ export async function loadConfig(configPath) {
|
|
|
95
99
|
const studioPaths = new Set();
|
|
96
100
|
const files = new Set();
|
|
97
101
|
const splitRoots = [];
|
|
98
|
-
const roots = raw.roots.flatMap((root, index) => {
|
|
99
|
-
const label = `roots[${index}]`;
|
|
102
|
+
const roots = [...raw.roots, ...(raw.discoveryRoots ?? [])].flatMap((root, index) => {
|
|
103
|
+
const label = index < raw.roots.length ? `roots[${index}]` : `discoveryRoots[${index - raw.roots.length}]`;
|
|
100
104
|
const id = requireString(root.id, `${label}.id`);
|
|
101
105
|
if (!ID_PATTERN.test(id)) {
|
|
102
106
|
throw new Error(`${label}.id must contain lowercase letters, numbers, or hyphens`);
|
|
@@ -145,6 +149,22 @@ export async function loadConfig(configPath) {
|
|
|
145
149
|
}];
|
|
146
150
|
});
|
|
147
151
|
|
|
152
|
+
const configuredRoots = [...roots, ...splitRoots];
|
|
153
|
+
for (const root of roots) {
|
|
154
|
+
if (configuredRoots.some((other) => other !== root
|
|
155
|
+
&& other.studioPath.length > root.studioPath.length
|
|
156
|
+
&& root.studioPath.every((segment, index) => other.studioPath[index] === segment))) {
|
|
157
|
+
throw new Error(`file root ${root.id} contains another sync root; use a directory root instead`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const root of splitRoots) {
|
|
161
|
+
root.excludedPaths = configuredRoots
|
|
162
|
+
.filter((other) => other !== root
|
|
163
|
+
&& other.studioPath.length > root.studioPath.length
|
|
164
|
+
&& root.studioPath.every((segment, index) => other.studioPath[index] === segment))
|
|
165
|
+
.map((other) => other.studioPath.slice(root.studioPath.length, root.studioPath.length + root.splitDepth));
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
return {
|
|
149
169
|
version: 1,
|
|
150
170
|
name: requireString(raw.name, "name"),
|
package/src/server.mjs
CHANGED
|
@@ -161,6 +161,7 @@ export class GoldSyncServer {
|
|
|
161
161
|
id: root.id,
|
|
162
162
|
studioPath: root.studioPath,
|
|
163
163
|
splitDepth: root.splitDepth,
|
|
164
|
+
excludedPaths: root.excludedPaths ?? [],
|
|
164
165
|
})),
|
|
165
166
|
roots: this.store.getManifests().map((manifest) => {
|
|
166
167
|
const conflict = conflicts.get(manifest.id);
|
package/src/store.mjs
CHANGED
|
@@ -140,7 +140,12 @@ export class SnapshotStore {
|
|
|
140
140
|
}
|
|
141
141
|
let changed = false;
|
|
142
142
|
for (const relativePath of relativePaths) {
|
|
143
|
-
|
|
143
|
+
const root = this.makeSplitRoot(splitRoot, relativePath);
|
|
144
|
+
if ((splitRoot.excludedPaths ?? []).some((excluded) =>
|
|
145
|
+
excluded.every((segment, index) => relativePath[index] === segment))) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
changed = (await this.addRoot(root)) || changed;
|
|
144
149
|
}
|
|
145
150
|
if (changed) {
|
|
146
151
|
await this.persistState();
|