dominus-cli 0.2.1 → 0.5.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.
@@ -209,7 +209,9 @@ function Explorer.searchScripts(query, maxResults)
209
209
  end)
210
210
 
211
211
  if not ok then
212
- ok, source = pcall(function() return instance.Source end)
212
+ ok, source = pcall(function()
213
+ return instance.Source
214
+ end)
213
215
  end
214
216
 
215
217
  if ok and source then
@@ -248,4 +250,124 @@ function Explorer.searchScripts(query, maxResults)
248
250
  return { results = results }
249
251
  end
250
252
 
253
+ function Explorer.findReplaceScripts(spec)
254
+ local find = spec.find
255
+ local replace = spec.replace
256
+ local usePattern = spec.usePattern or false
257
+ local dryRun = spec.dryRun or false
258
+
259
+ if not find or find == "" then
260
+ return { success = false, error = "Missing 'find' parameter" }
261
+ end
262
+
263
+ local root = game
264
+ if spec.root and spec.root ~= "" then
265
+ root = Explorer.resolveInstance(spec.root)
266
+ if not root then
267
+ local ok, svc = pcall(function()
268
+ return game:GetService(spec.root)
269
+ end)
270
+ if ok then
271
+ root = svc
272
+ else
273
+ return { success = false, error = "Root not found: " .. spec.root }
274
+ end
275
+ end
276
+ end
277
+
278
+ local ChangeHistoryService = game:GetService("ChangeHistoryService")
279
+ local recording = nil
280
+ if not dryRun then
281
+ recording = ChangeHistoryService:TryBeginRecording("Dominus: Find/replace in scripts")
282
+ end
283
+
284
+ local modified = {}
285
+ local totalMatches = 0
286
+ local totalFiles = 0
287
+
288
+ local function processScript(instance)
289
+ if not SCRIPT_CLASSES[instance.ClassName] then
290
+ return
291
+ end
292
+
293
+ local ok, source = pcall(function()
294
+ return ScriptEditorService:GetEditorSource(instance)
295
+ end)
296
+ if not ok then
297
+ ok, source = pcall(function()
298
+ return instance.Source
299
+ end)
300
+ end
301
+ if not ok or not source then
302
+ return
303
+ end
304
+
305
+ local count = 0
306
+ if usePattern then
307
+ _, count = source:gsub(find, "")
308
+ else
309
+ _, count = source:gsub(find, "", nil)
310
+ -- plain text: count occurrences with plain find
311
+ count = 0
312
+ local startPos = 1
313
+ while true do
314
+ local foundPos = source:find(find, startPos, true)
315
+ if not foundPos then
316
+ break
317
+ end
318
+ count = count + 1
319
+ startPos = foundPos + #find
320
+ end
321
+ end
322
+
323
+ if count > 0 then
324
+ totalMatches = totalMatches + count
325
+ totalFiles = totalFiles + 1
326
+
327
+ local path = Explorer.getPath(instance)
328
+ table.insert(modified, { path = path, matches = count })
329
+
330
+ if not dryRun then
331
+ local newSource
332
+ if usePattern then
333
+ newSource = source:gsub(find, replace)
334
+ else
335
+ newSource =
336
+ source:gsub(find:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"), replace:gsub("%%", "%%%%"))
337
+ end
338
+
339
+ pcall(function()
340
+ ScriptEditorService:UpdateSourceAsync(instance, function()
341
+ return newSource
342
+ end)
343
+ end)
344
+ end
345
+ end
346
+ end
347
+
348
+ local function walk(instance)
349
+ processScript(instance)
350
+ for _, child in instance:GetChildren() do
351
+ pcall(walk, child)
352
+ end
353
+ end
354
+
355
+ pcall(walk, root)
356
+
357
+ if recording then
358
+ if totalFiles > 0 then
359
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
360
+ else
361
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
362
+ end
363
+ end
364
+
365
+ return {
366
+ success = true,
367
+ modified = modified,
368
+ totalMatches = totalMatches,
369
+ totalFiles = totalFiles,
370
+ }
371
+ end
372
+
251
373
  return Explorer
@@ -18,7 +18,9 @@ local IGNORED_PROPERTIES = {
18
18
  local typeCache = {}
19
19
 
20
20
  local function getPropertyTypeMap(className)
21
- if typeCache[className] then return typeCache[className] end
21
+ if typeCache[className] then
22
+ return typeCache[className]
23
+ end
22
24
 
23
25
  local typeMap = {}
24
26
  local ok, props = pcall(function()
@@ -67,7 +69,8 @@ function Properties.get(path)
67
69
  if ok then
68
70
  properties = apiData
69
71
  else
70
- local commonProps = {"Name", "Position", "Size", "Color", "Material", "Transparency", "Anchored", "CanCollide"}
72
+ local commonProps =
73
+ { "Name", "Position", "Size", "Color", "Material", "Transparency", "Anchored", "CanCollide" }
71
74
  for _, propName in commonProps do
72
75
  local valOk, val = pcall(function()
73
76
  return instance[propName]
@@ -85,6 +88,80 @@ function Properties.get(path)
85
88
  return { properties = properties }
86
89
  end
87
90
 
91
+ function Properties.getDescendants(path)
92
+ local rootInstance = Explorer.resolveInstance(path)
93
+ if not rootInstance then
94
+ return { success = false, error = "Instance not found: " .. path }
95
+ end
96
+
97
+ local function getProps(instance)
98
+ local properties = {}
99
+
100
+ local ok, apiData = pcall(function()
101
+ local info = {}
102
+ local propData = ReflectionService:GetPropertiesOfClass(instance.ClassName)
103
+ for _, prop in propData do
104
+ if not IGNORED_PROPERTIES[prop.Name] then
105
+ local valueOk, value = pcall(function()
106
+ return instance[prop.Name]
107
+ end)
108
+ if valueOk then
109
+ table.insert(info, {
110
+ name = prop.Name,
111
+ value = tostring(value),
112
+ type = prop.ValueType and tostring(prop.ValueType) or typeof(value),
113
+ category = prop.Category or "Other",
114
+ })
115
+ end
116
+ end
117
+ end
118
+ return info
119
+ end)
120
+
121
+ if ok then
122
+ properties = apiData
123
+ else
124
+ local commonProps =
125
+ { "Name", "Position", "Size", "Color", "Material", "Transparency", "Anchored", "CanCollide" }
126
+ for _, propName in commonProps do
127
+ local valOk, val = pcall(function()
128
+ return instance[propName]
129
+ end)
130
+ if valOk and val ~= nil then
131
+ table.insert(properties, {
132
+ name = propName,
133
+ value = tostring(val),
134
+ type = typeof(val),
135
+ })
136
+ end
137
+ end
138
+ end
139
+
140
+ return properties
141
+ end
142
+
143
+ local results = {}
144
+
145
+ -- Include the root instance itself
146
+ table.insert(results, {
147
+ name = rootInstance.Name,
148
+ className = rootInstance.ClassName,
149
+ path = Explorer.getPath(rootInstance),
150
+ properties = getProps(rootInstance),
151
+ })
152
+
153
+ for _, descendant in rootInstance:GetDescendants() do
154
+ table.insert(results, {
155
+ name = descendant.Name,
156
+ className = descendant.ClassName,
157
+ path = Explorer.getPath(descendant),
158
+ properties = getProps(descendant),
159
+ })
160
+ end
161
+
162
+ return { success = true, descendants = results }
163
+ end
164
+
88
165
  --[[
89
166
  Smart type coercion: converts JSON-friendly values into Roblox types.
90
167
  Uses the expected property type from ReflectionService when available.
@@ -96,7 +173,9 @@ local function coerceValue(instance, key, value)
96
173
 
97
174
  -- If we don't know the expected type, try reading the current value
98
175
  if not expectedType then
99
- local curOk, curVal = pcall(function() return instance[key] end)
176
+ local curOk, curVal = pcall(function()
177
+ return instance[key]
178
+ end)
100
179
  if curOk and curVal ~= nil then
101
180
  expectedType = typeof(curVal)
102
181
  end
@@ -135,7 +214,9 @@ local function coerceValue(instance, key, value)
135
214
  if type(value) == "string" then
136
215
  -- "0.5, 0, 0.5, 0" format
137
216
  local a, b, c, d = value:match("([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)%s*,%s*([%d%.%-]+)")
138
- if a then return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d)) end
217
+ if a then
218
+ return UDim2.new(tonumber(a), tonumber(b), tonumber(c), tonumber(d))
219
+ end
139
220
  end
140
221
  end
141
222
 
@@ -169,10 +250,7 @@ local function coerceValue(instance, key, value)
169
250
  -- ═══════════════════════════════════════════
170
251
  if expectedType == "Vector2" then
171
252
  if type(value) == "table" then
172
- return Vector2.new(
173
- value.X or value.x or value[1] or 0,
174
- value.Y or value.y or value[2] or 0
175
- )
253
+ return Vector2.new(value.X or value.x or value[1] or 0, value.Y or value.y or value[2] or 0)
176
254
  end
177
255
  end
178
256
 
@@ -186,10 +264,14 @@ local function coerceValue(instance, key, value)
186
264
  end
187
265
  if value:match("^rgb") then
188
266
  local r, g, b = value:match("(%d+)%D+(%d+)%D+(%d+)")
189
- if r then return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b)) end
267
+ if r then
268
+ return Color3.fromRGB(tonumber(r), tonumber(g), tonumber(b))
269
+ end
190
270
  end
191
271
  local ok, bc = pcall(BrickColor.new, value)
192
- if ok then return bc.Color end
272
+ if ok then
273
+ return bc.Color
274
+ end
193
275
  end
194
276
  if type(value) == "table" then
195
277
  -- {R, G, B} 0-1 or {r, g, b}
@@ -198,7 +280,11 @@ local function coerceValue(instance, key, value)
198
280
  end
199
281
  -- {red, green, blue} 0-255
200
282
  if value.red or value.Red then
201
- return Color3.fromRGB(value.red or value.Red or 0, value.green or value.Green or 0, value.blue or value.Blue or 0)
283
+ return Color3.fromRGB(
284
+ value.red or value.Red or 0,
285
+ value.green or value.Green or 0,
286
+ value.blue or value.Blue or 0
287
+ )
202
288
  end
203
289
  -- Array [r, g, b]
204
290
  if value[1] then
@@ -229,14 +315,19 @@ local function coerceValue(instance, key, value)
229
315
  if type(value) == "table" then
230
316
  if value.Position or value.position then
231
317
  local pos = value.Position or value.position
232
- local cf = CFrame.new(pos.x or pos.X or pos[1] or 0, pos.y or pos.Y or pos[2] or 0, pos.z or pos.Z or pos[3] or 0)
318
+ local cf = CFrame.new(
319
+ pos.x or pos.X or pos[1] or 0,
320
+ pos.y or pos.Y or pos[2] or 0,
321
+ pos.z or pos.Z or pos[3] or 0
322
+ )
233
323
  if value.Rotation or value.rotation then
234
324
  local rot = value.Rotation or value.rotation
235
- cf = cf * CFrame.Angles(
236
- math.rad(rot.x or rot.X or rot[1] or 0),
237
- math.rad(rot.y or rot.Y or rot[2] or 0),
238
- math.rad(rot.z or rot.Z or rot[3] or 0)
239
- )
325
+ cf = cf
326
+ * CFrame.Angles(
327
+ math.rad(rot.x or rot.X or rot[1] or 0),
328
+ math.rad(rot.y or rot.Y or rot[2] or 0),
329
+ math.rad(rot.z or rot.Z or rot[3] or 0)
330
+ )
240
331
  end
241
332
  return cf
242
333
  end
@@ -252,8 +343,12 @@ local function coerceValue(instance, key, value)
252
343
  -- ═══════════════════════════════════════════
253
344
  if expectedType == "Rect" then
254
345
  if type(value) == "table" then
255
- return Rect.new(value[1] or value.Min and value.Min[1] or 0, value[2] or value.Min and value.Min[2] or 0,
256
- value[3] or value.Max and value.Max[1] or 0, value[4] or value.Max and value.Max[2] or 0)
346
+ return Rect.new(
347
+ value[1] or value.Min and value.Min[1] or 0,
348
+ value[2] or value.Min and value.Min[2] or 0,
349
+ value[3] or value.Max and value.Max[1] or 0,
350
+ value[4] or value.Max and value.Max[2] or 0
351
+ )
257
352
  end
258
353
  end
259
354
 
@@ -277,7 +372,14 @@ local function coerceValue(instance, key, value)
277
372
  if value[1] and type(value[1]) == "table" then
278
373
  local keypoints = {}
279
374
  for _, kp in value do
280
- table.insert(keypoints, NumberSequenceKeypoint.new(kp.Time or kp[1] or 0, kp.Value or kp[2] or 0, kp.Envelope or kp[3] or 0))
375
+ table.insert(
376
+ keypoints,
377
+ NumberSequenceKeypoint.new(
378
+ kp.Time or kp[1] or 0,
379
+ kp.Value or kp[2] or 0,
380
+ kp.Envelope or kp[3] or 0
381
+ )
382
+ )
281
383
  end
282
384
  return NumberSequence.new(keypoints)
283
385
  end
@@ -300,7 +402,7 @@ local function coerceValue(instance, key, value)
300
402
  for _, kp in value do
301
403
  local color = Color3.new(1, 1, 1)
302
404
  if kp.Color then
303
- if type(kp.Color) == "string" and kp.Color:sub(1,1) == "#" then
405
+ if type(kp.Color) == "string" and kp.Color:sub(1, 1) == "#" then
304
406
  color = Color3.fromHex(kp.Color)
305
407
  elseif type(kp.Color) == "table" then
306
408
  color = Color3.new(kp.Color[1] or 0, kp.Color[2] or 0, kp.Color[3] or 0)
@@ -322,10 +424,14 @@ local function coerceValue(instance, key, value)
322
424
  local weight = Enum.FontWeight.Regular
323
425
  local style = Enum.FontStyle.Normal
324
426
  if value.Weight or value.weight then
325
- pcall(function() weight = Enum.FontWeight[value.Weight or value.weight] end)
427
+ pcall(function()
428
+ weight = Enum.FontWeight[value.Weight or value.weight]
429
+ end)
326
430
  end
327
431
  if value.Style or value.style then
328
- pcall(function() style = Enum.FontStyle[value.Style or value.style] end)
432
+ pcall(function()
433
+ style = Enum.FontStyle[value.Style or value.style]
434
+ end)
329
435
  end
330
436
  return Font.new(family, weight, style)
331
437
  end
@@ -339,13 +445,17 @@ local function coerceValue(instance, key, value)
339
445
  -- ═══════════════════════════════════════════
340
446
  if type(value) == "string" then
341
447
  local currentVal = nil
342
- pcall(function() currentVal = instance[key] end)
448
+ pcall(function()
449
+ currentVal = instance[key]
450
+ end)
343
451
  if typeof(currentVal) == "EnumItem" then
344
452
  local enumType = tostring(currentVal.EnumType)
345
453
  local enumOk, enumVal = pcall(function()
346
454
  return Enum[enumType][value]
347
455
  end)
348
- if enumOk then return enumVal end
456
+ if enumOk then
457
+ return enumVal
458
+ end
349
459
  end
350
460
  end
351
461
 
@@ -356,8 +466,10 @@ local function coerceValue(instance, key, value)
356
466
  -- UDim2 guess: has XScale/YScale keys
357
467
  if value.XScale or value.xScale then
358
468
  return UDim2.new(
359
- value.XScale or value.xScale or 0, value.XOffset or value.xOffset or 0,
360
- value.YScale or value.yScale or 0, value.YOffset or value.yOffset or 0
469
+ value.XScale or value.xScale or 0,
470
+ value.XOffset or value.xOffset or 0,
471
+ value.YScale or value.yScale or 0,
472
+ value.YOffset or value.yOffset or 0
361
473
  )
362
474
  end
363
475
  -- Vector3 guess: 3 number elements or X/Y/Z keys
@@ -383,7 +495,9 @@ local function coerceValue(instance, key, value)
383
495
  return Color3.fromHex(value)
384
496
  end
385
497
  local ok, bc = pcall(BrickColor.new, value)
386
- if ok then return bc.Color end
498
+ if ok then
499
+ return bc.Color
500
+ end
387
501
  end
388
502
 
389
503
  if type(value) == "string" and key == "BrickColor" then
@@ -421,4 +535,75 @@ function Properties.set(path, propertiesToSet)
421
535
  return { success = true, set = set }
422
536
  end
423
537
 
538
+ function Properties.bulkSet(spec)
539
+ local ChangeHistoryService = game:GetService("ChangeHistoryService")
540
+ local recording = ChangeHistoryService:TryBeginRecording("Dominus: Bulk set properties")
541
+
542
+ local modified = 0
543
+ local errors = {}
544
+
545
+ if spec.targets and type(spec.targets) == "table" then
546
+ -- Explicit mode: array of {path, properties}
547
+ for _, target in ipairs(spec.targets) do
548
+ local instance = Explorer.resolveInstance(target.path)
549
+ if not instance then
550
+ table.insert(errors, "Not found: " .. tostring(target.path))
551
+ else
552
+ for key, value in target.properties do
553
+ local coerced = coerceValue(instance, key, value)
554
+ local ok, err = pcall(function()
555
+ instance[key] = coerced
556
+ end)
557
+ if not ok then
558
+ table.insert(errors, target.path .. "." .. key .. ": " .. tostring(err))
559
+ end
560
+ end
561
+ modified = modified + 1
562
+ end
563
+ end
564
+ elseif spec.root and spec.properties then
565
+ -- Filter mode: apply to all matching descendants
566
+ local root = Explorer.resolveInstance(spec.root)
567
+ if not root then
568
+ if recording then
569
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
570
+ end
571
+ return { success = false, error = "Root not found: " .. tostring(spec.root) }
572
+ end
573
+
574
+ local function processInstance(inst)
575
+ if spec.className and inst.ClassName ~= spec.className then
576
+ return
577
+ end
578
+ for key, value in spec.properties do
579
+ local coerced = coerceValue(inst, key, value)
580
+ local ok, err = pcall(function()
581
+ inst[key] = coerced
582
+ end)
583
+ if not ok then
584
+ table.insert(errors, Explorer.getPath(inst) .. "." .. key .. ": " .. tostring(err))
585
+ end
586
+ end
587
+ modified = modified + 1
588
+ end
589
+
590
+ -- Include root itself if it matches
591
+ processInstance(root)
592
+ for _, desc in root:GetDescendants() do
593
+ processInstance(desc)
594
+ end
595
+ else
596
+ if recording then
597
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
598
+ end
599
+ return { success = false, error = "Provide either 'targets' array or 'root' + 'properties'" }
600
+ end
601
+
602
+ if recording then
603
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
604
+ end
605
+
606
+ return { success = true, modified = modified, errors = errors }
607
+ end
608
+
424
609
  return Properties