@vibecheckai/cli 3.6.1 → 3.8.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.
Files changed (105) hide show
  1. package/README.md +135 -63
  2. package/bin/_deprecations.js +447 -19
  3. package/bin/_router.js +1 -1
  4. package/bin/registry.js +347 -280
  5. package/bin/runners/context/generators/cursor-enhanced.js +2439 -0
  6. package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
  7. package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
  8. package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
  9. package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
  10. package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
  11. package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
  12. package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
  13. package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
  14. package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
  15. package/bin/runners/lib/agent-firewall/index.js +200 -0
  16. package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
  17. package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
  18. package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +622 -0
  19. package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
  20. package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
  21. package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
  22. package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
  23. package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
  24. package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
  25. package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +31 -38
  26. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +68 -3
  27. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +4 -2
  28. package/bin/runners/lib/agent-firewall/risk/thresholds.js +5 -4
  29. package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
  30. package/bin/runners/lib/agent-firewall/session/index.js +26 -0
  31. package/bin/runners/lib/artifact-envelope.js +540 -0
  32. package/bin/runners/lib/auth-shared.js +977 -0
  33. package/bin/runners/lib/checkpoint.js +941 -0
  34. package/bin/runners/lib/cleanup/engine.js +571 -0
  35. package/bin/runners/lib/cleanup/index.js +53 -0
  36. package/bin/runners/lib/cleanup/output.js +375 -0
  37. package/bin/runners/lib/cleanup/rules.js +1060 -0
  38. package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
  39. package/bin/runners/lib/doctor/failure-signatures.js +526 -0
  40. package/bin/runners/lib/doctor/fix-script.js +336 -0
  41. package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
  42. package/bin/runners/lib/doctor/modules/index.js +62 -3
  43. package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
  44. package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
  45. package/bin/runners/lib/doctor/safe-repair.js +384 -0
  46. package/bin/runners/lib/engines/attack-detector.js +1192 -0
  47. package/bin/runners/lib/entitlements-v2.js +2 -2
  48. package/bin/runners/lib/error-messages.js +1 -1
  49. package/bin/runners/lib/missions/briefing.js +427 -0
  50. package/bin/runners/lib/missions/checkpoint.js +753 -0
  51. package/bin/runners/lib/missions/hardening.js +851 -0
  52. package/bin/runners/lib/missions/plan.js +421 -32
  53. package/bin/runners/lib/missions/safety-gates.js +645 -0
  54. package/bin/runners/lib/missions/schema.js +478 -0
  55. package/bin/runners/lib/packs/bundle.js +675 -0
  56. package/bin/runners/lib/packs/evidence-pack.js +671 -0
  57. package/bin/runners/lib/packs/pack-factory.js +837 -0
  58. package/bin/runners/lib/packs/permissions-pack.js +686 -0
  59. package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
  60. package/bin/runners/lib/report-output.js +6 -6
  61. package/bin/runners/lib/safelist/index.js +96 -0
  62. package/bin/runners/lib/safelist/integration.js +334 -0
  63. package/bin/runners/lib/safelist/matcher.js +696 -0
  64. package/bin/runners/lib/safelist/schema.js +948 -0
  65. package/bin/runners/lib/safelist/store.js +438 -0
  66. package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
  67. package/bin/runners/lib/ship-gate.js +832 -0
  68. package/bin/runners/lib/ship-manifest.js +1153 -0
  69. package/bin/runners/lib/ship-output.js +1 -1
  70. package/bin/runners/lib/unified-cli-output.js +710 -383
  71. package/bin/runners/lib/upsell.js +3 -3
  72. package/bin/runners/lib/why-tree.js +650 -0
  73. package/bin/runners/runAllowlist.js +33 -4
  74. package/bin/runners/runApprove.js +240 -1122
  75. package/bin/runners/runAudit.js +692 -0
  76. package/bin/runners/runAuth.js +325 -29
  77. package/bin/runners/runCheckpoint.js +442 -494
  78. package/bin/runners/runCleanup.js +343 -0
  79. package/bin/runners/runDoctor.js +269 -19
  80. package/bin/runners/runFix.js +411 -32
  81. package/bin/runners/runForge.js +411 -0
  82. package/bin/runners/runIntent.js +906 -0
  83. package/bin/runners/runKickoff.js +878 -0
  84. package/bin/runners/runLaunch.js +2000 -0
  85. package/bin/runners/runLink.js +785 -0
  86. package/bin/runners/runMcp.js +1741 -837
  87. package/bin/runners/runPacks.js +2089 -0
  88. package/bin/runners/runPolish.js +41 -0
  89. package/bin/runners/runSafelist.js +1190 -0
  90. package/bin/runners/runScan.js +21 -9
  91. package/bin/runners/runShield.js +1282 -0
  92. package/bin/runners/runShip.js +395 -16
  93. package/bin/vibecheck.js +34 -6
  94. package/mcp-server/README.md +117 -158
  95. package/mcp-server/handlers/tool-handler.ts +3 -3
  96. package/mcp-server/index.js +16 -0
  97. package/mcp-server/intent-firewall-interceptor.js +529 -0
  98. package/mcp-server/manifest.json +473 -0
  99. package/mcp-server/package.json +1 -1
  100. package/mcp-server/registry/tool-registry.js +315 -523
  101. package/mcp-server/registry/tools.json +442 -428
  102. package/mcp-server/tier-auth.js +164 -16
  103. package/mcp-server/tools-v3.js +70 -16
  104. package/package.json +1 -1
  105. package/bin/runners/runProof.zip +0 -0
