@weppy/roblox-mcp 2.2.2 → 2.3.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.
Files changed (75) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.github/workflows/install-test.yml +108 -0
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +14 -10
  5. package/docs/en/installation/README.md +4 -4
  6. package/docs/en/installation/roblox-explorer.md +13 -3
  7. package/docs/en/installation/roblox-plugin.md +42 -31
  8. package/docs/en/pro-upgrade.md +26 -12
  9. package/docs/en/sync/luau-lsp.md +41 -0
  10. package/docs/en/sync/overview.md +4 -1
  11. package/docs/es/README.md +13 -7
  12. package/docs/es/installation/README.md +4 -4
  13. package/docs/es/pro-upgrade.md +26 -12
  14. package/docs/es/sync/luau-lsp.md +41 -0
  15. package/docs/es/sync/overview.md +4 -1
  16. package/docs/id/README.md +12 -6
  17. package/docs/id/installation/README.md +4 -4
  18. package/docs/id/pro-upgrade.md +26 -12
  19. package/docs/id/sync/luau-lsp.md +41 -0
  20. package/docs/id/sync/overview.md +4 -1
  21. package/docs/installer/assets/index-Bz0amd7x.js +63 -0
  22. package/docs/installer/assets/index-ei4lRUa6.css +1 -0
  23. package/docs/installer/index.html +14 -0
  24. package/docs/installer/manifest.webmanifest +15 -0
  25. package/docs/installer/sw.js +7 -0
  26. package/docs/installer/wrox-icon.png +0 -0
  27. package/docs/ja/README.md +14 -10
  28. package/docs/ja/installation/README.md +3 -3
  29. package/docs/ja/pro-upgrade.md +26 -12
  30. package/docs/ja/sync/luau-lsp.md +41 -0
  31. package/docs/ja/sync/overview.md +4 -1
  32. package/docs/ko/README.md +14 -10
  33. package/docs/ko/installation/README.md +4 -4
  34. package/docs/ko/installation/roblox-explorer.md +13 -3
  35. package/docs/ko/installation/roblox-plugin.md +42 -31
  36. package/docs/ko/pro-upgrade.md +26 -12
  37. package/docs/ko/sync/luau-lsp.md +41 -0
  38. package/docs/ko/sync/overview.md +4 -1
  39. package/docs/pt-br/README.md +13 -7
  40. package/docs/pt-br/installation/README.md +4 -4
  41. package/docs/pt-br/pro-upgrade.md +26 -12
  42. package/docs/pt-br/sync/luau-lsp.md +41 -0
  43. package/docs/pt-br/sync/overview.md +4 -1
  44. package/docs/troubleshooting.md +1 -1
  45. package/install.ps1 +468 -98
  46. package/install.sh +461 -71
  47. package/llms-full.txt +14 -2
  48. package/llms.txt +10 -4
  49. package/package.json +1 -1
  50. package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +1 -1
  51. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ChangelogDetailPage-D7eMrarv.js → ChangelogDetailPage-ITTDURna.js} +1 -1
  52. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ChangelogPage-DFCCRyyK.js → ChangelogPage-DjVot-60.js} +1 -1
  53. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ConfirmModal-BmRJ2JXZ.js → ConfirmModal-B1q8BGeA.js} +1 -1
  54. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-9bG71eB1.css +1 -0
  55. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-D4y36l03.js +1 -0
  56. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{InfoLabel-CCDWZLC9.js → InfoLabel-COMRAIq0.js} +1 -1
  57. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{OverviewPage-BHpt3LI2.js → OverviewPage-DojgIxVT.js} +1 -1
  58. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{PlaytestPage-CNwwI5Ro.js → PlaytestPage-CkEvf6XW.js} +1 -1
  59. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/PropertyDiff-r9iLxfi8.js +6 -0
  60. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{SettingsPage-CPqQYZPN.js → SettingsPage-CEs6Ob3d.js} +1 -1
  61. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{StatusBadge-C8VKAPpk.js → StatusBadge-DHhSWmAT.js} +1 -1
  62. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/SyncPage-ifjnfsNg.js +4 -0
  63. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{TierComparison-7ofkPwj3.js → TierComparison-C29caZ6C.js} +1 -1
  64. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{TierPromoProgress-SnRUjAPh.js → TierPromoProgress-BdEtTxkK.js} +1 -1
  65. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ToolsPage-CrdNh3D9.js → ToolsPage-BOIC0ngW.js} +1 -1
  66. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/index-CQYn3Wfp.js +129 -0
  67. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{useLiveUptime-BnXeLpOw.js → useLiveUptime-Dco8Aiiz.js} +1 -1
  68. package/plugins/weppy-roblox-mcp/dashboard/dist/index.html +1 -1
  69. package/plugins/weppy-roblox-mcp/dist/index.js +86 -83
  70. package/plugins/weppy-roblox-mcp/roblox-plugin/WeppyRobloxMCP.rbxm +0 -0
  71. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-CN3LYLAT.css +0 -1
  72. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-CiaCY026.js +0 -1
  73. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/PropertyDiff-DIplDn-J.js +0 -6
  74. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/SyncPage-DTSKbpio.js +0 -4
  75. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/index-DGGmfli1.js +0 -129
