goldsync 0.1.14 → 0.1.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/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:
@@ -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.14"
25
+ local VERSION = "0.1.16"
26
26
  local REQUEST_INTERVAL_SECONDS = 0.15
27
27
  local REQUEST_RETRY_DELAYS = { 1, 2, 4 }
28
28
 
@@ -185,13 +185,13 @@ local searchBox = Instance.new("TextBox")
185
185
  searchBox.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
186
186
  searchBox.BorderSizePixel = 0
187
187
  searchBox.ClearTextOnFocus = false
188
- searchBox.Font = Enum.Font.BuilderSans
189
- searchBox.PlaceholderColor3 = Color3.fromRGB(130, 130, 140)
188
+ searchBox.FontFace = Font.new("rbxasset://fonts/families/BuilderSans.json", Enum.FontWeight.Medium)
189
+ searchBox.PlaceholderColor3 = Color3.fromRGB(195, 195, 205)
190
190
  searchBox.PlaceholderText = "Search files and instances..."
191
191
  searchBox.Size = UDim2.new(1, -132, 1, 0)
192
192
  searchBox.Text = ""
193
193
  searchBox.TextColor3 = Color3.fromRGB(235, 235, 240)
194
- searchBox.TextSize = 12
194
+ searchBox.TextSize = 13
195
195
  searchBox.TextXAlignment = Enum.TextXAlignment.Left
196
196
  searchBox.Parent = controls
197
197
  Instance.new("UICorner", searchBox).CornerRadius = UDim.new(0, 4)
@@ -202,12 +202,12 @@ searchPadding.PaddingRight = UDim.new(0, 8)
202
202
  local unsyncedButton = Instance.new("TextButton")
203
203
  unsyncedButton.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
204
204
  unsyncedButton.BorderSizePixel = 0
205
- unsyncedButton.Font = Enum.Font.BuilderSansBold
205
+ unsyncedButton.FontFace = Font.new("rbxasset://fonts/families/BuilderSans.json", Enum.FontWeight.Medium)
206
206
  unsyncedButton.Position = UDim2.new(1, -126, 0, 0)
207
207
  unsyncedButton.Size = UDim2.fromOffset(92, 28)
208
208
  unsyncedButton.Text = "Unsynced · 0"
209
209
  unsyncedButton.TextColor3 = Color3.fromRGB(190, 190, 198)
210
- unsyncedButton.TextSize = 11
210
+ unsyncedButton.TextSize = 13
211
211
  unsyncedButton.Parent = controls
212
212
  Instance.new("UICorner", unsyncedButton).CornerRadius = UDim.new(0, 4)
213
213
 
@@ -292,12 +292,12 @@ local statusBanner = Instance.new("TextLabel")
292
292
  statusBanner.AutomaticSize = Enum.AutomaticSize.Y
293
293
  statusBanner.BackgroundColor3 = Color3.fromRGB(66, 38, 40)
294
294
  statusBanner.BorderSizePixel = 0
295
- statusBanner.Font = Enum.Font.BuilderSans
295
+ statusBanner.FontFace = Font.new("rbxasset://fonts/families/BuilderSans.json", Enum.FontWeight.Medium)
296
296
  statusBanner.LayoutOrder = 4
297
297
  statusBanner.Size = UDim2.new(1, 0, 0, 0)
298
298
  statusBanner.Text = ""
299
299
  statusBanner.TextColor3 = Color3.fromRGB(255, 205, 205)
300
- statusBanner.TextSize = 11
300
+ statusBanner.TextSize = 14
301
301
  statusBanner.TextWrapped = true
302
302
  statusBanner.TextXAlignment = Enum.TextXAlignment.Left
303
303
  statusBanner.Visible = false
@@ -309,13 +309,66 @@ statusBannerPadding.PaddingLeft = UDim.new(0, 8)
309
309
  statusBannerPadding.PaddingRight = UDim.new(0, 8)
310
310
  statusBannerPadding.PaddingTop = UDim.new(0, 7)
311
311
 
