@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.
package/dist/index.js ADDED
@@ -0,0 +1,1212 @@
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, 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
+ DEFAULT_CONFIG = {
35
+ apiBaseUrl: process.env.API_BASE_URL || "https://api.bragduck.com",
36
+ defaultCommitDays: 30,
37
+ autoVersionCheck: true
38
+ };
39
+ OAUTH_CONFIG = {
40
+ CLIENT_ID: "bragduck-cli",
41
+ CALLBACK_PATH: "/callback",
42
+ TIMEOUT_MS: 12e4,
43
+ // 2 minutes
44
+ MIN_PORT: 8e3,
45
+ MAX_PORT: 9e3
46
+ };
47
+ API_ENDPOINTS = {
48
+ AUTH: {
49
+ INITIATE: "/v1/auth/cli/initiate",
50
+ TOKEN: "/v1/auth/cli/token"
51
+ },
52
+ COMMITS: {
53
+ REFINE: "/v1/commits/refine"
54
+ },
55
+ BRAGS: {
56
+ CREATE: "/v1/brags",
57
+ LIST: "/v1/brags"
58
+ },
59
+ VERSION: "/v1/cli/version"
60
+ };
61
+ ENCRYPTION_CONFIG = {
62
+ ALGORITHM: "aes-256-gcm",
63
+ KEY_LENGTH: 32,
64
+ IV_LENGTH: 16,
65
+ AUTH_TAG_LENGTH: 16,
66
+ SALT_LENGTH: 32
67
+ };
68
+ STORAGE_PATHS = {
69
+ CREDENTIALS_DIR: ".bragduck",
70
+ CREDENTIALS_FILE: "credentials.enc",
71
+ CONFIG_FILE: "config.json"
72
+ };
73
+ HTTP_STATUS = {
74
+ OK: 200,
75
+ CREATED: 201,
76
+ BAD_REQUEST: 400,
77
+ UNAUTHORIZED: 401,
78
+ FORBIDDEN: 403,
79
+ NOT_FOUND: 404,
80
+ INTERNAL_SERVER_ERROR: 500
81
+ };
82
+ }
83
+ });
84
+
85
+ // src/services/storage.service.ts
86
+ import Conf from "conf";
87
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
88
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
89
+ import { homedir } from "os";
90
+ import { join as join2 } from "path";
91
+ var StorageService, storageService;
92
+ var init_storage_service = __esm({
93
+ "src/services/storage.service.ts"() {
94
+ "use strict";
95
+ init_esm_shims();
96
+ init_constants();
97
+ StorageService = class {
98
+ config;
99
+ storageBackend;
100
+ credentialsDir;
101
+ credentialsFilePath;
102
+ constructor() {
103
+ this.config = new Conf({
104
+ projectName: APP_NAME,
105
+ defaults: DEFAULT_CONFIG
106
+ });
107
+ this.credentialsDir = join2(homedir(), STORAGE_PATHS.CREDENTIALS_DIR);
108
+ this.credentialsFilePath = join2(this.credentialsDir, STORAGE_PATHS.CREDENTIALS_FILE);
109
+ this.storageBackend = "file";
110
+ this.ensureCredentialsDir();
111
+ }
112
+ /**
113
+ * Ensure credentials directory exists
114
+ */
115
+ ensureCredentialsDir() {
116
+ if (!existsSync(this.credentialsDir)) {
117
+ mkdirSync(this.credentialsDir, { recursive: true, mode: 448 });
118
+ }
119
+ }
120
+ /**
121
+ * Encrypt data for file storage
122
+ */
123
+ encrypt(data, key) {
124
+ const iv = randomBytes(ENCRYPTION_CONFIG.IV_LENGTH);
125
+ const cipher = createCipheriv(ENCRYPTION_CONFIG.ALGORITHM, key, iv, {
126
+ authTagLength: ENCRYPTION_CONFIG.AUTH_TAG_LENGTH
127
+ });
128
+ let encrypted = cipher.update(data, "utf8", "hex");
129
+ encrypted += cipher.final("hex");
130
+ const authTag = cipher.getAuthTag();
131
+ return {
132
+ encrypted,
133
+ iv: iv.toString("hex"),
134
+ authTag: authTag.toString("hex"),
135
+ salt: ""
136
+ // Salt is stored separately
137
+ };
138
+ }
139
+ /**
140
+ * Decrypt data from file storage
141
+ */
142
+ decrypt(encryptedData, key) {
143
+ const decipher = createDecipheriv(
144
+ ENCRYPTION_CONFIG.ALGORITHM,
145
+ key,
146
+ Buffer.from(encryptedData.iv, "hex"),
147
+ {
148
+ authTagLength: ENCRYPTION_CONFIG.AUTH_TAG_LENGTH
149
+ }
150
+ );
151
+ decipher.setAuthTag(Buffer.from(encryptedData.authTag, "hex"));
152
+ let decrypted = decipher.update(encryptedData.encrypted, "hex", "utf8");
153
+ decrypted += decipher.final("utf8");
154
+ return decrypted;
155
+ }
156
+ /**
157
+ * Derive encryption key from machine-specific data
158
+ */
159
+ deriveEncryptionKey(salt) {
160
+ const password = `${APP_NAME}-${homedir()}-${process.platform}`;
161
+ return scryptSync(password, salt, ENCRYPTION_CONFIG.KEY_LENGTH);
162
+ }
163
+ /**
164
+ * Store credentials using encrypted file storage
165
+ */
166
+ async setCredentials(credentials) {
167
+ const data = JSON.stringify(credentials);
168
+ const salt = randomBytes(ENCRYPTION_CONFIG.SALT_LENGTH);
169
+ const key = this.deriveEncryptionKey(salt);
170
+ const encrypted = this.encrypt(data, key);
171
+ encrypted.salt = salt.toString("hex");
172
+ writeFileSync(this.credentialsFilePath, JSON.stringify(encrypted), {
173
+ mode: 384,
174
+ encoding: "utf8"
175
+ });
176
+ }
177
+ /**
178
+ * Retrieve credentials from encrypted file storage
179
+ */
180
+ async getCredentials() {
181
+ if (!existsSync(this.credentialsFilePath)) {
182
+ return null;
183
+ }
184
+ try {
185
+ const encryptedData = JSON.parse(
186
+ readFileSync(this.credentialsFilePath, "utf8")
187
+ );
188
+ const salt = Buffer.from(encryptedData.salt, "hex");
189
+ const key = this.deriveEncryptionKey(salt);
190
+ const decrypted = this.decrypt(encryptedData, key);
191
+ return JSON.parse(decrypted);
192
+ } catch (error) {
193
+ console.error("Failed to decrypt credentials:", error);
194
+ return null;
195
+ }
196
+ }
197
+ /**
198
+ * Delete credentials from file storage
199
+ */
200
+ async deleteCredentials() {
201
+ if (existsSync(this.credentialsFilePath)) {
202
+ try {
203
+ unlinkSync(this.credentialsFilePath);
204
+ } catch (error) {
205
+ console.error("Failed to delete credentials file:", error);
206
+ }
207
+ }
208
+ }
209
+ /**
210
+ * Check if user is authenticated
211
+ */
212
+ async isAuthenticated() {
213
+ const credentials = await this.getCredentials();
214
+ if (!credentials || !credentials.accessToken) {
215
+ return false;
216
+ }
217
+ if (credentials.expiresAt && credentials.expiresAt < Date.now()) {
218
+ return false;
219
+ }
220
+ return true;
221
+ }
222
+ /**
223
+ * Store user information
224
+ */
225
+ setUserInfo(userInfo) {
226
+ this.config.set("userInfo", userInfo);
227
+ }
228
+ /**
229
+ * Get user information
230
+ */
231
+ getUserInfo() {
232
+ return this.config.get("userInfo") || null;
233
+ }
234
+ /**
235
+ * Delete user information
236
+ */
237
+ deleteUserInfo() {
238
+ this.config.delete("userInfo");
239
+ }
240
+ /**
241
+ * Store OAuth state for CSRF protection
242
+ */
243
+ setOAuthState(state) {
244
+ this.config.set("oauthState", state);
245
+ }
246
+ /**
247
+ * Get OAuth state
248
+ */
249
+ getOAuthState() {
250
+ return this.config.get("oauthState") || null;
251
+ }
252
+ /**
253
+ * Delete OAuth state
254
+ */
255
+ deleteOAuthState() {
256
+ this.config.delete("oauthState");
257
+ }
258
+ /**
259
+ * Get configuration value
260
+ */
261
+ getConfig(key) {
262
+ return this.config.get(key);
263
+ }
264
+ /**
265
+ * Set configuration value
266
+ */
267
+ setConfig(key, value) {
268
+ this.config.set(key, value);
269
+ }
270
+ /**
271
+ * Get all configuration
272
+ */
273
+ getAllConfig() {
274
+ return this.config.store;
275
+ }
276
+ /**
277
+ * Reset configuration to defaults
278
+ */
279
+ resetConfig() {
280
+ this.config.clear();
281
+ Object.entries(DEFAULT_CONFIG).forEach(([key, value]) => {
282
+ this.config.set(key, value);
283
+ });
284
+ }
285
+ /**
286
+ * Get current storage backend
287
+ */
288
+ getStorageBackend() {
289
+ return this.storageBackend;
290
+ }
291
+ /**
292
+ * Clear all stored data (credentials + config)
293
+ */
294
+ async clearAll() {
295
+ await this.deleteCredentials();
296
+ this.deleteUserInfo();
297
+ this.deleteOAuthState();
298
+ this.resetConfig();
299
+ }
300
+ };
301
+ storageService = new StorageService();
302
+ }
303
+ });
304
+
305
+ // src/utils/errors.ts
306
+ var BragduckError, AuthenticationError, ApiError, NetworkError, OAuthError, TokenExpiredError;
307
+ var init_errors = __esm({
308
+ "src/utils/errors.ts"() {
309
+ "use strict";
310
+ init_esm_shims();
311
+ BragduckError = class extends Error {
312
+ constructor(message, code, details) {
313
+ super(message);
314
+ this.code = code;
315
+ this.details = details;
316
+ this.name = "BragduckError";
317
+ Error.captureStackTrace(this, this.constructor);
318
+ }
319
+ };
320
+ AuthenticationError = class extends BragduckError {
321
+ constructor(message, details) {
322
+ super(message, "AUTH_ERROR", details);
323
+ this.name = "AuthenticationError";
324
+ }
325
+ };
326
+ ApiError = class extends BragduckError {
327
+ constructor(message, statusCode, details) {
328
+ super(message, "API_ERROR", details);
329
+ this.statusCode = statusCode;
330
+ this.name = "ApiError";
331
+ }
332
+ };
333
+ NetworkError = class extends BragduckError {
334
+ constructor(message, details) {
335
+ super(message, "NETWORK_ERROR", details);
336
+ this.name = "NetworkError";
337
+ }
338
+ };
339
+ OAuthError = class extends AuthenticationError {
340
+ constructor(message, details) {
341
+ super(message, details);
342
+ this.name = "OAuthError";
343
+ }
344
+ };
345
+ TokenExpiredError = class extends AuthenticationError {
346
+ constructor(message = "Authentication token has expired") {
347
+ super(message);
348
+ this.name = "TokenExpiredError";
349
+ this.code = "TOKEN_EXPIRED";
350
+ }
351
+ };
352
+ }
353
+ });
354
+
355
+ // src/utils/logger.ts
356
+ import chalk from "chalk";
357
+ var logger;
358
+ var init_logger = __esm({
359
+ "src/utils/logger.ts"() {
360
+ "use strict";
361
+ init_esm_shims();
362
+ logger = {
363
+ /**
364
+ * Debug message (only shown when DEBUG env var is set)
365
+ */
366
+ debug: (message, ...args) => {
367
+ if (process.env.DEBUG) {
368
+ console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
369
+ }
370
+ },
371
+ /**
372
+ * Info message
373
+ */
374
+ info: (message) => {
375
+ console.log(chalk.blue(`\u2139 ${message}`));
376
+ },
377
+ /**
378
+ * Success message
379
+ */
380
+ success: (message) => {
381
+ console.log(chalk.green(`\u2713 ${message}`));
382
+ },
383
+ /**
384
+ * Warning message
385
+ */
386
+ warning: (message) => {
387
+ console.warn(chalk.yellow(`\u26A0 ${message}`));
388
+ },
389
+ /**
390
+ * Error message
391
+ */
392
+ error: (message) => {
393
+ console.error(chalk.red(`\u2717 ${message}`));
394
+ },
395
+ /**
396
+ * Plain log without formatting
397
+ */
398
+ log: (message) => {
399
+ console.log(message);
400
+ }
401
+ };
402
+ }
403
+ });
404
+
405
+ // src/utils/oauth-server.ts
406
+ import { createServer } from "http";
407
+ import { parse } from "url";
408
+ async function findAvailablePort() {
409
+ const { MIN_PORT, MAX_PORT } = OAUTH_CONFIG;
410
+ for (let port = MIN_PORT; port <= MAX_PORT; port++) {
411
+ try {
412
+ await new Promise((resolve, reject) => {
413
+ const testServer = createServer();
414
+ testServer.once("error", reject);
415
+ testServer.once("listening", () => {
416
+ testServer.close(() => resolve());
417
+ });
418
+ testServer.listen(port, "127.0.0.1");
419
+ });
420
+ return port;
421
+ } catch (error) {
422
+ continue;
423
+ }
424
+ }
425
+ throw new OAuthError(`No available ports found in range ${MIN_PORT}-${MAX_PORT}`);
426
+ }
427
+ async function startOAuthCallbackServer(expectedState) {
428
+ const port = await findAvailablePort();
429
+ const timeout = OAUTH_CONFIG.TIMEOUT_MS;
430
+ return new Promise((resolve, reject) => {
431
+ let server = null;
432
+ let timeoutId;
433
+ const cleanup = () => {
434
+ if (timeoutId) {
435
+ clearTimeout(timeoutId);
436
+ }
437
+ if (server) {
438
+ if (typeof server.closeAllConnections === "function") {
439
+ server.closeAllConnections();
440
+ }
441
+ server.close(() => {
442
+ logger.debug("OAuth server closed");
443
+ });
444
+ server.unref();
445
+ }
446
+ };
447
+ const handleRequest = (req, res) => {
448
+ const parsedUrl = parse(req.url || "", true);
449
+ logger.debug(`OAuth callback received: ${req.url}`);
450
+ if (parsedUrl.pathname === OAUTH_CONFIG.CALLBACK_PATH) {
451
+ const { code, state, error, error_description } = parsedUrl.query;
452
+ if (error) {
453
+ const errorMsg = error_description || error;
454
+ logger.debug(`OAuth error: ${errorMsg}`);
455
+ res.writeHead(400, { "Content-Type": "text/html" });
456
+ res.end(ERROR_HTML(String(errorMsg)));
457
+ cleanup();
458
+ reject(new OAuthError(`OAuth error: ${errorMsg}`));
459
+ return;
460
+ }
461
+ if (!code || !state) {
462
+ const errorMsg = "Missing code or state parameter";
463
+ logger.debug(errorMsg);
464
+ res.writeHead(400, { "Content-Type": "text/html" });
465
+ res.end(ERROR_HTML(errorMsg));
466
+ cleanup();
467
+ reject(new OAuthError(errorMsg));
468
+ return;
469
+ }
470
+ if (state !== expectedState) {
471
+ const errorMsg = "Invalid state parameter (possible CSRF attack)";
472
+ logger.debug(errorMsg);
473
+ res.writeHead(400, { "Content-Type": "text/html" });
474
+ res.end(ERROR_HTML(errorMsg));
475
+ cleanup();
476
+ reject(new OAuthError(errorMsg));
477
+ return;
478
+ }
479
+ res.writeHead(200, { "Content-Type": "text/html" });
480
+ res.end(SUCCESS_HTML);
481
+ setTimeout(() => {
482
+ cleanup();
483
+ resolve({
484
+ code: String(code),
485
+ state: String(state),
486
+ port
487
+ });
488
+ }, 100);
489
+ return;
490
+ }
491
+ res.writeHead(404, { "Content-Type": "text/plain" });
492
+ res.end("Not Found");
493
+ };
494
+ server = createServer(handleRequest);
495
+ server.on("error", (error) => {
496
+ logger.debug(`OAuth server error: ${error.message}`);
497
+ cleanup();
498
+ reject(new OAuthError(`OAuth server error: ${error.message}`));
499
+ });
500
+ server.listen(port, "127.0.0.1", () => {
501
+ logger.debug(`OAuth callback server listening on http://127.0.0.1:${port}${OAUTH_CONFIG.CALLBACK_PATH}`);
502
+ });
503
+ timeoutId = setTimeout(() => {
504
+ logger.debug("OAuth callback timeout");
505
+ cleanup();
506
+ reject(new OAuthError("Authentication timeout - no callback received within 2 minutes"));
507
+ }, timeout);
508
+ });
509
+ }
510
+ async function getCallbackUrl() {
511
+ const port = await findAvailablePort();
512
+ return `http://127.0.0.1:${port}${OAUTH_CONFIG.CALLBACK_PATH}`;
513
+ }
514
+ var SUCCESS_HTML, ERROR_HTML;
515
+ var init_oauth_server = __esm({
516
+ "src/utils/oauth-server.ts"() {
517
+ "use strict";
518
+ init_esm_shims();
519
+ init_constants();
520
+ init_errors();
521
+ init_logger();
522
+ SUCCESS_HTML = `
523
+ <!DOCTYPE html>
524
+ <html>
525
+ <head>
526
+ <meta charset="UTF-8">
527
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
528
+ <title>Bragduck - Authentication Successful</title>
529
+ <style>
530
+ body {
531
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
532
+ display: flex;
533
+ align-items: center;
534
+ justify-content: center;
535
+ min-height: 100vh;
536
+ margin: 0;
537
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
538
+ color: white;
539
+ }
540
+ .container {
541
+ text-align: center;
542
+ padding: 2rem;
543
+ background: rgba(255, 255, 255, 0.1);
544
+ border-radius: 1rem;
545
+ backdrop-filter: blur(10px);
546
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
547
+ max-width: 500px;
548
+ }
549
+ h1 {
550
+ font-size: 2.5rem;
551
+ margin: 0 0 1rem 0;
552
+ }
553
+ .checkmark {
554
+ font-size: 4rem;
555
+ animation: scale-in 0.3s ease-out;
556
+ }
557
+ p {
558
+ font-size: 1.2rem;
559
+ margin: 1rem 0;
560
+ opacity: 0.9;
561
+ }
562
+ @keyframes scale-in {
563
+ from { transform: scale(0); }
564
+ to { transform: scale(1); }
565
+ }
566
+ </style>
567
+ </head>
568
+ <body>
569
+ <div class="container">
570
+ <div class="checkmark">\u2713</div>
571
+ <h1>Authentication Successful!</h1>
572
+ <p>You can now close this window and return to your terminal.</p>
573
+ </div>
574
+ </body>
575
+ </html>
576
+ `;
577
+ ERROR_HTML = (error) => `
578
+ <!DOCTYPE html>
579
+ <html>
580
+ <head>
581
+ <meta charset="UTF-8">
582
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
583
+ <title>Bragduck - Authentication Failed</title>
584
+ <style>
585
+ body {
586
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
587
+ display: flex;
588
+ align-items: center;
589
+ justify-content: center;
590
+ min-height: 100vh;
591
+ margin: 0;
592
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
593
+ color: white;
594
+ }
595
+ .container {
596
+ text-align: center;
597
+ padding: 2rem;
598
+ background: rgba(255, 255, 255, 0.1);
599
+ border-radius: 1rem;
600
+ backdrop-filter: blur(10px);
601
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
602
+ max-width: 500px;
603
+ }
604
+ h1 {
605
+ font-size: 2.5rem;
606
+ margin: 0 0 1rem 0;
607
+ }
608
+ .error-icon {
609
+ font-size: 4rem;
610
+ }
611
+ p {
612
+ font-size: 1.2rem;
613
+ margin: 1rem 0;
614
+ opacity: 0.9;
615
+ }
616
+ .error-details {
617
+ background: rgba(0, 0, 0, 0.2);
618
+ padding: 1rem;
619
+ border-radius: 0.5rem;
620
+ font-family: monospace;
621
+ font-size: 0.9rem;
622
+ margin-top: 1rem;
623
+ }
624
+ </style>
625
+ </head>
626
+ <body>
627
+ <div class="container">
628
+ <div class="error-icon">\u2717</div>
629
+ <h1>Authentication Failed</h1>
630
+ <p>Please return to your terminal and try again.</p>
631
+ <div class="error-details">${error}</div>
632
+ </div>
633
+ </body>
634
+ </html>
635
+ `;
636
+ }
637
+ });
638
+
639
+ // src/utils/browser.ts
640
+ import { exec } from "child_process";
641
+ import { promisify } from "util";
642
+ async function openBrowser(url) {
643
+ const platform = process.platform;
644
+ let command;
645
+ switch (platform) {
646
+ case "darwin":
647
+ command = `open "${url}"`;
648
+ break;
649
+ case "win32":
650
+ command = `start "" "${url}"`;
651
+ break;
652
+ default:
653
+ command = `xdg-open "${url}"`;
654
+ break;
655
+ }
656
+ try {
657
+ logger.debug(`Opening browser with command: ${command}`);
658
+ await execAsync(command);
659
+ logger.debug("Browser opened successfully");
660
+ } catch (error) {
661
+ logger.debug(`Failed to open browser: ${error}`);
662
+ throw new Error(
663
+ `Failed to open browser automatically. Please open this URL manually:
664
+ ${url}`
665
+ );
666
+ }
667
+ }
668
+ var execAsync;
669
+ var init_browser = __esm({
670
+ "src/utils/browser.ts"() {
671
+ "use strict";
672
+ init_esm_shims();
673
+ init_logger();
674
+ execAsync = promisify(exec);
675
+ }
676
+ });
677
+
678
+ // src/services/auth.service.ts
679
+ import { randomBytes as randomBytes2 } from "crypto";
680
+ import { ofetch } from "ofetch";
681
+ var AuthService, authService;
682
+ var init_auth_service = __esm({
683
+ "src/services/auth.service.ts"() {
684
+ "use strict";
685
+ init_esm_shims();
686
+ init_constants();
687
+ init_storage_service();
688
+ init_oauth_server();
689
+ init_browser();
690
+ init_errors();
691
+ init_logger();
692
+ AuthService = class {
693
+ apiBaseUrl;
694
+ constructor() {
695
+ this.apiBaseUrl = process.env.API_BASE_URL || storageService.getConfig("apiBaseUrl") || "https://api.bragduck.com";
696
+ }
697
+ /**
698
+ * Generate a random state string for CSRF protection
699
+ */
700
+ generateState() {
701
+ return randomBytes2(32).toString("hex");
702
+ }
703
+ /**
704
+ * Build the OAuth authorization URL
705
+ */
706
+ async buildAuthUrl(state, callbackUrl) {
707
+ const params = new URLSearchParams({
708
+ client_id: OAUTH_CONFIG.CLIENT_ID,
709
+ redirect_uri: callbackUrl,
710
+ state
711
+ });
712
+ return `${this.apiBaseUrl}${API_ENDPOINTS.AUTH.INITIATE}?${params.toString()}`;
713
+ }
714
+ /**
715
+ * Exchange authorization code for access token
716
+ */
717
+ async exchangeCodeForToken(code, callbackUrl) {
718
+ try {
719
+ logger.debug("Exchanging authorization code for token");
720
+ const response = await ofetch(
721
+ `${this.apiBaseUrl}${API_ENDPOINTS.AUTH.TOKEN}`,
722
+ {
723
+ method: "POST",
724
+ body: {
725
+ code,
726
+ redirect_uri: callbackUrl,
727
+ client_id: OAUTH_CONFIG.CLIENT_ID,
728
+ grant_type: "authorization_code"
729
+ },
730
+ headers: {
731
+ "Content-Type": "application/json"
732
+ }
733
+ }
734
+ );
735
+ logger.debug("Token exchange successful");
736
+ return response;
737
+ } catch (error) {
738
+ logger.debug(`Token exchange failed: ${error.message}`);
739
+ if (error.response) {
740
+ throw new AuthenticationError(
741
+ `Token exchange failed: ${error.response.statusText || "Unknown error"}`,
742
+ {
743
+ statusCode: error.response.status,
744
+ body: error.response._data
745
+ }
746
+ );
747
+ }
748
+ throw new NetworkError("Failed to connect to authentication server", {
749
+ originalError: error.message
750
+ });
751
+ }
752
+ }
753
+ /**
754
+ * Complete OAuth flow: start server, open browser, wait for callback, exchange token
755
+ */
756
+ async login() {
757
+ logger.debug("Starting OAuth login flow");
758
+ const state = this.generateState();
759
+ const callbackUrl = await getCallbackUrl();
760
+ storageService.setOAuthState({
761
+ state,
762
+ createdAt: Date.now()
763
+ });
764
+ logger.debug(`OAuth state: ${state}`);
765
+ logger.debug(`Callback URL: ${callbackUrl}`);
766
+ const authUrl = await this.buildAuthUrl(state, callbackUrl);
767
+ logger.debug(`Authorization URL: ${authUrl}`);
768
+ const serverPromise = startOAuthCallbackServer(state);
769
+ try {
770
+ await openBrowser(authUrl);
771
+ } catch (error) {
772
+ logger.warning("Could not open browser automatically");
773
+ logger.info(`Please open this URL in your browser:`);
774
+ logger.log(authUrl);
775
+ }
776
+ let callbackResult;
777
+ try {
778
+ callbackResult = await serverPromise;
779
+ } catch (error) {
780
+ storageService.deleteOAuthState();
781
+ throw error;
782
+ }
783
+ storageService.deleteOAuthState();
784
+ const tokenResponse = await this.exchangeCodeForToken(callbackResult.code, callbackUrl);
785
+ const expiresAt = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0;
786
+ const credentials = {
787
+ accessToken: tokenResponse.access_token,
788
+ refreshToken: tokenResponse.refresh_token,
789
+ expiresAt
790
+ };
791
+ await storageService.setCredentials(credentials);
792
+ if (tokenResponse.user) {
793
+ storageService.setUserInfo(tokenResponse.user);
794
+ }
795
+ logger.debug("Login successful");
796
+ return tokenResponse.user || { id: "unknown", email: "unknown", name: "Unknown User" };
797
+ }
798
+ /**
799
+ * Logout: clear all stored credentials and user info
800
+ */
801
+ async logout() {
802
+ logger.debug("Logging out");
803
+ await storageService.deleteCredentials();
804
+ storageService.deleteUserInfo();
805
+ storageService.deleteOAuthState();
806
+ logger.debug("Logout complete");
807
+ }
808
+ /**
809
+ * Check if user is authenticated
810
+ */
811
+ async isAuthenticated() {
812
+ return storageService.isAuthenticated();
813
+ }
814
+ /**
815
+ * Get current access token
816
+ */
817
+ async getAccessToken() {
818
+ const credentials = await storageService.getCredentials();
819
+ return credentials?.accessToken || null;
820
+ }
821
+ /**
822
+ * Get current user info
823
+ */
824
+ getUserInfo() {
825
+ return storageService.getUserInfo();
826
+ }
827
+ /**
828
+ * Refresh access token using refresh token
829
+ */
830
+ async refreshToken() {
831
+ logger.debug("Refreshing access token");
832
+ const credentials = await storageService.getCredentials();
833
+ if (!credentials?.refreshToken) {
834
+ throw new AuthenticationError("No refresh token available");
835
+ }
836
+ try {
837
+ const response = await ofetch(
838
+ `${this.apiBaseUrl}${API_ENDPOINTS.AUTH.TOKEN}`,
839
+ {
840
+ method: "POST",
841
+ body: {
842
+ refresh_token: credentials.refreshToken,
843
+ client_id: OAUTH_CONFIG.CLIENT_ID,
844
+ grant_type: "refresh_token"
845
+ },
846
+ headers: {
847
+ "Content-Type": "application/json"
848
+ }
849
+ }
850
+ );
851
+ const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : void 0;
852
+ const newCredentials = {
853
+ accessToken: response.access_token,
854
+ refreshToken: response.refresh_token || credentials.refreshToken,
855
+ expiresAt
856
+ };
857
+ await storageService.setCredentials(newCredentials);
858
+ logger.debug("Token refresh successful");
859
+ } catch (error) {
860
+ logger.debug(`Token refresh failed: ${error.message}`);
861
+ await this.logout();
862
+ throw new AuthenticationError("Token refresh failed. Please log in again.");
863
+ }
864
+ }
865
+ };
866
+ authService = new AuthService();
867
+ }
868
+ });
869
+
870
+ // src/services/api.service.ts
871
+ import { ofetch as ofetch2 } from "ofetch";
872
+ var ApiService, apiService;
873
+ var init_api_service = __esm({
874
+ "src/services/api.service.ts"() {
875
+ "use strict";
876
+ init_esm_shims();
877
+ init_storage_service();
878
+ init_auth_service();
879
+ init_constants();
880
+ init_errors();
881
+ init_logger();
882
+ ApiService = class {
883
+ baseURL;
884
+ client;
885
+ constructor() {
886
+ this.baseURL = process.env.API_BASE_URL || storageService.getConfig("apiBaseUrl");
887
+ this.client = ofetch2.create({
888
+ baseURL: this.baseURL,
889
+ // Request interceptor
890
+ onRequest: async ({ options }) => {
891
+ logger.debug(`API Request: ${options.method} ${options.baseURL}${options.url}`);
892
+ const token = await authService.getAccessToken();
893
+ if (token) {
894
+ options.headers = {
895
+ ...options.headers,
896
+ Authorization: `Bearer ${token}`
897
+ };
898
+ }
899
+ if (options.method && ["POST", "PUT", "PATCH"].includes(options.method)) {
900
+ options.headers = {
901
+ ...options.headers,
902
+ "Content-Type": "application/json"
903
+ };
904
+ }
905
+ },
906
+ // Response interceptor for success
907
+ onResponse: ({ response }) => {
908
+ logger.debug(`API Response: ${response.status} ${response.statusText}`);
909
+ },
910
+ // Response interceptor for errors
911
+ onResponseError: async ({ response, options }) => {
912
+ const status = response.status;
913
+ const url = `${options.baseURL}${options.url}`;
914
+ logger.debug(`API Error: ${status} ${response.statusText} - ${url}`);
915
+ if (status === HTTP_STATUS.UNAUTHORIZED) {
916
+ logger.debug("Token expired, attempting refresh");
917
+ try {
918
+ await authService.refreshToken();
919
+ logger.debug("Token refreshed, retrying request");
920
+ throw new Error("RETRY_WITH_NEW_TOKEN");
921
+ } catch (error) {
922
+ if (error.message === "RETRY_WITH_NEW_TOKEN") {
923
+ throw error;
924
+ }
925
+ throw new TokenExpiredError('Your session has expired. Please run "bragduck init" to login again.');
926
+ }
927
+ }
928
+ let errorMessage = "An unexpected error occurred";
929
+ let errorDetails;
930
+ try {
931
+ const errorData = response._data;
932
+ if (errorData && errorData.message) {
933
+ errorMessage = errorData.message;
934
+ errorDetails = errorData.details;
935
+ }
936
+ } catch {
937
+ errorMessage = response.statusText || errorMessage;
938
+ }
939
+ throw new ApiError(errorMessage, status, errorDetails);
940
+ },
941
+ // Retry configuration
942
+ retry: 2,
943
+ retryDelay: 1e3,
944
+ retryStatusCodes: [408, 409, 425, 429, 500, 502, 503, 504],
945
+ // Timeout
946
+ timeout: 3e4
947
+ // 30 seconds
948
+ });
949
+ }
950
+ /**
951
+ * Make API request with retry for token refresh
952
+ */
953
+ async makeRequest(url, options = {}) {
954
+ try {
955
+ return await this.client(url, options);
956
+ } catch (error) {
957
+ if (error.message === "RETRY_WITH_NEW_TOKEN") {
958
+ logger.debug("Retrying request with refreshed token");
959
+ return await this.client(url, options);
960
+ }
961
+ if (error.name === "FetchError" || error.code === "ECONNREFUSED") {
962
+ throw new NetworkError("Failed to connect to Bragduck API", {
963
+ originalError: error.message,
964
+ baseURL: this.baseURL
965
+ });
966
+ }
967
+ throw error;
968
+ }
969
+ }
970
+ /**
971
+ * Refine commits using AI
972
+ */
973
+ async refineCommits(request) {
974
+ logger.debug(`Refining ${request.commits.length} commits`);
975
+ try {
976
+ const response = await this.makeRequest(
977
+ API_ENDPOINTS.COMMITS.REFINE,
978
+ {
979
+ method: "POST",
980
+ body: request
981
+ }
982
+ );
983
+ logger.debug(`Successfully refined ${response.refined_commits.length} commits`);
984
+ return response;
985
+ } catch (error) {
986
+ logger.debug("Failed to refine commits");
987
+ throw error;
988
+ }
989
+ }
990
+ /**
991
+ * Create brags from refined commits
992
+ */
993
+ async createBrags(request) {
994
+ logger.debug(`Creating ${request.brags.length} brags`);
995
+ try {
996
+ const response = await this.makeRequest(
997
+ API_ENDPOINTS.BRAGS.CREATE,
998
+ {
999
+ method: "POST",
1000
+ body: request
1001
+ }
1002
+ );
1003
+ logger.debug(`Successfully created ${response.created} brags`);
1004
+ return response;
1005
+ } catch (error) {
1006
+ logger.debug("Failed to create brags");
1007
+ throw error;
1008
+ }
1009
+ }
1010
+ /**
1011
+ * List existing brags
1012
+ */
1013
+ async listBrags(params = {}) {
1014
+ const { limit = 50, offset = 0, tags, search } = params;
1015
+ logger.debug(`Listing brags: limit=${limit}, offset=${offset}`);
1016
+ try {
1017
+ const queryParams = new URLSearchParams({
1018
+ limit: limit.toString(),
1019
+ offset: offset.toString()
1020
+ });
1021
+ if (search) {
1022
+ queryParams.append("search", search);
1023
+ }
1024
+ if (tags && tags.length > 0) {
1025
+ tags.forEach((tag) => queryParams.append("tags[]", tag));
1026
+ }
1027
+ const url = `${API_ENDPOINTS.BRAGS.LIST}?${queryParams.toString()}`;
1028
+ const response = await this.makeRequest(url, {
1029
+ method: "GET"
1030
+ });
1031
+ logger.debug(`Successfully fetched ${response.brags.length} brags (total: ${response.total})`);
1032
+ return response;
1033
+ } catch (error) {
1034
+ logger.debug("Failed to list brags");
1035
+ throw error;
1036
+ }
1037
+ }
1038
+ /**
1039
+ * Check for CLI updates
1040
+ */
1041
+ async checkVersion() {
1042
+ logger.debug("Checking for CLI updates");
1043
+ try {
1044
+ const response = await this.makeRequest(
1045
+ API_ENDPOINTS.VERSION,
1046
+ {
1047
+ method: "GET"
1048
+ }
1049
+ );
1050
+ logger.debug(`Latest version: ${response.latest_version}`);
1051
+ return response;
1052
+ } catch (error) {
1053
+ logger.debug("Failed to check version");
1054
+ const { version: version2 } = await Promise.resolve().then(() => (init_version(), version_exports));
1055
+ return {
1056
+ latest_version: version2,
1057
+ critical_update: false
1058
+ };
1059
+ }
1060
+ }
1061
+ /**
1062
+ * Test API connectivity
1063
+ */
1064
+ async testConnection() {
1065
+ try {
1066
+ await this.checkVersion();
1067
+ return true;
1068
+ } catch (error) {
1069
+ return false;
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Update base URL
1074
+ */
1075
+ setBaseURL(url) {
1076
+ this.baseURL = url;
1077
+ storageService.setConfig("apiBaseUrl", url);
1078
+ this.client = ofetch2.create({
1079
+ baseURL: url
1080
+ });
1081
+ }
1082
+ /**
1083
+ * Get current base URL
1084
+ */
1085
+ getBaseURL() {
1086
+ return this.baseURL;
1087
+ }
1088
+ };
1089
+ apiService = new ApiService();
1090
+ }
1091
+ });
1092
+
1093
+ // src/utils/version.ts
1094
+ var version_exports = {};
1095
+ __export(version_exports, {
1096
+ checkForUpdates: () => checkForUpdates,
1097
+ compareVersions: () => compareVersions,
1098
+ formatVersion: () => formatVersion,
1099
+ getCurrentVersion: () => getCurrentVersion,
1100
+ version: () => version
1101
+ });
1102
+ import { readFileSync as readFileSync2 } from "fs";
1103
+ import { fileURLToPath as fileURLToPath3 } from "url";
1104
+ import { dirname as dirname2, join as join3 } from "path";
1105
+ import chalk2 from "chalk";
1106
+ import boxen from "boxen";
1107
+ function getCurrentVersion() {
1108
+ try {
1109
+ const packageJsonPath = join3(__dirname3, "../../package.json");
1110
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
1111
+ return packageJson.version;
1112
+ } catch (error) {
1113
+ logger.debug("Failed to read package.json version");
1114
+ return "1.0.0";
1115
+ }
1116
+ }
1117
+ function compareVersions(v1, v2) {
1118
+ const parts1 = v1.split(".").map((p) => parseInt(p, 10));
1119
+ const parts2 = v2.split(".").map((p) => parseInt(p, 10));
1120
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
1121
+ const part1 = parts1[i] || 0;
1122
+ const part2 = parts2[i] || 0;
1123
+ if (part1 > part2) return 1;
1124
+ if (part1 < part2) return -1;
1125
+ }
1126
+ return 0;
1127
+ }
1128
+ async function checkForUpdates(options = {}) {
1129
+ const { silent = false, force = false } = options;
1130
+ try {
1131
+ if (!force) {
1132
+ const autoVersionCheck = storageService.getConfig("autoVersionCheck");
1133
+ if (!autoVersionCheck) {
1134
+ logger.debug("Version check disabled in config");
1135
+ return {
1136
+ updateAvailable: false,
1137
+ latestVersion: version,
1138
+ currentVersion: version
1139
+ };
1140
+ }
1141
+ }
1142
+ logger.debug("Checking for CLI updates...");
1143
+ const response = await apiService.checkVersion();
1144
+ const latestVersion = response.latest_version;
1145
+ const currentVersion = version;
1146
+ logger.debug(`Current version: ${currentVersion}, Latest version: ${latestVersion}`);
1147
+ const updateAvailable = compareVersions(latestVersion, currentVersion) > 0;
1148
+ if (!silent && updateAvailable) {
1149
+ displayUpdateNotification(currentVersion, latestVersion, response.critical_update);
1150
+ }
1151
+ return {
1152
+ updateAvailable,
1153
+ latestVersion,
1154
+ currentVersion
1155
+ };
1156
+ } catch (error) {
1157
+ logger.debug(`Version check failed: ${error.message}`);
1158
+ return {
1159
+ updateAvailable: false,
1160
+ latestVersion: version,
1161
+ currentVersion: version
1162
+ };
1163
+ }
1164
+ }
1165
+ function displayUpdateNotification(currentVersion, latestVersion, critical = false) {
1166
+ const urgency = critical ? chalk2.red.bold("CRITICAL UPDATE") : chalk2.yellow.bold("Update Available");
1167
+ const message = `${urgency}
1168
+
1169
+ Current version: ${chalk2.dim(currentVersion)}
1170
+ Latest version: ${chalk2.green(latestVersion)}
1171
+
1172
+ Update with: ${chalk2.cyan("npm install -g @bragduck/cli@latest")}`;
1173
+ console.log("");
1174
+ console.log(
1175
+ boxen(message, {
1176
+ padding: 1,
1177
+ margin: { top: 0, right: 1, bottom: 0, left: 1 },
1178
+ borderStyle: "round",
1179
+ borderColor: critical ? "red" : "yellow"
1180
+ })
1181
+ );
1182
+ console.log("");
1183
+ if (critical) {
1184
+ logger.warning("This is a critical update. Please update as soon as possible.");
1185
+ console.log("");
1186
+ }
1187
+ }
1188
+ function formatVersion(includePrefix = true) {
1189
+ const prefix = includePrefix ? "v" : "";
1190
+ return `${prefix}${version}`;
1191
+ }
1192
+ var __filename3, __dirname3, version;
1193
+ var init_version = __esm({
1194
+ "src/utils/version.ts"() {
1195
+ "use strict";
1196
+ init_esm_shims();
1197
+ init_api_service();
1198
+ init_storage_service();
1199
+ init_logger();
1200
+ __filename3 = fileURLToPath3(import.meta.url);
1201
+ __dirname3 = dirname2(__filename3);
1202
+ version = getCurrentVersion();
1203
+ }
1204
+ });
1205
+
1206
+ // src/index.ts
1207
+ init_esm_shims();
1208
+ init_version();
1209
+ export {
1210
+ version
1211
+ };
1212
+ //# sourceMappingURL=index.js.map