mikasa-cli 1.0.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.
@@ -0,0 +1,607 @@
1
+ import { cancel, confirm, intro, isCancel, outro } from "@clack/prompts";
2
+ import { logger } from "better-auth";
3
+ import { createAuthClient } from "better-auth/client";
4
+ import { deviceAuthorizationClient } from "better-auth/client/plugins";
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import fs from "fs/promises";
8
+ import open from "open";
9
+ import os from "os";
10
+ import path from "path";
11
+ import yoctoSpinner from "yocto-spinner";
12
+ import * as z from "zod/v4";
13
+ // import dotenv from "dotenv";
14
+ import "dotenv/config";
15
+ import prisma from "../../../lib/db.js";
16
+
17
+ // dotenv.config();
18
+
19
+ const DEMO_URL = "https://mikasa-cli-2.onrender.com";
20
+ const CLIENT_ID = process.env.GITHUB_CLIENT_ID;
21
+ const CONFIG_DIR = path.join(os.homedir(), ".better-auth");
22
+ const TOKEN_FILE = path.join(CONFIG_DIR, "token.json");
23
+
24
+ // ============================================
25
+ // TOKEN MANAGEMENT (Export these for use in other commands)
26
+ // ============================================
27
+
28
+ export async function getStoredToken() {
29
+ try {
30
+ const data = await fs.readFile(TOKEN_FILE, "utf-8");
31
+ const token = JSON.parse(data);
32
+ return token;
33
+ } catch (error) {
34
+ // File doesn't exist or can't be read
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export async function storeToken(token) {
40
+ try {
41
+ // Ensure config directory exists
42
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
43
+
44
+ // Store token with metadata
45
+ const tokenData = {
46
+ access_token: token.access_token,
47
+ refresh_token: token.refresh_token, // Store if available
48
+ token_type: token.token_type || "Bearer",
49
+ scope: token.scope,
50
+ expires_at: token.expires_in
51
+ ? new Date(Date.now() + token.expires_in * 1000).toISOString()
52
+ : null,
53
+ created_at: new Date().toISOString(),
54
+ };
55
+
56
+ await fs.writeFile(TOKEN_FILE, JSON.stringify(tokenData, null, 2), "utf-8");
57
+ return true;
58
+ } catch (error) {
59
+ console.error(chalk.red("Failed to store token:"), error.message);
60
+ return false;
61
+ }
62
+ }
63
+
64
+ export async function clearStoredToken() {
65
+ try {
66
+ await fs.unlink(TOKEN_FILE);
67
+ return true;
68
+ } catch (error) {
69
+ // File doesn't exist or can't be deleted
70
+ return false;
71
+ }
72
+ }
73
+
74
+ export async function isTokenExpired() {
75
+ const token = await getStoredToken();
76
+ if (!token || !token.expires_at) {
77
+ return true;
78
+ }
79
+
80
+ const expiresAt = new Date(token.expires_at);
81
+ const now = new Date();
82
+
83
+ // Consider expired if less than 5 minutes remaining
84
+ return expiresAt.getTime() - now.getTime() < 5 * 60 * 1000;
85
+ }
86
+
87
+ export async function requireAuth() {
88
+ const token = await getStoredToken();
89
+
90
+ if (!token) {
91
+ console.log(
92
+ chalk.red("❌ Not authenticated. Please run 'MikasaCLI login' first.")
93
+ );
94
+ process.exit(1);
95
+ }
96
+
97
+ if (await isTokenExpired()) {
98
+ console.log(
99
+ chalk.yellow("⚠️ Your session has expired. Please login again.")
100
+ );
101
+ console.log(chalk.gray(" Run: your-cli login\n"));
102
+ process.exit(1);
103
+ }
104
+
105
+ return token;
106
+ }
107
+
108
+ // ============================================
109
+ // LOGIN COMMAND
110
+ // ============================================
111
+
112
+ export async function loginAction(opts) {
113
+ const options = z
114
+ .object({
115
+ serverUrl: z.string().optional(),
116
+ clientId: z.string().optional(),
117
+ })
118
+ .parse(opts);
119
+
120
+ const serverUrl = options.serverUrl || DEMO_URL;
121
+ const clientId = options.clientId || CLIENT_ID;
122
+
123
+ intro(chalk.bold("🔐 MikasaCLI Login"));
124
+
125
+ if (!clientId) {
126
+ logger.error("CLIENT_ID is not set in .env file");
127
+ console.log(
128
+ chalk.red("\n❌ Please set GITHUB_CLIENT_ID in your .env file")
129
+ );
130
+ process.exit(1);
131
+ }
132
+
133
+ // Check if already logged in
134
+ const existingToken = await getStoredToken();
135
+ const expired = await isTokenExpired();
136
+
137
+ if (existingToken && !expired) {
138
+ const shouldReauth = await confirm({
139
+ message: "You're already logged in. Do you want to log in again?",
140
+ initialValue: false,
141
+ });
142
+
143
+ if (isCancel(shouldReauth) || !shouldReauth) {
144
+ cancel("Login cancelled");
145
+ process.exit(0);
146
+ }
147
+ }
148
+
149
+ // Create the auth client
150
+ const authClient = createAuthClient({
151
+ baseURL: serverUrl,
152
+ plugins: [deviceAuthorizationClient()],
153
+ });
154
+
155
+ const spinner = yoctoSpinner({ text: "Requesting device authorization..." });
156
+ spinner.start();
157
+
158
+ try {
159
+ // Request device code
160
+ const { data, error } = await authClient.device.code({
161
+ client_id: clientId,
162
+ scope: "openid profile email",
163
+ });
164
+
165
+ spinner.stop();
166
+
167
+ if (error || !data) {
168
+ logger.error(
169
+ `Failed to request device authorization: ${error?.error_description || error?.message || "Unknown error"
170
+ }`
171
+ );
172
+
173
+ if (error?.status === 404) {
174
+ console.log(chalk.red("\n❌ Device authorization endpoint not found."));
175
+ console.log(chalk.yellow(" Make sure your auth server is running."));
176
+ } else if (error?.status === 400) {
177
+ console.log(
178
+ chalk.red("\n❌ Bad request - check your CLIENT_ID configuration.")
179
+ );
180
+ }
181
+
182
+ process.exit(1);
183
+ }
184
+
185
+ const {
186
+ device_code,
187
+ user_code,
188
+ verification_uri,
189
+ verification_uri_complete,
190
+ interval = 5,
191
+ expires_in,
192
+ } = data;
193
+
194
+ // Display authorization instructions
195
+ console.log("");
196
+ console.log(chalk.cyan("📱 Device Authorization Required"));
197
+ console.log("");
198
+ console.log(
199
+ `Please visit: ${chalk.underline.blue(
200
+ verification_uri_complete || verification_uri
201
+ )}`
202
+ );
203
+ console.log(`Enter code: ${chalk.bold.green(user_code)}`);
204
+ console.log("");
205
+
206
+ // Ask if user wants to open browser
207
+ const shouldOpen = await confirm({
208
+ message: "Open browser automatically?",
209
+ initialValue: true,
210
+ });
211
+
212
+ if (!isCancel(shouldOpen) && shouldOpen) {
213
+ const urlToOpen = verification_uri_complete || verification_uri;
214
+ await open(urlToOpen);
215
+ }
216
+
217
+ // Start polling
218
+ console.log(
219
+ chalk.gray(
220
+ `Waiting for authorization (expires in ${Math.floor(
221
+ expires_in / 60
222
+ )} minutes)...`
223
+ )
224
+ );
225
+
226
+ const token = await pollForToken(
227
+ authClient,
228
+ device_code,
229
+ clientId,
230
+ interval
231
+ );
232
+
233
+ if (token) {
234
+ // Store the token
235
+ const saved = await storeToken(token);
236
+
237
+ if (!saved) {
238
+ console.log(
239
+ chalk.yellow("\n⚠️ Warning: Could not save authentication token.")
240
+ );
241
+ console.log(
242
+ chalk.yellow(" You may need to login again on next use.")
243
+ );
244
+ }
245
+
246
+ // Get user info
247
+ const { data: session } = await authClient.getSession({
248
+ fetchOptions: {
249
+ headers: {
250
+ Authorization: `Bearer ${token.access_token}`,
251
+ },
252
+ },
253
+ });
254
+
255
+ outro(
256
+ chalk.green(
257
+ `✅ Login successful! Welcome ${session?.user?.name || session?.user?.email || "User"
258
+ }`
259
+ )
260
+ );
261
+
262
+ console.log(chalk.gray(`\n📁 Token saved to: ${TOKEN_FILE}`));
263
+ console.log(
264
+ chalk.gray(" You can now use AI commands without logging in again.\n")
265
+ );
266
+ }
267
+ } catch (err) {
268
+ spinner.stop();
269
+ console.error(chalk.red("\nLogin failed:"), err.message);
270
+ process.exit(1);
271
+ }
272
+ }
273
+
274
+ async function pollForToken(authClient, deviceCode, clientId, initialInterval) {
275
+ let pollingInterval = initialInterval;
276
+ const spinner = yoctoSpinner({ text: "", color: "cyan" });
277
+ let dots = 0;
278
+
279
+ return new Promise((resolve, reject) => {
280
+ const poll = async () => {
281
+ dots = (dots + 1) % 4;
282
+ spinner.text = chalk.gray(
283
+ `Polling for authorization${".".repeat(dots)}${" ".repeat(3 - dots)}`
284
+ );
285
+ if (!spinner.isSpinning) spinner.start();
286
+
287
+ try {
288
+ const { data, error } = await authClient.device.token({
289
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
290
+ device_code: deviceCode,
291
+ client_id: clientId,
292
+ fetchOptions: {
293
+ headers: {
294
+ "user-agent": `Better Auth CLI`,
295
+ },
296
+ },
297
+ });
298
+
299
+ if (data?.access_token) {
300
+ console.log(
301
+ chalk.bold.yellow(`Your access token: ${data.access_token}`)
302
+ );
303
+ spinner.stop();
304
+ resolve(data);
305
+ return;
306
+ } else if (error) {
307
+ switch (error.error) {
308
+ case "authorization_pending":
309
+ // Continue polling
310
+ break;
311
+ case "slow_down":
312
+ pollingInterval += 5;
313
+ break;
314
+ case "access_denied":
315
+ spinner.stop();
316
+ logger.error("Access was denied by the user");
317
+ process.exit(1);
318
+ break;
319
+ case "expired_token":
320
+ spinner.stop();
321
+ logger.error("The device code has expired. Please try again.");
322
+ process.exit(1);
323
+ break;
324
+ default:
325
+ spinner.stop();
326
+ logger.error(`Error: ${error.error_description}`);
327
+ process.exit(1);
328
+ }
329
+ }
330
+ } catch (err) {
331
+ spinner.stop();
332
+ logger.error(`Network error: ${err.message}`);
333
+ process.exit(1);
334
+ }
335
+
336
+ setTimeout(poll, pollingInterval * 1000);
337
+ };
338
+
339
+ setTimeout(poll, pollingInterval * 1000);
340
+ });
341
+ }
342
+
343
+ // ============================================
344
+ // LOGOUT COMMAND
345
+ // ============================================
346
+
347
+ export async function logoutAction() {
348
+ intro(chalk.bold("👋 Logout"));
349
+
350
+ const token = await getStoredToken();
351
+
352
+ if (!token) {
353
+ console.log(chalk.yellow("You're not logged in."));
354
+ process.exit(0);
355
+ }
356
+
357
+ const shouldLogout = await confirm({
358
+ message: "Are you sure you want to logout?",
359
+ initialValue: false,
360
+ });
361
+
362
+ if (isCancel(shouldLogout) || !shouldLogout) {
363
+ cancel("Logout cancelled");
364
+ process.exit(0);
365
+ }
366
+
367
+ const cleared = await clearStoredToken();
368
+
369
+ if (cleared) {
370
+ outro(chalk.green("✅ Successfully logged out!"));
371
+ } else {
372
+ console.log(chalk.yellow("⚠️ Could not clear token file."));
373
+ }
374
+ }
375
+
376
+ // ============================================
377
+ // WHOAMI COMMAND
378
+ // ============================================
379
+
380
+ export async function whoamiAction(opts) {
381
+ const token = await requireAuth();
382
+ if (!token?.access_token) {
383
+ console.log("No access token found. Please login.");
384
+ process.exit(1);
385
+ }
386
+
387
+ const user = await prisma.user.findFirst({
388
+ where: {
389
+ sessions: {
390
+ some: {
391
+ token: token.access_token,
392
+ },
393
+ },
394
+ },
395
+ select: {
396
+ id: true,
397
+ name: true,
398
+ email: true,
399
+ image: true,
400
+ },
401
+ });
402
+
403
+ // Output user session info
404
+ console.log(
405
+ chalk.bold.greenBright(`\n👤 User: ${user.name}
406
+ 📧 Email: ${user.email}
407
+ 👤 ID: ${user.id}`)
408
+ );
409
+ }
410
+
411
+ // ============================================
412
+ // COMMANDER SETUP
413
+ // ============================================
414
+
415
+ export const login = new Command("login")
416
+ .description("Login to MikasaCLI")
417
+ .option("--server-url <url>", "The Better Auth server URL", DEMO_URL)
418
+ .option("--client-id <id>", "The OAuth client ID", CLIENT_ID)
419
+ .action(loginAction);
420
+
421
+ export const logout = new Command("logout")
422
+ .description("Logout and clear stored credentials")
423
+ .action(logoutAction);
424
+
425
+ export const whoami = new Command("status")
426
+ .description("Show current authenticated user")
427
+ .option("--server-url <url>", "The Better Auth server URL", DEMO_URL)
428
+ .action(whoamiAction);
429
+
430
+
431
+
432
+
433
+
434
+ // import { cancel, confirm, intro, isCancel, outro } from "@clack/prompts";
435
+ // import { createAuthClient } from "better-auth/client";
436
+ // import { deviceAuthorizationClient } from "better-auth/client/plugins";
437
+ // import chalk from "chalk";
438
+ // import { Command } from "commander";
439
+ // import open from "open";
440
+ // import os from "os";
441
+ // import path from "path";
442
+ // import yoctoSpinner from "yocto-spinner";
443
+ // import * as z from "zod";
444
+ // import dotenv from "dotenv";
445
+ // import { logger } from "better-auth";
446
+
447
+ // dotenv.config();
448
+
449
+ // const DEFAULT_URL = "http://localhost:5000";
450
+ // const DEFAULT_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
451
+
452
+ // export const CONFIG_DIR = path.join(os.homedir(), ".better-auth");
453
+ // export const TOKEN_FILE = path.join(CONFIG_DIR, "token.json");
454
+
455
+ // // token management (export these for use in other commands)
456
+
457
+
458
+
459
+ // export async function loginAction(opts) {
460
+ // const schema = z.object({
461
+ // serverUrl: z.string().optional(),
462
+ // clientId: z.string().optional(),
463
+ // });
464
+
465
+ // const { serverUrl, clientId } = schema.parse(opts);
466
+
467
+ // const baseURL = serverUrl || DEFAULT_URL;
468
+ // const clientID = clientId || DEFAULT_CLIENT_ID;
469
+
470
+ // if (!clientID) {
471
+ // cancel("GitHub Client ID is missing.");
472
+ // process.exit(1);
473
+ // }
474
+
475
+ // intro(chalk.green("Welcome to Mikasa CLI! Let's get you logged in."));
476
+
477
+ // const authClient = createAuthClient({
478
+ // baseURL,
479
+ // plugins: [deviceAuthorizationClient()],
480
+ // });
481
+
482
+ // const spinner = yoctoSpinner({ text: "Starting authentication..." });
483
+ // spinner.start();
484
+
485
+ // try {
486
+ // const { data, error } = await authClient.device.code({
487
+ // client_id: clientID,
488
+ // scope: "openid profile email",
489
+ // });
490
+
491
+ // spinner.stop();
492
+
493
+ // if (error || !data) {
494
+ // cancel("Failed to start device authorization.");
495
+ // process.exit(1);
496
+ // }
497
+
498
+ // const {
499
+ // device_code,
500
+ // user_code,
501
+ // verification_uri,
502
+ // verification_uri_complete,
503
+ // expires_in,
504
+ // interval = 5,
505
+ // } = data;
506
+
507
+ // console.log(chalk.cyan("\nDevice Authorization Required\n"));
508
+ // console.log(`Visit: ${chalk.underline.blue(verification_uri)}`);
509
+ // console.log(`Enter code: ${chalk.bold.yellow(user_code)}\n`);
510
+
511
+ // const shouldOpen = await confirm({
512
+ // message: "Open the verification URL in your browser?",
513
+ // initialValue: true,
514
+ // });
515
+
516
+ // if (!isCancel(shouldOpen) && shouldOpen) {
517
+ // await open(verification_uri_complete || verification_uri);
518
+ // }
519
+
520
+ // console.log(
521
+ // chalk.gray(
522
+ // `Waiting for authorization (expires in ${Math.floor(
523
+ // expires_in / 60
524
+ // )} minutes)...`
525
+ // )
526
+ // );
527
+
528
+ // const token = await pollForToken(
529
+ // authClient,
530
+ // device_code,
531
+ // interval,
532
+ // // expires_in,
533
+ // clientID
534
+ // );
535
+
536
+ // // outro(chalk.green("Complete authentication in the browser."));
537
+ // } catch (err) {
538
+ // spinner.stop();
539
+ // console.error(chalk.red("Authentication failed:"), err);
540
+ // process.exit(1);
541
+ // }
542
+ // }
543
+
544
+ // async function pollForToken(authClient, device_code, interval, clientID) {
545
+ // let pollingInterval = interval;
546
+ // const spinner = yoctoSpinner({ text: "", color: "cyan" });
547
+ // let dots = 0;
548
+
549
+ // return new Promise((resolve, reject) => {
550
+ // const poll = async () => {
551
+ // dots = (dots + 1) % 4;
552
+ // spinner.text = chalk.gray(`Polling for authorization${'.'.repeat(dots)}${' '.repeat(3 - dots)}`);
553
+ // if (!spinner.isSpinning) spinner.start();
554
+ // try {
555
+ // const { data, error } = await authClient.device.token({
556
+ // grant_type: "urn:ietf:params:oauth:grant-type:device_code",
557
+ // device_code,
558
+ // client_id: clientID,
559
+ // fatchOptions: {
560
+ // "user-agent": "mikasa-cli/1.0.0",
561
+ // },
562
+ // });
563
+
564
+ // if (data?.access_token) {
565
+ // console.log(
566
+ // chalk.bold.yellow(`Your access token: ${data.access_token}`)
567
+ // );
568
+ // spinner.stop();
569
+ // resolve(data);
570
+ // return;
571
+ // } else if (error) {
572
+ // switch (error.error) {
573
+ // case "authorization_pending":
574
+ // // continue polling
575
+ // break;
576
+ // case "slow_down":
577
+ // pollingInterval += 5;
578
+ // break;
579
+ // case "access_denied":
580
+ // console.error(chalk.red("Authorization denied by user."));
581
+ // return;
582
+ // case "expired_token":
583
+ // console.error(chalk.red("Device code has expired. Please restart the login process."));
584
+ // return;
585
+ // default:
586
+ // spinner.stop();
587
+ // logger.error(chalk.red(`Error: ${error.error_description}`));
588
+ // process.exit(1);
589
+ // }
590
+ // }
591
+ // } catch (error) {
592
+ // spinner.stop();
593
+ // logger.error(chalk.red("Polling failed:"), error);
594
+ // process.exit(1);
595
+ // }
596
+ // setTimeout(poll, pollingInterval * 1000);
597
+ // };
598
+ // z.set
599
+ // });
600
+ // }
601
+
602
+ // // CLI command
603
+ // export const login = new Command("login")
604
+ // .description("Login to Mikasa CLI using GitHub OAuth.")
605
+ // .option("--server-url <url>", "Authentication server URL")
606
+ // .option("--client-id <id>", "GitHub OAuth Client ID")
607
+ // .action(loginAction);
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ import dotenv from "dotenv";
4
+ import chalk from "chalk";
5
+ import figlet from "figlet";
6
+ import { Command } from "commander";
7
+ import { login, logout, whoami } from "./commands/auth/login.js";
8
+ import { wakeUp } from "./commands/ai/wakeUp.js";
9
+
10
+ dotenv.config();
11
+
12
+ async function main() {
13
+ console.log(
14
+ chalk.green(
15
+ figlet.textSync("Mikasa CLI", {
16
+ font: "Standard",
17
+ horizontalLayout: "default",
18
+ })
19
+ )
20
+ );
21
+
22
+ console.log(chalk.red("Welcome to Mikasa CLI!\n"));
23
+
24
+ const program = new Command("Mikasa");
25
+
26
+ program
27
+ .name("mikasa")
28
+ .version("1.0.0")
29
+ .description(
30
+ "Mikasa CLI - A powerful AI command line tool.\n" +
31
+ "Built by Suman Kayal.\n" +
32
+ "GitHub: https://github.com/SUMANKAYALS\n" +
33
+ "LinkedIn: https://www.linkedin.com/in/suman-kayal10/\n"
34
+ );
35
+
36
+ // 🔥 COMMANDS
37
+ program
38
+ .addCommand(login);
39
+ program.addCommand(logout);
40
+
41
+ program.addCommand(whoami);
42
+ program.addCommand(wakeUp);
43
+
44
+ // Default action → show help
45
+ program.action(() => {
46
+ program.help();
47
+ });
48
+
49
+ // 🔥 REQUIRED
50
+ program.parse(process.argv);
51
+ }
52
+
53
+ main().catch((err) => {
54
+ console.error(chalk.red("Error starting Mikasa CLI:"), err);
55
+ process.exit(1);
56
+ });