deskify-cli 1.0.0 ā 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -17
- package/deskify.js +9 -893
- package/package.json +2 -2
- package/src/flows/create.js +284 -0
- package/src/flows/uninstall.js +145 -0
- package/src/utils/colors.js +46 -0
- package/src/utils/favicon.js +196 -0
- package/src/utils/prompts.js +182 -0
- package/src/utils/spinner.js +41 -0
package/deskify.js
CHANGED
|
@@ -2,462 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* deskify.js
|
|
5
|
-
*
|
|
5
|
+
* Entry point for the deskify CLI.
|
|
6
6
|
* Author: Antigravity AI
|
|
7
7
|
* License: MIT
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const {
|
|
15
|
-
const
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const { colors, log } = require('./src/utils/colors');
|
|
12
|
+
const { selectOption, waitForKey } = require('./src/utils/prompts');
|
|
13
|
+
const { startSpinner, stopSpinner } = require('./src/utils/spinner');
|
|
14
|
+
const { createFlow } = require('./src/flows/create');
|
|
15
|
+
const { uninstallFlow } = require('./src/flows/uninstall');
|
|
16
16
|
|
|
17
|
-
// ANSI escape codes for styling
|
|
18
|
-
const colors = {
|
|
19
|
-
reset: '\x1b[0m',
|
|
20
|
-
bright: '\x1b[1m',
|
|
21
|
-
dim: '\x1b[2m',
|
|
22
|
-
underline: '\x1b[4m',
|
|
23
|
-
blink: '\x1b[5m',
|
|
24
|
-
reverse: '\x1b[7m',
|
|
25
|
-
hidden: '\x1b[8m',
|
|
26
|
-
|
|
27
|
-
black: '\x1b[30m',
|
|
28
|
-
red: '\x1b[31m',
|
|
29
|
-
green: '\x1b[32m',
|
|
30
|
-
yellow: '\x1b[33m',
|
|
31
|
-
blue: '\x1b[34m',
|
|
32
|
-
magenta: '\x1b[35m',
|
|
33
|
-
cyan: '\x1b[36m',
|
|
34
|
-
white: '\x1b[37m',
|
|
35
|
-
|
|
36
|
-
bgBlack: '\x1b[40m',
|
|
37
|
-
bgRed: '\x1b[41m',
|
|
38
|
-
bgGreen: '\x1b[42m',
|
|
39
|
-
bgYellow: '\x1b[43m',
|
|
40
|
-
bgBlue: '\x1b[44m',
|
|
41
|
-
bgMagenta: '\x1b[45m',
|
|
42
|
-
bgCyan: '\x1b[46m',
|
|
43
|
-
bgWhite: '\x1b[47m'
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Console output helpers
|
|
47
|
-
const log = {
|
|
48
|
-
info: (msg) => console.log(`${colors.cyan}info:${colors.reset} ${msg}`),
|
|
49
|
-
success: (msg) => console.log(`${colors.green}success:${colors.reset} ${colors.bright}${msg}${colors.reset}`),
|
|
50
|
-
warn: (msg) => console.log(`${colors.yellow}warning:${colors.reset} ${msg}`),
|
|
51
|
-
error: (msg) => console.error(`${colors.red}error:${colors.reset} ${colors.bright}${msg}${colors.reset}`),
|
|
52
|
-
bold: (msg) => console.log(`${colors.bright}${msg}${colors.reset}`),
|
|
53
|
-
accent: (msg) => console.log(`${colors.magenta}${msg}${colors.reset}`)
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Help helper to expand tilde in paths
|
|
57
|
-
function expandHome(pathStr) {
|
|
58
|
-
if (!pathStr) return '';
|
|
59
|
-
if (pathStr.startsWith('~/') || pathStr === '~') {
|
|
60
|
-
return pathStr.replace('~', os.homedir());
|
|
61
|
-
}
|
|
62
|
-
return pathStr;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Helper to ask a question via raw-keypress to capture the Escape (ESC) key
|
|
66
|
-
function ask(questionText, defaultValue = '') {
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
const prompt = defaultValue
|
|
69
|
-
? `${colors.bright}${questionText}${colors.reset} ${colors.dim}(default: ${defaultValue})${colors.reset}: `
|
|
70
|
-
: `${colors.bright}${questionText}${colors.reset}: `;
|
|
71
|
-
|
|
72
|
-
process.stdout.write(prompt);
|
|
73
|
-
|
|
74
|
-
let buffer = '';
|
|
75
|
-
const rl = readline.createInterface({
|
|
76
|
-
input: process.stdin,
|
|
77
|
-
output: process.stdout
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// Enable raw keypress listening
|
|
81
|
-
readline.emitKeypressEvents(process.stdin, rl);
|
|
82
|
-
if (process.stdin.isTTY) {
|
|
83
|
-
process.stdin.setRawMode(true);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function onKeypress(str, key) {
|
|
87
|
-
if (key) {
|
|
88
|
-
if (key.name === 'escape') {
|
|
89
|
-
cleanup();
|
|
90
|
-
reject(new Error('ESC'));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
if (key.ctrl && key.name === 'c') {
|
|
94
|
-
cleanup();
|
|
95
|
-
process.stdout.write('\n');
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
if (key.name === 'return' || key.name === 'enter') {
|
|
99
|
-
cleanup();
|
|
100
|
-
process.stdout.write('\n');
|
|
101
|
-
resolve(buffer.trim() || defaultValue);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (key.name === 'backspace') {
|
|
105
|
-
if (buffer.length > 0) {
|
|
106
|
-
buffer = buffer.slice(0, -1);
|
|
107
|
-
// Clear line and redraw prompt + buffer
|
|
108
|
-
process.stdout.write('\r\x1B[2K');
|
|
109
|
-
process.stdout.write(prompt + buffer);
|
|
110
|
-
}
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Write standard character
|
|
116
|
-
if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
117
|
-
buffer += str;
|
|
118
|
-
process.stdout.write(str);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function cleanup() {
|
|
123
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
124
|
-
if (process.stdin.isTTY) {
|
|
125
|
-
process.stdin.setRawMode(false);
|
|
126
|
-
}
|
|
127
|
-
rl.close();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
process.stdin.on('keypress', onKeypress);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Helper to ask a yes/no question
|
|
135
|
-
async function askYesNo(questionText, defaultYes = true) {
|
|
136
|
-
const defaultStr = defaultYes ? 'Y/n' : 'y/N';
|
|
137
|
-
const answer = await ask(`${questionText} (${defaultStr})`);
|
|
138
|
-
if (!answer) return defaultYes;
|
|
139
|
-
return answer.toLowerCase().startsWith('y');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Helper for arrow-key interactive selection menu (zero-dependency)
|
|
143
|
-
function selectOption(questionText, options, defaultIndex = 0) {
|
|
144
|
-
return new Promise((resolve) => {
|
|
145
|
-
let selectedIndex = defaultIndex;
|
|
146
|
-
const rl = readline.createInterface({
|
|
147
|
-
input: process.stdin,
|
|
148
|
-
output: process.stdout
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Enable raw keypress listening
|
|
152
|
-
readline.emitKeypressEvents(process.stdin, rl);
|
|
153
|
-
if (process.stdin.isTTY) {
|
|
154
|
-
process.stdin.setRawMode(true);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Hide standard cursor
|
|
158
|
-
process.stdout.write('\x1B[?25l');
|
|
159
|
-
|
|
160
|
-
function renderMenu() {
|
|
161
|
-
console.log(`\n${colors.bright}${colors.cyan}āÆ${colors.reset} ${colors.bright}${questionText}${colors.reset}`);
|
|
162
|
-
options.forEach((opt, idx) => {
|
|
163
|
-
if (idx === selectedIndex) {
|
|
164
|
-
console.log(` ${colors.cyan}${colors.bright}⯠${opt}${colors.reset}`);
|
|
165
|
-
} else {
|
|
166
|
-
console.log(` ${colors.dim}${opt}${colors.reset}`);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
renderMenu();
|
|
172
|
-
|
|
173
|
-
function onKeypress(str, key) {
|
|
174
|
-
// Move cursor back up options.length + 2 lines and clear them
|
|
175
|
-
const linesToMove = options.length + 2;
|
|
176
|
-
process.stdout.write(`\x1B[${linesToMove}A`);
|
|
177
|
-
for (let i = 0; i < linesToMove; i++) {
|
|
178
|
-
process.stdout.write('\x1B[2K\n');
|
|
179
|
-
}
|
|
180
|
-
process.stdout.write(`\x1B[${linesToMove}A`);
|
|
181
|
-
|
|
182
|
-
if (key) {
|
|
183
|
-
if (key.name === 'up') {
|
|
184
|
-
selectedIndex = (selectedIndex - 1 + options.length) % options.length;
|
|
185
|
-
} else if (key.name === 'down') {
|
|
186
|
-
selectedIndex = (selectedIndex + 1) % options.length;
|
|
187
|
-
} else if (key.name === 'return' || key.name === 'enter') {
|
|
188
|
-
cleanup();
|
|
189
|
-
resolve(selectedIndex);
|
|
190
|
-
return;
|
|
191
|
-
} else if (key.name === 'escape') {
|
|
192
|
-
cleanup();
|
|
193
|
-
resolve(options.length - 1); // select the last option (Cancel/Exit)
|
|
194
|
-
return;
|
|
195
|
-
} else if (key.ctrl && key.name === 'c') {
|
|
196
|
-
cleanup();
|
|
197
|
-
process.stdout.write('\n');
|
|
198
|
-
process.exit(0);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
renderMenu();
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function cleanup() {
|
|
205
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
206
|
-
if (process.stdin.isTTY) {
|
|
207
|
-
process.stdin.setRawMode(false);
|
|
208
|
-
}
|
|
209
|
-
// Show cursor again
|
|
210
|
-
process.stdout.write('\x1B[?25h');
|
|
211
|
-
rl.close();
|
|
212
|
-
|
|
213
|
-
// Clear the menu before resolving so the console is kept clean
|
|
214
|
-
const linesToMove = options.length + 2;
|
|
215
|
-
process.stdout.write(`\x1B[${linesToMove}A`);
|
|
216
|
-
for (let i = 0; i < linesToMove; i++) {
|
|
217
|
-
process.stdout.write('\x1B[2K\n');
|
|
218
|
-
}
|
|
219
|
-
process.stdout.write(`\x1B[${linesToMove}A`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
process.stdin.on('keypress', onKeypress);
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Spinner variables and functions
|
|
227
|
-
let spinnerInterval = null;
|
|
228
|
-
function startSpinner(msg) {
|
|
229
|
-
const frames = ['ā ', 'ā ', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ', 'ā '];
|
|
230
|
-
let i = 0;
|
|
231
|
-
// Hide cursor
|
|
232
|
-
process.stdout.write('\x1B[?25l');
|
|
233
|
-
process.stdout.write(`${colors.cyan}${frames[0]}${colors.reset} ${msg}`);
|
|
234
|
-
spinnerInterval = setInterval(() => {
|
|
235
|
-
i = (i + 1) % frames.length;
|
|
236
|
-
process.stdout.write(`\r${colors.cyan}${frames[i]}${colors.reset} ${msg}`);
|
|
237
|
-
}, 80);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function stopSpinner(success = true, statusMsg = '') {
|
|
241
|
-
if (spinnerInterval) {
|
|
242
|
-
clearInterval(spinnerInterval);
|
|
243
|
-
spinnerInterval = null;
|
|
244
|
-
}
|
|
245
|
-
// Clear the spinner line
|
|
246
|
-
process.stdout.write('\r\x1B[2K');
|
|
247
|
-
// Show cursor
|
|
248
|
-
process.stdout.write('\x1B[?25h');
|
|
249
|
-
|
|
250
|
-
if (statusMsg) {
|
|
251
|
-
if (success) {
|
|
252
|
-
console.log(`${colors.green}ā${colors.reset} ${statusMsg}`);
|
|
253
|
-
} else {
|
|
254
|
-
console.log(`${colors.red}ā${colors.reset} ${statusMsg}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Extract domain and subdomains for smart defaults
|
|
260
|
-
function parseUrlDetails(urlStr) {
|
|
261
|
-
try {
|
|
262
|
-
// Add protocol if missing for URL parsing
|
|
263
|
-
let cleanUrl = urlStr;
|
|
264
|
-
if (!/^https?:\/\//i.test(urlStr)) {
|
|
265
|
-
cleanUrl = 'https://' + urlStr;
|
|
266
|
-
}
|
|
267
|
-
const parsed = new URL(cleanUrl);
|
|
268
|
-
const hostname = parsed.hostname;
|
|
269
|
-
|
|
270
|
-
// Extract a nice default app name (first segment of domain, e.g. panggon.danyakmallun.dev -> Panggon)
|
|
271
|
-
const segments = hostname.split('.');
|
|
272
|
-
let nameDefault = 'Webapp';
|
|
273
|
-
if (segments.length > 0) {
|
|
274
|
-
const firstSegment = segments[0] === 'www' && segments.length > 1 ? segments[1] : segments[0];
|
|
275
|
-
nameDefault = firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Escape dots for regex
|
|
279
|
-
const escapedDomain = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
280
|
-
const regexDefault = `.*${escapedDomain}.*`;
|
|
281
|
-
|
|
282
|
-
return {
|
|
283
|
-
cleanUrl,
|
|
284
|
-
hostname,
|
|
285
|
-
nameDefault,
|
|
286
|
-
regexDefault
|
|
287
|
-
};
|
|
288
|
-
} catch (e) {
|
|
289
|
-
return {
|
|
290
|
-
cleanUrl: urlStr,
|
|
291
|
-
hostname: '',
|
|
292
|
-
nameDefault: 'Webapp',
|
|
293
|
-
regexDefault: '.*'
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Helper to download a file from a URL, following redirects
|
|
299
|
-
function downloadUrl(urlStr, outputPath, redirectCount = 0) {
|
|
300
|
-
return new Promise((resolve) => {
|
|
301
|
-
if (redirectCount > 5) {
|
|
302
|
-
resolve(false); // prevent infinite redirect loops
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
https.get(urlStr, (response) => {
|
|
308
|
-
// Handle redirects (301, 302, 307, 308)
|
|
309
|
-
if ([301, 302, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
310
|
-
let redirectUrl = response.headers.location;
|
|
311
|
-
// Resolve relative redirects
|
|
312
|
-
if (redirectUrl.startsWith('/')) {
|
|
313
|
-
const parsed = new URL(urlStr);
|
|
314
|
-
redirectUrl = `${parsed.protocol}//${parsed.host}${redirectUrl}`;
|
|
315
|
-
}
|
|
316
|
-
resolve(downloadUrl(redirectUrl, outputPath, redirectCount + 1));
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (response.statusCode === 200) {
|
|
321
|
-
const file = fs.createWriteStream(outputPath);
|
|
322
|
-
response.pipe(file);
|
|
323
|
-
file.on('finish', () => {
|
|
324
|
-
file.close();
|
|
325
|
-
resolve(true);
|
|
326
|
-
});
|
|
327
|
-
} else {
|
|
328
|
-
resolve(false);
|
|
329
|
-
}
|
|
330
|
-
}).on('error', () => {
|
|
331
|
-
resolve(false);
|
|
332
|
-
});
|
|
333
|
-
} catch (e) {
|
|
334
|
-
resolve(false);
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Helper to fetch HTML content of a URL, following redirects
|
|
340
|
-
function getHtml(urlStr, redirectCount = 0) {
|
|
341
|
-
return new Promise((resolve) => {
|
|
342
|
-
if (redirectCount > 5) {
|
|
343
|
-
resolve('');
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
try {
|
|
348
|
-
https.get(urlStr, (response) => {
|
|
349
|
-
if ([301, 302, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
350
|
-
let redirectUrl = response.headers.location;
|
|
351
|
-
if (redirectUrl.startsWith('/')) {
|
|
352
|
-
const parsed = new URL(urlStr);
|
|
353
|
-
redirectUrl = `${parsed.protocol}//${parsed.host}${redirectUrl}`;
|
|
354
|
-
}
|
|
355
|
-
resolve(getHtml(redirectUrl, redirectCount + 1));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (response.statusCode !== 200) {
|
|
360
|
-
resolve('');
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
let data = '';
|
|
365
|
-
response.on('data', chunk => { data += chunk; });
|
|
366
|
-
response.on('end', () => { resolve(data); });
|
|
367
|
-
}).on('error', () => {
|
|
368
|
-
resolve('');
|
|
369
|
-
});
|
|
370
|
-
} catch (e) {
|
|
371
|
-
resolve('');
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Helper to extract absolute icon URL from HTML
|
|
377
|
-
function extractIconUrl(html, baseUrl) {
|
|
378
|
-
// Look for rel="icon", rel="shortcut icon", or rel="apple-touch-icon"
|
|
379
|
-
const matches = html.match(/<link[^>]*rel=["'](icon|shortcut icon|apple-touch-icon)["'][^>]*>/gi);
|
|
380
|
-
if (matches) {
|
|
381
|
-
for (const link of matches) {
|
|
382
|
-
const hrefMatch = link.match(/href=["']([^"']+)["']/i);
|
|
383
|
-
if (hrefMatch) {
|
|
384
|
-
let href = hrefMatch[1];
|
|
385
|
-
// Resolve absolute URL
|
|
386
|
-
if (!/^https?:\/\//i.test(href)) {
|
|
387
|
-
try {
|
|
388
|
-
const parsed = new URL(baseUrl);
|
|
389
|
-
if (href.startsWith('//')) {
|
|
390
|
-
href = parsed.protocol + href;
|
|
391
|
-
} else if (href.startsWith('/')) {
|
|
392
|
-
href = `${parsed.protocol}//${parsed.host}${href}`;
|
|
393
|
-
} else {
|
|
394
|
-
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : path.dirname(parsed.pathname) + '/';
|
|
395
|
-
href = `${parsed.protocol}//${parsed.host}${pathname}${href}`;
|
|
396
|
-
}
|
|
397
|
-
} catch (e) {
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return href;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Dynamic favicon downloader flow supporting Google API, direct scraping, and default fallback
|
|
409
|
-
async function downloadFaviconFlow(targetUrl, domain, outputPath) {
|
|
410
|
-
startSpinner('Fetching website favicon...');
|
|
411
|
-
// 1. Try Google Favicon Service (follows redirects)
|
|
412
|
-
const googleUrl = `https://www.google.com/s2/favicons?sz=128&domain=${domain}`;
|
|
413
|
-
let success = await downloadUrl(googleUrl, outputPath);
|
|
414
|
-
if (success) {
|
|
415
|
-
// Verify it didn't download a tiny 1x1 default spacer/404 image (file size less than 1KB)
|
|
416
|
-
try {
|
|
417
|
-
const stats = fs.statSync(outputPath);
|
|
418
|
-
if (stats.size > 1000) {
|
|
419
|
-
stopSpinner(true, 'Favicon downloaded successfully!');
|
|
420
|
-
return true;
|
|
421
|
-
}
|
|
422
|
-
fs.unlinkSync(outputPath); // Delete generic tiny favicon
|
|
423
|
-
} catch (e) {}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// 2. Try scraping target URL directly (great for local/private servers)
|
|
427
|
-
try {
|
|
428
|
-
const html = await getHtml(targetUrl);
|
|
429
|
-
if (html) {
|
|
430
|
-
const iconUrl = extractIconUrl(html, targetUrl);
|
|
431
|
-
if (iconUrl) {
|
|
432
|
-
success = await downloadUrl(iconUrl, outputPath);
|
|
433
|
-
if (success) {
|
|
434
|
-
stopSpinner(true, 'Favicon scraped and downloaded successfully!');
|
|
435
|
-
return true;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
} catch (e) {
|
|
440
|
-
// Ignore scraper errors
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// 3. Try default fallback path: https://domain/favicon.ico
|
|
444
|
-
try {
|
|
445
|
-
const parsed = new URL(targetUrl);
|
|
446
|
-
const fallbackUrl = `${parsed.protocol}//${parsed.host}/favicon.ico`;
|
|
447
|
-
success = await downloadUrl(fallbackUrl, outputPath);
|
|
448
|
-
if (success) {
|
|
449
|
-
stopSpinner(true, 'Default favicon.ico downloaded successfully!');
|
|
450
|
-
return true;
|
|
451
|
-
}
|
|
452
|
-
} catch (e) {
|
|
453
|
-
// Ignore fallback errors
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
stopSpinner(false, 'Could not download favicon. Fallback to default Electron icon.');
|
|
457
|
-
return false;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Print startup header
|
|
461
17
|
function printHeader() {
|
|
462
18
|
console.clear();
|
|
463
19
|
console.log(`${colors.cyan}${colors.bright}`);
|
|
@@ -471,11 +27,9 @@ function printHeader() {
|
|
|
471
27
|
console.log(`${colors.dim}--------------------------------------------------${colors.reset}\n`);
|
|
472
28
|
}
|
|
473
29
|
|
|
474
|
-
// Verification checks
|
|
475
30
|
function runPreflightChecks() {
|
|
476
31
|
startSpinner('Running system pre-flight checks...');
|
|
477
32
|
|
|
478
|
-
// 1. Check Node.js
|
|
479
33
|
try {
|
|
480
34
|
execSync('node -v', { stdio: 'ignore' });
|
|
481
35
|
} catch (e) {
|
|
@@ -484,7 +38,6 @@ function runPreflightChecks() {
|
|
|
484
38
|
process.exit(1);
|
|
485
39
|
}
|
|
486
40
|
|
|
487
|
-
// 2. Check NPM
|
|
488
41
|
try {
|
|
489
42
|
execSync('npm -v', { stdio: 'ignore' });
|
|
490
43
|
} catch (e) {
|
|
@@ -493,7 +46,6 @@ function runPreflightChecks() {
|
|
|
493
46
|
process.exit(1);
|
|
494
47
|
}
|
|
495
48
|
|
|
496
|
-
// 3. Check update-desktop-database
|
|
497
49
|
let hasDesktopUtils = true;
|
|
498
50
|
try {
|
|
499
51
|
execSync('command -v update-desktop-database', { stdio: 'ignore' });
|
|
@@ -508,441 +60,6 @@ function runPreflightChecks() {
|
|
|
508
60
|
return { hasDesktopUtils };
|
|
509
61
|
}
|
|
510
62
|
|
|
511
|
-
// Core creation flow (formerly main)
|
|
512
|
-
async function createFlow(hasDesktopUtils) {
|
|
513
|
-
console.clear();
|
|
514
|
-
printHeader();
|
|
515
|
-
log.bold('--- Create a New Desktop Application ---');
|
|
516
|
-
console.log('');
|
|
517
|
-
|
|
518
|
-
// Variables declared outside block for subsequent Nativefier compilation
|
|
519
|
-
let targetUrl = '';
|
|
520
|
-
let appName = '';
|
|
521
|
-
let iconPath = '';
|
|
522
|
-
let persistSession = true;
|
|
523
|
-
let internalUrlsRegex = '';
|
|
524
|
-
let width = '1200';
|
|
525
|
-
let height = '800';
|
|
526
|
-
let installPath = '';
|
|
527
|
-
|
|
528
|
-
try {
|
|
529
|
-
// 1. Prompt for URL
|
|
530
|
-
let targetUrlInput = '';
|
|
531
|
-
while (!targetUrlInput) {
|
|
532
|
-
targetUrlInput = await ask('Enter Website URL (e.g., https://nextcloud.com)');
|
|
533
|
-
if (!targetUrlInput) {
|
|
534
|
-
log.error('URL cannot be empty. Please try again.');
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Parse details for defaults
|
|
539
|
-
const urlDetails = parseUrlDetails(targetUrlInput);
|
|
540
|
-
targetUrl = urlDetails.cleanUrl;
|
|
541
|
-
|
|
542
|
-
log.info(`Parsed Target URL: ${colors.bright}${targetUrl}${colors.reset}`);
|
|
543
|
-
|
|
544
|
-
// 2. Prompt for Name
|
|
545
|
-
appName = await ask('Enter Application Name', urlDetails.nameDefault);
|
|
546
|
-
|
|
547
|
-
// 3. Prompt for Icon
|
|
548
|
-
const useFavicon = await askYesNo('Download and use website favicon as app icon?', true);
|
|
549
|
-
|
|
550
|
-
if (useFavicon) {
|
|
551
|
-
const tempIconDir = path.join(os.tmpdir(), 'deskify');
|
|
552
|
-
if (!fs.existsSync(tempIconDir)) {
|
|
553
|
-
fs.mkdirSync(tempIconDir, { recursive: true });
|
|
554
|
-
}
|
|
555
|
-
const tempIconPath = path.join(tempIconDir, `${appName.toLowerCase()}_favicon.png`);
|
|
556
|
-
|
|
557
|
-
const success = await downloadFaviconFlow(targetUrl, urlDetails.hostname, tempIconPath);
|
|
558
|
-
|
|
559
|
-
if (success) {
|
|
560
|
-
iconPath = tempIconPath;
|
|
561
|
-
}
|
|
562
|
-
} else {
|
|
563
|
-
const localIcon = await ask('Enter local PNG icon file path (leave empty for none)');
|
|
564
|
-
if (localIcon) {
|
|
565
|
-
const expandedIconPath = expandHome(localIcon);
|
|
566
|
-
if (fs.existsSync(expandedIconPath)) {
|
|
567
|
-
iconPath = expandedIconPath;
|
|
568
|
-
} else {
|
|
569
|
-
log.warn(`File "${localIcon}" not found. Fallback to default Electron icon.`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// 4. Prompt for Persist option
|
|
575
|
-
persistSession = await askYesNo('Persist session cookies, local storage, and cache?', true);
|
|
576
|
-
|
|
577
|
-
// 5. Prompt for Internal URLs Regex
|
|
578
|
-
internalUrlsRegex = await ask('Internal URL pattern regex', urlDetails.regexDefault);
|
|
579
|
-
|
|
580
|
-
// 6. Window Dimensions
|
|
581
|
-
width = await ask('Window width (pixels)', '1200');
|
|
582
|
-
height = await ask('Window height (pixels)', '800');
|
|
583
|
-
|
|
584
|
-
// 7. Installation Path
|
|
585
|
-
const installPathInput = await ask('Installation directory', '~/Apps');
|
|
586
|
-
installPath = expandHome(installPathInput);
|
|
587
|
-
|
|
588
|
-
// Summary before execution
|
|
589
|
-
console.log(`\n${colors.bright}--- Build Configuration Summary ---${colors.reset}`);
|
|
590
|
-
console.log(`App Name: ${colors.green}${appName}${colors.reset}`);
|
|
591
|
-
console.log(`Target URL: ${targetUrl}`);
|
|
592
|
-
console.log(`Persist: ${persistSession ? 'Yes' : 'No'}`);
|
|
593
|
-
console.log(`Internal URLs: ${internalUrlsRegex}`);
|
|
594
|
-
console.log(`Dimensions: ${width}x${height}`);
|
|
595
|
-
console.log(`Install Dir: ${installPath}`);
|
|
596
|
-
if (iconPath) {
|
|
597
|
-
console.log(`Icon Path: ${iconPath}`);
|
|
598
|
-
}
|
|
599
|
-
console.log(`${colors.dim}------------------------------------${colors.reset}\n`);
|
|
600
|
-
|
|
601
|
-
const proceed = await askYesNo('Proceed with build?', true);
|
|
602
|
-
if (!proceed) {
|
|
603
|
-
log.warn('Build aborted by user.');
|
|
604
|
-
return { success: false, cancelled: true };
|
|
605
|
-
}
|
|
606
|
-
} catch (e) {
|
|
607
|
-
if (e.message === 'ESC') {
|
|
608
|
-
log.warn('\nBuild cancelled by user (ESC pressed). Returning to Main Menu...');
|
|
609
|
-
return { success: false, cancelled: true };
|
|
610
|
-
}
|
|
611
|
-
throw e;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Ensure install path exists
|
|
615
|
-
if (!fs.existsSync(installPath)) {
|
|
616
|
-
log.info(`Creating installation directory: ${installPath}`);
|
|
617
|
-
fs.mkdirSync(installPath, { recursive: true });
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// 8. Build using Nativefier
|
|
621
|
-
log.bold('\n[1/4] Running Nativefier compiler...');
|
|
622
|
-
|
|
623
|
-
// Construct arguments matching Nativefier's positional syntax:
|
|
624
|
-
// nativefier <targetUrl> [outputDirectory] [options]
|
|
625
|
-
const args = [
|
|
626
|
-
'nativefier',
|
|
627
|
-
targetUrl,
|
|
628
|
-
installPath,
|
|
629
|
-
'--name', appName,
|
|
630
|
-
'--width', `${width}px`,
|
|
631
|
-
'--height', `${height}px`,
|
|
632
|
-
'--internal-urls', internalUrlsRegex
|
|
633
|
-
];
|
|
634
|
-
|
|
635
|
-
if (persistSession) {
|
|
636
|
-
args.push('--persist');
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
if (iconPath) {
|
|
640
|
-
args.push('--icon', iconPath);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
log.info(`Running command: npx ${args.join(' ')}`);
|
|
644
|
-
|
|
645
|
-
try {
|
|
646
|
-
// Run npx nativefier synchronously, streaming the output to the console
|
|
647
|
-
execSync(`npx -y ${args.map(a => `"${a}"`).join(' ')}`, { stdio: 'inherit' });
|
|
648
|
-
log.success('Nativefier compile completed!');
|
|
649
|
-
} catch (error) {
|
|
650
|
-
log.error('Nativefier failed to compile the application.');
|
|
651
|
-
console.error(error.message);
|
|
652
|
-
return { success: false };
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// 9. Find generated directory
|
|
656
|
-
log.bold('\n[2/4] Locating generated application folder...');
|
|
657
|
-
|
|
658
|
-
// Search for the folder in the install directory. Nativefier outputs folder names like Name-linux-x64
|
|
659
|
-
const files = fs.readdirSync(installPath);
|
|
660
|
-
// Find folder matching appname-linux-x64 (case insensitive search)
|
|
661
|
-
const targetDirName = files.find(f => {
|
|
662
|
-
const isDir = fs.statSync(path.join(installPath, f)).isDirectory();
|
|
663
|
-
return isDir && f.toLowerCase().includes('linux-x64') && f.toLowerCase().includes(appName.toLowerCase());
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
if (!targetDirName) {
|
|
667
|
-
log.error(`Could not locate the generated application directory in ${installPath}.`);
|
|
668
|
-
log.info(`Available folders: ${files.join(', ')}`);
|
|
669
|
-
return { success: false };
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const appFolder = path.join(installPath, targetDirName);
|
|
673
|
-
log.success(`Located folder at: ${appFolder}`);
|
|
674
|
-
|
|
675
|
-
// Copy icon.png if we downloaded it and nativefier didn't put it in the root
|
|
676
|
-
const destIconPath = path.join(appFolder, 'icon.png');
|
|
677
|
-
if (iconPath && !fs.existsSync(destIconPath)) {
|
|
678
|
-
try {
|
|
679
|
-
fs.copyFileSync(iconPath, destIconPath);
|
|
680
|
-
log.info(`Copied application icon to: ${destIconPath}`);
|
|
681
|
-
} catch (e) {
|
|
682
|
-
log.warn(`Could not copy icon to root: ${e.message}`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Adjust permissions on the app directory and icon file to ensure GNOME can load them
|
|
687
|
-
try {
|
|
688
|
-
fs.chmodSync(appFolder, 0o755);
|
|
689
|
-
if (fs.existsSync(destIconPath)) {
|
|
690
|
-
fs.chmodSync(destIconPath, 0o644);
|
|
691
|
-
|
|
692
|
-
// Auto-resize extremely large custom icons to 256x256 using convert (ImageMagick)
|
|
693
|
-
try {
|
|
694
|
-
execSync('command -v convert', { stdio: 'ignore' });
|
|
695
|
-
log.info('Optimizing icon resolution (resizing to 256x256)...');
|
|
696
|
-
execSync(`convert "${destIconPath}" -resize 256x256 "${destIconPath}"`, { stdio: 'ignore' });
|
|
697
|
-
log.success('Icon optimized and resized successfully!');
|
|
698
|
-
} catch (e) {
|
|
699
|
-
// Skip optimization if convert is not installed
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
} catch (e) {
|
|
703
|
-
log.warn(`Could not adjust permissions on folder/icon: ${e.message}`);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// 10. Dynamic StartupWMClass resolver by reading package.json
|
|
707
|
-
log.bold('\n[3/4] Resolving Linux window class (StartupWMClass)...');
|
|
708
|
-
|
|
709
|
-
const pkgPath = path.join(appFolder, 'resources', 'app', 'package.json');
|
|
710
|
-
let startupWMClass = appName; // Default fallback
|
|
711
|
-
let packageJsonName = '';
|
|
712
|
-
|
|
713
|
-
if (fs.existsSync(pkgPath)) {
|
|
714
|
-
try {
|
|
715
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
716
|
-
packageJsonName = pkg.name; // e.g. nextcloud-nativefier-7cf0d6
|
|
717
|
-
log.info(`Found internal app ID in package.json: ${colors.bright}${packageJsonName}${colors.reset}`);
|
|
718
|
-
|
|
719
|
-
// Use the exact packageJsonName directly since Electron's window class instance name on Linux
|
|
720
|
-
// always matches the lowercase 'name' field in package.json.
|
|
721
|
-
startupWMClass = packageJsonName;
|
|
722
|
-
log.success(`Calculated StartupWMClass: ${colors.bright}${startupWMClass}${colors.reset}`);
|
|
723
|
-
} catch (e) {
|
|
724
|
-
log.warn(`Could not parse internal package.json: ${e.message}. Using app name fallback.`);
|
|
725
|
-
}
|
|
726
|
-
} else {
|
|
727
|
-
log.warn('Could not find internal package.json. StartupWMClass might be inaccurate.');
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// 11. Fix chrome-sandbox permissions via sudo
|
|
731
|
-
log.bold('\n[4/4] Setting up sandbox execution permissions (requires sudo)...');
|
|
732
|
-
const chromeSandboxPath = path.join(appFolder, 'chrome-sandbox');
|
|
733
|
-
|
|
734
|
-
if (fs.existsSync(chromeSandboxPath)) {
|
|
735
|
-
log.info(`Applying root ownership and SUID bit to: ${chromeSandboxPath}`);
|
|
736
|
-
try {
|
|
737
|
-
// Execute permissions fix. stdio: 'inherit' displays password prompt nicely
|
|
738
|
-
execSync(`sudo chown root:root "${chromeSandboxPath}" && sudo chmod 4755 "${chromeSandboxPath}"`, { stdio: 'inherit' });
|
|
739
|
-
log.success('Sandbox permissions applied successfully!');
|
|
740
|
-
} catch (e) {
|
|
741
|
-
log.error('Failed to configure chrome-sandbox permissions.');
|
|
742
|
-
log.info('You may need to run this command manually:');
|
|
743
|
-
console.log(` sudo chown root:root "${chromeSandboxPath}" && sudo chmod 4755 "${chromeSandboxPath}"`);
|
|
744
|
-
}
|
|
745
|
-
} else {
|
|
746
|
-
log.warn('chrome-sandbox file not found in build directory. Skipping permission adjustment.');
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// 12. Create Desktop File
|
|
750
|
-
log.bold('\n[5/5] Generating Desktop Shortcut launcher...');
|
|
751
|
-
|
|
752
|
-
const desktopEntriesDir = expandHome('~/.local/share/applications');
|
|
753
|
-
if (!fs.existsSync(desktopEntriesDir)) {
|
|
754
|
-
fs.mkdirSync(desktopEntriesDir, { recursive: true });
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const desktopFilePath = path.join(desktopEntriesDir, `${appName.replace(/\s+/g, '')}.desktop`);
|
|
758
|
-
|
|
759
|
-
// Locate binary executable (usually named after the AppName, case-sensitive)
|
|
760
|
-
const execPath = path.join(appFolder, appName);
|
|
761
|
-
// Locate icon path. Use root icon.png if exists, otherwise fallback to app folder
|
|
762
|
-
const finalIconPath = fs.existsSync(destIconPath) ? destIconPath : (iconPath || 'electron');
|
|
763
|
-
|
|
764
|
-
const desktopContent = `[Desktop Entry]
|
|
765
|
-
Name=${appName}
|
|
766
|
-
Comment=${appName} Web Desktop App
|
|
767
|
-
Exec=${execPath}
|
|
768
|
-
Icon=${finalIconPath}
|
|
769
|
-
Terminal=false
|
|
770
|
-
Type=Application
|
|
771
|
-
Categories=Network;WebBrowser;Application;
|
|
772
|
-
StartupWMClass=${startupWMClass}
|
|
773
|
-
X-Generated-By=deskify
|
|
774
|
-
`;
|
|
775
|
-
|
|
776
|
-
try {
|
|
777
|
-
fs.writeFileSync(desktopFilePath, desktopContent, 'utf8');
|
|
778
|
-
fs.chmodSync(desktopFilePath, 0o755); // make executable
|
|
779
|
-
log.success(`Shortcut created successfully at: ${desktopFilePath}`);
|
|
780
|
-
|
|
781
|
-
// Update desktop registry database
|
|
782
|
-
if (hasDesktopUtils) {
|
|
783
|
-
log.info('Updating local desktop shortcut registry database...');
|
|
784
|
-
execSync(`update-desktop-database "${desktopEntriesDir}"`, { stdio: 'ignore' });
|
|
785
|
-
log.success('Desktop shortcuts database updated!');
|
|
786
|
-
}
|
|
787
|
-
} catch (e) {
|
|
788
|
-
log.error(`Failed to write desktop launcher: ${e.message}`);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Clean up temporary icons if any
|
|
792
|
-
if (useFavicon && iconPath && fs.existsSync(iconPath)) {
|
|
793
|
-
try {
|
|
794
|
-
fs.unlinkSync(iconPath);
|
|
795
|
-
} catch (e) {}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
// Success summary
|
|
799
|
-
console.log(`\n${colors.green}${colors.bright}==================================================${colors.reset}`);
|
|
800
|
-
console.log(`${colors.green}${colors.bright} š CONGRATULATIONS! APP CREATED SUCCESSFULLY š${colors.reset}`);
|
|
801
|
-
console.log(`${colors.green}${colors.bright}==================================================${colors.reset}`);
|
|
802
|
-
console.log(`App Name: ${colors.bright}${appName}${colors.reset}`);
|
|
803
|
-
console.log(`Folder Path: ${colors.cyan}${appFolder}${colors.reset}`);
|
|
804
|
-
console.log(`Shortcut Path: ${colors.cyan}${desktopFilePath}${colors.reset}`);
|
|
805
|
-
console.log(`WM_Class ID: ${colors.magenta}${startupWMClass}${colors.reset}`);
|
|
806
|
-
console.log(`\nYou can now search for "${appName}" in your Linux App menu! š\n`);
|
|
807
|
-
return { success: true };
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Interactive Uninstallation flow
|
|
811
|
-
async function uninstallFlow(hasDesktopUtils) {
|
|
812
|
-
console.clear();
|
|
813
|
-
printHeader();
|
|
814
|
-
log.bold('--- Uninstall an Existing Application ---');
|
|
815
|
-
console.log('');
|
|
816
|
-
const desktopEntriesDir = expandHome('~/.local/share/applications');
|
|
817
|
-
|
|
818
|
-
if (!fs.existsSync(desktopEntriesDir)) {
|
|
819
|
-
log.warn('No local application shortcuts directory found.');
|
|
820
|
-
return { success: false };
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
const files = fs.readdirSync(desktopEntriesDir);
|
|
824
|
-
const deskifyApps = [];
|
|
825
|
-
|
|
826
|
-
for (const file of files) {
|
|
827
|
-
if (file.endsWith('.desktop')) {
|
|
828
|
-
const filePath = path.join(desktopEntriesDir, file);
|
|
829
|
-
try {
|
|
830
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
831
|
-
if (content.includes('X-Generated-By=deskify')) {
|
|
832
|
-
// Parse metadata
|
|
833
|
-
const nameMatch = content.match(/^Name=(.+)$/m);
|
|
834
|
-
const execMatch = content.match(/^Exec=(.+)$/m);
|
|
835
|
-
const appName = nameMatch ? nameMatch[1].trim() : file.replace('.desktop', '');
|
|
836
|
-
let execPath = execMatch ? execMatch[1].trim() : '';
|
|
837
|
-
|
|
838
|
-
// Strip wrapping quotes from Exec path if present
|
|
839
|
-
if (execPath.startsWith('"') && execPath.endsWith('"')) {
|
|
840
|
-
execPath = execPath.slice(1, -1);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
let appFolder = '';
|
|
844
|
-
if (execPath) {
|
|
845
|
-
appFolder = path.dirname(execPath);
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
deskifyApps.push({
|
|
849
|
-
file,
|
|
850
|
-
filePath,
|
|
851
|
-
appName,
|
|
852
|
-
appFolder
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
} catch (e) {
|
|
856
|
-
// Skip files that can't be read
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
if (deskifyApps.length === 0) {
|
|
862
|
-
log.info('No applications generated by deskify were found.');
|
|
863
|
-
return { success: false };
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const appOptions = deskifyApps.map(app => `${app.appName} ${colors.dim}(Shortcut: ${app.file})${colors.reset}`);
|
|
867
|
-
appOptions.push('Cancel');
|
|
868
|
-
|
|
869
|
-
const selectedIdx = await selectOption('Select an application to uninstall', appOptions, appOptions.length - 1);
|
|
870
|
-
|
|
871
|
-
if (selectedIdx === appOptions.length - 1) {
|
|
872
|
-
log.info('Uninstallation cancelled.');
|
|
873
|
-
return { success: false, cancelled: true };
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
const targetApp = deskifyApps[selectedIdx];
|
|
877
|
-
|
|
878
|
-
log.warn(`\nAre you sure you want to permanently delete ${colors.red}${targetApp.appName}${colors.reset}?`);
|
|
879
|
-
log.warn(`This will delete:`);
|
|
880
|
-
log.warn(` - Shortcut: ${targetApp.filePath}`);
|
|
881
|
-
if (targetApp.appFolder && fs.existsSync(targetApp.appFolder)) {
|
|
882
|
-
log.warn(` - App Folder: ${targetApp.appFolder}`);
|
|
883
|
-
}
|
|
884
|
-
console.log('');
|
|
885
|
-
|
|
886
|
-
try {
|
|
887
|
-
const confirm = await askYesNo('Confirm uninstallation?', false);
|
|
888
|
-
if (!confirm) {
|
|
889
|
-
log.info('Uninstallation cancelled.');
|
|
890
|
-
return { success: false, cancelled: true };
|
|
891
|
-
}
|
|
892
|
-
} catch (e) {
|
|
893
|
-
if (e.message === 'ESC') {
|
|
894
|
-
log.warn('\nUninstallation cancelled by user (ESC pressed). Returning to Main Menu...');
|
|
895
|
-
return { success: false, cancelled: true };
|
|
896
|
-
}
|
|
897
|
-
throw e;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// 1. Delete .desktop entry file
|
|
901
|
-
try {
|
|
902
|
-
if (fs.existsSync(targetApp.filePath)) {
|
|
903
|
-
fs.unlinkSync(targetApp.filePath);
|
|
904
|
-
log.success(`Deleted shortcut file: ${targetApp.file}`);
|
|
905
|
-
}
|
|
906
|
-
} catch (e) {
|
|
907
|
-
log.error(`Failed to delete shortcut file: ${e.message}`);
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
// 2. Delete the application installation directory recursively
|
|
911
|
-
if (targetApp.appFolder && fs.existsSync(targetApp.appFolder)) {
|
|
912
|
-
log.info(`Deleting application files at: ${targetApp.appFolder}...`);
|
|
913
|
-
try {
|
|
914
|
-
fs.rmSync(targetApp.appFolder, { recursive: true, force: true });
|
|
915
|
-
log.success('Application directory deleted successfully!');
|
|
916
|
-
} catch (e) {
|
|
917
|
-
log.error(`Failed to delete directory: ${e.message}`);
|
|
918
|
-
log.info('You may need to run this command manually as root/sudo:');
|
|
919
|
-
console.log(` sudo rm -rf "${targetApp.appFolder}"`);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// 3. Update local desktop registry database
|
|
924
|
-
if (hasDesktopUtils) {
|
|
925
|
-
log.info('Updating desktop shortcuts registry database...');
|
|
926
|
-
try {
|
|
927
|
-
execSync(`update-desktop-database "${desktopEntriesDir}"`, { stdio: 'ignore' });
|
|
928
|
-
log.success('Desktop shortcuts database updated!');
|
|
929
|
-
} catch (e) {}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
log.success(`\nš ${targetApp.appName} has been uninstalled successfully! šļø\n`);
|
|
933
|
-
return { success: true };
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// Helper to wait for enter key, allowing ESC to also go back
|
|
937
|
-
async function waitForKey() {
|
|
938
|
-
try {
|
|
939
|
-
await ask('Press Enter to return to the Main Menu...');
|
|
940
|
-
} catch (e) {
|
|
941
|
-
// Silently consume ESC or any other rejection to return to main menu
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Main execution function
|
|
946
63
|
async function main() {
|
|
947
64
|
while (true) {
|
|
948
65
|
printHeader();
|
|
@@ -962,7 +79,7 @@ async function main() {
|
|
|
962
79
|
if (result && result.success) {
|
|
963
80
|
await waitForKey();
|
|
964
81
|
} else if (result && result.cancelled) {
|
|
965
|
-
//
|
|
82
|
+
// Return to main menu directly
|
|
966
83
|
} else {
|
|
967
84
|
await waitForKey();
|
|
968
85
|
}
|
|
@@ -977,7 +94,7 @@ async function main() {
|
|
|
977
94
|
if (result && result.success) {
|
|
978
95
|
await waitForKey();
|
|
979
96
|
} else if (result && result.cancelled) {
|
|
980
|
-
//
|
|
97
|
+
// Return to main menu directly
|
|
981
98
|
} else {
|
|
982
99
|
await waitForKey();
|
|
983
100
|
}
|
|
@@ -993,7 +110,6 @@ async function main() {
|
|
|
993
110
|
}
|
|
994
111
|
}
|
|
995
112
|
|
|
996
|
-
// Run main program
|
|
997
113
|
main().catch((err) => {
|
|
998
114
|
log.error('An unexpected error occurred during execution:');
|
|
999
115
|
console.error(err);
|