312
+ local attentionBox = Instance.new("Frame")
313
+ attentionBox.Name = "AttentionBox"
314
+ attentionBox.AutomaticSize = Enum.AutomaticSize.Y
315
+ attentionBox.BackgroundColor3 = Color3.fromRGB(43, 42, 38)
316
+ attentionBox.BorderSizePixel = 0
317
+ attentionBox.LayoutOrder = 5
318
+ attentionBox.Size = UDim2.fromScale(1, 0)
319
+ attentionBox.Visible = false
320
+ attentionBox.Parent = content
321
+ Instance.new("UICorner", attentionBox).CornerRadius = UDim.new(0.04, 0)
322
+ local attentionBoxLayout = Instance.new("UIListLayout")
323
+ attentionBoxLayout.SortOrder = Enum.SortOrder.LayoutOrder
324
+ attentionBoxLayout.Parent = attentionBox
325
+
326
+ local attentionActions = Instance.new("Frame")
327
+ attentionActions.BackgroundTransparency = 1
328
+ attentionActions.LayoutOrder = 1
329
+ attentionActions.Size = UDim2.new(1, 0, 0, 30)
330
+ attentionActions.Parent = attentionBox
331
+ local attentionBulkButtons = {}
332
+ for index, text in { "Push All →", "← Pull All" } do
333
+ local button = Instance.new("ImageButton")
334
+ button.AnchorPoint = Vector2.new(0.5, 0.5)
335
+ button.BackgroundColor3 = Color3.fromRGB(57, 57, 67)
336
+ button.BorderSizePixel = 0
337
+ button.Position = UDim2.fromScale(0.25 + (index - 1) * 0.5, 0.5)
338
+ button.Size = UDim2.fromScale(0.47, 0.8)
339
+ button.Parent = attentionActions
340
+ Instance.new("UICorner", button).CornerRadius = UDim.new(0.22, 0)
341
+ local label = Instance.new("TextLabel")
342
+ label.BackgroundTransparency = 1
343
+ label.Position = UDim2.fromScale(0.12, 0.18)
344
+ label.Size = UDim2.fromScale(0.76, 0.64)
345
+ label.Font = Enum.Font.BuilderSansBold
346
+ label.Text = text
347
+ label.TextColor3 = Color3.fromRGB(215, 225, 240)
348
+ label.TextScaled = true
349
+ label.Parent = button
350
+ attentionBulkButtons[index] = button
351
+ end
352
+
353
+ local attentionList = Instance.new("Frame")
354
+ attentionList.Name = "AttentionList"
355
+ attentionList.AutomaticSize = Enum.AutomaticSize.Y
356
+ attentionList.BackgroundTransparency = 1
357
+ attentionList.LayoutOrder = 5
358
+ attentionList.Size = UDim2.fromScale(1, 0)
359
+ attentionList.Visible = false
360
+ attentionList.Parent = attentionBox
361
+ local attentionLayout = Instance.new("UIListLayout")
362
+ attentionLayout.SortOrder = Enum.SortOrder.LayoutOrder
363
+ attentionLayout.Parent = attentionList
364
+
312
365
  local directionLegend = Instance.new("TextLabel")
313
366
  directionLegend.BackgroundTransparency = 1
314
- directionLegend.Font = Enum.Font.BuilderSans
315
- directionLegend.LayoutOrder = 5
367
+ directionLegend.FontFace = Font.new("rbxasset://fonts/families/BuilderSans.json", Enum.FontWeight.Medium)
368
+ directionLegend.LayoutOrder = 6
316
369
  directionLegend.Size = UDim2.new(1, 0, 0, 18)
317
370
  directionLegend.Text = "→ Studio to file ← File to Studio"
318
- directionLegend.TextColor3 = Color3.fromRGB(145, 145, 155)
371
+ directionLegend.TextColor3 = Color3.fromRGB(195, 195, 205)
319
372
  directionLegend.TextSize = 11
320
373
  directionLegend.TextXAlignment = Enum.TextXAlignment.Right
321
374
  directionLegend.Parent = content
@@ -323,7 +376,7 @@ directionLegend.Parent = content
323
376
  local rootsContainer = Instance.new("Frame")
324
377
  rootsContainer.AutomaticSize = Enum.AutomaticSize.Y
325
378
  rootsContainer.BackgroundTransparency = 1
