@dollhousemcp/mcp-server 2.0.2 → 2.0.4
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 +35 -0
- package/README.github.md +8 -33
- package/README.md +10 -8
- package/README.md.backup +10 -8
- package/README.npm.md +10 -8
- package/dist/constants/version.d.ts +3 -0
- package/dist/constants/version.d.ts.map +1 -0
- package/dist/constants/version.js +4 -0
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +3 -3
- package/dist/logging/sinks/SSELogSink.d.ts +35 -0
- package/dist/logging/sinks/SSELogSink.d.ts.map +1 -0
- package/dist/logging/sinks/SSELogSink.js +181 -0
- package/dist/logging/viewer/viewerHtml.d.ts +8 -0
- package/dist/logging/viewer/viewerHtml.d.ts.map +1 -0
- package/dist/logging/viewer/viewerHtml.js +204 -0
- package/dist/security/audit/config/suppressions.d.ts.map +1 -1
- package/dist/security/audit/config/suppressions.js +6 -1
- package/dist/seed-elements/memories/dollhousemcp-baseline-knowledge.yaml +149 -0
- package/dist/seed-elements/memories/how-to-create-custom-auto-load-memories.yaml +455 -0
- package/dist/seed-elements/memories/priority-best-practices-for-teams.yaml +542 -0
- package/dist/seed-elements/memories/token-estimation-guidelines.yaml +602 -0
- package/dist/web/public/app.js +29 -10
- package/dist/web/public/fonts/ibmplexmono--F63fjptAgt5VM-kVkqdyU8n1i8q131nj-o.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F63fjptAgt5VM-kVkqdyU8n1iAq131nj-otFQ.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F63fjptAgt5VM-kVkqdyU8n1iEq131nj-otFQ.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F63fjptAgt5VM-kVkqdyU8n1iIq131nj-otFQ.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F63fjptAgt5VM-kVkqdyU8n1isq131nj-otFQ.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F6qfjptAgt5VM-kVkqdyU8n3twJwl1FgsAXHNlYzg.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F6qfjptAgt5VM-kVkqdyU8n3twJwl5FgsAXHNlYzg.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F6qfjptAgt5VM-kVkqdyU8n3twJwl9FgsAXHNlYzg.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F6qfjptAgt5VM-kVkqdyU8n3twJwlBFgsAXHNk.woff2 +0 -0
- package/dist/web/public/fonts/ibmplexmono--F6qfjptAgt5VM-kVkqdyU8n3twJwlRFgsAXHNlYzg.woff2 +0 -0
- package/dist/web/public/fonts/manrope-xn7gYHE41ni1AdIRggOxSvfedN62Zw.woff2 +0 -0
- package/dist/web/public/fonts/manrope-xn7gYHE41ni1AdIRggSxSvfedN62Zw.woff2 +0 -0
- package/dist/web/public/fonts/manrope-xn7gYHE41ni1AdIRggexSvfedN4.woff2 +0 -0
- package/dist/web/public/fonts/manrope-xn7gYHE41ni1AdIRggixSvfedN62Zw.woff2 +0 -0
- package/dist/web/public/fonts/manrope-xn7gYHE41ni1AdIRggmxSvfedN62Zw.woff2 +0 -0
- package/dist/web/public/fonts/manrope-xn7gYHE41ni1AdIRggqxSvfedN62Zw.woff2 +0 -0
- package/dist/web/public/fonts/plusjakartasans-LDIoaomQNQcsA88c7O9yZ4KMCoOg4Ko20yygg_vb.woff2 +0 -0
- package/dist/web/public/fonts/plusjakartasans-LDIoaomQNQcsA88c7O9yZ4KMCoOg4Ko40yygg_vbd-E.woff2 +0 -0
- package/dist/web/public/fonts/plusjakartasans-LDIoaomQNQcsA88c7O9yZ4KMCoOg4Ko50yygg_vbd-E.woff2 +0 -0
- package/dist/web/public/fonts/plusjakartasans-LDIoaomQNQcsA88c7O9yZ4KMCoOg4Ko70yygg_vbd-E.woff2 +0 -0
- package/dist/web/public/fonts.css +270 -0
- package/dist/web/public/index.html +365 -0
- package/dist/web/public/logs.css +472 -0
- package/dist/web/public/metrics.css +238 -0
- package/dist/web/public/permissions.css +364 -0
- package/dist/web/public/sessions.css +235 -0
- package/dist/web/public/setup.css +648 -0
- package/dist/web/public/setup.js +752 -0
- package/dist/web/public/styles.css +1717 -0
- package/dist/web/routes/setupRoutes.d.ts +18 -0
- package/dist/web/routes/setupRoutes.d.ts.map +1 -0
- package/dist/web/routes/setupRoutes.js +360 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +11 -1
- package/package.json +4 -1
- package/server.json +2 -2
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DollhouseMCP Console — Setup Tab
|
|
3
|
+
*
|
|
4
|
+
* OS detection, platform tab switching, install method toggle,
|
|
5
|
+
* auto-install via API, copy-to-clipboard for install configs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
(() => {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
// ── Config builders ────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const PKG = '@dollhousemcp/mcp-server';
|
|
14
|
+
|
|
15
|
+
/** Platform registry — drives config generation AND panel rendering */
|
|
16
|
+
const PLATFORMS = [
|
|
17
|
+
// Claude Desktop & Claude Code panels are handwritten in HTML (unique structure)
|
|
18
|
+
{ id: 'claude-desktop', rootKey: 'mcpServers' },
|
|
19
|
+
{ id: 'claude-code', rootKey: 'mcpServers', cli: 'claude' },
|
|
20
|
+
// These panels are generated from this data by renderGeneratedPanels()
|
|
21
|
+
{ id: 'cursor', rootKey: 'mcpServers', installClient: 'cursor', openClient: 'cursor', configPath: '<code>.cursor/mcp.json</code> in your project, or <code>~/.cursor/mcp.json</code> for all projects', hint: 'Or configure via Settings > MCP Servers in the Cursor UI.' },
|
|
22
|
+
{ id: 'vscode', rootKey: 'servers', installClient: 'vscode', configPath: '<code>.vscode/mcp.json</code> in your workspace', hint: 'VS Code uses <code>"servers"</code>, not <code>"mcpServers"</code>.' },
|
|
23
|
+
{ id: 'codex', rootKey: 'mcpServers', installClient: 'codex', openClient: 'codex', cli: 'codex', toml: true, tomlPath: '<code>~/.codex/config.toml</code> (Codex uses TOML, not JSON)' },
|
|
24
|
+
{ id: 'gemini', rootKey: 'mcpServers', installClient: 'gemini-cli', openClient: 'gemini-cli', cli: 'gemini', configPath: '<code>~/.gemini/settings.json</code> or <code>.gemini/settings.json</code> in your project' },
|
|
25
|
+
{ id: 'windsurf', rootKey: 'mcpServers', installClient: 'windsurf', openClient: 'windsurf', configPath: '<code>~/.codeium/windsurf/mcp_config.json</code>', hint: 'Or click the MCPs icon in the Cascade panel > Configure.' },
|
|
26
|
+
{ id: 'cline', rootKey: 'mcpServers', installClient: 'cline', configPath: '<code>cline_mcp_settings.json</code> via Cline\'s top nav > Configure > Advanced MCP Settings' },
|
|
27
|
+
{ id: 'lmstudio', rootKey: 'mcpServers', openClient: 'lmstudio', configPath: '<code>~/.lmstudio/mcp.json</code> (or open via Program tab > Install > Edit mcp.json)', hint: 'Restart LM Studio after saving.' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Build a JSON config block for a given npx command string */
|
|
31
|
+
function jsonConfig(rootKey, npxCmd) {
|
|
32
|
+
const parts = npxCmd.split(' ');
|
|
33
|
+
const obj = {};
|
|
34
|
+
obj[rootKey] = { dollhousemcp: { command: parts[0], args: parts.slice(1) } };
|
|
35
|
+
return { code: JSON.stringify(obj, null, 2), copyText: JSON.stringify(obj) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Build npx command string for a version tag */
|
|
39
|
+
const npxCmd = (tag) => `npx -y ${PKG}@${tag}`;
|
|
40
|
+
|
|
41
|
+
/** Build all platform configs for a given pinned version */
|
|
42
|
+
function buildConfigs(version) {
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const { id, rootKey, cli, toml } of PLATFORMS) {
|
|
45
|
+
const entry = {
|
|
46
|
+
npx: cli
|
|
47
|
+
? { code: `${cli} mcp add dollhousemcp -- ${npxCmd('latest')}`, isTerminal: true }
|
|
48
|
+
: jsonConfig(rootKey, npxCmd('latest')),
|
|
49
|
+
global: cli
|
|
50
|
+
? { code: `${cli} mcp add dollhousemcp -- ${npxCmd(version)}`, isTerminal: true }
|
|
51
|
+
: jsonConfig(rootKey, npxCmd(version)),
|
|
52
|
+
};
|
|
53
|
+
if (cli) {
|
|
54
|
+
entry.npxJson = jsonConfig(rootKey, npxCmd('latest'));
|
|
55
|
+
entry.globalJson = jsonConfig(rootKey, npxCmd(version));
|
|
56
|
+
}
|
|
57
|
+
if (toml) {
|
|
58
|
+
const tomlBlock = (tag) => `[mcp_servers.dollhousemcp]\ncommand = "npx"\nargs = ["-y", "${PKG}@${tag}"]`;
|
|
59
|
+
entry.npxToml = { code: tomlBlock('latest') };
|
|
60
|
+
entry.globalToml = { code: tomlBlock(version) };
|
|
61
|
+
}
|
|
62
|
+
result[id] = entry;
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Start with a placeholder version, update once we fetch from server
|
|
68
|
+
let pinnedVersion = 'latest';
|
|
69
|
+
let configs = buildConfigs(pinnedVersion);
|
|
70
|
+
|
|
71
|
+
// ── Current method state ──────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
let currentMethod = 'npx';
|
|
74
|
+
|
|
75
|
+
// ── OS detection ──────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const detectOS = () => {
|
|
78
|
+
const ua = navigator.userAgent;
|
|
79
|
+
if (/Mac/i.test(ua)) return 'macos';
|
|
80
|
+
if (/Win/i.test(ua)) return 'windows';
|
|
81
|
+
return 'linux';
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ── Highlight current OS in path lists ────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const highlightOSPaths = (os) => {
|
|
87
|
+
const labels = { macos: 'macOS', windows: 'Windows', linux: 'Linux' };
|
|
88
|
+
const label = labels[os];
|
|
89
|
+
if (!label) return;
|
|
90
|
+
|
|
91
|
+
document.querySelectorAll('.setup-paths li').forEach((li) => {
|
|
92
|
+
const strong = li.querySelector('strong');
|
|
93
|
+
if (strong && strong.textContent.trim().replace(':', '') === label) {
|
|
94
|
+
li.classList.add('is-current');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
document.querySelectorAll('.setup-os-path').forEach((el) => {
|
|
99
|
+
const osPath = el.dataset[os];
|
|
100
|
+
if (osPath) el.textContent = osPath;
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ── Method toggle ─────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const initMethodToggle = () => {
|
|
107
|
+
const toggle = document.getElementById('setup-method-toggle');
|
|
108
|
+
if (!toggle) return;
|
|
109
|
+
|
|
110
|
+
const buttons = toggle.querySelectorAll('.setup-method-btn');
|
|
111
|
+
// Cache DOM elements queried on every toggle click
|
|
112
|
+
const prereq = document.getElementById('setup-pinned-prereq');
|
|
113
|
+
const mcpbSection = document.getElementById('setup-mcpb-section');
|
|
114
|
+
|
|
115
|
+
const handleToggle = (btn) => {
|
|
116
|
+
const method = btn.dataset.method;
|
|
117
|
+
if (!method || method === currentMethod) return;
|
|
118
|
+
|
|
119
|
+
currentMethod = method;
|
|
120
|
+
|
|
121
|
+
buttons.forEach((b) => {
|
|
122
|
+
b.classList.toggle('is-active', b.dataset.method === method);
|
|
123
|
+
b.setAttribute('aria-pressed', b.dataset.method === method ? 'true' : 'false');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (prereq) prereq.hidden = method !== 'global';
|
|
127
|
+
if (mcpbSection) mcpbSection.hidden = method !== 'global';
|
|
128
|
+
|
|
129
|
+
updateAllConfigs(method);
|
|
130
|
+
updateInstallButtonLabels();
|
|
131
|
+
updateDetectionState();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
buttons.forEach((btn) => {
|
|
135
|
+
btn.addEventListener('click', () => handleToggle(btn));
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** Rewrite code blocks and copy-text for the selected method */
|
|
140
|
+
const updateAllConfigs = (method) => {
|
|
141
|
+
for (const [platformKey, platformConfigs] of Object.entries(configs)) {
|
|
142
|
+
const panel = document.getElementById('setup-panel-' + platformKey);
|
|
143
|
+
if (!panel) continue;
|
|
144
|
+
|
|
145
|
+
const codeBlocks = Array.from(panel.querySelectorAll('.setup-code-block'));
|
|
146
|
+
let blockIdx = 0;
|
|
147
|
+
|
|
148
|
+
// Primary (terminal command or JSON config) — first code block
|
|
149
|
+
const primary = platformConfigs[method];
|
|
150
|
+
if (primary && codeBlocks[blockIdx]) {
|
|
151
|
+
updateCodeBlock(codeBlocks[blockIdx], primary);
|
|
152
|
+
blockIdx++;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Secondary JSON (e.g., claude-code has terminal + JSON manual config)
|
|
156
|
+
const jsonKey = method + 'Json';
|
|
157
|
+
if (platformConfigs[jsonKey] && codeBlocks[blockIdx]) {
|
|
158
|
+
updateCodeBlock(codeBlocks[blockIdx], platformConfigs[jsonKey]);
|
|
159
|
+
blockIdx++;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Tertiary (TOML for Codex)
|
|
163
|
+
const tomlKey = method + 'Toml';
|
|
164
|
+
if (platformConfigs[tomlKey] && codeBlocks[blockIdx]) {
|
|
165
|
+
updateCodeBlock(codeBlocks[blockIdx], platformConfigs[tomlKey]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** Update a single code block's displayed code and copy button */
|
|
171
|
+
const updateCodeBlock = (block, config) => {
|
|
172
|
+
if (!block || !config) return;
|
|
173
|
+
|
|
174
|
+
const pre = block.querySelector('pre code');
|
|
175
|
+
const copyBtn = block.querySelector('.setup-copy-btn');
|
|
176
|
+
|
|
177
|
+
if (pre) pre.textContent = config.code;
|
|
178
|
+
if (copyBtn) copyBtn.dataset.copyText = config.copyText || config.code;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ── Platform tab switching ────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
const initPlatformTabs = () => {
|
|
184
|
+
const nav = document.getElementById('setup-platforms');
|
|
185
|
+
if (!nav) return;
|
|
186
|
+
|
|
187
|
+
const tabs = nav.querySelectorAll('[role="tab"]');
|
|
188
|
+
const container = nav.parentElement;
|
|
189
|
+
const panels = container.querySelectorAll('[role="tabpanel"]');
|
|
190
|
+
|
|
191
|
+
const activate = (tab) => {
|
|
192
|
+
const targetId = tab.getAttribute('aria-controls');
|
|
193
|
+
if (!targetId) return;
|
|
194
|
+
|
|
195
|
+
tabs.forEach((t) => {
|
|
196
|
+
t.classList.remove('is-active');
|
|
197
|
+
t.setAttribute('aria-selected', 'false');
|
|
198
|
+
t.setAttribute('tabindex', '-1');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
panels.forEach((p) => {
|
|
202
|
+
p.classList.remove('is-active');
|
|
203
|
+
p.hidden = true;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
tab.classList.add('is-active');
|
|
207
|
+
tab.setAttribute('aria-selected', 'true');
|
|
208
|
+
tab.setAttribute('tabindex', '0');
|
|
209
|
+
|
|
210
|
+
const panel = container.querySelector('#' + targetId);
|
|
211
|
+
if (panel) {
|
|
212
|
+
panel.classList.add('is-active');
|
|
213
|
+
panel.hidden = false;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
tabs.forEach((tab, i) => {
|
|
218
|
+
tab.addEventListener('click', () => activate(tab));
|
|
219
|
+
tab.setAttribute('tabindex', tab.classList.contains('is-active') ? '0' : '-1');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Keyboard navigation: arrow keys cycle through platform tabs
|
|
223
|
+
nav.addEventListener('keydown', (e) => {
|
|
224
|
+
const tabArr = Array.from(tabs);
|
|
225
|
+
const current = tabArr.findIndex(t => t.classList.contains('is-active'));
|
|
226
|
+
let next = -1;
|
|
227
|
+
|
|
228
|
+
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
229
|
+
next = (current + 1) % tabArr.length;
|
|
230
|
+
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
231
|
+
next = (current - 1 + tabArr.length) % tabArr.length;
|
|
232
|
+
} else if (e.key === 'Home') {
|
|
233
|
+
next = 0;
|
|
234
|
+
} else if (e.key === 'End') {
|
|
235
|
+
next = tabArr.length - 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (next >= 0) {
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
activate(tabArr[next]);
|
|
241
|
+
tabArr[next].focus();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// ── Copy buttons ──────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
const initCopyButtons = () => {
|
|
249
|
+
// Use event delegation so dynamically updated copy-text works
|
|
250
|
+
document.addEventListener('click', async (e) => {
|
|
251
|
+
const btn = e.target.closest('.setup-copy-btn');
|
|
252
|
+
if (!btn) return;
|
|
253
|
+
|
|
254
|
+
const text = btn.dataset.copyText;
|
|
255
|
+
if (!text) return;
|
|
256
|
+
|
|
257
|
+
const original = btn.textContent;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
let copyText = text;
|
|
261
|
+
try {
|
|
262
|
+
const parsed = JSON.parse(text);
|
|
263
|
+
copyText = JSON.stringify(parsed, null, 2);
|
|
264
|
+
} catch {
|
|
265
|
+
// Not JSON — copy as-is
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await navigator.clipboard.writeText(copyText);
|
|
269
|
+
btn.textContent = 'Copied';
|
|
270
|
+
btn.dataset.copied = '';
|
|
271
|
+
} catch {
|
|
272
|
+
btn.textContent = 'Failed';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
btn.textContent = original;
|
|
277
|
+
delete btn.dataset.copied;
|
|
278
|
+
}, 1400);
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// ── Update install button labels based on method ────────────────────────
|
|
283
|
+
|
|
284
|
+
const updateInstallButtonLabels = () => {
|
|
285
|
+
const isPinned = currentMethod === 'global' && pinnedVersion && pinnedVersion !== 'latest';
|
|
286
|
+
|
|
287
|
+
// Update Install buttons
|
|
288
|
+
document.querySelectorAll('.setup-install-btn').forEach((btn) => {
|
|
289
|
+
if (btn.classList.contains('is-success') || btn.classList.contains('is-match')) return;
|
|
290
|
+
btn.textContent = isPinned ? `Install v${pinnedVersion}` : 'Install Now';
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Update auto-install badges and descriptions
|
|
294
|
+
document.querySelectorAll('.setup-method-badge').forEach((badge) => {
|
|
295
|
+
badge.textContent = isPinned ? 'pinned version' : 'auto-updating';
|
|
296
|
+
});
|
|
297
|
+
document.querySelectorAll('.setup-method-desc').forEach((desc) => {
|
|
298
|
+
if (isPinned) {
|
|
299
|
+
desc.textContent = `Installs DollhouseMCP v${pinnedVersion}. This version will not auto-update.`;
|
|
300
|
+
} else {
|
|
301
|
+
// Restore original text based on which panel it's in
|
|
302
|
+
const panel = desc.closest('.setup-panel');
|
|
303
|
+
if (panel?.id === 'setup-panel-claude-desktop') {
|
|
304
|
+
desc.innerHTML = 'Pulls the latest version of DollhouseMCP on every startup. Uses <code>npx @latest</code> under the hood. Restart Claude Desktop after.';
|
|
305
|
+
} else if (panel?.id === 'setup-panel-claude-code') {
|
|
306
|
+
desc.textContent = 'Adds DollhouseMCP to Claude Code, pulling the latest version on every startup.';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// ── Install buttons ────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/** Handle Install Now button click */
|
|
315
|
+
const handleInstallClick = async (btn) => {
|
|
316
|
+
const client = btn.dataset.installClient;
|
|
317
|
+
if (!client) return;
|
|
318
|
+
|
|
319
|
+
const status = document.querySelector(`[data-install-status="${client}"]`);
|
|
320
|
+
const originalText = btn.textContent;
|
|
321
|
+
|
|
322
|
+
btn.disabled = true;
|
|
323
|
+
btn.textContent = 'Installing...';
|
|
324
|
+
btn.classList.add('is-loading');
|
|
325
|
+
if (status) {
|
|
326
|
+
status.textContent = '';
|
|
327
|
+
status.className = 'setup-install-status';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const payload = { client };
|
|
332
|
+
if (currentMethod === 'global' && pinnedVersion && pinnedVersion !== 'latest') {
|
|
333
|
+
payload.version = pinnedVersion;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const res = await fetch('/api/setup/install', {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: { 'Content-Type': 'application/json' },
|
|
339
|
+
body: JSON.stringify(payload),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const data = await res.json();
|
|
343
|
+
|
|
344
|
+
if (!data.success) throw new Error(data.error || 'Installation failed');
|
|
345
|
+
|
|
346
|
+
btn.textContent = 'Verifying...';
|
|
347
|
+
|
|
348
|
+
// Verify the install by re-detecting — confirms config was written
|
|
349
|
+
await fetchDetection();
|
|
350
|
+
const verified = detectedConfigs[clientToPlatformReverse[client]]?.installed;
|
|
351
|
+
|
|
352
|
+
btn.textContent = 'Installed';
|
|
353
|
+
btn.classList.remove('is-loading');
|
|
354
|
+
btn.classList.add('is-success');
|
|
355
|
+
if (status) {
|
|
356
|
+
status.textContent = verified
|
|
357
|
+
? 'Verified — config written. Restart the application to activate.'
|
|
358
|
+
: 'Restart the application to activate.';
|
|
359
|
+
status.classList.add('is-success');
|
|
360
|
+
}
|
|
361
|
+
} catch (err) {
|
|
362
|
+
btn.textContent = originalText;
|
|
363
|
+
btn.disabled = false;
|
|
364
|
+
btn.classList.remove('is-loading');
|
|
365
|
+
if (status) {
|
|
366
|
+
status.textContent = err.message || 'Installation failed. Try the manual config below.';
|
|
367
|
+
status.classList.add('is-error');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const initInstallButtons = () => {
|
|
373
|
+
document.querySelectorAll('.setup-install-btn').forEach((btn) => {
|
|
374
|
+
btn.addEventListener('click', () => handleInstallClick(btn));
|
|
375
|
+
// Link button to its status message for accessibility
|
|
376
|
+
const client = btn.dataset.installClient;
|
|
377
|
+
const status = document.querySelector(`[data-install-status="${client}"]`);
|
|
378
|
+
if (status) {
|
|
379
|
+
const statusId = `install-status-${client}`;
|
|
380
|
+
status.id = statusId;
|
|
381
|
+
btn.setAttribute('aria-describedby', statusId);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// ── Open config file buttons ───────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
/** Handle Open config file button click */
|
|
389
|
+
const handleOpenClick = async (btn) => {
|
|
390
|
+
const client = btn.dataset.openClient;
|
|
391
|
+
if (!client) return;
|
|
392
|
+
|
|
393
|
+
const originalText = btn.textContent;
|
|
394
|
+
btn.disabled = true;
|
|
395
|
+
btn.textContent = 'Opening...';
|
|
396
|
+
|
|
397
|
+
const resetBtn = () => {
|
|
398
|
+
btn.textContent = originalText;
|
|
399
|
+
btn.disabled = false;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const res = await fetch('/api/setup/open-config', {
|
|
404
|
+
method: 'POST',
|
|
405
|
+
headers: { 'Content-Type': 'application/json' },
|
|
406
|
+
body: JSON.stringify({ client }),
|
|
407
|
+
});
|
|
408
|
+
const data = await res.json();
|
|
409
|
+
if (!data.success) throw new Error(data.error || 'Could not open file');
|
|
410
|
+
|
|
411
|
+
btn.textContent = 'Opened';
|
|
412
|
+
setTimeout(resetBtn, 2000);
|
|
413
|
+
} catch {
|
|
414
|
+
btn.textContent = 'Failed';
|
|
415
|
+
setTimeout(resetBtn, 2000);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const initOpenButtons = () => {
|
|
420
|
+
document.querySelectorAll('.setup-open-btn').forEach((btn) => {
|
|
421
|
+
btn.addEventListener('click', () => handleOpenClick(btn));
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// ── Fetch version and update pinned configs ────────────────────────────
|
|
426
|
+
|
|
427
|
+
const fetchVersion = async () => {
|
|
428
|
+
try {
|
|
429
|
+
const res = await fetch('/api/setup/version');
|
|
430
|
+
if (!res.ok) return;
|
|
431
|
+
const data = await res.json();
|
|
432
|
+
|
|
433
|
+
// Use latest from GitHub if available, otherwise running version
|
|
434
|
+
pinnedVersion = data.latest?.version || data.running?.version || pinnedVersion;
|
|
435
|
+
if (pinnedVersion === 'latest') return;
|
|
436
|
+
|
|
437
|
+
// Rebuild configs with real version
|
|
438
|
+
configs = buildConfigs(pinnedVersion);
|
|
439
|
+
|
|
440
|
+
// Update prereq section
|
|
441
|
+
const versionLabel = document.getElementById('pinned-version-label');
|
|
442
|
+
if (versionLabel) versionLabel.textContent = `v${pinnedVersion}`;
|
|
443
|
+
|
|
444
|
+
// Update global install command
|
|
445
|
+
const globalCmd = document.getElementById('pinned-global-cmd');
|
|
446
|
+
const globalCopy = document.getElementById('pinned-global-copy');
|
|
447
|
+
if (globalCmd) globalCmd.textContent = `npm install -g ${PKG}@${pinnedVersion}`;
|
|
448
|
+
if (globalCopy) globalCopy.dataset.copyText = `npm install -g ${PKG}@${pinnedVersion}`;
|
|
449
|
+
|
|
450
|
+
// Update local install command
|
|
451
|
+
const localCmd = document.getElementById('pinned-local-cmd');
|
|
452
|
+
const localCopy = document.getElementById('pinned-local-copy');
|
|
453
|
+
if (localCmd) localCmd.textContent = `mkdir -p ~/mcp-servers && cd ~/mcp-servers\nnpm install ${PKG}@${pinnedVersion}`;
|
|
454
|
+
if (localCopy) localCopy.dataset.copyText = `mkdir -p ~/mcp-servers && cd ~/mcp-servers && npm install ${PKG}@${pinnedVersion}`;
|
|
455
|
+
|
|
456
|
+
// Update .mcpb download button version label
|
|
457
|
+
const mcpbVersion = document.getElementById('pinned-mcpb-version');
|
|
458
|
+
if (mcpbVersion) mcpbVersion.textContent = `(v${pinnedVersion})`;
|
|
459
|
+
|
|
460
|
+
// If currently showing pinned method, refresh all config snippets
|
|
461
|
+
if (currentMethod === 'global') {
|
|
462
|
+
updateAllConfigs('global');
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
// Offline or no API — keep defaults
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// ── Detect existing installations ──────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
// Map from detect API client IDs to platform panel IDs (and reverse for install verification)
|
|
472
|
+
const clientToPlatform = {
|
|
473
|
+
'claude': 'claude-desktop',
|
|
474
|
+
'claude-code': 'claude-code',
|
|
475
|
+
'cursor': 'cursor',
|
|
476
|
+
'windsurf': 'windsurf',
|
|
477
|
+
'lmstudio': 'lmstudio',
|
|
478
|
+
'gemini-cli': 'gemini',
|
|
479
|
+
'codex': 'codex',
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// Reverse map: installClient ID → platform panel ID (for install verification)
|
|
483
|
+
const clientToPlatformReverse = {};
|
|
484
|
+
for (const [detectId, platformId] of Object.entries(clientToPlatform)) {
|
|
485
|
+
// Also map installClient IDs that differ from detect IDs
|
|
486
|
+
clientToPlatformReverse[detectId] = platformId;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Stored detection results — keyed by platform panel ID
|
|
490
|
+
let detectedConfigs = {};
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Compare the detected config against what the current method would install.
|
|
494
|
+
* Returns true if command + args match (ignoring env vars and extra keys).
|
|
495
|
+
*/
|
|
496
|
+
const configsMatch = (platformId, method) => {
|
|
497
|
+
const detected = detectedConfigs[platformId];
|
|
498
|
+
if (!detected?.installed || !detected?.currentConfig) return false;
|
|
499
|
+
|
|
500
|
+
const current = detected.currentConfig;
|
|
501
|
+
|
|
502
|
+
// Get the generated config for this platform + method
|
|
503
|
+
const platformConfigs = configs[platformId];
|
|
504
|
+
if (!platformConfigs) return false;
|
|
505
|
+
|
|
506
|
+
const generated = platformConfigs[method];
|
|
507
|
+
if (!generated || generated.isTerminal) {
|
|
508
|
+
// For terminal-command platforms, compare via the JSON config instead
|
|
509
|
+
const jsonKey = method + 'Json';
|
|
510
|
+
const jsonConfig = platformConfigs[jsonKey];
|
|
511
|
+
if (!jsonConfig) return false;
|
|
512
|
+
return compareJsonConfig(current, jsonConfig);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return compareJsonConfig(current, generated);
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
/** Compare a detected config object against a generated config block.
|
|
519
|
+
* Matches on command + package reference. Ignores flags like -y and
|
|
520
|
+
* extra keys like env, type, etc. — those don't change the server version. */
|
|
521
|
+
const compareJsonConfig = (current, generated) => {
|
|
522
|
+
try {
|
|
523
|
+
const genText = generated.copyText || generated.code;
|
|
524
|
+
const genParsed = JSON.parse(genText);
|
|
525
|
+
const genServer = genParsed.mcpServers?.dollhousemcp || genParsed.servers?.dollhousemcp;
|
|
526
|
+
if (!genServer) return false;
|
|
527
|
+
|
|
528
|
+
// Command must match
|
|
529
|
+
if (current.command !== genServer.command) return false;
|
|
530
|
+
|
|
531
|
+
// Extract the package reference from args (the @dollhousemcp/... part)
|
|
532
|
+
const getPkgRef = (args) => (args || []).find(a => a.includes('@dollhousemcp/'));
|
|
533
|
+
const currentPkg = getPkgRef(current.args);
|
|
534
|
+
const genPkg = getPkgRef(genServer.args);
|
|
535
|
+
|
|
536
|
+
return currentPkg === genPkg;
|
|
537
|
+
} catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Update all detection notices and button states based on current method.
|
|
544
|
+
* Called on init and whenever the method toggle changes.
|
|
545
|
+
*/
|
|
546
|
+
const updateDetectionState = () => {
|
|
547
|
+
for (const platformId of Object.values(clientToPlatform)) {
|
|
548
|
+
updatePlatformDetectionState(platformId);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
/** Update notice, badge, and button for a single platform based on detection match */
|
|
553
|
+
const updatePlatformDetectionState = (platformId) => {
|
|
554
|
+
const detected = detectedConfigs[platformId];
|
|
555
|
+
if (!detected?.installed) return;
|
|
556
|
+
|
|
557
|
+
const matches = configsMatch(platformId, currentMethod);
|
|
558
|
+
const panel = document.getElementById('setup-panel-' + platformId);
|
|
559
|
+
const tabBtn = document.getElementById('setup-tab-' + platformId);
|
|
560
|
+
|
|
561
|
+
updateDetectionNotice(panel?.querySelector('.setup-installed-notice'), matches);
|
|
562
|
+
updateDetectionBadge(tabBtn?.querySelector('.setup-tab-badge'), matches);
|
|
563
|
+
updateDetectionButton(panel?.querySelector('.setup-install-btn'), matches);
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const updateDetectionNotice = (notice, matches) => {
|
|
567
|
+
if (!notice) return;
|
|
568
|
+
notice.className = matches ? 'setup-installed-notice is-match' : 'setup-installed-notice';
|
|
569
|
+
const strong = notice.querySelector('strong');
|
|
570
|
+
const msg = notice.querySelector('.setup-notice-msg');
|
|
571
|
+
if (strong) strong.textContent = matches
|
|
572
|
+
? 'DollhouseMCP is configured and matches these settings.'
|
|
573
|
+
: 'DollhouseMCP is already configured for this client.';
|
|
574
|
+
if (msg) msg.textContent = matches
|
|
575
|
+
? 'No changes would be made.'
|
|
576
|
+
: 'Installing will overwrite the existing configuration.';
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
const updateDetectionBadge = (badge, matches) => {
|
|
580
|
+
if (!badge) return;
|
|
581
|
+
badge.className = matches ? 'setup-tab-badge is-match' : 'setup-tab-badge';
|
|
582
|
+
badge.textContent = matches ? 'configured' : 'installed';
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const updateDetectionButton = (installBtn, matches) => {
|
|
586
|
+
if (!installBtn || installBtn.classList.contains('is-success')) return;
|
|
587
|
+
if (matches) {
|
|
588
|
+
installBtn.textContent = 'Already configured';
|
|
589
|
+
installBtn.disabled = true;
|
|
590
|
+
installBtn.classList.add('is-match');
|
|
591
|
+
} else {
|
|
592
|
+
const isPinned = currentMethod === 'global' && pinnedVersion && pinnedVersion !== 'latest';
|
|
593
|
+
installBtn.textContent = isPinned ? `Install v${pinnedVersion}` : 'Install Now';
|
|
594
|
+
installBtn.disabled = false;
|
|
595
|
+
installBtn.classList.remove('is-match');
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
/** Create a badge element for an installed platform tab */
|
|
600
|
+
const createTabBadge = (tabBtn) => {
|
|
601
|
+
const badge = document.createElement('span');
|
|
602
|
+
badge.className = 'setup-tab-badge';
|
|
603
|
+
badge.textContent = 'installed';
|
|
604
|
+
badge.title = 'DollhouseMCP is already configured for this client';
|
|
605
|
+
tabBtn.appendChild(badge);
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
/** Create a notice element for an installed platform panel */
|
|
609
|
+
const createPanelNotice = (panel, currentConfig) => {
|
|
610
|
+
const notice = document.createElement('div');
|
|
611
|
+
notice.className = 'setup-installed-notice';
|
|
612
|
+
let html = '<strong>DollhouseMCP is already configured for this client.</strong> ';
|
|
613
|
+
html += '<span class="setup-notice-msg">Installing will overwrite the existing configuration.</span>';
|
|
614
|
+
if (currentConfig) {
|
|
615
|
+
const configStr = JSON.stringify(currentConfig, null, 2);
|
|
616
|
+
html += `<details><summary>Current config</summary><pre><code>${escapeHtml(configStr)}</code></pre></details>`;
|
|
617
|
+
}
|
|
618
|
+
notice.innerHTML = html;
|
|
619
|
+
panel.insertBefore(notice, panel.firstChild);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/** Process detection results for a single client */
|
|
623
|
+
const applyDetectionResult = (clientId, info) => {
|
|
624
|
+
const platformId = clientToPlatform[clientId];
|
|
625
|
+
if (!platformId || !info) return;
|
|
626
|
+
|
|
627
|
+
detectedConfigs[platformId] = info;
|
|
628
|
+
if (!info.installed) return;
|
|
629
|
+
|
|
630
|
+
const tabBtn = document.getElementById('setup-tab-' + platformId);
|
|
631
|
+
if (tabBtn && !tabBtn.querySelector('.setup-tab-badge')) createTabBadge(tabBtn);
|
|
632
|
+
|
|
633
|
+
const panel = document.getElementById('setup-panel-' + platformId);
|
|
634
|
+
if (panel && !panel.querySelector('.setup-installed-notice')) createPanelNotice(panel, info.currentConfig);
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
/** Fetch detection results from API and update all platform states */
|
|
638
|
+
const fetchDetection = async () => {
|
|
639
|
+
try {
|
|
640
|
+
const res = await fetch('/api/setup/detect');
|
|
641
|
+
if (!res.ok) return;
|
|
642
|
+
const data = await res.json();
|
|
643
|
+
for (const [clientId, info] of Object.entries(data)) {
|
|
644
|
+
applyDetectionResult(clientId, info);
|
|
645
|
+
}
|
|
646
|
+
updateDetectionState();
|
|
647
|
+
} catch {
|
|
648
|
+
// Offline or no API — skip detection
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const escapeHtml = (str) => str
|
|
653
|
+
.replaceAll('&', '&')
|
|
654
|
+
.replaceAll('<', '<')
|
|
655
|
+
.replaceAll('>', '>')
|
|
656
|
+
.replaceAll('"', '"');
|
|
657
|
+
|
|
658
|
+
// ── Generate platform panels from registry ─────────────────────────────
|
|
659
|
+
|
|
660
|
+
/** Build an Open config file button string, or empty if no openClient */
|
|
661
|
+
const openBtnHtml = (openClient) =>
|
|
662
|
+
openClient ? ` <button class="setup-open-btn" type="button" data-open-client="${openClient}">Open config file</button>` : '';
|
|
663
|
+
|
|
664
|
+
/** Build the Install Now + CLI terminal command section */
|
|
665
|
+
const renderInstallSection = (p) => {
|
|
666
|
+
let html = '';
|
|
667
|
+
if (p.installClient) {
|
|
668
|
+
html += '<div class="setup-method setup-method-primary">';
|
|
669
|
+
html += `<div class="setup-install-row"><button class="setup-btn setup-btn-primary setup-install-btn" type="button" data-install-client="${p.installClient}">Install Now</button>`;
|
|
670
|
+
html += `<span class="setup-install-status" data-install-status="${p.installClient}"></span></div>`;
|
|
671
|
+
}
|
|
672
|
+
if (p.cli) {
|
|
673
|
+
const cmd = `${p.cli} mcp add dollhousemcp -- npx -y ${PKG}@latest`;
|
|
674
|
+
if (!p.installClient) html += '<div class="setup-method setup-method-primary">';
|
|
675
|
+
html += '<h3>Or run in your terminal</h3><p>Run this in your terminal:</p>';
|
|
676
|
+
html += `<div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text="${cmd}" aria-label="Copy command">Copy</button>`;
|
|
677
|
+
html += `<pre><code>${cmd}</code></pre></div>`;
|
|
678
|
+
}
|
|
679
|
+
return html;
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
/** Build the JSON config block section */
|
|
683
|
+
const renderJsonSection = (p, hasPrimaryBlock) => {
|
|
684
|
+
if (!p.configPath) return hasPrimaryBlock ? '</div>' : '';
|
|
685
|
+
|
|
686
|
+
const config = configs[p.id]?.npx;
|
|
687
|
+
const configCode = config?.code || '';
|
|
688
|
+
const copyText = config?.copyText || configCode;
|
|
689
|
+
let html = '';
|
|
690
|
+
|
|
691
|
+
if (hasPrimaryBlock) {
|
|
692
|
+
html += `</div><div class="setup-method"><h3>Or add config manually${openBtnHtml(p.openClient)}</h3>`;
|
|
693
|
+
} else {
|
|
694
|
+
html += `<div class="setup-method setup-method-primary"><h3>Config${openBtnHtml(p.openClient)}</h3>`;
|
|
695
|
+
}
|
|
696
|
+
html += `<p>Add to ${p.configPath}:</p>`;
|
|
697
|
+
html += `<div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${copyText}' aria-label="Copy config">Copy</button>`;
|
|
698
|
+
html += `<pre><code>${configCode}</code></pre></div>`;
|
|
699
|
+
if (p.hint) html += `<p class="setup-hint">${p.hint}</p>`;
|
|
700
|
+
html += '</div>';
|
|
701
|
+
return html;
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
/** Build the TOML config block section (Codex) */
|
|
705
|
+
const renderTomlSection = (p) => {
|
|
706
|
+
if (!p.tomlPath) return '';
|
|
707
|
+
const tomlConfig = configs[p.id]?.npxToml;
|
|
708
|
+
const tomlCode = tomlConfig?.code || '';
|
|
709
|
+
let html = `<div class="setup-method"><h3>Or add to config${openBtnHtml(p.openClient)}</h3>`;
|
|
710
|
+
html += `<p>Add to ${p.tomlPath}:</p>`;
|
|
711
|
+
html += `<div class="setup-code-block"><button class="setup-copy-btn" type="button" data-copy-text='${tomlCode}' aria-label="Copy config">Copy</button>`;
|
|
712
|
+
html += `<pre><code>${tomlCode}</code></pre></div></div>`;
|
|
713
|
+
return html;
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const renderGeneratedPanels = () => {
|
|
717
|
+
const container = document.getElementById('setup-generated-panels');
|
|
718
|
+
if (!container) return;
|
|
719
|
+
|
|
720
|
+
for (const p of PLATFORMS) {
|
|
721
|
+
if (!p.configPath && !p.tomlPath) continue;
|
|
722
|
+
|
|
723
|
+
const section = document.createElement('section');
|
|
724
|
+
section.className = 'setup-panel';
|
|
725
|
+
section.setAttribute('role', 'tabpanel');
|
|
726
|
+
section.id = 'setup-panel-' + p.id;
|
|
727
|
+
section.setAttribute('aria-labelledby', 'setup-tab-' + p.id);
|
|
728
|
+
section.hidden = true;
|
|
729
|
+
|
|
730
|
+
const hasPrimaryBlock = !!(p.installClient || p.cli);
|
|
731
|
+
section.innerHTML =
|
|
732
|
+
renderInstallSection(p) +
|
|
733
|
+
renderJsonSection(p, hasPrimaryBlock) +
|
|
734
|
+
renderTomlSection(p);
|
|
735
|
+
|
|
736
|
+
container.appendChild(section);
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// ── Init ──────────────────────────────────────────────────────────────
|
|
741
|
+
|
|
742
|
+
const os = detectOS();
|
|
743
|
+
renderGeneratedPanels();
|
|
744
|
+
highlightOSPaths(os);
|
|
745
|
+
initMethodToggle();
|
|
746
|
+
initPlatformTabs();
|
|
747
|
+
initCopyButtons();
|
|
748
|
+
initInstallButtons();
|
|
749
|
+
initOpenButtons();
|
|
750
|
+
fetchVersion();
|
|
751
|
+
fetchDetection();
|
|
752
|
+
})();
|