@fmdzc/cli-ai 3.0.3 → 3.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/LICENSE +15 -15
- package/README.md +171 -169
- package/dist/cli.js +13 -13
- package/dist/index.js +2 -2
- package/package.json +7 -3
- package/scripts/tag.js +120 -0
- package/scripts/uninstall.js +332 -332
- package/scripts/version.js +52 -0
package/scripts/uninstall.js
CHANGED
|
@@ -1,332 +1,332 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CLI AI Uninstall Script
|
|
5
|
-
* Safely removes all cli-ai components from the system
|
|
6
|
-
*
|
|
7
|
-
* Usage: node scripts/uninstall.js [--force]
|
|
8
|
-
*
|
|
9
|
-
* Components removed:
|
|
10
|
-
* - API key from system keyring
|
|
11
|
-
* - Config directory (~/.cli_ai_assistant)
|
|
12
|
-
* - Global npm/pnpm link (optional)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { execSync, spawnSync } from 'node:child_process';
|
|
16
|
-
import { existsSync, rmSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
17
|
-
import { homedir, platform } from 'node:os';
|
|
18
|
-
import { join } from 'node:path';
|
|
19
|
-
import { createInterface } from 'node:readline';
|
|
20
|
-
|
|
21
|
-
const CONFIG_DIR_NAME = '.cli_ai_assistant';
|
|
22
|
-
const KEYRING_SERVICE = 'cli-ai';
|
|
23
|
-
const KEYRING_ACCOUNT = 'anthropic';
|
|
24
|
-
|
|
25
|
-
const isWindows = platform() === 'win32';
|
|
26
|
-
const isMac = platform() === 'darwin';
|
|
27
|
-
const isLinux = platform() === 'linux';
|
|
28
|
-
|
|
29
|
-
const forceMode = process.argv.includes('--force') || process.argv.includes('-f');
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Print colored output
|
|
33
|
-
*/
|
|
34
|
-
function log(message, type = 'info') {
|
|
35
|
-
const colors = {
|
|
36
|
-
info: '\x1b[36m', // cyan
|
|
37
|
-
success: '\x1b[32m', // green
|
|
38
|
-
warn: '\x1b[33m', // yellow
|
|
39
|
-
error: '\x1b[31m', // red
|
|
40
|
-
reset: '\x1b[0m',
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const prefix = {
|
|
44
|
-
info: 'ℹ',
|
|
45
|
-
success: '✓',
|
|
46
|
-
warn: '⚠',
|
|
47
|
-
error: '✗',
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
console.log(`${colors[type]}${prefix[type]} ${message}${colors.reset}`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Prompt user for confirmation
|
|
55
|
-
*/
|
|
56
|
-
async function confirm(question) {
|
|
57
|
-
if (forceMode) return true;
|
|
58
|
-
|
|
59
|
-
const rl = createInterface({
|
|
60
|
-
input: process.stdin,
|
|
61
|
-
output: process.stdout,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return new Promise((resolve) => {
|
|
65
|
-
rl.question(`${question} (y/N): `, (answer) => {
|
|
66
|
-
rl.close();
|
|
67
|
-
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Remove API key from system keyring
|
|
74
|
-
*/
|
|
75
|
-
async function removeKeyringEntry() {
|
|
76
|
-
log('Removing API key from system keyring...');
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
// Try using @napi-rs/keyring first (if available)
|
|
80
|
-
try {
|
|
81
|
-
const { Entry } = await import('@napi-rs/keyring');
|
|
82
|
-
const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
83
|
-
entry.deleteCredential();
|
|
84
|
-
log('API key removed from keyring (via @napi-rs/keyring)', 'success');
|
|
85
|
-
return true;
|
|
86
|
-
} catch {
|
|
87
|
-
// Fall through to platform-specific methods
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Platform-specific fallbacks
|
|
91
|
-
if (isWindows) {
|
|
92
|
-
// Windows Credential Manager
|
|
93
|
-
const result = spawnSync('cmdkey', ['/delete:cli-ai'], {
|
|
94
|
-
encoding: 'utf-8',
|
|
95
|
-
shell: true,
|
|
96
|
-
});
|
|
97
|
-
if (result.status === 0) {
|
|
98
|
-
log('API key removed from Windows Credential Manager', 'success');
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
} else if (isMac) {
|
|
102
|
-
// macOS Keychain
|
|
103
|
-
const result = spawnSync('security', [
|
|
104
|
-
'delete-generic-password',
|
|
105
|
-
'-s', KEYRING_SERVICE,
|
|
106
|
-
'-a', KEYRING_ACCOUNT,
|
|
107
|
-
], { encoding: 'utf-8' });
|
|
108
|
-
if (result.status === 0) {
|
|
109
|
-
log('API key removed from macOS Keychain', 'success');
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
} else if (isLinux) {
|
|
113
|
-
// Linux secret-tool (GNOME Keyring)
|
|
114
|
-
const result = spawnSync('secret-tool', [
|
|
115
|
-
'clear',
|
|
116
|
-
'service', KEYRING_SERVICE,
|
|
117
|
-
'account', KEYRING_ACCOUNT,
|
|
118
|
-
], { encoding: 'utf-8' });
|
|
119
|
-
if (result.status === 0) {
|
|
120
|
-
log('API key removed from Linux keyring', 'success');
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
log('No API key found in keyring (may not have been stored there)', 'warn');
|
|
126
|
-
return true;
|
|
127
|
-
} catch (error) {
|
|
128
|
-
log(`Could not remove keyring entry: ${error.message}`, 'warn');
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Remove config directory
|
|
135
|
-
*/
|
|
136
|
-
function removeConfigDirectory() {
|
|
137
|
-
const configDir = join(homedir(), CONFIG_DIR_NAME);
|
|
138
|
-
|
|
139
|
-
if (!existsSync(configDir)) {
|
|
140
|
-
log(`Config directory not found: ${configDir}`, 'warn');
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
log(`Removing config directory: ${configDir}`);
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
rmSync(configDir, { recursive: true, force: true });
|
|
148
|
-
log('Config directory removed', 'success');
|
|
149
|
-
return true;
|
|
150
|
-
} catch (error) {
|
|
151
|
-
log(`Failed to remove config directory: ${error.message}`, 'error');
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Remove global package link
|
|
158
|
-
*/
|
|
159
|
-
function removeGlobalLink() {
|
|
160
|
-
log('Checking for global package installation...');
|
|
161
|
-
|
|
162
|
-
// Try pnpm first
|
|
163
|
-
try {
|
|
164
|
-
const pnpmResult = spawnSync('pnpm', ['list', '-g', '--depth=0'], {
|
|
165
|
-
encoding: 'utf-8',
|
|
166
|
-
shell: true,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
if (pnpmResult.stdout && pnpmResult.stdout.includes('cli-ai')) {
|
|
170
|
-
log('Found pnpm global link, removing...');
|
|
171
|
-
const unlinkResult = spawnSync('pnpm', ['unlink', '--global', '@fmdzc/cli-ai'], {
|
|
172
|
-
encoding: 'utf-8',
|
|
173
|
-
shell: true,
|
|
174
|
-
});
|
|
175
|
-
if (unlinkResult.status === 0) {
|
|
176
|
-
log('Removed pnpm global link', 'success');
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
} catch {
|
|
181
|
-
// pnpm not available
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Try npm
|
|
185
|
-
try {
|
|
186
|
-
const npmResult = spawnSync('npm', ['list', '-g', '--depth=0'], {
|
|
187
|
-
encoding: 'utf-8',
|
|
188
|
-
shell: true,
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
if (npmResult.stdout && npmResult.stdout.includes('cli-ai')) {
|
|
192
|
-
log('Found npm global link, removing...');
|
|
193
|
-
const unlinkResult = spawnSync('npm', ['unlink', '-g', '@fmdzc/cli-ai'], {
|
|
194
|
-
encoding: 'utf-8',
|
|
195
|
-
shell: true,
|
|
196
|
-
});
|
|
197
|
-
if (unlinkResult.status === 0) {
|
|
198
|
-
log('Removed npm global link', 'success');
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
} catch {
|
|
203
|
-
// npm not available
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
log('No global package link found', 'info');
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Remove shell alias from config files
|
|
212
|
-
*/
|
|
213
|
-
function removeShellAliases() {
|
|
214
|
-
log('Checking for shell aliases...');
|
|
215
|
-
|
|
216
|
-
const shellConfigs = [];
|
|
217
|
-
|
|
218
|
-
if (isWindows) {
|
|
219
|
-
// PowerShell profile locations
|
|
220
|
-
const psProfile = join(homedir(), 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
221
|
-
const pwshProfile = join(homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
222
|
-
if (existsSync(psProfile)) shellConfigs.push(psProfile);
|
|
223
|
-
if (existsSync(pwshProfile)) shellConfigs.push(pwshProfile);
|
|
224
|
-
} else {
|
|
225
|
-
// Unix shell configs
|
|
226
|
-
const bashrc = join(homedir(), '.bashrc');
|
|
227
|
-
const zshrc = join(homedir(), '.zshrc');
|
|
228
|
-
const fishConfig = join(homedir(), '.config', 'fish', 'config.fish');
|
|
229
|
-
|
|
230
|
-
if (existsSync(bashrc)) shellConfigs.push(bashrc);
|
|
231
|
-
if (existsSync(zshrc)) shellConfigs.push(zshrc);
|
|
232
|
-
if (existsSync(fishConfig)) shellConfigs.push(fishConfig);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
let aliasesRemoved = 0;
|
|
236
|
-
|
|
237
|
-
for (const configPath of shellConfigs) {
|
|
238
|
-
try {
|
|
239
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
240
|
-
|
|
241
|
-
// Look for cli-ai related aliases
|
|
242
|
-
const aliasPatterns = [
|
|
243
|
-
/^.*alias\s+s\s*=.*cli[_-]?ai.*$/gm,
|
|
244
|
-
/^.*Set-Alias.*cli[_-]?ai.*$/gm,
|
|
245
|
-
/^.*function\s+s\s*\(\).*cli[_-]?ai.*$/gm,
|
|
246
|
-
/^# CLI AI.*$/gm,
|
|
247
|
-
];
|
|
248
|
-
|
|
249
|
-
let newContent = content;
|
|
250
|
-
let hasChanges = false;
|
|
251
|
-
|
|
252
|
-
for (const pattern of aliasPatterns) {
|
|
253
|
-
if (pattern.test(newContent)) {
|
|
254
|
-
hasChanges = true;
|
|
255
|
-
newContent = newContent.replace(pattern, '');
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (hasChanges) {
|
|
260
|
-
// Create backup
|
|
261
|
-
const backupPath = `${configPath}.cli-ai-backup`;
|
|
262
|
-
copyFileSync(configPath, backupPath);
|
|
263
|
-
log(`Created backup: ${backupPath}`, 'info');
|
|
264
|
-
|
|
265
|
-
// Clean up empty lines
|
|
266
|
-
newContent = newContent.replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
267
|
-
writeFileSync(configPath, newContent);
|
|
268
|
-
log(`Removed alias from: ${configPath}`, 'success');
|
|
269
|
-
aliasesRemoved++;
|
|
270
|
-
}
|
|
271
|
-
} catch (error) {
|
|
272
|
-
log(`Could not process ${configPath}: ${error.message}`, 'warn');
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (aliasesRemoved === 0) {
|
|
277
|
-
log('No shell aliases found', 'info');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Main uninstall flow
|
|
285
|
-
*/
|
|
286
|
-
async function main() {
|
|
287
|
-
console.log('\n\x1b[1m\x1b[35m🗑️ CLI AI Uninstaller\x1b[0m\n');
|
|
288
|
-
|
|
289
|
-
if (!forceMode) {
|
|
290
|
-
console.log('This will remove:');
|
|
291
|
-
console.log(' • API key from system keyring');
|
|
292
|
-
console.log(` • Config directory (~/${CONFIG_DIR_NAME})`);
|
|
293
|
-
console.log(' • Global package link (if exists)');
|
|
294
|
-
console.log(' • Shell aliases (if configured)\n');
|
|
295
|
-
|
|
296
|
-
const proceed = await confirm('Do you want to proceed?');
|
|
297
|
-
if (!proceed) {
|
|
298
|
-
log('Uninstall cancelled', 'info');
|
|
299
|
-
process.exit(0);
|
|
300
|
-
}
|
|
301
|
-
console.log();
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const results = {
|
|
305
|
-
keyring: await removeKeyringEntry(),
|
|
306
|
-
config: removeConfigDirectory(),
|
|
307
|
-
globalLink: removeGlobalLink(),
|
|
308
|
-
aliases: removeShellAliases(),
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
console.log('\n\x1b[1m--- Summary ---\x1b[0m\n');
|
|
312
|
-
|
|
313
|
-
const allSuccess = Object.values(results).every(Boolean);
|
|
314
|
-
|
|
315
|
-
if (allSuccess) {
|
|
316
|
-
log('CLI AI has been completely removed from your system!', 'success');
|
|
317
|
-
console.log('\nNote: If you installed via npm/pnpm globally, you may also want to run:');
|
|
318
|
-
console.log(' npm uninstall -g @fmdzc/cli-ai');
|
|
319
|
-
console.log(' # or');
|
|
320
|
-
console.log(' pnpm uninstall -g @fmdzc/cli-ai\n');
|
|
321
|
-
} else {
|
|
322
|
-
log('Uninstall completed with some warnings (see above)', 'warn');
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Remind about shell restart
|
|
326
|
-
console.log('\x1b[33mRestart your terminal for changes to take effect.\x1b[0m\n');
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
main().catch((error) => {
|
|
330
|
-
log(`Uninstall failed: ${error.message}`, 'error');
|
|
331
|
-
process.exit(1);
|
|
332
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI AI Uninstall Script
|
|
5
|
+
* Safely removes all cli-ai components from the system
|
|
6
|
+
*
|
|
7
|
+
* Usage: node scripts/uninstall.js [--force]
|
|
8
|
+
*
|
|
9
|
+
* Components removed:
|
|
10
|
+
* - API key from system keyring
|
|
11
|
+
* - Config directory (~/.cli_ai_assistant)
|
|
12
|
+
* - Global npm/pnpm link (optional)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
16
|
+
import { existsSync, rmSync, readFileSync, writeFileSync, copyFileSync } from 'node:fs';
|
|
17
|
+
import { homedir, platform } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { createInterface } from 'node:readline';
|
|
20
|
+
|
|
21
|
+
const CONFIG_DIR_NAME = '.cli_ai_assistant';
|
|
22
|
+
const KEYRING_SERVICE = 'cli-ai';
|
|
23
|
+
const KEYRING_ACCOUNT = 'anthropic';
|
|
24
|
+
|
|
25
|
+
const isWindows = platform() === 'win32';
|
|
26
|
+
const isMac = platform() === 'darwin';
|
|
27
|
+
const isLinux = platform() === 'linux';
|
|
28
|
+
|
|
29
|
+
const forceMode = process.argv.includes('--force') || process.argv.includes('-f');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Print colored output
|
|
33
|
+
*/
|
|
34
|
+
function log(message, type = 'info') {
|
|
35
|
+
const colors = {
|
|
36
|
+
info: '\x1b[36m', // cyan
|
|
37
|
+
success: '\x1b[32m', // green
|
|
38
|
+
warn: '\x1b[33m', // yellow
|
|
39
|
+
error: '\x1b[31m', // red
|
|
40
|
+
reset: '\x1b[0m',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const prefix = {
|
|
44
|
+
info: 'ℹ',
|
|
45
|
+
success: '✓',
|
|
46
|
+
warn: '⚠',
|
|
47
|
+
error: '✗',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
console.log(`${colors[type]}${prefix[type]} ${message}${colors.reset}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Prompt user for confirmation
|
|
55
|
+
*/
|
|
56
|
+
async function confirm(question) {
|
|
57
|
+
if (forceMode) return true;
|
|
58
|
+
|
|
59
|
+
const rl = createInterface({
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
rl.question(`${question} (y/N): `, (answer) => {
|
|
66
|
+
rl.close();
|
|
67
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remove API key from system keyring
|
|
74
|
+
*/
|
|
75
|
+
async function removeKeyringEntry() {
|
|
76
|
+
log('Removing API key from system keyring...');
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Try using @napi-rs/keyring first (if available)
|
|
80
|
+
try {
|
|
81
|
+
const { Entry } = await import('@napi-rs/keyring');
|
|
82
|
+
const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
83
|
+
entry.deleteCredential();
|
|
84
|
+
log('API key removed from keyring (via @napi-rs/keyring)', 'success');
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
// Fall through to platform-specific methods
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Platform-specific fallbacks
|
|
91
|
+
if (isWindows) {
|
|
92
|
+
// Windows Credential Manager
|
|
93
|
+
const result = spawnSync('cmdkey', ['/delete:cli-ai'], {
|
|
94
|
+
encoding: 'utf-8',
|
|
95
|
+
shell: true,
|
|
96
|
+
});
|
|
97
|
+
if (result.status === 0) {
|
|
98
|
+
log('API key removed from Windows Credential Manager', 'success');
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
} else if (isMac) {
|
|
102
|
+
// macOS Keychain
|
|
103
|
+
const result = spawnSync('security', [
|
|
104
|
+
'delete-generic-password',
|
|
105
|
+
'-s', KEYRING_SERVICE,
|
|
106
|
+
'-a', KEYRING_ACCOUNT,
|
|
107
|
+
], { encoding: 'utf-8' });
|
|
108
|
+
if (result.status === 0) {
|
|
109
|
+
log('API key removed from macOS Keychain', 'success');
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
} else if (isLinux) {
|
|
113
|
+
// Linux secret-tool (GNOME Keyring)
|
|
114
|
+
const result = spawnSync('secret-tool', [
|
|
115
|
+
'clear',
|
|
116
|
+
'service', KEYRING_SERVICE,
|
|
117
|
+
'account', KEYRING_ACCOUNT,
|
|
118
|
+
], { encoding: 'utf-8' });
|
|
119
|
+
if (result.status === 0) {
|
|
120
|
+
log('API key removed from Linux keyring', 'success');
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
log('No API key found in keyring (may not have been stored there)', 'warn');
|
|
126
|
+
return true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
log(`Could not remove keyring entry: ${error.message}`, 'warn');
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Remove config directory
|
|
135
|
+
*/
|
|
136
|
+
function removeConfigDirectory() {
|
|
137
|
+
const configDir = join(homedir(), CONFIG_DIR_NAME);
|
|
138
|
+
|
|
139
|
+
if (!existsSync(configDir)) {
|
|
140
|
+
log(`Config directory not found: ${configDir}`, 'warn');
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
log(`Removing config directory: ${configDir}`);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
rmSync(configDir, { recursive: true, force: true });
|
|
148
|
+
log('Config directory removed', 'success');
|
|
149
|
+
return true;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
log(`Failed to remove config directory: ${error.message}`, 'error');
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Remove global package link
|
|
158
|
+
*/
|
|
159
|
+
function removeGlobalLink() {
|
|
160
|
+
log('Checking for global package installation...');
|
|
161
|
+
|
|
162
|
+
// Try pnpm first
|
|
163
|
+
try {
|
|
164
|
+
const pnpmResult = spawnSync('pnpm', ['list', '-g', '--depth=0'], {
|
|
165
|
+
encoding: 'utf-8',
|
|
166
|
+
shell: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (pnpmResult.stdout && pnpmResult.stdout.includes('cli-ai')) {
|
|
170
|
+
log('Found pnpm global link, removing...');
|
|
171
|
+
const unlinkResult = spawnSync('pnpm', ['unlink', '--global', '@fmdzc/cli-ai'], {
|
|
172
|
+
encoding: 'utf-8',
|
|
173
|
+
shell: true,
|
|
174
|
+
});
|
|
175
|
+
if (unlinkResult.status === 0) {
|
|
176
|
+
log('Removed pnpm global link', 'success');
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// pnpm not available
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Try npm
|
|
185
|
+
try {
|
|
186
|
+
const npmResult = spawnSync('npm', ['list', '-g', '--depth=0'], {
|
|
187
|
+
encoding: 'utf-8',
|
|
188
|
+
shell: true,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (npmResult.stdout && npmResult.stdout.includes('cli-ai')) {
|
|
192
|
+
log('Found npm global link, removing...');
|
|
193
|
+
const unlinkResult = spawnSync('npm', ['unlink', '-g', '@fmdzc/cli-ai'], {
|
|
194
|
+
encoding: 'utf-8',
|
|
195
|
+
shell: true,
|
|
196
|
+
});
|
|
197
|
+
if (unlinkResult.status === 0) {
|
|
198
|
+
log('Removed npm global link', 'success');
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// npm not available
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
log('No global package link found', 'info');
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Remove shell alias from config files
|
|
212
|
+
*/
|
|
213
|
+
function removeShellAliases() {
|
|
214
|
+
log('Checking for shell aliases...');
|
|
215
|
+
|
|
216
|
+
const shellConfigs = [];
|
|
217
|
+
|
|
218
|
+
if (isWindows) {
|
|
219
|
+
// PowerShell profile locations
|
|
220
|
+
const psProfile = join(homedir(), 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
221
|
+
const pwshProfile = join(homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
222
|
+
if (existsSync(psProfile)) shellConfigs.push(psProfile);
|
|
223
|
+
if (existsSync(pwshProfile)) shellConfigs.push(pwshProfile);
|
|
224
|
+
} else {
|
|
225
|
+
// Unix shell configs
|
|
226
|
+
const bashrc = join(homedir(), '.bashrc');
|
|
227
|
+
const zshrc = join(homedir(), '.zshrc');
|
|
228
|
+
const fishConfig = join(homedir(), '.config', 'fish', 'config.fish');
|
|
229
|
+
|
|
230
|
+
if (existsSync(bashrc)) shellConfigs.push(bashrc);
|
|
231
|
+
if (existsSync(zshrc)) shellConfigs.push(zshrc);
|
|
232
|
+
if (existsSync(fishConfig)) shellConfigs.push(fishConfig);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let aliasesRemoved = 0;
|
|
236
|
+
|
|
237
|
+
for (const configPath of shellConfigs) {
|
|
238
|
+
try {
|
|
239
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
240
|
+
|
|
241
|
+
// Look for cli-ai related aliases
|
|
242
|
+
const aliasPatterns = [
|
|
243
|
+
/^.*alias\s+s\s*=.*cli[_-]?ai.*$/gm,
|
|
244
|
+
/^.*Set-Alias.*cli[_-]?ai.*$/gm,
|
|
245
|
+
/^.*function\s+s\s*\(\).*cli[_-]?ai.*$/gm,
|
|
246
|
+
/^# CLI AI.*$/gm,
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
let newContent = content;
|
|
250
|
+
let hasChanges = false;
|
|
251
|
+
|
|
252
|
+
for (const pattern of aliasPatterns) {
|
|
253
|
+
if (pattern.test(newContent)) {
|
|
254
|
+
hasChanges = true;
|
|
255
|
+
newContent = newContent.replace(pattern, '');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (hasChanges) {
|
|
260
|
+
// Create backup
|
|
261
|
+
const backupPath = `${configPath}.cli-ai-backup`;
|
|
262
|
+
copyFileSync(configPath, backupPath);
|
|
263
|
+
log(`Created backup: ${backupPath}`, 'info');
|
|
264
|
+
|
|
265
|
+
// Clean up empty lines
|
|
266
|
+
newContent = newContent.replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
267
|
+
writeFileSync(configPath, newContent);
|
|
268
|
+
log(`Removed alias from: ${configPath}`, 'success');
|
|
269
|
+
aliasesRemoved++;
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
log(`Could not process ${configPath}: ${error.message}`, 'warn');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (aliasesRemoved === 0) {
|
|
277
|
+
log('No shell aliases found', 'info');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Main uninstall flow
|
|
285
|
+
*/
|
|
286
|
+
async function main() {
|
|
287
|
+
console.log('\n\x1b[1m\x1b[35m🗑️ CLI AI Uninstaller\x1b[0m\n');
|
|
288
|
+
|
|
289
|
+
if (!forceMode) {
|
|
290
|
+
console.log('This will remove:');
|
|
291
|
+
console.log(' • API key from system keyring');
|
|
292
|
+
console.log(` • Config directory (~/${CONFIG_DIR_NAME})`);
|
|
293
|
+
console.log(' • Global package link (if exists)');
|
|
294
|
+
console.log(' • Shell aliases (if configured)\n');
|
|
295
|
+
|
|
296
|
+
const proceed = await confirm('Do you want to proceed?');
|
|
297
|
+
if (!proceed) {
|
|
298
|
+
log('Uninstall cancelled', 'info');
|
|
299
|
+
process.exit(0);
|
|
300
|
+
}
|
|
301
|
+
console.log();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const results = {
|
|
305
|
+
keyring: await removeKeyringEntry(),
|
|
306
|
+
config: removeConfigDirectory(),
|
|
307
|
+
globalLink: removeGlobalLink(),
|
|
308
|
+
aliases: removeShellAliases(),
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
console.log('\n\x1b[1m--- Summary ---\x1b[0m\n');
|
|
312
|
+
|
|
313
|
+
const allSuccess = Object.values(results).every(Boolean);
|
|
314
|
+
|
|
315
|
+
if (allSuccess) {
|
|
316
|
+
log('CLI AI has been completely removed from your system!', 'success');
|
|
317
|
+
console.log('\nNote: If you installed via npm/pnpm globally, you may also want to run:');
|
|
318
|
+
console.log(' npm uninstall -g @fmdzc/cli-ai');
|
|
319
|
+
console.log(' # or');
|
|
320
|
+
console.log(' pnpm uninstall -g @fmdzc/cli-ai\n');
|
|
321
|
+
} else {
|
|
322
|
+
log('Uninstall completed with some warnings (see above)', 'warn');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Remind about shell restart
|
|
326
|
+
console.log('\x1b[33mRestart your terminal for changes to take effect.\x1b[0m\n');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
main().catch((error) => {
|
|
330
|
+
log(`Uninstall failed: ${error.message}`, 'error');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
});
|