326
- rootsContainer.LayoutOrder = 6
379
+ rootsContainer.LayoutOrder = 7
327
380
  rootsContainer.Size = UDim2.new(1, 0, 0, 0)
328
381
  rootsContainer.Parent = content
329
382
 
@@ -354,6 +407,34 @@ local resolveGitConflict
354
407
  local applyTreeFilter
355
408
  local resetInstanceTrees
356
409
 
410
+ for index, button in attentionBulkButtons do
411
+ button.Activated:Connect(function()
412
+ if bulkRunning or not serverConfig or not serverConfig.rojoConnected then
413
+ return
414
+ end
415
+ local affected = {}
416
+ for _, state in states do
417
+ if state.statusKind == "issue" and not state.manifest.gitConflict then
418
+ table.insert(affected, state)
419
+ end
420
+ end
421
+ bulkRunning = true
422
+ refreshStatusSummary()
423
+ for _, state in affected do
424
+ if states[state.manifest.id] ~= state or state.manifest.gitConflict then
425
+ continue
426
+ end
427
+ if index == 1 then
428
+ upload(state, true)
429
+ else
430
+ pull(state)
431
+ end
432
+ end
433
+ bulkRunning = false
434
+ refreshStatusSummary()
435
+ end)
436
+ end
437
+
357
438
  local function collectSplitPaths(root, depth)
358
439
  local paths = {}
359
440
  local function visit(instance, remainingDepth, relativePath)
@@ -527,6 +608,24 @@ local function discoverSplitRoot(splitRoot, root)
527
608
  end
528
609
  local missingPaths = {}
529
610
  for _, relativePath in collectSplitPaths(root, splitRoot.splitDepth) do
611
+ local excluded = false
612
+ for _, excludedPath in splitRoot.excludedPaths or {} do
613
+ local matches = true
614
+ for index, segment in excludedPath do
615
+ if relativePath[index] ~= segment then
616
+ matches = false
617
+ break
618
+ end
619
+ end
620
+ if matches then
621
+ excluded = true
622
+ break
623
+ end
624
+ end
625
+ if excluded then
626
+ continue
627
+ end
628
+
530
629
  local fullPath = table.clone(splitRoot.studioPath)
531
630
  for _, segment in relativePath do
532
631
  table.insert(fullPath, segment)
@@ -1553,14 +1652,6 @@ local function createGroup(path, order)
1553
1652
  section.LayoutOrder = order
1554
1653
  section.Size = UDim2.new(1, 0, 0, 0)
1555
1654
  section.Parent = if parentGroup then parentGroup.list else rootsContainer
1556
- if parentGroup then
1557
- local connector = Instance.new("Frame")
1558
- connector.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
1559
- connector.BorderSizePixel = 0
1560
- connector.Position = UDim2.new(0, -8, 0, 11)
1561
- connector.Size = UDim2.fromOffset(35, 1)
1562
- connector.Parent = section
1563
- end
1564
1655
 
1565
1656
  local sectionLayout = Instance.new("UIListLayout")
1566
1657
  sectionLayout.Padding = UDim.new(0, 2)
@@ -1573,6 +1664,14 @@ local function createGroup(path, order)
1573
1664
  header.LayoutOrder = 1
1574
1665
  header.Size = UDim2.new(1, 0, 0, 24)
1575
1666
  header.Parent = section
1667
+ if parentGroup then
1668
+ local connector = Instance.new("Frame")
1669
+ connector.BackgroundColor3 = Color3.fromRGB(72, 72, 82)
1670
+ connector.BorderSizePixel = 0
1671
+ connector.Position = UDim2.new(0, -8, 0, 11)
1672
+ connector.Size = UDim2.fromOffset(35, 1)
1673
+ connector.Parent = header
1674
+ end
1576
1675
 
1577
1676
  local disclosure = Instance.new("TextButton")
1578
1677
  disclosure.BackgroundTransparency = 1
@@ -1735,6 +1834,7 @@ updateGroupSummaries = function()
1735
1834
  local total = 0
1736
1835
  local synced = 0
1737
1836
  local issues = 0
1837
+ local issueColor = Color3.fromRGB(244, 194, 92)
1738
1838
  local pending = 0
1739
1839
  local paused = 0
1740
1840
  for _, state in states do
