@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
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
|