dominus-cli 2.2.0 → 2.2.1

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.
@@ -1,77 +1,547 @@
1
1
  #!/usr/bin/env node
2
- import fs3 from 'fs';
2
+ import fs4 from 'fs';
3
3
  import os2 from 'os';
4
- import path3 from 'path';
4
+ import path4 from 'path';
5
5
  import { fileURLToPath } from 'url';
6
+ import { spawn } from 'child_process';
7
+ import { EventEmitter } from 'events';
8
+ import { WebSocket } from 'ws';
9
+ import { nanoid } from 'nanoid';
10
+ import { z } from 'zod';
6
11
  import * as crypto from 'crypto';
7
12
 
8
- var DOMINUS_DIR = path3.join(os2.homedir(), ".dominus");
9
- path3.join(DOMINUS_DIR, "config.json");
10
- path3.join(DOMINUS_DIR, "dominus.db");
11
- path3.join(DOMINUS_DIR, "rules.json");
13
+ var DOMINUS_DIR = path4.join(os2.homedir(), ".dominus");
14
+ var CONFIG_PATH = path4.join(DOMINUS_DIR, "config.json");
15
+ path4.join(DOMINUS_DIR, "dominus.db");
16
+ path4.join(DOMINUS_DIR, "rules.json");
17
+ var DEFAULTS = {
18
+ provider: "openai",
19
+ apiBaseUrl: "",
20
+ model: "",
21
+ port: 18088,
22
+ maxToolIterations: 25,
23
+ maxVerifyRetries: 3,
24
+ tokenBudget: 128e3,
25
+ autoIndex: true,
26
+ autoLearn: true,
27
+ userName: "Developer"
28
+ };
12
29
  function ensureDir() {
13
- if (!fs3.existsSync(DOMINUS_DIR)) {
14
- fs3.mkdirSync(DOMINUS_DIR, { recursive: true });
30
+ if (!fs4.existsSync(DOMINUS_DIR)) {
31
+ fs4.mkdirSync(DOMINUS_DIR, { recursive: true });
15
32
  }
16
33
  }
34
+ function readJson(filePath, defaults) {
35
+ try {
36
+ if (fs4.existsSync(filePath)) {
37
+ const raw = fs4.readFileSync(filePath, "utf-8");
38
+ return { ...defaults, ...JSON.parse(raw) };
39
+ }
40
+ } catch {
41
+ }
42
+ return { ...defaults };
43
+ }
44
+ function loadConfig() {
45
+ ensureDir();
46
+ return readJson(CONFIG_PATH, DEFAULTS);
47
+ }
17
48
  function getDominusDir() {
18
49
  ensureDir();
19
50
  return DOMINUS_DIR;
20
51
  }
21
-
22
- // src/transport/auth.ts
52
+ var DOMINUS_PROTOCOL_VERSION = 2;
53
+ var MAX_MESSAGE_BYTES = 1024 * 1024;
54
+ var StudioMsg = {
55
+ HELLO: "studio:hello",
56
+ CONNECTED: "studio:connected",
57
+ DISCONNECTED: "studio:disconnected",
58
+ EXPLORER: "studio:explorer",
59
+ OUTPUT: "studio:output",
60
+ ERROR: "studio:error",
61
+ SELECTION: "studio:selection",
62
+ SCRIPT_CONTENT: "studio:script:content",
63
+ SCRIPT_CHANGED: "studio:script:changed",
64
+ RESPONSE: "studio:response",
65
+ REFLECTION: "studio:reflection",
66
+ TEST_RESULTS: "studio:test:results",
67
+ AUTHENTICATED: "server:authenticated",
68
+ AUTH_REJECTED: "server:auth_rejected"
69
+ };
70
+ var ControllerMsg = {
71
+ HELLO: "controller:hello",
72
+ WELCOME: "controller:welcome",
73
+ RELAY: "controller:relay",
74
+ TARGET_STATE: "controller:target_state",
75
+ SET_ACTIVE_CONNECTION: "controller:set_active_connection"
76
+ };
77
+ function createMessage(type, payload) {
78
+ return { id: nanoid(), type, payload, ts: Date.now() };
79
+ }
80
+ function parseMessage(raw) {
81
+ if (Buffer.byteLength(raw, "utf8") > MAX_MESSAGE_BYTES) {
82
+ throw new Error(`WebSocket message exceeds ${MAX_MESSAGE_BYTES} bytes`);
83
+ }
84
+ try {
85
+ const parsed = JSON.parse(raw);
86
+ return wsMessageSchema.parse(parsed);
87
+ } catch {
88
+ throw new Error(`Failed to parse WebSocket message: ${raw.slice(0, 100)}`);
89
+ }
90
+ }
91
+ function serializeMessage(msg) {
92
+ const serialized = JSON.stringify(msg);
93
+ if (Buffer.byteLength(serialized, "utf8") > MAX_MESSAGE_BYTES) {
94
+ throw new Error(`WebSocket message exceeds ${MAX_MESSAGE_BYTES} bytes`);
95
+ }
96
+ return serialized;
97
+ }
98
+ var wsMessageSchema = z.object({
99
+ id: z.string().min(1).max(128),
100
+ type: z.string().min(1).max(128),
101
+ payload: z.unknown(),
102
+ ts: z.number().finite()
103
+ }).strict();
23
104
  var TOKEN_FILE = "bridge-token";
24
105
  function loadOrCreateBridgeToken() {
25
- const tokenPath = path3.join(getDominusDir(), TOKEN_FILE);
106
+ const tokenPath = path4.join(getDominusDir(), TOKEN_FILE);
26
107
  try {
27
- const existing = fs3.readFileSync(tokenPath, "utf8").trim();
108
+ const existing = fs4.readFileSync(tokenPath, "utf8").trim();
28
109
  if (existing.length >= 32) return existing;
29
110
  } catch {
30
111
  }
31
112
  const token = crypto.randomBytes(32).toString("base64url");
32
- fs3.writeFileSync(tokenPath, `${token}
113
+ fs4.writeFileSync(tokenPath, `${token}
33
114
  `, { encoding: "utf8", mode: 384 });
34
115
  try {
35
- fs3.chmodSync(tokenPath, 384);
116
+ fs4.chmodSync(tokenPath, 384);
36
117
  } catch {
37
118
  }
38
119
  return token;
39
120
  }
40
121
 
122
+ // src/transport/ws-client.ts
123
+ var DominusClient = class extends EventEmitter {
124
+ ws = null;
125
+ port;
126
+ token;
127
+ connectTimeoutMs;
128
+ connections = /* @__PURE__ */ new Map();
129
+ activeConnectionId = null;
130
+ pendingRequests = /* @__PURE__ */ new Map();
131
+ constructor(port = 18088, options = {}) {
132
+ super();
133
+ this.port = port;
134
+ this.token = options.token ?? loadOrCreateBridgeToken();
135
+ this.connectTimeoutMs = options.connectTimeoutMs ?? 8e3;
136
+ }
137
+ connect() {
138
+ return new Promise((resolve, reject) => {
139
+ const ws = new WebSocket(`ws://127.0.0.1:${this.port}`, {
140
+ perMessageDeflate: false,
141
+ maxPayload: 1024 * 1024
142
+ });
143
+ let settled = false;
144
+ const timer = setTimeout(() => {
145
+ if (!settled) {
146
+ settled = true;
147
+ ws.close(1008, "Dominus controller handshake timed out");
148
+ reject(new Error("Timed out authenticating with the Dominus bridge"));
149
+ }
150
+ }, this.connectTimeoutMs);
151
+ ws.on("open", () => {
152
+ this.ws = ws;
153
+ ws.send(
154
+ serializeMessage(
155
+ createMessage(ControllerMsg.HELLO, {
156
+ protocolVersion: DOMINUS_PROTOCOL_VERSION,
157
+ token: this.token
158
+ })
159
+ )
160
+ );
161
+ });
162
+ ws.on("message", (raw) => {
163
+ try {
164
+ const msg = parseMessage(raw.toString());
165
+ if (msg.type === ControllerMsg.WELCOME && !settled) {
166
+ settled = true;
167
+ clearTimeout(timer);
168
+ this.applyWelcome(msg.payload);
169
+ this.emit("server:started", { port: this.port, relay: true });
170
+ resolve();
171
+ return;
172
+ }
173
+ if (msg.type === StudioMsg.AUTH_REJECTED && !settled) {
174
+ settled = true;
175
+ clearTimeout(timer);
176
+ const payload = msg.payload;
177
+ reject(new Error(payload.error ?? "Dominus controller authentication failed"));
178
+ ws.close(1008, "Controller authentication failed");
179
+ return;
180
+ }
181
+ this.handleMessage(msg);
182
+ } catch (err) {
183
+ this.emit("server:error", err);
184
+ }
185
+ });
186
+ ws.on("close", () => {
187
+ clearTimeout(timer);
188
+ this.ws = null;
189
+ this.connections.clear();
190
+ this.activeConnectionId = null;
191
+ if (!settled) {
192
+ settled = true;
193
+ reject(new Error("Dominus bridge closed before authentication completed"));
194
+ }
195
+ this.rejectPending(new Error("Dominus relay connection lost"));
196
+ this.emit("studio:disconnected");
197
+ });
198
+ ws.on("error", (err) => {
199
+ if (!settled) {
200
+ settled = true;
201
+ clearTimeout(timer);
202
+ reject(err);
203
+ }
204
+ this.emit("server:error", err);
205
+ });
206
+ });
207
+ }
208
+ applyWelcome(payload) {
209
+ const welcome = payload;
210
+ this.replaceConnections(welcome.connections ?? []);
211
+ this.activeConnectionId = welcome.activeConnectionId ?? null;
212
+ if (!this.resolveTargetInfo() && this.connections.size === 1) {
213
+ this.activeConnectionId = this.connections.keys().next().value ?? null;
214
+ }
215
+ }
216
+ handleMessage(msg) {
217
+ if (msg.type === ControllerMsg.TARGET_STATE) {
218
+ const payload = msg.payload;
219
+ this.replaceConnections(payload.connections ?? []);
220
+ if (this.activeConnectionId && !this.connections.has(this.activeConnectionId)) {
221
+ this.activeConnectionId = null;
222
+ }
223
+ if (!this.activeConnectionId && this.connections.size === 1) {
224
+ this.activeConnectionId = this.connections.keys().next().value ?? null;
225
+ }
226
+ this.emit("controller:target_state", payload);
227
+ return;
228
+ }
229
+ if (msg.type === StudioMsg.CONNECTED) {
230
+ const connection = msg.payload;
231
+ this.connections.set(connection.connectionId, toConnectionInfo(connection));
232
+ if (!this.activeConnectionId && this.connections.size === 1) {
233
+ this.activeConnectionId = connection.connectionId;
234
+ }
235
+ this.emit("studio:connected", connection);
236
+ return;
237
+ }
238
+ if (msg.type === StudioMsg.DISCONNECTED) {
239
+ const payload = msg.payload;
240
+ if (payload.connectionId) this.connections.delete(payload.connectionId);
241
+ if (this.activeConnectionId === payload.connectionId) this.activeConnectionId = null;
242
+ if (!this.activeConnectionId && this.connections.size === 1) {
243
+ this.activeConnectionId = this.connections.keys().next().value ?? null;
244
+ }
245
+ this.emit("studio:disconnected", payload);
246
+ return;
247
+ }
248
+ if (msg.type === StudioMsg.RESPONSE) {
249
+ const pending = this.pendingRequests.get(msg.id);
250
+ if (pending) {
251
+ clearTimeout(pending.timer);
252
+ this.pendingRequests.delete(msg.id);
253
+ pending.resolve(msg);
254
+ }
255
+ return;
256
+ }
257
+ this.emit(msg.type, msg.payload);
258
+ this.emit("message", msg);
259
+ }
260
+ replaceConnections(connections) {
261
+ this.connections.clear();
262
+ for (const connection of connections) {
263
+ this.connections.set(connection.connectionId, toConnectionInfo(connection));
264
+ }
265
+ }
266
+ sendMessage(message) {
267
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
268
+ throw new Error("Not connected to the Dominus bridge");
269
+ }
270
+ this.ws.send(serializeMessage(message));
271
+ }
272
+ waitForResponse(message, timeoutMs) {
273
+ return new Promise((resolve, reject) => {
274
+ const timer = setTimeout(() => {
275
+ this.pendingRequests.delete(message.id);
276
+ reject(new Error(`Dominus request timed out: ${message.type}`));
277
+ }, timeoutMs);
278
+ this.pendingRequests.set(message.id, {
279
+ resolve,
280
+ reject,
281
+ timer
282
+ });
283
+ try {
284
+ this.sendMessage(message);
285
+ } catch (err) {
286
+ clearTimeout(timer);
287
+ this.pendingRequests.delete(message.id);
288
+ reject(err);
289
+ }
290
+ });
291
+ }
292
+ async sendControlRequest(type, payload, timeoutMs = 1e4) {
293
+ const response = await this.waitForResponse(createMessage(type, payload), timeoutMs);
294
+ const result = response.payload;
295
+ if (result.error || result.success === false) throw new Error(result.error ?? `${type} failed`);
296
+ return result;
297
+ }
298
+ sendRequest(type, payload, timeoutMs = 15e3) {
299
+ const target = this.resolveTargetInfo();
300
+ if (!target || !this.activeConnectionId) {
301
+ return Promise.reject(
302
+ new Error(
303
+ this.connections.size > 1 ? "Multiple Studio sessions are connected. Call dominus_select_studio first." : "No authenticated Roblox Studio connection"
304
+ )
305
+ );
306
+ }
307
+ const request = createMessage(type, payload);
308
+ const relay = createMessage(ControllerMsg.RELAY, {
309
+ connectionId: this.activeConnectionId,
310
+ request
311
+ });
312
+ return new Promise((resolve, reject) => {
313
+ const timer = setTimeout(() => {
314
+ this.pendingRequests.delete(request.id);
315
+ reject(new Error(`Studio request timed out: ${type}`));
316
+ }, timeoutMs);
317
+ this.pendingRequests.set(request.id, {
318
+ resolve,
319
+ reject,
320
+ timer
321
+ });
322
+ try {
323
+ this.sendMessage(relay);
324
+ } catch (err) {
325
+ clearTimeout(timer);
326
+ this.pendingRequests.delete(request.id);
327
+ reject(err);
328
+ }
329
+ });
330
+ }
331
+ sendNotification(type, payload) {
332
+ if (!this.activeConnectionId) return;
333
+ const request = createMessage(type, payload);
334
+ this.sendMessage(
335
+ createMessage(ControllerMsg.RELAY, {
336
+ connectionId: this.activeConnectionId,
337
+ request
338
+ })
339
+ );
340
+ }
341
+ isConnected() {
342
+ return this.resolveTargetInfo() !== null;
343
+ }
344
+ getConnectionInfo() {
345
+ const target = this.resolveTargetInfo();
346
+ if (!target || !this.ws) return null;
347
+ return { ...target, ws: this.ws };
348
+ }
349
+ listConnections() {
350
+ return [...this.connections.values()].map((connection) => ({
351
+ placeId: connection.placeId ?? 0,
352
+ connectionId: connection.connectionId,
353
+ clientId: connection.clientId,
354
+ placeName: connection.placeName,
355
+ studioVersion: connection.studioVersion,
356
+ connectedAt: connection.connectedAt,
357
+ active: connection.connectionId === this.activeConnectionId
358
+ }));
359
+ }
360
+ getActiveConnectionId() {
361
+ return this.activeConnectionId;
362
+ }
363
+ async setActiveConnectionId(connectionId) {
364
+ if (connectionId && !this.connections.has(connectionId)) {
365
+ throw new Error(`Studio connection ${connectionId} is not connected`);
366
+ }
367
+ await this.sendControlRequest(ControllerMsg.SET_ACTIVE_CONNECTION, { connectionId });
368
+ this.activeConnectionId = connectionId;
369
+ }
370
+ getActivePlaceId() {
371
+ return this.resolveTargetInfo()?.placeId ?? 0;
372
+ }
373
+ setActivePlaceId(placeId) {
374
+ if (placeId === 0) {
375
+ void this.setActiveConnectionId(null);
376
+ return;
377
+ }
378
+ const matches = [...this.connections.values()].filter(
379
+ (connection) => connection.placeId === placeId
380
+ );
381
+ if (matches.length !== 1) {
382
+ throw new Error(
383
+ matches.length === 0 ? `Place ${placeId} is not connected` : `Place ${placeId} has multiple Studio sessions; select a connectionId instead`
384
+ );
385
+ }
386
+ void this.setActiveConnectionId(matches[0].connectionId);
387
+ }
388
+ resolveTargetInfo() {
389
+ if (this.activeConnectionId) return this.connections.get(this.activeConnectionId) ?? null;
390
+ if (this.connections.size === 1) return this.connections.values().next().value ?? null;
391
+ return null;
392
+ }
393
+ getPort() {
394
+ return this.port;
395
+ }
396
+ stop() {
397
+ this.rejectPending(new Error("Dominus client is shutting down"));
398
+ return new Promise((resolve) => {
399
+ if (!this.ws) {
400
+ resolve();
401
+ return;
402
+ }
403
+ const ws = this.ws;
404
+ this.ws = null;
405
+ ws.once("close", resolve);
406
+ ws.close(1e3, "Dominus client shutdown");
407
+ });
408
+ }
409
+ rejectPending(error) {
410
+ for (const pending of this.pendingRequests.values()) {
411
+ clearTimeout(pending.timer);
412
+ pending.reject(error);
413
+ }
414
+ this.pendingRequests.clear();
415
+ }
416
+ };
417
+ function toConnectionInfo(connection) {
418
+ return {
419
+ connectedAt: connection.connectedAt,
420
+ connectionId: connection.connectionId,
421
+ clientId: connection.clientId,
422
+ studioVersion: connection.studioVersion,
423
+ placeId: connection.placeId,
424
+ placeName: connection.placeName
425
+ };
426
+ }
427
+ async function isPortInUse(port) {
428
+ return new Promise((resolve) => {
429
+ const ws = new WebSocket(`ws://127.0.0.1:${port}`);
430
+ const timer = setTimeout(() => {
431
+ ws.close();
432
+ resolve(false);
433
+ }, 750);
434
+ ws.on("open", () => {
435
+ clearTimeout(timer);
436
+ ws.close();
437
+ resolve(true);
438
+ });
439
+ ws.on("error", () => {
440
+ clearTimeout(timer);
441
+ resolve(false);
442
+ });
443
+ });
444
+ }
445
+
446
+ // src/transport/daemon-launcher.ts
447
+ function resolveDaemonPath() {
448
+ const besideEntry = fileURLToPath(new URL("./bridge-daemon.js", import.meta.url));
449
+ if (fs4.existsSync(besideEntry)) return besideEntry;
450
+ const projectBuild = path4.resolve(
451
+ path4.dirname(fileURLToPath(import.meta.url)),
452
+ "../../dist/bridge-daemon.js"
453
+ );
454
+ if (fs4.existsSync(projectBuild)) return projectBuild;
455
+ throw new Error("Dominus bridge daemon is missing. Reinstall or rebuild dominus-cli.");
456
+ }
457
+ function launchBridgeDaemon(port, daemonPath = resolveDaemonPath()) {
458
+ const child = spawn(process.execPath, [daemonPath], {
459
+ detached: true,
460
+ env: { ...process.env, DOMINUS_BRIDGE_PORT: String(port) },
461
+ stdio: "ignore",
462
+ windowsHide: true
463
+ });
464
+ child.unref();
465
+ }
466
+ async function connectController(port, timeoutMs, token) {
467
+ const client = new DominusClient(port, { connectTimeoutMs: timeoutMs, token });
468
+ try {
469
+ await client.connect();
470
+ return client;
471
+ } catch (error) {
472
+ await client.stop().catch(() => void 0);
473
+ throw error;
474
+ }
475
+ }
476
+ async function ensureBridgeDaemon(port, options = {}) {
477
+ const attempts = options.attempts ?? 30;
478
+ const connectTimeoutMs = options.connectTimeoutMs ?? 750;
479
+ const retryDelayMs = options.retryDelayMs ?? 100;
480
+ try {
481
+ return await connectController(port, connectTimeoutMs, options.token);
482
+ } catch (initialError) {
483
+ if (await isPortInUse(port)) {
484
+ throw new Error(
485
+ `Port ${port} is occupied by a process that is not this Dominus bridge: ${formatError(initialError)}`
486
+ );
487
+ }
488
+ }
489
+ const launch = options.launch ?? ((targetPort) => launchBridgeDaemon(targetPort, options.daemonPath));
490
+ launch(port);
491
+ let lastError;
492
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
493
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
494
+ try {
495
+ return await connectController(port, connectTimeoutMs, options.token);
496
+ } catch (error) {
497
+ lastError = error;
498
+ }
499
+ }
500
+ throw new Error(
501
+ `Dominus could not start its local Studio bridge on port ${port}: ${formatError(lastError)}`
502
+ );
503
+ }
504
+ function formatError(error) {
505
+ return error instanceof Error ? error.message : String(error ?? "unknown error");
506
+ }
507
+
41
508
  // src/install-plugin.ts