@@ -1744,6 +1844,9 @@ updateGroupSummaries = function()
1744
1844
  synced += 1
1745
1845
  elseif state.statusKind == "issue" then
1746
1846
  issues += 1
1847
+ if state.status.TextColor3 == Color3.fromRGB(245, 106, 106) then
1848
+ issueColor = state.status.TextColor3
1849
+ end
1747
1850
  elseif state.statusKind == "paused" then
1748
1851
  paused += 1
1749
1852
  else
@@ -1766,7 +1869,7 @@ updateGroupSummaries = function()
1766
1869
  group.bulkPush.Visible = true
1767
1870
  group.bulkPull.Visible = true
1768
1871
  group.statusDot.TextColor3 = if issues > 0
1769
- then Color3.fromRGB(245, 106, 106)
1872
+ then issueColor
1770
1873
  elseif synced == total then Color3.fromRGB(102, 214, 139)
1771
1874
  elseif pending > 0 or paused > 0 then Color3.fromRGB(244, 194, 92)
1772
1875
  else Color3.fromRGB(180, 180, 190)
@@ -1802,7 +1905,7 @@ refreshUnsyncedButton = function()
1802
1905
  then Color3.fromRGB(40, 40, 46)
1803
1906
  else Color3.fromRGB(78, 66, 38)
1804
1907
  unsyncedButton.TextColor3 = if showSynced
1805
- then Color3.fromRGB(190, 190, 198)
1908
+ then Color3.fromRGB(220, 220, 230)
1806
1909
  else Color3.fromRGB(255, 218, 126)
1807
1910
  end
1808
1911
 
@@ -1813,13 +1916,29 @@ refreshStatusSummary = function()
1813
1916
  local entry = {
1814
1917
  path = table.concat(state.manifest.studioPath, "."),
1815
1918
  status = state.status.Text,
1919
+ state = state,
1816
1920
  }
1817
1921
  if state.statusKind == "issue" then
1818
1922
  table.insert(issues, entry)
