@weppy/roblox-mcp 2.7.5 → 2.7.7
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 +23 -0
- package/README.md +25 -5
- package/{plugins/weppy-roblox-mcp/dist → dist}/index.js +68 -68
- package/package.json +5 -5
- package/{plugins/weppy-roblox-mcp/roblox-plugin → roblox-plugin}/WeppyRobloxMCP.rbxm +0 -0
- package/.claude-plugin/marketplace.json +0 -43
- package/CODE_OF_CONDUCT.md +0 -29
- package/COMMERCIAL-LICENSE.md +0 -13
- package/CONTRIBUTING.md +0 -36
- package/SECURITY.md +0 -28
- package/SUPPORT.md +0 -25
- package/TRADEMARKS.md +0 -18
- package/glama.json +0 -7
- package/install.ps1 +0 -964
- package/install.sh +0 -939
- package/llms-full.txt +0 -13
- package/llms.txt +0 -69
- package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +0 -28
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ChangelogDetailPage-D6Tqz7ut.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ChangelogDetailPage-DglsIYkW.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ChangelogPage-65B3_w0_.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ChangelogPage-CNxAGfwG.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ConfirmModal-Cpk7SbKb.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ConnectionPage-B-IN5LsC.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ConnectionPage-CNtjimlm.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/GameChangeDetail-C1XtdYwk.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/GameChangeDetail-DM3mWsFX.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/InfoLabel-B_fEbHa7.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/OverviewPage-B4O0bv4R.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/OverviewPage-Dsfl-NRT.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/PlaytestPage-BHLRKn8U.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/PlaytestPage-DjjsIkke.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/SettingsPage-DmIKC_O1.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/SettingsPage-Du8-FZAO.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/StatusBadge-C2zYt5iE.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/StatusBadge-DRdnq30k.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/SyncPage-CW_0kNpZ.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/SyncPage-Dm7Ni3j_.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/Tabs-876h0_zB.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/Tabs-BsTVkBUh.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/TierComparison-DGh9vLz0.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/TierComparison-poRtDe46.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ToolsPage-Bt9vYA7u.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ToolsPage-D77yJ9jZ.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/TooltipText-DX5jnyNF.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/UiStudioPage-YtdlkQzT.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/UiStudioPage-eSinjpOX.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/WhatsNewPage--uCu0xCm.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/WhatsNewPage-Lxgj0StO.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/index-BPIBy2lU.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/index-CX4MHzNt.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/sample-requests-CwDMfktX.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/sample-requests-CygerZZ_.css +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ui-studio-sample-DrNTD6yi.png +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/useLiveUptime-ElD9lDzh.js +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/index.html +0 -0
- /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/weppy-icon.png +0 -0
package/install.sh
DELETED
|
@@ -1,939 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# WEPPY — One-line install script (macOS/Linux)
|
|
4
|
-
#
|
|
5
|
-
# Usage:
|
|
6
|
-
# curl -fsSL https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/install.sh | bash
|
|
7
|
-
#
|
|
8
|
-
# Interactive 2 steps:
|
|
9
|
-
# [1/2] Setup — install Roblox Studio Plugin via npx
|
|
10
|
-
# [2/2] Register MCP with AI apps (user selection)
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
set -euo pipefail
|
|
14
|
-
|
|
15
|
-
# ── Color definitions ──
|
|
16
|
-
GREEN='\033[0;32m'
|
|
17
|
-
YELLOW='\033[0;33m'
|
|
18
|
-
RED='\033[0;31m'
|
|
19
|
-
BLUE='\033[0;34m'
|
|
20
|
-
CYAN='\033[0;36m'
|
|
21
|
-
DIM='\033[2m'
|
|
22
|
-
BOLD='\033[1m'
|
|
23
|
-
NC='\033[0m'
|
|
24
|
-
|
|
25
|
-
INSTALL_LOG_FILE="$(mktemp "${TMPDIR:-/tmp}/weppy-install-XXXXXX.log" 2>/dev/null || true)"
|
|
26
|
-
if [ -z "${INSTALL_LOG_FILE:-}" ]; then
|
|
27
|
-
INSTALL_LOG_FILE="${HOME}/weppy-install-error.log"
|
|
28
|
-
: > "$INSTALL_LOG_FILE"
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
if command -v tee >/dev/null 2>&1; then
|
|
32
|
-
exec > >(tee -a "$INSTALL_LOG_FILE") 2>&1
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
# ── Utilities ──
|
|
36
|
-
# shellcheck disable=SC2059
|
|
37
|
-
info() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; }
|
|
38
|
-
# shellcheck disable=SC2059
|
|
39
|
-
success() { printf "${GREEN} ✓${NC} %s\n" "$1"; }
|
|
40
|
-
# shellcheck disable=SC2059
|
|
41
|
-
warn() { printf "${YELLOW} ⚠${NC} %s\n" "$1"; }
|
|
42
|
-
# shellcheck disable=SC2059
|
|
43
|
-
fail() { printf "${RED} ✗${NC} %s\n" "$1"; }
|
|
44
|
-
# shellcheck disable=SC2059
|
|
45
|
-
step() { printf "\n${BOLD}${CYAN}[%s]${NC} ${BOLD}%s${NC}\n" "$1" "$2"; }
|
|
46
|
-
|
|
47
|
-
pause_on_failure_if_interactive() {
|
|
48
|
-
if [ -t 1 ] && [ -r /dev/tty ]; then
|
|
49
|
-
printf "\nPress Enter to exit..." >/dev/tty
|
|
50
|
-
read -r _ </dev/tty || true
|
|
51
|
-
fi
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
handle_install_error() {
|
|
55
|
-
local exit_code=$?
|
|
56
|
-
local line_no="$1"
|
|
57
|
-
local failed_command="$2"
|
|
58
|
-
|
|
59
|
-
trap - ERR
|
|
60
|
-
|
|
61
|
-
printf "\n${RED}Installation failed.${NC}\n"
|
|
62
|
-
printf " Command: %s\n" "$failed_command"
|
|
63
|
-
printf " Line : %s\n" "$line_no"
|
|
64
|
-
printf " Log : %s\n" "$INSTALL_LOG_FILE"
|
|
65
|
-
|
|
66
|
-
pause_on_failure_if_interactive
|
|
67
|
-
exit "$exit_code"
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
trap 'handle_install_error "${LINENO}" "$BASH_COMMAND"' ERR
|
|
71
|
-
|
|
72
|
-
# Y/n prompt (default Y)
|
|
73
|
-
confirm() {
|
|
74
|
-
local prompt="$1"
|
|
75
|
-
local reply
|
|
76
|
-
if [ "${CI:-}" = "true" ]; then
|
|
77
|
-
printf "%s (Y/n): Y\n" "$prompt"
|
|
78
|
-
return 0
|
|
79
|
-
fi
|
|
80
|
-
printf "%s (Y/n): " "$prompt"
|
|
81
|
-
read -r reply </dev/tty
|
|
82
|
-
reply="${reply:-Y}"
|
|
83
|
-
case "$reply" in
|
|
84
|
-
[Yy]*) return 0 ;;
|
|
85
|
-
*) return 1 ;;
|
|
86
|
-
esac
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
# Add MCP server to JSON config file (path via env var — prevents shell injection)
|
|
90
|
-
add_mcp_to_config() {
|
|
91
|
-
local config_path="$1"
|
|
92
|
-
local parent_dir
|
|
93
|
-
parent_dir=$(dirname "$config_path")
|
|
94
|
-
mkdir -p "$parent_dir"
|
|
95
|
-
MCP_CONFIG_PATH="$config_path" node --input-type=commonjs -e '
|
|
96
|
-
const fs = require("fs");
|
|
97
|
-
const configPath = process.env.MCP_CONFIG_PATH;
|
|
98
|
-
let config = {};
|
|
99
|
-
try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
|
|
100
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
101
|
-
config.mcpServers["weppy-roblox-mcp"] = { command: "npx", args: ["-y", "@weppy/roblox-mcp@latest"] };
|
|
102
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
103
|
-
'
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
is_json_mcp_configured() {
|
|
107
|
-
local config_path="$1"
|
|
108
|
-
|
|
109
|
-
[ -f "$config_path" ] || return 1
|
|
110
|
-
|
|
111
|
-
MCP_CONFIG_PATH="$config_path" node --input-type=commonjs -e '
|
|
112
|
-
const fs = require("fs");
|
|
113
|
-
const configPath = process.env.MCP_CONFIG_PATH;
|
|
114
|
-
try {
|
|
115
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
116
|
-
process.exit(config?.mcpServers?.["weppy-roblox-mcp"] ? 0 : 1);
|
|
117
|
-
} catch {
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
' >/dev/null 2>&1
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
# Antigravity 설정에 canonical mcpServers 래퍼로 MCP 서버를 추가하고 legacy flat key를 정리
|
|
124
|
-
add_antigravity_mcp_config() {
|
|
125
|
-
local config_path="$1"
|
|
126
|
-
local parent_dir
|
|
127
|
-
parent_dir=$(dirname "$config_path")
|
|
128
|
-
mkdir -p "$parent_dir"
|
|
129
|
-
MCP_CONFIG_PATH="$config_path" node --input-type=commonjs -e '
|
|
130
|
-
const fs = require("fs");
|
|
131
|
-
const configPath = process.env.MCP_CONFIG_PATH;
|
|
132
|
-
let config = {};
|
|
133
|
-
try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
|
|
134
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
135
|
-
config = {};
|
|
136
|
-
}
|
|
137
|
-
const mcpServers = config.mcpServers;
|
|
138
|
-
if (mcpServers !== undefined && (typeof mcpServers !== "object" || mcpServers === null || Array.isArray(mcpServers))) {
|
|
139
|
-
throw new Error("Antigravity mcpServers must be an object");
|
|
140
|
-
}
|
|
141
|
-
const next = { ...config };
|
|
142
|
-
delete next["weppy-roblox-mcp"];
|
|
143
|
-
next.mcpServers = {
|
|
144
|
-
...(mcpServers || {}),
|
|
145
|
-
"weppy-roblox-mcp": { command: "npx", args: ["-y", "@weppy/roblox-mcp@latest"] }
|
|
146
|
-
};
|
|
147
|
-
config = next;
|
|
148
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
149
|
-
'
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
is_antigravity_mcp_configured() {
|
|
153
|
-
local config_path="$1"
|
|
154
|
-
|
|
155
|
-
[ -f "$config_path" ] || return 1
|
|
156
|
-
|
|
157
|
-
MCP_CONFIG_PATH="$config_path" node --input-type=commonjs -e '
|
|
158
|
-
const fs = require("fs");
|
|
159
|
-
const configPath = process.env.MCP_CONFIG_PATH;
|
|
160
|
-
function isJsonObject(value) {
|
|
161
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
162
|
-
}
|
|
163
|
-
function hasExpectedCommandShape(value) {
|
|
164
|
-
// Require an explicit `@<tag>` so the installer can upgrade legacy bare
|
|
165
|
-
// entries (`@weppy/roblox-mcp`) — those reuse npx cache and trap users on
|
|
166
|
-
// outdated versions. Tagged entries (`@latest`, `@2.6.4`, …) are preserved.
|
|
167
|
-
return (
|
|
168
|
-
isJsonObject(value) &&
|
|
169
|
-
value.command === "npx" &&
|
|
170
|
-
Array.isArray(value.args) &&
|
|
171
|
-
value.args.length === 2 &&
|
|
172
|
-
value.args[0] === "-y" &&
|
|
173
|
-
typeof value.args[1] === "string" &&
|
|
174
|
-
/^@weppy\/roblox-mcp@.+$/.test(value.args[1])
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
try {
|
|
178
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
179
|
-
const canonical = config?.mcpServers?.["weppy-roblox-mcp"];
|
|
180
|
-
const hasLegacyFlatKey = Object.prototype.hasOwnProperty.call(config, "weppy-roblox-mcp");
|
|
181
|
-
process.exit(hasExpectedCommandShape(canonical) && !hasLegacyFlatKey ? 0 : 1);
|
|
182
|
-
} catch {
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
' >/dev/null 2>&1
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
is_codex_config_configured() {
|
|
189
|
-
local config_path="$1"
|
|
190
|
-
|
|
191
|
-
[ -f "$config_path" ] || return 1
|
|
192
|
-
MCP_CODEX_CONFIG_PATH="$config_path" node --input-type=commonjs <<'NODE' >/dev/null 2>&1
|
|
193
|
-
const fs = require("fs");
|
|
194
|
-
|
|
195
|
-
const configPath = process.env.MCP_CODEX_CONFIG_PATH;
|
|
196
|
-
const serverName = "weppy-roblox-mcp";
|
|
197
|
-
const expectedCommand = "npx";
|
|
198
|
-
// Require an explicit `@<tag>` so the installer can upgrade legacy bare entries.
|
|
199
|
-
const packageSpecPattern = /^@weppy\/roblox-mcp@.+$/;
|
|
200
|
-
const headerPattern = new RegExp(
|
|
201
|
-
"^\\s*\\[\\s*mcp_servers\\." + serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\]\\s*(?:#.*)?$",
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
function stripCommentOutsideStrings(line) {
|
|
205
|
-
let inSingle = false;
|
|
206
|
-
let inDouble = false;
|
|
207
|
-
let escaped = false;
|
|
208
|
-
|
|
209
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
210
|
-
const char = line[index];
|
|
211
|
-
|
|
212
|
-
if (char === '"' && !inSingle && !escaped) {
|
|
213
|
-
inDouble = !inDouble;
|
|
214
|
-
} else if (char === "'" && !inDouble && !escaped) {
|
|
215
|
-
inSingle = !inSingle;
|
|
216
|
-
} else if (char === "#" && !inSingle && !inDouble) {
|
|
217
|
-
return line.slice(0, index).trimEnd();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
escaped = char === "\\" && !escaped;
|
|
221
|
-
if (char !== "\\") {
|
|
222
|
-
escaped = false;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return line.trimEnd();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function countTripleQuoteToggles(line, quote) {
|
|
230
|
-
let count = 0;
|
|
231
|
-
let inSingle = false;
|
|
232
|
-
let inDouble = false;
|
|
233
|
-
let escaped = false;
|
|
234
|
-
|
|
235
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
236
|
-
const char = line[index] ?? "";
|
|
237
|
-
const nextThree = line.slice(index, index + 3);
|
|
238
|
-
const isOutsideStrings = !inSingle && !inDouble;
|
|
239
|
-
|
|
240
|
-
if (isOutsideStrings && nextThree === quote.repeat(3)) {
|
|
241
|
-
count += 1;
|
|
242
|
-
index += 2;
|
|
243
|
-
escaped = false;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (char === '"' && !inSingle && !escaped) {
|
|
248
|
-
inDouble = !inDouble;
|
|
249
|
-
} else if (char === "'" && !inDouble && !escaped) {
|
|
250
|
-
inSingle = !inSingle;
|
|
251
|
-
} else if (char === "#" && !inSingle && !inDouble) {
|
|
252
|
-
break;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
escaped = char === "\\" && !escaped;
|
|
256
|
-
if (char !== "\\") {
|
|
257
|
-
escaped = false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return count;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function advanceTripleQuoteState(line, state) {
|
|
265
|
-
const next = { ...state };
|
|
266
|
-
const tripleDoubleCount = countTripleQuoteToggles(line, '"');
|
|
267
|
-
const tripleSingleCount = countTripleQuoteToggles(line, "'");
|
|
268
|
-
|
|
269
|
-
if (!next.inTripleSingle && tripleDoubleCount % 2 === 1) {
|
|
270
|
-
next.inTripleDouble = !next.inTripleDouble;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (!next.inTripleDouble && tripleSingleCount % 2 === 1) {
|
|
274
|
-
next.inTripleSingle = !next.inTripleSingle;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return next;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function isTomlTableHeaderLine(line) {
|
|
281
|
-
const normalized = stripCommentOutsideStrings(line).trim();
|
|
282
|
-
|
|
283
|
-
if (normalized.length === 0) {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return /^\[\[.*\]\]$/.test(normalized) || /^\[.*\]$/.test(normalized);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function findAllCodexBlocks(source) {
|
|
291
|
-
const lines = source.split("\n");
|
|
292
|
-
const blocks = [];
|
|
293
|
-
let activeLines = null;
|
|
294
|
-
let state = {
|
|
295
|
-
inTripleDouble: false,
|
|
296
|
-
inTripleSingle: false,
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
for (const line of lines) {
|
|
300
|
-
const isHeaderCandidate = !state.inTripleDouble && !state.inTripleSingle && isTomlTableHeaderLine(line);
|
|
301
|
-
const isCodexHeader = isHeaderCandidate && headerPattern.test(line);
|
|
302
|
-
|
|
303
|
-
if (isCodexHeader) {
|
|
304
|
-
if (activeLines !== null) {
|
|
305
|
-
blocks.push(activeLines.join("\n").trim());
|
|
306
|
-
}
|
|
307
|
-
activeLines = [line];
|
|
308
|
-
} else if (activeLines !== null && isHeaderCandidate) {
|
|
309
|
-
blocks.push(activeLines.join("\n").trim());
|
|
310
|
-
activeLines = null;
|
|
311
|
-
} else if (activeLines !== null) {
|
|
312
|
-
activeLines.push(line);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
state = advanceTripleQuoteState(line, state);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (activeLines !== null) {
|
|
319
|
-
blocks.push(activeLines.join("\n").trim());
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return blocks;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function parseStringAssignment(value, key) {
|
|
326
|
-
const match = new RegExp("^\\s*" + key + "\\s*=\\s*([\"'])([^\"']+)\\1\\s*$").exec(value);
|
|
327
|
-
return match ? match[2] : null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function parseTomlStringArray(value) {
|
|
331
|
-
const match = /^\s*args\s*=\s*\[(.*)\]\s*$/ms.exec(value.trim());
|
|
332
|
-
|
|
333
|
-
if (match === null) {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const body = match[1] ?? "";
|
|
338
|
-
const values = [];
|
|
339
|
-
let cursor = 0;
|
|
340
|
-
let expectValue = true;
|
|
341
|
-
|
|
342
|
-
while (cursor < body.length) {
|
|
343
|
-
while (cursor < body.length && /\s/.test(body[cursor] ?? "")) {
|
|
344
|
-
cursor += 1;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (cursor >= body.length) {
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (!expectValue) {
|
|
352
|
-
if (body[cursor] !== ",") {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
cursor += 1;
|
|
356
|
-
expectValue = true;
|
|
357
|
-
continue;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const quote = body[cursor];
|
|
361
|
-
if (quote !== '"' && quote !== "'") {
|
|
362
|
-
return null;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
cursor += 1;
|
|
366
|
-
let token = "";
|
|
367
|
-
let escaped = false;
|
|
368
|
-
|
|
369
|
-
while (cursor < body.length) {
|
|
370
|
-
const char = body[cursor] ?? "";
|
|
371
|
-
|
|
372
|
-
if (char === quote && !escaped) {
|
|
373
|
-
cursor += 1;
|
|
374
|
-
values.push(token);
|
|
375
|
-
break;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
token += char;
|
|
379
|
-
escaped = char === "\\" && !escaped;
|
|
380
|
-
if (char !== "\\") {
|
|
381
|
-
escaped = false;
|
|
382
|
-
}
|
|
383
|
-
cursor += 1;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (cursor > body.length) {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
expectValue = false;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const leftover = body.slice(cursor).trim();
|
|
394
|
-
if (leftover === ",") {
|
|
395
|
-
return values;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return leftover.length === 0 ? values : null;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function collectArrayLines(lines, startIndex) {
|
|
402
|
-
const collected = [stripCommentOutsideStrings(lines[startIndex] ?? "")];
|
|
403
|
-
let bracketDepth = 0;
|
|
404
|
-
let inSingle = false;
|
|
405
|
-
let inDouble = false;
|
|
406
|
-
let escaped = false;
|
|
407
|
-
|
|
408
|
-
for (let lineIndex = startIndex; lineIndex < lines.length; lineIndex += 1) {
|
|
409
|
-
const sanitized = stripCommentOutsideStrings(lines[lineIndex] ?? "");
|
|
410
|
-
if (lineIndex !== startIndex) {
|
|
411
|
-
collected.push(sanitized);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
for (let index = 0; index < sanitized.length; index += 1) {
|
|
415
|
-
const char = sanitized[index] ?? "";
|
|
416
|
-
|
|
417
|
-
if (char === '"' && !inSingle && !escaped) {
|
|
418
|
-
inDouble = !inDouble;
|
|
419
|
-
} else if (char === "'" && !inDouble && !escaped) {
|
|
420
|
-
inSingle = !inSingle;
|
|
421
|
-
} else if (!inSingle && !inDouble) {
|
|
422
|
-
if (char === "[") {
|
|
423
|
-
bracketDepth += 1;
|
|
424
|
-
} else if (char === "]") {
|
|
425
|
-
bracketDepth -= 1;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
escaped = char === "\\" && !escaped;
|
|
430
|
-
if (char !== "\\") {
|
|
431
|
-
escaped = false;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (bracketDepth <= 0) {
|
|
436
|
-
return {
|
|
437
|
-
nextIndex: lineIndex,
|
|
438
|
-
text: collected.join("\n"),
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
return null;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function parseCodexBlock(blockContent) {
|
|
447
|
-
const lines = blockContent.split("\n");
|
|
448
|
-
let command = null;
|
|
449
|
-
let args = null;
|
|
450
|
-
let hasConflict = false;
|
|
451
|
-
let inTripleDouble = false;
|
|
452
|
-
let inTripleSingle = false;
|
|
453
|
-
|
|
454
|
-
for (let index = 1; index < lines.length; index += 1) {
|
|
455
|
-
const line = lines[index] ?? "";
|
|
456
|
-
const sanitized = stripCommentOutsideStrings(line);
|
|
457
|
-
const trimmed = sanitized.trim();
|
|
458
|
-
|
|
459
|
-
if (inTripleDouble) {
|
|
460
|
-
if (countTripleQuoteToggles(sanitized, '"') % 2 === 1) {
|
|
461
|
-
inTripleDouble = false;
|
|
462
|
-
}
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (inTripleSingle) {
|
|
467
|
-
if (countTripleQuoteToggles(sanitized, "'") % 2 === 1) {
|
|
468
|
-
inTripleSingle = false;
|
|
469
|
-
}
|
|
470
|
-
continue;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (countTripleQuoteToggles(sanitized, '"') % 2 === 1) {
|
|
474
|
-
inTripleDouble = true;
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (countTripleQuoteToggles(sanitized, "'") % 2 === 1) {
|
|
479
|
-
inTripleSingle = true;
|
|
480
|
-
continue;
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
if (trimmed.length === 0) {
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (/^command\s*=/.test(trimmed)) {
|
|
488
|
-
const parsedCommand = parseStringAssignment(trimmed, "command");
|
|
489
|
-
if (command !== null || parsedCommand === null) {
|
|
490
|
-
hasConflict = true;
|
|
491
|
-
} else {
|
|
492
|
-
command = parsedCommand;
|
|
493
|
-
}
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (/^args\s*=/.test(trimmed)) {
|
|
498
|
-
const collected = collectArrayLines(lines, index);
|
|
499
|
-
const parsedArgs = collected === null ? null : parseTomlStringArray(collected.text);
|
|
500
|
-
|
|
501
|
-
if (args !== null || parsedArgs === null || collected === null) {
|
|
502
|
-
hasConflict = true;
|
|
503
|
-
} else {
|
|
504
|
-
args = parsedArgs;
|
|
505
|
-
index = collected.nextIndex;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return {
|
|
511
|
-
args,
|
|
512
|
-
command,
|
|
513
|
-
hasConflict,
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function isStructurallySafe(source) {
|
|
518
|
-
let bracketDepth = 0;
|
|
519
|
-
let braceDepth = 0;
|
|
520
|
-
let inSingle = false;
|
|
521
|
-
let inDouble = false;
|
|
522
|
-
let escaped = false;
|
|
523
|
-
let tripleState = {
|
|
524
|
-
inTripleDouble: false,
|
|
525
|
-
inTripleSingle: false,
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
for (const line of source.split("\n")) {
|
|
529
|
-
tripleState = advanceTripleQuoteState(line, tripleState);
|
|
530
|
-
|
|
531
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
532
|
-
const char = line[index] ?? "";
|
|
533
|
-
|
|
534
|
-
if (!inSingle && !inDouble && char === "#") {
|
|
535
|
-
break;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (char === '"' && !inSingle && !escaped) {
|
|
539
|
-
inDouble = !inDouble;
|
|
540
|
-
} else if (char === "'" && !inDouble && !escaped) {
|
|
541
|
-
inSingle = !inSingle;
|
|
542
|
-
} else if (!inSingle && !inDouble) {
|
|
543
|
-
if (char === "[") {
|
|
544
|
-
bracketDepth += 1;
|
|
545
|
-
} else if (char === "]") {
|
|
546
|
-
bracketDepth -= 1;
|
|
547
|
-
if (bracketDepth < 0) {
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
} else if (char === "{") {
|
|
551
|
-
braceDepth += 1;
|
|
552
|
-
} else if (char === "}") {
|
|
553
|
-
braceDepth -= 1;
|
|
554
|
-
if (braceDepth < 0) {
|
|
555
|
-
return false;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
escaped = char === "\\" && !escaped;
|
|
561
|
-
if (char !== "\\") {
|
|
562
|
-
escaped = false;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return (
|
|
568
|
-
!tripleState.inTripleDouble &&
|
|
569
|
-
!tripleState.inTripleSingle &&
|
|
570
|
-
bracketDepth === 0 &&
|
|
571
|
-
braceDepth === 0 &&
|
|
572
|
-
!inSingle &&
|
|
573
|
-
!inDouble
|
|
574
|
-
);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
try {
|
|
578
|
-
const source = fs.readFileSync(configPath, "utf8");
|
|
579
|
-
if (!isStructurallySafe(source)) {
|
|
580
|
-
process.exit(1);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const blocks = findAllCodexBlocks(source);
|
|
584
|
-
if (blocks.length !== 1) {
|
|
585
|
-
process.exit(1);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const parsed = parseCodexBlock(blocks[0]);
|
|
589
|
-
const isConfigured =
|
|
590
|
-
!parsed.hasConflict &&
|
|
591
|
-
parsed.command === expectedCommand &&
|
|
592
|
-
Array.isArray(parsed.args) &&
|
|
593
|
-
parsed.args.length === 2 &&
|
|
594
|
-
parsed.args[0] === "-y" &&
|
|
595
|
-
typeof parsed.args[1] === "string" &&
|
|
596
|
-
packageSpecPattern.test(parsed.args[1]);
|
|
597
|
-
|
|
598
|
-
process.exit(isConfigured ? 0 : 1);
|
|
599
|
-
} catch {
|
|
600
|
-
process.exit(1);
|
|
601
|
-
}
|
|
602
|
-
NODE
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
resolve_optional_cli_command() {
|
|
606
|
-
local command_name="$1"
|
|
607
|
-
local npm_prefix=""
|
|
608
|
-
|
|
609
|
-
if command -v "$command_name" >/dev/null 2>&1; then
|
|
610
|
-
command -v "$command_name"
|
|
611
|
-
return 0
|
|
612
|
-
fi
|
|
613
|
-
|
|
614
|
-
npm_prefix=$(npm prefix -g 2>/dev/null || true)
|
|
615
|
-
if [ -n "$npm_prefix" ] && [ -x "$npm_prefix/bin/$command_name" ]; then
|
|
616
|
-
printf "%s\n" "$npm_prefix/bin/$command_name"
|
|
617
|
-
return 0
|
|
618
|
-
fi
|
|
619
|
-
|
|
620
|
-
return 1
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
# ── Header ──
|
|
624
|
-
# shellcheck disable=SC2059
|
|
625
|
-
printf "\n${BOLD}WEPPY Installer${NC}\n"
|
|
626
|
-
# shellcheck disable=SC2059
|
|
627
|
-
printf "${DIM}AI-powered Roblox Studio integration${NC}\n"
|
|
628
|
-
printf "%s\n" "════════════════════════════════════"
|
|
629
|
-
|
|
630
|
-
# ── Node.js check ──
|
|
631
|
-
if ! command -v node &>/dev/null; then
|
|
632
|
-
fail "Node.js is not installed"
|
|
633
|
-
printf " Install Node.js 18+: https://nodejs.org\n"
|
|
634
|
-
exit 1
|
|
635
|
-
fi
|
|
636
|
-
|
|
637
|
-
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
|
638
|
-
if [ "$NODE_VERSION" -lt 18 ]; then
|
|
639
|
-
fail "Node.js 18 or higher required (current: $(node -v))"
|
|
640
|
-
printf " Upgrade: https://nodejs.org\n"
|
|
641
|
-
exit 1
|
|
642
|
-
fi
|
|
643
|
-
success "Node.js $(node -v) detected"
|
|
644
|
-
|
|
645
|
-
# ═══════════════════════════════════
|
|
646
|
-
# [1/2] Setup — Roblox Studio Plugin
|
|
647
|
-
# ═══════════════════════════════════
|
|
648
|
-
step "1/2" "Setup Roblox Studio Plugin"
|
|
649
|
-
|
|
650
|
-
if confirm " Run npx -y @weppy/roblox-mcp@latest --setup?"; then
|
|
651
|
-
setup_tmp_dir=""
|
|
652
|
-
if setup_tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/weppy-setup-XXXXXX" 2>/dev/null); then
|
|
653
|
-
:
|
|
654
|
-
elif setup_tmp_dir=$(mktemp -d 2>/dev/null); then
|
|
655
|
-
:
|
|
656
|
-
fi
|
|
657
|
-
|
|
658
|
-
if [ -n "${setup_tmp_dir:-}" ] && [ -d "$setup_tmp_dir" ]; then
|
|
659
|
-
# stdin을 /dev/null로 격리: curl|bash 파이프 모드에서 stdio MCP 서버가
|
|
660
|
-
# bash의 남은 스크립트 바이트를 소비해버리는 문제를 방지한다
|
|
661
|
-
# The @latest tag forces npx to resolve from the registry instead of
|
|
662
|
-
# reusing an older version pinned in the npm cache.
|
|
663
|
-
if (cd "$setup_tmp_dir" && npx -y "@weppy/roblox-mcp@latest" --setup </dev/null); then
|
|
664
|
-
success "Setup complete"
|
|
665
|
-
else
|
|
666
|
-
warn "Setup encountered a warning (non-blocking)"
|
|
667
|
-
fi
|
|
668
|
-
|
|
669
|
-
rm -rf "$setup_tmp_dir"
|
|
670
|
-
else
|
|
671
|
-
warn "Setup encountered a warning (failed to create temp working directory)"
|
|
672
|
-
fi
|
|
673
|
-
else
|
|
674
|
-
warn "Setup skipped"
|
|
675
|
-
fi
|
|
676
|
-
|
|
677
|
-
# ═══════════════════════════════════
|
|
678
|
-
# [2/2] Register MCP with AI apps
|
|
679
|
-
# ═══════════════════════════════════
|
|
680
|
-
step "2/2" "Register MCP with AI apps"
|
|
681
|
-
printf " Automatic registration: Claude Code, Claude Desktop, Cursor, Codex CLI/App, Gemini CLI, Antigravity\n"
|
|
682
|
-
|
|
683
|
-
MCP_COMMAND='npx -y @weppy/roblox-mcp@latest'
|
|
684
|
-
|
|
685
|
-
# AI app detection
|
|
686
|
-
declare -a DETECTED_NAMES=()
|
|
687
|
-
declare -a DETECTED_TYPES=()
|
|
688
|
-
declare -a NOT_DETECTED=()
|
|
689
|
-
|
|
690
|
-
# Claude Code
|
|
691
|
-
# `claude mcp add` stores entries under ~/.claude.json or in local/user scope,
|
|
692
|
-
# so prefer `claude mcp list` as the source of truth when the CLI is available
|
|
693
|
-
# (the JSON path checks remain as a fallback).
|
|
694
|
-
CLAUDE_PROJECT_MCP_CONFIG="$PWD/.mcp.json"
|
|
695
|
-
CLAUDE_GLOBAL_MCP_CONFIG="$HOME/.claude/mcp.json"
|
|
696
|
-
CLAUDE_CLI_COMMAND="$(resolve_optional_cli_command claude 2>/dev/null || true)"
|
|
697
|
-
|
|
698
|
-
is_claude_cli_configured() {
|
|
699
|
-
[ -n "$CLAUDE_CLI_COMMAND" ] || return 1
|
|
700
|
-
# The entry counts as configured only when its args carry an explicit `@tag`
|
|
701
|
-
# (e.g. `@latest`). Legacy bare entries fall through and get re-registered.
|
|
702
|
-
"$CLAUDE_CLI_COMMAND" mcp list 2>/dev/null \
|
|
703
|
-
| grep "^weppy-roblox-mcp:" \
|
|
704
|
-
| grep -q "@weppy/roblox-mcp@"
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if is_claude_cli_configured \
|
|
708
|
-
|| is_json_mcp_configured "$CLAUDE_PROJECT_MCP_CONFIG" \
|
|
709
|
-
|| is_json_mcp_configured "$CLAUDE_GLOBAL_MCP_CONFIG"; then
|
|
710
|
-
DETECTED_NAMES+=("Claude Code (configured)")
|
|
711
|
-
DETECTED_TYPES+=("claude-code")
|
|
712
|
-
elif [ -n "$CLAUDE_CLI_COMMAND" ]; then
|
|
713
|
-
DETECTED_NAMES+=("Claude Code (CLI)")
|
|
714
|
-
DETECTED_TYPES+=("claude-code")
|
|
715
|
-
else
|
|
716
|
-
NOT_DETECTED+=("Claude Code (not found)")
|
|
717
|
-
fi
|
|
718
|
-
|
|
719
|
-
# Claude Desktop (macOS)
|
|
720
|
-
CLAUDE_DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
|
|
721
|
-
if [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
|
|
722
|
-
DETECTED_NAMES+=("Claude Desktop")
|
|
723
|
-
DETECTED_TYPES+=("claude-desktop")
|
|
724
|
-
elif [ "$(uname)" = "Darwin" ]; then
|
|
725
|
-
NOT_DETECTED+=("Claude Desktop (config not found)")
|
|
726
|
-
fi
|
|
727
|
-
|
|
728
|
-
# Cursor (detect only when mcp.json or binary exists)
|
|
729
|
-
CURSOR_MCP_CONFIG="$HOME/.cursor/mcp.json"
|
|
730
|
-
if [ -f "$CURSOR_MCP_CONFIG" ] || command -v cursor &>/dev/null; then
|
|
731
|
-
DETECTED_NAMES+=("Cursor")
|
|
732
|
-
DETECTED_TYPES+=("cursor")
|
|
733
|
-
else
|
|
734
|
-
NOT_DETECTED+=("Cursor (not found)")
|
|
735
|
-
fi
|
|
736
|
-
|
|
737
|
-
# Codex CLI / Codex App (share the same ~/.codex/config.toml)
|
|
738
|
-
CODEX_CONFIG="$HOME/.codex/config.toml"
|
|
739
|
-
CODEX_CLI_COMMAND="$(resolve_optional_cli_command codex 2>/dev/null || true)"
|
|
740
|
-
if is_codex_config_configured "$CODEX_CONFIG"; then
|
|
741
|
-
DETECTED_NAMES+=("Codex CLI/App (configured)")
|
|
742
|
-
DETECTED_TYPES+=("codex-cli")
|
|
743
|
-
elif [ -n "$CODEX_CLI_COMMAND" ]; then
|
|
744
|
-
DETECTED_NAMES+=("Codex CLI/App")
|
|
745
|
-
DETECTED_TYPES+=("codex-cli")
|
|
746
|
-
else
|
|
747
|
-
NOT_DETECTED+=("Codex CLI/App (not found)")
|
|
748
|
-
fi
|
|
749
|
-
|
|
750
|
-
# Gemini CLI
|
|
751
|
-
# Gemini CLI
|
|
752
|
-
GEMINI_CONFIG="$HOME/.gemini/settings.json"
|
|
753
|
-
GEMINI_CLI_COMMAND="$(resolve_optional_cli_command gemini 2>/dev/null || true)"
|
|
754
|
-
if is_json_mcp_configured "$GEMINI_CONFIG"; then
|
|
755
|
-
DETECTED_NAMES+=("Gemini CLI (configured)")
|
|
756
|
-
DETECTED_TYPES+=("gemini-cli")
|
|
757
|
-
elif [ -n "$GEMINI_CLI_COMMAND" ]; then
|
|
758
|
-
DETECTED_NAMES+=("Gemini CLI")
|
|
759
|
-
DETECTED_TYPES+=("gemini-cli")
|
|
760
|
-
else
|
|
761
|
-
NOT_DETECTED+=("Gemini CLI (not found)")
|
|
762
|
-
fi
|
|
763
|
-
|
|
764
|
-
# Antigravity (unofficial path, auto-register if found)
|
|
765
|
-
ANTIGRAVITY_CONFIG="$HOME/.gemini/antigravity/mcp_config.json"
|
|
766
|
-
if is_antigravity_mcp_configured "$ANTIGRAVITY_CONFIG"; then
|
|
767
|
-
DETECTED_NAMES+=("Antigravity (configured)")
|
|
768
|
-
DETECTED_TYPES+=("antigravity")
|
|
769
|
-
elif [ -f "$ANTIGRAVITY_CONFIG" ]; then
|
|
770
|
-
DETECTED_NAMES+=("Antigravity")
|
|
771
|
-
DETECTED_TYPES+=("antigravity")
|
|
772
|
-
elif [ -d "$HOME/.gemini/antigravity" ]; then
|
|
773
|
-
DETECTED_NAMES+=("Antigravity")
|
|
774
|
-
DETECTED_TYPES+=("antigravity")
|
|
775
|
-
else
|
|
776
|
-
NOT_DETECTED+=("Antigravity (not found)")
|
|
777
|
-
fi
|
|
778
|
-
|
|
779
|
-
if [ ${#DETECTED_NAMES[@]} -eq 0 ]; then
|
|
780
|
-
warn "No AI apps detected"
|
|
781
|
-
info "Register MCP server manually: $MCP_COMMAND"
|
|
782
|
-
else
|
|
783
|
-
# shellcheck disable=SC2059
|
|
784
|
-
printf "\n ${BOLD}Detected:${NC}\n"
|
|
785
|
-
for i in "${!DETECTED_NAMES[@]}"; do
|
|
786
|
-
# shellcheck disable=SC2059
|
|
787
|
-
printf " ${GREEN}%d.${NC} %s\n" "$((i + 1))" "${DETECTED_NAMES[$i]}"
|
|
788
|
-
done
|
|
789
|
-
|
|
790
|
-
if [ ${#NOT_DETECTED[@]} -gt 0 ]; then
|
|
791
|
-
# shellcheck disable=SC2059
|
|
792
|
-
printf "\n ${DIM}Not detected:${NC}\n"
|
|
793
|
-
for item in "${NOT_DETECTED[@]}"; do
|
|
794
|
-
# shellcheck disable=SC2059
|
|
795
|
-
printf " ${DIM}- %s${NC}\n" "$item"
|
|
796
|
-
done
|
|
797
|
-
fi
|
|
798
|
-
|
|
799
|
-
# shellcheck disable=SC2059
|
|
800
|
-
printf "\n Select apps to register ${DIM}(comma-separated, 'a' for all, 'n' to skip)${NC}\n"
|
|
801
|
-
printf " → "
|
|
802
|
-
if [ "${CI:-}" = "true" ]; then
|
|
803
|
-
selection="a"
|
|
804
|
-
printf "a\n"
|
|
805
|
-
else
|
|
806
|
-
read -r selection </dev/tty
|
|
807
|
-
fi
|
|
808
|
-
selection="${selection:-n}"
|
|
809
|
-
|
|
810
|
-
# Parse selection
|
|
811
|
-
declare -a SELECTED_INDICES=()
|
|
812
|
-
case "$selection" in
|
|
813
|
-
[Nn])
|
|
814
|
-
warn "MCP registration skipped"
|
|
815
|
-
;;
|
|
816
|
-
[Aa])
|
|
817
|
-
for i in "${!DETECTED_NAMES[@]}"; do
|
|
818
|
-
SELECTED_INDICES+=("$i")
|
|
819
|
-
done
|
|
820
|
-
;;
|
|
821
|
-
*)
|
|
822
|
-
IFS=',' read -ra PARTS <<< "$selection"
|
|
823
|
-
for part in "${PARTS[@]}"; do
|
|
824
|
-
part=$(echo "$part" | tr -d ' ')
|
|
825
|
-
if [[ "$part" =~ ^[0-9]+$ ]]; then
|
|
826
|
-
idx=$((part - 1))
|
|
827
|
-
if [ "$idx" -ge 0 ] && [ "$idx" -lt ${#DETECTED_NAMES[@]} ]; then
|
|
828
|
-
SELECTED_INDICES+=("$idx")
|
|
829
|
-
fi
|
|
830
|
-
fi
|
|
831
|
-
done
|
|
832
|
-
;;
|
|
833
|
-
esac
|
|
834
|
-
|
|
835
|
-
# Register selected apps
|
|
836
|
-
for idx in "${SELECTED_INDICES[@]}"; do
|
|
837
|
-
app_type="${DETECTED_TYPES[$idx]}"
|
|
838
|
-
app_name="${DETECTED_NAMES[$idx]}"
|
|
839
|
-
|
|
840
|
-
case "$app_type" in
|
|
841
|
-
claude-code)
|
|
842
|
-
if is_claude_cli_configured \
|
|
843
|
-
|| is_json_mcp_configured "$CLAUDE_PROJECT_MCP_CONFIG" \
|
|
844
|
-
|| is_json_mcp_configured "$CLAUDE_GLOBAL_MCP_CONFIG"; then
|
|
845
|
-
success "Already configured: $app_name"
|
|
846
|
-
elif [ -n "$CLAUDE_CLI_COMMAND" ]; then
|
|
847
|
-
claude_stderr_file=$(mktemp "${TMPDIR:-/tmp}/weppy-claude-XXXXXX.err" 2>/dev/null || echo "${HOME}/weppy-claude.err")
|
|
848
|
-
# Best-effort remove any legacy bare entry so the subsequent add can
|
|
849
|
-
# install the canonical `@latest` form. Ignore errors when nothing
|
|
850
|
-
# exists.
|
|
851
|
-
"$CLAUDE_CLI_COMMAND" mcp remove weppy-roblox-mcp >/dev/null 2>&1 || true
|
|
852
|
-
# Capture the CLI exit code immediately so it isn't overwritten by the
|
|
853
|
-
# subsequent grep check (which would otherwise leak its own exit code).
|
|
854
|
-
claude_exit_code=0
|
|
855
|
-
"$CLAUDE_CLI_COMMAND" mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp@latest" 2>"$claude_stderr_file" || claude_exit_code=$?
|
|
856
|
-
if [ "$claude_exit_code" -eq 0 ]; then
|
|
857
|
-
success "Registered: $app_name"
|
|
858
|
-
elif grep -qi "already exists" "$claude_stderr_file"; then
|
|
859
|
-
# Already registered in another scope — not a failure
|
|
860
|
-
success "Already configured: $app_name"
|
|
861
|
-
else
|
|
862
|
-
fail "Failed: $app_name (exit=$claude_exit_code)"
|
|
863
|
-
printf " CLI: %s\n" "$CLAUDE_CLI_COMMAND"
|
|
864
|
-
printf " stderr:\n"
|
|
865
|
-
sed 's/^/ /' "$claude_stderr_file" || true
|
|
866
|
-
fi
|
|
867
|
-
rm -f "$claude_stderr_file"
|
|
868
|
-
else
|
|
869
|
-
fail "Failed: $app_name (claude CLI not found)"
|
|
870
|
-
fi
|
|
871
|
-
;;
|
|
872
|
-
claude-desktop)
|
|
873
|
-
if add_mcp_to_config "$CLAUDE_DESKTOP_CONFIG"; then
|
|
874
|
-
success "Registered: $app_name"
|
|
875
|
-
else
|
|
876
|
-
fail "Failed: $app_name"
|
|
877
|
-
fi
|
|
878
|
-
;;
|
|
879
|
-
cursor)
|
|
880
|
-
if add_mcp_to_config "$HOME/.cursor/mcp.json"; then
|
|
881
|
-
success "Registered: $app_name"
|
|
882
|
-
else
|
|
883
|
-
fail "Failed: $app_name"
|
|
884
|
-
fi
|
|
885
|
-
;;
|
|
886
|
-
codex-cli)
|
|
887
|
-
if is_codex_config_configured "$CODEX_CONFIG"; then
|
|
888
|
-
success "Already configured: $app_name"
|
|
889
|
-
else
|
|
890
|
-
[ -n "$CODEX_CLI_COMMAND" ] && "$CODEX_CLI_COMMAND" mcp remove weppy-roblox-mcp >/dev/null 2>&1 || true
|
|
891
|
-
fi
|
|
892
|
-
if is_codex_config_configured "$CODEX_CONFIG"; then
|
|
893
|
-
:
|
|
894
|
-
elif [ -n "$CODEX_CLI_COMMAND" ] && "$CODEX_CLI_COMMAND" mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp@latest" 2>/dev/null; then
|
|
895
|
-
success "Registered: $app_name"
|
|
896
|
-
elif is_codex_config_configured "$CODEX_CONFIG"; then
|
|
897
|
-
success "Already configured: $app_name"
|
|
898
|
-
else
|
|
899
|
-
fail "Failed: $app_name"
|
|
900
|
-
fi
|
|
901
|
-
;;
|
|
902
|
-
gemini-cli)
|
|
903
|
-
if is_json_mcp_configured "$GEMINI_CONFIG"; then
|
|
904
|
-
success "Already configured: $app_name"
|
|
905
|
-
elif add_mcp_to_config "$GEMINI_CONFIG"; then
|
|
906
|
-
success "Registered: $app_name"
|
|
907
|
-
else
|
|
908
|
-
fail "Failed: $app_name"
|
|
909
|
-
fi
|
|
910
|
-
;;
|
|
911
|
-
antigravity)
|
|
912
|
-
if is_antigravity_mcp_configured "$ANTIGRAVITY_CONFIG"; then
|
|
913
|
-
success "Already configured: $app_name"
|
|
914
|
-
elif add_antigravity_mcp_config "$ANTIGRAVITY_CONFIG"; then
|
|
915
|
-
success "Registered: $app_name"
|
|
916
|
-
else
|
|
917
|
-
fail "Failed: $app_name"
|
|
918
|
-
fi
|
|
919
|
-
;;
|
|
920
|
-
esac
|
|
921
|
-
done
|
|
922
|
-
fi
|
|
923
|
-
|
|
924
|
-
# ═══════════════════════════════════
|
|
925
|
-
# Installation summary
|
|
926
|
-
# ═══════════════════════════════════
|
|
927
|
-
# shellcheck disable=SC2059
|
|
928
|
-
printf "\n%s\n" "════════════════════════════════════"
|
|
929
|
-
# shellcheck disable=SC2059
|
|
930
|
-
printf "${BOLD}Installation complete!${NC}\n\n"
|
|
931
|
-
# shellcheck disable=SC2059
|
|
932
|
-
printf " ${BOLD}Next steps:${NC}\n"
|
|
933
|
-
printf " 1. Restart Roblox Studio\n"
|
|
934
|
-
# shellcheck disable=SC2059
|
|
935
|
-
printf " 2. Look for the ${BOLD}WEPPY${NC} button in the Plugins tab\n"
|
|
936
|
-
printf " 3. Click Connect and start building with AI!\n\n"
|
|
937
|
-
printf " Auto registration: Claude Code, Claude Desktop, Cursor, Codex CLI/App, Gemini CLI, Antigravity\n\n"
|
|
938
|
-
# shellcheck disable=SC2059
|
|
939
|
-
printf " ${DIM}Docs: https://weppyai.com/en/install${NC}\n\n"
|