goldsync 0.1.14 → 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 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.15"
26
26
  local REQUEST_INTERVAL_SECONDS = 0.15
27
27
  local REQUEST_RETRY_DELAYS = { 1, 2, 4 }
28
28
 
@@ -527,6 +527,24 @@ local function discoverSplitRoot(splitRoot, root)
527
527
  end
528
528
  local missingPaths = {}
529
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
+
530
548
  local fullPath = table.clone(splitRoot.studioPath)
531
549
  for _, segment in relativePath do
532
550
  table.insert(fullPath, segment)
@@ -1553,14 +1571,6 @@ local function createGroup(path, order)
1553
1571
  section.LayoutOrder = order
1554
1572
  section.Size = UDim2.new(1, 0, 0, 0)
1555
1573
  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
1574
 
1565
1575
  local sectionLayout = Instance.new("UIListLayout")
1566
1576
  sectionLayout.Padding = UDim.new(0, 2)
@@ -1573,6 +1583,14 @@ local function createGroup(path, order)
1573
1583
  header.LayoutOrder = 1
1574
1584
  header.Size = UDim2.new(1, 0, 0, 24)
1575
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
1576
1594
 
1577
1595
  local disclosure = Instance.new("TextButton")
1578
1596
  disclosure.BackgroundTransparency = 1
@@ -1865,7 +1883,6 @@ local function createInstanceNode(instance, parent, depth, order)
1865
1883
  connector.BorderSizePixel = 0
1866
1884
  connector.Position = UDim2.new(0, -8, 0, 11)
1867
1885
  connector.Size = UDim2.fromOffset(35, 1)
1868
- connector.Parent = section
1869
1886
 
1870
1887
  local header = Instance.new("Frame")
1871
1888
  header.BackgroundColor3 = Color3.fromRGB(40, 40, 46)
@@ -1873,6 +1890,7 @@ local function createInstanceNode(instance, parent, depth, order)
1873
1890
  header.LayoutOrder = 1
1874
1891
  header.Size = UDim2.new(1, 0, 0, 22)
1875
1892
  header.Parent = section
1893
+ connector.Parent = header
1876
1894
  header.MouseEnter:Connect(function()
1877
1895
  header.BackgroundTransparency = 0
1878
1896
  end)
@@ -2033,7 +2051,6 @@ local function createRootRow(manifest, order)
2033
2051
  connector.BorderSizePixel = 0
2034
2052
  connector.Position = UDim2.new(0, -8, 0, 11)
2035
2053
  connector.Size = UDim2.fromOffset(35, 1)
2036
- connector.Parent = row
2037
2054
 
2038
2055
  local corner = Instance.new("UICorner")
2039
2056
  corner.CornerRadius = UDim.new(0, 6)
@@ -2041,8 +2058,6 @@ local function createRootRow(manifest, order)
2041
2058
 
2042
2059
  local rowPadding = Instance.new("UIPadding")
2043
2060
  rowPadding.PaddingBottom = UDim.new(0, 5)
2044
- rowPadding.PaddingLeft = UDim.new(0, 6)
2045
- rowPadding.PaddingRight = UDim.new(0, 6)
2046
2061
  rowPadding.PaddingTop = UDim.new(0, 5)
2047
2062
  rowPadding.Parent = row
2048
2063
 
@@ -2057,6 +2072,7 @@ local function createRootRow(manifest, order)
2057
2072
  titleLine.LayoutOrder = 1
2058
2073
  titleLine.Size = UDim2.new(1, 0, 0, 22)
2059
2074
  titleLine.Parent = row
2075
+ connector.Parent = titleLine
2060
2076
 
2061
2077
  local contentDisclosure = Instance.new("TextButton")
2062
2078
  contentDisclosure.BackgroundTransparency = 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goldsync",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
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();