dominus-cli 0.1.0 → 0.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import fs4, { existsSync, readFileSync, unlinkSync } from 'fs';
3
- import path3, { join } from 'path';
4
- import os2, { tmpdir } from 'os';
2
+ import fs5, { existsSync, readFileSync, unlinkSync } from 'fs';
3
+ import path4, { join } from 'path';
4
+ import os3, { tmpdir } from 'os';
5
5
  import { nanoid } from 'nanoid';
6
6
  import { WebSocket, WebSocketServer } from 'ws';
7
7
  import { EventEmitter } from 'events';
@@ -18,6 +18,7 @@ import { execSync } from 'child_process';
18
18
  import { randomBytes } from 'crypto';
19
19
  import Spinner from 'ink-spinner';
20
20
  import OpenAI from 'openai';
21
+ import https from 'https';
21
22
 
22
23
  var __defProp = Object.defineProperty;
23
24
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -55,14 +56,14 @@ function resolveProviderSettings(config) {
55
56
  };
56
57
  }
57
58
  function ensureDir() {
58
- if (!fs4.existsSync(DOMINUS_DIR)) {
59
- fs4.mkdirSync(DOMINUS_DIR, { recursive: true });
59
+ if (!fs5.existsSync(DOMINUS_DIR)) {
60
+ fs5.mkdirSync(DOMINUS_DIR, { recursive: true });
60
61
  }
61
62
  }
62
63
  function readJson(filePath, defaults) {
63
64
  try {
64
- if (fs4.existsSync(filePath)) {
65
- const raw = fs4.readFileSync(filePath, "utf-8");
65
+ if (fs5.existsSync(filePath)) {
66
+ const raw = fs5.readFileSync(filePath, "utf-8");
66
67
  return { ...defaults, ...JSON.parse(raw) };
67
68
  }
68
69
  } catch {
@@ -71,7 +72,7 @@ function readJson(filePath, defaults) {
71
72
  }
72
73
  function writeJson(filePath, data) {
73
74
  ensureDir();
74
- fs4.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
75
+ fs5.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
75
76
  }
76
77
  function loadConfig() {
77
78
  ensureDir();
@@ -123,10 +124,10 @@ function createConfigAccess() {
123
124
  var DOMINUS_DIR, CONFIG_PATH, DB_PATH, RULES_PATH, PROVIDERS, DEFAULTS, DEFAULT_RULES;
124
125
  var init_config = __esm({
125
126
  "src/config/config.ts"() {
126
- DOMINUS_DIR = path3.join(os2.homedir(), ".dominus");
127
- CONFIG_PATH = path3.join(DOMINUS_DIR, "config.json");
128
- DB_PATH = path3.join(DOMINUS_DIR, "dominus.db");
129
- RULES_PATH = path3.join(DOMINUS_DIR, "rules.json");
127
+ DOMINUS_DIR = path4.join(os3.homedir(), ".dominus");
128
+ CONFIG_PATH = path4.join(DOMINUS_DIR, "config.json");
129
+ DB_PATH = path4.join(DOMINUS_DIR, "dominus.db");
130
+ RULES_PATH = path4.join(DOMINUS_DIR, "rules.json");
130
131
  PROVIDERS = {
131
132
  openai: {
132
133
  name: "OpenAI",
@@ -459,6 +460,166 @@ var init_ws_server = __esm({
459
460
  };
460
461
  }
461
462
  });
463
+ async function isPortInUse(port) {
464
+ return new Promise((resolve) => {
465
+ const ws = new WebSocket(`ws://localhost:${port}`);
466
+ const timer = setTimeout(() => {
467
+ ws.close();
468
+ resolve(false);
469
+ }, 1e3);
470
+ ws.on("open", () => {
471
+ clearTimeout(timer);
472
+ ws.close();
473
+ resolve(true);
474
+ });
475
+ ws.on("error", () => {
476
+ clearTimeout(timer);
477
+ resolve(false);
478
+ });
479
+ });
480
+ }
481
+ var DominusClient;
482
+ var init_ws_client = __esm({
483
+ "src/transport/ws-client.ts"() {
484
+ init_protocol();
485
+ DominusClient = class extends EventEmitter {
486
+ ws = null;
487
+ port;
488
+ studioConnected = false;
489
+ connectionInfo = null;
490
+ pendingRequests = /* @__PURE__ */ new Map();
491
+ constructor(port = 18088) {
492
+ super();
493
+ this.port = port;
494
+ }
495
+ connect() {
496
+ return new Promise((resolve, reject) => {
497
+ const url = `ws://localhost:${this.port}`;
498
+ const ws = new WebSocket(url);
499
+ ws.on("open", () => {
500
+ this.ws = ws;
501
+ ws.send(serializeMessage(createMessage("controller:connect", {})));
502
+ this.emit("server:started", { port: this.port, relay: true });
503
+ resolve();
504
+ });
505
+ ws.on("message", (raw) => {
506
+ try {
507
+ const msg = parseMessage(raw.toString());
508
+ this.handleMessage(msg);
509
+ } catch (err) {
510
+ this.emit("server:error", err);
511
+ }
512
+ });
513
+ ws.on("close", () => {
514
+ this.ws = null;
515
+ this.studioConnected = false;
516
+ this.emit("studio:disconnected");
517
+ for (const [id, pending] of this.pendingRequests) {
518
+ clearTimeout(pending.timer);
519
+ pending.reject(new Error("Relay connection lost"));
520
+ this.pendingRequests.delete(id);
521
+ }
522
+ });
523
+ ws.on("error", (err) => {
524
+ if (!this.ws) reject(err);
525
+ this.emit("server:error", err);
526
+ });
527
+ });
528
+ }
529
+ handleMessage(msg) {
530
+ if (msg.type === "controller:welcome") {
531
+ const p = msg.payload;
532
+ this.studioConnected = p.studioConnected;
533
+ if (this.studioConnected) {
534
+ this.emit("studio:connected", {});
535
+ }
536
+ return;
537
+ }
538
+ if (msg.type === "studio:connected") {
539
+ this.studioConnected = true;
540
+ const payload = msg.payload;
541
+ this.connectionInfo = {
542
+ connectedAt: Date.now(),
543
+ studioVersion: payload.studioVersion,
544
+ placeId: payload.placeId,
545
+ placeName: payload.placeName
546
+ };
547
+ this.emit("studio:connected", payload);
548
+ return;
549
+ }
550
+ if (msg.type === "studio:disconnected") {
551
+ this.studioConnected = false;
552
+ this.connectionInfo = null;
553
+ this.emit("studio:disconnected");
554
+ return;
555
+ }
556
+ if (msg.type === StudioMsg.RESPONSE) {
557
+ const pending = this.pendingRequests.get(msg.id);
558
+ if (pending) {
559
+ clearTimeout(pending.timer);
560
+ pending.resolve(msg);
561
+ this.pendingRequests.delete(msg.id);
562
+ }
563
+ return;
564
+ }
565
+ this.emit(msg.type, msg.payload);
566
+ this.emit("message", msg);
567
+ }
568
+ sendRequest(type, payload, timeoutMs = 1e4) {
569
+ return new Promise((resolve, reject) => {
570
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
571
+ reject(new Error("Not connected to Dominus server"));
572
+ return;
573
+ }
574
+ if (!this.studioConnected) {
575
+ reject(new Error("Studio not connected"));
576
+ return;
577
+ }
578
+ const msg = createMessage(type, payload);
579
+ const timer = setTimeout(() => {
580
+ this.pendingRequests.delete(msg.id);
581
+ reject(new Error(`Request timed out: ${type}`));
582
+ }, timeoutMs);
583
+ this.pendingRequests.set(msg.id, {
584
+ resolve,
585
+ reject,
586
+ timer
587
+ });
588
+ this.ws.send(serializeMessage(msg));
589
+ });
590
+ }
591
+ sendNotification(type, payload) {
592
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
593
+ const msg = createMessage(type, payload);
594
+ this.ws.send(serializeMessage(msg));
595
+ }
596
+ isConnected() {
597
+ return this.studioConnected;
598
+ }
599
+ getConnectionInfo() {
600
+ if (!this.connectionInfo) return null;
601
+ return { ...this.connectionInfo, ws: this.ws };
602
+ }
603
+ getPort() {
604
+ return this.port;
605
+ }
606
+ stop() {
607
+ return new Promise((resolve) => {
608
+ for (const [, pending] of this.pendingRequests) {
609
+ clearTimeout(pending.timer);
610
+ pending.reject(new Error("Client shutting down"));
611
+ }
612
+ this.pendingRequests.clear();
613
+ if (this.ws) {
614
+ this.ws.close();
615
+ this.ws = null;
616
+ }
617
+ resolve();
618
+ });
619
+ }
620
+ };
621
+ }
622
+ });
462
623
 
463
624
  // src/memory/store.ts
464
625
  var store_exports = {};
@@ -481,14 +642,14 @@ __export(store_exports, {
481
642
  function persist() {
482
643
  if (!db) return;
483
644
  const data = db.export();
484
- fs4.writeFileSync(dbPath, Buffer.from(data));
645
+ fs5.writeFileSync(dbPath, Buffer.from(data));
485
646
  }
486
647
  async function initMemoryStore() {
487
648
  if (db) return;
488
649
  dbPath = getDbPath();
489
650
  const SQL = await initSqlJs();
490
- if (fs4.existsSync(dbPath)) {
491
- const buffer = fs4.readFileSync(dbPath);
651
+ if (fs5.existsSync(dbPath)) {
652
+ const buffer = fs5.readFileSync(dbPath);
492
653
  db = new SQL.Database(buffer);
493
654
  } else {
494
655
  db = new SQL.Database();
@@ -779,10 +940,10 @@ var init_read_script = __esm({
779
940
  if (!ctx.isStudioConnected()) {
780
941
  return { success: false, error: "Studio is not connected" };
781
942
  }
782
- const path4 = params.path;
783
- const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path4 });
943
+ const path5 = params.path;
944
+ const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path5 });
784
945
  if (!response.payload) {
785
- return { success: false, error: `Script not found: ${path4}` };
946
+ return { success: false, error: `Script not found: ${path5}` };
786
947
  }
787
948
  return {
788
949
  success: true,
@@ -828,14 +989,14 @@ var init_edit_script = __esm({
828
989
  if (!ctx.isStudioConnected()) {
829
990
  return { success: false, error: "Studio is not connected" };
830
991
  }
831
- const path4 = params.path;
992
+ const path5 = params.path;
832
993
  const source = params.source;
833
- const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path4, source });
994
+ const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path5, source });
834
995
  const payload = response.payload;
835
996
  if (!payload.success) {
836
997
  return { success: false, error: payload.error ?? "Failed to edit script" };
837
998
  }
838
- return { success: true, data: { path: path4, linesWritten: source.split("\n").length } };
999
+ return { success: true, data: { path: path5, linesWritten: source.split("\n").length } };
839
1000
  }
840
1001
  };
841
1002
  }
@@ -1521,8 +1682,8 @@ __export(clone_instance_exports, {
1521
1682
  tool: () => tool18
1522
1683
  });
1523
1684
  function buildCloneCode(params) {
1524
- const path4 = params.path;
1525
- const parts = path4.split(".");
1685
+ const path5 = params.path;
1686
+ const parts = path5.split(".");
1526
1687
  let resolve = "game";
1527
1688
  for (const p of parts) {
1528
1689
  resolve += `["${p}"]`;
@@ -1663,8 +1824,8 @@ __export(build_multiple_exports, {
1663
1824
  });
1664
1825
  function generateBatchCode(parts, groupName, groupParent) {
1665
1826
  const lines = [];
1666
- function resolveParent(path4) {
1667
- const segs = path4.split(".");
1827
+ function resolveParent(path5) {
1828
+ const segs = path5.split(".");
1668
1829
  let r = "game";
1669
1830
  for (const s of segs) r += `["${s}"]`;
1670
1831
  return r;
@@ -1928,11 +2089,11 @@ var init_upload_asset = __esm({
1928
2089
  const assetType = params.assetType;
1929
2090
  const name = params.name;
1930
2091
  const description = params.description ?? "";
1931
- if (!fs4.existsSync(filePath)) {
2092
+ if (!fs5.existsSync(filePath)) {
1932
2093
  return { success: false, error: `File not found: ${filePath}` };
1933
2094
  }
1934
- const fileBuffer = fs4.readFileSync(filePath);
1935
- const fileName = path3.basename(filePath);
2095
+ const fileBuffer = fs5.readFileSync(filePath);
2096
+ const fileName = path4.basename(filePath);
1936
2097
  const typeMapping = {
1937
2098
  Image: "Decal",
1938
2099
  Audio: "Audio",
@@ -1994,19 +2155,27 @@ __export(server_exports, {
1994
2155
  async function startMcpServer() {
1995
2156
  const config = loadConfig();
1996
2157
  await initMemoryStore();
1997
- wsServer = new DominusServer(config.port);
1998
- await wsServer.start();
2158
+ const portTaken = await isPortInUse(config.port);
2159
+ if (portTaken) {
2160
+ const client = new DominusClient(config.port);
2161
+ await client.connect();
2162
+ bridge = client;
2163
+ } else {
2164
+ const server = new DominusServer(config.port);
2165
+ await server.start();
2166
+ bridge = server;
2167
+ }
1999
2168
  const mcp = new McpServer({
2000
2169
  name: "dominus",
2001
- version: "0.1.0"
2170
+ version: "0.2.1"
2002
2171
  });
2003
2172
  mcp.tool(
2004
2173
  "read_script",
2005
2174
  "Read the source code of a script in Roblox Studio",
2006
2175
  { path: z.string().describe('Full instance path, e.g. "ServerScriptService.GameManager"') },
2007
- async ({ path: path4 }) => {
2176
+ async ({ path: path5 }) => {
2008
2177
  assertConnected();
2009
- const res = await wsServer.sendRequest(CliMsg.GET_SCRIPT, { path: path4 });
2178
+ const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path5 });
2010
2179
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2011
2180
  }
2012
2181
  );
@@ -2017,9 +2186,9 @@ async function startMcpServer() {
2017
2186
  path: z.string().describe("Full instance path"),
2018
2187
  source: z.string().describe("New complete source code")
2019
2188
  },
2020
- async ({ path: path4, source }) => {
2189
+ async ({ path: path5, source }) => {
2021
2190
  assertConnected();
2022
- const res = await wsServer.sendRequest(CliMsg.SET_SCRIPT, { path: path4, source });
2191
+ const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path5, source });
2023
2192
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2024
2193
  }
2025
2194
  );
@@ -2029,7 +2198,7 @@ async function startMcpServer() {
2029
2198
  { code: z.string().describe("Luau code to execute") },
2030
2199
  async ({ code }) => {
2031
2200
  assertConnected();
2032
- const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code });
2201
+ const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
2033
2202
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2034
2203
  }
2035
2204
  );
@@ -2042,7 +2211,7 @@ async function startMcpServer() {
2042
2211
  },
2043
2212
  async ({ rootPath, maxDepth }) => {
2044
2213
  assertConnected();
2045
- const res = await wsServer.sendRequest(CliMsg.GET_EXPLORER, {
2214
+ const res = await bridge.sendRequest(CliMsg.GET_EXPLORER, {
2046
2215
  rootPath: rootPath ?? null,
2047
2216
  maxDepth
2048
2217
  });
@@ -2053,9 +2222,9 @@ async function startMcpServer() {
2053
2222
  "get_properties",
2054
2223
  "Get all properties of an instance in Roblox Studio",
2055
2224
  { path: z.string().describe("Full instance path") },
2056
- async ({ path: path4 }) => {
2225
+ async ({ path: path5 }) => {
2057
2226
  assertConnected();
2058
- const res = await wsServer.sendRequest(CliMsg.GET_PROPERTIES, { path: path4 });
2227
+ const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path5 });
2059
2228
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2060
2229
  }
2061
2230
  );
@@ -2066,9 +2235,9 @@ async function startMcpServer() {
2066
2235
  path: z.string().describe("Full instance path"),
2067
2236
  properties: z.record(z.unknown()).describe("Key-value pairs of properties to set")
2068
2237
  },
2069
- async ({ path: path4, properties }) => {
2238
+ async ({ path: path5, properties }) => {
2070
2239
  assertConnected();
2071
- const res = await wsServer.sendRequest(CliMsg.SET_PROPERTIES, { path: path4, properties });
2240
+ const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path5, properties });
2072
2241
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2073
2242
  }
2074
2243
  );
@@ -2083,7 +2252,7 @@ async function startMcpServer() {
2083
2252
  },
2084
2253
  async ({ className, parentPath, name, properties }) => {
2085
2254
  assertConnected();
2086
- const res = await wsServer.sendRequest(CliMsg.INSERT_INSTANCE, {
2255
+ const res = await bridge.sendRequest(CliMsg.INSERT_INSTANCE, {
2087
2256
  className,
2088
2257
  parentPath,
2089
2258
  name,
@@ -2096,9 +2265,9 @@ async function startMcpServer() {
2096
2265
  "delete_instance",
2097
2266
  "Delete an instance from the Roblox Studio DataModel",
2098
2267
  { path: z.string().describe("Full instance path to delete") },
2099
- async ({ path: path4 }) => {
2268
+ async ({ path: path5 }) => {
2100
2269
  assertConnected();
2101
- const res = await wsServer.sendRequest(CliMsg.DELETE_INSTANCE, { path: path4 });
2270
+ const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path5 });
2102
2271
  return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
2103
2272
  }
2104
2273
  );
@@ -2108,7 +2277,7 @@ async function startMcpServer() {
2108
2277
  {},
2109
2278
  async () => {
2110
2279
  assertConnected();
2111
- const res = await wsServer.sendRequest(CliMsg.GET_SELECTION, {});
2280
+ const res = await bridge.sendRequest(CliMsg.GET_SELECTION, {});
2112
2281
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2113
2282
  }
2114
2283
  );
@@ -2121,7 +2290,7 @@ async function startMcpServer() {
2121
2290
  },
2122
2291
  async ({ query, maxResults }) => {
2123
2292
  assertConnected();
2124
- const res = await wsServer.sendRequest(CliMsg.SEARCH_SCRIPTS, { query, maxResults });
2293
+ const res = await bridge.sendRequest(CliMsg.SEARCH_SCRIPTS, { query, maxResults });
2125
2294
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2126
2295
  }
2127
2296
  );
@@ -2134,7 +2303,7 @@ async function startMcpServer() {
2134
2303
  },
2135
2304
  async ({ limit, level }) => {
2136
2305
  assertConnected();
2137
- const res = await wsServer.sendRequest(CliMsg.GET_OUTPUT, {
2306
+ const res = await bridge.sendRequest(CliMsg.GET_OUTPUT, {
2138
2307
  limit,
2139
2308
  level: level ?? null
2140
2309
  });
@@ -2147,7 +2316,7 @@ async function startMcpServer() {
2147
2316
  { className: z.string().describe('Roblox class name, e.g. "Frame", "TextLabel", "Part", "MeshPart"') },
2148
2317
  async ({ className }) => {
2149
2318
  assertConnected();
2150
- const res = await wsServer.sendRequest(CliMsg.GET_REFLECTION, { className });
2319
+ const res = await bridge.sendRequest(CliMsg.GET_REFLECTION, { className });
2151
2320
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2152
2321
  }
2153
2322
  );
@@ -2162,7 +2331,7 @@ async function startMcpServer() {
2162
2331
  },
2163
2332
  async ({ parent, tree }) => {
2164
2333
  assertConnected();
2165
- const res = await wsServer.sendRequest(
2334
+ const res = await bridge.sendRequest(
2166
2335
  CliMsg.BUILD_UI,
2167
2336
  { parent: parent ?? "StarterGui", tree }
2168
2337
  );
@@ -2175,7 +2344,7 @@ async function startMcpServer() {
2175
2344
  { testName: z.string().optional().describe("Specific test name, or omit to run all") },
2176
2345
  async ({ testName }) => {
2177
2346
  assertConnected();
2178
- const res = await wsServer.sendRequest(CliMsg.RUN_TESTS, {
2347
+ const res = await bridge.sendRequest(CliMsg.RUN_TESTS, {
2179
2348
  testName: testName ?? null
2180
2349
  });
2181
2350
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
@@ -2187,7 +2356,7 @@ async function startMcpServer() {
2187
2356
  { args: z.unknown().optional().describe("Arguments passed to the test session. Server scripts get this via StudioTestService:GetTestArgs()") },
2188
2357
  async ({ args }) => {
2189
2358
  assertConnected();
2190
- const res = await wsServer.sendRequest(CliMsg.EXECUTE_RUN_TEST, { args: args ?? null }, 12e4);
2359
+ const res = await bridge.sendRequest(CliMsg.EXECUTE_RUN_TEST, { args: args ?? null }, 12e4);
2191
2360
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2192
2361
  }
2193
2362
  );
@@ -2197,7 +2366,7 @@ async function startMcpServer() {
2197
2366
  { args: z.unknown().optional().describe("Arguments passed to the test session. Server scripts get this via StudioTestService:GetTestArgs()") },
2198
2367
  async ({ args }) => {
2199
2368
  assertConnected();
2200
- const res = await wsServer.sendRequest(CliMsg.EXECUTE_PLAY_TEST, { args: args ?? null }, 12e4);
2369
+ const res = await bridge.sendRequest(CliMsg.EXECUTE_PLAY_TEST, { args: args ?? null }, 12e4);
2201
2370
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2202
2371
  }
2203
2372
  );
@@ -2236,7 +2405,7 @@ async function startMcpServer() {
2236
2405
  if (params.canCollide !== void 0) properties.CanCollide = params.canCollide;
2237
2406
  if (params.shape && params.shape !== "Block") properties.Shape = params.shape;
2238
2407
  const className = params.shape === "Wedge" ? "WedgePart" : params.shape === "CornerWedge" ? "CornerWedgePart" : "Part";
2239
- const res = await wsServer.sendRequest(CliMsg.INSERT_INSTANCE, {
2408
+ const res = await bridge.sendRequest(CliMsg.INSERT_INSTANCE, {
2240
2409
  className,
2241
2410
  parentPath: params.parent ?? "Workspace",
2242
2411
  name: params.name,
@@ -2275,7 +2444,7 @@ async function startMcpServer() {
2275
2444
  lines.push(`end`);
2276
2445
  }
2277
2446
  lines.push(`print("Cloned: " .. clone:GetFullName())`);
2278
- const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
2447
+ const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
2279
2448
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2280
2449
  }
2281
2450
  );
@@ -2295,8 +2464,8 @@ async function startMcpServer() {
2295
2464
  const parentParts = parent.split(".");
2296
2465
  let parentResolve = "game";
2297
2466
  for (const p of parentParts) parentResolve += `["${p}"]`;
2298
- const resolves = params.paths.map((path4) => {
2299
- const parts = path4.split(".");
2467
+ const resolves = params.paths.map((path5) => {
2468
+ const parts = path5.split(".");
2300
2469
  let r = "game";
2301
2470
  for (const part of parts) r += `["${part}"]`;
2302
2471
  return r;
@@ -2308,7 +2477,7 @@ async function startMcpServer() {
2308
2477
  ...resolves.map((r) => `${r}.Parent = group`),
2309
2478
  `print("Grouped ${params.paths.length} instances into: " .. group:GetFullName())`
2310
2479
  ].join("\n");
2311
- const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code });
2480
+ const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code });
2312
2481
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2313
2482
  }
2314
2483
  );
@@ -2336,8 +2505,8 @@ async function startMcpServer() {
2336
2505
  const parts = params.parts;
2337
2506
  const groupName = params.groupName;
2338
2507
  const groupParent = params.groupParent ?? "Workspace";
2339
- function resolveP(path4) {
2340
- const segs = path4.split(".");
2508
+ function resolveP(path5) {
2509
+ const segs = path5.split(".");
2341
2510
  let r = "game";
2342
2511
  for (const s of segs) r += `["${s}"]`;
2343
2512
  return r;
@@ -2367,7 +2536,7 @@ async function startMcpServer() {
2367
2536
  lines.push(`${v}.Parent = ${groupName ? "_group" : resolveP(p.parent ?? "Workspace")}`);
2368
2537
  }
2369
2538
  lines.push(`print("Built ${parts.length} parts${groupName ? ` in ${groupName}` : ""}")`);
2370
- const res = await wsServer.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
2539
+ const res = await bridge.sendRequest(CliMsg.RUN_CODE, { code: lines.join("\n") });
2371
2540
  return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
2372
2541
  }
2373
2542
  );
@@ -2403,16 +2572,16 @@ async function startMcpServer() {
2403
2572
  "studio://status",
2404
2573
  "studio://status",
2405
2574
  async () => {
2406
- const info = wsServer.getConnectionInfo();
2575
+ const info = bridge.getConnectionInfo();
2407
2576
  return {
2408
2577
  contents: [{
2409
2578
  uri: "studio://status",
2410
2579
  text: JSON.stringify({
2411
- connected: wsServer.isConnected(),
2580
+ connected: bridge.isConnected(),
2412
2581
  placeName: info?.placeName,
2413
2582
  placeId: info?.placeId,
2414
2583
  studioVersion: info?.studioVersion,
2415
- port: wsServer.getPort()
2584
+ port: bridge.getPort()
2416
2585
  }, null, 2),
2417
2586
  mimeType: "application/json"
2418
2587
  }]
@@ -2423,16 +2592,17 @@ async function startMcpServer() {
2423
2592
  await mcp.connect(transport);
2424
2593
  }
2425
2594
  function assertConnected() {
2426
- if (!wsServer.isConnected()) {
2595
+ if (!bridge.isConnected()) {
2427
2596
  throw new Error(
2428
2597
  "Roblox Studio is not connected. Open Studio and click the Dominus plugin Connect button."
2429
2598
  );
2430
2599
  }
2431
2600
  }
2432
- var wsServer;
2601
+ var bridge;
2433
2602
  var init_server = __esm({
2434
2603
  "src/mcp/server.ts"() {
2435
2604
  init_ws_server();
2605
+ init_ws_client();
2436
2606
  init_protocol();
2437
2607
  init_store();
2438
2608
  init_config();
@@ -3475,163 +3645,7 @@ var App = ({ server, agent: initialAgent, version }) => {
3475
3645
 
3476
3646
  // src/index.tsx
3477
3647
  init_ws_server();
3478
-
3479
- // src/transport/ws-client.ts
3480
- init_protocol();
3481
- var DominusClient = class extends EventEmitter {
3482
- ws = null;
3483
- port;
3484
- studioConnected = false;
3485
- connectionInfo = null;
3486
- pendingRequests = /* @__PURE__ */ new Map();
3487
- constructor(port = 18088) {
3488
- super();
3489
- this.port = port;
3490
- }
3491
- connect() {
3492
- return new Promise((resolve, reject) => {
3493
- const url = `ws://localhost:${this.port}`;
3494
- const ws = new WebSocket(url);
3495
- ws.on("open", () => {
3496
- this.ws = ws;
3497
- ws.send(serializeMessage(createMessage("controller:connect", {})));
3498
- this.emit("server:started", { port: this.port, relay: true });
3499
- resolve();
3500
- });
3501
- ws.on("message", (raw) => {
3502
- try {
3503
- const msg = parseMessage(raw.toString());
3504
- this.handleMessage(msg);
3505
- } catch (err) {
3506
- this.emit("server:error", err);
3507
- }
3508
- });
3509
- ws.on("close", () => {
3510
- this.ws = null;
3511
- this.studioConnected = false;
3512
- this.emit("studio:disconnected");
3513
- for (const [id, pending] of this.pendingRequests) {
3514
- clearTimeout(pending.timer);
3515
- pending.reject(new Error("Relay connection lost"));
3516
- this.pendingRequests.delete(id);
3517
- }
3518
- });
3519
- ws.on("error", (err) => {
3520
- if (!this.ws) reject(err);
3521
- this.emit("server:error", err);
3522
- });
3523
- });
3524
- }
3525
- handleMessage(msg) {
3526
- if (msg.type === "controller:welcome") {
3527
- const p = msg.payload;
3528
- this.studioConnected = p.studioConnected;
3529
- if (this.studioConnected) {
3530
- this.emit("studio:connected", {});
3531
- }
3532
- return;
3533
- }
3534
- if (msg.type === "studio:connected") {
3535
- this.studioConnected = true;
3536
- const payload = msg.payload;
3537
- this.connectionInfo = {
3538
- connectedAt: Date.now(),
3539
- studioVersion: payload.studioVersion,
3540
- placeId: payload.placeId,
3541
- placeName: payload.placeName
3542
- };
3543
- this.emit("studio:connected", payload);
3544
- return;
3545
- }
3546
- if (msg.type === "studio:disconnected") {
3547
- this.studioConnected = false;
3548
- this.connectionInfo = null;
3549
- this.emit("studio:disconnected");
3550
- return;
3551
- }
3552
- if (msg.type === StudioMsg.RESPONSE) {
3553
- const pending = this.pendingRequests.get(msg.id);
3554
- if (pending) {
3555
- clearTimeout(pending.timer);
3556
- pending.resolve(msg);
3557
- this.pendingRequests.delete(msg.id);
3558
- }
3559
- return;
3560
- }
3561
- this.emit(msg.type, msg.payload);
3562
- this.emit("message", msg);
3563
- }
3564
- sendRequest(type, payload, timeoutMs = 1e4) {
3565
- return new Promise((resolve, reject) => {
3566
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
3567
- reject(new Error("Not connected to Dominus server"));
3568
- return;
3569
- }
3570
- if (!this.studioConnected) {
3571
- reject(new Error("Studio not connected"));
3572
- return;
3573
- }
3574
- const msg = createMessage(type, payload);
3575
- const timer = setTimeout(() => {
3576
- this.pendingRequests.delete(msg.id);
3577
- reject(new Error(`Request timed out: ${type}`));
3578
- }, timeoutMs);
3579
- this.pendingRequests.set(msg.id, {
3580
- resolve,
3581
- reject,
3582
- timer
3583
- });
3584
- this.ws.send(serializeMessage(msg));
3585
- });
3586
- }
3587
- sendNotification(type, payload) {
3588
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
3589
- const msg = createMessage(type, payload);
3590
- this.ws.send(serializeMessage(msg));
3591
- }
3592
- isConnected() {
3593
- return this.studioConnected;
3594
- }
3595
- getConnectionInfo() {
3596
- if (!this.connectionInfo) return null;
3597
- return { ...this.connectionInfo, ws: this.ws };
3598
- }
3599
- getPort() {
3600
- return this.port;
3601
- }
3602
- stop() {
3603
- return new Promise((resolve) => {
3604
- for (const [, pending] of this.pendingRequests) {
3605
- clearTimeout(pending.timer);
3606
- pending.reject(new Error("Client shutting down"));
3607
- }
3608
- this.pendingRequests.clear();
3609
- if (this.ws) {
3610
- this.ws.close();
3611
- this.ws = null;
3612
- }
3613
- resolve();
3614
- });
3615
- }
3616
- };
3617
- async function isPortInUse(port) {
3618
- return new Promise((resolve) => {
3619
- const ws = new WebSocket(`ws://localhost:${port}`);
3620
- const timer = setTimeout(() => {
3621
- ws.close();
3622
- resolve(false);
3623
- }, 1e3);
3624
- ws.on("open", () => {
3625
- clearTimeout(timer);
3626
- ws.close();
3627
- resolve(true);
3628
- });
3629
- ws.on("error", () => {
3630
- clearTimeout(timer);
3631
- resolve(false);
3632
- });
3633
- });
3634
- }
3648
+ init_ws_client();
3635
3649
 
3636
3650
  // src/ai/provider.ts
3637
3651
  init_config();
@@ -4293,7 +4307,98 @@ function createDefaultRegistry() {
4293
4307
  // src/index.tsx
4294
4308
  init_store();
4295
4309
  init_config();
4296
- var VERSION = "0.1.0";
4310
+ var CACHE_DIR = path4.join(os3.homedir(), ".dominus");
4311
+ var CACHE_FILE = path4.join(CACHE_DIR, "update-check.json");
4312
+ var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
4313
+ var NPM_PACKAGE = "dominus-cli";
4314
+ function readCache() {
4315
+ try {
4316
+ if (fs5.existsSync(CACHE_FILE)) {
4317
+ return JSON.parse(fs5.readFileSync(CACHE_FILE, "utf-8"));
4318
+ }
4319
+ } catch {
4320
+ }
4321
+ return { lastCheck: 0, latestVersion: null };
4322
+ }
4323
+ function writeCache(cache) {
4324
+ try {
4325
+ if (!fs5.existsSync(CACHE_DIR)) fs5.mkdirSync(CACHE_DIR, { recursive: true });
4326
+ fs5.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf-8");
4327
+ } catch {
4328
+ }
4329
+ }
4330
+ function fetchLatestVersion() {
4331
+ return new Promise((resolve) => {
4332
+ const req = https.get(
4333
+ `https://registry.npmjs.org/${NPM_PACKAGE}/latest`,
4334
+ { headers: { Accept: "application/json" }, timeout: 5e3 },
4335
+ (res) => {
4336
+ if (res.statusCode !== 200) {
4337
+ resolve(null);
4338
+ res.resume();
4339
+ return;
4340
+ }
4341
+ let body = "";
4342
+ res.on("data", (c) => body += c);
4343
+ res.on("end", () => {
4344
+ try {
4345
+ resolve(JSON.parse(body).version ?? null);
4346
+ } catch {
4347
+ resolve(null);
4348
+ }
4349
+ });
4350
+ }
4351
+ );
4352
+ req.on("error", () => resolve(null));
4353
+ req.on("timeout", () => {
4354
+ req.destroy();
4355
+ resolve(null);
4356
+ });
4357
+ });
4358
+ }
4359
+ function compareVersions(current, latest) {
4360
+ const a = current.split(".").map(Number);
4361
+ const b = latest.split(".").map(Number);
4362
+ for (let i = 0; i < 3; i++) {
4363
+ if ((b[i] ?? 0) > (a[i] ?? 0)) return 1;
4364
+ if ((b[i] ?? 0) < (a[i] ?? 0)) return -1;
4365
+ }
4366
+ return 0;
4367
+ }
4368
+ async function checkForUpdate(currentVersion) {
4369
+ try {
4370
+ const cache = readCache();
4371
+ const now = Date.now();
4372
+ if (now - cache.lastCheck < CHECK_INTERVAL_MS && cache.latestVersion) {
4373
+ if (compareVersions(currentVersion, cache.latestVersion) > 0) {
4374
+ return { updateAvailable: true, currentVersion, latestVersion: cache.latestVersion };
4375
+ }
4376
+ return null;
4377
+ }
4378
+ const latest = await fetchLatestVersion();
4379
+ writeCache({ lastCheck: now, latestVersion: latest });
4380
+ if (latest && compareVersions(currentVersion, latest) > 0) {
4381
+ return { updateAvailable: true, currentVersion, latestVersion: latest };
4382
+ }
4383
+ } catch {
4384
+ }
4385
+ return null;
4386
+ }
4387
+ function formatUpdateMessage(info) {
4388
+ const line1 = `Update available: ${info.currentVersion} \u2192 ${info.latestVersion}`;
4389
+ const line2 = `Run: npm i -g ${NPM_PACKAGE}`;
4390
+ const width = Math.max(line1.length, line2.length) + 4;
4391
+ const pad = (s) => ` \u2502 ${s}${" ".repeat(width - s.length - 4)} \u2502`;
4392
+ return [
4393
+ "",
4394
+ ` \u256D${"\u2500".repeat(width)}\u256E`,
4395
+ pad(line1),
4396
+ pad(line2),
4397
+ ` \u2570${"\u2500".repeat(width)}\u256F`,
4398
+ ""
4399
+ ].join("\n");
4400
+ }
4401
+ var VERSION = "0.2.1";
4297
4402
  var program = new Command();
4298
4403
  program.name("dominus").description("AI-powered autonomous agent for Roblox Studio development").version(VERSION);
4299
4404
  program.command("start", { isDefault: true }).description("Launch the Dominus TUI agent").option("-p, --port <port>", "WebSocket server port", "18088").action(async (opts) => {
@@ -4313,6 +4418,10 @@ program.command("start", { isDefault: true }).description("Launch the Dominus TU
4313
4418
  setConfigValue("userName", firstName);
4314
4419
  }
4315
4420
  }
4421
+ checkForUpdate(VERSION).then((info) => {
4422
+ if (info) process.stderr.write(formatUpdateMessage(info));
4423
+ }).catch(() => {
4424
+ });
4316
4425
  await launchApp(port);
4317
4426
  });
4318
4427
  var configCmd = program.command("config").description("Manage Dominus configuration");
@@ -4403,24 +4512,24 @@ rulesCmd.command("list").description("List all rules").action(() => {
4403
4512
  });
4404
4513
  program.command("install-plugin").description("Build & install the Dominus plugin into Roblox Studio").action(async () => {
4405
4514
  const { execSync: execSync2 } = await import('child_process');
4406
- const packageRoot = path3.resolve(import.meta.dirname ?? __dirname, "..");
4407
- const pluginDir = path3.join(packageRoot, "plugin");
4408
- const pluginProject = path3.join(pluginDir, "default.project.json");
4409
- const studioPlugins = path3.join(
4410
- os2.homedir(),
4515
+ const packageRoot = path4.resolve(import.meta.dirname ?? __dirname, "..");
4516
+ const pluginDir = path4.join(packageRoot, "plugin");
4517
+ const pluginProject = path4.join(pluginDir, "default.project.json");
4518
+ const studioPlugins = path4.join(
4519
+ os3.homedir(),
4411
4520
  "AppData",
4412
4521
  "Local",
4413
4522
  "Roblox",
4414
4523
  "Plugins"
4415
4524
  );
4416
- const destFile = path3.join(studioPlugins, "Dominus.rbxm");
4417
- if (!fs4.existsSync(pluginProject)) {
4525
+ const destFile = path4.join(studioPlugins, "Dominus.rbxm");
4526
+ if (!fs5.existsSync(pluginProject)) {
4418
4527
  console.log(" \u2716 Plugin source not found.");
4419
4528
  console.log(` Expected at: ${pluginDir}`);
4420
4529
  return;
4421
4530
  }
4422
- if (!fs4.existsSync(studioPlugins)) {
4423
- fs4.mkdirSync(studioPlugins, { recursive: true });
4531
+ if (!fs5.existsSync(studioPlugins)) {
4532
+ fs5.mkdirSync(studioPlugins, { recursive: true });
4424
4533
  }
4425
4534
  let rojoAvailable = false;
4426
4535
  try {
@@ -4447,49 +4556,49 @@ program.command("mcp").description("Start the Dominus MCP server (for Cursor, Cl
4447
4556
  await startMcpServer2();
4448
4557
  });
4449
4558
  function getMcpTargets() {
4450
- const home = os2.homedir();
4451
- const appData = process.env.APPDATA || path3.join(home, "AppData", "Roaming");
4452
- const localAppData = process.env.LOCALAPPDATA || path3.join(home, "AppData", "Local");
4559
+ const home = os3.homedir();
4560
+ const appData = process.env.APPDATA || path4.join(home, "AppData", "Roaming");
4561
+ const localAppData = process.env.LOCALAPPDATA || path4.join(home, "AppData", "Local");
4453
4562
  const cwd = process.cwd();
4454
4563
  return [
4455
4564
  {
4456
4565
  name: "Cursor (project)",
4457
- configPath: path3.join(cwd, ".cursor", "mcp.json"),
4566
+ configPath: path4.join(cwd, ".cursor", "mcp.json"),
4458
4567
  keyPath: ["mcpServers"]
4459
4568
  },
4460
4569
  {
4461
4570
  name: "Cursor (global)",
4462
- configPath: path3.join(home, ".cursor", "mcp.json"),
4571
+ configPath: path4.join(home, ".cursor", "mcp.json"),
4463
4572
  keyPath: ["mcpServers"]
4464
4573
  },
4465
4574
  {
4466
4575
  name: "Claude Desktop",
4467
- configPath: path3.join(appData, "Claude", "claude_desktop_config.json"),
4576
+ configPath: path4.join(appData, "Claude", "claude_desktop_config.json"),
4468
4577
  keyPath: ["mcpServers"]
4469
4578
  },
4470
4579
  {
4471
4580
  name: "Claude Code",
4472
- configPath: path3.join(home, ".claude.json"),
4581
+ configPath: path4.join(home, ".claude.json"),
4473
4582
  keyPath: ["mcpServers"]
4474
4583
  },
4475
4584
  {
4476
4585
  name: "Windsurf (project)",
4477
- configPath: path3.join(cwd, ".windsurf", "mcp.json"),
4586
+ configPath: path4.join(cwd, ".windsurf", "mcp.json"),
4478
4587
  keyPath: ["mcpServers"]
4479
4588
  },
4480
4589
  {
4481
4590
  name: "Windsurf (global)",
4482
- configPath: path3.join(home, ".codeium", "windsurf", "mcp_config.json"),
4591
+ configPath: path4.join(home, ".codeium", "windsurf", "mcp_config.json"),
4483
4592
  keyPath: ["mcpServers"]
4484
4593
  },
4485
4594
  {
4486
4595
  name: "VS Code (project)",
4487
- configPath: path3.join(cwd, ".vscode", "mcp.json"),
4596
+ configPath: path4.join(cwd, ".vscode", "mcp.json"),
4488
4597
  keyPath: ["servers"]
4489
4598
  },
4490
4599
  {
4491
4600
  name: "VS Code (global)",
4492
- configPath: path3.join(localAppData, "Code", "User", "settings.json"),
4601
+ configPath: path4.join(localAppData, "Code", "User", "settings.json"),
4493
4602
  keyPath: ["mcp", "servers"]
4494
4603
  }
4495
4604
  ];
@@ -4501,9 +4610,9 @@ var dominusEntry = {
4501
4610
  };
4502
4611
  function injectMcpConfig(target) {
4503
4612
  let config = {};
4504
- if (fs4.existsSync(target.configPath)) {
4613
+ if (fs5.existsSync(target.configPath)) {
4505
4614
  try {
4506
- config = JSON.parse(fs4.readFileSync(target.configPath, "utf-8"));
4615
+ config = JSON.parse(fs5.readFileSync(target.configPath, "utf-8"));
4507
4616
  } catch {
4508
4617
  config = {};
4509
4618
  }
@@ -4519,12 +4628,12 @@ function injectMcpConfig(target) {
4519
4628
  return { status: "already" };
4520
4629
  }
4521
4630
  obj["dominus"] = { ...dominusEntry };
4522
- const dir = path3.dirname(target.configPath);
4523
- if (!fs4.existsSync(dir)) {
4524
- fs4.mkdirSync(dir, { recursive: true });
4631
+ const dir = path4.dirname(target.configPath);
4632
+ if (!fs5.existsSync(dir)) {
4633
+ fs5.mkdirSync(dir, { recursive: true });
4525
4634
  }
4526
- fs4.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
4527
- return { status: fs4.existsSync(target.configPath) ? "installed" : "created" };
4635
+ fs5.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
4636
+ return { status: fs5.existsSync(target.configPath) ? "installed" : "created" };
4528
4637
  }
4529
4638
  program.command("mcp-install").description("Auto-install Dominus MCP into Cursor, VS Code, Claude Desktop, Windsurf, etc.").option("-a, --all", "Install to all detected editors").action(async (opts) => {
4530
4639
  const targets = getMcpTargets();
@@ -4546,7 +4655,7 @@ program.command("mcp-install").description("Auto-install Dominus MCP into Cursor
4546
4655
  }
4547
4656
  const detected = targets.map((t) => ({
4548
4657
  target: t,
4549
- exists: fs4.existsSync(path3.dirname(t.configPath))
4658
+ exists: fs5.existsSync(path4.dirname(t.configPath))
4550
4659
  }));
4551
4660
  const toInstall = detected.filter((d) => d.exists);
4552
4661
  if (toInstall.length === 0) {
@@ -4619,23 +4728,23 @@ program.command("mcp-setup").description("Print MCP configuration for manual set
4619
4728
  console.log("");
4620
4729
  });
4621
4730
  function buildPluginXml(pluginDir, destFile) {
4622
- const srcDir = path3.join(pluginDir, "src");
4623
- if (!fs4.existsSync(srcDir)) {
4731
+ const srcDir = path4.join(pluginDir, "src");
4732
+ if (!fs5.existsSync(srcDir)) {
4624
4733
  console.log(` \u2716 Plugin source directory not found: ${srcDir}`);
4625
4734
  return;
4626
4735
  }
4627
- const files = fs4.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
4736
+ const files = fs5.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
4628
4737
  const mainFile = files.find((f) => f === "init.server.lua");
4629
4738
  const moduleFiles = files.filter((f) => f !== "init.server.lua");
4630
4739
  if (!mainFile) {
4631
4740
  console.log(" \u2716 init.server.lua not found in plugin source");
4632
4741
  return;
4633
4742
  }
4634
- const mainSource = fs4.readFileSync(path3.join(srcDir, mainFile), "utf-8");
4743
+ const mainSource = fs5.readFileSync(path4.join(srcDir, mainFile), "utf-8");
4635
4744
  let moduleItems = "";
4636
4745
  for (const mf of moduleFiles) {
4637
4746
  const modName = mf.replace(".lua", "");
4638
- const modSource = fs4.readFileSync(path3.join(srcDir, mf), "utf-8");
4747
+ const modSource = fs5.readFileSync(path4.join(srcDir, mf), "utf-8");
4639
4748
  moduleItems += `
4640
4749
  <Item class="ModuleScript" referent="${modName}">
4641
4750
  <Properties>
@@ -4653,7 +4762,7 @@ function buildPluginXml(pluginDir, destFile) {
4653
4762
  </Properties>${moduleItems}
4654
4763
  </Item>
4655
4764
  </roblox>`;
4656
- fs4.writeFileSync(destFile, rbxmx, "utf-8");
4765
+ fs5.writeFileSync(destFile, rbxmx, "utf-8");
4657
4766
  console.log(` \u2714 Plugin installed to: ${destFile}`);
4658
4767
  console.log(" Restart Roblox Studio to load the plugin.");
4659
4768
  }