error-ux-cli 1.0.0 → 1.1.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/commands.js CHANGED
@@ -1,50 +1,53 @@
1
- const fs = require('fs/promises');
2
- const auth = require('./auth');
3
- const git = require('./git');
4
- const news = require('./news');
5
- const storage = require('./storage');
6
- const utils = require('./utils');
7
-
8
- function askQuestion(rl, query) {
9
- return new Promise((resolve) => rl.question(query, resolve));
10
- }
11
-
12
- async function selectOption(rl, options) {
13
- options.forEach((option, index) => console.log(`${index + 1}. ${option}`));
14
- const choice = await askQuestion(rl, 'Select an option (number): ');
15
- const index = Number.parseInt(choice, 10) - 1;
16
- return index >= 0 && index < options.length ? index : -1;
17
- }
18
-
19
- function getUserShortcuts(user) {
20
- return user && user.shortcuts && typeof user.shortcuts === 'object' ? user.shortcuts : {};
21
- }
22
-
23
- function getUserTodos(user) {
24
- return Array.isArray(user?.todo) ? user.todo : [];
25
- }
26
-
27
- function getUserRepos(user) {
28
- return Array.isArray(user?.savedRepos) ? user.savedRepos : [];
29
- }
30
-
31
- function normalizeImportData(data) {
32
- return {
33
- name: typeof data?.name === 'string' && data.name.trim() ? data.name.trim() : 'Imported',
34
- todos: Array.isArray(data?.todos) ? data.todos : [],
35
- links: data?.links && typeof data.links === 'object' ? data.links : {},
36
- repos: Array.isArray(data?.repos) ? data.repos : [],
37
- newsApiKey: typeof data?.news_api_key === 'string' ? data.news_api_key : null
38
- };
39
- }
40
-
1
+ const fs = require('fs/promises');
2
+ const fsSync = require('fs');
3
+ const path = require('path');
4
+ const auth = require('./auth');
5
+ const git = require('./git');
6
+ const news = require('./news');
7
+ const cmPlugins = require('./plugins');
8
+ const storage = require('./storage');
9
+ const utils = require('./utils');
10
+
11
+ function askQuestion(rl, query) {
12
+ return new Promise((resolve) => rl.question(query, resolve));
13
+ }
14
+
15
+ async function selectOption(rl, options) {
16
+ options.forEach((option, index) => console.log(`${index + 1}. ${option}`));
17
+ const choice = await askQuestion(rl, 'Select an option (number): ');
18
+ const index = Number.parseInt(choice, 10) - 1;
19
+ return index >= 0 && index < options.length ? index : -1;
20
+ }
21
+
22
+ function getUserShortcuts(user) {
23
+ return user && user.shortcuts && typeof user.shortcuts === 'object' ? user.shortcuts : {};
24
+ }
25
+
26
+ function getUserTodos(user) {
27
+ return Array.isArray(user?.todo) ? user.todo : [];
28
+ }
29
+
30
+ function getUserRepos(user) {
31
+ return Array.isArray(user?.savedRepos) ? user.savedRepos : [];
32
+ }
33
+
34
+ function normalizeImportData(data) {
35
+ return {
36
+ name: typeof data?.name === 'string' && data.name.trim() ? data.name.trim() : 'Imported',
37
+ todos: Array.isArray(data?.todos) ? data.todos : [],
38
+ links: data?.links && typeof data.links === 'object' ? data.links : {},
39
+ repos: Array.isArray(data?.repos) ? data.repos : [],
40
+ newsApiKey: typeof data?.news_api_key === 'string' ? data.news_api_key : null
41
+ };
42
+ }
43
+
41
44
  function normalizePathInput(value) {
42
45
  const text = String(value || '').trim();
43
46
  if (
44
47
  (text.startsWith('"') && text.endsWith('"')) ||
45
48
  (text.startsWith("'") && text.endsWith("'"))
46
- ) {
47
- return text.slice(1, -1).trim();
49
+ ) {
50
+ return text.slice(1, -1).trim();
48
51
  }
49
52
  return text;
50
53
  }