42
509
  var TOKEN_PLACEHOLDER = "__DOMINUS_BRIDGE_TOKEN__";
43
510
  function getPluginDirectory() {
44
511
  if (process.platform === "win32") {
45
512
  const localAppData = process.env.LOCALAPPDATA;
46
513
  if (!localAppData) throw new Error("LOCALAPPDATA is not set");
47
- return path3.join(localAppData, "Roblox", "Plugins");
514
+ return path4.join(localAppData, "Roblox", "Plugins");
48
515
  }
49
516
  if (process.platform === "darwin") {
50
- return path3.join(os2.homedir(), "Documents", "Roblox", "Plugins");
517
+ return path4.join(os2.homedir(), "Documents", "Roblox", "Plugins");
51
518
  }
52
- return path3.join(os2.homedir(), ".local", "share", "Roblox", "Plugins");
519
+ return path4.join(os2.homedir(), ".local", "share", "Roblox", "Plugins");
53
520
  }
54
- var distDirectory = path3.dirname(fileURLToPath(import.meta.url));
55
- var source = path3.resolve(distDirectory, "..", "plugin", "Dominus.rbxmx");
521
+ var distDirectory = path4.dirname(fileURLToPath(import.meta.url));
522
+ var source = path4.resolve(distDirectory, "..", "plugin", "Dominus.rbxmx");
56
523
  var destinationDirectory = getPluginDirectory();
