error-ux-cli 1.0.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/README.md +7 -0
- package/index.js +154 -0
- package/lib/auth.js +52 -0
- package/lib/commands.js +928 -0
- package/lib/dashboard.mjs +152 -0
- package/lib/git.js +265 -0
- package/lib/news.js +51 -0
- package/lib/storage.js +119 -0
- package/lib/utils.js +386 -0
- package/package.json +32 -0
package/lib/commands.js
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
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
|
+
function normalizePathInput(value) {
|
|
42
|
+
const text = String(value || '').trim();
|
|
43
|
+
if (
|
|
44
|
+
(text.startsWith('"') && text.endsWith('"')) ||
|
|
45
|
+
(text.startsWith("'") && text.endsWith("'"))
|
|
46
|
+
) {
|
|
47
|
+
return text.slice(1, -1).trim();
|
|
48
|
+
}
|
|
49
|
+
return text;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeDueBucket(value) {
|
|
53
|
+
const text = String(value || '').toLowerCase().trim();
|
|
54
|
+
if (text === 'tomorrow') return 'tmrw';
|
|
55
|
+
if (text === 'upcoming') return 'week';
|
|
56
|
+
if (text === 'month') return 'monthly';
|
|
57
|
+
if (text === 'year') return 'yearly';
|
|
58
|
+
if (['today', 'tmrw', 'week', 'monthly', 'yearly'].includes(text)) {
|
|
59
|
+
return text;
|
|
60
|
+
}
|
|
61
|
+
return 'today';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function configureRemoteForPush(rl, targetRepo) {
|
|
65
|
+
const currentRemote = await git.getRemoteUrl();
|
|
66
|
+
|
|
67
|
+
if (!currentRemote) {
|
|
68
|
+
console.log('No remote origin found.');
|
|
69
|
+
const providedUrl = normalizePathInput(await askQuestion(rl, `Enter repository URL for ${targetRepo.name || 'origin'}: `));
|
|
70
|
+
if (!providedUrl || !utils.isValidUrl(providedUrl)) {
|
|
71
|
+
console.error('Error: Valid repository URL is required.');
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
targetRepo.url = providedUrl;
|
|
76
|
+
await git.setRemoteUrl(providedUrl, false);
|
|
77
|
+
return targetRepo;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Current remote: ${currentRemote}`);
|
|
81
|
+
|
|
82
|
+
if (targetRepo.url && targetRepo.url !== currentRemote) {
|
|
83
|
+
const useSaved = await askQuestion(rl, 'Use the saved repository URL instead? (y/n): ');
|
|
84
|
+
if (useSaved.toLowerCase() === 'y') {
|
|
85
|
+
await git.setRemoteUrl(targetRepo.url, true);
|
|
86
|
+
return targetRepo;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const replace = await askQuestion(rl, 'Do you want to change the remote? (y/n): ');
|
|
91
|
+
if (replace.toLowerCase() !== 'y') {
|
|
92
|
+
return {
|
|
93
|
+
...targetRepo,
|
|
94
|
+
url: currentRemote
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const newUrl = normalizePathInput(await askQuestion(rl, 'Enter new repository URL: '));
|
|
99
|
+
if (!newUrl || !utils.isValidUrl(newUrl)) {
|
|
100
|
+
console.error('Error: Valid repository URL is required.');
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await git.setRemoteUrl(newUrl, true);
|
|
105
|
+
return {
|
|
106
|
+
...targetRepo,
|
|
107
|
+
url: newUrl
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function promptForRemoteReplacement(rl, targetRepo) {
|
|
112
|
+
const newUrl = normalizePathInput(await askQuestion(rl, 'Enter new repository URL: '));
|
|
113
|
+
if (!newUrl || !utils.isValidUrl(newUrl)) {
|
|
114
|
+
console.error('Error: Valid repository URL is required.');
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await git.setRemoteUrl(newUrl, true);
|
|
119
|
+
return {
|
|
120
|
+
...targetRepo,
|
|
121
|
+
url: newUrl
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function ensureUserNewsApiKey(rl, config, user, options = {}) {
|
|
126
|
+
const forcePrompt = Boolean(options.forcePrompt);
|
|
127
|
+
const currentKey = String(user.news_api_key || process.env.NEWS_API_KEY || '').trim();
|
|
128
|
+
|
|
129
|
+
if (!forcePrompt && currentKey) {
|
|
130
|
+
return currentKey;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const prompt = currentKey
|
|
134
|
+
? 'News API Key (press Enter to keep current): '
|
|
135
|
+
: 'News API Key (press Enter to skip): ';
|
|
136
|
+
const entered = normalizePathInput(await askQuestion(rl, prompt)).trim();
|
|
137
|
+
const finalKey = entered || currentKey;
|
|
138
|
+
|
|
139
|
+
if (!finalKey) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
user.news_api_key = finalKey;
|
|
144
|
+
await storage.saveConfig(config);
|
|
145
|
+
utils.saveEnvValue('NEWS_API_KEY', finalKey);
|
|
146
|
+
return finalKey;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function resolvePushBranch(rl, requestedBranch) {
|
|
150
|
+
const normalized = String(requestedBranch || '').trim();
|
|
151
|
+
if (!normalized) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const exists = await git.branchExists(normalized);
|
|
156
|
+
if (!exists) {
|
|
157
|
+
console.log(`Branch not found. Creating new branch: ${normalized}`);
|
|
158
|
+
return normalized;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const nextVersion = await git.getNextBranchVersion(normalized);
|
|
162
|
+
const versionedBranch = `${normalized}_v${nextVersion}`;
|
|
163
|
+
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
|
+
]);
|
|
169
|
+
|
|
170
|
+
if (choice === 0) {
|
|
171
|
+
return normalized;
|
|
172
|
+
}
|
|
173
|
+
if (choice === 1) {
|
|
174
|
+
return versionedBranch;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
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
|
+
|
|
277
|
+
const config = {
|
|
278
|
+
activeUser: name,
|
|
279
|
+
users: {
|
|
280
|
+
[name]: {
|
|
281
|
+
password_hash: auth.hashPassword(password),
|
|
282
|
+
news_api_key: newsApiKey || null,
|
|
283
|
+
shortcuts: {},
|
|
284
|
+
activeRepo: null,
|
|
285
|
+
savedRepos: [],
|
|
286
|
+
todo: []
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (newsApiKey) {
|
|
292
|
+
utils.saveEnvValue('NEWS_API_KEY', newsApiKey);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await storage.saveConfig(config);
|
|
296
|
+
console.log(`\nSetup complete. Welcome aboard, ${name}.\n`);
|
|
297
|
+
return config;
|
|
298
|
+
}
|
|
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: ');
|
|
354
|
+
if (!auth.verifyPassword(loginPwd, config.users[loginName].password_hash)) {
|
|
355
|
+
console.error('error');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
config.activeUser = loginName;
|
|
360
|
+
await ensureUserNewsApiKey(rl, config, config.users[loginName], {forcePrompt: true});
|
|
361
|
+
await storage.saveConfig(config);
|
|
362
|
+
console.log(`Logged in as ${loginName}.`);
|
|
363
|
+
await handleDashboard(rl);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
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
|
+
|
|
434
|
+
if (!config.users[imported.name]) {
|
|
435
|
+
console.log(`Creating new profile for "${imported.name}"...`);
|
|
436
|
+
const pwd = await auth.askPassword(rl, `Set initial password for ${imported.name}: `);
|
|
437
|
+
if (!pwd) {
|
|
438
|
+
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
|
+
|
|
451
|
+
if (!config.activeUser) {
|
|
452
|
+
config.activeUser = imported.name;
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
console.log(`Replacing data in existing profile "${imported.name}"...`);
|
|
456
|
+
const user = config.users[imported.name];
|
|
457
|
+
user.shortcuts = imported.links;
|
|
458
|
+
user.savedRepos = imported.repos;
|
|
459
|
+
user.activeRepo = imported.repos[0] || null;
|
|
460
|
+
user.todo = imported.todos;
|
|
461
|
+
user.news_api_key = imported.newsApiKey;
|
|
462
|
+
}
|
|
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
|
+
|
|
569
|
+
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);
|
|
592
|
+
if (!resolvedBranch) {
|
|
593
|
+
console.log('Push cancelled.');
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const preparedRepo = await configureRemoteForPush(rl, {...target.targetRepo});
|
|
598
|
+
if (!preparedRepo) return;
|
|
599
|
+
|
|
600
|
+
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);
|
|
616
|
+
await storage.saveConfig(config);
|
|
617
|
+
}
|
|
618
|
+
} 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
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
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
|
+
|
|
763
|
+
async function handleTodo(rl, args) {
|
|
764
|
+
const config = await storage.loadConfig();
|
|
765
|
+
const user = storage.getActiveUser(config);
|
|
766
|
+
if (!user) return;
|
|
767
|
+
|
|
768
|
+
user.todo = getUserTodos(user).map((task) => ({
|
|
769
|
+
...task,
|
|
770
|
+
due: normalizeDueBucket(task.due)
|
|
771
|
+
}));
|
|
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
|
+
|
|
791
|
+
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});
|
|
807
|
+
await storage.saveConfig(config);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
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'));
|
|
822
|
+
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
|
+
|
|
910
|
+
module.exports = {
|
|
911
|
+
askQuestion,
|
|
912
|
+
ensureUserNewsApiKey,
|
|
913
|
+
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
|
+
};
|