@@ -67,7 +70,7 @@ async function configureRemoteForPush(rl, targetRepo) {
67
70
  if (!currentRemote) {
68
71
  console.log('No remote origin found.');
69
72
  const providedUrl = normalizePathInput(await askQuestion(rl, `Enter repository URL for ${targetRepo.name || 'origin'}: `));
70
- if (!providedUrl || !utils.isValidUrl(providedUrl)) {
73
+ if (!providedUrl || !utils.isValidResource(providedUrl)) {
71
74
  console.error('Error: Valid repository URL is required.');
72
75
  return null;
73
76
  }
@@ -96,7 +99,7 @@ async function configureRemoteForPush(rl, targetRepo) {
96
99
  }
97
100
 
98
101
  const newUrl = normalizePathInput(await askQuestion(rl, 'Enter new repository URL: '));
99
- if (!newUrl || !utils.isValidUrl(newUrl)) {
102
+ if (!newUrl || !utils.isValidResource(newUrl)) {
100
103
  console.error('Error: Valid repository URL is required.');
101
104
  return null;
102
105
  }
@@ -110,7 +113,7 @@ async function configureRemoteForPush(rl, targetRepo) {
110
113
 
111
114
  async function promptForRemoteReplacement(rl, targetRepo) {
112
115
  const newUrl = normalizePathInput(await askQuestion(rl, 'Enter new repository URL: '));
113
- if (!newUrl || !utils.isValidUrl(newUrl)) {
116
+ if (!newUrl || !utils.isValidResource(newUrl)) {
114
117
  console.error('Error: Valid repository URL is required.');
115
118
  return null;
116
119
  }
@@ -158,136 +161,151 @@ async function resolvePushBranch(rl, requestedBranch) {
158
161
  return normalized;
159
162
  }
160
163
 
161
- const nextVersion = await git.getNextBranchVersion(normalized);
162
- const versionedBranch = `${normalized}_v${nextVersion}`;
163
164
  console.log(`Branch "${normalized}" already exists.`);
164
- const choice = await selectOption(rl, [
165
- `Use existing branch: ${normalized}`,
166
- `Create new branch: ${versionedBranch}`,
167
- 'Cancel'
168
- ]);
165
+ const answer = await askQuestion(rl, `Use existing branch "${normalized}"? (y/n): `);
169
166
 
170
- if (choice === 0) {
167
+ if (answer.toLowerCase() === 'y') {
171
168
  return normalized;
172
- }
173
- if (choice === 1) {
169
+ } else if (answer.toLowerCase() === 'n') {
170
+ const nextVersion = await git.getNextBranchVersion(normalized);
171
+ const versionedBranch = `${normalized}_v${nextVersion}`;
172
+ console.log(`Automatically appending version. New branch: ${versionedBranch}`);
174
173
  return versionedBranch;
175
174
  }
175
+
176
176
  return null;
177
177
  }
178
-
179
- async function handleLink(rl) {
180
- const config = await storage.loadConfig();
181
- const user = storage.getActiveUser(config);
182
- if (!user) return;
183
-
184
- let url = await askQuestion(rl, 'Enter URL: ');
185
- url = url.trim();
186
- if (!url || !utils.isValidUrl(url)) {
187
- console.error('Error: Invalid URL format.');
188
- return;
189
- }
190
-
191
- let shortcut = await askQuestion(rl, 'Enter shortcut name: ');
192
- shortcut = shortcut.toLowerCase().trim();
193
-
194
- if (!shortcut || shortcut.includes(' ') || utils.isValidUrl(shortcut)) {
195
- console.error('Error: Invalid shortcut name.');
196
- return;
197
- }
198
-
199
- const shortcuts = getUserShortcuts(user);
200
- if (shortcuts[shortcut]) {
201
- const overwrite = await askQuestion(rl, `Shortcut "${shortcut}" already exists. Overwrite? (y/n): `);
202
- if (overwrite.toLowerCase() !== 'y') {
203
- console.log('Action cancelled.');
204
- return;
205
- }
206
- }
207
-
208
- shortcuts[shortcut] = {type: 'link', value: url};
209
- user.shortcuts = shortcuts;
210
-
211
- await storage.saveConfig(config);
212
- console.log(`Shortcut "${shortcut}" saved successfully!`);
213
- }
214
-
215
- async function handleLinks() {
216
- const config = await storage.loadConfig();
217
- const user = storage.getActiveUser(config);
218
- if (!user) return;
219
-
220
- const shortcuts = getUserShortcuts(user);
221
- const keys = Object.keys(shortcuts);
222
- if (keys.length === 0) {
223
- console.log('No shortcuts saved yet.');
224
- return;
225
- }
226
-
227
- console.log('');
228
- keys.sort().forEach((key) => {
229
- console.log(`${key} -> ${shortcuts[key].value}`);
230
- });
231
- console.log('');
232
- }
233
-
234
- async function handleUnlink(rl, shortcut) {
235
- let target = shortcut;
236
- if (!target) {
237
- target = await askQuestion(rl, 'Enter shortcut to delete: ');
238
- }
239
-
240
- const cleanShortcut = String(target || '').toLowerCase().trim();
241
- const config = await storage.loadConfig();
242
- const user = storage.getActiveUser(config);
243
- if (!user) return;
244
-
245
- const shortcuts = getUserShortcuts(user);
246
- if (!shortcuts[cleanShortcut]) {
247
- console.error(`Error: Shortcut "${cleanShortcut}" does not exist.`);
248
- return;
249
- }
250
-
251
- delete shortcuts[cleanShortcut];
252
- user.shortcuts = shortcuts;
253
-
254
- await storage.saveConfig(config);
255
- console.log(`Shortcut "${cleanShortcut}" deleted.`);
256
- }
257
-
258
- async function initOnboarding(rl) {
259
- console.log('\n--- First-Time Onboarding ---');
260
- console.log("Welcome to error-ux! Let's set up your profile.\n");
261
-
262
- let name = '';
263
- while (!name) {
264
- name = (await askQuestion(rl, 'Choose a username: ')).trim();
265
- if (!name) console.error('Error: Username is required.');
266
- }
267
-
268
- let password = '';
269
- while (!password) {
270
- password = await auth.askPassword(rl, 'Set a password (min 1 char): ');
271
- if (!password) console.error('Error: Password must be at least 1 character.');
272
- }
273
-
274
- console.log('\n(Optional) News API key from GNews. Skip by pressing Enter.');
275
- const newsApiKey = (await askQuestion(rl, 'News API Key: ')).trim();
276
-
178
+
179
+ async function handleLink(rl) {
180
+ const config = await storage.loadConfig();
181
+ const user = storage.getActiveUser(config);
182
+ if (!user) return;
183
+
184
+ let url = await askQuestion(rl, 'Enter URL or Local Path: ');
185
+ url = url.trim();
186
+ if (!url || !utils.isValidResource(url)) {
187
+ console.error('Error: Invalid URL or Local Path.');
188
+ return;
189
+ }
190
+
191
+ let shortcut = await askQuestion(rl, 'Enter shortcut name: ');
192
+ shortcut = shortcut.toLowerCase().trim();
193
+
194
+ if (!shortcut || shortcut.includes(' ') || utils.isValidResource(shortcut)) {
195
+ console.error('Error: Invalid shortcut name.');
196
+ return;
197
+ }
198
+
199
+ const shortcuts = getUserShortcuts(user);
200
+ if (shortcuts[shortcut]) {
201
+ const overwrite = await askQuestion(rl, `Shortcut "${shortcut}" already exists. Overwrite? (y/n): `);
202
+ if (overwrite.toLowerCase() !== 'y') {
203
+ console.log('Action cancelled.');
204
+ return;
205
+ }
206
+ }
207
+
208
+ shortcuts[shortcut] = { type: 'link', value: url };
209
+ user.shortcuts = shortcuts;
210
+
211
+ await storage.saveConfig(config);
212
+ console.log(`Shortcut "${shortcut}" saved successfully!`);
213
+ }
214
+
215
+ async function handleLinks() {
216
+ const config = await storage.loadConfig();
217
+ const user = storage.getActiveUser(config);
218
+ if (!user) return;
219
+
220
+ const shortcuts = getUserShortcuts(user);
221
+ const keys = Object.keys(shortcuts);
222
+ if (keys.length === 0) {
223
+ console.log('No shortcuts saved yet.');
224
+ return;
225
+ }
226
+
227
+ console.log('');
228
+ keys.sort().forEach((key) => {
229
+ console.log(`${key} -> ${shortcuts[key].value}`);
230
+ });
231
+ console.log('');
232
+ }
233
+
234
+ async function handleUnlink(rl, shortcut) {
235
+ let target = shortcut;
236
+ if (!target) {
237
+ target = await askQuestion(rl, 'Enter shortcut to delete: ');
238
+ }
239
+
240
+ const cleanShortcut = String(target || '').toLowerCase().trim();
241
+ const config = await storage.loadConfig();
242
+ const user = storage.getActiveUser(config);
243
+ if (!user) return;
244
+
245
+ const shortcuts = getUserShortcuts(user);
246
+ if (!shortcuts[cleanShortcut]) {
247
+ console.error(`Error: Shortcut "${cleanShortcut}" does not exist.`);
248
+ return;
249
+ }
250
+
251
+ delete shortcuts[cleanShortcut];
252
+ user.shortcuts = shortcuts;
253
+
254
+ await storage.saveConfig(config);
255
+ console.log(`Shortcut "${cleanShortcut}" deleted.`);
256
+ }
257
+
258
+ async function initOnboarding(rl) {
259
+ console.log('\n--- First-Time Onboarding ---');
260
+ console.log("Welcome to error-ux! Let's set up your profile.\n");
261
+
262
+ let name = '';
263
+ while (!name) {
264
+ name = (await askQuestion(rl, 'Choose a username: ')).trim();
265
+ if (!name) console.error('Error: Username is required.');
266
+ }
267
+
268
+ let password = '';
269
+ while (!password) {
270
+ password = await auth.askPassword(rl, 'Set a password (min 1 char): ');
271
+ if (!password) console.error('Error: Password must be at least 1 character.');
272
+ }
273
+
274
+ console.log('\n(Optional) News API key from GNews. Skip by pressing Enter.');
275
+ const newsApiKey = (await askQuestion(rl, 'News API Key: ')).trim();
276
+
277
277
  const config = {
278
278
  activeUser: name,
279
279
  users: {
280
280
  [name]: {
281
281
  password_hash: auth.hashPassword(password),
282
- news_api_key: newsApiKey || null,
283
- shortcuts: {},
284
- activeRepo: null,
285
- savedRepos: [],
286
- todo: []
287
- }
282
+ news_api_key: newsApiKey || null,
283
+ shortcuts: {},
284
+ activeRepo: null,
285
+ savedRepos: [],
286
+ todo: []
287
+ }
288
288
  }
289
289
  };
290
290
 
291
+ // If Admin detected (128-char password), setup encrypted vault
292
+ const isAdminKey = password.length === 128;
293
+
294
+ if (isAdminKey) {
295
+ console.log('\n--- \uD83D\uDD12 Admin Master Key Detected ---');
296
+ console.log('Setting up secure Cloud Sync vault...\n');
297
+ const pat = (await askQuestion(rl, 'GitHub Personal Access Token: ')).trim();
298
+ const gistId = (await askQuestion(rl, 'Private Gist ID: ')).trim();
299
+
300
+ if (pat && gistId) {
301
+ const vaultData = JSON.stringify({ pat, gistId });
302
+ config.users[name].vault = storage.encrypt(vaultData, password);
303
+ console.log('Vault initialized and encrypted locally.');
304
+ } else {
305
+ console.warn('Admin sync skipped (PAT or Gist ID missing).');
306
+ }
307
+ }
308
+
291
309
  if (newsApiKey) {
292
310
  utils.saveEnvValue('NEWS_API_KEY', newsApiKey);
293
311
  }
@@ -296,158 +314,158 @@ async function initOnboarding(rl) {
296
314
  console.log(`\nSetup complete. Welcome aboard, ${name}.\n`);
297
315
  return config;
298
316
  }
299
-
300
- async function handleUser(rl, args) {
301
- const config = (await storage.loadConfig()) || {activeUser: null, users: {}};
302
- if (!config.users || typeof config.users !== 'object') {
303
- config.users = {};
304
- }
305
-
306
- const parts = args ? args.trim().split(/\s+/) : [];
307
- const command = parts[0]?.toLowerCase();
308
-
309
- switch (command) {
310
- case 'create': {
311
- const inputName = parts[1] || await askQuestion(rl, 'Enter new username: ');
312
- const newName = String(inputName || '').trim();
313
- if (!newName) {
314
- console.error('error');
315
- return;
316
- }
317
- if (config.users[newName]) {
318
- console.error('error');
319
- return;
320
- }
321
-
322
- const newPwd = await auth.askPassword(rl, `Enter password for ${newName}: `);
323
- if (!newPwd) {
324
- console.error('error');
325
- return;
326
- }
327
-
328
- config.users[newName] = {
329
- password_hash: auth.hashPassword(newPwd),
330
- news_api_key: null,
331
- shortcuts: {},
332
- activeRepo: null,
333
- savedRepos: [],
334
- todo: []
335
- };
336
- if (!config.activeUser) {
337
- config.activeUser = newName;
338
- }
339
-
340
- await storage.saveConfig(config);
341
- console.log(`User "${newName}" created.`);
342
- return;
343
- }
344
-
345
- case 'login': {
346
- const inputName = parts[1] || await askQuestion(rl, 'Username: ');
347
- const loginName = String(inputName || '').trim();
348
- if (!config.users[loginName]) {
349
- console.error('error');
350
- return;
351
- }
352
-
353
- const loginPwd = await auth.askPassword(rl, 'Password: ');
317
+
318
+ async function handleUser(rl, args) {
319
+ const config = (await storage.loadConfig()) || { activeUser: null, users: {} };
320
+ if (!config.users || typeof config.users !== 'object') {
321
+ config.users = {};
322
+ }
323
+
324
+ const parts = args ? args.trim().split(/\s+/) : [];
325
+ const command = parts[0]?.toLowerCase();
326
+
327
+ switch (command) {
328
+ case 'create': {
329
+ const inputName = parts[1] || await askQuestion(rl, 'Enter new username: ');
330
+ const newName = String(inputName || '').trim();
331
+ if (!newName) {
332
+ console.error('error');
333
+ return;
334
+ }
335
+ if (config.users[newName]) {
336
+ console.error('error');
337
+ return;
338
+ }
339
+
340
+ const newPwd = await auth.askPassword(rl, `Enter password for ${newName}: `);
341
+ if (!newPwd) {
342
+ console.error('error');
343
+ return;
344
+ }
345
+
346
+ config.users[newName] = {
347
+ password_hash: auth.hashPassword(newPwd),
348
+ news_api_key: null,
349
+ shortcuts: {},
350
+ activeRepo: null,
351
+ savedRepos: [],
352
+ todo: []
353
+ };
354
+ if (!config.activeUser) {
355
+ config.activeUser = newName;
356
+ }
357
+
358
+ await storage.saveConfig(config);
359
+ console.log(`User "${newName}" created.`);
360
+ return;
361
+ }
362
+
363
+ case 'login': {
364
+ const inputName = parts[1] || await askQuestion(rl, 'Username: ');
365
+ const loginName = String(inputName || '').trim();
366
+ if (!config.users[loginName]) {
367
+ console.error('error');
368
+ return;
369
+ }
370
+
371
+ const loginPwd = await auth.askPassword(rl, 'Password: ');
354
372
  if (!auth.verifyPassword(loginPwd, config.users[loginName].password_hash)) {
355
373
  console.error('error');
356
374
  return;
357
375
  }
358
376
 
359
377
  config.activeUser = loginName;
360
- await ensureUserNewsApiKey(rl, config, config.users[loginName], {forcePrompt: true});
378
+ await ensureUserNewsApiKey(rl, config, config.users[loginName], { forcePrompt: true });
361
379
  await storage.saveConfig(config);
362
380
  console.log(`Logged in as ${loginName}.`);
363
381
  await handleDashboard(rl);
364
382
  return;
365
383
  }
366
-
367
- case 'list':
368
- console.log('\nProfiles:');
369
- Object.keys(config.users).sort().forEach((username) => {
370
- const active = config.activeUser === username ? '*' : ' ';
371
- console.log(`${active} ${username}`);
372
- });
373
- return;
374
-
375
- case 'current':
376
- console.log(`Active user: ${config.activeUser || '(none)'}`);
377
- return;
378
-
379
- case 'delete': {
380
- const inputName = parts[1] || await askQuestion(rl, 'Username to delete: ');
381
- const deleteName = String(inputName || '').trim();
382
- if (!config.users[deleteName]) {
383
- console.error('error');
384
- return;
385
- }
386
- if (deleteName === config.activeUser) {
387
- console.error('error');
388
- return;
389
- }
390
-
391
- delete config.users[deleteName];
392
- await storage.saveConfig(config);
393
- console.log(`User "${deleteName}" deleted.`);
394
- return;
395
- }
396
-
397
- default:
398
- console.log('Usage: user [create|login|list|current|delete]');
399
- }
400
- }
401
-
402
- async function handleExport(rl, args) {
403
- const config = await storage.loadConfig();
404
- const user = storage.getActiveUser(config);
405
- if (!user || !config?.activeUser) return;
406
-
407
- const outputPath = normalizePathInput(args) || `error-ux-${config.activeUser}-data.json`;
408
- const exportData = {
409
- version: 1,
410
- exported_at: new Date().toISOString(),
411
- name: config.activeUser,
412
- todos: getUserTodos(user),
413
- links: getUserShortcuts(user),
414
- repos: getUserRepos(user),
415
- news_api_key: user.news_api_key || null
416
- };
417
-
418
- await fs.writeFile(outputPath, JSON.stringify(exportData, null, 2), 'utf8');
419
- console.log(`Data exported to ${outputPath} (passwords excluded)`);
420
- }
421
-
422
- async function handleImport(rl, args) {
423
- const filePath = normalizePathInput(args) || normalizePathInput(await askQuestion(rl, 'Enter path to import file: '));
424
- if (!filePath) return;
425
-
426
- try {
427
- const raw = await fs.readFile(filePath.trim(), 'utf8');
428
- const imported = normalizeImportData(JSON.parse(raw));
429
- const config = (await storage.loadConfig()) || {activeUser: null, users: {}};
430
- if (!config.users || typeof config.users !== 'object') {
431
- config.users = {};
432
- }
433
-
384
+
385
+ case 'list':
386
+ console.log('\nProfiles:');
387
+ Object.keys(config.users).sort().forEach((username) => {
388
+ const active = config.activeUser === username ? '*' : ' ';
389
+ console.log(`${active} ${username}`);
390
+ });
391
+ return;
392
+
393
+ case 'current':
394
+ console.log(`Active user: ${config.activeUser || '(none)'}`);
395
+ return;
396
+
397
+ case 'delete': {
398
+ const inputName = parts[1] || await askQuestion(rl, 'Username to delete: ');
399
+ const deleteName = String(inputName || '').trim();
400
+ if (!config.users[deleteName]) {
401
+ console.error('error');
402
+ return;
403
+ }
404
+ if (deleteName === config.activeUser) {
405
+ console.error('error');
406
+ return;
407
+ }
408
+
409
+ delete config.users[deleteName];
410
+ await storage.saveConfig(config);
411
+ console.log(`User "${deleteName}" deleted.`);
412
+ return;
413
+ }
414
+
415
+ default:
416
+ console.log('Usage: user [create|login|list|current|delete]');
417
+ }
418
+ }
419
+
420
+ async function handleExport(rl, args) {
421
+ const config = await storage.loadConfig();
422
+ const user = storage.getActiveUser(config);
423
+ if (!user || !config?.activeUser) return;
424
+
425
+ const outputPath = normalizePathInput(args) || `error-ux-${config.activeUser}-data.json`;
426
+ const exportData = {
427
+ version: 1,
428
+ exported_at: new Date().toISOString(),
429
+ name: config.activeUser,
430
+ todos: getUserTodos(user),
431
+ links: getUserShortcuts(user),
432
+ repos: getUserRepos(user),
433
+ news_api_key: user.news_api_key || null
434
+ };
435
+
436
+ await fs.writeFile(outputPath, JSON.stringify(exportData, null, 2), 'utf8');
437
+ console.log(`Data exported to ${outputPath} (passwords excluded)`);
438
+ }
439
+
440
+ async function handleImport(rl, args) {
441
+ const filePath = normalizePathInput(args) || normalizePathInput(await askQuestion(rl, 'Enter path to import file: '));
442
+ if (!filePath) return;
443
+
444
+ try {
445
+ const raw = await fs.readFile(filePath.trim(), 'utf8');
446
+ const imported = normalizeImportData(JSON.parse(raw));
447
+ const config = (await storage.loadConfig()) || { activeUser: null, users: {} };
448
+ if (!config.users || typeof config.users !== 'object') {
449
+ config.users = {};
450
+ }
451
+
434
452
  if (!config.users[imported.name]) {
435
453
  console.log(`Creating new profile for "${imported.name}"...`);
436
454
  const pwd = await auth.askPassword(rl, `Set initial password for ${imported.name}: `);
437
455
  if (!pwd) {
438
456
  console.error('error');
439
- return;
440
- }
441
-
442
- config.users[imported.name] = {
443
- password_hash: auth.hashPassword(pwd),
444
- news_api_key: imported.newsApiKey,
445
- shortcuts: imported.links,
446
- activeRepo: imported.repos[0] || null,
447
- savedRepos: imported.repos,
448
- todo: imported.todos
449
- };
450
-
457
+ return;
458
+ }
459
+
460
+ config.users[imported.name] = {
461
+ password_hash: auth.hashPassword(pwd),
462
+ news_api_key: imported.newsApiKey,
463
+ shortcuts: imported.links,
464
+ activeRepo: imported.repos[0] || null,
465
+ savedRepos: imported.repos,
466
+ todo: imported.todos
467
+ };
468
+
451
469
  if (!config.activeUser) {
452
470
  config.activeUser = imported.name;
453
471
  }
@@ -460,306 +478,330 @@ async function handleImport(rl, args) {
460
478
  user.todo = imported.todos;
461
479
  user.news_api_key = imported.newsApiKey;
462
480
  }
463
-
464
- await storage.saveConfig(config);
465
- console.log('Import successful.');
466
- } catch {
467
- console.error('error');
468
- }
469
- }
470
-
471
- async function handleRepo(rl, args) {
472
- const config = await storage.loadConfig();
473
- const user = storage.getActiveUser(config);
474
- if (!user) return;
475
-
476
- user.savedRepos = getUserRepos(user);
477
-
478
- const parts = args ? args.trim().split(/\s+/) : [];
479
- const command = parts[0]?.toLowerCase();
480
-
481
- switch (command) {
482
- case 'set': {
483
- const name = parts[1];
484
- const url = parts[2];
485
- if (!name || !url || !utils.isValidUrl(url)) {
486
- console.error('Usage: repo set <name> <url>');
487
- return;
488
- }
489
-
490
- const existingIndex = user.savedRepos.findIndex((repo) => repo.name === name);
491
- if (existingIndex !== -1) {
492
- user.savedRepos[existingIndex].url = url;
493
- } else {
494
- user.savedRepos.push({name, url});
495
- }
496
-
497
- await storage.saveConfig(config);
498
- console.log(`Repository "${name}" saved.`);
499
- return;
500
- }
501
-
502
- case 'use': {
503
- const useName = parts[1];
504
- const repo = user.savedRepos.find((item) => item.name === useName);
505
- if (!repo) {
506
- console.error('error');
507
- return;
508
- }
509
-
510
- user.activeRepo = repo;
511
- await storage.saveConfig(config);
512
- console.log(`Now using repository: ${repo.name}`);
513
- return;
514
- }
515
-
516
- case 'list':
517
- if (user.savedRepos.length === 0) {
518
- console.log('No saved repositories.');
519
- return;
520
- }
521
-
522
- console.log('\nSaved Repositories:');
523
- user.savedRepos.forEach((repo) => {
524
- const active = user.activeRepo && user.activeRepo.name === repo.name ? '*' : ' ';
525
- console.log(`${active} ${repo.name.padEnd(12)} -> ${repo.url}`);
526
- });
527
- return;
528
-
529
- case 'current':
530
- if (user.activeRepo) {
531
- console.log(`Current active repository: ${user.activeRepo.name} (${user.activeRepo.url})`);
532
- } else {
533
- console.log('No active repository set.');
534
- }
535
- return;
536
-
537
- default:
538
- console.log('Usage: repo [set|use|list|current]');
539
- }
540
- }
541
-
542
- async function handleUninstall(rl) {
543
- const confirm = await askQuestion(rl, 'Are you sure you want to delete all error-ux data? (y/n): ');
544
- if (confirm.toLowerCase() !== 'y') {
545
- console.log('Uninstall cancelled.');
546
- return;
547
- }
548
-
549
- const config = await storage.loadConfig();
550
- const user = storage.getActiveUser(config);
551
- if (!user) {
552
- console.error('error');
553
- return;
554
- }
555
-
556
- const authPassword = await auth.askPassword(rl, 'Confirm password to uninstall: ');
557
- if (!auth.verifyPassword(authPassword, user.password_hash)) {
558
- console.error('error');
559
- return;
560
- }
561
-
562
- await storage.wipeAllData();
563
- process.env.NEWS_API_KEY = '';
564
- console.log('\nAll data deleted successfully.');
565
- console.log('To complete the removal, run: npm uninstall -g error-ux');
566
- process.exit(0);
567
- }
568
-
481
+
482
+ await storage.saveConfig(config);
483
+ console.log('Import successful.');
484
+ } catch {
485
+ console.error('error');
486
+ }
487
+ }
488
+
489
+ async function handleRepo(rl, args) {
490
+ const config = await storage.loadConfig();
491
+ const user = storage.getActiveUser(config);
492
+ if (!user) return;
493
+
494
+ user.savedRepos = getUserRepos(user);
495
+
496
+ const parts = args ? args.trim().split(/\s+/) : [];
497
+ const command = parts[0]?.toLowerCase();
498
+
499
+ switch (command) {
500
+ case 'set': {
501
+ const name = parts[1];
502
+ const url = parts[2];
503
+ if (!name || !url || !utils.isValidResource(url)) {
504
+ console.error('Usage: repo set <name> <url>');
505
+ return;
506
+ }
507
+
508
+ const existingIndex = user.savedRepos.findIndex((repo) => repo.name === name);
509
+ if (existingIndex !== -1) {
510
+ user.savedRepos[existingIndex].url = url;
511
+ } else {
512
+ user.savedRepos.push({ name, url });
513
+ }
514
+
515
+ await storage.saveConfig(config);
516
+ console.log(`Repository "${name}" saved.`);
517
+ return;
518
+ }
519
+
520
+ case 'use': {
521
+ const useName = parts[1];
522
+ const repo = user.savedRepos.find((item) => item.name === useName);
523
+ if (!repo) {
524
+ console.error('error');
525
+ return;
526
+ }
527
+
528
+ user.activeRepo = repo;
529
+ await storage.saveConfig(config);
530
+ console.log(`Now using repository: ${repo.name}`);
531
+ return;
532
+ }
533
+
534
+ case 'list':
535
+ if (user.savedRepos.length === 0) {
536
+ console.log('No saved repositories.');
537
+ return;
538
+ }
539
+
540
+ console.log('\nSaved Repositories:');
541
+ user.savedRepos.forEach((repo) => {
542
+ const active = user.activeRepo && user.activeRepo.name === repo.name ? '*' : ' ';
543
+ console.log(`${active} ${repo.name.padEnd(12)} -> ${repo.url}`);
544
+ });
545
+ return;
546
+
547
+ case 'current':
548
+ if (user.activeRepo) {
549
+ console.log(`Current active repository: ${user.activeRepo.name} (${user.activeRepo.url})`);
550
+ } else {
551
+ console.log('No active repository set.');
552
+ }
553
+ return;
554
+
555
+ default:
556
+ console.log('Usage: repo [set|use|list|current]');
557
+ }
558
+ }
559
+
560
+ async function handleUninstall(rl) {
561
+ const confirm = await askQuestion(rl, 'Are you sure you want to delete all error-ux data? (y/n): ');
562
+ if (confirm.toLowerCase() !== 'y') {
563
+ console.log('Uninstall cancelled.');
564
+ return;
565
+ }
566
+
567
+ const config = await storage.loadConfig();
568
+ const user = storage.getActiveUser(config);
569
+ if (!user) {
570
+ console.error('error');
571
+ return;
572
+ }
573
+
574
+ const authPassword = await auth.askPassword(rl, 'Confirm password to uninstall: ');
575
+ if (!auth.verifyPassword(authPassword, user.password_hash)) {
576
+ console.error('error');
577
+ return;
578
+ }
579
+
580
+ await storage.wipeAllData();
581
+ process.env.NEWS_API_KEY = '';
582
+ console.log('\nAll data deleted successfully.');
583
+ console.log('To complete the removal, run: npm uninstall -g error-ux-cli');
584
+ process.exit(0);
585
+ }
586
+
569
587
  async function handlePush(rl, args) {
570
- if (!args) {
571
- console.error('Error: Please provide a name for the push (e.g., push feature)');
572
- return;
573
- }
574
-
575
- const config = await storage.loadConfig();
576
- const user = storage.getActiveUser(config);
577
- if (!user) return;
578
-
579
- user.savedRepos = getUserRepos(user);
580
-
581
- if (utils.isSystemFolder(process.cwd())) {
582
- console.warn('\n--- WARNING ---');
583
- console.warn('You are pushing from a system folder.');
584
- const confirm = await askQuestion(rl, 'Proceed? (y/n): ');
585
- if (confirm.toLowerCase() !== 'y') return;
586
- }
587
-
588
- const target = await git.getPushContext(args, user.savedRepos, rl);
589
- if (!target || !target.targetRepo) return;
590
-
591
- const resolvedBranch = await resolvePushBranch(rl, target.branchBase);
588
+ const config = await storage.loadConfig();
589
+ const user = storage.getActiveUser(config);
590
+ if (!user) return;
591
+
592
+ const currentDir = process.cwd();
593
+ user.savedRepos = getUserRepos(user);
594
+
595
+ // 1. Resolve Project Repo (Folder-Aware)
596
+ let linkedRepo = storage.getProjectLink(user, currentDir);
597
+ let targetRepo = null;
598
+
599
+ if (linkedRepo) {
600
+ targetRepo = linkedRepo;
601
+ } else {
602
+ // If no link, see if we can deduce from current origin
603
+ const currentRemote = await git.getRemoteUrl();
604
+ if (currentRemote) {
605
+ targetRepo = { name: 'origin', url: currentRemote };
606
+ storage.saveProjectLink(user, currentDir, targetRepo);
607
+ await storage.saveConfig(config);
608
+ } else {
609
+ // New onboarding for this folder
610
+ console.log('\n--- Project Setup ---');
611
+ console.log('This folder is not linked to a repository yet.');
612
+
613
+ if (user.savedRepos.length > 0) {
614
+ console.log('Select a saved repository to link to this project:');
615
+ const options = [...user.savedRepos.map(r => `${r.name} (${r.url})`), 'Enter new URL'];
616
+ const choice = await selectOption(rl, options);
617
+
618
+ if (choice >= 0 && choice < user.savedRepos.length) {
619
+ targetRepo = user.savedRepos[choice];
620
+ } else if (choice === user.savedRepos.length) {
621
+ const url = normalizePathInput(await askQuestion(rl, 'Enter repository URL: '));
622
+ if (url && utils.isValidResource(url)) {
623
+ targetRepo = { name: 'origin', url };
624
+ }
625
+ }
626
+ } else {
627
+ const url = normalizePathInput(await askQuestion(rl, 'Enter repository URL: '));
628
+ if (url && utils.isValidUrl(url)) {
629
+ targetRepo = { name: 'origin', url };
630
+ }
631
+ }
632
+
633
+ if (!targetRepo) {
634
+ console.error('Error: No target repository configured.');
635
+ return;
636
+ }
637
+
638
+ // Save the link for future "sticky" detection
639
+ storage.saveProjectLink(user, currentDir, targetRepo);
640
+ await storage.saveConfig(config);
641
+ console.log(`Linked this project to: ${targetRepo.url}`);
642
+ }
643
+ }
644
+
645
+ // 2. Resolve Arguments
646
+ const parts = String(args || '').trim().split(/\s+/).filter(Boolean);
647
+ const branchBase = parts[0] || 'main'; // Default to main if empty
648
+ const message = parts.slice(1).join(' ') || `Update - ${new Date().toLocaleDateString()}`;
649
+
650
+ // 3. Resolve Branch (Smart Y/N)
651
+ const resolvedBranch = await resolvePushBranch(rl, branchBase);
592
652
  if (!resolvedBranch) {
593
653
  console.log('Push cancelled.');
594
654
  return;
595
655
  }
596
656
 
597
- const preparedRepo = await configureRemoteForPush(rl, {...target.targetRepo});
598
- if (!preparedRepo) return;
599
-
657
+ // 4. Execute with Spinner
600
658
  try {
601
- console.log(`Preparing push for branch: ${resolvedBranch}`);
602
- const pushResult = await git.automatedPush(preparedRepo, target.message, resolvedBranch);
603
- if (pushResult.branchExisted) {
604
- console.log(`Using existing branch: ${pushResult.branch}`);
605
- } else {
606
- console.log(`Created branch: ${pushResult.branch}`);
607
- }
608
- if (pushResult.committed) {
609
- console.log(`Committed changes: ${pushResult.message}`);
610
- } else {
611
- console.log('No new changes to commit. Pushing existing branch state.');
612
- }
613
- console.log(`Push successful: ${pushResult.branch} -> ${pushResult.remoteUrl}`);
614
- if (!user.savedRepos.find((repo) => repo.url === preparedRepo.url)) {
615
- user.savedRepos.push(preparedRepo);
659
+ console.log('');
660
+ const pushResult = await utils.withSpinner('Preparing and pushing changes...', async () => {
661
+ return await git.automatedPush(targetRepo, message, resolvedBranch);
662
+ });
663
+
664
+ // 5. Success Summary Box
665
+ const summaryLines = [
666
+ `Branch: ${pushResult.branch}`,
667
+ `Action: ${pushResult.branchExisted ? 'Updated' : 'Created'} branch`,
668
+ `Changes: ${pushResult.committed ? 'Committed new changes' : 'Existing state'}`,
669
+ `Destination: ${pushResult.remoteUrl}`
670
+ ];
671
+
672
+ const box = utils.generateBox('PUSH SUCCESSFUL', summaryLines, 50);
673
+ console.log('\n' + box.join('\n') + '\n');
674
+
675
+ // Ensure the repo is in savedRepos for future use elsewhere
676
+ if (!user.savedRepos.find(r => r.url === targetRepo.url)) {
677
+ user.savedRepos.push(targetRepo);
616
678
  await storage.saveConfig(config);
617
679
  }
618
680
  } catch (error) {
619
- const detail = error?.stderr || error?.stdout || error?.message || '';
620
- if (String(detail).includes('Repository not found')) {
621
- console.error('Push failed: remote repository not found.');
622
- const retry = await askQuestion(rl, 'Do you want to update the remote and retry? (y/n): ');
623
- if (retry.toLowerCase() === 'y') {
624
- const retriedRepo = await promptForRemoteReplacement(rl, preparedRepo);
625
- if (!retriedRepo) return;
626
-
627
- try {
628
- console.log(`Retrying push for branch: ${resolvedBranch}`);
629
- const retryResult = await git.automatedPush(retriedRepo, target.message, resolvedBranch);
630
- if (retryResult.branchExisted) {
631
- console.log(`Using existing branch: ${retryResult.branch}`);
632
- } else {
633
- console.log(`Created branch: ${retryResult.branch}`);
634
- }
635
- if (retryResult.committed) {
636
- console.log(`Committed changes: ${retryResult.message}`);
637
- } else {
638
- console.log('No new changes to commit. Pushing existing branch state.');
639
- }
640
- console.log(`Push successful: ${retryResult.branch} -> ${retryResult.remoteUrl}`);
641
- if (!user.savedRepos.find((repo) => repo.url === retriedRepo.url)) {
642
- user.savedRepos.push(retriedRepo);
643
- await storage.saveConfig(config);
644
- }
645
- return;
646
- } catch (retryError) {
647
- const retryDetail = retryError?.stderr || retryError?.stdout || retryError?.message || '';
648
- const retryLine = String(retryDetail).trim().split(/\r?\n/).find(Boolean);
649
- console.error(retryLine ? `Push failed: ${retryLine}` : 'error');
650
- return;
651
- }
681
+ const detail = error?.stderr || error?.stdout || error?.message || 'Unknown error occurred.';
682
+ const line = String(detail).trim().split(/\r?\n/).find(l => l.toLowerCase().includes('error:') || l.toLowerCase().includes('fatal:')) || detail;
683
+
684
+ console.error(`\nPush failed: ${line.trim()}`);
685
+ }
686
+ }
687
+
688
+ async function handleDashboard(rl, masterKey) {
689
+ const config = await storage.loadConfig();
690
+ const user = storage.getActiveUser(config);
691
+ if (!user) return;
692
+
693
+ // Check visibility setting
694
+ if (user.settings?.dashboardVisible === false) {
695
+ return;
696
+ }
697
+
698
+ let vaultStatus = '';
699
+ if (user.vault) {
700
+ if (masterKey) {
701
+ vaultStatus = ' \uD83D\uDD12 Admin';
702
+ } else {
703
+ vaultStatus = ' \uD83D\uDD12 Locked';
704
+ }
705
+ }
706
+
707
+ const apiKey = user.news_api_key || process.env.NEWS_API_KEY;
708
+
709
+ const todoLines = getUserTodos(user).slice(0, 4).map((todo) => {
710
+ const icon = todo.status === 'completed' ? '[x]' : '[ ]';
711
+ return `${icon} ${todo.text}`;
712
+ });
713
+ if (todoLines.length === 0) todoLines.push('(No active tasks)');
714
+
715
+ const shortcutLines = Object.entries(getUserShortcuts(user)).slice(0, 4).map(([key, value]) => {
716
+ const rawValue = typeof value === 'string' ? value : value?.value;
717
+ const label = rawValue ? rawValue.replace(/^https?:\/\//, '').slice(0, 14) : '';
718
+ return label ? `${key} -> ${label}` : key;
719
+ });
720
+ if (shortcutLines.length === 0) shortcutLines.push('(No shortcuts)');
721
+
722
+ let newsLines = [];
723
+ if (apiKey) {
724
+ try {
725
+ const result = await news.fetchArticles(null, 2, apiKey);
726
+ if (result.articles && result.articles.length > 0) {
727
+ newsLines = result.articles.slice(0, 2).map((article) => `* ${article.title}`);
728
+ } else {
729
+ newsLines = ['* No recent news available'];
652
730
  }
731
+ } catch {
732
+ newsLines = ['* No recent news available'];
733
+ }
734
+ } else {
735
+ newsLines = ['* No API key (onboarding optional)'];
736
+ }
737
+
738
+ const now = new Date();
739
+ const dateStr = now.toLocaleDateString('en-US', { month: 'short', day: '2-digit' }).toUpperCase();
740
+ const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
741
+ const { renderDashboard } = await import('./dashboard.mjs');
742
+
743
+ await renderDashboard({
744
+ username: (config.activeUser || 'User') + vaultStatus,
745
+ menu: 'LINK | PUSH | TODO | NEWS | HELP',
746
+ dateTime: `${dateStr} ${timeStr}`,
747
+ todoLines,
748
+ shortcutLines,
749
+ newsLines
750
+ });
751
+
752
+ console.log('');
753
+ }
754
+
755
+ async function handleNews(rl, args) {
756
+ const config = await storage.loadConfig();
757
+ const user = storage.getActiveUser(config);
758
+ if (!user) return;
759
+
760
+ const apiKey = user.news_api_key || process.env.NEWS_API_KEY;
761
+ if (!apiKey) {
762
+ console.log('No API key found. Opening Google News...');
763
+ utils.openResource('https://news.google.com');
764
+ return;
765
+ }
766
+
767
+ let query = '';
768
+ let limit = 5;
769
+ if (args) {
770
+ const parts = args.trim().split(/\s+/);
771
+ if (parts.length > 1 && !Number.isNaN(Number.parseInt(parts[parts.length - 1], 10))) {
772
+ limit = Number.parseInt(parts.pop(), 10);
773
+ query = parts.join(' ');
774
+ } else if (parts.length === 1 && !Number.isNaN(Number.parseInt(parts[0], 10))) {
775
+ limit = Number.parseInt(parts[0], 10);
776
+ } else {
777
+ query = args.trim();
778
+ }
779
+ }
780
+
781
+ console.log('\nFetching latest news...');
782
+ try {
783
+ const result = await news.fetchArticles(query, limit, apiKey);
784
+ if (!result.articles || result.articles.length === 0) {
785
+ console.log('No news found.');
653
786
  return;
654
787
  }
655
788
 
656
- const line = String(detail).trim().split(/\r?\n/).find(Boolean);
657
- console.error(line ? `Push failed: ${line}` : 'error');
658
- }
659
- }
660
-
661
- async function handleDashboard(rl) {
662
- const config = await storage.loadConfig();
663
- const user = storage.getActiveUser(config);
664
- if (!user) return;
665
-
666
- const apiKey = user.news_api_key || process.env.NEWS_API_KEY;
667
-
668
- const todoLines = getUserTodos(user).slice(0, 4).map((todo) => {
669
- const icon = todo.status === 'completed' ? '[x]' : '[ ]';
670
- return `${icon} ${todo.text}`;
671
- });
672
- if (todoLines.length === 0) todoLines.push('(No active tasks)');
673
-
674
- const shortcutLines = Object.entries(getUserShortcuts(user)).slice(0, 4).map(([key, value]) => {
675
- const rawValue = typeof value === 'string' ? value : value?.value;
676
- const label = rawValue ? rawValue.replace(/^https?:\/\//, '').slice(0, 14) : '';
677
- return label ? `${key} -> ${label}` : key;
678
- });
679
- if (shortcutLines.length === 0) shortcutLines.push('(No shortcuts)');
680
-
681
- let newsLines = [];
682
- if (apiKey) {
683
- try {
684
- const result = await news.fetchArticles(null, 2, apiKey);
685
- if (result.articles && result.articles.length > 0) {
686
- newsLines = result.articles.slice(0, 2).map((article) => `* ${article.title}`);
687
- } else {
688
- newsLines = ['* No recent news available'];
689
- }
690
- } catch {
691
- newsLines = ['* No recent news available'];
692
- }
693
- } else {
694
- newsLines = ['* No API key (onboarding optional)'];
695
- }
696
-
697
- const now = new Date();
698
- const dateStr = now.toLocaleDateString('en-US', {month: 'short', day: '2-digit'}).toUpperCase();
699
- const timeStr = now.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit', hour12: true});
700
- const {renderDashboard} = await import('./dashboard.mjs');
701
-
702
- await renderDashboard({
703
- menu: 'LINK | PUSH | TODO | NEWS | HELP',
704
- dateTime: `${dateStr} ${timeStr}`,
705
- todoLines,
706
- shortcutLines,
707
- newsLines
708
- });
709
-
710
- console.log('');
711
- }
712
-
713
- async function handleNews(rl, args) {
714
- const config = await storage.loadConfig();
715
- const user = storage.getActiveUser(config);
716
- if (!user) return;
717
-
718
- const apiKey = user.news_api_key || process.env.NEWS_API_KEY;
719
- if (!apiKey) {
720
- console.log('No API key found. Opening Google News...');
721
- utils.openBrowser('https://news.google.com');
722
- return;
723
- }
724
-
725
- let query = '';
726
- let limit = 5;
727
- if (args) {
728
- const parts = args.trim().split(/\s+/);
729
- if (parts.length > 1 && !Number.isNaN(Number.parseInt(parts[parts.length - 1], 10))) {
730
- limit = Number.parseInt(parts.pop(), 10);
731
- query = parts.join(' ');
732
- } else if (parts.length === 1 && !Number.isNaN(Number.parseInt(parts[0], 10))) {
733
- limit = Number.parseInt(parts[0], 10);
734
- } else {
735
- query = args.trim();
736
- }
737
- }
738
-
739
- console.log('\nFetching latest news...');
740
- try {
741
- const result = await news.fetchArticles(query, limit, apiKey);
742
- if (!result.articles || result.articles.length === 0) {
743
- console.log('No news found.');
744
- return;
745
- }
746
-
747
- console.log('\nNews:\n');
748
- result.articles.forEach((article, index) => {
749
- console.log(`${index + 1}. ${article.title}`);
750
- console.log(` Source: ${article.source.name}\n`);
751
- });
752
-
753
- const choice = await askQuestion(rl, 'Enter number once you open article or Enter to exit: ');
754
- const index = Number.parseInt(choice, 10);
755
- if (!Number.isNaN(index) && index > 0 && index <= result.articles.length) {
756
- utils.openBrowser(result.articles[index - 1].url);
757
- }
758
- } catch {
759
- console.error('error');
760
- }
761
- }
762
-
789
+ console.log('\nNews:\n');
790
+ result.articles.forEach((article, index) => {
791
+ console.log(`${index + 1}. ${article.title}`);
792
+ console.log(` Source: ${article.source.name}\n`);
793
+ });
794
+
795
+ const choice = await askQuestion(rl, 'Enter number once you open article or Enter to exit: ');
796
+ const index = Number.parseInt(choice, 10);
797
+ if (!Number.isNaN(index) && index > 0 && index <= result.articles.length) {
798
+ utils.openResource(result.articles[index - 1].url);
799
+ }
800
+ } catch {
801
+ console.error('error');
802
+ }
803
+ }
804
+
763
805
  async function handleTodo(rl, args) {
764
806
  const config = await storage.loadConfig();
765
807
  const user = storage.getActiveUser(config);
@@ -769,160 +811,672 @@ async function handleTodo(rl, args) {
769
811
  ...task,
770
812
  due: normalizeDueBucket(task.due)
771
813
  }));
772
-
773
- const renderHeader = (text) => console.log(`\n${text}:`);
774
- const renderTasks = (tasks) => {
775
- if (tasks.length === 0) {
776
- console.log(' (No tasks)');
777
- return;
778
- }
779
-
780
- const sorted = [...tasks].sort((left, right) => {
781
- if (left.status === right.status) return 0;
782
- return left.status === 'pending' ? -1 : 1;
783
- });
784
-
785
- sorted.forEach((task) => {
786
- const icon = task.status === 'completed' ? '[x]' : '[ ]';
787
- console.log(` ${icon} ${task.id}. ${task.text}`);
788
- });
789
- };
790
-
814
+
815
+ const renderHeader = (text) => console.log(`\n${text}:`);
816
+ const renderTasks = (tasks) => {
817
+ if (tasks.length === 0) {
818
+ console.log(' (No tasks)');
819
+ return;
820
+ }
821
+
822
+ const sorted = [...tasks].sort((left, right) => {
823
+ if (left.status === right.status) return 0;
824
+ return left.status === 'pending' ? -1 : 1;
825
+ });
826
+
827
+ sorted.forEach((task) => {
828
+ const icon = task.status === 'completed' ? '[x]' : '[ ]';
829
+ console.log(` ${icon} ${task.id}. ${task.text}`);
830
+ });
831
+ };
832
+
791
833
  if (!args || args.trim() === '') {
792
- console.log('\n--- Todo Management ---');
793
- const options = ['Add', 'List', 'Done', 'Delete', 'Today', 'Tmrw', 'Week', 'Monthly', 'Yearly', 'Back'];
794
- const choice = await selectOption(rl, options);
795
- if (choice === -1 || choice === 9) return;
796
-
797
- switch (choice) {
798
- case 0: {
799
- const text = await askQuestion(rl, 'Task: ');
800
- if (!text) return;
801
-
802
- const dueChoice = await selectOption(rl, ['Today', 'Tmrw', 'Week', 'Monthly', 'Yearly']);
803
- const dueList = ['today', 'tmrw', 'week', 'monthly', 'yearly'];
804
- const due = dueChoice !== -1 ? dueList[dueChoice] : 'today';
805
- const maxId = user.todo.reduce((max, task) => Math.max(max, task.id || 0), 0);
806
- user.todo.push({id: maxId + 1, text: text.trim(), status: 'pending', due});
834
+ while (true) {
835
+ console.log('\n--- Todo Management ---');
836
+ console.log('Available: add | list | done | delete | today | tmrw | week | monthly | yearly | back');
837
+ const action = (await askQuestion(rl, 'Todo Action: ')).toLowerCase().trim();
838
+
839
+ if (!action || action === 'back') break;
840
+
841
+ switch (action) {
842
+ case 'add': {
843
+ const text = await askQuestion(rl, 'Task: ');
844
+ if (!text) break;
845
+
846
+ console.log('Due: today | tmrw | week | monthly | yearly');
847
+ const due = (await askQuestion(rl, 'Due date (default: today): ')).toLowerCase().trim() || 'today';
848
+
849
+ const maxId = user.todo.reduce((max, task) => Math.max(max, task.id || 0), 0);
850
+ user.todo.push({ id: maxId + 1, text: text.trim(), status: 'pending', due: normalizeDueBucket(due) });
851
+ await storage.saveConfig(config);
852
+ console.log('Task added.');
853
+ break;
854
+ }
855
+
856
+ case 'list':
857
+ renderHeader('All Tasks');
858
+ renderTasks(user.todo);
859
+ break;
860
+
861
+ case 'today':
862
+ case 'tmrw':
863
+ case 'week':
864
+ case 'monthly':
865
+ case 'yearly':
866
+ renderHeader(action.charAt(0).toUpperCase() + action.slice(1));
867
+ renderTasks(user.todo.filter((task) => task.due === normalizeDueBucket(action)));
868
+ break;
869
+
870
+ case 'done':
871
+ case 'd': {
872
+ const doneId = Number.parseInt(await askQuestion(rl, 'ID to mark as done: '), 10);
873
+ const task = user.todo.find((item) => item.id === doneId);
874
+ if (task) {
875
+ task.status = 'completed';
876
+ await storage.saveConfig(config);
877
+ console.log(`Task ${doneId} marked as done.`);
878
+ } else {
879
+ console.log('Task not found.');
880
+ }
881
+ break;
882
+ }
883
+
884
+ case 'delete':
885
+ case 'rm': {
886
+ const deleteId = Number.parseInt(await askQuestion(rl, 'ID to delete: '), 10);
887
+ const deleteIndex = user.todo.findIndex((item) => item.id === deleteId);
888
+ if (deleteIndex !== -1) {
889
+ user.todo.splice(deleteIndex, 1);
890
+ await storage.saveConfig(config);
891
+ console.log(`Task ${deleteId} deleted.`);
892
+ } else {
893
+ console.log('Task not found.');
894
+ }
895
+ break;
896
+ }
897
+
898
+ default:
899
+ console.log('Unknown action. Try again.');
900
+ break;
901
+ }
902
+ }
903
+ return;
904
+ }
905
+
906
+ const parts = args.trim().split(/\s+/);
907
+ const command = parts[0]?.toLowerCase();
908
+ const rest = parts.slice(1).join(' ');
909
+
910
+ switch (command) {
911
+ case 'add': {
912
+ const maxId = user.todo.reduce((max, task) => Math.max(max, task.id || 0), 0);
913
+ user.todo.push({ id: maxId + 1, text: rest || 'Task', status: 'pending', due: 'today' });
914
+ await storage.saveConfig(config);
915
+ return;
916
+ }
917
+
918
+ case 'done': {
919
+ const task = user.todo.find((item) => item.id === Number.parseInt(rest, 10));
920
+ if (task) {
921
+ task.status = 'completed';
807
922
  await storage.saveConfig(config);
808
- return;
809
923
  }
924
+ return;
925
+ }
926
+
927
+ case 'list':
928
+ default:
929
+ renderTasks(user.todo);
930
+ }
931
+ }
932
+
933
+ async function handleMenu(masterKey, activePlugins = {}) {
934
+ console.log('\nAvailable Commands:');
935
+ if (!masterKey) {
936
+ console.log('user -> Manage profiles (create|login|list|current|delete)');
937
+ }
938
+ console.log('export -> Backup your active profile');
939
+ console.log('import -> Restore or merge profile data');
940
+ console.log('repo -> Manage repositories (set|use|list|current)');
941
+ console.log('link -> Add a new shortcut');
942
+ console.log('links -> View all shortcuts');
943
+ console.log('unlink -> Delete a shortcut');
944
+ console.log('push -> Automated Git push');
945
+ console.log('news -> Fetch latest news');
946
+ console.log('todo -> Manage tasks');
947
+ if (masterKey) {
948
+ console.log('sync -> Manage Cloud Sync (Push/Pull Admin Data)');
949
+ console.log('pg -> PostgreSQL God-Mode Explorer (Local Only)');
950
+ console.log('plugin -> Manage Admin Plugins (list|create)');
951
+ console.log('settings -> Admin Settings (Dashboard Toggle, PG Creds)');
952
+ console.log('mission -> Manage Complex Launch Workflows [ADMIN]');
953
+
954
+ // Dynamic Plugin Commands
955
+ Object.keys(activePlugins).forEach(name => {
956
+ console.log(`${name.padEnd(11)} -> ${activePlugins[name].description} [Plugin]`);
957
+ });
958
+ }
959
+ console.log('help -> Show this menu');
960
+ console.log('uninstall -> Wipe all error-ux data');
961
+ console.log('exit -> Exit CLI');
962
+ console.log('<shortcut> -> Open a saved link\n');
963
+ }
964
+
965
+ async function handleShortcut(shortcut, masterKey) {
966
+ const cleanShortcut = String(shortcut || '').toLowerCase().trim();
967
+ if (!cleanShortcut) return false;
968
+
969
+ const config = await storage.loadConfig();
970
+ const user = storage.getActiveUser(config);
971
+ if (!user) return false;
972
+
973
+ const shortcuts = getUserShortcuts(user);
974
+ const target = shortcuts[cleanShortcut];
975
+
976
+ // 1. Check Saved Shortcuts (Available to ALL users)
977
+ if (target) {
978
+ const url = typeof target === 'string' ? target : target.value;
979
+ console.log(`[Shortcut] Opening ${url}...`);
980
+ utils.openResource(url);
981
+ return true;
982
+ }
983
+
984
+ // 2. Check Admin Missions (If session is unlocked)
985
+ if (masterKey && user.missions && user.missions[cleanShortcut]) {
986
+ const items = user.missions[cleanShortcut];
987
+ console.log(`\nšŸš€ Launching Mission: ${cleanShortcut.toUpperCase()}...`);
988
+ for (const item of items) {
989
+ console.log(` -> ${item}`);
990
+ utils.openResource(item);
991
+ }
992
+ return true;
993
+ }
994
+
995
+ // 3. Restricted System Discovery (Admin-Only Whitelist)
996
+ if (masterKey) {
997
+ const whitelist = {
998
+ 'calc': 'calc',
999
+ 'chrome': 'chrome',
1000
+ 'edge': 'microsoft-edge:',
1001
+ 'telegram': 'tg://',
1002
+ 'pdadmin': 'pgadmin4',
1003
+ 'pgadmin': 'pgadmin4'
1004
+ };
1005
+
1006
+ if (whitelist[cleanShortcut]) {
1007
+ const target = whitelist[cleanShortcut];
1008
+ console.log(`\n[⚔ Admin Gateway] Launching system resource: "${target}"...`);
1009
+ utils.openResource(target);
1010
+ return true;
1011
+ }
1012
+ }
1013
+
1014
+ return false;
1015
+ }
810
1016
 
811
- case 1:
812
- renderHeader('Today');
813
- renderTasks(user.todo.filter((task) => task.due === 'today'));
814
- renderHeader('Tmrw');
815
- renderTasks(user.todo.filter((task) => task.due === 'tmrw'));
816
- renderHeader('Week');
817
- renderTasks(user.todo.filter((task) => task.due === 'week'));
818
- renderHeader('Monthly');
819
- renderTasks(user.todo.filter((task) => task.due === 'monthly'));
820
- renderHeader('Yearly');
821
- renderTasks(user.todo.filter((task) => task.due === 'yearly'));
1017
+ async function handleAdminSync(rl, masterKey) {
1018
+ if (!masterKey) {
1019
+ console.log('Error: This command is only available in Admin mode (Unlocked).');
1020
+ return;
1021
+ }
1022
+
1023
+ const config = await storage.loadConfig();
1024
+ const user = storage.getActiveUser(config);
1025
+ if (!user || !user.vault) return;
1026
+
1027
+ // Decrypt credentials
1028
+ const decrypted = storage.decrypt(user.vault, masterKey);
1029
+ if (!decrypted) {
1030
+ console.error('Error: Failed to decrypt vault. Your Master Key might be incorrect for this vault.');
1031
+ return;
1032
+ }
1033
+
1034
+ const { pat, gistId } = JSON.parse(decrypted);
1035
+
1036
+ if (!pat || !gistId) {
1037
+ console.error('\nāŒ Error: Sync credentials not set. Please choose "Update Credentials" first.');
1038
+ console.log('--- Cloud Sync Management ---');
1039
+ const options = ['Update Credentials', 'Back'];
1040
+ const choice = await selectOption(rl, options);
1041
+ if (choice === 0) {
1042
+ // Re-run with intent to update
1043
+ return await handleAdminSync(rl, masterKey);
1044
+ }
1045
+ return;
1046
+ }
1047
+
1048
+ console.log('\n--- Cloud Sync Management ---');
1049
+ const options = ['Push (Upload to Cloud)', 'Pull (Download from Cloud)', 'Update Credentials', 'Back'];
1050
+ const choice = await selectOption(rl, options);
1051
+
1052
+ try {
1053
+ if (choice === 0) {
1054
+ // Push
1055
+ console.log('Preparing full cockpit data for upload...');
1056
+ const syncData = JSON.stringify({
1057
+ todo: user.todo,
1058
+ shortcuts: user.shortcuts,
1059
+ savedRepos: user.savedRepos,
1060
+ news_api_key: user.news_api_key,
1061
+ missions: user.missions,
1062
+ localVault: user.localVault,
1063
+ settings: user.settings,
1064
+ plugins: cmPlugins.getAllPluginSource()
1065
+ });
1066
+ const encryptedSyncData = storage.encrypt(syncData, masterKey);
1067
+ await utils.withSpinner('Uploading to GitHub...', async () => {
1068
+ await git.gistSync(pat, gistId, encryptedSyncData, 'push');
1069
+ });
1070
+ console.log('\nSuccess! Entire cockpit state PUSHED to cloud.');
1071
+ } else if (choice === 1) {
1072
+ // Pull
1073
+ console.log('Fetching remote cockpit data...');
1074
+ const encryptedBody = await utils.withSpinner('Downloading from GitHub...', async () => {
1075
+ return await git.gistSync(pat, gistId, null, 'pull');
1076
+ });
1077
+
1078
+ if (!encryptedBody) {
1079
+ console.log('No data found in Gist.');
822
1080
  return;
823
-
824
- case 2: {
825
- const doneId = Number.parseInt(await askQuestion(rl, 'ID: '), 10);
826
- const task = user.todo.find((item) => item.id === doneId);
827
- if (task) {
828
- task.status = 'completed';
829
- await storage.saveConfig(config);
830
- }
831
- return;
832
- }
833
-
834
- case 3: {
835
- const deleteId = Number.parseInt(await askQuestion(rl, 'ID: '), 10);
836
- const deleteIndex = user.todo.findIndex((item) => item.id === deleteId);
837
- if (deleteIndex !== -1) {
838
- user.todo.splice(deleteIndex, 1);
839
- await storage.saveConfig(config);
840
- }
841
- return;
842
- }
843
-
844
- default:
845
- return;
846
- }
847
- }
848
-
849
- const parts = args.trim().split(/\s+/);
850
- const command = parts[0]?.toLowerCase();
851
- const rest = parts.slice(1).join(' ');
852
-
853
- switch (command) {
854
- case 'add': {
855
- const maxId = user.todo.reduce((max, task) => Math.max(max, task.id || 0), 0);
856
- user.todo.push({id: maxId + 1, text: rest || 'Task', status: 'pending', due: 'today'});
857
- await storage.saveConfig(config);
858
- return;
859
- }
860
-
861
- case 'done': {
862
- const task = user.todo.find((item) => item.id === Number.parseInt(rest, 10));
863
- if (task) {
864
- task.status = 'completed';
865
- await storage.saveConfig(config);
866
- }
867
- return;
868
- }
869
-
870
- case 'list':
871
- default:
872
- renderTasks(user.todo);
873
- }
874
- }
875
-
876
- async function handleMenu() {
877
- console.log('\nAvailable Commands:');
878
- console.log('user -> Manage profiles (create|login|list|current|delete)');
879
- console.log('export -> Backup your active profile');
880
- console.log('import -> Restore or merge profile data');
881
- console.log('repo -> Manage repositories (set|use|list|current)');
882
- console.log('link -> Add a new shortcut');
883
- console.log('links -> View all shortcuts');
884
- console.log('unlink -> Delete a shortcut');
885
- console.log('push -> Automated Git push');
886
- console.log('news -> Fetch latest news');
887
- console.log('todo -> Manage tasks');
888
- console.log('help -> Show this menu');
889
- console.log('uninstall -> Wipe all error-ux data');
890
- console.log('exit -> Exit CLI');
891
- console.log('<shortcut> -> Open a saved link\n');
892
- }
893
-
894
- async function handleShortcut(shortcut) {
895
- const cleanShortcut = String(shortcut || '').toLowerCase().trim();
896
- const config = await storage.loadConfig();
897
- const user = storage.getActiveUser(config);
898
- if (!user) return false;
899
-
900
- const shortcuts = getUserShortcuts(user);
901
- const target = shortcuts[cleanShortcut];
902
- if (!target) return false;
903
-
904
- const url = typeof target === 'string' ? target : target.value;
905
- console.log(`Opening ${url}...`);
906
- utils.openBrowser(url);
907
- return true;
908
- }
909
-
1081
+ }
1082
+
1083
+ const decryptedBody = storage.decrypt(encryptedBody, masterKey);
1084
+ if (!decryptedBody) {
1085
+ throw new Error('Remote data could not be decrypted with current Master Key.');
1086
+ }
1087
+
1088
+ const remoteData = JSON.parse(decryptedBody);
1089
+
1090
+ // Restore Data Fields
1091
+ user.todo = remoteData.todo || user.todo;
1092
+ user.shortcuts = remoteData.shortcuts || user.shortcuts;
1093
+ user.savedRepos = remoteData.savedRepos || user.savedRepos;
1094
+ user.news_api_key = remoteData.news_api_key || user.news_api_key;
1095
+ user.missions = remoteData.missions || user.missions;
1096
+ user.localVault = remoteData.localVault || user.localVault;
1097
+ user.settings = remoteData.settings || user.settings;
1098
+
1099
+ // Restore Plugins (Filesystem)
1100
+ if (remoteData.plugins) {
1101
+ console.log('Synchronizing plugins to local directory...');
1102
+ for (const filename in remoteData.plugins) {
1103
+ cmPlugins.savePlugin(filename, remoteData.plugins[filename]);
1104
+ }
1105
+ }
1106
+
1107
+ await storage.saveConfig(config);
1108
+ console.log('\nSuccess! Cockpit state PULLED and updated. All systems ready.');
1109
+ } else if (choice === 2) {
1110
+ // Update
1111
+ const newPat = (await askQuestion(rl, 'New GitHub PAT: ')).trim();
1112
+ let newGistId = (await askQuestion(rl, 'New Gist ID (or URL): ')).trim();
1113
+
1114
+ // Extract ID if URL is provided
1115
+ if (newGistId.includes('gist.github.com/')) {
1116
+ const parts = newGistId.split('/');
1117
+ newGistId = parts[parts.length - 1];
1118
+ }
1119
+
1120
+ if (newPat && newGistId) {
1121
+ user.vault = storage.encrypt(JSON.stringify({ pat: newPat, gistId: newGistId }), masterKey);
1122
+ await storage.saveConfig(config);
1123
+ console.log('Credentials updated.');
1124
+ }
1125
+ }
1126
+ } catch (e) {
1127
+ console.error(`\nSync Failed: ${e.message}`);
1128
+ }
1129
+ }
1130
+
1131
+ async function setupDB(rl, masterKey, user, config) {
1132
+ console.log('\n--- PostgreSQL Credential Setup ---');
1133
+ const host = (await askQuestion(rl, 'Host (default: localhost): ')).trim() || 'localhost';
1134
+ const userStr = (await askQuestion(rl, 'User: ')).trim();
1135
+ const password = await auth.askPassword(rl, 'Password: ');
1136
+ const port = Number.parseInt((await askQuestion(rl, 'Port (default: 5432): ')).trim(), 10) || 5432;
1137
+
1138
+ if (userStr && password) {
1139
+ const dbCreds = { host, user: userStr, password, port };
1140
+ user.localVault = storage.encrypt(JSON.stringify(dbCreds), masterKey);
1141
+ await storage.saveConfig(config);
1142
+ console.log('Credentials updated and saved locally.');
1143
+ return true;
1144
+ } else {
1145
+ console.error('Invalid credentials. Update aborted.');
1146
+ return false;
1147
+ }
1148
+ }
1149
+
1150
+ async function handleDB(rl, masterKey) {
1151
+ if (!masterKey) {
1152
+ console.log('Error: This command is only available in Admin mode.');
1153
+ return;
1154
+ }
1155
+
1156
+ const config = await storage.loadConfig();
1157
+ const user = storage.getActiveUser(config);
1158
+ if (!user) return;
1159
+
1160
+ let dbCreds = null;
1161
+ if (user.localVault) {
1162
+ const decrypted = storage.decrypt(user.localVault, masterKey);
1163
+ if (decrypted) {
1164
+ dbCreds = JSON.parse(decrypted);
1165
+ }
1166
+ }
1167
+
1168
+ if (!dbCreds) {
1169
+ console.log('\n--- PostgreSQL Local Setup ---');
1170
+ console.log('Enter your database connection details (saved locally only).');
1171
+ const host = (await askQuestion(rl, 'Host (default: localhost): ')).trim() || 'localhost';
1172
+ const userStr = (await askQuestion(rl, 'User: ')).trim();
1173
+ const password = await auth.askPassword(rl, 'Password: ');
1174
+ const port = Number.parseInt((await askQuestion(rl, 'Port (default: 5432): ')).trim(), 10) || 5432;
1175
+
1176
+ if (userStr && password) {
1177
+ dbCreds = { host, user: userStr, password, port };
1178
+ user.localVault = storage.encrypt(JSON.stringify(dbCreds), masterKey);
1179
+ await storage.saveConfig(config);
1180
+ console.log('Credentials saved locally.');
1181
+ } else {
1182
+ console.error('Invalid credentials. Setup cancelled.');
1183
+ return;
1184
+ }
1185
+ }
1186
+
1187
+ console.log('\nLaunching Cyberpunk Explorer...');
1188
+ const { renderExplorer } = await import('./dbExplorer.mjs');
1189
+ await renderExplorer(dbCreds);
1190
+ }
1191
+
1192
+ async function handleSettings(rl, masterKey) {
1193
+ if (!masterKey) {
1194
+ console.log('Error: Settings are only available in Admin mode.');
1195
+ return masterKey;
1196
+ }
1197
+
1198
+ const config = await storage.loadConfig();
1199
+ const user = storage.getActiveUser(config);
1200
+ if (!user) return masterKey;
1201
+
1202
+ console.log('\n--- Admin Settings Hub ---');
1203
+ const dbVisible = user.settings?.dashboardVisible !== false;
1204
+
1205
+ const choice = await selectOption(rl, [
1206
+ `Toggle Dashboard Visibility (Currently: ${dbVisible ? 'ON' : 'OFF'})`,
1207
+ 'Update PostgreSQL Credentials',
1208
+ 'Change Admin Master Key',
1209
+ 'Back'
1210
+ ]);
1211
+
1212
+ if (choice === 0) {
1213
+ // Toggle dashboard
1214
+ user.settings.dashboardVisible = !dbVisible;
1215
+ await storage.saveConfig(config);
1216
+ console.log(`\nSuccess: Dashboard is now ${!dbVisible ? 'VISIBLE' : 'HIDDEN'}.`);
1217
+ } else if (choice === 1) {
1218
+ // Update PG Credentials
1219
+ try {
1220
+ await setupDB(rl, masterKey, user, config);
1221
+ } catch (err) {
1222
+ console.error(`\nError: ${err.message}`);
1223
+ }
1224
+ } else if (choice === 2) {
1225
+ // Change Master Key
1226
+ console.log('\n--- MASTER KEY ROTATION ---');
1227
+ console.log('Warning: This will re-encrypt your Database and Cloud Sync credentials.');
1228
+
1229
+ const newKey1 = await auth.askPassword(rl, 'Enter NEW 128-char Master Key: ');
1230
+ if (newKey1.length !== 128) {
1231
+ console.error('Error: Master Key must be exactly 128 characters.');
1232
+ return masterKey;
1233
+ }
1234
+
1235
+ const newKey2 = await auth.askPassword(rl, 'Confirm NEW Master Key: ');
1236
+ if (newKey1 !== newKey2) {
1237
+ console.error('Error: Keys do not match. Rotation cancelled.');
1238
+ return masterKey;
1239
+ }
1240
+
1241
+ try {
1242
+ // 1. Re-encrypt user.vault (Sync)
1243
+ if (user.vault) {
1244
+ const decryptedVault = storage.decrypt(user.vault, masterKey);
1245
+ if (decryptedVault) {
1246
+ user.vault = storage.encrypt(decryptedVault, newKey1);
1247
+ }
1248
+ }
1249
+
1250
+ // 2. Re-encrypt user.localVault (Database)
1251
+ if (user.localVault) {
1252
+ const decryptedLocalVault = storage.decrypt(user.localVault, masterKey);
1253
+ if (decryptedLocalVault) {
1254
+ user.localVault = storage.encrypt(decryptedLocalVault, newKey1);
1255
+ }
1256
+ }
1257
+
1258
+ await storage.saveConfig(config);
1259
+ console.log('\nSuccess! Master Key rotated and all data re-encrypted.');
1260
+ return newKey1; // Return the new key to update the session
1261
+ } catch (err) {
1262
+ console.error(`\nMigration Failed: ${err.message}`);
1263
+ }
1264
+ }
1265
+
1266
+ return masterKey;
1267
+ }
1268
+
1269
+ async function handlePlugin(rl, masterKey) {
1270
+ if (!masterKey) {
1271
+ console.log('Error: This command is only available in Admin mode.');
1272
+ return;
1273
+ }
1274
+
1275
+ console.log('\n--- Admin Plugin Management ---');
1276
+ const choice = await selectOption(rl, [
1277
+ 'List Installed Plugins',
1278
+ 'Create New Plugin Boilerplate',
1279
+ 'Delete Existing Plugin',
1280
+ 'Back'
1281
+ ]);
1282
+
1283
+ if (choice === 0) {
1284
+ const active = cmPlugins.loadPlugins();
1285
+ const names = Object.keys(active);
1286
+ if (names.length === 0) {
1287
+ console.log('No plugins installed in ~/.error-ux/plugins/');
1288
+ } else {
1289
+ console.log('\nInstalled Plugins:');
1290
+ names.forEach(name => {
1291
+ console.log(`- ${name}: ${active[name].description}`);
1292
+ });
1293
+ }
1294
+ } else if (choice === 1) {
1295
+ const name = (await askQuestion(rl, 'Plugin Name (internal name): ')).trim().toLowerCase();
1296
+ if (!name) return;
1297
+
1298
+ const fileName = `${name}.js`;
1299
+ const targetPath = path.join(cmPlugins.getPluginDir(), fileName);
1300
+
1301
+ if (fsSync.existsSync(targetPath)) {
1302
+ console.error(`Error: Plugin "${name}" already exists.`);
1303
+ return;
1304
+ }
1305
+
1306
+ const boilerplate = `/**
1307
+ * Error-UX Plugin: ${name}
1308
+ * To use: Restart the CLI as Admin, then type "${name}"
1309
+ */
1310
+
1311
+ module.exports = {
1312
+ name: '${name}',
1313
+ description: 'A custom plugin for error-ux.',
1314
+ async run(rl, args, masterKey) {
1315
+ console.log('Hello from my new plugin!');
1316
+ }
1317
+ };
1318
+ `;
1319
+ fsSync.writeFileSync(targetPath, boilerplate, 'utf8');
1320
+ console.log(`\nSuccess! Plugin boilerplate created at: ${targetPath}`);
1321
+ console.log('Please restart the CLI to load your new plugin.');
1322
+ } else if (choice === 2) {
1323
+ const active = cmPlugins.loadPlugins();
1324
+ const names = Object.keys(active);
1325
+ if (names.length === 0) {
1326
+ console.log('No plugins found to delete.');
1327
+ return;
1328
+ }
1329
+
1330
+ console.log('\nSelect a plugin to DELETE:');
1331
+ const pluginChoice = await selectOption(rl, [...names, 'Cancel']);
1332
+ if (pluginChoice >= names.length) return;
1333
+
1334
+ const targetName = names[pluginChoice];
1335
+ const confirm = (await askQuestion(rl, `Are you SURE you want to delete "${targetName}"? (y/N): `)).toLowerCase();
1336
+ if (confirm === 'y') {
1337
+ if (cmPlugins.deletePlugin(targetName)) {
1338
+ console.log(`\nSuccess! Plugin "${targetName}" has been purged.`);
1339
+ } else {
1340
+ console.error(`Error: Could not delete "${targetName}".`);
1341
+ }
1342
+ }
1343
+ }
1344
+ }
1345
+
1346
+ async function handleMission(rl, args, masterKey) {
1347
+ if (!masterKey) {
1348
+ console.error('Error: Mission Workflows are restricted to "God-Mode" Admin sessions.');
1349
+ return;
1350
+ }
1351
+
1352
+ const config = await storage.loadConfig();
1353
+ const user = storage.getActiveUser(config);
1354
+ if (!user) return;
1355
+
1356
+ if (!user.missions) user.missions = {};
1357
+
1358
+ let subCommand = (args[0] || '').toLowerCase();
1359
+
1360
+ // Interactive Wizard if no subcommand
1361
+ if (!subCommand) {
1362
+ console.log('\n--- šŸš€ Mission Quick-Start ---');
1363
+ console.log('1. [List] View current missions');
1364
+ console.log('2. [Create] Build a new mission workflow');
1365
+ console.log('3. [Delete] Remove a mission');
1366
+
1367
+ const ans = (await askQuestion(rl, '\nChoice (1-3) or mission name to run: ')).trim().toLowerCase();
1368
+
1369
+ if (ans === '1' || ans === 'list') {
1370
+ subCommand = 'list';
1371
+ } else if (ans === '2' || ans === 'create' || ans === 'save') {
1372
+ subCommand = 'save';
1373
+ } else if (ans === '3' || ans === 'delete') {
1374
+ subCommand = 'delete';
1375
+ } else if (ans) {
1376
+ // Treat as mission name to run
1377
+ subCommand = ans;
1378
+ } else {
1379
+ return;
1380
+ }
1381
+ }
1382
+
1383
+ if (subCommand === 'save' || subCommand === 'create') {
1384
+ let name = (args[1] || '').toLowerCase().trim();
1385
+ if (!name) {
1386
+ name = (await askQuestion(rl, 'Name your new Mission: ')).trim().toLowerCase();
1387
+ }
1388
+
1389
+ if (!name) {
1390
+ console.log('Cancelled.');
1391
+ return;
1392
+ }
1393
+
1394
+ console.log(`\n--- Defining Mission: ${name.toUpperCase()} ---`);
1395
+ console.log('Add items one by one (URLs, Paths, or Apps). Press Enter on an empty line to finish.');
1396
+
1397
+ const resources = [];
1398
+ while (true) {
1399
+ const res = (await askQuestion(rl, `[Item ${resources.length + 1}] > `)).trim();
1400
+ if (!res) break;
1401
+ resources.push(res);
1402
+ }
1403
+
1404
+ if (resources.length > 0) {
1405
+ user.missions[name] = resources;
1406
+ await storage.saveConfig(config);
1407
+ console.log(`\n✨ Mission "${name}" synchronized and ready.`);
1408
+ } else {
1409
+ console.log('No items added. Mission discarded.');
1410
+ }
1411
+
1412
+ } else if (subCommand === 'list') {
1413
+ const missions = Object.keys(user.missions);
1414
+ if (missions.length === 0) {
1415
+ console.log('\nNo missions found. Type "mission" to create one.');
1416
+ return;
1417
+ }
1418
+
1419
+ console.log('\n--- šŸ“‚ Admin Mission Hub ---');
1420
+ missions.forEach(m => {
1421
+ console.log(`\n[ ${m.toUpperCase()} ]`);
1422
+ user.missions[m].forEach(item => console.log(` -> ${item}`));
1423
+ });
1424
+
1425
+ } else if (subCommand === 'delete') {
1426
+ let name = (args[1] || '').toLowerCase().trim();
1427
+ if (!name) {
1428
+ name = (await askQuestion(rl, 'Name of mission to delete: ')).trim().toLowerCase();
1429
+ }
1430
+
1431
+ if (!user.missions[name]) {
1432
+ console.error(`Error: Mission "${name}" not found.`);
1433
+ return;
1434
+ }
1435
+
1436
+ delete user.missions[name];
1437
+ await storage.saveConfig(config);
1438
+ console.log(`\nMission "${name}" purged.`);
1439
+
1440
+ } else {
1441
+ // Direct run
1442
+ let name = subCommand;
1443
+ if (name === 'run') name = (args[1] || '').toLowerCase();
1444
+
1445
+ if (user.missions[name]) {
1446
+ console.log(`\nšŸš€ Executing Mission: ${name.toUpperCase()}...`);
1447
+ const items = user.missions[name];
1448
+ for (const item of items) {
1449
+ console.log(` Launching: ${item}`);
1450
+ utils.openResource(item);
1451
+ }
1452
+ } else {
1453
+ console.error(`Error: Mission "${name}" not found.`);
1454
+ console.log('Type "mission" for options.');
1455
+ }
1456
+ }
1457
+ }
1458
+
910
1459
  module.exports = {
911
1460
  askQuestion,
912
1461
  ensureUserNewsApiKey,
1462
+ handleAdminSync,
1463
+ handleDB,
913
1464
  handleDashboard,
914
- handleExport,
915
- handleImport,
916
- handleLink,
917
- handleLinks,
918
- handleMenu,
919
- handleNews,
920
- handlePush,
921
- handleRepo,
922
- handleShortcut,
923
- handleTodo,
924
- handleUninstall,
925
- handleUnlink,
926
- handleUser,
927
- initOnboarding
928
- };
1465
+ handleExport,
1466
+ handleImport,
1467
+ handleLink,
1468
+ handleLinks,
1469
+ handleMenu,
1470
+ handleNews,
1471
+ handlePlugin,
1472
+ handlePush,
1473
+ handleRepo,
1474
+ handleSettings,
1475
+ handleShortcut,
1476
+ handleTodo,
1477
+ handleUninstall,
1478
+ handleUnlink,
1479
+ handleUser,
1480
+ handleMission,
1481
+ initOnboarding
1482
+ };