azul-sync 1.3.0

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.
Files changed (133) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. package/README.md +142 -0
  5. package/dist/build.d.ts +19 -0
  6. package/dist/build.d.ts.map +1 -0
  7. package/dist/build.js +92 -0
  8. package/dist/build.js.map +1 -0
  9. package/dist/cli.d.ts +3 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +397 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/config.d.ts +26 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +105 -0
  16. package/dist/config.js.map +1 -0
  17. package/dist/fs/fileWriter.d.ts +100 -0
  18. package/dist/fs/fileWriter.d.ts.map +1 -0
  19. package/dist/fs/fileWriter.js +342 -0
  20. package/dist/fs/fileWriter.js.map +1 -0
  21. package/dist/fs/treeManager.d.ts +84 -0
  22. package/dist/fs/treeManager.d.ts.map +1 -0
  23. package/dist/fs/treeManager.js +365 -0
  24. package/dist/fs/treeManager.js.map +1 -0
  25. package/dist/fs/watcher.d.ts +39 -0
  26. package/dist/fs/watcher.d.ts.map +1 -0
  27. package/dist/fs/watcher.js +120 -0
  28. package/dist/fs/watcher.js.map +1 -0
  29. package/dist/index.d.ts +61 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +349 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/ipc/httpPolling.d.ts +56 -0
  34. package/dist/ipc/httpPolling.d.ts.map +1 -0
  35. package/dist/ipc/httpPolling.js +171 -0
  36. package/dist/ipc/httpPolling.js.map +1 -0
  37. package/dist/ipc/messages.d.ts +112 -0
  38. package/dist/ipc/messages.d.ts.map +1 -0
  39. package/dist/ipc/messages.js +5 -0
  40. package/dist/ipc/messages.js.map +1 -0
  41. package/dist/ipc/server.d.ts +50 -0
  42. package/dist/ipc/server.d.ts.map +1 -0
  43. package/dist/ipc/server.js +168 -0
  44. package/dist/ipc/server.js.map +1 -0
  45. package/dist/pack.d.ts +19 -0
  46. package/dist/pack.d.ts.map +1 -0
  47. package/dist/pack.js +225 -0
  48. package/dist/pack.js.map +1 -0
  49. package/dist/push.d.ts +43 -0
  50. package/dist/push.d.ts.map +1 -0
  51. package/dist/push.js +532 -0
  52. package/dist/push.js.map +1 -0
  53. package/dist/rojo.d.ts +9 -0
  54. package/dist/rojo.d.ts.map +1 -0
  55. package/dist/rojo.js +114 -0
  56. package/dist/rojo.js.map +1 -0
  57. package/dist/snapshot/rojo.d.ts +39 -0
  58. package/dist/snapshot/rojo.d.ts.map +1 -0
  59. package/dist/snapshot/rojo.js +364 -0
  60. package/dist/snapshot/rojo.js.map +1 -0
  61. package/dist/snapshot.d.ts +23 -0
  62. package/dist/snapshot.d.ts.map +1 -0
  63. package/dist/snapshot.js +132 -0
  64. package/dist/snapshot.js.map +1 -0
  65. package/dist/sourcemap/generator.d.ts +78 -0
  66. package/dist/sourcemap/generator.d.ts.map +1 -0
  67. package/dist/sourcemap/generator.js +351 -0
  68. package/dist/sourcemap/generator.js.map +1 -0
  69. package/dist/sourcemap/propertyLoader.d.ts +19 -0
  70. package/dist/sourcemap/propertyLoader.d.ts.map +1 -0
  71. package/dist/sourcemap/propertyLoader.js +131 -0
  72. package/dist/sourcemap/propertyLoader.js.map +1 -0
  73. package/dist/util/id.d.ts +9 -0
  74. package/dist/util/id.d.ts.map +1 -0
  75. package/dist/util/id.js +14 -0
  76. package/dist/util/id.js.map +1 -0
  77. package/dist/util/log.d.ts +13 -0
  78. package/dist/util/log.d.ts.map +1 -0
  79. package/dist/util/log.js +51 -0
  80. package/dist/util/log.js.map +1 -0
  81. package/docs/assets/azul-logo.pdn +0 -0
  82. package/docs/assets/logo-200px.png +0 -0
  83. package/docs/assets/logo.png +0 -0
  84. package/docs/assets/plugin/toolbox.png +0 -0
  85. package/docs/assets/synced.png +0 -0
  86. package/package.json +41 -0
  87. package/plugin/README.md +54 -0
  88. package/plugin/sourcemap.json +264 -0
  89. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Actor/AzulSync.server.luau +905 -0
  90. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/AzulService.luau +1010 -0
  91. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Config.luau +29 -0
  92. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/Enums.luau +11 -0
  93. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CollapsibleTitledSection.luau +214 -0
  94. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ColorPicker.luau +360 -0
  95. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/CustomTextButton.luau +170 -0
  96. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/DropdownMenu.luau +363 -0
  97. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/HorizontalLine.luau +43 -0
  98. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/ImageButtonWithText.luau +181 -0
  99. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledCheckbox.luau +295 -0
  100. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledColorInputPicker.luau +294 -0
  101. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledMultiChoice.luau +163 -0
  102. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledNumberInput.luau +312 -0
  103. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledRadioButton.luau +55 -0
  104. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledSlider.luau +151 -0
  105. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledTextInput.luau +222 -0
  106. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/LabeledToggleButton.luau +73 -0
  107. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/StatefulImageButton.luau +125 -0
  108. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalScrollingFrame.luau +100 -0
  109. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticalSpacer.luau +35 -0
  110. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/Components/VerticallyScalingListFrame.luau +107 -0
  111. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/GuiUtilities.luau +429 -0
  112. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/StudioWidgets/RbxGui.luau +4363 -0
  113. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/UI.luau +425 -0
  114. package/plugin/sync/ReplicatedFirst/AzulCompanionPlugin/WebSocketClient.luau +161 -0
  115. package/src/build.ts +120 -0
  116. package/src/cli.ts +496 -0
  117. package/src/config.ts +170 -0
  118. package/src/fs/fileWriter.ts +414 -0
  119. package/src/fs/treeManager.ts +458 -0
  120. package/src/fs/watcher.ts +142 -0
  121. package/src/index.ts +450 -0
  122. package/src/ipc/httpPolling.ts +214 -0
  123. package/src/ipc/messages.ts +159 -0
  124. package/src/ipc/server.ts +196 -0
  125. package/src/pack.ts +309 -0
  126. package/src/push.ts +726 -0
  127. package/src/snapshot/rojo.ts +467 -0
  128. package/src/snapshot.ts +161 -0
  129. package/src/sourcemap/generator.ts +504 -0
  130. package/src/sourcemap/propertyLoader.ts +195 -0
  131. package/src/util/id.ts +15 -0
  132. package/src/util/log.ts +94 -0
  133. package/tsconfig.json +24 -0
