dominus-cli 2.0.0 → 2.1.1

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.
@@ -0,0 +1,231 @@
1
+ local InstanceRegistry = require(script.Parent.InstanceRegistry)
2
+
3
+ local ValueCodec = {}
4
+
5
+ local function number(value, label)
6
+ if type(value) ~= "number" then
7
+ error(label .. " must be a number")
8
+ end
9
+ return value
10
+ end
11
+
12
+ local function component(value, names, index, default)
13
+ for _, name in names do
14
+ if value[name] ~= nil then
15
+ return number(value[name], name)
16
+ end
17
+ end
18
+ if value[index] ~= nil then
19
+ return number(value[index], tostring(index))
20
+ end
21
+ return default
22
+ end
23
+
24
+ local function decodeColor3(value)
25
+ if type(value) == "string" then
26
+ if value:sub(1, 1) == "#" then
27
+ return Color3.fromHex(value)
28
+ end
29
+ local r, g, b = value:match("rgb%s*%((%d+)%D+(%d+)%D+(%d+)%)")
30
+ if r then
31
+ return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))
32
+ end
33
+ error("Color3 strings must be #RRGGBB or rgb(r,g,b)")
34
+ end
35
+ if type(value) ~= "table" then
36
+ error("Color3 must be a string or array")
37
+ end
38
+ local r = component(value, { "R", "r", "red" }, 1, 0)
39
+ local g = component(value, { "G", "g", "green" }, 2, 0)
40
+ local b = component(value, { "B", "b", "blue" }, 3, 0)
41
+ if r > 1 or g > 1 or b > 1 then
42
+ return Color3.fromRGB(r, g, b)
43
+ end
44
+ return Color3.new(r, g, b)
45
+ end
46
+
47
+ local function decodeEnum(current, value)
48
+ if typeof(value) == "EnumItem" then
49
+ return value
50
+ end
51
+ if type(value) ~= "string" then
52
+ error("Enum value must be a string")
53
+ end
54
+ local itemName = value:match("Enum%.[^.]+%.(.+)") or value
55
+ for _, item in current.EnumType:GetEnumItems() do
56
+ if item.Name == itemName then
57
+ return item
58
+ end
59
+ end
60
+ error("Invalid enum value " .. value .. " for " .. tostring(current.EnumType))
61
+ end
62
+
63
+ function ValueCodec.decodeForCurrent(current, value)
64
+ local expectedType = typeof(current)
65
+ if expectedType == "UDim2" then
66
+ assert(type(value) == "table", "UDim2 must be an array or object")
67
+ return UDim2.new(
68
+ component(value, { "XScale", "xScale" }, 1, 0),
69
+ component(value, { "XOffset", "xOffset" }, 2, 0),
70
+ component(value, { "YScale", "yScale" }, 3, 0),
71
+ component(value, { "YOffset", "yOffset" }, 4, 0)
72
+ )
73
+ elseif expectedType == "UDim" then
74
+ assert(type(value) == "table", "UDim must be an array or object")
75
+ return UDim.new(component(value, { "Scale", "scale" }, 1, 0), component(value, { "Offset", "offset" }, 2, 0))
76
+ elseif expectedType == "Vector2" then
77
+ assert(type(value) == "table", "Vector2 must be an array or object")
78
+ return Vector2.new(component(value, { "X", "x" }, 1, 0), component(value, { "Y", "y" }, 2, 0))
79
+ elseif expectedType == "Vector3" then
80
+ assert(type(value) == "table", "Vector3 must be an array or object")
81
+ return Vector3.new(
82
+ component(value, { "X", "x" }, 1, 0),
83
+ component(value, { "Y", "y" }, 2, 0),
84
+ component(value, { "Z", "z" }, 3, 0)
85
+ )
86
+ elseif expectedType == "Color3" then
87
+ return decodeColor3(value)
88
+ elseif expectedType == "BrickColor" then
89
+ return BrickColor.new(value)
90
+ elseif expectedType == "EnumItem" then
91
+ return decodeEnum(current, value)
92
+ elseif expectedType == "Rect" then
93
+ assert(type(value) == "table", "Rect must be a four-number array")
94
+ return Rect.new(number(value[1], "1"), number(value[2], "2"), number(value[3], "3"), number(value[4], "4"))
95
+ elseif expectedType == "NumberRange" then
96
+ if type(value) == "number" then
97
+ return NumberRange.new(value)
98
+ end
99
+ assert(type(value) == "table", "NumberRange must be a number or two-number array")
100
+ return NumberRange.new(number(value[1], "1"), number(value[2] or value[1], "2"))
101
+ elseif expectedType == "CFrame" then
102
+ assert(type(value) == "table", "CFrame must be an array")
103
+ if #value == 3 then
104
+ return CFrame.new(value[1], value[2], value[3])
105
+ elseif #value == 12 then
106
+ return CFrame.new(table.unpack(value))
107
+ end
108
+ error("CFrame requires 3 or 12 numbers")
109
+ elseif expectedType == "NumberSequence" then
110
+ if type(value) == "number" then
111
+ return NumberSequence.new(value)
112
+ end
113
+ assert(type(value) == "table", "NumberSequence must be a number or keypoint array")
114
+ local keypoints = {}
115
+ for _, keypoint in value do
116
+ table.insert(keypoints, NumberSequenceKeypoint.new(keypoint[1], keypoint[2], keypoint[3] or 0))
117
+ end
118
+ return NumberSequence.new(keypoints)
119
+ elseif expectedType == "ColorSequence" then
120
+ if type(value) == "string" then
121
+ return ColorSequence.new(decodeColor3(value))
122
+ end
123
+ assert(type(value) == "table", "ColorSequence must be a color or keypoint array")
124
+ local keypoints = {}
125
+ for _, keypoint in value do
126
+ table.insert(keypoints, ColorSequenceKeypoint.new(keypoint[1], decodeColor3(keypoint[2])))
127
+ end
128
+ return ColorSequence.new(keypoints)
129
+ elseif expectedType == "Font" then
130
+ assert(type(value) == "table", "Font must be an object")
131
+ local weight = Enum.FontWeight[value.Weight or value.weight or "Regular"]
132
+ local style = Enum.FontStyle[value.Style or value.style or "Normal"]
133
+ assert(weight and style, "Invalid Font weight or style")
134
+ return Font.new(value.Family or value.family, weight, style)
135
+ elseif expectedType == "Instance" then
136
+ local resolved, err = InstanceRegistry.resolve(value)
137
+ if not resolved then
138
+ error(err)
139
+ end
140
+ return resolved
141
+ elseif expectedType == "number" then
142
+ return number(value, "Property")
143
+ elseif expectedType == "boolean" then
144
+ assert(type(value) == "boolean", "Property must be a boolean")
145
+ return value
146
+ elseif expectedType == "string" then
147
+ assert(type(value) == "string", "Property must be a string")
148
+ return value
149
+ end
150
+ return value
151
+ end
152
+
153
+ function ValueCodec.encode(value)
154
+ local valueType = typeof(value)
155
+ if valueType == "UDim2" then
156
+ return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
157
+ end
158
+ if valueType == "UDim" then
159
+ return { value.Scale, value.Offset }
160
+ end
161
+ if valueType == "Vector2" then
162
+ return { value.X, value.Y }
163
+ end
164
+ if valueType == "Vector3" then
165
+ return { value.X, value.Y, value.Z }
166
+ end
167
+ if valueType == "Color3" then
168
+ return "#" .. value:ToHex()
169
+ end
170
+ if valueType == "BrickColor" then
171
+ return value.Name
172
+ end
173
+ if valueType == "EnumItem" then
174
+ return tostring(value)
175
+ end
176
+ if valueType == "Rect" then
177
+ return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }
178
+ end
179
+ if valueType == "NumberRange" then
180
+ return { value.Min, value.Max }
181
+ end
182
+ if valueType == "CFrame" then
183
+ return { value:GetComponents() }
184
+ end
185
+ if valueType == "NumberSequence" then
186
+ local result = {}
187
+ for _, keypoint in value.Keypoints do
188
+ table.insert(result, { keypoint.Time, keypoint.Value, keypoint.Envelope })
189
+ end
190
+ return result
191
+ end
192
+ if valueType == "ColorSequence" then
193
+ local result = {}
194
+ for _, keypoint in value.Keypoints do
195
+ table.insert(result, { keypoint.Time, "#" .. keypoint.Value:ToHex() })
196
+ end
197
+ return result
198
+ end
199
+ if valueType == "Font" then
200
+ return { Family = value.Family, Weight = value.Weight.Name, Style = value.Style.Name }
201
+ end
202
+ if valueType == "Instance" then
203
+ return InstanceRegistry.toRef(value)
204
+ end
205
+ if valueType == "boolean" or valueType == "number" or valueType == "string" then
206
+ return value
207
+ end
208
+ return tostring(value)
209
+ end
210
+
211
+ function ValueCodec.setProperty(instance, propertyName, value)
212
+ if propertyName == "Parent" or propertyName == "ClassName" then
213
+ error("Property cannot be changed through setProperties: " .. propertyName)
214
+ end
215
+ local readOk, current = pcall(function()
216
+ return instance[propertyName]
217
+ end)
218
+ if not readOk then
219
+ error("Unknown or unreadable property: " .. propertyName)
220
+ end
221
+ local decoded = ValueCodec.decodeForCurrent(current, value)
222
+ local writeOk, writeErr = pcall(function()
223
+ instance[propertyName] = decoded
224
+ end)
225
+ if not writeOk then
226
+ error("Failed to set " .. propertyName .. ": " .. tostring(writeErr))
227
+ end
228
+ return ValueCodec.encode(instance[propertyName])
229
+ end
230
+
231
+ return ValueCodec
@@ -1,85 +1,85 @@
1
- --[[
2
- Watcher: monitors LogService output and forwards to CLI
3
- ]]
4
-
5
- local LogService = game:GetService("LogService")
6
-
7
- local Watcher = {}
8
-
9
- local outputBuffer = {}
10
- local MAX_BUFFER = 500
11
- local connection = nil
12
- local outputCallback = nil
13
-
14
- local function classifyLevel(messageType)
15
- if messageType == Enum.MessageType.MessageOutput then
16
- return "info"
17
- elseif messageType == Enum.MessageType.MessageWarning then
18
- return "warn"
19
- elseif messageType == Enum.MessageType.MessageError then
20
- return "error"
21
- end
22
- return "info"
23
- end
24
-
25
- function Watcher.start(callback)
26
- outputCallback = callback
27
-
28
- if connection then
29
- connection:Disconnect()
30
- end
31
-
32
- connection = LogService.MessageOut:Connect(function(message, messageType)
33
- -- Skip Dominus's own messages
34
- if string.find(message, "[Dominus]", 1, true) then
35
- return
36
- end
37
-
38
- local entry = {
39
- message = message,
40
- level = classifyLevel(messageType),
41
- timestamp = os.clock(),
42
- }
43
-
44
- -- Buffer for get_output requests
45
- table.insert(outputBuffer, entry)
46
- if #outputBuffer > MAX_BUFFER then
47
- table.remove(outputBuffer, 1)
48
- end
49
-
50
- -- Forward to CLI
51
- if outputCallback then
52
- outputCallback(entry)
53
- end
54
- end)
55
- end
56
-
57
- function Watcher.stop()
58
- if connection then
59
- connection:Disconnect()
60
- connection = nil
61
- end
62
- outputCallback = nil
63
- end
64
-
65
- function Watcher.getRecentOutput(limit, levelFilter)
66
- limit = limit or 50
67
- local result = {}
68
-
69
- for i = math.max(1, #outputBuffer - limit + 1), #outputBuffer do
70
- local entry = outputBuffer[i]
71
- if entry then
72
- if not levelFilter or entry.level == levelFilter then
73
- table.insert(result, entry)
74
- end
75
- end
76
- end
77
-
78
- return { entries = result }
79
- end
80
-
81
- function Watcher.clearBuffer()
82
- outputBuffer = {}
83
- end
84
-
85
- return Watcher
1
+ --[[
2
+ Watcher: monitors LogService output and forwards to CLI
3
+ ]]
4
+
5
+ local LogService = game:GetService("LogService")
6
+
7
+ local Watcher = {}
8
+
9
+ local outputBuffer = {}
10
+ local MAX_BUFFER = 500
11
+ local connection = nil
12
+ local outputCallback = nil
13
+
14
+ local function classifyLevel(messageType)
15
+ if messageType == Enum.MessageType.MessageOutput then
16
+ return "info"
17
+ elseif messageType == Enum.MessageType.MessageWarning then
18
+ return "warn"
19
+ elseif messageType == Enum.MessageType.MessageError then
20
+ return "error"
21
+ end
22
+ return "info"
23
+ end
24
+
25
+ function Watcher.start(callback)
26
+ outputCallback = callback
27
+
28
+ if connection then
29
+ connection:Disconnect()
30
+ end
31
+
32
+ connection = LogService.MessageOut:Connect(function(message, messageType)
33
+ -- Skip every Dominus-prefixed message, including transport errors.
34
+ if string.find(message, "[Dominus", 1, true) then
35
+ return
36
+ end
37
+
38
+ local entry = {
39
+ message = message,
40
+ level = classifyLevel(messageType),
41
+ timestamp = os.clock(),
42
+ }
43
+
44
+ -- Buffer for get_output requests
45
+ table.insert(outputBuffer, entry)
46
+ if #outputBuffer > MAX_BUFFER then
47
+ table.remove(outputBuffer, 1)
48
+ end
49
+
50
+ -- Forward to CLI
51
+ if outputCallback then
52
+ outputCallback(entry)
53
+ end
54
+ end)
55
+ end
56
+
57
+ function Watcher.stop()
58
+ if connection then
59
+ connection:Disconnect()
60
+ connection = nil
61
+ end
62
+ outputCallback = nil
63
+ end
64
+
65
+ function Watcher.getRecentOutput(limit, levelFilter)
66
+ limit = limit or 50
67
+ local result = {}
68
+
69
+ for i = math.max(1, #outputBuffer - limit + 1), #outputBuffer do
70
+ local entry = outputBuffer[i]
71
+ if entry then
72
+ if not levelFilter or entry.level == levelFilter then
73
+ table.insert(result, entry)
74
+ end
75
+ end
76
+ end
77
+
78
+ return { entries = result }
79
+ end
80
+
81
+ function Watcher.clearBuffer()
82
+ outputBuffer = {}
83
+ end
84
+
85
+ return Watcher
@@ -1,81 +1,166 @@
1
- --[[
2
- WebSocket client wrapper using HttpService:CreateWebStreamClient
3
- Uses the new WebSocket API available in Studio plugins (Oct 2025+)
4
- ]]
5
-
6
- local HttpService = game:GetService("HttpService")
7
-
8
- local WsClient = {}
9
- WsClient.__index = WsClient
10
-
11
- local currentConnection = nil
12
- local messageCallback = nil
13
- local closeCallback = nil
14
- local errorCallback = nil
15
-
16
- function WsClient.connect(url, callbacks)
17
- local ok, wsOrErr = pcall(function()
18
- return HttpService:CreateWebStreamClient(Enum.WebStreamClientType.WebSocket, {
19
- Url = url,
20
- })
21
- end)
22
-
23
- if not ok then
24
- warn("[Dominus WS] Failed to create WebSocket:", wsOrErr)
25
- if callbacks.onError then
26
- callbacks.onError(tostring(wsOrErr))
27
- end
28
- return false
29
- end
30
-
31
- local wsClient = wsOrErr
32
- currentConnection = wsClient
33
-
34
- -- Handle incoming messages
35
- wsClient.MessageReceived:Connect(function(message)
36
- if callbacks.onMessage then
37
- callbacks.onMessage(message)
38
- end
39
- end)
40
-
41
- -- Handle disconnection
42
- wsClient.Closed:Connect(function()
43
- currentConnection = nil
44
- if callbacks.onClose then
45
- callbacks.onClose()
46
- end
47
- end)
48
-
49
- -- Notify connected
50
- if callbacks.onOpen then
51
- callbacks.onOpen()
52
- end
53
-
54
- return true
55
- end
56
-
57
- function WsClient.send(message)
58
- if currentConnection then
59
- local ok, err = pcall(function()
60
- currentConnection:Send(message)
61
- end)
62
- if not ok then
63
- warn("[Dominus WS] Send error:", err)
64
- end
65
- end
66
- end
67
-
68
- function WsClient.disconnect()
69
- if currentConnection then
70
- pcall(function()
71
- currentConnection:Close()
72
- end)
73
- currentConnection = nil
74
- end
75
- end
76
-
77
- function WsClient.isConnected()
78
- return currentConnection ~= nil
79
- end
80
-
81
- return WsClient
1
+ local HttpService = game:GetService("HttpService")
2
+
3
+ local WsClient = {}
4
+
5
+ local currentConnection = nil
6
+ local currentSignals = {}
7
+ local currentCancel = nil
8
+ local generation = 0
9
+
10
+ local function disconnectSignals()
11
+ for _, signal in currentSignals do
12
+ signal:Disconnect()
13
+ end
14
+ currentSignals = {}
15
+ end
16
+
17
+ local function closeCandidate(candidate)
18
+ pcall(function()
19
+ candidate:Close()
20
+ end)
21
+ end
22
+
23
+ function WsClient.connect(url, callbacks, timeoutSeconds)
24
+ WsClient.disconnect()
25
+ generation += 1
26
+ local attempt = generation
27
+ local completion = Instance.new("BindableEvent")
28
+ local completed = false
29
+ local opened = false
30
+
31
+ local ok, candidateOrError = pcall(function()
32
+ return HttpService:CreateWebStreamClient(Enum.WebStreamClientType.WebSocket, { Url = url })
33
+ end)
34
+ if not ok then
35
+ completion:Destroy()
36
+ if callbacks.onError then
37
+ callbacks.onError(0, tostring(candidateOrError))
38
+ end
39
+ return false, tostring(candidateOrError)
40
+ end
41
+ local candidate = candidateOrError
42
+
43
+ local function finish(success, reason)
44
+ if completed then
45
+ return
46
+ end
47
+ completed = true
48
+ completion:Fire(success, reason)
49
+ end
50
+ local function cancelPending()
51
+ finish(false, "Connection cancelled")
52
+ end
53
+ currentCancel = cancelPending
54
+
55
+ table.insert(
56
+ currentSignals,
57
+ candidate.Opened:Connect(function(statusCode, headers)
58
+ if attempt ~= generation then
59
+ return
60
+ end
61
+ opened = true
62
+ currentConnection = candidate
63
+ if callbacks.onOpen then
64
+ callbacks.onOpen(statusCode, headers)
65
+ end
66
+ finish(true)
67
+ end)
68
+ )
69
+
70
+ table.insert(
71
+ currentSignals,
72
+ candidate.MessageReceived:Connect(function(message)
73
+ if attempt == generation and opened and callbacks.onMessage then
74
+ callbacks.onMessage(message)
75
+ end
76
+ end)
77
+ )
78
+
79
+ table.insert(
80
+ currentSignals,
81
+ candidate.Error:Connect(function(statusCode, errorMessage)
82
+ if attempt ~= generation then
83
+ return
84
+ end
85
+ if callbacks.onError then
86
+ callbacks.onError(statusCode, errorMessage)
87
+ end
88
+ if not opened then
89
+ finish(false, errorMessage)
90
+ end
91
+ end)
92
+ )
93
+
94
+ table.insert(
95
+ currentSignals,
96
+ candidate.Closed:Connect(function()
97
+ if attempt ~= generation then
98
+ return
99
+ end
100
+ local wasOpened = opened
101
+ currentConnection = nil
102
+ opened = false
103
+ if callbacks.onClose then
104
+ callbacks.onClose()
105
+ end
106
+ if not wasOpened then
107
+ finish(false, "Connection closed before opening")
108
+ end
109
+ end)
110
+ )
111
+
112
+ task.delay(timeoutSeconds or 4, function()
113
+ if attempt == generation and not completed then
114
+ closeCandidate(candidate)
115
+ finish(false, "Connection timed out")
116
+ end
117
+ end)
118
+
119
+ local success, reason = completion.Event:Wait()
120
+ if currentCancel == cancelPending then
121
+ currentCancel = nil
122
+ end
123
+ completion:Destroy()
124
+ if not success then
125
+ closeCandidate(candidate)
126
+ if attempt == generation then
127
+ currentConnection = nil
128
+ disconnectSignals()
129
+ end
130
+ end
131
+ return success, reason
132
+ end
133
+
134
+ function WsClient.send(message)
135
+ if not currentConnection then
136
+ return false, "WebSocket is not open"
137
+ end
138
+ local ok, err = pcall(function()
139
+ currentConnection:Send(message)
140
+ end)
141
+ if not ok then
142
+ return false, tostring(err)
143
+ end
144
+ return true
145
+ end
146
+
147
+ function WsClient.disconnect()
148
+ generation += 1
149
+ local cancelPending = currentCancel
150
+ currentCancel = nil
151
+ if cancelPending then
152
+ cancelPending()
153
+ end
154
+ local connection = currentConnection
155
+ currentConnection = nil
156
+ disconnectSignals()
157
+ if connection then
158
+ closeCandidate(connection)
159
+ end
160
+ end
161
+
162
+ function WsClient.isConnected()
163
+ return currentConnection ~= nil
164
+ end
165
+
166
+ return WsClient