deskify-cli 1.0.3 → 1.0.5

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/deskify.js CHANGED
@@ -12,6 +12,7 @@ const { colors, log } = require('./src/utils/colors');
12
12
  const { selectOption, waitForKey } = require('./src/utils/prompts');
13
13
  const { startSpinner, stopSpinner } = require('./src/utils/spinner');
14
14
  const { createFlow } = require('./src/flows/create');
15
+ const { manageFlow } = require('./src/flows/manage');
15
16
  const { uninstallFlow } = require('./src/flows/uninstall');
16
17
 
17
18
  function printHeader() {
@@ -67,6 +68,7 @@ async function main() {
67
68
 
68
69
  const options = [
69
70
  'Create a new Desktop App',
71
+ 'List & Manage existing Apps',
70
72
  'Uninstall an existing App',
71
73
  'Exit'
72
74
  ];
@@ -89,6 +91,14 @@ async function main() {
89
91
  await waitForKey();
90
92
  }
91
93
  } else if (selectedIdx === 1) {
94
+ try {
95
+ await manageFlow(hasDesktopUtils);
96
+ } catch (e) {
97
+ log.error('An error occurred during application management:');
98
+ console.error(e);
99
+ await waitForKey();
100
+ }
101
+ } else if (selectedIdx === 2) {
92
102
  try {
93
103
  const result = await uninstallFlow(hasDesktopUtils);
94
104
  if (result && result.success) {
@@ -103,7 +113,7 @@ async function main() {
103
113
  console.error(e);
104
114
  await waitForKey();
105
115
  }
106
- } else if (selectedIdx === 2) {
116
+ } else if (selectedIdx === 3) {
107
117
  log.info('Thank you for using Deskify! Goodbye! šŸ‘‹');
108
118
  process.exit(0);
109
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deskify-cli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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": {
@@ -24,12 +24,25 @@ function printHeader() {
24
24
  console.log(`${colors.dim}--------------------------------------------------${colors.reset}\n`);
25
25
  }
26
26
 
27
- async function createFlow(hasDesktopUtils) {
27
+ async function createFlow(hasDesktopUtils, existingConfig = null) {
28
28
  console.clear();
29
29
  printHeader();
30
- log.bold('--- Create a New Desktop Application ---');
30
+ if (existingConfig) {
31
+ log.bold(`--- Edit/Update Desktop Application: ${existingConfig.appName} ---`);
32
+ } else {
33
+ log.bold('--- Create a New Desktop Application ---');
34
+ }
31
35
  console.log('');
32
36
 
37
+ let defaultTargetUrl = existingConfig ? existingConfig.url : '';
38
+ let defaultAppName = existingConfig ? existingConfig.appName : '';
39
+ let defaultPersist = existingConfig ? existingConfig.persist : true;
40
+ let defaultInternalUrls = existingConfig ? existingConfig.internalUrls : '';
41
+ let defaultWidth = existingConfig ? existingConfig.width : '1200';
42
+ let defaultHeight = existingConfig ? existingConfig.height : '800';
43
+ let defaultInstallPath = existingConfig ? path.dirname(existingConfig.appFolder) : '~/Apps';
44
+ let defaultNoSandbox = existingConfig ? existingConfig.noSandbox : false;
45
+
33
46
  let targetUrl = '';
34
47
  let appName = '';
35
48
  let iconPath = '';
@@ -38,11 +51,12 @@ async function createFlow(hasDesktopUtils) {
38
51
  let width = '1200';
39
52
  let height = '800';
40
53
  let installPath = '';
54
+ let noSandbox = false;
41
55
 
42
56
  try {
43
57
  let targetUrlInput = '';
44
58
  while (!targetUrlInput) {
45
- targetUrlInput = await ask('Enter Website URL (e.g., https://nextcloud.com)');
59
+ targetUrlInput = await ask('Enter Website URL (e.g., https://nextcloud.com)', defaultTargetUrl);
46
60
  if (!targetUrlInput) {
47
61
  log.error('URL cannot be empty. Please try again.');
48
62
  }
@@ -53,7 +67,7 @@ async function createFlow(hasDesktopUtils) {
53
67
 
54
68
  log.info(`Parsed Target URL: ${colors.bright}${targetUrl}${colors.reset}`);
55
69
 
56
- appName = await ask('Enter Application Name', urlDetails.nameDefault);
70
+ appName = await ask('Enter Application Name', defaultAppName || urlDetails.nameDefault);
57
71
 
58
72
  const useFavicon = await askYesNo('Download and use website favicon as app icon?', true);
59
73
 
@@ -81,12 +95,13 @@ async function createFlow(hasDesktopUtils) {
81
95
  }
82
96
  }
83
97
 
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');
98
+ persistSession = await askYesNo('Persist session cookies, local storage, and cache?', defaultPersist);
99
+ internalUrlsRegex = await ask('Internal URL pattern regex', defaultInternalUrls || urlDetails.regexDefault);
100
+ width = await ask('Window width (pixels)', defaultWidth);
101
+ height = await ask('Window height (pixels)', defaultHeight);
102
+ noSandbox = await askYesNo('Disable Chromium sandbox (avoids requiring sudo)?', defaultNoSandbox);
88
103
 
89
- const installPathInput = await ask('Installation directory', '~/Apps');
104
+ const installPathInput = await ask('Installation directory', defaultInstallPath);
90
105
  installPath = expandHome(installPathInput);
91
106
 
92
107
  console.log(`\n${colors.bright}--- Build Configuration Summary ---${colors.reset}`);
@@ -95,6 +110,7 @@ async function createFlow(hasDesktopUtils) {
95
110
  console.log(`Persist: ${persistSession ? 'Yes' : 'No'}`);
96
111
  console.log(`Internal URLs: ${internalUrlsRegex}`);
97
112
  console.log(`Dimensions: ${width}x${height}`);
113
+ console.log(`Disable Sandbox: ${noSandbox ? 'Yes (Sudo-free)' : 'No (Requires Sudo)'}`);
98
114
  console.log(`Install Dir: ${installPath}`);
99
115
  if (iconPath) {
100
116
  console.log(`Icon Path: ${iconPath}`);
@@ -114,6 +130,16 @@ async function createFlow(hasDesktopUtils) {
114
130
  throw e;
115
131
  }
116
132
 
133
+ // Deleting the existing application folder before rebuild to avoid folder name collisions
134
+ if (existingConfig && existingConfig.appFolder && fs.existsSync(existingConfig.appFolder)) {
135
+ log.info(`Deleting existing application folder for update: ${existingConfig.appFolder}`);
136
+ try {
137
+ fs.rmSync(existingConfig.appFolder, { recursive: true, force: true });
138
+ } catch (e) {
139
+ log.warn(`Could not delete existing folder: ${e.message}`);
140
+ }
141
+ }
142
+
117
143
  if (!fs.existsSync(installPath)) {
118
144
  log.info(`Creating installation directory: ${installPath}`);
119
145
  fs.mkdirSync(installPath, { recursive: true });
@@ -210,20 +236,24 @@ async function createFlow(hasDesktopUtils) {
210
236
  log.warn('Could not find internal package.json. StartupWMClass might be inaccurate.');
211
237
  }
212
238
 
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
- }
239
+ if (noSandbox) {
240
+ log.info('\n[4/4] Skipping chrome-sandbox permissions adjustment (sandbox disabled)...');
225
241
  } else {
226
- log.warn('chrome-sandbox file not found in build directory. Skipping permission adjustment.');
242
+ log.bold('\n[4/4] Setting up sandbox execution permissions (requires sudo)...');
243
+ const chromeSandboxPath = path.join(appFolder, 'chrome-sandbox');
244
+ if (fs.existsSync(chromeSandboxPath)) {
245
+ log.info(`Applying root ownership and SUID bit to: ${chromeSandboxPath}`);
246
+ try {
247
+ execSync(`sudo chown root:root "${chromeSandboxPath}" && sudo chmod 4755 "${chromeSandboxPath}"`, { stdio: 'inherit' });
248
+ log.success('Sandbox permissions applied successfully!');
249
+ } catch (e) {
250
+ log.error('Failed to configure chrome-sandbox permissions.');
251
+ log.info('You may need to run this command manually:');
252
+ console.log(` sudo chown root:root "${chromeSandboxPath}" && sudo chmod 4755 "${chromeSandboxPath}"`);
253
+ }
254
+ } else {
255
+ log.warn('chrome-sandbox file not found in build directory. Skipping permission adjustment.');
256
+ }
227
257
  }
228
258
 
229
259
  log.bold('\n[5/5] Generating Desktop Shortcut launcher...');
@@ -232,6 +262,16 @@ async function createFlow(hasDesktopUtils) {
232
262
  fs.mkdirSync(desktopEntriesDir, { recursive: true });
233
263
  }
234
264
 
265
+ // Delete the old desktop shortcut if updating to avoid leftovers (e.g. if the name changed)
266
+ if (existingConfig && existingConfig.filePath && fs.existsSync(existingConfig.filePath)) {
267
+ try {
268
+ fs.unlinkSync(existingConfig.filePath);
269
+ log.info(`Cleaned up old desktop shortcut: ${existingConfig.filePath}`);
270
+ } catch (e) {
271
+ log.warn(`Could not delete old desktop shortcut: ${e.message}`);
272
+ }
273
+ }
274
+
235
275
  const desktopFilePath = path.join(desktopEntriesDir, `${appName.replace(/\s+/g, '')}.desktop`);
236
276
  const execPath = path.join(appFolder, appName);
237
277
  const finalIconPath = fs.existsSync(destIconPath) ? destIconPath : (iconPath || 'electron');
@@ -239,13 +279,19 @@ async function createFlow(hasDesktopUtils) {
239
279
  const desktopContent = `[Desktop Entry]
240
280
  Name=${appName}
241
281
  Comment=${appName} Web Desktop App
242
- Exec=${execPath}
282
+ Exec=${execPath}${noSandbox ? ' --no-sandbox' : ''}
243
283
  Icon=${finalIconPath}
244
284
  Terminal=false
245
285
  Type=Application
246
286
  Categories=Network;WebBrowser;Application;
247
287
  StartupWMClass=${startupWMClass}
248
288
  X-Generated-By=deskify
289
+ X-Deskify-URL=${targetUrl}
290
+ X-Deskify-Width=${width}
291
+ X-Deskify-Height=${height}
292
+ X-Deskify-Persist=${persistSession}
293
+ X-Deskify-NoSandbox=${noSandbox}
294
+ X-Deskify-InternalUrls=${internalUrlsRegex}
249
295
  `;
250
296
 
251
297
  try {
@@ -0,0 +1,240 @@
1
+ /**
2
+ * manage.js
3
+ * Application list and management flow for the deskify CLI.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { execSync, spawn } = require('child_process');
9
+ const { colors, log } = require('../utils/colors');
10
+ const { expandHome, selectOption, askYesNo, waitForKey } = require('../utils/prompts');
11
+ const { createFlow } = require('./create');
12
+
13
+ function printHeader() {
14
+ console.clear();
15
+ console.log(`${colors.cyan}${colors.bright}`);
16
+ console.log(` _ _ _ __ `);
17
+ console.log(` __| | ___ ___| |_( )/ _|_ _ `);
18
+ console.log(` / _\` |/ _ \\/ __| |/ /| |_| | | |`);
19
+ console.log(` | (_| | __/\\__ \\ <| _| |_| |`);
20
+ console.log(` \\__,_|\\___||___/_|\\_\\\\_| \\__, |`);
21
+ console.log(` |___/ `);
22
+ console.log(` Web to Linux Desktop App Packager${colors.reset}`);
23
+ console.log(`${colors.dim}--------------------------------------------------${colors.reset}\n`);
24
+ }
25
+
26
+ async function getDeskifyApps() {
27
+ const desktopEntriesDir = expandHome('~/.local/share/applications');
28
+ if (!fs.existsSync(desktopEntriesDir)) {
29
+ return [];
30
+ }
31
+
32
+ const files = fs.readdirSync(desktopEntriesDir);
33
+ const apps = [];
34
+
35
+ for (const file of files) {
36
+ if (file.endsWith('.desktop')) {
37
+ const filePath = path.join(desktopEntriesDir, file);
38
+ try {
39
+ const content = fs.readFileSync(filePath, 'utf8');
40
+ if (content.includes('X-Generated-By=deskify')) {
41
+ const nameMatch = content.match(/^Name=(.+)$/m);
42
+ const execMatch = content.match(/^Exec=(.+)$/m);
43
+ const iconMatch = content.match(/^Icon=(.+)$/m);
44
+
45
+ const urlMatch = content.match(/^X-Deskify-URL=(.+)$/m);
46
+ const widthMatch = content.match(/^X-Deskify-Width=(.+)$/m);
47
+ const heightMatch = content.match(/^X-Deskify-Height=(.+)$/m);
48
+ const persistMatch = content.match(/^X-Deskify-Persist=(.+)$/m);
49
+ const noSandboxMatch = content.match(/^X-Deskify-NoSandbox=(.+)$/m);
50
+ const internalUrlsMatch = content.match(/^X-Deskify-InternalUrls=(.+)$/m);
51
+
52
+ const appName = nameMatch ? nameMatch[1].trim() : file.replace('.desktop', '');
53
+ let execRaw = execMatch ? execMatch[1].trim() : '';
54
+
55
+ if (execRaw.startsWith('"') && execRaw.endsWith('"')) {
56
+ execRaw = execRaw.slice(1, -1);
57
+ }
58
+
59
+ let execPath = execRaw;
60
+ let hasNoSandboxFlag = execRaw.includes('--no-sandbox');
61
+ if (hasNoSandboxFlag) {
62
+ execPath = execRaw.replace('--no-sandbox', '').trim();
63
+ }
64
+
65
+ let appFolder = execPath ? path.dirname(execPath) : '';
66
+
67
+ apps.push({
68
+ file,
69
+ filePath,
70
+ appName,
71
+ execPath,
72
+ appFolder,
73
+ icon: iconMatch ? iconMatch[1].trim() : '',
74
+ url: urlMatch ? urlMatch[1].trim() : '',
75
+ width: widthMatch ? widthMatch[1].trim() : '1200',
76
+ height: heightMatch ? heightMatch[1].trim() : '800',
77
+ persist: persistMatch ? persistMatch[1].trim() === 'true' : true,
78
+ noSandbox: noSandboxMatch ? noSandboxMatch[1].trim() === 'true' : hasNoSandboxFlag,
79
+ internalUrls: internalUrlsMatch ? internalUrlsMatch[1].trim() : ''
80
+ });
81
+ }
82
+ } catch (e) {}
83
+ }
84
+ }
85
+ return apps;
86
+ }
87
+
88
+ async function uninstallSingleApp(hasDesktopUtils, app) {
89
+ log.warn(`\nAre you sure you want to permanently delete ${colors.red}${app.appName}${colors.reset}?`);
90
+ log.warn('This will delete:');
91
+ log.warn(` - Shortcut: ${app.filePath}`);
92
+ if (app.appFolder && fs.existsSync(app.appFolder)) {
93
+ log.warn(` - App Folder: ${app.appFolder}`);
94
+ }
95
+ console.log('');
96
+
97
+ try {
98
+ const confirm = await askYesNo('Confirm uninstallation?', false);
99
+ if (!confirm) {
100
+ log.info('Uninstallation cancelled.');
101
+ return false;
102
+ }
103
+ } catch (e) {
104
+ if (e.message === 'ESC') {
105
+ log.warn('\nUninstallation cancelled by user (ESC pressed).');
106
+ return false;
107
+ }
108
+ throw e;
109
+ }
110
+
111
+ try {
112
+ if (fs.existsSync(app.filePath)) {
113
+ fs.unlinkSync(app.filePath);
114
+ log.success(`Deleted shortcut file: ${app.file}`);
115
+ }
116
+ } catch (e) {
117
+ log.error(`Failed to delete shortcut file: ${e.message}`);
118
+ }
119
+
120
+ if (app.appFolder && fs.existsSync(app.appFolder)) {
121
+ log.info(`Deleting application files at: ${app.appFolder}...`);
122
+ try {
123
+ fs.rmSync(app.appFolder, { recursive: true, force: true });
124
+ log.success('Application directory deleted successfully!');
125
+ } catch (e) {
126
+ log.error(`Failed to delete directory: ${e.message}`);
127
+ log.info('You may need to run this command manually as root/sudo:');
128
+ console.log(` sudo rm -rf "${app.appFolder}"`);
129
+ }
130
+ }
131
+
132
+ if (hasDesktopUtils) {
133
+ log.info('Updating desktop shortcuts registry database...');
134
+ try {
135
+ const desktopEntriesDir = expandHome('~/.local/share/applications');
136
+ execSync(`update-desktop-database "${desktopEntriesDir}"`, { stdio: 'ignore' });
137
+ log.success('Desktop shortcuts database updated!');
138
+ } catch (e) {}
139
+ }
140
+
141
+ log.success(`\nšŸŽ‰ ${app.appName} has been uninstalled successfully! šŸ—‘ļø\n`);
142
+ return true;
143
+ }
144
+
145
+ async function manageSingleApp(hasDesktopUtils, app) {
146
+ while (true) {
147
+ console.clear();
148
+ printHeader();
149
+ log.bold(`--- Application: ${app.appName} ---`);
150
+ console.log('');
151
+ console.log(`Name: ${colors.bright}${app.appName}${colors.reset}`);
152
+ console.log(`URL: ${colors.cyan}${app.url || 'N/A'}${colors.reset}`);
153
+ console.log(`Sandbox: ${app.noSandbox ? colors.green + 'Disabled (Sudo-free)' : colors.yellow + 'Enabled (Requires Sudo setup)'}${colors.reset}`);
154
+ console.log(`Persist: ${app.persist ? 'Yes' : 'No'}`);
155
+ console.log(`Window Size: ${app.width}x${app.height}`);
156
+ console.log(`Folder: ${app.appFolder}`);
157
+ console.log(`Shortcut: ${app.filePath}`);
158
+ console.log('');
159
+
160
+ const options = [
161
+ 'Launch Application',
162
+ 'Edit / Update Configuration',
163
+ 'Uninstall Application',
164
+ 'Back to List'
165
+ ];
166
+
167
+ const selectedIdx = await selectOption('Choose action', options, 0);
168
+
169
+ if (selectedIdx === 0) {
170
+ log.info(`Launching ${app.appName} in background...`);
171
+ try {
172
+ const runExec = app.noSandbox ? `"${app.execPath}" --no-sandbox` : `"${app.execPath}"`;
173
+ const child = spawn(runExec, [], {
174
+ shell: true,
175
+ detached: true,
176
+ stdio: 'ignore'
177
+ });
178
+ child.unref();
179
+ log.success('Launched successfully!');
180
+ await new Promise(r => setTimeout(r, 1500));
181
+ } catch (e) {
182
+ log.error(`Failed to launch application: ${e.message}`);
183
+ await waitForKey();
184
+ }
185
+ } else if (selectedIdx === 1) {
186
+ const result = await createFlow(hasDesktopUtils, app);
187
+ if (result && result.success) {
188
+ const refreshedApps = await getDeskifyApps();
189
+ const updated = refreshedApps.find(a => a.filePath === app.filePath || a.appName === app.appName);
190
+ if (updated) {
191
+ app = updated;
192
+ } else {
193
+ return;
194
+ }
195
+ await waitForKey();
196
+ }
197
+ } else if (selectedIdx === 2) {
198
+ const success = await uninstallSingleApp(hasDesktopUtils, app);
199
+ if (success) {
200
+ await waitForKey();
201
+ return;
202
+ }
203
+ } else if (selectedIdx === 3) {
204
+ return;
205
+ }
206
+ }
207
+ }
208
+
209
+ async function manageFlow(hasDesktopUtils) {
210
+ while (true) {
211
+ console.clear();
212
+ printHeader();
213
+ log.bold('--- List & Manage Existing Applications ---');
214
+ console.log('');
215
+
216
+ const apps = await getDeskifyApps();
217
+
218
+ if (apps.length === 0) {
219
+ log.info('No applications generated by deskify were found.');
220
+ await waitForKey();
221
+ return;
222
+ }
223
+
224
+ const options = apps.map(app => `${app.appName} ${colors.dim}(${app.url || 'No URL stored'})${colors.reset}`);
225
+ options.push('Back to Main Menu');
226
+
227
+ const selectedIdx = await selectOption('Select an application to manage', options, 0);
228
+
229
+ if (selectedIdx === options.length - 1) {
230
+ return;
231
+ }
232
+
233
+ const targetApp = apps[selectedIdx];
234
+ await manageSingleApp(hasDesktopUtils, targetApp);
235
+ }
236
+ }
237
+
238
+ module.exports = {
239
+ manageFlow
240
+ };
@@ -25,8 +25,7 @@ function ask(questionText, defaultValue = '') {
25
25
 
26
26
  let buffer = '';
27
27
  const rl = readline.createInterface({
28
- input: process.stdin,
29
- output: process.stdout
28
+ input: process.stdin
30
29
  });
31
30
 
32
31
  readline.emitKeypressEvents(process.stdin, rl);
@@ -91,8 +90,7 @@ function selectOption(questionText, options, defaultIndex = 0) {
91
90
  return new Promise((resolve) => {
92
91
  let selectedIndex = defaultIndex;
93
92
  const rl = readline.createInterface({
94
- input: process.stdin,
95
- output: process.stdout
93
+ input: process.stdin
96
94
  });
97
95
 
98
96
  readline.emitKeypressEvents(process.stdin, rl);