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