@thinremote/thinr-cli 1.0.0-alpha.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/lib/console.js ADDED
@@ -0,0 +1,172 @@
1
+ import WebSocket from 'ws';
2
+ import chalk from 'chalk';
3
+ import { readConfig } from './config.js';
4
+ import api from './api.js';
5
+
6
+ /**
7
+ * Creates a WebSocket connection to a device terminal
8
+ * @param {string} deviceId - The ID of the device to connect to
9
+ */
10
+ export async function connectToDeviceConsole(deviceId) {
11
+ // Get configuration
12
+ const config = readConfig();
13
+
14
+ if (!config.token || !config.server || !config.username) {
15
+ console.error(chalk.red('Error: Not configured. Run thinr without parameters to set up.'));
16
+ process.exit(1);
17
+ }
18
+
19
+ // Create a unique session ID
20
+ const sessionId = createUUID();
21
+
22
+ // Create WebSocket URL
23
+ const wsUrl = `wss://${config.server}/v3/users/${config.username}/devices/${deviceId}/resources/$terminal/${sessionId}?raw=1`;
24
+
25
+ //console.log(chalk.blue(`Connecting to ${deviceId}...`));
26
+
27
+ // Create WebSocket connection
28
+ const ws = new WebSocket(wsUrl, {
29
+ headers: {
30
+ 'Authorization': `Bearer ${config.token}`
31
+ }
32
+ });
33
+
34
+ // Flag to track if terminal settings have been restored
35
+ let terminalRestored = false;
36
+
37
+ // Setup raw mode for process.stdin
38
+ process.stdin.setRawMode(true);
39
+ process.stdin.resume();
40
+ process.stdin.setEncoding('utf8');
41
+
42
+ // Function to restore terminal settings
43
+ const restoreTerminal = () => {
44
+ if (!terminalRestored) {
45
+ // Restore terminal settings
46
+ process.stdin.setRawMode(false);
47
+ process.stdin.pause();
48
+ terminalRestored = true;
49
+ }
50
+ };
51
+
52
+ // Handle WebSocket connection
53
+ ws.on('open', () => {
54
+ //console.log(chalk.green(`Connected to ${deviceId}`));
55
+
56
+ // Set initial terminal size
57
+ updateTerminalSize(config, deviceId, sessionId, process.stdout.columns, process.stdout.rows);
58
+
59
+ // Handle window resize
60
+ process.stdout.on('resize', () => {
61
+ updateTerminalSize(config, deviceId, sessionId, process.stdout.columns, process.stdout.rows);
62
+ });
63
+
64
+ // Forward stdin to WebSocket
65
+ process.stdin.on('data', (data) => {
66
+ // Check for Ctrl+D (EOT character, ASCII 4) to exit gracefully
67
+ if (data.toString() === '\u0004') {
68
+ ws.close();
69
+ return;
70
+ }
71
+
72
+ // Forward any other input (including Ctrl+C) to the remote device
73
+ ws.send(data);
74
+ });
75
+
76
+ // Forward WebSocket data to stdout
77
+ ws.on('message', (data) => {
78
+ // Ensure we're writing Buffer data to avoid encoding issues
79
+ if (!(data instanceof Buffer)) {
80
+ data = Buffer.from(data);
81
+ }
82
+
83
+ process.stdout.write(data);
84
+ });
85
+ });
86
+
87
+ // Handle WebSocket close
88
+ ws.on('close', () => {
89
+ console.log(chalk.yellow('\nConnection closed'));
90
+ restoreTerminal();
91
+
92
+ // Small delay before exiting to flush any remaining output
93
+ setTimeout(() => {
94
+ process.exit(0);
95
+ }, 100);
96
+ });
97
+
98
+ // Handle WebSocket errors
99
+ ws.on('error', (error) => {
100
+ console.error(chalk.red(`\nWebSocket error: ${error.message}`));
101
+ restoreTerminal();
102
+ process.exit(1);
103
+ });
104
+
105
+ // Handle SIGINT (Ctrl+C) at process level
106
+ process.on('SIGINT', () => {
107
+ // Try to send Ctrl+C to the remote terminal
108
+ if (ws.readyState === WebSocket.OPEN) {
109
+ ws.send('\u0003');
110
+ }
111
+
112
+ // Don't exit our process - let the remote terminal handle it
113
+ return false;
114
+ });
115
+
116
+ // Ensure cleanup on other exit signals
117
+ process.on('SIGTERM', () => {
118
+ restoreTerminal();
119
+ if (ws.readyState === WebSocket.OPEN) {
120
+ ws.close();
121
+ }
122
+ });
123
+
124
+ // Handle uncaught exceptions gracefully
125
+ process.on('uncaughtException', (error) => {
126
+ console.error(chalk.red(`\nUnexpected error: ${error.message}`));
127
+ restoreTerminal();
128
+ if (ws.readyState === WebSocket.OPEN) {
129
+ ws.close();
130
+ }
131
+ process.exit(1);
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Update terminal size on remote device
137
+ * @param {Object} config - Configuration object
138
+ * @param {string} deviceId - Device ID
139
+ * @param {string} sessionId - Session ID
140
+ * @param {number} cols - Number of columns
141
+ * @param {number} rows - Number of rows
142
+ */
143
+ async function updateTerminalSize(config, deviceId, sessionId, cols, rows) {
144
+ try {
145
+ const url = `/v3/users/${config.username}/devices/${deviceId}/resources/$terminal/${sessionId}/params`;
146
+
147
+ await api.post(
148
+ url,
149
+ {
150
+ size: {
151
+ cols: cols,
152
+ rows: rows
153
+ }
154
+ },
155
+ );
156
+ } catch (error) {
157
+ console.error(chalk.red(`Error updating terminal size: ${error.message}`));
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Generate a UUID
163
+ * @returns {string} UUID
164
+ */
165
+ function createUUID() {
166
+ let dt = new Date().getTime();
167
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
168
+ const r = (dt + Math.random() * 16) % 16 | 0;
169
+ dt = Math.floor(dt / 16);
170
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
171
+ });
172
+ }
package/lib/device.js ADDED
@@ -0,0 +1,76 @@
1
+ import chalk from 'chalk';
2
+ import { readConfig } from './config.js';
3
+ import api from './api.js';
4
+
5
+ /**
6
+ * Get devices belonging to a product
7
+ * @param {Object} filter - Filters to query devices
8
+ * @return {Promise<Array>} List of devices
9
+ */
10
+ export async function getDevices(filter) {
11
+ // Get configuration
12
+ const config = readConfig();
13
+
14
+ if (!config.token || !config.server || !config.username) {
15
+ throw new Error('Not configured. Run thinr without parameters to set up.');
16
+ }
17
+
18
+ let devices = [];
19
+
20
+ // Query parameters to string
21
+ let queryParametersString = "";
22
+ for (const [key, value] of Object.entries(filter)) {
23
+ if ( value ) queryParametersString += key+"="+value+"&";
24
+ }
25
+
26
+ try {
27
+ const count = 50;
28
+ let url = `/v1/users/${config.username}/devices?count=${count}&${queryParametersString}`;
29
+
30
+ let index = 0;
31
+ let res_length = 0;
32
+
33
+ do {
34
+ // Fetch devices for the product
35
+ const response = await api.get(
36
+ `${url}index=${index}`,
37
+ );
38
+
39
+ res_length = response.data.length;
40
+ index += res_length;
41
+ devices = devices.concat(response.data);
42
+
43
+ } while ( res_length === count );
44
+
45
+ return devices;
46
+
47
+ } catch (error) {
48
+ if (error.response) {
49
+ // The request was made and the server responded with a status code
50
+ if (error.response.status === 404) {
51
+ throw new Error(`Product not found: ${productId}`);
52
+ } else if (error.response.status === 401) {
53
+ throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
54
+ } else {
55
+ throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
56
+ }
57
+ } else if (error.request) {
58
+ // The request was made but no response was received
59
+ throw new Error('No response from server. Please check your connection.');
60
+ } else {
61
+ // Something happened in setting up the request that triggered an Error
62
+ throw new Error(`Error: ${error.message}`);
63
+ }
64
+ }
65
+
66
+ }
67
+
68
+ /**
69
+ * Filter active devices from a list of devices
70
+ * @param {Array} devices - List of devices to filter
71
+ * @return {Array} List of active devices
72
+ */
73
+ export function filterActiveDevices(devices) {
74
+ return devices.filter(device => device.connection && device.connection.active === true);
75
+ }
76
+
@@ -0,0 +1,134 @@
1
+ import chalk from 'chalk';
2
+ import { readConfig } from './config.js';
3
+ import api from './api.js';
4
+
5
+ /**
6
+ * Get device properties
7
+ * @param {string} deviceId - Device ID to query
8
+ * @returns {Promise<Object>} Device properties
9
+ */
10
+ export async function getDeviceProperties(deviceId) {
11
+ // Get configuration
12
+ const config = readConfig();
13
+
14
+ if (!config.token || !config.server || !config.username) {
15
+ throw new Error('Not configured. Run thinr without parameters to set up.');
16
+ }
17
+
18
+ try {
19
+ // Fetch device properties
20
+ const response = await api.get(
21
+ `/v3/users/${config.username}/devices/${deviceId}/properties`,
22
+ );
23
+
24
+ return response.data;
25
+ } catch (error) {
26
+ if (error.response) {
27
+ // The request was made and the server responded with a status code
28
+ if (error.response.status === 404) {
29
+ throw new Error(`Device not found: ${deviceId}`);
30
+ } else if (error.response.status === 401) {
31
+ throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
32
+ } else {
33
+ throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
34
+ }
35
+ } else if (error.request) {
36
+ // The request was made but no response was received
37
+ throw new Error('No response from server. Please check your connection.');
38
+ } else {
39
+ // Something happened in setting up the request that triggered an Error
40
+ throw new Error(`Error: ${error.message}`);
41
+ }
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Get device property value
47
+ * @param {string} deviceId - Device ID to query
48
+ * @param {string} propertyId - Property ID to retrieve
49
+ * @returns {Promise<Object>} Property value
50
+ */
51
+ export async function getDeviceProperty(deviceId, propertyId) {
52
+ // Get configuration
53
+ const config = readConfig();
54
+
55
+ if (!config.token || !config.server || !config.username) {
56
+ throw new Error('Not configured. Run thinr without parameters to set up.');
57
+ }
58
+
59
+ try {
60
+ // Fetch property value
61
+ const response = await api.get(
62
+ `/v3/users/${config.username}/devices/${deviceId}/properties/${propertyId}`
63
+ );
64
+
65
+ return response.data.value;
66
+ } catch (error) {
67
+ console.log(error);
68
+ if (error.response) {
69
+ // The request was made and the server responded with a status code
70
+ if (error.response.status === 404) {
71
+ throw new Error(`Property not found: ${propertyId} on device ${deviceId}`);
72
+ } else if (error.response.status === 401) {
73
+ throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
74
+ } else {
75
+ throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
76
+ }
77
+ } else if (error.request) {
78
+ // The request was made but no response was received
79
+ throw new Error('No response from server. Please check your connection.');
80
+ } else {
81
+ // Something happened in setting up the request that triggered an Error
82
+ throw new Error(`Error: ${error.message}`);
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Format device properties for display
89
+ * @param {string} deviceId - Device ID
90
+ * @param {Object} properties - Properties object from API
91
+ * @returns {string} Formatted properties
92
+ */
93
+ export function formatDeviceProperties(deviceId, properties) {
94
+
95
+ let formattedProperties = '\nProperties of device ' + chalk.cyan(deviceId) + ':\n';
96
+
97
+ properties.forEach(property => {
98
+ formattedProperties += `- ${property.property}\n`;
99
+ })
100
+
101
+ return formattedProperties;
102
+ }
103
+
104
+ /**
105
+ * Format device property value for display
106
+ * @param {string} deviceId - Device ID
107
+ * @param {string} propertyId - Property ID
108
+ * @param {Object} propertyValue - Property value from API
109
+ * @returns {string} Formatted property value
110
+ */
111
+ export function formatDeviceProperty(deviceId, propertyId, propertyValue) {
112
+ // show json with flattened structure
113
+ let formattedValue = '\nValue of property ' + chalk.cyan(propertyId) + ' for device ' + chalk.cyan(deviceId) + ':\n';
114
+ const flattenedValue = flatten(propertyValue);
115
+ for (const key in flattenedValue) {
116
+ formattedValue += `${key}: ${chalk.green(flattenedValue[key])}\n`;
117
+ }
118
+ return formattedValue;
119
+ }
120
+
121
+ function flatten(obj, prefix = '', res = {}) {
122
+ for (const key in obj) {
123
+ const val = obj[key];
124
+ const path = prefix ? `${prefix}.${key}` : key;
125
+ if (typeof val === 'object' && val !== null) {
126
+ flatten(val, path, res);
127
+ } else {
128
+ res[path] = val;
129
+ }
130
+ }
131
+ return res;
132
+ }
133
+
134
+
package/lib/proxy.js ADDED
@@ -0,0 +1,310 @@
1
+ import chalk from 'chalk';
2
+ import open, { apps } from 'open';
3
+ import { configExists, readConfig } from './config.js';
4
+ import api from './api.js';
5
+
6
+ /**
7
+ * Parse target argument into address and port
8
+ * @param {string} target - Target in format 'address:port', 'port', 'address', or 'protocol://hostname_or_ip:port'
9
+ * @param {number} defaultPort - Default port if not specified
10
+ * @returns {Object} Parsed address, port, and secure flag
11
+ */
12
+ function parseTarget(target, defaultPort) {
13
+ if (!target) {
14
+ return { address: 'localhost', port: defaultPort, isSecure: false };
15
+ }
16
+
17
+ // Check if target is just a number (port only)
18
+ if (/^\d+$/.test(target)) {
19
+ return { address: 'localhost', port: parseInt(target), isSecure: false };
20
+ }
21
+
22
+ let isSecure = false;
23
+ let cleanTarget = target;
24
+
25
+ // Check if target starts with a protocol (full URL format)
26
+ if (target.startsWith('https://')) {
27
+ isSecure = true;
28
+ cleanTarget = target.replace('https://', '');
29
+ } else if (target.startsWith('http://')) {
30
+ isSecure = false;
31
+ cleanTarget = target.replace('http://', '');
32
+ }
33
+
34
+ // Remove any path/query/fragment from the URL (everything after the first slash)
35
+ const slashIndex = cleanTarget.indexOf('/');
36
+ if (slashIndex !== -1) {
37
+ cleanTarget = cleanTarget.substring(0, slashIndex);
38
+ }
39
+
40
+ // Parse the remaining part (hostname_or_ip:port or just hostname_or_ip)
41
+ let address;
42
+ let port;
43
+
44
+ if (cleanTarget.includes(':')) {
45
+ // Format: hostname_or_ip:port
46
+ const lastColonIndex = cleanTarget.lastIndexOf(':');
47
+ address = cleanTarget.substring(0, lastColonIndex);
48
+ const portStr = cleanTarget.substring(lastColonIndex + 1);
49
+
50
+ // Check if the part after the last colon is actually a port number
51
+ const parsedPort = parseInt(portStr);
52
+ if (!isNaN(parsedPort) && parsedPort > 0 && parsedPort <= 65535) {
53
+ port = parsedPort;
54
+ } else {
55
+ // Not a valid port, treat the whole thing as address
56
+ address = cleanTarget;
57
+ port = defaultPort;
58
+ }
59
+ } else {
60
+ // Format: just hostname_or_ip
61
+ address = cleanTarget;
62
+ port = defaultPort;
63
+ }
64
+
65
+ // If we detected a protocol but no explicit port, use protocol defaults
66
+ if ((target.startsWith('https://') || target.startsWith('http://')) && port === defaultPort) {
67
+ if (isSecure && defaultPort !== 443) {
68
+ port = 443; // Default HTTPS port
69
+ } else if (!isSecure && defaultPort !== 80) {
70
+ port = 80; // Default HTTP port
71
+ }
72
+ }
73
+
74
+ return {
75
+ address: address || 'localhost',
76
+ port: port,
77
+ isSecure
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Create proxy action handler
83
+ */
84
+ export async function handleProxyAction(deviceId, protocol, target, options) {
85
+ // Check if configured
86
+ if (!configExists()) {
87
+ console.error(chalk.red('Error: Not configured. Run thinr without parameters to set up.'));
88
+ process.exit(1);
89
+ }
90
+
91
+ // Determine default port based on protocol
92
+ const defaultPort = protocol === 'http' ? 80 : (protocol === 'tls' ? 443 : 22);
93
+ const { address, port, isSecure } = parseTarget(target, defaultPort);
94
+
95
+ // Determine proxy configuration based on protocol
96
+ let proxyConfig = {
97
+ targetAddress: address,
98
+ targetPort: port,
99
+ serverPort: options.port ? parseInt(options.port) : null,
100
+ targetSecure: false,
101
+ serverSecure: false,
102
+ web: false
103
+ };
104
+
105
+ switch (protocol) {
106
+ case 'tcp':
107
+ // TCP proxy without TLS
108
+ proxyConfig.serverSecure = false;
109
+ break;
110
+ case 'tls':
111
+ // TLS proxy
112
+ proxyConfig.targetSecure = true;
113
+ proxyConfig.serverSecure = true;
114
+ break;
115
+ case 'http':
116
+ // HTTP proxy
117
+ proxyConfig.web = true;
118
+ proxyConfig.serverSecure = true; // Default to HTTPS for web interface
119
+ // Use the isSecure flag from parseTarget
120
+ proxyConfig.targetSecure = isSecure || port === 443;
121
+ break;
122
+ }
123
+
124
+ const proxyType = proxyConfig.web ? 'HTTP proxy' : `${protocol.toUpperCase()} proxy`;
125
+ const spinner = ora(`Creating ${proxyType} to ${deviceId} (${address}:${port})...`).start();
126
+ let proxyId = null;
127
+
128
+ try {
129
+ // Create proxy
130
+ const proxy = await createProxy(deviceId, proxyConfig);
131
+ proxyId = proxy.proxyId;
132
+
133
+ if (proxyConfig.web) {
134
+ // For HTTP proxy, show target as URL format
135
+ const targetProtocol = isSecure ? 'https' : 'http';
136
+ const targetUrl = `${targetProtocol}://${address}${port === 80 || port === 443 ? '' : `:${port}`}`;
137
+
138
+ spinner.succeed(`HTTP proxy running at ${chalk.blue(proxy.url)} → ${deviceId} -> ${chalk.cyan(targetUrl)}`);
139
+
140
+ // Open browser if not disabled
141
+ if (options.openBrowser !== false) {
142
+ await openBrowser(proxy.url);
143
+ }
144
+ } else {
145
+ // For TCP/TLS proxy, show address:port format
146
+ const secureInfo = isSecure ? ' [TLS]' : '';
147
+ spinner.succeed(`${protocol.toUpperCase()} proxy running on ${chalk.blue(proxy.serverHost + ':' + proxy.serverPort)} → ${deviceId} (${address}:${port})${secureInfo}`);
148
+ }
149
+
150
+ console.log(chalk.gray(`Press Ctrl+C to stop`));
151
+
152
+ // Handle process termination
153
+ process.on('SIGINT', async () => {
154
+ console.log(chalk.yellow('\nStopping proxy...'));
155
+ if (proxyId) {
156
+ await deleteProxy(proxyId);
157
+ }
158
+ process.exit(0);
159
+ });
160
+
161
+ // Keep the process running
162
+ process.stdin.resume();
163
+
164
+ } catch (error) {
165
+ spinner.fail(`Failed to create ${proxyType}`);
166
+ console.error(chalk.red(`Error: ${error.message}`));
167
+
168
+ // Clean up if needed
169
+ if (proxyId) {
170
+ await deleteProxy(proxyId);
171
+ }
172
+
173
+ process.exit(1);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Generate a random port number within a range
179
+ * @param {number} min - Minimum port number
180
+ * @param {number} max - Maximum port number
181
+ * @returns {number} Random port number
182
+ */
183
+ function getRandomPort(min = 50000, max = 51000) {
184
+ return Math.floor(Math.random() * (max - min + 1)) + min;
185
+ }
186
+
187
+ /**
188
+ * Create a proxy to a device
189
+ * @param {string} deviceId - Device ID
190
+ * @param {Object} options - Proxy options
191
+ * @returns {Promise<Object>} Proxy information
192
+ */
193
+ async function createProxy(deviceId, options) {
194
+ // Get configuration
195
+ const config = readConfig();
196
+
197
+ if (!config.token || !config.server || !config.username) {
198
+ throw new Error('Not configured. Run thinr without parameters to set up.');
199
+ }
200
+
201
+ const protocol = options.web ? 'http_iotmp' : 'tcp_iotmp';
202
+ const targetAddress = options.targetAddress || 'localhost';
203
+ const targetPort = options.targetPort || (options.web ? 80 : 22);
204
+ const serverPort = options.serverPort || getRandomPort();
205
+ const serverSecure = options.serverSecure || (!!options.web);
206
+ const targetSecure = options.targetSecure || false;
207
+
208
+ // Create a unique proxy ID based on device, protocol and a timestamp
209
+ const timestamp = new Date().getTime().toString(36);
210
+ const proxyId = `${protocol}_${deviceId.slice(0,12)}_${timestamp}`;
211
+
212
+ try {
213
+ // Create proxy
214
+ const response = await api.post(
215
+ `/v1/proxies`,
216
+ {
217
+ enabled: true,
218
+ config: {
219
+ target: {
220
+ type: 'address',
221
+ user: config.username,
222
+ device: deviceId,
223
+ address: targetAddress,
224
+ port: targetPort,
225
+ secure: targetSecure
226
+ },
227
+ protocol: protocol,
228
+ source: {
229
+ port: serverPort,
230
+ secure: serverSecure
231
+ }
232
+ },
233
+ proxy: proxyId,
234
+ name: options.web ? `Web access for ${deviceId}` : `TCP proxy for ${deviceId}`,
235
+ description: `${options.web ? 'Web interface' : 'TCP proxy'} for ${deviceId}:${targetPort} created by ThingR CLI`
236
+ },
237
+ {
238
+ headers: {
239
+ 'Content-Type': 'application/json'
240
+ }
241
+ }
242
+ );
243
+
244
+ // Determine the URL based on protocol and SSL setting
245
+ const scheme = serverSecure ? 'https' : 'http';
246
+ const url = options.web ? `${scheme}://${config.server}:${serverPort}` : null;
247
+
248
+ return {
249
+ proxyId: proxyId,
250
+ url: url,
251
+ serverPort: serverPort,
252
+ serverHost: config.server,
253
+ targetAddress: targetAddress,
254
+ targetPort: targetPort,
255
+ protocol: protocol,
256
+ serverSecure: serverSecure,
257
+ targetSecure: targetSecure
258
+ };
259
+ } catch (error) {
260
+ if (error.response) {
261
+ if (error.response.status === 401) {
262
+ throw new Error('Unauthorized. Your token may have expired. Please reconfigure.');
263
+ } else {
264
+ throw new Error(`Server error: ${error.response.status} ${error.response.statusText}`);
265
+ }
266
+ } else if (error.request) {
267
+ throw new Error('No response from server. Please check your connection.');
268
+ } else {
269
+ throw new Error(`Error: ${error.message}`);
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Delete a proxy
276
+ * @param {string} proxyId - Proxy ID to delete
277
+ */
278
+ async function deleteProxy(proxyId) {
279
+ // Get configuration
280
+ const config = readConfig();
281
+
282
+ if (!config.token || !config.server) {
283
+ return; // Silently fail on config issues during cleanup
284
+ }
285
+
286
+ try {
287
+ await api.delete(
288
+ `/v1/proxies/${proxyId}`,
289
+ );
290
+
291
+ return true;
292
+ } catch (error) {
293
+ console.error(chalk.red(`Error deleting proxy: ${error.message}`));
294
+ return false;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Open web browser to the proxy URL
300
+ * @param {string} url - URL to open
301
+ */
302
+ async function openBrowser(url) {
303
+ try {
304
+ await open(url, {app: [{name: apps.browser}, 'firefox-developer-edition']});
305
+ return true;
306
+ } catch (error) {
307
+ console.error(chalk.red(`Error opening browser: ${error.message}`));
308
+ return false;
309
+ }
310
+ }