deskify-cli 1.0.0 → 1.0.2
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 +34 -24
- 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/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deskify-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "An interactive, zero-dependency CLI tool to turn web pages into Linux desktop applications using Nativefier.",
|
|
5
5
|
"main": "deskify.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"deskify": "
|
|
7
|
+
"deskify": "deskify.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node deskify.js"
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* create.js
|
|
3
|
+
* Application packaging build flow for the deskify CLI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
const { colors, log } = require('../utils/colors');
|
|
11
|
+
const { expandHome, ask, askYesNo } = require('../utils/prompts');
|
|
12
|
+
const { parseUrlDetails, downloadFaviconFlow } = require('../utils/favicon');
|
|
13
|
+
|
|
14
|
+
function printHeader() {
|
|
15
|
+
console.clear();
|
|
16
|
+
console.log(`${colors.cyan}${colors.bright}`);
|
|
17
|
+
console.log(` _ _ _ __ `);
|
|
18
|
+
console.log(` __| | ___ ___| |_( )/ _|_ _ `);
|
|
19
|
+
console.log(` / _\` |/ _ \\/ __| |/ /| |_| | | |`);
|
|
20
|
+
console.log(` | (_| | __/\\__ \\ <| _| |_| |`);
|
|
21
|
+
console.log(` \\__,_|\\___||___/_|\\_\\\\_| \\__, |`);
|
|
22
|
+
console.log(` |___/ `);
|
|
23
|
+
console.log(` Web to Linux Desktop App Packager${colors.reset}`);
|
|
24
|
+
console.log(`${colors.dim}--------------------------------------------------${colors.reset}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function createFlow(hasDesktopUtils) {
|
|
28
|
+
console.clear();
|
|
29
|
+
printHeader();
|
|
30
|
+
log.bold('--- Create a New Desktop Application ---');
|
|
31
|
+
console.log('');
|
|
32
|
+
|
|
33
|
+
let targetUrl = '';
|
|
34
|
+
let appName = '';
|
|
35
|
+
let iconPath = '';
|
|
36
|
+
let persistSession = true;
|
|
37
|
+
let internalUrlsRegex = '';
|
|
38
|
+
let width = '1200';
|
|
39
|
+
let height = '800';
|
|
40
|
+
let installPath = '';
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
let targetUrlInput = '';
|
|
44
|
+
while (!targetUrlInput) {
|
|
45
|
+
targetUrlInput = await ask('Enter Website URL (e.g., https://nextcloud.com)');
|
|
46
|
+
if (!targetUrlInput) {
|
|
47
|
+
log.error('URL cannot be empty. Please try again.');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const urlDetails = parseUrlDetails(targetUrlInput);
|
|
52
|
+
targetUrl = urlDetails.cleanUrl;
|
|
53
|
+
|
|
54
|
+
log.info(`Parsed Target URL: ${colors.bright}${targetUrl}${colors.reset}`);
|
|
55
|
+
|
|
56
|
+
appName = await ask('Enter Application Name', urlDetails.nameDefault);
|
|
57
|
+
|
|
58
|
+
const useFavicon = await askYesNo('Download and use website favicon as app icon?', true);
|
|
59
|
+
|
|
60
|
+
if (useFavicon) {
|
|
61
|
+
const tempIconDir = path.join(os.tmpdir(), 'deskify');
|
|
62
|
+
if (!fs.existsSync(tempIconDir)) {
|
|
63
|
+
fs.mkdirSync(tempIconDir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
const tempIconPath = path.join(tempIconDir, `${appName.toLowerCase()}_favicon.png`);
|
|
66
|
+
|
|
67
|
+
const success = await downloadFaviconFlow(targetUrl, urlDetails.hostname, tempIconPath);
|
|
68
|
+
|
|
69
|
+
if (success) {
|
|
70
|
+
iconPath = tempIconPath;
|
|
71
|
+
}
|
|
72
|
+
} else {
|
|
73
|
+
const localIcon = await ask('Enter local PNG icon file path (leave empty for none)');
|
|
74
|
+
if (localIcon) {
|
|
75
|
+
const expandedIconPath = expandHome(localIcon);
|
|
76
|
+
if (fs.existsSync(expandedIconPath)) {
|
|
77
|
+
iconPath = expandedIconPath;
|
|
78
|
+
} else {
|
|
79
|
+
log.warn(`File "${localIcon}" not found. Fallback to default Electron icon.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
persistSession = await askYesNo('Persist session cookies, local storage, and cache?', true);
|
|
85
|
+
internalUrlsRegex = await ask('Internal URL pattern regex', urlDetails.regexDefault);
|
|
86
|
+
width = await ask('Window width (pixels)', '1200');
|
|
87
|
+
height = await ask('Window height (pixels)', '800');
|
|
88
|
+
|
|
89
|
+
const installPathInput = await ask('Installation directory', '~/Apps');
|
|
90
|
+
installPath = expandHome(installPathInput);
|
|
91
|
+
|
|
92
|
+
console.log(`\n${colors.bright}--- Build Configuration Summary ---${colors.reset}`);
|
|
93
|
+
console.log(`App Name: ${colors.green}${appName}${colors.reset}`);
|
|
94
|
+
console.log(`Target URL: ${targetUrl}`);
|
|
95
|
+
console.log(`Persist: ${persistSession ? 'Yes' : 'No'}`);
|
|
96
|
+
console.log(`Internal URLs: ${internalUrlsRegex}`);
|
|
97
|
+
console.log(`Dimensions: ${width}x${height}`);
|
|
98
|
+
console.log(`Install Dir: ${installPath}`);
|
|
99
|
+
if (iconPath) {
|
|
100
|
+
console.log(`Icon Path: ${iconPath}`);
|
|
101
|
+
}
|
|
102
|
+
console.log(`${colors.dim}------------------------------------${colors.reset}\n`);
|
|
103
|
+
|
|
104
|
+
const proceed = await askYesNo('Proceed with build?', true);
|
|
105
|
+
if (!proceed) {
|
|
106
|
+
log.warn('Build aborted by user.');
|
|
107
|
+
return { success: false, cancelled: true };
|
|
108
|
+
}
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (e.message === 'ESC') {
|
|
111
|
+
log.warn('\nBuild cancelled by user (ESC pressed). Returning to Main Menu...');
|
|
112
|
+
return { success: false, cancelled: true };
|
|
113
|
+
}
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!fs.existsSync(installPath)) {
|
|
118
|
+
log.info(`Creating installation directory: ${installPath}`);
|
|
119
|
+
fs.mkdirSync(installPath, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
log.bold('\n[1/4] Running Nativefier compiler...');
|
|
123
|
+
|
|
124
|
+
const args = [
|
|
125
|
+
'nativefier',
|
|
126
|
+
targetUrl,
|
|
127
|
+
installPath,
|
|
128
|
+
'--name', appName,
|
|
129
|
+
'--width', `${width}px`,
|
|
130
|
+
'--height', `${height}px`,
|
|
131
|
+
'--internal-urls', internalUrlsRegex
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
if (persistSession) {
|
|
135
|
+
args.push('--persist');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (iconPath) {
|
|
139
|
+
args.push('--icon', iconPath);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
log.info(`Running command: npx ${args.join(' ')}`);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
execSync(`npx -y ${args.map(a => `"${a}"`).join(' ')}`, { stdio: 'inherit' });
|
|
146
|
+
log.success('Nativefier compile completed!');
|
|
147
|
+
} catch (error) {
|
|
148
|
+
log.error('Nativefier failed to compile the application.');
|
|
149
|
+
console.error(error.message);
|
|
150
|
+
return { success: false };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
log.bold('\n[2/4] Locating generated application folder...');
|
|
154
|
+
const files = fs.readdirSync(installPath);
|
|
155
|
+
const targetDirName = files.find(f => {
|
|
156
|
+
const isDir = fs.statSync(path.join(installPath, f)).isDirectory();
|
|
157
|
+
return isDir && f.toLowerCase().includes('linux-x64') && f.toLowerCase().includes(appName.toLowerCase());
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!targetDirName) {
|
|
161
|
+
log.error(`Could not locate the generated application directory in ${installPath}.`);
|
|
162
|
+
log.info(`Available folders: ${files.join(', ')}`);
|
|
163
|
+
return { success: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const appFolder = path.join(installPath, targetDirName);
|
|
167
|
+
log.success(`Located folder at: ${appFolder}`);
|
|
168
|
+
|
|
169
|
+
const destIconPath = path.join(appFolder, 'icon.png');
|
|
170
|
+
if (iconPath && !fs.existsSync(destIconPath)) {
|
|
171
|
+
try {
|
|
172
|
+
fs.copyFileSync(iconPath, destIconPath);
|
|
173
|
+
log.info(`Copied application icon to: ${destIconPath}`);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
log.warn(`Could not copy icon to root: ${e.message}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
fs.chmodSync(appFolder, 0o755);
|
|
181
|
+
if (fs.existsSync(destIconPath)) {
|
|
182
|
+
fs.chmodSync(destIconPath, 0o644);
|
|
183
|
+
try {
|
|
184
|
+
execSync('command -v convert', { stdio: 'ignore' });
|
|
185
|
+
log.info('Optimizing icon resolution (resizing to 256x256)...');
|
|
186
|
+
execSync(`convert "${destIconPath}" -resize 256x256 "${destIconPath}"`, { stdio: 'ignore' });
|
|
187
|
+
log.success('Icon optimized and resized successfully!');
|
|
188
|
+
} catch (e) {}
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
log.warn(`Could not adjust permissions on folder/icon: ${e.message}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
log.bold('\n[3/4] Resolving Linux window class (StartupWMClass)...');
|
|
195
|
+
const pkgPath = path.join(appFolder, 'resources', 'app', 'package.json');
|
|
196
|
+
let startupWMClass = appName;
|
|
197
|
+
let packageJsonName = '';
|
|
198
|
+
|
|
199
|
+
if (fs.existsSync(pkgPath)) {
|
|
200
|
+
try {
|
|
201
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
202
|
+
packageJsonName = pkg.name;
|
|
203
|
+
log.info(`Found internal app ID in package.json: ${colors.bright}${packageJsonName}${colors.reset}`);
|
|
204
|
+
startupWMClass = packageJsonName;
|
|
205
|
+
log.success(`Calculated StartupWMClass: ${colors.bright}${startupWMClass}${colors.reset}`);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
log.warn(`Could not parse internal package.json: ${e.message}. Using app name fallback.`);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
log.warn('Could not find internal package.json. StartupWMClass might be inaccurate.');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
log.bold('\n[4/4] Setting up sandbox execution permissions (requires sudo)...');
|
|
214
|
+
const chromeSandboxPath = path.join(appFolder, 'chrome-sandbox');
|
|
215
|
+
if (fs.existsSync(chromeSandboxPath)) {
|
|
216
|
+
log.info(`Applying root ownership and SUID bit to: ${chromeSandboxPath}`);
|
|
217
|
+
try {
|
|
218
|
+
execSync(`sudo chown root:root "${chromeSandboxPath}" && sudo chmod 4755 "${chromeSandboxPath}"`, { stdio: 'inherit' });
|
|
219
|
+
log.success('Sandbox permissions applied successfully!');
|
|
220
|
+
} catch (e) {
|
|
221
|
+
log.error('Failed to configure chrome-sandbox permissions.');
|
|
222
|
+
log.info('You may need to run this command manually:');
|
|
223
|
+
console.log(` sudo chown root:root "${chromeSandboxPath}" && sudo chmod 4755 "${chromeSandboxPath}"`);
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
log.warn('chrome-sandbox file not found in build directory. Skipping permission adjustment.');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
log.bold('\n[5/5] Generating Desktop Shortcut launcher...');
|
|
230
|
+
const desktopEntriesDir = expandHome('~/.local/share/applications');
|
|
231
|
+
if (!fs.existsSync(desktopEntriesDir)) {
|
|
232
|
+
fs.mkdirSync(desktopEntriesDir, { recursive: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const desktopFilePath = path.join(desktopEntriesDir, `${appName.replace(/\s+/g, '')}.desktop`);
|
|
236
|
+
const execPath = path.join(appFolder, appName);
|
|
237
|
+
const finalIconPath = fs.existsSync(destIconPath) ? destIconPath : (iconPath || 'electron');
|
|
238
|
+
|
|
239
|
+
const desktopContent = `[Desktop Entry]
|
|
240
|
+
Name=${appName}
|
|
241
|
+
Comment=${appName} Web Desktop App
|
|
242
|
+
Exec=${execPath}
|
|
243
|
+
Icon=${finalIconPath}
|
|
244
|
+
Terminal=false
|
|
245
|
+
Type=Application
|
|
246
|
+
Categories=Network;WebBrowser;Application;
|
|
247
|
+
StartupWMClass=${startupWMClass}
|
|
248
|
+
X-Generated-By=deskify
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
fs.writeFileSync(desktopFilePath, desktopContent, 'utf8');
|
|
253
|
+
fs.chmodSync(desktopFilePath, 0o755);
|
|
254
|
+
log.success(`Shortcut created successfully at: ${desktopFilePath}`);
|
|
255
|
+
|
|
256
|
+
if (hasDesktopUtils) {
|
|
257
|
+
log.info('Updating local desktop shortcut registry database...');
|
|
258
|
+
execSync(`update-desktop-database "${desktopEntriesDir}"`, { stdio: 'ignore' });
|
|
259
|
+
log.success('Desktop shortcuts database updated!');
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
log.error(`Failed to write desktop launcher: ${e.message}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (useFavicon && iconPath && fs.existsSync(iconPath)) {
|
|
266
|
+
try {
|
|
267
|
+
fs.unlinkSync(iconPath);
|
|
268
|
+
} catch (e) {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
console.log(`\n${colors.green}${colors.bright}==================================================${colors.reset}`);
|
|
272
|
+
console.log(`${colors.green}${colors.bright} 🎉 CONGRATULATIONS! APP CREATED SUCCESSFULLY 🎉${colors.reset}`);
|
|
273
|
+
console.log(`${colors.green}${colors.bright}==================================================${colors.reset}`);
|
|
274
|
+
console.log(`App Name: ${colors.bright}${appName}${colors.reset}`);
|
|
275
|
+
console.log(`Folder Path: ${colors.cyan}${appFolder}${colors.reset}`);
|
|
276
|
+
console.log(`Shortcut Path: ${colors.cyan}${desktopFilePath}${colors.reset}`);
|
|
277
|
+
console.log(`WM_Class ID: ${colors.magenta}${startupWMClass}${colors.reset}`);
|
|
278
|
+
console.log(`\nYou can now search for "${appName}" in your Linux App menu! 🚀\n`);
|
|
279
|
+
return { success: true };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
createFlow
|
|
284
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uninstall.js
|
|
3
|
+
* Application uninstallation flow for the deskify CLI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const { colors, log } = require('../utils/colors');
|
|
10
|
+
const { expandHome, selectOption, askYesNo } = require('../utils/prompts');
|
|
11
|
+
|
|
12
|
+
function printHeader() {
|
|
13
|
+
console.clear();
|
|
14
|
+
console.log(`${colors.cyan}${colors.bright}`);
|
|
15
|
+
console.log(` _ _ _ __ `);
|
|
16
|
+
console.log(` __| | ___ ___| |_( )/ _|_ _ `);
|
|
17
|
+
console.log(` / _\` |/ _ \\/ __| |/ /| |_| | | |`);
|
|
18
|
+
console.log(` | (_| | __/\\__ \\ <| _| |_| |`);
|
|
19
|
+
console.log(` \\__,_|\\___||___/_|\\_\\\\_| \\__, |`);
|
|
20
|
+
console.log(` |___/ `);
|
|
21
|
+
console.log(` Web to Linux Desktop App Packager${colors.reset}`);
|
|
22
|
+
console.log(`${colors.dim}--------------------------------------------------${colors.reset}\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function uninstallFlow(hasDesktopUtils) {
|
|
26
|
+
console.clear();
|
|
27
|
+
printHeader();
|
|
28
|
+
log.bold('--- Uninstall an Existing Application ---');
|
|
29
|
+
console.log('');
|
|
30
|
+
const desktopEntriesDir = expandHome('~/.local/share/applications');
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(desktopEntriesDir)) {
|
|
33
|
+
log.warn('No local application shortcuts directory found.');
|
|
34
|
+
return { success: false };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const files = fs.readdirSync(desktopEntriesDir);
|
|
38
|
+
const deskifyApps = [];
|
|
39
|
+
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
if (file.endsWith('.desktop')) {
|
|
42
|
+
const filePath = path.join(desktopEntriesDir, file);
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
if (content.includes('X-Generated-By=deskify')) {
|
|
46
|
+
const nameMatch = content.match(/^Name=(.+)$/m);
|
|
47
|
+
const execMatch = content.match(/^Exec=(.+)$/m);
|
|
48
|
+
const appName = nameMatch ? nameMatch[1].trim() : file.replace('.desktop', '');
|
|
49
|
+
let execPath = execMatch ? execMatch[1].trim() : '';
|
|
50
|
+
|
|
51
|
+
if (execPath.startsWith('"') && execPath.endsWith('"')) {
|
|
52
|
+
execPath = execPath.slice(1, -1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let appFolder = '';
|
|
56
|
+
if (execPath) {
|
|
57
|
+
appFolder = path.dirname(execPath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
deskifyApps.push({
|
|
61
|
+
file,
|
|
62
|
+
filePath,
|
|
63
|
+
appName,
|
|
64
|
+
appFolder
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (deskifyApps.length === 0) {
|
|
72
|
+
log.info('No applications generated by deskify were found.');
|
|
73
|
+
return { success: false };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const appOptions = deskifyApps.map(app => `${app.appName} ${colors.dim}(Shortcut: ${app.file})${colors.reset}`);
|
|
77
|
+
appOptions.push('Cancel');
|
|
78
|
+
|
|
79
|
+
const selectedIdx = await selectOption('Select an application to uninstall', appOptions, appOptions.length - 1);
|
|
80
|
+
|
|
81
|
+
if (selectedIdx === appOptions.length - 1) {
|
|
82
|
+
log.info('Uninstallation cancelled.');
|
|
83
|
+
return { success: false, cancelled: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const targetApp = deskifyApps[selectedIdx];
|
|
87
|
+
|
|
88
|
+
log.warn(`\nAre you sure you want to permanently delete ${colors.red}${targetApp.appName}${colors.reset}?`);
|
|
89
|
+
log.warn('This will delete:');
|
|
90
|
+
log.warn(` - Shortcut: ${targetApp.filePath}`);
|
|
91
|
+
if (targetApp.appFolder && fs.existsSync(targetApp.appFolder)) {
|
|
92
|
+
log.warn(` - App Folder: ${targetApp.appFolder}`);
|
|
93
|
+
}
|
|
94
|
+
console.log('');
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const confirm = await askYesNo('Confirm uninstallation?', false);
|
|
98
|
+
if (!confirm) {
|
|
99
|
+
log.info('Uninstallation cancelled.');
|
|
100
|
+
return { success: false, cancelled: true };
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
if (e.message === 'ESC') {
|
|
104
|
+
log.warn('\nUninstallation cancelled by user (ESC pressed). Returning to Main Menu...');
|
|
105
|
+
return { success: false, cancelled: true };
|
|
106
|
+
}
|
|
107
|
+
throw e;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(targetApp.filePath)) {
|
|
112
|
+
fs.unlinkSync(targetApp.filePath);
|
|
113
|
+
log.success(`Deleted shortcut file: ${targetApp.file}`);
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
log.error(`Failed to delete shortcut file: ${e.message}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (targetApp.appFolder && fs.existsSync(targetApp.appFolder)) {
|
|
120
|
+
log.info(`Deleting application files at: ${targetApp.appFolder}...`);
|
|
121
|
+
try {
|
|
122
|
+
fs.rmSync(targetApp.appFolder, { recursive: true, force: true });
|
|
123
|
+
log.success('Application directory deleted successfully!');
|
|
124
|
+
} catch (e) {
|
|
125
|
+
log.error(`Failed to delete directory: ${e.message}`);
|
|
126
|
+
log.info('You may need to run this command manually as root/sudo:');
|
|
127
|
+
console.log(` sudo rm -rf "${targetApp.appFolder}"`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (hasDesktopUtils) {
|
|
132
|
+
log.info('Updating desktop shortcuts registry database...');
|
|
133
|
+
try {
|
|
134
|
+
execSync(`update-desktop-database "${desktopEntriesDir}"`, { stdio: 'ignore' });
|
|
135
|
+
log.success('Desktop shortcuts database updated!');
|
|
136
|
+
} catch (e) {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log.success(`\n🎉 ${targetApp.appName} has been uninstalled successfully! 🗑️\n`);
|
|
140
|
+
return { success: true };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
uninstallFlow
|
|
145
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* colors.js
|
|
3
|
+
* Formatting tokens and utility outputs for the deskify CLI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const colors = {
|
|
7
|
+
reset: '\x1b[0m',
|
|
8
|
+
bright: '\x1b[1m',
|
|
9
|
+
dim: '\x1b[2m',
|
|
10
|
+
underline: '\x1b[4m',
|
|
11
|
+
blink: '\x1b[5m',
|
|
12
|
+
reverse: '\x1b[7m',
|
|
13
|
+
hidden: '\x1b[8m',
|
|
14
|
+
|
|
15
|
+
black: '\x1b[30m',
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
green: '\x1b[32m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
blue: '\x1b[34m',
|
|
20
|
+
magenta: '\x1b[35m',
|
|
21
|
+
cyan: '\x1b[36m',
|
|
22
|
+
white: '\x1b[37m',
|
|
23
|
+
|
|
24
|
+
bgBlack: '\x1b[40m',
|
|
25
|
+
bgRed: '\x1b[41m',
|
|
26
|
+
bgGreen: '\x1b[42m',
|
|
27
|
+
bgYellow: '\x1b[43m',
|
|
28
|
+
bgBlue: '\x1b[44m',
|
|
29
|
+
bgMagenta: '\x1b[45m',
|
|
30
|
+
bgCyan: '\x1b[46m',
|
|
31
|
+
bgWhite: '\x1b[47m'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const log = {
|
|
35
|
+
info: (msg) => console.log(`${colors.cyan}info:${colors.reset} ${msg}`),
|
|
36
|
+
success: (msg) => console.log(`${colors.green}success:${colors.reset} ${colors.bright}${msg}${colors.reset}`),
|
|
37
|
+
warn: (msg) => console.log(`${colors.yellow}warning:${colors.reset} ${msg}`),
|
|
38
|
+
error: (msg) => console.error(`${colors.red}error:${colors.reset} ${colors.bright}${msg}${colors.reset}`),
|
|
39
|
+
bold: (msg) => console.log(`${colors.bright}${msg}${colors.reset}`),
|
|
40
|
+
accent: (msg) => console.log(`${colors.magenta}${msg}${colors.reset}`)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
colors,
|
|
45
|
+
log
|
|
46
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* favicon.js
|
|
3
|
+
* Scrapes and downloads high-quality favicons for the deskify CLI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const https = require('https');
|
|
9
|
+
const { colors } = require('./colors');
|
|
10
|
+
const { startSpinner, stopSpinner } = require('./spinner');
|
|
11
|
+
|
|
12
|
+
function parseUrlDetails(urlStr) {
|
|
13
|
+
try {
|
|
14
|
+
let cleanUrl = urlStr;
|
|
15
|
+
if (!/^https?:\/\//i.test(urlStr)) {
|
|
16
|
+
cleanUrl = 'https://' + urlStr;
|
|
17
|
+
}
|
|
18
|
+
const parsed = new URL(cleanUrl);
|
|
19
|
+
const hostname = parsed.hostname;
|
|
20
|
+
|
|
21
|
+
const segments = hostname.split('.');
|
|
22
|
+
let nameDefault = 'Webapp';
|
|
23
|
+
if (segments.length > 0) {
|
|
24
|
+
const firstSegment = segments[0] === 'www' && segments.length > 1 ? segments[1] : segments[0];
|
|
25
|
+
nameDefault = firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const escapedDomain = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
const regexDefault = `.*${escapedDomain}.*`;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
cleanUrl,
|
|
33
|
+
hostname,
|
|
34
|
+
nameDefault,
|
|
35
|
+
regexDefault
|
|
36
|
+
};
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return {
|
|
39
|
+
cleanUrl: urlStr,
|
|
40
|
+
hostname: '',
|
|
41
|
+
nameDefault: 'Webapp',
|
|
42
|
+
regexDefault: '.*'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function downloadUrl(urlStr, outputPath, redirectCount = 0) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
if (redirectCount > 5) {
|
|
50
|
+
resolve(false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
https.get(urlStr, (response) => {
|
|
56
|
+
if ([301, 302, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
57
|
+
let redirectUrl = response.headers.location;
|
|
58
|
+
if (redirectUrl.startsWith('/')) {
|
|
59
|
+
const parsed = new URL(urlStr);
|
|
60
|
+
redirectUrl = `${parsed.protocol}//${parsed.host}${redirectUrl}`;
|
|
61
|
+
}
|
|
62
|
+
resolve(downloadUrl(redirectUrl, outputPath, redirectCount + 1));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (response.statusCode === 200) {
|
|
67
|
+
const file = fs.createWriteStream(outputPath);
|
|
68
|
+
response.pipe(file);
|
|
69
|
+
file.on('finish', () => {
|
|
70
|
+
file.close();
|
|
71
|
+
resolve(true);
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
resolve(false);
|
|
75
|
+
}
|
|
76
|
+
}).on('error', () => {
|
|
77
|
+
resolve(false);
|
|
78
|
+
});
|
|
79
|
+
} catch (e) {
|
|
80
|
+
resolve(false);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getHtml(urlStr, redirectCount = 0) {
|
|
86
|
+
return new Promise((resolve) => {
|
|
87
|
+
if (redirectCount > 5) {
|
|
88
|
+
resolve('');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
https.get(urlStr, (response) => {
|
|
94
|
+
if ([301, 302, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
95
|
+
let redirectUrl = response.headers.location;
|
|
96
|
+
if (redirectUrl.startsWith('/')) {
|
|
97
|
+
const parsed = new URL(urlStr);
|
|
98
|
+
redirectUrl = `${parsed.protocol}//${parsed.host}${redirectUrl}`;
|
|
99
|
+
}
|
|
100
|
+
resolve(getHtml(redirectUrl, redirectCount + 1));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (response.statusCode !== 200) {
|
|
105
|
+
resolve('');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let data = '';
|
|
110
|
+
response.on('data', chunk => { data += chunk; });
|
|
111
|
+
response.on('end', () => { resolve(data); });
|
|
112
|
+
}).on('error', () => {
|
|
113
|
+
resolve('');
|
|
114
|
+
});
|
|
115
|
+
} catch (e) {
|
|
116
|
+
resolve('');
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractIconUrl(html, baseUrl) {
|
|
122
|
+
const matches = html.match(/<link[^>]*rel=["'](icon|shortcut icon|apple-touch-icon)["'][^>]*>/gi);
|
|
123
|
+
if (matches) {
|
|
124
|
+
for (const link of matches) {
|
|
125
|
+
const hrefMatch = link.match(/href=["']([^"']+)["']/i);
|
|
126
|
+
if (hrefMatch) {
|
|
127
|
+
let href = hrefMatch[1];
|
|
128
|
+
if (!/^https?:\/\//i.test(href)) {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = new URL(baseUrl);
|
|
131
|
+
if (href.startsWith('//')) {
|
|
132
|
+
href = parsed.protocol + href;
|
|
133
|
+
} else if (href.startsWith('/')) {
|
|
134
|
+
href = `${parsed.protocol}//${parsed.host}${href}`;
|
|
135
|
+
} else {
|
|
136
|
+
const pathname = parsed.pathname.endsWith('/') ? parsed.pathname : path.dirname(parsed.pathname) + '/';
|
|
137
|
+
href = `${parsed.protocol}//${parsed.host}${pathname}${href}`;
|
|
138
|
+
}
|
|
139
|
+
} catch (e) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return href;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function downloadFaviconFlow(targetUrl, domain, outputPath) {
|
|
151
|
+
startSpinner('Fetching website favicon...');
|
|
152
|
+
const googleUrl = `https://www.google.com/s2/favicons?sz=128&domain=${domain}`;
|
|
153
|
+
let success = await downloadUrl(googleUrl, outputPath);
|
|
154
|
+
if (success) {
|
|
155
|
+
try {
|
|
156
|
+
const stats = fs.statSync(outputPath);
|
|
157
|
+
if (stats.size > 1000) {
|
|
158
|
+
stopSpinner(true, 'Favicon downloaded successfully!');
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
fs.unlinkSync(outputPath);
|
|
162
|
+
} catch (e) {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const html = await getHtml(targetUrl);
|
|
167
|
+
if (html) {
|
|
168
|
+
const iconUrl = extractIconUrl(html, targetUrl);
|
|
169
|
+
if (iconUrl) {
|
|
170
|
+
success = await downloadUrl(iconUrl, outputPath);
|
|
171
|
+
if (success) {
|
|
172
|
+
stopSpinner(true, 'Favicon scraped and downloaded successfully!');
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (e) {}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const parsed = new URL(targetUrl);
|
|
181
|
+
const fallbackUrl = `${parsed.protocol}//${parsed.host}/favicon.ico`;
|
|
182
|
+
success = await downloadUrl(fallbackUrl, outputPath);
|
|
183
|
+
if (success) {
|
|
184
|
+
stopSpinner(true, 'Default favicon.ico downloaded successfully!');
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {}
|
|
188
|
+
|
|
189
|
+
stopSpinner(false, 'Could not download favicon. Fallback to default Electron icon.');
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
parseUrlDetails,
|
|
195
|
+
downloadFaviconFlow
|
|
196
|
+
};
|