@vizzly-testing/cli 0.13.0 → 0.13.2
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/README.md +552 -88
- package/claude-plugin/.claude-plugin/README.md +4 -0
- package/claude-plugin/.mcp.json +4 -0
- package/claude-plugin/CHANGELOG.md +27 -0
- package/claude-plugin/mcp/vizzly-docs-server/README.md +95 -0
- package/claude-plugin/mcp/vizzly-docs-server/docs-fetcher.js +110 -0
- package/claude-plugin/mcp/vizzly-docs-server/index.js +283 -0
- package/claude-plugin/mcp/vizzly-server/cloud-api-provider.js +26 -10
- package/claude-plugin/mcp/vizzly-server/index.js +14 -1
- package/claude-plugin/mcp/vizzly-server/local-tdd-provider.js +61 -28
- package/dist/cli.js +4 -4
- package/dist/commands/run.js +1 -1
- package/dist/commands/tdd-daemon.js +54 -8
- package/dist/commands/tdd.js +8 -8
- package/dist/container/index.js +34 -3
- package/dist/reporter/reporter-bundle.css +1 -1
- package/dist/reporter/reporter-bundle.iife.js +29 -59
- package/dist/server/handlers/tdd-handler.js +28 -63
- package/dist/server/http-server.js +473 -4
- package/dist/services/config-service.js +371 -0
- package/dist/services/project-service.js +245 -0
- package/dist/services/server-manager.js +4 -5
- package/dist/services/static-report-generator.js +208 -0
- package/dist/services/tdd-service.js +14 -6
- package/dist/types/reporter/src/components/ui/form-field.d.ts +16 -0
- package/dist/types/reporter/src/components/views/projects-view.d.ts +1 -0
- package/dist/types/reporter/src/components/views/settings-view.d.ts +1 -0
- package/dist/types/reporter/src/hooks/use-auth.d.ts +10 -0
- package/dist/types/reporter/src/hooks/use-config.d.ts +9 -0
- package/dist/types/reporter/src/hooks/use-projects.d.ts +10 -0
- package/dist/types/reporter/src/services/api-client.d.ts +7 -0
- package/dist/types/server/http-server.d.ts +1 -1
- package/dist/types/services/config-service.d.ts +98 -0
- package/dist/types/services/project-service.d.ts +103 -0
- package/dist/types/services/server-manager.d.ts +2 -1
- package/dist/types/services/static-report-generator.d.ts +25 -0
- package/dist/types/services/tdd-service.d.ts +2 -2
- package/dist/utils/console-ui.js +26 -2
- package/docs/tdd-mode.md +31 -15
- package/package.json +4 -4
|
@@ -1,54 +1,16 @@
|
|
|
1
1
|
import { Buffer } from 'buffer';
|
|
2
2
|
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
3
3
|
import { join, resolve } from 'path';
|
|
4
|
+
import honeydiff from '@vizzly-testing/honeydiff';
|
|
4
5
|
import { createServiceLogger } from '../../utils/logger-factory.js';
|
|
5
6
|
import { TddService } from '../../services/tdd-service.js';
|
|
6
7
|
import { sanitizeScreenshotName, validateScreenshotProperties } from '../../utils/security.js';
|
|
7
8
|
import { detectImageInputType } from '../../utils/image-input-detector.js';
|
|
9
|
+
let {
|
|
10
|
+
getDimensionsSync
|
|
11
|
+
} = honeydiff;
|
|
8
12
|
const logger = createServiceLogger('TDD-HANDLER');
|
|
9
13
|
|
|
10
|
-
/**
|
|
11
|
-
* Detect PNG dimensions by reading the IHDR chunk header
|
|
12
|
-
* PNG spec (ISO/IEC 15948:2004) guarantees width/height at bytes 16-23
|
|
13
|
-
* @param {Buffer} buffer - PNG image buffer
|
|
14
|
-
* @returns {{ width: number, height: number } | null} Dimensions or null if not a valid PNG
|
|
15
|
-
*/
|
|
16
|
-
const detectPNGDimensions = buffer => {
|
|
17
|
-
// Full PNG signature (8 bytes): 89 50 4E 47 0D 0A 1A 0A
|
|
18
|
-
const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
19
|
-
|
|
20
|
-
// Need at least 24 bytes (8 signature + 4 length + 4 type + 8 width/height)
|
|
21
|
-
if (!buffer || buffer.length < 24) {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Validate full 8-byte PNG signature
|
|
26
|
-
for (let i = 0; i < PNG_SIGNATURE.length; i++) {
|
|
27
|
-
if (buffer[i] !== PNG_SIGNATURE[i]) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Validate IHDR chunk type at bytes 12-15 (should be 'IHDR')
|
|
33
|
-
// 0x49484452 = 'IHDR' in ASCII
|
|
34
|
-
if (buffer[12] !== 0x49 || buffer[13] !== 0x48 || buffer[14] !== 0x44 || buffer[15] !== 0x52) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Read width and height from IHDR chunk (guaranteed positions per PNG spec)
|
|
39
|
-
const width = buffer.readUInt32BE(16); // Bytes 16-19
|
|
40
|
-
const height = buffer.readUInt32BE(20); // Bytes 20-23
|
|
41
|
-
|
|
42
|
-
// Sanity check: dimensions should be positive and reasonable
|
|
43
|
-
if (width <= 0 || height <= 0 || width > 65535 || height > 65535) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
return {
|
|
47
|
-
width,
|
|
48
|
-
height
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
|
|
52
14
|
/**
|
|
53
15
|
* Group comparisons by screenshot name with variant structure
|
|
54
16
|
* Matches cloud product's grouping logic from comparison.js
|
|
@@ -349,18 +311,19 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
349
311
|
};
|
|
350
312
|
}
|
|
351
313
|
|
|
352
|
-
// Auto-detect image dimensions
|
|
353
|
-
// This matches cloud API behavior but without requiring Sharp
|
|
314
|
+
// Auto-detect image dimensions if viewport not provided
|
|
354
315
|
if (!extractedProperties.viewport_width || !extractedProperties.viewport_height) {
|
|
355
|
-
|
|
356
|
-
|
|
316
|
+
try {
|
|
317
|
+
const dimensions = getDimensionsSync(imageBuffer);
|
|
357
318
|
if (!extractedProperties.viewport_width) {
|
|
358
319
|
extractedProperties.viewport_width = dimensions.width;
|
|
359
320
|
}
|
|
360
321
|
if (!extractedProperties.viewport_height) {
|
|
361
322
|
extractedProperties.viewport_height = dimensions.height;
|
|
362
323
|
}
|
|
363
|
-
logger.debug(`Auto-detected dimensions
|
|
324
|
+
logger.debug(`Auto-detected dimensions: ${dimensions.width}x${dimensions.height}`);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
logger.debug(`Failed to auto-detect dimensions: ${err.message}`);
|
|
364
327
|
}
|
|
365
328
|
}
|
|
366
329
|
|
|
@@ -464,24 +427,24 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
464
427
|
};
|
|
465
428
|
const acceptBaseline = async comparisonId => {
|
|
466
429
|
try {
|
|
467
|
-
//
|
|
468
|
-
const result = await tddService.acceptBaseline(comparisonId);
|
|
469
|
-
|
|
470
|
-
// Read current report data and update the comparison status
|
|
430
|
+
// Read current report data to get the comparison
|
|
471
431
|
const reportData = readReportData();
|
|
472
432
|
const comparison = reportData.comparisons.find(c => c.id === comparisonId);
|
|
473
|
-
if (comparison) {
|
|
474
|
-
|
|
475
|
-
const updatedComparison = {
|
|
476
|
-
...comparison,
|
|
477
|
-
status: 'passed',
|
|
478
|
-
diffPercentage: 0,
|
|
479
|
-
diff: null
|
|
480
|
-
};
|
|
481
|
-
updateComparison(updatedComparison);
|
|
482
|
-
} else {
|
|
483
|
-
logger.error(`Comparison not found in report data for ID: ${comparisonId}`);
|
|
433
|
+
if (!comparison) {
|
|
434
|
+
throw new Error(`Comparison not found with ID: ${comparisonId}`);
|
|
484
435
|
}
|
|
436
|
+
|
|
437
|
+
// Pass the comparison object to tddService instead of just the ID
|
|
438
|
+
const result = await tddService.acceptBaseline(comparison);
|
|
439
|
+
|
|
440
|
+
// Update the comparison to passed status
|
|
441
|
+
const updatedComparison = {
|
|
442
|
+
...comparison,
|
|
443
|
+
status: 'passed',
|
|
444
|
+
diffPercentage: 0,
|
|
445
|
+
diff: null
|
|
446
|
+
};
|
|
447
|
+
updateComparison(updatedComparison);
|
|
485
448
|
logger.info(`Baseline accepted for comparison ${comparisonId}`);
|
|
486
449
|
return result;
|
|
487
450
|
} catch (error) {
|
|
@@ -498,7 +461,9 @@ export const createTddHandler = (config, workingDir, baselineBuild, baselineComp
|
|
|
498
461
|
// Accept all failed or new comparisons
|
|
499
462
|
for (const comparison of reportData.comparisons) {
|
|
500
463
|
if (comparison.status === 'failed' || comparison.status === 'new') {
|
|
501
|
-
|
|
464
|
+
// Pass the comparison object directly instead of just the ID
|
|
465
|
+
// This allows tddService to work with comparisons from report-data.json
|
|
466
|
+
await tddService.acceptBaseline(comparison);
|
|
502
467
|
|
|
503
468
|
// Update the comparison to passed status
|
|
504
469
|
updateComparison({
|
|
@@ -7,8 +7,13 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
8
|
const PROJECT_ROOT = join(__dirname, '..', '..');
|
|
9
9
|
const logger = createServiceLogger('HTTP-SERVER');
|
|
10
|
-
export const createHttpServer = (port, screenshotHandler) => {
|
|
10
|
+
export const createHttpServer = (port, screenshotHandler, services = {}) => {
|
|
11
11
|
let server = null;
|
|
12
|
+
|
|
13
|
+
// Extract services for config/auth/project management
|
|
14
|
+
let configService = services.configService;
|
|
15
|
+
let authService = services.authService;
|
|
16
|
+
let projectService = services.projectService;
|
|
12
17
|
const parseRequestBody = req => {
|
|
13
18
|
return new Promise((resolve, reject) => {
|
|
14
19
|
let body = '';
|
|
@@ -80,7 +85,7 @@ export const createHttpServer = (port, screenshotHandler) => {
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
// Serve the main React app for all non-API routes
|
|
83
|
-
if (req.method === 'GET' && (parsedUrl.pathname === '/' || parsedUrl.pathname === '/dashboard' || parsedUrl.pathname === '/stats')) {
|
|
88
|
+
if (req.method === 'GET' && (parsedUrl.pathname === '/' || parsedUrl.pathname === '/dashboard' || parsedUrl.pathname === '/stats' || parsedUrl.pathname === '/settings' || parsedUrl.pathname === '/projects')) {
|
|
84
89
|
// Serve React-powered dashboard
|
|
85
90
|
const reportDataPath = join(process.cwd(), '.vizzly', 'report-data.json');
|
|
86
91
|
|
|
@@ -98,7 +103,7 @@ export const createHttpServer = (port, screenshotHandler) => {
|
|
|
98
103
|
<!DOCTYPE html>
|
|
99
104
|
<html>
|
|
100
105
|
<head>
|
|
101
|
-
<title>Vizzly
|
|
106
|
+
<title>Vizzly Dev Dashboard</title>
|
|
102
107
|
<meta charset="utf-8">
|
|
103
108
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
104
109
|
<link rel="stylesheet" href="/reporter-bundle.css">
|
|
@@ -108,7 +113,7 @@ export const createHttpServer = (port, screenshotHandler) => {
|
|
|
108
113
|
<div class="reporter-loading">
|
|
109
114
|
<div>
|
|
110
115
|
<div class="spinner"></div>
|
|
111
|
-
<p>Loading Vizzly
|
|
116
|
+
<p>Loading Vizzly Dev Dashboard...</p>
|
|
112
117
|
</div>
|
|
113
118
|
</div>
|
|
114
119
|
</div>
|
|
@@ -401,6 +406,470 @@ export const createHttpServer = (port, screenshotHandler) => {
|
|
|
401
406
|
}
|
|
402
407
|
return;
|
|
403
408
|
}
|
|
409
|
+
|
|
410
|
+
// ===== CONFIG MANAGEMENT ENDPOINTS =====
|
|
411
|
+
|
|
412
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/config') {
|
|
413
|
+
// Get merged config with sources
|
|
414
|
+
if (!configService) {
|
|
415
|
+
res.statusCode = 503;
|
|
416
|
+
res.end(JSON.stringify({
|
|
417
|
+
error: 'Config service not available'
|
|
418
|
+
}));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const configData = await configService.getConfig('merged');
|
|
423
|
+
res.statusCode = 200;
|
|
424
|
+
res.end(JSON.stringify(configData));
|
|
425
|
+
} catch (error) {
|
|
426
|
+
logger.error('Error fetching config:', error);
|
|
427
|
+
res.statusCode = 500;
|
|
428
|
+
res.end(JSON.stringify({
|
|
429
|
+
error: error.message
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/config/project') {
|
|
435
|
+
// Get project-level config
|
|
436
|
+
if (!configService) {
|
|
437
|
+
res.statusCode = 503;
|
|
438
|
+
res.end(JSON.stringify({
|
|
439
|
+
error: 'Config service not available'
|
|
440
|
+
}));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
const configData = await configService.getConfig('project');
|
|
445
|
+
res.statusCode = 200;
|
|
446
|
+
res.end(JSON.stringify(configData));
|
|
447
|
+
} catch (error) {
|
|
448
|
+
logger.error('Error fetching project config:', error);
|
|
449
|
+
res.statusCode = 500;
|
|
450
|
+
res.end(JSON.stringify({
|
|
451
|
+
error: error.message
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/config/global') {
|
|
457
|
+
// Get global config
|
|
458
|
+
if (!configService) {
|
|
459
|
+
res.statusCode = 503;
|
|
460
|
+
res.end(JSON.stringify({
|
|
461
|
+
error: 'Config service not available'
|
|
462
|
+
}));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
const configData = await configService.getConfig('global');
|
|
467
|
+
res.statusCode = 200;
|
|
468
|
+
res.end(JSON.stringify(configData));
|
|
469
|
+
} catch (error) {
|
|
470
|
+
logger.error('Error fetching global config:', error);
|
|
471
|
+
res.statusCode = 500;
|
|
472
|
+
res.end(JSON.stringify({
|
|
473
|
+
error: error.message
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/config/project') {
|
|
479
|
+
// Update project config
|
|
480
|
+
if (!configService) {
|
|
481
|
+
res.statusCode = 503;
|
|
482
|
+
res.end(JSON.stringify({
|
|
483
|
+
error: 'Config service not available'
|
|
484
|
+
}));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const body = await parseRequestBody(req);
|
|
489
|
+
const result = await configService.updateConfig('project', body);
|
|
490
|
+
res.statusCode = 200;
|
|
491
|
+
res.end(JSON.stringify({
|
|
492
|
+
success: true,
|
|
493
|
+
...result
|
|
494
|
+
}));
|
|
495
|
+
} catch (error) {
|
|
496
|
+
logger.error('Error updating project config:', error);
|
|
497
|
+
res.statusCode = 500;
|
|
498
|
+
res.end(JSON.stringify({
|
|
499
|
+
error: error.message
|
|
500
|
+
}));
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/config/global') {
|
|
505
|
+
// Update global config
|
|
506
|
+
if (!configService) {
|
|
507
|
+
res.statusCode = 503;
|
|
508
|
+
res.end(JSON.stringify({
|
|
509
|
+
error: 'Config service not available'
|
|
510
|
+
}));
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const body = await parseRequestBody(req);
|
|
515
|
+
const result = await configService.updateConfig('global', body);
|
|
516
|
+
res.statusCode = 200;
|
|
517
|
+
res.end(JSON.stringify({
|
|
518
|
+
success: true,
|
|
519
|
+
...result
|
|
520
|
+
}));
|
|
521
|
+
} catch (error) {
|
|
522
|
+
logger.error('Error updating global config:', error);
|
|
523
|
+
res.statusCode = 500;
|
|
524
|
+
res.end(JSON.stringify({
|
|
525
|
+
error: error.message
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/config/validate') {
|
|
531
|
+
// Validate config
|
|
532
|
+
if (!configService) {
|
|
533
|
+
res.statusCode = 503;
|
|
534
|
+
res.end(JSON.stringify({
|
|
535
|
+
error: 'Config service not available'
|
|
536
|
+
}));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const body = await parseRequestBody(req);
|
|
541
|
+
const result = await configService.validateConfig(body);
|
|
542
|
+
res.statusCode = 200;
|
|
543
|
+
res.end(JSON.stringify(result));
|
|
544
|
+
} catch (error) {
|
|
545
|
+
logger.error('Error validating config:', error);
|
|
546
|
+
res.statusCode = 500;
|
|
547
|
+
res.end(JSON.stringify({
|
|
548
|
+
error: error.message
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ===== AUTH ENDPOINTS =====
|
|
555
|
+
|
|
556
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/auth/status') {
|
|
557
|
+
// Get auth status and user info
|
|
558
|
+
if (!authService) {
|
|
559
|
+
res.statusCode = 503;
|
|
560
|
+
res.end(JSON.stringify({
|
|
561
|
+
error: 'Auth service not available'
|
|
562
|
+
}));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
const isAuthenticated = await authService.isAuthenticated();
|
|
567
|
+
let user = null;
|
|
568
|
+
if (isAuthenticated) {
|
|
569
|
+
const whoami = await authService.whoami();
|
|
570
|
+
user = whoami.user;
|
|
571
|
+
}
|
|
572
|
+
res.statusCode = 200;
|
|
573
|
+
res.end(JSON.stringify({
|
|
574
|
+
authenticated: isAuthenticated,
|
|
575
|
+
user
|
|
576
|
+
}));
|
|
577
|
+
} catch (error) {
|
|
578
|
+
logger.error('Error getting auth status:', error);
|
|
579
|
+
res.statusCode = 200;
|
|
580
|
+
res.end(JSON.stringify({
|
|
581
|
+
authenticated: false,
|
|
582
|
+
user: null
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/login') {
|
|
588
|
+
// Initiate device flow login
|
|
589
|
+
if (!authService) {
|
|
590
|
+
res.statusCode = 503;
|
|
591
|
+
res.end(JSON.stringify({
|
|
592
|
+
error: 'Auth service not available'
|
|
593
|
+
}));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
const deviceFlow = await authService.initiateDeviceFlow();
|
|
598
|
+
|
|
599
|
+
// Transform snake_case to camelCase for frontend
|
|
600
|
+
const response = {
|
|
601
|
+
deviceCode: deviceFlow.device_code,
|
|
602
|
+
userCode: deviceFlow.user_code,
|
|
603
|
+
verificationUri: deviceFlow.verification_uri,
|
|
604
|
+
verificationUriComplete: deviceFlow.verification_uri_complete,
|
|
605
|
+
expiresIn: deviceFlow.expires_in,
|
|
606
|
+
interval: deviceFlow.interval
|
|
607
|
+
};
|
|
608
|
+
res.statusCode = 200;
|
|
609
|
+
res.end(JSON.stringify(response));
|
|
610
|
+
} catch (error) {
|
|
611
|
+
logger.error('Error initiating device flow:', error);
|
|
612
|
+
res.statusCode = 500;
|
|
613
|
+
res.end(JSON.stringify({
|
|
614
|
+
error: error.message
|
|
615
|
+
}));
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/poll') {
|
|
620
|
+
// Poll device authorization status
|
|
621
|
+
if (!authService) {
|
|
622
|
+
res.statusCode = 503;
|
|
623
|
+
res.end(JSON.stringify({
|
|
624
|
+
error: 'Auth service not available'
|
|
625
|
+
}));
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const body = await parseRequestBody(req);
|
|
630
|
+
const {
|
|
631
|
+
deviceCode
|
|
632
|
+
} = body;
|
|
633
|
+
if (!deviceCode) {
|
|
634
|
+
res.statusCode = 400;
|
|
635
|
+
res.end(JSON.stringify({
|
|
636
|
+
error: 'deviceCode is required'
|
|
637
|
+
}));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
let result;
|
|
641
|
+
try {
|
|
642
|
+
result = await authService.pollDeviceAuthorization(deviceCode);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
// Handle "Authorization pending" as a valid response
|
|
645
|
+
if (error.message && error.message.includes('Authorization pending')) {
|
|
646
|
+
res.statusCode = 200;
|
|
647
|
+
res.end(JSON.stringify({
|
|
648
|
+
status: 'pending'
|
|
649
|
+
}));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
// Other errors are actual failures
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Check if authorization is complete by looking for tokens
|
|
657
|
+
if (result.tokens && result.tokens.accessToken) {
|
|
658
|
+
// Handle both snake_case and camelCase for token data
|
|
659
|
+
let tokensData = result.tokens;
|
|
660
|
+
let tokenExpiresIn = tokensData.expiresIn || tokensData.expires_in;
|
|
661
|
+
let tokenExpiresAt = tokenExpiresIn ? new Date(Date.now() + tokenExpiresIn * 1000).toISOString() : result.expires_at || result.expiresAt;
|
|
662
|
+
let tokens = {
|
|
663
|
+
accessToken: tokensData.accessToken || tokensData.access_token,
|
|
664
|
+
refreshToken: tokensData.refreshToken || tokensData.refresh_token,
|
|
665
|
+
expiresAt: tokenExpiresAt,
|
|
666
|
+
user: result.user
|
|
667
|
+
};
|
|
668
|
+
await authService.completeDeviceFlow(tokens);
|
|
669
|
+
|
|
670
|
+
// Return a simplified response to the client
|
|
671
|
+
res.statusCode = 200;
|
|
672
|
+
res.end(JSON.stringify({
|
|
673
|
+
status: 'complete',
|
|
674
|
+
user: result.user
|
|
675
|
+
}));
|
|
676
|
+
} else {
|
|
677
|
+
// Still pending or other status
|
|
678
|
+
res.statusCode = 200;
|
|
679
|
+
res.end(JSON.stringify({
|
|
680
|
+
status: 'pending'
|
|
681
|
+
}));
|
|
682
|
+
}
|
|
683
|
+
} catch (error) {
|
|
684
|
+
logger.error('Error polling device authorization:', error);
|
|
685
|
+
res.statusCode = 500;
|
|
686
|
+
res.end(JSON.stringify({
|
|
687
|
+
error: error.message
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/auth/logout') {
|
|
693
|
+
// Logout user
|
|
694
|
+
if (!authService) {
|
|
695
|
+
res.statusCode = 503;
|
|
696
|
+
res.end(JSON.stringify({
|
|
697
|
+
error: 'Auth service not available'
|
|
698
|
+
}));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
await authService.logout();
|
|
703
|
+
res.statusCode = 200;
|
|
704
|
+
res.end(JSON.stringify({
|
|
705
|
+
success: true,
|
|
706
|
+
message: 'Logged out successfully'
|
|
707
|
+
}));
|
|
708
|
+
} catch (error) {
|
|
709
|
+
logger.error('Error logging out:', error);
|
|
710
|
+
res.statusCode = 500;
|
|
711
|
+
res.end(JSON.stringify({
|
|
712
|
+
error: error.message
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ===== PROJECT MANAGEMENT ENDPOINTS =====
|
|
719
|
+
|
|
720
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/projects') {
|
|
721
|
+
// List all projects from API
|
|
722
|
+
if (!projectService) {
|
|
723
|
+
res.statusCode = 503;
|
|
724
|
+
res.end(JSON.stringify({
|
|
725
|
+
error: 'Project service not available'
|
|
726
|
+
}));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
const projects = await projectService.listProjects();
|
|
731
|
+
res.statusCode = 200;
|
|
732
|
+
res.end(JSON.stringify({
|
|
733
|
+
projects
|
|
734
|
+
}));
|
|
735
|
+
} catch (error) {
|
|
736
|
+
logger.error('Error listing projects:', error);
|
|
737
|
+
res.statusCode = 500;
|
|
738
|
+
res.end(JSON.stringify({
|
|
739
|
+
error: error.message
|
|
740
|
+
}));
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/projects/mappings') {
|
|
745
|
+
// List project directory mappings
|
|
746
|
+
if (!projectService) {
|
|
747
|
+
res.statusCode = 503;
|
|
748
|
+
res.end(JSON.stringify({
|
|
749
|
+
error: 'Project service not available'
|
|
750
|
+
}));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
const mappings = await projectService.listMappings();
|
|
755
|
+
res.statusCode = 200;
|
|
756
|
+
res.end(JSON.stringify({
|
|
757
|
+
mappings
|
|
758
|
+
}));
|
|
759
|
+
} catch (error) {
|
|
760
|
+
logger.error('Error listing project mappings:', error);
|
|
761
|
+
res.statusCode = 500;
|
|
762
|
+
res.end(JSON.stringify({
|
|
763
|
+
error: error.message
|
|
764
|
+
}));
|
|
765
|
+
}
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (req.method === 'POST' && parsedUrl.pathname === '/api/projects/mappings') {
|
|
769
|
+
// Create or update project mapping
|
|
770
|
+
if (!projectService) {
|
|
771
|
+
res.statusCode = 503;
|
|
772
|
+
res.end(JSON.stringify({
|
|
773
|
+
error: 'Project service not available'
|
|
774
|
+
}));
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
const body = await parseRequestBody(req);
|
|
779
|
+
const {
|
|
780
|
+
directory,
|
|
781
|
+
projectSlug,
|
|
782
|
+
organizationSlug,
|
|
783
|
+
token,
|
|
784
|
+
projectName
|
|
785
|
+
} = body;
|
|
786
|
+
const mapping = await projectService.createMapping(directory, {
|
|
787
|
+
projectSlug,
|
|
788
|
+
organizationSlug,
|
|
789
|
+
token,
|
|
790
|
+
projectName
|
|
791
|
+
});
|
|
792
|
+
res.statusCode = 200;
|
|
793
|
+
res.end(JSON.stringify({
|
|
794
|
+
success: true,
|
|
795
|
+
mapping
|
|
796
|
+
}));
|
|
797
|
+
} catch (error) {
|
|
798
|
+
logger.error('Error creating project mapping:', error);
|
|
799
|
+
res.statusCode = 500;
|
|
800
|
+
res.end(JSON.stringify({
|
|
801
|
+
error: error.message
|
|
802
|
+
}));
|
|
803
|
+
}
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (req.method === 'DELETE' && parsedUrl.pathname.startsWith('/api/projects/mappings/')) {
|
|
807
|
+
// Delete project mapping
|
|
808
|
+
if (!projectService) {
|
|
809
|
+
res.statusCode = 503;
|
|
810
|
+
res.end(JSON.stringify({
|
|
811
|
+
error: 'Project service not available'
|
|
812
|
+
}));
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const directory = decodeURIComponent(parsedUrl.pathname.replace('/api/projects/mappings/', ''));
|
|
817
|
+
await projectService.removeMapping(directory);
|
|
818
|
+
res.statusCode = 200;
|
|
819
|
+
res.end(JSON.stringify({
|
|
820
|
+
success: true,
|
|
821
|
+
message: 'Mapping deleted'
|
|
822
|
+
}));
|
|
823
|
+
} catch (error) {
|
|
824
|
+
logger.error('Error deleting project mapping:', error);
|
|
825
|
+
res.statusCode = 500;
|
|
826
|
+
res.end(JSON.stringify({
|
|
827
|
+
error: error.message
|
|
828
|
+
}));
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (req.method === 'GET' && parsedUrl.pathname === '/api/builds/recent') {
|
|
833
|
+
// Get recent builds for current project
|
|
834
|
+
if (!projectService || !configService) {
|
|
835
|
+
res.statusCode = 503;
|
|
836
|
+
res.end(JSON.stringify({
|
|
837
|
+
error: 'Required services not available'
|
|
838
|
+
}));
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
try {
|
|
842
|
+
const config = await configService.getConfig('merged');
|
|
843
|
+
const {
|
|
844
|
+
projectSlug,
|
|
845
|
+
organizationSlug
|
|
846
|
+
} = config.config;
|
|
847
|
+
if (!projectSlug || !organizationSlug) {
|
|
848
|
+
res.statusCode = 400;
|
|
849
|
+
res.end(JSON.stringify({
|
|
850
|
+
error: 'No project configured for this directory'
|
|
851
|
+
}));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const limit = parseInt(parsedUrl.searchParams.get('limit') || '10', 10);
|
|
855
|
+
const branch = parsedUrl.searchParams.get('branch') || undefined;
|
|
856
|
+
const builds = await projectService.getRecentBuilds(projectSlug, organizationSlug, {
|
|
857
|
+
limit,
|
|
858
|
+
branch
|
|
859
|
+
});
|
|
860
|
+
res.statusCode = 200;
|
|
861
|
+
res.end(JSON.stringify({
|
|
862
|
+
builds
|
|
863
|
+
}));
|
|
864
|
+
} catch (error) {
|
|
865
|
+
logger.error('Error fetching recent builds:', error);
|
|
866
|
+
res.statusCode = 500;
|
|
867
|
+
res.end(JSON.stringify({
|
|
868
|
+
error: error.message
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
404
873
|
res.statusCode = 404;
|
|
405
874
|
res.end(JSON.stringify({
|
|
406
875
|
error: 'Not found'
|