@chrrxs/robloxstudio-mcp-inspector 2.20.0 → 2.22.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/dist/index.js +2626 -229
- package/package.json +2 -2
- package/studio-plugin/MCPInspectorPlugin.rbxmx +3 -3
- package/studio-plugin/MCPPlugin.rbxmx +3 -3
package/dist/index.js
CHANGED
|
@@ -51,7 +51,26 @@ var init_bridge_service = __esm({
|
|
|
51
51
|
// Keyed by pluginSessionId (the per-plugin GUID).
|
|
52
52
|
instances = /* @__PURE__ */ new Map();
|
|
53
53
|
instanceAliases = /* @__PURE__ */ new Map();
|
|
54
|
+
instanceRegisteredListeners = /* @__PURE__ */ new Set();
|
|
54
55
|
requestTimeout = 3e4;
|
|
56
|
+
onInstanceRegistered(listener) {
|
|
57
|
+
this.instanceRegisteredListeners.add(listener);
|
|
58
|
+
for (const instance of this.getPublicInstances()) {
|
|
59
|
+
try {
|
|
60
|
+
listener(instance);
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return () => this.instanceRegisteredListeners.delete(listener);
|
|
65
|
+
}
|
|
66
|
+
notifyInstanceRegistered(instance) {
|
|
67
|
+
for (const listener of this.instanceRegisteredListeners) {
|
|
68
|
+
try {
|
|
69
|
+
listener(instance);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
55
74
|
canonicalInstanceId(instanceId, placeId) {
|
|
56
75
|
return publishedInstanceId(placeId) ?? instanceId;
|
|
57
76
|
}
|
|
@@ -157,7 +176,7 @@ var init_bridge_service = __esm({
|
|
|
157
176
|
}
|
|
158
177
|
};
|
|
159
178
|
}
|
|
160
|
-
|
|
179
|
+
const registered = {
|
|
161
180
|
pluginSessionId,
|
|
162
181
|
instanceId,
|
|
163
182
|
role: assignedRole,
|
|
@@ -171,7 +190,9 @@ var init_bridge_service = __esm({
|
|
|
171
190
|
versionMismatch,
|
|
172
191
|
lastActivity: Date.now(),
|
|
173
192
|
connectedAt: prior?.connectedAt ?? Date.now()
|
|
174
|
-
}
|
|
193
|
+
};
|
|
194
|
+
this.instances.set(pluginSessionId, registered);
|
|
195
|
+
this.notifyInstanceRegistered(toPublic(registered));
|
|
175
196
|
return { ok: true, assignedRole, instanceId };
|
|
176
197
|
}
|
|
177
198
|
unregisterInstance(pluginSessionId) {
|
|
@@ -369,7 +390,7 @@ var init_bridge_service = __esm({
|
|
|
369
390
|
async sendRequest(endpoint, data, targetInstanceId, targetRole, timeoutMs = this.requestTimeout) {
|
|
370
391
|
const requestId = uuidv4();
|
|
371
392
|
const effectiveTimeoutMs = Math.max(1, timeoutMs);
|
|
372
|
-
return new Promise((
|
|
393
|
+
return new Promise((resolve5, reject) => {
|
|
373
394
|
const timeoutId = setTimeout(() => {
|
|
374
395
|
if (this.pendingRequests.has(requestId)) {
|
|
375
396
|
this.pendingRequests.delete(requestId);
|
|
@@ -384,7 +405,7 @@ var init_bridge_service = __esm({
|
|
|
384
405
|
targetRole,
|
|
385
406
|
timestamp: Date.now(),
|
|
386
407
|
inFlight: false,
|
|
387
|
-
resolve:
|
|
408
|
+
resolve: resolve5,
|
|
388
409
|
reject,
|
|
389
410
|
timeoutId,
|
|
390
411
|
timeoutMs: effectiveTimeoutMs
|
|
@@ -653,9 +674,57 @@ var init_mcp_compat = __esm({
|
|
|
653
674
|
}
|
|
654
675
|
});
|
|
655
676
|
|
|
677
|
+
// ../core/dist/auth.js
|
|
678
|
+
import { randomBytes, createHash, timingSafeEqual } from "crypto";
|
|
679
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
680
|
+
import { join, dirname } from "path";
|
|
681
|
+
import { homedir } from "os";
|
|
682
|
+
function authTokenFilePath() {
|
|
683
|
+
return join(homedir(), ".robloxstudio-mcp", "auth-token");
|
|
684
|
+
}
|
|
685
|
+
function resolveAuthToken() {
|
|
686
|
+
const noAuth = (process.env.ROBLOX_STUDIO_NO_AUTH || "").toLowerCase();
|
|
687
|
+
if (noAuth === "1" || noAuth === "true") {
|
|
688
|
+
return { source: "disabled" };
|
|
689
|
+
}
|
|
690
|
+
const envToken = process.env.ROBLOX_STUDIO_AUTH_TOKEN?.trim();
|
|
691
|
+
if (envToken) {
|
|
692
|
+
return { token: envToken, source: "env" };
|
|
693
|
+
}
|
|
694
|
+
const filePath = authTokenFilePath();
|
|
695
|
+
try {
|
|
696
|
+
if (existsSync(filePath)) {
|
|
697
|
+
const existing = readFileSync(filePath, "utf8").trim();
|
|
698
|
+
if (existing) {
|
|
699
|
+
return { token: existing, source: "file", filePath };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const fresh = randomBytes(32).toString("hex");
|
|
703
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 448 });
|
|
704
|
+
writeFileSync(filePath, fresh + "\n", { encoding: "utf8", mode: 384 });
|
|
705
|
+
try {
|
|
706
|
+
chmodSync(filePath, 384);
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
return { token: fresh, source: "file", filePath };
|
|
710
|
+
} catch (err2) {
|
|
711
|
+
console.error(`[auth] Could not read/create ${filePath} (${err2 instanceof Error ? err2.message : err2}). Using an in-memory token for this process; set ROBLOX_STUDIO_AUTH_TOKEN to share one across sessions.`);
|
|
712
|
+
return { token: randomBytes(32).toString("hex"), source: "file" };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function tokensMatch(provided, expected) {
|
|
716
|
+
const a = createHash("sha256").update(provided).digest();
|
|
717
|
+
const b = createHash("sha256").update(expected).digest();
|
|
718
|
+
return timingSafeEqual(a, b);
|
|
719
|
+
}
|
|
720
|
+
var init_auth = __esm({
|
|
721
|
+
"../core/dist/auth.js"() {
|
|
722
|
+
"use strict";
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
|
|
656
726
|
// ../core/dist/http-server.js
|
|
657
727
|
import express from "express";
|
|
658
|
-
import cors from "cors";
|
|
659
728
|
import http from "http";
|
|
660
729
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
661
730
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -708,7 +777,7 @@ function requiredClosedLineRange(body, toolName) {
|
|
|
708
777
|
}
|
|
709
778
|
return { startLine: parsed.startLine, endLine: parsed.endLine };
|
|
710
779
|
}
|
|
711
|
-
function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
780
|
+
function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
712
781
|
const app = express();
|
|
713
782
|
let mcpServerActive = false;
|
|
714
783
|
let lastMCPActivity = 0;
|
|
@@ -738,7 +807,49 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
738
807
|
const isPluginConnected = () => {
|
|
739
808
|
return bridge.getInstances().length > 0;
|
|
740
809
|
};
|
|
741
|
-
|
|
810
|
+
const allowedOrigins = new Set(security?.allowedOrigins ?? []);
|
|
811
|
+
app.use((req, res, next) => {
|
|
812
|
+
const origin = req.headers.origin;
|
|
813
|
+
if (typeof origin !== "string" || origin === "") {
|
|
814
|
+
next();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (!allowedOrigins.has(origin)) {
|
|
818
|
+
res.status(403).json({
|
|
819
|
+
error: "forbidden_origin",
|
|
820
|
+
message: `Cross-origin requests are not allowed from ${origin}. Set ROBLOX_STUDIO_ALLOWED_ORIGINS to allowlist specific origins.`
|
|
821
|
+
});
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
825
|
+
res.setHeader("Vary", "Origin");
|
|
826
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
827
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-MCP-Auth, Mcp-Protocol-Version");
|
|
828
|
+
if (req.method === "OPTIONS") {
|
|
829
|
+
res.status(204).end();
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
next();
|
|
833
|
+
});
|
|
834
|
+
const authToken = security?.authToken;
|
|
835
|
+
const authRequired = (path6) => path6 === "/mcp" || path6.startsWith("/mcp/") || path6 === "/proxy" || path6 === "/instances" || path6 === "/unregister-instance-id";
|
|
836
|
+
app.use((req, res, next) => {
|
|
837
|
+
if (!authToken || !authRequired(req.path)) {
|
|
838
|
+
next();
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
const headerToken = req.headers["x-mcp-auth"];
|
|
842
|
+
const bearer = typeof req.headers.authorization === "string" && req.headers.authorization.startsWith("Bearer ") ? req.headers.authorization.slice("Bearer ".length) : void 0;
|
|
843
|
+
const provided = typeof headerToken === "string" && headerToken !== "" ? headerToken : bearer;
|
|
844
|
+
if (provided !== void 0 && tokensMatch(provided, authToken)) {
|
|
845
|
+
next();
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
res.status(401).json({
|
|
849
|
+
error: "unauthorized",
|
|
850
|
+
message: 'Missing or invalid auth token. Send it as "X-MCP-Auth: <token>" or "Authorization: Bearer <token>". ' + (security?.authTokenHint ?? "The token is in ~/.robloxstudio-mcp/auth-token (or ROBLOX_STUDIO_AUTH_TOKEN).")
|
|
851
|
+
});
|
|
852
|
+
});
|
|
742
853
|
app.use(express.json({ limit: "50mb" }));
|
|
743
854
|
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
|
744
855
|
app.get("/health", (req, res) => {
|
|
@@ -801,11 +912,11 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
801
912
|
pluginVariant: typeof pluginVariant === "string" ? pluginVariant : "unknown",
|
|
802
913
|
serverVersion: serverConfig?.version ?? ""
|
|
803
914
|
});
|
|
804
|
-
} catch (
|
|
915
|
+
} catch (err2) {
|
|
805
916
|
res.status(500).json({
|
|
806
917
|
success: false,
|
|
807
918
|
error: "ready_registration_exception",
|
|
808
|
-
message:
|
|
919
|
+
message: err2 instanceof Error ? err2.message : String(err2),
|
|
809
920
|
request: requestContext
|
|
810
921
|
});
|
|
811
922
|
return;
|
|
@@ -956,8 +1067,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
956
1067
|
try {
|
|
957
1068
|
const response = await bridge.sendRequest(endpoint, data, targetInstanceId, targetRole);
|
|
958
1069
|
res.json({ response });
|
|
959
|
-
} catch (
|
|
960
|
-
res.status(500).json({ error:
|
|
1070
|
+
} catch (err2) {
|
|
1071
|
+
res.status(500).json({ error: err2.message || "Proxy request failed" });
|
|
961
1072
|
}
|
|
962
1073
|
});
|
|
963
1074
|
if (serverConfig) {
|
|
@@ -1069,19 +1180,19 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
1069
1180
|
return app;
|
|
1070
1181
|
}
|
|
1071
1182
|
function listenWithRetry(app, host, startPort, maxAttempts = 5) {
|
|
1072
|
-
return new Promise(async (
|
|
1183
|
+
return new Promise(async (resolve5, reject) => {
|
|
1073
1184
|
for (let i = 0; i < maxAttempts; i++) {
|
|
1074
1185
|
const port = startPort + i;
|
|
1075
1186
|
try {
|
|
1076
1187
|
const server = await bindPort(app, host, port);
|
|
1077
|
-
|
|
1188
|
+
resolve5({ server, port });
|
|
1078
1189
|
return;
|
|
1079
|
-
} catch (
|
|
1080
|
-
if (
|
|
1190
|
+
} catch (err2) {
|
|
1191
|
+
if (err2.code === "EADDRINUSE") {
|
|
1081
1192
|
console.error(`Port ${port} in use, trying next...`);
|
|
1082
1193
|
continue;
|
|
1083
1194
|
}
|
|
1084
|
-
reject(
|
|
1195
|
+
reject(err2);
|
|
1085
1196
|
return;
|
|
1086
1197
|
}
|
|
1087
1198
|
}
|
|
@@ -1089,16 +1200,16 @@ function listenWithRetry(app, host, startPort, maxAttempts = 5) {
|
|
|
1089
1200
|
});
|
|
1090
1201
|
}
|
|
1091
1202
|
function bindPort(app, host, port) {
|
|
1092
|
-
return new Promise((
|
|
1203
|
+
return new Promise((resolve5, reject) => {
|
|
1093
1204
|
const server = http.createServer(app);
|
|
1094
|
-
const onError = (
|
|
1205
|
+
const onError = (err2) => {
|
|
1095
1206
|
server.removeListener("error", onError);
|
|
1096
|
-
reject(
|
|
1207
|
+
reject(err2);
|
|
1097
1208
|
};
|
|
1098
1209
|
server.once("error", onError);
|
|
1099
1210
|
server.listen(port, host, () => {
|
|
1100
1211
|
server.removeListener("error", onError);
|
|
1101
|
-
|
|
1212
|
+
resolve5(server);
|
|
1102
1213
|
});
|
|
1103
1214
|
});
|
|
1104
1215
|
}
|
|
@@ -1108,7 +1219,9 @@ var init_http_server = __esm({
|
|
|
1108
1219
|
"use strict";
|
|
1109
1220
|
init_bridge_service();
|
|
1110
1221
|
init_mcp_compat();
|
|
1222
|
+
init_auth();
|
|
1111
1223
|
TOOL_HANDLERS = {
|
|
1224
|
+
get_roblox_skills: (tools, body) => tools.getRobloxSkills(body.action, body.name),
|
|
1112
1225
|
get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
|
|
1113
1226
|
get_file_tree: (tools, body) => tools.getFileTree(body.path, body.instance_id),
|
|
1114
1227
|
search_files: (tools, body) => tools.searchFiles(body.query, body.searchType, body.instance_id),
|
|
@@ -1275,8 +1388,844 @@ var init_studio_client = __esm({
|
|
|
1275
1388
|
}
|
|
1276
1389
|
});
|
|
1277
1390
|
|
|
1391
|
+
// ../core/dist/tools/build-interpreter.js
|
|
1392
|
+
import * as acorn from "acorn";
|
|
1393
|
+
function isPlainObject(value) {
|
|
1394
|
+
if (value === null || typeof value !== "object")
|
|
1395
|
+
return false;
|
|
1396
|
+
const proto = Object.getPrototypeOf(value);
|
|
1397
|
+
return proto === Object.prototype || proto === null;
|
|
1398
|
+
}
|
|
1399
|
+
function runRestrictedScript(code, globals, options) {
|
|
1400
|
+
const timeoutMs = options?.timeoutMs ?? 1e4;
|
|
1401
|
+
const maxOps = options?.maxOps ?? 2e7;
|
|
1402
|
+
const deadline = Date.now() + timeoutMs;
|
|
1403
|
+
let ops = 0;
|
|
1404
|
+
let ast;
|
|
1405
|
+
try {
|
|
1406
|
+
ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: "script" });
|
|
1407
|
+
} catch (err2) {
|
|
1408
|
+
throw new Error(`Syntax error: ${err2.message}`);
|
|
1409
|
+
}
|
|
1410
|
+
const globalScope = new Scope(void 0, true);
|
|
1411
|
+
for (const [name, value] of Object.entries(globals)) {
|
|
1412
|
+
globalScope.declare(name, value, "const");
|
|
1413
|
+
}
|
|
1414
|
+
function tick() {
|
|
1415
|
+
ops++;
|
|
1416
|
+
if (ops > maxOps) {
|
|
1417
|
+
throw new ScriptTimeoutError(`Operation budget exceeded (${maxOps} ops)`);
|
|
1418
|
+
}
|
|
1419
|
+
if ((ops & 8191) === 0 && Date.now() > deadline) {
|
|
1420
|
+
throw new ScriptTimeoutError(`Execution timed out after ${timeoutMs}ms`);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function assertPropAllowed(prop) {
|
|
1424
|
+
const name = typeof prop === "symbol" ? String(prop) : String(prop);
|
|
1425
|
+
if (FORBIDDEN_PROPS.has(name)) {
|
|
1426
|
+
throw new Error(`Access to property "${name}" is not allowed`);
|
|
1427
|
+
}
|
|
1428
|
+
return name;
|
|
1429
|
+
}
|
|
1430
|
+
function getMember(obj, prop) {
|
|
1431
|
+
if (obj === null || obj === void 0) {
|
|
1432
|
+
throw new Error(`Cannot read property "${String(prop)}" of ${obj}`);
|
|
1433
|
+
}
|
|
1434
|
+
const name = assertPropAllowed(prop);
|
|
1435
|
+
if (Array.isArray(obj)) {
|
|
1436
|
+
if (name === "length")
|
|
1437
|
+
return obj.length;
|
|
1438
|
+
const idx = Number(name);
|
|
1439
|
+
if (Number.isInteger(idx) && idx >= 0)
|
|
1440
|
+
return obj[idx];
|
|
1441
|
+
if (ARRAY_MEMBERS.has(name))
|
|
1442
|
+
return obj[name];
|
|
1443
|
+
throw new Error(`Array method "${name}" is not allowed`);
|
|
1444
|
+
}
|
|
1445
|
+
if (typeof obj === "string") {
|
|
1446
|
+
if (name === "length")
|
|
1447
|
+
return obj.length;
|
|
1448
|
+
const idx = Number(name);
|
|
1449
|
+
if (Number.isInteger(idx) && idx >= 0)
|
|
1450
|
+
return obj[idx];
|
|
1451
|
+
if (STRING_MEMBERS.has(name))
|
|
1452
|
+
return obj[name];
|
|
1453
|
+
throw new Error(`String method "${name}" is not allowed`);
|
|
1454
|
+
}
|
|
1455
|
+
if (typeof obj === "number") {
|
|
1456
|
+
if (NUMBER_MEMBERS.has(name))
|
|
1457
|
+
return obj[name];
|
|
1458
|
+
throw new Error(`Number method "${name}" is not allowed`);
|
|
1459
|
+
}
|
|
1460
|
+
if (isPlainObject(obj)) {
|
|
1461
|
+
if (Object.prototype.hasOwnProperty.call(obj, name))
|
|
1462
|
+
return obj[name];
|
|
1463
|
+
return void 0;
|
|
1464
|
+
}
|
|
1465
|
+
throw new Error(`Property access on this value type is not allowed`);
|
|
1466
|
+
}
|
|
1467
|
+
function setMember(obj, prop, value) {
|
|
1468
|
+
if (obj === null || obj === void 0) {
|
|
1469
|
+
throw new Error(`Cannot set property "${String(prop)}" of ${obj}`);
|
|
1470
|
+
}
|
|
1471
|
+
const name = assertPropAllowed(prop);
|
|
1472
|
+
if (Object.isFrozen(obj)) {
|
|
1473
|
+
throw new Error(`Cannot assign to property "${name}" of a frozen object`);
|
|
1474
|
+
}
|
|
1475
|
+
if (Array.isArray(obj)) {
|
|
1476
|
+
const idx = Number(name);
|
|
1477
|
+
if (Number.isInteger(idx) && idx >= 0 || name === "length") {
|
|
1478
|
+
obj[name] = value;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
throw new Error(`Cannot set property "${name}" on an array`);
|
|
1482
|
+
}
|
|
1483
|
+
if (isPlainObject(obj)) {
|
|
1484
|
+
obj[name] = value;
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
throw new Error(`Property assignment on this value type is not allowed`);
|
|
1488
|
+
}
|
|
1489
|
+
function callFunction(fn, thisArg, args, desc) {
|
|
1490
|
+
if (typeof fn !== "function") {
|
|
1491
|
+
throw new Error(`${desc} is not a function`);
|
|
1492
|
+
}
|
|
1493
|
+
return fn.apply(thisArg, args);
|
|
1494
|
+
}
|
|
1495
|
+
function bindPattern(pattern, value, scope, kind) {
|
|
1496
|
+
switch (pattern.type) {
|
|
1497
|
+
case "Identifier":
|
|
1498
|
+
scope.declare(pattern.name, value, kind);
|
|
1499
|
+
return;
|
|
1500
|
+
case "AssignmentPattern":
|
|
1501
|
+
bindPattern(pattern.left, value === void 0 ? evaluate(pattern.right, scope) : value, scope, kind);
|
|
1502
|
+
return;
|
|
1503
|
+
case "ArrayPattern": {
|
|
1504
|
+
if (value === null || value === void 0)
|
|
1505
|
+
throw new Error("Cannot destructure null/undefined");
|
|
1506
|
+
let i = 0;
|
|
1507
|
+
for (const el of pattern.elements) {
|
|
1508
|
+
if (el === null) {
|
|
1509
|
+
i++;
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
if (el.type === "RestElement") {
|
|
1513
|
+
bindPattern(el.argument, Array.isArray(value) ? value.slice(i) : [], scope, kind);
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
bindPattern(el, getMember(value, i), scope, kind);
|
|
1517
|
+
i++;
|
|
1518
|
+
}
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
case "ObjectPattern": {
|
|
1522
|
+
if (value === null || value === void 0)
|
|
1523
|
+
throw new Error("Cannot destructure null/undefined");
|
|
1524
|
+
const used = /* @__PURE__ */ new Set();
|
|
1525
|
+
for (const propNode of pattern.properties) {
|
|
1526
|
+
if (propNode.type === "RestElement") {
|
|
1527
|
+
const rest = {};
|
|
1528
|
+
if (isPlainObject(value)) {
|
|
1529
|
+
for (const k of Object.keys(value)) {
|
|
1530
|
+
if (!used.has(k) && !FORBIDDEN_PROPS.has(k))
|
|
1531
|
+
rest[k] = value[k];
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
bindPattern(propNode.argument, rest, scope, kind);
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
const key = propNode.computed ? String(evaluate(propNode.key, scope)) : propNode.key.type === "Identifier" ? propNode.key.name : String(propNode.key.value);
|
|
1538
|
+
used.add(key);
|
|
1539
|
+
bindPattern(propNode.value, getMember(value, key), scope, kind);
|
|
1540
|
+
}
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
default:
|
|
1544
|
+
throw new Error(`Unsupported binding pattern: ${pattern.type}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
function makeFunction(node, closureScope) {
|
|
1548
|
+
return (...args) => {
|
|
1549
|
+
tick();
|
|
1550
|
+
const fnScope = new Scope(closureScope, true);
|
|
1551
|
+
node.params.forEach((param, i) => {
|
|
1552
|
+
if (param.type === "RestElement") {
|
|
1553
|
+
bindPattern(param.argument, args.slice(i), fnScope, "let");
|
|
1554
|
+
} else {
|
|
1555
|
+
bindPattern(param, args[i], fnScope, "let");
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
try {
|
|
1559
|
+
if (node.body.type === "BlockStatement") {
|
|
1560
|
+
executeBlock(node.body.body, new Scope(fnScope));
|
|
1561
|
+
return void 0;
|
|
1562
|
+
}
|
|
1563
|
+
return evaluate(node.body, fnScope);
|
|
1564
|
+
} catch (signal) {
|
|
1565
|
+
if (signal instanceof ReturnSignal)
|
|
1566
|
+
return signal.value;
|
|
1567
|
+
throw signal;
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
function hoistFunctions(statements, scope) {
|
|
1572
|
+
for (const stmt of statements) {
|
|
1573
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id) {
|
|
1574
|
+
if (stmt.async || stmt.generator)
|
|
1575
|
+
throw new Error("async/generator functions are not supported");
|
|
1576
|
+
scope.declare(stmt.id.name, makeFunction(stmt, scope), "let");
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
function executeBlock(statements, scope) {
|
|
1581
|
+
hoistFunctions(statements, scope);
|
|
1582
|
+
for (const stmt of statements) {
|
|
1583
|
+
execute(stmt, scope);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
function execute(node, scope) {
|
|
1587
|
+
tick();
|
|
1588
|
+
switch (node.type) {
|
|
1589
|
+
case "ExpressionStatement":
|
|
1590
|
+
evaluate(node.expression, scope);
|
|
1591
|
+
return;
|
|
1592
|
+
case "VariableDeclaration":
|
|
1593
|
+
for (const decl of node.declarations) {
|
|
1594
|
+
const value = decl.init ? evaluate(decl.init, scope) : void 0;
|
|
1595
|
+
bindPattern(decl.id, value, scope, node.kind);
|
|
1596
|
+
}
|
|
1597
|
+
return;
|
|
1598
|
+
case "FunctionDeclaration":
|
|
1599
|
+
return;
|
|
1600
|
+
// hoisted by executeBlock
|
|
1601
|
+
case "BlockStatement":
|
|
1602
|
+
executeBlock(node.body, new Scope(scope));
|
|
1603
|
+
return;
|
|
1604
|
+
case "EmptyStatement":
|
|
1605
|
+
return;
|
|
1606
|
+
case "IfStatement":
|
|
1607
|
+
if (evaluate(node.test, scope)) {
|
|
1608
|
+
execute(node.consequent, scope);
|
|
1609
|
+
} else if (node.alternate) {
|
|
1610
|
+
execute(node.alternate, scope);
|
|
1611
|
+
}
|
|
1612
|
+
return;
|
|
1613
|
+
case "ForStatement": {
|
|
1614
|
+
const forScope = new Scope(scope);
|
|
1615
|
+
if (node.init) {
|
|
1616
|
+
if (node.init.type === "VariableDeclaration")
|
|
1617
|
+
execute(node.init, forScope);
|
|
1618
|
+
else
|
|
1619
|
+
evaluate(node.init, forScope);
|
|
1620
|
+
}
|
|
1621
|
+
while (node.test === null || evaluate(node.test, forScope)) {
|
|
1622
|
+
tick();
|
|
1623
|
+
try {
|
|
1624
|
+
execute(node.body, new Scope(forScope));
|
|
1625
|
+
} catch (signal) {
|
|
1626
|
+
if (signal instanceof BreakSignal)
|
|
1627
|
+
break;
|
|
1628
|
+
if (!(signal instanceof ContinueSignal))
|
|
1629
|
+
throw signal;
|
|
1630
|
+
}
|
|
1631
|
+
if (node.update)
|
|
1632
|
+
evaluate(node.update, forScope);
|
|
1633
|
+
}
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
case "ForOfStatement": {
|
|
1637
|
+
const iterable = evaluate(node.right, scope);
|
|
1638
|
+
if (!Array.isArray(iterable) && typeof iterable !== "string") {
|
|
1639
|
+
throw new Error("for...of only supports arrays and strings");
|
|
1640
|
+
}
|
|
1641
|
+
for (const item of iterable) {
|
|
1642
|
+
tick();
|
|
1643
|
+
const iterScope = new Scope(scope);
|
|
1644
|
+
if (node.left.type === "VariableDeclaration") {
|
|
1645
|
+
bindPattern(node.left.declarations[0].id, item, iterScope, node.left.kind);
|
|
1646
|
+
} else {
|
|
1647
|
+
assignTo(node.left, item, iterScope);
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
execute(node.body, iterScope);
|
|
1651
|
+
} catch (signal) {
|
|
1652
|
+
if (signal instanceof BreakSignal)
|
|
1653
|
+
break;
|
|
1654
|
+
if (!(signal instanceof ContinueSignal))
|
|
1655
|
+
throw signal;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
case "ForInStatement": {
|
|
1661
|
+
const obj = evaluate(node.right, scope);
|
|
1662
|
+
const keys = isPlainObject(obj) ? Object.keys(obj) : Array.isArray(obj) ? obj.map((_, i) => String(i)) : [];
|
|
1663
|
+
for (const key of keys) {
|
|
1664
|
+
tick();
|
|
1665
|
+
const iterScope = new Scope(scope);
|
|
1666
|
+
if (node.left.type === "VariableDeclaration") {
|
|
1667
|
+
bindPattern(node.left.declarations[0].id, key, iterScope, node.left.kind);
|
|
1668
|
+
} else {
|
|
1669
|
+
assignTo(node.left, key, iterScope);
|
|
1670
|
+
}
|
|
1671
|
+
try {
|
|
1672
|
+
execute(node.body, iterScope);
|
|
1673
|
+
} catch (signal) {
|
|
1674
|
+
if (signal instanceof BreakSignal)
|
|
1675
|
+
break;
|
|
1676
|
+
if (!(signal instanceof ContinueSignal))
|
|
1677
|
+
throw signal;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
case "WhileStatement":
|
|
1683
|
+
while (evaluate(node.test, scope)) {
|
|
1684
|
+
tick();
|
|
1685
|
+
try {
|
|
1686
|
+
execute(node.body, new Scope(scope));
|
|
1687
|
+
} catch (signal) {
|
|
1688
|
+
if (signal instanceof BreakSignal)
|
|
1689
|
+
break;
|
|
1690
|
+
if (!(signal instanceof ContinueSignal))
|
|
1691
|
+
throw signal;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
return;
|
|
1695
|
+
case "DoWhileStatement":
|
|
1696
|
+
do {
|
|
1697
|
+
tick();
|
|
1698
|
+
try {
|
|
1699
|
+
execute(node.body, new Scope(scope));
|
|
1700
|
+
} catch (signal) {
|
|
1701
|
+
if (signal instanceof BreakSignal)
|
|
1702
|
+
break;
|
|
1703
|
+
if (!(signal instanceof ContinueSignal))
|
|
1704
|
+
throw signal;
|
|
1705
|
+
}
|
|
1706
|
+
} while (evaluate(node.test, scope));
|
|
1707
|
+
return;
|
|
1708
|
+
case "BreakStatement":
|
|
1709
|
+
throw new BreakSignal(node.label?.name);
|
|
1710
|
+
case "ContinueStatement":
|
|
1711
|
+
throw new ContinueSignal(node.label?.name);
|
|
1712
|
+
case "ReturnStatement":
|
|
1713
|
+
throw new ReturnSignal(node.argument ? evaluate(node.argument, scope) : void 0);
|
|
1714
|
+
case "ThrowStatement":
|
|
1715
|
+
throw evaluate(node.argument, scope);
|
|
1716
|
+
case "TryStatement": {
|
|
1717
|
+
try {
|
|
1718
|
+
execute(node.block, scope);
|
|
1719
|
+
} catch (err2) {
|
|
1720
|
+
if (err2 instanceof BreakSignal || err2 instanceof ContinueSignal || err2 instanceof ReturnSignal)
|
|
1721
|
+
throw err2;
|
|
1722
|
+
if (err2 instanceof ScriptTimeoutError)
|
|
1723
|
+
throw err2;
|
|
1724
|
+
if (node.handler) {
|
|
1725
|
+
const catchScope = new Scope(scope);
|
|
1726
|
+
if (node.handler.param) {
|
|
1727
|
+
const message = err2 instanceof Error ? { message: err2.message } : err2;
|
|
1728
|
+
bindPattern(node.handler.param, message, catchScope, "let");
|
|
1729
|
+
}
|
|
1730
|
+
execute(node.handler.body, catchScope);
|
|
1731
|
+
}
|
|
1732
|
+
} finally {
|
|
1733
|
+
if (node.finalizer)
|
|
1734
|
+
execute(node.finalizer, scope);
|
|
1735
|
+
}
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
case "SwitchStatement": {
|
|
1739
|
+
const disc = evaluate(node.discriminant, scope);
|
|
1740
|
+
const switchScope = new Scope(scope);
|
|
1741
|
+
let matched = false;
|
|
1742
|
+
try {
|
|
1743
|
+
for (const caseNode of node.cases) {
|
|
1744
|
+
if (!matched) {
|
|
1745
|
+
if (caseNode.test === null)
|
|
1746
|
+
matched = true;
|
|
1747
|
+
else if (evaluate(caseNode.test, switchScope) === disc)
|
|
1748
|
+
matched = true;
|
|
1749
|
+
}
|
|
1750
|
+
if (matched) {
|
|
1751
|
+
for (const stmt of caseNode.consequent)
|
|
1752
|
+
execute(stmt, switchScope);
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
} catch (signal) {
|
|
1756
|
+
if (!(signal instanceof BreakSignal))
|
|
1757
|
+
throw signal;
|
|
1758
|
+
}
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
case "LabeledStatement":
|
|
1762
|
+
try {
|
|
1763
|
+
execute(node.body, scope);
|
|
1764
|
+
} catch (signal) {
|
|
1765
|
+
if (signal instanceof BreakSignal && signal.label === node.label.name)
|
|
1766
|
+
return;
|
|
1767
|
+
throw signal;
|
|
1768
|
+
}
|
|
1769
|
+
return;
|
|
1770
|
+
default:
|
|
1771
|
+
throw new Error(`Unsupported statement: ${node.type}`);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
function assignTo(target, value, scope) {
|
|
1775
|
+
if (target.type === "Identifier") {
|
|
1776
|
+
scope.set(target.name, value);
|
|
1777
|
+
} else if (target.type === "MemberExpression") {
|
|
1778
|
+
const obj = evaluate(target.object, scope);
|
|
1779
|
+
const prop = target.computed ? evaluate(target.property, scope) : target.property.name;
|
|
1780
|
+
setMember(obj, prop, value);
|
|
1781
|
+
} else {
|
|
1782
|
+
throw new Error(`Unsupported assignment target: ${target.type}`);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function evaluate(node, scope) {
|
|
1786
|
+
tick();
|
|
1787
|
+
switch (node.type) {
|
|
1788
|
+
case "Literal":
|
|
1789
|
+
if (node.regex)
|
|
1790
|
+
throw new Error("Regular expression literals are not supported");
|
|
1791
|
+
return node.value;
|
|
1792
|
+
case "TemplateLiteral": {
|
|
1793
|
+
let out = "";
|
|
1794
|
+
node.quasis.forEach((quasi, i) => {
|
|
1795
|
+
out += quasi.value.cooked ?? "";
|
|
1796
|
+
if (i < node.expressions.length)
|
|
1797
|
+
out += String(evaluate(node.expressions[i], scope));
|
|
1798
|
+
});
|
|
1799
|
+
return out;
|
|
1800
|
+
}
|
|
1801
|
+
case "Identifier":
|
|
1802
|
+
return scope.get(node.name);
|
|
1803
|
+
case "ArrayExpression": {
|
|
1804
|
+
const arr = [];
|
|
1805
|
+
for (const el of node.elements) {
|
|
1806
|
+
if (el === null) {
|
|
1807
|
+
arr.length++;
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
if (el.type === "SpreadElement") {
|
|
1811
|
+
const spread = evaluate(el.argument, scope);
|
|
1812
|
+
if (!Array.isArray(spread) && typeof spread !== "string") {
|
|
1813
|
+
throw new Error("Spread only supports arrays and strings");
|
|
1814
|
+
}
|
|
1815
|
+
for (const item of spread)
|
|
1816
|
+
arr.push(item);
|
|
1817
|
+
} else {
|
|
1818
|
+
arr.push(evaluate(el, scope));
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
return arr;
|
|
1822
|
+
}
|
|
1823
|
+
case "ObjectExpression": {
|
|
1824
|
+
const obj = {};
|
|
1825
|
+
for (const prop of node.properties) {
|
|
1826
|
+
if (prop.type === "SpreadElement") {
|
|
1827
|
+
const spread = evaluate(prop.argument, scope);
|
|
1828
|
+
if (isPlainObject(spread)) {
|
|
1829
|
+
for (const k of Object.keys(spread)) {
|
|
1830
|
+
if (!FORBIDDEN_PROPS.has(k))
|
|
1831
|
+
obj[k] = spread[k];
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
if (prop.kind !== "init")
|
|
1837
|
+
throw new Error("Getters/setters are not supported");
|
|
1838
|
+
const key = prop.computed ? String(evaluate(prop.key, scope)) : prop.key.type === "Identifier" ? prop.key.name : String(prop.key.value);
|
|
1839
|
+
assertPropAllowed(key);
|
|
1840
|
+
obj[key] = evaluate(prop.value, scope);
|
|
1841
|
+
}
|
|
1842
|
+
return obj;
|
|
1843
|
+
}
|
|
1844
|
+
case "ArrowFunctionExpression":
|
|
1845
|
+
case "FunctionExpression":
|
|
1846
|
+
if (node.async || node.generator)
|
|
1847
|
+
throw new Error("async/generator functions are not supported");
|
|
1848
|
+
return makeFunction(node, scope);
|
|
1849
|
+
case "UnaryExpression": {
|
|
1850
|
+
if (node.operator === "typeof" && node.argument.type === "Identifier" && !scope.has(node.argument.name)) {
|
|
1851
|
+
return "undefined";
|
|
1852
|
+
}
|
|
1853
|
+
if (node.operator === "delete") {
|
|
1854
|
+
if (node.argument.type !== "MemberExpression")
|
|
1855
|
+
return true;
|
|
1856
|
+
const obj = evaluate(node.argument.object, scope);
|
|
1857
|
+
const prop = node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name;
|
|
1858
|
+
const name = assertPropAllowed(prop);
|
|
1859
|
+
if (isPlainObject(obj) && !Object.isFrozen(obj)) {
|
|
1860
|
+
delete obj[name];
|
|
1861
|
+
return true;
|
|
1862
|
+
}
|
|
1863
|
+
throw new Error("delete is only allowed on plain objects");
|
|
1864
|
+
}
|
|
1865
|
+
const arg = evaluate(node.argument, scope);
|
|
1866
|
+
switch (node.operator) {
|
|
1867
|
+
case "-":
|
|
1868
|
+
return -arg;
|
|
1869
|
+
case "+":
|
|
1870
|
+
return +arg;
|
|
1871
|
+
case "!":
|
|
1872
|
+
return !arg;
|
|
1873
|
+
case "~":
|
|
1874
|
+
return ~arg;
|
|
1875
|
+
case "typeof":
|
|
1876
|
+
return typeof arg;
|
|
1877
|
+
case "void":
|
|
1878
|
+
return void 0;
|
|
1879
|
+
default:
|
|
1880
|
+
throw new Error(`Unsupported unary operator: ${node.operator}`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
case "UpdateExpression": {
|
|
1884
|
+
const old = node.argument.type === "Identifier" ? scope.get(node.argument.name) : getMember(evaluate(node.argument.object, scope), node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name);
|
|
1885
|
+
const next = node.operator === "++" ? old + 1 : old - 1;
|
|
1886
|
+
assignTo(node.argument, next, scope);
|
|
1887
|
+
return node.prefix ? next : old;
|
|
1888
|
+
}
|
|
1889
|
+
case "BinaryExpression": {
|
|
1890
|
+
if (node.operator === "in") {
|
|
1891
|
+
const key = assertPropAllowed(evaluate(node.left, scope));
|
|
1892
|
+
const obj = evaluate(node.right, scope);
|
|
1893
|
+
if (isPlainObject(obj))
|
|
1894
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1895
|
+
if (Array.isArray(obj))
|
|
1896
|
+
return Number(key) >= 0 && Number(key) < obj.length;
|
|
1897
|
+
throw new Error('"in" is only supported on plain objects and arrays');
|
|
1898
|
+
}
|
|
1899
|
+
if (node.operator === "instanceof")
|
|
1900
|
+
throw new Error("instanceof is not supported");
|
|
1901
|
+
const l = evaluate(node.left, scope);
|
|
1902
|
+
const r = evaluate(node.right, scope);
|
|
1903
|
+
switch (node.operator) {
|
|
1904
|
+
case "+":
|
|
1905
|
+
return l + r;
|
|
1906
|
+
case "-":
|
|
1907
|
+
return l - r;
|
|
1908
|
+
case "*":
|
|
1909
|
+
return l * r;
|
|
1910
|
+
case "/":
|
|
1911
|
+
return l / r;
|
|
1912
|
+
case "%":
|
|
1913
|
+
return l % r;
|
|
1914
|
+
case "**":
|
|
1915
|
+
return l ** r;
|
|
1916
|
+
case "==":
|
|
1917
|
+
return l == r;
|
|
1918
|
+
// eslint-disable-line eqeqeq
|
|
1919
|
+
case "!=":
|
|
1920
|
+
return l != r;
|
|
1921
|
+
// eslint-disable-line eqeqeq
|
|
1922
|
+
case "===":
|
|
1923
|
+
return l === r;
|
|
1924
|
+
case "!==":
|
|
1925
|
+
return l !== r;
|
|
1926
|
+
case "<":
|
|
1927
|
+
return l < r;
|
|
1928
|
+
case "<=":
|
|
1929
|
+
return l <= r;
|
|
1930
|
+
case ">":
|
|
1931
|
+
return l > r;
|
|
1932
|
+
case ">=":
|
|
1933
|
+
return l >= r;
|
|
1934
|
+
case "&":
|
|
1935
|
+
return l & r;
|
|
1936
|
+
case "|":
|
|
1937
|
+
return l | r;
|
|
1938
|
+
case "^":
|
|
1939
|
+
return l ^ r;
|
|
1940
|
+
case "<<":
|
|
1941
|
+
return l << r;
|
|
1942
|
+
case ">>":
|
|
1943
|
+
return l >> r;
|
|
1944
|
+
case ">>>":
|
|
1945
|
+
return l >>> r;
|
|
1946
|
+
default:
|
|
1947
|
+
throw new Error(`Unsupported binary operator: ${node.operator}`);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
case "LogicalExpression": {
|
|
1951
|
+
const l = evaluate(node.left, scope);
|
|
1952
|
+
if (node.operator === "&&")
|
|
1953
|
+
return l ? evaluate(node.right, scope) : l;
|
|
1954
|
+
if (node.operator === "||")
|
|
1955
|
+
return l ? l : evaluate(node.right, scope);
|
|
1956
|
+
if (node.operator === "??")
|
|
1957
|
+
return l ?? evaluate(node.right, scope);
|
|
1958
|
+
throw new Error(`Unsupported logical operator: ${node.operator}`);
|
|
1959
|
+
}
|
|
1960
|
+
case "AssignmentExpression": {
|
|
1961
|
+
let value;
|
|
1962
|
+
if (node.operator === "=") {
|
|
1963
|
+
value = evaluate(node.right, scope);
|
|
1964
|
+
} else {
|
|
1965
|
+
const current = node.left.type === "Identifier" ? scope.get(node.left.name) : getMember(evaluate(node.left.object, scope), node.left.computed ? evaluate(node.left.property, scope) : node.left.property.name);
|
|
1966
|
+
const rhs = () => evaluate(node.right, scope);
|
|
1967
|
+
switch (node.operator) {
|
|
1968
|
+
case "+=":
|
|
1969
|
+
value = current + rhs();
|
|
1970
|
+
break;
|
|
1971
|
+
case "-=":
|
|
1972
|
+
value = current - rhs();
|
|
1973
|
+
break;
|
|
1974
|
+
case "*=":
|
|
1975
|
+
value = current * rhs();
|
|
1976
|
+
break;
|
|
1977
|
+
case "/=":
|
|
1978
|
+
value = current / rhs();
|
|
1979
|
+
break;
|
|
1980
|
+
case "%=":
|
|
1981
|
+
value = current % rhs();
|
|
1982
|
+
break;
|
|
1983
|
+
case "**=":
|
|
1984
|
+
value = current ** rhs();
|
|
1985
|
+
break;
|
|
1986
|
+
case "&=":
|
|
1987
|
+
value = current & rhs();
|
|
1988
|
+
break;
|
|
1989
|
+
case "|=":
|
|
1990
|
+
value = current | rhs();
|
|
1991
|
+
break;
|
|
1992
|
+
case "^=":
|
|
1993
|
+
value = current ^ rhs();
|
|
1994
|
+
break;
|
|
1995
|
+
case "<<=":
|
|
1996
|
+
value = current << rhs();
|
|
1997
|
+
break;
|
|
1998
|
+
case ">>=":
|
|
1999
|
+
value = current >> rhs();
|
|
2000
|
+
break;
|
|
2001
|
+
case ">>>=":
|
|
2002
|
+
value = current >>> rhs();
|
|
2003
|
+
break;
|
|
2004
|
+
case "&&=":
|
|
2005
|
+
value = current ? rhs() : current;
|
|
2006
|
+
break;
|
|
2007
|
+
case "||=":
|
|
2008
|
+
value = current ? current : rhs();
|
|
2009
|
+
break;
|
|
2010
|
+
case "??=":
|
|
2011
|
+
value = current ?? rhs();
|
|
2012
|
+
break;
|
|
2013
|
+
default:
|
|
2014
|
+
throw new Error(`Unsupported assignment operator: ${node.operator}`);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
assignTo(node.left, value, scope);
|
|
2018
|
+
return value;
|
|
2019
|
+
}
|
|
2020
|
+
case "ConditionalExpression":
|
|
2021
|
+
return evaluate(node.test, scope) ? evaluate(node.consequent, scope) : evaluate(node.alternate, scope);
|
|
2022
|
+
case "CallExpression": {
|
|
2023
|
+
const args = [];
|
|
2024
|
+
for (const argNode of node.arguments) {
|
|
2025
|
+
if (argNode.type === "SpreadElement") {
|
|
2026
|
+
const spread = evaluate(argNode.argument, scope);
|
|
2027
|
+
if (!Array.isArray(spread) && typeof spread !== "string") {
|
|
2028
|
+
throw new Error("Spread only supports arrays and strings");
|
|
2029
|
+
}
|
|
2030
|
+
for (const item of spread)
|
|
2031
|
+
args.push(item);
|
|
2032
|
+
} else {
|
|
2033
|
+
args.push(evaluate(argNode, scope));
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (node.callee.type === "MemberExpression") {
|
|
2037
|
+
const obj = evaluate(node.callee.object, scope);
|
|
2038
|
+
if (node.optional && (obj === null || obj === void 0))
|
|
2039
|
+
return void 0;
|
|
2040
|
+
const prop = node.callee.computed ? evaluate(node.callee.property, scope) : node.callee.property.name;
|
|
2041
|
+
const method = getMember(obj, prop);
|
|
2042
|
+
if (node.callee.optional && (method === null || method === void 0))
|
|
2043
|
+
return void 0;
|
|
2044
|
+
return callFunction(method, obj, args, `${String(prop)}`);
|
|
2045
|
+
}
|
|
2046
|
+
const fn = evaluate(node.callee, scope);
|
|
2047
|
+
if (node.optional && (fn === null || fn === void 0))
|
|
2048
|
+
return void 0;
|
|
2049
|
+
const desc = node.callee.type === "Identifier" ? node.callee.name : "expression";
|
|
2050
|
+
return callFunction(fn, void 0, args, desc);
|
|
2051
|
+
}
|
|
2052
|
+
case "MemberExpression": {
|
|
2053
|
+
const obj = evaluate(node.object, scope);
|
|
2054
|
+
if (node.optional && (obj === null || obj === void 0))
|
|
2055
|
+
return void 0;
|
|
2056
|
+
const prop = node.computed ? evaluate(node.property, scope) : node.property.name;
|
|
2057
|
+
return getMember(obj, prop);
|
|
2058
|
+
}
|
|
2059
|
+
case "ChainExpression":
|
|
2060
|
+
return evaluate(node.expression, scope);
|
|
2061
|
+
case "SequenceExpression": {
|
|
2062
|
+
let result;
|
|
2063
|
+
for (const expr of node.expressions)
|
|
2064
|
+
result = evaluate(expr, scope);
|
|
2065
|
+
return result;
|
|
2066
|
+
}
|
|
2067
|
+
case "NewExpression":
|
|
2068
|
+
throw new Error('"new" is not supported in build code');
|
|
2069
|
+
case "ThisExpression":
|
|
2070
|
+
throw new Error('"this" is not supported in build code');
|
|
2071
|
+
case "AwaitExpression":
|
|
2072
|
+
throw new Error("await is not supported in build code");
|
|
2073
|
+
case "TaggedTemplateExpression":
|
|
2074
|
+
throw new Error("Tagged templates are not supported in build code");
|
|
2075
|
+
default:
|
|
2076
|
+
throw new Error(`Unsupported expression: ${node.type}`);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
executeBlock(ast.body, globalScope);
|
|
2080
|
+
}
|
|
2081
|
+
var ScriptTimeoutError, BreakSignal, ContinueSignal, ReturnSignal, FORBIDDEN_PROPS, ARRAY_MEMBERS, STRING_MEMBERS, NUMBER_MEMBERS, Scope;
|
|
2082
|
+
var init_build_interpreter = __esm({
|
|
2083
|
+
"../core/dist/tools/build-interpreter.js"() {
|
|
2084
|
+
"use strict";
|
|
2085
|
+
ScriptTimeoutError = class extends Error {
|
|
2086
|
+
constructor(message) {
|
|
2087
|
+
super(message);
|
|
2088
|
+
this.name = "ScriptTimeoutError";
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
BreakSignal = class {
|
|
2092
|
+
label;
|
|
2093
|
+
constructor(label) {
|
|
2094
|
+
this.label = label;
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
ContinueSignal = class {
|
|
2098
|
+
label;
|
|
2099
|
+
constructor(label) {
|
|
2100
|
+
this.label = label;
|
|
2101
|
+
}
|
|
2102
|
+
};
|
|
2103
|
+
ReturnSignal = class {
|
|
2104
|
+
value;
|
|
2105
|
+
constructor(value) {
|
|
2106
|
+
this.value = value;
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
FORBIDDEN_PROPS = /* @__PURE__ */ new Set([
|
|
2110
|
+
"__proto__",
|
|
2111
|
+
"constructor",
|
|
2112
|
+
"prototype",
|
|
2113
|
+
"__defineGetter__",
|
|
2114
|
+
"__defineSetter__",
|
|
2115
|
+
"__lookupGetter__",
|
|
2116
|
+
"__lookupSetter__",
|
|
2117
|
+
"caller",
|
|
2118
|
+
"callee",
|
|
2119
|
+
"arguments",
|
|
2120
|
+
"bind",
|
|
2121
|
+
"call",
|
|
2122
|
+
"apply"
|
|
2123
|
+
]);
|
|
2124
|
+
ARRAY_MEMBERS = /* @__PURE__ */ new Set([
|
|
2125
|
+
"length",
|
|
2126
|
+
"push",
|
|
2127
|
+
"pop",
|
|
2128
|
+
"shift",
|
|
2129
|
+
"unshift",
|
|
2130
|
+
"slice",
|
|
2131
|
+
"splice",
|
|
2132
|
+
"indexOf",
|
|
2133
|
+
"lastIndexOf",
|
|
2134
|
+
"includes",
|
|
2135
|
+
"join",
|
|
2136
|
+
"map",
|
|
2137
|
+
"filter",
|
|
2138
|
+
"forEach",
|
|
2139
|
+
"reduce",
|
|
2140
|
+
"reduceRight",
|
|
2141
|
+
"concat",
|
|
2142
|
+
"reverse",
|
|
2143
|
+
"sort",
|
|
2144
|
+
"find",
|
|
2145
|
+
"findIndex",
|
|
2146
|
+
"some",
|
|
2147
|
+
"every",
|
|
2148
|
+
"flat",
|
|
2149
|
+
"flatMap",
|
|
2150
|
+
"fill"
|
|
2151
|
+
]);
|
|
2152
|
+
STRING_MEMBERS = /* @__PURE__ */ new Set([
|
|
2153
|
+
"length",
|
|
2154
|
+
"charAt",
|
|
2155
|
+
"charCodeAt",
|
|
2156
|
+
"codePointAt",
|
|
2157
|
+
"indexOf",
|
|
2158
|
+
"lastIndexOf",
|
|
2159
|
+
"includes",
|
|
2160
|
+
"startsWith",
|
|
2161
|
+
"endsWith",
|
|
2162
|
+
"slice",
|
|
2163
|
+
"substring",
|
|
2164
|
+
"toUpperCase",
|
|
2165
|
+
"toLowerCase",
|
|
2166
|
+
"trim",
|
|
2167
|
+
"trimStart",
|
|
2168
|
+
"trimEnd",
|
|
2169
|
+
"split",
|
|
2170
|
+
"repeat",
|
|
2171
|
+
"padStart",
|
|
2172
|
+
"padEnd",
|
|
2173
|
+
"concat",
|
|
2174
|
+
"at",
|
|
2175
|
+
"replace",
|
|
2176
|
+
"replaceAll"
|
|
2177
|
+
]);
|
|
2178
|
+
NUMBER_MEMBERS = /* @__PURE__ */ new Set(["toFixed", "toPrecision", "toString"]);
|
|
2179
|
+
Scope = class {
|
|
2180
|
+
parent;
|
|
2181
|
+
isFunctionScope;
|
|
2182
|
+
vars = /* @__PURE__ */ new Map();
|
|
2183
|
+
consts = /* @__PURE__ */ new Set();
|
|
2184
|
+
constructor(parent, isFunctionScope = false) {
|
|
2185
|
+
this.parent = parent;
|
|
2186
|
+
this.isFunctionScope = isFunctionScope;
|
|
2187
|
+
}
|
|
2188
|
+
declare(name, value, kind) {
|
|
2189
|
+
if (kind === "var" && !this.isFunctionScope && this.parent) {
|
|
2190
|
+
this.parent.declare(name, value, kind);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
this.vars.set(name, value);
|
|
2194
|
+
if (kind === "const")
|
|
2195
|
+
this.consts.add(name);
|
|
2196
|
+
}
|
|
2197
|
+
resolve(name) {
|
|
2198
|
+
if (this.vars.has(name))
|
|
2199
|
+
return this;
|
|
2200
|
+
return this.parent?.resolve(name);
|
|
2201
|
+
}
|
|
2202
|
+
globalScope() {
|
|
2203
|
+
return this.parent ? this.parent.globalScope() : this;
|
|
2204
|
+
}
|
|
2205
|
+
has(name) {
|
|
2206
|
+
return this.resolve(name) !== void 0;
|
|
2207
|
+
}
|
|
2208
|
+
get(name) {
|
|
2209
|
+
const s = this.resolve(name);
|
|
2210
|
+
if (!s)
|
|
2211
|
+
throw new Error(`${name} is not defined`);
|
|
2212
|
+
return s.vars.get(name);
|
|
2213
|
+
}
|
|
2214
|
+
set(name, value) {
|
|
2215
|
+
const s = this.resolve(name);
|
|
2216
|
+
if (!s) {
|
|
2217
|
+
this.globalScope().vars.set(name, value);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (s.consts.has(name))
|
|
2221
|
+
throw new Error(`Assignment to constant variable "${name}"`);
|
|
2222
|
+
s.vars.set(name, value);
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
});
|
|
2227
|
+
|
|
1278
2228
|
// ../core/dist/tools/build-executor.js
|
|
1279
|
-
import * as vm from "vm";
|
|
1280
2229
|
function createSeededRng(seed) {
|
|
1281
2230
|
let s = seed;
|
|
1282
2231
|
return () => {
|
|
@@ -1626,6 +2575,7 @@ function runBuildExecutor(code, palette, seed, options) {
|
|
|
1626
2575
|
]);
|
|
1627
2576
|
}
|
|
1628
2577
|
const rng = createSeededRng(seed ?? 42);
|
|
2578
|
+
const safeMath = Object.freeze(Object.create(null, Object.getOwnPropertyDescriptors(Math)));
|
|
1629
2579
|
const sandbox = {
|
|
1630
2580
|
part: partFn,
|
|
1631
2581
|
rpart: rpartFn,
|
|
@@ -1642,25 +2592,21 @@ function runBuildExecutor(code, palette, seed, options) {
|
|
|
1642
2592
|
column: columnFn,
|
|
1643
2593
|
pew: pewFn,
|
|
1644
2594
|
fence: fenceFn,
|
|
1645
|
-
Math,
|
|
2595
|
+
Math: safeMath,
|
|
1646
2596
|
GRID_SIZE: 1,
|
|
1647
2597
|
rng,
|
|
1648
|
-
console: { log: () => {
|
|
2598
|
+
console: Object.freeze({ log: () => {
|
|
1649
2599
|
}, warn: () => {
|
|
1650
2600
|
}, error: () => {
|
|
1651
|
-
} }
|
|
2601
|
+
} })
|
|
1652
2602
|
};
|
|
1653
|
-
const context = vm.createContext(sandbox, {
|
|
1654
|
-
codeGeneration: { strings: false, wasm: false }
|
|
1655
|
-
});
|
|
1656
|
-
const script = new vm.Script(code, { filename: "build-generator.js" });
|
|
1657
2603
|
try {
|
|
1658
|
-
|
|
1659
|
-
} catch (
|
|
1660
|
-
if (
|
|
2604
|
+
runRestrictedScript(code, sandbox, { timeoutMs: timeout });
|
|
2605
|
+
} catch (err2) {
|
|
2606
|
+
if (err2 instanceof ScriptTimeoutError) {
|
|
1661
2607
|
throw new Error(`Build code execution timed out after ${timeout}ms`);
|
|
1662
2608
|
}
|
|
1663
|
-
throw new Error(`Build code execution error: ${
|
|
2609
|
+
throw new Error(`Build code execution error: ${err2?.message ?? String(err2)}`);
|
|
1664
2610
|
}
|
|
1665
2611
|
if (parts.length === 0) {
|
|
1666
2612
|
throw new Error("Build code produced no parts. Make sure to call part(), wall(), floor(), etc.");
|
|
@@ -1672,6 +2618,7 @@ var DEFAULT_TIMEOUT, DEFAULT_MAX_PARTS, VALID_SHAPES;
|
|
|
1672
2618
|
var init_build_executor = __esm({
|
|
1673
2619
|
"../core/dist/tools/build-executor.js"() {
|
|
1674
2620
|
"use strict";
|
|
2621
|
+
init_build_interpreter();
|
|
1675
2622
|
DEFAULT_TIMEOUT = 1e4;
|
|
1676
2623
|
DEFAULT_MAX_PARTS = 1e4;
|
|
1677
2624
|
VALID_SHAPES = /* @__PURE__ */ new Set(["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"]);
|
|
@@ -1918,7 +2865,7 @@ var init_opencloud_client = __esm({
|
|
|
1918
2865
|
if (result.error) {
|
|
1919
2866
|
throw new Error(`Asset upload failed: ${result.error.message}`);
|
|
1920
2867
|
}
|
|
1921
|
-
await new Promise((
|
|
2868
|
+
await new Promise((resolve5) => setTimeout(resolve5, intervalMs));
|
|
1922
2869
|
}
|
|
1923
2870
|
throw new Error(`Asset upload timed out after ${maxAttempts * intervalMs / 1e3}s. Operation ID: ${operationId}`);
|
|
1924
2871
|
}
|
|
@@ -2039,7 +2986,7 @@ function isRecord(value) {
|
|
|
2039
2986
|
const record = value;
|
|
2040
2987
|
return record.version === REGISTRY_VERSION && typeof record.recordId === "string" && typeof record.source === "string" && typeof record.exe === "string" && Array.isArray(record.args) && typeof record.launchedAt === "number" && typeof record.bootId === "string";
|
|
2041
2988
|
}
|
|
2042
|
-
var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, ManagedInstanceRegistry;
|
|
2989
|
+
var REGISTRY_VERSION, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, EVENT_RETENTION_DAYS, TERMINAL_RECORD_RETENTION_MS, ManagedInstanceRegistry;
|
|
2043
2990
|
var init_managed_instance_registry = __esm({
|
|
2044
2991
|
"../core/dist/managed-instance-registry.js"() {
|
|
2045
2992
|
"use strict";
|
|
@@ -2048,6 +2995,7 @@ var init_managed_instance_registry = __esm({
|
|
|
2048
2995
|
LOCK_RETRY_MS = 25;
|
|
2049
2996
|
LOCK_TIMEOUT_MS = 5e3;
|
|
2050
2997
|
EVENT_RETENTION_DAYS = 2;
|
|
2998
|
+
TERMINAL_RECORD_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
2051
2999
|
ManagedInstanceRegistry = class {
|
|
2052
3000
|
dir;
|
|
2053
3001
|
constructor(dir = defaultManagedInstanceRegistryDir()) {
|
|
@@ -2075,12 +3023,21 @@ var init_managed_instance_registry = __esm({
|
|
|
2075
3023
|
findAnyByInstanceId(instanceId) {
|
|
2076
3024
|
return this.withLock(() => this.readRecordsUnlocked().find((record) => record.instanceId === instanceId));
|
|
2077
3025
|
}
|
|
3026
|
+
findAnyByRecordId(recordId, options) {
|
|
3027
|
+
return this.withLock(() => {
|
|
3028
|
+
this.sweepUnlocked(options);
|
|
3029
|
+
return this.readRecordsUnlocked().find((record) => record.recordId === recordId);
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
2078
3032
|
listOpen(options) {
|
|
2079
3033
|
return this.withLock(() => {
|
|
2080
3034
|
this.sweepUnlocked(options);
|
|
2081
3035
|
return this.readOpenRecordsUnlocked();
|
|
2082
3036
|
});
|
|
2083
3037
|
}
|
|
3038
|
+
listOpenUnchecked() {
|
|
3039
|
+
return this.withLock(() => this.readOpenRecordsUnlocked());
|
|
3040
|
+
}
|
|
2084
3041
|
markClosed(recordId, closedAt = Date.now()) {
|
|
2085
3042
|
this.withLock(() => {
|
|
2086
3043
|
const record = this.readRecordUnlocked(recordId);
|
|
@@ -2252,41 +3209,52 @@ var init_managed_instance_registry = __esm({
|
|
|
2252
3209
|
}, now);
|
|
2253
3210
|
continue;
|
|
2254
3211
|
}
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
3212
|
+
const terminalAt = parsed.closedAt ?? parsed.exitedAt;
|
|
3213
|
+
if (terminalAt !== void 0) {
|
|
3214
|
+
if (now - terminalAt > TERMINAL_RECORD_RETENTION_MS) {
|
|
3215
|
+
fs.rmSync(file, { force: true });
|
|
3216
|
+
this.appendEventUnlocked({
|
|
3217
|
+
event: "registry_pruned_terminal_record",
|
|
3218
|
+
recordId: parsed.recordId,
|
|
3219
|
+
instanceId: parsed.instanceId,
|
|
3220
|
+
source: parsed.source,
|
|
3221
|
+
reason: "terminal_retention_expired",
|
|
3222
|
+
action: "deleted_record"
|
|
3223
|
+
}, now);
|
|
3224
|
+
}
|
|
2265
3225
|
continue;
|
|
2266
3226
|
}
|
|
2267
3227
|
if (parsed.bootId !== options.currentBootId) {
|
|
2268
3228
|
this.cleanupRecord(options, parsed);
|
|
2269
|
-
|
|
3229
|
+
parsed.state = parsed.state === "failed" ? "failed" : "exited";
|
|
3230
|
+
parsed.exitedAt = now;
|
|
3231
|
+
parsed.closedAt = now;
|
|
3232
|
+
parsed.failureReason ??= "Studio process belongs to a previous host boot.";
|
|
3233
|
+
this.writeRecordUnlocked(parsed);
|
|
2270
3234
|
this.appendEventUnlocked({
|
|
2271
|
-
event: "
|
|
3235
|
+
event: "registry_marked_previous_boot_exited",
|
|
2272
3236
|
recordId: parsed.recordId,
|
|
2273
3237
|
instanceId: parsed.instanceId,
|
|
2274
3238
|
source: parsed.source,
|
|
2275
3239
|
reason: "boot_id_changed",
|
|
2276
|
-
action: "
|
|
3240
|
+
action: "marked_exited_and_cleaned_baseplate"
|
|
2277
3241
|
}, now);
|
|
2278
3242
|
continue;
|
|
2279
3243
|
}
|
|
2280
3244
|
if (options.isProcessRunning && (parsed.nativeProcessId || parsed.spawnPid) && !options.isProcessRunning(parsed)) {
|
|
2281
3245
|
this.cleanupRecord(options, parsed);
|
|
2282
|
-
|
|
3246
|
+
parsed.state = parsed.state === "failed" ? "failed" : "exited";
|
|
3247
|
+
parsed.exitedAt = now;
|
|
3248
|
+
parsed.closedAt = now;
|
|
3249
|
+
parsed.failureReason ??= parsed.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.";
|
|
3250
|
+
this.writeRecordUnlocked(parsed);
|
|
2283
3251
|
this.appendEventUnlocked({
|
|
2284
|
-
event: "
|
|
3252
|
+
event: "registry_marked_process_exited",
|
|
2285
3253
|
recordId: parsed.recordId,
|
|
2286
3254
|
instanceId: parsed.instanceId,
|
|
2287
3255
|
source: parsed.source,
|
|
2288
3256
|
reason: "pid_not_running",
|
|
2289
|
-
action: "
|
|
3257
|
+
action: "marked_exited_and_cleaned_baseplate"
|
|
2290
3258
|
}, now);
|
|
2291
3259
|
}
|
|
2292
3260
|
}
|
|
@@ -2297,7 +3265,7 @@ var init_managed_instance_registry = __esm({
|
|
|
2297
3265
|
|
|
2298
3266
|
// ../core/dist/studio-instance-manager.js
|
|
2299
3267
|
import { execFileSync, spawn } from "child_process";
|
|
2300
|
-
import { copyFileSync, existsSync, mkdirSync as
|
|
3268
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
2301
3269
|
import { randomUUID } from "crypto";
|
|
2302
3270
|
import * as os2 from "os";
|
|
2303
3271
|
import * as path2 from "path";
|
|
@@ -2312,14 +3280,14 @@ function isWsl() {
|
|
|
2312
3280
|
if (process.platform !== "linux")
|
|
2313
3281
|
return false;
|
|
2314
3282
|
try {
|
|
2315
|
-
return /microsoft|wsl/i.test(
|
|
3283
|
+
return /microsoft|wsl/i.test(readFileSync3("/proc/version", "utf8"));
|
|
2316
3284
|
} catch {
|
|
2317
3285
|
return false;
|
|
2318
3286
|
}
|
|
2319
3287
|
}
|
|
2320
3288
|
function powershell(script) {
|
|
2321
3289
|
return run("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
2322
|
-
cwd: isWsl() &&
|
|
3290
|
+
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
2323
3291
|
});
|
|
2324
3292
|
}
|
|
2325
3293
|
function windowsLocalAppData() {
|
|
@@ -2329,7 +3297,7 @@ function windowsLocalAppData() {
|
|
|
2329
3297
|
return void 0;
|
|
2330
3298
|
try {
|
|
2331
3299
|
return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
|
|
2332
|
-
cwd:
|
|
3300
|
+
cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
2333
3301
|
});
|
|
2334
3302
|
} catch {
|
|
2335
3303
|
return void 0;
|
|
@@ -2341,10 +3309,67 @@ function toWslPath(windowsPath) {
|
|
|
2341
3309
|
return run("wslpath", ["-u", windowsPath]);
|
|
2342
3310
|
}
|
|
2343
3311
|
function toStudioLaunchArg(arg) {
|
|
2344
|
-
if (!isWsl() || !path2.isAbsolute(arg) || !
|
|
3312
|
+
if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
|
|
2345
3313
|
return arg;
|
|
2346
3314
|
return run("wslpath", ["-w", arg]);
|
|
2347
3315
|
}
|
|
3316
|
+
function powershellStringLiteral(value) {
|
|
3317
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
3318
|
+
}
|
|
3319
|
+
function quoteWindowsCommandLineArg(value) {
|
|
3320
|
+
if (value.length > 0 && !/[\s"]/u.test(value))
|
|
3321
|
+
return value;
|
|
3322
|
+
let quoted = '"';
|
|
3323
|
+
let backslashes = 0;
|
|
3324
|
+
for (const char of value) {
|
|
3325
|
+
if (char === "\\") {
|
|
3326
|
+
backslashes += 1;
|
|
3327
|
+
continue;
|
|
3328
|
+
}
|
|
3329
|
+
if (char === '"') {
|
|
3330
|
+
quoted += "\\".repeat(backslashes * 2 + 1) + '"';
|
|
3331
|
+
backslashes = 0;
|
|
3332
|
+
continue;
|
|
3333
|
+
}
|
|
3334
|
+
quoted += "\\".repeat(backslashes) + char;
|
|
3335
|
+
backslashes = 0;
|
|
3336
|
+
}
|
|
3337
|
+
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
3338
|
+
}
|
|
3339
|
+
function buildWindowsStudioStartScript(exe, args) {
|
|
3340
|
+
const windowsExe = toStudioLaunchArg(exe);
|
|
3341
|
+
const commandLine = args.map(quoteWindowsCommandLineArg).join(" ");
|
|
3342
|
+
return [
|
|
3343
|
+
"$psi = New-Object System.Diagnostics.ProcessStartInfo",
|
|
3344
|
+
`$psi.FileName = ${powershellStringLiteral(windowsExe)}`,
|
|
3345
|
+
`$psi.Arguments = ${powershellStringLiteral(commandLine)}`,
|
|
3346
|
+
// With UseShellExecute=false, Studio inherits the synchronous
|
|
3347
|
+
// powershell.exe invocation's stdout/stderr pipe handles under WSL. Those
|
|
3348
|
+
// handles keep execFileSync waiting until Studio exits even though
|
|
3349
|
+
// PowerShell already printed the PID. Shell execution prevents that
|
|
3350
|
+
// inheritance while Process.Start still returns the native Studio PID.
|
|
3351
|
+
"$psi.UseShellExecute = $true",
|
|
3352
|
+
"$studio = [System.Diagnostics.Process]::Start($psi)",
|
|
3353
|
+
'if ($null -eq $studio) { throw "Roblox Studio process did not start." }'
|
|
3354
|
+
].join("; ");
|
|
3355
|
+
}
|
|
3356
|
+
function spawnWindowsStudioFromWsl(exe, args) {
|
|
3357
|
+
const script = buildWindowsStudioStartScript(exe, args);
|
|
3358
|
+
const output = powershell(`${script}; [PSCustomObject]@{ pid = $studio.Id; started = $studio.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } | ConvertTo-Json -Compress`);
|
|
3359
|
+
const parsed = JSON.parse(output);
|
|
3360
|
+
const nativePid = Number(parsed.pid);
|
|
3361
|
+
const nativeStartedAt = typeof parsed.started === "string" && /^\d+$/u.test(parsed.started) ? parsed.started : void 0;
|
|
3362
|
+
if (!Number.isSafeInteger(nativePid) || nativePid <= 0) {
|
|
3363
|
+
throw new Error(`Could not determine the Windows Studio process id from: ${nativePid}`);
|
|
3364
|
+
}
|
|
3365
|
+
return {
|
|
3366
|
+
pid: nativePid,
|
|
3367
|
+
nativePid,
|
|
3368
|
+
nativeStartedAt,
|
|
3369
|
+
unref: () => {
|
|
3370
|
+
}
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
2348
3373
|
function resolveEntrypointDir() {
|
|
2349
3374
|
const entrypoint = process.argv[1];
|
|
2350
3375
|
if (!entrypoint)
|
|
@@ -2367,13 +3392,44 @@ function resolveBaseplateTemplatePath() {
|
|
|
2367
3392
|
path2.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
|
|
2368
3393
|
];
|
|
2369
3394
|
for (const candidate of candidates) {
|
|
2370
|
-
if (
|
|
3395
|
+
if (existsSync2(candidate))
|
|
2371
3396
|
return candidate;
|
|
2372
3397
|
}
|
|
2373
3398
|
throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
|
|
2374
3399
|
}
|
|
3400
|
+
function isProcessAlive(pid) {
|
|
3401
|
+
try {
|
|
3402
|
+
process.kill(pid, 0);
|
|
3403
|
+
return true;
|
|
3404
|
+
} catch (err2) {
|
|
3405
|
+
return err2.code === "EPERM";
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
function sweepStaleBaseplateFiles() {
|
|
3409
|
+
let entries;
|
|
3410
|
+
try {
|
|
3411
|
+
entries = readdirSync2(BASEPLATE_TEMP_DIR);
|
|
3412
|
+
} catch {
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
const cutoff = Date.now() - STALE_BASEPLATE_MAX_AGE_MS;
|
|
3416
|
+
for (const entry of entries) {
|
|
3417
|
+
const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
|
|
3418
|
+
if (!match)
|
|
3419
|
+
continue;
|
|
3420
|
+
if (Number(match[1]) !== process.pid && isProcessAlive(Number(match[1])))
|
|
3421
|
+
continue;
|
|
3422
|
+
const file = path2.join(BASEPLATE_TEMP_DIR, entry);
|
|
3423
|
+
try {
|
|
3424
|
+
if (statSync2(file).mtimeMs < cutoff)
|
|
3425
|
+
rmSync2(file, { force: true });
|
|
3426
|
+
} catch {
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
2375
3430
|
function createBaseplatePlaceFile() {
|
|
2376
|
-
|
|
3431
|
+
mkdirSync3(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
3432
|
+
sweepStaleBaseplateFiles();
|
|
2377
3433
|
const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
|
|
2378
3434
|
copyFileSync(resolveBaseplateTemplatePath(), file);
|
|
2379
3435
|
return file;
|
|
@@ -2387,8 +3443,12 @@ function cleanupManagedBaseplateFiles(record) {
|
|
|
2387
3443
|
return;
|
|
2388
3444
|
if (!isGeneratedBaseplatePlaceFile(record.localPlaceFile))
|
|
2389
3445
|
return;
|
|
2390
|
-
|
|
2391
|
-
|
|
3446
|
+
for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
|
|
3447
|
+
try {
|
|
3448
|
+
rmSync2(file, { force: true });
|
|
3449
|
+
} catch {
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
2392
3452
|
}
|
|
2393
3453
|
function prepareStudioLaunchOptions(options) {
|
|
2394
3454
|
if (options.source !== "baseplate" || options.localPlaceFile)
|
|
@@ -2409,10 +3469,10 @@ function resolveStudioExe() {
|
|
|
2409
3469
|
}
|
|
2410
3470
|
const localAppData = windowsLocalAppData();
|
|
2411
3471
|
const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
2412
|
-
if (!
|
|
3472
|
+
if (!existsSync2(root)) {
|
|
2413
3473
|
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
2414
3474
|
}
|
|
2415
|
-
const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) =>
|
|
3475
|
+
const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs);
|
|
2416
3476
|
if (candidates.length === 0) {
|
|
2417
3477
|
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
2418
3478
|
}
|
|
@@ -2435,7 +3495,7 @@ function listStudioProcesses() {
|
|
|
2435
3495
|
return [];
|
|
2436
3496
|
let out = "";
|
|
2437
3497
|
try {
|
|
2438
|
-
out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue |
|
|
3498
|
+
out = powershell("Get-Process RobloxStudioBeta -ErrorAction SilentlyContinue | ForEach-Object { [PSCustomObject]@{ Id = $_.Id; Name = $_.Name; Path = $_.Path; MainWindowTitle = $_.MainWindowTitle; StartTimeUtcFileTime = $_.StartTime.ToUniversalTime().ToFileTimeUtc().ToString() } } | ConvertTo-Json -Compress");
|
|
2439
3499
|
} catch {
|
|
2440
3500
|
return [];
|
|
2441
3501
|
}
|
|
@@ -2447,7 +3507,7 @@ function listStudioProcesses() {
|
|
|
2447
3507
|
function currentBootId() {
|
|
2448
3508
|
if (process.platform === "linux") {
|
|
2449
3509
|
try {
|
|
2450
|
-
return
|
|
3510
|
+
return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
2451
3511
|
} catch {
|
|
2452
3512
|
}
|
|
2453
3513
|
}
|
|
@@ -2499,12 +3559,12 @@ function buildStudioLaunchArgs(options) {
|
|
|
2499
3559
|
}
|
|
2500
3560
|
}
|
|
2501
3561
|
function delay(ms) {
|
|
2502
|
-
return new Promise((
|
|
3562
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2503
3563
|
}
|
|
2504
3564
|
function basenameAny(filePath) {
|
|
2505
3565
|
return path2.basename(filePath.replace(/\\/g, "/"));
|
|
2506
3566
|
}
|
|
2507
|
-
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, StudioInstanceManager;
|
|
3567
|
+
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
|
|
2508
3568
|
var init_studio_instance_manager = __esm({
|
|
2509
3569
|
"../core/dist/studio-instance-manager.js"() {
|
|
2510
3570
|
"use strict";
|
|
@@ -2512,9 +3572,13 @@ var init_studio_instance_manager = __esm({
|
|
|
2512
3572
|
BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
2513
3573
|
BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
2514
3574
|
BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
3575
|
+
STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3576
|
+
BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
|
|
2515
3577
|
StudioInstanceManager = class {
|
|
2516
3578
|
managedByInstanceId = /* @__PURE__ */ new Map();
|
|
2517
3579
|
pending = /* @__PURE__ */ new Set();
|
|
3580
|
+
monitors = /* @__PURE__ */ new Map();
|
|
3581
|
+
connectionTimers = /* @__PURE__ */ new Map();
|
|
2518
3582
|
registry;
|
|
2519
3583
|
processAdapter;
|
|
2520
3584
|
constructor(options = {}) {
|
|
@@ -2523,6 +3587,9 @@ var init_studio_instance_manager = __esm({
|
|
|
2523
3587
|
}
|
|
2524
3588
|
list() {
|
|
2525
3589
|
this.sweepRegistry();
|
|
3590
|
+
for (const record of [...this.managedByInstanceId.values(), ...this.pending]) {
|
|
3591
|
+
this.refresh(record);
|
|
3592
|
+
}
|
|
2526
3593
|
const records = [...this.managedByInstanceId.values(), ...this.pending];
|
|
2527
3594
|
for (const registryRecord of this.registry.listOpen(this.registrySweepOptions())) {
|
|
2528
3595
|
const record = this.fromRegistryRecord(registryRecord);
|
|
@@ -2531,40 +3598,119 @@ var init_studio_instance_manager = __esm({
|
|
|
2531
3598
|
}
|
|
2532
3599
|
records.push(record);
|
|
2533
3600
|
}
|
|
2534
|
-
return records.filter((instance, index, all) => all.indexOf(instance) === index);
|
|
3601
|
+
return records.filter((record) => record.closedAt === void 0).filter((instance, index, all) => all.indexOf(instance) === index);
|
|
2535
3602
|
}
|
|
2536
3603
|
get(instanceId) {
|
|
2537
3604
|
this.sweepRegistry();
|
|
2538
3605
|
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
2539
3606
|
if (memoryRecord)
|
|
2540
|
-
return memoryRecord;
|
|
2541
|
-
const registryRecord = this.registry.
|
|
2542
|
-
return registryRecord ? this.fromRegistryRecord(registryRecord) : void 0;
|
|
3607
|
+
return this.refresh(memoryRecord);
|
|
3608
|
+
const registryRecord = this.registry.findAnyByInstanceId(instanceId);
|
|
3609
|
+
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
|
|
3610
|
+
}
|
|
3611
|
+
getByLaunchId(launchId) {
|
|
3612
|
+
this.sweepRegistry();
|
|
3613
|
+
const memoryRecord = [...this.managedByInstanceId.values(), ...this.pending].find((record) => record.recordId === launchId);
|
|
3614
|
+
if (memoryRecord)
|
|
3615
|
+
return this.refresh(memoryRecord);
|
|
3616
|
+
const registryRecord = this.registry.findAnyByRecordId(launchId, this.registrySweepOptions());
|
|
3617
|
+
return registryRecord ? this.refresh(this.fromRegistryRecord(registryRecord)) : void 0;
|
|
3618
|
+
}
|
|
3619
|
+
pendingLaunches() {
|
|
3620
|
+
const now = Date.now();
|
|
3621
|
+
const records = [...this.pending];
|
|
3622
|
+
for (const registryRecord of this.registry.listOpenUnchecked()) {
|
|
3623
|
+
if (registryRecord.bootId !== this.getCurrentBootId())
|
|
3624
|
+
continue;
|
|
3625
|
+
if (records.some((record) => record.recordId === registryRecord.recordId))
|
|
3626
|
+
continue;
|
|
3627
|
+
records.push(this.fromRegistryRecord(registryRecord));
|
|
3628
|
+
}
|
|
3629
|
+
return records.filter((record) => record.instanceId === void 0).filter((record) => record.state === "launching").filter((record) => record.connectionDeadlineAt === void 0 || record.connectionDeadlineAt > now);
|
|
2543
3630
|
}
|
|
2544
3631
|
attachInstanceId(record, instanceId) {
|
|
3632
|
+
this.refresh(record);
|
|
3633
|
+
if (record.closedAt !== void 0 || record.state === "failed" || record.state === "exited")
|
|
3634
|
+
return;
|
|
3635
|
+
if (record.instanceId && record.instanceId !== instanceId)
|
|
3636
|
+
return;
|
|
2545
3637
|
record.instanceId = instanceId;
|
|
3638
|
+
record.state = "connected";
|
|
3639
|
+
record.connectedAt = record.connectedAt ?? Date.now();
|
|
3640
|
+
this.clearConnectionTimer(record);
|
|
2546
3641
|
this.pending.delete(record);
|
|
2547
3642
|
this.managedByInstanceId.set(instanceId, record);
|
|
2548
|
-
|
|
2549
|
-
|
|
3643
|
+
this.persist(record);
|
|
3644
|
+
}
|
|
3645
|
+
markFailed(record, reason) {
|
|
3646
|
+
this.refresh(record);
|
|
3647
|
+
if (record.closedAt !== void 0 || record.state !== "launching")
|
|
3648
|
+
return record;
|
|
3649
|
+
record.state = "failed";
|
|
3650
|
+
record.failedAt = Date.now();
|
|
3651
|
+
record.failureReason = reason;
|
|
3652
|
+
this.clearConnectionTimer(record);
|
|
3653
|
+
this.persist(record);
|
|
3654
|
+
return record;
|
|
3655
|
+
}
|
|
3656
|
+
refresh(record) {
|
|
3657
|
+
if (record.closedAt !== void 0)
|
|
3658
|
+
return record;
|
|
3659
|
+
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
3660
|
+
const studioProcess = processId ? this.findProcessById(processId) : void 0;
|
|
3661
|
+
if (processId && (!studioProcess || !this.verifyProcessForRecord(record, studioProcess))) {
|
|
3662
|
+
return this.markProcessExited(record, void 0, studioProcess ? "Studio process identity changed; the retained PID was not reused." : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
|
|
3663
|
+
}
|
|
3664
|
+
if (record.state === "launching" && record.connectionDeadlineAt !== void 0 && Date.now() >= record.connectionDeadlineAt) {
|
|
3665
|
+
record.state = "failed";
|
|
3666
|
+
record.failedAt = Date.now();
|
|
3667
|
+
record.failureReason = "Studio launched, but the MCP plugin did not connect before timeout.";
|
|
3668
|
+
this.clearConnectionTimer(record);
|
|
3669
|
+
this.persist(record);
|
|
2550
3670
|
}
|
|
3671
|
+
return record;
|
|
2551
3672
|
}
|
|
2552
3673
|
async launch(options) {
|
|
2553
3674
|
this.sweepRegistry();
|
|
2554
3675
|
const preparedOptions = prepareStudioLaunchOptions(options);
|
|
3676
|
+
const bootId = this.getCurrentBootId();
|
|
2555
3677
|
const before = new Set(this.listStudioProcesses().map((proc2) => proc2.Id));
|
|
2556
3678
|
const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
|
|
2557
3679
|
const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
|
|
2558
3680
|
const spawnOptions = {
|
|
2559
|
-
cwd: isWsl() &&
|
|
3681
|
+
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
2560
3682
|
detached: true,
|
|
2561
3683
|
stdio: "ignore"
|
|
2562
3684
|
};
|
|
2563
|
-
|
|
2564
|
-
|
|
3685
|
+
let proc;
|
|
3686
|
+
try {
|
|
3687
|
+
if (this.processAdapter.spawnStudio) {
|
|
3688
|
+
proc = this.processAdapter.spawnStudio(exe, args, spawnOptions);
|
|
3689
|
+
} else if (isWsl()) {
|
|
3690
|
+
proc = spawnWindowsStudioFromWsl(exe, args);
|
|
3691
|
+
} else {
|
|
3692
|
+
const child = spawn(exe, args, spawnOptions);
|
|
3693
|
+
proc = {
|
|
3694
|
+
pid: child.pid,
|
|
3695
|
+
nativePid: child.pid,
|
|
3696
|
+
unref: () => child.unref(),
|
|
3697
|
+
onExit: (listener) => {
|
|
3698
|
+
child.once("exit", listener);
|
|
3699
|
+
},
|
|
3700
|
+
onError: (listener) => {
|
|
3701
|
+
child.once("error", listener);
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
} catch (error) {
|
|
3706
|
+
cleanupManagedBaseplateFiles({ source: preparedOptions.source, localPlaceFile: preparedOptions.localPlaceFile });
|
|
3707
|
+
throw error;
|
|
3708
|
+
}
|
|
2565
3709
|
const record = {
|
|
2566
3710
|
recordId: randomUUID(),
|
|
2567
3711
|
source: options.source,
|
|
3712
|
+
nativeProcessId: proc.nativePid,
|
|
3713
|
+
nativeProcessStartedAt: proc.nativeStartedAt,
|
|
2568
3714
|
spawnPid: proc.pid,
|
|
2569
3715
|
exe,
|
|
2570
3716
|
args,
|
|
@@ -2573,29 +3719,74 @@ var init_studio_instance_manager = __esm({
|
|
|
2573
3719
|
placeVersion: preparedOptions.placeVersion,
|
|
2574
3720
|
localPlaceFile: preparedOptions.localPlaceFile,
|
|
2575
3721
|
launchedAt: Date.now(),
|
|
3722
|
+
connectionDeadlineAt: Date.now() + (options.connectionTimeoutMs ?? 12e4),
|
|
3723
|
+
state: "launching",
|
|
2576
3724
|
ownerPid: process.pid,
|
|
2577
|
-
bootId
|
|
3725
|
+
bootId,
|
|
2578
3726
|
deleteLocalPlaceFileOnClose: options.source === "baseplate"
|
|
2579
3727
|
};
|
|
2580
3728
|
this.pending.add(record);
|
|
2581
|
-
|
|
3729
|
+
try {
|
|
3730
|
+
this.persist(record);
|
|
3731
|
+
} catch (error) {
|
|
3732
|
+
this.pending.delete(record);
|
|
3733
|
+
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
3734
|
+
let stopError;
|
|
3735
|
+
if (processId) {
|
|
3736
|
+
try {
|
|
3737
|
+
this.closeProcess(processId);
|
|
3738
|
+
} catch (caught) {
|
|
3739
|
+
stopError = caught;
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
cleanupManagedBaseplateFiles(record);
|
|
3743
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
3744
|
+
const cleanupDetail = stopError ? ` The newly launched process could not be stopped: ${stopError instanceof Error ? stopError.message : String(stopError)}` : "";
|
|
3745
|
+
throw new Error(`Studio launched, but its managed-instance record could not be persisted: ${detail}.${cleanupDetail}`);
|
|
3746
|
+
}
|
|
3747
|
+
proc.unref();
|
|
3748
|
+
proc.onExit?.((code, signal) => {
|
|
3749
|
+
this.markProcessExited(record, code ?? void 0, signal ? `Studio process exited from signal ${signal}.` : record.instanceId ? "Studio process exited." : "Studio process exited before the MCP plugin connected.");
|
|
3750
|
+
});
|
|
3751
|
+
proc.onError?.((error) => {
|
|
3752
|
+
this.markFailed(record, `Studio process failed to start: ${error.message}`);
|
|
3753
|
+
});
|
|
2582
3754
|
const deadline = Date.now() + 5e3;
|
|
2583
3755
|
while (Date.now() < deadline && record.nativeProcessId === void 0) {
|
|
2584
3756
|
const created = this.listStudioProcesses().find((candidate) => !before.has(candidate.Id));
|
|
2585
3757
|
if (created) {
|
|
2586
3758
|
record.nativeProcessId = created.Id;
|
|
2587
|
-
|
|
3759
|
+
record.nativeProcessStartedAt = created.StartTimeUtcFileTime;
|
|
3760
|
+
this.persist(record);
|
|
2588
3761
|
break;
|
|
2589
3762
|
}
|
|
2590
3763
|
await delay(250);
|
|
2591
3764
|
}
|
|
2592
3765
|
if (record.nativeProcessId === void 0 && process.platform !== "win32" && !isWsl()) {
|
|
2593
3766
|
record.nativeProcessId = proc.pid;
|
|
2594
|
-
this.
|
|
3767
|
+
this.persist(record);
|
|
3768
|
+
}
|
|
3769
|
+
if (record.nativeProcessId !== void 0 && record.nativeProcessStartedAt === void 0) {
|
|
3770
|
+
const nativeProcess = this.findProcessById(record.nativeProcessId);
|
|
3771
|
+
if (nativeProcess?.StartTimeUtcFileTime !== void 0) {
|
|
3772
|
+
record.nativeProcessStartedAt = nativeProcess.StartTimeUtcFileTime;
|
|
3773
|
+
this.persist(record);
|
|
3774
|
+
}
|
|
2595
3775
|
}
|
|
3776
|
+
this.startMonitor(record);
|
|
2596
3777
|
return record;
|
|
2597
3778
|
}
|
|
3779
|
+
closeByLaunchId(launchId) {
|
|
3780
|
+
const record = this.getByLaunchId(launchId);
|
|
3781
|
+
if (!record)
|
|
3782
|
+
return { status: "not_found", launchId };
|
|
3783
|
+
if (record.closedAt !== void 0) {
|
|
3784
|
+
return { status: "already_closed", launchId, instanceId: record.instanceId };
|
|
3785
|
+
}
|
|
3786
|
+
return this.close(record);
|
|
3787
|
+
}
|
|
2598
3788
|
closeByInstanceId(instanceId) {
|
|
3789
|
+
this.sweepRegistry();
|
|
2599
3790
|
const memoryRecord = this.managedByInstanceId.get(instanceId);
|
|
2600
3791
|
if (memoryRecord)
|
|
2601
3792
|
return this.close(memoryRecord);
|
|
@@ -2606,33 +3797,24 @@ var init_studio_instance_manager = __esm({
|
|
|
2606
3797
|
}
|
|
2607
3798
|
if (registryRecord.closedAt !== void 0) {
|
|
2608
3799
|
this.cleanupManagedRecord(registryRecord);
|
|
2609
|
-
this.registry.delete(registryRecord.recordId);
|
|
2610
3800
|
this.registry.logEvent({
|
|
2611
3801
|
event: "registry_close_already_stopped",
|
|
2612
3802
|
recordId: registryRecord.recordId,
|
|
2613
3803
|
instanceId: registryRecord.instanceId,
|
|
2614
3804
|
source: registryRecord.source,
|
|
2615
3805
|
reason: "closed_at_present",
|
|
2616
|
-
action: "
|
|
2617
|
-
});
|
|
2618
|
-
return { status: "already_closed", instanceId };
|
|
2619
|
-
}
|
|
2620
|
-
if (registryRecord.bootId !== this.getCurrentBootId()) {
|
|
2621
|
-
this.cleanupManagedRecord(registryRecord);
|
|
2622
|
-
this.registry.delete(registryRecord.recordId);
|
|
2623
|
-
this.registry.logEvent({
|
|
2624
|
-
event: "registry_pruned_previous_boot",
|
|
2625
|
-
recordId: registryRecord.recordId,
|
|
2626
|
-
instanceId: registryRecord.instanceId,
|
|
2627
|
-
source: registryRecord.source,
|
|
2628
|
-
reason: "boot_id_changed",
|
|
2629
|
-
action: "deleted_record_and_cleaned_baseplate"
|
|
3806
|
+
action: "retained_terminal_record_and_cleaned_baseplate"
|
|
2630
3807
|
});
|
|
2631
|
-
return { status: "already_closed", instanceId };
|
|
3808
|
+
return { status: "already_closed", launchId: registryRecord.recordId, instanceId };
|
|
2632
3809
|
}
|
|
2633
3810
|
return this.close(this.fromRegistryRecord(registryRecord));
|
|
2634
3811
|
}
|
|
2635
3812
|
close(record) {
|
|
3813
|
+
this.stopMonitor(record);
|
|
3814
|
+
this.refresh(record);
|
|
3815
|
+
if (record.closedAt !== void 0) {
|
|
3816
|
+
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
3817
|
+
}
|
|
2636
3818
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
2637
3819
|
if (!processId) {
|
|
2638
3820
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
@@ -2640,8 +3822,7 @@ var init_studio_instance_manager = __esm({
|
|
|
2640
3822
|
const studioProcess = this.findProcessById(processId);
|
|
2641
3823
|
if (!studioProcess) {
|
|
2642
3824
|
this.cleanupManagedRecord(record);
|
|
2643
|
-
this.
|
|
2644
|
-
this.markClosedInRegistry(record);
|
|
3825
|
+
this.markProcessExited(record, void 0, record.failureReason);
|
|
2645
3826
|
this.registry.logEvent({
|
|
2646
3827
|
event: "registry_close_already_stopped",
|
|
2647
3828
|
recordId: record.recordId,
|
|
@@ -2650,7 +3831,7 @@ var init_studio_instance_manager = __esm({
|
|
|
2650
3831
|
reason: "pid_not_running",
|
|
2651
3832
|
action: "marked_closed_and_cleaned_baseplate"
|
|
2652
3833
|
});
|
|
2653
|
-
return { status: "already_closed", instanceId: record.instanceId };
|
|
3834
|
+
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
2654
3835
|
}
|
|
2655
3836
|
if (!this.verifyProcessForRecord(record, studioProcess)) {
|
|
2656
3837
|
this.registry.logEvent({
|
|
@@ -2676,15 +3857,18 @@ var init_studio_instance_manager = __esm({
|
|
|
2676
3857
|
action: "marked_closed_and_cleaned_baseplate"
|
|
2677
3858
|
});
|
|
2678
3859
|
this.cleanupManagedRecord(record);
|
|
2679
|
-
this.
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
record.closedAt =
|
|
3860
|
+
this.markProcessExited(record, void 0, record.failureReason);
|
|
3861
|
+
return { status: "already_closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
3862
|
+
}
|
|
3863
|
+
const closedAt = Date.now();
|
|
3864
|
+
record.closedAt = closedAt;
|
|
3865
|
+
record.exitedAt = record.exitedAt ?? closedAt;
|
|
3866
|
+
if (record.state !== "failed")
|
|
3867
|
+
record.state = "exited";
|
|
2684
3868
|
this.cleanupManagedRecord(record);
|
|
2685
3869
|
this.markClosedInMemory(record);
|
|
2686
|
-
this.
|
|
2687
|
-
return { status: "closed", instanceId: record.instanceId };
|
|
3870
|
+
this.persist(record);
|
|
3871
|
+
return { status: "closed", launchId: record.recordId, instanceId: record.instanceId };
|
|
2688
3872
|
}
|
|
2689
3873
|
closeConnectedInstance(instance) {
|
|
2690
3874
|
const process2 = this.findProcessForConnectedInstance(instance);
|
|
@@ -2759,6 +3943,9 @@ var init_studio_instance_manager = __esm({
|
|
|
2759
3943
|
const processName = `${studioProcess.Name ?? ""} ${studioProcess.Path ?? ""}`.toLowerCase();
|
|
2760
3944
|
if (!processName.includes("robloxstudio"))
|
|
2761
3945
|
return false;
|
|
3946
|
+
if (record.nativeProcessStartedAt !== void 0 && studioProcess.StartTimeUtcFileTime !== record.nativeProcessStartedAt) {
|
|
3947
|
+
return false;
|
|
3948
|
+
}
|
|
2762
3949
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
2763
3950
|
if (record.spawnPid && record.spawnPid === processId && studioProcess.Id === processId)
|
|
2764
3951
|
return true;
|
|
@@ -2784,10 +3971,64 @@ var init_studio_instance_manager = __esm({
|
|
|
2784
3971
|
if (record.instanceId)
|
|
2785
3972
|
this.managedByInstanceId.delete(record.instanceId);
|
|
2786
3973
|
this.pending.delete(record);
|
|
3974
|
+
this.stopMonitor(record);
|
|
3975
|
+
}
|
|
3976
|
+
markProcessExited(record, exitCode, reason) {
|
|
3977
|
+
if (record.closedAt !== void 0)
|
|
3978
|
+
return record;
|
|
3979
|
+
const exitedAt = Date.now();
|
|
3980
|
+
record.exitedAt = exitedAt;
|
|
3981
|
+
record.closedAt = exitedAt;
|
|
3982
|
+
if (record.state !== "failed")
|
|
3983
|
+
record.state = "exited";
|
|
3984
|
+
if (exitCode !== void 0)
|
|
3985
|
+
record.exitCode = exitCode;
|
|
3986
|
+
if (reason)
|
|
3987
|
+
record.failureReason = reason;
|
|
3988
|
+
this.cleanupManagedRecord(record);
|
|
3989
|
+
this.markClosedInMemory(record);
|
|
3990
|
+
this.persist(record);
|
|
3991
|
+
return record;
|
|
3992
|
+
}
|
|
3993
|
+
startMonitor(record) {
|
|
3994
|
+
if (!record.recordId || record.closedAt !== void 0 || this.monitors.has(record.recordId))
|
|
3995
|
+
return;
|
|
3996
|
+
if (record.state === "launching" && record.connectionDeadlineAt !== void 0) {
|
|
3997
|
+
const timeout = setTimeout(() => {
|
|
3998
|
+
this.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
3999
|
+
}, Math.max(0, record.connectionDeadlineAt - Date.now()));
|
|
4000
|
+
if (typeof timeout === "object" && "unref" in timeout)
|
|
4001
|
+
timeout.unref();
|
|
4002
|
+
this.connectionTimers.set(record.recordId, timeout);
|
|
4003
|
+
}
|
|
4004
|
+
const timer = setInterval(() => {
|
|
4005
|
+
this.refresh(record);
|
|
4006
|
+
if (record.closedAt !== void 0)
|
|
4007
|
+
this.stopMonitor(record);
|
|
4008
|
+
}, 5e3);
|
|
4009
|
+
if (typeof timer === "object" && "unref" in timer)
|
|
4010
|
+
timer.unref();
|
|
4011
|
+
this.monitors.set(record.recordId, timer);
|
|
2787
4012
|
}
|
|
2788
|
-
|
|
2789
|
-
if (record.recordId)
|
|
2790
|
-
|
|
4013
|
+
stopMonitor(record) {
|
|
4014
|
+
if (!record.recordId)
|
|
4015
|
+
return;
|
|
4016
|
+
const timer = this.monitors.get(record.recordId);
|
|
4017
|
+
if (timer)
|
|
4018
|
+
clearInterval(timer);
|
|
4019
|
+
this.monitors.delete(record.recordId);
|
|
4020
|
+
this.clearConnectionTimer(record);
|
|
4021
|
+
}
|
|
4022
|
+
clearConnectionTimer(record) {
|
|
4023
|
+
if (!record.recordId)
|
|
4024
|
+
return;
|
|
4025
|
+
const timer = this.connectionTimers.get(record.recordId);
|
|
4026
|
+
if (timer)
|
|
4027
|
+
clearTimeout(timer);
|
|
4028
|
+
this.connectionTimers.delete(record.recordId);
|
|
4029
|
+
}
|
|
4030
|
+
persist(record) {
|
|
4031
|
+
this.registry.upsert(this.toRegistryRecord(record));
|
|
2791
4032
|
}
|
|
2792
4033
|
toRegistryRecord(record) {
|
|
2793
4034
|
if (!record.recordId)
|
|
@@ -2800,6 +4041,7 @@ var init_studio_instance_manager = __esm({
|
|
|
2800
4041
|
instanceId: record.instanceId,
|
|
2801
4042
|
source: record.source,
|
|
2802
4043
|
nativeProcessId: record.nativeProcessId,
|
|
4044
|
+
nativeProcessStartedAt: record.nativeProcessStartedAt,
|
|
2803
4045
|
spawnPid: record.spawnPid,
|
|
2804
4046
|
exe: record.exe,
|
|
2805
4047
|
args: record.args,
|
|
@@ -2809,18 +4051,26 @@ var init_studio_instance_manager = __esm({
|
|
|
2809
4051
|
localPlaceFile: record.localPlaceFile,
|
|
2810
4052
|
deleteLocalPlaceFileOnClose: record.deleteLocalPlaceFileOnClose,
|
|
2811
4053
|
launchedAt: record.launchedAt,
|
|
2812
|
-
attachedAt: record.
|
|
4054
|
+
attachedAt: record.connectedAt,
|
|
4055
|
+
connectionDeadlineAt: record.connectionDeadlineAt,
|
|
4056
|
+
state: record.state,
|
|
4057
|
+
failedAt: record.failedAt,
|
|
4058
|
+
exitedAt: record.exitedAt,
|
|
4059
|
+
exitCode: record.exitCode,
|
|
4060
|
+
failureReason: record.failureReason,
|
|
2813
4061
|
closedAt: record.closedAt,
|
|
2814
4062
|
ownerPid: record.ownerPid,
|
|
2815
4063
|
bootId: record.bootId
|
|
2816
4064
|
};
|
|
2817
4065
|
}
|
|
2818
4066
|
fromRegistryRecord(record) {
|
|
4067
|
+
const state = record.state ?? (record.closedAt !== void 0 ? "exited" : record.instanceId ? "connected" : "launching");
|
|
2819
4068
|
return {
|
|
2820
4069
|
recordId: record.recordId,
|
|
2821
4070
|
source: record.source,
|
|
2822
4071
|
instanceId: record.instanceId,
|
|
2823
4072
|
nativeProcessId: record.nativeProcessId,
|
|
4073
|
+
nativeProcessStartedAt: record.nativeProcessStartedAt,
|
|
2824
4074
|
spawnPid: record.spawnPid,
|
|
2825
4075
|
exe: record.exe,
|
|
2826
4076
|
args: record.args,
|
|
@@ -2829,6 +4079,13 @@ var init_studio_instance_manager = __esm({
|
|
|
2829
4079
|
placeVersion: record.placeVersion,
|
|
2830
4080
|
localPlaceFile: record.localPlaceFile,
|
|
2831
4081
|
launchedAt: record.launchedAt,
|
|
4082
|
+
connectionDeadlineAt: record.connectionDeadlineAt ?? (state === "launching" ? record.launchedAt + 12e4 : void 0),
|
|
4083
|
+
state,
|
|
4084
|
+
connectedAt: record.attachedAt,
|
|
4085
|
+
failedAt: record.failedAt,
|
|
4086
|
+
exitedAt: record.exitedAt,
|
|
4087
|
+
exitCode: record.exitCode,
|
|
4088
|
+
failureReason: record.failureReason,
|
|
2832
4089
|
closedAt: record.closedAt,
|
|
2833
4090
|
ownerPid: record.ownerPid,
|
|
2834
4091
|
bootId: record.bootId,
|
|
@@ -3014,6 +4271,982 @@ var init_image_decode = __esm({
|
|
|
3014
4271
|
}
|
|
3015
4272
|
});
|
|
3016
4273
|
|
|
4274
|
+
// ../../node_modules/fzstd/esm/index.mjs
|
|
4275
|
+
function decompress(dat, buf) {
|
|
4276
|
+
var bufs = [], nb = +!buf;
|
|
4277
|
+
var bt = 0, ol = 0;
|
|
4278
|
+
for (; dat.length; ) {
|
|
4279
|
+
var st = rzfh(dat, nb || buf);
|
|
4280
|
+
if (typeof st == "object") {
|
|
4281
|
+
if (nb) {
|
|
4282
|
+
buf = null;
|
|
4283
|
+
if (st.w.length == st.u) {
|
|
4284
|
+
bufs.push(buf = st.w);
|
|
4285
|
+
ol += st.u;
|
|
4286
|
+
}
|
|
4287
|
+
} else {
|
|
4288
|
+
bufs.push(buf);
|
|
4289
|
+
st.e = 0;
|
|
4290
|
+
}
|
|
4291
|
+
for (; !st.l; ) {
|
|
4292
|
+
var blk = rzb(dat, st, buf);
|
|
4293
|
+
if (!blk)
|
|
4294
|
+
err(5);
|
|
4295
|
+
if (buf)
|
|
4296
|
+
st.e = st.y;
|
|
4297
|
+
else {
|
|
4298
|
+
bufs.push(blk);
|
|
4299
|
+
ol += blk.length;
|
|
4300
|
+
cpw(st.w, 0, blk.length);
|
|
4301
|
+
st.w.set(blk, st.w.length - blk.length);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
bt = st.b + st.c * 4;
|
|
4305
|
+
} else
|
|
4306
|
+
bt = st;
|
|
4307
|
+
dat = dat.subarray(bt);
|
|
4308
|
+
}
|
|
4309
|
+
return cct(bufs, ol);
|
|
4310
|
+
}
|
|
4311
|
+
var ab, u8, u16, i16, i32, slc, fill, cpw, ec, err, rb, b4, rzfh, msb, rfse, rhu, dllt, dmlt, doct, b2bl, llb, llbl, mlb, mlbl, dhu, dhu4, rzb, cct;
|
|
4312
|
+
var init_esm = __esm({
|
|
4313
|
+
"../../node_modules/fzstd/esm/index.mjs"() {
|
|
4314
|
+
"use strict";
|
|
4315
|
+
ab = ArrayBuffer;
|
|
4316
|
+
u8 = Uint8Array;
|
|
4317
|
+
u16 = Uint16Array;
|
|
4318
|
+
i16 = Int16Array;
|
|
4319
|
+
i32 = Int32Array;
|
|
4320
|
+
slc = function(v, s, e) {
|
|
4321
|
+
if (u8.prototype.slice)
|
|
4322
|
+
return u8.prototype.slice.call(v, s, e);
|
|
4323
|
+
if (s == null || s < 0)
|
|
4324
|
+
s = 0;
|
|
4325
|
+
if (e == null || e > v.length)
|
|
4326
|
+
e = v.length;
|
|
4327
|
+
var n = new u8(e - s);
|
|
4328
|
+
n.set(v.subarray(s, e));
|
|
4329
|
+
return n;
|
|
4330
|
+
};
|
|
4331
|
+
fill = function(v, n, s, e) {
|
|
4332
|
+
if (u8.prototype.fill)
|
|
4333
|
+
return u8.prototype.fill.call(v, n, s, e);
|
|
4334
|
+
if (s == null || s < 0)
|
|
4335
|
+
s = 0;
|
|
4336
|
+
if (e == null || e > v.length)
|
|
4337
|
+
e = v.length;
|
|
4338
|
+
for (; s < e; ++s)
|
|
4339
|
+
v[s] = n;
|
|
4340
|
+
return v;
|
|
4341
|
+
};
|
|
4342
|
+
cpw = function(v, t, s, e) {
|
|
4343
|
+
if (u8.prototype.copyWithin)
|
|
4344
|
+
return u8.prototype.copyWithin.call(v, t, s, e);
|
|
4345
|
+
if (s == null || s < 0)
|
|
4346
|
+
s = 0;
|
|
4347
|
+
if (e == null || e > v.length)
|
|
4348
|
+
e = v.length;
|
|
4349
|
+
while (s < e) {
|
|
4350
|
+
v[t++] = v[s++];
|
|
4351
|
+
}
|
|
4352
|
+
};
|
|
4353
|
+
ec = [
|
|
4354
|
+
"invalid zstd data",
|
|
4355
|
+
"window size too large (>2046MB)",
|
|
4356
|
+
"invalid block type",
|
|
4357
|
+
"FSE accuracy too high",
|
|
4358
|
+
"match distance too far back",
|
|
4359
|
+
"unexpected EOF"
|
|
4360
|
+
];
|
|
4361
|
+
err = function(ind, msg, nt) {
|
|
4362
|
+
var e = new Error(msg || ec[ind]);
|
|
4363
|
+
e.code = ind;
|
|
4364
|
+
if (Error.captureStackTrace)
|
|
4365
|
+
Error.captureStackTrace(e, err);
|
|
4366
|
+
if (!nt)
|
|
4367
|
+
throw e;
|
|
4368
|
+
return e;
|
|
4369
|
+
};
|
|
4370
|
+
rb = function(d, b, n) {
|
|
4371
|
+
var i = 0, o = 0;
|
|
4372
|
+
for (; i < n; ++i)
|
|
4373
|
+
o |= d[b++] << (i << 3);
|
|
4374
|
+
return o;
|
|
4375
|
+
};
|
|
4376
|
+
b4 = function(d, b) {
|
|
4377
|
+
return (d[b] | d[b + 1] << 8 | d[b + 2] << 16 | d[b + 3] << 24) >>> 0;
|
|
4378
|
+
};
|
|
4379
|
+
rzfh = function(dat, w) {
|
|
4380
|
+
var n3 = dat[0] | dat[1] << 8 | dat[2] << 16;
|
|
4381
|
+
if (n3 == 3126568 && dat[3] == 253) {
|
|
4382
|
+
var flg = dat[4];
|
|
4383
|
+
var ss = flg >> 5 & 1, cc = flg >> 2 & 1, df = flg & 3, fcf = flg >> 6;
|
|
4384
|
+
if (flg & 8)
|
|
4385
|
+
err(0);
|
|
4386
|
+
var bt = 6 - ss;
|
|
4387
|
+
var db = df == 3 ? 4 : df;
|
|
4388
|
+
var di = rb(dat, bt, db);
|
|
4389
|
+
bt += db;
|
|
4390
|
+
var fsb = fcf ? 1 << fcf : ss;
|
|
4391
|
+
var fss = rb(dat, bt, fsb) + (fcf == 1 && 256);
|
|
4392
|
+
var ws = fss;
|
|
4393
|
+
if (!ss) {
|
|
4394
|
+
var wb = 1 << 10 + (dat[5] >> 3);
|
|
4395
|
+
ws = wb + (wb >> 3) * (dat[5] & 7);
|
|
4396
|
+
}
|
|
4397
|
+
if (ws > 2145386496)
|
|
4398
|
+
err(1);
|
|
4399
|
+
var buf = new u8((w == 1 ? fss || ws : w ? 0 : ws) + 12);
|
|
4400
|
+
buf[0] = 1, buf[4] = 4, buf[8] = 8;
|
|
4401
|
+
return {
|
|
4402
|
+
b: bt + fsb,
|
|
4403
|
+
y: 0,
|
|
4404
|
+
l: 0,
|
|
4405
|
+
d: di,
|
|
4406
|
+
w: w && w != 1 ? w : buf.subarray(12),
|
|
4407
|
+
e: ws,
|
|
4408
|
+
o: new i32(buf.buffer, 0, 3),
|
|
4409
|
+
u: fss,
|
|
4410
|
+
c: cc,
|
|
4411
|
+
m: Math.min(131072, ws)
|
|
4412
|
+
};
|
|
4413
|
+
} else if ((n3 >> 4 | dat[3] << 20) == 25481893) {
|
|
4414
|
+
return b4(dat, 4) + 8;
|
|
4415
|
+
}
|
|
4416
|
+
err(0);
|
|
4417
|
+
};
|
|
4418
|
+
msb = function(val) {
|
|
4419
|
+
var bits = 0;
|
|
4420
|
+
for (; 1 << bits <= val; ++bits)
|
|
4421
|
+
;
|
|
4422
|
+
return bits - 1;
|
|
4423
|
+
};
|
|
4424
|
+
rfse = function(dat, bt, mal) {
|
|
4425
|
+
var tpos = (bt << 3) + 4;
|
|
4426
|
+
var al = (dat[bt] & 15) + 5;
|
|
4427
|
+
if (al > mal)
|
|
4428
|
+
err(3);
|
|
4429
|
+
var sz = 1 << al;
|
|
4430
|
+
var probs = sz, sym = -1, re = -1, i = -1, ht = sz;
|
|
4431
|
+
var buf = new ab(512 + (sz << 2));
|
|
4432
|
+
var freq = new i16(buf, 0, 256);
|
|
4433
|
+
var dstate = new u16(buf, 0, 256);
|
|
4434
|
+
var nstate = new u16(buf, 512, sz);
|
|
4435
|
+
var bb1 = 512 + (sz << 1);
|
|
4436
|
+
var syms = new u8(buf, bb1, sz);
|
|
4437
|
+
var nbits = new u8(buf, bb1 + sz);
|
|
4438
|
+
while (sym < 255 && probs > 0) {
|
|
4439
|
+
var bits = msb(probs + 1);
|
|
4440
|
+
var cbt = tpos >> 3;
|
|
4441
|
+
var msk = (1 << bits + 1) - 1;
|
|
4442
|
+
var val = (dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (tpos & 7) & msk;
|
|
4443
|
+
var msk1fb = (1 << bits) - 1;
|
|
4444
|
+
var msv = msk - probs - 1;
|
|
4445
|
+
var sval = val & msk1fb;
|
|
4446
|
+
if (sval < msv)
|
|
4447
|
+
tpos += bits, val = sval;
|
|
4448
|
+
else {
|
|
4449
|
+
tpos += bits + 1;
|
|
4450
|
+
if (val > msk1fb)
|
|
4451
|
+
val -= msv;
|
|
4452
|
+
}
|
|
4453
|
+
freq[++sym] = --val;
|
|
4454
|
+
if (val == -1) {
|
|
4455
|
+
probs += val;
|
|
4456
|
+
syms[--ht] = sym;
|
|
4457
|
+
} else
|
|
4458
|
+
probs -= val;
|
|
4459
|
+
if (!val) {
|
|
4460
|
+
do {
|
|
4461
|
+
var rbt = tpos >> 3;
|
|
4462
|
+
re = (dat[rbt] | dat[rbt + 1] << 8) >> (tpos & 7) & 3;
|
|
4463
|
+
tpos += 2;
|
|
4464
|
+
sym += re;
|
|
4465
|
+
} while (re == 3);
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
if (sym > 255 || probs)
|
|
4469
|
+
err(0);
|
|
4470
|
+
var sympos = 0;
|
|
4471
|
+
var sstep = (sz >> 1) + (sz >> 3) + 3;
|
|
4472
|
+
var smask = sz - 1;
|
|
4473
|
+
for (var s = 0; s <= sym; ++s) {
|
|
4474
|
+
var sf = freq[s];
|
|
4475
|
+
if (sf < 1) {
|
|
4476
|
+
dstate[s] = -sf;
|
|
4477
|
+
continue;
|
|
4478
|
+
}
|
|
4479
|
+
for (i = 0; i < sf; ++i) {
|
|
4480
|
+
syms[sympos] = s;
|
|
4481
|
+
do {
|
|
4482
|
+
sympos = sympos + sstep & smask;
|
|
4483
|
+
} while (sympos >= ht);
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
if (sympos)
|
|
4487
|
+
err(0);
|
|
4488
|
+
for (i = 0; i < sz; ++i) {
|
|
4489
|
+
var ns = dstate[syms[i]]++;
|
|
4490
|
+
var nb = nbits[i] = al - msb(ns);
|
|
4491
|
+
nstate[i] = (ns << nb) - sz;
|
|
4492
|
+
}
|
|
4493
|
+
return [tpos + 7 >> 3, {
|
|
4494
|
+
b: al,
|
|
4495
|
+
s: syms,
|
|
4496
|
+
n: nbits,
|
|
4497
|
+
t: nstate
|
|
4498
|
+
}];
|
|
4499
|
+
};
|
|
4500
|
+
rhu = function(dat, bt) {
|
|
4501
|
+
var i = 0, wc = -1;
|
|
4502
|
+
var buf = new u8(292), hb = dat[bt];
|
|
4503
|
+
var hw = buf.subarray(0, 256);
|
|
4504
|
+
var rc = buf.subarray(256, 268);
|
|
4505
|
+
var ri = new u16(buf.buffer, 268);
|
|
4506
|
+
if (hb < 128) {
|
|
4507
|
+
var _a = rfse(dat, bt + 1, 6), ebt = _a[0], fdt = _a[1];
|
|
4508
|
+
bt += hb;
|
|
4509
|
+
var epos = ebt << 3;
|
|
4510
|
+
var lb = dat[bt];
|
|
4511
|
+
if (!lb)
|
|
4512
|
+
err(0);
|
|
4513
|
+
var st1 = 0, st2 = 0, btr1 = fdt.b, btr2 = btr1;
|
|
4514
|
+
var fpos = (++bt << 3) - 8 + msb(lb);
|
|
4515
|
+
for (; ; ) {
|
|
4516
|
+
fpos -= btr1;
|
|
4517
|
+
if (fpos < epos)
|
|
4518
|
+
break;
|
|
4519
|
+
var cbt = fpos >> 3;
|
|
4520
|
+
st1 += (dat[cbt] | dat[cbt + 1] << 8) >> (fpos & 7) & (1 << btr1) - 1;
|
|
4521
|
+
hw[++wc] = fdt.s[st1];
|
|
4522
|
+
fpos -= btr2;
|
|
4523
|
+
if (fpos < epos)
|
|
4524
|
+
break;
|
|
4525
|
+
cbt = fpos >> 3;
|
|
4526
|
+
st2 += (dat[cbt] | dat[cbt + 1] << 8) >> (fpos & 7) & (1 << btr2) - 1;
|
|
4527
|
+
hw[++wc] = fdt.s[st2];
|
|
4528
|
+
btr1 = fdt.n[st1];
|
|
4529
|
+
st1 = fdt.t[st1];
|
|
4530
|
+
btr2 = fdt.n[st2];
|
|
4531
|
+
st2 = fdt.t[st2];
|
|
4532
|
+
}
|
|
4533
|
+
if (++wc > 255)
|
|
4534
|
+
err(0);
|
|
4535
|
+
} else {
|
|
4536
|
+
wc = hb - 127;
|
|
4537
|
+
for (; i < wc; i += 2) {
|
|
4538
|
+
var byte = dat[++bt];
|
|
4539
|
+
hw[i] = byte >> 4;
|
|
4540
|
+
hw[i + 1] = byte & 15;
|
|
4541
|
+
}
|
|
4542
|
+
++bt;
|
|
4543
|
+
}
|
|
4544
|
+
var wes = 0;
|
|
4545
|
+
for (i = 0; i < wc; ++i) {
|
|
4546
|
+
var wt = hw[i];
|
|
4547
|
+
if (wt > 11)
|
|
4548
|
+
err(0);
|
|
4549
|
+
wes += wt && 1 << wt - 1;
|
|
4550
|
+
}
|
|
4551
|
+
var mb = msb(wes) + 1;
|
|
4552
|
+
var ts = 1 << mb;
|
|
4553
|
+
var rem = ts - wes;
|
|
4554
|
+
if (rem & rem - 1)
|
|
4555
|
+
err(0);
|
|
4556
|
+
hw[wc++] = msb(rem) + 1;
|
|
4557
|
+
for (i = 0; i < wc; ++i) {
|
|
4558
|
+
var wt = hw[i];
|
|
4559
|
+
++rc[hw[i] = wt && mb + 1 - wt];
|
|
4560
|
+
}
|
|
4561
|
+
var hbuf = new u8(ts << 1);
|
|
4562
|
+
var syms = hbuf.subarray(0, ts), nb = hbuf.subarray(ts);
|
|
4563
|
+
ri[mb] = 0;
|
|
4564
|
+
for (i = mb; i > 0; --i) {
|
|
4565
|
+
var pv = ri[i];
|
|
4566
|
+
fill(nb, i, pv, ri[i - 1] = pv + rc[i] * (1 << mb - i));
|
|
4567
|
+
}
|
|
4568
|
+
if (ri[0] != ts)
|
|
4569
|
+
err(0);
|
|
4570
|
+
for (i = 0; i < wc; ++i) {
|
|
4571
|
+
var bits = hw[i];
|
|
4572
|
+
if (bits) {
|
|
4573
|
+
var code = ri[bits];
|
|
4574
|
+
fill(syms, i, code, ri[bits] = code + (1 << mb - bits));
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
return [bt, {
|
|
4578
|
+
n: nb,
|
|
4579
|
+
b: mb,
|
|
4580
|
+
s: syms
|
|
4581
|
+
}];
|
|
4582
|
+
};
|
|
4583
|
+
dllt = rfse(/* @__PURE__ */ new u8([
|
|
4584
|
+
81,
|
|
4585
|
+
16,
|
|
4586
|
+
99,
|
|
4587
|
+
140,
|
|
4588
|
+
49,
|
|
4589
|
+
198,
|
|
4590
|
+
24,
|
|
4591
|
+
99,
|
|
4592
|
+
12,
|
|
4593
|
+
33,
|
|
4594
|
+
196,
|
|
4595
|
+
24,
|
|
4596
|
+
99,
|
|
4597
|
+
102,
|
|
4598
|
+
102,
|
|
4599
|
+
134,
|
|
4600
|
+
70,
|
|
4601
|
+
146,
|
|
4602
|
+
4
|
|
4603
|
+
]), 0, 6)[1];
|
|
4604
|
+
dmlt = rfse(/* @__PURE__ */ new u8([
|
|
4605
|
+
33,
|
|
4606
|
+
20,
|
|
4607
|
+
196,
|
|
4608
|
+
24,
|
|
4609
|
+
99,
|
|
4610
|
+
140,
|
|
4611
|
+
33,
|
|
4612
|
+
132,
|
|
4613
|
+
16,
|
|
4614
|
+
66,
|
|
4615
|
+
8,
|
|
4616
|
+
33,
|
|
4617
|
+
132,
|
|
4618
|
+
16,
|
|
4619
|
+
66,
|
|
4620
|
+
8,
|
|
4621
|
+
33,
|
|
4622
|
+
68,
|
|
4623
|
+
68,
|
|
4624
|
+
68,
|
|
4625
|
+
68,
|
|
4626
|
+
68,
|
|
4627
|
+
68,
|
|
4628
|
+
68,
|
|
4629
|
+
68,
|
|
4630
|
+
36,
|
|
4631
|
+
9
|
|
4632
|
+
]), 0, 6)[1];
|
|
4633
|
+
doct = rfse(/* @__PURE__ */ new u8([
|
|
4634
|
+
32,
|
|
4635
|
+
132,
|
|
4636
|
+
16,
|
|
4637
|
+
66,
|
|
4638
|
+
102,
|
|
4639
|
+
70,
|
|
4640
|
+
68,
|
|
4641
|
+
68,
|
|
4642
|
+
68,
|
|
4643
|
+
68,
|
|
4644
|
+
36,
|
|
4645
|
+
73,
|
|
4646
|
+
2
|
|
4647
|
+
]), 0, 5)[1];
|
|
4648
|
+
b2bl = function(b, s) {
|
|
4649
|
+
var len = b.length, bl = new i32(len);
|
|
4650
|
+
for (var i = 0; i < len; ++i) {
|
|
4651
|
+
bl[i] = s;
|
|
4652
|
+
s += 1 << b[i];
|
|
4653
|
+
}
|
|
4654
|
+
return bl;
|
|
4655
|
+
};
|
|
4656
|
+
llb = /* @__PURE__ */ new u8((/* @__PURE__ */ new i32([
|
|
4657
|
+
0,
|
|
4658
|
+
0,
|
|
4659
|
+
0,
|
|
4660
|
+
0,
|
|
4661
|
+
16843009,
|
|
4662
|
+
50528770,
|
|
4663
|
+
134678020,
|
|
4664
|
+
202050057,
|
|
4665
|
+
269422093
|
|
4666
|
+
])).buffer, 0, 36);
|
|
4667
|
+
llbl = /* @__PURE__ */ b2bl(llb, 0);
|
|
4668
|
+
mlb = /* @__PURE__ */ new u8((/* @__PURE__ */ new i32([
|
|
4669
|
+
0,
|
|
4670
|
+
0,
|
|
4671
|
+
0,
|
|
4672
|
+
0,
|
|
4673
|
+
0,
|
|
4674
|
+
0,
|
|
4675
|
+
0,
|
|
4676
|
+
0,
|
|
4677
|
+
16843009,
|
|
4678
|
+
50528770,
|
|
4679
|
+
117769220,
|
|
4680
|
+
185207048,
|
|
4681
|
+
252579084,
|
|
4682
|
+
16
|
|
4683
|
+
])).buffer, 0, 53);
|
|
4684
|
+
mlbl = /* @__PURE__ */ b2bl(mlb, 3);
|
|
4685
|
+
dhu = function(dat, out, hu) {
|
|
4686
|
+
var len = dat.length, ss = out.length, lb = dat[len - 1], msk = (1 << hu.b) - 1, eb = -hu.b;
|
|
4687
|
+
if (!lb)
|
|
4688
|
+
err(0);
|
|
4689
|
+
var st = 0, btr = hu.b, pos = (len << 3) - 8 + msb(lb) - btr, i = -1;
|
|
4690
|
+
for (; pos > eb && i < ss; ) {
|
|
4691
|
+
var cbt = pos >> 3;
|
|
4692
|
+
var val = (dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (pos & 7);
|
|
4693
|
+
st = (st << btr | val) & msk;
|
|
4694
|
+
out[++i] = hu.s[st];
|
|
4695
|
+
pos -= btr = hu.n[st];
|
|
4696
|
+
}
|
|
4697
|
+
if (pos != eb || i + 1 != ss)
|
|
4698
|
+
err(0);
|
|
4699
|
+
};
|
|
4700
|
+
dhu4 = function(dat, out, hu) {
|
|
4701
|
+
var bt = 6;
|
|
4702
|
+
var ss = out.length, sz1 = ss + 3 >> 2, sz2 = sz1 << 1, sz3 = sz1 + sz2;
|
|
4703
|
+
dhu(dat.subarray(bt, bt += dat[0] | dat[1] << 8), out.subarray(0, sz1), hu);
|
|
4704
|
+
dhu(dat.subarray(bt, bt += dat[2] | dat[3] << 8), out.subarray(sz1, sz2), hu);
|
|
4705
|
+
dhu(dat.subarray(bt, bt += dat[4] | dat[5] << 8), out.subarray(sz2, sz3), hu);
|
|
4706
|
+
dhu(dat.subarray(bt), out.subarray(sz3), hu);
|
|
4707
|
+
};
|
|
4708
|
+
rzb = function(dat, st, out) {
|
|
4709
|
+
var _a;
|
|
4710
|
+
var bt = st.b;
|
|
4711
|
+
var b0 = dat[bt], btype = b0 >> 1 & 3;
|
|
4712
|
+
st.l = b0 & 1;
|
|
4713
|
+
var sz = b0 >> 3 | dat[bt + 1] << 5 | dat[bt + 2] << 13;
|
|
4714
|
+
var ebt = (bt += 3) + sz;
|
|
4715
|
+
if (btype == 1) {
|
|
4716
|
+
if (bt >= dat.length)
|
|
4717
|
+
return;
|
|
4718
|
+
st.b = bt + 1;
|
|
4719
|
+
if (out) {
|
|
4720
|
+
fill(out, dat[bt], st.y, st.y += sz);
|
|
4721
|
+
return out;
|
|
4722
|
+
}
|
|
4723
|
+
return fill(new u8(sz), dat[bt]);
|
|
4724
|
+
}
|
|
4725
|
+
if (ebt > dat.length)
|
|
4726
|
+
return;
|
|
4727
|
+
if (btype == 0) {
|
|
4728
|
+
st.b = ebt;
|
|
4729
|
+
if (out) {
|
|
4730
|
+
out.set(dat.subarray(bt, ebt), st.y);
|
|
4731
|
+
st.y += sz;
|
|
4732
|
+
return out;
|
|
4733
|
+
}
|
|
4734
|
+
return slc(dat, bt, ebt);
|
|
4735
|
+
}
|
|
4736
|
+
if (btype == 2) {
|
|
4737
|
+
var b3 = dat[bt], lbt = b3 & 3, sf = b3 >> 2 & 3;
|
|
4738
|
+
var lss = b3 >> 4, lcs = 0, s4 = 0;
|
|
4739
|
+
if (lbt < 2) {
|
|
4740
|
+
if (sf & 1)
|
|
4741
|
+
lss |= dat[++bt] << 4 | (sf & 2 && dat[++bt] << 12);
|
|
4742
|
+
else
|
|
4743
|
+
lss = b3 >> 3;
|
|
4744
|
+
} else {
|
|
4745
|
+
s4 = sf;
|
|
4746
|
+
if (sf < 2)
|
|
4747
|
+
lss |= (dat[++bt] & 63) << 4, lcs = dat[bt] >> 6 | dat[++bt] << 2;
|
|
4748
|
+
else if (sf == 2)
|
|
4749
|
+
lss |= dat[++bt] << 4 | (dat[++bt] & 3) << 12, lcs = dat[bt] >> 2 | dat[++bt] << 6;
|
|
4750
|
+
else
|
|
4751
|
+
lss |= dat[++bt] << 4 | (dat[++bt] & 63) << 12, lcs = dat[bt] >> 6 | dat[++bt] << 2 | dat[++bt] << 10;
|
|
4752
|
+
}
|
|
4753
|
+
++bt;
|
|
4754
|
+
var buf = out ? out.subarray(st.y, st.y + st.m) : new u8(st.m);
|
|
4755
|
+
var spl = buf.length - lss;
|
|
4756
|
+
if (lbt == 0)
|
|
4757
|
+
buf.set(dat.subarray(bt, bt += lss), spl);
|
|
4758
|
+
else if (lbt == 1)
|
|
4759
|
+
fill(buf, dat[bt++], spl);
|
|
4760
|
+
else {
|
|
4761
|
+
var hu = st.h;
|
|
4762
|
+
if (lbt == 2) {
|
|
4763
|
+
var hud = rhu(dat, bt);
|
|
4764
|
+
lcs += bt - (bt = hud[0]);
|
|
4765
|
+
st.h = hu = hud[1];
|
|
4766
|
+
} else if (!hu)
|
|
4767
|
+
err(0);
|
|
4768
|
+
(s4 ? dhu4 : dhu)(dat.subarray(bt, bt += lcs), buf.subarray(spl), hu);
|
|
4769
|
+
}
|
|
4770
|
+
var ns = dat[bt++];
|
|
4771
|
+
if (ns) {
|
|
4772
|
+
if (ns == 255)
|
|
4773
|
+
ns = (dat[bt++] | dat[bt++] << 8) + 32512;
|
|
4774
|
+
else if (ns > 127)
|
|
4775
|
+
ns = ns - 128 << 8 | dat[bt++];
|
|
4776
|
+
var scm = dat[bt++];
|
|
4777
|
+
if (scm & 3)
|
|
4778
|
+
err(0);
|
|
4779
|
+
var dts = [dmlt, doct, dllt];
|
|
4780
|
+
for (var i = 2; i > -1; --i) {
|
|
4781
|
+
var md = scm >> (i << 1) + 2 & 3;
|
|
4782
|
+
if (md == 1) {
|
|
4783
|
+
var rbuf = new u8([0, 0, dat[bt++]]);
|
|
4784
|
+
dts[i] = {
|
|
4785
|
+
s: rbuf.subarray(2, 3),
|
|
4786
|
+
n: rbuf.subarray(0, 1),
|
|
4787
|
+
t: new u16(rbuf.buffer, 0, 1),
|
|
4788
|
+
b: 0
|
|
4789
|
+
};
|
|
4790
|
+
} else if (md == 2) {
|
|
4791
|
+
_a = rfse(dat, bt, 9 - (i & 1)), bt = _a[0], dts[i] = _a[1];
|
|
4792
|
+
} else if (md == 3) {
|
|
4793
|
+
if (!st.t)
|
|
4794
|
+
err(0);
|
|
4795
|
+
dts[i] = st.t[i];
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
var _b = st.t = dts, mlt = _b[0], oct = _b[1], llt = _b[2];
|
|
4799
|
+
var lb = dat[ebt - 1];
|
|
4800
|
+
if (!lb)
|
|
4801
|
+
err(0);
|
|
4802
|
+
var spos = (ebt << 3) - 8 + msb(lb) - llt.b, cbt = spos >> 3, oubt = 0;
|
|
4803
|
+
var lst = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << llt.b) - 1;
|
|
4804
|
+
cbt = (spos -= oct.b) >> 3;
|
|
4805
|
+
var ost = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << oct.b) - 1;
|
|
4806
|
+
cbt = (spos -= mlt.b) >> 3;
|
|
4807
|
+
var mst = (dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << mlt.b) - 1;
|
|
4808
|
+
for (++ns; --ns; ) {
|
|
4809
|
+
var llc = llt.s[lst];
|
|
4810
|
+
var lbtr = llt.n[lst];
|
|
4811
|
+
var mlc = mlt.s[mst];
|
|
4812
|
+
var mbtr = mlt.n[mst];
|
|
4813
|
+
var ofc = oct.s[ost];
|
|
4814
|
+
var obtr = oct.n[ost];
|
|
4815
|
+
cbt = (spos -= ofc) >> 3;
|
|
4816
|
+
var ofp = 1 << ofc;
|
|
4817
|
+
var off = ofp + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16 | dat[cbt + 3] << 24) >>> (spos & 7) & ofp - 1);
|
|
4818
|
+
cbt = (spos -= mlb[mlc]) >> 3;
|
|
4819
|
+
var ml = mlbl[mlc] + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (spos & 7) & (1 << mlb[mlc]) - 1);
|
|
4820
|
+
cbt = (spos -= llb[llc]) >> 3;
|
|
4821
|
+
var ll = llbl[llc] + ((dat[cbt] | dat[cbt + 1] << 8 | dat[cbt + 2] << 16) >> (spos & 7) & (1 << llb[llc]) - 1);
|
|
4822
|
+
cbt = (spos -= lbtr) >> 3;
|
|
4823
|
+
lst = llt.t[lst] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << lbtr) - 1);
|
|
4824
|
+
cbt = (spos -= mbtr) >> 3;
|
|
4825
|
+
mst = mlt.t[mst] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << mbtr) - 1);
|
|
4826
|
+
cbt = (spos -= obtr) >> 3;
|
|
4827
|
+
ost = oct.t[ost] + ((dat[cbt] | dat[cbt + 1] << 8) >> (spos & 7) & (1 << obtr) - 1);
|
|
4828
|
+
if (off > 3) {
|
|
4829
|
+
st.o[2] = st.o[1];
|
|
4830
|
+
st.o[1] = st.o[0];
|
|
4831
|
+
st.o[0] = off -= 3;
|
|
4832
|
+
} else {
|
|
4833
|
+
var idx = off - (ll != 0);
|
|
4834
|
+
if (idx) {
|
|
4835
|
+
off = idx == 3 ? st.o[0] - 1 : st.o[idx];
|
|
4836
|
+
if (idx > 1)
|
|
4837
|
+
st.o[2] = st.o[1];
|
|
4838
|
+
st.o[1] = st.o[0];
|
|
4839
|
+
st.o[0] = off;
|
|
4840
|
+
} else
|
|
4841
|
+
off = st.o[0];
|
|
4842
|
+
}
|
|
4843
|
+
for (var i = 0; i < ll; ++i) {
|
|
4844
|
+
buf[oubt + i] = buf[spl + i];
|
|
4845
|
+
}
|
|
4846
|
+
oubt += ll, spl += ll;
|
|
4847
|
+
var stin = oubt - off;
|
|
4848
|
+
if (stin < 0) {
|
|
4849
|
+
var len = -stin;
|
|
4850
|
+
var bs = st.e + stin;
|
|
4851
|
+
if (len > ml)
|
|
4852
|
+
len = ml;
|
|
4853
|
+
for (var i = 0; i < len; ++i) {
|
|
4854
|
+
buf[oubt + i] = st.w[bs + i];
|
|
4855
|
+
}
|
|
4856
|
+
oubt += len, ml -= len, stin = 0;
|
|
4857
|
+
}
|
|
4858
|
+
for (var i = 0; i < ml; ++i) {
|
|
4859
|
+
buf[oubt + i] = buf[stin + i];
|
|
4860
|
+
}
|
|
4861
|
+
oubt += ml;
|
|
4862
|
+
}
|
|
4863
|
+
if (oubt != spl) {
|
|
4864
|
+
while (spl < buf.length) {
|
|
4865
|
+
buf[oubt++] = buf[spl++];
|
|
4866
|
+
}
|
|
4867
|
+
} else
|
|
4868
|
+
oubt = buf.length;
|
|
4869
|
+
if (out)
|
|
4870
|
+
st.y += oubt;
|
|
4871
|
+
else
|
|
4872
|
+
buf = slc(buf, 0, oubt);
|
|
4873
|
+
} else if (out) {
|
|
4874
|
+
st.y += lss;
|
|
4875
|
+
if (spl) {
|
|
4876
|
+
for (var i = 0; i < lss; ++i) {
|
|
4877
|
+
buf[i] = buf[spl + i];
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
} else if (spl)
|
|
4881
|
+
buf = slc(buf, spl);
|
|
4882
|
+
st.b = ebt;
|
|
4883
|
+
return buf;
|
|
4884
|
+
}
|
|
4885
|
+
err(2);
|
|
4886
|
+
};
|
|
4887
|
+
cct = function(bufs, ol) {
|
|
4888
|
+
if (bufs.length == 1)
|
|
4889
|
+
return bufs[0];
|
|
4890
|
+
var buf = new u8(ol);
|
|
4891
|
+
for (var i = 0, b = 0; i < bufs.length; ++i) {
|
|
4892
|
+
var chk = bufs[i];
|
|
4893
|
+
buf.set(chk, b);
|
|
4894
|
+
b += chk.length;
|
|
4895
|
+
}
|
|
4896
|
+
return buf;
|
|
4897
|
+
};
|
|
4898
|
+
}
|
|
4899
|
+
});
|
|
4900
|
+
|
|
4901
|
+
// ../core/dist/studio-skills.js
|
|
4902
|
+
import { createHash as createHash2 } from "crypto";
|
|
4903
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
4904
|
+
import * as path4 from "path";
|
|
4905
|
+
function decompressLz4Block(input, outputLength) {
|
|
4906
|
+
const output = Buffer.allocUnsafe(outputLength);
|
|
4907
|
+
let inputOffset = 0;
|
|
4908
|
+
let outputOffset = 0;
|
|
4909
|
+
const readExtendedLength = (initial) => {
|
|
4910
|
+
let length = initial;
|
|
4911
|
+
if (initial !== 15)
|
|
4912
|
+
return length;
|
|
4913
|
+
let next = 255;
|
|
4914
|
+
while (next === 255) {
|
|
4915
|
+
if (inputOffset >= input.length) {
|
|
4916
|
+
throw new Error("Invalid LZ4 chunk: truncated extended length");
|
|
4917
|
+
}
|
|
4918
|
+
next = input[inputOffset++];
|
|
4919
|
+
length += next;
|
|
4920
|
+
}
|
|
4921
|
+
return length;
|
|
4922
|
+
};
|
|
4923
|
+
while (inputOffset < input.length) {
|
|
4924
|
+
const token = input[inputOffset++];
|
|
4925
|
+
const literalLength = readExtendedLength(token >>> 4);
|
|
4926
|
+
if (inputOffset + literalLength > input.length || outputOffset + literalLength > output.length) {
|
|
4927
|
+
throw new Error("Invalid LZ4 chunk: literal exceeds chunk boundary");
|
|
4928
|
+
}
|
|
4929
|
+
input.copy(output, outputOffset, inputOffset, inputOffset + literalLength);
|
|
4930
|
+
inputOffset += literalLength;
|
|
4931
|
+
outputOffset += literalLength;
|
|
4932
|
+
if (inputOffset === input.length)
|
|
4933
|
+
break;
|
|
4934
|
+
if (inputOffset + 2 > input.length) {
|
|
4935
|
+
throw new Error("Invalid LZ4 chunk: missing match offset");
|
|
4936
|
+
}
|
|
4937
|
+
const matchOffset = input.readUInt16LE(inputOffset);
|
|
4938
|
+
inputOffset += 2;
|
|
4939
|
+
if (matchOffset === 0 || matchOffset > outputOffset) {
|
|
4940
|
+
throw new Error("Invalid LZ4 chunk: invalid match offset");
|
|
4941
|
+
}
|
|
4942
|
+
const matchLength = readExtendedLength(token & 15) + 4;
|
|
4943
|
+
if (outputOffset + matchLength > output.length) {
|
|
4944
|
+
throw new Error("Invalid LZ4 chunk: match exceeds output boundary");
|
|
4945
|
+
}
|
|
4946
|
+
for (let index = 0; index < matchLength; index += 1) {
|
|
4947
|
+
output[outputOffset] = output[outputOffset - matchOffset];
|
|
4948
|
+
outputOffset += 1;
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
if (outputOffset !== output.length) {
|
|
4952
|
+
throw new Error(`Invalid LZ4 chunk: produced ${outputOffset} bytes, expected ${output.length}`);
|
|
4953
|
+
}
|
|
4954
|
+
return output;
|
|
4955
|
+
}
|
|
4956
|
+
function decompressChunk(compressed, outputLength) {
|
|
4957
|
+
if (compressed.subarray(0, ZSTD_MAGIC.length).equals(ZSTD_MAGIC)) {
|
|
4958
|
+
const decompressed = Buffer.from(decompress(compressed));
|
|
4959
|
+
if (decompressed.length !== outputLength) {
|
|
4960
|
+
throw new Error(`Invalid Zstandard chunk: produced ${decompressed.length} bytes, expected ${outputLength}`);
|
|
4961
|
+
}
|
|
4962
|
+
return decompressed;
|
|
4963
|
+
}
|
|
4964
|
+
return decompressLz4Block(compressed, outputLength);
|
|
4965
|
+
}
|
|
4966
|
+
function readChunk(reader) {
|
|
4967
|
+
if (reader.remaining < RBXM_CHUNK_HEADER_BYTES) {
|
|
4968
|
+
throw new Error("Invalid Assistant.rbxm: truncated chunk header");
|
|
4969
|
+
}
|
|
4970
|
+
const type = reader.readBytes(4).toString("latin1");
|
|
4971
|
+
const compressedLength = reader.readUint32();
|
|
4972
|
+
const uncompressedLength = reader.readUint32();
|
|
4973
|
+
reader.skip(4);
|
|
4974
|
+
if (type === "END\0")
|
|
4975
|
+
return { type };
|
|
4976
|
+
if (compressedLength === 0) {
|
|
4977
|
+
return { type, content: reader.readBytes(uncompressedLength) };
|
|
4978
|
+
}
|
|
4979
|
+
return {
|
|
4980
|
+
type,
|
|
4981
|
+
content: decompressChunk(reader.readBytes(compressedLength), uncompressedLength)
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
function parseFrontmatter(markdown) {
|
|
4985
|
+
const match = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
4986
|
+
if (!match)
|
|
4987
|
+
return void 0;
|
|
4988
|
+
const fields = /* @__PURE__ */ new Map();
|
|
4989
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
4990
|
+
const separator = line.indexOf(":");
|
|
4991
|
+
if (separator <= 0)
|
|
4992
|
+
continue;
|
|
4993
|
+
const key = line.slice(0, separator).trim().toLowerCase();
|
|
4994
|
+
let value = line.slice(separator + 1).trim();
|
|
4995
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
4996
|
+
try {
|
|
4997
|
+
value = JSON.parse(value);
|
|
4998
|
+
} catch {
|
|
4999
|
+
}
|
|
5000
|
+
} else if (value.startsWith("'") && value.endsWith("'")) {
|
|
5001
|
+
value = value.slice(1, -1).replace(/''/g, "'");
|
|
5002
|
+
}
|
|
5003
|
+
fields.set(key, value);
|
|
5004
|
+
}
|
|
5005
|
+
const name = fields.get("name")?.trim();
|
|
5006
|
+
if (!name)
|
|
5007
|
+
return void 0;
|
|
5008
|
+
return { name, description: fields.get("description")?.trim() ?? "" };
|
|
5009
|
+
}
|
|
5010
|
+
function canonicalBuiltInSkillName(sourceName) {
|
|
5011
|
+
return sourceName.toLowerCase().startsWith("rbx-") ? sourceName : `rbx-${sourceName}`;
|
|
5012
|
+
}
|
|
5013
|
+
function parseBuiltInStudioSkills(buffer) {
|
|
5014
|
+
if (buffer.length < RBXM_HEADER_BYTES || !buffer.subarray(0, RBXM_MAGIC.length).equals(RBXM_MAGIC)) {
|
|
5015
|
+
throw new Error("Invalid Assistant.rbxm: Roblox binary model header not found");
|
|
5016
|
+
}
|
|
5017
|
+
const reader = new ByteReader(buffer, "Assistant.rbxm");
|
|
5018
|
+
reader.skip(RBXM_MAGIC.length);
|
|
5019
|
+
reader.readUint16();
|
|
5020
|
+
reader.readInt32();
|
|
5021
|
+
reader.readInt32();
|
|
5022
|
+
reader.skip(8);
|
|
5023
|
+
const stringValueClasses = /* @__PURE__ */ new Map();
|
|
5024
|
+
let foundEnd = false;
|
|
5025
|
+
while (reader.remaining > 0) {
|
|
5026
|
+
const chunk = readChunk(reader);
|
|
5027
|
+
if (chunk.type === "END\0") {
|
|
5028
|
+
foundEnd = true;
|
|
5029
|
+
break;
|
|
5030
|
+
}
|
|
5031
|
+
if (!chunk.content)
|
|
5032
|
+
continue;
|
|
5033
|
+
const chunkReader = new ByteReader(chunk.content, `${chunk.type} chunk`);
|
|
5034
|
+
if (chunk.type === "INST") {
|
|
5035
|
+
const classId2 = chunkReader.readUint32();
|
|
5036
|
+
const className = chunkReader.readString();
|
|
5037
|
+
chunkReader.readUint8();
|
|
5038
|
+
const count = chunkReader.readUint32();
|
|
5039
|
+
chunkReader.skip(count * 4);
|
|
5040
|
+
if (className === "StringValue")
|
|
5041
|
+
stringValueClasses.set(classId2, { count });
|
|
5042
|
+
continue;
|
|
5043
|
+
}
|
|
5044
|
+
if (chunk.type !== "PROP")
|
|
5045
|
+
continue;
|
|
5046
|
+
const classId = chunkReader.readUint32();
|
|
5047
|
+
const propertyName = chunkReader.readString();
|
|
5048
|
+
const propertyType = chunkReader.readUint8();
|
|
5049
|
+
const classInfo = stringValueClasses.get(classId);
|
|
5050
|
+
if (!classInfo || propertyType !== STRING_PROPERTY_TYPE)
|
|
5051
|
+
continue;
|
|
5052
|
+
if (propertyName !== "Name" && propertyName !== "Value")
|
|
5053
|
+
continue;
|
|
5054
|
+
const values = [];
|
|
5055
|
+
for (let index = 0; index < classInfo.count; index += 1) {
|
|
5056
|
+
values.push(chunkReader.readString());
|
|
5057
|
+
}
|
|
5058
|
+
if (propertyName === "Name")
|
|
5059
|
+
classInfo.names = values;
|
|
5060
|
+
else
|
|
5061
|
+
classInfo.values = values;
|
|
5062
|
+
}
|
|
5063
|
+
if (!foundEnd)
|
|
5064
|
+
throw new Error("Invalid Assistant.rbxm: END chunk not found");
|
|
5065
|
+
const documents = [];
|
|
5066
|
+
for (const classInfo of stringValueClasses.values()) {
|
|
5067
|
+
if (!classInfo.names || !classInfo.values)
|
|
5068
|
+
continue;
|
|
5069
|
+
for (let index = 0; index < classInfo.count; index += 1) {
|
|
5070
|
+
const document = classInfo.names[index];
|
|
5071
|
+
if (document !== "SKILL" && document !== "SKILL-combined")
|
|
5072
|
+
continue;
|
|
5073
|
+
const content = classInfo.values[index];
|
|
5074
|
+
const frontmatter = parseFrontmatter(content);
|
|
5075
|
+
if (!frontmatter)
|
|
5076
|
+
continue;
|
|
5077
|
+
documents.push({
|
|
5078
|
+
document,
|
|
5079
|
+
sourceName: frontmatter.name,
|
|
5080
|
+
description: frontmatter.description,
|
|
5081
|
+
content
|
|
5082
|
+
});
|
|
5083
|
+
}
|
|
5084
|
+
}
|
|
5085
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
5086
|
+
for (const document of documents) {
|
|
5087
|
+
const key = document.sourceName.toLowerCase();
|
|
5088
|
+
const group = grouped.get(key) ?? {};
|
|
5089
|
+
if (document.document === "SKILL-combined")
|
|
5090
|
+
group.combined = document;
|
|
5091
|
+
else
|
|
5092
|
+
group.base = document;
|
|
5093
|
+
grouped.set(key, group);
|
|
5094
|
+
}
|
|
5095
|
+
return [...grouped.values()].map((group) => {
|
|
5096
|
+
const selected = group.combined ?? group.base;
|
|
5097
|
+
return {
|
|
5098
|
+
name: canonicalBuiltInSkillName(selected.sourceName),
|
|
5099
|
+
sourceName: selected.sourceName,
|
|
5100
|
+
description: selected.description,
|
|
5101
|
+
document: selected.document,
|
|
5102
|
+
hasCombinedDocument: group.combined !== void 0,
|
|
5103
|
+
content: selected.content,
|
|
5104
|
+
contentLength: Buffer.byteLength(selected.content, "utf8"),
|
|
5105
|
+
contentSha256: createHash2("sha256").update(selected.content).digest("hex")
|
|
5106
|
+
};
|
|
5107
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
5108
|
+
}
|
|
5109
|
+
function discoverNamedFile(root, fileName, depth = 0) {
|
|
5110
|
+
if (depth > MAX_DISCOVERY_DEPTH || !existsSync4(root))
|
|
5111
|
+
return [];
|
|
5112
|
+
const matches = [];
|
|
5113
|
+
let entries;
|
|
5114
|
+
try {
|
|
5115
|
+
entries = readdirSync3(root, { withFileTypes: true });
|
|
5116
|
+
} catch {
|
|
5117
|
+
return matches;
|
|
5118
|
+
}
|
|
5119
|
+
for (const entry of entries) {
|
|
5120
|
+
const entryPath = path4.join(root, entry.name);
|
|
5121
|
+
if (entry.isFile() && entry.name.toLowerCase() === fileName.toLowerCase()) {
|
|
5122
|
+
matches.push(entryPath);
|
|
5123
|
+
} else if (entry.isDirectory()) {
|
|
5124
|
+
matches.push(...discoverNamedFile(entryPath, fileName, depth + 1));
|
|
5125
|
+
}
|
|
5126
|
+
}
|
|
5127
|
+
return matches;
|
|
5128
|
+
}
|
|
5129
|
+
function resolveAssistantBundlePath(studioExe = resolveStudioExe()) {
|
|
5130
|
+
const override = process.env.ROBLOX_STUDIO_ASSISTANT_BUNDLE;
|
|
5131
|
+
if (override) {
|
|
5132
|
+
if (!existsSync4(override)) {
|
|
5133
|
+
throw new Error(`ROBLOX_STUDIO_ASSISTANT_BUNDLE does not exist: ${override}`);
|
|
5134
|
+
}
|
|
5135
|
+
return override;
|
|
5136
|
+
}
|
|
5137
|
+
const exeDirectory = path4.dirname(studioExe);
|
|
5138
|
+
const roots = [
|
|
5139
|
+
path4.join(exeDirectory, "BuiltInStandalonePlugins"),
|
|
5140
|
+
path4.resolve(exeDirectory, "..", "Resources", "BuiltInStandalonePlugins"),
|
|
5141
|
+
path4.resolve(exeDirectory, "..", "..", "OTAPlugins")
|
|
5142
|
+
];
|
|
5143
|
+
const direct = path4.join(exeDirectory, "BuiltInStandalonePlugins", "Optimized_Embedded_Signature", "Assistant.rbxm");
|
|
5144
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
5145
|
+
if (existsSync4(direct))
|
|
5146
|
+
candidates.add(direct);
|
|
5147
|
+
for (const root of roots) {
|
|
5148
|
+
for (const candidate of discoverNamedFile(root, "Assistant.rbxm"))
|
|
5149
|
+
candidates.add(candidate);
|
|
5150
|
+
}
|
|
5151
|
+
const newest = [...candidates].map((candidate) => ({ candidate, modifiedAt: statSync3(candidate).mtimeMs })).sort((left, right) => right.modifiedAt - left.modifiedAt)[0]?.candidate;
|
|
5152
|
+
if (!newest) {
|
|
5153
|
+
throw new Error(`Studio Assistant bundle not found for ${studioExe}. Set ROBLOX_STUDIO_ASSISTANT_BUNDLE to the installed Assistant.rbxm path.`);
|
|
5154
|
+
}
|
|
5155
|
+
return newest;
|
|
5156
|
+
}
|
|
5157
|
+
function loadBuiltInStudioSkills(bundlePath = resolveAssistantBundlePath()) {
|
|
5158
|
+
const stats = statSync3(bundlePath);
|
|
5159
|
+
if (cachedBundle?.path === bundlePath && cachedBundle.modifiedAt === stats.mtimeMs && cachedBundle.size === stats.size) {
|
|
5160
|
+
return cachedBundle.value;
|
|
5161
|
+
}
|
|
5162
|
+
const buffer = readFileSync5(bundlePath);
|
|
5163
|
+
const skills = parseBuiltInStudioSkills(buffer);
|
|
5164
|
+
if (skills.length === 0) {
|
|
5165
|
+
throw new Error(`No built-in skill documents found in ${bundlePath}`);
|
|
5166
|
+
}
|
|
5167
|
+
const studioVersion = bundlePath.split(path4.sep).find((segment) => /^version-[a-z0-9]+$/i.test(segment));
|
|
5168
|
+
const value = {
|
|
5169
|
+
bundlePath,
|
|
5170
|
+
bundleModifiedAt: stats.mtime.toISOString(),
|
|
5171
|
+
bundleSha256: createHash2("sha256").update(buffer).digest("hex"),
|
|
5172
|
+
studioVersion,
|
|
5173
|
+
skills
|
|
5174
|
+
};
|
|
5175
|
+
cachedBundle = { path: bundlePath, modifiedAt: stats.mtimeMs, size: stats.size, value };
|
|
5176
|
+
return value;
|
|
5177
|
+
}
|
|
5178
|
+
function findBuiltInStudioSkill(bundle, requestedName) {
|
|
5179
|
+
const wanted = requestedName.trim().toLowerCase();
|
|
5180
|
+
return bundle.skills.find((skill) => skill.name.toLowerCase() === wanted || skill.sourceName.toLowerCase() === wanted);
|
|
5181
|
+
}
|
|
5182
|
+
var RBXM_MAGIC, RBXM_HEADER_BYTES, RBXM_CHUNK_HEADER_BYTES, ZSTD_MAGIC, STRING_PROPERTY_TYPE, MAX_DISCOVERY_DEPTH, ByteReader, cachedBundle;
|
|
5183
|
+
var init_studio_skills = __esm({
|
|
5184
|
+
"../core/dist/studio-skills.js"() {
|
|
5185
|
+
"use strict";
|
|
5186
|
+
init_esm();
|
|
5187
|
+
init_studio_instance_manager();
|
|
5188
|
+
RBXM_MAGIC = Buffer.from("<roblox!\x89\xFF\r\n\n", "latin1");
|
|
5189
|
+
RBXM_HEADER_BYTES = 32;
|
|
5190
|
+
RBXM_CHUNK_HEADER_BYTES = 16;
|
|
5191
|
+
ZSTD_MAGIC = Buffer.from([40, 181, 47, 253]);
|
|
5192
|
+
STRING_PROPERTY_TYPE = 1;
|
|
5193
|
+
MAX_DISCOVERY_DEPTH = 6;
|
|
5194
|
+
ByteReader = class {
|
|
5195
|
+
bytes;
|
|
5196
|
+
label;
|
|
5197
|
+
offset = 0;
|
|
5198
|
+
constructor(bytes, label) {
|
|
5199
|
+
this.bytes = bytes;
|
|
5200
|
+
this.label = label;
|
|
5201
|
+
}
|
|
5202
|
+
get remaining() {
|
|
5203
|
+
return this.bytes.length - this.offset;
|
|
5204
|
+
}
|
|
5205
|
+
requireBytes(length) {
|
|
5206
|
+
if (!Number.isSafeInteger(length) || length < 0 || this.remaining < length) {
|
|
5207
|
+
throw new Error(`Invalid ${this.label}: requested ${length} bytes with ${this.remaining} remaining`);
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
readUint8() {
|
|
5211
|
+
this.requireBytes(1);
|
|
5212
|
+
return this.bytes[this.offset++];
|
|
5213
|
+
}
|
|
5214
|
+
readUint16() {
|
|
5215
|
+
this.requireBytes(2);
|
|
5216
|
+
const value = this.bytes.readUInt16LE(this.offset);
|
|
5217
|
+
this.offset += 2;
|
|
5218
|
+
return value;
|
|
5219
|
+
}
|
|
5220
|
+
readUint32() {
|
|
5221
|
+
this.requireBytes(4);
|
|
5222
|
+
const value = this.bytes.readUInt32LE(this.offset);
|
|
5223
|
+
this.offset += 4;
|
|
5224
|
+
return value;
|
|
5225
|
+
}
|
|
5226
|
+
readInt32() {
|
|
5227
|
+
this.requireBytes(4);
|
|
5228
|
+
const value = this.bytes.readInt32LE(this.offset);
|
|
5229
|
+
this.offset += 4;
|
|
5230
|
+
return value;
|
|
5231
|
+
}
|
|
5232
|
+
readBytes(length) {
|
|
5233
|
+
this.requireBytes(length);
|
|
5234
|
+
const value = this.bytes.subarray(this.offset, this.offset + length);
|
|
5235
|
+
this.offset += length;
|
|
5236
|
+
return value;
|
|
5237
|
+
}
|
|
5238
|
+
skip(length) {
|
|
5239
|
+
this.requireBytes(length);
|
|
5240
|
+
this.offset += length;
|
|
5241
|
+
}
|
|
5242
|
+
readString() {
|
|
5243
|
+
const length = this.readUint32();
|
|
5244
|
+
return this.readBytes(length).toString("utf8");
|
|
5245
|
+
}
|
|
5246
|
+
};
|
|
5247
|
+
}
|
|
5248
|
+
});
|
|
5249
|
+
|
|
3017
5250
|
// ../core/dist/jpeg-encoder.js
|
|
3018
5251
|
function rgbaToJpeg(rgba, width, height, quality = 80) {
|
|
3019
5252
|
if (width <= 0 || height <= 0)
|
|
@@ -4033,7 +6266,7 @@ var init_png_encoder = __esm({
|
|
|
4033
6266
|
// ../core/dist/tools/index.js
|
|
4034
6267
|
import * as fs3 from "fs";
|
|
4035
6268
|
import * as os3 from "os";
|
|
4036
|
-
import * as
|
|
6269
|
+
import * as path5 from "path";
|
|
4037
6270
|
function multiplayerStopDisabledBody() {
|
|
4038
6271
|
return {
|
|
4039
6272
|
success: false,
|
|
@@ -4058,7 +6291,7 @@ function encodeImageFromRgbaResponse(response, format, quality) {
|
|
|
4058
6291
|
};
|
|
4059
6292
|
}
|
|
4060
6293
|
function sleep(ms) {
|
|
4061
|
-
return new Promise((
|
|
6294
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
4062
6295
|
}
|
|
4063
6296
|
function errorMessage(error) {
|
|
4064
6297
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -4120,7 +6353,7 @@ function loadMicroProfilerBaseline(source, sourcePath) {
|
|
|
4120
6353
|
if (typeof sourcePath !== "string" || sourcePath === "") {
|
|
4121
6354
|
throw new Error("baseline_path must be a non-empty string when provided");
|
|
4122
6355
|
}
|
|
4123
|
-
const resolved =
|
|
6356
|
+
const resolved = path5.resolve(sourcePath);
|
|
4124
6357
|
const parsed = JSON.parse(fs3.readFileSync(resolved, "utf8"));
|
|
4125
6358
|
const record = asRecord(parsed);
|
|
4126
6359
|
if (!record)
|
|
@@ -4581,6 +6814,7 @@ var init_tools = __esm({
|
|
|
4581
6814
|
init_studio_instance_manager();
|
|
4582
6815
|
init_image_decode();
|
|
4583
6816
|
init_roblox_docs();
|
|
6817
|
+
init_studio_skills();
|
|
4584
6818
|
init_jpeg_encoder();
|
|
4585
6819
|
init_png_encoder();
|
|
4586
6820
|
MAX_INLINE_IMAGE_BYTES = 6e6;
|
|
@@ -4648,10 +6882,52 @@ var init_tools = __esm({
|
|
|
4648
6882
|
this.openCloudClient = new OpenCloudClient();
|
|
4649
6883
|
this.cookieClient = new RobloxCookieClient();
|
|
4650
6884
|
this.instanceManager = new StudioInstanceManager();
|
|
6885
|
+
this.bridge.onInstanceRegistered((instance) => this._associateManagedEditConnection(instance));
|
|
4651
6886
|
}
|
|
4652
6887
|
_textResult(body) {
|
|
4653
6888
|
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
4654
6889
|
}
|
|
6890
|
+
async getRobloxSkills(action, name) {
|
|
6891
|
+
if (action !== "list" && action !== "get") {
|
|
6892
|
+
throw new Error('get_roblox_skills action must be "list" or "get"');
|
|
6893
|
+
}
|
|
6894
|
+
const bundle = loadBuiltInStudioSkills();
|
|
6895
|
+
const bundleMetadata = {
|
|
6896
|
+
source: "installed-studio-assistant",
|
|
6897
|
+
studioVersion: bundle.studioVersion,
|
|
6898
|
+
bundlePath: bundle.bundlePath,
|
|
6899
|
+
bundleModifiedAt: bundle.bundleModifiedAt,
|
|
6900
|
+
bundleSha256: bundle.bundleSha256
|
|
6901
|
+
};
|
|
6902
|
+
if (action === "list") {
|
|
6903
|
+
return this._textResult({
|
|
6904
|
+
action,
|
|
6905
|
+
...bundleMetadata,
|
|
6906
|
+
count: bundle.skills.length,
|
|
6907
|
+
skills: bundle.skills.map((skill2) => ({
|
|
6908
|
+
name: skill2.name,
|
|
6909
|
+
sourceName: skill2.sourceName,
|
|
6910
|
+
description: skill2.description,
|
|
6911
|
+
document: skill2.document,
|
|
6912
|
+
hasCombinedDocument: skill2.hasCombinedDocument,
|
|
6913
|
+
contentLength: skill2.contentLength,
|
|
6914
|
+
contentSha256: skill2.contentSha256
|
|
6915
|
+
}))
|
|
6916
|
+
});
|
|
6917
|
+
}
|
|
6918
|
+
if (!name || typeof name !== "string" || !name.trim()) {
|
|
6919
|
+
throw new Error('get_roblox_skills action="get" requires a skill name from action="list"');
|
|
6920
|
+
}
|
|
6921
|
+
const skill = findBuiltInStudioSkill(bundle, name);
|
|
6922
|
+
if (!skill) {
|
|
6923
|
+
throw new Error(`Built-in Studio skill "${name}" was not found. Available skills: ` + bundle.skills.map((candidate) => candidate.name).join(", "));
|
|
6924
|
+
}
|
|
6925
|
+
return this._textResult({
|
|
6926
|
+
action,
|
|
6927
|
+
...bundleMetadata,
|
|
6928
|
+
skill
|
|
6929
|
+
});
|
|
6930
|
+
}
|
|
4655
6931
|
async getRobloxDocs(name, docType, section) {
|
|
4656
6932
|
if (!name || typeof name !== "string") {
|
|
4657
6933
|
throw new Error('get_roblox_docs requires a name (e.g. "ProximityPrompt")');
|
|
@@ -5004,8 +7280,8 @@ var init_tools = __esm({
|
|
|
5004
7280
|
timedOut: true
|
|
5005
7281
|
};
|
|
5006
7282
|
}
|
|
5007
|
-
async getFileTree(
|
|
5008
|
-
const response = await this._callSingle("/api/file-tree", { path:
|
|
7283
|
+
async getFileTree(path6 = "", instance_id) {
|
|
7284
|
+
const response = await this._callSingle("/api/file-tree", { path: path6 }, void 0, instance_id);
|
|
5009
7285
|
return {
|
|
5010
7286
|
content: [
|
|
5011
7287
|
{
|
|
@@ -5122,9 +7398,9 @@ var init_tools = __esm({
|
|
|
5122
7398
|
]
|
|
5123
7399
|
};
|
|
5124
7400
|
}
|
|
5125
|
-
async getProjectStructure(
|
|
7401
|
+
async getProjectStructure(path6, maxDepth, scriptsOnly, instance_id) {
|
|
5126
7402
|
const response = await this._callSingle("/api/project-structure", {
|
|
5127
|
-
path:
|
|
7403
|
+
path: path6,
|
|
5128
7404
|
maxDepth,
|
|
5129
7405
|
scriptsOnly
|
|
5130
7406
|
}, void 0, instance_id);
|
|
@@ -6048,8 +8324,8 @@ ${code}`
|
|
|
6048
8324
|
const rawJson = mutable.raw_json;
|
|
6049
8325
|
if (typeof rawJson === "string") {
|
|
6050
8326
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
6051
|
-
const resolvedOutputPath =
|
|
6052
|
-
fs3.mkdirSync(
|
|
8327
|
+
const resolvedOutputPath = path5.resolve(outputPath);
|
|
8328
|
+
fs3.mkdirSync(path5.dirname(resolvedOutputPath), { recursive: true });
|
|
6053
8329
|
fs3.writeFileSync(resolvedOutputPath, rawJson, "utf8");
|
|
6054
8330
|
mutable.output_path = resolvedOutputPath;
|
|
6055
8331
|
}
|
|
@@ -6111,8 +8387,8 @@ ${code}`
|
|
|
6111
8387
|
const rawSnapshotBase64 = mutable.raw_snapshot_base64;
|
|
6112
8388
|
if (typeof rawSnapshotBase64 === "string") {
|
|
6113
8389
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
6114
|
-
const resolvedOutputPath =
|
|
6115
|
-
fs3.mkdirSync(
|
|
8390
|
+
const resolvedOutputPath = path5.resolve(outputPath);
|
|
8391
|
+
fs3.mkdirSync(path5.dirname(resolvedOutputPath), { recursive: true });
|
|
6116
8392
|
fs3.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
|
|
6117
8393
|
mutable.output_path = resolvedOutputPath;
|
|
6118
8394
|
}
|
|
@@ -6127,8 +8403,8 @@ ${code}`
|
|
|
6127
8403
|
});
|
|
6128
8404
|
}
|
|
6129
8405
|
if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
|
|
6130
|
-
const resolvedSummaryPath =
|
|
6131
|
-
fs3.mkdirSync(
|
|
8406
|
+
const resolvedSummaryPath = path5.resolve(summaryOutputPath);
|
|
8407
|
+
fs3.mkdirSync(path5.dirname(resolvedSummaryPath), { recursive: true });
|
|
6132
8408
|
fs3.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
|
|
6133
8409
|
mutable.summary_output_path = resolvedSummaryPath;
|
|
6134
8410
|
}
|
|
@@ -6196,11 +8472,18 @@ ${code}`
|
|
|
6196
8472
|
return record.placeId !== void 0 && instance.placeId === record.placeId;
|
|
6197
8473
|
}
|
|
6198
8474
|
if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
|
|
6199
|
-
const expectedName =
|
|
8475
|
+
const expectedName = path5.basename(record.localPlaceFile);
|
|
6200
8476
|
return instance.placeName === expectedName || instance.dataModelName === expectedName;
|
|
6201
8477
|
}
|
|
6202
8478
|
return true;
|
|
6203
8479
|
}
|
|
8480
|
+
_associateManagedEditConnection(instance) {
|
|
8481
|
+
if (instance.role !== "edit")
|
|
8482
|
+
return;
|
|
8483
|
+
const candidate = this.instanceManager.pendingLaunches().filter((record) => instance.connectedAt >= record.launchedAt - 1e3).filter((record) => this._matchesManagedLaunch(record, instance)).sort((a, b) => a.launchedAt - b.launchedAt)[0];
|
|
8484
|
+
if (candidate)
|
|
8485
|
+
this.instanceManager.attachInstanceId(candidate, instance.instanceId);
|
|
8486
|
+
}
|
|
6204
8487
|
async _deriveUniverseId(placeId) {
|
|
6205
8488
|
const response = await fetch(`https://apis.roblox.com/universes/v1/places/${placeId}/universe`);
|
|
6206
8489
|
if (!response.ok) {
|
|
@@ -6216,6 +8499,10 @@ ${code}`
|
|
|
6216
8499
|
async _waitForManagedEditConnection(record, beforeKeys, timeoutMs) {
|
|
6217
8500
|
const deadline = Date.now() + timeoutMs;
|
|
6218
8501
|
while (Date.now() < deadline) {
|
|
8502
|
+
this.instanceManager.refresh(record);
|
|
8503
|
+
if (record.state === "failed" || record.state === "exited" || record.closedAt !== void 0) {
|
|
8504
|
+
return void 0;
|
|
8505
|
+
}
|
|
6219
8506
|
const candidates = this.bridge.getPublicInstances().filter((instance) => instance.role === "edit").filter((instance) => !beforeKeys.has(this._publicInstanceKey(instance))).filter((instance) => instance.connectedAt >= record.launchedAt - 1e3).filter((instance) => this._matchesManagedLaunch(record, instance)).sort((a, b) => b.connectedAt - a.connectedAt);
|
|
6220
8507
|
if (candidates[0])
|
|
6221
8508
|
return candidates[0];
|
|
@@ -6224,12 +8511,25 @@ ${code}`
|
|
|
6224
8511
|
return void 0;
|
|
6225
8512
|
}
|
|
6226
8513
|
_managedStatus(record) {
|
|
8514
|
+
this.instanceManager.refresh(record);
|
|
6227
8515
|
const connected = record.instanceId ? this.bridge.getPublicInstances().filter((instance) => instance.instanceId === record.instanceId) : [];
|
|
6228
8516
|
return {
|
|
8517
|
+
launch_id: record.recordId,
|
|
6229
8518
|
instance_id: record.instanceId,
|
|
8519
|
+
managed: true,
|
|
8520
|
+
state: record.state,
|
|
8521
|
+
pid: record.nativeProcessId ?? record.spawnPid,
|
|
8522
|
+
process_running: record.closedAt === void 0 && record.exitedAt === void 0,
|
|
6230
8523
|
source: record.source,
|
|
8524
|
+
local_place_file: record.localPlaceFile,
|
|
6231
8525
|
place_id: record.placeId,
|
|
6232
8526
|
place_version: record.placeVersion,
|
|
8527
|
+
launched_at: new Date(record.launchedAt).toISOString(),
|
|
8528
|
+
connected_at: record.connectedAt ? new Date(record.connectedAt).toISOString() : void 0,
|
|
8529
|
+
failed_at: record.failedAt ? new Date(record.failedAt).toISOString() : void 0,
|
|
8530
|
+
exited_at: record.exitedAt ? new Date(record.exitedAt).toISOString() : void 0,
|
|
8531
|
+
exit_code: record.exitCode,
|
|
8532
|
+
failure_reason: record.failureReason,
|
|
6233
8533
|
connected: connected.length > 0,
|
|
6234
8534
|
roles: connected.map((instance) => instance.role).sort()
|
|
6235
8535
|
};
|
|
@@ -6241,6 +8541,10 @@ ${code}`
|
|
|
6241
8541
|
async manageInstance(request) {
|
|
6242
8542
|
const action = request.action;
|
|
6243
8543
|
const instance_id = typeof request.instance_id === "string" ? request.instance_id : void 0;
|
|
8544
|
+
const launch_id = typeof request.launch_id === "string" ? request.launch_id : void 0;
|
|
8545
|
+
if (instance_id && launch_id) {
|
|
8546
|
+
throw new Error("manage_instance accepts only one of instance_id or launch_id.");
|
|
8547
|
+
}
|
|
6244
8548
|
if (action !== "launch" && action !== "close" && action !== "status" && action !== "list_place_versions") {
|
|
6245
8549
|
throw new Error("manage_instance requires action=launch|close|status|list_place_versions");
|
|
6246
8550
|
}
|
|
@@ -6268,18 +8572,26 @@ ${code}`
|
|
|
6268
8572
|
return this._textResult(body);
|
|
6269
8573
|
}
|
|
6270
8574
|
if (action === "status") {
|
|
8575
|
+
if (launch_id) {
|
|
8576
|
+
const record2 = this.instanceManager.getByLaunchId(launch_id);
|
|
8577
|
+
if (!record2)
|
|
8578
|
+
return this._textResult({ error: "Launch is not managed.", launch_id });
|
|
8579
|
+
return this._textResult(this._managedStatus(record2));
|
|
8580
|
+
}
|
|
6271
8581
|
if (instance_id) {
|
|
6272
8582
|
const record2 = this.instanceManager.get(instance_id);
|
|
6273
8583
|
const connected2 = this.bridge.getPublicInstances().filter((instance) => instance.instanceId === instance_id);
|
|
6274
8584
|
if (!record2 && connected2.length === 0) {
|
|
6275
8585
|
return this._textResult({ error: "Instance is not connected or managed.", instance_id });
|
|
6276
8586
|
}
|
|
8587
|
+
if (record2)
|
|
8588
|
+
return this._textResult(this._managedStatus(record2));
|
|
6277
8589
|
return this._textResult({
|
|
6278
8590
|
instance_id,
|
|
6279
|
-
managed:
|
|
6280
|
-
|
|
6281
|
-
place_id:
|
|
6282
|
-
|
|
8591
|
+
managed: false,
|
|
8592
|
+
state: "connected",
|
|
8593
|
+
place_id: connected2[0]?.placeId,
|
|
8594
|
+
connected: true,
|
|
6283
8595
|
roles: connected2.map((instance) => instance.role).sort()
|
|
6284
8596
|
});
|
|
6285
8597
|
}
|
|
@@ -6295,14 +8607,32 @@ ${code}`
|
|
|
6295
8607
|
}
|
|
6296
8608
|
if (action === "close") {
|
|
6297
8609
|
let record2;
|
|
8610
|
+
if (launch_id) {
|
|
8611
|
+
record2 = this.instanceManager.getByLaunchId(launch_id);
|
|
8612
|
+
if (!record2)
|
|
8613
|
+
return this._textResult({ error: "Launch is not managed.", launch_id });
|
|
8614
|
+
const connectedInstanceId = record2.instanceId;
|
|
8615
|
+
const closeResult2 = record2.closedAt === void 0 ? this.instanceManager.close(record2) : { status: "already_closed" };
|
|
8616
|
+
if (connectedInstanceId) {
|
|
8617
|
+
await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
|
|
8618
|
+
await sleep(500);
|
|
8619
|
+
await this.bridge.unregisterInstanceIdEverywhere(connectedInstanceId);
|
|
8620
|
+
}
|
|
8621
|
+
return this._textResult({
|
|
8622
|
+
...this._managedStatus(record2),
|
|
8623
|
+
message: closeResult2.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
8624
|
+
});
|
|
8625
|
+
}
|
|
6298
8626
|
if (instance_id) {
|
|
8627
|
+
const recordBeforeClose = this.instanceManager.get(instance_id);
|
|
6299
8628
|
const managedClose = this.instanceManager.closeByInstanceId(instance_id);
|
|
6300
8629
|
if (managedClose.status !== "not_found") {
|
|
6301
8630
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
6302
8631
|
await sleep(500);
|
|
6303
8632
|
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
8633
|
+
const closedRecord = recordBeforeClose ?? (managedClose.launchId ? this.instanceManager.getByLaunchId(managedClose.launchId) : void 0);
|
|
6304
8634
|
return this._textResult({
|
|
6305
|
-
instance_id,
|
|
8635
|
+
...closedRecord ? this._managedStatus(closedRecord) : { instance_id },
|
|
6306
8636
|
message: managedClose.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
6307
8637
|
});
|
|
6308
8638
|
}
|
|
@@ -6349,7 +8679,7 @@ ${code}`
|
|
|
6349
8679
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
6350
8680
|
}
|
|
6351
8681
|
return this._textResult({
|
|
6352
|
-
|
|
8682
|
+
...this._managedStatus(record2),
|
|
6353
8683
|
message: closeResult.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
6354
8684
|
});
|
|
6355
8685
|
}
|
|
@@ -6376,24 +8706,34 @@ ${code}`
|
|
|
6376
8706
|
localPlaceFile,
|
|
6377
8707
|
placeId,
|
|
6378
8708
|
universeId,
|
|
6379
|
-
placeVersion
|
|
8709
|
+
placeVersion,
|
|
8710
|
+
connectionTimeoutMs: timeoutMs
|
|
6380
8711
|
});
|
|
6381
8712
|
if (!waitForConnection) {
|
|
6382
|
-
return this._textResult({
|
|
8713
|
+
return this._textResult({
|
|
8714
|
+
...this._managedStatus(record),
|
|
8715
|
+
message: "Studio launch requested."
|
|
8716
|
+
});
|
|
6383
8717
|
}
|
|
6384
8718
|
const connected = await this._waitForManagedEditConnection(record, beforeKeys, timeoutMs);
|
|
6385
8719
|
if (!connected) {
|
|
6386
|
-
|
|
6387
|
-
this.instanceManager.
|
|
6388
|
-
}
|
|
8720
|
+
if (record.state === "launching") {
|
|
8721
|
+
this.instanceManager.markFailed(record, "Studio launched, but the MCP plugin did not connect before timeout.");
|
|
8722
|
+
}
|
|
8723
|
+
if (record.closedAt === void 0) {
|
|
8724
|
+
try {
|
|
8725
|
+
this.instanceManager.close(record);
|
|
8726
|
+
} catch {
|
|
8727
|
+
}
|
|
6389
8728
|
}
|
|
6390
8729
|
return this._textResult({
|
|
6391
|
-
|
|
8730
|
+
...this._managedStatus(record),
|
|
8731
|
+
error: record.failureReason ?? "Studio launched, but the MCP plugin did not connect before timeout."
|
|
6392
8732
|
});
|
|
6393
8733
|
}
|
|
6394
8734
|
this.instanceManager.attachInstanceId(record, connected.instanceId);
|
|
6395
8735
|
return this._textResult({
|
|
6396
|
-
|
|
8736
|
+
...this._managedStatus(record),
|
|
6397
8737
|
message: launchSource === "place_revision" ? `Studio opened place revision ${placeVersion}.` : "Studio opened."
|
|
6398
8738
|
});
|
|
6399
8739
|
}
|
|
@@ -6576,16 +8916,16 @@ ${code}`
|
|
|
6576
8916
|
try {
|
|
6577
8917
|
editState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "edit");
|
|
6578
8918
|
body.edit = editState;
|
|
6579
|
-
} catch (
|
|
6580
|
-
body.edit = { error:
|
|
8919
|
+
} catch (err2) {
|
|
8920
|
+
body.edit = { error: err2 instanceof Error ? err2.message : String(err2) };
|
|
6581
8921
|
}
|
|
6582
8922
|
}
|
|
6583
8923
|
if (server) {
|
|
6584
8924
|
try {
|
|
6585
8925
|
serverState = await this.client.request("/api/multiplayer-test-state", {}, instanceId, "server");
|
|
6586
8926
|
body.server = serverState;
|
|
6587
|
-
} catch (
|
|
6588
|
-
body.server = { error:
|
|
8927
|
+
} catch (err2) {
|
|
8928
|
+
body.server = { error: err2 instanceof Error ? err2.message : String(err2) };
|
|
6589
8929
|
}
|
|
6590
8930
|
}
|
|
6591
8931
|
const session = editState?.session;
|
|
@@ -6911,14 +9251,14 @@ ${code}`
|
|
|
6911
9251
|
};
|
|
6912
9252
|
}
|
|
6913
9253
|
static findProjectRoot(startDir) {
|
|
6914
|
-
let dir =
|
|
9254
|
+
let dir = path5.resolve(startDir);
|
|
6915
9255
|
let previous = "";
|
|
6916
9256
|
while (dir !== previous) {
|
|
6917
|
-
if (fs3.existsSync(
|
|
9257
|
+
if (fs3.existsSync(path5.join(dir, ".git")) || fs3.existsSync(path5.join(dir, "package.json"))) {
|
|
6918
9258
|
return dir;
|
|
6919
9259
|
}
|
|
6920
9260
|
previous = dir;
|
|
6921
|
-
dir =
|
|
9261
|
+
dir = path5.dirname(dir);
|
|
6922
9262
|
}
|
|
6923
9263
|
return null;
|
|
6924
9264
|
}
|
|
@@ -6932,7 +9272,7 @@ ${code}`
|
|
|
6932
9272
|
}
|
|
6933
9273
|
}
|
|
6934
9274
|
static ensureWritableDirectory(candidate, label) {
|
|
6935
|
-
const resolved =
|
|
9275
|
+
const resolved = path5.resolve(candidate);
|
|
6936
9276
|
try {
|
|
6937
9277
|
fs3.mkdirSync(resolved, { recursive: true });
|
|
6938
9278
|
} catch (error) {
|
|
@@ -6953,11 +9293,11 @@ ${code}`
|
|
|
6953
9293
|
if (_RobloxStudioTools._cachedLibraryPath)
|
|
6954
9294
|
return _RobloxStudioTools._cachedLibraryPath;
|
|
6955
9295
|
const overridePath = process.env.ROBLOXSTUDIO_MCP_BUILD_LIBRARY || process.env.BUILD_LIBRARY_PATH;
|
|
6956
|
-
const cwd =
|
|
9296
|
+
const cwd = path5.resolve(process.cwd());
|
|
6957
9297
|
const projectRoot = _RobloxStudioTools.findProjectRoot(cwd);
|
|
6958
|
-
const homeLibraryPath =
|
|
6959
|
-
const projectLibraryPath = projectRoot ?
|
|
6960
|
-
const cwdLibraryPath =
|
|
9298
|
+
const homeLibraryPath = path5.join(os3.homedir(), ".robloxstudio-mcp", "build-library");
|
|
9299
|
+
const projectLibraryPath = projectRoot ? path5.join(projectRoot, "build-library") : null;
|
|
9300
|
+
const cwdLibraryPath = path5.join(cwd, "build-library");
|
|
6961
9301
|
let result;
|
|
6962
9302
|
if (overridePath) {
|
|
6963
9303
|
result = _RobloxStudioTools.ensureWritableDirectory(overridePath, "override");
|
|
@@ -6971,12 +9311,12 @@ ${code}`
|
|
|
6971
9311
|
}
|
|
6972
9312
|
})());
|
|
6973
9313
|
if (existing) {
|
|
6974
|
-
result =
|
|
9314
|
+
result = path5.resolve(existing);
|
|
6975
9315
|
} else if (projectLibraryPath) {
|
|
6976
9316
|
try {
|
|
6977
9317
|
result = _RobloxStudioTools.ensureWritableDirectory(projectLibraryPath, "project-root");
|
|
6978
|
-
} catch (
|
|
6979
|
-
console.error(`Warning: could not create build-library at project root (${projectLibraryPath}): ${
|
|
9318
|
+
} catch (err2) {
|
|
9319
|
+
console.error(`Warning: could not create build-library at project root (${projectLibraryPath}): ${err2.message}. Falling back to home directory.`);
|
|
6980
9320
|
result = _RobloxStudioTools.ensureWritableDirectory(homeLibraryPath, "home");
|
|
6981
9321
|
}
|
|
6982
9322
|
} else {
|
|
@@ -6998,8 +9338,8 @@ ${code}`
|
|
|
6998
9338
|
if (response && response.success && response.buildData) {
|
|
6999
9339
|
const buildData = response.buildData;
|
|
7000
9340
|
const buildId = buildData.id || `${style}/exported`;
|
|
7001
|
-
const filePath =
|
|
7002
|
-
const dirPath =
|
|
9341
|
+
const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildId}.json`);
|
|
9342
|
+
const dirPath = path5.dirname(filePath);
|
|
7003
9343
|
if (!fs3.existsSync(dirPath)) {
|
|
7004
9344
|
fs3.mkdirSync(dirPath, { recursive: true });
|
|
7005
9345
|
}
|
|
@@ -7100,8 +9440,8 @@ ${code}`
|
|
|
7100
9440
|
const normalizedParts = this.normalizeBuildParts(parts, new Set(Object.keys(normalizedPalette)));
|
|
7101
9441
|
const computedBounds = bounds || this.computeBounds(normalizedParts);
|
|
7102
9442
|
const buildData = { id, style, bounds: computedBounds, palette: normalizedPalette, parts: normalizedParts };
|
|
7103
|
-
const filePath =
|
|
7104
|
-
const dirPath =
|
|
9443
|
+
const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
9444
|
+
const dirPath = path5.dirname(filePath);
|
|
7105
9445
|
if (!fs3.existsSync(dirPath)) {
|
|
7106
9446
|
fs3.mkdirSync(dirPath, { recursive: true });
|
|
7107
9447
|
}
|
|
@@ -7159,8 +9499,8 @@ ${code}`
|
|
|
7159
9499
|
};
|
|
7160
9500
|
if (seed !== void 0)
|
|
7161
9501
|
buildData.generatorSeed = seed;
|
|
7162
|
-
const filePath =
|
|
7163
|
-
const dirPath =
|
|
9502
|
+
const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
9503
|
+
const dirPath = path5.dirname(filePath);
|
|
7164
9504
|
if (!fs3.existsSync(dirPath)) {
|
|
7165
9505
|
fs3.mkdirSync(dirPath, { recursive: true });
|
|
7166
9506
|
}
|
|
@@ -7188,13 +9528,13 @@ ${code}`
|
|
|
7188
9528
|
}
|
|
7189
9529
|
let resolved;
|
|
7190
9530
|
if (typeof buildData === "string") {
|
|
7191
|
-
const filePath =
|
|
9531
|
+
const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildData}.json`);
|
|
7192
9532
|
if (!fs3.existsSync(filePath)) {
|
|
7193
9533
|
throw new Error(`Build not found in library: ${buildData}`);
|
|
7194
9534
|
}
|
|
7195
9535
|
resolved = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
7196
9536
|
} else if (buildData.id && !buildData.parts) {
|
|
7197
|
-
const filePath =
|
|
9537
|
+
const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${buildData.id}.json`);
|
|
7198
9538
|
if (!fs3.existsSync(filePath)) {
|
|
7199
9539
|
throw new Error(`Build not found in library: ${buildData.id}`);
|
|
7200
9540
|
}
|
|
@@ -7221,13 +9561,13 @@ ${code}`
|
|
|
7221
9561
|
const styles = style ? [style] : ["medieval", "modern", "nature", "scifi", "misc"];
|
|
7222
9562
|
const builds = [];
|
|
7223
9563
|
for (const s of styles) {
|
|
7224
|
-
const dirPath =
|
|
9564
|
+
const dirPath = path5.join(libraryPath, s);
|
|
7225
9565
|
if (!fs3.existsSync(dirPath))
|
|
7226
9566
|
continue;
|
|
7227
9567
|
const files = fs3.readdirSync(dirPath).filter((f) => f.endsWith(".json"));
|
|
7228
9568
|
for (const file of files) {
|
|
7229
9569
|
try {
|
|
7230
|
-
const content = fs3.readFileSync(
|
|
9570
|
+
const content = fs3.readFileSync(path5.join(dirPath, file), "utf-8");
|
|
7231
9571
|
const data = JSON.parse(content);
|
|
7232
9572
|
builds.push({
|
|
7233
9573
|
id: data.id || `${s}/${file.replace(".json", "")}`,
|
|
@@ -7266,7 +9606,7 @@ ${code}`
|
|
|
7266
9606
|
if (!id) {
|
|
7267
9607
|
throw new Error("Build ID is required for get_build");
|
|
7268
9608
|
}
|
|
7269
|
-
const filePath =
|
|
9609
|
+
const filePath = path5.join(_RobloxStudioTools.findLibraryPath(), `${id}.json`);
|
|
7270
9610
|
if (!fs3.existsSync(filePath)) {
|
|
7271
9611
|
throw new Error(`Build not found in library: ${id}`);
|
|
7272
9612
|
}
|
|
@@ -7353,7 +9693,7 @@ ${code}`
|
|
|
7353
9693
|
if (!buildId) {
|
|
7354
9694
|
throw new Error(`Invalid ${validatedKeyPath}: model key "${modelKey}" is not defined in sceneData.models`);
|
|
7355
9695
|
}
|
|
7356
|
-
const filePath =
|
|
9696
|
+
const filePath = path5.join(libraryPath, `${buildId}.json`);
|
|
7357
9697
|
if (!fs3.existsSync(filePath)) {
|
|
7358
9698
|
throw new Error(`Build not found in library: ${buildId}`);
|
|
7359
9699
|
}
|
|
@@ -7701,7 +10041,7 @@ ${code}`
|
|
|
7701
10041
|
throw new Error(`File not found: ${filePath}`);
|
|
7702
10042
|
}
|
|
7703
10043
|
const fileContent = fs3.readFileSync(filePath);
|
|
7704
|
-
const fileName =
|
|
10044
|
+
const fileName = path5.basename(filePath);
|
|
7705
10045
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
7706
10046
|
const result2 = await this.cookieClient.uploadDecal(fileContent, displayName, description || "");
|
|
7707
10047
|
return {
|
|
@@ -7926,12 +10266,12 @@ ${code}`
|
|
|
7926
10266
|
return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
|
|
7927
10267
|
}
|
|
7928
10268
|
const bytes = Buffer.from(response.base64, "base64");
|
|
7929
|
-
const resolved =
|
|
10269
|
+
const resolved = path5.resolve(outputPath);
|
|
7930
10270
|
try {
|
|
7931
|
-
fs3.mkdirSync(
|
|
10271
|
+
fs3.mkdirSync(path5.dirname(resolved), { recursive: true });
|
|
7932
10272
|
fs3.writeFileSync(resolved, bytes);
|
|
7933
|
-
} catch (
|
|
7934
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${
|
|
10273
|
+
} catch (err2) {
|
|
10274
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
|
|
7935
10275
|
}
|
|
7936
10276
|
return {
|
|
7937
10277
|
content: [{
|
|
@@ -7962,11 +10302,11 @@ ${code}`
|
|
|
7962
10302
|
let bytes;
|
|
7963
10303
|
let sourceLabel;
|
|
7964
10304
|
if (source.path !== void 0) {
|
|
7965
|
-
const resolved =
|
|
10305
|
+
const resolved = path5.resolve(source.path);
|
|
7966
10306
|
try {
|
|
7967
10307
|
bytes = fs3.readFileSync(resolved);
|
|
7968
|
-
} catch (
|
|
7969
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${
|
|
10308
|
+
} catch (err2) {
|
|
10309
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err2.message}` }) }] };
|
|
7970
10310
|
}
|
|
7971
10311
|
sourceLabel = resolved;
|
|
7972
10312
|
} else if (source.url !== void 0) {
|
|
@@ -7995,15 +10335,15 @@ ${code}`
|
|
|
7995
10335
|
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url}: downloaded ${arr.byteLength} bytes exceeds ${MAX_IMPORT_BYTES} byte cap` }) }] };
|
|
7996
10336
|
}
|
|
7997
10337
|
bytes = Buffer.from(arr);
|
|
7998
|
-
} catch (
|
|
7999
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} failed: ${
|
|
10338
|
+
} catch (err2) {
|
|
10339
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `fetch ${source.url} failed: ${err2.message}` }) }] };
|
|
8000
10340
|
}
|
|
8001
10341
|
sourceLabel = source.url;
|
|
8002
10342
|
} else {
|
|
8003
10343
|
try {
|
|
8004
10344
|
bytes = Buffer.from(source.base64, "base64");
|
|
8005
|
-
} catch (
|
|
8006
|
-
return { content: [{ type: "text", text: JSON.stringify({ error: `base64 decode failed: ${
|
|
10345
|
+
} catch (err2) {
|
|
10346
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `base64 decode failed: ${err2.message}` }) }] };
|
|
8007
10347
|
}
|
|
8008
10348
|
sourceLabel = `base64(${bytes.length}B)`;
|
|
8009
10349
|
}
|
|
@@ -8107,26 +10447,42 @@ var init_proxy_bridge_service = __esm({
|
|
|
8107
10447
|
init_bridge_service();
|
|
8108
10448
|
ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
8109
10449
|
primaryBaseUrl;
|
|
10450
|
+
authToken;
|
|
8110
10451
|
proxyInstanceId;
|
|
8111
10452
|
proxyRequestTimeout = 3e4;
|
|
8112
10453
|
cachedInstances = [];
|
|
8113
10454
|
refreshTimer;
|
|
8114
10455
|
static REFRESH_INTERVAL_MS = 1e3;
|
|
8115
|
-
constructor(primaryBaseUrl) {
|
|
10456
|
+
constructor(primaryBaseUrl, authToken) {
|
|
8116
10457
|
super();
|
|
8117
10458
|
this.primaryBaseUrl = primaryBaseUrl;
|
|
10459
|
+
this.authToken = authToken;
|
|
8118
10460
|
this.proxyInstanceId = uuidv42();
|
|
8119
10461
|
this.refreshInstances();
|
|
8120
10462
|
this.refreshTimer = setInterval(() => this.refreshInstances(), _ProxyBridgeService.REFRESH_INTERVAL_MS);
|
|
8121
10463
|
}
|
|
10464
|
+
authHeaders(extra) {
|
|
10465
|
+
const headers = { ...extra };
|
|
10466
|
+
if (this.authToken)
|
|
10467
|
+
headers["X-MCP-Auth"] = this.authToken;
|
|
10468
|
+
return headers;
|
|
10469
|
+
}
|
|
8122
10470
|
async refreshInstances() {
|
|
8123
10471
|
try {
|
|
8124
|
-
const res = await fetch(`${this.primaryBaseUrl}/instances
|
|
10472
|
+
const res = await fetch(`${this.primaryBaseUrl}/instances`, {
|
|
10473
|
+
headers: this.authHeaders()
|
|
10474
|
+
});
|
|
8125
10475
|
if (!res.ok)
|
|
8126
10476
|
return;
|
|
8127
10477
|
const body = await res.json();
|
|
8128
10478
|
if (Array.isArray(body.instances)) {
|
|
10479
|
+
const previousKeys = new Set(this.cachedInstances.map((instance) => `${instance.pluginSessionId}\0${instance.instanceId}\0${instance.role}\0${instance.connectedAt}`));
|
|
8129
10480
|
this.cachedInstances = body.instances;
|
|
10481
|
+
for (const instance of body.instances) {
|
|
10482
|
+
const key = `${instance.pluginSessionId}\0${instance.instanceId}\0${instance.role}\0${instance.connectedAt}`;
|
|
10483
|
+
if (!previousKeys.has(key))
|
|
10484
|
+
this.notifyInstanceRegistered(toPublic(instance));
|
|
10485
|
+
}
|
|
8130
10486
|
}
|
|
8131
10487
|
} catch {
|
|
8132
10488
|
}
|
|
@@ -8137,7 +10493,7 @@ var init_proxy_bridge_service = __esm({
|
|
|
8137
10493
|
async unregisterInstanceIdEverywhere(instanceId) {
|
|
8138
10494
|
const response = await fetch(`${this.primaryBaseUrl}/unregister-instance-id`, {
|
|
8139
10495
|
method: "POST",
|
|
8140
|
-
headers: { "Content-Type": "application/json" },
|
|
10496
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
8141
10497
|
body: JSON.stringify({ instanceId })
|
|
8142
10498
|
});
|
|
8143
10499
|
if (!response.ok) {
|
|
@@ -8166,7 +10522,7 @@ var init_proxy_bridge_service = __esm({
|
|
|
8166
10522
|
try {
|
|
8167
10523
|
const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
|
|
8168
10524
|
method: "POST",
|
|
8169
|
-
headers: { "Content-Type": "application/json" },
|
|
10525
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
8170
10526
|
body: JSON.stringify({
|
|
8171
10527
|
endpoint,
|
|
8172
10528
|
data,
|
|
@@ -8186,12 +10542,12 @@ var init_proxy_bridge_service = __esm({
|
|
|
8186
10542
|
throw new Error(result.error);
|
|
8187
10543
|
}
|
|
8188
10544
|
return result.response;
|
|
8189
|
-
} catch (
|
|
10545
|
+
} catch (err2) {
|
|
8190
10546
|
clearTimeout(timeoutId);
|
|
8191
|
-
if (
|
|
10547
|
+
if (err2.name === "AbortError") {
|
|
8192
10548
|
throw new Error("Proxy request timeout");
|
|
8193
10549
|
}
|
|
8194
|
-
throw
|
|
10550
|
+
throw err2;
|
|
8195
10551
|
}
|
|
8196
10552
|
}
|
|
8197
10553
|
cleanupOldRequests() {
|
|
@@ -8211,6 +10567,7 @@ var init_server = __esm({
|
|
|
8211
10567
|
"../core/dist/server.js"() {
|
|
8212
10568
|
"use strict";
|
|
8213
10569
|
init_http_server();
|
|
10570
|
+
init_auth();
|
|
8214
10571
|
init_tools();
|
|
8215
10572
|
init_bridge_service();
|
|
8216
10573
|
init_proxy_bridge_service();
|
|
@@ -8280,14 +10637,28 @@ var init_server = __esm({
|
|
|
8280
10637
|
}
|
|
8281
10638
|
async run() {
|
|
8282
10639
|
const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
|
|
8283
|
-
const host = process.env.ROBLOX_STUDIO_HOST || "
|
|
10640
|
+
const host = process.env.ROBLOX_STUDIO_HOST?.trim() || "127.0.0.1";
|
|
10641
|
+
if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
|
|
10642
|
+
console.error(`WARNING: ROBLOX_STUDIO_HOST=${host} exposes the MCP bridge beyond this machine. Anyone who can reach the port and knows the auth token can control Roblox Studio.`);
|
|
10643
|
+
}
|
|
10644
|
+
const auth = resolveAuthToken();
|
|
10645
|
+
const security = {
|
|
10646
|
+
authToken: auth.token,
|
|
10647
|
+
authTokenHint: auth.source === "env" ? "The token comes from ROBLOX_STUDIO_AUTH_TOKEN." : auth.filePath ? `The token is in ${auth.filePath}.` : void 0,
|
|
10648
|
+
allowedOrigins: (process.env.ROBLOX_STUDIO_ALLOWED_ORIGINS || "").split(",").map((o) => o.trim()).filter((o) => o !== "")
|
|
10649
|
+
};
|
|
10650
|
+
if (auth.source === "disabled") {
|
|
10651
|
+
console.error("WARNING: ROBLOX_STUDIO_NO_AUTH is set - tool endpoints accept unauthenticated requests.");
|
|
10652
|
+
} else if (auth.filePath) {
|
|
10653
|
+
console.error(`Auth token loaded from ${auth.filePath} (HTTP clients must send X-MCP-Auth or Authorization: Bearer)`);
|
|
10654
|
+
}
|
|
8284
10655
|
let bridgeMode = "primary";
|
|
8285
10656
|
let httpHandle;
|
|
8286
10657
|
let primaryApp;
|
|
8287
10658
|
let boundPort = 0;
|
|
8288
10659
|
let promotionInterval;
|
|
8289
10660
|
try {
|
|
8290
|
-
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
|
|
10661
|
+
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
|
|
8291
10662
|
const result = await listenWithRetry(primaryApp, host, basePort, 1);
|
|
8292
10663
|
httpHandle = result.server;
|
|
8293
10664
|
boundPort = result.port;
|
|
@@ -8296,7 +10667,7 @@ var init_server = __esm({
|
|
|
8296
10667
|
} catch {
|
|
8297
10668
|
bridgeMode = "proxy";
|
|
8298
10669
|
primaryApp = void 0;
|
|
8299
|
-
const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}
|
|
10670
|
+
const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`, auth.token);
|
|
8300
10671
|
this.bridge = proxyBridge;
|
|
8301
10672
|
this.tools = new RobloxStudioTools(this.bridge);
|
|
8302
10673
|
console.error(`Port ${basePort} in use - entering proxy mode (forwarding to localhost:${basePort})`);
|
|
@@ -8304,7 +10675,7 @@ var init_server = __esm({
|
|
|
8304
10675
|
promotionInterval = setInterval(async () => {
|
|
8305
10676
|
const candidateBridge = new BridgeService();
|
|
8306
10677
|
const candidateTools = new RobloxStudioTools(candidateBridge);
|
|
8307
|
-
const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config);
|
|
10678
|
+
const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config, security);
|
|
8308
10679
|
try {
|
|
8309
10680
|
const result = await listenWithRetry(candidateApp, host, basePort, 1);
|
|
8310
10681
|
const oldBridge = this.bridge;
|
|
@@ -8329,7 +10700,7 @@ var init_server = __esm({
|
|
|
8329
10700
|
let legacyHandle;
|
|
8330
10701
|
let legacyApp;
|
|
8331
10702
|
if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
|
|
8332
|
-
legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
|
|
10703
|
+
legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
|
|
8333
10704
|
try {
|
|
8334
10705
|
const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
|
|
8335
10706
|
legacyHandle = result.server;
|
|
@@ -9359,7 +11730,7 @@ var init_definitions = __esm({
|
|
|
9359
11730
|
{
|
|
9360
11731
|
name: "manage_instance",
|
|
9361
11732
|
category: "write",
|
|
9362
|
-
description: 'Launch, close, inspect, and find revisions for Studio instances. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, and place_version to open an older revision. action="
|
|
11733
|
+
description: 'Launch, close, inspect, and find revisions for Studio instances. Every launch returns launch_id, native pid, source, and lifecycle state; status and close accept launch_id before the plugin connects and instance_id after association. Use action="launch" with source="baseplate" for a blank place, or source="local_file" with local_place_file for a local place; neither uses place_id. Use action="list_place_versions" with place_id to retrieve version numbers through Open Cloud asset versions, then action="launch" with source="place_revision", place_id, and place_version to open an older revision. action="launch" source="published_place" opens the latest published place and is blocked if that place_id is already connected; source="place_revision" is allowed because Studio opens explicit past revisions as anonymous local copies. Requires ROBLOX_OPEN_CLOUD_API_KEY with asset:read for list_place_versions.',
|
|
9363
11734
|
inputSchema: {
|
|
9364
11735
|
type: "object",
|
|
9365
11736
|
properties: {
|
|
@@ -9387,11 +11758,11 @@ var init_definitions = __esm({
|
|
|
9387
11758
|
},
|
|
9388
11759
|
wait_for_connection: {
|
|
9389
11760
|
type: "boolean",
|
|
9390
|
-
description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true).'
|
|
11761
|
+
description: 'For action="launch": wait until the MCP plugin connects and return instance_id (default true). false returns launch_id immediately and continues association/failure tracking asynchronously.'
|
|
9391
11762
|
},
|
|
9392
11763
|
timeout_ms: {
|
|
9393
11764
|
type: "number",
|
|
9394
|
-
description: 'For action="launch": max milliseconds
|
|
11765
|
+
description: 'For action="launch": max milliseconds for plugin connection (default 120000). The deadline also applies asynchronously when wait_for_connection=false.'
|
|
9395
11766
|
},
|
|
9396
11767
|
max_page_size: {
|
|
9397
11768
|
type: "number",
|
|
@@ -9403,7 +11774,11 @@ var init_definitions = __esm({
|
|
|
9403
11774
|
},
|
|
9404
11775
|
instance_id: {
|
|
9405
11776
|
type: "string",
|
|
9406
|
-
description: 'For action="close" or action="status": Studio instance to inspect or close.
|
|
11777
|
+
description: 'For action="close" or action="status": connected Studio instance to inspect or close. Mutually exclusive with launch_id.'
|
|
11778
|
+
},
|
|
11779
|
+
launch_id: {
|
|
11780
|
+
type: "string",
|
|
11781
|
+
description: 'For action="close" or action="status": opaque identifier returned by launch. Works before plugin connection and for retained terminal launch status. Mutually exclusive with instance_id.'
|
|
9407
11782
|
}
|
|
9408
11783
|
},
|
|
9409
11784
|
required: ["action"]
|
|
@@ -10186,7 +12561,7 @@ CYLINDER AXIS: Roblox cylinders extend along the X axis. For upright cylinders,
|
|
|
10186
12561
|
EXAMPLE - compact cabin (17 lines):
|
|
10187
12562
|
room(0,0,0,8,4,6,"a","b","a")
|
|
10188
12563
|
roof(0,4,0,8,6,"gable","c")
|
|
10189
|
-
wall(-4
|
|
12564
|
+
wall(-4,-2,4,-2,4,1,"a")
|
|
10190
12565
|
part(0,2,3,3,3,0.3,"a","Block",0.4)
|
|
10191
12566
|
row(-2,0,-1,3,0,2,(i,cx,cy,cz)=>{pew(cx,0,cz,3,2,"d")})
|
|
10192
12567
|
column(-3,0,-2,4,0.5,"a","b")
|
|
@@ -10981,6 +13356,27 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
10981
13356
|
required: ["pattern", "replacement"]
|
|
10982
13357
|
}
|
|
10983
13358
|
},
|
|
13359
|
+
// === Installed Studio Skills ===
|
|
13360
|
+
{
|
|
13361
|
+
name: "get_roblox_skills",
|
|
13362
|
+
category: "read",
|
|
13363
|
+
description: `List or retrieve Roblox-authored skills embedded in the locally installed Studio Assistant bundle. This reads the installed Assistant.rbxm directly, so it does not require a connected Studio place or Roblox's built-in MCP. Use action="list" to discover available names, then action="get" to retrieve the exact Markdown for one skill.`,
|
|
13364
|
+
inputSchema: {
|
|
13365
|
+
type: "object",
|
|
13366
|
+
properties: {
|
|
13367
|
+
action: {
|
|
13368
|
+
type: "string",
|
|
13369
|
+
enum: ["list", "get"],
|
|
13370
|
+
description: "List installed built-in skills or get one skill document."
|
|
13371
|
+
},
|
|
13372
|
+
name: {
|
|
13373
|
+
type: "string",
|
|
13374
|
+
description: 'Skill name returned by action="list". Required for action="get"; both canonical rbx-* and embedded source names are accepted.'
|
|
13375
|
+
}
|
|
13376
|
+
},
|
|
13377
|
+
required: ["action"]
|
|
13378
|
+
}
|
|
13379
|
+
},
|
|
10984
13380
|
// === Documentation ===
|
|
10985
13381
|
{
|
|
10986
13382
|
name: "get_roblox_docs",
|
|
@@ -11164,15 +13560,15 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
11164
13560
|
});
|
|
11165
13561
|
|
|
11166
13562
|
// ../core/dist/install-plugin-helpers.js
|
|
11167
|
-
import { existsSync as
|
|
13563
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, unlinkSync } from "fs";
|
|
11168
13564
|
import { execSync } from "child_process";
|
|
11169
|
-
import { join as
|
|
11170
|
-
import { homedir as
|
|
13565
|
+
import { join as join6 } from "path";
|
|
13566
|
+
import { homedir as homedir5 } from "os";
|
|
11171
13567
|
function isWSL() {
|
|
11172
13568
|
if (process.platform !== "linux")
|
|
11173
13569
|
return false;
|
|
11174
13570
|
try {
|
|
11175
|
-
const v =
|
|
13571
|
+
const v = readFileSync7("/proc/version", "utf8");
|
|
11176
13572
|
return /microsoft|wsl/i.test(v);
|
|
11177
13573
|
} catch {
|
|
11178
13574
|
return false;
|
|
@@ -11190,7 +13586,7 @@ function getWindowsUserPluginsDir() {
|
|
|
11190
13586
|
}).toString().trim();
|
|
11191
13587
|
if (!linuxPath)
|
|
11192
13588
|
return null;
|
|
11193
|
-
return
|
|
13589
|
+
return join6(linuxPath, "Roblox", "Plugins");
|
|
11194
13590
|
} catch {
|
|
11195
13591
|
return null;
|
|
11196
13592
|
}
|
|
@@ -11199,7 +13595,7 @@ function getPluginsFolder() {
|
|
|
11199
13595
|
if (process.env.MCP_PLUGINS_DIR)
|
|
11200
13596
|
return process.env.MCP_PLUGINS_DIR;
|
|
11201
13597
|
if (process.platform === "win32") {
|
|
11202
|
-
return
|
|
13598
|
+
return join6(process.env.LOCALAPPDATA || join6(homedir5(), "AppData", "Local"), "Roblox", "Plugins");
|
|
11203
13599
|
}
|
|
11204
13600
|
if (isWSL()) {
|
|
11205
13601
|
const win = getWindowsUserPluginsDir();
|
|
@@ -11207,18 +13603,18 @@ function getPluginsFolder() {
|
|
|
11207
13603
|
return win;
|
|
11208
13604
|
console.warn("[install-plugin] WSL detected but could not resolve Windows %LOCALAPPDATA%. Falling back to ~/Documents/Roblox/Plugins/ - you will likely need to copy the rbxmx to /mnt/c/Users/<you>/AppData/Local/Roblox/Plugins/ manually. Set MCP_PLUGINS_DIR to skip detection.");
|
|
11209
13605
|
}
|
|
11210
|
-
return
|
|
13606
|
+
return join6(homedir5(), "Documents", "Roblox", "Plugins");
|
|
11211
13607
|
}
|
|
11212
13608
|
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
11213
|
-
const otherDest =
|
|
11214
|
-
if (!
|
|
13609
|
+
const otherDest = join6(pluginsFolder, otherAssetName);
|
|
13610
|
+
if (!existsSync6(otherDest))
|
|
11215
13611
|
return;
|
|
11216
13612
|
if (replace) {
|
|
11217
13613
|
try {
|
|
11218
13614
|
unlinkSync(otherDest);
|
|
11219
13615
|
log(`Removed conflicting ${otherAssetName}.`);
|
|
11220
|
-
} catch (
|
|
11221
|
-
warn(`[install-plugin] Could not remove ${otherDest}: ${
|
|
13616
|
+
} catch (err2) {
|
|
13617
|
+
warn(`[install-plugin] Could not remove ${otherDest}: ${err2}. Continuing.`);
|
|
11222
13618
|
}
|
|
11223
13619
|
return;
|
|
11224
13620
|
}
|
|
@@ -11247,6 +13643,7 @@ var init_dist = __esm({
|
|
|
11247
13643
|
init_opencloud_client();
|
|
11248
13644
|
init_install_plugin_helpers();
|
|
11249
13645
|
init_roblox_cookie_client();
|
|
13646
|
+
init_studio_skills();
|
|
11250
13647
|
}
|
|
11251
13648
|
});
|
|
11252
13649
|
|
|
@@ -11256,13 +13653,13 @@ __export(install_plugin_exports, {
|
|
|
11256
13653
|
installBundledPlugin: () => installBundledPlugin,
|
|
11257
13654
|
installPlugin: () => installPlugin
|
|
11258
13655
|
});
|
|
11259
|
-
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as
|
|
11260
|
-
import { dirname as
|
|
13656
|
+
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
13657
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
11261
13658
|
import { fileURLToPath } from "url";
|
|
11262
13659
|
import { get } from "https";
|
|
11263
13660
|
function httpsGet(url) {
|
|
11264
|
-
return new Promise((
|
|
11265
|
-
const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp-inspector" } },
|
|
13661
|
+
return new Promise((resolve5, reject) => {
|
|
13662
|
+
const req = get(url, { headers: { "User-Agent": "robloxstudio-mcp-inspector" } }, resolve5);
|
|
11266
13663
|
req.on("error", reject);
|
|
11267
13664
|
req.setTimeout(TIMEOUT_MS, () => {
|
|
11268
13665
|
req.destroy(new Error(`Request timed out after ${TIMEOUT_MS}ms`));
|
|
@@ -11280,21 +13677,21 @@ async function download(url, dest, redirects = 0) {
|
|
|
11280
13677
|
if (res.statusCode !== 200) {
|
|
11281
13678
|
throw new Error(`Download failed: HTTP ${res.statusCode}`);
|
|
11282
13679
|
}
|
|
11283
|
-
return new Promise((
|
|
13680
|
+
return new Promise((resolve5, reject) => {
|
|
11284
13681
|
const file = createWriteStream(dest);
|
|
11285
|
-
const cleanup = (
|
|
13682
|
+
const cleanup = (err2) => {
|
|
11286
13683
|
file.close(() => {
|
|
11287
13684
|
try {
|
|
11288
13685
|
unlinkSync2(dest);
|
|
11289
13686
|
} catch {
|
|
11290
13687
|
}
|
|
11291
|
-
reject(
|
|
13688
|
+
reject(err2);
|
|
11292
13689
|
});
|
|
11293
13690
|
};
|
|
11294
13691
|
res.pipe(file);
|
|
11295
13692
|
file.on("finish", () => {
|
|
11296
13693
|
file.close();
|
|
11297
|
-
|
|
13694
|
+
resolve5();
|
|
11298
13695
|
});
|
|
11299
13696
|
file.on("error", cleanup);
|
|
11300
13697
|
res.on("error", cleanup);
|
|
@@ -11317,8 +13714,8 @@ function prepareInstall({
|
|
|
11317
13714
|
warn
|
|
11318
13715
|
}) {
|
|
11319
13716
|
const pluginsFolder = getPluginsFolder();
|
|
11320
|
-
if (!
|
|
11321
|
-
|
|
13717
|
+
if (!existsSync7(pluginsFolder)) {
|
|
13718
|
+
mkdirSync5(pluginsFolder, { recursive: true });
|
|
11322
13719
|
}
|
|
11323
13720
|
handleVariantConflict({
|
|
11324
13721
|
pluginsFolder,
|
|
@@ -11330,23 +13727,23 @@ function prepareInstall({
|
|
|
11330
13727
|
return pluginsFolder;
|
|
11331
13728
|
}
|
|
11332
13729
|
function bundledAssetPath() {
|
|
11333
|
-
const currentDir =
|
|
13730
|
+
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
11334
13731
|
const candidates = [
|
|
11335
|
-
|
|
11336
|
-
|
|
13732
|
+
join7(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
13733
|
+
join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
11337
13734
|
];
|
|
11338
|
-
return candidates.find((candidate) =>
|
|
13735
|
+
return candidates.find((candidate) => existsSync7(candidate)) ?? null;
|
|
11339
13736
|
}
|
|
11340
13737
|
function packageVersion() {
|
|
11341
|
-
const currentDir =
|
|
11342
|
-
const pkg = JSON.parse(
|
|
13738
|
+
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
13739
|
+
const pkg = JSON.parse(readFileSync8(join7(currentDir, "..", "package.json"), "utf8"));
|
|
11343
13740
|
if (!pkg.version) {
|
|
11344
13741
|
throw new Error("Package version not found");
|
|
11345
13742
|
}
|
|
11346
13743
|
return pkg.version;
|
|
11347
13744
|
}
|
|
11348
13745
|
function bundledPluginVersion(source) {
|
|
11349
|
-
const match =
|
|
13746
|
+
const match = readFileSync8(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
11350
13747
|
return match ? match[1] : null;
|
|
11351
13748
|
}
|
|
11352
13749
|
function assertBundledPluginVersion(source) {
|
|
@@ -11359,9 +13756,9 @@ function assertBundledPluginVersion(source) {
|
|
|
11359
13756
|
}
|
|
11360
13757
|
}
|
|
11361
13758
|
function filesMatch(a, b) {
|
|
11362
|
-
if (!
|
|
11363
|
-
const aBytes =
|
|
11364
|
-
const bBytes =
|
|
13759
|
+
if (!existsSync7(b)) return false;
|
|
13760
|
+
const aBytes = readFileSync8(a);
|
|
13761
|
+
const bBytes = readFileSync8(b);
|
|
11365
13762
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
11366
13763
|
}
|
|
11367
13764
|
async function installBundledPlugin(options = {}) {
|
|
@@ -11374,7 +13771,7 @@ async function installBundledPlugin(options = {}) {
|
|
|
11374
13771
|
}
|
|
11375
13772
|
assertBundledPluginVersion(source);
|
|
11376
13773
|
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
11377
|
-
const dest =
|
|
13774
|
+
const dest = join7(pluginsFolder, ASSET_NAME);
|
|
11378
13775
|
if (filesMatch(source, dest)) return;
|
|
11379
13776
|
copyFileSync2(source, dest);
|
|
11380
13777
|
log(`Installed ${ASSET_NAME} to ${dest}`);
|
|
@@ -11387,7 +13784,7 @@ async function installPlugin(options = {}) {
|
|
|
11387
13784
|
const bundled = bundledAssetPath();
|
|
11388
13785
|
if (bundled) {
|
|
11389
13786
|
assertBundledPluginVersion(bundled);
|
|
11390
|
-
const dest2 =
|
|
13787
|
+
const dest2 = join7(pluginsFolder, ASSET_NAME);
|
|
11391
13788
|
if (filesMatch(bundled, dest2)) {
|
|
11392
13789
|
log(`${ASSET_NAME} already installed.`);
|
|
11393
13790
|
return;
|
|
@@ -11402,7 +13799,7 @@ async function installPlugin(options = {}) {
|
|
|
11402
13799
|
if (!asset) {
|
|
11403
13800
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
11404
13801
|
}
|
|
11405
|
-
const dest =
|
|
13802
|
+
const dest = join7(pluginsFolder, ASSET_NAME);
|
|
11406
13803
|
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
11407
13804
|
await download(asset.browser_download_url, dest);
|
|
11408
13805
|
log(`Installed to ${dest}`);
|
|
@@ -11425,8 +13822,8 @@ init_dist();
|
|
|
11425
13822
|
import { createRequire } from "module";
|
|
11426
13823
|
if (process.argv.includes("--install-plugin")) {
|
|
11427
13824
|
const { installPlugin: installPlugin2 } = await Promise.resolve().then(() => (init_install_plugin(), install_plugin_exports));
|
|
11428
|
-
await installPlugin2().catch((
|
|
11429
|
-
console.error(
|
|
13825
|
+
await installPlugin2().catch((err2) => {
|
|
13826
|
+
console.error(err2 instanceof Error ? err2.message : String(err2));
|
|
11430
13827
|
process.exitCode = 1;
|
|
11431
13828
|
});
|
|
11432
13829
|
} else {
|
|
@@ -11435,9 +13832,9 @@ if (process.argv.includes("--install-plugin")) {
|
|
|
11435
13832
|
await installBundledPlugin2({
|
|
11436
13833
|
log: (message) => console.error(`[install-plugin] ${message}`),
|
|
11437
13834
|
warn: (message) => console.error(message)
|
|
11438
|
-
}).catch((
|
|
13835
|
+
}).catch((err2) => {
|
|
11439
13836
|
console.error(
|
|
11440
|
-
`[install-plugin] Auto-install skipped: ${
|
|
13837
|
+
`[install-plugin] Auto-install skipped: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
11441
13838
|
);
|
|
11442
13839
|
});
|
|
11443
13840
|
}
|