dominus-cli 0.6.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.
- package/CHANGELOG.md +175 -0
- package/README.md +164 -248
- package/dist/install-plugin.js +30 -0
- package/dist/install-plugin.js.map +1 -0
- package/dist/mcp.js +1576 -1346
- package/dist/mcp.js.map +1 -1
- package/docs/DOMINUS_2_PLAN.md +75 -0
- package/package.json +24 -25
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/CommandRouter.lua +94 -0
- package/plugin/src/Explorer.lua +530 -373
- package/plugin/src/InstanceRegistry.lua +122 -0
- package/plugin/src/Mutation.lua +135 -0
- package/plugin/src/Properties.lua +859 -725
- package/plugin/src/Protocol.lua +30 -30
- package/plugin/src/Reflection.lua +91 -59
- package/plugin/src/TestRunner.lua +69 -141
- package/plugin/src/UIBuilder.lua +857 -727
- package/plugin/src/ValueCodec.lua +230 -0
- package/plugin/src/Watcher.lua +85 -85
- package/plugin/src/WsClient.lua +153 -81
- package/plugin/src/init.server.lua +175 -362
- package/dist/index.js +0 -5796
- package/dist/index.js.map +0 -1
- package/plugin/src/Executor.lua +0 -117
package/plugin/src/Explorer.lua
CHANGED
|
@@ -1,373 +1,530 @@
|
|
|
1
|
-
--[[
|
|
2
|
-
Explorer module: serialize DataModel tree, read/write scripts, search, manage instances
|
|
3
|
-
]]
|
|
4
|
-
|
|
5
|
-
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
local
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
local
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
local
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
local
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if not
|
|
153
|
-
return { success = false, error = "
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if not
|
|
182
|
-
return { success = false, error = "
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
local
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
local
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
|
|
1
|
+
--[[
|
|
2
|
+
Explorer module: serialize DataModel tree, read/write scripts, search, manage instances
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
6
|
+
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
7
|
+
|
|
8
|
+
local Explorer = {}
|
|
9
|
+
|
|
10
|
+
local SCRIPT_CLASSES = {
|
|
11
|
+
Script = true,
|
|
12
|
+
LocalScript = true,
|
|
13
|
+
ModuleScript = true,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Explorer.getPath(instance)
|
|
17
|
+
local parts = {}
|
|
18
|
+
local current = instance
|
|
19
|
+
while current and current ~= game do
|
|
20
|
+
table.insert(parts, 1, current.Name)
|
|
21
|
+
current = current.Parent
|
|
22
|
+
end
|
|
23
|
+
return table.concat(parts, ".")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
function Explorer.getRef(instance)
|
|
27
|
+
return InstanceRegistry.toRef(instance)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
function Explorer.resolveTarget(target)
|
|
31
|
+
return InstanceRegistry.resolve(target)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
function Explorer.resolveInstance(path)
|
|
35
|
+
local parts = string.split(path, ".")
|
|
36
|
+
local current = game
|
|
37
|
+
for _, part in parts do
|
|
38
|
+
local child = current:FindFirstChild(part)
|
|
39
|
+
if not child then
|
|
40
|
+
return nil
|
|
41
|
+
end
|
|
42
|
+
current = child
|
|
43
|
+
end
|
|
44
|
+
return current
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
function Explorer.getTree(rootPath, maxDepth)
|
|
48
|
+
maxDepth = maxDepth or 3
|
|
49
|
+
|
|
50
|
+
local root
|
|
51
|
+
if rootPath and rootPath ~= "" then
|
|
52
|
+
root = Explorer.resolveInstance(rootPath)
|
|
53
|
+
if not root then
|
|
54
|
+
return {}
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
root = game
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
local function serialize(instance, depth)
|
|
61
|
+
if depth > maxDepth then
|
|
62
|
+
return nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local node = {
|
|
66
|
+
name = instance.Name,
|
|
67
|
+
className = instance.ClassName,
|
|
68
|
+
path = Explorer.getPath(instance),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
local children = {}
|
|
72
|
+
for _, child in instance:GetChildren() do
|
|
73
|
+
local childNode = serialize(child, depth + 1)
|
|
74
|
+
if childNode then
|
|
75
|
+
table.insert(children, childNode)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if #children > 0 then
|
|
80
|
+
node.children = children
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return node
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if root == game then
|
|
87
|
+
local topLevel = {}
|
|
88
|
+
for _, service in game:GetChildren() do
|
|
89
|
+
local ok, node = pcall(serialize, service, 1)
|
|
90
|
+
if ok and node then
|
|
91
|
+
table.insert(topLevel, node)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
return topLevel
|
|
95
|
+
else
|
|
96
|
+
local node = serialize(root, 0)
|
|
97
|
+
return node and { node } or {}
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
function Explorer.getScriptSource(path)
|
|
102
|
+
local instance = Explorer.resolveInstance(path)
|
|
103
|
+
if not instance then
|
|
104
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if not SCRIPT_CLASSES[instance.ClassName] then
|
|
108
|
+
return { success = false, error = "Not a script: " .. path }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
local ok, source = pcall(function()
|
|
112
|
+
return ScriptEditorService:GetEditorSource(instance)
|
|
113
|
+
end)
|
|
114
|
+
|
|
115
|
+
if not ok then
|
|
116
|
+
-- Fallback to .Source property
|
|
117
|
+
ok, source = pcall(function()
|
|
118
|
+
return instance.Source
|
|
119
|
+
end)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if not ok then
|
|
123
|
+
return { success = false, error = "Could not read source: " .. tostring(source) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
local lines = select(2, source:gsub("\n", "\n")) + 1
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
path = path,
|
|
130
|
+
className = instance.ClassName,
|
|
131
|
+
source = source,
|
|
132
|
+
lineCount = lines,
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
function Explorer.setScriptSource(path, newSource)
|
|
137
|
+
local instance = Explorer.resolveInstance(path)
|
|
138
|
+
if not instance then
|
|
139
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if not SCRIPT_CLASSES[instance.ClassName] then
|
|
143
|
+
return { success = false, error = "Not a script: " .. path }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
local ok, err = pcall(function()
|
|
147
|
+
ScriptEditorService:UpdateSourceAsync(instance, function()
|
|
148
|
+
return newSource
|
|
149
|
+
end)
|
|
150
|
+
end)
|
|
151
|
+
|
|
152
|
+
if not ok then
|
|
153
|
+
return { success = false, error = "Failed to update source: " .. tostring(err) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
return { success = true }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
function Explorer.insertInstance(className, parentPath, name, properties)
|
|
160
|
+
local parent = Explorer.resolveInstance(parentPath)
|
|
161
|
+
if not parent then
|
|
162
|
+
return { success = false, error = "Parent not found: " .. parentPath }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
local ok, instance = pcall(function()
|
|
166
|
+
local obj = Instance.new(className)
|
|
167
|
+
obj.Name = name
|
|
168
|
+
|
|
169
|
+
if properties then
|
|
170
|
+
for key, value in properties do
|
|
171
|
+
pcall(function()
|
|
172
|
+
obj[key] = value
|
|
173
|
+
end)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
obj.Parent = parent
|
|
178
|
+
return obj
|
|
179
|
+
end)
|
|
180
|
+
|
|
181
|
+
if not ok then
|
|
182
|
+
return { success = false, error = "Failed to create instance: " .. tostring(instance) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
return { success = true, path = Explorer.getPath(instance) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
function Explorer.deleteInstance(path)
|
|
189
|
+
local instance = Explorer.resolveInstance(path)
|
|
190
|
+
if not instance then
|
|
191
|
+
return { success = false, error = "Instance not found: " .. path }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
local ok, err = pcall(function()
|
|
195
|
+
instance:Destroy()
|
|
196
|
+
end)
|
|
197
|
+
|
|
198
|
+
if not ok then
|
|
199
|
+
return { success = false, error = "Failed to delete: " .. tostring(err) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
return { success = true }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
function Explorer.searchScripts(query, maxResults)
|
|
206
|
+
maxResults = maxResults or 20
|
|
207
|
+
local results = {}
|
|
208
|
+
local queryLower = string.lower(query)
|
|
209
|
+
|
|
210
|
+
local function searchIn(instance)
|
|
211
|
+
if #results >= maxResults then
|
|
212
|
+
return
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if SCRIPT_CLASSES[instance.ClassName] then
|
|
216
|
+
local ok, source = pcall(function()
|
|
217
|
+
return ScriptEditorService:GetEditorSource(instance)
|
|
218
|
+
end)
|
|
219
|
+
|
|
220
|
+
if not ok then
|
|
221
|
+
ok, source = pcall(function()
|
|
222
|
+
return instance.Source
|
|
223
|
+
end)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
if ok and source then
|
|
227
|
+
local matches = {}
|
|
228
|
+
local lineNum = 0
|
|
229
|
+
for line in source:gmatch("[^\n]+") do
|
|
230
|
+
lineNum = lineNum + 1
|
|
231
|
+
if string.find(string.lower(line), queryLower, 1, true) then
|
|
232
|
+
table.insert(matches, { line = lineNum, text = line })
|
|
233
|
+
if #matches >= 5 then
|
|
234
|
+
break
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
if #matches > 0 then
|
|
240
|
+
table.insert(results, {
|
|
241
|
+
path = Explorer.getPath(instance),
|
|
242
|
+
className = instance.ClassName,
|
|
243
|
+
matches = matches,
|
|
244
|
+
})
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
for _, child in instance:GetChildren() do
|
|
250
|
+
if #results >= maxResults then
|
|
251
|
+
return
|
|
252
|
+
end
|
|
253
|
+
pcall(searchIn, child)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
pcall(searchIn, game)
|
|
258
|
+
|
|
259
|
+
return { results = results }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
function Explorer.findReplaceScripts(spec)
|
|
263
|
+
local find = spec.find
|
|
264
|
+
local replace = spec.replace
|
|
265
|
+
local usePattern = spec.usePattern or false
|
|
266
|
+
local dryRun = spec.dryRun or false
|
|
267
|
+
|
|
268
|
+
if not find or find == "" then
|
|
269
|
+
return { success = false, error = "Missing 'find' parameter" }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
local root = game
|
|
273
|
+
if spec.root and spec.root ~= "" then
|
|
274
|
+
root = Explorer.resolveInstance(spec.root)
|
|
275
|
+
if not root then
|
|
276
|
+
local ok, svc = pcall(function()
|
|
277
|
+
return game:GetService(spec.root)
|
|
278
|
+
end)
|
|
279
|
+
if ok then
|
|
280
|
+
root = svc
|
|
281
|
+
else
|
|
282
|
+
return { success = false, error = "Root not found: " .. spec.root }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
288
|
+
local recording = nil
|
|
289
|
+
if not dryRun then
|
|
290
|
+
recording = ChangeHistoryService:TryBeginRecording("Dominus: Find/replace in scripts")
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
local modified = {}
|
|
294
|
+
local totalMatches = 0
|
|
295
|
+
local totalFiles = 0
|
|
296
|
+
|
|
297
|
+
local function processScript(instance)
|
|
298
|
+
if not SCRIPT_CLASSES[instance.ClassName] then
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
local ok, source = pcall(function()
|
|
303
|
+
return ScriptEditorService:GetEditorSource(instance)
|
|
304
|
+
end)
|
|
305
|
+
if not ok then
|
|
306
|
+
ok, source = pcall(function()
|
|
307
|
+
return instance.Source
|
|
308
|
+
end)
|
|
309
|
+
end
|
|
310
|
+
if not ok or not source then
|
|
311
|
+
return
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
local count = 0
|
|
315
|
+
if usePattern then
|
|
316
|
+
_, count = source:gsub(find, "")
|
|
317
|
+
else
|
|
318
|
+
_, count = source:gsub(find, "", nil)
|
|
319
|
+
-- plain text: count occurrences with plain find
|
|
320
|
+
count = 0
|
|
321
|
+
local startPos = 1
|
|
322
|
+
while true do
|
|
323
|
+
local foundPos = source:find(find, startPos, true)
|
|
324
|
+
if not foundPos then
|
|
325
|
+
break
|
|
326
|
+
end
|
|
327
|
+
count = count + 1
|
|
328
|
+
startPos = foundPos + #find
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
if count > 0 then
|
|
333
|
+
totalMatches = totalMatches + count
|
|
334
|
+
totalFiles = totalFiles + 1
|
|
335
|
+
|
|
336
|
+
local path = Explorer.getPath(instance)
|
|
337
|
+
table.insert(modified, { path = path, matches = count })
|
|
338
|
+
|
|
339
|
+
if not dryRun then
|
|
340
|
+
local newSource
|
|
341
|
+
if usePattern then
|
|
342
|
+
newSource = source:gsub(find, replace)
|
|
343
|
+
else
|
|
344
|
+
newSource =
|
|
345
|
+
source:gsub(find:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"), replace:gsub("%%", "%%%%"))
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
pcall(function()
|
|
349
|
+
ScriptEditorService:UpdateSourceAsync(instance, function()
|
|
350
|
+
return newSource
|
|
351
|
+
end)
|
|
352
|
+
end)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
local function walk(instance)
|
|
358
|
+
processScript(instance)
|
|
359
|
+
for _, child in instance:GetChildren() do
|
|
360
|
+
pcall(walk, child)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
pcall(walk, root)
|
|
365
|
+
|
|
366
|
+
if recording then
|
|
367
|
+
if totalFiles > 0 then
|
|
368
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
369
|
+
else
|
|
370
|
+
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
success = true,
|
|
376
|
+
modified = modified,
|
|
377
|
+
totalMatches = totalMatches,
|
|
378
|
+
totalFiles = totalFiles,
|
|
379
|
+
}
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
local function sourceHash(source)
|
|
383
|
+
local hash = 5381
|
|
384
|
+
for index = 1, #source do
|
|
385
|
+
hash = (hash * 33 + string.byte(source, index)) % 4294967296
|
|
386
|
+
end
|
|
387
|
+
return string.format("%d:%08x", #source, hash)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
function Explorer.getTreeV2(spec)
|
|
391
|
+
local root = game
|
|
392
|
+
if spec.root then
|
|
393
|
+
local resolved, err = InstanceRegistry.resolve(spec.root)
|
|
394
|
+
if not resolved then
|
|
395
|
+
return { success = false, error = err }
|
|
396
|
+
end
|
|
397
|
+
root = resolved
|
|
398
|
+
end
|
|
399
|
+
local maxDepth = math.clamp(spec.maxDepth or 3, 0, 12)
|
|
400
|
+
local maxNodes = math.clamp(spec.maxNodes or 1000, 1, 5000)
|
|
401
|
+
local nodeCount = 0
|
|
402
|
+
local truncated = false
|
|
403
|
+
|
|
404
|
+
local function serialize(instance, depth)
|
|
405
|
+
if depth > maxDepth or nodeCount >= maxNodes then
|
|
406
|
+
truncated = true
|
|
407
|
+
return nil
|
|
408
|
+
end
|
|
409
|
+
nodeCount += 1
|
|
410
|
+
local node = {
|
|
411
|
+
name = instance.Name,
|
|
412
|
+
className = instance.ClassName,
|
|
413
|
+
ref = InstanceRegistry.toRef(instance),
|
|
414
|
+
}
|
|
415
|
+
local children = instance:GetChildren()
|
|
416
|
+
table.sort(children, function(a, b)
|
|
417
|
+
if a.Name == b.Name then
|
|
418
|
+
return a.ClassName < b.ClassName
|
|
419
|
+
end
|
|
420
|
+
return a.Name < b.Name
|
|
421
|
+
end)
|
|
422
|
+
for _, child in children do
|
|
423
|
+
local childNode = serialize(child, depth + 1)
|
|
424
|
+
if childNode then
|
|
425
|
+
node.children = node.children or {}
|
|
426
|
+
table.insert(node.children, childNode)
|
|
427
|
+
end
|
|
428
|
+
if nodeCount >= maxNodes then
|
|
429
|
+
break
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
return node
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
if root == game then
|
|
436
|
+
local roots = {}
|
|
437
|
+
local services = game:GetChildren()
|
|
438
|
+
table.sort(services, function(a, b)
|
|
439
|
+
return a.Name < b.Name
|
|
440
|
+
end)
|
|
441
|
+
for _, service in services do
|
|
442
|
+
local node = serialize(service, 0)
|
|
443
|
+
if node then
|
|
444
|
+
table.insert(roots, node)
|
|
445
|
+
end
|
|
446
|
+
if nodeCount >= maxNodes then
|
|
447
|
+
break
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
return { success = true, roots = roots, nodeCount = nodeCount, truncated = truncated }
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
return { success = true, roots = { serialize(root, 0) }, nodeCount = nodeCount, truncated = truncated }
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
function Explorer.readScriptV2(spec)
|
|
457
|
+
local instance, err = InstanceRegistry.resolve(spec.target)
|
|
458
|
+
if not instance then
|
|
459
|
+
return { success = false, error = err }
|
|
460
|
+
end
|
|
461
|
+
if not SCRIPT_CLASSES[instance.ClassName] then
|
|
462
|
+
return { success = false, error = "Target is not a LuaSourceContainer" }
|
|
463
|
+
end
|
|
464
|
+
local ok, source = pcall(function()
|
|
465
|
+
return ScriptEditorService:GetEditorSource(instance)
|
|
466
|
+
end)
|
|
467
|
+
if not ok then
|
|
468
|
+
return { success = false, error = "Could not read script source: " .. tostring(source) }
|
|
469
|
+
end
|
|
470
|
+
return {
|
|
471
|
+
success = true,
|
|
472
|
+
target = InstanceRegistry.toRef(instance),
|
|
473
|
+
className = instance.ClassName,
|
|
474
|
+
source = source,
|
|
475
|
+
lineCount = select(2, source:gsub("\n", "\n")) + 1,
|
|
476
|
+
revision = sourceHash(source),
|
|
477
|
+
}
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
function Explorer.updateScriptV2(spec)
|
|
481
|
+
local instance, err = InstanceRegistry.resolve(spec.target)
|
|
482
|
+
if not instance then
|
|
483
|
+
return { success = false, error = err }
|
|
484
|
+
end
|
|
485
|
+
if not SCRIPT_CLASSES[instance.ClassName] then
|
|
486
|
+
return { success = false, error = "Target is not a LuaSourceContainer" }
|
|
487
|
+
end
|
|
488
|
+
if type(spec.source) ~= "string" then
|
|
489
|
+
return { success = false, error = "source must be a string" }
|
|
490
|
+
end
|
|
491
|
+
if #spec.source > 750000 then
|
|
492
|
+
return { success = false, error = "source exceeds 750,000 characters" }
|
|
493
|
+
end
|
|
494
|
+
if type(spec.expectedRevision) ~= "string" then
|
|
495
|
+
return { success = false, error = "expectedRevision is required; call studio_read_script first" }
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
local conflictRevision = nil
|
|
499
|
+
local ok, updateErr = pcall(function()
|
|
500
|
+
ScriptEditorService:UpdateSourceAsync(instance, function(oldSource)
|
|
501
|
+
local currentRevision = sourceHash(oldSource)
|
|
502
|
+
if currentRevision ~= spec.expectedRevision then
|
|
503
|
+
conflictRevision = currentRevision
|
|
504
|
+
return nil
|
|
505
|
+
end
|
|
506
|
+
return spec.source
|
|
507
|
+
end)
|
|
508
|
+
end)
|
|
509
|
+
if conflictRevision then
|
|
510
|
+
return {
|
|
511
|
+
success = false,
|
|
512
|
+
conflict = true,
|
|
513
|
+
error = "Script changed since it was read",
|
|
514
|
+
currentRevision = conflictRevision,
|
|
515
|
+
}
|
|
516
|
+
end
|
|
517
|
+
if not ok then
|
|
518
|
+
return { success = false, error = "Script update failed: " .. tostring(updateErr) }
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
local readback = Explorer.readScriptV2({ target = InstanceRegistry.toRef(instance) })
|
|
522
|
+
return {
|
|
523
|
+
success = true,
|
|
524
|
+
target = readback.target,
|
|
525
|
+
revision = readback.revision,
|
|
526
|
+
lineCount = readback.lineCount,
|
|
527
|
+
}
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
return Explorer
|