@@ -0,0 +1,675 @@
1
+ // bin/runners/lib/packs/bundle.js
2
+ // ═══════════════════════════════════════════════════════════════════════════════
3
+ // BUNDLE - One command to produce ZIP bundle + manifest + HTML index
4
+ // Combines permissions, proof graph, evidence, and reports into shareable bundle
5
+ // ═══════════════════════════════════════════════════════════════════════════════
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const crypto = require('crypto');
10
+ const {
11
+ createPackBuilder,
12
+ PACK_TYPE,
13
+ ARTIFACT_TYPE,
14
+ generatePackId,
15
+ collectReceiptReferences,
16
+ getGitInfo,
17
+ formatSize,
18
+ } = require('./pack-factory');
19
+ const { buildPermissionsPack } = require('./permissions-pack');
20
+ const { buildProofGraphPack } = require('./proof-graph-pack');
21
+ const { buildEvidencePack } = require('./evidence-pack');
22
+
23
+ // ═══════════════════════════════════════════════════════════════════════════════
24
+ // BUNDLE SCHEMA
25
+ // ═══════════════════════════════════════════════════════════════════════════════
26
+
27
+ const BUNDLE_SCHEMA = 'vibecheck/bundle/v1';
28
+ const BUNDLE_VERSION = '1.0.0';
29
+
30
+ /**
31
+ * Bundle component types
32
+ */
33
+ const BUNDLE_COMPONENTS = {
34
+ EVIDENCE: 'evidence',
35
+ PERMISSIONS: 'permissions',
36
+ PROOF_GRAPH: 'proof-graph',
37
+ REPORT: 'report',
38
+ };
39
+
40
+ // ═══════════════════════════════════════════════════════════════════════════════
41
+ // REPORT LOADING
42
+ // ═══════════════════════════════════════════════════════════════════════════════
43
+
44
+ /**
45
+ * Load report data from .vibecheck
46
+ * @param {string} repoRoot - Repository root
47
+ * @returns {object} Report data
48
+ */
49
+ function loadReportData(repoRoot) {
50
+ const data = {
51
+ scan: null,
52
+ findings: [],
53
+ verdict: null,
54
+ score: null,
55
+ };
56
+
57
+ // Load latest scan
58
+ const scanPath = path.join(repoRoot, '.vibecheck/results/latest.json');
59
+ if (fs.existsSync(scanPath)) {
60
+ try {
61
+ data.scan = JSON.parse(fs.readFileSync(scanPath, 'utf8'));
62
+ data.findings = data.scan.findings || data.scan.report?.findings || [];
63
+ data.verdict = data.scan.verdict || data.scan.report?.verdict;
64
+ data.score = data.scan.score || data.scan.report?.score;
65
+ } catch (e) {
66
+ // Skip
67
+ }
68
+ }
69
+
70
+ // Load ship manifest for verdict
71
+ const shipPath = path.join(repoRoot, '.vibecheck/ship/manifest.json');
72
+ if (fs.existsSync(shipPath)) {
73
+ try {
74
+ const ship = JSON.parse(fs.readFileSync(shipPath, 'utf8'));
75
+ data.verdict = ship.verdict?.status || data.verdict;
76
+ data.score = ship.verdict?.score || data.score;
77
+ } catch (e) {
78
+ // Skip
79
+ }
80
+ }
81
+
82
+ return data;
83
+ }
84
+
85
+ // ═══════════════════════════════════════════════════════════════════════════════
86
+ // HTML INDEX GENERATION
87
+ // ═══════════════════════════════════════════════════════════════════════════════
88
+
89
+ /**
90
+ * Generate unified bundle HTML index
91
+ * @param {object} bundleManifest - Bundle manifest
92
+ * @param {object} components - Component manifests
93
+ * @returns {string} HTML content
94
+ */
95
+ function generateBundleHtml(bundleManifest, components) {
96
+ const componentCards = Object.entries(components)
97
+ .filter(([_, c]) => c !== null)
98
+ .map(([type, comp]) => {
99
+ const artifactCount = comp.manifest?.artifacts?.length || 0;
100
+ const icon = {
101
+ evidence: '📸',
102
+ permissions: '🔐',
103
+ 'proof-graph': '🕸️',
104
+ report: '📊',
105
+ }[type] || '📦';
106
+
107
+ return `
108
+ <div class="component-card">
109
+ <div class="component-icon">${icon}</div>
110
+ <div class="component-info">
111
+ <h3>${type.charAt(0).toUpperCase() + type.slice(1).replace('-', ' ')}</h3>
112
+ <p>${artifactCount} artifacts</p>
113
+ <a href="${type}/index.html" class="btn">View</a>
114
+ </div>
115
+ </div>
116
+ `;
117
+ }).join('');
118
+
119
+ const receiptBadges = [];
120
+ if (bundleManifest.receipts?.ship) {
121
+ const verdict = bundleManifest.receipts.ship.verdict;
122
+ const color = verdict === 'SHIP' ? 'success' : verdict === 'WARN' ? 'warning' : 'danger';
123
+ receiptBadges.push(`<span class="badge ${color}">Ship: ${verdict}</span>`);
124
+ }
125
+ if (bundleManifest.receipts?.reality) {
126
+ receiptBadges.push(`<span class="badge info">Reality: ${bundleManifest.receipts.reality.verdict || 'N/A'}</span>`);
127
+ }
128
+
129
+ return `<!DOCTYPE html>
130
+ <html lang="en">
131
+ <head>
132
+ <meta charset="UTF-8">
133
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
134
+ <title>Vibecheck Bundle - ${bundleManifest.project.name}</title>
135
+ <style>
136
+ :root {
137
+ --bg: #0d1117;
138
+ --surface: #161b22;
139
+ --border: #30363d;
140
+ --text: #c9d1d9;
141
+ --text-muted: #8b949e;
142
+ --accent: #ff9650;
143
+ --success: #3fb950;
144
+ --warning: #d29922;
145
+ --danger: #f85149;
146
+ --info: #58a6ff;
147
+ }
148
+
149
+ * { box-sizing: border-box; margin: 0; padding: 0; }
150
+
151
+ body {
152
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
153
+ background: var(--bg);
154
+ color: var(--text);
155
+ line-height: 1.6;
156
+ min-height: 100vh;
157
+ }
158
+
159
+ .hero {
160
+ background: linear-gradient(135deg, var(--surface) 0%, var(--bg) 100%);
161
+ border-bottom: 1px solid var(--border);
162
+ padding: 4rem 2rem;
163
+ text-align: center;
164
+ }
165
+
166
+ .logo {
167
+ font-size: 4rem;
168
+ margin-bottom: 1rem;
169
+ }
170
+
171
+ h1 {
172
+ font-size: 2.5rem;
173
+ color: var(--text);
174
+ margin-bottom: 0.5rem;
175
+ }
176
+
177
+ h1 span { color: var(--accent); }
178
+
179
+ .project-name {
180
+ font-size: 1.25rem;
181
+ color: var(--text-muted);
182
+ margin-bottom: 1.5rem;
183
+ }
184
+
185
+ .badges {
186
+ display: flex;
187
+ justify-content: center;
188
+ gap: 0.5rem;
189
+ flex-wrap: wrap;
190
+ margin-bottom: 2rem;
191
+ }
192
+
193
+ .badge {
194
+ display: inline-block;
195
+ padding: 0.5rem 1rem;
196
+ border-radius: 20px;
197
+ font-size: 0.875rem;
198
+ font-weight: 500;
199
+ }
200
+
201
+ .badge.success { background: var(--success)22; color: var(--success); }
202
+ .badge.warning { background: var(--warning)22; color: var(--warning); }
203
+ .badge.danger { background: var(--danger)22; color: var(--danger); }
204
+ .badge.info { background: var(--info)22; color: var(--info); }
205
+
206
+ .meta {
207
+ display: flex;
208
+ justify-content: center;
209
+ gap: 2rem;
210
+ flex-wrap: wrap;
211
+ font-size: 0.875rem;
212
+ color: var(--text-muted);
213
+ }
214
+
215
+ .meta-item strong { color: var(--text); }
216
+
217
+ .container {
218
+ max-width: 1200px;
219
+ margin: 0 auto;
220
+ padding: 3rem 2rem;
221
+ }
222
+
223
+ h2 {
224
+ font-size: 1.5rem;
225
+ margin-bottom: 1.5rem;
226
+ text-align: center;
227
+ }
228
+
229
+ .components {
230
+ display: grid;
231
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
232
+ gap: 1.5rem;
233
+ margin-bottom: 3rem;
234
+ }
235
+
236
+ .component-card {
237
+ background: var(--surface);
238
+ border: 1px solid var(--border);
239
+ border-radius: 12px;
240
+ padding: 2rem;
241
+ text-align: center;
242
+ transition: border-color 0.2s, transform 0.2s;
243
+ }
244
+
245
+ .component-card:hover {
246
+ border-color: var(--accent);
247
+ transform: translateY(-2px);
248
+ }
249
+
250
+ .component-icon {
251
+ font-size: 3rem;
252
+ margin-bottom: 1rem;
253
+ }
254
+
255
+ .component-info h3 {
256
+ margin-bottom: 0.5rem;
257
+ }
258
+
259
+ .component-info p {
260
+ color: var(--text-muted);
261
+ margin-bottom: 1rem;
262
+ }
263
+
264
+ .btn {
265
+ display: inline-block;
266
+ background: var(--accent);
267
+ color: var(--bg);
268
+ padding: 0.5rem 1.5rem;
269
+ border-radius: 6px;
270
+ text-decoration: none;
271
+ font-weight: 500;
272
+ transition: opacity 0.2s;
273
+ }
274
+
275
+ .btn:hover { opacity: 0.9; }
276
+
277
+ .info-section {
278
+ background: var(--surface);
279
+ border: 1px solid var(--border);
280
+ border-radius: 12px;
281
+ padding: 2rem;
282
+ }
283
+
284
+ .info-grid {
285
+ display: grid;
286
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
287
+ gap: 1.5rem;
288
+ }
289
+
290
+ .info-item {
291
+ text-align: center;
292
+ }
293
+
294
+ .info-label {
295
+ font-size: 0.75rem;
296
+ color: var(--text-muted);
297
+ text-transform: uppercase;
298
+ letter-spacing: 0.05em;
299
+ margin-bottom: 0.25rem;
300
+ }
301
+
302
+ .info-value {
303
+ font-family: monospace;
304
+ font-size: 0.875rem;
305
+ word-break: break-all;
306
+ }
307
+
308
+ footer {
309
+ text-align: center;
310
+ padding: 2rem;
311
+ border-top: 1px solid var(--border);
312
+ margin-top: 3rem;
313
+ color: var(--text-muted);
314
+ font-size: 0.875rem;
315
+ }
316
+ </style>
317
+ </head>
318
+ <body>
319
+ <div class="hero">
320
+ <div class="logo">📦</div>
321
+ <h1>Vibecheck <span>Bundle</span></h1>
322
+ <p class="project-name">${bundleManifest.project.name}</p>
323
+
324
+ <div class="badges">
325
+ ${receiptBadges.join('')}
326
+ <span class="badge info">v${BUNDLE_VERSION}</span>
327
+ </div>
328
+
329
+ <div class="meta">
330
+ <span class="meta-item"><strong>Generated:</strong> ${new Date(bundleManifest.generatedAt).toLocaleString()}</span>
331
+ <span class="meta-item"><strong>Git:</strong> ${bundleManifest.git.branch || 'N/A'} @ ${bundleManifest.git.commit?.slice(0, 7) || 'N/A'}</span>
332
+ <span class="meta-item"><strong>Components:</strong> ${Object.keys(components).filter(k => components[k]).length}</span>
333
+ </div>
334
+ </div>
335
+
336
+ <div class="container">
337
+ <h2>Components</h2>
338
+
339
+ <div class="components">
340
+ ${componentCards}
341
+ </div>
342
+
343
+ <div class="info-section">
344
+ <h2>Build Info</h2>
345
+ <div class="info-grid">
346
+ <div class="info-item">
347
+ <div class="info-label">Bundle ID</div>
348
+ <div class="info-value">${bundleManifest.id}</div>
349
+ </div>
350
+ <div class="info-item">
351
+ <div class="info-label">Reproducible</div>
352
+ <div class="info-value">${bundleManifest.build.reproducible ? '✓ Yes' : '✗ No (dirty)'}</div>
353
+ </div>
354
+ <div class="info-item">
355
+ <div class="info-label">Platform</div>
356
+ <div class="info-value">${bundleManifest.build.platform}</div>
357
+ </div>
358
+ <div class="info-item">
359
+ <div class="info-label">Fingerprint</div>
360
+ <div class="info-value">${bundleManifest.build.fingerprint.slice(0, 16)}...</div>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+
366
+ <footer>
367
+ <p>Generated by <strong>Vibecheck</strong> • Schema: ${bundleManifest.$schema}</p>
368
+ </footer>
369
+
370
+ <script>
371
+ window.VIBECHECK_BUNDLE = ${JSON.stringify(bundleManifest, null, 2)};
372
+ </script>
373
+ </body>
374
+ </html>`;
375
+ }
376
+
377
+ // ═══════════════════════════════════════════════════════════════════════════════
378
+ // BUNDLE BUILDER
379
+ // ═══════════════════════════════════════════════════════════════════════════════
380
+
381
+ /**
382
+ * Build unified bundle
383
+ * @param {string} repoRoot - Repository root
384
+ * @param {object} options - Build options
385
+ * @returns {Promise<object>} Build result
386
+ */
387
+ async function buildBundle(repoRoot, options = {}) {
388
+ const {
389
+ outputDir,
390
+ createZip = true,
391
+ includeEvidence = true,
392
+ includePermissions = true,
393
+ includeProofGraph = true,
394
+ includeReport = true,
395
+ } = options;
396
+
397
+ const projectName = path.basename(repoRoot);
398
+ const bundleId = generatePackId('bundle', projectName, Date.now().toString());
399
+
400
+ // Determine output directory
401
+ const bundleDir = outputDir || path.join(repoRoot, '.vibecheck/bundles', bundleId);
402
+ fs.mkdirSync(bundleDir, { recursive: true });
403
+
404
+ const components = {
405
+ evidence: null,
406
+ permissions: null,
407
+ 'proof-graph': null,
408
+ report: null,
409
+ };
410
+
411
+ // Build components
412
+ console.log('📦 Building bundle components...\n');
413
+
414
+ if (includeEvidence) {
415
+ console.log(' 📸 Building evidence pack...');
416
+ try {
417
+ const evidenceDir = path.join(bundleDir, 'evidence');
418
+ const result = await buildEvidencePack(repoRoot, { outputDir: evidenceDir });
419
+ components.evidence = result;
420
+ console.log(` ✓ ${result.artifactCount} artifacts`);
421
+ } catch (e) {
422
+ console.log(` ✗ Failed: ${e.message}`);
423
+ }
424
+ }
425
+
426
+ if (includePermissions) {
427
+ console.log(' 🔐 Building permissions pack...');
428
+ try {
429
+ const permDir = path.join(bundleDir, 'permissions');
430
+ const result = await buildPermissionsPack(repoRoot, { outputDir: permDir });
431
+ components.permissions = result;
432
+ console.log(` ✓ ${result.manifest.metadata.rolesCount || 0} roles, ${result.manifest.metadata.permissionsCount || 0} permissions`);
433
+ } catch (e) {
434
+ console.log(` ✗ Failed: ${e.message}`);
435
+ }
436
+ }
437
+
438
+ if (includeProofGraph) {
439
+ console.log(' 🕸️ Building proof graph pack...');
440
+ try {
441
+ const graphDir = path.join(bundleDir, 'proof-graph');
442
+ const result = await buildProofGraphPack(repoRoot, { outputDir: graphDir });
443
+ components['proof-graph'] = result;
444
+ console.log(` ✓ ${result.manifest.metadata.nodeCount || 0} nodes, ${result.manifest.metadata.edgeCount || 0} edges`);
445
+ } catch (e) {
446
+ console.log(` ✗ Failed: ${e.message}`);
447
+ }
448
+ }
449
+
450
+ if (includeReport) {
451
+ console.log(' 📊 Building report pack...');
452
+ try {
453
+ const reportDir = path.join(bundleDir, 'report');
454
+ fs.mkdirSync(reportDir, { recursive: true });
455
+
456
+ const reportData = loadReportData(repoRoot);
457
+
458
+ // Create simple report artifacts
459
+ const reportJson = JSON.stringify(reportData, null, 2);
460
+ fs.writeFileSync(path.join(reportDir, 'report.json'), reportJson);
461
+
462
+ components.report = {
463
+ packId: `report_${bundleId}`,
464
+ outputDir: reportDir,
465
+ manifest: {
466
+ artifacts: [{ filename: 'report.json', type: 'json' }],
467
+ metadata: {
468
+ findingCount: reportData.findings.length,
469
+ verdict: reportData.verdict,
470
+ score: reportData.score,
471
+ },
472
+ },
473
+ };
474
+ console.log(` ✓ ${reportData.findings.length} findings, verdict: ${reportData.verdict || 'N/A'}`);
475
+ } catch (e) {
476
+ console.log(` ✗ Failed: ${e.message}`);
477
+ }
478
+ }
479
+
480
+ console.log('');
481
+
482
+ // Collect receipts and git info
483
+ const receipts = collectReceiptReferences(repoRoot);
484
+ const git = getGitInfo(repoRoot);
485
+
486
+ // Create bundle manifest
487
+ const bundleManifest = {
488
+ $schema: BUNDLE_SCHEMA,
489
+ schemaVersion: BUNDLE_VERSION,
490
+ id: bundleId,
491
+ type: 'bundle',
492
+ generatedAt: new Date().toISOString(),
493
+ project: {
494
+ name: projectName,
495
+ root: repoRoot,
496
+ },
497
+ git,
498
+ build: {
499
+ fingerprint: crypto.createHash('sha256')
500
+ .update(JSON.stringify({ bundleId, projectName, git, components: Object.keys(components).filter(k => components[k]) }))
501
+ .digest('hex'),
502
+ reproducible: !git.dirty,
503
+ nodeVersion: process.version,
504
+ platform: process.platform,
505
+ },
506
+ receipts,
507
+ components: Object.entries(components).reduce((acc, [key, val]) => {
508
+ if (val) {
509
+ acc[key] = {
510
+ packId: val.packId,
511
+ artifactCount: val.manifest?.artifacts?.length || val.artifactCount || 0,
512
+ indexPath: `${key}/index.html`,
513
+ };
514
+ }
515
+ return acc;
516
+ }, {}),
517
+ summary: {
518
+ componentCount: Object.values(components).filter(Boolean).length,
519
+ hasShipReceipt: !!receipts.ship,
520
+ hasRealityReceipt: !!receipts.reality,
521
+ verdict: receipts.ship?.verdict || null,
522
+ },
523
+ };
524
+
525
+ // Write bundle manifest
526
+ fs.writeFileSync(
527
+ path.join(bundleDir, 'manifest.json'),
528
+ JSON.stringify(bundleManifest, null, 2)
529
+ );
530
+
531
+ // Generate bundle HTML index
532
+ const bundleHtml = generateBundleHtml(bundleManifest, components);
533
+ fs.writeFileSync(path.join(bundleDir, 'index.html'), bundleHtml);
534
+
535
+ // Create ZIP if requested
536
+ let zipPath = null;
537
+ let zipSize = 0;
538
+
539
+ if (createZip) {
540
+ try {
541
+ const archiver = require('archiver');
542
+ zipPath = `${bundleDir}.zip`;
543
+
544
+ console.log('📦 Creating ZIP bundle...');
545
+
546
+ const output = fs.createWriteStream(zipPath);
547
+ const archive = archiver('zip', { zlib: { level: 9 } });
548
+
549
+ await new Promise((resolve, reject) => {
550
+ output.on('close', () => {
551
+ zipSize = archive.pointer();
552
+ resolve();
553
+ });
554
+ archive.on('error', reject);
555
+ archive.pipe(output);
556
+ archive.directory(bundleDir, false);
557
+ archive.finalize();
558
+ });
559
+
560
+ console.log(` ✓ Created: ${path.basename(zipPath)} (${formatSize(zipSize)})`);
561
+ } catch (e) {
562
+ console.log(` ⚠ ZIP creation skipped: archiver not available`);
563
+ }
564
+ }
565
+
566
+ return {
567
+ bundleId,
568
+ bundleDir,
569
+ manifest: bundleManifest,
570
+ components,
571
+ zipPath,
572
+ zipSize,
573
+ };
574
+ }
575
+
576
+ // ═══════════════════════════════════════════════════════════════════════════════
577
+ // LIST AND CLEANUP
578
+ // ═══════════════════════════════════════════════════════════════════════════════
579
+
580
+ /**
581
+ * List all bundles
582
+ * @param {string} repoRoot - Repository root
583
+ * @returns {Array} Bundle list
584
+ */
585
+ function listBundles(repoRoot) {
586
+ const bundlesDir = path.join(repoRoot, '.vibecheck/bundles');
587
+ if (!fs.existsSync(bundlesDir)) return [];
588
+
589
+ const bundles = [];
590
+ const dirs = fs.readdirSync(bundlesDir, { withFileTypes: true })
591
+ .filter(d => d.isDirectory())
592
+ .map(d => d.name);
593
+
594
+ for (const dir of dirs) {
595
+ const manifestPath = path.join(bundlesDir, dir, 'manifest.json');
596
+ if (fs.existsSync(manifestPath)) {
597
+ try {
598
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
599
+ bundles.push({
600
+ id: manifest.id,
601
+ generatedAt: manifest.generatedAt,
602
+ componentCount: manifest.summary?.componentCount || 0,
603
+ verdict: manifest.summary?.verdict,
604
+ path: path.join(bundlesDir, dir),
605
+ });
606
+ } catch (e) {
607
+ // Skip invalid
608
+ }
609
+ }
610
+ }
611
+
612
+ return bundles.sort((a, b) => new Date(b.generatedAt) - new Date(a.generatedAt));
613
+ }
614
+
615
+ /**
616
+ * Cleanup old bundles
617
+ * @param {string} repoRoot - Repository root
618
+ * @param {object} options - Cleanup options
619
+ * @returns {number} Number of bundles removed
620
+ */
621
+ function cleanupBundles(repoRoot, options = {}) {
622
+ const { maxCount = 5, maxAgeDays = 30 } = options;
623
+
624
+ const bundles = listBundles(repoRoot);
625
+ const now = new Date();
626
+ const maxAge = maxAgeDays * 24 * 60 * 60 * 1000;
627
+
628
+ let removed = 0;
629
+
630
+ for (let i = 0; i < bundles.length; i++) {
631
+ const bundle = bundles[i];
632
+ const age = now - new Date(bundle.generatedAt);
633
+
634
+ if (i >= maxCount || age > maxAge) {
635
+ try {
636
+ fs.rmSync(bundle.path, { recursive: true, force: true });
637
+
638
+ // Also remove ZIP if exists
639
+ if (fs.existsSync(`${bundle.path}.zip`)) {
640
+ fs.unlinkSync(`${bundle.path}.zip`);
641
+ }
642
+
643
+ removed++;
644
+ } catch (e) {
645
+ // Skip if can't remove
646
+ }
647
+ }
648
+ }
649
+
650
+ return removed;
651
+ }
652
+
653
+ // ═══════════════════════════════════════════════════════════════════════════════
654
+ // EXPORTS
655
+ // ═══════════════════════════════════════════════════════════════════════════════
656
+
657
+ module.exports = {
658
+ // Constants
659
+ BUNDLE_SCHEMA,
660
+ BUNDLE_VERSION,
661
+ BUNDLE_COMPONENTS,
662
+
663
+ // Loading
664
+ loadReportData,
665
+
666
+ // HTML
667
+ generateBundleHtml,
668
+
669
+ // Builder
670
+ buildBundle,
671
+
672
+ // Management
673
+ listBundles,
674
+ cleanupBundles,
675
+ };