package/install.ps1 CHANGED
@@ -4,17 +4,15 @@
4
4
  # Usage:
5
5
  # irm https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/install.ps1 | iex
6
6
  #
7
- # Interactive 3 steps:
8
- # [1/3] MCP server install (npm)
9
- # [2/3] Roblox Studio Plugin install (.rbxm)
10
- # [3/3] Register MCP with AI apps (user selection)
7
+ # Interactive 2 steps:
8
+ # [1/2] Setup install Roblox Studio Plugin via npx
9
+ # [2/2] Register MCP with AI apps (user selection)
11
10
  #
12
11
 
13
12
  $ErrorActionPreference = "Stop"
14
13
  $script:InstallLogPath = Join-Path ([System.IO.Path]::GetTempPath()) ("wrox-install-{0:yyyyMMdd-HHmmss}.log" -f (Get-Date))
15
14
  $script:TranscriptStarted = $false
16
15
  $script:NpmCommandPath = $null
17
- $script:NpmGlobalPrefix = $null
18
16
 
19
17
  # ── Utilities ──
20
18
  function Write-Step($step, $msg) { Write-Host "`n[$step] $msg" -ForegroundColor Cyan -NoNewline; Write-Host "" }
@@ -50,6 +48,10 @@ trap {
50
48
  }
51
49
 
