@quenty/studio-bridge 0.15.0 → 0.16.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/studio-bridge",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "WebSocket-based bridge for running Luau scripts in Roblox Studio",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -39,5 +39,5 @@
39
39
  "test:plugin": "lune run templates/studio-bridge-plugin-test/test/test-runner",
40
40
  "test:integration": "node dist/test/integration/smoke-test.js"
41
41
  },
42
- "gitHead": "adebe068574c67bcef060e24600cb6e6f65bcbe7"
42
+ "gitHead": "0a4c7f67c4bd89a16d507c3ce2dd556291f16f6b"
43
43
  }
@@ -29,15 +29,38 @@ describe('launchHandlerAsync', () => {
29
29
  placePath: '/tmp/game.rbxl',
30
30
  });
31
31
 
32
- expect(launchStudioAsync).toHaveBeenCalledWith('/tmp/game.rbxl');
32
+ expect(launchStudioAsync).toHaveBeenCalledWith({
33
+ placePath: '/tmp/game.rbxl',
34
+ });
33
35
  expect(result.launched).toBe(true);
34
36
  expect(result.summary).toBe('Studio launched with /tmp/game.rbxl');
35
37
  });
36
38
 
37
- it('passes empty string when no place path', async () => {
39
+ it('passes cloud place + universe ids to launchStudioAsync', async () => {
40
+ const result = await launchHandlerAsync({
41
+ placeId: 84760113521251,
42
+ universeId: 9893751595,
43
+ });
44
+
45
+ expect(launchStudioAsync).toHaveBeenCalledWith({
46
+ placeId: 84760113521251,
47
+ universeId: 9893751595,
48
+ });
49
+ expect(result.summary).toBe(
50
+ 'Studio launched with place 84760113521251 (universe 9893751595)'
51
+ );
52
+ });
53
+
54
+ it('summarizes a cloud place id without a universe id', async () => {
55
+ const result = await launchHandlerAsync({ placeId: 123 });
56
+
57
+ expect(result.summary).toBe('Studio launched with place 123');
58
+ });
59
+
60
+ it('passes an empty options object when nothing is specified', async () => {
38
61
  await launchHandlerAsync({});
39
62
 
40
- expect(launchStudioAsync).toHaveBeenCalledWith('');
63
+ expect(launchStudioAsync).toHaveBeenCalledWith({});
41
64
  });
42
65
 
43
66
  it('returns correct summary without place path', async () => {
@@ -8,6 +8,8 @@ import { launchStudioAsync } from '../../../process/studio-process-manager.js';
8
8
 
9
9
  export interface LaunchOptions {
10
10
  placePath?: string;
11
+ placeId?: number;
12
+ universeId?: number;
11
13
  }
12
14
 
13
15
  export interface LaunchResult {
@@ -18,17 +20,23 @@ export interface LaunchResult {
18
20
  export async function launchHandlerAsync(
19
21
  options: LaunchOptions = {}
20
22
  ): Promise<LaunchResult> {
21
- await launchStudioAsync(options.placePath ?? '');
23
+ await launchStudioAsync(options);
24
+ const target =
25
+ options.placeId != null
26
+ ? `place ${options.placeId}${
27
+ options.universeId != null ? ` (universe ${options.universeId})` : ''
28
+ }`
29
+ : options.placePath;
22
30
  return {
23
31
  launched: true,
24
- summary: `Studio launched${
25
- options.placePath ? ` with ${options.placePath}` : ''
26
- }`,
32
+ summary: `Studio launched${target ? ` with ${target}` : ''}`,
27
33
  };
28
34
  }
29
35
 
30
36
  interface LaunchArgs {
31
37
  place?: string;
38
+ placeId?: number;
39
+ universeId?: number;
32
40
  }
33
41
 
34
42
  export const launchCommand = defineCommand<LaunchArgs, LaunchResult>({
@@ -40,6 +48,21 @@ export const launchCommand = defineCommand<LaunchArgs, LaunchResult>({
40
48
  scope: 'standalone',
41
49
  args: {
42
50
  place: arg.option({ description: 'Path to a .rbxl place file' }),
51
+ placeId: arg.option({
52
+ description:
53
+ 'Cloud place id to open in Studio (edit mode, via the EditPlace deep-link)',
54
+ type: 'number',
55
+ }),
56
+ universeId: arg.option({
57
+ description:
58
+ 'Cloud universe id for the place (recommended with --place-id)',
59
+ type: 'number',
60
+ }),
43
61
  },
44
- handler: async (args) => launchHandlerAsync({ placePath: args.place }),
62
+ handler: async (args) =>
63
+ launchHandlerAsync({
64
+ placePath: args.place,
65
+ placeId: args.placeId,
66
+ universeId: args.universeId,
67
+ }),
45
68
  });
@@ -1,5 +1,40 @@
1
1
  import { describe, it, expect, afterEach } from 'vitest';
2
- import { findPluginsFolder } from './studio-process-manager.js';
2
+ import {
3
+ findPluginsFolder,
4
+ buildStudioLaunchArg,
5
+ } from './studio-process-manager.js';
6
+
7
+ describe('buildStudioLaunchArg', () => {
8
+ it('builds an EditPlace deep-link from place + universe ids', () => {
9
+ expect(
10
+ buildStudioLaunchArg({ placeId: 84760113521251, universeId: 9893751595 })
11
+ ).toBe(
12
+ 'roblox-studio:1+launchmode:edit+task:EditPlace+placeId:84760113521251+universeId:9893751595'
13
+ );
14
+ });
15
+
16
+ it('omits universeId from the deep-link when not provided', () => {
17
+ expect(buildStudioLaunchArg({ placeId: 123 })).toBe(
18
+ 'roblox-studio:1+launchmode:edit+task:EditPlace+placeId:123'
19
+ );
20
+ });
21
+
22
+ it('prefers the cloud place id over a local place path', () => {
23
+ expect(
24
+ buildStudioLaunchArg({ placeId: 5, placePath: '/tmp/game.rbxl' })
25
+ ).toContain('placeId:5');
26
+ });
27
+
28
+ it('returns the local place path when no place id is given', () => {
29
+ expect(buildStudioLaunchArg({ placePath: '/tmp/game.rbxl' })).toBe(
30
+ '/tmp/game.rbxl'
31
+ );
32
+ });
33
+
34
+ it('returns an empty string for no place at all', () => {
35
+ expect(buildStudioLaunchArg({})).toBe('');
36
+ });
37
+ });
3
38
 
4
39
  describe('findPluginsFolder', () => {
5
40
  const originalPlatform = process.platform;
@@ -125,24 +125,63 @@ export interface StudioProcess {
125
125
  killAsync: () => Promise<void>;
126
126
  }
127
127
 
128
+ export interface LaunchStudioOptions {
129
+ /** Path to a local `.rbxl` place file to open. */
130
+ placePath?: string;
131
+ /** Cloud place id to open in Studio (via the `EditPlace` deep-link). */
132
+ placeId?: number;
133
+ /** Cloud universe id for the place (recommended alongside `placeId`). */
134
+ universeId?: number;
135
+ }
136
+
128
137
  /**
129
- * Launch Roblox Studio with the given place file.
138
+ * Resolve the single argument handed to `RobloxStudioBeta`: a cloud
139
+ * `roblox-studio:` EditPlace deep-link when a `placeId` is given, otherwise
140
+ * the local `.rbxl` path (or `''` to open Studio with no place). The deep-link
141
+ * is exactly what the OS protocol handler passes to Studio, so Studio opens the
142
+ * cloud place in edit mode using the current logged-in session.
143
+ */
144
+ export function buildStudioLaunchArg(options: LaunchStudioOptions): string {
145
+ if (options.placeId != null) {
146
+ const parts = [
147
+ 'roblox-studio:1',
148
+ 'launchmode:edit',
149
+ 'task:EditPlace',
150
+ `placeId:${options.placeId}`,
151
+ ];
152
+ if (options.universeId != null) {
153
+ parts.push(`universeId:${options.universeId}`);
154
+ }
155
+ return parts.join('+');
156
+ }
157
+ return options.placePath ?? '';
158
+ }
159
+
160
+ /**
161
+ * Launch Roblox Studio with a local place file or a cloud place.
162
+ *
163
+ * Accepts a `LaunchStudioOptions` object (`{ placePath | placeId, universeId }`)
164
+ * or, for backward compatibility, a bare `.rbxl` path string.
130
165
  *
131
166
  * Uses Node's built-in `spawn` with `detached: true` + `unref()` so that
132
167
  * Studio survives after the CLI process exits. execa's internal Job Object
133
168
  * on Windows kills children on parent exit, so we avoid it here.
134
169
  */
135
170
  export async function launchStudioAsync(
136
- placePath: string
171
+ options: LaunchStudioOptions | string = {}
137
172
  ): Promise<StudioProcess> {
173
+ const resolved: LaunchStudioOptions =
174
+ typeof options === 'string' ? { placePath: options } : options;
175
+ const launchArg = buildStudioLaunchArg(resolved);
176
+
138
177
  if (process.platform === 'linux') {
139
- return launchStudioLinuxAsync(placePath);
178
+ return launchStudioLinuxAsync(launchArg);
140
179
  }
141
180
 
142
181
  const studioExe = await findStudioPathAsync();
143
- OutputHelper.verbose(`[StudioBridge] ${studioExe} "${placePath}"`);
182
+ OutputHelper.verbose(`[StudioBridge] ${studioExe} "${launchArg}"`);
144
183
 
145
- const proc = spawn(studioExe, placePath ? [placePath] : [], {
184
+ const proc = spawn(studioExe, launchArg ? [launchArg] : [], {
146
185
  detached: true,
147
186
  stdio: 'ignore',
148
187
  });