error-ux-cli 1.0.0 → 1.1.0

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