52
50
  function Confirm-Action($prompt) {
51
+ if ($env:CI -eq 'true') {
52
+ Write-Host "$prompt (Y/n): Y"
53
+ return $true
54
+ }
53
55
  $reply = Read-Host "$prompt (Y/n)"
54
56
  if ([string]::IsNullOrWhiteSpace($reply)) { $reply = "Y" }
55
57
  return $reply -match '^[Yy]'
@@ -69,30 +71,6 @@ function Resolve-NpmCommand() {
69
71
  return $script:NpmCommandPath
70
72
  }
71
73
 
72
- function Get-NpmGlobalPrefix() {
73
- if ($script:NpmGlobalPrefix) {
74
- return $script:NpmGlobalPrefix
75
- }
76
-
77
- $script:NpmGlobalPrefix = (Invoke-Npm prefix -g 2>$null | Out-String).Trim()
78
- return $script:NpmGlobalPrefix
79
- }
80
-
81
- function Invoke-Npm {
82
- param(
83
- [Parameter(ValueFromRemainingArguments = $true)]
84
- [string[]]$Args
85
- )
86
-
87
- $npmCommandPath = Resolve-NpmCommand
88
- $output = & $npmCommandPath @Args
89
- if ($LASTEXITCODE -ne 0) {
90
- throw "npm $($Args -join ' ') failed with exit code $LASTEXITCODE"
91
- }
92
-
93
- return $output
94
- }
95
-
96
74
  function Resolve-OptionalCliCommand($commandName) {
97
75
  $resolvedCommand = Get-Command $commandName -ErrorAction SilentlyContinue
98
76
  if ($resolvedCommand) {
@@ -100,11 +78,6 @@ function Resolve-OptionalCliCommand($commandName) {
100
78
  }
101
79
 
102
80
  $candidatePaths = @()
103
- $npmPrefix = Get-NpmGlobalPrefix
104
- if ($npmPrefix) {
105
- $candidatePaths += (Join-Path $npmPrefix "$commandName.cmd")
106
- $candidatePaths += (Join-Path $npmPrefix $commandName)
107
- }
108
81
 
109
82
  if ($env:APPDATA) {
110
83
  $appDataNpmDir = Join-Path $env:APPDATA 'npm'
@@ -139,7 +112,7 @@ function Test-McpJsonConfigured($configPath) {
139
112
  }
140
113
  }
141
114
 
142
- # Add MCP server to Antigravity config (flat top-level keys, no mcpServers wrapper)
115
+ # Antigravity 설정에 canonical mcpServers 래퍼로 MCP 서버를 추가하고 legacy flat key를 정리
143
116
  function Add-AntigravityMcpConfig($configPath) {
144
117
  $parentDir = Split-Path $configPath -Parent
145
118
  if (-not (Test-Path $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null }
@@ -150,7 +123,20 @@ const fs = require('fs');
150
123
  const configPath = process.env.MCP_CONFIG_PATH;
151
124
  let config = {};
152
125
  try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
153
- config['weppy-roblox-mcp'] = { command: 'npx', args: ['-y', '@weppy/roblox-mcp'] };
126
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
127
+ config = {};
128
+ }
129
+ const mcpServers = config.mcpServers;
130
+ if (mcpServers !== undefined && (typeof mcpServers !== 'object' || mcpServers === null || Array.isArray(mcpServers))) {
131
+ throw new Error('Antigravity mcpServers must be an object');
132
+ }
133
+ const next = { ...config };
134
+ delete next['weppy-roblox-mcp'];
135
+ next.mcpServers = {
136
+ ...(mcpServers || {}),
137
+ 'weppy-roblox-mcp': { command: 'npx', args: ['-y', '@weppy/roblox-mcp'] }
138
+ };
139
+ config = next;
154
140
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
155
141
  "@
156
142
  } finally {
@@ -165,7 +151,10 @@ function Test-AntigravityMcpConfigured($configPath) {
165
151
 
166
152
  try {
167
153
  $config = Get-Content -Path $configPath -Raw | ConvertFrom-Json
168
- return $null -ne $config.'weppy-roblox-mcp'
154
+ $hasLegacyFlatKey = $config.PSObject.Properties.Name -contains 'weppy-roblox-mcp'
155
+ $server = $config.mcpServers.'weppy-roblox-mcp'
156
+ $hasCanonicalArgs = ($server.args -is [System.Array]) -and ($server.args.Count -eq 2) -and ($server.args[0] -eq '-y') -and ($server.args[1] -eq '@weppy/roblox-mcp')
157
+ return ($server.command -eq 'npx') -and $hasCanonicalArgs -and (-not $hasLegacyFlatKey)
169
158
  }
170
159
  catch {
171
160
  return $false
@@ -177,7 +166,421 @@ function Test-CodexConfigConfigured($configPath) {
177
166
  return $false
178
167
  }
179
168
 
180
- return Select-String -Path $configPath -Pattern '^\s*\[mcp_servers\.weppy-roblox-mcp\]\s*$' -Quiet
169
+ $env:MCP_CODEX_CONFIG_PATH = $configPath
170
+ try {
171
+ node -e @'
172
+ const fs = require('fs');
173
+
174
+ const configPath = process.env.MCP_CODEX_CONFIG_PATH;
175
+ const serverName = 'weppy-roblox-mcp';
176
+ const expectedCommand = 'npx';
177
+ const expectedArgs = ['-y', '@weppy/roblox-mcp'];
178
+ const headerPattern = new RegExp(
179
+ '^\\s*\\[\\s*mcp_servers\\.' + serverName.replace(/[.*+?^${}()|[\]\\\\]/g, '\\$&') + '\\s*\\]\\s*(?:#.*)?$'
180
+ );
181
+
182
+ function stripCommentOutsideStrings(line) {
183
+ let inSingle = false;
184
+ let inDouble = false;
185
+ let escaped = false;
186
+
187
+ for (let index = 0; index < line.length; index += 1) {
188
+ const char = line[index];
189
+
190
+ if (char === '"' && !inSingle && !escaped) {
191
+ inDouble = !inDouble;
192
+ } else if (char === "'" && !inDouble && !escaped) {
193
+ inSingle = !inSingle;
194
+ } else if (char === '#' && !inSingle && !inDouble) {
195
+ return line.slice(0, index).trimEnd();
196
+ }
197
+
198
+ escaped = char === '\\' && !escaped;
199
+ if (char !== '\\') {
200
+ escaped = false;
201
+ }
202
+ }
203
+
204
+ return line.trimEnd();
205
+ }
206
+
207
+ function countTripleQuoteToggles(line, quote) {
208
+ let count = 0;
209
+ let inSingle = false;
210
+ let inDouble = false;
211
+ let escaped = false;
212
+
213
+ for (let index = 0; index < line.length; index += 1) {
214
+ const char = line[index] ?? '';
215
+ const nextThree = line.slice(index, index + 3);
216
+ const isOutsideStrings = !inSingle && !inDouble;
217
+
218
+ if (isOutsideStrings && nextThree === quote.repeat(3)) {
219
+ count += 1;
220
+ index += 2;
221
+ escaped = false;
222
+ continue;
223
+ }
224
+
225
+ if (char === '"' && !inSingle && !escaped) {
226
+ inDouble = !inDouble;
227
+ } else if (char === "'" && !inDouble && !escaped) {
228
+ inSingle = !inSingle;
229
+ } else if (char === '#' && !inSingle && !inDouble) {
230
+ break;
231
+ }
232
+
233
+ escaped = char === '\\' && !escaped;
234
+ if (char !== '\\') {
235
+ escaped = false;
236
+ }
237
+ }
238
+
239
+ return count;
240
+ }
241
+
242
+ function advanceTripleQuoteState(line, state) {
243
+ const next = { ...state };
244
+ const tripleDoubleCount = countTripleQuoteToggles(line, '"');
245
+ const tripleSingleCount = countTripleQuoteToggles(line, "'");
246
+
247
+ if (!next.inTripleSingle && tripleDoubleCount % 2 === 1) {
248
+ next.inTripleDouble = !next.inTripleDouble;
249
+ }
250
+
251
+ if (!next.inTripleDouble && tripleSingleCount % 2 === 1) {
252
+ next.inTripleSingle = !next.inTripleSingle;
253
+ }
254
+
255
+ return next;
256
+ }
257
+
258
+ function isTomlTableHeaderLine(line) {
259
+ const normalized = stripCommentOutsideStrings(line).trim();
260
+
261
+ if (normalized.length === 0) {
262
+ return false;
263
+ }
264
+
265
+ return /^\[\[.*\]\]$/.test(normalized) || /^\[.*\]$/.test(normalized);
266
+ }
267
+
268
+ function findAllCodexBlocks(source) {
269
+ const lines = source.split('\n');
270
+ const blocks = [];
271
+ let activeLines = null;
272
+ let state = {
273
+ inTripleDouble: false,
274
+ inTripleSingle: false,
275
+ };
276
+
277
+ for (const line of lines) {
278
+ const isHeaderCandidate = !state.inTripleDouble && !state.inTripleSingle && isTomlTableHeaderLine(line);
279
+ const isCodexHeader = isHeaderCandidate && headerPattern.test(line);
280
+
281
+ if (isCodexHeader) {
282
+ if (activeLines !== null) {
283
+ blocks.push(activeLines.join('\n').trim());
284
+ }
285
+ activeLines = [line];
286
+ } else if (activeLines !== null && isHeaderCandidate) {
287
+ blocks.push(activeLines.join('\n').trim());
288
+ activeLines = null;
289
+ } else if (activeLines !== null) {
290
+ activeLines.push(line);
291
+ }
292
+
293
+ state = advanceTripleQuoteState(line, state);
294
+ }
295
+
296
+ if (activeLines !== null) {
297
+ blocks.push(activeLines.join('\n').trim());
298
+ }
299
+
300
+ return blocks;
301
+ }
302
+
303
+ function parseStringAssignment(value, key) {
304
+ const match = new RegExp('^\\s*' + key + '\\s*=\\s*(["\\'])([^"\\']+)\\1\\s*$').exec(value);
305
+ return match ? match[2] : null;
306
+ }
307
+
308
+ function parseTomlStringArray(value) {
309
+ const match = /^\s*args\s*=\s*\[(.*)\]\s*$/ms.exec(value.trim());
310
+
311
+ if (match === null) {
312
+ return null;
313
+ }
314
+
315
+ const body = match[1] ?? '';
316
+ const values = [];
317
+ let cursor = 0;
318
+ let expectValue = true;
319
+
320
+ while (cursor < body.length) {
321
+ while (cursor < body.length && /\s/.test(body[cursor] ?? '')) {
322
+ cursor += 1;
323
+ }
324
+
325
+ if (cursor >= body.length) {
326
+ break;
327
+ }
328
+
329
+ if (!expectValue) {
330
+ if (body[cursor] !== ',') {
331
+ return null;
332
+ }
333
+ cursor += 1;
334
+ expectValue = true;
335
+ continue;
336
+ }
337
+
338
+ const quote = body[cursor];
339
+ if (quote !== '"' && quote !== "'") {
340
+ return null;
341
+ }
342
+
343
+ cursor += 1;
344
+ let token = '';
345
+ let escaped = false;
346
+
347
+ while (cursor < body.length) {
348
+ const char = body[cursor] ?? '';
349
+
350
+ if (char === quote && !escaped) {
351
+ cursor += 1;
352
+ values.push(token);
353
+ break;
354
+ }
355
+
356
+ token += char;
357
+ escaped = char === '\\' && !escaped;
358
+ if (char !== '\\') {
359
+ escaped = false;
360
+ }
361
+ cursor += 1;
362
+ }
363
+
364
+ if (cursor > body.length) {
365
+ return null;
366
+ }
367
+
368
+ expectValue = false;
369
+ }
370
+
371
+ const leftover = body.slice(cursor).trim();
372
+ if (leftover === ',') {
373
+ return values;
374
+ }
375
+
376
+ return leftover.length === 0 ? values : null;
377
+ }
378
+
379
+ function collectArrayLines(lines, startIndex) {
380
+ const collected = [stripCommentOutsideStrings(lines[startIndex] ?? '')];
381
+ let bracketDepth = 0;
382
+ let inSingle = false;
383
+ let inDouble = false;
384
+ let escaped = false;
385
+
386
+ for (let lineIndex = startIndex; lineIndex < lines.length; lineIndex += 1) {
387
+ const sanitized = stripCommentOutsideStrings(lines[lineIndex] ?? '');
388
+ if (lineIndex !== startIndex) {
389
+ collected.push(sanitized);
390
+ }
391
+
392
+ for (let index = 0; index < sanitized.length; index += 1) {
393
+ const char = sanitized[index] ?? '';
394
+
395
+ if (char === '"' && !inSingle && !escaped) {
396
+ inDouble = !inDouble;
397
+ } else if (char === "'" && !inDouble && !escaped) {
398
+ inSingle = !inSingle;
399
+ } else if (!inSingle && !inDouble) {
400
+ if (char === '[') {
401
+ bracketDepth += 1;
402
+ } else if (char === ']') {
403
+ bracketDepth -= 1;
404
+ }
405
+ }
406
+
407
+ escaped = char === '\\' && !escaped;
408
+ if (char !== '\\') {
409
+ escaped = false;
410
+ }
411
+ }
412
+
413
+ if (bracketDepth <= 0) {
414
+ return {
415
+ nextIndex: lineIndex,
416
+ text: collected.join('\n'),
417
+ };
418
+ }
419
+ }
420
+
421
+ return null;
422
+ }
423
+
424
+ function parseCodexBlock(blockContent) {
425
+ const lines = blockContent.split('\n');
426
+ let command = null;
427
+ let args = null;
428
+ let hasConflict = false;
429
+ let inTripleDouble = false;
430
+ let inTripleSingle = false;
431
+
432
+ for (let index = 1; index < lines.length; index += 1) {
433
+ const line = lines[index] ?? '';
434
+ const sanitized = stripCommentOutsideStrings(line);
435
+ const trimmed = sanitized.trim();
436
+
437
+ if (inTripleDouble) {
438
+ if (countTripleQuoteToggles(sanitized, '"') % 2 === 1) {
439
+ inTripleDouble = false;
440
+ }
441
+ continue;
442
+ }
443
+
444
+ if (inTripleSingle) {
445
+ if (countTripleQuoteToggles(sanitized, "'") % 2 === 1) {
446
+ inTripleSingle = false;
447
+ }
448
+ continue;
449
+ }
450
+
451
+ if (countTripleQuoteToggles(sanitized, '"') % 2 === 1) {
452
+ inTripleDouble = true;
453
+ continue;
454
+ }
455
+
456
+ if (countTripleQuoteToggles(sanitized, "'") % 2 === 1) {
457
+ inTripleSingle = true;
458
+ continue;
459
+ }
460
+
461
+ if (trimmed.length === 0) {
462
+ continue;
463
+ }
464
+
465
+ if (/^command\s*=/.test(trimmed)) {
466
+ const parsedCommand = parseStringAssignment(trimmed, 'command');
467
+ if (command !== null || parsedCommand === null) {
468
+ hasConflict = true;
469
+ } else {
470
+ command = parsedCommand;
471
+ }
472
+ continue;
473
+ }
474
+
475
+ if (/^args\s*=/.test(trimmed)) {
476
+ const collected = collectArrayLines(lines, index);
477
+ const parsedArgs = collected === null ? null : parseTomlStringArray(collected.text);
478
+
479
+ if (args !== null || parsedArgs === null || collected === null) {
480
+ hasConflict = true;
481
+ } else {
482
+ args = parsedArgs;
483
+ index = collected.nextIndex;
484
+ }
485
+ }
486
+ }
487
+
488
+ return {
489
+ args,
490
+ command,
491
+ hasConflict,
492
+ };
493
+ }
494
+
495
+ function isStructurallySafe(source) {
496
+ let bracketDepth = 0;
497
+ let braceDepth = 0;
498
+ let inSingle = false;
499
+ let inDouble = false;
500
+ let escaped = false;
501
+ let tripleState = {
502
+ inTripleDouble: false,
503
+ inTripleSingle: false,
504
+ };
505
+
506
+ for (const line of source.split('\n')) {
507
+ tripleState = advanceTripleQuoteState(line, tripleState);
508
+
509
+ for (let index = 0; index < line.length; index += 1) {
510
+ const char = line[index] ?? '';
511
+
512
+ if (!inSingle && !inDouble && char === '#') {
513
+ break;
514
+ }
515
+
516
+ if (char === '"' && !inSingle && !escaped) {
517
+ inDouble = !inDouble;
518
+ } else if (char === "'" && !inDouble && !escaped) {
519
+ inSingle = !inSingle;
520
+ } else if (!inSingle && !inDouble) {
521
+ if (char === '[') {
522
+ bracketDepth += 1;
523
+ } else if (char === ']') {
524
+ bracketDepth -= 1;
525
+ if (bracketDepth < 0) {
526
+ return false;
527
+ }
528
+ } else if (char === '{') {
529
+ braceDepth += 1;
530
+ } else if (char === '}') {
531
+ braceDepth -= 1;
532
+ if (braceDepth < 0) {
533
+ return false;
534
+ }
535
+ }
536
+ }
537
+
538
+ escaped = char === '\\' && !escaped;
539
+ if (char !== '\\') {
540
+ escaped = false;
541
+ }
542
+ }
543
+ }
544
+
545
+ return (
546
+ !tripleState.inTripleDouble &&
547
+ !tripleState.inTripleSingle &&
548
+ bracketDepth === 0 &&
549
+ braceDepth === 0 &&
550
+ !inSingle &&
551
+ !inDouble
552
+ );
553
+ }
554
+
555
+ try {
556
+ const source = fs.readFileSync(configPath, 'utf8');
557
+ if (!isStructurallySafe(source)) {
558
+ process.exit(1);
559
+ }
560
+
561
+ const blocks = findAllCodexBlocks(source);
562
+ if (blocks.length !== 1) {
563
+ process.exit(1);
564
+ }
565
+
566
+ const parsed = parseCodexBlock(blocks[0]);
567
+ const isConfigured =
568
+ !parsed.hasConflict &&
569
+ parsed.command === expectedCommand &&
570
+ Array.isArray(parsed.args) &&
571
+ parsed.args.length === expectedArgs.length &&
572
+ parsed.args.every((entry, index) => entry === expectedArgs[index]);
573
+
574
+ process.exit(isConfigured ? 0 : 1);
575
+ } catch {
576
+ process.exit(1);
577
+ }
578
+ '@
579
+ return $LASTEXITCODE -eq 0
580
+ }
581
+ finally {
582
+ Remove-Item Env:\MCP_CODEX_CONFIG_PATH -ErrorAction SilentlyContinue
583
+ }
181
584
  }
182
585
 
183
586
  # Add MCP server to JSON config file (PowerShell 5.1 compatible — edits JSON via node)
@@ -200,11 +603,6 @@ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
200
603
  }
201
604
  }
202
605
 
203
- function Test-LfsPointer($filePath) {
204
- if (-not (Test-Path $filePath)) { return $false }
205
- return Select-String -Path $filePath -Pattern "git-lfs.github.com/spec/v1" -Quiet
206
- }
207
-
208
606
  # ── Header ──
209
607
  Write-Host ""
210
608
  Write-Host "WROX Installer" -ForegroundColor White -BackgroundColor DarkCyan
@@ -225,71 +623,37 @@ catch {
225
623
  }
226
624
 
227
625
  # ═══════════════════════════════════
228
- # [1/3] MCP server install
626
+ # [1/2] Setup Roblox Studio Plugin
229
627
  # ═══════════════════════════════════
230
- Write-Step "1/3" "Install @weppy/roblox-mcp via npm"
628
+ Write-Step "1/2" "Setup Roblox Studio Plugin"
231
629
 
232
- if (Confirm-Action " Run npm install -g @weppy/roblox-mcp?") {
630
+ if (Confirm-Action " Run npx -y @weppy/roblox-mcp --setup?") {
233
631
  try {
234
- Invoke-Npm install -g "@weppy/roblox-mcp"
235
- Write-Ok "Installed @weppy/roblox-mcp"
236
- }
237
- catch {
238
- Abort-Install "npm install failed: $_"
239
- }
240
- }
241
- else {
242
- Write-Warn "MCP server install skipped"
243
- }
244
-
245
- # ═══════════════════════════════════
246
- # [2/3] Roblox Studio Plugin install
247
- # ═══════════════════════════════════
248
- Write-Step "2/3" "Install Roblox Studio Plugin"
249
-
250
- $pluginsDir = Join-Path $env:LOCALAPPDATA "Roblox\Plugins"
251
- $pluginName = "WeppyRobloxMCP.rbxm"
252
-
253
- # Search for .rbxm in npm global path
254
- $npmPrefix = Get-NpmGlobalPrefix
255
- $bundledPlugin = $null
256
- $searchPaths = @(
257
- (Join-Path $npmPrefix "node_modules\@weppy\roblox-mcp\plugins\weppy-roblox-mcp\roblox-plugin\$pluginName"),
258
- (Join-Path $npmPrefix "node_modules\@weppy\roblox-mcp\roblox-plugin\$pluginName")
259
- )
260
- foreach ($p in $searchPaths) {
261
- if (Test-Path $p) {
262
- $bundledPlugin = $p
263
- break
264
- }
265
- }
266
-
267
- if ($bundledPlugin) {
268
- if (Test-LfsPointer $bundledPlugin) {
269
- Abort-Install "Bundled plugin payload is invalid (Git LFS pointer detected). Install the plugin from the GitHub release ZIP instead."
270
- }
271
-
272
- Write-Host " → $pluginsDir\$pluginName"
273
- if (Confirm-Action " Copy plugin to Roblox Plugins folder?") {
274
- if (-not (Test-Path $pluginsDir)) {
275
- New-Item -ItemType Directory -Path $pluginsDir -Force | Out-Null
632
+ $npmCommandPath = Resolve-NpmCommand
633
+ $npmDir = Split-Path $npmCommandPath -Parent
634
+ $npxPath = Join-Path $npmDir "npx.cmd"
635
+ if (-not (Test-Path $npxPath)) {
636
+ $npxPath = "npx"
637
+ }
638
+ & $npxPath -y "@weppy/roblox-mcp" --setup
639
+ if ($LASTEXITCODE -ne 0) {
640
+ Write-Warn "Setup encountered a warning (non-blocking)"
641
+ } else {
642
+ Write-Ok "Setup complete"
276
643
  }
277
- Copy-Item $bundledPlugin -Destination (Join-Path $pluginsDir $pluginName) -Force
278
- Write-Ok "Plugin installed → $pluginsDir\$pluginName"
279
644
  }
280
- else {
281
- Write-Warn "Plugin install skipped"
645
+ catch {
646
+ Write-Warn "Setup encountered a warning: $_"
282
647
  }
283
648
  }
284
649
  else {
285
- Write-Warn "Bundled plugin file not found"
286
- Write-Info "Will be installed automatically on first MCP server run"
650
+ Write-Warn "Setup skipped"
287
651
  }
288
652
 
289
653
  # ═══════════════════════════════════
290
- # [3/3] Register MCP with AI apps
654
+ # [2/2] Register MCP with AI apps
291
655
  # ═══════════════════════════════════
292
- Write-Step "3/3" "Register MCP with AI apps"
656
+ Write-Step "2/2" "Register MCP with AI apps"
293
657
  Write-Host " Automatic registration: Claude Code, Claude Desktop, Cursor, Codex CLI/App, Gemini CLI, Antigravity"
294
658
 
295
659
  $detectedNames = @()
@@ -403,7 +767,12 @@ else {
403
767
  }
404
768
 
405
769
  Write-Host ""
406
- $selection = Read-Host " Select apps to register (comma-separated, 'a' for all, 'n' to skip)"
770
+ if ($env:CI -eq 'true') {
771
+ Write-Host " Select apps to register (comma-separated, 'a' for all, 'n' to skip): a"
772
+ $selection = 'a'
773
+ } else {
774
+ $selection = Read-Host " Select apps to register (comma-separated, 'a' for all, 'n' to skip)"
775
+ }
407
776
  if ([string]::IsNullOrWhiteSpace($selection)) { $selection = "n" }
408
777
 
409
778
  $selectedIndices = @()
@@ -436,7 +805,8 @@ else {
436
805
  elseif ($claudeCodeCliCommand) {
437
806
  & $claudeCodeCliCommand mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp"
438
807
  if ($LASTEXITCODE -ne 0) {
439
- throw 'claude mcp add failed'
808
+ # CLI 실패 (Windows에서 -- 파싱 문제 등) JSON config에 직접 쓰기로 폴백
809
+ Add-McpToConfig $claudeGlobalConfig
440
810
  }
441
811
  Write-Ok "Registered: $appName"
442
812
  }