@membranehq/cli 0.1.1

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.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +9 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +54 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +335 -0
  6. package/package.json +46 -0
  7. package/scripts/add-shebang.sh +6 -0
  8. package/scripts/prepare-package-json.ts +29 -0
  9. package/src/agent.tsx +50 -0
  10. package/src/cli.ts +72 -0
  11. package/src/commands/open.command.ts +51 -0
  12. package/src/commands/pull.command.ts +75 -0
  13. package/src/commands/push.command.ts +79 -0
  14. package/src/commands/test.command.ts +99 -0
  15. package/src/components/AddMcpServerScreen.tsx +215 -0
  16. package/src/components/AgentStatus.tsx +15 -0
  17. package/src/components/Main.tsx +64 -0
  18. package/src/components/OverviewSection.tsx +24 -0
  19. package/src/components/PersonalAccessTokenInput.tsx +56 -0
  20. package/src/components/RecentChanges.tsx +65 -0
  21. package/src/components/SelectWorkspace.tsx +112 -0
  22. package/src/components/Setup.tsx +121 -0
  23. package/src/components/WorkspaceStatus.tsx +61 -0
  24. package/src/contexts/FileWatcherContext.tsx +81 -0
  25. package/src/index.ts +27 -0
  26. package/src/legacy/commands/pullWorkspace.ts +70 -0
  27. package/src/legacy/commands/pushWorkspace.ts +246 -0
  28. package/src/legacy/integrationElements.ts +78 -0
  29. package/src/legacy/push/types.ts +17 -0
  30. package/src/legacy/reader/index.ts +113 -0
  31. package/src/legacy/types.ts +17 -0
  32. package/src/legacy/util.ts +149 -0
  33. package/src/legacy/workspace-elements/connectors.ts +397 -0
  34. package/src/legacy/workspace-elements/index.ts +27 -0
  35. package/src/legacy/workspace-tools/commands/pullWorkspace.ts +70 -0
  36. package/src/legacy/workspace-tools/integrationElements.ts +78 -0
  37. package/src/legacy/workspace-tools/util.ts +149 -0
  38. package/src/mcp/server-status.ts +27 -0
  39. package/src/mcp/server.ts +36 -0
  40. package/src/mcp/tools/getTestAccessToken.ts +32 -0
  41. package/src/modules/api/account-api-client.ts +89 -0
  42. package/src/modules/api/index.ts +3 -0
  43. package/src/modules/api/membrane-api-client.ts +116 -0
  44. package/src/modules/api/workspace-api-client.ts +11 -0
  45. package/src/modules/config/cwd-context.tsx +11 -0
  46. package/src/modules/config/project/getAgentVersion.ts +16 -0
  47. package/src/modules/config/project/index.ts +8 -0
  48. package/src/modules/config/project/paths.ts +25 -0
  49. package/src/modules/config/project/readProjectConfig.ts +27 -0
  50. package/src/modules/config/project/useProjectConfig.tsx +103 -0
  51. package/src/modules/config/system/index.ts +35 -0
  52. package/src/modules/file-watcher/index.ts +166 -0
  53. package/src/modules/file-watcher/types.ts +14 -0
  54. package/src/modules/setup/steps.ts +9 -0
  55. package/src/modules/setup/useSetup.ts +16 -0
  56. package/src/modules/status/useStatus.ts +16 -0
  57. package/src/modules/workspace-element-service/constants.ts +121 -0
  58. package/src/modules/workspace-element-service/getTypeAndKeyFromPath.ts +69 -0
  59. package/src/modules/workspace-element-service/index.ts +304 -0
  60. package/src/testing/environment.ts +172 -0
  61. package/src/testing/runners/base.runner.ts +27 -0
  62. package/src/testing/runners/test.runner.ts +123 -0
  63. package/src/testing/scripts/generate-test-report.ts +757 -0
  64. package/src/testing/test-suites/base.ts +92 -0
  65. package/src/testing/test-suites/data-collection.ts +128 -0
  66. package/src/testing/testers/base.ts +115 -0
  67. package/src/testing/testers/create.ts +273 -0
  68. package/src/testing/testers/delete.ts +155 -0
  69. package/src/testing/testers/find-by-id.ts +135 -0
  70. package/src/testing/testers/list.ts +110 -0
  71. package/src/testing/testers/match.ts +149 -0
  72. package/src/testing/testers/search.ts +148 -0
  73. package/src/testing/testers/spec.ts +30 -0
  74. package/src/testing/testers/update.ts +284 -0
  75. package/src/utils/auth.ts +19 -0
  76. package/src/utils/constants.ts +27 -0
  77. package/src/utils/fields.ts +83 -0
  78. package/src/utils/logger.ts +106 -0
  79. package/src/utils/templating.ts +50 -0
  80. package/tsconfig.json +21 -0