1819
- elseif state.statusKind == "working" then
1820
- table.insert(working, entry)
1923
+ else
1924
+ if state.row.Parent == attentionList then
1925
+ state.row.Parent = state.group.list
1926
+ state.connector.Visible = true
1927
+ state.row.LayoutOrder = state.treeOrder
1928
+ state.title.Text = state.manifest.studioPath[#state.manifest.studioPath]
1929
+ setRootExpanded(state, state.expandedBeforeAttention)
1930
+ end
1931
+ if state.statusKind == "working" then
1932
+ table.insert(working, entry)
1933
+ end
1821
1934
  end
1822
1935
  end
1936
+ attentionList.Visible = #issues > 0
1937
+ attentionBox.Visible = #issues > 0
1938
+ for _, button in attentionBulkButtons do
1939
+ button.Active = serverConfig.rojoConnected and not bulkRunning
1940
+ button.AutoButtonColor = button.Active
1941
+ end
1823
1942
  local entries = if #issues > 0 then issues else working
1824
1943
  if #entries == 0 then
1825
1944
  statusBanner.Visible = false
@@ -1833,19 +1952,25 @@ refreshStatusSummary = function()
1833
1952
  then string.format("%d need attention", #issues)
1834
1953
  else string.format("%d still syncing", #working),
1835
1954
  }
1836
- for index = 1, math.min(#entries, 3) do
1837
- local entry = entries[index]
1838
- table.insert(lines, entry.path .. " — " .. entry.status)
1839
- end
1840
- if #entries > 3 then
1841
- table.insert(lines, string.format("+%d more", #entries - 3))
1955
+ for index, entry in entries do
1956
+ if #issues > 0 then
1957
+ local state = entry.state
1958
+ if state.row.Parent ~= attentionList then
1959
+ state.expandedBeforeAttention = state.expanded
1960
+ state.row.Parent = attentionList
1961
+ state.connector.Visible = false
1962
+ setRootExpanded(state, false)
1963
+ end
1964
+ state.row.LayoutOrder = index
1965
+ state.title.Text = entry.path
1966
+ else
1967
+ table.insert(lines, entry.path .. " — " .. entry.status)
1968
+ end
1842
1969
  end
1843
1970
  statusBanner.BackgroundColor3 = if #issues > 0
1844
- then Color3.fromRGB(66, 38, 40)
1845
- else Color3.fromRGB(66, 56, 34)
1846
- statusBanner.TextColor3 = if #issues > 0
1847
- then Color3.fromRGB(255, 205, 205)
1848
- else Color3.fromRGB(255, 224, 145)
1971
+ then Color3.fromRGB(49, 46, 38)
1972
+ else Color3.fromRGB(43, 42, 38)
1973
+ statusBanner.TextColor3 = Color3.fromRGB(230, 222, 200)
1849
1974
  statusBanner.Text = table.concat(lines, "\n")
1850
1975
  statusBanner.Visible = true
1851
1976
  end
@@ -1865,7 +1990,6 @@ local function createInstanceNode(instance, parent, depth, order)
1865
1990
  connector.BorderSizePixel = 0
1866
1991
  connector.Position = UDim2.new(0, -8, 0, 11)
1867
1992
  connector.Size = UDim2.fromOffset(35, 1)
1868
- connector.Parent = section
1869
1993
 
1870
1994
  local header = Instance.new("Frame")
1871
1995
  header.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
@@ -1873,6 +1997,7 @@ local function createInstanceNode(instance, parent, depth, order)
1873
1997
  header.LayoutOrder = 1
1874
1998
  header.Size = UDim2.new(1, 0, 0, 22)
1875
1999
  header.Parent = section
2000
+ connector.Parent = header
1876
2001
  header.MouseEnter:Connect(function()
1877
2002
  header.BackgroundTransparency = 0
1878
2003
  end)
@@ -2033,7 +2158,6 @@ local function createRootRow(manifest, order)
2033
2158
  connector.BorderSizePixel = 0
2034
2159
  connector.Position = UDim2.new(0, -8, 0, 11)
2035
2160
  connector.Size = UDim2.fromOffset(35, 1)
2036
- connector.Parent = row
2037
2161
 
2038
2162
  local corner = Instance.new("UICorner")
2039
2163
  corner.CornerRadius = UDim.new(0, 6)
@@ -2041,8 +2165,6 @@ local function createRootRow(manifest, order)
2041
2165
 
2042
2166
  local rowPadding = Instance.new("UIPadding")
2043
2167
  rowPadding.PaddingBottom = UDim.new(0, 5)
2044
- rowPadding.PaddingLeft = UDim.new(0, 6)
2045
- rowPadding.PaddingRight = UDim.new(0, 6)
2046
2168
  rowPadding.PaddingTop = UDim.new(0, 5)
2047
2169
  rowPadding.Parent = row
2048
2170
 
@@ -2057,6 +2179,7 @@ local function createRootRow(manifest, order)
2057
2179
  titleLine.LayoutOrder = 1
2058
2180
  titleLine.Size = UDim2.new(1, 0, 0, 22)
2059
2181
  titleLine.Parent = row
2182
+ connector.Parent = titleLine
2060
2183
 
2061
2184
  local contentDisclosure = Instance.new("TextButton")
2062
2185
  contentDisclosure.BackgroundTransparency = 1
@@ -2256,6 +2379,8 @@ local function createRootRow(manifest, order)
2256
2379
  manifest = manifest,
2257
2380
  group = group,
2258
2381
  row = row,
2382
+ treeOrder = order,
2383
+ connector = connector,
2259
2384
  title = title,
2260
2385
  icon = icon,
2261
2386
  iconClass = if rootInstance then rootInstance.ClassName else "Model",
@@ -2566,6 +2691,7 @@ refreshConfig = function()
2566
2691
  for _, state in states do
2567
2692
  disconnectRoot(state)
2568
2693
  destroyConflictWorkspace(state)
2694
+ state.row:Destroy()
2569
2695
  end
2570
2696
  disconnectSplitRoots()
2571
2697
  table.clear(states)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goldsync",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Automatic native Roblox asset synchronization for Rojo projects.",
5
5
  "author": "tay",
6
6
  "license": "SEE LICENSE IN LICENSE.txt",
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
- changed = (await this.addRoot(this.makeSplitRoot(splitRoot, relativePath))) || changed;
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();