@bragduck/cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2346 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // node_modules/tsup/assets/esm_shims.js
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+ var init_esm_shims = __esm({
16
+ "node_modules/tsup/assets/esm_shims.js"() {
17
+ "use strict";
18
+ }
19
+ });
20
+
21
+ // src/constants.ts
22
+ import { config } from "dotenv";
23
+ import { fileURLToPath as fileURLToPath2 } from "url";
24
+ import { dirname, join } from "path";
25
+ var __filename, __dirname, APP_NAME, CONFIG_KEYS, DEFAULT_CONFIG, OAUTH_CONFIG, API_ENDPOINTS, ENCRYPTION_CONFIG, STORAGE_PATHS, HTTP_STATUS;
26
+ var init_constants = __esm({
27
+ "src/constants.ts"() {
28
+ "use strict";
29
+ init_esm_shims();
30
+ __filename = fileURLToPath2(import.meta.url);
31
+ __dirname = dirname(__filename);
32
+ config({ path: join(__dirname, "..", "..", ".env") });
33
+ APP_NAME = "bragduck";
34
+ CONFIG_KEYS = {
35
+ API_BASE_URL: "apiBaseUrl",
36
+ DEFAULT_COMMIT_DAYS: "defaultCommitDays",
37
+ AUTO_VERSION_CHECK: "autoVersionCheck"
38
+ };
39
+ DEFAULT_CONFIG = {
40
+ apiBaseUrl: process.env.API_BASE_URL || "https://api.bragduck.com",
41
+ defaultCommitDays: 30,
42
+ autoVersionCheck: true
43
+ };
44
+ OAUTH_CONFIG = {
45
+ CLIENT_ID: "bragduck-cli",
46
+ CALLBACK_PATH: "/callback",
47
+ TIMEOUT_MS: 12e4,
48
+ // 2 minutes
49
+ MIN_PORT: 8e3,
50
+ MAX_PORT: 9e3
51
+ };
52
+ API_ENDPOINTS = {
53
+ AUTH: {
54
+ INITIATE: "/v1/auth/cli/initiate",
55
+ TOKEN: "/v1/auth/cli/token"
56
+ },
57
+ COMMITS: {
58
+ REFINE: "/v1/commits/refine"
59
+ },
60
+ BRAGS: {
61
+ CREATE: "/v1/brags",
62
+ LIST: "/v1/brags"
63
+ },
64
+ VERSION: "/v1/cli/version"
65
+ };
66
+ ENCRYPTION_CONFIG = {
67
+ ALGORITHM: "aes-256-gcm",
68
+ KEY_LENGTH: 32,
69
+ IV_LENGTH: 16,
70
+ AUTH_TAG_LENGTH: 16,
71
+ SALT_LENGTH: 32
72
+ };
73
+ STORAGE_PATHS = {
74
+ CREDENTIALS_DIR: ".bragduck",
75
+ CREDENTIALS_FILE: "credentials.enc",
76
+ CONFIG_FILE: "config.json"
77
+ };
78
+ HTTP_STATUS = {
79
+ OK: 200,
80
+ CREATED: 201,
81
+ BAD_REQUEST: 400,
82
+ UNAUTHORIZED: 401,
83
+ FORBIDDEN: 403,
84
+ NOT_FOUND: 404,
85
+ INTERNAL_SERVER_ERROR: 500
86
+ };
87
+ }
88
+ });
89
+
90
+ // src/services/storage.service.ts
91
+ import Conf from "conf";
92
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
93
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
94
+ import { homedir } from "os";
95
+ import { join as join2 } from "path";
96
+ var StorageService, storageService;
97
+ var init_storage_service = __esm({
98
+ "src/services/storage.service.ts"() {
99
+ "use strict";
100
+ init_esm_shims();
101
+ init_constants();
102
+ StorageService = class {
103
+ config;
104
+ storageBackend;
105
+ credentialsDir;
106
+ credentialsFilePath;
107
+ constructor() {
108
+ this.config = new Conf({
109
+ projectName: APP_NAME,
110
+ defaults: DEFAULT_CONFIG
111
+ });
112
+ this.credentialsDir = join2(homedir(), STORAGE_PATHS.CREDENTIALS_DIR);
113
+ this.credentialsFilePath = join2(this.credentialsDir, STORAGE_PATHS.CREDENTIALS_FILE);
114
+ this.storageBackend = "file";
115
+ this.ensureCredentialsDir();
116
+ }
117
+ /**
118
+ * Ensure credentials directory exists
119
+ */
120
+ ensureCredentialsDir() {
121
+ if (!existsSync(this.credentialsDir)) {
122
+ mkdirSync(this.credentialsDir, { recursive: true, mode: 448 });
123
+ }
124
+ }
125
+ /**
126
+ * Encrypt data for file storage
127
+ */
128
+ encrypt(data, key) {
129
+ const iv = randomBytes(ENCRYPTION_CONFIG.IV_LENGTH);
130
+ const cipher = createCipheriv(ENCRYPTION_CONFIG.ALGORITHM, key, iv, {
131
+ authTagLength: ENCRYPTION_CONFIG.AUTH_TAG_LENGTH
132
+ });
133
+ let encrypted = cipher.update(data, "utf8", "hex");
134
+ encrypted += cipher.final("hex");
135
+ const authTag = cipher.getAuthTag();
136
+ return {
137
+ encrypted,
138
+ iv: iv.toString("hex"),
139
+ authTag: authTag.toString("hex"),
140
+ salt: ""
141
+ // Salt is stored separately
142
+ };
143
+ }
144
+ /**
145
+ * Decrypt data from file storage
146
+ */
147
+ decrypt(encryptedData, key) {
148
+ const decipher = createDecipheriv(
149
+ ENCRYPTION_CONFIG.ALGORITHM,
150
+ key,
151
+ Buffer.from(encryptedData.iv, "hex"),
152
+ {
153
+ authTagLength: ENCRYPTION_CONFIG.AUTH_TAG_LENGTH
154
+ }
155
+ );
156
+ decipher.setAuthTag(Buffer.from(encryptedData.authTag, "hex"));
157
+ let decrypted = decipher.update(encryptedData.encrypted, "hex", "utf8");
158
+ decrypted += decipher.final("utf8");
159
+ return decrypted;
160
+ }
161
+ /**
162
+ * Derive encryption key from machine-specific data
163
+ */
164
+ deriveEncryptionKey(salt) {
165
+ const password = `${APP_NAME}-${homedir()}-${process.platform}`;
166
+ return scryptSync(password, salt, ENCRYPTION_CONFIG.KEY_LENGTH);
167
+ }
168
+ /**
169
+ * Store credentials using encrypted file storage
170
+ */
171
+ async setCredentials(credentials) {
172
+ const data = JSON.stringify(credentials);
173
+ const salt = randomBytes(ENCRYPTION_CONFIG.SALT_LENGTH);
174
+ const key = this.deriveEncryptionKey(salt);
175
+ const encrypted = this.encrypt(data, key);
176
+ encrypted.salt = salt.toString("hex");
177
+ writeFileSync(this.credentialsFilePath, JSON.stringify(encrypted), {
178
+ mode: 384,
179
+ encoding: "utf8"
180
+ });
181
+ }
182
+ /**
183
+ * Retrieve credentials from encrypted file storage
184
+ */
185
+ async getCredentials() {
186
+ if (!existsSync(this.credentialsFilePath)) {
187
+ return null;
188
+ }
189
+ try {
190
+ const encryptedData = JSON.parse(
191
+ readFileSync(this.credentialsFilePath, "utf8")
192
+ );
193
+ const salt = Buffer.from(encryptedData.salt, "hex");
194
+ const key = this.deriveEncryptionKey(salt);
195
+ const decrypted = this.decrypt(encryptedData, key);
196
+ return JSON.parse(decrypted);
197
+ } catch (error) {
198
+ console.error("Failed to decrypt credentials:", error);
199
+ return null;
200
+ }
201
+ }
202
+ /**
203
+ * Delete credentials from file storage
204
+ */
205
+ async deleteCredentials() {
206
+ if (existsSync(this.credentialsFilePath)) {
207
+ try {
208
+ unlinkSync(this.credentialsFilePath);
209
+ } catch (error) {
210
+ console.error("Failed to delete credentials file:", error);
211
+ }
212
+ }
213
+ }
214
+ /**
215
+ * Check if user is authenticated
216
+ */
217
+ async isAuthenticated() {
218
+ const credentials = await this.getCredentials();
219
+ if (!credentials || !credentials.accessToken) {
220
+ return false;
221
+ }
222
+ if (credentials.expiresAt && credentials.expiresAt < Date.now()) {
223
+ return false;
224
+ }
225
+ return true;
226
+ }
227
+ /**
228
+ * Store user information
229
+ */
230
+ setUserInfo(userInfo) {
231
+ this.config.set("userInfo", userInfo);
232
+ }
233
+ /**
234
+ * Get user information
235
+ */
236
+ getUserInfo() {
237
+ return this.config.get("userInfo") || null;
238
+ }
239
+ /**
240
+ * Delete user information
241
+ */
242
+ deleteUserInfo() {
243
+ this.config.delete("userInfo");
244
+ }
245
+ /**
246
+ * Store OAuth state for CSRF protection
247
+ */
248
+ setOAuthState(state) {
249
+ this.config.set("oauthState", state);
250
+ }
251
+ /**
252
+ * Get OAuth state
253
+ */
254
+ getOAuthState() {
255
+ return this.config.get("oauthState") || null;
256
+ }
257
+ /**
258
+ * Delete OAuth state
259
+ */
260
+ deleteOAuthState() {
261
+ this.config.delete("oauthState");
262
+ }
263
+ /**
264
+ * Get configuration value
265
+ */
266
+ getConfig(key) {
267
+ return this.config.get(key);
268
+ }
269
+ /**
270
+ * Set configuration value
271
+ */
272
+ setConfig(key, value) {
273
+ this.config.set(key, value);
274
+ }
275
+ /**
276
+ * Get all configuration
277
+ */
278
+ getAllConfig() {
279
+ return this.config.store;
280
+ }
281
+ /**
282
+ * Reset configuration to defaults
283
+ */
284
+ resetConfig() {
285
+ this.config.clear();
286
+ Object.entries(DEFAULT_CONFIG).forEach(([key, value]) => {
287
+ this.config.set(key, value);
288
+ });
289
+ }
290
+ /**
291
+ * Get current storage backend
292
+ */
293
+ getStorageBackend() {
294
+ return this.storageBackend;
295
+ }
296
+ /**
297
+ * Clear all stored data (credentials + config)
298
+ */
299
+ async clearAll() {
300
+ await this.deleteCredentials();
301
+ this.deleteUserInfo();
302
+ this.deleteOAuthState();
303
+ this.resetConfig();
304
+ }
305
+ };
306
+ storageService = new StorageService();
307
+ }
308
+ });
309
+
310
+ // src/utils/errors.ts
311
+ var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError;
312
+ var init_errors = __esm({
313
+ "src/utils/errors.ts"() {
314
+ "use strict";
315
+ init_esm_shims();
316
+ BragduckError = class extends Error {
317
+ constructor(message, code, details) {
318
+ super(message);
319
+ this.code = code;
320
+ this.details = details;
321
+ this.name = "BragduckError";
322
+ Error.captureStackTrace(this, this.constructor);
323
+ }
324
+ };
325
+ AuthenticationError = class extends BragduckError {
326
+ constructor(message, details) {
327
+ super(message, "AUTH_ERROR", details);
328
+ this.name = "AuthenticationError";
329
+ }
330
+ };
331
+ GitError = class extends BragduckError {
332
+ constructor(message, details) {
333
+ super(message, "GIT_ERROR", details);
334
+ this.name = "GitError";
335
+ }
336
+ };
337
+ ApiError = class extends BragduckError {
338
+ constructor(message, statusCode, details) {
339
+ super(message, "API_ERROR", details);
340
+ this.statusCode = statusCode;
341
+ this.name = "ApiError";
342
+ }
343
+ };
344
+ NetworkError = class extends BragduckError {
345
+ constructor(message, details) {
346
+ super(message, "NETWORK_ERROR", details);
347
+ this.name = "NetworkError";
348
+ }
349
+ };
350
+ ValidationError = class extends BragduckError {
351
+ constructor(message, details) {
352
+ super(message, "VALIDATION_ERROR", details);
353
+ this.name = "ValidationError";
354
+ }
355
+ };
356
+ OAuthError = class extends AuthenticationError {
357
+ constructor(message, details) {
358
+ super(message, details);
359
+ this.name = "OAuthError";
360
+ }
361
+ };
362
+ TokenExpiredError = class extends AuthenticationError {
363
+ constructor(message = "Authentication token has expired") {
364
+ super(message);
365
+ this.name = "TokenExpiredError";
366
+ this.code = "TOKEN_EXPIRED";
367
+ }
368
+ };
369
+ }
370
+ });
371
+
372
+ // src/utils/logger.ts
373
+ import chalk from "chalk";
374
+ var logger;
375
+ var init_logger = __esm({
376
+ "src/utils/logger.ts"() {
377
+ "use strict";
378
+ init_esm_shims();
379
+ logger = {
380
+ /**
381
+ * Debug message (only shown when DEBUG env var is set)
382
+ */
383
+ debug: (message, ...args) => {
384
+ if (process.env.DEBUG) {
385
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
386
+ }
387
+ },
388
+ /**
389
+ * Info message
390
+ */
391
+ info: (message) => {
392
+ console.log(chalk.blue(`\u2139 ${message}`));
393
+ },
394
+ /**
395
+ * Success message
396
+ */
397
+ success: (message) => {
398
+ console.log(chalk.green(`\u2713 ${message}`));
399
+ },
400
+ /**
401
+ * Warning message
402
+ */
403
+ warning: (message) => {
404
+ console.warn(chalk.yellow(`\u26A0 ${message}`));
405
+ },
406
+ /**
407
+ * Error message
408
+ */
409
+ error: (message) => {
410
+ console.error(chalk.red(`\u2717 ${message}`));
411
+ },
412
+ /**
413
+ * Plain log without formatting
414
+ */
415
+ log: (message) => {
416
+ console.log(message);
417
+ }
418
+ };
419
+ }
420
+ });
421
+
422
+ // src/utils/oauth-server.ts
423
+ import { createServer } from "http";
424
+ import { parse } from "url";
425
+ async function findAvailablePort() {
426
+ const { MIN_PORT, MAX_PORT } = OAUTH_CONFIG;
427
+ for (let port = MIN_PORT; port <= MAX_PORT; port++) {
428
+ try {
429
+ await new Promise((resolve, reject) => {
430
+ const testServer = createServer();
431
+ testServer.once("error", reject);
432
+ testServer.once("listening", () => {
433
+ testServer.close(() => resolve());
434
+ });
435
+ testServer.listen(port, "127.0.0.1");
436
+ });
437
+ return port;
438
+ } catch (error) {
439
+ continue;
440
+ }
441
+ }
442
+ throw new OAuthError(`No available ports found in range ${MIN_PORT}-${MAX_PORT}`);
443
+ }
444
+ async function startOAuthCallbackServer(expectedState) {
445
+ const port = await findAvailablePort();
446
+ const timeout = OAUTH_CONFIG.TIMEOUT_MS;
447
+ return new Promise((resolve, reject) => {
448
+ let server = null;
449
+ let timeoutId;
450
+ const cleanup = () => {
451
+ if (timeoutId) {
452
+ clearTimeout(timeoutId);
453
+ }
454
+ if (server) {
455
+ if (typeof server.closeAllConnections === "function") {
456
+ server.closeAllConnections();
457
+ }
458
+ server.close(() => {
459
+ logger.debug("OAuth server closed");
460
+ });
461
+ server.unref();
462
+ }
463
+ };
464
+ const handleRequest = (req, res) => {
465
+ const parsedUrl = parse(req.url || "", true);
466
+ logger.debug(`OAuth callback received: ${req.url}`);
467
+ if (parsedUrl.pathname === OAUTH_CONFIG.CALLBACK_PATH) {
468
+ const { code, state, error, error_description } = parsedUrl.query;
469
+ if (error) {
470
+ const errorMsg = error_description || error;
471
+ logger.debug(`OAuth error: ${errorMsg}`);
472
+ res.writeHead(400, { "Content-Type": "text/html" });
473
+ res.end(ERROR_HTML(String(errorMsg)));
474
+ cleanup();
475
+ reject(new OAuthError(`OAuth error: ${errorMsg}`));
476
+ return;
477
+ }
478
+ if (!code || !state) {
479
+ const errorMsg = "Missing code or state parameter";
480
+ logger.debug(errorMsg);
481
+ res.writeHead(400, { "Content-Type": "text/html" });
482
+ res.end(ERROR_HTML(errorMsg));
483
+ cleanup();
484
+ reject(new OAuthError(errorMsg));
485
+ return;
486
+ }
487
+ if (state !== expectedState) {
488
+ const errorMsg = "Invalid state parameter (possible CSRF attack)";
489
+ logger.debug(errorMsg);
490
+ res.writeHead(400, { "Content-Type": "text/html" });
491
+ res.end(ERROR_HTML(errorMsg));
492
+ cleanup();
493
+ reject(new OAuthError(errorMsg));
494
+ return;
495
+ }
496
+ res.writeHead(200, { "Content-Type": "text/html" });
497
+ res.end(SUCCESS_HTML);
498
+ setTimeout(() => {
499
+ cleanup();
500
+ resolve({
501
+ code: String(code),
502
+ state: String(state),
503
+ port
504
+ });
505
+ }, 100);
506
+ return;
507
+ }
508
+ res.writeHead(404, { "Content-Type": "text/plain" });
509
+ res.end("Not Found");
510
+ };
511
+ server = createServer(handleRequest);
512
+ server.on("error", (error) => {
513
+ logger.debug(`OAuth server error: ${error.message}`);
514
+ cleanup();
515
+ reject(new OAuthError(`OAuth server error: ${error.message}`));
516
+ });
517
+ server.listen(port, "127.0.0.1", () => {
518
+ logger.debug(`OAuth callback server listening on http://127.0.0.1:${port}${OAUTH_CONFIG.CALLBACK_PATH}`);
519
+ });
520
+ timeoutId = setTimeout(() => {
521
+ logger.debug("OAuth callback timeout");
522
+ cleanup();
523
+ reject(new OAuthError("Authentication timeout - no callback received within 2 minutes"));
524
+ }, timeout);
525
+ });
526
+ }
527
+ async function getCallbackUrl() {
528
+ const port = await findAvailablePort();
529
+ return `http://127.0.0.1:${port}${OAUTH_CONFIG.CALLBACK_PATH}`;
530
+ }
531
+ var SUCCESS_HTML, ERROR_HTML;
532
+ var init_oauth_server = __esm({
533
+ "src/utils/oauth-server.ts"() {
534
+ "use strict";
535
+ init_esm_shims();
536
+ init_constants();
537
+ init_errors();
538
+ init_logger();
539
+ SUCCESS_HTML = `
540
+ <!DOCTYPE html>
541
+ <html>
542
+ <head>
543
+ <meta charset="UTF-8">
544
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
545
+ <title>Bragduck - Authentication Successful</title>
546
+ <style>
547
+ body {
548
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
549
+ display: flex;
550
+ align-items: center;
551
+ justify-content: center;
552
+ min-height: 100vh;
553
+ margin: 0;
554
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
555
+ color: white;
556
+ }
557
+ .container {
558
+ text-align: center;
559
+ padding: 2rem;
560
+ background: rgba(255, 255, 255, 0.1);
561
+ border-radius: 1rem;
562
+ backdrop-filter: blur(10px);
563
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
564
+ max-width: 500px;
565
+ }
566
+ h1 {
567
+ font-size: 2.5rem;
568
+ margin: 0 0 1rem 0;
569
+ }
570
+ .checkmark {
571
+ font-size: 4rem;
572
+ animation: scale-in 0.3s ease-out;
573
+ }
574
+ p {
575
+ font-size: 1.2rem;
576
+ margin: 1rem 0;
577
+ opacity: 0.9;
578
+ }
579
+ @keyframes scale-in {
580
+ from { transform: scale(0); }
581
+ to { transform: scale(1); }
582
+ }
583
+ </style>
584
+ </head>
585
+ <body>
586
+ <div class="container">
587
+ <div class="checkmark">\u2713</div>
588
+ <h1>Authentication Successful!</h1>
589
+ <p>You can now close this window and return to your terminal.</p>
590
+ </div>
591
+ </body>
592
+ </html>
593
+ `;
594
+ ERROR_HTML = (error) => `
595
+ <!DOCTYPE html>
596
+ <html>
597
+ <head>
598
+ <meta charset="UTF-8">
599
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
600
+ <title>Bragduck - Authentication Failed</title>
601
+ <style>
602
+ body {
603
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
604
+ display: flex;
605
+ align-items: center;
606
+ justify-content: center;
607
+ min-height: 100vh;
608
+ margin: 0;
609
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
610
+ color: white;
611
+ }
612
+ .container {
613
+ text-align: center;
614
+ padding: 2rem;
615
+ background: rgba(255, 255, 255, 0.1);
616
+ border-radius: 1rem;
617
+ backdrop-filter: blur(10px);
618
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
619
+ max-width: 500px;
620
+ }
621
+ h1 {
622
+ font-size: 2.5rem;
623
+ margin: 0 0 1rem 0;
624
+ }
625
+ .error-icon {
626
+ font-size: 4rem;
627
+ }
628
+ p {
629
+ font-size: 1.2rem;
630
+ margin: 1rem 0;
631
+ opacity: 0.9;
632
+ }
633
+ .error-details {
634
+ background: rgba(0, 0, 0, 0.2);
635
+ padding: 1rem;
636
+ border-radius: 0.5rem;
637
+ font-family: monospace;
638
+ font-size: 0.9rem;
639
+ margin-top: 1rem;
640
+ }
641
+ </style>
642
+ </head>
643
+ <body>
644
+ <div class="container">
645
+ <div class="error-icon">\u2717</div>
646
+ <h1>Authentication Failed</h1>
647
+ <p>Please return to your terminal and try again.</p>
648
+ <div class="error-details">${error}</div>
649
+ </div>
650
+ </body>
651
+ </html>
652
+ `;
653
+ }
654
+ });
655
+
656
+ // src/utils/browser.ts
657
+ import { exec } from "child_process";
658
+ import { promisify } from "util";
659
+ async function openBrowser(url) {
660
+ const platform = process.platform;
661
+ let command;
662
+ switch (platform) {
663
+ case "darwin":
664
+ command = `open "${url}"`;
665
+ break;
666
+ case "win32":
667
+ command = `start "" "${url}"`;
668
+ break;
669
+ default:
670
+ command = `xdg-open "${url}"`;
671
+ break;
672
+ }
673
+ try {
674
+ logger.debug(`Opening browser with command: ${command}`);
675
+ await execAsync(command);
676
+ logger.debug("Browser opened successfully");
677
+ } catch (error) {
678
+ logger.debug(`Failed to open browser: ${error}`);
679
+ throw new Error(
680
+ `Failed to open browser automatically. Please open this URL manually:
681
+ ${url}`
682
+ );
683
+ }
684
+ }
685
+ var execAsync;
686
+ var init_browser = __esm({
687
+ "src/utils/browser.ts"() {
688
+ "use strict";
689
+ init_esm_shims();
690
+ init_logger();
691
+ execAsync = promisify(exec);
692
+ }
693
+ });
694
+
695
+ // src/services/auth.service.ts
696
+ import { randomBytes as randomBytes2 } from "crypto";
697
+ import { ofetch } from "ofetch";
698
+ var AuthService, authService;
699
+ var init_auth_service = __esm({
700
+ "src/services/auth.service.ts"() {
701
+ "use strict";
702
+ init_esm_shims();
703
+ init_constants();
704
+ init_storage_service();
705
+ init_oauth_server();
706
+ init_browser();
707
+ init_errors();
708
+ init_logger();
709
+ AuthService = class {
710
+ apiBaseUrl;
711
+ constructor() {
712
+ this.apiBaseUrl = process.env.API_BASE_URL || storageService.getConfig("apiBaseUrl") || "https://api.bragduck.com";
713
+ }
714
+ /**
715
+ * Generate a random state string for CSRF protection
716
+ */
717
+ generateState() {
718
+ return randomBytes2(32).toString("hex");
719
+ }
720
+ /**
721
+ * Build the OAuth authorization URL
722
+ */
723
+ async buildAuthUrl(state, callbackUrl) {
724
+ const params = new URLSearchParams({
725
+ client_id: OAUTH_CONFIG.CLIENT_ID,
726
+ redirect_uri: callbackUrl,
727
+ state
728
+ });
729
+ return `${this.apiBaseUrl}${API_ENDPOINTS.AUTH.INITIATE}?${params.toString()}`;
730
+ }
731
+ /**
732
+ * Exchange authorization code for access token
733
+ */
734
+ async exchangeCodeForToken(code, callbackUrl) {
735
+ try {
736
+ logger.debug("Exchanging authorization code for token");
737
+ const response = await ofetch(
738
+ `${this.apiBaseUrl}${API_ENDPOINTS.AUTH.TOKEN}`,
739
+ {
740
+ method: "POST",
741
+ body: {
742
+ code,
743
+ redirect_uri: callbackUrl,
744
+ client_id: OAUTH_CONFIG.CLIENT_ID,
745
+ grant_type: "authorization_code"
746
+ },
747
+ headers: {
748
+ "Content-Type": "application/json"
749
+ }
750
+ }
751
+ );
752
+ logger.debug("Token exchange successful");
753
+ return response;
754
+ } catch (error) {
755
+ logger.debug(`Token exchange failed: ${error.message}`);
756
+ if (error.response) {
757
+ throw new AuthenticationError(
758
+ `Token exchange failed: ${error.response.statusText || "Unknown error"}`,
759
+ {
760
+ statusCode: error.response.status,
761
+ body: error.response._data
762
+ }
763
+ );
764
+ }
765
+ throw new NetworkError("Failed to connect to authentication server", {
766
+ originalError: error.message
767
+ });
768
+ }
769
+ }
770
+ /**
771
+ * Complete OAuth flow: start server, open browser, wait for callback, exchange token
772
+ */
773
+ async login() {
774
+ logger.debug("Starting OAuth login flow");
775
+ const state = this.generateState();
776
+ const callbackUrl = await getCallbackUrl();
777
+ storageService.setOAuthState({
778
+ state,
779
+ createdAt: Date.now()
780
+ });
781
+ logger.debug(`OAuth state: ${state}`);
782
+ logger.debug(`Callback URL: ${callbackUrl}`);
783
+ const authUrl = await this.buildAuthUrl(state, callbackUrl);
784
+ logger.debug(`Authorization URL: ${authUrl}`);
785
+ const serverPromise = startOAuthCallbackServer(state);
786
+ try {
787
+ await openBrowser(authUrl);
788
+ } catch (error) {
789
+ logger.warning("Could not open browser automatically");
790
+ logger.info(`Please open this URL in your browser:`);
791
+ logger.log(authUrl);
792
+ }
793
+ let callbackResult;
794
+ try {
795
+ callbackResult = await serverPromise;
796
+ } catch (error) {
797
+ storageService.deleteOAuthState();
798
+ throw error;
799
+ }
800
+ storageService.deleteOAuthState();
801
+ const tokenResponse = await this.exchangeCodeForToken(callbackResult.code, callbackUrl);
802
+ const expiresAt = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0;
803
+ const credentials = {
804
+ accessToken: tokenResponse.access_token,
805
+ refreshToken: tokenResponse.refresh_token,
806
+ expiresAt
807
+ };
808
+ await storageService.setCredentials(credentials);
809
+ if (tokenResponse.user) {
810
+ storageService.setUserInfo(tokenResponse.user);
811
+ }
812
+ logger.debug("Login successful");
813
+ return tokenResponse.user || { id: "unknown", email: "unknown", name: "Unknown User" };
814
+ }
815
+ /**
816
+ * Logout: clear all stored credentials and user info
817
+ */
818
+ async logout() {
819
+ logger.debug("Logging out");
820
+ await storageService.deleteCredentials();
821
+ storageService.deleteUserInfo();
822
+ storageService.deleteOAuthState();
823
+ logger.debug("Logout complete");
824
+ }
825
+ /**
826
+ * Check if user is authenticated
827
+ */
828
+ async isAuthenticated() {
829
+ return storageService.isAuthenticated();
830
+ }
831
+ /**
832
+ * Get current access token
833
+ */
834
+ async getAccessToken() {
835
+ const credentials = await storageService.getCredentials();
836
+ return credentials?.accessToken || null;
837
+ }
838
+ /**
839
+ * Get current user info
840
+ */
841
+ getUserInfo() {
842
+ return storageService.getUserInfo();
843
+ }
844
+ /**
845
+ * Refresh access token using refresh token
846
+ */
847
+ async refreshToken() {
848
+ logger.debug("Refreshing access token");
849
+ const credentials = await storageService.getCredentials();
850
+ if (!credentials?.refreshToken) {
851
+ throw new AuthenticationError("No refresh token available");
852
+ }
853
+ try {
854
+ const response = await ofetch(
855
+ `${this.apiBaseUrl}${API_ENDPOINTS.AUTH.TOKEN}`,
856
+ {
857
+ method: "POST",
858
+ body: {
859
+ refresh_token: credentials.refreshToken,
860
+ client_id: OAUTH_CONFIG.CLIENT_ID,
861
+ grant_type: "refresh_token"
862
+ },
863
+ headers: {
864
+ "Content-Type": "application/json"
865
+ }
866
+ }
867
+ );
868
+ const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : void 0;
869
+ const newCredentials = {
870
+ accessToken: response.access_token,
871
+ refreshToken: response.refresh_token || credentials.refreshToken,
872
+ expiresAt
873
+ };
874
+ await storageService.setCredentials(newCredentials);
875
+ logger.debug("Token refresh successful");
876
+ } catch (error) {
877
+ logger.debug(`Token refresh failed: ${error.message}`);
878
+ await this.logout();
879
+ throw new AuthenticationError("Token refresh failed. Please log in again.");
880
+ }
881
+ }
882
+ };
883
+ authService = new AuthService();
884
+ }
885
+ });
886
+
887
+ // src/utils/version.ts
888
+ var version_exports = {};
889
+ __export(version_exports, {
890
+ checkForUpdates: () => checkForUpdates,
891
+ compareVersions: () => compareVersions,
892
+ formatVersion: () => formatVersion,
893
+ getCurrentVersion: () => getCurrentVersion,
894
+ version: () => version
895
+ });
896
+ import { readFileSync as readFileSync2 } from "fs";
897
+ import { fileURLToPath as fileURLToPath3 } from "url";
898
+ import { dirname as dirname2, join as join4 } from "path";
899
+ import chalk4 from "chalk";
900
+ import boxen3 from "boxen";
901
+ function getCurrentVersion() {
902
+ try {
903
+ const packageJsonPath2 = join4(__dirname3, "../../package.json");
904
+ const packageJson2 = JSON.parse(readFileSync2(packageJsonPath2, "utf-8"));
905
+ return packageJson2.version;
906
+ } catch (error) {
907
+ logger.debug("Failed to read package.json version");
908
+ return "1.0.0";
909
+ }
910
+ }
911
+ function compareVersions(v1, v2) {
912
+ const parts1 = v1.split(".").map((p) => parseInt(p, 10));
913
+ const parts2 = v2.split(".").map((p) => parseInt(p, 10));
914
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
915
+ const part1 = parts1[i] || 0;
916
+ const part2 = parts2[i] || 0;
917
+ if (part1 > part2) return 1;
918
+ if (part1 < part2) return -1;
919
+ }
920
+ return 0;
921
+ }
922
+ async function checkForUpdates(options = {}) {
923
+ const { silent = false, force = false } = options;
924
+ try {
925
+ if (!force) {
926
+ const autoVersionCheck = storageService.getConfig("autoVersionCheck");
927
+ if (!autoVersionCheck) {
928
+ logger.debug("Version check disabled in config");
929
+ return {
930
+ updateAvailable: false,
931
+ latestVersion: version,
932
+ currentVersion: version
933
+ };
934
+ }
935
+ }
936
+ logger.debug("Checking for CLI updates...");
937
+ const response = await apiService.checkVersion();
938
+ const latestVersion = response.latest_version;
939
+ const currentVersion = version;
940
+ logger.debug(`Current version: ${currentVersion}, Latest version: ${latestVersion}`);
941
+ const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
942
+ if (!silent && updateAvailable) {
943
+ displayUpdateNotification(currentVersion, latestVersion, response.critical_update);
944
+ }
945
+ return {
946
+ updateAvailable,
947
+ latestVersion,
948
+ currentVersion
949
+ };
950
+ } catch (error) {
951
+ logger.debug(`Version check failed: ${error.message}`);
952
+ return {
953
+ updateAvailable: false,
954
+ latestVersion: version,
955
+ currentVersion: version
956
+ };
957
+ }
958
+ }
959
+ function displayUpdateNotification(currentVersion, latestVersion, critical = false) {
960
+ const urgency = critical ? chalk4.red.bold("CRITICAL UPDATE") : chalk4.yellow.bold("Update Available");
961
+ const message = `${urgency}
962
+
963
+ Current version: ${chalk4.dim(currentVersion)}
964
+ Latest version: ${chalk4.green(latestVersion)}
965
+
966
+ Update with: ${chalk4.cyan("npm install -g @bragduck/cli@latest")}`;
967
+ console.log("");
968
+ console.log(
969
+ boxen3(message, {
970
+ padding: 1,
971
+ margin: { top: 0, right: 1, bottom: 0, left: 1 },
972
+ borderStyle: "round",
973
+ borderColor: critical ? "red" : "yellow"
974
+ })
975
+ );
976
+ console.log("");
977
+ if (critical) {
978
+ logger.warning("This is a critical update. Please update as soon as possible.");
979
+ console.log("");
980
+ }
981
+ }
982
+ function formatVersion(includePrefix = true) {
983
+ const prefix = includePrefix ? "v" : "";
984
+ return `${prefix}${version}`;
985
+ }
986
+ var __filename3, __dirname3, version;
987
+ var init_version = __esm({
988
+ "src/utils/version.ts"() {
989
+ "use strict";
990
+ init_esm_shims();
991
+ init_api_service();
992
+ init_storage_service();
993
+ init_logger();
994
+ __filename3 = fileURLToPath3(import.meta.url);
995
+ __dirname3 = dirname2(__filename3);
996
+ version = getCurrentVersion();
997
+ }
998
+ });
999
+
1000
+ // src/services/api.service.ts
1001
+ import { ofetch as ofetch2 } from "ofetch";
1002
+ var ApiService, apiService;
1003
+ var init_api_service = __esm({
1004
+ "src/services/api.service.ts"() {
1005
+ "use strict";
1006
+ init_esm_shims();
1007
+ init_storage_service();
1008
+ init_auth_service();
1009
+ init_constants();
1010
+ init_errors();
1011
+ init_logger();
1012
+ ApiService = class {
1013
+ baseURL;
1014
+ client;
1015
+ constructor() {
1016
+ this.baseURL = process.env.API_BASE_URL || storageService.getConfig("apiBaseUrl");
1017
+ this.client = ofetch2.create({
1018
+ baseURL: this.baseURL,
1019
+ // Request interceptor
1020
+ onRequest: async ({ options }) => {
1021
+ logger.debug(`API Request: ${options.method} ${options.baseURL}${options.url}`);
1022
+ const token = await authService.getAccessToken();
1023
+ if (token) {
1024
+ options.headers = {
1025
+ ...options.headers,
1026
+ Authorization: `Bearer ${token}`
1027
+ };
1028
+ }
1029
+ if (options.method && ["POST", "PUT", "PATCH"].includes(options.method)) {
1030
+ options.headers = {
1031
+ ...options.headers,
1032
+ "Content-Type": "application/json"
1033
+ };
1034
+ }
1035
+ },
1036
+ // Response interceptor for success
1037
+ onResponse: ({ response }) => {
1038
+ logger.debug(`API Response: ${response.status} ${response.statusText}`);
1039
+ },
1040
+ // Response interceptor for errors
1041
+ onResponseError: async ({ response, options }) => {
1042
+ const status = response.status;
1043
+ const url = `${options.baseURL}${options.url}`;
1044
+ logger.debug(`API Error: ${status} ${response.statusText} - ${url}`);
1045
+ if (status === HTTP_STATUS.UNAUTHORIZED) {
1046
+ logger.debug("Token expired, attempting refresh");
1047
+ try {
1048
+ await authService.refreshToken();
1049
+ logger.debug("Token refreshed, retrying request");
1050
+ throw new Error("RETRY_WITH_NEW_TOKEN");
1051
+ } catch (error) {
1052
+ if (error.message === "RETRY_WITH_NEW_TOKEN") {
1053
+ throw error;
1054
+ }
1055
+ throw new TokenExpiredError('Your session has expired. Please run "bragduck init" to login again.');
1056
+ }
1057
+ }
1058
+ let errorMessage = "An unexpected error occurred";
1059
+ let errorDetails;
1060
+ try {
1061
+ const errorData = response._data;
1062
+ if (errorData && errorData.message) {
1063
+ errorMessage = errorData.message;
1064
+ errorDetails = errorData.details;
1065
+ }
1066
+ } catch {
1067
+ errorMessage = response.statusText || errorMessage;
1068
+ }
1069
+ throw new ApiError(errorMessage, status, errorDetails);
1070
+ },
1071
+ // Retry configuration
1072
+ retry: 2,
1073
+ retryDelay: 1e3,
1074
+ retryStatusCodes: [408, 409, 425, 429, 500, 502, 503, 504],
1075
+ // Timeout
1076
+ timeout: 3e4
1077
+ // 30 seconds
1078
+ });
1079
+ }
1080
+ /**
1081
+ * Make API request with retry for token refresh
1082
+ */
1083
+ async makeRequest(url, options = {}) {
1084
+ try {
1085
+ return await this.client(url, options);
1086
+ } catch (error) {
1087
+ if (error.message === "RETRY_WITH_NEW_TOKEN") {
1088
+ logger.debug("Retrying request with refreshed token");
1089
+ return await this.client(url, options);
1090
+ }
1091
+ if (error.name === "FetchError" || error.code === "ECONNREFUSED") {
1092
+ throw new NetworkError("Failed to connect to Bragduck API", {
1093
+ originalError: error.message,
1094
+ baseURL: this.baseURL
1095
+ });
1096
+ }
1097
+ throw error;
1098
+ }
1099
+ }
1100
+ /**
1101
+ * Refine commits using AI
1102
+ */
1103
+ async refineCommits(request) {
1104
+ logger.debug(`Refining ${request.commits.length} commits`);
1105
+ try {
1106
+ const response = await this.makeRequest(
1107
+ API_ENDPOINTS.COMMITS.REFINE,
1108
+ {
1109
+ method: "POST",
1110
+ body: request
1111
+ }
1112
+ );
1113
+ logger.debug(`Successfully refined ${response.refined_commits.length} commits`);
1114
+ return response;
1115
+ } catch (error) {
1116
+ logger.debug("Failed to refine commits");
1117
+ throw error;
1118
+ }
1119
+ }
1120
+ /**
1121
+ * Create brags from refined commits
1122
+ */
1123
+ async createBrags(request) {
1124
+ logger.debug(`Creating ${request.brags.length} brags`);
1125
+ try {
1126
+ const response = await this.makeRequest(
1127
+ API_ENDPOINTS.BRAGS.CREATE,
1128
+ {
1129
+ method: "POST",
1130
+ body: request
1131
+ }
1132
+ );
1133
+ logger.debug(`Successfully created ${response.created} brags`);
1134
+ return response;
1135
+ } catch (error) {
1136
+ logger.debug("Failed to create brags");
1137
+ throw error;
1138
+ }
1139
+ }
1140
+ /**
1141
+ * List existing brags
1142
+ */
1143
+ async listBrags(params = {}) {
1144
+ const { limit = 50, offset = 0, tags, search } = params;
1145
+ logger.debug(`Listing brags: limit=${limit}, offset=${offset}`);
1146
+ try {
1147
+ const queryParams = new URLSearchParams({
1148
+ limit: limit.toString(),
1149
+ offset: offset.toString()
1150
+ });
1151
+ if (search) {
1152
+ queryParams.append("search", search);
1153
+ }
1154
+ if (tags && tags.length > 0) {
1155
+ tags.forEach((tag) => queryParams.append("tags[]", tag));
1156
+ }
1157
+ const url = `${API_ENDPOINTS.BRAGS.LIST}?${queryParams.toString()}`;
1158
+ const response = await this.makeRequest(url, {
1159
+ method: "GET"
1160
+ });
1161
+ logger.debug(`Successfully fetched ${response.brags.length} brags (total: ${response.total})`);
1162
+ return response;
1163
+ } catch (error) {
1164
+ logger.debug("Failed to list brags");
1165
+ throw error;
1166
+ }
1167
+ }
1168
+ /**
1169
+ * Check for CLI updates
1170
+ */
1171
+ async checkVersion() {
1172
+ logger.debug("Checking for CLI updates");
1173
+ try {
1174
+ const response = await this.makeRequest(
1175
+ API_ENDPOINTS.VERSION,
1176
+ {
1177
+ method: "GET"
1178
+ }
1179
+ );
1180
+ logger.debug(`Latest version: ${response.latest_version}`);
1181
+ return response;
1182
+ } catch (error) {
1183
+ logger.debug("Failed to check version");
1184
+ const { version: version2 } = await Promise.resolve().then(() => (init_version(), version_exports));
1185
+ return {
1186
+ latest_version: version2,
1187
+ critical_update: false
1188
+ };
1189
+ }
1190
+ }
1191
+ /**
1192
+ * Test API connectivity
1193
+ */
1194
+ async testConnection() {
1195
+ try {
1196
+ await this.checkVersion();
1197
+ return true;
1198
+ } catch (error) {
1199
+ return false;
1200
+ }
1201
+ }
1202
+ /**
1203
+ * Update base URL
1204
+ */
1205
+ setBaseURL(url) {
1206
+ this.baseURL = url;
1207
+ storageService.setConfig("apiBaseUrl", url);
1208
+ this.client = ofetch2.create({
1209
+ baseURL: url
1210
+ });
1211
+ }
1212
+ /**
1213
+ * Get current base URL
1214
+ */
1215
+ getBaseURL() {
1216
+ return this.baseURL;
1217
+ }
1218
+ };
1219
+ apiService = new ApiService();
1220
+ }
1221
+ });
1222
+
1223
+ // src/cli.ts
1224
+ init_esm_shims();
1225
+ import { Command } from "commander";
1226
+ import { readFileSync as readFileSync3 } from "fs";
1227
+ import { fileURLToPath as fileURLToPath4 } from "url";
1228
+ import { dirname as dirname3, join as join5 } from "path";
1229
+
1230
+ // src/commands/init.ts
1231
+ init_esm_shims();
1232
+ init_auth_service();
1233
+ init_logger();
1234
+ import ora from "ora";
1235
+ import boxen from "boxen";
1236
+ import chalk2 from "chalk";
1237
+ async function initCommand() {
1238
+ logger.log("");
1239
+ logger.info("Starting authentication flow...");
1240
+ logger.log("");
1241
+ const isAuthenticated = await authService.isAuthenticated();
1242
+ if (isAuthenticated) {
1243
+ const userInfo = authService.getUserInfo();
1244
+ if (userInfo) {
1245
+ logger.log(
1246
+ boxen(
1247
+ `${chalk2.yellow("Already authenticated!")}
1248
+
1249
+ ${chalk2.gray("User:")} ${userInfo.name}
1250
+ ${chalk2.gray("Email:")} ${userInfo.email}
1251
+
1252
+ ${chalk2.dim("Run")} ${chalk2.cyan("bragduck logout")} ${chalk2.dim("to sign out")}`,
1253
+ {
1254
+ padding: 1,
1255
+ margin: 1,
1256
+ borderStyle: "round",
1257
+ borderColor: "yellow"
1258
+ }
1259
+ )
1260
+ );
1261
+ logger.log("");
1262
+ setTimeout(() => {
1263
+ process.exit(0);
1264
+ }, 100);
1265
+ return;
1266
+ }
1267
+ }
1268
+ const spinner = ora("Opening browser for authentication...").start();
1269
+ try {
1270
+ spinner.text = "Waiting for authentication...";
1271
+ const userInfo = await authService.login();
1272
+ spinner.succeed("Authentication successful!");
1273
+ logger.log("");
1274
+ logger.log(
1275
+ boxen(
1276
+ `${chalk2.green.bold("\u2713 Successfully authenticated!")}
1277
+
1278
+ ${chalk2.gray("Welcome,")} ${chalk2.cyan(userInfo.name)}
1279
+ ${chalk2.gray("Email:")} ${userInfo.email}
1280
+
1281
+ ${chalk2.dim("You can now use")} ${chalk2.cyan("bragduck scan")} ${chalk2.dim("to create brags!")}`,
1282
+ {
1283
+ padding: 1,
1284
+ margin: 1,
1285
+ borderStyle: "round",
1286
+ borderColor: "green"
1287
+ }
1288
+ )
1289
+ );
1290
+ logger.log("");
1291
+ setTimeout(() => {
1292
+ process.exit(0);
1293
+ }, 100);
1294
+ return;
1295
+ } catch (error) {
1296
+ spinner.fail("Authentication failed");
1297
+ logger.log("");
1298
+ const err = error;
1299
+ logger.log(
1300
+ boxen(
1301
+ `${chalk2.red.bold("\u2717 Authentication Failed")}
1302
+
1303
+ ${err.message}
1304
+
1305
+ ${chalk2.dim("Hint:")} ${getErrorHint(err)}`,
1306
+ {
1307
+ padding: 1,
1308
+ margin: 1,
1309
+ borderStyle: "round",
1310
+ borderColor: "red"
1311
+ }
1312
+ )
1313
+ );
1314
+ process.exit(1);
1315
+ }
1316
+ }
1317
+ function getErrorHint(error) {
1318
+ if (error.name === "OAuthError") {
1319
+ if (error.message.includes("timeout")) {
1320
+ return "Try again and complete the authentication within 2 minutes";
1321
+ }
1322
+ if (error.message.includes("CSRF")) {
1323
+ return "This might be a security issue. Try running the command again";
1324
+ }
1325
+ return "Check your internet connection and try again";
1326
+ }
1327
+ if (error.name === "NetworkError") {
1328
+ return "Check your internet connection and firewall settings";
1329
+ }
1330
+ if (error.name === "AuthenticationError") {
1331
+ return "Verify your credentials and try again";
1332
+ }
1333
+ return "Try running the command again or check the logs with DEBUG=* bragduck init";
1334
+ }
1335
+
1336
+ // src/commands/logout.ts
1337
+ init_esm_shims();
1338
+ init_auth_service();
1339
+ init_logger();
1340
+ import { confirm } from "@inquirer/prompts";
1341
+ import boxen2 from "boxen";
1342
+ import chalk3 from "chalk";
1343
+ import ora2 from "ora";
1344
+ async function logoutCommand() {
1345
+ const isAuthenticated = await authService.isAuthenticated();
1346
+ if (!isAuthenticated) {
1347
+ logger.log(
1348
+ boxen2(`${chalk3.yellow("Not currently authenticated")}
1349
+
1350
+ ${chalk3.dim("Nothing to logout from")}`, {
1351
+ padding: 1,
1352
+ margin: 1,
1353
+ borderStyle: "round",
1354
+ borderColor: "yellow"
1355
+ })
1356
+ );
1357
+ return;
1358
+ }
1359
+ const userInfo = authService.getUserInfo();
1360
+ const userName = userInfo?.name || "Unknown User";
1361
+ logger.log("");
1362
+ const shouldLogout = await confirm({
1363
+ message: `Are you sure you want to logout? (${chalk3.cyan(userName)})`,
1364
+ default: false
1365
+ });
1366
+ if (!shouldLogout) {
1367
+ logger.info("Logout cancelled");
1368
+ return;
1369
+ }
1370
+ const spinner = ora2("Logging out...").start();
1371
+ try {
1372
+ await authService.logout();
1373
+ spinner.succeed("Logged out successfully");
1374
+ logger.log("");
1375
+ logger.log(
1376
+ boxen2(
1377
+ `${chalk3.green.bold("\u2713 Logged out successfully")}
1378
+
1379
+ ${chalk3.dim("Your credentials have been cleared")}
1380
+
1381
+ ${chalk3.dim("Run")} ${chalk3.cyan("bragduck init")} ${chalk3.dim("to login again")}`,
1382
+ {
1383
+ padding: 1,
1384
+ margin: 1,
1385
+ borderStyle: "round",
1386
+ borderColor: "green"
1387
+ }
1388
+ )
1389
+ );
1390
+ } catch (error) {
1391
+ spinner.fail("Logout failed");
1392
+ logger.error("Failed to logout. Please try again.");
1393
+ process.exit(1);
1394
+ }
1395
+ }
1396
+
1397
+ // src/commands/scan.ts
1398
+ init_esm_shims();
1399
+ import boxen4 from "boxen";
1400
+ import chalk7 from "chalk";
1401
+
1402
+ // src/services/git.service.ts
1403
+ init_esm_shims();
1404
+ import simpleGit from "simple-git";
1405
+
1406
+ // src/utils/validators.ts
1407
+ init_esm_shims();
1408
+ init_errors();
1409
+ import { existsSync as existsSync2 } from "fs";
1410
+ import { join as join3 } from "path";
1411
+ function validateGitRepository(path2) {
1412
+ const gitDir = join3(path2, ".git");
1413
+ if (!existsSync2(gitDir)) {
1414
+ throw new GitError(
1415
+ "Not a git repository. Please run this command from within a git repository.",
1416
+ {
1417
+ path: path2,
1418
+ hint: 'Run "git init" to initialize a git repository, or navigate to an existing one'
1419
+ }
1420
+ );
1421
+ }
1422
+ }
1423
+
1424
+ // src/services/git.service.ts
1425
+ init_errors();
1426
+ init_logger();
1427
+ var GitService = class {
1428
+ git;
1429
+ repoPath;
1430
+ constructor(repoPath = process.cwd()) {
1431
+ this.repoPath = repoPath;
1432
+ this.git = simpleGit(repoPath);
1433
+ }
1434
+ /**
1435
+ * Validate that the current directory is a git repository
1436
+ */
1437
+ async validateRepository() {
1438
+ try {
1439
+ validateGitRepository(this.repoPath);
1440
+ const isRepo = await this.git.checkIsRepo();
1441
+ if (!isRepo) {
1442
+ throw new GitError("Not a valid git repository", {
1443
+ path: this.repoPath
1444
+ });
1445
+ }
1446
+ } catch (error) {
1447
+ if (error instanceof GitError) {
1448
+ throw error;
1449
+ }
1450
+ throw new GitError("Failed to validate git repository", {
1451
+ originalError: error.message,
1452
+ path: this.repoPath
1453
+ });
1454
+ }
1455
+ }
1456
+ /**
1457
+ * Get repository information
1458
+ */
1459
+ async getRepositoryInfo() {
1460
+ try {
1461
+ await this.validateRepository();
1462
+ const status = await this.git.status();
1463
+ const remotes = await this.git.getRemotes(true);
1464
+ const primaryRemote = remotes.find((r) => r.name === "origin");
1465
+ return {
1466
+ path: this.repoPath,
1467
+ currentBranch: status.current || "unknown",
1468
+ remoteUrl: primaryRemote?.refs?.fetch || primaryRemote?.refs?.push,
1469
+ isClean: status.isClean()
1470
+ };
1471
+ } catch (error) {
1472
+ if (error instanceof GitError) {
1473
+ throw error;
1474
+ }
1475
+ throw new GitError("Failed to get repository information", {
1476
+ originalError: error.message
1477
+ });
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Fetch recent commits
1482
+ */
1483
+ async getRecentCommits(options = {}) {
1484
+ const { days = 30, limit, author, branch } = options;
1485
+ try {
1486
+ await this.validateRepository();
1487
+ logger.debug(`Fetching commits from last ${days} days`);
1488
+ const since = /* @__PURE__ */ new Date();
1489
+ since.setDate(since.getDate() - days);
1490
+ const logOptions = {
1491
+ "--since": since.toISOString(),
1492
+ "--no-merges": null
1493
+ };
1494
+ if (limit) {
1495
+ logOptions.maxCount = limit;
1496
+ }
1497
+ if (author) {
1498
+ logOptions["--author"] = author;
1499
+ }
1500
+ const log = await this.git.log(logOptions);
1501
+ logger.debug(`Found ${log.all.length} commits`);
1502
+ const commits = log.all.map((commit) => ({
1503
+ sha: commit.hash,
1504
+ message: commit.message,
1505
+ author: commit.author_name,
1506
+ authorEmail: commit.author_email,
1507
+ date: commit.date
1508
+ }));
1509
+ return commits;
1510
+ } catch (error) {
1511
+ if (error instanceof GitError) {
1512
+ throw error;
1513
+ }
1514
+ throw new GitError("Failed to fetch commits", {
1515
+ originalError: error.message,
1516
+ days
1517
+ });
1518
+ }
1519
+ }
1520
+ /**
1521
+ * Get diff statistics for a specific commit
1522
+ */
1523
+ async getCommitStats(sha) {
1524
+ try {
1525
+ logger.debug(`Getting stats for commit ${sha}`);
1526
+ const diffSummary = await this.git.diffSummary([`${sha}^`, sha]);
1527
+ const stats = {
1528
+ filesChanged: diffSummary.files.length,
1529
+ insertions: diffSummary.insertions,
1530
+ deletions: diffSummary.deletions
1531
+ };
1532
+ logger.debug(`Commit ${sha}: ${stats.filesChanged} files, +${stats.insertions} -${stats.deletions}`);
1533
+ return stats;
1534
+ } catch (error) {
1535
+ if (error.message && error.message.includes("unknown revision")) {
1536
+ logger.debug(`Commit ${sha} has no parent (first commit)`);
1537
+ try {
1538
+ const diffSummary = await this.git.diffSummary([sha]);
1539
+ return {
1540
+ filesChanged: diffSummary.files.length,
1541
+ insertions: diffSummary.insertions,
1542
+ deletions: diffSummary.deletions
1543
+ };
1544
+ } catch {
1545
+ return {
1546
+ filesChanged: 0,
1547
+ insertions: 0,
1548
+ deletions: 0
1549
+ };
1550
+ }
1551
+ }
1552
+ throw new GitError("Failed to get commit statistics", {
1553
+ originalError: error.message,
1554
+ sha
1555
+ });
1556
+ }
1557
+ }
1558
+ /**
1559
+ * Get commits with their diff statistics
1560
+ */
1561
+ async getCommitsWithStats(options = {}) {
1562
+ const commits = await this.getRecentCommits(options);
1563
+ logger.debug(`Fetching diff stats for ${commits.length} commits`);
1564
+ const commitsWithStats = await Promise.all(
1565
+ commits.map(async (commit) => {
1566
+ try {
1567
+ const stats = await this.getCommitStats(commit.sha);
1568
+ return {
1569
+ ...commit,
1570
+ diffStats: stats
1571
+ };
1572
+ } catch (error) {
1573
+ logger.debug(`Failed to get stats for commit ${commit.sha}, continuing without stats`);
1574
+ return commit;
1575
+ }
1576
+ })
1577
+ );
1578
+ return commitsWithStats;
1579
+ }
1580
+ /**
1581
+ * Get the current git user email
1582
+ */
1583
+ async getCurrentUserEmail() {
1584
+ try {
1585
+ const email = await this.git.raw(["config", "user.email"]);
1586
+ return email.trim() || null;
1587
+ } catch (error) {
1588
+ logger.debug("Failed to get git user email");
1589
+ return null;
1590
+ }
1591
+ }
1592
+ /**
1593
+ * Get the current git user name
1594
+ */
1595
+ async getCurrentUserName() {
1596
+ try {
1597
+ const name = await this.git.raw(["config", "user.name"]);
1598
+ return name.trim() || null;
1599
+ } catch (error) {
1600
+ logger.debug("Failed to get git user name");
1601
+ return null;
1602
+ }
1603
+ }
1604
+ /**
1605
+ * Filter commits by current user
1606
+ */
1607
+ async getCommitsByCurrentUser(options = {}) {
1608
+ const userEmail = await this.getCurrentUserEmail();
1609
+ if (!userEmail) {
1610
+ logger.warning("Could not determine git user email, returning all commits");
1611
+ return this.getCommitsWithStats(options);
1612
+ }
1613
+ logger.debug(`Filtering commits by user: ${userEmail}`);
1614
+ return this.getCommitsWithStats({
1615
+ ...options,
1616
+ author: userEmail
1617
+ });
1618
+ }
1619
+ };
1620
+ var gitService = new GitService();
1621
+
1622
+ // src/commands/scan.ts
1623
+ init_api_service();
1624
+ init_auth_service();
1625
+ init_storage_service();
1626
+ init_logger();
1627
+
1628
+ // src/ui/prompts.ts
1629
+ init_esm_shims();
1630
+ import { checkbox, confirm as confirm2, input, select } from "@inquirer/prompts";
1631
+
1632
+ // src/ui/formatters.ts
1633
+ init_esm_shims();
1634
+ import chalk5 from "chalk";
1635
+ import Table from "cli-table3";
1636
+ function formatCommitChoice(commit) {
1637
+ const sha = chalk5.yellow(commit.sha.substring(0, 7));
1638
+ const message = commit.message.split("\n")[0];
1639
+ const author = chalk5.gray(`by ${commit.author}`);
1640
+ const date = chalk5.gray(new Date(commit.date).toLocaleDateString());
1641
+ let stats = "";
1642
+ if (commit.diffStats) {
1643
+ const { filesChanged, insertions, deletions } = commit.diffStats;
1644
+ stats = chalk5.gray(
1645
+ ` [${filesChanged} files, ${chalk5.green(`+${insertions}`)} ${chalk5.red(`-${deletions}`)}]`
1646
+ );
1647
+ }
1648
+ return `${sha} ${message}${stats}
1649
+ ${author} \u2022 ${date}`;
1650
+ }
1651
+ function formatRefinedCommitsTable(commits) {
1652
+ const table = new Table({
1653
+ head: [
1654
+ chalk5.cyan("SHA"),
1655
+ chalk5.cyan("Original"),
1656
+ chalk5.cyan("Refined Title"),
1657
+ chalk5.cyan("Description"),
1658
+ chalk5.cyan("Tags")
1659
+ ],
1660
+ colWidths: [10, 30, 30, 40, 20],
1661
+ wordWrap: true,
1662
+ style: {
1663
+ head: [],
1664
+ border: ["gray"]
1665
+ }
1666
+ });
1667
+ commits.forEach((commit) => {
1668
+ const sha = commit.sha.substring(0, 7);
1669
+ const original = commit.original_message.split("\n")[0] || "";
1670
+ const title = commit.refined_title;
1671
+ const description = commit.refined_description.substring(0, 100) + "...";
1672
+ const tags = (commit.suggested_tags || []).join(", ") || "none";
1673
+ table.push([
1674
+ chalk5.yellow(sha),
1675
+ chalk5.gray(original),
1676
+ chalk5.white(title),
1677
+ chalk5.dim(description),
1678
+ chalk5.blue(tags)
1679
+ ]);
1680
+ });
1681
+ return table.toString();
1682
+ }
1683
+ function formatCommitStats(commits) {
1684
+ const totalFiles = commits.reduce((sum, c) => sum + (c.diffStats?.filesChanged || 0), 0);
1685
+ const totalInsertions = commits.reduce((sum, c) => sum + (c.diffStats?.insertions || 0), 0);
1686
+ const totalDeletions = commits.reduce((sum, c) => sum + (c.diffStats?.deletions || 0), 0);
1687
+ return chalk5.gray("Total changes: ") + chalk5.white(`${totalFiles} files`) + chalk5.gray(", ") + chalk5.green(`+${totalInsertions}`) + chalk5.gray(" ") + chalk5.red(`-${totalDeletions}`);
1688
+ }
1689
+ function formatSuccessMessage(count) {
1690
+ const emoji = "\u{1F389}";
1691
+ const title = chalk5.green.bold(`${emoji} Successfully created ${count} brag${count > 1 ? "s" : ""}!`);
1692
+ const message = chalk5.white("\nYour achievements are now saved and ready to showcase.");
1693
+ const hint = chalk5.dim("\nRun ") + chalk5.cyan("bragduck list") + chalk5.dim(" to see all your brags");
1694
+ return `${title}${message}${hint}`;
1695
+ }
1696
+ function formatErrorMessage(message, hint) {
1697
+ const title = chalk5.red.bold("\u2717 Error");
1698
+ const error = chalk5.white(message);
1699
+ const hintText = hint ? chalk5.dim("\n\nHint: ") + chalk5.cyan(hint) : "";
1700
+ return `${title}
1701
+
1702
+ ${error}${hintText}`;
1703
+ }
1704
+
1705
+ // src/ui/prompts.ts
1706
+ async function promptSelectCommits(commits) {
1707
+ if (commits.length === 0) {
1708
+ return [];
1709
+ }
1710
+ const choices = commits.map((commit) => ({
1711
+ name: formatCommitChoice(commit),
1712
+ value: commit.sha,
1713
+ checked: false
1714
+ }));
1715
+ const selected = await checkbox({
1716
+ message: "Select commits to brag about (use space to select, enter to confirm):",
1717
+ choices,
1718
+ pageSize: 10,
1719
+ loop: false
1720
+ });
1721
+ return selected;
1722
+ }
1723
+ async function promptConfirm(message, defaultValue = true) {
1724
+ return await confirm2({
1725
+ message,
1726
+ default: defaultValue
1727
+ });
1728
+ }
1729
+ async function promptDaysToScan(defaultDays = 30) {
1730
+ const choices = [
1731
+ { name: "7 days", value: "7", description: "Last week" },
1732
+ { name: "30 days (Recommended)", value: "30", description: "Last month" },
1733
+ { name: "60 days", value: "60", description: "Last 2 months" },
1734
+ { name: "90 days", value: "90", description: "Last 3 months" },
1735
+ { name: "Custom", value: "custom", description: "Enter custom number of days" }
1736
+ ];
1737
+ const selected = await select({
1738
+ message: "How many days back should we scan for commits?",
1739
+ choices,
1740
+ default: "30"
1741
+ });
1742
+ if (selected === "custom") {
1743
+ const customDays = await input({
1744
+ message: "Enter number of days:",
1745
+ default: defaultDays.toString(),
1746
+ validate: (value) => {
1747
+ const num = parseInt(value, 10);
1748
+ if (isNaN(num) || num <= 0) {
1749
+ return "Please enter a valid positive number";
1750
+ }
1751
+ return true;
1752
+ }
1753
+ });
1754
+ return parseInt(customDays, 10);
1755
+ }
1756
+ return parseInt(selected, 10);
1757
+ }
1758
+
1759
+ // src/ui/spinners.ts
1760
+ init_esm_shims();
1761
+ import ora3 from "ora";
1762
+ import chalk6 from "chalk";
1763
+ function createSpinner(text) {
1764
+ return ora3({
1765
+ text,
1766
+ color: "cyan",
1767
+ spinner: "dots"
1768
+ });
1769
+ }
1770
+ function fetchingCommitsSpinner(days) {
1771
+ return createSpinner(`Fetching commits from the last ${days} days...`);
1772
+ }
1773
+ function refiningCommitsSpinner(count) {
1774
+ return createSpinner(`Refining ${count} commit${count > 1 ? "s" : ""} with AI...`);
1775
+ }
1776
+ function creatingBragsSpinner(count) {
1777
+ return createSpinner(`Creating ${count} brag${count > 1 ? "s" : ""}...`);
1778
+ }
1779
+ function fetchingBragsSpinner() {
1780
+ return createSpinner("Fetching your brags...");
1781
+ }
1782
+ function succeedSpinner(spinner, text) {
1783
+ if (text) {
1784
+ spinner.succeed(chalk6.green(text));
1785
+ } else {
1786
+ spinner.succeed();
1787
+ }
1788
+ }
1789
+ function failSpinner(spinner, text) {
1790
+ if (text) {
1791
+ spinner.fail(chalk6.red(text));
1792
+ } else {
1793
+ spinner.fail();
1794
+ }
1795
+ }
1796
+
1797
+ // src/commands/scan.ts
1798
+ async function scanCommand(options = {}) {
1799
+ logger.log("");
1800
+ try {
1801
+ const isAuthenticated = await authService.isAuthenticated();
1802
+ if (!isAuthenticated) {
1803
+ logger.log(
1804
+ boxen4(
1805
+ `${chalk7.yellow.bold("\u26A0 Not authenticated")}
1806
+
1807
+ Please run ${chalk7.cyan("bragduck init")} to login first.`,
1808
+ {
1809
+ padding: 1,
1810
+ margin: 1,
1811
+ borderStyle: "round",
1812
+ borderColor: "yellow"
1813
+ }
1814
+ )
1815
+ );
1816
+ process.exit(1);
1817
+ }
1818
+ await gitService.validateRepository();
1819
+ const repoInfo = await gitService.getRepositoryInfo();
1820
+ logger.info(`Repository: ${chalk7.cyan(repoInfo.currentBranch)} branch`);
1821
+ logger.log("");
1822
+ let days = options.days;
1823
+ if (!days) {
1824
+ const defaultDays = storageService.getConfig("defaultCommitDays");
1825
+ days = await promptDaysToScan(defaultDays);
1826
+ logger.log("");
1827
+ }
1828
+ const spinner = fetchingCommitsSpinner(days);
1829
+ spinner.start();
1830
+ let commits;
1831
+ if (options.all) {
1832
+ commits = await gitService.getCommitsWithStats({ days });
1833
+ } else {
1834
+ commits = await gitService.getCommitsByCurrentUser({ days });
1835
+ }
1836
+ if (commits.length === 0) {
1837
+ failSpinner(spinner, `No commits found in the last ${days} days`);
1838
+ logger.log("");
1839
+ logger.info("Try increasing the number of days or check your git configuration");
1840
+ return;
1841
+ }
1842
+ succeedSpinner(spinner, `Found ${commits.length} commit${commits.length > 1 ? "s" : ""}`);
1843
+ logger.log("");
1844
+ logger.log(formatCommitStats(commits));
1845
+ logger.log("");
1846
+ const selectedShas = await promptSelectCommits(commits);
1847
+ if (selectedShas.length === 0) {
1848
+ logger.warning("No commits selected");
1849
+ return;
1850
+ }
1851
+ const selectedCommits = commits.filter((c) => selectedShas.includes(c.sha));
1852
+ logger.log("");
1853
+ logger.success(`Selected ${selectedCommits.length} commit${selectedCommits.length > 1 ? "s" : ""}`);
1854
+ logger.log("");
1855
+ const refineSpinner = refiningCommitsSpinner(selectedCommits.length);
1856
+ refineSpinner.start();
1857
+ const refineRequest = {
1858
+ commits: selectedCommits.map((c) => ({
1859
+ sha: c.sha,
1860
+ message: c.message,
1861
+ author: c.author,
1862
+ date: c.date,
1863
+ diff_stats: c.diffStats ? {
1864
+ filesChanged: c.diffStats.filesChanged,
1865
+ insertions: c.diffStats.insertions,
1866
+ deletions: c.diffStats.deletions
1867
+ } : void 0
1868
+ }))
1869
+ };
1870
+ const refineResponse = await apiService.refineCommits(refineRequest);
1871
+ const refinedCommits = refineResponse.refined_commits;
1872
+ succeedSpinner(refineSpinner, "Commits refined successfully");
1873
+ logger.log("");
1874
+ logger.info("Preview of refined brags:");
1875
+ logger.log("");
1876
+ logger.log(formatRefinedCommitsTable(refinedCommits));
1877
+ logger.log("");
1878
+ const shouldCreate = await promptConfirm("Create these brags?", true);
1879
+ if (!shouldCreate) {
1880
+ logger.warning("Cancelled");
1881
+ return;
1882
+ }
1883
+ logger.log("");
1884
+ const createSpinner2 = creatingBragsSpinner(refinedCommits.length);
1885
+ createSpinner2.start();
1886
+ const createRequest = {
1887
+ brags: refinedCommits.map((refined) => {
1888
+ const originalCommit = selectedCommits.find((c) => c.sha === refined.sha);
1889
+ return {
1890
+ commit_sha: refined.sha,
1891
+ title: refined.refined_title,
1892
+ description: refined.refined_description,
1893
+ tags: refined.suggested_tags,
1894
+ repository: repoInfo.remoteUrl,
1895
+ date: originalCommit?.date || (/* @__PURE__ */ new Date()).toISOString()
1896
+ };
1897
+ })
1898
+ };
1899
+ const createResponse = await apiService.createBrags(createRequest);
1900
+ succeedSpinner(createSpinner2, `Created ${createResponse.created} brags`);
1901
+ logger.log("");
1902
+ logger.log(
1903
+ boxen4(formatSuccessMessage(createResponse.created), {
1904
+ padding: 1,
1905
+ margin: 1,
1906
+ borderStyle: "round",
1907
+ borderColor: "green"
1908
+ })
1909
+ );
1910
+ } catch (error) {
1911
+ const err = error;
1912
+ logger.log("");
1913
+ logger.log(
1914
+ boxen4(formatErrorMessage(err.message, getErrorHint2(err)), {
1915
+ padding: 1,
1916
+ margin: 1,
1917
+ borderStyle: "round",
1918
+ borderColor: "red"
1919
+ })
1920
+ );
1921
+ process.exit(1);
1922
+ }
1923
+ }
1924
+ function getErrorHint2(error) {
1925
+ if (error.name === "GitError") {
1926
+ return "Make sure you are in a git repository with commits";
1927
+ }
1928
+ if (error.name === "TokenExpiredError" || error.name === "AuthenticationError") {
1929
+ return 'Run "bragduck init" to login again';
1930
+ }
1931
+ if (error.name === "NetworkError") {
1932
+ return "Check your internet connection and try again";
1933
+ }
1934
+ if (error.name === "ApiError") {
1935
+ return "The server might be experiencing issues. Try again later";
1936
+ }
1937
+ return "Try running with DEBUG=* for more information";
1938
+ }
1939
+
1940
+ // src/commands/list.ts
1941
+ init_esm_shims();
1942
+ init_api_service();
1943
+ init_auth_service();
1944
+ init_logger();
1945
+ import boxen5 from "boxen";
1946
+ import chalk8 from "chalk";
1947
+ import Table2 from "cli-table3";
1948
+ async function listCommand(options = {}) {
1949
+ logger.log("");
1950
+ try {
1951
+ const isAuthenticated = await authService.isAuthenticated();
1952
+ if (!isAuthenticated) {
1953
+ logger.log(
1954
+ boxen5(
1955
+ `${chalk8.yellow.bold("\u26A0 Not authenticated")}
1956
+
1957
+ Please run ${chalk8.cyan("bragduck init")} to login first.`,
1958
+ {
1959
+ padding: 1,
1960
+ margin: 1,
1961
+ borderStyle: "round",
1962
+ borderColor: "yellow"
1963
+ }
1964
+ )
1965
+ );
1966
+ process.exit(1);
1967
+ }
1968
+ const limit = options.limit || 50;
1969
+ const offset = options.offset || 0;
1970
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : void 0;
1971
+ const search = options.search;
1972
+ const spinner = fetchingBragsSpinner();
1973
+ spinner.start();
1974
+ const response = await apiService.listBrags({
1975
+ limit,
1976
+ offset,
1977
+ tags,
1978
+ search
1979
+ });
1980
+ if (response.brags.length === 0) {
1981
+ failSpinner(spinner, "No brags found");
1982
+ logger.log("");
1983
+ if (search || tags) {
1984
+ logger.info("Try adjusting your filters or run without filters to see all brags");
1985
+ } else {
1986
+ logger.info(`Run ${chalk8.cyan("bragduck scan")} to create your first brag!`);
1987
+ }
1988
+ return;
1989
+ }
1990
+ succeedSpinner(spinner, `Found ${response.total} brag${response.total > 1 ? "s" : ""}`);
1991
+ logger.log("");
1992
+ logger.log(formatBragsTable(response.brags));
1993
+ logger.log("");
1994
+ if (response.has_more) {
1995
+ const nextOffset = offset + limit;
1996
+ logger.info(
1997
+ `Showing ${offset + 1}-${offset + response.brags.length} of ${response.total} total brags`
1998
+ );
1999
+ logger.info(
2000
+ chalk8.dim(`Run `) + chalk8.cyan(`bragduck list --offset ${nextOffset}`) + chalk8.dim(` to see more`)
2001
+ );
2002
+ logger.log("");
2003
+ } else if (offset > 0) {
2004
+ logger.info(
2005
+ `Showing ${offset + 1}-${offset + response.brags.length} of ${response.total} total brags`
2006
+ );
2007
+ logger.log("");
2008
+ }
2009
+ if (search || tags) {
2010
+ const filterInfo = [];
2011
+ if (search) filterInfo.push(`search: "${search}"`);
2012
+ if (tags) filterInfo.push(`tags: ${tags.join(", ")}`);
2013
+ logger.info(chalk8.dim(`Filters applied: ${filterInfo.join(", ")}`));
2014
+ logger.log("");
2015
+ }
2016
+ } catch (error) {
2017
+ const err = error;
2018
+ logger.log("");
2019
+ logger.log(
2020
+ boxen5(formatErrorMessage(err.message, getErrorHint3(err)), {
2021
+ padding: 1,
2022
+ margin: 1,
2023
+ borderStyle: "round",
2024
+ borderColor: "red"
2025
+ })
2026
+ );
2027
+ process.exit(1);
2028
+ }
2029
+ }
2030
+ function formatBragsTable(brags) {
2031
+ const table = new Table2({
2032
+ head: [
2033
+ chalk8.cyan("Date"),
2034
+ chalk8.cyan("Title"),
2035
+ chalk8.cyan("Description"),
2036
+ chalk8.cyan("Tags"),
2037
+ chalk8.cyan("Repository")
2038
+ ],
2039
+ colWidths: [12, 30, 40, 20, 30],
2040
+ wordWrap: true,
2041
+ style: {
2042
+ head: [],
2043
+ border: ["gray"]
2044
+ }
2045
+ });
2046
+ brags.forEach((brag) => {
2047
+ const date = new Date(brag.date).toLocaleDateString();
2048
+ const title = brag.title;
2049
+ const description = truncateText(brag.description, 100);
2050
+ const tags = brag.tags.length > 0 ? brag.tags.join(", ") : chalk8.dim("none");
2051
+ const repository = brag.repository ? truncateText(extractRepoName(brag.repository), 25) : chalk8.dim("none");
2052
+ table.push([
2053
+ chalk8.yellow(date),
2054
+ chalk8.white(title),
2055
+ chalk8.dim(description),
2056
+ chalk8.blue(tags),
2057
+ chalk8.gray(repository)
2058
+ ]);
2059
+ });
2060
+ return table.toString();
2061
+ }
2062
+ function truncateText(text, maxLength) {
2063
+ if (text.length <= maxLength) {
2064
+ return text;
2065
+ }
2066
+ return text.substring(0, maxLength - 3) + "...";
2067
+ }
2068
+ function extractRepoName(url) {
2069
+ try {
2070
+ const match = url.match(/([^/]+\/[^/]+?)(\.git)?$/);
2071
+ return match ? match[1] : url;
2072
+ } catch {
2073
+ return url;
2074
+ }
2075
+ }
2076
+ function getErrorHint3(error) {
2077
+ if (error.name === "TokenExpiredError" || error.name === "AuthenticationError") {
2078
+ return 'Run "bragduck init" to login again';
2079
+ }
2080
+ if (error.name === "NetworkError") {
2081
+ return "Check your internet connection and try again";
2082
+ }
2083
+ if (error.name === "ApiError") {
2084
+ return "The server might be experiencing issues. Try again later";
2085
+ }
2086
+ return "Try running with DEBUG=* for more information";
2087
+ }
2088
+
2089
+ // src/commands/config.ts
2090
+ init_esm_shims();
2091
+ init_storage_service();
2092
+ init_api_service();
2093
+ init_logger();
2094
+ init_constants();
2095
+ import boxen6 from "boxen";
2096
+ import chalk9 from "chalk";
2097
+ import Table3 from "cli-table3";
2098
+ init_errors();
2099
+ var VALID_CONFIG_KEYS = Object.values(CONFIG_KEYS);
2100
+ async function configCommand(subcommand, key, value) {
2101
+ logger.log("");
2102
+ try {
2103
+ if (!subcommand) {
2104
+ subcommand = "list";
2105
+ }
2106
+ switch (subcommand) {
2107
+ case "list":
2108
+ await handleListConfig();
2109
+ break;
2110
+ case "get":
2111
+ if (!key) {
2112
+ throw new ValidationError('Config key is required for "get" command');
2113
+ }
2114
+ await handleGetConfig(key);
2115
+ break;
2116
+ case "set":
2117
+ if (!key || value === void 0) {
2118
+ throw new ValidationError('Config key and value are required for "set" command');
2119
+ }
2120
+ await handleSetConfig(key, value);
2121
+ break;
2122
+ default:
2123
+ throw new ValidationError(
2124
+ `Invalid subcommand: "${subcommand}". Valid subcommands: list, get, set`
2125
+ );
2126
+ }
2127
+ } catch (error) {
2128
+ logger.log("");
2129
+ logger.log(
2130
+ boxen6(formatErrorMessage(error.message, getConfigHint(error)), {
2131
+ padding: 1,
2132
+ margin: 1,
2133
+ borderStyle: "round",
2134
+ borderColor: "red"
2135
+ })
2136
+ );
2137
+ process.exit(1);
2138
+ }
2139
+ }
2140
+ async function handleListConfig() {
2141
+ const config2 = {
2142
+ apiBaseUrl: storageService.getConfig("apiBaseUrl"),
2143
+ defaultCommitDays: storageService.getConfig("defaultCommitDays"),
2144
+ autoVersionCheck: storageService.getConfig("autoVersionCheck")
2145
+ };
2146
+ const table = new Table3({
2147
+ head: [chalk9.cyan("Key"), chalk9.cyan("Value"), chalk9.cyan("Default")],
2148
+ colWidths: [25, 40, 40],
2149
+ wordWrap: true,
2150
+ style: {
2151
+ head: [],
2152
+ border: ["gray"]
2153
+ }
2154
+ });
2155
+ table.push([
2156
+ chalk9.white("apiBaseUrl"),
2157
+ chalk9.yellow(config2.apiBaseUrl),
2158
+ chalk9.dim(DEFAULT_CONFIG.apiBaseUrl)
2159
+ ]);
2160
+ table.push([
2161
+ chalk9.white("defaultCommitDays"),
2162
+ chalk9.yellow(String(config2.defaultCommitDays)),
2163
+ chalk9.dim(String(DEFAULT_CONFIG.defaultCommitDays))
2164
+ ]);
2165
+ table.push([
2166
+ chalk9.white("autoVersionCheck"),
2167
+ chalk9.yellow(String(config2.autoVersionCheck)),
2168
+ chalk9.dim(String(DEFAULT_CONFIG.autoVersionCheck))
2169
+ ]);
2170
+ logger.info("Current configuration:");
2171
+ logger.log("");
2172
+ logger.log(table.toString());
2173
+ logger.log("");
2174
+ logger.info(chalk9.dim("To change a value: ") + chalk9.cyan("bragduck config set <key> <value>"));
2175
+ logger.log("");
2176
+ }
2177
+ async function handleGetConfig(key) {
2178
+ validateConfigKey(key);
2179
+ const value = storageService.getConfig(key);
2180
+ const defaultValue = DEFAULT_CONFIG[key];
2181
+ logger.info(`Configuration for ${chalk9.cyan(key)}:`);
2182
+ logger.log("");
2183
+ logger.log(` ${chalk9.white("Current:")} ${chalk9.yellow(String(value))}`);
2184
+ logger.log(` ${chalk9.white("Default:")} ${chalk9.dim(String(defaultValue))}`);
2185
+ logger.log("");
2186
+ if (value === defaultValue) {
2187
+ logger.info(chalk9.dim("Using default value"));
2188
+ } else {
2189
+ logger.info(
2190
+ chalk9.dim("Custom value set. Reset with: ") + chalk9.cyan(`bragduck config set ${key} ${defaultValue}`)
2191
+ );
2192
+ }
2193
+ logger.log("");
2194
+ }
2195
+ async function handleSetConfig(key, value) {
2196
+ validateConfigKey(key);
2197
+ const typedValue = validateAndConvertValue(key, value);
2198
+ storageService.setConfig(key, typedValue);
2199
+ if (key === CONFIG_KEYS.API_BASE_URL) {
2200
+ apiService.setBaseURL(typedValue);
2201
+ }
2202
+ logger.log(
2203
+ boxen6(
2204
+ `${chalk9.green.bold("\u2713 Configuration updated")}
2205
+
2206
+ ${chalk9.white(key)}: ${chalk9.yellow(String(typedValue))}`,
2207
+ {
2208
+ padding: 1,
2209
+ margin: 1,
2210
+ borderStyle: "round",
2211
+ borderColor: "green"
2212
+ }
2213
+ )
2214
+ );
2215
+ }
2216
+ function validateConfigKey(key) {
2217
+ if (!VALID_CONFIG_KEYS.includes(key)) {
2218
+ throw new ValidationError(
2219
+ `Invalid config key: "${key}"
2220
+
2221
+ Valid keys:
2222
+ ${VALID_CONFIG_KEYS.map((k) => ` - ${k}`).join("\n")}`
2223
+ );
2224
+ }
2225
+ }
2226
+ function validateAndConvertValue(key, value) {
2227
+ switch (key) {
2228
+ case CONFIG_KEYS.API_BASE_URL:
2229
+ if (!isValidUrl(value)) {
2230
+ throw new ValidationError(
2231
+ `Invalid URL format: "${value}"
2232
+
2233
+ Example: https://api.bragduck.com`
2234
+ );
2235
+ }
2236
+ return value;
2237
+ case CONFIG_KEYS.DEFAULT_COMMIT_DAYS:
2238
+ const days = parseInt(value, 10);
2239
+ if (isNaN(days) || days < 1 || days > 365) {
2240
+ throw new ValidationError(
2241
+ `Invalid value for defaultCommitDays: "${value}"
2242
+
2243
+ Must be a number between 1 and 365`
2244
+ );
2245
+ }
2246
+ return days;
2247
+ case CONFIG_KEYS.AUTO_VERSION_CHECK:
2248
+ const lowerValue = value.toLowerCase();
2249
+ if (lowerValue === "true" || lowerValue === "1" || lowerValue === "yes") {
2250
+ return true;
2251
+ } else if (lowerValue === "false" || lowerValue === "0" || lowerValue === "no") {
2252
+ return false;
2253
+ }
2254
+ throw new ValidationError(
2255
+ `Invalid value for autoVersionCheck: "${value}"
2256
+
2257
+ Must be one of: true, false, yes, no, 1, 0`
2258
+ );
2259
+ default:
2260
+ return value;
2261
+ }
2262
+ }
2263
+ function isValidUrl(url) {
2264
+ try {
2265
+ const parsed = new URL(url);
2266
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
2267
+ } catch {
2268
+ return false;
2269
+ }
2270
+ }
2271
+ function getConfigHint(error) {
2272
+ if (error.name === "ValidationError") {
2273
+ return 'Run "bragduck config list" to see all available configuration keys';
2274
+ }
2275
+ return 'Run "bragduck config --help" for usage information';
2276
+ }
2277
+
2278
+ // src/cli.ts
2279
+ init_version();
2280
+ init_logger();
2281
+ var __filename4 = fileURLToPath4(import.meta.url);
2282
+ var __dirname4 = dirname3(__filename4);
2283
+ var packageJsonPath = join5(__dirname4, "../../package.json");
2284
+ var packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
2285
+ var program = new Command();
2286
+ program.name("bragduck").description("CLI tool for managing developer achievements and brags").version(packageJson.version, "-v, --version", "Display version number").helpOption("-h, --help", "Display help information").option("--skip-version-check", "Skip automatic version check on startup").option("--debug", "Enable debug mode (shows detailed logs)");
2287
+ program.command("init").description("Authenticate with Bragduck").action(async () => {
2288
+ try {
2289
+ await initCommand();
2290
+ } catch (error) {
2291
+ console.error(error);
2292
+ process.exit(1);
2293
+ }
2294
+ });
2295
+ program.command("scan").description("Scan git commits and create brags").option("-d, --days <number>", "Number of days to scan", (val) => parseInt(val, 10)).option("-a, --all", "Include all commits (not just current user)").action(async (options) => {
2296
+ try {
2297
+ await scanCommand(options);
2298
+ } catch (error) {
2299
+ console.error(error);
2300
+ process.exit(1);
2301
+ }
2302
+ });
2303
+ program.command("list").description("List your existing brags").option("-l, --limit <number>", "Number of brags to display", (val) => parseInt(val, 10), 50).option("-o, --offset <number>", "Number of brags to skip", (val) => parseInt(val, 10), 0).option("-t, --tags <tags>", "Filter by tags (comma-separated)").option("-s, --search <query>", "Search brags by keyword").action(async (options) => {
2304
+ try {
2305
+ await listCommand(options);
2306
+ } catch (error) {
2307
+ console.error(error);
2308
+ process.exit(1);
2309
+ }
2310
+ });
2311
+ program.command("logout").description("Clear stored credentials").action(async () => {
2312
+ try {
2313
+ await logoutCommand();
2314
+ } catch (error) {
2315
+ console.error(error);
2316
+ process.exit(1);
2317
+ }
2318
+ });
2319
+ program.command("config [subcommand] [key] [value]").description("Manage CLI configuration (subcommands: list, get, set)").action(async (subcommand, key, value) => {
2320
+ try {
2321
+ await configCommand(subcommand, key, value);
2322
+ } catch (error) {
2323
+ console.error(error);
2324
+ process.exit(1);
2325
+ }
2326
+ });
2327
+ program.hook("preAction", async (_thisCommand) => {
2328
+ const options = program.opts();
2329
+ if (options.debug) {
2330
+ process.env.DEBUG = "true";
2331
+ logger.debug("Debug mode enabled");
2332
+ }
2333
+ const isVersionOrHelp = process.argv.includes("--version") || process.argv.includes("-v") || process.argv.includes("--help") || process.argv.includes("-h");
2334
+ if (!options.skipVersionCheck && !isVersionOrHelp) {
2335
+ try {
2336
+ await checkForUpdates({ silent: false });
2337
+ } catch (error) {
2338
+ logger.debug("Version check failed, continuing...");
2339
+ }
2340
+ }
2341
+ });
2342
+ program.parse(process.argv);
2343
+ if (!process.argv.slice(2).length) {
2344
+ program.outputHelp();
2345
+ }
2346
+ //# sourceMappingURL=bragduck.js.map