@@ -0,0 +1,757 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+
4
+ import yaml from 'js-yaml'
5
+
6
+ interface TestNode {
7
+ name: string
8
+ path: string
9
+ content?: string
10
+ parsedContent?: unknown
11
+ children: TestNode[]
12
+ }
13
+
14
+ interface TestResult {
15
+ logs: any[]
16
+ assertions: TestAssertion[]
17
+ error?: any
18
+ }
19
+
20
+ interface TestAssertion {
21
+ message: string
22
+ result: boolean
23
+ details?: any
24
+ }
25
+
26
+ interface TestSummary {
27
+ totalDataCollections: number
28
+ fullyFunctional: number
29
+ partiallyFunctional: number
30
+ failed: number
31
+ untested: number
32
+ // Test-level statistics
33
+ totalTests: number
34
+ successfulTests: number
35
+ warningTests: number
36
+ errorTests: number
37
+ untestedTests: number
38
+ }
39
+
40
+ function calculateTestStatus(node: TestNode): 'success' | 'warning' | 'error' | 'untested' {
41
+ if (!node.parsedContent || typeof node.parsedContent !== 'object') {
42
+ return 'untested'
43
+ }
44
+
45
+ const parsed = node.parsedContent as any
46
+ const result = parsed.result as TestResult | undefined
47
+
48
+ if (!result) {
49
+ return 'untested'
50
+ }
51
+
52
+ if (result.error) {
53
+ return 'error'
54
+ }
55
+
56
+ if (!result.assertions || result.assertions.length === 0) {
57
+ return 'untested'
58
+ }
59
+
60
+ const hasFailures = result.assertions.some((assertion: TestAssertion) => !assertion.result)
61
+ const allPassed = result.assertions.every((assertion: TestAssertion) => assertion.result)
62
+
63
+ if (allPassed) {
64
+ return 'success'
65
+ } else if (hasFailures) {
66
+ return 'warning'
67
+ }
68
+
69
+ return 'untested'
70
+ }
71
+
72
+ function calculateDataCollectionSummary(nodes: TestNode[]): TestSummary {
73
+ let totalDataCollections = 0
74
+ let fullyFunctional = 0
75
+ let partiallyFunctional = 0
76
+ let failed = 0
77
+ let untested = 0
78
+ let totalTests = 0
79
+ let successfulTests = 0
80
+ let warningTests = 0
81
+ let errorTests = 0
82
+ let untestedTests = 0
83
+
84
+ function processNodes(nodeList: TestNode[]) {
85
+ for (const node of nodeList) {
86
+ if (node.children.length > 0) {
87
+ // Check if this is a data collection folder (has test files)
88
+ const hasTestFiles = node.children.some(
89
+ (child) => child.name.endsWith('.test.yml') || child.name.endsWith('.test.yaml'),
90
+ )
91
+
92
+ if (hasTestFiles) {
93
+ totalDataCollections++
94
+
95
+ const testResults = node.children
96
+ .filter((child) => child.name.endsWith('.test.yml') || child.name.endsWith('.test.yaml'))
97
+ .map((child) => calculateTestStatus(child))
98
+
99
+ totalTests += testResults.length
100
+ const successCount = testResults.filter((status) => status === 'success').length
101
+ const errorCount = testResults.filter((status) => status === 'error').length
102
+ const warningCount = testResults.filter((status) => status === 'warning').length
103
+ const untestedCount = testResults.filter((status) => status === 'untested').length
104
+
105
+ successfulTests += successCount
106
+ errorTests += errorCount
107
+ warningTests += warningCount
108
+ untestedTests += untestedCount
109
+
110
+ if (errorCount > 0) {
111
+ failed++
112
+ } else if (untestedCount > 0) {
113
+ untested++
114
+ } else if (warningCount > 0) {
115
+ partiallyFunctional++
116
+ } else if (successCount > 0) {
117
+ fullyFunctional++
118
+ } else {
119
+ untested++
120
+ }
121
+ }
122
+
123
+ processNodes(node.children)
124
+ }
125
+ }
126
+ }
127
+
128
+ processNodes(nodes)
129
+
130
+ return {
131
+ totalDataCollections,
132
+ fullyFunctional,
133
+ partiallyFunctional,
134
+ failed,
135
+ untested,
136
+ totalTests,
137
+ successfulTests,
138
+ warningTests,
139
+ errorTests,
140
+ untestedTests,
141
+ }
142
+ }
143
+
144
+ function buildTestTree(dir: string, basePath: string = ''): TestNode[] {
145
+ const nodes: TestNode[] = []
146
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
147
+
148
+ for (const entry of entries) {
149
+ const fullPath = path.join(dir, entry.name)
150
+ const relativePath = path.join(basePath, entry.name)
151
+
152
+ if (entry.isDirectory()) {
153
+ nodes.push({
154
+ name: entry.name,
155
+ path: relativePath,
156
+ children: buildTestTree(fullPath, relativePath),
157
+ })
158
+ } else if (entry.name.endsWith('.yaml') || entry.name.endsWith('.yml')) {
159
+ const content = fs.readFileSync(fullPath, 'utf8')
160
+ let parsedContent: unknown
161
+ try {
162
+ parsedContent = yaml.load(content)
163
+ console.warn(`Successfully loaded YAML file: ${fullPath}`)
164
+ } catch (error) {
165
+ console.error(`Error parsing YAML file ${fullPath}:`, error)
166
+ }
167
+ nodes.push({
168
+ name: entry.name,
169
+ path: relativePath,
170
+ content,
171
+ parsedContent,
172
+ children: [],
173
+ })
174
+ }
175
+ }
176
+
177
+ return nodes.sort((a, b) => {
178
+ // Directories first, then files
179
+ if (a.children.length > 0 && b.children.length === 0) return -1
180
+ if (a.children.length === 0 && b.children.length > 0) return 1
181
+ return a.name.localeCompare(b.name)
182
+ })
183
+ }
184
+
185
+ let folderIdCounter = 0
186
+
187
+ function getStatusIcon(status: string): string {
188
+ switch (status) {
189
+ case 'success':
190
+ return '✅'
191
+ case 'warning':
192
+ return '⚠️'
193
+ case 'error':
194
+ return '❌'
195
+ case 'untested':
196
+ return '⭕'
197
+ default:
198
+ return '📄'
199
+ }
200
+ }
201
+
202
+ function getStatusClass(status: string): string {
203
+ switch (status) {
204
+ case 'success':
205
+ return 'status-success'
206
+ case 'warning':
207
+ return 'status-warning'
208
+ case 'error':
209
+ return 'status-error'
210
+ case 'untested':
211
+ return 'status-untested'
212
+ default:
213
+ return ''
214
+ }
215
+ }
216
+
217
+ function generateTreeHtml(nodes: TestNode[], parentId = 'root', depth = 0): string {
218
+ return nodes
219
+ .map((node) => {
220
+ const hasChildren = node.children.length > 0
221
+ const isYaml = node.name.endsWith('.yaml') || node.name.endsWith('.yml')
222
+ const isTestYaml = node.name.endsWith('.test.yaml') || node.name.endsWith('.test.yml')
223
+ let content = ''
224
+ let folderId = ''
225
+ if (hasChildren) {
226
+ folderId = `folder-${parentId}-${folderIdCounter++}`
227
+ content = `
228
+ <div id="${folderId}" class="folder-children" style="display: none;">
229
+ ${generateTreeHtml(node.children, folderId, depth + 1)}
230
+ </div>
231
+ `
232
+ }
233
+ let displayName = node.name
234
+ let icon = ''
235
+ let statusClass = ''
236
+
237
+ if (hasChildren) {
238
+ icon = '📁'
239
+ } else if (isTestYaml) {
240
+ displayName = node.name.replace(/\.test\.ya?ml$/, '')
241
+ const status = calculateTestStatus(node)
242
+ icon = getStatusIcon(status)
243
+ statusClass = getStatusClass(status)
244
+ } else if (isYaml) {
245
+ icon = '📄'
246
+ }
247
+
248
+ const chevron = hasChildren ? `<span class="folder-chevron" data-target="${folderId}">▶</span>` : ''
249
+ const className = hasChildren
250
+ ? 'tree-node folder-node'
251
+ : isYaml
252
+ ? `tree-node yaml-file ${statusClass}`
253
+ : 'tree-node'
254
+ const dataContent = isYaml && node.content ? `data-content="${escapeHtml(node.content)}"` : ''
255
+
256
+ return `
257
+ <div class="${className}" ${dataContent}>
258
+ <span class="tree-line" style="padding-left: ${depth * 1.25}rem">
259
+ ${chevron} ${icon} <span class="file-name">${displayName}</span>
260
+ </span>
261
+ ${content}
262
+ </div>
263
+ `
264
+ })
265
+ .join('')
266
+ }
267
+
268
+ function generateSummaryHtml(summary: TestSummary): string {
269
+ const total = summary.totalDataCollections
270
+ const successPercent = total > 0 ? Math.round((summary.fullyFunctional / total) * 100) : 0
271
+ const warningPercent = total > 0 ? Math.round((summary.partiallyFunctional / total) * 100) : 0
272
+ const errorPercent = total > 0 ? Math.round((summary.failed / total) * 100) : 0
273
+ const untestedPercent = total > 0 ? Math.round((summary.untested / total) * 100) : 0
274
+
275
+ const testTotal = summary.totalTests
276
+ const testSuccessPercent = testTotal > 0 ? Math.round((summary.successfulTests / testTotal) * 100) : 0
277
+ const testWarningPercent = testTotal > 0 ? Math.round((summary.warningTests / testTotal) * 100) : 0
278
+ const testErrorPercent = testTotal > 0 ? Math.round((summary.errorTests / testTotal) * 100) : 0
279
+ const testUntestedPercent = testTotal > 0 ? Math.round((summary.untestedTests / testTotal) * 100) : 0
280
+
281
+ return `
282
+ <div class="summary-dashboard">
283
+ <h2 class="summary-title">Test Summary</h2>
284
+
285
+ <h3 class="summary-subtitle">Data Collections Overview</h3>
286
+ <div class="summary-grid">
287
+ <div class="summary-card total">
288
+ <div class="summary-number">${total}</div>
289
+ <div class="summary-label">Total Data Collections</div>
290
+ </div>
291
+ <div class="summary-card success">
292
+ <div class="summary-number">${summary.fullyFunctional}</div>
293
+ <div class="summary-label">Fully Functional (${successPercent}%)</div>
294
+ </div>
295
+ <div class="summary-card warning">
296
+ <div class="summary-number">${summary.partiallyFunctional}</div>
297
+ <div class="summary-label">Partially Functional (${warningPercent}%)</div>
298
+ </div>
299
+ <div class="summary-card error">
300
+ <div class="summary-number">${summary.failed}</div>
301
+ <div class="summary-label">Failed (${errorPercent}%)</div>
302
+ </div>
303
+ <div class="summary-card untested">
304
+ <div class="summary-number">${summary.untested}</div>
305
+ <div class="summary-label">Untested (${untestedPercent}%)</div>
306
+ </div>
307
+ </div>
308
+
309
+ <h3 class="summary-subtitle">Individual Tests Breakdown</h3>
310
+ <div class="summary-grid">
311
+ <div class="summary-card total">
312
+ <div class="summary-number">${testTotal}</div>
313
+ <div class="summary-label">Total Tests</div>
314
+ </div>
315
+ <div class="summary-card success">
316
+ <div class="summary-number">${summary.successfulTests}</div>
317
+ <div class="summary-label">Successful (${testSuccessPercent}%)</div>
318
+ </div>
319
+ <div class="summary-card warning">
320
+ <div class="summary-number">${summary.warningTests}</div>
321
+ <div class="summary-label">Warnings (${testWarningPercent}%)</div>
322
+ </div>
323
+ <div class="summary-card error">
324
+ <div class="summary-number">${summary.errorTests}</div>
325
+ <div class="summary-label">Errors (${testErrorPercent}%)</div>
326
+ </div>
327
+ <div class="summary-card untested">
328
+ <div class="summary-number">${summary.untestedTests}</div>
329
+ <div class="summary-label">Untested (${testUntestedPercent}%)</div>
330
+ </div>
331
+ </div>
332
+ <div class="legend">
333
+ <h3>Status Legend</h3>
334
+ <div class="legend-items">
335
+ <div class="legend-item">
336
+ <span class="legend-icon">✅</span>
337
+ <span>Fully Functional - All tests pass, operation works correctly</span>
338
+ </div>
339
+ <div class="legend-item">
340
+ <span class="legend-icon">⚠️</span>
341
+ <span>Partially Functional - Some tests fail, may have issues</span>
342
+ </div>
343
+ <div class="legend-item">
344
+ <span class="legend-icon">❌</span>
345
+ <span>Failed - Tests failed or errors occurred</span>
346
+ </div>
347
+ <div class="legend-item">
348
+ <span class="legend-icon">⭕</span>
349
+ <span>Untested - No test results available</span>
350
+ </div>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ `
355
+ }
356
+
357
+ function generateHtml(nodes: TestNode[]): string {
358
+ folderIdCounter = 0
359
+ const summary = calculateDataCollectionSummary(nodes)
360
+
361
+ const html = `
362
+ <!DOCTYPE html>
363
+ <html lang="en">
364
+ <head>
365
+ <meta charset="UTF-8">
366
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
367
+ <title>Integration Tests Report</title>
368
+ <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
369
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
370
+ <style>
371
+ .tree-node {
372
+ padding: 0.25rem 0;
373
+ }
374
+ .tree-line {
375
+ display: block;
376
+ width: 100%;
377
+ box-sizing: border-box;
378
+ padding: 0.25rem 0.5rem;
379
+ border-radius: 0.25rem;
380
+ transition: background 0.2s;
381
+ }
382
+ .tree-line:hover {
383
+ background-color: #f3f4f6;
384
+ }
385
+ .folder-node {
386
+ font-weight: 500;
387
+ }
388
+ .folder-chevron {
389
+ cursor: pointer;
390
+ user-select: none;
391
+ display: inline-block;
392
+ width: 1.5rem;
393
+ text-align: center;
394
+ }
395
+ .folder-chevron:hover {
396
+ color: #2563eb;
397
+ }
398
+ .yaml-file {
399
+ cursor: pointer;
400
+ }
401
+ .file-name {
402
+ margin-left: 0.25rem;
403
+ }
404
+
405
+ /* Status-based styling */
406
+ .status-success .tree-line {
407
+ background-color: #f0fdf4;
408
+ border-left: 4px solid #22c55e;
409
+ }
410
+ .status-warning .tree-line {
411
+ background-color: #fefce8;
412
+ border-left: 4px solid #eab308;
413
+ }
414
+ .status-error .tree-line {
415
+ background-color: #fef2f2;
416
+ border-left: 4px solid #ef4444;
417
+ }
418
+ .status-untested .tree-line {
419
+ background-color: #f8fafc;
420
+ border-left: 4px solid #94a3b8;
421
+ }
422
+
423
+ /* Summary Dashboard */
424
+ .summary-dashboard {
425
+ background: white;
426
+ border-radius: 0.5rem;
427
+ padding: 2rem;
428
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
429
+ margin-bottom: 2rem;
430
+ }
431
+ .summary-title {
432
+ font-size: 1.5rem;
433
+ font-weight: 600;
434
+ margin-bottom: 1.5rem;
435
+ color: #1f2937;
436
+ }
437
+ .summary-subtitle {
438
+ font-size: 1.25rem;
439
+ font-weight: 600;
440
+ margin: 1.5rem 0 1rem 0;
441
+ color: #374151;
442
+ border-bottom: 2px solid #e5e7eb;
443
+ padding-bottom: 0.5rem;
444
+ }
445
+ .summary-grid {
446
+ display: grid;
447
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
448
+ gap: 1rem;
449
+ margin-bottom: 2rem;
450
+ }
451
+ .summary-card {
452
+ padding: 1.5rem;
453
+ border-radius: 0.5rem;
454
+ text-align: center;
455
+ }
456
+ .summary-card.total {
457
+ background-color: #f8fafc;
458
+ border: 2px solid #e2e8f0;
459
+ }
460
+ .summary-card.success {
461
+ background-color: #f0fdf4;
462
+ border: 2px solid #22c55e;
463
+ }
464
+ .summary-card.warning {
465
+ background-color: #fefce8;
466
+ border: 2px solid #eab308;
467
+ }
468
+ .summary-card.error {
469
+ background-color: #fef2f2;
470
+ border: 2px solid #ef4444;
471
+ }
472
+ .summary-card.untested {
473
+ background-color: #f1f5f9;
474
+ border: 2px solid #94a3b8;
475
+ }
476
+ .summary-number {
477
+ font-size: 2rem;
478
+ font-weight: 700;
479
+ margin-bottom: 0.5rem;
480
+ }
481
+ .summary-label {
482
+ font-size: 0.875rem;
483
+ font-weight: 500;
484
+ opacity: 0.8;
485
+ }
486
+
487
+ /* Legend */
488
+ .legend {
489
+ border-top: 1px solid #e2e8f0;
490
+ padding-top: 1.5rem;
491
+ }
492
+ .legend h3 {
493
+ font-size: 1.125rem;
494
+ font-weight: 600;
495
+ margin-bottom: 1rem;
496
+ color: #374151;
497
+ }
498
+ .legend-items {
499
+ display: grid;
500
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
501
+ gap: 0.75rem;
502
+ }
503
+ .legend-item {
504
+ display: flex;
505
+ align-items: center;
506
+ gap: 0.75rem;
507
+ }
508
+ .legend-icon {
509
+ font-size: 1.25rem;
510
+ }
511
+
512
+ .modal {
513
+ display: none;
514
+ position: fixed;
515
+ top: 0;
516
+ left: 0;
517
+ width: 100%;
518
+ height: 100%;
519
+ background-color: rgba(0, 0, 0, 0.5);
520
+ z-index: 1000;
521
+ }
522
+ .modal-content {
523
+ position: relative;
524
+ background-color: #2d2d2d;
525
+ margin: 5% auto;
526
+ padding: 1.5rem;
527
+ width: 80%;
528
+ max-width: 1000px;
529
+ max-height: 80vh;
530
+ border-radius: 0.5rem;
531
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
532
+ overflow: auto;
533
+ }
534
+ .modal-header {
535
+ display: flex;
536
+ justify-content: space-between;
537
+ align-items: center;
538
+ margin-bottom: 1rem;
539
+ padding-bottom: 0.5rem;
540
+ border-bottom: 1px solid #4a4a4a;
541
+ }
542
+ .modal-title {
543
+ color: #e6e6e6;
544
+ font-size: 1.25rem;
545
+ font-weight: 600;
546
+ }
547
+ .modal-close {
548
+ color: #e6e6e6;
549
+ font-size: 1.5rem;
550
+ cursor: pointer;
551
+ padding: 0.25rem;
552
+ }
553
+ .modal-close:hover {
554
+ color: #fff;
555
+ }
556
+ .modal-body {
557
+ background-color: #2d2d2d;
558
+ border-radius: 0.25rem;
559
+ overflow: auto;
560
+ }
561
+ .modal-body pre {
562
+ margin: 0;
563
+ padding: 0;
564
+ }
565
+ .modal-body code {
566
+ font-family: 'Fira Code', monospace;
567
+ font-size: 0.9rem;
568
+ line-height: 1.5;
569
+ }
570
+ .tree-controls {
571
+ display: flex;
572
+ gap: 1rem;
573
+ margin-bottom: 1.5rem;
574
+ }
575
+ .tree-btn {
576
+ background: #2563eb;
577
+ color: #fff;
578
+ border: none;
579
+ border-radius: 0.375rem;
580
+ padding: 0.5rem 1.25rem;
581
+ font-size: 1rem;
582
+ font-weight: 500;
583
+ cursor: pointer;
584
+ transition: background 0.2s;
585
+ }
586
+ .tree-btn:hover {
587
+ background: #1d4ed8;
588
+ }
589
+ </style>
590
+ </head>
591
+ <body class="bg-gray-50 min-h-screen p-8">
592
+ <div class="max-w-7xl mx-auto">
593
+ <h1 class="text-3xl font-bold mb-8 text-gray-800">Integration Tests Report</h1>
594
+
595
+ ${generateSummaryHtml(summary)}
596
+
597
+ <div class="bg-white rounded-lg shadow-lg p-6">
598
+ <h2 class="text-xl font-semibold mb-4 text-gray-800">Detailed Test Results</h2>
599
+ <div class="tree-controls">
600
+ <button id="expandAllBtn" class="tree-btn">Expand All</button>
601
+ <button id="collapseAllBtn" class="tree-btn">Collapse All</button>
602
+ </div>
603
+ <div id="folder-root" class="folder-children" style="display: block;">
604
+ ${generateTreeHtml(nodes, 'root')}
605
+ </div>
606
+ </div>
607
+ </div>
608
+
609
+ <!-- Modal -->
610
+ <div id="yamlModal" class="modal">
611
+ <div class="modal-content">
612
+ <div class="modal-header">
613
+ <h2 class="modal-title"></h2>
614
+ <span class="modal-close">&times;</span>
615
+ </div>
616
+ <div class="modal-body">
617
+ <pre><code class="language-yaml"></code></pre>
618
+ </div>
619
+ </div>
620
+ </div>
621
+
622
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
623
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
624
+ <script>
625
+ const modal = document.getElementById('yamlModal');
626
+ const modalTitle = modal.querySelector('.modal-title');
627
+ const modalCode = modal.querySelector('code');
628
+ const modalClose = modal.querySelector('.modal-close');
629
+
630
+ function showYamlContent(fileName, content) {
631
+ modalTitle.textContent = fileName;
632
+ modalCode.textContent = content;
633
+ modal.style.display = 'block';
634
+ Prism.highlightElement(modalCode);
635
+ }
636
+
637
+ function closeModal() {
638
+ modal.style.display = 'none';
639
+ }
640
+
641
+ // Folder toggle logic
642
+ function toggleFolder(chevron, children) {
643
+ if (!chevron || !children) return;
644
+ const isOpen = children.style.display === 'block';
645
+ children.style.display = isOpen ? 'none' : 'block';
646
+ chevron.textContent = isOpen ? '▶' : '▼';
647
+ }
648
+
649
+ // Close modal when clicking the close button or outside the modal
650
+ modalClose.addEventListener('click', closeModal);
651
+ window.addEventListener('click', (e) => {
652
+ if (e.target === modal) {
653
+ closeModal();
654
+ }
655
+ });
656
+
657
+ // Close modal with Escape key
658
+ document.addEventListener('keydown', (e) => {
659
+ if (e.key === 'Escape') {
660
+ closeModal();
661
+ }
662
+ });
663
+
664
+ document.addEventListener('DOMContentLoaded', () => {
665
+ // Folder chevrons
666
+ document.querySelectorAll('.folder-chevron').forEach(chevron => {
667
+ chevron.addEventListener('click', (e) => {
668
+ e.stopPropagation();
669
+ const targetId = chevron.getAttribute('data-target');
670
+ const children = document.getElementById(targetId);
671
+ toggleFolder(chevron, children);
672
+ });
673
+ });
674
+ // Folder line click (expand/collapse)
675
+ document.querySelectorAll('.folder-node .tree-line').forEach(line => {
676
+ line.addEventListener('click', (e) => {
677
+ // Prevent double toggle if chevron was clicked
678
+ if ((e.target).classList.contains('folder-chevron')) return;
679
+ const chevron = line.querySelector('.folder-chevron');
680
+ if (chevron) {
681
+ const targetId = chevron.getAttribute('data-target');
682
+ const children = document.getElementById(targetId);
683
+ toggleFolder(chevron, children);
684
+ }
685
+ });
686
+ });
687
+ // YAML file modal
688
+ document.querySelectorAll('.yaml-file').forEach(node => {
689
+ node.addEventListener('click', (e) => {
690
+ const fileName = node.querySelector('.file-name').textContent;
691
+ const content = node.getAttribute('data-content');
692
+ if (content) {
693
+ showYamlContent(fileName, content);
694
+ }
695
+ });
696
+ });
697
+ // Expand All
698
+ document.getElementById('expandAllBtn').addEventListener('click', () => {
699
+ document.querySelectorAll('.folder-children').forEach(el => {
700
+ el.style.display = 'block';
701
+ });
702
+ document.querySelectorAll('.folder-chevron').forEach(chevron => {
703
+ chevron.textContent = '▼';
704
+ });
705
+ });
706
+ // Collapse All
707
+ document.getElementById('collapseAllBtn').addEventListener('click', () => {
708
+ document.querySelectorAll('.folder-children').forEach(el => {
709
+ if (el.id !== 'folder-root') {
710
+ el.style.display = 'none';
711
+ }
712
+ });
713
+ document.querySelectorAll('.folder-chevron').forEach(chevron => {
714
+ chevron.textContent = '▶';
715
+ });
716
+ });
717
+ });
718
+ </script>
719
+ </body>
720
+ </html>
721
+ `
722
+
723
+ return html
724
+ }
725
+
726
+ function escapeHtml(unsafe: string): string {
727
+ return unsafe
728
+ .replace(/&/g, '&amp;')
729
+ .replace(/</g, '&lt;')
730
+ .replace(/>/g, '&gt;')
731
+ .replace(/"/g, '&quot;')
732
+ .replace(/'/g, '&#039;')
733
+ }
734
+
735
+ export function generateTestReport(testsDir: string, outputFile: string): void {
736
+ try {
737
+ const tree = buildTestTree(testsDir)
738
+ const html = generateHtml(tree)
739
+ fs.writeFileSync(outputFile, html)
740
+ console.warn(`Report generated successfully at: ${outputFile}`)
741
+ } catch (error) {
742
+ console.error('Error generating report:', error)
743
+ throw error
744
+ }
745
+ }
746
+
747
+ function main() {
748
+ const testsDir = path.join(__dirname, '..', 'tests')
749
+ const outputFile = path.join(__dirname, '..', 'test-report.html')
750
+
751
+ generateTestReport(testsDir, outputFile)
752
+ }
753
+
754
+ // Only run main if this file is executed directly
755
+ if (import.meta.url === `file://${process.argv[1]}`) {
756
+ main()
757
+ }