dominus-cli 2.1.1 → 2.2.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.
- package/CHANGELOG.md +138 -95
- package/README.md +33 -12
- package/dist/bridge-daemon.js +581 -0
- package/dist/bridge-daemon.js.map +1 -0
- package/dist/install-plugin.js +499 -27
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +1298 -690
- package/dist/mcp.js.map +1 -1
- package/docs/DOMINUS_2_PLAN.md +8 -2
- package/package.json +1 -1
- package/plugin/Dominus.rbxmx +8 -4
- package/plugin/default.project.json +4 -1
- package/plugin/src/ValueCodec.lua +1 -1
- package/plugin/src/init.server.lua +5 -1
package/dist/install-plugin.js
CHANGED
|
@@ -1,75 +1,547 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import fs4 from 'fs';
|
|
3
3
|
import os2 from 'os';
|
|
4
|
-
import
|
|
4
|
+
import path4 from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import { WebSocket } from 'ws';
|
|
9
|
+
import { nanoid } from 'nanoid';
|
|
10
|
+
import { z } from 'zod';
|
|
6
11
|
import * as crypto from 'crypto';
|
|
7
12
|
|
|
8
|
-
var DOMINUS_DIR =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
var DOMINUS_DIR = path4.join(os2.homedir(), ".dominus");
|
|
14
|
+
var CONFIG_PATH = path4.join(DOMINUS_DIR, "config.json");
|
|
15
|
+
path4.join(DOMINUS_DIR, "dominus.db");
|
|
16
|
+
path4.join(DOMINUS_DIR, "rules.json");
|
|
17
|
+
var DEFAULTS = {
|
|
18
|
+
provider: "openai",
|
|
19
|
+
apiBaseUrl: "",
|
|
20
|
+
model: "",
|
|
21
|
+
port: 18088,
|
|
22
|
+
maxToolIterations: 25,
|
|
23
|
+
maxVerifyRetries: 3,
|
|
24
|
+
tokenBudget: 128e3,
|
|
25
|
+
autoIndex: true,
|
|
26
|
+
autoLearn: true,
|
|
27
|
+
userName: "Developer"
|
|
28
|
+
};
|
|
12
29
|
function ensureDir() {
|
|
13
|
-
if (!
|
|
14
|
-
|
|
30
|
+
if (!fs4.existsSync(DOMINUS_DIR)) {
|
|
31
|
+
fs4.mkdirSync(DOMINUS_DIR, { recursive: true });
|
|
15
32
|
}
|
|
16
33
|
}
|
|
34
|
+
function readJson(filePath, defaults) {
|
|
35
|
+
try {
|
|
36
|
+
if (fs4.existsSync(filePath)) {
|
|
37
|
+
const raw = fs4.readFileSync(filePath, "utf-8");
|
|
38
|
+
return { ...defaults, ...JSON.parse(raw) };
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
return { ...defaults };
|
|
43
|
+
}
|
|
44
|
+
function loadConfig() {
|
|
45
|
+
ensureDir();
|
|
46
|
+
return readJson(CONFIG_PATH, DEFAULTS);
|
|
47
|
+
}
|
|
17
48
|
function getDominusDir() {
|
|
18
49
|
ensureDir();
|
|
19
50
|
return DOMINUS_DIR;
|
|
20
51
|
}
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
var DOMINUS_PROTOCOL_VERSION = 2;
|
|
53
|
+
var MAX_MESSAGE_BYTES = 1024 * 1024;
|
|
54
|
+
var StudioMsg = {
|
|
55
|
+
HELLO: "studio:hello",
|
|
56
|
+
CONNECTED: "studio:connected",
|
|
57
|
+
DISCONNECTED: "studio:disconnected",
|
|
58
|
+
EXPLORER: "studio:explorer",
|
|
59
|
+
OUTPUT: "studio:output",
|
|
60
|
+
ERROR: "studio:error",
|
|
61
|
+
SELECTION: "studio:selection",
|
|
62
|
+
SCRIPT_CONTENT: "studio:script:content",
|
|
63
|
+
SCRIPT_CHANGED: "studio:script:changed",
|
|
64
|
+
RESPONSE: "studio:response",
|
|
65
|
+
REFLECTION: "studio:reflection",
|
|
66
|
+
TEST_RESULTS: "studio:test:results",
|
|
67
|
+
AUTHENTICATED: "server:authenticated",
|
|
68
|
+
AUTH_REJECTED: "server:auth_rejected"
|
|
69
|
+
};
|
|
70
|
+
var ControllerMsg = {
|
|
71
|
+
HELLO: "controller:hello",
|
|
72
|
+
WELCOME: "controller:welcome",
|
|
73
|
+
RELAY: "controller:relay",
|
|
74
|
+
TARGET_STATE: "controller:target_state",
|
|
75
|
+
SET_ACTIVE_CONNECTION: "controller:set_active_connection"
|
|
76
|
+
};
|
|
77
|
+
function createMessage(type, payload) {
|
|
78
|
+
return { id: nanoid(), type, payload, ts: Date.now() };
|
|
79
|
+
}
|
|
80
|
+
function parseMessage(raw) {
|
|
81
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_MESSAGE_BYTES) {
|
|
82
|
+
throw new Error(`WebSocket message exceeds ${MAX_MESSAGE_BYTES} bytes`);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(raw);
|
|
86
|
+
return wsMessageSchema.parse(parsed);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new Error(`Failed to parse WebSocket message: ${raw.slice(0, 100)}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function serializeMessage(msg) {
|
|
92
|
+
const serialized = JSON.stringify(msg);
|
|
93
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_MESSAGE_BYTES) {
|
|
94
|
+
throw new Error(`WebSocket message exceeds ${MAX_MESSAGE_BYTES} bytes`);
|
|
95
|
+
}
|
|
96
|
+
return serialized;
|
|
97
|
+
}
|
|
98
|
+
var wsMessageSchema = z.object({
|
|
99
|
+
id: z.string().min(1).max(128),
|
|
100
|
+
type: z.string().min(1).max(128),
|
|
101
|
+
payload: z.unknown(),
|
|
102
|
+
ts: z.number().finite()
|
|
103
|
+
}).strict();
|
|
23
104
|
var TOKEN_FILE = "bridge-token";
|
|
24
105
|
function loadOrCreateBridgeToken() {
|
|
25
|
-
const tokenPath =
|
|
106
|
+
const tokenPath = path4.join(getDominusDir(), TOKEN_FILE);
|
|
26
107
|
try {
|
|
27
|
-
const existing =
|
|
108
|
+
const existing = fs4.readFileSync(tokenPath, "utf8").trim();
|
|
28
109
|
if (existing.length >= 32) return existing;
|
|
29
110
|
} catch {
|
|
30
111
|
}
|
|
31
112
|
const token = crypto.randomBytes(32).toString("base64url");
|
|
32
|
-
|
|
113
|
+
fs4.writeFileSync(tokenPath, `${token}
|
|
33
114
|
`, { encoding: "utf8", mode: 384 });
|
|
34
115
|
try {
|
|
35
|
-
|
|
116
|
+
fs4.chmodSync(tokenPath, 384);
|
|
36
117
|
} catch {
|
|
37
118
|
}
|
|
38
119
|
return token;
|
|
39
120
|
}
|
|
40
121
|
|
|
122
|
+
// src/transport/ws-client.ts
|
|
123
|
+
var DominusClient = class extends EventEmitter {
|
|
124
|
+
ws = null;
|
|
125
|
+
port;
|
|
126
|
+
token;
|
|
127
|
+
connectTimeoutMs;
|
|
128
|
+
connections = /* @__PURE__ */ new Map();
|
|
129
|
+
activeConnectionId = null;
|
|
130
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
131
|
+
constructor(port = 18088, options = {}) {
|
|
132
|
+
super();
|
|
133
|
+
this.port = port;
|
|
134
|
+
this.token = options.token ?? loadOrCreateBridgeToken();
|
|
135
|
+
this.connectTimeoutMs = options.connectTimeoutMs ?? 8e3;
|
|
136
|
+
}
|
|
137
|
+
connect() {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const ws = new WebSocket(`ws://127.0.0.1:${this.port}`, {
|
|
140
|
+
perMessageDeflate: false,
|
|
141
|
+
maxPayload: 1024 * 1024
|
|
142
|
+
});
|
|
143
|
+
let settled = false;
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
if (!settled) {
|
|
146
|
+
settled = true;
|
|
147
|
+
ws.close(1008, "Dominus controller handshake timed out");
|
|
148
|
+
reject(new Error("Timed out authenticating with the Dominus bridge"));
|
|
149
|
+
}
|
|
150
|
+
}, this.connectTimeoutMs);
|
|
151
|
+
ws.on("open", () => {
|
|
152
|
+
this.ws = ws;
|
|
153
|
+
ws.send(
|
|
154
|
+
serializeMessage(
|
|
155
|
+
createMessage(ControllerMsg.HELLO, {
|
|
156
|
+
protocolVersion: DOMINUS_PROTOCOL_VERSION,
|
|
157
|
+
token: this.token
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
ws.on("message", (raw) => {
|
|
163
|
+
try {
|
|
164
|
+
const msg = parseMessage(raw.toString());
|
|
165
|
+
if (msg.type === ControllerMsg.WELCOME && !settled) {
|
|
166
|
+
settled = true;
|
|
167
|
+
clearTimeout(timer);
|
|
168
|
+
this.applyWelcome(msg.payload);
|
|
169
|
+
this.emit("server:started", { port: this.port, relay: true });
|
|
170
|
+
resolve();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (msg.type === StudioMsg.AUTH_REJECTED && !settled) {
|
|
174
|
+
settled = true;
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
const payload = msg.payload;
|
|
177
|
+
reject(new Error(payload.error ?? "Dominus controller authentication failed"));
|
|
178
|
+
ws.close(1008, "Controller authentication failed");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
this.handleMessage(msg);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
this.emit("server:error", err);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
ws.on("close", () => {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
this.ws = null;
|
|
189
|
+
this.connections.clear();
|
|
190
|
+
this.activeConnectionId = null;
|
|
191
|
+
if (!settled) {
|
|
192
|
+
settled = true;
|
|
193
|
+
reject(new Error("Dominus bridge closed before authentication completed"));
|
|
194
|
+
}
|
|
195
|
+
this.rejectPending(new Error("Dominus relay connection lost"));
|
|
196
|
+
this.emit("studio:disconnected");
|
|
197
|
+
});
|
|
198
|
+
ws.on("error", (err) => {
|
|
199
|
+
if (!settled) {
|
|
200
|
+
settled = true;
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
reject(err);
|
|
203
|
+
}
|
|
204
|
+
this.emit("server:error", err);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
applyWelcome(payload) {
|
|
209
|
+
const welcome = payload;
|
|
210
|
+
this.replaceConnections(welcome.connections ?? []);
|
|
211
|
+
this.activeConnectionId = welcome.activeConnectionId ?? null;
|
|
212
|
+
if (!this.resolveTargetInfo() && this.connections.size === 1) {
|
|
213
|
+
this.activeConnectionId = this.connections.keys().next().value ?? null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
handleMessage(msg) {
|
|
217
|
+
if (msg.type === ControllerMsg.TARGET_STATE) {
|
|
218
|
+
const payload = msg.payload;
|
|
219
|
+
this.replaceConnections(payload.connections ?? []);
|
|
220
|
+
if (this.activeConnectionId && !this.connections.has(this.activeConnectionId)) {
|
|
221
|
+
this.activeConnectionId = null;
|
|
222
|
+
}
|
|
223
|
+
if (!this.activeConnectionId && this.connections.size === 1) {
|
|
224
|
+
this.activeConnectionId = this.connections.keys().next().value ?? null;
|
|
225
|
+
}
|
|
226
|
+
this.emit("controller:target_state", payload);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (msg.type === StudioMsg.CONNECTED) {
|
|
230
|
+
const connection = msg.payload;
|
|
231
|
+
this.connections.set(connection.connectionId, toConnectionInfo(connection));
|
|
232
|
+
if (!this.activeConnectionId && this.connections.size === 1) {
|
|
233
|
+
this.activeConnectionId = connection.connectionId;
|
|
234
|
+
}
|
|
235
|
+
this.emit("studio:connected", connection);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (msg.type === StudioMsg.DISCONNECTED) {
|
|
239
|
+
const payload = msg.payload;
|
|
240
|
+
if (payload.connectionId) this.connections.delete(payload.connectionId);
|
|
241
|
+
if (this.activeConnectionId === payload.connectionId) this.activeConnectionId = null;
|
|
242
|
+
if (!this.activeConnectionId && this.connections.size === 1) {
|
|
243
|
+
this.activeConnectionId = this.connections.keys().next().value ?? null;
|
|
244
|
+
}
|
|
245
|
+
this.emit("studio:disconnected", payload);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (msg.type === StudioMsg.RESPONSE) {
|
|
249
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
250
|
+
if (pending) {
|
|
251
|
+
clearTimeout(pending.timer);
|
|
252
|
+
this.pendingRequests.delete(msg.id);
|
|
253
|
+
pending.resolve(msg);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
this.emit(msg.type, msg.payload);
|
|
258
|
+
this.emit("message", msg);
|
|
259
|
+
}
|
|
260
|
+
replaceConnections(connections) {
|
|
261
|
+
this.connections.clear();
|
|
262
|
+
for (const connection of connections) {
|
|
263
|
+
this.connections.set(connection.connectionId, toConnectionInfo(connection));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
sendMessage(message) {
|
|
267
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
268
|
+
throw new Error("Not connected to the Dominus bridge");
|
|
269
|
+
}
|
|
270
|
+
this.ws.send(serializeMessage(message));
|
|
271
|
+
}
|
|
272
|
+
waitForResponse(message, timeoutMs) {
|
|
273
|
+
return new Promise((resolve, reject) => {
|
|
274
|
+
const timer = setTimeout(() => {
|
|
275
|
+
this.pendingRequests.delete(message.id);
|
|
276
|
+
reject(new Error(`Dominus request timed out: ${message.type}`));
|
|
277
|
+
}, timeoutMs);
|
|
278
|
+
this.pendingRequests.set(message.id, {
|
|
279
|
+
resolve,
|
|
280
|
+
reject,
|
|
281
|
+
timer
|
|
282
|
+
});
|
|
283
|
+
try {
|
|
284
|
+
this.sendMessage(message);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
this.pendingRequests.delete(message.id);
|
|
288
|
+
reject(err);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async sendControlRequest(type, payload, timeoutMs = 1e4) {
|
|
293
|
+
const response = await this.waitForResponse(createMessage(type, payload), timeoutMs);
|
|
294
|
+
const result = response.payload;
|
|
295
|
+
if (result.error || result.success === false) throw new Error(result.error ?? `${type} failed`);
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
sendRequest(type, payload, timeoutMs = 15e3) {
|
|
299
|
+
const target = this.resolveTargetInfo();
|
|
300
|
+
if (!target || !this.activeConnectionId) {
|
|
301
|
+
return Promise.reject(
|
|
302
|
+
new Error(
|
|
303
|
+
this.connections.size > 1 ? "Multiple Studio sessions are connected. Call dominus_select_studio first." : "No authenticated Roblox Studio connection"
|
|
304
|
+
)
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const request = createMessage(type, payload);
|
|
308
|
+
const relay = createMessage(ControllerMsg.RELAY, {
|
|
309
|
+
connectionId: this.activeConnectionId,
|
|
310
|
+
request
|
|
311
|
+
});
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const timer = setTimeout(() => {
|
|
314
|
+
this.pendingRequests.delete(request.id);
|
|
315
|
+
reject(new Error(`Studio request timed out: ${type}`));
|
|
316
|
+
}, timeoutMs);
|
|
317
|
+
this.pendingRequests.set(request.id, {
|
|
318
|
+
resolve,
|
|
319
|
+
reject,
|
|
320
|
+
timer
|
|
321
|
+
});
|
|
322
|
+
try {
|
|
323
|
+
this.sendMessage(relay);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
this.pendingRequests.delete(request.id);
|
|
327
|
+
reject(err);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
sendNotification(type, payload) {
|
|
332
|
+
if (!this.activeConnectionId) return;
|
|
333
|
+
const request = createMessage(type, payload);
|
|
334
|
+
this.sendMessage(
|
|
335
|
+
createMessage(ControllerMsg.RELAY, {
|
|
336
|
+
connectionId: this.activeConnectionId,
|
|
337
|
+
request
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
isConnected() {
|
|
342
|
+
return this.resolveTargetInfo() !== null;
|
|
343
|
+
}
|
|
344
|
+
getConnectionInfo() {
|
|
345
|
+
const target = this.resolveTargetInfo();
|
|
346
|
+
if (!target || !this.ws) return null;
|
|
347
|
+
return { ...target, ws: this.ws };
|
|
348
|
+
}
|
|
349
|
+
listConnections() {
|
|
350
|
+
return [...this.connections.values()].map((connection) => ({
|
|
351
|
+
placeId: connection.placeId ?? 0,
|
|
352
|
+
connectionId: connection.connectionId,
|
|
353
|
+
clientId: connection.clientId,
|
|
354
|
+
placeName: connection.placeName,
|
|
355
|
+
studioVersion: connection.studioVersion,
|
|
356
|
+
connectedAt: connection.connectedAt,
|
|
357
|
+
active: connection.connectionId === this.activeConnectionId
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
getActiveConnectionId() {
|
|
361
|
+
return this.activeConnectionId;
|
|
362
|
+
}
|
|
363
|
+
async setActiveConnectionId(connectionId) {
|
|
364
|
+
if (connectionId && !this.connections.has(connectionId)) {
|
|
365
|
+
throw new Error(`Studio connection ${connectionId} is not connected`);
|
|
366
|
+
}
|
|
367
|
+
await this.sendControlRequest(ControllerMsg.SET_ACTIVE_CONNECTION, { connectionId });
|
|
368
|
+
this.activeConnectionId = connectionId;
|
|
369
|
+
}
|
|
370
|
+
getActivePlaceId() {
|
|
371
|
+
return this.resolveTargetInfo()?.placeId ?? 0;
|
|
372
|
+
}
|
|
373
|
+
setActivePlaceId(placeId) {
|
|
374
|
+
if (placeId === 0) {
|
|
375
|
+
void this.setActiveConnectionId(null);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const matches = [...this.connections.values()].filter(
|
|
379
|
+
(connection) => connection.placeId === placeId
|
|
380
|
+
);
|
|
381
|
+
if (matches.length !== 1) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
matches.length === 0 ? `Place ${placeId} is not connected` : `Place ${placeId} has multiple Studio sessions; select a connectionId instead`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
void this.setActiveConnectionId(matches[0].connectionId);
|
|
387
|
+
}
|
|
388
|
+
resolveTargetInfo() {
|
|
389
|
+
if (this.activeConnectionId) return this.connections.get(this.activeConnectionId) ?? null;
|
|
390
|
+
if (this.connections.size === 1) return this.connections.values().next().value ?? null;
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
getPort() {
|
|
394
|
+
return this.port;
|
|
395
|
+
}
|
|
396
|
+
stop() {
|
|
397
|
+
this.rejectPending(new Error("Dominus client is shutting down"));
|
|
398
|
+
return new Promise((resolve) => {
|
|
399
|
+
if (!this.ws) {
|
|
400
|
+
resolve();
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const ws = this.ws;
|
|
404
|
+
this.ws = null;
|
|
405
|
+
ws.once("close", resolve);
|
|
406
|
+
ws.close(1e3, "Dominus client shutdown");
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
rejectPending(error) {
|
|
410
|
+
for (const pending of this.pendingRequests.values()) {
|
|
411
|
+
clearTimeout(pending.timer);
|
|
412
|
+
pending.reject(error);
|
|
413
|
+
}
|
|
414
|
+
this.pendingRequests.clear();
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
function toConnectionInfo(connection) {
|
|
418
|
+
return {
|
|
419
|
+
connectedAt: connection.connectedAt,
|
|
420
|
+
connectionId: connection.connectionId,
|
|
421
|
+
clientId: connection.clientId,
|
|
422
|
+
studioVersion: connection.studioVersion,
|
|
423
|
+
placeId: connection.placeId,
|
|
424
|
+
placeName: connection.placeName
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function isPortInUse(port) {
|
|
428
|
+
return new Promise((resolve) => {
|
|
429
|
+
const ws = new WebSocket(`ws://127.0.0.1:${port}`);
|
|
430
|
+
const timer = setTimeout(() => {
|
|
431
|
+
ws.close();
|
|
432
|
+
resolve(false);
|
|
433
|
+
}, 750);
|
|
434
|
+
ws.on("open", () => {
|
|
435
|
+
clearTimeout(timer);
|
|
436
|
+
ws.close();
|
|
437
|
+
resolve(true);
|
|
438
|
+
});
|
|
439
|
+
ws.on("error", () => {
|
|
440
|
+
clearTimeout(timer);
|
|
441
|
+
resolve(false);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/transport/daemon-launcher.ts
|
|
447
|
+
function resolveDaemonPath() {
|
|
448
|
+
const besideEntry = fileURLToPath(new URL("./bridge-daemon.js", import.meta.url));
|
|
449
|
+
if (fs4.existsSync(besideEntry)) return besideEntry;
|
|
450
|
+
const projectBuild = path4.resolve(
|
|
451
|
+
path4.dirname(fileURLToPath(import.meta.url)),
|
|
452
|
+
"../../dist/bridge-daemon.js"
|
|
453
|
+
);
|
|
454
|
+
if (fs4.existsSync(projectBuild)) return projectBuild;
|
|
455
|
+
throw new Error("Dominus bridge daemon is missing. Reinstall or rebuild dominus-cli.");
|
|
456
|
+
}
|
|
457
|
+
function launchBridgeDaemon(port, daemonPath = resolveDaemonPath()) {
|
|
458
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
459
|
+
detached: true,
|
|
460
|
+
env: { ...process.env, DOMINUS_BRIDGE_PORT: String(port) },
|
|
461
|
+
stdio: "ignore",
|
|
462
|
+
windowsHide: true
|
|
463
|
+
});
|
|
464
|
+
child.unref();
|
|
465
|
+
}
|
|
466
|
+
async function connectController(port, timeoutMs, token) {
|
|
467
|
+
const client = new DominusClient(port, { connectTimeoutMs: timeoutMs, token });
|
|
468
|
+
try {
|
|
469
|
+
await client.connect();
|
|
470
|
+
return client;
|
|
471
|
+
} catch (error) {
|
|
472
|
+
await client.stop().catch(() => void 0);
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function ensureBridgeDaemon(port, options = {}) {
|
|
477
|
+
const attempts = options.attempts ?? 30;
|
|
478
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? 750;
|
|
479
|
+
const retryDelayMs = options.retryDelayMs ?? 100;
|
|
480
|
+
try {
|
|
481
|
+
return await connectController(port, connectTimeoutMs, options.token);
|
|
482
|
+
} catch (initialError) {
|
|
483
|
+
if (await isPortInUse(port)) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`Port ${port} is occupied by a process that is not this Dominus bridge: ${formatError(initialError)}`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const launch = options.launch ?? ((targetPort) => launchBridgeDaemon(targetPort, options.daemonPath));
|
|
490
|
+
launch(port);
|
|
491
|
+
let lastError;
|
|
492
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
493
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
494
|
+
try {
|
|
495
|
+
return await connectController(port, connectTimeoutMs, options.token);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
lastError = error;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
throw new Error(
|
|
501
|
+
`Dominus could not start its local Studio bridge on port ${port}: ${formatError(lastError)}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
function formatError(error) {
|
|
505
|
+
return error instanceof Error ? error.message : String(error ?? "unknown error");
|
|
506
|
+
}
|
|
507
|
+
|
|
41
508
|
// src/install-plugin.ts
|
|
42
509
|
var TOKEN_PLACEHOLDER = "__DOMINUS_BRIDGE_TOKEN__";
|
|
43
510
|
function getPluginDirectory() {
|
|
44
511
|
if (process.platform === "win32") {
|
|
45
512
|
const localAppData = process.env.LOCALAPPDATA;
|
|
46
513
|
if (!localAppData) throw new Error("LOCALAPPDATA is not set");
|
|
47
|
-
return
|
|
514
|
+
return path4.join(localAppData, "Roblox", "Plugins");
|
|
48
515
|
}
|
|
49
516
|
if (process.platform === "darwin") {
|
|
50
|
-
return
|
|
517
|
+
return path4.join(os2.homedir(), "Documents", "Roblox", "Plugins");
|
|
51
518
|
}
|
|
52
|
-
return
|
|
519
|
+
return path4.join(os2.homedir(), ".local", "share", "Roblox", "Plugins");
|
|
53
520
|
}
|
|
54
|
-
var distDirectory =
|
|
55
|
-
var source =
|
|
521
|
+
var distDirectory = path4.dirname(fileURLToPath(import.meta.url));
|
|
522
|
+
var source = path4.resolve(distDirectory, "..", "plugin", "Dominus.rbxmx");
|
|
56
523
|
var destinationDirectory = getPluginDirectory();
|
|
57
|
-
var destination =
|
|
58
|
-
if (!
|
|
524
|
+
var destination = path4.join(destinationDirectory, "Dominus2.rbxmx");
|
|
525
|
+
if (!fs4.existsSync(source)) {
|
|
59
526
|
throw new Error(`Packaged plugin not found at ${source}. Run pnpm build:plugin first.`);
|
|
60
527
|
}
|
|
61
|
-
var template =
|
|
528
|
+
var template = fs4.readFileSync(source, "utf8");
|
|
62
529
|
if (!template.includes(TOKEN_PLACEHOLDER)) {
|
|
63
530
|
throw new Error("Packaged plugin template is missing its bridge token placeholder");
|
|
64
531
|
}
|
|
65
532
|
var personalizedPlugin = template.replaceAll(TOKEN_PLACEHOLDER, loadOrCreateBridgeToken());
|
|
66
|
-
|
|
67
|
-
|
|
533
|
+
fs4.mkdirSync(destinationDirectory, { recursive: true });
|
|
534
|
+
fs4.writeFileSync(destination, personalizedPlugin, "utf8");
|
|
68
535
|
for (const obsoleteName of ["Dominus2.rbxm", "Dominus.rbxm", "Dominus.rbxmx"]) {
|
|
69
|
-
const obsoletePath =
|
|
70
|
-
if (obsoletePath !== destination)
|
|
536
|
+
const obsoletePath = path4.join(destinationDirectory, obsoleteName);
|
|
537
|
+
if (obsoletePath !== destination) fs4.rmSync(obsoletePath, { force: true });
|
|
71
538
|
}
|
|
72
539
|
console.log(`Installed Dominus 2 Studio plugin to ${destination}`);
|
|
73
|
-
console.log(
|
|
540
|
+
console.log(
|
|
541
|
+
"Dominus 2 authenticates automatically. Restart Studio only if the toolbar does not reconnect."
|
|
542
|
+
);
|
|
543
|
+
var bridge = await ensureBridgeDaemon(loadConfig().port);
|
|
544
|
+
await bridge.stop();
|
|
545
|
+
console.log("Dominus Studio bridge is running in the background.");
|
|
74
546
|
//# sourceMappingURL=install-plugin.js.map
|
|
75
547
|
//# sourceMappingURL=install-plugin.js.map
|