@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/LICENSE +21 -0
- package/README.md +494 -0
- package/dist/bin/bragduck.d.ts +2 -0
- package/dist/bin/bragduck.js +2346 -0
- package/dist/bin/bragduck.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1212 -0
- package/dist/index.js.map +1 -0
- package/package.json +93 -0
|
@@ -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
|