@modootoday/datalab-extension-mcp 1.4.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.
@@ -0,0 +1,1622 @@
1
+ import "./chunk-MLKGABMK.js";
2
+
3
+ // ../mcp-installer/dist/index.js
4
+ import { spawn as nodeSpawn } from "child_process";
5
+ import { promises as fs } from "fs";
6
+ import { homedir } from "os";
7
+ import { createInterface } from "readline";
8
+ var ART = [
9
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557",
10
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
11
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
12
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
13
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
14
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D",
15
+ "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \uB370\uC774\uD130\uB7A9\uD234\uC988 \xB7 datalab.tools"
16
+ ];
17
+ function printBanner(out, subtitle) {
18
+ out("");
19
+ for (const row of ART) {
20
+ out(row);
21
+ }
22
+ out("");
23
+ out(` [\uB370\uC774\uD130\uB7A9\uD234\uC988] ${subtitle}`);
24
+ out("");
25
+ }
26
+ var INSTALL_SUBTITLE = "\uCEE4\uB125\uD130 \uC124\uCE58 \uB3C4\uC6B0\uBBF8";
27
+ var UNINSTALL_SUBTITLE = "\uCEE4\uB125\uD130 \uC815\uB9AC \uB3C4\uC6B0\uBBF8";
28
+ var SERVER_NAME = "datalab";
29
+ var SERVER_PACKAGE = "@modootoday/datalab-extension-mcp";
30
+ var DEFAULT_PORT = "8765";
31
+ var DEFAULT_EXTENSION_ID = "ldoknfkedngbdfgdkeicojmhnojgpdcb";
32
+ function packageSpec(version) {
33
+ return `${SERVER_PACKAGE}@${version}`;
34
+ }
35
+ function isCustomPort(port) {
36
+ if (port === void 0 || port === "") {
37
+ return false;
38
+ }
39
+ return port !== DEFAULT_PORT;
40
+ }
41
+ function buildEnv(opts) {
42
+ const env = {
43
+ DATALAB_MCP_TOKEN: opts.token,
44
+ DATALAB_MCP_EXTENSION_ID: opts.extensionId
45
+ };
46
+ if (isCustomPort(opts.port)) {
47
+ env["DATALAB_MCP_PORT"] = opts.port;
48
+ }
49
+ return env;
50
+ }
51
+ function daemonMcpUrl(opts) {
52
+ const port = isCustomPort(opts.port) ? opts.port : DEFAULT_PORT;
53
+ return `http://127.0.0.1:${port}/mcp`;
54
+ }
55
+ function buildUrlEntry(opts) {
56
+ return { url: daemonMcpUrl(opts) };
57
+ }
58
+ function buildFileEntry(opts, platform) {
59
+ const spec = packageSpec(opts.version);
60
+ if (platform === "win32") {
61
+ return {
62
+ command: "cmd",
63
+ args: ["/c", "npx", "-y", spec],
64
+ env: buildEnv(opts)
65
+ };
66
+ }
67
+ return {
68
+ command: "npx",
69
+ args: ["-y", spec],
70
+ env: buildEnv(opts)
71
+ };
72
+ }
73
+ var CODEX_CHATGPT_NOTE = "ChatGPT \uB370\uC2A4\uD06C\uD1B1\uACFC \uD568\uAED8 \uC5F0\uACB0\uB3FC\uC694.";
74
+ function envPairs(opts) {
75
+ const pairs = [
76
+ `DATALAB_MCP_TOKEN=${opts.token}`,
77
+ `DATALAB_MCP_EXTENSION_ID=${opts.extensionId}`
78
+ ];
79
+ if (isCustomPort(opts.port)) {
80
+ pairs.push(`DATALAB_MCP_PORT=${opts.port}`);
81
+ }
82
+ return pairs;
83
+ }
84
+ var CLI_HOSTS = [
85
+ {
86
+ id: "claude",
87
+ tier: 1,
88
+ displayName: "Claude Code",
89
+ bin: "claude",
90
+ // --scope user is mandatory: the default "local" scope binds the server to
91
+ // whatever directory the user happened to paste the command in, which
92
+ // looks like a success and then silently does nothing everywhere else.
93
+ buildAddArgs(opts) {
94
+ const args = ["mcp", "add", SERVER_NAME, "--scope", "user"];
95
+ for (const pair of envPairs(opts)) {
96
+ args.push("--env", pair);
97
+ }
98
+ args.push("--", "npx", "-y", packageSpec(opts.version));
99
+ return args;
100
+ },
101
+ buildRemoveArgs() {
102
+ return ["mcp", "remove", SERVER_NAME, "--scope", "user"];
103
+ }
104
+ },
105
+ {
106
+ id: "codex",
107
+ tier: 1,
108
+ displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex",
109
+ bin: "codex",
110
+ // The Codex CLI and the ChatGPT desktop app share one config, so a single
111
+ // registration covers both — worth saying out loud in the output.
112
+ note: CODEX_CHATGPT_NOTE,
113
+ buildAddArgs(opts) {
114
+ const args = ["mcp", "add", SERVER_NAME];
115
+ for (const pair of envPairs(opts)) {
116
+ args.push("--env", pair);
117
+ }
118
+ args.push("--", "npx", "-y", packageSpec(opts.version));
119
+ return args;
120
+ },
121
+ buildRemoveArgs() {
122
+ return ["mcp", "remove", SERVER_NAME];
123
+ }
124
+ },
125
+ {
126
+ id: "gemini",
127
+ tier: 1,
128
+ displayName: "Gemini CLI",
129
+ bin: "gemini",
130
+ // -s user for the same cwd-binding reason as Claude Code's --scope user.
131
+ buildAddArgs(opts) {
132
+ const args = ["mcp", "add", "-s", "user"];
133
+ for (const pair of envPairs(opts)) {
134
+ args.push("-e", pair);
135
+ }
136
+ args.push(SERVER_NAME, "npx", "-y", packageSpec(opts.version));
137
+ return args;
138
+ },
139
+ buildRemoveArgs() {
140
+ return ["mcp", "remove", "-s", "user", SERVER_NAME];
141
+ }
142
+ }
143
+ ];
144
+ var INSTALLABLE_CLIS = [
145
+ {
146
+ id: "claude",
147
+ displayName: "Claude Code",
148
+ npmPackage: "@anthropic-ai/claude-code"
149
+ },
150
+ { id: "gemini", displayName: "Gemini CLI", npmPackage: "@google/gemini-cli" },
151
+ {
152
+ id: "codex",
153
+ displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex",
154
+ npmPackage: "@openai/codex"
155
+ }
156
+ ];
157
+ function joinPath(io, ...parts) {
158
+ if (io.platform === "win32") {
159
+ return parts.join("\\");
160
+ }
161
+ return parts.join("/");
162
+ }
163
+ function pathRoot(io, kind) {
164
+ if (kind === "home") {
165
+ return io.homedir();
166
+ }
167
+ if (kind === "homeConfig") {
168
+ return joinPath(io, io.homedir(), ".config");
169
+ }
170
+ if (io.platform === "win32") {
171
+ const appData = io.env["APPDATA"];
172
+ return appData === void 0 || appData === "" ? null : appData;
173
+ }
174
+ if (kind === "appData" && io.platform === "darwin") {
175
+ return joinPath(io, io.homedir(), "Library", "Application Support");
176
+ }
177
+ const xdg = io.env["XDG_CONFIG_HOME"];
178
+ if (kind === "xdg" && io.platform === "linux" && xdg !== void 0 && xdg !== "") {
179
+ return xdg;
180
+ }
181
+ return joinPath(io, io.homedir(), ".config");
182
+ }
183
+ function resolveHostPath(io, spec) {
184
+ if (spec.platforms !== void 0 && !spec.platforms.includes(io.platform)) {
185
+ return null;
186
+ }
187
+ const root = pathRoot(io, spec.kind);
188
+ if (root === null) {
189
+ return null;
190
+ }
191
+ const segments = io.platform === "win32" && spec.win32Segments !== void 0 ? spec.win32Segments : spec.segments;
192
+ return joinPath(io, root, ...segments);
193
+ }
194
+ async function msixRoamingDir(io, familyPrefix, appDir) {
195
+ if (io.platform !== "win32") {
196
+ return null;
197
+ }
198
+ const localAppData = io.env["LOCALAPPDATA"];
199
+ if (localAppData === void 0 || localAppData === "") {
200
+ return null;
201
+ }
202
+ const packages = joinPath(io, localAppData, "Packages");
203
+ let names;
204
+ try {
205
+ names = await io.listDir(packages);
206
+ } catch {
207
+ return null;
208
+ }
209
+ for (const name of names) {
210
+ if (!name.startsWith(familyPrefix)) {
211
+ continue;
212
+ }
213
+ const dir = joinPath(io, packages, name, "LocalCache", "Roaming", appDir);
214
+ if (await io.exists(dir)) {
215
+ return dir;
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+ var FILE_HOSTS = [
221
+ {
222
+ id: "claude-desktop",
223
+ tier: 2,
224
+ displayName: "Claude Desktop",
225
+ configKey: "mcpServers",
226
+ entryKind: "stdio",
227
+ // 🔴 리눅스는 공식 경로가 없어 Tier 3 가 맡는다 — 여기서 추측하면 false-success.
228
+ configPath(io) {
229
+ return resolveHostPath(io, {
230
+ kind: "appData",
231
+ segments: ["Claude", "claude_desktop_config.json"],
232
+ platforms: ["darwin", "win32"]
233
+ });
234
+ },
235
+ /**
236
+ * 🔴 Store(MSIX) 설치판이면 `%APPDATA%\Claude` 가 아니라 패키지 컨테이너 안을
237
+ * 쓴다. 두 설치 경로(exe / Store)가 완전히 다른 위치를 쓰는데 앱은 어느 쪽인지
238
+ * 말해 주지 않는다 — 컨테이너가 실제로 있으면 그쪽이 앱이 읽는 곳이므로 그쪽을
239
+ * 택한다. 없으면 null 을 돌려 기존 `%APPDATA%` 경로로 되돌아간다.
240
+ */
241
+ async resolveConfigPath(io) {
242
+ const dir = await msixRoamingDir(io, "Claude_", "Claude");
243
+ if (dir === null) {
244
+ return null;
245
+ }
246
+ return joinPath(io, dir, "claude_desktop_config.json");
247
+ }
248
+ },
249
+ {
250
+ id: "cursor",
251
+ tier: 2,
252
+ displayName: "Cursor",
253
+ configKey: "mcpServers",
254
+ // Cursor speaks local HTTP MCP, so it connects to the daemon directly
255
+ // (url entry) — no adapter process spawned for it.
256
+ entryKind: "url",
257
+ configPath(io) {
258
+ return resolveHostPath(io, {
259
+ kind: "home",
260
+ segments: [".cursor", "mcp.json"]
261
+ });
262
+ }
263
+ },
264
+ {
265
+ // Windsurf's config is documented strict JSON with an `mcpServers` map, so
266
+ // it graduates from a printed snippet to a safe single-key merge. The
267
+ // hygiene floor still refuses (and falls back to a snippet) if a given
268
+ // install turns out to carry comments, so the promotion cannot corrupt.
269
+ id: "windsurf",
270
+ tier: 2,
271
+ displayName: "Windsurf",
272
+ configKey: "mcpServers",
273
+ entryKind: "stdio",
274
+ configPath(io) {
275
+ return joinPath(
276
+ io,
277
+ io.homedir(),
278
+ ".codeium",
279
+ "windsurf",
280
+ "mcp_config.json"
281
+ );
282
+ }
283
+ },
284
+ {
285
+ // GitHub Copilot CLI — 표준 `mcpServers` + {command,args,env}(공식 문서).
286
+ // 🔴 경로는 `~/.copilot/mcp-config.json` 이지만 `COPILOT_HOME` 이 설정돼 있으면
287
+ // 그 아래다. override 를 무시하면 사용자가 옮겨 둔 설정 대신 엉뚱한 곳에 써서
288
+ // "성공했다는데 안 보인다" 가 된다.
289
+ id: "copilot-cli",
290
+ tier: 2,
291
+ displayName: "GitHub Copilot CLI",
292
+ configKey: "mcpServers",
293
+ entryKind: "stdio",
294
+ configPath(io) {
295
+ const home = io.env["COPILOT_HOME"];
296
+ if (home !== void 0 && home !== "") {
297
+ return joinPath(io, home, "mcp-config.json");
298
+ }
299
+ return resolveHostPath(io, {
300
+ kind: "home",
301
+ segments: [".copilot", "mcp-config.json"]
302
+ });
303
+ }
304
+ },
305
+ {
306
+ // JetBrains Junie — documented strict-JSON user config, home-relative on
307
+ // every OS.
308
+ id: "junie",
309
+ tier: 2,
310
+ displayName: "JetBrains Junie",
311
+ configKey: "mcpServers",
312
+ entryKind: "stdio",
313
+ configPath(io) {
314
+ return resolveHostPath(io, {
315
+ kind: "home",
316
+ segments: [".junie", "mcp", "mcp.json"]
317
+ });
318
+ }
319
+ },
320
+ {
321
+ // Kiro — documented strict-JSON user settings, home-relative on every OS.
322
+ id: "kiro",
323
+ tier: 2,
324
+ displayName: "Kiro",
325
+ configKey: "mcpServers",
326
+ entryKind: "stdio",
327
+ configPath(io) {
328
+ return resolveHostPath(io, {
329
+ kind: "home",
330
+ segments: [".kiro", "settings", "mcp.json"]
331
+ });
332
+ }
333
+ }
334
+ ];
335
+ function buildEntryForHost(host, opts, platform) {
336
+ if (host.entryKind === "url") {
337
+ return buildUrlEntry(opts);
338
+ }
339
+ return buildFileEntry(opts, platform);
340
+ }
341
+ function jsonSnippet(topKey, opts, platform) {
342
+ const entry = buildFileEntry(opts, platform);
343
+ return JSON.stringify({ [topKey]: { [SERVER_NAME]: entry } }, null, 2);
344
+ }
345
+ function vscodeUserDir(io) {
346
+ return resolveHostPath(io, { kind: "appData", segments: ["Code", "User"] });
347
+ }
348
+ function vscodeConfigPath(io) {
349
+ const dir = vscodeUserDir(io);
350
+ if (dir === null) {
351
+ return null;
352
+ }
353
+ return joinPath(io, dir, "mcp.json");
354
+ }
355
+ async function fileOrParentExists(io, path) {
356
+ if (path === null) {
357
+ return false;
358
+ }
359
+ if (await io.exists(path)) {
360
+ return true;
361
+ }
362
+ const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
363
+ if (cut <= 0) {
364
+ return false;
365
+ }
366
+ return io.exists(path.slice(0, cut));
367
+ }
368
+ function tomlSnippet(opts) {
369
+ const lines = [
370
+ `[mcp_servers.${SERVER_NAME}]`,
371
+ `command = "npx"`,
372
+ `args = ["-y", "${packageSpec(opts.version)}"]`,
373
+ "",
374
+ `[mcp_servers.${SERVER_NAME}.env]`
375
+ ];
376
+ for (const [key, value] of Object.entries(buildEnv(opts))) {
377
+ lines.push(`${key} = "${value}"`);
378
+ }
379
+ return lines.join("\n");
380
+ }
381
+ function zedConfigPath(io) {
382
+ return resolveHostPath(io, {
383
+ kind: "xdg",
384
+ segments: ["zed", "settings.json"],
385
+ // Zed 문서: Windows 만 `%APPDATA%\Zed` (대문자), 그 외는 `~/.config/zed`.
386
+ win32Segments: ["Zed", "settings.json"]
387
+ });
388
+ }
389
+ var LMSTUDIO_PATH = {
390
+ kind: "home",
391
+ segments: [".lmstudio", "mcp.json"]
392
+ };
393
+ var WARP_PATH = {
394
+ kind: "home",
395
+ segments: [".warp", ".mcp.json"]
396
+ };
397
+ var OPENCODE_PATH = {
398
+ kind: "homeConfig",
399
+ segments: ["opencode", "opencode.json"]
400
+ };
401
+ var CLINE_PATH = {
402
+ kind: "home",
403
+ segments: [".cline", "mcp.json"]
404
+ };
405
+ var CODEX_PATH = {
406
+ kind: "home",
407
+ segments: [".codex", "config.toml"]
408
+ };
409
+ function codexConfigPath(io) {
410
+ const home = io.env["CODEX_HOME"];
411
+ if (home !== void 0 && home !== "") {
412
+ return joinPath(io, home, "config.toml");
413
+ }
414
+ return resolveHostPath(io, CODEX_PATH);
415
+ }
416
+ function opencodeConfigPath(io) {
417
+ const dir = io.env["OPENCODE_CONFIG_DIR"];
418
+ if (dir !== void 0 && dir !== "") {
419
+ return joinPath(io, dir, "opencode.json");
420
+ }
421
+ return resolveHostPath(io, OPENCODE_PATH);
422
+ }
423
+ function opencodeSnippet(opts) {
424
+ return JSON.stringify(
425
+ {
426
+ mcp: {
427
+ [SERVER_NAME]: {
428
+ type: "local",
429
+ command: ["npx", "-y", packageSpec(opts.version)],
430
+ enabled: true,
431
+ environment: buildEnv(opts)
432
+ }
433
+ }
434
+ },
435
+ null,
436
+ 2
437
+ );
438
+ }
439
+ async function msixPackageInstalled(io, familyPrefix) {
440
+ if (io.platform !== "win32") {
441
+ return false;
442
+ }
443
+ const localAppData = io.env["LOCALAPPDATA"];
444
+ if (localAppData === void 0 || localAppData === "") {
445
+ return false;
446
+ }
447
+ try {
448
+ const names = await io.listDir(joinPath(io, localAppData, "Packages"));
449
+ return names.some((name) => name.startsWith(familyPrefix));
450
+ } catch {
451
+ return false;
452
+ }
453
+ }
454
+ var CHATGPT_DESKTOP_PACKAGE = "OpenAI.Codex_";
455
+ var SNIPPET_HOSTS = [
456
+ {
457
+ id: "vscode",
458
+ tier: 3,
459
+ displayName: "VS Code",
460
+ async detect(io) {
461
+ const path = vscodeConfigPath(io);
462
+ if (path === null) {
463
+ return false;
464
+ }
465
+ return fileOrParentExists(io, path);
466
+ },
467
+ detectedPath(io) {
468
+ return vscodeConfigPath(io);
469
+ },
470
+ reason: "\uC8FC\uC11D\uC774 \uC788\uB294 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
471
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "servers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
472
+ // 🔴 프로필을 함께 말한다. 기본 프로필이 아니면 mcp.json 이 User/profiles/<id>/
473
+ // 아래에 따로 있어서, 위에 적힌 경로에 붙여넣어도 그 프로필에는 반영되지 않는다.
474
+ createHint: "\uD30C\uC77C\uC774 \uC5C6\uC73C\uBA74 VS Code \uC5D0\uC11C \uBA85\uB839 \uD314\uB808\uD2B8(Ctrl+Shift+P) \u2192 'MCP: Open User Configuration' \uC744 \uC2E4\uD589\uD558\uBA74 \uB9CC\uB4E4\uC5B4\uC838\uC694. \uAE30\uBCF8 \uD504\uB85C\uD544\uC774 \uC544\uB2C8\uBA74 \uADF8 \uD504\uB85C\uD544\uC758 mcp.json \uC774 User/profiles \uC544\uB798\uC5D0 \uB530\uB85C \uC788\uC73C\uB2C8, \uADF8 \uBA85\uB839\uC73C\uB85C \uC5F4\uB9B0 \uD30C\uC77C\uC5D0 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
475
+ bootstrapWhenAbsent: true,
476
+ buildSnippet(opts, platform) {
477
+ return jsonSnippet("servers", opts, platform);
478
+ }
479
+ },
480
+ {
481
+ id: "opencode",
482
+ tier: 3,
483
+ displayName: "OpenCode",
484
+ async detect(io) {
485
+ return fileOrParentExists(io, opencodeConfigPath(io));
486
+ },
487
+ detectedPath(io) {
488
+ return opencodeConfigPath(io);
489
+ },
490
+ reason: "\uB3C4\uAD6C \uC804\uCCB4 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
491
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcp" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
492
+ // 🔴 만들지 않는다. 이 파일은 OpenCode **전체 설정**이고, 같은 폴더에
493
+ // `opencode.jsonc` 를 쓰고 있을 수 있다 — 그때 우리가 `opencode.json` 을 만들면
494
+ // 설정 파일이 둘이 되고 어느 쪽이 이기는지 사용자가 알 수 없다.
495
+ createHint: "\uC124\uC815 \uD30C\uC77C\uC740 opencode.json \uB610\uB294 opencode.jsonc \uC608\uC694. \uC4F0\uACE0 \uC788\uB294 \uCABD\uC5D0 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
496
+ buildSnippet(opts) {
497
+ return opencodeSnippet(opts);
498
+ }
499
+ },
500
+ {
501
+ id: "zed",
502
+ tier: 3,
503
+ displayName: "Zed",
504
+ async detect(io) {
505
+ const path = zedConfigPath(io);
506
+ if (path === null) {
507
+ return false;
508
+ }
509
+ return fileOrParentExists(io, path);
510
+ },
511
+ detectedPath(io) {
512
+ return zedConfigPath(io);
513
+ },
514
+ reason: "\uC8FC\uC11D\uC774 \uC788\uB294 \uC124\uC815 \uD30C\uC77C\uC774\uB77C \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
515
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "context_servers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
516
+ buildSnippet(opts, platform) {
517
+ return jsonSnippet("context_servers", opts, platform);
518
+ }
519
+ },
520
+ {
521
+ // Cline — 🔴 벤더 문서가 명시하는 경로는 `~/.cline/mcp.json` 하나다(키 mcpServers).
522
+ // 예전에는 VS Code 확장의 globalStorage 경로를 썼는데 그건 서드파티 표에만 있었고
523
+ // 벤더 문서는 "확장이 쓰는 설정 파일" 이라고만 하고 경로를 주지 않는다. 확인되지
524
+ // 않은 경로를 안내하면 붙여넣어도 아무 일이 없고, 사용자는 우리 안내를 의심한다.
525
+ id: "cline",
526
+ tier: 3,
527
+ displayName: "Cline",
528
+ async detect(io) {
529
+ return fileOrParentExists(io, resolveHostPath(io, CLINE_PATH));
530
+ },
531
+ detectedPath(io) {
532
+ return resolveHostPath(io, CLINE_PATH);
533
+ },
534
+ reason: "\uC124\uC815 \uD30C\uC77C\uC744 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
535
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
536
+ buildSnippet(opts, platform) {
537
+ return jsonSnippet("mcpServers", opts, platform);
538
+ }
539
+ },
540
+ {
541
+ // LM Studio — same `mcpServers` shape ("Cursor notation"). The exact path
542
+ // is only medium-confidence, so it stays a snippet: a wrong detect just
543
+ // means no card (safe), where a wrong auto-write would be a false success.
544
+ id: "lmstudio",
545
+ tier: 3,
546
+ displayName: "LM Studio",
547
+ async detect(io) {
548
+ return fileOrParentExists(io, resolveHostPath(io, LMSTUDIO_PATH));
549
+ },
550
+ detectedPath(io) {
551
+ return resolveHostPath(io, LMSTUDIO_PATH);
552
+ },
553
+ reason: "\uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uACF5\uC2DD\uC801\uC73C\uB85C \uD655\uC815\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
554
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
555
+ buildSnippet(opts, platform) {
556
+ return jsonSnippet("mcpServers", opts, platform);
557
+ }
558
+ },
559
+ {
560
+ // Warp terminal — same `mcpServers` shape at `~/.warp/.mcp.json`
561
+ // (medium-confidence path → snippet, not auto-write).
562
+ id: "warp",
563
+ tier: 3,
564
+ displayName: "Warp",
565
+ async detect(io) {
566
+ return fileOrParentExists(io, resolveHostPath(io, WARP_PATH));
567
+ },
568
+ detectedPath(io) {
569
+ return resolveHostPath(io, WARP_PATH);
570
+ },
571
+ reason: "\uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uACF5\uC2DD\uC801\uC73C\uB85C \uD655\uC815\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
572
+ pasteWhere: '\uC544\uB798 \uB0B4\uC6A9\uC744 \uC124\uC815 \uD30C\uC77C\uC758 "mcpServers" \uD56D\uBAA9\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.',
573
+ buildSnippet(opts, platform) {
574
+ return jsonSnippet("mcpServers", opts, platform);
575
+ }
576
+ },
577
+ {
578
+ id: "claude-desktop-linux",
579
+ tier: 3,
580
+ displayName: "Claude Desktop (Linux)",
581
+ async detect(io) {
582
+ if (io.platform !== "linux") {
583
+ return false;
584
+ }
585
+ return io.exists(joinPath(io, io.homedir(), ".config", "Claude"));
586
+ },
587
+ detectedPath(io) {
588
+ return joinPath(io, io.homedir(), ".config", "Claude");
589
+ },
590
+ // Guessing a path here would manufacture false successes that only
591
+ // surface after a restart — the worst possible failure mode.
592
+ reason: "\uB9AC\uB205\uC2A4\uC6A9 \uACF5\uC2DD \uC124\uC815 \uD30C\uC77C \uACBD\uB85C\uAC00 \uD655\uC778\uB418\uC9C0 \uC54A\uC544 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
593
+ pasteWhere: "Claude Desktop \uACF5\uC2DD \uBB38\uC11C\uAC00 \uC548\uB0B4\uD558\uB294 \uC124\uC815 \uD30C\uC77C(claude_desktop_config.json)\uC5D0 \uC544\uB798 \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
594
+ buildSnippet(opts, platform) {
595
+ return jsonSnippet("mcpServers", opts, platform);
596
+ }
597
+ },
598
+ {
599
+ id: "codex-config-only",
600
+ tier: 3,
601
+ displayName: "ChatGPT \uB370\uC2A4\uD06C\uD1B1 / Codex (\uC124\uC815 \uD30C\uC77C\uB9CC \uBC1C\uACAC)",
602
+ async detect(io, ctx) {
603
+ if (ctx.cliDetected.has("codex")) {
604
+ return false;
605
+ }
606
+ if (await fileOrParentExists(io, codexConfigPath(io))) {
607
+ return true;
608
+ }
609
+ return msixPackageInstalled(io, CHATGPT_DESKTOP_PACKAGE);
610
+ },
611
+ detectedPath(io) {
612
+ return codexConfigPath(io);
613
+ },
614
+ reason: "Codex \uBA85\uB839\uC5B4(CLI)\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC544 TOML \uC124\uC815 \uD30C\uC77C\uC744 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694.",
615
+ pasteWhere: "\uC544\uB798 \uB0B4\uC6A9\uC744 \uC704 \uC124\uC815 \uD30C\uC77C\uC5D0 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.",
616
+ // 데스크톱 앱·Codex CLI·IDE 확장이 이 파일 하나를 공유한다(OpenAI 공식 문서).
617
+ createHint: "\uD30C\uC77C\uC774 \uC5C6\uC73C\uBA74 ChatGPT \uB370\uC2A4\uD06C\uD1B1\uC5D0\uC11C \uC124\uC815 \u2192 MCP \uC11C\uBC84 \u2192 \uC11C\uBC84 \uCD94\uAC00\uB97C \uD55C \uBC88 \uD558\uBA74 \uB9CC\uB4E4\uC5B4\uC838\uC694. \uD3F4\uB354\uAC00 \uC5C6\uC73C\uBA74 \uC9C1\uC811 \uB9CC\uB4E4\uC5B4\uB3C4 \uB3FC\uC694.",
618
+ bootstrapWhenAbsent: true,
619
+ buildSnippet(opts) {
620
+ return tomlSnippet(opts);
621
+ }
622
+ }
623
+ ];
624
+ var SUPPORTED_APPS = [
625
+ { name: "Claude Desktop", url: "https://claude.ai/download" },
626
+ { name: "Claude Code", url: "https://claude.com/claude-code" },
627
+ { name: "ChatGPT \uB370\uC2A4\uD06C\uD1B1", url: "https://openai.com/chatgpt/download/" },
628
+ { name: "Codex CLI", url: "https://developers.openai.com/codex/" },
629
+ { name: "Gemini CLI", url: "https://github.com/google-gemini/gemini-cli" },
630
+ { name: "Cursor", url: "https://cursor.com/downloads" },
631
+ { name: "Windsurf", url: "https://windsurf.com/download" },
632
+ { name: "JetBrains Junie", url: "https://www.jetbrains.com/junie/" },
633
+ { name: "Kiro", url: "https://kiro.dev/" },
634
+ { name: "GitHub Copilot CLI", url: "https://github.com/github/copilot-cli" },
635
+ { name: "OpenCode", url: "https://opencode.ai/" },
636
+ { name: "VS Code", url: "https://code.visualstudio.com/" },
637
+ { name: "Zed", url: "https://zed.dev/download" },
638
+ { name: "Cline", url: "https://cline.bot/" },
639
+ { name: "LM Studio", url: "https://lmstudio.ai/" },
640
+ { name: "Warp", url: "https://www.warp.dev/" }
641
+ ];
642
+ var RESTART_NOTICE = "\uB9C8\uC9C0\uB9C9 \uD55C \uB2E8\uACC4: AI \uC571\uC744 \uC644\uC804\uD788 \uC885\uB8CC\uD588\uB2E4\uAC00 \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.\n(Windows: \uC791\uC5C5 \uD45C\uC2DC\uC904 \uD2B8\uB808\uC774 \uC544\uC774\uCF58\uC5D0\uC11C \uC885\uB8CC)";
643
+ var NOTHING_CHANGED = "\uC544\uBB34\uAC83\uB3C4 \uBC14\uAFB8\uC9C0 \uC54A\uC558\uC5B4\uC694.";
644
+ var UNINSTALL_DONE = "\uC815\uB9AC\uAC00 \uB05D\uB0AC\uC5B4\uC694.";
645
+ var UNINSTALL_TOKEN_REMINDER = "\uC5F0\uACB0 \uD1A0\uD070\uAE4C\uC9C0 \uC5C6\uC560\uB824\uBA74 \uB370\uC774\uD130\uB7A9\uD234\uC988 \uD328\uB110\uC5D0\uC11C '\uC5F0\uB3D9 \uD574\uC81C' \uBC84\uD2BC\uC744 \uB20C\uB7EC \uC8FC\uC138\uC694. \uD3D0\uAE30\uB41C \uD1A0\uD070\uC740 \uC124\uC815\uC5D0 \uB0A8\uC740 \uD56D\uBAA9\uC744 \uC804\uBD80 \uBB34\uB825\uD654\uD574\uC694.";
646
+ var PERMISSION_DENIED_HINT = "\uD30C\uC77C \uAD8C\uD55C \uB54C\uBB38\uC5D0 \uC124\uC815\uC744 \uC4F8 \uC218 \uC5C6\uC5C8\uC5B4\uC694. \uD30C\uC77C \uAD8C\uD55C\uC744 \uD655\uC778\uD55C \uB4A4 \uAC19\uC740 \uBA85\uB839\uC5B4\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.";
647
+ var NO_HOSTS_DETECTED = "\uC5F0\uACB0\uD560 \uC218 \uC788\uB294 AI \uD504\uB85C\uADF8\uB7A8\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC5B4\uC694.";
648
+ var SUPPORTED_APPS_HEADER = "\uC9C0\uC6D0\uD558\uB294 \uD504\uB85C\uADF8\uB7A8\uACFC \uB2E4\uC6B4\uB85C\uB4DC \uC8FC\uC18C\uC608\uC694:";
649
+ var AFTER_INSTALL_RETRY = "\uD504\uB85C\uADF8\uB7A8\uC744 \uC124\uCE58\uD55C \uB4A4 \uAC19\uC740 \uBA85\uB839\uC5B4\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.";
650
+ var ALREADY_INSTALLED_HINT = "\uC774\uBBF8 \uC124\uCE58\uD588\uB294\uB370 \uBAA9\uB85D\uC5D0 \uC5C6\uB2E4\uBA74, \uC124\uC815 \uD30C\uC77C\uC774 \uC544\uC9C1 \uB9CC\uB4E4\uC5B4\uC9C0\uC9C0 \uC54A\uC558\uC744 \uC218 \uC788\uC5B4\uC694. \uC124\uC815 \uD30C\uC77C\uC740 \uD504\uB85C\uADF8\uB7A8\uC744 \uC124\uCE58\uD560 \uB54C\uAC00 \uC544\uB2C8\uB77C \uCC98\uC74C \uC4F8 \uB54C \uB9CC\uB4E4\uC5B4\uC838\uC694:\n - ChatGPT \uB370\uC2A4\uD06C\uD1B1: \uC124\uC815 \u2192 MCP \uC11C\uBC84 \u2192 \uC11C\uBC84 \uCD94\uAC00\uB97C \uD55C \uBC88 \uC2E4\uD589\n - VS Code: \uBA85\uB839 \uD314\uB808\uD2B8(Ctrl+Shift+P) \u2192 'MCP: Open User Configuration' \uC2E4\uD589\n\uADF8 \uB4A4 \uAC19\uC740 \uBA85\uB839\uC5B4\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uCC3E\uC744 \uC218 \uC788\uC5B4\uC694.";
651
+ var TOKEN_PROMPT_GUIDE = "\uD06C\uB86C\uC5D0\uC11C \uB370\uC774\uD130\uB7A9\uD234\uC988 \uC0AC\uC774\uB4DC \uD328\uB110\uC744 \uC5F4\uACE0 'MCP \uC5F0\uACB0' \uCE74\uB4DC\uC758 \uC5F0\uACB0 \uD1A0\uD070\uC744 \uBCF5\uC0AC\uD574 \uC8FC\uC138\uC694.";
652
+ var TOKEN_PROMPT_QUESTION = "\uBCF5\uC0AC\uD55C \uC5F0\uACB0 \uD1A0\uD070\uC744 \uC5EC\uAE30\uC5D0 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694:";
653
+ var TOKEN_PROMPT_RETRY = "\uD1A0\uD070 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC544\uC694. \uCE74\uB4DC\uC758 \uD1A0\uD070\uC744 \uB2E4\uC2DC \uBCF5\uC0AC\uD574\uC11C \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.";
654
+ var TOKEN_REQUIRED_NON_INTERACTIVE = "\uC5F0\uACB0 \uD1A0\uD070\uC774 \uD544\uC694\uD574\uC694. \uD06C\uB86C\uC5D0\uC11C \uB370\uC774\uD130\uB7A9\uD234\uC988 \uC0AC\uC774\uB4DC \uD328\uB110\uC744 \uC5F4\uACE0 'MCP \uC5F0\uACB0' \uCE74\uB4DC\uC758 [\uC124\uCE58 \uBA85\uB839 \uBCF5\uC0AC] \uBC84\uD2BC\uC744 \uB20C\uB7EC, \uBCF5\uC0AC\uB41C \uBA85\uB839\uC744 \uADF8\uB300\uB85C \uBD99\uC5EC\uB123\uC5B4 \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.";
655
+ var CLI_OFFER_INTRO = "\uC6D0\uD558\uC2DC\uBA74 \uC544\uB798 AI \uD504\uB85C\uADF8\uB7A8 \uC911 \uD558\uB098\uB97C \uC9C0\uAE08 \uBC14\uB85C \uC124\uCE58\uD560 \uC218 \uC788\uC5B4\uC694. Node.js \uAE30\uBC18\uC774\uB77C \uC774 \uC790\uB9AC\uC5D0\uC11C \uC124\uCE58\uB3FC\uC694 \u2014 \uC548 \uD558\uC154\uB3C4 \uAD1C\uCC2E\uC544\uC694.";
656
+ var CLI_OFFER_SKIP_LABEL = "0. \uC9C0\uAE08\uC740 \uC124\uCE58\uD558\uC9C0 \uC54A\uAE30";
657
+ var CLI_OFFER_QUESTION = "\uC124\uCE58\uD560 \uD504\uB85C\uADF8\uB7A8\uC758 \uBC88\uD638\uB97C \uC785\uB825\uD574 \uC8FC\uC138\uC694";
658
+ function cliInstalling(name, pkg) {
659
+ return `${name}\uC744(\uB97C) \uC124\uCE58\uD558\uACE0 \uC788\uC5B4\uC694... (npm install -g ${pkg})`;
660
+ }
661
+ function cliInstalled(name) {
662
+ return `${name} \uC124\uCE58\uB97C \uB9C8\uCCE4\uC5B4\uC694.`;
663
+ }
664
+ function cliInstallFailed(name) {
665
+ return `${name} \uC124\uCE58\uAC00 \uC2E4\uD328\uD588\uC5B4\uC694. \uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uAC70\uB098 \uC9C1\uC811 \uC124\uCE58\uD55C \uB4A4 \uAC19\uC740 \uBA85\uB839\uC744 \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.`;
666
+ }
667
+ function cliInstalledRetry(name) {
668
+ return `${name}\uC744(\uB97C) \uC124\uCE58\uD588\uC5B4\uC694. \uD130\uBBF8\uB110\uC744 \uC0C8\uB85C \uC5F4\uACE0 \uAC19\uC740 \uBA85\uB839\uC744 \uD55C \uBC88 \uB354 \uC2E4\uD589\uD558\uBA74 \uC5F0\uACB0\uB3FC\uC694.`;
669
+ }
670
+ function installQuestion(count) {
671
+ return `\uC704 ${count}\uAC1C \uD504\uB85C\uADF8\uB7A8\uC5D0 \uC5F0\uACB0\uD560\uAE4C\uC694?`;
672
+ }
673
+ function uninstallQuestion(count) {
674
+ return `\uC704 ${count}\uAC1C \uD504\uB85C\uADF8\uB7A8\uC5D0\uC11C \uC5F0\uACB0\uC744 \uD574\uC81C\uD560\uAE4C\uC694?`;
675
+ }
676
+ var TOKEN_RE = /^[0-9a-f]{32,128}$/i;
677
+ var EXTENSION_ID_RE = /^[a-p]{32}$/;
678
+ var PORT_RE = /^\d{2,5}$/;
679
+ var VERSION_RE = /^\d+\.\d+\.\d+$/;
680
+ var RETRY_HINT = "\uD328\uB110\uC5D0\uC11C \uBA85\uB839\uC5B4\uB97C \uB2E4\uC2DC \uBCF5\uC0AC\uD574\uC11C \uC2E4\uD589\uD574 \uC8FC\uC138\uC694.";
681
+ var INVALID_TOKEN_MESSAGE = `\uC5F0\uACB0 \uD1A0\uD070 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC544\uC694. ${RETRY_HINT}`;
682
+ var INVALID_EXTENSION_ID_MESSAGE = `\uD655\uC7A5 \uD504\uB85C\uADF8\uB7A8 ID \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC544\uC694. ${RETRY_HINT}`;
683
+ var INVALID_PORT_MESSAGE = `\uD3EC\uD2B8 \uBC88\uD638 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC544\uC694. ${RETRY_HINT}`;
684
+ var INVALID_VERSION_MESSAGE = `\uBC84\uC804 \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC544\uC694. ${RETRY_HINT}`;
685
+ function validateInstallOptions(opts) {
686
+ if (typeof opts.version !== "string" || !VERSION_RE.test(opts.version)) {
687
+ return INVALID_VERSION_MESSAGE;
688
+ }
689
+ if (typeof opts.token !== "string" || !TOKEN_RE.test(opts.token)) {
690
+ return INVALID_TOKEN_MESSAGE;
691
+ }
692
+ if (typeof opts.extensionId !== "string" || !EXTENSION_ID_RE.test(opts.extensionId)) {
693
+ return INVALID_EXTENSION_ID_MESSAGE;
694
+ }
695
+ if (opts.port !== void 0 && !PORT_RE.test(opts.port)) {
696
+ return INVALID_PORT_MESSAGE;
697
+ }
698
+ return null;
699
+ }
700
+ function validateUninstallOptions(opts) {
701
+ if (opts.version !== void 0 && opts.version !== "" && !VERSION_RE.test(opts.version)) {
702
+ return INVALID_VERSION_MESSAGE;
703
+ }
704
+ if (opts.token !== void 0 && !TOKEN_RE.test(opts.token)) {
705
+ return INVALID_TOKEN_MESSAGE;
706
+ }
707
+ if (opts.extensionId !== void 0 && !EXTENSION_ID_RE.test(opts.extensionId)) {
708
+ return INVALID_EXTENSION_ID_MESSAGE;
709
+ }
710
+ if (opts.port !== void 0 && !PORT_RE.test(opts.port)) {
711
+ return INVALID_PORT_MESSAGE;
712
+ }
713
+ return null;
714
+ }
715
+ var BACKUP_KEEP = 3;
716
+ function formatBackupTimestamp(date) {
717
+ const pad = (n) => String(n).padStart(2, "0");
718
+ return String(date.getUTCFullYear()) + pad(date.getUTCMonth() + 1) + pad(date.getUTCDate()) + pad(date.getUTCHours()) + pad(date.getUTCMinutes()) + pad(date.getUTCSeconds());
719
+ }
720
+ function splitPath(filePath) {
721
+ const cut = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
722
+ if (cut < 0) {
723
+ return { dir: ".", base: filePath };
724
+ }
725
+ return { dir: filePath.slice(0, cut), base: filePath.slice(cut + 1) };
726
+ }
727
+ function joinDir(dir, name) {
728
+ if (dir.includes("\\") && !dir.includes("/")) {
729
+ return `${dir}\\${name}`;
730
+ }
731
+ return `${dir}/${name}`;
732
+ }
733
+ function isPlainObject(value) {
734
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
735
+ return false;
736
+ }
737
+ return true;
738
+ }
739
+ async function readDoc(io, filePath) {
740
+ if (!await io.exists(filePath)) {
741
+ return { exists: false, raw: "", doc: null, parseError: false };
742
+ }
743
+ const raw = await io.readFile(filePath);
744
+ try {
745
+ const parsed = JSON.parse(raw);
746
+ if (!isPlainObject(parsed)) {
747
+ return { exists: true, raw, doc: null, parseError: true };
748
+ }
749
+ return { exists: true, raw, doc: parsed, parseError: false };
750
+ } catch {
751
+ return { exists: true, raw, doc: null, parseError: true };
752
+ }
753
+ }
754
+ function serialize(doc) {
755
+ return `${JSON.stringify(doc, null, 2)}
756
+ `;
757
+ }
758
+ async function atomicWrite(io, filePath, content) {
759
+ const tempPath = `${filePath}.tmp-${formatBackupTimestamp(io.now())}`;
760
+ await io.writeFile(tempPath, content);
761
+ await io.rename(tempPath, filePath);
762
+ }
763
+ async function writeBackup(io, filePath, raw) {
764
+ const { dir, base } = splitPath(filePath);
765
+ const timestamp = formatBackupTimestamp(io.now());
766
+ const backupPath = joinDir(dir, `${base}.backup-${timestamp}`);
767
+ await io.writeFile(backupPath, raw);
768
+ const prefix = `${base}.backup-`;
769
+ const names = await io.listDir(dir);
770
+ const backups = names.filter((name) => {
771
+ if (!name.startsWith(prefix)) {
772
+ return false;
773
+ }
774
+ return /^\d{14}$/.test(name.slice(prefix.length));
775
+ }).sort();
776
+ const excess = backups.slice(0, Math.max(0, backups.length - BACKUP_KEEP));
777
+ for (const name of excess) {
778
+ await io.unlink(joinDir(dir, name));
779
+ }
780
+ return backupPath;
781
+ }
782
+ async function findNewestBackup(io, filePath) {
783
+ const { dir, base } = splitPath(filePath);
784
+ const prefix = `${base}.backup-`;
785
+ const names = await io.listDir(dir);
786
+ const backups = names.filter((name) => {
787
+ if (!name.startsWith(prefix)) {
788
+ return false;
789
+ }
790
+ return /^\d{14}$/.test(name.slice(prefix.length));
791
+ }).sort();
792
+ const newest = backups[backups.length - 1];
793
+ if (newest === void 0) {
794
+ return null;
795
+ }
796
+ return joinDir(dir, newest);
797
+ }
798
+ async function restoreNewestBackup(io, filePath) {
799
+ const backupPath = await findNewestBackup(io, filePath);
800
+ if (backupPath === null) {
801
+ return false;
802
+ }
803
+ const raw = await io.readFile(backupPath);
804
+ await atomicWrite(io, filePath, raw);
805
+ return true;
806
+ }
807
+ function deepEqual(a, b) {
808
+ if (a === b) {
809
+ return true;
810
+ }
811
+ if (Array.isArray(a) && Array.isArray(b)) {
812
+ if (a.length !== b.length) {
813
+ return false;
814
+ }
815
+ return a.every((item, i) => deepEqual(item, b[i]));
816
+ }
817
+ if (isPlainObject(a) && isPlainObject(b)) {
818
+ const keysA = Object.keys(a);
819
+ const keysB = Object.keys(b);
820
+ if (keysA.length !== keysB.length) {
821
+ return false;
822
+ }
823
+ return keysA.every((key) => deepEqual(a[key], b[key]));
824
+ }
825
+ return false;
826
+ }
827
+ async function upsertServerKey(io, filePath, entry) {
828
+ const state = await readDoc(io, filePath);
829
+ if (state.parseError) {
830
+ return { ok: false, changed: false, reason: "parse" };
831
+ }
832
+ let doc;
833
+ if (state.doc !== null) {
834
+ doc = state.doc;
835
+ } else {
836
+ doc = {};
837
+ }
838
+ const existingServers = doc["mcpServers"];
839
+ let servers;
840
+ if (existingServers === void 0) {
841
+ servers = {};
842
+ } else if (isPlainObject(existingServers)) {
843
+ servers = existingServers;
844
+ } else {
845
+ return { ok: false, changed: false, reason: "parse" };
846
+ }
847
+ doc["mcpServers"] = servers;
848
+ servers[SERVER_NAME] = entry;
849
+ let backupPath;
850
+ if (state.exists) {
851
+ backupPath = await writeBackup(io, filePath, state.raw);
852
+ }
853
+ await atomicWrite(io, filePath, serialize(doc));
854
+ const after = await readDoc(io, filePath);
855
+ let verified = false;
856
+ if (after.doc !== null) {
857
+ const afterServers = after.doc["mcpServers"];
858
+ if (isPlainObject(afterServers)) {
859
+ verified = deepEqual(afterServers[SERVER_NAME], entry);
860
+ }
861
+ }
862
+ if (!verified) {
863
+ if (backupPath !== void 0) {
864
+ await restoreNewestBackup(io, filePath);
865
+ }
866
+ if (backupPath !== void 0) {
867
+ return { ok: false, changed: false, reason: "verify", backupPath };
868
+ }
869
+ return { ok: false, changed: false, reason: "verify" };
870
+ }
871
+ if (backupPath !== void 0) {
872
+ return { ok: true, changed: true, backupPath };
873
+ }
874
+ return { ok: true, changed: true };
875
+ }
876
+ async function removeServerKey(io, filePath) {
877
+ const state = await readDoc(io, filePath);
878
+ if (!state.exists) {
879
+ return { ok: true, changed: false, reason: "missing" };
880
+ }
881
+ if (state.parseError || state.doc === null) {
882
+ return { ok: false, changed: false, reason: "parse" };
883
+ }
884
+ const servers = state.doc["mcpServers"];
885
+ if (!isPlainObject(servers) || !(SERVER_NAME in servers)) {
886
+ return { ok: true, changed: false, reason: "missing" };
887
+ }
888
+ delete servers[SERVER_NAME];
889
+ const backupPath = await writeBackup(io, filePath, state.raw);
890
+ await atomicWrite(io, filePath, serialize(state.doc));
891
+ const after = await readDoc(io, filePath);
892
+ let verified = false;
893
+ if (after.doc !== null) {
894
+ const afterServers = after.doc["mcpServers"];
895
+ if (isPlainObject(afterServers)) {
896
+ verified = !(SERVER_NAME in afterServers);
897
+ } else if (afterServers === void 0) {
898
+ verified = true;
899
+ }
900
+ }
901
+ if (!verified) {
902
+ await restoreNewestBackup(io, filePath);
903
+ return { ok: false, changed: false, reason: "verify", backupPath };
904
+ }
905
+ return { ok: true, changed: true, backupPath };
906
+ }
907
+ async function detectHosts(io) {
908
+ const detected = [];
909
+ const useShell = io.platform === "win32";
910
+ const cliDetected = /* @__PURE__ */ new Set();
911
+ for (const host of CLI_HOSTS) {
912
+ let responded = false;
913
+ try {
914
+ const result = await io.spawn(host.bin, ["--version"], {
915
+ shell: useShell
916
+ });
917
+ responded = result.code === 0;
918
+ } catch {
919
+ responded = false;
920
+ }
921
+ if (responded) {
922
+ cliDetected.add(host.id);
923
+ detected.push({
924
+ tier: 1,
925
+ id: host.id,
926
+ displayName: host.displayName,
927
+ cli: host
928
+ });
929
+ }
930
+ }
931
+ for (const host of FILE_HOSTS) {
932
+ let configPath = null;
933
+ if (host.resolveConfigPath !== void 0) {
934
+ try {
935
+ configPath = await host.resolveConfigPath(io);
936
+ } catch {
937
+ configPath = null;
938
+ }
939
+ }
940
+ configPath ??= host.configPath(io);
941
+ if (configPath === null) {
942
+ continue;
943
+ }
944
+ let present = await io.exists(configPath);
945
+ if (!present) {
946
+ const cut = Math.max(
947
+ configPath.lastIndexOf("/"),
948
+ configPath.lastIndexOf("\\")
949
+ );
950
+ if (cut > 0) {
951
+ present = await io.exists(configPath.slice(0, cut));
952
+ }
953
+ }
954
+ if (present) {
955
+ detected.push({
956
+ tier: 2,
957
+ id: host.id,
958
+ displayName: host.displayName,
959
+ file: host,
960
+ configPath
961
+ });
962
+ }
963
+ }
964
+ for (const host of SNIPPET_HOSTS) {
965
+ let present = false;
966
+ try {
967
+ present = await host.detect(io, { cliDetected });
968
+ } catch {
969
+ present = false;
970
+ }
971
+ if (present) {
972
+ detected.push({
973
+ tier: 3,
974
+ id: host.id,
975
+ displayName: host.displayName,
976
+ snippet: host
977
+ });
978
+ }
979
+ }
980
+ return detected;
981
+ }
982
+ function tierLabel(tier) {
983
+ if (tier === 1) {
984
+ return "\uACF5\uC2DD \uBA85\uB839\uC5B4\uB85C \uC5F0\uACB0\uD574\uC694";
985
+ }
986
+ if (tier === 2) {
987
+ return "\uC124\uC815 \uD30C\uC77C\uC5D0 \uC548\uC804\uD558\uAC8C \uCD94\uAC00\uD574\uC694";
988
+ }
989
+ return "\uC9C1\uC811 \uBD99\uC5EC\uB123\uB3C4\uB85D \uC548\uB0B4\uB9CC \uD574\uC694";
990
+ }
991
+ function printDetected(io, detected) {
992
+ io.out("\uC5F0\uACB0\uD560 \uC218 \uC788\uB294 \uD504\uB85C\uADF8\uB7A8\uC744 \uCC3E\uC558\uC5B4\uC694:");
993
+ for (const tier of [1, 2, 3]) {
994
+ const group = detected.filter((d) => d.tier === tier);
995
+ if (group.length === 0) {
996
+ continue;
997
+ }
998
+ io.out(` ${tierLabel(tier)}:`);
999
+ for (const d of group) {
1000
+ io.out(` - ${d.displayName}`);
1001
+ }
1002
+ }
1003
+ }
1004
+ function printSupportedApps(io) {
1005
+ io.out(NO_HOSTS_DETECTED);
1006
+ io.out(ALREADY_INSTALLED_HINT);
1007
+ io.out("");
1008
+ io.out(SUPPORTED_APPS_HEADER);
1009
+ for (const app of SUPPORTED_APPS) {
1010
+ io.out(` - ${app.name}: ${app.url}`);
1011
+ }
1012
+ io.out(AFTER_INSTALL_RETRY);
1013
+ }
1014
+ async function offerCliInstall(io) {
1015
+ if (!io.isInteractive()) {
1016
+ return null;
1017
+ }
1018
+ io.out(NO_HOSTS_DETECTED);
1019
+ io.out("");
1020
+ io.out(CLI_OFFER_INTRO);
1021
+ INSTALLABLE_CLIS.forEach((cli, i) => {
1022
+ io.out(` ${i + 1}. ${cli.displayName}`);
1023
+ });
1024
+ io.out(` ${CLI_OFFER_SKIP_LABEL}`);
1025
+ const answer = await io.prompt(
1026
+ `${CLI_OFFER_QUESTION} (0-${INSTALLABLE_CLIS.length}):`
1027
+ );
1028
+ const n = Number(answer.trim());
1029
+ if (!Number.isInteger(n) || n < 1 || n > INSTALLABLE_CLIS.length) {
1030
+ return null;
1031
+ }
1032
+ return INSTALLABLE_CLIS[n - 1] ?? null;
1033
+ }
1034
+ async function installCli(io, cli) {
1035
+ io.out("");
1036
+ io.out(cliInstalling(cli.displayName, cli.npmPackage));
1037
+ let code = -1;
1038
+ try {
1039
+ const result = await io.spawn("npm", ["install", "-g", cli.npmPackage], {
1040
+ shell: io.platform === "win32",
1041
+ inheritStdio: true
1042
+ });
1043
+ code = result.code;
1044
+ } catch {
1045
+ code = -1;
1046
+ }
1047
+ if (code === 0) {
1048
+ io.out(cliInstalled(cli.displayName));
1049
+ return true;
1050
+ }
1051
+ io.out(cliInstallFailed(cli.displayName));
1052
+ return false;
1053
+ }
1054
+ function printResult(io, result) {
1055
+ let label;
1056
+ if (result.status === "success") {
1057
+ label = "[\uC131\uACF5]";
1058
+ } else if (result.status === "failed") {
1059
+ label = "[\uC2E4\uD328]";
1060
+ } else {
1061
+ label = "[\uAC74\uB108\uB700]";
1062
+ }
1063
+ let line = `${label} ${result.displayName}`;
1064
+ if (result.message !== void 0 && result.message !== "") {
1065
+ line = `${line} \u2014 ${result.message}`;
1066
+ }
1067
+ io.out(line);
1068
+ if (result.backupPath !== void 0) {
1069
+ io.out(` \uBC31\uC5C5: ${result.backupPath}`);
1070
+ }
1071
+ }
1072
+ async function bootstrapSnippetFile(io, host, opts) {
1073
+ if (host.bootstrapWhenAbsent !== true) {
1074
+ return null;
1075
+ }
1076
+ const path = host.detectedPath(io);
1077
+ if (path === null || await io.exists(path)) {
1078
+ return null;
1079
+ }
1080
+ const cut = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
1081
+ if (cut > 0) {
1082
+ await io.mkdir(path.slice(0, cut));
1083
+ }
1084
+ await io.writeFile(path, `${host.buildSnippet(opts, io.platform)}
1085
+ `);
1086
+ return {
1087
+ hostId: host.id,
1088
+ displayName: host.displayName,
1089
+ tier: 3,
1090
+ status: "success",
1091
+ message: `\uC124\uC815 \uD30C\uC77C\uC774 \uC5C6\uC5B4\uC11C \uC0C8\uB85C \uB9CC\uB4E4\uC5C8\uC5B4\uC694: ${path}`
1092
+ };
1093
+ }
1094
+ async function printSnippetBlock(io, host, opts) {
1095
+ io.out("");
1096
+ const path = host.detectedPath(io);
1097
+ if (path === null) {
1098
+ io.out(`${host.displayName} \uB97C \uCC3E\uC558\uC5B4\uC694.`);
1099
+ } else if (await io.exists(path)) {
1100
+ io.out(`${host.displayName} \u2014 \uCC3E\uC740 \uC704\uCE58: ${path}`);
1101
+ } else {
1102
+ io.out(`${host.displayName} \u2014 \uC124\uC815 \uD30C\uC77C\uC774 \uC544\uC9C1 \uC5C6\uC5B4\uC694: ${path}`);
1103
+ if (host.createHint !== void 0) {
1104
+ io.out(host.createHint);
1105
+ }
1106
+ }
1107
+ io.out(host.reason);
1108
+ io.out(host.pasteWhere);
1109
+ io.out(host.buildSnippet(opts, io.platform));
1110
+ }
1111
+ function isPermissionError(error) {
1112
+ if (typeof error !== "object" || error === null) {
1113
+ return false;
1114
+ }
1115
+ const code = error.code;
1116
+ if (code === "EACCES" || code === "EPERM") {
1117
+ return true;
1118
+ }
1119
+ return false;
1120
+ }
1121
+ function describeError(error) {
1122
+ if (isPermissionError(error)) {
1123
+ return PERMISSION_DENIED_HINT;
1124
+ }
1125
+ if (error instanceof Error && error.message !== "") {
1126
+ return `\uC608\uC0C1\uD558\uC9C0 \uBABB\uD55C \uBB38\uC81C\uAC00 \uC0DD\uACBC\uC5B4\uC694. (${error.message})`;
1127
+ }
1128
+ return "\uC608\uC0C1\uD558\uC9C0 \uBABB\uD55C \uBB38\uC81C\uAC00 \uC0DD\uACBC\uC5B4\uC694.";
1129
+ }
1130
+ function answeredNo(answer) {
1131
+ const normalized = answer.trim().toLowerCase();
1132
+ if (normalized === "n" || normalized === "no") {
1133
+ return true;
1134
+ }
1135
+ return false;
1136
+ }
1137
+ function tier2Snippet(host, opts, platform) {
1138
+ const entry = buildEntryForHost(host, opts, platform);
1139
+ return JSON.stringify({ mcpServers: { [SERVER_NAME]: entry } }, null, 2);
1140
+ }
1141
+ async function applyCliInstall(io, host, opts) {
1142
+ const useShell = io.platform === "win32";
1143
+ await io.spawn(host.bin, host.buildRemoveArgs(), { shell: useShell });
1144
+ const result = await io.spawn(host.bin, host.buildAddArgs(opts), {
1145
+ shell: useShell
1146
+ });
1147
+ if (result.code === 0) {
1148
+ return {
1149
+ hostId: host.id,
1150
+ displayName: host.displayName,
1151
+ tier: 1,
1152
+ status: "success",
1153
+ message: "\uC5F0\uACB0\uD588\uC5B4\uC694."
1154
+ };
1155
+ }
1156
+ return {
1157
+ hostId: host.id,
1158
+ displayName: host.displayName,
1159
+ tier: 1,
1160
+ status: "failed",
1161
+ message: `\uBA85\uB839\uC5B4 \uC2E4\uD589\uC774 \uC2E4\uD328\uD588\uC5B4\uC694. (\uC885\uB8CC \uCF54\uB4DC ${result.code})`
1162
+ };
1163
+ }
1164
+ async function applyFileInstall(io, detected, opts) {
1165
+ const host = detected.file;
1166
+ const configPath = detected.configPath;
1167
+ if (host === void 0 || configPath === void 0) {
1168
+ return {
1169
+ hostId: detected.id,
1170
+ displayName: detected.displayName,
1171
+ tier: 2,
1172
+ status: "failed",
1173
+ message: describeError(null)
1174
+ };
1175
+ }
1176
+ const entry = buildEntryForHost(host, opts, io.platform);
1177
+ const outcome = await upsertServerKey(io, configPath, entry);
1178
+ if (outcome.ok) {
1179
+ const result = {
1180
+ hostId: host.id,
1181
+ displayName: host.displayName,
1182
+ tier: 2,
1183
+ status: "success",
1184
+ message: "\uC124\uC815 \uD30C\uC77C\uC5D0 \uCD94\uAC00\uD588\uC5B4\uC694."
1185
+ };
1186
+ if (outcome.backupPath !== void 0) {
1187
+ result.backupPath = outcome.backupPath;
1188
+ }
1189
+ return result;
1190
+ }
1191
+ if (outcome.reason === "parse") {
1192
+ io.out(
1193
+ `${host.displayName} \uC124\uC815 \uD30C\uC77C\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC5B4 \uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694: ${configPath}`
1194
+ );
1195
+ io.out("\uC544\uB798 \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694.");
1196
+ io.out(tier2Snippet(host, opts, io.platform));
1197
+ return {
1198
+ hostId: host.id,
1199
+ displayName: host.displayName,
1200
+ tier: 2,
1201
+ status: "failed",
1202
+ message: "\uC124\uC815 \uD30C\uC77C\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC5B4 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uC5B4\uC694. \uC704\uC5D0 \uCD9C\uB825\uB41C \uB0B4\uC6A9\uC744 \uC9C1\uC811 \uBD99\uC5EC\uB123\uC5B4 \uC8FC\uC138\uC694."
1203
+ };
1204
+ }
1205
+ const failed = {
1206
+ hostId: host.id,
1207
+ displayName: host.displayName,
1208
+ tier: 2,
1209
+ status: "failed",
1210
+ message: "\uC4F0\uAE30 \uAC80\uC99D\uC5D0 \uC2E4\uD328\uD574\uC11C \uBC31\uC5C5\uC73C\uB85C \uB418\uB3CC\uB838\uC5B4\uC694. \uAC19\uC740 \uBA85\uB839\uC5B4\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD574 \uC8FC\uC138\uC694."
1211
+ };
1212
+ if (outcome.backupPath !== void 0) {
1213
+ failed.backupPath = outcome.backupPath;
1214
+ }
1215
+ return failed;
1216
+ }
1217
+ async function applyCliUninstall(io, host) {
1218
+ const useShell = io.platform === "win32";
1219
+ const result = await io.spawn(host.bin, host.buildRemoveArgs(), {
1220
+ shell: useShell
1221
+ });
1222
+ if (result.code === 0) {
1223
+ return {
1224
+ hostId: host.id,
1225
+ displayName: host.displayName,
1226
+ tier: 1,
1227
+ status: "success",
1228
+ message: "\uC5F0\uACB0\uC744 \uD574\uC81C\uD588\uC5B4\uC694."
1229
+ };
1230
+ }
1231
+ return {
1232
+ hostId: host.id,
1233
+ displayName: host.displayName,
1234
+ tier: 1,
1235
+ status: "skipped",
1236
+ message: "\uC774\uBBF8 \uD574\uC81C\uB418\uC5B4 \uC788\uAC70\uB098 \uB4F1\uB85D\uB41C \uD56D\uBAA9\uC774 \uC5C6\uC5B4\uC694."
1237
+ };
1238
+ }
1239
+ async function applyFileUninstall(io, detected) {
1240
+ const host = detected.file;
1241
+ const configPath = detected.configPath;
1242
+ if (host === void 0 || configPath === void 0) {
1243
+ return {
1244
+ hostId: detected.id,
1245
+ displayName: detected.displayName,
1246
+ tier: 2,
1247
+ status: "failed",
1248
+ message: describeError(null)
1249
+ };
1250
+ }
1251
+ const outcome = await removeServerKey(io, configPath);
1252
+ if (outcome.ok && outcome.changed) {
1253
+ const result = {
1254
+ hostId: host.id,
1255
+ displayName: host.displayName,
1256
+ tier: 2,
1257
+ status: "success",
1258
+ message: "\uC124\uC815 \uD30C\uC77C\uC5D0\uC11C \uD56D\uBAA9\uC744 \uC9C0\uC6E0\uC5B4\uC694."
1259
+ };
1260
+ if (outcome.backupPath !== void 0) {
1261
+ result.backupPath = outcome.backupPath;
1262
+ }
1263
+ return result;
1264
+ }
1265
+ if (outcome.ok) {
1266
+ return {
1267
+ hostId: host.id,
1268
+ displayName: host.displayName,
1269
+ tier: 2,
1270
+ status: "skipped",
1271
+ message: "\uC124\uC815\uC5D0 \uC9C0\uC6B8 \uD56D\uBAA9\uC774 \uC5C6\uC5B4\uC694."
1272
+ };
1273
+ }
1274
+ if (outcome.reason === "parse") {
1275
+ return {
1276
+ hostId: host.id,
1277
+ displayName: host.displayName,
1278
+ tier: 2,
1279
+ status: "failed",
1280
+ message: `\uC124\uC815 \uD30C\uC77C\uC744 \uD574\uC11D\uD560 \uC218 \uC5C6\uC5B4 \uC218\uC815\uD558\uC9C0 \uC54A\uC558\uC5B4\uC694. "${SERVER_NAME}" \uD56D\uBAA9\uC774 \uC788\uB2E4\uBA74 \uC9C1\uC811 \uC9C0\uC6CC \uC8FC\uC138\uC694: ${configPath}`
1281
+ };
1282
+ }
1283
+ const failed = {
1284
+ hostId: host.id,
1285
+ displayName: host.displayName,
1286
+ tier: 2,
1287
+ status: "failed",
1288
+ message: "\uC4F0\uAE30 \uAC80\uC99D\uC5D0 \uC2E4\uD328\uD574\uC11C \uBC31\uC5C5\uC73C\uB85C \uB418\uB3CC\uB838\uC5B4\uC694."
1289
+ };
1290
+ if (outcome.backupPath !== void 0) {
1291
+ failed.backupPath = outcome.backupPath;
1292
+ }
1293
+ return failed;
1294
+ }
1295
+ function filterByRequestedHosts(detected, hosts) {
1296
+ if (hosts === void 0 || hosts.length === 0) {
1297
+ return detected;
1298
+ }
1299
+ return detected.filter((d) => hosts.includes(d.id));
1300
+ }
1301
+ async function resolveInstallCredentials(opts, io) {
1302
+ if (typeof opts.token === "string" && opts.token !== "") {
1303
+ return opts;
1304
+ }
1305
+ if (!io.isInteractive()) {
1306
+ io.out(TOKEN_REQUIRED_NON_INTERACTIVE);
1307
+ return null;
1308
+ }
1309
+ io.out(TOKEN_PROMPT_GUIDE);
1310
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1311
+ const entered = await io.prompt(TOKEN_PROMPT_QUESTION);
1312
+ if (TOKEN_RE.test(entered)) {
1313
+ return {
1314
+ ...opts,
1315
+ token: entered,
1316
+ extensionId: opts.extensionId ?? DEFAULT_EXTENSION_ID
1317
+ };
1318
+ }
1319
+ if (attempt === 0) io.out(TOKEN_PROMPT_RETRY);
1320
+ }
1321
+ return { ...opts, extensionId: opts.extensionId ?? DEFAULT_EXTENSION_ID };
1322
+ }
1323
+ async function runInstall(opts, io) {
1324
+ printBanner((line) => io.out(line), INSTALL_SUBTITLE);
1325
+ const resolved = await resolveInstallCredentials(opts, io);
1326
+ if (resolved === null) return 1;
1327
+ const validationError = validateInstallOptions(resolved);
1328
+ if (validationError !== null) {
1329
+ io.out(validationError);
1330
+ return 1;
1331
+ }
1332
+ const entryOpts = {
1333
+ version: resolved.version,
1334
+ token: resolved.token,
1335
+ extensionId: resolved.extensionId
1336
+ };
1337
+ if (resolved.port !== void 0) {
1338
+ entryOpts.port = resolved.port;
1339
+ }
1340
+ let detected = filterByRequestedHosts(await detectHosts(io), opts.hosts);
1341
+ if (detected.length === 0) {
1342
+ const chosen = await offerCliInstall(io);
1343
+ if (chosen !== null && await installCli(io, chosen)) {
1344
+ detected = filterByRequestedHosts(await detectHosts(io), opts.hosts);
1345
+ if (detected.length === 0) {
1346
+ io.out(cliInstalledRetry(chosen.displayName));
1347
+ return 0;
1348
+ }
1349
+ }
1350
+ if (detected.length === 0) {
1351
+ printSupportedApps(io);
1352
+ return 1;
1353
+ }
1354
+ }
1355
+ printDetected(io, detected);
1356
+ const actionable = detected.filter((d) => d.tier !== 3);
1357
+ if (actionable.length > 0 && opts.yes !== true) {
1358
+ const answer = await io.ask(installQuestion(actionable.length));
1359
+ if (answeredNo(answer)) {
1360
+ io.out(NOTHING_CHANGED);
1361
+ return 0;
1362
+ }
1363
+ }
1364
+ const results = [];
1365
+ for (const d of detected) {
1366
+ try {
1367
+ if (d.tier === 1 && d.cli !== void 0) {
1368
+ results.push(await applyCliInstall(io, d.cli, entryOpts));
1369
+ } else if (d.tier === 2) {
1370
+ results.push(await applyFileInstall(io, d, entryOpts));
1371
+ } else if (d.snippet !== void 0) {
1372
+ const made = await bootstrapSnippetFile(io, d.snippet, entryOpts);
1373
+ results.push(
1374
+ made ?? {
1375
+ hostId: d.id,
1376
+ displayName: d.displayName,
1377
+ tier: 3,
1378
+ status: "skipped",
1379
+ message: `${d.snippet.reason} \uC544\uB798 \uC548\uB0B4\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694.`
1380
+ }
1381
+ );
1382
+ }
1383
+ } catch (error) {
1384
+ results.push({
1385
+ hostId: d.id,
1386
+ displayName: d.displayName,
1387
+ tier: d.tier,
1388
+ status: "failed",
1389
+ message: describeError(error)
1390
+ });
1391
+ }
1392
+ }
1393
+ io.out("");
1394
+ for (const result of results) {
1395
+ printResult(io, result);
1396
+ if (result.status === "success" && result.tier === 1) {
1397
+ const cliHost = CLI_HOSTS.find((h) => h.id === result.hostId);
1398
+ if (cliHost !== void 0 && cliHost.note !== void 0) {
1399
+ io.out(` ${cliHost.note}`);
1400
+ }
1401
+ }
1402
+ }
1403
+ for (const d of detected) {
1404
+ if (d.tier === 3 && d.snippet !== void 0) {
1405
+ await printSnippetBlock(io, d.snippet, entryOpts);
1406
+ }
1407
+ }
1408
+ const anyConfigured = results.some((r) => r.status === "success");
1409
+ const anyFailed = results.some((r) => r.status === "failed" && r.tier !== 3);
1410
+ if (anyConfigured) {
1411
+ io.out("");
1412
+ io.out(RESTART_NOTICE);
1413
+ }
1414
+ if (anyFailed) {
1415
+ return 1;
1416
+ }
1417
+ return 0;
1418
+ }
1419
+ async function runUninstall(opts, io) {
1420
+ printBanner((line) => io.out(line), UNINSTALL_SUBTITLE);
1421
+ const validationError = validateUninstallOptions(opts);
1422
+ if (validationError !== null) {
1423
+ io.out(validationError);
1424
+ return 1;
1425
+ }
1426
+ const detected = filterByRequestedHosts(await detectHosts(io), opts.hosts);
1427
+ if (detected.length === 0) {
1428
+ io.out("\uC815\uB9AC\uD560 \uD56D\uBAA9\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC5B4\uC694.");
1429
+ io.out(UNINSTALL_DONE);
1430
+ io.out(UNINSTALL_TOKEN_REMINDER);
1431
+ return 0;
1432
+ }
1433
+ printDetected(io, detected);
1434
+ const actionable = detected.filter((d) => d.tier !== 3);
1435
+ if (actionable.length > 0 && opts.yes !== true) {
1436
+ const answer = await io.ask(uninstallQuestion(actionable.length));
1437
+ if (answeredNo(answer)) {
1438
+ io.out(NOTHING_CHANGED);
1439
+ return 0;
1440
+ }
1441
+ }
1442
+ const results = [];
1443
+ for (const d of detected) {
1444
+ try {
1445
+ if (d.tier === 1 && d.cli !== void 0) {
1446
+ results.push(await applyCliUninstall(io, d.cli));
1447
+ } else if (d.tier === 2) {
1448
+ results.push(await applyFileUninstall(io, d));
1449
+ } else if (d.snippet !== void 0) {
1450
+ results.push({
1451
+ hostId: d.id,
1452
+ displayName: d.displayName,
1453
+ tier: 3,
1454
+ status: "skipped",
1455
+ message: `\uC790\uB3D9 \uC218\uC815\uD558\uC9C0 \uC54A\uC544\uC694. \uC124\uC815\uC5D0 "${SERVER_NAME}" \uD56D\uBAA9\uC774 \uC788\uB2E4\uBA74 \uC9C1\uC811 \uC9C0\uC6CC \uC8FC\uC138\uC694. \uC9C0\uC6B0\uC9C0 \uC54A\uC544\uB3C4 \uC544\uBB34 \uC77C\uB3C4 \uD558\uC9C0 \uC54A\uC544\uC694.`
1456
+ });
1457
+ }
1458
+ } catch (error) {
1459
+ results.push({
1460
+ hostId: d.id,
1461
+ displayName: d.displayName,
1462
+ tier: d.tier,
1463
+ status: "failed",
1464
+ message: describeError(error)
1465
+ });
1466
+ }
1467
+ }
1468
+ io.out("");
1469
+ for (const result of results) {
1470
+ printResult(io, result);
1471
+ }
1472
+ io.out("");
1473
+ io.out(UNINSTALL_DONE);
1474
+ io.out(UNINSTALL_TOKEN_REMINDER);
1475
+ const anyFailed = results.some((r) => r.status === "failed" && r.tier !== 3);
1476
+ if (anyFailed) {
1477
+ return 1;
1478
+ }
1479
+ return 0;
1480
+ }
1481
+ function createNodeIo() {
1482
+ return {
1483
+ platform: process.platform,
1484
+ homedir() {
1485
+ return homedir();
1486
+ },
1487
+ env: process.env,
1488
+ async readFile(path) {
1489
+ return fs.readFile(path, "utf8");
1490
+ },
1491
+ async writeFile(path, content) {
1492
+ await fs.writeFile(path, content, "utf8");
1493
+ },
1494
+ async rename(from, to) {
1495
+ await fs.rename(from, to);
1496
+ },
1497
+ async mkdir(path) {
1498
+ await fs.mkdir(path, { recursive: true });
1499
+ },
1500
+ async exists(path) {
1501
+ try {
1502
+ await fs.access(path);
1503
+ return true;
1504
+ } catch {
1505
+ return false;
1506
+ }
1507
+ },
1508
+ async unlink(path) {
1509
+ await fs.unlink(path);
1510
+ },
1511
+ async listDir(dir) {
1512
+ try {
1513
+ return await fs.readdir(dir);
1514
+ } catch {
1515
+ return [];
1516
+ }
1517
+ },
1518
+ async spawn(command, args, opts) {
1519
+ return new Promise((resolve) => {
1520
+ const useShell = opts.shell === true;
1521
+ const spawnCommand = useShell ? [command, ...args].join(" ") : command;
1522
+ const spawnArgs = useShell ? [] : args;
1523
+ const child = nodeSpawn(spawnCommand, spawnArgs, {
1524
+ shell: opts.shell,
1525
+ stdio: opts.inheritStdio ? ["inherit", "inherit", "inherit"] : ["ignore", "ignore", "ignore"]
1526
+ });
1527
+ child.on("error", () => {
1528
+ resolve({ code: -1 });
1529
+ });
1530
+ child.on("close", (code) => {
1531
+ if (code === null) {
1532
+ resolve({ code: -1 });
1533
+ } else {
1534
+ resolve({ code });
1535
+ }
1536
+ });
1537
+ });
1538
+ },
1539
+ async ask(question) {
1540
+ const rl = createInterface({
1541
+ input: process.stdin,
1542
+ output: process.stdout
1543
+ });
1544
+ try {
1545
+ return await new Promise((resolve) => {
1546
+ rl.question(`${question} (Y/n) `, resolve);
1547
+ });
1548
+ } finally {
1549
+ rl.close();
1550
+ }
1551
+ },
1552
+ async prompt(question) {
1553
+ const rl = createInterface({
1554
+ input: process.stdin,
1555
+ output: process.stdout
1556
+ });
1557
+ try {
1558
+ const answer = await new Promise((resolve) => {
1559
+ rl.question(`${question} `, resolve);
1560
+ });
1561
+ return answer.trim();
1562
+ } finally {
1563
+ rl.close();
1564
+ }
1565
+ },
1566
+ isInteractive() {
1567
+ return Boolean(process.stdin.isTTY);
1568
+ },
1569
+ out(line) {
1570
+ process.stdout.write(`${line}
1571
+ `);
1572
+ },
1573
+ now() {
1574
+ return /* @__PURE__ */ new Date();
1575
+ }
1576
+ };
1577
+ }
1578
+ var PACKAGE_NAME = "@modootoday/extension-app-mcp-installer";
1579
+ export {
1580
+ BACKUP_KEEP,
1581
+ CLI_HOSTS,
1582
+ CODEX_CHATGPT_NOTE,
1583
+ DEFAULT_PORT,
1584
+ EXTENSION_ID_RE,
1585
+ FILE_HOSTS,
1586
+ INSTALLABLE_CLIS,
1587
+ INSTALL_SUBTITLE,
1588
+ INVALID_EXTENSION_ID_MESSAGE,
1589
+ INVALID_PORT_MESSAGE,
1590
+ INVALID_TOKEN_MESSAGE,
1591
+ INVALID_VERSION_MESSAGE,
1592
+ NOTHING_CHANGED,
1593
+ NO_HOSTS_DETECTED,
1594
+ PACKAGE_NAME,
1595
+ PERMISSION_DENIED_HINT,
1596
+ PORT_RE,
1597
+ RESTART_NOTICE,
1598
+ SERVER_NAME,
1599
+ SERVER_PACKAGE,
1600
+ SNIPPET_HOSTS,
1601
+ SUPPORTED_APPS,
1602
+ TOKEN_RE,
1603
+ UNINSTALL_DONE,
1604
+ UNINSTALL_SUBTITLE,
1605
+ UNINSTALL_TOKEN_REMINDER,
1606
+ VERSION_RE,
1607
+ buildEnv,
1608
+ buildFileEntry,
1609
+ createNodeIo,
1610
+ detectHosts,
1611
+ formatBackupTimestamp,
1612
+ installQuestion,
1613
+ packageSpec,
1614
+ printBanner,
1615
+ removeServerKey,
1616
+ runInstall,
1617
+ runUninstall,
1618
+ uninstallQuestion,
1619
+ upsertServerKey,
1620
+ validateInstallOptions,
1621
+ validateUninstallOptions
1622
+ };