@wonderwhy-er/desktop-commander 0.2.8 → 0.2.9
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/README.md +238 -38
- package/dist/server.js +3 -0
- package/dist/setup-claude-server.js +11 -1
- package/dist/terminal-manager.js +9 -3
- package/dist/tools/config.js +1 -0
- package/dist/tools/improved-process-tools.js +12 -1
- package/dist/tools/schemas.d.ts +1 -1
- package/dist/uninstall-claude-server.js +753 -0
- package/dist/utils/dockerPrompt.d.ts +17 -0
- package/dist/utils/dockerPrompt.js +53 -0
- package/dist/utils/system-info.d.ts +18 -0
- package/dist/utils/system-info.js +232 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -4
- package/dist/index-dxt.d.ts +0 -2
- package/dist/index-dxt.js +0 -39
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import { homedir, platform } from 'os';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync, mkdirSync } from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
import { exec } from "node:child_process";
|
|
9
|
+
import { version as nodeVersion } from 'process';
|
|
10
|
+
import * as https from 'https';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
12
|
+
|
|
13
|
+
// Google Analytics configuration
|
|
14
|
+
const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L'; // Replace with your GA4 Measurement ID
|
|
15
|
+
const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A'; // Replace with your GA4 API Secret
|
|
16
|
+
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
|
|
17
|
+
|
|
18
|
+
// Read clientId and telemetry settings from existing config
|
|
19
|
+
let uniqueUserId = 'unknown';
|
|
20
|
+
let telemetryEnabled = false; // Default to disabled for privacy
|
|
21
|
+
|
|
22
|
+
async function getConfigSettings() {
|
|
23
|
+
try {
|
|
24
|
+
const USER_HOME = homedir();
|
|
25
|
+
const CONFIG_DIR = path.join(USER_HOME, '.claude-server-commander');
|
|
26
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
27
|
+
|
|
28
|
+
if (existsSync(CONFIG_FILE)) {
|
|
29
|
+
const configData = readFileSync(CONFIG_FILE, 'utf8');
|
|
30
|
+
const config = JSON.parse(configData);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
clientId: config.clientId || randomUUID(),
|
|
34
|
+
telemetryEnabled: config.telemetryEnabled === true // Explicit check for true
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback: generate new ID and default telemetry to false if config doesn't exist
|
|
39
|
+
return {
|
|
40
|
+
clientId: `unknown-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`,
|
|
41
|
+
telemetryEnabled: false
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Final fallback
|
|
45
|
+
return {
|
|
46
|
+
clientId: `random-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`,
|
|
47
|
+
telemetryEnabled: false
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Uninstall tracking
|
|
53
|
+
let uninstallSteps = [];
|
|
54
|
+
let uninstallStartTime = Date.now();
|
|
55
|
+
|
|
56
|
+
// Fix for Windows ESM path resolution
|
|
57
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
58
|
+
const __dirname = dirname(__filename);
|
|
59
|
+
|
|
60
|
+
// Setup logging
|
|
61
|
+
const LOG_FILE = join(__dirname, 'setup.log');
|
|
62
|
+
|
|
63
|
+
function logToFile(message, isError = false) {
|
|
64
|
+
const timestamp = new Date().toISOString();
|
|
65
|
+
const logMessage = `${timestamp} - ${isError ? 'ERROR: ' : ''}${message}\n`;
|
|
66
|
+
try {
|
|
67
|
+
appendFileSync(LOG_FILE, logMessage);
|
|
68
|
+
const jsonOutput = {
|
|
69
|
+
type: isError ? 'error' : 'info',
|
|
70
|
+
timestamp,
|
|
71
|
+
message
|
|
72
|
+
};
|
|
73
|
+
process.stdout.write(`${message}\n`);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
process.stderr.write(`${JSON.stringify({
|
|
76
|
+
type: 'error',
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
message: `Failed to write to log file: ${err.message}`
|
|
79
|
+
})}\n`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Function to get npm version
|
|
84
|
+
async function getNpmVersion() {
|
|
85
|
+
try {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
exec('npm --version', (error, stdout, stderr) => {
|
|
88
|
+
if (error) {
|
|
89
|
+
resolve('unknown');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
resolve(stdout.trim());
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return 'unknown';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Get Desktop Commander version
|
|
101
|
+
const getVersion = async () => {
|
|
102
|
+
try {
|
|
103
|
+
if (process.env.npm_package_version) {
|
|
104
|
+
return process.env.npm_package_version;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const versionPath = join(__dirname, 'version.js');
|
|
108
|
+
if (existsSync(versionPath)) {
|
|
109
|
+
const { VERSION } = await import(versionPath);
|
|
110
|
+
return VERSION;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const packageJsonPath = join(__dirname, 'package.json');
|
|
114
|
+
if (existsSync(packageJsonPath)) {
|
|
115
|
+
const packageJsonContent = readFileSync(packageJsonPath, 'utf8');
|
|
116
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
117
|
+
if (packageJson.version) {
|
|
118
|
+
return packageJson.version;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return 'unknown';
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return 'unknown';
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
// Function to detect shell environment
|
|
128
|
+
function detectShell() {
|
|
129
|
+
if (process.platform === 'win32') {
|
|
130
|
+
if (process.env.TERM_PROGRAM === 'vscode') return 'vscode-terminal';
|
|
131
|
+
if (process.env.WT_SESSION) return 'windows-terminal';
|
|
132
|
+
if (process.env.SHELL?.includes('bash')) return 'git-bash';
|
|
133
|
+
if (process.env.TERM?.includes('xterm')) return 'xterm-on-windows';
|
|
134
|
+
if (process.env.ComSpec?.toLowerCase().includes('powershell')) return 'powershell';
|
|
135
|
+
if (process.env.PROMPT) return 'cmd';
|
|
136
|
+
|
|
137
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
|
|
138
|
+
return `wsl-${process.env.WSL_DISTRO_NAME || 'unknown'}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return 'windows-unknown';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (process.env.SHELL) {
|
|
145
|
+
const shellPath = process.env.SHELL.toLowerCase();
|
|
146
|
+
if (shellPath.includes('bash')) return 'bash';
|
|
147
|
+
if (shellPath.includes('zsh')) return 'zsh';
|
|
148
|
+
if (shellPath.includes('fish')) return 'fish';
|
|
149
|
+
if (shellPath.includes('ksh')) return 'ksh';
|
|
150
|
+
if (shellPath.includes('csh')) return 'csh';
|
|
151
|
+
if (shellPath.includes('dash')) return 'dash';
|
|
152
|
+
return `other-unix-${shellPath.split('/').pop()}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (process.env.TERM_PROGRAM) {
|
|
156
|
+
return process.env.TERM_PROGRAM.toLowerCase();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return 'unknown-shell';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Function to determine execution context
|
|
163
|
+
function getExecutionContext() {
|
|
164
|
+
const isNpx = process.env.npm_lifecycle_event === 'npx' ||
|
|
165
|
+
process.env.npm_execpath?.includes('npx') ||
|
|
166
|
+
process.env._?.includes('npx') ||
|
|
167
|
+
import.meta.url.includes('node_modules');
|
|
168
|
+
|
|
169
|
+
const isGlobal = process.env.npm_config_global === 'true' ||
|
|
170
|
+
process.argv[1]?.includes('node_modules/.bin');
|
|
171
|
+
|
|
172
|
+
const isNpmScript = !!process.env.npm_lifecycle_script;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
runMethod: isNpx ? 'npx' : (isGlobal ? 'global' : (isNpmScript ? 'npm_script' : 'direct')),
|
|
176
|
+
isCI: !!process.env.CI || !!process.env.GITHUB_ACTIONS || !!process.env.TRAVIS || !!process.env.CIRCLECI,
|
|
177
|
+
shell: detectShell()
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// Enhanced tracking properties
|
|
181
|
+
let npmVersionCache = null;
|
|
182
|
+
|
|
183
|
+
async function getTrackingProperties(additionalProps = {}) {
|
|
184
|
+
const propertiesStep = addUninstallStep('get_tracking_properties');
|
|
185
|
+
try {
|
|
186
|
+
if (npmVersionCache === null) {
|
|
187
|
+
npmVersionCache = await getNpmVersion();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const context = getExecutionContext();
|
|
191
|
+
const version = await getVersion();
|
|
192
|
+
|
|
193
|
+
updateUninstallStep(propertiesStep, 'completed');
|
|
194
|
+
return {
|
|
195
|
+
platform: platform(),
|
|
196
|
+
node_version: nodeVersion,
|
|
197
|
+
npm_version: npmVersionCache,
|
|
198
|
+
execution_context: context.runMethod,
|
|
199
|
+
is_ci: context.isCI,
|
|
200
|
+
shell: context.shell,
|
|
201
|
+
app_version: version,
|
|
202
|
+
engagement_time_msec: "100",
|
|
203
|
+
...additionalProps
|
|
204
|
+
};
|
|
205
|
+
} catch (error) {
|
|
206
|
+
updateUninstallStep(propertiesStep, 'failed', error);
|
|
207
|
+
return {
|
|
208
|
+
platform: platform(),
|
|
209
|
+
node_version: nodeVersion,
|
|
210
|
+
error: error.message,
|
|
211
|
+
...additionalProps
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Enhanced tracking function with retries
|
|
216
|
+
async function trackEvent(eventName, additionalProps = {}) {
|
|
217
|
+
const trackingStep = addUninstallStep(`track_event_${eventName}`);
|
|
218
|
+
|
|
219
|
+
// Check if telemetry is disabled
|
|
220
|
+
if (!telemetryEnabled) {
|
|
221
|
+
updateUninstallStep(trackingStep, 'skipped_telemetry_disabled');
|
|
222
|
+
return true; // Return success since this is expected behavior
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!GA_MEASUREMENT_ID || !GA_API_SECRET) {
|
|
226
|
+
updateUninstallStep(trackingStep, 'skipped', new Error('GA not configured'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const maxRetries = 2;
|
|
231
|
+
let attempt = 0;
|
|
232
|
+
let lastError = null;
|
|
233
|
+
|
|
234
|
+
while (attempt <= maxRetries) {
|
|
235
|
+
try {
|
|
236
|
+
attempt++;
|
|
237
|
+
|
|
238
|
+
const eventProperties = await getTrackingProperties(additionalProps);
|
|
239
|
+
|
|
240
|
+
const payload = {
|
|
241
|
+
client_id: uniqueUserId,
|
|
242
|
+
non_personalized_ads: false,
|
|
243
|
+
timestamp_micros: Date.now() * 1000,
|
|
244
|
+
events: [{
|
|
245
|
+
name: eventName,
|
|
246
|
+
params: eventProperties
|
|
247
|
+
}]
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const postData = JSON.stringify(payload);
|
|
251
|
+
|
|
252
|
+
const options = {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: {
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = await new Promise((resolve, reject) => {
|
|
261
|
+
const req = https.request(GA_BASE_URL, options);
|
|
262
|
+
|
|
263
|
+
const timeoutId = setTimeout(() => {
|
|
264
|
+
req.destroy();
|
|
265
|
+
reject(new Error('Request timeout'));
|
|
266
|
+
}, 5000);
|
|
267
|
+
|
|
268
|
+
req.on('error', (error) => {
|
|
269
|
+
clearTimeout(timeoutId);
|
|
270
|
+
reject(error);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
req.on('response', (res) => {
|
|
274
|
+
clearTimeout(timeoutId);
|
|
275
|
+
let data = '';
|
|
276
|
+
|
|
277
|
+
res.on('data', (chunk) => {
|
|
278
|
+
data += chunk;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
res.on('error', (error) => {
|
|
282
|
+
reject(error);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
res.on('end', () => {
|
|
286
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
287
|
+
resolve({ success: true, data });
|
|
288
|
+
} else {
|
|
289
|
+
reject(new Error(`HTTP error ${res.statusCode}: ${data}`));
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
req.write(postData);
|
|
295
|
+
req.end();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
updateUninstallStep(trackingStep, 'completed');
|
|
299
|
+
return result;
|
|
300
|
+
|
|
301
|
+
} catch (error) {
|
|
302
|
+
lastError = error;
|
|
303
|
+
if (attempt <= maxRetries) {
|
|
304
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
updateUninstallStep(trackingStep, 'failed', lastError);
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
// Ensure tracking completes before process exits
|
|
313
|
+
async function ensureTrackingCompleted(eventName, additionalProps = {}, timeoutMs = 6000) {
|
|
314
|
+
return new Promise(async (resolve) => {
|
|
315
|
+
const timeoutId = setTimeout(() => {
|
|
316
|
+
resolve(false);
|
|
317
|
+
}, timeoutMs);
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
await trackEvent(eventName, additionalProps);
|
|
321
|
+
clearTimeout(timeoutId);
|
|
322
|
+
resolve(true);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
clearTimeout(timeoutId);
|
|
325
|
+
resolve(false);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Setup global error handlers (will be initialized after config is loaded)
|
|
331
|
+
let errorHandlersInitialized = false;
|
|
332
|
+
|
|
333
|
+
function initializeErrorHandlers() {
|
|
334
|
+
if (errorHandlersInitialized) return;
|
|
335
|
+
|
|
336
|
+
process.on('uncaughtException', async (error) => {
|
|
337
|
+
if (telemetryEnabled) {
|
|
338
|
+
await trackEvent('uninstall_uncaught_exception', { error: error.message });
|
|
339
|
+
}
|
|
340
|
+
setTimeout(() => {
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}, 1000);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
346
|
+
if (telemetryEnabled) {
|
|
347
|
+
await trackEvent('uninstall_unhandled_rejection', { error: String(reason) });
|
|
348
|
+
}
|
|
349
|
+
setTimeout(() => {
|
|
350
|
+
process.exit(1);
|
|
351
|
+
}, 1000);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
errorHandlersInitialized = true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Track when the process is about to exit
|
|
358
|
+
let isExiting = false;
|
|
359
|
+
process.on('exit', () => {
|
|
360
|
+
if (!isExiting) {
|
|
361
|
+
isExiting = true;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Determine OS and set appropriate config path
|
|
366
|
+
const os = platform();
|
|
367
|
+
const isWindows = os === 'win32';
|
|
368
|
+
let claudeConfigPath;
|
|
369
|
+
|
|
370
|
+
switch (os) {
|
|
371
|
+
case 'win32':
|
|
372
|
+
claudeConfigPath = join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json');
|
|
373
|
+
break;
|
|
374
|
+
case 'darwin':
|
|
375
|
+
claudeConfigPath = join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
376
|
+
break;
|
|
377
|
+
case 'linux':
|
|
378
|
+
claudeConfigPath = join(homedir(), '.config', 'Claude', 'claude_desktop_config.json');
|
|
379
|
+
break;
|
|
380
|
+
default:
|
|
381
|
+
claudeConfigPath = join(homedir(), '.claude_desktop_config.json');
|
|
382
|
+
}
|
|
383
|
+
// Step tracking functions
|
|
384
|
+
function addUninstallStep(step, status = 'started', error = null) {
|
|
385
|
+
const timestamp = Date.now();
|
|
386
|
+
uninstallSteps.push({
|
|
387
|
+
step,
|
|
388
|
+
status,
|
|
389
|
+
timestamp,
|
|
390
|
+
timeFromStart: timestamp - uninstallStartTime,
|
|
391
|
+
error: error ? error.message || String(error) : null
|
|
392
|
+
});
|
|
393
|
+
return uninstallSteps.length - 1;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function updateUninstallStep(index, status, error = null) {
|
|
397
|
+
if (uninstallSteps[index]) {
|
|
398
|
+
const timestamp = Date.now();
|
|
399
|
+
uninstallSteps[index].status = status;
|
|
400
|
+
uninstallSteps[index].completionTime = timestamp;
|
|
401
|
+
uninstallSteps[index].timeFromStart = timestamp - uninstallStartTime;
|
|
402
|
+
if (error) {
|
|
403
|
+
uninstallSteps[index].error = error.message || String(error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function execAsync(command) {
|
|
409
|
+
const execStep = addUninstallStep(`exec_${command.substring(0, 20)}...`);
|
|
410
|
+
return new Promise((resolve, reject) => {
|
|
411
|
+
const actualCommand = isWindows
|
|
412
|
+
? `cmd.exe /c ${command}`
|
|
413
|
+
: command;
|
|
414
|
+
|
|
415
|
+
exec(actualCommand, { timeout: 10000 }, (error, stdout, stderr) => {
|
|
416
|
+
if (error) {
|
|
417
|
+
updateUninstallStep(execStep, 'failed', error);
|
|
418
|
+
reject(error);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
updateUninstallStep(execStep, 'completed');
|
|
422
|
+
resolve({ stdout, stderr });
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
// Backup configuration before removal
|
|
427
|
+
async function createConfigBackup(configPath) {
|
|
428
|
+
const backupStep = addUninstallStep('create_config_backup');
|
|
429
|
+
try {
|
|
430
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
431
|
+
const backupPath = `${configPath}.backup.${timestamp}`;
|
|
432
|
+
|
|
433
|
+
if (existsSync(configPath)) {
|
|
434
|
+
const configData = readFileSync(configPath, 'utf8');
|
|
435
|
+
writeFileSync(backupPath, configData, 'utf8');
|
|
436
|
+
updateUninstallStep(backupStep, 'completed');
|
|
437
|
+
logToFile(`Configuration backup created: ${backupPath}`);
|
|
438
|
+
await trackEvent('uninstall_backup_created');
|
|
439
|
+
return backupPath;
|
|
440
|
+
} else {
|
|
441
|
+
updateUninstallStep(backupStep, 'no_config_file');
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
} catch (error) {
|
|
445
|
+
updateUninstallStep(backupStep, 'failed', error);
|
|
446
|
+
await trackEvent('uninstall_backup_failed', { error: error.message });
|
|
447
|
+
logToFile(`Failed to create backup: ${error.message}`, true);
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Restore configuration from backup
|
|
453
|
+
async function restoreFromBackup(backupPath) {
|
|
454
|
+
if (!backupPath || !existsSync(backupPath)) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const backupData = readFileSync(backupPath, 'utf8');
|
|
460
|
+
writeFileSync(claudeConfigPath, backupData, 'utf8');
|
|
461
|
+
logToFile(`Configuration restored from backup: ${backupPath}`);
|
|
462
|
+
await trackEvent('uninstall_backup_restored');
|
|
463
|
+
return true;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
logToFile(`Failed to restore from backup: ${error.message}`, true);
|
|
466
|
+
await trackEvent('uninstall_backup_restore_failed', { error: error.message });
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function restartClaude() {
|
|
472
|
+
const restartStep = addUninstallStep('restart_claude');
|
|
473
|
+
try {
|
|
474
|
+
const platform = process.platform;
|
|
475
|
+
logToFile('Attempting to restart Claude...');
|
|
476
|
+
await trackEvent('uninstall_restart_claude_attempt');
|
|
477
|
+
|
|
478
|
+
// Try to kill Claude process first
|
|
479
|
+
const killStep = addUninstallStep('kill_claude_process');
|
|
480
|
+
try {
|
|
481
|
+
switch (platform) {
|
|
482
|
+
case "win32":
|
|
483
|
+
await execAsync(`taskkill /F /IM "Claude.exe"`);
|
|
484
|
+
break;
|
|
485
|
+
case "darwin":
|
|
486
|
+
await execAsync(`killall "Claude"`);
|
|
487
|
+
break;
|
|
488
|
+
case "linux":
|
|
489
|
+
await execAsync(`pkill -f "claude"`);
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
updateUninstallStep(killStep, 'completed');
|
|
493
|
+
logToFile("Claude process terminated successfully");
|
|
494
|
+
await trackEvent('uninstall_kill_claude_success');
|
|
495
|
+
} catch (killError) {
|
|
496
|
+
updateUninstallStep(killStep, 'no_process_found', killError);
|
|
497
|
+
logToFile("Claude process not found or already terminated");
|
|
498
|
+
await trackEvent('uninstall_kill_claude_not_needed');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Wait a bit to ensure process termination
|
|
502
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
503
|
+
|
|
504
|
+
// Try to start Claude
|
|
505
|
+
const startStep = addUninstallStep('start_claude_process');
|
|
506
|
+
try {
|
|
507
|
+
if (platform === "win32") {
|
|
508
|
+
logToFile("Windows: Claude restart skipped - please restart Claude manually");
|
|
509
|
+
updateUninstallStep(startStep, 'skipped');
|
|
510
|
+
await trackEvent('uninstall_start_claude_skipped');
|
|
511
|
+
} else if (platform === "darwin") {
|
|
512
|
+
await execAsync(`open -a "Claude"`);
|
|
513
|
+
updateUninstallStep(startStep, 'completed');
|
|
514
|
+
logToFile("✅ Claude has been restarted automatically!");
|
|
515
|
+
await trackEvent('uninstall_start_claude_success');
|
|
516
|
+
} else if (platform === "linux") {
|
|
517
|
+
await execAsync(`claude`);
|
|
518
|
+
updateUninstallStep(startStep, 'completed');
|
|
519
|
+
logToFile("✅ Claude has been restarted automatically!");
|
|
520
|
+
await trackEvent('uninstall_start_claude_success');
|
|
521
|
+
} else {
|
|
522
|
+
logToFile('To complete uninstallation, restart Claude if it\'s currently running');
|
|
523
|
+
updateUninstallStep(startStep, 'manual_required');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
updateUninstallStep(restartStep, 'completed');
|
|
527
|
+
await trackEvent('uninstall_restart_claude_success');
|
|
528
|
+
} catch (startError) {
|
|
529
|
+
updateUninstallStep(startStep, 'failed', startError);
|
|
530
|
+
await trackEvent('uninstall_start_claude_error', {
|
|
531
|
+
error: startError.message
|
|
532
|
+
});
|
|
533
|
+
logToFile(`Could not automatically restart Claude: ${startError.message}. Please restart it manually.`);
|
|
534
|
+
}
|
|
535
|
+
} catch (error) {
|
|
536
|
+
updateUninstallStep(restartStep, 'failed', error);
|
|
537
|
+
await trackEvent('uninstall_restart_claude_error', { error: error.message });
|
|
538
|
+
logToFile(`Failed to restart Claude: ${error.message}. Please restart it manually.`, true);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async function removeDesktopCommanderConfig() {
|
|
542
|
+
const configStep = addUninstallStep('remove_mcp_config');
|
|
543
|
+
let backupPath = null;
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
// Check if config file exists
|
|
547
|
+
if (!existsSync(claudeConfigPath)) {
|
|
548
|
+
updateUninstallStep(configStep, 'no_config_file');
|
|
549
|
+
logToFile(`Claude config file not found at: ${claudeConfigPath}`);
|
|
550
|
+
logToFile('✅ Desktop Commander was not configured or already removed.');
|
|
551
|
+
await trackEvent('uninstall_config_not_found');
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Create backup before making changes
|
|
556
|
+
backupPath = await createConfigBackup(claudeConfigPath);
|
|
557
|
+
|
|
558
|
+
// Read existing config
|
|
559
|
+
let config;
|
|
560
|
+
const readStep = addUninstallStep('read_config_file');
|
|
561
|
+
try {
|
|
562
|
+
const configData = readFileSync(claudeConfigPath, 'utf8');
|
|
563
|
+
config = JSON.parse(configData);
|
|
564
|
+
updateUninstallStep(readStep, 'completed');
|
|
565
|
+
} catch (readError) {
|
|
566
|
+
updateUninstallStep(readStep, 'failed', readError);
|
|
567
|
+
await trackEvent('uninstall_config_read_error', { error: readError.message });
|
|
568
|
+
throw new Error(`Failed to read config file: ${readError.message}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Check if mcpServers exists
|
|
572
|
+
if (!config.mcpServers) {
|
|
573
|
+
updateUninstallStep(configStep, 'no_mcp_servers');
|
|
574
|
+
logToFile('No MCP servers configured in Claude.');
|
|
575
|
+
logToFile('✅ Desktop Commander was not configured or already removed.');
|
|
576
|
+
await trackEvent('uninstall_no_mcp_servers');
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
// Track what we're removing
|
|
580
|
+
const serversToRemove = [];
|
|
581
|
+
|
|
582
|
+
if (config.mcpServers["desktop-commander"]) {
|
|
583
|
+
serversToRemove.push("desktop-commander");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (serversToRemove.length === 0) {
|
|
587
|
+
updateUninstallStep(configStep, 'not_found');
|
|
588
|
+
logToFile('Desktop Commander MCP server not found in configuration.');
|
|
589
|
+
logToFile('✅ Desktop Commander was not configured or already removed.');
|
|
590
|
+
await trackEvent('uninstall_server_not_found');
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Remove the server configurations
|
|
595
|
+
const removeStep = addUninstallStep('remove_server_configs');
|
|
596
|
+
try {
|
|
597
|
+
serversToRemove.forEach(serverName => {
|
|
598
|
+
delete config.mcpServers[serverName];
|
|
599
|
+
logToFile(`Removed "${serverName}" from Claude configuration`);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
updateUninstallStep(removeStep, 'completed');
|
|
603
|
+
await trackEvent('uninstall_servers_removed');
|
|
604
|
+
} catch (removeError) {
|
|
605
|
+
updateUninstallStep(removeStep, 'failed', removeError);
|
|
606
|
+
await trackEvent('uninstall_servers_remove_error', { error: removeError.message });
|
|
607
|
+
throw new Error(`Failed to remove server configs: ${removeError.message}`);
|
|
608
|
+
} // Write the updated config back
|
|
609
|
+
const writeStep = addUninstallStep('write_updated_config');
|
|
610
|
+
try {
|
|
611
|
+
writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), 'utf8');
|
|
612
|
+
updateUninstallStep(writeStep, 'completed');
|
|
613
|
+
updateUninstallStep(configStep, 'completed');
|
|
614
|
+
logToFile('✅ Desktop Commander successfully removed from Claude configuration');
|
|
615
|
+
logToFile(`Configuration updated at: ${claudeConfigPath}`);
|
|
616
|
+
await trackEvent('uninstall_config_updated');
|
|
617
|
+
} catch (writeError) {
|
|
618
|
+
updateUninstallStep(writeStep, 'failed', writeError);
|
|
619
|
+
await trackEvent('uninstall_config_write_error', { error: writeError.message });
|
|
620
|
+
|
|
621
|
+
// Try to restore from backup
|
|
622
|
+
if (backupPath) {
|
|
623
|
+
logToFile('Attempting to restore configuration from backup...');
|
|
624
|
+
const restored = await restoreFromBackup(backupPath);
|
|
625
|
+
if (restored) {
|
|
626
|
+
throw new Error(`Failed to write updated config, but backup was restored: ${writeError.message}`);
|
|
627
|
+
} else {
|
|
628
|
+
throw new Error(`Failed to write updated config and backup restoration failed: ${writeError.message}`);
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
throw new Error(`Failed to write updated config: ${writeError.message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return true;
|
|
636
|
+
} catch (error) {
|
|
637
|
+
updateUninstallStep(configStep, 'failed', error);
|
|
638
|
+
await trackEvent('uninstall_config_error', { error: error.message });
|
|
639
|
+
logToFile(`Error removing Desktop Commander configuration: ${error.message}`, true);
|
|
640
|
+
|
|
641
|
+
// Try to restore from backup if we have one
|
|
642
|
+
if (backupPath) {
|
|
643
|
+
logToFile('Attempting to restore configuration from backup...');
|
|
644
|
+
await restoreFromBackup(backupPath);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
// Main uninstall function
|
|
651
|
+
export default async function uninstall() {
|
|
652
|
+
// Initialize clientId and telemetry settings from existing config
|
|
653
|
+
const configSettings = await getConfigSettings();
|
|
654
|
+
uniqueUserId = configSettings.clientId;
|
|
655
|
+
telemetryEnabled = configSettings.telemetryEnabled;
|
|
656
|
+
|
|
657
|
+
// Initialize error handlers now that telemetry setting is known
|
|
658
|
+
initializeErrorHandlers();
|
|
659
|
+
|
|
660
|
+
// Log telemetry status for transparency
|
|
661
|
+
if (!telemetryEnabled) {
|
|
662
|
+
logToFile('Telemetry disabled - no analytics will be sent');
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Initial tracking (only if telemetry enabled)
|
|
666
|
+
await ensureTrackingCompleted('uninstall_start');
|
|
667
|
+
|
|
668
|
+
const mainStep = addUninstallStep('main_uninstall');
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
logToFile('Starting Desktop Commander uninstallation...');
|
|
672
|
+
|
|
673
|
+
// Remove the server configuration from Claude
|
|
674
|
+
const configRemoved = await removeDesktopCommanderConfig();
|
|
675
|
+
|
|
676
|
+
if (configRemoved) {
|
|
677
|
+
// Try to restart Claude
|
|
678
|
+
// await restartClaude();
|
|
679
|
+
|
|
680
|
+
updateUninstallStep(mainStep, 'completed');
|
|
681
|
+
|
|
682
|
+
const appVersion = await getVersion();
|
|
683
|
+
logToFile(`\n✅ Desktop Commander has been successfully uninstalled!`);
|
|
684
|
+
logToFile('The MCP server has been removed from Claude\'s configuration.');
|
|
685
|
+
|
|
686
|
+
logToFile('\nIf you want to reinstall later, you can run:');
|
|
687
|
+
logToFile('npx @wonderwhy-er/desktop-commander@latest setup');
|
|
688
|
+
|
|
689
|
+
logToFile('\n🎁 We\'re sorry to see you leaving, we’d love to understand your decision not to use Desktop Commander.')
|
|
690
|
+
logToFile('In return for a brief 30-minute call, we’ll send you a $20 Amazon gift card as a thank-you.');
|
|
691
|
+
logToFile('To get a gift card, please fill out this form:');
|
|
692
|
+
logToFile(' https://tally.so/r/w8lyRo');
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
logToFile('\nThank you for using Desktop Commander! 👋\n');
|
|
696
|
+
|
|
697
|
+
// Send final tracking event
|
|
698
|
+
await ensureTrackingCompleted('uninstall_complete');
|
|
699
|
+
|
|
700
|
+
return true;
|
|
701
|
+
} else {
|
|
702
|
+
updateUninstallStep(mainStep, 'failed_config_removal');
|
|
703
|
+
|
|
704
|
+
logToFile('\n❌ Uninstallation completed with errors.');
|
|
705
|
+
logToFile('You may need to manually remove Desktop Commander from Claude\'s configuration.');
|
|
706
|
+
logToFile(`Configuration file location: ${claudeConfigPath}\n`);
|
|
707
|
+
|
|
708
|
+
logToFile('\n🎁 We\'re sorry to see you leaving, we\'d love to understand your decision not to use Desktop Commander.')
|
|
709
|
+
logToFile('In return for a brief 30-minute call, we\'ll send you a $20 Amazon gift card as a thank-you.');
|
|
710
|
+
logToFile('To get a gift card, please fill out this form:');
|
|
711
|
+
logToFile(' https://tally.so/r/w8lyRo');
|
|
712
|
+
|
|
713
|
+
await ensureTrackingCompleted('uninstall_partial_failure');
|
|
714
|
+
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
} catch (error) {
|
|
718
|
+
updateUninstallStep(mainStep, 'fatal_error', error);
|
|
719
|
+
|
|
720
|
+
await ensureTrackingCompleted('uninstall_fatal_error', {
|
|
721
|
+
error: error.message,
|
|
722
|
+
error_stack: error.stack,
|
|
723
|
+
last_successful_step: uninstallSteps.filter(s => s.status === 'completed').pop()?.step || 'none'
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
logToFile(`Fatal error during uninstallation: ${error.message}`, true);
|
|
727
|
+
logToFile('\n❌ Uninstallation failed.');
|
|
728
|
+
logToFile('You may need to manually remove Desktop Commander from Claude\'s configuration.');
|
|
729
|
+
logToFile(`Configuration file location: ${claudeConfigPath}\n`);
|
|
730
|
+
|
|
731
|
+
logToFile('\n🎁 We\'re sorry to see you leaving, we\'d love to understand your decision not to use Desktop Commander.')
|
|
732
|
+
logToFile('In return for a brief 30-minute call, we\'ll send you a $20 Amazon gift card as a thank-you.');
|
|
733
|
+
logToFile('To get a gift card, please fill out this form:');
|
|
734
|
+
logToFile('https://tally.so/r/w8lyRo');
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Allow direct execution
|
|
740
|
+
if (process.argv.length >= 2 && process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
741
|
+
uninstall().then(success => {
|
|
742
|
+
if (!success) {
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
}).catch(async error => {
|
|
746
|
+
await ensureTrackingCompleted('uninstall_execution_error', {
|
|
747
|
+
error: error.message,
|
|
748
|
+
error_stack: error.stack
|
|
749
|
+
});
|
|
750
|
+
logToFile(`Fatal error: ${error}`, true);
|
|
751
|
+
process.exit(1);
|
|
752
|
+
});
|
|
753
|
+
}
|