dominus-cli 2.0.0 → 2.1.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.
@@ -0,0 +1,230 @@
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 enumName = tostring(current.EnumType):match("Enum%.(.+)")
55
+ local itemName = value:match("Enum%.[^.]+%.(.+)") or value
56
+ if not enumName or not Enum[enumName] or not Enum[enumName][itemName] then
57
+ error("Invalid enum value " .. value)
58
+ end
59
+ return Enum[enumName][itemName]
60
+ end
61
+
62
+ function ValueCodec.decodeForCurrent(current, value)
63
+ local expectedType = typeof(current)
64
+ if expectedType == "UDim2" then
65
+ assert(type(value) == "table", "UDim2 must be an array or object")
66
+ return UDim2.new(
67
+ component(value, { "XScale", "xScale" }, 1, 0),
68
+ component(value, { "XOffset", "xOffset" }, 2, 0),
69
+ component(value, { "YScale", "yScale" }, 3, 0),
70
+ component(value, { "YOffset", "yOffset" }, 4, 0)
71
+ )
72
+ elseif expectedType == "UDim" then
73
+ assert(type(value) == "table", "UDim must be an array or object")
74
+ return UDim.new(component(value, { "Scale", "scale" }, 1, 0), component(value, { "Offset", "offset" }, 2, 0))
75
+ elseif expectedType == "Vector2" then
76
+ assert(type(value) == "table", "Vector2 must be an array or object")
77
+ return Vector2.new(component(value, { "X", "x" }, 1, 0), component(value, { "Y", "y" }, 2, 0))
78
+ elseif expectedType == "Vector3" then
79
+ assert(type(value) == "table", "Vector3 must be an array or object")
80
+ return Vector3.new(
81
+ component(value, { "X", "x" }, 1, 0),
82
+ component(value, { "Y", "y" }, 2, 0),
83
+ component(value, { "Z", "z" }, 3, 0)
84
+ )
85
+ elseif expectedType == "Color3" then
86
+ return decodeColor3(value)
87
+ elseif expectedType == "BrickColor" then
88
+ return BrickColor.new(value)
89
+ elseif expectedType == "EnumItem" then
90
+ return decodeEnum(current, value)
91
+ elseif expectedType == "Rect" then
92
+ assert(type(value) == "table", "Rect must be a four-number array")
93
+ return Rect.new(number(value[1], "1"), number(value[2], "2"), number(value[3], "3"), number(value[4], "4"))
94
+ elseif expectedType == "NumberRange" then
95
+ if type(value) == "number" then
96
+ return NumberRange.new(value)
97
+ end
98
+ assert(type(value) == "table", "NumberRange must be a number or two-number array")
99
+ return NumberRange.new(number(value[1], "1"), number(value[2] or value[1], "2"))
100
+ elseif expectedType == "CFrame" then
101
+ assert(type(value) == "table", "CFrame must be an array")
102
+ if #value == 3 then
103
+ return CFrame.new(value[1], value[2], value[3])
104
+ elseif #value == 12 then
105
+ return CFrame.new(table.unpack(value))
106
+ end
107
+ error("CFrame requires 3 or 12 numbers")
108
+ elseif expectedType == "NumberSequence" then
109
+ if type(value) == "number" then
110
+ return NumberSequence.new(value)
111
+ end
112
+ assert(type(value) == "table", "NumberSequence must be a number or keypoint array")
113
+ local keypoints = {}
114
+ for _, keypoint in value do
115
+ table.insert(keypoints, NumberSequenceKeypoint.new(keypoint[1], keypoint[2], keypoint[3] or 0))
116
+ end
117
+ return NumberSequence.new(keypoints)
118
+ elseif expectedType == "ColorSequence" then
119
+ if type(value) == "string" then
120
+ return ColorSequence.new(decodeColor3(value))
121
+ end
122
+ assert(type(value) == "table", "ColorSequence must be a color or keypoint array")
123
+ local keypoints = {}
124
+ for _, keypoint in value do
125
+ table.insert(keypoints, ColorSequenceKeypoint.new(keypoint[1], decodeColor3(keypoint[2])))
126
+ end
127
+ return ColorSequence.new(keypoints)
128
+ elseif expectedType == "Font" then
129
+ assert(type(value) == "table", "Font must be an object")
130
+ local weight = Enum.FontWeight[value.Weight or value.weight or "Regular"]
131
+ local style = Enum.FontStyle[value.Style or value.style or "Normal"]
132
+ assert(weight and style, "Invalid Font weight or style")
133
+ return Font.new(value.Family or value.family, weight, style)
134
+ elseif expectedType == "Instance" then
135
+ local resolved, err = InstanceRegistry.resolve(value)
136
+ if not resolved then
137
+ error(err)
138
+ end
139
+ return resolved
140
+ elseif expectedType == "number" then
141
+ return number(value, "Property")
142
+ elseif expectedType == "boolean" then
143
+ assert(type(value) == "boolean", "Property must be a boolean")
144
+ return value
145
+ elseif expectedType == "string" then
146
+ assert(type(value) == "string", "Property must be a string")
147
+ return value
148
+ end
149
+ return value
150
+ end
151
+
152
+ function ValueCodec.encode(value)
153
+ local valueType = typeof(value)
154
+ if valueType == "UDim2" then
155
+ return { value.X.Scale, value.X.Offset, value.Y.Scale, value.Y.Offset }
156
+ end
157
+ if valueType == "UDim" then
158
+ return { value.Scale, value.Offset }
159
+ end
160
+ if valueType == "Vector2" then
161
+ return { value.X, value.Y }
162
+ end
163
+ if valueType == "Vector3" then
164
+ return { value.X, value.Y, value.Z }
165
+ end
166
+ if valueType == "Color3" then
167
+ return "#" .. value:ToHex()
168
+ end
169
+ if valueType == "BrickColor" then
170
+ return value.Name
171
+ end
172
+ if valueType == "EnumItem" then
173
+ return tostring(value)
174
+ end
175
+ if valueType == "Rect" then
176
+ return { value.Min.X, value.Min.Y, value.Max.X, value.Max.Y }
177
+ end
178
+ if valueType == "NumberRange" then
179
+ return { value.Min, value.Max }
180
+ end
181
+ if valueType == "CFrame" then
182
+ return { value:GetComponents() }
183
+ end
184
+ if valueType == "NumberSequence" then
185
+ local result = {}
186
+ for _, keypoint in value.Keypoints do
187
+ table.insert(result, { keypoint.Time, keypoint.Value, keypoint.Envelope })
188
+ end
189
+ return result
190
+ end
191
+ if valueType == "ColorSequence" then
192
+ local result = {}
193
+ for _, keypoint in value.Keypoints do
194
+ table.insert(result, { keypoint.Time, "#" .. keypoint.Value:ToHex() })
195
+ end
196
+ return result
197
+ end
198
+ if valueType == "Font" then
199
+ return { Family = value.Family, Weight = value.Weight.Name, Style = value.Style.Name }
200
+ end
201
+ if valueType == "Instance" then
202
+ return InstanceRegistry.toRef(value)
203
+ end
204
+ if valueType == "boolean" or valueType == "number" or valueType == "string" then
205
+ return value
206
+ end
207
+ return tostring(value)
208
+ end
209
+
210
+ function ValueCodec.setProperty(instance, propertyName, value)
211
+ if propertyName == "Parent" or propertyName == "ClassName" then
212
+ error("Property cannot be changed through setProperties: " .. propertyName)
213
+ end
214
+ local readOk, current = pcall(function()
215
+ return instance[propertyName]
216
+ end)
217
+ if not readOk then
218
+ error("Unknown or unreadable property: " .. propertyName)
219
+ end
220
+ local decoded = ValueCodec.decodeForCurrent(current, value)
221
+ local writeOk, writeErr = pcall(function()
222
+ instance[propertyName] = decoded
223
+ end)
224
+ if not writeOk then
225
+ error("Failed to set " .. propertyName .. ": " .. tostring(writeErr))
226
+ end
227
+ return ValueCodec.encode(instance[propertyName])
228
+ end
229
+
230
+ 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,153 @@
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 generation = 0
8
+
9
+ local function disconnectSignals()
10
+ for _, signal in currentSignals do
11
+ signal:Disconnect()
12
+ end
13
+ currentSignals = {}
14
+ end
15
+
16
+ local function closeCandidate(candidate)
17
+ pcall(function()
18
+ candidate:Close()
19
+ end)
20
+ end
21
+
22
+ function WsClient.connect(url, callbacks, timeoutSeconds)
23
+ WsClient.disconnect()
24
+ generation += 1
25
+ local attempt = generation
26
+ local completion = Instance.new("BindableEvent")
27
+ local completed = false
28
+ local opened = false
29
+
30
+ local ok, candidateOrError = pcall(function()
31
+ return HttpService:CreateWebStreamClient(Enum.WebStreamClientType.WebSocket, { Url = url })
32
+ end)
33
+ if not ok then
34
+ completion:Destroy()
35
+ if callbacks.onError then
36
+ callbacks.onError(0, tostring(candidateOrError))
37
+ end
38
+ return false, tostring(candidateOrError)
39
+ end
40
+ local candidate = candidateOrError
41
+
42
+ local function finish(success, reason)
43
+ if completed then
44
+ return
45
+ end
46
+ completed = true
47
+ completion:Fire(success, reason)
48
+ end
49
+
50
+ table.insert(
51
+ currentSignals,
52
+ candidate.Opened:Connect(function(statusCode, headers)
53
+ if attempt ~= generation then
54
+ return
55
+ end
56
+ opened = true
57
+ currentConnection = candidate
58
+ if callbacks.onOpen then
59
+ callbacks.onOpen(statusCode, headers)
60
+ end
61
+ finish(true)
62
+ end)
63
+ )
64
+
65
+ table.insert(
66
+ currentSignals,
67
+ candidate.MessageReceived:Connect(function(message)
68
+ if attempt == generation and opened and callbacks.onMessage then
69
+ callbacks.onMessage(message)
70
+ end
71
+ end)
72
+ )
73
+
74
+ table.insert(
75
+ currentSignals,
76
+ candidate.Error:Connect(function(statusCode, errorMessage)
77
+ if attempt ~= generation then
78
+ return
79
+ end
80
+ if callbacks.onError then
81
+ callbacks.onError(statusCode, errorMessage)
82
+ end
83
+ if not opened then
84
+ finish(false, errorMessage)
85
+ end
86
+ end)
87
+ )
88
+
89
+ table.insert(
90
+ currentSignals,
91
+ candidate.Closed:Connect(function()
92
+ if attempt ~= generation then
93
+ return
94
+ end
95
+ local wasOpened = opened
96
+ currentConnection = nil
97
+ opened = false
98
+ if callbacks.onClose then
99
+ callbacks.onClose()
100
+ end
101
+ if not wasOpened then
102
+ finish(false, "Connection closed before opening")
103
+ end
104
+ end)
105
+ )
106
+
107
+ task.delay(timeoutSeconds or 4, function()
108
+ if attempt == generation and not completed then
109
+ closeCandidate(candidate)
110
+ finish(false, "Connection timed out")
111
+ end
112
+ end)
113
+
114
+ local success, reason = completion.Event:Wait()
115
+ completion:Destroy()
116
+ if not success then
117
+ closeCandidate(candidate)
118
+ if attempt == generation then
119
+ currentConnection = nil
120
+ disconnectSignals()
121
+ end
122
+ end
123
+ return success, reason
124
+ end
125
+
126
+ function WsClient.send(message)
127
+ if not currentConnection then
128
+ return false, "WebSocket is not open"
129
+ end
130
+ local ok, err = pcall(function()
131
+ currentConnection:Send(message)
132
+ end)
133
+ if not ok then
134
+ return false, tostring(err)
135
+ end
136
+ return true
137
+ end
138
+
139
+ function WsClient.disconnect()
140
+ generation += 1
141
+ local connection = currentConnection
142
+ currentConnection = nil
143
+ disconnectSignals()
144
+ if connection then
145
+ closeCandidate(connection)
146
+ end
147
+ end
148
+
149
+ function WsClient.isConnected()
150
+ return currentConnection ~= nil
151
+ end
152
+
153
+ return WsClient