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