ropilot 0.1.23 → 0.1.25
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/lib/proxy.js +22 -0
- package/lib/setup.js +36 -164
- package/package.json +1 -1
package/lib/proxy.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
|
+
import { spawn } from 'child_process';
|
|
11
12
|
import { getApiKey, EDGE_URL, loadGlobalConfig, saveGlobalConfig } from './config.js';
|
|
12
13
|
|
|
13
14
|
// Plugin version check URL (served from ropilot.ai main server)
|
|
@@ -111,6 +112,24 @@ async function processRequest(apiKey, request) {
|
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Run update in background (non-blocking)
|
|
117
|
+
* Claude/Cursor spawn MCP servers from the project root, so process.cwd() is correct
|
|
118
|
+
*/
|
|
119
|
+
function runBackgroundUpdate() {
|
|
120
|
+
try {
|
|
121
|
+
const child = spawn('npx', ['ropilot', 'update'], {
|
|
122
|
+
cwd: process.cwd(),
|
|
123
|
+
detached: true,
|
|
124
|
+
stdio: 'ignore',
|
|
125
|
+
shell: true
|
|
126
|
+
});
|
|
127
|
+
child.unref();
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Silently ignore - update is not critical
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
114
133
|
/**
|
|
115
134
|
* Check for plugin updates (runs in background, writes to stderr)
|
|
116
135
|
*/
|
|
@@ -161,6 +180,9 @@ export async function serve() {
|
|
|
161
180
|
process.exit(1);
|
|
162
181
|
}
|
|
163
182
|
|
|
183
|
+
// Run update in background (non-blocking) - keeps prompts fresh
|
|
184
|
+
runBackgroundUpdate();
|
|
185
|
+
|
|
164
186
|
// Check for plugin updates in background (non-blocking)
|
|
165
187
|
checkPluginUpdate();
|
|
166
188
|
|
package/lib/setup.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Setup and initialization for Ropilot CLI
|
|
3
|
-
*
|
|
4
|
-
* Handles:
|
|
5
|
-
* - First-time setup
|
|
6
|
-
* - Downloading system prompts
|
|
7
|
-
* - Creating project configs
|
|
8
|
-
* - Installing Studio plugin
|
|
9
3
|
*/
|
|
10
4
|
|
|
11
5
|
import { existsSync, writeFileSync, mkdirSync, readFileSync, readdirSync } from 'fs';
|
|
@@ -46,13 +40,11 @@ function isWSL() {
|
|
|
46
40
|
* Get Windows username when running in WSL (tries multiple methods)
|
|
47
41
|
*/
|
|
48
42
|
function getWindowsUsername() {
|
|
49
|
-
// Method 1: cmd.exe
|
|
50
43
|
try {
|
|
51
44
|
const result = execSync('cmd.exe /c echo %USERNAME%', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
52
45
|
if (result && !result.includes('%')) return result;
|
|
53
46
|
} catch {}
|
|
54
47
|
|
|
55
|
-
// Method 2: whoami.exe (returns DOMAIN\user)
|
|
56
48
|
try {
|
|
57
49
|
const result = execSync('/mnt/c/Windows/System32/whoami.exe', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
58
50
|
if (result && result.includes('\\')) {
|
|
@@ -60,7 +52,6 @@ function getWindowsUsername() {
|
|
|
60
52
|
}
|
|
61
53
|
} catch {}
|
|
62
54
|
|
|
63
|
-
// Method 3: powershell
|
|
64
55
|
try {
|
|
65
56
|
const result = execSync('powershell.exe -NoProfile -Command "$env:USERNAME"', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
66
57
|
if (result) return result;
|
|
@@ -86,20 +77,16 @@ function getValidWindowsUsers() {
|
|
|
86
77
|
|
|
87
78
|
/**
|
|
88
79
|
* Get Roblox plugins folder path based on OS (with WSL detection)
|
|
89
|
-
* Returns { path, needsPrompt, users } - needsPrompt true if user selection needed
|
|
90
80
|
*/
|
|
91
81
|
function getPluginsFolder() {
|
|
92
82
|
const os = platform();
|
|
93
83
|
|
|
94
|
-
// Check for WSL first - need to use Windows path
|
|
95
84
|
if (os === 'linux' && isWSL()) {
|
|
96
|
-
// Try to auto-detect Windows username
|
|
97
85
|
const windowsUser = getWindowsUsername();
|
|
98
86
|
if (windowsUser && existsSync(join('/mnt/c/Users', windowsUser))) {
|
|
99
87
|
return { path: join('/mnt/c/Users', windowsUser, 'AppData', 'Local', 'Roblox', 'Plugins') };
|
|
100
88
|
}
|
|
101
89
|
|
|
102
|
-
// Fallback: scan /mnt/c/Users
|
|
103
90
|
const validUsers = getValidWindowsUsers();
|
|
104
91
|
if (validUsers.length === 1) {
|
|
105
92
|
return { path: join('/mnt/c/Users', validUsers[0], 'AppData', 'Local', 'Roblox', 'Plugins') };
|
|
@@ -115,7 +102,6 @@ function getPluginsFolder() {
|
|
|
115
102
|
} else if (os === 'darwin') {
|
|
116
103
|
return { path: join(homedir(), 'Documents', 'Roblox', 'Plugins') };
|
|
117
104
|
} else {
|
|
118
|
-
// Linux without WSL - Roblox doesn't officially support Linux
|
|
119
105
|
return { path: null };
|
|
120
106
|
}
|
|
121
107
|
}
|
|
@@ -141,33 +127,26 @@ function prompt(question) {
|
|
|
141
127
|
* Install Studio plugin to Roblox plugins folder
|
|
142
128
|
*/
|
|
143
129
|
async function installPlugin() {
|
|
144
|
-
console.log('Installing Studio plugin...');
|
|
145
|
-
|
|
146
130
|
try {
|
|
147
|
-
// Check plugin version
|
|
148
131
|
const versionResponse = await fetch(PLUGIN_VERSION_URL);
|
|
149
132
|
if (!versionResponse.ok) {
|
|
150
133
|
throw new Error(`Failed to fetch plugin version: ${versionResponse.status}`);
|
|
151
134
|
}
|
|
152
135
|
const versionInfo = await versionResponse.json();
|
|
153
|
-
console.log(` Plugin version: ${versionInfo.version}`);
|
|
154
136
|
|
|
155
|
-
// Download plugin
|
|
156
137
|
const pluginResponse = await fetch(PLUGIN_DOWNLOAD_URL);
|
|
157
138
|
if (!pluginResponse.ok) {
|
|
158
139
|
throw new Error(`Failed to download plugin: ${pluginResponse.status}`);
|
|
159
140
|
}
|
|
160
141
|
const pluginSource = await pluginResponse.text();
|
|
161
142
|
|
|
162
|
-
// Get plugins folder
|
|
163
143
|
let folderResult = getPluginsFolder();
|
|
164
144
|
let pluginsFolder = folderResult.path;
|
|
165
145
|
|
|
166
|
-
// If WSL detected multiple users, prompt for selection
|
|
167
146
|
if (folderResult.needsPrompt && folderResult.users) {
|
|
168
|
-
console.log('
|
|
169
|
-
folderResult.users.forEach((u, i) => console.log(`
|
|
170
|
-
const choice = await prompt(`
|
|
147
|
+
console.log('Multiple Windows users detected:');
|
|
148
|
+
folderResult.users.forEach((u, i) => console.log(` ${i + 1}. ${u}`));
|
|
149
|
+
const choice = await prompt(`Select user (1-${folderResult.users.length}): `);
|
|
171
150
|
|
|
172
151
|
let selectedUser;
|
|
173
152
|
const num = parseInt(choice);
|
|
@@ -184,81 +163,63 @@ async function installPlugin() {
|
|
|
184
163
|
}
|
|
185
164
|
}
|
|
186
165
|
|
|
187
|
-
// If still no folder and running in WSL, prompt for username directly
|
|
188
166
|
if (!pluginsFolder && isWSL()) {
|
|
189
|
-
const username = await prompt('
|
|
190
|
-
if (username
|
|
191
|
-
pluginsFolder = join('/mnt/c/Users', username, 'AppData', 'Local', 'Roblox', 'Plugins');
|
|
192
|
-
} else if (username) {
|
|
193
|
-
// Trust user input even if path doesn't exist yet
|
|
167
|
+
const username = await prompt('Enter your Windows username: ');
|
|
168
|
+
if (username) {
|
|
194
169
|
pluginsFolder = join('/mnt/c/Users', username, 'AppData', 'Local', 'Roblox', 'Plugins');
|
|
195
170
|
}
|
|
196
171
|
}
|
|
197
172
|
|
|
198
173
|
if (!pluginsFolder) {
|
|
199
|
-
console.log('
|
|
200
|
-
console.log('
|
|
174
|
+
console.log('Could not find Roblox plugins folder');
|
|
175
|
+
console.log('Download manually: https://ropilot.ai/plugin/StudioPlugin.lua');
|
|
201
176
|
return false;
|
|
202
177
|
}
|
|
203
178
|
|
|
204
|
-
// Create plugins folder if it doesn't exist
|
|
205
179
|
mkdirSync(pluginsFolder, { recursive: true });
|
|
206
180
|
|
|
207
|
-
// Write plugin file
|
|
208
181
|
const pluginPath = join(pluginsFolder, 'RopilotPlugin.lua');
|
|
209
182
|
writeFileSync(pluginPath, pluginSource);
|
|
210
183
|
|
|
211
|
-
console.log(`
|
|
184
|
+
console.log(`Plugin v${versionInfo.version} installed`);
|
|
212
185
|
|
|
213
|
-
// Save version to config
|
|
214
186
|
const config = loadGlobalConfig();
|
|
215
187
|
config.pluginVersion = versionInfo.version;
|
|
216
188
|
saveGlobalConfig(config);
|
|
217
189
|
|
|
218
190
|
return true;
|
|
219
191
|
} catch (err) {
|
|
220
|
-
console.error(`
|
|
221
|
-
console.log('
|
|
192
|
+
console.error(`Plugin install failed: ${err.message}`);
|
|
193
|
+
console.log('Download manually: https://ropilot.ai/plugin/StudioPlugin.lua');
|
|
222
194
|
return false;
|
|
223
195
|
}
|
|
224
196
|
}
|
|
225
197
|
|
|
226
|
-
|
|
227
198
|
/**
|
|
228
199
|
* Download prompts from edge server
|
|
229
200
|
*/
|
|
230
201
|
async function downloadPrompts(apiKey) {
|
|
231
|
-
console.log('Downloading latest agent package...');
|
|
232
|
-
|
|
233
202
|
try {
|
|
234
|
-
// Fetch from edge server
|
|
235
203
|
const response = await fetch(`${EDGE_URL}/package`);
|
|
236
204
|
if (!response.ok) {
|
|
237
205
|
throw new Error(`Server returned ${response.status}`);
|
|
238
206
|
}
|
|
239
207
|
|
|
240
208
|
const pkg = await response.json();
|
|
241
|
-
console.log(`Package version: ${pkg.version}`);
|
|
242
209
|
|
|
243
|
-
// Ensure prompts directory exists
|
|
244
210
|
mkdirSync(PROMPTS_DIR, { recursive: true });
|
|
245
|
-
|
|
246
|
-
// Write package info
|
|
247
211
|
writeFileSync(join(PROMPTS_DIR, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
248
212
|
|
|
249
|
-
// Update config with version info
|
|
250
213
|
const config = loadGlobalConfig();
|
|
251
214
|
config.promptsVersion = pkg.version;
|
|
252
215
|
config.lastUpdated = new Date().toISOString();
|
|
253
216
|
saveGlobalConfig(config);
|
|
254
217
|
|
|
255
|
-
console.log(
|
|
218
|
+
console.log(`Agent package v${pkg.version} downloaded`);
|
|
256
219
|
return pkg;
|
|
257
220
|
} catch (err) {
|
|
258
|
-
console.error('Failed to download
|
|
259
|
-
console.log('Using bundled defaults...');
|
|
221
|
+
console.error('Failed to download package:', err.message);
|
|
260
222
|
|
|
261
|
-
// Fallback to bundled defaults
|
|
262
223
|
const prompts = getDefaultPrompts();
|
|
263
224
|
mkdirSync(PROMPTS_DIR, { recursive: true });
|
|
264
225
|
for (const [name, content] of Object.entries(prompts)) {
|
|
@@ -275,57 +236,12 @@ async function downloadPrompts(apiKey) {
|
|
|
275
236
|
}
|
|
276
237
|
|
|
277
238
|
/**
|
|
278
|
-
* Get default prompts (bundled)
|
|
239
|
+
* Get default prompts (bundled fallback)
|
|
279
240
|
*/
|
|
280
241
|
function getDefaultPrompts() {
|
|
281
242
|
return {
|
|
282
|
-
'CLAUDE.md':
|
|
283
|
-
|
|
284
|
-
You are connected to Ropilot, an AI assistant for Roblox Studio development.
|
|
285
|
-
|
|
286
|
-
## Available Tools
|
|
287
|
-
|
|
288
|
-
Ropilot provides MCP tools for interacting with Roblox Studio:
|
|
289
|
-
|
|
290
|
-
### Reading & Inspection
|
|
291
|
-
- \`ropilot_listchildren\` - List children of an instance
|
|
292
|
-
- \`ropilot_getproperties\` - Get properties of an instance
|
|
293
|
-
- \`ropilot_search\` - Search for instances by name
|
|
294
|
-
- \`ropilot_searchbyclass\` - Search for instances by class name
|
|
295
|
-
|
|
296
|
-
### Playtesting
|
|
297
|
-
- \`ropilot_test_start_playtest\` - Start a playtest session
|
|
298
|
-
- \`ropilot_test_stop_playtest\` - Stop the current playtest
|
|
299
|
-
- \`ropilot_test_run_client_lua\` - Run Lua code on the client
|
|
300
|
-
- \`ropilot_test_run_server_lua\` - Run Lua code on the server
|
|
301
|
-
|
|
302
|
-
### Data Management
|
|
303
|
-
- \`ropilot_reset_data\` - Reset player data in DataStore
|
|
304
|
-
|
|
305
|
-
## Context Targeting
|
|
306
|
-
|
|
307
|
-
Commands can target different contexts:
|
|
308
|
-
- \`edit\` - Edit mode (default)
|
|
309
|
-
- \`server\` - Server context during playtest
|
|
310
|
-
- \`client\` - Client context during playtest
|
|
311
|
-
|
|
312
|
-
Use the \`target_context\` parameter to specify which context should execute the command.
|
|
313
|
-
|
|
314
|
-
## Best Practices
|
|
315
|
-
|
|
316
|
-
1. **Read before modify** - Always inspect the current state before making changes
|
|
317
|
-
2. **Use specific paths** - Reference instances by their full path (e.g., "Workspace.Map.Building")
|
|
318
|
-
3. **Delegate playtesting** - Use the roblox-tester subagent for playtest operations
|
|
319
|
-
4. **Test incrementally** - Start playtests to verify changes work correctly
|
|
320
|
-
`,
|
|
321
|
-
|
|
322
|
-
'subagents.json': JSON.stringify({
|
|
323
|
-
"roblox-tester": {
|
|
324
|
-
"description": "Fast subagent for executing Roblox playtest steps",
|
|
325
|
-
"tools": ["ropilot_test_*", "ropilot_get_*", "ropilot_execute_lua"],
|
|
326
|
-
"prompt": "You are a Roblox playtest executor. Run the specified test steps and report results concisely."
|
|
327
|
-
}
|
|
328
|
-
}, null, 2)
|
|
243
|
+
'CLAUDE.md': `@import ROPILOT.md\n`,
|
|
244
|
+
'ROPILOT.md': `# Ropilot\n\nRoblox Studio AI assistant. Use ropilot_* MCP tools to interact with Studio.\n`
|
|
329
245
|
};
|
|
330
246
|
}
|
|
331
247
|
|
|
@@ -334,70 +250,61 @@ Use the \`target_context\` parameter to specify which context should execute the
|
|
|
334
250
|
*/
|
|
335
251
|
function setupProjectPrompts(pkg) {
|
|
336
252
|
const files = pkg.files || {};
|
|
337
|
-
|
|
338
|
-
// Files that should NOT be overwritten (user may have customized)
|
|
339
253
|
const preserveFiles = ['CLAUDE.md'];
|
|
340
254
|
|
|
255
|
+
let created = 0, updated = 0, skipped = 0;
|
|
256
|
+
|
|
341
257
|
for (const [filePath, content] of Object.entries(files)) {
|
|
342
258
|
const fullPath = filePath;
|
|
343
259
|
const dir = dirname(fullPath);
|
|
344
260
|
|
|
345
|
-
// Create directory if needed
|
|
346
261
|
if (dir && dir !== '.') {
|
|
347
262
|
mkdirSync(dir, { recursive: true });
|
|
348
263
|
}
|
|
349
264
|
|
|
350
|
-
// Check if file exists
|
|
351
265
|
if (existsSync(fullPath)) {
|
|
352
|
-
// For CLAUDE.md, only append @import line (preserve user content)
|
|
353
266
|
if (fullPath === 'CLAUDE.md') {
|
|
354
267
|
const existing = readFileSync(fullPath, 'utf-8');
|
|
355
268
|
if (!existing.includes('@import ROPILOT.md')) {
|
|
356
269
|
writeFileSync(fullPath, existing.trimEnd() + '\n\n@import ROPILOT.md\n');
|
|
357
|
-
|
|
270
|
+
updated++;
|
|
358
271
|
} else {
|
|
359
|
-
|
|
272
|
+
skipped++;
|
|
360
273
|
}
|
|
361
274
|
} else if (preserveFiles.includes(fullPath)) {
|
|
362
|
-
|
|
363
|
-
console.log(` Exists: ${fullPath}`);
|
|
275
|
+
skipped++;
|
|
364
276
|
} else {
|
|
365
|
-
// Overwrite Ropilot-managed files with latest content
|
|
366
277
|
writeFileSync(fullPath, content);
|
|
367
|
-
|
|
278
|
+
updated++;
|
|
368
279
|
}
|
|
369
280
|
} else {
|
|
370
281
|
writeFileSync(fullPath, content);
|
|
371
|
-
|
|
282
|
+
created++;
|
|
372
283
|
}
|
|
373
284
|
}
|
|
285
|
+
|
|
286
|
+
console.log(`Project files: ${created} created, ${updated} updated, ${skipped} unchanged`);
|
|
374
287
|
}
|
|
375
288
|
|
|
376
289
|
/**
|
|
377
290
|
* Initialize Ropilot in current project
|
|
378
291
|
*/
|
|
379
292
|
export async function init(providedApiKey = null) {
|
|
380
|
-
console.log('');
|
|
381
|
-
console.log('Welcome to Ropilot!');
|
|
382
|
-
console.log('===================');
|
|
383
|
-
console.log('');
|
|
293
|
+
console.log('\nRopilot Setup\n');
|
|
384
294
|
|
|
385
|
-
// Always prompt for API key (per-project config)
|
|
386
295
|
const existingKey = loadProjectConfig()?.apiKey;
|
|
387
296
|
const defaultHint = existingKey ? ` [${existingKey.slice(0, 20)}...]` : '';
|
|
388
297
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
298
|
+
if (!existingKey) {
|
|
299
|
+
console.log('Get your API key at: https://ropilot.ai\n');
|
|
300
|
+
}
|
|
392
301
|
|
|
393
302
|
let apiKey = providedApiKey;
|
|
394
303
|
if (!apiKey) {
|
|
395
|
-
apiKey = await prompt(`
|
|
304
|
+
apiKey = await prompt(`API key${defaultHint}: `);
|
|
396
305
|
|
|
397
|
-
// If empty and existing key, use existing
|
|
398
306
|
if (!apiKey && existingKey) {
|
|
399
307
|
apiKey = existingKey;
|
|
400
|
-
console.log(`Using existing key: ${apiKey.slice(0, 20)}...`);
|
|
401
308
|
}
|
|
402
309
|
}
|
|
403
310
|
|
|
@@ -406,45 +313,26 @@ export async function init(providedApiKey = null) {
|
|
|
406
313
|
process.exit(1);
|
|
407
314
|
}
|
|
408
315
|
|
|
409
|
-
// Save to project config (.ropilot.json)
|
|
410
316
|
setProjectApiKey(apiKey);
|
|
411
|
-
console.log('API key saved to .ropilot.json');
|
|
412
|
-
console.log('');
|
|
413
|
-
|
|
317
|
+
console.log('API key saved to .ropilot.json\n');
|
|
414
318
|
|
|
415
|
-
// Install Studio plugin
|
|
416
319
|
await installPlugin();
|
|
417
|
-
console.log('');
|
|
418
|
-
|
|
419
|
-
// Download prompts
|
|
420
320
|
const pkg = await downloadPrompts(apiKey);
|
|
421
|
-
console.log('');
|
|
422
|
-
|
|
423
|
-
// Set up project files (includes .mcp.json from package)
|
|
424
|
-
console.log('Setting up project files...');
|
|
425
321
|
setupProjectPrompts(pkg);
|
|
426
|
-
console.log('');
|
|
427
322
|
|
|
428
|
-
|
|
429
|
-
console.log('Setup complete!');
|
|
430
|
-
console.log('');
|
|
323
|
+
console.log('\nSetup complete!\n');
|
|
431
324
|
console.log('Next steps:');
|
|
432
|
-
console.log('1. Open
|
|
433
|
-
console.log('2.
|
|
434
|
-
console.log('3.
|
|
435
|
-
console.log('');
|
|
325
|
+
console.log('1. Open Studio → Plugins tab → Click "Ropilot AI"');
|
|
326
|
+
console.log('2. Enter your API key and click Connect');
|
|
327
|
+
console.log('3. Start Claude Code or Cursor in this directory\n');
|
|
436
328
|
}
|
|
437
329
|
|
|
438
330
|
/**
|
|
439
331
|
* Update prompts to latest version
|
|
440
332
|
*/
|
|
441
333
|
export async function update() {
|
|
442
|
-
console.log('');
|
|
443
|
-
console.log('Updating Ropilot...');
|
|
444
|
-
console.log('===================');
|
|
445
|
-
console.log('');
|
|
334
|
+
console.log('\nUpdating Ropilot...\n');
|
|
446
335
|
|
|
447
|
-
// Check project config first, then global
|
|
448
336
|
const projectConfig = loadProjectConfig();
|
|
449
337
|
const apiKey = projectConfig?.apiKey || getApiKey();
|
|
450
338
|
|
|
@@ -453,25 +341,9 @@ export async function update() {
|
|
|
453
341
|
process.exit(1);
|
|
454
342
|
}
|
|
455
343
|
|
|
456
|
-
const source = projectConfig?.apiKey ? '.ropilot.json' : '~/.ropilot/config.json';
|
|
457
|
-
console.log(`Using API key from ${source}: ${apiKey.slice(0, 20)}...`);
|
|
458
|
-
console.log('');
|
|
459
|
-
|
|
460
|
-
// Re-install Studio plugin
|
|
461
344
|
await installPlugin();
|
|
462
|
-
console.log('');
|
|
463
|
-
|
|
464
|
-
// Re-download prompts
|
|
465
345
|
const pkg = await downloadPrompts(apiKey);
|
|
466
|
-
console.log('');
|
|
467
|
-
|
|
468
|
-
// Re-write project files
|
|
469
|
-
console.log('Updating project files...');
|
|
470
346
|
setupProjectPrompts(pkg);
|
|
471
|
-
console.log('');
|
|
472
347
|
|
|
473
|
-
console.log('
|
|
474
|
-
console.log('');
|
|
475
|
-
console.log('Note: Check the Studio Plugins tab to verify the updated plugin is loaded.');
|
|
476
|
-
console.log('');
|
|
348
|
+
console.log('\nUpdate complete! Restart Studio to reload the plugin.\n');
|
|
477
349
|
}
|