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,122 @@
1
+ local HttpService = game:GetService("HttpService")
2
+
3
+ local InstanceRegistry = {}
4
+
5
+ local instanceToId = setmetatable({}, { __mode = "k" })
6
+ local idToInstance = {}
7
+
8
+ local function register(instance)
9
+ local existing = instanceToId[instance]
10
+ if existing then
11
+ return existing
12
+ end
13
+
14
+ local id = HttpService:GenerateGUID(false)
15
+ instanceToId[instance] = id
16
+ idToInstance[id] = instance
17
+ instance.Destroying:Connect(function()
18
+ if idToInstance[id] == instance then
19
+ idToInstance[id] = nil
20
+ end
21
+ instanceToId[instance] = nil
22
+ end)
23
+ return id
24
+ end
25
+
26
+ function InstanceRegistry.getPathSegments(instance)
27
+ local segments = {}
28
+ local current = instance
29
+ while current and current ~= game do
30
+ table.insert(segments, 1, current.Name)
31
+ current = current.Parent
32
+ end
33
+ return segments
34
+ end
35
+
36
+ function InstanceRegistry.getDisplayPath(instance)
37
+ local path = "game"
38
+ for _, segment in InstanceRegistry.getPathSegments(instance) do
39
+ local escaped = segment:gsub("\\", "\\\\"):gsub('"', '\\"')
40
+ path ..= '["' .. escaped .. '"]'
41
+ end
42
+ return path
43
+ end
44
+
45
+ function InstanceRegistry.toRef(instance)
46
+ return {
47
+ instanceId = register(instance),
48
+ pathSegments = InstanceRegistry.getPathSegments(instance),
49
+ displayPath = InstanceRegistry.getDisplayPath(instance),
50
+ }
51
+ end
52
+
53
+ local function resolveSegments(segments)
54
+ if type(segments) ~= "table" or #segments == 0 then
55
+ return nil, "pathSegments must contain at least one name"
56
+ end
57
+
58
+ local current = game
59
+ for index, segment in segments do
60
+ if type(segment) ~= "string" then
61
+ return nil, "pathSegments[" .. index .. "] must be a string"
62
+ end
63
+ local match = nil
64
+ local count = 0
65
+ for _, child in current:GetChildren() do
66
+ if child.Name == segment then
67
+ match = child
68
+ count += 1
69
+ end
70
+ end
71
+ if count == 0 and current == game then
72
+ local ok, service = pcall(function()
73
+ return game:GetService(segment)
74
+ end)
75
+ if ok then
76
+ match = service
77
+ count = 1
78
+ end
79
+ end
80
+ if count == 0 then
81
+ return nil, "Instance path does not exist at segment: " .. segment
82
+ end
83
+ if count > 1 then
84
+ return nil, "Instance path is ambiguous at duplicate sibling name: " .. segment
85
+ end
86
+ current = match
87
+ end
88
+ return current
89
+ end
90
+
91
+ function InstanceRegistry.resolve(target)
92
+ if typeof(target) == "Instance" then
93
+ return target
94
+ end
95
+ if type(target) ~= "table" then
96
+ return nil, "Target must be an instance reference"
97
+ end
98
+
99
+ if type(target.instanceId) == "string" then
100
+ local instance = idToInstance[target.instanceId]
101
+ if instance then
102
+ return instance
103
+ end
104
+ return nil, "Instance reference is stale: " .. target.instanceId
105
+ end
106
+
107
+ if target.pathSegments then
108
+ local instance, err = resolveSegments(target.pathSegments)
109
+ if instance then
110
+ register(instance)
111
+ end
112
+ return instance, err
113
+ end
114
+
115
+ return nil, "Target requires instanceId or pathSegments"
116
+ end
117
+
118
+ function InstanceRegistry.register(instance)
119
+ return register(instance)
120
+ end
121
+
122
+ return InstanceRegistry
@@ -0,0 +1,135 @@
1
+ local ChangeHistoryService = game:GetService("ChangeHistoryService")
2
+
3
+ local InstanceRegistry = require(script.Parent.InstanceRegistry)
4
+ local ValueCodec = require(script.Parent.ValueCodec)
5
+
6
+ local Mutation = {}
7
+ local MAX_OPERATIONS = 100
8
+ local MAX_PROPERTIES = 100
9
+
10
+ local function resolve(target, label)
11
+ local instance, err = InstanceRegistry.resolve(target)
12
+ if not instance then
13
+ error(label .. ": " .. tostring(err))
14
+ end
15
+ return instance
16
+ end
17
+
18
+ local function setProperties(instance, properties)
19
+ assert(type(properties) == "table", "properties must be an object")
20
+ local count = 0
21
+ local applied = {}
22
+ for propertyName, value in properties do
23
+ count += 1
24
+ if count > MAX_PROPERTIES then
25
+ error("Too many properties in one operation")
26
+ end
27
+ applied[propertyName] = ValueCodec.setProperty(instance, propertyName, value)
28
+ end
29
+ return applied
30
+ end
31
+
32
+ function Mutation.apply(spec)
33
+ local operations = spec.operations
34
+ if type(operations) ~= "table" or #operations == 0 then
35
+ return { success = false, error = "operations must contain at least one mutation" }
36
+ end
37
+ if #operations > MAX_OPERATIONS then
38
+ return { success = false, error = "A mutation batch is limited to " .. MAX_OPERATIONS .. " operations" }
39
+ end
40
+
41
+ local recording = ChangeHistoryService:TryBeginRecording("Dominus 2: Apply mutation batch")
42
+ if not recording then
43
+ return { success = false, error = "Another plugin recording is already active" }
44
+ end
45
+
46
+ local results = {}
47
+ local ok, err = pcall(function()
48
+ for index, operation in operations do
49
+ assert(type(operation) == "table", "Operation " .. index .. " must be an object")
50
+ if operation.op == "setProperties" then
51
+ local target = resolve(operation.target, "Operation " .. index .. " target")
52
+ table.insert(results, {
53
+ op = operation.op,
54
+ target = InstanceRegistry.toRef(target),
55
+ properties = setProperties(target, operation.properties),
56
+ })
57
+ elseif operation.op == "create" then
58
+ local parent = resolve(operation.parent, "Operation " .. index .. " parent")
59
+ assert(type(operation.className) == "string", "create.className must be a string")
60
+ assert(type(operation.name) == "string", "create.name must be a string")
61
+ local instance = Instance.new(operation.className)
62
+ instance.Name = operation.name
63
+ if operation.properties then
64
+ setProperties(instance, operation.properties)
65
+ end
66
+ instance.Parent = parent
67
+ table.insert(results, { op = operation.op, created = InstanceRegistry.toRef(instance) })
68
+ elseif operation.op == "move" then
69
+ local target = resolve(operation.target, "Operation " .. index .. " target")
70
+ local parent = resolve(operation.parent, "Operation " .. index .. " parent")
71
+ assert(target ~= game and target.Parent ~= game, "Services cannot be moved")
72
+ target.Parent = parent
73
+ table.insert(results, { op = operation.op, target = InstanceRegistry.toRef(target) })
74
+ else
75
+ error("Unsupported mutation operation: " .. tostring(operation.op))
76
+ end
77
+ end
78
+ end)
79
+
80
+ if not ok then
81
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
82
+ return { success = false, error = tostring(err), rolledBack = true }
83
+ end
84
+
85
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
86
+ return { success = true, operationCount = #results, results = results }
87
+ end
88
+
89
+ function Mutation.delete(spec)
90
+ if spec.confirm ~= true then
91
+ return { success = false, error = "confirm must be true" }
92
+ end
93
+ if type(spec.targets) ~= "table" or #spec.targets == 0 then
94
+ return { success = false, error = "targets must contain at least one instance reference" }
95
+ end
96
+ if #spec.targets > 50 then
97
+ return { success = false, error = "At most 50 instances can be deleted at once" }
98
+ end
99
+
100
+ local targets = {}
101
+ local seen = {}
102
+ for index, targetRef in spec.targets do
103
+ local target = resolve(targetRef, "Target " .. index)
104
+ assert(target ~= game and target.Parent ~= game, "Services cannot be deleted")
105
+ local id = InstanceRegistry.register(target)
106
+ if not seen[id] then
107
+ seen[id] = true
108
+ table.insert(targets, target)
109
+ end
110
+ end
111
+
112
+ local recording = ChangeHistoryService:TryBeginRecording("Dominus 2: Delete instances")
113
+ if not recording then
114
+ return { success = false, error = "Another plugin recording is already active" }
115
+ end
116
+ local deleted = {}
117
+ local ok, err = pcall(function()
118
+ for _, target in targets do
119
+ table.insert(deleted, {
120
+ name = target.Name,
121
+ className = target.ClassName,
122
+ pathSegments = InstanceRegistry.getPathSegments(target),
123
+ })
124
+ target:Destroy()
125
+ end
126
+ end)
127
+ if not ok then
128
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
129
+ return { success = false, error = tostring(err), rolledBack = true }
130
+ end
131
+ ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
132
+ return { success = true, deleted = deleted }
133
+ end
134
+
135
+ return Mutation