@chrrxs/robloxstudio-mcp-inspector 3.1.4 → 3.1.5
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 +920 -261
- package/package.json +1 -1
- package/studio-plugin/MCPInspectorPlugin.rbxmx +976 -132
package/dist/index.js
CHANGED
|
@@ -2869,7 +2869,7 @@ var BridgeService = class {
|
|
|
2869
2869
|
operation.status.executionStartedAt = now;
|
|
2870
2870
|
else {
|
|
2871
2871
|
operation.status.executionOutcome = outcome ?? "unknown";
|
|
2872
|
-
if (outcome
|
|
2872
|
+
if (outcome === "success" || outcome === "error")
|
|
2873
2873
|
operation.status.executionCompletedAt = now;
|
|
2874
2874
|
}
|
|
2875
2875
|
operation.updatedAt = now;
|
|
@@ -2885,7 +2885,7 @@ var BridgeService = class {
|
|
|
2885
2885
|
requestId,
|
|
2886
2886
|
targetPeerId: operation.status.targetPeerId,
|
|
2887
2887
|
...observations(operation.status),
|
|
2888
|
-
executionCompletedAt: executionOutcome === "
|
|
2888
|
+
executionCompletedAt: executionOutcome === "success" || executionOutcome === "error" ? operation.status.executionCompletedAt ?? Date.now() : void 0,
|
|
2889
2889
|
stage: "response_delivery",
|
|
2890
2890
|
outcome: executionOutcome === "not_executed" ? "not_executed" : "unknown",
|
|
2891
2891
|
executionOutcome
|
|
@@ -2913,13 +2913,15 @@ var BridgeService = class {
|
|
|
2913
2913
|
operation.status.state = "settled";
|
|
2914
2914
|
const localRejection = error instanceof RequestFailure && error.details.transportStage === "server_send";
|
|
2915
2915
|
const responseOutcome = handlerOutcome(response);
|
|
2916
|
-
const completedOutcome = !hasError && responseOutcome === "error" ? "error" : executionOutcome ?? (localRejection ? "not_executed" : hasError ? operation.status.executionOutcome === "success" ? "success" : "error" : responseOutcome);
|
|
2916
|
+
const completedOutcome = executionOutcome === "unknown" || executionOutcome === "not_executed" ? executionOutcome : !hasError && responseOutcome === "error" ? "error" : executionOutcome ?? (localRejection ? "not_executed" : hasError ? operation.status.executionOutcome === "success" ? "success" : "error" : responseOutcome);
|
|
2917
2917
|
operation.status.executionOutcome = completedOutcome;
|
|
2918
|
-
operation.status.outcome = hasError || completedOutcome === "error" || completedOutcome === "not_executed" ? "error" : "success";
|
|
2918
|
+
operation.status.outcome = hasError || responseOutcome === "error" || completedOutcome === "error" || completedOutcome === "not_executed" ? "error" : "success";
|
|
2919
2919
|
if (!localRejection) {
|
|
2920
2920
|
operation.status.stage = "response_delivery";
|
|
2921
|
-
if (completedOutcome
|
|
2921
|
+
if (completedOutcome === "success" || completedOutcome === "error")
|
|
2922
2922
|
operation.status.executionCompletedAt ??= now;
|
|
2923
|
+
else
|
|
2924
|
+
delete operation.status.executionCompletedAt;
|
|
2923
2925
|
}
|
|
2924
2926
|
operation.status.settledAt = now;
|
|
2925
2927
|
operation.updatedAt = now;
|
|
@@ -3322,6 +3324,10 @@ execute_luau runs through the Studio plugin. eval_server_runtime and eval_client
|
|
|
3322
3324
|
|
|
3323
3325
|
Read output with get_runtime_logs. Reuse nextCursor as cursor for one Instance, or nextCursorByInstance as cursor_by_instance for a Multiplayer Group, instead of requesting the full process streams again.
|
|
3324
3326
|
|
|
3327
|
+
- Each Peer VM retains approximately 64 KiB of message text, rather than a fixed message count. Reads do not consume or clear the buffer; incoming messages evict old entries.
|
|
3328
|
+
- totalDropped sums lifetime eviction counts from successfully read Peers. It counts messages evicted, not messages missed by this caller, and may decrease when Peers fail, disconnect, or reload.
|
|
3329
|
+
- For successfully read Peers, nextCursor advances past all captured entries, including those omitted by filter or tail. It is a continuation watermark, not pagination through omitted results; continuing from it will not return those entries.
|
|
3330
|
+
|
|
3325
3331
|
## Simulation and input
|
|
3326
3332
|
|
|
3327
3333
|
- Inspect current settings with get_simulation_state before changing them.
|
|
@@ -3950,6 +3956,40 @@ var StudioLaunchPreDispatchError = class extends Error {
|
|
|
3950
3956
|
};
|
|
3951
3957
|
}
|
|
3952
3958
|
};
|
|
3959
|
+
var StudioCloseVerificationError = class extends Error {
|
|
3960
|
+
observation;
|
|
3961
|
+
constructor(message, observation) {
|
|
3962
|
+
super(message);
|
|
3963
|
+
this.observation = observation;
|
|
3964
|
+
}
|
|
3965
|
+
};
|
|
3966
|
+
async function beforeCloseDeadline(operation, deadline, processId) {
|
|
3967
|
+
const { promise, reject } = Promise.withResolvers();
|
|
3968
|
+
const timeoutError = () => new StudioCloseVerificationError(`Timed out verifying Studio process ${processId} termination; process state is unknown.`, { status: "unknown", observedAt: Date.now(), error: "Close operation deadline exceeded." });
|
|
3969
|
+
if (Date.now() >= deadline)
|
|
3970
|
+
throw timeoutError();
|
|
3971
|
+
const timer = setTimeout(() => reject(timeoutError()), deadline - Date.now());
|
|
3972
|
+
try {
|
|
3973
|
+
return await Promise.race([operation(), promise]);
|
|
3974
|
+
} finally {
|
|
3975
|
+
clearTimeout(timer);
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
function observePosixProcess(processId) {
|
|
3979
|
+
try {
|
|
3980
|
+
process.kill(processId, 0);
|
|
3981
|
+
return { status: "running", observedAt: Date.now() };
|
|
3982
|
+
} catch (error) {
|
|
3983
|
+
if (error instanceof Error && "code" in error && error.code === "ESRCH") {
|
|
3984
|
+
return { status: "not_running", observedAt: Date.now(), reason: "missing" };
|
|
3985
|
+
}
|
|
3986
|
+
return {
|
|
3987
|
+
status: "unknown",
|
|
3988
|
+
observedAt: Date.now(),
|
|
3989
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3953
3993
|
function run(command, args, options = {}) {
|
|
3954
3994
|
return execFileSync2(command, args, {
|
|
3955
3995
|
encoding: "utf8",
|
|
@@ -3971,9 +4011,10 @@ async function runAsync(command, args, options = {}) {
|
|
|
3971
4011
|
function isWsl() {
|
|
3972
4012
|
return getStudioPlatformCapabilities().isWsl;
|
|
3973
4013
|
}
|
|
3974
|
-
async function powershellAsync(script) {
|
|
4014
|
+
async function powershellAsync(script, timeoutMs = 15e3) {
|
|
3975
4015
|
return runAsync("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
3976
|
-
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd()
|
|
4016
|
+
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
4017
|
+
timeout: timeoutMs
|
|
3977
4018
|
});
|
|
3978
4019
|
}
|
|
3979
4020
|
function windowsLocalAppData() {
|
|
@@ -4108,8 +4149,15 @@ function quoteWindowsCommandLineArg(value) {
|
|
|
4108
4149
|
}
|
|
4109
4150
|
return `${quoted}${"\\".repeat(backslashes * 2)}"`;
|
|
4110
4151
|
}
|
|
4111
|
-
function
|
|
4152
|
+
function parseStudioTestWorkerJobName(name) {
|
|
4153
|
+
if (name !== void 0 && !/^Local\\RsmcpStudioWorker-[a-f0-9]{32}$/u.test(name)) {
|
|
4154
|
+
throw new Error("Invalid Studio test worker job name.");
|
|
4155
|
+
}
|
|
4156
|
+
return name;
|
|
4157
|
+
}
|
|
4158
|
+
function buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory, testWorkerJobName = process.env.RSMCP_STUDIO_TEST_WORKER_JOB) {
|
|
4112
4159
|
const environmentPatch = parseStudioProcessEnvironmentPatch(processEnvironment);
|
|
4160
|
+
parseStudioTestWorkerJobName(testWorkerJobName);
|
|
4113
4161
|
const commandLine = [windowsExe, ...args].map(quoteWindowsCommandLineArg).join(" ");
|
|
4114
4162
|
return [
|
|
4115
4163
|
"$ErrorActionPreference = 'Stop'",
|
|
@@ -4222,6 +4270,15 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4222
4270
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
4223
4271
|
private static extern IntPtr CreateJobObjectW(IntPtr jobAttributes, string name);
|
|
4224
4272
|
|
|
4273
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
4274
|
+
private static extern IntPtr OpenJobObjectW(uint access, bool inheritHandle, string name);
|
|
4275
|
+
|
|
4276
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
4277
|
+
private static extern bool GetHandleInformation(IntPtr handle, out uint flags);
|
|
4278
|
+
|
|
4279
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
4280
|
+
private static extern bool QueryInformationJobObject(IntPtr job, int informationClass, IntPtr information, uint informationLength, out uint returned);
|
|
4281
|
+
|
|
4225
4282
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
4226
4283
|
private static extern bool SetInformationJobObject(
|
|
4227
4284
|
IntPtr job,
|
|
@@ -4309,20 +4366,60 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4309
4366
|
throw new InvalidOperationException("Unexpected Studio process wait result: " + wait);
|
|
4310
4367
|
}
|
|
4311
4368
|
|
|
4312
|
-
|
|
4369
|
+
private static Win32Exception TestWorkerError(string operation)
|
|
4370
|
+
{
|
|
4371
|
+
int error = Marshal.GetLastWin32Error();
|
|
4372
|
+
return new Win32Exception(error, operation + " (Win32 " + error + "): " + new Win32Exception(error).Message);
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
private static IntPtr OpenTestWorkerJob(string name)
|
|
4376
|
+
{
|
|
4377
|
+
if (name == null) return IntPtr.Zero;
|
|
4378
|
+
if (!System.Text.RegularExpressions.Regex.IsMatch(name, @"\\ALocal\\\\RsmcpStudioWorker-[a-f0-9]{32}\\z"))
|
|
4379
|
+
throw new ArgumentException("Invalid Studio test worker job name");
|
|
4380
|
+
IntPtr handle = OpenJobObjectW(5, false, name);
|
|
4381
|
+
if (handle == IntPtr.Zero)
|
|
4382
|
+
throw TestWorkerError("Open test worker job failed");
|
|
4383
|
+
try
|
|
4384
|
+
{
|
|
4385
|
+
uint flags;
|
|
4386
|
+
if (!GetHandleInformation(handle, out flags))
|
|
4387
|
+
throw TestWorkerError("Get test worker job handle flags failed");
|
|
4388
|
+
if ((flags & 1) != 0)
|
|
4389
|
+
throw new InvalidOperationException("Test worker job handle must not be inherited");
|
|
4390
|
+
int length = Marshal.SizeOf(typeof(ExtendedLimitInformation));
|
|
4391
|
+
IntPtr buffer = Marshal.AllocHGlobal(length);
|
|
4392
|
+
try
|
|
4393
|
+
{
|
|
4394
|
+
uint returned;
|
|
4395
|
+
if (!QueryInformationJobObject(handle, JOB_OBJECT_EXTENDED_LIMIT_INFORMATION, buffer, (uint)length, out returned))
|
|
4396
|
+
throw TestWorkerError("Query test worker job limits failed");
|
|
4397
|
+
var information = (ExtendedLimitInformation)Marshal.PtrToStructure(buffer, typeof(ExtendedLimitInformation));
|
|
4398
|
+
if (returned != length || information.BasicLimitInformation.LimitFlags != JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
|
|
4399
|
+
throw new InvalidOperationException("Test worker job must retain kill-on-close without breakaway");
|
|
4400
|
+
}
|
|
4401
|
+
finally { Marshal.FreeHGlobal(buffer); }
|
|
4402
|
+
return handle;
|
|
4403
|
+
}
|
|
4404
|
+
catch { CloseHandle(handle); throw; }
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
public static McpSuspendedStudio Start(string application, string commandLine, string currentDirectory, string testWorkerJobName)
|
|
4313
4408
|
{
|
|
4314
4409
|
if (String.IsNullOrEmpty(currentDirectory))
|
|
4315
4410
|
currentDirectory = null;
|
|
4316
4411
|
IntPtr jobHandle = CreateJobObjectW(IntPtr.Zero, null);
|
|
4317
4412
|
if (jobHandle == IntPtr.Zero)
|
|
4318
4413
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateJobObjectW failed");
|
|
4414
|
+
IntPtr workerJob = IntPtr.Zero;
|
|
4319
4415
|
try
|
|
4320
4416
|
{
|
|
4321
4417
|
ConfigureKillOnClose(jobHandle, true);
|
|
4418
|
+
workerJob = OpenTestWorkerJob(testWorkerJobName);
|
|
4322
4419
|
var startup = new StartupInfo();
|
|
4323
4420
|
startup.cb = Marshal.SizeOf(startup);
|
|
4324
4421
|
ProcessInformation created;
|
|
4325
|
-
uint flags = CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB;
|
|
4422
|
+
uint flags = CREATE_SUSPENDED | (workerJob == IntPtr.Zero ? CREATE_BREAKAWAY_FROM_JOB : 0);
|
|
4326
4423
|
bool started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, flags, IntPtr.Zero, currentDirectory, ref startup, out created);
|
|
4327
4424
|
if (!started && Marshal.GetLastWin32Error() == 5)
|
|
4328
4425
|
started = CreateProcessW(application, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, CREATE_SUSPENDED, IntPtr.Zero, currentDirectory, ref startup, out created);
|
|
@@ -4330,6 +4427,10 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4330
4427
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateProcessW failed");
|
|
4331
4428
|
try
|
|
4332
4429
|
{
|
|
4430
|
+
// Worker lifetime encloses the private launch-authorization job.
|
|
4431
|
+
// COMPLETE may release authorization, never test-worker ownership.
|
|
4432
|
+
if (workerJob != IntPtr.Zero && !AssignProcessToJobObject(workerJob, created.hProcess))
|
|
4433
|
+
throw TestWorkerError("Assign Studio to test worker job failed");
|
|
4333
4434
|
if (!AssignProcessToJobObject(jobHandle, created.hProcess))
|
|
4334
4435
|
throw new Win32Exception(Marshal.GetLastWin32Error(), "AssignProcessToJobObject failed");
|
|
4335
4436
|
return new McpSuspendedStudio(created, jobHandle);
|
|
@@ -4353,6 +4454,10 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4353
4454
|
CloseHandle(jobHandle);
|
|
4354
4455
|
throw;
|
|
4355
4456
|
}
|
|
4457
|
+
finally
|
|
4458
|
+
{
|
|
4459
|
+
if (workerJob != IntPtr.Zero) CloseHandle(workerJob);
|
|
4460
|
+
}
|
|
4356
4461
|
}
|
|
4357
4462
|
|
|
4358
4463
|
public void Resume()
|
|
@@ -4402,7 +4507,7 @@ public sealed class McpSuspendedStudio : IDisposable
|
|
|
4402
4507
|
}
|
|
4403
4508
|
}
|
|
4404
4509
|
'@`,
|
|
4405
|
-
`$launch = [McpSuspendedStudio]::Start(${powershellStringLiteral(windowsExe)}, ${powershellStringLiteral(commandLine)}, ${windowsWorkingDirectory === void 0 ? "$null" : powershellStringLiteral(windowsWorkingDirectory)})`,
|
|
4510
|
+
`$launch = [McpSuspendedStudio]::Start(${powershellStringLiteral(windowsExe)}, ${powershellStringLiteral(commandLine)}, ${windowsWorkingDirectory === void 0 ? "$null" : powershellStringLiteral(windowsWorkingDirectory)}, ${testWorkerJobName === void 0 ? "$null" : powershellStringLiteral(testWorkerJobName)})`,
|
|
4406
4511
|
"try {",
|
|
4407
4512
|
"[Console]::Out.WriteLine((ConvertTo-Json @{ pid = $launch.ProcessId; started = [string]$launch.StartedAtFileTime } -Compress)); [Console]::Out.Flush()",
|
|
4408
4513
|
"$accepted = $false",
|
|
@@ -4441,13 +4546,14 @@ function buildWindowsStudioStopScript(processId, startedAt) {
|
|
|
4441
4546
|
"} finally { if ($null -ne $studio) { $studio.Dispose() } }"
|
|
4442
4547
|
].join("; ");
|
|
4443
4548
|
}
|
|
4444
|
-
async function stopWindowsStudio(processId, startedAt) {
|
|
4445
|
-
await powershellAsync(buildWindowsStudioStopScript(processId, startedAt));
|
|
4549
|
+
async function stopWindowsStudio(processId, startedAt, timeoutMs = 15e3) {
|
|
4550
|
+
await powershellAsync(buildWindowsStudioStopScript(processId, startedAt), timeoutMs);
|
|
4446
4551
|
}
|
|
4447
4552
|
async function spawnWindowsStudio(exe, args, processEnvironment, studioWorkingDirectory) {
|
|
4448
4553
|
const windowsExe = await toStudioLaunchArgAsync(exe);
|
|
4449
4554
|
const windowsWorkingDirectory = studioWorkingDirectory ? await toStudioLaunchArgAsync(studioWorkingDirectory) : void 0;
|
|
4450
|
-
const
|
|
4555
|
+
const testWorkerJobName = process.env.RSMCP_STUDIO_TEST_WORKER_JOB;
|
|
4556
|
+
const script = buildWindowsStudioStartScriptFromConvertedExe(windowsExe, args, processEnvironment, windowsWorkingDirectory, testWorkerJobName);
|
|
4451
4557
|
const launcher = spawn("powershell.exe", ["-NoProfile", "-Command", script], {
|
|
4452
4558
|
cwd: isWsl() && existsSync2("/mnt/c/Windows") ? "/mnt/c/Windows" : process.cwd(),
|
|
4453
4559
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -4754,6 +4860,24 @@ function prepareStudioLaunchOptions(options) {
|
|
|
4754
4860
|
localPlaceFile: createBaseplatePlaceFile()
|
|
4755
4861
|
};
|
|
4756
4862
|
}
|
|
4863
|
+
function resolveStudioExeFromVersions(root) {
|
|
4864
|
+
if (!existsSync2(root)) {
|
|
4865
|
+
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4866
|
+
}
|
|
4867
|
+
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
4868
|
+
if (candidates.length === 0) {
|
|
4869
|
+
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4870
|
+
}
|
|
4871
|
+
const exe = candidates[0];
|
|
4872
|
+
const installationDir = path2.dirname(exe);
|
|
4873
|
+
const hasIncompleteDownload = readdirSync(installationDir).some((name) => /\.crdownload$/i.test(name));
|
|
4874
|
+
const settings = statSync(path2.join(installationDir, "AppSettings.xml"), { throwIfNoEntry: false });
|
|
4875
|
+
if (hasIncompleteDownload || !settings?.isFile() || settings.size === 0) {
|
|
4876
|
+
const reason = hasIncompleteDownload ? "an unfinished .crdownload download is present" : "AppSettings.xml is missing, empty, or not a regular file";
|
|
4877
|
+
throw new Error(`Roblox Studio installation/update is incomplete at ${installationDir}: ${reason}. Complete the installation/update or reinstall Roblox Studio under the owning Windows account before launching.`);
|
|
4878
|
+
}
|
|
4879
|
+
return exe;
|
|
4880
|
+
}
|
|
4757
4881
|
function resolveStudioExe() {
|
|
4758
4882
|
if (process.env.ROBLOX_STUDIO_EXE)
|
|
4759
4883
|
return process.env.ROBLOX_STUDIO_EXE;
|
|
@@ -4765,14 +4889,7 @@ function resolveStudioExe() {
|
|
|
4765
4889
|
}
|
|
4766
4890
|
const localAppData = windowsLocalAppData();
|
|
4767
4891
|
const root = localAppData ? path2.join(toWslPath(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
4768
|
-
|
|
4769
|
-
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4770
|
-
}
|
|
4771
|
-
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
4772
|
-
if (candidates.length === 0) {
|
|
4773
|
-
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4774
|
-
}
|
|
4775
|
-
return candidates[0];
|
|
4892
|
+
return resolveStudioExeFromVersions(root);
|
|
4776
4893
|
}
|
|
4777
4894
|
async function resolveStudioExeAsync() {
|
|
4778
4895
|
if (process.env.ROBLOX_STUDIO_EXE)
|
|
@@ -4785,14 +4902,7 @@ async function resolveStudioExeAsync() {
|
|
|
4785
4902
|
}
|
|
4786
4903
|
const localAppData = await windowsLocalAppDataAsync();
|
|
4787
4904
|
const root = localAppData ? path2.join(await toWslPathAsync(localAppData), "Roblox", "Versions") : path2.join(os2.homedir(), "AppData", "Local", "Roblox", "Versions");
|
|
4788
|
-
|
|
4789
|
-
throw new Error(`Roblox Studio Versions folder not found: ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4790
|
-
}
|
|
4791
|
-
const candidates = readdirSync(root).filter((name) => name.startsWith("version-")).map((name) => path2.join(root, name, "RobloxStudioBeta.exe")).filter((candidate) => existsSync2(candidate)).sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
4792
|
-
if (candidates.length === 0) {
|
|
4793
|
-
throw new Error(`RobloxStudioBeta.exe not found under ${root}. Set ROBLOX_STUDIO_EXE.`);
|
|
4794
|
-
}
|
|
4795
|
-
return candidates[0];
|
|
4905
|
+
return resolveStudioExeFromVersions(root);
|
|
4796
4906
|
}
|
|
4797
4907
|
var WINDOWS_STUDIO_PROCESS_QUERY = [
|
|
4798
4908
|
"$ErrorActionPreference = 'Stop'",
|
|
@@ -4809,13 +4919,13 @@ function parseWindowsStudioProcesses(output) {
|
|
|
4809
4919
|
}
|
|
4810
4920
|
return processes;
|
|
4811
4921
|
}
|
|
4812
|
-
async function observeStudioProcesses() {
|
|
4922
|
+
async function observeStudioProcesses(timeoutMs = 15e3) {
|
|
4813
4923
|
const observedAt = Date.now();
|
|
4814
4924
|
try {
|
|
4815
4925
|
if (process.platform === "darwin") {
|
|
4816
4926
|
let out2 = "";
|
|
4817
4927
|
try {
|
|
4818
|
-
out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"]);
|
|
4928
|
+
out2 = await runAsync("pgrep", ["-fl", "RobloxStudio"], { timeout: timeoutMs });
|
|
4819
4929
|
} catch (error) {
|
|
4820
4930
|
const code = error.code;
|
|
4821
4931
|
if (Number(code) === 1)
|
|
@@ -4831,7 +4941,7 @@ async function observeStudioProcesses() {
|
|
|
4831
4941
|
if (process.platform !== "win32" && !isWsl()) {
|
|
4832
4942
|
return { status: "ok", observedAt, processes: [] };
|
|
4833
4943
|
}
|
|
4834
|
-
const out = await powershellAsync(WINDOWS_STUDIO_PROCESS_QUERY);
|
|
4944
|
+
const out = await powershellAsync(WINDOWS_STUDIO_PROCESS_QUERY, timeoutMs);
|
|
4835
4945
|
return { status: "ok", observedAt, processes: parseWindowsStudioProcesses(out) };
|
|
4836
4946
|
} catch (error) {
|
|
4837
4947
|
return {
|
|
@@ -4915,6 +5025,7 @@ var StudioInstanceManager = class {
|
|
|
4915
5025
|
confirmedExitGraceMs;
|
|
4916
5026
|
snapshotCacheMs;
|
|
4917
5027
|
launchCompletionTimeoutMs;
|
|
5028
|
+
closeTimeoutMs;
|
|
4918
5029
|
coordinatorTimer;
|
|
4919
5030
|
coordinatorRefresh;
|
|
4920
5031
|
cachedSnapshot;
|
|
@@ -4929,6 +5040,7 @@ var StudioInstanceManager = class {
|
|
|
4929
5040
|
this.confirmedExitGraceMs = options.confirmedExitGraceMs ?? 5e3;
|
|
4930
5041
|
this.snapshotCacheMs = options.snapshotCacheMs ?? 0;
|
|
4931
5042
|
this.launchCompletionTimeoutMs = options.launchCompletionTimeoutMs ?? LAUNCH_COMPLETION_TIMEOUT_MS;
|
|
5043
|
+
this.closeTimeoutMs = options.closeTimeoutMs ?? 1e4;
|
|
4932
5044
|
}
|
|
4933
5045
|
getLifecycleCapabilities() {
|
|
4934
5046
|
const platform = this.platformCapabilities ?? getStudioPlatformCapabilities();
|
|
@@ -5326,58 +5438,57 @@ var StudioInstanceManager = class {
|
|
|
5326
5438
|
instanceId: record.instanceId
|
|
5327
5439
|
};
|
|
5328
5440
|
}
|
|
5441
|
+
const deadline = Date.now() + this.closeTimeoutMs;
|
|
5329
5442
|
const processId = record.nativeProcessId ?? record.spawnPid;
|
|
5330
5443
|
if (!processId) {
|
|
5331
5444
|
throw new Error(`Cannot close ${record.instanceId ?? "Studio launch"} because its process id was not detected.`);
|
|
5332
5445
|
}
|
|
5333
5446
|
const control = record.recordId ? this.launchControls.get(record.recordId) : void 0;
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
this.
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
source: record.source,
|
|
5367
|
-
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
5368
|
-
action: "marked_closed_and_cleaned_baseplate"
|
|
5369
|
-
});
|
|
5370
|
-
return {
|
|
5371
|
-
status: "already_closed",
|
|
5372
|
-
launchId: record.recordId,
|
|
5373
|
-
instanceId: record.instanceId
|
|
5374
|
-
};
|
|
5447
|
+
const abort = record.processAuthorizationState !== "released" && control ? () => control.abort() : void 0;
|
|
5448
|
+
if (!abort) {
|
|
5449
|
+
let snapshot;
|
|
5450
|
+
try {
|
|
5451
|
+
snapshot = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, processId);
|
|
5452
|
+
} catch (error) {
|
|
5453
|
+
if (error instanceof StudioCloseVerificationError) {
|
|
5454
|
+
await this.applyProcessObservation(record, error.observation);
|
|
5455
|
+
}
|
|
5456
|
+
throw error;
|
|
5457
|
+
}
|
|
5458
|
+
const observation = this.observeRecord(record, snapshot);
|
|
5459
|
+
if (observation.status === "unknown") {
|
|
5460
|
+
await this.applyProcessObservation(record, observation);
|
|
5461
|
+
throw new Error(`Cannot verify the managed Studio process because process observation failed: ${observation.error}`);
|
|
5462
|
+
}
|
|
5463
|
+
if (observation.status === "not_running") {
|
|
5464
|
+
await this.markProcessExited(record, void 0, observation.reason === "identity_mismatch" ? "Studio process identity changed; the retained PID was not reused." : record.failureReason);
|
|
5465
|
+
await this.registry.logEvent({
|
|
5466
|
+
event: "registry_close_already_stopped",
|
|
5467
|
+
recordId: record.recordId,
|
|
5468
|
+
instanceId: record.instanceId,
|
|
5469
|
+
source: record.source,
|
|
5470
|
+
reason: observation.reason === "identity_mismatch" ? "identity_mismatch" : "pid_not_running",
|
|
5471
|
+
action: "marked_closed_and_cleaned_baseplate"
|
|
5472
|
+
});
|
|
5473
|
+
return {
|
|
5474
|
+
status: "already_closed",
|
|
5475
|
+
launchId: record.recordId,
|
|
5476
|
+
instanceId: record.instanceId
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5375
5479
|
}
|
|
5376
5480
|
try {
|
|
5377
|
-
await this.closeProcess(processId, record.nativeProcessStartedAt);
|
|
5481
|
+
await this.closeProcess(processId, record.nativeProcessStartedAt, abort, deadline);
|
|
5378
5482
|
} catch (error) {
|
|
5379
|
-
|
|
5483
|
+
if (error instanceof StudioCloseVerificationError) {
|
|
5484
|
+
await this.applyProcessObservation(record, error.observation);
|
|
5485
|
+
throw error;
|
|
5486
|
+
}
|
|
5487
|
+
if (abort)
|
|
5488
|
+
throw error;
|
|
5489
|
+
const retry = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, processId);
|
|
5380
5490
|
const retryObservation = this.observeRecord(record, retry);
|
|
5491
|
+
await this.applyProcessObservation(record, retryObservation);
|
|
5381
5492
|
if (retryObservation.status === "running" || retryObservation.status === "unknown")
|
|
5382
5493
|
throw error;
|
|
5383
5494
|
await this.registry.logEvent({
|
|
@@ -5414,7 +5525,8 @@ var StudioInstanceManager = class {
|
|
|
5414
5525
|
};
|
|
5415
5526
|
}
|
|
5416
5527
|
async closeConnectedInstance(instance) {
|
|
5417
|
-
const
|
|
5528
|
+
const deadline = Date.now() + this.closeTimeoutMs;
|
|
5529
|
+
const snapshot = await beforeCloseDeadline(() => this.readProcessSnapshot(deadline - Date.now()), deadline, 0);
|
|
5418
5530
|
if (snapshot.status === "error") {
|
|
5419
5531
|
throw new Error(`Could not enumerate Studio processes: ${snapshot.error}`);
|
|
5420
5532
|
}
|
|
@@ -5422,27 +5534,65 @@ var StudioInstanceManager = class {
|
|
|
5422
5534
|
if (!process2) {
|
|
5423
5535
|
throw new Error(`Could not find a Studio process for connected instance "${instance.instanceId}".`);
|
|
5424
5536
|
}
|
|
5425
|
-
await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime);
|
|
5537
|
+
await this.closeProcess(process2.Id, process2.StartTimeUtcFileTime, void 0, deadline);
|
|
5426
5538
|
}
|
|
5427
|
-
async closeProcess(processId, startedAt) {
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5539
|
+
async closeProcess(processId, startedAt, stop, deadline = Date.now() + this.closeTimeoutMs) {
|
|
5540
|
+
this.cachedSnapshot = void 0;
|
|
5541
|
+
await beforeCloseDeadline(async () => {
|
|
5542
|
+
if (stop) {
|
|
5543
|
+
await stop();
|
|
5544
|
+
} else if (this.processAdapter.stopProcess) {
|
|
5545
|
+
await this.processAdapter.stopProcess(processId, startedAt);
|
|
5546
|
+
} else if (process.platform === "win32" || isWsl()) {
|
|
5547
|
+
if (startedAt === void 0) {
|
|
5548
|
+
throw new Error("Cannot stop a Windows Studio process without its creation-time identity.");
|
|
5549
|
+
}
|
|
5550
|
+
await stopWindowsStudio(processId, startedAt, Math.max(1, deadline - Date.now()));
|
|
5551
|
+
} else {
|
|
5552
|
+
try {
|
|
5553
|
+
process.kill(processId, "SIGTERM");
|
|
5554
|
+
} catch (error) {
|
|
5555
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ESRCH")
|
|
5556
|
+
throw error;
|
|
5557
|
+
}
|
|
5435
5558
|
}
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5559
|
+
}, deadline, processId);
|
|
5560
|
+
while (true) {
|
|
5561
|
+
const observation = await beforeCloseDeadline(() => this.observeClosingProcess(processId, startedAt, deadline), deadline, processId);
|
|
5562
|
+
if (observation.status === "not_running")
|
|
5563
|
+
return;
|
|
5564
|
+
if (observation.status === "unknown") {
|
|
5565
|
+
throw new StudioCloseVerificationError(`Could not verify Studio process ${processId} termination: ${observation.error}`, observation);
|
|
5566
|
+
}
|
|
5567
|
+
const remaining = deadline - Date.now();
|
|
5568
|
+
if (remaining <= 0) {
|
|
5569
|
+
throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
|
|
5570
|
+
}
|
|
5571
|
+
await delay2(Math.min(100, remaining));
|
|
5572
|
+
if (Date.now() >= deadline) {
|
|
5573
|
+
throw new StudioCloseVerificationError(`Timed out closing Studio process ${processId}: the process is still running. Close can be retried.`, observation);
|
|
5443
5574
|
}
|
|
5444
5575
|
}
|
|
5445
5576
|
}
|
|
5577
|
+
async observeClosingProcess(processId, startedAt, deadline) {
|
|
5578
|
+
if (process.platform === "darwin" && !this.processAdapter.observeStudioProcesses && !this.processAdapter.listStudioProcesses) {
|
|
5579
|
+
return observePosixProcess(processId);
|
|
5580
|
+
}
|
|
5581
|
+
const snapshot = await this.readProcessSnapshot(Math.max(1, deadline - Date.now()));
|
|
5582
|
+
if (snapshot.status === "error") {
|
|
5583
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: snapshot.error };
|
|
5584
|
+
}
|
|
5585
|
+
const target = snapshot.processes.find((candidate) => candidate.Id === processId);
|
|
5586
|
+
if (!target)
|
|
5587
|
+
return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
|
|
5588
|
+
if (startedAt !== void 0 && target.StartTimeUtcFileTime !== startedAt) {
|
|
5589
|
+
if (target.StartTimeUtcFileTime === void 0) {
|
|
5590
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: "Process creation-time identity is unavailable." };
|
|
5591
|
+
}
|
|
5592
|
+
return { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
|
|
5593
|
+
}
|
|
5594
|
+
return { status: "running", observedAt: snapshot.observedAt };
|
|
5595
|
+
}
|
|
5446
5596
|
findProcessForConnectedInstance(instance, processes) {
|
|
5447
5597
|
if (processes.length === 0)
|
|
5448
5598
|
return void 0;
|
|
@@ -5471,24 +5621,7 @@ var StudioInstanceManager = class {
|
|
|
5471
5621
|
return this.snapshotInFlight;
|
|
5472
5622
|
this.snapshotInFlight = (async () => {
|
|
5473
5623
|
try {
|
|
5474
|
-
|
|
5475
|
-
if (this.processAdapter.observeStudioProcesses) {
|
|
5476
|
-
snapshot = await this.processAdapter.observeStudioProcesses();
|
|
5477
|
-
} else if (this.processAdapter.listStudioProcesses) {
|
|
5478
|
-
const observedAt = Date.now();
|
|
5479
|
-
const processes = await this.processAdapter.listStudioProcesses();
|
|
5480
|
-
snapshot = { status: "ok", observedAt, processes };
|
|
5481
|
-
} else {
|
|
5482
|
-
snapshot = await observeStudioProcesses();
|
|
5483
|
-
}
|
|
5484
|
-
this.cachedSnapshot = snapshot;
|
|
5485
|
-
return snapshot;
|
|
5486
|
-
} catch (error) {
|
|
5487
|
-
const snapshot = {
|
|
5488
|
-
status: "error",
|
|
5489
|
-
observedAt: Date.now(),
|
|
5490
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5491
|
-
};
|
|
5624
|
+
const snapshot = await this.readProcessSnapshot();
|
|
5492
5625
|
this.cachedSnapshot = snapshot;
|
|
5493
5626
|
return snapshot;
|
|
5494
5627
|
} finally {
|
|
@@ -5497,6 +5630,25 @@ var StudioInstanceManager = class {
|
|
|
5497
5630
|
})();
|
|
5498
5631
|
return this.snapshotInFlight;
|
|
5499
5632
|
}
|
|
5633
|
+
async readProcessSnapshot(timeoutMs = 15e3) {
|
|
5634
|
+
try {
|
|
5635
|
+
if (this.processAdapter.observeStudioProcesses) {
|
|
5636
|
+
return await this.processAdapter.observeStudioProcesses();
|
|
5637
|
+
}
|
|
5638
|
+
if (this.processAdapter.listStudioProcesses) {
|
|
5639
|
+
const observedAt = Date.now();
|
|
5640
|
+
const processes = await this.processAdapter.listStudioProcesses();
|
|
5641
|
+
return { status: "ok", observedAt, processes };
|
|
5642
|
+
}
|
|
5643
|
+
return await observeStudioProcesses(Math.max(1, timeoutMs));
|
|
5644
|
+
} catch (error) {
|
|
5645
|
+
return {
|
|
5646
|
+
status: "error",
|
|
5647
|
+
observedAt: Date.now(),
|
|
5648
|
+
error: error instanceof Error ? error.message : String(error)
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5500
5652
|
async getCurrentBootId() {
|
|
5501
5653
|
return this.processAdapter.currentBootId ? await this.processAdapter.currentBootId() : currentBootIdAsync();
|
|
5502
5654
|
}
|
|
@@ -5562,8 +5714,16 @@ var StudioInstanceManager = class {
|
|
|
5562
5714
|
if (!processId)
|
|
5563
5715
|
return { status: "running", observedAt: snapshot.observedAt };
|
|
5564
5716
|
const studioProcess = snapshot.processes.find((candidate) => candidate.Id === processId);
|
|
5565
|
-
if (!studioProcess)
|
|
5717
|
+
if (!studioProcess) {
|
|
5718
|
+
if (process.platform === "darwin" && !this.processAdapter.observeStudioProcesses && !this.processAdapter.listStudioProcesses) {
|
|
5719
|
+
const observation = observePosixProcess(processId);
|
|
5720
|
+
return observation.status === "running" ? { status: "unknown", observedAt: observation.observedAt, error: `Studio PID ${processId} is still alive but missing from process enumeration.` } : observation;
|
|
5721
|
+
}
|
|
5566
5722
|
return { status: "not_running", observedAt: snapshot.observedAt, reason: "missing" };
|
|
5723
|
+
}
|
|
5724
|
+
if (record.nativeProcessStartedAt !== void 0 && studioProcess.StartTimeUtcFileTime === void 0) {
|
|
5725
|
+
return { status: "unknown", observedAt: snapshot.observedAt, error: "Process creation-time identity is unavailable." };
|
|
5726
|
+
}
|
|
5567
5727
|
return this.verifyProcessForRecord(record, studioProcess) ? { status: "running", observedAt: snapshot.observedAt } : { status: "not_running", observedAt: snapshot.observedAt, reason: "identity_mismatch" };
|
|
5568
5728
|
}
|
|
5569
5729
|
verifyProcessForRecord(record, studioProcess) {
|
|
@@ -6488,7 +6648,7 @@ var TOOL_PROXY_ENDPOINTS = {
|
|
|
6488
6648
|
insert_asset: ["/api/insert-asset"],
|
|
6489
6649
|
generate_model: ["/api/generate-model"],
|
|
6490
6650
|
preview_asset: ["/api/preview-asset"],
|
|
6491
|
-
capture_screenshot: ["/api/capture-studio", "/api/capture-screenshot", "/api/capture-begin", "/api/capture-read"],
|
|
6651
|
+
capture_screenshot: ["/api/capture-studio", "/api/capture-screenshot", "/api/capture-begin", "/api/capture-read", "/api/capture-markers"],
|
|
6492
6652
|
simulate_mouse_input: ["/api/simulate-mouse-input"],
|
|
6493
6653
|
simulate_keyboard_input: ["/api/simulate-keyboard-input"],
|
|
6494
6654
|
get_memory_breakdown: ["/api/get-memory-breakdown"],
|
|
@@ -6830,8 +6990,8 @@ function createHttpServer(tools, bridge, allowedTools, serverConfig, security) {
|
|
|
6830
6990
|
});
|
|
6831
6991
|
const authToken = security?.authToken;
|
|
6832
6992
|
const authRequired = (requestPath) => {
|
|
6833
|
-
const
|
|
6834
|
-
return
|
|
6993
|
+
const path7 = requestPath.toLowerCase().replace(/\/+$/, "");
|
|
6994
|
+
return path7 === "/mcp" || path7.startsWith("/mcp/") || path7 === "/proxy" || path7 === "/topology" || path7 === "/request-status" || path7 === "/unregister-instance-id" || path7 === "/create-multiplayer-group" || path7 === "/remove-multiplayer-group";
|
|
6835
6995
|
};
|
|
6836
6996
|
app.use((req, res, next) => {
|
|
6837
6997
|
if (!authToken || !authRequired(req.path)) {
|
|
@@ -7824,12 +7984,10 @@ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
|
7824
7984
|
var MAX_EDITABLE_IMAGE_DIMENSION = 1024;
|
|
7825
7985
|
var MAX_PNG_DIMENSION = 16 * 1024;
|
|
7826
7986
|
var MAX_PNG_PIXELS = 8 * 1024 * 1024;
|
|
7987
|
+
var MAX_SCREENSHOT_PNG_PIXELS = 32 * 1024 * 1024;
|
|
7827
7988
|
var MAX_PNG_INPUT_BYTES = 32 * 1024 * 1024;
|
|
7828
7989
|
var MAX_PNG_BASE64_CHARACTERS = 4 * Math.ceil(MAX_PNG_INPUT_BYTES / 3);
|
|
7829
|
-
var MAX_PNG_CHUNK_BYTES = 32 * 1024 * 1024;
|
|
7830
|
-
var MAX_PNG_COMPRESSED_BYTES = 32 * 1024 * 1024;
|
|
7831
7990
|
var MAX_PNG_CHUNKS = 65536;
|
|
7832
|
-
var MAX_PNG_INFLATED_BYTES = MAX_PNG_PIXELS * 4 + MAX_PNG_DIMENSION;
|
|
7833
7991
|
function paethPredictor(a, b, c) {
|
|
7834
7992
|
const p = a + b - c;
|
|
7835
7993
|
const pa = Math.abs(p - a);
|
|
@@ -7852,12 +8010,12 @@ function bytesPerPixel(colorType) {
|
|
|
7852
8010
|
return 2;
|
|
7853
8011
|
throw new Error(`Unsupported PNG color type ${colorType}. Supported color types: 0, 2, 4, 6.`);
|
|
7854
8012
|
}
|
|
7855
|
-
function validatePngInputSize(byteLength) {
|
|
8013
|
+
function validatePngInputSize(byteLength, maxInputBytes = MAX_PNG_INPUT_BYTES) {
|
|
7856
8014
|
if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
|
|
7857
8015
|
throw new Error(`Invalid PNG input length ${byteLength}.`);
|
|
7858
8016
|
}
|
|
7859
|
-
if (byteLength >
|
|
7860
|
-
throw new Error(`PNG input length ${byteLength} exceeds the ${
|
|
8017
|
+
if (byteLength > maxInputBytes) {
|
|
8018
|
+
throw new Error(`PNG input length ${byteLength} exceeds the ${maxInputBytes}-byte limit.`);
|
|
7861
8019
|
}
|
|
7862
8020
|
}
|
|
7863
8021
|
function getBase64DecodedLength(encoded) {
|
|
@@ -7919,7 +8077,7 @@ function readPngFileWithinLimit(resolved, imagePath) {
|
|
|
7919
8077
|
fs2.closeSync(descriptor);
|
|
7920
8078
|
}
|
|
7921
8079
|
}
|
|
7922
|
-
function validateSourceDimensions(width, height) {
|
|
8080
|
+
function validateSourceDimensions(width, height, maxPixels) {
|
|
7923
8081
|
if (width <= 0 || height <= 0) {
|
|
7924
8082
|
throw new Error("PNG IHDR dimensions must be positive.");
|
|
7925
8083
|
}
|
|
@@ -7927,78 +8085,65 @@ function validateSourceDimensions(width, height) {
|
|
|
7927
8085
|
throw new Error(`PNG dimensions ${width}x${height} exceed the ${MAX_PNG_DIMENSION}-pixel dimension limit.`);
|
|
7928
8086
|
}
|
|
7929
8087
|
const pixelCount = width * height;
|
|
7930
|
-
if (!Number.isSafeInteger(pixelCount) || pixelCount >
|
|
7931
|
-
throw new Error(`PNG pixel count ${pixelCount} exceeds the ${
|
|
8088
|
+
if (!Number.isSafeInteger(pixelCount) || pixelCount > maxPixels) {
|
|
8089
|
+
throw new Error(`PNG pixel count ${pixelCount} exceeds the ${maxPixels}-pixel limit.`);
|
|
7932
8090
|
}
|
|
7933
8091
|
}
|
|
7934
|
-
function getExpectedScanlineLength(width, height, bpp) {
|
|
8092
|
+
function getExpectedScanlineLength(width, height, bpp, maxPixels) {
|
|
8093
|
+
const maxInflatedBytes = maxPixels * 4 + MAX_PNG_DIMENSION;
|
|
7935
8094
|
const stride = width * bpp;
|
|
7936
8095
|
const expected = height * (stride + 1);
|
|
7937
|
-
if (!Number.isSafeInteger(stride) || !Number.isSafeInteger(expected) || expected >
|
|
7938
|
-
throw new Error(`PNG scanline data length ${expected} exceeds the ${
|
|
8096
|
+
if (!Number.isSafeInteger(stride) || !Number.isSafeInteger(expected) || expected > maxInflatedBytes) {
|
|
8097
|
+
throw new Error(`PNG scanline data length ${expected} exceeds the ${maxInflatedBytes}-byte limit.`);
|
|
7939
8098
|
}
|
|
7940
8099
|
return expected;
|
|
7941
8100
|
}
|
|
7942
|
-
function
|
|
7943
|
-
const
|
|
7944
|
-
if (
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
const upLeft = y > 0 && x >= bpp ? unfiltered[prevRowOffset + x - bpp] : 0;
|
|
7961
|
-
let decoded;
|
|
7962
|
-
if (filter === 0) {
|
|
7963
|
-
decoded = value;
|
|
7964
|
-
} else if (filter === 1) {
|
|
7965
|
-
decoded = value + left;
|
|
7966
|
-
} else if (filter === 2) {
|
|
7967
|
-
decoded = value + up;
|
|
7968
|
-
} else if (filter === 3) {
|
|
7969
|
-
decoded = value + Math.floor((left + up) / 2);
|
|
7970
|
-
} else if (filter === 4) {
|
|
7971
|
-
decoded = value + paethPredictor(left, up, upLeft);
|
|
7972
|
-
} else {
|
|
7973
|
-
throw new Error(`Unsupported PNG filter type ${filter}.`);
|
|
7974
|
-
}
|
|
7975
|
-
unfiltered[rowOffset + x] = decoded & 255;
|
|
8101
|
+
function unfilterPngRow(raw, rowOffset, stride, bpp) {
|
|
8102
|
+
const filter = raw[rowOffset - 1];
|
|
8103
|
+
if (filter === 0)
|
|
8104
|
+
return;
|
|
8105
|
+
const prevRowOffset = rowOffset - stride - 1;
|
|
8106
|
+
for (let x = 0; x < stride; x++) {
|
|
8107
|
+
const left = x >= bpp ? raw[rowOffset + x - bpp] : 0;
|
|
8108
|
+
const up = prevRowOffset > 0 ? raw[prevRowOffset + x] : 0;
|
|
8109
|
+
const upLeft = prevRowOffset > 0 && x >= bpp ? raw[prevRowOffset + x - bpp] : 0;
|
|
8110
|
+
let predictor;
|
|
8111
|
+
if (filter === 1) {
|
|
8112
|
+
predictor = left;
|
|
8113
|
+
} else if (filter === 2) {
|
|
8114
|
+
predictor = up;
|
|
8115
|
+
} else if (filter === 3) {
|
|
8116
|
+
predictor = Math.floor((left + up) / 2);
|
|
8117
|
+
} else {
|
|
8118
|
+
predictor = paethPredictor(left, up, upLeft);
|
|
7976
8119
|
}
|
|
8120
|
+
raw[rowOffset + x] = raw[rowOffset + x] + predictor & 255;
|
|
7977
8121
|
}
|
|
7978
|
-
|
|
7979
|
-
|
|
8122
|
+
}
|
|
8123
|
+
function convertScanlinesToRgba({ raw, width, height, colorType, bpp }) {
|
|
8124
|
+
const stride = width * bpp;
|
|
7980
8125
|
const rgba = Buffer.alloc(width * height * 4);
|
|
7981
|
-
let si = 0;
|
|
7982
8126
|
let di = 0;
|
|
7983
|
-
for (let
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
rgba
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
}
|
|
7990
|
-
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8127
|
+
for (let rowOffset = 1; rowOffset < raw.length; rowOffset += stride + 1) {
|
|
8128
|
+
unfilterPngRow(raw, rowOffset, stride, bpp);
|
|
8129
|
+
if (colorType === 6) {
|
|
8130
|
+
raw.copy(rgba, di, rowOffset, rowOffset + stride);
|
|
8131
|
+
di += stride;
|
|
8132
|
+
continue;
|
|
8133
|
+
}
|
|
8134
|
+
for (let si = rowOffset; si < rowOffset + stride; si += bpp) {
|
|
8135
|
+
if (colorType === 2) {
|
|
8136
|
+
rgba[di++] = raw[si];
|
|
8137
|
+
rgba[di++] = raw[si + 1];
|
|
8138
|
+
rgba[di++] = raw[si + 2];
|
|
8139
|
+
rgba[di++] = 255;
|
|
8140
|
+
} else {
|
|
8141
|
+
const gray = raw[si];
|
|
8142
|
+
rgba[di++] = gray;
|
|
8143
|
+
rgba[di++] = gray;
|
|
8144
|
+
rgba[di++] = gray;
|
|
8145
|
+
rgba[di++] = colorType === 4 ? raw[si + 1] : 255;
|
|
8146
|
+
}
|
|
8002
8147
|
}
|
|
8003
8148
|
}
|
|
8004
8149
|
return rgba;
|
|
@@ -8025,8 +8170,9 @@ function resizeRgbaNearest(image) {
|
|
|
8025
8170
|
}
|
|
8026
8171
|
return { width, height, rgba };
|
|
8027
8172
|
}
|
|
8028
|
-
function
|
|
8029
|
-
|
|
8173
|
+
function inflatePng(data, maxPixels) {
|
|
8174
|
+
const maxInputBytes = maxPixels * 4;
|
|
8175
|
+
validatePngInputSize(data.length, maxInputBytes);
|
|
8030
8176
|
if (data.length < PNG_SIGNATURE.length || !data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) {
|
|
8031
8177
|
throw new Error("Unsupported image format. generate_model currently supports PNG images.");
|
|
8032
8178
|
}
|
|
@@ -8047,8 +8193,8 @@ function decodePngToRgba(data) {
|
|
|
8047
8193
|
}
|
|
8048
8194
|
const length = data.readUInt32BE(offset);
|
|
8049
8195
|
const type = data.toString("ascii", offset + 4, offset + 8);
|
|
8050
|
-
if (length >
|
|
8051
|
-
throw new Error(`PNG chunk ${type} length ${length} exceeds the ${
|
|
8196
|
+
if (length > maxInputBytes) {
|
|
8197
|
+
throw new Error(`PNG chunk ${type} length ${length} exceeds the ${maxInputBytes}-byte chunk limit.`);
|
|
8052
8198
|
}
|
|
8053
8199
|
if (++chunkCount > MAX_PNG_CHUNKS) {
|
|
8054
8200
|
throw new Error(`PNG contains more than the ${MAX_PNG_CHUNKS}-chunk limit.`);
|
|
@@ -8084,12 +8230,12 @@ function decodePngToRgba(data) {
|
|
|
8084
8230
|
if (interlace !== 0)
|
|
8085
8231
|
throw new Error("Interlaced PNG images are not supported.");
|
|
8086
8232
|
bpp = bytesPerPixel(colorType);
|
|
8087
|
-
validateSourceDimensions(width, height);
|
|
8088
|
-
expectedScanlineLength = getExpectedScanlineLength(width, height, bpp);
|
|
8233
|
+
validateSourceDimensions(width, height, maxPixels);
|
|
8234
|
+
expectedScanlineLength = getExpectedScanlineLength(width, height, bpp, maxPixels);
|
|
8089
8235
|
sawIhdr = true;
|
|
8090
8236
|
} else if (type === "IDAT") {
|
|
8091
|
-
if (length >
|
|
8092
|
-
throw new Error(`PNG image data exceeds the ${
|
|
8237
|
+
if (length > maxInputBytes - idatLength) {
|
|
8238
|
+
throw new Error(`PNG image data exceeds the ${maxInputBytes}-byte compressed-data limit.`);
|
|
8093
8239
|
}
|
|
8094
8240
|
idatLength += length;
|
|
8095
8241
|
idatChunks.push(chunk);
|
|
@@ -8117,8 +8263,43 @@ function decodePngToRgba(data) {
|
|
|
8117
8263
|
const detail = error instanceof Error ? error.message : String(error);
|
|
8118
8264
|
throw new Error(`Invalid PNG compressed data: ${detail}`);
|
|
8119
8265
|
}
|
|
8120
|
-
|
|
8121
|
-
|
|
8266
|
+
if (raw.length !== expectedScanlineLength) {
|
|
8267
|
+
throw new Error(`PNG data ended early. Expected ${expectedScanlineLength} bytes after inflate, got ${raw.length}.`);
|
|
8268
|
+
}
|
|
8269
|
+
const rowLength = width * bpp + 1;
|
|
8270
|
+
for (let rowOffset = 0; rowOffset < raw.length; rowOffset += rowLength) {
|
|
8271
|
+
if (raw[rowOffset] > 4) {
|
|
8272
|
+
throw new Error(`Unsupported PNG filter type ${raw[rowOffset]}.`);
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
return { width, height, colorType, bpp, raw };
|
|
8276
|
+
}
|
|
8277
|
+
function isUniformPng(data) {
|
|
8278
|
+
const { raw, width, colorType, bpp } = inflatePng(data, MAX_SCREENSHOT_PNG_PIXELS);
|
|
8279
|
+
const stride = width * bpp;
|
|
8280
|
+
unfilterPngRow(raw, 1, stride, bpp);
|
|
8281
|
+
const red = raw[1];
|
|
8282
|
+
const grayscale = colorType === 0 || colorType === 4;
|
|
8283
|
+
const green = grayscale ? red : raw[2];
|
|
8284
|
+
const blue = grayscale ? red : raw[3];
|
|
8285
|
+
for (let rowOffset = 1; rowOffset < raw.length; rowOffset += stride + 1) {
|
|
8286
|
+
if (rowOffset !== 1)
|
|
8287
|
+
unfilterPngRow(raw, rowOffset, stride, bpp);
|
|
8288
|
+
for (let offset = rowOffset; offset < rowOffset + stride; offset += bpp) {
|
|
8289
|
+
if (raw[offset] !== red || !grayscale && (raw[offset + 1] !== green || raw[offset + 2] !== blue)) {
|
|
8290
|
+
return false;
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
}
|
|
8294
|
+
return true;
|
|
8295
|
+
}
|
|
8296
|
+
function decodePngToRgba(data) {
|
|
8297
|
+
const png = inflatePng(data, MAX_PNG_PIXELS);
|
|
8298
|
+
return resizeRgbaNearest({
|
|
8299
|
+
width: png.width,
|
|
8300
|
+
height: png.height,
|
|
8301
|
+
rgba: convertScanlinesToRgba(png)
|
|
8302
|
+
});
|
|
8122
8303
|
}
|
|
8123
8304
|
function decodePngBase64ToRgba(encoded) {
|
|
8124
8305
|
const expectedLength = getBase64DecodedLength(encoded);
|
|
@@ -10102,9 +10283,308 @@ function rgbaToPng(rgba, width, height) {
|
|
|
10102
10283
|
]);
|
|
10103
10284
|
}
|
|
10104
10285
|
|
|
10105
|
-
// ../core/dist/
|
|
10286
|
+
// ../core/dist/host-capture.js
|
|
10287
|
+
import { spawn as spawn2 } from "child_process";
|
|
10106
10288
|
import * as fs3 from "fs";
|
|
10289
|
+
import * as os3 from "os";
|
|
10107
10290
|
import * as path5 from "path";
|
|
10291
|
+
var HOST_CAPTURE_TIMEOUT_MS = 2e4;
|
|
10292
|
+
var MARKER_MIN_RB = 200;
|
|
10293
|
+
var MARKER_MAX_G = 60;
|
|
10294
|
+
var MARKER_SCALE_TOLERANCE = 0.03;
|
|
10295
|
+
function isHostCaptureDisabled(env = process.env) {
|
|
10296
|
+
const value = (env.ROBLOX_STUDIO_HOST_CAPTURE ?? "").trim().toLowerCase();
|
|
10297
|
+
return value === "0" || value === "false" || value === "off";
|
|
10298
|
+
}
|
|
10299
|
+
function isHostCaptureSupported(platform = process.platform) {
|
|
10300
|
+
return platform === "win32";
|
|
10301
|
+
}
|
|
10302
|
+
function hostCaptureUnsupportedReason(platform = process.platform) {
|
|
10303
|
+
return `host window capture is only implemented on Windows (this is ${platform})`;
|
|
10304
|
+
}
|
|
10305
|
+
function isUniformFrame(rgba, width, height) {
|
|
10306
|
+
const pixels = width * height;
|
|
10307
|
+
if (pixels <= 0 || rgba.length < pixels * 4)
|
|
10308
|
+
return false;
|
|
10309
|
+
const r = rgba[0];
|
|
10310
|
+
const g = rgba[1];
|
|
10311
|
+
const b = rgba[2];
|
|
10312
|
+
for (let i = 0; i < pixels; i++) {
|
|
10313
|
+
const o = i * 4;
|
|
10314
|
+
if (rgba[o] !== r || rgba[o + 1] !== g || rgba[o + 2] !== b)
|
|
10315
|
+
return false;
|
|
10316
|
+
}
|
|
10317
|
+
return true;
|
|
10318
|
+
}
|
|
10319
|
+
function isMarkerPixel(rgba, offset) {
|
|
10320
|
+
return rgba[offset] >= MARKER_MIN_RB && rgba[offset + 1] <= MARKER_MAX_G && rgba[offset + 2] >= MARKER_MIN_RB;
|
|
10321
|
+
}
|
|
10322
|
+
function findViewportRect(rgba, width, height, hint) {
|
|
10323
|
+
let minX = width;
|
|
10324
|
+
let minY = height;
|
|
10325
|
+
let maxX = -1;
|
|
10326
|
+
let maxY = -1;
|
|
10327
|
+
for (let y = 0; y < height; y++) {
|
|
10328
|
+
const row = y * width * 4;
|
|
10329
|
+
for (let x = 0; x < width; x++) {
|
|
10330
|
+
if (!isMarkerPixel(rgba, row + x * 4))
|
|
10331
|
+
continue;
|
|
10332
|
+
if (x < minX)
|
|
10333
|
+
minX = x;
|
|
10334
|
+
if (x > maxX)
|
|
10335
|
+
maxX = x;
|
|
10336
|
+
if (y < minY)
|
|
10337
|
+
minY = y;
|
|
10338
|
+
if (y > maxY)
|
|
10339
|
+
maxY = y;
|
|
10340
|
+
}
|
|
10341
|
+
}
|
|
10342
|
+
if (maxX < 0) {
|
|
10343
|
+
return { error: "no viewport markers were visible in the Studio window capture" };
|
|
10344
|
+
}
|
|
10345
|
+
const rect = { x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1 };
|
|
10346
|
+
const inset = Math.max(1, Math.floor(hint.markerSize / 4));
|
|
10347
|
+
const corners = [
|
|
10348
|
+
[minX + inset, minY + inset],
|
|
10349
|
+
[maxX - inset, minY + inset],
|
|
10350
|
+
[minX + inset, maxY - inset],
|
|
10351
|
+
[maxX - inset, maxY - inset]
|
|
10352
|
+
];
|
|
10353
|
+
for (const [x, y] of corners) {
|
|
10354
|
+
if (!isMarkerPixel(rgba, (y * width + x) * 4)) {
|
|
10355
|
+
return { error: `viewport markers were found but do not form a rectangle (box ${rect.width}x${rect.height} at ${rect.x},${rect.y})` };
|
|
10356
|
+
}
|
|
10357
|
+
}
|
|
10358
|
+
const scaleX = rect.width / hint.viewportWidth;
|
|
10359
|
+
const scaleY = rect.height / hint.viewportHeight;
|
|
10360
|
+
if (Math.abs(scaleX - scaleY) > MARKER_SCALE_TOLERANCE * Math.max(scaleX, scaleY)) {
|
|
10361
|
+
return {
|
|
10362
|
+
error: `viewport marker box ${rect.width}x${rect.height} does not match the reported ${hint.viewportWidth}x${hint.viewportHeight} viewport aspect`
|
|
10363
|
+
};
|
|
10364
|
+
}
|
|
10365
|
+
return { rect };
|
|
10366
|
+
}
|
|
10367
|
+
function cropToViewport(rgba, width, height, rect, targetWidth, targetHeight) {
|
|
10368
|
+
const x0 = Math.max(0, Math.min(width - 1, rect.x));
|
|
10369
|
+
const y0 = Math.max(0, Math.min(height - 1, rect.y));
|
|
10370
|
+
const srcW = Math.max(1, Math.min(rect.width, width - x0));
|
|
10371
|
+
const srcH = Math.max(1, Math.min(rect.height, height - y0));
|
|
10372
|
+
const exact = srcW === targetWidth && srcH === targetHeight;
|
|
10373
|
+
const outW = targetWidth;
|
|
10374
|
+
const outH = targetHeight;
|
|
10375
|
+
const out = Buffer.alloc(outW * outH * 4);
|
|
10376
|
+
if (exact) {
|
|
10377
|
+
for (let y = 0; y < outH; y++) {
|
|
10378
|
+
rgba.copy(out, y * outW * 4, ((y0 + y) * width + x0) * 4, ((y0 + y) * width + x0 + outW) * 4);
|
|
10379
|
+
}
|
|
10380
|
+
return { width: outW, height: outH, rgba: out };
|
|
10381
|
+
}
|
|
10382
|
+
const sx = srcW / outW;
|
|
10383
|
+
const sy = srcH / outH;
|
|
10384
|
+
for (let y = 0; y < outH; y++) {
|
|
10385
|
+
const fy = Math.min(srcH - 1, (y + 0.5) * sy - 0.5);
|
|
10386
|
+
const iy = Math.max(0, Math.floor(fy));
|
|
10387
|
+
const ny = Math.min(srcH - 1, iy + 1);
|
|
10388
|
+
const wy = Math.max(0, fy - iy);
|
|
10389
|
+
for (let x = 0; x < outW; x++) {
|
|
10390
|
+
const fx = Math.min(srcW - 1, (x + 0.5) * sx - 0.5);
|
|
10391
|
+
const ix = Math.max(0, Math.floor(fx));
|
|
10392
|
+
const nx = Math.min(srcW - 1, ix + 1);
|
|
10393
|
+
const wx = Math.max(0, fx - ix);
|
|
10394
|
+
const o00 = ((y0 + iy) * width + x0 + ix) * 4;
|
|
10395
|
+
const o10 = ((y0 + iy) * width + x0 + nx) * 4;
|
|
10396
|
+
const o01 = ((y0 + ny) * width + x0 + ix) * 4;
|
|
10397
|
+
const o11 = ((y0 + ny) * width + x0 + nx) * 4;
|
|
10398
|
+
const dst = (y * outW + x) * 4;
|
|
10399
|
+
for (let c = 0; c < 4; c++) {
|
|
10400
|
+
const top = rgba[o00 + c] * (1 - wx) + rgba[o10 + c] * wx;
|
|
10401
|
+
const bottom = rgba[o01 + c] * (1 - wx) + rgba[o11 + c] * wx;
|
|
10402
|
+
out[dst + c] = Math.round(top * (1 - wy) + bottom * wy);
|
|
10403
|
+
}
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10406
|
+
return { width: outW, height: outH, rgba: out };
|
|
10407
|
+
}
|
|
10408
|
+
var WINDOWS_CAPTURE_SCRIPT = String.raw`
|
|
10409
|
+
$ErrorActionPreference = 'Stop'
|
|
10410
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
10411
|
+
Add-Type -AssemblyName System.Drawing
|
|
10412
|
+
Add-Type -TypeDefinition @"
|
|
10413
|
+
using System;
|
|
10414
|
+
using System.Collections.Generic;
|
|
10415
|
+
using System.Runtime.InteropServices;
|
|
10416
|
+
using System.Text;
|
|
10417
|
+
public static class McpStudioCapture {
|
|
10418
|
+
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc cb, IntPtr lParam);
|
|
10419
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hWnd);
|
|
10420
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr hWnd);
|
|
10421
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
|
|
10422
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowTextW(IntPtr hWnd, StringBuilder text, int count);
|
|
10423
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowTextLengthW(IntPtr hWnd);
|
|
10424
|
+
[DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr hWnd, out RECT rect);
|
|
10425
|
+
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdc, uint flags);
|
|
10426
|
+
delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
|
|
10427
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
|
10428
|
+
public class Candidate { public IntPtr Handle; public uint Pid; public string Title; }
|
|
10429
|
+
public static List<Candidate> Find(string processName) {
|
|
10430
|
+
var found = new List<Candidate>();
|
|
10431
|
+
EnumWindows((hWnd, lParam) => {
|
|
10432
|
+
if (!IsWindowVisible(hWnd)) return true;
|
|
10433
|
+
int length = GetWindowTextLengthW(hWnd);
|
|
10434
|
+
if (length <= 0) return true;
|
|
10435
|
+
var sb = new StringBuilder(length + 1);
|
|
10436
|
+
GetWindowTextW(hWnd, sb, sb.Capacity);
|
|
10437
|
+
string title = sb.ToString();
|
|
10438
|
+
if (!title.EndsWith("Roblox Studio", StringComparison.Ordinal)) return true;
|
|
10439
|
+
uint pid;
|
|
10440
|
+
GetWindowThreadProcessId(hWnd, out pid);
|
|
10441
|
+
try {
|
|
10442
|
+
var proc = System.Diagnostics.Process.GetProcessById((int)pid);
|
|
10443
|
+
if (!string.Equals(proc.ProcessName, processName, StringComparison.OrdinalIgnoreCase)) return true;
|
|
10444
|
+
} catch { return true; }
|
|
10445
|
+
found.Add(new Candidate { Handle = hWnd, Pid = pid, Title = title });
|
|
10446
|
+
return true;
|
|
10447
|
+
}, IntPtr.Zero);
|
|
10448
|
+
return found;
|
|
10449
|
+
}
|
|
10450
|
+
}
|
|
10451
|
+
"@
|
|
10452
|
+
function Emit($obj) { Write-Output ($obj | ConvertTo-Json -Compress) }
|
|
10453
|
+
$hint = $env:MCP_CAPTURE_TITLE_HINT
|
|
10454
|
+
$outFile = $env:MCP_CAPTURE_OUT
|
|
10455
|
+
$candidates = [McpStudioCapture]::Find('RobloxStudioBeta')
|
|
10456
|
+
if ($candidates.Count -eq 0) { Emit @{ ok = $false; error = 'no visible Roblox Studio window was found' }; exit 0 }
|
|
10457
|
+
$pick = $null
|
|
10458
|
+
if ($hint) { $pick = $candidates | Where-Object { $_.Title.StartsWith($hint) } | Select-Object -First 1 }
|
|
10459
|
+
if ($pick -eq $null -and $candidates.Count -eq 1) { $pick = $candidates[0] }
|
|
10460
|
+
if ($pick -eq $null) {
|
|
10461
|
+
$titles = ($candidates | ForEach-Object { $_.Title }) -join ' | '
|
|
10462
|
+
Emit @{ ok = $false; error = "could not pick a Studio window for '$hint' among: $titles" }; exit 0
|
|
10463
|
+
}
|
|
10464
|
+
if ([McpStudioCapture]::IsIconic($pick.Handle)) {
|
|
10465
|
+
Emit @{ ok = $false; error = "the Studio window '$($pick.Title)' is minimized; restore it (it may stay behind other windows)" }; exit 0
|
|
10466
|
+
}
|
|
10467
|
+
$rect = New-Object McpStudioCapture+RECT
|
|
10468
|
+
[void][McpStudioCapture]::GetClientRect($pick.Handle, [ref]$rect)
|
|
10469
|
+
$w = $rect.Right - $rect.Left; $h = $rect.Bottom - $rect.Top
|
|
10470
|
+
if ($w -le 0 -or $h -le 0) { Emit @{ ok = $false; error = "the Studio window client area is empty ($w x $h)" }; exit 0 }
|
|
10471
|
+
$bmp = New-Object System.Drawing.Bitmap $w, $h, ([System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
10472
|
+
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
|
10473
|
+
$hdc = $g.GetHdc()
|
|
10474
|
+
# PW_CLIENTONLY (1) | PW_RENDERFULLCONTENT (2): DWM-composited client area, occluded windows included.
|
|
10475
|
+
$printed = [McpStudioCapture]::PrintWindow($pick.Handle, $hdc, 3)
|
|
10476
|
+
$g.ReleaseHdc($hdc); $g.Dispose()
|
|
10477
|
+
if (-not $printed) { $bmp.Dispose(); Emit @{ ok = $false; error = 'PrintWindow failed for the Studio window' }; exit 0 }
|
|
10478
|
+
$bounds = New-Object System.Drawing.Rectangle 0, 0, $w, $h
|
|
10479
|
+
$data = $bmp.LockBits($bounds, [System.Drawing.Imaging.ImageLockMode]::ReadOnly, [System.Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
|
10480
|
+
$bytes = New-Object byte[] ($data.Stride * $h)
|
|
10481
|
+
[System.Runtime.InteropServices.Marshal]::Copy($data.Scan0, $bytes, 0, $bytes.Length)
|
|
10482
|
+
$stride = $data.Stride
|
|
10483
|
+
$bmp.UnlockBits($data); $bmp.Dispose()
|
|
10484
|
+
[System.IO.File]::WriteAllBytes($outFile, $bytes)
|
|
10485
|
+
Emit @{ ok = $true; width = $w; height = $h; stride = $stride; title = $pick.Title }
|
|
10486
|
+
`;
|
|
10487
|
+
function powershellPath() {
|
|
10488
|
+
const systemRoot = process.env.SystemRoot ?? process.env.windir;
|
|
10489
|
+
if (systemRoot) {
|
|
10490
|
+
const candidate = path5.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
10491
|
+
if (fs3.existsSync(candidate))
|
|
10492
|
+
return candidate;
|
|
10493
|
+
}
|
|
10494
|
+
return "powershell.exe";
|
|
10495
|
+
}
|
|
10496
|
+
function runWindowsCapture(titleHint, outFile) {
|
|
10497
|
+
return new Promise((resolve5, reject) => {
|
|
10498
|
+
const encoded = Buffer.from(WINDOWS_CAPTURE_SCRIPT, "utf16le").toString("base64");
|
|
10499
|
+
const child = spawn2(powershellPath(), ["-NoProfile", "-NonInteractive", "-NoLogo", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], {
|
|
10500
|
+
env: { ...process.env, MCP_CAPTURE_TITLE_HINT: titleHint ?? "", MCP_CAPTURE_OUT: outFile },
|
|
10501
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10502
|
+
windowsHide: true
|
|
10503
|
+
});
|
|
10504
|
+
let stdout = "";
|
|
10505
|
+
let stderr = "";
|
|
10506
|
+
let settled = false;
|
|
10507
|
+
const timer = setTimeout(() => {
|
|
10508
|
+
if (settled)
|
|
10509
|
+
return;
|
|
10510
|
+
settled = true;
|
|
10511
|
+
child.kill();
|
|
10512
|
+
reject(new Error(`host window capture timed out after ${HOST_CAPTURE_TIMEOUT_MS}ms`));
|
|
10513
|
+
}, HOST_CAPTURE_TIMEOUT_MS);
|
|
10514
|
+
child.stdout.setEncoding("utf8");
|
|
10515
|
+
child.stderr.setEncoding("utf8");
|
|
10516
|
+
child.stdout.on("data", (chunk) => {
|
|
10517
|
+
stdout += chunk;
|
|
10518
|
+
});
|
|
10519
|
+
child.stderr.on("data", (chunk) => {
|
|
10520
|
+
stderr += chunk;
|
|
10521
|
+
});
|
|
10522
|
+
child.on("error", (error) => {
|
|
10523
|
+
if (settled)
|
|
10524
|
+
return;
|
|
10525
|
+
settled = true;
|
|
10526
|
+
clearTimeout(timer);
|
|
10527
|
+
reject(new Error(`could not start PowerShell for host window capture: ${error.message}`));
|
|
10528
|
+
});
|
|
10529
|
+
child.on("close", (code) => {
|
|
10530
|
+
if (settled)
|
|
10531
|
+
return;
|
|
10532
|
+
settled = true;
|
|
10533
|
+
clearTimeout(timer);
|
|
10534
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
10535
|
+
const last = lines[lines.length - 1];
|
|
10536
|
+
if (last && last.startsWith("{")) {
|
|
10537
|
+
try {
|
|
10538
|
+
resolve5(JSON.parse(last));
|
|
10539
|
+
return;
|
|
10540
|
+
} catch {
|
|
10541
|
+
}
|
|
10542
|
+
}
|
|
10543
|
+
const detail = (stderr.trim() || stdout.trim()).slice(0, 600);
|
|
10544
|
+
reject(new Error(`host window capture helper exited with code ${code}${detail ? `: ${detail}` : ""}`));
|
|
10545
|
+
});
|
|
10546
|
+
});
|
|
10547
|
+
}
|
|
10548
|
+
async function captureStudioWindow(titleHint) {
|
|
10549
|
+
if (isHostCaptureDisabled()) {
|
|
10550
|
+
return { ok: false, error: "host window capture is disabled by ROBLOX_STUDIO_HOST_CAPTURE" };
|
|
10551
|
+
}
|
|
10552
|
+
if (!isHostCaptureSupported()) {
|
|
10553
|
+
return { ok: false, error: hostCaptureUnsupportedReason() };
|
|
10554
|
+
}
|
|
10555
|
+
const outFile = path5.join(os3.tmpdir(), `robloxstudio-mcp-capture-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.bgra`);
|
|
10556
|
+
try {
|
|
10557
|
+
const report = await runWindowsCapture(titleHint, outFile);
|
|
10558
|
+
if (!report.ok || !report.width || !report.height || !report.stride) {
|
|
10559
|
+
return { ok: false, error: report.error ?? "host window capture returned no image" };
|
|
10560
|
+
}
|
|
10561
|
+
const raw = fs3.readFileSync(outFile);
|
|
10562
|
+
const { width, height, stride } = report;
|
|
10563
|
+
const rgba = Buffer.alloc(width * height * 4);
|
|
10564
|
+
for (let y = 0; y < height; y++) {
|
|
10565
|
+
const src = y * stride;
|
|
10566
|
+
const dst = y * width * 4;
|
|
10567
|
+
for (let x = 0; x < width; x++) {
|
|
10568
|
+
const s = src + x * 4;
|
|
10569
|
+
const d = dst + x * 4;
|
|
10570
|
+
rgba[d] = raw[s + 2];
|
|
10571
|
+
rgba[d + 1] = raw[s + 1];
|
|
10572
|
+
rgba[d + 2] = raw[s];
|
|
10573
|
+
rgba[d + 3] = 255;
|
|
10574
|
+
}
|
|
10575
|
+
}
|
|
10576
|
+
return { ok: true, capture: { width, height, rgba, title: report.title ?? "" } };
|
|
10577
|
+
} catch (error) {
|
|
10578
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
10579
|
+
} finally {
|
|
10580
|
+
fs3.rmSync(outFile, { force: true });
|
|
10581
|
+
}
|
|
10582
|
+
}
|
|
10583
|
+
|
|
10584
|
+
// ../core/dist/tools/index.js
|
|
10585
|
+
import * as fs4 from "fs";
|
|
10586
|
+
import * as path6 from "path";
|
|
10587
|
+
var HOST_VIEWPORT_RECT_TTL_MS = 6e4;
|
|
10108
10588
|
var MAX_INLINE_IMAGE_BYTES = 6e6;
|
|
10109
10589
|
var MAX_MATRIX_IMAGE_BYTES = 8e6;
|
|
10110
10590
|
var DEFAULT_ASSET_AUDIO_PREVIEWS = 3;
|
|
@@ -10283,9 +10763,9 @@ function compactSoundReference(sound) {
|
|
|
10283
10763
|
name: stringField(sound, "name"),
|
|
10284
10764
|
className: stringField(sound, "className")
|
|
10285
10765
|
};
|
|
10286
|
-
const
|
|
10287
|
-
if (
|
|
10288
|
-
compact.path =
|
|
10766
|
+
const path7 = stringField(sound, "path");
|
|
10767
|
+
if (path7)
|
|
10768
|
+
compact.path = path7;
|
|
10289
10769
|
const assetId = robloxAssetIdFromContentId(sound.assetId ?? sound.soundId ?? sound.asset);
|
|
10290
10770
|
if (assetId !== void 0)
|
|
10291
10771
|
compact.assetId = assetId;
|
|
@@ -10348,8 +10828,8 @@ function loadMicroProfilerBaseline(source, sourcePath) {
|
|
|
10348
10828
|
if (typeof sourcePath !== "string" || sourcePath === "") {
|
|
10349
10829
|
throw new Error("baseline_path must be a non-empty string when provided");
|
|
10350
10830
|
}
|
|
10351
|
-
const resolved =
|
|
10352
|
-
const parsed = JSON.parse(
|
|
10831
|
+
const resolved = path6.resolve(sourcePath);
|
|
10832
|
+
const parsed = JSON.parse(fs4.readFileSync(resolved, "utf8"));
|
|
10353
10833
|
const record = asRecord(parsed);
|
|
10354
10834
|
if (!record)
|
|
10355
10835
|
throw new Error(`baseline_path did not contain a JSON object: ${resolved}`);
|
|
@@ -10851,6 +11331,9 @@ var RobloxStudioTools = class {
|
|
|
10851
11331
|
cookieClient;
|
|
10852
11332
|
instanceManager;
|
|
10853
11333
|
managedConnectionAssociations = Promise.resolve();
|
|
11334
|
+
hostWindowCapture = captureStudioWindow;
|
|
11335
|
+
hostViewportRects = /* @__PURE__ */ new Map();
|
|
11336
|
+
viewportCaptureQueues = /* @__PURE__ */ new Map();
|
|
10854
11337
|
constructor(bridge) {
|
|
10855
11338
|
this.client = new StudioHttpClient(bridge);
|
|
10856
11339
|
this.bridge = bridge;
|
|
@@ -11290,8 +11773,8 @@ var RobloxStudioTools = class {
|
|
|
11290
11773
|
timedOut: true
|
|
11291
11774
|
};
|
|
11292
11775
|
}
|
|
11293
|
-
async getFileTree(
|
|
11294
|
-
const response = await this._callSingle("/api/file-tree", { path:
|
|
11776
|
+
async getFileTree(path7 = "", instance_id) {
|
|
11777
|
+
const response = await this._callSingle("/api/file-tree", { path: path7 }, void 0, instance_id);
|
|
11295
11778
|
return {
|
|
11296
11779
|
content: [
|
|
11297
11780
|
{
|
|
@@ -11383,9 +11866,9 @@ var RobloxStudioTools = class {
|
|
|
11383
11866
|
]
|
|
11384
11867
|
};
|
|
11385
11868
|
}
|
|
11386
|
-
async getProjectStructure(
|
|
11869
|
+
async getProjectStructure(path7, maxDepth, scriptsOnly, instance_id) {
|
|
11387
11870
|
const response = await this._callSingle("/api/project-structure", {
|
|
11388
|
-
path:
|
|
11871
|
+
path: path7,
|
|
11389
11872
|
maxDepth,
|
|
11390
11873
|
scriptsOnly
|
|
11391
11874
|
}, void 0, instance_id);
|
|
@@ -11546,7 +12029,7 @@ var RobloxStudioTools = class {
|
|
|
11546
12029
|
};
|
|
11547
12030
|
}
|
|
11548
12031
|
async setSelection(paths, mode, instance_id) {
|
|
11549
|
-
if (!Array.isArray(paths) || paths.some((
|
|
12032
|
+
if (!Array.isArray(paths) || paths.some((path7) => typeof path7 !== "string" || path7.length === 0)) {
|
|
11550
12033
|
throw new Error("selection paths must contain only non-empty instance paths");
|
|
11551
12034
|
}
|
|
11552
12035
|
const selectionMode = mode ?? "set";
|
|
@@ -12283,9 +12766,9 @@ var RobloxStudioTools = class {
|
|
|
12283
12766
|
const rawJson = mutable.raw_json;
|
|
12284
12767
|
if (typeof rawJson === "string") {
|
|
12285
12768
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
12286
|
-
const resolvedOutputPath =
|
|
12287
|
-
|
|
12288
|
-
|
|
12769
|
+
const resolvedOutputPath = path6.resolve(outputPath);
|
|
12770
|
+
fs4.mkdirSync(path6.dirname(resolvedOutputPath), { recursive: true });
|
|
12771
|
+
fs4.writeFileSync(resolvedOutputPath, rawJson, "utf8");
|
|
12289
12772
|
mutable.output_path = resolvedOutputPath;
|
|
12290
12773
|
}
|
|
12291
12774
|
delete mutable.raw_json;
|
|
@@ -12346,9 +12829,9 @@ var RobloxStudioTools = class {
|
|
|
12346
12829
|
const rawSnapshotBase64 = mutable.raw_snapshot_base64;
|
|
12347
12830
|
if (typeof rawSnapshotBase64 === "string") {
|
|
12348
12831
|
if (typeof outputPath === "string" && outputPath !== "") {
|
|
12349
|
-
const resolvedOutputPath =
|
|
12350
|
-
|
|
12351
|
-
|
|
12832
|
+
const resolvedOutputPath = path6.resolve(outputPath);
|
|
12833
|
+
fs4.mkdirSync(path6.dirname(resolvedOutputPath), { recursive: true });
|
|
12834
|
+
fs4.writeFileSync(resolvedOutputPath, Buffer.from(rawSnapshotBase64, "base64"));
|
|
12352
12835
|
mutable.output_path = resolvedOutputPath;
|
|
12353
12836
|
}
|
|
12354
12837
|
delete mutable.raw_snapshot_base64;
|
|
@@ -12362,9 +12845,9 @@ var RobloxStudioTools = class {
|
|
|
12362
12845
|
});
|
|
12363
12846
|
}
|
|
12364
12847
|
if (typeof summaryOutputPath === "string" && summaryOutputPath !== "") {
|
|
12365
|
-
const resolvedSummaryPath =
|
|
12366
|
-
|
|
12367
|
-
|
|
12848
|
+
const resolvedSummaryPath = path6.resolve(summaryOutputPath);
|
|
12849
|
+
fs4.mkdirSync(path6.dirname(resolvedSummaryPath), { recursive: true });
|
|
12850
|
+
fs4.writeFileSync(resolvedSummaryPath, JSON.stringify(mutable, null, 2), "utf8");
|
|
12368
12851
|
mutable.summary_output_path = resolvedSummaryPath;
|
|
12369
12852
|
}
|
|
12370
12853
|
if (!includeComparisonIndex) {
|
|
@@ -12426,7 +12909,7 @@ var RobloxStudioTools = class {
|
|
|
12426
12909
|
return record.placeId !== void 0 && instance.placeId === record.placeId;
|
|
12427
12910
|
}
|
|
12428
12911
|
if ((record.source === "baseplate" || record.source === "local_file") && record.localPlaceFile) {
|
|
12429
|
-
const expectedName =
|
|
12912
|
+
const expectedName = path6.basename(record.localPlaceFile);
|
|
12430
12913
|
return instance.placeName === expectedName || instance.dataModelName === expectedName;
|
|
12431
12914
|
}
|
|
12432
12915
|
return true;
|
|
@@ -12573,6 +13056,7 @@ var RobloxStudioTools = class {
|
|
|
12573
13056
|
});
|
|
12574
13057
|
}
|
|
12575
13058
|
return this._textResult({
|
|
13059
|
+
test_worker_job_name: parseStudioTestWorkerJobName(process.env.RSMCP_STUDIO_TEST_WORKER_JOB),
|
|
12576
13060
|
managed: (await this.instanceManager.list()).filter((record2) => record2.closedAt === void 0).map((record2) => this._managedStatus(record2)),
|
|
12577
13061
|
connected: this.bridge.getPublicInstances().map((instance) => ({
|
|
12578
13062
|
instance_id: instance.id,
|
|
@@ -12651,10 +13135,9 @@ var RobloxStudioTools = class {
|
|
|
12651
13135
|
}
|
|
12652
13136
|
record2 = active[0];
|
|
12653
13137
|
}
|
|
12654
|
-
if (record2.instanceId)
|
|
12655
|
-
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
12656
13138
|
const closeResult = await this.instanceManager.close(record2);
|
|
12657
13139
|
if (record2.instanceId) {
|
|
13140
|
+
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
12658
13141
|
await sleep(500);
|
|
12659
13142
|
await this.bridge.unregisterInstanceIdEverywhere(record2.instanceId);
|
|
12660
13143
|
}
|
|
@@ -13757,11 +14240,11 @@ var RobloxStudioTools = class {
|
|
|
13757
14240
|
return this.resolveUploadedReferenceImageId(decalId, instance_id);
|
|
13758
14241
|
}
|
|
13759
14242
|
async uploadAsset(filePath, assetType, displayName, description, userId, groupId) {
|
|
13760
|
-
if (!
|
|
14243
|
+
if (!fs4.existsSync(filePath)) {
|
|
13761
14244
|
throw new Error(`File not found: ${filePath}`);
|
|
13762
14245
|
}
|
|
13763
|
-
const fileContent =
|
|
13764
|
-
const fileName =
|
|
14246
|
+
const fileContent = fs4.readFileSync(filePath);
|
|
14247
|
+
const fileName = path6.basename(filePath);
|
|
13765
14248
|
const resolvedGroupId = groupId || process.env.ROBLOX_CREATOR_GROUP_ID;
|
|
13766
14249
|
const resolvedUserId = userId || process.env.ROBLOX_CREATOR_USER_ID;
|
|
13767
14250
|
if (assetType === "Decal" && this.cookieClient.hasCookie()) {
|
|
@@ -13977,10 +14460,10 @@ var RobloxStudioTools = class {
|
|
|
13977
14460
|
return { content: [{ type: "text", text: JSON.stringify({ error: "plugin returned no base64 payload" }) }] };
|
|
13978
14461
|
}
|
|
13979
14462
|
const bytes = Buffer.from(response.base64, "base64");
|
|
13980
|
-
const resolved =
|
|
14463
|
+
const resolved = path6.resolve(outputPath);
|
|
13981
14464
|
try {
|
|
13982
|
-
|
|
13983
|
-
|
|
14465
|
+
fs4.mkdirSync(path6.dirname(resolved), { recursive: true });
|
|
14466
|
+
fs4.writeFileSync(resolved, bytes);
|
|
13984
14467
|
} catch (err2) {
|
|
13985
14468
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to write ${resolved}: ${err2.message}` }) }] };
|
|
13986
14469
|
}
|
|
@@ -14013,9 +14496,9 @@ var RobloxStudioTools = class {
|
|
|
14013
14496
|
let bytes;
|
|
14014
14497
|
let sourceLabel;
|
|
14015
14498
|
if (source.path !== void 0) {
|
|
14016
|
-
const resolved =
|
|
14499
|
+
const resolved = path6.resolve(source.path);
|
|
14017
14500
|
try {
|
|
14018
|
-
bytes =
|
|
14501
|
+
bytes = fs4.readFileSync(resolved);
|
|
14019
14502
|
} catch (err2) {
|
|
14020
14503
|
return { content: [{ type: "text", text: JSON.stringify({ error: `failed to read ${resolved}: ${err2.message}` }) }] };
|
|
14021
14504
|
}
|
|
@@ -14065,7 +14548,21 @@ var RobloxStudioTools = class {
|
|
|
14065
14548
|
}, tgt, instance_id);
|
|
14066
14549
|
return { content: [{ type: "text", text: JSON.stringify(response) }] };
|
|
14067
14550
|
}
|
|
14551
|
+
// Even native capture must wait while another call has fitted the simulator
|
|
14552
|
+
// or drawn markers. Otherwise it could observe temporary UI or dimensions.
|
|
14068
14553
|
async _captureViewportImage(instanceId, targetRole, format, quality, maxBytes = MAX_INLINE_IMAGE_BYTES) {
|
|
14554
|
+
const previous = this.viewportCaptureQueues.get(instanceId) ?? Promise.resolve();
|
|
14555
|
+
const capture = previous.then(() => this._captureViewportImageNow(instanceId, targetRole, format, quality, maxBytes));
|
|
14556
|
+
const settled = capture.then(() => void 0, () => void 0);
|
|
14557
|
+
this.viewportCaptureQueues.set(instanceId, settled);
|
|
14558
|
+
try {
|
|
14559
|
+
return await capture;
|
|
14560
|
+
} finally {
|
|
14561
|
+
if (this.viewportCaptureQueues.get(instanceId) === settled)
|
|
14562
|
+
this.viewportCaptureQueues.delete(instanceId);
|
|
14563
|
+
}
|
|
14564
|
+
}
|
|
14565
|
+
async _captureViewportImageNow(instanceId, targetRole, format, quality, maxBytes = MAX_INLINE_IMAGE_BYTES) {
|
|
14069
14566
|
const fmt = format === "png" ? "png" : "jpeg";
|
|
14070
14567
|
const q = quality === void 0 ? 92 : Math.max(1, Math.min(100, Math.floor(quality)));
|
|
14071
14568
|
let response = await this._callSingle("/api/capture-studio", { encoding: fmt === "png" ? "png" : "rgba8" }, targetRole, instanceId);
|
|
@@ -14084,6 +14581,25 @@ var RobloxStudioTools = class {
|
|
|
14084
14581
|
response = await this._callSingle("/api/capture-screenshot", {}, "edit", instanceId);
|
|
14085
14582
|
}
|
|
14086
14583
|
}
|
|
14584
|
+
let hostNote = "";
|
|
14585
|
+
let hostReason;
|
|
14586
|
+
try {
|
|
14587
|
+
hostReason = response.error ? `Studio's capture failed (${response.error})` : this._isUniformCaptureResponse(response) ? "Studio's CaptureService returned a blank (single-colour) frame" : void 0;
|
|
14588
|
+
} catch (error) {
|
|
14589
|
+
response = { ...response, error: `Could not decode Studio's screenshot: ${error instanceof Error ? error.message : String(error)}` };
|
|
14590
|
+
hostReason = `Studio's capture failed (${response.error})`;
|
|
14591
|
+
}
|
|
14592
|
+
if (hostReason !== void 0) {
|
|
14593
|
+
const host = await this._captureViewportFromHostWindow(instanceId, targetRole);
|
|
14594
|
+
if (host.success) {
|
|
14595
|
+
response = host.response;
|
|
14596
|
+
hostNote = ` Captured from the Studio window through the host OS because ${hostReason}.`;
|
|
14597
|
+
} else if (response.error) {
|
|
14598
|
+
response = { ...response, error: `${response.error} Host window capture also failed: ${host.error}.` };
|
|
14599
|
+
} else {
|
|
14600
|
+
hostNote = ` Warning: ${hostReason} and host window capture also failed (${host.error}), so this image may be blank.`;
|
|
14601
|
+
}
|
|
14602
|
+
}
|
|
14087
14603
|
if (response.error) {
|
|
14088
14604
|
let text = response.error;
|
|
14089
14605
|
if (targetRole.startsWith("client-") && response.error.includes("Failed to load texture, unexpected format") && await this._isMultiplayerTestRunning(instanceId)) {
|
|
@@ -14133,7 +14649,7 @@ var RobloxStudioTools = class {
|
|
|
14133
14649
|
}
|
|
14134
14650
|
const nativeW = response.nativeWidth ?? w;
|
|
14135
14651
|
const nativeH = response.nativeHeight ?? h;
|
|
14136
|
-
const message = nativeW !== w || nativeH !== h ? `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}, downscaled from the ${nativeW}x${nativeH} viewport to fit transport limits. For simulate_mouse_input, multiply x read off this image by ${(nativeW / w).toFixed(4)} and y by ${(nativeH / h).toFixed(4)} to get viewport pixel coordinates ((0,0) at the top-left).` : `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image
|
|
14652
|
+
const message = (nativeW !== w || nativeH !== h ? `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}, downscaled from the ${nativeW}x${nativeH} viewport to fit transport limits. For simulate_mouse_input, multiply x read off this image by ${(nativeW / w).toFixed(4)} and y by ${(nativeH / h).toFixed(4)} to get viewport pixel coordinates ((0,0) at the top-left).` : `Screenshot ${w}x${h}px (${fmt}${fmt === "jpeg" ? ` q${usedQ}` : ""})${note}. For simulate_mouse_input, x/y are pixel coordinates in this exact image with (0,0) at the top-left; it is not downscaled, so use coordinates as you read them off the image.`) + hostNote;
|
|
14137
14653
|
return {
|
|
14138
14654
|
success: true,
|
|
14139
14655
|
width: w,
|
|
@@ -14146,6 +14662,149 @@ var RobloxStudioTools = class {
|
|
|
14146
14662
|
message
|
|
14147
14663
|
};
|
|
14148
14664
|
}
|
|
14665
|
+
_isUniformCaptureResponse(response) {
|
|
14666
|
+
if (!response.data || !response.width || !response.height)
|
|
14667
|
+
return false;
|
|
14668
|
+
const data = Buffer.from(response.data, "base64");
|
|
14669
|
+
if (response.encoding === "png")
|
|
14670
|
+
return isUniformPng(data);
|
|
14671
|
+
return isUniformFrame(data, response.width, response.height);
|
|
14672
|
+
}
|
|
14673
|
+
_hostCaptureTitleHint(instanceId) {
|
|
14674
|
+
const peers = this.bridge.getPeers().filter((peer) => peer.instanceId === instanceId);
|
|
14675
|
+
const edit = peers.find((peer) => peer.role === "edit") ?? peers[0];
|
|
14676
|
+
const name = edit?.dataModelName || edit?.placeName;
|
|
14677
|
+
return name ? name : void 0;
|
|
14678
|
+
}
|
|
14679
|
+
async _callViewportMarkers(action, targetRole, instanceId, captureId) {
|
|
14680
|
+
return await this._callSingle("/api/capture-markers", { action, ...captureId ? { captureId } : {} }, targetRole, instanceId);
|
|
14681
|
+
}
|
|
14682
|
+
// Always restore the simulator, including failures and cached window grabs.
|
|
14683
|
+
async _captureViewportFromHostWindow(instanceId, targetRole) {
|
|
14684
|
+
let prepared;
|
|
14685
|
+
let result;
|
|
14686
|
+
try {
|
|
14687
|
+
prepared = await this._callViewportMarkers("prepare", targetRole, instanceId);
|
|
14688
|
+
if (prepared.error || !prepared.captureId || !prepared.viewportWidth || !prepared.viewportHeight) {
|
|
14689
|
+
result = { success: false, error: prepared.error ?? "the Studio plugin could not prepare host capture (update the plugin)" };
|
|
14690
|
+
} else {
|
|
14691
|
+
if (prepared.viewportChanged)
|
|
14692
|
+
this.hostViewportRects.delete(`${instanceId}|${targetRole}`);
|
|
14693
|
+
result = await this._capturePreparedViewportFromHostWindow(instanceId, targetRole, prepared);
|
|
14694
|
+
}
|
|
14695
|
+
} catch (error) {
|
|
14696
|
+
result = { success: false, error: `host capture failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
14697
|
+
} finally {
|
|
14698
|
+
if (prepared?.captureId) {
|
|
14699
|
+
try {
|
|
14700
|
+
const restored = await this._callViewportMarkers("finish", targetRole, instanceId, prepared.captureId);
|
|
14701
|
+
if (restored.error)
|
|
14702
|
+
result = { success: false, error: `could not restore the Studio viewport: ${restored.error}` };
|
|
14703
|
+
else if (restored.stale || !restored.success)
|
|
14704
|
+
result = { success: false, error: "the host capture transaction expired or lost ownership; retry the capture" };
|
|
14705
|
+
} catch (error) {
|
|
14706
|
+
result = { success: false, error: `could not restore the Studio viewport: ${error instanceof Error ? error.message : String(error)}` };
|
|
14707
|
+
}
|
|
14708
|
+
}
|
|
14709
|
+
}
|
|
14710
|
+
if (!result.success)
|
|
14711
|
+
this.hostViewportRects.delete(`${instanceId}|${targetRole}`);
|
|
14712
|
+
return result;
|
|
14713
|
+
}
|
|
14714
|
+
// Grabs the Studio window through the host OS and crops it to the viewport
|
|
14715
|
+
// of `targetRole`. The plugin pins magenta markers to the viewport corners
|
|
14716
|
+
// so the crop is exact regardless of Studio's dock layout or DPI scale; the
|
|
14717
|
+
// located rect is cached briefly so repeated captures cost one window grab.
|
|
14718
|
+
async _capturePreparedViewportFromHostWindow(instanceId, targetRole, prepared) {
|
|
14719
|
+
const titleHint = this._hostCaptureTitleHint(instanceId);
|
|
14720
|
+
const cacheKey = `${instanceId}|${targetRole}`;
|
|
14721
|
+
const cached = this.hostViewportRects.get(cacheKey);
|
|
14722
|
+
let sizeInfo;
|
|
14723
|
+
try {
|
|
14724
|
+
sizeInfo = await this._callViewportMarkers("query", targetRole, instanceId, prepared.captureId);
|
|
14725
|
+
} catch (error) {
|
|
14726
|
+
return { success: false, error: `viewport markers unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
14727
|
+
}
|
|
14728
|
+
if (sizeInfo.error || !sizeInfo.viewportWidth || !sizeInfo.viewportHeight) {
|
|
14729
|
+
return {
|
|
14730
|
+
success: false,
|
|
14731
|
+
error: sizeInfo.error ?? "the Studio plugin did not report a viewport size (update the plugin to a build with /api/capture-markers)"
|
|
14732
|
+
};
|
|
14733
|
+
}
|
|
14734
|
+
const viewportWidth = prepared.viewportWidth ?? sizeInfo.viewportWidth;
|
|
14735
|
+
const viewportHeight = prepared.viewportHeight ?? sizeInfo.viewportHeight;
|
|
14736
|
+
if (cached && Date.now() - cached.cachedAt < HOST_VIEWPORT_RECT_TTL_MS && cached.viewportWidth === viewportWidth && cached.viewportHeight === viewportHeight) {
|
|
14737
|
+
const grab = await this.hostWindowCapture(titleHint);
|
|
14738
|
+
if (!grab.ok)
|
|
14739
|
+
return { success: false, error: grab.error };
|
|
14740
|
+
if (grab.capture.width === cached.windowWidth && grab.capture.height === cached.windowHeight) {
|
|
14741
|
+
return {
|
|
14742
|
+
success: true,
|
|
14743
|
+
response: this._hostResponseFromCrop(grab.capture.rgba, grab.capture.width, grab.capture.height, cached.rect, viewportWidth, viewportHeight)
|
|
14744
|
+
};
|
|
14745
|
+
}
|
|
14746
|
+
this.hostViewportRects.delete(cacheKey);
|
|
14747
|
+
}
|
|
14748
|
+
let shown;
|
|
14749
|
+
try {
|
|
14750
|
+
shown = await this._callViewportMarkers("show", targetRole, instanceId, prepared.captureId);
|
|
14751
|
+
} catch (error) {
|
|
14752
|
+
return { success: false, error: `could not draw viewport markers: ${error instanceof Error ? error.message : String(error)}` };
|
|
14753
|
+
}
|
|
14754
|
+
if (shown.error)
|
|
14755
|
+
return { success: false, error: `could not draw viewport markers: ${shown.error}` };
|
|
14756
|
+
const markerSize = shown.markerSize ?? 12;
|
|
14757
|
+
let markerGrab;
|
|
14758
|
+
let hidden;
|
|
14759
|
+
try {
|
|
14760
|
+
markerGrab = await this.hostWindowCapture(titleHint);
|
|
14761
|
+
} finally {
|
|
14762
|
+
hidden = await this._callViewportMarkers("hide", targetRole, instanceId, prepared.captureId);
|
|
14763
|
+
}
|
|
14764
|
+
if (hidden.error)
|
|
14765
|
+
return { success: false, error: `could not hide viewport markers: ${hidden.error}` };
|
|
14766
|
+
if (!markerGrab.ok)
|
|
14767
|
+
return { success: false, error: markerGrab.error };
|
|
14768
|
+
const located = findViewportRect(markerGrab.capture.rgba, markerGrab.capture.width, markerGrab.capture.height, {
|
|
14769
|
+
viewportWidth: shown.viewportWidth ?? viewportWidth,
|
|
14770
|
+
viewportHeight: shown.viewportHeight ?? viewportHeight,
|
|
14771
|
+
markerSize
|
|
14772
|
+
});
|
|
14773
|
+
if ("error" in located)
|
|
14774
|
+
return { success: false, error: located.error };
|
|
14775
|
+
this.hostViewportRects.set(cacheKey, {
|
|
14776
|
+
rect: located.rect,
|
|
14777
|
+
windowWidth: markerGrab.capture.width,
|
|
14778
|
+
windowHeight: markerGrab.capture.height,
|
|
14779
|
+
viewportWidth,
|
|
14780
|
+
viewportHeight,
|
|
14781
|
+
cachedAt: Date.now()
|
|
14782
|
+
});
|
|
14783
|
+
const cleanGrab = await this.hostWindowCapture(titleHint);
|
|
14784
|
+
if (!cleanGrab.ok)
|
|
14785
|
+
return { success: false, error: cleanGrab.error };
|
|
14786
|
+
if (cleanGrab.capture.width !== markerGrab.capture.width || cleanGrab.capture.height !== markerGrab.capture.height) {
|
|
14787
|
+
this.hostViewportRects.delete(cacheKey);
|
|
14788
|
+
return { success: false, error: "the Studio window was resized while locating the viewport; retry the capture" };
|
|
14789
|
+
}
|
|
14790
|
+
return {
|
|
14791
|
+
success: true,
|
|
14792
|
+
response: this._hostResponseFromCrop(cleanGrab.capture.rgba, cleanGrab.capture.width, cleanGrab.capture.height, located.rect, viewportWidth, viewportHeight)
|
|
14793
|
+
};
|
|
14794
|
+
}
|
|
14795
|
+
_hostResponseFromCrop(rgba, width, height, rect, viewportWidth, viewportHeight) {
|
|
14796
|
+
const cropped = cropToViewport(rgba, width, height, rect, viewportWidth, viewportHeight);
|
|
14797
|
+
return {
|
|
14798
|
+
success: true,
|
|
14799
|
+
encoding: "rgba8",
|
|
14800
|
+
source: "host-window",
|
|
14801
|
+
width: cropped.width,
|
|
14802
|
+
height: cropped.height,
|
|
14803
|
+
nativeWidth: viewportWidth,
|
|
14804
|
+
nativeHeight: viewportHeight,
|
|
14805
|
+
data: cropped.rgba.toString("base64")
|
|
14806
|
+
};
|
|
14807
|
+
}
|
|
14149
14808
|
async captureScreenshot(instance_id, format, quality) {
|
|
14150
14809
|
const refresh = this.bridge.refreshTopologyForRouting();
|
|
14151
14810
|
if (refresh)
|
|
@@ -16689,10 +17348,10 @@ var getReadOnlyTools = () => TOOL_DEFINITIONS.filter((t) => t.category === "read
|
|
|
16689
17348
|
|
|
16690
17349
|
// ../core/dist/install-plugin-helpers.js
|
|
16691
17350
|
var import_saxes = __toESM(require_saxes(), 1);
|
|
16692
|
-
import { existsSync as
|
|
17351
|
+
import { existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync, rmSync as rmSync3, rmdirSync, statSync as statSync3, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
16693
17352
|
import { execSync } from "child_process";
|
|
16694
17353
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
16695
|
-
import { basename as basename3, join as
|
|
17354
|
+
import { basename as basename3, join as join6 } from "path";
|
|
16696
17355
|
import { homedir as homedir4 } from "os";
|
|
16697
17356
|
import { isUtf8 } from "buffer";
|
|
16698
17357
|
var DEFAULT_PLUGIN_PORT = 58741;
|
|
@@ -16744,15 +17403,15 @@ function getPluginsFolder() {
|
|
|
16744
17403
|
if (process.env.MCP_PLUGINS_DIR)
|
|
16745
17404
|
return process.env.MCP_PLUGINS_DIR;
|
|
16746
17405
|
if (process.platform === "win32") {
|
|
16747
|
-
return
|
|
17406
|
+
return join6(process.env.LOCALAPPDATA || join6(homedir4(), "AppData", "Local"), "Roblox", "Plugins");
|
|
16748
17407
|
}
|
|
16749
17408
|
if (isWSL()) {
|
|
16750
17409
|
const localAppData = getWindowsUserLocalAppData();
|
|
16751
17410
|
if (localAppData)
|
|
16752
|
-
return
|
|
17411
|
+
return join6(localAppData.linuxPath, "Roblox", "Plugins");
|
|
16753
17412
|
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.");
|
|
16754
17413
|
}
|
|
16755
|
-
return
|
|
17414
|
+
return join6(homedir4(), "Documents", "Roblox", "Plugins");
|
|
16756
17415
|
}
|
|
16757
17416
|
var STALE_TEST_PLUGINS_DIRECTORY = "RsmcpIsolatedPlugins";
|
|
16758
17417
|
function pluginDirectorySettingPattern() {
|
|
@@ -16762,12 +17421,12 @@ function escapeXmlText(value) {
|
|
|
16762
17421
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
16763
17422
|
}
|
|
16764
17423
|
function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDirectory }) {
|
|
16765
|
-
if (!
|
|
17424
|
+
if (!existsSync7(settingsPath))
|
|
16766
17425
|
return false;
|
|
16767
17426
|
if (studioPluginsDirectory === "") {
|
|
16768
17427
|
throw new Error("studioPluginsDirectory must be a non-empty path.");
|
|
16769
17428
|
}
|
|
16770
|
-
const contents =
|
|
17429
|
+
const contents = readFileSync7(settingsPath, "utf8");
|
|
16771
17430
|
const matches = [...contents.matchAll(pluginDirectorySettingPattern())];
|
|
16772
17431
|
if (matches.length !== 1) {
|
|
16773
17432
|
throw new Error(`Expected exactly one Studio.PluginsDir QDir in ${settingsPath}; found ${matches.length}.`);
|
|
@@ -16784,7 +17443,7 @@ function repairStaleStudioPluginDirectorySetting({ settingsPath, studioPluginsDi
|
|
|
16784
17443
|
});
|
|
16785
17444
|
renameSync(temporaryPath, settingsPath);
|
|
16786
17445
|
} finally {
|
|
16787
|
-
|
|
17446
|
+
rmSync3(temporaryPath, { force: true });
|
|
16788
17447
|
}
|
|
16789
17448
|
return true;
|
|
16790
17449
|
}
|
|
@@ -16795,16 +17454,16 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
|
|
|
16795
17454
|
let defaultPluginsFolder;
|
|
16796
17455
|
let studioPluginsDirectory;
|
|
16797
17456
|
if (process.platform === "win32") {
|
|
16798
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
16799
|
-
defaultPluginsFolder =
|
|
16800
|
-
settingsPath =
|
|
17457
|
+
const localAppData = process.env.LOCALAPPDATA || join6(homedir4(), "AppData", "Local");
|
|
17458
|
+
defaultPluginsFolder = join6(localAppData, "Roblox", "Plugins");
|
|
17459
|
+
settingsPath = join6(localAppData, "Roblox", "GlobalSettings_13.xml");
|
|
16801
17460
|
studioPluginsDirectory = defaultPluginsFolder.replaceAll("\\", "/");
|
|
16802
17461
|
} else if (isWSL()) {
|
|
16803
17462
|
const localAppData = getWindowsUserLocalAppData();
|
|
16804
17463
|
if (!localAppData)
|
|
16805
17464
|
return;
|
|
16806
|
-
defaultPluginsFolder =
|
|
16807
|
-
settingsPath =
|
|
17465
|
+
defaultPluginsFolder = join6(localAppData.linuxPath, "Roblox", "Plugins");
|
|
17466
|
+
settingsPath = join6(localAppData.linuxPath, "Roblox", "GlobalSettings_13.xml");
|
|
16808
17467
|
studioPluginsDirectory = [
|
|
16809
17468
|
localAppData.windowsPath.replace(/[\\/]+$/u, ""),
|
|
16810
17469
|
"Roblox",
|
|
@@ -16820,8 +17479,8 @@ function repairDefaultStudioPluginDirectorySetting(pluginsFolder, log) {
|
|
|
16820
17479
|
}
|
|
16821
17480
|
}
|
|
16822
17481
|
function handleVariantConflict({ pluginsFolder, otherAssetName, replace, log = console.log, warn = console.warn }) {
|
|
16823
|
-
const otherDest =
|
|
16824
|
-
if (!
|
|
17482
|
+
const otherDest = join6(pluginsFolder, otherAssetName);
|
|
17483
|
+
if (!existsSync7(otherDest))
|
|
16825
17484
|
return;
|
|
16826
17485
|
if (replace) {
|
|
16827
17486
|
try {
|
|
@@ -16846,11 +17505,11 @@ function pluginInstallLockError(lockPath, reason) {
|
|
|
16846
17505
|
return new Error(`${reason}: ${lockPath}. If no installer or lock recovery is running, remove that lock directory and retry.`);
|
|
16847
17506
|
}
|
|
16848
17507
|
function readPluginInstallLockOwner(lockPath) {
|
|
16849
|
-
const ownerPath =
|
|
17508
|
+
const ownerPath = join6(lockPath, "owner.json");
|
|
16850
17509
|
if (!lstatSync(lockPath).isDirectory() || !lstatSync(ownerPath).isFile()) {
|
|
16851
17510
|
throw new Error("Lock and owner metadata must be a directory and regular file");
|
|
16852
17511
|
}
|
|
16853
|
-
const owner = JSON.parse(
|
|
17512
|
+
const owner = JSON.parse(readFileSync7(ownerPath, "utf8"));
|
|
16854
17513
|
if (owner === null || typeof owner !== "object" || !("pid" in owner) || typeof owner.pid !== "number" || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || owner.pid > 2147483647 || !("token" in owner) || typeof owner.token !== "string" || !/^[a-zA-Z0-9-]{1,128}$/.test(owner.token) || !("createdAt" in owner) || typeof owner.createdAt !== "number" || !Number.isFinite(owner.createdAt) || owner.createdAt < 0) {
|
|
16855
17514
|
throw new Error("Invalid lock owner metadata");
|
|
16856
17515
|
}
|
|
@@ -16876,7 +17535,7 @@ function recoverPluginInstallLock(lockPath, warn) {
|
|
|
16876
17535
|
throw pluginInstallLockError(lockPath, `Cannot safely read plugin installation lock owner (${error})`);
|
|
16877
17536
|
}
|
|
16878
17537
|
assertPluginInstallOwnerExited(lockPath, owner);
|
|
16879
|
-
const claimPath =
|
|
17538
|
+
const claimPath = join6(lockPath, `recovery-${owner.token}`);
|
|
16880
17539
|
try {
|
|
16881
17540
|
mkdirSync4(claimPath);
|
|
16882
17541
|
} catch (error) {
|
|
@@ -16909,14 +17568,14 @@ function recoverPluginInstallLock(lockPath, warn) {
|
|
|
16909
17568
|
}
|
|
16910
17569
|
}
|
|
16911
17570
|
try {
|
|
16912
|
-
|
|
17571
|
+
rmSync3(abandonedPath, { recursive: true, force: true });
|
|
16913
17572
|
} catch (error) {
|
|
16914
17573
|
warn(`[install-plugin] Could not clean abandoned install lock ${abandonedPath}: ${error}`);
|
|
16915
17574
|
}
|
|
16916
17575
|
}
|
|
16917
17576
|
function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
16918
|
-
const lockPath =
|
|
16919
|
-
const ownerPath =
|
|
17577
|
+
const lockPath = join6(pluginsFolder, PLUGIN_INSTALL_LOCK_NAME);
|
|
17578
|
+
const ownerPath = join6(lockPath, "owner.json");
|
|
16920
17579
|
const owner = {
|
|
16921
17580
|
pid: process.pid,
|
|
16922
17581
|
token: randomUUID5(),
|
|
@@ -16944,7 +17603,7 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
|
16944
17603
|
});
|
|
16945
17604
|
} catch (error) {
|
|
16946
17605
|
try {
|
|
16947
|
-
|
|
17606
|
+
rmSync3(lockPath, { recursive: true, force: true });
|
|
16948
17607
|
} catch (cleanupError) {
|
|
16949
17608
|
warn(`[install-plugin] Could not clean failed install lock ${lockPath}: ${cleanupError}`);
|
|
16950
17609
|
}
|
|
@@ -16954,7 +17613,7 @@ function acquirePluginInstallLock(pluginsFolder, warn) {
|
|
|
16954
17613
|
try {
|
|
16955
17614
|
const currentOwner = readPluginInstallLockOwner(lockPath);
|
|
16956
17615
|
if (currentOwner.pid === owner.pid && currentOwner.token === owner.token) {
|
|
16957
|
-
|
|
17616
|
+
rmSync3(lockPath, { recursive: true, force: true });
|
|
16958
17617
|
}
|
|
16959
17618
|
} catch (error) {
|
|
16960
17619
|
if (errorCode(error) !== "ENOENT") {
|
|
@@ -17046,11 +17705,11 @@ function assertPluginAssetIdentity(source, assetName, expectedVersion, expectedV
|
|
|
17046
17705
|
}
|
|
17047
17706
|
}
|
|
17048
17707
|
function installedFileMatches(source, destination) {
|
|
17049
|
-
if (!
|
|
17708
|
+
if (!existsSync7(destination))
|
|
17050
17709
|
return false;
|
|
17051
17710
|
if (!lstatSync(destination).isFile())
|
|
17052
17711
|
return false;
|
|
17053
|
-
const installed =
|
|
17712
|
+
const installed = readFileSync7(destination);
|
|
17054
17713
|
return installed.length === source.length && installed.equals(source);
|
|
17055
17714
|
}
|
|
17056
17715
|
function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source, expectedVersion, expectedVariant, rawPort, replaceVariant = true, log = console.log, warn = console.warn }) {
|
|
@@ -17062,12 +17721,12 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
|
|
|
17062
17721
|
const configured = configurePluginAssetForPort(source, rawPort);
|
|
17063
17722
|
assertPluginAssetIdentity(configured, assetName, expectedVersion, expectedVariant);
|
|
17064
17723
|
mkdirSync4(pluginsFolder, { recursive: true });
|
|
17065
|
-
const destination =
|
|
17724
|
+
const destination = join6(pluginsFolder, assetName);
|
|
17066
17725
|
const releaseLock = acquirePluginInstallLock(pluginsFolder, warn);
|
|
17067
17726
|
try {
|
|
17068
17727
|
let installed = false;
|
|
17069
17728
|
if (!installedFileMatches(configured, destination)) {
|
|
17070
|
-
const staged =
|
|
17729
|
+
const staged = join6(pluginsFolder, `.${assetName}.${process.pid}.${randomUUID5()}.tmp`);
|
|
17071
17730
|
try {
|
|
17072
17731
|
writeFileSync3(staged, configured, { flag: "wx" });
|
|
17073
17732
|
renameSync(staged, destination);
|
|
@@ -17100,8 +17759,8 @@ function installPluginAsset({ pluginsFolder, assetName, otherAssetName, source,
|
|
|
17100
17759
|
import { createRequire } from "module";
|
|
17101
17760
|
|
|
17102
17761
|
// src/install-plugin.ts
|
|
17103
|
-
import { existsSync as
|
|
17104
|
-
import { dirname as dirname5, join as
|
|
17762
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
17763
|
+
import { dirname as dirname5, join as join7 } from "path";
|
|
17105
17764
|
import { fileURLToPath } from "url";
|
|
17106
17765
|
import { get } from "https";
|
|
17107
17766
|
var REPO = "chrrxs/robloxstudio-mcp";
|
|
@@ -17161,18 +17820,18 @@ async function fetchJson(url) {
|
|
|
17161
17820
|
function bundledAssetPath() {
|
|
17162
17821
|
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
17163
17822
|
const candidates = [
|
|
17164
|
-
|
|
17165
|
-
|
|
17823
|
+
join7(currentDir, "..", "studio-plugin", ASSET_NAME),
|
|
17824
|
+
join7(currentDir, "..", "..", "..", "studio-plugin", ASSET_NAME)
|
|
17166
17825
|
];
|
|
17167
|
-
return candidates.find((candidate) =>
|
|
17826
|
+
return candidates.find((candidate) => existsSync8(candidate)) ?? null;
|
|
17168
17827
|
}
|
|
17169
17828
|
function resolvePluginAssetPath(sourcePath) {
|
|
17170
17829
|
if (sourcePath === void 0) return bundledAssetPath();
|
|
17171
|
-
return
|
|
17830
|
+
return existsSync8(sourcePath) ? sourcePath : null;
|
|
17172
17831
|
}
|
|
17173
17832
|
function packageVersion() {
|
|
17174
17833
|
const currentDir = dirname5(fileURLToPath(import.meta.url));
|
|
17175
|
-
const pkg = JSON.parse(
|
|
17834
|
+
const pkg = JSON.parse(readFileSync8(join7(currentDir, "..", "package.json"), "utf8"));
|
|
17176
17835
|
if (!pkg.version) {
|
|
17177
17836
|
throw new Error("Package version not found");
|
|
17178
17837
|
}
|
|
@@ -17192,7 +17851,7 @@ async function installBundledPlugin(options = {}) {
|
|
|
17192
17851
|
pluginsFolder: getPluginsFolder(),
|
|
17193
17852
|
assetName: ASSET_NAME,
|
|
17194
17853
|
otherAssetName: OTHER_VARIANT,
|
|
17195
|
-
source:
|
|
17854
|
+
source: readFileSync8(sourcePath),
|
|
17196
17855
|
expectedVersion: packageVersion(),
|
|
17197
17856
|
expectedVariant: "inspector",
|
|
17198
17857
|
replaceVariant,
|
|
@@ -17216,7 +17875,7 @@ async function installPlugin(options = {}) {
|
|
|
17216
17875
|
pluginsFolder: getPluginsFolder(),
|
|
17217
17876
|
assetName: ASSET_NAME,
|
|
17218
17877
|
otherAssetName: OTHER_VARIANT,
|
|
17219
|
-
source:
|
|
17878
|
+
source: readFileSync8(bundled),
|
|
17220
17879
|
expectedVersion: packageVersion(),
|
|
17221
17880
|
expectedVariant: "inspector",
|
|
17222
17881
|
replaceVariant,
|