57
- var destination = path3.join(destinationDirectory, "Dominus2.rbxmx");
58
- if (!fs3.existsSync(source)) {
524
+ var destination = path4.join(destinationDirectory, "Dominus2.rbxmx");
525
+ if (!fs4.existsSync(source)) {
59
526
  throw new Error(`Packaged plugin not found at ${source}. Run pnpm build:plugin first.`);
60
527
  }
61
- var template = fs3.readFileSync(source, "utf8");
528
+ var template = fs4.readFileSync(source, "utf8");
62
529
  if (!template.includes(TOKEN_PLACEHOLDER)) {
63
530
  throw new Error("Packaged plugin template is missing its bridge token placeholder");
64
531
  }
65
532
  var personalizedPlugin = template.replaceAll(TOKEN_PLACEHOLDER, loadOrCreateBridgeToken());
66
- fs3.mkdirSync(destinationDirectory, { recursive: true });
67
- fs3.writeFileSync(destination, personalizedPlugin, "utf8");
533
+ fs4.mkdirSync(destinationDirectory, { recursive: true });
534
+ fs4.writeFileSync(destination, personalizedPlugin, "utf8");
68
535
  for (const obsoleteName of ["Dominus2.rbxm", "Dominus.rbxm", "Dominus.rbxmx"]) {
69
- const obsoletePath = path3.join(destinationDirectory, obsoleteName);
70
- if (obsoletePath !== destination) fs3.rmSync(obsoletePath, { force: true });
536
+ const obsoletePath = path4.join(destinationDirectory, obsoleteName);
537
+ if (obsoletePath !== destination) fs4.rmSync(obsoletePath, { force: true });
71
538
  }
72
539
  console.log(`Installed Dominus 2 Studio plugin to ${destination}`);
73
540
  console.log(
74
- "Roblox Studio normally reloads Dominus 2 automatically. Restart Studio only if the toolbar does not reconnect."
541
+ "Dominus 2 authenticates automatically. Restart Studio only if the toolbar does not reconnect."
75
542
  );
543
+ var bridge = await ensureBridgeDaemon(loadConfig().port);
544
+ await bridge.stop();
545
+ console.log("Dominus Studio bridge is running in the background.");
76
546
  //# sourceMappingURL=install-plugin.js.map
77
547
  //# sourceMappingURL=install-plugin.js.map