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.
- package/LICENSE +21 -0
- package/README.md +248 -0
- package/dist/index.js +4700 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.js +907 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +78 -0
- package/plugin/default.project.json +6 -0
- package/plugin/src/Executor.lua +116 -0
- package/plugin/src/Explorer.lua +251 -0
- package/plugin/src/Properties.lua +424 -0
- package/plugin/src/Protocol.lua +30 -0
- package/plugin/src/Reflection.lua +137 -0
- package/plugin/src/TestRunner.lua +141 -0
- package/plugin/src/UIBuilder.lua +247 -0
- package/plugin/src/Watcher.lua +85 -0
- package/plugin/src/WsClient.lua +81 -0
- package/plugin/src/init.server.lua +315 -0
|
@@ -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)
|