@chrrxs/robloxstudio-mcp-inspector 2.19.1 → 2.21.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 +1307 -73
- package/package.json +2 -2
- package/studio-plugin/MCPInspectorPlugin.rbxmx +3 -3
- package/studio-plugin/MCPPlugin.rbxmx +3 -3
package/dist/index.js
CHANGED
|
@@ -196,6 +196,9 @@ var init_bridge_service = __esm({
|
|
|
196
196
|
}
|
|
197
197
|
return removed;
|
|
198
198
|
}
|
|
199
|
+
async unregisterInstanceIdEverywhere(instanceId) {
|
|
200
|
+
return this.unregisterInstanceId(instanceId);
|
|
201
|
+
}
|
|
199
202
|
getInstances() {
|
|
200
203
|
return Array.from(this.instances.values());
|
|
201
204
|
}
|
|
@@ -451,29 +454,256 @@ var init_bridge_service = __esm({
|
|
|
451
454
|
}
|
|
452
455
|
});
|
|
453
456
|
|
|
457
|
+
// ../core/dist/roblox-docs.js
|
|
458
|
+
function isDocCategory(value) {
|
|
459
|
+
return DOC_CATEGORIES.includes(value);
|
|
460
|
+
}
|
|
461
|
+
function cacheGet(key) {
|
|
462
|
+
const entry = cache.get(key);
|
|
463
|
+
if (!entry)
|
|
464
|
+
return void 0;
|
|
465
|
+
const ttl = entry.notFound ? NEGATIVE_CACHE_TTL_MS : CACHE_TTL_MS;
|
|
466
|
+
if (Date.now() - entry.fetchedAt > ttl) {
|
|
467
|
+
cache.delete(key);
|
|
468
|
+
return void 0;
|
|
469
|
+
}
|
|
470
|
+
return entry;
|
|
471
|
+
}
|
|
472
|
+
function cacheSet(key, entry) {
|
|
473
|
+
if (cache.size >= MAX_CACHE_ENTRIES) {
|
|
474
|
+
const oldest = cache.keys().next().value;
|
|
475
|
+
if (oldest !== void 0)
|
|
476
|
+
cache.delete(oldest);
|
|
477
|
+
}
|
|
478
|
+
cache.set(key, entry);
|
|
479
|
+
}
|
|
480
|
+
function docUrl(category, name) {
|
|
481
|
+
return `${DOCS_BASE_URL}/${category}/${encodeURIComponent(name)}.md`;
|
|
482
|
+
}
|
|
483
|
+
async function fetchRobloxDoc(category, name) {
|
|
484
|
+
const key = `${category}/${name}`;
|
|
485
|
+
const cached = cacheGet(key);
|
|
486
|
+
if (cached) {
|
|
487
|
+
if (cached.notFound)
|
|
488
|
+
throw new DocNotFoundError(category, name);
|
|
489
|
+
return cached.content;
|
|
490
|
+
}
|
|
491
|
+
const controller = new AbortController();
|
|
492
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
493
|
+
let response;
|
|
494
|
+
try {
|
|
495
|
+
response = await fetch(docUrl(category, name), {
|
|
496
|
+
signal: controller.signal,
|
|
497
|
+
headers: { Accept: "text/markdown, text/plain" }
|
|
498
|
+
});
|
|
499
|
+
} catch (error) {
|
|
500
|
+
throw new Error(`Failed to fetch Roblox docs for ${key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
501
|
+
} finally {
|
|
502
|
+
clearTimeout(timer);
|
|
503
|
+
}
|
|
504
|
+
if (response.status === 404) {
|
|
505
|
+
cacheSet(key, { fetchedAt: Date.now(), notFound: true });
|
|
506
|
+
throw new DocNotFoundError(category, name);
|
|
507
|
+
}
|
|
508
|
+
if (!response.ok) {
|
|
509
|
+
throw new Error(`Failed to fetch Roblox docs for ${key}: HTTP ${response.status}`);
|
|
510
|
+
}
|
|
511
|
+
const content = await response.text();
|
|
512
|
+
cacheSet(key, { fetchedAt: Date.now(), content });
|
|
513
|
+
return content;
|
|
514
|
+
}
|
|
515
|
+
function listSections(markdown) {
|
|
516
|
+
const sections = [];
|
|
517
|
+
for (const line of markdown.split("\n")) {
|
|
518
|
+
const match = line.match(/^##\s+(.+?)\s*$/);
|
|
519
|
+
if (match)
|
|
520
|
+
sections.push(match[1]);
|
|
521
|
+
}
|
|
522
|
+
return sections;
|
|
523
|
+
}
|
|
524
|
+
function extractSection(markdown, section) {
|
|
525
|
+
const lines = markdown.split("\n");
|
|
526
|
+
const wanted = section.trim().toLowerCase();
|
|
527
|
+
let start = -1;
|
|
528
|
+
for (let i = 0; i < lines.length; i++) {
|
|
529
|
+
const match = lines[i].match(/^##\s+(.+?)\s*$/);
|
|
530
|
+
if (match && match[1].toLowerCase() === wanted) {
|
|
531
|
+
start = i;
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (start === -1)
|
|
536
|
+
return void 0;
|
|
537
|
+
let end = lines.length;
|
|
538
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
539
|
+
if (/^##?\s+/.test(lines[i])) {
|
|
540
|
+
end = i;
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return lines.slice(start, end).join("\n").trimEnd();
|
|
545
|
+
}
|
|
546
|
+
async function getRobloxDoc(category, name, section) {
|
|
547
|
+
const markdown = await fetchRobloxDoc(category, name);
|
|
548
|
+
const sections = listSections(markdown);
|
|
549
|
+
if (section) {
|
|
550
|
+
const extracted = extractSection(markdown, section);
|
|
551
|
+
if (extracted === void 0) {
|
|
552
|
+
throw new Error(`Section "${section}" not found in ${category}/${name}. Available sections: ${sections.join(", ") || "(none)"}`);
|
|
553
|
+
}
|
|
554
|
+
return { content: extracted, truncated: false, sections };
|
|
555
|
+
}
|
|
556
|
+
if (markdown.length > MAX_DOC_CHARS) {
|
|
557
|
+
const head = markdown.slice(0, MAX_DOC_CHARS);
|
|
558
|
+
const note = `
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
[Truncated at ${MAX_DOC_CHARS} of ${markdown.length} characters. Re-request with the "section" parameter to read one section in full. Available sections: ${sections.join(", ")}]`;
|
|
562
|
+
return { content: head + note, truncated: true, sections };
|
|
563
|
+
}
|
|
564
|
+
return { content: markdown, truncated: false, sections };
|
|
565
|
+
}
|
|
566
|
+
var DOC_CATEGORIES, DOCS_BASE_URL, FETCH_TIMEOUT_MS, CACHE_TTL_MS, NEGATIVE_CACHE_TTL_MS, MAX_CACHE_ENTRIES, MAX_DOC_CHARS, cache, DocNotFoundError;
|
|
567
|
+
var init_roblox_docs = __esm({
|
|
568
|
+
"../core/dist/roblox-docs.js"() {
|
|
569
|
+
"use strict";
|
|
570
|
+
DOC_CATEGORIES = ["classes", "enums", "datatypes", "libraries", "globals"];
|
|
571
|
+
DOCS_BASE_URL = "https://create.roblox.com/docs/reference/engine";
|
|
572
|
+
FETCH_TIMEOUT_MS = 15e3;
|
|
573
|
+
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
574
|
+
NEGATIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
575
|
+
MAX_CACHE_ENTRIES = 200;
|
|
576
|
+
MAX_DOC_CHARS = 5e4;
|
|
577
|
+
cache = /* @__PURE__ */ new Map();
|
|
578
|
+
DocNotFoundError = class extends Error {
|
|
579
|
+
constructor(category, name) {
|
|
580
|
+
super(`No Roblox documentation found for ${category}/${name}. Names are case-sensitive PascalCase (e.g. "ProximityPrompt", "TweenService"). Valid categories: ${DOC_CATEGORIES.join(", ")}.`);
|
|
581
|
+
this.name = "DocNotFoundError";
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
|
|
454
587
|
// ../core/dist/mcp-compat.js
|
|
455
588
|
import { ErrorCode, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
456
|
-
function
|
|
589
|
+
function registerResourceHandlers(server) {
|
|
457
590
|
server.registerCapabilities({ resources: {} });
|
|
458
591
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
459
592
|
resources: []
|
|
460
593
|
}));
|
|
461
594
|
server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
|
|
462
|
-
resourceTemplates: [
|
|
595
|
+
resourceTemplates: [
|
|
596
|
+
{
|
|
597
|
+
uriTemplate: "robloxdocs://classes/{className}",
|
|
598
|
+
name: "Roblox class documentation",
|
|
599
|
+
description: "Official engine reference for a class (e.g. robloxdocs://classes/ProximityPrompt) \u2014 properties, methods, events, and usage guidance as markdown.",
|
|
600
|
+
mimeType: "text/markdown"
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
uriTemplate: "robloxdocs://enums/{enumName}",
|
|
604
|
+
name: "Roblox enum documentation",
|
|
605
|
+
description: "Official engine reference for an enum (e.g. robloxdocs://enums/KeyCode).",
|
|
606
|
+
mimeType: "text/markdown"
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
uriTemplate: "robloxdocs://datatypes/{dataTypeName}",
|
|
610
|
+
name: "Roblox datatype documentation",
|
|
611
|
+
description: "Official engine reference for a datatype (e.g. robloxdocs://datatypes/CFrame).",
|
|
612
|
+
mimeType: "text/markdown"
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
uriTemplate: "robloxdocs://libraries/{libraryName}",
|
|
616
|
+
name: "Roblox library documentation",
|
|
617
|
+
description: "Official engine reference for a Luau library (e.g. robloxdocs://libraries/table).",
|
|
618
|
+
mimeType: "text/markdown"
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
uriTemplate: "robloxdocs://globals/{globalsPage}",
|
|
622
|
+
name: "Roblox globals documentation",
|
|
623
|
+
description: "Official engine reference for global functions and variables (e.g. robloxdocs://globals/LuaGlobals, robloxdocs://globals/RobloxGlobals).",
|
|
624
|
+
mimeType: "text/markdown"
|
|
625
|
+
}
|
|
626
|
+
]
|
|
463
627
|
}));
|
|
464
628
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
465
|
-
|
|
629
|
+
const uri = request.params.uri;
|
|
630
|
+
const match = uri.match(/^robloxdocs:\/\/([^/]+)\/([^/]+)$/);
|
|
631
|
+
if (!match || !isDocCategory(match[1])) {
|
|
632
|
+
throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found`);
|
|
633
|
+
}
|
|
634
|
+
const [, category, rawName] = match;
|
|
635
|
+
const name = decodeURIComponent(rawName);
|
|
636
|
+
try {
|
|
637
|
+
const content = await fetchRobloxDoc(category, name);
|
|
638
|
+
return {
|
|
639
|
+
contents: [{ uri, mimeType: "text/markdown", text: content }]
|
|
640
|
+
};
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (error instanceof DocNotFoundError) {
|
|
643
|
+
throw new McpError(ErrorCode.InvalidParams, `Resource ${uri} not found. Names are case-sensitive PascalCase; valid categories: ${DOC_CATEGORIES.join(", ")}.`);
|
|
644
|
+
}
|
|
645
|
+
throw new McpError(ErrorCode.InternalError, `Failed to read ${uri}: ${error instanceof Error ? error.message : String(error)}`);
|
|
646
|
+
}
|
|
466
647
|
});
|
|
467
648
|
}
|
|
468
649
|
var init_mcp_compat = __esm({
|
|
469
650
|
"../core/dist/mcp-compat.js"() {
|
|
470
651
|
"use strict";
|
|
652
|
+
init_roblox_docs();
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// ../core/dist/auth.js
|
|
657
|
+
import { randomBytes, createHash, timingSafeEqual } from "crypto";
|
|
658
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
|
|
659
|
+
import { join, dirname } from "path";
|
|
660
|
+
import { homedir } from "os";
|
|
661
|
+
function authTokenFilePath() {
|
|
662
|
+
return join(homedir(), ".robloxstudio-mcp", "auth-token");
|
|
663
|
+
}
|
|
664
|
+
function resolveAuthToken() {
|
|
665
|
+
const noAuth = (process.env.ROBLOX_STUDIO_NO_AUTH || "").toLowerCase();
|
|
666
|
+
if (noAuth === "1" || noAuth === "true") {
|
|
667
|
+
return { source: "disabled" };
|
|
668
|
+
}
|
|
669
|
+
const envToken = process.env.ROBLOX_STUDIO_AUTH_TOKEN?.trim();
|
|
670
|
+
if (envToken) {
|
|
671
|
+
return { token: envToken, source: "env" };
|
|
672
|
+
}
|
|
673
|
+
const filePath = authTokenFilePath();
|
|
674
|
+
try {
|
|
675
|
+
if (existsSync(filePath)) {
|
|
676
|
+
const existing = readFileSync(filePath, "utf8").trim();
|
|
677
|
+
if (existing) {
|
|
678
|
+
return { token: existing, source: "file", filePath };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
const fresh = randomBytes(32).toString("hex");
|
|
682
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 448 });
|
|
683
|
+
writeFileSync(filePath, fresh + "\n", { encoding: "utf8", mode: 384 });
|
|
684
|
+
try {
|
|
685
|
+
chmodSync(filePath, 384);
|
|
686
|
+
} catch {
|
|
687
|
+
}
|
|
688
|
+
return { token: fresh, source: "file", filePath };
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.error(`[auth] Could not read/create ${filePath} (${err instanceof Error ? err.message : err}). Using an in-memory token for this process; set ROBLOX_STUDIO_AUTH_TOKEN to share one across sessions.`);
|
|
691
|
+
return { token: randomBytes(32).toString("hex"), source: "file" };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function tokensMatch(provided, expected) {
|
|
695
|
+
const a = createHash("sha256").update(provided).digest();
|
|
696
|
+
const b = createHash("sha256").update(expected).digest();
|
|
697
|
+
return timingSafeEqual(a, b);
|
|
698
|
+
}
|
|
699
|
+
var init_auth = __esm({
|
|
700
|
+
"../core/dist/auth.js"() {
|
|
701
|
+
"use strict";
|
|
471
702
|
}
|
|
472
703
|
});
|
|
473
704
|
|
|
474
705
|
// ../core/dist/http-server.js
|
|
475
706
|
import express from "express";
|
|
476
|
-
import cors from "cors";
|
|
477
707
|
import http from "http";
|
|
478
708
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
479
709
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -526,7 +756,7 @@ function requiredClosedLineRange(body, toolName) {
|
|
|
526
756
|
}
|
|
527
757
|
return { startLine: parsed.startLine, endLine: parsed.endLine };
|
|
528
758
|
}
|
|
529
|
-
function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
759
|
+
function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
530
760
|
const app = express();
|
|
531
761
|
let mcpServerActive = false;
|
|
532
762
|
let lastMCPActivity = 0;
|
|
@@ -556,7 +786,49 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
556
786
|
const isPluginConnected = () => {
|
|
557
787
|
return bridge.getInstances().length > 0;
|
|
558
788
|
};
|
|
559
|
-
|
|
789
|
+
const allowedOrigins = new Set(security?.allowedOrigins ?? []);
|
|
790
|
+
app.use((req, res, next) => {
|
|
791
|
+
const origin = req.headers.origin;
|
|
792
|
+
if (typeof origin !== "string" || origin === "") {
|
|
793
|
+
next();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (!allowedOrigins.has(origin)) {
|
|
797
|
+
res.status(403).json({
|
|
798
|
+
error: "forbidden_origin",
|
|
799
|
+
message: `Cross-origin requests are not allowed from ${origin}. Set ROBLOX_STUDIO_ALLOWED_ORIGINS to allowlist specific origins.`
|
|
800
|
+
});
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
804
|
+
res.setHeader("Vary", "Origin");
|
|
805
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
806
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-MCP-Auth, Mcp-Protocol-Version");
|
|
807
|
+
if (req.method === "OPTIONS") {
|
|
808
|
+
res.status(204).end();
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
next();
|
|
812
|
+
});
|
|
813
|
+
const authToken = security?.authToken;
|
|
814
|
+
const authRequired = (path5) => path5 === "/mcp" || path5.startsWith("/mcp/") || path5 === "/proxy" || path5 === "/instances" || path5 === "/unregister-instance-id";
|
|
815
|
+
app.use((req, res, next) => {
|
|
816
|
+
if (!authToken || !authRequired(req.path)) {
|
|
817
|
+
next();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const headerToken = req.headers["x-mcp-auth"];
|
|
821
|
+
const bearer = typeof req.headers.authorization === "string" && req.headers.authorization.startsWith("Bearer ") ? req.headers.authorization.slice("Bearer ".length) : void 0;
|
|
822
|
+
const provided = typeof headerToken === "string" && headerToken !== "" ? headerToken : bearer;
|
|
823
|
+
if (provided !== void 0 && tokensMatch(provided, authToken)) {
|
|
824
|
+
next();
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
res.status(401).json({
|
|
828
|
+
error: "unauthorized",
|
|
829
|
+
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).")
|
|
830
|
+
});
|
|
831
|
+
});
|
|
560
832
|
app.use(express.json({ limit: "50mb" }));
|
|
561
833
|
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
|
562
834
|
app.get("/health", (req, res) => {
|
|
@@ -658,6 +930,15 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
658
930
|
}
|
|
659
931
|
res.json({ success: true });
|
|
660
932
|
});
|
|
933
|
+
app.post("/unregister-instance-id", (req, res) => {
|
|
934
|
+
const { instanceId } = req.body;
|
|
935
|
+
if (typeof instanceId !== "string" || instanceId.length === 0) {
|
|
936
|
+
res.status(400).json({ error: "instanceId is required" });
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const removed = bridge.unregisterInstanceId(instanceId);
|
|
940
|
+
res.json({ success: true, removed });
|
|
941
|
+
});
|
|
661
942
|
app.get("/status", (req, res) => {
|
|
662
943
|
const instances = bridge.getInstances();
|
|
663
944
|
const publicInstances = instances.map(toPublic);
|
|
@@ -775,7 +1056,7 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig) {
|
|
|
775
1056
|
try {
|
|
776
1057
|
trackMCPActivity();
|
|
777
1058
|
const server = new Server({ name: serverConfig.name, version: serverConfig.version }, { capabilities: { tools: {} } });
|
|
778
|
-
|
|
1059
|
+
registerResourceHandlers(server);
|
|
779
1060
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
780
1061
|
tools: filteredTools.map((t) => ({
|
|
781
1062
|
name: t.name,
|
|
@@ -917,7 +1198,9 @@ var init_http_server = __esm({
|
|
|
917
1198
|
"use strict";
|
|
918
1199
|
init_bridge_service();
|
|
919
1200
|
init_mcp_compat();
|
|
1201
|
+
init_auth();
|
|
920
1202
|
TOOL_HANDLERS = {
|
|
1203
|
+
get_roblox_docs: (tools, body) => tools.getRobloxDocs(body.name, body.doc_type, body.section),
|
|
921
1204
|
get_file_tree: (tools, body) => tools.getFileTree(body.path, body.instance_id),
|
|
922
1205
|
search_files: (tools, body) => tools.searchFiles(body.query, body.searchType, body.instance_id),
|
|
923
1206
|
get_place_info: (tools, body) => tools.getPlaceInfo(body.instance_id),
|
|
@@ -1083,8 +1366,844 @@ var init_studio_client = __esm({
|
|
|
1083
1366
|
}
|
|
1084
1367
|
});
|
|
1085
1368
|
|
|
1369
|
+
// ../core/dist/tools/build-interpreter.js
|
|
1370
|
+
import * as acorn from "acorn";
|
|
1371
|
+
function isPlainObject(value) {
|
|
1372
|
+
if (value === null || typeof value !== "object")
|
|
1373
|
+
return false;
|
|
1374
|
+
const proto = Object.getPrototypeOf(value);
|
|
1375
|
+
return proto === Object.prototype || proto === null;
|
|
1376
|
+
}
|
|
1377
|
+
function runRestrictedScript(code, globals, options) {
|
|
1378
|
+
const timeoutMs = options?.timeoutMs ?? 1e4;
|
|
1379
|
+
const maxOps = options?.maxOps ?? 2e7;
|
|
1380
|
+
const deadline = Date.now() + timeoutMs;
|
|
1381
|
+
let ops = 0;
|
|
1382
|
+
let ast;
|
|
1383
|
+
try {
|
|
1384
|
+
ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: "script" });
|
|
1385
|
+
} catch (err) {
|
|
1386
|
+
throw new Error(`Syntax error: ${err.message}`);
|
|
1387
|
+
}
|
|
1388
|
+
const globalScope = new Scope(void 0, true);
|
|
1389
|
+
for (const [name, value] of Object.entries(globals)) {
|
|
1390
|
+
globalScope.declare(name, value, "const");
|
|
1391
|
+
}
|
|
1392
|
+
function tick() {
|
|
1393
|
+
ops++;
|
|
1394
|
+
if (ops > maxOps) {
|
|
1395
|
+
throw new ScriptTimeoutError(`Operation budget exceeded (${maxOps} ops)`);
|
|
1396
|
+
}
|
|
1397
|
+
if ((ops & 8191) === 0 && Date.now() > deadline) {
|
|
1398
|
+
throw new ScriptTimeoutError(`Execution timed out after ${timeoutMs}ms`);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function assertPropAllowed(prop) {
|
|
1402
|
+
const name = typeof prop === "symbol" ? String(prop) : String(prop);
|
|
1403
|
+
if (FORBIDDEN_PROPS.has(name)) {
|
|
1404
|
+
throw new Error(`Access to property "${name}" is not allowed`);
|
|
1405
|
+
}
|
|
1406
|
+
return name;
|
|
1407
|
+
}
|
|
1408
|
+
function getMember(obj, prop) {
|
|
1409
|
+
if (obj === null || obj === void 0) {
|
|
1410
|
+
throw new Error(`Cannot read property "${String(prop)}" of ${obj}`);
|
|
1411
|
+
}
|
|
1412
|
+
const name = assertPropAllowed(prop);
|
|
1413
|
+
if (Array.isArray(obj)) {
|
|
1414
|
+
if (name === "length")
|
|
1415
|
+
return obj.length;
|
|
1416
|
+
const idx = Number(name);
|
|
1417
|
+
if (Number.isInteger(idx) && idx >= 0)
|
|
1418
|
+
return obj[idx];
|
|
1419
|
+
if (ARRAY_MEMBERS.has(name))
|
|
1420
|
+
return obj[name];
|
|
1421
|
+
throw new Error(`Array method "${name}" is not allowed`);
|
|
1422
|
+
}
|
|
1423
|
+
if (typeof obj === "string") {
|
|
1424
|
+
if (name === "length")
|
|
1425
|
+
return obj.length;
|
|
1426
|
+
const idx = Number(name);
|
|
1427
|
+
if (Number.isInteger(idx) && idx >= 0)
|
|
1428
|
+
return obj[idx];
|
|
1429
|
+
if (STRING_MEMBERS.has(name))
|
|
1430
|
+
return obj[name];
|
|
1431
|
+
throw new Error(`String method "${name}" is not allowed`);
|
|
1432
|
+
}
|
|
1433
|
+
if (typeof obj === "number") {
|
|
1434
|
+
if (NUMBER_MEMBERS.has(name))
|
|
1435
|
+
return obj[name];
|
|
1436
|
+
throw new Error(`Number method "${name}" is not allowed`);
|
|
1437
|
+
}
|
|
1438
|
+
if (isPlainObject(obj)) {
|
|
1439
|
+
if (Object.prototype.hasOwnProperty.call(obj, name))
|
|
1440
|
+
return obj[name];
|
|
1441
|
+
return void 0;
|
|
1442
|
+
}
|
|
1443
|
+
throw new Error(`Property access on this value type is not allowed`);
|
|
1444
|
+
}
|
|
1445
|
+
function setMember(obj, prop, value) {
|
|
1446
|
+
if (obj === null || obj === void 0) {
|
|
1447
|
+
throw new Error(`Cannot set property "${String(prop)}" of ${obj}`);
|
|
1448
|
+
}
|
|
1449
|
+
const name = assertPropAllowed(prop);
|
|
1450
|
+
if (Object.isFrozen(obj)) {
|
|
1451
|
+
throw new Error(`Cannot assign to property "${name}" of a frozen object`);
|
|
1452
|
+
}
|
|
1453
|
+
if (Array.isArray(obj)) {
|
|
1454
|
+
const idx = Number(name);
|
|
1455
|
+
if (Number.isInteger(idx) && idx >= 0 || name === "length") {
|
|
1456
|
+
obj[name] = value;
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
throw new Error(`Cannot set property "${name}" on an array`);
|
|
1460
|
+
}
|
|
1461
|
+
if (isPlainObject(obj)) {
|
|
1462
|
+
obj[name] = value;
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
throw new Error(`Property assignment on this value type is not allowed`);
|
|
1466
|
+
}
|
|
1467
|
+
function callFunction(fn, thisArg, args, desc) {
|
|
1468
|
+
if (typeof fn !== "function") {
|
|
1469
|
+
throw new Error(`${desc} is not a function`);
|
|
1470
|
+
}
|
|
1471
|
+
return fn.apply(thisArg, args);
|
|
1472
|
+
}
|
|
1473
|
+
function bindPattern(pattern, value, scope, kind) {
|
|
1474
|
+
switch (pattern.type) {
|
|
1475
|
+
case "Identifier":
|
|
1476
|
+
scope.declare(pattern.name, value, kind);
|
|
1477
|
+
return;
|
|
1478
|
+
case "AssignmentPattern":
|
|
1479
|
+
bindPattern(pattern.left, value === void 0 ? evaluate(pattern.right, scope) : value, scope, kind);
|
|
1480
|
+
return;
|
|
1481
|
+
case "ArrayPattern": {
|
|
1482
|
+
if (value === null || value === void 0)
|
|
1483
|
+
throw new Error("Cannot destructure null/undefined");
|
|
1484
|
+
let i = 0;
|
|
1485
|
+
for (const el of pattern.elements) {
|
|
1486
|
+
if (el === null) {
|
|
1487
|
+
i++;
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
if (el.type === "RestElement") {
|
|
1491
|
+
bindPattern(el.argument, Array.isArray(value) ? value.slice(i) : [], scope, kind);
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
bindPattern(el, getMember(value, i), scope, kind);
|
|
1495
|
+
i++;
|
|
1496
|
+
}
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
case "ObjectPattern": {
|
|
1500
|
+
if (value === null || value === void 0)
|
|
1501
|
+
throw new Error("Cannot destructure null/undefined");
|
|
1502
|
+
const used = /* @__PURE__ */ new Set();
|
|
1503
|
+
for (const propNode of pattern.properties) {
|
|
1504
|
+
if (propNode.type === "RestElement") {
|
|
1505
|
+
const rest = {};
|
|
1506
|
+
if (isPlainObject(value)) {
|
|
1507
|
+
for (const k of Object.keys(value)) {
|
|
1508
|
+
if (!used.has(k) && !FORBIDDEN_PROPS.has(k))
|
|
1509
|
+
rest[k] = value[k];
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
bindPattern(propNode.argument, rest, scope, kind);
|
|
1513
|
+
continue;
|
|
1514
|
+
}
|
|
1515
|
+
const key = propNode.computed ? String(evaluate(propNode.key, scope)) : propNode.key.type === "Identifier" ? propNode.key.name : String(propNode.key.value);
|
|
1516
|
+
used.add(key);
|
|
1517
|
+
bindPattern(propNode.value, getMember(value, key), scope, kind);
|
|
1518
|
+
}
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
default:
|
|
1522
|
+
throw new Error(`Unsupported binding pattern: ${pattern.type}`);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function makeFunction(node, closureScope) {
|
|
1526
|
+
return (...args) => {
|
|
1527
|
+
tick();
|
|
1528
|
+
const fnScope = new Scope(closureScope, true);
|
|
1529
|
+
node.params.forEach((param, i) => {
|
|
1530
|
+
if (param.type === "RestElement") {
|
|
1531
|
+
bindPattern(param.argument, args.slice(i), fnScope, "let");
|
|
1532
|
+
} else {
|
|
1533
|
+
bindPattern(param, args[i], fnScope, "let");
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
try {
|
|
1537
|
+
if (node.body.type === "BlockStatement") {
|
|
1538
|
+
executeBlock(node.body.body, new Scope(fnScope));
|
|
1539
|
+
return void 0;
|
|
1540
|
+
}
|
|
1541
|
+
return evaluate(node.body, fnScope);
|
|
1542
|
+
} catch (signal) {
|
|
1543
|
+
if (signal instanceof ReturnSignal)
|
|
1544
|
+
return signal.value;
|
|
1545
|
+
throw signal;
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
function hoistFunctions(statements, scope) {
|
|
1550
|
+
for (const stmt of statements) {
|
|
1551
|
+
if (stmt.type === "FunctionDeclaration" && stmt.id) {
|
|
1552
|
+
if (stmt.async || stmt.generator)
|
|
1553
|
+
throw new Error("async/generator functions are not supported");
|
|
1554
|
+
scope.declare(stmt.id.name, makeFunction(stmt, scope), "let");
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
function executeBlock(statements, scope) {
|
|
1559
|
+
hoistFunctions(statements, scope);
|
|
1560
|
+
for (const stmt of statements) {
|
|
1561
|
+
execute(stmt, scope);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
function execute(node, scope) {
|
|
1565
|
+
tick();
|
|
1566
|
+
switch (node.type) {
|
|
1567
|
+
case "ExpressionStatement":
|
|
1568
|
+
evaluate(node.expression, scope);
|
|
1569
|
+
return;
|
|
1570
|
+
case "VariableDeclaration":
|
|
1571
|
+
for (const decl of node.declarations) {
|
|
1572
|
+
const value = decl.init ? evaluate(decl.init, scope) : void 0;
|
|
1573
|
+
bindPattern(decl.id, value, scope, node.kind);
|
|
1574
|
+
}
|
|
1575
|
+
return;
|
|
1576
|
+
case "FunctionDeclaration":
|
|
1577
|
+
return;
|
|
1578
|
+
// hoisted by executeBlock
|
|
1579
|
+
case "BlockStatement":
|
|
1580
|
+
executeBlock(node.body, new Scope(scope));
|
|
1581
|
+
return;
|
|
1582
|
+
case "EmptyStatement":
|
|
1583
|
+
return;
|
|
1584
|
+
case "IfStatement":
|
|
1585
|
+
if (evaluate(node.test, scope)) {
|
|
1586
|
+
execute(node.consequent, scope);
|
|
1587
|
+
} else if (node.alternate) {
|
|
1588
|
+
execute(node.alternate, scope);
|
|
1589
|
+
}
|
|
1590
|
+
return;
|
|
1591
|
+
case "ForStatement": {
|
|
1592
|
+
const forScope = new Scope(scope);
|
|
1593
|
+
if (node.init) {
|
|
1594
|
+
if (node.init.type === "VariableDeclaration")
|
|
1595
|
+
execute(node.init, forScope);
|
|
1596
|
+
else
|
|
1597
|
+
evaluate(node.init, forScope);
|
|
1598
|
+
}
|
|
1599
|
+
while (node.test === null || evaluate(node.test, forScope)) {
|
|
1600
|
+
tick();
|
|
1601
|
+
try {
|
|
1602
|
+
execute(node.body, new Scope(forScope));
|
|
1603
|
+
} catch (signal) {
|
|
1604
|
+
if (signal instanceof BreakSignal)
|
|
1605
|
+
break;
|
|
1606
|
+
if (!(signal instanceof ContinueSignal))
|
|
1607
|
+
throw signal;
|
|
1608
|
+
}
|
|
1609
|
+
if (node.update)
|
|
1610
|
+
evaluate(node.update, forScope);
|
|
1611
|
+
}
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
case "ForOfStatement": {
|
|
1615
|
+
const iterable = evaluate(node.right, scope);
|
|
1616
|
+
if (!Array.isArray(iterable) && typeof iterable !== "string") {
|
|
1617
|
+
throw new Error("for...of only supports arrays and strings");
|
|
1618
|
+
}
|
|
1619
|
+
for (const item of iterable) {
|
|
1620
|
+
tick();
|
|
1621
|
+
const iterScope = new Scope(scope);
|
|
1622
|
+
if (node.left.type === "VariableDeclaration") {
|
|
1623
|
+
bindPattern(node.left.declarations[0].id, item, iterScope, node.left.kind);
|
|
1624
|
+
} else {
|
|
1625
|
+
assignTo(node.left, item, iterScope);
|
|
1626
|
+
}
|
|
1627
|
+
try {
|
|
1628
|
+
execute(node.body, iterScope);
|
|
1629
|
+
} catch (signal) {
|
|
1630
|
+
if (signal instanceof BreakSignal)
|
|
1631
|
+
break;
|
|
1632
|
+
if (!(signal instanceof ContinueSignal))
|
|
1633
|
+
throw signal;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
case "ForInStatement": {
|
|
1639
|
+
const obj = evaluate(node.right, scope);
|
|
1640
|
+
const keys = isPlainObject(obj) ? Object.keys(obj) : Array.isArray(obj) ? obj.map((_, i) => String(i)) : [];
|
|
1641
|
+
for (const key of keys) {
|
|
1642
|
+
tick();
|
|
1643
|
+
const iterScope = new Scope(scope);
|
|
1644
|
+
if (node.left.type === "VariableDeclaration") {
|
|
1645
|
+
bindPattern(node.left.declarations[0].id, key, iterScope, node.left.kind);
|
|
1646
|
+
} else {
|
|
1647
|
+
assignTo(node.left, key, 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 "WhileStatement":
|
|
1661
|
+
while (evaluate(node.test, scope)) {
|
|
1662
|
+
tick();
|
|
1663
|
+
try {
|
|
1664
|
+
execute(node.body, new Scope(scope));
|
|
1665
|
+
} catch (signal) {
|
|
1666
|
+
if (signal instanceof BreakSignal)
|
|
1667
|
+
break;
|
|
1668
|
+
if (!(signal instanceof ContinueSignal))
|
|
1669
|
+
throw signal;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
return;
|
|
1673
|
+
case "DoWhileStatement":
|
|
1674
|
+
do {
|
|
1675
|
+
tick();
|
|
1676
|
+
try {
|
|
1677
|
+
execute(node.body, new Scope(scope));
|
|
1678
|
+
} catch (signal) {
|
|
1679
|
+
if (signal instanceof BreakSignal)
|
|
1680
|
+
break;
|
|
1681
|
+
if (!(signal instanceof ContinueSignal))
|
|
1682
|
+
throw signal;
|
|
1683
|
+
}
|
|
1684
|
+
} while (evaluate(node.test, scope));
|
|
1685
|
+
return;
|
|
1686
|
+
case "BreakStatement":
|
|
1687
|
+
throw new BreakSignal(node.label?.name);
|
|
1688
|
+
case "ContinueStatement":
|
|
1689
|
+
throw new ContinueSignal(node.label?.name);
|
|
1690
|
+
case "ReturnStatement":
|
|
1691
|
+
throw new ReturnSignal(node.argument ? evaluate(node.argument, scope) : void 0);
|
|
1692
|
+
case "ThrowStatement":
|
|
1693
|
+
throw evaluate(node.argument, scope);
|
|
1694
|
+
case "TryStatement": {
|
|
1695
|
+
try {
|
|
1696
|
+
execute(node.block, scope);
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
if (err instanceof BreakSignal || err instanceof ContinueSignal || err instanceof ReturnSignal)
|
|
1699
|
+
throw err;
|
|
1700
|
+
if (err instanceof ScriptTimeoutError)
|
|
1701
|
+
throw err;
|
|
1702
|
+
if (node.handler) {
|
|
1703
|
+
const catchScope = new Scope(scope);
|
|
1704
|
+
if (node.handler.param) {
|
|
1705
|
+
const message = err instanceof Error ? { message: err.message } : err;
|
|
1706
|
+
bindPattern(node.handler.param, message, catchScope, "let");
|
|
1707
|
+
}
|
|
1708
|
+
execute(node.handler.body, catchScope);
|
|
1709
|
+
}
|
|
1710
|
+
} finally {
|
|
1711
|
+
if (node.finalizer)
|
|
1712
|
+
execute(node.finalizer, scope);
|
|
1713
|
+
}
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
case "SwitchStatement": {
|
|
1717
|
+
const disc = evaluate(node.discriminant, scope);
|
|
1718
|
+
const switchScope = new Scope(scope);
|
|
1719
|
+
let matched = false;
|
|
1720
|
+
try {
|
|
1721
|
+
for (const caseNode of node.cases) {
|
|
1722
|
+
if (!matched) {
|
|
1723
|
+
if (caseNode.test === null)
|
|
1724
|
+
matched = true;
|
|
1725
|
+
else if (evaluate(caseNode.test, switchScope) === disc)
|
|
1726
|
+
matched = true;
|
|
1727
|
+
}
|
|
1728
|
+
if (matched) {
|
|
1729
|
+
for (const stmt of caseNode.consequent)
|
|
1730
|
+
execute(stmt, switchScope);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
} catch (signal) {
|
|
1734
|
+
if (!(signal instanceof BreakSignal))
|
|
1735
|
+
throw signal;
|
|
1736
|
+
}
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
case "LabeledStatement":
|
|
1740
|
+
try {
|
|
1741
|
+
execute(node.body, scope);
|
|
1742
|
+
} catch (signal) {
|
|
1743
|
+
if (signal instanceof BreakSignal && signal.label === node.label.name)
|
|
1744
|
+
return;
|
|
1745
|
+
throw signal;
|
|
1746
|
+
}
|
|
1747
|
+
return;
|
|
1748
|
+
default:
|
|
1749
|
+
throw new Error(`Unsupported statement: ${node.type}`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function assignTo(target, value, scope) {
|
|
1753
|
+
if (target.type === "Identifier") {
|
|
1754
|
+
scope.set(target.name, value);
|
|
1755
|
+
} else if (target.type === "MemberExpression") {
|
|
1756
|
+
const obj = evaluate(target.object, scope);
|
|
1757
|
+
const prop = target.computed ? evaluate(target.property, scope) : target.property.name;
|
|
1758
|
+
setMember(obj, prop, value);
|
|
1759
|
+
} else {
|
|
1760
|
+
throw new Error(`Unsupported assignment target: ${target.type}`);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
function evaluate(node, scope) {
|
|
1764
|
+
tick();
|
|
1765
|
+
switch (node.type) {
|
|
1766
|
+
case "Literal":
|
|
1767
|
+
if (node.regex)
|
|
1768
|
+
throw new Error("Regular expression literals are not supported");
|
|
1769
|
+
return node.value;
|
|
1770
|
+
case "TemplateLiteral": {
|
|
1771
|
+
let out = "";
|
|
1772
|
+
node.quasis.forEach((quasi, i) => {
|
|
1773
|
+
out += quasi.value.cooked ?? "";
|
|
1774
|
+
if (i < node.expressions.length)
|
|
1775
|
+
out += String(evaluate(node.expressions[i], scope));
|
|
1776
|
+
});
|
|
1777
|
+
return out;
|
|
1778
|
+
}
|
|
1779
|
+
case "Identifier":
|
|
1780
|
+
return scope.get(node.name);
|
|
1781
|
+
case "ArrayExpression": {
|
|
1782
|
+
const arr = [];
|
|
1783
|
+
for (const el of node.elements) {
|
|
1784
|
+
if (el === null) {
|
|
1785
|
+
arr.length++;
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
if (el.type === "SpreadElement") {
|
|
1789
|
+
const spread = evaluate(el.argument, scope);
|
|
1790
|
+
if (!Array.isArray(spread) && typeof spread !== "string") {
|
|
1791
|
+
throw new Error("Spread only supports arrays and strings");
|
|
1792
|
+
}
|
|
1793
|
+
for (const item of spread)
|
|
1794
|
+
arr.push(item);
|
|
1795
|
+
} else {
|
|
1796
|
+
arr.push(evaluate(el, scope));
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return arr;
|
|
1800
|
+
}
|
|
1801
|
+
case "ObjectExpression": {
|
|
1802
|
+
const obj = {};
|
|
1803
|
+
for (const prop of node.properties) {
|
|
1804
|
+
if (prop.type === "SpreadElement") {
|
|
1805
|
+
const spread = evaluate(prop.argument, scope);
|
|
1806
|
+
if (isPlainObject(spread)) {
|
|
1807
|
+
for (const k of Object.keys(spread)) {
|
|
1808
|
+
if (!FORBIDDEN_PROPS.has(k))
|
|
1809
|
+
obj[k] = spread[k];
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1814
|
+
if (prop.kind !== "init")
|
|
1815
|
+
throw new Error("Getters/setters are not supported");
|
|
1816
|
+
const key = prop.computed ? String(evaluate(prop.key, scope)) : prop.key.type === "Identifier" ? prop.key.name : String(prop.key.value);
|
|
1817
|
+
assertPropAllowed(key);
|
|
1818
|
+
obj[key] = evaluate(prop.value, scope);
|
|
1819
|
+
}
|
|
1820
|
+
return obj;
|
|
1821
|
+
}
|
|
1822
|
+
case "ArrowFunctionExpression":
|
|
1823
|
+
case "FunctionExpression":
|
|
1824
|
+
if (node.async || node.generator)
|
|
1825
|
+
throw new Error("async/generator functions are not supported");
|
|
1826
|
+
return makeFunction(node, scope);
|
|
1827
|
+
case "UnaryExpression": {
|
|
1828
|
+
if (node.operator === "typeof" && node.argument.type === "Identifier" && !scope.has(node.argument.name)) {
|
|
1829
|
+
return "undefined";
|
|
1830
|
+
}
|
|
1831
|
+
if (node.operator === "delete") {
|
|
1832
|
+
if (node.argument.type !== "MemberExpression")
|
|
1833
|
+
return true;
|
|
1834
|
+
const obj = evaluate(node.argument.object, scope);
|
|
1835
|
+
const prop = node.argument.computed ? evaluate(node.argument.property, scope) : node.argument.property.name;
|
|
1836
|
+
const name = assertPropAllowed(prop);
|
|
1837
|
+
if (isPlainObject(obj) && !Object.isFrozen(obj)) {
|
|
1838
|
+
delete obj[name];
|
|
1839
|
+
return true;
|
|
1840
|
+
}
|
|
1841
|
+
throw new Error("delete is only allowed on plain objects");
|
|
1842
|
+
}
|
|
1843
|
+
const arg = evaluate(node.argument, scope);
|
|
1844
|
+
switch (node.operator) {
|
|
1845
|
+
case "-":
|
|
1846
|
+
return -arg;
|
|
1847
|
+
case "+":
|
|
1848
|
+
return +arg;
|
|
1849
|
+
case "!":
|
|
1850
|
+
return !arg;
|
|
1851
|
+
case "~":
|
|
1852
|
+
return ~arg;
|
|
1853
|
+
case "typeof":
|
|
1854
|
+
return typeof arg;
|
|
1855
|
+
case "void":
|
|
1856
|
+
return void 0;
|
|
1857
|
+
default:
|
|
1858
|
+
throw new Error(`Unsupported unary operator: ${node.operator}`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
case "UpdateExpression": {
|
|
1862
|
+
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);
|
|
1863
|
+
const next = node.operator === "++" ? old + 1 : old - 1;
|
|
1864
|
+
assignTo(node.argument, next, scope);
|
|
1865
|
+
return node.prefix ? next : old;
|
|
1866
|
+
}
|
|
1867
|
+
case "BinaryExpression": {
|
|
1868
|
+
if (node.operator === "in") {
|
|
1869
|
+
const key = assertPropAllowed(evaluate(node.left, scope));
|
|
1870
|
+
const obj = evaluate(node.right, scope);
|
|
1871
|
+
if (isPlainObject(obj))
|
|
1872
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1873
|
+
if (Array.isArray(obj))
|
|
1874
|
+
return Number(key) >= 0 && Number(key) < obj.length;
|
|
1875
|
+
throw new Error('"in" is only supported on plain objects and arrays');
|
|
1876
|
+
}
|
|
1877
|
+
if (node.operator === "instanceof")
|
|
1878
|
+
throw new Error("instanceof is not supported");
|
|
1879
|
+
const l = evaluate(node.left, scope);
|
|
1880
|
+
const r = evaluate(node.right, scope);
|
|
1881
|
+
switch (node.operator) {
|
|
1882
|
+
case "+":
|
|
1883
|
+
return l + r;
|
|
1884
|
+
case "-":
|
|
1885
|
+
return l - r;
|
|
1886
|
+
case "*":
|
|
1887
|
+
return l * r;
|
|
1888
|
+
case "/":
|
|
1889
|
+
return l / r;
|
|
1890
|
+
case "%":
|
|
1891
|
+
return l % r;
|
|
1892
|
+
case "**":
|
|
1893
|
+
return l ** r;
|
|
1894
|
+
case "==":
|
|
1895
|
+
return l == r;
|
|
1896
|
+
// eslint-disable-line eqeqeq
|
|
1897
|
+
case "!=":
|
|
1898
|
+
return l != r;
|
|
1899
|
+
// eslint-disable-line eqeqeq
|
|
1900
|
+
case "===":
|
|
1901
|
+
return l === r;
|
|
1902
|
+
case "!==":
|
|
1903
|
+
return l !== r;
|
|
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
|
+
case "<<":
|
|
1919
|
+
return l << r;
|
|
1920
|
+
case ">>":
|
|
1921
|
+
return l >> r;
|
|
1922
|
+
case ">>>":
|
|
1923
|
+
return l >>> r;
|
|
1924
|
+
default:
|
|
1925
|
+
throw new Error(`Unsupported binary operator: ${node.operator}`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
case "LogicalExpression": {
|
|
1929
|
+
const l = evaluate(node.left, scope);
|
|
1930
|
+
if (node.operator === "&&")
|
|
1931
|
+
return l ? evaluate(node.right, scope) : l;
|
|
1932
|
+
if (node.operator === "||")
|
|
1933
|
+
return l ? l : evaluate(node.right, scope);
|
|
1934
|
+
if (node.operator === "??")
|
|
1935
|
+
return l ?? evaluate(node.right, scope);
|
|
1936
|
+
throw new Error(`Unsupported logical operator: ${node.operator}`);
|
|
1937
|
+
}
|
|
1938
|
+
case "AssignmentExpression": {
|
|
1939
|
+
let value;
|
|
1940
|
+
if (node.operator === "=") {
|
|
1941
|
+
value = evaluate(node.right, scope);
|
|
1942
|
+
} else {
|
|
1943
|
+
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);
|
|
1944
|
+
const rhs = () => evaluate(node.right, scope);
|
|
1945
|
+
switch (node.operator) {
|
|
1946
|
+
case "+=":
|
|
1947
|
+
value = current + rhs();
|
|
1948
|
+
break;
|
|
1949
|
+
case "-=":
|
|
1950
|
+
value = current - rhs();
|
|
1951
|
+
break;
|
|
1952
|
+
case "*=":
|
|
1953
|
+
value = current * rhs();
|
|
1954
|
+
break;
|
|
1955
|
+
case "/=":
|
|
1956
|
+
value = current / rhs();
|
|
1957
|
+
break;
|
|
1958
|
+
case "%=":
|
|
1959
|
+
value = current % rhs();
|
|
1960
|
+
break;
|
|
1961
|
+
case "**=":
|
|
1962
|
+
value = current ** rhs();
|
|
1963
|
+
break;
|
|
1964
|
+
case "&=":
|
|
1965
|
+
value = current & rhs();
|
|
1966
|
+
break;
|
|
1967
|
+
case "|=":
|
|
1968
|
+
value = current | rhs();
|
|
1969
|
+
break;
|
|
1970
|
+
case "^=":
|
|
1971
|
+
value = current ^ rhs();
|
|
1972
|
+
break;
|
|
1973
|
+
case "<<=":
|
|
1974
|
+
value = current << rhs();
|
|
1975
|
+
break;
|
|
1976
|
+
case ">>=":
|
|
1977
|
+
value = current >> rhs();
|
|
1978
|
+
break;
|
|
1979
|
+
case ">>>=":
|
|
1980
|
+
value = current >>> rhs();
|
|
1981
|
+
break;
|
|
1982
|
+
case "&&=":
|
|
1983
|
+
value = current ? rhs() : current;
|
|
1984
|
+
break;
|
|
1985
|
+
case "||=":
|
|
1986
|
+
value = current ? current : rhs();
|
|
1987
|
+
break;
|
|
1988
|
+
case "??=":
|
|
1989
|
+
value = current ?? rhs();
|
|
1990
|
+
break;
|
|
1991
|
+
default:
|
|
1992
|
+
throw new Error(`Unsupported assignment operator: ${node.operator}`);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
assignTo(node.left, value, scope);
|
|
1996
|
+
return value;
|
|
1997
|
+
}
|
|
1998
|
+
case "ConditionalExpression":
|
|
1999
|
+
return evaluate(node.test, scope) ? evaluate(node.consequent, scope) : evaluate(node.alternate, scope);
|
|
2000
|
+
case "CallExpression": {
|
|
2001
|
+
const args = [];
|
|
2002
|
+
for (const argNode of node.arguments) {
|
|
2003
|
+
if (argNode.type === "SpreadElement") {
|
|
2004
|
+
const spread = evaluate(argNode.argument, scope);
|
|
2005
|
+
if (!Array.isArray(spread) && typeof spread !== "string") {
|
|
2006
|
+
throw new Error("Spread only supports arrays and strings");
|
|
2007
|
+
}
|
|
2008
|
+
for (const item of spread)
|
|
2009
|
+
args.push(item);
|
|
2010
|
+
} else {
|
|
2011
|
+
args.push(evaluate(argNode, scope));
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (node.callee.type === "MemberExpression") {
|
|
2015
|
+
const obj = evaluate(node.callee.object, scope);
|
|
2016
|
+
if (node.optional && (obj === null || obj === void 0))
|
|
2017
|
+
return void 0;
|
|
2018
|
+
const prop = node.callee.computed ? evaluate(node.callee.property, scope) : node.callee.property.name;
|
|
2019
|
+
const method = getMember(obj, prop);
|
|
2020
|
+
if (node.callee.optional && (method === null || method === void 0))
|
|
2021
|
+
return void 0;
|
|
2022
|
+
return callFunction(method, obj, args, `${String(prop)}`);
|
|
2023
|
+
}
|
|
2024
|
+
const fn = evaluate(node.callee, scope);
|
|
2025
|
+
if (node.optional && (fn === null || fn === void 0))
|
|
2026
|
+
return void 0;
|
|
2027
|
+
const desc = node.callee.type === "Identifier" ? node.callee.name : "expression";
|
|
2028
|
+
return callFunction(fn, void 0, args, desc);
|
|
2029
|
+
}
|
|
2030
|
+
case "MemberExpression": {
|
|
2031
|
+
const obj = evaluate(node.object, scope);
|
|
2032
|
+
if (node.optional && (obj === null || obj === void 0))
|
|
2033
|
+
return void 0;
|
|
2034
|
+
const prop = node.computed ? evaluate(node.property, scope) : node.property.name;
|
|
2035
|
+
return getMember(obj, prop);
|
|
2036
|
+
}
|
|
2037
|
+
case "ChainExpression":
|
|
2038
|
+
return evaluate(node.expression, scope);
|
|
2039
|
+
case "SequenceExpression": {
|
|
2040
|
+
let result;
|
|
2041
|
+
for (const expr of node.expressions)
|
|
2042
|
+
result = evaluate(expr, scope);
|
|
2043
|
+
return result;
|
|
2044
|
+
}
|
|
2045
|
+
case "NewExpression":
|
|
2046
|
+
throw new Error('"new" is not supported in build code');
|
|
2047
|
+
case "ThisExpression":
|
|
2048
|
+
throw new Error('"this" is not supported in build code');
|
|
2049
|
+
case "AwaitExpression":
|
|
2050
|
+
throw new Error("await is not supported in build code");
|
|
2051
|
+
case "TaggedTemplateExpression":
|
|
2052
|
+
throw new Error("Tagged templates are not supported in build code");
|
|
2053
|
+
default:
|
|
2054
|
+
throw new Error(`Unsupported expression: ${node.type}`);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
executeBlock(ast.body, globalScope);
|
|
2058
|
+
}
|
|
2059
|
+
var ScriptTimeoutError, BreakSignal, ContinueSignal, ReturnSignal, FORBIDDEN_PROPS, ARRAY_MEMBERS, STRING_MEMBERS, NUMBER_MEMBERS, Scope;
|
|
2060
|
+
var init_build_interpreter = __esm({
|
|
2061
|
+
"../core/dist/tools/build-interpreter.js"() {
|
|
2062
|
+
"use strict";
|
|
2063
|
+
ScriptTimeoutError = class extends Error {
|
|
2064
|
+
constructor(message) {
|
|
2065
|
+
super(message);
|
|
2066
|
+
this.name = "ScriptTimeoutError";
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
BreakSignal = class {
|
|
2070
|
+
label;
|
|
2071
|
+
constructor(label) {
|
|
2072
|
+
this.label = label;
|
|
2073
|
+
}
|
|
2074
|
+
};
|
|
2075
|
+
ContinueSignal = class {
|
|
2076
|
+
label;
|
|
2077
|
+
constructor(label) {
|
|
2078
|
+
this.label = label;
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
ReturnSignal = class {
|
|
2082
|
+
value;
|
|
2083
|
+
constructor(value) {
|
|
2084
|
+
this.value = value;
|
|
2085
|
+
}
|
|
2086
|
+
};
|
|
2087
|
+
FORBIDDEN_PROPS = /* @__PURE__ */ new Set([
|
|
2088
|
+
"__proto__",
|
|
2089
|
+
"constructor",
|
|
2090
|
+
"prototype",
|
|
2091
|
+
"__defineGetter__",
|
|
2092
|
+
"__defineSetter__",
|
|
2093
|
+
"__lookupGetter__",
|
|
2094
|
+
"__lookupSetter__",
|
|
2095
|
+
"caller",
|
|
2096
|
+
"callee",
|
|
2097
|
+
"arguments",
|
|
2098
|
+
"bind",
|
|
2099
|
+
"call",
|
|
2100
|
+
"apply"
|
|
2101
|
+
]);
|
|
2102
|
+
ARRAY_MEMBERS = /* @__PURE__ */ new Set([
|
|
2103
|
+
"length",
|
|
2104
|
+
"push",
|
|
2105
|
+
"pop",
|
|
2106
|
+
"shift",
|
|
2107
|
+
"unshift",
|
|
2108
|
+
"slice",
|
|
2109
|
+
"splice",
|
|
2110
|
+
"indexOf",
|
|
2111
|
+
"lastIndexOf",
|
|
2112
|
+
"includes",
|
|
2113
|
+
"join",
|
|
2114
|
+
"map",
|
|
2115
|
+
"filter",
|
|
2116
|
+
"forEach",
|
|
2117
|
+
"reduce",
|
|
2118
|
+
"reduceRight",
|
|
2119
|
+
"concat",
|
|
2120
|
+
"reverse",
|
|
2121
|
+
"sort",
|
|
2122
|
+
"find",
|
|
2123
|
+
"findIndex",
|
|
2124
|
+
"some",
|
|
2125
|
+
"every",
|
|
2126
|
+
"flat",
|
|
2127
|
+
"flatMap",
|
|
2128
|
+
"fill"
|
|
2129
|
+
]);
|
|
2130
|
+
STRING_MEMBERS = /* @__PURE__ */ new Set([
|
|
2131
|
+
"length",
|
|
2132
|
+
"charAt",
|
|
2133
|
+
"charCodeAt",
|
|
2134
|
+
"codePointAt",
|
|
2135
|
+
"indexOf",
|
|
2136
|
+
"lastIndexOf",
|
|
2137
|
+
"includes",
|
|
2138
|
+
"startsWith",
|
|
2139
|
+
"endsWith",
|
|
2140
|
+
"slice",
|
|
2141
|
+
"substring",
|
|
2142
|
+
"toUpperCase",
|
|
2143
|
+
"toLowerCase",
|
|
2144
|
+
"trim",
|
|
2145
|
+
"trimStart",
|
|
2146
|
+
"trimEnd",
|
|
2147
|
+
"split",
|
|
2148
|
+
"repeat",
|
|
2149
|
+
"padStart",
|
|
2150
|
+
"padEnd",
|
|
2151
|
+
"concat",
|
|
2152
|
+
"at",
|
|
2153
|
+
"replace",
|
|
2154
|
+
"replaceAll"
|
|
2155
|
+
]);
|
|
2156
|
+
NUMBER_MEMBERS = /* @__PURE__ */ new Set(["toFixed", "toPrecision", "toString"]);
|
|
2157
|
+
Scope = class {
|
|
2158
|
+
parent;
|
|
2159
|
+
isFunctionScope;
|
|
2160
|
+
vars = /* @__PURE__ */ new Map();
|
|
2161
|
+
consts = /* @__PURE__ */ new Set();
|
|
2162
|
+
constructor(parent, isFunctionScope = false) {
|
|
2163
|
+
this.parent = parent;
|
|
2164
|
+
this.isFunctionScope = isFunctionScope;
|
|
2165
|
+
}
|
|
2166
|
+
declare(name, value, kind) {
|
|
2167
|
+
if (kind === "var" && !this.isFunctionScope && this.parent) {
|
|
2168
|
+
this.parent.declare(name, value, kind);
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
this.vars.set(name, value);
|
|
2172
|
+
if (kind === "const")
|
|
2173
|
+
this.consts.add(name);
|
|
2174
|
+
}
|
|
2175
|
+
resolve(name) {
|
|
2176
|
+
if (this.vars.has(name))
|
|
2177
|
+
return this;
|
|
2178
|
+
return this.parent?.resolve(name);
|
|
2179
|
+
}
|
|
2180
|
+
globalScope() {
|
|
2181
|
+
return this.parent ? this.parent.globalScope() : this;
|
|
2182
|
+
}
|
|
2183
|
+
has(name) {
|
|
2184
|
+
return this.resolve(name) !== void 0;
|
|
2185
|
+
}
|
|
2186
|
+
get(name) {
|
|
2187
|
+
const s = this.resolve(name);
|
|
2188
|
+
if (!s)
|
|
2189
|
+
throw new Error(`${name} is not defined`);
|
|
2190
|
+
return s.vars.get(name);
|
|
2191
|
+
}
|
|
2192
|
+
set(name, value) {
|
|
2193
|
+
const s = this.resolve(name);
|
|
2194
|
+
if (!s) {
|
|
2195
|
+
this.globalScope().vars.set(name, value);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
if (s.consts.has(name))
|
|
2199
|
+
throw new Error(`Assignment to constant variable "${name}"`);
|
|
2200
|
+
s.vars.set(name, value);
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
|
|
1086
2206
|
// ../core/dist/tools/build-executor.js
|
|
1087
|
-
import * as vm from "vm";
|
|
1088
2207
|
function createSeededRng(seed) {
|
|
1089
2208
|
let s = seed;
|
|
1090
2209
|
return () => {
|
|
@@ -1434,6 +2553,7 @@ function runBuildExecutor(code, palette, seed, options) {
|
|
|
1434
2553
|
]);
|
|
1435
2554
|
}
|
|
1436
2555
|
const rng = createSeededRng(seed ?? 42);
|
|
2556
|
+
const safeMath = Object.freeze(Object.create(null, Object.getOwnPropertyDescriptors(Math)));
|
|
1437
2557
|
const sandbox = {
|
|
1438
2558
|
part: partFn,
|
|
1439
2559
|
rpart: rpartFn,
|
|
@@ -1450,25 +2570,21 @@ function runBuildExecutor(code, palette, seed, options) {
|
|
|
1450
2570
|
column: columnFn,
|
|
1451
2571
|
pew: pewFn,
|
|
1452
2572
|
fence: fenceFn,
|
|
1453
|
-
Math,
|
|
2573
|
+
Math: safeMath,
|
|
1454
2574
|
GRID_SIZE: 1,
|
|
1455
2575
|
rng,
|
|
1456
|
-
console: { log: () => {
|
|
2576
|
+
console: Object.freeze({ log: () => {
|
|
1457
2577
|
}, warn: () => {
|
|
1458
2578
|
}, error: () => {
|
|
1459
|
-
} }
|
|
2579
|
+
} })
|
|
1460
2580
|
};
|
|
1461
|
-
const context = vm.createContext(sandbox, {
|
|
1462
|
-
codeGeneration: { strings: false, wasm: false }
|
|
1463
|
-
});
|
|
1464
|
-
const script = new vm.Script(code, { filename: "build-generator.js" });
|
|
1465
2581
|
try {
|
|
1466
|
-
|
|
2582
|
+
runRestrictedScript(code, sandbox, { timeoutMs: timeout });
|
|
1467
2583
|
} catch (err) {
|
|
1468
|
-
if (err
|
|
2584
|
+
if (err instanceof ScriptTimeoutError) {
|
|
1469
2585
|
throw new Error(`Build code execution timed out after ${timeout}ms`);
|
|
1470
2586
|
}
|
|
1471
|
-
throw new Error(`Build code execution error: ${err
|
|
2587
|
+
throw new Error(`Build code execution error: ${err?.message ?? String(err)}`);
|
|
1472
2588
|
}
|
|
1473
2589
|
if (parts.length === 0) {
|
|
1474
2590
|
throw new Error("Build code produced no parts. Make sure to call part(), wall(), floor(), etc.");
|
|
@@ -1480,6 +2596,7 @@ var DEFAULT_TIMEOUT, DEFAULT_MAX_PARTS, VALID_SHAPES;
|
|
|
1480
2596
|
var init_build_executor = __esm({
|
|
1481
2597
|
"../core/dist/tools/build-executor.js"() {
|
|
1482
2598
|
"use strict";
|
|
2599
|
+
init_build_interpreter();
|
|
1483
2600
|
DEFAULT_TIMEOUT = 1e4;
|
|
1484
2601
|
DEFAULT_MAX_PARTS = 1e4;
|
|
1485
2602
|
VALID_SHAPES = /* @__PURE__ */ new Set(["Block", "Wedge", "Cylinder", "Ball", "CornerWedge"]);
|
|
@@ -2105,7 +3222,7 @@ var init_managed_instance_registry = __esm({
|
|
|
2105
3222
|
|
|
2106
3223
|
// ../core/dist/studio-instance-manager.js
|
|
2107
3224
|
import { execFileSync, spawn } from "child_process";
|
|
2108
|
-
import { copyFileSync, existsSync, mkdirSync as
|
|
3225
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync, rmSync as rmSync2, statSync as statSync2 } from "fs";
|
|
2109
3226
|
import { randomUUID } from "crypto";
|
|
2110
3227
|
import * as os2 from "os";
|
|
2111
3228
|
import * as path2 from "path";
|
|
@@ -2120,14 +3237,14 @@ function isWsl() {
|
|
|
2120
3237
|
if (process.platform !== "linux")
|
|
2121
3238
|
return false;
|
|
2122
3239
|
try {
|
|
2123
|
-
return /microsoft|wsl/i.test(
|
|
3240
|
+
return /microsoft|wsl/i.test(readFileSync3("/proc/version", "utf8"));
|
|
2124
3241
|
} catch {
|
|
2125
3242
|
return false;
|
|
2126
3243
|
}
|
|
2127
3244
|
}
|
|
2128
3245
|
function powershell(script) {
|
|
2129
3246
|
return run("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
2130
|
-
cwd: isWsl() &&
|
|
3247
|
+
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
2131
3248
|
});
|
|
2132
3249
|
}
|
|
2133
3250
|
function windowsLocalAppData() {
|
|
@@ -2137,7 +3254,7 @@ function windowsLocalAppData() {
|
|
|
2137
3254
|
return void 0;
|
|
2138
3255
|
try {
|
|
2139
3256
|
return run("cmd.exe", ["/c", "echo %LOCALAPPDATA%"], {
|
|
2140
|
-
cwd:
|
|
3257
|
+
cwd: existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
2141
3258
|
});
|
|
2142
3259
|
} catch {
|
|
2143
3260
|
return void 0;
|
|
@@ -2149,7 +3266,7 @@ function toWslPath(windowsPath) {
|
|
|
2149
3266
|
return run("wslpath", ["-u", windowsPath]);
|
|
2150
3267
|
}
|
|
2151
3268
|
function toStudioLaunchArg(arg) {
|
|
2152
|
-
if (!isWsl() || !path2.isAbsolute(arg) || !
|
|
3269
|
+
if (!isWsl() || !path2.isAbsolute(arg) || !existsSync2(arg))
|
|
2153
3270
|
return arg;
|
|
2154
3271
|
return run("wslpath", ["-w", arg]);
|
|
2155
3272
|
}
|
|
@@ -2175,13 +3292,44 @@ function resolveBaseplateTemplatePath() {
|
|
|
2175
3292
|
path2.join(process.cwd(), "assets", BASEPLATE_TEMPLATE_NAME)
|
|
2176
3293
|
];
|
|
2177
3294
|
for (const candidate of candidates) {
|
|
2178
|
-
if (
|
|
3295
|
+
if (existsSync2(candidate))
|
|
2179
3296
|
return candidate;
|
|
2180
3297
|
}
|
|
2181
3298
|
throw new Error(`Baseplate template not found. Expected ${BASEPLATE_TEMPLATE_NAME} in one of: ${candidates.join(", ")}`);
|
|
2182
3299
|
}
|
|
3300
|
+
function isProcessAlive(pid) {
|
|
3301
|
+
try {
|
|
3302
|
+
process.kill(pid, 0);
|
|
3303
|
+
return true;
|
|
3304
|
+
} catch (err) {
|
|
3305
|
+
return err.code === "EPERM";
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
function sweepStaleBaseplateFiles() {
|
|
3309
|
+
let entries;
|
|
3310
|
+
try {
|
|
3311
|
+
entries = readdirSync2(BASEPLATE_TEMP_DIR);
|
|
3312
|
+
} catch {
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
const cutoff = Date.now() - STALE_BASEPLATE_MAX_AGE_MS;
|
|
3316
|
+
for (const entry of entries) {
|
|
3317
|
+
const match = BASEPLATE_TEMP_SWEEP_NAME.exec(entry);
|
|
3318
|
+
if (!match)
|
|
3319
|
+
continue;
|
|
3320
|
+
if (Number(match[1]) !== process.pid && isProcessAlive(Number(match[1])))
|
|
3321
|
+
continue;
|
|
3322
|
+
const file = path2.join(BASEPLATE_TEMP_DIR, entry);
|
|
3323
|
+
try {
|
|
3324
|
+
if (statSync2(file).mtimeMs < cutoff)
|
|
3325
|
+
rmSync2(file, { force: true });
|
|
3326
|
+
} catch {
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
2183
3330
|
function createBaseplatePlaceFile() {
|
|
2184
|
-
|
|
3331
|
+
mkdirSync3(BASEPLATE_TEMP_DIR, { recursive: true });
|
|
3332
|
+
sweepStaleBaseplateFiles();
|
|
2185
3333
|
const file = path2.join(BASEPLATE_TEMP_DIR, `Baseplate-${process.pid}-${Date.now()}.rbxl`);
|
|
2186
3334
|
copyFileSync(resolveBaseplateTemplatePath(), file);
|
|
2187
3335
|
return file;
|
|
@@ -2195,8 +3343,12 @@ function cleanupManagedBaseplateFiles(record) {
|
|
|
2195
3343
|
return;
|
|
2196
3344
|
if (!isGeneratedBaseplatePlaceFile(record.localPlaceFile))
|
|
2197
3345
|
return;
|
|
2198
|
-
|
|
2199
|
-
|
|
3346
|
+
for (const file of [record.localPlaceFile, `${record.localPlaceFile}.lock`]) {
|
|
3347
|
+
try {
|
|
3348
|
+
rmSync2(file, { force: true });
|
|
3349
|
+
} catch {
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
2200
3352
|
}
|
|
2201
3353
|
function prepareStudioLaunchOptions(options) {
|
|
2202
3354
|
if (options.source !== "baseplate" || options.localPlaceFile)
|
|
@@ -2217,10 +3369,10 @@ function resolveStudioExe() {
|
|
|
2217
3369
|
}
|
|
2218
3370
|
const localAppData = windowsLocalAppData();
|
|
2219
3371
|
const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
2220
|
-
if (!
|
|
3372
|
+
if (!existsSync2(root)) {
|
|
2221
3373
|
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
2222
3374
|
}
|
|
2223
|
-
const candidates = readdirSync2(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) =>
|
|
3375
|
+
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);
|
|
2224
3376
|
if (candidates.length === 0) {
|
|
2225
3377
|
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
2226
3378
|
}
|
|
@@ -2255,7 +3407,7 @@ function listStudioProcesses() {
|
|
|
2255
3407
|
function currentBootId() {
|
|
2256
3408
|
if (process.platform === "linux") {
|
|
2257
3409
|
try {
|
|
2258
|
-
return
|
|
3410
|
+
return readFileSync3("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
2259
3411
|
} catch {
|
|
2260
3412
|
}
|
|
2261
3413
|
}
|
|
@@ -2312,7 +3464,7 @@ function delay(ms) {
|
|
|
2312
3464
|
function basenameAny(filePath) {
|
|
2313
3465
|
return path2.basename(filePath.replace(/\\/g, "/"));
|
|
2314
3466
|
}
|
|
2315
|
-
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, StudioInstanceManager;
|
|
3467
|
+
var BASEPLATE_TEMP_DIR, BASEPLATE_TEMP_NAME, BASEPLATE_TEMPLATE_NAME, STALE_BASEPLATE_MAX_AGE_MS, BASEPLATE_TEMP_SWEEP_NAME, StudioInstanceManager;
|
|
2316
3468
|
var init_studio_instance_manager = __esm({
|
|
2317
3469
|
"../core/dist/studio-instance-manager.js"() {
|
|
2318
3470
|
"use strict";
|
|
@@ -2320,6 +3472,8 @@ var init_studio_instance_manager = __esm({
|
|
|
2320
3472
|
BASEPLATE_TEMP_DIR = path2.join(os2.tmpdir(), "robloxstudio-mcp-baseplates");
|
|
2321
3473
|
BASEPLATE_TEMP_NAME = /^Baseplate-\d+-\d+\.rbxl$/;
|
|
2322
3474
|
BASEPLATE_TEMPLATE_NAME = "Baseplate.rbxl";
|
|
3475
|
+
STALE_BASEPLATE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
3476
|
+
BASEPLATE_TEMP_SWEEP_NAME = /^Baseplate-(\d+)-\d+\.rbxl(\.lock)?$/;
|
|
2323
3477
|
StudioInstanceManager = class {
|
|
2324
3478
|
managedByInstanceId = /* @__PURE__ */ new Map();
|
|
2325
3479
|
pending = /* @__PURE__ */ new Set();
|
|
@@ -2364,7 +3518,7 @@ var init_studio_instance_manager = __esm({
|
|
|
2364
3518
|
const exe = this.processAdapter.resolveStudioExe?.() ?? resolveStudioExe();
|
|
2365
3519
|
const args = buildStudioLaunchArgs(preparedOptions).map(toStudioLaunchArg);
|
|
2366
3520
|
const spawnOptions = {
|
|
2367
|
-
cwd: isWsl() &&
|
|
3521
|
+
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
2368
3522
|
detached: true,
|
|
2369
3523
|
stdio: "ignore"
|
|
2370
3524
|
};
|
|
@@ -4388,6 +5542,7 @@ var init_tools = __esm({
|
|
|
4388
5542
|
init_roblox_cookie_client();
|
|
4389
5543
|
init_studio_instance_manager();
|
|
4390
5544
|
init_image_decode();
|
|
5545
|
+
init_roblox_docs();
|
|
4391
5546
|
init_jpeg_encoder();
|
|
4392
5547
|
init_png_encoder();
|
|
4393
5548
|
MAX_INLINE_IMAGE_BYTES = 6e6;
|
|
@@ -4459,6 +5614,17 @@ var init_tools = __esm({
|
|
|
4459
5614
|
_textResult(body) {
|
|
4460
5615
|
return { content: [{ type: "text", text: JSON.stringify(body) }] };
|
|
4461
5616
|
}
|
|
5617
|
+
async getRobloxDocs(name, docType, section) {
|
|
5618
|
+
if (!name || typeof name !== "string") {
|
|
5619
|
+
throw new Error('get_roblox_docs requires a name (e.g. "ProximityPrompt")');
|
|
5620
|
+
}
|
|
5621
|
+
const category = docType ?? "classes";
|
|
5622
|
+
if (!isDocCategory(category)) {
|
|
5623
|
+
throw new Error(`Invalid doc_type "${category}". Valid categories: ${DOC_CATEGORIES.join(", ")}`);
|
|
5624
|
+
}
|
|
5625
|
+
const result = await getRobloxDoc(category, name.trim(), section);
|
|
5626
|
+
return { content: [{ type: "text", text: result.content }] };
|
|
5627
|
+
}
|
|
4462
5628
|
_parseTextResult(result) {
|
|
4463
5629
|
const text = result?.content?.[0]?.text;
|
|
4464
5630
|
if (typeof text !== "string")
|
|
@@ -6094,9 +7260,9 @@ ${code}`
|
|
|
6094
7260
|
if (instance_id) {
|
|
6095
7261
|
const managedClose = this.instanceManager.closeByInstanceId(instance_id);
|
|
6096
7262
|
if (managedClose.status !== "not_found") {
|
|
6097
|
-
this.bridge.
|
|
7263
|
+
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
6098
7264
|
await sleep(500);
|
|
6099
|
-
this.bridge.
|
|
7265
|
+
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
6100
7266
|
return this._textResult({
|
|
6101
7267
|
instance_id,
|
|
6102
7268
|
message: managedClose.status === "already_closed" ? "Studio instance was already closed." : "Studio instance closed."
|
|
@@ -6119,7 +7285,7 @@ ${code}`
|
|
|
6119
7285
|
instance_id
|
|
6120
7286
|
});
|
|
6121
7287
|
}
|
|
6122
|
-
this.bridge.
|
|
7288
|
+
await this.bridge.unregisterInstanceIdEverywhere(instance_id);
|
|
6123
7289
|
return this._textResult({
|
|
6124
7290
|
instance_id,
|
|
6125
7291
|
message: "Studio instance closed."
|
|
@@ -6138,11 +7304,11 @@ ${code}`
|
|
|
6138
7304
|
record2 = active[0];
|
|
6139
7305
|
}
|
|
6140
7306
|
if (record2.instanceId)
|
|
6141
|
-
this.bridge.
|
|
7307
|
+
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
6142
7308
|
const closeResult = this.instanceManager.close(record2);
|
|
6143
7309
|
if (record2.instanceId) {
|
|
6144
7310
|
await sleep(500);
|
|
6145
|
-
this.bridge.
|
|
7311
|
+
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
6146
7312
|
}
|
|
6147
7313
|
return this._textResult({
|
|
6148
7314
|
instance_id: record2.instanceId,
|
|
@@ -7903,21 +9069,31 @@ var init_proxy_bridge_service = __esm({
|
|
|
7903
9069
|
init_bridge_service();
|
|
7904
9070
|
ProxyBridgeService = class _ProxyBridgeService extends BridgeService {
|
|
7905
9071
|
primaryBaseUrl;
|
|
9072
|
+
authToken;
|
|
7906
9073
|
proxyInstanceId;
|
|
7907
9074
|
proxyRequestTimeout = 3e4;
|
|
7908
9075
|
cachedInstances = [];
|
|
7909
9076
|
refreshTimer;
|
|
7910
9077
|
static REFRESH_INTERVAL_MS = 1e3;
|
|
7911
|
-
constructor(primaryBaseUrl) {
|
|
9078
|
+
constructor(primaryBaseUrl, authToken) {
|
|
7912
9079
|
super();
|
|
7913
9080
|
this.primaryBaseUrl = primaryBaseUrl;
|
|
9081
|
+
this.authToken = authToken;
|
|
7914
9082
|
this.proxyInstanceId = uuidv42();
|
|
7915
9083
|
this.refreshInstances();
|
|
7916
9084
|
this.refreshTimer = setInterval(() => this.refreshInstances(), _ProxyBridgeService.REFRESH_INTERVAL_MS);
|
|
7917
9085
|
}
|
|
9086
|
+
authHeaders(extra) {
|
|
9087
|
+
const headers = { ...extra };
|
|
9088
|
+
if (this.authToken)
|
|
9089
|
+
headers["X-MCP-Auth"] = this.authToken;
|
|
9090
|
+
return headers;
|
|
9091
|
+
}
|
|
7918
9092
|
async refreshInstances() {
|
|
7919
9093
|
try {
|
|
7920
|
-
const res = await fetch(`${this.primaryBaseUrl}/instances
|
|
9094
|
+
const res = await fetch(`${this.primaryBaseUrl}/instances`, {
|
|
9095
|
+
headers: this.authHeaders()
|
|
9096
|
+
});
|
|
7921
9097
|
if (!res.ok)
|
|
7922
9098
|
return;
|
|
7923
9099
|
const body = await res.json();
|
|
@@ -7930,6 +9106,24 @@ var init_proxy_bridge_service = __esm({
|
|
|
7930
9106
|
getInstances() {
|
|
7931
9107
|
return this.cachedInstances;
|
|
7932
9108
|
}
|
|
9109
|
+
async unregisterInstanceIdEverywhere(instanceId) {
|
|
9110
|
+
const response = await fetch(`${this.primaryBaseUrl}/unregister-instance-id`, {
|
|
9111
|
+
method: "POST",
|
|
9112
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
9113
|
+
body: JSON.stringify({ instanceId })
|
|
9114
|
+
});
|
|
9115
|
+
if (!response.ok) {
|
|
9116
|
+
const body = await response.text().catch(() => "");
|
|
9117
|
+
throw new Error(`Proxy unregister failed (${response.status}): ${body || response.statusText}`);
|
|
9118
|
+
}
|
|
9119
|
+
const result = await response.json();
|
|
9120
|
+
const removed = Array.isArray(result.removed) ? result.removed : [];
|
|
9121
|
+
const removedKeys = new Set(removed.map((inst) => `${inst.instanceId}\0${inst.role}`));
|
|
9122
|
+
if (removedKeys.size > 0) {
|
|
9123
|
+
this.cachedInstances = this.cachedInstances.filter((inst) => !removedKeys.has(`${inst.instanceId}\0${inst.role}`));
|
|
9124
|
+
}
|
|
9125
|
+
return removed;
|
|
9126
|
+
}
|
|
7933
9127
|
/** Called when this proxy is being discarded (e.g. promotion to primary
|
|
7934
9128
|
replaced it). Stops the background refresh so it doesn't leak. */
|
|
7935
9129
|
stop() {
|
|
@@ -7944,7 +9138,7 @@ var init_proxy_bridge_service = __esm({
|
|
|
7944
9138
|
try {
|
|
7945
9139
|
const response = await fetch(`${this.primaryBaseUrl}/proxy`, {
|
|
7946
9140
|
method: "POST",
|
|
7947
|
-
headers: { "Content-Type": "application/json" },
|
|
9141
|
+
headers: this.authHeaders({ "Content-Type": "application/json" }),
|
|
7948
9142
|
body: JSON.stringify({
|
|
7949
9143
|
endpoint,
|
|
7950
9144
|
data,
|
|
@@ -7989,6 +9183,7 @@ var init_server = __esm({
|
|
|
7989
9183
|
"../core/dist/server.js"() {
|
|
7990
9184
|
"use strict";
|
|
7991
9185
|
init_http_server();
|
|
9186
|
+
init_auth();
|
|
7992
9187
|
init_tools();
|
|
7993
9188
|
init_bridge_service();
|
|
7994
9189
|
init_proxy_bridge_service();
|
|
@@ -8012,7 +9207,7 @@ var init_server = __esm({
|
|
|
8012
9207
|
});
|
|
8013
9208
|
this.bridge = new BridgeService();
|
|
8014
9209
|
this.tools = new RobloxStudioTools(this.bridge);
|
|
8015
|
-
|
|
9210
|
+
registerResourceHandlers(this.server);
|
|
8016
9211
|
this.setupToolHandlers();
|
|
8017
9212
|
}
|
|
8018
9213
|
setupToolHandlers() {
|
|
@@ -8058,14 +9253,28 @@ var init_server = __esm({
|
|
|
8058
9253
|
}
|
|
8059
9254
|
async run() {
|
|
8060
9255
|
const basePort = process.env.ROBLOX_STUDIO_PORT ? parseInt(process.env.ROBLOX_STUDIO_PORT) : 58741;
|
|
8061
|
-
const host = process.env.ROBLOX_STUDIO_HOST || "
|
|
9256
|
+
const host = process.env.ROBLOX_STUDIO_HOST?.trim() || "127.0.0.1";
|
|
9257
|
+
if (host !== "127.0.0.1" && host !== "localhost" && host !== "::1") {
|
|
9258
|
+
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.`);
|
|
9259
|
+
}
|
|
9260
|
+
const auth = resolveAuthToken();
|
|
9261
|
+
const security = {
|
|
9262
|
+
authToken: auth.token,
|
|
9263
|
+
authTokenHint: auth.source === "env" ? "The token comes from ROBLOX_STUDIO_AUTH_TOKEN." : auth.filePath ? `The token is in ${auth.filePath}.` : void 0,
|
|
9264
|
+
allowedOrigins: (process.env.ROBLOX_STUDIO_ALLOWED_ORIGINS || "").split(",").map((o) => o.trim()).filter((o) => o !== "")
|
|
9265
|
+
};
|
|
9266
|
+
if (auth.source === "disabled") {
|
|
9267
|
+
console.error("WARNING: ROBLOX_STUDIO_NO_AUTH is set - tool endpoints accept unauthenticated requests.");
|
|
9268
|
+
} else if (auth.filePath) {
|
|
9269
|
+
console.error(`Auth token loaded from ${auth.filePath} (HTTP clients must send X-MCP-Auth or Authorization: Bearer)`);
|
|
9270
|
+
}
|
|
8062
9271
|
let bridgeMode = "primary";
|
|
8063
9272
|
let httpHandle;
|
|
8064
9273
|
let primaryApp;
|
|
8065
9274
|
let boundPort = 0;
|
|
8066
9275
|
let promotionInterval;
|
|
8067
9276
|
try {
|
|
8068
|
-
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
|
|
9277
|
+
primaryApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
|
|
8069
9278
|
const result = await listenWithRetry(primaryApp, host, basePort, 1);
|
|
8070
9279
|
httpHandle = result.server;
|
|
8071
9280
|
boundPort = result.port;
|
|
@@ -8074,7 +9283,7 @@ var init_server = __esm({
|
|
|
8074
9283
|
} catch {
|
|
8075
9284
|
bridgeMode = "proxy";
|
|
8076
9285
|
primaryApp = void 0;
|
|
8077
|
-
const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}
|
|
9286
|
+
const proxyBridge = new ProxyBridgeService(`http://localhost:${basePort}`, auth.token);
|
|
8078
9287
|
this.bridge = proxyBridge;
|
|
8079
9288
|
this.tools = new RobloxStudioTools(this.bridge);
|
|
8080
9289
|
console.error(`Port ${basePort} in use - entering proxy mode (forwarding to localhost:${basePort})`);
|
|
@@ -8082,7 +9291,7 @@ var init_server = __esm({
|
|
|
8082
9291
|
promotionInterval = setInterval(async () => {
|
|
8083
9292
|
const candidateBridge = new BridgeService();
|
|
8084
9293
|
const candidateTools = new RobloxStudioTools(candidateBridge);
|
|
8085
|
-
const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config);
|
|
9294
|
+
const candidateApp = createHttpServer(candidateTools, candidateBridge, this.allowedToolNames, this.config, security);
|
|
8086
9295
|
try {
|
|
8087
9296
|
const result = await listenWithRetry(candidateApp, host, basePort, 1);
|
|
8088
9297
|
const oldBridge = this.bridge;
|
|
@@ -8107,7 +9316,7 @@ var init_server = __esm({
|
|
|
8107
9316
|
let legacyHandle;
|
|
8108
9317
|
let legacyApp;
|
|
8109
9318
|
if (boundPort !== LEGACY_PORT && bridgeMode === "primary") {
|
|
8110
|
-
legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config);
|
|
9319
|
+
legacyApp = createHttpServer(this.tools, this.bridge, this.allowedToolNames, this.config, security);
|
|
8111
9320
|
try {
|
|
8112
9321
|
const result = await listenWithRetry(legacyApp, host, LEGACY_PORT, 1);
|
|
8113
9322
|
legacyHandle = result.server;
|
|
@@ -9964,7 +11173,7 @@ CYLINDER AXIS: Roblox cylinders extend along the X axis. For upright cylinders,
|
|
|
9964
11173
|
EXAMPLE - compact cabin (17 lines):
|
|
9965
11174
|
room(0,0,0,8,4,6,"a","b","a")
|
|
9966
11175
|
roof(0,4,0,8,6,"gable","c")
|
|
9967
|
-
wall(-4
|
|
11176
|
+
wall(-4,-2,4,-2,4,1,"a")
|
|
9968
11177
|
part(0,2,3,3,3,0.3,"a","Block",0.4)
|
|
9969
11178
|
row(-2,0,-1,3,0,2,(i,cx,cy,cz)=>{pew(cx,0,cz,3,2,"d")})
|
|
9970
11179
|
column(-3,0,-2,4,0.5,"a","b")
|
|
@@ -10758,6 +11967,31 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
10758
11967
|
},
|
|
10759
11968
|
required: ["pattern", "replacement"]
|
|
10760
11969
|
}
|
|
11970
|
+
},
|
|
11971
|
+
// === Documentation ===
|
|
11972
|
+
{
|
|
11973
|
+
name: "get_roblox_docs",
|
|
11974
|
+
category: "read",
|
|
11975
|
+
description: 'Fetch official Roblox engine API documentation as markdown from create.roblox.com. Call this BEFORE writing or editing code that uses an engine class, enum, datatype, or Luau library you are not fully certain about (e.g. ProximityPrompt, Enum.KeyCode, CFrame, TweenService) \u2014 the page includes the description, properties, methods, events, and code samples. Results are cached, so repeat lookups are cheap. Very large pages are truncated with a section index; pass section (e.g. "Properties", "Methods", "Events") to read one section in full.',
|
|
11976
|
+
inputSchema: {
|
|
11977
|
+
type: "object",
|
|
11978
|
+
properties: {
|
|
11979
|
+
name: {
|
|
11980
|
+
type: "string",
|
|
11981
|
+
description: 'Exact PascalCase name of the class, enum, datatype, or library (e.g. "ProximityPrompt", "KeyCode", "CFrame", "table")'
|
|
11982
|
+
},
|
|
11983
|
+
doc_type: {
|
|
11984
|
+
type: "string",
|
|
11985
|
+
enum: ["classes", "enums", "datatypes", "libraries", "globals"],
|
|
11986
|
+
description: "Documentation category (default: classes)"
|
|
11987
|
+
},
|
|
11988
|
+
section: {
|
|
11989
|
+
type: "string",
|
|
11990
|
+
description: 'Optional "##"-level section to return instead of the whole page (e.g. "Description", "Properties", "Methods", "Events", "Code Samples")'
|
|
11991
|
+
}
|
|
11992
|
+
},
|
|
11993
|
+
required: ["name"]
|
|
11994
|
+
}
|
|
10761
11995
|
}
|
|
10762
11996
|
];
|
|
10763
11997
|
DEPRECATED_TOOL_DEFINITIONS = [
|
|
@@ -10917,15 +12151,15 @@ part(0,2,0,2,1,1,"b")`,
|
|
|
10917
12151
|
});
|
|
10918
12152
|
|
|
10919
12153
|
// ../core/dist/install-plugin-helpers.js
|
|
10920
|
-
import { existsSync as
|
|
12154
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, unlinkSync } from "fs";
|
|
10921
12155
|
import { execSync } from "child_process";
|
|
10922
|
-
import { join as
|
|
10923
|
-
import { homedir as
|
|
12156
|
+
import { join as join5 } from "path";
|
|
12157
|
+
import { homedir as homedir5 } from "os";
|
|
10924
12158
|
function isWSL() {
|
|
10925
12159
|
if (process.platform !== "linux")
|
|
10926
12160
|
return false;
|
|
10927
12161
|
try {
|
|
10928
|
-
const v =
|
|
12162
|
+
const v = readFileSync6("/proc/version", "utf8");
|
|
10929
12163
|
return /microsoft|wsl/i.test(v);
|
|
10930
12164
|
} catch {
|
|
10931
12165
|
return false;
|
|
@@ -10943,7 +12177,7 @@ function getWindowsUserPluginsDir() {
|
|
|
10943
12177
|
}).toString().trim();
|
|
10944
12178
|
if (!linuxPath)
|
|
10945
12179
|
return null;
|
|
10946
|
-
return
|
|
12180
|
+
return join5(linuxPath, "Roblox", "Plugins");
|
|
10947
12181
|
} catch {
|
|
10948
12182
|
return null;
|
|
10949
12183
|
}
|
|
@@ -10952,7 +12186,7 @@ function getPluginsFolder() {
|
|
|
10952
12186
|
if (process.env.MCP_PLUGINS_DIR)
|
|
10953
12187
|
return process.env.MCP_PLUGINS_DIR;
|
|
10954
12188
|
if (process.platform === "win32") {
|
|
10955
|
-
return
|
|
12189
|
+
return join5(process.env.LOCALAPPDATA || join5(homedir5(), "AppData", "Local"), "Roblox", "Plugins");
|
|
10956
12190
|
}
|
|
10957
12191
|
if (isWSL()) {
|
|
10958
12192
|
const win = getWindowsUserPluginsDir();
|
|
@@ -10960,11 +12194,11 @@ function getPluginsFolder() {
|
|
|
10960
12194
|
return win;
|
|
10961
12195
|
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.");
|
|
10962
12196
|
}
|
|
10963
|
-
return
|
|
12197
|
+
return join5(homedir5(), "Documents", "Roblox", "Plugins");
|
|
10964
12198
|
}
|
|
10965
12199
|
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
10966
|
-
const otherDest =
|
|
10967
|
-
if (!
|
|
12200
|
+
const otherDest = join5(pluginsFolder, otherAssetName);
|
|
12201
|
+
if (!existsSync5(otherDest))
|
|
10968
12202
|
return;
|
|
10969
12203
|
if (replace) {
|
|
10970
12204
|
try {
|
|
@@ -11009,8 +12243,8 @@ __export(install_plugin_exports, {
|
|
|
11009
12243
|
installBundledPlugin: () => installBundledPlugin,
|
|
11010
12244
|
installPlugin: () => installPlugin
|
|
11011
12245
|
});
|
|
11012
|
-
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as
|
|
11013
|
-
import { dirname as
|
|
12246
|
+
import { copyFileSync as copyFileSync2, createWriteStream, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync7, unlinkSync as unlinkSync2 } from "fs";
|
|
12247
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
11014
12248
|
import { fileURLToPath } from "url";
|
|
11015
12249
|
import { get } from "https";
|
|
11016
12250
|
function httpsGet(url) {
|
|
@@ -11070,8 +12304,8 @@ function prepareInstall({
|
|
|
11070
12304
|
warn
|
|
11071
12305
|
}) {
|
|
11072
12306
|
const pluginsFolder = getPluginsFolder();
|
|
11073
|
-
if (!
|
|
11074
|
-
|
|
12307
|
+
if (!existsSync6(pluginsFolder)) {
|
|
12308
|
+
mkdirSync5(pluginsFolder, { recursive: true });
|
|
11075
12309
|
}
|
|
11076
12310
|
handleVariantConflict({
|
|
11077
12311
|
pluginsFolder,
|
|
@@ -11083,23 +12317,23 @@ function prepareInstall({
|
|
|
11083
12317
|
return pluginsFolder;
|
|
11084
12318
|
}
|
|
11085
12319
|
function bundledAssetPath() {
|
|
11086
|
-
const currentDir =
|
|
12320
|
+
const currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
11087
12321
|
const candidates = [
|
|
11088
|
-
|
|
11089
|
-
|
|
12322
|
+
join6(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
12323
|
+
join6(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
11090
12324
|
];
|
|
11091
|
-
return candidates.find((candidate) =>
|
|
12325
|
+
return candidates.find((candidate) => existsSync6(candidate)) ?? null;
|
|
11092
12326
|
}
|
|
11093
12327
|
function packageVersion() {
|
|
11094
|
-
const currentDir =
|
|
11095
|
-
const pkg = JSON.parse(
|
|
12328
|
+
const currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
12329
|
+
const pkg = JSON.parse(readFileSync7(join6(currentDir, "..", "package.json"), "utf8"));
|
|
11096
12330
|
if (!pkg.version) {
|
|
11097
12331
|
throw new Error("Package version not found");
|
|
11098
12332
|
}
|
|
11099
12333
|
return pkg.version;
|
|
11100
12334
|
}
|
|
11101
12335
|
function bundledPluginVersion(source) {
|
|
11102
|
-
const match =
|
|
12336
|
+
const match = readFileSync7(source, "utf8").match(/local CURRENT_VERSION = "([^"]+)"/);
|
|
11103
12337
|
return match ? match[1] : null;
|
|
11104
12338
|
}
|
|
11105
12339
|
function assertBundledPluginVersion(source) {
|
|
@@ -11112,9 +12346,9 @@ function assertBundledPluginVersion(source) {
|
|
|
11112
12346
|
}
|
|
11113
12347
|
}
|
|
11114
12348
|
function filesMatch(a, b) {
|
|
11115
|
-
if (!
|
|
11116
|
-
const aBytes =
|
|
11117
|
-
const bBytes =
|
|
12349
|
+
if (!existsSync6(b)) return false;
|
|
12350
|
+
const aBytes = readFileSync7(a);
|
|
12351
|
+
const bBytes = readFileSync7(b);
|
|
11118
12352
|
return aBytes.length === bBytes.length && aBytes.equals(bBytes);
|
|
11119
12353
|
}
|
|
11120
12354
|
async function installBundledPlugin(options = {}) {
|
|
@@ -11127,7 +12361,7 @@ async function installBundledPlugin(options = {}) {
|
|
|
11127
12361
|
}
|
|
11128
12362
|
assertBundledPluginVersion(source);
|
|
11129
12363
|
const pluginsFolder = prepareInstall({ replaceVariant, log, warn });
|
|
11130
|
-
const dest =
|
|
12364
|
+
const dest = join6(pluginsFolder, ASSET_NAME);
|
|
11131
12365
|
if (filesMatch(source, dest)) return;
|
|
11132
12366
|
copyFileSync2(source, dest);
|
|
11133
12367
|
log(`Installed ${ASSET_NAME} to ${dest}`);
|
|
@@ -11140,7 +12374,7 @@ async function installPlugin(options = {}) {
|
|
|
11140
12374
|
const bundled = bundledAssetPath();
|
|
11141
12375
|
if (bundled) {
|
|
11142
12376
|
assertBundledPluginVersion(bundled);
|
|
11143
|
-
const dest2 =
|
|
12377
|
+
const dest2 = join6(pluginsFolder, ASSET_NAME);
|
|
11144
12378
|
if (filesMatch(bundled, dest2)) {
|
|
11145
12379
|
log(`${ASSET_NAME} already installed.`);
|
|
11146
12380
|
return;
|
|
@@ -11155,7 +12389,7 @@ async function installPlugin(options = {}) {
|
|
|
11155
12389
|
if (!asset) {
|
|
11156
12390
|
throw new Error(`${ASSET_NAME} not found in release ${release.tag_name}`);
|
|
11157
12391
|
}
|
|
11158
|
-
const dest =
|
|
12392
|
+
const dest = join6(pluginsFolder, ASSET_NAME);
|
|
11159
12393
|
log(`Downloading ${ASSET_NAME} from ${release.tag_name}...`);
|
|
11160
12394
|
await download(asset.browser_download_url, dest);
|
|
11161
12395
|
log(`Installed to ${dest}`);
|