@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/.github/workflows/publish.yml +79 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +22 -0
- package/README.md +206 -0
- package/bin/thinr.js +78 -0
- package/commands/device.js +272 -0
- package/commands/logout.js +45 -0
- package/commands/product.js +198 -0
- package/lib/api.js +60 -0
- package/lib/auth.js +463 -0
- package/lib/config.js +88 -0
- package/lib/console.js +172 -0
- package/lib/device.js +76 -0
- package/lib/property.js +134 -0
- package/lib/proxy.js +310 -0
- package/lib/resource.js +124 -0
- package/lib/status.js +96 -0
- package/package.json +39 -0
package/lib/auth.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { input, select } from '@inquirer/prompts';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import open, { apps } from 'open';
|
|
6
|
+
import { writeConfig, readConfig, mergeConfig } from './config.js';
|
|
7
|
+
|
|
8
|
+
import api, { setBaseURL } from './api.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Authenticate user
|
|
14
|
+
* @returns {Promise<void>}
|
|
15
|
+
*/
|
|
16
|
+
export async function authenticate() {
|
|
17
|
+
// Ask for authentication method
|
|
18
|
+
const method = await select({
|
|
19
|
+
message: 'How would you like to authenticate?',
|
|
20
|
+
choices: [
|
|
21
|
+
{ name: 'OAuth', value: 'oauth' },
|
|
22
|
+
{ name: 'Token', value: 'token' },
|
|
23
|
+
{ name: 'Username and password', value: 'userpass' },
|
|
24
|
+
]
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Ask for server URL
|
|
28
|
+
const server = await input({
|
|
29
|
+
message: 'Enter server URL:',
|
|
30
|
+
default: 'console.thinr.io',
|
|
31
|
+
validate: (input) => {
|
|
32
|
+
return input ? true : 'Server URL is required';
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
setBaseURL(`https://${server}`);
|
|
37
|
+
|
|
38
|
+
writeConfig({
|
|
39
|
+
server,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (method === 'userpass') {
|
|
43
|
+
// Authenticate with username and password
|
|
44
|
+
await authenticateWithUserPass();
|
|
45
|
+
} else if (method === 'oauth') {
|
|
46
|
+
await authenticateWithOAuth();
|
|
47
|
+
} else {
|
|
48
|
+
// Authenticate with token
|
|
49
|
+
await authenticateWithToken();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Authenticate with username and password using OAuth2 password flow
|
|
56
|
+
* @returns {Promise<void>}
|
|
57
|
+
*/
|
|
58
|
+
async function authenticateWithUserPass() {
|
|
59
|
+
try {
|
|
60
|
+
// Ask for credentials
|
|
61
|
+
const username = await input({
|
|
62
|
+
message: 'Enter username:',
|
|
63
|
+
validate: (input) => {
|
|
64
|
+
return input ? true : 'Username is required';
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const password = await input({
|
|
68
|
+
message: 'Enter password:',
|
|
69
|
+
type: 'password',
|
|
70
|
+
mask: '*',
|
|
71
|
+
validate: (input) => {
|
|
72
|
+
return input ? true : 'Password is required';
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Show spinner
|
|
77
|
+
const spinner = ora('Authenticating...').start();
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Prepare OAuth2 password grant form data
|
|
81
|
+
const formData = new URLSearchParams();
|
|
82
|
+
formData.append('grant_type', 'password');
|
|
83
|
+
formData.append('username', username);
|
|
84
|
+
formData.append('password', password);
|
|
85
|
+
|
|
86
|
+
// Authenticate with server using OAuth2 password flow
|
|
87
|
+
const response = await api.post(
|
|
88
|
+
`/oauth/token`,
|
|
89
|
+
formData,
|
|
90
|
+
{
|
|
91
|
+
headers: {
|
|
92
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Update spinner message
|
|
98
|
+
spinner.text = 'Preparing user token...';
|
|
99
|
+
|
|
100
|
+
// Delete existing token if it exists
|
|
101
|
+
try {
|
|
102
|
+
await api.delete(
|
|
103
|
+
`/v1/users/${username}/tokens/ThinRemote`,
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Ignore 404 errors (token doesn't exist)
|
|
107
|
+
if (error.response && error.response.status !== 404) {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Create new token
|
|
113
|
+
spinner.text = 'Creating new user token...';
|
|
114
|
+
const createTokenResponse = await api.post(
|
|
115
|
+
`/v1/users/${username}/tokens`,
|
|
116
|
+
{
|
|
117
|
+
"enabled": true,
|
|
118
|
+
"allow": {"*": {"*": "*"}},
|
|
119
|
+
"deny": {},
|
|
120
|
+
"token": "ThinRemote",
|
|
121
|
+
"name": "ThinRemote",
|
|
122
|
+
"description": ""
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'application/json'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
const permanentToken = createTokenResponse.data.access_token;
|
|
131
|
+
|
|
132
|
+
// Save configuration with permanent token
|
|
133
|
+
writeConfig({
|
|
134
|
+
username,
|
|
135
|
+
server,
|
|
136
|
+
token: permanentToken
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
spinner.succeed('Authentication successful');
|
|
140
|
+
} catch (error) {
|
|
141
|
+
spinner.fail('Authentication failed');
|
|
142
|
+
|
|
143
|
+
if (error.response && error.response.status === 401) {
|
|
144
|
+
console.error(chalk.red('Invalid username or password'));
|
|
145
|
+
} else if (error.response && error.response.status === 404) {
|
|
146
|
+
console.error(chalk.red('Authentication endpoint not found. Please check the server URL.'));
|
|
147
|
+
} else {
|
|
148
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function refreshToken() {
|
|
160
|
+
try {
|
|
161
|
+
const config = readConfig();
|
|
162
|
+
if (!config || !config.username || !config.server || !config.refresh_token) {
|
|
163
|
+
console.log(chalk.yellow('No configuration found'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
//const spinner = ora('Refreshing user token...').start();
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
// Prepare OAuth2 refresh token form data
|
|
171
|
+
const formData = new URLSearchParams();
|
|
172
|
+
formData.append('grant_type', 'refresh_token');
|
|
173
|
+
formData.append('client_id', '3b40164d28730e416cbd');
|
|
174
|
+
formData.append('refresh_token', config.refresh_token);
|
|
175
|
+
|
|
176
|
+
// Refresh token using OAuth2 refresh flow
|
|
177
|
+
const response = await api.post(
|
|
178
|
+
`/oauth/token`,
|
|
179
|
+
formData,
|
|
180
|
+
{
|
|
181
|
+
headers: {
|
|
182
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Save new token in configuration
|
|
188
|
+
mergeConfig({
|
|
189
|
+
token: response.data.access_token,
|
|
190
|
+
refresh_token: response.data.refresh_token
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
//spinner.succeed('Token refreshed successfully');
|
|
194
|
+
return response.data.access_token;
|
|
195
|
+
|
|
196
|
+
} catch (error) {
|
|
197
|
+
//spinner.fail('Failed to refresh token');
|
|
198
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Delete user token from server
|
|
207
|
+
* @returns {Promise<void>}
|
|
208
|
+
*/
|
|
209
|
+
export async function deleteToken() {
|
|
210
|
+
try {
|
|
211
|
+
const config = readConfig();
|
|
212
|
+
if (!config || !config.username || !config.server || !config.token) {
|
|
213
|
+
console.log(chalk.yellow('No configuration found'));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const spinner = ora('Deleting user token...').start();
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
await api.delete(
|
|
221
|
+
`/v1/users/${config.username}/tokens/ThinRemote`,
|
|
222
|
+
);
|
|
223
|
+
spinner.succeed('Token deleted successfully');
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error.response && error.response.status === 404) {
|
|
226
|
+
spinner.succeed('Token was already deleted');
|
|
227
|
+
} else {
|
|
228
|
+
spinner.fail('Failed to delete token');
|
|
229
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Authenticate with token
|
|
239
|
+
* @returns {Promise<void>}
|
|
240
|
+
*/
|
|
241
|
+
async function authenticateWithToken() {
|
|
242
|
+
try {
|
|
243
|
+
// Ask for username and token
|
|
244
|
+
const username = await input({
|
|
245
|
+
message: 'Enter username:',
|
|
246
|
+
validate: (input) => {
|
|
247
|
+
return input ? true : 'Username is required';
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
const token = await input({
|
|
251
|
+
message: 'Enter token:',
|
|
252
|
+
type: 'password',
|
|
253
|
+
mask: '*',
|
|
254
|
+
validate: (input) => {
|
|
255
|
+
return input ? true : 'Token is required';
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Show spinner
|
|
260
|
+
const spinner = ora('Validating token...').start();
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Validate token by making a request to the proxies API
|
|
264
|
+
await api.get(
|
|
265
|
+
`/v1/proxies`,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Save configuration
|
|
269
|
+
mergeConfig({
|
|
270
|
+
username,
|
|
271
|
+
token
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
spinner.succeed('Token validation successful');
|
|
275
|
+
} catch (error) {
|
|
276
|
+
spinner.fail('Token validation failed');
|
|
277
|
+
|
|
278
|
+
if (error.response && error.response.status === 401) {
|
|
279
|
+
console.error(chalk.red('Invalid token'));
|
|
280
|
+
} else {
|
|
281
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Authenticate with OAuth token
|
|
294
|
+
* @returns {Promise<void>}
|
|
295
|
+
*/
|
|
296
|
+
async function authenticateWithOAuth() {
|
|
297
|
+
|
|
298
|
+
const client_id = '3b40164d28730e416cbd';
|
|
299
|
+
let device_code = '';
|
|
300
|
+
let oauth_tokens = {};
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
|
|
304
|
+
// Show spinner
|
|
305
|
+
const spinner = ora('Requesting OAuth User Code...').start();
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// Validate OAuth token by making a request to the proxies API
|
|
309
|
+
const { data } = await api.post(
|
|
310
|
+
`/oauth/device/authorize`,
|
|
311
|
+
{
|
|
312
|
+
client_id: client_id
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
headers: {
|
|
316
|
+
'content-type': 'application/x-www-form-urlencoded'
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
spinner.succeed('OAuth User Code received');
|
|
322
|
+
|
|
323
|
+
console.log('\nYour one time device code is: ' + chalk.bold(data.user_code));
|
|
324
|
+
|
|
325
|
+
console.log(chalk.blue('Press ' + chalk.bold('ENTER')) + ' to open your browser or submit your device code here: ' + data.verification_uri + '\n');
|
|
326
|
+
|
|
327
|
+
device_code = data.device_code;
|
|
328
|
+
|
|
329
|
+
let validated = false;
|
|
330
|
+
let pollDeviceIntervalId;
|
|
331
|
+
let interval = 5000; // Default polling interval
|
|
332
|
+
const controller = new AbortController();
|
|
333
|
+
|
|
334
|
+
function setPollingInterval(interval) {
|
|
335
|
+
if (pollDeviceIntervalId) {
|
|
336
|
+
clearInterval(pollDeviceIntervalId);
|
|
337
|
+
}
|
|
338
|
+
pollDeviceIntervalId = setInterval(pollDevice, interval);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function pollDevice () {
|
|
342
|
+
api.post(
|
|
343
|
+
`/oauth/token`,
|
|
344
|
+
{
|
|
345
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
346
|
+
client_id: client_id,
|
|
347
|
+
device_code: device_code
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
headers: {
|
|
351
|
+
'content-type': 'application/x-www-form-urlencoded'
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
.then((response) => {
|
|
356
|
+
if (response.status === 200) {
|
|
357
|
+
console.log(response);
|
|
358
|
+
validated = true;
|
|
359
|
+
oauth_tokens = response.data;
|
|
360
|
+
controller.abort("validated");
|
|
361
|
+
}
|
|
362
|
+
}).catch((e) => {
|
|
363
|
+
if (e.status === 400) {
|
|
364
|
+
// Check for specific OAuth error messages
|
|
365
|
+
if ( e.response.data.error === 'authorization_pending' ) {
|
|
366
|
+
//console.debug("Authorization pending, continuing to poll...");
|
|
367
|
+
} else if ( e.response.data.error === 'slow_down' ) {
|
|
368
|
+
//console.debug("Received slow_down, increasing polling interval");
|
|
369
|
+
// Increase polling interval to avoid hitting rate limits
|
|
370
|
+
clearInterval(pollDeviceIntervalId);
|
|
371
|
+
interval = 5000+1000;
|
|
372
|
+
setPollingInterval(interval); // Increase to 10 seconds
|
|
373
|
+
} else if ( e.response.data.error === 'access_denied' ) {
|
|
374
|
+
console.error(chalk.red('User denied the authorization request'));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
} else if ( e.response.data.error === 'expired_token' ) {
|
|
377
|
+
console.error(chalk.red('Device code expired'));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
} else {
|
|
380
|
+
console.error(chalk.red(`OAuth error: ${e.response.data.error}`));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Poll every 5 seconds
|
|
388
|
+
setPollingInterval(interval);
|
|
389
|
+
//pollDeviceIntervalId = setInterval(pollDevice, 5000, client_id, data.device_code);
|
|
390
|
+
|
|
391
|
+
// Timeout after 10 minutes
|
|
392
|
+
setTimeout(() => {
|
|
393
|
+
clearInterval(pollDeviceIntervalId)
|
|
394
|
+
controller.abort("timeout");
|
|
395
|
+
}, 600000);
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
// Wait for validation
|
|
399
|
+
await input({
|
|
400
|
+
message: 'Press ENTER to continue or wait for authentication...',
|
|
401
|
+
waitUserInput: true,
|
|
402
|
+
validate: function (input) {
|
|
403
|
+
open(data.verification_uri_complete, {app: [{name: apps.browser}, 'firefox-developer-edition']});
|
|
404
|
+
return '';
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
signal: controller.signal
|
|
409
|
+
}).catch((error) => {
|
|
410
|
+
if (error.cause === 'timeout') {
|
|
411
|
+
console.log(chalk.yellow('Polling aborted due to timeout'));
|
|
412
|
+
// Polling was aborted
|
|
413
|
+
} else if (error.cause === 'validated') {
|
|
414
|
+
console.log(chalk.green('Device authenticated successfully'));
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
clearInterval(pollDeviceIntervalId);
|
|
419
|
+
|
|
420
|
+
const token = oauth_tokens.access_token;
|
|
421
|
+
const refresh_token = oauth_tokens.refresh_token;
|
|
422
|
+
const decodedJWT = JSON.parse(Buffer.from(oauth_tokens.access_token.split('.')[1], 'base64').toString());
|
|
423
|
+
const username = decodedJWT.usr;
|
|
424
|
+
|
|
425
|
+
// Save configuration
|
|
426
|
+
mergeConfig({
|
|
427
|
+
username,
|
|
428
|
+
token,
|
|
429
|
+
refresh_token,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
spinner.succeed('OAuth token validation successful\n');
|
|
433
|
+
} catch (error) {
|
|
434
|
+
spinner.fail('OAuth token validation failed');
|
|
435
|
+
|
|
436
|
+
if (error.response && error.response.status === 401) {
|
|
437
|
+
console.error(chalk.red('Invalid OAuth token'));
|
|
438
|
+
} else {
|
|
439
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get access token from configuration
|
|
454
|
+
* @returns {string|axios.CancelToken|*|null}
|
|
455
|
+
*/
|
|
456
|
+
export function getAccessToken() {
|
|
457
|
+
const config = readConfig();
|
|
458
|
+
if (!config || !config.token) {
|
|
459
|
+
console.log(chalk.yellow('No token found in configuration'));
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
return config.token;
|
|
463
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get the config file path
|
|
7
|
+
* @returns {string} Config file path
|
|
8
|
+
*/
|
|
9
|
+
export function getConfigPath() {
|
|
10
|
+
const homeDir = os.homedir();
|
|
11
|
+
const configDir = path.join(homeDir, '.config', 'thinr-cli');
|
|
12
|
+
|
|
13
|
+
// Create config directory if it doesn't exist
|
|
14
|
+
if (!fs.existsSync(configDir)) {
|
|
15
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return path.join(configDir, 'config.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if configuration exists
|
|
23
|
+
* @returns {boolean} True if configuration exists
|
|
24
|
+
*/
|
|
25
|
+
export function configExists() {
|
|
26
|
+
const configPath = getConfigPath();
|
|
27
|
+
return fs.existsSync(configPath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read configuration
|
|
32
|
+
* @returns {Object} Configuration object
|
|
33
|
+
*/
|
|
34
|
+
export function readConfig() {
|
|
35
|
+
const configPath = getConfigPath();
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(configPath)) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const configData = fs.readFileSync(configPath, 'utf8');
|
|
43
|
+
return JSON.parse(configData);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
// If there's an error reading the config, return an empty object
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Write configuration
|
|
52
|
+
* @param {Object} config - Configuration object
|
|
53
|
+
*/
|
|
54
|
+
export function writeConfig(config) {
|
|
55
|
+
const configPath = getConfigPath();
|
|
56
|
+
const configData = JSON.stringify(config, null, 2);
|
|
57
|
+
|
|
58
|
+
fs.writeFileSync(configPath, configData, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Merge new configuration properties with existing one
|
|
63
|
+
* @param {Object} newConfig - New configuration properties
|
|
64
|
+
*/
|
|
65
|
+
export function mergeConfig(newConfig) {
|
|
66
|
+
const existingConfig = readConfig();
|
|
67
|
+
const mergedConfig = {...existingConfig, ...newConfig};
|
|
68
|
+
writeConfig(mergedConfig);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Delete configuration
|
|
73
|
+
* @returns {boolean} True if deleted successfully
|
|
74
|
+
*/
|
|
75
|
+
export function deleteConfig() {
|
|
76
|
+
const configPath = getConfigPath();
|
|
77
|
+
|
|
78
|
+
if (fs.existsSync(configPath)) {
|
|
79
|
+
try {
|
|
80
|
+
fs.unlinkSync(configPath);
|
|
81
|
+
return true;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true; // Already doesn't exist
|
|
88
|
+
}
|