@quenty/studio-bridge 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +36 -8
- package/dist/src/bridge/bridge-connection.d.ts.map +1 -1
- package/dist/src/bridge/bridge-connection.js +1 -0
- package/dist/src/bridge/bridge-connection.js.map +1 -1
- package/dist/src/bridge/bridge-session.d.ts +11 -16
- package/dist/src/bridge/bridge-session.d.ts.map +1 -1
- package/dist/src/bridge/bridge-session.js +37 -29
- package/dist/src/bridge/bridge-session.js.map +1 -1
- package/dist/src/bridge/bridge-session.test.js +35 -0
- package/dist/src/bridge/bridge-session.test.js.map +1 -1
- package/dist/src/bridge/internal/bridge-host.d.ts +2 -0
- package/dist/src/bridge/internal/bridge-host.d.ts.map +1 -1
- package/dist/src/bridge/internal/bridge-host.js +1 -0
- package/dist/src/bridge/internal/bridge-host.js.map +1 -1
- package/dist/src/bridge/internal/host-protocol.d.ts.map +1 -1
- package/dist/src/bridge/internal/host-protocol.js +23 -10
- package/dist/src/bridge/internal/host-protocol.js.map +1 -1
- package/dist/src/bridge/types.d.ts +5 -2
- package/dist/src/bridge/types.d.ts.map +1 -1
- package/dist/src/bridge/types.js.map +1 -1
- package/dist/src/commands/console/exec/exec.d.ts +13 -3
- package/dist/src/commands/console/exec/exec.d.ts.map +1 -1
- package/dist/src/commands/console/exec/exec.js +82 -7
- package/dist/src/commands/console/exec/exec.js.map +1 -1
- package/dist/src/commands/console/exec/exec.test.js +63 -1
- package/dist/src/commands/console/exec/exec.test.js.map +1 -1
- package/dist/src/commands/framework/action-loader.test.js +1 -1
- package/dist/src/commands/framework/action-loader.test.js.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.d.ts.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.js +15 -93
- package/dist/src/commands/playtest/player/playtest-players.js.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.test.js +34 -21
- package/dist/src/commands/playtest/player/playtest-players.test.js.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.d.ts +19 -3
- package/dist/src/commands/playtest/playtest-sessions.d.ts.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.js +54 -74
- package/dist/src/commands/playtest/playtest-sessions.js.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.test.js +74 -8
- package/dist/src/commands/playtest/playtest-sessions.test.js.map +1 -1
- package/dist/src/commands/playtest/run/run.d.ts +9 -7
- package/dist/src/commands/playtest/run/run.d.ts.map +1 -1
- package/dist/src/commands/playtest/run/run.js +33 -38
- package/dist/src/commands/playtest/run/run.js.map +1 -1
- package/dist/src/commands/process/close/close.d.ts +4 -0
- package/dist/src/commands/process/close/close.d.ts.map +1 -1
- package/dist/src/commands/process/close/close.js +54 -4
- package/dist/src/commands/process/close/close.js.map +1 -1
- package/dist/src/commands/process/close/close.test.js +36 -3
- package/dist/src/commands/process/close/close.test.js.map +1 -1
- package/dist/src/commands/process/info/info.d.ts +15 -2
- package/dist/src/commands/process/info/info.d.ts.map +1 -1
- package/dist/src/commands/process/info/info.js +44 -4
- package/dist/src/commands/process/info/info.js.map +1 -1
- package/dist/src/commands/process/info/info.test.js +48 -0
- package/dist/src/commands/process/info/info.test.js.map +1 -1
- package/dist/src/commands/process/launch/launch.d.ts.map +1 -1
- package/dist/src/commands/process/launch/launch.js +22 -3
- package/dist/src/commands/process/launch/launch.js.map +1 -1
- package/dist/src/commands/process/launch/launch.test.js +27 -5
- package/dist/src/commands/process/launch/launch.test.js.map +1 -1
- package/dist/src/commands/process/list/list.d.ts +49 -6
- package/dist/src/commands/process/list/list.d.ts.map +1 -1
- package/dist/src/commands/process/list/list.js +185 -18
- package/dist/src/commands/process/list/list.js.map +1 -1
- package/dist/src/commands/process/list/list.test.js +92 -1
- package/dist/src/commands/process/list/list.test.js.map +1 -1
- package/dist/src/commands/process/run/run.d.ts +1 -1
- package/dist/src/commands/process/run/run.d.ts.map +1 -1
- package/dist/src/commands/process/run/run.js +3 -3
- package/dist/src/commands/process/run/run.js.map +1 -1
- package/dist/src/docker/docker-delegator.js +1 -1
- package/dist/src/docker/docker-delegator.js.map +1 -1
- package/dist/src/docker/docker-delegator.test.js +1 -1
- package/dist/src/docker/docker-delegator.test.js.map +1 -1
- package/dist/src/plugin/playtest-bridge.d.ts +36 -0
- package/dist/src/plugin/playtest-bridge.d.ts.map +1 -0
- package/dist/src/plugin/playtest-bridge.js +65 -0
- package/dist/src/plugin/playtest-bridge.js.map +1 -0
- package/dist/src/plugin/playtest-bridge.test.d.ts +2 -0
- package/dist/src/plugin/playtest-bridge.test.d.ts.map +1 -0
- package/dist/src/plugin/playtest-bridge.test.js +35 -0
- package/dist/src/plugin/playtest-bridge.test.js.map +1 -0
- package/dist/src/process/studio-launch-registry.d.ts +14 -0
- package/dist/src/process/studio-launch-registry.d.ts.map +1 -0
- package/dist/src/process/studio-launch-registry.js +37 -0
- package/dist/src/process/studio-launch-registry.js.map +1 -0
- package/dist/src/process/studio-launch-registry.test.d.ts +2 -0
- package/dist/src/process/studio-launch-registry.test.d.ts.map +1 -0
- package/dist/src/process/studio-launch-registry.test.js +38 -0
- package/dist/src/process/studio-launch-registry.test.js.map +1 -0
- package/dist/src/process/studio-process-closer.d.ts +3 -1
- package/dist/src/process/studio-process-closer.d.ts.map +1 -1
- package/dist/src/process/studio-process-closer.js +29 -11
- package/dist/src/process/studio-process-closer.js.map +1 -1
- package/dist/src/process/studio-process-identity.d.ts +10 -0
- package/dist/src/process/studio-process-identity.d.ts.map +1 -0
- package/dist/src/process/studio-process-identity.js +63 -0
- package/dist/src/process/studio-process-identity.js.map +1 -0
- package/dist/src/process/studio-process-identity.test.d.ts +2 -0
- package/dist/src/process/studio-process-identity.test.d.ts.map +1 -0
- package/dist/src/process/studio-process-identity.test.js +30 -0
- package/dist/src/process/studio-process-identity.test.js.map +1 -0
- package/dist/src/roblox-mcp/index.d.ts +3 -0
- package/dist/src/roblox-mcp/index.d.ts.map +1 -0
- package/dist/src/roblox-mcp/index.js +3 -0
- package/dist/src/roblox-mcp/index.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.d.ts +60 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.js +171 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.js +104 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.js +22 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.js +42 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.js.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.d.ts +34 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.d.ts.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.js +190 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.js.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.d.ts +2 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.js +123 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.d.ts +39 -0
- package/dist/src/roblox-mcp/studio/studio-session.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.js +175 -0
- package/dist/src/roblox-mcp/studio/studio-session.js.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.js +213 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.d.ts +19 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.js +121 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.js +107 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.d.ts +23 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.js +65 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.js +81 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.js.map +1 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.d.ts +27 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.d.ts.map +1 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.js +55 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.js.map +1 -0
- package/dist/src/server/web-socket-protocol.d.ts +50 -15
- package/dist/src/server/web-socket-protocol.d.ts.map +1 -1
- package/dist/src/server/web-socket-protocol.js +63 -13
- package/dist/src/server/web-socket-protocol.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/playtest-bridge.md +21 -0
- package/package.json +2 -2
- package/src/bridge/bridge-connection.ts +1 -0
- package/src/bridge/bridge-session.test.ts +51 -0
- package/src/bridge/bridge-session.ts +66 -47
- package/src/bridge/internal/bridge-host.ts +3 -0
- package/src/bridge/internal/host-protocol.ts +22 -9
- package/src/bridge/types.ts +5 -0
- package/src/commands/console/exec/exec.test.ts +94 -0
- package/src/commands/console/exec/exec.ts +130 -16
- package/src/commands/console/exec/execute.lua +147 -5
- package/src/commands/console/exec/execute.spec.lua +22 -2
- package/src/commands/framework/action-loader.test.ts +1 -1
- package/src/commands/playtest/player/playtest-players.lua +127 -0
- package/src/commands/playtest/player/playtest-players.spec.lua +48 -0
- package/src/commands/playtest/player/playtest-players.test.ts +40 -21
- package/src/commands/playtest/player/playtest-players.ts +30 -110
- package/src/commands/playtest/playtest-sessions.test.ts +116 -14
- package/src/commands/playtest/playtest-sessions.ts +77 -88
- package/src/commands/playtest/run/run.ts +44 -58
- package/src/commands/playtest/start/start-playtest.lua +105 -6
- package/src/commands/process/close/close.test.ts +52 -3
- package/src/commands/process/close/close.ts +78 -4
- package/src/commands/process/info/info.test.ts +76 -0
- package/src/commands/process/info/info.ts +74 -6
- package/src/commands/process/launch/launch.test.ts +33 -5
- package/src/commands/process/launch/launch.ts +41 -3
- package/src/commands/process/list/list.test.ts +130 -1
- package/src/commands/process/list/list.ts +290 -22
- package/src/commands/process/run/run.ts +4 -4
- package/src/docker/docker-delegator.test.ts +1 -1
- package/src/docker/docker-delegator.ts +1 -1
- package/src/plugin/playtest-bridge.test.ts +53 -0
- package/src/plugin/playtest-bridge.ts +98 -0
- package/src/process/studio-launch-registry.test.ts +56 -0
- package/src/process/studio-launch-registry.ts +63 -0
- package/src/process/studio-process-closer.ts +36 -11
- package/src/process/studio-process-identity.test.ts +45 -0
- package/src/process/studio-process-identity.ts +90 -0
- package/src/roblox-mcp/index.ts +17 -0
- package/src/roblox-mcp/mcp/mcp-stdio-client.test.ts +148 -0
- package/src/roblox-mcp/mcp/mcp-stdio-client.ts +270 -0
- package/src/roblox-mcp/mcp/repair-list-result.test.ts +52 -0
- package/src/roblox-mcp/mcp/repair-list-result.ts +23 -0
- package/src/roblox-mcp/roblox-studio-mcp.test.ts +168 -0
- package/src/roblox-mcp/roblox-studio-mcp.ts +330 -0
- package/src/roblox-mcp/studio/studio-session.test.ts +284 -0
- package/src/roblox-mcp/studio/studio-session.ts +279 -0
- package/src/roblox-mcp/studio-mcp-locator.test.ts +186 -0
- package/src/roblox-mcp/studio-mcp-locator.ts +180 -0
- package/src/roblox-mcp/studio-mcp-snapshot.test.ts +113 -0
- package/src/roblox-mcp/studio-mcp-snapshot.ts +104 -0
- package/src/roblox-mcp/test/fake-mcp-server.ts +74 -0
- package/src/server/web-socket-protocol.ts +119 -29
- package/templates/studio-bridge-plugin/playtest/StudioBridgePlaytest.client.lua +32 -0
- package/templates/studio-bridge-plugin/playtest/StudioBridgePlaytest.server.lua +40 -0
- package/templates/studio-bridge-plugin/src/Shared/ActionRouter.lua +25 -3
- package/templates/studio-bridge-plugin/src/Shared/PrecompiledSources.lua +47 -0
- package/templates/studio-bridge-plugin/src/Shared/PrecompiledSources.spec.lua +57 -0
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeClient.lua +357 -0
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeSessionUtils.lua +29 -3
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeSessionUtils.spec.lua +35 -2
- package/templates/studio-bridge-plugin/src/StudioBridgePlugin.server.lua +16 -292
- package/src/commands/playtest/run/playtest-runner.luau +0 -264
- package/src/commands/playtest/run/playtest-runner.spec.lua +0 -71
- package/src/commands/playtest/run/run-playtest-script.lua +0 -139
- package/src/commands/playtest/run/run-playtest-script.spec.lua +0 -43
|
@@ -14,7 +14,11 @@ vi.mock('../../../process/studio-process-closer.js', async (importOriginal) => {
|
|
|
14
14
|
import * as fs from 'fs/promises';
|
|
15
15
|
import * as os from 'os';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
closeAllStudiosAsync,
|
|
19
|
+
closeDeployTargetAsync,
|
|
20
|
+
processCloseHandlerAsync,
|
|
21
|
+
} from './close.js';
|
|
18
22
|
import {
|
|
19
23
|
closeStudioProcessAsync,
|
|
20
24
|
listStudioProcessesAsync,
|
|
@@ -124,13 +128,13 @@ describe('processCloseHandlerAsync', () => {
|
|
|
124
128
|
expect(closeStudioProcessAsync).toHaveBeenCalledWith(10, { force: true });
|
|
125
129
|
});
|
|
126
130
|
|
|
127
|
-
it('
|
|
131
|
+
it('says Studio is asking to save when it stays open', async () => {
|
|
128
132
|
vi.mocked(closeStudioProcessAsync).mockResolvedValue(false);
|
|
129
133
|
|
|
130
134
|
const result = await processCloseHandlerAsync(session);
|
|
131
135
|
|
|
132
136
|
expect(result.success).toBe(false);
|
|
133
|
-
expect(result.summary).toContain('
|
|
137
|
+
expect(result.summary).toContain('likely asking to save');
|
|
134
138
|
});
|
|
135
139
|
|
|
136
140
|
it('fails without closing when no window shows the place', async () => {
|
|
@@ -248,3 +252,48 @@ describe('closeDeployTargetAsync', () => {
|
|
|
248
252
|
expect(result).toMatchObject({ success: true, pid: 50 });
|
|
249
253
|
});
|
|
250
254
|
});
|
|
255
|
+
|
|
256
|
+
describe('closeAllStudiosAsync', () => {
|
|
257
|
+
beforeEach(() => {
|
|
258
|
+
vi.mocked(closeStudioProcessAsync).mockReset().mockResolvedValue(true);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('closes every running Studio without saving', async () => {
|
|
262
|
+
vi.mocked(listStudioProcessesAsync).mockResolvedValue([
|
|
263
|
+
{ pid: 10, windowTitle: 'Place1 - Roblox Studio' },
|
|
264
|
+
{ pid: 20, windowTitle: 'Place2 - Roblox Studio' },
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
const result = await closeAllStudiosAsync({ force: true });
|
|
268
|
+
|
|
269
|
+
expect(closeStudioProcessAsync).toHaveBeenCalledWith(10, { force: true });
|
|
270
|
+
expect(closeStudioProcessAsync).toHaveBeenCalledWith(20, { force: true });
|
|
271
|
+
expect(result).toMatchObject({ success: true, pids: [10, 20] });
|
|
272
|
+
expect(result.summary).toBe('Closed 2 Studios (pids 10, 20)');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('succeeds when no Studio is running', async () => {
|
|
276
|
+
vi.mocked(listStudioProcessesAsync).mockResolvedValue([]);
|
|
277
|
+
|
|
278
|
+
const result = await closeAllStudiosAsync({ force: true });
|
|
279
|
+
|
|
280
|
+
expect(closeStudioProcessAsync).not.toHaveBeenCalled();
|
|
281
|
+
expect(result).toMatchObject({ success: true, pids: [] });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('names the Studios that stayed open', async () => {
|
|
285
|
+
vi.mocked(listStudioProcessesAsync).mockResolvedValue([
|
|
286
|
+
{ pid: 10, windowTitle: 'Place1 - Roblox Studio' },
|
|
287
|
+
{ pid: 20, windowTitle: 'Place2 - Roblox Studio' },
|
|
288
|
+
]);
|
|
289
|
+
vi.mocked(closeStudioProcessAsync).mockImplementation(
|
|
290
|
+
async (pid) => pid === 10
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const result = await closeAllStudiosAsync({ force: false });
|
|
294
|
+
|
|
295
|
+
expect(result.success).toBe(false);
|
|
296
|
+
expect(result.pids).toEqual([10]);
|
|
297
|
+
expect(result.summary).toContain('still open (pids 20)');
|
|
298
|
+
});
|
|
299
|
+
});
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolveDeployPlaceAsync,
|
|
19
19
|
} from '../../../deploy/deploy-places.js';
|
|
20
20
|
import { explainMissingPlaytestContextAsync } from '../../playtest/playtest-sessions.js';
|
|
21
|
+
import { resolveSessionPidsAsync } from '../../../process/studio-process-identity.js';
|
|
21
22
|
import { describePlaytest } from '../info/info.js';
|
|
22
23
|
|
|
23
24
|
export interface ProcessCloseResult {
|
|
@@ -26,6 +27,7 @@ export interface ProcessCloseResult {
|
|
|
26
27
|
/** The `deploy.nevermore.json` target whose Studio was closed, if one was named. */
|
|
27
28
|
deployTarget?: string;
|
|
28
29
|
pid?: number;
|
|
30
|
+
pids?: number[];
|
|
29
31
|
summary: string;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -36,6 +38,8 @@ export interface ProcessCloseOptions {
|
|
|
36
38
|
|
|
37
39
|
interface ProcessCloseArgs {
|
|
38
40
|
deployTarget?: string;
|
|
41
|
+
all?: boolean;
|
|
42
|
+
askToSave?: boolean;
|
|
39
43
|
force?: boolean;
|
|
40
44
|
pid?: number;
|
|
41
45
|
}
|
|
@@ -74,6 +78,15 @@ export async function processCloseHandlerAsync(
|
|
|
74
78
|
?.placeId;
|
|
75
79
|
candidates = matchStudioProcesses(processes, placeName, placeId);
|
|
76
80
|
}
|
|
81
|
+
if (candidates.length > 1 && options.pid === undefined && connection) {
|
|
82
|
+
const pid = (
|
|
83
|
+
await resolveSessionPidsAsync(connection, processes, [instanceId])
|
|
84
|
+
).get(instanceId);
|
|
85
|
+
const own = candidates.filter((proc) => proc.pid === pid);
|
|
86
|
+
if (own.length === 1) {
|
|
87
|
+
candidates = own;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
77
90
|
|
|
78
91
|
return closeCandidateAsync(candidates, `'${placeName}'`, options, {
|
|
79
92
|
sessionId,
|
|
@@ -129,6 +142,47 @@ export async function closeDeployTargetAsync(
|
|
|
129
142
|
return closeCandidateAsync(candidates, showing, options, base);
|
|
130
143
|
}
|
|
131
144
|
|
|
145
|
+
export async function closeAllStudiosAsync(
|
|
146
|
+
options: Pick<ProcessCloseOptions, 'force'> = {}
|
|
147
|
+
): Promise<ProcessCloseResult> {
|
|
148
|
+
const processes = await listStudioProcessesAsync();
|
|
149
|
+
if (processes.length === 0) {
|
|
150
|
+
return { success: true, pids: [], summary: 'No Studio is running' };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const results = await Promise.all(
|
|
154
|
+
processes.map(async ({ pid }) => ({
|
|
155
|
+
pid,
|
|
156
|
+
exited: await closeStudioProcessAsync(pid, { force: options.force }),
|
|
157
|
+
}))
|
|
158
|
+
);
|
|
159
|
+
const closed = results.filter((r) => r.exited).map((r) => r.pid);
|
|
160
|
+
const open = results.filter((r) => !r.exited).map((r) => r.pid);
|
|
161
|
+
const plural = (n: number) => (n === 1 ? 'Studio' : 'Studios');
|
|
162
|
+
|
|
163
|
+
if (open.length > 0) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
pids: closed,
|
|
167
|
+
summary:
|
|
168
|
+
`Closed ${closed.length} ${plural(closed.length)}; ${
|
|
169
|
+
open.length
|
|
170
|
+
} ${plural(open.length)} still open (pids ${open.join(', ')})` +
|
|
171
|
+
(options.force
|
|
172
|
+
? ''
|
|
173
|
+
: ', likely asking to save. Run without --ask-to-save to close without saving.'),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
success: true,
|
|
179
|
+
pids: closed,
|
|
180
|
+
summary: `Closed ${closed.length} ${plural(
|
|
181
|
+
closed.length
|
|
182
|
+
)} (pids ${closed.join(', ')})`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
132
186
|
async function closeCandidateAsync(
|
|
133
187
|
candidates: RunningStudioProcess[],
|
|
134
188
|
showing: string,
|
|
@@ -163,7 +217,7 @@ async function closeCandidateAsync(
|
|
|
163
217
|
return fail(
|
|
164
218
|
options.force
|
|
165
219
|
? `Studio (pid ${pid}) did not exit`
|
|
166
|
-
: `Studio (pid ${pid}) is still open, likely asking to save.
|
|
220
|
+
: `Studio (pid ${pid}) is still open, likely asking to save. Run without --ask-to-save to close without saving.`,
|
|
167
221
|
pid
|
|
168
222
|
);
|
|
169
223
|
}
|
|
@@ -183,7 +237,7 @@ export const processCloseCommand = defineCommand<
|
|
|
183
237
|
group: 'process',
|
|
184
238
|
name: 'close',
|
|
185
239
|
description:
|
|
186
|
-
"Close a Studio: the one a session belongs to,
|
|
240
|
+
"Close a Studio without saving: the one a session belongs to, the one showing a deploy.nevermore.json target's place, or with --all every Studio",
|
|
187
241
|
category: 'infrastructure',
|
|
188
242
|
safety: 'mutate',
|
|
189
243
|
scope: 'connection',
|
|
@@ -193,8 +247,16 @@ export const processCloseCommand = defineCommand<
|
|
|
193
247
|
'deploy.nevermore.json target whose Studio to close, e.g. test or integration.places.hub',
|
|
194
248
|
required: false,
|
|
195
249
|
}),
|
|
250
|
+
all: arg.flag({
|
|
251
|
+
description: 'Close every running Studio, connected or not',
|
|
252
|
+
}),
|
|
253
|
+
askToSave: arg.flag({
|
|
254
|
+
description:
|
|
255
|
+
'Let Studio prompt to save first, instead of closing without saving',
|
|
256
|
+
}),
|
|
196
257
|
force: arg.flag({
|
|
197
|
-
description:
|
|
258
|
+
description:
|
|
259
|
+
'Close without saving. The default, accepted for scripts that pass it',
|
|
198
260
|
alias: 'f',
|
|
199
261
|
}),
|
|
200
262
|
pid: arg.option({
|
|
@@ -203,7 +265,19 @@ export const processCloseCommand = defineCommand<
|
|
|
203
265
|
}),
|
|
204
266
|
},
|
|
205
267
|
handler: async (connection, args, targeting) => {
|
|
206
|
-
const options = { force: args.
|
|
268
|
+
const options = { force: !args.askToSave, pid: args.pid };
|
|
269
|
+
if (args.all) {
|
|
270
|
+
if (
|
|
271
|
+
args.deployTarget !== undefined ||
|
|
272
|
+
args.pid !== undefined ||
|
|
273
|
+
targeting.target !== undefined
|
|
274
|
+
) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
'--all closes every Studio; drop the target, --target and --pid.'
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return closeAllStudiosAsync(options);
|
|
280
|
+
}
|
|
207
281
|
if (args.deployTarget !== undefined) {
|
|
208
282
|
return closeDeployTargetAsync(connection, args.deployTarget, options);
|
|
209
283
|
}
|
|
@@ -228,3 +228,79 @@ describe('describePlaytest', () => {
|
|
|
228
228
|
expect(playtest?.sessions).toHaveLength(3);
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
describe('queryStateHandlerAsync with MCP', () => {
|
|
233
|
+
const ok = (studios: any[]) => async () => ({
|
|
234
|
+
status: 'ok' as const,
|
|
235
|
+
studios,
|
|
236
|
+
checkedAt: Date.now(),
|
|
237
|
+
latencyMs: 500,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('reports the studio_id MCP reaches this Studio by', async () => {
|
|
241
|
+
const session = createMockSession({});
|
|
242
|
+
|
|
243
|
+
const result = await queryStateHandlerAsync(session, undefined, {
|
|
244
|
+
mcpSnapshotAsync: ok([
|
|
245
|
+
{
|
|
246
|
+
studioId: 'mcp-1',
|
|
247
|
+
placeName: 'TestPlace',
|
|
248
|
+
sessionId: 'test-place-3f9a-edit',
|
|
249
|
+
},
|
|
250
|
+
]),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(result.mcp).toEqual({ status: 'ok', studioId: 'mcp-1' });
|
|
254
|
+
expect(formatStateText(result)).toContain(
|
|
255
|
+
'MCP: reachable (studio_id mcp-1)'
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('says when MCP does not see this Studio', async () => {
|
|
260
|
+
const result = await queryStateHandlerAsync(
|
|
261
|
+
createMockSession({}),
|
|
262
|
+
undefined,
|
|
263
|
+
{
|
|
264
|
+
mcpSnapshotAsync: ok([]),
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(formatStateText(result)).toContain(
|
|
269
|
+
'MCP: not seen by its MCP server'
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('reports MCP being unavailable without failing the rest', async () => {
|
|
274
|
+
const result = await queryStateHandlerAsync(
|
|
275
|
+
createMockSession({}),
|
|
276
|
+
undefined,
|
|
277
|
+
{
|
|
278
|
+
mcpSnapshotAsync: async () => ({
|
|
279
|
+
status: 'timeout' as const,
|
|
280
|
+
studios: [],
|
|
281
|
+
message: 'tools/call timed out',
|
|
282
|
+
checkedAt: Date.now(),
|
|
283
|
+
latencyMs: 3000,
|
|
284
|
+
}),
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
expect(result.placeId).toBe(12345);
|
|
289
|
+
expect(formatStateText(result)).toContain(
|
|
290
|
+
'MCP: timeout (tools/call timed out)'
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('leaves MCP out when the check gave up', async () => {
|
|
295
|
+
const result = await queryStateHandlerAsync(
|
|
296
|
+
createMockSession({}),
|
|
297
|
+
undefined,
|
|
298
|
+
{
|
|
299
|
+
mcpSnapshotAsync: async () => undefined,
|
|
300
|
+
}
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(result.mcp).toBeUndefined();
|
|
304
|
+
expect(formatStateText(result)).not.toContain('MCP:');
|
|
305
|
+
});
|
|
306
|
+
});
|
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
} from '../../../process/studio-process-closer.js';
|
|
22
22
|
import { tryGetStudioWindowStateAsync } from '../../../process/studio-window-state.js';
|
|
23
23
|
import { getPluginBuildVersionAsync } from '../../../plugin/plugin-build-version.js';
|
|
24
|
+
import { arg } from '../../framework/arg-builder.js';
|
|
25
|
+
import {
|
|
26
|
+
getStudioMcpSnapshotAsync,
|
|
27
|
+
type StudioMcpSnapshot,
|
|
28
|
+
} from '../../../roblox-mcp/index.js';
|
|
24
29
|
|
|
25
30
|
export interface PlaytestInfo {
|
|
26
31
|
mode: 'play' | 'run';
|
|
@@ -65,13 +70,25 @@ export interface StateResult {
|
|
|
65
70
|
focused: boolean;
|
|
66
71
|
minimized: boolean;
|
|
67
72
|
};
|
|
73
|
+
mcp?: {
|
|
74
|
+
status: StudioMcpSnapshot['status'];
|
|
75
|
+
studioId?: string;
|
|
76
|
+
message?: string;
|
|
77
|
+
};
|
|
68
78
|
connectedAt: string;
|
|
69
79
|
summary: string;
|
|
70
80
|
}
|
|
71
81
|
|
|
82
|
+
export interface QueryStateOptions {
|
|
83
|
+
mcpSnapshotAsync?: (
|
|
84
|
+
sessions: SessionInfo[]
|
|
85
|
+
) => Promise<StudioMcpSnapshot | undefined>;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
export async function queryStateHandlerAsync(
|
|
73
89
|
session: BridgeSession,
|
|
74
|
-
connection?: BridgeConnection
|
|
90
|
+
connection?: BridgeConnection,
|
|
91
|
+
options: QueryStateOptions = {}
|
|
75
92
|
): Promise<StateResult> {
|
|
76
93
|
// Listed sessions carry the edit session's place name for a playtest
|
|
77
94
|
const listed = connection
|
|
@@ -84,9 +101,10 @@ export async function queryStateHandlerAsync(
|
|
|
84
101
|
// playtest shows in the edit window
|
|
85
102
|
const ownWindow =
|
|
86
103
|
session.info.context !== 'edit' && playtest?.playerCount !== undefined;
|
|
87
|
-
const [result, expectedVersion] = await Promise.all([
|
|
104
|
+
const [result, expectedVersion, snapshot] = await Promise.all([
|
|
88
105
|
session.queryStateAsync(),
|
|
89
106
|
getPluginBuildVersionAsync(),
|
|
107
|
+
options.mcpSnapshotAsync?.(connection?.listSessions() ?? [session.info]),
|
|
90
108
|
]);
|
|
91
109
|
const window = await tryGetWindowAsync(
|
|
92
110
|
ownWindow ? [result.placeName] : [result.placeName, listed?.placeName],
|
|
@@ -111,6 +129,7 @@ export async function queryStateHandlerAsync(
|
|
|
111
129
|
},
|
|
112
130
|
playtest,
|
|
113
131
|
window,
|
|
132
|
+
...(snapshot ? { mcp: describeMcp(snapshot, session.info) } : {}),
|
|
114
133
|
connectedAt: new Date(session.info.connectedAt).toISOString(),
|
|
115
134
|
summary: `Mode: ${result.state}, Place: ${result.placeName} (${result.placeId})`,
|
|
116
135
|
};
|
|
@@ -166,6 +185,36 @@ async function tryGetWindowAsync(
|
|
|
166
185
|
return undefined;
|
|
167
186
|
}
|
|
168
187
|
|
|
188
|
+
function describeMcp(
|
|
189
|
+
snapshot: StudioMcpSnapshot,
|
|
190
|
+
info: SessionInfo
|
|
191
|
+
): NonNullable<StateResult['mcp']> {
|
|
192
|
+
if (snapshot.status !== 'ok') {
|
|
193
|
+
return {
|
|
194
|
+
status: snapshot.status,
|
|
195
|
+
...(snapshot.message ? { message: snapshot.message } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const editSessionId =
|
|
199
|
+
info.context === 'edit' ? info.sessionId : `${info.instanceId}-edit`;
|
|
200
|
+
const studio = snapshot.studios.find((s) => s.sessionId === editSessionId);
|
|
201
|
+
return studio
|
|
202
|
+
? { status: 'ok', studioId: studio.studioId }
|
|
203
|
+
: { status: 'ok', message: "Studio's MCP server does not see this Studio" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function formatMcp(mcp: StateResult['mcp']): string | undefined {
|
|
207
|
+
if (!mcp || mcp.status === 'unsupported') return undefined;
|
|
208
|
+
if (mcp.status === 'ok') {
|
|
209
|
+
return mcp.studioId
|
|
210
|
+
? `reachable (studio_id ${mcp.studioId})`
|
|
211
|
+
: 'not seen by its MCP server';
|
|
212
|
+
}
|
|
213
|
+
return OutputHelper.formatWarning(
|
|
214
|
+
`${mcp.status}${mcp.message ? ` (${mcp.message})` : ''}`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
169
218
|
function formatAge(isoTime: string): string {
|
|
170
219
|
const seconds = Math.max(
|
|
171
220
|
0,
|
|
@@ -223,10 +272,18 @@ export function formatStateText(result: StateResult): string {
|
|
|
223
272
|
)
|
|
224
273
|
}`
|
|
225
274
|
);
|
|
275
|
+
const mcp = formatMcp(result.mcp);
|
|
276
|
+
if (mcp) {
|
|
277
|
+
lines.push(`MCP: ${mcp}`);
|
|
278
|
+
}
|
|
226
279
|
return lines.join('\n');
|
|
227
280
|
}
|
|
228
281
|
|
|
229
|
-
|
|
282
|
+
interface InfoArgs {
|
|
283
|
+
mcp?: boolean;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export const infoCommand = defineCommand<InfoArgs, StateResult>({
|
|
230
287
|
group: 'process',
|
|
231
288
|
name: 'info',
|
|
232
289
|
description:
|
|
@@ -234,10 +291,21 @@ export const infoCommand = defineCommand({
|
|
|
234
291
|
category: 'execution',
|
|
235
292
|
safety: 'read',
|
|
236
293
|
scope: 'session',
|
|
237
|
-
args: {
|
|
294
|
+
args: {
|
|
295
|
+
mcp: arg.flag({
|
|
296
|
+
description:
|
|
297
|
+
"Also ask Studio's MCP server whether it can reach this Studio (--no-mcp to skip)",
|
|
298
|
+
default: true,
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
238
301
|
cli: {
|
|
239
302
|
format: formatStateText,
|
|
240
303
|
},
|
|
241
|
-
handler: async (session,
|
|
242
|
-
queryStateHandlerAsync(session, connection
|
|
304
|
+
handler: async (session, args, connection) =>
|
|
305
|
+
queryStateHandlerAsync(session, connection, {
|
|
306
|
+
mcpSnapshotAsync:
|
|
307
|
+
args.mcp && !connection?.isRemote
|
|
308
|
+
? (sessions) => getStudioMcpSnapshotAsync({ sessions })
|
|
309
|
+
: undefined,
|
|
310
|
+
}),
|
|
243
311
|
});
|
|
@@ -15,8 +15,13 @@ vi.mock('../../../process/studio-process-manager.js', () => ({
|
|
|
15
15
|
}),
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
|
+
vi.mock('../../../process/studio-launch-registry.js', () => ({
|
|
19
|
+
recordStudioLaunchAsync: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
}));
|
|
21
|
+
|
|
18
22
|
import { launchHandlerAsync } from './launch.js';
|
|
19
23
|
import { launchStudioAsync } from '../../../process/studio-process-manager.js';
|
|
24
|
+
import { recordStudioLaunchAsync } from '../../../process/studio-launch-registry.js';
|
|
20
25
|
|
|
21
26
|
let tempDir: string;
|
|
22
27
|
|
|
@@ -44,12 +49,32 @@ const place = (placeId: number, name?: string) => ({
|
|
|
44
49
|
});
|
|
45
50
|
|
|
46
51
|
describe('launchHandlerAsync', () => {
|
|
47
|
-
it('
|
|
52
|
+
it('opens a new Baseplate outside any deploy.nevermore.json', async () => {
|
|
48
53
|
const result = await launchHandlerAsync({ cwd: tempDir });
|
|
49
54
|
|
|
50
|
-
expect(launchStudioAsync).toHaveBeenCalledWith({
|
|
55
|
+
expect(launchStudioAsync).toHaveBeenCalledWith({
|
|
56
|
+
placeId: 95206881,
|
|
57
|
+
universeId: 28220420,
|
|
58
|
+
});
|
|
51
59
|
expect(result.launched).toBe(true);
|
|
52
|
-
expect(result.summary).toBe('Studio launched');
|
|
60
|
+
expect(result.summary).toBe('Studio launched with a new Baseplate');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('records where a Baseplate was launched from', async () => {
|
|
64
|
+
vi.mocked(launchStudioAsync).mockResolvedValueOnce({
|
|
65
|
+
process: { pid: 4242 },
|
|
66
|
+
killAsync: vi.fn(),
|
|
67
|
+
} as any);
|
|
68
|
+
|
|
69
|
+
await launchHandlerAsync({ cwd: tempDir });
|
|
70
|
+
|
|
71
|
+
expect(recordStudioLaunchAsync).toHaveBeenCalledWith(
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
pid: 4242,
|
|
74
|
+
cwd: tempDir,
|
|
75
|
+
opened: 'a new Baseplate',
|
|
76
|
+
})
|
|
77
|
+
);
|
|
53
78
|
});
|
|
54
79
|
|
|
55
80
|
it('passes place path to launchStudioAsync', async () => {
|
|
@@ -163,8 +188,11 @@ describe('launchHandlerAsync with deploy.nevermore.json', () => {
|
|
|
163
188
|
|
|
164
189
|
const result = await launchHandlerAsync({ cwd: tempDir });
|
|
165
190
|
|
|
166
|
-
expect(launchStudioAsync).toHaveBeenCalledWith({
|
|
167
|
-
|
|
191
|
+
expect(launchStudioAsync).toHaveBeenCalledWith({
|
|
192
|
+
placeId: 95206881,
|
|
193
|
+
universeId: 28220420,
|
|
194
|
+
});
|
|
195
|
+
expect(result.summary).toBe('Studio launched with a new Baseplate');
|
|
168
196
|
});
|
|
169
197
|
|
|
170
198
|
it('asks for a target when the config has no default', async () => {
|
|
@@ -7,11 +7,14 @@ import * as path from 'path';
|
|
|
7
7
|
import { defineCommand } from '../../framework/define-command.js';
|
|
8
8
|
import { arg } from '../../framework/arg-builder.js';
|
|
9
9
|
import { launchStudioAsync } from '../../../process/studio-process-manager.js';
|
|
10
|
+
import { recordStudioLaunchAsync } from '../../../process/studio-launch-registry.js';
|
|
10
11
|
import {
|
|
11
12
|
DEPLOY_CONFIG_FILE_NAME,
|
|
12
13
|
resolveDeployPlaceAsync,
|
|
13
14
|
} from '../../../deploy/deploy-places.js';
|
|
14
15
|
|
|
16
|
+
const BASEPLATE_TEMPLATE = { placeId: 95206881, universeId: 28220420 };
|
|
17
|
+
|
|
15
18
|
export interface LaunchOptions {
|
|
16
19
|
placePath?: string;
|
|
17
20
|
placeId?: number;
|
|
@@ -50,7 +53,11 @@ export async function launchHandlerAsync(
|
|
|
50
53
|
);
|
|
51
54
|
if (deployPlace) {
|
|
52
55
|
const { placeId, universeId } = deployPlace.place;
|
|
53
|
-
await
|
|
56
|
+
await _launchAndRecordAsync(
|
|
57
|
+
{ placeId, universeId },
|
|
58
|
+
`'${deployPlace.selector}' (place ${placeId})`,
|
|
59
|
+
options.cwd
|
|
60
|
+
);
|
|
54
61
|
const configDir = path.relative(
|
|
55
62
|
options.cwd ?? process.cwd(),
|
|
56
63
|
path.dirname(deployPlace.configPath)
|
|
@@ -69,8 +76,24 @@ export async function launchHandlerAsync(
|
|
|
69
76
|
};
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
if (!hasExplicitPlace) {
|
|
80
|
+
await _launchAndRecordAsync(
|
|
81
|
+
BASEPLATE_TEMPLATE,
|
|
82
|
+
'a new Baseplate',
|
|
83
|
+
options.cwd
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
launched: true,
|
|
87
|
+
summary: 'Studio launched with a new Baseplate',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
72
91
|
const { placePath, placeId, universeId } = options;
|
|
73
|
-
await
|
|
92
|
+
await _launchAndRecordAsync(
|
|
93
|
+
{ placePath, placeId, universeId },
|
|
94
|
+
placeId != null ? `place ${placeId}` : placePath ?? 'Studio',
|
|
95
|
+
options.cwd
|
|
96
|
+
);
|
|
74
97
|
const opened =
|
|
75
98
|
placeId != null
|
|
76
99
|
? `place ${placeId}${
|
|
@@ -85,6 +108,21 @@ export async function launchHandlerAsync(
|
|
|
85
108
|
};
|
|
86
109
|
}
|
|
87
110
|
|
|
111
|
+
async function _launchAndRecordAsync(
|
|
112
|
+
launch: Parameters<typeof launchStudioAsync>[0],
|
|
113
|
+
opened: string,
|
|
114
|
+
cwd = process.cwd()
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const launchedAt = Date.now();
|
|
117
|
+
const studio = await launchStudioAsync(launch);
|
|
118
|
+
await recordStudioLaunchAsync({
|
|
119
|
+
pid: studio?.process?.pid,
|
|
120
|
+
cwd,
|
|
121
|
+
opened,
|
|
122
|
+
launchedAt,
|
|
123
|
+
}).catch(() => {});
|
|
124
|
+
}
|
|
125
|
+
|
|
88
126
|
interface LaunchArgs {
|
|
89
127
|
deployTarget?: string;
|
|
90
128
|
place?: string;
|
|
@@ -96,7 +134,7 @@ export const launchCommand = defineCommand<LaunchArgs, LaunchResult>({
|
|
|
96
134
|
group: 'process',
|
|
97
135
|
name: 'launch',
|
|
98
136
|
description:
|
|
99
|
-
"Launch Roblox Studio, by default with the place of the nearest deploy.nevermore.json's default target",
|
|
137
|
+
"Launch Roblox Studio, by default with the place of the nearest deploy.nevermore.json's default target, or else a new Baseplate",
|
|
100
138
|
category: 'infrastructure',
|
|
101
139
|
safety: 'none',
|
|
102
140
|
scope: 'standalone',
|