repotrailer 0.1.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.
@@ -0,0 +1,588 @@
1
+ import { copyFile, mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { escapeHtml, run } from "./utils.js";
6
+
7
+ function titleClass(value) {
8
+ const length = String(value).length;
9
+ if (length > 54) {
10
+ return "title-long";
11
+ }
12
+ if (length > 32) {
13
+ return "title-medium";
14
+ }
15
+ return "title-short";
16
+ }
17
+
18
+ function sceneBody(scene, index) {
19
+ const metrics = scene.metrics
20
+ ? `<div class="metrics entrance entrance-metrics">
21
+ ${scene.metrics.map((metric) => `<div class="metric">
22
+ <strong>${escapeHtml(metric.value)}</strong>
23
+ <span>${escapeHtml(metric.label)}</span>
24
+ </div>`).join("")}
25
+ </div>`
26
+ : "";
27
+
28
+ const terminal = scene.kind === "install"
29
+ ? `<div class="terminal entrance entrance-terminal">
30
+ <span class="prompt">$</span>
31
+ <code>${escapeHtml(scene.title)}</code>
32
+ <span class="cursor"></span>
33
+ </div>`
34
+ : "";
35
+
36
+ const title = scene.kind === "install"
37
+ ? "One command."
38
+ : escapeHtml(scene.title);
39
+
40
+ return `<section
41
+ class="hf-scene scene-${escapeHtml(scene.kind)}"
42
+ id="scene-${index}"
43
+ data-scene-index="${index}"
44
+ data-layout-allow-overflow
45
+ >
46
+ <div class="grid" data-layout-ignore></div>
47
+ <div class="orb orb-hot entrance entrance-orb" data-layout-allow-overflow></div>
48
+ <div class="orb orb-accent entrance entrance-orb" data-layout-allow-overflow></div>
49
+ <div class="index-mark entrance entrance-index">${String(index + 1).padStart(2, "0")}</div>
50
+ <div class="scene-content">
51
+ <p class="eyebrow entrance entrance-eyebrow">${escapeHtml(scene.eyebrow)}</p>
52
+ <h2 class="headline ${titleClass(title)} entrance entrance-title">${title}</h2>
53
+ ${terminal}
54
+ <p class="copy entrance entrance-copy">${escapeHtml(scene.body)}</p>
55
+ ${metrics}
56
+ </div>
57
+ <div class="rail entrance entrance-rail">
58
+ <span>REPOTRAILER</span>
59
+ <span>${String(scene.start).padStart(4, "0")}S</span>
60
+ </div>
61
+ </section>`;
62
+ }
63
+
64
+ function animationScript(scenes) {
65
+ const entrances = scenes.map((scene, index) => {
66
+ const root = `#scene-${index}`;
67
+ const direction = index % 2 ? -1 : 1;
68
+ const ambientStart = Number((scene.start + 0.84).toFixed(2));
69
+ const repeats = Math.max(0, Math.ceil(scene.duration / 1.8) - 2);
70
+ const terminalEntrance = scene.kind === "install"
71
+ ? `
72
+ tl.from("${root} .entrance-terminal", {
73
+ x: -90, scale: .96, opacity: 0, duration: .5, ease: "back.out(1.7)"
74
+ }, ${Number((scene.start + 0.3).toFixed(2))});`
75
+ : "";
76
+ const metricsEntrance = scene.metrics
77
+ ? `
78
+ tl.from("${root} .entrance-metrics .metric", {
79
+ y: 54, opacity: 0, duration: .4, stagger: .09, ease: "back.out(1.5)"
80
+ }, ${Number((scene.start + 0.34).toFixed(2))});`
81
+ : "";
82
+
83
+ return ` tl.from("${root} .entrance-eyebrow", {
84
+ y: 46, opacity: 0, duration: .46, ease: "expo.out"
85
+ }, ${Number((scene.start + 0.08).toFixed(2))});
86
+ tl.from("${root} .entrance-title", {
87
+ y: 78, rotation: ${index % 2 ? 1.8 : -1.4}, opacity: 0,
88
+ duration: .58, ease: "power4.out"
89
+ }, ${Number((scene.start + 0.16).toFixed(2))});${terminalEntrance}
90
+ tl.from("${root} .entrance-copy", {
91
+ x: ${52 * direction}, opacity: 0, duration: .44, ease: "power3.out"
92
+ }, ${Number((scene.start + 0.36).toFixed(2))});${metricsEntrance}
93
+ tl.from("${root} .entrance-index", {
94
+ scale: .72, rotation: -5, opacity: 0, duration: .62, ease: "expo.out"
95
+ }, ${Number((scene.start + 0.12).toFixed(2))});
96
+ tl.from("${root} .entrance-rail", {
97
+ y: -42, opacity: 0, duration: .38, ease: "power2.out"
98
+ }, ${Number((scene.start + 0.48).toFixed(2))});
99
+ tl.from("${root} .entrance-orb", {
100
+ scale: .64, opacity: 0, duration: .72, stagger: .07, ease: "circ.out"
101
+ }, ${Number((scene.start + 0.04).toFixed(2))});
102
+ tl.to("${root} .orb-accent", {
103
+ scale: 1.035, x: ${16 * direction}, duration: .9,
104
+ repeat: ${repeats}, yoyo: true, ease: "sine.inOut"
105
+ }, ${ambientStart});`;
106
+ });
107
+
108
+ const transitions = scenes.slice(1).map((scene, offset) => {
109
+ const index = offset + 1;
110
+ const previous = index - 1;
111
+ const transitionStart = Number((scene.start - 0.34).toFixed(2));
112
+ const direction = index % 2 === 0 ? 1 : -1;
113
+ const flash = scene.kind === "proof"
114
+ ? `
115
+ tl.fromTo(".transition-flash", {
116
+ opacity: 0
117
+ }, {
118
+ opacity: .82,
119
+ duration: .11,
120
+ ease: "expo.in",
121
+ immediateRender: false
122
+ }, ${transitionStart});
123
+ tl.to(".transition-flash", {
124
+ opacity: 0,
125
+ duration: .17,
126
+ ease: "expo.out"
127
+ }, ${Number((transitionStart + 0.12).toFixed(2))});`
128
+ : "";
129
+
130
+ return ` tl.to("#scene-${previous}", {
131
+ xPercent: ${-100 * direction},
132
+ opacity: 0,
133
+ duration: .34,
134
+ ease: "power4.inOut"
135
+ }, ${transitionStart});
136
+ tl.fromTo("#scene-${index}", {
137
+ xPercent: ${100 * direction},
138
+ opacity: 1
139
+ }, {
140
+ xPercent: 0,
141
+ opacity: 1,
142
+ duration: .34,
143
+ ease: "power4.inOut",
144
+ immediateRender: false
145
+ }, ${transitionStart});${flash}`;
146
+ });
147
+
148
+ const last = scenes.length - 1;
149
+ const finalStart = Number(
150
+ (scenes[last].start + scenes[last].duration - 0.68).toFixed(2),
151
+ );
152
+
153
+ return `${transitions.join("\n\n")}
154
+
155
+ ${entrances.join("\n\n")}
156
+
157
+ tl.to("#scene-${last} .entrance", {
158
+ y: -18,
159
+ opacity: 0,
160
+ duration: .48,
161
+ stagger: .025,
162
+ ease: "power2.in"
163
+ }, ${finalStart});`;
164
+ }
165
+
166
+ function compositionHtml(repo, scenes, palette) {
167
+ const duration = Number(
168
+ (scenes.at(-1).start + scenes.at(-1).duration).toFixed(2),
169
+ );
170
+ return `<!doctype html>
171
+ <html lang="en">
172
+ <head>
173
+ <meta charset="UTF-8">
174
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
175
+ <title>${escapeHtml(repo.title)} · RepoTrailer</title>
176
+ <script src="./gsap.min.js"></script>
177
+ <style>
178
+ html, body {
179
+ margin: 0;
180
+ width: 1920px;
181
+ height: 1080px;
182
+ overflow: hidden;
183
+ background-color: ${palette.ink};
184
+ color: ${palette.paper};
185
+ font-family: Inter, "Helvetica Neue", Arial, sans-serif;
186
+ }
187
+ * { box-sizing: border-box; }
188
+ #repotrailer {
189
+ position: relative;
190
+ width: 1920px;
191
+ height: 1080px;
192
+ overflow: hidden;
193
+ background-color: ${palette.ink};
194
+ }
195
+ .hf-scene {
196
+ position: absolute;
197
+ inset: 0;
198
+ width: 100%;
199
+ height: 100%;
200
+ overflow: hidden;
201
+ background-color: ${palette.ink};
202
+ opacity: 0;
203
+ will-change: transform;
204
+ }
205
+ .hf-scene:first-child { opacity: 1; }
206
+ .grid {
207
+ position: absolute;
208
+ inset: 0;
209
+ background-image:
210
+ linear-gradient(rgba(199, 255, 69, .055) 1px, rgba(199, 255, 69, 0) 1px),
211
+ linear-gradient(90deg, rgba(199, 255, 69, .055) 1px, rgba(199, 255, 69, 0) 1px);
212
+ background-size: 120px 120px;
213
+ }
214
+ .grid::after {
215
+ content: "";
216
+ position: absolute;
217
+ inset: 0;
218
+ background-image: repeating-linear-gradient(
219
+ 0deg,
220
+ rgba(245, 242, 233, .025) 0,
221
+ rgba(245, 242, 233, .025) 1px,
222
+ rgba(245, 242, 233, 0) 1px,
223
+ rgba(245, 242, 233, 0) 7px
224
+ );
225
+ }
226
+ .scene-content {
227
+ position: relative;
228
+ z-index: 4;
229
+ display: flex;
230
+ flex-direction: column;
231
+ justify-content: center;
232
+ width: 100%;
233
+ height: 100%;
234
+ padding: 120px 150px 150px;
235
+ gap: 28px;
236
+ }
237
+ .eyebrow {
238
+ margin: 0;
239
+ color: ${palette.accent};
240
+ font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
241
+ font-size: 25px;
242
+ font-weight: 800;
243
+ letter-spacing: .22em;
244
+ text-transform: uppercase;
245
+ }
246
+ .headline {
247
+ max-width: 1480px;
248
+ margin: 0;
249
+ color: ${palette.paper};
250
+ font-weight: 900;
251
+ letter-spacing: -.065em;
252
+ line-height: .88;
253
+ text-wrap: balance;
254
+ }
255
+ .title-short { font-size: 168px; }
256
+ .title-medium { font-size: 126px; }
257
+ .title-long { font-size: 94px; line-height: .96; }
258
+ .copy {
259
+ max-width: 1120px;
260
+ margin: 4px 0 0;
261
+ color: ${palette.muted};
262
+ font-size: 39px;
263
+ font-weight: 450;
264
+ line-height: 1.3;
265
+ text-wrap: balance;
266
+ }
267
+ .orb {
268
+ position: absolute;
269
+ z-index: 2;
270
+ border-radius: 999px;
271
+ will-change: transform, opacity;
272
+ }
273
+ .orb-hot {
274
+ top: -340px;
275
+ right: -210px;
276
+ width: 780px;
277
+ height: 780px;
278
+ background-color: ${palette.hot};
279
+ }
280
+ .orb-accent {
281
+ top: -230px;
282
+ right: -100px;
283
+ width: 500px;
284
+ height: 500px;
285
+ background-color: ${palette.accent};
286
+ }
287
+ .index-mark {
288
+ position: absolute;
289
+ z-index: 3;
290
+ right: 110px;
291
+ bottom: 58px;
292
+ color: rgba(245, 242, 233, .08);
293
+ font-size: 280px;
294
+ font-weight: 900;
295
+ letter-spacing: -.08em;
296
+ }
297
+ .rail {
298
+ position: absolute;
299
+ z-index: 5;
300
+ left: 0;
301
+ right: 0;
302
+ bottom: 0;
303
+ display: flex;
304
+ justify-content: space-between;
305
+ padding: 28px 150px 34px;
306
+ border-top: 2px solid rgba(245, 242, 233, .18);
307
+ color: ${palette.paper};
308
+ font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
309
+ font-size: 18px;
310
+ font-weight: 800;
311
+ letter-spacing: .16em;
312
+ }
313
+ .metrics {
314
+ display: flex;
315
+ gap: 34px;
316
+ width: min(1240px, 100%);
317
+ margin-top: 18px;
318
+ }
319
+ .metric {
320
+ display: flex;
321
+ flex-direction: column;
322
+ min-width: 260px;
323
+ padding: 26px 30px 28px;
324
+ border: 2px solid rgba(245, 242, 233, .2);
325
+ background-color: rgba(17, 19, 25, .9);
326
+ }
327
+ .metric strong {
328
+ color: ${palette.accent};
329
+ font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
330
+ font-size: 86px;
331
+ font-variant-numeric: tabular-nums;
332
+ line-height: .95;
333
+ }
334
+ .metric span {
335
+ margin-top: 14px;
336
+ color: ${palette.muted};
337
+ font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
338
+ font-size: 18px;
339
+ font-weight: 800;
340
+ letter-spacing: .14em;
341
+ text-transform: uppercase;
342
+ }
343
+ .terminal {
344
+ display: flex;
345
+ align-items: center;
346
+ width: min(1500px, 100%);
347
+ min-height: 138px;
348
+ padding: 0 42px;
349
+ border: 3px solid ${palette.accent};
350
+ background-color: #111319;
351
+ box-shadow: 18px 18px 0 ${palette.hot};
352
+ font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
353
+ font-size: 47px;
354
+ overflow: hidden;
355
+ }
356
+ .terminal .prompt { color: ${palette.hot}; margin-right: 24px; }
357
+ .terminal code {
358
+ color: ${palette.paper};
359
+ white-space: nowrap;
360
+ overflow: hidden;
361
+ text-overflow: ellipsis;
362
+ }
363
+ .cursor {
364
+ flex: 0 0 auto;
365
+ width: 24px;
366
+ height: 58px;
367
+ margin-left: 16px;
368
+ background-color: ${palette.accent};
369
+ }
370
+ .scene-feature:nth-of-type(even) .scene-content {
371
+ align-items: flex-end;
372
+ text-align: right;
373
+ }
374
+ .scene-feature:nth-of-type(even) .eyebrow,
375
+ .scene-feature:nth-of-type(even) .headline,
376
+ .scene-feature:nth-of-type(even) .copy {
377
+ max-width: 1320px;
378
+ }
379
+ .scene-feature:nth-of-type(even) .orb-hot {
380
+ right: auto;
381
+ left: -260px;
382
+ top: 650px;
383
+ }
384
+ .scene-feature:nth-of-type(even) .orb-accent {
385
+ right: auto;
386
+ left: -40px;
387
+ top: 740px;
388
+ }
389
+ .scene-proof .headline { max-width: 1560px; }
390
+ .scene-proof .orb-hot { top: -180px; right: -120px; }
391
+ .scene-proof .orb-accent { top: -80px; right: 60px; }
392
+ .scene-outro .scene-content { align-items: center; text-align: center; }
393
+ .scene-outro .headline { font-size: 188px; }
394
+ .scene-outro .copy { font-family: ui-monospace, Menlo, Monaco, Consolas, monospace; font-size: 28px; }
395
+ .transition-flash {
396
+ position: absolute;
397
+ z-index: 20;
398
+ inset: 0;
399
+ pointer-events: none;
400
+ background-color: ${palette.hot};
401
+ opacity: 0;
402
+ }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <main
407
+ id="repotrailer"
408
+ data-composition-id="repotrailer"
409
+ data-width="1920"
410
+ data-height="1080"
411
+ data-start="0"
412
+ data-duration="${duration}"
413
+ >
414
+ ${scenes.map(sceneBody).join("")}
415
+ <div class="transition-flash" data-layout-ignore></div>
416
+ <script>
417
+ window.__timelines = window.__timelines || {};
418
+ const tl = gsap.timeline({ paused: true });
419
+ ${animationScript(scenes)}
420
+
421
+ window.__timelines["repotrailer"] = tl;
422
+ </script>
423
+ </main>
424
+ </body>
425
+ </html>`;
426
+ }
427
+
428
+ function designMarkdown(palette) {
429
+ return `# RepoTrailer Visual Identity
430
+
431
+ ## Style Prompt
432
+
433
+ Swiss Pulse precision with Maximalist Type launch energy. Grid-locked developer
434
+ tool graphics on a near-black canvas, oversized heavy typography, and one toxic
435
+ lime accent balanced by a coral transition hit. Motion is fast, directional,
436
+ and decisive: elements snap into place with no floating or decorative softness.
437
+
438
+ ## Colors
439
+
440
+ - Canvas: \`${palette.ink}\`
441
+ - Primary text: \`${palette.paper}\`
442
+ - Accent: \`${palette.accent}\`
443
+ - Transition hit: \`${palette.hot}\`
444
+ - Secondary text: \`${palette.muted}\`
445
+
446
+ ## Typography
447
+
448
+ - Headlines: Inter Black, 900
449
+ - Labels and commands: Menlo / system monospace
450
+
451
+ ## Motion
452
+
453
+ - Primary transition: 0.34-second push slide with \`power4.inOut\`
454
+ - Entrance signature: \`expo.out\`, \`power4.out\`, and restrained back easing
455
+ - Proof scene accent: one coral overexposure flash
456
+ - Final scene: simple fade to black
457
+
458
+ ## What NOT to Do
459
+
460
+ - No gradients or gradient text
461
+ - No blue-purple AI palette
462
+ - No generic equal-sized card grid
463
+ - No floating glassmorphism
464
+ - No invented metrics or decorative charts
465
+ `;
466
+ }
467
+
468
+ export async function writeHyperframesProject(
469
+ repo,
470
+ scenes,
471
+ outputDirectory,
472
+ palette,
473
+ ) {
474
+ const project = path.join(path.resolve(outputDirectory), "hyperframes");
475
+ await mkdir(project, { recursive: true });
476
+ const files = {
477
+ project,
478
+ index: path.join(project, "index.html"),
479
+ design: path.join(project, "DESIGN.md"),
480
+ config: path.join(project, "hyperframes.json"),
481
+ package: path.join(project, "package.json"),
482
+ gsap: path.join(project, "gsap.min.js"),
483
+ };
484
+
485
+ await Promise.all([
486
+ writeFile(files.index, compositionHtml(repo, scenes, palette)),
487
+ writeFile(files.design, designMarkdown(palette)),
488
+ copyFile(
489
+ fileURLToPath(import.meta.resolve("gsap/dist/gsap.min.js")),
490
+ files.gsap,
491
+ ),
492
+ writeFile(
493
+ files.config,
494
+ `${JSON.stringify({
495
+ $schema: "https://hyperframes.heygen.com/schema/hyperframes.json",
496
+ registry: "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
497
+ paths: {
498
+ blocks: "compositions",
499
+ components: "compositions/components",
500
+ assets: "assets",
501
+ },
502
+ }, null, 2)}\n`,
503
+ ),
504
+ writeFile(
505
+ files.package,
506
+ `${JSON.stringify({
507
+ name: "repotrailer-render",
508
+ private: true,
509
+ scripts: {
510
+ lint: "npx hyperframes lint",
511
+ inspect: "npx hyperframes inspect --samples 18",
512
+ render: "npx hyperframes render --output ../trailer.mp4 --workers 1 --quality standard",
513
+ },
514
+ }, null, 2)}\n`,
515
+ ),
516
+ ]);
517
+
518
+ return files;
519
+ }
520
+
521
+ export async function renderHyperframesProject(
522
+ projectDirectory,
523
+ trailerPath,
524
+ options = {},
525
+ ) {
526
+ const npxArgs = ["--yes", "hyperframes"];
527
+ const lint = await run("npx", [...npxArgs, "lint"], {
528
+ cwd: projectDirectory,
529
+ timeout: 120_000,
530
+ maxBuffer: 16 * 1024 * 1024,
531
+ });
532
+ if (!lint.ok) {
533
+ throw new Error(`HyperFrames lint failed:\n${lint.stdout}\n${lint.stderr}`);
534
+ }
535
+
536
+ const inspect = await run(
537
+ "npx",
538
+ [...npxArgs, "inspect", "--samples", "18", "--strict"],
539
+ {
540
+ cwd: projectDirectory,
541
+ timeout: 180_000,
542
+ maxBuffer: 16 * 1024 * 1024,
543
+ },
544
+ );
545
+ if (!inspect.ok) {
546
+ throw new Error(
547
+ `HyperFrames layout inspection failed:\n${inspect.stdout}\n${inspect.stderr}`,
548
+ );
549
+ }
550
+
551
+ const render = await run(
552
+ "npx",
553
+ [
554
+ ...npxArgs,
555
+ "render",
556
+ "--output",
557
+ trailerPath,
558
+ "--workers",
559
+ String(options.workers ?? 1),
560
+ "--quality",
561
+ options.quality ?? "standard",
562
+ "--strict",
563
+ ],
564
+ {
565
+ cwd: projectDirectory,
566
+ timeout: options.timeout ?? 900_000,
567
+ maxBuffer: 32 * 1024 * 1024,
568
+ },
569
+ );
570
+ if (!render.ok) {
571
+ throw new Error(
572
+ `HyperFrames render failed:\n${render.stdout}\n${render.stderr}`,
573
+ );
574
+ }
575
+
576
+ return {
577
+ trailer: trailerPath,
578
+ lint: lint.stdout,
579
+ inspect: inspect.stdout,
580
+ render: render.stdout,
581
+ };
582
+ }
583
+
584
+ export const __test = {
585
+ compositionHtml,
586
+ designMarkdown,
587
+ titleClass,
588
+ };
package/src/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { analyzeRepository } from "./analyze.js";
2
+ export {
3
+ renderHyperframesProject,
4
+ writeHyperframesProject,
5
+ } from "./hyperframes.js";
6
+ export { buildStoryboard } from "./storyboard.js";
7
+ export { writeLaunchKit } from "./launch-kit.js";