@@ -0,0 +1,425 @@
1
+ --!strict
2
+ --[[
3
+ Azul UI Module
4
+ Manages the companion plugin's dock widget and settings interface
5
+ ]]
6
+
7
+ local UI = {}
8
+
9
+ local Config = require("./Config")
10
+ local Enums = require("./Enums")
11
+
12
+ UI.__index = UI
13
+
14
+ export type CallbackFunctions = {
15
+ onStartSync: () -> (),
16
+ onStopSync: () -> (),
17
+ onDebugModeChanged: (boolean) -> ()?,
18
+ onSilentModeChanged: (boolean) -> ()?,
19
+ onConfigChanged: (string, any) -> (),
20
+ onSettingsScopeChanged: (number) -> (),
21
+ onClearGuids: () -> (),
22
+ onSourcemapReload: () -> (),
23
+ }
24
+
25
+ type SelfType = {
26
+ plugin: Plugin,
27
+ config: { any },
28
+ callbacks: CallbackFunctions,
29
+ LOGO: string,
30
+ LOGO_SYNCED: string,
31
+ settingsScope: string,
32
+ debugPrint: (...string) -> (),
33
+ infoPrint: (...string) -> (),
34
+ saveSettings: () -> (),
35
+ loadSettings: () -> (),
36
+ rebuildServiceSet: () -> (),
37
+
38
+ isSyncEnabled: boolean,
39
+
40
+ azulWidget: DockWidgetPluginGui,
41
+ connectButton: PluginToolbarButton,
42
+ titleImageLabel: ImageLabel,
43
+ syncButton: any,
44
+ debugModeCheckbox: any,
45
+ silentModeCheckbox: any,
46
+ settingsScopeDropdown: any,
47
+ websocketUrlLabel: any,
48
+ heartbeatIntervalLabel: any,
49
+ listTypeLabel: any,
50
+ serviceListLabel: any,
51
+ excludedParentsLabel: any,
52
+ clearGuidsButton: any,
53
+ reloadSourcemapButton: any,
54
+
55
+ GetSyncState: (SelfType) -> boolean,
56
+ UpdateSyncState: (SelfType, boolean) -> (),
57
+ UpdateConfig: (SelfType) -> (),
58
+ SetSettingsScope: (SelfType, any) -> (),
59
+ }
60
+
61
+ UI.logo = "rbxassetid://134336592598474"
62
+ UI.syncedLogo = "rbxassetid://103599828888609"
63
+
64
+ local function createInfoLabel(text: string)
65
+ local label = Instance.new("TextLabel")
66
+ label.Name = "InfoLabel"
67
+ label.RichText = true
68
+ label.AutomaticSize = Enum.AutomaticSize.Y
69
+ label.TextWrapped = true
70
+ label.Size = UDim2.new(1, 0, 0, 15)
71
+ label.BackgroundTransparency = 1
72
+ label.TextColor3 = Color3.new(0.5, 0.5, 0.5)
73
+ label.Font = Enum.Font.SourceSans
74
+ label.TextSize = 15
75
+ label.Text = `{text}<br/>`
76
+ label.TextXAlignment = Enum.TextXAlignment.Left
77
+
78
+ local padding = Instance.new("UIPadding")
79
+ padding.PaddingLeft = UDim.new(0, 30)
80
+ padding.PaddingRight = UDim.new(0, 30)
81
+ padding.Parent = label
82
+
83
+ return label
84
+ end
85
+
86
+ --[=[
87
+ Initialize the UI widget
88
+
89
+ Parameters:
90
+ - plugin: The plugin object from the Roblox plugin API
91
+ - config: Reference to the CONFIG table
92
+ - settingsScope: Reference to the settingsScope variable`
93
+ - callbacks: Table with callbacks:
94
+ - onStartSync: Called when user clicks Connect
95
+ - onStopSync: Called when user clicks Disconnect
96
+ - onDebugModeChanged: Called when debug mode checkbox changes
97
+ - onSilentModeChanged: Called when silent mode checkbox changes
98
+ - onConfigChanged: Called when any config value changes (receives {key, value})
99
+ - onSettingsScopeChanged: Called when scope changes
100
+ - onClearGuids: Called when clear GUIDs button is clicked
101
+ - helpers: Table with helper functions:
102
+ - debugPrint: Debug print function
103
+ - infoPrint: Info print function
104
+ - saveSettings: Save settings function
105
+ - loadSettings: Load settings function
106
+ - rebuildServiceSet: Rebuild service set function
107
+
108
+ Returns: Table with methods:
109
+ - updateSyncState(enabled: boolean): Update UI to reflect sync state
110
+ - updateConfig(): Refresh UI with current CONFIG values
111
+ ]=]
112
+ function UI.new(plugin, callbacks, settingsScope, helpers): SelfType
113
+ local self = {} :: SelfType
114
+ setmetatable(self, UI)
115
+
116
+ -- Store references
117
+ self.plugin = plugin
118
+ self.callbacks = callbacks
119
+ self.LOGO = UI.logo
120
+ self.LOGO_SYNCED = UI.syncedLogo
121
+ self.settingsScope = settingsScope
122
+ self.debugPrint = helpers.debugPrint
123
+ self.infoPrint = helpers.infoPrint
124
+ self.saveSettings = helpers.saveSettings
125
+ self.loadSettings = helpers.loadSettings
126
+ self.rebuildServiceSet = helpers.rebuildServiceSet
127
+
128
+ -- Track sync enabled state locally (PluginToolbarButton has SetActive but no GetActive)
129
+ self.isSyncEnabled = false
130
+
131
+ -- Import UI components
132
+ local VerticalScrollingFrame = require("./StudioWidgets/Components/VerticalScrollingFrame")
133
+ local CollapsibleTitledSection = require("./StudioWidgets/Components/CollapsibleTitledSection")
134
+ local CustomTextButton = require("./StudioWidgets/Components/CustomTextButton")
135
+ local LabeledCheckbox = require("./StudioWidgets/Components/LabeledCheckbox")
136
+ local LabeledMultiChoice = require("./StudioWidgets/Components/LabeledMultiChoice")
137
+ local LabeledTextInput = require("./StudioWidgets/Components/LabeledTextInput")
138
+ local DropdownMenu = require("./StudioWidgets/Components/DropdownMenu")
139
+
140
+ -- Create toolbar button
141
+ local toolbar = plugin:CreateToolbar("Azul")
142
+ self.connectButton = toolbar:CreateButton("Azul", "Connect/disconnect from sync daemon", self.LOGO)
143
+
144
+ -- Create widget
145
+ local widgetInfo = DockWidgetPluginGuiInfo.new(Enum.InitialDockState.Float, true, true, 345, 640, 300, 300)
146
+
147
+ local azulWidget = plugin:CreateDockWidgetPluginGuiAsync("azulWidget", widgetInfo)
148
+ azulWidget.Name = "AzulCompanionPlugin"
149
+ azulWidget.Title = "Azul"
150
+ self.azulWidget = azulWidget
151
+
152
+ -- Create main scroll frame
153
+ local mainScrollFrame = VerticalScrollingFrame.new("main")
154
+ mainScrollFrame:GetContentsFrame().Parent = azulWidget
155
+
156
+ local mainSectionListLayout = Instance.new("UIListLayout")
157
+ mainSectionListLayout.Parent = mainScrollFrame:GetSectionFrame()
158
+ mainSectionListLayout.Padding = UDim.new(0, 0)
159
+ mainSectionListLayout.SortOrder = Enum.SortOrder.LayoutOrder
160
+ mainSectionListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
161
+
162
+ -- Main section
163
+ local mainSection = CollapsibleTitledSection.new("mainSection", "Azul Companion Plugin", true, false, false, false)
164
+ mainSection:GetSectionFrame().LayoutOrder = 0
165
+ mainSection:GetSectionFrame().Parent = mainScrollFrame:GetContentsFrame()
166
+
167
+ -- Title image
168
+ local imageContainer = Instance.new("Frame")
169
+ imageContainer.Size = UDim2.new(1, 0, 0, 70)
170
+ imageContainer.BackgroundTransparency = 1
171
+
172
+ local imageContainerListLayout = Instance.new("UIListLayout")
173
+ imageContainerListLayout.FillDirection = Enum.FillDirection.Horizontal
174
+ imageContainerListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
175
+ imageContainerListLayout.VerticalAlignment = Enum.VerticalAlignment.Center
176
+ imageContainerListLayout.Parent = imageContainer
177
+
178
+ local titleImageLabel = Instance.new("ImageLabel")
179
+ titleImageLabel.Size = UDim2.fromScale(0.2, 1)
180
+ titleImageLabel.AutomaticSize = Enum.AutomaticSize.X
181
+ titleImageLabel.BackgroundTransparency = 1
182
+ titleImageLabel.Image = self.LOGO
183
+ titleImageLabel.ScaleType = Enum.ScaleType.Fit
184
+ titleImageLabel.LayoutOrder = 0
185
+ titleImageLabel.Parent = imageContainer
186
+
187
+ local azulText = Instance.new("TextLabel")
188
+ azulText.Text = "<b>Azul</b>"
189
+ azulText.RichText = true
190
+ azulText.Font = Enum.Font.RobotoMono
191
+ azulText.TextSize = 36
192
+ azulText.TextScaled = true
193
+ azulText.Size = UDim2.fromScale(0.1, 1)
194
+ azulText.AutomaticSize = Enum.AutomaticSize.X
195
+ azulText.TextColor3 = Color3.fromRGB(255, 255, 255)
196
+ azulText.BackgroundTransparency = 1
197
+ azulText.LayoutOrder = 1
198
+ azulText.Parent = imageContainer
199
+
200
+ local padding = Instance.new("UIPadding")
201
+ padding.PaddingTop = UDim.new(0, 10)
202
+ padding.PaddingBottom = UDim.new(0, 10)
203
+ padding.Parent = imageContainer
204
+
205
+ mainSection:AddChild(imageContainer, 0)
206
+ self.titleImageLabel = titleImageLabel
207
+
208
+ -- Sync button
209
+ local syncButton = CustomTextButton.new("toggleSync", "Connect", false)
210
+ syncButton.Parent = mainScrollFrame:GetContentsFrame()
211
+ syncButton:SetSize(UDim2.fromScale(1, 0.2))
212
+
213
+ local syncButtonPadding = Instance.new("UIPadding")
214
+ syncButtonPadding.PaddingLeft = UDim.new(0.1, 0)
215
+ syncButtonPadding.PaddingRight = UDim.new(0.1, 0)
216
+ syncButtonPadding.Parent = syncButton:GetFrame()
217
+
218
+ syncButton:SetClickedFunction(function()
219
+ if self.isSyncEnabled then
220
+ syncButton:GetButton().Text = "Stopping..."
221
+ self.callbacks.onStopSync()
222
+ else
223
+ syncButton:GetButton().Text = "Connecting..."
224
+ self.callbacks.onStartSync()
225
+ end
226
+ end)
227
+ mainSection:AddChild(syncButton:GetFrame())
228
+ self.syncButton = syncButton
229
+
230
+ -- Silent Mode checkbox
231
+ local silentModeCheckbox = LabeledCheckbox.new("silentMode", "Silent Mode", Config.SILENT_MODE, false)
232
+ silentModeCheckbox:SetValue(Config.SILENT_MODE)
233
+ silentModeCheckbox:SetValueChangedFunction(function(newValue)
234
+ Config.SILENT_MODE = newValue
235
+ helpers.infoPrint(`[🐛 AzulUI]: Silent mode set to: {newValue}`)
236
+ end)
237
+ mainSection:AddChild(silentModeCheckbox:GetFrame())
238
+ mainSection:AddChild(createInfoLabel("Silent mode suppresses all console output except errors."))
239
+ self.silentModeCheckbox = silentModeCheckbox
240
+
241
+ -- Debug Mode checkbox
242
+ local debugModeCheckbox = LabeledCheckbox.new("debugMode", "Debug Mode", Config.DEBUG_MODE, false)
243
+ debugModeCheckbox:SetValue(Config.DEBUG_MODE)
244
+ debugModeCheckbox:SetValueChangedFunction(function(newValue)
245
+ Config.DEBUG_MODE = newValue
246
+ helpers.debugPrint(`[🐛 AzulUI]: Debug mode set to: {newValue}`)
247
+ end)
248
+ mainSection:AddChild(debugModeCheckbox:GetFrame())
249
+ mainSection:AddChild(createInfoLabel("Debug mode enables verbose logging to the output console."))
250
+ self.debugModeCheckbox = debugModeCheckbox
251
+
252
+ -- Settings section
253
+ local settingsSection = CollapsibleTitledSection.new("settings", "Plugin Settings", true, true, true)
254
+ settingsSection:GetSectionFrame().LayoutOrder = 1
255
+ settingsSection:GetSectionFrame().Parent = mainScrollFrame:GetContentsFrame()
256
+
257
+ -- Settings scope dropdown
258
+ local scopeChoices = {
259
+ { Id = 1, Text = "Global" },
260
+ { Id = 2, Text = "Project" },
261
+ }
262
+ local settingsScopeDropdown = DropdownMenu.new("settingsScope", "Scope", scopeChoices, "Select ...")
263
+ settingsSection:AddChild(settingsScopeDropdown:GetSectionFrame())
264
+ self.settingsScopeDropdown = settingsScopeDropdown
265
+
266
+ -- WebSocket URL input
267
+ local websocketUrlLabel = LabeledTextInput.new("websocketUrl", "WebSocket URL:", Config.WS_URL)
268
+ websocketUrlLabel:SetValue(Config.WS_URL)
269
+ websocketUrlLabel:SetValueChangedFunction(function(newValue)
270
+ Config.WS_URL = newValue
271
+ self.callbacks.onConfigChanged("WS_URL", newValue)
272
+ end)
273
+ settingsSection:AddChild(websocketUrlLabel:GetFrame())
274
+ self.websocketUrlLabel = websocketUrlLabel
275
+
276
+ -- Heartbeat Interval input
277
+ local heartbeatIntervalLabel =
278
+ LabeledTextInput.new("heartbeatInterval", "Heartbeat Interval (s):", tostring(Config.HEARTBEAT_INTERVAL))
279
+ heartbeatIntervalLabel:SetValue(tostring(Config.HEARTBEAT_INTERVAL))
280
+ heartbeatIntervalLabel:SetValueChangedFunction(function(newValue)
281
+ local interval = tonumber(newValue)
282
+ if interval and interval > 0 then
283
+ Config.HEARTBEAT_INTERVAL = interval
284
+ self.callbacks.onConfigChanged("HEARTBEAT_INTERVAL", interval)
285
+ end
286
+ end)
287
+ settingsSection:AddChild(heartbeatIntervalLabel:GetFrame())
288
+ settingsSection:AddChild(createInfoLabel("Interval in seconds between heartbeat pings to the daemon."))
289
+ self.heartbeatIntervalLabel = heartbeatIntervalLabel
290
+
291
+ -- List type dropdown
292
+ local listTypeLabel = LabeledMultiChoice.new("listType", "Service List Type:", {
293
+ { Id = "WHITELIST", Text = "Whitelist" },
294
+ { Id = "BLACKLIST", Text = "Blacklist" },
295
+ }, if Config.LIST_TYPE == Enums.listType.WHITELIST then 1 else 2)
296
+ listTypeLabel:SetValueChangedFunction(function(newIndex)
297
+ if newIndex == 1 then
298
+ Config.LIST_TYPE = Enums.listType.WHITELIST
299
+ else
300
+ Config.LIST_TYPE = Enums.listType.BLACKLIST
301
+ end
302
+ self.callbacks.onConfigChanged("LIST_TYPE", Config.LIST_TYPE)
303
+ end)
304
+ settingsSection:AddChild(listTypeLabel:GetFrame())
305
+ settingsSection:AddChild(
306
+ createInfoLabel(
307
+ "Whitelist: only services in the list are synced.<br />Blacklist: services in the list are excluded."
308
+ )
309
+ )
310
+ self.listTypeLabel = listTypeLabel
311
+
312
+ -- Service List input
313
+ local serviceListLabel =
314
+ LabeledTextInput.new("serviceList", "Service List:", table.concat(Config.SERVICE_LIST, ", "))
315
+ serviceListLabel:SetMaxGraphemes(9999)
316
+ serviceListLabel:SetValue(table.concat(Config.SERVICE_LIST, ", "))
317
+ serviceListLabel:SetValueChangedFunction(function(newValue)
318
+ local services = {}
319
+ for serviceName in string.gmatch(newValue, "([^,%s]+)") do
320
+ table.insert(services, serviceName)
321
+ end
322
+ Config.SERVICE_LIST = services
323
+ self.rebuildServiceSet()
324
+ self.callbacks.onConfigChanged("SERVICE_LIST", services)
325
+ end)
326
+ settingsSection:AddChild(serviceListLabel:GetFrame())
327
+ settingsSection:AddChild(createInfoLabel("List of services to include/exclude based on the selected List Type."))
328
+ self.serviceListLabel = serviceListLabel
329
+
330
+ -- Excluded Parents input
331
+ local excludedParentsLabel =
332
+ LabeledTextInput.new("excludedParents", "Excluded Parents:", table.concat(Config.EXCLUDED_PARENTS, ", "))
333
+ excludedParentsLabel:SetValue(table.concat(Config.EXCLUDED_PARENTS, ", "))
334
+ excludedParentsLabel:SetValueChangedFunction(function(newValue)
335
+ local excluded = {}
336
+ for parentName in string.gmatch(newValue, "([^,%s]+)") do
337
+ table.insert(excluded, parentName)
338
+ end
339
+ Config.EXCLUDED_PARENTS = excluded
340
+ self.callbacks.onConfigChanged("EXCLUDED_PARENTS", excluded)
341
+ end)
342
+ settingsSection:AddChild(excludedParentsLabel:GetFrame())
343
+ settingsSection:AddChild(createInfoLabel("List of parent paths to exclude from syncing, separated by commas."))
344
+ self.excludedParentsLabel = excludedParentsLabel
345
+
346
+ settingsScopeDropdown:SetValue(if self.settingsScope == Enums.scope.GLOBAL then 1 else 2)
347
+
348
+ -- Settings scope callback
349
+ settingsScopeDropdown:SetValueChangedFunction(function(newValue: number | string, newText: string)
350
+ helpers.debugPrint("[🐛 AzulUI]: Settings scope changed to:", newText)
351
+
352
+ local newScope
353
+ if newValue == 1 then
354
+ newScope = Enums.scope.GLOBAL
355
+ elseif newValue == 2 then
356
+ newScope = Enums.scope.PROJECT
357
+ else
358
+ return
359
+ end
360
+
361
+ self.callbacks.onSettingsScopeChanged(newScope)
362
+ end)
363
+
364
+ -- Reload sourcemap button
365
+ local reloadSourcemapButton = CustomTextButton.new("reloadSourcemap", "Reload Sourcemap", false)
366
+ reloadSourcemapButton:SetClickedFunction(function()
367
+ self.callbacks.onSourcemapReload()
368
+ end)
369
+ reloadSourcemapButton:SetSize(UDim2.fromScale(1, 0.07))
370
+
371
+ local clearButtonPadding = Instance.new("UIPadding")
372
+ clearButtonPadding.PaddingLeft = UDim.new(0.1, 0)
373
+ clearButtonPadding.PaddingRight = UDim.new(0.1, 0)
374
+ clearButtonPadding.Parent = reloadSourcemapButton:GetFrame()
375
+
376
+ settingsSection:AddChild(reloadSourcemapButton:GetFrame())
377
+ settingsSection:AddChild(
378
+ createInfoLabel("Something's not right? Force reload the sourcemap to reset the sync state.")
379
+ )
380
+ self.reloadSourcemapButton = reloadSourcemapButton
381
+
382
+ -- Connect button click handler
383
+ self.connectButton.Click:Connect(function()
384
+ azulWidget.Enabled = not azulWidget.Enabled
385
+ end)
386
+
387
+ return (self :: any) :: SelfType
388
+ end
389
+
390
+ function UI:GetSyncState()
391
+ return self.isSyncEnabled
392
+ end
393
+
394
+ function UI:UpdateSyncState(enabled: boolean)
395
+ self.isSyncEnabled = enabled
396
+ self.connectButton:SetActive(enabled)
397
+ if enabled then
398
+ self.connectButton.Icon = self.LOGO_SYNCED
399
+ self.syncButton:GetButton().Text = "Disconnect"
400
+ self.titleImageLabel.Image = self.LOGO_SYNCED
401
+ else
402
+ self.connectButton.Icon = self.LOGO
403
+ self.syncButton:GetButton().Text = "Connect"
404
+ self.titleImageLabel.Image = self.LOGO
405
+ end
406
+ end
407
+
408
+ function UI:SetSettingsScope(scope)
409
+ self.settingsScope = scope
410
+ end
411
+
412
+ function UI:UpdateConfig()
413
+ self = self :: SelfType
414
+
415
+ self.debugModeCheckbox:SetValue(Config.DEBUG_MODE)
416
+ self.silentModeCheckbox:SetValue(Config.SILENT_MODE)
417
+ self.settingsScopeDropdown:SetValue(if self.settingsScope == Enums.scope.GLOBAL then 1 else 2)
418
+ self.websocketUrlLabel:SetValue(Config.WS_URL)
419
+ self.heartbeatIntervalLabel:SetValue(tostring(Config.HEARTBEAT_INTERVAL))
420
+ self.listTypeLabel:SetSelectedIndex(if Config.LIST_TYPE == Enums.listType.WHITELIST then 1 else 2)
421
+ self.serviceListLabel:SetValue(table.concat(Config.SERVICE_LIST, ", "))
422
+ self.excludedParentsLabel:SetValue(table.concat(Config.EXCLUDED_PARENTS, ", "))
423
+ end
424
+
425
+ return UI
@@ -0,0 +1,161 @@
1
+ --!strict
2
+ --[[
3
+ WebSocket Client for Roblox Studio
4
+
5
+ Uses Roblox Studio's native WebSocket support (WebStreamClient) for real-time
6
+ bidirectional communication with the sync daemon.
7
+
8
+ Ransomwave 2025
9
+ ]]
10
+
11
+ local HttpService = game:GetService("HttpService")
12
+
13
+ local WebSocketClient = {}
14
+ WebSocketClient.__index = WebSocketClient
15
+
16
+ type configType = {
17
+ debugMode: boolean?,
18
+ silentMode: boolean?,
19
+ }
20
+
21
+ -- self type
22
+ type WebSocketClient = {
23
+ url: string,
24
+ client: WebStreamClient,
25
+ connected: boolean,
26
+ messageHandlers: { [string]: (any) -> () },
27
+ config: configType,
28
+
29
+ new: (url: string?) -> WebSocketClient,
30
+ on: (self: WebSocketClient, event: string, handler: (any) -> ()) -> (),
31
+ connect: (self: WebSocketClient) -> boolean,
32
+ handleMessage: (self: WebSocketClient, message: string) -> (),
33
+ send: (self: WebSocketClient, message: string) -> boolean,
34
+ disconnect: (self: WebSocketClient) -> (),
35
+ debugPrint: (self: WebSocketClient, ...any) -> (),
36
+ infoPrint: (self: WebSocketClient, ...any) -> (),
37
+ }
38
+
39
+ function WebSocketClient.new(url, config: configType?)
40
+ local self = setmetatable({}, WebSocketClient)
41
+ self.url = url or "ws://localhost:8080"
42
+ self.client = nil
43
+ self.connected = false
44
+ self.messageHandlers = {}
45
+ self.onClosed = Instance.new("BindableEvent")
46
+ self.config = config or {
47
+ debugMode = false,
48
+ silentMode = true,
49
+ }
50
+
51
+ if self.config.debugMode then print("[🐛 WebSocket]: Debug mode is enabled!") end
52
+
53
+ return (self :: any) :: WebSocketClient
54
+ end
55
+
56
+ function WebSocketClient:debugPrint(...)
57
+ self = self :: WebSocketClient
58
+ if self.config.silentMode or not self.config.debugMode then return end
59
+ print(...)
60
+ end
61
+
62
+ function WebSocketClient:infoPrint(...)
63
+ self = self :: WebSocketClient
64
+ if self.config.silentMode then return end
65
+ print(...)
66
+ end
67
+
68
+ function WebSocketClient:on(event, handler)
69
+ self = self :: WebSocketClient
70
+ self.messageHandlers[event] = handler
71
+ end
72
+
73
+ function WebSocketClient:connect()
74
+ self = self :: WebSocketClient
75
+ if self.connected then return true end
76
+
77
+ -- Create WebSocket client using CreateWebStreamClient
78
+ local success, result = pcall(function()
79
+ return HttpService:CreateWebStreamClient(Enum.WebStreamClientType.WebSocket, {
80
+ Url = self.url,
81
+ })
82
+ end)
83
+
84
+ if not success then
85
+ warn("[WebSocket]: Connection failed:", result)
86
+ if self.messageHandlers.error then self.messageHandlers.error(result) end
87
+ return false
88
+ end
89
+
90
+ self.client = result
91
+ self.connected = true
92
+
93
+ -- Set up message handler (only MessageReceived is documented)
94
+ self.client.MessageReceived:Connect(function(message)
95
+ local parseSuccess, parseError = pcall(function()
96
+ self:handleMessage(message)
97
+ end)
98
+ if not parseSuccess then warn("[WebSocket]: Error handling message:", parseError) end
99
+ end)
100
+
101
+ -- Notify connection established
102
+ self:debugPrint("[🐛 WebSocket]: Connected to", self.url)
103
+ if self.messageHandlers.connect then task.defer(function()
104
+ self.messageHandlers.connect()
105
+ end) end
106
+
107
+ return true
108
+ end
109
+
110
+ function WebSocketClient:handleMessage(message)
111
+ self = self :: WebSocketClient
112
+ if not message or message == "" then return end
113
+
114
+ self:debugPrint("[🐛 WebSocket]: Received message:", string.sub(message, 1, 100))
115
+
116
+ local success, data = pcall(function()
117
+ return HttpService:JSONDecode(message)
118
+ end)
119
+
120
+ if success and self.messageHandlers.message then
121
+ self.messageHandlers.message(data)
122
+ elseif not success then
123
+ warn("[WebSocket]: Failed to parse message:", message)
124
+ end
125
+ end
126
+
127
+ function WebSocketClient:send(message)
128
+ self = self :: WebSocketClient
129
+ if not self.connected or not self.client then
130
+ warn("[WebSocket]: Cannot send: not connected")
131
+ return false
132
+ end
133
+
134
+ self:debugPrint("[🐛 WebSocket]: Sending:", string.sub(message, 1, 200))
135
+
136
+ local success, err = pcall(function()
137
+ self.client:Send(message)
138
+ end)
139
+
140
+ if not success then
141
+ warn("[WebSocket]: Send failed:", err)
142
+ return false
143
+ end
144
+
145
+ return true
146
+ end
147
+
148
+ function WebSocketClient:disconnect()
149
+ self = self :: WebSocketClient
150
+
151
+ if not self.connected or not self.client then return end
152
+
153
+ self.connected = false
154
+
155
+ self.client:Close()
156
+ -- self.client = nil
157
+
158
+ if self.messageHandlers.disconnect then self.messageHandlers.disconnect() end
159
+ end
160
+
161
+ return WebSocketClient
package/src/build.ts ADDED
@@ -0,0 +1,120 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { IPCServer } from "./ipc/server.js";
4
+ import { config } from "./config.js";
5
+ import { log } from "./util/log.js";
6
+ import { SnapshotBuilder } from "./snapshot.js";
7
+ import { RojoSnapshotBuilder } from "./snapshot/rojo.js";
8
+ import type { InstanceData } from "./ipc/messages.js";
9
+ import {
10
+ applySourcemapProperties,
11
+ buildInstancesFromSourcemap,
12
+ loadSourcemapPropertyIndex,
13
+ } from "./sourcemap/propertyLoader.js";
14
+
15
+ interface BuildOptions {
16
+ syncDir?: string;
17
+ rojoMode?: boolean;
18
+ rojoProjectFile?: string;
19
+ applySourcemap?: boolean;
20
+ fromSourcemap?: boolean;
21
+ }
22
+
23
+ export class BuildCommand {
24
+ private ipc: IPCServer;
25
+ private syncDir: string;
26
+ private rojoMode: boolean;
27
+ private rojoProjectFile?: string;
28
+ private applySourcemap: boolean;
29
+ private fromSourcemap: boolean;
30
+
31
+ constructor(options: BuildOptions = {}) {
32
+ this.syncDir = path.resolve(options.syncDir ?? config.syncDir);
33
+ this.rojoMode = Boolean(options.rojoMode);
34
+ this.rojoProjectFile = options.rojoProjectFile;
35
+ this.applySourcemap = options.applySourcemap !== false;
36
+ this.fromSourcemap = options.fromSourcemap === true;
37
+ this.ipc = new IPCServer(config.port, undefined, {
38
+ requestSnapshotOnConnect: false,
39
+ });
40
+ }
41
+
42
+ public async run(): Promise<void> {
43
+ const builder = this.rojoMode
44
+ ? new RojoSnapshotBuilder({
45
+ projectFile: this.rojoProjectFile,
46
+ cwd: process.cwd(),
47
+ destPrefix: [],
48
+ })
49
+ : new SnapshotBuilder({
50
+ sourceDir: this.syncDir,
51
+ destPrefix: [],
52
+ skipSymlinks: true,
53
+ });
54
+
55
+ if (this.rojoMode) {
56
+ log.info(
57
+ `Preparing Rojo compatibility build from ${
58
+ this.rojoProjectFile ?? "default.project.json"
59
+ }`,
60
+ );
61
+ } else {
62
+ log.info(`Preparing build snapshot from ${this.syncDir}`);
63
+ }
64
+ let instances: InstanceData[] = [];
65
+
66
+ if (!this.rojoMode && this.fromSourcemap) {
67
+ const built = buildInstancesFromSourcemap(config.sourcemapPath);
68
+ if (!built) {
69
+ log.warn(
70
+ "Falling back to filesystem build because sourcemap import failed.",
71
+ );
72
+ } else {
73
+ instances = built;
74
+ }
75
+ }
76
+
77
+ if (instances.length === 0) {
78
+ try {
79
+ instances = await builder.build();
80
+ } catch (error) {
81
+ log.error(`${error}`);
82
+ return;
83
+ }
84
+ }
85
+
86
+ if (!this.rojoMode && this.applySourcemap && !this.fromSourcemap) {
87
+ const index = loadSourcemapPropertyIndex(config.sourcemapPath);
88
+ const applied = applySourcemapProperties(instances, index);
89
+ if (applied > 0) {
90
+ log.success(
91
+ `Applied properties/attributes from sourcemap to ${applied} instance(s)`,
92
+ );
93
+ } else {
94
+ if (!index && fs.existsSync(config.sourcemapPath)) {
95
+ log.warn(
96
+ "Sourcemap present but could not be parsed; continuing without properties.",
97
+ );
98
+ } else {
99
+ log.info(
100
+ "No packed properties found in sourcemap; continuing with script/folder snapshot only.",
101
+ );
102
+ }
103
+ }
104
+ }
105
+
106
+ log.info(`Waiting for Studio to connect on port ${config.port}...`);
107
+
108
+ await new Promise<void>((resolve) => {
109
+ this.ipc.onConnection(() => {
110
+ log.info("Studio connected. Sending build snapshot...");
111
+ this.ipc.send({ type: "buildSnapshot", data: instances });
112
+ log.success(`Sent ${instances.length} instances`);
113
+ setTimeout(() => {
114
+ this.ipc.close();
115
+ resolve();
116
+ }, 200);
117
+ });
118
+ });
119
+ }
120
+ }