dominus-cli 0.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,315 @@
1
+ --[[
2
+ Dominus - Roblox Studio Plugin
3
+ AI-powered development agent bridge via WebSocket
4
+
5
+ This plugin connects to the Dominus CLI running on your machine,
6
+ enabling real-time bidirectional communication for AI-assisted development.
7
+ ]]
8
+
9
+ local toolbar = plugin:CreateToolbar("Dominus")
10
+ local connectButton = toolbar:CreateButton(
11
+ "Connect",
12
+ "Connect to Dominus CLI agent",
13
+ "rbxassetid://12974519994"
14
+ )
15
+
16
+ local WsClient = require(script.WsClient)
17
+ local Protocol = require(script.Protocol)
18
+ local Explorer = require(script.Explorer)
19
+ local Executor = require(script.Executor)
20
+ local Watcher = require(script.Watcher)
21
+ local Properties = require(script.Properties)
22
+ local Reflection = require(script.Reflection)
23
+ local TestRunner = require(script.TestRunner)
24
+ local UIBuilder = require(script.UIBuilder)
25
+
26
+ local BASE_PORT = 18088
27
+ local MAX_PORT_SCAN = 10
28
+ local activePort = BASE_PORT
29
+ local connected = false
30
+ local ws = nil
31
+
32
+ local function log(msg)
33
+ print("[Dominus]", msg)
34
+ end
35
+
36
+ local function handleMessage(rawMessage)
37
+ local ok, msg = pcall(Protocol.decode, rawMessage)
38
+ if not ok then
39
+ warn("[Dominus] Failed to decode message:", msg)
40
+ return
41
+ end
42
+
43
+ local msgType = msg.type
44
+ local payload = msg.payload
45
+ local msgId = msg.id
46
+
47
+ -- Route message to appropriate handler
48
+ if msgType == "cli:get:explorer" then
49
+ local tree = Explorer.getTree(payload.rootPath, payload.maxDepth or 3)
50
+ WsClient.send(Protocol.encode({
51
+ id = msgId,
52
+ type = "studio:response",
53
+ payload = { tree = tree },
54
+ ts = os.clock(),
55
+ }))
56
+
57
+ elseif msgType == "cli:get:script" then
58
+ local result = Explorer.getScriptSource(payload.path)
59
+ WsClient.send(Protocol.encode({
60
+ id = msgId,
61
+ type = "studio:response",
62
+ payload = result,
63
+ ts = os.clock(),
64
+ }))
65
+
66
+ elseif msgType == "cli:set:script" then
67
+ local result = Explorer.setScriptSource(payload.path, payload.source)
68
+ WsClient.send(Protocol.encode({
69
+ id = msgId,
70
+ type = "studio:response",
71
+ payload = result,
72
+ ts = os.clock(),
73
+ }))
74
+
75
+ elseif msgType == "cli:run" then
76
+ local result = Executor.run(payload.code)
77
+ WsClient.send(Protocol.encode({
78
+ id = msgId,
79
+ type = "studio:response",
80
+ payload = result,
81
+ ts = os.clock(),
82
+ }))
83
+
84
+ elseif msgType == "cli:get:properties" then
85
+ local result = Properties.get(payload.path)
86
+ WsClient.send(Protocol.encode({
87
+ id = msgId,
88
+ type = "studio:response",
89
+ payload = result,
90
+ ts = os.clock(),
91
+ }))
92
+
93
+ elseif msgType == "cli:set:properties" then
94
+ local result = Properties.set(payload.path, payload.properties)
95
+ WsClient.send(Protocol.encode({
96
+ id = msgId,
97
+ type = "studio:response",
98
+ payload = result,
99
+ ts = os.clock(),
100
+ }))
101
+
102
+ elseif msgType == "cli:insert" then
103
+ local result = Explorer.insertInstance(payload.className, payload.parentPath, payload.name, payload.properties)
104
+ WsClient.send(Protocol.encode({
105
+ id = msgId,
106
+ type = "studio:response",
107
+ payload = result,
108
+ ts = os.clock(),
109
+ }))
110
+
111
+ elseif msgType == "cli:delete" then
112
+ local result = Explorer.deleteInstance(payload.path)
113
+ WsClient.send(Protocol.encode({
114
+ id = msgId,
115
+ type = "studio:response",
116
+ payload = result,
117
+ ts = os.clock(),
118
+ }))
119
+
120
+ elseif msgType == "cli:get:selection" then
121
+ local Selection = game:GetService("Selection")
122
+ local selected = Selection:Get()
123
+ local paths = {}
124
+ local classNames = {}
125
+ for _, obj in selected do
126
+ table.insert(paths, Explorer.getPath(obj))
127
+ table.insert(classNames, obj.ClassName)
128
+ end
129
+ WsClient.send(Protocol.encode({
130
+ id = msgId,
131
+ type = "studio:response",
132
+ payload = { paths = paths, classNames = classNames },
133
+ ts = os.clock(),
134
+ }))
135
+
136
+ elseif msgType == "cli:search:scripts" then
137
+ local result = Explorer.searchScripts(payload.query, payload.maxResults or 20)
138
+ WsClient.send(Protocol.encode({
139
+ id = msgId,
140
+ type = "studio:response",
141
+ payload = result,
142
+ ts = os.clock(),
143
+ }))
144
+
145
+ elseif msgType == "cli:get:output" then
146
+ local result = Watcher.getRecentOutput(payload.limit or 50, payload.level)
147
+ WsClient.send(Protocol.encode({
148
+ id = msgId,
149
+ type = "studio:response",
150
+ payload = result,
151
+ ts = os.clock(),
152
+ }))
153
+
154
+ elseif msgType == "cli:run:tests" then
155
+ local result = TestRunner.run(payload.testName)
156
+ WsClient.send(Protocol.encode({
157
+ id = msgId,
158
+ type = "studio:response",
159
+ payload = result,
160
+ ts = os.clock(),
161
+ }))
162
+
163
+ elseif msgType == "cli:test:run" then
164
+ -- ExecuteRunModeAsync yields until EndTest is called, so run in a thread
165
+ task.spawn(function()
166
+ local result = TestRunner.executeRunMode(payload.args)
167
+ WsClient.send(Protocol.encode({
168
+ id = msgId,
169
+ type = "studio:response",
170
+ payload = result,
171
+ ts = os.clock(),
172
+ }))
173
+ end)
174
+
175
+ elseif msgType == "cli:test:play" then
176
+ -- ExecutePlayModeAsync yields until EndTest is called, so run in a thread
177
+ task.spawn(function()
178
+ local result = TestRunner.executePlayMode(payload.args)
179
+ WsClient.send(Protocol.encode({
180
+ id = msgId,
181
+ type = "studio:response",
182
+ payload = result,
183
+ ts = os.clock(),
184
+ }))
185
+ end)
186
+
187
+ elseif msgType == "cli:test:end" then
188
+ local result = TestRunner.endTest(payload.value)
189
+ WsClient.send(Protocol.encode({
190
+ id = msgId,
191
+ type = "studio:response",
192
+ payload = result,
193
+ ts = os.clock(),
194
+ }))
195
+
196
+ elseif msgType == "cli:build:ui" then
197
+ local result = UIBuilder.build(payload)
198
+ WsClient.send(Protocol.encode({
199
+ id = msgId,
200
+ type = "studio:response",
201
+ payload = result,
202
+ ts = os.clock(),
203
+ }))
204
+
205
+ elseif msgType == "cli:get:reflection" then
206
+ local result = Reflection.getClassInfo(payload.className)
207
+ WsClient.send(Protocol.encode({
208
+ id = msgId,
209
+ type = "studio:response",
210
+ payload = result,
211
+ ts = os.clock(),
212
+ }))
213
+
214
+ else
215
+ warn("[Dominus] Unknown message type:", msgType)
216
+ end
217
+ end
218
+
219
+ local function tryConnectToPort(port)
220
+ log("Trying ws://localhost:" .. port .. "...")
221
+
222
+ local success = WsClient.connect("ws://localhost:" .. port, {
223
+ onOpen = function()
224
+ connected = true
225
+ activePort = port
226
+ connectButton:SetActive(true)
227
+ log("Connected to Dominus on port " .. port .. "!")
228
+
229
+ local studioVersion = game:GetService("RobloxPluginGuiService") and "Studio" or "Unknown"
230
+ local placeName = "Unknown"
231
+ pcall(function()
232
+ placeName = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId).Name
233
+ end)
234
+
235
+ WsClient.send(Protocol.encode({
236
+ id = Protocol.generateId(),
237
+ type = "studio:connected",
238
+ payload = {
239
+ studioVersion = studioVersion,
240
+ placeId = game.PlaceId,
241
+ placeName = placeName,
242
+ },
243
+ ts = os.clock(),
244
+ }))
245
+
246
+ Watcher.start(function(entry)
247
+ if connected then
248
+ WsClient.send(Protocol.encode({
249
+ id = Protocol.generateId(),
250
+ type = "studio:output",
251
+ payload = entry,
252
+ ts = os.clock(),
253
+ }))
254
+ end
255
+ end)
256
+ end,
257
+
258
+ onMessage = function(message)
259
+ task.spawn(handleMessage, message)
260
+ end,
261
+
262
+ onClose = function()
263
+ connected = false
264
+ connectButton:SetActive(false)
265
+ Watcher.stop()
266
+ log("Disconnected from Dominus CLI")
267
+ end,
268
+
269
+ onError = function(err)
270
+ warn("[Dominus] WebSocket error on port " .. port .. ":", err)
271
+ end,
272
+ })
273
+
274
+ return success
275
+ end
276
+
277
+ local function connect()
278
+ if connected then
279
+ log("Already connected, disconnecting first...")
280
+ WsClient.disconnect()
281
+ connected = false
282
+ end
283
+
284
+ log("Scanning for Dominus server (ports " .. BASE_PORT .. "-" .. (BASE_PORT + MAX_PORT_SCAN - 1) .. ")...")
285
+
286
+ for i = 0, MAX_PORT_SCAN - 1 do
287
+ local port = BASE_PORT + i
288
+ local success = tryConnectToPort(port)
289
+ if success then
290
+ return
291
+ end
292
+ end
293
+
294
+ warn("[Dominus] No Dominus server found. Is `dominus` or `dominus mcp` running?")
295
+ end
296
+
297
+ connectButton.Click:Connect(function()
298
+ if connected then
299
+ WsClient.disconnect()
300
+ connected = false
301
+ connectButton:SetActive(false)
302
+ else
303
+ connect()
304
+ end
305
+ end)
306
+
307
+ -- Auto-connect on plugin load
308
+ task.delay(1, function()
309
+ connect()
310
+ end)
311
+
312
+ plugin.Unloading:Connect(function()
313
+ Watcher.stop()
314
+ WsClient.disconnect()
315
+ end)