@bobschlowinskii/clicraft 0.4.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.
@@ -0,0 +1,427 @@
1
+ import chalk from 'chalk';
2
+ import inquirer from 'inquirer';
3
+ import {
4
+ loadAccounts,
5
+ getCurrentAccount,
6
+ getAccount,
7
+ addAccount,
8
+ removeAccount,
9
+ switchAccount as switchAccountStorage,
10
+ getAllAccounts,
11
+ getAccountCount,
12
+ updateAccount,
13
+ migrateFromLegacy
14
+ } from '../helpers/auth/index.js';
15
+ import {
16
+ performAuthentication,
17
+ refreshAccountAuth
18
+ } from '../helpers/auth/microsoft.js';
19
+
20
+ // Migrate from legacy auth.json on first run
21
+ migrateFromLegacy();
22
+
23
+ /**
24
+ * Load current auth (for backward compatibility)
25
+ */
26
+ export function loadAuth() {
27
+ return getCurrentAccount();
28
+ }
29
+
30
+ /**
31
+ * Refresh authentication for current account
32
+ */
33
+ export async function refreshAuth() {
34
+ const account = getCurrentAccount();
35
+
36
+ if (!account) {
37
+ return null;
38
+ }
39
+
40
+ // Check if token is still valid (with 5 min buffer)
41
+ if (account.expiresAt && Date.now() < account.expiresAt - 300000) {
42
+ return account;
43
+ }
44
+
45
+ // Try to refresh
46
+ try {
47
+ console.log(chalk.gray('Refreshing authentication...'));
48
+ const refreshed = await refreshAccountAuth(account);
49
+
50
+ if (refreshed) {
51
+ updateAccount(account.uuid, refreshed);
52
+ console.log(chalk.green('āœ“ Token refreshed successfully'));
53
+ return refreshed;
54
+ }
55
+ } catch (err) {
56
+ console.log(chalk.yellow('Token refresh failed, please login again.'));
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ /**
63
+ * Login command - adds a new account or updates existing
64
+ */
65
+ async function login(options) {
66
+ try {
67
+ console.log(chalk.cyan('\nšŸ” Microsoft Login\n'));
68
+
69
+ const existingCount = getAccountCount();
70
+ if (existingCount > 0) {
71
+ console.log(chalk.gray(`You have ${existingCount} account(s) saved.`));
72
+ console.log(chalk.gray('This will add a new account or update an existing one.\n'));
73
+ }
74
+
75
+ const accountData = await performAuthentication((type, data) => {
76
+ switch (type) {
77
+ case 'open_url':
78
+ console.log(chalk.white('Please open this URL in your browser to login:\n'));
79
+ console.log(chalk.cyan(data));
80
+ console.log();
81
+ break;
82
+ case 'browser_opened':
83
+ if (data) console.log(chalk.gray('(Browser opened automatically)'));
84
+ console.log(chalk.white('\nAfter logging in, you will be redirected to a blank page.'));
85
+ console.log(chalk.white('Copy the ENTIRE URL from your browser\'s address bar and paste it below:'));
86
+ break;
87
+ case 'status':
88
+ console.log(chalk.gray(data));
89
+ break;
90
+ }
91
+ });
92
+
93
+ // Check if account already exists
94
+ const existing = getAccount(accountData.uuid);
95
+ const action = existing ? 'updated' : 'added';
96
+
97
+ addAccount(accountData, true);
98
+
99
+ console.log(chalk.green(`\nāœ… Successfully ${action} account: ${chalk.bold(accountData.username)}`));
100
+ console.log(chalk.gray(` UUID: ${accountData.uuid}`));
101
+
102
+ const count = getAccountCount();
103
+ if (count > 1) {
104
+ console.log(chalk.gray(` Total accounts: ${count}`));
105
+ console.log(chalk.gray(` This account is now active.`));
106
+ }
107
+
108
+ } catch (error) {
109
+ console.error(chalk.red('\nLogin failed:'), error.message);
110
+ if (options?.verbose) {
111
+ console.error(error);
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Logout command - removes an account
118
+ */
119
+ async function logout(identifier, options) {
120
+ console.log(chalk.cyan('\nšŸ” Logout\n'));
121
+
122
+ const accounts = getAllAccounts();
123
+
124
+ if (accounts.length === 0) {
125
+ console.log(chalk.yellow('No accounts are logged in.'));
126
+ return;
127
+ }
128
+
129
+ let accountToRemove;
130
+
131
+ if (identifier) {
132
+ // Remove specific account
133
+ accountToRemove = getAccount(identifier);
134
+ if (!accountToRemove) {
135
+ console.log(chalk.red(`Account "${identifier}" not found.`));
136
+ console.log(chalk.gray('Available accounts:'));
137
+ for (const acc of accounts) {
138
+ console.log(chalk.gray(` - ${acc.username} (${acc.uuid})`));
139
+ }
140
+ return;
141
+ }
142
+ } else if (accounts.length === 1) {
143
+ // Only one account, remove it
144
+ accountToRemove = accounts[0];
145
+ } else {
146
+ // Multiple accounts, ask which one
147
+ const { selected } = await inquirer.prompt([{
148
+ type: 'list',
149
+ name: 'selected',
150
+ message: 'Which account do you want to logout?',
151
+ choices: [
152
+ ...accounts.map(acc => ({
153
+ name: `${acc.username} (${acc.uuid.substring(0, 8)}...)`,
154
+ value: acc.uuid
155
+ })),
156
+ { name: chalk.red('Cancel'), value: null }
157
+ ]
158
+ }]);
159
+
160
+ if (!selected) {
161
+ console.log(chalk.gray('Cancelled.'));
162
+ return;
163
+ }
164
+
165
+ accountToRemove = getAccount(selected);
166
+ }
167
+
168
+ if (!options?.force) {
169
+ const { confirm } = await inquirer.prompt([{
170
+ type: 'confirm',
171
+ name: 'confirm',
172
+ message: `Are you sure you want to logout ${accountToRemove.username}?`,
173
+ default: false
174
+ }]);
175
+
176
+ if (!confirm) {
177
+ console.log(chalk.gray('Cancelled.'));
178
+ return;
179
+ }
180
+ }
181
+
182
+ const removed = removeAccount(accountToRemove.uuid);
183
+ if (removed) {
184
+ console.log(chalk.green(`āœ… Logged out: ${removed.username}`));
185
+
186
+ const remaining = getAccountCount();
187
+ if (remaining > 0) {
188
+ const current = getCurrentAccount();
189
+ console.log(chalk.gray(` Remaining accounts: ${remaining}`));
190
+ if (current) {
191
+ console.log(chalk.gray(` Active account: ${current.username}`));
192
+ }
193
+ } else {
194
+ console.log(chalk.gray(' No accounts remaining.'));
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Switch account command
201
+ */
202
+ async function switchAccountCmd(identifier, options) {
203
+ console.log(chalk.cyan('\nšŸ”„ Switch Account\n'));
204
+
205
+ const accounts = getAllAccounts();
206
+
207
+ if (accounts.length === 0) {
208
+ console.log(chalk.yellow('No accounts are logged in.'));
209
+ console.log(chalk.gray('Use "clicraft auth login" to add an account.'));
210
+ return;
211
+ }
212
+
213
+ if (accounts.length === 1) {
214
+ console.log(chalk.yellow('Only one account is logged in.'));
215
+ console.log(chalk.gray('Use "clicraft auth login" to add another account.'));
216
+ return;
217
+ }
218
+
219
+ const currentAccount = getCurrentAccount();
220
+ let targetAccount;
221
+
222
+ if (identifier) {
223
+ targetAccount = getAccount(identifier);
224
+ if (!targetAccount) {
225
+ console.log(chalk.red(`Account "${identifier}" not found.`));
226
+ console.log(chalk.gray('Available accounts:'));
227
+ for (const acc of accounts) {
228
+ const current = acc.uuid === currentAccount?.uuid ? chalk.green(' (current)') : '';
229
+ console.log(chalk.gray(` - ${acc.username}${current}`));
230
+ }
231
+ return;
232
+ }
233
+ } else {
234
+ // Interactive selection
235
+ const { selected } = await inquirer.prompt([{
236
+ type: 'list',
237
+ name: 'selected',
238
+ message: 'Select an account to switch to:',
239
+ choices: accounts.map(acc => ({
240
+ name: acc.uuid === currentAccount?.uuid
241
+ ? `${acc.username} ${chalk.green('(current)')}`
242
+ : acc.username,
243
+ value: acc.uuid,
244
+ disabled: acc.uuid === currentAccount?.uuid ? 'current' : false
245
+ }))
246
+ }]);
247
+
248
+ targetAccount = getAccount(selected);
249
+ }
250
+
251
+ if (targetAccount.uuid === currentAccount?.uuid) {
252
+ console.log(chalk.yellow(`Already using account: ${targetAccount.username}`));
253
+ return;
254
+ }
255
+
256
+ const switched = switchAccountStorage(targetAccount.uuid);
257
+ if (switched) {
258
+ console.log(chalk.green(`āœ… Switched to: ${chalk.bold(switched.username)}`));
259
+
260
+ // Check token status
261
+ const isExpired = switched.expiresAt && Date.now() >= switched.expiresAt;
262
+ if (isExpired) {
263
+ console.log(chalk.yellow(' Token expired (will refresh on next launch)'));
264
+ }
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Status command - show all accounts or specific account info
270
+ */
271
+ async function status(identifier, options) {
272
+ console.log(chalk.cyan('\nšŸŽ® Account Status\n'));
273
+
274
+ const accounts = getAllAccounts();
275
+ const currentAccount = getCurrentAccount();
276
+
277
+ if (accounts.length === 0) {
278
+ console.log(chalk.yellow('No accounts are logged in.'));
279
+ console.log(chalk.gray('Use "clicraft auth login" to add an account.'));
280
+ return;
281
+ }
282
+
283
+ if (identifier) {
284
+ // Show specific account
285
+ const account = getAccount(identifier);
286
+ if (!account) {
287
+ console.log(chalk.red(`Account "${identifier}" not found.`));
288
+ return;
289
+ }
290
+
291
+ displayAccountDetails(account, account.uuid === currentAccount?.uuid);
292
+ } else {
293
+ // Show all accounts
294
+ console.log(chalk.white(`${accounts.length} account(s) saved:\n`));
295
+
296
+ for (const account of accounts) {
297
+ const isCurrent = account.uuid === currentAccount?.uuid;
298
+ displayAccountSummary(account, isCurrent);
299
+ }
300
+
301
+ console.log();
302
+ console.log(chalk.gray('Use "clicraft auth status <username>" for detailed info.'));
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Display account summary (one line)
308
+ */
309
+ function displayAccountSummary(account, isCurrent) {
310
+ const marker = isCurrent ? chalk.green('ā–¶ ') : ' ';
311
+ const username = isCurrent
312
+ ? chalk.bold.green(account.username)
313
+ : chalk.white(account.username);
314
+
315
+ const isExpired = account.expiresAt && Date.now() >= account.expiresAt;
316
+ const tokenStatus = isExpired
317
+ ? chalk.yellow('(expired)')
318
+ : chalk.green('(valid)');
319
+
320
+ console.log(`${marker}${username} ${chalk.gray(`(${account.uuid.substring(0, 8)}...)`)} ${tokenStatus}`);
321
+ }
322
+
323
+ /**
324
+ * Display detailed account info
325
+ */
326
+ function displayAccountDetails(account, isCurrent) {
327
+ const status = isCurrent ? chalk.green(' (active)') : '';
328
+
329
+ console.log(chalk.white(`Username: ${chalk.bold(account.username)}${status}`));
330
+ console.log(chalk.gray(`UUID: ${account.uuid}`));
331
+ console.log(chalk.gray(`Authenticated: ${account.authenticatedAt}`));
332
+
333
+ if (account.refreshedAt) {
334
+ console.log(chalk.gray(`Last Refreshed: ${account.refreshedAt}`));
335
+ }
336
+
337
+ const isExpired = account.expiresAt && Date.now() >= account.expiresAt;
338
+ const canRefresh = !!account.refreshToken;
339
+
340
+ if (isExpired) {
341
+ if (canRefresh) {
342
+ console.log(chalk.yellow('Token expired (will refresh on next launch)'));
343
+ } else {
344
+ console.log(chalk.red('Token expired (please login again)'));
345
+ }
346
+ } else {
347
+ const expiresIn = Math.round((account.expiresAt - Date.now()) / 60000);
348
+ console.log(chalk.green(`Token valid for ${expiresIn} minutes`));
349
+ }
350
+ }
351
+
352
+ /**
353
+ * List accounts (alias for status without args)
354
+ */
355
+ async function listAccounts(options) {
356
+ await status(null, options);
357
+ }
358
+
359
+ /**
360
+ * Main auth command handler
361
+ */
362
+ export async function authCommand(action, args, options) {
363
+ // Handle the case where action might be in args for 'auth status <name>'
364
+ const actionArg = Array.isArray(args) ? args[0] : args;
365
+
366
+ switch (action) {
367
+ case 'login':
368
+ case 'add':
369
+ await login(options);
370
+ break;
371
+
372
+ case 'logout':
373
+ case 'remove':
374
+ await logout(actionArg, options);
375
+ break;
376
+
377
+ case 'switch':
378
+ case 'switch-account':
379
+ case 'use':
380
+ await switchAccountCmd(actionArg, options);
381
+ break;
382
+
383
+ case 'status':
384
+ case 'info':
385
+ await status(actionArg, options);
386
+ break;
387
+
388
+ case 'list':
389
+ case 'accounts':
390
+ await listAccounts(options);
391
+ break;
392
+
393
+ case undefined:
394
+ // No action, show help
395
+ console.log(chalk.cyan('\nšŸ” Auth Commands\n'));
396
+ console.log(chalk.gray('Available actions:\n'));
397
+ console.log(` ${chalk.white('login')} Add a new account or update existing`);
398
+ console.log(` ${chalk.white('logout [account]')} Remove an account`);
399
+ console.log(` ${chalk.white('switch [account]')} Switch to a different account`);
400
+ console.log(` ${chalk.white('status [account]')} Show account status`);
401
+ console.log(` ${chalk.white('list')} List all accounts`);
402
+ console.log();
403
+ console.log(chalk.gray('Examples:'));
404
+ console.log(chalk.gray(' clicraft auth login'));
405
+ console.log(chalk.gray(' clicraft auth switch PlayerName'));
406
+ console.log(chalk.gray(' clicraft auth logout'));
407
+ console.log(chalk.gray(' clicraft auth status'));
408
+ break;
409
+
410
+ default:
411
+ // Treat as account identifier for status
412
+ await status(action, options);
413
+ break;
414
+ }
415
+ }
416
+
417
+ // Legacy exports for backward compatibility
418
+ export { login, logout };
419
+ export { status as authStatus };
420
+
421
+ export default {
422
+ authCommand,
423
+ login,
424
+ logout,
425
+ loadAuth,
426
+ refreshAuth
427
+ };